Skip to main content

ling/runtime/
mod.rs

1// src/runtime/mod.rs — tree-walking interpreter with graphics support
2#[cfg(not(target_arch = "wasm32"))]
3mod ai;
4#[cfg(not(target_arch = "wasm32"))]
5mod gamepad;
6#[cfg(target_arch = "wasm32")]
7mod input_web;
8#[cfg(not(target_arch = "wasm32"))]
9pub(crate) mod jit_abi;
10
11/// Initialize the AOT/JIT runtime. Must be called before any AOT-compiled code.
12/// Creates a new interpreter instance for runtime function dispatch.
13#[cfg(not(target_arch = "wasm32"))]
14pub fn init_aot_runtime() {
15    let interp = Interpreter::new();
16    jit_abi::init(interp);
17}
18
19/// Returns seconds since Unix epoch. On wasm32 uses `js_sys::Date::now()`
20/// (milliseconds / 1000); on native uses `SystemTime`.
21pub fn now_secs() -> f64 {
22    #[cfg(target_arch = "wasm32")]
23    {
24        js_sys::Date::now() / 1000.0
25    }
26    #[cfg(not(target_arch = "wasm32"))]
27    {
28        std::time::SystemTime::now()
29            .duration_since(std::time::UNIX_EPOCH)
30            .map(|d| d.as_secs_f64())
31            .unwrap_or(0.0)
32    }
33}
34
35/// Global hue-cycle for wireframe line strokes (enabled via `set_line_hue_cycle`).
36/// Stored as f64 bits: the cycle rate (radians/sec) and the epoch-seconds baseline
37/// captured when it was last (re)enabled. A rate of 0 disables the effect.
38static LINE_HUE_RATE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
39static LINE_HUE_START: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
40
41/// Enable/disable the wireframe line hue-cycle. `rate` is in radians/sec (0 = off).
42pub fn set_line_hue_rate(rate: f64) {
43    LINE_HUE_RATE.store(rate.to_bits(), std::sync::atomic::Ordering::Relaxed);
44    LINE_HUE_START.store(now_secs().to_bits(), std::sync::atomic::Ordering::Relaxed);
45}
46
47/// Current hue phase (radians) for line strokes, or `None` when the cycle is off.
48/// Elapsed is computed in f64 (epoch seconds are huge) before the caller casts to
49/// f32, so `sin` keeps precision across a long session.
50pub fn line_hue_phase() -> Option<f64> {
51    let rate = f64::from_bits(LINE_HUE_RATE.load(std::sync::atomic::Ordering::Relaxed));
52    if rate <= 0.0 {
53        return None;
54    }
55    let start = f64::from_bits(LINE_HUE_START.load(std::sync::atomic::Ordering::Relaxed));
56    Some((now_secs() - start) * rate)
57}
58
59// Wasm-only module registry: seeded by JS before `run_program` is called so
60// that `use "path"` statements resolve without a real filesystem.
61#[cfg(target_arch = "wasm32")]
62thread_local! {
63    static WASM_MODULES: std::cell::RefCell<std::collections::HashMap<String, String>> =
64        std::cell::RefCell::new(std::collections::HashMap::new());
65}
66
67/// Register a module source for wasm32 `use` resolution.
68/// Called from JS via `wasm_bindgen` before `run_program`.
69#[cfg(target_arch = "wasm32")]
70pub fn register_wasm_module(path: &str, source: &str) {
71    WASM_MODULES.with(|m| m.borrow_mut().insert(path.to_string(), source.to_string()));
72}
73
74/// Look up a registered module source on wasm32.
75#[cfg(target_arch = "wasm32")]
76pub(crate) fn get_wasm_module(path: &str) -> Option<String> {
77    WASM_MODULES.with(|m| m.borrow().get(path).cloned())
78}
79
80#[cfg(target_arch = "wasm32")]
81#[inline]
82fn wasm_sleep_ms(ms: i32) {
83    if ms <= 0 {
84        return;
85    }
86
87    // Keep this in Rust/js_sys so wasm-bindgen doesn't emit `require(...)`
88    // snippets, which break worker/no-modules output.
89    let global = js_sys::global();
90    let has_sab = js_sys::Reflect::has(
91        &global,
92        &wasm_bindgen::JsValue::from_str("SharedArrayBuffer"),
93    )
94    .unwrap_or(false);
95    let has_atomics =
96        js_sys::Reflect::has(&global, &wasm_bindgen::JsValue::from_str("Atomics")).unwrap_or(false);
97    if has_sab && has_atomics {
98        let sab = js_sys::SharedArrayBuffer::new(4);
99        let i32a = js_sys::Int32Array::new(&sab);
100        if js_sys::Atomics::wait_with_timeout(&i32a, 0, 0, ms as f64).is_ok() {
101            return;
102        }
103    }
104
105    let end = js_sys::Date::now() + ms as f64;
106    while js_sys::Date::now() < end {}
107}
108
109#[cfg(target_arch = "wasm32")]
110fn wasm_fetch_sync(
111    path: &str,
112    response_type: &str,
113    return_expr: &str,
114) -> Result<wasm_bindgen::JsValue, String> {
115    let quoted = js_sys::JSON::stringify(&wasm_bindgen::JsValue::from_str(path))
116        .ok()
117        .and_then(|s| s.as_string())
118        .unwrap_or_else(|| "\"\"".to_string());
119
120    let script = format!(
121        "(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}})()"
122    );
123
124    js_sys::eval(&script).map_err(|e| {
125        e.as_string()
126            .unwrap_or_else(|| format!("JS eval failed: {:?}", e))
127    })
128}
129
130#[cfg(target_arch = "wasm32")]
131fn wasm_fetch_bytes(path: &str) -> Result<Vec<u8>, String> {
132    let value = wasm_fetch_sync(
133        path,
134        "arraybuffer",
135        "new Uint8Array(xhr.response || new ArrayBuffer(0))",
136    )?;
137    let arr = js_sys::Uint8Array::new(&value);
138    let mut out = vec![0u8; arr.length() as usize];
139    arr.copy_to(&mut out);
140    Ok(out)
141}
142
143#[cfg(target_arch = "wasm32")]
144fn wasm_fetch_text(path: &str) -> Result<String, String> {
145    let value = wasm_fetch_sync(path, "text", "String(xhr.responseText || '')")?;
146    Ok(value.as_string().unwrap_or_default())
147}
148
149#[cfg(not(target_arch = "wasm32"))]
150mod net;
151#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
152pub mod web;
153use crate::gfx::{GfxState, Light};
154use crate::parser::ast::*;
155#[cfg(target_arch = "wasm32")]
156use js_sys;
157use std::cell::RefCell;
158use std::collections::HashMap;
159use std::rc::Rc;
160// `raster` is wasm-safe (pure CPU framebuffer), so `draw_line` is available on
161// web too; `fill_triangle` is only reached from native-gated 3-D fill paths.
162use crate::gfx::raster::draw_line;
163#[cfg(not(target_arch = "wasm32"))]
164use crate::gfx::raster::fill_triangle;
165#[cfg(not(target_arch = "wasm32"))]
166use ling_audio::{AudioEngine, ToneParams, Wave};
167
168#[cfg(not(target_arch = "wasm32"))]
169use ling_audio::FftAnalyzer;
170
171#[cfg(not(target_arch = "wasm32"))]
172use ling_mic;
173
174// ─── Values ──────────────────────────────────────────────────────────────────
175
176#[derive(Debug, Clone)]
177pub enum Value {
178    Str(String),
179    Number(f64),
180    Bool(bool),
181    Unit,
182    List(Rc<Vec<Value>>),
183    Ok(Box<Value>),
184    Err(Box<Value>),
185    Fn(Vec<String>, Vec<Stmt>, Env),
186    /// `form` record instance — ordered named fields.
187    Struct {
188        name: String,
189        fields: Vec<(String, Value)>,
190    },
191    /// `choose` enum instance — variant tag plus ordered payload.
192    Variant {
193        enum_name: String,
194        variant: String,
195        payload: Vec<Value>,
196    },
197}
198
199// Interpreter-hot maps use a fast non-crypto hasher: short Thai identifier keys
200// are hashed on every variable access and builtin dispatch, where SipHash dominates.
201use rustc_hash::FxHashMap;
202type Env = FxHashMap<String, Value>;
203
204#[inline]
205fn new_env() -> Env {
206    FxHashMap::default()
207}
208
209impl std::fmt::Display for Value {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        match self {
212            Value::Str(s) => write!(f, "{s}"),
213            Value::Number(n) => {
214                if n.fract() == 0.0 && n.abs() < 1e15 {
215                    write!(f, "{}", *n as i64)
216                } else {
217                    write!(f, "{n}")
218                }
219            },
220            Value::Bool(b) => write!(f, "{b}"),
221            Value::Unit => write!(f, "()"),
222            Value::List(v) => {
223                write!(f, "[")?;
224                for (i, x) in v.iter().enumerate() {
225                    if i > 0 {
226                        write!(f, ", ")?;
227                    }
228                    write!(f, "{x}")?;
229                }
230                write!(f, "]")
231            },
232            Value::Ok(v) => write!(f, "Ok({v})"),
233            Value::Err(v) => write!(f, "Err({v})"),
234            Value::Fn(_, _, _) => write!(f, "<fn>"),
235            Value::Struct { name, fields } => {
236                write!(f, "{name} {{ ")?;
237                for (i, (k, v)) in fields.iter().enumerate() {
238                    if i > 0 {
239                        write!(f, ", ")?;
240                    }
241                    write!(f, "{k}: {v}")?;
242                }
243                write!(f, " }}")
244            },
245            Value::Variant { variant, payload, .. } => {
246                write!(f, "{variant}")?;
247                if !payload.is_empty() {
248                    write!(f, "(")?;
249                    for (i, v) in payload.iter().enumerate() {
250                        if i > 0 {
251                            write!(f, ", ")?;
252                        }
253                        write!(f, "{v}")?;
254                    }
255                    write!(f, ")")?;
256                }
257                Ok(())
258            },
259        }
260    }
261}
262
263#[cfg(target_arch = "wasm32")]
264fn wasm_unsupported_builtin(name: &str) -> Option<Value> {
265    Some(match name {
266        // interface blips (native-only today)
267        "audio_blip"
268        | "提示音"
269        | "ビープ音"
270        | "효과음"
271        | "เสียงบี๊บ"
272        | "ui_sound"
273        | "界面音"
274        | "UI音"
275        | "인터페이스음"
276        | "เสียงปุ่ม"
277        | "audio_stop_sfx"
278        | "停止音效"
279        | "効果音停止"
280        | "효과음정지"
281        | "หยุดเอฟเฟกต์ทั้งหมด" | "بوق_کوتاه" | "نغمة_قصيرة" | "ביפ" | "بلپ_آواز" => Value::Unit,
282
283        // music loading / analysis / playback / midi (native-only today)
284        "music_load"
285        | "载入音乐"
286        | "音楽読込"
287        | "음악로드"
288        | "โหลดเพลง"
289        | "music_patch"
290        | "乐器音色"
291        | "音色読込"
292        | "악기패치"
293        | "แพตช์เครื่องดนตรี"
294        | "music_lrc"
295        | "载入歌词"
296        | "歌詞読込"
297        | "가사로드"
298        | "โหลดเนื้อเพลง"
299        | "music_midi_load"
300        | "载入MIDI"
301        | "MIDI読込"
302        | "미디로드"
303        | "โหลดมิดี" | "بارگذاری_موسیقی" | "تحميل_الموسيقى" | "טעינת_מוזיקה" | "موسیقی_لوڈ" => Value::Number(-1.0),
304
305        "music_duration"
306        | "音乐时长"
307        | "音楽長さ"
308        | "음악길이"
309        | "ความยาวเพลง"
310        | "music_bpm"
311        | "节拍速度"
312        | "テンポ"
313        | "템포"
314        | "จังหวะต่อนาที"
315        | "music_pos"
316        | "音乐位置"
317        | "音楽位置"
318        | "음악위치"
319        | "ตำแหน่งเพลง"
320        | "music_mic_pitch"
321        | "麦克风音高"
322        | "マイク音程"
323        | "마이크음정"
324        | "ระดับเสียงไมค์"
325        | "music_hz"
326        | "音符频率"
327        | "音符周波数"
328        | "음표주파수"
329        | "ความถี่โน้ต"
330        | "music_pitch_score"
331        | "音准评分"
332        | "音程スコア"
333        | "음정점수"
334        | "คะแนนเสียง"
335        | "music_judge"
336        | "判定"
337        | "判定する"
338        | "판정"
339        | "ตัดสินจังหวะ"
340        | "music_midi_count"
341        | "MIDI数量"
342        | "MIDI数"
343        | "미디수"
344        | "จำนวนมิดี" | "مدت_موسیقی" | "مدة_الموسيقى" | "משך_מוזיקה" | "موسیقی_دورانیہ" => Value::Number(0.0),
345
346        "music_key"
347        | "调性"
348        | "調性"
349        | "조성"
350        | "คีย์เพลง"
351        | "music_lyric"
352        | "当前歌词"
353        | "現在歌詞"
354        | "현재가사"
355        | "เนื้อเพลงปัจจุบัน"
356        | "music_note_name"
357        | "音名"
358        | "音名称"
359        | "음이름"
360        | "ชื่อโน้ต"
361        | "music_grade_name"
362        | "判定名"
363        | "判定名称"
364        | "판정이름"
365        | "ชื่อการตัดสิน" | "گام_موسیقی" | "مقام_الموسيقى" | "סולם_מוזיקלי" | "موسیقی_کلید" => Value::Str(String::new()),
366
367        "music_onsets"
368        | "音符起点"
369        | "オンセット"
370        | "온셋"
371        | "จุดเริ่มเสียง"
372        | "music_beat_grid"
373        | "节拍网格"
374        | "ビートグリッド"
375        | "비트그리드"
376        | "กริดจังหวะ"
377        | "music_midi_notes"
378        | "MIDI音符"
379        | "MIDIノート"
380        | "미디음표"
381        | "โน้ตมิดี"
382        | "music_midi_bars"
383        | "MIDI音条"
384        | "MIDIバー"
385        | "미디바"
386        | "แท่งมิดี"
387        | "music_fft"
388        | "音乐频谱"
389        | "音楽スペクトル"
390        | "음악스펙트럼"
391        | "สเปกตรัมเพลง" | "آغازهای_نت" | "بدايات_النغمات" | "התחלות_תווים" | "نوٹ_شروعات" => Value::List(Vec::new().into()),
392
393        "music_play"
394        | "播放音乐"
395        | "音楽再生"
396        | "음악재생"
397        | "เล่นเพลง"
398        | "music_pause"
399        | "暂停音乐"
400        | "音楽一時停止"
401        | "음악일시정지"
402        | "หยุดเพลงชั่วคราว"
403        | "music_stop"
404        | "停止音乐"
405        | "音楽停止"
406        | "음악정지"
407        | "หยุดเพลง"
408        | "music_seek"
409        | "定位音乐"
410        | "音楽シーク"
411        | "음악탐색"
412        | "ค้นหาเพลง"
413        | "music_volume"
414        | "音乐音量"
415        | "音楽音量"
416        | "음악음량"
417        | "ระดับเพลง"
418        | "music_note"
419        | "弹音符"
420        | "音符演奏"
421        | "음표연주"
422        | "เล่นโน้ต"
423        | "music_note_on"
424        | "音符开始"
425        | "音符オン"
426        | "음표켜기"
427        | "โน้ตเริ่ม"
428        | "music_note_off"
429        | "音符结束"
430        | "音符オフ"
431        | "음표끄기"
432        | "โน้ตจบ" | "پخش_موسیقی" | "شغّل_الموسيقى" | "נגן_מוזיקה" | "موسیقی_چلاؤ" => Value::Unit,
433
434        // liquid sim — return handle 0 for new, Unit for everything else
435        "liquid_new" | "新建液体" | "液体新規" | "액체생성" | "สร้างของเหลว" | "مایع_جدید" | "سائل_جديد" | "נוזל_חדש" | "نیا_مائع" => {
436            Value::Number(0.0)
437        },
438        "liquid_mix" | "液体混合" | "液体混合度" | "액체혼합" | "การผสมของเหลว" | "ترکیب_مایع" | "مزج_سائل" | "ערבוב_נוזל" | "مائع_ملاؤ" => {
439            Value::Number(0.0)
440        },
441        "liquid_set_colors"
442        | "液体颜色"
443        | "液体配色"
444        | "액체색상"
445        | "สีของเหลว"
446        | "liquid_splat"
447        | "液体注入"
448        | "液体追加"
449        | "액체분사"
450        | "หยดของเหลว"
451        | "liquid_gravity"
452        | "液体重力"
453        | "液体重力ベクトル"
454        | "액체중력"
455        | "แรงโน้มถ่วงเหลว"
456        | "liquid_step"
457        | "液体步进"
458        | "液体更新"
459        | "액체스텝"
460        | "ก้าวของเหลว"
461        | "liquid_step_all"
462        | "液体全步进"
463        | "液体全更新"
464        | "전체액체스텝"
465        | "ก้าวของเหลวทั้งหมด"
466        | "liquid_rainbow"
467        | "液体彩虹"
468        | "液体虹"
469        | "액체무지개"
470        | "ของเหลวสายรุ้ง"
471        | "liquid_draw"
472        | "绘制液体"
473        | "液体描画"
474        | "액체그리기"
475        | "วาดของเหลว"
476        | "liquid_draw_surface"
477        | "液体贴面"
478        | "液体曲面"
479        | "액체곡면"
480        | "ของเหลวบนพื้นผิว" | "تنظیم_رنگ_مایع" | "عيّن_ألوان_السائل" | "קבע_צבעי_נוזל" | "مائع_رنگ_مقرر_کرو" => Value::Unit,
481
482        // ── game AI: neural networks ─────────────────────────────────────────
483        "nn_new"
484        | "建神经网"
485        | "ニューラル作成"
486        | "신경망생성"
487        | "สร้างโครงข่าย"
488        | "nn_load"
489        | "载入网"
490        | "網読込"
491        | "신경망불러오기"
492        | "โหลดโครงข่าย" | "شبکه_جدید" | "شبكة_جديدة" | "רשת_חדשה" | "نئی_نیورل_نیٹ" => Value::Number(-1.0),
493        "nn_forward" | "神经前向" | "順伝播" | "순전파" | "ส่งต่อโครงข่าย" | "پیش‌روی_شبکه" | "تمرير_أمامي" | "העברה_קדימה" | "فارورڈ_پاس" => {
494            Value::List(Vec::new().into())
495        },
496        "nn_train"
497        | "训练网"
498        | "ニューラル学習"
499        | "신경망학습"
500        | "ฝึกโครงข่าย"
501        | "nn_dense"
502        | "密集层"
503        | "密層追加"
504        | "밀집층"
505        | "ชั้นหนาแน่น" | "آموزش_شبکه" | "درّب_الشبكة" | "אמן_רשת" | "نیٹ_ٹریننگ" => Value::Number(0.0),
506        "nn_save" | "保存网" | "網保存" | "신경망저장" | "บันทึกโครงข่าย" | "ذخیره_شبکه" | "احفظ_الشبكة" | "שמור_רשת" | "نیٹ_محفوظ_کرو" => {
507            Value::Bool(false)
508        },
509
510        // ── game AI: behavior trees ─────────────────────────────────────────
511        "bt_build" | "建行为树" | "行動木構築" | "행동트리구성" | "สร้างต้นไม้พฤติกรรม" | "ساخت_درخت_رفتار" | "ابنِ_شجرة_السلوك" | "בנה_עץ_התנהגות" | "بی_ٹی_تعمیر" => {
512            Value::Number(-1.0)
513        },
514        "bt_tick" | "行为树滴答" | "行動木更新" | "행동트리틱" | "เดินต้นไม้พฤติกรรม" | "تیک_درخت_رفتار" | "نبضة_شجرة_السلوك" | "טיק_עץ_התנהגות" | "بی_ٹی_ٹک" => {
515            Value::Str(String::new())
516        },
517        "bt_status" | "行为树状态" | "行動木状態" | "행동트리상태" | "สถานะต้นไม้พฤติกรรม" | "وضعیت_درخت_رفتار" | "حالة_شجرة_السلوك" | "סטטוס_עץ_התנהגות" | "بی_ٹی_حالت" => {
518            Value::Number(0.0)
519        },
520        "bt_set" | "设事实" | "事実設定" | "사실설정" | "ตั้งข้อเท็จจริง" | "تنظیم_واقعیت" | "عيّن_حقيقة" | "קבע_עובדה" | "بی_ٹی_سیٹ" => {
521            Value::Unit
522        },
523
524        // ── game AI: dialog LLM ─────────────────────────────────────────────
525        "dialog_new"
526        | "建对话模型"
527        | "対話モデル作成"
528        | "대화모델생성"
529        | "สร้างโมเดลสนทนา"
530        | "dialog_load_model"
531        | "对话载模"
532        | "対話モデル読込"
533        | "대화모델불러오기"
534        | "โหลดโมเดลสนทนา"
535        | "dialog_train"
536        | "对话训练"
537        | "対話訓練"
538        | "대화훈련"
539        | "ฝึกสนทนา"
540        | "dialog_load"
541        | "对话载入"
542        | "対話読込"
543        | "대화불러오기"
544        | "โหลดชุดสนทนา" | "مدل_گفتگوی_جدید" | "نموذج_حوار_جديد" | "מודל_דיאלוג_חדש" | "نیا_مکالمہ_ماڈل" => Value::Number(-1.0),
545        "dialog_say" | "对话生成" | "対話生成" | "대화생성" | "พูดสนทนา" | "بگو" | "قل" | "אמור" | "کہو" => {
546            Value::Str(String::new())
547        },
548        "dialog_save" | "对话存模" | "対話モデル保存" | "대화모델저장" | "บันทึกโมเดลสนทนา" | "ذخیره_مدل_گفتگو" | "احفظ_نموذج_الحوار" | "שמור_מודל_דיאלוג" | "مکالمہ_ماڈل_محفوظ" => {
549            Value::Bool(false)
550        },
551        "dialog_learn" | "对话学习" | "対話学習" | "대화학습" | "เรียนรู้สนทนา" | "یادگیری_گفتگو" | "تعلّم_الحوار" | "למד_דיאלוג" | "مکالمہ_سیکھو" => {
552            Value::Unit
553        },
554
555        // ── networking ──────────────────────────────────────────────────────
556        "net_connect"
557        | "联网"
558        | "ネット接続"
559        | "네트연결"
560        | "เชื่อมเน็ต"
561        | "net_listen"
562        | "监听"
563        | "待機"
564        | "리슨"
565        | "รอรับ"
566        | "net_send"
567        | "发送"
568        | "送信"
569        | "전송"
570        | "ส่ง" | "اتصال_شبکه" | "اتصل_بالشبكة" | "התחבר_לרשת" | "نیٹ_کنیکٹ" => Value::Number(-1.0),
571        "net_recv"
572        | "接收"
573        | "受信"
574        | "수신"
575        | "รับ"
576        | "net_status"
577        | "连接状态"
578        | "接続状態"
579        | "연결상태"
580        | "สถานะการเชื่อม" | "دریافت_شبکه" | "استقبل_من_الشبكة" | "קבל_מרשת" | "نیٹ_وصول" => Value::Str(String::new()),
581        "net_discover" | "发现" | "探索" | "검색" | "ค้นหาเครือข่าย" | "کشف_شبکه" | "اكتشف_الشبكة" | "גלה_רשת" | "نیٹ_دریافت" => {
582            Value::List(Vec::new().into())
583        },
584        "net_close"
585        | "断开"
586        | "切断"
587        | "연결종료"
588        | "ตัดเชื่อม"
589        | "net_test"
590        | "测连接"
591        | "接続テスト"
592        | "연결테스트"
593        | "ทดสอบเน็ต" | "بستن_شبکه" | "أغلق_الشبكة" | "סגור_רשת" | "نیٹ_بند" => Value::Number(0.0),
594
595        // catch-all: any other native-only builtin silently no-ops on wasm32
596        _ => Value::Unit,
597    })
598}
599
600// ─── Control flow ────────────────────────────────────────────────────────────
601
602#[derive(Debug)]
603pub(crate) enum EvalErr {
604    Runtime(String),
605    Return(Value),
606    #[allow(dead_code)] // reserved for future `break` statement support
607    Break,
608}
609
610impl From<String> for EvalErr {
611    fn from(s: String) -> Self {
612        EvalErr::Runtime(s)
613    }
614}
615
616type EvalResult = Result<Value, EvalErr>;
617
618/// RFC 4648 base32 encode (no padding) — used for TOTP secrets.
619#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
620fn base32_encode(data: &[u8]) -> String {
621    const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
622    let mut out = String::new();
623    let mut buffer: u32 = 0;
624    let mut bits = 0u32;
625    for &b in data {
626        buffer = (buffer << 8) | b as u32;
627        bits += 8;
628        while bits >= 5 {
629            bits -= 5;
630            out.push(ALPHABET[((buffer >> bits) & 0x1f) as usize] as char);
631        }
632    }
633    if bits > 0 {
634        out.push(ALPHABET[((buffer << (5 - bits)) & 0x1f) as usize] as char);
635    }
636    out
637}
638
639/// RFC 4648 base32 decode (case-insensitive, ignores padding/whitespace).
640#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
641fn base32_decode(s: &str) -> Option<Vec<u8>> {
642    let mut buffer: u32 = 0;
643    let mut bits = 0u32;
644    let mut out = Vec::new();
645    for c in s.chars() {
646        if c == '=' || c.is_whitespace() {
647            continue;
648        }
649        let v = match c.to_ascii_uppercase() {
650            'A'..='Z' => c.to_ascii_uppercase() as u32 - 'A' as u32,
651            '2'..='7' => c as u32 - '2' as u32 + 26,
652            _ => return None,
653        };
654        buffer = (buffer << 5) | v;
655        bits += 5;
656        if bits >= 8 {
657            bits -= 8;
658            out.push((buffer >> bits) as u8);
659        }
660    }
661    Some(out)
662}
663
664/// One RFC 6238 TOTP code (HMAC-SHA1, 6 digits) for the given 30s time step.
665#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
666fn totp_code(secret_b32: &str, step: u64) -> Option<String> {
667    let key = base32_decode(secret_b32)?;
668    let msg = step.to_be_bytes();
669    let mac = hmac_sha1(&key, &msg);
670    let offset = (mac[19] & 0x0f) as usize;
671    let bin = ((mac[offset] as u32 & 0x7f) << 24)
672        | ((mac[offset + 1] as u32) << 16)
673        | ((mac[offset + 2] as u32) << 8)
674        | (mac[offset + 3] as u32);
675    Some(format!("{:06}", bin % 1_000_000))
676}
677
678/// TOTP verify with a ±1 step window (tolerates minor clock skew).
679#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
680fn totp_check(secret_b32: &str, code: &str) -> bool {
681    if code.len() != 6 || !code.bytes().all(|b| b.is_ascii_digit()) {
682        return false;
683    }
684    let now_step = (crate::runtime::now_secs() as u64) / 30;
685    for delta in [-1i64, 0, 1] {
686        let step = (now_step as i64 + delta) as u64;
687        if let Some(expected) = totp_code(secret_b32, step) {
688            // constant-time-ish compare (fixed 6-char length)
689            if expected.as_bytes().ct_eq_str(code.as_bytes()) {
690                return true;
691            }
692        }
693    }
694    false
695}
696
697/// HMAC-SHA1 built on the `hmac`+`sha1` crates (both via the web feature).
698#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
699fn hmac_sha1(key: &[u8], msg: &[u8]) -> [u8; 20] {
700    use hmac::{Mac, SimpleHmac};
701    let mut mac = SimpleHmac::<sha1::Sha1>::new_from_slice(key).expect("hmac key");
702    mac.update(msg);
703    let out = mac.finalize().into_bytes();
704    let mut arr = [0u8; 20];
705    arr.copy_from_slice(&out);
706    arr
707}
708
709/// Tiny fixed-length constant-time byte compare helper for TOTP codes.
710#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
711trait CtEqStr {
712    fn ct_eq_str(&self, other: &[u8]) -> bool;
713}
714#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
715impl CtEqStr for [u8] {
716    fn ct_eq_str(&self, other: &[u8]) -> bool {
717        if self.len() != other.len() {
718            return false;
719        }
720        let mut diff = 0u8;
721        for (a, b) in self.iter().zip(other.iter()) {
722            diff |= a ^ b;
723        }
724        diff == 0
725    }
726}
727
728/// Percent-decodes a URL query component (`+` → space, `%41` → `A`).
729fn url_decode(s: &str) -> String {
730    let bytes = s.as_bytes();
731    let mut out = Vec::with_capacity(bytes.len());
732    let mut i = 0;
733    while i < bytes.len() {
734        match bytes[i] {
735            b'+' => {
736                out.push(b' ');
737                i += 1;
738            },
739            b'%' if i + 2 < bytes.len() => {
740                let hi = (bytes[i + 1] as char).to_digit(16);
741                let lo = (bytes[i + 2] as char).to_digit(16);
742                if let (Some(h), Some(l)) = (hi, lo) {
743                    out.push((h * 16 + l) as u8);
744                    i += 3;
745                } else {
746                    out.push(bytes[i]);
747                    i += 1;
748                }
749            },
750            b => {
751                out.push(b);
752                i += 1;
753            },
754        }
755    }
756    String::from_utf8_lossy(&out).into_owned()
757}
758
759/// Maps Ling values to owned rusqlite parameter values for `db_exec`/`db_query`.
760#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
761fn values_to_sql_params(args: &[Value]) -> Vec<ling_http::rusqlite::types::Value> {
762    use ling_http::rusqlite::types::Value as Sql;
763    args.iter()
764        .map(|v| match v {
765            Value::Number(n) if n.fract() == 0.0 && n.abs() < 9e15 => Sql::Integer(*n as i64),
766            Value::Number(n) => Sql::Real(*n),
767            Value::Bool(b) => Sql::Integer(*b as i64),
768            other => Sql::Text(other.to_string()),
769        })
770        .collect()
771}
772
773// GfxState is now defined in crate::gfx — see src/gfx/mod.rs.
774
775// ─── SVG writer ───────────────────────────────────────────────────────────────
776
777struct SvgWriter {
778    path: String,
779    width: f64,
780    height: f64,
781    elements: Vec<String>,
782}
783
784impl SvgWriter {
785    fn new(path: String, width: f64, height: f64) -> Self {
786        let bg = format!("<rect width=\"{width}\" height=\"{height}\" fill=\"#0a0a0a\"/>");
787        Self { path, width, height, elements: vec![bg] }
788    }
789
790    fn save(&self) -> std::io::Result<()> {
791        let w = self.width;
792        let h = self.height;
793        let mut out = format!(
794            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
795             <svg xmlns=\"http://www.w3.org/2000/svg\" \
796             width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\">\n"
797        );
798        for elem in &self.elements {
799            out.push_str("  ");
800            out.push_str(elem);
801            out.push('\n');
802        }
803        out.push_str("</svg>\n");
804        // Create parent directory if it doesn't exist.
805        if let Some(parent) = std::path::Path::new(&self.path).parent() {
806            if !parent.as_os_str().is_empty() {
807                let _ = std::fs::create_dir_all(parent);
808            }
809        }
810        std::fs::write(&self.path, out.as_bytes())
811    }
812}
813
814fn hsl_to_hex(h: f64, s: f64, l: f64) -> String {
815    let s = s / 100.0;
816    let l = l / 100.0;
817    let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
818    let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
819    let m = l - c / 2.0;
820    let (r1, g1, b1) = if h < 60.0 {
821        (c, x, 0.0)
822    } else if h < 120.0 {
823        (x, c, 0.0)
824    } else if h < 180.0 {
825        (0.0, c, x)
826    } else if h < 240.0 {
827        (0.0, x, c)
828    } else if h < 300.0 {
829        (x, 0.0, c)
830    } else {
831        (c, 0.0, x)
832    };
833    let r = ((r1 + m) * 255.0).round() as u8;
834    let g = ((g1 + m) * 255.0).round() as u8;
835    let b = ((b1 + m) * 255.0).round() as u8;
836    format!("#{r:02x}{g:02x}{b:02x}")
837}
838
839// ─── Procedural texture helpers ───────────────────────────────────────────────
840
841fn tex_hash(x: i32, y: i32, seed: u32) -> f32 {
842    let mut h = (x as u32)
843        .wrapping_add((y as u32).wrapping_mul(2654435769))
844        .wrapping_add(seed.wrapping_mul(1234567891));
845    h ^= h >> 16;
846    h = h.wrapping_mul(0x45d9f3b);
847    h ^= h >> 16;
848    h as f32 / u32::MAX as f32
849}
850
851fn tex_vnoise(x: f32, y: f32, seed: u32) -> f32 {
852    let xi = x.floor() as i32;
853    let yi = y.floor() as i32;
854    let sm = |t: f32| t * t * (3.0 - 2.0 * t);
855    let xf = sm(x - xi as f32);
856    let yf = sm(y - yi as f32);
857    let a = tex_hash(xi, yi, seed);
858    let b = tex_hash(xi + 1, yi, seed);
859    let c = tex_hash(xi, yi + 1, seed);
860    let d = tex_hash(xi + 1, yi + 1, seed);
861    a + (b - a) * xf + (c - a) * yf + (a - b - c + d) * xf * yf
862}
863
864fn tex_fbm(x: f32, y: f32, octaves: u32, seed: u32) -> f32 {
865    let mut v = 0.0f32;
866    let mut amp = 0.5f32;
867    let mut f = 1.0f32;
868    for i in 0..octaves {
869        v += tex_vnoise(x * f, y * f, seed.wrapping_add(i * 7919)) * amp;
870        amp *= 0.5;
871        f *= 2.0;
872    }
873    v
874}
875
876fn tex_palette(name: &str, t: f32) -> [f32; 3] {
877    let (a, b, c, d): ([f32; 3], [f32; 3], [f32; 3], [f32; 3]) = match name {
878        "fire" => (
879            [0.8, 0.4, 0.1],
880            [0.7, 0.3, 0.1],
881            [1.0, 0.5, 0.3],
882            [0.0, 0.5, 0.8],
883        ),
884        "ocean" => (
885            [0.1, 0.4, 0.7],
886            [0.3, 0.3, 0.4],
887            [0.8, 1.0, 0.5],
888            [0.3, 0.0, 0.6],
889        ),
890        "psychedelic" => (
891            [0.5, 0.5, 0.5],
892            [0.8, 0.8, 0.8],
893            [1.0, 1.3, 0.7],
894            [0.0, 0.15, 0.3],
895        ),
896        "neon" => (
897            [0.5, 0.5, 0.5],
898            [0.5, 0.5, 0.5],
899            [2.0, 1.0, 0.0],
900            [0.5, 0.2, 0.25],
901        ),
902        "forest" => (
903            [0.3, 0.5, 0.2],
904            [0.2, 0.3, 0.1],
905            [1.0, 0.5, 0.8],
906            [0.1, 0.3, 0.6],
907        ),
908        _ => (
909            [0.5, 0.5, 0.5],
910            [0.5, 0.5, 0.5],
911            [1.0, 1.0, 1.0],
912            [0.0, 0.333, 0.667],
913        ),
914    };
915    [0, 1, 2]
916        .map(|i| (a[i] + b[i] * (std::f32::consts::TAU * (c[i] * t + d[i])).cos()).clamp(0.0, 1.0))
917}
918
919/// Map a physical key to a typed character for ling-ui text input (lowercase).
920#[cfg(not(target_arch = "wasm32"))]
921// Full US-QWERTY keyboard → char, shift-aware. `key_char` only ever emits
922// printable ASCII (never Tab/Enter/control bytes — those keys have no case
923// here at all) so anything read through `text_poll` is safe by construction
924// to drop straight into the game's tab/comma/semicolon/pipe-framed wire
925// protocols without needing per-keystroke sanitization.
926fn key_char(k: minifb::Key, shift: bool) -> Option<char> {
927    use minifb::Key::*;
928    let base = match k {
929        A => 'a',
930        B => 'b',
931        C => 'c',
932        D => 'd',
933        E => 'e',
934        F => 'f',
935        G => 'g',
936        H => 'h',
937        I => 'i',
938        J => 'j',
939        K => 'k',
940        L => 'l',
941        M => 'm',
942        N => 'n',
943        O => 'o',
944        P => 'p',
945        Q => 'q',
946        R => 'r',
947        S => 's',
948        T => 't',
949        U => 'u',
950        V => 'v',
951        W => 'w',
952        X => 'x',
953        Y => 'y',
954        Z => 'z',
955        Key0 => '0',
956        Key1 => '1',
957        Key2 => '2',
958        Key3 => '3',
959        Key4 => '4',
960        Key5 => '5',
961        Key6 => '6',
962        Key7 => '7',
963        Key8 => '8',
964        Key9 => '9',
965        Space => ' ',
966        Minus => '-',
967        Equal => '=',
968        Period => '.',
969        Comma => ',',
970        Slash => '/',
971        Backslash => '\\',
972        Semicolon => ';',
973        Apostrophe => '\'',
974        LeftBracket => '[',
975        RightBracket => ']',
976        Backquote => '`',
977        _ => return None,
978    };
979    if !shift {
980        return Some(base);
981    }
982    Some(match base {
983        'a'..='z' => base.to_ascii_uppercase(),
984        '0' => ')',
985        '1' => '!',
986        '2' => '@',
987        '3' => '#',
988        '4' => '$',
989        '5' => '%',
990        '6' => '^',
991        '7' => '&',
992        '8' => '*',
993        '9' => '(',
994        '-' => '_',
995        '=' => '+',
996        '.' => '>',
997        ',' => '<',
998        '/' => '?',
999        '\\' => '|',
1000        ';' => ':',
1001        '\'' => '"',
1002        '[' => '{',
1003        ']' => '}',
1004        '`' => '~',
1005        other => other,
1006    })
1007}
1008
1009/// Win32 virtual-key code → char, mirroring `key_char` exactly but keyed by
1010/// VK code instead of `minifb::Key` — used by the `GetAsyncKeyState` input
1011/// path (see `os_key_down` / the `topmost_window` fallback in `text_poll`).
1012#[cfg(all(not(target_arch = "wasm32"), windows))]
1013fn vk_char(vk: i32, shift: bool) -> Option<char> {
1014    let base = match vk {
1015        0x41..=0x5A => (b'a' + (vk - 0x41) as u8) as char, // 'A'..'Z'
1016        0x30..=0x39 => (b'0' + (vk - 0x30) as u8) as char, // '0'..'9'
1017        0x20 => ' ',  // VK_SPACE
1018        0xBD => '-',  // VK_OEM_MINUS
1019        0xBB => '=',  // VK_OEM_PLUS (unshifted '=')
1020        0xBE => '.',  // VK_OEM_PERIOD
1021        0xBC => ',',  // VK_OEM_COMMA
1022        0xBF => '/',  // VK_OEM_2
1023        0xDC => '\\', // VK_OEM_5
1024        0xBA => ';',  // VK_OEM_1
1025        0xDE => '\'', // VK_OEM_7
1026        0xDB => '[',  // VK_OEM_4
1027        0xDD => ']',  // VK_OEM_6
1028        0xC0 => '`',  // VK_OEM_3
1029        _ => return None,
1030    };
1031    if !shift {
1032        return Some(base);
1033    }
1034    Some(match base {
1035        'a'..='z' => base.to_ascii_uppercase(),
1036        '0' => ')',
1037        '1' => '!',
1038        '2' => '@',
1039        '3' => '#',
1040        '4' => '$',
1041        '5' => '%',
1042        '6' => '^',
1043        '7' => '&',
1044        '8' => '*',
1045        '9' => '(',
1046        '-' => '_',
1047        '=' => '+',
1048        '.' => '>',
1049        ',' => '<',
1050        '/' => '?',
1051        '\\' => '|',
1052        ';' => ':',
1053        '\'' => '"',
1054        '[' => '{',
1055        ']' => '}',
1056        '`' => '~',
1057        other => other,
1058    })
1059}
1060
1061/// The VK codes `text_poll`'s `GetAsyncKeyState` fallback scans each frame —
1062/// every key `vk_char` can turn into a character.
1063#[cfg(all(not(target_arch = "wasm32"), windows))]
1064const TEXT_POLL_VKS: &[i32] = &[
1065    0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
1066    0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x30, 0x31, 0x32, 0x33,
1067    0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x20, 0xBD, 0xBB, 0xBE, 0xBC, 0xBF, 0xDC, 0xBA, 0xDE,
1068    0xDB, 0xDD, 0xC0,
1069];
1070/// VK_BACK — polled separately from `TEXT_POLL_VKS` since it edits (pops)
1071/// rather than appends.
1072#[cfg(all(not(target_arch = "wasm32"), windows))]
1073const VK_BACK: i32 = 0x08;
1074#[cfg(all(not(target_arch = "wasm32"), windows))]
1075const VK_SHIFT: i32 = 0x10;
1076
1077/// Read the OS's live key-state table directly — unlike minifb's
1078/// `is_key_down`/`get_keys_pressed` (populated from `WM_KEYDOWN`, which only
1079/// arrives at a window holding real Win32 keyboard focus), this works even
1080/// when the borderless-fullscreen window is topmost/visually in front but
1081/// didn't actually win the OS focus fight (Windows' foreground-lock — see
1082/// `force_window_focus`). High bit of `GetAsyncKeyState` = currently down.
1083#[cfg(all(not(target_arch = "wasm32"), windows))]
1084fn os_key_down(vk: i32) -> bool {
1085    unsafe {
1086        extern "system" {
1087            fn GetAsyncKeyState(vkey: i32) -> i16;
1088        }
1089        (GetAsyncKeyState(vk) as u16 & 0x8000) != 0
1090    }
1091}
1092
1093/// True when `hwnd` is the actual OS foreground window. `GetAsyncKeyState`
1094/// (see `os_key_down`) reads the global key-state table regardless of which
1095/// window is focused, so the `topmost_window` input fallback must check this
1096/// before trusting it — otherwise alt-tabbing away from the topmost/
1097/// fullscreen window to type in some other app would still feed keystrokes
1098/// into the game sitting behind it, since nothing about GetAsyncKeyState
1099/// itself would notice the window lost focus.
1100#[cfg(all(not(target_arch = "wasm32"), windows))]
1101fn window_is_foreground(hwnd: isize) -> bool {
1102    unsafe {
1103        extern "system" {
1104            fn GetForegroundWindow() -> isize;
1105        }
1106        hwnd != 0 && GetForegroundWindow() == hwnd
1107    }
1108}
1109
1110/// Delay (seconds) a key must stay held before it starts auto-repeating —
1111/// matches the initial "hesitation" of a normal OS text field.
1112#[cfg(all(not(target_arch = "wasm32"), windows))]
1113const KEY_REPEAT_DELAY: f64 = 0.45;
1114/// Interval (seconds) between repeats once a held key is auto-repeating.
1115#[cfg(all(not(target_arch = "wasm32"), windows))]
1116const KEY_REPEAT_RATE: f64 = 0.045;
1117
1118/// Edge/repeat detector for the `GetAsyncKeyState` text-input fallback:
1119/// fires true on the initial press, then again every `KEY_REPEAT_RATE`
1120/// seconds once the key has been held past `KEY_REPEAT_DELAY` — the same
1121/// press-then-hold-to-repeat behavior minifb's `KeyRepeat::Yes` gives the
1122/// normal (focused-window) path, which this fallback otherwise lacks since
1123/// it does its own from-scratch edge detection per VK code.
1124#[cfg(all(not(target_arch = "wasm32"), windows))]
1125fn key_repeat_fire(now: f64, down: bool, was_down: bool, down_since: &mut f64, last_fire: &mut f64) -> bool {
1126    if !down {
1127        return false;
1128    }
1129    if !was_down {
1130        *down_since = now;
1131        *last_fire = now;
1132        return true;
1133    }
1134    if now - *down_since >= KEY_REPEAT_DELAY && now - *last_fire >= KEY_REPEAT_RATE {
1135        *last_fire = now;
1136        return true;
1137    }
1138    false
1139}
1140
1141/// Map the same key-name strings `key_down`/`key_pressed` already accept
1142/// (see `str_to_minifb_key`) to Win32 virtual-key codes, for the
1143/// `GetAsyncKeyState` fallback path.
1144#[cfg(all(not(target_arch = "wasm32"), windows))]
1145fn str_to_vk(name: &str) -> Option<i32> {
1146    Some(match name {
1147        "numpad0" | "kp0" => 0x60,
1148        "numpad1" | "kp1" => 0x61,
1149        "numpad2" | "kp2" => 0x62,
1150        "numpad3" | "kp3" => 0x63,
1151        "numpad4" | "kp4" => 0x64,
1152        "numpad5" | "kp5" => 0x65,
1153        "numpad6" | "kp6" => 0x66,
1154        "numpad7" | "kp7" => 0x67,
1155        "numpad8" | "kp8" => 0x68,
1156        "numpad9" | "kp9" => 0x69,
1157        "numpad+" | "kp+" => 0x6B,
1158        "numpad-" | "kp-" => 0x6D,
1159        "numpad*" | "kp*" => 0x6A,
1160        "numpad/" | "kp/" => 0x6F,
1161        "left" => 0x25,
1162        "up" => 0x26,
1163        "right" => 0x27,
1164        "down" => 0x28,
1165        "space" => 0x20,
1166        "enter" => 0x0D,
1167        "escape" => 0x1B,
1168        "pageup" => 0x21,
1169        "pagedown" => 0x22,
1170        "lshift" | "leftshift" => 0xA0,
1171        "rshift" | "rightshift" => 0xA1,
1172        "lctrl" | "leftctrl" => 0xA2,
1173        "rctrl" | "rightctrl" => 0xA3,
1174        "lalt" | "leftalt" => 0xA4,
1175        "ralt" | "rightalt" => 0xA5,
1176        "tab" => 0x09,
1177        "backspace" => VK_BACK,
1178        "delete" => 0x2E,
1179        "insert" => 0x2D,
1180        "home" => 0x24,
1181        "end" => 0x23,
1182        "a" => 0x41,
1183        "b" => 0x42,
1184        "c" => 0x43,
1185        "d" => 0x44,
1186        "e" => 0x45,
1187        "f" => 0x46,
1188        "g" => 0x47,
1189        "h" => 0x48,
1190        "i" => 0x49,
1191        "j" => 0x4A,
1192        "k" => 0x4B,
1193        "l" => 0x4C,
1194        "m" => 0x4D,
1195        "n" => 0x4E,
1196        "o" => 0x4F,
1197        "p" => 0x50,
1198        "q" => 0x51,
1199        "r" => 0x52,
1200        "s" => 0x53,
1201        "t" => 0x54,
1202        "u" => 0x55,
1203        "v" => 0x56,
1204        "w" => 0x57,
1205        "x" => 0x58,
1206        "y" => 0x59,
1207        "z" => 0x5A,
1208        "0" => 0x30,
1209        "1" => 0x31,
1210        "2" => 0x32,
1211        "3" => 0x33,
1212        "4" => 0x34,
1213        "5" => 0x35,
1214        "6" => 0x36,
1215        "7" => 0x37,
1216        "8" => 0x38,
1217        "9" => 0x39,
1218        _ => return None,
1219    })
1220}
1221
1222/// Lowercase-hex encode bytes (the wire format for crypto values in Ling).
1223fn hex_encode(bytes: &[u8]) -> String {
1224    let mut s = String::with_capacity(bytes.len() * 2);
1225    for b in bytes {
1226        s.push_str(&format!("{b:02x}"));
1227    }
1228    s
1229}
1230
1231/// Decode a `ling convert` blob: base64 → zlib-inflate → raw little-endian bytes.
1232#[cfg(not(target_arch = "wasm32"))]
1233fn decode_blob(s: &str) -> Result<Vec<u8>, String> {
1234    use base64::Engine as _;
1235    use std::io::Read as _;
1236    let comp = base64::engine::general_purpose::STANDARD
1237        .decode(s.trim())
1238        .map_err(|e| format!("base64: {e}"))?;
1239    let mut out = Vec::new();
1240    flate2::read::ZlibDecoder::new(&comp[..])
1241        .read_to_end(&mut out)
1242        .map_err(|e| format!("inflate: {e}"))?;
1243    Ok(out)
1244}
1245
1246/// Decode a lowercase/uppercase hex string to bytes (ignores malformed tail).
1247#[cfg(not(target_arch = "wasm32"))]
1248fn hex_decode(s: &str) -> Vec<u8> {
1249    let s = s.trim();
1250    (0..s.len() / 2)
1251        .filter_map(|i| u8::from_str_radix(s.get(i * 2..i * 2 + 2)?, 16).ok())
1252        .collect()
1253}
1254
1255/// Decode a hex string into a fixed 32-byte key (zero-padded / truncated).
1256#[cfg(not(target_arch = "wasm32"))]
1257fn hex_to_32(s: &str) -> [u8; 32] {
1258    let v = hex_decode(s);
1259    let mut out = [0u8; 32];
1260    let n = v.len().min(32);
1261    out[..n].copy_from_slice(&v[..n]);
1262    out
1263}
1264
1265/// Builds a minimal, valid PDF with one page per input image (decoded via
1266/// `image`, page sized to the image's own pixel dimensions), backing the
1267/// `pdf_from_images` builtin. No PDF crate: each page is three objects
1268/// (Page / Contents / Image XObject) hand-written directly, with the image
1269/// stream flate-compressed raw RGB8 (`/Filter /FlateDecode /ColorSpace
1270/// /DeviceRGB /BitsPerComponent 8`) — no separate re-encoding step beyond
1271/// what `flate2` (already a dependency) does for the stream itself.
1272#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1273fn build_pdf_from_images(paths: &[String], out_path: &str) -> Result<(), Box<dyn std::error::Error>> {
1274    use std::io::Write as _;
1275
1276    struct PageImg {
1277        w: u32,
1278        h: u32,
1279        compressed: Vec<u8>,
1280    }
1281
1282    let mut pages = Vec::with_capacity(paths.len());
1283    for p in paths {
1284        let img = image::open(p)?.to_rgb8();
1285        let (w, h) = (img.width(), img.height());
1286        let mut enc = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
1287        enc.write_all(img.as_raw())?;
1288        pages.push(PageImg { w, h, compressed: enc.finish()? });
1289    }
1290
1291    let n = pages.len();
1292    // Object numbers: 1=Catalog, 2=Pages, then per page i (0-indexed):
1293    // 3+3i=Page, 4+3i=Contents, 5+3i=Image XObject.
1294    let page_nums: Vec<u32> = (0..n).map(|i| 3 + (i as u32) * 3).collect();
1295    let total_objs = 2 + n * 3;
1296    let mut off = vec![0usize; total_objs + 1]; // 1-based; off[0] unused
1297
1298    let mut buf: Vec<u8> = Vec::new();
1299    buf.extend_from_slice(b"%PDF-1.4\n%\xE2\xE3\xCF\xD3\n");
1300
1301    off[1] = buf.len();
1302    buf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
1303
1304    off[2] = buf.len();
1305    let kids = page_nums.iter().map(|n| format!("{n} 0 R")).collect::<Vec<_>>().join(" ");
1306    buf.extend_from_slice(format!("2 0 obj\n<< /Type /Pages /Kids [{kids}] /Count {n} >>\nendobj\n").as_bytes());
1307
1308    for (i, page) in pages.iter().enumerate() {
1309        let page_num = page_nums[i];
1310        let content_num = page_num + 1;
1311        let image_num = page_num + 2;
1312        let (w, h) = (page.w, page.h);
1313
1314        off[page_num as usize] = buf.len();
1315        buf.extend_from_slice(format!(
1316            "{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"
1317        ).as_bytes());
1318
1319        let content = format!("q {w} 0 0 {h} 0 0 cm /Im0 Do Q");
1320        off[content_num as usize] = buf.len();
1321        buf.extend_from_slice(format!(
1322            "{content_num} 0 obj\n<< /Length {} >>\nstream\n{content}\nendstream\nendobj\n",
1323            content.len()
1324        ).as_bytes());
1325
1326        off[image_num as usize] = buf.len();
1327        buf.extend_from_slice(format!(
1328            "{image_num} 0 obj\n<< /Type /XObject /Subtype /Image /Width {w} /Height {h} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode /Length {} >>\nstream\n",
1329            page.compressed.len()
1330        ).as_bytes());
1331        buf.extend_from_slice(&page.compressed);
1332        buf.extend_from_slice(b"\nendstream\nendobj\n");
1333    }
1334
1335    let xref_offset = buf.len();
1336    buf.extend_from_slice(format!("xref\n0 {}\n", total_objs + 1).as_bytes());
1337    buf.extend_from_slice(b"0000000000 65535 f \n");
1338    for entry in off.iter().skip(1) {
1339        buf.extend_from_slice(format!("{entry:010} 00000 n \n").as_bytes());
1340    }
1341    buf.extend_from_slice(
1342        format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref_offset}\n%%EOF", total_objs + 1).as_bytes(),
1343    );
1344
1345    std::fs::write(out_path, buf)?;
1346    Ok(())
1347}
1348
1349fn tex_rgb(r: f32, g: f32, b: f32) -> u32 {
1350    ((r * 255.0) as u32) << 16 | ((g * 255.0) as u32) << 8 | (b * 255.0) as u32
1351}
1352
1353// ─── 3D Perlin Noise (Improved Perlin 2002) ───────────────────────────────────
1354
1355const PERM: [u8; 512] = [
1356    151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69,
1357    142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219,
1358    203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 35, 63, 189, 114, 56, 42, 123,
1359    165, 38, 72, 93, 69, 139, 138, 78, 149, 159, 56, 89, 152, 78, 61, 140, 63, 26, 142, 76, 124,
1360    132, 72, 11, 90, 44, 82, 59, 96, 41, 148, 126, 157, 13, 49, 27, 176, 33, 47, 14, 97, 78, 71,
1361    40, 87, 183, 4, 122, 92, 7, 72, 3, 246, 17, 225, 87, 91, 106, 203, 190, 57, 74, 76, 88, 207,
1362    208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146,
1363    157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196,
1364    167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94,
1365    11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55,
1366    109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, 186, 120, 37, 46, 28, 166, 180,
1367    198, 232, 221, 116, 31, 75, 189, 139, 138, 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185,
1368    134, 193, 29, 158, 225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223,
1369    140, 161, 137, 13, 191, 230, 66, 104, 153, 199, 167, 147, 99, 179, 92,
1370    // Duplicate for wrap-around indexing
1371    151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69,
1372    142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219,
1373    203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 35, 63, 189, 114, 56, 42, 123,
1374    165, 38, 72, 93, 69, 139, 138, 78, 149, 159, 56, 89, 152, 78, 61, 140, 63, 26, 142, 76, 124,
1375    132, 72, 11, 90, 44, 82, 59, 96, 41, 148, 126, 157, 13, 49, 27, 176, 33, 47, 14, 97, 78, 71,
1376    40, 87, 183, 4, 122, 92, 7, 72, 3, 246, 17, 225, 87, 91, 106, 203, 190, 57, 74, 76, 88, 207,
1377    208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146,
1378    157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196,
1379    167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94,
1380    11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55,
1381    109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174,
1382];
1383
1384fn fade(t: f32) -> f32 {
1385    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
1386}
1387
1388fn grad(hash: u8, x: f32, y: f32, z: f32) -> f32 {
1389    let h = hash & 15;
1390    let u = if h < 8 { x } else { y };
1391    let v = if h < 8 { y } else { z };
1392    (if (h & 1) == 0 { u } else { -u }) + (if (h & 2) == 0 { v } else { -v })
1393}
1394
1395fn perlin3(x: f32, y: f32, z: f32) -> f32 {
1396    let xi = (x.floor() as i32) & 255;
1397    let yi = (y.floor() as i32) & 255;
1398    let zi = (z.floor() as i32) & 255;
1399
1400    let xf = x - x.floor();
1401    let yf = y - y.floor();
1402    let zf = z - z.floor();
1403
1404    let u = fade(xf);
1405    let v = fade(yf);
1406    let w = fade(zf);
1407
1408    let p0 = PERM[xi as usize] as usize;
1409    let p1 = PERM[((xi + 1) & 255) as usize] as usize;
1410    let pa = PERM[(p0 + yi as usize) & 255] as usize;
1411    let pb = PERM[(p0 + ((yi + 1) & 255) as usize) & 255] as usize;
1412    let pc = PERM[(p1 + yi as usize) & 255] as usize;
1413    let pd = PERM[(p1 + ((yi + 1) & 255) as usize) & 255] as usize;
1414
1415    let g000 = grad(PERM[(pa + zi as usize) & 255], xf, yf, zf);
1416    let g001 = grad(
1417        PERM[(pa + ((zi + 1) & 255) as usize) & 255],
1418        xf,
1419        yf,
1420        zf - 1.0,
1421    );
1422    let g010 = grad(PERM[(pb + zi as usize) & 255], xf, yf - 1.0, zf);
1423    let g011 = grad(
1424        PERM[(pb + ((zi + 1) & 255) as usize) & 255],
1425        xf,
1426        yf - 1.0,
1427        zf - 1.0,
1428    );
1429    let g100 = grad(PERM[(pc + zi as usize) & 255], xf - 1.0, yf, zf);
1430    let g101 = grad(
1431        PERM[(pc + ((zi + 1) & 255) as usize) & 255],
1432        xf - 1.0,
1433        yf,
1434        zf - 1.0,
1435    );
1436    let g110 = grad(PERM[(pd + zi as usize) & 255], xf - 1.0, yf - 1.0, zf);
1437    let g111 = grad(
1438        PERM[(pd + ((zi + 1) & 255) as usize) & 255],
1439        xf - 1.0,
1440        yf - 1.0,
1441        zf - 1.0,
1442    );
1443
1444    let l00 = g000 + u * (g100 - g000);
1445    let l01 = g001 + u * (g101 - g001);
1446    let l10 = g010 + u * (g110 - g010);
1447    let l11 = g011 + u * (g111 - g011);
1448
1449    let l0 = l00 + v * (l10 - l00);
1450    let l1 = l01 + v * (l11 - l01);
1451
1452    l0 + w * (l1 - l0)
1453}
1454
1455fn fast_rand_f64(state: &mut u64) -> f64 {
1456    *state = state
1457        .wrapping_mul(6364136223846793005)
1458        .wrapping_add(1442695040888963407);
1459    ((*state >> 32) as u32) as f64 / 4294967296.0
1460}
1461
1462// ─── Circle Drawing Primitives ────────────────────────────────────────────────
1463
1464/// Write one pixel into the framebuffer (normal or additive blend).
1465#[inline]
1466fn put_px(buf: &mut [u32], idx: usize, color: u32, blend: u8) {
1467    if idx >= buf.len() {
1468        return;
1469    }
1470    if blend == 0 {
1471        buf[idx] = color;
1472    } else {
1473        let old = buf[idx];
1474        let r = (((old >> 16) & 255) + ((color >> 16) & 255)).min(255);
1475        let g = (((old >> 8) & 255) + ((color >> 8) & 255)).min(255);
1476        let b = ((old & 255) + (color & 255)).min(255);
1477        buf[idx] = (r << 16) | (g << 8) | b;
1478    }
1479}
1480
1481/// Pack three float colour channels (0..255) into a 0x00RRGGBB word, clamping.
1482#[inline]
1483fn rgb(r: f64, g: f64, b: f64) -> u32 {
1484    let r = (r as i64).clamp(0, 255) as u32;
1485    let g = (g as i64).clamp(0, 255) as u32;
1486    let b = (b as i64).clamp(0, 255) as u32;
1487    (r << 16) | (g << 8) | b
1488}
1489
1490#[allow(clippy::too_many_arguments)]
1491fn draw_circle_outline(
1492    buf: &mut [u32],
1493    w: i32,
1494    h: i32,
1495    cx: i32,
1496    cy: i32,
1497    r: i32,
1498    color: u32,
1499    blend: u8,
1500) {
1501    let r = r.clamp(0, 20000); // guard against overflow / runaway from tiny depths
1502    if r == 0 {
1503        return;
1504    }
1505    let mut x = 0;
1506    let mut y = r;
1507    let mut d = 3 - 2 * r;
1508    while x <= y {
1509        plot_circle_points(buf, w, h, cx, cy, x, y, color, blend);
1510        if d < 0 {
1511            d += 4 * x + 6;
1512        } else {
1513            d += 4 * (x - y) + 10;
1514            y -= 1;
1515        }
1516        x += 1;
1517    }
1518}
1519
1520#[allow(clippy::too_many_arguments)]
1521fn plot_circle_points(
1522    buf: &mut [u32],
1523    w: i32,
1524    h: i32,
1525    cx: i32,
1526    cy: i32,
1527    x: i32,
1528    y: i32,
1529    color: u32,
1530    blend: u8,
1531) {
1532    let points = [
1533        (cx + x, cy + y),
1534        (cx - x, cy + y),
1535        (cx + x, cy - y),
1536        (cx - x, cy - y),
1537        (cx + y, cy + x),
1538        (cx - y, cy + x),
1539        (cx + y, cy - x),
1540        (cx - y, cy - x),
1541    ];
1542    for &(px, py) in &points {
1543        if px >= 0 && px < w && py >= 0 && py < h {
1544            put_px(buf, (py * w + px) as usize, color, blend);
1545        }
1546    }
1547}
1548
1549#[allow(clippy::too_many_arguments)]
1550fn draw_circle_filled(
1551    buf: &mut [u32],
1552    w: i32,
1553    h: i32,
1554    cx: i32,
1555    cy: i32,
1556    r: i32,
1557    color: u32,
1558    blend: u8,
1559) {
1560    if r <= 0 {
1561        return;
1562    }
1563    for dy in -r..=r {
1564        let dx_max = ((r * r - dy * dy) as f64).sqrt() as i32;
1565        let py = cy + dy;
1566        if py < 0 || py >= h {
1567            continue;
1568        }
1569        for dx in -dx_max..=dx_max {
1570            let px = cx + dx;
1571            if px >= 0 && px < w {
1572                put_px(buf, (py * w + px) as usize, color, blend);
1573            }
1574        }
1575    }
1576}
1577
1578#[cfg(test)]
1579mod draw_tests {
1580    use super::*;
1581
1582    #[test]
1583    fn filled_circle_actually_writes_pixels() {
1584        let mut buf = vec![0u32; 100 * 100];
1585        draw_circle_filled(&mut buf, 100, 100, 50, 50, 10, 0xFF00FF, 0);
1586        assert_eq!(buf[50 * 100 + 50], 0xFF00FF, "centre pixel must be filled");
1587        assert_eq!(buf[0], 0, "far corner must stay clear");
1588        let n = buf.iter().filter(|&&p| p != 0).count();
1589        assert!(n > 200 && n < 500, "r=10 disc area ≈ 314, got {n}");
1590    }
1591
1592    #[test]
1593    fn circle_outline_writes_a_ring() {
1594        let mut buf = vec![0u32; 100 * 100];
1595        draw_circle_outline(&mut buf, 100, 100, 50, 50, 20, 0x00FF00, 0);
1596        assert_eq!(buf[50 * 100 + 50], 0, "outline must NOT fill the centre");
1597        assert!(
1598            buf.iter().any(|&p| p == 0x00FF00),
1599            "outline must draw a ring"
1600        );
1601    }
1602
1603    #[test]
1604    fn additive_blend_accumulates_channels() {
1605        let mut buf = vec![0x202020u32; 1];
1606        put_px(&mut buf, 0, 0x404040, 1);
1607        assert_eq!(buf[0], 0x606060);
1608    }
1609}
1610
1611// ─── Interpreter ─────────────────────────────────────────────────────────────
1612
1613/// Customizable colour palette for the vector UI toolkit (packed 0x00RRGGBB).
1614/// `ui_theme(...)` sets it; every widget falls back to these and accepts a
1615/// trailing `r,g,b` override.
1616#[derive(Clone, Copy)]
1617pub struct UiTheme {
1618    pub primary: u32,
1619    pub accent: u32,
1620    pub track: u32,
1621    pub warn: u32,
1622    pub text: u32,
1623    pub bg: u32,
1624}
1625
1626impl Default for UiTheme {
1627    fn default() -> Self {
1628        Self {
1629            primary: 0x00D2FF, // holographic cyan
1630            accent: 0x28FFB4,  // mint
1631            track: 0x2C3E64,   // dim slate
1632            warn: 0xFF5A5A,    // alert red
1633            text: 0xBEEBFF,    // pale cyan
1634            bg: 0x0A1018,      // near-black panel
1635        }
1636    }
1637}
1638
1639#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
1640pub struct Interpreter {
1641    globals: HashMap<String, Expr>,
1642    /// Globals evaluated ONCE at program start (immutable after load).
1643    /// call_named clones this instead of re-evaluating every global per call.
1644    global_seed: Env,
1645    functions: FxHashMap<String, Rc<FnDef>>,
1646    /// `form` definitions: struct name → ordered field names.
1647    pub(crate) structs: HashMap<String, Vec<String>>,
1648    /// `choose` variants: variant name (bare and `Enum::Variant`) → (enum name, arity).
1649    enum_variants: HashMap<String, (String, usize)>,
1650    _modules: HashMap<String, Vec<FnDef>>,
1651    gfx: RefCell<GfxState>,
1652    svg: RefCell<Option<SvgWriter>>,
1653    /// Directory of the primary source file, for relative `use` resolution.
1654    pub source_dir: Option<std::path::PathBuf>,
1655    /// Files already loaded — prevents circular imports.
1656    loaded_files: std::collections::HashSet<String>,
1657    /// Optional audio engine — `None` if no audio device is available.
1658    #[cfg(not(target_arch = "wasm32"))]
1659    audio: Option<AudioEngine>,
1660    #[cfg(not(target_arch = "wasm32"))]
1661    fft: RefCell<FftAnalyzer>,
1662    fft_bands_cache: RefCell<Vec<f32>>,
1663    /// Real-time clock — seconds since Unix epoch at startup (f64 works on both
1664    /// native and wasm32; Instant is not available on wasm32).
1665    start_time_secs: f64,
1666    /// Frame counter — incremented at each present()
1667    frame_num: u64,
1668    /// Target framerate used to pace `present()` on wasm32.
1669    #[cfg(target_arch = "wasm32")]
1670    wasm_target_fps: f64,
1671    /// Next frame deadline (ms since epoch) for wasm frame pacing.
1672    #[cfg(target_arch = "wasm32")]
1673    wasm_next_present_ms: f64,
1674    /// Random state for rand() builtin (xorshift)
1675    rand_state: u64,
1676    /// Microphone input (Phase 1 audio reactivity)
1677    #[cfg(not(target_arch = "wasm32"))]
1678    mic: Option<ling_mic::MicInput>,
1679    /// Persistent KEM keypairs (knot / hybrid identities), referenced by handle.
1680    #[cfg(not(target_arch = "wasm32"))]
1681    crypto_ids: Vec<ling_crypto::KnotIdentity>,
1682    /// Persistent Ed25519 signing keypairs, referenced by handle.
1683    #[cfg(not(target_arch = "wasm32"))]
1684    ed25519_ids: Vec<ling_crypto::Ed25519Keypair>,
1685    /// Editable text-input buffer (ling-ui text fields).
1686    text_buffer: String,
1687    /// Frame counter for record_frame().
1688    record_n: u32,
1689    /// Accumulated microphone samples (for turning sound into crypto donuts).
1690    #[cfg(not(target_arch = "wasm32"))]
1691    mic_buffer: Vec<f32>,
1692    /// Loaded vector UI fonts, referenced by handle (index) from `font_load`.
1693    #[cfg(not(target_arch = "wasm32"))]
1694    fonts: Vec<ling_graphics::VectorFont>,
1695    /// Loaded raster images (PNG/etc.), referenced by handle (index) from
1696    /// `image_load` — read pixel-by-pixel via `image_pixel_r/g/b/a`.
1697    images: Vec<image::RgbaImage>,
1698    /// Customizable UI colour palette (set via `ui_theme`).
1699    ui_theme: UiTheme,
1700    /// Left-mouse state on the previous frame — for widget click-edge detection.
1701    mouse_was_down: bool,
1702    /// Live music engine (decode playback + GM synth) — lazily initialised.
1703    #[cfg(not(target_arch = "wasm32"))]
1704    music: Option<ling_music::MusicEngine>,
1705    #[cfg(not(target_arch = "wasm32"))]
1706    music_init: bool,
1707    /// Decoded tracks (for analysis + playback), by `music_load` handle.
1708    tracks: Vec<ling_music::DecodedAudio>,
1709    /// Parsed `.lrc` lyrics, by `music_lrc` handle.
1710    lyrics: Vec<ling_music::Lyrics>,
1711    /// Parsed MIDI songs, by `music_midi_load` handle.
1712    midis: Vec<ling_music::MidiSong>,
1713    /// Soft bodies (deformable balls), by `soft_ball` handle.
1714    soft_bodies: Vec<ling_physics::soft::SoftBody>,
1715    /// Rigid-body world (angular dynamics), shared by `rb_*`.
1716    rigid_world: ling_physics::rigid::PhysicsWorld,
1717    /// Liquid grids (water/oil), by `liquid_new` handle.
1718    liquids: Vec<ling_physics::liquid::LiquidGrid>,
1719    meshes: Vec<crate::gfx::shapes::ColorMesh>,
1720    /// Loaded glTF models (skeleton + skin weights + animations), by `mesh_load` handle.
1721    gltf_models: std::cell::RefCell<Vec<ling_physics::gltf::GltfModel>>,
1722    /// Active cinematic dialog box (Ocarina/Majora-style), if any.
1723    dialog: Option<ling_game::dialog::Dialog>,
1724    /// Dialog highlight colours by role: text, name, place, item (0x00RRGGBB).
1725    dialog_colors: [u32; 4],
1726    /// Active user-function call frames (names), for error tracebacks.
1727    frames: Vec<String>,
1728    /// Snapshot of `frames` captured the moment a runtime error first arose
1729    /// (the deepest call). Consumed by `take_error_trace`.
1730    error_trace: Option<Vec<String>>,
1731    /// Unified input (gamepads/joysticks/VR/touch via the ling-input
1732    /// "Sensorium"). Lazily initialised on the first `pad_*` builtin call;
1733    /// `None` if no native input backend is available.
1734    #[cfg(not(target_arch = "wasm32"))]
1735    input: RefCell<Option<InputState>>,
1736    /// Routes registered by `http_route(method, path, handler)`, consumed
1737    /// by `http_serve`. Lives here (not a global, unlike `net`) because the
1738    /// handler is a `Value::Fn` closure — `Value` holds `Rc` and so can't
1739    /// safely live in a `static`.
1740    #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1741    http_routes: Vec<(String, String, Value)>,
1742    /// SQLite handle opened by `db_open` — plain rusqlite Connection, no
1743    /// pool: the interpreter is single-threaded, so one connection is both
1744    /// sufficient and contention-free.
1745    #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1746    db: Option<ling_http::rusqlite::Connection>,
1747    /// `(url_prefix, disk_dir)` pairs registered by `http_static`, consumed by
1748    /// `http_serve` — served as raw bytes, bypassing the String-only Value bridge.
1749    #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1750    http_static_dirs: Vec<(String, String)>,
1751    /// Background jobs started by `http_post_async`, polled by `http_job_poll`.
1752    /// A plain `Arc<Mutex<..>>` handle (not `Value`), so it's fine to touch from
1753    /// the background tokio task that fills in each job's result.
1754    #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1755    async_jobs: web::AsyncJobs,
1756}
1757
1758/// Live gamepad input state: a ling-input hub fed by the native `gilrs` backend.
1759#[cfg(not(target_arch = "wasm32"))]
1760struct InputState {
1761    sensorium: ling_input::Sensorium,
1762    backend: ling_input::backend::GilrsBackend,
1763}
1764
1765impl Default for Interpreter {
1766    fn default() -> Self {
1767        Self::new()
1768    }
1769}
1770
1771impl Interpreter {
1772    pub fn new() -> Self {
1773        #[cfg(not(target_arch = "wasm32"))]
1774        let audio = AudioEngine::new()
1775            .map_err(|e| eprintln!("audio init failed (no sound): {e}"))
1776            .ok();
1777        Self {
1778            globals: HashMap::new(),
1779            global_seed: new_env(),
1780            functions: FxHashMap::default(),
1781            structs: HashMap::new(),
1782            enum_variants: HashMap::new(),
1783            _modules: HashMap::new(),
1784            gfx: RefCell::new(GfxState::new()),
1785            svg: RefCell::new(None),
1786            source_dir: None,
1787            loaded_files: std::collections::HashSet::new(),
1788            #[cfg(not(target_arch = "wasm32"))]
1789            audio,
1790            #[cfg(not(target_arch = "wasm32"))]
1791            fft: RefCell::new(FftAnalyzer::new(2048, 44100)),
1792            fft_bands_cache: RefCell::new(vec![]),
1793            start_time_secs: crate::runtime::now_secs(),
1794            frame_num: 0,
1795            #[cfg(target_arch = "wasm32")]
1796            wasm_target_fps: 60.0,
1797            #[cfg(target_arch = "wasm32")]
1798            wasm_next_present_ms: 0.0,
1799            rand_state: 0x123456789ABCDEF,
1800            #[cfg(not(target_arch = "wasm32"))]
1801            mic: None,
1802            #[cfg(not(target_arch = "wasm32"))]
1803            crypto_ids: Vec::new(),
1804            #[cfg(not(target_arch = "wasm32"))]
1805            ed25519_ids: Vec::new(),
1806            text_buffer: String::new(),
1807            record_n: 0,
1808            #[cfg(not(target_arch = "wasm32"))]
1809            mic_buffer: Vec::new(),
1810            #[cfg(not(target_arch = "wasm32"))]
1811            fonts: Vec::new(),
1812            images: Vec::new(),
1813            ui_theme: UiTheme::default(),
1814            mouse_was_down: false,
1815            #[cfg(not(target_arch = "wasm32"))]
1816            music: None,
1817            #[cfg(not(target_arch = "wasm32"))]
1818            music_init: false,
1819            tracks: Vec::new(),
1820            lyrics: Vec::new(),
1821            midis: Vec::new(),
1822            soft_bodies: Vec::new(),
1823            rigid_world: ling_physics::rigid::PhysicsWorld::new(),
1824            liquids: Vec::new(),
1825            meshes: Vec::new(),
1826            gltf_models: std::cell::RefCell::new(Vec::new()),
1827            dialog: None,
1828            dialog_colors: [0xE6F2FF, 0xFFD24A, 0x4AD2FF, 0x6CFF8C], // text · name · place · item
1829            frames: Vec::new(),
1830            error_trace: None,
1831            #[cfg(not(target_arch = "wasm32"))]
1832            input: RefCell::new(None),
1833            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1834            http_routes: Vec::new(),
1835            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1836            db: None,
1837            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1838            http_static_dirs: Vec::new(),
1839            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1840            async_jobs: web::AsyncJobs::new(),
1841        }
1842    }
1843
1844    /// Lazily initialise the input system and advance it one frame; returns the
1845    /// number of connected gamepads. Call once per game-loop iteration (like a
1846    /// window update) before reading `pad_*` state.
1847    #[cfg(not(target_arch = "wasm32"))]
1848    fn pad_poll(&self) -> usize {
1849        let mut slot = self.input.borrow_mut();
1850        if slot.is_none() {
1851            match ling_input::backend::GilrsBackend::new() {
1852                Ok(backend) => {
1853                    *slot = Some(InputState { sensorium: ling_input::Sensorium::new(4), backend });
1854                },
1855                Err(_) => return 0,
1856            }
1857        }
1858        let st = slot.as_mut().unwrap();
1859        st.sensorium.begin_frame();
1860        st.sensorium.pump(&mut st.backend);
1861        st.sensorium.update(1.0 / 60.0);
1862        st.sensorium.devices.count()
1863    }
1864
1865    /// Read player `slot`'s gamepad with `f`, or return `default` if there is no
1866    /// input system / no such pad.
1867    #[cfg(not(target_arch = "wasm32"))]
1868    fn with_pad<T>(&self, slot: usize, default: T, f: impl FnOnce(&ling_input::Gamepad) -> T) -> T {
1869        let inp = self.input.borrow();
1870        match inp.as_ref().and_then(|s| s.sensorium.player(slot)) {
1871            Some(p) => f(p),
1872            None => default,
1873        }
1874    }
1875
1876    /// Take the call-stack snapshot captured at the deepest runtime error, if any.
1877    /// Frames are ordered outermost-first (entry point first, failing call last).
1878    pub fn take_error_trace(&mut self) -> Vec<String> {
1879        self.error_trace.take().unwrap_or_default()
1880    }
1881
1882    #[cfg(target_arch = "wasm32")]
1883    fn wasm_pace_frame(&mut self) {
1884        let fps = self.wasm_target_fps.max(1.0);
1885        let frame_ms = 1000.0 / fps;
1886        let now = js_sys::Date::now();
1887        if self.wasm_next_present_ms <= 0.0 {
1888            self.wasm_next_present_ms = now + frame_ms;
1889            return;
1890        }
1891
1892        let wait_ms = (self.wasm_next_present_ms - now).floor() as i32;
1893        if wait_ms > 0 {
1894            wasm_sleep_ms(wait_ms);
1895        }
1896
1897        let after = js_sys::Date::now();
1898        if after > self.wasm_next_present_ms + frame_ms * 3.0 {
1899            self.wasm_next_present_ms = after + frame_ms;
1900        } else {
1901            self.wasm_next_present_ms += frame_ms;
1902        }
1903    }
1904
1905    /// Run `body`, recording `name` as a call frame and snapshotting the stack on
1906    /// the first runtime error so a traceback can be reported.
1907    fn framed<T, F>(&mut self, name: &str, body: F) -> Result<T, EvalErr>
1908    where
1909        F: FnOnce(&mut Self) -> Result<T, EvalErr>,
1910    {
1911        self.frames.push(name.to_string());
1912        let result = body(self);
1913        if matches!(result, Err(EvalErr::Runtime(_))) && self.error_trace.is_none() {
1914            self.error_trace = Some(self.frames.clone());
1915        }
1916        self.frames.pop();
1917        result
1918    }
1919
1920    /// Render the active dialog box: beveled frame + dark fill, then the visible
1921    /// (typewriter-revealed) text word-wrapped with colour-coded runs, plus a
1922    /// blinking advance arrow once the page is fully typed.
1923    #[cfg(not(target_arch = "wasm32"))]
1924    fn render_dialog(&mut self, x: f32, y: f32, w: f32, h: f32, font: i64, t: f32) {
1925        let (runs, typing) = match &self.dialog {
1926            Some(d) if !d.is_closed() => {
1927                let runs: Vec<(String, usize, bool)> = d
1928                    .visible_runs()
1929                    .into_iter()
1930                    .map(|r| (r.text, r.role.index(), r.newline_before))
1931                    .collect();
1932                (runs, d.is_typing())
1933            },
1934            _ => return,
1935        };
1936        let colors = self.dialog_colors;
1937        // ── frame + fill ──
1938        let b = 12.0;
1939        let corners: Vec<[f32; 2]> = vec![
1940            [x + b, y],
1941            [x + w - b, y],
1942            [x + w, y + b],
1943            [x + w, y + h - b],
1944            [x + w - b, y + h],
1945            [x + b, y + h],
1946            [x, y + h - b],
1947            [x, y + b],
1948            [x + b, y],
1949        ];
1950        {
1951            let mut gfx = self.gfx.borrow_mut();
1952            let (bw, bh) = (gfx.width, gfx.height);
1953            crate::gfx::raster::fill_contours_aa(
1954                &mut gfx.buffer,
1955                bw,
1956                bh,
1957                0x0A1018,
1958                false,
1959                std::slice::from_ref(&corners),
1960            );
1961            for seg in corners.windows(2) {
1962                crate::gfx::raster::draw_line_aa(
1963                    &mut gfx.buffer,
1964                    bw,
1965                    bh,
1966                    0x00D2FF,
1967                    false,
1968                    seg[0][0],
1969                    seg[0][1],
1970                    seg[1][0],
1971                    seg[1][1],
1972                );
1973            }
1974        }
1975        // ── word-wrapped, colour-coded text ──
1976        let px = 22.0f32;
1977        let pad = 20.0f32;
1978        let line_h = px * 1.45;
1979        let mut cx = x + pad;
1980        let mut cy = y + pad;
1981        let use_font = font >= 0 && (font as usize) < self.fonts.len();
1982        for (text, role, nl) in &runs {
1983            if *nl {
1984                cx = x + pad;
1985                cy += line_h;
1986            }
1987            for word in text.split_inclusive(' ') {
1988                let wpx = if use_font {
1989                    self.fonts[font as usize].measure(word, px)
1990                } else {
1991                    ling_ui::holo::text_width(word, px * 0.6, px * 0.24)
1992                };
1993                if cx + wpx > x + w - pad && cx > x + pad + 1.0 {
1994                    cx = x + pad;
1995                    cy += line_h;
1996                }
1997                if cy + line_h > y + h {
1998                    break;
1999                }
2000                let col = colors[(*role).min(3)];
2001                if use_font {
2002                    let glyphs = self.font_layout_2d_glyphs(font as usize, cx, cy, px, word);
2003                    let mut gfx = self.gfx.borrow_mut();
2004                    let (bw, bh, add) = (gfx.width, gfx.height, gfx.blend == 1);
2005                    for contours in &glyphs {
2006                        crate::gfx::raster::fill_contours_aa(
2007                            &mut gfx.buffer,
2008                            bw,
2009                            bh,
2010                            col,
2011                            add,
2012                            contours,
2013                        );
2014                    }
2015                } else {
2016                    let segs = ling_ui::holo::text_lines(word, cx, cy, px * 0.6, px, px * 0.24);
2017                    let mut gfx = self.gfx.borrow_mut();
2018                    let (bw, bh) = (gfx.width, gfx.height);
2019                    for s in segs {
2020                        draw_line(&mut gfx.buffer, bw, bh, col, s[0], s[1], s[2], s[3]);
2021                    }
2022                }
2023                cx += wpx;
2024            }
2025        }
2026        // ── blinking advance arrow when fully typed ──
2027        if !typing && (t * 3.0).sin() > 0.0 {
2028            let ax = x + w - 26.0;
2029            let ay = y + h - 22.0;
2030            let mut gfx = self.gfx.borrow_mut();
2031            let (bw, bh) = (gfx.width, gfx.height);
2032            crate::gfx::raster::fill_contours_aa(
2033                &mut gfx.buffer,
2034                bw,
2035                bh,
2036                0x00D2FF,
2037                false,
2038                std::slice::from_ref(&vec![
2039                    [ax - 7.0, ay],
2040                    [ax + 7.0, ay],
2041                    [ax, ay + 9.0],
2042                    [ax - 7.0, ay],
2043                ]),
2044            );
2045        }
2046    }
2047
2048    /// Lazily start the music engine on first use (playback/synth need a device;
2049    /// analysis/decoding do not). Returns `false` if no audio device is available.
2050    #[cfg(not(target_arch = "wasm32"))]
2051    fn ensure_music(&mut self) -> bool {
2052        if self.music.is_some() {
2053            return true;
2054        }
2055        if self.music_init {
2056            return false;
2057        }
2058        self.music_init = true;
2059        match ling_music::MusicEngine::new() {
2060            Ok(m) => {
2061                self.music = Some(m);
2062                true
2063            },
2064            Err(e) => {
2065                eprintln!("music engine init failed (no music playback): {e}");
2066                false
2067            },
2068        }
2069    }
2070
2071    #[cfg(target_arch = "wasm32")]
2072    fn wasm_resolve_source_path(&self, path: &str) -> String {
2073        let p = path.trim();
2074        if p.is_empty() {
2075            return String::new();
2076        }
2077        if p.contains("://") || p.starts_with('/') || p.starts_with("./") || p.starts_with("../") {
2078            return p.to_string();
2079        }
2080        if let Some(d) = &self.source_dir {
2081            let base = d.to_string_lossy().replace('\\', "/");
2082            if !base.is_empty() {
2083                return format!(
2084                    "{}/{}",
2085                    base.trim_end_matches('/'),
2086                    p.trim_start_matches("./")
2087                );
2088            }
2089        }
2090        p.to_string()
2091    }
2092
2093    #[cfg(target_arch = "wasm32")]
2094    fn wasm_music_builtin(&mut self, name: &str, args: &[Value]) -> Result<Option<Value>, EvalErr> {
2095        match name {
2096            // music_load(path) -> track handle (decode from fetched bytes)
2097            "music_load" | "载入音乐" | "音楽読込" | "음악로드" | "โหลดเพลง" | "بارگذاری_موسیقی" | "تحميل_الموسيقى" | "טעינת_מוזיקה" | "موسیقی_لوڈ" | "charger_musique" | "musik_laden" | "загрузить_музыку" =>
2098            {
2099                let path = self.arg_str(args, 0, "");
2100                let resolved = self.wasm_resolve_source_path(&path);
2101                match wasm_fetch_bytes(&resolved)
2102                    .and_then(|bytes| ling_music::from_bytes(&bytes).map_err(|e| e.to_string()))
2103                {
2104                    Ok(t) => {
2105                        let id = self.tracks.len();
2106                        self.tracks.push(t);
2107                        return Ok(Some(Value::Number(id as f64)));
2108                    },
2109                    Err(e) => {
2110                        eprintln!("music_load failed ({path}): {e}");
2111                        return Ok(Some(Value::Number(-1.0)));
2112                    },
2113                }
2114            },
2115            "music_duration" | "音乐时长" | "音楽長さ" | "음악길이" | "ความยาวเพลง" | "مدت_موسیقی" | "مدة_الموسيقى" | "משך_מוזיקה" | "موسیقی_دورانیہ" | "durée_musique" | "musik_dauer" | "длительность_музыки" =>
2116            {
2117                let id = self.arg_num(args, 0, 0.0)? as i64;
2118                let d = self
2119                    .tracks
2120                    .get(id as usize)
2121                    .map(|t| t.duration)
2122                    .unwrap_or(0.0);
2123                return Ok(Some(Value::Number(d as f64)));
2124            },
2125            "music_bpm" | "节拍速度" | "テンポ" | "템포" | "จังหวะต่อนาที" | "ضربان_در_دقیقه" | "نبضات_بالدقيقة" | "פעימות_לדקה" | "بی_پی_ایم" | "bpm_musique" | "musik_bpm" | "музыка_bpm" =>
2126            {
2127                let id = self.arg_num(args, 0, 0.0)? as i64;
2128                let b = self
2129                    .tracks
2130                    .get(id as usize)
2131                    .map(|t| ling_music::analysis::bpm(&t.mono, t.rate))
2132                    .unwrap_or(0.0);
2133                return Ok(Some(Value::Number(b as f64)));
2134            },
2135            "music_key" | "调性" | "調性" | "조성" | "คีย์เพลง" | "گام_موسیقی" | "مقام_الموسيقى" | "סולם_מוזיקלי" | "موسیقی_کلید" | "tonalité_musique" | "musik_tonart" | "тональность_музыки" => {
2136                let id = self.arg_num(args, 0, 0.0)? as i64;
2137                let k = self
2138                    .tracks
2139                    .get(id as usize)
2140                    .map(|t| ling_music::analysis::key_name(&t.mono, t.rate))
2141                    .unwrap_or_default();
2142                return Ok(Some(Value::Str(k)));
2143            },
2144            "music_onsets" | "音符起点" | "オンセット" | "온셋" | "จุดเริ่มเสียง" | "آغازهای_نت" | "بدايات_النغمات" | "התחלות_תווים" | "نوٹ_شروعات" | "attaques_musique" | "musik_einsätze" | "атаки_музыки" =>
2145            {
2146                let id = self.arg_num(args, 0, 0.0)? as i64;
2147                let v = self
2148                    .tracks
2149                    .get(id as usize)
2150                    .map(|t| ling_music::analysis::onsets(&t.mono, t.rate))
2151                    .unwrap_or_default();
2152                return Ok(Some(Value::List(
2153                    v.into_iter().map(|x| Value::Number(x as f64)).collect::<Vec<_>>().into(),
2154                )));
2155            },
2156            "music_beat_grid" | "节拍网格" | "ビートグリッド" | "비트그리드" | "กริดจังหวะ" | "شبکه_ضرب" | "شبكة_الإيقاع" | "רשת_פעימות" | "بیٹ_گرڈ" | "grille_temps_musique" | "musik_taktraster" | "сетка_ритма_музыки" =>
2157            {
2158                let id = self.arg_num(args, 0, 0.0)? as i64;
2159                let beats = self
2160                    .tracks
2161                    .get(id as usize)
2162                    .map(|t| {
2163                        let b = ling_music::analysis::bpm(&t.mono, t.rate);
2164                        ling_music::analysis::beat_grid(&t.mono, t.rate, b)
2165                    })
2166                    .unwrap_or_default();
2167                return Ok(Some(Value::List(
2168                    beats.into_iter().map(|x| Value::Number(x as f64)).collect::<Vec<_>>().into(),
2169                )));
2170            },
2171            "music_lrc" | "载入歌词" | "歌詞読込" | "가사로드" | "โหลดเนื้อเพลง" | "بارگذاری_متن_ترانه" | "تحميل_كلمات_الأغنية" | "טעינת_מילות_שיר" | "گیت_متن_لوڈ" | "lrc_musique" | "musik_lrc" | "lrc_музыки" =>
2172            {
2173                let path = self.arg_str(args, 0, "");
2174                let resolved = self.wasm_resolve_source_path(&path);
2175                match wasm_fetch_text(&resolved) {
2176                    Ok(text) => {
2177                        let id = self.lyrics.len();
2178                        self.lyrics.push(ling_music::Lyrics::parse(&text));
2179                        return Ok(Some(Value::Number(id as f64)));
2180                    },
2181                    Err(e) => {
2182                        eprintln!("music_lrc failed ({path}): {e}");
2183                        return Ok(Some(Value::Number(-1.0)));
2184                    },
2185                }
2186            },
2187            "music_lyric" | "当前歌词" | "現在歌詞" | "현재가사" | "เนื้อเพลงปัจจุบัน" | "متن_ترانه_فعلی" | "كلمات_الأغنية_الحالية" | "מילות_שיר_נוכחיות" | "موجودہ_گیت_متن" | "paroles_musique" | "musik_liedtext" | "текст_песни" =>
2188            {
2189                let id = self.arg_num(args, 0, 0.0)? as i64;
2190                let t = self.arg_num(args, 1, 0.0)? as f32;
2191                let line = self
2192                    .lyrics
2193                    .get(id as usize)
2194                    .map(|l| l.line_at(t).to_string())
2195                    .unwrap_or_default();
2196                return Ok(Some(Value::Str(line)));
2197            },
2198            "music_midi_load" | "载入MIDI" | "MIDI読込" | "미디로드" | "โหลดมิดี" | "بارگذاری_MIDI" | "تحميل_MIDI" | "טעינת_MIDI" | "MIDI_لوڈ" | "charger_midi_musique" | "musik_midi_laden" | "загрузить_midi_музыки" =>
2199            {
2200                let path = self.arg_str(args, 0, "");
2201                let resolved = self.wasm_resolve_source_path(&path);
2202                match wasm_fetch_bytes(&resolved).and_then(|bytes| {
2203                    ling_music::midi::from_bytes(&bytes).map_err(|e| e.to_string())
2204                }) {
2205                    Ok(m) => {
2206                        let id = self.midis.len();
2207                        self.midis.push(m);
2208                        return Ok(Some(Value::Number(id as f64)));
2209                    },
2210                    Err(e) => {
2211                        eprintln!("music_midi_load failed ({path}): {e}");
2212                        return Ok(Some(Value::Number(-1.0)));
2213                    },
2214                }
2215            },
2216            "music_midi_count" | "MIDI数量" | "MIDI数" | "미디수" | "จำนวนมิดี" | "تعداد_MIDI" | "عدد_MIDI" | "מספר_MIDI" | "MIDI_تعداد" | "nombre_midi_musique" | "musik_midi_anzahl" | "число_midi_музыки" =>
2217            {
2218                let id = self.arg_num(args, 0, 0.0)? as i64;
2219                let n = self
2220                    .midis
2221                    .get(id as usize)
2222                    .map(|m| m.notes.len())
2223                    .unwrap_or(0);
2224                return Ok(Some(Value::Number(n as f64)));
2225            },
2226            "music_midi_notes" | "MIDI音符" | "MIDIノート" | "미디음표" | "โน้ตมิดี" | "نت‌های_MIDI" | "نغمات_MIDI" | "תווי_MIDI" | "MIDI_نوٹس" | "notes_midi_musique" | "musik_midi_noten" | "ноты_midi_музыки" =>
2227            {
2228                let id = self.arg_num(args, 0, 0.0)? as i64;
2229                let mut out = Vec::new();
2230                if let Some(m) = self.midis.get(id as usize) {
2231                    for n in &m.notes {
2232                        out.push(Value::Number(n.time as f64));
2233                        out.push(Value::Number(n.midi as f64));
2234                    }
2235                }
2236                return Ok(Some(Value::List(out.into())));
2237            },
2238            "music_midi_bars" | "MIDI音条" | "MIDIバー" | "미디바" | "แท่งมิดี" | "میله‌های_MIDI" | "أعمدة_MIDI" | "עמודות_MIDI" | "MIDI_بارز" | "mesures_midi_musique" | "musik_midi_takte" | "такты_midi_музыки" =>
2239            {
2240                let id = self.arg_num(args, 0, 0.0)? as i64;
2241                let mut out = Vec::new();
2242                if let Some(m) = self.midis.get(id as usize) {
2243                    for n in &m.notes {
2244                        out.push(Value::Number(n.time as f64));
2245                        out.push(Value::Number(n.midi as f64));
2246                        out.push(Value::Number(n.dur as f64));
2247                    }
2248                }
2249                return Ok(Some(Value::List(out.into())));
2250            },
2251            "music_judge" | "判定" | "判定する" | "판정" | "ตัดสินจังหวะ" | "داوری_ضرب" | "حكم_الإيقاع" | "שיפוט_קצב" | "بیٹ_فیصلہ" | "juger_musique" | "musik_bewerten" | "оценить_музыку" =>
2252            {
2253                let delta_ms = self.arg_num(args, 0, 9999.0)? as f32;
2254                return Ok(Some(Value::Number(
2255                    ling_music::Grade::judge(delta_ms).index() as f64,
2256                )));
2257            },
2258            "music_grade_name" | "判定名" | "判定名称" | "판정이름" | "ชื่อการตัดสิน" | "نام_رتبه" | "اسم_التقييم" | "שם_דירוג" | "گریڈ_نام" | "nom_grade_musique" | "musik_bewertungsname" | "имя_оценки_музыки" =>
2259            {
2260                let idx = self.arg_num(args, 0, 4.0)? as i32;
2261                return Ok(Some(Value::Str(
2262                    ling_music::Grade::from_index(idx).name().to_string(),
2263                )));
2264            },
2265            "music_note_name" | "音名" | "音名称" | "음이름" | "ชื่อโน้ต" | "نام_نت" | "اسم_النغمة" | "שם_תו" | "نوٹ_نام" | "nom_note_musique" | "musik_notenname" | "имя_ноты_музыки" =>
2266            {
2267                let hz = self.arg_num(args, 0, 0.0)? as f32;
2268                return Ok(Some(Value::Str(ling_music::note::hz_to_name(hz))));
2269            },
2270            "music_hz" | "音符频率" | "音符周波数" | "음표주파수" | "ความถี่โน้ต" | "فرکانس_نت" | "تردد_النغمة" | "תדר_תו" | "نوٹ_ہرٹز" | "hz_musique" | "musik_hz" | "музыка_гц" =>
2271            {
2272                let midi = match args.get(0) {
2273                    Some(Value::Str(s)) => ling_music::note::parse_pitch(s).unwrap_or(69),
2274                    Some(Value::Number(n)) => *n as i32,
2275                    _ => 69,
2276                };
2277                return Ok(Some(Value::Number(
2278                    ling_music::note::midi_to_hz(midi as f32) as f64,
2279                )));
2280            },
2281            "music_pitch_score" | "音准评分" | "音程スコア" | "음정점수" | "คะแนนเสียง" | "امتیاز_زیروبمی" | "درجة_طبقة_الصوت" | "ציון_גובה_צליל" | "پچ_اسکور" | "score_hauteur_musique" | "musik_tonhöhen_punktzahl" | "счёт_высоты_тона" =>
2282            {
2283                let hz = self.arg_num(args, 0, 0.0)? as f32;
2284                let target = self.arg_num(args, 1, 0.0)? as f32;
2285                return Ok(Some(Value::Number(
2286                    ling_music::karaoke::pitch_score(hz, target) as f64,
2287                )));
2288            },
2289
2290            // ── Playback ──────────────────────────────────────────────────────
2291            "music_play" | "播放音乐" | "音楽再生" | "음악재생" | "เล่นเพลง" | "پخش_موسیقی" | "شغّل_الموسيقى" | "נגן_מוזיקה" | "موسیقی_چلاؤ" | "jouer_musique" | "musik_abspielen" | "играть_музыку" =>
2292            {
2293                let id = self.arg_num(args, 0, 0.0)? as usize;
2294                if let Some(t) = self.tracks.get(id) {
2295                    crate::gfx::audio_web::play_music(id, &t.stereo, t.channels, t.rate, 1.0);
2296                }
2297                return Ok(Some(Value::Unit));
2298            },
2299            "music_pause"
2300            | "暂停音乐"
2301            | "音楽一時停止"
2302            | "음악일시정지"
2303            | "หยุดเพลงชั่วคราว"
2304            | "music_stop"
2305            | "停止音乐"
2306            | "音楽停止"
2307            | "음악정지"
2308            | "หยุดเพลง" | "مکث_موسیقی" | "ألبث_الموسيقى" | "השהה_מוזיקה" | "موسیقی_روکو_مؤقت" | "pause_musique" | "musik_pausieren" | "пауза_музыки" => {
2309                let id = self.arg_num(args, 0, 0.0)? as usize;
2310                crate::gfx::audio_web::stop_music(id);
2311                return Ok(Some(Value::Unit));
2312            },
2313            "music_seek" | "定位音乐" | "音楽シーク" | "음악탐색" | "ค้นหาเพลง" | "جستجوی_موسیقی" | "ابحث_في_الموسيقى" | "חפש_במוזיקה" | "موسیقی_تلاش" | "chercher_musique" | "musik_suchen" | "перемотать_музыку" =>
2314            {
2315                // Seek is not straightforward on AudioBufferSourceNode; no-op for now.
2316                return Ok(Some(Value::Unit));
2317            },
2318            "music_pos" | "音乐位置" | "音楽位置" | "음악위치" | "ตำแหน่งเพลง" | "موقعیت_موسیقی" | "موضع_الموسيقى" | "מיקום_מוזיקה" | "موسیقی_مقام" | "position_musique" | "musik_position" | "позиция_музыки" =>
2319            {
2320                return Ok(Some(Value::Number(
2321                    crate::gfx::audio_web::current_music_position(),
2322                )));
2323            },
2324            "music_volume" | "音乐音量" | "音楽音量" | "음악음량" | "ระดับเพลง" | "بلندی_موسیقی" | "مستوى_الموسيقى" | "עוצמת_מוזיקה" | "موسیقی_شدت" | "volume_musique" | "musik_lautstärke" | "громкость_музыки" =>
2325            {
2326                let vol = self.arg_num(args, 0, 0.8)? as f32;
2327                // Apply to the most-recently started slot (slot 0 is typical).
2328                crate::gfx::audio_web::set_music_volume(0, vol);
2329                return Ok(Some(Value::Unit));
2330            },
2331
2332            // ── FFT bands at current playback position ─────────────────────
2333            "music_fft" | "音乐频谱" | "音楽スペクトル" | "음악스펙트럼" | "สเปกตรัมเพลง" | "طیف_موسیقی" | "طيف_الموسيقى" | "ספקטרום_מוזיקה" | "میوزک_اسپیکٹرم" | "fft_musique" | "musik_fft" | "fft_музыки" =>
2334            {
2335                let id = self.arg_num(args, 0, 0.0)? as usize;
2336                let nbands = self.arg_num(args, 1, 16.0)? as usize;
2337                let pos = crate::gfx::audio_web::current_music_position() as f32;
2338                let bands = if let Some(t) = self.tracks.get(id) {
2339                    ling_music::analysis::fft_bands_at_pos(&t.mono, t.rate, pos, nbands)
2340                } else {
2341                    vec![0.0f32; nbands]
2342                };
2343                return Ok(Some(Value::List(
2344                    bands.into_iter().map(|x| Value::Number(x as f64)).collect::<Vec<_>>().into(),
2345                )));
2346            },
2347
2348            _ => {},
2349        }
2350        Ok(None)
2351    }
2352
2353    /// Lay out `text` for font `id` at size `px`, returning every glyph contour as
2354    /// a screen-space polyline (x→right, y→down). `(x, y)` is the text box top-left;
2355    /// the baseline is placed `ascent*px` below it. Curves are flattened to 0.3 px.
2356    #[cfg(not(target_arch = "wasm32"))]
2357    fn font_layout_2d(
2358        &mut self,
2359        id: usize,
2360        x: f32,
2361        y: f32,
2362        px: f32,
2363        text: &str,
2364    ) -> Vec<Vec<[f32; 2]>> {
2365        let mut out = Vec::new();
2366        for g in self.font_layout_2d_glyphs(id, x, y, px, text) {
2367            out.extend(g);
2368        }
2369        out
2370    }
2371
2372    /// Same as [`font_layout_2d`] but grouped per glyph (so a fill can apply the
2373    /// non-zero winding rule within each glyph, preserving interior holes).
2374    #[cfg(not(target_arch = "wasm32"))]
2375    fn font_layout_2d_glyphs(
2376        &mut self,
2377        id: usize,
2378        x: f32,
2379        y: f32,
2380        px: f32,
2381        text: &str,
2382    ) -> Vec<Vec<Vec<[f32; 2]>>> {
2383        let font = &mut self.fonts[id];
2384        let asc = font.ascent();
2385        let tol = 0.3 / px;
2386        let mut pen = 0.0f32;
2387        let mut glyphs = Vec::new();
2388        for ch in text.chars() {
2389            let go = font.glyph_outline(ch, tol);
2390            let mut contours = Vec::with_capacity(go.polylines.len());
2391            for pl in &go.polylines {
2392                let mapped: Vec<[f32; 2]> = pl
2393                    .iter()
2394                    .map(|p| [x + (pen + p[0]) * px, y + (asc - p[1]) * px])
2395                    .collect();
2396                contours.push(mapped);
2397            }
2398            glyphs.push(contours);
2399            pen += go.advance;
2400        }
2401        glyphs
2402    }
2403
2404    /// Register every item (functions, structs, globals) and evaluate the
2405    /// non-`do` globals into `global_seed`, WITHOUT running the entry. Used to
2406    /// prime the JIT's fallback interpreter so cranelift-skipped (oversized)
2407    /// functions can still be interpreted with full access to globals + peers.
2408    pub fn register_program(&mut self, program: &Program) -> Result<(), String> {
2409        for item in &program.items {
2410            self.register_item("", item)?;
2411        }
2412        let mut env = new_env();
2413        let non_do: Vec<_> = self
2414            .globals
2415            .iter()
2416            .filter(|(_, e)| !matches!(e, Expr::Do(_)))
2417            .map(|(k, e)| (k.clone(), e.clone()))
2418            .collect();
2419        let mut pending: Vec<(String, Expr)> = Vec::new();
2420        for (k, expr) in &non_do {
2421            let mut tmp = new_env();
2422            if let Ok(v) = self.eval_expr(expr, &mut tmp) {
2423                env.insert(k.clone(), v);
2424            } else {
2425                pending.push((k.clone(), expr.clone()));
2426            }
2427        }
2428        for (k, expr) in &pending {
2429            let mut tmp = env.clone();
2430            if let Ok(v) = self.eval_expr(expr, &mut tmp) {
2431                env.insert(k.clone(), v);
2432            }
2433        }
2434        self.global_seed = env;
2435        Ok(())
2436    }
2437
2438    pub fn run_program(&mut self, program: &Program) -> Result<(), String> {
2439        self.register_program(program)?;
2440        let entry = self
2441            .find_entry()
2442            .ok_or("no entry point — need `bind start = do {...}` or `ผูก เริ่ม = ทำ {...}`")?;
2443        let mut env = self.global_seed.clone();
2444        self.framed("start", |me| me.eval_expr(&entry, &mut env))
2445            .map(|_| ())
2446            .map_err(|e| match e {
2447                EvalErr::Runtime(s) => s,
2448                EvalErr::Return(_) => "unexpected top-level return".to_string(),
2449                EvalErr::Break => "unexpected break at top level".to_string(),
2450            })
2451    }
2452
2453    fn register_item(&mut self, ns: &str, item: &Item) -> Result<(), String> {
2454        match item {
2455            Item::Bind(name, expr) => {
2456                let key = if ns.is_empty() {
2457                    name.clone()
2458                } else {
2459                    format!("{ns}::{name}")
2460                };
2461                self.globals.insert(key, expr.clone());
2462            },
2463            Item::Fn(def) => {
2464                let key = if ns.is_empty() {
2465                    def.name.clone()
2466                } else {
2467                    format!("{ns}::{}", def.name)
2468                };
2469                self.functions.insert(key, Rc::new(def.clone()));
2470            },
2471            Item::Mod(name, body) => {
2472                let child_ns = if ns.is_empty() {
2473                    name.clone()
2474                } else {
2475                    format!("{ns}::{name}")
2476                };
2477                for child in body {
2478                    self.register_item(&child_ns, child)?;
2479                }
2480            },
2481            Item::TypeAlias(_, _) => {},
2482            Item::Struct(name, fields) => {
2483                self.structs.insert(name.clone(), fields.clone());
2484                if !ns.is_empty() {
2485                    self.structs.insert(format!("{ns}::{name}"), fields.clone());
2486                }
2487            },
2488            Item::Enum(name, variants) => {
2489                for v in variants {
2490                    self.enum_variants
2491                        .insert(v.name.clone(), (name.clone(), v.arity));
2492                    self.enum_variants
2493                        .insert(format!("{name}::{}", v.name), (name.clone(), v.arity));
2494                    if !ns.is_empty() {
2495                        self.enum_variants
2496                            .insert(format!("{ns}::{name}::{}", v.name), (name.clone(), v.arity));
2497                    }
2498                }
2499            },
2500            Item::Use { path, alias } => {
2501                self.load_module(path, alias.as_deref(), ns)?;
2502            },
2503        }
2504        Ok(())
2505    }
2506
2507    /// Resolve `path` relative to `source_dir`, load and parse it, then
2508    /// register all its definitions.  If `alias` is given, every name is
2509    /// prefixed with `<parent_ns>::<alias>`.  Circular imports are silently
2510    /// skipped.
2511    fn load_module(
2512        &mut self,
2513        path: &str,
2514        alias: Option<&str>,
2515        parent_ns: &str,
2516    ) -> Result<(), String> {
2517        // ── Wasm32: no filesystem — use the pre-registered module registry ──
2518        #[cfg(target_arch = "wasm32")]
2519        let (source, sub_dir) = {
2520            // Skip if already loaded (circular import guard)
2521            if self.loaded_files.contains(path) {
2522                return Ok(());
2523            }
2524            self.loaded_files.insert(path.to_string());
2525
2526            let src = crate::runtime::get_wasm_module(path)
2527                .or_else(|| crate::runtime::get_wasm_module(&format!("{}.ling", path)))
2528                .ok_or_else(|| format!("use: cannot find module '{path}'"))?;
2529            (src, None::<std::path::PathBuf>)
2530        };
2531
2532        // ── Native: resolve against filesystem ──
2533        #[cfg(not(target_arch = "wasm32"))]
2534        let (source, sub_dir) = {
2535            let base_dir = self
2536                .source_dir
2537                .clone()
2538                .unwrap_or_else(|| std::path::PathBuf::from("."));
2539            let raw = std::path::Path::new(path);
2540            let candidates: Vec<std::path::PathBuf> = vec![
2541                base_dir.join(format!("{}.ling", path)),
2542                base_dir.join(format!("{}.灵", path)),
2543                base_dir.join(format!("{}.령", path)),
2544                base_dir.join(format!("{}.霊", path)),
2545                base_dir.join(format!("{}.ลิง", path)),
2546                base_dir.join(raw),
2547                std::path::PathBuf::from(format!("{}.ling", path)),
2548                std::path::PathBuf::from(path),
2549            ];
2550
2551            let resolved = candidates
2552                .into_iter()
2553                .find(|p| p.exists())
2554                .ok_or_else(|| format!("use: cannot find module '{path}'"))?;
2555
2556            let canonical = resolved
2557                .canonicalize()
2558                .unwrap_or_else(|_| resolved.clone())
2559                .to_string_lossy()
2560                .to_string();
2561
2562            // Skip if already loaded (circular import guard)
2563            if self.loaded_files.contains(&canonical) {
2564                return Ok(());
2565            }
2566            self.loaded_files.insert(canonical.clone());
2567
2568            let src = std::fs::read_to_string(&resolved)
2569                .map_err(|e| format!("use: failed to read '{path}': {e}"))?;
2570            let dir = resolved.parent().map(|p| p.to_path_buf());
2571            (src, dir)
2572        };
2573
2574        let program = crate::parser::parse(&source)
2575            .map_err(|e| format!("use: parse error in '{path}': {e}"))?;
2576
2577        // Compute target namespace: parent_ns :: alias (or just alias, or just parent_ns)
2578        let target_ns = match (parent_ns.is_empty(), alias) {
2579            (_, Some(a)) if !parent_ns.is_empty() => format!("{parent_ns}::{a}"),
2580            (_, Some(a)) => a.to_string(),
2581            (false, None) => parent_ns.to_string(),
2582            (true, None) => String::new(),
2583        };
2584
2585        // Save/restore source_dir for nested relative imports
2586        let prev_dir = self.source_dir.clone();
2587        self.source_dir = sub_dir;
2588
2589        for item in &program.items {
2590            self.register_item(&target_ns, item)?;
2591        }
2592
2593        self.source_dir = prev_dir;
2594        Ok(())
2595    }
2596
2597    fn find_entry(&self) -> Option<Expr> {
2598        // Known entry-point names across supported human languages.
2599        for key in crate::entry::ENTRY_NAMES {
2600            if let Some(e) = self.globals.get(*key) {
2601                return Some(e.clone());
2602            }
2603        }
2604        self.globals
2605            .values()
2606            .find(|e| matches!(e, Expr::Do(_)))
2607            .cloned()
2608    }
2609
2610    // ─── Expression evaluation ────────────────────────────────────────────────
2611
2612    fn eval_expr(&mut self, expr: &Expr, env: &mut Env) -> EvalResult {
2613        match expr {
2614            Expr::Str(s) => Ok(Value::Str(s.clone())),
2615            Expr::Number(n) => Ok(Value::Number(*n)),
2616            Expr::Bool(b) => Ok(Value::Bool(*b)),
2617            Expr::Unit => Ok(Value::Unit),
2618            Expr::Array(elems) => {
2619                let vs: Vec<_> = elems
2620                    .iter()
2621                    .map(|e| self.eval_expr(e, env))
2622                    .collect::<Result<_, _>>()?;
2623                Ok(Value::List(Rc::new(vs)))
2624            },
2625
2626            Expr::Ident(name) => self.lookup(name, env),
2627
2628            Expr::Path(segs) => {
2629                if segs.len() == 1 {
2630                    return self.lookup(&segs[0], env);
2631                }
2632                Ok(Value::Str(segs.join("::")))
2633            },
2634
2635            Expr::Ref(inner) => self.eval_expr(inner, env),
2636            Expr::Await(inner) => self.eval_expr(inner, env),
2637
2638            Expr::Do(stmts) => {
2639                let mut local = env.clone();
2640                Ok(self.exec_block(stmts, &mut local)?.unwrap_or(Value::Unit))
2641            },
2642
2643            Expr::BinOp(op, lhs, rhs) => {
2644                let l = self.eval_expr(lhs, env)?;
2645                let r = self.eval_expr(rhs, env)?;
2646                self.apply_binop(op, l, r)
2647            },
2648
2649            Expr::If { cond, then, elseifs, else_body } => {
2650                let cond_val = self.eval_expr(cond, env)?;
2651                if self.is_truthy(&cond_val) {
2652                    return Ok(self.exec_block(then, env)?.unwrap_or(Value::Unit));
2653                }
2654                for (ei_cond, ei_body) in elseifs {
2655                    let ei_cond_val = self.eval_expr(ei_cond, env)?;
2656                    if self.is_truthy(&ei_cond_val) {
2657                        return Ok(self.exec_block(ei_body, env)?.unwrap_or(Value::Unit));
2658                    }
2659                }
2660                if let Some(eb) = else_body {
2661                    return Ok(self.exec_block(eb, env)?.unwrap_or(Value::Unit));
2662                }
2663                Ok(Value::Unit)
2664            },
2665
2666            Expr::While { cond, body } => {
2667                // Run the body directly in the *outer* env so that
2668                // `bind counter = counter + 1` persists across iterations,
2669                // which is the expected behaviour in a scripting language.
2670                loop {
2671                    let cv = self.eval_expr(cond, env)?;
2672                    if !self.is_truthy(&cv) {
2673                        break;
2674                    }
2675                    match self.exec_block(body, env) {
2676                        Ok(_) => {},
2677                        Err(EvalErr::Break) => break,
2678                        Err(e) => return Err(e),
2679                    }
2680                }
2681                Ok(Value::Unit)
2682            },
2683
2684            Expr::For { var, iter, body } => {
2685                let iter_val = self.eval_expr(iter, env)?;
2686                let items = self.value_to_iter(iter_val)?;
2687                for item in items {
2688                    let mut local = env.clone();
2689                    local.insert(var.clone(), item);
2690                    match self.exec_block(body, &mut local) {
2691                        Ok(_) => {},
2692                        Err(EvalErr::Break) => break,
2693                        Err(e) => return Err(e),
2694                    }
2695                }
2696                Ok(Value::Unit)
2697            },
2698
2699            Expr::Match(subject, arms) => {
2700                let subj = self.eval_expr(subject, env)?;
2701                for arm in arms {
2702                    if let Some(bindings) = self.match_pattern(&arm.pattern, &subj) {
2703                        let mut local = env.clone();
2704                        local.extend(bindings);
2705                        return self.eval_expr(&arm.body, &mut local);
2706                    }
2707                }
2708                Ok(Value::Unit)
2709            },
2710
2711            Expr::Range(lo, hi) => {
2712                let lo_v = self.eval_expr(lo, env)?;
2713                let hi_v = self.eval_expr(hi, env)?;
2714                let lo_n = self.to_number(&lo_v)? as i64;
2715                let hi_n = self.to_number(&hi_v)? as i64;
2716                Ok(Value::List(Rc::new(
2717                    (lo_n..hi_n).map(|i| Value::Number(i as f64)).collect(),
2718                )))
2719            },
2720
2721            Expr::Index(base, idx) => {
2722                let b = self.eval_expr(base, env)?;
2723                let i = self.eval_expr(idx, env)?;
2724                let n = self.to_number(&i)? as usize;
2725                match b {
2726                    Value::List(v) => v
2727                        .get(n)
2728                        .cloned()
2729                        .ok_or_else(|| EvalErr::from(format!("index {n} out of bounds"))),
2730                    Value::Str(s) => s
2731                        .chars()
2732                        .nth(n)
2733                        .map(|c| Value::Str(c.to_string()))
2734                        .ok_or_else(|| EvalErr::from(format!("index {n} out of bounds"))),
2735                    other => Err(EvalErr::from(format!("cannot index {:?}", other))),
2736                }
2737            },
2738
2739            Expr::Call(callee, args) => {
2740                let arg_vals: Vec<Value> = args
2741                    .iter()
2742                    .map(|a| self.eval_expr(a, env))
2743                    .collect::<Result<_, _>>()?;
2744                match callee.as_ref() {
2745                    Expr::Ident(name) => self.call_named(name, arg_vals, env),
2746                    Expr::Path(segs) => self.call_named(&segs.join("::"), arg_vals, env),
2747                    _ => {
2748                        let v = self.eval_expr(callee, env)?;
2749                        self.call_value(v, arg_vals)
2750                    },
2751                }
2752            },
2753
2754            Expr::MethodCall { receiver, method, args } => {
2755                let recv = self.eval_expr(receiver, env)?;
2756                let arg_vals: Vec<Value> = args
2757                    .iter()
2758                    .map(|a| self.eval_expr(a, env))
2759                    .collect::<Result<_, _>>()?;
2760                self.call_method(recv, method, arg_vals)
2761            },
2762
2763            Expr::Closure(params, body) => Ok(Value::Fn(
2764                params.clone(),
2765                vec![Stmt::Expr(*body.clone())],
2766                env.clone(),
2767            )),
2768
2769            Expr::Asm(_) => Ok(Value::Unit),
2770        }
2771    }
2772
2773    // ─── Block execution ─────────────────────────────────────────────────────
2774
2775    fn exec_block(&mut self, stmts: &[Stmt], env: &mut Env) -> Result<Option<Value>, EvalErr> {
2776        let mut last: Option<Value> = None;
2777        for stmt in stmts {
2778            match stmt {
2779                Stmt::Bind(name, expr) => {
2780                    match self.try_inplace_list_update(name, expr, env)? {
2781                        Some(v) => env.insert(name.clone(), v),
2782                        None => {
2783                            let v = self.eval_expr(expr, env)?;
2784                            env.insert(name.clone(), v)
2785                        },
2786                    };
2787                    last = None;
2788                },
2789                Stmt::Return(expr) => {
2790                    let v = self.eval_expr(expr, env)?;
2791                    return Err(EvalErr::Return(v));
2792                },
2793                Stmt::Expr(expr) => {
2794                    last = Some(self.eval_expr(expr, env)?);
2795                },
2796            }
2797        }
2798        Ok(last)
2799    }
2800
2801    /// Fast path for `bind v = list_push(v, x)` / `bind v = list_set(v, i, x)`:
2802    /// the binding aliases the same list being rebuilt, so the env copy keeps the
2803    /// `Rc` shared and `make_mut` copies the whole vector every call. Taking the
2804    /// value out of env first leaves the `Rc` unique (unless truly aliased
2805    /// elsewhere, where copy-on-write still applies), turning O(n) into O(1).
2806    /// Returns `None` to fall back to normal evaluation.
2807    fn try_inplace_list_update(
2808        &mut self,
2809        name: &str,
2810        expr: &Expr,
2811        env: &mut Env,
2812    ) -> Result<Option<Value>, EvalErr> {
2813        let Expr::Call(callee, args) = expr else { return Ok(None) };
2814        let Expr::Ident(fname) = callee.as_ref() else { return Ok(None) };
2815        let is_push = matches!(
2816            fname.as_str(),
2817            "list_push" | "เพิ่มรายการ" | "列表添加" | "リスト追加" | "목록추가"
2818        );
2819        let is_set = matches!(
2820            fname.as_str(),
2821            "list_set" | "ตั้งรายการ" | "设元素" | "要素設定" | "요소설정"
2822        );
2823        if !is_push && !is_set {
2824            return Ok(None);
2825        }
2826        // First arg must be the same variable we are binding, and the builtin
2827        // must not be shadowed by a user function.
2828        match args.first() {
2829            Some(Expr::Ident(a0)) if a0 == name => {},
2830            _ => return Ok(None),
2831        }
2832        if self.functions.contains_key(fname.as_str()) {
2833            return Ok(None);
2834        }
2835        if is_push {
2836            if args.len() != 2 {
2837                return Ok(None);
2838            }
2839            let val = self.eval_expr(&args[1], env)?;
2840            match env.remove(name) {
2841                Some(Value::List(mut v)) => {
2842                    Rc::make_mut(&mut v).push(val);
2843                    Ok(Some(Value::List(v)))
2844                },
2845                other => {
2846                    if let Some(o) = other {
2847                        env.insert(name.to_string(), o);
2848                    }
2849                    Ok(None)
2850                },
2851            }
2852        } else {
2853            if args.len() != 3 {
2854                return Ok(None);
2855            }
2856            let idx_v = self.eval_expr(&args[1], env)?;
2857            let idx = self.to_number(&idx_v).unwrap_or(0.0) as usize;
2858            let val = self.eval_expr(&args[2], env)?;
2859            match env.remove(name) {
2860                Some(Value::List(mut v)) => {
2861                    if idx < v.len() {
2862                        Rc::make_mut(&mut v)[idx] = val;
2863                    }
2864                    Ok(Some(Value::List(v)))
2865                },
2866                other => {
2867                    if let Some(o) = other {
2868                        env.insert(name.to_string(), o);
2869                    }
2870                    Ok(None)
2871                },
2872            }
2873        }
2874    }
2875
2876    // ─── Dispatch helpers ─────────────────────────────────────────────────────
2877
2878    fn lookup(&self, name: &str, env: &Env) -> EvalResult {
2879        if let Some(v) = env.get(name) {
2880            return Ok(v.clone());
2881        }
2882        // Globals are an immutable load-time snapshot shared by every call frame;
2883        // a function reads them here instead of receiving a per-call clone.
2884        if let Some(v) = self.global_seed.get(name) {
2885            return Ok(v.clone());
2886        }
2887        if self.functions.contains_key(name) {
2888            let def = &self.functions[name];
2889            return Ok(Value::Fn(def.params.clone(), def.body.clone(), new_env()));
2890        }
2891        // Bare nullary enum variant used as a value (e.g. `bind p = Origin`).
2892        if let Some((enum_name, 0)) = self.enum_variants.get(name).cloned() {
2893            let variant = name.rsplit("::").next().unwrap_or(name).to_string();
2894            return Ok(Value::Variant { enum_name, variant, payload: Vec::new() });
2895        }
2896        // Math constants usable as plain identifiers (e.g. `sin(pi)`)
2897        match name {
2898            "pi" | "π" | "พาย" | "圆周率" | "円周率" | "파이" | "پی" | "باي" | "פאי" | "پائی" | "пи" => {
2899                return Ok(Value::Number(std::f64::consts::PI))
2900            },
2901            "tau" | "τ" | "双周率" | "タウ" | "타우" | "ทาว" | "تاو" | "تاو" | "טאו" | "ٹاؤ" | "тау" => {
2902                return Ok(Value::Number(std::f64::consts::TAU))
2903            },
2904            _ => {},
2905        }
2906        Err(EvalErr::from(format!("undefined: '{name}'")))
2907    }
2908
2909    /// Profiling wrapper around the real dispatch. Zero overhead unless
2910    /// `LING_PROFILE` is set (one thread-local bool check per call). When on,
2911    /// it tallies per-name call count + inclusive time and, on each frame
2912    /// boundary (`present`), prints a sorted top-down report every
2913    /// `LING_PROFILE_EVERY` frames (default 240). Both the JIT (`ling_builtin` →
2914    /// here) and the tree-walker route through this, so it sees every builtin —
2915    /// in JIT mode user fns are native, so it's a clean builtin/render/physics
2916    /// profile with no nesting double-count.
2917    pub(crate) fn call_named(&mut self, name: &str, args: Vec<Value>, env: &Env) -> EvalResult {
2918        if !ling_profile_enabled() {
2919            return self.call_named_inner(name, args, env);
2920        }
2921        let t0 = crate::runtime::now_secs();
2922        let r = self.call_named_inner(name, args, env);
2923        ling_profile_record(
2924            name,
2925            ((crate::runtime::now_secs() - t0) * 1_000_000_000.0) as u128,
2926        );
2927        r
2928    }
2929
2930    fn call_named_inner(&mut self, name: &str, args: Vec<Value>, env: &Env) -> EvalResult {
2931        // A user-defined function shadows any builtin of the same name, matching
2932        // the JIT/AOT backends (which always resolve a defined function first).
2933        if let Some(def) = self.functions.get(name).cloned() {
2934            let mut call_env =
2935                FxHashMap::with_capacity_and_hasher(def.params.len(), Default::default());
2936            let _ = env; // call-site locals are intentionally NOT visible to fns
2937            for (param, arg) in def.params.iter().zip(args) {
2938                call_env.insert(param.clone(), arg);
2939            }
2940            return match self.framed(name, |me| me.exec_block(&def.body, &mut call_env)) {
2941                Ok(v) => Ok(v.unwrap_or(Value::Unit)),
2942                Err(EvalErr::Return(v)) => Ok(v),
2943                Err(e) => Err(e),
2944            };
2945        }
2946
2947        #[cfg(target_arch = "wasm32")]
2948        if let Some(v) = self.wasm_music_builtin(name, &args)? {
2949            return Ok(v);
2950        }
2951
2952        match name {
2953            // Module global read emitted by the MIR backend: resolve against the
2954            // evaluated global snapshot (functions see globals read-only).
2955            "__ling_global" => {
2956                if let Some(Value::Str(g)) = args.first() {
2957                    if let Some(v) = self.global_seed.get(g.as_str()) {
2958                        return Ok(v.clone());
2959                    }
2960                }
2961                return Ok(Value::Unit);
2962            },
2963            // ── Print ──
2964            "print" | "println" | "印" | "打印" | "印刷" | "พิมพ์" | "출력" | "вывести"
2965            | "imprimir" | "afficher" | "چاپ" | "اطبع" | "הדפס" | "چھاپو" | "drucken" | "печать" => {
2966                let s = args
2967                    .iter()
2968                    .map(|v| v.to_string())
2969                    .collect::<Vec<_>>()
2970                    .join("");
2971                println!("{s}");
2972                return Ok(Value::Unit);
2973            },
2974            // print_color(colorIdx, text...) — ANSI-coloured console line.
2975            //   colorIdx 0..7 → bright fg (90+idx): 1=red 2=green 3=yellow 4=blue 6=cyan 7=white.
2976            "print_color" | "พิมพ์สี" | "چاپ_رنگی" | "اطبع_بلون" | "הדפס_בצבע" | "رنگین_چھاپو" => {
2977                #[cfg(windows)]
2978                {
2979                    use std::sync::Once;
2980                    static VT: Once = Once::new();
2981                    VT.call_once(|| {
2982                        extern "system" {
2983                            fn GetStdHandle(n: u32) -> *mut std::ffi::c_void;
2984                            fn GetConsoleMode(h: *mut std::ffi::c_void, m: *mut u32) -> i32;
2985                            fn SetConsoleMode(h: *mut std::ffi::c_void, m: u32) -> i32;
2986                        }
2987                        unsafe {
2988                            let h = GetStdHandle(0xFFFF_FFF5u32); // STD_OUTPUT_HANDLE (-11)
2989                            let mut mode = 0u32;
2990                            if GetConsoleMode(h, &mut mode) != 0 {
2991                                SetConsoleMode(h, mode | 0x0004); // ENABLE_VIRTUAL_TERMINAL_PROCESSING
2992                            }
2993                        }
2994                    });
2995                }
2996                let col = self.arg_num(&args, 0, 7.0)? as i64;
2997                let s = args
2998                    .iter()
2999                    .skip(1)
3000                    .map(|v| v.to_string())
3001                    .collect::<Vec<_>>()
3002                    .join("");
3003                let code = 90 + col.clamp(0, 7);
3004                println!("\x1b[1;{code}m{s}\x1b[0m");
3005                return Ok(Value::Unit);
3006            },
3007            // ── Format ──
3008            "format"
3009            | "格式"
3010            | "フォーマット"
3011            | "서식"
3012            | "รูปแบบ"
3013            | "форматировать"
3014            | "formatear"
3015            | "formater" | "قالب‌بندی" | "نسّق" | "פרמט" | "فارمیٹ" | "formatieren" => {
3016                return Ok(Value::Str(self.builtin_format(&args)?));
3017            },
3018            // ── String join / concatenation ──
3019            "格式::拼接" | "format::join" => match args.first() {
3020                Some(Value::List(items)) => {
3021                    return Ok(Value::Str(items.iter().map(|v| v.to_string()).collect()));
3022                },
3023                _ => return Ok(Value::Str(self.builtin_format(&args)?)),
3024            },
3025            // ── Result constructors ──
3026            "ok" | "好" | "良し" | "좋아" | "โอเค" | "تایید" | "تمام" | "בסדר" | "ٹھیک" | "bon" | "gut" | "хорошо" => {
3027                let val = args.into_iter().next().unwrap_or(Value::Unit);
3028                return Ok(Value::Ok(Box::new(val)));
3029            },
3030            "bad" | "坏" | "err" | "悪い" | "나쁨" | "ผิด" | "بد" | "سيء" | "רע" | "برا" | "mauvais" | "schlecht" | "плохо" => {
3031                let val = args.into_iter().next().unwrap_or(Value::Unit);
3032                return Ok(Value::Err(Box::new(val)));
3033            },
3034            // ── Vec constructors ──
3035            "向量::从" | "Vec::from" => {
3036                if let Some(Value::List(v)) = args.first() {
3037                    return Ok(Value::List(v.clone()));
3038                }
3039                return Ok(Value::List(Rc::new(args)));
3040            },
3041            "向量::有容量" | "Vec::with_capacity" => {
3042                return Ok(Value::List(Rc::new(Vec::new())))
3043            },
3044            // ── Timer stubs ──
3045            "计时::获取当前小时" | "Timer::hour" => return Ok(Value::Number(14.0)),
3046            "计时::现在" | "Timer::now" => return Ok(Value::Number(1000.0)),
3047            // ── Sleep ──
3048            "sleep" | "หยุด" | "นอน" | "sleep_ms" | "睡眠" | "眠る" | "スリープ" | "잠자기"
3049            | "잠" | "流水::睡眠" | "Flow::sleep" | "خواب" | "نم" | "שינה" | "سو_جاؤ" | "dormir" | "schlafen" | "спать" => {
3050                if let Some(ms_val) = args.first() {
3051                    if let Ok(ms) = self.to_number(ms_val) {
3052                        #[cfg(target_arch = "wasm32")]
3053                        wasm_sleep_ms(ms.max(0.0) as i32);
3054                        #[cfg(not(target_arch = "wasm32"))]
3055                        std::thread::sleep(std::time::Duration::from_millis(ms as u64));
3056                    }
3057                }
3058                return Ok(Value::Unit);
3059            },
3060            // ── Flow::parallel stub ──
3061            "流水::并行" | "Flow::parallel" => {
3062                if let Some(Value::Fn(params, body, mut cap)) = args.first().cloned() {
3063                    let _ = params;
3064                    match self.exec_block(&body, &mut cap) {
3065                        Ok(Some(v)) => return Ok(v),
3066                        Ok(None) => return Ok(Value::Unit),
3067                        Err(EvalErr::Return(v)) => return Ok(v),
3068                        Err(e) => return Err(e),
3069                    }
3070                }
3071                return Ok(Value::Unit);
3072            },
3073
3074            // ══════════════════════════════════════════════════════════════════
3075            // MATH BUILTINS  (all args and results are f64)
3076            // Thai aliases: ไซน์ โคไซน์ แทนเจนต์ รากที่สอง ค่าสัมบูรณ์
3077            //               ปัดลง ปัดขึ้น ปัดเศษ ตัดทศนิยม ต่ำสุด สูงสุด
3078            //               จำกัด ยกกำลัง ลอการิทึม พาย
3079            // ══════════════════════════════════════════════════════════════════
3080
3081            // ── Trigonometry (input in radians) ──
3082            "sin" | "ไซน์" | "正弦" | "サイン" | "사인" | "سینوس" | "جا" | "סינוס" | "سائن" | "sinus" | "синус" => {
3083                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.sin()));
3084            },
3085            "cos" | "โคไซน์" | "余弦" | "コサイン" | "코사인" | "کسینوس" | "جتا" | "קוסינוס" | "کوسائن" | "cosinus" | "kosinus" | "косинус" => {
3086                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.cos()));
3087            },
3088
3089            // ── Hyperbolic functions ──
3090            // Hyperbolic tangent
3091            "tanh" | "tanhf" | "双曲正切" | "双曲線正接" | "쌍곡탄젠트" => {
3092                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.tanh()));
3093            },
3094
3095            "tan" | "แทนเจนต์" | "正切" | "タンジェント" | "탄젠트" | "تانژانت" | "ظا" | "טנגנס" | "ٹینجنٹ" | "tangente" | "tangens" | "тангенс" => {
3096                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.tan()));
3097            },
3098            "asin" | "arcsin" | "反正弦" | "アークサイン" | "아크사인" | "อาร์กไซน์" | "آرک‌سینوس" | "قوس_جا" | "ארקסינוס" | "آرک_سائن" | "arcsinus" | "arkussinus" | "арксинус" =>
3099            {
3100                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.asin()));
3101            },
3102            "acos" | "arccos" | "反余弦" | "アークコサイン" | "아크코사인" | "อาร์กโคไซน์" | "آرک‌کسینوس" | "قوس_جتا" | "ארקוקוסינוס" | "آرک_کوسائن" | "arccosinus" | "arkuskosinus" | "арккосинус" =>
3103            {
3104                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.acos()));
3105            },
3106            "atan" | "arctan" | "反正切" | "アークタンジェント" | "아크탄젠트" | "อาร์กแทนเจนต์" | "آرک‌تانژانت" | "قوس_ظا" | "ארקטנגנס" | "آرک_ٹینجنٹ" | "arctangente" | "arkustangens" | "арктангенс" =>
3107            {
3108                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.atan()));
3109            },
3110            "atan2" | "arctan2" | "反正切2" | "アークタンジェント2" | "아크탄젠트2" =>
3111            {
3112                let y = self.arg_num(&args, 0, 0.0)?;
3113                let x = self.arg_num(&args, 1, 1.0)?;
3114                return Ok(Value::Number(y.atan2(x)));
3115            },
3116
3117            // ── Roots / powers ──
3118            "sqrt" | "รากที่สอง" | "平方根" | "根" | "제곱근" | "جذر" | "جذر" | "שורש" | "جذر" | "racine_carrée" | "quadratwurzel" | "корень" => {
3119                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.sqrt()));
3120            },
3121            "cbrt" | "立方根" | "세제곱근" | "รากที่สาม" | "ریشه_سوم" | "جذر_تكعيبي" | "שורש_שלישי" | "مکعب_جذر" | "racine_cubique" | "kubikwurzel" | "кубический_корень" => {
3122                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.cbrt()));
3123            },
3124            "pow" | "ยกกำลัง" | "幂" | "べき乗" | "거듭제곱" | "توان" | "أس" | "חזקה" | "قوت" | "puissance" | "potenz" | "степень" => {
3125                let base = self.arg_num(&args, 0, 0.0)?;
3126                let exp = self.arg_num(&args, 1, 1.0)?;
3127                return Ok(Value::Number(base.powf(exp)));
3128            },
3129            "exp" | "指数" | "指数関数" | "지수" => {
3130                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.exp()));
3131            },
3132            "hypot" | "斜边" | "斜辺" | "빗변" => {
3133                let x = self.arg_num(&args, 0, 0.0)?;
3134                let y = self.arg_num(&args, 1, 0.0)?;
3135                return Ok(Value::Number(x.hypot(y)));
3136            },
3137
3138            // ── Logarithms ──
3139            "ln" | "log" | "ลอการิทึม" | "对数" | "対数" | "로그" | "لگاریتم_طبیعی" | "لوغاريتم_طبيعي" | "לוגריתם_טבעי" | "فطری_لوگ" => {
3140                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.ln()));
3141            },
3142            "log2" | "对数2" | "対数2" | "로그2" => {
3143                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.log2()));
3144            },
3145            "log10" | "对数10" | "対数10" | "로그10" => {
3146                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.log10()));
3147            },
3148
3149            // ── Rounding / truncation ──
3150            "abs" | "ค่าสัมบูรณ์" | "绝对值" | "绝对" | "絶対値" | "절댓값" | "절대값" | "قدرمطلق" | "مطلق" | "ערך_מוחלט" | "مطلق_قدر" | "valeur_absolue" | "betrag" | "модуль_числа" =>
3151            {
3152                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.abs()));
3153            },
3154            "floor" | "ปัดลง" | "向下取整" | "下整" | "床関数" | "내림" | "کف" | "أرضية" | "רצפה" | "فرش" | "plancher" | "abrunden" | "вниз" => {
3155                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.floor()));
3156            },
3157            "ceil" | "ปัดขึ้น" | "向上取整" | "上整" | "天井関数" | "올림" | "سقف" | "سقف" | "תקרה" | "چھت" | "plafond" | "aufrunden" | "вверх" =>
3158            {
3159                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.ceil()));
3160            },
3161            "round" | "ปัดเศษ" | "四舍五入" | "四舍" | "四捨五入" | "반올림" | "گرد_کردن" | "تقريب" | "עיגול" | "گول" | "arrondir" | "runden" | "округлить" =>
3162            {
3163                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.round()));
3164            },
3165            "trunc"
3166            | "int"
3167            | "ตัดทศนิยม"
3168            | "取整"
3169            | "整数化"
3170            | "整数"
3171            | "截整"
3172            | "정수화"
3173            | "정수"
3174            | "切り捨て"
3175            | "버림" | "برش" | "اقتطاع" | "קיטום" | "کٹائی" | "tronquer" | "abschneiden" | "усечь" => {
3176                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.trunc()));
3177            },
3178            "fract" | "小数部分" | "小数部" | "소수부" => {
3179                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.fract()));
3180            },
3181
3182            // ── min / max / clamp ──
3183            "min" | "ต่ำสุด" | "最小" | "최솟값" | "کمینه" | "أصغر" | "מינימום" | "کم_ترین" | "minimum" | "минимум" => {
3184                let a = self.arg_num(&args, 0, 0.0)?;
3185                let b = self.arg_num(&args, 1, 0.0)?;
3186                return Ok(Value::Number(a.min(b)));
3187            },
3188            "max" | "สูงสุด" | "最大" | "최댓값" | "بیشینه" | "أكبر" | "מקסימום" | "زیادہ_ترین" | "maximum" | "максимум" => {
3189                let a = self.arg_num(&args, 0, 0.0)?;
3190                let b = self.arg_num(&args, 1, 0.0)?;
3191                return Ok(Value::Number(a.max(b)));
3192            },
3193            "clamp" | "จำกัด" | "截取" | "範囲制限" | "범위제한" | "محدود" | "قيّد" | "הגבל" | "محدود" | "limiter" | "begrenzen" | "ограничить" => {
3194                let x = self.arg_num(&args, 0, 0.0)?;
3195                let lo = self.arg_num(&args, 1, 0.0)?;
3196                let hi = self.arg_num(&args, 2, 1.0)?;
3197                return Ok(Value::Number(x.clamp(lo, hi)));
3198            },
3199
3200            // ── Constants (also accessible as plain identifiers via lookup) ──
3201            "pi" | "π" | "พาย" | "圆周率" | "円周率" | "파이" | "پی" | "باي" | "פאי" | "پائی" | "пи" => {
3202                return Ok(Value::Number(std::f64::consts::PI))
3203            },
3204            "tau" | "τ" | "双周率" | "タウ" | "타우" | "ทาว" | "تاو" | "تاو" | "טאו" | "ٹاؤ" | "тау" => {
3205                return Ok(Value::Number(std::f64::consts::TAU))
3206            },
3207
3208            // ══════════════════════════════════════════════════════════════════
3209            // PHASE 1: DMT TRIP CODER FEATURES
3210            // ══════════════════════════════════════════════════════════════════
3211
3212            // ── Step 1: Noise Functions ──
3213            "vnoise" | "noise2" | "นอยส์2ดี" | "柏林噪声2D" | "バリューノイズ2D" | "값노이즈2D" | "نویز_برداری" | "ضجيج_متجه" | "רעש_וקטורי" | "ویکٹر_نوائز" | "bruit_v" | "v_rauschen" | "шум_v" =>
3214            {
3215                let x = self.arg_num(&args, 0, 0.0)? as f32;
3216                let y = self.arg_num(&args, 1, 0.0)? as f32;
3217                let seed = self.arg_num(&args, 2, 0.0)? as u32;
3218                return Ok(Value::Number(tex_vnoise(x, y, seed) as f64));
3219            },
3220
3221            "fbm" | "นอยส์ออร์แกนิก" | "分形噪声" | "フラクタルノイズ" | "프랙탈노이즈" | "نویز_فراکتالی" | "ضجيج_عضوي" | "רעש_פרקטלי" | "فریکٹل_نوائز" =>
3222            {
3223                let x = self.arg_num(&args, 0, 0.0)? as f32;
3224                let y = self.arg_num(&args, 1, 0.0)? as f32;
3225                let octaves = self.arg_num(&args, 2, 4.0)? as u32;
3226                let seed = self.arg_num(&args, 3, 0.0)? as u32;
3227                return Ok(Value::Number(tex_fbm(x, y, octaves, seed) as f64));
3228            },
3229
3230            "perlin"
3231            | "perlin3"
3232            | "เพอร์ลิน3ดี"
3233            | "柏林噪声3D"
3234            | "パーリンノイズ3D"
3235            | "펄린노이즈3D" | "نویز_پرلین" | "بيرلين" | "רעש_פרלין" | "پرلن_نوائز" | "перлин" => {
3236                let x = self.arg_num(&args, 0, 0.0)? as f32;
3237                let y = self.arg_num(&args, 1, 0.0)? as f32;
3238                let z = self.arg_num(&args, 2, 0.0)? as f32;
3239                return Ok(Value::Number(perlin3(x, y, z) as f64));
3240            },
3241
3242            // ── Step 2: Math Ergonomics ──
3243            "lerp" | "ค่าระหว่าง" | "线性插值" | "線形補間" | "선형보간" | "میان‌یابی" | "استيفاء" | "אינטרפולציה" | "درمیانی_قدر" | "interpoler" | "interpolieren" | "интерполировать" =>
3244            {
3245                let a = self.arg_num(&args, 0, 0.0)?;
3246                let b = self.arg_num(&args, 1, 1.0)?;
3247                let t = self.arg_num(&args, 2, 0.0)?;
3248                return Ok(Value::Number(a + (b - a) * t));
3249            },
3250
3251            "smoothstep" | "เปลี่ยนแบบนุ่ม" | "平滑步进" | "スムーズステップ" | "스무스스텝" | "گام_نرم" | "تدرج_ناعم" | "מדרגה_חלקה" | "ہموار_قدم" | "lissage" | "glättung" | "сглаживание" =>
3252            {
3253                let lo = self.arg_num(&args, 0, 0.0)?;
3254                let hi = self.arg_num(&args, 1, 1.0)?;
3255                let x = self.arg_num(&args, 2, 0.5)?;
3256                let t = ((x - lo) / (hi - lo)).clamp(0.0, 1.0);
3257                return Ok(Value::Number(t * t * (3.0 - 2.0 * t)));
3258            },
3259
3260            "rand" | "สุ่ม" | "随机" | "乱数" | "난수" | "تصادفی" | "عشوائي" | "אקראי" | "بے_ترتیب" | "aléatoire" | "zufall" | "случайное" => {
3261                let val = fast_rand_f64(&mut self.rand_state);
3262                return Ok(Value::Number(val));
3263            },
3264
3265            "sign" | "เครื่องหมาย" | "符号" | "符号関数" | "부호" | "علامت" | "إشارة" | "סימן" | "نشان" | "signe" | "vorzeichen" | "знак" => {
3266                let x = self.arg_num(&args, 0, 0.0)?;
3267                return Ok(Value::Number(x.signum()));
3268            },
3269
3270            "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" =>
3271            {
3272                let h = self.arg_num(&args, 0, 0.0)?; // 0-360
3273                let s = self.arg_num(&args, 1, 1.0)?; // 0-1
3274                let v = self.arg_num(&args, 2, 1.0)?; // 0-1
3275                let c = v * s;
3276                let x = c * (1.0 - (((h / 60.0) % 2.0) - 1.0).abs());
3277                let m = v - c;
3278                let (r1, g1, b1) = if h < 60.0 {
3279                    (c, x, 0.0)
3280                } else if h < 120.0 {
3281                    (x, c, 0.0)
3282                } else if h < 180.0 {
3283                    (0.0, c, x)
3284                } else if h < 240.0 {
3285                    (0.0, x, c)
3286                } else if h < 300.0 {
3287                    (x, 0.0, c)
3288                } else {
3289                    (c, 0.0, x)
3290                };
3291                let r = ((r1 + m) * 255.0).round();
3292                let g = ((g1 + m) * 255.0).round();
3293                let b = ((b1 + m) * 255.0).round();
3294                return Ok(Value::List(Rc::new(vec![
3295                    Value::Number(r),
3296                    Value::Number(g),
3297                    Value::Number(b),
3298                ])));
3299            },
3300
3301            "lerp_color" | "ไล่สี" | "颜色插值" | "色補間" | "색보간" | "میان‌یابی_رنگ" | "استيفاء_اللون" | "אינטרפולציית_צבע" | "رنگ_درمیانی_قدر" | "interpoler_couleur" | "farbe_interpolieren" | "интерполировать_цвет" => {
3302                let r1 = self.arg_num(&args, 0, 0.0)?;
3303                let g1 = self.arg_num(&args, 1, 0.0)?;
3304                let b1 = self.arg_num(&args, 2, 0.0)?;
3305                let r2 = self.arg_num(&args, 3, 255.0)?;
3306                let g2 = self.arg_num(&args, 4, 255.0)?;
3307                let b2 = self.arg_num(&args, 5, 255.0)?;
3308                let t = self.arg_num(&args, 6, 0.0)?;
3309                let r = r1 + (r2 - r1) * t;
3310                let g = g1 + (g2 - g1) * t;
3311                let b = b1 + (b2 - b1) * t;
3312                let c = ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
3313                self.gfx.borrow_mut().color = c;
3314                return Ok(Value::Unit);
3315            },
3316
3317            // ── Step 3: Real-Time Clock ──
3318            "time_now" | "เวลาปัจจุบัน" | "当前时间" | "経過時間" | "현재시간" | "زمان_اکنون" | "الوقت_الآن" | "הזמן_עכשיו" | "ابھی_کا_وقت" | "temps_actuel" | "aktuelle_zeit" | "текущее_время" =>
3319            {
3320                return Ok(Value::Number(
3321                    crate::runtime::now_secs() - self.start_time_secs,
3322                ));
3323            },
3324
3325            // Wall-clock seconds since the Unix epoch (real date/time). Lets a
3326            // program defer deterministic-yet-evolving generation to the actual
3327            // datetime — same clock → same world, advancing as real time passes.
3328            "epoch_now" | "เวลาโลก" | "datetime" | "现在时刻" | "現在時刻" | "현재시각" | "مهر_زمانی_اکنون" | "طابع_الوقت_الآن" | "חותמת_זמן_עכשיו" | "ایپاک_وقت" =>
3329            {
3330                return Ok(Value::Number(crate::runtime::now_secs()));
3331            },
3332
3333            "frame_count" | "เฟรม" | "帧数" | "フレーム数" | "프레임수" | "شمار_فریم" | "عدد_الإطارات" | "ספירת_פריימים" | "فریم_شمار" | "nombre_images" | "bildanzahl" | "число_кадров" => {
3334                return Ok(Value::Number(self.frame_num as f64));
3335            },
3336
3337            // ── Step 4: Microphone Input ──
3338            "mic_open" | "เปิดไมค์" | "开麦克风" | "マイク開く" | "마이크열기" | "باز_کردن_میکروفون" | "افتح_الميكروفون" | "פתח_מיקרופון" | "مائیکروفون_کھولو" | "ouvrir_micro" | "mikrofon_öffnen" | "открыть_микрофон" =>
3339            {
3340                #[cfg(not(target_arch = "wasm32"))]
3341                {
3342                    match ling_mic::MicInput::open(Default::default()) {
3343                        Ok(mic) => {
3344                            let _ = mic.start(|_samples: &[f32]| {}); // No-op callback
3345                            self.mic = Some(mic);
3346                            return Ok(Value::Number(1.0)); // opened
3347                        },
3348                        // No device / permission denied → graceful: don't crash the game loop.
3349                        // Returns 0.0; mic_rms/mic_peak return 0.0 while self.mic is None.
3350                        Err(_e) => {
3351                            self.mic = None;
3352                            return Ok(Value::Number(0.0));
3353                        },
3354                    }
3355                }
3356                #[cfg(target_arch = "wasm32")]
3357                return Ok(Value::Unit);
3358            },
3359
3360            "mic_rms" | "เสียงRMS" | "麦克风音量" | "マイクRMS" | "마이크RMS" | "RMS_میکروفون" | "RMS_الميكروفون" | "RMS_מיקרופון" | "مائیکروفون_RMS" | "rms_micro" | "mikrofon_rms" | "rms_микрофона" =>
3361            {
3362                #[cfg(not(target_arch = "wasm32"))]
3363                {
3364                    let rms = self
3365                        .mic
3366                        .as_ref()
3367                        .map(|m: &ling_mic::MicInput| m.rms())
3368                        .unwrap_or(0.0);
3369                    return Ok(Value::Number(rms as f64));
3370                }
3371                #[cfg(target_arch = "wasm32")]
3372                return Ok(Value::Number(0.0));
3373            },
3374
3375            "mic_peak" | "เสียงพีค" | "麦克风峰值" | "マイクピーク" | "마이크피크" | "اوج_میکروفون" | "ذروة_الميكروفون" | "שיא_מיקרופון" | "مائیکروفون_چوٹی" | "crête_micro" | "mikrofon_spitze" | "пик_микрофона" =>
3376            {
3377                #[cfg(not(target_arch = "wasm32"))]
3378                {
3379                    let peak = self
3380                        .mic
3381                        .as_ref()
3382                        .map(|m: &ling_mic::MicInput| m.peak())
3383                        .unwrap_or(0.0);
3384                    return Ok(Value::Number(peak as f64));
3385                }
3386                #[cfg(target_arch = "wasm32")]
3387                return Ok(Value::Number(0.0));
3388            },
3389
3390            "mic_fft" | "วิเคราะห์เสียงสด" | "实时频谱" | "リアルタイムFFT" | "실시간FFT" | "FFT_میکروفون" | "FFT_الميكروفون" | "FFT_מיקרופון" | "مائیکروفون_FFT" | "fft_micro" | "mikrofon_fft" | "fft_микрофона" =>
3391            {
3392                #[cfg(not(target_arch = "wasm32"))]
3393                {
3394                    let n = self.arg_num(&args, 0, 8.0)? as usize;
3395                    if let Some(mic) = self.mic.as_ref() {
3396                        let samples = mic.latest_samples();
3397                        self.fft.borrow_mut().push_samples(&samples);
3398                    }
3399                    let bands = self.fft.borrow().freq_bands(n);
3400                    let result: Vec<Value> =
3401                        bands.iter().map(|&v| Value::Number(v as f64)).collect();
3402                    return Ok(Value::List(Rc::new(result)));
3403                }
3404                #[cfg(target_arch = "wasm32")]
3405                return Ok(Value::List(Vec::new().into()));
3406            },
3407
3408            // ── Step 5: Additive Blend Mode ──
3409            "set_blend" | "โหมดผสม" | "混合模式" | "ブレンドモード" | "블렌드모드" | "تنظیم_ترکیب" | "عيّن_المزج" | "קבע_מיזוג" | "بلینڈ_مقرر_کرو" | "définir_mélange" | "mischmodus_setzen" | "задать_смешивание" =>
3410            {
3411                let mode = self.arg_num(&args, 0, 0.0)? as u8;
3412                let mut gfx = self.gfx.borrow_mut();
3413                gfx.blend = mode;
3414                let a = gfx.alpha;
3415                gfx.depth_queue.set_state(mode, a); // 3-D queue captures blend for subsequent pushes
3416                return Ok(Value::Unit);
3417            },
3418
3419            // set_antialias(on) — smooth wireframe strokes (lines / edges / arcs /
3420            // circle outlines) via Xiaolin-Wu coverage. Default OFF = crisp,
3421            // opaque, aliased pixels; pass 1 to opt into smooth edges.
3422            "set_antialias" | "ตั้งลบรอยหยัก" | "抗锯齿" | "アンチエイリアス" | "안티에일리어싱" | "تنظیم_ضدلبه‌دندانه" | "عيّن_مضاد_التسنن" | "קבע_החלקת_קצוות" | "اینٹی_الائیسنگ_مقرر_کرو" =>
3423            {
3424                let on = self.arg_num(&args, 0, 1.0)? > 0.5;
3425                self.gfx.borrow_mut().antialias = on;
3426                return Ok(Value::Unit);
3427            },
3428            // get_antialias() -> bool — current wireframe anti-aliasing state.
3429            "get_antialias"
3430            | "อ่านลบรอยหยัก"
3431            | "读取抗锯齿"
3432            | "アンチエイリアス取得"
3433            | "안티에일리어싱상태" | "خواندن_ضدلبه‌دندانه" | "اقرأ_مضاد_التسنن" | "קרא_החלקת_קצוות" | "اینٹی_الائیسنگ_پڑھو" => {
3434                return Ok(Value::Bool(self.gfx.borrow().antialias));
3435            },
3436
3437            // set_font_antialias(on) — smooth `font_text`/`font_text_fill` glyph
3438            // edges, independent of `set_antialias` (which only covers wireframe
3439            // strokes). Default OFF = crisp, hard-edged text; pass 1 to opt in.
3440            "set_font_antialias" | "글꼴안티에일리어싱" => {
3441                let on = self.arg_num(&args, 0, 1.0)? > 0.5;
3442                self.gfx.borrow_mut().font_antialias = on;
3443                return Ok(Value::Unit);
3444            },
3445            // get_font_antialias() -> bool — current font anti-aliasing state.
3446            "get_font_antialias" | "글꼴안티에일리어싱상태" => {
3447                return Ok(Value::Bool(self.gfx.borrow().font_antialias));
3448            },
3449
3450            // ── Step 6: Circle Primitives ──
3451            "draw_circle" | "วาดวงกลม" | "画圆" | "円描画" | "원그리기" | "رسم_دایره" | "ارسم_دائرة" | "צייר_עיגול" | "دائرہ_کھینچو" | "dessiner_cercle" | "kreis_zeichnen" | "рисовать_круг" =>
3452            {
3453                let cx = self.arg_num(&args, 0, 0.0)? as i32;
3454                let cy = self.arg_num(&args, 1, 0.0)? as i32;
3455                let r = self.arg_num(&args, 2, 10.0)? as i32;
3456                let mut gfx = self.gfx.borrow_mut();
3457                let (w, h, color, blend) =
3458                    (gfx.width as i32, gfx.height as i32, gfx.color, gfx.blend);
3459                if gfx.antialias {
3460                    let (uw, uh) = (gfx.width, gfx.height);
3461                    let segs = ((r.max(1) as u32) * 4).clamp(24, 512);
3462                    crate::gfx::raster::draw_arc(
3463                        &mut gfx.buffer,
3464                        uw,
3465                        uh,
3466                        color,
3467                        true,
3468                        blend == 1,
3469                        cx as f32,
3470                        cy as f32,
3471                        r as f32,
3472                        0.0,
3473                        std::f32::consts::TAU,
3474                        segs,
3475                    );
3476                } else {
3477                    draw_circle_outline(&mut gfx.buffer, w, h, cx, cy, r, color, blend);
3478                }
3479                return Ok(Value::Unit);
3480            },
3481
3482            "draw_filled_circle"
3483            | "draw_disc"
3484            | "วาดวงกลมทึบ"
3485            | "画实心圆"
3486            | "塗りつぶし円"
3487            | "원채우기" | "رسم_دایره_توپر" | "ارسم_دائرة_ممتلئة" | "צייר_עיגול_מלא" | "بھرا_دائرہ_کھینچو" => {
3488                let cx = self.arg_num(&args, 0, 0.0)? as i32;
3489                let cy = self.arg_num(&args, 1, 0.0)? as i32;
3490                let r = self.arg_num(&args, 2, 10.0)? as i32;
3491                let mut gfx = self.gfx.borrow_mut();
3492                let (w, h, color, blend) =
3493                    (gfx.width as i32, gfx.height as i32, gfx.color, gfx.blend);
3494                draw_circle_filled(&mut gfx.buffer, w, h, cx, cy, r, color, blend);
3495                return Ok(Value::Unit);
3496            },
3497
3498            // draw_arc(cx, cy, r, a0, a1 [, segments]) — stroke a circular arc in
3499            // the pen colour (full circle when a1-a0 = TAU). Honors the antialias
3500            // flag; opaque by default (additive when blend = 1).
3501            "draw_arc" | "arc" | "วาดส่วนโค้ง" | "画弧" | "円弧描画" | "호그리기" | "رسم_کمان" | "ارسم_قوسا" | "צייר_קשת" | "آرک_کھینچو" =>
3502            {
3503                let cx = self.arg_num(&args, 0, 0.0)? as f32;
3504                let cy = self.arg_num(&args, 1, 0.0)? as f32;
3505                let r = self.arg_num(&args, 2, 10.0)? as f32;
3506                let a0 = self.arg_num(&args, 3, 0.0)? as f32;
3507                let a1 = self.arg_num(&args, 4, std::f64::consts::TAU)? as f32;
3508                let default_segs = ((r.abs() * (a1 - a0).abs()).ceil() as u32).clamp(8, 1024);
3509                let segs = self.arg_num(&args, 5, default_segs as f64)? as u32;
3510                let mut gfx = self.gfx.borrow_mut();
3511                let color = gfx.color;
3512                #[cfg(not(target_arch = "wasm32"))]
3513                {
3514                    let (uw, uh, aa, add) = (gfx.width, gfx.height, gfx.antialias, gfx.blend == 1);
3515                    crate::gfx::raster::draw_arc(
3516                        &mut gfx.buffer,
3517                        uw,
3518                        uh,
3519                        color,
3520                        aa,
3521                        add,
3522                        cx,
3523                        cy,
3524                        r,
3525                        a0,
3526                        a1,
3527                        segs,
3528                    );
3529                }
3530                #[cfg(target_arch = "wasm32")]
3531                {
3532                    let segs_f = segs.max(1);
3533                    let step = (a1 - a0) / segs_f as f32;
3534                    let mut px = cx + r * a0.cos();
3535                    let mut py = cy + r * a0.sin();
3536                    let mut i = 1u32;
3537                    while i <= segs_f {
3538                        let a = a0 + step * i as f32;
3539                        let nx = cx + r * a.cos();
3540                        let ny = cy + r * a.sin();
3541                        gfx.depth_queue.push_line(0.0, color, px, py, nx, ny);
3542                        px = nx;
3543                        py = ny;
3544                        i += 1;
3545                    }
3546                }
3547                return Ok(Value::Unit);
3548            },
3549
3550            // ── Step 7: Transparent fills, gradient surfaces & colored shadows ──
3551            // These all write straight into the software framebuffer (gfx.buffer)
3552            // on both native and web, so no target gating is needed.
3553
3554            // set_alpha(a) — pen opacity 0..1 for the alpha-blended fills below.
3555            "set_alpha" | "ตั้งความโปร่งใส" | "设透明" | "アルファ設定" | "투명도설정" | "تنظیم_شفافیت" | "عيّن_الشفافية" | "קבע_שקיפות" | "شفافیت_مقرر_کرو" | "définir_alpha" | "alpha_setzen" | "задать_альфа" =>
3556            {
3557                let a = self.arg_num(&args, 0, 1.0)? as f32;
3558                let mut gfx = self.gfx.borrow_mut();
3559                gfx.alpha = a.clamp(0.0, 1.0);
3560                let (m, al) = (gfx.blend, gfx.alpha);
3561                gfx.depth_queue.set_state(m, al); // 3-D queue captures alpha for subsequent pushes
3562                return Ok(Value::Unit);
3563            },
3564
3565            // mesh_hue(radians) — hue-rotate the baked per-tri colours of every
3566            // subsequent mesh_draw (.lmesh). 0 resets. Cheap: one matrix per call.
3567            "mesh_hue" | "หมุนสีเมช" | "فام_مش" | "صبغة_الشبكة" | "גוון_רשת" | "میش_ہیو" =>
3568            {
3569                let h = self.arg_num(&args, 0, 0.0)? as f32;
3570                let g = self.arg_num(&args, 1, 1.0)? as f32;
3571                let mut gfx = self.gfx.borrow_mut();
3572                gfx.mesh_hue = h;
3573                gfx.mesh_hue_gain = g.max(0.0);
3574                return Ok(Value::Unit);
3575            },
3576
3577            // set_frame_blur(amount 0..0.95) — afterimage trails: blend the previous
3578            // presented frame into each new one. 0 = off (also frees the ghost buffer).
3579            "set_frame_blur" | "frame_blur" | "เบลอเฟรม" | "تنظیم_تاری_فریم" | "عيّن_ضبابية_الإطار" | "קבע_טשטוש_פריים" | "فریم_بلر_مقرر_کرو" =>
3580            {
3581                let a = self.arg_num(&args, 0, 0.0)? as f32;
3582                let mut gfx = self.gfx.borrow_mut();
3583                gfx.frame_blur = a.clamp(0.0, 0.95);
3584                if gfx.frame_blur <= 0.0 {
3585                    gfx.prev_frame = Vec::new();
3586                }
3587                return Ok(Value::Unit);
3588            },
3589
3590            // set_line_hue_cycle(rate) — rapidly cycle the hue of ALL wireframe line
3591            // strokes (draw_line / draw_line_3d). `rate` in radians/sec; 0 = off.
3592            // Process-global so a single call covers every stroke, every frame.
3593            "set_line_hue_cycle" | "ตั้งวนสีเส้น" | "تنظیم_چرخه_فام_خط" | "عيّن_دورة_صبغة_الخط" | "קבע_מחזור_גוון_קו" | "لائن_ہیو_سائیکل_مقرر_کرو" => {
3594                let rate = self.arg_num(&args, 0, 0.0)?;
3595                crate::runtime::set_line_hue_rate(rate);
3596                return Ok(Value::Unit);
3597            },
3598
3599            // set_color_space(mode) — 0 = legacy sRGB compositing (default),
3600            // 1 = gamma-correct linear-light compositing (blend in linear, store
3601            // sRGB) so alpha and gradients don't darken/shift hue.
3602            "set_color_space" | "ปริภูมิสี" | "色彩空间" | "色空間" | "색공간" | "تنظیم_فضای_رنگ" | "عيّن_فضاء_اللون" | "קבע_מרחב_צבע" | "کلر_اسپیس_مقرر_کرو" | "définir_espace_couleur" | "farbraum_setzen" | "задать_цветовое_пространство" =>
3603            {
3604                let m = self.arg_num(&args, 0, 0.0)? as i64;
3605                self.gfx.borrow_mut().linear_blend = m != 0;
3606                return Ok(Value::Unit);
3607            },
3608
3609            // set_gradient_space(mode) — 1 = perceptual OkLab gradient interp
3610            // (default), 0 = legacy sRGB. Affects grad_triangle / grad_rect.
3611            "set_gradient_space" | "ปริภูมิไล่สี" | "渐变空间" | "グラデ空間" | "그라데이션공간" | "تنظیم_فضای_گرادیان" | "عيّن_فضاء_التدرج" | "קבע_מרחב_גרדיאנט" | "گریڈینٹ_اسپیس_مقرر_کرو" | "définir_espace_dégradé" | "verlaufsraum_setzen" | "задать_пространство_градиента" =>
3612            {
3613                let m = self.arg_num(&args, 0, 1.0)? as i64;
3614                self.gfx.borrow_mut().grad_oklab = m != 0;
3615                return Ok(Value::Unit);
3616            },
3617
3618            // mix_color(r0,g0,b0, r1,g1,b1, t) — set the pen colour to the
3619            // perceptual OkLab blend of two colours (t in 0..1). Far nicer
3620            // mid-tones than a raw RGB lerp.
3621            "mix_color" | "ผสมสี" | "混合颜色" | "色混合" | "색혼합" | "ترکیب_رنگ" | "امزج_اللون" | "ערבב_צבע" | "رنگ_ملاؤ" | "mélanger_couleur" | "farbe_mischen" | "смешать_цвет" => {
3622                let c0 = rgb(
3623                    self.arg_num(&args, 0, 0.0)?,
3624                    self.arg_num(&args, 1, 0.0)?,
3625                    self.arg_num(&args, 2, 0.0)?,
3626                );
3627                let c1 = rgb(
3628                    self.arg_num(&args, 3, 255.0)?,
3629                    self.arg_num(&args, 4, 255.0)?,
3630                    self.arg_num(&args, 5, 255.0)?,
3631                );
3632                let t = self.arg_num(&args, 6, 0.5)? as f32;
3633                self.gfx.borrow_mut().color = crate::gfx::color::mix_oklab(c0, c1, t);
3634                return Ok(Value::Unit);
3635            },
3636
3637            // set_depth_test(on) — enable the per-pixel z-buffer for the deferred
3638            // 3-D/queued draws (correct interpenetration) instead of painter's-
3639            // only sort. 0 = off (default), non-zero = on.
3640            "set_depth_test" | "ทดสอบความลึก" | "深度测试" | "深度テスト" | "깊이테스트" | "تنظیم_آزمون_عمق" | "عيّن_اختبار_العمق" | "קבע_בדיקת_עומק" | "ڈیپتھ_ٹیسٹ_مقرر_کرو" | "définir_test_profondeur" | "tiefentest_setzen" | "задать_тест_глубины" =>
3641            {
3642                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
3643                self.gfx.borrow_mut().depth_test = on;
3644                return Ok(Value::Unit);
3645            },
3646
3647            // set_flat_shade(on) / ตั้งแฟลตเชด — perf test: skip all per-triangle/mesh
3648            // lighting (compute_lit_color) and draw with the raw pen colour.
3649            "set_flat_shade" | "ตั้งแฟลตเชด" | "平面着色" | "フラット着色" | "평면음영" | "تنظیم_سایه‌پردازی_تخت" | "عيّن_تظليلا_مسطحا" | "קבע_הצללה_שטוחה" | "فلیٹ_شیڈ_مقرر_کرو" =>
3650            {
3651                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
3652                self.gfx.borrow_mut().flat_shade = on;
3653                return Ok(Value::Unit);
3654            },
3655
3656            // set_normal_override(x,y,z) - force subsequent triangle/mesh lighting
3657            // to use a stylized world-space normal until reset_normal_override().
3658            "set_normal_override" =>
3659            {
3660                let x = self.arg_num(&args, 0, 0.0)? as f32;
3661                let y = self.arg_num(&args, 1, -1.0)? as f32;
3662                let z = self.arg_num(&args, 2, 0.0)? as f32;
3663                self.gfx.borrow_mut().normal_override = Some([x, y, z]);
3664                return Ok(Value::Unit);
3665            },
3666
3667            "reset_normal_override" =>
3668            {
3669                self.gfx.borrow_mut().normal_override = None;
3670                return Ok(Value::Unit);
3671            },
3672
3673            // clear_depth() / ล้างความลึก — force the z-buffer to clear on the next
3674            // flush. `เติม` already does this; call explicitly to start a fresh
3675            // depth pass mid-frame (e.g. a separate overlay scene).
3676            "clear_depth" | "ล้างความลึก" | "清深度" | "深度クリア" | "깊이지우기" | "پاک‌کردن_عمق" | "امسح_العمق" | "נקה_עומק" | "گہرائی_صاف_کرو" =>
3677            {
3678                self.gfx.borrow_mut().zbuf_needs_clear = true;
3679                return Ok(Value::Unit);
3680            },
3681
3682            // depth_blur(focus, range, radius) / เบลอความลึก — depth-of-field post
3683            // pass over the framebuffer using the z-buffer: sharp at camera-space
3684            // depth `focus`, blurred up to `radius` px as depth departs by `range`.
3685            // Background (no geometry) blurs fully. Call AFTER `flush_3d` (so the
3686            // z-buffer is populated) and BEFORE `present`. Needs `set_depth_test(1)`.
3687            "depth_blur" | "เบลอความลึก" | "dof" | "depth_of_field" | "景深" | "تاری_عمق" | "ضبابية_العمق" | "טשטוש_עומק" | "ڈیپتھ_بلر" =>
3688            {
3689                let focus = self.arg_num(&args, 0, 30.0)? as f32;
3690                let range = self.arg_num(&args, 1, 60.0)? as f32;
3691                let radius = self.arg_num(&args, 2, 3.0)?.max(0.0) as usize;
3692                // oil [0..1] — oil-slick treatment of the blurred zone:
3693                // iridescent chroma fringe + hue swirl (water / heat haze).
3694                let oil = self.arg_num(&args, 3, 0.0)? as f32;
3695                let mut gfx = self.gfx.borrow_mut();
3696                let w = gfx.width;
3697                let h = gfx.height;
3698                if gfx.depth_buf.len() == w * h {
3699                    let g = &mut *gfx;
3700                    crate::gfx::raster::depth_of_field(
3701                        &mut g.buffer,
3702                        &g.depth_buf,
3703                        w,
3704                        h,
3705                        focus,
3706                        range,
3707                        radius,
3708                        oil,
3709                    );
3710                }
3711                return Ok(Value::Unit);
3712            },
3713
3714            // light_pool(x, y, z, radius, r, g, b, intensity) / แอ่งแสง —
3715            // volumetric light splash: a soft additive radial vector gradient on
3716            // the floor at height y — the coloured pool a light throws on the
3717            // ground (underwater-light look). Smooth transparent edge, distance-
3718            // fog aware. Colours 0-255; intensity ~0.2-1.5.
3719            "light_pool" | "แอ่งแสง" | "光池" | "ライトプール" | "빛웅덩이" | "برکه_نور" | "بركة_ضوء" | "בריכת_אור" | "روشنی_تالاب" | "bassin_lumière" | "lichtpfütze" | "лужа_света" =>
3720            {
3721                let x = self.arg_num(&args, 0, 0.0)? as f32;
3722                let y = self.arg_num(&args, 1, 0.0)? as f32;
3723                let z = self.arg_num(&args, 2, 0.0)? as f32;
3724                let radius = self.arg_num(&args, 3, 20.0)? as f32;
3725                let r = self.arg_num(&args, 4, 255.0)? as f32 / 255.0;
3726                let g = self.arg_num(&args, 5, 255.0)? as f32 / 255.0;
3727                let b = self.arg_num(&args, 6, 255.0)? as f32 / 255.0;
3728                let inten = self.arg_num(&args, 7, 1.0)? as f32;
3729                self.gfx
3730                    .borrow_mut()
3731                    .emit_light_pool(x, y, z, radius, [r, g, b], inten);
3732                return Ok(Value::Unit);
3733            },
3734
3735            // light_beam(x, y, z, floor_y, radius, r, g, b, intensity) / ลำแสงไฟ —
3736            // volumetric god-ray shaft: a soft additive double-cone from the
3737            // light position down to the floor plane, spreading to `radius`.
3738            // Pair with light_pool at the base. Colours 0-255.
3739            "light_beam" | "ลำแสงไฟ" | "光柱" | "ライトビーム" | "빛기둥" | "پرتو_نور" | "شعاع_ضوء" | "קרן_אור" | "روشنی_شعاع" | "faisceau_lumière" | "lichtstrahl" | "луч_света" =>
3740            {
3741                let x = self.arg_num(&args, 0, 0.0)? as f32;
3742                let y = self.arg_num(&args, 1, 0.0)? as f32;
3743                let z = self.arg_num(&args, 2, 0.0)? as f32;
3744                let fy = self.arg_num(&args, 3, 0.0)? as f32;
3745                let radius = self.arg_num(&args, 4, 14.0)? as f32;
3746                let r = self.arg_num(&args, 5, 255.0)? as f32 / 255.0;
3747                let g = self.arg_num(&args, 6, 255.0)? as f32 / 255.0;
3748                let b = self.arg_num(&args, 7, 255.0)? as f32 / 255.0;
3749                let inten = self.arg_num(&args, 8, 1.0)? as f32;
3750                self.gfx
3751                    .borrow_mut()
3752                    .emit_light_beam(x, y, z, fy, radius, [r, g, b], inten);
3753                return Ok(Value::Unit);
3754            },
3755
3756            // grad_triangle(x0,y0,r0,g0,b0, x1,y1,r1,g1,b1, x2,y2,r2,g2,b2)
3757            // Smooth per-vertex gradient triangle — a cheap lit surface: put the
3758            // bright colour on the vertex facing the light. Honours set_alpha.
3759            "grad_triangle" | "สามเหลี่ยมไล่สี" | "渐变三角" | "グラデ三角" | "그라데삼각" | "مثلث_گرادیان" | "مثلث_متدرج" | "משולש_גרדיאנט" | "گریڈینٹ_مثلث" | "triangle_dégradé" | "dreieck_verlauf" | "градиент_треугольник" =>
3760            {
3761                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
3762                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
3763                let c0 = rgb(
3764                    self.arg_num(&args, 2, 255.0)?,
3765                    self.arg_num(&args, 3, 255.0)?,
3766                    self.arg_num(&args, 4, 255.0)?,
3767                );
3768                let x1 = self.arg_num(&args, 5, 0.0)? as f32;
3769                let y1 = self.arg_num(&args, 6, 0.0)? as f32;
3770                let c1 = rgb(
3771                    self.arg_num(&args, 7, 255.0)?,
3772                    self.arg_num(&args, 8, 255.0)?,
3773                    self.arg_num(&args, 9, 255.0)?,
3774                );
3775                let x2 = self.arg_num(&args, 10, 0.0)? as f32;
3776                let y2 = self.arg_num(&args, 11, 0.0)? as f32;
3777                let c2 = rgb(
3778                    self.arg_num(&args, 12, 255.0)?,
3779                    self.arg_num(&args, 13, 255.0)?,
3780                    self.arg_num(&args, 14, 255.0)?,
3781                );
3782                let mut gfx = self.gfx.borrow_mut();
3783                let (w, h, alpha, mode, lin, ok) = (
3784                    gfx.width,
3785                    gfx.height,
3786                    gfx.alpha,
3787                    gfx.blend,
3788                    gfx.linear_blend,
3789                    gfx.grad_oklab,
3790                );
3791                crate::gfx::raster::fill_triangle_grad(
3792                    &mut gfx.buffer,
3793                    w,
3794                    h,
3795                    alpha,
3796                    mode,
3797                    lin,
3798                    ok,
3799                    x0,
3800                    y0,
3801                    c0,
3802                    x1,
3803                    y1,
3804                    c1,
3805                    x2,
3806                    y2,
3807                    c2,
3808                );
3809                return Ok(Value::Unit);
3810            },
3811
3812            // grad_rect(x,y,w,h, r0,g0,b0, r1,g1,b1, dir) — linear-gradient rect.
3813            // dir 0 = horizontal (left→right), else vertical (top→bottom).
3814            "grad_rect" | "สี่เหลี่ยมไล่สี" | "渐变矩形" | "グラデ矩形" | "그라데사각" | "مستطیل_گرادیان" | "مستطيل_متدرج" | "מלבן_גרדיאנט" | "گریڈینٹ_مستطیل" | "rectangle_dégradé" | "rechteck_verlauf" | "градиент_прямоугольник" =>
3815            {
3816                let x = self.arg_num(&args, 0, 0.0)? as f32;
3817                let y = self.arg_num(&args, 1, 0.0)? as f32;
3818                let rw = self.arg_num(&args, 2, 0.0)? as f32;
3819                let rh = self.arg_num(&args, 3, 0.0)? as f32;
3820                let c0 = rgb(
3821                    self.arg_num(&args, 4, 255.0)?,
3822                    self.arg_num(&args, 5, 255.0)?,
3823                    self.arg_num(&args, 6, 255.0)?,
3824                );
3825                let c1 = rgb(
3826                    self.arg_num(&args, 7, 0.0)?,
3827                    self.arg_num(&args, 8, 0.0)?,
3828                    self.arg_num(&args, 9, 0.0)?,
3829                );
3830                let dir = self.arg_num(&args, 10, 1.0)? as u8;
3831                let mut gfx = self.gfx.borrow_mut();
3832                let (w, h, alpha, mode, lin, ok) = (
3833                    gfx.width,
3834                    gfx.height,
3835                    gfx.alpha,
3836                    gfx.blend,
3837                    gfx.linear_blend,
3838                    gfx.grad_oklab,
3839                );
3840                crate::gfx::raster::fill_rect_grad(
3841                    &mut gfx.buffer,
3842                    w,
3843                    h,
3844                    alpha,
3845                    mode,
3846                    lin,
3847                    ok,
3848                    x,
3849                    y,
3850                    rw,
3851                    rh,
3852                    c0,
3853                    c1,
3854                    dir,
3855                );
3856                return Ok(Value::Unit);
3857            },
3858
3859            // shadow_blob(cx,cy, rx,ry, alpha) — soft colored shadow ellipse in
3860            // the current pen colour. Dark colour = normal shadow; any hue = a
3861            // tinted/coloured shadow. Edge softness comes from shadow_params.
3862            "shadow_blob" | "เงาวงรี" | "阴影斑" | "影ブロブ" | "그림자블롭" | "لکه_سایه" | "بقعة_ظل" | "כתם_צל" | "سایہ_دھبہ" | "tache_ombre" | "schattenklecks" | "пятно_тени" =>
3863            {
3864                let cx = self.arg_num(&args, 0, 0.0)? as f32;
3865                let cy = self.arg_num(&args, 1, 0.0)? as f32;
3866                let rx = self.arg_num(&args, 2, 16.0)? as f32;
3867                let ry = self.arg_num(&args, 3, 8.0)? as f32;
3868                let a = self.arg_num(&args, 4, 0.5)? as f32;
3869                let mut gfx = self.gfx.borrow_mut();
3870                let (w, h, color, soft, mode, lin) = (
3871                    gfx.width,
3872                    gfx.height,
3873                    gfx.color,
3874                    gfx.shadow.soft,
3875                    gfx.blend,
3876                    gfx.linear_blend,
3877                );
3878                crate::gfx::raster::fill_disc_soft(
3879                    &mut gfx.buffer,
3880                    w,
3881                    h,
3882                    cx,
3883                    cy,
3884                    rx,
3885                    ry,
3886                    color,
3887                    a,
3888                    soft,
3889                    mode,
3890                    lin,
3891                );
3892                return Ok(Value::Unit);
3893            },
3894
3895            // cast_shadow(cx,cy, height) — height-driven contact shadow in the
3896            // current pen colour. Closer to the surface (small height) = smaller,
3897            // darker, sharper; farther (large height) = bigger, fainter, softer.
3898            // Tune the ramp with shadow_params.
3899            "cast_shadow" | "ทอดเงา" | "投射阴影" | "影を落とす" | "그림자드리우기" | "افکندن_سایه" | "ألقِ_ظلا" | "הטל_צל" | "سایہ_ڈالو" | "projeter_ombre" | "schatten_werfen" | "отбросить_тень" =>
3900            {
3901                let cx = self.arg_num(&args, 0, 0.0)? as f32;
3902                let cy = self.arg_num(&args, 1, 0.0)? as f32;
3903                let height = (self.arg_num(&args, 2, 0.0)? as f32).max(0.0);
3904                let mut gfx = self.gfx.borrow_mut();
3905                let sp = gfx.shadow;
3906                let radius = (sp.base + sp.grow * height).max(0.5);
3907                let alpha = (sp.alpha - sp.fade * height).clamp(0.04, 1.0);
3908                let soft = (sp.soft + height * 0.004).clamp(0.0, 0.95);
3909                let (w, h, color, mode, lin) = (
3910                    gfx.width,
3911                    gfx.height,
3912                    gfx.color,
3913                    gfx.blend,
3914                    gfx.linear_blend,
3915                );
3916                crate::gfx::raster::fill_disc_soft(
3917                    &mut gfx.buffer,
3918                    w,
3919                    h,
3920                    cx,
3921                    cy,
3922                    radius,
3923                    radius * 0.62,
3924                    color,
3925                    alpha,
3926                    soft,
3927                    mode,
3928                    lin,
3929                );
3930                return Ok(Value::Unit);
3931            },
3932
3933            // shadow_params(base, grow, alpha, fade, soft) — tune cast_shadow.
3934            // Each arg defaults to the current value, so you can set just one.
3935            "shadow_params" | "ตั้งค่าเงา" | "阴影参数" | "影設定" | "그림자설정" | "پارامترهای_سایه" | "معاملات_الظل" | "פרמטרי_צל" | "سایہ_پیرامیٹرز" | "paramètres_ombre" | "schattenparameter" | "параметры_тени" =>
3936            {
3937                let cur = self.gfx.borrow().shadow;
3938                let base = self.arg_num(&args, 0, cur.base as f64)? as f32;
3939                let grow = self.arg_num(&args, 1, cur.grow as f64)? as f32;
3940                let alpha = self.arg_num(&args, 2, cur.alpha as f64)? as f32;
3941                let fade = self.arg_num(&args, 3, cur.fade as f64)? as f32;
3942                let soft = self.arg_num(&args, 4, cur.soft as f64)? as f32;
3943                self.gfx.borrow_mut().shadow =
3944                    crate::gfx::ShadowParams { base, grow, alpha, fade, soft };
3945                return Ok(Value::Unit);
3946            },
3947
3948            // depth_triangle(x0,y0, x1,y1, x2,y2, z) — queue a depth-sorted tri in
3949            // the current colour. Drawn back-to-front (painter's algorithm) at
3950            // present(); larger z = farther away. Lets 2-D sprites/quads sort by
3951            // depth the same way 3-D faces do.
3952            "depth_triangle" | "สามเหลี่ยมเรียงลึก" | "深度三角" | "深度三角形" | "깊이삼각" | "مثلث_عمق" | "مثلث_العمق" | "משולש_עומק" | "گہرائی_مثلث" | "triangle_profondeur" | "tiefendreieck" | "глубина_треугольник" =>
3953            {
3954                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
3955                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
3956                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
3957                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
3958                let x2 = self.arg_num(&args, 4, 0.0)? as f32;
3959                let y2 = self.arg_num(&args, 5, 0.0)? as f32;
3960                let z = self.arg_num(&args, 6, 0.0)? as f32;
3961                let mut gfx = self.gfx.borrow_mut();
3962                let color = gfx.color;
3963                gfx.depth_queue
3964                    .push_triangle(z, color, x0, y0, x1, y1, x2, y2);
3965                return Ok(Value::Unit);
3966            },
3967
3968            // depth_line(x0,y0, x1,y1, z) — queue a depth-sorted line in the
3969            // current colour (same painter's queue as depth_triangle).
3970            "depth_line" | "เส้นเรียงลึก" | "深度线" | "深度線" | "깊이선" | "خط_عمق" | "خط_العمق" | "קו_עומק" | "گہرائی_لکیر" | "ligne_profondeur" | "tiefenlinie" | "глубина_линия" =>
3971            {
3972                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
3973                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
3974                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
3975                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
3976                let z = self.arg_num(&args, 4, 0.0)? as f32;
3977                let mut gfx = self.gfx.borrow_mut();
3978                let color = gfx.color;
3979                gfx.depth_queue.push_line(z, color, x0, y0, x1, y1);
3980                return Ok(Value::Unit);
3981            },
3982
3983            // ══════════════════════════════════════════════════════════════════
3984            // GRAPHICS BUILTINS
3985            // Thai names first, then English aliases.
3986            // ══════════════════════════════════════════════════════════════════
3987
3988            // ── เปิดหน้าต่าง(width, height, title) — open_window ──
3989            "เปิดหน้าต่าง" | "open_window" | "gfx_window" | "开窗" | "ウィンドウ開く" | "창열기" | "باز_کردن_پنجره" | "افتح_نافذة" | "פתח_חלון" | "ونڈو_کھولو" =>
3990            {
3991                let w = self.arg_num(&args, 0, 800.0)? as usize;
3992                let h = self.arg_num(&args, 1, 600.0)? as usize;
3993                #[cfg(not(target_arch = "wasm32"))]
3994                {
3995                    let title = args
3996                        .get(2)
3997                        .map(|v| v.to_string())
3998                        .unwrap_or_else(|| "Ling".into());
3999                    let mut gfx = self.gfx.borrow_mut();
4000                    let mut win = minifb::Window::new(
4001                        &title,
4002                        w,
4003                        h,
4004                        minifb::WindowOptions {
4005                            resize: false,
4006                            scale: minifb::Scale::X1,
4007                            ..Default::default()
4008                        },
4009                    )
4010                    .map_err(|e| EvalErr::from(format!("cannot open window: {e}")))?;
4011                    apply_frame_pacing(&mut win, gfx.vsync);
4012                    gfx.buffer = vec![0u32; w * h];
4013                    gfx.width = w;
4014                    gfx.height = h;
4015                    gfx.window = Some(win);
4016                    gfx.topmost_window = false;
4017                    gfx.sync_projection();
4018                    hide_console_window();
4019                }
4020                #[cfg(target_arch = "wasm32")]
4021                {
4022                    let mut gfx = self.gfx.borrow_mut();
4023                    gfx.width = w;
4024                    gfx.height = h;
4025                    gfx.buffer.resize(w * h, 0); // keep the CPU framebuffer in sync
4026                    gfx.sync_projection();
4027                    crate::gfx::webgl::resize(w as u32, h as u32);
4028                }
4029                return Ok(Value::Unit);
4030            },
4031
4032            // ── เติม(r, g, b) — fill / clear screen with colour ──
4033            "เติม" | "fill" | "gfx_fill" | "clear" | "填" | "塗り潰し" | "채우기" | "清"
4034            | "消去" | "지우기" | "پر_کن" | "املأ" | "מלא" | "بھرو" => {
4035                let r = self.arg_num(&args, 0, 0.0)? as u32;
4036                let g = self.arg_num(&args, 1, 0.0)? as u32;
4037                let b = self.arg_num(&args, 2, 0.0)? as u32;
4038                #[cfg(not(target_arch = "wasm32"))]
4039                {
4040                    let c = (r << 16) | (g << 8) | b;
4041                    let mut gfx = self.gfx.borrow_mut();
4042                    gfx.buffer.fill(c);
4043                    gfx.zbuf_needs_clear = true; // clear color ⇒ clear depth next flush
4044                    gfx.edge_set.clear(); // reset shared-edge dedup for new frame
4045                }
4046                #[cfg(target_arch = "wasm32")]
4047                {
4048                    let mut gfx = self.gfx.borrow_mut();
4049                    gfx.fill_r = r as f32 / 255.0;
4050                    gfx.fill_g = g as f32 / 255.0;
4051                    gfx.fill_b = b as f32 / 255.0;
4052                    let c = (r << 16) | (g << 8) | b;
4053                    gfx.buffer.fill(c);
4054                    gfx.zbuf_needs_clear = true;
4055                    gfx.edge_set.clear();
4056                }
4057                return Ok(Value::Unit);
4058            },
4059
4060            // ── set_color_hsl(h, s, l) — set drawing colour from HSL ──
4061            // h: 0–360 degrees, s: 0–100 saturation, l: 0–100 lightness
4062            "set_color_hsl" | "颜色HSL" | "色相" | "HSL色" | "HSL색설정" | "สีHSLวาด" | "تنظیم_رنگ_HSL" | "عيّن_اللون_HSL" | "קבע_צבע_HSL" | "HSL_رنگ_مقرر_کرو" | "définir_couleur_hsl" | "farbe_hsl_setzen" | "задать_цвет_hsl" =>
4063            {
4064                let h = self.arg_num(&args, 0, 0.0)?;
4065                let s = self.arg_num(&args, 1, 70.0)?;
4066                let l = self.arg_num(&args, 2, 50.0)?;
4067                let hex = hsl_to_hex(h, s, l);
4068                let r = u32::from_str_radix(&hex[1..3], 16).unwrap_or(255);
4069                let g = u32::from_str_radix(&hex[3..5], 16).unwrap_or(255);
4070                let b = u32::from_str_radix(&hex[5..7], 16).unwrap_or(255);
4071                self.gfx.borrow_mut().color = (r << 16) | (g << 8) | b;
4072                return Ok(Value::Unit);
4073            },
4074
4075            // ── สีดินสอ(r, g, b) — set drawing colour ──
4076            "สีดินสอ" | "set_color" | "gfx_color" | "color" | "设色" | "色設定" | "색설정" | "تنظیم_رنگ" | "عيّن_اللون" | "קבע_צבע" | "رنگ_مقرر_کرو" =>
4077            {
4078                let r = self.arg_num(&args, 0, 255.0)? as u32;
4079                let g = self.arg_num(&args, 1, 255.0)? as u32;
4080                let b = self.arg_num(&args, 2, 255.0)? as u32;
4081                self.gfx.borrow_mut().color = (r << 16) | (g << 8) | b;
4082                return Ok(Value::Unit);
4083            },
4084
4085            // ── วาดสามเหลี่ยม(x1,y1, x2,y2, x3,y3) — draw filled triangle ──
4086            "วาดสามเหลี่ยม"
4087            | "draw_triangle"
4088            | "gfx_triangle"
4089            | "triangle"
4090            | "画三角"
4091            | "三角形描画"
4092            | "삼각형그리기" | "رسم_مثلث" | "ارسم_مثلثا" | "צייר_משולש" | "مثلث_کھینچو" => {
4093                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
4094                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
4095                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
4096                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
4097                let x2 = self.arg_num(&args, 4, 0.0)? as f32;
4098                let y2 = self.arg_num(&args, 5, 0.0)? as f32;
4099                let mut gfx = self.gfx.borrow_mut();
4100                let color = gfx.color;
4101                #[cfg(not(target_arch = "wasm32"))]
4102                {
4103                    let w = gfx.width;
4104                    let h = gfx.height;
4105                    fill_triangle(&mut gfx.buffer, w, h, color, x0, y0, x1, y1, x2, y2);
4106                }
4107                #[cfg(target_arch = "wasm32")]
4108                gfx.depth_queue
4109                    .push_triangle(0.0, color, x0, y0, x1, y1, x2, y2);
4110                return Ok(Value::Unit);
4111            },
4112
4113            // ── วาดเส้น(x1,y1, x2,y2) — draw line ──
4114            "วาดเส้น" | "draw_line" | "gfx_line" | "line" | "画线" | "線描く" | "선그리기" | "رسم_خط" | "ارسم_خط" | "צייר_קו" | "لکیر_کھینچو" =>
4115            {
4116                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
4117                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
4118                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
4119                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
4120                let mut gfx = self.gfx.borrow_mut();
4121                let color = gfx.color;
4122                #[cfg(not(target_arch = "wasm32"))]
4123                {
4124                    let w = gfx.width;
4125                    let h = gfx.height;
4126                    let aa = gfx.antialias;
4127                    let add = gfx.blend == 1;
4128                    if aa {
4129                        crate::gfx::raster::draw_line_aa(
4130                            &mut gfx.buffer,
4131                            w,
4132                            h,
4133                            color,
4134                            add,
4135                            x0,
4136                            y0,
4137                            x1,
4138                            y1,
4139                        );
4140                    } else {
4141                        draw_line(&mut gfx.buffer, w, h, color, x0, y0, x1, y1);
4142                    }
4143                }
4144                #[cfg(target_arch = "wasm32")]
4145                gfx.depth_queue.push_line(0.0, color, x0, y0, x1, y1);
4146                return Ok(Value::Unit);
4147            },
4148
4149            // ── วาดจุด(x, y) — plot a single pixel ──
4150            "วาดจุด" | "draw_pixel" | "gfx_pixel" | "pixel" | "画点" | "点描く" | "점그리기" | "رسم_نقطه" | "ارسم_نقطة" | "צייר_פיקסל" | "پکسل_کھینچو" =>
4151            {
4152                let px = self.arg_num(&args, 0, 0.0)? as i32;
4153                let py = self.arg_num(&args, 1, 0.0)? as i32;
4154                #[cfg(not(target_arch = "wasm32"))]
4155                {
4156                    let mut gfx = self.gfx.borrow_mut();
4157                    let color = gfx.color;
4158                    let w = gfx.width;
4159                    let h = gfx.height;
4160                    if px >= 0 && py >= 0 && (px as usize) < w && (py as usize) < h {
4161                        gfx.buffer[py as usize * w + px as usize] = color;
4162                    }
4163                }
4164                #[cfg(target_arch = "wasm32")]
4165                {
4166                    // Render pixel as a 1×1 square via two triangles.
4167                    let mut gfx = self.gfx.borrow_mut();
4168                    let color = gfx.color;
4169                    let x = px as f32;
4170                    let y = py as f32;
4171                    gfx.depth_queue
4172                        .push_triangle(0.0, color, x, y, x + 1.0, y, x + 1.0, y + 1.0);
4173                    gfx.depth_queue
4174                        .push_triangle(0.0, color, x, y, x + 1.0, y + 1.0, x, y + 1.0);
4175                }
4176                return Ok(Value::Unit);
4177            },
4178
4179            // ── แสดงผล() — flush depth queue, then present frame to screen ──
4180            "แสดงผล" | "present" | "gfx_present" | "show" | "显" | "呈现" | "表示" | "표시" | "نمایش" | "اعرض" | "הצג" | "دکھاؤ" =>
4181            {
4182                // Click-edge widgets (ui_button etc.) compare THIS frame's
4183                // mouse_now() against `mouse_was_down` to detect a fresh
4184                // press. That comparison only works if mouse_was_down
4185                // reflects what the script itself observed this frame — i.e.
4186                // the state from BEFORE update_with_buffer below pulls in new
4187                // OS events. Capturing it after (as the "freshest" read)
4188                // would mean a just-arrived click is already baked into
4189                // mouse_was_down by the time next frame's ui_button compares
4190                // against it, so `down && !mouse_was_down` is never true and
4191                // clicks never register at all. Declared at the top of the
4192                // match arm (not inside the block below) so it survives past
4193                // the wasm32/non-wasm32 split further down.
4194                #[cfg(not(target_arch = "wasm32"))]
4195                let pre_update_mouse_down = self
4196                    .gfx
4197                    .borrow()
4198                    .window
4199                    .as_ref()
4200                    .map(|w| w.get_mouse_down(minifb::MouseButton::Left))
4201                    .unwrap_or(false);
4202                #[cfg(not(target_arch = "wasm32"))]
4203                {
4204                    ling_fps_tick();
4205                    ling_phase_frame();
4206                    // Flush depth queue and present — release borrow before reading mouse.
4207                    {
4208                        let mut gfx = self.gfx.borrow_mut();
4209                        if !gfx.depth_queue.is_empty() {
4210                            let w = gfx.width;
4211                            let h = gfx.height;
4212                            let dt = gfx.depth_test;
4213                            let reset_z = gfx.zbuf_needs_clear;
4214                            let (bm, ba) = (gfx.blend, gfx.alpha);
4215                            let aa = gfx.antialias;
4216                            let queue = std::mem::take(&mut gfx.depth_queue);
4217                            {
4218                                let g = &mut *gfx;
4219                                let z = if dt { Some(&mut g.depth_buf) } else { None };
4220                                queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
4221                            }
4222                            gfx.depth_queue.set_state(bm, ba);
4223                            gfx.zbuf_needs_clear = false;
4224                        }
4225                        let _t = std::time::Instant::now();
4226                        if !gfx.post_done {
4227                            gfx.toon_post_process();
4228                        }
4229                        gfx.post_done = false;
4230                        ling_phase_add(phase::TOON, _t.elapsed().as_nanos());
4231                        let w = gfx.width;
4232                        let h = gfx.height;
4233                        let g = &mut *gfx;
4234                        if g.frame_blur > 0.0 {
4235                            // Afterimage trails: previous frame decays by `frame_blur`
4236                            // per frame and composites with MAX — fresh content stays
4237                            // full-brightness, ghosts fade out over time.
4238                            // retention 0.98 @60fps ≈ trails last ~2.6 s.
4239                            let a = (g.frame_blur.clamp(0.0, 0.995) * 256.0) as u32;
4240                            if g.prev_frame.len() != g.buffer.len() {
4241                                g.prev_frame = g.buffer.clone();
4242                            }
4243                            for (dst, prev) in g.buffer.iter_mut().zip(g.prev_frame.iter_mut()) {
4244                                let c = *dst;
4245                                let pv = *prev;
4246                                let pr = (((pv >> 16) & 0xFF) * a) >> 8;
4247                                let pg = (((pv >> 8) & 0xFF) * a) >> 8;
4248                                let pb = ((pv & 0xFF) * a) >> 8;
4249                                let cr = (c >> 16) & 0xFF;
4250                                let cg = (c >> 8) & 0xFF;
4251                                let cb = c & 0xFF;
4252                                let outp = (cr.max(pr) << 16) | (cg.max(pg) << 8) | cb.max(pb);
4253                                *dst = outp;
4254                                *prev = outp;
4255                            }
4256                        }
4257                        if let Some(win) = g.window.as_mut() {
4258                            let _b = std::time::Instant::now();
4259                            win.update_with_buffer(&g.buffer, w, h)
4260                                .map_err(|e| EvalErr::from(format!("present error: {e}")))?;
4261                            ling_phase_add(phase::BLIT, _b.elapsed().as_nanos());
4262                        }
4263                    }
4264                    // Read mouse AFTER update_with_buffer so events are processed.
4265                    let mouse_pos = {
4266                        let gfx = self.gfx.borrow();
4267                        gfx.window
4268                            .as_ref()
4269                            .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
4270                    };
4271                    let mut gfx = self.gfx.borrow_mut();
4272                    if gfx.mouse_captured {
4273                        let w = gfx.width as f32;
4274                        let h = gfx.height as f32;
4275                        if let Some((mx, my)) = mouse_pos {
4276                            if gfx.last_mx.is_nan() {
4277                                gfx.mouse_dx = 0.0;
4278                                gfx.mouse_dy = 0.0;
4279                                gfx.last_mx = mx;
4280                                gfx.last_my = my;
4281                            } else {
4282                                gfx.mouse_dx = mx - gfx.last_mx;
4283                                gfx.mouse_dy = my - gfx.last_my;
4284                                // Wrap the cursor at every edge (L/R/U/D) → infinite look
4285                                // on both axes, and the cursor is NOT trapped (alt-tab works).
4286                                let margin = 6.0;
4287                                let (mut nx, mut ny, mut warp) = (mx, my, false);
4288                                if mx < margin {
4289                                    nx = w - margin - 2.0;
4290                                    warp = true;
4291                                } else if mx > w - margin {
4292                                    nx = margin + 2.0;
4293                                    warp = true;
4294                                }
4295                                if my < margin {
4296                                    ny = h - margin - 2.0;
4297                                    warp = true;
4298                                } else if my > h - margin {
4299                                    ny = margin + 2.0;
4300                                    warp = true;
4301                                }
4302                                if warp {
4303                                    #[cfg(windows)]
4304                                    unsafe {
4305                                        #[repr(C)]
4306                                        struct RECT {
4307                                            left: i32,
4308                                            top: i32,
4309                                            right: i32,
4310                                            bottom: i32,
4311                                        }
4312                                        extern "system" {
4313                                            fn GetForegroundWindow() -> isize;
4314                                            fn GetWindowRect(hwnd: isize, lpRect: *mut RECT)
4315                                                -> i32;
4316                                            fn SetCursorPos(x: i32, y: i32) -> i32;
4317                                        }
4318                                        let hwnd = GetForegroundWindow();
4319                                        let mut rect =
4320                                            RECT { left: 0, top: 0, right: 0, bottom: 0 };
4321                                        if GetWindowRect(hwnd, &mut rect) != 0 {
4322                                            SetCursorPos(
4323                                                rect.left + nx as i32,
4324                                                rect.top + ny as i32,
4325                                            );
4326                                        }
4327                                    }
4328                                    gfx.last_mx = nx;
4329                                    gfx.last_my = ny;
4330                                } else {
4331                                    gfx.last_mx = mx;
4332                                    gfx.last_my = my;
4333                                }
4334                            }
4335                        } else {
4336                            gfx.mouse_dx = 0.0;
4337                            gfx.mouse_dy = 0.0;
4338                        }
4339                    } else if let Some((mx, my)) = mouse_pos {
4340                        if gfx.last_mx.is_nan() {
4341                            gfx.mouse_dx = 0.0;
4342                            gfx.mouse_dy = 0.0;
4343                        } else {
4344                            gfx.mouse_dx = mx - gfx.last_mx;
4345                            gfx.mouse_dy = my - gfx.last_my;
4346                        }
4347                        gfx.last_mx = mx;
4348                        gfx.last_my = my;
4349                    } else {
4350                        gfx.mouse_dx = 0.0;
4351                        gfx.mouse_dy = 0.0;
4352                    }
4353
4354                    // Alt-tab support: minifb has no WM_KILLFOCUS handler on Windows,
4355                    // so a key/button released while another window was focused can
4356                    // still read as "down" for one stale frame right after the user
4357                    // alt-tabs back. Detect the unfocused→focused transition and
4358                    // swallow raw input for a short grace window afterward instead of
4359                    // letting a phantom held key jerk the camera. See key_down /
4360                    // mouse_down* below (they early-out on gfx.input_suppressed()).
4361                    let is_active = gfx.window.as_mut().map(|w| w.is_active()).unwrap_or(true);
4362                    if is_active && !gfx.was_active {
4363                        gfx.focus_grace_frames = 5;
4364                        // Regained focus: restore HWND_TOPMOST so the borderless-
4365                        // fullscreen window covers the taskbar again.
4366                        #[cfg(windows)]
4367                        if gfx.topmost_window {
4368                            if let Some(w) = gfx.window.as_ref() {
4369                                set_window_topmost(w.get_window_handle() as isize, true);
4370                            }
4371                        }
4372                    } else if !is_active && gfx.was_active {
4373                        // Lost focus (alt-tab): drop topmost so the game stops
4374                        // covering whatever window the user just switched to —
4375                        // otherwise a topmost borderless window visually "wins"
4376                        // even though it's no longer focused, making alt-tab
4377                        // look broken.
4378                        #[cfg(windows)]
4379                        if gfx.topmost_window {
4380                            if let Some(w) = gfx.window.as_ref() {
4381                                set_window_topmost(w.get_window_handle() as isize, false);
4382                            }
4383                        }
4384                    }
4385                    gfx.was_active = is_active;
4386                    if gfx.focus_grace_frames > 0 {
4387                        gfx.focus_grace_frames -= 1;
4388                    }
4389                }
4390                #[cfg(target_arch = "wasm32")]
4391                {
4392                    {
4393                        // Software-render everything (3-D depth queue + 2-D vtex/ui that
4394                        // already wrote into the buffer) into the framebuffer, exactly
4395                        // like native, then upload that buffer to the canvas in one blit.
4396                        let mut gfx = self.gfx.borrow_mut();
4397                        let w = gfx.width;
4398                        let h = gfx.height;
4399                        if gfx.buffer.len() != w * h {
4400                            gfx.buffer.resize(w * h, 0);
4401                        }
4402                        if !gfx.depth_queue.is_empty() {
4403                            let dt = gfx.depth_test;
4404                            let reset_z = gfx.zbuf_needs_clear;
4405                            let aa = gfx.antialias;
4406                            let queue = std::mem::take(&mut gfx.depth_queue);
4407                            {
4408                                let g = &mut *gfx;
4409                                let z = if dt { Some(&mut g.depth_buf) } else { None };
4410                                queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
4411                            }
4412                            gfx.zbuf_needs_clear = false;
4413                        }
4414                        if !gfx.post_done {
4415                            gfx.toon_post_process();
4416                        }
4417                        gfx.post_done = false;
4418                        crate::gfx::webgl::blit_rgb(&gfx.buffer, w, h);
4419                    }
4420                    self.wasm_pace_frame();
4421                }
4422                // Update the click-edge latch for interactive UI widgets —
4423                // using the PRE-update_with_buffer snapshot captured at the
4424                // top of this function (see the comment there for why).
4425                #[cfg(not(target_arch = "wasm32"))]
4426                {
4427                    self.mouse_was_down = pre_update_mouse_down;
4428                }
4429                // Increment frame counter
4430                self.frame_num += 1;
4431                return Ok(Value::Unit);
4432            },
4433
4434            // ── เปิดหน้าต่างเต็มจอ(title) — true native-res fullscreen window ──
4435            "เปิดหน้าต่างเต็มจอ"
4436            | "open_fullscreen"
4437            | "fullscreen"
4438            | "全屏"
4439            | "全画面"
4440            | "전체화면" | "باز_کردن_تمام‌صفحه" | "افتح_ملء_الشاشة" | "פתח_מסך_מלא" | "فل_سکرین_کھولو" => {
4441                // In WASM the canvas defines the viewport; use its current size
4442                // as the default so the projection matches what's actually visible.
4443                #[cfg(target_arch = "wasm32")]
4444                let (default_w, default_h) = {
4445                    let (cw, ch) = crate::gfx::webgl::canvas_size();
4446                    (cw as f64, ch as f64)
4447                };
4448                // On native: query the actual primary monitor resolution.
4449                #[cfg(all(not(target_arch = "wasm32"), windows))]
4450                let (default_w, default_h) = unsafe {
4451                    extern "system" {
4452                        fn GetSystemMetrics(nIndex: i32) -> i32;
4453                    }
4454                    (GetSystemMetrics(0) as f64, GetSystemMetrics(1) as f64)
4455                };
4456                #[cfg(all(not(target_arch = "wasm32"), not(windows)))]
4457                let (default_w, default_h) = native_screen_size();
4458
4459                let w = args
4460                    .get(1)
4461                    .map(|v| self.to_number(v).unwrap_or(default_w) as usize)
4462                    .unwrap_or(default_w as usize);
4463                let h = args
4464                    .get(2)
4465                    .map(|v| self.to_number(v).unwrap_or(default_h) as usize)
4466                    .unwrap_or(default_h as usize);
4467                #[cfg(not(target_arch = "wasm32"))]
4468                {
4469                    let title = args
4470                        .first()
4471                        .map(|v| v.to_string())
4472                        .unwrap_or_else(|| "Ling".into());
4473                    let mut gfx = self.gfx.borrow_mut();
4474                    let mut win = minifb::Window::new(
4475                        &title,
4476                        w,
4477                        h,
4478                        minifb::WindowOptions {
4479                            borderless: true,
4480                            title: false,
4481                            resize: false,
4482                            topmost: true,
4483                            scale: minifb::Scale::X1,
4484                            ..Default::default()
4485                        },
4486                    )
4487                    .map_err(|e| EvalErr::from(format!("cannot open fullscreen: {e}")))?;
4488                    apply_frame_pacing(&mut win, gfx.vsync);
4489                    // Grab the native handle *before* moving the window into gfx.
4490                    #[cfg(windows)]
4491                    let hwnd = win.get_window_handle() as isize;
4492                    gfx.buffer = vec![0u32; w * h];
4493                    gfx.width = w;
4494                    gfx.height = h;
4495                    gfx.window = Some(win);
4496                    gfx.topmost_window = true;
4497                    #[cfg(windows)]
4498                    {
4499                        gfx.hwnd = hwnd;
4500                    }
4501                    gfx.sync_projection();
4502                    // Strip all chrome and cover the full screen, above the taskbar.
4503                    #[cfg(windows)]
4504                    make_borderless_fullscreen(hwnd, w as i32, h as i32);
4505                    hide_console_window();
4506                    // hide_console_window() can itself reassign the OS foreground
4507                    // window (hiding whatever previously had it, e.g. the terminal
4508                    // that ran `ling run`, can hand focus to something other than
4509                    // this window) — so the focus claim has to be the LAST word,
4510                    // after every other window-visibility change, not just inside
4511                    // make_borderless_fullscreen further up.
4512                    #[cfg(windows)]
4513                    force_window_focus(hwnd);
4514                }
4515                #[cfg(target_arch = "wasm32")]
4516                {
4517                    let mut gfx = self.gfx.borrow_mut();
4518                    gfx.width = w;
4519                    gfx.height = h;
4520                    gfx.buffer.resize(w * h, 0); // keep the CPU framebuffer in sync
4521                    gfx.sync_projection();
4522                    crate::gfx::webgl::resize(w as u32, h as u32);
4523                }
4524                return Ok(Value::Unit);
4525            },
4526
4527            // ── ความกว้าง() / ความสูง() — current framebuffer size ──
4528            "get_width" | "ความกว้าง" | "宽" | "幅取得" | "너비" | "عرض" | "العرض" | "רוחב" | "چوڑائی" | "obtenir_largeur" | "breite_abrufen" | "получить_ширину" => {
4529                return Ok(Value::Number(self.gfx.borrow().width as f64));
4530            },
4531            "get_height" | "ความสูง" | "高" | "高取得" | "높이" | "ارتفاع" | "الارتفاع" | "גובה" | "اونچائی" | "obtenir_hauteur" | "höhe_abrufen" | "получить_высоту" => {
4532                return Ok(Value::Number(self.gfx.borrow().height as f64));
4533            },
4534
4535            // ── monitor detection: physical display, not the framebuffer ──────
4536            // monitor_width() → primary-monitor pixel width
4537            "monitor_width" | "screen_width" | "屏宽" | "画面幅" | "화면너비" | "ความกว้างจอ" | "عرض_مانیتور" | "عرض_الشاشة" | "רוחב_צג" | "مانیٹر_چوڑائی" | "largeur_moniteur" | "monitor_breite" | "ширина_монитора" =>
4538            {
4539                return Ok(Value::Number(monitor_info().0 as f64));
4540            },
4541            // monitor_height() → primary-monitor pixel height
4542            "monitor_height" | "screen_height" | "屏高" | "画面高" | "화면높이" | "ความสูงจอ" | "ارتفاع_مانیتور" | "ارتفاع_الشاشة" | "גובה_צג" | "مانیٹر_اونچائی" | "hauteur_moniteur" | "monitor_höhe" | "высота_монитора" =>
4543            {
4544                return Ok(Value::Number(monitor_info().1 as f64));
4545            },
4546            // monitor_refresh() → refresh rate in Hz (a.k.a. the monitor framerate)
4547            "monitor_refresh"
4548            | "monitor_hz"
4549            | "monitor_fps"
4550            | "refresh_rate"
4551            | "刷新率"
4552            | "リフレッシュレート"
4553            | "주사율"
4554            | "อัตรารีเฟรช" | "نرخ_بروزرسانی_مانیتور" | "معدل_تحديث_الشاشة" | "קצב_רענון_צג" | "مانیٹر_ریفریش_ریٹ" | "fréquence_moniteur" | "monitor_bildwiederholrate" | "частота_монитора" => {
4555                return Ok(Value::Number(monitor_info().2 as f64));
4556            },
4557            // monitor_info() → [width, height, refresh_hz]
4558            "monitor_info" | "screen_info" | "屏幕信息" | "画面情報" | "화면정보" | "ข้อมูลจอ" | "اطلاعات_مانیتور" | "معلومات_الشاشة" | "מידע_צג" | "مانیٹر_معلومات" | "info_moniteur" | "bildschirminfo" | "инфо_монитора" =>
4559            {
4560                let (w, h, hz) = monitor_info();
4561                return Ok(Value::List(Rc::new(vec![
4562                    Value::Number(w as f64),
4563                    Value::Number(h as f64),
4564                    Value::Number(hz as f64),
4565                ])));
4566            },
4567            // set_fps(n) → cap the render loop at n frames per second
4568            "set_fps"
4569            | "set_target_fps"
4570            | "target_fps"
4571            | "设帧率"
4572            | "フレームレート設定"
4573            | "프레임설정"
4574            | "ตั้งเฟรมเรต" | "تنظیم_نرخ_فریم" | "عيّن_معدل_الإطارات" | "קבע_קצב_פריימים" | "ایف_پی_ایس_مقرر_کرو" | "définir_fps" | "fps_setzen" | "задать_fps" => {
4575                #[cfg(not(target_arch = "wasm32"))]
4576                {
4577                    let fps = self.arg_num(&args, 0, 60.0)?.max(1.0) as usize;
4578                    let mut gfx = self.gfx.borrow_mut();
4579                    if let Some(win) = gfx.window.as_mut() {
4580                        win.set_target_fps(fps);
4581                    }
4582                }
4583                #[cfg(target_arch = "wasm32")]
4584                {
4585                    self.wasm_target_fps = self.arg_num(&args, 0, 60.0)?.max(1.0);
4586                    self.wasm_next_present_ms = 0.0;
4587                }
4588                return Ok(Value::Unit);
4589            },
4590
4591            // set_vsync(on) → pace the window to the monitor's refresh rate.
4592            // Frame-rate pacing (minifb has no swap-interval), not tear-free
4593            // vsync; `LING_FPS_CAP` and an explicit `set_fps` call still win.
4594            "set_vsync" | "vsync" | "垂直同步" | "垂直同期" | "수직동기" | "ตั้งวีซิงก์" | "تنظیم_وی‌سینک" | "عيّن_تزامن_رأسي" | "קבע_וי_סינק" | "وی_سینک_مقرر_کرو" | "définir_vsync" | "vsync_setzen" | "задать_vsync" =>
4595            {
4596                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
4597                #[cfg(not(target_arch = "wasm32"))]
4598                {
4599                    let mut gfx = self.gfx.borrow_mut();
4600                    gfx.vsync = on;
4601                    if let Some(win) = gfx.window.as_mut() {
4602                        apply_frame_pacing(win, on);
4603                    }
4604                }
4605                #[cfg(target_arch = "wasm32")]
4606                {
4607                    self.wasm_target_fps = if on { monitor_info().2 as f64 } else { 240.0 };
4608                    self.wasm_next_present_ms = 0.0;
4609                }
4610                return Ok(Value::Unit);
4611            },
4612
4613            // ── หน้าต่างเปิดอยู่() → bool — is the window still open? ──
4614            "หน้าต่างเปิดอยู่"
4615            | "window_is_open"
4616            | "gfx_is_open"
4617            | "is_open"
4618            | "窗开"
4619            | "開いている"
4620            | "창열림" | "پنجره_باز_است" | "النافذة_مفتوحة" | "החלון_פתוח" | "ونڈو_کھلی_ہے" => {
4621                #[cfg(not(target_arch = "wasm32"))]
4622                {
4623                    let gfx = self.gfx.borrow();
4624                    if gfx.want_quit {
4625                        return Ok(Value::Bool(false));
4626                    }
4627                    // Escape-to-quit needs the same GetAsyncKeyState fallback
4628                    // as key_down/key_pressed/text_poll (see those) — raw
4629                    // w.is_key_down(Escape) is WM_KEYDOWN-based and silently
4630                    // never fires if this topmost window didn't actually win
4631                    // real Win32 keyboard focus.
4632                    #[cfg(windows)]
4633                    let escape_down = if gfx.topmost_window {
4634                        window_is_foreground(gfx.hwnd) && os_key_down(0x1B) // VK_ESCAPE
4635                    } else {
4636                        gfx.window
4637                            .as_ref()
4638                            .map(|w| w.is_key_down(minifb::Key::Escape))
4639                            .unwrap_or(false)
4640                    };
4641                    #[cfg(not(windows))]
4642                    let escape_down = gfx
4643                        .window
4644                        .as_ref()
4645                        .map(|w| w.is_key_down(minifb::Key::Escape))
4646                        .unwrap_or(false);
4647                    let open = gfx.window.as_ref().map(|w| w.is_open()).unwrap_or(false)
4648                        && !escape_down;
4649                    return Ok(Value::Bool(open));
4650                }
4651                #[cfg(target_arch = "wasm32")]
4652                return Ok(Value::Bool(true));
4653            },
4654
4655            // quit() — close the window the same way Escape does, for a
4656            // script-drawn UI element (an exit button) to call.
4657            "quit" | "exit_game" | "close_window" => {
4658                #[cfg(not(target_arch = "wasm32"))]
4659                {
4660                    self.gfx.borrow_mut().want_quit = true;
4661                }
4662                return Ok(Value::Unit);
4663            },
4664
4665            // ── key_down(name) → bool — is a key held? ──
4666            "key_down" | "กดค้าง" | "按键" | "キー押す" | "키누름" | "کلید_فشرده" | "المفتاح_مضغوط" | "מקש_לחוץ" | "بٹن_دبا_ہوا" | "touche_enfoncée" | "taste_gedrückt" | "клавиша_нажата" => {
4667                #[cfg(not(target_arch = "wasm32"))]
4668                {
4669                    let name = self.arg_str(&args, 0, "");
4670                    let mut gfx = self.gfx.borrow_mut();
4671                    // The borderless-fullscreen/topmost window can be
4672                    // visually in front without ever winning real Win32
4673                    // keyboard focus (Windows' foreground-lock) — minifb's
4674                    // is_key_down is populated from WM_KEYDOWN, which then
4675                    // never arrives. GetAsyncKeyState reads the OS key-state
4676                    // table directly and doesn't need focus, so use it
4677                    // whenever this is that window (see force_window_focus).
4678                    #[cfg(windows)]
4679                    if gfx.topmost_window {
4680                        if !window_is_foreground(gfx.hwnd) {
4681                            return Ok(Value::Bool(false));
4682                        }
4683                        return Ok(Value::Bool(
4684                            str_to_vk(&name).map(os_key_down).unwrap_or(false),
4685                        ));
4686                    }
4687                    if gfx.input_suppressed() {
4688                        return Ok(Value::Bool(false));
4689                    }
4690                    let down = gfx
4691                        .window
4692                        .as_ref()
4693                        .and_then(|w| str_to_minifb_key(&name).map(|k| w.is_key_down(k)))
4694                        .unwrap_or(false);
4695                    return Ok(Value::Bool(down));
4696                }
4697                #[cfg(target_arch = "wasm32")]
4698                {
4699                    let name = self.arg_str(&args, 0, "");
4700                    return Ok(Value::Bool(crate::gfx::wasm_is_key_down(&name)));
4701                }
4702            },
4703
4704            // ── key_pressed(name) → bool — was a key pressed this frame? ──
4705            "key_pressed" | "กดปุ่ม" | "键按" | "キー押した" | "키눌림" | "فشردن_کلید" | "ضغط_المفتاح" | "לחיצת_מקש" | "بٹن_دبانا" | "touche_appuyée" | "taste_getippt" | "клавиша_нажатие" => {
4706                #[cfg(not(target_arch = "wasm32"))]
4707                {
4708                    let name = self.arg_str(&args, 0, "");
4709                    let pressed = {
4710                        let mut gfx = self.gfx.borrow_mut();
4711                        #[cfg(windows)]
4712                        let topmost = gfx.topmost_window;
4713                        #[cfg(not(windows))]
4714                        let topmost = false;
4715                        if topmost {
4716                            #[cfg(windows)]
4717                            {
4718                                if !window_is_foreground(gfx.hwnd) {
4719                                    false
4720                                } else {
4721                                    match str_to_vk(&name) {
4722                                        Some(vk) => {
4723                                            let idx = (vk as usize) & 0xFF;
4724                                            let down = os_key_down(vk);
4725                                            let was = gfx.raw_keys_prev[idx];
4726                                            gfx.raw_keys_prev[idx] = down;
4727                                            down && !was
4728                                        },
4729                                        None => false,
4730                                    }
4731                                }
4732                            }
4733                            #[cfg(not(windows))]
4734                            {
4735                                false
4736                            }
4737                        } else if gfx.input_suppressed() {
4738                            false
4739                        } else {
4740                            gfx.window
4741                                .as_ref()
4742                                .and_then(|w| {
4743                                    str_to_minifb_key(&name)
4744                                        .map(|k| w.is_key_pressed(k, minifb::KeyRepeat::No))
4745                                })
4746                                .unwrap_or(false)
4747                        }
4748                    };
4749                    // gamepad Start behaves like Enter everywhere
4750                    let pressed =
4751                        pressed || ((name == "enter" || name == "return") && gamepad::start_edge());
4752                    return Ok(Value::Bool(pressed));
4753                }
4754                #[cfg(target_arch = "wasm32")]
4755                {
4756                    let name = self.arg_str(&args, 0, "");
4757                    let pressed = crate::gfx::wasm_is_key_pressed(&name);
4758                    return Ok(Value::Bool(pressed));
4759                }
4760            },
4761
4762            // ── mouse_dx() / mouse_dy() → f64 — delta since last frame ──
4763            "mouse_dx" | "เมาส์X" | "鼠ΔX" | "マウスΔX" | "마우스ΔX" | "دلتا_ماوس_ایکس" | "فارق_الفأرة_س" | "דלתא_עכבר_X" | "ماؤس_ڈیلٹا_ایکس" | "souris_dx" | "maus_dx" | "мышь_dx" => {
4764                #[cfg(not(target_arch = "wasm32"))]
4765                return Ok(Value::Number(self.gfx.borrow().mouse_dx as f64));
4766                #[cfg(target_arch = "wasm32")]
4767                return Ok(Value::Number(crate::gfx::wasm_mouse_dx() as f64));
4768            },
4769            // ── mouse_scroll() → f64 — vertical scroll-wheel delta this frame ──
4770            #[cfg(not(target_arch = "wasm32"))]
4771            "mouse_scroll" | "ล้อเมาส์" | "滚轮" | "ホイール" | "스크롤" | "غلتک_ماوس" | "عجلة_الفأرة" | "גלגלת_עכבר" | "ماؤس_اسکرول" =>
4772            {
4773                let gfx = self.gfx.borrow();
4774                let s = gfx
4775                    .window
4776                    .as_ref()
4777                    .and_then(|w| w.get_scroll_wheel())
4778                    .map(|(_, y)| y as f64)
4779                    .unwrap_or(0.0);
4780                return Ok(Value::Number(s));
4781            },
4782            #[cfg(target_arch = "wasm32")]
4783            "mouse_scroll" | "ล้อเมาส์" | "滚轮" | "ホイール" | "스크롤" | "غلتک_ماوس" | "عجلة_الفأرة" | "גלגלת_עכבר" | "ماؤس_اسکرول" =>
4784            {
4785                return Ok(Value::Number(0.0));
4786            },
4787            "mouse_dy" | "เมาส์Y" | "鼠ΔY" | "マウスΔY" | "마우스ΔY" | "دلتا_ماوس_ایگرگ" | "فارق_الفأرة_ص" | "דלתא_עכבר_Y" | "ماؤس_ڈیلٹا_وائی" | "souris_dy" | "maus_dy" | "мышь_dy" => {
4788                #[cfg(not(target_arch = "wasm32"))]
4789                return Ok(Value::Number(self.gfx.borrow().mouse_dy as f64));
4790                #[cfg(target_arch = "wasm32")]
4791                return Ok(Value::Number(crate::gfx::wasm_mouse_dy() as f64));
4792            },
4793
4794            // ── Gamepad / joystick input (ling-input "Sensorium" + gilrs) ──
4795            // pad_poll() → number — advance input one frame; returns # connected pads.
4796            "pad_poll" | "手柄轮询" | "パッド更新" | "패드폴링" | "อัปเดตแพด" | "بررسی_دسته" | "استطلع_اليد" | "בדוק_בקר" | "گیم_پیڈ_پول" | "interroger_manette" | "gamepad_abfragen" | "опросить_геймпад" =>
4797            {
4798                #[cfg(not(target_arch = "wasm32"))]
4799                return Ok(Value::Number(self.pad_poll() as f64));
4800                #[cfg(target_arch = "wasm32")]
4801                return Ok(Value::Number(input_web::poll() as f64));
4802            },
4803            // pad_count() → number — connected gamepads.
4804            "pad_count" | "手柄数" | "パッド数" | "패드수" | "จำนวนแพด" | "تعداد_دسته" | "عدد_أيدي_التحكم" | "מספר_בקרים" | "گیم_پیڈ_تعداد" | "nombre_manettes" | "gamepad_anzahl" | "число_геймпадов" =>
4805            {
4806                #[cfg(not(target_arch = "wasm32"))]
4807                {
4808                    let inp = self.input.borrow();
4809                    let n = inp.as_ref().map_or(0, |s| s.sensorium.devices.count());
4810                    return Ok(Value::Number(n as f64));
4811                }
4812                #[cfg(target_arch = "wasm32")]
4813                return Ok(Value::Number(input_web::count() as f64));
4814            },
4815            // pad_connected(i) → bool.
4816            "pad_connected" | "手柄连接" | "パッド接続" | "패드연결" | "แพดเชื่อม" | "دسته_متصل" | "يد_التحكم_متصلة" | "בקר_מחובר" | "گیم_پیڈ_منسلک" | "manette_connectée" | "gamepad_verbunden" | "геймпад_подключён" =>
4817            {
4818                #[cfg(not(target_arch = "wasm32"))]
4819                {
4820                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4821                    let inp = self.input.borrow();
4822                    let c = inp
4823                        .as_ref()
4824                        .is_some_and(|s| s.sensorium.devices.for_player(i as u8).is_some());
4825                    return Ok(Value::Bool(c));
4826                }
4827                #[cfg(target_arch = "wasm32")]
4828                {
4829                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4830                    return Ok(Value::Bool(input_web::is_connected(i)));
4831                }
4832            },
4833            // pad_button(i, name) → bool — is the button held?
4834            "pad_button" | "手柄按键" | "パッドボタン" | "패드버튼" | "ปุ่มแพด" | "دکمه_دسته" | "زر_اليد" | "כפתור_בקר" | "گیم_پیڈ_بٹن" | "bouton_manette" | "gamepad_taste" | "кнопка_геймпада" =>
4835            {
4836                #[cfg(not(target_arch = "wasm32"))]
4837                {
4838                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4839                    let name = self.arg_str(&args, 1, "");
4840                    let down = parse_pad_button(&name)
4841                        .is_some_and(|b| self.with_pad(i, false, |p| p.is_down(b)));
4842                    return Ok(Value::Bool(down));
4843                }
4844                #[cfg(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                    return Ok(Value::Bool(input_web::button_down(i, &name)));
4849                }
4850            },
4851            // pad_pressed(i, name) → bool — pressed this frame?
4852            // On WASM we only have the current snapshot, so treat as button_down.
4853            "pad_pressed" | "手柄按下" | "パッド押下" | "패드눌림" | "แพดกด" | "دکمه_دسته_فشرده" | "زر_اليد_مضغوط" | "כפתור_בקר_לחוץ" | "گیم_پیڈ_دبایا" | "manette_appuyée" | "gamepad_gedrückt" | "геймпад_нажат" =>
4854            {
4855                #[cfg(not(target_arch = "wasm32"))]
4856                {
4857                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4858                    let name = self.arg_str(&args, 1, "");
4859                    let p = parse_pad_button(&name)
4860                        .is_some_and(|b| self.with_pad(i, false, |g| g.just_pressed(b)));
4861                    return Ok(Value::Bool(p));
4862                }
4863                #[cfg(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                    return Ok(Value::Bool(input_web::button_down(i, &name)));
4868                }
4869            },
4870            // pad_lx(i)/pad_ly(i)/pad_rx(i)/pad_ry(i) → number — stick axes (−1..=1).
4871            "pad_lx" | "手柄左X" | "パッド左X" | "패드왼X" | "แพดซ้ายX" | "آنالوگ_چپ_ایکس" | "عصا_اليسرى_س" | "ג'ויסטיק_שמאל_X" | "بائیں_اسٹک_ایکس" | "manette_axe_gauche_x" | "gamepad_lx" | "геймпад_ось_лево_x" => {
4872                #[cfg(not(target_arch = "wasm32"))]
4873                {
4874                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4875                    return Ok(Value::Number(
4876                        self.with_pad(i, 0.0, |p| p.left_stick.x as f64),
4877                    ));
4878                }
4879                #[cfg(target_arch = "wasm32")]
4880                {
4881                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4882                    return Ok(Value::Number(input_web::axis_lx(i) as f64));
4883                }
4884            },
4885            "pad_ly" | "手柄左Y" | "パッド左Y" | "패드왼Y" | "แพดซ้ายY" | "آنالوگ_چپ_ایگرگ" | "عصا_اليسرى_ص" | "ג'ויסטיק_שמאל_Y" | "بائیں_اسٹک_وائی" | "manette_axe_gauche_y" | "gamepad_ly" | "геймпад_ось_лево_y" => {
4886                #[cfg(not(target_arch = "wasm32"))]
4887                {
4888                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4889                    return Ok(Value::Number(
4890                        self.with_pad(i, 0.0, |p| p.left_stick.y as f64),
4891                    ));
4892                }
4893                #[cfg(target_arch = "wasm32")]
4894                {
4895                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4896                    return Ok(Value::Number(input_web::axis_ly(i) as f64));
4897                }
4898            },
4899            "pad_rx" | "手柄右X" | "パッド右X" | "패드오X" | "แพดขวาX" | "آنالوگ_راست_ایکس" | "عصا_اليمنى_س" | "ג'ויסטיק_ימין_X" | "دائیں_اسٹک_ایکس" | "manette_axe_droit_x" | "gamepad_rx" | "геймпад_ось_право_x" => {
4900                #[cfg(not(target_arch = "wasm32"))]
4901                {
4902                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4903                    return Ok(Value::Number(
4904                        self.with_pad(i, 0.0, |p| p.right_stick.x as f64),
4905                    ));
4906                }
4907                #[cfg(target_arch = "wasm32")]
4908                {
4909                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4910                    return Ok(Value::Number(input_web::axis_rx(i) as f64));
4911                }
4912            },
4913            "pad_ry" | "手柄右Y" | "パッド右Y" | "패드오Y" | "แพดขวาY" | "آنالوگ_راست_ایگرگ" | "عصا_اليمنى_ص" | "ג'ויסטיק_ימין_Y" | "دائیں_اسٹک_وائی" | "manette_axe_droit_y" | "gamepad_ry" | "геймпад_ось_право_y" => {
4914                #[cfg(not(target_arch = "wasm32"))]
4915                {
4916                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4917                    return Ok(Value::Number(
4918                        self.with_pad(i, 0.0, |p| p.right_stick.y as f64),
4919                    ));
4920                }
4921                #[cfg(target_arch = "wasm32")]
4922                {
4923                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4924                    return Ok(Value::Number(input_web::axis_ry(i) as f64));
4925                }
4926            },
4927            // pad_lt(i)/pad_rt(i) → number — analog triggers (0..=1).
4928            "pad_lt" | "手柄左扳机" | "パッド左トリガー" | "패드왼트리거" | "ไกแพดซ้าย" | "ماشه_چپ" | "زناد_اليسار" | "הדק_שמאל" | "بائیں_ٹریگر" | "manette_gâchette_gauche" | "gamepad_lt" | "геймпад_триггер_лево" =>
4929            {
4930                #[cfg(not(target_arch = "wasm32"))]
4931                {
4932                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4933                    return Ok(Value::Number(
4934                        self.with_pad(i, 0.0, |p| p.left_trigger as f64),
4935                    ));
4936                }
4937                #[cfg(target_arch = "wasm32")]
4938                {
4939                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4940                    return Ok(Value::Number(input_web::trigger_lt(i) as f64));
4941                }
4942            },
4943            "pad_rt" | "手柄右扳机" | "パッド右トリガー" | "패드오트리거" | "ไกแพดขวา" | "ماشه_راست" | "زناد_اليمين" | "הדק_ימין" | "دائیں_ٹریگر" | "manette_gâchette_droite" | "gamepad_rt" | "геймпад_триггер_право" =>
4944            {
4945                #[cfg(not(target_arch = "wasm32"))]
4946                {
4947                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4948                    return Ok(Value::Number(
4949                        self.with_pad(i, 0.0, |p| p.right_trigger as f64),
4950                    ));
4951                }
4952                #[cfg(target_arch = "wasm32")]
4953                {
4954                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4955                    return Ok(Value::Number(input_web::trigger_rt(i) as f64));
4956                }
4957            },
4958            // pad_rumble(i, lo, hi) → unit — set rumble motor amplitudes (0..=1).
4959            "pad_rumble" | "手柄震动" | "パッド振動" | "패드진동" | "แพดสั่น" | "لرزش_دسته" | "اهتزاز_اليد" | "רטט_בקר" | "گیم_پیڈ_تھرتھراہٹ" | "vibration_manette" | "gamepad_vibration" | "вибрация_геймпада" =>
4960            {
4961                #[cfg(not(target_arch = "wasm32"))]
4962                {
4963                    use ling_input::backend::InputBackend;
4964                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4965                    let lo = self.arg_num(&args, 1, 0.0)? as f32;
4966                    let hi = self.arg_num(&args, 2, lo as f64)? as f32;
4967                    let mut inp = self.input.borrow_mut();
4968                    if let Some(s) = inp.as_mut() {
4969                        if let Some(dev) = s.sensorium.devices.for_player(i as u8).map(|d| d.id) {
4970                            s.backend.set_rumble(
4971                                dev,
4972                                ling_input::Rumble { low: lo, high: hi, ..Default::default() },
4973                            );
4974                        }
4975                    }
4976                    return Ok(Value::Unit);
4977                }
4978                #[cfg(target_arch = "wasm32")]
4979                return Ok(Value::Unit);
4980            },
4981
4982            // ── set_camera_pos(x, y, z) — move camera to world position ──
4983            "set_camera_pos" | "ตั้งตำแหน่งกล้อง" | "镜坐标" | "カメラ座標" | "카메라좌표" | "تنظیم_موقعیت_دوربین" | "عيّن_موضع_الكاميرا" | "קבע_מיקום_מצלמה" | "کیمرہ_مقام_مقرر_کرو" | "définir_position_caméra" | "kameraposition_setzen" | "задать_позицию_камеры" =>
4984            {
4985                let x = self.arg_num(&args, 0, 0.0)? as f32;
4986                let y = self.arg_num(&args, 1, 0.0)? as f32;
4987                let z = self.arg_num(&args, 2, 0.0)? as f32;
4988                {
4989                    let mut gfx = self.gfx.borrow_mut();
4990                    gfx.camera.tx = x;
4991                    gfx.camera.ty = y;
4992                    gfx.camera.tz = z;
4993                }
4994                #[cfg(not(target_arch = "wasm32"))]
4995                if let Some(audio) = &self.audio {
4996                    audio.set_listener_pos(x, y, z);
4997                }
4998                return Ok(Value::Unit);
4999            },
5000
5001            // ── move_camera(dx, dy, dz) — translate camera by delta ──
5002            "move_camera" => {
5003                let dx = self.arg_num(&args, 0, 0.0)? as f32;
5004                let dy = self.arg_num(&args, 1, 0.0)? as f32;
5005                let dz = self.arg_num(&args, 2, 0.0)? as f32;
5006                let mut gfx = self.gfx.borrow_mut();
5007                gfx.camera.tx += dx;
5008                gfx.camera.ty += dy;
5009                gfx.camera.tz += dz;
5010                return Ok(Value::Unit);
5011            },
5012
5013            // ── set_zdist(d) — set perspective z-offset (field-of-view taper) ──
5014            "set_zdist" | "ตั้งระยะห่าง" | "镜距" | "Z距離設定" | "Z거리설정" | "تنظیم_فاصله_عمق" | "عيّن_مسافة_العمق" | "קבע_מרחק_עומק" | "گہرائی_فاصلہ_مقرر_کرو" | "définir_distance_z" | "z_abstand_setzen" | "задать_дистанцию_z" =>
5015            {
5016                let d = self.arg_num(&args, 0, 5.0)? as f32;
5017                self.gfx.borrow_mut().camera.zdist = d;
5018                return Ok(Value::Unit);
5019            },
5020
5021            // ── capture_mouse() — hide cursor and warp to centre each frame ──
5022            "capture_mouse" | "จับเมาส์" | "捕鼠" | "マウス捕捉" | "마우스잡기" | "ضبط_ماوس" | "امسك_الفأرة" | "לכוד_עכבר" | "ماؤس_پکڑو" | "capturer_souris" | "maus_erfassen" | "захватить_мышь" =>
5023            {
5024                #[cfg(not(target_arch = "wasm32"))]
5025                {
5026                    let mut gfx = self.gfx.borrow_mut();
5027                    gfx.mouse_captured = true;
5028                    gfx.last_mx = f32::NAN;
5029                    if let Some(win) = gfx.window.as_mut() {
5030                        win.set_cursor_visibility(false);
5031                    }
5032                }
5033                return Ok(Value::Unit);
5034            },
5035
5036            // ── release_mouse() — restore cursor and remove clip region ──
5037            "release_mouse" => {
5038                #[cfg(not(target_arch = "wasm32"))]
5039                {
5040                    let mut gfx = self.gfx.borrow_mut();
5041                    gfx.mouse_captured = false;
5042                    gfx.last_mx = f32::NAN;
5043                    if let Some(win) = gfx.window.as_mut() {
5044                        win.set_cursor_visibility(true);
5045                    }
5046                    #[cfg(windows)]
5047                    unsafe {
5048                        // Null releases the clip; reuse the RECT-typed declaration above.
5049                        extern "system" {
5050                            fn ClipCursor(lpRect: *const std::ffi::c_void) -> i32;
5051                        }
5052                        ClipCursor(std::ptr::null());
5053                    }
5054                }
5055                return Ok(Value::Unit);
5056            },
5057
5058            // ── cursor_hide() / cursor_show() — just the OS cursor's visibility,
5059            // no warp-to-centre or clip region (unlike capture_mouse/release_mouse,
5060            // which are for FPS-style look-around). For point-and-click play where
5061            // the cursor still needs to move freely and mouse_x()/mouse_y() still
5062            // need to track real position, just hide the system pointer glyph.
5063            "cursor_hide" => {
5064                #[cfg(not(target_arch = "wasm32"))]
5065                if let Some(win) = self.gfx.borrow_mut().window.as_mut() {
5066                    win.set_cursor_visibility(false);
5067                }
5068                return Ok(Value::Unit);
5069            },
5070            "cursor_show" => {
5071                #[cfg(not(target_arch = "wasm32"))]
5072                if let Some(win) = self.gfx.borrow_mut().window.as_mut() {
5073                    win.set_cursor_visibility(true);
5074                }
5075                return Ok(Value::Unit);
5076            },
5077
5078            // ══════════════════════════════════════════════════════════════════
5079            // 3-D / 4-D DRAWING — camera, lights, depth-sorted geometry
5080            // ══════════════════════════════════════════════════════════════════
5081
5082            // ── set_camera(cry, sry, crx, srx) — store precomputed camera trig ──
5083            // Call once per frame after computing cos/sin of your rotation angles.
5084            "set_camera" | "ตั้งกล้อง" | "设镜" | "设置摄像机" | "カメラ設定" | "카메라설정" | "تنظیم_دوربین" | "عيّن_الكاميرا" | "קבע_מצלמה" | "کیمرہ_مقرر_کرو" | "définir_caméra" | "kamera_setzen" | "задать_камеру" =>
5085            {
5086                let cry = self.arg_num(&args, 0, 1.0)? as f32;
5087                let sry = self.arg_num(&args, 1, 0.0)? as f32;
5088                let crx = self.arg_num(&args, 2, 1.0)? as f32;
5089                let srx = self.arg_num(&args, 3, 0.0)? as f32;
5090                let mut gfx = self.gfx.borrow_mut();
5091                gfx.camera.cry = cry;
5092                gfx.camera.sry = sry;
5093                gfx.camera.crx = crx;
5094                gfx.camera.srx = srx;
5095                return Ok(Value::Unit);
5096            },
5097
5098            // ── set_projection(cx, cy, focal, zdist) — override projection params ──
5099            // Automatically set when the window opens; override only if needed.
5100            "set_projection" | "ตั้งโปรเจกชัน" | "投影" | "投影設定" | "투영설정" | "تنظیم_فرافکنی" | "عيّن_الإسقاط" | "קבע_הטלה" | "پروجیکشن_مقرر_کرو" | "définir_projection" | "projektion_setzen" | "задать_проекцию" =>
5101            {
5102                let cx = self.arg_num(&args, 0, 960.0)? as f32;
5103                let cy = self.arg_num(&args, 1, 540.0)? as f32;
5104                let focal = self.arg_num(&args, 2, 1080.0)? as f32;
5105                let zdist = self.arg_num(&args, 3, 5.0)? as f32;
5106                let mut gfx = self.gfx.borrow_mut();
5107                gfx.camera.cx = cx;
5108                gfx.camera.cy = cy;
5109                gfx.camera.focal = focal;
5110                gfx.camera.zdist = zdist;
5111                return Ok(Value::Unit);
5112            },
5113
5114            // ── mesh_load(path) → handle · loads a glb/gltf (skeleton + skin + animation) ──
5115            "gltf_load" => {
5116                let path = self.arg_str(&args, 0, "");
5117                match ling_physics::gltf::GltfModel::load(&path) {
5118                    Ok(m) => {
5119                        self.gltf_models.borrow_mut().push(m);
5120                        let h = self.gltf_models.borrow().len() - 1;
5121                        return Ok(Value::Number(h as f64));
5122                    }
5123                    Err(e) => {
5124                        eprintln!("mesh_load failed ({path}): {e}");
5125                        return Ok(Value::Number(-1.0));
5126                    }
5127                }
5128            },
5129            // mesh_anim_count(handle) → number of animation clips
5130            "gltf_anim_count" => {
5131                let h = self.arg_num(&args, 0, -1.0)? as i64;
5132                let n = self
5133                    .gltf_models
5134                    .borrow()
5135                    .get(h as usize)
5136                    .map(|m| m.animations.len())
5137                    .unwrap_or(0);
5138                return Ok(Value::Number(n as f64));
5139            },
5140            // mesh_anim_name(handle, i) → clip name
5141            "gltf_anim_name" => {
5142                let h = self.arg_num(&args, 0, -1.0)? as i64;
5143                let i = self.arg_num(&args, 1, 0.0)? as usize;
5144                let s = self
5145                    .gltf_models
5146                    .borrow()
5147                    .get(h as usize)
5148                    .and_then(|m| m.animations.get(i))
5149                    .map(|a| a.name.clone())
5150                    .unwrap_or_default();
5151                return Ok(Value::Str(s));
5152            },
5153            // mesh_anim_dur(handle, i) → clip duration (seconds)
5154            "gltf_anim_dur" => {
5155                let h = self.arg_num(&args, 0, -1.0)? as i64;
5156                let i = self.arg_num(&args, 1, 0.0)? as usize;
5157                let d = self
5158                    .gltf_models
5159                    .borrow()
5160                    .get(h as usize)
5161                    .and_then(|m| m.animations.get(i))
5162                    .map(|a| a.duration)
5163                    .unwrap_or(0.0);
5164                return Ok(Value::Number(d as f64));
5165            },
5166            // mesh_tris(handle) → total triangle count (perf sanity check)
5167            "gltf_tris" => {
5168                let h = self.arg_num(&args, 0, -1.0)? as i64;
5169                let n: usize = self
5170                    .gltf_models
5171                    .borrow()
5172                    .get(h as usize)
5173                    .map(|m| m.meshes.iter().map(|mm| mm.indices.len()).sum::<usize>() / 3)
5174                    .unwrap_or(0);
5175                return Ok(Value::Number(n as f64));
5176            },
5177
5178            // gltf_joint_count(handle) → number of skin joints (bones)
5179            "gltf_joint_count" => {
5180                let h = self.arg_num(&args, 0, -1.0)? as i64;
5181                let n = self
5182                    .gltf_models
5183                    .borrow()
5184                    .get(h as usize)
5185                    .and_then(|m| m.skins.first())
5186                    .map(|s| s.joints.len())
5187                    .unwrap_or(0);
5188                return Ok(Value::Number(n as f64));
5189            },
5190            // gltf_joint_name(handle, j) → bone name (its node's name)
5191            "gltf_joint_name" => {
5192                let h = self.arg_num(&args, 0, -1.0)? as i64;
5193                let j = self.arg_num(&args, 1, 0.0)? as usize;
5194                let models = self.gltf_models.borrow();
5195                let s = models
5196                    .get(h as usize)
5197                    .and_then(|m| {
5198                        m.skins
5199                            .first()
5200                            .and_then(|sk| sk.joints.get(j))
5201                            .and_then(|jt| m.nodes.get(jt.node_idx))
5202                            .map(|n| n.name.clone())
5203                    })
5204                    .unwrap_or_default();
5205                return Ok(Value::Str(s));
5206            },
5207
5208            // ── gltf_draw(handle, ox,oy,oz, scale, yaw) — filled render of a loaded model ──
5209            //   glTF is Y-up / -Z-forward; the engine is Y-down, so we flip Y and Z, then
5210            //   yaw about Y, scale, translate. Per-part colour by mesh name. Lit + depth-queued
5211            //   exactly like draw_mesh, so it shares the camera + z-buffer.
5212            "gltf_draw" => {
5213                let hh = self.arg_num(&args, 0, -1.0)? as i64;
5214                let ox = self.arg_num(&args, 1, 0.0)? as f32;
5215                let oy = self.arg_num(&args, 2, 0.0)? as f32;
5216                let oz = self.arg_num(&args, 3, 0.0)? as f32;
5217                let scale = self.arg_num(&args, 4, 1.0)? as f32;
5218                let yaw = self.arg_num(&args, 5, 0.0)? as f32;
5219                let (sy, cyy) = yaw.sin_cos();
5220                let models = self.gltf_models.borrow();
5221                let model = match models.get(hh as usize) {
5222                    Some(m) => m,
5223                    None => return Ok(Value::Unit),
5224                };
5225                let mut gfx = self.gfx.borrow_mut();
5226                let cp = {
5227                    let c = &gfx.camera;
5228                    ling_gpu::CameraParams {
5229                        cry: c.cry, sry: c.sry, crx: c.crx, srx: c.srx,
5230                        cx: c.cx, cy: c.cy, focal: c.focal, zdist: c.zdist,
5231                        tx: c.tx, ty: c.ty, tz: c.tz,
5232                    }
5233                };
5234                let near = -gfx.camera.zdist + 0.02;
5235                let ambient = gfx.ambient;
5236                for mesh in &model.meshes {
5237                    let nlow = mesh.name.to_lowercase();
5238                    let base: u32 = if nlow.contains("hair") {
5239                        0x7a4a28
5240                    } else if nlow.contains("cloth") || nlow.contains("top") {
5241                        0x4a86e0
5242                    } else if nlow.contains("wing") {
5243                        0xe6ecf5
5244                    } else if nlow.contains("star") {
5245                        0xffd24d
5246                    } else {
5247                        0xf2d6b8
5248                    };
5249                    let nv = mesh.verts.len();
5250                    if nv == 0 {
5251                        continue;
5252                    }
5253                    let mut world = vec![0.0f32; nv * 3];
5254                    for (i, v) in mesh.verts.iter().enumerate() {
5255                        let gx = v.pos.x * scale;
5256                        let gy = -v.pos.y * scale;
5257                        let gz = -v.pos.z * scale;
5258                        let rx = gx * cyy + gz * sy;
5259                        let rz = -gx * sy + gz * cyy;
5260                        world[i * 3] = ox + rx;
5261                        world[i * 3 + 1] = oy + gy;
5262                        world[i * 3 + 2] = oz + rz;
5263                    }
5264                    let mut proj = vec![0.0f32; nv * 3];
5265                    ling_gpu::backend().project_points(&world, &cp, &mut proj);
5266                    let idx = &mesh.indices;
5267                    let nt = idx.len() / 3;
5268                    for t in 0..nt {
5269                        let ia = idx[t * 3] as usize;
5270                        let ib = idx[t * 3 + 1] as usize;
5271                        let ic = idx[t * 3 + 2] as usize;
5272                        if ia >= nv || ib >= nv || ic >= nv {
5273                            continue;
5274                        }
5275                        let (da, db, dc) = (proj[ia * 3 + 2], proj[ib * 3 + 2], proj[ic * 3 + 2]);
5276                        if (da + db + dc) / 3.0 <= near {
5277                            continue;
5278                        }
5279                        let col = {
5280                            let (ax, ay, az) = (world[ia * 3], world[ia * 3 + 1], world[ia * 3 + 2]);
5281                            let (bx, by, bz) = (world[ib * 3], world[ib * 3 + 1], world[ib * 3 + 2]);
5282                            let (px, py, pz) = (world[ic * 3], world[ic * 3 + 1], world[ic * 3 + 2]);
5283                            let (ux, uy, uz) = (bx - ax, by - ay, bz - az);
5284                            let (vx, vy, vz) = (px - ax, py - ay, pz - az);
5285                            let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
5286                            let centroid =
5287                                [(ax + bx + px) / 3.0, (ay + by + py) / 3.0, (az + bz + pz) / 3.0];
5288                            if gfx.flat_shade {
5289                                base
5290                            } else {
5291                                crate::gfx::light::compute_lit_color(
5292                                    base, normal, centroid, &gfx.lights, ambient,
5293                                )
5294                            }
5295                        };
5296                        let depth = (da + db + dc) / 3.0;
5297                        let col = gfx.fog_apply(col, depth);
5298                        gfx.depth_queue.push_triangle_zv(
5299                            col,
5300                            proj[ia * 3], proj[ia * 3 + 1], da,
5301                            proj[ib * 3], proj[ib * 3 + 1], db,
5302                            proj[ic * 3], proj[ic * 3 + 1], dc,
5303                        );
5304                    }
5305                }
5306                return Ok(Value::Unit);
5307            },
5308
5309            // ── gltf_autorig(handle) → synthesize a humanoid skeleton + skin weights ──
5310            "gltf_autorig" => {
5311                let hh = self.arg_num(&args, 0, -1.0)? as i64;
5312                let mut models = self.gltf_models.borrow_mut();
5313                if let Some(m) = models.get_mut(hh as usize) {
5314                    return Ok(Value::Number(m.autorig() as f64));
5315                }
5316                return Ok(Value::Number(0.0));
5317            },
5318
5319            // ── gltf_pose_draw(handle, ox,oy,oz, scale, yaw, poseList) ──
5320            //   Like gltf_draw, but linear-blend-skins the mesh by `poseList` first.
5321            //   poseList = flat XYZ-euler radians, 3 per bone (12 bones → 36 values).
5322            "gltf_pose_draw" => {
5323                let hh = self.arg_num(&args, 0, -1.0)? as i64;
5324                let ox = self.arg_num(&args, 1, 0.0)? as f32;
5325                let oy = self.arg_num(&args, 2, 0.0)? as f32;
5326                let oz = self.arg_num(&args, 3, 0.0)? as f32;
5327                let scale = self.arg_num(&args, 4, 1.0)? as f32;
5328                let yaw = self.arg_num(&args, 5, 0.0)? as f32;
5329                let euler: Vec<f32> = match args.get(6) {
5330                    Some(Value::List(v)) => {
5331                        v.iter().map(|x| self.to_number(x).unwrap_or(0.0) as f32).collect()
5332                    },
5333                    _ => Vec::new(),
5334                };
5335                let (sy, cyy) = yaw.sin_cos();
5336                let models = self.gltf_models.borrow();
5337                let model = match models.get(hh as usize) {
5338                    Some(m) => m,
5339                    None => return Ok(Value::Unit),
5340                };
5341                let skinned = model.skin_local(&euler);
5342                let mut gfx = self.gfx.borrow_mut();
5343                let cp = {
5344                    let c = &gfx.camera;
5345                    ling_gpu::CameraParams {
5346                        cry: c.cry, sry: c.sry, crx: c.crx, srx: c.srx,
5347                        cx: c.cx, cy: c.cy, focal: c.focal, zdist: c.zdist,
5348                        tx: c.tx, ty: c.ty, tz: c.tz,
5349                    }
5350                };
5351                let near = -gfx.camera.zdist + 0.02;
5352                let ambient = gfx.ambient;
5353                for (mi, mesh) in model.meshes.iter().enumerate() {
5354                    let nlow = mesh.name.to_lowercase();
5355                    let base: u32 = if nlow.contains("hair") {
5356                        0x7a4a28
5357                    } else if nlow.contains("cloth") || nlow.contains("top") {
5358                        0x4a86e0
5359                    } else if nlow.contains("wing") {
5360                        0xe6ecf5
5361                    } else if nlow.contains("star") {
5362                        0xffd24d
5363                    } else {
5364                        0xf2d6b8
5365                    };
5366                    let sk = match skinned.get(mi) {
5367                        Some(s) => s,
5368                        None => continue,
5369                    };
5370                    let nv = sk.len();
5371                    if nv == 0 {
5372                        continue;
5373                    }
5374                    let mut world = vec![0.0f32; nv * 3];
5375                    for i in 0..nv {
5376                        let gx = sk[i][0] * scale;
5377                        let gy = -sk[i][1] * scale;
5378                        let gz = -sk[i][2] * scale;
5379                        let rx = gx * cyy + gz * sy;
5380                        let rz = -gx * sy + gz * cyy;
5381                        world[i * 3] = ox + rx;
5382                        world[i * 3 + 1] = oy + gy;
5383                        world[i * 3 + 2] = oz + rz;
5384                    }
5385                    let mut proj = vec![0.0f32; nv * 3];
5386                    ling_gpu::backend().project_points(&world, &cp, &mut proj);
5387                    let idx = &mesh.indices;
5388                    let nt = idx.len() / 3;
5389                    for t in 0..nt {
5390                        let ia = idx[t * 3] as usize;
5391                        let ib = idx[t * 3 + 1] as usize;
5392                        let ic = idx[t * 3 + 2] as usize;
5393                        if ia >= nv || ib >= nv || ic >= nv {
5394                            continue;
5395                        }
5396                        let (da, db, dc) = (proj[ia * 3 + 2], proj[ib * 3 + 2], proj[ic * 3 + 2]);
5397                        if (da + db + dc) / 3.0 <= near {
5398                            continue;
5399                        }
5400                        let col = {
5401                            let (ax, ay, az) = (world[ia * 3], world[ia * 3 + 1], world[ia * 3 + 2]);
5402                            let (bx, by, bz) = (world[ib * 3], world[ib * 3 + 1], world[ib * 3 + 2]);
5403                            let (px, py, pz) = (world[ic * 3], world[ic * 3 + 1], world[ic * 3 + 2]);
5404                            let (ux, uy, uz) = (bx - ax, by - ay, bz - az);
5405                            let (vx, vy, vz) = (px - ax, py - ay, pz - az);
5406                            let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
5407                            let centroid =
5408                                [(ax + bx + px) / 3.0, (ay + by + py) / 3.0, (az + bz + pz) / 3.0];
5409                            if gfx.flat_shade {
5410                                base
5411                            } else {
5412                                crate::gfx::light::compute_lit_color(
5413                                    base, normal, centroid, &gfx.lights, ambient,
5414                                )
5415                            }
5416                        };
5417                        let depth = (da + db + dc) / 3.0;
5418                        let col = gfx.fog_apply(col, depth);
5419                        gfx.depth_queue.push_triangle_zv(
5420                            col,
5421                            proj[ia * 3], proj[ia * 3 + 1], da,
5422                            proj[ib * 3], proj[ib * 3 + 1], db,
5423                            proj[ic * 3], proj[ic * 3 + 1], dc,
5424                        );
5425                    }
5426                }
5427                return Ok(Value::Unit);
5428            },
5429
5430            // ── draw_mesh(pos, idx, ox, oy, oz, scale, mode) ──
5431            //   Native batched triangle mesh. pos = flat [x,y,z,…], idx = flat tri indices.
5432            //   mode 0 = lit with current pen colour; 1 = per-face hue cycle.
5433            //   Vertices are batch-projected via ling-gpu (CPU fallback, or CUDA when the
5434            //   `cuda` feature is on); the per-triangle loop runs natively (not in the
5435            //   interpreter) so dense meshes (imported glTF, grids) stay fast.
5436            "draw_mesh" | "วาดเมช" | "رسم_مش" | "ارسم_شبكة" | "צייר_רשת" | "میش_کھینچو" | "dessiner_maillage" | "netz_zeichnen" | "рисовать_меш" => {
5437                let pos = match args.first() {
5438                    Some(Value::List(v)) => v,
5439                    _ => return Ok(Value::Unit),
5440                };
5441                let idx = match args.get(1) {
5442                    Some(Value::List(v)) => v,
5443                    _ => return Ok(Value::Unit),
5444                };
5445                let ox = self.arg_num(&args, 2, 0.0)? as f32;
5446                let oy = self.arg_num(&args, 3, 0.0)? as f32;
5447                let oz = self.arg_num(&args, 4, 0.0)? as f32;
5448                let scale = self.arg_num(&args, 5, 1.0)? as f32;
5449                let mode = self.arg_num(&args, 6, 0.0)? as i64;
5450                let nv = pos.len() / 3;
5451                if nv == 0 {
5452                    return Ok(Value::Unit);
5453                }
5454                let mut world = vec![0.0f32; nv * 3];
5455                for i in 0..nv {
5456                    world[i * 3] = ox + self.to_number(&pos[i * 3]).unwrap_or(0.0) as f32 * scale;
5457                    world[i * 3 + 1] =
5458                        oy + self.to_number(&pos[i * 3 + 1]).unwrap_or(0.0) as f32 * scale;
5459                    world[i * 3 + 2] =
5460                        oz + self.to_number(&pos[i * 3 + 2]).unwrap_or(0.0) as f32 * scale;
5461                }
5462                let mut gfx = self.gfx.borrow_mut();
5463                let cp = {
5464                    let c = &gfx.camera;
5465                    ling_gpu::CameraParams {
5466                        cry: c.cry,
5467                        sry: c.sry,
5468                        crx: c.crx,
5469                        srx: c.srx,
5470                        cx: c.cx,
5471                        cy: c.cy,
5472                        focal: c.focal,
5473                        zdist: c.zdist,
5474                        tx: c.tx,
5475                        ty: c.ty,
5476                        tz: c.tz,
5477                    }
5478                };
5479                let near = -gfx.camera.zdist + 0.02;
5480                let base = gfx.color;
5481                let ambient = gfx.ambient;
5482                let mut proj = vec![0.0f32; nv * 3]; // (sx, sy, depth) per vertex
5483                ling_gpu::backend().project_points(&world, &cp, &mut proj);
5484                let nt = idx.len() / 3;
5485                for t in 0..nt {
5486                    let ia = self.to_number(&idx[t * 3]).unwrap_or(0.0) as usize;
5487                    let ib = self.to_number(&idx[t * 3 + 1]).unwrap_or(0.0) as usize;
5488                    let ic = self.to_number(&idx[t * 3 + 2]).unwrap_or(0.0) as usize;
5489                    if ia >= nv || ib >= nv || ic >= nv {
5490                        continue;
5491                    }
5492                    let (da, db, dc) = (proj[ia * 3 + 2], proj[ib * 3 + 2], proj[ic * 3 + 2]);
5493                    if (da + db + dc) / 3.0 <= near {
5494                        continue;
5495                    } // near-plane cull (centroid)
5496                    let col = if mode == 1 {
5497                        let h = t as f32 * 0.6;
5498                        let r = ((h.sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
5499                        let g = (((h + 2.094).sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
5500                        let b = (((h + 4.189).sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
5501                        (r << 16) | (g << 8) | b
5502                    } else {
5503                        let (ax, ay, az) = (world[ia * 3], world[ia * 3 + 1], world[ia * 3 + 2]);
5504                        let (bx, by, bz) = (world[ib * 3], world[ib * 3 + 1], world[ib * 3 + 2]);
5505                        let (px, py, pz) = (world[ic * 3], world[ic * 3 + 1], world[ic * 3 + 2]);
5506                        let (ux, uy, uz) = (bx - ax, by - ay, bz - az);
5507                        let (vx, vy, vz) = (px - ax, py - ay, pz - az);
5508                        let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
5509                        let centroid = [
5510                            (ax + bx + px) / 3.0,
5511                            (ay + by + py) / 3.0,
5512                            (az + bz + pz) / 3.0,
5513                        ];
5514                        if gfx.flat_shade {
5515                            base
5516                        } else {
5517                            crate::gfx::light::compute_lit_color(
5518                                base,
5519                                normal,
5520                                centroid,
5521                                &gfx.lights,
5522                                ambient,
5523                            )
5524                        }
5525                    };
5526                    let depth = (da + db + dc) / 3.0;
5527                    let col = gfx.fog_apply(col, depth);
5528                    // True per-vertex depth so the z-buffer resolves mesh
5529                    // self-occlusion (when depth_test is off, the flush ignores
5530                    // z and uses the screen x/y exactly as before).
5531                    gfx.depth_queue.push_triangle_zv(
5532                        col,
5533                        proj[ia * 3],
5534                        proj[ia * 3 + 1],
5535                        da,
5536                        proj[ib * 3],
5537                        proj[ib * 3 + 1],
5538                        db,
5539                        proj[ic * 3],
5540                        proj[ic * 3 + 1],
5541                        dc,
5542                    );
5543                }
5544                return Ok(Value::Unit);
5545            },
5546
5547            // ── add_light(x, y, z, r, g, b, intensity, radius) ──
5548            // Adds a point light in world space.  r/g/b in [0..1].
5549            // radius == 0 → no distance falloff.
5550            "add_light" | "เพิ่มแสง" | "加灯" | "ライト追加" | "조명추가" | "افزودن_نور" | "أضف_ضوء" | "הוסף_אור" | "روشنی_شامل_کرو" | "ajouter_lumière" | "licht_hinzufügen" | "добавить_свет" =>
5551            {
5552                let x = self.arg_num(&args, 0, 0.0)? as f32;
5553                let y = self.arg_num(&args, 1, -3.0)? as f32;
5554                let z = self.arg_num(&args, 2, 3.0)? as f32;
5555                let mut r = self.arg_num(&args, 3, 1.0)? as f32;
5556                let mut g = self.arg_num(&args, 4, 1.0)? as f32;
5557                let mut b = self.arg_num(&args, 5, 1.0)? as f32;
5558                // Forgive 0-255 colour values: if any channel is clearly > 1,
5559                // treat the triple as 0-255 and normalise. Keeps 0-1 callers exact.
5560                if r > 1.5 || g > 1.5 || b > 1.5 {
5561                    r /= 255.0;
5562                    g /= 255.0;
5563                    b /= 255.0;
5564                }
5565                let intensity = self.arg_num(&args, 6, 1.0)? as f32;
5566                let radius = self.arg_num(&args, 7, 0.0)? as f32;
5567                self.gfx
5568                    .borrow_mut()
5569                    .lights
5570                    .push(Light { x, y, z, r, g, b, intensity, radius });
5571                return Ok(Value::Unit);
5572            },
5573
5574            // ── clear_lights() — remove all lights ──
5575            "clear_lights" | "ล้างแสง" | "清灯" | "ライト消去" | "조명초기화" | "پاک‌کردن_نورها" | "امسح_الأضواء" | "נקה_אורות" | "روشنیاں_صاف_کرو" | "effacer_lumières" | "lichter_löschen" | "очистить_свет" =>
5576            {
5577                self.gfx.borrow_mut().lights.clear();
5578                return Ok(Value::Unit);
5579            },
5580
5581            // ── set_material(key, value) — configure LingMaterial field ──
5582            // Activates the material BSDF for subsequent polygon/triangle draws.
5583            // Keys (string): "albedo" "roughness" "metallic" "emission"
5584            //   "emission_strength" "specular" "specular_tint" "subsurface"
5585            //   "subsurface_color" "clearcoat" "clearcoat_roughness"
5586            //   "transmission" "ior" "iridescence" "sheen" "anisotropy"
5587            //   "anisotropy_angle" "toon_bands" "shadow_softness"
5588            //   "outline_px" "outline_color" "highlight_color"
5589            // Value: number (or packed 0xRRGGBB for colour fields)
5590            "set_material" | "ตั้งวัสดุ" | "设置材质" | "マテリアル設定" | "재질설정" | "تنظیم_متریال" | "عيّن_المادة" | "קבע_חומר" | "میٹریل_مقرر_کرو" =>
5591            {
5592                let key = self.arg_str(&args, 0, "");
5593                let val = self.arg_num(&args, 1, 0.0)?;
5594                let mut gfx = self.gfx.borrow_mut();
5595                let mat = gfx
5596                    .material
5597                    .get_or_insert_with(crate::gfx::LingMaterial::default);
5598                match key.as_str() {
5599                    "albedo" => mat.albedo = val as u32,
5600                    "roughness" => mat.roughness = val as f32,
5601                    "metallic" => mat.metallic = val as f32,
5602                    "emission" => mat.emission = val as u32,
5603                    "emission_strength" => mat.emission_strength = val as f32,
5604                    "specular" => mat.specular = val as f32,
5605                    "specular_tint" => mat.specular_tint = val as f32,
5606                    "subsurface" => mat.subsurface = val as f32,
5607                    "subsurface_color" => mat.subsurface_color = val as u32,
5608                    "clearcoat" => mat.clearcoat = val as f32,
5609                    "clearcoat_roughness" => mat.clearcoat_roughness = val as f32,
5610                    "transmission" => mat.transmission = val as f32,
5611                    "ior" => mat.ior = val as f32,
5612                    "iridescence" => mat.iridescence = val as f32,
5613                    "sheen" => mat.sheen = val as f32,
5614                    "anisotropy" => mat.anisotropy = val as f32,
5615                    "anisotropy_angle" => mat.anisotropy_angle = val as f32,
5616                    "toon_bands" => mat.toon_bands = val as u32,
5617                    "shadow_softness" => mat.shadow_softness = val as f32,
5618                    "outline_px" => mat.outline_px = val as f32,
5619                    "outline_color" => mat.outline_color = val as u32,
5620                    "highlight_color" => mat.highlight_color = val as u32,
5621                    _ => {},
5622                }
5623                return Ok(Value::Unit);
5624            },
5625
5626            // ── reset_material() — disable material override ──
5627            // After this call, draws use the legacy compute_lit_color_linear path.
5628            "reset_material" | "รีเซ็ตวัสดุ" | "重置材质" | "マテリアルリセット" | "재질초기화" | "بازنشانی_متریال" | "أعد_ضبط_المادة" | "אפס_חומר" | "میٹریل_ری_سیٹ" =>
5629            {
5630                self.gfx.borrow_mut().material = None;
5631                return Ok(Value::Unit);
5632            },
5633
5634            // ── toon_outlines(thickness, color, threshold) ──
5635            // Enable vector-smooth ink outlines on depth discontinuities.
5636            //   thickness  — ink-line half-width in pixels (0 = off, 1.5 = anime default)
5637            //   color      — 0xRRGGBB ink colour (default black = 0)
5638            //   threshold  — depth delta that triggers an edge (0.05 recommended)
5639            "toon_outlines"
5640            | "ตั้งเส้นขอบการ์ตูน"
5641            | "卡通轮廓"
5642            | "トゥーンアウトライン"
5643            | "툰아웃라인" | "خطوط_کارتونی" | "حدود_كرتونية" | "קווי_מתאר_מצוירים" | "ٹون_آؤٹ_لائنز" => {
5644                let px = self.arg_num(&args, 0, 0.0)? as f32;
5645                let color = self.arg_num(&args, 1, 0.0)? as u32;
5646                let thresh = self.arg_num(&args, 2, 0.05)? as f32;
5647                let mut gfx = self.gfx.borrow_mut();
5648                gfx.toon.outline_px = px;
5649                gfx.toon.outline_color = color;
5650                gfx.toon.outline_thresh = thresh;
5651                return Ok(Value::Unit);
5652            },
5653
5654            // ── tone_stop(t, value) ──
5655            // Add a stop to the tone ramp.
5656            //   t      — input luminance position [0..1]
5657            //   value  — output brightness [0..1]
5658            // Stops are automatically sorted; call tone_ramp_reset() first to clear.
5659            "tone_stop" | "ตั้งจุดโทน" | "色调停止" | "トーンストップ" | "톤스톱" | "نقطه_توقف_تن_رنگ" | "نقطة_توقف_اللون" | "נקודת_עצירת_גוון" | "ٹون_اسٹاپ" =>
5660            {
5661                let t = self.arg_num(&args, 0, 0.0)? as f32;
5662                let val = self.arg_num(&args, 1, 1.0)? as f32;
5663                let mut gfx = self.gfx.borrow_mut();
5664                gfx.toon.ramp.stops.push(crate::gfx::toon::ToneStop {
5665                    t: t.clamp(0.0, 1.0),
5666                    value: val.clamp(0.0, 1.0),
5667                });
5668                gfx.toon
5669                    .ramp
5670                    .stops
5671                    .sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap_or(std::cmp::Ordering::Equal));
5672                return Ok(Value::Unit);
5673            },
5674
5675            // ── tone_smooth(enabled) ──
5676            // 0 = hard cel snap between stops (default); 1 = smooth gradient lerp.
5677            "tone_smooth" | "ตั้งโทนนุ่ม" | "色调平滑" | "トーンスムーズ" | "톤스무스" | "تن_رنگ_نرم" | "تدرج_لون_ناعم" | "גוון_חלק" | "ٹون_ہموار" =>
5678            {
5679                let v = self.arg_num(&args, 0, 0.0)? as f32;
5680                self.gfx.borrow_mut().toon.ramp.smooth = v > 0.5;
5681                return Ok(Value::Unit);
5682            },
5683
5684            // ── tone_bezier(y1, y2) ──
5685            // Apply a cubic Bézier remap to the input luminance before stop lookup.
5686            //   y1, y2 — control-point y-values (identity: y1=0.333 y2=0.667)
5687            //   0 args or tone_bezier(0, 0)  → ease-in (shadow-heavy)
5688            //   tone_bezier(1, 1)            → ease-out (highlight-heavy)
5689            //   tone_bezier(0.1, 0.9)        → S-curve (smooth both ends)
5690            //   tone_bezier_off()            → disable (back to linear)
5691            "tone_bezier" | "ตั้งโทนเบซิเยร์" | "色调贝塞尔" | "トーンベジェ" | "톤베지어" | "تن_رنگ_بزیه" | "تدرج_لون_بيزيه" | "גוון_בזייה" | "بیزیئر_ٹون" =>
5692            {
5693                let y1 = self.arg_num(&args, 0, 1.0 / 3.0)? as f32;
5694                let y2 = self.arg_num(&args, 1, 2.0 / 3.0)? as f32;
5695                self.gfx.borrow_mut().toon.ramp.bezier = Some([y1, y2]);
5696                return Ok(Value::Unit);
5697            },
5698
5699            // ── tone_bezier_off() — disable Bézier remap ──
5700            "tone_bezier_off"
5701            | "ปิดโทนเบซิเยร์"
5702            | "关闭色调贝塞尔"
5703            | "トーンベジェオフ"
5704            | "톤베지어끄기" | "خاموش‌کردن_بزیه" | "إيقاف_تدرج_بيزيه" | "כבה_גוון_בזייה" | "بیزیئر_ٹون_بند" => {
5705                self.gfx.borrow_mut().toon.ramp.bezier = None;
5706                return Ok(Value::Unit);
5707            },
5708
5709            // ── tone_ramp_reset() — restore default 3-band cel ramp ──
5710            "tone_ramp_reset"
5711            | "รีเซ็ตการไล่โทน"
5712            | "重置色调渐变"
5713            | "トーンランプリセット"
5714            | "톤램프리셋" | "بازنشانی_شیب_تن_رنگ" | "أعد_ضبط_تدرج_اللون" | "אפס_שיפוע_גוון" | "ٹون_ریمپ_ری_سیٹ" => {
5715                self.gfx.borrow_mut().toon.ramp = crate::gfx::toon::ToneRamp::default();
5716                return Ok(Value::Unit);
5717            },
5718
5719            // ── tone_ramp_clear() — clear all stops (build your own ramp) ──
5720            "tone_ramp_clear"
5721            | "ล้างการไล่โทน"
5722            | "清除色调渐变"
5723            | "トーンランプクリア"
5724            | "톤램프클리어" | "پاک‌کردن_شیب_تن_رنگ" | "امسح_تدرج_اللون" | "נקה_שיפוע_גוון" | "ٹون_ریمپ_صاف" => {
5725                self.gfx.borrow_mut().toon.ramp.stops.clear();
5726                return Ok(Value::Unit);
5727            },
5728
5729            // ── tone_soft(soft, sheen) — band-edge softness + highlight sheen ──
5730            //   soft  [0..1] — fraction of each band gap that blends smoothly
5731            //                  across the boundary (0 = crisp cel, ~0.3 = soft
5732            //                  Wind Waker shadow edges). Default 0.32.
5733            //   sheen [0..1] — bright pixels keep their smooth gradient instead
5734            //                  of being quantised (clean specular/rim sheen
5735            //                  rather than scratchy banded highlights). 0.65.
5736            "tone_soft" | "โทนขอบนุ่ม" | "色调柔边" | "トーンソフト" | "톤소프트" | "تن_رنگ_لبه‌نرم" | "تدرج_ناعم_الحواف" | "גוון_קצה_רך" | "نرم_کنارہ_ٹون" | "tonalité_douce" | "weicher_ton" | "мягкий_тон" => {
5737                let s = self.arg_num(&args, 0, 0.32)? as f32;
5738                let sh = self.arg_num(&args, 1, 0.65)? as f32;
5739                let mut gfx = self.gfx.borrow_mut();
5740                gfx.toon.ramp.soft = s.clamp(0.0, 1.0);
5741                gfx.toon.ramp.sheen = sh.clamp(0.0, 1.0);
5742                return Ok(Value::Unit);
5743            },
5744
5745            // ── set_ssao(strength, radius_px, zrange) — ambient occlusion ──
5746            // Depth-buffer contact shading: soft darkening in corners/under
5747            // objects, computed half-res + smoothed (no grain). Needs
5748            // set_depth_test(1). strength 0 disables. Defaults (0.35, 6, 12).
5749            "set_ssao" | "ตั้งเงาสัมผัส" | "环境光遮蔽" | "アンビエントオクルージョン"
5750            | "앰비언트오클루전" | "تنظیم_انسداد_محیطی" | "عيّن_تظليل_محيطي" | "קבע_הצללה_סביבתית" | "ایس_ایس_اے_او_مقرر_کرو" | "définir_ssao" | "ssao_setzen" | "задать_ssao" => {
5751                let s = self.arg_num(&args, 0, 0.35)? as f32;
5752                let r = self.arg_num(&args, 1, 6.0)? as f32;
5753                let z = self.arg_num(&args, 2, 12.0)? as f32;
5754                let mut gfx = self.gfx.borrow_mut();
5755                gfx.toon.ao_strength = s.clamp(0.0, 1.0);
5756                gfx.toon.ao_radius = r.max(1.0);
5757                gfx.toon.ao_range = z.max(0.01);
5758                return Ok(Value::Unit);
5759            },
5760
5761            // ── set_fxaa(on) — FXAA-lite screen-space edge anti-aliasing ──
5762            // Softens polygon stair-steps and ink-line jaggies over the whole
5763            // frame; flat fills are untouched. Applied last in the present
5764            // post-chain. (set_antialias smooths wireframe STROKES; this pass
5765            // smooths the composited IMAGE.)
5766            "set_fxaa" | "ลบรอยหยัก" | "屏幕抗锯齿" | "画面アンチエイリアス" | "화면안티앨리어싱" | "تنظیم_ضدلبه‌دندانه_سریع" | "عيّن_مضاد_التسنن_السريع" | "קבע_החלקת_מסך" | "ایف_ایکس_اے_اے_مقرر_کرو" | "définir_fxaa" | "fxaa_setzen" | "задать_fxaa" => {
5767                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
5768                self.gfx.borrow_mut().toon.fxaa = on;
5769                return Ok(Value::Unit);
5770            },
5771
5772            // ── set_bloom(strength, threshold) — soft HDR-style glow ──
5773            // Bright pixels (rim sheen, emissive, additive FX) bleed a soft
5774            // quarter-res glow — the "HDR material" feel for toon/vector art.
5775            // strength 0 disables; threshold = luminance cutoff [0..1].
5776            "set_bloom" | "ตั้งบลูม" | "泛光" | "ブルーム" | "블룸" | "تنظیم_درخشش" | "عيّن_التوهج" | "קבע_זוהר" | "بلوم_مقرر_کرو" | "définir_bloom" | "bloom_setzen" | "задать_блум" => {
5777                let s = self.arg_num(&args, 0, 0.45)? as f32;
5778                let t = self.arg_num(&args, 1, 0.74)? as f32;
5779                let mut gfx = self.gfx.borrow_mut();
5780                gfx.toon.bloom_strength = s.max(0.0);
5781                gfx.toon.bloom_thresh = t.clamp(0.0, 0.99);
5782                return Ok(Value::Unit);
5783            },
5784
5785            // ── shadow_smooth(softness) [compat] → tone_smooth + tone_bezier ──
5786            // Deprecated: use tone_smooth + tone_bezier instead.
5787            "shadow_smooth" | "ตั้งเงานุ่ม" | "柔化阴影" | "影ソフト" | "그림자부드럽게" | "سایه_نرم" | "ظل_ناعم" | "צל_חלק" | "نرم_سایہ" =>
5788            {
5789                let s = self.arg_num(&args, 0, 0.0)? as f32;
5790                let mut gfx = self.gfx.borrow_mut();
5791                gfx.toon.ramp.smooth = s > 0.05;
5792                if s > 0.05 {
5793                    let y1 = (0.333 + s * 0.2).clamp(0.0, 1.0);
5794                    let y2 = (0.667 - s * 0.2).clamp(0.0, 1.0);
5795                    gfx.toon.ramp.bezier = Some([y1, y2]);
5796                } else {
5797                    gfx.toon.ramp.bezier = None;
5798                }
5799                return Ok(Value::Unit);
5800            },
5801
5802            // ── toon_highlight [compat] — no-op, use tone_stop instead ──
5803            "toon_highlight"
5804            | "ตั้งไฮไลท์การ์ตูน"
5805            | "卡通高光"
5806            | "トゥーンハイライト"
5807            | "툰하이라이트" | "هایلایت_کارتونی" | "إبراز_كرتوني" | "הדגשה_מצוירת" | "ٹون_ہائی_لائٹ" => {
5808                // Remap as a lit-band brightness boost: adds a stop near the highlight threshold.
5809                let _strength = self.arg_num(&args, 0, 0.0)? as f32;
5810                let _thresh = self.arg_num(&args, 2, 0.78)? as f32;
5811                // No-op: configure via tone_stop() for precise control.
5812                return Ok(Value::Unit);
5813            },
5814
5815            // ── set_ambient(v) — ambient light level [0..1] ──
5816            "set_ambient" | "ตั้งแสงรอบข้าง" | "环境光" | "環境光設定" | "환경광설정" | "تنظیم_نور_محیطی" | "عيّن_الإضاءة_المحيطة" | "קבע_תאורה_סביבתית" | "ماحولیاتی_روشنی_مقرر_کرو" | "définir_ambiante" | "umgebungslicht_setzen" | "задать_фон" =>
5817            {
5818                let v = self.arg_num(&args, 0, 0.15)? as f32;
5819                self.gfx.borrow_mut().ambient = v;
5820                return Ok(Value::Unit);
5821            },
5822
5823            // ── set_fog(r,g,b, start, end) — distance fog toward (r,g,b).
5824            //    triangles/lines fade from `start`..`end` camera depth. end<=0 = off.
5825            "set_fog" | "ตั้งหมอก" | "雾" | "霧設定" | "안개설정" | "تنظیم_مه" | "عيّن_الضباب" | "קבע_ערפל" | "دھند_مقرر_کرو" => {
5826                let r = self.arg_num(&args, 0, 0.0)?.clamp(0.0, 255.0) as u32;
5827                let g = self.arg_num(&args, 1, 0.0)?.clamp(0.0, 255.0) as u32;
5828                let b = self.arg_num(&args, 2, 0.0)?.clamp(0.0, 255.0) as u32;
5829                let start = self.arg_num(&args, 3, 0.0)? as f32;
5830                let end = self.arg_num(&args, 4, 0.0)? as f32;
5831                let mut gfx = self.gfx.borrow_mut();
5832                gfx.fog_color = (r << 16) | (g << 8) | b;
5833                gfx.fog_start = start;
5834                gfx.fog_end = end;
5835                return Ok(Value::Unit);
5836            },
5837
5838            // ── วาดสามเหลี่ยม3มิติ(ax,ay,az, bx,by,bz, cx,cy,cz) ──
5839            // Computes lighting from world-space normal + active lights (cel shading),
5840            // projects via the stored camera, and pushes to the depth queue.
5841            "วาดสามเหลี่ยม3มิติ" | "draw_triangle_3d" | "triangle3d" | "رسم_مثلث_سه‌بعدی" | "ارسم_مثلثا_ثلاثي_الأبعاد" | "צייר_משולש_תלת_ממדי" | "تھری_ڈی_مثلث_کھینچو" =>
5842            {
5843                let ax = self.arg_num(&args, 0, 0.0)? as f32;
5844                let ay = self.arg_num(&args, 1, 0.0)? as f32;
5845                let az = self.arg_num(&args, 2, 0.0)? as f32;
5846                let bx = self.arg_num(&args, 3, 0.0)? as f32;
5847                let by = self.arg_num(&args, 4, 0.0)? as f32;
5848                let bz = self.arg_num(&args, 5, 0.0)? as f32;
5849                let cx = self.arg_num(&args, 6, 0.0)? as f32;
5850                let cy = self.arg_num(&args, 7, 0.0)? as f32;
5851                let cz = self.arg_num(&args, 8, 0.0)? as f32;
5852
5853                let mut gfx = self.gfx.borrow_mut();
5854
5855                // Mesh capture: record raw local coords + pen colour, skip submit.
5856                if gfx.mesh_capture.is_some() {
5857                    let col = gfx.color;
5858                    gfx.mesh_capture
5859                        .as_mut()
5860                        .unwrap()
5861                        .push(([ax, ay, az, bx, by, bz, cx, cy, cz], col));
5862                    return Ok(Value::Unit);
5863                }
5864
5865                gfx.submit_triangle(ax, ay, az, bx, by, bz, cx, cy, cz);
5866                return Ok(Value::Unit);
5867            },
5868
5869            // ── เริ่มอบเมช() — begin capturing 3-D triangles into a display list ──
5870            "เริ่มอบเมช" | "mesh_bake_begin" | "شروع_پخت_مش" | "ابدأ_خبز_الشبكة" | "התחל_אפיית_רשת" | "میش_بیک_شروع" => {
5871                self.gfx.borrow_mut().mesh_capture = Some(Vec::new());
5872                return Ok(Value::Unit);
5873            },
5874
5875            // ── เมชแคชรับ(key) — keyed display-list cache lookup (-1 = miss) ──
5876            "เมชแคชรับ" | "mesh_cache_get" | "دریافت_کش_مش" | "اجلب_مخبأ_الشبكة" | "קבל_מטמון_רשת" | "میش_کیش_حاصل_کرو" => {
5877                let key = self.arg_num(&args, 0, 0.0)? as i64;
5878                let h = self.gfx.borrow().mesh_cache.get(&key).copied();
5879                return Ok(Value::Number(h.map(|x| x as f64).unwrap_or(-1.0)));
5880            },
5881
5882            // ── เมชแคชตั้ง(key, handle) — store a baked mesh under key (bounded) ──
5883            "เมชแคชตั้ง" | "mesh_cache_put" | "ذخیره_در_کش_مش" | "ضع_في_مخبأ_الشبكة" | "שמור_במטמון_רשת" | "میش_کیش_رکھو" => {
5884                let key = self.arg_num(&args, 0, 0.0)? as i64;
5885                let h = self.arg_num(&args, 1, 0.0)? as usize;
5886                let mut gfx = self.gfx.borrow_mut();
5887                const CAP: usize = 256;
5888                if gfx.mesh_cache.len() >= CAP {
5889                    let evict: Vec<usize> = gfx.mesh_cache.values().copied().collect();
5890                    gfx.mesh_cache.clear();
5891                    for id in evict {
5892                        if id < gfx.meshes.len() {
5893                            gfx.meshes[id].clear();
5894                            gfx.mesh_free.push(id);
5895                        }
5896                    }
5897                }
5898                gfx.mesh_cache.insert(key, h);
5899                return Ok(Value::Unit);
5900            },
5901
5902            // ── เมชแคชล้าง() — drop the keyed cache (e.g. on level change) ──
5903            "เมชแคชล้าง" | "mesh_cache_clear" | "پاک‌کردن_کش_مش" | "امسح_مخبأ_الشبكة" | "נקה_מטמון_רשת" | "میش_کیش_صاف" => {
5904                let mut gfx = self.gfx.borrow_mut();
5905                let evict: Vec<usize> = gfx.mesh_cache.values().copied().collect();
5906                gfx.mesh_cache.clear();
5907                for id in evict {
5908                    if id < gfx.meshes.len() {
5909                        gfx.meshes[id].clear();
5910                        gfx.mesh_free.push(id);
5911                    }
5912                }
5913                return Ok(Value::Unit);
5914            },
5915
5916            // ── จบอบเมช() — bake captured triangles, return mesh handle ──
5917            "จบอบเมช" | "mesh_bake_end" | "پایان_پخت_مش" | "أنهِ_خبز_الشبكة" | "סיים_אפיית_רשת" | "میش_بیک_ختم" => {
5918                let mut gfx = self.gfx.borrow_mut();
5919                let tris = gfx.mesh_capture.take().unwrap_or_default();
5920                let id = gfx.mesh_register(tris);
5921                return Ok(Value::Number(id as f64));
5922            },
5923
5924            // ── วาดอบเมช[สี](id, ox,oy,oz, rx,ry,rz, ux,uy,uz, s) — draw a baked mesh ──
5925            //   วาดอบเมช: current pen colour (tinted glyphs)
5926            //   วาดอบเมชสี: per-triangle baked colour (multi-colour models)
5927            "วาดอบเมช" | "mesh_bake_draw" | "วาดอบเมชสี" | "mesh_bake_draw_col" | "رسم_مش_پخته" | "ارسم_شبكة_مخبوزة" | "צייר_רשת_אפויה" | "بیکڈ_میش_کھینچو" =>
5928            {
5929                let baked_col = matches!(name, "วาดอบเมชสี" | "mesh_bake_draw_col");
5930                let id = self.arg_num(&args, 0, 0.0)? as usize;
5931                let ox = self.arg_num(&args, 1, 0.0)? as f32;
5932                let oy = self.arg_num(&args, 2, 0.0)? as f32;
5933                let oz = self.arg_num(&args, 3, 0.0)? as f32;
5934                let rx = self.arg_num(&args, 4, 1.0)? as f32;
5935                let ry = self.arg_num(&args, 5, 0.0)? as f32;
5936                let rz = self.arg_num(&args, 6, 0.0)? as f32;
5937                let ux = self.arg_num(&args, 7, 0.0)? as f32;
5938                let uy = self.arg_num(&args, 8, 1.0)? as f32;
5939                let uz = self.arg_num(&args, 9, 0.0)? as f32;
5940                let s = self.arg_num(&args, 10, 1.0)? as f32;
5941                self.gfx
5942                    .borrow_mut()
5943                    .mesh_draw(id, ox, oy, oz, rx, ry, rz, ux, uy, uz, s, baked_col);
5944                return Ok(Value::Unit);
5945            },
5946
5947            // ── draw_quad_3d / draw_pent_3d / draw_hex_3d / draw_polygon_3d ──
5948            // Fan-triangulate convex n-gons.  Lighting, near-plane clip, fog, and
5949            // Gouraud shading all mirror draw_triangle_3d exactly.
5950            "draw_quad_3d"
5951            | "quad3d"
5952            | "วาดสี่เหลี่ยม3มิติ"
5953            | "draw_pent_3d"
5954            | "pent3d"
5955            | "วาดห้าเหลี่ยม3มิติ"
5956            | "draw_hex_3d"
5957            | "hex3d"
5958            | "วาดหกเหลี่ยม3มิติ"
5959            | "draw_polygon_3d"
5960            | "polygon3d"
5961            | "วาดรูปหลายเหลี่ยม3มิติ" | "رسم_چهارضلعی_سه‌بعدی" | "ارسم_رباعيا_ثلاثي_الأبعاد" | "צייר_מרובע_תלת_ממדי" | "تھری_ڈی_چوکور_کھینچو" => {
5962                // Collect (wx, wy, wz) triples from args or list
5963                let mut wxs: [f32; 8] = [0.0; 8];
5964                let mut wys: [f32; 8] = [0.0; 8];
5965                let mut wzs: [f32; 8] = [0.0; 8];
5966                let n_verts;
5967
5968                if args.len() == 1 {
5969                    // draw_polygon_3d([x0,y0,z0, x1,y1,z1, ...])
5970                    let list = match &args[0] {
5971                        Value::List(l) => l.clone(),
5972                        _ => {
5973                            return Err(EvalErr::from("draw_polygon_3d: expected list".to_string()))
5974                        },
5975                    };
5976                    let coords: Vec<f32> = list
5977                        .iter()
5978                        .map(|v| match v {
5979                            Value::Number(n) => *n as f32,
5980                            _ => 0.0,
5981                        })
5982                        .collect();
5983                    n_verts = (coords.len() / 3).min(8);
5984                    for i in 0..n_verts {
5985                        wxs[i] = coords[i * 3];
5986                        wys[i] = coords[i * 3 + 1];
5987                        wzs[i] = coords[i * 3 + 2];
5988                    }
5989                } else {
5990                    // draw_quad/pent/hex_3d(x0,y0,z0, x1,y1,z1, ...)
5991                    n_verts = (args.len() / 3).min(8);
5992                    for i in 0..n_verts {
5993                        wxs[i] = self.arg_num(&args, i * 3, 0.0)? as f32;
5994                        wys[i] = self.arg_num(&args, i * 3 + 1, 0.0)? as f32;
5995                        wzs[i] = self.arg_num(&args, i * 3 + 2, 0.0)? as f32;
5996                    }
5997                }
5998                if n_verts < 3 {
5999                    return Ok(Value::Unit);
6000                }
6001
6002                let mut gfx = self.gfx.borrow_mut();
6003
6004                // Mesh capture: fan-triangulate and record raw local coords +
6005                // pen colour, exactly like วาดสามเหลี่ยม3มิติ. Quads used to
6006                // fall through here and bake EMPTY display lists — the 3-D
6007                // glyph fonts (letter pickups) are built from draw_quad_3d,
6008                // which made every baked glyph invisible.
6009                if gfx.mesh_capture.is_some() {
6010                    let col = gfx.color;
6011                    let cap = gfx.mesh_capture.as_mut().unwrap();
6012                    for i in 1..n_verts - 1 {
6013                        cap.push((
6014                            [
6015                                wxs[0], wys[0], wzs[0],
6016                                wxs[i], wys[i], wzs[i],
6017                                wxs[i + 1], wys[i + 1], wzs[i + 1],
6018                            ],
6019                            col,
6020                        ));
6021                    }
6022                    return Ok(Value::Unit);
6023                }
6024
6025                // Face normal from first triangle of the fan
6026                let normal = crate::gfx::poly::face_normal(
6027                    wxs[0], wys[0], wzs[0], wxs[1], wys[1], wzs[1], wxs[2], wys[2], wzs[2],
6028                );
6029
6030                // Per-vertex lit colours
6031                let mut wcs: [u32; 8] = [0; 8];
6032                if gfx.flat_shade {
6033                    let c = gfx.color;
6034                    for wc in wcs.iter_mut().take(n_verts) {
6035                        *wc = c;
6036                    }
6037                } else if let Some(ref mat) = gfx.material.clone() {
6038                    let cam = [gfx.camera.cx, gfx.camera.cy, gfx.camera.zdist];
6039                    let lights: Vec<_> = gfx.lights.clone();
6040                    let ambient = gfx.ambient;
6041                    for i in 0..n_verts {
6042                        let v = [wxs[i], wys[i], wzs[i]];
6043                        let vd = [cam[0] - v[0], cam[1] - v[1], cam[2] - v[2]];
6044                        wcs[i] = crate::gfx::material::shade(mat, normal, vd, v, &lights, ambient);
6045                    }
6046                } else {
6047                    let base = gfx.color;
6048                    let lights: Vec<_> = gfx.lights.clone();
6049                    let ambient = gfx.ambient;
6050                    for i in 0..n_verts {
6051                        wcs[i] = crate::gfx::light::compute_lit_color_linear(
6052                            base,
6053                            normal,
6054                            [wxs[i], wys[i], wzs[i]],
6055                            &lights,
6056                            ambient,
6057                        );
6058                    }
6059                }
6060
6061                // Near-plane clip (Sutherland-Hodgman per vertex)
6062                let near = -gfx.camera.zdist + 0.05;
6063                let mut clip_in: [(f32, f32, f32, f32, u32); crate::gfx::poly::MAX_CLIP_VERTS] =
6064                    [(0.0, 0.0, 0.0, 0.0, 0); crate::gfx::poly::MAX_CLIP_VERTS];
6065                for i in 0..n_verts {
6066                    let d = gfx.camera.depth(wxs[i], wys[i], wzs[i]);
6067                    clip_in[i] = (wxs[i], wys[i], wzs[i], d, wcs[i]);
6068                }
6069                let mut clip_out: [(f32, f32, f32, f32, u32); crate::gfx::poly::MAX_CLIP_VERTS] =
6070                    [(0.0, 0.0, 0.0, 0.0, 0); crate::gfx::poly::MAX_CLIP_VERTS];
6071                let pn = crate::gfx::poly::clip_near(&clip_in, n_verts, near, &mut clip_out);
6072                if pn < 3 {
6073                    return Ok(Value::Unit);
6074                }
6075
6076                // Project + fog
6077                let mut proj: [(f32, f32, f32, u32); crate::gfx::poly::MAX_CLIP_VERTS] =
6078                    [(0.0, 0.0, 0.0, 0); crate::gfx::poly::MAX_CLIP_VERTS];
6079                for i in 0..pn {
6080                    let (sx, sy, sz) =
6081                        gfx.camera
6082                            .project(clip_out[i].0, clip_out[i].1, clip_out[i].2);
6083                    let fc = gfx.fog_apply(clip_out[i].4, sz);
6084                    proj[i] = (sx, sy, sz, fc);
6085                }
6086
6087                // Fan-triangulate and push
6088                let unlit = gfx.flat_shade;
6089                crate::gfx::poly::fan_emit_proj(
6090                    &proj,
6091                    pn,
6092                    |x0, y0, z0, c0, x1, y1, z1, c1, x2, y2, z2, c2| {
6093                        gfx.depth_queue.push_triangle_g_zv(
6094                            x0, y0, z0, c0, x1, y1, z1, c1, x2, y2, z2, c2, 3, unlit,
6095                        );
6096                    },
6097                );
6098                return Ok(Value::Unit);
6099            },
6100
6101            // ── วาดเส้น3มิติ(ax,ay,az, bx,by,bz) ──
6102            // Projects two world-space points via the stored camera and pushes
6103            // a line to the depth queue.
6104            "วาดเส้น3มิติ" | "draw_line_3d" | "line3d" | "画3D线" | "3D線描く" | "3D선그리기" | "رسم_خط_سه‌بعدی" | "ارسم_خطا_ثلاثي_الأبعاد" | "צייר_קו_תלת_ממדי" | "تھری_ڈی_لکیر_کھینچو" =>
6105            {
6106                let ax = self.arg_num(&args, 0, 0.0)? as f32;
6107                let ay = self.arg_num(&args, 1, 0.0)? as f32;
6108                let az = self.arg_num(&args, 2, 0.0)? as f32;
6109                let bx = self.arg_num(&args, 3, 0.0)? as f32;
6110                let by = self.arg_num(&args, 4, 0.0)? as f32;
6111                let bz = self.arg_num(&args, 5, 0.0)? as f32;
6112
6113                let mut gfx = self.gfx.borrow_mut();
6114                let color = gfx.color;
6115                // Near-plane clip in 3-D before perspective divide
6116                let near = -gfx.camera.zdist + 0.05;
6117                let mut lax = ax;
6118                let mut lay = ay;
6119                let mut laz = az;
6120                let mut lbx = bx;
6121                let mut lby = by;
6122                let mut lbz = bz;
6123                let da_raw = gfx.camera.depth(lax, lay, laz);
6124                let db_raw = gfx.camera.depth(lbx, lby, lbz);
6125                if da_raw <= near && db_raw <= near {
6126                    return Ok(Value::Unit);
6127                }
6128                if da_raw <= near {
6129                    let t = (near - da_raw) / (db_raw - da_raw);
6130                    lax += t * (lbx - lax);
6131                    lay += t * (lby - lay);
6132                    laz += t * (lbz - laz);
6133                } else if db_raw <= near {
6134                    let t = (near - da_raw) / (db_raw - da_raw);
6135                    lbx = lax + t * (lbx - lax);
6136                    lby = lay + t * (lby - lay);
6137                    lbz = laz + t * (lbz - laz);
6138                }
6139                // Shared-edge dedup: skip if this world-space edge was already queued.
6140                if !gfx.edge_set.try_insert(lax, lay, laz, lbx, lby, lbz) {
6141                    return Ok(Value::Unit);
6142                }
6143                let (sax, say, da) = gfx.camera.project(lax, lay, laz);
6144                let (sbx, sby, db) = gfx.camera.project(lbx, lby, lbz);
6145                let depth = (da + db) / 2.0;
6146                let color = gfx.fog_apply(color, depth);
6147                gfx.depth_queue.push_line(depth, color, sax, say, sbx, sby);
6148                return Ok(Value::Unit);
6149            },
6150
6151            // orb_shell(cx,cy,cz, radius, rot_y, rot_x, density, r,g,b)
6152            //   A single trippy, grayscale, depth-faded vector pattern wound around
6153            //   a sphere — two families of interleaved spherical spirals (a guilloché
6154            //   weave), NOT a lat/long cage. Each segment's brightness follows its
6155            //   facing (front bright, back dim), so it reads as a translucent
6156            //   grayscale "texture" with alpha rather than a hard wireframe; the
6157            //   inner marble shows through. `rot_y`/`rot_x` roll the texture around
6158            //   the orb; `density` = spirals per winding direction. r,g,b tint it
6159            //   (pass a gray like 230,230,230 for pure grayscale).
6160            #[cfg(not(target_arch = "wasm32"))]
6161            "orb_shell" | "球壳" | "オーブ殻" | "오브껍질" | "เปลือกทรงกลม" | "پوسته_کروی" | "قشرة_كروية" | "קליפת_כדור" | "کروی_خول" | "coque_orbe" | "orb_hülle" | "оболочка_сферы" =>
6162            {
6163                let cx = self.arg_num(&args, 0, 0.)? as f32;
6164                let cy = self.arg_num(&args, 1, 0.)? as f32;
6165                let cz = self.arg_num(&args, 2, 0.)? as f32;
6166                let radius = self.arg_num(&args, 3, 1.0)? as f32;
6167                let ry = self.arg_num(&args, 4, 0.)? as f32;
6168                let rx = self.arg_num(&args, 5, 0.)? as f32;
6169                let density = (self.arg_num(&args, 6, 10.)? as i32).clamp(1, 48);
6170                let tr = (self.arg_num(&args, 7, 230.)? as f32).clamp(0., 255.);
6171                let tg = (self.arg_num(&args, 8, 230.)? as f32).clamp(0., 255.);
6172                let tb = (self.arg_num(&args, 9, 235.)? as f32).clamp(0., 255.);
6173                let (cyr, syr) = (ry.cos(), ry.sin());
6174                let (cxr, sxr) = (rx.cos(), rx.sin());
6175                let tau = std::f32::consts::TAU;
6176                let pi = std::f32::consts::PI;
6177                let turns = 6.0_f32; // how many times each spiral wraps pole→pole
6178                let nseg = 96; // segments per spiral (smoothness)
6179                let inv_r = if radius.abs() > 1e-5 {
6180                    1.0 / radius
6181                } else {
6182                    0.0
6183                };
6184                // a point along a spiral (param u 0..1, start angle theta0, winding dir),
6185                // spun by ry/rx — returns (world point, facing 0..1 where 1 = toward camera)
6186                let pt = |u: f32, theta0: f32, dir: f32| -> ([f32; 3], f32) {
6187                    let phi = pi * u; // 0..pi  (north → south)
6188                    let th = dir * turns * tau * u + theta0;
6189                    let (mut x, y, mut z) = (
6190                        phi.sin() * th.cos() * radius,
6191                        phi.cos() * radius,
6192                        phi.sin() * th.sin() * radius,
6193                    );
6194                    let x1 = x * cyr + z * syr; // yaw about Y
6195                    let z1 = -x * syr + z * cyr;
6196                    x = x1;
6197                    z = z1;
6198                    let y2 = y * cxr - z * sxr; // pitch about X
6199                    let z2 = y * sxr + z * cxr;
6200                    // facing: camera sits at -zdist looking +z, so smaller z2 = nearer = brighter
6201                    let facing = (0.5 - 0.5 * z2 * inv_r).clamp(0.0, 1.0);
6202                    ([cx + x, cy + y2, cz + z2], facing)
6203                };
6204                let mut gfx = self.gfx.borrow_mut();
6205                let near = -gfx.camera.zdist + 0.05;
6206                // draw one segment (near-clipped) in a grayscale tint scaled by `lum`
6207                let seg = |gfx: &mut crate::gfx::GfxState, a: [f32; 3], b: [f32; 3], lum: f32| {
6208                    let (mut lax, mut lay, mut laz) = (a[0], a[1], a[2]);
6209                    let (mut lbx, mut lby, mut lbz) = (b[0], b[1], b[2]);
6210                    let da = gfx.camera.depth(lax, lay, laz);
6211                    let db = gfx.camera.depth(lbx, lby, lbz);
6212                    if da <= near && db <= near {
6213                        return;
6214                    }
6215                    if da <= near {
6216                        let t = (near - da) / (db - da);
6217                        lax += t * (lbx - lax);
6218                        lay += t * (lby - lay);
6219                        laz += t * (lbz - laz);
6220                    } else if db <= near {
6221                        let t = (near - da) / (db - da);
6222                        lbx = lax + t * (lbx - lax);
6223                        lby = lay + t * (lby - lay);
6224                        lbz = laz + t * (lbz - laz);
6225                    }
6226                    let (sax, say, da2) = gfx.camera.project(lax, lay, laz);
6227                    let (sbx, sby, db2) = gfx.camera.project(lbx, lby, lbz);
6228                    // grayscale-alpha: front-facing bright, back faded toward black
6229                    let l = (0.12 + 0.88 * lum).clamp(0.0, 1.0);
6230                    let cr = (tr * l) as u32;
6231                    let cg = (tg * l) as u32;
6232                    let cb = (tb * l) as u32;
6233                    let color = (cr << 16) | (cg << 8) | cb;
6234                    gfx.depth_queue
6235                        .push_line((da2 + db2) * 0.5, color, sax, say, sbx, sby);
6236                };
6237                // two opposite winding directions → a soft guilloché weave (not a cage)
6238                for &dir in &[1.0_f32, -1.0_f32] {
6239                    for s in 0..density {
6240                        let theta0 = s as f32 * tau / density as f32;
6241                        let mut prev = pt(0.0, theta0, dir);
6242                        for k in 1..=nseg {
6243                            let cur = pt(k as f32 / nseg as f32, theta0, dir);
6244                            seg(&mut gfx, prev.0, cur.0, (prev.1 + cur.1) * 0.5);
6245                            prev = cur;
6246                        }
6247                    }
6248                }
6249                return Ok(Value::Unit);
6250            },
6251
6252            // orb_particles(cx,cy,cz, radius, count, t, r,g,b)
6253            //   Fills the VOLUME of a sphere with `count` swirling vector points —
6254            //   like motes suspended inside a snow-globe orb. Points are distributed
6255            //   uniformly through the ball, slowly tumble as a cloud + wobble
6256            //   individually over time `t`, and are depth-shaded (near = bright,
6257            //   far = dim) so the cloud has real volume. Additive, so it layers under
6258            //   a shell / over a liquid marble.
6259            #[cfg(not(target_arch = "wasm32"))]
6260            "orb_particles" | "球内粒子" | "オーブ粒子" | "오브입자" | "อนุภาคทรงกลม" | "ذرات_کروی" | "جسيمات_كروية" | "חלקיקי_כדור" | "کروی_ذرات" | "particules_orbe" | "orb_partikel" | "частицы_сферы" =>
6261            {
6262                let cx = self.arg_num(&args, 0, 0.)? as f32;
6263                let cy = self.arg_num(&args, 1, 0.)? as f32;
6264                let cz = self.arg_num(&args, 2, 0.)? as f32;
6265                let radius = self.arg_num(&args, 3, 1.0)? as f32;
6266                let count = (self.arg_num(&args, 4, 160.)? as i32).clamp(1, 4000);
6267                let t = self.arg_num(&args, 5, 0.)? as f32;
6268                let tr = (self.arg_num(&args, 6, 255.)? as f32).clamp(0., 255.);
6269                let tg = (self.arg_num(&args, 7, 255.)? as f32).clamp(0., 255.);
6270                let tb = (self.arg_num(&args, 8, 255.)? as f32).clamp(0., 255.);
6271                let inv_r = if radius.abs() > 1e-5 {
6272                    1.0 / radius
6273                } else {
6274                    0.0
6275                };
6276                // cheap deterministic hash → [0,1)
6277                let h = |mut x: u32| -> f32 {
6278                    x = x.wrapping_mul(747796405).wrapping_add(2891336453);
6279                    x = ((x >> ((x >> 28).wrapping_add(4))) ^ x).wrapping_mul(277803737);
6280                    (((x >> 22) ^ x) & 0xFFFFFF) as f32 / 16_777_216.0
6281                };
6282                let tau = std::f32::consts::TAU;
6283                // slow tumble of the whole cloud
6284                let (cyr, syr) = ((t * 0.5).cos(), (t * 0.5).sin());
6285                let (cxr, sxr) = ((t * 0.23).cos(), (t * 0.23).sin());
6286                let mut gfx = self.gfx.borrow_mut();
6287                let near = -gfx.camera.zdist + 0.05;
6288                let (sw, sh) = (gfx.width as i32, gfx.height as i32);
6289                for i in 0..count {
6290                    let i = i as u32;
6291                    // uniform-in-volume: r = cbrt(u) * radius; direction from two hashes
6292                    let u = h(i.wrapping_mul(3) + 1);
6293                    let rr = u.cbrt() * radius * (0.85 + 0.15 * (t * 1.3 + i as f32).sin()); // gentle pulse
6294                    let th = h(i.wrapping_mul(3) + 2) * tau + t * (0.3 + 0.5 * h(i * 7 + 5)); // per-mote orbit
6295                    let ph = (h(i.wrapping_mul(3) + 3) * 2.0 - 1.0).acos(); // uniform cos(phi)
6296                    let (mut x, y, mut z) = (
6297                        rr * ph.sin() * th.cos(),
6298                        rr * ph.cos(),
6299                        rr * ph.sin() * th.sin(),
6300                    );
6301                    // tumble the cloud (yaw then pitch)
6302                    let x1 = x * cyr + z * syr;
6303                    let z1 = -x * syr + z * cyr;
6304                    x = x1;
6305                    z = z1;
6306                    let y2 = y * cxr - z * sxr;
6307                    let z2 = y * sxr + z * cxr;
6308                    let (wx, wy, wz) = (cx + x, cy + y2, cz + z2);
6309                    if gfx.camera.depth(wx, wy, wz) <= near {
6310                        continue;
6311                    }
6312                    let (sx, sy, dep) = gfx.camera.project(wx, wy, wz);
6313                    let sxi = sx as i32;
6314                    let syi = sy as i32;
6315                    if sxi < 0 || syi < 0 || sxi >= sw || syi >= sh {
6316                        continue;
6317                    }
6318                    // depth-shade: nearer (smaller z2) = brighter
6319                    let facing = (0.5 - 0.5 * z2 * inv_r).clamp(0.15, 1.0);
6320                    let l = facing;
6321                    let cr = (tr * l) as u32;
6322                    let cg = (tg * l) as u32;
6323                    let cb = (tb * l) as u32;
6324                    let color = (cr << 16) | (cg << 8) | cb;
6325                    // a 1–2px dot (bigger when near) as a short segment in the depth queue
6326                    let len = if facing > 0.7 { 1.0 } else { 0.0 };
6327                    gfx.depth_queue.push_line(dep, color, sx, sy, sx + len, sy);
6328                }
6329                return Ok(Value::Unit);
6330            },
6331
6332            // project_3d(x,y,z) -> [screen_x, screen_y, depth]; behind the camera
6333            // returns a sentinel ([-99999,-99999, depth]) so scripts can skip it.
6334            // Lets scripts place 2-D overlays (e.g. filled teardrop flames) onto 3-D points.
6335            "project_3d" | "投影3D" | "3D投影" | "3D투영" | "ฉาย3มิติ" | "فرافکنی_سه‌بعدی" | "إسقاط_ثلاثي_الأبعاد" | "הטלה_תלת_ממדית" | "تھری_ڈی_پروجیکشن" | "projeter_3d" | "projizieren_3d" | "проекция_3d" => {
6336                let x = self.arg_num(&args, 0, 0.0)? as f32;
6337                let y = self.arg_num(&args, 1, 0.0)? as f32;
6338                let z = self.arg_num(&args, 2, 0.0)? as f32;
6339                let gfx = self.gfx.borrow();
6340                let near = -gfx.camera.zdist + 0.05;
6341                let d = gfx.camera.depth(x, y, z);
6342                if d <= near {
6343                    return Ok(Value::List(Rc::new(vec![
6344                        Value::Number(-99999.0),
6345                        Value::Number(-99999.0),
6346                        Value::Number(d as f64),
6347                    ])));
6348                }
6349                let (sx, sy, depth) = gfx.camera.project(x, y, z);
6350                return Ok(Value::List(Rc::new(vec![
6351                    Value::Number(sx as f64),
6352                    Value::Number(sy as f64),
6353                    Value::Number(depth as f64),
6354                ])));
6355            },
6356
6357            // mouse_ray() -> [ox,oy,oz, dx,dy,dz] — world-space ray from the eye
6358            // through the actual mouse cursor pixel, exact inverse of project_3d's
6359            // pipeline (translate → Y-rotate → X-rotate → perspective divide by
6360            // rz+zdist). Scripts previously marched a ray along the CENTRE-SCREEN
6361            // forward vector regardless of where the cursor was — accurate only by
6362            // coincidence when the cursor happened to sit near the crosshair.
6363            #[cfg(not(target_arch = "wasm32"))]
6364            "mouse_ray" => {
6365                let gfx = self.gfx.borrow();
6366                let (mx, my) = gfx
6367                    .window
6368                    .as_ref()
6369                    .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
6370                    .unwrap_or((gfx.camera.cx, gfx.camera.cy));
6371                let cam = &gfx.camera;
6372                // Eye-relative pinhole direction for this pixel, in rotation-space
6373                // (before undoing the Y-then-X rotation project() applied).
6374                let dcx = (mx - cam.cx) / cam.focal;
6375                let dcy = (my - cam.cy) / cam.focal;
6376                // Undo the X-rotation, then the Y-rotation (reverse of project()'s
6377                // forward order), on the direction vector (dcx, dcy, 1.0).
6378                let a_x = dcx;
6379                let a_y = cam.crx * dcy + cam.srx * 1.0;
6380                let a_z = 0.0 - cam.srx * dcy + cam.crx * 1.0;
6381                let dir_x = cam.cry * a_x + cam.sry * a_z;
6382                let dir_y = a_y;
6383                let dir_z = 0.0 - cam.sry * a_x + cam.cry * a_z;
6384                let dlen = (dir_x * dir_x + dir_y * dir_y + dir_z * dir_z)
6385                    .sqrt()
6386                    .max(1e-6);
6387                let (dir_x, dir_y, dir_z) = (dir_x / dlen, dir_y / dlen, dir_z / dlen);
6388                // Origin: the camera's rotation pivot (tx,ty,tz) — i.e. wherever
6389                // the script last put it with set_camera_pos, NOT the "true"
6390                // pinhole eye zdist further back. The pivot is what scripts
6391                // already keep clear of the ground (a ground-collision pull-in
6392                // loop is standard practice for orbit cameras); the true eye
6393                // would need its own separate ground clearance since zdist is
6394                // often large relative to a close-in camera distance, and
6395                // starting a ray underground makes it hit "ground" instantly
6396                // regardless of aim. The zdist offset only matters for the
6397                // near-field parallax, which a click-to-move ray (aimed at
6398                // terrain many units out) doesn't need.
6399                let ox = cam.tx;
6400                let oy = cam.ty;
6401                let oz = cam.tz;
6402                return Ok(Value::List(Rc::new(vec![
6403                    Value::Number(ox as f64),
6404                    Value::Number(oy as f64),
6405                    Value::Number(oz as f64),
6406                    Value::Number(dir_x as f64),
6407                    Value::Number(dir_y as f64),
6408                    Value::Number(dir_z as f64),
6409                ])));
6410            },
6411            #[cfg(target_arch = "wasm32")]
6412            "mouse_ray" => {
6413                let gfx = self.gfx.borrow();
6414                let mx = crate::gfx::wasm_mouse_x();
6415                let my = crate::gfx::wasm_mouse_y();
6416                let cam = &gfx.camera;
6417                let dcx = (mx - cam.cx) / cam.focal;
6418                let dcy = (my - cam.cy) / cam.focal;
6419                let a_x = dcx;
6420                let a_y = cam.crx * dcy + cam.srx * 1.0;
6421                let a_z = 0.0 - cam.srx * dcy + cam.crx * 1.0;
6422                let dir_x = cam.cry * a_x + cam.sry * a_z;
6423                let dir_y = a_y;
6424                let dir_z = 0.0 - cam.sry * a_x + cam.cry * a_z;
6425                let dlen = (dir_x * dir_x + dir_y * dir_y + dir_z * dir_z)
6426                    .sqrt()
6427                    .max(1e-6);
6428                let (dir_x, dir_y, dir_z) = (dir_x / dlen, dir_y / dlen, dir_z / dlen);
6429                let ox = cam.tx;
6430                let oy = cam.ty;
6431                let oz = cam.tz;
6432                return Ok(Value::List(Rc::new(vec![
6433                    Value::Number(ox as f64),
6434                    Value::Number(oy as f64),
6435                    Value::Number(oz as f64),
6436                    Value::Number(dir_x as f64),
6437                    Value::Number(dir_y as f64),
6438                    Value::Number(dir_z as f64),
6439                ])));
6440            },
6441            // draw_poly([x0,y0,x1,y1,…]) — filled 2-D polygon in the current colour,
6442            // honouring the blend mode (additive → translucent glow). Auto-closes.
6443            #[cfg(not(target_arch = "wasm32"))]
6444            "draw_poly" | "填充多边形" | "ポリゴン塗り" | "다각형채우기" | "เติมรูปหลายเหลี่ยม" | "رسم_چندضلعی" | "ارسم_مضلع" | "צייר_מצולע" | "کثیر_الاضلاع_کھینچو" | "dessiner_polygone" | "polygon_zeichnen" | "рисовать_полигон" =>
6445            {
6446                let mut pts: Vec<[f32; 2]> = Vec::new();
6447                if let Some(Value::List(v)) = args.first() {
6448                    let mut i = 0;
6449                    while i + 1 < v.len() {
6450                        let x = self.to_number(&v[i]).unwrap_or(0.0) as f32;
6451                        let y = self.to_number(&v[i + 1]).unwrap_or(0.0) as f32;
6452                        pts.push([x, y]);
6453                        i += 2;
6454                    }
6455                }
6456                if pts.len() >= 3 {
6457                    if pts[0] != pts[pts.len() - 1] {
6458                        let p0 = pts[0];
6459                        pts.push(p0);
6460                    } // close
6461                    let mut gfx = self.gfx.borrow_mut();
6462                    let (w, h, color, add) = (gfx.width, gfx.height, gfx.color, gfx.blend == 1);
6463                    crate::gfx::raster::fill_contours_aa(
6464                        &mut gfx.buffer,
6465                        w,
6466                        h,
6467                        color,
6468                        add,
6469                        std::slice::from_ref(&pts),
6470                    );
6471                }
6472                return Ok(Value::Unit);
6473            },
6474
6475            // ══════════════════════════════════════════════════════════════════
6476            // VECTOR TEXTURE BUILTINS  (src/gfx/vtex.rs)
6477            // All patterns are depth-biased so they appear on top of surfaces.
6478            // Plane defined by: centre (cx,cy,cz) + U tangent + V tangent.
6479            // Last two args always: fr (frame f32), hue (phase offset f32).
6480            // ══════════════════════════════════════════════════════════════════
6481
6482            // vtex_grid(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cw,ch, fr,hue)
6483            "vtex_grid" | "ลายตาราง" | "纹格" | "格子模様" | "격자무늬" | "الگوی_شبکه" | "نقش_شبكة" | "דוגמת_רשת" | "نقش_جالی" | "motif_grille" | "muster_gitter" | "узор_сетка" =>
6484            {
6485                let cx = self.arg_num(&args, 0, 0.)? as f32;
6486                let cy = self.arg_num(&args, 1, 0.)? as f32;
6487                let cz = self.arg_num(&args, 2, 0.)? as f32;
6488                let ux = self.arg_num(&args, 3, 1.)? as f32;
6489                let uy = self.arg_num(&args, 4, 0.)? as f32;
6490                let uz = self.arg_num(&args, 5, 0.)? as f32;
6491                let vx = self.arg_num(&args, 6, 0.)? as f32;
6492                let vy = self.arg_num(&args, 7, 0.)? as f32;
6493                let vz = self.arg_num(&args, 8, 1.)? as f32;
6494                let cols = self.arg_num(&args, 9, 10.)? as usize;
6495                let rows = self.arg_num(&args, 10, 10.)? as usize;
6496                let cw = self.arg_num(&args, 11, 1.)? as f32;
6497                let ch = self.arg_num(&args, 12, 1.)? as f32;
6498                let fr = self.arg_num(&args, 13, 0.)? as f32;
6499                let hue = self.arg_num(&args, 14, 0.)? as f32;
6500                let mut gfx = self.gfx.borrow_mut();
6501                let cam = gfx.camera.clone();
6502                crate::gfx::vtex::draw_grid(
6503                    &mut gfx.depth_queue,
6504                    &cam,
6505                    cx,
6506                    cy,
6507                    cz,
6508                    ux,
6509                    uy,
6510                    uz,
6511                    vx,
6512                    vy,
6513                    vz,
6514                    cols,
6515                    rows,
6516                    cw,
6517                    ch,
6518                    fr,
6519                    hue,
6520                );
6521                return Ok(Value::Unit);
6522            },
6523
6524            // vtex_rings(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_rings,n_sides, max_r,twist, fr,hue)
6525            "vtex_rings" | "ลายวงซ้อน" | "纹环" | "同心円" | "동심원" | "الگوی_حلقه" | "نقش_حلقات" | "דוגמת_טבעות" | "نقش_حلقے" | "motif_anneaux" | "muster_ringe" | "узор_кольца" => {
6526                let cx = self.arg_num(&args, 0, 0.)? as f32;
6527                let cy = self.arg_num(&args, 1, 0.)? as f32;
6528                let cz = self.arg_num(&args, 2, 0.)? as f32;
6529                let ux = self.arg_num(&args, 3, 1.)? as f32;
6530                let uy = self.arg_num(&args, 4, 0.)? as f32;
6531                let uz = self.arg_num(&args, 5, 0.)? as f32;
6532                let vx = self.arg_num(&args, 6, 0.)? as f32;
6533                let vy = self.arg_num(&args, 7, 0.)? as f32;
6534                let vz = self.arg_num(&args, 8, 1.)? as f32;
6535                let nr = self.arg_num(&args, 9, 6.)? as usize;
6536                let ns = self.arg_num(&args, 10, 6.)? as usize;
6537                let mr = self.arg_num(&args, 11, 3.)? as f32;
6538                let tw = self.arg_num(&args, 12, 0.)? as f32;
6539                let fr = self.arg_num(&args, 13, 0.)? as f32;
6540                let hue = self.arg_num(&args, 14, 0.)? as f32;
6541                let mut gfx = self.gfx.borrow_mut();
6542                let cam = gfx.camera.clone();
6543                crate::gfx::vtex::draw_rings(
6544                    &mut gfx.depth_queue,
6545                    &cam,
6546                    cx,
6547                    cy,
6548                    cz,
6549                    ux,
6550                    uy,
6551                    uz,
6552                    vx,
6553                    vy,
6554                    vz,
6555                    nr,
6556                    ns,
6557                    mr,
6558                    tw,
6559                    fr,
6560                    hue,
6561                );
6562                return Ok(Value::Unit);
6563            },
6564
6565            // vtex_star(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_pts,r_out,r_in, rot_speed, fr,hue)
6566            "vtex_star" | "ลายดาว" | "纹星" | "星模様" | "별무늬" | "الگوی_ستاره" | "نقش_نجمة" | "דוגמת_כוכב" | "نقش_ستارہ" | "motif_étoile" | "muster_stern" | "узор_звезда" => {
6567                let cx = self.arg_num(&args, 0, 0.)? as f32;
6568                let cy = self.arg_num(&args, 1, 0.)? as f32;
6569                let cz = self.arg_num(&args, 2, 0.)? as f32;
6570                let ux = self.arg_num(&args, 3, 1.)? as f32;
6571                let uy = self.arg_num(&args, 4, 0.)? as f32;
6572                let uz = self.arg_num(&args, 5, 0.)? as f32;
6573                let vx = self.arg_num(&args, 6, 0.)? as f32;
6574                let vy = self.arg_num(&args, 7, 0.)? as f32;
6575                let vz = self.arg_num(&args, 8, 1.)? as f32;
6576                let np = self.arg_num(&args, 9, 6.)? as usize;
6577                let ro = self.arg_num(&args, 10, 2.)? as f32;
6578                let ri = self.arg_num(&args, 11, 1.)? as f32;
6579                let rs = self.arg_num(&args, 12, 0.01)? as f32;
6580                let fr = self.arg_num(&args, 13, 0.)? as f32;
6581                let hue = self.arg_num(&args, 14, 0.)? as f32;
6582                let mut gfx = self.gfx.borrow_mut();
6583                let cam = gfx.camera.clone();
6584                crate::gfx::vtex::draw_star(
6585                    &mut gfx.depth_queue,
6586                    &cam,
6587                    cx,
6588                    cy,
6589                    cz,
6590                    ux,
6591                    uy,
6592                    uz,
6593                    vx,
6594                    vy,
6595                    vz,
6596                    np,
6597                    ro,
6598                    ri,
6599                    rs,
6600                    fr,
6601                    hue,
6602                );
6603                return Ok(Value::Unit);
6604            },
6605
6606            // vtex_spiral(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_turns,max_r,steps, fr,hue)
6607            "vtex_spiral" | "ลายเกลียว" | "纹螺" | "螺旋" | "나선" | "الگوی_مارپیچ" | "نقش_حلزوني" | "דוגמת_ספירלה" | "نقش_سرپیچ" | "motif_spirale" | "muster_spirale" | "узор_спираль" => {
6608                let cx = self.arg_num(&args, 0, 0.)? as f32;
6609                let cy = self.arg_num(&args, 1, 0.)? as f32;
6610                let cz = self.arg_num(&args, 2, 0.)? as f32;
6611                let ux = self.arg_num(&args, 3, 1.)? as f32;
6612                let uy = self.arg_num(&args, 4, 0.)? as f32;
6613                let uz = self.arg_num(&args, 5, 0.)? as f32;
6614                let vx = self.arg_num(&args, 6, 0.)? as f32;
6615                let vy = self.arg_num(&args, 7, 0.)? as f32;
6616                let vz = self.arg_num(&args, 8, 1.)? as f32;
6617                let nt = self.arg_num(&args, 9, 3.)? as f32;
6618                let mr = self.arg_num(&args, 10, 3.)? as f32;
6619                let st = self.arg_num(&args, 11, 120.)? as usize;
6620                let fr = self.arg_num(&args, 12, 0.)? as f32;
6621                let hue = self.arg_num(&args, 13, 0.)? as f32;
6622                let mut gfx = self.gfx.borrow_mut();
6623                let cam = gfx.camera.clone();
6624                crate::gfx::vtex::draw_spiral(
6625                    &mut gfx.depth_queue,
6626                    &cam,
6627                    cx,
6628                    cy,
6629                    cz,
6630                    ux,
6631                    uy,
6632                    uz,
6633                    vx,
6634                    vy,
6635                    vz,
6636                    nt,
6637                    mr,
6638                    st,
6639                    fr,
6640                    hue,
6641                );
6642                return Ok(Value::Unit);
6643            },
6644
6645            // vtex_flower(cx,cy,cz, ux,uy,uz, vx,vy,vz, radius,n_sides, fr,hue)
6646            "vtex_flower" | "ลายดอก" | "纹花" | "花模様" | "꽃무늬" | "الگوی_گل" | "نقش_زهرة" | "דוגמת_פרח" | "نقش_پھول" | "motif_fleur" | "muster_blume" | "узор_цветок" => {
6647                let cx = self.arg_num(&args, 0, 0.)? as f32;
6648                let cy = self.arg_num(&args, 1, 0.)? as f32;
6649                let cz = self.arg_num(&args, 2, 0.)? as f32;
6650                let ux = self.arg_num(&args, 3, 1.)? as f32;
6651                let uy = self.arg_num(&args, 4, 0.)? as f32;
6652                let uz = self.arg_num(&args, 5, 0.)? as f32;
6653                let vx = self.arg_num(&args, 6, 0.)? as f32;
6654                let vy = self.arg_num(&args, 7, 0.)? as f32;
6655                let vz = self.arg_num(&args, 8, 1.)? as f32;
6656                let r = self.arg_num(&args, 9, 1.)? as f32;
6657                let ns = self.arg_num(&args, 10, 24.)? as usize;
6658                let fr = self.arg_num(&args, 11, 0.)? as f32;
6659                let hue = self.arg_num(&args, 12, 0.)? as f32;
6660                let mut gfx = self.gfx.borrow_mut();
6661                let cam = gfx.camera.clone();
6662                crate::gfx::vtex::draw_flower(
6663                    &mut gfx.depth_queue,
6664                    &cam,
6665                    cx,
6666                    cy,
6667                    cz,
6668                    ux,
6669                    uy,
6670                    uz,
6671                    vx,
6672                    vy,
6673                    vz,
6674                    r,
6675                    ns,
6676                    fr,
6677                    hue,
6678                );
6679                return Ok(Value::Unit);
6680            },
6681
6682            // vtex_letter_rain(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_cols,n_vis, col_w,row_h, speed, fr,hue)
6683            "vtex_letter_rain" | "ลายอักษรไหล" | "纹字雨" | "文字雨" | "글자비" | "الگوی_باران_حروف" | "نقش_مطر_الحروف" | "דוגמת_גשם_אותיות" | "نقش_حروف_بارش" | "motif_pluie_lettres" | "muster_buchstabenregen" | "узор_дождь_букв" =>
6684            {
6685                let cx = self.arg_num(&args, 0, 0.)? as f32;
6686                let cy = self.arg_num(&args, 1, 0.)? as f32;
6687                let cz = self.arg_num(&args, 2, 0.)? as f32;
6688                let ux = self.arg_num(&args, 3, 1.)? as f32;
6689                let uy = self.arg_num(&args, 4, 0.)? as f32;
6690                let uz = self.arg_num(&args, 5, 0.)? as f32;
6691                let vx = self.arg_num(&args, 6, 0.)? as f32;
6692                let vy = self.arg_num(&args, 7, 0.)? as f32;
6693                let vz = self.arg_num(&args, 8, 1.)? as f32;
6694                let nc = self.arg_num(&args, 9, 16.)? as usize;
6695                let nv = self.arg_num(&args, 10, 14.)? as usize;
6696                let cw = self.arg_num(&args, 11, 0.65)? as f32;
6697                let rh = self.arg_num(&args, 12, 0.60)? as f32;
6698                let sp = self.arg_num(&args, 13, 0.025)? as f32;
6699                let fr = self.arg_num(&args, 14, 0.)? as f32;
6700                let hue = self.arg_num(&args, 15, 0.)? as f32;
6701                let mut gfx = self.gfx.borrow_mut();
6702                let cam = gfx.camera.clone();
6703                crate::gfx::vtex::draw_letter_rain(
6704                    &mut gfx.depth_queue,
6705                    &cam,
6706                    cx,
6707                    cy,
6708                    cz,
6709                    ux,
6710                    uy,
6711                    uz,
6712                    vx,
6713                    vy,
6714                    vz,
6715                    nc,
6716                    nv,
6717                    cw,
6718                    rh,
6719                    sp,
6720                    fr,
6721                    hue,
6722                );
6723                return Ok(Value::Unit);
6724            },
6725
6726            // vtex_hyperbolic_uv(cx,cy,cz, ux,uy,uz, vx,vy,vz, max_r,n_circles,n_rays, fr,hue)
6727            "vtex_hyperbolic_uv" | "ลายไฮเพอร์โบลิก" | "纹曲面" | "双曲線" | "쌍곡선" | "الگوی_هذلولی" | "نقش_زائدي" | "דוגמת_היפרבולית" | "نقش_ہائپربولک" | "motif_uv_hyperbolique" | "muster_hyperbolische_uv" | "узор_гиперболический_uv" =>
6728            {
6729                let cx = self.arg_num(&args, 0, 0.)? as f32;
6730                let cy = self.arg_num(&args, 1, 0.)? as f32;
6731                let cz = self.arg_num(&args, 2, 0.)? as f32;
6732                let ux = self.arg_num(&args, 3, 1.)? as f32;
6733                let uy = self.arg_num(&args, 4, 0.)? as f32;
6734                let uz = self.arg_num(&args, 5, 0.)? as f32;
6735                let vx = self.arg_num(&args, 6, 0.)? as f32;
6736                let vy = self.arg_num(&args, 7, 0.)? as f32;
6737                let vz = self.arg_num(&args, 8, 1.)? as f32;
6738                let mr = self.arg_num(&args, 9, 5.)? as f32;
6739                let nc = self.arg_num(&args, 10, 12.)? as usize;
6740                let nr = self.arg_num(&args, 11, 18.)? as usize;
6741                let fr = self.arg_num(&args, 12, 0.)? as f32;
6742                let hue = self.arg_num(&args, 13, 0.)? as f32;
6743                let mut gfx = self.gfx.borrow_mut();
6744                let cam = gfx.camera.clone();
6745                crate::gfx::vtex::draw_hyperbolic_uv(
6746                    &mut gfx.depth_queue,
6747                    &cam,
6748                    cx,
6749                    cy,
6750                    cz,
6751                    ux,
6752                    uy,
6753                    uz,
6754                    vx,
6755                    vy,
6756                    vz,
6757                    mr,
6758                    nc,
6759                    nr,
6760                    fr,
6761                    hue,
6762                );
6763                return Ok(Value::Unit);
6764            },
6765
6766            // vtex_halftone(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cell_w,cell_h, density, fr,hue)
6767            "vtex_halftone" | "ลายจุด" | "纹半调" | "網点模様" | "망점" | "الگوی_نیم‌تن" | "نقش_نصفي" | "דוגמת_חצי_גוון" | "نقش_ہاف_ٹون" | "motif_demi_ton" | "muster_halbton" | "узор_растр" => {
6768                let cx = self.arg_num(&args, 0, 0.)? as f32;
6769                let cy = self.arg_num(&args, 1, 0.)? as f32;
6770                let cz = self.arg_num(&args, 2, 0.)? as f32;
6771                let ux = self.arg_num(&args, 3, 1.)? as f32;
6772                let uy = self.arg_num(&args, 4, 0.)? as f32;
6773                let uz = self.arg_num(&args, 5, 0.)? as f32;
6774                let vx = self.arg_num(&args, 6, 0.)? as f32;
6775                let vy = self.arg_num(&args, 7, 0.)? as f32;
6776                let vz = self.arg_num(&args, 8, 1.)? as f32;
6777                let cols = self.arg_num(&args, 9, 16.)? as usize;
6778                let rows = self.arg_num(&args, 10, 12.)? as usize;
6779                let cw = self.arg_num(&args, 11, 0.5)? as f32;
6780                let ch = self.arg_num(&args, 12, 0.5)? as f32;
6781                let dens = self.arg_num(&args, 13, 0.4)? as f32;
6782                let fr = self.arg_num(&args, 14, 0.)? as f32;
6783                let hue = self.arg_num(&args, 15, 0.)? as f32;
6784                let mut gfx = self.gfx.borrow_mut();
6785                let cam = gfx.camera.clone();
6786                crate::gfx::vtex::draw_halftone(
6787                    &mut gfx.depth_queue,
6788                    &cam,
6789                    cx,
6790                    cy,
6791                    cz,
6792                    ux,
6793                    uy,
6794                    uz,
6795                    vx,
6796                    vy,
6797                    vz,
6798                    cols,
6799                    rows,
6800                    cw,
6801                    ch,
6802                    dens,
6803                    fr,
6804                    hue,
6805                );
6806                return Ok(Value::Unit);
6807            },
6808
6809            // vtex_tessellated(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cell, amplitude,freq, fr,hue)
6810            "vtex_tessellated" | "ลายตาข่าย" | "纹镶嵌" | "網目模様" | "격자망" | "الگوی_کاشی‌کاری" | "نقش_مرصوف_متكرر" | "דוגמת_ריצוף_חוזר" | "نقش_ٹائلنگ" | "motif_tesselle" | "muster_tessellation" | "узор_мозаика" =>
6811            {
6812                let cx = self.arg_num(&args, 0, 0.)? as f32;
6813                let cy = self.arg_num(&args, 1, 0.)? as f32;
6814                let cz = self.arg_num(&args, 2, 0.)? as f32;
6815                let ux = self.arg_num(&args, 3, 1.)? as f32;
6816                let uy = self.arg_num(&args, 4, 0.)? as f32;
6817                let uz = self.arg_num(&args, 5, 0.)? as f32;
6818                let vx = self.arg_num(&args, 6, 0.)? as f32;
6819                let vy = self.arg_num(&args, 7, 0.)? as f32;
6820                let vz = self.arg_num(&args, 8, 1.)? as f32;
6821                let cols = self.arg_num(&args, 9, 14.)? as usize;
6822                let rows = self.arg_num(&args, 10, 10.)? as usize;
6823                let cell = self.arg_num(&args, 11, 0.6)? as f32;
6824                let amp = self.arg_num(&args, 12, 0.25)? as f32;
6825                let freq = self.arg_num(&args, 13, 4.)? as f32;
6826                let fr = self.arg_num(&args, 14, 0.)? as f32;
6827                let hue = self.arg_num(&args, 15, 0.)? as f32;
6828                let mut gfx = self.gfx.borrow_mut();
6829                let cam = gfx.camera.clone();
6830                crate::gfx::vtex::draw_tessellated(
6831                    &mut gfx.depth_queue,
6832                    &cam,
6833                    cx,
6834                    cy,
6835                    cz,
6836                    ux,
6837                    uy,
6838                    uz,
6839                    vx,
6840                    vy,
6841                    vz,
6842                    cols,
6843                    rows,
6844                    cell,
6845                    amp,
6846                    freq,
6847                    fr,
6848                    hue,
6849                );
6850                return Ok(Value::Unit);
6851            },
6852
6853            // vtex_lotus(cx,cy,cz, ux,uy,uz, vx,vy,vz, r_inner,r_outer,n_petals, fr,hue)
6854            "vtex_lotus" | "ลายดอกบัว" | "纹莲" | "蓮模様" | "연꽃무늬" | "الگوی_لوتوس" | "نقش_لوتس" | "דוגמת_לוטוס" | "نقش_کنول" | "motif_lotus" | "muster_lotus" | "узор_лотос" =>
6855            {
6856                let cx = self.arg_num(&args, 0, 0.)? as f32;
6857                let cy = self.arg_num(&args, 1, 0.)? as f32;
6858                let cz = self.arg_num(&args, 2, 0.)? as f32;
6859                let ux = self.arg_num(&args, 3, 1.)? as f32;
6860                let uy = self.arg_num(&args, 4, 0.)? as f32;
6861                let uz = self.arg_num(&args, 5, 0.)? as f32;
6862                let vx = self.arg_num(&args, 6, 0.)? as f32;
6863                let vy = self.arg_num(&args, 7, 0.)? as f32;
6864                let vz = self.arg_num(&args, 8, 1.)? as f32;
6865                let ri = self.arg_num(&args, 9, 1.)? as f32;
6866                let ro = self.arg_num(&args, 10, 2.)? as f32;
6867                let np = self.arg_num(&args, 11, 12.)? as usize;
6868                let fr = self.arg_num(&args, 12, 0.)? as f32;
6869                let hue = self.arg_num(&args, 13, 0.)? as f32;
6870                let mut gfx = self.gfx.borrow_mut();
6871                let cam = gfx.camera.clone();
6872                crate::gfx::vtex::draw_lotus(
6873                    &mut gfx.depth_queue,
6874                    &cam,
6875                    cx,
6876                    cy,
6877                    cz,
6878                    ux,
6879                    uy,
6880                    uz,
6881                    vx,
6882                    vy,
6883                    vz,
6884                    ri,
6885                    ro,
6886                    np,
6887                    fr,
6888                    hue,
6889                );
6890                return Ok(Value::Unit);
6891            },
6892
6893            // vtex_chakra(cx,cy,cz, ux,uy,uz, vx,vy,vz, r,n_spokes, fr,hue)
6894            "vtex_chakra" | "ลายจักร" | "纹轮" | "輪模様" | "바퀴무늬" | "الگوی_چاکرا" | "نقش_تشاكرا" | "דוגמת_צ'אקרה" | "نقش_چکر" | "motif_chakra" | "muster_chakra" | "узор_чакра" => {
6895                let cx = self.arg_num(&args, 0, 0.)? as f32;
6896                let cy = self.arg_num(&args, 1, 0.)? as f32;
6897                let cz = self.arg_num(&args, 2, 0.)? as f32;
6898                let ux = self.arg_num(&args, 3, 1.)? as f32;
6899                let uy = self.arg_num(&args, 4, 0.)? as f32;
6900                let uz = self.arg_num(&args, 5, 0.)? as f32;
6901                let vx = self.arg_num(&args, 6, 0.)? as f32;
6902                let vy = self.arg_num(&args, 7, 0.)? as f32;
6903                let vz = self.arg_num(&args, 8, 1.)? as f32;
6904                let r = self.arg_num(&args, 9, 2.)? as f32;
6905                let ns = self.arg_num(&args, 10, 8.)? as usize;
6906                let fr = self.arg_num(&args, 11, 0.)? as f32;
6907                let hue = self.arg_num(&args, 12, 0.)? as f32;
6908                let mut gfx = self.gfx.borrow_mut();
6909                let cam = gfx.camera.clone();
6910                crate::gfx::vtex::draw_chakra(
6911                    &mut gfx.depth_queue,
6912                    &cam,
6913                    cx,
6914                    cy,
6915                    cz,
6916                    ux,
6917                    uy,
6918                    uz,
6919                    vx,
6920                    vy,
6921                    vz,
6922                    r,
6923                    ns,
6924                    fr,
6925                    hue,
6926                );
6927                return Ok(Value::Unit);
6928            },
6929
6930            // vtex_yantra(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_layers,max_r, fr,hue)
6931            "vtex_yantra" | "ลายยันต์" | "纹咒" | "護符模様" | "부적무늬" | "الگوی_یانترا" | "نقش_يانترا" | "דוגמת_יאנטרה" | "نقش_ینترا" | "motif_yantra" | "muster_yantra" | "узор_янтра" =>
6932            {
6933                let cx = self.arg_num(&args, 0, 0.)? as f32;
6934                let cy = self.arg_num(&args, 1, 0.)? as f32;
6935                let cz = self.arg_num(&args, 2, 0.)? as f32;
6936                let ux = self.arg_num(&args, 3, 1.)? as f32;
6937                let uy = self.arg_num(&args, 4, 0.)? as f32;
6938                let uz = self.arg_num(&args, 5, 0.)? as f32;
6939                let vx = self.arg_num(&args, 6, 0.)? as f32;
6940                let vy = self.arg_num(&args, 7, 0.)? as f32;
6941                let vz = self.arg_num(&args, 8, 1.)? as f32;
6942                let nl = self.arg_num(&args, 9, 4.)? as usize;
6943                let mr = self.arg_num(&args, 10, 3.)? as f32;
6944                let fr = self.arg_num(&args, 11, 0.)? as f32;
6945                let hue = self.arg_num(&args, 12, 0.)? as f32;
6946                let mut gfx = self.gfx.borrow_mut();
6947                let cam = gfx.camera.clone();
6948                crate::gfx::vtex::draw_yantra(
6949                    &mut gfx.depth_queue,
6950                    &cam,
6951                    cx,
6952                    cy,
6953                    cz,
6954                    ux,
6955                    uy,
6956                    uz,
6957                    vx,
6958                    vy,
6959                    vz,
6960                    nl,
6961                    mr,
6962                    fr,
6963                    hue,
6964                );
6965                return Ok(Value::Unit);
6966            },
6967
6968            // vtex_spiked_cog(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_teeth,r_body,r_spike,r_hub,n_spokes, fr,hue)
6969            "vtex_spiked_cog" | "ฟันเฟืองหนาม" | "纹棘轮" | "歯車模様" | "톱니바퀴" | "الگوی_چرخ‌دنده_خاردار" | "نقش_ترس_شائك" | "דוגמת_גלגל_קוצני" | "نقش_خاردار_گیئر" | "motif_engrenage_pointes" | "muster_stachelzahnrad" | "узор_шестерня_шипы" =>
6970            {
6971                let cx = self.arg_num(&args, 0, 0.)? as f32;
6972                let cy = self.arg_num(&args, 1, 0.)? as f32;
6973                let cz = self.arg_num(&args, 2, 0.)? as f32;
6974                let ux = self.arg_num(&args, 3, 1.)? as f32;
6975                let uy = self.arg_num(&args, 4, 0.)? as f32;
6976                let uz = self.arg_num(&args, 5, 0.)? as f32;
6977                let vx = self.arg_num(&args, 6, 0.)? as f32;
6978                let vy = self.arg_num(&args, 7, 0.)? as f32;
6979                let vz = self.arg_num(&args, 8, 1.)? as f32;
6980                let nt = self.arg_num(&args, 9, 12.)? as usize;
6981                let rb = self.arg_num(&args, 10, 1.)? as f32;
6982                let rs = self.arg_num(&args, 11, 1.3)? as f32;
6983                let rh = self.arg_num(&args, 12, 0.2)? as f32;
6984                let ns = self.arg_num(&args, 13, 6.)? as usize;
6985                let fr = self.arg_num(&args, 14, 0.)? as f32;
6986                let hue = self.arg_num(&args, 15, 0.)? as f32;
6987                let mut gfx = self.gfx.borrow_mut();
6988                let cam = gfx.camera.clone();
6989                crate::gfx::vtex::draw_spiked_cog(
6990                    &mut gfx.depth_queue,
6991                    &cam,
6992                    cx,
6993                    cy,
6994                    cz,
6995                    ux,
6996                    uy,
6997                    uz,
6998                    vx,
6999                    vy,
7000                    vz,
7001                    nt,
7002                    rb,
7003                    rs,
7004                    rh,
7005                    ns,
7006                    fr,
7007                    hue,
7008                );
7009                return Ok(Value::Unit);
7010            },
7011
7012            // vtex_torii(cx,cy,cz, ux,uy,uz, vx,vy,vz, width,height, fr,hue)
7013            "vtex_torii" | "ประตูโทริอิ" | "纹鸟居" | "鳥居" | "도리이" | "الگوی_توری_ژاپنی" | "نقش_توري" | "דוגמת_טוריי" | "نقش_توری_گیٹ" | "motif_torii" | "muster_torii" | "узор_тории" =>
7014            {
7015                let cx = self.arg_num(&args, 0, 0.)? as f32;
7016                let cy = self.arg_num(&args, 1, 0.)? as f32;
7017                let cz = self.arg_num(&args, 2, 0.)? as f32;
7018                let ux = self.arg_num(&args, 3, 1.)? as f32;
7019                let uy = self.arg_num(&args, 4, 0.)? as f32;
7020                let uz = self.arg_num(&args, 5, 0.)? as f32;
7021                let vx = self.arg_num(&args, 6, 0.)? as f32;
7022                let vy = self.arg_num(&args, 7, 0.)? as f32;
7023                let vz = self.arg_num(&args, 8, 1.)? as f32;
7024                let w = self.arg_num(&args, 9, 4.)? as f32;
7025                let h = self.arg_num(&args, 10, 5.)? as f32;
7026                let fr = self.arg_num(&args, 11, 0.)? as f32;
7027                let hue = self.arg_num(&args, 12, 0.)? as f32;
7028                let mut gfx = self.gfx.borrow_mut();
7029                let cam = gfx.camera.clone();
7030                crate::gfx::vtex::draw_torii(
7031                    &mut gfx.depth_queue,
7032                    &cam,
7033                    cx,
7034                    cy,
7035                    cz,
7036                    ux,
7037                    uy,
7038                    uz,
7039                    vx,
7040                    vy,
7041                    vz,
7042                    w,
7043                    h,
7044                    fr,
7045                    hue,
7046                );
7047                return Ok(Value::Unit);
7048            },
7049
7050            // vtex_pagoda(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_tiers,base_w,tier_h,taper,eave_out, fr,hue)
7051            "vtex_pagoda" | "เจดีย์" | "纹塔" | "塔" | "탑" | "الگوی_پاگودا" | "نقش_باغودا" | "דוגמת_פגודה" | "نقش_پگوڈا" | "motif_pagode" | "muster_pagode" | "узор_пагода" => {
7052                let cx = self.arg_num(&args, 0, 0.)? as f32;
7053                let cy = self.arg_num(&args, 1, 0.)? as f32;
7054                let cz = self.arg_num(&args, 2, 0.)? as f32;
7055                let ux = self.arg_num(&args, 3, 1.)? as f32;
7056                let uy = self.arg_num(&args, 4, 0.)? as f32;
7057                let uz = self.arg_num(&args, 5, 0.)? as f32;
7058                let vx = self.arg_num(&args, 6, 0.)? as f32;
7059                let vy = self.arg_num(&args, 7, 0.)? as f32;
7060                let vz = self.arg_num(&args, 8, 1.)? as f32;
7061                let nt = self.arg_num(&args, 9, 5.)? as usize;
7062                let bw = self.arg_num(&args, 10, 2.)? as f32;
7063                let th = self.arg_num(&args, 11, 1.)? as f32;
7064                let tp = self.arg_num(&args, 12, 0.72)? as f32;
7065                let eo = self.arg_num(&args, 13, 0.28)? as f32;
7066                let fr = self.arg_num(&args, 14, 0.)? as f32;
7067                let hue = self.arg_num(&args, 15, 0.)? as f32;
7068                let mut gfx = self.gfx.borrow_mut();
7069                let cam = gfx.camera.clone();
7070                crate::gfx::vtex::draw_pagoda(
7071                    &mut gfx.depth_queue,
7072                    &cam,
7073                    cx,
7074                    cy,
7075                    cz,
7076                    ux,
7077                    uy,
7078                    uz,
7079                    vx,
7080                    vy,
7081                    vz,
7082                    nt,
7083                    bw,
7084                    th,
7085                    tp,
7086                    eo,
7087                    fr,
7088                    hue,
7089                );
7090                return Ok(Value::Unit);
7091            },
7092
7093            // ══════════════════════════════════════════════════════════════════
7094            // AUDIO BUILTINS
7095            // ══════════════════════════════════════════════════════════════════
7096
7097            // audio_tone(idx, x, y, z, w, freq, amp, lfo_rate, lfo_depth)
7098            #[cfg(not(target_arch = "wasm32"))]
7099            "audio_tone"
7100            | "เสียงโทน"
7101            | "音调"
7102            | "音調"
7103            | "음조"
7104            | "空间音"
7105            | "空間音"
7106            | "공간음" | "تن_صدا" | "نغمة" | "צליל" | "آواز_کا_سر" | "tonalité_audio" | "audioton" | "звук_тон" => {
7107                let idx = self.arg_num(&args, 0, 0.0)? as usize;
7108                let x = self.arg_num(&args, 1, 0.0)? as f32;
7109                let y = self.arg_num(&args, 2, 0.0)? as f32;
7110                let z = self.arg_num(&args, 3, 0.0)? as f32;
7111                let w = self.arg_num(&args, 4, 1.0)? as f32;
7112                let freq = self.arg_num(&args, 5, 220.0)? as f32;
7113                let amp = self.arg_num(&args, 6, 0.15)? as f32;
7114                let lfo_rate = self.arg_num(&args, 7, 0.5)? as f32;
7115                let lfo_depth = self.arg_num(&args, 8, 0.02)? as f32;
7116                if let Some(audio) = &self.audio {
7117                    audio.set_tone(
7118                        idx,
7119                        ToneParams { x, y, z, w, freq, amp, lfo_rate, lfo_depth },
7120                    );
7121                }
7122                return Ok(Value::Unit);
7123            },
7124
7125            #[cfg(not(target_arch = "wasm32"))]
7126            "audio_listener" | "ผู้ฟัง" | "音频监听" | "音声リスナー" | "오디오리스너" | "شنونده_صدا" | "مستمع_الصوت" | "מאזין_קול" | "آواز_سننے_والا" | "auditeur_audio" | "audiohörer" | "звук_слушатель" =>
7127            {
7128                let cry = self.arg_num(&args, 0, 1.0)? as f32;
7129                let sry = self.arg_num(&args, 1, 0.0)? as f32;
7130                let crx = self.arg_num(&args, 2, 1.0)? as f32;
7131                let srx = self.arg_num(&args, 3, 0.0)? as f32;
7132                if let Some(audio) = &self.audio {
7133                    audio.set_listener(cry, sry, crx, srx);
7134                }
7135                return Ok(Value::Unit);
7136            },
7137
7138            #[cfg(not(target_arch = "wasm32"))]
7139            "audio_bgm" | "เพลงพื้นหลัง" | "เพลงประกอบ" | "背景乐" | "BGM" | "배경음악" | "موسیقی_پس‌زمینه" | "موسيقى_خلفية" | "מוזיקת_רקע" | "پس_منظر_موسیقی" | "musique_fond" | "hintergrundmusik" | "фоновая_музыка" =>
7140            {
7141                let path = match args.first() {
7142                    Some(Value::Str(s)) => s.clone(),
7143                    _ => return Ok(Value::Unit),
7144                };
7145                let vol = self.arg_num(&args, 1, 0.5)? as f32;
7146                if let Some(audio) = &self.audio {
7147                    audio.load_bgm(&path, vol);
7148                }
7149                return Ok(Value::Unit);
7150            },
7151
7152            #[cfg(not(target_arch = "wasm32"))]
7153            "audio_bgm_volume"
7154            | "ระดับเสียงพื้นหลัง"
7155            | "ระดับเพลงประกอบ"
7156            | "背景乐音量"
7157            | "BGM音量"
7158            | "배경음악음량" | "بلندی_موسیقی_پس‌زمینه" | "مستوى_موسيقى_الخلفية" | "עוצמת_מוזיקת_רקע" | "پس_منظر_موسیقی_شدت" | "volume_musique_fond" | "hintergrundmusiklautstärke" | "громкость_фоновой_музыки" => {
7159                let vol = self.arg_num(&args, 0, 0.5)? as f32;
7160                if let Some(audio) = &self.audio {
7161                    audio.set_bgm_volume(vol);
7162                }
7163                return Ok(Value::Unit);
7164            },
7165
7166            #[cfg(not(target_arch = "wasm32"))]
7167            "audio_volume" | "ระดับเสียง" | "音量" | "음량" | "بلندی_صدا" | "مستوى_الصوت" | "עוצמת_קול" | "آواز_کی_شدت" | "volume_audio" | "audiolautstärke" | "звук_громкость" => {
7168                let vol = self.arg_num(&args, 0, 0.7)? as f32;
7169                if let Some(audio) = &self.audio {
7170                    audio.set_master_volume(vol);
7171                }
7172                return Ok(Value::Unit);
7173            },
7174
7175            // WASM audio builtins — delegate to Web Audio API
7176            #[cfg(target_arch = "wasm32")]
7177            "audio_tone"
7178            | "เสียงโทน"
7179            | "音调"
7180            | "音調"
7181            | "음조"
7182            | "空间音"
7183            | "空間音"
7184            | "공간음" | "تن_صدا" | "نغمة" | "צליל" | "آواز_کا_سر" | "tonalité_audio" | "audioton" | "звук_тон" => {
7185                let idx = self.arg_num(&args, 0, 0.0)? as usize;
7186                let x = self.arg_num(&args, 1, 0.0)? as f32;
7187                let y = self.arg_num(&args, 2, 0.0)? as f32;
7188                let z = self.arg_num(&args, 3, 0.0)? as f32;
7189                let w = self.arg_num(&args, 4, 1.0)? as f32;
7190                let freq = self.arg_num(&args, 5, 220.0)? as f32;
7191                let amp = self.arg_num(&args, 6, 0.15)? as f32;
7192                let lfo_rate = self.arg_num(&args, 7, 0.5)? as f32;
7193                let lfo_depth = self.arg_num(&args, 8, 0.02)? as f32;
7194                crate::gfx::audio_web::set_tone(idx, x, y, z, w, freq, amp, lfo_rate, lfo_depth);
7195                return Ok(Value::Unit);
7196            },
7197
7198            #[cfg(target_arch = "wasm32")]
7199            "audio_listener" | "ผู้ฟัง" | "音频监听" | "音声リスナー" | "오디오리스너" | "شنونده_صدا" | "مستمع_الصوت" | "מאזין_קול" | "آواز_سننے_والا" | "auditeur_audio" | "audiohörer" | "звук_слушатель" =>
7200            {
7201                let cry = self.arg_num(&args, 0, 1.0)? as f32;
7202                let sry = self.arg_num(&args, 1, 0.0)? as f32;
7203                let crx = self.arg_num(&args, 2, 1.0)? as f32;
7204                let srx = self.arg_num(&args, 3, 0.0)? as f32;
7205                crate::gfx::audio_web::set_listener(cry, sry, crx, srx);
7206                return Ok(Value::Unit);
7207            },
7208
7209            #[cfg(target_arch = "wasm32")]
7210            "audio_bgm" | "เพลงพื้นหลัง" | "เพลงประกอบ" | "背景乐" | "BGM" | "배경음악" | "موسیقی_پس‌زمینه" | "موسيقى_خلفية" | "מוזיקת_רקע" | "پس_منظر_موسیقی" | "musique_fond" | "hintergrundmusik" | "фоновая_музыка" =>
7211            {
7212                let path = self.arg_str(&args, 0, "");
7213                let vol = self.arg_num(&args, 1, 0.5)? as f32;
7214                crate::gfx::audio_web::load_bgm(&path, vol);
7215                return Ok(Value::Unit);
7216            },
7217
7218            #[cfg(target_arch = "wasm32")]
7219            "audio_bgm_volume"
7220            | "ระดับเสียงพื้นหลัง"
7221            | "ระดับเพลงประกอบ"
7222            | "背景乐音量"
7223            | "BGM音量"
7224            | "배경음악음량" | "بلندی_موسیقی_پس‌زمینه" | "مستوى_موسيقى_الخلفية" | "עוצמת_מוזיקת_רקע" | "پس_منظر_موسیقی_شدت" | "volume_musique_fond" | "hintergrundmusiklautstärke" | "громкость_фоновой_музыки" => {
7225                let vol = self.arg_num(&args, 0, 0.5)? as f32;
7226                crate::gfx::audio_web::set_bgm_volume(vol);
7227                return Ok(Value::Unit);
7228            },
7229
7230            #[cfg(target_arch = "wasm32")]
7231            "audio_volume" | "ระดับเสียง" | "音量" | "음량" | "بلندی_صدا" | "مستوى_الصوت" | "עוצמת_קול" | "آواز_کی_شدت" | "volume_audio" | "audiolautstärke" | "звук_громкость" => {
7232                let vol = self.arg_num(&args, 0, 0.7)? as f32;
7233                crate::gfx::audio_web::set_master_volume(vol);
7234                return Ok(Value::Unit);
7235            },
7236
7237            // ── WASM sample load / play / stop / FX (Web Audio pool) ─────────
7238            #[cfg(target_arch = "wasm32")]
7239            "audio_sample_load" | "载入采样" | "サンプル読込" | "샘플로드" | "โหลดตัวอย่างเสียง" | "بارگذاری_نمونه_صدا" | "تحميل_عينة_صوتية" | "טעינת_דגימת_קול" | "آواز_نمونہ_لوڈ" | "charger_échantillon" | "sample_laden" | "загрузить_семпл" =>
7240            {
7241                let path = self.arg_str(&args, 0, "");
7242                let resolved = self.wasm_resolve_source_path(&path);
7243                match wasm_fetch_bytes(&resolved)
7244                    .and_then(|bytes| ling_music::from_bytes(&bytes).map_err(|e| e.to_string()))
7245                {
7246                    Ok(t) => {
7247                        let id = crate::gfx::audio_web::add_sample(&t.stereo, t.channels, t.rate);
7248                        return Ok(Value::Number(id as f64));
7249                    },
7250                    Err(e) => {
7251                        eprintln!("audio_sample_load failed ({path}): {e}");
7252                        return Ok(Value::Number(-1.0));
7253                    },
7254                }
7255            },
7256            #[cfg(target_arch = "wasm32")]
7257            "audio_sample_play" | "播放采样" | "サンプル再生" | "샘플재생" | "เล่นตัวอย่างเสียง" | "پخش_نمونه_صدا" | "تشغيل_عينة_صوتية" | "נגינת_דגימת_קול" | "آواز_نمونہ_چلاؤ" | "jouer_échantillon" | "sample_abspielen" | "играть_семпл" =>
7258            {
7259                let id = self.arg_num(&args, 0, 0.0)? as usize;
7260                let x = self.arg_num(&args, 1, 0.0)? as f32;
7261                let y = self.arg_num(&args, 2, 0.0)? as f32;
7262                let z = self.arg_num(&args, 3, 0.0)? as f32;
7263                // arg 4 is w (4th spatial dim) — ignored for 3-D panner
7264                let vol = self.arg_num(&args, 5, 1.0)? as f32;
7265                let looping = self.arg_num(&args, 6, 0.0)? > 0.5;
7266                crate::gfx::audio_web::play_sample(id, x, y, z, vol, looping);
7267                return Ok(Value::Number(0.0));
7268            },
7269            #[cfg(target_arch = "wasm32")]
7270            "audio_sample_stop"
7271            | "停止采样"
7272            | "サンプル停止"
7273            | "샘플정지"
7274            | "หยุดตัวอย่างเสียง"
7275            | "audio_fx_reverb"
7276            | "混响"
7277            | "リバーブ"
7278            | "리버브"
7279            | "เสียงก้อง"
7280            | "audio_fx_delay"
7281            | "回声"
7282            | "ディレイ効果"
7283            | "딜레이"
7284            | "เสียงสะท้อน"
7285            | "audio_fx_lowpass"
7286            | "低通滤波"
7287            | "ローパス"
7288            | "저역통과"
7289            | "กรองความถี่ต่ำ" | "توقف_نمونه_صدا" | "إيقاف_عينة_صوتية" | "עצירת_דגימת_קול" | "آواز_نمونہ_روکو" | "arrêter_échantillon" | "sample_stoppen" | "остановить_семпл" => {
7290                return Ok(Value::Unit);
7291            },
7292
7293            // ── รอหน้าต่าง() — block until window closed / Escape ──
7294            "รอหน้าต่าง" | "wait_window" | "gfx_wait" | "انتظار_پنجره" | "انتظر_النافذة" | "המתן_לחלון" | "ونڈو_انتظار" => {
7295                #[cfg(not(target_arch = "wasm32"))]
7296                loop {
7297                    let still_open = {
7298                        let gfx = self.gfx.borrow();
7299                        gfx.window
7300                            .as_ref()
7301                            .map(|w| w.is_open() && !w.is_key_down(minifb::Key::Escape))
7302                            .unwrap_or(false)
7303                    };
7304                    if !still_open {
7305                        break;
7306                    }
7307                    let (buf, w, h) = {
7308                        let gfx = self.gfx.borrow();
7309                        (gfx.buffer.clone(), gfx.width, gfx.height)
7310                    };
7311                    let mut gfx = self.gfx.borrow_mut();
7312                    if let Some(win) = gfx.window.as_mut() {
7313                        if win.update_with_buffer(&buf, w, h).is_err() {
7314                            break;
7315                        }
7316                    }
7317                }
7318                return Ok(Value::Unit);
7319            },
7320
7321            // ── File I/O ──────────────────────────────────────────────────────
7322            "read_file" | "อ่านไฟล์" | "خواندن_فایل" | "اقرأ_الملف" | "קרא_קובץ" | "فائل_پڑھو" => {
7323                #[cfg(target_arch = "wasm32")]
7324                return Ok(Value::Str(String::new()));
7325                #[cfg(not(target_arch = "wasm32"))]
7326                {
7327                    let path = self.arg_str(&args, 0, "");
7328                    return std::fs::read_to_string(&path)
7329                        .map(Value::Str)
7330                        .map_err(|e| EvalErr::from(format!("read_file '{path}': {e}")));
7331                }
7332            },
7333            // ── networking (TCP, 2-peer co-op) ───────────────────────────────
7334            #[cfg(not(target_arch = "wasm32"))]
7335            "net_host" | "เน็ตโฮสต์" | "میزبانی_شبکه" | "استضف_الشبكة" | "ארח_רשת" | "نیٹ_ہوسٹ" => {
7336                let port = self.arg_num(&args, 0, 7777.0)? as u16;
7337                net::host(port);
7338                return Ok(Value::Unit);
7339            },
7340            #[cfg(not(target_arch = "wasm32"))]
7341            "net_join" | "เน็ตจอย" | "پیوستن_شبکه" | "انضم_للشبكة" | "הצטרף_לרשת" | "نیٹ_شمولیت" => {
7342                let ip = self.arg_str(&args, 0, "127.0.0.1");
7343                let port = self.arg_num(&args, 1, 7777.0)? as u16;
7344                net::join(&ip, port);
7345                return Ok(Value::Unit);
7346            },
7347            #[cfg(not(target_arch = "wasm32"))]
7348            "net_send" | "เน็ตส่ง" | "ارسال_شبکه" | "أرسل_عبر_الشبكة" | "שלח_ברשת" | "نیٹ_بھیجو" => {
7349                let s = self.arg_str(&args, 0, "");
7350                net::send(&s);
7351                return Ok(Value::Unit);
7352            },
7353            #[cfg(not(target_arch = "wasm32"))]
7354            "net_recv" | "เน็ตรับ" | "دریافت_شبکه" | "استقبل_من_الشبكة" | "קבל_מרשת" | "نیٹ_وصول" => {
7355                return Ok(Value::Str(net::recv()));
7356            },
7357            #[cfg(not(target_arch = "wasm32"))]
7358            "net_status" | "เน็ตสถานะ" | "وضعیت_شبکه" | "حالة_الشبكة" | "סטטוס_רשת" | "نیٹ_حالت" => {
7359                return Ok(Value::Number(net::status() as f64));
7360            },
7361            #[cfg(not(target_arch = "wasm32"))]
7362            "net_recv_from" => {
7363                return Ok(Value::Str(net::recv_from()));
7364            },
7365            #[cfg(not(target_arch = "wasm32"))]
7366            "net_send_to" => {
7367                let id = self.arg_num(&args, 0, 0.0)? as u64;
7368                let s = self.arg_str(&args, 1, "");
7369                net::send_to(id, &s);
7370                return Ok(Value::Unit);
7371            },
7372            #[cfg(not(target_arch = "wasm32"))]
7373            "net_close" | "연결종료" => {
7374                net::close();
7375                return Ok(Value::Unit);
7376            },
7377            // ── LAN lobby discovery (UDP broadcast) ──
7378            #[cfg(not(target_arch = "wasm32"))]
7379            "net_announce" | "เน็ตประกาศ" | "اعلام_شبکه" | "أعلن_في_الشبكة" | "הכרז_ברשת" | "نیٹ_اعلان" => {
7380                let port = self.arg_num(&args, 0, 7778.0)? as u16;
7381                let info = self.arg_str(&args, 1, "");
7382                net::announce(port, &info);
7383                return Ok(Value::Unit);
7384            },
7385            #[cfg(not(target_arch = "wasm32"))]
7386            "net_announce_stop" | "เน็ตหยุดประกาศ" | "توقف_اعلام" | "أوقف_الإعلان" | "עצור_הכרזה" | "اعلان_روکو" => {
7387                net::announce_stop();
7388                return Ok(Value::Unit);
7389            },
7390            #[cfg(not(target_arch = "wasm32"))]
7391            "net_discover" | "เน็ตค้นหา" | "کشف_شبکه" | "اكتشف_الشبكة" | "גלה_רשת" | "نیٹ_دریافت" => {
7392                let port = self.arg_num(&args, 0, 7778.0)? as u16;
7393                return Ok(Value::Str(net::discover(port)));
7394            },
7395            #[cfg(not(target_arch = "wasm32"))]
7396            "net_test" | "เน็ตทดสอบ" | "آزمون_شبکه" | "اختبر_الشبكة" | "בדוק_רשת" | "نیٹ_ٹیسٹ" => {
7397                let port = self.arg_num(&args, 0, 7777.0)? as u16;
7398                return Ok(Value::Str(net::test_bind(port)));
7399            },
7400            // ── HTTP server (interpreter <-> async bridge, see runtime::web) ──
7401            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7402            "http_route" | "เว็บเส้นทาง" | "مسیر_HTTP" | "مسار_HTTP" | "נתיב_HTTP" | "HTTP_روٹ" => {
7403                let method = self.arg_str(&args, 0, "GET").to_uppercase();
7404                let path = self.arg_str(&args, 1, "/");
7405                let handler = args.get(2).cloned().unwrap_or(Value::Unit);
7406                self.http_routes.push((method, path, handler));
7407                return Ok(Value::Unit);
7408            },
7409            // Registers a directory to be served as raw bytes at `prefix` (fonts,
7410            // images, generated zips/PDFs) — bypasses the String-only Request/
7411            // Response bridge entirely, so binary files come through intact.
7412            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7413            "http_static" | "เว็บสแตติก" | "فایل_ایستای_HTTP" | "ملفات_HTTP_ثابتة" | "קבצים_סטטיים_HTTP" | "HTTP_مستقل_فائل" => {
7414                let prefix = self.arg_str(&args, 0, "/static");
7415                let dir = self.arg_str(&args, 1, "static");
7416                self.http_static_dirs.push((prefix, dir));
7417                return Ok(Value::Unit);
7418            },
7419            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7420            "http_serve" | "เว็บเสิร์ฟ" | "سرویس_HTTP" | "قدّم_HTTP" | "הגש_HTTP" | "HTTP_سرو" => {
7421                let host = self.arg_str(&args, 0, "127.0.0.1");
7422                let port = self.arg_num(&args, 1, 8080.0)? as u16;
7423                let routes = std::mem::take(&mut self.http_routes);
7424                let static_dirs = std::mem::take(&mut self.http_static_dirs);
7425                // No premature "listening" print here: ling_http::serve_http
7426                // (called from spawn_server's background thread) now prints
7427                // its own banner, but only after the socket is actually
7428                // bound — a more honest signal than printing right after
7429                // requesting the background thread be spawned.
7430                let rx = web::spawn_server(host.clone(), port, static_dirs);
7431                for pending in rx {
7432                    let matched = routes
7433                        .iter()
7434                        .find(|(m, p, _)| m == &pending.method && p == &pending.path);
7435                    let response = match matched {
7436                        Some((_, _, handler)) => {
7437                            let req_value = Value::Struct {
7438                                name: "Request".to_string(),
7439                                fields: vec![
7440                                    ("method".to_string(), Value::Str(pending.method.clone())),
7441                                    ("path".to_string(), Value::Str(pending.path.clone())),
7442                                    ("query".to_string(), Value::Str(pending.query.clone())),
7443                                    ("body".to_string(), Value::Str(pending.body.clone())),
7444                                    ("cookie".to_string(), Value::Str(pending.cookie.clone())),
7445                                    (
7446                                        "authorization".to_string(),
7447                                        Value::Str(pending.authorization.clone()),
7448                                    ),
7449                                    (
7450                                        "client_ip".to_string(),
7451                                        Value::Str(pending.client_ip.clone()),
7452                                    ),
7453                                ],
7454                            };
7455                            match self.call_value(handler.clone(), vec![req_value]) {
7456                                Ok(v) => web::value_to_response(&v),
7457                                Err(e) => web::HttpResponse {
7458                                    status: 500,
7459                                    content_type: "text/plain; charset=utf-8".to_string(),
7460                                    body: format!("handler error: {e:?}"),
7461                                    set_cookie: None,
7462                                    location: None,
7463                                },
7464                            }
7465                        },
7466                        None => web::HttpResponse {
7467                            status: 404,
7468                            content_type: "text/plain; charset=utf-8".to_string(),
7469                            body: "not found".to_string(),
7470                            set_cookie: None,
7471                            location: None,
7472                        },
7473                    };
7474                    let _ = pending.respond_to.send(response);
7475                }
7476                return Ok(Value::Unit);
7477            },
7478            // Fires a POST request on a background async runtime and returns a job
7479            // id immediately — for slow external calls (e.g. local Stable Diffusion
7480            // generation) that must not block http_serve's single-threaded loop.
7481            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7482            "http_post_async" | "เว็บโพสต์ไม่บล็อก" | "ارسال_ناهمگام_HTTP" | "أرسل_HTTP_غير_متزامن" | "שלח_HTTP_אסינכרוני" | "HTTP_غیر_ہمزمان_بھیجو" => {
7483                let url = self.arg_str(&args, 0, "");
7484                let body = self.arg_str(&args, 1, "");
7485                let content_type = self.arg_str(&args, 2, "application/json");
7486                let id = self.async_jobs.start_post(url, content_type, body);
7487                return Ok(Value::Str(id));
7488            },
7489            // Non-blocking poll: "" while the job named by http_post_async (or
7490            // sdai_generate_start) is still running, the result once it
7491            // completes — same job table, same builtin polls both.
7492            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7493            "http_job_poll" | "เว็บงานสำรวจ" | "بررسی_وظیفه_HTTP" | "استطلع_مهمة_HTTP" | "בדוק_משימת_HTTP" | "HTTP_کام_پول" => {
7494                let id = self.arg_str(&args, 0, "");
7495                return Ok(Value::Str(self.async_jobs.poll(&id).unwrap_or_default()));
7496            },
7497            // Starts an AUTOMATIC1111-compatible txt2img generation in the
7498            // background against `base_url` (e.g. "http://127.0.0.1:1342").
7499            // Poll with http_job_poll: the result is the plain base64 PNG
7500            // once ready, or a string starting with "ERROR:" on failure —
7501            // the JSON response itself is parsed in Rust (see
7502            // AsyncJobs::start_sdai_txt2img), since `.ling` has no JSON parser.
7503            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7504            "sdai_generate_start" | "เอสดีเอไอเริ่มสร้าง" | "شروع_تولید_هوش" | "ابدأ_توليد_الذكاء" | "התחל_יצירת_בינה" | "اے_آئی_تخلیق_شروع" => {
7505                let base_url = self.arg_str(&args, 0, "http://127.0.0.1:1342");
7506                let prompt = self.arg_str(&args, 1, "");
7507                let width = self.arg_num(&args, 2, 512.0)? as u32;
7508                let height = self.arg_num(&args, 3, 512.0)? as u32;
7509                let id = self.async_jobs.start_sdai_txt2img(base_url, prompt, width, height);
7510                return Ok(Value::Str(id));
7511            },
7512            // ── query_param("q=a&page=2", "q", "") → "a" (URL-decoded) ──
7513            "query_param" | "พารามิเตอร์" | "پارامتر_پرسوجو" | "معامل_الاستعلام" | "פרמטר_שאילתה" | "کوئری_پیرامیٹر" => {
7514                let qs = self.arg_str(&args, 0, "");
7515                let name = self.arg_str(&args, 1, "");
7516                let default = self.arg_str(&args, 2, "");
7517                let mut found = default;
7518                for pair in qs.split('&') {
7519                    let mut it = pair.splitn(2, '=');
7520                    if it.next().unwrap_or("") == name {
7521                        found = url_decode(it.next().unwrap_or(""));
7522                        break;
7523                    }
7524                }
7525                return Ok(Value::Str(found));
7526            },
7527            // ── cookie_get("sid=abc; x=1", "sid", "") → "abc" ──
7528            "cookie_get" | "รับคุกกี้" | "دریافت_کوکی" | "اجلب_الكعكة" | "קבל_עוגייה" | "کوکی_حاصل_کرو" => {
7529                let header = self.arg_str(&args, 0, "");
7530                let name = self.arg_str(&args, 1, "");
7531                let default = self.arg_str(&args, 2, "");
7532                let mut found = default;
7533                for pair in header.split(';') {
7534                    let p = pair.trim();
7535                    let mut it = p.splitn(2, '=');
7536                    if it.next().unwrap_or("") == name {
7537                        found = it.next().unwrap_or("").to_string();
7538                        break;
7539                    }
7540                }
7541                return Ok(Value::Str(found));
7542            },
7543            // ── html_escape(s) — & < > " ' → entities, for echoing user input ──
7544            "html_escape" | "กันเอชทีเอ็มแอล" | "فرار_HTML" | "أفلت_HTML" | "בריחת_HTML" | "HTML_ایسکیپ" => {
7545                let s = self.arg_str(&args, 0, "");
7546                return Ok(Value::Str(
7547                    s.replace('&', "&amp;")
7548                        .replace('<', "&lt;")
7549                        .replace('>', "&gt;")
7550                        .replace('"', "&quot;")
7551                        .replace('\'', "&#39;"),
7552                ));
7553            },
7554            // json_escape(s) — escape a string for embedding inside a JSON
7555            // string literal (", \, and control chars). Needed because the
7556            // registry builds JSON API responses by concatenation; without
7557            // this a value containing " or \ breaks or injects into the JSON.
7558            "json_escape" | "หนีเจสัน" | "فرار_JSON" | "أفلت_JSON" | "בריחת_JSON" | "JSON_ایسکیپ" => {
7559                let s = self.arg_str(&args, 0, "");
7560                let mut out = String::with_capacity(s.len() + 8);
7561                for c in s.chars() {
7562                    match c {
7563                        '"' => out.push_str("\\\""),
7564                        '\\' => out.push_str("\\\\"),
7565                        '\n' => out.push_str("\\n"),
7566                        '\r' => out.push_str("\\r"),
7567                        '\t' => out.push_str("\\t"),
7568                        c if (c as u32) < 0x20 => {
7569                            out.push_str(&format!("\\u{:04x}", c as u32))
7570                        },
7571                        c => out.push(c),
7572                    }
7573                }
7574                return Ok(Value::Str(out));
7575            },
7576            // ── CLI arguments: cli_arg("port", "8080") reads `--port 6688` ──
7577            "cli_arg" | "อาร์กิวเมนต์" | "آرگومان_خط‌فرمان" | "معامل_سطر_الأوامر" | "ארגומנט_שורת_פקודה" | "سی_ایل_آئی_دلیل" => {
7578                let name = self.arg_str(&args, 0, "");
7579                let default = self.arg_str(&args, 1, "");
7580                let flag = format!("--{name}");
7581                let argv: Vec<String> = std::env::args().collect();
7582                let found = argv
7583                    .iter()
7584                    .position(|a| a == &flag)
7585                    .and_then(|i| argv.get(i + 1))
7586                    .cloned()
7587                    .unwrap_or(default);
7588                return Ok(Value::Str(found));
7589            },
7590            // ── SQLite (rusqlite, synchronous — matches the interpreter) ──
7591            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7592            "db_open" | "ฐานข้อมูลเปิด" | "باز_کردن_پایگاه_داده" | "افتح_قاعدة_البيانات" | "פתח_מסד_נתונים" | "ڈیٹا_بیس_کھولو" => {
7593                let path = self.arg_str(&args, 0, "app.db");
7594                let conn = ling_http::rusqlite::Connection::open(&path)
7595                    .map_err(|e| EvalErr::from(format!("db_open '{path}': {e}")))?;
7596                let _ = conn.execute_batch("PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL;");
7597                self.db = Some(conn);
7598                return Ok(Value::Unit);
7599            },
7600            // db_exec(sql, ...params) → rows affected. Params bind positionally
7601            // (?1, ?2, ...): numbers as REAL, bools as 0/1, everything else TEXT.
7602            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7603            "db_exec" | "ฐานข้อมูลรัน" | "اجرای_پایگاه_داده" | "نفّذ_في_قاعدة_البيانات" | "בצע_במסד_נתונים" | "ڈیٹا_بیس_عمل" => {
7604                let sql = self.arg_str(&args, 0, "");
7605                let params = values_to_sql_params(&args[1.min(args.len())..]);
7606                let conn = self
7607                    .db
7608                    .as_ref()
7609                    .ok_or_else(|| EvalErr::from("db_exec: call db_open first".to_string()))?;
7610                let n = conn
7611                    .execute(
7612                        &sql,
7613                        ling_http::rusqlite::params_from_iter(params.iter()),
7614                    )
7615                    .map_err(|e| EvalErr::from(format!("db_exec: {e}\n  sql: {sql}")))?;
7616                return Ok(Value::Number(n as f64));
7617            },
7618            // db_query(sql, ...params) → List of Row structs (row.column_name).
7619            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7620            "db_query" | "ฐานข้อมูลถาม" | "پرسوجوی_پایگاه_داده" | "استعلم_قاعدة_البيانات" | "שאילתת_מסד_נתונים" | "ڈیٹا_بیس_سوال" => {
7621                let sql = self.arg_str(&args, 0, "");
7622                let params = values_to_sql_params(&args[1.min(args.len())..]);
7623                let conn = self
7624                    .db
7625                    .as_ref()
7626                    .ok_or_else(|| EvalErr::from("db_query: call db_open first".to_string()))?;
7627                let mut stmt = conn
7628                    .prepare(&sql)
7629                    .map_err(|e| EvalErr::from(format!("db_query: {e}\n  sql: {sql}")))?;
7630                let col_names: Vec<String> =
7631                    stmt.column_names().iter().map(|s| s.to_string()).collect();
7632                let mut rows = stmt
7633                    .query(ling_http::rusqlite::params_from_iter(params.iter()))
7634                    .map_err(|e| EvalErr::from(format!("db_query: {e}")))?;
7635                let mut out = Vec::new();
7636                while let Some(row) = rows
7637                    .next()
7638                    .map_err(|e| EvalErr::from(format!("db_query row: {e}")))?
7639                {
7640                    let mut fields = Vec::with_capacity(col_names.len());
7641                    for (i, col) in col_names.iter().enumerate() {
7642                        use ling_http::rusqlite::types::ValueRef;
7643                        let v = match row.get_ref(i) {
7644                            Ok(ValueRef::Null) => Value::Str(String::new()),
7645                            Ok(ValueRef::Integer(n)) => Value::Number(n as f64),
7646                            Ok(ValueRef::Real(n)) => Value::Number(n),
7647                            Ok(ValueRef::Text(t)) => {
7648                                Value::Str(String::from_utf8_lossy(t).into_owned())
7649                            },
7650                            Ok(ValueRef::Blob(b)) =>
7651
7652                            {
7653                                use base64::Engine as _;
7654                                Value::Str(base64::engine::general_purpose::STANDARD.encode(b))
7655                            },
7656                            Err(_) => Value::Str(String::new()),
7657                        };
7658                        fields.push((col.clone(), v));
7659                    }
7660                    out.push(Value::Struct { name: "Row".to_string(), fields });
7661                }
7662                return Ok(Value::List(Rc::new(out)));
7663            },
7664            // ── gamepad (gilrs) ──
7665            #[cfg(not(target_arch = "wasm32"))]
7666            "gamepad_poll" | "จอยโพล" | "بررسی_دسته_بازی" | "استطلع_يد_اللعب" | "בדוק_בקר_משחק" | "گیم_پیڈ_پول_کرو" => {
7667                gamepad::poll();
7668                return Ok(Value::Unit);
7669            },
7670            #[cfg(not(target_arch = "wasm32"))]
7671            "gamepad_button" | "จอยปุ่ม" | "دکمه_دسته_بازی" | "زر_يد_اللعب" | "כפתור_בקר_משחק" | "گیم_پیڈ_بٹن" => {
7672                let name = self.arg_str(&args, 0, "");
7673                return Ok(Value::Number(if gamepad::button(&name) {
7674                    1.0
7675                } else {
7676                    0.0
7677                }));
7678            },
7679            #[cfg(not(target_arch = "wasm32"))]
7680            "gamepad_axis" | "จอยแกน" | "محور_دسته_بازی" | "محور_يد_اللعب" | "ציר_בקר_משחק" | "گیم_پیڈ_محور" => {
7681                let name = self.arg_str(&args, 0, "");
7682                return Ok(Value::Number(gamepad::axis(&name) as f64));
7683            },
7684            #[cfg(not(target_arch = "wasm32"))]
7685            "gamepad_rumble" | "จอยสั่น" | "لرزش_دسته_بازی" | "اهتزاز_يد_اللعب" | "רטט_בקר_משחק" | "گیم_پیڈ_تھرتھراہٹ" => {
7686                let low = self.arg_num(&args, 0, 0.0)? as f32;
7687                let high = self.arg_num(&args, 1, 0.0)? as f32;
7688                let ms = self.arg_num(&args, 2, 200.0)? as u32;
7689                gamepad::rumble(low, high, ms);
7690                return Ok(Value::Unit);
7691            },
7692            #[cfg(not(target_arch = "wasm32"))]
7693            "gamepad_list" | "จอยรายการ" | "فهرست_دسته‌های_بازی" | "قائمة_أيدي_اللعب" | "רשימת_בקרי_משחק" | "گیم_پیڈ_فہرست" => {
7694                return Ok(Value::Str(gamepad::list()));
7695            },
7696            #[cfg(not(target_arch = "wasm32"))]
7697            "gamepad_any" | "จอยใดๆ" | "هر_دسته_بازی" | "أي_يد_لعب" | "בקר_כלשהו" | "کوئی_بھی_گیم_پیڈ" => {
7698                return Ok(Value::Number(if gamepad::any_button() { 1.0 } else { 0.0 }));
7699            },
7700            // wasm32: gamepad not available — return safe no-op values
7701            #[cfg(target_arch = "wasm32")]
7702            "gamepad_poll" | "จอยโพล" | "gamepad_rumble" | "จอยสั่น" | "بررسی_دسته_بازی" | "استطلع_يد_اللعب" | "בדוק_בקר_משחק" | "گیم_پیڈ_پول_کرو" => {
7703                return Ok(Value::Unit);
7704            },
7705            #[cfg(target_arch = "wasm32")]
7706            "gamepad_button" | "จอยปุ่ม" | "gamepad_axis" | "จอยแกน" | "gamepad_any" | "จอยใดๆ" | "دکمه_دسته_بازی" | "زر_يد_اللعب" | "כפתור_בקר_משחק" | "گیم_پیڈ_بٹن" =>
7707            {
7708                return Ok(Value::Number(0.0));
7709            },
7710            #[cfg(target_arch = "wasm32")]
7711            "gamepad_list" | "จอยรายการ" | "فهرست_دسته‌های_بازی" | "قائمة_أيدي_اللعب" | "רשימת_בקרי_משחק" | "گیم_پیڈ_فہرست" => {
7712                return Ok(Value::Str(String::new()));
7713            },
7714
7715            // ── game AI: neural networks ─────────────────────────────────────
7716            // nn_new(inputs[, seed]) → handle
7717            #[cfg(not(target_arch = "wasm32"))]
7718            "nn_new" | "建神经网" | "ニューラル作成" | "신경망생성" | "สร้างโครงข่าย" | "شبکه_جدید" | "شبكة_جديدة" | "רשת_חדשה" | "نئی_نیورل_نیٹ" | "nouveau_réseau" | "neues_netz" | "новая_сеть" =>
7719            {
7720                let n_in = self.arg_num(&args, 0, 1.0)?.max(0.0) as usize;
7721                let seed = self.arg_num(&args, 1, 1.0)? as u64;
7722                return Ok(Value::Number(ai::nn_new(n_in, seed) as f64));
7723            },
7724            // nn_dense(handle, units[, activation]) — append a layer
7725            #[cfg(not(target_arch = "wasm32"))]
7726            "nn_dense" | "密集层" | "密層追加" | "밀집층" | "ชั้นหนาแน่น" | "لایه_متراکم" | "طبقة_كثيفة" | "שכבה_צפופה" | "ڈینس_لیئر" | "réseau_dense" | "netz_dicht" | "плотная_сеть" =>
7727            {
7728                let id = self.arg_num(&args, 0, -1.0)? as i64;
7729                let units = self.arg_num(&args, 1, 1.0)?.max(1.0) as usize;
7730                let act = self.arg_str(&args, 2, "relu");
7731                ai::nn_dense(id, units, &act);
7732                return Ok(Value::Unit);
7733            },
7734            // nn_forward(handle, [inputs]) → [outputs]
7735            #[cfg(not(target_arch = "wasm32"))]
7736            "nn_forward" | "神经前向" | "順伝播" | "순전파" | "ส่งต่อโครงข่าย" | "پیش‌روی_شبکه" | "تمرير_أمامي" | "העברה_קדימה" | "فارورڈ_پاس" | "propager_réseau" | "netz_vorwärts" | "прямой_проход_сети" =>
7737            {
7738                let id = self.arg_num(&args, 0, -1.0)? as i64;
7739                let input = self.arg_list_f32(&args, 1);
7740                let out = ai::nn_forward(id, &input);
7741                return Ok(Value::List(Rc::new(
7742                    out.into_iter().map(|v| Value::Number(v as f64)).collect(),
7743                )));
7744            },
7745            // nn_train(handle, [inputs], [targets][, lr]) → loss
7746            #[cfg(not(target_arch = "wasm32"))]
7747            "nn_train" | "训练网" | "ニューラル学習" | "신경망학습" | "ฝึกโครงข่าย" | "آموزش_شبکه" | "درّب_الشبكة" | "אמן_רשת" | "نیٹ_ٹریننگ" | "entraîner_réseau" | "netz_trainieren" | "обучить_сеть" =>
7748            {
7749                let id = self.arg_num(&args, 0, -1.0)? as i64;
7750                let input = self.arg_list_f32(&args, 1);
7751                let target = self.arg_list_f32(&args, 2);
7752                let lr = self.arg_num(&args, 3, 0.01)? as f32;
7753                return Ok(Value::Number(ai::nn_train(id, &input, &target, lr) as f64));
7754            },
7755            // nn_save(handle, path) → bool
7756            #[cfg(not(target_arch = "wasm32"))]
7757            "nn_save" | "保存网" | "網保存" | "신경망저장" | "บันทึกโครงข่าย" | "ذخیره_شبکه" | "احفظ_الشبكة" | "שמור_רשת" | "نیٹ_محفوظ_کرو" | "sauvegarder_réseau" | "netz_speichern" | "сохранить_сеть" =>
7758            {
7759                let id = self.arg_num(&args, 0, -1.0)? as i64;
7760                let path = self.arg_str(&args, 1, "model.lnn");
7761                return Ok(Value::Bool(ai::nn_save(id, &path)));
7762            },
7763            // nn_load(path) → handle (-1 on failure)
7764            #[cfg(not(target_arch = "wasm32"))]
7765            "nn_load" | "载入网" | "網読込" | "신경망불러오기" | "โหลดโครงข่าย" | "بارگذاری_شبکه" | "حمّل_الشبكة" | "טען_רשת" | "نیٹ_لوڈ" | "charger_réseau" | "netz_laden" | "загрузить_сеть" =>
7766            {
7767                let path = self.arg_str(&args, 0, "model.lnn");
7768                return Ok(Value::Number(ai::nn_load(&path) as f64));
7769            },
7770
7771            // ── game AI: behavior trees ──────────────────────────────────────
7772            // bt_build(dsl_string) → handle
7773            #[cfg(not(target_arch = "wasm32"))]
7774            "bt_build" | "建行为树" | "行動木構築" | "행동트리구성" | "สร้างต้นไม้พฤติกรรม" | "ساخت_درخت_رفتار" | "ابنِ_شجرة_السلوك" | "בנה_עץ_התנהגות" | "بی_ٹی_تعمیر" | "construire_arbre_comportement" | "verhaltensbaum_bauen" | "построить_дерево_поведения" =>
7775            {
7776                let spec = self.arg_str(&args, 0, "");
7777                return Ok(Value::Number(ai::bt_build(&spec) as f64));
7778            },
7779            // bt_set(handle, key, value) — set a blackboard fact
7780            #[cfg(not(target_arch = "wasm32"))]
7781            "bt_set" | "设事实" | "事実設定" | "사실설정" | "ตั้งข้อเท็จจริง" | "تنظیم_واقعیت" | "عيّن_حقيقة" | "קבע_עובדה" | "بی_ٹی_سیٹ" | "définir_arbre_comportement" | "verhaltensbaum_setzen" | "задать_дерево_поведения" =>
7782            {
7783                let id = self.arg_num(&args, 0, -1.0)? as i64;
7784                let key = self.arg_str(&args, 1, "");
7785                let val = self.arg_num(&args, 2, 0.0)? as f32;
7786                ai::bt_set(id, &key, val);
7787                return Ok(Value::Unit);
7788            },
7789            // bt_tick(handle) → chosen action name ("" if none)
7790            #[cfg(not(target_arch = "wasm32"))]
7791            "bt_tick" | "行为树滴答" | "行動木更新" | "행동트리틱" | "เดินต้นไม้พฤติกรรม" | "تیک_درخت_رفتار" | "نبضة_شجرة_السلوك" | "טיק_עץ_התנהגות" | "بی_ٹی_ٹک" | "tick_arbre_comportement" | "verhaltensbaum_tick" | "тик_дерева_поведения" =>
7792            {
7793                let id = self.arg_num(&args, 0, -1.0)? as i64;
7794                return Ok(Value::Str(ai::bt_tick(id)));
7795            },
7796            // bt_status(handle) → 0 fail / 1 success / 2 running
7797            #[cfg(not(target_arch = "wasm32"))]
7798            "bt_status" | "行为树状态" | "行動木状態" | "행동트리상태" | "สถานะต้นไม้พฤติกรรม" | "وضعیت_درخت_رفتار" | "حالة_شجرة_السلوك" | "סטטוס_עץ_התנהגות" | "بی_ٹی_حالت" | "statut_arbre_comportement" | "verhaltensbaum_status" | "статус_дерева_поведения" =>
7799            {
7800                let id = self.arg_num(&args, 0, -1.0)? as i64;
7801                return Ok(Value::Number(ai::bt_status(id) as f64));
7802            },
7803
7804            // ── game AI: miniature dialog LLM ────────────────────────────────
7805            // dialog_new([ctx, embed, hidden, seed]) → handle
7806            #[cfg(not(target_arch = "wasm32"))]
7807            "dialog_new" | "建对话模型" | "対話モデル作成" | "대화모델생성" | "สร้างโมเดลสนทนา" | "مدل_گفتگوی_جدید" | "نموذج_حوار_جديد" | "מודל_דיאלוג_חדש" | "نیا_مکالمہ_ماڈل" | "nouveau_dialogue" | "neuer_dialog" | "новый_диалог" =>
7808            {
7809                let ctx = self.arg_num(&args, 0, 3.0)?.max(1.0) as usize;
7810                let embed = self.arg_num(&args, 1, 32.0)?.max(1.0) as usize;
7811                let hidden = self.arg_num(&args, 2, 64.0)?.max(1.0) as usize;
7812                let seed = self.arg_num(&args, 3, 1.0)? as u64;
7813                return Ok(Value::Number(
7814                    ai::dialog_new(ctx, embed, hidden, seed) as f64
7815                ));
7816            },
7817            // dialog_learn(handle, text) — add one utterance to the corpus
7818            #[cfg(not(target_arch = "wasm32"))]
7819            "dialog_learn" | "对话学习" | "対話学習" | "대화학습" | "เรียนรู้สนทนา" | "یادگیری_گفتگو" | "تعلّم_الحوار" | "למד_דיאלוג" | "مکالمہ_سیکھو" | "apprendre_dialogue" | "dialog_lernen" | "обучить_диалог" =>
7820            {
7821                let id = self.arg_num(&args, 0, -1.0)? as i64;
7822                let text = self.arg_str(&args, 1, "");
7823                ai::dialog_learn(id, &text);
7824                return Ok(Value::Unit);
7825            },
7826            // dialog_load(handle, path) → lines added (-1 on error)
7827            #[cfg(not(target_arch = "wasm32"))]
7828            "dialog_load" | "对话载入" | "対話読込" | "대화불러오기" | "โหลดชุดสนทนา" | "بارگذاری_مجموعه_گفتگو" | "حمّل_مجموعة_الحوار" | "טען_מערך_דיאלוג" | "مکالمہ_مجموعہ_لوڈ" | "charger_dialogue" | "dialog_laden" | "загрузить_диалог" =>
7829            {
7830                let id = self.arg_num(&args, 0, -1.0)? as i64;
7831                let path = self.arg_str(&args, 1, "");
7832                return Ok(Value::Number(ai::dialog_load(id, &path) as f64));
7833            },
7834            // dialog_train(handle[, epochs, lr]) → loss
7835            #[cfg(not(target_arch = "wasm32"))]
7836            "dialog_train" | "对话训练" | "対話訓練" | "대화훈련" | "ฝึกสนทนา" | "آموزش_گفتگو" | "درّب_الحوار" | "אמן_דיאלוג" | "مکالمہ_ٹریننگ" | "entraîner_dialogue" | "dialog_trainieren" | "тренировать_диалог" =>
7837            {
7838                let id = self.arg_num(&args, 0, -1.0)? as i64;
7839                let epochs = self.arg_num(&args, 1, 20.0)?.max(1.0) as usize;
7840                let lr = self.arg_num(&args, 2, 0.1)? as f32;
7841                return Ok(Value::Number(ai::dialog_train(id, epochs, lr) as f64));
7842            },
7843            // dialog_say(handle, prompt[, max_tokens, temperature]) → reply text
7844            #[cfg(not(target_arch = "wasm32"))]
7845            "dialog_say" | "对话生成" | "対話生成" | "대화생성" | "พูดสนทนา" | "بگو" | "قل" | "אמור" | "کہو" | "dire_dialogue" | "dialog_sagen" | "сказать_диалог" =>
7846            {
7847                let id = self.arg_num(&args, 0, -1.0)? as i64;
7848                let prompt = self.arg_str(&args, 1, "");
7849                let max = self.arg_num(&args, 2, 24.0)?.max(1.0) as usize;
7850                let temp = self.arg_num(&args, 3, 0.8)? as f32;
7851                return Ok(Value::Str(ai::dialog_say(id, &prompt, max, temp)));
7852            },
7853            // dialog_save(handle, path) → bool
7854            #[cfg(not(target_arch = "wasm32"))]
7855            "dialog_save" | "对话存模" | "対話モデル保存" | "대화모델저장" | "บันทึกโมเดลสนทนา" | "ذخیره_مدل_گفتگو" | "احفظ_نموذج_الحوار" | "שמור_מודל_דיאלוג" | "مکالمہ_ماڈل_محفوظ" | "sauvegarder_dialogue" | "dialog_speichern" | "сохранить_диалог" =>
7856            {
7857                let id = self.arg_num(&args, 0, -1.0)? as i64;
7858                let path = self.arg_str(&args, 1, "model.llm");
7859                return Ok(Value::Bool(ai::dialog_save(id, &path)));
7860            },
7861            // dialog_load_model(path) → handle (-1 on failure)
7862            #[cfg(not(target_arch = "wasm32"))]
7863            "dialog_load_model"
7864            | "对话载模"
7865            | "対話モデル読込"
7866            | "대화모델불러오기"
7867            | "โหลดโมเดลสนทนา" | "بارگذاری_مدل_گفتگو" | "حمّل_نموذج_الحوار" | "טען_מודל_דיאלוג" | "مکالمہ_ماڈل_لوڈ" | "charger_modèle_dialogue" | "dialog_modell_laden" | "загрузить_модель_диалога" => {
7868                let path = self.arg_str(&args, 0, "model.llm");
7869                return Ok(Value::Number(ai::dialog_load_model(&path) as f64));
7870            },
7871
7872            // Decodes `application/x-www-form-urlencoded` text: '+' -> space,
7873            // '%XX' -> byte. Needed to read plain HTML `<form>` POST bodies.
7874            "url_decode" | "网址解码" => {
7875                let s = self.arg_str(&args, 0, "");
7876                let bytes = s.as_bytes();
7877                let mut out = Vec::with_capacity(bytes.len());
7878                let mut i = 0;
7879                while i < bytes.len() {
7880                    match bytes[i] {
7881                        b'+' => {
7882                            out.push(b' ');
7883                            i += 1;
7884                        },
7885                        b'%' if i + 2 < bytes.len() => {
7886                            let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or("");
7887                            match u8::from_str_radix(hex, 16) {
7888                                Ok(b) => {
7889                                    out.push(b);
7890                                    i += 3;
7891                                },
7892                                Err(_) => {
7893                                    out.push(bytes[i]);
7894                                    i += 1;
7895                                },
7896                            }
7897                        },
7898                        b => {
7899                            out.push(b);
7900                            i += 1;
7901                        },
7902                    }
7903                }
7904                return Ok(Value::Str(String::from_utf8_lossy(&out).into_owned()));
7905            },
7906            // Seconds since Unix epoch (float — sub-second precision). No ISO/date
7907            // formatting builtin exists yet; `.ling` code that wants a display
7908            // string currently just uses the raw number.
7909            "now_unix" | "现在时间" => {
7910                return Ok(Value::Number(now_secs()));
7911            },
7912            "file_exists" | "文件存在" => {
7913                #[cfg(target_arch = "wasm32")]
7914                return Ok(Value::Bool(false));
7915                #[cfg(not(target_arch = "wasm32"))]
7916                {
7917                    let path = self.arg_str(&args, 0, "");
7918                    return Ok(Value::Bool(std::path::Path::new(&path).exists()));
7919                }
7920            },
7921            "write_file" | "เขียนไฟล์" | "نوشتن_فایل" | "اكتب_الملف" | "כתוב_קובץ" | "فائل_لکھو" => {
7922                #[cfg(target_arch = "wasm32")]
7923                return Ok(Value::Unit);
7924                #[cfg(not(target_arch = "wasm32"))]
7925                {
7926                    let path = self.arg_str(&args, 0, "");
7927                    let content = self.arg_str(&args, 1, "");
7928                    std::fs::write(&path, content.as_bytes())
7929                        .map_err(|e| EvalErr::from(format!("write_file '{path}': {e}")))?;
7930                    return Ok(Value::Unit);
7931                }
7932            },
7933            "print_file" | "พิมพ์ไฟล์" | "چاپ_فایل" | "اطبع_الملف" | "הדפס_קובץ" | "فائل_چھاپو" => {
7934                let content = self.arg_str(&args, 0, "");
7935                print!("{content}");
7936                return Ok(Value::Unit);
7937            },
7938
7939            // ── CLI arguments ─────────────────────────────────────────────────
7940            "get_args" | "รับอาร์กิวเมนต์" | "دریافت_آرگومان‌ها" | "اجلب_المعاملات" | "קבל_ארגומנטים" | "دلائل_حاصل_کرو" => {
7941                let v: Vec<Value> = std::env::args().map(Value::Str).collect();
7942                return Ok(Value::List(Rc::new(v)));
7943            },
7944
7945            // ── Filesystem: directory walking, stat, content hashing (native) ──
7946            // These power headless batch tools (asset pipelines, indexers). Errors
7947            // degrade gracefully (empty list / 0 / "") so a walk never aborts on one
7948            // unreadable entry.
7949            #[cfg(not(target_arch = "wasm32"))]
7950            "list_dir" | "รายการไดเรกทอรี" | "فهرست_پوشه" | "اسرد_المجلد" | "רשום_תיקייה" | "فولڈر_فہرست" => {
7951                let path = self.arg_str(&args, 0, ".");
7952                let mut paths: Vec<String> = Vec::new();
7953                if let Ok(rd) = std::fs::read_dir(&path) {
7954                    for e in rd.flatten() {
7955                        paths.push(e.path().to_string_lossy().into_owned());
7956                    }
7957                }
7958                paths.sort();
7959                let out: Vec<Value> = paths.into_iter().map(Value::Str).collect();
7960                return Ok(Value::List(Rc::new(out)));
7961            },
7962            #[cfg(not(target_arch = "wasm32"))]
7963            "is_dir" | "เป็นไดเรกทอรี" | "آیا_پوشه_است" | "هل_مجلد" | "האם_תיקייה" | "کیا_فولڈر_ہے" => {
7964                let path = self.arg_str(&args, 0, "");
7965                return Ok(Value::Bool(std::path::Path::new(&path).is_dir()));
7966            },
7967            #[cfg(not(target_arch = "wasm32"))]
7968            "is_file" | "เป็นไฟล์" | "آیا_فایل_است" | "هل_ملف" | "האם_קובץ" | "کیا_فائل_ہے" => {
7969                let path = self.arg_str(&args, 0, "");
7970                return Ok(Value::Bool(std::path::Path::new(&path).is_file()));
7971            },
7972            #[cfg(not(target_arch = "wasm32"))]
7973            "path_name" | "ชื่อไฟล์" | "نام_مسیر" | "اسم_المسار" | "שם_נתיב" | "پاتھ_نام" => {
7974                let path = self.arg_str(&args, 0, "");
7975                let name = std::path::Path::new(&path)
7976                    .file_name()
7977                    .map(|s| s.to_string_lossy().into_owned())
7978                    .unwrap_or_default();
7979                return Ok(Value::Str(name));
7980            },
7981            #[cfg(not(target_arch = "wasm32"))]
7982            "path_ext" | "นามสกุลไฟล์" | "پسوند_مسیر" | "امتداد_المسار" | "סיומת_נתיב" | "پاتھ_ایکسٹینشن" => {
7983                let path = self.arg_str(&args, 0, "");
7984                let ext = std::path::Path::new(&path)
7985                    .extension()
7986                    .map(|s| s.to_string_lossy().to_lowercase())
7987                    .unwrap_or_default();
7988                return Ok(Value::Str(ext));
7989            },
7990            #[cfg(not(target_arch = "wasm32"))]
7991            "file_size" | "ขนาดไฟล์" | "اندازه_فایل" | "حجم_الملف" | "גודל_קובץ" | "فائل_سائز" => {
7992                let path = self.arg_str(&args, 0, "");
7993                let sz = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
7994                return Ok(Value::Number(sz as f64));
7995            },
7996            #[cfg(not(target_arch = "wasm32"))]
7997            "file_modified" | "เวลาที่แก้ไข" | "زمان_تغییر_فایل" | "وقت_تعديل_الملف" | "זמן_עדכון_קובץ" | "فائل_ترمیم_وقت" => {
7998                let path = self.arg_str(&args, 0, "");
7999                let secs = std::fs::metadata(&path)
8000                    .and_then(|m| m.modified())
8001                    .ok()
8002                    .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
8003                    .map(|d| d.as_secs_f64())
8004                    .unwrap_or(0.0);
8005                return Ok(Value::Number(secs));
8006            },
8007            #[cfg(not(target_arch = "wasm32"))]
8008            "file_created" | "เวลาที่สร้าง" | "زمان_ایجاد_فایل" | "وقت_إنشاء_الملف" | "זמן_יצירת_קובץ" | "فائل_تخلیق_وقت" => {
8009                let path = self.arg_str(&args, 0, "");
8010                let secs = std::fs::metadata(&path)
8011                    .ok()
8012                    .and_then(|m| m.created().or_else(|_| m.modified()).ok())
8013                    .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
8014                    .map(|d| d.as_secs_f64())
8015                    .unwrap_or(0.0);
8016                return Ok(Value::Number(secs));
8017            },
8018            #[cfg(not(target_arch = "wasm32"))]
8019            "make_dir" | "สร้างไดเรกทอรี" | "ساخت_پوشه" | "أنشئ_مجلدا" | "צור_תיקייה" | "فولڈر_بناؤ" => {
8020                let path = self.arg_str(&args, 0, "");
8021                return Ok(Value::Bool(std::fs::create_dir_all(&path).is_ok()));
8022            },
8023            // str_strip_prefix("Bearer x", "Bearer ") → "x" (unchanged if absent).
8024            "str_strip_prefix" | "ตัดคำนำหน้า" | "حذف_پیشوند" | "أزل_البادئة" | "הסר_קידומת" | "سابقہ_ہٹاؤ" => {
8025                let s = self.arg_str(&args, 0, "");
8026                let prefix = self.arg_str(&args, 1, "");
8027                return Ok(Value::Str(
8028                    s.strip_prefix(&prefix).unwrap_or(&s).to_string(),
8029                ));
8030            },
8031            // Classify a file by magic bytes: "gzip" | "zip" | "other" | "missing".
8032            // The build-verification gate: only real built archives may publish.
8033            #[cfg(not(target_arch = "wasm32"))]
8034            "file_magic" | "มายาไฟล์" | "امضای_فایل" | "توقيع_الملف" | "חתימת_קובץ" | "فائل_میجک" => {
8035                let path = self.arg_str(&args, 0, "");
8036                let kind = match std::fs::File::open(&path) {
8037                    Ok(mut f) => {
8038                        use std::io::Read;
8039                        let mut buf = [0u8; 4];
8040                        let n = f.read(&mut buf).unwrap_or(0);
8041                        if n >= 2 && buf[0] == 0x1f && buf[1] == 0x8b {
8042                            "gzip"
8043                        } else if n >= 4 && &buf[0..2] == b"PK" {
8044                            "zip"
8045                        } else {
8046                            "other"
8047                        }
8048                    },
8049                    Err(_) => "missing",
8050                };
8051                return Ok(Value::Str(kind.to_string()));
8052            },
8053            // Binary-safe file copy (backups): copy_file(src, dst) → bool.
8054            #[cfg(not(target_arch = "wasm32"))]
8055            "copy_file" | "คัดลอกไฟล์" | "کپی_فایل" | "انسخ_الملف" | "העתק_קובץ" | "فائل_کاپی" => {
8056                let src = self.arg_str(&args, 0, "");
8057                let dst = self.arg_str(&args, 1, "");
8058                if let Some(parent) = std::path::Path::new(&dst).parent() {
8059                    let _ = std::fs::create_dir_all(parent);
8060                }
8061                return Ok(Value::Bool(std::fs::copy(&src, &dst).is_ok()));
8062            },
8063            // ── Read a .tgz (gzip tarball): list file entries / read one file ──
8064            // Powers the GitHub-style "Code / Files" browser: tar_gz_list gives
8065            // the file tree, tar_gz_read pulls one file's text for the viewer.
8066            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8067            "tar_gz_list" | "รายการทาร์" | "فهرست_TAR_GZ" | "اسرد_TAR_GZ" | "רשום_TAR_GZ" | "TAR_GZ_فہرست" => {
8068                let path = self.arg_str(&args, 0, "");
8069                let mut names: Vec<String> = Vec::new();
8070                if let Ok(file) = std::fs::File::open(&path) {
8071                    let gz = flate2::read::GzDecoder::new(file);
8072                    let mut ar = tar::Archive::new(gz);
8073                    if let Ok(entries) = ar.entries() {
8074                        for entry in entries.flatten() {
8075                            if entry.header().entry_type().is_file() {
8076                                if let Ok(p) = entry.path() {
8077                                    names.push(
8078                                        p.to_string_lossy()
8079                                            .trim_start_matches("./")
8080                                            .replace('\\', "/"),
8081                                    );
8082                                }
8083                            }
8084                        }
8085                    }
8086                }
8087                names.sort();
8088                names.dedup();
8089                let out: Vec<Value> = names.into_iter().map(Value::Str).collect();
8090                return Ok(Value::List(Rc::new(out)));
8091            },
8092            // tar_gz_read(archive, entry) → that file's text (utf-8 lossy,
8093            // capped at 256 KiB). Only reads entries that exist in the archive,
8094            // so a caller can't traverse outside it. "" if not found/unreadable.
8095            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8096            "tar_gz_read" | "อ่านทาร์" | "خواندن_TAR_GZ" | "اقرأ_TAR_GZ" | "קרא_TAR_GZ" | "TAR_GZ_پڑھو" => {
8097                let path = self.arg_str(&args, 0, "");
8098                let want = self.arg_str(&args, 1, "");
8099                let want = want.trim_start_matches("./").replace('\\', "/");
8100                let mut content = String::new();
8101                if let Ok(file) = std::fs::File::open(&path) {
8102                    let gz = flate2::read::GzDecoder::new(file);
8103                    let mut ar = tar::Archive::new(gz);
8104                    if let Ok(entries) = ar.entries() {
8105                        for entry in entries.flatten() {
8106                            let mut entry = entry;
8107                            let name = match entry.path() {
8108                                Ok(p) => p
8109                                    .to_string_lossy()
8110                                    .trim_start_matches("./")
8111                                    .replace('\\', "/"),
8112                                Err(_) => continue,
8113                            };
8114                            if name == want {
8115                                use std::io::Read;
8116                                let mut buf = Vec::new();
8117                                let cap = 256 * 1024;
8118                                if entry.take(cap as u64 + 1).read_to_end(&mut buf).is_ok() {
8119                                    let slice = if buf.len() > cap { &buf[..cap] } else { &buf[..] };
8120                                    content = String::from_utf8_lossy(slice).into_owned();
8121                                }
8122                                break;
8123                            }
8124                        }
8125                    }
8126                }
8127                return Ok(Value::Str(content));
8128            },
8129            // ── TOTP (RFC 6238, HMAC-SHA1, 6 digits, 30s) for 2FA ──
8130            // Base32 secret compatible with Google Authenticator / Authy etc.
8131            // `base32_encode`/`totp_code`/`totp_check` only exist under this
8132            // same `feature = "web"` gate (see their definitions above) — a
8133            // build without it must skip these arms too, not just fail to
8134            // link; matches how `file_hash` etc. gate their own arms below.
8135            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8136            "totp_secret" | "โทเทนลับ" | "راز_TOTP" | "سر_TOTP" | "סוד_TOTP" | "TOTP_راز" => {
8137                let mut bytes = [0u8; 20];
8138                rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut bytes);
8139                return Ok(Value::Str(base32_encode(&bytes)));
8140            },
8141            // otpauth:// URI to paste into an authenticator app (or make a QR of).
8142            // No cfg gate needed — pure string formatting, no dependency on
8143            // the web-only TOTP helpers.
8144            "totp_uri" | "โทเทนยูอาร์ไอ" | "آدرس_TOTP" | "رابط_TOTP" | "כתובת_TOTP" | "TOTP_یو_آر_آئی" => {
8145                let secret = self.arg_str(&args, 0, "");
8146                let account = self.arg_str(&args, 1, "user");
8147                let issuer = self.arg_str(&args, 2, "lingfu");
8148                return Ok(Value::Str(format!(
8149                    "otpauth://totp/{issuer}:{account}?secret={secret}&issuer={issuer}&algorithm=SHA1&digits=6&period=30"
8150                )));
8151            },
8152            // Verify a 6-digit code against the secret, allowing ±1 time step.
8153            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8154            "totp_verify" | "โทเทนตรวจ" | "تایید_TOTP" | "تحقق_TOTP" | "אמת_TOTP" | "TOTP_تصدیق" => {
8155                let secret = self.arg_str(&args, 0, "");
8156                let code = self.arg_str(&args, 1, "");
8157                let ok = totp_check(&secret, code.trim());
8158                return Ok(Value::Bool(ok));
8159            },
8160            // The current valid code, for tests/tools.
8161            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8162            "totp_now" | "โทเทนตอนนี้" | "TOTP_اکنون" | "TOTP_الآن" | "TOTP_עכשיו" | "TOTP_ابھی" => {
8163                let secret = self.arg_str(&args, 0, "");
8164                let step = (crate::runtime::now_secs() as u64) / 30;
8165                return Ok(Value::Str(
8166                    totp_code(&secret, step).unwrap_or_default(),
8167                ));
8168            },
8169            // BLAKE3 hex of a file's bytes (binary-safe content fingerprint).
8170            #[cfg(not(target_arch = "wasm32"))]
8171            "file_hash" | "แฮชไฟล์" | "درهم_فایل" | "بصمة_الملف" | "גיבוב_קובץ" | "فائل_ہیش" => {
8172                let path = self.arg_str(&args, 0, "");
8173                match std::fs::read(&path) {
8174                    Ok(bytes) => {
8175                        return Ok(Value::Str(hex_encode(&ling_crypto::Blake3::hash(&bytes))))
8176                    },
8177                    Err(_) => return Ok(Value::Str(String::new())),
8178                }
8179            },
8180            // BLAKE3 hex of an arbitrary string (deterministic id/colour/role seed).
8181            "hash_hex" | "แฮชสตริง" | "درهم_هگزادسیمال" | "بصمة_سداسية" | "גיבוב_הקסדצימלי" | "ہیکس_ہیش" => {
8182                let s = self.arg_str(&args, 0, "");
8183                return Ok(Value::Str(hex_encode(&ling_crypto::Blake3::hash(
8184                    s.as_bytes(),
8185                ))));
8186            },
8187            // Read an environment variable, falling back to a default.
8188            #[cfg(not(target_arch = "wasm32"))]
8189            "env_get" | "รับตัวแปรแวดล้อม" | "دریافت_متغیر_محیطی" | "اجلب_متغير_البيئة" | "קבל_משתנה_סביבה" | "ماحولیاتی_متغیر_حاصل_کرو" => {
8190                let name = self.arg_str(&args, 0, "");
8191                let dflt = self.arg_str(&args, 1, "");
8192                return Ok(Value::Str(std::env::var(&name).unwrap_or(dflt)));
8193            },
8194
8195            // ── String utilities ──────────────────────────────────────────────
8196            // Parses a string to a number (0 on failure — degrades gracefully,
8197            // like the other filesystem/parsing builtins in this file).
8198            "to_number" | "转数字" => {
8199                let s = self.arg_str(&args, 0, "");
8200                return Ok(Value::Number(s.trim().parse().unwrap_or(0.0)));
8201            },
8202            // Plain SHA-256 hex — matches the browser's native SubtleCrypto
8203            // digest("SHA-256", ...), which is what proof-of-work mining uses
8204            // client-side (Web Crypto has no Blake3/SHA-3, so this is the one
8205            // hash both sides can compute natively and fast).
8206            "sha256_hex" | "SHA256哈希" => {
8207                use sha2::Digest;
8208                let s = self.arg_str(&args, 0, "");
8209                let mut h = sha2::Sha256::new();
8210                h.update(s.as_bytes());
8211                return Ok(Value::Str(hex_encode(&h.finalize())));
8212            },
8213            // Parses a hex string (no "0x" prefix) to a number — `to_number`
8214            // uses Rust's plain f64 parser, which doesn't understand hex.
8215            "hex_to_number" | "十六进制转数字" => {
8216                let s = self.arg_str(&args, 0, "");
8217                let v = u64::from_str_radix(s.trim(), 16).unwrap_or(0);
8218                return Ok(Value::Number(v as f64));
8219            },
8220            "split" | "str_split" | "แยก" | "جداسازی" | "قسّم" | "פצל" | "تقسیم_کرو" => {
8221                let s = self.arg_str(&args, 0, "");
8222                let sep = self.arg_str(&args, 1, "\n");
8223                let sep = if sep.is_empty() { "\n".into() } else { sep };
8224                let parts: Vec<Value> = s
8225                    .split(sep.as_str())
8226                    .map(|p| Value::Str(p.to_string()))
8227                    .collect();
8228                return Ok(Value::List(Rc::new(parts)));
8229            },
8230            "trim" | "str_trim" | "ตัดช่องว่าง" | "حذف_فاصله" | "اقتطع_الفراغات" | "חתוך_רווחים" | "خالی_جگہ_کاٹو" => {
8231                let s = self.arg_str(&args, 0, "");
8232                return Ok(Value::Str(s.trim().to_string()));
8233            },
8234            "starts_with" | "str_starts_with" | "เริ่มด้วย" | "شروع_می‌شود_با" | "يبدأ_بـ" | "מתחיל_ב" | "شروع_ہوتا_ہے" => {
8235                let s = self.arg_str(&args, 0, "");
8236                let prefix = self.arg_str(&args, 1, "");
8237                return Ok(Value::Bool(s.starts_with(prefix.as_str())));
8238            },
8239            "ends_with" | "str_ends_with" | "ลงท้ายด้วย" | "پایان_می‌یابد_با" | "ينتهي_بـ" | "מסתיים_ב" | "ختم_ہوتا_ہے" => {
8240                let s = self.arg_str(&args, 0, "");
8241                let suffix = self.arg_str(&args, 1, "");
8242                return Ok(Value::Bool(s.ends_with(suffix.as_str())));
8243            },
8244            "str_replace" | "แทนสตริง" | "جایگزینی_رشته" | "استبدل_النص" | "החלף_מחרוזת" | "اسٹرنگ_تبدیل" => {
8245                let s = self.arg_str(&args, 0, "");
8246                let from = self.arg_str(&args, 1, "");
8247                let to = self.arg_str(&args, 2, "");
8248                return Ok(Value::Str(s.replace(from.as_str(), to.as_str())));
8249            },
8250            "str_find" | "หาในสตริง" | "جستجوی_رشته" | "ابحث_في_النص" | "חפש_מחרוזת" | "اسٹرنگ_تلاش" => {
8251                let s = self.arg_str(&args, 0, "");
8252                let needle = self.arg_str(&args, 1, "");
8253                // Return char index (not byte index) for consistency with substr
8254                let pos = s
8255                    .find(needle.as_str())
8256                    .map(|byte_i| s[..byte_i].chars().count() as f64)
8257                    .unwrap_or(-1.0);
8258                return Ok(Value::Number(pos));
8259            },
8260            "substr" | "str_slice" | "ส่วนสตริง" | "زیررشته" | "جزء_النص" | "תת_מחרוזת" | "ذیلی_اسٹرنگ" => {
8261                let s = self.arg_str(&args, 0, "");
8262                let start = self.arg_num(&args, 1, 0.0)? as usize;
8263                let len = args
8264                    .get(2)
8265                    .map(|v| self.to_number(v).unwrap_or(999999.0) as usize)
8266                    .unwrap_or_else(|| s.chars().count().saturating_sub(start));
8267                let chars: Vec<char> = s.chars().collect();
8268                let end = (start + len).min(chars.len());
8269                let slice: String = chars.get(start..end).unwrap_or(&[]).iter().collect();
8270                return Ok(Value::Str(slice));
8271            },
8272            "to_str" | "str" | "num_str" | "แปลงสตริง" | "تبدیل_به_رشته" | "حوّل_لنص" | "המר_למחרוזת" | "اسٹرنگ_میں_بدلو" => {
8273                let v = args.into_iter().next().unwrap_or(Value::Unit);
8274                return Ok(Value::Str(v.to_string()));
8275            },
8276            "str_repeat" | "ทำซ้ำสตริง" | "تکرار_رشته" | "كرّر_النص" | "חזור_על_מחרוזת" | "اسٹرنگ_دہراؤ" => {
8277                let s = self.arg_str(&args, 0, "");
8278                let n = self.arg_num(&args, 1, 1.0)? as usize;
8279                return Ok(Value::Str(s.repeat(n)));
8280            },
8281            "str_upper" => {
8282                let s = self.arg_str(&args, 0, "");
8283                return Ok(Value::Str(s.to_uppercase()));
8284            },
8285            "str_lower" => {
8286                let s = self.arg_str(&args, 0, "");
8287                return Ok(Value::Str(s.to_lowercase()));
8288            },
8289            "str_len" | "len" | "ความยาว" | "长度" | "長さ" | "길이" | "طول_رشته" | "طول_النص" | "אורך_מחרוזת" | "اسٹرنگ_لمبائی" => {
8290                match args.first() {
8291                    Some(Value::Str(s)) => return Ok(Value::Number(s.chars().count() as f64)),
8292                    Some(Value::List(v)) => return Ok(Value::Number(v.len() as f64)),
8293                    _ => return Ok(Value::Number(0.0)),
8294                }
8295            },
8296
8297            // ── FNV-1a hash (deterministic, normalized 0.0–1.0) ──────────────
8298            "hash_str" | "แฮช" | "درهم_رشته" | "بصمة_نص" | "גיבוב_מחרוזת" | "اسٹرنگ_ہیش" => {
8299                let s = self.arg_str(&args, 0, "");
8300                let mut h: u64 = 14695981039346656037_u64;
8301                for b in s.bytes() {
8302                    h ^= b as u64;
8303                    h = h.wrapping_mul(1099511628211);
8304                }
8305                return Ok(Value::Number((h & 0xFFFFFF) as f64 / 16777215.0));
8306            },
8307            "hash_int" | "แฮชจำนวน" | "درهم_عدد" | "بصمة_عدد" | "גיבוב_מספר" | "نمبر_ہیش" => {
8308                let s = self.arg_str(&args, 0, "");
8309                let n = self.arg_num(&args, 1, 100.0)? as u64;
8310                let mut h: u64 = 14695981039346656037_u64;
8311                for b in s.bytes() {
8312                    h ^= b as u64;
8313                    h = h.wrapping_mul(1099511628211);
8314                }
8315                return Ok(Value::Number((h % n.max(1)) as f64));
8316            },
8317
8318            // ── List utilities ────────────────────────────────────────────────
8319            "list_new" | "รายการใหม่" | "新建列表" | "新規リスト" | "새목록" | "فهرست_جدید" | "قائمة_جديدة" | "רשימה_חדשה" | "نئی_فہرست" | "nouvelle_liste" | "neue_liste" | "новый_список" =>
8320            {
8321                return Ok(Value::List(Rc::new(Vec::new())));
8322            },
8323            "list_push" | "เพิ่มรายการ" | "列表添加" | "リスト追加" | "목록추가" | "افزودن_به_فهرست" | "أضف_للقائمة" | "הוסף_לרשימה" | "فہرست_میں_شامل_کرو" | "ajouter_liste" | "liste_anhängen" | "добавить_в_список" =>
8324            {
8325                let lst = args
8326                    .first()
8327                    .cloned()
8328                    .unwrap_or(Value::List(Rc::new(vec![])));
8329                let val = args.get(1).cloned().unwrap_or(Value::Unit);
8330                if let Value::List(mut v) = lst {
8331                    Rc::make_mut(&mut v).push(val);
8332                    return Ok(Value::List(v));
8333                }
8334                return Ok(Value::List(Rc::new(vec![val])));
8335            },
8336            "list_get" | "รับรายการ" | "取元素" | "要素取得" | "요소가져오기" | "دریافت_از_فهرست" | "اجلب_من_القائمة" | "קבל_מרשימה" | "فہرست_سے_حاصل_کرو" | "obtenir_liste" | "liste_abrufen" | "получить_из_списка" =>
8337            {
8338                // Borrow the list; clone only the element (was cloning the whole list).
8339                let i = self.arg_num(&args, 1, 0.0)? as usize;
8340                if let Some(Value::List(v)) = args.first() {
8341                    return Ok(v.get(i).cloned().unwrap_or(Value::Str(String::new())));
8342                }
8343                return Ok(Value::Str(String::new()));
8344            },
8345            // list_max(numbers, default) / list_min(numbers, default) — `default`
8346            // is returned for an empty list (there's no numeric identity element
8347            // to fall back to otherwise).
8348            "list_max" | "列表最大值" => {
8349                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8350                let default = self.arg_num(&args, 1, 0.0)?;
8351                if let Value::List(v) = lst {
8352                    let mut best = default;
8353                    let mut any = false;
8354                    for item in v.iter() {
8355                        if let Value::Number(n) = item {
8356                            if !any || *n > best {
8357                                best = *n;
8358                                any = true;
8359                            }
8360                        }
8361                    }
8362                    return Ok(Value::Number(best));
8363                }
8364                return Ok(Value::Number(default));
8365            },
8366            "list_min" | "列表最小值" => {
8367                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8368                let default = self.arg_num(&args, 1, 0.0)?;
8369                if let Value::List(v) = lst {
8370                    let mut best = default;
8371                    let mut any = false;
8372                    for item in v.iter() {
8373                        if let Value::Number(n) = item {
8374                            if !any || *n < best {
8375                                best = *n;
8376                                any = true;
8377                            }
8378                        }
8379                    }
8380                    return Ok(Value::Number(best));
8381                }
8382                return Ok(Value::Number(default));
8383            },
8384            // list_set(lst, idx, val) → new list with index replaced. Engine builtin
8385            // (O(n) one copy) to replace the O(n²) ling `ตั้งรายการ` that looped
8386            // list_push + list_get (each of which copied the whole list).
8387            "list_set" | "ตั้งรายการ" | "设元素" | "要素設定" | "요소설정" | "تنظیم_عنصر_فهرست" | "عيّن_عنصر_القائمة" | "קבע_איבר_רשימה" | "فہرست_سیٹ" =>
8388            {
8389                let idx = self.arg_num(&args, 1, 0.0)? as usize;
8390                let mut ai = args.into_iter();
8391                let lst = ai.next().unwrap_or(Value::List(Rc::new(vec![])));
8392                ai.next(); // skip idx
8393                let val = ai.next().unwrap_or(Value::Unit);
8394                if let Value::List(mut v) = lst {
8395                    if idx < v.len() {
8396                        Rc::make_mut(&mut v)[idx] = val;
8397                    }
8398                    return Ok(Value::List(v));
8399                }
8400                return Ok(Value::List(Rc::new(vec![])));
8401            },
8402            "list_join" | "join" | "รวมรายการ" | "连接" | "連結" | "연결" | "پیوستن_فهرست" | "اربط_القائمة" | "חבר_רשימה" | "فہرست_جوڑو" =>
8403            {
8404                let lst = args
8405                    .first()
8406                    .cloned()
8407                    .unwrap_or(Value::List(Rc::new(vec![])));
8408                let sep = args.get(1).map(|v| v.to_string()).unwrap_or_default();
8409                if let Value::List(v) = lst {
8410                    return Ok(Value::Str(
8411                        v.iter()
8412                            .map(|x| x.to_string())
8413                            .collect::<Vec<_>>()
8414                            .join(&sep),
8415                    ));
8416                }
8417                return Ok(Value::Str(String::new()));
8418            },
8419            // list_map/list_filter/list_find — take a closure. Necessary as real
8420            // builtins (not expressible in `.ling` itself): a bare-identifier call
8421            // `f(x)` where `f` is a local variable always resolves through
8422            // `call_named`, which only looks at top-level `fn` definitions by
8423            // design ("call-site locals are intentionally NOT visible to fns") —
8424            // so a closure held in a variable/parameter can't be invoked from
8425            // `.ling` source directly. These call it from the Rust side instead,
8426            // the same way `http_serve` already invokes route-handler closures.
8427            "list_map" | "映射列表" => {
8428                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8429                let f = args.get(1).cloned().unwrap_or(Value::Unit);
8430                if let Value::List(v) = lst {
8431                    let mut out = Vec::with_capacity(v.len());
8432                    for item in v.iter() {
8433                        out.push(self.call_value(f.clone(), vec![item.clone()])?);
8434                    }
8435                    return Ok(Value::List(Rc::new(out)));
8436                }
8437                return Ok(Value::List(Rc::new(vec![])));
8438            },
8439            "list_filter" | "过滤列表" => {
8440                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8441                let f = args.get(1).cloned().unwrap_or(Value::Unit);
8442                if let Value::List(v) = lst {
8443                    let mut out = Vec::new();
8444                    for item in v.iter() {
8445                        if matches!(self.call_value(f.clone(), vec![item.clone()])?, Value::Bool(true)) {
8446                            out.push(item.clone());
8447                        }
8448                    }
8449                    return Ok(Value::List(Rc::new(out)));
8450                }
8451                return Ok(Value::List(Rc::new(vec![])));
8452            },
8453            // First element for which `f` returns true, or Unit if none match.
8454            "list_find" | "查找列表" => {
8455                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8456                let f = args.get(1).cloned().unwrap_or(Value::Unit);
8457                if let Value::List(v) = lst {
8458                    for item in v.iter() {
8459                        if matches!(self.call_value(f.clone(), vec![item.clone()])?, Value::Bool(true)) {
8460                            return Ok(item.clone());
8461                        }
8462                    }
8463                }
8464                return Ok(Value::Unit);
8465            },
8466            // blob_f32("<deflate+base64>") / blob_i32(...) — decode an embedded,
8467            // losslessly-compressed numeric blob into a list. Produced by
8468            // `ling convert`; lets converted assets carry geometry/PCM/etc. compactly.
8469            #[cfg(not(target_arch = "wasm32"))]
8470            "blob_f32" | "blob_i32" => {
8471                let s = self.arg_str(&args, 0, "");
8472                let is_i32 = name == "blob_i32";
8473                match decode_blob(&s) {
8474                    Ok(bytes) => {
8475                        let mut out = Vec::with_capacity(bytes.len() / 4);
8476                        for ch in bytes.chunks_exact(4) {
8477                            let arr = [ch[0], ch[1], ch[2], ch[3]];
8478                            let n = if is_i32 {
8479                                i32::from_le_bytes(arr) as f64
8480                            } else {
8481                                f32::from_le_bytes(arr) as f64
8482                            };
8483                            out.push(Value::Number(n));
8484                        }
8485                        return Ok(Value::List(Rc::new(out)));
8486                    },
8487                    Err(e) => {
8488                        eprintln!("blob decode failed: {e}");
8489                        return Ok(Value::List(Rc::new(vec![])));
8490                    },
8491                }
8492            },
8493
8494            // ══════════════════════════════════════════════════════════════════
8495            // SVG EXPORT  (svg_begin / svg_rect / svg_circle / svg_line /
8496            //              svg_polyline / svg_text / svg_end / hsl_color)
8497            // Chinese aliases: 开始SVG 结束SVG SVG矩形 SVG圆形 SVG线段 SVG折线 SVG文本 HSL颜色
8498            // Thai aliases:    เริ่มSVG จบSVG SVGสี่เหลี่ยม SVGวงกลม SVGเส้น SVGเส้นหัก SVGข้อความ สีHSL
8499            // ══════════════════════════════════════════════════════════════════
8500            "svg_begin" | "开始SVG" | "เริ่มSVG" | "شروع_SVG" | "ابدأ_SVG" | "התחל_SVG" | "SVG_شروع" | "commencer_svg" | "svg_beginnen" | "начать_svg" => {
8501                let path = self.arg_str(&args, 0, "output.svg");
8502                let width = self.arg_num(&args, 1, 800.0)?;
8503                let height = self.arg_num(&args, 2, 600.0)?;
8504                *self.svg.borrow_mut() = Some(SvgWriter::new(path, width, height));
8505                return Ok(Value::Unit);
8506            },
8507
8508            "svg_rect" | "SVG矩形" | "SVGสี่เหลี่ยม" | "مستطیل_SVG" | "مستطيل_SVG" | "מלבן_SVG" | "SVG_مستطیل" | "rectangle_svg" | "svg_rechteck" | "прямоугольник_svg" => {
8509                let x = self.arg_num(&args, 0, 0.0)?;
8510                let y = self.arg_num(&args, 1, 0.0)?;
8511                let w = self.arg_num(&args, 2, 10.0)?;
8512                let h = self.arg_num(&args, 3, 10.0)?;
8513                let fill = self.arg_str(&args, 4, "#ffffff");
8514                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8515                    svg.elements.push(format!(
8516                        "<rect x=\"{x:.1}\" y=\"{y:.1}\" width=\"{w:.1}\" \
8517                         height=\"{h:.1}\" fill=\"{fill}\"/>"
8518                    ));
8519                }
8520                return Ok(Value::Unit);
8521            },
8522
8523            "svg_circle" | "SVG圆形" | "SVGวงกลม" | "دایره_SVG" | "دائرة_SVG" | "עיגול_SVG" | "SVG_دائرہ" | "cercle_svg" | "svg_kreis" | "круг_svg" => {
8524                let cx = self.arg_num(&args, 0, 0.0)?;
8525                let cy = self.arg_num(&args, 1, 0.0)?;
8526                let r = self.arg_num(&args, 2, 5.0)?;
8527                let fill = self.arg_str(&args, 3, "#ffffff");
8528                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8529                    svg.elements.push(format!(
8530                        "<circle cx=\"{cx:.1}\" cy=\"{cy:.1}\" r=\"{r:.1}\" fill=\"{fill}\"/>"
8531                    ));
8532                }
8533                return Ok(Value::Unit);
8534            },
8535
8536            "svg_line" | "SVG线段" | "SVGเส้น" | "خط_SVG" | "خط_SVG" | "קו_SVG" | "SVG_لکیر" | "ligne_svg" | "svg_linie" | "линия_svg" => {
8537                let x1 = self.arg_num(&args, 0, 0.0)?;
8538                let y1 = self.arg_num(&args, 1, 0.0)?;
8539                let x2 = self.arg_num(&args, 2, 0.0)?;
8540                let y2 = self.arg_num(&args, 3, 0.0)?;
8541                let stroke = self.arg_str(&args, 4, "#ffffff");
8542                let sw = self.arg_num(&args, 5, 1.0)?;
8543                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8544                    svg.elements.push(format!(
8545                        "<line x1=\"{x1:.1}\" y1=\"{y1:.1}\" x2=\"{x2:.1}\" y2=\"{y2:.1}\" \
8546                         stroke=\"{stroke}\" stroke-width=\"{sw:.1}\"/>"
8547                    ));
8548                }
8549                return Ok(Value::Unit);
8550            },
8551
8552            "svg_polyline" | "SVG折线" | "SVGเส้นหัก" | "چندخطی_SVG" | "خط_متعدد_SVG" | "קו_שבור_SVG" | "SVG_پولی_لائن" | "polyligne_svg" | "svg_polylinie" | "ломаная_svg" => {
8553                let pts = self.arg_str(&args, 0, "");
8554                let stroke = self.arg_str(&args, 1, "#ffffff");
8555                let sw = self.arg_num(&args, 2, 1.0)?;
8556                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8557                    svg.elements.push(format!(
8558                        "<polyline points=\"{pts}\" fill=\"none\" \
8559                         stroke=\"{stroke}\" stroke-width=\"{sw:.1}\"/>"
8560                    ));
8561                }
8562                return Ok(Value::Unit);
8563            },
8564
8565            "svg_text" | "SVG文本" | "SVGข้อความ" | "متن_SVG" | "نص_SVG" | "טקסט_SVG" | "SVG_متن" | "texte_svg" | "текст_svg" => {
8566                let x = self.arg_num(&args, 0, 0.0)?;
8567                let y = self.arg_num(&args, 1, 0.0)?;
8568                let text = self.arg_str(&args, 2, "");
8569                let fill = self.arg_str(&args, 3, "#ffffff");
8570                let size = self.arg_num(&args, 4, 12.0)?;
8571                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8572                    let safe = text
8573                        .replace('&', "&amp;")
8574                        .replace('<', "&lt;")
8575                        .replace('>', "&gt;");
8576                    svg.elements.push(format!(
8577                        "<text x=\"{x:.1}\" y=\"{y:.1}\" fill=\"{fill}\" \
8578                         font-family=\"monospace\" font-size=\"{size:.0}\">{safe}</text>"
8579                    ));
8580                }
8581                return Ok(Value::Unit);
8582            },
8583
8584            "svg_end" | "结束SVG" | "จบSVG" | "پایان_SVG" | "أنهِ_SVG" | "סיים_SVG" | "SVG_ختم" | "terminer_svg" | "svg_beenden" | "закончить_svg" => {
8585                {
8586                    let borrow = self.svg.borrow();
8587                    if let Some(svg) = borrow.as_ref() {
8588                        svg.save()
8589                            .map_err(|e| EvalErr::from(format!("svg_end: {e}")))?;
8590                    }
8591                }
8592                *self.svg.borrow_mut() = None;
8593                return Ok(Value::Unit);
8594            },
8595
8596            "hsl_color" | "HSL颜色" | "สีHSL" | "رنگ_HSL" | "لون_HSL" | "צבע_HSL" | "HSL_رنگ" | "couleur_hsl" | "hsl_farbe" | "цвет_hsl" => {
8597                let h = self.arg_num(&args, 0, 0.0)?;
8598                let s = self.arg_num(&args, 1, 70.0)?;
8599                let l = self.arg_num(&args, 2, 50.0)?;
8600                return Ok(Value::Str(hsl_to_hex(h, s, l)));
8601            },
8602
8603            // ══════════════════════════════════════════════════════════════════
8604            // FFT / AUDIO ANALYSIS BUILTINS  (native only)
8605            // ══════════════════════════════════════════════════════════════════
8606
8607            // fft_push(samples_list) — feed raw audio samples and run FFT
8608            #[cfg(not(target_arch = "wasm32"))]
8609            "fft_push" | "วิเคราะห์เสียง" | "频谱输入" | "FFT入力" | "FFT입력" | "ورودی_FFT" | "أدخل_FFT" | "הזנת_FFT" | "FFT_ان_پٹ" | "fft_entrée" | "fft_eingabe" | "fft_вход" =>
8610            {
8611                if let Some(Value::List(v)) = args.first() {
8612                    let samples: Vec<f32> = v
8613                        .iter()
8614                        .filter_map(|x| {
8615                            if let Value::Number(n) = x {
8616                                Some(*n as f32)
8617                            } else {
8618                                None
8619                            }
8620                        })
8621                        .collect();
8622                    self.fft.borrow_mut().push_samples(&samples);
8623                }
8624                return Ok(Value::Unit);
8625            },
8626
8627            // fft_bands(n) → list of n log-spaced magnitude bands (0..1)
8628            #[cfg(not(target_arch = "wasm32"))]
8629            "fft_bands" | "แถบความถี่" | "频段" | "周波数帯" | "주파수대" | "باندهای_FFT" | "نطاقات_FFT" | "פסי_FFT" | "FFT_بینڈز" | "fft_bandes" | "fft_bänder" | "fft_полосы" =>
8630            {
8631                let n = self.arg_num(&args, 0, 32.0)? as usize;
8632                let bands = self.fft.borrow().freq_bands(n);
8633                *self.fft_bands_cache.borrow_mut() = bands.clone();
8634                return Ok(Value::List(Rc::new(
8635                    bands.into_iter().map(|v| Value::Number(v as f64)).collect(),
8636                )));
8637            },
8638
8639            // fft_beat() → bool
8640            #[cfg(not(target_arch = "wasm32"))]
8641            "fft_beat" | "จังหวะเสียง" | "节拍检测" | "ビート検出" | "비트" | "ضرب_FFT" | "نبضة_FFT" | "פעימת_FFT" | "FFT_دھڑکن" | "fft_battement" | "fft_takt" | "fft_удар" =>
8642            {
8643                return Ok(Value::Bool(self.fft.borrow().is_beat()));
8644            },
8645
8646            // fft_beat_ratio() → f64  (1.0 = at threshold, >1 = strong beat)
8647            #[cfg(not(target_arch = "wasm32"))]
8648            "fft_beat_ratio" | "อัตราจังหวะ" | "节拍比" | "ビート比" | "비트비율" | "نسبت_ضرب_FFT" | "نسبة_نبضة_FFT" | "יחס_פעימת_FFT" | "FFT_بیٹ_تناسب" =>
8649            {
8650                return Ok(Value::Number(self.fft.borrow().beat_ratio() as f64));
8651            },
8652
8653            // fft_rms() → f64
8654            #[cfg(not(target_arch = "wasm32"))]
8655            "fft_rms" | "ระดับRMS" | "均方根" | "二乗平均" | "RMS레벨" | "RMS_صدا" | "جذر_متوسط_مربع_FFT" | "RMS_של_FFT" | "FFT_RMS" => {
8656                return Ok(Value::Number(self.fft.borrow().rms() as f64));
8657            },
8658
8659            // fft_dominant_freq() → f64  in Hz
8660            #[cfg(not(target_arch = "wasm32"))]
8661            "fft_dominant_freq" | "ความถี่หลัก" | "主频" | "主要周波数" | "주파수" | "فرکانس_غالب" | "التردد_السائد" | "תדר_דומיננטי" | "غالب_فریکوئنسی" | "fft_fréquence_dominante" | "fft_dominante_frequenz" | "fft_доминирующая_частота" =>
8662            {
8663                return Ok(Value::Number(self.fft.borrow().dominant_freq() as f64));
8664            },
8665
8666            // ── wasm32 stubs: fft builtins are no-ops on web ───────────────
8667            #[cfg(target_arch = "wasm32")]
8668            "fft_push" | "วิเคราะห์เสียง" | "频谱输入" | "FFT入力" | "FFT입력" | "ورودی_FFT" | "أدخل_FFT" | "הזנת_FFT" | "FFT_ان_پٹ" | "fft_entrée" | "fft_eingabe" | "fft_вход" =>
8669            {
8670                return Ok(Value::Unit);
8671            },
8672            #[cfg(target_arch = "wasm32")]
8673            "fft_bands" | "แถบความถี่" | "频段" | "周波数帯" | "주파수대" | "باندهای_FFT" | "نطاقات_FFT" | "פסי_FFT" | "FFT_بینڈز" | "fft_bandes" | "fft_bänder" | "fft_полосы" =>
8674            {
8675                let n = self.arg_num(&args, 0, 32.0)? as usize;
8676                return Ok(Value::List(vec![Value::Number(0.0); n].into()));
8677            },
8678            #[cfg(target_arch = "wasm32")]
8679            "fft_beat" | "จังหวะเสียง" | "节拍检测" | "ビート検出" | "비트" | "ضرب_FFT" | "نبضة_FFT" | "פעימת_FFT" | "FFT_دھڑکن" | "fft_battement" | "fft_takt" | "fft_удар" =>
8680            {
8681                return Ok(Value::Bool(false));
8682            },
8683            #[cfg(target_arch = "wasm32")]
8684            "fft_beat_ratio" | "อัตราจังหวะ" | "节拍比" | "ビート比" | "비트비율" | "نسبت_ضرب_FFT" | "نسبة_نبضة_FFT" | "יחס_פעימת_FFT" | "FFT_بیٹ_تناسب" =>
8685            {
8686                return Ok(Value::Number(1.0));
8687            },
8688            #[cfg(target_arch = "wasm32")]
8689            "fft_rms" | "ระดับRMS" | "均方根" | "二乗平均" | "RMS레벨" | "RMS_صدا" | "جذر_متوسط_مربع_FFT" | "RMS_של_FFT" | "FFT_RMS" => {
8690                return Ok(Value::Number(0.0));
8691            },
8692            #[cfg(target_arch = "wasm32")]
8693            "fft_dominant_freq" | "ความถี่หลัก" | "主频" | "主要周波数" | "주파수" | "فرکانس_غالب" | "التردد_السائد" | "תדר_דומיננטי" | "غالب_فریکوئنسی" | "fft_fréquence_dominante" | "fft_dominante_frequenz" | "fft_доминирующая_частота" =>
8694            {
8695                return Ok(Value::Number(0.0));
8696            },
8697
8698            // ══════════════════════════════════════════════════════════════════
8699            // PROCEDURAL TEXTURE BLIT BUILTINS  (screen-space)
8700            // All: name(dst_x, dst_y, width, height, ...params, palette)
8701            // palette: "rainbow" | "fire" | "ocean" | "psychedelic" | "neon" | "forest"
8702            // ══════════════════════════════════════════════════════════════════
8703
8704            // tex_checkerboard(x, y, w, h, tiles, r1,g1,b1, r2,g2,b2)
8705            "tex_checkerboard" | "ลายตารางหมากรุก" | "بافت_شطرنجی" | "نسيج_رقعة_الشطرنج" | "מרקם_שחמט" | "شطرنج_ٹیکسچر" => {
8706                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8707                let tiles = self.arg_num(&args, 4, 8.0)? as u32;
8708                let (r1, g1, b1) = (
8709                    self.arg_num(&args, 5, 255.)? as u32,
8710                    self.arg_num(&args, 6, 255.)? as u32,
8711                    self.arg_num(&args, 7, 255.)? as u32,
8712                );
8713                let (r2, g2, b2) = (
8714                    self.arg_num(&args, 8, 0.)? as u32,
8715                    self.arg_num(&args, 9, 0.)? as u32,
8716                    self.arg_num(&args, 10, 0.)? as u32,
8717                );
8718                let c1 = (r1 << 16) | (g1 << 8) | b1;
8719                let c2 = (r2 << 16) | (g2 << 8) | b2;
8720                let mut gfx = self.gfx.borrow_mut();
8721                let (bw, bh) = (gfx.width, gfx.height);
8722                for row in 0..th {
8723                    for col in 0..tw {
8724                        let cx = col as u32 * tiles / tw as u32;
8725                        let cy = row as u32 * tiles / th as u32;
8726                        let (dx, dy) = (tx + col, ty + row);
8727                        if dx < bw && dy < bh {
8728                            gfx.buffer[dy * bw + dx] = if (cx + cy) % 2 == 0 { c1 } else { c2 };
8729                        }
8730                    }
8731                }
8732                return Ok(Value::Unit);
8733            },
8734
8735            // tex_gradient(x, y, w, h, angle_deg, r1,g1,b1, r2,g2,b2)
8736            "tex_gradient" | "ลายไล่สี" | "بافت_گرادیان" | "نسيج_متدرج" | "מרקם_גרדיאנט" | "گریڈینٹ_ٹیکسچر" => {
8737                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8738                let angle = self.arg_num(&args, 4, 0.0)? as f32;
8739                let (r1, g1, b1) = (
8740                    self.arg_num(&args, 5, 0.)? as f32 / 255.,
8741                    self.arg_num(&args, 6, 0.)? as f32 / 255.,
8742                    self.arg_num(&args, 7, 0.)? as f32 / 255.,
8743                );
8744                let (r2, g2, b2) = (
8745                    self.arg_num(&args, 8, 255.)? as f32 / 255.,
8746                    self.arg_num(&args, 9, 255.)? as f32 / 255.,
8747                    self.arg_num(&args, 10, 255.)? as f32 / 255.,
8748                );
8749                let (ca, sa) = (angle.to_radians().cos(), angle.to_radians().sin());
8750                let mut gfx = self.gfx.borrow_mut();
8751                let (bw, bh) = (gfx.width, gfx.height);
8752                for row in 0..th {
8753                    for col in 0..tw {
8754                        let nx = col as f32 / tw as f32 - 0.5;
8755                        let ny = row as f32 / th as f32 - 0.5;
8756                        let t = ((nx * ca + ny * sa + 0.707) / 1.414).clamp(0., 1.);
8757                        let (dx, dy) = (tx + col, ty + row);
8758                        if dx < bw && dy < bh {
8759                            gfx.buffer[dy * bw + dx] =
8760                                tex_rgb(r1 + (r2 - r1) * t, g1 + (g2 - g1) * t, b1 + (b2 - b1) * t);
8761                        }
8762                    }
8763                }
8764                return Ok(Value::Unit);
8765            },
8766
8767            // tex_noise(x, y, w, h, scale, octaves, seed, palette)
8768            "tex_noise" | "ลายนอยส์" | "بافت_نویز" | "نسيج_ضجيج" | "מרקם_רעש" | "نوائز_ٹیکسچر" => {
8769                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8770                let scale = self.arg_num(&args, 4, 4.0)? as f32;
8771                let octaves = self.arg_num(&args, 5, 4.0)? as u32;
8772                let seed = self.arg_num(&args, 6, 0.0)? as u32;
8773                let palette = self.arg_str(&args, 7, "rainbow");
8774                let mut gfx = self.gfx.borrow_mut();
8775                let (bw, bh) = (gfx.width, gfx.height);
8776                for row in 0..th {
8777                    for col in 0..tw {
8778                        let v = tex_fbm(
8779                            col as f32 * scale / tw as f32,
8780                            row as f32 * scale / th as f32,
8781                            octaves,
8782                            seed,
8783                        );
8784                        let [r, g, b] = tex_palette(&palette, v);
8785                        let (dx, dy) = (tx + col, ty + row);
8786                        if dx < bw && dy < bh {
8787                            gfx.buffer[dy * bw + dx] = tex_rgb(r, g, b);
8788                        }
8789                    }
8790                }
8791                return Ok(Value::Unit);
8792            },
8793
8794            // tex_freq_map(x, y, w, h, time, speed, palette)
8795            // Uses bands written by the last fft_bands() call.
8796            "tex_freq_map" | "ลายความถี่" | "نقشه_فرکانس_بافت" | "خريطة_تردد_النسيج" | "מפת_תדר_מרקם" | "فریکوئنسی_میپ_ٹیکسچر" => {
8797                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8798                let time = self.arg_num(&args, 4, 0.0)? as f32;
8799                let speed = self.arg_num(&args, 5, 0.3)? as f32;
8800                let palette = self.arg_str(&args, 6, "rainbow");
8801                let bands: Vec<f32> = {
8802                    let c = self.fft_bands_cache.borrow();
8803                    if c.is_empty() {
8804                        vec![0.0; 32]
8805                    } else {
8806                        c.clone()
8807                    }
8808                };
8809                let n = bands.len().max(1);
8810                let mut gfx = self.gfx.borrow_mut();
8811                let (bw, bh) = (gfx.width, gfx.height);
8812                for row in 0..th {
8813                    for col in 0..tw {
8814                        let band_idx = (col * n / tw.max(1)).min(n - 1);
8815                        let mag = bands[band_idx].clamp(0., 1.);
8816                        let fill_y = (mag * th as f32) as usize;
8817                        if row >= th.saturating_sub(fill_y) {
8818                            let t = (col as f32 / tw as f32 + time * speed) % 1.0;
8819                            let [r, g, b] = tex_palette(&palette, t);
8820                            let bright = mag * (1.0 - row as f32 / th as f32 * 0.5);
8821                            let (dx, dy) = (tx + col, ty + row);
8822                            if dx < bw && dy < bh {
8823                                gfx.buffer[dy * bw + dx] =
8824                                    tex_rgb(r * bright, g * bright, b * bright);
8825                            }
8826                        }
8827                    }
8828                }
8829                return Ok(Value::Unit);
8830            },
8831
8832            // tex_spiral(x, y, w, h, freq, bands, time, palette)
8833            "tex_spiral" | "ลายเกลียวหมุน" | "بافت_مارپیچ" | "نسيج_حلزوني" | "מרקם_ספירלה" | "سرپیچ_ٹیکسچر" => {
8834                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8835                let freq = self.arg_num(&args, 4, 5.0)? as f32;
8836                let n_bands = self.arg_num(&args, 5, 8.0)? as f32;
8837                let time = self.arg_num(&args, 6, 0.0)? as f32;
8838                let palette = self.arg_str(&args, 7, "rainbow");
8839                let mut gfx = self.gfx.borrow_mut();
8840                let (bw, bh) = (gfx.width, gfx.height);
8841                for row in 0..th {
8842                    for col in 0..tw {
8843                        let nx = col as f32 / tw as f32 - 0.5;
8844                        let ny = row as f32 / th as f32 - 0.5;
8845                        let r = (nx * nx + ny * ny).sqrt();
8846                        let theta = ny.atan2(nx);
8847                        let t = ((r * freq - theta / std::f32::consts::TAU + time * 0.5) * n_bands
8848                            % 1.0)
8849                            .abs();
8850                        let [cr, cg, cb] = tex_palette(&palette, t);
8851                        let (dx, dy) = (tx + col, ty + row);
8852                        if dx < bw && dy < bh {
8853                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
8854                        }
8855                    }
8856                }
8857                return Ok(Value::Unit);
8858            },
8859
8860            // tex_ripple(x, y, w, h, freq, cx, cy, time, palette)
8861            "tex_ripple" | "ลายระลอก" | "بافت_موج" | "نسيج_تموج" | "מרקם_אדווה" | "ریپل_ٹیکسچر" => {
8862                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8863                let freq = self.arg_num(&args, 4, 10.0)? as f32;
8864                let rcx = self.arg_num(&args, 5, 0.5)? as f32;
8865                let rcy = self.arg_num(&args, 6, 0.5)? as f32;
8866                let time = self.arg_num(&args, 7, 0.0)? as f32;
8867                let palette = self.arg_str(&args, 8, "ocean");
8868                let mut gfx = self.gfx.borrow_mut();
8869                let (bw, bh) = (gfx.width, gfx.height);
8870                for row in 0..th {
8871                    for col in 0..tw {
8872                        let nx = col as f32 / tw as f32 - rcx;
8873                        let ny = row as f32 / th as f32 - rcy;
8874                        let r = (nx * nx + ny * ny).sqrt();
8875                        let t = ((r * freq - time) % 1.0).abs();
8876                        let [cr, cg, cb] = tex_palette(&palette, t);
8877                        let (dx, dy) = (tx + col, ty + row);
8878                        if dx < bw && dy < bh {
8879                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
8880                        }
8881                    }
8882                }
8883                return Ok(Value::Unit);
8884            },
8885
8886            // tex_mandelbrot(x, y, w, h, zoom, cx, cy, max_iter, palette)
8887            "tex_mandelbrot" | "ลายแมนเดลบรอต" | "بافت_ماندلبرو" | "نسيج_مانديلبروت" | "מרקם_מנדלברוט" | "مینڈل_بروٹ_ٹیکسچر" => {
8888                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8889                let zoom = self.arg_num(&args, 4, 1.0)?;
8890                let mcx = self.arg_num(&args, 5, -0.5)?;
8891                let mcy = self.arg_num(&args, 6, 0.0)?;
8892                let max_iter = self.arg_num(&args, 7, 64.0)? as u32;
8893                let palette = self.arg_str(&args, 8, "psychedelic");
8894                let mut gfx = self.gfx.borrow_mut();
8895                let (bw, bh) = (gfx.width, gfx.height);
8896                for row in 0..th {
8897                    for col in 0..tw {
8898                        let zx0 = (col as f64 / tw as f64 - 0.5) / zoom + mcx;
8899                        let zy0 = (row as f64 / th as f64 - 0.5) / zoom + mcy;
8900                        let mut x = 0.0f64;
8901                        let mut y = 0.0f64;
8902                        let mut i = 0u32;
8903                        while i < max_iter && x * x + y * y < 4.0 {
8904                            let t = x * x - y * y + zx0;
8905                            y = 2.0 * x * y + zy0;
8906                            x = t;
8907                            i += 1;
8908                        }
8909                        let t = if i == max_iter {
8910                            0.0f32
8911                        } else {
8912                            (i as f32
8913                                - (x as f32 * x as f32 + y as f32 * y as f32).ln().ln()
8914                                    / 2.0f32.ln())
8915                                / max_iter as f32
8916                        };
8917                        let [cr, cg, cb] = tex_palette(&palette, t.clamp(0., 1.));
8918                        let (dx, dy) = (tx + col, ty + row);
8919                        if dx < bw && dy < bh {
8920                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
8921                        }
8922                    }
8923                }
8924                return Ok(Value::Unit);
8925            },
8926
8927            // tex_julia(x, y, w, h, c_re, c_im, max_iter, palette)
8928            "tex_julia" | "ลายจูเลีย" | "بافت_ژولیا" | "نسيج_جوليا" | "מרקם_ג'וליה" | "جولیا_ٹیکسچر" => {
8929                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8930                let c_re = self.arg_num(&args, 4, -0.7)?;
8931                let c_im = self.arg_num(&args, 5, 0.27)?;
8932                let max_iter = self.arg_num(&args, 6, 64.0)? as u32;
8933                let palette = self.arg_str(&args, 7, "neon");
8934                let mut gfx = self.gfx.borrow_mut();
8935                let (bw, bh) = (gfx.width, gfx.height);
8936                for row in 0..th {
8937                    for col in 0..tw {
8938                        let mut zx = (col as f64 / tw as f64 - 0.5) * 3.5;
8939                        let mut zy = (row as f64 / th as f64 - 0.5) * 3.5;
8940                        let mut i = 0u32;
8941                        while i < max_iter && zx * zx + zy * zy < 4.0 {
8942                            let t = zx * zx - zy * zy + c_re;
8943                            zy = 2.0 * zx * zy + c_im;
8944                            zx = t;
8945                            i += 1;
8946                        }
8947                        let t = i as f32 / max_iter as f32;
8948                        let [cr, cg, cb] = tex_palette(&palette, t);
8949                        let (dx, dy) = (tx + col, ty + row);
8950                        if dx < bw && dy < bh {
8951                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
8952                        }
8953                    }
8954                }
8955                return Ok(Value::Unit);
8956            },
8957
8958            // tex_voronoi(x, y, w, h, cells, seed, palette)
8959            "tex_voronoi" | "ลายโวโรนอย" | "بافت_ورونوی" | "نسيج_فورونوي" | "מרקם_וורונוי" | "ورونوئی_ٹیکسچر" => {
8960                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8961                let cells = self.arg_num(&args, 4, 16.0)? as u32;
8962                let seed = self.arg_num(&args, 5, 42.0)? as u32;
8963                let palette = self.arg_str(&args, 6, "rainbow");
8964                let pts: Vec<[f32; 2]> = (0..cells)
8965                    .map(|i| {
8966                        [
8967                            tex_hash(i as i32, 0, seed),
8968                            tex_hash(i as i32, 1, seed + 999),
8969                        ]
8970                    })
8971                    .collect();
8972                let mut gfx = self.gfx.borrow_mut();
8973                let (bw, bh) = (gfx.width, gfx.height);
8974                for row in 0..th {
8975                    for col in 0..tw {
8976                        let (fx, fy) = (col as f32 / tw as f32, row as f32 / th as f32);
8977                        let (min_d, nearest) = pts.iter().enumerate().fold(
8978                            (f32::MAX, 0usize),
8979                            |(d, idx), (i, &[cx, cy])| {
8980                                let dd = (fx - cx).powi(2) + (fy - cy).powi(2);
8981                                if dd < d {
8982                                    (dd, i)
8983                                } else {
8984                                    (d, idx)
8985                                }
8986                            },
8987                        );
8988                        let t = (nearest as f32 / cells as f32 + min_d * 4.0) % 1.0;
8989                        let [cr, cg, cb] = tex_palette(&palette, t);
8990                        let (dx, dy) = (tx + col, ty + row);
8991                        if dx < bw && dy < bh {
8992                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
8993                        }
8994                    }
8995                }
8996                return Ok(Value::Unit);
8997            },
8998
8999            // tex_halftone(x, y, w, h, dot_size, time, palette)
9000            "tex_halftone" | "ลายฮาล์ฟโทน" | "بافت_نیم‌تن" | "نسيج_نصفي" | "מרקם_חצי_גוון" | "ہاف_ٹون_ٹیکسچر" => {
9001                let (tx, ty, tw, th) = self.tex_rect(&args)?;
9002                let dot_size = self.arg_num(&args, 4, 0.05)? as f32;
9003                let time = self.arg_num(&args, 5, 0.0)? as f32;
9004                let palette = self.arg_str(&args, 6, "rainbow");
9005                let mut gfx = self.gfx.borrow_mut();
9006                let (bw, bh) = (gfx.width, gfx.height);
9007                for row in 0..th {
9008                    for col in 0..tw {
9009                        let (fx, fy) = (col as f32 / tw as f32, row as f32 / th as f32);
9010                        let gx = (fx / dot_size).floor();
9011                        let gy = (fy / dot_size).floor();
9012                        let lx = (fx / dot_size - gx - 0.5) * 2.0;
9013                        let ly = (fy / dot_size - gy - 0.5) * 2.0;
9014                        let r = (lx * lx + ly * ly).sqrt();
9015                        let t = (gx / (1.0 / dot_size) + time * 0.1) % 1.0;
9016                        let a = if r < 0.7 {
9017                            ((0.7 - r) / 0.7).clamp(0., 1.)
9018                        } else {
9019                            0.0
9020                        };
9021                        if a > 0.0 {
9022                            let [cr, cg, cb] = tex_palette(&palette, t);
9023                            let (dx, dy) = (tx + col, ty + row);
9024                            if dx < bw && dy < bh {
9025                                gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
9026                            }
9027                        }
9028                    }
9029                }
9030                return Ok(Value::Unit);
9031            },
9032
9033            // ══════════════════════════════════════════════════════════════════
9034            // RENDER / LIGHTING MODES  (holographic cel shading)
9035            // ══════════════════════════════════════════════════════════════════
9036            // set_shade_mode(m) — 0 flat · 1 cel · 2 holo (default)
9037            "set_shade_mode" | "设置着色" | "シェード設定" | "셰이드모드" | "ตั้งการแรเงา" | "تنظیم_حالت_سایه‌پردازی" | "عيّن_نمط_التظليل" | "קבע_מצב_הצללה" | "شیڈ_موڈ_مقرر_کرو" | "définir_mode_ombrage" | "schattierungsmodus_setzen" | "задать_режим_затенения" =>
9038            {
9039                let m = self.arg_num(&args, 0, 2.0)? as u8;
9040                self.gfx.borrow_mut().shade_mode = m;
9041                return Ok(Value::Unit);
9042            },
9043            // set_cel_bands(n) — number of posterisation bands (>=2)
9044            "set_cel_bands" | "设置色阶" | "セル段数" | "셀밴드" | "ตั้งระดับสี" | "تنظیم_باندهای_سل" | "عيّن_نطاقات_التظليل" | "קבע_רצועות_הצללה" | "سیل_بینڈز_مقرر_کرو" | "définir_bandes_cel" | "cel_bänder_setzen" | "задать_полосы_cel" =>
9045            {
9046                let n = (self.arg_num(&args, 0, 4.0)? as u32).max(2);
9047                self.gfx.borrow_mut().shade.bands = n;
9048                return Ok(Value::Unit);
9049            },
9050            // set_shadow_color(r,g,b) — coloured-shadow tint, 0-255
9051            "set_shadow_color" | "设置阴影色" | "影の色" | "그림자색" | "ตั้งสีเงา" | "تنظیم_رنگ_سایه" | "عيّن_لون_الظل" | "קבע_צבע_צל" | "سایہ_رنگ_مقرر_کرو" | "définir_couleur_ombre" | "schattenfarbe_setzen" | "задать_цвет_тени" =>
9052            {
9053                let r = self.arg_num(&args, 0, 26.)? as f32 / 255.0;
9054                let g = self.arg_num(&args, 1, 33.)? as f32 / 255.0;
9055                let b = self.arg_num(&args, 2, 77.)? as f32 / 255.0;
9056                self.gfx.borrow_mut().shade.shadow = [r, g, b];
9057                return Ok(Value::Unit);
9058            },
9059            // set_rim(strength, r,g,b) — holographic fresnel edge glow
9060            // ══════════════════════════════════════════════════════════════════
9061            // CRYPTOGRAPHY (ling-crypto) — geo suite, hybrid PQ KEM, holographic
9062            // Bytes cross the language boundary as lowercase hex strings.
9063            // ══════════════════════════════════════════════════════════════════
9064            #[cfg(not(target_arch = "wasm32"))]
9065            "crypto_hash" | "แฮชเข้ารหัส" | "几何哈希" | "幾何ハッシュ" | "기하해시" | "درهم_رمزنگاری" | "بصمة_تشفير" | "גיבוב_הצפנה" | "خفیہ_ہیش" | "hachage_crypto" | "krypto_hash" | "крипто_хеш" =>
9066            {
9067                let s = self.arg_str(&args, 0, "");
9068                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(
9069                    s.as_bytes(),
9070                ))));
9071            },
9072            #[cfg(target_arch = "wasm32")]
9073            "crypto_hash" | "แฮชเข้ารหัส" | "几何哈希" | "幾何ハッシュ" | "기하해시" | "درهم_رمزنگاری" | "بصمة_تشفير" | "גיבוב_הצפנה" | "خفیہ_ہیش" | "hachage_crypto" | "krypto_hash" | "крипто_хеш" =>
9074            {
9075                let s = self.arg_str(&args, 0, "");
9076                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(
9077                    s.as_bytes(),
9078                ))));
9079            },
9080            // 3-D torus-knot fingerprint of any text/key → flat [x,y,z, x,y,z, …]
9081            #[cfg(not(target_arch = "wasm32"))]
9082            "knot_points" | "จุดปม" | "结点坐标" | "結び目点" | "매듭점" | "نقاط_گره" | "نقاط_العقدة" | "נקודות_קשר" | "گرہ_پوائنٹس" | "points_nœud" | "knotenpunkte" | "точки_узла" => {
9083                let s = self.arg_str(&args, 0, "");
9084                let shape = ling_crypto::geo::KnotShape::from_bytes(s.as_bytes());
9085                let mut out = Vec::with_capacity(shape.points.len() * 3);
9086                for p in &shape.points {
9087                    out.push(Value::Number(p[0] as f64));
9088                    out.push(Value::Number(p[1] as f64));
9089                    out.push(Value::Number(p[2] as f64));
9090                }
9091                return Ok(Value::List(Rc::new(out)));
9092            },
9093            #[cfg(target_arch = "wasm32")]
9094            "knot_points" | "จุดปม" | "结点坐标" | "結び目点" | "매듭점" | "نقاط_گره" | "نقاط_العقدة" | "נקודות_קשר" | "گرہ_پوائنٹس" | "points_nœud" | "knotenpunkte" | "точки_узла" => {
9095                let s = self.arg_str(&args, 0, "");
9096                let shape = ling_crypto::geo::KnotShape::from_bytes(s.as_bytes());
9097                let mut out = Vec::with_capacity(shape.points.len() * 3);
9098                for p in &shape.points {
9099                    out.push(Value::Number(p[0] as f64));
9100                    out.push(Value::Number(p[1] as f64));
9101                    out.push(Value::Number(p[2] as f64));
9102                }
9103                return Ok(Value::List(out.into()));
9104            },
9105            #[cfg(not(target_arch = "wasm32"))]
9106            "knot_label" | "ป้ายปม" | "结点标签" | "結び目ラベル" | "매듭라벨" | "برچسب_گره" | "تسمية_العقدة" | "תווית_קשר" | "گرہ_لیبل" | "étiquette_nœud" | "knotenbezeichnung" | "метка_узла" =>
9107            {
9108                let s = self.arg_str(&args, 0, "");
9109                return Ok(Value::Str(
9110                    ling_crypto::geo::KnotShape::from_bytes(s.as_bytes()).label(),
9111                ));
9112            },
9113            #[cfg(target_arch = "wasm32")]
9114            "knot_label" | "ป้ายปม" | "结点标签" | "結び目ラベル" | "매듭라벨" | "برچسب_گره" | "تسمية_العقدة" | "תווית_קשר" | "گرہ_لیبل" | "étiquette_nœud" | "knotenbezeichnung" | "метка_узла" =>
9115            {
9116                let s = self.arg_str(&args, 0, "");
9117                return Ok(Value::Str(
9118                    ling_crypto::geo::KnotShape::from_bytes(s.as_bytes()).label(),
9119                ));
9120            },
9121            // KEM keypair (hybrid X25519+ML-KEM-768) → integer handle
9122            #[cfg(not(target_arch = "wasm32"))]
9123            "knot_keygen" | "hybrid_keygen" | "สร้างกุญแจปม" | "生成密钥" | "鍵生成" | "키생성" | "تولید_کلید_گره" | "توليد_مفتاح_العقدة" | "יצירת_מפתח_קשר" | "گرہ_کلید_تخلیق" | "génération_clé_nœud" | "knotenschlüsselerzeugung" | "генерация_ключа_узла" =>
9124            {
9125                self.crypto_ids.push(ling_crypto::KnotIdentity::generate());
9126                return Ok(Value::Number((self.crypto_ids.len() - 1) as f64));
9127            },
9128            #[cfg(not(target_arch = "wasm32"))]
9129            "knot_public" | "hybrid_public" | "กุญแจสาธารณะปม" | "公钥" | "公開鍵" | "공개키" | "کلید_عمومی_گره" | "مفتاح_العقدة_العام" | "מפתח_ציבורי_קשר" | "گرہ_عوامی_کلید" | "clé_publique_nœud" | "knoten_öffentlicher_schlüssel" | "публичный_ключ_узла" =>
9130            {
9131                let h = self.arg_num(&args, 0, 0.0)? as usize;
9132                let pk = self
9133                    .crypto_ids
9134                    .get(h)
9135                    .map(|id| hex_encode(id.public_key()))
9136                    .unwrap_or_default();
9137                return Ok(Value::Str(pk));
9138            },
9139            // encapsulate(pubkey_hex) → [ciphertext_hex, shared_secret_hex]
9140            #[cfg(not(target_arch = "wasm32"))]
9141            "knot_encapsulate"
9142            | "hybrid_encapsulate"
9143            | "ห่อกุญแจปม"
9144            | "封装密钥"
9145            | "カプセル化"
9146            | "캡슐화" | "کپسوله‌سازی_گره" | "تغليف_مفتاح_العقدة" | "עטיפת_קשר" | "گرہ_احاطہ" | "encapsuler_nœud" | "knoten_kapseln" | "инкапсулировать_узел" => {
9147                let pk = hex_decode(&self.arg_str(&args, 0, ""));
9148                match ling_crypto::geo::knot_encapsulate(&pk) {
9149                    Ok((ct, ss)) => {
9150                        return Ok(Value::List(Rc::new(vec![
9151                            Value::Str(hex_encode(&ct)),
9152                            Value::Str(hex_encode(&ss)),
9153                        ])))
9154                    },
9155                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
9156                }
9157            },
9158            // decapsulate(handle, ciphertext_hex) → shared_secret_hex
9159            #[cfg(not(target_arch = "wasm32"))]
9160            "knot_decapsulate"
9161            | "hybrid_decapsulate"
9162            | "แกะกุญแจปม"
9163            | "解封装密钥"
9164            | "カプセル解除"
9165            | "캡슐해제" | "بازکردن_کپسوله_گره" | "فك_تغليف_مفتاح_العقدة" | "פתיחת_עטיפת_קשר" | "گرہ_احاطہ_کھولو" | "décapsuler_nœud" | "knoten_entkapseln" | "декапсулировать_узел" => {
9166                let h = self.arg_num(&args, 0, 0.0)? as usize;
9167                let ct = hex_decode(&self.arg_str(&args, 1, ""));
9168                let ss = self
9169                    .crypto_ids
9170                    .get(h)
9171                    .and_then(|id| id.decapsulate(&ct).ok())
9172                    .map(|s| hex_encode(&s))
9173                    .unwrap_or_default();
9174                return Ok(Value::Str(ss));
9175            },
9176            // Authenticated encryption (XChaCha20-Poly1305) — seal(key_hex, text) → ct_hex
9177            #[cfg(not(target_arch = "wasm32"))]
9178            "crypto_seal" | "ผนึก" | "封印" | "封印する" | "봉인" | "مهر_رمزنگاری" | "ختم_تشفير" | "חתימת_הצפנה" | "خفیہ_مہر" | "sceller_crypto" | "krypto_versiegeln" | "запечатать_крипто" => {
9179                let key = hex_to_32(&self.arg_str(&args, 0, ""));
9180                let pt = self.arg_str(&args, 1, "");
9181                match ling_crypto::geo::holo_seal(key, pt.as_bytes()) {
9182                    Ok(ct) => return Ok(Value::Str(hex_encode(&ct))),
9183                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
9184                }
9185            },
9186            #[cfg(not(target_arch = "wasm32"))]
9187            "crypto_open" | "เปิดผนึก" | "解封" | "封印解除" | "봉인해제" | "بازکردن_مهر" | "فتح_الختم" | "פתיחת_חתימה" | "مہر_کھولو" | "ouvrir_crypto" | "krypto_öffnen" | "открыть_крипто" =>
9188            {
9189                let key = hex_to_32(&self.arg_str(&args, 0, ""));
9190                let ct = hex_decode(&self.arg_str(&args, 1, ""));
9191                match ling_crypto::geo::holo_open(key, &ct) {
9192                    Ok(pt) => return Ok(Value::Str(String::from_utf8_lossy(&pt).into_owned())),
9193                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
9194                }
9195            },
9196            // Holographic all-or-nothing transform — 4-D fragment coords [a,b,c,d, …]
9197            #[cfg(not(target_arch = "wasm32"))]
9198            "holo_points" | "จุดโฮโลแกรม" | "全息点" | "ホログラム点" | "홀로그램점" | "نقاط_هولوگرام" | "نقاط_الهولوغرام" | "נקודות_הולוגרמה" | "ہولوگرام_پوائنٹس" | "points_holo" | "holo_punkte" | "точки_голо" =>
9199            {
9200                let s = self.arg_str(&args, 0, "");
9201                let frags = ling_crypto::geo::scatter(s.as_bytes());
9202                let mut out = Vec::with_capacity(frags.len() * 4);
9203                for f in &frags {
9204                    for c in f.coord {
9205                        out.push(Value::Number(c as f64));
9206                    }
9207                }
9208                return Ok(Value::List(Rc::new(out)));
9209            },
9210            #[cfg(not(target_arch = "wasm32"))]
9211            "holo_fragment_count"
9212            | "จำนวนชิ้นโฮโลแกรม"
9213            | "全息碎片数"
9214            | "ホログラム断片数"
9215            | "홀로그램조각수" | "تعداد_قطعات_هولوگرام" | "عدد_شظايا_الهولوغرام" | "מספר_שברי_הולוגרמה" | "ہولوگرام_ٹکڑے_تعداد" | "nombre_fragments_holo" | "holo_fragmentanzahl" | "число_фрагментов_голо" => {
9216                let s = self.arg_str(&args, 0, "");
9217                return Ok(Value::Number(
9218                    ling_crypto::geo::scatter(s.as_bytes()).len() as f64
9219                ));
9220            },
9221            // SHAKE-256 XOF, squeezed to an arbitrary output length in bytes
9222            // (`shake_hex(s, 128)` = a 1024-bit seal digest).
9223            #[cfg(not(target_arch = "wasm32"))]
9224            "shake_hex" | "SHAKE哈希" => {
9225                let s = self.arg_str(&args, 0, "");
9226                let len = self.arg_num(&args, 1, 32.0)?.max(0.0) as usize;
9227                return Ok(Value::Str(hex_encode(&ling_crypto::Shake256::hash(s.as_bytes(), len))));
9228            },
9229            // Ed25519 signing keypair (issuer identity) → integer handle.
9230            #[cfg(not(target_arch = "wasm32"))]
9231            "ed25519_keygen" | "생성서명키" => {
9232                self.ed25519_ids.push(ling_crypto::Ed25519Keypair::generate());
9233                return Ok(Value::Number((self.ed25519_ids.len() - 1) as f64));
9234            },
9235            // Deterministic keypair from a 32-byte hex seed — the same seed
9236            // always yields the same keypair, so a program can persist just the
9237            // seed (e.g. a bank's issuer identity) and rederive identical keys
9238            // across restarts instead of every run minting a fresh, unrelated one.
9239            #[cfg(not(target_arch = "wasm32"))]
9240            "ed25519_keygen_from_seed" | "씨앗에서생성서명키" => {
9241                let seed = hex_to_32(&self.arg_str(&args, 0, ""));
9242                self.ed25519_ids.push(ling_crypto::Ed25519Keypair::from_seed(seed));
9243                return Ok(Value::Number((self.ed25519_ids.len() - 1) as f64));
9244            },
9245            #[cfg(not(target_arch = "wasm32"))]
9246            "ed25519_public" | "서명공개키" => {
9247                let h = self.arg_num(&args, 0, 0.0)? as usize;
9248                let pk = self
9249                    .ed25519_ids
9250                    .get(h)
9251                    .map(|kp| hex_encode(&kp.public_key()))
9252                    .unwrap_or_default();
9253                return Ok(Value::Str(pk));
9254            },
9255            // ed25519_sign(handle, message) → signature hex (64 bytes)
9256            #[cfg(not(target_arch = "wasm32"))]
9257            "ed25519_sign" | "서명하다" => {
9258                let h = self.arg_num(&args, 0, 0.0)? as usize;
9259                let msg = self.arg_str(&args, 1, "");
9260                let sig = self
9261                    .ed25519_ids
9262                    .get(h)
9263                    .map(|kp| hex_encode(&kp.sign(msg.as_bytes())))
9264                    .unwrap_or_default();
9265                return Ok(Value::Str(sig));
9266            },
9267            // ed25519_verify(pubkey_hex, message, signature_hex) → bool
9268            #[cfg(not(target_arch = "wasm32"))]
9269            "ed25519_verify" | "서명확인" => {
9270                let pk_hex = self.arg_str(&args, 0, "");
9271                let msg = self.arg_str(&args, 1, "");
9272                let sig_hex = self.arg_str(&args, 2, "");
9273                let pk_bytes = hex_decode(&pk_hex);
9274                let sig_bytes = hex_decode(&sig_hex);
9275                let ok = (|| {
9276                    let pk: [u8; 32] = pk_bytes.try_into().ok()?;
9277                    let sig: [u8; 64] = sig_bytes.try_into().ok()?;
9278                    Some(ling_crypto::Ed25519Keypair::verify(&pk, msg.as_bytes(), &sig).is_ok())
9279                })()
9280                .unwrap_or(false);
9281                return Ok(Value::Bool(ok));
9282            },
9283            // Argon2id password hashing — password_hash(pw) → PHC string,
9284            // password_verify(pw, phc_string) → bool.
9285            #[cfg(not(target_arch = "wasm32"))]
9286            "password_hash" | "비밀번호해시" => {
9287                let pw = self.arg_str(&args, 0, "");
9288                let hash = ling_crypto::Argon2idParams::default()
9289                    .hash_password(pw.as_bytes())
9290                    .unwrap_or_default();
9291                return Ok(Value::Str(hash));
9292            },
9293            #[cfg(not(target_arch = "wasm32"))]
9294            "password_verify" | "비밀번호확인" => {
9295                let pw = self.arg_str(&args, 0, "");
9296                let hash = self.arg_str(&args, 1, "");
9297                let ok = ling_crypto::Argon2idParams::verify_password(pw.as_bytes(), &hash).is_ok();
9298                return Ok(Value::Bool(ok));
9299            },
9300            // OS-CSPRNG random bytes as hex — session ids / nonces (not the
9301            // xorshift `rand` builtin, which is for game logic, not security).
9302            #[cfg(not(target_arch = "wasm32"))]
9303            "random_hex" | "무작위16진수" => {
9304                use rand::RngCore;
9305                let n = self.arg_num(&args, 0, 16.0)?.max(0.0) as usize;
9306                let mut buf = vec![0u8; n];
9307                rand::rngs::OsRng.fill_bytes(&mut buf);
9308                return Ok(Value::Str(hex_encode(&buf)));
9309            },
9310            // base64_encode(s) — text -> base64 (matches what canvas.toDataURL()
9311            // already produces client-side, so PNG uploads never need a binary
9312            // request body).
9313            #[cfg(not(target_arch = "wasm32"))]
9314            "base64_encode" | "base64인코딩" => {
9315                use base64::Engine as _;
9316                let s = self.arg_str(&args, 0, "");
9317                return Ok(Value::Str(
9318                    base64::engine::general_purpose::STANDARD.encode(s.as_bytes()),
9319                ));
9320            },
9321            // base64_decode_to_file(b64, path) — writes decoded bytes straight to
9322            // disk; returns true on success. The only way binary data (an
9323            // uploaded/rendered PNG) reaches the filesystem from `.ling` source.
9324            #[cfg(not(target_arch = "wasm32"))]
9325            "base64_decode_to_file" | "base64파일로저장" => {
9326                use base64::Engine as _;
9327                let b64 = self.arg_str(&args, 0, "");
9328                let path = self.arg_str(&args, 1, "");
9329                let b64 = b64
9330                    .split(',')
9331                    .next_back()
9332                    .unwrap_or(&b64); // tolerate a "data:image/png;base64,..." prefix
9333                let ok = base64::engine::general_purpose::STANDARD
9334                    .decode(b64)
9335                    .ok()
9336                    .and_then(|bytes| std::fs::write(&path, bytes).ok())
9337                    .is_some();
9338                return Ok(Value::Bool(ok));
9339            },
9340            // qr_svg(text) — a scannable QR code as an inline <svg>...</svg>
9341            // string (e.g. for an otpauth:// 2FA enrollment URI). Kept as SVG
9342            // rather than a rasterized image so it fits the same "everything
9343            // stays vector" theme as the banknote seal art.
9344            #[cfg(not(target_arch = "wasm32"))]
9345            "qr_svg" | "QR코드" => {
9346                let text = self.arg_str(&args, 0, "");
9347                let svg = qrcode::QrCode::new(text.as_bytes())
9348                    .map(|code| {
9349                        code.render::<qrcode::render::svg::Color>()
9350                            .min_dimensions(240, 240)
9351                            .dark_color(qrcode::render::svg::Color("#1a0f3d"))
9352                            .light_color(qrcode::render::svg::Color("#ffffff"))
9353                            .build()
9354                    })
9355                    .unwrap_or_default();
9356                return Ok(Value::Str(svg));
9357            },
9358            // zip_files(paths_list, out_path) — bundles files into a zip archive
9359            // (used by the "render" step to package a note's PNG/SVG/PDF).
9360            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
9361            "zip_files" | "압축파일" => {
9362                let paths = match args.first() {
9363                    Some(Value::List(l)) => l.iter().map(|v| v.to_string()).collect::<Vec<_>>(),
9364                    _ => Vec::new(),
9365                };
9366                let out_path = self.arg_str(&args, 1, "out.zip");
9367                let ok = (|| -> std::io::Result<()> {
9368                    let file = std::fs::File::create(&out_path)?;
9369                    let mut writer = zip::ZipWriter::new(file);
9370                    let options: zip::write::FileOptions<'_, ()> = zip::write::FileOptions::default()
9371                        .compression_method(zip::CompressionMethod::Deflated);
9372                    for p in &paths {
9373                        let name = std::path::Path::new(p)
9374                            .file_name()
9375                            .map(|n| n.to_string_lossy().into_owned())
9376                            .unwrap_or_else(|| p.clone());
9377                        let bytes = std::fs::read(p)?;
9378                        writer.start_file(name, options)?;
9379                        std::io::Write::write_all(&mut writer, &bytes)?;
9380                    }
9381                    writer.finish()?;
9382                    Ok(())
9383                })()
9384                .is_ok();
9385                return Ok(Value::Bool(ok));
9386            },
9387            // pdf_from_images(png_paths_list, out_path) — one page per image,
9388            // sized to its pixel dimensions. No PDF crate dependency: `image`
9389            // (decode) and `flate2` (deflate the page's raw RGB stream) are
9390            // already unconditional deps, so this hand-writes the handful of
9391            // PDF objects (Catalog/Pages/Page/Contents/Image XObject) directly.
9392            // `build_pdf_from_images` itself only exists under this same gate.
9393            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
9394            "pdf_from_images" | "PDF来自图片" => {
9395                let paths = match args.first() {
9396                    Some(Value::List(l)) => l.iter().map(|v| v.to_string()).collect::<Vec<_>>(),
9397                    _ => Vec::new(),
9398                };
9399                let out_path = self.arg_str(&args, 1, "out.pdf");
9400                let ok = build_pdf_from_images(&paths, &out_path).is_ok();
9401                return Ok(Value::Bool(ok));
9402            },
9403
9404            // ══════════════════════════════════════════════════════════════════
9405            // ling-ui — animation easings + holographic vector widgets + text I/O
9406            // ══════════════════════════════════════════════════════════════════
9407            "ease" => {
9408                let name = self.arg_str(&args, 0, "ease");
9409                let t = self.arg_num(&args, 1, 0.0)? as f32;
9410                return Ok(Value::Number(
9411                    ling_ui::Easing::from_name(&name).apply(t) as f64
9412                ));
9413            },
9414
9415            // ══════════════════════════════════════════════════════════════════
9416            // Anima — unified animation drivers (ling-animation). Organic 灵 +
9417            // mechanical 机 scalar drivers, callable per frame from a script.
9418            // ══════════════════════════════════════════════════════════════════
9419            "tween" | "补间" | "補間" | "트윈" | "แทรกค่า" | "میان‌فریم" | "تدرج_حركي" | "טווין" | "ٹوئین" | "твин" => {
9420                let a = self.arg_num(&args, 0, 0.0)?;
9421                let b = self.arg_num(&args, 1, 0.0)?;
9422                let t = self.arg_num(&args, 2, 0.0)?.clamp(0.0, 1.0);
9423                return Ok(Value::Number(a + (b - a) * t));
9424            },
9425            "tween_ease" | "缓动补间" | "緩和補間" | "이징트윈" | "แทรกนุ่ม" | "میان‌فریم_نرم" | "تدرج_ناعم_حركي" | "טווין_חלק" | "ٹوئین_ایز" | "tween_lisse" | "tween_glättung" | "твин_плавность" =>
9426            {
9427                let a = self.arg_num(&args, 0, 0.0)? as f32;
9428                let b = self.arg_num(&args, 1, 0.0)? as f32;
9429                let t = self.arg_num(&args, 2, 0.0)? as f32;
9430                let kind = self.arg_str(&args, 3, "linear");
9431                let e = ling_animation::EaseFunction::from_name(&kind);
9432                return Ok(Value::Number(
9433                    ling_animation::ease::tween_ease(&a, &b, t, e) as f64,
9434                ));
9435            },
9436            // ── Organic 灵 ──
9437            "breathe" | "呼吸" | "호흡" | "หายใจ" | "تنفس" | "تنفس" | "נשימה" | "سانس" | "respirer" | "atmen" | "дышать" => {
9438                let t = self.arg_num(&args, 0, 0.0)? as f32;
9439                let rate = self.arg_num(&args, 1, 1.0)? as f32;
9440                let depth = self.arg_num(&args, 2, 0.1)? as f32;
9441                return Ok(Value::Number(
9442                    ling_animation::scalar::breathe(t, rate, depth) as f64,
9443                ));
9444            },
9445            "wobble" | "摆动" | "揺れ" | "흔들림" | "โยก" | "نوسان" | "تذبذب" | "תנודה" | "لرزش" | "osciller" | "wackeln" | "покачивание" => {
9446                let t = self.arg_num(&args, 0, 0.0)? as f32;
9447                let freq = self.arg_num(&args, 1, 1.0)? as f32;
9448                let amp = self.arg_num(&args, 2, 1.0)? as f32;
9449                let phase = self.arg_num(&args, 3, 0.0)? as f32;
9450                return Ok(Value::Number(
9451                    ling_animation::scalar::wobble(t, freq, amp, phase) as f64,
9452                ));
9453            },
9454            "gait_phase" | "步相" | "歩相" | "걸음위상" | "เฟสก้าว" | "فاز_گام" | "طور_المشية" | "שלב_הליכה" | "چال_مرحلہ" | "phase_démarche" | "gangphase" | "фаза_походки" => {
9455                let t = self.arg_num(&args, 0, 0.0)? as f32;
9456                let speed = self.arg_num(&args, 1, 1.0)? as f32;
9457                return Ok(Value::Number(
9458                    ling_animation::scalar::gait_phase(t, speed) as f64
9459                ));
9460            },
9461            "gait_swing" | "步摆" | "歩振り" | "걸음흔들" | "ก้าวแกว่ง" | "نوسان_گام" | "أرجحة_المشية" | "נדנוד_הליכה" | "چال_جھولا" | "balancement_démarche" | "gangschwung" | "мах_походки" =>
9462            {
9463                let t = self.arg_num(&args, 0, 0.0)? as f32;
9464                let speed = self.arg_num(&args, 1, 1.0)? as f32;
9465                let stride = self.arg_num(&args, 2, 1.0)? as f32;
9466                return Ok(Value::Number(
9467                    ling_animation::scalar::gait_swing(t, speed, stride) as f64,
9468                ));
9469            },
9470            "gait_lift" | "抬脚" | "足上げ" | "발들기" | "ยกเท้า" | "بلندشدن_گام" | "رفع_المشية" | "הרמת_הליכה" | "چال_اٹھاؤ" | "levée_démarche" | "ganghub" | "подъём_походки" => {
9471                let t = self.arg_num(&args, 0, 0.0)? as f32;
9472                let speed = self.arg_num(&args, 1, 1.0)? as f32;
9473                let height = self.arg_num(&args, 2, 1.0)? as f32;
9474                return Ok(Value::Number(
9475                    ling_animation::scalar::gait_lift(t, speed, height) as f64,
9476                ));
9477            },
9478            "spring_to" | "弹向" | "バネ寄せ" | "스프링이동" | "สปริงไป" | "فنر_به‌سوی" | "نابض_إلى" | "קפיץ_אל" | "اسپرنگ_تک" | "ressort_vers" | "feder_zu" | "пружина_к" =>
9479            {
9480                let pos = self.arg_num(&args, 0, 0.0)? as f32;
9481                let vel = self.arg_num(&args, 1, 0.0)? as f32;
9482                let target = self.arg_num(&args, 2, 0.0)? as f32;
9483                let stiffness = self.arg_num(&args, 3, 120.0)? as f32;
9484                let damping = self.arg_num(&args, 4, 14.0)? as f32;
9485                let dt = self.arg_num(&args, 5, 1.0 / 60.0)? as f32;
9486                let (np, nv) =
9487                    ling_animation::scalar::spring_step(pos, vel, target, stiffness, damping, dt);
9488                return Ok(Value::List(Rc::new(vec![
9489                    Value::Number(np as f64),
9490                    Value::Number(nv as f64),
9491                ])));
9492            },
9493            "ik2" | "反解" | "逆運動" | "역운동" | "ไอเค2" | "سینماتیک_معکوس2" | "حركية_عكسية2" | "קינמטיקה_הפוכה2" | "آئی_کے2" | "cinématique_inverse2" | "inverse_kinematik2" | "обратная_кинематика2" => {
9494                let l1 = self.arg_num(&args, 0, 1.0)? as f32;
9495                let l2 = self.arg_num(&args, 1, 1.0)? as f32;
9496                let tx = self.arg_num(&args, 2, 0.0)? as f32;
9497                let ty = self.arg_num(&args, 3, 0.0)? as f32;
9498                let (sh, el) = ling_animation::scalar::two_bone_ik(l1, l2, tx, ty);
9499                return Ok(Value::List(Rc::new(vec![
9500                    Value::Number(sh as f64),
9501                    Value::Number(el as f64),
9502                ])));
9503            },
9504            // ── Mechanical 机 ──
9505            "gear_couple" | "齿轮联动" | "歯車連動" | "기어연동" | "เฟืองทด" | "جفت_چرخ‌دنده" | "اقتران_التروس" | "צימוד_גלגלי_שיניים" | "گیئر_جوڑا" | "accoupler_engrenage" | "zahnrad_koppeln" | "сцепить_шестерни" =>
9506            {
9507                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9508                let ti = self.arg_num(&args, 1, 1.0)? as f32;
9509                let to = self.arg_num(&args, 2, 1.0)? as f32;
9510                return Ok(Value::Number(
9511                    ling_animation::scalar::gear(angle, ti, to) as f64
9512                ));
9513            },
9514            "gear_train" | "齿轮组" | "歯車列" | "기어열" | "ชุดเฟือง" | "مجموعه_چرخ‌دنده" | "قطار_التروس" | "שרשרת_גלגלי_שיניים" | "گیئر_ٹرین" | "train_engrenages" | "zahnradgetriebe" | "передача_шестерён" => {
9515                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9516                let teeth: Vec<f32> = match args.get(1) {
9517                    Some(Value::List(items)) => items
9518                        .iter()
9519                        .filter_map(|v| {
9520                            if let Value::Number(n) = v {
9521                                Some(*n as f32)
9522                            } else {
9523                                None
9524                            }
9525                        })
9526                        .collect(),
9527                    _ => Vec::new(),
9528                };
9529                let out = ling_animation::mechanism::gear_train(angle, &teeth);
9530                return Ok(Value::List(Rc::new(
9531                    out.into_iter().map(|a| Value::Number(a as f64)).collect(),
9532                )));
9533            },
9534            "cam_lift" | "凸轮升程" | "カム揚程" | "캠리프트" | "ยกลูกเบี้ยว" | "بلندشدن_بادامک" | "رفع_الكامة" | "הרמת_קאם" | "کیم_اٹھاؤ" | "levée_came" | "nockenhub" | "подъём_кулачка" =>
9535            {
9536                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9537                let lift = self.arg_num(&args, 1, 1.0)? as f32;
9538                return Ok(Value::Number(
9539                    ling_animation::scalar::cam_lift(angle, lift) as f64
9540                ));
9541            },
9542            "piston" | "活塞" | "ピストン" | "피스톤" | "ลูกสูบ" | "پیستون" | "مكبس" | "בוכנה" | "پسٹن" | "kolben" | "поршень" => {
9543                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9544                let crank = self.arg_num(&args, 1, 1.0)? as f32;
9545                let rod = self.arg_num(&args, 2, 2.0)? as f32;
9546                return Ok(Value::Number(
9547                    ling_animation::scalar::piston(angle, crank, rod) as f64,
9548                ));
9549            },
9550            "rack" | "齿条" | "ラック" | "랙" | "แร็ค" | "زبانه‌دنده" | "سكة_مسننة" | "מוט_שיניים" | "ریک" | "crémaillère" | "zahnstange" | "рейка" => {
9551                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9552                let radius = self.arg_num(&args, 1, 1.0)? as f32;
9553                return Ok(Value::Number(
9554                    ling_animation::scalar::rack(angle, radius) as f64
9555                ));
9556            },
9557            #[cfg(not(target_arch = "wasm32"))]
9558            "mouse_x" => {
9559                let gfx = self.gfx.borrow();
9560                let v = gfx
9561                    .window
9562                    .as_ref()
9563                    .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
9564                    .map(|p| p.0 as f64)
9565                    .unwrap_or(0.0);
9566                return Ok(Value::Number(v));
9567            },
9568            #[cfg(target_arch = "wasm32")]
9569            "mouse_x" => {
9570                return Ok(Value::Number(crate::gfx::wasm_mouse_x() as f64));
9571            },
9572            #[cfg(not(target_arch = "wasm32"))]
9573            "mouse_y" => {
9574                let gfx = self.gfx.borrow();
9575                let v = gfx
9576                    .window
9577                    .as_ref()
9578                    .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
9579                    .map(|p| p.1 as f64)
9580                    .unwrap_or(0.0);
9581                return Ok(Value::Number(v));
9582            },
9583            #[cfg(target_arch = "wasm32")]
9584            "mouse_y" => {
9585                return Ok(Value::Number(crate::gfx::wasm_mouse_y() as f64));
9586            },
9587            #[cfg(not(target_arch = "wasm32"))]
9588            "mouse_down" => {
9589                let mut gfx = self.gfx.borrow_mut();
9590                let d = !gfx.input_suppressed()
9591                    && gfx
9592                        .window
9593                        .as_ref()
9594                        .map(|w| w.get_mouse_down(minifb::MouseButton::Left))
9595                        .unwrap_or(false);
9596                return Ok(Value::Bool(d));
9597            },
9598            #[cfg(target_arch = "wasm32")]
9599            "mouse_down" => {
9600                return Ok(Value::Bool(crate::gfx::wasm_mouse_down()));
9601            },
9602            #[cfg(not(target_arch = "wasm32"))]
9603            "mouse_down_right" | "เมาส์ขวา" | "ماوس_راست_فشرده" | "الفأرة_اليمنى_مضغوطة" | "עכבר_ימני_לחוץ" | "دایاں_ماؤس_دبا_ہوا" => {
9604                let mut gfx = self.gfx.borrow_mut();
9605                let d = !gfx.input_suppressed()
9606                    && gfx
9607                        .window
9608                        .as_ref()
9609                        .map(|w| w.get_mouse_down(minifb::MouseButton::Right))
9610                        .unwrap_or(false);
9611                return Ok(Value::Bool(d));
9612            },
9613            #[cfg(target_arch = "wasm32")]
9614            "mouse_down_right" | "เมาส์ขวา" | "ماوس_راست_فشرده" | "الفأرة_اليمنى_مضغوطة" | "עכבר_ימני_לחוץ" | "دایاں_ماؤس_دبا_ہوا" => {
9615                return Ok(Value::Bool(crate::gfx::wasm_mouse_down_right()));
9616            },
9617            #[cfg(not(target_arch = "wasm32"))]
9618            "mouse_down_middle" | "เมาส์กลาง" | "ماوس_وسط_فشرده" | "الفأرة_الوسطى_مضغوطة" | "עכבר_אמצעי_לחוץ" | "درمیانی_ماؤس_دبا_ہوا" => {
9619                let mut gfx = self.gfx.borrow_mut();
9620                let d = !gfx.input_suppressed()
9621                    && gfx
9622                        .window
9623                        .as_ref()
9624                        .map(|w| w.get_mouse_down(minifb::MouseButton::Middle))
9625                        .unwrap_or(false);
9626                return Ok(Value::Bool(d));
9627            },
9628            #[cfg(target_arch = "wasm32")]
9629            "mouse_down_middle" | "เมาส์กลาง" | "ماوس_وسط_فشرده" | "الفأرة_الوسطى_مضغوطة" | "עכבר_אמצעי_לחוץ" | "درمیانی_ماؤس_دبا_ہوا" => {
9630                return Ok(Value::Bool(crate::gfx::wasm_mouse_down_middle()));
9631            },
9632            #[cfg(not(target_arch = "wasm32"))]
9633            "ui_hot" | "热区" | "ホットエリア" | "핫존" | "พื้นที่สัมผัส" | "ناحیه_فعال" | "منطقة_ساخنة" | "אזור_חם" | "ہاٹ_زون" | "survol_ui" | "ui_hover" | "ui_наведение" =>
9634            {
9635                let x = self.arg_num(&args, 0, 0.0)? as f32;
9636                let y = self.arg_num(&args, 1, 0.0)? as f32;
9637                let w = self.arg_num(&args, 2, 0.0)? as f32;
9638                let h = self.arg_num(&args, 3, 0.0)? as f32;
9639                let gfx = self.gfx.borrow();
9640                let (mx, my) = gfx
9641                    .window
9642                    .as_ref()
9643                    .and_then(|win| win.get_mouse_pos(minifb::MouseMode::Clamp))
9644                    .unwrap_or((0.0, 0.0));
9645                return Ok(Value::Bool(ling_ui::holo::hit_rect(mx, my, x, y, w, h)));
9646            },
9647            #[cfg(target_arch = "wasm32")]
9648            "ui_hot" | "热区" | "ホットエリア" | "핫존" | "พื้นที่สัมผัส" | "ناحیه_فعال" | "منطقة_ساخنة" | "אזור_חם" | "ہاٹ_زون" | "survol_ui" | "ui_hover" | "ui_наведение" =>
9649            {
9650                return Ok(Value::Bool(false));
9651            },
9652            // ui_text(x, y, scale, "string") — holographic vector text
9653            "ui_text" | "界面文字" | "UI文字" | "UI텍스트" | "ข้อความหน้าจอ" | "متن_رابط" | "نص_الواجهة" | "טקסט_ממשק" | "یو_آئی_متن" | "texte_ui" | "ui_beschriftung" | "ui_текст" =>
9654            {
9655                let x = self.arg_num(&args, 0, 0.0)? as f32;
9656                let y = self.arg_num(&args, 1, 0.0)? as f32;
9657                let scale = self.arg_num(&args, 2, 16.0)? as f32;
9658                let s = self.arg_str(&args, 3, "");
9659                let segs = ling_ui::holo::text_lines(&s, x, y, scale * 0.62, scale, scale * 0.24);
9660                let mut gfx = self.gfx.borrow_mut();
9661                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
9662                for sg in segs {
9663                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
9664                }
9665                return Ok(Value::Unit);
9666            },
9667            // font_load("path.ttf") — load a vector font (outlines cached lazily as
9668            // cache/fonts/<stem>/<codepoint>.ling). Returns a handle, or -1 on failure.
9669            #[cfg(not(target_arch = "wasm32"))]
9670            "font_load" | "โหลดฟอนต์" | "加载字体" | "フォント読込" | "글꼴로드" | "بارگذاری_فونت" | "تحميل_الخط" | "טעינת_גופן" | "فونٹ_لوڈ" | "charger_police" | "schriftart_laden" | "загрузить_шрифт" =>
9671            {
9672                let path = self.arg_str(&args, 0, "");
9673                // Optional 2nd arg: variable-font weight (e.g. 600 for a solid, bold UI).
9674                let weight = match self.arg_num(&args, 1, 0.0)? {
9675                    w if w > 0.0 => Some(w as f32),
9676                    _ => None,
9677                };
9678                // Try the path as given, then relative to the script's directory.
9679                let mut loaded = ling_graphics::VectorFont::from_path_weight(&path, weight);
9680                if loaded.is_err() {
9681                    if let Some(dir) = &self.source_dir {
9682                        let joined = dir.join(&path);
9683                        loaded = ling_graphics::VectorFont::from_path_weight(
9684                            &joined.to_string_lossy(),
9685                            weight,
9686                        );
9687                    }
9688                }
9689                match loaded {
9690                    Ok(f) => {
9691                        let id = self.fonts.len();
9692                        self.fonts.push(f);
9693                        return Ok(Value::Number(id as f64));
9694                    },
9695                    Err(e) => {
9696                        eprintln!("font_load failed ({path}): {e}");
9697                        return Ok(Value::Number(-1.0));
9698                    },
9699                }
9700            },
9701            #[cfg(target_arch = "wasm32")]
9702            "font_load" | "โหลดฟอนต์" | "加载字体" | "フォント読込" | "글꼴로드" | "بارگذاری_فونت" | "تحميل_الخط" | "טעינת_גופן" | "فونٹ_لوڈ" | "charger_police" | "schriftart_laden" | "загрузить_шрифт" =>
9703            {
9704                // Web runtime does not load host TTF/OTF files yet.
9705                // Return -1 so scripts can fall back to ui_text.
9706                return Ok(Value::Number(-1.0));
9707            },
9708            // image_load("path.png") — decode a raster image (via the `image` crate)
9709            // for pixel sampling (image_width/image_height/image_pixel_r/g/b/a) —
9710            // used by the coin-stamp mosaic tool to read a source photo's
9711            // colour/darkness. Returns a handle, or -1 on failure.
9712            #[cfg(not(target_arch = "wasm32"))]
9713            "image_load" =>
9714            {
9715                let path = self.arg_str(&args, 0, "");
9716                let mut loaded = image::open(&path);
9717                if loaded.is_err() {
9718                    if let Some(dir) = &self.source_dir {
9719                        let joined = dir.join(&path);
9720                        loaded = image::open(&joined);
9721                    }
9722                }
9723                match loaded {
9724                    Ok(img) => {
9725                        let id = self.images.len();
9726                        self.images.push(img.to_rgba8());
9727                        return Ok(Value::Number(id as f64));
9728                    },
9729                    Err(e) => {
9730                        eprintln!("image_load failed ({path}): {e}");
9731                        return Ok(Value::Number(-1.0));
9732                    },
9733                }
9734            },
9735            #[cfg(target_arch = "wasm32")]
9736            "image_load" =>
9737            {
9738                // Web runtime does not load host image files yet.
9739                return Ok(Value::Number(-1.0));
9740            },
9741            "image_width" =>
9742            {
9743                let id = self.arg_num(&args, 0, -1.0)? as i64;
9744                if id >= 0 && (id as usize) < self.images.len() {
9745                    return Ok(Value::Number(self.images[id as usize].width() as f64));
9746                }
9747                return Ok(Value::Number(0.0));
9748            },
9749            "image_height" =>
9750            {
9751                let id = self.arg_num(&args, 0, -1.0)? as i64;
9752                if id >= 0 && (id as usize) < self.images.len() {
9753                    return Ok(Value::Number(self.images[id as usize].height() as f64));
9754                }
9755                return Ok(Value::Number(0.0));
9756            },
9757            "image_pixel_r" | "image_pixel_g" | "image_pixel_b" | "image_pixel_a" =>
9758            {
9759                let id = self.arg_num(&args, 0, -1.0)? as i64;
9760                let px = self.arg_num(&args, 1, 0.0)? as i64;
9761                let py = self.arg_num(&args, 2, 0.0)? as i64;
9762                if id >= 0 && (id as usize) < self.images.len() {
9763                    let img = &self.images[id as usize];
9764                    if px >= 0 && py >= 0 && (px as u32) < img.width() && (py as u32) < img.height() {
9765                        let p = img.get_pixel(px as u32, py as u32);
9766                        let ch = match name {
9767                            "image_pixel_r" => p[0],
9768                            "image_pixel_g" => p[1],
9769                            "image_pixel_b" => p[2],
9770                            _ => p[3],
9771                        };
9772                        return Ok(Value::Number(ch as f64));
9773                    }
9774                }
9775                return Ok(Value::Number(0.0));
9776            },
9777            // image_new(w, h) — a new blank (fully transparent) RGBA image the
9778            // script can paint into with image_set_pixel and write out with
9779            // image_save. Lives in the same self.images table as image_load,
9780            // so image_width/image_height/image_pixel_* all work on it too.
9781            // Used by the coin-stamp tool to build cropped, physically-sized
9782            // (mm x DPI) PNG exports — something a raw window screenshot()
9783            // can't do, since it always captures the whole on-screen
9784            // framebuffer at whatever size the window happens to be.
9785            "image_new" =>
9786            {
9787                let w = self.arg_num(&args, 0, 1.0)?.max(1.0) as u32;
9788                let h = self.arg_num(&args, 1, 1.0)?.max(1.0) as u32;
9789                let id = self.images.len();
9790                self.images.push(image::RgbaImage::new(w, h));
9791                return Ok(Value::Number(id as f64));
9792            },
9793            // image_set_pixel(id, x, y, r, g, b, a) — paint one pixel of an
9794            // image created with image_new (0..255 channels; out-of-bounds is
9795            // a silent no-op, matching image_pixel_*'s own out-of-bounds
9796            // behaviour).
9797            "image_set_pixel" =>
9798            {
9799                let id = self.arg_num(&args, 0, -1.0)? as i64;
9800                let px = self.arg_num(&args, 1, 0.0)? as i64;
9801                let py = self.arg_num(&args, 2, 0.0)? as i64;
9802                let r = self.arg_num(&args, 3, 0.0)?.clamp(0.0, 255.0) as u8;
9803                let g = self.arg_num(&args, 4, 0.0)?.clamp(0.0, 255.0) as u8;
9804                let b = self.arg_num(&args, 5, 0.0)?.clamp(0.0, 255.0) as u8;
9805                let a = self.arg_num(&args, 6, 255.0)?.clamp(0.0, 255.0) as u8;
9806                if id >= 0 && (id as usize) < self.images.len() {
9807                    let img = &mut self.images[id as usize];
9808                    if px >= 0 && py >= 0 && (px as u32) < img.width() && (py as u32) < img.height() {
9809                        img.put_pixel(px as u32, py as u32, image::Rgba([r, g, b, a]));
9810                    }
9811                }
9812                return Ok(Value::Unit);
9813            },
9814            // image_save(id, "path.png") — encode an image (from image_new or
9815            // image_load) to disk, alpha preserved. Returns 1 on success, -1
9816            // on failure (bad id or write error), mirroring image_load's own
9817            // -1-on-failure convention. Path resolves the same way
9818            // write_file/copy_file's outputs do: relative to the script's own
9819            // working directory (typically the app dir the launcher cd's
9820            // into), not source_dir.
9821            #[cfg(not(target_arch = "wasm32"))]
9822            "image_save" =>
9823            {
9824                let id = self.arg_num(&args, 0, -1.0)? as i64;
9825                let path = self.arg_str(&args, 1, "");
9826                if id >= 0 && (id as usize) < self.images.len() {
9827                    if let Some(parent) = std::path::Path::new(&path).parent() {
9828                        if !parent.as_os_str().is_empty() {
9829                            let _ = std::fs::create_dir_all(parent);
9830                        }
9831                    }
9832                    if self.images[id as usize].save(&path).is_ok() {
9833                        return Ok(Value::Number(1.0));
9834                    }
9835                }
9836                return Ok(Value::Number(-1.0));
9837            },
9838            #[cfg(target_arch = "wasm32")]
9839            "image_save" =>
9840            {
9841                return Ok(Value::Number(-1.0));
9842            },
9843            // image_draw(id, x, y, w, h) — blit an image (nearest-neighbour
9844            // scaled to w x h, alpha-blended against whatever's already in
9845            // the framebuffer) into the current frame. A native pixel loop,
9846            // not a .ling-level per-pixel image_pixel_*+pixel() loop: doing
9847            // this from script for even a modest thumbnail grid re-incurs
9848            // the exact per-frame interpreted-call-volume cost that made
9849            // small mosaic tiles hang the UI (see mosaic.ling's
9850            // xform_glyph_pts_fit fix) — this is the "read the framebuffer
9851            // out" direction's counterpart to screenshot().
9852            #[cfg(not(target_arch = "wasm32"))]
9853            "image_draw" =>
9854            {
9855                let id = self.arg_num(&args, 0, -1.0)? as i64;
9856                let dx = self.arg_num(&args, 1, 0.0)? as i32;
9857                let dy = self.arg_num(&args, 2, 0.0)? as i32;
9858                let dw = self.arg_num(&args, 3, 0.0)?.max(0.0) as i32;
9859                let dh = self.arg_num(&args, 4, 0.0)?.max(0.0) as i32;
9860                if id >= 0 && (id as usize) < self.images.len() && dw > 0 && dh > 0 {
9861                    let img = &self.images[id as usize];
9862                    let sw = img.width() as i32;
9863                    let sh = img.height() as i32;
9864                    if sw > 0 && sh > 0 {
9865                        let mut gfx = self.gfx.borrow_mut();
9866                        let (fw, fh) = (gfx.width as i32, gfx.height as i32);
9867                        for py in 0..dh {
9868                            let ty = dy + py;
9869                            if ty < 0 || ty >= fh {
9870                                continue;
9871                            }
9872                            let sy = (py * sh / dh).clamp(0, sh - 1) as u32;
9873                            for px in 0..dw {
9874                                let tx = dx + px;
9875                                if tx < 0 || tx >= fw {
9876                                    continue;
9877                                }
9878                                let sx = (px * sw / dw).clamp(0, sw - 1) as u32;
9879                                let p = img.get_pixel(sx, sy);
9880                                let a = p[3] as u32;
9881                                if a == 0 {
9882                                    continue;
9883                                }
9884                                let idx = ty as usize * gfx.width + tx as usize;
9885                                if a >= 255 {
9886                                    gfx.buffer[idx] =
9887                                        ((p[0] as u32) << 16) | ((p[1] as u32) << 8) | (p[2] as u32);
9888                                } else {
9889                                    let bg = gfx.buffer[idx];
9890                                    let br = (bg >> 16) & 0xff;
9891                                    let bg_g = (bg >> 8) & 0xff;
9892                                    let bb = bg & 0xff;
9893                                    let r = (p[0] as u32 * a + br * (255 - a)) / 255;
9894                                    let g = (p[1] as u32 * a + bg_g * (255 - a)) / 255;
9895                                    let b = (p[2] as u32 * a + bb * (255 - a)) / 255;
9896                                    gfx.buffer[idx] = (r << 16) | (g << 8) | b;
9897                                }
9898                            }
9899                        }
9900                    }
9901                }
9902                return Ok(Value::Unit);
9903            },
9904            #[cfg(target_arch = "wasm32")]
9905            "image_draw" =>
9906            {
9907                return Ok(Value::Unit);
9908            },
9909            // font_text(handle, x, y, px, "string") — anti-aliased *stroked* vector outline
9910            // in the current set_color / set_blend. (x,y) is the text box top-left.
9911            #[cfg(not(target_arch = "wasm32"))]
9912            "font_text" | "ข้อความฟอนต์" | "字体文本" | "フォント文字" | "글꼴텍스트" | "متن_فونت" | "نص_الخط" | "טקסט_גופן" | "فونٹ_متن" | "texte_police" | "schriftart_text" | "текст_шрифт" =>
9913            {
9914                let id = self.arg_num(&args, 0, 0.0)? as i64;
9915                let x = self.arg_num(&args, 1, 0.0)? as f32;
9916                let y = self.arg_num(&args, 2, 0.0)? as f32;
9917                let px = self.arg_num(&args, 3, 16.0)? as f32;
9918                let s = self.arg_str(&args, 4, "");
9919                if id >= 0 && (id as usize) < self.fonts.len() && px > 0.0 {
9920                    let strokes = self.font_layout_2d(id as usize, x, y, px, &s);
9921                    let mut gfx = self.gfx.borrow_mut();
9922                    let (w, h, color, add, aa) =
9923                        (gfx.width, gfx.height, gfx.color, gfx.blend == 1, gfx.font_antialias);
9924                    for pl in &strokes {
9925                        for seg in pl.windows(2) {
9926                            if aa {
9927                                crate::gfx::raster::draw_line_aa(
9928                                    &mut gfx.buffer,
9929                                    w,
9930                                    h,
9931                                    color,
9932                                    add,
9933                                    seg[0][0],
9934                                    seg[0][1],
9935                                    seg[1][0],
9936                                    seg[1][1],
9937                                );
9938                            } else {
9939                                crate::gfx::raster::draw_line(
9940                                    &mut gfx.buffer,
9941                                    w,
9942                                    h,
9943                                    color,
9944                                    seg[0][0],
9945                                    seg[0][1],
9946                                    seg[1][0],
9947                                    seg[1][1],
9948                                );
9949                            }
9950                        }
9951                    }
9952                }
9953                return Ok(Value::Unit);
9954            },
9955            #[cfg(target_arch = "wasm32")]
9956            "font_text" | "ข้อความฟอนต์" | "字体文本" | "フォント文字" | "글꼴텍스트" | "متن_فونت" | "نص_الخط" | "טקסט_גופן" | "فونٹ_متن" | "texte_police" | "schriftart_text" | "текст_шрифт" =>
9957            {
9958                return Ok(Value::Unit);
9959            },
9960            // font_text_fill(handle, x, y, px, "string") — filled vector glyphs;
9961            // anti-aliased when `set_font_antialias(1)` is on (default off = crisp).
9962            #[cfg(not(target_arch = "wasm32"))]
9963            "font_text_fill" | "เติมฟอนต์" | "填充字体" | "フォント塗り" | "글꼴채움" | "پرکردن_متن_فونت" | "تعبئة_نص_الخط" | "מילוי_טקסט_גופן" | "فونٹ_متن_بھرو" | "remplir_texte_police" | "schriftart_text_füllen" | "заполнить_текст_шрифт" =>
9964            {
9965                let id = self.arg_num(&args, 0, 0.0)? as i64;
9966                let x = self.arg_num(&args, 1, 0.0)? as f32;
9967                let y = self.arg_num(&args, 2, 0.0)? as f32;
9968                let px = self.arg_num(&args, 3, 16.0)? as f32;
9969                let s = self.arg_str(&args, 4, "");
9970                if id >= 0 && (id as usize) < self.fonts.len() && px > 0.0 {
9971                    // fill each glyph independently so interior holes (winding) stay correct
9972                    let glyphs = self.font_layout_2d_glyphs(id as usize, x, y, px, &s);
9973                    let mut gfx = self.gfx.borrow_mut();
9974                    let (w, h, color, add, aa) =
9975                        (gfx.width, gfx.height, gfx.color, gfx.blend == 1, gfx.font_antialias);
9976                    for contours in &glyphs {
9977                        if aa {
9978                            crate::gfx::raster::fill_contours_aa(
9979                                &mut gfx.buffer,
9980                                w,
9981                                h,
9982                                color,
9983                                add,
9984                                contours,
9985                            );
9986                        } else {
9987                            crate::gfx::raster::fill_contours(
9988                                &mut gfx.buffer,
9989                                w,
9990                                h,
9991                                color,
9992                                add,
9993                                contours,
9994                            );
9995                        }
9996                    }
9997                }
9998                return Ok(Value::Unit);
9999            },
10000            #[cfg(target_arch = "wasm32")]
10001            "font_text_fill" | "เติมฟอนต์" | "填充字体" | "フォント塗り" | "글꼴채움" | "پرکردن_متن_فونت" | "تعبئة_نص_الخط" | "מילוי_טקסט_גופן" | "فونٹ_متن_بھرو" | "remplir_texte_police" | "schriftart_text_füllen" | "заполнить_текст_шрифт" =>
10002            {
10003                return Ok(Value::Unit);
10004            },
10005            // font_text_3d(handle, cx,cy,cz, ux,uy,uz, vx,vy,vz, size, "string")
10006            // — stroked vector text on a 3D plane: u = advance dir, v = up dir, size = world/em.
10007            //   Flows through the depth-sorted line pipeline, so it rotates with the camera (and 4D).
10008            #[cfg(not(target_arch = "wasm32"))]
10009            "font_text_3d" | "ข้อความฟอนต์3มิติ" | "字体3D" | "フォント3D" | "글꼴3D" | "متن_فونت_سه‌بعدی" | "نص_خط_ثلاثي_الأبعاد" | "טקסט_גופן_תלת_ממדי" | "تھری_ڈی_فونٹ_متن" | "texte_police_3d" | "schriftart_text_3d" | "текст_шрифт_3d" =>
10010            {
10011                let id = self.arg_num(&args, 0, 0.0)? as i64;
10012                let cx = self.arg_num(&args, 1, 0.0)? as f32;
10013                let cy = self.arg_num(&args, 2, 0.0)? as f32;
10014                let cz = self.arg_num(&args, 3, 0.0)? as f32;
10015                let ux = self.arg_num(&args, 4, 1.0)? as f32;
10016                let uy = self.arg_num(&args, 5, 0.0)? as f32;
10017                let uz = self.arg_num(&args, 6, 0.0)? as f32;
10018                let vx = self.arg_num(&args, 7, 0.0)? as f32;
10019                let vy = self.arg_num(&args, 8, 1.0)? as f32;
10020                let vz = self.arg_num(&args, 9, 0.0)? as f32;
10021                let size = self.arg_num(&args, 10, 1.0)? as f32;
10022                let s = self.arg_str(&args, 11, "");
10023                // Optional arg 12: fill_rows — when > 0, each glyph interior is
10024                // filled with that many even-odd scanline spans (true filled
10025                // letterforms, not a bounding box). 0/omitted = outline only.
10026                let fill_rows = self.arg_num(&args, 12, 0.0)? as i32;
10027                if id >= 0 && (id as usize) < self.fonts.len() && size > 0.0 {
10028                    // Build world-space polylines: world = C + (pen+ex)*size*U + ey*size*V
10029                    let font = &mut self.fonts[id as usize];
10030                    let asc = font.ascent();
10031                    let mut pen = 0.0f32;
10032                    let mut lines: Vec<[f32; 6]> = Vec::new();
10033                    for ch in s.chars() {
10034                        let go = font.glyph_outline(ch, 0.01);
10035                        let map = |p: [f32; 2], pen: f32| {
10036                            let a = pen + p[0];
10037                            let b = p[1] - asc; // shift so the top of the cap sits near C
10038                            [
10039                                cx + a * size * ux + b * size * vx,
10040                                cy + a * size * uy + b * size * vy,
10041                                cz + a * size * uz + b * size * vz,
10042                            ]
10043                        };
10044                        for pl in &go.polylines {
10045                            for seg in pl.windows(2) {
10046                                let p0 = map(seg[0], pen);
10047                                let p1 = map(seg[1], pen);
10048                                lines.push([p0[0], p0[1], p0[2], p1[0], p1[1], p1[2]]);
10049                            }
10050                        }
10051                        if fill_rows > 0 {
10052                            // Even-odd scanline fill in glyph space. Contours may
10053                            // omit their closing edge, so the implicit last→first
10054                            // segment is scanned too (skipped when degenerate).
10055                            let (mut ymin, mut ymax) = (f32::MAX, f32::MIN);
10056                            for pl in &go.polylines {
10057                                for p in pl {
10058                                    ymin = ymin.min(p[1]);
10059                                    ymax = ymax.max(p[1]);
10060                                }
10061                            }
10062                            if ymax > ymin {
10063                                for r in 0..fill_rows {
10064                                    let y =
10065                                        ymin + (r as f32 + 0.5) * (ymax - ymin) / fill_rows as f32;
10066                                    let mut xs: Vec<f32> = Vec::new();
10067                                    for pl in &go.polylines {
10068                                        let n = pl.len();
10069                                        if n < 2 {
10070                                            continue;
10071                                        }
10072                                        for k in 0..n {
10073                                            let p0 = pl[k];
10074                                            let p1 = pl[(k + 1) % n];
10075                                            if k + 1 == n
10076                                                && (p1[0] - p0[0]).abs() < 1e-6
10077                                                && (p1[1] - p0[1]).abs() < 1e-6
10078                                            {
10079                                                continue; // contour already closed
10080                                            }
10081                                            let (y0, y1) = (p0[1], p1[1]);
10082                                            if (y0 <= y && y1 > y) || (y1 <= y && y0 > y) {
10083                                                let t = (y - y0) / (y1 - y0);
10084                                                xs.push(p0[0] + t * (p1[0] - p0[0]));
10085                                            }
10086                                        }
10087                                    }
10088                                    xs.sort_by(|a, b| {
10089                                        a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
10090                                    });
10091                                    let mut k = 0;
10092                                    while k + 1 < xs.len() {
10093                                        let a = map([xs[k], y], pen);
10094                                        let b = map([xs[k + 1], y], pen);
10095                                        lines.push([a[0], a[1], a[2], b[0], b[1], b[2]]);
10096                                        k += 2;
10097                                    }
10098                                }
10099                            }
10100                        }
10101                        pen += go.advance;
10102                    }
10103                    let mut gfx = self.gfx.borrow_mut();
10104                    let color = gfx.color;
10105                    let near = -gfx.camera.zdist + 0.05;
10106                    for l in &lines {
10107                        let (mut ax, mut ay, mut az) = (l[0], l[1], l[2]);
10108                        let (mut bx, mut by, mut bz) = (l[3], l[4], l[5]);
10109                        let da = gfx.camera.depth(ax, ay, az);
10110                        let db = gfx.camera.depth(bx, by, bz);
10111                        if da <= near && db <= near {
10112                            continue;
10113                        }
10114                        if da <= near {
10115                            let t = (near - da) / (db - da);
10116                            ax += t * (bx - ax);
10117                            ay += t * (by - ay);
10118                            az += t * (bz - az);
10119                        } else if db <= near {
10120                            let t = (near - da) / (db - da);
10121                            bx = ax + t * (bx - ax);
10122                            by = ay + t * (by - ay);
10123                            bz = az + t * (bz - az);
10124                        }
10125                        let (sax, say, da2) = gfx.camera.project(ax, ay, az);
10126                        let (sbx, sby, db2) = gfx.camera.project(bx, by, bz);
10127                        let depth = (da2 + db2) / 2.0;
10128                        gfx.depth_queue.push_line(depth, color, sax, say, sbx, sby);
10129                    }
10130                }
10131                return Ok(Value::Unit);
10132            },
10133            #[cfg(target_arch = "wasm32")]
10134            "font_text_3d" | "ข้อความฟอนต์3มิติ" | "字体3D" | "フォント3D" | "글꼴3D" | "متن_فونت_سه‌بعدی" | "نص_خط_ثلاثي_الأبعاد" | "טקסט_גופן_תלת_ממדי" | "تھری_ڈی_فونٹ_متن" | "texte_police_3d" | "schriftart_text_3d" | "текст_шрифт_3d" =>
10135            {
10136                return Ok(Value::Unit);
10137            },
10138            // font_width(handle, px, "string") — pixel width of a string in a loaded font.
10139            #[cfg(not(target_arch = "wasm32"))]
10140            "font_width" | "ความกว้างฟอนต์" | "字体宽度" | "フォント幅" | "글꼴너비" | "عرض_فونت" | "عرض_الخط" | "רוחב_גופן" | "فونٹ_چوڑائی" | "largeur_police" | "schriftart_breite" | "ширина_шрифта" =>
10141            {
10142                let id = self.arg_num(&args, 0, 0.0)? as i64;
10143                let px = self.arg_num(&args, 1, 16.0)? as f32;
10144                let s = self.arg_str(&args, 2, "");
10145                if id >= 0 && (id as usize) < self.fonts.len() {
10146                    return Ok(Value::Number(self.fonts[id as usize].measure(&s, px) as f64));
10147                }
10148                return Ok(Value::Number(0.0));
10149            },
10150            #[cfg(target_arch = "wasm32")]
10151            "font_width" | "ความกว้างฟอนต์" | "字体宽度" | "フォント幅" | "글꼴너비" | "عرض_فونت" | "عرض_الخط" | "רוחב_גופן" | "فونٹ_چوڑائی" | "largeur_police" | "schriftart_breite" | "ширина_шрифта" =>
10152            {
10153                return Ok(Value::Number(0.0));
10154            },
10155            // font_glyph_outline(handle, "char", tol_em) — flattened vector outline of
10156            // ONE glyph in normalized em space (x→right, y→up, baseline at 0). Returns a
10157            // list of contours; each contour is a flat list [x0,y0,x1,y1,…]. Curves are
10158            // subdivided so deviation stays under tol_em (default 0.01). Empty on failure.
10159            #[cfg(not(target_arch = "wasm32"))]
10160            "font_glyph_outline" | "font_outline" | "เส้นขอบฟอนต์" | "字体轮廓"
10161            | "フォント輪郭" | "글꼴윤곽" | "خط‌دور_نویسه_فونت" | "حدود_حرف_الخط" | "קו_מתאר_גליף" | "فونٹ_گلف_آؤٹ_لائن" => {
10162                let id = self.arg_num(&args, 0, 0.0)? as i64;
10163                let s = self.arg_str(&args, 1, "");
10164                let tol = self.arg_num(&args, 2, 0.01)? as f32;
10165                let ch = s.chars().next().unwrap_or(' ');
10166                if id >= 0 && (id as usize) < self.fonts.len() {
10167                    let go = self.fonts[id as usize].glyph_outline(ch, tol.max(1e-4));
10168                    let mut contours: Vec<Value> = Vec::with_capacity(go.polylines.len());
10169                    for pl in &go.polylines {
10170                        let mut flat: Vec<Value> = Vec::with_capacity(pl.len() * 2);
10171                        for p in pl {
10172                            flat.push(Value::Number(p[0] as f64));
10173                            flat.push(Value::Number(p[1] as f64));
10174                        }
10175                        contours.push(Value::List(Rc::new(flat)));
10176                    }
10177                    return Ok(Value::List(Rc::new(contours)));
10178                }
10179                return Ok(Value::List(Rc::new(vec![])));
10180            },
10181            #[cfg(target_arch = "wasm32")]
10182            "font_glyph_outline" | "font_outline" | "เส้นขอบฟอนต์" | "字体轮廓"
10183            | "フォント輪郭" | "글꼴윤곽" | "خط‌دور_نویسه_فونت" | "حدود_حرف_الخط" | "קו_מתאר_גליף" | "فونٹ_گلف_آؤٹ_لائن" => {
10184                return Ok(Value::List(Rc::new(vec![])));
10185            },
10186            // font_advance(handle, "char") — normalized em advance width of ONE glyph
10187            // (baseline metric, ignores side bearings). Multiply by px for pixels.
10188            #[cfg(not(target_arch = "wasm32"))]
10189            "font_advance" | "ระยะฟอนต์" | "字体步进" | "フォント送り" | "글꼴전진" | "پیشروی_فونت" | "تقدم_الخط" | "קידום_גופן" | "فونٹ_ایڈوانس" => {
10190                let id = self.arg_num(&args, 0, 0.0)? as i64;
10191                let s = self.arg_str(&args, 1, "");
10192                let ch = s.chars().next().unwrap_or(' ');
10193                if id >= 0 && (id as usize) < self.fonts.len() {
10194                    return Ok(Value::Number(self.fonts[id as usize].advance(ch) as f64));
10195                }
10196                return Ok(Value::Number(0.0));
10197            },
10198            #[cfg(target_arch = "wasm32")]
10199            "font_advance" | "ระยะฟอนต์" | "字体步进" | "フォント送り" | "글꼴전진" | "پیشروی_فونت" | "تقدم_الخط" | "קידום_גופן" | "فونٹ_ایڈوانس" => {
10200                return Ok(Value::Number(0.0));
10201            },
10202
10203            // ui_frame(x,y,w,h, bracketLen) — sci-fi corner brackets
10204            "ui_frame" | "边框" | "フレーム枠" | "프레임틀" | "กรอบ" | "قاب_رابط" | "إطار_الواجهة" | "מסגרת_ממשק" | "یو_آئی_فریم" | "cadre_ui" | "ui_rahmen" | "ui_рамка" => {
10205                let x = self.arg_num(&args, 0, 0.0)? as f32;
10206                let y = self.arg_num(&args, 1, 0.0)? as f32;
10207                let w0 = self.arg_num(&args, 2, 0.0)? as f32;
10208                let h0 = self.arg_num(&args, 3, 0.0)? as f32;
10209                let l = self.arg_num(&args, 4, 14.0)? as f32;
10210                let segs = ling_ui::holo::corner_brackets(x, y, w0, h0, l);
10211                let mut gfx = self.gfx.borrow_mut();
10212                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
10213                for sg in segs {
10214                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
10215                }
10216                return Ok(Value::Unit);
10217            },
10218            // ui_bevel(x,y,w,h, bevel) — beveled holographic panel outline
10219            "ui_bevel" | "斜角框" | "ベベル枠" | "베벨틀" | "กรอบเฉียง" | "لبه_شیبدار" | "حافة_مشطوفة" | "מסגרת_משופעת" | "یو_آئی_بیول" | "biseau_ui" | "ui_fase" | "ui_фаска" =>
10220            {
10221                let x = self.arg_num(&args, 0, 0.0)? as f32;
10222                let y = self.arg_num(&args, 1, 0.0)? as f32;
10223                let w0 = self.arg_num(&args, 2, 0.0)? as f32;
10224                let h0 = self.arg_num(&args, 3, 0.0)? as f32;
10225                let bv = self.arg_num(&args, 4, 10.0)? as f32;
10226                let segs = ling_ui::holo::beveled_rect(x, y, w0, h0, bv);
10227                let mut gfx = self.gfx.borrow_mut();
10228                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
10229                for sg in segs {
10230                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
10231                }
10232                return Ok(Value::Unit);
10233            },
10234
10235            // ══════════════════════════════════════════════════════════════════
10236            // VECTOR UI TOOLKIT  (crates/ling-ui/src/widgets.rs)
10237            // All widgets are vector + theme-coloured with an optional trailing
10238            // r,g,b override; interactive ones read the mouse and return state.
10239            // ══════════════════════════════════════════════════════════════════
10240            #[cfg(not(target_arch = "wasm32"))]
10241            "ui_theme" | "界面主题" | "UIテーマ" | "인터페이스테마" | "ธีมส่วนติดต่อ" | "پوسته_رابط" | "سمة_الواجهة" | "ערכת_נושא" | "یو_آئی_تھیم" | "thème_ui" | "ui_thema" | "ui_тема" =>
10242            {
10243                let cur = self.ui_theme;
10244                let primary = self.color_at(&args, 0, cur.primary);
10245                let accent = self.color_at(&args, 3, cur.accent);
10246                let track = self.color_at(&args, 6, cur.track);
10247                let warn = self.color_at(&args, 9, cur.warn);
10248                let text = self.color_at(&args, 12, cur.text);
10249                let bg = self.color_at(&args, 15, cur.bg);
10250                self.ui_theme = UiTheme { primary, accent, track, warn, text, bg };
10251                return Ok(Value::Unit);
10252            },
10253
10254            // ui_theme_colors() -> [pr,pg,pb, ar,ag,ab, tr,tg,tb, wr,wg,wb,
10255            // xr,xg,xb, br,bg,bb] — the live theme every ui_* widget already
10256            // draws from (primary/accent/track/warn/text/bg, each 0-255),
10257            // so script-drawn UI (e.g. a hand-rolled text field) can match it
10258            // instead of guessing its own colours.
10259            "ui_theme_colors" | "인터페이스테마색상" => {
10260                let th = self.ui_theme;
10261                let mut out = Vec::with_capacity(18);
10262                for c in [th.primary, th.accent, th.track, th.warn, th.text, th.bg] {
10263                    out.push(Value::Number(((c >> 16) & 0xFF) as f64));
10264                    out.push(Value::Number(((c >> 8) & 0xFF) as f64));
10265                    out.push(Value::Number((c & 0xFF) as f64));
10266                }
10267                return Ok(Value::List(Rc::new(out)));
10268            },
10269
10270            // ── HUD ──────────────────────────────────────────────────────────
10271            #[cfg(not(target_arch = "wasm32"))]
10272            "ui_radar" | "雷达" | "レーダー" | "레이더" | "เรดาร์" | "رادار_رابط" | "رادار_الواجهة" | "מכ״ם_ממשק" | "یو_آئی_ریڈار" | "radar_ui" | "ui_радар" => {
10273                let cx = self.arg_num(&args, 0, 0.)? as f32;
10274                let cy = self.arg_num(&args, 1, 0.)? as f32;
10275                let r = self.arg_num(&args, 2, 60.)? as f32;
10276                let sweep = self.arg_num(&args, 3, 0.)? as f32;
10277                let th = self.ui_theme;
10278                let prim = self.color_at(&args, 4, th.primary);
10279                self.draw_ui(&ling_ui::widgets::radar(
10280                    cx, cy, r, sweep, prim, th.accent, th.track,
10281                ));
10282                return Ok(Value::Unit);
10283            },
10284            #[cfg(not(target_arch = "wasm32"))]
10285            "ui_compass" | "罗盘" | "コンパス" | "나침반" | "เข็มทิศ" | "قطب‌نمای_رابط" | "بوصلة_الواجهة" | "מצפן_ממשק" | "یو_آئی_قطب_نما" | "boussole_ui" | "ui_kompass" | "ui_компас" => {
10286                let x = self.arg_num(&args, 0, 0.)? as f32;
10287                let y = self.arg_num(&args, 1, 0.)? as f32;
10288                let w0 = self.arg_num(&args, 2, 300.)? as f32;
10289                let h0 = self.arg_num(&args, 3, 24.)? as f32;
10290                let head = self.arg_num(&args, 4, 0.)? as f32;
10291                let th = self.ui_theme;
10292                let prim = self.color_at(&args, 5, th.primary);
10293                self.draw_ui(&ling_ui::widgets::compass(
10294                    x, y, w0, h0, head, prim, th.track,
10295                ));
10296                return Ok(Value::Unit);
10297            },
10298            #[cfg(not(target_arch = "wasm32"))]
10299            "ui_reticle" | "准星" | "照準" | "조준선" | "เป้าเล็ง" | "نشانه_رابط" | "علامة_تصويب" | "כוונת" | "نشانہ" | "réticule_ui" | "ui_fadenkreuz" | "ui_прицел" => {
10300                let cx = self.arg_num(&args, 0, 0.)? as f32;
10301                let cy = self.arg_num(&args, 1, 0.)? as f32;
10302                let r = self.arg_num(&args, 2, 30.)? as f32;
10303                let spread = self.arg_num(&args, 3, 0.)? as f32;
10304                let th = self.ui_theme;
10305                let prim = self.color_at(&args, 4, th.primary);
10306                self.draw_ui(&ling_ui::widgets::reticle(cx, cy, r, spread, prim));
10307                return Ok(Value::Unit);
10308            },
10309            #[cfg(not(target_arch = "wasm32"))]
10310            "ui_target" | "锁定框" | "ターゲット" | "표적" | "กรอบเป้า" | "قاب_هدف" | "إطار_الهدف" | "מסגרת_מטרה" | "ہدف_فریم" | "cible_ui" | "ui_ziel" | "ui_цель" =>
10311            {
10312                let x = self.arg_num(&args, 0, 0.)? as f32;
10313                let y = self.arg_num(&args, 1, 0.)? as f32;
10314                let w0 = self.arg_num(&args, 2, 80.)? as f32;
10315                let h0 = self.arg_num(&args, 3, 80.)? as f32;
10316                let lock = self.arg_num(&args, 4, 0.)? as f32;
10317                let th = self.ui_theme;
10318                let prim = self.color_at(&args, 5, th.primary);
10319                self.draw_ui(&ling_ui::widgets::target(
10320                    x, y, w0, h0, lock, prim, th.accent,
10321                ));
10322                return Ok(Value::Unit);
10323            },
10324            #[cfg(not(target_arch = "wasm32"))]
10325            "ui_panel" | "面板" | "パネル" | "패널" | "แผง" | "پنل_رابط" | "لوحة_الواجهة" | "לוח_ממשק" | "یو_آئی_پینل" | "panneau_ui" | "ui_feld" | "ui_панель" => {
10326                let x = self.arg_num(&args, 0, 0.)? as f32;
10327                let y = self.arg_num(&args, 1, 0.)? as f32;
10328                let w0 = self.arg_num(&args, 2, 200.)? as f32;
10329                let h0 = self.arg_num(&args, 3, 120.)? as f32;
10330                let bv = self.arg_num(&args, 4, 12.)? as f32;
10331                let th = self.ui_theme;
10332                let prim = self.color_at(&args, 5, th.primary);
10333                self.draw_ui(&ling_ui::widgets::panel(x, y, w0, h0, bv, prim, th.bg));
10334                return Ok(Value::Unit);
10335            },
10336            #[cfg(not(target_arch = "wasm32"))]
10337            "ui_scanlines" | "扫描线" | "走査線" | "스캔라인" | "เส้นสแกน" | "خطوط_اسکن" | "خطوط_المسح" | "קווי_סריקה" | "اسکین_لائنز" | "lignes_balayage_ui" | "ui_abtastzeilen" | "ui_линии_развёртки" =>
10338            {
10339                let x = self.arg_num(&args, 0, 0.)? as f32;
10340                let y = self.arg_num(&args, 1, 0.)? as f32;
10341                let w0 = self.arg_num(&args, 2, 200.)? as f32;
10342                let h0 = self.arg_num(&args, 3, 120.)? as f32;
10343                let dens = self.arg_num(&args, 4, 24.)? as usize;
10344                let th = self.ui_theme;
10345                let line = self.color_at(&args, 5, th.track);
10346                self.draw_ui(&ling_ui::widgets::scanlines(x, y, w0, h0, dens, line));
10347                return Ok(Value::Unit);
10348            },
10349
10350            // ── Meters ───────────────────────────────────────────────────────
10351            #[cfg(not(target_arch = "wasm32"))]
10352            "ui_bar" | "进度条" | "バー" | "막대" | "แถบ" | "نوار_رابط" | "شريط_الواجهة" | "סרגל_ממשק" | "یو_آئی_بار" | "barre_ui" | "ui_leiste" | "ui_полоса" => {
10353                let x = self.arg_num(&args, 0, 0.)? as f32;
10354                let y = self.arg_num(&args, 1, 0.)? as f32;
10355                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10356                let h0 = self.arg_num(&args, 3, 16.)? as f32;
10357                let val = self.arg_num(&args, 4, 0.)? as f32;
10358                let max = self.arg_num(&args, 5, 1.)? as f32;
10359                let th = self.ui_theme;
10360                let fill = self.color_at(&args, 6, th.primary);
10361                self.draw_ui(&ling_ui::widgets::bar(
10362                    x,
10363                    y,
10364                    w0,
10365                    h0,
10366                    val / max.max(1e-6),
10367                    fill,
10368                    th.track,
10369                ));
10370                return Ok(Value::Unit);
10371            },
10372            #[cfg(not(target_arch = "wasm32"))]
10373            "ui_segbar" | "分段条" | "分割バー" | "분할막대" | "แถบแบ่ง" | "نوار_قطعه‌ای" | "شريط_مقسم" | "סרגל_מקוטע" | "سیگمنٹ_بار" | "barre_segmentée_ui" | "ui_segmentleiste" | "ui_сегментная_полоса" =>
10374            {
10375                let x = self.arg_num(&args, 0, 0.)? as f32;
10376                let y = self.arg_num(&args, 1, 0.)? as f32;
10377                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10378                let h0 = self.arg_num(&args, 3, 16.)? as f32;
10379                let val = self.arg_num(&args, 4, 0.)? as f32;
10380                let max = self.arg_num(&args, 5, 1.)? as f32;
10381                let segs = self.arg_num(&args, 6, 10.)? as usize;
10382                let th = self.ui_theme;
10383                let fill = self.color_at(&args, 7, th.primary);
10384                self.draw_ui(&ling_ui::widgets::segbar(
10385                    x,
10386                    y,
10387                    w0,
10388                    h0,
10389                    val / max.max(1e-6),
10390                    segs,
10391                    fill,
10392                    th.track,
10393                ));
10394                return Ok(Value::Unit);
10395            },
10396            #[cfg(not(target_arch = "wasm32"))]
10397            "ui_gauge" | "仪表" | "ゲージ" | "게이지" | "มาตรวัด" | "گیج_رابط" | "مقياس_الواجهة" | "מד_ממשק" | "یو_آئی_گیج" | "jauge_ui" | "ui_anzeige" | "ui_индикатор" => {
10398                let cx = self.arg_num(&args, 0, 0.)? as f32;
10399                let cy = self.arg_num(&args, 1, 0.)? as f32;
10400                let r = self.arg_num(&args, 2, 50.)? as f32;
10401                let val = self.arg_num(&args, 3, 0.)? as f32;
10402                let max = self.arg_num(&args, 4, 1.)? as f32;
10403                let th = self.ui_theme;
10404                let needle = self.color_at(&args, 5, th.warn);
10405                self.draw_ui(&ling_ui::widgets::gauge(
10406                    cx,
10407                    cy,
10408                    r,
10409                    val / max.max(1e-6),
10410                    needle,
10411                    th.accent,
10412                    th.track,
10413                ));
10414                return Ok(Value::Unit);
10415            },
10416            #[cfg(not(target_arch = "wasm32"))]
10417            "ui_ring" | "环表" | "リングメーター" | "링미터" | "วงแหวนวัด" | "حلقه_گیج" | "حلقة_قياس" | "טבעת_מד" | "رنگ_گیج" | "anneau_ui" | "ui_кольцо" =>
10418            {
10419                let cx = self.arg_num(&args, 0, 0.)? as f32;
10420                let cy = self.arg_num(&args, 1, 0.)? as f32;
10421                let r = self.arg_num(&args, 2, 40.)? as f32;
10422                let val = self.arg_num(&args, 3, 0.)? as f32;
10423                let max = self.arg_num(&args, 4, 1.)? as f32;
10424                let th = self.ui_theme;
10425                let fill = self.color_at(&args, 5, th.primary);
10426                self.draw_ui(&ling_ui::widgets::ring(
10427                    cx,
10428                    cy,
10429                    r,
10430                    val / max.max(1e-6),
10431                    fill,
10432                    th.track,
10433                ));
10434                return Ok(Value::Unit);
10435            },
10436            #[cfg(not(target_arch = "wasm32"))]
10437            "ui_vu" | "音量条" | "VUメーター" | "음량막대" | "มาตรเสียง" | "گیج_صدا" | "مقياس_مستوى_الصوت" | "מד_עוצמה" | "وی_یو_میٹر" | "vumètre_ui" | "ui_vumeter" | "ui_вю_метр" =>
10438            {
10439                let x = self.arg_num(&args, 0, 0.)? as f32;
10440                let y = self.arg_num(&args, 1, 0.)? as f32;
10441                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10442                let h0 = self.arg_num(&args, 3, 60.)? as f32;
10443                let levels = self.arg_list_f32(&args, 4);
10444                let th = self.ui_theme;
10445                let fill = self.color_at(&args, 5, th.primary);
10446                self.draw_ui(&ling_ui::widgets::vu(x, y, w0, h0, &levels, fill, th.warn));
10447                return Ok(Value::Unit);
10448            },
10449            #[cfg(not(target_arch = "wasm32"))]
10450            "ui_spark" | "迷你图" | "スパークライン" | "스파크라인" | "กราฟจิ๋ว" | "نمودار_ریز" | "رسم_مصغر" | "גרף_זעיר" | "اسپارک_لائن" | "mini_graphe_ui" | "ui_sparkline" | "ui_мини_график" =>
10451            {
10452                let x = self.arg_num(&args, 0, 0.)? as f32;
10453                let y = self.arg_num(&args, 1, 0.)? as f32;
10454                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10455                let h0 = self.arg_num(&args, 3, 40.)? as f32;
10456                let vals = self.arg_list_f32(&args, 4);
10457                let th = self.ui_theme;
10458                let line = self.color_at(&args, 5, th.accent);
10459                self.draw_ui(&ling_ui::widgets::spark(x, y, w0, h0, &vals, line));
10460                return Ok(Value::Unit);
10461            },
10462            #[cfg(not(target_arch = "wasm32"))]
10463            "ui_battery" | "电池" | "バッテリー" | "배터리" | "แบตเตอรี่" | "نشانگر_باتری" | "مؤشر_البطارية" | "מחוון_סוללה" | "بیٹری_انڈیکیٹر" | "batterie_ui" | "ui_batterie" | "ui_батарея" =>
10464            {
10465                let x = self.arg_num(&args, 0, 0.)? as f32;
10466                let y = self.arg_num(&args, 1, 0.)? as f32;
10467                let w0 = self.arg_num(&args, 2, 50.)? as f32;
10468                let h0 = self.arg_num(&args, 3, 22.)? as f32;
10469                let val = self.arg_num(&args, 4, 1.)? as f32;
10470                let max = self.arg_num(&args, 5, 1.)? as f32;
10471                let th = self.ui_theme;
10472                let fill = self.color_at(&args, 6, th.accent);
10473                self.draw_ui(&ling_ui::widgets::battery(
10474                    x,
10475                    y,
10476                    w0,
10477                    h0,
10478                    val / max.max(1e-6),
10479                    fill,
10480                    th.track,
10481                    th.warn,
10482                ));
10483                return Ok(Value::Unit);
10484            },
10485
10486            // ── Interface controls (interactive → return state) ──────────────
10487            #[cfg(not(target_arch = "wasm32"))]
10488            "ui_button" | "按钮" | "ボタン" | "버튼" | "ปุ่ม" | "دکمه_رابط" | "زر_الواجهة" | "כפתור_ממשק" | "یو_آئی_بٹن" | "bouton_ui" | "ui_knopf" | "ui_кнопка" => {
10489                let x = self.arg_num(&args, 0, 0.)? as f32;
10490                let y = self.arg_num(&args, 1, 0.)? as f32;
10491                let w0 = self.arg_num(&args, 2, 120.)? as f32;
10492                let h0 = self.arg_num(&args, 3, 40.)? as f32;
10493                let (mx, my, down) = self.mouse_now();
10494                let hover = ling_ui::holo::hit_rect(mx, my, x, y, w0, h0);
10495                let clicked = hover && down && !self.mouse_was_down;
10496                let th = self.ui_theme;
10497                let prim = self.color_at(&args, 4, th.primary);
10498                self.draw_ui(&ling_ui::widgets::button(
10499                    x,
10500                    y,
10501                    w0,
10502                    h0,
10503                    hover,
10504                    down && hover,
10505                    prim,
10506                    th.bg,
10507                ));
10508                return Ok(Value::Number(if clicked { 1.0 } else { 0.0 }));
10509            },
10510            #[cfg(not(target_arch = "wasm32"))]
10511            "ui_toggle" | "开关" | "トグル" | "토글" | "สวิตช์" | "کلید_ضامن" | "مفتاح_تبديل" | "מתג" | "ٹوگل" | "bascule_ui" | "ui_schalter" | "ui_переключатель" => {
10512                let x = self.arg_num(&args, 0, 0.)? as f32;
10513                let y = self.arg_num(&args, 1, 0.)? as f32;
10514                let w0 = self.arg_num(&args, 2, 52.)? as f32;
10515                let h0 = self.arg_num(&args, 3, 24.)? as f32;
10516                let mut state = self.arg_num(&args, 4, 0.)? > 0.5;
10517                let (mx, my, down) = self.mouse_now();
10518                let hover = ling_ui::holo::hit_rect(mx, my, x, y, w0, h0);
10519                if hover && down && !self.mouse_was_down {
10520                    state = !state;
10521                }
10522                let th = self.ui_theme;
10523                let on = self.color_at(&args, 5, th.accent);
10524                self.draw_ui(&ling_ui::widgets::toggle(x, y, w0, h0, state, on, th.track));
10525                return Ok(Value::Number(if state { 1.0 } else { 0.0 }));
10526            },
10527            #[cfg(not(target_arch = "wasm32"))]
10528            "ui_slider" | "滑块" | "スライダー" | "슬라이더" | "แถบเลื่อน" | "لغزنده" | "شريط_انزلاق" | "מחוון_החלקה" | "سلائیڈر" | "curseur_ui" | "ui_schieberegler" | "ui_ползунок" =>
10529            {
10530                let x = self.arg_num(&args, 0, 0.)? as f32;
10531                let y = self.arg_num(&args, 1, 0.)? as f32;
10532                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10533                let mut val = self.arg_num(&args, 3, 0.)? as f32;
10534                let mn = self.arg_num(&args, 4, 0.)? as f32;
10535                let mx_ = self.arg_num(&args, 5, 1.)? as f32;
10536                let (mx, my, down) = self.mouse_now();
10537                let hover = ling_ui::holo::hit_rect(mx, my, x - 8.0, y - 10.0, w0 + 16.0, 20.0);
10538                if hover && down {
10539                    let frac = ((mx - x) / w0).clamp(0.0, 1.0);
10540                    val = mn + (mx_ - mn) * frac;
10541                }
10542                let frac = ((val - mn) / (mx_ - mn).abs().max(1e-6)).clamp(0.0, 1.0);
10543                let th = self.ui_theme;
10544                let fill = self.color_at(&args, 6, th.primary);
10545                self.draw_ui(&ling_ui::widgets::slider(
10546                    x, y, w0, frac, hover, fill, th.track,
10547                ));
10548                return Ok(Value::Number(val as f64));
10549            },
10550            #[cfg(not(target_arch = "wasm32"))]
10551            "ui_checkbox" | "复选框" | "チェックボックス" | "체크박스" | "ช่องเลือก" | "جعبه_علامت" | "مربع_اختيار" | "תיבת_סימון" | "چیک_باکس" | "case_cocher_ui" | "ui_kontrollkästchen" | "ui_флажок" =>
10552            {
10553                let x = self.arg_num(&args, 0, 0.)? as f32;
10554                let y = self.arg_num(&args, 1, 0.)? as f32;
10555                let s = self.arg_num(&args, 2, 20.)? as f32;
10556                let mut checked = self.arg_num(&args, 3, 0.)? > 0.5;
10557                let (mx, my, down) = self.mouse_now();
10558                let hover = ling_ui::holo::hit_rect(mx, my, x, y, s, s);
10559                if hover && down && !self.mouse_was_down {
10560                    checked = !checked;
10561                }
10562                let th = self.ui_theme;
10563                let prim = self.color_at(&args, 4, th.primary);
10564                self.draw_ui(&ling_ui::widgets::checkbox(
10565                    x, y, s, checked, hover, prim, th.track,
10566                ));
10567                return Ok(Value::Number(if checked { 1.0 } else { 0.0 }));
10568            },
10569            #[cfg(not(target_arch = "wasm32"))]
10570            "ui_tabs" | "标签页" | "タブ" | "탭" | "แท็บ" | "برگه‌ها" | "ألسنة_الواجهة" | "לשוניות" | "ٹیبز" | "onglets_ui" | "ui_reiter" | "ui_вкладки" => {
10571                let x = self.arg_num(&args, 0, 0.)? as f32;
10572                let y = self.arg_num(&args, 1, 0.)? as f32;
10573                let w0 = self.arg_num(&args, 2, 240.)? as f32;
10574                let h0 = self.arg_num(&args, 3, 28.)? as f32;
10575                let count = self.arg_num(&args, 4, 3.)? as usize;
10576                let mut active = self.arg_num(&args, 5, 0.)? as i32;
10577                let (mx, my, down) = self.mouse_now();
10578                let mut hover = -1;
10579                if my >= y && my <= y + h0 && mx >= x && mx <= x + w0 && count > 0 {
10580                    hover = (((mx - x) / (w0 / count as f32)) as i32)
10581                        .max(0)
10582                        .min(count as i32 - 1);
10583                    if down && !self.mouse_was_down {
10584                        active = hover;
10585                    }
10586                }
10587                let th = self.ui_theme;
10588                let prim = self.color_at(&args, 6, th.primary);
10589                self.draw_ui(&ling_ui::widgets::tabs(
10590                    x,
10591                    y,
10592                    w0,
10593                    h0,
10594                    count,
10595                    active as usize,
10596                    hover,
10597                    prim,
10598                    th.track,
10599                ));
10600                return Ok(Value::Number(active as f64));
10601            },
10602            #[cfg(not(target_arch = "wasm32"))]
10603            "ui_progress" | "进度" | "プログレス" | "진행바" | "ความคืบหน้า" | "نوار_پیشرفت" | "شريط_التقدم" | "פס_התקדמות" | "پیش_رفت_بار" | "progression_ui" | "ui_fortschritt" | "ui_прогресс" =>
10604            {
10605                let x = self.arg_num(&args, 0, 0.)? as f32;
10606                let y = self.arg_num(&args, 1, 0.)? as f32;
10607                let w0 = self.arg_num(&args, 2, 200.)? as f32;
10608                let h0 = self.arg_num(&args, 3, 12.)? as f32;
10609                let frac = self.arg_num(&args, 4, 0.)? as f32;
10610                let th = self.ui_theme;
10611                let fill = self.color_at(&args, 5, th.accent);
10612                self.draw_ui(&ling_ui::widgets::progress(
10613                    x, y, w0, h0, frac, fill, th.track,
10614                ));
10615                return Ok(Value::Unit);
10616            },
10617            #[cfg(not(target_arch = "wasm32"))]
10618            "ui_tooltip" | "提示框" | "ツールチップ" | "툴팁" | "คำแนะนำ" | "راهنمای_شناور" | "تلميح_الواجهة" | "חלונית_עזרה" | "ٹول_ٹپ" | "infobulle_ui" | "ui_подсказка" =>
10619            {
10620                let x = self.arg_num(&args, 0, 0.)? as f32;
10621                let y = self.arg_num(&args, 1, 0.)? as f32;
10622                let w0 = self.arg_num(&args, 2, 120.)? as f32;
10623                let h0 = self.arg_num(&args, 3, 28.)? as f32;
10624                let th = self.ui_theme;
10625                let prim = self.color_at(&args, 4, th.primary);
10626                self.draw_ui(&ling_ui::widgets::tooltip(x, y, w0, h0, prim, th.bg));
10627                return Ok(Value::Unit);
10628            },
10629            #[cfg(not(target_arch = "wasm32"))]
10630            "ui_stepper" | "步进器" | "ステッパー" | "스테퍼" | "ตัวปรับค่า" | "پله‌گر" | "زر_خطوات" | "בורר_מדורג" | "اسٹیپر" | "pas_à_pas_ui" | "ui_schrittsteuerung" | "ui_степпер" =>
10631            {
10632                let x = self.arg_num(&args, 0, 0.)? as f32;
10633                let y = self.arg_num(&args, 1, 0.)? as f32;
10634                let w0 = self.arg_num(&args, 2, 120.)? as f32;
10635                let h0 = self.arg_num(&args, 3, 28.)? as f32;
10636                let mut val = self.arg_num(&args, 4, 0.)? as f32;
10637                let step = self.arg_num(&args, 5, 1.)? as f32;
10638                let (mx, my, down) = self.mouse_now();
10639                let hm = ling_ui::holo::hit_rect(mx, my, x, y, h0, h0);
10640                let hp = ling_ui::holo::hit_rect(mx, my, x + w0 - h0, y, h0, h0);
10641                if down && !self.mouse_was_down {
10642                    if hm {
10643                        val -= step;
10644                    }
10645                    if hp {
10646                        val += step;
10647                    }
10648                }
10649                let th = self.ui_theme;
10650                let prim = self.color_at(&args, 6, th.primary);
10651                self.draw_ui(&ling_ui::widgets::stepper(
10652                    x, y, w0, h0, hm, hp, prim, th.track,
10653                ));
10654                return Ok(Value::Number(val as f64));
10655            },
10656
10657            // ── Game UI ──────────────────────────────────────────────────────
10658            #[cfg(not(target_arch = "wasm32"))]
10659            "ui_healthbar" | "血条" | "体力バー" | "체력바" | "แถบพลังชีวิต" | "نوار_سلامتی" | "شريط_الصحة" | "פס_בריאות" | "ہیلتھ_بار" | "barre_vie_ui" | "ui_lebensbalken" | "ui_полоса_здоровья" =>
10660            {
10661                let x = self.arg_num(&args, 0, 0.)? as f32;
10662                let y = self.arg_num(&args, 1, 0.)? as f32;
10663                let w0 = self.arg_num(&args, 2, 180.)? as f32;
10664                let h0 = self.arg_num(&args, 3, 16.)? as f32;
10665                let val = self.arg_num(&args, 4, 1.)? as f32;
10666                let max = self.arg_num(&args, 5, 1.)? as f32;
10667                let pulse = self.arg_num(&args, 6, 0.)? as f32;
10668                let th = self.ui_theme;
10669                let full = self.color_at(&args, 7, th.accent);
10670                self.draw_ui(&ling_ui::widgets::healthbar(
10671                    x,
10672                    y,
10673                    w0,
10674                    h0,
10675                    val / max.max(1e-6),
10676                    pulse,
10677                    full,
10678                    th.warn,
10679                    th.track,
10680                ));
10681                return Ok(Value::Unit);
10682            },
10683            #[cfg(not(target_arch = "wasm32"))]
10684            "ui_cooldown" | "冷却" | "クールダウン" | "쿨다운" | "คูลดาวน์" | "زمان_خنک‌سازی" | "مؤقت_التهدئة" | "זמן_קירור" | "کول_ڈاؤن" | "recharge_ui" | "ui_abklingzeit" | "ui_перезарядка" =>
10685            {
10686                let cx = self.arg_num(&args, 0, 0.)? as f32;
10687                let cy = self.arg_num(&args, 1, 0.)? as f32;
10688                let r = self.arg_num(&args, 2, 28.)? as f32;
10689                let frac = self.arg_num(&args, 3, 0.)? as f32;
10690                let th = self.ui_theme;
10691                let fill = self.color_at(&args, 4, th.primary);
10692                self.draw_ui(&ling_ui::widgets::cooldown(cx, cy, r, frac, fill, th.track));
10693                return Ok(Value::Unit);
10694            },
10695            #[cfg(not(target_arch = "wasm32"))]
10696            "ui_counter" | "计数器" | "カウンター" | "카운터" | "ตัวนับ" | "شمارشگر" | "عداد_الواجهة" | "מונה_ממשק" | "کاؤنٹر" | "compteur_ui" | "ui_zähler" | "ui_счётчик" => {
10697                let x = self.arg_num(&args, 0, 0.)? as f32;
10698                let y = self.arg_num(&args, 1, 0.)? as f32;
10699                let dw = self.arg_num(&args, 2, 14.)? as f32;
10700                let dh = self.arg_num(&args, 3, 24.)? as f32;
10701                let val = self.arg_num(&args, 4, 0.)? as i64;
10702                let digits = self.arg_num(&args, 5, 4.)? as usize;
10703                let th = self.ui_theme;
10704                let on = self.color_at(&args, 6, th.primary);
10705                let off = ling_ui::widgets::shade(th.track, 0.5);
10706                self.draw_ui(&ling_ui::widgets::counter(
10707                    x, y, dw, dh, val, digits, on, off,
10708                ));
10709                return Ok(Value::Unit);
10710            },
10711            #[cfg(not(target_arch = "wasm32"))]
10712            "ui_minimap" | "小地图" | "ミニマップ" | "미니맵" | "แผนที่ย่อ" | "نقشه_کوچک" | "خريطة_مصغرة" | "מפה_מוקטנת" | "منی_میپ" | "minicarte_ui" | "ui_minikarte" | "ui_миникарта" =>
10713            {
10714                let x = self.arg_num(&args, 0, 0.)? as f32;
10715                let y = self.arg_num(&args, 1, 0.)? as f32;
10716                let w0 = self.arg_num(&args, 2, 140.)? as f32;
10717                let h0 = self.arg_num(&args, 3, 140.)? as f32;
10718                let th = self.ui_theme;
10719                let prim = self.color_at(&args, 4, th.primary);
10720                self.draw_ui(&ling_ui::widgets::minimap(x, y, w0, h0, prim, th.bg));
10721                return Ok(Value::Unit);
10722            },
10723            #[cfg(not(target_arch = "wasm32"))]
10724            "ui_dpad" | "方向键" | "方向パッド" | "방향패드" | "ปุ่มทิศทาง" | "دسته_جهت‌دار" | "لوحة_الاتجاهات" | "לוח_כיוונים" | "ڈی_پیڈ" | "croix_direction_ui" | "ui_steuerkreuz" | "ui_крестовина" =>
10725            {
10726                let cx = self.arg_num(&args, 0, 0.)? as f32;
10727                let cy = self.arg_num(&args, 1, 0.)? as f32;
10728                let r = self.arg_num(&args, 2, 50.)? as f32;
10729                let (mx, my, down) = self.mouse_now();
10730                let mut dir = 0;
10731                if down {
10732                    let (dx, dy) = (mx - cx, my - cy);
10733                    if dx * dx + dy * dy <= r * r {
10734                        if dx.abs() > dy.abs() {
10735                            dir = if dx > 0.0 { 2 } else { 4 };
10736                        } else {
10737                            dir = if dy > 0.0 { 3 } else { 1 };
10738                        }
10739                    }
10740                }
10741                let th = self.ui_theme;
10742                let prim = self.color_at(&args, 3, th.primary);
10743                self.draw_ui(&ling_ui::widgets::dpad(cx, cy, r, dir, prim, th.track));
10744                return Ok(Value::Number(dir as f64));
10745            },
10746            #[cfg(not(target_arch = "wasm32"))]
10747            "ui_slotgrid" | "物品格" | "スロットグリッド" | "슬롯격자" | "ช่องไอเทม" | "شبکه_شیار" | "شبكة_الفتحات" | "רשת_חריצים" | "سلاٹ_گرڈ" | "grille_emplacements_ui" | "ui_slotraster" | "ui_сетка_слотов" =>
10748            {
10749                let x = self.arg_num(&args, 0, 0.)? as f32;
10750                let y = self.arg_num(&args, 1, 0.)? as f32;
10751                let cols = self.arg_num(&args, 2, 4.)? as usize;
10752                let rows = self.arg_num(&args, 3, 1.)? as usize;
10753                let cell = self.arg_num(&args, 4, 36.)? as f32;
10754                let sel = self.arg_num(&args, 5, -1.)? as i32;
10755                let th = self.ui_theme;
10756                let prim = self.color_at(&args, 6, th.primary);
10757                self.draw_ui(&ling_ui::widgets::slotgrid(
10758                    x, y, cols, rows, cell, sel, prim, th.track,
10759                ));
10760                return Ok(Value::Unit);
10761            },
10762            #[cfg(not(target_arch = "wasm32"))]
10763            "ui_vignette" | "暗角" | "ビネット" | "비네트" | "ขอบมืด" | "سایه‌گرد_کادر" | "تظليل_الحواف" | "הצללת_מסגרת" | "ویگنیٹ" | "vignette_ui" | "ui_виньетка" => {
10764                let intensity = self.arg_num(&args, 0, 0.5)? as f32;
10765                let (w, h) = {
10766                    let g = self.gfx.borrow();
10767                    (g.width as f32, g.height as f32)
10768                };
10769                let th = self.ui_theme;
10770                let col = self.color_at(&args, 1, th.warn);
10771                self.draw_ui(&ling_ui::widgets::vignette(w, h, intensity, col));
10772                return Ok(Value::Unit);
10773            },
10774
10775            // ── Faux-3D in 2D space ──────────────────────────────────────────
10776            #[cfg(not(target_arch = "wasm32"))]
10777            "ui_gauge3d" | "立体仪表" | "立体ゲージ" | "입체게이지" | "มาตรวัด3มิติ" | "گیج_سه‌بعدی" | "مقياس_ثلاثي_الأبعاد" | "מד_תלת_ממדי" | "تھری_ڈی_گیج" | "jauge_3d_ui" | "ui_anzeige_3d" | "ui_индикатор_3d" =>
10778            {
10779                let cx = self.arg_num(&args, 0, 0.)? as f32;
10780                let cy = self.arg_num(&args, 1, 0.)? as f32;
10781                let r = self.arg_num(&args, 2, 50.)? as f32;
10782                let val = self.arg_num(&args, 3, 0.)? as f32;
10783                let max = self.arg_num(&args, 4, 1.)? as f32;
10784                let spin = self.arg_num(&args, 5, 0.)? as f32;
10785                let th = self.ui_theme;
10786                let fill = self.color_at(&args, 6, th.primary);
10787                self.draw_ui(&ling_ui::widgets::gauge3d(
10788                    cx,
10789                    cy,
10790                    r,
10791                    val / max.max(1e-6),
10792                    spin,
10793                    fill,
10794                    th.track,
10795                ));
10796                return Ok(Value::Unit);
10797            },
10798            #[cfg(not(target_arch = "wasm32"))]
10799            "ui_panel3d" | "立体面板" | "立体パネル" | "입체패널" | "แผง3มิติ" | "پنل_سه‌بعدی" | "لوحة_ثلاثية_الأبعاد" | "לוח_תלת_ממדי" | "تھری_ڈی_پینل" | "panneau_3d_ui" | "ui_feld_3d" | "ui_панель_3d" =>
10800            {
10801                let x = self.arg_num(&args, 0, 0.)? as f32;
10802                let y = self.arg_num(&args, 1, 0.)? as f32;
10803                let w0 = self.arg_num(&args, 2, 200.)? as f32;
10804                let h0 = self.arg_num(&args, 3, 120.)? as f32;
10805                let depth = self.arg_num(&args, 4, 14.)? as f32;
10806                let th = self.ui_theme;
10807                let prim = self.color_at(&args, 5, th.primary);
10808                self.draw_ui(&ling_ui::widgets::panel3d(x, y, w0, h0, depth, prim, th.bg));
10809                return Ok(Value::Unit);
10810            },
10811            #[cfg(not(target_arch = "wasm32"))]
10812            "ui_radar3d" | "立体雷达" | "立体レーダー" | "입체레이더" | "เรดาร์3มิติ" | "رادار_سه‌بعدی" | "رادار_ثلاثي_الأبعاد" | "מכ״ם_תלת_ממדי" | "تھری_ڈی_ریڈار" | "radar_3d_ui" | "ui_radar_3d" | "ui_радар_3d" =>
10813            {
10814                let cx = self.arg_num(&args, 0, 0.)? as f32;
10815                let cy = self.arg_num(&args, 1, 0.)? as f32;
10816                let r = self.arg_num(&args, 2, 60.)? as f32;
10817                let tilt = self.arg_num(&args, 3, 0.9)? as f32;
10818                let sweep = self.arg_num(&args, 4, 0.)? as f32;
10819                let th = self.ui_theme;
10820                let prim = self.color_at(&args, 5, th.primary);
10821                self.draw_ui(&ling_ui::widgets::radar3d(
10822                    cx, cy, r, tilt, sweep, prim, th.track,
10823                ));
10824                return Ok(Value::Unit);
10825            },
10826
10827            // ── Interface sounds ─────────────────────────────────────────────
10828            #[cfg(not(target_arch = "wasm32"))]
10829            "audio_blip" | "提示音" | "ビープ音" | "효과음" | "เสียงบี๊บ" | "بوق_کوتاه" | "نغمة_قصيرة" | "ביפ" | "بلپ_آواز" | "bip_audio" | "звук_бип" =>
10830            {
10831                let freq = self.arg_num(&args, 0, 660.)? as f32;
10832                let dur = self.arg_num(&args, 1, 0.08)? as f32;
10833                let wave = Wave::from_name(&self.arg_str(&args, 2, "sine"));
10834                let amp = self.arg_num(&args, 3, 0.25)? as f32;
10835                if let Some(audio) = &self.audio {
10836                    audio.blip(freq, amp, dur, wave);
10837                }
10838                return Ok(Value::Unit);
10839            },
10840            #[cfg(not(target_arch = "wasm32"))]
10841            "ui_sound" | "界面音" | "UI音" | "인터페이스음" | "เสียงปุ่ม" | "صدای_رابط" | "صوت_الواجهة" | "צליל_ממשק" | "یو_آئی_آواز" | "son_ui" | "ui_klang" | "ui_звук" =>
10842            {
10843                let name = self.arg_str(&args, 0, "click");
10844                if let Some(audio) = &self.audio {
10845                    match name.as_str() {
10846                        "hover" => audio.blip(880.0, 0.10, 0.04, Wave::Sine),
10847                        "confirm" => {
10848                            audio.blip(660.0, 0.22, 0.07, Wave::Square);
10849                            audio.blip(990.0, 0.18, 0.10, Wave::Square);
10850                        },
10851                        "error" => {
10852                            audio.blip(180.0, 0.30, 0.16, Wave::Saw);
10853                            audio.blip(140.0, 0.30, 0.18, Wave::Saw);
10854                        },
10855                        "toggle" => audio.blip(520.0, 0.22, 0.05, Wave::Triangle),
10856                        "tick" => audio.blip(1500.0, 0.12, 0.02, Wave::Square),
10857                        _ => audio.blip(720.0, 0.26, 0.05, Wave::Square), // "click"
10858                    }
10859                }
10860                return Ok(Value::Unit);
10861            },
10862
10863            // ══════════════════════════════════════════════════════════════════
10864            // MUSIC TOOLKIT  (crates/ling-music) — decode · analysis · GM synth ·
10865            // rhythm · karaoke. Analysis/decoding need no audio device; playback
10866            // and synthesis lazily start a dedicated music engine.
10867            // ══════════════════════════════════════════════════════════════════
10868
10869            // music_load(path) -> track handle (decodes WAV/FLAC/OGG/MP3/AAC)
10870            #[cfg(not(target_arch = "wasm32"))]
10871            "music_load" | "载入音乐" | "音楽読込" | "음악로드" | "โหลดเพลง" | "بارگذاری_موسیقی" | "تحميل_الموسيقى" | "טעינת_מוזיקה" | "موسیقی_لوڈ" | "charger_musique" | "musik_laden" | "загрузить_музыку" =>
10872            {
10873                let path = self.arg_str(&args, 0, "");
10874                let resolved = if std::path::Path::new(&path).exists() {
10875                    path.clone()
10876                } else if let Some(d) = &self.source_dir {
10877                    d.join(&path).to_string_lossy().into_owned()
10878                } else {
10879                    path.clone()
10880                };
10881                match ling_music::load(&resolved) {
10882                    Ok(t) => {
10883                        let id = self.tracks.len();
10884                        self.tracks.push(t);
10885                        return Ok(Value::Number(id as f64));
10886                    },
10887                    Err(e) => {
10888                        eprintln!("music_load failed ({path}): {e}");
10889                        return Ok(Value::Number(-1.0));
10890                    },
10891                }
10892            },
10893            #[cfg(not(target_arch = "wasm32"))]
10894            "music_duration" | "音乐时长" | "音楽長さ" | "음악길이" | "ความยาวเพลง" | "مدت_موسیقی" | "مدة_الموسيقى" | "משך_מוזיקה" | "موسیقی_دورانیہ" | "durée_musique" | "musik_dauer" | "длительность_музыки" =>
10895            {
10896                let id = self.arg_num(&args, 0, 0.0)? as i64;
10897                let d = self
10898                    .tracks
10899                    .get(id as usize)
10900                    .map(|t| t.duration)
10901                    .unwrap_or(0.0);
10902                return Ok(Value::Number(d as f64));
10903            },
10904            #[cfg(not(target_arch = "wasm32"))]
10905            "music_bpm" | "节拍速度" | "テンポ" | "템포" | "จังหวะต่อนาที" | "ضربان_در_دقیقه" | "نبضات_بالدقيقة" | "פעימות_לדקה" | "بی_پی_ایم" | "bpm_musique" | "musik_bpm" | "музыка_bpm" =>
10906            {
10907                let id = self.arg_num(&args, 0, 0.0)? as i64;
10908                let b = self
10909                    .tracks
10910                    .get(id as usize)
10911                    .map(|t| ling_music::analysis::bpm(&t.mono, t.rate))
10912                    .unwrap_or(0.0);
10913                return Ok(Value::Number(b as f64));
10914            },
10915            #[cfg(not(target_arch = "wasm32"))]
10916            "music_key" | "调性" | "調性" | "조성" | "คีย์เพลง" | "گام_موسیقی" | "مقام_الموسيقى" | "סולם_מוזיקלי" | "موسیقی_کلید" | "tonalité_musique" | "musik_tonart" | "тональность_музыки" => {
10917                let id = self.arg_num(&args, 0, 0.0)? as i64;
10918                let k = self
10919                    .tracks
10920                    .get(id as usize)
10921                    .map(|t| ling_music::analysis::key_name(&t.mono, t.rate))
10922                    .unwrap_or_default();
10923                return Ok(Value::Str(k));
10924            },
10925            #[cfg(not(target_arch = "wasm32"))]
10926            "music_onsets" | "音符起点" | "オンセット" | "온셋" | "จุดเริ่มเสียง" | "آغازهای_نت" | "بدايات_النغمات" | "התחלות_תווים" | "نوٹ_شروعات" | "attaques_musique" | "musik_einsätze" | "атаки_музыки" =>
10927            {
10928                let id = self.arg_num(&args, 0, 0.0)? as i64;
10929                let v = self
10930                    .tracks
10931                    .get(id as usize)
10932                    .map(|t| ling_music::analysis::onsets(&t.mono, t.rate))
10933                    .unwrap_or_default();
10934                return Ok(Value::List(Rc::new(
10935                    v.into_iter().map(|x| Value::Number(x as f64)).collect(),
10936                )));
10937            },
10938            #[cfg(not(target_arch = "wasm32"))]
10939            "music_beat_grid" | "节拍网格" | "ビートグリッド" | "비트그리드" | "กริดจังหวะ" | "شبکه_ضرب" | "شبكة_الإيقاع" | "רשת_פעימות" | "بیٹ_گرڈ" | "grille_temps_musique" | "musik_taktraster" | "сетка_ритма_музыки" =>
10940            {
10941                let id = self.arg_num(&args, 0, 0.0)? as i64;
10942                let beats = self
10943                    .tracks
10944                    .get(id as usize)
10945                    .map(|t| {
10946                        let b = ling_music::analysis::bpm(&t.mono, t.rate);
10947                        ling_music::analysis::beat_grid(&t.mono, t.rate, b)
10948                    })
10949                    .unwrap_or_default();
10950                return Ok(Value::List(Rc::new(
10951                    beats.into_iter().map(|x| Value::Number(x as f64)).collect(),
10952                )));
10953            },
10954
10955            // ── playback ──
10956            #[cfg(not(target_arch = "wasm32"))]
10957            "music_play" | "播放音乐" | "音楽再生" | "음악재생" | "เล่นเพลง" | "پخش_موسیقی" | "شغّل_الموسيقى" | "נגן_מוזיקה" | "موسیقی_چلاؤ" | "jouer_musique" | "musik_abspielen" | "играть_музыку" =>
10958            {
10959                let id = self.arg_num(&args, 0, 0.0)? as i64;
10960                if self.ensure_music() {
10961                    let track = self
10962                        .tracks
10963                        .get(id as usize)
10964                        .map(|t| (t.stereo.clone(), t.rate));
10965                    if let (Some((st, rate)), Some(m)) = (track, &self.music) {
10966                        m.set_track(st, rate);
10967                        m.play();
10968                    } else if let Some(m) = &self.music {
10969                        m.play();
10970                    }
10971                }
10972                return Ok(Value::Unit);
10973            },
10974            #[cfg(not(target_arch = "wasm32"))]
10975            "music_pause" | "暂停音乐" | "音楽一時停止" | "음악일시정지" | "หยุดเพลงชั่วคราว" | "مکث_موسیقی" | "ألبث_الموسيقى" | "השהה_מוזיקה" | "موسیقی_روکو_مؤقت" | "pause_musique" | "musik_pausieren" | "пауза_музыки" =>
10976            {
10977                if let Some(m) = &self.music {
10978                    m.pause();
10979                }
10980                return Ok(Value::Unit);
10981            },
10982            #[cfg(not(target_arch = "wasm32"))]
10983            "music_stop" | "停止音乐" | "音楽停止" | "음악정지" | "หยุดเพลง" | "توقف_موسیقی" | "أوقف_الموسيقى" | "עצור_מוזיקה" | "موسیقی_روکو" | "arrêter_musique" | "musik_stoppen" | "остановить_музыку" =>
10984            {
10985                if let Some(m) = &self.music {
10986                    m.stop();
10987                }
10988                return Ok(Value::Unit);
10989            },
10990            #[cfg(not(target_arch = "wasm32"))]
10991            "music_seek" | "定位音乐" | "音楽シーク" | "음악탐색" | "ค้นหาเพลง" | "جستجوی_موسیقی" | "ابحث_في_الموسيقى" | "חפש_במוזיקה" | "موسیقی_تلاش" | "chercher_musique" | "musik_suchen" | "перемотать_музыку" =>
10992            {
10993                let sec = self.arg_num(&args, 0, 0.0)? as f32;
10994                if let Some(m) = &self.music {
10995                    m.seek(sec);
10996                }
10997                return Ok(Value::Unit);
10998            },
10999            #[cfg(not(target_arch = "wasm32"))]
11000            "music_pos" | "音乐位置" | "音楽位置" | "음악위치" | "ตำแหน่งเพลง" | "موقعیت_موسیقی" | "موضع_الموسيقى" | "מיקום_מוזיקה" | "موسیقی_مقام" | "position_musique" | "musik_position" | "позиция_музыки" =>
11001            {
11002                let p = self.music.as_ref().map(|m| m.position()).unwrap_or(0.0);
11003                return Ok(Value::Number(p as f64));
11004            },
11005            #[cfg(not(target_arch = "wasm32"))]
11006            "music_volume" | "音乐音量" | "音楽音量" | "음악음량" | "ระดับเพลง" | "بلندی_موسیقی" | "مستوى_الموسيقى" | "עוצמת_מוזיקה" | "موسیقی_شدت" | "volume_musique" | "musik_lautstärke" | "громкость_музыки" =>
11007            {
11008                let v = self.arg_num(&args, 0, 0.8)? as f32;
11009                if self.ensure_music() {
11010                    if let Some(m) = &self.music {
11011                        m.set_volume(v);
11012                    }
11013                }
11014                return Ok(Value::Unit);
11015            },
11016
11017            // ── synthesis (GM-capable, patches from .ling files) ──
11018            #[cfg(not(target_arch = "wasm32"))]
11019            "music_patch" | "乐器音色" | "音色読込" | "악기패치" | "แพตช์เครื่องดนตรี" | "پچ_موسیقی" | "آلة_الموسيقى" | "תיקון_כלי_נגינה" | "میوزک_پیچ" | "patch_musique" | "musik_patch" | "патч_музыки" =>
11020            {
11021                let path = self.arg_str(&args, 0, "");
11022                let resolved = if std::path::Path::new(&path).exists() {
11023                    path.clone()
11024                } else if let Some(d) = &self.source_dir {
11025                    d.join(&path).to_string_lossy().into_owned()
11026                } else {
11027                    path.clone()
11028                };
11029                if !self.ensure_music() {
11030                    return Ok(Value::Number(-1.0));
11031                }
11032                match ling_music::patch::from_path(&resolved) {
11033                    Ok(p) => {
11034                        let id = self.music.as_ref().unwrap().add_patch(p);
11035                        return Ok(Value::Number(id as f64));
11036                    },
11037                    Err(e) => {
11038                        eprintln!("music_patch failed ({path}): {e}");
11039                        return Ok(Value::Number(-1.0));
11040                    },
11041                }
11042            },
11043            #[cfg(not(target_arch = "wasm32"))]
11044            "music_note" | "弹音符" | "音符演奏" | "음표연주" | "เล่นโน้ต" | "نواختن_نت" | "عزف_نغمة" | "נגן_תו" | "نوٹ_بجاؤ" | "note_musique" | "musik_note" | "нота_музыки" =>
11045            {
11046                let inst = self.arg_num(&args, 0, 0.0)? as usize;
11047                let midi = self.pitch_arg(&args, 1, 60);
11048                let dur = self.arg_num(&args, 2, 0.5)? as f32;
11049                let vel = self.arg_num(&args, 3, 0.9)? as f32;
11050                if self.ensure_music() {
11051                    if let Some(m) = &self.music {
11052                        m.note(inst, midi, vel, dur);
11053                    }
11054                }
11055                return Ok(Value::Unit);
11056            },
11057            #[cfg(not(target_arch = "wasm32"))]
11058            "music_note_on" | "音符开始" | "音符オン" | "음표켜기" | "โน้ตเริ่ม" | "شروع_نت" | "بدء_النغمة" | "התחלת_תו" | "نوٹ_شروع" | "note_musique_on" | "musik_note_an" | "нота_музыки_вкл" =>
11059            {
11060                let inst = self.arg_num(&args, 0, 0.0)? as usize;
11061                let midi = self.pitch_arg(&args, 1, 60);
11062                let vel = self.arg_num(&args, 2, 0.9)? as f32;
11063                if self.ensure_music() {
11064                    if let Some(m) = &self.music {
11065                        m.note_on(inst, midi, vel);
11066                    }
11067                }
11068                return Ok(Value::Unit);
11069            },
11070            #[cfg(not(target_arch = "wasm32"))]
11071            "music_note_off" | "音符结束" | "音符オフ" | "음표끄기" | "โน้ตจบ" | "پایان_نت" | "إيقاف_النغمة" | "סיום_תו" | "نوٹ_ختم" | "note_musique_off" | "musik_note_aus" | "нота_музыки_выкл" =>
11072            {
11073                let inst = self.arg_num(&args, 0, 0.0)? as usize;
11074                let midi = self.pitch_arg(&args, 1, 60);
11075                if let Some(m) = &self.music {
11076                    m.note_off(inst, midi);
11077                }
11078                return Ok(Value::Unit);
11079            },
11080
11081            // ── rhythm-game judging ──
11082            #[cfg(not(target_arch = "wasm32"))]
11083            "music_judge" | "判定" | "判定する" | "판정" | "ตัดสินจังหวะ" | "داوری_ضرب" | "حكم_الإيقاع" | "שיפוט_קצב" | "بیٹ_فیصلہ" | "juger_musique" | "musik_bewerten" | "оценить_музыку" =>
11084            {
11085                let delta_ms = self.arg_num(&args, 0, 9999.0)? as f32;
11086                return Ok(Value::Number(
11087                    ling_music::Grade::judge(delta_ms).index() as f64
11088                ));
11089            },
11090            #[cfg(not(target_arch = "wasm32"))]
11091            "music_grade_name" | "判定名" | "判定名称" | "판정이름" | "ชื่อการตัดสิน" | "نام_رتبه" | "اسم_التقييم" | "שם_דירוג" | "گریڈ_نام" | "nom_grade_musique" | "musik_bewertungsname" | "имя_оценки_музыки" =>
11092            {
11093                let idx = self.arg_num(&args, 0, 4.0)? as i32;
11094                return Ok(Value::Str(
11095                    ling_music::Grade::from_index(idx).name().to_string(),
11096                ));
11097            },
11098
11099            // ── karaoke ──
11100            #[cfg(not(target_arch = "wasm32"))]
11101            "music_lrc" | "载入歌词" | "歌詞読込" | "가사로드" | "โหลดเนื้อเพลง" | "بارگذاری_متن_ترانه" | "تحميل_كلمات_الأغنية" | "טעינת_מילות_שיר" | "گیت_متن_لوڈ" | "lrc_musique" | "musik_lrc" | "lrc_музыки" =>
11102            {
11103                let path = self.arg_str(&args, 0, "");
11104                let resolved = if std::path::Path::new(&path).exists() {
11105                    path.clone()
11106                } else if let Some(d) = &self.source_dir {
11107                    d.join(&path).to_string_lossy().into_owned()
11108                } else {
11109                    path.clone()
11110                };
11111                match std::fs::read_to_string(&resolved) {
11112                    Ok(text) => {
11113                        let id = self.lyrics.len();
11114                        self.lyrics.push(ling_music::Lyrics::parse(&text));
11115                        return Ok(Value::Number(id as f64));
11116                    },
11117                    Err(e) => {
11118                        eprintln!("music_lrc failed ({path}): {e}");
11119                        return Ok(Value::Number(-1.0));
11120                    },
11121                }
11122            },
11123            #[cfg(not(target_arch = "wasm32"))]
11124            "music_lyric" | "当前歌词" | "現在歌詞" | "현재가사" | "เนื้อเพลงปัจจุบัน" | "متن_ترانه_فعلی" | "كلمات_الأغنية_الحالية" | "מילות_שיר_נוכחיות" | "موجودہ_گیت_متن" | "paroles_musique" | "musik_liedtext" | "текст_песни" =>
11125            {
11126                let id = self.arg_num(&args, 0, 0.0)? as i64;
11127                let t = self.arg_num(&args, 1, 0.0)? as f32;
11128                let line = self
11129                    .lyrics
11130                    .get(id as usize)
11131                    .map(|l| l.line_at(t).to_string())
11132                    .unwrap_or_default();
11133                return Ok(Value::Str(line));
11134            },
11135            #[cfg(not(target_arch = "wasm32"))]
11136            "music_mic_pitch" | "麦克风音高" | "マイク音程" | "마이크음정" | "ระดับเสียงไมค์" | "زیروبمی_میکروفون" | "طبقة_صوت_الميكروفون" | "גובה_צליל_מיקרופון" | "مائیکروفون_پچ" | "hauteur_micro_musique" | "musik_mikrofon_tonhöhe" | "высота_тона_микрофона" =>
11137            {
11138                let hz = if let Some(mic) = self.mic.as_ref() {
11139                    let s = mic.latest_samples();
11140                    let rate = mic.sample_rate();
11141                    ling_music::pitch::detect(&s, rate).unwrap_or(0.0)
11142                } else {
11143                    0.0
11144                };
11145                return Ok(Value::Number(hz as f64));
11146            },
11147            #[cfg(not(target_arch = "wasm32"))]
11148            "music_note_name" | "音名" | "音名称" | "음이름" | "ชื่อโน้ต" | "نام_نت" | "اسم_النغمة" | "שם_תו" | "نوٹ_نام" | "nom_note_musique" | "musik_notenname" | "имя_ноты_музыки" =>
11149            {
11150                let hz = self.arg_num(&args, 0, 0.0)? as f32;
11151                return Ok(Value::Str(ling_music::note::hz_to_name(hz)));
11152            },
11153            #[cfg(not(target_arch = "wasm32"))]
11154            "music_hz" | "音符频率" | "音符周波数" | "음표주파수" | "ความถี่โน้ต" | "فرکانس_نت" | "تردد_النغمة" | "תדר_תו" | "نوٹ_ہرٹز" | "hz_musique" | "musik_hz" | "музыка_гц" =>
11155            {
11156                let midi = self.pitch_arg(&args, 0, 69);
11157                return Ok(Value::Number(
11158                    ling_music::note::midi_to_hz(midi as f32) as f64
11159                ));
11160            },
11161            #[cfg(not(target_arch = "wasm32"))]
11162            "music_pitch_score" | "音准评分" | "音程スコア" | "음정점수" | "คะแนนเสียง" | "امتیاز_زیروبمی" | "درجة_طبقة_الصوت" | "ציון_גובה_צליל" | "پچ_اسکور" | "score_hauteur_musique" | "musik_tonhöhen_punktzahl" | "счёт_высоты_тона" =>
11163            {
11164                let hz = self.arg_num(&args, 0, 0.0)? as f32;
11165                let target = self.arg_num(&args, 1, 0.0)? as f32;
11166                return Ok(Value::Number(
11167                    ling_music::karaoke::pitch_score(hz, target) as f64
11168                ));
11169            },
11170
11171            // ── MIDI (inaudible note source: drive coins, cues, etc.) ──
11172            #[cfg(not(target_arch = "wasm32"))]
11173            "music_midi_load" | "载入MIDI" | "MIDI読込" | "미디로드" | "โหลดมิดี" | "بارگذاری_MIDI" | "تحميل_MIDI" | "טעינת_MIDI" | "MIDI_لوڈ" | "charger_midi_musique" | "musik_midi_laden" | "загрузить_midi_музыки" =>
11174            {
11175                let path = self.arg_str(&args, 0, "");
11176                let resolved = if std::path::Path::new(&path).exists() {
11177                    path.clone()
11178                } else if let Some(d) = &self.source_dir {
11179                    d.join(&path).to_string_lossy().into_owned()
11180                } else {
11181                    path.clone()
11182                };
11183                match ling_music::midi::load(&resolved) {
11184                    Ok(m) => {
11185                        let id = self.midis.len();
11186                        self.midis.push(m);
11187                        return Ok(Value::Number(id as f64));
11188                    },
11189                    Err(e) => {
11190                        eprintln!("music_midi_load failed ({path}): {e}");
11191                        return Ok(Value::Number(-1.0));
11192                    },
11193                }
11194            },
11195            #[cfg(not(target_arch = "wasm32"))]
11196            "music_midi_count" | "MIDI数量" | "MIDI数" | "미디수" | "จำนวนมิดี" | "تعداد_MIDI" | "عدد_MIDI" | "מספר_MIDI" | "MIDI_تعداد" | "nombre_midi_musique" | "musik_midi_anzahl" | "число_midi_музыки" =>
11197            {
11198                let id = self.arg_num(&args, 0, 0.0)? as i64;
11199                let n = self
11200                    .midis
11201                    .get(id as usize)
11202                    .map(|m| m.notes.len())
11203                    .unwrap_or(0);
11204                return Ok(Value::Number(n as f64));
11205            },
11206            // music_midi_notes(id) -> flat [time, midi, time, midi, …]
11207            #[cfg(not(target_arch = "wasm32"))]
11208            "music_midi_notes" | "MIDI音符" | "MIDIノート" | "미디음표" | "โน้ตมิดี" | "نت‌های_MIDI" | "نغمات_MIDI" | "תווי_MIDI" | "MIDI_نوٹس" | "notes_midi_musique" | "musik_midi_noten" | "ноты_midi_музыки" =>
11209            {
11210                let id = self.arg_num(&args, 0, 0.0)? as i64;
11211                let mut out = Vec::new();
11212                if let Some(m) = self.midis.get(id as usize) {
11213                    for n in &m.notes {
11214                        out.push(Value::Number(n.time as f64));
11215                        out.push(Value::Number(n.midi as f64));
11216                    }
11217                }
11218                return Ok(Value::List(Rc::new(out)));
11219            },
11220            // music_midi_bars(id) -> flat [time, midi, dur, …] (for karaoke note bars)
11221            #[cfg(not(target_arch = "wasm32"))]
11222            "music_midi_bars" | "MIDI音条" | "MIDIバー" | "미디바" | "แท่งมิดี" | "میله‌های_MIDI" | "أعمدة_MIDI" | "עמודות_MIDI" | "MIDI_بارز" | "mesures_midi_musique" | "musik_midi_takte" | "такты_midi_музыки" =>
11223            {
11224                let id = self.arg_num(&args, 0, 0.0)? as i64;
11225                let mut out = Vec::new();
11226                if let Some(m) = self.midis.get(id as usize) {
11227                    for n in &m.notes {
11228                        out.push(Value::Number(n.time as f64));
11229                        out.push(Value::Number(n.midi as f64));
11230                        out.push(Value::Number(n.dur as f64));
11231                    }
11232                }
11233                return Ok(Value::List(Rc::new(out)));
11234            },
11235
11236            // music_fft(track_id, nbands) -> spectrum at the current playback position
11237            #[cfg(not(target_arch = "wasm32"))]
11238            "music_fft" | "音乐频谱" | "音楽スペクトル" | "음악스펙트럼" | "สเปกตรัมเพลง" | "طیف_موسیقی" | "طيف_الموسيقى" | "ספקטרום_מוזיקה" | "میوزک_اسپیکٹرم" | "fft_musique" | "musik_fft" | "fft_музыки" =>
11239            {
11240                let id = self.arg_num(&args, 0, 0.0)? as i64;
11241                let nbands = self.arg_num(&args, 1, 16.0)? as usize;
11242                let pos = self.music.as_ref().map(|m| m.position()).unwrap_or(0.0);
11243                if let Some(t) = self.tracks.get(id as usize) {
11244                    let idx = (pos * t.rate as f32) as usize;
11245                    let end = (idx + 2048).min(t.mono.len());
11246                    if end > idx + 64 {
11247                        self.fft.borrow_mut().push_samples(&t.mono[idx..end]);
11248                    }
11249                }
11250                let bands = self.fft.borrow().freq_bands(nbands);
11251                return Ok(Value::List(Rc::new(
11252                    bands.into_iter().map(|x| Value::Number(x as f64)).collect(),
11253                )));
11254            },
11255
11256            // ── stop every one-shot SFX/morph/sample voice (scene cleanup) ──
11257            #[cfg(not(target_arch = "wasm32"))]
11258            "audio_stop_sfx" | "停止音效" | "効果音停止" | "효과음정지" | "หยุดเอฟเฟกต์ทั้งหมด" | "توقف_همه_جلوه‌ها" | "أوقف_كل_المؤثرات" | "עצור_כל_האפקטים" | "تمام_ایفیکٹ_روکو" =>
11259            {
11260                if let Some(a) = &self.audio {
11261                    a.stop_all_sfx();
11262                }
11263                return Ok(Value::Unit);
11264            },
11265            // ── spatial (2D/3D/4D) one-shot SFX ──
11266            #[cfg(not(target_arch = "wasm32"))]
11267            "audio_sfx" | "音效" | "空間効果音" | "공간효과음" | "เสียงเอฟเฟกต์" | "جلوه_صوتی" | "مؤثرات_صوتية" | "אפקט_קול" | "آواز_ایفیکٹ" | "effet_sonore" | "klangeffekt" | "звуковой_эффект" =>
11268            {
11269                let x = self.arg_num(&args, 0, 0.0)? as f32;
11270                let y = self.arg_num(&args, 1, 0.0)? as f32;
11271                let z = self.arg_num(&args, 2, 0.0)? as f32;
11272                let w = self.arg_num(&args, 3, 1.0)? as f32;
11273                let freq = self.arg_num(&args, 4, 440.0)? as f32;
11274                let amp = self.arg_num(&args, 5, 0.3)? as f32;
11275                let dur = self.arg_num(&args, 6, 0.15)? as f32;
11276                let wave = Wave::from_name(&self.arg_str(&args, 7, "sine"));
11277                if let Some(a) = &self.audio {
11278                    a.sfx(x, y, z, w, freq, amp, dur, wave);
11279                }
11280                return Ok(Value::Unit);
11281            },
11282            // ── YIN-YANG morph synth note: physical-model(light) ↔ FM/crush(dark) ──
11283            // โน้ตมอร์ฟ(x,y,z,w, freq, amp, dur, material, morph)
11284            //   material: 0 bowed-string · 1 plucked · 2 blown · 3 struck-metal
11285            //   morph:    0.0 light/acoustic .. 1.0 dark/digital
11286            #[cfg(not(target_arch = "wasm32"))]
11287            "morph_note" | "โน้ตมอร์ฟ" | "变形音" | "モーフ音" | "모프음" | "نت_مورف" | "نغمة_متحولة" | "תו_מורף" | "مورف_نوٹ" =>
11288            {
11289                let x = self.arg_num(&args, 0, 0.0)? as f32;
11290                let y = self.arg_num(&args, 1, 0.0)? as f32;
11291                let z = self.arg_num(&args, 2, 0.0)? as f32;
11292                let w = self.arg_num(&args, 3, 1.0)? as f32;
11293                let freq = self.arg_num(&args, 4, 220.0)? as f32;
11294                let amp = self.arg_num(&args, 5, 0.3)? as f32;
11295                let dur = self.arg_num(&args, 6, 0.6)? as f32;
11296                let material = self.arg_num(&args, 7, 0.0)?.clamp(0.0, 3.0) as u8;
11297                let morph = self.arg_num(&args, 8, 0.0)? as f32;
11298                if let Some(a) = &self.audio {
11299                    a.morph_note(x, y, z, w, freq, amp, dur, material, morph);
11300                }
11301                return Ok(Value::Unit);
11302            },
11303            // ── sample load / positional play / loop / stop ──
11304            #[cfg(not(target_arch = "wasm32"))]
11305            "audio_sample_load" | "载入采样" | "サンプル読込" | "샘플로드" | "โหลดตัวอย่างเสียง" | "بارگذاری_نمونه_صدا" | "تحميل_عينة_صوتية" | "טעינת_דגימת_קול" | "آواز_نمونہ_لوڈ" | "charger_échantillon" | "sample_laden" | "загрузить_семпл" =>
11306            {
11307                let path = self.arg_str(&args, 0, "");
11308                let resolved = if std::path::Path::new(&path).exists() {
11309                    path.clone()
11310                } else if let Some(d) = &self.source_dir {
11311                    d.join(&path).to_string_lossy().into_owned()
11312                } else {
11313                    path.clone()
11314                };
11315                match ling_music::load(&resolved) {
11316                    Ok(t) => {
11317                        if let Some(a) = &self.audio {
11318                            return Ok(Value::Number(a.add_sample(t.mono, t.rate) as f64));
11319                        }
11320                        return Ok(Value::Number(-1.0));
11321                    },
11322                    Err(e) => {
11323                        eprintln!("audio_sample_load failed ({path}): {e}");
11324                        return Ok(Value::Number(-1.0));
11325                    },
11326                }
11327            },
11328            #[cfg(not(target_arch = "wasm32"))]
11329            "audio_sample_play" | "播放采样" | "サンプル再生" | "샘플재생" | "เล่นตัวอย่างเสียง" | "پخش_نمونه_صدا" | "تشغيل_عينة_صوتية" | "נגינת_דגימת_קול" | "آواز_نمونہ_چلاؤ" | "jouer_échantillon" | "sample_abspielen" | "играть_семпл" =>
11330            {
11331                let id = self.arg_num(&args, 0, 0.0)? as usize;
11332                let x = self.arg_num(&args, 1, 0.0)? as f32;
11333                let y = self.arg_num(&args, 2, 0.0)? as f32;
11334                let z = self.arg_num(&args, 3, 0.0)? as f32;
11335                let w = self.arg_num(&args, 4, 1.0)? as f32;
11336                let vol = self.arg_num(&args, 5, 1.0)? as f32;
11337                let looping = self.arg_num(&args, 6, 0.0)? > 0.5;
11338                let v = self
11339                    .audio
11340                    .as_ref()
11341                    .map(|a| a.play_sample(id, x, y, z, w, vol, looping))
11342                    .unwrap_or(0);
11343                return Ok(Value::Number(v as f64));
11344            },
11345            #[cfg(not(target_arch = "wasm32"))]
11346            "audio_sample_stop" | "停止采样" | "サンプル停止" | "샘플정지" | "หยุดตัวอย่างเสียง" | "توقف_نمونه_صدا" | "إيقاف_عينة_صوتية" | "עצירת_דגימת_קול" | "آواز_نمونہ_روکو" | "arrêter_échantillon" | "sample_stoppen" | "остановить_семпл" =>
11347            {
11348                let v = self.arg_num(&args, 0, 0.0)? as u32;
11349                if let Some(a) = &self.audio {
11350                    a.stop_sample(v);
11351                }
11352                return Ok(Value::Unit);
11353            },
11354            // ── master FX: delay / reverb / low-pass (underwater) ──
11355            #[cfg(not(target_arch = "wasm32"))]
11356            "audio_fx_delay" | "回声" | "ディレイ効果" | "딜레이" | "เสียงสะท้อน" | "افکت_تاخیر" | "صدى_تأخير" | "אפקט_עיכוב" | "تاخیر_ایفیکٹ" | "délai_audio" | "audio_verzögerung" | "звук_задержка" =>
11357            {
11358                let time = self.arg_num(&args, 0, 0.3)? as f32;
11359                let fb = self.arg_num(&args, 1, 0.3)? as f32;
11360                let mix = self.arg_num(&args, 2, 0.3)? as f32;
11361                if let Some(a) = &self.audio {
11362                    a.fx_delay(time, fb, mix);
11363                }
11364                return Ok(Value::Unit);
11365            },
11366            #[cfg(not(target_arch = "wasm32"))]
11367            "audio_fx_reverb" | "混响" | "リバーブ" | "리버브" | "เสียงก้อง" | "افکت_پژواک" | "صدى_ارتداد" | "אפקט_הדהוד" | "بازگشت_آواز_ایفیکٹ" | "réverbération_audio" | "audio_nachhall" | "звук_реверберация" =>
11368            {
11369                let mix = self.arg_num(&args, 0, 0.3)? as f32;
11370                if let Some(a) = &self.audio {
11371                    a.fx_reverb(mix);
11372                }
11373                return Ok(Value::Unit);
11374            },
11375            #[cfg(not(target_arch = "wasm32"))]
11376            "audio_fx_lowpass" | "低通滤波" | "ローパス" | "저역통과" | "กรองความถี่ต่ำ" | "فیلتر_پایین‌گذر" | "مرشح_تمرير_منخفض" | "מסנן_תדר_נמוך" | "لو_پاس_فلٹر" | "passe_bas_audio" | "audio_tiefpass" | "звук_фнч" =>
11377            {
11378                let cutoff = self.arg_num(&args, 0, 1.0)? as f32;
11379                if let Some(a) = &self.audio {
11380                    a.fx_lowpass(cutoff);
11381                }
11382                return Ok(Value::Unit);
11383            },
11384
11385            // ══════════════════════════════════════════════════════════════════
11386            // PHYSICS BUILTINS  (crates/ling-physics) — soft bodies, rigid+angular,
11387            // and a fast 2-D water/oil liquid sim mappable onto 3-D surfaces.
11388            // ══════════════════════════════════════════════════════════════════
11389
11390            // ── soft bodies (deformable bouncy balls) ──
11391            #[cfg(not(target_arch = "wasm32"))]
11392            "soft_ball" | "软球" | "ソフトボール" | "소프트볼" | "ลูกบอลนุ่ม" | "توپ_نرم" | "كرة_ناعمة" | "כדור_רך" | "نرم_گیند" | "balle_molle" | "weicher_ball" | "мягкий_шар" =>
11393            {
11394                let x = self.arg_num(&args, 0, 0.)? as f32;
11395                let y = self.arg_num(&args, 1, 0.)? as f32;
11396                let z = self.arg_num(&args, 2, 0.)? as f32;
11397                let r = self.arg_num(&args, 3, 1.0)? as f32;
11398                let b = ling_physics::soft::SoftBody::sphere(
11399                    ling_physics::Vec3::new(x, y, z),
11400                    r,
11401                    8,
11402                    12,
11403                    1.0,
11404                );
11405                let id = self.soft_bodies.len();
11406                self.soft_bodies.push(b);
11407                return Ok(Value::Number(id as f64));
11408            },
11409            #[cfg(not(target_arch = "wasm32"))]
11410            "soft_step" | "软体步进" | "ソフト更新" | "소프트스텝" | "ก้าวนุ่ม" | "گام_نرم_جسم" | "خطوة_ناعمة" | "צעד_רך" | "نرم_قدم" | "pas_mou" | "weicher_schritt" | "мягкий_шаг" =>
11411            {
11412                let id = self.arg_num(&args, 0, 0.)? as usize;
11413                let dt = self.arg_num(&args, 1, 0.016)? as f32;
11414                let gy = self.arg_num(&args, 2, 15.0)? as f32;
11415                if let Some(b) = self.soft_bodies.get_mut(id) {
11416                    b.integrate(dt, ling_physics::Vec3::new(0.0, gy, 0.0), 4);
11417                }
11418                return Ok(Value::Unit);
11419            },
11420            #[cfg(not(target_arch = "wasm32"))]
11421            "soft_bounce" | "软体落地" | "ソフト着地" | "소프트바운스" | "เด้งนุ่ม" | "جهش_نرم" | "ارتداد_ناعم" | "קפיצה_רכה" | "نرم_اچھال" | "rebond_mou" | "weicher_abprall" | "мягкий_отскок" =>
11422            {
11423                let id = self.arg_num(&args, 0, 0.)? as usize;
11424                let fy = self.arg_num(&args, 1, 0.)? as f32;
11425                let rest = self.arg_num(&args, 2, 0.5)? as f32;
11426                if let Some(b) = self.soft_bodies.get_mut(id) {
11427                    b.floor_collision(fy, rest);
11428                }
11429                return Ok(Value::Unit);
11430            },
11431            #[cfg(not(target_arch = "wasm32"))]
11432            "soft_contain" | "软体边界" | "ソフト箱" | "소프트경계" | "กล่องนุ่ม" | "محفظه_نرم" | "احتواء_ناعم" | "הכלה_רכה" | "نرم_احاطہ" | "contenir_mou" | "weiche_eindämmung" | "мягкое_сдерживание" =>
11433            {
11434                let id = self.arg_num(&args, 0, 0.)? as usize;
11435                let nx = self.arg_num(&args, 1, -5.)? as f32;
11436                let ny = self.arg_num(&args, 2, -5.)? as f32;
11437                let nz = self.arg_num(&args, 3, -5.)? as f32;
11438                let mx = self.arg_num(&args, 4, 5.)? as f32;
11439                let my = self.arg_num(&args, 5, 5.)? as f32;
11440                let mz = self.arg_num(&args, 6, 5.)? as f32;
11441                let rest = self.arg_num(&args, 7, 0.6)? as f32;
11442                if let Some(b) = self.soft_bodies.get_mut(id) {
11443                    b.contain(
11444                        ling_physics::Vec3::new(nx, ny, nz),
11445                        ling_physics::Vec3::new(mx, my, mz),
11446                        rest,
11447                    );
11448                }
11449                return Ok(Value::Unit);
11450            },
11451            #[cfg(not(target_arch = "wasm32"))]
11452            "soft_kick" | "软体踢" | "ソフト衝撃" | "소프트킥" | "เตะนุ่ม" | "ضربه_نرم" | "ركلة_ناعمة" | "בעיטה_רכה" | "نرم_ٹھوکر" | "coup_mou" | "weicher_stoß" | "мягкий_удар" =>
11453            {
11454                let id = self.arg_num(&args, 0, 0.)? as usize;
11455                let dx = self.arg_num(&args, 1, 0.)? as f32;
11456                let dy = self.arg_num(&args, 2, 0.)? as f32;
11457                let dz = self.arg_num(&args, 3, 0.)? as f32;
11458                let s = self.arg_num(&args, 4, 0.1)? as f32;
11459                if let Some(b) = self.soft_bodies.get_mut(id) {
11460                    b.kick(ling_physics::Vec3::new(dx, dy, dz), s);
11461                }
11462                return Ok(Value::Unit);
11463            },
11464            // soft_spin(id, ax, ay, az, rate) — add angular velocity about the axis
11465            // through the centroid (rate = rad/step; ≈ surface_speed / radius to roll)
11466            #[cfg(not(target_arch = "wasm32"))]
11467            "soft_spin" | "软体自旋" | "ソフト回転" | "소프트회전" | "หมุนนุ่ม" | "چرخش_نرم" | "دوران_ناعم" | "סיבוב_רך" | "نرم_گھماؤ" | "rotation_molle" | "weicher_spin" | "мягкое_вращение" =>
11468            {
11469                let id = self.arg_num(&args, 0, 0.)? as usize;
11470                let ax = self.arg_num(&args, 1, 0.)? as f32;
11471                let ay = self.arg_num(&args, 2, 0.)? as f32;
11472                let az = self.arg_num(&args, 3, 0.)? as f32;
11473                let rate = self.arg_num(&args, 4, 0.1)? as f32;
11474                if let Some(b) = self.soft_bodies.get_mut(id) {
11475                    b.spin(ling_physics::Vec3::new(ax, ay, az), rate);
11476                }
11477                return Ok(Value::Unit);
11478            },
11479            #[cfg(not(target_arch = "wasm32"))]
11480            "soft_deform" | "形变量" | "変形量" | "변형량" | "ความบิดเบี้ยว" | "تغییرشکل_نرم" | "تشوه_ناعم" | "עיוות_רך" | "نرم_بگاڑ" | "déformer_mou" | "weiches_verformen" | "мягкая_деформация" =>
11481            {
11482                let id = self.arg_num(&args, 0, 0.)? as usize;
11483                let d = self
11484                    .soft_bodies
11485                    .get(id)
11486                    .map(|b| b.deformation())
11487                    .unwrap_or(0.0);
11488                return Ok(Value::Number(d as f64));
11489            },
11490            // soft_angular_speed(id) -> magnitude of the body's angular velocity
11491            // (how fast it is tumbling/rolling), derived from its node velocities.
11492            #[cfg(not(target_arch = "wasm32"))]
11493            "soft_angular_speed"
11494            | "软体角速"
11495            | "ソフト角速度"
11496            | "소프트각속도"
11497            | "ความเร็วเชิงมุมนุ่ม" | "سرعت_زاویه‌ای_نرم" | "سرعة_زاوية_ناعمة" | "מהירות_זוויתית_רכה" | "نرم_زاویائی_رفتار" | "vitesse_angulaire_molle" | "weiche_winkelgeschwindigkeit" | "мягкая_угловая_скорость" => {
11498                let id = self.arg_num(&args, 0, 0.)? as usize;
11499                let w = self
11500                    .soft_bodies
11501                    .get(id)
11502                    .map(|b| b.angular_speed())
11503                    .unwrap_or(0.0);
11504                return Ok(Value::Number(w as f64));
11505            },
11506            #[cfg(not(target_arch = "wasm32"))]
11507            "soft_centroid" | "软体质心" | "ソフト重心" | "소프트중심" | "จุดศูนย์กลางนุ่ม" | "مرکز_جرم_نرم" | "مركز_ثقل_ناعم" | "מרכז_כובד_רך" | "نرم_مرکز_ثقل" | "centroïde_mou" | "weicher_schwerpunkt" | "мягкий_центроид" =>
11508            {
11509                let id = self.arg_num(&args, 0, 0.)? as usize;
11510                let c = self
11511                    .soft_bodies
11512                    .get(id)
11513                    .map(|b| b.centroid())
11514                    .unwrap_or(ling_physics::Vec3::ZERO);
11515                return Ok(Value::List(Rc::new(vec![
11516                    Value::Number(c.x as f64),
11517                    Value::Number(c.y as f64),
11518                    Value::Number(c.z as f64),
11519                ])));
11520            },
11521            // soft_nodes(id) -> flat [x,y,z, x,y,z, …] for rendering the deformed mesh
11522            #[cfg(not(target_arch = "wasm32"))]
11523            "soft_nodes" | "软体节点" | "ソフト節点" | "소프트노드" | "จุดนุ่ม" | "گره‌های_نرم" | "عقد_ناعمة" | "צמתי_רך" | "نرم_نوڈز" | "nœuds_mous" | "weiche_knoten" | "мягкие_узлы" =>
11524            {
11525                let id = self.arg_num(&args, 0, 0.)? as usize;
11526                let mut out = Vec::new();
11527                if let Some(b) = self.soft_bodies.get(id) {
11528                    for n in &b.nodes {
11529                        out.push(Value::Number(n.pos.x as f64));
11530                        out.push(Value::Number(n.pos.y as f64));
11531                        out.push(Value::Number(n.pos.z as f64));
11532                    }
11533                }
11534                return Ok(Value::List(Rc::new(out)));
11535            },
11536
11537            // ── rigid bodies with angular dynamics ──
11538            #[cfg(not(target_arch = "wasm32"))]
11539            "rb_add" | "刚体添加" | "剛体追加" | "강체추가" | "เพิ่มวัตถุแข็ง" | "افزودن_جسم_صلب" | "أضف_جسما_صلبا" | "הוסף_גוף_קשיח" | "سخت_جسم_شامل_کرو" | "ajouter_corps_rigide" | "starrkörper_hinzufügen" | "добавить_твёрдое_тело" =>
11540            {
11541                let x = self.arg_num(&args, 0, 0.)? as f32;
11542                let y = self.arg_num(&args, 1, 0.)? as f32;
11543                let z = self.arg_num(&args, 2, 0.)? as f32;
11544                let mass = self.arg_num(&args, 3, 1.0)? as f32;
11545                let mut b =
11546                    ling_physics::rigid::RigidBody::new(ling_physics::Vec3::new(x, y, z), mass);
11547                b.restitution = 0.6;
11548                return Ok(Value::Number(self.rigid_world.add(b) as f64));
11549            },
11550            #[cfg(not(target_arch = "wasm32"))]
11551            "rb_torque" | "扭矩" | "トルク" | "토크" | "แรงบิด" | "گشتاور" | "عزم_دوران" | "מומנט" | "ٹارک" | "couple_corps_rigide" | "starrkörper_drehmoment" | "крутящий_момент_твёрдого_тела" => {
11552                let i = self.arg_num(&args, 0, 0.)? as usize;
11553                let tx = self.arg_num(&args, 1, 0.)? as f32;
11554                let ty = self.arg_num(&args, 2, 0.)? as f32;
11555                let tz = self.arg_num(&args, 3, 0.)? as f32;
11556                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
11557                    b.apply_torque(ling_physics::Vec3::new(tx, ty, tz));
11558                }
11559                return Ok(Value::Unit);
11560            },
11561            #[cfg(not(target_arch = "wasm32"))]
11562            "rb_spin" | "自旋" | "スピン" | "스핀" | "หมุน" | "چرخش_جسم_صلب" | "دوران_جسم_صلب" | "סיבוב_גוף_קשיח" | "سخت_جسم_گھماؤ" | "spin_corps_rigide" | "starrkörper_spin" | "вращение_твёрдого_тела" => {
11563                let i = self.arg_num(&args, 0, 0.)? as usize;
11564                let wx = self.arg_num(&args, 1, 0.)? as f32;
11565                let wy = self.arg_num(&args, 2, 0.)? as f32;
11566                let wz = self.arg_num(&args, 3, 0.)? as f32;
11567                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
11568                    b.apply_spin(ling_physics::Vec3::new(wx, wy, wz));
11569                }
11570                return Ok(Value::Unit);
11571            },
11572            #[cfg(not(target_arch = "wasm32"))]
11573            "rb_impulse" | "刚体冲量" | "剛体インパルス" | "강체충격" | "แรงดลแข็ง" | "ضربه_جسم_صلب" | "دفعة_جسم_صلب" | "דחף_גוף_קשיח" | "سخت_جسم_دھکا" | "impulsion_corps_rigide" | "starrkörper_impuls" | "импульс_твёрдого_тела" =>
11574            {
11575                let i = self.arg_num(&args, 0, 0.)? as usize;
11576                let ix = self.arg_num(&args, 1, 0.)? as f32;
11577                let iy = self.arg_num(&args, 2, 0.)? as f32;
11578                let iz = self.arg_num(&args, 3, 0.)? as f32;
11579                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
11580                    b.apply_impulse(ling_physics::Vec3::new(ix, iy, iz));
11581                }
11582                return Ok(Value::Unit);
11583            },
11584            #[cfg(not(target_arch = "wasm32"))]
11585            "rb_floor" | "刚体落地" | "剛体着地" | "강체바닥" | "พื้นแข็ง" | "کف_جسم_صلب" | "أرضية_جسم_صلب" | "רצפת_גוף_קשיח" | "سخت_جسم_فرش" | "sol_corps_rigide" | "starrkörper_boden" | "пол_твёрдого_тела" =>
11586            {
11587                let i = self.arg_num(&args, 0, 0.)? as usize;
11588                let fy = self.arg_num(&args, 1, 0.)? as f32;
11589                let rest = self.arg_num(&args, 2, 0.6)? as f32;
11590                let fric = self.arg_num(&args, 3, 0.6)? as f32;
11591                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
11592                    b.bounce_floor(fy, rest, fric);
11593                }
11594                return Ok(Value::Unit);
11595            },
11596            #[cfg(not(target_arch = "wasm32"))]
11597            "rb_gravity" | "刚体重力" | "剛体重力" | "강체중력" | "แรงโน้มถ่วงแข็ง" | "گرانش_جسم_صلب" | "جاذبية_جسم_صلب" | "כבידת_גוף_קשיח" | "سخت_جسم_کشش_ثقل" | "gravité_corps_rigide" | "starrkörper_schwerkraft" | "гравитация_твёрдого_тела" =>
11598            {
11599                let gx = self.arg_num(&args, 0, 0.)? as f32;
11600                let gy = self.arg_num(&args, 1, 9.81)? as f32;
11601                let gz = self.arg_num(&args, 2, 0.)? as f32;
11602                self.rigid_world.gravity = ling_physics::Vec3::new(gx, gy, gz);
11603                return Ok(Value::Unit);
11604            },
11605            #[cfg(not(target_arch = "wasm32"))]
11606            "rb_step" | "刚体步进" | "剛体更新" | "강체스텝" | "ก้าวแข็ง" | "گام_جسم_صلب" | "خطوة_جسم_صلب" | "צעד_גוף_קשיח" | "سخت_جسم_قدم" | "pas_corps_rigide" | "starrkörper_schritt" | "шаг_твёрдого_тела" =>
11607            {
11608                let dt = self.arg_num(&args, 0, 0.016)? as f32;
11609                self.rigid_world.step(dt);
11610                return Ok(Value::Unit);
11611            },
11612            #[cfg(not(target_arch = "wasm32"))]
11613            "rb_pos" | "刚体位置" | "剛体位置" | "강체위치" | "ตำแหน่งแข็ง" | "موقعیت_جسم_صلب" | "موضع_جسم_صلب" | "מיקום_גוף_קשיח" | "سخت_جسم_مقام" | "position_corps_rigide" | "starrkörper_position" | "позиция_твёрдого_тела" =>
11614            {
11615                let i = self.arg_num(&args, 0, 0.)? as usize;
11616                let p = self
11617                    .rigid_world
11618                    .bodies
11619                    .get(i)
11620                    .map(|b| b.pos)
11621                    .unwrap_or(ling_physics::Vec3::ZERO);
11622                return Ok(Value::List(Rc::new(vec![
11623                    Value::Number(p.x as f64),
11624                    Value::Number(p.y as f64),
11625                    Value::Number(p.z as f64),
11626                ])));
11627            },
11628            #[cfg(not(target_arch = "wasm32"))]
11629            "rb_rot" | "刚体旋转" | "剛体回転" | "강체회전" | "การหมุนแข็ง" | "چرخش_جسم_صلب" | "دوران_وضعية_جسم_صلب" | "סיבוב_גוף_קשיח" | "سخت_جسم_گردش" | "rotation_corps_rigide" | "starrkörper_rotation" | "поворот_твёрдого_тела" =>
11630            {
11631                let i = self.arg_num(&args, 0, 0.)? as usize;
11632                let q = self
11633                    .rigid_world
11634                    .bodies
11635                    .get(i)
11636                    .map(|b| b.orientation)
11637                    .unwrap_or(ling_physics::Quat::IDENTITY);
11638                return Ok(Value::List(Rc::new(vec![
11639                    Value::Number(q.x as f64),
11640                    Value::Number(q.y as f64),
11641                    Value::Number(q.z as f64),
11642                    Value::Number(q.w as f64),
11643                ])));
11644            },
11645
11646            // ── native-res mesh (.lmesh): load once, draw fast (unlit, per-tri colour) ──
11647            #[cfg(not(target_arch = "wasm32"))]
11648            "mesh_load" | "โหลดเมช" | "载入网格" | "メッシュ読込" | "메시로드" | "بارگذاری_مش" | "حمّل_شبكة" | "טען_רשת" | "میش_لوڈ" =>
11649            {
11650                let path = self.arg_str(&args, 0, "");
11651                let resolved = if std::path::Path::new(&path).exists() {
11652                    path.clone()
11653                } else if let Some(d) = &self.source_dir {
11654                    d.join(&path).to_string_lossy().into_owned()
11655                } else {
11656                    path.clone()
11657                };
11658                let bytes = match std::fs::read(&resolved) {
11659                    Ok(b) => b,
11660                    Err(e) => {
11661                        eprintln!("mesh_load failed ({path}): {e}");
11662                        return Ok(Value::Number(-1.0));
11663                    },
11664                };
11665                if bytes.len() < 16 || &bytes[0..4] != b"LMSH" {
11666                    eprintln!("mesh_load: bad header ({path})");
11667                    return Ok(Value::Number(-1.0));
11668                }
11669                let rd4 =
11670                    |o: usize| -> [u8; 4] { [bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]] };
11671                let height = f32::from_le_bytes(rd4(8));
11672                let ntri = u32::from_le_bytes(rd4(12)) as usize;
11673                let need = 16usize.saturating_add(ntri.saturating_mul(9 * 4 + 3));
11674                if bytes.len() < need {
11675                    eprintln!("mesh_load: truncated ({path})");
11676                    return Ok(Value::Number(-1.0));
11677                }
11678                let mut pos = Vec::with_capacity(ntri * 3);
11679                let mut col = Vec::with_capacity(ntri);
11680                let mut off = 16usize;
11681                for _ in 0..ntri {
11682                    for _k in 0..3 {
11683                        let x = f32::from_le_bytes(rd4(off));
11684                        let y = f32::from_le_bytes(rd4(off + 4));
11685                        let z = f32::from_le_bytes(rd4(off + 8));
11686                        off += 12;
11687                        pos.push([x, y, z]);
11688                    }
11689                    col.push([bytes[off], bytes[off + 1], bytes[off + 2]]);
11690                    off += 3;
11691                }
11692                eprintln!("mesh_load: {} ({} tris, h={:.2})", path, ntri, height);
11693                let id = self.meshes.len();
11694                self.meshes
11695                    .push(crate::gfx::shapes::ColorMesh { pos, col, height });
11696                return Ok(Value::Number(id as f64));
11697            },
11698            #[cfg(target_arch = "wasm32")]
11699            "mesh_load" | "โหลดเมช" | "载入网格" | "メッシュ読込" | "메시로드" | "بارگذاری_مش" | "حمّل_شبكة" | "טען_רשת" | "میش_لوڈ" =>
11700            {
11701                // Native .lmesh loading is file-system based and not wired for wasm yet.
11702                // Return an invalid handle so scripts can choose a fallback path.
11703                return Ok(Value::Number(-1.0));
11704            },
11705            #[cfg(not(target_arch = "wasm32"))]
11706            "mesh_draw" | "วาดเมชสี" | "绘制网格" | "メッシュ描画" | "메시그리기" | "رسم_مش_رنگی" | "ارسم_شبكة_ملونة" | "צייר_רשת_צבעונית" | "رنگین_میش_کھینچو" =>
11707            {
11708                // ('วาดเมช' is taken by draw_mesh — use a distinct Thai alias)
11709                let id = self.arg_num(&args, 0, 0.)? as usize;
11710                let cx = self.arg_num(&args, 1, 0.)? as f32;
11711                let cy = self.arg_num(&args, 2, 0.)? as f32;
11712                let cz = self.arg_num(&args, 3, 0.)? as f32;
11713                let sc = self.arg_num(&args, 4, 1.)? as f32;
11714                let yaw = self.arg_num(&args, 5, 0.)? as f32;
11715                let sway = self.arg_num(&args, 6, 0.)? as f32;
11716                let arm = self.arg_num(&args, 7, 0.)? as f32;
11717                let lean = self.arg_num(&args, 8, 0.)? as f32;
11718                let leg = self.arg_num(&args, 9, 0.)? as f32;
11719                let tuck = self.arg_num(&args, 10, 0.)? as f32;
11720                if id < self.meshes.len() {
11721                    let m = &self.meshes[id];
11722                    let mut gfx = self.gfx.borrow_mut();
11723                    gfx.draw_color_mesh(m, cx, cy, cz, sc, yaw, sway, arm, lean, leg, tuck);
11724                }
11725                return Ok(Value::Unit);
11726            },
11727            #[cfg(target_arch = "wasm32")]
11728            "mesh_draw" | "วาดเมชสี" | "绘制网格" | "メッシュ描画" | "메시그리기" | "رسم_مش_رنگی" | "ارسم_شبكة_ملونة" | "צייר_רשת_צבעונית" | "رنگین_میش_کھینچو" =>
11729            {
11730                return Ok(Value::Unit);
11731            },
11732
11733            // ── liquid sim (water + oil, immiscible) ──
11734            "liquid_new" | "新建液体" | "液体新規" | "액체생성" | "สร้างของเหลว" | "مایع_جدید" | "سائل_جديد" | "נוזל_חדש" | "نیا_مائع" | "nouveau_liquide" | "neue_flüssigkeit" | "новая_жидкость" =>
11735            {
11736                let w = self.arg_num(&args, 0, 64.)? as usize;
11737                let h = self.arg_num(&args, 1, 64.)? as usize;
11738                let id = self.liquids.len();
11739                self.liquids
11740                    .push(ling_physics::liquid::LiquidGrid::new(w, h));
11741                return Ok(Value::Number(id as f64));
11742            },
11743            "liquid_set_colors" | "液体颜色" | "液体配色" | "액체색상" | "สีของเหลว" | "تنظیم_رنگ_مایع" | "عيّن_ألوان_السائل" | "קבע_צבעי_נוזל" | "مائع_رنگ_مقرر_کرو" =>
11744            {
11745                let id = self.arg_num(&args, 0, 0.)? as usize;
11746                let wr = self.arg_num(&args, 1, 40.)? as f32;
11747                let wg = self.arg_num(&args, 2, 110.)? as f32;
11748                let wb = self.arg_num(&args, 3, 235.)? as f32;
11749                let or_ = self.arg_num(&args, 4, 240.)? as f32;
11750                let og = self.arg_num(&args, 5, 175.)? as f32;
11751                let ob = self.arg_num(&args, 6, 45.)? as f32;
11752                if let Some(g) = self.liquids.get_mut(id) {
11753                    g.set_colors(wr, wg, wb, or_, og, ob);
11754                }
11755                return Ok(Value::Unit);
11756            },
11757            "liquid_splat" | "液体注入" | "液体追加" | "액체분사" | "หยดของเหลว" | "پاشش_مایع" | "بقعة_سائل" | "התזת_נוזל" | "مائع_چھینٹا" | "éclaboussure_liquide" | "flüssigkeit_spritzer" | "брызги_жидкости" =>
11758            {
11759                let id = self.arg_num(&args, 0, 0.)? as usize;
11760                let x = self.arg_num(&args, 1, 0.)? as f32;
11761                let y = self.arg_num(&args, 2, 0.)? as f32;
11762                let kind = self.arg_num(&args, 3, 0.)? as i32;
11763                let amt = self.arg_num(&args, 4, 1.0)? as f32;
11764                let rad = self.arg_num(&args, 5, 4.0)? as f32;
11765                if let Some(g) = self.liquids.get_mut(id) {
11766                    g.splat(x, y, kind, amt, rad);
11767                }
11768                return Ok(Value::Unit);
11769            },
11770            "liquid_gravity" | "液体重力" | "液体重力ベクトル" | "액체중력" | "แรงโน้มถ่วงเหลว" | "گرانش_مایع" | "جاذبية_السائل" | "כבידת_נוזל" | "مائع_کشش_ثقل" | "gravité_liquide" | "flüssigkeit_schwerkraft" | "гравитация_жидкости" =>
11771            {
11772                let id = self.arg_num(&args, 0, 0.)? as usize;
11773                let gx = self.arg_num(&args, 1, 0.)? as f32;
11774                let gy = self.arg_num(&args, 2, 60.)? as f32;
11775                if let Some(g) = self.liquids.get_mut(id) {
11776                    g.set_gravity(gx, gy);
11777                }
11778                return Ok(Value::Unit);
11779            },
11780            "liquid_step" | "液体步进" | "液体更新" | "액체스텝" | "ก้าวของเหลว" | "گام_مایع" | "خطوة_السائل" | "צעד_נוזל" | "مائع_قدم" | "pas_liquide" | "flüssigkeit_schritt" | "шаг_жидкости" =>
11781            {
11782                let id = self.arg_num(&args, 0, 0.)? as usize;
11783                let dt = self.arg_num(&args, 1, 0.016)? as f32;
11784                if let Some(g) = self.liquids.get_mut(id) {
11785                    g.step(dt);
11786                }
11787                return Ok(Value::Unit);
11788            },
11789            // liquid_step_all(dt) — advance EVERY liquid grid one tick, in parallel
11790            // across instances (rayon). Independent grids share no state, so this is
11791            // an embarrassingly-parallel batch: a scene with many liquid surfaces
11792            // steps in one call that scales across cores instead of N serial
11793            // `liquid_step` calls.
11794            "liquid_step_all"
11795            | "液体全步进"
11796            | "液体全更新"
11797            | "전체액체스텝"
11798            | "ก้าวของเหลวทั้งหมด" | "گام_همه_مایعات" | "خطوة_كل_السوائل" | "צעד_כל_הנוזלים" | "تمام_مائع_قدم" => {
11799                let dt = self.arg_num(&args, 0, 0.016)? as f32;
11800                ling_physics::liquid::step_all(&mut self.liquids, dt);
11801                return Ok(Value::Unit);
11802            },
11803            // liquid_rainbow(id, on) — colour the fluid as a flowing ROYGBIV marble
11804            "liquid_rainbow" | "液体彩虹" | "液体虹" | "액체무지개" | "ของเหลวสายรุ้ง" | "مایع_رنگین‌کمان" | "سائل_قوس_قزح" | "נוזל_קשת" | "قوس_قزح_مائع" | "arc_en_ciel_liquide" | "flüssigkeit_regenbogen" | "радуга_жидкости" =>
11805            {
11806                let id = self.arg_num(&args, 0, 0.)? as usize;
11807                let on = self.arg_num(&args, 1, 1.0)? > 0.5;
11808                if let Some(g) = self.liquids.get_mut(id) {
11809                    g.rainbow = on;
11810                }
11811                return Ok(Value::Unit);
11812            },
11813            // liquid_mix(id) -> 0 (oil/water separated) .. 1 (fully intermixed)
11814            "liquid_mix" | "液体混合" | "液体混合度" | "액체혼합" | "การผสมของเหลว" | "ترکیب_مایع" | "مزج_سائل" | "ערבוב_נוזל" | "مائع_ملاؤ" | "mélanger_liquide" | "flüssigkeit_mischen" | "смешать_жидкость" =>
11815            {
11816                let id = self.arg_num(&args, 0, 0.)? as usize;
11817                let m = self.liquids.get(id).map(|g| g.mix_amount()).unwrap_or(0.0);
11818                return Ok(Value::Number(m as f64));
11819            },
11820            // liquid_draw(id, sx, sy, scale) — fast flat 2-D blit of the colour field
11821            #[cfg(not(target_arch = "wasm32"))]
11822            "liquid_draw" | "绘制液体" | "液体描画" | "액체그리기" | "วาดของเหลว" | "رسم_مایع" | "ارسم_سائلا" | "צייר_נוזל" | "مائع_کھینچو" | "dessiner_liquide" | "flüssigkeit_zeichnen" | "рисовать_жидкость" =>
11823            {
11824                let id = self.arg_num(&args, 0, 0.)? as usize;
11825                let sx = self.arg_num(&args, 1, 0.)? as i32;
11826                let sy = self.arg_num(&args, 2, 0.)? as i32;
11827                let scale = (self.arg_num(&args, 3, 4.)? as i32).max(1);
11828                if id < self.liquids.len() {
11829                    let (gw, gh) = {
11830                        let g = &self.liquids[id];
11831                        (g.w, g.h)
11832                    };
11833                    let mut gfx = self.gfx.borrow_mut();
11834                    let (w, h) = (gfx.width as i32, gfx.height as i32);
11835                    let g = &self.liquids[id];
11836                    for cy in 0..gh {
11837                        for cx in 0..gw {
11838                            let col = g.sample_rgb(cx, cy);
11839                            let bx = sx + cx as i32 * scale;
11840                            let by = sy + cy as i32 * scale;
11841                            for dy in 0..scale {
11842                                for dx in 0..scale {
11843                                    let px = bx + dx;
11844                                    let py = by + dy;
11845                                    if px >= 0 && py >= 0 && px < w && py < h {
11846                                        gfx.buffer[(py * w + px) as usize] = col;
11847                                    }
11848                                }
11849                            }
11850                        }
11851                    }
11852                }
11853                return Ok(Value::Unit);
11854            },
11855            // liquid_draw_surface(id, kind, cx,cy,cz, radius, height)
11856            //   kind: 0 plane · 1 sphere · 2 cylinder · 3 cone · 4 dome
11857            "liquid_draw_surface" | "液体贴面" | "液体曲面" | "액체곡면" | "ของเหลวบนพื้นผิว" | "رسم_سطح_مایع" | "ارسم_سطح_السائل" | "צייר_משטח_נוזל" | "مائع_سطح_کھینچو" | "dessiner_surface_liquide" | "flüssigkeit_oberfläche_zeichnen" | "рисовать_поверхность_жидкости" =>
11858            {
11859                #[cfg(not(target_arch = "wasm32"))]
11860                {
11861                    let id = self.arg_num(&args, 0, 0.)? as usize;
11862                    let kind = self.arg_num(&args, 1, 1.)? as i32;
11863                    let cx = self.arg_num(&args, 2, 0.)? as f32;
11864                    let cy = self.arg_num(&args, 3, 0.)? as f32;
11865                    let cz = self.arg_num(&args, 4, 0.)? as f32;
11866                    let radius = self.arg_num(&args, 5, 2.0)? as f32;
11867                    let height = self.arg_num(&args, 6, 3.0)? as f32;
11868                    if id < self.liquids.len() {
11869                        let (gw, gh) = {
11870                            let g = &self.liquids[id];
11871                            (g.w, g.h)
11872                        };
11873                        let mut gfx = self.gfx.borrow_mut();
11874                        let (w, h, add) = (gfx.width, gfx.height, gfx.blend == 1);
11875                        let cam = gfx.camera.clone();
11876                        let near = -cam.zdist + 0.05;
11877                        let g = &self.liquids[id];
11878                        let tau = std::f32::consts::TAU;
11879                        let pi = std::f32::consts::PI;
11880                        // surface point for a (u,v) in [0,1] on the chosen primitive
11881                        let sp = |u: f32, v: f32| -> [f32; 3] {
11882                            if kind == 0 {
11883                                [
11884                                    cx + (u - 0.5) * 2.0 * radius,
11885                                    cy,
11886                                    cz + (v - 0.5) * 2.0 * radius,
11887                                ]
11888                            } else if kind == 2 {
11889                                let th = u * tau;
11890                                [
11891                                    cx + th.cos() * radius,
11892                                    cy + (v - 0.5) * height,
11893                                    cz + th.sin() * radius,
11894                                ]
11895                            } else if kind == 3 {
11896                                let th = u * tau;
11897                                let rr = radius * (1.0 - v);
11898                                [
11899                                    cx + th.cos() * rr,
11900                                    cy + (v - 0.5) * height,
11901                                    cz + th.sin() * rr,
11902                                ]
11903                            } else if kind == 4 {
11904                                let th = u * tau;
11905                                let ph = v * pi * 0.5;
11906                                [
11907                                    cx + ph.sin() * th.cos() * radius,
11908                                    cy - ph.cos() * radius,
11909                                    cz + ph.sin() * th.sin() * radius,
11910                                ]
11911                            } else {
11912                                let th = u * tau;
11913                                let ph = v * pi;
11914                                [
11915                                    cx + ph.sin() * th.cos() * radius,
11916                                    cy + ph.cos() * radius,
11917                                    cz + ph.sin() * th.sin() * radius,
11918                                ]
11919                            }
11920                        };
11921                        let nrm = |u: f32, v: f32| -> [f32; 3] {
11922                            if kind == 0 {
11923                                [0.0, -1.0, 0.0]
11924                            } else if kind == 2 {
11925                                let th = u * tau;
11926                                [th.cos(), 0.0, th.sin()]
11927                            } else if kind == 3 {
11928                                let th = u * tau;
11929                                let s = (radius / height.max(0.01)).atan();
11930                                [th.cos() * s.cos(), s.sin(), th.sin() * s.cos()]
11931                            } else if kind == 4 {
11932                                let th = u * tau;
11933                                let ph = v * pi * 0.5;
11934                                [ph.sin() * th.cos(), -ph.cos(), ph.sin() * th.sin()]
11935                            } else {
11936                                let th = u * tau;
11937                                let ph = v * pi;
11938                                [ph.sin() * th.cos(), ph.cos(), ph.sin() * th.sin()]
11939                            }
11940                        };
11941                        let gwf = gw as f32;
11942                        let ghf = gh as f32;
11943                        let mut cyc = 0usize;
11944                        while cyc < gh {
11945                            let mut cxc = 0usize;
11946                            while cxc < gw {
11947                                // cull by the cell centre's outward normal
11948                                let uc = (cxc as f32 + 0.5) / gwf;
11949                                let vc = (cyc as f32 + 0.5) / ghf;
11950                                let c = sp(uc, vc);
11951                                let n = nrm(uc, vc);
11952                                let dc = cam.depth(c[0], c[1], c[2]);
11953                                if dc > near {
11954                                    let cull = kind != 0
11955                                        && cam.depth(
11956                                            c[0] + n[0] * 0.06,
11957                                            c[1] + n[1] * 0.06,
11958                                            c[2] + n[2] * 0.06,
11959                                        ) > dc;
11960                                    if !cull {
11961                                        // project the 4 cell corners → a filled AA vector quad
11962                                        let u0 = cxc as f32 / gwf;
11963                                        let u1 = (cxc + 1) as f32 / gwf;
11964                                        let v0 = cyc as f32 / ghf;
11965                                        let v1 = (cyc + 1) as f32 / ghf;
11966                                        let q = [sp(u0, v0), sp(u1, v0), sp(u1, v1), sp(u0, v1)];
11967                                        let mut poly: Vec<[f32; 2]> = Vec::with_capacity(5);
11968                                        let mut ok = true;
11969                                        for p in &q {
11970                                            if cam.depth(p[0], p[1], p[2]) <= near {
11971                                                ok = false;
11972                                                break;
11973                                            }
11974                                            let (sx, sy, _) = cam.project(p[0], p[1], p[2]);
11975                                            poly.push([sx, sy]);
11976                                        }
11977                                        if ok {
11978                                            let p0 = poly[0];
11979                                            poly.push(p0);
11980                                            let col = g.sample_rgb(cxc, cyc);
11981                                            crate::gfx::raster::fill_contours_aa(
11982                                                &mut gfx.buffer,
11983                                                w,
11984                                                h,
11985                                                col,
11986                                                add,
11987                                                std::slice::from_ref(&poly),
11988                                            );
11989                                        }
11990                                    }
11991                                }
11992                                cxc += 1;
11993                            }
11994                            cyc += 1;
11995                        }
11996                    }
11997                }
11998                #[cfg(target_arch = "wasm32")]
11999                {
12000                    // WASM: liquid_draw_surface is a no-op for now (would need WebGL shader)
12001                    // The liquid simulation still runs, just not rendered to 3D surfaces
12002                }
12003                return Ok(Value::Unit);
12004            },
12005            // sparkle(x, y, w, h, count [, t]) — scatter twinkling vector star-sparkles
12006            // in a rect (snowglobe effect) in the current colour + blend mode.
12007            #[cfg(not(target_arch = "wasm32"))]
12008            "sparkle" | "闪光" | "きらめき" | "반짝임" | "ประกาย" | "درخشش" | "بريق" | "נצנוץ" | "چمک" | "scintillement" | "funkeln" | "искриться" => {
12009                let x = self.arg_num(&args, 0, 0.)? as f32;
12010                let y = self.arg_num(&args, 1, 0.)? as f32;
12011                let ww = self.arg_num(&args, 2, 200.)? as f32;
12012                let hh = self.arg_num(&args, 3, 200.)? as f32;
12013                let count = self.arg_num(&args, 4, 40.)? as i32;
12014                let t = self.arg_num(&args, 5, 0.)? as f32;
12015                let mut gfx = self.gfx.borrow_mut();
12016                let (w, h, add, color) = (gfx.width, gfx.height, gfx.blend == 1, gfx.color);
12017                let (cr, cg, cb) = (
12018                    (color >> 16 & 0xFF) as f32,
12019                    (color >> 8 & 0xFF) as f32,
12020                    (color & 0xFF) as f32,
12021                );
12022                let mut n = 0i32;
12023                while n < count {
12024                    let hsh = (n as u32).wrapping_mul(2654435761).wrapping_add(0x9E3779B9);
12025                    let u = ((hsh >> 8) & 1023) as f32 / 1023.0;
12026                    let v = ((hsh >> 18) & 1023) as f32 / 1023.0;
12027                    let phase = (hsh & 255) as f32 / 255.0;
12028                    let tw = (t * 3.0 + phase * std::f32::consts::TAU + n as f32).sin() * 0.5 + 0.5;
12029                    let sz = 1.5 + tw * 5.0;
12030                    let px = x + u * ww;
12031                    let py = y + v * hh;
12032                    let b = tw * tw; // sharp twinkle
12033                    let col =
12034                        (((cr * b) as u32) << 16) | (((cg * b) as u32) << 8) | ((cb * b) as u32);
12035                    crate::gfx::raster::draw_line_aa(
12036                        &mut gfx.buffer,
12037                        w,
12038                        h,
12039                        col,
12040                        add,
12041                        px - sz,
12042                        py,
12043                        px + sz,
12044                        py,
12045                    );
12046                    crate::gfx::raster::draw_line_aa(
12047                        &mut gfx.buffer,
12048                        w,
12049                        h,
12050                        col,
12051                        add,
12052                        px,
12053                        py - sz,
12054                        px,
12055                        py + sz,
12056                    );
12057                    let d = sz * 0.55;
12058                    crate::gfx::raster::draw_line_aa(
12059                        &mut gfx.buffer,
12060                        w,
12061                        h,
12062                        col,
12063                        add,
12064                        px - d,
12065                        py - d,
12066                        px + d,
12067                        py + d,
12068                    );
12069                    crate::gfx::raster::draw_line_aa(
12070                        &mut gfx.buffer,
12071                        w,
12072                        h,
12073                        col,
12074                        add,
12075                        px - d,
12076                        py + d,
12077                        px + d,
12078                        py - d,
12079                    );
12080                    n += 1;
12081                }
12082                return Ok(Value::Unit);
12083            },
12084
12085            // ══════════════════════════════════════════════════════════════════
12086            // DIALOG BUILTINS  (crates/ling-game/src/dialog.rs) — cinematic,
12087            // typed-out, colour-coded text boxes. Markup: {n}name{/} {p}place{/}
12088            // {i}item{/}, \n newline, || page break.
12089            // ══════════════════════════════════════════════════════════════════
12090            #[cfg(not(target_arch = "wasm32"))]
12091            "dialog_show" | "对话显示" | "会話表示" | "대화표시" | "แสดงบทสนทนา" | "نمایش_گفتگو" | "اعرض_الحوار" | "הצג_דיאלוג" | "مکالمہ_دکھاؤ" | "afficher_dialogue" | "dialog_anzeigen" | "показать_диалог" =>
12092            {
12093                let text = self.arg_str(&args, 0, "");
12094                let cps = self.arg_num(&args, 1, 32.0)? as f32;
12095                self.dialog = Some(ling_game::dialog::Dialog::new(&text, cps));
12096                return Ok(Value::Unit);
12097            },
12098            #[cfg(not(target_arch = "wasm32"))]
12099            "dialog_step" | "对话步进" | "会話更新" | "대화스텝" | "ก้าวบทสนทนา" | "گام_گفتگو" | "خطوة_الحوار" | "צעד_דיאלוג" | "مکالمہ_قدم" | "pas_dialogue" | "dialog_schritt" | "шаг_диалога" =>
12100            {
12101                let dt = self.arg_num(&args, 0, 0.016)? as f32;
12102                if let Some(d) = self.dialog.as_mut() {
12103                    d.update(dt);
12104                }
12105                return Ok(Value::Unit);
12106            },
12107            #[cfg(not(target_arch = "wasm32"))]
12108            "dialog_advance" | "对话推进" | "会話送り" | "대화진행" | "เลื่อนบทสนทนา" | "پیشروی_گفتگو" | "تقدّم_الحوار" | "קדם_דיאלוג" | "مکالمہ_آگے_بڑھاؤ" | "avancer_dialogue" | "dialog_weiter" | "продолжить_диалог" =>
12109            {
12110                if let Some(d) = self.dialog.as_mut() {
12111                    d.advance();
12112                }
12113                return Ok(Value::Unit);
12114            },
12115            #[cfg(not(target_arch = "wasm32"))]
12116            "dialog_active" | "对话激活" | "会話中" | "대화중" | "บทสนทนาทำงาน" | "گفتگو_فعال" | "الحوار_نشط" | "דיאלוג_פעיל" | "مکالمہ_فعال" | "dialogue_actif" | "dialog_aktiv" | "диалог_активен" =>
12117            {
12118                let a = self
12119                    .dialog
12120                    .as_ref()
12121                    .map(|d| !d.is_closed())
12122                    .unwrap_or(false);
12123                return Ok(Value::Bool(a));
12124            },
12125            #[cfg(not(target_arch = "wasm32"))]
12126            "dialog_typing" | "对话打字" | "会話タイプ中" | "대화타이핑" | "กำลังพิมพ์บทสนทนา" | "گفتگو_در_حال_تایپ" | "الحوار_يكتب" | "דיאלוג_מקליד" | "مکالمہ_ٹائپنگ" | "dialogue_frappe" | "dialog_tippen" | "диалог_печатает" =>
12127            {
12128                use ling_game::dialog::Dialog;
12129
12130                let a = self
12131                    .dialog
12132                    .as_ref()
12133                    .map(|d: &Dialog| !d.is_closed() && d.is_typing())
12134                    .unwrap_or(false);
12135                return Ok(Value::Bool(a));
12136            },
12137            #[cfg(not(target_arch = "wasm32"))]
12138            "dialog_close" | "对话关闭" | "会話閉じる" | "대화닫기" | "ปิดบทสนทนา" | "بستن_گفتگو" | "أغلق_الحوار" | "סגור_דיאלוג" | "مکالمہ_بند" | "fermer_dialogue" | "dialog_schließen" | "закрыть_диалог" =>
12139            {
12140                self.dialog = None;
12141                return Ok(Value::Unit);
12142            },
12143            // dialog_color(role, r, g, b) — role: 0 text · 1 name · 2 place · 3 item
12144            #[cfg(not(target_arch = "wasm32"))]
12145            "dialog_color" | "对话颜色" | "会話色" | "대화색" | "สีบทสนทนา" | "رنگ_گفتگو" | "لون_الحوار" | "צבע_דיאלוג" | "مکالمہ_رنگ" | "couleur_dialogue" | "dialog_farbe" | "цвет_диалога" =>
12146            {
12147                let role = (self.arg_num(&args, 0, 0.0)? as usize).min(3);
12148                let r = self.arg_num(&args, 1, 255.0)? as u32 & 0xFF;
12149                let g = self.arg_num(&args, 2, 255.0)? as u32 & 0xFF;
12150                let b = self.arg_num(&args, 3, 255.0)? as u32 & 0xFF;
12151                self.dialog_colors[role] = (r << 16) | (g << 8) | b;
12152                return Ok(Value::Unit);
12153            },
12154            // dialog_draw(x, y, w, h [, font_handle]) — draw the box + typed text
12155            #[cfg(not(target_arch = "wasm32"))]
12156            "dialog_draw" | "对话绘制" | "会話描画" | "대화그리기" | "วาดบทสนทนา" | "رسم_گفتگو" | "ارسم_الحوار" | "צייר_דיאלוג" | "مکالمہ_کھینچو" | "dessiner_dialogue" | "dialog_zeichnen" | "рисовать_диалог" =>
12157            {
12158                let x = self.arg_num(&args, 0, 40.0)? as f32;
12159                let y = self.arg_num(&args, 1, 0.0)? as f32;
12160                let ww = self.arg_num(&args, 2, 720.0)? as f32;
12161                let hh = self.arg_num(&args, 3, 150.0)? as f32;
12162                let font = self.arg_num(&args, 4, -1.0)? as i64;
12163                let t = (crate::runtime::now_secs() - self.start_time_secs) as f32;
12164                self.render_dialog(x, y, ww, hh, font, t);
12165                return Ok(Value::Unit);
12166            },
12167
12168            // text_poll() — fold newly-typed keys into the input buffer, return it.
12169            // Repeat is enabled (KeyRepeat::Yes) so holding a key/Backspace behaves
12170            // like a normal text field; length is capped so a stuck key or a runaway
12171            // script can't grow the buffer without bound.
12172            #[cfg(not(target_arch = "wasm32"))]
12173            "text_poll" => {
12174                const TEXT_BUFFER_MAX: usize = 240;
12175                // See key_down/key_pressed: our topmost fullscreen window can
12176                // be visually in front without real Win32 keyboard focus, so
12177                // WM_KEYDOWN/WM_CHAR (what minifb's get_keys_pressed reads)
12178                // never arrive. Poll the OS key-state table directly instead
12179                // — no focus required — with our own repeat-aware edge
12180                // detection (key_repeat_fire) so holding a key behaves like
12181                // the KeyRepeat::Yes path below: one char on press, then
12182                // repeats after a short hold delay.
12183                #[cfg(windows)]
12184                {
12185                    let topmost = self.gfx.borrow().topmost_window;
12186                    if topmost {
12187                        if !window_is_foreground(self.gfx.borrow().hwnd) {
12188                            return Ok(Value::Str(self.text_buffer.clone()));
12189                        }
12190                        let shift = os_key_down(VK_SHIFT);
12191                        let now = crate::runtime::now_secs();
12192                        let mut gfx = self.gfx.borrow_mut();
12193                        let back_idx = VK_BACK as usize;
12194                        let back_down = os_key_down(VK_BACK);
12195                        let back_was = gfx.raw_keys_prev[back_idx];
12196                        let (mut back_since, mut back_fire) = (
12197                            gfx.raw_keys_down_since[back_idx],
12198                            gfx.raw_keys_last_fire[back_idx],
12199                        );
12200                        if key_repeat_fire(now, back_down, back_was, &mut back_since, &mut back_fire) {
12201                            self.text_buffer.pop();
12202                        }
12203                        gfx.raw_keys_down_since[back_idx] = back_since;
12204                        gfx.raw_keys_last_fire[back_idx] = back_fire;
12205                        gfx.raw_keys_prev[back_idx] = back_down;
12206                        for &vk in TEXT_POLL_VKS {
12207                            let idx = (vk as usize) & 0xFF;
12208                            let down = os_key_down(vk);
12209                            let was = gfx.raw_keys_prev[idx];
12210                            let (mut since, mut fire) =
12211                                (gfx.raw_keys_down_since[idx], gfx.raw_keys_last_fire[idx]);
12212                            if key_repeat_fire(now, down, was, &mut since, &mut fire) {
12213                                if let Some(c) = vk_char(vk, shift) {
12214                                    if self.text_buffer.chars().count() < TEXT_BUFFER_MAX {
12215                                        self.text_buffer.push(c);
12216                                    }
12217                                }
12218                            }
12219                            gfx.raw_keys_down_since[idx] = since;
12220                            gfx.raw_keys_last_fire[idx] = fire;
12221                            gfx.raw_keys_prev[idx] = down;
12222                        }
12223                        return Ok(Value::Str(self.text_buffer.clone()));
12224                    }
12225                }
12226                let (keys, shift) = {
12227                    let gfx = self.gfx.borrow();
12228                    match gfx.window.as_ref() {
12229                        Some(w) => (
12230                            w.get_keys_pressed(minifb::KeyRepeat::Yes),
12231                            w.is_key_down(minifb::Key::LeftShift)
12232                                || w.is_key_down(minifb::Key::RightShift),
12233                        ),
12234                        None => (Vec::new(), false),
12235                    }
12236                };
12237                for k in keys {
12238                    if k == minifb::Key::Backspace {
12239                        self.text_buffer.pop();
12240                    } else if let Some(c) = key_char(k, shift) {
12241                        if self.text_buffer.chars().count() < TEXT_BUFFER_MAX {
12242                            self.text_buffer.push(c);
12243                        }
12244                    }
12245                }
12246                return Ok(Value::Str(self.text_buffer.clone()));
12247            },
12248            #[cfg(target_arch = "wasm32")]
12249            "text_poll" => {
12250                return Ok(Value::Str(self.text_buffer.clone()));
12251            },
12252            "text_get" => return Ok(Value::Str(self.text_buffer.clone())),
12253            "text_set" => {
12254                self.text_buffer = self.arg_str(&args, 0, "");
12255                return Ok(Value::Unit);
12256            },
12257            "text_clear" => {
12258                self.text_buffer.clear();
12259                return Ok(Value::Unit);
12260            },
12261            // record_frame() — append the current framebuffer as a PPM, return frame #
12262            #[cfg(not(target_arch = "wasm32"))]
12263            "record_frame" => {
12264                let n = self.record_n;
12265                let (buf, w, h) = {
12266                    let gfx = self.gfx.borrow();
12267                    (gfx.buffer.clone(), gfx.width, gfx.height)
12268                };
12269                let _ = std::fs::create_dir_all("recordings");
12270                let mut out = Vec::with_capacity(w * h * 3 + 32);
12271                out.extend_from_slice(format!("P6\n{w} {h}\n255\n").as_bytes());
12272                for px in &buf {
12273                    let p = *px;
12274                    out.push((p >> 16) as u8);
12275                    out.push((p >> 8) as u8);
12276                    out.push(p as u8);
12277                }
12278                let _ = std::fs::write(format!("recordings/frame_{n:05}.ppm"), out);
12279                self.record_n += 1;
12280                return Ok(Value::Number(n as f64));
12281            },
12282            "record_count" => return Ok(Value::Number(self.record_n as f64)),
12283            // ── screenshot(mode) → PNG in ./screenshots/ with timestamp + mode + size ──
12284            #[cfg(not(target_arch = "wasm32"))]
12285            "screenshot" | "บันทึกภาพ" | "عکس‌صفحه" | "لقطة_شاشة" | "צילום_מסך" | "اسکرین_شاٹ" => {
12286                let mode = self.arg_str(&args, 0, "game");
12287                let (buf, w, h) = {
12288                    let gfx = self.gfx.borrow();
12289                    (gfx.buffer.clone(), gfx.width, gfx.height)
12290                };
12291                let _ = std::fs::create_dir_all("screenshots");
12292                let ts = std::time::SystemTime::now()
12293                    .duration_since(std::time::UNIX_EPOCH)
12294                    .map(|d| d.as_secs())
12295                    .unwrap_or(0);
12296                let safe: String = mode
12297                    .chars()
12298                    .map(|c| if c.is_alphanumeric() { c } else { '_' })
12299                    .collect();
12300                let path = format!("screenshots/ss_{ts}_{safe}_{w}x{h}.png");
12301                let mut rgb = Vec::with_capacity(w * h * 3);
12302                for px in &buf {
12303                    let p = *px;
12304                    rgb.push((p >> 16) as u8);
12305                    rgb.push((p >> 8) as u8);
12306                    rgb.push(p as u8);
12307                }
12308                if let Some(img) = image::RgbImage::from_raw(w as u32, h as u32, rgb) {
12309                    let _ = img.save(&path);
12310                }
12311                return Ok(Value::Str(path));
12312            },
12313            // ── microphone → crypto donut ──
12314            // mic_capture() — append the latest mic samples to the record buffer
12315            // (call each frame while recording). Returns the buffer length.
12316            #[cfg(not(target_arch = "wasm32"))]
12317            "mic_capture" => {
12318                if let Some(mic) = self.mic.as_ref() {
12319                    let s = mic.latest_samples();
12320                    self.mic_buffer.extend_from_slice(&s);
12321                    let cap = 96_000usize; // ~2 s @ 48 kHz
12322                    if self.mic_buffer.len() > cap {
12323                        let drop = self.mic_buffer.len() - cap;
12324                        self.mic_buffer.drain(0..drop);
12325                    }
12326                }
12327                return Ok(Value::Number(self.mic_buffer.len() as f64));
12328            },
12329            // mic_seed() — SHA3-256 hex of the recorded audio, usable as a donut seed
12330            #[cfg(not(target_arch = "wasm32"))]
12331            "mic_seed" => {
12332                let mut bytes = Vec::with_capacity(self.mic_buffer.len() * 4);
12333                for f in &self.mic_buffer {
12334                    bytes.extend_from_slice(&f.to_le_bytes());
12335                }
12336                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(&bytes))));
12337            },
12338            #[cfg(not(target_arch = "wasm32"))]
12339            "mic_clear" => {
12340                self.mic_buffer.clear();
12341                return Ok(Value::Number(0.0));
12342            },
12343            // flush the 3-D depth queue onto the framebuffer WITHOUT presenting,
12344            // so 2-D UI drawn afterwards overlays the 3-D scene.
12345            #[cfg(not(target_arch = "wasm32"))]
12346            "flush_3d" | "render_3d" => {
12347                let mut gfx = self.gfx.borrow_mut();
12348                if !gfx.depth_queue.is_empty() {
12349                    let w = gfx.width;
12350                    let h = gfx.height;
12351                    let dt = gfx.depth_test;
12352                    let reset_z = gfx.zbuf_needs_clear;
12353                    let (bm, ba) = (gfx.blend, gfx.alpha);
12354                    let aa = gfx.antialias;
12355                    let queue = std::mem::take(&mut gfx.depth_queue);
12356                    {
12357                        let g = &mut *gfx;
12358                        let z = if dt { Some(&mut g.depth_buf) } else { None };
12359                        queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
12360                    }
12361                    gfx.zbuf_needs_clear = false;
12362                    gfx.depth_queue.set_state(bm, ba); // keep active blend/alpha across the mid-frame flush
12363                }
12364                return Ok(Value::Unit);
12365            },
12366            #[cfg(target_arch = "wasm32")]
12367            "flush_3d" | "render_3d" => {
12368                let mut gfx = self.gfx.borrow_mut();
12369                if !gfx.depth_queue.is_empty() {
12370                    let w = gfx.width;
12371                    let h = gfx.height;
12372                    let dt = gfx.depth_test;
12373                    let reset_z = gfx.zbuf_needs_clear;
12374                    let (bm, ba) = (gfx.blend, gfx.alpha);
12375                    let aa = gfx.antialias;
12376                    let queue = std::mem::take(&mut gfx.depth_queue);
12377                    {
12378                        let g = &mut *gfx;
12379                        let z = if dt { Some(&mut g.depth_buf) } else { None };
12380                        queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
12381                    }
12382                    gfx.zbuf_needs_clear = false;
12383                    gfx.depth_queue.set_state(bm, ba);
12384                }
12385                return Ok(Value::Unit);
12386            },
12387
12388            // flush_post() — flush the 3-D queue like `flush_3d`, then run the
12389            // toon post-chain (SSAO → outlines → tone ramp → bloom → FXAA) over
12390            // the SCENE immediately. `present` skips the chain this frame, so
12391            // 2-D UI drawn after this call stays exact — no bloom/blur on HUDs.
12392            "flush_post" | "post_now" | "포스트플러시" | "后期冲刷" => {
12393                let mut gfx = self.gfx.borrow_mut();
12394                if !gfx.depth_queue.is_empty() {
12395                    let w = gfx.width;
12396                    let h = gfx.height;
12397                    let dt = gfx.depth_test;
12398                    let reset_z = gfx.zbuf_needs_clear;
12399                    let (bm, ba) = (gfx.blend, gfx.alpha);
12400                    let aa = gfx.antialias;
12401                    let queue = std::mem::take(&mut gfx.depth_queue);
12402                    {
12403                        let g = &mut *gfx;
12404                        let z = if dt { Some(&mut g.depth_buf) } else { None };
12405                        queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
12406                    }
12407                    gfx.zbuf_needs_clear = false;
12408                    gfx.depth_queue.set_state(bm, ba);
12409                }
12410                gfx.toon_post_process();
12411                gfx.post_done = true;
12412                return Ok(Value::Unit);
12413            },
12414
12415            // Viscous full-screen distortion (warp/pucker/bloat, edge-wrapped). Call
12416            // after the 3-D flush and before the UI so only the world layer warps.
12417            #[cfg(not(target_arch = "wasm32"))]
12418            "screen_distort" | "บิดจอ" | "屏幕扭曲" | "画面歪み" | "화면왜곡" | "اعوجاج_صفحه" | "شوّه_الشاشة" | "עוות_מסך" | "اسکرین_ڈسٹورٹ" =>
12419            {
12420                let amount = self.arg_num(&args, 0, 8.0)? as f32;
12421                let t = self.arg_num(&args, 1, 0.0)? as f32;
12422                // optional `step` (default 1 = full res): 2 = half-res block warp
12423                // (~4× fewer warp computes, slightly softer — suits a liquid look).
12424                let step = self.arg_num(&args, 2, 1.0)?.max(1.0) as usize;
12425                let _d = std::time::Instant::now();
12426                self.gfx.borrow_mut().distort(amount, t, step);
12427                ling_phase_add(phase::DISTORT, _d.elapsed().as_nanos());
12428                return Ok(Value::Unit);
12429            },
12430
12431            "set_rim" | "设置边缘光" | "リム設定" | "림라이트" | "ตั้งขอบเรือง" | "تنظیم_نور_لبه" | "عيّن_إضاءة_الحافة" | "קבע_תאורת_קצה" | "رم_لائٹ_مقرر_کرو" | "définir_contour_lumineux" | "rimlicht_setzen" | "задать_контурный_свет" =>
12432            {
12433                let s = self.arg_num(&args, 0, 0.6)? as f32;
12434                let r = self.arg_num(&args, 1, 115.)? as f32 / 255.0;
12435                let g = self.arg_num(&args, 2, 217.)? as f32 / 255.0;
12436                let b = self.arg_num(&args, 3, 255.)? as f32 / 255.0;
12437                let mut gfx = self.gfx.borrow_mut();
12438                gfx.shade.rim = s;
12439                gfx.shade.rim_color = [r, g, b];
12440                return Ok(Value::Unit);
12441            },
12442
12443            // ══════════════════════════════════════════════════════════════════
12444            // 3-D PRIMITIVES  (src/gfx/shapes.rs)  — "Inkscape for 3-D"
12445            //   shape(cx,cy,cz,  sx,sy,sz,  rx,ry,rz,  mode,  e0,e1,e2)
12446            //     centre (cx,cy,cz), per-axis scale, Euler rotation (radians),
12447            //     mode: 0 filled · 1 wireframe · 2 both,
12448            //     e0..e2: shape-specific (segments / sides / ratio …).
12449            //   Pen colour (set_color) drives fill lighting and wireframe colour.
12450            // ══════════════════════════════════════════════════════════════════
12451            n if crate::gfx::shapes::canon(n).is_some() => {
12452                let kind = crate::gfx::shapes::canon(n).unwrap();
12453                let cx = self.arg_num(&args, 0, 0.)? as f32;
12454                let cy = self.arg_num(&args, 1, 0.)? as f32;
12455                let cz = self.arg_num(&args, 2, 0.)? as f32;
12456                let sx = self.arg_num(&args, 3, 1.)? as f32;
12457                let sy = self.arg_num(&args, 4, 1.)? as f32;
12458                let sz = self.arg_num(&args, 5, 1.)? as f32;
12459                let rx = self.arg_num(&args, 6, 0.)? as f32;
12460                let ry = self.arg_num(&args, 7, 0.)? as f32;
12461                let rz = self.arg_num(&args, 8, 0.)? as f32;
12462                let mode = self.arg_num(&args, 9, 0.)? as i32;
12463                let e0 = self.arg_num(&args, 10, 0.)? as f32;
12464                let e1 = self.arg_num(&args, 11, 0.)? as f32;
12465                let e2 = self.arg_num(&args, 12, 0.)? as f32;
12466                if let Some(mesh) = crate::gfx::shapes::build(
12467                    kind,
12468                    [cx, cy, cz, sx, sy, sz, rx, ry, rz],
12469                    e0,
12470                    e1,
12471                    e2,
12472                ) {
12473                    let mut gfx = self.gfx.borrow_mut();
12474                    gfx.emit_mesh(&mesh, mode);
12475                }
12476                return Ok(Value::Unit);
12477            },
12478
12479            _ => {},
12480        }
12481
12482        // `form` struct constructor: positional `Name(v0, v1, ...)`.
12483        if let Some(field_names) = self.structs.get(name).cloned() {
12484            if args.len() != field_names.len() {
12485                return Err(EvalErr::from(format!(
12486                    "{name} expects {} field(s), got {}",
12487                    field_names.len(),
12488                    args.len()
12489                )));
12490            }
12491            let fields = field_names.into_iter().zip(args).collect();
12492            return Ok(Value::Struct { name: name.to_string(), fields });
12493        }
12494
12495        // `choose` enum variant constructor: `Variant(...)` or `Enum::Variant(...)`.
12496        if let Some((enum_name, arity)) = self.enum_variants.get(name).cloned() {
12497            if args.len() != arity {
12498                return Err(EvalErr::from(format!(
12499                    "{name} expects {arity} value(s), got {}",
12500                    args.len()
12501                )));
12502            }
12503            let variant = name.rsplit("::").next().unwrap_or(name).to_string();
12504            return Ok(Value::Variant { enum_name, variant, payload: args });
12505        }
12506
12507        #[cfg(target_arch = "wasm32")]
12508        if let Some(v) = wasm_unsupported_builtin(name) {
12509            return Ok(v);
12510        }
12511
12512        Err(EvalErr::from(format!("unknown function '{name}'")))
12513    }
12514
12515    fn call_value(&mut self, v: Value, args: Vec<Value>) -> EvalResult {
12516        match v {
12517            Value::Fn(params, body, mut captured) => {
12518                for (p, a) in params.iter().zip(args) {
12519                    captured.insert(p.clone(), a);
12520                }
12521                match self.framed("<closure>", |me| me.exec_block(&body, &mut captured)) {
12522                    Ok(v) => Ok(v.unwrap_or(Value::Unit)),
12523                    Err(EvalErr::Return(v)) => Ok(v),
12524                    Err(e) => Err(e),
12525                }
12526            },
12527            other => Err(EvalErr::from(format!("cannot call {:?}", other))),
12528        }
12529    }
12530
12531    fn call_method(&self, recv: Value, method: &str, args: Vec<Value>) -> EvalResult {
12532        match (&recv, method) {
12533            (Value::Str(s), "is_empty" | "是空") => Ok(Value::Bool(s.is_empty())),
12534            // All of `lingfu normalize`'s per-language spellings of len/push
12535            // (see ling-fu normalize.rs alias table), not just the Chinese
12536            // ones — normalize rewrites method calls into whichever language
12537            // the project is normalized to, and any spelling missing here
12538            // makes those calls un-callable post-normalize (first hit with
12539            // `.长度()`, then again with Thai `.ความยาว()`).
12540            (Value::Str(s), "len" | "长" | "长度" | "長さ" | "길이" | "ความยาว") => Ok(Value::Number(s.len() as f64)),
12541            (Value::Str(s), "to_string" | "转文") => Ok(Value::Str(s.clone())),
12542            (Value::Str(s), "contains" | "包含") => {
12543                if let Some(Value::Str(sub)) = args.first() {
12544                    Ok(Value::Bool(s.contains(sub.as_str())))
12545                } else {
12546                    Ok(Value::Bool(false))
12547                }
12548            },
12549            (Value::Str(s), "push_str" | "推_文") => {
12550                let mut s2 = s.clone();
12551                if let Some(Value::Str(a)) = args.first() {
12552                    s2.push_str(a);
12553                }
12554                Ok(Value::Str(s2))
12555            },
12556            (Value::List(v), "len" | "长" | "长度" | "長さ" | "길이" | "ความยาว") => Ok(Value::Number(v.len() as f64)),
12557            (Value::List(v), "push" | "推" | "添加" | "追加" | "추가" | "เพิ่ม") => {
12558                let mut v2: Vec<Value> = (**v).clone();
12559                if let Some(a) = args.first() {
12560                    v2.push(a.clone());
12561                }
12562                Ok(Value::List(Rc::new(v2)))
12563            },
12564            // `form` field access: `point.x` (no-arg method == field read).
12565            (Value::Struct { fields, .. }, _) if args.is_empty() => fields
12566                .iter()
12567                .find(|(k, _)| k == method)
12568                .map(|(_, v)| v.clone())
12569                .ok_or_else(|| EvalErr::from(format!("no field '{method}' on {recv}"))),
12570            // Enum introspection: `.tag` → variant name, `.is(Name)` not needed for now.
12571            (Value::Variant { variant, .. }, "tag" | "标签" | "タグ" | "태그" | "ป้าย")
12572                if args.is_empty() =>
12573            {
12574                Ok(Value::Str(variant.clone()))
12575            },
12576            (Value::Ok(inner), _) | (Value::Err(inner), _) => Ok(*inner.clone()),
12577            _ => Err(EvalErr::from(format!("no method '{method}' on {recv}"))),
12578        }
12579    }
12580
12581    // ─── Pattern matching ─────────────────────────────────────────────────────
12582
12583    fn match_pattern(&self, pat: &Pattern, val: &Value) -> Option<Env> {
12584        match (pat, val) {
12585            (Pattern::Wildcard, _) => Some(new_env()),
12586            (Pattern::Str(s), Value::Str(v)) if s == v => Some(new_env()),
12587            (Pattern::Number(n), Value::Number(v)) if (n - v).abs() < 1e-12 => Some(new_env()),
12588            (Pattern::Bool(b), Value::Bool(v)) if b == v => Some(new_env()),
12589            (Pattern::Ident(name), _) => {
12590                let mut e = new_env();
12591                e.insert(name.clone(), val.clone());
12592                Some(e)
12593            },
12594            (Pattern::Constructor(ctor, inner_pat), _) => {
12595                let (matches, inner_val) = match (ctor.as_str(), val) {
12596                    ("ok" | "好", Value::Ok(v)) => (true, Some(v.as_ref().clone())),
12597                    ("bad" | "坏", Value::Err(v)) => (true, Some(v.as_ref().clone())),
12598                    ("ok" | "好", v) if !matches!(v, Value::Err(_)) => (true, Some(v.clone())),
12599                    _ => (false, None),
12600                };
12601                if !matches {
12602                    return None;
12603                }
12604                match (inner_pat, inner_val) {
12605                    (Some(p), Some(v)) => self.match_pattern(p, &v),
12606                    (None, _) => Some(new_env()),
12607                    (Some(p), None) => self.match_pattern(p, &Value::Unit),
12608                }
12609            },
12610            // User enum variant pattern: `Circle(r)`, `Pair(a, b)`, nullary `Origin()`.
12611            (Pattern::Variant(vname, sub_pats), Value::Variant { variant, payload, .. }) => {
12612                if vname != variant || sub_pats.len() != payload.len() {
12613                    return None;
12614                }
12615                let mut bindings = new_env();
12616                for (p, v) in sub_pats.iter().zip(payload.iter()) {
12617                    bindings.extend(self.match_pattern(p, v)?);
12618                }
12619                Some(bindings)
12620            },
12621            // A zero-payload variant pattern also matches the bare result-style `ok`/`bad`
12622            // values so `Ok()`-style patterns keep working uniformly.
12623            (Pattern::Variant(vname, sub), Value::Ok(v)) if (vname == "ok" || vname == "好") => {
12624                match sub.as_slice() {
12625                    [] => Some(new_env()),
12626                    [p] => self.match_pattern(p, v),
12627                    _ => None,
12628                }
12629            },
12630            (Pattern::Variant(vname, sub), Value::Err(v))
12631                if (vname == "bad" || vname == "坏" || vname == "err") =>
12632            {
12633                match sub.as_slice() {
12634                    [] => Some(new_env()),
12635                    [p] => self.match_pattern(p, v),
12636                    _ => None,
12637                }
12638            },
12639            _ => None,
12640        }
12641    }
12642
12643    // ─── Utilities ───────────────────────────────────────────────────────────
12644
12645    fn value_to_iter(&self, val: Value) -> Result<Vec<Value>, EvalErr> {
12646        match val {
12647            Value::List(v) => Ok(Rc::try_unwrap(v).unwrap_or_else(|rc| (*rc).clone())),
12648            Value::Str(s) => Ok(s.chars().map(|c| Value::Str(c.to_string())).collect()),
12649            Value::Number(n) => Ok((0..n as i64).map(|i| Value::Number(i as f64)).collect()),
12650            other => Err(EvalErr::from(format!("cannot iterate over {:?}", other))),
12651        }
12652    }
12653
12654    pub(crate) fn is_truthy(&self, val: &Value) -> bool {
12655        match val {
12656            Value::Bool(b) => *b,
12657            Value::Unit => false,
12658            Value::Number(n) => *n != 0.0,
12659            Value::Str(s) => !s.is_empty(),
12660            Value::List(v) => !v.is_empty(),
12661            Value::Ok(_) => true,
12662            Value::Err(_) => false,
12663            Value::Fn(_, _, _) => true,
12664            Value::Struct { .. } => true,
12665            Value::Variant { .. } => true,
12666        }
12667    }
12668
12669    fn to_number(&self, val: &Value) -> Result<f64, EvalErr> {
12670        match val {
12671            Value::Number(n) => Ok(*n),
12672            Value::Str(s) => s
12673                .parse()
12674                .map_err(|_| EvalErr::from(format!("cannot convert '{s}' to number"))),
12675            other => Err(EvalErr::from(format!("expected number, got {:?}", other))),
12676        }
12677    }
12678
12679    /// Get the n-th argument as f64, falling back to `default` if missing.
12680    fn arg_num(&self, args: &[Value], n: usize, default: f64) -> Result<f64, EvalErr> {
12681        match args.get(n) {
12682            Some(v) => self.to_number(v),
12683            None => Ok(default),
12684        }
12685    }
12686
12687    fn arg_str(&self, args: &[Value], n: usize, default: &str) -> String {
12688        args.get(n)
12689            .map(|v| v.to_string())
12690            .unwrap_or_else(|| default.to_string())
12691    }
12692
12693    /// Read a list-of-numbers argument as `Vec<f32>` (empty if absent/not a list).
12694    #[allow(dead_code)]
12695    fn arg_list_f32(&self, args: &[Value], n: usize) -> Vec<f32> {
12696        match args.get(n) {
12697            Some(Value::List(v)) => v
12698                .iter()
12699                .filter_map(|x| match x {
12700                    Value::Number(n) => Some(*n as f32),
12701                    _ => None,
12702                })
12703                .collect(),
12704            _ => Vec::new(),
12705        }
12706    }
12707
12708    /// Optional `r,g,b` colour override starting at arg `i` → packed 0x00RRGGBB,
12709    /// or `default` if those three numeric args aren't present.
12710    #[cfg(not(target_arch = "wasm32"))]
12711    fn color_at(&self, args: &[Value], i: usize, default: u32) -> u32 {
12712        match (args.get(i), args.get(i + 1), args.get(i + 2)) {
12713            (Some(a), Some(b), Some(c)) => {
12714                match (self.to_number(a), self.to_number(b), self.to_number(c)) {
12715                    (Ok(r), Ok(g), Ok(bl)) => {
12716                        ((r as u32 & 0xFF) << 16) | ((g as u32 & 0xFF) << 8) | (bl as u32 & 0xFF)
12717                    },
12718                    _ => default,
12719                }
12720            },
12721            _ => default,
12722        }
12723    }
12724
12725    /// A pitch argument: a note-name string (`"C4"`, `"A#3"`) or a numeric MIDI value.
12726    #[cfg(not(target_arch = "wasm32"))]
12727    fn pitch_arg(&self, args: &[Value], i: usize, default: i32) -> i32 {
12728        match args.get(i) {
12729            Some(Value::Str(s)) => ling_music::note::parse_pitch(s).unwrap_or(default),
12730            Some(Value::Number(n)) => *n as i32,
12731            _ => default,
12732        }
12733    }
12734
12735    /// Current mouse position + left-button-down (native window only).
12736    #[cfg(not(target_arch = "wasm32"))]
12737    fn mouse_now(&self) -> (f32, f32, bool) {
12738        let gfx = self.gfx.borrow();
12739        let (mx, my) = gfx
12740            .window
12741            .as_ref()
12742            .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
12743            .unwrap_or((0.0, 0.0));
12744        let down = gfx
12745            .window
12746            .as_ref()
12747            .map(|w| w.get_mouse_down(minifb::MouseButton::Left))
12748            .unwrap_or(false);
12749        (mx, my, down)
12750    }
12751
12752    /// Rasterize a UI [`ling_ui::widgets::Draw`] into the framebuffer: filled
12753    /// polygons via the AA scanline fill, polylines via AA lines, honouring the
12754    /// current blend mode.
12755    #[cfg(not(target_arch = "wasm32"))]
12756    fn draw_ui(&self, d: &ling_ui::widgets::Draw) {
12757        let mut gfx = self.gfx.borrow_mut();
12758        let (w, h, add) = (gfx.width, gfx.height, gfx.blend == 1);
12759        for (c, poly) in &d.fills {
12760            crate::gfx::raster::fill_contours_aa(
12761                &mut gfx.buffer,
12762                w,
12763                h,
12764                *c,
12765                add,
12766                std::slice::from_ref(poly),
12767            );
12768        }
12769        for (c, pl) in &d.strokes {
12770            for s in pl.windows(2) {
12771                crate::gfx::raster::draw_line_aa(
12772                    &mut gfx.buffer,
12773                    w,
12774                    h,
12775                    *c,
12776                    add,
12777                    s[0][0],
12778                    s[0][1],
12779                    s[1][0],
12780                    s[1][1],
12781                );
12782            }
12783        }
12784    }
12785
12786    /// Parse (dst_x, dst_y, width, height) from the first four args of a tex_* builtin.
12787    fn tex_rect(&self, args: &[Value]) -> Result<(usize, usize, usize, usize), EvalErr> {
12788        let tx = self.arg_num(args, 0, 0.0)? as usize;
12789        let ty = self.arg_num(args, 1, 0.0)? as usize;
12790        let tw = self.arg_num(args, 2, 256.0)? as usize;
12791        let th = self.arg_num(args, 3, 256.0)? as usize;
12792        Ok((tx, ty, tw.max(1), th.max(1)))
12793    }
12794
12795    pub(crate) fn apply_binop(&self, op: &BinOp, l: Value, r: Value) -> EvalResult {
12796        match op {
12797            BinOp::Add => match (l, r) {
12798                (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a + b)),
12799                (Value::Str(a), Value::Str(b)) => Ok(Value::Str(a + &b)),
12800                (Value::Str(a), b) => Ok(Value::Str(a + &b.to_string())),
12801                (a, Value::Str(b)) => Ok(Value::Str(a.to_string() + &b)),
12802                (a, b) => Err(EvalErr::from(format!("cannot add {:?} and {:?}", a, b))),
12803            },
12804            BinOp::Sub => Ok(Value::Number(self.to_number(&l)? - self.to_number(&r)?)),
12805            BinOp::Mul => Ok(Value::Number(self.to_number(&l)? * self.to_number(&r)?)),
12806            BinOp::Div => Ok(Value::Number(self.to_number(&l)? / self.to_number(&r)?)),
12807            BinOp::Rem => Ok(Value::Number(self.to_number(&l)? % self.to_number(&r)?)),
12808            BinOp::Eq => Ok(Value::Bool(values_equal(&l, &r))),
12809            BinOp::Ne => Ok(Value::Bool(!values_equal(&l, &r))),
12810            BinOp::Lt => Ok(Value::Bool(self.to_number(&l)? < self.to_number(&r)?)),
12811            BinOp::Gt => Ok(Value::Bool(self.to_number(&l)? > self.to_number(&r)?)),
12812            BinOp::Le => Ok(Value::Bool(self.to_number(&l)? <= self.to_number(&r)?)),
12813            BinOp::Ge => Ok(Value::Bool(self.to_number(&l)? >= self.to_number(&r)?)),
12814            BinOp::And => Ok(Value::Bool(self.is_truthy(&l) && self.is_truthy(&r))),
12815            BinOp::Or => Ok(Value::Bool(self.is_truthy(&l) || self.is_truthy(&r))),
12816        }
12817    }
12818
12819    fn builtin_format(&self, args: &[Value]) -> Result<String, EvalErr> {
12820        if args.is_empty() {
12821            return Ok(String::new());
12822        }
12823        let fmt = match &args[0] {
12824            Value::Str(s) => s.clone(),
12825            other => return Ok(other.to_string()),
12826        };
12827
12828        let mut result = String::new();
12829        let mut arg_idx = 1usize;
12830        let mut chars = fmt.chars().peekable();
12831        while let Some(c) = chars.next() {
12832            if c == '{' {
12833                if chars.peek() == Some(&'}') {
12834                    chars.next();
12835                    if arg_idx < args.len() {
12836                        result.push_str(&args[arg_idx].to_string());
12837                        arg_idx += 1;
12838                    }
12839                } else {
12840                    let mut spec = String::new();
12841                    for ch in chars.by_ref() {
12842                        if ch == '}' {
12843                            break;
12844                        }
12845                        spec.push(ch);
12846                    }
12847                    if arg_idx < args.len() {
12848                        if let Some(suffix) = spec.strip_prefix(":.") {
12849                            if let Value::Number(n) = &args[arg_idx] {
12850                                let prec: usize =
12851                                    suffix.trim_end_matches('f').parse().unwrap_or(2);
12852                                result.push_str(&format!("{:.prec$}", n));
12853                                arg_idx += 1;
12854                                continue;
12855                            }
12856                        }
12857                        result.push_str(&args[arg_idx].to_string());
12858                        arg_idx += 1;
12859                    }
12860                }
12861            } else {
12862                result.push(c);
12863            }
12864        }
12865        Ok(result)
12866    }
12867}
12868
12869#[cfg(not(target_arch = "wasm32"))]
12870/// Map a friendly button name (any vendor / d-pad alias) to a gamepad button.
12871#[cfg(not(target_arch = "wasm32"))]
12872fn parse_pad_button(name: &str) -> Option<ling_input::GamepadButton> {
12873    use ling_input::GamepadButton as B;
12874    Some(match name.to_ascii_lowercase().as_str() {
12875        "a" | "south" | "cross" => B::South,
12876        "b" | "east" | "circle" => B::East,
12877        "x" | "west" | "square" => B::West,
12878        "y" | "north" | "triangle" => B::North,
12879        "lb" | "l1" | "left_shoulder" => B::LeftShoulder,
12880        "rb" | "r1" | "right_shoulder" => B::RightShoulder,
12881        "lt" | "l2" | "left_trigger" => B::LeftTrigger,
12882        "rt" | "r2" | "right_trigger" => B::RightTrigger,
12883        "start" | "menu" | "options" | "démarrer" | "начать" => B::Start,
12884        "select" | "back" | "share" | "view" => B::Select,
12885        "guide" | "home" => B::Guide,
12886        "l3" | "left_stick" => B::LeftStick,
12887        "r3" | "right_stick" => B::RightStick,
12888        "up" | "dpad_up" => B::DpadUp,
12889        "down" | "dpad_down" => B::DpadDown,
12890        "left" | "dpad_left" => B::DpadLeft,
12891        "right" | "dpad_right" => B::DpadRight,
12892        _ => return None,
12893    })
12894}
12895
12896#[cfg(not(target_arch = "wasm32"))]
12897fn str_to_minifb_key(name: &str) -> Option<minifb::Key> {
12898    use minifb::Key;
12899    Some(match name {
12900        "numpad0" | "kp0" => Key::NumPad0,
12901        "numpad1" | "kp1" => Key::NumPad1,
12902        "numpad2" | "kp2" => Key::NumPad2,
12903        "numpad3" | "kp3" => Key::NumPad3,
12904        "numpad4" | "kp4" => Key::NumPad4,
12905        "numpad5" | "kp5" => Key::NumPad5,
12906        "numpad6" | "kp6" => Key::NumPad6,
12907        "numpad7" | "kp7" => Key::NumPad7,
12908        "numpad8" | "kp8" => Key::NumPad8,
12909        "numpad9" | "kp9" => Key::NumPad9,
12910        "numpad+" | "kp+" => Key::NumPadPlus,
12911        "numpad-" | "kp-" => Key::NumPadMinus,
12912        "numpad*" | "kp*" => Key::NumPadAsterisk,
12913        "numpad/" | "kp/" => Key::NumPadSlash,
12914        "left" => Key::Left,
12915        "right" => Key::Right,
12916        "up" => Key::Up,
12917        "down" => Key::Down,
12918        "space" => Key::Space,
12919        "enter" => Key::Enter,
12920        "escape" => Key::Escape,
12921        "pageup" => Key::PageUp,
12922        "pagedown" => Key::PageDown,
12923        "lshift" | "leftshift" => Key::LeftShift,
12924        "rshift" | "rightshift" => Key::RightShift,
12925        "lctrl" | "leftctrl" => Key::LeftCtrl,
12926        "rctrl" | "rightctrl" => Key::RightCtrl,
12927        "lalt" | "leftalt" => Key::LeftAlt,
12928        "ralt" | "rightalt" => Key::RightAlt,
12929        "tab" => Key::Tab,
12930        "backspace" => Key::Backspace,
12931        "delete" => Key::Delete,
12932        "insert" => Key::Insert,
12933        "home" => Key::Home,
12934        "end" => Key::End,
12935        "a" => Key::A,
12936        "b" => Key::B,
12937        "c" => Key::C,
12938        "d" => Key::D,
12939        "e" => Key::E,
12940        "f" => Key::F,
12941        "g" => Key::G,
12942        "h" => Key::H,
12943        "i" => Key::I,
12944        "j" => Key::J,
12945        "k" => Key::K,
12946        "l" => Key::L,
12947        "m" => Key::M,
12948        "n" => Key::N,
12949        "o" => Key::O,
12950        "p" => Key::P,
12951        "q" => Key::Q,
12952        "r" => Key::R,
12953        "s" => Key::S,
12954        "t" => Key::T,
12955        "u" => Key::U,
12956        "v" => Key::V,
12957        "w" => Key::W,
12958        "x" => Key::X,
12959        "y" => Key::Y,
12960        "z" => Key::Z,
12961        "0" => Key::Key0,
12962        "1" => Key::Key1,
12963        "2" => Key::Key2,
12964        "3" => Key::Key3,
12965        "4" => Key::Key4,
12966        "5" => Key::Key5,
12967        "6" => Key::Key6,
12968        "7" => Key::Key7,
12969        "8" => Key::Key8,
12970        "9" => Key::Key9,
12971        _ => return None,
12972    })
12973}
12974
12975pub(crate) fn values_equal(a: &Value, b: &Value) -> bool {
12976    match (a, b) {
12977        (Value::Number(x), Value::Number(y)) => (x - y).abs() < 1e-12,
12978        (Value::Str(x), Value::Str(y)) => x == y,
12979        (Value::Bool(x), Value::Bool(y)) => x == y,
12980        (Value::Unit, Value::Unit) => true,
12981        _ => false,
12982    }
12983}
12984
12985// Rasteriser functions live in crate::gfx::raster — imported at top of file.
12986
12987// ── Window platform helpers ────────────────────────────────────────────────────
12988
12989/// Hide the console window that the OS auto-attaches to console-subsystem
12990/// processes. No-op on non-Windows and when no console is present.
12991#[cfg(not(target_arch = "wasm32"))]
12992fn hide_console_window() {
12993    #[cfg(windows)]
12994    unsafe {
12995        extern "system" {
12996            fn GetConsoleWindow() -> isize;
12997            fn ShowWindow(hwnd: isize, nCmdShow: i32) -> i32;
12998        }
12999        let hwnd = GetConsoleWindow();
13000        if hwnd != 0 {
13001            ShowWindow(hwnd, 0); // SW_HIDE = 0
13002        }
13003    }
13004}
13005
13006/// Strip *all* window chrome from `hwnd` and make it cover the whole primary
13007/// monitor (0,0 → screen_w × screen_h), above the taskbar. This turns the
13008/// minifb window into a true borderless-fullscreen surface: no title bar, no
13009/// frame, no resize grips — there is no visible window "handle" left.
13010#[cfg(all(not(target_arch = "wasm32"), windows))]
13011fn make_borderless_fullscreen(hwnd: isize, screen_w: i32, screen_h: i32) {
13012    if hwnd == 0 {
13013        return;
13014    }
13015    unsafe {
13016        extern "system" {
13017            fn SetWindowLongPtrW(hwnd: isize, index: i32, new: isize) -> isize;
13018            fn SetWindowPos(
13019                hwnd: isize,
13020                insert_after: isize,
13021                x: i32,
13022                y: i32,
13023                cx: i32,
13024                cy: i32,
13025                flags: u32,
13026            ) -> i32;
13027            fn ShowWindow(hwnd: isize, cmd: i32) -> i32;
13028        }
13029        const GWL_STYLE: i32 = -16;
13030        const GWL_EXSTYLE: i32 = -20;
13031        // WS_POPUP (0x80000000) | WS_VISIBLE (0x10000000) — a bare top-level
13032        // window with no caption, border, or system menu.
13033        SetWindowLongPtrW(hwnd, GWL_STYLE, 0x9000_0000isize);
13034        // Clear extended edges (WS_EX_WINDOWEDGE / CLIENTEDGE / DLGMODALFRAME).
13035        SetWindowLongPtrW(hwnd, GWL_EXSTYLE, 0);
13036        // HWND_TOPMOST = -1; SWP_FRAMECHANGED (0x0020) | SWP_SHOWWINDOW (0x0040).
13037        SetWindowPos(hwnd, -1isize, 0, 0, screen_w, screen_h, 0x0020 | 0x0040);
13038        ShowWindow(hwnd, 3); // SW_MAXIMIZE-equivalent paint; 3 = SW_SHOWMAXIMIZED
13039    }
13040}
13041
13042/// Force real OS keyboard focus onto `hwnd`, not just Z-order prominence.
13043/// Windows' foreground-lock can leave a freshly-created window topmost — so
13044/// VISUALLY it covers everything — without actually handing it keyboard
13045/// focus, e.g. when launched from a terminal that still holds real focus:
13046/// clicks can nudge focus over (a more "user-driven" event) but typed keys
13047/// silently keep going to whatever app really has it, which looks exactly
13048/// like "clicking a text field doesn't focus it". AttachThreadInput is the
13049/// standard documented workaround — it lets SetForegroundWindow succeed even
13050/// under the lock by sharing input state with whichever thread currently
13051/// owns the foreground window. Call this LAST, after every other
13052/// window-visibility change for this launch (anything that shows/hides a
13053/// window afterward — e.g. hiding the launching console — can itself
13054/// reassign the foreground window and undo an earlier focus claim).
13055#[cfg(all(not(target_arch = "wasm32"), windows))]
13056fn force_window_focus(hwnd: isize) {
13057    if hwnd == 0 {
13058        return;
13059    }
13060    unsafe {
13061        extern "system" {
13062            fn GetForegroundWindow() -> isize;
13063            fn GetWindowThreadProcessId(hwnd: isize, pid: *mut u32) -> u32;
13064            fn GetCurrentThreadId() -> u32;
13065            fn AttachThreadInput(id_attach: u32, id_attach_to: u32, attach: i32) -> i32;
13066            fn SetForegroundWindow(hwnd: isize) -> i32;
13067            fn BringWindowToTop(hwnd: isize) -> i32;
13068            fn SetFocus(hwnd: isize) -> isize;
13069            fn SetActiveWindow(hwnd: isize) -> isize;
13070        }
13071        let fg = GetForegroundWindow();
13072        if fg != 0 && fg != hwnd {
13073            let mut fg_pid: u32 = 0;
13074            let fg_tid = GetWindowThreadProcessId(fg, &mut fg_pid);
13075            let my_tid = GetCurrentThreadId();
13076            if fg_tid != 0 && fg_tid != my_tid {
13077                AttachThreadInput(my_tid, fg_tid, 1);
13078                SetForegroundWindow(hwnd);
13079                BringWindowToTop(hwnd);
13080                SetFocus(hwnd);
13081                SetActiveWindow(hwnd);
13082                AttachThreadInput(my_tid, fg_tid, 0);
13083                return;
13084            }
13085        }
13086        SetForegroundWindow(hwnd);
13087        BringWindowToTop(hwnd);
13088        SetFocus(hwnd);
13089        SetActiveWindow(hwnd);
13090    }
13091}
13092
13093/// Toggle `hwnd`'s HWND_TOPMOST z-order style without moving/resizing/
13094/// activating it — used to drop the borderless-fullscreen window's topmost
13095/// flag on alt-tab (so it stops covering whatever the user switched to) and
13096/// restore it when the user switches back.
13097#[cfg(all(not(target_arch = "wasm32"), windows))]
13098fn set_window_topmost(hwnd: isize, topmost: bool) {
13099    if hwnd == 0 {
13100        return;
13101    }
13102    unsafe {
13103        extern "system" {
13104            fn SetWindowPos(
13105                hwnd: isize,
13106                insert_after: isize,
13107                x: i32,
13108                y: i32,
13109                cx: i32,
13110                cy: i32,
13111                flags: u32,
13112            ) -> i32;
13113        }
13114        let insert_after: isize = if topmost { -1 } else { -2 }; // HWND_TOPMOST / HWND_NOTOPMOST
13115        // SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE — pure z-order change,
13116        // must not steal focus back when restoring topmost on refocus.
13117        SetWindowPos(hwnd, insert_after, 0, 0, 0, 0, 0x0002 | 0x0001 | 0x0010);
13118    }
13119}
13120
13121/// Pace `win` to `vsync`'s target rate. `LING_FPS_CAP` (0 = uncapped, else an
13122/// explicit fps) always overrides; otherwise vsync-on paces to the monitor's
13123/// real refresh rate and vsync-off runs uncapped. minifb has no swap-interval
13124/// vsync (it owns no GPU present queue), so this is frame-rate pacing to the
13125/// refresh rate, not a tear-free guarantee.
13126#[cfg(not(target_arch = "wasm32"))]
13127fn apply_frame_pacing(win: &mut minifb::Window, vsync: bool) {
13128    match std::env::var("LING_FPS_CAP")
13129        .ok()
13130        .and_then(|v| v.parse::<usize>().ok())
13131    {
13132        Some(0) => win.set_target_fps(100_000),
13133        Some(cap) => win.set_target_fps(cap),
13134        None if vsync => win.set_target_fps(monitor_info().2.max(30) as usize),
13135        None => win.set_target_fps(100_000),
13136    }
13137}
13138
13139/// Primary-monitor resolution and refresh rate as `(width, height, hz)`.
13140/// `hz` falls back to 60 when the driver reports an unknown/`default` rate.
13141#[cfg(all(not(target_arch = "wasm32"), windows))]
13142fn monitor_info() -> (i32, i32, i32) {
13143    unsafe {
13144        extern "system" {
13145            fn GetSystemMetrics(index: i32) -> i32;
13146            fn GetDC(hwnd: isize) -> isize;
13147            fn ReleaseDC(hwnd: isize, hdc: isize) -> i32;
13148            fn GetDeviceCaps(hdc: isize, index: i32) -> i32;
13149        }
13150        let w = GetSystemMetrics(0).max(1); // SM_CXSCREEN
13151        let h = GetSystemMetrics(1).max(1); // SM_CYSCREEN
13152        let hdc = GetDC(0);
13153        let mut hz = if hdc != 0 { GetDeviceCaps(hdc, 116) } else { 0 }; // VREFRESH
13154        if hdc != 0 {
13155            ReleaseDC(0, hdc);
13156        }
13157        if hz <= 1 {
13158            hz = 60; // 0 or 1 means "device default" → assume 60 Hz
13159        }
13160        (w, h, hz)
13161    }
13162}
13163
13164/// Non-Windows native fallback: resolution from [`native_screen_size`]; refresh
13165/// from the active X11/RandR mode (so a 144 Hz panel drives the loop at 144),
13166/// falling back to 60 Hz when it can't be detected (Wayland, headless, macOS).
13167#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
13168fn monitor_info() -> (i32, i32, i32) {
13169    let (w, h) = native_screen_size();
13170    (w as i32, h as i32, linux_refresh_hz().unwrap_or(60))
13171}
13172
13173/// Active display refresh rate via `xrandr`. Each connected output's active
13174/// mode is the token flagged with `*` (e.g. `1920x1080 144.00*+`); we take the
13175/// max across all active outputs so a multi-monitor rig drives the loop at
13176/// its fastest panel.
13177#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
13178fn linux_refresh_hz() -> Option<i32> {
13179    let out = std::process::Command::new("xrandr")
13180        .arg("--current")
13181        .output()
13182        .ok()?;
13183    if !out.status.success() {
13184        return None;
13185    }
13186    parse_xrandr_max_hz(&String::from_utf8_lossy(&out.stdout))
13187}
13188
13189/// Pure parse used by [`linux_refresh_hz`]: the highest `*`-flagged refresh
13190/// rate across all active outputs in `xrandr --current` output.
13191#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
13192fn parse_xrandr_max_hz(text: &str) -> Option<i32> {
13193    text.split_whitespace()
13194        .filter(|tok| tok.contains('*'))
13195        .filter_map(|tok| {
13196            tok.trim_matches(|c: char| !c.is_ascii_digit() && c != '.')
13197                .parse::<f64>()
13198                .ok()
13199        })
13200        .map(|hz| hz.round() as i32)
13201        .filter(|&hz| (24..=1000).contains(&hz))
13202        .max()
13203}
13204
13205/// WASM fallback: the canvas is the display surface; assume 60 Hz.
13206#[cfg(target_arch = "wasm32")]
13207fn monitor_info() -> (i32, i32, i32) {
13208    let (w, h) = crate::gfx::webgl::canvas_size();
13209    (w as i32, h as i32, 60)
13210}
13211
13212#[cfg(all(test, not(target_arch = "wasm32"), not(windows)))]
13213mod xrandr_tests {
13214    use super::parse_xrandr_max_hz;
13215
13216    #[test]
13217    fn picks_highest_active_output() {
13218        let text = "\
13219eDP-1 connected primary 1920x1080+0+0
13220   1920x1080     60.00*+  59.94
13221DP-1 connected 2560x1440+1920+0
13222   2560x1440    144.00*+  120.00  60.00
13223";
13224        assert_eq!(parse_xrandr_max_hz(text), Some(144));
13225    }
13226
13227    #[test]
13228    fn single_output() {
13229        let text = "   1920x1080     75.00*+  60.00\n";
13230        assert_eq!(parse_xrandr_max_hz(text), Some(75));
13231    }
13232
13233    #[test]
13234    fn no_active_mode_returns_none() {
13235        let text = "eDP-1 disconnected\n";
13236        assert_eq!(parse_xrandr_max_hz(text), None);
13237    }
13238
13239    #[test]
13240    fn out_of_range_hz_filtered() {
13241        let text = "   1x1     5000.00*+\n";
13242        assert_eq!(parse_xrandr_max_hz(text), None);
13243    }
13244}
13245
13246/// Query the primary display resolution on non-Windows platforms.
13247/// Falls back to 1920×1080 if the size cannot be determined.
13248#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
13249fn native_screen_size() -> (f64, f64) {
13250    // On Linux/macOS we don't have an easy dependency-free call; return a
13251    // sensible default. Callers can always pass explicit dimensions.
13252    (1920.0, 1080.0)
13253}
13254
13255// ════════════════════════════════════════════════════════════════════════════
13256// Builtin call profiler  (env-gated, near-zero cost when off)
13257//
13258//   LING_PROFILE=1            enable per-builtin call-count + inclusive-time tally
13259//   LING_PROFILE_EVERY=N      print the report every N frames (default 240)
13260//
13261// Every builtin call funnels through `Interp::call_named` (JIT via `ling_builtin`,
13262// tree-walker directly), so this captures the full render/physics/audio builtin
13263// hot-path. Report is sorted by total time and prints calls, calls/frame,
13264// total_ms and ms/frame — the top-down "what's making so many calls" view.
13265// ════════════════════════════════════════════════════════════════════════════
13266struct LingProfileState {
13267    enabled: bool,
13268    every: u64,
13269    frames: u64,
13270    calls: std::collections::HashMap<String, (u64, u128)>, // name -> (count, nanos)
13271}
13272
13273thread_local! {
13274    static LING_PROFILE: std::cell::RefCell<LingProfileState> = std::cell::RefCell::new({
13275        let enabled = std::env::var("LING_PROFILE").map(|v| v != "0" && !v.is_empty()).unwrap_or(false);
13276        let every = std::env::var("LING_PROFILE_EVERY").ok()
13277            .and_then(|v| v.parse::<u64>().ok()).filter(|&n| n > 0).unwrap_or(240);
13278        if enabled {
13279            eprintln!("[ling-profile] ON — report every {every} frames (set LING_PROFILE_EVERY to change)");
13280        }
13281        LingProfileState { enabled, every, frames: 0, calls: std::collections::HashMap::new() }
13282    });
13283}
13284
13285#[inline]
13286fn ling_profile_enabled() -> bool {
13287    LING_PROFILE.with(|p| p.borrow().enabled)
13288}
13289
13290thread_local! {
13291    static LING_FPS: std::cell::RefCell<(bool, f64, u32, f64)> = std::cell::RefCell::new(
13292        (std::env::var("LING_FPS").map(|v| v != "0" && !v.is_empty()).unwrap_or(false), 0.0, 0, 0.0)
13293    );
13294}
13295
13296#[cfg(not(target_arch = "wasm32"))]
13297fn ling_fps_tick() {
13298    LING_FPS.with(|s| {
13299        let mut s = s.borrow_mut();
13300        if !s.0 {
13301            return;
13302        }
13303        let now = crate::runtime::now_secs();
13304        if s.1 > 0.0 {
13305            s.3 += now - s.1;
13306            s.2 += 1;
13307            if s.2 >= 120 {
13308                let avg = s.3 / s.2 as f64;
13309                eprintln!(
13310                    "[fps] {:.1} fps  ({:.2} ms/frame, wall, {} frames)",
13311                    1.0 / avg,
13312                    avg * 1000.0,
13313                    s.2
13314                );
13315                s.2 = 0;
13316                s.3 = 0.0;
13317            }
13318        }
13319        s.1 = now;
13320    });
13321}
13322
13323// Coarse render-pipeline timers (set LING_PHASE=1). Each accumulates wall-time
13324// per frame at flush/present granularity, so the cost is negligible. Reports the
13325// software-rasteriser breakdown the builtin profiler can't separate (the work is
13326// all inside the `present`/`flush_3d` builtins).
13327thread_local! {
13328    static LING_PHASE: std::cell::RefCell<(bool, u64, [u128; 5])> = std::cell::RefCell::new(
13329        (std::env::var_os("LING_PHASE").is_some(), 0, [0; 5])
13330    );
13331}
13332
13333/// Phase indices for [`ling_phase_add`].
13334pub mod phase {
13335    pub const FLUSH: usize = 0;
13336    pub const TOON: usize = 1;
13337    pub const BLIT: usize = 2;
13338    pub const DISTORT: usize = 3;
13339    pub const SORT: usize = 4;
13340}
13341
13342#[cfg(not(target_arch = "wasm32"))]
13343#[inline]
13344pub fn ling_phase_add(idx: usize, nanos: u128) {
13345    LING_PHASE.with(|p| {
13346        let mut p = p.borrow_mut();
13347        if p.0 {
13348            p.2[idx] += nanos;
13349        }
13350    });
13351}
13352
13353#[cfg(not(target_arch = "wasm32"))]
13354fn ling_phase_frame() {
13355    LING_PHASE.with(|p| {
13356        let mut p = p.borrow_mut();
13357        if !p.0 {
13358            return;
13359        }
13360        p.1 += 1;
13361        if p.1 >= 120 {
13362            let f = p.1 as f64;
13363            let ms = |i: usize| p.2[i] as f64 / 1e6 / f;
13364            eprintln!(
13365                "[phase] sort={:.2} flush={:.2} toon={:.2} blit={:.2} distort={:.2} ms/frame",
13366                ms(phase::SORT),
13367                ms(phase::FLUSH),
13368                ms(phase::TOON),
13369                ms(phase::BLIT),
13370                ms(phase::DISTORT)
13371            );
13372            p.1 = 0;
13373            p.2 = [0; 5];
13374        }
13375    });
13376}
13377
13378fn ling_profile_record(name: &str, nanos: u128) {
13379    // Frame boundary = a present() call.
13380    let is_frame = matches!(
13381        name,
13382        "present" | "แสดงผล" | "gfx_present" | "show" | "显" | "呈现" | "表示" | "표시"
13383    );
13384    LING_PROFILE.with(|p| {
13385        let mut p = p.borrow_mut();
13386        let e = p.calls.entry(name.to_string()).or_insert((0, 0));
13387        e.0 += 1;
13388        e.1 += nanos;
13389        if is_frame {
13390            p.frames += 1;
13391            if p.frames % p.every == 0 {
13392                ling_profile_print(&p);
13393            }
13394        }
13395    });
13396}
13397
13398fn ling_profile_print(p: &LingProfileState) {
13399    let mut rows: Vec<(&String, u64, u128)> =
13400        p.calls.iter().map(|(n, (c, ns))| (n, *c, *ns)).collect();
13401    use std::cmp::Reverse;
13402    rows.sort_by_key(|x| Reverse(x.2)); // by total time desc
13403    let total_ns: u128 = p.calls.values().map(|(_, ns)| *ns).sum();
13404    let total_calls: u64 = p.calls.values().map(|(c, _)| *c).sum();
13405    let fr = p.frames.max(1) as f64;
13406    eprintln!(
13407        "\n┌─ LING PROFILE ── frames={} ─ builtin calls by total inclusive time ─────────────",
13408        p.frames
13409    );
13410    eprintln!(
13411        "│ {:<24} {:>9} {:>9} {:>10} {:>9} {:>6}",
13412        "builtin", "calls", "calls/fr", "total_ms", "ms/frame", "%time"
13413    );
13414    eprintln!("├──────────────────────────────────────────────────────────────────────────────");
13415    for (name, count, ns) in rows.iter().take(30) {
13416        let ms = *ns as f64 / 1e6;
13417        let pct = if total_ns > 0 {
13418            *ns as f64 / total_ns as f64 * 100.0
13419        } else {
13420            0.0
13421        };
13422        eprintln!(
13423            "│ {:<24} {:>9} {:>9.1} {:>10.1} {:>9.3} {:>5.1}%",
13424            truncate_name(name),
13425            count,
13426            *count as f64 / fr,
13427            ms,
13428            ms / fr,
13429            pct
13430        );
13431    }
13432    eprintln!("├──────────────────────────────────────────────────────────────────────────────");
13433    eprintln!(
13434        "│ TOTAL {} builtin calls, {:.1} ms over {} frames  →  {:.0} calls/frame, {:.2} ms/frame in builtins",
13435        total_calls,
13436        total_ns as f64 / 1e6,
13437        p.frames,
13438        total_calls as f64 / fr,
13439        total_ns as f64 / 1e6 / fr
13440    );
13441    eprintln!("└──────────────────────────────────────────────────────────────────────────────");
13442}
13443
13444/// Trim a builtin name to fit the report column (counts chars, good enough for
13445/// the mixed-script names).
13446fn truncate_name(s: &str) -> String {
13447    let max = 24;
13448    if s.chars().count() <= max {
13449        s.to_string()
13450    } else {
13451        let mut t: String = s.chars().take(max - 1).collect();
13452        t.push('…');
13453        t
13454    }
13455}