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    /// Persistent ML-DSA-65 (post-quantum) signing keypairs, referenced by handle.
1686    #[cfg(not(target_arch = "wasm32"))]
1687    mldsa_ids: Vec<ling_crypto::MlDsa65Keypair>,
1688    /// Editable text-input buffer (ling-ui text fields).
1689    text_buffer: String,
1690    /// Frame counter for record_frame().
1691    record_n: u32,
1692    /// Accumulated microphone samples (for turning sound into crypto donuts).
1693    #[cfg(not(target_arch = "wasm32"))]
1694    mic_buffer: Vec<f32>,
1695    /// Loaded vector UI fonts, referenced by handle (index) from `font_load`.
1696    #[cfg(not(target_arch = "wasm32"))]
1697    fonts: Vec<ling_graphics::VectorFont>,
1698    /// Loaded raster images (PNG/etc.), referenced by handle (index) from
1699    /// `image_load` — read pixel-by-pixel via `image_pixel_r/g/b/a`.
1700    images: Vec<image::RgbaImage>,
1701    /// Customizable UI colour palette (set via `ui_theme`).
1702    ui_theme: UiTheme,
1703    /// Left-mouse state on the previous frame — for widget click-edge detection.
1704    mouse_was_down: bool,
1705    /// Live music engine (decode playback + GM synth) — lazily initialised.
1706    #[cfg(not(target_arch = "wasm32"))]
1707    music: Option<ling_music::MusicEngine>,
1708    #[cfg(not(target_arch = "wasm32"))]
1709    music_init: bool,
1710    /// Decoded tracks (for analysis + playback), by `music_load` handle.
1711    tracks: Vec<ling_music::DecodedAudio>,
1712    /// Parsed `.lrc` lyrics, by `music_lrc` handle.
1713    lyrics: Vec<ling_music::Lyrics>,
1714    /// Parsed MIDI songs, by `music_midi_load` handle.
1715    midis: Vec<ling_music::MidiSong>,
1716    /// Soft bodies (deformable balls), by `soft_ball` handle.
1717    soft_bodies: Vec<ling_physics::soft::SoftBody>,
1718    /// Rigid-body world (angular dynamics), shared by `rb_*`.
1719    rigid_world: ling_physics::rigid::PhysicsWorld,
1720    /// Liquid grids (water/oil), by `liquid_new` handle.
1721    liquids: Vec<ling_physics::liquid::LiquidGrid>,
1722    meshes: Vec<crate::gfx::shapes::ColorMesh>,
1723    /// Loaded glTF models (skeleton + skin weights + animations), by `mesh_load` handle.
1724    gltf_models: std::cell::RefCell<Vec<ling_physics::gltf::GltfModel>>,
1725    /// Active cinematic dialog box (Ocarina/Majora-style), if any.
1726    dialog: Option<ling_game::dialog::Dialog>,
1727    /// Dialog highlight colours by role: text, name, place, item (0x00RRGGBB).
1728    dialog_colors: [u32; 4],
1729    /// Active user-function call frames (names), for error tracebacks.
1730    frames: Vec<String>,
1731    /// Snapshot of `frames` captured the moment a runtime error first arose
1732    /// (the deepest call). Consumed by `take_error_trace`.
1733    error_trace: Option<Vec<String>>,
1734    /// Unified input (gamepads/joysticks/VR/touch via the ling-input
1735    /// "Sensorium"). Lazily initialised on the first `pad_*` builtin call;
1736    /// `None` if no native input backend is available.
1737    #[cfg(not(target_arch = "wasm32"))]
1738    input: RefCell<Option<InputState>>,
1739    /// Routes registered by `http_route(method, path, handler)`, consumed
1740    /// by `http_serve`. Lives here (not a global, unlike `net`) because the
1741    /// handler is a `Value::Fn` closure — `Value` holds `Rc` and so can't
1742    /// safely live in a `static`.
1743    #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1744    http_routes: Vec<(String, String, Value)>,
1745    /// SQLite handle opened by `db_open` — plain rusqlite Connection, no
1746    /// pool: the interpreter is single-threaded, so one connection is both
1747    /// sufficient and contention-free.
1748    #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1749    db: Option<ling_http::rusqlite::Connection>,
1750    /// `(url_prefix, disk_dir)` pairs registered by `http_static`, consumed by
1751    /// `http_serve` — served as raw bytes, bypassing the String-only Value bridge.
1752    #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1753    http_static_dirs: Vec<(String, String)>,
1754    /// Background jobs started by `http_post_async`, polled by `http_job_poll`.
1755    /// A plain `Arc<Mutex<..>>` handle (not `Value`), so it's fine to touch from
1756    /// the background tokio task that fills in each job's result.
1757    #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1758    async_jobs: web::AsyncJobs,
1759}
1760
1761/// Live gamepad input state: a ling-input hub fed by the native `gilrs` backend.
1762#[cfg(not(target_arch = "wasm32"))]
1763struct InputState {
1764    sensorium: ling_input::Sensorium,
1765    backend: ling_input::backend::GilrsBackend,
1766}
1767
1768impl Default for Interpreter {
1769    fn default() -> Self {
1770        Self::new()
1771    }
1772}
1773
1774impl Interpreter {
1775    pub fn new() -> Self {
1776        #[cfg(not(target_arch = "wasm32"))]
1777        let audio = AudioEngine::new()
1778            .map_err(|e| eprintln!("audio init failed (no sound): {e}"))
1779            .ok();
1780        Self {
1781            globals: HashMap::new(),
1782            global_seed: new_env(),
1783            functions: FxHashMap::default(),
1784            structs: HashMap::new(),
1785            enum_variants: HashMap::new(),
1786            _modules: HashMap::new(),
1787            gfx: RefCell::new(GfxState::new()),
1788            svg: RefCell::new(None),
1789            source_dir: None,
1790            loaded_files: std::collections::HashSet::new(),
1791            #[cfg(not(target_arch = "wasm32"))]
1792            audio,
1793            #[cfg(not(target_arch = "wasm32"))]
1794            fft: RefCell::new(FftAnalyzer::new(2048, 44100)),
1795            fft_bands_cache: RefCell::new(vec![]),
1796            start_time_secs: crate::runtime::now_secs(),
1797            frame_num: 0,
1798            #[cfg(target_arch = "wasm32")]
1799            wasm_target_fps: 60.0,
1800            #[cfg(target_arch = "wasm32")]
1801            wasm_next_present_ms: 0.0,
1802            rand_state: 0x123456789ABCDEF,
1803            #[cfg(not(target_arch = "wasm32"))]
1804            mic: None,
1805            #[cfg(not(target_arch = "wasm32"))]
1806            crypto_ids: Vec::new(),
1807            #[cfg(not(target_arch = "wasm32"))]
1808            ed25519_ids: Vec::new(),
1809            #[cfg(not(target_arch = "wasm32"))]
1810            mldsa_ids: Vec::new(),
1811            text_buffer: String::new(),
1812            record_n: 0,
1813            #[cfg(not(target_arch = "wasm32"))]
1814            mic_buffer: Vec::new(),
1815            #[cfg(not(target_arch = "wasm32"))]
1816            fonts: Vec::new(),
1817            images: Vec::new(),
1818            ui_theme: UiTheme::default(),
1819            mouse_was_down: false,
1820            #[cfg(not(target_arch = "wasm32"))]
1821            music: None,
1822            #[cfg(not(target_arch = "wasm32"))]
1823            music_init: false,
1824            tracks: Vec::new(),
1825            lyrics: Vec::new(),
1826            midis: Vec::new(),
1827            soft_bodies: Vec::new(),
1828            rigid_world: ling_physics::rigid::PhysicsWorld::new(),
1829            liquids: Vec::new(),
1830            meshes: Vec::new(),
1831            gltf_models: std::cell::RefCell::new(Vec::new()),
1832            dialog: None,
1833            dialog_colors: [0xE6F2FF, 0xFFD24A, 0x4AD2FF, 0x6CFF8C], // text · name · place · item
1834            frames: Vec::new(),
1835            error_trace: None,
1836            #[cfg(not(target_arch = "wasm32"))]
1837            input: RefCell::new(None),
1838            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1839            http_routes: Vec::new(),
1840            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1841            db: None,
1842            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1843            http_static_dirs: Vec::new(),
1844            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1845            async_jobs: web::AsyncJobs::new(),
1846        }
1847    }
1848
1849    /// Lazily initialise the input system and advance it one frame; returns the
1850    /// number of connected gamepads. Call once per game-loop iteration (like a
1851    /// window update) before reading `pad_*` state.
1852    #[cfg(not(target_arch = "wasm32"))]
1853    fn pad_poll(&self) -> usize {
1854        let mut slot = self.input.borrow_mut();
1855        if slot.is_none() {
1856            match ling_input::backend::GilrsBackend::new() {
1857                Ok(backend) => {
1858                    *slot = Some(InputState { sensorium: ling_input::Sensorium::new(4), backend });
1859                },
1860                Err(_) => return 0,
1861            }
1862        }
1863        let st = slot.as_mut().unwrap();
1864        st.sensorium.begin_frame();
1865        st.sensorium.pump(&mut st.backend);
1866        st.sensorium.update(1.0 / 60.0);
1867        st.sensorium.devices.count()
1868    }
1869
1870    /// Read player `slot`'s gamepad with `f`, or return `default` if there is no
1871    /// input system / no such pad.
1872    #[cfg(not(target_arch = "wasm32"))]
1873    fn with_pad<T>(&self, slot: usize, default: T, f: impl FnOnce(&ling_input::Gamepad) -> T) -> T {
1874        let inp = self.input.borrow();
1875        match inp.as_ref().and_then(|s| s.sensorium.player(slot)) {
1876            Some(p) => f(p),
1877            None => default,
1878        }
1879    }
1880
1881    /// Take the call-stack snapshot captured at the deepest runtime error, if any.
1882    /// Frames are ordered outermost-first (entry point first, failing call last).
1883    pub fn take_error_trace(&mut self) -> Vec<String> {
1884        self.error_trace.take().unwrap_or_default()
1885    }
1886
1887    #[cfg(target_arch = "wasm32")]
1888    fn wasm_pace_frame(&mut self) {
1889        let fps = self.wasm_target_fps.max(1.0);
1890        let frame_ms = 1000.0 / fps;
1891        let now = js_sys::Date::now();
1892        if self.wasm_next_present_ms <= 0.0 {
1893            self.wasm_next_present_ms = now + frame_ms;
1894            return;
1895        }
1896
1897        let wait_ms = (self.wasm_next_present_ms - now).floor() as i32;
1898        if wait_ms > 0 {
1899            wasm_sleep_ms(wait_ms);
1900        }
1901
1902        let after = js_sys::Date::now();
1903        if after > self.wasm_next_present_ms + frame_ms * 3.0 {
1904            self.wasm_next_present_ms = after + frame_ms;
1905        } else {
1906            self.wasm_next_present_ms += frame_ms;
1907        }
1908    }
1909
1910    /// Run `body`, recording `name` as a call frame and snapshotting the stack on
1911    /// the first runtime error so a traceback can be reported.
1912    fn framed<T, F>(&mut self, name: &str, body: F) -> Result<T, EvalErr>
1913    where
1914        F: FnOnce(&mut Self) -> Result<T, EvalErr>,
1915    {
1916        self.frames.push(name.to_string());
1917        let result = body(self);
1918        if matches!(result, Err(EvalErr::Runtime(_))) && self.error_trace.is_none() {
1919            self.error_trace = Some(self.frames.clone());
1920        }
1921        self.frames.pop();
1922        result
1923    }
1924
1925    /// Render the active dialog box: beveled frame + dark fill, then the visible
1926    /// (typewriter-revealed) text word-wrapped with colour-coded runs, plus a
1927    /// blinking advance arrow once the page is fully typed.
1928    #[cfg(not(target_arch = "wasm32"))]
1929    fn render_dialog(&mut self, x: f32, y: f32, w: f32, h: f32, font: i64, t: f32) {
1930        let (runs, typing) = match &self.dialog {
1931            Some(d) if !d.is_closed() => {
1932                let runs: Vec<(String, usize, bool)> = d
1933                    .visible_runs()
1934                    .into_iter()
1935                    .map(|r| (r.text, r.role.index(), r.newline_before))
1936                    .collect();
1937                (runs, d.is_typing())
1938            },
1939            _ => return,
1940        };
1941        let colors = self.dialog_colors;
1942        // ── frame + fill ──
1943        let b = 12.0;
1944        let corners: Vec<[f32; 2]> = vec![
1945            [x + b, y],
1946            [x + w - b, y],
1947            [x + w, y + b],
1948            [x + w, y + h - b],
1949            [x + w - b, y + h],
1950            [x + b, y + h],
1951            [x, y + h - b],
1952            [x, y + b],
1953            [x + b, y],
1954        ];
1955        {
1956            let mut gfx = self.gfx.borrow_mut();
1957            let (bw, bh) = (gfx.width, gfx.height);
1958            crate::gfx::raster::fill_contours_aa(
1959                &mut gfx.buffer,
1960                bw,
1961                bh,
1962                0x0A1018,
1963                false,
1964                std::slice::from_ref(&corners),
1965            );
1966            for seg in corners.windows(2) {
1967                crate::gfx::raster::draw_line_aa(
1968                    &mut gfx.buffer,
1969                    bw,
1970                    bh,
1971                    0x00D2FF,
1972                    false,
1973                    seg[0][0],
1974                    seg[0][1],
1975                    seg[1][0],
1976                    seg[1][1],
1977                );
1978            }
1979        }
1980        // ── word-wrapped, colour-coded text ──
1981        let px = 22.0f32;
1982        let pad = 20.0f32;
1983        let line_h = px * 1.45;
1984        let mut cx = x + pad;
1985        let mut cy = y + pad;
1986        let use_font = font >= 0 && (font as usize) < self.fonts.len();
1987        for (text, role, nl) in &runs {
1988            if *nl {
1989                cx = x + pad;
1990                cy += line_h;
1991            }
1992            for word in text.split_inclusive(' ') {
1993                let wpx = if use_font {
1994                    self.fonts[font as usize].measure(word, px)
1995                } else {
1996                    ling_ui::holo::text_width(word, px * 0.6, px * 0.24)
1997                };
1998                if cx + wpx > x + w - pad && cx > x + pad + 1.0 {
1999                    cx = x + pad;
2000                    cy += line_h;
2001                }
2002                if cy + line_h > y + h {
2003                    break;
2004                }
2005                let col = colors[(*role).min(3)];
2006                if use_font {
2007                    let glyphs = self.font_layout_2d_glyphs(font as usize, cx, cy, px, word);
2008                    let mut gfx = self.gfx.borrow_mut();
2009                    let (bw, bh, add) = (gfx.width, gfx.height, gfx.blend == 1);
2010                    for contours in &glyphs {
2011                        crate::gfx::raster::fill_contours_aa(
2012                            &mut gfx.buffer,
2013                            bw,
2014                            bh,
2015                            col,
2016                            add,
2017                            contours,
2018                        );
2019                    }
2020                } else {
2021                    let segs = ling_ui::holo::text_lines(word, cx, cy, px * 0.6, px, px * 0.24);
2022                    let mut gfx = self.gfx.borrow_mut();
2023                    let (bw, bh) = (gfx.width, gfx.height);
2024                    for s in segs {
2025                        draw_line(&mut gfx.buffer, bw, bh, col, s[0], s[1], s[2], s[3]);
2026                    }
2027                }
2028                cx += wpx;
2029            }
2030        }
2031        // ── blinking advance arrow when fully typed ──
2032        if !typing && (t * 3.0).sin() > 0.0 {
2033            let ax = x + w - 26.0;
2034            let ay = y + h - 22.0;
2035            let mut gfx = self.gfx.borrow_mut();
2036            let (bw, bh) = (gfx.width, gfx.height);
2037            crate::gfx::raster::fill_contours_aa(
2038                &mut gfx.buffer,
2039                bw,
2040                bh,
2041                0x00D2FF,
2042                false,
2043                std::slice::from_ref(&vec![
2044                    [ax - 7.0, ay],
2045                    [ax + 7.0, ay],
2046                    [ax, ay + 9.0],
2047                    [ax - 7.0, ay],
2048                ]),
2049            );
2050        }
2051    }
2052
2053    /// Lazily start the music engine on first use (playback/synth need a device;
2054    /// analysis/decoding do not). Returns `false` if no audio device is available.
2055    #[cfg(not(target_arch = "wasm32"))]
2056    fn ensure_music(&mut self) -> bool {
2057        if self.music.is_some() {
2058            return true;
2059        }
2060        if self.music_init {
2061            return false;
2062        }
2063        self.music_init = true;
2064        match ling_music::MusicEngine::new() {
2065            Ok(m) => {
2066                self.music = Some(m);
2067                true
2068            },
2069            Err(e) => {
2070                eprintln!("music engine init failed (no music playback): {e}");
2071                false
2072            },
2073        }
2074    }
2075
2076    #[cfg(target_arch = "wasm32")]
2077    fn wasm_resolve_source_path(&self, path: &str) -> String {
2078        let p = path.trim();
2079        if p.is_empty() {
2080            return String::new();
2081        }
2082        if p.contains("://") || p.starts_with('/') || p.starts_with("./") || p.starts_with("../") {
2083            return p.to_string();
2084        }
2085        if let Some(d) = &self.source_dir {
2086            let base = d.to_string_lossy().replace('\\', "/");
2087            if !base.is_empty() {
2088                return format!(
2089                    "{}/{}",
2090                    base.trim_end_matches('/'),
2091                    p.trim_start_matches("./")
2092                );
2093            }
2094        }
2095        p.to_string()
2096    }
2097
2098    #[cfg(target_arch = "wasm32")]
2099    fn wasm_music_builtin(&mut self, name: &str, args: &[Value]) -> Result<Option<Value>, EvalErr> {
2100        match name {
2101            // music_load(path) -> track handle (decode from fetched bytes)
2102            "music_load" | "载入音乐" | "音楽読込" | "음악로드" | "โหลดเพลง" | "بارگذاری_موسیقی" | "تحميل_الموسيقى" | "טעינת_מוזיקה" | "موسیقی_لوڈ" | "charger_musique" | "musik_laden" | "загрузить_музыку" =>
2103            {
2104                let path = self.arg_str(args, 0, "");
2105                let resolved = self.wasm_resolve_source_path(&path);
2106                match wasm_fetch_bytes(&resolved)
2107                    .and_then(|bytes| ling_music::from_bytes(&bytes).map_err(|e| e.to_string()))
2108                {
2109                    Ok(t) => {
2110                        let id = self.tracks.len();
2111                        self.tracks.push(t);
2112                        return Ok(Some(Value::Number(id as f64)));
2113                    },
2114                    Err(e) => {
2115                        eprintln!("music_load failed ({path}): {e}");
2116                        return Ok(Some(Value::Number(-1.0)));
2117                    },
2118                }
2119            },
2120            "music_duration" | "音乐时长" | "音楽長さ" | "음악길이" | "ความยาวเพลง" | "مدت_موسیقی" | "مدة_الموسيقى" | "משך_מוזיקה" | "موسیقی_دورانیہ" | "durée_musique" | "musik_dauer" | "длительность_музыки" =>
2121            {
2122                let id = self.arg_num(args, 0, 0.0)? as i64;
2123                let d = self
2124                    .tracks
2125                    .get(id as usize)
2126                    .map(|t| t.duration)
2127                    .unwrap_or(0.0);
2128                return Ok(Some(Value::Number(d as f64)));
2129            },
2130            "music_bpm" | "节拍速度" | "テンポ" | "템포" | "จังหวะต่อนาที" | "ضربان_در_دقیقه" | "نبضات_بالدقيقة" | "פעימות_לדקה" | "بی_پی_ایم" | "bpm_musique" | "musik_bpm" | "музыка_bpm" =>
2131            {
2132                let id = self.arg_num(args, 0, 0.0)? as i64;
2133                let b = self
2134                    .tracks
2135                    .get(id as usize)
2136                    .map(|t| ling_music::analysis::bpm(&t.mono, t.rate))
2137                    .unwrap_or(0.0);
2138                return Ok(Some(Value::Number(b as f64)));
2139            },
2140            "music_key" | "调性" | "調性" | "조성" | "คีย์เพลง" | "گام_موسیقی" | "مقام_الموسيقى" | "סולם_מוזיקלי" | "موسیقی_کلید" | "tonalité_musique" | "musik_tonart" | "тональность_музыки" => {
2141                let id = self.arg_num(args, 0, 0.0)? as i64;
2142                let k = self
2143                    .tracks
2144                    .get(id as usize)
2145                    .map(|t| ling_music::analysis::key_name(&t.mono, t.rate))
2146                    .unwrap_or_default();
2147                return Ok(Some(Value::Str(k)));
2148            },
2149            "music_onsets" | "音符起点" | "オンセット" | "온셋" | "จุดเริ่มเสียง" | "آغازهای_نت" | "بدايات_النغمات" | "התחלות_תווים" | "نوٹ_شروعات" | "attaques_musique" | "musik_einsätze" | "атаки_музыки" =>
2150            {
2151                let id = self.arg_num(args, 0, 0.0)? as i64;
2152                let v = self
2153                    .tracks
2154                    .get(id as usize)
2155                    .map(|t| ling_music::analysis::onsets(&t.mono, t.rate))
2156                    .unwrap_or_default();
2157                return Ok(Some(Value::List(
2158                    v.into_iter().map(|x| Value::Number(x as f64)).collect::<Vec<_>>().into(),
2159                )));
2160            },
2161            "music_beat_grid" | "节拍网格" | "ビートグリッド" | "비트그리드" | "กริดจังหวะ" | "شبکه_ضرب" | "شبكة_الإيقاع" | "רשת_פעימות" | "بیٹ_گرڈ" | "grille_temps_musique" | "musik_taktraster" | "сетка_ритма_музыки" =>
2162            {
2163                let id = self.arg_num(args, 0, 0.0)? as i64;
2164                let beats = self
2165                    .tracks
2166                    .get(id as usize)
2167                    .map(|t| {
2168                        let b = ling_music::analysis::bpm(&t.mono, t.rate);
2169                        ling_music::analysis::beat_grid(&t.mono, t.rate, b)
2170                    })
2171                    .unwrap_or_default();
2172                return Ok(Some(Value::List(
2173                    beats.into_iter().map(|x| Value::Number(x as f64)).collect::<Vec<_>>().into(),
2174                )));
2175            },
2176            "music_lrc" | "载入歌词" | "歌詞読込" | "가사로드" | "โหลดเนื้อเพลง" | "بارگذاری_متن_ترانه" | "تحميل_كلمات_الأغنية" | "טעינת_מילות_שיר" | "گیت_متن_لوڈ" | "lrc_musique" | "musik_lrc" | "lrc_музыки" =>
2177            {
2178                let path = self.arg_str(args, 0, "");
2179                let resolved = self.wasm_resolve_source_path(&path);
2180                match wasm_fetch_text(&resolved) {
2181                    Ok(text) => {
2182                        let id = self.lyrics.len();
2183                        self.lyrics.push(ling_music::Lyrics::parse(&text));
2184                        return Ok(Some(Value::Number(id as f64)));
2185                    },
2186                    Err(e) => {
2187                        eprintln!("music_lrc failed ({path}): {e}");
2188                        return Ok(Some(Value::Number(-1.0)));
2189                    },
2190                }
2191            },
2192            "music_lyric" | "当前歌词" | "現在歌詞" | "현재가사" | "เนื้อเพลงปัจจุบัน" | "متن_ترانه_فعلی" | "كلمات_الأغنية_الحالية" | "מילות_שיר_נוכחיות" | "موجودہ_گیت_متن" | "paroles_musique" | "musik_liedtext" | "текст_песни" =>
2193            {
2194                let id = self.arg_num(args, 0, 0.0)? as i64;
2195                let t = self.arg_num(args, 1, 0.0)? as f32;
2196                let line = self
2197                    .lyrics
2198                    .get(id as usize)
2199                    .map(|l| l.line_at(t).to_string())
2200                    .unwrap_or_default();
2201                return Ok(Some(Value::Str(line)));
2202            },
2203            "music_midi_load" | "载入MIDI" | "MIDI読込" | "미디로드" | "โหลดมิดี" | "بارگذاری_MIDI" | "تحميل_MIDI" | "טעינת_MIDI" | "MIDI_لوڈ" | "charger_midi_musique" | "musik_midi_laden" | "загрузить_midi_музыки" =>
2204            {
2205                let path = self.arg_str(args, 0, "");
2206                let resolved = self.wasm_resolve_source_path(&path);
2207                match wasm_fetch_bytes(&resolved).and_then(|bytes| {
2208                    ling_music::midi::from_bytes(&bytes).map_err(|e| e.to_string())
2209                }) {
2210                    Ok(m) => {
2211                        let id = self.midis.len();
2212                        self.midis.push(m);
2213                        return Ok(Some(Value::Number(id as f64)));
2214                    },
2215                    Err(e) => {
2216                        eprintln!("music_midi_load failed ({path}): {e}");
2217                        return Ok(Some(Value::Number(-1.0)));
2218                    },
2219                }
2220            },
2221            "music_midi_count" | "MIDI数量" | "MIDI数" | "미디수" | "จำนวนมิดี" | "تعداد_MIDI" | "عدد_MIDI" | "מספר_MIDI" | "MIDI_تعداد" | "nombre_midi_musique" | "musik_midi_anzahl" | "число_midi_музыки" =>
2222            {
2223                let id = self.arg_num(args, 0, 0.0)? as i64;
2224                let n = self
2225                    .midis
2226                    .get(id as usize)
2227                    .map(|m| m.notes.len())
2228                    .unwrap_or(0);
2229                return Ok(Some(Value::Number(n as f64)));
2230            },
2231            "music_midi_notes" | "MIDI音符" | "MIDIノート" | "미디음표" | "โน้ตมิดี" | "نت‌های_MIDI" | "نغمات_MIDI" | "תווי_MIDI" | "MIDI_نوٹس" | "notes_midi_musique" | "musik_midi_noten" | "ноты_midi_музыки" =>
2232            {
2233                let id = self.arg_num(args, 0, 0.0)? as i64;
2234                let mut out = Vec::new();
2235                if let Some(m) = self.midis.get(id as usize) {
2236                    for n in &m.notes {
2237                        out.push(Value::Number(n.time as f64));
2238                        out.push(Value::Number(n.midi as f64));
2239                    }
2240                }
2241                return Ok(Some(Value::List(out.into())));
2242            },
2243            "music_midi_bars" | "MIDI音条" | "MIDIバー" | "미디바" | "แท่งมิดี" | "میله‌های_MIDI" | "أعمدة_MIDI" | "עמודות_MIDI" | "MIDI_بارز" | "mesures_midi_musique" | "musik_midi_takte" | "такты_midi_музыки" =>
2244            {
2245                let id = self.arg_num(args, 0, 0.0)? as i64;
2246                let mut out = Vec::new();
2247                if let Some(m) = self.midis.get(id as usize) {
2248                    for n in &m.notes {
2249                        out.push(Value::Number(n.time as f64));
2250                        out.push(Value::Number(n.midi as f64));
2251                        out.push(Value::Number(n.dur as f64));
2252                    }
2253                }
2254                return Ok(Some(Value::List(out.into())));
2255            },
2256            "music_judge" | "判定" | "判定する" | "판정" | "ตัดสินจังหวะ" | "داوری_ضرب" | "حكم_الإيقاع" | "שיפוט_קצב" | "بیٹ_فیصلہ" | "juger_musique" | "musik_bewerten" | "оценить_музыку" =>
2257            {
2258                let delta_ms = self.arg_num(args, 0, 9999.0)? as f32;
2259                return Ok(Some(Value::Number(
2260                    ling_music::Grade::judge(delta_ms).index() as f64,
2261                )));
2262            },
2263            "music_grade_name" | "判定名" | "判定名称" | "판정이름" | "ชื่อการตัดสิน" | "نام_رتبه" | "اسم_التقييم" | "שם_דירוג" | "گریڈ_نام" | "nom_grade_musique" | "musik_bewertungsname" | "имя_оценки_музыки" =>
2264            {
2265                let idx = self.arg_num(args, 0, 4.0)? as i32;
2266                return Ok(Some(Value::Str(
2267                    ling_music::Grade::from_index(idx).name().to_string(),
2268                )));
2269            },
2270            "music_note_name" | "音名" | "音名称" | "음이름" | "ชื่อโน้ต" | "نام_نت" | "اسم_النغمة" | "שם_תו" | "نوٹ_نام" | "nom_note_musique" | "musik_notenname" | "имя_ноты_музыки" =>
2271            {
2272                let hz = self.arg_num(args, 0, 0.0)? as f32;
2273                return Ok(Some(Value::Str(ling_music::note::hz_to_name(hz))));
2274            },
2275            "music_hz" | "音符频率" | "音符周波数" | "음표주파수" | "ความถี่โน้ต" | "فرکانس_نت" | "تردد_النغمة" | "תדר_תו" | "نوٹ_ہرٹز" | "hz_musique" | "musik_hz" | "музыка_гц" =>
2276            {
2277                let midi = match args.get(0) {
2278                    Some(Value::Str(s)) => ling_music::note::parse_pitch(s).unwrap_or(69),
2279                    Some(Value::Number(n)) => *n as i32,
2280                    _ => 69,
2281                };
2282                return Ok(Some(Value::Number(
2283                    ling_music::note::midi_to_hz(midi as f32) as f64,
2284                )));
2285            },
2286            "music_pitch_score" | "音准评分" | "音程スコア" | "음정점수" | "คะแนนเสียง" | "امتیاز_زیروبمی" | "درجة_طبقة_الصوت" | "ציון_גובה_צליל" | "پچ_اسکور" | "score_hauteur_musique" | "musik_tonhöhen_punktzahl" | "счёт_высоты_тона" =>
2287            {
2288                let hz = self.arg_num(args, 0, 0.0)? as f32;
2289                let target = self.arg_num(args, 1, 0.0)? as f32;
2290                return Ok(Some(Value::Number(
2291                    ling_music::karaoke::pitch_score(hz, target) as f64,
2292                )));
2293            },
2294
2295            // ── Playback ──────────────────────────────────────────────────────
2296            "music_play" | "播放音乐" | "音楽再生" | "음악재생" | "เล่นเพลง" | "پخش_موسیقی" | "شغّل_الموسيقى" | "נגן_מוזיקה" | "موسیقی_چلاؤ" | "jouer_musique" | "musik_abspielen" | "играть_музыку" =>
2297            {
2298                let id = self.arg_num(args, 0, 0.0)? as usize;
2299                if let Some(t) = self.tracks.get(id) {
2300                    crate::gfx::audio_web::play_music(id, &t.stereo, t.channels, t.rate, 1.0);
2301                }
2302                return Ok(Some(Value::Unit));
2303            },
2304            "music_pause"
2305            | "暂停音乐"
2306            | "音楽一時停止"
2307            | "음악일시정지"
2308            | "หยุดเพลงชั่วคราว"
2309            | "music_stop"
2310            | "停止音乐"
2311            | "音楽停止"
2312            | "음악정지"
2313            | "หยุดเพลง" | "مکث_موسیقی" | "ألبث_الموسيقى" | "השהה_מוזיקה" | "موسیقی_روکو_مؤقت" | "pause_musique" | "musik_pausieren" | "пауза_музыки" => {
2314                let id = self.arg_num(args, 0, 0.0)? as usize;
2315                crate::gfx::audio_web::stop_music(id);
2316                return Ok(Some(Value::Unit));
2317            },
2318            "music_seek" | "定位音乐" | "音楽シーク" | "음악탐색" | "ค้นหาเพลง" | "جستجوی_موسیقی" | "ابحث_في_الموسيقى" | "חפש_במוזיקה" | "موسیقی_تلاش" | "chercher_musique" | "musik_suchen" | "перемотать_музыку" =>
2319            {
2320                // Seek is not straightforward on AudioBufferSourceNode; no-op for now.
2321                return Ok(Some(Value::Unit));
2322            },
2323            "music_pos" | "音乐位置" | "音楽位置" | "음악위치" | "ตำแหน่งเพลง" | "موقعیت_موسیقی" | "موضع_الموسيقى" | "מיקום_מוזיקה" | "موسیقی_مقام" | "position_musique" | "musik_position" | "позиция_музыки" =>
2324            {
2325                return Ok(Some(Value::Number(
2326                    crate::gfx::audio_web::current_music_position(),
2327                )));
2328            },
2329            "music_volume" | "音乐音量" | "音楽音量" | "음악음량" | "ระดับเพลง" | "بلندی_موسیقی" | "مستوى_الموسيقى" | "עוצמת_מוזיקה" | "موسیقی_شدت" | "volume_musique" | "musik_lautstärke" | "громкость_музыки" =>
2330            {
2331                let vol = self.arg_num(args, 0, 0.8)? as f32;
2332                // Apply to the most-recently started slot (slot 0 is typical).
2333                crate::gfx::audio_web::set_music_volume(0, vol);
2334                return Ok(Some(Value::Unit));
2335            },
2336
2337            // ── FFT bands at current playback position ─────────────────────
2338            "music_fft" | "音乐频谱" | "音楽スペクトル" | "음악스펙트럼" | "สเปกตรัมเพลง" | "طیف_موسیقی" | "طيف_الموسيقى" | "ספקטרום_מוזיקה" | "میوزک_اسپیکٹرم" | "fft_musique" | "musik_fft" | "fft_музыки" =>
2339            {
2340                let id = self.arg_num(args, 0, 0.0)? as usize;
2341                let nbands = self.arg_num(args, 1, 16.0)? as usize;
2342                let pos = crate::gfx::audio_web::current_music_position() as f32;
2343                let bands = if let Some(t) = self.tracks.get(id) {
2344                    ling_music::analysis::fft_bands_at_pos(&t.mono, t.rate, pos, nbands)
2345                } else {
2346                    vec![0.0f32; nbands]
2347                };
2348                return Ok(Some(Value::List(
2349                    bands.into_iter().map(|x| Value::Number(x as f64)).collect::<Vec<_>>().into(),
2350                )));
2351            },
2352
2353            _ => {},
2354        }
2355        Ok(None)
2356    }
2357
2358    /// Lay out `text` for font `id` at size `px`, returning every glyph contour as
2359    /// a screen-space polyline (x→right, y→down). `(x, y)` is the text box top-left;
2360    /// the baseline is placed `ascent*px` below it. Curves are flattened to 0.3 px.
2361    #[cfg(not(target_arch = "wasm32"))]
2362    fn font_layout_2d(
2363        &mut self,
2364        id: usize,
2365        x: f32,
2366        y: f32,
2367        px: f32,
2368        text: &str,
2369    ) -> Vec<Vec<[f32; 2]>> {
2370        let mut out = Vec::new();
2371        for g in self.font_layout_2d_glyphs(id, x, y, px, text) {
2372            out.extend(g);
2373        }
2374        out
2375    }
2376
2377    /// Same as [`font_layout_2d`] but grouped per glyph (so a fill can apply the
2378    /// non-zero winding rule within each glyph, preserving interior holes).
2379    #[cfg(not(target_arch = "wasm32"))]
2380    fn font_layout_2d_glyphs(
2381        &mut self,
2382        id: usize,
2383        x: f32,
2384        y: f32,
2385        px: f32,
2386        text: &str,
2387    ) -> Vec<Vec<Vec<[f32; 2]>>> {
2388        let font = &mut self.fonts[id];
2389        let asc = font.ascent();
2390        let tol = 0.3 / px;
2391        let mut pen = 0.0f32;
2392        let mut glyphs = Vec::new();
2393        for ch in text.chars() {
2394            let go = font.glyph_outline(ch, tol);
2395            let mut contours = Vec::with_capacity(go.polylines.len());
2396            for pl in &go.polylines {
2397                let mapped: Vec<[f32; 2]> = pl
2398                    .iter()
2399                    .map(|p| [x + (pen + p[0]) * px, y + (asc - p[1]) * px])
2400                    .collect();
2401                contours.push(mapped);
2402            }
2403            glyphs.push(contours);
2404            pen += go.advance;
2405        }
2406        glyphs
2407    }
2408
2409    /// Register every item (functions, structs, globals) and evaluate the
2410    /// non-`do` globals into `global_seed`, WITHOUT running the entry. Used to
2411    /// prime the JIT's fallback interpreter so cranelift-skipped (oversized)
2412    /// functions can still be interpreted with full access to globals + peers.
2413    pub fn register_program(&mut self, program: &Program) -> Result<(), String> {
2414        for item in &program.items {
2415            self.register_item("", item)?;
2416        }
2417        let mut env = new_env();
2418        let non_do: Vec<_> = self
2419            .globals
2420            .iter()
2421            .filter(|(_, e)| !matches!(e, Expr::Do(_)))
2422            .map(|(k, e)| (k.clone(), e.clone()))
2423            .collect();
2424        let mut pending: Vec<(String, Expr)> = Vec::new();
2425        for (k, expr) in &non_do {
2426            let mut tmp = new_env();
2427            if let Ok(v) = self.eval_expr(expr, &mut tmp) {
2428                env.insert(k.clone(), v);
2429            } else {
2430                pending.push((k.clone(), expr.clone()));
2431            }
2432        }
2433        for (k, expr) in &pending {
2434            let mut tmp = env.clone();
2435            if let Ok(v) = self.eval_expr(expr, &mut tmp) {
2436                env.insert(k.clone(), v);
2437            }
2438        }
2439        self.global_seed = env;
2440        Ok(())
2441    }
2442
2443    pub fn run_program(&mut self, program: &Program) -> Result<(), String> {
2444        self.register_program(program)?;
2445        let entry = self
2446            .find_entry()
2447            .ok_or("no entry point — need `bind start = do {...}` or `ผูก เริ่ม = ทำ {...}`")?;
2448        let mut env = self.global_seed.clone();
2449        self.framed("start", |me| me.eval_expr(&entry, &mut env))
2450            .map(|_| ())
2451            .map_err(|e| match e {
2452                EvalErr::Runtime(s) => s,
2453                EvalErr::Return(_) => "unexpected top-level return".to_string(),
2454                EvalErr::Break => "unexpected break at top level".to_string(),
2455            })
2456    }
2457
2458    fn register_item(&mut self, ns: &str, item: &Item) -> Result<(), String> {
2459        match item {
2460            Item::Bind(name, expr) => {
2461                let key = if ns.is_empty() {
2462                    name.clone()
2463                } else {
2464                    format!("{ns}::{name}")
2465                };
2466                self.globals.insert(key, expr.clone());
2467            },
2468            Item::Fn(def) => {
2469                let key = if ns.is_empty() {
2470                    def.name.clone()
2471                } else {
2472                    format!("{ns}::{}", def.name)
2473                };
2474                self.functions.insert(key, Rc::new(def.clone()));
2475            },
2476            Item::Mod(name, body) => {
2477                let child_ns = if ns.is_empty() {
2478                    name.clone()
2479                } else {
2480                    format!("{ns}::{name}")
2481                };
2482                for child in body {
2483                    self.register_item(&child_ns, child)?;
2484                }
2485            },
2486            Item::TypeAlias(_, _) => {},
2487            Item::Struct(name, fields) => {
2488                self.structs.insert(name.clone(), fields.clone());
2489                if !ns.is_empty() {
2490                    self.structs.insert(format!("{ns}::{name}"), fields.clone());
2491                }
2492            },
2493            Item::Enum(name, variants) => {
2494                for v in variants {
2495                    self.enum_variants
2496                        .insert(v.name.clone(), (name.clone(), v.arity));
2497                    self.enum_variants
2498                        .insert(format!("{name}::{}", v.name), (name.clone(), v.arity));
2499                    if !ns.is_empty() {
2500                        self.enum_variants
2501                            .insert(format!("{ns}::{name}::{}", v.name), (name.clone(), v.arity));
2502                    }
2503                }
2504            },
2505            Item::Use { path, alias } => {
2506                self.load_module(path, alias.as_deref(), ns)?;
2507            },
2508        }
2509        Ok(())
2510    }
2511
2512    /// Resolve `path` relative to `source_dir`, load and parse it, then
2513    /// register all its definitions.  If `alias` is given, every name is
2514    /// prefixed with `<parent_ns>::<alias>`.  Circular imports are silently
2515    /// skipped.
2516    fn load_module(
2517        &mut self,
2518        path: &str,
2519        alias: Option<&str>,
2520        parent_ns: &str,
2521    ) -> Result<(), String> {
2522        // ── Wasm32: no filesystem — use the pre-registered module registry ──
2523        #[cfg(target_arch = "wasm32")]
2524        let (source, sub_dir) = {
2525            // Skip if already loaded (circular import guard)
2526            if self.loaded_files.contains(path) {
2527                return Ok(());
2528            }
2529            self.loaded_files.insert(path.to_string());
2530
2531            let src = crate::runtime::get_wasm_module(path)
2532                .or_else(|| crate::runtime::get_wasm_module(&format!("{}.ling", path)))
2533                .ok_or_else(|| format!("use: cannot find module '{path}'"))?;
2534            (src, None::<std::path::PathBuf>)
2535        };
2536
2537        // ── Native: resolve against filesystem ──
2538        #[cfg(not(target_arch = "wasm32"))]
2539        let (source, sub_dir) = {
2540            let base_dir = self
2541                .source_dir
2542                .clone()
2543                .unwrap_or_else(|| std::path::PathBuf::from("."));
2544            let raw = std::path::Path::new(path);
2545            let candidates: Vec<std::path::PathBuf> = vec![
2546                base_dir.join(format!("{}.ling", path)),
2547                base_dir.join(format!("{}.灵", path)),
2548                base_dir.join(format!("{}.령", path)),
2549                base_dir.join(format!("{}.霊", path)),
2550                base_dir.join(format!("{}.ลิง", path)),
2551                base_dir.join(raw),
2552                std::path::PathBuf::from(format!("{}.ling", path)),
2553                std::path::PathBuf::from(path),
2554            ];
2555
2556            let resolved = candidates
2557                .into_iter()
2558                .find(|p| p.exists())
2559                .ok_or_else(|| format!("use: cannot find module '{path}'"))?;
2560
2561            let canonical = resolved
2562                .canonicalize()
2563                .unwrap_or_else(|_| resolved.clone())
2564                .to_string_lossy()
2565                .to_string();
2566
2567            // Skip if already loaded (circular import guard)
2568            if self.loaded_files.contains(&canonical) {
2569                return Ok(());
2570            }
2571            self.loaded_files.insert(canonical.clone());
2572
2573            let src = std::fs::read_to_string(&resolved)
2574                .map_err(|e| format!("use: failed to read '{path}': {e}"))?;
2575            let dir = resolved.parent().map(|p| p.to_path_buf());
2576            (src, dir)
2577        };
2578
2579        let program = crate::parser::parse(&source)
2580            .map_err(|e| format!("use: parse error in '{path}': {e}"))?;
2581
2582        // Compute target namespace: parent_ns :: alias (or just alias, or just parent_ns)
2583        let target_ns = match (parent_ns.is_empty(), alias) {
2584            (_, Some(a)) if !parent_ns.is_empty() => format!("{parent_ns}::{a}"),
2585            (_, Some(a)) => a.to_string(),
2586            (false, None) => parent_ns.to_string(),
2587            (true, None) => String::new(),
2588        };
2589
2590        // Save/restore source_dir for nested relative imports
2591        let prev_dir = self.source_dir.clone();
2592        self.source_dir = sub_dir;
2593
2594        for item in &program.items {
2595            self.register_item(&target_ns, item)?;
2596        }
2597
2598        self.source_dir = prev_dir;
2599        Ok(())
2600    }
2601
2602    fn find_entry(&self) -> Option<Expr> {
2603        // Known entry-point names across supported human languages.
2604        for key in crate::entry::ENTRY_NAMES {
2605            if let Some(e) = self.globals.get(*key) {
2606                return Some(e.clone());
2607            }
2608        }
2609        self.globals
2610            .values()
2611            .find(|e| matches!(e, Expr::Do(_)))
2612            .cloned()
2613    }
2614
2615    // ─── Expression evaluation ────────────────────────────────────────────────
2616
2617    fn eval_expr(&mut self, expr: &Expr, env: &mut Env) -> EvalResult {
2618        match expr {
2619            Expr::Str(s) => Ok(Value::Str(s.clone())),
2620            Expr::Number(n) => Ok(Value::Number(*n)),
2621            Expr::Bool(b) => Ok(Value::Bool(*b)),
2622            Expr::Unit => Ok(Value::Unit),
2623            Expr::Array(elems) => {
2624                let vs: Vec<_> = elems
2625                    .iter()
2626                    .map(|e| self.eval_expr(e, env))
2627                    .collect::<Result<_, _>>()?;
2628                Ok(Value::List(Rc::new(vs)))
2629            },
2630
2631            Expr::Ident(name) => self.lookup(name, env),
2632
2633            Expr::Path(segs) => {
2634                if segs.len() == 1 {
2635                    return self.lookup(&segs[0], env);
2636                }
2637                Ok(Value::Str(segs.join("::")))
2638            },
2639
2640            Expr::Ref(inner) => self.eval_expr(inner, env),
2641            Expr::Await(inner) => self.eval_expr(inner, env),
2642
2643            Expr::Do(stmts) => {
2644                let mut local = env.clone();
2645                Ok(self.exec_block(stmts, &mut local)?.unwrap_or(Value::Unit))
2646            },
2647
2648            Expr::BinOp(op, lhs, rhs) => {
2649                let l = self.eval_expr(lhs, env)?;
2650                let r = self.eval_expr(rhs, env)?;
2651                self.apply_binop(op, l, r)
2652            },
2653
2654            Expr::If { cond, then, elseifs, else_body } => {
2655                let cond_val = self.eval_expr(cond, env)?;
2656                if self.is_truthy(&cond_val) {
2657                    return Ok(self.exec_block(then, env)?.unwrap_or(Value::Unit));
2658                }
2659                for (ei_cond, ei_body) in elseifs {
2660                    let ei_cond_val = self.eval_expr(ei_cond, env)?;
2661                    if self.is_truthy(&ei_cond_val) {
2662                        return Ok(self.exec_block(ei_body, env)?.unwrap_or(Value::Unit));
2663                    }
2664                }
2665                if let Some(eb) = else_body {
2666                    return Ok(self.exec_block(eb, env)?.unwrap_or(Value::Unit));
2667                }
2668                Ok(Value::Unit)
2669            },
2670
2671            Expr::While { cond, body } => {
2672                // Run the body directly in the *outer* env so that
2673                // `bind counter = counter + 1` persists across iterations,
2674                // which is the expected behaviour in a scripting language.
2675                loop {
2676                    let cv = self.eval_expr(cond, env)?;
2677                    if !self.is_truthy(&cv) {
2678                        break;
2679                    }
2680                    match self.exec_block(body, env) {
2681                        Ok(_) => {},
2682                        Err(EvalErr::Break) => break,
2683                        Err(e) => return Err(e),
2684                    }
2685                }
2686                Ok(Value::Unit)
2687            },
2688
2689            Expr::For { var, iter, body } => {
2690                let iter_val = self.eval_expr(iter, env)?;
2691                let items = self.value_to_iter(iter_val)?;
2692                for item in items {
2693                    let mut local = env.clone();
2694                    local.insert(var.clone(), item);
2695                    match self.exec_block(body, &mut local) {
2696                        Ok(_) => {},
2697                        Err(EvalErr::Break) => break,
2698                        Err(e) => return Err(e),
2699                    }
2700                }
2701                Ok(Value::Unit)
2702            },
2703
2704            Expr::Match(subject, arms) => {
2705                let subj = self.eval_expr(subject, env)?;
2706                for arm in arms {
2707                    if let Some(bindings) = self.match_pattern(&arm.pattern, &subj) {
2708                        let mut local = env.clone();
2709                        local.extend(bindings);
2710                        return self.eval_expr(&arm.body, &mut local);
2711                    }
2712                }
2713                Ok(Value::Unit)
2714            },
2715
2716            Expr::Range(lo, hi) => {
2717                let lo_v = self.eval_expr(lo, env)?;
2718                let hi_v = self.eval_expr(hi, env)?;
2719                let lo_n = self.to_number(&lo_v)? as i64;
2720                let hi_n = self.to_number(&hi_v)? as i64;
2721                Ok(Value::List(Rc::new(
2722                    (lo_n..hi_n).map(|i| Value::Number(i as f64)).collect(),
2723                )))
2724            },
2725
2726            Expr::Index(base, idx) => {
2727                let b = self.eval_expr(base, env)?;
2728                let i = self.eval_expr(idx, env)?;
2729                let n = self.to_number(&i)? as usize;
2730                match b {
2731                    Value::List(v) => v
2732                        .get(n)
2733                        .cloned()
2734                        .ok_or_else(|| EvalErr::from(format!("index {n} out of bounds"))),
2735                    Value::Str(s) => s
2736                        .chars()
2737                        .nth(n)
2738                        .map(|c| Value::Str(c.to_string()))
2739                        .ok_or_else(|| EvalErr::from(format!("index {n} out of bounds"))),
2740                    other => Err(EvalErr::from(format!("cannot index {:?}", other))),
2741                }
2742            },
2743
2744            Expr::Call(callee, args) => {
2745                let arg_vals: Vec<Value> = args
2746                    .iter()
2747                    .map(|a| self.eval_expr(a, env))
2748                    .collect::<Result<_, _>>()?;
2749                match callee.as_ref() {
2750                    Expr::Ident(name) => self.call_named(name, arg_vals, env),
2751                    Expr::Path(segs) => self.call_named(&segs.join("::"), arg_vals, env),
2752                    _ => {
2753                        let v = self.eval_expr(callee, env)?;
2754                        self.call_value(v, arg_vals)
2755                    },
2756                }
2757            },
2758
2759            Expr::MethodCall { receiver, method, args } => {
2760                let recv = self.eval_expr(receiver, env)?;
2761                let arg_vals: Vec<Value> = args
2762                    .iter()
2763                    .map(|a| self.eval_expr(a, env))
2764                    .collect::<Result<_, _>>()?;
2765                self.call_method(recv, method, arg_vals)
2766            },
2767
2768            Expr::Closure(params, body) => Ok(Value::Fn(
2769                params.clone(),
2770                vec![Stmt::Expr(*body.clone())],
2771                env.clone(),
2772            )),
2773
2774            Expr::Asm(_) => Ok(Value::Unit),
2775        }
2776    }
2777
2778    // ─── Block execution ─────────────────────────────────────────────────────
2779
2780    fn exec_block(&mut self, stmts: &[Stmt], env: &mut Env) -> Result<Option<Value>, EvalErr> {
2781        let mut last: Option<Value> = None;
2782        for stmt in stmts {
2783            match stmt {
2784                Stmt::Bind(name, expr) => {
2785                    match self.try_inplace_list_update(name, expr, env)? {
2786                        Some(v) => env.insert(name.clone(), v),
2787                        None => {
2788                            let v = self.eval_expr(expr, env)?;
2789                            env.insert(name.clone(), v)
2790                        },
2791                    };
2792                    last = None;
2793                },
2794                Stmt::Return(expr) => {
2795                    let v = self.eval_expr(expr, env)?;
2796                    return Err(EvalErr::Return(v));
2797                },
2798                Stmt::Expr(expr) => {
2799                    last = Some(self.eval_expr(expr, env)?);
2800                },
2801            }
2802        }
2803        Ok(last)
2804    }
2805
2806    /// Fast path for `bind v = list_push(v, x)` / `bind v = list_set(v, i, x)`:
2807    /// the binding aliases the same list being rebuilt, so the env copy keeps the
2808    /// `Rc` shared and `make_mut` copies the whole vector every call. Taking the
2809    /// value out of env first leaves the `Rc` unique (unless truly aliased
2810    /// elsewhere, where copy-on-write still applies), turning O(n) into O(1).
2811    /// Returns `None` to fall back to normal evaluation.
2812    fn try_inplace_list_update(
2813        &mut self,
2814        name: &str,
2815        expr: &Expr,
2816        env: &mut Env,
2817    ) -> Result<Option<Value>, EvalErr> {
2818        let Expr::Call(callee, args) = expr else { return Ok(None) };
2819        let Expr::Ident(fname) = callee.as_ref() else { return Ok(None) };
2820        let is_push = matches!(
2821            fname.as_str(),
2822            "list_push" | "เพิ่มรายการ" | "列表添加" | "リスト追加" | "목록추가"
2823        );
2824        let is_set = matches!(
2825            fname.as_str(),
2826            "list_set" | "ตั้งรายการ" | "设元素" | "要素設定" | "요소설정"
2827        );
2828        if !is_push && !is_set {
2829            return Ok(None);
2830        }
2831        // First arg must be the same variable we are binding, and the builtin
2832        // must not be shadowed by a user function.
2833        match args.first() {
2834            Some(Expr::Ident(a0)) if a0 == name => {},
2835            _ => return Ok(None),
2836        }
2837        if self.functions.contains_key(fname.as_str()) {
2838            return Ok(None);
2839        }
2840        if is_push {
2841            if args.len() != 2 {
2842                return Ok(None);
2843            }
2844            let val = self.eval_expr(&args[1], env)?;
2845            match env.remove(name) {
2846                Some(Value::List(mut v)) => {
2847                    Rc::make_mut(&mut v).push(val);
2848                    Ok(Some(Value::List(v)))
2849                },
2850                other => {
2851                    if let Some(o) = other {
2852                        env.insert(name.to_string(), o);
2853                    }
2854                    Ok(None)
2855                },
2856            }
2857        } else {
2858            if args.len() != 3 {
2859                return Ok(None);
2860            }
2861            let idx_v = self.eval_expr(&args[1], env)?;
2862            let idx = self.to_number(&idx_v).unwrap_or(0.0) as usize;
2863            let val = self.eval_expr(&args[2], env)?;
2864            match env.remove(name) {
2865                Some(Value::List(mut v)) => {
2866                    if idx < v.len() {
2867                        Rc::make_mut(&mut v)[idx] = val;
2868                    }
2869                    Ok(Some(Value::List(v)))
2870                },
2871                other => {
2872                    if let Some(o) = other {
2873                        env.insert(name.to_string(), o);
2874                    }
2875                    Ok(None)
2876                },
2877            }
2878        }
2879    }
2880
2881    // ─── Dispatch helpers ─────────────────────────────────────────────────────
2882
2883    fn lookup(&self, name: &str, env: &Env) -> EvalResult {
2884        if let Some(v) = env.get(name) {
2885            return Ok(v.clone());
2886        }
2887        // Globals are an immutable load-time snapshot shared by every call frame;
2888        // a function reads them here instead of receiving a per-call clone.
2889        if let Some(v) = self.global_seed.get(name) {
2890            return Ok(v.clone());
2891        }
2892        if self.functions.contains_key(name) {
2893            let def = &self.functions[name];
2894            return Ok(Value::Fn(def.params.clone(), def.body.clone(), new_env()));
2895        }
2896        // Bare nullary enum variant used as a value (e.g. `bind p = Origin`).
2897        if let Some((enum_name, 0)) = self.enum_variants.get(name).cloned() {
2898            let variant = name.rsplit("::").next().unwrap_or(name).to_string();
2899            return Ok(Value::Variant { enum_name, variant, payload: Vec::new() });
2900        }
2901        // Math constants usable as plain identifiers (e.g. `sin(pi)`)
2902        match name {
2903            "pi" | "π" | "พาย" | "圆周率" | "円周率" | "파이" | "پی" | "باي" | "פאי" | "پائی" | "пи" => {
2904                return Ok(Value::Number(std::f64::consts::PI))
2905            },
2906            "tau" | "τ" | "双周率" | "タウ" | "타우" | "ทาว" | "تاو" | "טאו" | "ٹاؤ" | "тау" => {
2907                return Ok(Value::Number(std::f64::consts::TAU))
2908            },
2909            _ => {},
2910        }
2911        Err(EvalErr::from(format!("undefined: '{name}'")))
2912    }
2913
2914    /// Profiling wrapper around the real dispatch. Zero overhead unless
2915    /// `LING_PROFILE` is set (one thread-local bool check per call). When on,
2916    /// it tallies per-name call count + inclusive time and, on each frame
2917    /// boundary (`present`), prints a sorted top-down report every
2918    /// `LING_PROFILE_EVERY` frames (default 240). Both the JIT (`ling_builtin` →
2919    /// here) and the tree-walker route through this, so it sees every builtin —
2920    /// in JIT mode user fns are native, so it's a clean builtin/render/physics
2921    /// profile with no nesting double-count.
2922    pub(crate) fn call_named(&mut self, name: &str, args: Vec<Value>, env: &Env) -> EvalResult {
2923        if !ling_profile_enabled() {
2924            return self.call_named_inner(name, args, env);
2925        }
2926        let t0 = crate::runtime::now_secs();
2927        let r = self.call_named_inner(name, args, env);
2928        ling_profile_record(
2929            name,
2930            ((crate::runtime::now_secs() - t0) * 1_000_000_000.0) as u128,
2931        );
2932        r
2933    }
2934
2935    fn call_named_inner(&mut self, name: &str, args: Vec<Value>, env: &Env) -> EvalResult {
2936        // A user-defined function shadows any builtin of the same name, matching
2937        // the JIT/AOT backends (which always resolve a defined function first).
2938        if let Some(def) = self.functions.get(name).cloned() {
2939            let mut call_env =
2940                FxHashMap::with_capacity_and_hasher(def.params.len(), Default::default());
2941            let _ = env; // call-site locals are intentionally NOT visible to fns
2942            for (param, arg) in def.params.iter().zip(args) {
2943                call_env.insert(param.clone(), arg);
2944            }
2945            return match self.framed(name, |me| me.exec_block(&def.body, &mut call_env)) {
2946                Ok(v) => Ok(v.unwrap_or(Value::Unit)),
2947                Err(EvalErr::Return(v)) => Ok(v),
2948                Err(e) => Err(e),
2949            };
2950        }
2951
2952        #[cfg(target_arch = "wasm32")]
2953        if let Some(v) = self.wasm_music_builtin(name, &args)? {
2954            return Ok(v);
2955        }
2956
2957        match name {
2958            // Module global read emitted by the MIR backend: resolve against the
2959            // evaluated global snapshot (functions see globals read-only).
2960            "__ling_global" => {
2961                if let Some(Value::Str(g)) = args.first() {
2962                    if let Some(v) = self.global_seed.get(g.as_str()) {
2963                        return Ok(v.clone());
2964                    }
2965                }
2966                return Ok(Value::Unit);
2967            },
2968            // ── Print ──
2969            "print" | "println" | "印" | "打印" | "印刷" | "พิมพ์" | "출력" | "вывести"
2970            | "imprimir" | "afficher" | "چاپ" | "اطبع" | "הדפס" | "چھاپو" | "drucken" | "печать" => {
2971                let s = args
2972                    .iter()
2973                    .map(|v| v.to_string())
2974                    .collect::<Vec<_>>()
2975                    .join("");
2976                println!("{s}");
2977                return Ok(Value::Unit);
2978            },
2979            // print_color(colorIdx, text...) — ANSI-coloured console line.
2980            //   colorIdx 0..7 → bright fg (90+idx): 1=red 2=green 3=yellow 4=blue 6=cyan 7=white.
2981            "print_color" | "พิมพ์สี" | "چاپ_رنگی" | "اطبع_بلون" | "הדפס_בצבע" | "رنگین_چھاپو" => {
2982                #[cfg(windows)]
2983                {
2984                    use std::sync::Once;
2985                    static VT: Once = Once::new();
2986                    VT.call_once(|| {
2987                        extern "system" {
2988                            fn GetStdHandle(n: u32) -> *mut std::ffi::c_void;
2989                            fn GetConsoleMode(h: *mut std::ffi::c_void, m: *mut u32) -> i32;
2990                            fn SetConsoleMode(h: *mut std::ffi::c_void, m: u32) -> i32;
2991                        }
2992                        unsafe {
2993                            let h = GetStdHandle(0xFFFF_FFF5u32); // STD_OUTPUT_HANDLE (-11)
2994                            let mut mode = 0u32;
2995                            if GetConsoleMode(h, &mut mode) != 0 {
2996                                SetConsoleMode(h, mode | 0x0004); // ENABLE_VIRTUAL_TERMINAL_PROCESSING
2997                            }
2998                        }
2999                    });
3000                }
3001                let col = self.arg_num(&args, 0, 7.0)? as i64;
3002                let s = args
3003                    .iter()
3004                    .skip(1)
3005                    .map(|v| v.to_string())
3006                    .collect::<Vec<_>>()
3007                    .join("");
3008                let code = 90 + col.clamp(0, 7);
3009                println!("\x1b[1;{code}m{s}\x1b[0m");
3010                return Ok(Value::Unit);
3011            },
3012            // ── Format ──
3013            "format"
3014            | "格式"
3015            | "フォーマット"
3016            | "서식"
3017            | "รูปแบบ"
3018            | "форматировать"
3019            | "formatear"
3020            | "formater" | "قالب‌بندی" | "نسّق" | "פרמט" | "فارمیٹ" | "formatieren" => {
3021                return Ok(Value::Str(self.builtin_format(&args)?));
3022            },
3023            // ── String join / concatenation ──
3024            "格式::拼接" | "format::join" => match args.first() {
3025                Some(Value::List(items)) => {
3026                    return Ok(Value::Str(items.iter().map(|v| v.to_string()).collect()));
3027                },
3028                _ => return Ok(Value::Str(self.builtin_format(&args)?)),
3029            },
3030            // ── Result constructors ──
3031            "ok" | "好" | "良し" | "좋아" | "โอเค" | "تایید" | "تمام" | "בסדר" | "ٹھیک" | "bon" | "gut" | "хорошо" => {
3032                let val = args.into_iter().next().unwrap_or(Value::Unit);
3033                return Ok(Value::Ok(Box::new(val)));
3034            },
3035            "bad" | "坏" | "err" | "悪い" | "나쁨" | "ผิด" | "بد" | "سيء" | "רע" | "برا" | "mauvais" | "schlecht" | "плохо" => {
3036                let val = args.into_iter().next().unwrap_or(Value::Unit);
3037                return Ok(Value::Err(Box::new(val)));
3038            },
3039            // ── Vec constructors ──
3040            "向量::从" | "Vec::from" => {
3041                if let Some(Value::List(v)) = args.first() {
3042                    return Ok(Value::List(v.clone()));
3043                }
3044                return Ok(Value::List(Rc::new(args)));
3045            },
3046            "向量::有容量" | "Vec::with_capacity" => {
3047                return Ok(Value::List(Rc::new(Vec::new())))
3048            },
3049            // ── Timer stubs ──
3050            "计时::获取当前小时" | "Timer::hour" => return Ok(Value::Number(14.0)),
3051            "计时::现在" | "Timer::now" => return Ok(Value::Number(1000.0)),
3052            // ── Sleep ──
3053            "sleep" | "หยุด" | "นอน" | "sleep_ms" | "睡眠" | "眠る" | "スリープ" | "잠자기"
3054            | "잠" | "流水::睡眠" | "Flow::sleep" | "خواب" | "نم" | "שינה" | "سو_جاؤ" | "dormir" | "schlafen" | "спать" => {
3055                if let Some(ms_val) = args.first() {
3056                    if let Ok(ms) = self.to_number(ms_val) {
3057                        #[cfg(target_arch = "wasm32")]
3058                        wasm_sleep_ms(ms.max(0.0) as i32);
3059                        #[cfg(not(target_arch = "wasm32"))]
3060                        std::thread::sleep(std::time::Duration::from_millis(ms as u64));
3061                    }
3062                }
3063                return Ok(Value::Unit);
3064            },
3065            // ── Flow::parallel stub ──
3066            "流水::并行" | "Flow::parallel" => {
3067                if let Some(Value::Fn(params, body, mut cap)) = args.first().cloned() {
3068                    let _ = params;
3069                    match self.exec_block(&body, &mut cap) {
3070                        Ok(Some(v)) => return Ok(v),
3071                        Ok(None) => return Ok(Value::Unit),
3072                        Err(EvalErr::Return(v)) => return Ok(v),
3073                        Err(e) => return Err(e),
3074                    }
3075                }
3076                return Ok(Value::Unit);
3077            },
3078
3079            // ══════════════════════════════════════════════════════════════════
3080            // MATH BUILTINS  (all args and results are f64)
3081            // Thai aliases: ไซน์ โคไซน์ แทนเจนต์ รากที่สอง ค่าสัมบูรณ์
3082            //               ปัดลง ปัดขึ้น ปัดเศษ ตัดทศนิยม ต่ำสุด สูงสุด
3083            //               จำกัด ยกกำลัง ลอการิทึม พาย
3084            // ══════════════════════════════════════════════════════════════════
3085
3086            // ── Trigonometry (input in radians) ──
3087            "sin" | "ไซน์" | "正弦" | "サイン" | "사인" | "سینوس" | "جا" | "סינוס" | "سائن" | "sinus" | "синус" => {
3088                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.sin()));
3089            },
3090            "cos" | "โคไซน์" | "余弦" | "コサイン" | "코사인" | "کسینوس" | "جتا" | "קוסינוס" | "کوسائن" | "cosinus" | "kosinus" | "косинус" => {
3091                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.cos()));
3092            },
3093
3094            // ── Hyperbolic functions ──
3095            // Hyperbolic tangent
3096            "tanh" | "tanhf" | "双曲正切" | "双曲線正接" | "쌍곡탄젠트" => {
3097                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.tanh()));
3098            },
3099
3100            "tan" | "แทนเจนต์" | "正切" | "タンジェント" | "탄젠트" | "تانژانت" | "ظا" | "טנגנס" | "ٹینجنٹ" | "tangente" | "tangens" | "тангенс" => {
3101                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.tan()));
3102            },
3103            "asin" | "arcsin" | "反正弦" | "アークサイン" | "아크사인" | "อาร์กไซน์" | "آرک‌سینوس" | "قوس_جا" | "ארקסינוס" | "آرک_سائن" | "arcsinus" | "arkussinus" | "арксинус" =>
3104            {
3105                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.asin()));
3106            },
3107            "acos" | "arccos" | "反余弦" | "アークコサイン" | "아크코사인" | "อาร์กโคไซน์" | "آرک‌کسینوس" | "قوس_جتا" | "ארקוקוסינוס" | "آرک_کوسائن" | "arccosinus" | "arkuskosinus" | "арккосинус" =>
3108            {
3109                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.acos()));
3110            },
3111            "atan" | "arctan" | "反正切" | "アークタンジェント" | "아크탄젠트" | "อาร์กแทนเจนต์" | "آرک‌تانژانت" | "قوس_ظا" | "ארקטנגנס" | "آرک_ٹینجنٹ" | "arctangente" | "arkustangens" | "арктангенс" =>
3112            {
3113                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.atan()));
3114            },
3115            "atan2" | "arctan2" | "反正切2" | "アークタンジェント2" | "아크탄젠트2" =>
3116            {
3117                let y = self.arg_num(&args, 0, 0.0)?;
3118                let x = self.arg_num(&args, 1, 1.0)?;
3119                return Ok(Value::Number(y.atan2(x)));
3120            },
3121
3122            // ── Roots / powers ──
3123            "sqrt" | "รากที่สอง" | "平方根" | "根" | "제곱근" | "جذر" | "שורש" | "racine_carrée" | "quadratwurzel" | "корень" => {
3124                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.sqrt()));
3125            },
3126            "cbrt" | "立方根" | "세제곱근" | "รากที่สาม" | "ریشه_سوم" | "جذر_تكعيبي" | "שורש_שלישי" | "مکعب_جذر" | "racine_cubique" | "kubikwurzel" | "кубический_корень" => {
3127                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.cbrt()));
3128            },
3129            "pow" | "ยกกำลัง" | "幂" | "べき乗" | "거듭제곱" | "توان" | "أس" | "חזקה" | "قوت" | "puissance" | "potenz" | "степень" => {
3130                let base = self.arg_num(&args, 0, 0.0)?;
3131                let exp = self.arg_num(&args, 1, 1.0)?;
3132                return Ok(Value::Number(base.powf(exp)));
3133            },
3134            "exp" | "指数" | "指数関数" | "지수" => {
3135                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.exp()));
3136            },
3137            "hypot" | "斜边" | "斜辺" | "빗변" => {
3138                let x = self.arg_num(&args, 0, 0.0)?;
3139                let y = self.arg_num(&args, 1, 0.0)?;
3140                return Ok(Value::Number(x.hypot(y)));
3141            },
3142
3143            // ── Logarithms ──
3144            "ln" | "log" | "ลอการิทึม" | "对数" | "対数" | "로그" | "لگاریتم_طبیعی" | "لوغاريتم_طبيعي" | "לוגריתם_טבעי" | "فطری_لوگ" => {
3145                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.ln()));
3146            },
3147            "log2" | "对数2" | "対数2" | "로그2" => {
3148                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.log2()));
3149            },
3150            "log10" | "对数10" | "対数10" | "로그10" => {
3151                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.log10()));
3152            },
3153
3154            // ── Rounding / truncation ──
3155            "abs" | "ค่าสัมบูรณ์" | "绝对值" | "绝对" | "絶対値" | "절댓값" | "절대값" | "قدرمطلق" | "مطلق" | "ערך_מוחלט" | "مطلق_قدر" | "valeur_absolue" | "betrag" | "модуль_числа" =>
3156            {
3157                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.abs()));
3158            },
3159            "floor" | "ปัดลง" | "向下取整" | "下整" | "床関数" | "내림" | "کف" | "أرضية" | "רצפה" | "فرش" | "plancher" | "abrunden" | "вниз" => {
3160                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.floor()));
3161            },
3162            "ceil" | "ปัดขึ้น" | "向上取整" | "上整" | "天井関数" | "올림" | "سقف" | "תקרה" | "چھت" | "plafond" | "aufrunden" | "вверх" =>
3163            {
3164                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.ceil()));
3165            },
3166            "round" | "ปัดเศษ" | "四舍五入" | "四舍" | "四捨五入" | "반올림" | "گرد_کردن" | "تقريب" | "עיגול" | "گول" | "arrondir" | "runden" | "округлить" =>
3167            {
3168                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.round()));
3169            },
3170            "trunc"
3171            | "int"
3172            | "ตัดทศนิยม"
3173            | "取整"
3174            | "整数化"
3175            | "整数"
3176            | "截整"
3177            | "정수화"
3178            | "정수"
3179            | "切り捨て"
3180            | "버림" | "برش" | "اقتطاع" | "קיטום" | "کٹائی" | "tronquer" | "abschneiden" | "усечь" => {
3181                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.trunc()));
3182            },
3183            "fract" | "小数部分" | "小数部" | "소수부" => {
3184                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.fract()));
3185            },
3186
3187            // ── min / max / clamp ──
3188            "min" | "ต่ำสุด" | "最小" | "최솟값" | "کمینه" | "أصغر" | "מינימום" | "کم_ترین" | "minimum" | "минимум" => {
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.min(b)));
3192            },
3193            "max" | "สูงสุด" | "最大" | "최댓값" | "بیشینه" | "أكبر" | "מקסימום" | "زیادہ_ترین" | "maximum" | "максимум" => {
3194                let a = self.arg_num(&args, 0, 0.0)?;
3195                let b = self.arg_num(&args, 1, 0.0)?;
3196                return Ok(Value::Number(a.max(b)));
3197            },
3198            "clamp" | "จำกัด" | "截取" | "範囲制限" | "범위제한" | "محدود" | "قيّد" | "הגבל" | "limiter" | "begrenzen" | "ограничить" => {
3199                let x = self.arg_num(&args, 0, 0.0)?;
3200                let lo = self.arg_num(&args, 1, 0.0)?;
3201                let hi = self.arg_num(&args, 2, 1.0)?;
3202                return Ok(Value::Number(x.clamp(lo, hi)));
3203            },
3204
3205            // ── Constants (also accessible as plain identifiers via lookup) ──
3206            "pi" | "π" | "พาย" | "圆周率" | "円周率" | "파이" | "پی" | "باي" | "פאי" | "پائی" | "пи" => {
3207                return Ok(Value::Number(std::f64::consts::PI))
3208            },
3209            "tau" | "τ" | "双周率" | "タウ" | "타우" | "ทาว" | "تاو" | "טאו" | "ٹاؤ" | "тау" => {
3210                return Ok(Value::Number(std::f64::consts::TAU))
3211            },
3212
3213            // ══════════════════════════════════════════════════════════════════
3214            // PHASE 1: DMT TRIP CODER FEATURES
3215            // ══════════════════════════════════════════════════════════════════
3216
3217            // ── Step 1: Noise Functions ──
3218            "vnoise" | "noise2" | "นอยส์2ดี" | "柏林噪声2D" | "バリューノイズ2D" | "값노이즈2D" | "نویز_برداری" | "ضجيج_متجه" | "רעש_וקטורי" | "ویکٹر_نوائز" | "bruit_v" | "v_rauschen" | "шум_v" =>
3219            {
3220                let x = self.arg_num(&args, 0, 0.0)? as f32;
3221                let y = self.arg_num(&args, 1, 0.0)? as f32;
3222                let seed = self.arg_num(&args, 2, 0.0)? as u32;
3223                return Ok(Value::Number(tex_vnoise(x, y, seed) as f64));
3224            },
3225
3226            "fbm" | "นอยส์ออร์แกนิก" | "分形噪声" | "フラクタルノイズ" | "프랙탈노이즈" | "نویز_فراکتالی" | "ضجيج_عضوي" | "רעש_פרקטלי" | "فریکٹل_نوائز" =>
3227            {
3228                let x = self.arg_num(&args, 0, 0.0)? as f32;
3229                let y = self.arg_num(&args, 1, 0.0)? as f32;
3230                let octaves = self.arg_num(&args, 2, 4.0)? as u32;
3231                let seed = self.arg_num(&args, 3, 0.0)? as u32;
3232                return Ok(Value::Number(tex_fbm(x, y, octaves, seed) as f64));
3233            },
3234
3235            "perlin"
3236            | "perlin3"
3237            | "เพอร์ลิน3ดี"
3238            | "柏林噪声3D"
3239            | "パーリンノイズ3D"
3240            | "펄린노이즈3D" | "نویز_پرلین" | "بيرلين" | "רעש_פרלין" | "پرلن_نوائز" | "перлин" => {
3241                let x = self.arg_num(&args, 0, 0.0)? as f32;
3242                let y = self.arg_num(&args, 1, 0.0)? as f32;
3243                let z = self.arg_num(&args, 2, 0.0)? as f32;
3244                return Ok(Value::Number(perlin3(x, y, z) as f64));
3245            },
3246
3247            // ── Step 2: Math Ergonomics ──
3248            "lerp" | "ค่าระหว่าง" | "线性插值" | "線形補間" | "선형보간" | "میان‌یابی" | "استيفاء" | "אינטרפולציה" | "درمیانی_قدر" | "interpoler" | "interpolieren" | "интерполировать" =>
3249            {
3250                let a = self.arg_num(&args, 0, 0.0)?;
3251                let b = self.arg_num(&args, 1, 1.0)?;
3252                let t = self.arg_num(&args, 2, 0.0)?;
3253                return Ok(Value::Number(a + (b - a) * t));
3254            },
3255
3256            "smoothstep" | "เปลี่ยนแบบนุ่ม" | "平滑步进" | "スムーズステップ" | "스무스스텝" | "گام_نرم" | "تدرج_ناعم" | "מדרגה_חלקה" | "ہموار_قدم" | "lissage" | "glättung" | "сглаживание" =>
3257            {
3258                let lo = self.arg_num(&args, 0, 0.0)?;
3259                let hi = self.arg_num(&args, 1, 1.0)?;
3260                let x = self.arg_num(&args, 2, 0.5)?;
3261                let t = ((x - lo) / (hi - lo)).clamp(0.0, 1.0);
3262                return Ok(Value::Number(t * t * (3.0 - 2.0 * t)));
3263            },
3264
3265            "rand" | "สุ่ม" | "随机" | "乱数" | "난수" | "تصادفی" | "عشوائي" | "אקראי" | "بے_ترتیب" | "aléatoire" | "zufall" | "случайное" => {
3266                let val = fast_rand_f64(&mut self.rand_state);
3267                return Ok(Value::Number(val));
3268            },
3269
3270            "sign" | "เครื่องหมาย" | "符号" | "符号関数" | "부호" | "علامت" | "إشارة" | "סימן" | "نشان" | "signe" | "vorzeichen" | "знак" => {
3271                let x = self.arg_num(&args, 0, 0.0)?;
3272                return Ok(Value::Number(x.signum()));
3273            },
3274
3275            "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" =>
3276            {
3277                let h = self.arg_num(&args, 0, 0.0)?; // 0-360
3278                let s = self.arg_num(&args, 1, 1.0)?; // 0-1
3279                let v = self.arg_num(&args, 2, 1.0)?; // 0-1
3280                let c = v * s;
3281                let x = c * (1.0 - (((h / 60.0) % 2.0) - 1.0).abs());
3282                let m = v - c;
3283                let (r1, g1, b1) = if h < 60.0 {
3284                    (c, x, 0.0)
3285                } else if h < 120.0 {
3286                    (x, c, 0.0)
3287                } else if h < 180.0 {
3288                    (0.0, c, x)
3289                } else if h < 240.0 {
3290                    (0.0, x, c)
3291                } else if h < 300.0 {
3292                    (x, 0.0, c)
3293                } else {
3294                    (c, 0.0, x)
3295                };
3296                let r = ((r1 + m) * 255.0).round();
3297                let g = ((g1 + m) * 255.0).round();
3298                let b = ((b1 + m) * 255.0).round();
3299                return Ok(Value::List(Rc::new(vec![
3300                    Value::Number(r),
3301                    Value::Number(g),
3302                    Value::Number(b),
3303                ])));
3304            },
3305
3306            "lerp_color" | "ไล่สี" | "颜色插值" | "色補間" | "색보간" | "میان‌یابی_رنگ" | "استيفاء_اللون" | "אינטרפולציית_צבע" | "رنگ_درمیانی_قدر" | "interpoler_couleur" | "farbe_interpolieren" | "интерполировать_цвет" => {
3307                let r1 = self.arg_num(&args, 0, 0.0)?;
3308                let g1 = self.arg_num(&args, 1, 0.0)?;
3309                let b1 = self.arg_num(&args, 2, 0.0)?;
3310                let r2 = self.arg_num(&args, 3, 255.0)?;
3311                let g2 = self.arg_num(&args, 4, 255.0)?;
3312                let b2 = self.arg_num(&args, 5, 255.0)?;
3313                let t = self.arg_num(&args, 6, 0.0)?;
3314                let r = r1 + (r2 - r1) * t;
3315                let g = g1 + (g2 - g1) * t;
3316                let b = b1 + (b2 - b1) * t;
3317                let c = ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
3318                self.gfx.borrow_mut().color = c;
3319                return Ok(Value::Unit);
3320            },
3321
3322            // ── Step 3: Real-Time Clock ──
3323            "time_now" | "เวลาปัจจุบัน" | "当前时间" | "経過時間" | "현재시간" | "زمان_اکنون" | "الوقت_الآن" | "הזמן_עכשיו" | "ابھی_کا_وقت" | "temps_actuel" | "aktuelle_zeit" | "текущее_время" =>
3324            {
3325                return Ok(Value::Number(
3326                    crate::runtime::now_secs() - self.start_time_secs,
3327                ));
3328            },
3329
3330            // Wall-clock seconds since the Unix epoch (real date/time). Lets a
3331            // program defer deterministic-yet-evolving generation to the actual
3332            // datetime — same clock → same world, advancing as real time passes.
3333            "epoch_now" | "เวลาโลก" | "datetime" | "现在时刻" | "現在時刻" | "현재시각" | "مهر_زمانی_اکنون" | "طابع_الوقت_الآن" | "חותמת_זמן_עכשיו" | "ایپاک_وقت" =>
3334            {
3335                return Ok(Value::Number(crate::runtime::now_secs()));
3336            },
3337
3338            "frame_count" | "เฟรม" | "帧数" | "フレーム数" | "프레임수" | "شمار_فریم" | "عدد_الإطارات" | "ספירת_פריימים" | "فریم_شمار" | "nombre_images" | "bildanzahl" | "число_кадров" => {
3339                return Ok(Value::Number(self.frame_num as f64));
3340            },
3341
3342            // ── Step 4: Microphone Input ──
3343            "mic_open" | "เปิดไมค์" | "开麦克风" | "マイク開く" | "마이크열기" | "باز_کردن_میکروفون" | "افتح_الميكروفون" | "פתח_מיקרופון" | "مائیکروفون_کھولو" | "ouvrir_micro" | "mikrofon_öffnen" | "открыть_микрофон" =>
3344            {
3345                #[cfg(not(target_arch = "wasm32"))]
3346                {
3347                    match ling_mic::MicInput::open(Default::default()) {
3348                        Ok(mic) => {
3349                            let _ = mic.start(|_samples: &[f32]| {}); // No-op callback
3350                            self.mic = Some(mic);
3351                            return Ok(Value::Number(1.0)); // opened
3352                        },
3353                        // No device / permission denied → graceful: don't crash the game loop.
3354                        // Returns 0.0; mic_rms/mic_peak return 0.0 while self.mic is None.
3355                        Err(_e) => {
3356                            self.mic = None;
3357                            return Ok(Value::Number(0.0));
3358                        },
3359                    }
3360                }
3361                #[cfg(target_arch = "wasm32")]
3362                return Ok(Value::Unit);
3363            },
3364
3365            "mic_rms" | "เสียงRMS" | "麦克风音量" | "マイクRMS" | "마이크RMS" | "RMS_میکروفون" | "RMS_الميكروفون" | "RMS_מיקרופון" | "مائیکروفون_RMS" | "rms_micro" | "mikrofon_rms" | "rms_микрофона" =>
3366            {
3367                #[cfg(not(target_arch = "wasm32"))]
3368                {
3369                    let rms = self
3370                        .mic
3371                        .as_ref()
3372                        .map(|m: &ling_mic::MicInput| m.rms())
3373                        .unwrap_or(0.0);
3374                    return Ok(Value::Number(rms as f64));
3375                }
3376                #[cfg(target_arch = "wasm32")]
3377                return Ok(Value::Number(0.0));
3378            },
3379
3380            "mic_peak" | "เสียงพีค" | "麦克风峰值" | "マイクピーク" | "마이크피크" | "اوج_میکروفون" | "ذروة_الميكروفون" | "שיא_מיקרופון" | "مائیکروفون_چوٹی" | "crête_micro" | "mikrofon_spitze" | "пик_микрофона" =>
3381            {
3382                #[cfg(not(target_arch = "wasm32"))]
3383                {
3384                    let peak = self
3385                        .mic
3386                        .as_ref()
3387                        .map(|m: &ling_mic::MicInput| m.peak())
3388                        .unwrap_or(0.0);
3389                    return Ok(Value::Number(peak as f64));
3390                }
3391                #[cfg(target_arch = "wasm32")]
3392                return Ok(Value::Number(0.0));
3393            },
3394
3395            "mic_fft" | "วิเคราะห์เสียงสด" | "实时频谱" | "リアルタイムFFT" | "실시간FFT" | "FFT_میکروفون" | "FFT_الميكروفون" | "FFT_מיקרופון" | "مائیکروفون_FFT" | "fft_micro" | "mikrofon_fft" | "fft_микрофона" =>
3396            {
3397                #[cfg(not(target_arch = "wasm32"))]
3398                {
3399                    let n = self.arg_num(&args, 0, 8.0)? as usize;
3400                    if let Some(mic) = self.mic.as_ref() {
3401                        let samples = mic.latest_samples();
3402                        self.fft.borrow_mut().push_samples(&samples);
3403                    }
3404                    let bands = self.fft.borrow().freq_bands(n);
3405                    let result: Vec<Value> =
3406                        bands.iter().map(|&v| Value::Number(v as f64)).collect();
3407                    return Ok(Value::List(Rc::new(result)));
3408                }
3409                #[cfg(target_arch = "wasm32")]
3410                return Ok(Value::List(Vec::new().into()));
3411            },
3412
3413            // ── Step 5: Additive Blend Mode ──
3414            "set_blend" | "โหมดผสม" | "混合模式" | "ブレンドモード" | "블렌드모드" | "تنظیم_ترکیب" | "عيّن_المزج" | "קבע_מיזוג" | "بلینڈ_مقرر_کرو" | "définir_mélange" | "mischmodus_setzen" | "задать_смешивание" =>
3415            {
3416                let mode = self.arg_num(&args, 0, 0.0)? as u8;
3417                let mut gfx = self.gfx.borrow_mut();
3418                gfx.blend = mode;
3419                let a = gfx.alpha;
3420                gfx.depth_queue.set_state(mode, a); // 3-D queue captures blend for subsequent pushes
3421                return Ok(Value::Unit);
3422            },
3423
3424            // set_antialias(on) — smooth wireframe strokes (lines / edges / arcs /
3425            // circle outlines) via Xiaolin-Wu coverage. Default OFF = crisp,
3426            // opaque, aliased pixels; pass 1 to opt into smooth edges.
3427            "set_antialias" | "ตั้งลบรอยหยัก" | "抗锯齿" | "アンチエイリアス" | "안티에일리어싱" | "تنظیم_ضدلبه‌دندانه" | "عيّن_مضاد_التسنن" | "קבע_החלקת_קצוות" | "اینٹی_الائیسنگ_مقرر_کرو" =>
3428            {
3429                let on = self.arg_num(&args, 0, 1.0)? > 0.5;
3430                self.gfx.borrow_mut().antialias = on;
3431                return Ok(Value::Unit);
3432            },
3433            // get_antialias() -> bool — current wireframe anti-aliasing state.
3434            "get_antialias"
3435            | "อ่านลบรอยหยัก"
3436            | "读取抗锯齿"
3437            | "アンチエイリアス取得"
3438            | "안티에일리어싱상태" | "خواندن_ضدلبه‌دندانه" | "اقرأ_مضاد_التسنن" | "קרא_החלקת_קצוות" | "اینٹی_الائیسنگ_پڑھو" => {
3439                return Ok(Value::Bool(self.gfx.borrow().antialias));
3440            },
3441
3442            // set_font_antialias(on) — smooth `font_text`/`font_text_fill` glyph
3443            // edges, independent of `set_antialias` (which only covers wireframe
3444            // strokes). Default OFF = crisp, hard-edged text; pass 1 to opt in.
3445            "set_font_antialias" | "글꼴안티에일리어싱" => {
3446                let on = self.arg_num(&args, 0, 1.0)? > 0.5;
3447                self.gfx.borrow_mut().font_antialias = on;
3448                return Ok(Value::Unit);
3449            },
3450            // get_font_antialias() -> bool — current font anti-aliasing state.
3451            "get_font_antialias" | "글꼴안티에일리어싱상태" => {
3452                return Ok(Value::Bool(self.gfx.borrow().font_antialias));
3453            },
3454
3455            // ── Step 6: Circle Primitives ──
3456            "draw_circle" | "วาดวงกลม" | "画圆" | "円描画" | "원그리기" | "رسم_دایره" | "ارسم_دائرة" | "צייר_עיגול" | "دائرہ_کھینچو" | "dessiner_cercle" | "kreis_zeichnen" | "рисовать_круг" =>
3457            {
3458                let cx = self.arg_num(&args, 0, 0.0)? as i32;
3459                let cy = self.arg_num(&args, 1, 0.0)? as i32;
3460                let r = self.arg_num(&args, 2, 10.0)? as i32;
3461                let mut gfx = self.gfx.borrow_mut();
3462                let (w, h, color, blend) =
3463                    (gfx.width as i32, gfx.height as i32, gfx.color, gfx.blend);
3464                if gfx.antialias {
3465                    let (uw, uh) = (gfx.width, gfx.height);
3466                    let segs = ((r.max(1) as u32) * 4).clamp(24, 512);
3467                    crate::gfx::raster::draw_arc(
3468                        &mut gfx.buffer,
3469                        uw,
3470                        uh,
3471                        color,
3472                        true,
3473                        blend == 1,
3474                        cx as f32,
3475                        cy as f32,
3476                        r as f32,
3477                        0.0,
3478                        std::f32::consts::TAU,
3479                        segs,
3480                    );
3481                } else {
3482                    draw_circle_outline(&mut gfx.buffer, w, h, cx, cy, r, color, blend);
3483                }
3484                return Ok(Value::Unit);
3485            },
3486
3487            "draw_filled_circle"
3488            | "draw_disc"
3489            | "วาดวงกลมทึบ"
3490            | "画实心圆"
3491            | "塗りつぶし円"
3492            | "원채우기" | "رسم_دایره_توپر" | "ارسم_دائرة_ممتلئة" | "צייר_עיגול_מלא" | "بھرا_دائرہ_کھینچو" => {
3493                let cx = self.arg_num(&args, 0, 0.0)? as i32;
3494                let cy = self.arg_num(&args, 1, 0.0)? as i32;
3495                let r = self.arg_num(&args, 2, 10.0)? as i32;
3496                let mut gfx = self.gfx.borrow_mut();
3497                let (w, h, color, blend) =
3498                    (gfx.width as i32, gfx.height as i32, gfx.color, gfx.blend);
3499                draw_circle_filled(&mut gfx.buffer, w, h, cx, cy, r, color, blend);
3500                return Ok(Value::Unit);
3501            },
3502
3503            // draw_arc(cx, cy, r, a0, a1 [, segments]) — stroke a circular arc in
3504            // the pen colour (full circle when a1-a0 = TAU). Honors the antialias
3505            // flag; opaque by default (additive when blend = 1).
3506            "draw_arc" | "arc" | "วาดส่วนโค้ง" | "画弧" | "円弧描画" | "호그리기" | "رسم_کمان" | "ارسم_قوسا" | "צייר_קשת" | "آرک_کھینچو" =>
3507            {
3508                let cx = self.arg_num(&args, 0, 0.0)? as f32;
3509                let cy = self.arg_num(&args, 1, 0.0)? as f32;
3510                let r = self.arg_num(&args, 2, 10.0)? as f32;
3511                let a0 = self.arg_num(&args, 3, 0.0)? as f32;
3512                let a1 = self.arg_num(&args, 4, std::f64::consts::TAU)? as f32;
3513                let default_segs = ((r.abs() * (a1 - a0).abs()).ceil() as u32).clamp(8, 1024);
3514                let segs = self.arg_num(&args, 5, default_segs as f64)? as u32;
3515                let mut gfx = self.gfx.borrow_mut();
3516                let color = gfx.color;
3517                #[cfg(not(target_arch = "wasm32"))]
3518                {
3519                    let (uw, uh, aa, add) = (gfx.width, gfx.height, gfx.antialias, gfx.blend == 1);
3520                    crate::gfx::raster::draw_arc(
3521                        &mut gfx.buffer,
3522                        uw,
3523                        uh,
3524                        color,
3525                        aa,
3526                        add,
3527                        cx,
3528                        cy,
3529                        r,
3530                        a0,
3531                        a1,
3532                        segs,
3533                    );
3534                }
3535                #[cfg(target_arch = "wasm32")]
3536                {
3537                    let segs_f = segs.max(1);
3538                    let step = (a1 - a0) / segs_f as f32;
3539                    let mut px = cx + r * a0.cos();
3540                    let mut py = cy + r * a0.sin();
3541                    let mut i = 1u32;
3542                    while i <= segs_f {
3543                        let a = a0 + step * i as f32;
3544                        let nx = cx + r * a.cos();
3545                        let ny = cy + r * a.sin();
3546                        gfx.depth_queue.push_line(0.0, color, px, py, nx, ny);
3547                        px = nx;
3548                        py = ny;
3549                        i += 1;
3550                    }
3551                }
3552                return Ok(Value::Unit);
3553            },
3554
3555            // ── Step 7: Transparent fills, gradient surfaces & colored shadows ──
3556            // These all write straight into the software framebuffer (gfx.buffer)
3557            // on both native and web, so no target gating is needed.
3558
3559            // set_alpha(a) — pen opacity 0..1 for the alpha-blended fills below.
3560            "set_alpha" | "ตั้งความโปร่งใส" | "设透明" | "アルファ設定" | "투명도설정" | "تنظیم_شفافیت" | "عيّن_الشفافية" | "קבע_שקיפות" | "شفافیت_مقرر_کرو" | "définir_alpha" | "alpha_setzen" | "задать_альфа" =>
3561            {
3562                let a = self.arg_num(&args, 0, 1.0)? as f32;
3563                let mut gfx = self.gfx.borrow_mut();
3564                gfx.alpha = a.clamp(0.0, 1.0);
3565                let (m, al) = (gfx.blend, gfx.alpha);
3566                gfx.depth_queue.set_state(m, al); // 3-D queue captures alpha for subsequent pushes
3567                return Ok(Value::Unit);
3568            },
3569
3570            // mesh_hue(radians) — hue-rotate the baked per-tri colours of every
3571            // subsequent mesh_draw (.lmesh). 0 resets. Cheap: one matrix per call.
3572            "mesh_hue" | "หมุนสีเมช" | "فام_مش" | "صبغة_الشبكة" | "גוון_רשת" | "میش_ہیو" =>
3573            {
3574                let h = self.arg_num(&args, 0, 0.0)? as f32;
3575                let g = self.arg_num(&args, 1, 1.0)? as f32;
3576                let mut gfx = self.gfx.borrow_mut();
3577                gfx.mesh_hue = h;
3578                gfx.mesh_hue_gain = g.max(0.0);
3579                return Ok(Value::Unit);
3580            },
3581
3582            // set_frame_blur(amount 0..0.95) — afterimage trails: blend the previous
3583            // presented frame into each new one. 0 = off (also frees the ghost buffer).
3584            "set_frame_blur" | "frame_blur" | "เบลอเฟรม" | "تنظیم_تاری_فریم" | "عيّن_ضبابية_الإطار" | "קבע_טשטוש_פריים" | "فریم_بلر_مقرر_کرو" =>
3585            {
3586                let a = self.arg_num(&args, 0, 0.0)? as f32;
3587                let mut gfx = self.gfx.borrow_mut();
3588                gfx.frame_blur = a.clamp(0.0, 0.95);
3589                if gfx.frame_blur <= 0.0 {
3590                    gfx.prev_frame = Vec::new();
3591                }
3592                return Ok(Value::Unit);
3593            },
3594
3595            // set_line_hue_cycle(rate) — rapidly cycle the hue of ALL wireframe line
3596            // strokes (draw_line / draw_line_3d). `rate` in radians/sec; 0 = off.
3597            // Process-global so a single call covers every stroke, every frame.
3598            "set_line_hue_cycle" | "ตั้งวนสีเส้น" | "تنظیم_چرخه_فام_خط" | "عيّن_دورة_صبغة_الخط" | "קבע_מחזור_גוון_קו" | "لائن_ہیو_سائیکل_مقرر_کرو" => {
3599                let rate = self.arg_num(&args, 0, 0.0)?;
3600                crate::runtime::set_line_hue_rate(rate);
3601                return Ok(Value::Unit);
3602            },
3603
3604            // set_color_space(mode) — 0 = legacy sRGB compositing (default),
3605            // 1 = gamma-correct linear-light compositing (blend in linear, store
3606            // sRGB) so alpha and gradients don't darken/shift hue.
3607            "set_color_space" | "ปริภูมิสี" | "色彩空间" | "色空間" | "색공간" | "تنظیم_فضای_رنگ" | "عيّن_فضاء_اللون" | "קבע_מרחב_צבע" | "کلر_اسپیس_مقرر_کرو" | "définir_espace_couleur" | "farbraum_setzen" | "задать_цветовое_пространство" =>
3608            {
3609                let m = self.arg_num(&args, 0, 0.0)? as i64;
3610                self.gfx.borrow_mut().linear_blend = m != 0;
3611                return Ok(Value::Unit);
3612            },
3613
3614            // set_gradient_space(mode) — 1 = perceptual OkLab gradient interp
3615            // (default), 0 = legacy sRGB. Affects grad_triangle / grad_rect.
3616            "set_gradient_space" | "ปริภูมิไล่สี" | "渐变空间" | "グラデ空間" | "그라데이션공간" | "تنظیم_فضای_گرادیان" | "عيّن_فضاء_التدرج" | "קבע_מרחב_גרדיאנט" | "گریڈینٹ_اسپیس_مقرر_کرو" | "définir_espace_dégradé" | "verlaufsraum_setzen" | "задать_пространство_градиента" =>
3617            {
3618                let m = self.arg_num(&args, 0, 1.0)? as i64;
3619                self.gfx.borrow_mut().grad_oklab = m != 0;
3620                return Ok(Value::Unit);
3621            },
3622
3623            // mix_color(r0,g0,b0, r1,g1,b1, t) — set the pen colour to the
3624            // perceptual OkLab blend of two colours (t in 0..1). Far nicer
3625            // mid-tones than a raw RGB lerp.
3626            "mix_color" | "ผสมสี" | "混合颜色" | "色混合" | "색혼합" | "ترکیب_رنگ" | "امزج_اللون" | "ערבב_צבע" | "رنگ_ملاؤ" | "mélanger_couleur" | "farbe_mischen" | "смешать_цвет" => {
3627                let c0 = rgb(
3628                    self.arg_num(&args, 0, 0.0)?,
3629                    self.arg_num(&args, 1, 0.0)?,
3630                    self.arg_num(&args, 2, 0.0)?,
3631                );
3632                let c1 = rgb(
3633                    self.arg_num(&args, 3, 255.0)?,
3634                    self.arg_num(&args, 4, 255.0)?,
3635                    self.arg_num(&args, 5, 255.0)?,
3636                );
3637                let t = self.arg_num(&args, 6, 0.5)? as f32;
3638                self.gfx.borrow_mut().color = crate::gfx::color::mix_oklab(c0, c1, t);
3639                return Ok(Value::Unit);
3640            },
3641
3642            // set_depth_test(on) — enable the per-pixel z-buffer for the deferred
3643            // 3-D/queued draws (correct interpenetration) instead of painter's-
3644            // only sort. 0 = off (default), non-zero = on.
3645            "set_depth_test" | "ทดสอบความลึก" | "深度测试" | "深度テスト" | "깊이테스트" | "تنظیم_آزمون_عمق" | "عيّن_اختبار_العمق" | "קבע_בדיקת_עומק" | "ڈیپتھ_ٹیسٹ_مقرر_کرو" | "définir_test_profondeur" | "tiefentest_setzen" | "задать_тест_глубины" =>
3646            {
3647                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
3648                self.gfx.borrow_mut().depth_test = on;
3649                return Ok(Value::Unit);
3650            },
3651
3652            // set_flat_shade(on) / ตั้งแฟลตเชด — perf test: skip all per-triangle/mesh
3653            // lighting (compute_lit_color) and draw with the raw pen colour.
3654            "set_flat_shade" | "ตั้งแฟลตเชด" | "平面着色" | "フラット着色" | "평면음영" | "تنظیم_سایه‌پردازی_تخت" | "عيّن_تظليلا_مسطحا" | "קבע_הצללה_שטוחה" | "فلیٹ_شیڈ_مقرر_کرو" =>
3655            {
3656                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
3657                self.gfx.borrow_mut().flat_shade = on;
3658                return Ok(Value::Unit);
3659            },
3660
3661            // set_normal_override(x,y,z) - force subsequent triangle/mesh lighting
3662            // to use a stylized world-space normal until reset_normal_override().
3663            "set_normal_override" =>
3664            {
3665                let x = self.arg_num(&args, 0, 0.0)? as f32;
3666                let y = self.arg_num(&args, 1, -1.0)? as f32;
3667                let z = self.arg_num(&args, 2, 0.0)? as f32;
3668                self.gfx.borrow_mut().normal_override = Some([x, y, z]);
3669                return Ok(Value::Unit);
3670            },
3671
3672            "reset_normal_override" =>
3673            {
3674                self.gfx.borrow_mut().normal_override = None;
3675                return Ok(Value::Unit);
3676            },
3677
3678            // clear_depth() / ล้างความลึก — force the z-buffer to clear on the next
3679            // flush. `เติม` already does this; call explicitly to start a fresh
3680            // depth pass mid-frame (e.g. a separate overlay scene).
3681            "clear_depth" | "ล้างความลึก" | "清深度" | "深度クリア" | "깊이지우기" | "پاک‌کردن_عمق" | "امسح_العمق" | "נקה_עומק" | "گہرائی_صاف_کرو" =>
3682            {
3683                self.gfx.borrow_mut().zbuf_needs_clear = true;
3684                return Ok(Value::Unit);
3685            },
3686
3687            // depth_blur(focus, range, radius) / เบลอความลึก — depth-of-field post
3688            // pass over the framebuffer using the z-buffer: sharp at camera-space
3689            // depth `focus`, blurred up to `radius` px as depth departs by `range`.
3690            // Background (no geometry) blurs fully. Call AFTER `flush_3d` (so the
3691            // z-buffer is populated) and BEFORE `present`. Needs `set_depth_test(1)`.
3692            "depth_blur" | "เบลอความลึก" | "dof" | "depth_of_field" | "景深" | "تاری_عمق" | "ضبابية_العمق" | "טשטוש_עומק" | "ڈیپتھ_بلر" =>
3693            {
3694                let focus = self.arg_num(&args, 0, 30.0)? as f32;
3695                let range = self.arg_num(&args, 1, 60.0)? as f32;
3696                let radius = self.arg_num(&args, 2, 3.0)?.max(0.0) as usize;
3697                // oil [0..1] — oil-slick treatment of the blurred zone:
3698                // iridescent chroma fringe + hue swirl (water / heat haze).
3699                let oil = self.arg_num(&args, 3, 0.0)? as f32;
3700                let mut gfx = self.gfx.borrow_mut();
3701                let w = gfx.width;
3702                let h = gfx.height;
3703                if gfx.depth_buf.len() == w * h {
3704                    let g = &mut *gfx;
3705                    crate::gfx::raster::depth_of_field(
3706                        &mut g.buffer,
3707                        &g.depth_buf,
3708                        w,
3709                        h,
3710                        focus,
3711                        range,
3712                        radius,
3713                        oil,
3714                    );
3715                }
3716                return Ok(Value::Unit);
3717            },
3718
3719            // light_pool(x, y, z, radius, r, g, b, intensity) / แอ่งแสง —
3720            // volumetric light splash: a soft additive radial vector gradient on
3721            // the floor at height y — the coloured pool a light throws on the
3722            // ground (underwater-light look). Smooth transparent edge, distance-
3723            // fog aware. Colours 0-255; intensity ~0.2-1.5.
3724            "light_pool" | "แอ่งแสง" | "光池" | "ライトプール" | "빛웅덩이" | "برکه_نور" | "بركة_ضوء" | "בריכת_אור" | "روشنی_تالاب" | "bassin_lumière" | "lichtpfütze" | "лужа_света" =>
3725            {
3726                let x = self.arg_num(&args, 0, 0.0)? as f32;
3727                let y = self.arg_num(&args, 1, 0.0)? as f32;
3728                let z = self.arg_num(&args, 2, 0.0)? as f32;
3729                let radius = self.arg_num(&args, 3, 20.0)? as f32;
3730                let r = self.arg_num(&args, 4, 255.0)? as f32 / 255.0;
3731                let g = self.arg_num(&args, 5, 255.0)? as f32 / 255.0;
3732                let b = self.arg_num(&args, 6, 255.0)? as f32 / 255.0;
3733                let inten = self.arg_num(&args, 7, 1.0)? as f32;
3734                self.gfx
3735                    .borrow_mut()
3736                    .emit_light_pool(x, y, z, radius, [r, g, b], inten);
3737                return Ok(Value::Unit);
3738            },
3739
3740            // light_beam(x, y, z, floor_y, radius, r, g, b, intensity) / ลำแสงไฟ —
3741            // volumetric god-ray shaft: a soft additive double-cone from the
3742            // light position down to the floor plane, spreading to `radius`.
3743            // Pair with light_pool at the base. Colours 0-255.
3744            "light_beam" | "ลำแสงไฟ" | "光柱" | "ライトビーム" | "빛기둥" | "پرتو_نور" | "شعاع_ضوء" | "קרן_אור" | "روشنی_شعاع" | "faisceau_lumière" | "lichtstrahl" | "луч_света" =>
3745            {
3746                let x = self.arg_num(&args, 0, 0.0)? as f32;
3747                let y = self.arg_num(&args, 1, 0.0)? as f32;
3748                let z = self.arg_num(&args, 2, 0.0)? as f32;
3749                let fy = self.arg_num(&args, 3, 0.0)? as f32;
3750                let radius = self.arg_num(&args, 4, 14.0)? as f32;
3751                let r = self.arg_num(&args, 5, 255.0)? as f32 / 255.0;
3752                let g = self.arg_num(&args, 6, 255.0)? as f32 / 255.0;
3753                let b = self.arg_num(&args, 7, 255.0)? as f32 / 255.0;
3754                let inten = self.arg_num(&args, 8, 1.0)? as f32;
3755                self.gfx
3756                    .borrow_mut()
3757                    .emit_light_beam(x, y, z, fy, radius, [r, g, b], inten);
3758                return Ok(Value::Unit);
3759            },
3760
3761            // grad_triangle(x0,y0,r0,g0,b0, x1,y1,r1,g1,b1, x2,y2,r2,g2,b2)
3762            // Smooth per-vertex gradient triangle — a cheap lit surface: put the
3763            // bright colour on the vertex facing the light. Honours set_alpha.
3764            "grad_triangle" | "สามเหลี่ยมไล่สี" | "渐变三角" | "グラデ三角" | "그라데삼각" | "مثلث_گرادیان" | "مثلث_متدرج" | "משולש_גרדיאנט" | "گریڈینٹ_مثلث" | "triangle_dégradé" | "dreieck_verlauf" | "градиент_треугольник" =>
3765            {
3766                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
3767                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
3768                let c0 = rgb(
3769                    self.arg_num(&args, 2, 255.0)?,
3770                    self.arg_num(&args, 3, 255.0)?,
3771                    self.arg_num(&args, 4, 255.0)?,
3772                );
3773                let x1 = self.arg_num(&args, 5, 0.0)? as f32;
3774                let y1 = self.arg_num(&args, 6, 0.0)? as f32;
3775                let c1 = rgb(
3776                    self.arg_num(&args, 7, 255.0)?,
3777                    self.arg_num(&args, 8, 255.0)?,
3778                    self.arg_num(&args, 9, 255.0)?,
3779                );
3780                let x2 = self.arg_num(&args, 10, 0.0)? as f32;
3781                let y2 = self.arg_num(&args, 11, 0.0)? as f32;
3782                let c2 = rgb(
3783                    self.arg_num(&args, 12, 255.0)?,
3784                    self.arg_num(&args, 13, 255.0)?,
3785                    self.arg_num(&args, 14, 255.0)?,
3786                );
3787                let mut gfx = self.gfx.borrow_mut();
3788                let (w, h, alpha, mode, lin, ok) = (
3789                    gfx.width,
3790                    gfx.height,
3791                    gfx.alpha,
3792                    gfx.blend,
3793                    gfx.linear_blend,
3794                    gfx.grad_oklab,
3795                );
3796                crate::gfx::raster::fill_triangle_grad(
3797                    &mut gfx.buffer,
3798                    w,
3799                    h,
3800                    alpha,
3801                    mode,
3802                    lin,
3803                    ok,
3804                    x0,
3805                    y0,
3806                    c0,
3807                    x1,
3808                    y1,
3809                    c1,
3810                    x2,
3811                    y2,
3812                    c2,
3813                );
3814                return Ok(Value::Unit);
3815            },
3816
3817            // grad_rect(x,y,w,h, r0,g0,b0, r1,g1,b1, dir) — linear-gradient rect.
3818            // dir 0 = horizontal (left→right), else vertical (top→bottom).
3819            "grad_rect" | "สี่เหลี่ยมไล่สี" | "渐变矩形" | "グラデ矩形" | "그라데사각" | "مستطیل_گرادیان" | "مستطيل_متدرج" | "מלבן_גרדיאנט" | "گریڈینٹ_مستطیل" | "rectangle_dégradé" | "rechteck_verlauf" | "градиент_прямоугольник" =>
3820            {
3821                let x = self.arg_num(&args, 0, 0.0)? as f32;
3822                let y = self.arg_num(&args, 1, 0.0)? as f32;
3823                let rw = self.arg_num(&args, 2, 0.0)? as f32;
3824                let rh = self.arg_num(&args, 3, 0.0)? as f32;
3825                let c0 = rgb(
3826                    self.arg_num(&args, 4, 255.0)?,
3827                    self.arg_num(&args, 5, 255.0)?,
3828                    self.arg_num(&args, 6, 255.0)?,
3829                );
3830                let c1 = rgb(
3831                    self.arg_num(&args, 7, 0.0)?,
3832                    self.arg_num(&args, 8, 0.0)?,
3833                    self.arg_num(&args, 9, 0.0)?,
3834                );
3835                let dir = self.arg_num(&args, 10, 1.0)? as u8;
3836                let mut gfx = self.gfx.borrow_mut();
3837                let (w, h, alpha, mode, lin, ok) = (
3838                    gfx.width,
3839                    gfx.height,
3840                    gfx.alpha,
3841                    gfx.blend,
3842                    gfx.linear_blend,
3843                    gfx.grad_oklab,
3844                );
3845                crate::gfx::raster::fill_rect_grad(
3846                    &mut gfx.buffer,
3847                    w,
3848                    h,
3849                    alpha,
3850                    mode,
3851                    lin,
3852                    ok,
3853                    x,
3854                    y,
3855                    rw,
3856                    rh,
3857                    c0,
3858                    c1,
3859                    dir,
3860                );
3861                return Ok(Value::Unit);
3862            },
3863
3864            // shadow_blob(cx,cy, rx,ry, alpha) — soft colored shadow ellipse in
3865            // the current pen colour. Dark colour = normal shadow; any hue = a
3866            // tinted/coloured shadow. Edge softness comes from shadow_params.
3867            "shadow_blob" | "เงาวงรี" | "阴影斑" | "影ブロブ" | "그림자블롭" | "لکه_سایه" | "بقعة_ظل" | "כתם_צל" | "سایہ_دھبہ" | "tache_ombre" | "schattenklecks" | "пятно_тени" =>
3868            {
3869                let cx = self.arg_num(&args, 0, 0.0)? as f32;
3870                let cy = self.arg_num(&args, 1, 0.0)? as f32;
3871                let rx = self.arg_num(&args, 2, 16.0)? as f32;
3872                let ry = self.arg_num(&args, 3, 8.0)? as f32;
3873                let a = self.arg_num(&args, 4, 0.5)? as f32;
3874                let mut gfx = self.gfx.borrow_mut();
3875                let (w, h, color, soft, mode, lin) = (
3876                    gfx.width,
3877                    gfx.height,
3878                    gfx.color,
3879                    gfx.shadow.soft,
3880                    gfx.blend,
3881                    gfx.linear_blend,
3882                );
3883                crate::gfx::raster::fill_disc_soft(
3884                    &mut gfx.buffer,
3885                    w,
3886                    h,
3887                    cx,
3888                    cy,
3889                    rx,
3890                    ry,
3891                    color,
3892                    a,
3893                    soft,
3894                    mode,
3895                    lin,
3896                );
3897                return Ok(Value::Unit);
3898            },
3899
3900            // cast_shadow(cx,cy, height) — height-driven contact shadow in the
3901            // current pen colour. Closer to the surface (small height) = smaller,
3902            // darker, sharper; farther (large height) = bigger, fainter, softer.
3903            // Tune the ramp with shadow_params.
3904            "cast_shadow" | "ทอดเงา" | "投射阴影" | "影を落とす" | "그림자드리우기" | "افکندن_سایه" | "ألقِ_ظلا" | "הטל_צל" | "سایہ_ڈالو" | "projeter_ombre" | "schatten_werfen" | "отбросить_тень" =>
3905            {
3906                let cx = self.arg_num(&args, 0, 0.0)? as f32;
3907                let cy = self.arg_num(&args, 1, 0.0)? as f32;
3908                let height = (self.arg_num(&args, 2, 0.0)? as f32).max(0.0);
3909                let mut gfx = self.gfx.borrow_mut();
3910                let sp = gfx.shadow;
3911                let radius = (sp.base + sp.grow * height).max(0.5);
3912                let alpha = (sp.alpha - sp.fade * height).clamp(0.04, 1.0);
3913                let soft = (sp.soft + height * 0.004).clamp(0.0, 0.95);
3914                let (w, h, color, mode, lin) = (
3915                    gfx.width,
3916                    gfx.height,
3917                    gfx.color,
3918                    gfx.blend,
3919                    gfx.linear_blend,
3920                );
3921                crate::gfx::raster::fill_disc_soft(
3922                    &mut gfx.buffer,
3923                    w,
3924                    h,
3925                    cx,
3926                    cy,
3927                    radius,
3928                    radius * 0.62,
3929                    color,
3930                    alpha,
3931                    soft,
3932                    mode,
3933                    lin,
3934                );
3935                return Ok(Value::Unit);
3936            },
3937
3938            // shadow_params(base, grow, alpha, fade, soft) — tune cast_shadow.
3939            // Each arg defaults to the current value, so you can set just one.
3940            "shadow_params" | "ตั้งค่าเงา" | "阴影参数" | "影設定" | "그림자설정" | "پارامترهای_سایه" | "معاملات_الظل" | "פרמטרי_צל" | "سایہ_پیرامیٹرز" | "paramètres_ombre" | "schattenparameter" | "параметры_тени" =>
3941            {
3942                let cur = self.gfx.borrow().shadow;
3943                let base = self.arg_num(&args, 0, cur.base as f64)? as f32;
3944                let grow = self.arg_num(&args, 1, cur.grow as f64)? as f32;
3945                let alpha = self.arg_num(&args, 2, cur.alpha as f64)? as f32;
3946                let fade = self.arg_num(&args, 3, cur.fade as f64)? as f32;
3947                let soft = self.arg_num(&args, 4, cur.soft as f64)? as f32;
3948                self.gfx.borrow_mut().shadow =
3949                    crate::gfx::ShadowParams { base, grow, alpha, fade, soft };
3950                return Ok(Value::Unit);
3951            },
3952
3953            // depth_triangle(x0,y0, x1,y1, x2,y2, z) — queue a depth-sorted tri in
3954            // the current colour. Drawn back-to-front (painter's algorithm) at
3955            // present(); larger z = farther away. Lets 2-D sprites/quads sort by
3956            // depth the same way 3-D faces do.
3957            "depth_triangle" | "สามเหลี่ยมเรียงลึก" | "深度三角" | "深度三角形" | "깊이삼각" | "مثلث_عمق" | "مثلث_العمق" | "משולש_עומק" | "گہرائی_مثلث" | "triangle_profondeur" | "tiefendreieck" | "глубина_треугольник" =>
3958            {
3959                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
3960                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
3961                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
3962                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
3963                let x2 = self.arg_num(&args, 4, 0.0)? as f32;
3964                let y2 = self.arg_num(&args, 5, 0.0)? as f32;
3965                let z = self.arg_num(&args, 6, 0.0)? as f32;
3966                let mut gfx = self.gfx.borrow_mut();
3967                let color = gfx.color;
3968                gfx.depth_queue
3969                    .push_triangle(z, color, x0, y0, x1, y1, x2, y2);
3970                return Ok(Value::Unit);
3971            },
3972
3973            // depth_line(x0,y0, x1,y1, z) — queue a depth-sorted line in the
3974            // current colour (same painter's queue as depth_triangle).
3975            "depth_line" | "เส้นเรียงลึก" | "深度线" | "深度線" | "깊이선" | "خط_عمق" | "خط_العمق" | "קו_עומק" | "گہرائی_لکیر" | "ligne_profondeur" | "tiefenlinie" | "глубина_линия" =>
3976            {
3977                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
3978                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
3979                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
3980                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
3981                let z = self.arg_num(&args, 4, 0.0)? as f32;
3982                let mut gfx = self.gfx.borrow_mut();
3983                let color = gfx.color;
3984                gfx.depth_queue.push_line(z, color, x0, y0, x1, y1);
3985                return Ok(Value::Unit);
3986            },
3987
3988            // ══════════════════════════════════════════════════════════════════
3989            // GRAPHICS BUILTINS
3990            // Thai names first, then English aliases.
3991            // ══════════════════════════════════════════════════════════════════
3992
3993            // ── เปิดหน้าต่าง(width, height, title) — open_window ──
3994            "เปิดหน้าต่าง" | "open_window" | "gfx_window" | "开窗" | "ウィンドウ開く" | "창열기" | "باز_کردن_پنجره" | "افتح_نافذة" | "פתח_חלון" | "ونڈو_کھولو" =>
3995            {
3996                let w = self.arg_num(&args, 0, 800.0)? as usize;
3997                let h = self.arg_num(&args, 1, 600.0)? as usize;
3998                #[cfg(not(target_arch = "wasm32"))]
3999                {
4000                    let title = args
4001                        .get(2)
4002                        .map(|v| v.to_string())
4003                        .unwrap_or_else(|| "Ling".into());
4004                    let mut gfx = self.gfx.borrow_mut();
4005                    let mut win = minifb::Window::new(
4006                        &title,
4007                        w,
4008                        h,
4009                        minifb::WindowOptions {
4010                            resize: false,
4011                            scale: minifb::Scale::X1,
4012                            ..Default::default()
4013                        },
4014                    )
4015                    .map_err(|e| EvalErr::from(format!("cannot open window: {e}")))?;
4016                    apply_frame_pacing(&mut win, gfx.vsync);
4017                    gfx.buffer = vec![0u32; w * h];
4018                    gfx.width = w;
4019                    gfx.height = h;
4020                    gfx.window = Some(win);
4021                    gfx.topmost_window = false;
4022                    gfx.sync_projection();
4023                }
4024                #[cfg(target_arch = "wasm32")]
4025                {
4026                    let mut gfx = self.gfx.borrow_mut();
4027                    gfx.width = w;
4028                    gfx.height = h;
4029                    gfx.buffer.resize(w * h, 0); // keep the CPU framebuffer in sync
4030                    gfx.sync_projection();
4031                    crate::gfx::webgl::resize(w as u32, h as u32);
4032                }
4033                return Ok(Value::Unit);
4034            },
4035
4036            // ── เติม(r, g, b) — fill / clear screen with colour ──
4037            "เติม" | "fill" | "gfx_fill" | "clear" | "填" | "塗り潰し" | "채우기" | "清"
4038            | "消去" | "지우기" | "پر_کن" | "املأ" | "מלא" | "بھرو" => {
4039                let r = self.arg_num(&args, 0, 0.0)? as u32;
4040                let g = self.arg_num(&args, 1, 0.0)? as u32;
4041                let b = self.arg_num(&args, 2, 0.0)? as u32;
4042                #[cfg(not(target_arch = "wasm32"))]
4043                {
4044                    let c = (r << 16) | (g << 8) | b;
4045                    let mut gfx = self.gfx.borrow_mut();
4046                    gfx.buffer.fill(c);
4047                    gfx.zbuf_needs_clear = true; // clear color ⇒ clear depth next flush
4048                    gfx.edge_set.clear(); // reset shared-edge dedup for new frame
4049                }
4050                #[cfg(target_arch = "wasm32")]
4051                {
4052                    let mut gfx = self.gfx.borrow_mut();
4053                    gfx.fill_r = r as f32 / 255.0;
4054                    gfx.fill_g = g as f32 / 255.0;
4055                    gfx.fill_b = b as f32 / 255.0;
4056                    let c = (r << 16) | (g << 8) | b;
4057                    gfx.buffer.fill(c);
4058                    gfx.zbuf_needs_clear = true;
4059                    gfx.edge_set.clear();
4060                }
4061                return Ok(Value::Unit);
4062            },
4063
4064            // ── set_color_hsl(h, s, l) — set drawing colour from HSL ──
4065            // h: 0–360 degrees, s: 0–100 saturation, l: 0–100 lightness
4066            "set_color_hsl" | "颜色HSL" | "色相" | "HSL色" | "HSL색설정" | "สีHSLวาด" | "تنظیم_رنگ_HSL" | "عيّن_اللون_HSL" | "קבע_צבע_HSL" | "HSL_رنگ_مقرر_کرو" | "définir_couleur_hsl" | "farbe_hsl_setzen" | "задать_цвет_hsl" =>
4067            {
4068                let h = self.arg_num(&args, 0, 0.0)?;
4069                let s = self.arg_num(&args, 1, 70.0)?;
4070                let l = self.arg_num(&args, 2, 50.0)?;
4071                let hex = hsl_to_hex(h, s, l);
4072                let r = u32::from_str_radix(&hex[1..3], 16).unwrap_or(255);
4073                let g = u32::from_str_radix(&hex[3..5], 16).unwrap_or(255);
4074                let b = u32::from_str_radix(&hex[5..7], 16).unwrap_or(255);
4075                self.gfx.borrow_mut().color = (r << 16) | (g << 8) | b;
4076                return Ok(Value::Unit);
4077            },
4078
4079            // ── สีดินสอ(r, g, b) — set drawing colour ──
4080            "สีดินสอ" | "set_color" | "gfx_color" | "color" | "设色" | "色設定" | "색설정" | "تنظیم_رنگ" | "عيّن_اللون" | "קבע_צבע" | "رنگ_مقرر_کرو" =>
4081            {
4082                let r = self.arg_num(&args, 0, 255.0)? as u32;
4083                let g = self.arg_num(&args, 1, 255.0)? as u32;
4084                let b = self.arg_num(&args, 2, 255.0)? as u32;
4085                self.gfx.borrow_mut().color = (r << 16) | (g << 8) | b;
4086                return Ok(Value::Unit);
4087            },
4088
4089            // ── วาดสามเหลี่ยม(x1,y1, x2,y2, x3,y3) — draw filled triangle ──
4090            "วาดสามเหลี่ยม"
4091            | "draw_triangle"
4092            | "gfx_triangle"
4093            | "triangle"
4094            | "画三角"
4095            | "三角形描画"
4096            | "삼각형그리기" | "رسم_مثلث" | "ارسم_مثلثا" | "צייר_משולש" | "مثلث_کھینچو" => {
4097                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
4098                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
4099                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
4100                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
4101                let x2 = self.arg_num(&args, 4, 0.0)? as f32;
4102                let y2 = self.arg_num(&args, 5, 0.0)? as f32;
4103                let mut gfx = self.gfx.borrow_mut();
4104                let color = gfx.color;
4105                #[cfg(not(target_arch = "wasm32"))]
4106                {
4107                    let w = gfx.width;
4108                    let h = gfx.height;
4109                    fill_triangle(&mut gfx.buffer, w, h, color, x0, y0, x1, y1, x2, y2);
4110                }
4111                #[cfg(target_arch = "wasm32")]
4112                gfx.depth_queue
4113                    .push_triangle(0.0, color, x0, y0, x1, y1, x2, y2);
4114                return Ok(Value::Unit);
4115            },
4116
4117            // ── วาดเส้น(x1,y1, x2,y2) — draw line ──
4118            "วาดเส้น" | "draw_line" | "gfx_line" | "line" | "画线" | "線描く" | "선그리기" | "رسم_خط" | "ارسم_خط" | "צייר_קו" | "لکیر_کھینچو" =>
4119            {
4120                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
4121                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
4122                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
4123                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
4124                let mut gfx = self.gfx.borrow_mut();
4125                let color = gfx.color;
4126                #[cfg(not(target_arch = "wasm32"))]
4127                {
4128                    let w = gfx.width;
4129                    let h = gfx.height;
4130                    let aa = gfx.antialias;
4131                    let add = gfx.blend == 1;
4132                    if aa {
4133                        crate::gfx::raster::draw_line_aa(
4134                            &mut gfx.buffer,
4135                            w,
4136                            h,
4137                            color,
4138                            add,
4139                            x0,
4140                            y0,
4141                            x1,
4142                            y1,
4143                        );
4144                    } else {
4145                        draw_line(&mut gfx.buffer, w, h, color, x0, y0, x1, y1);
4146                    }
4147                }
4148                #[cfg(target_arch = "wasm32")]
4149                gfx.depth_queue.push_line(0.0, color, x0, y0, x1, y1);
4150                return Ok(Value::Unit);
4151            },
4152
4153            // ── วาดจุด(x, y) — plot a single pixel ──
4154            "วาดจุด" | "draw_pixel" | "gfx_pixel" | "pixel" | "画点" | "点描く" | "점그리기" | "رسم_نقطه" | "ارسم_نقطة" | "צייר_פיקסל" | "پکسل_کھینچو" =>
4155            {
4156                let px = self.arg_num(&args, 0, 0.0)? as i32;
4157                let py = self.arg_num(&args, 1, 0.0)? as i32;
4158                #[cfg(not(target_arch = "wasm32"))]
4159                {
4160                    let mut gfx = self.gfx.borrow_mut();
4161                    let color = gfx.color;
4162                    let w = gfx.width;
4163                    let h = gfx.height;
4164                    if px >= 0 && py >= 0 && (px as usize) < w && (py as usize) < h {
4165                        gfx.buffer[py as usize * w + px as usize] = color;
4166                    }
4167                }
4168                #[cfg(target_arch = "wasm32")]
4169                {
4170                    // Render pixel as a 1×1 square via two triangles.
4171                    let mut gfx = self.gfx.borrow_mut();
4172                    let color = gfx.color;
4173                    let x = px as f32;
4174                    let y = py as f32;
4175                    gfx.depth_queue
4176                        .push_triangle(0.0, color, x, y, x + 1.0, y, x + 1.0, y + 1.0);
4177                    gfx.depth_queue
4178                        .push_triangle(0.0, color, x, y, x + 1.0, y + 1.0, x, y + 1.0);
4179                }
4180                return Ok(Value::Unit);
4181            },
4182
4183            // ── แสดงผล() — flush depth queue, then present frame to screen ──
4184            "แสดงผล" | "present" | "gfx_present" | "show" | "显" | "呈现" | "表示" | "표시" | "نمایش" | "اعرض" | "הצג" | "دکھاؤ" =>
4185            {
4186                // Click-edge widgets (ui_button etc.) compare THIS frame's
4187                // mouse_now() against `mouse_was_down` to detect a fresh
4188                // press. That comparison only works if mouse_was_down
4189                // reflects what the script itself observed this frame — i.e.
4190                // the state from BEFORE update_with_buffer below pulls in new
4191                // OS events. Capturing it after (as the "freshest" read)
4192                // would mean a just-arrived click is already baked into
4193                // mouse_was_down by the time next frame's ui_button compares
4194                // against it, so `down && !mouse_was_down` is never true and
4195                // clicks never register at all. Declared at the top of the
4196                // match arm (not inside the block below) so it survives past
4197                // the wasm32/non-wasm32 split further down.
4198                #[cfg(not(target_arch = "wasm32"))]
4199                let pre_update_mouse_down = self
4200                    .gfx
4201                    .borrow()
4202                    .window
4203                    .as_ref()
4204                    .map(|w| w.get_mouse_down(minifb::MouseButton::Left))
4205                    .unwrap_or(false);
4206                #[cfg(not(target_arch = "wasm32"))]
4207                {
4208                    ling_fps_tick();
4209                    ling_phase_frame();
4210                    // Flush depth queue and present — release borrow before reading mouse.
4211                    {
4212                        let mut gfx = self.gfx.borrow_mut();
4213                        if !gfx.depth_queue.is_empty() {
4214                            let w = gfx.width;
4215                            let h = gfx.height;
4216                            let dt = gfx.depth_test;
4217                            let reset_z = gfx.zbuf_needs_clear;
4218                            let (bm, ba) = (gfx.blend, gfx.alpha);
4219                            let aa = gfx.antialias;
4220                            let queue = std::mem::take(&mut gfx.depth_queue);
4221                            {
4222                                let g = &mut *gfx;
4223                                let z = if dt { Some(&mut g.depth_buf) } else { None };
4224                                queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
4225                            }
4226                            gfx.depth_queue.set_state(bm, ba);
4227                            gfx.zbuf_needs_clear = false;
4228                        }
4229                        let _t = std::time::Instant::now();
4230                        if !gfx.post_done {
4231                            gfx.toon_post_process();
4232                        }
4233                        gfx.post_done = false;
4234                        ling_phase_add(phase::TOON, _t.elapsed().as_nanos());
4235                        let w = gfx.width;
4236                        let h = gfx.height;
4237                        let g = &mut *gfx;
4238                        if g.frame_blur > 0.0 {
4239                            // Afterimage trails: previous frame decays by `frame_blur`
4240                            // per frame and composites with MAX — fresh content stays
4241                            // full-brightness, ghosts fade out over time.
4242                            // retention 0.98 @60fps ≈ trails last ~2.6 s.
4243                            let a = (g.frame_blur.clamp(0.0, 0.995) * 256.0) as u32;
4244                            if g.prev_frame.len() != g.buffer.len() {
4245                                g.prev_frame = g.buffer.clone();
4246                            }
4247                            for (dst, prev) in g.buffer.iter_mut().zip(g.prev_frame.iter_mut()) {
4248                                let c = *dst;
4249                                let pv = *prev;
4250                                let pr = (((pv >> 16) & 0xFF) * a) >> 8;
4251                                let pg = (((pv >> 8) & 0xFF) * a) >> 8;
4252                                let pb = ((pv & 0xFF) * a) >> 8;
4253                                let cr = (c >> 16) & 0xFF;
4254                                let cg = (c >> 8) & 0xFF;
4255                                let cb = c & 0xFF;
4256                                let outp = (cr.max(pr) << 16) | (cg.max(pg) << 8) | cb.max(pb);
4257                                *dst = outp;
4258                                *prev = outp;
4259                            }
4260                        }
4261                        if let Some(win) = g.window.as_mut() {
4262                            let _b = std::time::Instant::now();
4263                            win.update_with_buffer(&g.buffer, w, h)
4264                                .map_err(|e| EvalErr::from(format!("present error: {e}")))?;
4265                            ling_phase_add(phase::BLIT, _b.elapsed().as_nanos());
4266                        }
4267                    }
4268                    // Read mouse AFTER update_with_buffer so events are processed.
4269                    let mouse_pos = {
4270                        let gfx = self.gfx.borrow();
4271                        gfx.window
4272                            .as_ref()
4273                            .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
4274                    };
4275                    let mut gfx = self.gfx.borrow_mut();
4276                    if gfx.mouse_captured {
4277                        let w = gfx.width as f32;
4278                        let h = gfx.height as f32;
4279                        if let Some((mx, my)) = mouse_pos {
4280                            if gfx.last_mx.is_nan() {
4281                                gfx.mouse_dx = 0.0;
4282                                gfx.mouse_dy = 0.0;
4283                                gfx.last_mx = mx;
4284                                gfx.last_my = my;
4285                            } else {
4286                                gfx.mouse_dx = mx - gfx.last_mx;
4287                                gfx.mouse_dy = my - gfx.last_my;
4288                                // Wrap the cursor at every edge (L/R/U/D) → infinite look
4289                                // on both axes, and the cursor is NOT trapped (alt-tab works).
4290                                let margin = 6.0;
4291                                let (mut nx, mut ny, mut warp) = (mx, my, false);
4292                                if mx < margin {
4293                                    nx = w - margin - 2.0;
4294                                    warp = true;
4295                                } else if mx > w - margin {
4296                                    nx = margin + 2.0;
4297                                    warp = true;
4298                                }
4299                                if my < margin {
4300                                    ny = h - margin - 2.0;
4301                                    warp = true;
4302                                } else if my > h - margin {
4303                                    ny = margin + 2.0;
4304                                    warp = true;
4305                                }
4306                                if warp {
4307                                    #[cfg(windows)]
4308                                    unsafe {
4309                                        #[repr(C)]
4310                                        struct RECT {
4311                                            left: i32,
4312                                            top: i32,
4313                                            right: i32,
4314                                            bottom: i32,
4315                                        }
4316                                        extern "system" {
4317                                            fn GetForegroundWindow() -> isize;
4318                                            fn GetWindowRect(hwnd: isize, lpRect: *mut RECT)
4319                                                -> i32;
4320                                            fn SetCursorPos(x: i32, y: i32) -> i32;
4321                                        }
4322                                        let hwnd = GetForegroundWindow();
4323                                        let mut rect =
4324                                            RECT { left: 0, top: 0, right: 0, bottom: 0 };
4325                                        if GetWindowRect(hwnd, &mut rect) != 0 {
4326                                            SetCursorPos(
4327                                                rect.left + nx as i32,
4328                                                rect.top + ny as i32,
4329                                            );
4330                                        }
4331                                    }
4332                                    gfx.last_mx = nx;
4333                                    gfx.last_my = ny;
4334                                } else {
4335                                    gfx.last_mx = mx;
4336                                    gfx.last_my = my;
4337                                }
4338                            }
4339                        } else {
4340                            gfx.mouse_dx = 0.0;
4341                            gfx.mouse_dy = 0.0;
4342                        }
4343                    } else if let Some((mx, my)) = mouse_pos {
4344                        if gfx.last_mx.is_nan() {
4345                            gfx.mouse_dx = 0.0;
4346                            gfx.mouse_dy = 0.0;
4347                        } else {
4348                            gfx.mouse_dx = mx - gfx.last_mx;
4349                            gfx.mouse_dy = my - gfx.last_my;
4350                        }
4351                        gfx.last_mx = mx;
4352                        gfx.last_my = my;
4353                    } else {
4354                        gfx.mouse_dx = 0.0;
4355                        gfx.mouse_dy = 0.0;
4356                    }
4357
4358                    // Alt-tab support: minifb has no WM_KILLFOCUS handler on Windows,
4359                    // so a key/button released while another window was focused can
4360                    // still read as "down" for one stale frame right after the user
4361                    // alt-tabs back. Detect the unfocused→focused transition and
4362                    // swallow raw input for a short grace window afterward instead of
4363                    // letting a phantom held key jerk the camera. See key_down /
4364                    // mouse_down* below (they early-out on gfx.input_suppressed()).
4365                    let is_active = gfx.window.as_mut().map(|w| w.is_active()).unwrap_or(true);
4366                    if is_active && !gfx.was_active {
4367                        gfx.focus_grace_frames = 5;
4368                        // Regained focus: restore HWND_TOPMOST so the borderless-
4369                        // fullscreen window covers the taskbar again.
4370                        #[cfg(windows)]
4371                        if gfx.topmost_window {
4372                            if let Some(w) = gfx.window.as_ref() {
4373                                set_window_topmost(w.get_window_handle() as isize, true);
4374                            }
4375                        }
4376                    } else if !is_active && gfx.was_active {
4377                        // Lost focus (alt-tab): drop topmost so the game stops
4378                        // covering whatever window the user just switched to —
4379                        // otherwise a topmost borderless window visually "wins"
4380                        // even though it's no longer focused, making alt-tab
4381                        // look broken.
4382                        #[cfg(windows)]
4383                        if gfx.topmost_window {
4384                            if let Some(w) = gfx.window.as_ref() {
4385                                set_window_topmost(w.get_window_handle() as isize, false);
4386                            }
4387                        }
4388                    }
4389                    gfx.was_active = is_active;
4390                    if gfx.focus_grace_frames > 0 {
4391                        gfx.focus_grace_frames -= 1;
4392                    }
4393                }
4394                #[cfg(target_arch = "wasm32")]
4395                {
4396                    {
4397                        // Software-render everything (3-D depth queue + 2-D vtex/ui that
4398                        // already wrote into the buffer) into the framebuffer, exactly
4399                        // like native, then upload that buffer to the canvas in one blit.
4400                        let mut gfx = self.gfx.borrow_mut();
4401                        let w = gfx.width;
4402                        let h = gfx.height;
4403                        if gfx.buffer.len() != w * h {
4404                            gfx.buffer.resize(w * h, 0);
4405                        }
4406                        if !gfx.depth_queue.is_empty() {
4407                            let dt = gfx.depth_test;
4408                            let reset_z = gfx.zbuf_needs_clear;
4409                            let aa = gfx.antialias;
4410                            let queue = std::mem::take(&mut gfx.depth_queue);
4411                            {
4412                                let g = &mut *gfx;
4413                                let z = if dt { Some(&mut g.depth_buf) } else { None };
4414                                queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
4415                            }
4416                            gfx.zbuf_needs_clear = false;
4417                        }
4418                        if !gfx.post_done {
4419                            gfx.toon_post_process();
4420                        }
4421                        gfx.post_done = false;
4422                        crate::gfx::webgl::blit_rgb(&gfx.buffer, w, h);
4423                    }
4424                    self.wasm_pace_frame();
4425                }
4426                // Update the click-edge latch for interactive UI widgets —
4427                // using the PRE-update_with_buffer snapshot captured at the
4428                // top of this function (see the comment there for why).
4429                #[cfg(not(target_arch = "wasm32"))]
4430                {
4431                    self.mouse_was_down = pre_update_mouse_down;
4432                }
4433                // Increment frame counter
4434                self.frame_num += 1;
4435                return Ok(Value::Unit);
4436            },
4437
4438            // ── เปิดหน้าต่างเต็มจอ(title) — true native-res fullscreen window ──
4439            "เปิดหน้าต่างเต็มจอ"
4440            | "open_fullscreen"
4441            | "fullscreen"
4442            | "全屏"
4443            | "全画面"
4444            | "전체화면" | "باز_کردن_تمام‌صفحه" | "افتح_ملء_الشاشة" | "פתח_מסך_מלא" | "فل_سکرین_کھولو" => {
4445                // In WASM the canvas defines the viewport; use its current size
4446                // as the default so the projection matches what's actually visible.
4447                #[cfg(target_arch = "wasm32")]
4448                let (default_w, default_h) = {
4449                    let (cw, ch) = crate::gfx::webgl::canvas_size();
4450                    (cw as f64, ch as f64)
4451                };
4452                // On native: query the actual primary monitor resolution.
4453                #[cfg(all(not(target_arch = "wasm32"), windows))]
4454                let (default_w, default_h) = unsafe {
4455                    extern "system" {
4456                        fn GetSystemMetrics(nIndex: i32) -> i32;
4457                    }
4458                    (GetSystemMetrics(0) as f64, GetSystemMetrics(1) as f64)
4459                };
4460                #[cfg(all(not(target_arch = "wasm32"), not(windows)))]
4461                let (default_w, default_h) = native_screen_size();
4462
4463                let w = args
4464                    .get(1)
4465                    .map(|v| self.to_number(v).unwrap_or(default_w) as usize)
4466                    .unwrap_or(default_w as usize);
4467                let h = args
4468                    .get(2)
4469                    .map(|v| self.to_number(v).unwrap_or(default_h) as usize)
4470                    .unwrap_or(default_h as usize);
4471                #[cfg(not(target_arch = "wasm32"))]
4472                {
4473                    let title = args
4474                        .first()
4475                        .map(|v| v.to_string())
4476                        .unwrap_or_else(|| "Ling".into());
4477                    let mut gfx = self.gfx.borrow_mut();
4478                    let mut win = minifb::Window::new(
4479                        &title,
4480                        w,
4481                        h,
4482                        minifb::WindowOptions {
4483                            borderless: true,
4484                            title: false,
4485                            resize: false,
4486                            topmost: true,
4487                            scale: minifb::Scale::X1,
4488                            ..Default::default()
4489                        },
4490                    )
4491                    .map_err(|e| EvalErr::from(format!("cannot open fullscreen: {e}")))?;
4492                    apply_frame_pacing(&mut win, gfx.vsync);
4493                    // Grab the native handle *before* moving the window into gfx.
4494                    #[cfg(windows)]
4495                    let hwnd = win.get_window_handle() as isize;
4496                    gfx.buffer = vec![0u32; w * h];
4497                    gfx.width = w;
4498                    gfx.height = h;
4499                    gfx.window = Some(win);
4500                    gfx.topmost_window = true;
4501                    #[cfg(windows)]
4502                    {
4503                        gfx.hwnd = hwnd;
4504                    }
4505                    gfx.sync_projection();
4506                    // Strip all chrome and cover the full screen, above the taskbar.
4507                    #[cfg(windows)]
4508                    make_borderless_fullscreen(hwnd, w as i32, h as i32);
4509                    #[cfg(windows)]
4510                    force_window_focus(hwnd);
4511                }
4512                #[cfg(target_arch = "wasm32")]
4513                {
4514                    let mut gfx = self.gfx.borrow_mut();
4515                    gfx.width = w;
4516                    gfx.height = h;
4517                    gfx.buffer.resize(w * h, 0); // keep the CPU framebuffer in sync
4518                    gfx.sync_projection();
4519                    crate::gfx::webgl::resize(w as u32, h as u32);
4520                }
4521                return Ok(Value::Unit);
4522            },
4523
4524            // ── ความกว้าง() / ความสูง() — current framebuffer size ──
4525            "get_width" | "ความกว้าง" | "宽" | "幅取得" | "너비" | "عرض" | "العرض" | "רוחב" | "چوڑائی" | "obtenir_largeur" | "breite_abrufen" | "получить_ширину" => {
4526                return Ok(Value::Number(self.gfx.borrow().width as f64));
4527            },
4528            "get_height" | "ความสูง" | "高" | "高取得" | "높이" | "ارتفاع" | "الارتفاع" | "גובה" | "اونچائی" | "obtenir_hauteur" | "höhe_abrufen" | "получить_высоту" => {
4529                return Ok(Value::Number(self.gfx.borrow().height as f64));
4530            },
4531
4532            // ── monitor detection: physical display, not the framebuffer ──────
4533            // monitor_width() → primary-monitor pixel width
4534            "monitor_width" | "screen_width" | "屏宽" | "画面幅" | "화면너비" | "ความกว้างจอ" | "عرض_مانیتور" | "عرض_الشاشة" | "רוחב_צג" | "مانیٹر_چوڑائی" | "largeur_moniteur" | "monitor_breite" | "ширина_монитора" =>
4535            {
4536                return Ok(Value::Number(monitor_info().0 as f64));
4537            },
4538            // monitor_height() → primary-monitor pixel height
4539            "monitor_height" | "screen_height" | "屏高" | "画面高" | "화면높이" | "ความสูงจอ" | "ارتفاع_مانیتور" | "ارتفاع_الشاشة" | "גובה_צג" | "مانیٹر_اونچائی" | "hauteur_moniteur" | "monitor_höhe" | "высота_монитора" =>
4540            {
4541                return Ok(Value::Number(monitor_info().1 as f64));
4542            },
4543            // monitor_refresh() → refresh rate in Hz (a.k.a. the monitor framerate)
4544            "monitor_refresh"
4545            | "monitor_hz"
4546            | "monitor_fps"
4547            | "refresh_rate"
4548            | "刷新率"
4549            | "リフレッシュレート"
4550            | "주사율"
4551            | "อัตรารีเฟรช" | "نرخ_بروزرسانی_مانیتور" | "معدل_تحديث_الشاشة" | "קצב_רענון_צג" | "مانیٹر_ریفریش_ریٹ" | "fréquence_moniteur" | "monitor_bildwiederholrate" | "частота_монитора" => {
4552                return Ok(Value::Number(monitor_info().2 as f64));
4553            },
4554            // monitor_info() → [width, height, refresh_hz]
4555            "monitor_info" | "screen_info" | "屏幕信息" | "画面情報" | "화면정보" | "ข้อมูลจอ" | "اطلاعات_مانیتور" | "معلومات_الشاشة" | "מידע_צג" | "مانیٹر_معلومات" | "info_moniteur" | "bildschirminfo" | "инфо_монитора" =>
4556            {
4557                let (w, h, hz) = monitor_info();
4558                return Ok(Value::List(Rc::new(vec![
4559                    Value::Number(w as f64),
4560                    Value::Number(h as f64),
4561                    Value::Number(hz as f64),
4562                ])));
4563            },
4564            // set_fps(n) → cap the render loop at n frames per second
4565            "set_fps"
4566            | "set_target_fps"
4567            | "target_fps"
4568            | "设帧率"
4569            | "フレームレート設定"
4570            | "프레임설정"
4571            | "ตั้งเฟรมเรต" | "تنظیم_نرخ_فریم" | "عيّن_معدل_الإطارات" | "קבע_קצב_פריימים" | "ایف_پی_ایس_مقرر_کرو" | "définir_fps" | "fps_setzen" | "задать_fps" => {
4572                #[cfg(not(target_arch = "wasm32"))]
4573                {
4574                    let fps = self.arg_num(&args, 0, 60.0)?.max(1.0) as usize;
4575                    let mut gfx = self.gfx.borrow_mut();
4576                    if let Some(win) = gfx.window.as_mut() {
4577                        win.set_target_fps(fps);
4578                    }
4579                }
4580                #[cfg(target_arch = "wasm32")]
4581                {
4582                    self.wasm_target_fps = self.arg_num(&args, 0, 60.0)?.max(1.0);
4583                    self.wasm_next_present_ms = 0.0;
4584                }
4585                return Ok(Value::Unit);
4586            },
4587
4588            // set_vsync(on) → pace the window to the monitor's refresh rate.
4589            // Frame-rate pacing (minifb has no swap-interval), not tear-free
4590            // vsync; `LING_FPS_CAP` and an explicit `set_fps` call still win.
4591            "set_vsync" | "vsync" | "垂直同步" | "垂直同期" | "수직동기" | "ตั้งวีซิงก์" | "تنظیم_وی‌سینک" | "عيّن_تزامن_رأسي" | "קבע_וי_סינק" | "وی_سینک_مقرر_کرو" | "définir_vsync" | "vsync_setzen" | "задать_vsync" =>
4592            {
4593                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
4594                #[cfg(not(target_arch = "wasm32"))]
4595                {
4596                    let mut gfx = self.gfx.borrow_mut();
4597                    gfx.vsync = on;
4598                    if let Some(win) = gfx.window.as_mut() {
4599                        apply_frame_pacing(win, on);
4600                    }
4601                }
4602                #[cfg(target_arch = "wasm32")]
4603                {
4604                    self.wasm_target_fps = if on { monitor_info().2 as f64 } else { 240.0 };
4605                    self.wasm_next_present_ms = 0.0;
4606                }
4607                return Ok(Value::Unit);
4608            },
4609
4610            // ── หน้าต่างเปิดอยู่() → bool — is the window still open? ──
4611            "หน้าต่างเปิดอยู่"
4612            | "window_is_open"
4613            | "gfx_is_open"
4614            | "is_open"
4615            | "窗开"
4616            | "開いている"
4617            | "창열림" | "پنجره_باز_است" | "النافذة_مفتوحة" | "החלון_פתוח" | "ونڈو_کھلی_ہے" => {
4618                #[cfg(not(target_arch = "wasm32"))]
4619                {
4620                    let gfx = self.gfx.borrow();
4621                    if gfx.want_quit {
4622                        return Ok(Value::Bool(false));
4623                    }
4624                    // Escape-to-quit needs the same GetAsyncKeyState fallback
4625                    // as key_down/key_pressed/text_poll (see those) — raw
4626                    // w.is_key_down(Escape) is WM_KEYDOWN-based and silently
4627                    // never fires if this topmost window didn't actually win
4628                    // real Win32 keyboard focus.
4629                    #[cfg(windows)]
4630                    let escape_down = if gfx.topmost_window {
4631                        window_is_foreground(gfx.hwnd) && os_key_down(0x1B) // VK_ESCAPE
4632                    } else {
4633                        gfx.window
4634                            .as_ref()
4635                            .map(|w| w.is_key_down(minifb::Key::Escape))
4636                            .unwrap_or(false)
4637                    };
4638                    #[cfg(not(windows))]
4639                    let escape_down = gfx
4640                        .window
4641                        .as_ref()
4642                        .map(|w| w.is_key_down(minifb::Key::Escape))
4643                        .unwrap_or(false);
4644                    let open = gfx.window.as_ref().map(|w| w.is_open()).unwrap_or(false)
4645                        && !escape_down;
4646                    return Ok(Value::Bool(open));
4647                }
4648                #[cfg(target_arch = "wasm32")]
4649                return Ok(Value::Bool(true));
4650            },
4651
4652            // quit() — close the window the same way Escape does, for a
4653            // script-drawn UI element (an exit button) to call.
4654            "quit" | "exit_game" | "close_window" => {
4655                #[cfg(not(target_arch = "wasm32"))]
4656                {
4657                    self.gfx.borrow_mut().want_quit = true;
4658                }
4659                return Ok(Value::Unit);
4660            },
4661
4662            // ── key_down(name) → bool — is a key held? ──
4663            "key_down" | "กดค้าง" | "按键" | "キー押す" | "키누름" | "کلید_فشرده" | "المفتاح_مضغوط" | "מקש_לחוץ" | "بٹن_دبا_ہوا" | "touche_enfoncée" | "taste_gedrückt" | "клавиша_нажата" => {
4664                #[cfg(not(target_arch = "wasm32"))]
4665                {
4666                    let name = self.arg_str(&args, 0, "");
4667                    let mut gfx = self.gfx.borrow_mut();
4668                    // The borderless-fullscreen/topmost window can be
4669                    // visually in front without ever winning real Win32
4670                    // keyboard focus (Windows' foreground-lock) — minifb's
4671                    // is_key_down is populated from WM_KEYDOWN, which then
4672                    // never arrives. GetAsyncKeyState reads the OS key-state
4673                    // table directly and doesn't need focus, so use it
4674                    // whenever this is that window (see force_window_focus).
4675                    #[cfg(windows)]
4676                    if gfx.topmost_window {
4677                        if !window_is_foreground(gfx.hwnd) {
4678                            return Ok(Value::Bool(false));
4679                        }
4680                        return Ok(Value::Bool(
4681                            str_to_vk(&name).map(os_key_down).unwrap_or(false),
4682                        ));
4683                    }
4684                    if gfx.input_suppressed() {
4685                        return Ok(Value::Bool(false));
4686                    }
4687                    let down = gfx
4688                        .window
4689                        .as_ref()
4690                        .and_then(|w| str_to_minifb_key(&name).map(|k| w.is_key_down(k)))
4691                        .unwrap_or(false);
4692                    return Ok(Value::Bool(down));
4693                }
4694                #[cfg(target_arch = "wasm32")]
4695                {
4696                    let name = self.arg_str(&args, 0, "");
4697                    return Ok(Value::Bool(crate::gfx::wasm_is_key_down(&name)));
4698                }
4699            },
4700
4701            // ── key_pressed(name) → bool — was a key pressed this frame? ──
4702            "key_pressed" | "กดปุ่ม" | "键按" | "キー押した" | "키눌림" | "فشردن_کلید" | "ضغط_المفتاح" | "לחיצת_מקש" | "بٹن_دبانا" | "touche_appuyée" | "taste_getippt" | "клавиша_нажатие" => {
4703                #[cfg(not(target_arch = "wasm32"))]
4704                {
4705                    let name = self.arg_str(&args, 0, "");
4706                    let pressed = {
4707                        let mut gfx = self.gfx.borrow_mut();
4708                        #[cfg(windows)]
4709                        let topmost = gfx.topmost_window;
4710                        #[cfg(not(windows))]
4711                        let topmost = false;
4712                        if topmost {
4713                            #[cfg(windows)]
4714                            {
4715                                if !window_is_foreground(gfx.hwnd) {
4716                                    false
4717                                } else {
4718                                    match str_to_vk(&name) {
4719                                        Some(vk) => {
4720                                            let idx = (vk as usize) & 0xFF;
4721                                            let down = os_key_down(vk);
4722                                            let was = gfx.raw_keys_prev[idx];
4723                                            gfx.raw_keys_prev[idx] = down;
4724                                            down && !was
4725                                        },
4726                                        None => false,
4727                                    }
4728                                }
4729                            }
4730                            #[cfg(not(windows))]
4731                            {
4732                                false
4733                            }
4734                        } else if gfx.input_suppressed() {
4735                            false
4736                        } else {
4737                            gfx.window
4738                                .as_ref()
4739                                .and_then(|w| {
4740                                    str_to_minifb_key(&name)
4741                                        .map(|k| w.is_key_pressed(k, minifb::KeyRepeat::No))
4742                                })
4743                                .unwrap_or(false)
4744                        }
4745                    };
4746                    // gamepad Start behaves like Enter everywhere
4747                    let pressed =
4748                        pressed || ((name == "enter" || name == "return") && gamepad::start_edge());
4749                    return Ok(Value::Bool(pressed));
4750                }
4751                #[cfg(target_arch = "wasm32")]
4752                {
4753                    let name = self.arg_str(&args, 0, "");
4754                    let pressed = crate::gfx::wasm_is_key_pressed(&name);
4755                    return Ok(Value::Bool(pressed));
4756                }
4757            },
4758
4759            // ── mouse_dx() / mouse_dy() → f64 — delta since last frame ──
4760            "mouse_dx" | "เมาส์X" | "鼠ΔX" | "マウスΔX" | "마우스ΔX" | "دلتا_ماوس_ایکس" | "فارق_الفأرة_س" | "דלתא_עכבר_X" | "ماؤس_ڈیلٹا_ایکس" | "souris_dx" | "maus_dx" | "мышь_dx" => {
4761                #[cfg(not(target_arch = "wasm32"))]
4762                return Ok(Value::Number(self.gfx.borrow().mouse_dx as f64));
4763                #[cfg(target_arch = "wasm32")]
4764                return Ok(Value::Number(crate::gfx::wasm_mouse_dx() as f64));
4765            },
4766            // ── mouse_scroll() → f64 — vertical scroll-wheel delta this frame ──
4767            #[cfg(not(target_arch = "wasm32"))]
4768            "mouse_scroll" | "ล้อเมาส์" | "滚轮" | "ホイール" | "스크롤" | "غلتک_ماوس" | "عجلة_الفأرة" | "גלגלת_עכבר" | "ماؤس_اسکرول" =>
4769            {
4770                let gfx = self.gfx.borrow();
4771                let s = gfx
4772                    .window
4773                    .as_ref()
4774                    .and_then(|w| w.get_scroll_wheel())
4775                    .map(|(_, y)| y as f64)
4776                    .unwrap_or(0.0);
4777                return Ok(Value::Number(s));
4778            },
4779            #[cfg(target_arch = "wasm32")]
4780            "mouse_scroll" | "ล้อเมาส์" | "滚轮" | "ホイール" | "스크롤" | "غلتک_ماوس" | "عجلة_الفأرة" | "גלגלת_עכבר" | "ماؤس_اسکرول" =>
4781            {
4782                return Ok(Value::Number(0.0));
4783            },
4784            "mouse_dy" | "เมาส์Y" | "鼠ΔY" | "マウスΔY" | "마우스ΔY" | "دلتا_ماوس_ایگرگ" | "فارق_الفأرة_ص" | "דלתא_עכבר_Y" | "ماؤس_ڈیلٹا_وائی" | "souris_dy" | "maus_dy" | "мышь_dy" => {
4785                #[cfg(not(target_arch = "wasm32"))]
4786                return Ok(Value::Number(self.gfx.borrow().mouse_dy as f64));
4787                #[cfg(target_arch = "wasm32")]
4788                return Ok(Value::Number(crate::gfx::wasm_mouse_dy() as f64));
4789            },
4790
4791            // ── Gamepad / joystick input (ling-input "Sensorium" + gilrs) ──
4792            // pad_poll() → number — advance input one frame; returns # connected pads.
4793            "pad_poll" | "手柄轮询" | "パッド更新" | "패드폴링" | "อัปเดตแพด" | "بررسی_دسته" | "استطلع_اليد" | "בדוק_בקר" | "گیم_پیڈ_پول" | "interroger_manette" | "gamepad_abfragen" | "опросить_геймпад" =>
4794            {
4795                #[cfg(not(target_arch = "wasm32"))]
4796                return Ok(Value::Number(self.pad_poll() as f64));
4797                #[cfg(target_arch = "wasm32")]
4798                return Ok(Value::Number(input_web::poll() as f64));
4799            },
4800            // pad_count() → number — connected gamepads.
4801            "pad_count" | "手柄数" | "パッド数" | "패드수" | "จำนวนแพด" | "تعداد_دسته" | "عدد_أيدي_التحكم" | "מספר_בקרים" | "گیم_پیڈ_تعداد" | "nombre_manettes" | "gamepad_anzahl" | "число_геймпадов" =>
4802            {
4803                #[cfg(not(target_arch = "wasm32"))]
4804                {
4805                    let inp = self.input.borrow();
4806                    let n = inp.as_ref().map_or(0, |s| s.sensorium.devices.count());
4807                    return Ok(Value::Number(n as f64));
4808                }
4809                #[cfg(target_arch = "wasm32")]
4810                return Ok(Value::Number(input_web::count() as f64));
4811            },
4812            // pad_connected(i) → bool.
4813            "pad_connected" | "手柄连接" | "パッド接続" | "패드연결" | "แพดเชื่อม" | "دسته_متصل" | "يد_التحكم_متصلة" | "בקר_מחובר" | "گیم_پیڈ_منسلک" | "manette_connectée" | "gamepad_verbunden" | "геймпад_подключён" =>
4814            {
4815                #[cfg(not(target_arch = "wasm32"))]
4816                {
4817                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4818                    let inp = self.input.borrow();
4819                    let c = inp
4820                        .as_ref()
4821                        .is_some_and(|s| s.sensorium.devices.for_player(i as u8).is_some());
4822                    return Ok(Value::Bool(c));
4823                }
4824                #[cfg(target_arch = "wasm32")]
4825                {
4826                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4827                    return Ok(Value::Bool(input_web::is_connected(i)));
4828                }
4829            },
4830            // pad_button(i, name) → bool — is the button held?
4831            "pad_button" | "手柄按键" | "パッドボタン" | "패드버튼" | "ปุ่มแพด" | "دکمه_دسته" | "زر_اليد" | "כפתור_בקר" | "گیم_پیڈ_بٹن" | "bouton_manette" | "gamepad_taste" | "кнопка_геймпада" =>
4832            {
4833                #[cfg(not(target_arch = "wasm32"))]
4834                {
4835                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4836                    let name = self.arg_str(&args, 1, "");
4837                    let down = parse_pad_button(&name)
4838                        .is_some_and(|b| self.with_pad(i, false, |p| p.is_down(b)));
4839                    return Ok(Value::Bool(down));
4840                }
4841                #[cfg(target_arch = "wasm32")]
4842                {
4843                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4844                    let name = self.arg_str(&args, 1, "");
4845                    return Ok(Value::Bool(input_web::button_down(i, &name)));
4846                }
4847            },
4848            // pad_pressed(i, name) → bool — pressed this frame?
4849            // On WASM we only have the current snapshot, so treat as button_down.
4850            "pad_pressed" | "手柄按下" | "パッド押下" | "패드눌림" | "แพดกด" | "دکمه_دسته_فشرده" | "زر_اليد_مضغوط" | "כפתור_בקר_לחוץ" | "گیم_پیڈ_دبایا" | "manette_appuyée" | "gamepad_gedrückt" | "геймпад_нажат" =>
4851            {
4852                #[cfg(not(target_arch = "wasm32"))]
4853                {
4854                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4855                    let name = self.arg_str(&args, 1, "");
4856                    let p = parse_pad_button(&name)
4857                        .is_some_and(|b| self.with_pad(i, false, |g| g.just_pressed(b)));
4858                    return Ok(Value::Bool(p));
4859                }
4860                #[cfg(target_arch = "wasm32")]
4861                {
4862                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4863                    let name = self.arg_str(&args, 1, "");
4864                    return Ok(Value::Bool(input_web::button_down(i, &name)));
4865                }
4866            },
4867            // pad_lx(i)/pad_ly(i)/pad_rx(i)/pad_ry(i) → number — stick axes (−1..=1).
4868            "pad_lx" | "手柄左X" | "パッド左X" | "패드왼X" | "แพดซ้ายX" | "آنالوگ_چپ_ایکس" | "عصا_اليسرى_س" | "ג'ויסטיק_שמאל_X" | "بائیں_اسٹک_ایکس" | "manette_axe_gauche_x" | "gamepad_lx" | "геймпад_ось_лево_x" => {
4869                #[cfg(not(target_arch = "wasm32"))]
4870                {
4871                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4872                    return Ok(Value::Number(
4873                        self.with_pad(i, 0.0, |p| p.left_stick.x as f64),
4874                    ));
4875                }
4876                #[cfg(target_arch = "wasm32")]
4877                {
4878                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4879                    return Ok(Value::Number(input_web::axis_lx(i) as f64));
4880                }
4881            },
4882            "pad_ly" | "手柄左Y" | "パッド左Y" | "패드왼Y" | "แพดซ้ายY" | "آنالوگ_چپ_ایگرگ" | "عصا_اليسرى_ص" | "ג'ויסטיק_שמאל_Y" | "بائیں_اسٹک_وائی" | "manette_axe_gauche_y" | "gamepad_ly" | "геймпад_ось_лево_y" => {
4883                #[cfg(not(target_arch = "wasm32"))]
4884                {
4885                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4886                    return Ok(Value::Number(
4887                        self.with_pad(i, 0.0, |p| p.left_stick.y as f64),
4888                    ));
4889                }
4890                #[cfg(target_arch = "wasm32")]
4891                {
4892                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4893                    return Ok(Value::Number(input_web::axis_ly(i) as f64));
4894                }
4895            },
4896            "pad_rx" | "手柄右X" | "パッド右X" | "패드오X" | "แพดขวาX" | "آنالوگ_راست_ایکس" | "عصا_اليمنى_س" | "ג'ויסטיק_ימין_X" | "دائیں_اسٹک_ایکس" | "manette_axe_droit_x" | "gamepad_rx" | "геймпад_ось_право_x" => {
4897                #[cfg(not(target_arch = "wasm32"))]
4898                {
4899                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4900                    return Ok(Value::Number(
4901                        self.with_pad(i, 0.0, |p| p.right_stick.x as f64),
4902                    ));
4903                }
4904                #[cfg(target_arch = "wasm32")]
4905                {
4906                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4907                    return Ok(Value::Number(input_web::axis_rx(i) as f64));
4908                }
4909            },
4910            "pad_ry" | "手柄右Y" | "パッド右Y" | "패드오Y" | "แพดขวาY" | "آنالوگ_راست_ایگرگ" | "عصا_اليمنى_ص" | "ג'ויסטיק_ימין_Y" | "دائیں_اسٹک_وائی" | "manette_axe_droit_y" | "gamepad_ry" | "геймпад_ось_право_y" => {
4911                #[cfg(not(target_arch = "wasm32"))]
4912                {
4913                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4914                    return Ok(Value::Number(
4915                        self.with_pad(i, 0.0, |p| p.right_stick.y as f64),
4916                    ));
4917                }
4918                #[cfg(target_arch = "wasm32")]
4919                {
4920                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4921                    return Ok(Value::Number(input_web::axis_ry(i) as f64));
4922                }
4923            },
4924            // pad_lt(i)/pad_rt(i) → number — analog triggers (0..=1).
4925            "pad_lt" | "手柄左扳机" | "パッド左トリガー" | "패드왼트리거" | "ไกแพดซ้าย" | "ماشه_چپ" | "زناد_اليسار" | "הדק_שמאל" | "بائیں_ٹریگر" | "manette_gâchette_gauche" | "gamepad_lt" | "геймпад_триггер_лево" =>
4926            {
4927                #[cfg(not(target_arch = "wasm32"))]
4928                {
4929                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4930                    return Ok(Value::Number(
4931                        self.with_pad(i, 0.0, |p| p.left_trigger as f64),
4932                    ));
4933                }
4934                #[cfg(target_arch = "wasm32")]
4935                {
4936                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4937                    return Ok(Value::Number(input_web::trigger_lt(i) as f64));
4938                }
4939            },
4940            "pad_rt" | "手柄右扳机" | "パッド右トリガー" | "패드오트리거" | "ไกแพดขวา" | "ماشه_راست" | "زناد_اليمين" | "הדק_ימין" | "دائیں_ٹریگر" | "manette_gâchette_droite" | "gamepad_rt" | "геймпад_триггер_право" =>
4941            {
4942                #[cfg(not(target_arch = "wasm32"))]
4943                {
4944                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4945                    return Ok(Value::Number(
4946                        self.with_pad(i, 0.0, |p| p.right_trigger as f64),
4947                    ));
4948                }
4949                #[cfg(target_arch = "wasm32")]
4950                {
4951                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4952                    return Ok(Value::Number(input_web::trigger_rt(i) as f64));
4953                }
4954            },
4955            // pad_rumble(i, lo, hi) → unit — set rumble motor amplitudes (0..=1).
4956            "pad_rumble" | "手柄震动" | "パッド振動" | "패드진동" | "แพดสั่น" | "لرزش_دسته" | "اهتزاز_اليد" | "רטט_בקר" | "گیم_پیڈ_تھرتھراہٹ" | "vibration_manette" | "gamepad_vibration" | "вибрация_геймпада" =>
4957            {
4958                #[cfg(not(target_arch = "wasm32"))]
4959                {
4960                    use ling_input::backend::InputBackend;
4961                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4962                    let lo = self.arg_num(&args, 1, 0.0)? as f32;
4963                    let hi = self.arg_num(&args, 2, lo as f64)? as f32;
4964                    let mut inp = self.input.borrow_mut();
4965                    if let Some(s) = inp.as_mut() {
4966                        if let Some(dev) = s.sensorium.devices.for_player(i as u8).map(|d| d.id) {
4967                            s.backend.set_rumble(
4968                                dev,
4969                                ling_input::Rumble { low: lo, high: hi, ..Default::default() },
4970                            );
4971                        }
4972                    }
4973                    return Ok(Value::Unit);
4974                }
4975                #[cfg(target_arch = "wasm32")]
4976                return Ok(Value::Unit);
4977            },
4978
4979            // ── set_camera_pos(x, y, z) — move camera to world position ──
4980            "set_camera_pos" | "ตั้งตำแหน่งกล้อง" | "镜坐标" | "カメラ座標" | "카메라좌표" | "تنظیم_موقعیت_دوربین" | "عيّن_موضع_الكاميرا" | "קבע_מיקום_מצלמה" | "کیمرہ_مقام_مقرر_کرو" | "définir_position_caméra" | "kameraposition_setzen" | "задать_позицию_камеры" =>
4981            {
4982                let x = self.arg_num(&args, 0, 0.0)? as f32;
4983                let y = self.arg_num(&args, 1, 0.0)? as f32;
4984                let z = self.arg_num(&args, 2, 0.0)? as f32;
4985                {
4986                    let mut gfx = self.gfx.borrow_mut();
4987                    gfx.camera.tx = x;
4988                    gfx.camera.ty = y;
4989                    gfx.camera.tz = z;
4990                }
4991                #[cfg(not(target_arch = "wasm32"))]
4992                if let Some(audio) = &self.audio {
4993                    audio.set_listener_pos(x, y, z);
4994                }
4995                return Ok(Value::Unit);
4996            },
4997
4998            // ── move_camera(dx, dy, dz) — translate camera by delta ──
4999            "move_camera" => {
5000                let dx = self.arg_num(&args, 0, 0.0)? as f32;
5001                let dy = self.arg_num(&args, 1, 0.0)? as f32;
5002                let dz = self.arg_num(&args, 2, 0.0)? as f32;
5003                let mut gfx = self.gfx.borrow_mut();
5004                gfx.camera.tx += dx;
5005                gfx.camera.ty += dy;
5006                gfx.camera.tz += dz;
5007                return Ok(Value::Unit);
5008            },
5009
5010            // ── set_zdist(d) — set perspective z-offset (field-of-view taper) ──
5011            "set_zdist" | "ตั้งระยะห่าง" | "镜距" | "Z距離設定" | "Z거리설정" | "تنظیم_فاصله_عمق" | "عيّن_مسافة_العمق" | "קבע_מרחק_עומק" | "گہرائی_فاصلہ_مقرر_کرو" | "définir_distance_z" | "z_abstand_setzen" | "задать_дистанцию_z" =>
5012            {
5013                let d = self.arg_num(&args, 0, 5.0)? as f32;
5014                self.gfx.borrow_mut().camera.zdist = d;
5015                return Ok(Value::Unit);
5016            },
5017
5018            // ── capture_mouse() — hide cursor and warp to centre each frame ──
5019            "capture_mouse" | "จับเมาส์" | "捕鼠" | "マウス捕捉" | "마우스잡기" | "ضبط_ماوس" | "امسك_الفأرة" | "לכוד_עכבר" | "ماؤس_پکڑو" | "capturer_souris" | "maus_erfassen" | "захватить_мышь" =>
5020            {
5021                #[cfg(not(target_arch = "wasm32"))]
5022                {
5023                    let mut gfx = self.gfx.borrow_mut();
5024                    gfx.mouse_captured = true;
5025                    gfx.last_mx = f32::NAN;
5026                    if let Some(win) = gfx.window.as_mut() {
5027                        win.set_cursor_visibility(false);
5028                    }
5029                }
5030                return Ok(Value::Unit);
5031            },
5032
5033            // ── release_mouse() — restore cursor and remove clip region ──
5034            "release_mouse" => {
5035                #[cfg(not(target_arch = "wasm32"))]
5036                {
5037                    let mut gfx = self.gfx.borrow_mut();
5038                    gfx.mouse_captured = false;
5039                    gfx.last_mx = f32::NAN;
5040                    if let Some(win) = gfx.window.as_mut() {
5041                        win.set_cursor_visibility(true);
5042                    }
5043                    #[cfg(windows)]
5044                    unsafe {
5045                        // Null releases the clip; reuse the RECT-typed declaration above.
5046                        extern "system" {
5047                            fn ClipCursor(lpRect: *const std::ffi::c_void) -> i32;
5048                        }
5049                        ClipCursor(std::ptr::null());
5050                    }
5051                }
5052                return Ok(Value::Unit);
5053            },
5054
5055            // ── cursor_hide() / cursor_show() — just the OS cursor's visibility,
5056            // no warp-to-centre or clip region (unlike capture_mouse/release_mouse,
5057            // which are for FPS-style look-around). For point-and-click play where
5058            // the cursor still needs to move freely and mouse_x()/mouse_y() still
5059            // need to track real position, just hide the system pointer glyph.
5060            "cursor_hide" => {
5061                #[cfg(not(target_arch = "wasm32"))]
5062                if let Some(win) = self.gfx.borrow_mut().window.as_mut() {
5063                    win.set_cursor_visibility(false);
5064                }
5065                return Ok(Value::Unit);
5066            },
5067            "cursor_show" => {
5068                #[cfg(not(target_arch = "wasm32"))]
5069                if let Some(win) = self.gfx.borrow_mut().window.as_mut() {
5070                    win.set_cursor_visibility(true);
5071                }
5072                return Ok(Value::Unit);
5073            },
5074
5075            // ══════════════════════════════════════════════════════════════════
5076            // 3-D / 4-D DRAWING — camera, lights, depth-sorted geometry
5077            // ══════════════════════════════════════════════════════════════════
5078
5079            // ── set_camera(cry, sry, crx, srx) — store precomputed camera trig ──
5080            // Call once per frame after computing cos/sin of your rotation angles.
5081            "set_camera" | "ตั้งกล้อง" | "设镜" | "设置摄像机" | "カメラ設定" | "카메라설정" | "تنظیم_دوربین" | "عيّن_الكاميرا" | "קבע_מצלמה" | "کیمرہ_مقرر_کرو" | "définir_caméra" | "kamera_setzen" | "задать_камеру" =>
5082            {
5083                let cry = self.arg_num(&args, 0, 1.0)? as f32;
5084                let sry = self.arg_num(&args, 1, 0.0)? as f32;
5085                let crx = self.arg_num(&args, 2, 1.0)? as f32;
5086                let srx = self.arg_num(&args, 3, 0.0)? as f32;
5087                let mut gfx = self.gfx.borrow_mut();
5088                gfx.camera.cry = cry;
5089                gfx.camera.sry = sry;
5090                gfx.camera.crx = crx;
5091                gfx.camera.srx = srx;
5092                return Ok(Value::Unit);
5093            },
5094
5095            // ── set_projection(cx, cy, focal, zdist) — override projection params ──
5096            // Automatically set when the window opens; override only if needed.
5097            "set_projection" | "ตั้งโปรเจกชัน" | "投影" | "投影設定" | "투영설정" | "تنظیم_فرافکنی" | "عيّن_الإسقاط" | "קבע_הטלה" | "پروجیکشن_مقرر_کرو" | "définir_projection" | "projektion_setzen" | "задать_проекцию" =>
5098            {
5099                let cx = self.arg_num(&args, 0, 960.0)? as f32;
5100                let cy = self.arg_num(&args, 1, 540.0)? as f32;
5101                let focal = self.arg_num(&args, 2, 1080.0)? as f32;
5102                let zdist = self.arg_num(&args, 3, 5.0)? as f32;
5103                let mut gfx = self.gfx.borrow_mut();
5104                gfx.camera.cx = cx;
5105                gfx.camera.cy = cy;
5106                gfx.camera.focal = focal;
5107                gfx.camera.zdist = zdist;
5108                return Ok(Value::Unit);
5109            },
5110
5111            // ── mesh_load(path) → handle · loads a glb/gltf (skeleton + skin + animation) ──
5112            "gltf_load" => {
5113                let path = self.arg_str(&args, 0, "");
5114                match ling_physics::gltf::GltfModel::load(&path) {
5115                    Ok(m) => {
5116                        self.gltf_models.borrow_mut().push(m);
5117                        let h = self.gltf_models.borrow().len() - 1;
5118                        return Ok(Value::Number(h as f64));
5119                    }
5120                    Err(e) => {
5121                        eprintln!("mesh_load failed ({path}): {e}");
5122                        return Ok(Value::Number(-1.0));
5123                    }
5124                }
5125            },
5126            // mesh_anim_count(handle) → number of animation clips
5127            "gltf_anim_count" => {
5128                let h = self.arg_num(&args, 0, -1.0)? as i64;
5129                let n = self
5130                    .gltf_models
5131                    .borrow()
5132                    .get(h as usize)
5133                    .map(|m| m.animations.len())
5134                    .unwrap_or(0);
5135                return Ok(Value::Number(n as f64));
5136            },
5137            // mesh_anim_name(handle, i) → clip name
5138            "gltf_anim_name" => {
5139                let h = self.arg_num(&args, 0, -1.0)? as i64;
5140                let i = self.arg_num(&args, 1, 0.0)? as usize;
5141                let s = self
5142                    .gltf_models
5143                    .borrow()
5144                    .get(h as usize)
5145                    .and_then(|m| m.animations.get(i))
5146                    .map(|a| a.name.clone())
5147                    .unwrap_or_default();
5148                return Ok(Value::Str(s));
5149            },
5150            // mesh_anim_dur(handle, i) → clip duration (seconds)
5151            "gltf_anim_dur" => {
5152                let h = self.arg_num(&args, 0, -1.0)? as i64;
5153                let i = self.arg_num(&args, 1, 0.0)? as usize;
5154                let d = self
5155                    .gltf_models
5156                    .borrow()
5157                    .get(h as usize)
5158                    .and_then(|m| m.animations.get(i))
5159                    .map(|a| a.duration)
5160                    .unwrap_or(0.0);
5161                return Ok(Value::Number(d as f64));
5162            },
5163            // mesh_tris(handle) → total triangle count (perf sanity check)
5164            "gltf_tris" => {
5165                let h = self.arg_num(&args, 0, -1.0)? as i64;
5166                let n: usize = self
5167                    .gltf_models
5168                    .borrow()
5169                    .get(h as usize)
5170                    .map(|m| m.meshes.iter().map(|mm| mm.indices.len()).sum::<usize>() / 3)
5171                    .unwrap_or(0);
5172                return Ok(Value::Number(n as f64));
5173            },
5174
5175            // gltf_joint_count(handle) → number of skin joints (bones)
5176            "gltf_joint_count" => {
5177                let h = self.arg_num(&args, 0, -1.0)? as i64;
5178                let n = self
5179                    .gltf_models
5180                    .borrow()
5181                    .get(h as usize)
5182                    .and_then(|m| m.skins.first())
5183                    .map(|s| s.joints.len())
5184                    .unwrap_or(0);
5185                return Ok(Value::Number(n as f64));
5186            },
5187            // gltf_joint_name(handle, j) → bone name (its node's name)
5188            "gltf_joint_name" => {
5189                let h = self.arg_num(&args, 0, -1.0)? as i64;
5190                let j = self.arg_num(&args, 1, 0.0)? as usize;
5191                let models = self.gltf_models.borrow();
5192                let s = models
5193                    .get(h as usize)
5194                    .and_then(|m| {
5195                        m.skins
5196                            .first()
5197                            .and_then(|sk| sk.joints.get(j))
5198                            .and_then(|jt| m.nodes.get(jt.node_idx))
5199                            .map(|n| n.name.clone())
5200                    })
5201                    .unwrap_or_default();
5202                return Ok(Value::Str(s));
5203            },
5204
5205            // ── gltf_draw(handle, ox,oy,oz, scale, yaw) — filled render of a loaded model ──
5206            //   glTF is Y-up / -Z-forward; the engine is Y-down, so we flip Y and Z, then
5207            //   yaw about Y, scale, translate. Per-part colour by mesh name. Lit + depth-queued
5208            //   exactly like draw_mesh, so it shares the camera + z-buffer.
5209            "gltf_draw" => {
5210                let hh = self.arg_num(&args, 0, -1.0)? as i64;
5211                let ox = self.arg_num(&args, 1, 0.0)? as f32;
5212                let oy = self.arg_num(&args, 2, 0.0)? as f32;
5213                let oz = self.arg_num(&args, 3, 0.0)? as f32;
5214                let scale = self.arg_num(&args, 4, 1.0)? as f32;
5215                let yaw = self.arg_num(&args, 5, 0.0)? as f32;
5216                let (sy, cyy) = yaw.sin_cos();
5217                let models = self.gltf_models.borrow();
5218                let model = match models.get(hh as usize) {
5219                    Some(m) => m,
5220                    None => return Ok(Value::Unit),
5221                };
5222                let mut gfx = self.gfx.borrow_mut();
5223                let cp = {
5224                    let c = &gfx.camera;
5225                    ling_gpu::CameraParams {
5226                        cry: c.cry, sry: c.sry, crx: c.crx, srx: c.srx,
5227                        cx: c.cx, cy: c.cy, focal: c.focal, zdist: c.zdist,
5228                        tx: c.tx, ty: c.ty, tz: c.tz,
5229                    }
5230                };
5231                let near = -gfx.camera.zdist + 0.02;
5232                let ambient = gfx.ambient;
5233                for mesh in &model.meshes {
5234                    let nlow = mesh.name.to_lowercase();
5235                    let base: u32 = if nlow.contains("hair") {
5236                        0x7a4a28
5237                    } else if nlow.contains("cloth") || nlow.contains("top") {
5238                        0x4a86e0
5239                    } else if nlow.contains("wing") {
5240                        0xe6ecf5
5241                    } else if nlow.contains("star") {
5242                        0xffd24d
5243                    } else {
5244                        0xf2d6b8
5245                    };
5246                    let nv = mesh.verts.len();
5247                    if nv == 0 {
5248                        continue;
5249                    }
5250                    let mut world = vec![0.0f32; nv * 3];
5251                    for (i, v) in mesh.verts.iter().enumerate() {
5252                        let gx = v.pos.x * scale;
5253                        let gy = -v.pos.y * scale;
5254                        let gz = -v.pos.z * scale;
5255                        let rx = gx * cyy + gz * sy;
5256                        let rz = -gx * sy + gz * cyy;
5257                        world[i * 3] = ox + rx;
5258                        world[i * 3 + 1] = oy + gy;
5259                        world[i * 3 + 2] = oz + rz;
5260                    }
5261                    let mut proj = vec![0.0f32; nv * 3];
5262                    ling_gpu::backend().project_points(&world, &cp, &mut proj);
5263                    let idx = &mesh.indices;
5264                    let nt = idx.len() / 3;
5265                    for t in 0..nt {
5266                        let ia = idx[t * 3] as usize;
5267                        let ib = idx[t * 3 + 1] as usize;
5268                        let ic = idx[t * 3 + 2] as usize;
5269                        if ia >= nv || ib >= nv || ic >= nv {
5270                            continue;
5271                        }
5272                        let (da, db, dc) = (proj[ia * 3 + 2], proj[ib * 3 + 2], proj[ic * 3 + 2]);
5273                        if (da + db + dc) / 3.0 <= near {
5274                            continue;
5275                        }
5276                        let col = {
5277                            let (ax, ay, az) = (world[ia * 3], world[ia * 3 + 1], world[ia * 3 + 2]);
5278                            let (bx, by, bz) = (world[ib * 3], world[ib * 3 + 1], world[ib * 3 + 2]);
5279                            let (px, py, pz) = (world[ic * 3], world[ic * 3 + 1], world[ic * 3 + 2]);
5280                            let (ux, uy, uz) = (bx - ax, by - ay, bz - az);
5281                            let (vx, vy, vz) = (px - ax, py - ay, pz - az);
5282                            let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
5283                            let centroid =
5284                                [(ax + bx + px) / 3.0, (ay + by + py) / 3.0, (az + bz + pz) / 3.0];
5285                            if gfx.flat_shade {
5286                                base
5287                            } else {
5288                                crate::gfx::light::compute_lit_color(
5289                                    base, normal, centroid, &gfx.lights, ambient,
5290                                )
5291                            }
5292                        };
5293                        let depth = (da + db + dc) / 3.0;
5294                        let col = gfx.fog_apply(col, depth);
5295                        gfx.depth_queue.push_triangle_zv(
5296                            col,
5297                            proj[ia * 3], proj[ia * 3 + 1], da,
5298                            proj[ib * 3], proj[ib * 3 + 1], db,
5299                            proj[ic * 3], proj[ic * 3 + 1], dc,
5300                        );
5301                    }
5302                }
5303                return Ok(Value::Unit);
5304            },
5305
5306            // ── gltf_autorig(handle) → synthesize a humanoid skeleton + skin weights ──
5307            "gltf_autorig" => {
5308                let hh = self.arg_num(&args, 0, -1.0)? as i64;
5309                let mut models = self.gltf_models.borrow_mut();
5310                if let Some(m) = models.get_mut(hh as usize) {
5311                    return Ok(Value::Number(m.autorig() as f64));
5312                }
5313                return Ok(Value::Number(0.0));
5314            },
5315
5316            // ── gltf_pose_draw(handle, ox,oy,oz, scale, yaw, poseList) ──
5317            //   Like gltf_draw, but linear-blend-skins the mesh by `poseList` first.
5318            //   poseList = flat XYZ-euler radians, 3 per bone (12 bones → 36 values).
5319            "gltf_pose_draw" => {
5320                let hh = self.arg_num(&args, 0, -1.0)? as i64;
5321                let ox = self.arg_num(&args, 1, 0.0)? as f32;
5322                let oy = self.arg_num(&args, 2, 0.0)? as f32;
5323                let oz = self.arg_num(&args, 3, 0.0)? as f32;
5324                let scale = self.arg_num(&args, 4, 1.0)? as f32;
5325                let yaw = self.arg_num(&args, 5, 0.0)? as f32;
5326                let euler: Vec<f32> = match args.get(6) {
5327                    Some(Value::List(v)) => {
5328                        v.iter().map(|x| self.to_number(x).unwrap_or(0.0) as f32).collect()
5329                    },
5330                    _ => Vec::new(),
5331                };
5332                let (sy, cyy) = yaw.sin_cos();
5333                let models = self.gltf_models.borrow();
5334                let model = match models.get(hh as usize) {
5335                    Some(m) => m,
5336                    None => return Ok(Value::Unit),
5337                };
5338                let skinned = model.skin_local(&euler);
5339                let mut gfx = self.gfx.borrow_mut();
5340                let cp = {
5341                    let c = &gfx.camera;
5342                    ling_gpu::CameraParams {
5343                        cry: c.cry, sry: c.sry, crx: c.crx, srx: c.srx,
5344                        cx: c.cx, cy: c.cy, focal: c.focal, zdist: c.zdist,
5345                        tx: c.tx, ty: c.ty, tz: c.tz,
5346                    }
5347                };
5348                let near = -gfx.camera.zdist + 0.02;
5349                let ambient = gfx.ambient;
5350                for (mi, mesh) in model.meshes.iter().enumerate() {
5351                    let nlow = mesh.name.to_lowercase();
5352                    let base: u32 = if nlow.contains("hair") {
5353                        0x7a4a28
5354                    } else if nlow.contains("cloth") || nlow.contains("top") {
5355                        0x4a86e0
5356                    } else if nlow.contains("wing") {
5357                        0xe6ecf5
5358                    } else if nlow.contains("star") {
5359                        0xffd24d
5360                    } else {
5361                        0xf2d6b8
5362                    };
5363                    let sk = match skinned.get(mi) {
5364                        Some(s) => s,
5365                        None => continue,
5366                    };
5367                    let nv = sk.len();
5368                    if nv == 0 {
5369                        continue;
5370                    }
5371                    let mut world = vec![0.0f32; nv * 3];
5372                    for i in 0..nv {
5373                        let gx = sk[i][0] * scale;
5374                        let gy = -sk[i][1] * scale;
5375                        let gz = -sk[i][2] * scale;
5376                        let rx = gx * cyy + gz * sy;
5377                        let rz = -gx * sy + gz * cyy;
5378                        world[i * 3] = ox + rx;
5379                        world[i * 3 + 1] = oy + gy;
5380                        world[i * 3 + 2] = oz + rz;
5381                    }
5382                    let mut proj = vec![0.0f32; nv * 3];
5383                    ling_gpu::backend().project_points(&world, &cp, &mut proj);
5384                    let idx = &mesh.indices;
5385                    let nt = idx.len() / 3;
5386                    for t in 0..nt {
5387                        let ia = idx[t * 3] as usize;
5388                        let ib = idx[t * 3 + 1] as usize;
5389                        let ic = idx[t * 3 + 2] as usize;
5390                        if ia >= nv || ib >= nv || ic >= nv {
5391                            continue;
5392                        }
5393                        let (da, db, dc) = (proj[ia * 3 + 2], proj[ib * 3 + 2], proj[ic * 3 + 2]);
5394                        if (da + db + dc) / 3.0 <= near {
5395                            continue;
5396                        }
5397                        let col = {
5398                            let (ax, ay, az) = (world[ia * 3], world[ia * 3 + 1], world[ia * 3 + 2]);
5399                            let (bx, by, bz) = (world[ib * 3], world[ib * 3 + 1], world[ib * 3 + 2]);
5400                            let (px, py, pz) = (world[ic * 3], world[ic * 3 + 1], world[ic * 3 + 2]);
5401                            let (ux, uy, uz) = (bx - ax, by - ay, bz - az);
5402                            let (vx, vy, vz) = (px - ax, py - ay, pz - az);
5403                            let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
5404                            let centroid =
5405                                [(ax + bx + px) / 3.0, (ay + by + py) / 3.0, (az + bz + pz) / 3.0];
5406                            if gfx.flat_shade {
5407                                base
5408                            } else {
5409                                crate::gfx::light::compute_lit_color(
5410                                    base, normal, centroid, &gfx.lights, ambient,
5411                                )
5412                            }
5413                        };
5414                        let depth = (da + db + dc) / 3.0;
5415                        let col = gfx.fog_apply(col, depth);
5416                        gfx.depth_queue.push_triangle_zv(
5417                            col,
5418                            proj[ia * 3], proj[ia * 3 + 1], da,
5419                            proj[ib * 3], proj[ib * 3 + 1], db,
5420                            proj[ic * 3], proj[ic * 3 + 1], dc,
5421                        );
5422                    }
5423                }
5424                return Ok(Value::Unit);
5425            },
5426
5427            // ── draw_mesh(pos, idx, ox, oy, oz, scale, mode) ──
5428            //   Native batched triangle mesh. pos = flat [x,y,z,…], idx = flat tri indices.
5429            //   mode 0 = lit with current pen colour; 1 = per-face hue cycle.
5430            //   Vertices are batch-projected via ling-gpu (CPU fallback, or CUDA when the
5431            //   `cuda` feature is on); the per-triangle loop runs natively (not in the
5432            //   interpreter) so dense meshes (imported glTF, grids) stay fast.
5433            "draw_mesh" | "วาดเมช" | "رسم_مش" | "ارسم_شبكة" | "צייר_רשת" | "میش_کھینچو" | "dessiner_maillage" | "netz_zeichnen" | "рисовать_меш" => {
5434                let pos = match args.first() {
5435                    Some(Value::List(v)) => v,
5436                    _ => return Ok(Value::Unit),
5437                };
5438                let idx = match args.get(1) {
5439                    Some(Value::List(v)) => v,
5440                    _ => return Ok(Value::Unit),
5441                };
5442                let ox = self.arg_num(&args, 2, 0.0)? as f32;
5443                let oy = self.arg_num(&args, 3, 0.0)? as f32;
5444                let oz = self.arg_num(&args, 4, 0.0)? as f32;
5445                let scale = self.arg_num(&args, 5, 1.0)? as f32;
5446                let mode = self.arg_num(&args, 6, 0.0)? as i64;
5447                let nv = pos.len() / 3;
5448                if nv == 0 {
5449                    return Ok(Value::Unit);
5450                }
5451                let mut world = vec![0.0f32; nv * 3];
5452                for i in 0..nv {
5453                    world[i * 3] = ox + self.to_number(&pos[i * 3]).unwrap_or(0.0) as f32 * scale;
5454                    world[i * 3 + 1] =
5455                        oy + self.to_number(&pos[i * 3 + 1]).unwrap_or(0.0) as f32 * scale;
5456                    world[i * 3 + 2] =
5457                        oz + self.to_number(&pos[i * 3 + 2]).unwrap_or(0.0) as f32 * scale;
5458                }
5459                let mut gfx = self.gfx.borrow_mut();
5460                let cp = {
5461                    let c = &gfx.camera;
5462                    ling_gpu::CameraParams {
5463                        cry: c.cry,
5464                        sry: c.sry,
5465                        crx: c.crx,
5466                        srx: c.srx,
5467                        cx: c.cx,
5468                        cy: c.cy,
5469                        focal: c.focal,
5470                        zdist: c.zdist,
5471                        tx: c.tx,
5472                        ty: c.ty,
5473                        tz: c.tz,
5474                    }
5475                };
5476                let near = -gfx.camera.zdist + 0.02;
5477                let base = gfx.color;
5478                let ambient = gfx.ambient;
5479                let mut proj = vec![0.0f32; nv * 3]; // (sx, sy, depth) per vertex
5480                ling_gpu::backend().project_points(&world, &cp, &mut proj);
5481                let nt = idx.len() / 3;
5482                for t in 0..nt {
5483                    let ia = self.to_number(&idx[t * 3]).unwrap_or(0.0) as usize;
5484                    let ib = self.to_number(&idx[t * 3 + 1]).unwrap_or(0.0) as usize;
5485                    let ic = self.to_number(&idx[t * 3 + 2]).unwrap_or(0.0) as usize;
5486                    if ia >= nv || ib >= nv || ic >= nv {
5487                        continue;
5488                    }
5489                    let (da, db, dc) = (proj[ia * 3 + 2], proj[ib * 3 + 2], proj[ic * 3 + 2]);
5490                    if (da + db + dc) / 3.0 <= near {
5491                        continue;
5492                    } // near-plane cull (centroid)
5493                    let col = if mode == 1 {
5494                        let h = t as f32 * 0.6;
5495                        let r = ((h.sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
5496                        let g = (((h + 2.094).sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
5497                        let b = (((h + 4.189).sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
5498                        (r << 16) | (g << 8) | b
5499                    } else {
5500                        let (ax, ay, az) = (world[ia * 3], world[ia * 3 + 1], world[ia * 3 + 2]);
5501                        let (bx, by, bz) = (world[ib * 3], world[ib * 3 + 1], world[ib * 3 + 2]);
5502                        let (px, py, pz) = (world[ic * 3], world[ic * 3 + 1], world[ic * 3 + 2]);
5503                        let (ux, uy, uz) = (bx - ax, by - ay, bz - az);
5504                        let (vx, vy, vz) = (px - ax, py - ay, pz - az);
5505                        let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
5506                        let centroid = [
5507                            (ax + bx + px) / 3.0,
5508                            (ay + by + py) / 3.0,
5509                            (az + bz + pz) / 3.0,
5510                        ];
5511                        if gfx.flat_shade {
5512                            base
5513                        } else {
5514                            crate::gfx::light::compute_lit_color(
5515                                base,
5516                                normal,
5517                                centroid,
5518                                &gfx.lights,
5519                                ambient,
5520                            )
5521                        }
5522                    };
5523                    let depth = (da + db + dc) / 3.0;
5524                    let col = gfx.fog_apply(col, depth);
5525                    // True per-vertex depth so the z-buffer resolves mesh
5526                    // self-occlusion (when depth_test is off, the flush ignores
5527                    // z and uses the screen x/y exactly as before).
5528                    gfx.depth_queue.push_triangle_zv(
5529                        col,
5530                        proj[ia * 3],
5531                        proj[ia * 3 + 1],
5532                        da,
5533                        proj[ib * 3],
5534                        proj[ib * 3 + 1],
5535                        db,
5536                        proj[ic * 3],
5537                        proj[ic * 3 + 1],
5538                        dc,
5539                    );
5540                }
5541                return Ok(Value::Unit);
5542            },
5543
5544            // ── add_light(x, y, z, r, g, b, intensity, radius) ──
5545            // Adds a point light in world space.  r/g/b in [0..1].
5546            // radius == 0 → no distance falloff.
5547            "add_light" | "เพิ่มแสง" | "加灯" | "ライト追加" | "조명추가" | "افزودن_نور" | "أضف_ضوء" | "הוסף_אור" | "روشنی_شامل_کرو" | "ajouter_lumière" | "licht_hinzufügen" | "добавить_свет" =>
5548            {
5549                let x = self.arg_num(&args, 0, 0.0)? as f32;
5550                let y = self.arg_num(&args, 1, -3.0)? as f32;
5551                let z = self.arg_num(&args, 2, 3.0)? as f32;
5552                let mut r = self.arg_num(&args, 3, 1.0)? as f32;
5553                let mut g = self.arg_num(&args, 4, 1.0)? as f32;
5554                let mut b = self.arg_num(&args, 5, 1.0)? as f32;
5555                // Forgive 0-255 colour values: if any channel is clearly > 1,
5556                // treat the triple as 0-255 and normalise. Keeps 0-1 callers exact.
5557                if r > 1.5 || g > 1.5 || b > 1.5 {
5558                    r /= 255.0;
5559                    g /= 255.0;
5560                    b /= 255.0;
5561                }
5562                let intensity = self.arg_num(&args, 6, 1.0)? as f32;
5563                let radius = self.arg_num(&args, 7, 0.0)? as f32;
5564                self.gfx
5565                    .borrow_mut()
5566                    .lights
5567                    .push(Light { x, y, z, r, g, b, intensity, radius });
5568                return Ok(Value::Unit);
5569            },
5570
5571            // ── clear_lights() — remove all lights ──
5572            "clear_lights" | "ล้างแสง" | "清灯" | "ライト消去" | "조명초기화" | "پاک‌کردن_نورها" | "امسح_الأضواء" | "נקה_אורות" | "روشنیاں_صاف_کرو" | "effacer_lumières" | "lichter_löschen" | "очистить_свет" =>
5573            {
5574                self.gfx.borrow_mut().lights.clear();
5575                return Ok(Value::Unit);
5576            },
5577
5578            // ── set_material(key, value) — configure LingMaterial field ──
5579            // Activates the material BSDF for subsequent polygon/triangle draws.
5580            // Keys (string): "albedo" "roughness" "metallic" "emission"
5581            //   "emission_strength" "specular" "specular_tint" "subsurface"
5582            //   "subsurface_color" "clearcoat" "clearcoat_roughness"
5583            //   "transmission" "ior" "iridescence" "sheen" "anisotropy"
5584            //   "anisotropy_angle" "toon_bands" "shadow_softness"
5585            //   "outline_px" "outline_color" "highlight_color"
5586            // Value: number (or packed 0xRRGGBB for colour fields)
5587            "set_material" | "ตั้งวัสดุ" | "设置材质" | "マテリアル設定" | "재질설정" | "تنظیم_متریال" | "عيّن_المادة" | "קבע_חומר" | "میٹریل_مقرر_کرو" =>
5588            {
5589                let key = self.arg_str(&args, 0, "");
5590                let val = self.arg_num(&args, 1, 0.0)?;
5591                let mut gfx = self.gfx.borrow_mut();
5592                let mat = gfx
5593                    .material
5594                    .get_or_insert_with(crate::gfx::LingMaterial::default);
5595                match key.as_str() {
5596                    "albedo" => mat.albedo = val as u32,
5597                    "roughness" => mat.roughness = val as f32,
5598                    "metallic" => mat.metallic = val as f32,
5599                    "emission" => mat.emission = val as u32,
5600                    "emission_strength" => mat.emission_strength = val as f32,
5601                    "specular" => mat.specular = val as f32,
5602                    "specular_tint" => mat.specular_tint = val as f32,
5603                    "subsurface" => mat.subsurface = val as f32,
5604                    "subsurface_color" => mat.subsurface_color = val as u32,
5605                    "clearcoat" => mat.clearcoat = val as f32,
5606                    "clearcoat_roughness" => mat.clearcoat_roughness = val as f32,
5607                    "transmission" => mat.transmission = val as f32,
5608                    "ior" => mat.ior = val as f32,
5609                    "iridescence" => mat.iridescence = val as f32,
5610                    "sheen" => mat.sheen = val as f32,
5611                    "anisotropy" => mat.anisotropy = val as f32,
5612                    "anisotropy_angle" => mat.anisotropy_angle = val as f32,
5613                    "toon_bands" => mat.toon_bands = val as u32,
5614                    "shadow_softness" => mat.shadow_softness = val as f32,
5615                    "outline_px" => mat.outline_px = val as f32,
5616                    "outline_color" => mat.outline_color = val as u32,
5617                    "highlight_color" => mat.highlight_color = val as u32,
5618                    _ => {},
5619                }
5620                return Ok(Value::Unit);
5621            },
5622
5623            // ── reset_material() — disable material override ──
5624            // After this call, draws use the legacy compute_lit_color_linear path.
5625            "reset_material" | "รีเซ็ตวัสดุ" | "重置材质" | "マテリアルリセット" | "재질초기화" | "بازنشانی_متریال" | "أعد_ضبط_المادة" | "אפס_חומר" | "میٹریل_ری_سیٹ" =>
5626            {
5627                self.gfx.borrow_mut().material = None;
5628                return Ok(Value::Unit);
5629            },
5630
5631            // ── toon_outlines(thickness, color, threshold) ──
5632            // Enable vector-smooth ink outlines on depth discontinuities.
5633            //   thickness  — ink-line half-width in pixels (0 = off, 1.5 = anime default)
5634            //   color      — 0xRRGGBB ink colour (default black = 0)
5635            //   threshold  — depth delta that triggers an edge (0.05 recommended)
5636            "toon_outlines"
5637            | "ตั้งเส้นขอบการ์ตูน"
5638            | "卡通轮廓"
5639            | "トゥーンアウトライン"
5640            | "툰아웃라인" | "خطوط_کارتونی" | "حدود_كرتونية" | "קווי_מתאר_מצוירים" | "ٹون_آؤٹ_لائنز" => {
5641                let px = self.arg_num(&args, 0, 0.0)? as f32;
5642                let color = self.arg_num(&args, 1, 0.0)? as u32;
5643                let thresh = self.arg_num(&args, 2, 0.05)? as f32;
5644                let mut gfx = self.gfx.borrow_mut();
5645                gfx.toon.outline_px = px;
5646                gfx.toon.outline_color = color;
5647                gfx.toon.outline_thresh = thresh;
5648                return Ok(Value::Unit);
5649            },
5650
5651            // ── tone_stop(t, value) ──
5652            // Add a stop to the tone ramp.
5653            //   t      — input luminance position [0..1]
5654            //   value  — output brightness [0..1]
5655            // Stops are automatically sorted; call tone_ramp_reset() first to clear.
5656            "tone_stop" | "ตั้งจุดโทน" | "色调停止" | "トーンストップ" | "톤스톱" | "نقطه_توقف_تن_رنگ" | "نقطة_توقف_اللون" | "נקודת_עצירת_גוון" | "ٹون_اسٹاپ" =>
5657            {
5658                let t = self.arg_num(&args, 0, 0.0)? as f32;
5659                let val = self.arg_num(&args, 1, 1.0)? as f32;
5660                let mut gfx = self.gfx.borrow_mut();
5661                gfx.toon.ramp.stops.push(crate::gfx::toon::ToneStop {
5662                    t: t.clamp(0.0, 1.0),
5663                    value: val.clamp(0.0, 1.0),
5664                });
5665                gfx.toon
5666                    .ramp
5667                    .stops
5668                    .sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap_or(std::cmp::Ordering::Equal));
5669                return Ok(Value::Unit);
5670            },
5671
5672            // ── tone_smooth(enabled) ──
5673            // 0 = hard cel snap between stops (default); 1 = smooth gradient lerp.
5674            "tone_smooth" | "ตั้งโทนนุ่ม" | "色调平滑" | "トーンスムーズ" | "톤스무스" | "تن_رنگ_نرم" | "تدرج_لون_ناعم" | "גוון_חלק" | "ٹون_ہموار" =>
5675            {
5676                let v = self.arg_num(&args, 0, 0.0)? as f32;
5677                self.gfx.borrow_mut().toon.ramp.smooth = v > 0.5;
5678                return Ok(Value::Unit);
5679            },
5680
5681            // ── tone_bezier(y1, y2) ──
5682            // Apply a cubic Bézier remap to the input luminance before stop lookup.
5683            //   y1, y2 — control-point y-values (identity: y1=0.333 y2=0.667)
5684            //   0 args or tone_bezier(0, 0)  → ease-in (shadow-heavy)
5685            //   tone_bezier(1, 1)            → ease-out (highlight-heavy)
5686            //   tone_bezier(0.1, 0.9)        → S-curve (smooth both ends)
5687            //   tone_bezier_off()            → disable (back to linear)
5688            "tone_bezier" | "ตั้งโทนเบซิเยร์" | "色调贝塞尔" | "トーンベジェ" | "톤베지어" | "تن_رنگ_بزیه" | "تدرج_لون_بيزيه" | "גוון_בזייה" | "بیزیئر_ٹون" =>
5689            {
5690                let y1 = self.arg_num(&args, 0, 1.0 / 3.0)? as f32;
5691                let y2 = self.arg_num(&args, 1, 2.0 / 3.0)? as f32;
5692                self.gfx.borrow_mut().toon.ramp.bezier = Some([y1, y2]);
5693                return Ok(Value::Unit);
5694            },
5695
5696            // ── tone_bezier_off() — disable Bézier remap ──
5697            "tone_bezier_off"
5698            | "ปิดโทนเบซิเยร์"
5699            | "关闭色调贝塞尔"
5700            | "トーンベジェオフ"
5701            | "톤베지어끄기" | "خاموش‌کردن_بزیه" | "إيقاف_تدرج_بيزيه" | "כבה_גוון_בזייה" | "بیزیئر_ٹون_بند" => {
5702                self.gfx.borrow_mut().toon.ramp.bezier = None;
5703                return Ok(Value::Unit);
5704            },
5705
5706            // ── tone_ramp_reset() — restore default 3-band cel ramp ──
5707            "tone_ramp_reset"
5708            | "รีเซ็ตการไล่โทน"
5709            | "重置色调渐变"
5710            | "トーンランプリセット"
5711            | "톤램프리셋" | "بازنشانی_شیب_تن_رنگ" | "أعد_ضبط_تدرج_اللون" | "אפס_שיפוע_גוון" | "ٹون_ریمپ_ری_سیٹ" => {
5712                self.gfx.borrow_mut().toon.ramp = crate::gfx::toon::ToneRamp::default();
5713                return Ok(Value::Unit);
5714            },
5715
5716            // ── tone_ramp_clear() — clear all stops (build your own ramp) ──
5717            "tone_ramp_clear"
5718            | "ล้างการไล่โทน"
5719            | "清除色调渐变"
5720            | "トーンランプクリア"
5721            | "톤램프클리어" | "پاک‌کردن_شیب_تن_رنگ" | "امسح_تدرج_اللون" | "נקה_שיפוע_גוון" | "ٹون_ریمپ_صاف" => {
5722                self.gfx.borrow_mut().toon.ramp.stops.clear();
5723                return Ok(Value::Unit);
5724            },
5725
5726            // ── tone_soft(soft, sheen) — band-edge softness + highlight sheen ──
5727            //   soft  [0..1] — fraction of each band gap that blends smoothly
5728            //                  across the boundary (0 = crisp cel, ~0.3 = soft
5729            //                  Wind Waker shadow edges). Default 0.32.
5730            //   sheen [0..1] — bright pixels keep their smooth gradient instead
5731            //                  of being quantised (clean specular/rim sheen
5732            //                  rather than scratchy banded highlights). 0.65.
5733            "tone_soft" | "โทนขอบนุ่ม" | "色调柔边" | "トーンソフト" | "톤소프트" | "تن_رنگ_لبه‌نرم" | "تدرج_ناعم_الحواف" | "גוון_קצה_רך" | "نرم_کنارہ_ٹون" | "tonalité_douce" | "weicher_ton" | "мягкий_тон" => {
5734                let s = self.arg_num(&args, 0, 0.32)? as f32;
5735                let sh = self.arg_num(&args, 1, 0.65)? as f32;
5736                let mut gfx = self.gfx.borrow_mut();
5737                gfx.toon.ramp.soft = s.clamp(0.0, 1.0);
5738                gfx.toon.ramp.sheen = sh.clamp(0.0, 1.0);
5739                return Ok(Value::Unit);
5740            },
5741
5742            // ── set_ssao(strength, radius_px, zrange) — ambient occlusion ──
5743            // Depth-buffer contact shading: soft darkening in corners/under
5744            // objects, computed half-res + smoothed (no grain). Needs
5745            // set_depth_test(1). strength 0 disables. Defaults (0.35, 6, 12).
5746            "set_ssao" | "ตั้งเงาสัมผัส" | "环境光遮蔽" | "アンビエントオクルージョン"
5747            | "앰비언트오클루전" | "تنظیم_انسداد_محیطی" | "عيّن_تظليل_محيطي" | "קבע_הצללה_סביבתית" | "ایس_ایس_اے_او_مقرر_کرو" | "définir_ssao" | "ssao_setzen" | "задать_ssao" => {
5748                let s = self.arg_num(&args, 0, 0.35)? as f32;
5749                let r = self.arg_num(&args, 1, 6.0)? as f32;
5750                let z = self.arg_num(&args, 2, 12.0)? as f32;
5751                let mut gfx = self.gfx.borrow_mut();
5752                gfx.toon.ao_strength = s.clamp(0.0, 1.0);
5753                gfx.toon.ao_radius = r.max(1.0);
5754                gfx.toon.ao_range = z.max(0.01);
5755                return Ok(Value::Unit);
5756            },
5757
5758            // ── set_fxaa(on) — FXAA-lite screen-space edge anti-aliasing ──
5759            // Softens polygon stair-steps and ink-line jaggies over the whole
5760            // frame; flat fills are untouched. Applied last in the present
5761            // post-chain. (set_antialias smooths wireframe STROKES; this pass
5762            // smooths the composited IMAGE.)
5763            "set_fxaa" | "ลบรอยหยัก" | "屏幕抗锯齿" | "画面アンチエイリアス" | "화면안티앨리어싱" | "تنظیم_ضدلبه‌دندانه_سریع" | "عيّن_مضاد_التسنن_السريع" | "קבע_החלקת_מסך" | "ایف_ایکس_اے_اے_مقرر_کرو" | "définir_fxaa" | "fxaa_setzen" | "задать_fxaa" => {
5764                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
5765                self.gfx.borrow_mut().toon.fxaa = on;
5766                return Ok(Value::Unit);
5767            },
5768
5769            // ── set_bloom(strength, threshold) — soft HDR-style glow ──
5770            // Bright pixels (rim sheen, emissive, additive FX) bleed a soft
5771            // quarter-res glow — the "HDR material" feel for toon/vector art.
5772            // strength 0 disables; threshold = luminance cutoff [0..1].
5773            "set_bloom" | "ตั้งบลูม" | "泛光" | "ブルーム" | "블룸" | "تنظیم_درخشش" | "عيّن_التوهج" | "קבע_זוהר" | "بلوم_مقرر_کرو" | "définir_bloom" | "bloom_setzen" | "задать_блум" => {
5774                let s = self.arg_num(&args, 0, 0.45)? as f32;
5775                let t = self.arg_num(&args, 1, 0.74)? as f32;
5776                let mut gfx = self.gfx.borrow_mut();
5777                gfx.toon.bloom_strength = s.max(0.0);
5778                gfx.toon.bloom_thresh = t.clamp(0.0, 0.99);
5779                return Ok(Value::Unit);
5780            },
5781
5782            // ── shadow_smooth(softness) [compat] → tone_smooth + tone_bezier ──
5783            // Deprecated: use tone_smooth + tone_bezier instead.
5784            "shadow_smooth" | "ตั้งเงานุ่ม" | "柔化阴影" | "影ソフト" | "그림자부드럽게" | "سایه_نرم" | "ظل_ناعم" | "צל_חלק" | "نرم_سایہ" =>
5785            {
5786                let s = self.arg_num(&args, 0, 0.0)? as f32;
5787                let mut gfx = self.gfx.borrow_mut();
5788                gfx.toon.ramp.smooth = s > 0.05;
5789                if s > 0.05 {
5790                    let y1 = (0.333 + s * 0.2).clamp(0.0, 1.0);
5791                    let y2 = (0.667 - s * 0.2).clamp(0.0, 1.0);
5792                    gfx.toon.ramp.bezier = Some([y1, y2]);
5793                } else {
5794                    gfx.toon.ramp.bezier = None;
5795                }
5796                return Ok(Value::Unit);
5797            },
5798
5799            // ── toon_highlight [compat] — no-op, use tone_stop instead ──
5800            "toon_highlight"
5801            | "ตั้งไฮไลท์การ์ตูน"
5802            | "卡通高光"
5803            | "トゥーンハイライト"
5804            | "툰하이라이트" | "هایلایت_کارتونی" | "إبراز_كرتوني" | "הדגשה_מצוירת" | "ٹون_ہائی_لائٹ" => {
5805                // Remap as a lit-band brightness boost: adds a stop near the highlight threshold.
5806                let _strength = self.arg_num(&args, 0, 0.0)? as f32;
5807                let _thresh = self.arg_num(&args, 2, 0.78)? as f32;
5808                // No-op: configure via tone_stop() for precise control.
5809                return Ok(Value::Unit);
5810            },
5811
5812            // ── set_ambient(v) — ambient light level [0..1] ──
5813            "set_ambient" | "ตั้งแสงรอบข้าง" | "环境光" | "環境光設定" | "환경광설정" | "تنظیم_نور_محیطی" | "عيّن_الإضاءة_المحيطة" | "קבע_תאורה_סביבתית" | "ماحولیاتی_روشنی_مقرر_کرو" | "définir_ambiante" | "umgebungslicht_setzen" | "задать_фон" =>
5814            {
5815                let v = self.arg_num(&args, 0, 0.15)? as f32;
5816                self.gfx.borrow_mut().ambient = v;
5817                return Ok(Value::Unit);
5818            },
5819
5820            // ── set_fog(r,g,b, start, end) — distance fog toward (r,g,b).
5821            //    triangles/lines fade from `start`..`end` camera depth. end<=0 = off.
5822            "set_fog" | "ตั้งหมอก" | "雾" | "霧設定" | "안개설정" | "تنظیم_مه" | "عيّن_الضباب" | "קבע_ערפל" | "دھند_مقرر_کرو" => {
5823                let r = self.arg_num(&args, 0, 0.0)?.clamp(0.0, 255.0) as u32;
5824                let g = self.arg_num(&args, 1, 0.0)?.clamp(0.0, 255.0) as u32;
5825                let b = self.arg_num(&args, 2, 0.0)?.clamp(0.0, 255.0) as u32;
5826                let start = self.arg_num(&args, 3, 0.0)? as f32;
5827                let end = self.arg_num(&args, 4, 0.0)? as f32;
5828                let mut gfx = self.gfx.borrow_mut();
5829                gfx.fog_color = (r << 16) | (g << 8) | b;
5830                gfx.fog_start = start;
5831                gfx.fog_end = end;
5832                return Ok(Value::Unit);
5833            },
5834
5835            // ── วาดสามเหลี่ยม3มิติ(ax,ay,az, bx,by,bz, cx,cy,cz) ──
5836            // Computes lighting from world-space normal + active lights (cel shading),
5837            // projects via the stored camera, and pushes to the depth queue.
5838            "วาดสามเหลี่ยม3มิติ" | "draw_triangle_3d" | "triangle3d" | "رسم_مثلث_سه‌بعدی" | "ارسم_مثلثا_ثلاثي_الأبعاد" | "צייר_משולש_תלת_ממדי" | "تھری_ڈی_مثلث_کھینچو" =>
5839            {
5840                let ax = self.arg_num(&args, 0, 0.0)? as f32;
5841                let ay = self.arg_num(&args, 1, 0.0)? as f32;
5842                let az = self.arg_num(&args, 2, 0.0)? as f32;
5843                let bx = self.arg_num(&args, 3, 0.0)? as f32;
5844                let by = self.arg_num(&args, 4, 0.0)? as f32;
5845                let bz = self.arg_num(&args, 5, 0.0)? as f32;
5846                let cx = self.arg_num(&args, 6, 0.0)? as f32;
5847                let cy = self.arg_num(&args, 7, 0.0)? as f32;
5848                let cz = self.arg_num(&args, 8, 0.0)? as f32;
5849
5850                let mut gfx = self.gfx.borrow_mut();
5851
5852                // Mesh capture: record raw local coords + pen colour, skip submit.
5853                if gfx.mesh_capture.is_some() {
5854                    let col = gfx.color;
5855                    gfx.mesh_capture
5856                        .as_mut()
5857                        .unwrap()
5858                        .push(([ax, ay, az, bx, by, bz, cx, cy, cz], col));
5859                    return Ok(Value::Unit);
5860                }
5861
5862                gfx.submit_triangle(ax, ay, az, bx, by, bz, cx, cy, cz);
5863                return Ok(Value::Unit);
5864            },
5865
5866            // ── เริ่มอบเมช() — begin capturing 3-D triangles into a display list ──
5867            "เริ่มอบเมช" | "mesh_bake_begin" | "شروع_پخت_مش" | "ابدأ_خبز_الشبكة" | "התחל_אפיית_רשת" | "میش_بیک_شروع" => {
5868                self.gfx.borrow_mut().mesh_capture = Some(Vec::new());
5869                return Ok(Value::Unit);
5870            },
5871
5872            // ── เมชแคชรับ(key) — keyed display-list cache lookup (-1 = miss) ──
5873            "เมชแคชรับ" | "mesh_cache_get" | "دریافت_کش_مش" | "اجلب_مخبأ_الشبكة" | "קבל_מטמון_רשת" | "میش_کیش_حاصل_کرو" => {
5874                let key = self.arg_num(&args, 0, 0.0)? as i64;
5875                let h = self.gfx.borrow().mesh_cache.get(&key).copied();
5876                return Ok(Value::Number(h.map(|x| x as f64).unwrap_or(-1.0)));
5877            },
5878
5879            // ── เมชแคชตั้ง(key, handle) — store a baked mesh under key (bounded) ──
5880            "เมชแคชตั้ง" | "mesh_cache_put" | "ذخیره_در_کش_مش" | "ضع_في_مخبأ_الشبكة" | "שמור_במטמון_רשת" | "میش_کیش_رکھو" => {
5881                let key = self.arg_num(&args, 0, 0.0)? as i64;
5882                let h = self.arg_num(&args, 1, 0.0)? as usize;
5883                let mut gfx = self.gfx.borrow_mut();
5884                const CAP: usize = 256;
5885                if gfx.mesh_cache.len() >= CAP {
5886                    let evict: Vec<usize> = gfx.mesh_cache.values().copied().collect();
5887                    gfx.mesh_cache.clear();
5888                    for id in evict {
5889                        if id < gfx.meshes.len() {
5890                            gfx.meshes[id].clear();
5891                            gfx.mesh_free.push(id);
5892                        }
5893                    }
5894                }
5895                gfx.mesh_cache.insert(key, h);
5896                return Ok(Value::Unit);
5897            },
5898
5899            // ── เมชแคชล้าง() — drop the keyed cache (e.g. on level change) ──
5900            "เมชแคชล้าง" | "mesh_cache_clear" | "پاک‌کردن_کش_مش" | "امسح_مخبأ_الشبكة" | "נקה_מטמון_רשת" | "میش_کیش_صاف" => {
5901                let mut gfx = self.gfx.borrow_mut();
5902                let evict: Vec<usize> = gfx.mesh_cache.values().copied().collect();
5903                gfx.mesh_cache.clear();
5904                for id in evict {
5905                    if id < gfx.meshes.len() {
5906                        gfx.meshes[id].clear();
5907                        gfx.mesh_free.push(id);
5908                    }
5909                }
5910                return Ok(Value::Unit);
5911            },
5912
5913            // ── จบอบเมช() — bake captured triangles, return mesh handle ──
5914            "จบอบเมช" | "mesh_bake_end" | "پایان_پخت_مش" | "أنهِ_خبز_الشبكة" | "סיים_אפיית_רשת" | "میش_بیک_ختم" => {
5915                let mut gfx = self.gfx.borrow_mut();
5916                let tris = gfx.mesh_capture.take().unwrap_or_default();
5917                let id = gfx.mesh_register(tris);
5918                return Ok(Value::Number(id as f64));
5919            },
5920
5921            // ── วาดอบเมช[สี](id, ox,oy,oz, rx,ry,rz, ux,uy,uz, s) — draw a baked mesh ──
5922            //   วาดอบเมช: current pen colour (tinted glyphs)
5923            //   วาดอบเมชสี: per-triangle baked colour (multi-colour models)
5924            "วาดอบเมช" | "mesh_bake_draw" | "วาดอบเมชสี" | "mesh_bake_draw_col" | "رسم_مش_پخته" | "ارسم_شبكة_مخبوزة" | "צייר_רשת_אפויה" | "بیکڈ_میش_کھینچو" =>
5925            {
5926                let baked_col = matches!(name, "วาดอบเมชสี" | "mesh_bake_draw_col");
5927                let id = self.arg_num(&args, 0, 0.0)? as usize;
5928                let ox = self.arg_num(&args, 1, 0.0)? as f32;
5929                let oy = self.arg_num(&args, 2, 0.0)? as f32;
5930                let oz = self.arg_num(&args, 3, 0.0)? as f32;
5931                let rx = self.arg_num(&args, 4, 1.0)? as f32;
5932                let ry = self.arg_num(&args, 5, 0.0)? as f32;
5933                let rz = self.arg_num(&args, 6, 0.0)? as f32;
5934                let ux = self.arg_num(&args, 7, 0.0)? as f32;
5935                let uy = self.arg_num(&args, 8, 1.0)? as f32;
5936                let uz = self.arg_num(&args, 9, 0.0)? as f32;
5937                let s = self.arg_num(&args, 10, 1.0)? as f32;
5938                self.gfx
5939                    .borrow_mut()
5940                    .mesh_draw(id, ox, oy, oz, rx, ry, rz, ux, uy, uz, s, baked_col);
5941                return Ok(Value::Unit);
5942            },
5943
5944            // ── draw_quad_3d / draw_pent_3d / draw_hex_3d / draw_polygon_3d ──
5945            // Fan-triangulate convex n-gons.  Lighting, near-plane clip, fog, and
5946            // Gouraud shading all mirror draw_triangle_3d exactly.
5947            "draw_quad_3d"
5948            | "quad3d"
5949            | "วาดสี่เหลี่ยม3มิติ"
5950            | "draw_pent_3d"
5951            | "pent3d"
5952            | "วาดห้าเหลี่ยม3มิติ"
5953            | "draw_hex_3d"
5954            | "hex3d"
5955            | "วาดหกเหลี่ยม3มิติ"
5956            | "draw_polygon_3d"
5957            | "polygon3d"
5958            | "วาดรูปหลายเหลี่ยม3มิติ" | "رسم_چهارضلعی_سه‌بعدی" | "ارسم_رباعيا_ثلاثي_الأبعاد" | "צייר_מרובע_תלת_ממדי" | "تھری_ڈی_چوکور_کھینچو" => {
5959                // Collect (wx, wy, wz) triples from args or list
5960                let mut wxs: [f32; 8] = [0.0; 8];
5961                let mut wys: [f32; 8] = [0.0; 8];
5962                let mut wzs: [f32; 8] = [0.0; 8];
5963                let n_verts;
5964
5965                if args.len() == 1 {
5966                    // draw_polygon_3d([x0,y0,z0, x1,y1,z1, ...])
5967                    let list = match &args[0] {
5968                        Value::List(l) => l.clone(),
5969                        _ => {
5970                            return Err(EvalErr::from("draw_polygon_3d: expected list".to_string()))
5971                        },
5972                    };
5973                    let coords: Vec<f32> = list
5974                        .iter()
5975                        .map(|v| match v {
5976                            Value::Number(n) => *n as f32,
5977                            _ => 0.0,
5978                        })
5979                        .collect();
5980                    n_verts = (coords.len() / 3).min(8);
5981                    for i in 0..n_verts {
5982                        wxs[i] = coords[i * 3];
5983                        wys[i] = coords[i * 3 + 1];
5984                        wzs[i] = coords[i * 3 + 2];
5985                    }
5986                } else {
5987                    // draw_quad/pent/hex_3d(x0,y0,z0, x1,y1,z1, ...)
5988                    n_verts = (args.len() / 3).min(8);
5989                    for i in 0..n_verts {
5990                        wxs[i] = self.arg_num(&args, i * 3, 0.0)? as f32;
5991                        wys[i] = self.arg_num(&args, i * 3 + 1, 0.0)? as f32;
5992                        wzs[i] = self.arg_num(&args, i * 3 + 2, 0.0)? as f32;
5993                    }
5994                }
5995                if n_verts < 3 {
5996                    return Ok(Value::Unit);
5997                }
5998
5999                let mut gfx = self.gfx.borrow_mut();
6000
6001                // Mesh capture: fan-triangulate and record raw local coords +
6002                // pen colour, exactly like วาดสามเหลี่ยม3มิติ. Quads used to
6003                // fall through here and bake EMPTY display lists — the 3-D
6004                // glyph fonts (letter pickups) are built from draw_quad_3d,
6005                // which made every baked glyph invisible.
6006                if gfx.mesh_capture.is_some() {
6007                    let col = gfx.color;
6008                    let cap = gfx.mesh_capture.as_mut().unwrap();
6009                    for i in 1..n_verts - 1 {
6010                        cap.push((
6011                            [
6012                                wxs[0], wys[0], wzs[0],
6013                                wxs[i], wys[i], wzs[i],
6014                                wxs[i + 1], wys[i + 1], wzs[i + 1],
6015                            ],
6016                            col,
6017                        ));
6018                    }
6019                    return Ok(Value::Unit);
6020                }
6021
6022                // Face normal from first triangle of the fan
6023                let normal = crate::gfx::poly::face_normal(
6024                    wxs[0], wys[0], wzs[0], wxs[1], wys[1], wzs[1], wxs[2], wys[2], wzs[2],
6025                );
6026
6027                // Per-vertex lit colours
6028                let mut wcs: [u32; 8] = [0; 8];
6029                if gfx.flat_shade {
6030                    let c = gfx.color;
6031                    for wc in wcs.iter_mut().take(n_verts) {
6032                        *wc = c;
6033                    }
6034                } else if let Some(ref mat) = gfx.material.clone() {
6035                    let cam = [gfx.camera.cx, gfx.camera.cy, gfx.camera.zdist];
6036                    let lights: Vec<_> = gfx.lights.clone();
6037                    let ambient = gfx.ambient;
6038                    for i in 0..n_verts {
6039                        let v = [wxs[i], wys[i], wzs[i]];
6040                        let vd = [cam[0] - v[0], cam[1] - v[1], cam[2] - v[2]];
6041                        wcs[i] = crate::gfx::material::shade(mat, normal, vd, v, &lights, ambient);
6042                    }
6043                } else {
6044                    let base = gfx.color;
6045                    let lights: Vec<_> = gfx.lights.clone();
6046                    let ambient = gfx.ambient;
6047                    for i in 0..n_verts {
6048                        wcs[i] = crate::gfx::light::compute_lit_color_linear(
6049                            base,
6050                            normal,
6051                            [wxs[i], wys[i], wzs[i]],
6052                            &lights,
6053                            ambient,
6054                        );
6055                    }
6056                }
6057
6058                // Near-plane clip (Sutherland-Hodgman per vertex)
6059                let near = -gfx.camera.zdist + 0.05;
6060                let mut clip_in: [(f32, f32, f32, f32, u32); crate::gfx::poly::MAX_CLIP_VERTS] =
6061                    [(0.0, 0.0, 0.0, 0.0, 0); crate::gfx::poly::MAX_CLIP_VERTS];
6062                for i in 0..n_verts {
6063                    let d = gfx.camera.depth(wxs[i], wys[i], wzs[i]);
6064                    clip_in[i] = (wxs[i], wys[i], wzs[i], d, wcs[i]);
6065                }
6066                let mut clip_out: [(f32, f32, f32, f32, u32); crate::gfx::poly::MAX_CLIP_VERTS] =
6067                    [(0.0, 0.0, 0.0, 0.0, 0); crate::gfx::poly::MAX_CLIP_VERTS];
6068                let pn = crate::gfx::poly::clip_near(&clip_in, n_verts, near, &mut clip_out);
6069                if pn < 3 {
6070                    return Ok(Value::Unit);
6071                }
6072
6073                // Project + fog
6074                let mut proj: [(f32, f32, f32, u32); crate::gfx::poly::MAX_CLIP_VERTS] =
6075                    [(0.0, 0.0, 0.0, 0); crate::gfx::poly::MAX_CLIP_VERTS];
6076                for i in 0..pn {
6077                    let (sx, sy, sz) =
6078                        gfx.camera
6079                            .project(clip_out[i].0, clip_out[i].1, clip_out[i].2);
6080                    let fc = gfx.fog_apply(clip_out[i].4, sz);
6081                    proj[i] = (sx, sy, sz, fc);
6082                }
6083
6084                // Fan-triangulate and push
6085                let unlit = gfx.flat_shade;
6086                crate::gfx::poly::fan_emit_proj(
6087                    &proj,
6088                    pn,
6089                    |x0, y0, z0, c0, x1, y1, z1, c1, x2, y2, z2, c2| {
6090                        gfx.depth_queue.push_triangle_g_zv(
6091                            x0, y0, z0, c0, x1, y1, z1, c1, x2, y2, z2, c2, 3, unlit,
6092                        );
6093                    },
6094                );
6095                return Ok(Value::Unit);
6096            },
6097
6098            // ── วาดเส้น3มิติ(ax,ay,az, bx,by,bz) ──
6099            // Projects two world-space points via the stored camera and pushes
6100            // a line to the depth queue.
6101            "วาดเส้น3มิติ" | "draw_line_3d" | "line3d" | "画3D线" | "3D線描く" | "3D선그리기" | "رسم_خط_سه‌بعدی" | "ارسم_خطا_ثلاثي_الأبعاد" | "צייר_קו_תלת_ממדי" | "تھری_ڈی_لکیر_کھینچو" =>
6102            {
6103                let ax = self.arg_num(&args, 0, 0.0)? as f32;
6104                let ay = self.arg_num(&args, 1, 0.0)? as f32;
6105                let az = self.arg_num(&args, 2, 0.0)? as f32;
6106                let bx = self.arg_num(&args, 3, 0.0)? as f32;
6107                let by = self.arg_num(&args, 4, 0.0)? as f32;
6108                let bz = self.arg_num(&args, 5, 0.0)? as f32;
6109
6110                let mut gfx = self.gfx.borrow_mut();
6111                let color = gfx.color;
6112                // Near-plane clip in 3-D before perspective divide
6113                let near = -gfx.camera.zdist + 0.05;
6114                let mut lax = ax;
6115                let mut lay = ay;
6116                let mut laz = az;
6117                let mut lbx = bx;
6118                let mut lby = by;
6119                let mut lbz = bz;
6120                let da_raw = gfx.camera.depth(lax, lay, laz);
6121                let db_raw = gfx.camera.depth(lbx, lby, lbz);
6122                if da_raw <= near && db_raw <= near {
6123                    return Ok(Value::Unit);
6124                }
6125                if da_raw <= near {
6126                    let t = (near - da_raw) / (db_raw - da_raw);
6127                    lax += t * (lbx - lax);
6128                    lay += t * (lby - lay);
6129                    laz += t * (lbz - laz);
6130                } else if db_raw <= near {
6131                    let t = (near - da_raw) / (db_raw - da_raw);
6132                    lbx = lax + t * (lbx - lax);
6133                    lby = lay + t * (lby - lay);
6134                    lbz = laz + t * (lbz - laz);
6135                }
6136                // Shared-edge dedup: skip if this world-space edge was already queued.
6137                if !gfx.edge_set.try_insert(lax, lay, laz, lbx, lby, lbz) {
6138                    return Ok(Value::Unit);
6139                }
6140                let (sax, say, da) = gfx.camera.project(lax, lay, laz);
6141                let (sbx, sby, db) = gfx.camera.project(lbx, lby, lbz);
6142                let depth = (da + db) / 2.0;
6143                let color = gfx.fog_apply(color, depth);
6144                gfx.depth_queue.push_line(depth, color, sax, say, sbx, sby);
6145                return Ok(Value::Unit);
6146            },
6147
6148            // orb_shell(cx,cy,cz, radius, rot_y, rot_x, density, r,g,b)
6149            //   A single trippy, grayscale, depth-faded vector pattern wound around
6150            //   a sphere — two families of interleaved spherical spirals (a guilloché
6151            //   weave), NOT a lat/long cage. Each segment's brightness follows its
6152            //   facing (front bright, back dim), so it reads as a translucent
6153            //   grayscale "texture" with alpha rather than a hard wireframe; the
6154            //   inner marble shows through. `rot_y`/`rot_x` roll the texture around
6155            //   the orb; `density` = spirals per winding direction. r,g,b tint it
6156            //   (pass a gray like 230,230,230 for pure grayscale).
6157            #[cfg(not(target_arch = "wasm32"))]
6158            "orb_shell" | "球壳" | "オーブ殻" | "오브껍질" | "เปลือกทรงกลม" | "پوسته_کروی" | "قشرة_كروية" | "קליפת_כדור" | "کروی_خول" | "coque_orbe" | "orb_hülle" | "оболочка_сферы" =>
6159            {
6160                let cx = self.arg_num(&args, 0, 0.)? as f32;
6161                let cy = self.arg_num(&args, 1, 0.)? as f32;
6162                let cz = self.arg_num(&args, 2, 0.)? as f32;
6163                let radius = self.arg_num(&args, 3, 1.0)? as f32;
6164                let ry = self.arg_num(&args, 4, 0.)? as f32;
6165                let rx = self.arg_num(&args, 5, 0.)? as f32;
6166                let density = (self.arg_num(&args, 6, 10.)? as i32).clamp(1, 48);
6167                let tr = (self.arg_num(&args, 7, 230.)? as f32).clamp(0., 255.);
6168                let tg = (self.arg_num(&args, 8, 230.)? as f32).clamp(0., 255.);
6169                let tb = (self.arg_num(&args, 9, 235.)? as f32).clamp(0., 255.);
6170                let (cyr, syr) = (ry.cos(), ry.sin());
6171                let (cxr, sxr) = (rx.cos(), rx.sin());
6172                let tau = std::f32::consts::TAU;
6173                let pi = std::f32::consts::PI;
6174                let turns = 6.0_f32; // how many times each spiral wraps pole→pole
6175                let nseg = 96; // segments per spiral (smoothness)
6176                let inv_r = if radius.abs() > 1e-5 {
6177                    1.0 / radius
6178                } else {
6179                    0.0
6180                };
6181                // a point along a spiral (param u 0..1, start angle theta0, winding dir),
6182                // spun by ry/rx — returns (world point, facing 0..1 where 1 = toward camera)
6183                let pt = |u: f32, theta0: f32, dir: f32| -> ([f32; 3], f32) {
6184                    let phi = pi * u; // 0..pi  (north → south)
6185                    let th = dir * turns * tau * u + theta0;
6186                    let (mut x, y, mut z) = (
6187                        phi.sin() * th.cos() * radius,
6188                        phi.cos() * radius,
6189                        phi.sin() * th.sin() * radius,
6190                    );
6191                    let x1 = x * cyr + z * syr; // yaw about Y
6192                    let z1 = -x * syr + z * cyr;
6193                    x = x1;
6194                    z = z1;
6195                    let y2 = y * cxr - z * sxr; // pitch about X
6196                    let z2 = y * sxr + z * cxr;
6197                    // facing: camera sits at -zdist looking +z, so smaller z2 = nearer = brighter
6198                    let facing = (0.5 - 0.5 * z2 * inv_r).clamp(0.0, 1.0);
6199                    ([cx + x, cy + y2, cz + z2], facing)
6200                };
6201                let mut gfx = self.gfx.borrow_mut();
6202                let near = -gfx.camera.zdist + 0.05;
6203                // draw one segment (near-clipped) in a grayscale tint scaled by `lum`
6204                let seg = |gfx: &mut crate::gfx::GfxState, a: [f32; 3], b: [f32; 3], lum: f32| {
6205                    let (mut lax, mut lay, mut laz) = (a[0], a[1], a[2]);
6206                    let (mut lbx, mut lby, mut lbz) = (b[0], b[1], b[2]);
6207                    let da = gfx.camera.depth(lax, lay, laz);
6208                    let db = gfx.camera.depth(lbx, lby, lbz);
6209                    if da <= near && db <= near {
6210                        return;
6211                    }
6212                    if da <= near {
6213                        let t = (near - da) / (db - da);
6214                        lax += t * (lbx - lax);
6215                        lay += t * (lby - lay);
6216                        laz += t * (lbz - laz);
6217                    } else if db <= near {
6218                        let t = (near - da) / (db - da);
6219                        lbx = lax + t * (lbx - lax);
6220                        lby = lay + t * (lby - lay);
6221                        lbz = laz + t * (lbz - laz);
6222                    }
6223                    let (sax, say, da2) = gfx.camera.project(lax, lay, laz);
6224                    let (sbx, sby, db2) = gfx.camera.project(lbx, lby, lbz);
6225                    // grayscale-alpha: front-facing bright, back faded toward black
6226                    let l = (0.12 + 0.88 * lum).clamp(0.0, 1.0);
6227                    let cr = (tr * l) as u32;
6228                    let cg = (tg * l) as u32;
6229                    let cb = (tb * l) as u32;
6230                    let color = (cr << 16) | (cg << 8) | cb;
6231                    gfx.depth_queue
6232                        .push_line((da2 + db2) * 0.5, color, sax, say, sbx, sby);
6233                };
6234                // two opposite winding directions → a soft guilloché weave (not a cage)
6235                for &dir in &[1.0_f32, -1.0_f32] {
6236                    for s in 0..density {
6237                        let theta0 = s as f32 * tau / density as f32;
6238                        let mut prev = pt(0.0, theta0, dir);
6239                        for k in 1..=nseg {
6240                            let cur = pt(k as f32 / nseg as f32, theta0, dir);
6241                            seg(&mut gfx, prev.0, cur.0, (prev.1 + cur.1) * 0.5);
6242                            prev = cur;
6243                        }
6244                    }
6245                }
6246                return Ok(Value::Unit);
6247            },
6248
6249            // orb_particles(cx,cy,cz, radius, count, t, r,g,b)
6250            //   Fills the VOLUME of a sphere with `count` swirling vector points —
6251            //   like motes suspended inside a snow-globe orb. Points are distributed
6252            //   uniformly through the ball, slowly tumble as a cloud + wobble
6253            //   individually over time `t`, and are depth-shaded (near = bright,
6254            //   far = dim) so the cloud has real volume. Additive, so it layers under
6255            //   a shell / over a liquid marble.
6256            #[cfg(not(target_arch = "wasm32"))]
6257            "orb_particles" | "球内粒子" | "オーブ粒子" | "오브입자" | "อนุภาคทรงกลม" | "ذرات_کروی" | "جسيمات_كروية" | "חלקיקי_כדור" | "کروی_ذرات" | "particules_orbe" | "orb_partikel" | "частицы_сферы" =>
6258            {
6259                let cx = self.arg_num(&args, 0, 0.)? as f32;
6260                let cy = self.arg_num(&args, 1, 0.)? as f32;
6261                let cz = self.arg_num(&args, 2, 0.)? as f32;
6262                let radius = self.arg_num(&args, 3, 1.0)? as f32;
6263                let count = (self.arg_num(&args, 4, 160.)? as i32).clamp(1, 4000);
6264                let t = self.arg_num(&args, 5, 0.)? as f32;
6265                let tr = (self.arg_num(&args, 6, 255.)? as f32).clamp(0., 255.);
6266                let tg = (self.arg_num(&args, 7, 255.)? as f32).clamp(0., 255.);
6267                let tb = (self.arg_num(&args, 8, 255.)? as f32).clamp(0., 255.);
6268                let inv_r = if radius.abs() > 1e-5 {
6269                    1.0 / radius
6270                } else {
6271                    0.0
6272                };
6273                // cheap deterministic hash → [0,1)
6274                let h = |mut x: u32| -> f32 {
6275                    x = x.wrapping_mul(747796405).wrapping_add(2891336453);
6276                    x = ((x >> ((x >> 28).wrapping_add(4))) ^ x).wrapping_mul(277803737);
6277                    (((x >> 22) ^ x) & 0xFFFFFF) as f32 / 16_777_216.0
6278                };
6279                let tau = std::f32::consts::TAU;
6280                // slow tumble of the whole cloud
6281                let (cyr, syr) = ((t * 0.5).cos(), (t * 0.5).sin());
6282                let (cxr, sxr) = ((t * 0.23).cos(), (t * 0.23).sin());
6283                let mut gfx = self.gfx.borrow_mut();
6284                let near = -gfx.camera.zdist + 0.05;
6285                let (sw, sh) = (gfx.width as i32, gfx.height as i32);
6286                for i in 0..count {
6287                    let i = i as u32;
6288                    // uniform-in-volume: r = cbrt(u) * radius; direction from two hashes
6289                    let u = h(i.wrapping_mul(3) + 1);
6290                    let rr = u.cbrt() * radius * (0.85 + 0.15 * (t * 1.3 + i as f32).sin()); // gentle pulse
6291                    let th = h(i.wrapping_mul(3) + 2) * tau + t * (0.3 + 0.5 * h(i * 7 + 5)); // per-mote orbit
6292                    let ph = (h(i.wrapping_mul(3) + 3) * 2.0 - 1.0).acos(); // uniform cos(phi)
6293                    let (mut x, y, mut z) = (
6294                        rr * ph.sin() * th.cos(),
6295                        rr * ph.cos(),
6296                        rr * ph.sin() * th.sin(),
6297                    );
6298                    // tumble the cloud (yaw then pitch)
6299                    let x1 = x * cyr + z * syr;
6300                    let z1 = -x * syr + z * cyr;
6301                    x = x1;
6302                    z = z1;
6303                    let y2 = y * cxr - z * sxr;
6304                    let z2 = y * sxr + z * cxr;
6305                    let (wx, wy, wz) = (cx + x, cy + y2, cz + z2);
6306                    if gfx.camera.depth(wx, wy, wz) <= near {
6307                        continue;
6308                    }
6309                    let (sx, sy, dep) = gfx.camera.project(wx, wy, wz);
6310                    let sxi = sx as i32;
6311                    let syi = sy as i32;
6312                    if sxi < 0 || syi < 0 || sxi >= sw || syi >= sh {
6313                        continue;
6314                    }
6315                    // depth-shade: nearer (smaller z2) = brighter
6316                    let facing = (0.5 - 0.5 * z2 * inv_r).clamp(0.15, 1.0);
6317                    let l = facing;
6318                    let cr = (tr * l) as u32;
6319                    let cg = (tg * l) as u32;
6320                    let cb = (tb * l) as u32;
6321                    let color = (cr << 16) | (cg << 8) | cb;
6322                    // a 1–2px dot (bigger when near) as a short segment in the depth queue
6323                    let len = if facing > 0.7 { 1.0 } else { 0.0 };
6324                    gfx.depth_queue.push_line(dep, color, sx, sy, sx + len, sy);
6325                }
6326                return Ok(Value::Unit);
6327            },
6328
6329            // project_3d(x,y,z) -> [screen_x, screen_y, depth]; behind the camera
6330            // returns a sentinel ([-99999,-99999, depth]) so scripts can skip it.
6331            // Lets scripts place 2-D overlays (e.g. filled teardrop flames) onto 3-D points.
6332            "project_3d" | "投影3D" | "3D投影" | "3D투영" | "ฉาย3มิติ" | "فرافکنی_سه‌بعدی" | "إسقاط_ثلاثي_الأبعاد" | "הטלה_תלת_ממדית" | "تھری_ڈی_پروجیکشن" | "projeter_3d" | "projizieren_3d" | "проекция_3d" => {
6333                let x = self.arg_num(&args, 0, 0.0)? as f32;
6334                let y = self.arg_num(&args, 1, 0.0)? as f32;
6335                let z = self.arg_num(&args, 2, 0.0)? as f32;
6336                let gfx = self.gfx.borrow();
6337                let near = -gfx.camera.zdist + 0.05;
6338                let d = gfx.camera.depth(x, y, z);
6339                if d <= near {
6340                    return Ok(Value::List(Rc::new(vec![
6341                        Value::Number(-99999.0),
6342                        Value::Number(-99999.0),
6343                        Value::Number(d as f64),
6344                    ])));
6345                }
6346                let (sx, sy, depth) = gfx.camera.project(x, y, z);
6347                return Ok(Value::List(Rc::new(vec![
6348                    Value::Number(sx as f64),
6349                    Value::Number(sy as f64),
6350                    Value::Number(depth as f64),
6351                ])));
6352            },
6353
6354            // mouse_ray() -> [ox,oy,oz, dx,dy,dz] — world-space ray from the eye
6355            // through the actual mouse cursor pixel, exact inverse of project_3d's
6356            // pipeline (translate → Y-rotate → X-rotate → perspective divide by
6357            // rz+zdist). Scripts previously marched a ray along the CENTRE-SCREEN
6358            // forward vector regardless of where the cursor was — accurate only by
6359            // coincidence when the cursor happened to sit near the crosshair.
6360            #[cfg(not(target_arch = "wasm32"))]
6361            "mouse_ray" => {
6362                let gfx = self.gfx.borrow();
6363                let (mx, my) = gfx
6364                    .window
6365                    .as_ref()
6366                    .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
6367                    .unwrap_or((gfx.camera.cx, gfx.camera.cy));
6368                let cam = &gfx.camera;
6369                // Eye-relative pinhole direction for this pixel, in rotation-space
6370                // (before undoing the Y-then-X rotation project() applied).
6371                let dcx = (mx - cam.cx) / cam.focal;
6372                let dcy = (my - cam.cy) / cam.focal;
6373                // Undo the X-rotation, then the Y-rotation (reverse of project()'s
6374                // forward order), on the direction vector (dcx, dcy, 1.0).
6375                let a_x = dcx;
6376                let a_y = cam.crx * dcy + cam.srx * 1.0;
6377                let a_z = 0.0 - cam.srx * dcy + cam.crx * 1.0;
6378                let dir_x = cam.cry * a_x + cam.sry * a_z;
6379                let dir_y = a_y;
6380                let dir_z = 0.0 - cam.sry * a_x + cam.cry * a_z;
6381                let dlen = (dir_x * dir_x + dir_y * dir_y + dir_z * dir_z)
6382                    .sqrt()
6383                    .max(1e-6);
6384                let (dir_x, dir_y, dir_z) = (dir_x / dlen, dir_y / dlen, dir_z / dlen);
6385                // Origin: the camera's rotation pivot (tx,ty,tz) — i.e. wherever
6386                // the script last put it with set_camera_pos, NOT the "true"
6387                // pinhole eye zdist further back. The pivot is what scripts
6388                // already keep clear of the ground (a ground-collision pull-in
6389                // loop is standard practice for orbit cameras); the true eye
6390                // would need its own separate ground clearance since zdist is
6391                // often large relative to a close-in camera distance, and
6392                // starting a ray underground makes it hit "ground" instantly
6393                // regardless of aim. The zdist offset only matters for the
6394                // near-field parallax, which a click-to-move ray (aimed at
6395                // terrain many units out) doesn't need.
6396                let ox = cam.tx;
6397                let oy = cam.ty;
6398                let oz = cam.tz;
6399                return Ok(Value::List(Rc::new(vec![
6400                    Value::Number(ox as f64),
6401                    Value::Number(oy as f64),
6402                    Value::Number(oz as f64),
6403                    Value::Number(dir_x as f64),
6404                    Value::Number(dir_y as f64),
6405                    Value::Number(dir_z as f64),
6406                ])));
6407            },
6408            #[cfg(target_arch = "wasm32")]
6409            "mouse_ray" => {
6410                let gfx = self.gfx.borrow();
6411                let mx = crate::gfx::wasm_mouse_x();
6412                let my = crate::gfx::wasm_mouse_y();
6413                let cam = &gfx.camera;
6414                let dcx = (mx - cam.cx) / cam.focal;
6415                let dcy = (my - cam.cy) / cam.focal;
6416                let a_x = dcx;
6417                let a_y = cam.crx * dcy + cam.srx * 1.0;
6418                let a_z = 0.0 - cam.srx * dcy + cam.crx * 1.0;
6419                let dir_x = cam.cry * a_x + cam.sry * a_z;
6420                let dir_y = a_y;
6421                let dir_z = 0.0 - cam.sry * a_x + cam.cry * a_z;
6422                let dlen = (dir_x * dir_x + dir_y * dir_y + dir_z * dir_z)
6423                    .sqrt()
6424                    .max(1e-6);
6425                let (dir_x, dir_y, dir_z) = (dir_x / dlen, dir_y / dlen, dir_z / dlen);
6426                let ox = cam.tx;
6427                let oy = cam.ty;
6428                let oz = cam.tz;
6429                return Ok(Value::List(Rc::new(vec![
6430                    Value::Number(ox as f64),
6431                    Value::Number(oy as f64),
6432                    Value::Number(oz as f64),
6433                    Value::Number(dir_x as f64),
6434                    Value::Number(dir_y as f64),
6435                    Value::Number(dir_z as f64),
6436                ])));
6437            },
6438            // draw_poly([x0,y0,x1,y1,…]) — filled 2-D polygon in the current colour,
6439            // honouring the blend mode (additive → translucent glow). Auto-closes.
6440            #[cfg(not(target_arch = "wasm32"))]
6441            "draw_poly" | "填充多边形" | "ポリゴン塗り" | "다각형채우기" | "เติมรูปหลายเหลี่ยม" | "رسم_چندضلعی" | "ارسم_مضلع" | "צייר_מצולע" | "کثیر_الاضلاع_کھینچو" | "dessiner_polygone" | "polygon_zeichnen" | "рисовать_полигон" =>
6442            {
6443                let mut pts: Vec<[f32; 2]> = Vec::new();
6444                if let Some(Value::List(v)) = args.first() {
6445                    let mut i = 0;
6446                    while i + 1 < v.len() {
6447                        let x = self.to_number(&v[i]).unwrap_or(0.0) as f32;
6448                        let y = self.to_number(&v[i + 1]).unwrap_or(0.0) as f32;
6449                        pts.push([x, y]);
6450                        i += 2;
6451                    }
6452                }
6453                if pts.len() >= 3 {
6454                    if pts[0] != pts[pts.len() - 1] {
6455                        let p0 = pts[0];
6456                        pts.push(p0);
6457                    } // close
6458                    let mut gfx = self.gfx.borrow_mut();
6459                    let (w, h, color, add) = (gfx.width, gfx.height, gfx.color, gfx.blend == 1);
6460                    crate::gfx::raster::fill_contours_aa(
6461                        &mut gfx.buffer,
6462                        w,
6463                        h,
6464                        color,
6465                        add,
6466                        std::slice::from_ref(&pts),
6467                    );
6468                }
6469                return Ok(Value::Unit);
6470            },
6471
6472            // ══════════════════════════════════════════════════════════════════
6473            // VECTOR TEXTURE BUILTINS  (src/gfx/vtex.rs)
6474            // All patterns are depth-biased so they appear on top of surfaces.
6475            // Plane defined by: centre (cx,cy,cz) + U tangent + V tangent.
6476            // Last two args always: fr (frame f32), hue (phase offset f32).
6477            // ══════════════════════════════════════════════════════════════════
6478
6479            // vtex_grid(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cw,ch, fr,hue)
6480            "vtex_grid" | "ลายตาราง" | "纹格" | "格子模様" | "격자무늬" | "الگوی_شبکه" | "نقش_شبكة" | "דוגמת_רשת" | "نقش_جالی" | "motif_grille" | "muster_gitter" | "узор_сетка" =>
6481            {
6482                let cx = self.arg_num(&args, 0, 0.)? as f32;
6483                let cy = self.arg_num(&args, 1, 0.)? as f32;
6484                let cz = self.arg_num(&args, 2, 0.)? as f32;
6485                let ux = self.arg_num(&args, 3, 1.)? as f32;
6486                let uy = self.arg_num(&args, 4, 0.)? as f32;
6487                let uz = self.arg_num(&args, 5, 0.)? as f32;
6488                let vx = self.arg_num(&args, 6, 0.)? as f32;
6489                let vy = self.arg_num(&args, 7, 0.)? as f32;
6490                let vz = self.arg_num(&args, 8, 1.)? as f32;
6491                let cols = self.arg_num(&args, 9, 10.)? as usize;
6492                let rows = self.arg_num(&args, 10, 10.)? as usize;
6493                let cw = self.arg_num(&args, 11, 1.)? as f32;
6494                let ch = self.arg_num(&args, 12, 1.)? as f32;
6495                let fr = self.arg_num(&args, 13, 0.)? as f32;
6496                let hue = self.arg_num(&args, 14, 0.)? as f32;
6497                let mut gfx = self.gfx.borrow_mut();
6498                let cam = gfx.camera.clone();
6499                crate::gfx::vtex::draw_grid(
6500                    &mut gfx.depth_queue,
6501                    &cam,
6502                    cx,
6503                    cy,
6504                    cz,
6505                    ux,
6506                    uy,
6507                    uz,
6508                    vx,
6509                    vy,
6510                    vz,
6511                    cols,
6512                    rows,
6513                    cw,
6514                    ch,
6515                    fr,
6516                    hue,
6517                );
6518                return Ok(Value::Unit);
6519            },
6520
6521            // vtex_rings(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_rings,n_sides, max_r,twist, fr,hue)
6522            "vtex_rings" | "ลายวงซ้อน" | "纹环" | "同心円" | "동심원" | "الگوی_حلقه" | "نقش_حلقات" | "דוגמת_טבעות" | "نقش_حلقے" | "motif_anneaux" | "muster_ringe" | "узор_кольца" => {
6523                let cx = self.arg_num(&args, 0, 0.)? as f32;
6524                let cy = self.arg_num(&args, 1, 0.)? as f32;
6525                let cz = self.arg_num(&args, 2, 0.)? as f32;
6526                let ux = self.arg_num(&args, 3, 1.)? as f32;
6527                let uy = self.arg_num(&args, 4, 0.)? as f32;
6528                let uz = self.arg_num(&args, 5, 0.)? as f32;
6529                let vx = self.arg_num(&args, 6, 0.)? as f32;
6530                let vy = self.arg_num(&args, 7, 0.)? as f32;
6531                let vz = self.arg_num(&args, 8, 1.)? as f32;
6532                let nr = self.arg_num(&args, 9, 6.)? as usize;
6533                let ns = self.arg_num(&args, 10, 6.)? as usize;
6534                let mr = self.arg_num(&args, 11, 3.)? as f32;
6535                let tw = self.arg_num(&args, 12, 0.)? as f32;
6536                let fr = self.arg_num(&args, 13, 0.)? as f32;
6537                let hue = self.arg_num(&args, 14, 0.)? as f32;
6538                let mut gfx = self.gfx.borrow_mut();
6539                let cam = gfx.camera.clone();
6540                crate::gfx::vtex::draw_rings(
6541                    &mut gfx.depth_queue,
6542                    &cam,
6543                    cx,
6544                    cy,
6545                    cz,
6546                    ux,
6547                    uy,
6548                    uz,
6549                    vx,
6550                    vy,
6551                    vz,
6552                    nr,
6553                    ns,
6554                    mr,
6555                    tw,
6556                    fr,
6557                    hue,
6558                );
6559                return Ok(Value::Unit);
6560            },
6561
6562            // vtex_star(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_pts,r_out,r_in, rot_speed, fr,hue)
6563            "vtex_star" | "ลายดาว" | "纹星" | "星模様" | "별무늬" | "الگوی_ستاره" | "نقش_نجمة" | "דוגמת_כוכב" | "نقش_ستارہ" | "motif_étoile" | "muster_stern" | "узор_звезда" => {
6564                let cx = self.arg_num(&args, 0, 0.)? as f32;
6565                let cy = self.arg_num(&args, 1, 0.)? as f32;
6566                let cz = self.arg_num(&args, 2, 0.)? as f32;
6567                let ux = self.arg_num(&args, 3, 1.)? as f32;
6568                let uy = self.arg_num(&args, 4, 0.)? as f32;
6569                let uz = self.arg_num(&args, 5, 0.)? as f32;
6570                let vx = self.arg_num(&args, 6, 0.)? as f32;
6571                let vy = self.arg_num(&args, 7, 0.)? as f32;
6572                let vz = self.arg_num(&args, 8, 1.)? as f32;
6573                let np = self.arg_num(&args, 9, 6.)? as usize;
6574                let ro = self.arg_num(&args, 10, 2.)? as f32;
6575                let ri = self.arg_num(&args, 11, 1.)? as f32;
6576                let rs = self.arg_num(&args, 12, 0.01)? as f32;
6577                let fr = self.arg_num(&args, 13, 0.)? as f32;
6578                let hue = self.arg_num(&args, 14, 0.)? as f32;
6579                let mut gfx = self.gfx.borrow_mut();
6580                let cam = gfx.camera.clone();
6581                crate::gfx::vtex::draw_star(
6582                    &mut gfx.depth_queue,
6583                    &cam,
6584                    cx,
6585                    cy,
6586                    cz,
6587                    ux,
6588                    uy,
6589                    uz,
6590                    vx,
6591                    vy,
6592                    vz,
6593                    np,
6594                    ro,
6595                    ri,
6596                    rs,
6597                    fr,
6598                    hue,
6599                );
6600                return Ok(Value::Unit);
6601            },
6602
6603            // vtex_spiral(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_turns,max_r,steps, fr,hue)
6604            "vtex_spiral" | "ลายเกลียว" | "纹螺" | "螺旋" | "나선" | "الگوی_مارپیچ" | "نقش_حلزوني" | "דוגמת_ספירלה" | "نقش_سرپیچ" | "motif_spirale" | "muster_spirale" | "узор_спираль" => {
6605                let cx = self.arg_num(&args, 0, 0.)? as f32;
6606                let cy = self.arg_num(&args, 1, 0.)? as f32;
6607                let cz = self.arg_num(&args, 2, 0.)? as f32;
6608                let ux = self.arg_num(&args, 3, 1.)? as f32;
6609                let uy = self.arg_num(&args, 4, 0.)? as f32;
6610                let uz = self.arg_num(&args, 5, 0.)? as f32;
6611                let vx = self.arg_num(&args, 6, 0.)? as f32;
6612                let vy = self.arg_num(&args, 7, 0.)? as f32;
6613                let vz = self.arg_num(&args, 8, 1.)? as f32;
6614                let nt = self.arg_num(&args, 9, 3.)? as f32;
6615                let mr = self.arg_num(&args, 10, 3.)? as f32;
6616                let st = self.arg_num(&args, 11, 120.)? as usize;
6617                let fr = self.arg_num(&args, 12, 0.)? as f32;
6618                let hue = self.arg_num(&args, 13, 0.)? as f32;
6619                let mut gfx = self.gfx.borrow_mut();
6620                let cam = gfx.camera.clone();
6621                crate::gfx::vtex::draw_spiral(
6622                    &mut gfx.depth_queue,
6623                    &cam,
6624                    cx,
6625                    cy,
6626                    cz,
6627                    ux,
6628                    uy,
6629                    uz,
6630                    vx,
6631                    vy,
6632                    vz,
6633                    nt,
6634                    mr,
6635                    st,
6636                    fr,
6637                    hue,
6638                );
6639                return Ok(Value::Unit);
6640            },
6641
6642            // vtex_flower(cx,cy,cz, ux,uy,uz, vx,vy,vz, radius,n_sides, fr,hue)
6643            "vtex_flower" | "ลายดอก" | "纹花" | "花模様" | "꽃무늬" | "الگوی_گل" | "نقش_زهرة" | "דוגמת_פרח" | "نقش_پھول" | "motif_fleur" | "muster_blume" | "узор_цветок" => {
6644                let cx = self.arg_num(&args, 0, 0.)? as f32;
6645                let cy = self.arg_num(&args, 1, 0.)? as f32;
6646                let cz = self.arg_num(&args, 2, 0.)? as f32;
6647                let ux = self.arg_num(&args, 3, 1.)? as f32;
6648                let uy = self.arg_num(&args, 4, 0.)? as f32;
6649                let uz = self.arg_num(&args, 5, 0.)? as f32;
6650                let vx = self.arg_num(&args, 6, 0.)? as f32;
6651                let vy = self.arg_num(&args, 7, 0.)? as f32;
6652                let vz = self.arg_num(&args, 8, 1.)? as f32;
6653                let r = self.arg_num(&args, 9, 1.)? as f32;
6654                let ns = self.arg_num(&args, 10, 24.)? as usize;
6655                let fr = self.arg_num(&args, 11, 0.)? as f32;
6656                let hue = self.arg_num(&args, 12, 0.)? as f32;
6657                let mut gfx = self.gfx.borrow_mut();
6658                let cam = gfx.camera.clone();
6659                crate::gfx::vtex::draw_flower(
6660                    &mut gfx.depth_queue,
6661                    &cam,
6662                    cx,
6663                    cy,
6664                    cz,
6665                    ux,
6666                    uy,
6667                    uz,
6668                    vx,
6669                    vy,
6670                    vz,
6671                    r,
6672                    ns,
6673                    fr,
6674                    hue,
6675                );
6676                return Ok(Value::Unit);
6677            },
6678
6679            // vtex_letter_rain(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_cols,n_vis, col_w,row_h, speed, fr,hue)
6680            "vtex_letter_rain" | "ลายอักษรไหล" | "纹字雨" | "文字雨" | "글자비" | "الگوی_باران_حروف" | "نقش_مطر_الحروف" | "דוגמת_גשם_אותיות" | "نقش_حروف_بارش" | "motif_pluie_lettres" | "muster_buchstabenregen" | "узор_дождь_букв" =>
6681            {
6682                let cx = self.arg_num(&args, 0, 0.)? as f32;
6683                let cy = self.arg_num(&args, 1, 0.)? as f32;
6684                let cz = self.arg_num(&args, 2, 0.)? as f32;
6685                let ux = self.arg_num(&args, 3, 1.)? as f32;
6686                let uy = self.arg_num(&args, 4, 0.)? as f32;
6687                let uz = self.arg_num(&args, 5, 0.)? as f32;
6688                let vx = self.arg_num(&args, 6, 0.)? as f32;
6689                let vy = self.arg_num(&args, 7, 0.)? as f32;
6690                let vz = self.arg_num(&args, 8, 1.)? as f32;
6691                let nc = self.arg_num(&args, 9, 16.)? as usize;
6692                let nv = self.arg_num(&args, 10, 14.)? as usize;
6693                let cw = self.arg_num(&args, 11, 0.65)? as f32;
6694                let rh = self.arg_num(&args, 12, 0.60)? as f32;
6695                let sp = self.arg_num(&args, 13, 0.025)? as f32;
6696                let fr = self.arg_num(&args, 14, 0.)? as f32;
6697                let hue = self.arg_num(&args, 15, 0.)? as f32;
6698                let mut gfx = self.gfx.borrow_mut();
6699                let cam = gfx.camera.clone();
6700                crate::gfx::vtex::draw_letter_rain(
6701                    &mut gfx.depth_queue,
6702                    &cam,
6703                    cx,
6704                    cy,
6705                    cz,
6706                    ux,
6707                    uy,
6708                    uz,
6709                    vx,
6710                    vy,
6711                    vz,
6712                    nc,
6713                    nv,
6714                    cw,
6715                    rh,
6716                    sp,
6717                    fr,
6718                    hue,
6719                );
6720                return Ok(Value::Unit);
6721            },
6722
6723            // vtex_hyperbolic_uv(cx,cy,cz, ux,uy,uz, vx,vy,vz, max_r,n_circles,n_rays, fr,hue)
6724            "vtex_hyperbolic_uv" | "ลายไฮเพอร์โบลิก" | "纹曲面" | "双曲線" | "쌍곡선" | "الگوی_هذلولی" | "نقش_زائدي" | "דוגמת_היפרבולית" | "نقش_ہائپربولک" | "motif_uv_hyperbolique" | "muster_hyperbolische_uv" | "узор_гиперболический_uv" =>
6725            {
6726                let cx = self.arg_num(&args, 0, 0.)? as f32;
6727                let cy = self.arg_num(&args, 1, 0.)? as f32;
6728                let cz = self.arg_num(&args, 2, 0.)? as f32;
6729                let ux = self.arg_num(&args, 3, 1.)? as f32;
6730                let uy = self.arg_num(&args, 4, 0.)? as f32;
6731                let uz = self.arg_num(&args, 5, 0.)? as f32;
6732                let vx = self.arg_num(&args, 6, 0.)? as f32;
6733                let vy = self.arg_num(&args, 7, 0.)? as f32;
6734                let vz = self.arg_num(&args, 8, 1.)? as f32;
6735                let mr = self.arg_num(&args, 9, 5.)? as f32;
6736                let nc = self.arg_num(&args, 10, 12.)? as usize;
6737                let nr = self.arg_num(&args, 11, 18.)? as usize;
6738                let fr = self.arg_num(&args, 12, 0.)? as f32;
6739                let hue = self.arg_num(&args, 13, 0.)? as f32;
6740                let mut gfx = self.gfx.borrow_mut();
6741                let cam = gfx.camera.clone();
6742                crate::gfx::vtex::draw_hyperbolic_uv(
6743                    &mut gfx.depth_queue,
6744                    &cam,
6745                    cx,
6746                    cy,
6747                    cz,
6748                    ux,
6749                    uy,
6750                    uz,
6751                    vx,
6752                    vy,
6753                    vz,
6754                    mr,
6755                    nc,
6756                    nr,
6757                    fr,
6758                    hue,
6759                );
6760                return Ok(Value::Unit);
6761            },
6762
6763            // vtex_halftone(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cell_w,cell_h, density, fr,hue)
6764            "vtex_halftone" | "ลายจุด" | "纹半调" | "網点模様" | "망점" | "الگوی_نیم‌تن" | "نقش_نصفي" | "דוגמת_חצי_גוון" | "نقش_ہاف_ٹون" | "motif_demi_ton" | "muster_halbton" | "узор_растр" => {
6765                let cx = self.arg_num(&args, 0, 0.)? as f32;
6766                let cy = self.arg_num(&args, 1, 0.)? as f32;
6767                let cz = self.arg_num(&args, 2, 0.)? as f32;
6768                let ux = self.arg_num(&args, 3, 1.)? as f32;
6769                let uy = self.arg_num(&args, 4, 0.)? as f32;
6770                let uz = self.arg_num(&args, 5, 0.)? as f32;
6771                let vx = self.arg_num(&args, 6, 0.)? as f32;
6772                let vy = self.arg_num(&args, 7, 0.)? as f32;
6773                let vz = self.arg_num(&args, 8, 1.)? as f32;
6774                let cols = self.arg_num(&args, 9, 16.)? as usize;
6775                let rows = self.arg_num(&args, 10, 12.)? as usize;
6776                let cw = self.arg_num(&args, 11, 0.5)? as f32;
6777                let ch = self.arg_num(&args, 12, 0.5)? as f32;
6778                let dens = self.arg_num(&args, 13, 0.4)? as f32;
6779                let fr = self.arg_num(&args, 14, 0.)? as f32;
6780                let hue = self.arg_num(&args, 15, 0.)? as f32;
6781                let mut gfx = self.gfx.borrow_mut();
6782                let cam = gfx.camera.clone();
6783                crate::gfx::vtex::draw_halftone(
6784                    &mut gfx.depth_queue,
6785                    &cam,
6786                    cx,
6787                    cy,
6788                    cz,
6789                    ux,
6790                    uy,
6791                    uz,
6792                    vx,
6793                    vy,
6794                    vz,
6795                    cols,
6796                    rows,
6797                    cw,
6798                    ch,
6799                    dens,
6800                    fr,
6801                    hue,
6802                );
6803                return Ok(Value::Unit);
6804            },
6805
6806            // vtex_tessellated(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cell, amplitude,freq, fr,hue)
6807            "vtex_tessellated" | "ลายตาข่าย" | "纹镶嵌" | "網目模様" | "격자망" | "الگوی_کاشی‌کاری" | "نقش_مرصوف_متكرر" | "דוגמת_ריצוף_חוזר" | "نقش_ٹائلنگ" | "motif_tesselle" | "muster_tessellation" | "узор_мозаика" =>
6808            {
6809                let cx = self.arg_num(&args, 0, 0.)? as f32;
6810                let cy = self.arg_num(&args, 1, 0.)? as f32;
6811                let cz = self.arg_num(&args, 2, 0.)? as f32;
6812                let ux = self.arg_num(&args, 3, 1.)? as f32;
6813                let uy = self.arg_num(&args, 4, 0.)? as f32;
6814                let uz = self.arg_num(&args, 5, 0.)? as f32;
6815                let vx = self.arg_num(&args, 6, 0.)? as f32;
6816                let vy = self.arg_num(&args, 7, 0.)? as f32;
6817                let vz = self.arg_num(&args, 8, 1.)? as f32;
6818                let cols = self.arg_num(&args, 9, 14.)? as usize;
6819                let rows = self.arg_num(&args, 10, 10.)? as usize;
6820                let cell = self.arg_num(&args, 11, 0.6)? as f32;
6821                let amp = self.arg_num(&args, 12, 0.25)? as f32;
6822                let freq = self.arg_num(&args, 13, 4.)? as f32;
6823                let fr = self.arg_num(&args, 14, 0.)? as f32;
6824                let hue = self.arg_num(&args, 15, 0.)? as f32;
6825                let mut gfx = self.gfx.borrow_mut();
6826                let cam = gfx.camera.clone();
6827                crate::gfx::vtex::draw_tessellated(
6828                    &mut gfx.depth_queue,
6829                    &cam,
6830                    cx,
6831                    cy,
6832                    cz,
6833                    ux,
6834                    uy,
6835                    uz,
6836                    vx,
6837                    vy,
6838                    vz,
6839                    cols,
6840                    rows,
6841                    cell,
6842                    amp,
6843                    freq,
6844                    fr,
6845                    hue,
6846                );
6847                return Ok(Value::Unit);
6848            },
6849
6850            // vtex_lotus(cx,cy,cz, ux,uy,uz, vx,vy,vz, r_inner,r_outer,n_petals, fr,hue)
6851            "vtex_lotus" | "ลายดอกบัว" | "纹莲" | "蓮模様" | "연꽃무늬" | "الگوی_لوتوس" | "نقش_لوتس" | "דוגמת_לוטוס" | "نقش_کنول" | "motif_lotus" | "muster_lotus" | "узор_лотос" =>
6852            {
6853                let cx = self.arg_num(&args, 0, 0.)? as f32;
6854                let cy = self.arg_num(&args, 1, 0.)? as f32;
6855                let cz = self.arg_num(&args, 2, 0.)? as f32;
6856                let ux = self.arg_num(&args, 3, 1.)? as f32;
6857                let uy = self.arg_num(&args, 4, 0.)? as f32;
6858                let uz = self.arg_num(&args, 5, 0.)? as f32;
6859                let vx = self.arg_num(&args, 6, 0.)? as f32;
6860                let vy = self.arg_num(&args, 7, 0.)? as f32;
6861                let vz = self.arg_num(&args, 8, 1.)? as f32;
6862                let ri = self.arg_num(&args, 9, 1.)? as f32;
6863                let ro = self.arg_num(&args, 10, 2.)? as f32;
6864                let np = self.arg_num(&args, 11, 12.)? as usize;
6865                let fr = self.arg_num(&args, 12, 0.)? as f32;
6866                let hue = self.arg_num(&args, 13, 0.)? as f32;
6867                let mut gfx = self.gfx.borrow_mut();
6868                let cam = gfx.camera.clone();
6869                crate::gfx::vtex::draw_lotus(
6870                    &mut gfx.depth_queue,
6871                    &cam,
6872                    cx,
6873                    cy,
6874                    cz,
6875                    ux,
6876                    uy,
6877                    uz,
6878                    vx,
6879                    vy,
6880                    vz,
6881                    ri,
6882                    ro,
6883                    np,
6884                    fr,
6885                    hue,
6886                );
6887                return Ok(Value::Unit);
6888            },
6889
6890            // vtex_chakra(cx,cy,cz, ux,uy,uz, vx,vy,vz, r,n_spokes, fr,hue)
6891            "vtex_chakra" | "ลายจักร" | "纹轮" | "輪模様" | "바퀴무늬" | "الگوی_چاکرا" | "نقش_تشاكرا" | "דוגמת_צ'אקרה" | "نقش_چکر" | "motif_chakra" | "muster_chakra" | "узор_чакра" => {
6892                let cx = self.arg_num(&args, 0, 0.)? as f32;
6893                let cy = self.arg_num(&args, 1, 0.)? as f32;
6894                let cz = self.arg_num(&args, 2, 0.)? as f32;
6895                let ux = self.arg_num(&args, 3, 1.)? as f32;
6896                let uy = self.arg_num(&args, 4, 0.)? as f32;
6897                let uz = self.arg_num(&args, 5, 0.)? as f32;
6898                let vx = self.arg_num(&args, 6, 0.)? as f32;
6899                let vy = self.arg_num(&args, 7, 0.)? as f32;
6900                let vz = self.arg_num(&args, 8, 1.)? as f32;
6901                let r = self.arg_num(&args, 9, 2.)? as f32;
6902                let ns = self.arg_num(&args, 10, 8.)? as usize;
6903                let fr = self.arg_num(&args, 11, 0.)? as f32;
6904                let hue = self.arg_num(&args, 12, 0.)? as f32;
6905                let mut gfx = self.gfx.borrow_mut();
6906                let cam = gfx.camera.clone();
6907                crate::gfx::vtex::draw_chakra(
6908                    &mut gfx.depth_queue,
6909                    &cam,
6910                    cx,
6911                    cy,
6912                    cz,
6913                    ux,
6914                    uy,
6915                    uz,
6916                    vx,
6917                    vy,
6918                    vz,
6919                    r,
6920                    ns,
6921                    fr,
6922                    hue,
6923                );
6924                return Ok(Value::Unit);
6925            },
6926
6927            // vtex_yantra(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_layers,max_r, fr,hue)
6928            "vtex_yantra" | "ลายยันต์" | "纹咒" | "護符模様" | "부적무늬" | "الگوی_یانترا" | "نقش_يانترا" | "דוגמת_יאנטרה" | "نقش_ینترا" | "motif_yantra" | "muster_yantra" | "узор_янтра" =>
6929            {
6930                let cx = self.arg_num(&args, 0, 0.)? as f32;
6931                let cy = self.arg_num(&args, 1, 0.)? as f32;
6932                let cz = self.arg_num(&args, 2, 0.)? as f32;
6933                let ux = self.arg_num(&args, 3, 1.)? as f32;
6934                let uy = self.arg_num(&args, 4, 0.)? as f32;
6935                let uz = self.arg_num(&args, 5, 0.)? as f32;
6936                let vx = self.arg_num(&args, 6, 0.)? as f32;
6937                let vy = self.arg_num(&args, 7, 0.)? as f32;
6938                let vz = self.arg_num(&args, 8, 1.)? as f32;
6939                let nl = self.arg_num(&args, 9, 4.)? as usize;
6940                let mr = self.arg_num(&args, 10, 3.)? as f32;
6941                let fr = self.arg_num(&args, 11, 0.)? as f32;
6942                let hue = self.arg_num(&args, 12, 0.)? as f32;
6943                let mut gfx = self.gfx.borrow_mut();
6944                let cam = gfx.camera.clone();
6945                crate::gfx::vtex::draw_yantra(
6946                    &mut gfx.depth_queue,
6947                    &cam,
6948                    cx,
6949                    cy,
6950                    cz,
6951                    ux,
6952                    uy,
6953                    uz,
6954                    vx,
6955                    vy,
6956                    vz,
6957                    nl,
6958                    mr,
6959                    fr,
6960                    hue,
6961                );
6962                return Ok(Value::Unit);
6963            },
6964
6965            // vtex_spiked_cog(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_teeth,r_body,r_spike,r_hub,n_spokes, fr,hue)
6966            "vtex_spiked_cog" | "ฟันเฟืองหนาม" | "纹棘轮" | "歯車模様" | "톱니바퀴" | "الگوی_چرخ‌دنده_خاردار" | "نقش_ترس_شائك" | "דוגמת_גלגל_קוצני" | "نقش_خاردار_گیئر" | "motif_engrenage_pointes" | "muster_stachelzahnrad" | "узор_шестерня_шипы" =>
6967            {
6968                let cx = self.arg_num(&args, 0, 0.)? as f32;
6969                let cy = self.arg_num(&args, 1, 0.)? as f32;
6970                let cz = self.arg_num(&args, 2, 0.)? as f32;
6971                let ux = self.arg_num(&args, 3, 1.)? as f32;
6972                let uy = self.arg_num(&args, 4, 0.)? as f32;
6973                let uz = self.arg_num(&args, 5, 0.)? as f32;
6974                let vx = self.arg_num(&args, 6, 0.)? as f32;
6975                let vy = self.arg_num(&args, 7, 0.)? as f32;
6976                let vz = self.arg_num(&args, 8, 1.)? as f32;
6977                let nt = self.arg_num(&args, 9, 12.)? as usize;
6978                let rb = self.arg_num(&args, 10, 1.)? as f32;
6979                let rs = self.arg_num(&args, 11, 1.3)? as f32;
6980                let rh = self.arg_num(&args, 12, 0.2)? as f32;
6981                let ns = self.arg_num(&args, 13, 6.)? as usize;
6982                let fr = self.arg_num(&args, 14, 0.)? as f32;
6983                let hue = self.arg_num(&args, 15, 0.)? as f32;
6984                let mut gfx = self.gfx.borrow_mut();
6985                let cam = gfx.camera.clone();
6986                crate::gfx::vtex::draw_spiked_cog(
6987                    &mut gfx.depth_queue,
6988                    &cam,
6989                    cx,
6990                    cy,
6991                    cz,
6992                    ux,
6993                    uy,
6994                    uz,
6995                    vx,
6996                    vy,
6997                    vz,
6998                    nt,
6999                    rb,
7000                    rs,
7001                    rh,
7002                    ns,
7003                    fr,
7004                    hue,
7005                );
7006                return Ok(Value::Unit);
7007            },
7008
7009            // vtex_torii(cx,cy,cz, ux,uy,uz, vx,vy,vz, width,height, fr,hue)
7010            "vtex_torii" | "ประตูโทริอิ" | "纹鸟居" | "鳥居" | "도리이" | "الگوی_توری_ژاپنی" | "نقش_توري" | "דוגמת_טוריי" | "نقش_توری_گیٹ" | "motif_torii" | "muster_torii" | "узор_тории" =>
7011            {
7012                let cx = self.arg_num(&args, 0, 0.)? as f32;
7013                let cy = self.arg_num(&args, 1, 0.)? as f32;
7014                let cz = self.arg_num(&args, 2, 0.)? as f32;
7015                let ux = self.arg_num(&args, 3, 1.)? as f32;
7016                let uy = self.arg_num(&args, 4, 0.)? as f32;
7017                let uz = self.arg_num(&args, 5, 0.)? as f32;
7018                let vx = self.arg_num(&args, 6, 0.)? as f32;
7019                let vy = self.arg_num(&args, 7, 0.)? as f32;
7020                let vz = self.arg_num(&args, 8, 1.)? as f32;
7021                let w = self.arg_num(&args, 9, 4.)? as f32;
7022                let h = self.arg_num(&args, 10, 5.)? as f32;
7023                let fr = self.arg_num(&args, 11, 0.)? as f32;
7024                let hue = self.arg_num(&args, 12, 0.)? as f32;
7025                let mut gfx = self.gfx.borrow_mut();
7026                let cam = gfx.camera.clone();
7027                crate::gfx::vtex::draw_torii(
7028                    &mut gfx.depth_queue,
7029                    &cam,
7030                    cx,
7031                    cy,
7032                    cz,
7033                    ux,
7034                    uy,
7035                    uz,
7036                    vx,
7037                    vy,
7038                    vz,
7039                    w,
7040                    h,
7041                    fr,
7042                    hue,
7043                );
7044                return Ok(Value::Unit);
7045            },
7046
7047            // vtex_pagoda(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_tiers,base_w,tier_h,taper,eave_out, fr,hue)
7048            "vtex_pagoda" | "เจดีย์" | "纹塔" | "塔" | "탑" | "الگوی_پاگودا" | "نقش_باغودا" | "דוגמת_פגודה" | "نقش_پگوڈا" | "motif_pagode" | "muster_pagode" | "узор_пагода" => {
7049                let cx = self.arg_num(&args, 0, 0.)? as f32;
7050                let cy = self.arg_num(&args, 1, 0.)? as f32;
7051                let cz = self.arg_num(&args, 2, 0.)? as f32;
7052                let ux = self.arg_num(&args, 3, 1.)? as f32;
7053                let uy = self.arg_num(&args, 4, 0.)? as f32;
7054                let uz = self.arg_num(&args, 5, 0.)? as f32;
7055                let vx = self.arg_num(&args, 6, 0.)? as f32;
7056                let vy = self.arg_num(&args, 7, 0.)? as f32;
7057                let vz = self.arg_num(&args, 8, 1.)? as f32;
7058                let nt = self.arg_num(&args, 9, 5.)? as usize;
7059                let bw = self.arg_num(&args, 10, 2.)? as f32;
7060                let th = self.arg_num(&args, 11, 1.)? as f32;
7061                let tp = self.arg_num(&args, 12, 0.72)? as f32;
7062                let eo = self.arg_num(&args, 13, 0.28)? as f32;
7063                let fr = self.arg_num(&args, 14, 0.)? as f32;
7064                let hue = self.arg_num(&args, 15, 0.)? as f32;
7065                let mut gfx = self.gfx.borrow_mut();
7066                let cam = gfx.camera.clone();
7067                crate::gfx::vtex::draw_pagoda(
7068                    &mut gfx.depth_queue,
7069                    &cam,
7070                    cx,
7071                    cy,
7072                    cz,
7073                    ux,
7074                    uy,
7075                    uz,
7076                    vx,
7077                    vy,
7078                    vz,
7079                    nt,
7080                    bw,
7081                    th,
7082                    tp,
7083                    eo,
7084                    fr,
7085                    hue,
7086                );
7087                return Ok(Value::Unit);
7088            },
7089
7090            // ══════════════════════════════════════════════════════════════════
7091            // AUDIO BUILTINS
7092            // ══════════════════════════════════════════════════════════════════
7093
7094            // audio_tone(idx, x, y, z, w, freq, amp, lfo_rate, lfo_depth)
7095            #[cfg(not(target_arch = "wasm32"))]
7096            "audio_tone"
7097            | "เสียงโทน"
7098            | "音调"
7099            | "音調"
7100            | "음조"
7101            | "空间音"
7102            | "空間音"
7103            | "공간음" | "تن_صدا" | "نغمة" | "צליל" | "آواز_کا_سر" | "tonalité_audio" | "audioton" | "звук_тон" => {
7104                let idx = self.arg_num(&args, 0, 0.0)? as usize;
7105                let x = self.arg_num(&args, 1, 0.0)? as f32;
7106                let y = self.arg_num(&args, 2, 0.0)? as f32;
7107                let z = self.arg_num(&args, 3, 0.0)? as f32;
7108                let w = self.arg_num(&args, 4, 1.0)? as f32;
7109                let freq = self.arg_num(&args, 5, 220.0)? as f32;
7110                let amp = self.arg_num(&args, 6, 0.15)? as f32;
7111                let lfo_rate = self.arg_num(&args, 7, 0.5)? as f32;
7112                let lfo_depth = self.arg_num(&args, 8, 0.02)? as f32;
7113                if let Some(audio) = &self.audio {
7114                    audio.set_tone(
7115                        idx,
7116                        ToneParams { x, y, z, w, freq, amp, lfo_rate, lfo_depth },
7117                    );
7118                }
7119                return Ok(Value::Unit);
7120            },
7121
7122            #[cfg(not(target_arch = "wasm32"))]
7123            "audio_listener" | "ผู้ฟัง" | "音频监听" | "音声リスナー" | "오디오리스너" | "شنونده_صدا" | "مستمع_الصوت" | "מאזין_קול" | "آواز_سننے_والا" | "auditeur_audio" | "audiohörer" | "звук_слушатель" =>
7124            {
7125                let cry = self.arg_num(&args, 0, 1.0)? as f32;
7126                let sry = self.arg_num(&args, 1, 0.0)? as f32;
7127                let crx = self.arg_num(&args, 2, 1.0)? as f32;
7128                let srx = self.arg_num(&args, 3, 0.0)? as f32;
7129                if let Some(audio) = &self.audio {
7130                    audio.set_listener(cry, sry, crx, srx);
7131                }
7132                return Ok(Value::Unit);
7133            },
7134
7135            #[cfg(not(target_arch = "wasm32"))]
7136            "audio_bgm" | "เพลงพื้นหลัง" | "เพลงประกอบ" | "背景乐" | "BGM" | "배경음악" | "موسیقی_پس‌زمینه" | "موسيقى_خلفية" | "מוזיקת_רקע" | "پس_منظر_موسیقی" | "musique_fond" | "hintergrundmusik" | "фоновая_музыка" =>
7137            {
7138                let path = match args.first() {
7139                    Some(Value::Str(s)) => s.clone(),
7140                    _ => return Ok(Value::Unit),
7141                };
7142                let vol = self.arg_num(&args, 1, 0.5)? as f32;
7143                if let Some(audio) = &self.audio {
7144                    audio.load_bgm(&path, vol);
7145                }
7146                return Ok(Value::Unit);
7147            },
7148
7149            #[cfg(not(target_arch = "wasm32"))]
7150            "audio_bgm_volume"
7151            | "ระดับเสียงพื้นหลัง"
7152            | "ระดับเพลงประกอบ"
7153            | "背景乐音量"
7154            | "BGM音量"
7155            | "배경음악음량" | "بلندی_موسیقی_پس‌زمینه" | "مستوى_موسيقى_الخلفية" | "עוצמת_מוזיקת_רקע" | "پس_منظر_موسیقی_شدت" | "volume_musique_fond" | "hintergrundmusiklautstärke" | "громкость_фоновой_музыки" => {
7156                let vol = self.arg_num(&args, 0, 0.5)? as f32;
7157                if let Some(audio) = &self.audio {
7158                    audio.set_bgm_volume(vol);
7159                }
7160                return Ok(Value::Unit);
7161            },
7162
7163            #[cfg(not(target_arch = "wasm32"))]
7164            "audio_volume" | "ระดับเสียง" | "音量" | "음량" | "بلندی_صدا" | "مستوى_الصوت" | "עוצמת_קול" | "آواز_کی_شدت" | "volume_audio" | "audiolautstärke" | "звук_громкость" => {
7165                let vol = self.arg_num(&args, 0, 0.7)? as f32;
7166                if let Some(audio) = &self.audio {
7167                    audio.set_master_volume(vol);
7168                }
7169                return Ok(Value::Unit);
7170            },
7171
7172            // WASM audio builtins — delegate to Web Audio API
7173            #[cfg(target_arch = "wasm32")]
7174            "audio_tone"
7175            | "เสียงโทน"
7176            | "音调"
7177            | "音調"
7178            | "음조"
7179            | "空间音"
7180            | "空間音"
7181            | "공간음" | "تن_صدا" | "نغمة" | "צליל" | "آواز_کا_سر" | "tonalité_audio" | "audioton" | "звук_тон" => {
7182                let idx = self.arg_num(&args, 0, 0.0)? as usize;
7183                let x = self.arg_num(&args, 1, 0.0)? as f32;
7184                let y = self.arg_num(&args, 2, 0.0)? as f32;
7185                let z = self.arg_num(&args, 3, 0.0)? as f32;
7186                let w = self.arg_num(&args, 4, 1.0)? as f32;
7187                let freq = self.arg_num(&args, 5, 220.0)? as f32;
7188                let amp = self.arg_num(&args, 6, 0.15)? as f32;
7189                let lfo_rate = self.arg_num(&args, 7, 0.5)? as f32;
7190                let lfo_depth = self.arg_num(&args, 8, 0.02)? as f32;
7191                crate::gfx::audio_web::set_tone(idx, x, y, z, w, freq, amp, lfo_rate, lfo_depth);
7192                return Ok(Value::Unit);
7193            },
7194
7195            #[cfg(target_arch = "wasm32")]
7196            "audio_listener" | "ผู้ฟัง" | "音频监听" | "音声リスナー" | "오디오리스너" | "شنونده_صدا" | "مستمع_الصوت" | "מאזין_קול" | "آواز_سننے_والا" | "auditeur_audio" | "audiohörer" | "звук_слушатель" =>
7197            {
7198                let cry = self.arg_num(&args, 0, 1.0)? as f32;
7199                let sry = self.arg_num(&args, 1, 0.0)? as f32;
7200                let crx = self.arg_num(&args, 2, 1.0)? as f32;
7201                let srx = self.arg_num(&args, 3, 0.0)? as f32;
7202                crate::gfx::audio_web::set_listener(cry, sry, crx, srx);
7203                return Ok(Value::Unit);
7204            },
7205
7206            #[cfg(target_arch = "wasm32")]
7207            "audio_bgm" | "เพลงพื้นหลัง" | "เพลงประกอบ" | "背景乐" | "BGM" | "배경음악" | "موسیقی_پس‌زمینه" | "موسيقى_خلفية" | "מוזיקת_רקע" | "پس_منظر_موسیقی" | "musique_fond" | "hintergrundmusik" | "фоновая_музыка" =>
7208            {
7209                let path = self.arg_str(&args, 0, "");
7210                let vol = self.arg_num(&args, 1, 0.5)? as f32;
7211                crate::gfx::audio_web::load_bgm(&path, vol);
7212                return Ok(Value::Unit);
7213            },
7214
7215            #[cfg(target_arch = "wasm32")]
7216            "audio_bgm_volume"
7217            | "ระดับเสียงพื้นหลัง"
7218            | "ระดับเพลงประกอบ"
7219            | "背景乐音量"
7220            | "BGM音量"
7221            | "배경음악음량" | "بلندی_موسیقی_پس‌زمینه" | "مستوى_موسيقى_الخلفية" | "עוצמת_מוזיקת_רקע" | "پس_منظر_موسیقی_شدت" | "volume_musique_fond" | "hintergrundmusiklautstärke" | "громкость_фоновой_музыки" => {
7222                let vol = self.arg_num(&args, 0, 0.5)? as f32;
7223                crate::gfx::audio_web::set_bgm_volume(vol);
7224                return Ok(Value::Unit);
7225            },
7226
7227            #[cfg(target_arch = "wasm32")]
7228            "audio_volume" | "ระดับเสียง" | "音量" | "음량" | "بلندی_صدا" | "مستوى_الصوت" | "עוצמת_קול" | "آواز_کی_شدت" | "volume_audio" | "audiolautstärke" | "звук_громкость" => {
7229                let vol = self.arg_num(&args, 0, 0.7)? as f32;
7230                crate::gfx::audio_web::set_master_volume(vol);
7231                return Ok(Value::Unit);
7232            },
7233
7234            // ── WASM sample load / play / stop / FX (Web Audio pool) ─────────
7235            #[cfg(target_arch = "wasm32")]
7236            "audio_sample_load" | "载入采样" | "サンプル読込" | "샘플로드" | "โหลดตัวอย่างเสียง" | "بارگذاری_نمونه_صدا" | "تحميل_عينة_صوتية" | "טעינת_דגימת_קול" | "آواز_نمونہ_لوڈ" | "charger_échantillon" | "sample_laden" | "загрузить_семпл" =>
7237            {
7238                let path = self.arg_str(&args, 0, "");
7239                let resolved = self.wasm_resolve_source_path(&path);
7240                match wasm_fetch_bytes(&resolved)
7241                    .and_then(|bytes| ling_music::from_bytes(&bytes).map_err(|e| e.to_string()))
7242                {
7243                    Ok(t) => {
7244                        let id = crate::gfx::audio_web::add_sample(&t.stereo, t.channels, t.rate);
7245                        return Ok(Value::Number(id as f64));
7246                    },
7247                    Err(e) => {
7248                        eprintln!("audio_sample_load failed ({path}): {e}");
7249                        return Ok(Value::Number(-1.0));
7250                    },
7251                }
7252            },
7253            #[cfg(target_arch = "wasm32")]
7254            "audio_sample_play" | "播放采样" | "サンプル再生" | "샘플재생" | "เล่นตัวอย่างเสียง" | "پخش_نمونه_صدا" | "تشغيل_عينة_صوتية" | "נגינת_דגימת_קול" | "آواز_نمونہ_چلاؤ" | "jouer_échantillon" | "sample_abspielen" | "играть_семпл" =>
7255            {
7256                let id = self.arg_num(&args, 0, 0.0)? as usize;
7257                let x = self.arg_num(&args, 1, 0.0)? as f32;
7258                let y = self.arg_num(&args, 2, 0.0)? as f32;
7259                let z = self.arg_num(&args, 3, 0.0)? as f32;
7260                // arg 4 is w (4th spatial dim) — ignored for 3-D panner
7261                let vol = self.arg_num(&args, 5, 1.0)? as f32;
7262                let looping = self.arg_num(&args, 6, 0.0)? > 0.5;
7263                crate::gfx::audio_web::play_sample(id, x, y, z, vol, looping);
7264                return Ok(Value::Number(0.0));
7265            },
7266            #[cfg(target_arch = "wasm32")]
7267            "audio_sample_stop"
7268            | "停止采样"
7269            | "サンプル停止"
7270            | "샘플정지"
7271            | "หยุดตัวอย่างเสียง"
7272            | "audio_fx_reverb"
7273            | "混响"
7274            | "リバーブ"
7275            | "리버브"
7276            | "เสียงก้อง"
7277            | "audio_fx_delay"
7278            | "回声"
7279            | "ディレイ効果"
7280            | "딜레이"
7281            | "เสียงสะท้อน"
7282            | "audio_fx_lowpass"
7283            | "低通滤波"
7284            | "ローパス"
7285            | "저역통과"
7286            | "กรองความถี่ต่ำ" | "توقف_نمونه_صدا" | "إيقاف_عينة_صوتية" | "עצירת_דגימת_קול" | "آواز_نمونہ_روکو" | "arrêter_échantillon" | "sample_stoppen" | "остановить_семпл" => {
7287                return Ok(Value::Unit);
7288            },
7289
7290            // ── รอหน้าต่าง() — block until window closed / Escape ──
7291            "รอหน้าต่าง" | "wait_window" | "gfx_wait" | "انتظار_پنجره" | "انتظر_النافذة" | "המתן_לחלון" | "ونڈو_انتظار" => {
7292                #[cfg(not(target_arch = "wasm32"))]
7293                loop {
7294                    let still_open = {
7295                        let gfx = self.gfx.borrow();
7296                        gfx.window
7297                            .as_ref()
7298                            .map(|w| w.is_open() && !w.is_key_down(minifb::Key::Escape))
7299                            .unwrap_or(false)
7300                    };
7301                    if !still_open {
7302                        break;
7303                    }
7304                    let (buf, w, h) = {
7305                        let gfx = self.gfx.borrow();
7306                        (gfx.buffer.clone(), gfx.width, gfx.height)
7307                    };
7308                    let mut gfx = self.gfx.borrow_mut();
7309                    if let Some(win) = gfx.window.as_mut() {
7310                        if win.update_with_buffer(&buf, w, h).is_err() {
7311                            break;
7312                        }
7313                    }
7314                }
7315                return Ok(Value::Unit);
7316            },
7317
7318            // ── File I/O ──────────────────────────────────────────────────────
7319            "read_file" | "อ่านไฟล์" | "خواندن_فایل" | "اقرأ_الملف" | "קרא_קובץ" | "فائل_پڑھو" => {
7320                #[cfg(target_arch = "wasm32")]
7321                return Ok(Value::Str(String::new()));
7322                #[cfg(not(target_arch = "wasm32"))]
7323                {
7324                    let path = self.arg_str(&args, 0, "").replace('\\', "/");
7325                    return std::fs::read_to_string(&path)
7326                        .map(Value::Str)
7327                        .map_err(|e| EvalErr::from(format!("read_file '{path}': {e}")));
7328                }
7329            },
7330            // ── networking (TCP, 2-peer co-op) ───────────────────────────────
7331            #[cfg(not(target_arch = "wasm32"))]
7332            "net_host" | "เน็ตโฮสต์" | "میزبانی_شبکه" | "استضف_الشبكة" | "ארח_רשת" | "نیٹ_ہوسٹ" => {
7333                let port = self.arg_num(&args, 0, 7777.0)? as u16;
7334                net::host(port);
7335                return Ok(Value::Unit);
7336            },
7337            #[cfg(not(target_arch = "wasm32"))]
7338            "net_join" | "เน็ตจอย" | "پیوستن_شبکه" | "انضم_للشبكة" | "הצטרף_לרשת" | "نیٹ_شمولیت" => {
7339                let ip = self.arg_str(&args, 0, "127.0.0.1");
7340                let port = self.arg_num(&args, 1, 7777.0)? as u16;
7341                net::join(&ip, port);
7342                return Ok(Value::Unit);
7343            },
7344            #[cfg(not(target_arch = "wasm32"))]
7345            "net_send" | "เน็ตส่ง" | "ارسال_شبکه" | "أرسل_عبر_الشبكة" | "שלח_ברשת" | "نیٹ_بھیجو" => {
7346                let s = self.arg_str(&args, 0, "");
7347                net::send(&s);
7348                return Ok(Value::Unit);
7349            },
7350            #[cfg(not(target_arch = "wasm32"))]
7351            "net_recv" | "เน็ตรับ" | "دریافت_شبکه" | "استقبل_من_الشبكة" | "קבל_מרשת" | "نیٹ_وصول" => {
7352                return Ok(Value::Str(net::recv()));
7353            },
7354            #[cfg(not(target_arch = "wasm32"))]
7355            "net_status" | "เน็ตสถานะ" | "وضعیت_شبکه" | "حالة_الشبكة" | "סטטוס_רשת" | "نیٹ_حالت" => {
7356                return Ok(Value::Number(net::status() as f64));
7357            },
7358            #[cfg(not(target_arch = "wasm32"))]
7359            "net_recv_from" => {
7360                return Ok(Value::Str(net::recv_from()));
7361            },
7362            #[cfg(not(target_arch = "wasm32"))]
7363            "net_send_to" => {
7364                let id = self.arg_num(&args, 0, 0.0)? as u64;
7365                let s = self.arg_str(&args, 1, "");
7366                net::send_to(id, &s);
7367                return Ok(Value::Unit);
7368            },
7369            #[cfg(not(target_arch = "wasm32"))]
7370            "net_close" | "연결종료" => {
7371                net::close();
7372                return Ok(Value::Unit);
7373            },
7374            // ── LAN lobby discovery (UDP broadcast) ──
7375            #[cfg(not(target_arch = "wasm32"))]
7376            "net_announce" | "เน็ตประกาศ" | "اعلام_شبکه" | "أعلن_في_الشبكة" | "הכרז_ברשת" | "نیٹ_اعلان" => {
7377                let port = self.arg_num(&args, 0, 7778.0)? as u16;
7378                let info = self.arg_str(&args, 1, "");
7379                net::announce(port, &info);
7380                return Ok(Value::Unit);
7381            },
7382            #[cfg(not(target_arch = "wasm32"))]
7383            "net_announce_stop" | "เน็ตหยุดประกาศ" | "توقف_اعلام" | "أوقف_الإعلان" | "עצור_הכרזה" | "اعلان_روکو" => {
7384                net::announce_stop();
7385                return Ok(Value::Unit);
7386            },
7387            #[cfg(not(target_arch = "wasm32"))]
7388            "net_discover" | "เน็ตค้นหา" | "کشف_شبکه" | "اكتشف_الشبكة" | "גלה_רשת" | "نیٹ_دریافت" => {
7389                let port = self.arg_num(&args, 0, 7778.0)? as u16;
7390                return Ok(Value::Str(net::discover(port)));
7391            },
7392            #[cfg(not(target_arch = "wasm32"))]
7393            "net_test" | "เน็ตทดสอบ" | "آزمون_شبکه" | "اختبر_الشبكة" | "בדוק_רשת" | "نیٹ_ٹیسٹ" => {
7394                let port = self.arg_num(&args, 0, 7777.0)? as u16;
7395                return Ok(Value::Str(net::test_bind(port)));
7396            },
7397            // ── HTTP server (interpreter <-> async bridge, see runtime::web) ──
7398            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7399            "http_route" | "เว็บเส้นทาง" | "مسیر_HTTP" | "مسار_HTTP" | "נתיב_HTTP" | "HTTP_روٹ" => {
7400                let method = self.arg_str(&args, 0, "GET").to_uppercase();
7401                let path = self.arg_str(&args, 1, "/");
7402                let handler = args.get(2).cloned().unwrap_or(Value::Unit);
7403                self.http_routes.push((method, path, handler));
7404                return Ok(Value::Unit);
7405            },
7406            // Registers a directory to be served as raw bytes at `prefix` (fonts,
7407            // images, generated zips/PDFs) — bypasses the String-only Request/
7408            // Response bridge entirely, so binary files come through intact.
7409            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7410            "http_static" | "เว็บสแตติก" | "فایل_ایستای_HTTP" | "ملفات_HTTP_ثابتة" | "קבצים_סטטיים_HTTP" | "HTTP_مستقل_فائل" => {
7411                let prefix = self.arg_str(&args, 0, "/static");
7412                let dir = self.arg_str(&args, 1, "static");
7413                self.http_static_dirs.push((prefix, dir));
7414                return Ok(Value::Unit);
7415            },
7416            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7417            "http_serve" | "เว็บเสิร์ฟ" | "سرویس_HTTP" | "قدّم_HTTP" | "הגש_HTTP" | "HTTP_سرو" => {
7418                let host = self.arg_str(&args, 0, "127.0.0.1");
7419                let port = self.arg_num(&args, 1, 8080.0)? as u16;
7420                let routes = std::mem::take(&mut self.http_routes);
7421                let static_dirs = std::mem::take(&mut self.http_static_dirs);
7422                // No premature "listening" print here: ling_http::serve_http
7423                // (called from spawn_server's background thread) now prints
7424                // its own banner, but only after the socket is actually
7425                // bound — a more honest signal than printing right after
7426                // requesting the background thread be spawned.
7427                let rx = web::spawn_server(host.clone(), port, static_dirs);
7428                for pending in rx {
7429                    let matched = routes
7430                        .iter()
7431                        .find(|(m, p, _)| m == &pending.method && p == &pending.path);
7432                    let response = match matched {
7433                        Some((_, _, handler)) => {
7434                            let req_value = Value::Struct {
7435                                name: "Request".to_string(),
7436                                fields: vec![
7437                                    ("method".to_string(), Value::Str(pending.method.clone())),
7438                                    ("path".to_string(), Value::Str(pending.path.clone())),
7439                                    ("query".to_string(), Value::Str(pending.query.clone())),
7440                                    ("body".to_string(), Value::Str(pending.body.clone())),
7441                                    ("cookie".to_string(), Value::Str(pending.cookie.clone())),
7442                                    (
7443                                        "authorization".to_string(),
7444                                        Value::Str(pending.authorization.clone()),
7445                                    ),
7446                                    (
7447                                        "client_ip".to_string(),
7448                                        Value::Str(pending.client_ip.clone()),
7449                                    ),
7450                                ],
7451                            };
7452                            match self.call_value(handler.clone(), vec![req_value]) {
7453                                Ok(v) => web::value_to_response(&v),
7454                                Err(e) => web::HttpResponse {
7455                                    status: 500,
7456                                    content_type: "text/plain; charset=utf-8".to_string(),
7457                                    body: format!("handler error: {e:?}"),
7458                                    set_cookie: None,
7459                                    location: None,
7460                                },
7461                            }
7462                        },
7463                        None => web::HttpResponse {
7464                            status: 404,
7465                            content_type: "text/plain; charset=utf-8".to_string(),
7466                            body: "not found".to_string(),
7467                            set_cookie: None,
7468                            location: None,
7469                        },
7470                    };
7471                    let _ = pending.respond_to.send(response);
7472                }
7473                return Ok(Value::Unit);
7474            },
7475            // Fires a POST request on a background async runtime and returns a job
7476            // id immediately — for slow external calls (e.g. local Stable Diffusion
7477            // generation) that must not block http_serve's single-threaded loop.
7478            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7479            "http_post_async" | "เว็บโพสต์ไม่บล็อก" | "ارسال_ناهمگام_HTTP" | "أرسل_HTTP_غير_متزامن" | "שלח_HTTP_אסינכרוני" | "HTTP_غیر_ہمزمان_بھیجو" => {
7480                let url = self.arg_str(&args, 0, "");
7481                let body = self.arg_str(&args, 1, "");
7482                let content_type = self.arg_str(&args, 2, "application/json");
7483                let id = self.async_jobs.start_post(url, content_type, body);
7484                return Ok(Value::Str(id));
7485            },
7486            // Non-blocking poll: "" while the job named by http_post_async (or
7487            // sdai_generate_start) is still running, the result once it
7488            // completes — same job table, same builtin polls both.
7489            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7490            "http_job_poll" | "เว็บงานสำรวจ" | "بررسی_وظیفه_HTTP" | "استطلع_مهمة_HTTP" | "בדוק_משימת_HTTP" | "HTTP_کام_پول" => {
7491                let id = self.arg_str(&args, 0, "");
7492                return Ok(Value::Str(self.async_jobs.poll(&id).unwrap_or_default()));
7493            },
7494            // Starts an AUTOMATIC1111-compatible txt2img generation in the
7495            // background against `base_url` (e.g. "http://127.0.0.1:1342").
7496            // Poll with http_job_poll: the result is the plain base64 PNG
7497            // once ready, or a string starting with "ERROR:" on failure —
7498            // the JSON response itself is parsed in Rust (see
7499            // AsyncJobs::start_sdai_txt2img), since `.ling` has no JSON parser.
7500            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7501            "sdai_generate_start" | "เอสดีเอไอเริ่มสร้าง" | "شروع_تولید_هوش" | "ابدأ_توليد_الذكاء" | "התחל_יצירת_בינה" | "اے_آئی_تخلیق_شروع" => {
7502                let base_url = self.arg_str(&args, 0, "http://127.0.0.1:1342");
7503                let prompt = self.arg_str(&args, 1, "");
7504                let width = self.arg_num(&args, 2, 512.0)? as u32;
7505                let height = self.arg_num(&args, 3, 512.0)? as u32;
7506                let id = self.async_jobs.start_sdai_txt2img(base_url, prompt, width, height);
7507                return Ok(Value::Str(id));
7508            },
7509            // ── query_param("q=a&page=2", "q", "") → "a" (URL-decoded) ──
7510            "query_param" | "พารามิเตอร์" | "پارامتر_پرسوجو" | "معامل_الاستعلام" | "פרמטר_שאילתה" | "کوئری_پیرامیٹر" => {
7511                let qs = self.arg_str(&args, 0, "");
7512                let name = self.arg_str(&args, 1, "");
7513                let default = self.arg_str(&args, 2, "");
7514                let mut found = default;
7515                for pair in qs.split('&') {
7516                    let mut it = pair.splitn(2, '=');
7517                    if it.next().unwrap_or("") == name {
7518                        found = url_decode(it.next().unwrap_or(""));
7519                        break;
7520                    }
7521                }
7522                return Ok(Value::Str(found));
7523            },
7524            // ── cookie_get("sid=abc; x=1", "sid", "") → "abc" ──
7525            "cookie_get" | "รับคุกกี้" | "دریافت_کوکی" | "اجلب_الكعكة" | "קבל_עוגייה" | "کوکی_حاصل_کرو" => {
7526                let header = self.arg_str(&args, 0, "");
7527                let name = self.arg_str(&args, 1, "");
7528                let default = self.arg_str(&args, 2, "");
7529                let mut found = default;
7530                for pair in header.split(';') {
7531                    let p = pair.trim();
7532                    let mut it = p.splitn(2, '=');
7533                    if it.next().unwrap_or("") == name {
7534                        found = it.next().unwrap_or("").to_string();
7535                        break;
7536                    }
7537                }
7538                return Ok(Value::Str(found));
7539            },
7540            // ── html_escape(s) — & < > " ' → entities, for echoing user input ──
7541            "html_escape" | "กันเอชทีเอ็มแอล" | "فرار_HTML" | "أفلت_HTML" | "בריחת_HTML" | "HTML_ایسکیپ" => {
7542                let s = self.arg_str(&args, 0, "");
7543                return Ok(Value::Str(
7544                    s.replace('&', "&amp;")
7545                        .replace('<', "&lt;")
7546                        .replace('>', "&gt;")
7547                        .replace('"', "&quot;")
7548                        .replace('\'', "&#39;"),
7549                ));
7550            },
7551            // json_escape(s) — escape a string for embedding inside a JSON
7552            // string literal (", \, and control chars). Needed because the
7553            // registry builds JSON API responses by concatenation; without
7554            // this a value containing " or \ breaks or injects into the JSON.
7555            "json_escape" | "หนีเจสัน" | "فرار_JSON" | "أفلت_JSON" | "בריחת_JSON" | "JSON_ایسکیپ" => {
7556                let s = self.arg_str(&args, 0, "");
7557                let mut out = String::with_capacity(s.len() + 8);
7558                for c in s.chars() {
7559                    match c {
7560                        '"' => out.push_str("\\\""),
7561                        '\\' => out.push_str("\\\\"),
7562                        '\n' => out.push_str("\\n"),
7563                        '\r' => out.push_str("\\r"),
7564                        '\t' => out.push_str("\\t"),
7565                        c if (c as u32) < 0x20 => {
7566                            out.push_str(&format!("\\u{:04x}", c as u32))
7567                        },
7568                        c => out.push(c),
7569                    }
7570                }
7571                return Ok(Value::Str(out));
7572            },
7573            // ── CLI arguments: cli_arg("port", "8080") reads `--port 6688` ──
7574            "cli_arg" | "อาร์กิวเมนต์" | "آرگومان_خط‌فرمان" | "معامل_سطر_الأوامر" | "ארגומנט_שורת_פקודה" | "سی_ایل_آئی_دلیل" => {
7575                let name = self.arg_str(&args, 0, "");
7576                let default = self.arg_str(&args, 1, "");
7577                let flag = format!("--{name}");
7578                let argv: Vec<String> = std::env::args().collect();
7579                let found = argv
7580                    .iter()
7581                    .position(|a| a == &flag)
7582                    .and_then(|i| argv.get(i + 1))
7583                    .cloned()
7584                    .unwrap_or(default);
7585                return Ok(Value::Str(found));
7586            },
7587            // ── SQLite (rusqlite, synchronous — matches the interpreter) ──
7588            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7589            "db_open" | "ฐานข้อมูลเปิด" | "باز_کردن_پایگاه_داده" | "افتح_قاعدة_البيانات" | "פתח_מסד_נתונים" | "ڈیٹا_بیس_کھولو" => {
7590                let path = self.arg_str(&args, 0, "app.db");
7591                let conn = ling_http::rusqlite::Connection::open(&path)
7592                    .map_err(|e| EvalErr::from(format!("db_open '{path}': {e}")))?;
7593                let _ = conn.execute_batch("PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL;");
7594                self.db = Some(conn);
7595                return Ok(Value::Unit);
7596            },
7597            // db_exec(sql, ...params) → rows affected. Params bind positionally
7598            // (?1, ?2, ...): numbers as REAL, bools as 0/1, everything else TEXT.
7599            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7600            "db_exec" | "ฐานข้อมูลรัน" | "اجرای_پایگاه_داده" | "نفّذ_في_قاعدة_البيانات" | "בצע_במסד_נתונים" | "ڈیٹا_بیس_عمل" => {
7601                let sql = self.arg_str(&args, 0, "");
7602                let params = values_to_sql_params(&args[1.min(args.len())..]);
7603                let conn = self
7604                    .db
7605                    .as_ref()
7606                    .ok_or_else(|| EvalErr::from("db_exec: call db_open first".to_string()))?;
7607                let n = conn
7608                    .execute(
7609                        &sql,
7610                        ling_http::rusqlite::params_from_iter(params.iter()),
7611                    )
7612                    .map_err(|e| EvalErr::from(format!("db_exec: {e}\n  sql: {sql}")))?;
7613                return Ok(Value::Number(n as f64));
7614            },
7615            // db_query(sql, ...params) → List of Row structs (row.column_name).
7616            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7617            "db_query" | "ฐานข้อมูลถาม" | "پرسوجوی_پایگاه_داده" | "استعلم_قاعدة_البيانات" | "שאילתת_מסד_נתונים" | "ڈیٹا_بیس_سوال" => {
7618                let sql = self.arg_str(&args, 0, "");
7619                let params = values_to_sql_params(&args[1.min(args.len())..]);
7620                let conn = self
7621                    .db
7622                    .as_ref()
7623                    .ok_or_else(|| EvalErr::from("db_query: call db_open first".to_string()))?;
7624                let mut stmt = conn
7625                    .prepare(&sql)
7626                    .map_err(|e| EvalErr::from(format!("db_query: {e}\n  sql: {sql}")))?;
7627                let col_names: Vec<String> =
7628                    stmt.column_names().iter().map(|s| s.to_string()).collect();
7629                let mut rows = stmt
7630                    .query(ling_http::rusqlite::params_from_iter(params.iter()))
7631                    .map_err(|e| EvalErr::from(format!("db_query: {e}")))?;
7632                let mut out = Vec::new();
7633                while let Some(row) = rows
7634                    .next()
7635                    .map_err(|e| EvalErr::from(format!("db_query row: {e}")))?
7636                {
7637                    let mut fields = Vec::with_capacity(col_names.len());
7638                    for (i, col) in col_names.iter().enumerate() {
7639                        use ling_http::rusqlite::types::ValueRef;
7640                        let v = match row.get_ref(i) {
7641                            Ok(ValueRef::Null) => Value::Str(String::new()),
7642                            Ok(ValueRef::Integer(n)) => Value::Number(n as f64),
7643                            Ok(ValueRef::Real(n)) => Value::Number(n),
7644                            Ok(ValueRef::Text(t)) => {
7645                                Value::Str(String::from_utf8_lossy(t).into_owned())
7646                            },
7647                            Ok(ValueRef::Blob(b)) =>
7648
7649                            {
7650                                use base64::Engine as _;
7651                                Value::Str(base64::engine::general_purpose::STANDARD.encode(b))
7652                            },
7653                            Err(_) => Value::Str(String::new()),
7654                        };
7655                        fields.push((col.clone(), v));
7656                    }
7657                    out.push(Value::Struct { name: "Row".to_string(), fields });
7658                }
7659                return Ok(Value::List(Rc::new(out)));
7660            },
7661            // ── gamepad (gilrs) ──
7662            #[cfg(not(target_arch = "wasm32"))]
7663            "gamepad_poll" | "จอยโพล" | "بررسی_دسته_بازی" | "استطلع_يد_اللعب" | "בדוק_בקר_משחק" | "گیم_پیڈ_پول_کرو" => {
7664                gamepad::poll();
7665                return Ok(Value::Unit);
7666            },
7667            #[cfg(not(target_arch = "wasm32"))]
7668            "gamepad_button" | "จอยปุ่ม" | "دکمه_دسته_بازی" | "زر_يد_اللعب" | "כפתור_בקר_משחק" | "گیم_پیڈ_بٹن_دبایا" => {
7669                let name = self.arg_str(&args, 0, "");
7670                return Ok(Value::Number(if gamepad::button(&name) {
7671                    1.0
7672                } else {
7673                    0.0
7674                }));
7675            },
7676            #[cfg(not(target_arch = "wasm32"))]
7677            "gamepad_axis" | "จอยแกน" | "محور_دسته_بازی" | "محور_يد_اللعب" | "ציר_בקר_משחק" | "گیم_پیڈ_محور" => {
7678                let name = self.arg_str(&args, 0, "");
7679                return Ok(Value::Number(gamepad::axis(&name) as f64));
7680            },
7681            #[cfg(not(target_arch = "wasm32"))]
7682            "gamepad_rumble" | "จอยสั่น" | "لرزش_دسته_بازی" | "اهتزاز_يد_اللعب" | "רטט_בקר_משחק" | "گیم_پیڈ_لرزش" => {
7683                let low = self.arg_num(&args, 0, 0.0)? as f32;
7684                let high = self.arg_num(&args, 1, 0.0)? as f32;
7685                let ms = self.arg_num(&args, 2, 200.0)? as u32;
7686                gamepad::rumble(low, high, ms);
7687                return Ok(Value::Unit);
7688            },
7689            #[cfg(not(target_arch = "wasm32"))]
7690            "gamepad_list" | "จอยรายการ" | "فهرست_دسته‌های_بازی" | "قائمة_أيدي_اللعب" | "רשימת_בקרי_משחק" | "گیم_پیڈ_فہرست" => {
7691                return Ok(Value::Str(gamepad::list()));
7692            },
7693            #[cfg(not(target_arch = "wasm32"))]
7694            "gamepad_any" | "จอยใดๆ" | "هر_دسته_بازی" | "أي_يد_لعب" | "בקר_כלשהו" | "کوئی_بھی_گیم_پیڈ" => {
7695                return Ok(Value::Number(if gamepad::any_button() { 1.0 } else { 0.0 }));
7696            },
7697            // wasm32: gamepad not available — return safe no-op values
7698            #[cfg(target_arch = "wasm32")]
7699            "gamepad_poll" | "จอยโพล" | "gamepad_rumble" | "จอยสั่น" | "بررسی_دسته_بازی" | "استطلع_يد_اللعب" | "בדוק_בקר_משחק" | "گیم_پیڈ_پول_کرو" => {
7700                return Ok(Value::Unit);
7701            },
7702            #[cfg(target_arch = "wasm32")]
7703            "gamepad_button" | "จอยปุ่ม" | "gamepad_axis" | "จอยแกน" | "gamepad_any" | "จอยใดๆ" | "دکمه_دسته_بازی" | "زر_يد_اللعب" | "כפתור_בקר_משחק" | "گیم_پیڈ_بٹن" =>
7704            {
7705                return Ok(Value::Number(0.0));
7706            },
7707            #[cfg(target_arch = "wasm32")]
7708            "gamepad_list" | "จอยรายการ" | "فهرست_دسته‌های_بازی" | "قائمة_أيدي_اللعب" | "רשימת_בקרי_משחק" | "گیم_پیڈ_فہرست" => {
7709                return Ok(Value::Str(String::new()));
7710            },
7711
7712            // ── game AI: neural networks ─────────────────────────────────────
7713            // nn_new(inputs[, seed]) → handle
7714            #[cfg(not(target_arch = "wasm32"))]
7715            "nn_new" | "建神经网" | "ニューラル作成" | "신경망생성" | "สร้างโครงข่าย" | "شبکه_جدید" | "شبكة_جديدة" | "רשת_חדשה" | "نئی_نیورل_نیٹ" | "nouveau_réseau" | "neues_netz" | "новая_сеть" =>
7716            {
7717                let n_in = self.arg_num(&args, 0, 1.0)?.max(0.0) as usize;
7718                let seed = self.arg_num(&args, 1, 1.0)? as u64;
7719                return Ok(Value::Number(ai::nn_new(n_in, seed) as f64));
7720            },
7721            // nn_dense(handle, units[, activation]) — append a layer
7722            #[cfg(not(target_arch = "wasm32"))]
7723            "nn_dense" | "密集层" | "密層追加" | "밀집층" | "ชั้นหนาแน่น" | "لایه_متراکم" | "طبقة_كثيفة" | "שכבה_צפופה" | "ڈینس_لیئر" | "réseau_dense" | "netz_dicht" | "плотная_сеть" =>
7724            {
7725                let id = self.arg_num(&args, 0, -1.0)? as i64;
7726                let units = self.arg_num(&args, 1, 1.0)?.max(1.0) as usize;
7727                let act = self.arg_str(&args, 2, "relu");
7728                ai::nn_dense(id, units, &act);
7729                return Ok(Value::Unit);
7730            },
7731            // nn_forward(handle, [inputs]) → [outputs]
7732            #[cfg(not(target_arch = "wasm32"))]
7733            "nn_forward" | "神经前向" | "順伝播" | "순전파" | "ส่งต่อโครงข่าย" | "پیش‌روی_شبکه" | "تمرير_أمامي" | "העברה_קדימה" | "فارورڈ_پاس" | "propager_réseau" | "netz_vorwärts" | "прямой_проход_сети" =>
7734            {
7735                let id = self.arg_num(&args, 0, -1.0)? as i64;
7736                let input = self.arg_list_f32(&args, 1);
7737                let out = ai::nn_forward(id, &input);
7738                return Ok(Value::List(Rc::new(
7739                    out.into_iter().map(|v| Value::Number(v as f64)).collect(),
7740                )));
7741            },
7742            // nn_train(handle, [inputs], [targets][, lr]) → loss
7743            #[cfg(not(target_arch = "wasm32"))]
7744            "nn_train" | "训练网" | "ニューラル学習" | "신경망학습" | "ฝึกโครงข่าย" | "آموزش_شبکه" | "درّب_الشبكة" | "אמן_רשת" | "نیٹ_ٹریننگ" | "entraîner_réseau" | "netz_trainieren" | "обучить_сеть" =>
7745            {
7746                let id = self.arg_num(&args, 0, -1.0)? as i64;
7747                let input = self.arg_list_f32(&args, 1);
7748                let target = self.arg_list_f32(&args, 2);
7749                let lr = self.arg_num(&args, 3, 0.01)? as f32;
7750                return Ok(Value::Number(ai::nn_train(id, &input, &target, lr) as f64));
7751            },
7752            // nn_save(handle, path) → bool
7753            #[cfg(not(target_arch = "wasm32"))]
7754            "nn_save" | "保存网" | "網保存" | "신경망저장" | "บันทึกโครงข่าย" | "ذخیره_شبکه" | "احفظ_الشبكة" | "שמור_רשת" | "نیٹ_محفوظ_کرو" | "sauvegarder_réseau" | "netz_speichern" | "сохранить_сеть" =>
7755            {
7756                let id = self.arg_num(&args, 0, -1.0)? as i64;
7757                let path = self.arg_str(&args, 1, "model.lnn");
7758                return Ok(Value::Bool(ai::nn_save(id, &path)));
7759            },
7760            // nn_load(path) → handle (-1 on failure)
7761            #[cfg(not(target_arch = "wasm32"))]
7762            "nn_load" | "载入网" | "網読込" | "신경망불러오기" | "โหลดโครงข่าย" | "بارگذاری_شبکه" | "حمّل_الشبكة" | "טען_רשת" | "نیٹ_لوڈ" | "charger_réseau" | "netz_laden" | "загрузить_сеть" =>
7763            {
7764                let path = self.arg_str(&args, 0, "model.lnn");
7765                return Ok(Value::Number(ai::nn_load(&path) as f64));
7766            },
7767
7768            // ── game AI: behavior trees ──────────────────────────────────────
7769            // bt_build(dsl_string) → handle
7770            #[cfg(not(target_arch = "wasm32"))]
7771            "bt_build" | "建行为树" | "行動木構築" | "행동트리구성" | "สร้างต้นไม้พฤติกรรม" | "ساخت_درخت_رفتار" | "ابنِ_شجرة_السلوك" | "בנה_עץ_התנהגות" | "بی_ٹی_تعمیر" | "construire_arbre_comportement" | "verhaltensbaum_bauen" | "построить_дерево_поведения" =>
7772            {
7773                let spec = self.arg_str(&args, 0, "");
7774                return Ok(Value::Number(ai::bt_build(&spec) as f64));
7775            },
7776            // bt_set(handle, key, value) — set a blackboard fact
7777            #[cfg(not(target_arch = "wasm32"))]
7778            "bt_set" | "设事实" | "事実設定" | "사실설정" | "ตั้งข้อเท็จจริง" | "تنظیم_واقعیت" | "عيّن_حقيقة" | "קבע_עובדה" | "بی_ٹی_سیٹ" | "définir_arbre_comportement" | "verhaltensbaum_setzen" | "задать_дерево_поведения" =>
7779            {
7780                let id = self.arg_num(&args, 0, -1.0)? as i64;
7781                let key = self.arg_str(&args, 1, "");
7782                let val = self.arg_num(&args, 2, 0.0)? as f32;
7783                ai::bt_set(id, &key, val);
7784                return Ok(Value::Unit);
7785            },
7786            // bt_tick(handle) → chosen action name ("" if none)
7787            #[cfg(not(target_arch = "wasm32"))]
7788            "bt_tick" | "行为树滴答" | "行動木更新" | "행동트리틱" | "เดินต้นไม้พฤติกรรม" | "تیک_درخت_رفتار" | "نبضة_شجرة_السلوك" | "טיק_עץ_התנהגות" | "بی_ٹی_ٹک" | "tick_arbre_comportement" | "verhaltensbaum_tick" | "тик_дерева_поведения" =>
7789            {
7790                let id = self.arg_num(&args, 0, -1.0)? as i64;
7791                return Ok(Value::Str(ai::bt_tick(id)));
7792            },
7793            // bt_status(handle) → 0 fail / 1 success / 2 running
7794            #[cfg(not(target_arch = "wasm32"))]
7795            "bt_status" | "行为树状态" | "行動木状態" | "행동트리상태" | "สถานะต้นไม้พฤติกรรม" | "وضعیت_درخت_رفتار" | "حالة_شجرة_السلوك" | "סטטוס_עץ_התנהגות" | "بی_ٹی_حالت" | "statut_arbre_comportement" | "verhaltensbaum_status" | "статус_дерева_поведения" =>
7796            {
7797                let id = self.arg_num(&args, 0, -1.0)? as i64;
7798                return Ok(Value::Number(ai::bt_status(id) as f64));
7799            },
7800
7801            // ── game AI: miniature dialog LLM ────────────────────────────────
7802            // dialog_new([ctx, embed, hidden, seed]) → handle
7803            #[cfg(not(target_arch = "wasm32"))]
7804            "dialog_new" | "建对话模型" | "対話モデル作成" | "대화모델생성" | "สร้างโมเดลสนทนา" | "مدل_گفتگوی_جدید" | "نموذج_حوار_جديد" | "מודל_דיאלוג_חדש" | "نیا_مکالمہ_ماڈل" | "nouveau_dialogue" | "neuer_dialog" | "новый_диалог" =>
7805            {
7806                let ctx = self.arg_num(&args, 0, 3.0)?.max(1.0) as usize;
7807                let embed = self.arg_num(&args, 1, 32.0)?.max(1.0) as usize;
7808                let hidden = self.arg_num(&args, 2, 64.0)?.max(1.0) as usize;
7809                let seed = self.arg_num(&args, 3, 1.0)? as u64;
7810                return Ok(Value::Number(
7811                    ai::dialog_new(ctx, embed, hidden, seed) as f64
7812                ));
7813            },
7814            // dialog_learn(handle, text) — add one utterance to the corpus
7815            #[cfg(not(target_arch = "wasm32"))]
7816            "dialog_learn" | "对话学习" | "対話学習" | "대화학습" | "เรียนรู้สนทนา" | "یادگیری_گفتگو" | "تعلّم_الحوار" | "למד_דיאלוג" | "مکالمہ_سیکھو" | "apprendre_dialogue" | "dialog_lernen" | "обучить_диалог" =>
7817            {
7818                let id = self.arg_num(&args, 0, -1.0)? as i64;
7819                let text = self.arg_str(&args, 1, "");
7820                ai::dialog_learn(id, &text);
7821                return Ok(Value::Unit);
7822            },
7823            // dialog_load(handle, path) → lines added (-1 on error)
7824            #[cfg(not(target_arch = "wasm32"))]
7825            "dialog_load" | "对话载入" | "対話読込" | "대화불러오기" | "โหลดชุดสนทนา" | "بارگذاری_مجموعه_گفتگو" | "حمّل_مجموعة_الحوار" | "טען_מערך_דיאלוג" | "مکالمہ_مجموعہ_لوڈ" | "charger_dialogue" | "dialog_laden" | "загрузить_диалог" =>
7826            {
7827                let id = self.arg_num(&args, 0, -1.0)? as i64;
7828                let path = self.arg_str(&args, 1, "");
7829                return Ok(Value::Number(ai::dialog_load(id, &path) as f64));
7830            },
7831            // dialog_train(handle[, epochs, lr]) → loss
7832            #[cfg(not(target_arch = "wasm32"))]
7833            "dialog_train" | "对话训练" | "対話訓練" | "대화훈련" | "ฝึกสนทนา" | "آموزش_گفتگو" | "درّب_الحوار" | "אמן_דיאלוג" | "مکالمہ_ٹریننگ" | "entraîner_dialogue" | "dialog_trainieren" | "тренировать_диалог" =>
7834            {
7835                let id = self.arg_num(&args, 0, -1.0)? as i64;
7836                let epochs = self.arg_num(&args, 1, 20.0)?.max(1.0) as usize;
7837                let lr = self.arg_num(&args, 2, 0.1)? as f32;
7838                return Ok(Value::Number(ai::dialog_train(id, epochs, lr) as f64));
7839            },
7840            // dialog_say(handle, prompt[, max_tokens, temperature]) → reply text
7841            #[cfg(not(target_arch = "wasm32"))]
7842            "dialog_say" | "对话生成" | "対話生成" | "대화생성" | "พูดสนทนา" | "بگو" | "قل" | "אמור" | "کہو" | "dire_dialogue" | "dialog_sagen" | "сказать_диалог" =>
7843            {
7844                let id = self.arg_num(&args, 0, -1.0)? as i64;
7845                let prompt = self.arg_str(&args, 1, "");
7846                let max = self.arg_num(&args, 2, 24.0)?.max(1.0) as usize;
7847                let temp = self.arg_num(&args, 3, 0.8)? as f32;
7848                return Ok(Value::Str(ai::dialog_say(id, &prompt, max, temp)));
7849            },
7850            // dialog_save(handle, path) → bool
7851            #[cfg(not(target_arch = "wasm32"))]
7852            "dialog_save" | "对话存模" | "対話モデル保存" | "대화모델저장" | "บันทึกโมเดลสนทนา" | "ذخیره_مدل_گفتگو" | "احفظ_نموذج_الحوار" | "שמור_מודל_דיאלוג" | "مکالمہ_ماڈل_محفوظ" | "sauvegarder_dialogue" | "dialog_speichern" | "сохранить_диалог" =>
7853            {
7854                let id = self.arg_num(&args, 0, -1.0)? as i64;
7855                let path = self.arg_str(&args, 1, "model.llm");
7856                return Ok(Value::Bool(ai::dialog_save(id, &path)));
7857            },
7858            // dialog_load_model(path) → handle (-1 on failure)
7859            #[cfg(not(target_arch = "wasm32"))]
7860            "dialog_load_model"
7861            | "对话载模"
7862            | "対話モデル読込"
7863            | "대화모델불러오기"
7864            | "โหลดโมเดลสนทนา" | "بارگذاری_مدل_گفتگو" | "حمّل_نموذج_الحوار" | "טען_מודל_דיאלוג" | "مکالمہ_ماڈل_لوڈ" | "charger_modèle_dialogue" | "dialog_modell_laden" | "загрузить_модель_диалога" => {
7865                let path = self.arg_str(&args, 0, "model.llm");
7866                return Ok(Value::Number(ai::dialog_load_model(&path) as f64));
7867            },
7868
7869            // Decodes `application/x-www-form-urlencoded` text: '+' -> space,
7870            // '%XX' -> byte. Needed to read plain HTML `<form>` POST bodies.
7871            "url_decode" | "网址解码" => {
7872                let s = self.arg_str(&args, 0, "");
7873                let bytes = s.as_bytes();
7874                let mut out = Vec::with_capacity(bytes.len());
7875                let mut i = 0;
7876                while i < bytes.len() {
7877                    match bytes[i] {
7878                        b'+' => {
7879                            out.push(b' ');
7880                            i += 1;
7881                        },
7882                        b'%' if i + 2 < bytes.len() => {
7883                            let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or("");
7884                            match u8::from_str_radix(hex, 16) {
7885                                Ok(b) => {
7886                                    out.push(b);
7887                                    i += 3;
7888                                },
7889                                Err(_) => {
7890                                    out.push(bytes[i]);
7891                                    i += 1;
7892                                },
7893                            }
7894                        },
7895                        b => {
7896                            out.push(b);
7897                            i += 1;
7898                        },
7899                    }
7900                }
7901                return Ok(Value::Str(String::from_utf8_lossy(&out).into_owned()));
7902            },
7903            // Seconds since Unix epoch (float — sub-second precision). No ISO/date
7904            // formatting builtin exists yet; `.ling` code that wants a display
7905            // string currently just uses the raw number.
7906            "now_unix" | "现在时间" => {
7907                return Ok(Value::Number(now_secs()));
7908            },
7909            "file_exists" | "文件存在" => {
7910                #[cfg(target_arch = "wasm32")]
7911                return Ok(Value::Bool(false));
7912                #[cfg(not(target_arch = "wasm32"))]
7913                {
7914                    let path = self.arg_str(&args, 0, "").replace('\\', "/");
7915                    return Ok(Value::Bool(std::path::Path::new(&path).exists()));
7916                }
7917            },
7918            "write_file" | "เขียนไฟล์" | "نوشتن_فایل" | "اكتب_الملف" | "כתוב_קובץ" | "فائل_لکھو" => {
7919                #[cfg(target_arch = "wasm32")]
7920                return Ok(Value::Unit);
7921                #[cfg(not(target_arch = "wasm32"))]
7922                {
7923                    let path = self.arg_str(&args, 0, "").replace('\\', "/");
7924                    let content = self.arg_str(&args, 1, "");
7925                    if let Some(parent) = std::path::Path::new(&path).parent() {
7926                        if !parent.as_os_str().is_empty() {
7927                            let _ = std::fs::create_dir_all(parent);
7928                        }
7929                    }
7930                    std::fs::write(&path, content.as_bytes())
7931                        .map_err(|e| EvalErr::from(format!("write_file '{path}': {e}")))?;
7932                    return Ok(Value::Unit);
7933                }
7934            },
7935            "print_file" | "พิมพ์ไฟล์" | "چاپ_فایل" | "اطبع_الملف" | "הדפס_קובץ" | "فائل_چھاپو" => {
7936                let content = self.arg_str(&args, 0, "");
7937                print!("{content}");
7938                return Ok(Value::Unit);
7939            },
7940
7941            // ── CLI arguments ─────────────────────────────────────────────────
7942            "get_args" | "รับอาร์กิวเมนต์" | "دریافت_آرگومان‌ها" | "اجلب_المعاملات" | "קבל_ארגומנטים" | "دلائل_حاصل_کرو" => {
7943                let v: Vec<Value> = std::env::args().map(Value::Str).collect();
7944                return Ok(Value::List(Rc::new(v)));
7945            },
7946
7947            // ── Filesystem: directory walking, stat, content hashing (native) ──
7948            // These power headless batch tools (asset pipelines, indexers). Errors
7949            // degrade gracefully (empty list / 0 / "") so a walk never aborts on one
7950            // unreadable entry.
7951            #[cfg(not(target_arch = "wasm32"))]
7952            "list_dir" | "รายการไดเรกทอรี" | "فهرست_پوشه" | "اسرد_المجلد" | "רשום_תיקייה" | "فولڈر_فہرست" => {
7953                let path = self.arg_str(&args, 0, ".").replace('\\', "/");
7954                let mut paths: Vec<String> = Vec::new();
7955                if let Ok(rd) = std::fs::read_dir(&path) {
7956                    for e in rd.flatten() {
7957                        paths.push(e.path().to_string_lossy().replace('\\', "/"));
7958                    }
7959                }
7960                paths.sort();
7961                let out: Vec<Value> = paths.into_iter().map(Value::Str).collect();
7962                return Ok(Value::List(Rc::new(out)));
7963            },
7964            #[cfg(not(target_arch = "wasm32"))]
7965            "is_dir" | "เป็นไดเรกทอรี" | "آیا_پوشه_است" | "هل_مجلد" | "האם_תיקייה" | "کیا_فولڈر_ہے" => {
7966                let path = self.arg_str(&args, 0, "").replace('\\', "/");
7967                return Ok(Value::Bool(std::path::Path::new(&path).is_dir()));
7968            },
7969            #[cfg(not(target_arch = "wasm32"))]
7970            "is_file" | "เป็นไฟล์" | "آیا_فایل_است" | "هل_ملف" | "האם_קובץ" | "کیا_فائل_ہے" => {
7971                let path = self.arg_str(&args, 0, "").replace('\\', "/");
7972                return Ok(Value::Bool(std::path::Path::new(&path).is_file()));
7973            },
7974            #[cfg(not(target_arch = "wasm32"))]
7975            "path_name" | "ชื่อไฟล์" | "نام_مسیر" | "اسم_المسار" | "שם_נתיב" | "پاتھ_نام" => {
7976                let path = self.arg_str(&args, 0, "").replace('\\', "/");
7977                let name = std::path::Path::new(&path)
7978                    .file_name()
7979                    .map(|s| s.to_string_lossy().into_owned())
7980                    .unwrap_or_default();
7981                return Ok(Value::Str(name));
7982            },
7983            #[cfg(not(target_arch = "wasm32"))]
7984            "path_ext" | "นามสกุลไฟล์" | "پسوند_مسیر" | "امتداد_المسار" | "סיומת_נתיב" | "پاتھ_ایکسٹینشن" => {
7985                let path = self.arg_str(&args, 0, "").replace('\\', "/");
7986                let ext = std::path::Path::new(&path)
7987                    .extension()
7988                    .map(|s| s.to_string_lossy().to_lowercase())
7989                    .unwrap_or_default();
7990                return Ok(Value::Str(ext));
7991            },
7992            #[cfg(not(target_arch = "wasm32"))]
7993            "file_size" | "ขนาดไฟล์" | "اندازه_فایل" | "حجم_الملف" | "גודל_קובץ" | "فائل_سائز" => {
7994                let path = self.arg_str(&args, 0, "");
7995                let sz = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
7996                return Ok(Value::Number(sz as f64));
7997            },
7998            #[cfg(not(target_arch = "wasm32"))]
7999            "file_modified" | "เวลาที่แก้ไข" | "زمان_تغییر_فایل" | "وقت_تعديل_الملف" | "זמן_עדכון_קובץ" | "فائل_ترمیم_وقت" => {
8000                let path = self.arg_str(&args, 0, "");
8001                let secs = std::fs::metadata(&path)
8002                    .and_then(|m| m.modified())
8003                    .ok()
8004                    .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
8005                    .map(|d| d.as_secs_f64())
8006                    .unwrap_or(0.0);
8007                return Ok(Value::Number(secs));
8008            },
8009            #[cfg(not(target_arch = "wasm32"))]
8010            "file_created" | "เวลาที่สร้าง" | "زمان_ایجاد_فایل" | "وقت_إنشاء_الملف" | "זמן_יצירת_קובץ" | "فائل_تخلیق_وقت" => {
8011                let path = self.arg_str(&args, 0, "");
8012                let secs = std::fs::metadata(&path)
8013                    .ok()
8014                    .and_then(|m| m.created().or_else(|_| m.modified()).ok())
8015                    .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
8016                    .map(|d| d.as_secs_f64())
8017                    .unwrap_or(0.0);
8018                return Ok(Value::Number(secs));
8019            },
8020            #[cfg(not(target_arch = "wasm32"))]
8021            "make_dir" | "สร้างไดเรกทอรี" | "ساخت_پوشه" | "أنشئ_مجلدا" | "צור_תיקייה" | "فولڈر_بناؤ" => {
8022                let path = self.arg_str(&args, 0, "");
8023                return Ok(Value::Bool(std::fs::create_dir_all(&path).is_ok()));
8024            },
8025            // str_strip_prefix("Bearer x", "Bearer ") → "x" (unchanged if absent).
8026            "str_strip_prefix" | "ตัดคำนำหน้า" | "حذف_پیشوند" | "أزل_البادئة" | "הסר_קידומת" | "سابقہ_ہٹاؤ" => {
8027                let s = self.arg_str(&args, 0, "");
8028                let prefix = self.arg_str(&args, 1, "");
8029                return Ok(Value::Str(
8030                    s.strip_prefix(&prefix).unwrap_or(&s).to_string(),
8031                ));
8032            },
8033            // Classify a file by magic bytes: "gzip" | "zip" | "other" | "missing".
8034            // The build-verification gate: only real built archives may publish.
8035            #[cfg(not(target_arch = "wasm32"))]
8036            "file_magic" | "มายาไฟล์" | "امضای_فایل" | "توقيع_الملف" | "חתימת_קובץ" | "فائل_میجک" => {
8037                let path = self.arg_str(&args, 0, "");
8038                let kind = match std::fs::File::open(&path) {
8039                    Ok(mut f) => {
8040                        use std::io::Read;
8041                        let mut buf = [0u8; 4];
8042                        let n = f.read(&mut buf).unwrap_or(0);
8043                        if n >= 2 && buf[0] == 0x1f && buf[1] == 0x8b {
8044                            "gzip"
8045                        } else if n >= 4 && &buf[0..2] == b"PK" {
8046                            "zip"
8047                        } else {
8048                            "other"
8049                        }
8050                    },
8051                    Err(_) => "missing",
8052                };
8053                return Ok(Value::Str(kind.to_string()));
8054            },
8055            // Binary-safe file copy (backups): copy_file(src, dst) → bool.
8056            #[cfg(not(target_arch = "wasm32"))]
8057            "copy_file" | "คัดลอกไฟล์" | "کپی_فایل" | "انسخ_الملف" | "העתק_קובץ" | "فائل_کاپی" => {
8058                let src = self.arg_str(&args, 0, "");
8059                let dst = self.arg_str(&args, 1, "");
8060                if let Some(parent) = std::path::Path::new(&dst).parent() {
8061                    let _ = std::fs::create_dir_all(parent);
8062                }
8063                return Ok(Value::Bool(std::fs::copy(&src, &dst).is_ok()));
8064            },
8065            // ── Read a .tgz (gzip tarball): list file entries / read one file ──
8066            // Powers the GitHub-style "Code / Files" browser: tar_gz_list gives
8067            // the file tree, tar_gz_read pulls one file's text for the viewer.
8068            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8069            "tar_gz_list" | "รายการทาร์" | "فهرست_TAR_GZ" | "اسرد_TAR_GZ" | "רשום_TAR_GZ" | "TAR_GZ_فہرست" => {
8070                let path = self.arg_str(&args, 0, "");
8071                let mut names: Vec<String> = Vec::new();
8072                if let Ok(file) = std::fs::File::open(&path) {
8073                    let gz = flate2::read::GzDecoder::new(file);
8074                    let mut ar = tar::Archive::new(gz);
8075                    if let Ok(entries) = ar.entries() {
8076                        for entry in entries.flatten() {
8077                            if entry.header().entry_type().is_file() {
8078                                if let Ok(p) = entry.path() {
8079                                    names.push(
8080                                        p.to_string_lossy()
8081                                            .trim_start_matches("./")
8082                                            .replace('\\', "/"),
8083                                    );
8084                                }
8085                            }
8086                        }
8087                    }
8088                }
8089                names.sort();
8090                names.dedup();
8091                let out: Vec<Value> = names.into_iter().map(Value::Str).collect();
8092                return Ok(Value::List(Rc::new(out)));
8093            },
8094            // tar_gz_read(archive, entry) → that file's text (utf-8 lossy,
8095            // capped at 256 KiB). Only reads entries that exist in the archive,
8096            // so a caller can't traverse outside it. "" if not found/unreadable.
8097            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8098            "tar_gz_read" | "อ่านทาร์" | "خواندن_TAR_GZ" | "اقرأ_TAR_GZ" | "קרא_TAR_GZ" | "TAR_GZ_پڑھو" => {
8099                let path = self.arg_str(&args, 0, "");
8100                let want = self.arg_str(&args, 1, "");
8101                let want = want.trim_start_matches("./").replace('\\', "/");
8102                let mut content = String::new();
8103                if let Ok(file) = std::fs::File::open(&path) {
8104                    let gz = flate2::read::GzDecoder::new(file);
8105                    let mut ar = tar::Archive::new(gz);
8106                    if let Ok(entries) = ar.entries() {
8107                        for entry in entries.flatten() {
8108                            let mut entry = entry;
8109                            let name = match entry.path() {
8110                                Ok(p) => p
8111                                    .to_string_lossy()
8112                                    .trim_start_matches("./")
8113                                    .replace('\\', "/"),
8114                                Err(_) => continue,
8115                            };
8116                            if name == want {
8117                                use std::io::Read;
8118                                let mut buf = Vec::new();
8119                                let cap = 256 * 1024;
8120                                if entry.take(cap as u64 + 1).read_to_end(&mut buf).is_ok() {
8121                                    let slice = if buf.len() > cap { &buf[..cap] } else { &buf[..] };
8122                                    content = String::from_utf8_lossy(slice).into_owned();
8123                                }
8124                                break;
8125                            }
8126                        }
8127                    }
8128                }
8129                return Ok(Value::Str(content));
8130            },
8131            // ── TOTP (RFC 6238, HMAC-SHA1, 6 digits, 30s) for 2FA ──
8132            // Base32 secret compatible with Google Authenticator / Authy etc.
8133            // `base32_encode`/`totp_code`/`totp_check` only exist under this
8134            // same `feature = "web"` gate (see their definitions above) — a
8135            // build without it must skip these arms too, not just fail to
8136            // link; matches how `file_hash` etc. gate their own arms below.
8137            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8138            "totp_secret" | "โทเทนลับ" | "راز_TOTP" | "سر_TOTP" | "סוד_TOTP" | "TOTP_راز" => {
8139                let mut bytes = [0u8; 20];
8140                rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut bytes);
8141                return Ok(Value::Str(base32_encode(&bytes)));
8142            },
8143            // otpauth:// URI to paste into an authenticator app (or make a QR of).
8144            // No cfg gate needed — pure string formatting, no dependency on
8145            // the web-only TOTP helpers.
8146            "totp_uri" | "โทเทนยูอาร์ไอ" | "آدرس_TOTP" | "رابط_TOTP" | "כתובת_TOTP" | "TOTP_یو_آر_آئی" => {
8147                let secret = self.arg_str(&args, 0, "");
8148                let account = self.arg_str(&args, 1, "user");
8149                let issuer = self.arg_str(&args, 2, "lingfu");
8150                return Ok(Value::Str(format!(
8151                    "otpauth://totp/{issuer}:{account}?secret={secret}&issuer={issuer}&algorithm=SHA1&digits=6&period=30"
8152                )));
8153            },
8154            // Verify a 6-digit code against the secret, allowing ±1 time step.
8155            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8156            "totp_verify" | "โทเทนตรวจ" | "تایید_TOTP" | "تحقق_TOTP" | "אמת_TOTP" | "TOTP_تصدیق" => {
8157                let secret = self.arg_str(&args, 0, "");
8158                let code = self.arg_str(&args, 1, "");
8159                let ok = totp_check(&secret, code.trim());
8160                return Ok(Value::Bool(ok));
8161            },
8162            // The current valid code, for tests/tools.
8163            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8164            "totp_now" | "โทเทนตอนนี้" | "TOTP_اکنون" | "TOTP_الآن" | "TOTP_עכשיו" | "TOTP_ابھی" => {
8165                let secret = self.arg_str(&args, 0, "");
8166                let step = (crate::runtime::now_secs() as u64) / 30;
8167                return Ok(Value::Str(
8168                    totp_code(&secret, step).unwrap_or_default(),
8169                ));
8170            },
8171            // BLAKE3 hex of a file's bytes (binary-safe content fingerprint).
8172            #[cfg(not(target_arch = "wasm32"))]
8173            "file_hash" | "แฮชไฟล์" | "درهم_فایل" | "بصمة_الملف" | "גיבוב_קובץ" | "فائل_ہیش" => {
8174                let path = self.arg_str(&args, 0, "");
8175                match std::fs::read(&path) {
8176                    Ok(bytes) => {
8177                        return Ok(Value::Str(hex_encode(&ling_crypto::Blake3::hash(&bytes))))
8178                    },
8179                    Err(_) => return Ok(Value::Str(String::new())),
8180                }
8181            },
8182            // BLAKE3 hex of an arbitrary string (deterministic id/colour/role seed).
8183            "hash_hex" | "แฮชสตริง" | "درهم_هگزادسیمال" | "بصمة_سداسية" | "גיבוב_הקסדצימלי" | "ہیکس_ہیش" => {
8184                let s = self.arg_str(&args, 0, "");
8185                return Ok(Value::Str(hex_encode(&ling_crypto::Blake3::hash(
8186                    s.as_bytes(),
8187                ))));
8188            },
8189            // Read an environment variable, falling back to a default.
8190            #[cfg(not(target_arch = "wasm32"))]
8191            "env_get" | "รับตัวแปรแวดล้อม" | "دریافت_متغیر_محیطی" | "اجلب_متغير_البيئة" | "קבל_משתנה_סביבה" | "ماحولیاتی_متغیر_حاصل_کرو" => {
8192                let name = self.arg_str(&args, 0, "");
8193                let dflt = self.arg_str(&args, 1, "");
8194                return Ok(Value::Str(std::env::var(&name).unwrap_or(dflt)));
8195            },
8196
8197            // ── String utilities ──────────────────────────────────────────────
8198            // Parses a string to a number (0 on failure — degrades gracefully,
8199            // like the other filesystem/parsing builtins in this file).
8200            "to_number" | "转数字" => {
8201                let s = self.arg_str(&args, 0, "");
8202                return Ok(Value::Number(s.trim().parse().unwrap_or(0.0)));
8203            },
8204            // Plain SHA-256 hex — matches the browser's native SubtleCrypto
8205            // digest("SHA-256", ...), which is what proof-of-work mining uses
8206            // client-side (Web Crypto has no Blake3/SHA-3, so this is the one
8207            // hash both sides can compute natively and fast).
8208            "sha256_hex" | "SHA256哈希" => {
8209                use sha2::Digest;
8210                let s = self.arg_str(&args, 0, "");
8211                let mut h = sha2::Sha256::new();
8212                h.update(s.as_bytes());
8213                return Ok(Value::Str(hex_encode(&h.finalize())));
8214            },
8215            // Parses a hex string (no "0x" prefix) to a number — `to_number`
8216            // uses Rust's plain f64 parser, which doesn't understand hex.
8217            "hex_to_number" | "十六进制转数字" => {
8218                let s = self.arg_str(&args, 0, "");
8219                let v = u64::from_str_radix(s.trim(), 16).unwrap_or(0);
8220                return Ok(Value::Number(v as f64));
8221            },
8222            "split" | "str_split" | "แยก" | "جداسازی" | "قسّم" | "פצל" | "تقسیم_کرو" => {
8223                let s = self.arg_str(&args, 0, "");
8224                let sep = self.arg_str(&args, 1, "\n");
8225                let sep = if sep.is_empty() { "\n".into() } else { sep };
8226                let parts: Vec<Value> = s
8227                    .split(sep.as_str())
8228                    .map(|p| Value::Str(p.to_string()))
8229                    .collect();
8230                return Ok(Value::List(Rc::new(parts)));
8231            },
8232            "trim" | "str_trim" | "ตัดช่องว่าง" | "حذف_فاصله" | "اقتطع_الفراغات" | "חתוך_רווחים" | "خالی_جگہ_کاٹو" => {
8233                let s = self.arg_str(&args, 0, "");
8234                return Ok(Value::Str(s.trim().to_string()));
8235            },
8236            "starts_with" | "str_starts_with" | "เริ่มด้วย" | "شروع_می‌شود_با" | "يبدأ_بـ" | "מתחיל_ב" | "شروع_ہوتا_ہے" => {
8237                let s = self.arg_str(&args, 0, "");
8238                let prefix = self.arg_str(&args, 1, "");
8239                return Ok(Value::Bool(s.starts_with(prefix.as_str())));
8240            },
8241            "ends_with" | "str_ends_with" | "ลงท้ายด้วย" | "پایان_می‌یابد_با" | "ينتهي_بـ" | "מסתיים_ב" | "ختم_ہوتا_ہے" => {
8242                let s = self.arg_str(&args, 0, "");
8243                let suffix = self.arg_str(&args, 1, "");
8244                return Ok(Value::Bool(s.ends_with(suffix.as_str())));
8245            },
8246            "str_replace" | "แทนสตริง" | "جایگزینی_رشته" | "استبدل_النص" | "החלף_מחרוזת" | "اسٹرنگ_تبدیل" => {
8247                let s = self.arg_str(&args, 0, "");
8248                let from = self.arg_str(&args, 1, "");
8249                let to = self.arg_str(&args, 2, "");
8250                return Ok(Value::Str(s.replace(from.as_str(), to.as_str())));
8251            },
8252            "str_find" | "หาในสตริง" | "جستجوی_رشته" | "ابحث_في_النص" | "חפש_מחרוזת" | "اسٹرنگ_تلاش" => {
8253                let s = self.arg_str(&args, 0, "");
8254                let needle = self.arg_str(&args, 1, "");
8255                // Return char index (not byte index) for consistency with substr
8256                let pos = s
8257                    .find(needle.as_str())
8258                    .map(|byte_i| s[..byte_i].chars().count() as f64)
8259                    .unwrap_or(-1.0);
8260                return Ok(Value::Number(pos));
8261            },
8262            "substr" | "str_slice" | "ส่วนสตริง" | "زیررشته" | "جزء_النص" | "תת_מחרוזת" | "ذیلی_اسٹرنگ" => {
8263                let s = self.arg_str(&args, 0, "");
8264                let start = self.arg_num(&args, 1, 0.0)? as usize;
8265                let len = args
8266                    .get(2)
8267                    .map(|v| self.to_number(v).unwrap_or(999999.0) as usize)
8268                    .unwrap_or_else(|| s.chars().count().saturating_sub(start));
8269                let chars: Vec<char> = s.chars().collect();
8270                let end = (start + len).min(chars.len());
8271                let slice: String = chars.get(start..end).unwrap_or(&[]).iter().collect();
8272                return Ok(Value::Str(slice));
8273            },
8274            "to_str" | "str" | "num_str" | "แปลงสตริง" | "تبدیل_به_رشته" | "حوّل_لنص" | "המר_למחרוזת" | "اسٹرنگ_میں_بدلو" => {
8275                let v = args.into_iter().next().unwrap_or(Value::Unit);
8276                return Ok(Value::Str(v.to_string()));
8277            },
8278            "str_repeat" | "ทำซ้ำสตริง" | "تکرار_رشته" | "كرّر_النص" | "חזור_על_מחרוזת" | "اسٹرنگ_دہراؤ" => {
8279                let s = self.arg_str(&args, 0, "");
8280                let n = self.arg_num(&args, 1, 1.0)? as usize;
8281                return Ok(Value::Str(s.repeat(n)));
8282            },
8283            "str_upper" => {
8284                let s = self.arg_str(&args, 0, "");
8285                return Ok(Value::Str(s.to_uppercase()));
8286            },
8287            "str_lower" => {
8288                let s = self.arg_str(&args, 0, "");
8289                return Ok(Value::Str(s.to_lowercase()));
8290            },
8291            "str_len" | "len" | "ความยาว" | "长度" | "長さ" | "길이" | "طول_رشته" | "طول_النص" | "אורך_מחרוזת" | "اسٹرنگ_لمبائی" => {
8292                match args.first() {
8293                    Some(Value::Str(s)) => return Ok(Value::Number(s.chars().count() as f64)),
8294                    Some(Value::List(v)) => return Ok(Value::Number(v.len() as f64)),
8295                    _ => return Ok(Value::Number(0.0)),
8296                }
8297            },
8298
8299            // ── FNV-1a hash (deterministic, normalized 0.0–1.0) ──────────────
8300            "hash_str" | "แฮช" | "درهم_رشته" | "بصمة_نص" | "גיבוב_מחרוזת" | "اسٹرنگ_ہیش" => {
8301                let s = self.arg_str(&args, 0, "");
8302                let mut h: u64 = 14695981039346656037_u64;
8303                for b in s.bytes() {
8304                    h ^= b as u64;
8305                    h = h.wrapping_mul(1099511628211);
8306                }
8307                return Ok(Value::Number((h & 0xFFFFFF) as f64 / 16777215.0));
8308            },
8309            "hash_int" | "แฮชจำนวน" | "درهم_عدد" | "بصمة_عدد" | "גיבוב_מספר" | "نمبر_ہیش" => {
8310                let s = self.arg_str(&args, 0, "");
8311                let n = self.arg_num(&args, 1, 100.0)? as u64;
8312                let mut h: u64 = 14695981039346656037_u64;
8313                for b in s.bytes() {
8314                    h ^= b as u64;
8315                    h = h.wrapping_mul(1099511628211);
8316                }
8317                return Ok(Value::Number((h % n.max(1)) as f64));
8318            },
8319
8320            // ── List utilities ────────────────────────────────────────────────
8321            "list_new" | "รายการใหม่" | "新建列表" | "新規リスト" | "새목록" | "فهرست_جدید" | "قائمة_جديدة" | "רשימה_חדשה" | "نئی_فہرست" | "nouvelle_liste" | "neue_liste" | "новый_список" =>
8322            {
8323                return Ok(Value::List(Rc::new(Vec::new())));
8324            },
8325            "list_push" | "เพิ่มรายการ" | "列表添加" | "リスト追加" | "목록추가" | "افزودن_به_فهرست" | "أضف_للقائمة" | "הוסף_לרשימה" | "فہرست_میں_شامل_کرو" | "ajouter_liste" | "liste_anhängen" | "добавить_в_список" =>
8326            {
8327                let lst = args
8328                    .first()
8329                    .cloned()
8330                    .unwrap_or(Value::List(Rc::new(vec![])));
8331                let val = args.get(1).cloned().unwrap_or(Value::Unit);
8332                if let Value::List(mut v) = lst {
8333                    Rc::make_mut(&mut v).push(val);
8334                    return Ok(Value::List(v));
8335                }
8336                return Ok(Value::List(Rc::new(vec![val])));
8337            },
8338            "list_get" | "รับรายการ" | "取元素" | "要素取得" | "요소가져오기" | "دریافت_از_فهرست" | "اجلب_من_القائمة" | "קבל_מרשימה" | "فہرست_سے_حاصل_کرو" | "obtenir_liste" | "liste_abrufen" | "получить_из_списка" =>
8339            {
8340                // Borrow the list; clone only the element (was cloning the whole list).
8341                let i = self.arg_num(&args, 1, 0.0)? as usize;
8342                if let Some(Value::List(v)) = args.first() {
8343                    return Ok(v.get(i).cloned().unwrap_or(Value::Str(String::new())));
8344                }
8345                return Ok(Value::Str(String::new()));
8346            },
8347            // list_max(numbers, default) / list_min(numbers, default) — `default`
8348            // is returned for an empty list (there's no numeric identity element
8349            // to fall back to otherwise).
8350            "list_max" | "列表最大值" => {
8351                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8352                let default = self.arg_num(&args, 1, 0.0)?;
8353                if let Value::List(v) = lst {
8354                    let mut best = default;
8355                    let mut any = false;
8356                    for item in v.iter() {
8357                        if let Value::Number(n) = item {
8358                            if !any || *n > best {
8359                                best = *n;
8360                                any = true;
8361                            }
8362                        }
8363                    }
8364                    return Ok(Value::Number(best));
8365                }
8366                return Ok(Value::Number(default));
8367            },
8368            "list_min" | "列表最小值" => {
8369                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8370                let default = self.arg_num(&args, 1, 0.0)?;
8371                if let Value::List(v) = lst {
8372                    let mut best = default;
8373                    let mut any = false;
8374                    for item in v.iter() {
8375                        if let Value::Number(n) = item {
8376                            if !any || *n < best {
8377                                best = *n;
8378                                any = true;
8379                            }
8380                        }
8381                    }
8382                    return Ok(Value::Number(best));
8383                }
8384                return Ok(Value::Number(default));
8385            },
8386            // list_set(lst, idx, val) → new list with index replaced. Engine builtin
8387            // (O(n) one copy) to replace the O(n²) ling `ตั้งรายการ` that looped
8388            // list_push + list_get (each of which copied the whole list).
8389            "list_set" | "ตั้งรายการ" | "设元素" | "要素設定" | "요소설정" | "تنظیم_عنصر_فهرست" | "عيّن_عنصر_القائمة" | "קבע_איבר_רשימה" | "فہرست_سیٹ" =>
8390            {
8391                let idx = self.arg_num(&args, 1, 0.0)? as usize;
8392                let mut ai = args.into_iter();
8393                let lst = ai.next().unwrap_or(Value::List(Rc::new(vec![])));
8394                ai.next(); // skip idx
8395                let val = ai.next().unwrap_or(Value::Unit);
8396                if let Value::List(mut v) = lst {
8397                    if idx < v.len() {
8398                        Rc::make_mut(&mut v)[idx] = val;
8399                    }
8400                    return Ok(Value::List(v));
8401                }
8402                return Ok(Value::List(Rc::new(vec![])));
8403            },
8404            "list_join" | "join" | "รวมรายการ" | "连接" | "連結" | "연결" | "پیوستن_فهرست" | "اربط_القائمة" | "חבר_רשימה" | "فہرست_جوڑو" =>
8405            {
8406                let lst = args
8407                    .first()
8408                    .cloned()
8409                    .unwrap_or(Value::List(Rc::new(vec![])));
8410                let sep = args.get(1).map(|v| v.to_string()).unwrap_or_default();
8411                if let Value::List(v) = lst {
8412                    return Ok(Value::Str(
8413                        v.iter()
8414                            .map(|x| x.to_string())
8415                            .collect::<Vec<_>>()
8416                            .join(&sep),
8417                    ));
8418                }
8419                return Ok(Value::Str(String::new()));
8420            },
8421            // list_map/list_filter/list_find — take a closure. Necessary as real
8422            // builtins (not expressible in `.ling` itself): a bare-identifier call
8423            // `f(x)` where `f` is a local variable always resolves through
8424            // `call_named`, which only looks at top-level `fn` definitions by
8425            // design ("call-site locals are intentionally NOT visible to fns") —
8426            // so a closure held in a variable/parameter can't be invoked from
8427            // `.ling` source directly. These call it from the Rust side instead,
8428            // the same way `http_serve` already invokes route-handler closures.
8429            "list_map" | "映射列表" => {
8430                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8431                let f = args.get(1).cloned().unwrap_or(Value::Unit);
8432                if let Value::List(v) = lst {
8433                    let mut out = Vec::with_capacity(v.len());
8434                    for item in v.iter() {
8435                        out.push(self.call_value(f.clone(), vec![item.clone()])?);
8436                    }
8437                    return Ok(Value::List(Rc::new(out)));
8438                }
8439                return Ok(Value::List(Rc::new(vec![])));
8440            },
8441            "list_filter" | "过滤列表" => {
8442                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8443                let f = args.get(1).cloned().unwrap_or(Value::Unit);
8444                if let Value::List(v) = lst {
8445                    let mut out = Vec::new();
8446                    for item in v.iter() {
8447                        if matches!(self.call_value(f.clone(), vec![item.clone()])?, Value::Bool(true)) {
8448                            out.push(item.clone());
8449                        }
8450                    }
8451                    return Ok(Value::List(Rc::new(out)));
8452                }
8453                return Ok(Value::List(Rc::new(vec![])));
8454            },
8455            // First element for which `f` returns true, or Unit if none match.
8456            "list_find" | "查找列表" => {
8457                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8458                let f = args.get(1).cloned().unwrap_or(Value::Unit);
8459                if let Value::List(v) = lst {
8460                    for item in v.iter() {
8461                        if matches!(self.call_value(f.clone(), vec![item.clone()])?, Value::Bool(true)) {
8462                            return Ok(item.clone());
8463                        }
8464                    }
8465                }
8466                return Ok(Value::Unit);
8467            },
8468            // blob_f32("<deflate+base64>") / blob_i32(...) — decode an embedded,
8469            // losslessly-compressed numeric blob into a list. Produced by
8470            // `ling convert`; lets converted assets carry geometry/PCM/etc. compactly.
8471            #[cfg(not(target_arch = "wasm32"))]
8472            "blob_f32" | "blob_i32" => {
8473                let s = self.arg_str(&args, 0, "");
8474                let is_i32 = name == "blob_i32";
8475                match decode_blob(&s) {
8476                    Ok(bytes) => {
8477                        let mut out = Vec::with_capacity(bytes.len() / 4);
8478                        for ch in bytes.chunks_exact(4) {
8479                            let arr = [ch[0], ch[1], ch[2], ch[3]];
8480                            let n = if is_i32 {
8481                                i32::from_le_bytes(arr) as f64
8482                            } else {
8483                                f32::from_le_bytes(arr) as f64
8484                            };
8485                            out.push(Value::Number(n));
8486                        }
8487                        return Ok(Value::List(Rc::new(out)));
8488                    },
8489                    Err(e) => {
8490                        eprintln!("blob decode failed: {e}");
8491                        return Ok(Value::List(Rc::new(vec![])));
8492                    },
8493                }
8494            },
8495
8496            // ══════════════════════════════════════════════════════════════════
8497            // SVG EXPORT  (svg_begin / svg_rect / svg_circle / svg_line /
8498            //              svg_polyline / svg_text / svg_end / hsl_color)
8499            // Chinese aliases: 开始SVG 结束SVG SVG矩形 SVG圆形 SVG线段 SVG折线 SVG文本 HSL颜色
8500            // Thai aliases:    เริ่มSVG จบSVG SVGสี่เหลี่ยม SVGวงกลม SVGเส้น SVGเส้นหัก SVGข้อความ สีHSL
8501            // ══════════════════════════════════════════════════════════════════
8502            "svg_begin" | "开始SVG" | "เริ่มSVG" | "شروع_SVG" | "ابدأ_SVG" | "התחל_SVG" | "SVG_شروع" | "commencer_svg" | "svg_beginnen" | "начать_svg" => {
8503                let path = self.arg_str(&args, 0, "output.svg");
8504                let width = self.arg_num(&args, 1, 800.0)?;
8505                let height = self.arg_num(&args, 2, 600.0)?;
8506                *self.svg.borrow_mut() = Some(SvgWriter::new(path, width, height));
8507                return Ok(Value::Unit);
8508            },
8509
8510            "svg_rect" | "SVG矩形" | "SVGสี่เหลี่ยม" | "مستطیل_SVG" | "مستطيل_SVG" | "מלבן_SVG" | "SVG_مستطیل" | "rectangle_svg" | "svg_rechteck" | "прямоугольник_svg" => {
8511                let x = self.arg_num(&args, 0, 0.0)?;
8512                let y = self.arg_num(&args, 1, 0.0)?;
8513                let w = self.arg_num(&args, 2, 10.0)?;
8514                let h = self.arg_num(&args, 3, 10.0)?;
8515                let fill = self.arg_str(&args, 4, "#ffffff");
8516                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8517                    svg.elements.push(format!(
8518                        "<rect x=\"{x:.1}\" y=\"{y:.1}\" width=\"{w:.1}\" \
8519                         height=\"{h:.1}\" fill=\"{fill}\"/>"
8520                    ));
8521                }
8522                return Ok(Value::Unit);
8523            },
8524
8525            "svg_circle" | "SVG圆形" | "SVGวงกลม" | "دایره_SVG" | "دائرة_SVG" | "עיגול_SVG" | "SVG_دائرہ" | "cercle_svg" | "svg_kreis" | "круг_svg" => {
8526                let cx = self.arg_num(&args, 0, 0.0)?;
8527                let cy = self.arg_num(&args, 1, 0.0)?;
8528                let r = self.arg_num(&args, 2, 5.0)?;
8529                let fill = self.arg_str(&args, 3, "#ffffff");
8530                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8531                    svg.elements.push(format!(
8532                        "<circle cx=\"{cx:.1}\" cy=\"{cy:.1}\" r=\"{r:.1}\" fill=\"{fill}\"/>"
8533                    ));
8534                }
8535                return Ok(Value::Unit);
8536            },
8537
8538            "svg_line" | "SVG线段" | "SVGเส้น" | "خط_SVG" | "קו_SVG" | "SVG_لکیر" | "ligne_svg" | "svg_linie" | "линия_svg" => {
8539                let x1 = self.arg_num(&args, 0, 0.0)?;
8540                let y1 = self.arg_num(&args, 1, 0.0)?;
8541                let x2 = self.arg_num(&args, 2, 0.0)?;
8542                let y2 = self.arg_num(&args, 3, 0.0)?;
8543                let stroke = self.arg_str(&args, 4, "#ffffff");
8544                let sw = self.arg_num(&args, 5, 1.0)?;
8545                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8546                    svg.elements.push(format!(
8547                        "<line x1=\"{x1:.1}\" y1=\"{y1:.1}\" x2=\"{x2:.1}\" y2=\"{y2:.1}\" \
8548                         stroke=\"{stroke}\" stroke-width=\"{sw:.1}\"/>"
8549                    ));
8550                }
8551                return Ok(Value::Unit);
8552            },
8553
8554            "svg_polyline" | "SVG折线" | "SVGเส้นหัก" | "چندخطی_SVG" | "خط_متعدد_SVG" | "קו_שבור_SVG" | "SVG_پولی_لائن" | "polyligne_svg" | "svg_polylinie" | "ломаная_svg" => {
8555                let pts = self.arg_str(&args, 0, "");
8556                let stroke = self.arg_str(&args, 1, "#ffffff");
8557                let sw = self.arg_num(&args, 2, 1.0)?;
8558                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8559                    svg.elements.push(format!(
8560                        "<polyline points=\"{pts}\" fill=\"none\" \
8561                         stroke=\"{stroke}\" stroke-width=\"{sw:.1}\"/>"
8562                    ));
8563                }
8564                return Ok(Value::Unit);
8565            },
8566
8567            "svg_text" | "SVG文本" | "SVGข้อความ" | "متن_SVG" | "نص_SVG" | "טקסט_SVG" | "SVG_متن" | "texte_svg" | "текст_svg" => {
8568                let x = self.arg_num(&args, 0, 0.0)?;
8569                let y = self.arg_num(&args, 1, 0.0)?;
8570                let text = self.arg_str(&args, 2, "");
8571                let fill = self.arg_str(&args, 3, "#ffffff");
8572                let size = self.arg_num(&args, 4, 12.0)?;
8573                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8574                    let safe = text
8575                        .replace('&', "&amp;")
8576                        .replace('<', "&lt;")
8577                        .replace('>', "&gt;");
8578                    svg.elements.push(format!(
8579                        "<text x=\"{x:.1}\" y=\"{y:.1}\" fill=\"{fill}\" \
8580                         font-family=\"monospace\" font-size=\"{size:.0}\">{safe}</text>"
8581                    ));
8582                }
8583                return Ok(Value::Unit);
8584            },
8585
8586            "svg_end" | "结束SVG" | "จบSVG" | "پایان_SVG" | "أنهِ_SVG" | "סיים_SVG" | "SVG_ختم" | "terminer_svg" | "svg_beenden" | "закончить_svg" => {
8587                {
8588                    let borrow = self.svg.borrow();
8589                    if let Some(svg) = borrow.as_ref() {
8590                        svg.save()
8591                            .map_err(|e| EvalErr::from(format!("svg_end: {e}")))?;
8592                    }
8593                }
8594                *self.svg.borrow_mut() = None;
8595                return Ok(Value::Unit);
8596            },
8597
8598            "hsl_color" | "HSL颜色" | "สีHSL" | "رنگ_HSL" | "لون_HSL" | "צבע_HSL" | "HSL_رنگ" | "couleur_hsl" | "hsl_farbe" | "цвет_hsl" => {
8599                let h = self.arg_num(&args, 0, 0.0)?;
8600                let s = self.arg_num(&args, 1, 70.0)?;
8601                let l = self.arg_num(&args, 2, 50.0)?;
8602                return Ok(Value::Str(hsl_to_hex(h, s, l)));
8603            },
8604
8605            // ══════════════════════════════════════════════════════════════════
8606            // FFT / AUDIO ANALYSIS BUILTINS  (native only)
8607            // ══════════════════════════════════════════════════════════════════
8608
8609            // fft_push(samples_list) — feed raw audio samples and run FFT
8610            #[cfg(not(target_arch = "wasm32"))]
8611            "fft_push" | "วิเคราะห์เสียง" | "频谱输入" | "FFT入力" | "FFT입력" | "ورودی_FFT" | "أدخل_FFT" | "הזנת_FFT" | "FFT_ان_پٹ" | "fft_entrée" | "fft_eingabe" | "fft_вход" =>
8612            {
8613                if let Some(Value::List(v)) = args.first() {
8614                    let samples: Vec<f32> = v
8615                        .iter()
8616                        .filter_map(|x| {
8617                            if let Value::Number(n) = x {
8618                                Some(*n as f32)
8619                            } else {
8620                                None
8621                            }
8622                        })
8623                        .collect();
8624                    self.fft.borrow_mut().push_samples(&samples);
8625                }
8626                return Ok(Value::Unit);
8627            },
8628
8629            // fft_bands(n) → list of n log-spaced magnitude bands (0..1)
8630            #[cfg(not(target_arch = "wasm32"))]
8631            "fft_bands" | "แถบความถี่" | "频段" | "周波数帯" | "주파수대" | "باندهای_FFT" | "نطاقات_FFT" | "פסי_FFT" | "FFT_بینڈز" | "fft_bandes" | "fft_bänder" | "fft_полосы" =>
8632            {
8633                let n = self.arg_num(&args, 0, 32.0)? as usize;
8634                let bands = self.fft.borrow().freq_bands(n);
8635                *self.fft_bands_cache.borrow_mut() = bands.clone();
8636                return Ok(Value::List(Rc::new(
8637                    bands.into_iter().map(|v| Value::Number(v as f64)).collect(),
8638                )));
8639            },
8640
8641            // fft_beat() → bool
8642            #[cfg(not(target_arch = "wasm32"))]
8643            "fft_beat" | "จังหวะเสียง" | "节拍检测" | "ビート検出" | "비트" | "ضرب_FFT" | "نبضة_FFT" | "פעימת_FFT" | "FFT_دھڑکن" | "fft_battement" | "fft_takt" | "fft_удар" =>
8644            {
8645                return Ok(Value::Bool(self.fft.borrow().is_beat()));
8646            },
8647
8648            // fft_beat_ratio() → f64  (1.0 = at threshold, >1 = strong beat)
8649            #[cfg(not(target_arch = "wasm32"))]
8650            "fft_beat_ratio" | "อัตราจังหวะ" | "节拍比" | "ビート比" | "비트비율" | "نسبت_ضرب_FFT" | "نسبة_نبضة_FFT" | "יחס_פעימת_FFT" | "FFT_بیٹ_تناسب" =>
8651            {
8652                return Ok(Value::Number(self.fft.borrow().beat_ratio() as f64));
8653            },
8654
8655            // fft_rms() → f64
8656            #[cfg(not(target_arch = "wasm32"))]
8657            "fft_rms" | "ระดับRMS" | "均方根" | "二乗平均" | "RMS레벨" | "RMS_صدا" | "جذر_متوسط_مربع_FFT" | "RMS_של_FFT" | "FFT_RMS" => {
8658                return Ok(Value::Number(self.fft.borrow().rms() as f64));
8659            },
8660
8661            // fft_dominant_freq() → f64  in Hz
8662            #[cfg(not(target_arch = "wasm32"))]
8663            "fft_dominant_freq" | "ความถี่หลัก" | "主频" | "主要周波数" | "주파수" | "فرکانس_غالب" | "التردد_السائد" | "תדר_דומיננטי" | "غالب_فریکوئنسی" | "fft_fréquence_dominante" | "fft_dominante_frequenz" | "fft_доминирующая_частота" =>
8664            {
8665                return Ok(Value::Number(self.fft.borrow().dominant_freq() as f64));
8666            },
8667
8668            // ── wasm32 stubs: fft builtins are no-ops on web ───────────────
8669            #[cfg(target_arch = "wasm32")]
8670            "fft_push" | "วิเคราะห์เสียง" | "频谱输入" | "FFT入力" | "FFT입력" | "ورودی_FFT" | "أدخل_FFT" | "הזנת_FFT" | "FFT_ان_پٹ" | "fft_entrée" | "fft_eingabe" | "fft_вход" =>
8671            {
8672                return Ok(Value::Unit);
8673            },
8674            #[cfg(target_arch = "wasm32")]
8675            "fft_bands" | "แถบความถี่" | "频段" | "周波数帯" | "주파수대" | "باندهای_FFT" | "نطاقات_FFT" | "פסי_FFT" | "FFT_بینڈز" | "fft_bandes" | "fft_bänder" | "fft_полосы" =>
8676            {
8677                let n = self.arg_num(&args, 0, 32.0)? as usize;
8678                return Ok(Value::List(vec![Value::Number(0.0); n].into()));
8679            },
8680            #[cfg(target_arch = "wasm32")]
8681            "fft_beat" | "จังหวะเสียง" | "节拍检测" | "ビート検出" | "비트" | "ضرب_FFT" | "نبضة_FFT" | "פעימת_FFT" | "FFT_دھڑکن" | "fft_battement" | "fft_takt" | "fft_удар" =>
8682            {
8683                return Ok(Value::Bool(false));
8684            },
8685            #[cfg(target_arch = "wasm32")]
8686            "fft_beat_ratio" | "อัตราจังหวะ" | "节拍比" | "ビート比" | "비트비율" | "نسبت_ضرب_FFT" | "نسبة_نبضة_FFT" | "יחס_פעימת_FFT" | "FFT_بیٹ_تناسب" =>
8687            {
8688                return Ok(Value::Number(1.0));
8689            },
8690            #[cfg(target_arch = "wasm32")]
8691            "fft_rms" | "ระดับRMS" | "均方根" | "二乗平均" | "RMS레벨" | "RMS_صدا" | "جذر_متوسط_مربع_FFT" | "RMS_של_FFT" | "FFT_RMS" => {
8692                return Ok(Value::Number(0.0));
8693            },
8694            #[cfg(target_arch = "wasm32")]
8695            "fft_dominant_freq" | "ความถี่หลัก" | "主频" | "主要周波数" | "주파수" | "فرکانس_غالب" | "التردد_السائد" | "תדר_דומיננטי" | "غالب_فریکوئنسی" | "fft_fréquence_dominante" | "fft_dominante_frequenz" | "fft_доминирующая_частота" =>
8696            {
8697                return Ok(Value::Number(0.0));
8698            },
8699
8700            // ══════════════════════════════════════════════════════════════════
8701            // PROCEDURAL TEXTURE BLIT BUILTINS  (screen-space)
8702            // All: name(dst_x, dst_y, width, height, ...params, palette)
8703            // palette: "rainbow" | "fire" | "ocean" | "psychedelic" | "neon" | "forest"
8704            // ══════════════════════════════════════════════════════════════════
8705
8706            // tex_checkerboard(x, y, w, h, tiles, r1,g1,b1, r2,g2,b2)
8707            "tex_checkerboard" | "ลายตารางหมากรุก" | "بافت_شطرنجی" | "نسيج_رقعة_الشطرنج" | "מרקם_שחמט" | "شطرنج_ٹیکسچر" => {
8708                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8709                let tiles = self.arg_num(&args, 4, 8.0)? as u32;
8710                let (r1, g1, b1) = (
8711                    self.arg_num(&args, 5, 255.)? as u32,
8712                    self.arg_num(&args, 6, 255.)? as u32,
8713                    self.arg_num(&args, 7, 255.)? as u32,
8714                );
8715                let (r2, g2, b2) = (
8716                    self.arg_num(&args, 8, 0.)? as u32,
8717                    self.arg_num(&args, 9, 0.)? as u32,
8718                    self.arg_num(&args, 10, 0.)? as u32,
8719                );
8720                let c1 = (r1 << 16) | (g1 << 8) | b1;
8721                let c2 = (r2 << 16) | (g2 << 8) | b2;
8722                let mut gfx = self.gfx.borrow_mut();
8723                let (bw, bh) = (gfx.width, gfx.height);
8724                for row in 0..th {
8725                    for col in 0..tw {
8726                        let cx = col as u32 * tiles / tw as u32;
8727                        let cy = row as u32 * tiles / th as u32;
8728                        let (dx, dy) = (tx + col, ty + row);
8729                        if dx < bw && dy < bh {
8730                            gfx.buffer[dy * bw + dx] = if (cx + cy) % 2 == 0 { c1 } else { c2 };
8731                        }
8732                    }
8733                }
8734                return Ok(Value::Unit);
8735            },
8736
8737            // tex_gradient(x, y, w, h, angle_deg, r1,g1,b1, r2,g2,b2)
8738            "tex_gradient" | "ลายไล่สี" | "بافت_گرادیان" | "نسيج_متدرج" | "מרקם_גרדיאנט" | "گریڈینٹ_ٹیکسچر" => {
8739                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8740                let angle = self.arg_num(&args, 4, 0.0)? as f32;
8741                let (r1, g1, b1) = (
8742                    self.arg_num(&args, 5, 0.)? as f32 / 255.,
8743                    self.arg_num(&args, 6, 0.)? as f32 / 255.,
8744                    self.arg_num(&args, 7, 0.)? as f32 / 255.,
8745                );
8746                let (r2, g2, b2) = (
8747                    self.arg_num(&args, 8, 255.)? as f32 / 255.,
8748                    self.arg_num(&args, 9, 255.)? as f32 / 255.,
8749                    self.arg_num(&args, 10, 255.)? as f32 / 255.,
8750                );
8751                let (ca, sa) = (angle.to_radians().cos(), angle.to_radians().sin());
8752                let mut gfx = self.gfx.borrow_mut();
8753                let (bw, bh) = (gfx.width, gfx.height);
8754                for row in 0..th {
8755                    for col in 0..tw {
8756                        let nx = col as f32 / tw as f32 - 0.5;
8757                        let ny = row as f32 / th as f32 - 0.5;
8758                        let t = ((nx * ca + ny * sa + 0.707) / 1.414).clamp(0., 1.);
8759                        let (dx, dy) = (tx + col, ty + row);
8760                        if dx < bw && dy < bh {
8761                            gfx.buffer[dy * bw + dx] =
8762                                tex_rgb(r1 + (r2 - r1) * t, g1 + (g2 - g1) * t, b1 + (b2 - b1) * t);
8763                        }
8764                    }
8765                }
8766                return Ok(Value::Unit);
8767            },
8768
8769            // tex_noise(x, y, w, h, scale, octaves, seed, palette)
8770            "tex_noise" | "ลายนอยส์" | "بافت_نویز" | "نسيج_ضجيج" | "מרקם_רעש" | "نوائز_ٹیکسچر" => {
8771                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8772                let scale = self.arg_num(&args, 4, 4.0)? as f32;
8773                let octaves = self.arg_num(&args, 5, 4.0)? as u32;
8774                let seed = self.arg_num(&args, 6, 0.0)? as u32;
8775                let palette = self.arg_str(&args, 7, "rainbow");
8776                let mut gfx = self.gfx.borrow_mut();
8777                let (bw, bh) = (gfx.width, gfx.height);
8778                for row in 0..th {
8779                    for col in 0..tw {
8780                        let v = tex_fbm(
8781                            col as f32 * scale / tw as f32,
8782                            row as f32 * scale / th as f32,
8783                            octaves,
8784                            seed,
8785                        );
8786                        let [r, g, b] = tex_palette(&palette, v);
8787                        let (dx, dy) = (tx + col, ty + row);
8788                        if dx < bw && dy < bh {
8789                            gfx.buffer[dy * bw + dx] = tex_rgb(r, g, b);
8790                        }
8791                    }
8792                }
8793                return Ok(Value::Unit);
8794            },
8795
8796            // tex_freq_map(x, y, w, h, time, speed, palette)
8797            // Uses bands written by the last fft_bands() call.
8798            "tex_freq_map" | "ลายความถี่" | "نقشه_فرکانس_بافت" | "خريطة_تردد_النسيج" | "מפת_תדר_מרקם" | "فریکوئنسی_میپ_ٹیکسچر" => {
8799                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8800                let time = self.arg_num(&args, 4, 0.0)? as f32;
8801                let speed = self.arg_num(&args, 5, 0.3)? as f32;
8802                let palette = self.arg_str(&args, 6, "rainbow");
8803                let bands: Vec<f32> = {
8804                    let c = self.fft_bands_cache.borrow();
8805                    if c.is_empty() {
8806                        vec![0.0; 32]
8807                    } else {
8808                        c.clone()
8809                    }
8810                };
8811                let n = bands.len().max(1);
8812                let mut gfx = self.gfx.borrow_mut();
8813                let (bw, bh) = (gfx.width, gfx.height);
8814                for row in 0..th {
8815                    for col in 0..tw {
8816                        let band_idx = (col * n / tw.max(1)).min(n - 1);
8817                        let mag = bands[band_idx].clamp(0., 1.);
8818                        let fill_y = (mag * th as f32) as usize;
8819                        if row >= th.saturating_sub(fill_y) {
8820                            let t = (col as f32 / tw as f32 + time * speed) % 1.0;
8821                            let [r, g, b] = tex_palette(&palette, t);
8822                            let bright = mag * (1.0 - row as f32 / th as f32 * 0.5);
8823                            let (dx, dy) = (tx + col, ty + row);
8824                            if dx < bw && dy < bh {
8825                                gfx.buffer[dy * bw + dx] =
8826                                    tex_rgb(r * bright, g * bright, b * bright);
8827                            }
8828                        }
8829                    }
8830                }
8831                return Ok(Value::Unit);
8832            },
8833
8834            // tex_spiral(x, y, w, h, freq, bands, time, palette)
8835            "tex_spiral" | "ลายเกลียวหมุน" | "بافت_مارپیچ" | "نسيج_حلزوني" | "מרקם_ספירלה" | "سرپیچ_ٹیکسچر" => {
8836                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8837                let freq = self.arg_num(&args, 4, 5.0)? as f32;
8838                let n_bands = self.arg_num(&args, 5, 8.0)? as f32;
8839                let time = self.arg_num(&args, 6, 0.0)? as f32;
8840                let palette = self.arg_str(&args, 7, "rainbow");
8841                let mut gfx = self.gfx.borrow_mut();
8842                let (bw, bh) = (gfx.width, gfx.height);
8843                for row in 0..th {
8844                    for col in 0..tw {
8845                        let nx = col as f32 / tw as f32 - 0.5;
8846                        let ny = row as f32 / th as f32 - 0.5;
8847                        let r = (nx * nx + ny * ny).sqrt();
8848                        let theta = ny.atan2(nx);
8849                        let t = ((r * freq - theta / std::f32::consts::TAU + time * 0.5) * n_bands
8850                            % 1.0)
8851                            .abs();
8852                        let [cr, cg, cb] = tex_palette(&palette, t);
8853                        let (dx, dy) = (tx + col, ty + row);
8854                        if dx < bw && dy < bh {
8855                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
8856                        }
8857                    }
8858                }
8859                return Ok(Value::Unit);
8860            },
8861
8862            // tex_ripple(x, y, w, h, freq, cx, cy, time, palette)
8863            "tex_ripple" | "ลายระลอก" | "بافت_موج" | "نسيج_تموج" | "מרקם_אדווה" | "ریپل_ٹیکسچر" => {
8864                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8865                let freq = self.arg_num(&args, 4, 10.0)? as f32;
8866                let rcx = self.arg_num(&args, 5, 0.5)? as f32;
8867                let rcy = self.arg_num(&args, 6, 0.5)? as f32;
8868                let time = self.arg_num(&args, 7, 0.0)? as f32;
8869                let palette = self.arg_str(&args, 8, "ocean");
8870                let mut gfx = self.gfx.borrow_mut();
8871                let (bw, bh) = (gfx.width, gfx.height);
8872                for row in 0..th {
8873                    for col in 0..tw {
8874                        let nx = col as f32 / tw as f32 - rcx;
8875                        let ny = row as f32 / th as f32 - rcy;
8876                        let r = (nx * nx + ny * ny).sqrt();
8877                        let t = ((r * freq - time) % 1.0).abs();
8878                        let [cr, cg, cb] = tex_palette(&palette, t);
8879                        let (dx, dy) = (tx + col, ty + row);
8880                        if dx < bw && dy < bh {
8881                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
8882                        }
8883                    }
8884                }
8885                return Ok(Value::Unit);
8886            },
8887
8888            // tex_mandelbrot(x, y, w, h, zoom, cx, cy, max_iter, palette)
8889            "tex_mandelbrot" | "ลายแมนเดลบรอต" | "بافت_ماندلبرو" | "نسيج_مانديلبروت" | "מרקם_מנדלברוט" | "مینڈل_بروٹ_ٹیکسچر" => {
8890                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8891                let zoom = self.arg_num(&args, 4, 1.0)?;
8892                let mcx = self.arg_num(&args, 5, -0.5)?;
8893                let mcy = self.arg_num(&args, 6, 0.0)?;
8894                let max_iter = self.arg_num(&args, 7, 64.0)? as u32;
8895                let palette = self.arg_str(&args, 8, "psychedelic");
8896                let mut gfx = self.gfx.borrow_mut();
8897                let (bw, bh) = (gfx.width, gfx.height);
8898                for row in 0..th {
8899                    for col in 0..tw {
8900                        let zx0 = (col as f64 / tw as f64 - 0.5) / zoom + mcx;
8901                        let zy0 = (row as f64 / th as f64 - 0.5) / zoom + mcy;
8902                        let mut x = 0.0f64;
8903                        let mut y = 0.0f64;
8904                        let mut i = 0u32;
8905                        while i < max_iter && x * x + y * y < 4.0 {
8906                            let t = x * x - y * y + zx0;
8907                            y = 2.0 * x * y + zy0;
8908                            x = t;
8909                            i += 1;
8910                        }
8911                        let t = if i == max_iter {
8912                            0.0f32
8913                        } else {
8914                            (i as f32
8915                                - (x as f32 * x as f32 + y as f32 * y as f32).ln().ln()
8916                                    / 2.0f32.ln())
8917                                / max_iter as f32
8918                        };
8919                        let [cr, cg, cb] = tex_palette(&palette, t.clamp(0., 1.));
8920                        let (dx, dy) = (tx + col, ty + row);
8921                        if dx < bw && dy < bh {
8922                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
8923                        }
8924                    }
8925                }
8926                return Ok(Value::Unit);
8927            },
8928
8929            // tex_julia(x, y, w, h, c_re, c_im, max_iter, palette)
8930            "tex_julia" | "ลายจูเลีย" | "بافت_ژولیا" | "نسيج_جوليا" | "מרקם_ג'וליה" | "جولیا_ٹیکسچر" => {
8931                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8932                let c_re = self.arg_num(&args, 4, -0.7)?;
8933                let c_im = self.arg_num(&args, 5, 0.27)?;
8934                let max_iter = self.arg_num(&args, 6, 64.0)? as u32;
8935                let palette = self.arg_str(&args, 7, "neon");
8936                let mut gfx = self.gfx.borrow_mut();
8937                let (bw, bh) = (gfx.width, gfx.height);
8938                for row in 0..th {
8939                    for col in 0..tw {
8940                        let mut zx = (col as f64 / tw as f64 - 0.5) * 3.5;
8941                        let mut zy = (row as f64 / th as f64 - 0.5) * 3.5;
8942                        let mut i = 0u32;
8943                        while i < max_iter && zx * zx + zy * zy < 4.0 {
8944                            let t = zx * zx - zy * zy + c_re;
8945                            zy = 2.0 * zx * zy + c_im;
8946                            zx = t;
8947                            i += 1;
8948                        }
8949                        let t = i as f32 / max_iter as f32;
8950                        let [cr, cg, cb] = tex_palette(&palette, t);
8951                        let (dx, dy) = (tx + col, ty + row);
8952                        if dx < bw && dy < bh {
8953                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
8954                        }
8955                    }
8956                }
8957                return Ok(Value::Unit);
8958            },
8959
8960            // tex_voronoi(x, y, w, h, cells, seed, palette)
8961            "tex_voronoi" | "ลายโวโรนอย" | "بافت_ورونوی" | "نسيج_فورونوي" | "מרקם_וורונוי" | "ورونوئی_ٹیکسچر" => {
8962                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8963                let cells = self.arg_num(&args, 4, 16.0)? as u32;
8964                let seed = self.arg_num(&args, 5, 42.0)? as u32;
8965                let palette = self.arg_str(&args, 6, "rainbow");
8966                let pts: Vec<[f32; 2]> = (0..cells)
8967                    .map(|i| {
8968                        [
8969                            tex_hash(i as i32, 0, seed),
8970                            tex_hash(i as i32, 1, seed + 999),
8971                        ]
8972                    })
8973                    .collect();
8974                let mut gfx = self.gfx.borrow_mut();
8975                let (bw, bh) = (gfx.width, gfx.height);
8976                for row in 0..th {
8977                    for col in 0..tw {
8978                        let (fx, fy) = (col as f32 / tw as f32, row as f32 / th as f32);
8979                        let (min_d, nearest) = pts.iter().enumerate().fold(
8980                            (f32::MAX, 0usize),
8981                            |(d, idx), (i, &[cx, cy])| {
8982                                let dd = (fx - cx).powi(2) + (fy - cy).powi(2);
8983                                if dd < d {
8984                                    (dd, i)
8985                                } else {
8986                                    (d, idx)
8987                                }
8988                            },
8989                        );
8990                        let t = (nearest as f32 / cells as f32 + min_d * 4.0) % 1.0;
8991                        let [cr, cg, cb] = tex_palette(&palette, t);
8992                        let (dx, dy) = (tx + col, ty + row);
8993                        if dx < bw && dy < bh {
8994                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
8995                        }
8996                    }
8997                }
8998                return Ok(Value::Unit);
8999            },
9000
9001            // tex_halftone(x, y, w, h, dot_size, time, palette)
9002            "tex_halftone" | "ลายฮาล์ฟโทน" | "بافت_نیم‌تن" | "نسيج_نصفي" | "מרקם_חצי_גוון" | "ہاف_ٹون_ٹیکسچر" => {
9003                let (tx, ty, tw, th) = self.tex_rect(&args)?;
9004                let dot_size = self.arg_num(&args, 4, 0.05)? as f32;
9005                let time = self.arg_num(&args, 5, 0.0)? as f32;
9006                let palette = self.arg_str(&args, 6, "rainbow");
9007                let mut gfx = self.gfx.borrow_mut();
9008                let (bw, bh) = (gfx.width, gfx.height);
9009                for row in 0..th {
9010                    for col in 0..tw {
9011                        let (fx, fy) = (col as f32 / tw as f32, row as f32 / th as f32);
9012                        let gx = (fx / dot_size).floor();
9013                        let gy = (fy / dot_size).floor();
9014                        let lx = (fx / dot_size - gx - 0.5) * 2.0;
9015                        let ly = (fy / dot_size - gy - 0.5) * 2.0;
9016                        let r = (lx * lx + ly * ly).sqrt();
9017                        let t = (gx / (1.0 / dot_size) + time * 0.1) % 1.0;
9018                        let a = if r < 0.7 {
9019                            ((0.7 - r) / 0.7).clamp(0., 1.)
9020                        } else {
9021                            0.0
9022                        };
9023                        if a > 0.0 {
9024                            let [cr, cg, cb] = tex_palette(&palette, t);
9025                            let (dx, dy) = (tx + col, ty + row);
9026                            if dx < bw && dy < bh {
9027                                gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
9028                            }
9029                        }
9030                    }
9031                }
9032                return Ok(Value::Unit);
9033            },
9034
9035            // ══════════════════════════════════════════════════════════════════
9036            // RENDER / LIGHTING MODES  (holographic cel shading)
9037            // ══════════════════════════════════════════════════════════════════
9038            // set_shade_mode(m) — 0 flat · 1 cel · 2 holo (default)
9039            "set_shade_mode" | "设置着色" | "シェード設定" | "셰이드모드" | "ตั้งการแรเงา" | "تنظیم_حالت_سایه‌پردازی" | "عيّن_نمط_التظليل" | "קבע_מצב_הצללה" | "شیڈ_موڈ_مقرر_کرو" | "définir_mode_ombrage" | "schattierungsmodus_setzen" | "задать_режим_затенения" =>
9040            {
9041                let m = self.arg_num(&args, 0, 2.0)? as u8;
9042                self.gfx.borrow_mut().shade_mode = m;
9043                return Ok(Value::Unit);
9044            },
9045            // set_cel_bands(n) — number of posterisation bands (>=2)
9046            "set_cel_bands" | "设置色阶" | "セル段数" | "셀밴드" | "ตั้งระดับสี" | "تنظیم_باندهای_سل" | "عيّن_نطاقات_التظليل" | "קבע_רצועות_הצללה" | "سیل_بینڈز_مقرر_کرو" | "définir_bandes_cel" | "cel_bänder_setzen" | "задать_полосы_cel" =>
9047            {
9048                let n = (self.arg_num(&args, 0, 4.0)? as u32).max(2);
9049                self.gfx.borrow_mut().shade.bands = n;
9050                return Ok(Value::Unit);
9051            },
9052            // set_shadow_color(r,g,b) — coloured-shadow tint, 0-255
9053            "set_shadow_color" | "设置阴影色" | "影の色" | "그림자색" | "ตั้งสีเงา" | "تنظیم_رنگ_سایه" | "عيّن_لون_الظل" | "קבע_צבע_צל" | "سایہ_رنگ_مقرر_کرو" | "définir_couleur_ombre" | "schattenfarbe_setzen" | "задать_цвет_тени" =>
9054            {
9055                let r = self.arg_num(&args, 0, 26.)? as f32 / 255.0;
9056                let g = self.arg_num(&args, 1, 33.)? as f32 / 255.0;
9057                let b = self.arg_num(&args, 2, 77.)? as f32 / 255.0;
9058                self.gfx.borrow_mut().shade.shadow = [r, g, b];
9059                return Ok(Value::Unit);
9060            },
9061            // set_rim(strength, r,g,b) — holographic fresnel edge glow
9062            // ══════════════════════════════════════════════════════════════════
9063            // CRYPTOGRAPHY (ling-crypto) — geo suite, hybrid PQ KEM, holographic
9064            // Bytes cross the language boundary as lowercase hex strings.
9065            // ══════════════════════════════════════════════════════════════════
9066            #[cfg(not(target_arch = "wasm32"))]
9067            "crypto_hash" | "แฮชเข้ารหัส" | "几何哈希" | "幾何ハッシュ" | "기하해시" | "درهم_رمزنگاری" | "بصمة_تشفير" | "גיבוב_הצפנה" | "خفیہ_ہیش" | "hachage_crypto" | "krypto_hash" | "крипто_хеш" =>
9068            {
9069                let s = self.arg_str(&args, 0, "");
9070                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(
9071                    s.as_bytes(),
9072                ))));
9073            },
9074            #[cfg(target_arch = "wasm32")]
9075            "crypto_hash" | "แฮชเข้ารหัส" | "几何哈希" | "幾何ハッシュ" | "기하해시" | "درهم_رمزنگاری" | "بصمة_تشفير" | "גיבוב_הצפנה" | "خفیہ_ہیش" | "hachage_crypto" | "krypto_hash" | "крипто_хеш" =>
9076            {
9077                let s = self.arg_str(&args, 0, "");
9078                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(
9079                    s.as_bytes(),
9080                ))));
9081            },
9082            // 3-D torus-knot fingerprint of any text/key → flat [x,y,z, x,y,z, …]
9083            #[cfg(not(target_arch = "wasm32"))]
9084            "knot_points" | "จุดปม" | "结点坐标" | "結び目点" | "매듭점" | "نقاط_گره" | "نقاط_العقدة" | "נקודות_קשר" | "گرہ_پوائنٹس" | "points_nœud" | "knotenpunkte" | "точки_узла" => {
9085                let s = self.arg_str(&args, 0, "");
9086                let shape = ling_crypto::geo::KnotShape::from_bytes(s.as_bytes());
9087                let mut out = Vec::with_capacity(shape.points.len() * 3);
9088                for p in &shape.points {
9089                    out.push(Value::Number(p[0] as f64));
9090                    out.push(Value::Number(p[1] as f64));
9091                    out.push(Value::Number(p[2] as f64));
9092                }
9093                return Ok(Value::List(Rc::new(out)));
9094            },
9095            #[cfg(target_arch = "wasm32")]
9096            "knot_points" | "จุดปม" | "结点坐标" | "結び目点" | "매듭점" | "نقاط_گره" | "نقاط_العقدة" | "נקודות_קשר" | "گرہ_پوائنٹس" | "points_nœud" | "knotenpunkte" | "точки_узла" => {
9097                let s = self.arg_str(&args, 0, "");
9098                let shape = ling_crypto::geo::KnotShape::from_bytes(s.as_bytes());
9099                let mut out = Vec::with_capacity(shape.points.len() * 3);
9100                for p in &shape.points {
9101                    out.push(Value::Number(p[0] as f64));
9102                    out.push(Value::Number(p[1] as f64));
9103                    out.push(Value::Number(p[2] as f64));
9104                }
9105                return Ok(Value::List(out.into()));
9106            },
9107            #[cfg(not(target_arch = "wasm32"))]
9108            "knot_label" | "ป้ายปม" | "结点标签" | "結び目ラベル" | "매듭라벨" | "برچسب_گره" | "تسمية_العقدة" | "תווית_קשר" | "گرہ_لیبل" | "étiquette_nœud" | "knotenbezeichnung" | "метка_узла" =>
9109            {
9110                let s = self.arg_str(&args, 0, "");
9111                return Ok(Value::Str(
9112                    ling_crypto::geo::KnotShape::from_bytes(s.as_bytes()).label(),
9113                ));
9114            },
9115            #[cfg(target_arch = "wasm32")]
9116            "knot_label" | "ป้ายปม" | "结点标签" | "結び目ラベル" | "매듭라벨" | "برچسب_گره" | "تسمية_العقدة" | "תווית_קשר" | "گرہ_لیبل" | "étiquette_nœud" | "knotenbezeichnung" | "метка_узла" =>
9117            {
9118                let s = self.arg_str(&args, 0, "");
9119                return Ok(Value::Str(
9120                    ling_crypto::geo::KnotShape::from_bytes(s.as_bytes()).label(),
9121                ));
9122            },
9123            // KEM keypair (hybrid X25519+ML-KEM-768) → integer handle
9124            #[cfg(not(target_arch = "wasm32"))]
9125            "knot_keygen" | "hybrid_keygen" | "สร้างกุญแจปม" | "生成密钥" | "鍵生成" | "키생성" | "تولید_کلید_گره" | "توليد_مفتاح_العقدة" | "יצירת_מפתח_קשר" | "گرہ_کلید_تخلیق" | "génération_clé_nœud" | "knotenschlüsselerzeugung" | "генерация_ключа_узла" =>
9126            {
9127                self.crypto_ids.push(ling_crypto::KnotIdentity::generate());
9128                return Ok(Value::Number((self.crypto_ids.len() - 1) as f64));
9129            },
9130            #[cfg(not(target_arch = "wasm32"))]
9131            "knot_public" | "hybrid_public" | "กุญแจสาธารณะปม" | "公钥" | "公開鍵" | "공개키" | "کلید_عمومی_گره" | "مفتاح_العقدة_العام" | "מפתח_ציבורי_קשר" | "گرہ_عوامی_کلید" | "clé_publique_nœud" | "knoten_öffentlicher_schlüssel" | "публичный_ключ_узла" =>
9132            {
9133                let h = self.arg_num(&args, 0, 0.0)? as usize;
9134                let pk = self
9135                    .crypto_ids
9136                    .get(h)
9137                    .map(|id| hex_encode(id.public_key()))
9138                    .unwrap_or_default();
9139                return Ok(Value::Str(pk));
9140            },
9141            // encapsulate(pubkey_hex) → [ciphertext_hex, shared_secret_hex]
9142            #[cfg(not(target_arch = "wasm32"))]
9143            "knot_encapsulate"
9144            | "hybrid_encapsulate"
9145            | "ห่อกุญแจปม"
9146            | "封装密钥"
9147            | "カプセル化"
9148            | "캡슐화" | "کپسوله‌سازی_گره" | "تغليف_مفتاح_العقدة" | "עטיפת_קשר" | "گرہ_احاطہ" | "encapsuler_nœud" | "knoten_kapseln" | "инкапсулировать_узел" => {
9149                let pk = hex_decode(&self.arg_str(&args, 0, ""));
9150                match ling_crypto::geo::knot_encapsulate(&pk) {
9151                    Ok((ct, ss)) => {
9152                        return Ok(Value::List(Rc::new(vec![
9153                            Value::Str(hex_encode(&ct)),
9154                            Value::Str(hex_encode(&ss)),
9155                        ])))
9156                    },
9157                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
9158                }
9159            },
9160            // decapsulate(handle, ciphertext_hex) → shared_secret_hex
9161            #[cfg(not(target_arch = "wasm32"))]
9162            "knot_decapsulate"
9163            | "hybrid_decapsulate"
9164            | "แกะกุญแจปม"
9165            | "解封装密钥"
9166            | "カプセル解除"
9167            | "캡슐해제" | "بازکردن_کپسوله_گره" | "فك_تغليف_مفتاح_العقدة" | "פתיחת_עטיפת_קשר" | "گرہ_احاطہ_کھولو" | "décapsuler_nœud" | "knoten_entkapseln" | "декапсулировать_узел" => {
9168                let h = self.arg_num(&args, 0, 0.0)? as usize;
9169                let ct = hex_decode(&self.arg_str(&args, 1, ""));
9170                let ss = self
9171                    .crypto_ids
9172                    .get(h)
9173                    .and_then(|id| id.decapsulate(&ct).ok())
9174                    .map(|s| hex_encode(&s))
9175                    .unwrap_or_default();
9176                return Ok(Value::Str(ss));
9177            },
9178            // Authenticated encryption (XChaCha20-Poly1305) — seal(key_hex, text) → ct_hex
9179            #[cfg(not(target_arch = "wasm32"))]
9180            "crypto_seal" | "ผนึก" | "封印" | "封印する" | "봉인" | "مهر_رمزنگاری" | "ختم_تشفير" | "חתימת_הצפנה" | "خفیہ_مہر" | "sceller_crypto" | "krypto_versiegeln" | "запечатать_крипто" => {
9181                let key = hex_to_32(&self.arg_str(&args, 0, ""));
9182                let pt = self.arg_str(&args, 1, "");
9183                match ling_crypto::geo::holo_seal(key, pt.as_bytes()) {
9184                    Ok(ct) => return Ok(Value::Str(hex_encode(&ct))),
9185                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
9186                }
9187            },
9188            #[cfg(not(target_arch = "wasm32"))]
9189            "crypto_open" | "เปิดผนึก" | "解封" | "封印解除" | "봉인해제" | "بازکردن_مهر" | "فتح_الختم" | "פתיחת_חתימה" | "مہر_کھولو" | "ouvrir_crypto" | "krypto_öffnen" | "открыть_крипто" =>
9190            {
9191                let key = hex_to_32(&self.arg_str(&args, 0, ""));
9192                let ct = hex_decode(&self.arg_str(&args, 1, ""));
9193                match ling_crypto::geo::holo_open(key, &ct) {
9194                    Ok(pt) => return Ok(Value::Str(String::from_utf8_lossy(&pt).into_owned())),
9195                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
9196                }
9197            },
9198            // Holographic all-or-nothing transform — 4-D fragment coords [a,b,c,d, …]
9199            #[cfg(not(target_arch = "wasm32"))]
9200            "holo_points" | "จุดโฮโลแกรม" | "全息点" | "ホログラム点" | "홀로그램점" | "نقاط_هولوگرام" | "نقاط_الهولوغرام" | "נקודות_הולוגרמה" | "ہولوگرام_پوائنٹس" | "points_holo" | "holo_punkte" | "точки_голо" =>
9201            {
9202                let s = self.arg_str(&args, 0, "");
9203                let frags = ling_crypto::geo::scatter(s.as_bytes());
9204                let mut out = Vec::with_capacity(frags.len() * 4);
9205                for f in &frags {
9206                    for c in f.coord {
9207                        out.push(Value::Number(c as f64));
9208                    }
9209                }
9210                return Ok(Value::List(Rc::new(out)));
9211            },
9212            #[cfg(not(target_arch = "wasm32"))]
9213            "holo_fragment_count"
9214            | "จำนวนชิ้นโฮโลแกรม"
9215            | "全息碎片数"
9216            | "ホログラム断片数"
9217            | "홀로그램조각수" | "تعداد_قطعات_هولوگرام" | "عدد_شظايا_الهولوغرام" | "מספר_שברי_הולוגרמה" | "ہولوگرام_ٹکڑے_تعداد" | "nombre_fragments_holo" | "holo_fragmentanzahl" | "число_фрагментов_голо" => {
9218                let s = self.arg_str(&args, 0, "");
9219                return Ok(Value::Number(
9220                    ling_crypto::geo::scatter(s.as_bytes()).len() as f64
9221                ));
9222            },
9223            // SHAKE-256 XOF, squeezed to an arbitrary output length in bytes
9224            // (`shake_hex(s, 128)` = a 1024-bit seal digest).
9225            #[cfg(not(target_arch = "wasm32"))]
9226            "shake_hex" | "SHAKE哈希" => {
9227                let s = self.arg_str(&args, 0, "");
9228                let len = self.arg_num(&args, 1, 32.0)?.max(0.0) as usize;
9229                return Ok(Value::Str(hex_encode(&ling_crypto::Shake256::hash(s.as_bytes(), len))));
9230            },
9231            // Ed25519 signing keypair (issuer identity) → integer handle.
9232            #[cfg(not(target_arch = "wasm32"))]
9233            "ed25519_keygen"
9234            | "생성서명키"
9235            | "สร้างกุญแจลายเซ็น"
9236            | "生成签名密钥"
9237            | "署名鍵生成"
9238            | "تولید_کلید_امضا"
9239            | "توليد_مفتاح_التوقيع"
9240            | "יצירת_מפתח_חתימה"
9241            | "دستخط_کلید_تخلیق"
9242            | "génération_clé_signature"
9243            | "signaturschlüsselerzeugung"
9244            | "генерация_ключа_подписи" => {
9245                self.ed25519_ids.push(ling_crypto::Ed25519Keypair::generate());
9246                return Ok(Value::Number((self.ed25519_ids.len() - 1) as f64));
9247            },
9248            // Deterministic keypair from a 32-byte hex seed — the same seed
9249            // always yields the same keypair, so a program can persist just the
9250            // seed (e.g. a bank's issuer identity) and rederive identical keys
9251            // across restarts instead of every run minting a fresh, unrelated one.
9252            #[cfg(not(target_arch = "wasm32"))]
9253            "ed25519_keygen_from_seed"
9254            | "씨앗에서생성서명키"
9255            | "สร้างกุญแจลายเซ็นจากเมล็ด"
9256            | "从种子生成签名密钥"
9257            | "シードから署名鍵生成"
9258            | "تولید_کلید_امضا_از_دانه"
9259            | "توليد_مفتاح_التوقيع_من_البذرة"
9260            | "יצירת_מפתח_חתימה_מזרע"
9261            | "بیج_سے_دستخط_کلید_تخلیق"
9262            | "génération_clé_signature_depuis_graine"
9263            | "signaturschlüsselerzeugung_aus_saat"
9264            | "генерация_ключа_подписи_из_семени" => {
9265                let seed = hex_to_32(&self.arg_str(&args, 0, ""));
9266                self.ed25519_ids.push(ling_crypto::Ed25519Keypair::from_seed(seed));
9267                return Ok(Value::Number((self.ed25519_ids.len() - 1) as f64));
9268            },
9269            #[cfg(not(target_arch = "wasm32"))]
9270            "ed25519_public"
9271            | "서명공개키"
9272            | "กุญแจสาธารณะลายเซ็น"
9273            | "签名公钥"
9274            | "署名公開鍵"
9275            | "کلید_عمومی_امضا"
9276            | "مفتاح_التوقيع_العام"
9277            | "מפתח_חתימה_ציבורי"
9278            | "دستخط_عوامی_کلید"
9279            | "clé_publique_signature"
9280            | "signatur_öffentlicher_schlüssel"
9281            | "публичный_ключ_подписи" => {
9282                let h = self.arg_num(&args, 0, 0.0)? as usize;
9283                let pk = self
9284                    .ed25519_ids
9285                    .get(h)
9286                    .map(|kp| hex_encode(&kp.public_key()))
9287                    .unwrap_or_default();
9288                return Ok(Value::Str(pk));
9289            },
9290            // ed25519_sign(handle, message) → signature hex (64 bytes)
9291            #[cfg(not(target_arch = "wasm32"))]
9292            "ed25519_sign"
9293            | "서명하다"
9294            | "เซ็นชื่อ"
9295            | "签名"
9296            | "署名する"
9297            | "امضا_کردن"
9298            | "توقيع"
9299            | "לחתום"
9300            | "دستخط_کریں"
9301            | "signer"
9302            | "signieren"
9303            | "подписать" => {
9304                let h = self.arg_num(&args, 0, 0.0)? as usize;
9305                let msg = self.arg_str(&args, 1, "");
9306                let sig = self
9307                    .ed25519_ids
9308                    .get(h)
9309                    .map(|kp| hex_encode(&kp.sign(msg.as_bytes())))
9310                    .unwrap_or_default();
9311                return Ok(Value::Str(sig));
9312            },
9313            // ed25519_verify(pubkey_hex, message, signature_hex) → bool
9314            #[cfg(not(target_arch = "wasm32"))]
9315            "ed25519_verify"
9316            | "서명확인"
9317            | "ยืนยันลายเซ็น"
9318            | "验证签名"
9319            | "署名検証"
9320            | "تایید_امضا"
9321            | "التحقق_من_التوقيع"
9322            | "אימות_חתימה"
9323            | "دستخط_تصدیق"
9324            | "vérifier_signature"
9325            | "signatur_verifizieren"
9326            | "проверить_подпись" => {
9327                let pk_hex = self.arg_str(&args, 0, "");
9328                let msg = self.arg_str(&args, 1, "");
9329                let sig_hex = self.arg_str(&args, 2, "");
9330                let pk_bytes = hex_decode(&pk_hex);
9331                let sig_bytes = hex_decode(&sig_hex);
9332                let ok = (|| {
9333                    let pk: [u8; 32] = pk_bytes.try_into().ok()?;
9334                    let sig: [u8; 64] = sig_bytes.try_into().ok()?;
9335                    Some(ling_crypto::Ed25519Keypair::verify(&pk, msg.as_bytes(), &sig).is_ok())
9336                })()
9337                .unwrap_or(false);
9338                return Ok(Value::Bool(ok));
9339            },
9340            // ML-DSA-65 (FIPS 204, post-quantum) signing keypair → integer
9341            // handle. Meant to be composed with ed25519_* by application code
9342            // into a hybrid signature (verify passes only if both check out),
9343            // not as a replacement for Ed25519 on its own. Aliases below mirror
9344            // the ed25519_* ones with each language's word for "quantum" folded
9345            // in, the same way knot_keygen/hybrid_keygen distinguish themselves.
9346            #[cfg(not(target_arch = "wasm32"))]
9347            "mldsa_keygen"
9348            | "양자서명키생성"
9349            | "สร้างกุญแจลายเซ็นควอนตัม"
9350            | "生成量子签名密钥"
9351            | "量子署名鍵生成"
9352            | "تولید_کلید_امضای_کوانتومی"
9353            | "توليد_مفتاح_التوقيع_الكمي"
9354            | "יצירת_מפתח_חתימה_קוונטי"
9355            | "کوانٹم_دستخط_کلید_تخلیق"
9356            | "génération_clé_signature_quantique"
9357            | "quantensignaturschlüsselerzeugung"
9358            | "генерация_ключа_квантовой_подписи" => {
9359                self.mldsa_ids.push(ling_crypto::MlDsa65Keypair::generate());
9360                return Ok(Value::Number((self.mldsa_ids.len() - 1) as f64));
9361            },
9362            // Deterministic keypair from a 32-byte hex seed — same rederive-
9363            // from-seed contract as ed25519_keygen_from_seed.
9364            #[cfg(not(target_arch = "wasm32"))]
9365            "mldsa_keygen_from_seed"
9366            | "씨앗에서양자서명키생성"
9367            | "สร้างกุญแจลายเซ็นควอนตัมจากเมล็ด"
9368            | "从种子生成量子签名密钥"
9369            | "シードから量子署名鍵生成"
9370            | "تولید_کلید_امضای_کوانتومی_از_دانه"
9371            | "توليد_مفتاح_التوقيع_الكمي_من_البذرة"
9372            | "יצירת_מפתח_חתימה_קוונטי_מזרע"
9373            | "بیج_سے_کوانٹم_دستخط_کلید_تخلیق"
9374            | "génération_clé_signature_quantique_depuis_graine"
9375            | "quantensignaturschlüsselerzeugung_aus_saat"
9376            | "генерация_ключа_квантовой_подписи_из_семени" => {
9377                let seed = hex_to_32(&self.arg_str(&args, 0, ""));
9378                self.mldsa_ids.push(ling_crypto::MlDsa65Keypair::from_seed(seed));
9379                return Ok(Value::Number((self.mldsa_ids.len() - 1) as f64));
9380            },
9381            #[cfg(not(target_arch = "wasm32"))]
9382            "mldsa_public"
9383            | "양자서명공개키"
9384            | "กุญแจสาธารณะลายเซ็นควอนตัม"
9385            | "量子签名公钥"
9386            | "量子署名公開鍵"
9387            | "کلید_عمومی_امضای_کوانتومی"
9388            | "مفتاح_التوقيع_الكمي_العام"
9389            | "מפתח_חתימה_קוונטי_ציבורי"
9390            | "کوانٹم_دستخط_عوامی_کلید"
9391            | "clé_publique_signature_quantique"
9392            | "quantensignatur_öffentlicher_schlüssel"
9393            | "публичный_ключ_квантовой_подписи" => {
9394                let h = self.arg_num(&args, 0, 0.0)? as usize;
9395                let pk = self
9396                    .mldsa_ids
9397                    .get(h)
9398                    .map(|kp| hex_encode(&kp.public_key()))
9399                    .unwrap_or_default();
9400                return Ok(Value::Str(pk));
9401            },
9402            // mldsa_sign(handle, message) → signature hex (~3309 bytes)
9403            #[cfg(not(target_arch = "wasm32"))]
9404            "mldsa_sign"
9405            | "양자서명하다"
9406            | "เซ็นชื่อควอนตัม"
9407            | "量子签名"
9408            | "量子署名する"
9409            | "امضای_کوانتومی_کردن"
9410            | "توقيع_كمي"
9411            | "לחתום_קוונטית"
9412            | "کوانٹم_دستخط_کریں"
9413            | "signer_quantique"
9414            | "quantensignieren"
9415            | "подписать_квантово" => {
9416                let h = self.arg_num(&args, 0, 0.0)? as usize;
9417                let msg = self.arg_str(&args, 1, "");
9418                let sig = self
9419                    .mldsa_ids
9420                    .get(h)
9421                    .map(|kp| hex_encode(&kp.sign(msg.as_bytes())))
9422                    .unwrap_or_default();
9423                return Ok(Value::Str(sig));
9424            },
9425            // mldsa_verify(pubkey_hex, message, signature_hex) → bool
9426            #[cfg(not(target_arch = "wasm32"))]
9427            "mldsa_verify"
9428            | "양자서명확인"
9429            | "ยืนยันลายเซ็นควอนตัม"
9430            | "验证量子签名"
9431            | "量子署名検証"
9432            | "تایید_امضای_کوانتومی"
9433            | "التحقق_من_التوقيع_الكمي"
9434            | "אימות_חתימה_קוונטית"
9435            | "کوانٹم_دستخط_تصدیق"
9436            | "vérifier_signature_quantique"
9437            | "quantensignatur_verifizieren"
9438            | "проверить_квантовую_подпись" => {
9439                let pk_hex = self.arg_str(&args, 0, "");
9440                let msg = self.arg_str(&args, 1, "");
9441                let sig_hex = self.arg_str(&args, 2, "");
9442                let pk_bytes = hex_decode(&pk_hex);
9443                let sig_bytes = hex_decode(&sig_hex);
9444                let ok = ling_crypto::MlDsa65Keypair::verify(&pk_bytes, msg.as_bytes(), &sig_bytes)
9445                    .is_ok();
9446                return Ok(Value::Bool(ok));
9447            },
9448            // Argon2id password hashing — password_hash(pw) → PHC string,
9449            // password_verify(pw, phc_string) → bool.
9450            #[cfg(not(target_arch = "wasm32"))]
9451            "password_hash" | "비밀번호해시" => {
9452                let pw = self.arg_str(&args, 0, "");
9453                let hash = ling_crypto::Argon2idParams::default()
9454                    .hash_password(pw.as_bytes())
9455                    .unwrap_or_default();
9456                return Ok(Value::Str(hash));
9457            },
9458            #[cfg(not(target_arch = "wasm32"))]
9459            "password_verify" | "비밀번호확인" => {
9460                let pw = self.arg_str(&args, 0, "");
9461                let hash = self.arg_str(&args, 1, "");
9462                let ok = ling_crypto::Argon2idParams::verify_password(pw.as_bytes(), &hash).is_ok();
9463                return Ok(Value::Bool(ok));
9464            },
9465            // OS-CSPRNG random bytes as hex — session ids / nonces (not the
9466            // xorshift `rand` builtin, which is for game logic, not security).
9467            #[cfg(not(target_arch = "wasm32"))]
9468            "random_hex" | "무작위16진수" => {
9469                use rand::RngCore;
9470                let n = self.arg_num(&args, 0, 16.0)?.max(0.0) as usize;
9471                let mut buf = vec![0u8; n];
9472                rand::rngs::OsRng.fill_bytes(&mut buf);
9473                return Ok(Value::Str(hex_encode(&buf)));
9474            },
9475            // base64_encode(s) — text -> base64 (matches what canvas.toDataURL()
9476            // already produces client-side, so PNG uploads never need a binary
9477            // request body).
9478            #[cfg(not(target_arch = "wasm32"))]
9479            "base64_encode" | "base64인코딩" => {
9480                use base64::Engine as _;
9481                let s = self.arg_str(&args, 0, "");
9482                return Ok(Value::Str(
9483                    base64::engine::general_purpose::STANDARD.encode(s.as_bytes()),
9484                ));
9485            },
9486            // base64_decode_to_file(b64, path) — writes decoded bytes straight to
9487            // disk; returns true on success. The only way binary data (an
9488            // uploaded/rendered PNG) reaches the filesystem from `.ling` source.
9489            #[cfg(not(target_arch = "wasm32"))]
9490            "base64_decode_to_file" | "base64파일로저장" => {
9491                use base64::Engine as _;
9492                let b64 = self.arg_str(&args, 0, "");
9493                let path = self.arg_str(&args, 1, "");
9494                let b64 = b64
9495                    .split(',')
9496                    .next_back()
9497                    .unwrap_or(&b64); // tolerate a "data:image/png;base64,..." prefix
9498                let ok = base64::engine::general_purpose::STANDARD
9499                    .decode(b64)
9500                    .ok()
9501                    .and_then(|bytes| std::fs::write(&path, bytes).ok())
9502                    .is_some();
9503                return Ok(Value::Bool(ok));
9504            },
9505            // qr_svg(text) — a scannable QR code as an inline <svg>...</svg>
9506            // string (e.g. for an otpauth:// 2FA enrollment URI). Kept as SVG
9507            // rather than a rasterized image so it fits the same "everything
9508            // stays vector" theme as the banknote seal art.
9509            #[cfg(not(target_arch = "wasm32"))]
9510            "qr_svg" | "QR코드" => {
9511                let text = self.arg_str(&args, 0, "");
9512                let svg = qrcode::QrCode::new(text.as_bytes())
9513                    .map(|code| {
9514                        code.render::<qrcode::render::svg::Color>()
9515                            .min_dimensions(240, 240)
9516                            .dark_color(qrcode::render::svg::Color("#1a0f3d"))
9517                            .light_color(qrcode::render::svg::Color("#ffffff"))
9518                            .build()
9519                    })
9520                    .unwrap_or_default();
9521                return Ok(Value::Str(svg));
9522            },
9523            // zip_files(paths_list, out_path) — bundles files into a zip archive
9524            // (used by the "render" step to package a note's PNG/SVG/PDF).
9525            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
9526            "zip_files" | "압축파일" => {
9527                let paths = match args.first() {
9528                    Some(Value::List(l)) => l.iter().map(|v| v.to_string()).collect::<Vec<_>>(),
9529                    _ => Vec::new(),
9530                };
9531                let out_path = self.arg_str(&args, 1, "out.zip");
9532                let ok = (|| -> std::io::Result<()> {
9533                    let file = std::fs::File::create(&out_path)?;
9534                    let mut writer = zip::ZipWriter::new(file);
9535                    let options: zip::write::FileOptions<'_, ()> = zip::write::FileOptions::default()
9536                        .compression_method(zip::CompressionMethod::Deflated);
9537                    for p in &paths {
9538                        let name = std::path::Path::new(p)
9539                            .file_name()
9540                            .map(|n| n.to_string_lossy().into_owned())
9541                            .unwrap_or_else(|| p.clone());
9542                        let bytes = std::fs::read(p)?;
9543                        writer.start_file(name, options)?;
9544                        std::io::Write::write_all(&mut writer, &bytes)?;
9545                    }
9546                    writer.finish()?;
9547                    Ok(())
9548                })()
9549                .is_ok();
9550                return Ok(Value::Bool(ok));
9551            },
9552            // pdf_from_images(png_paths_list, out_path) — one page per image,
9553            // sized to its pixel dimensions. No PDF crate dependency: `image`
9554            // (decode) and `flate2` (deflate the page's raw RGB stream) are
9555            // already unconditional deps, so this hand-writes the handful of
9556            // PDF objects (Catalog/Pages/Page/Contents/Image XObject) directly.
9557            // `build_pdf_from_images` itself only exists under this same gate.
9558            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
9559            "pdf_from_images" | "PDF来自图片" => {
9560                let paths = match args.first() {
9561                    Some(Value::List(l)) => l.iter().map(|v| v.to_string()).collect::<Vec<_>>(),
9562                    _ => Vec::new(),
9563                };
9564                let out_path = self.arg_str(&args, 1, "out.pdf");
9565                let ok = build_pdf_from_images(&paths, &out_path).is_ok();
9566                return Ok(Value::Bool(ok));
9567            },
9568
9569            // ══════════════════════════════════════════════════════════════════
9570            // ling-ui — animation easings + holographic vector widgets + text I/O
9571            // ══════════════════════════════════════════════════════════════════
9572            "ease" => {
9573                let name = self.arg_str(&args, 0, "ease");
9574                let t = self.arg_num(&args, 1, 0.0)? as f32;
9575                return Ok(Value::Number(
9576                    ling_ui::Easing::from_name(&name).apply(t) as f64
9577                ));
9578            },
9579
9580            // ══════════════════════════════════════════════════════════════════
9581            // Anima — unified animation drivers (ling-animation). Organic 灵 +
9582            // mechanical 机 scalar drivers, callable per frame from a script.
9583            // ══════════════════════════════════════════════════════════════════
9584            "tween" | "补间" | "補間" | "트윈" | "แทรกค่า" | "میان‌فریم" | "تدرج_حركي" | "טווין" | "ٹوئین" | "твин" => {
9585                let a = self.arg_num(&args, 0, 0.0)?;
9586                let b = self.arg_num(&args, 1, 0.0)?;
9587                let t = self.arg_num(&args, 2, 0.0)?.clamp(0.0, 1.0);
9588                return Ok(Value::Number(a + (b - a) * t));
9589            },
9590            "tween_ease" | "缓动补间" | "緩和補間" | "이징트윈" | "แทรกนุ่ม" | "میان‌فریم_نرم" | "تدرج_ناعم_حركي" | "טווין_חלק" | "ٹوئین_ایز" | "tween_lisse" | "tween_glättung" | "твин_плавность" =>
9591            {
9592                let a = self.arg_num(&args, 0, 0.0)? as f32;
9593                let b = self.arg_num(&args, 1, 0.0)? as f32;
9594                let t = self.arg_num(&args, 2, 0.0)? as f32;
9595                let kind = self.arg_str(&args, 3, "linear");
9596                let e = ling_animation::EaseFunction::from_name(&kind);
9597                return Ok(Value::Number(
9598                    ling_animation::ease::tween_ease(&a, &b, t, e) as f64,
9599                ));
9600            },
9601            // ── Organic 灵 ──
9602            "breathe" | "呼吸" | "호흡" | "หายใจ" | "تنفس" | "נשימה" | "سانس" | "respirer" | "atmen" | "дышать" => {
9603                let t = self.arg_num(&args, 0, 0.0)? as f32;
9604                let rate = self.arg_num(&args, 1, 1.0)? as f32;
9605                let depth = self.arg_num(&args, 2, 0.1)? as f32;
9606                return Ok(Value::Number(
9607                    ling_animation::scalar::breathe(t, rate, depth) as f64,
9608                ));
9609            },
9610            "wobble" | "摆动" | "揺れ" | "흔들림" | "โยก" | "نوسان" | "تذبذب" | "תנודה" | "لرزش" | "osciller" | "wackeln" | "покачивание" => {
9611                let t = self.arg_num(&args, 0, 0.0)? as f32;
9612                let freq = self.arg_num(&args, 1, 1.0)? as f32;
9613                let amp = self.arg_num(&args, 2, 1.0)? as f32;
9614                let phase = self.arg_num(&args, 3, 0.0)? as f32;
9615                return Ok(Value::Number(
9616                    ling_animation::scalar::wobble(t, freq, amp, phase) as f64,
9617                ));
9618            },
9619            "gait_phase" | "步相" | "歩相" | "걸음위상" | "เฟสก้าว" | "فاز_گام" | "طور_المشية" | "שלב_הליכה" | "چال_مرحلہ" | "phase_démarche" | "gangphase" | "фаза_походки" => {
9620                let t = self.arg_num(&args, 0, 0.0)? as f32;
9621                let speed = self.arg_num(&args, 1, 1.0)? as f32;
9622                return Ok(Value::Number(
9623                    ling_animation::scalar::gait_phase(t, speed) as f64
9624                ));
9625            },
9626            "gait_swing" | "步摆" | "歩振り" | "걸음흔들" | "ก้าวแกว่ง" | "نوسان_گام" | "أرجحة_المشية" | "נדנוד_הליכה" | "چال_جھولا" | "balancement_démarche" | "gangschwung" | "мах_походки" =>
9627            {
9628                let t = self.arg_num(&args, 0, 0.0)? as f32;
9629                let speed = self.arg_num(&args, 1, 1.0)? as f32;
9630                let stride = self.arg_num(&args, 2, 1.0)? as f32;
9631                return Ok(Value::Number(
9632                    ling_animation::scalar::gait_swing(t, speed, stride) as f64,
9633                ));
9634            },
9635            "gait_lift" | "抬脚" | "足上げ" | "발들기" | "ยกเท้า" | "بلندشدن_گام" | "رفع_المشية" | "הרמת_הליכה" | "چال_اٹھاؤ" | "levée_démarche" | "ganghub" | "подъём_походки" => {
9636                let t = self.arg_num(&args, 0, 0.0)? as f32;
9637                let speed = self.arg_num(&args, 1, 1.0)? as f32;
9638                let height = self.arg_num(&args, 2, 1.0)? as f32;
9639                return Ok(Value::Number(
9640                    ling_animation::scalar::gait_lift(t, speed, height) as f64,
9641                ));
9642            },
9643            "spring_to" | "弹向" | "バネ寄せ" | "스프링이동" | "สปริงไป" | "فنر_به‌سوی" | "نابض_إلى" | "קפיץ_אל" | "اسپرنگ_تک" | "ressort_vers" | "feder_zu" | "пружина_к" =>
9644            {
9645                let pos = self.arg_num(&args, 0, 0.0)? as f32;
9646                let vel = self.arg_num(&args, 1, 0.0)? as f32;
9647                let target = self.arg_num(&args, 2, 0.0)? as f32;
9648                let stiffness = self.arg_num(&args, 3, 120.0)? as f32;
9649                let damping = self.arg_num(&args, 4, 14.0)? as f32;
9650                let dt = self.arg_num(&args, 5, 1.0 / 60.0)? as f32;
9651                let (np, nv) =
9652                    ling_animation::scalar::spring_step(pos, vel, target, stiffness, damping, dt);
9653                return Ok(Value::List(Rc::new(vec![
9654                    Value::Number(np as f64),
9655                    Value::Number(nv as f64),
9656                ])));
9657            },
9658            "ik2" | "反解" | "逆運動" | "역운동" | "ไอเค2" | "سینماتیک_معکوس2" | "حركية_عكسية2" | "קינמטיקה_הפוכה2" | "آئی_کے2" | "cinématique_inverse2" | "inverse_kinematik2" | "обратная_кинематика2" => {
9659                let l1 = self.arg_num(&args, 0, 1.0)? as f32;
9660                let l2 = self.arg_num(&args, 1, 1.0)? as f32;
9661                let tx = self.arg_num(&args, 2, 0.0)? as f32;
9662                let ty = self.arg_num(&args, 3, 0.0)? as f32;
9663                let (sh, el) = ling_animation::scalar::two_bone_ik(l1, l2, tx, ty);
9664                return Ok(Value::List(Rc::new(vec![
9665                    Value::Number(sh as f64),
9666                    Value::Number(el as f64),
9667                ])));
9668            },
9669            // ── Mechanical 机 ──
9670            "gear_couple" | "齿轮联动" | "歯車連動" | "기어연동" | "เฟืองทด" | "جفت_چرخ‌دنده" | "اقتران_التروس" | "צימוד_גלגלי_שיניים" | "گیئر_جوڑا" | "accoupler_engrenage" | "zahnrad_koppeln" | "сцепить_шестерни" =>
9671            {
9672                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9673                let ti = self.arg_num(&args, 1, 1.0)? as f32;
9674                let to = self.arg_num(&args, 2, 1.0)? as f32;
9675                return Ok(Value::Number(
9676                    ling_animation::scalar::gear(angle, ti, to) as f64
9677                ));
9678            },
9679            "gear_train" | "齿轮组" | "歯車列" | "기어열" | "ชุดเฟือง" | "مجموعه_چرخ‌دنده" | "قطار_التروس" | "שרשרת_גלגלי_שיניים" | "گیئر_ٹرین" | "train_engrenages" | "zahnradgetriebe" | "передача_шестерён" => {
9680                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9681                let teeth: Vec<f32> = match args.get(1) {
9682                    Some(Value::List(items)) => items
9683                        .iter()
9684                        .filter_map(|v| {
9685                            if let Value::Number(n) = v {
9686                                Some(*n as f32)
9687                            } else {
9688                                None
9689                            }
9690                        })
9691                        .collect(),
9692                    _ => Vec::new(),
9693                };
9694                let out = ling_animation::mechanism::gear_train(angle, &teeth);
9695                return Ok(Value::List(Rc::new(
9696                    out.into_iter().map(|a| Value::Number(a as f64)).collect(),
9697                )));
9698            },
9699            "cam_lift" | "凸轮升程" | "カム揚程" | "캠리프트" | "ยกลูกเบี้ยว" | "بلندشدن_بادامک" | "رفع_الكامة" | "הרמת_קאם" | "کیم_اٹھاؤ" | "levée_came" | "nockenhub" | "подъём_кулачка" =>
9700            {
9701                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9702                let lift = self.arg_num(&args, 1, 1.0)? as f32;
9703                return Ok(Value::Number(
9704                    ling_animation::scalar::cam_lift(angle, lift) as f64
9705                ));
9706            },
9707            "piston" | "活塞" | "ピストン" | "피스톤" | "ลูกสูบ" | "پیستون" | "مكبس" | "בוכנה" | "پسٹن" | "kolben" | "поршень" => {
9708                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9709                let crank = self.arg_num(&args, 1, 1.0)? as f32;
9710                let rod = self.arg_num(&args, 2, 2.0)? as f32;
9711                return Ok(Value::Number(
9712                    ling_animation::scalar::piston(angle, crank, rod) as f64,
9713                ));
9714            },
9715            "rack" | "齿条" | "ラック" | "랙" | "แร็ค" | "زبانه‌دنده" | "سكة_مسننة" | "מוט_שיניים" | "ریک" | "crémaillère" | "zahnstange" | "рейка" => {
9716                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9717                let radius = self.arg_num(&args, 1, 1.0)? as f32;
9718                return Ok(Value::Number(
9719                    ling_animation::scalar::rack(angle, radius) as f64
9720                ));
9721            },
9722            #[cfg(not(target_arch = "wasm32"))]
9723            "mouse_x" => {
9724                let gfx = self.gfx.borrow();
9725                let v = gfx
9726                    .window
9727                    .as_ref()
9728                    .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
9729                    .map(|p| p.0 as f64)
9730                    .unwrap_or(0.0);
9731                return Ok(Value::Number(v));
9732            },
9733            #[cfg(target_arch = "wasm32")]
9734            "mouse_x" => {
9735                return Ok(Value::Number(crate::gfx::wasm_mouse_x() as f64));
9736            },
9737            #[cfg(not(target_arch = "wasm32"))]
9738            "mouse_y" => {
9739                let gfx = self.gfx.borrow();
9740                let v = gfx
9741                    .window
9742                    .as_ref()
9743                    .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
9744                    .map(|p| p.1 as f64)
9745                    .unwrap_or(0.0);
9746                return Ok(Value::Number(v));
9747            },
9748            #[cfg(target_arch = "wasm32")]
9749            "mouse_y" => {
9750                return Ok(Value::Number(crate::gfx::wasm_mouse_y() as f64));
9751            },
9752            #[cfg(not(target_arch = "wasm32"))]
9753            "mouse_down" => {
9754                let mut gfx = self.gfx.borrow_mut();
9755                let d = !gfx.input_suppressed()
9756                    && gfx
9757                        .window
9758                        .as_ref()
9759                        .map(|w| w.get_mouse_down(minifb::MouseButton::Left))
9760                        .unwrap_or(false);
9761                return Ok(Value::Bool(d));
9762            },
9763            #[cfg(target_arch = "wasm32")]
9764            "mouse_down" => {
9765                return Ok(Value::Bool(crate::gfx::wasm_mouse_down()));
9766            },
9767            #[cfg(not(target_arch = "wasm32"))]
9768            "mouse_down_right" | "เมาส์ขวา" | "ماوس_راست_فشرده" | "الفأرة_اليمنى_مضغوطة" | "עכבר_ימני_לחוץ" | "دایاں_ماؤس_دبا_ہوا" => {
9769                let mut gfx = self.gfx.borrow_mut();
9770                let d = !gfx.input_suppressed()
9771                    && gfx
9772                        .window
9773                        .as_ref()
9774                        .map(|w| w.get_mouse_down(minifb::MouseButton::Right))
9775                        .unwrap_or(false);
9776                return Ok(Value::Bool(d));
9777            },
9778            #[cfg(target_arch = "wasm32")]
9779            "mouse_down_right" | "เมาส์ขวา" | "ماوس_راست_فشرده" | "الفأرة_اليمنى_مضغوطة" | "עכבר_ימני_לחוץ" | "دایاں_ماؤس_دبا_ہوا" => {
9780                return Ok(Value::Bool(crate::gfx::wasm_mouse_down_right()));
9781            },
9782            #[cfg(not(target_arch = "wasm32"))]
9783            "mouse_down_middle" | "เมาส์กลาง" | "ماوس_وسط_فشرده" | "الفأرة_الوسطى_مضغوطة" | "עכבר_אמצעי_לחוץ" | "درمیانی_ماؤس_دبا_ہوا" => {
9784                let mut gfx = self.gfx.borrow_mut();
9785                let d = !gfx.input_suppressed()
9786                    && gfx
9787                        .window
9788                        .as_ref()
9789                        .map(|w| w.get_mouse_down(minifb::MouseButton::Middle))
9790                        .unwrap_or(false);
9791                return Ok(Value::Bool(d));
9792            },
9793            #[cfg(target_arch = "wasm32")]
9794            "mouse_down_middle" | "เมาส์กลาง" | "ماوس_وسط_فشرده" | "الفأرة_الوسطى_مضغوطة" | "עכבר_אמצעי_לחוץ" | "درمیانی_ماؤس_دبا_ہوا" => {
9795                return Ok(Value::Bool(crate::gfx::wasm_mouse_down_middle()));
9796            },
9797            #[cfg(not(target_arch = "wasm32"))]
9798            "ui_hot" | "热区" | "ホットエリア" | "핫존" | "พื้นที่สัมผัส" | "ناحیه_فعال" | "منطقة_ساخنة" | "אזור_חם" | "ہاٹ_زون" | "survol_ui" | "ui_hover" | "ui_наведение" =>
9799            {
9800                let x = self.arg_num(&args, 0, 0.0)? as f32;
9801                let y = self.arg_num(&args, 1, 0.0)? as f32;
9802                let w = self.arg_num(&args, 2, 0.0)? as f32;
9803                let h = self.arg_num(&args, 3, 0.0)? as f32;
9804                let gfx = self.gfx.borrow();
9805                let (mx, my) = gfx
9806                    .window
9807                    .as_ref()
9808                    .and_then(|win| win.get_mouse_pos(minifb::MouseMode::Clamp))
9809                    .unwrap_or((0.0, 0.0));
9810                return Ok(Value::Bool(ling_ui::holo::hit_rect(mx, my, x, y, w, h)));
9811            },
9812            #[cfg(target_arch = "wasm32")]
9813            "ui_hot" | "热区" | "ホットエリア" | "핫존" | "พื้นที่สัมผัส" | "ناحیه_فعال" | "منطقة_ساخنة" | "אזור_חם" | "ہاٹ_زون" | "survol_ui" | "ui_hover" | "ui_наведение" =>
9814            {
9815                return Ok(Value::Bool(false));
9816            },
9817            // ui_text(x, y, scale, "string") — holographic vector text
9818            "ui_text" | "界面文字" | "UI文字" | "UI텍스트" | "ข้อความหน้าจอ" | "متن_رابط" | "نص_الواجهة" | "טקסט_ממשק" | "یو_آئی_متن" | "texte_ui" | "ui_beschriftung" | "ui_текст" =>
9819            {
9820                let x = self.arg_num(&args, 0, 0.0)? as f32;
9821                let y = self.arg_num(&args, 1, 0.0)? as f32;
9822                let scale = self.arg_num(&args, 2, 16.0)? as f32;
9823                let s = self.arg_str(&args, 3, "");
9824                let segs = ling_ui::holo::text_lines(&s, x, y, scale * 0.62, scale, scale * 0.24);
9825                let mut gfx = self.gfx.borrow_mut();
9826                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
9827                for sg in segs {
9828                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
9829                }
9830                return Ok(Value::Unit);
9831            },
9832            // font_load("path.ttf") — load a vector font (outlines cached lazily as
9833            // cache/fonts/<stem>/<codepoint>.ling). Returns a handle, or -1 on failure.
9834            #[cfg(not(target_arch = "wasm32"))]
9835            "font_load" | "โหลดฟอนต์" | "加载字体" | "フォント読込" | "글꼴로드" | "بارگذاری_فونت" | "تحميل_الخط" | "טעינת_גופן" | "فونٹ_لوڈ" | "charger_police" | "schriftart_laden" | "загрузить_шрифт" =>
9836            {
9837                let path = self.arg_str(&args, 0, "");
9838                // Optional 2nd arg: variable-font weight (e.g. 600 for a solid, bold UI).
9839                let weight = match self.arg_num(&args, 1, 0.0)? {
9840                    w if w > 0.0 => Some(w as f32),
9841                    _ => None,
9842                };
9843                // Try the path as given, then relative to the script's directory.
9844                let mut loaded = ling_graphics::VectorFont::from_path_weight(&path, weight);
9845                if loaded.is_err() {
9846                    if let Some(dir) = &self.source_dir {
9847                        let joined = dir.join(&path);
9848                        loaded = ling_graphics::VectorFont::from_path_weight(
9849                            &joined.to_string_lossy(),
9850                            weight,
9851                        );
9852                    }
9853                }
9854                match loaded {
9855                    Ok(f) => {
9856                        let id = self.fonts.len();
9857                        self.fonts.push(f);
9858                        return Ok(Value::Number(id as f64));
9859                    },
9860                    Err(e) => {
9861                        eprintln!("font_load failed ({path}): {e}");
9862                        return Ok(Value::Number(-1.0));
9863                    },
9864                }
9865            },
9866            #[cfg(target_arch = "wasm32")]
9867            "font_load" | "โหลดฟอนต์" | "加载字体" | "フォント読込" | "글꼴로드" | "بارگذاری_فونت" | "تحميل_الخط" | "טעינת_גופן" | "فونٹ_لوڈ" | "charger_police" | "schriftart_laden" | "загрузить_шрифт" =>
9868            {
9869                // Web runtime does not load host TTF/OTF files yet.
9870                // Return -1 so scripts can fall back to ui_text.
9871                return Ok(Value::Number(-1.0));
9872            },
9873            // image_load("path.png") — decode a raster image (via the `image` crate)
9874            // for pixel sampling (image_width/image_height/image_pixel_r/g/b/a) —
9875            // used by the coin-stamp mosaic tool to read a source photo's
9876            // colour/darkness. Returns a handle, or -1 on failure.
9877            #[cfg(not(target_arch = "wasm32"))]
9878            "image_load" =>
9879            {
9880                let path = self.arg_str(&args, 0, "").replace('\\', "/");
9881                let mut loaded = image::open(&path);
9882                if loaded.is_err() {
9883                    if let Some(dir) = &self.source_dir {
9884                        let joined = dir.join(&path);
9885                        loaded = image::open(&joined);
9886                    }
9887                }
9888                match loaded {
9889                    Ok(img) => {
9890                        let id = self.images.len();
9891                        self.images.push(img.to_rgba8());
9892                        return Ok(Value::Number(id as f64));
9893                    },
9894                    Err(e) => {
9895                        eprintln!("image_load failed ({path}): {e}");
9896                        return Ok(Value::Number(-1.0));
9897                    },
9898                }
9899            },
9900            #[cfg(target_arch = "wasm32")]
9901            "image_load" =>
9902            {
9903                // Web runtime does not load host image files yet.
9904                return Ok(Value::Number(-1.0));
9905            },
9906            "image_width" =>
9907            {
9908                let id = self.arg_num(&args, 0, -1.0)? as i64;
9909                if id >= 0 && (id as usize) < self.images.len() {
9910                    return Ok(Value::Number(self.images[id as usize].width() as f64));
9911                }
9912                return Ok(Value::Number(0.0));
9913            },
9914            "image_height" =>
9915            {
9916                let id = self.arg_num(&args, 0, -1.0)? as i64;
9917                if id >= 0 && (id as usize) < self.images.len() {
9918                    return Ok(Value::Number(self.images[id as usize].height() as f64));
9919                }
9920                return Ok(Value::Number(0.0));
9921            },
9922            "image_pixel_r" | "image_pixel_g" | "image_pixel_b" | "image_pixel_a" =>
9923            {
9924                let id = self.arg_num(&args, 0, -1.0)? as i64;
9925                let px = self.arg_num(&args, 1, 0.0)? as i64;
9926                let py = self.arg_num(&args, 2, 0.0)? as i64;
9927                if id >= 0 && (id as usize) < self.images.len() {
9928                    let img = &self.images[id as usize];
9929                    if px >= 0 && py >= 0 && (px as u32) < img.width() && (py as u32) < img.height() {
9930                        let p = img.get_pixel(px as u32, py as u32);
9931                        let ch = match name {
9932                            "image_pixel_r" => p[0],
9933                            "image_pixel_g" => p[1],
9934                            "image_pixel_b" => p[2],
9935                            _ => p[3],
9936                        };
9937                        return Ok(Value::Number(ch as f64));
9938                    }
9939                }
9940                return Ok(Value::Number(0.0));
9941            },
9942            // image_new(w, h) — a new blank (fully transparent) RGBA image the
9943            // script can paint into with image_set_pixel and write out with
9944            // image_save. Lives in the same self.images table as image_load,
9945            // so image_width/image_height/image_pixel_* all work on it too.
9946            // Used by the coin-stamp tool to build cropped, physically-sized
9947            // (mm x DPI) PNG exports — something a raw window screenshot()
9948            // can't do, since it always captures the whole on-screen
9949            // framebuffer at whatever size the window happens to be.
9950            "image_new" =>
9951            {
9952                let w = self.arg_num(&args, 0, 1.0)?.max(1.0) as u32;
9953                let h = self.arg_num(&args, 1, 1.0)?.max(1.0) as u32;
9954                let id = self.images.len();
9955                self.images.push(image::RgbaImage::new(w, h));
9956                return Ok(Value::Number(id as f64));
9957            },
9958            // image_set_pixel(id, x, y, r, g, b, a) — paint one pixel of an
9959            // image created with image_new (0..255 channels; out-of-bounds is
9960            // a silent no-op, matching image_pixel_*'s own out-of-bounds
9961            // behaviour).
9962            "image_set_pixel" =>
9963            {
9964                let id = self.arg_num(&args, 0, -1.0)? as i64;
9965                let px = self.arg_num(&args, 1, 0.0)? as i64;
9966                let py = self.arg_num(&args, 2, 0.0)? as i64;
9967                let r = self.arg_num(&args, 3, 0.0)?.clamp(0.0, 255.0) as u8;
9968                let g = self.arg_num(&args, 4, 0.0)?.clamp(0.0, 255.0) as u8;
9969                let b = self.arg_num(&args, 5, 0.0)?.clamp(0.0, 255.0) as u8;
9970                let a = self.arg_num(&args, 6, 255.0)?.clamp(0.0, 255.0) as u8;
9971                if id >= 0 && (id as usize) < self.images.len() {
9972                    let img = &mut self.images[id as usize];
9973                    if px >= 0 && py >= 0 && (px as u32) < img.width() && (py as u32) < img.height() {
9974                        img.put_pixel(px as u32, py as u32, image::Rgba([r, g, b, a]));
9975                    }
9976                }
9977                return Ok(Value::Unit);
9978            },
9979            // image_save(id, "path.png") — encode an image (from image_new or
9980            // image_load) to disk, alpha preserved. Returns 1 on success, -1
9981            // on failure (bad id or write error), mirroring image_load's own
9982            // -1-on-failure convention. Path resolves the same way
9983            // write_file/copy_file's outputs do: relative to the script's own
9984            // working directory (typically the app dir the launcher cd's
9985            // into), not source_dir.
9986            #[cfg(not(target_arch = "wasm32"))]
9987            "image_save" =>
9988            {
9989                let id = self.arg_num(&args, 0, -1.0)? as i64;
9990                let path = self.arg_str(&args, 1, "");
9991                if id >= 0 && (id as usize) < self.images.len() {
9992                    if let Some(parent) = std::path::Path::new(&path).parent() {
9993                        if !parent.as_os_str().is_empty() {
9994                            let _ = std::fs::create_dir_all(parent);
9995                        }
9996                    }
9997                    if self.images[id as usize].save(&path).is_ok() {
9998                        return Ok(Value::Number(1.0));
9999                    }
10000                }
10001                return Ok(Value::Number(-1.0));
10002            },
10003            #[cfg(target_arch = "wasm32")]
10004            "image_save" =>
10005            {
10006                return Ok(Value::Number(-1.0));
10007            },
10008            // image_draw(id, x, y, w, h) — blit an image (nearest-neighbour
10009            // scaled to w x h, alpha-blended against whatever's already in
10010            // the framebuffer) into the current frame. A native pixel loop,
10011            // not a .ling-level per-pixel image_pixel_*+pixel() loop: doing
10012            // this from script for even a modest thumbnail grid re-incurs
10013            // the exact per-frame interpreted-call-volume cost that made
10014            // small mosaic tiles hang the UI (see mosaic.ling's
10015            // xform_glyph_pts_fit fix) — this is the "read the framebuffer
10016            // out" direction's counterpart to screenshot().
10017            #[cfg(not(target_arch = "wasm32"))]
10018            "image_draw" =>
10019            {
10020                let id = self.arg_num(&args, 0, -1.0)? as i64;
10021                let dx = self.arg_num(&args, 1, 0.0)? as i32;
10022                let dy = self.arg_num(&args, 2, 0.0)? as i32;
10023                let dw = self.arg_num(&args, 3, 0.0)?.max(0.0) as i32;
10024                let dh = self.arg_num(&args, 4, 0.0)?.max(0.0) as i32;
10025                if id >= 0 && (id as usize) < self.images.len() && dw > 0 && dh > 0 {
10026                    let img = &self.images[id as usize];
10027                    let sw = img.width() as i32;
10028                    let sh = img.height() as i32;
10029                    if sw > 0 && sh > 0 {
10030                        let mut gfx = self.gfx.borrow_mut();
10031                        let (fw, fh) = (gfx.width as i32, gfx.height as i32);
10032                        for py in 0..dh {
10033                            let ty = dy + py;
10034                            if ty < 0 || ty >= fh {
10035                                continue;
10036                            }
10037                            let sy = (py * sh / dh).clamp(0, sh - 1) as u32;
10038                            for px in 0..dw {
10039                                let tx = dx + px;
10040                                if tx < 0 || tx >= fw {
10041                                    continue;
10042                                }
10043                                let sx = (px * sw / dw).clamp(0, sw - 1) as u32;
10044                                let p = img.get_pixel(sx, sy);
10045                                let a = p[3] as u32;
10046                                if a == 0 {
10047                                    continue;
10048                                }
10049                                let idx = ty as usize * gfx.width + tx as usize;
10050                                if a >= 255 {
10051                                    gfx.buffer[idx] =
10052                                        ((p[0] as u32) << 16) | ((p[1] as u32) << 8) | (p[2] as u32);
10053                                } else {
10054                                    let bg = gfx.buffer[idx];
10055                                    let br = (bg >> 16) & 0xff;
10056                                    let bg_g = (bg >> 8) & 0xff;
10057                                    let bb = bg & 0xff;
10058                                    let r = (p[0] as u32 * a + br * (255 - a)) / 255;
10059                                    let g = (p[1] as u32 * a + bg_g * (255 - a)) / 255;
10060                                    let b = (p[2] as u32 * a + bb * (255 - a)) / 255;
10061                                    gfx.buffer[idx] = (r << 16) | (g << 8) | b;
10062                                }
10063                            }
10064                        }
10065                    }
10066                }
10067                return Ok(Value::Unit);
10068            },
10069            #[cfg(target_arch = "wasm32")]
10070            "image_draw" =>
10071            {
10072                return Ok(Value::Unit);
10073            },
10074            // font_text(handle, x, y, px, "string") — anti-aliased *stroked* vector outline
10075            // in the current set_color / set_blend. (x,y) is the text box top-left.
10076            #[cfg(not(target_arch = "wasm32"))]
10077            "font_text" | "ข้อความฟอนต์" | "字体文本" | "フォント文字" | "글꼴텍스트" | "متن_فونت" | "نص_الخط" | "טקסט_גופן" | "فونٹ_متن" | "texte_police" | "schriftart_text" | "текст_шрифт" =>
10078            {
10079                let id = self.arg_num(&args, 0, 0.0)? as i64;
10080                let x = self.arg_num(&args, 1, 0.0)? as f32;
10081                let y = self.arg_num(&args, 2, 0.0)? as f32;
10082                let px = self.arg_num(&args, 3, 16.0)? as f32;
10083                let s = self.arg_str(&args, 4, "");
10084                if id >= 0 && (id as usize) < self.fonts.len() && px > 0.0 {
10085                    let strokes = self.font_layout_2d(id as usize, x, y, px, &s);
10086                    let mut gfx = self.gfx.borrow_mut();
10087                    let (w, h, color, add, aa) =
10088                        (gfx.width, gfx.height, gfx.color, gfx.blend == 1, gfx.font_antialias);
10089                    for pl in &strokes {
10090                        for seg in pl.windows(2) {
10091                            if aa {
10092                                crate::gfx::raster::draw_line_aa(
10093                                    &mut gfx.buffer,
10094                                    w,
10095                                    h,
10096                                    color,
10097                                    add,
10098                                    seg[0][0],
10099                                    seg[0][1],
10100                                    seg[1][0],
10101                                    seg[1][1],
10102                                );
10103                            } else {
10104                                crate::gfx::raster::draw_line(
10105                                    &mut gfx.buffer,
10106                                    w,
10107                                    h,
10108                                    color,
10109                                    seg[0][0],
10110                                    seg[0][1],
10111                                    seg[1][0],
10112                                    seg[1][1],
10113                                );
10114                            }
10115                        }
10116                    }
10117                }
10118                return Ok(Value::Unit);
10119            },
10120            #[cfg(target_arch = "wasm32")]
10121            "font_text" | "ข้อความฟอนต์" | "字体文本" | "フォント文字" | "글꼴텍스트" | "متن_فونت" | "نص_الخط" | "טקסט_גופן" | "فونٹ_متن" | "texte_police" | "schriftart_text" | "текст_шрифт" =>
10122            {
10123                return Ok(Value::Unit);
10124            },
10125            // font_text_fill(handle, x, y, px, "string") — filled vector glyphs;
10126            // anti-aliased when `set_font_antialias(1)` is on (default off = crisp).
10127            #[cfg(not(target_arch = "wasm32"))]
10128            "font_text_fill" | "เติมฟอนต์" | "填充字体" | "フォント塗り" | "글꼴채움" | "پرکردن_متن_فونت" | "تعبئة_نص_الخط" | "מילוי_טקסט_גופן" | "فونٹ_متن_بھرو" | "remplir_texte_police" | "schriftart_text_füllen" | "заполнить_текст_шрифт" =>
10129            {
10130                let id = self.arg_num(&args, 0, 0.0)? as i64;
10131                let x = self.arg_num(&args, 1, 0.0)? as f32;
10132                let y = self.arg_num(&args, 2, 0.0)? as f32;
10133                let px = self.arg_num(&args, 3, 16.0)? as f32;
10134                let s = self.arg_str(&args, 4, "");
10135                if id >= 0 && (id as usize) < self.fonts.len() && px > 0.0 {
10136                    // fill each glyph independently so interior holes (winding) stay correct
10137                    let glyphs = self.font_layout_2d_glyphs(id as usize, x, y, px, &s);
10138                    let mut gfx = self.gfx.borrow_mut();
10139                    let (w, h, color, add, aa) =
10140                        (gfx.width, gfx.height, gfx.color, gfx.blend == 1, gfx.font_antialias);
10141                    for contours in &glyphs {
10142                        if aa {
10143                            crate::gfx::raster::fill_contours_aa(
10144                                &mut gfx.buffer,
10145                                w,
10146                                h,
10147                                color,
10148                                add,
10149                                contours,
10150                            );
10151                        } else {
10152                            crate::gfx::raster::fill_contours(
10153                                &mut gfx.buffer,
10154                                w,
10155                                h,
10156                                color,
10157                                add,
10158                                contours,
10159                            );
10160                        }
10161                    }
10162                }
10163                return Ok(Value::Unit);
10164            },
10165            #[cfg(target_arch = "wasm32")]
10166            "font_text_fill" | "เติมฟอนต์" | "填充字体" | "フォント塗り" | "글꼴채움" | "پرکردن_متن_فونت" | "تعبئة_نص_الخط" | "מילוי_טקסט_גופן" | "فونٹ_متن_بھرو" | "remplir_texte_police" | "schriftart_text_füllen" | "заполнить_текст_шрифт" =>
10167            {
10168                return Ok(Value::Unit);
10169            },
10170            // font_text_3d(handle, cx,cy,cz, ux,uy,uz, vx,vy,vz, size, "string")
10171            // — stroked vector text on a 3D plane: u = advance dir, v = up dir, size = world/em.
10172            //   Flows through the depth-sorted line pipeline, so it rotates with the camera (and 4D).
10173            #[cfg(not(target_arch = "wasm32"))]
10174            "font_text_3d" | "ข้อความฟอนต์3มิติ" | "字体3D" | "フォント3D" | "글꼴3D" | "متن_فونت_سه‌بعدی" | "نص_خط_ثلاثي_الأبعاد" | "טקסט_גופן_תלת_ממדי" | "تھری_ڈی_فونٹ_متن" | "texte_police_3d" | "schriftart_text_3d" | "текст_шрифт_3d" =>
10175            {
10176                let id = self.arg_num(&args, 0, 0.0)? as i64;
10177                let cx = self.arg_num(&args, 1, 0.0)? as f32;
10178                let cy = self.arg_num(&args, 2, 0.0)? as f32;
10179                let cz = self.arg_num(&args, 3, 0.0)? as f32;
10180                let ux = self.arg_num(&args, 4, 1.0)? as f32;
10181                let uy = self.arg_num(&args, 5, 0.0)? as f32;
10182                let uz = self.arg_num(&args, 6, 0.0)? as f32;
10183                let vx = self.arg_num(&args, 7, 0.0)? as f32;
10184                let vy = self.arg_num(&args, 8, 1.0)? as f32;
10185                let vz = self.arg_num(&args, 9, 0.0)? as f32;
10186                let size = self.arg_num(&args, 10, 1.0)? as f32;
10187                let s = self.arg_str(&args, 11, "");
10188                // Optional arg 12: fill_rows — when > 0, each glyph interior is
10189                // filled with that many even-odd scanline spans (true filled
10190                // letterforms, not a bounding box). 0/omitted = outline only.
10191                let fill_rows = self.arg_num(&args, 12, 0.0)? as i32;
10192                if id >= 0 && (id as usize) < self.fonts.len() && size > 0.0 {
10193                    // Build world-space polylines: world = C + (pen+ex)*size*U + ey*size*V
10194                    let font = &mut self.fonts[id as usize];
10195                    let asc = font.ascent();
10196                    let mut pen = 0.0f32;
10197                    let mut lines: Vec<[f32; 6]> = Vec::new();
10198                    for ch in s.chars() {
10199                        let go = font.glyph_outline(ch, 0.01);
10200                        let map = |p: [f32; 2], pen: f32| {
10201                            let a = pen + p[0];
10202                            let b = p[1] - asc; // shift so the top of the cap sits near C
10203                            [
10204                                cx + a * size * ux + b * size * vx,
10205                                cy + a * size * uy + b * size * vy,
10206                                cz + a * size * uz + b * size * vz,
10207                            ]
10208                        };
10209                        for pl in &go.polylines {
10210                            for seg in pl.windows(2) {
10211                                let p0 = map(seg[0], pen);
10212                                let p1 = map(seg[1], pen);
10213                                lines.push([p0[0], p0[1], p0[2], p1[0], p1[1], p1[2]]);
10214                            }
10215                        }
10216                        if fill_rows > 0 {
10217                            // Even-odd scanline fill in glyph space. Contours may
10218                            // omit their closing edge, so the implicit last→first
10219                            // segment is scanned too (skipped when degenerate).
10220                            let (mut ymin, mut ymax) = (f32::MAX, f32::MIN);
10221                            for pl in &go.polylines {
10222                                for p in pl {
10223                                    ymin = ymin.min(p[1]);
10224                                    ymax = ymax.max(p[1]);
10225                                }
10226                            }
10227                            if ymax > ymin {
10228                                for r in 0..fill_rows {
10229                                    let y =
10230                                        ymin + (r as f32 + 0.5) * (ymax - ymin) / fill_rows as f32;
10231                                    let mut xs: Vec<f32> = Vec::new();
10232                                    for pl in &go.polylines {
10233                                        let n = pl.len();
10234                                        if n < 2 {
10235                                            continue;
10236                                        }
10237                                        for k in 0..n {
10238                                            let p0 = pl[k];
10239                                            let p1 = pl[(k + 1) % n];
10240                                            if k + 1 == n
10241                                                && (p1[0] - p0[0]).abs() < 1e-6
10242                                                && (p1[1] - p0[1]).abs() < 1e-6
10243                                            {
10244                                                continue; // contour already closed
10245                                            }
10246                                            let (y0, y1) = (p0[1], p1[1]);
10247                                            if (y0 <= y && y1 > y) || (y1 <= y && y0 > y) {
10248                                                let t = (y - y0) / (y1 - y0);
10249                                                xs.push(p0[0] + t * (p1[0] - p0[0]));
10250                                            }
10251                                        }
10252                                    }
10253                                    xs.sort_by(|a, b| {
10254                                        a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
10255                                    });
10256                                    let mut k = 0;
10257                                    while k + 1 < xs.len() {
10258                                        let a = map([xs[k], y], pen);
10259                                        let b = map([xs[k + 1], y], pen);
10260                                        lines.push([a[0], a[1], a[2], b[0], b[1], b[2]]);
10261                                        k += 2;
10262                                    }
10263                                }
10264                            }
10265                        }
10266                        pen += go.advance;
10267                    }
10268                    let mut gfx = self.gfx.borrow_mut();
10269                    let color = gfx.color;
10270                    let near = -gfx.camera.zdist + 0.05;
10271                    for l in &lines {
10272                        let (mut ax, mut ay, mut az) = (l[0], l[1], l[2]);
10273                        let (mut bx, mut by, mut bz) = (l[3], l[4], l[5]);
10274                        let da = gfx.camera.depth(ax, ay, az);
10275                        let db = gfx.camera.depth(bx, by, bz);
10276                        if da <= near && db <= near {
10277                            continue;
10278                        }
10279                        if da <= near {
10280                            let t = (near - da) / (db - da);
10281                            ax += t * (bx - ax);
10282                            ay += t * (by - ay);
10283                            az += t * (bz - az);
10284                        } else if db <= near {
10285                            let t = (near - da) / (db - da);
10286                            bx = ax + t * (bx - ax);
10287                            by = ay + t * (by - ay);
10288                            bz = az + t * (bz - az);
10289                        }
10290                        let (sax, say, da2) = gfx.camera.project(ax, ay, az);
10291                        let (sbx, sby, db2) = gfx.camera.project(bx, by, bz);
10292                        let depth = (da2 + db2) / 2.0;
10293                        gfx.depth_queue.push_line(depth, color, sax, say, sbx, sby);
10294                    }
10295                }
10296                return Ok(Value::Unit);
10297            },
10298            #[cfg(target_arch = "wasm32")]
10299            "font_text_3d" | "ข้อความฟอนต์3มิติ" | "字体3D" | "フォント3D" | "글꼴3D" | "متن_فونت_سه‌بعدی" | "نص_خط_ثلاثي_الأبعاد" | "טקסט_גופן_תלת_ממדי" | "تھری_ڈی_فونٹ_متن" | "texte_police_3d" | "schriftart_text_3d" | "текст_шрифт_3d" =>
10300            {
10301                return Ok(Value::Unit);
10302            },
10303            // font_width(handle, px, "string") — pixel width of a string in a loaded font.
10304            #[cfg(not(target_arch = "wasm32"))]
10305            "font_width" | "ความกว้างฟอนต์" | "字体宽度" | "フォント幅" | "글꼴너비" | "عرض_فونت" | "عرض_الخط" | "רוחב_גופן" | "فونٹ_چوڑائی" | "largeur_police" | "schriftart_breite" | "ширина_шрифта" =>
10306            {
10307                let id = self.arg_num(&args, 0, 0.0)? as i64;
10308                let px = self.arg_num(&args, 1, 16.0)? as f32;
10309                let s = self.arg_str(&args, 2, "");
10310                if id >= 0 && (id as usize) < self.fonts.len() {
10311                    return Ok(Value::Number(self.fonts[id as usize].measure(&s, px) as f64));
10312                }
10313                return Ok(Value::Number(0.0));
10314            },
10315            #[cfg(target_arch = "wasm32")]
10316            "font_width" | "ความกว้างฟอนต์" | "字体宽度" | "フォント幅" | "글꼴너비" | "عرض_فونت" | "عرض_الخط" | "רוחב_גופן" | "فونٹ_چوڑائی" | "largeur_police" | "schriftart_breite" | "ширина_шрифта" =>
10317            {
10318                return Ok(Value::Number(0.0));
10319            },
10320            // font_glyph_outline(handle, "char", tol_em) — flattened vector outline of
10321            // ONE glyph in normalized em space (x→right, y→up, baseline at 0). Returns a
10322            // list of contours; each contour is a flat list [x0,y0,x1,y1,…]. Curves are
10323            // subdivided so deviation stays under tol_em (default 0.01). Empty on failure.
10324            #[cfg(not(target_arch = "wasm32"))]
10325            "font_glyph_outline" | "font_outline" | "เส้นขอบฟอนต์" | "字体轮廓"
10326            | "フォント輪郭" | "글꼴윤곽" | "خط‌دور_نویسه_فونت" | "حدود_حرف_الخط" | "קו_מתאר_גליף" | "فونٹ_گلف_آؤٹ_لائن" => {
10327                let id = self.arg_num(&args, 0, 0.0)? as i64;
10328                let s = self.arg_str(&args, 1, "");
10329                let tol = self.arg_num(&args, 2, 0.01)? as f32;
10330                let ch = s.chars().next().unwrap_or(' ');
10331                if id >= 0 && (id as usize) < self.fonts.len() {
10332                    let go = self.fonts[id as usize].glyph_outline(ch, tol.max(1e-4));
10333                    let mut contours: Vec<Value> = Vec::with_capacity(go.polylines.len());
10334                    for pl in &go.polylines {
10335                        let mut flat: Vec<Value> = Vec::with_capacity(pl.len() * 2);
10336                        for p in pl {
10337                            flat.push(Value::Number(p[0] as f64));
10338                            flat.push(Value::Number(p[1] as f64));
10339                        }
10340                        contours.push(Value::List(Rc::new(flat)));
10341                    }
10342                    return Ok(Value::List(Rc::new(contours)));
10343                }
10344                return Ok(Value::List(Rc::new(vec![])));
10345            },
10346            #[cfg(target_arch = "wasm32")]
10347            "font_glyph_outline" | "font_outline" | "เส้นขอบฟอนต์" | "字体轮廓"
10348            | "フォント輪郭" | "글꼴윤곽" | "خط‌دور_نویسه_فونت" | "حدود_حرف_الخط" | "קו_מתאר_גליף" | "فونٹ_گلف_آؤٹ_لائن" => {
10349                return Ok(Value::List(Rc::new(vec![])));
10350            },
10351            // font_advance(handle, "char") — normalized em advance width of ONE glyph
10352            // (baseline metric, ignores side bearings). Multiply by px for pixels.
10353            #[cfg(not(target_arch = "wasm32"))]
10354            "font_advance" | "ระยะฟอนต์" | "字体步进" | "フォント送り" | "글꼴전진" | "پیشروی_فونت" | "تقدم_الخط" | "קידום_גופן" | "فونٹ_ایڈوانس" => {
10355                let id = self.arg_num(&args, 0, 0.0)? as i64;
10356                let s = self.arg_str(&args, 1, "");
10357                let ch = s.chars().next().unwrap_or(' ');
10358                if id >= 0 && (id as usize) < self.fonts.len() {
10359                    return Ok(Value::Number(self.fonts[id as usize].advance(ch) as f64));
10360                }
10361                return Ok(Value::Number(0.0));
10362            },
10363            #[cfg(target_arch = "wasm32")]
10364            "font_advance" | "ระยะฟอนต์" | "字体步进" | "フォント送り" | "글꼴전진" | "پیشروی_فونت" | "تقدم_الخط" | "קידום_גופן" | "فونٹ_ایڈوانس" => {
10365                return Ok(Value::Number(0.0));
10366            },
10367
10368            // ui_frame(x,y,w,h, bracketLen) — sci-fi corner brackets
10369            "ui_frame" | "边框" | "フレーム枠" | "프레임틀" | "กรอบ" | "قاب_رابط" | "إطار_الواجهة" | "מסגרת_ממשק" | "یو_آئی_فریم" | "cadre_ui" | "ui_rahmen" | "ui_рамка" => {
10370                let x = self.arg_num(&args, 0, 0.0)? as f32;
10371                let y = self.arg_num(&args, 1, 0.0)? as f32;
10372                let w0 = self.arg_num(&args, 2, 0.0)? as f32;
10373                let h0 = self.arg_num(&args, 3, 0.0)? as f32;
10374                let l = self.arg_num(&args, 4, 14.0)? as f32;
10375                let segs = ling_ui::holo::corner_brackets(x, y, w0, h0, l);
10376                let mut gfx = self.gfx.borrow_mut();
10377                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
10378                for sg in segs {
10379                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
10380                }
10381                return Ok(Value::Unit);
10382            },
10383            // ui_bevel(x,y,w,h, bevel) — beveled holographic panel outline
10384            "ui_bevel" | "斜角框" | "ベベル枠" | "베벨틀" | "กรอบเฉียง" | "لبه_شیبدار" | "حافة_مشطوفة" | "מסגרת_משופעת" | "یو_آئی_بیول" | "biseau_ui" | "ui_fase" | "ui_фаска" =>
10385            {
10386                let x = self.arg_num(&args, 0, 0.0)? as f32;
10387                let y = self.arg_num(&args, 1, 0.0)? as f32;
10388                let w0 = self.arg_num(&args, 2, 0.0)? as f32;
10389                let h0 = self.arg_num(&args, 3, 0.0)? as f32;
10390                let bv = self.arg_num(&args, 4, 10.0)? as f32;
10391                let segs = ling_ui::holo::beveled_rect(x, y, w0, h0, bv);
10392                let mut gfx = self.gfx.borrow_mut();
10393                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
10394                for sg in segs {
10395                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
10396                }
10397                return Ok(Value::Unit);
10398            },
10399
10400            // ══════════════════════════════════════════════════════════════════
10401            // VECTOR UI TOOLKIT  (crates/ling-ui/src/widgets.rs)
10402            // All widgets are vector + theme-coloured with an optional trailing
10403            // r,g,b override; interactive ones read the mouse and return state.
10404            // ══════════════════════════════════════════════════════════════════
10405            #[cfg(not(target_arch = "wasm32"))]
10406            "ui_theme" | "界面主题" | "UIテーマ" | "인터페이스테마" | "ธีมส่วนติดต่อ" | "پوسته_رابط" | "سمة_الواجهة" | "ערכת_נושא" | "یو_آئی_تھیم" | "thème_ui" | "ui_thema" | "ui_тема" =>
10407            {
10408                let cur = self.ui_theme;
10409                let primary = self.color_at(&args, 0, cur.primary);
10410                let accent = self.color_at(&args, 3, cur.accent);
10411                let track = self.color_at(&args, 6, cur.track);
10412                let warn = self.color_at(&args, 9, cur.warn);
10413                let text = self.color_at(&args, 12, cur.text);
10414                let bg = self.color_at(&args, 15, cur.bg);
10415                self.ui_theme = UiTheme { primary, accent, track, warn, text, bg };
10416                return Ok(Value::Unit);
10417            },
10418
10419            // ui_theme_colors() -> [pr,pg,pb, ar,ag,ab, tr,tg,tb, wr,wg,wb,
10420            // xr,xg,xb, br,bg,bb] — the live theme every ui_* widget already
10421            // draws from (primary/accent/track/warn/text/bg, each 0-255),
10422            // so script-drawn UI (e.g. a hand-rolled text field) can match it
10423            // instead of guessing its own colours.
10424            "ui_theme_colors" | "인터페이스테마색상" => {
10425                let th = self.ui_theme;
10426                let mut out = Vec::with_capacity(18);
10427                for c in [th.primary, th.accent, th.track, th.warn, th.text, th.bg] {
10428                    out.push(Value::Number(((c >> 16) & 0xFF) as f64));
10429                    out.push(Value::Number(((c >> 8) & 0xFF) as f64));
10430                    out.push(Value::Number((c & 0xFF) as f64));
10431                }
10432                return Ok(Value::List(Rc::new(out)));
10433            },
10434
10435            // ── HUD ──────────────────────────────────────────────────────────
10436            #[cfg(not(target_arch = "wasm32"))]
10437            "ui_radar" | "雷达" | "レーダー" | "레이더" | "เรดาร์" | "رادار_رابط" | "رادار_الواجهة" | "מכ״ם_ממשק" | "یو_آئی_ریڈار" | "radar_ui" | "ui_радар" => {
10438                let cx = self.arg_num(&args, 0, 0.)? as f32;
10439                let cy = self.arg_num(&args, 1, 0.)? as f32;
10440                let r = self.arg_num(&args, 2, 60.)? as f32;
10441                let sweep = self.arg_num(&args, 3, 0.)? as f32;
10442                let th = self.ui_theme;
10443                let prim = self.color_at(&args, 4, th.primary);
10444                self.draw_ui(&ling_ui::widgets::radar(
10445                    cx, cy, r, sweep, prim, th.accent, th.track,
10446                ));
10447                return Ok(Value::Unit);
10448            },
10449            #[cfg(not(target_arch = "wasm32"))]
10450            "ui_compass" | "罗盘" | "コンパス" | "나침반" | "เข็มทิศ" | "قطب‌نمای_رابط" | "بوصلة_الواجهة" | "מצפן_ממשק" | "یو_آئی_قطب_نما" | "boussole_ui" | "ui_kompass" | "ui_компас" => {
10451                let x = self.arg_num(&args, 0, 0.)? as f32;
10452                let y = self.arg_num(&args, 1, 0.)? as f32;
10453                let w0 = self.arg_num(&args, 2, 300.)? as f32;
10454                let h0 = self.arg_num(&args, 3, 24.)? as f32;
10455                let head = self.arg_num(&args, 4, 0.)? as f32;
10456                let th = self.ui_theme;
10457                let prim = self.color_at(&args, 5, th.primary);
10458                self.draw_ui(&ling_ui::widgets::compass(
10459                    x, y, w0, h0, head, prim, th.track,
10460                ));
10461                return Ok(Value::Unit);
10462            },
10463            #[cfg(not(target_arch = "wasm32"))]
10464            "ui_reticle" | "准星" | "照準" | "조준선" | "เป้าเล็ง" | "نشانه_رابط" | "علامة_تصويب" | "כוונת" | "نشانہ" | "réticule_ui" | "ui_fadenkreuz" | "ui_прицел" => {
10465                let cx = self.arg_num(&args, 0, 0.)? as f32;
10466                let cy = self.arg_num(&args, 1, 0.)? as f32;
10467                let r = self.arg_num(&args, 2, 30.)? as f32;
10468                let spread = self.arg_num(&args, 3, 0.)? as f32;
10469                let th = self.ui_theme;
10470                let prim = self.color_at(&args, 4, th.primary);
10471                self.draw_ui(&ling_ui::widgets::reticle(cx, cy, r, spread, prim));
10472                return Ok(Value::Unit);
10473            },
10474            #[cfg(not(target_arch = "wasm32"))]
10475            "ui_target" | "锁定框" | "ターゲット" | "표적" | "กรอบเป้า" | "قاب_هدف" | "إطار_الهدف" | "מסגרת_מטרה" | "ہدف_فریم" | "cible_ui" | "ui_ziel" | "ui_цель" =>
10476            {
10477                let x = self.arg_num(&args, 0, 0.)? as f32;
10478                let y = self.arg_num(&args, 1, 0.)? as f32;
10479                let w0 = self.arg_num(&args, 2, 80.)? as f32;
10480                let h0 = self.arg_num(&args, 3, 80.)? as f32;
10481                let lock = self.arg_num(&args, 4, 0.)? as f32;
10482                let th = self.ui_theme;
10483                let prim = self.color_at(&args, 5, th.primary);
10484                self.draw_ui(&ling_ui::widgets::target(
10485                    x, y, w0, h0, lock, prim, th.accent,
10486                ));
10487                return Ok(Value::Unit);
10488            },
10489            #[cfg(not(target_arch = "wasm32"))]
10490            "ui_panel" | "面板" | "パネル" | "패널" | "แผง" | "پنل_رابط" | "لوحة_الواجهة" | "לוח_ממשק" | "یو_آئی_پینل" | "panneau_ui" | "ui_feld" | "ui_панель" => {
10491                let x = self.arg_num(&args, 0, 0.)? as f32;
10492                let y = self.arg_num(&args, 1, 0.)? as f32;
10493                let w0 = self.arg_num(&args, 2, 200.)? as f32;
10494                let h0 = self.arg_num(&args, 3, 120.)? as f32;
10495                let bv = self.arg_num(&args, 4, 12.)? as f32;
10496                let th = self.ui_theme;
10497                let prim = self.color_at(&args, 5, th.primary);
10498                self.draw_ui(&ling_ui::widgets::panel(x, y, w0, h0, bv, prim, th.bg));
10499                return Ok(Value::Unit);
10500            },
10501            #[cfg(not(target_arch = "wasm32"))]
10502            "ui_scanlines" | "扫描线" | "走査線" | "스캔라인" | "เส้นสแกน" | "خطوط_اسکن" | "خطوط_المسح" | "קווי_סריקה" | "اسکین_لائنز" | "lignes_balayage_ui" | "ui_abtastzeilen" | "ui_линии_развёртки" =>
10503            {
10504                let x = self.arg_num(&args, 0, 0.)? as f32;
10505                let y = self.arg_num(&args, 1, 0.)? as f32;
10506                let w0 = self.arg_num(&args, 2, 200.)? as f32;
10507                let h0 = self.arg_num(&args, 3, 120.)? as f32;
10508                let dens = self.arg_num(&args, 4, 24.)? as usize;
10509                let th = self.ui_theme;
10510                let line = self.color_at(&args, 5, th.track);
10511                self.draw_ui(&ling_ui::widgets::scanlines(x, y, w0, h0, dens, line));
10512                return Ok(Value::Unit);
10513            },
10514
10515            // ── Meters ───────────────────────────────────────────────────────
10516            #[cfg(not(target_arch = "wasm32"))]
10517            "ui_bar" | "进度条" | "バー" | "막대" | "แถบ" | "نوار_رابط" | "شريط_الواجهة" | "סרגל_ממשק" | "یو_آئی_بار" | "barre_ui" | "ui_leiste" | "ui_полоса" => {
10518                let x = self.arg_num(&args, 0, 0.)? as f32;
10519                let y = self.arg_num(&args, 1, 0.)? as f32;
10520                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10521                let h0 = self.arg_num(&args, 3, 16.)? as f32;
10522                let val = self.arg_num(&args, 4, 0.)? as f32;
10523                let max = self.arg_num(&args, 5, 1.)? as f32;
10524                let th = self.ui_theme;
10525                let fill = self.color_at(&args, 6, th.primary);
10526                self.draw_ui(&ling_ui::widgets::bar(
10527                    x,
10528                    y,
10529                    w0,
10530                    h0,
10531                    val / max.max(1e-6),
10532                    fill,
10533                    th.track,
10534                ));
10535                return Ok(Value::Unit);
10536            },
10537            #[cfg(not(target_arch = "wasm32"))]
10538            "ui_segbar" | "分段条" | "分割バー" | "분할막대" | "แถบแบ่ง" | "نوار_قطعه‌ای" | "شريط_مقسم" | "סרגל_מקוטע" | "سیگمنٹ_بار" | "barre_segmentée_ui" | "ui_segmentleiste" | "ui_сегментная_полоса" =>
10539            {
10540                let x = self.arg_num(&args, 0, 0.)? as f32;
10541                let y = self.arg_num(&args, 1, 0.)? as f32;
10542                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10543                let h0 = self.arg_num(&args, 3, 16.)? as f32;
10544                let val = self.arg_num(&args, 4, 0.)? as f32;
10545                let max = self.arg_num(&args, 5, 1.)? as f32;
10546                let segs = self.arg_num(&args, 6, 10.)? as usize;
10547                let th = self.ui_theme;
10548                let fill = self.color_at(&args, 7, th.primary);
10549                self.draw_ui(&ling_ui::widgets::segbar(
10550                    x,
10551                    y,
10552                    w0,
10553                    h0,
10554                    val / max.max(1e-6),
10555                    segs,
10556                    fill,
10557                    th.track,
10558                ));
10559                return Ok(Value::Unit);
10560            },
10561            #[cfg(not(target_arch = "wasm32"))]
10562            "ui_gauge" | "仪表" | "ゲージ" | "게이지" | "มาตรวัด" | "گیج_رابط" | "مقياس_الواجهة" | "מד_ממשק" | "یو_آئی_گیج" | "jauge_ui" | "ui_anzeige" | "ui_индикатор" => {
10563                let cx = self.arg_num(&args, 0, 0.)? as f32;
10564                let cy = self.arg_num(&args, 1, 0.)? as f32;
10565                let r = self.arg_num(&args, 2, 50.)? as f32;
10566                let val = self.arg_num(&args, 3, 0.)? as f32;
10567                let max = self.arg_num(&args, 4, 1.)? as f32;
10568                let th = self.ui_theme;
10569                let needle = self.color_at(&args, 5, th.warn);
10570                self.draw_ui(&ling_ui::widgets::gauge(
10571                    cx,
10572                    cy,
10573                    r,
10574                    val / max.max(1e-6),
10575                    needle,
10576                    th.accent,
10577                    th.track,
10578                ));
10579                return Ok(Value::Unit);
10580            },
10581            #[cfg(not(target_arch = "wasm32"))]
10582            "ui_ring" | "环表" | "リングメーター" | "링미터" | "วงแหวนวัด" | "حلقه_گیج" | "حلقة_قياس" | "טבעת_מד" | "رنگ_گیج" | "anneau_ui" | "ui_кольцо" =>
10583            {
10584                let cx = self.arg_num(&args, 0, 0.)? as f32;
10585                let cy = self.arg_num(&args, 1, 0.)? as f32;
10586                let r = self.arg_num(&args, 2, 40.)? as f32;
10587                let val = self.arg_num(&args, 3, 0.)? as f32;
10588                let max = self.arg_num(&args, 4, 1.)? as f32;
10589                let th = self.ui_theme;
10590                let fill = self.color_at(&args, 5, th.primary);
10591                self.draw_ui(&ling_ui::widgets::ring(
10592                    cx,
10593                    cy,
10594                    r,
10595                    val / max.max(1e-6),
10596                    fill,
10597                    th.track,
10598                ));
10599                return Ok(Value::Unit);
10600            },
10601            #[cfg(not(target_arch = "wasm32"))]
10602            "ui_vu" | "音量条" | "VUメーター" | "음량막대" | "มาตรเสียง" | "گیج_صدا" | "مقياس_مستوى_الصوت" | "מד_עוצמה" | "وی_یو_میٹر" | "vumètre_ui" | "ui_vumeter" | "ui_вю_метр" =>
10603            {
10604                let x = self.arg_num(&args, 0, 0.)? as f32;
10605                let y = self.arg_num(&args, 1, 0.)? as f32;
10606                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10607                let h0 = self.arg_num(&args, 3, 60.)? as f32;
10608                let levels = self.arg_list_f32(&args, 4);
10609                let th = self.ui_theme;
10610                let fill = self.color_at(&args, 5, th.primary);
10611                self.draw_ui(&ling_ui::widgets::vu(x, y, w0, h0, &levels, fill, th.warn));
10612                return Ok(Value::Unit);
10613            },
10614            #[cfg(not(target_arch = "wasm32"))]
10615            "ui_spark" | "迷你图" | "スパークライン" | "스파크라인" | "กราฟจิ๋ว" | "نمودار_ریز" | "رسم_مصغر" | "גרף_זעיר" | "اسپارک_لائن" | "mini_graphe_ui" | "ui_sparkline" | "ui_мини_график" =>
10616            {
10617                let x = self.arg_num(&args, 0, 0.)? as f32;
10618                let y = self.arg_num(&args, 1, 0.)? as f32;
10619                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10620                let h0 = self.arg_num(&args, 3, 40.)? as f32;
10621                let vals = self.arg_list_f32(&args, 4);
10622                let th = self.ui_theme;
10623                let line = self.color_at(&args, 5, th.accent);
10624                self.draw_ui(&ling_ui::widgets::spark(x, y, w0, h0, &vals, line));
10625                return Ok(Value::Unit);
10626            },
10627            #[cfg(not(target_arch = "wasm32"))]
10628            "ui_battery" | "电池" | "バッテリー" | "배터리" | "แบตเตอรี่" | "نشانگر_باتری" | "مؤشر_البطارية" | "מחוון_סוללה" | "بیٹری_انڈیکیٹر" | "batterie_ui" | "ui_batterie" | "ui_батарея" =>
10629            {
10630                let x = self.arg_num(&args, 0, 0.)? as f32;
10631                let y = self.arg_num(&args, 1, 0.)? as f32;
10632                let w0 = self.arg_num(&args, 2, 50.)? as f32;
10633                let h0 = self.arg_num(&args, 3, 22.)? as f32;
10634                let val = self.arg_num(&args, 4, 1.)? as f32;
10635                let max = self.arg_num(&args, 5, 1.)? as f32;
10636                let th = self.ui_theme;
10637                let fill = self.color_at(&args, 6, th.accent);
10638                self.draw_ui(&ling_ui::widgets::battery(
10639                    x,
10640                    y,
10641                    w0,
10642                    h0,
10643                    val / max.max(1e-6),
10644                    fill,
10645                    th.track,
10646                    th.warn,
10647                ));
10648                return Ok(Value::Unit);
10649            },
10650
10651            // ── Interface controls (interactive → return state) ──────────────
10652            #[cfg(not(target_arch = "wasm32"))]
10653            "ui_button" | "按钮" | "ボタン" | "버튼" | "ปุ่ม" | "دکمه_رابط" | "زر_الواجهة" | "כפתור_ממשק" | "یو_آئی_بٹن" | "bouton_ui" | "ui_knopf" | "ui_кнопка" => {
10654                let x = self.arg_num(&args, 0, 0.)? as f32;
10655                let y = self.arg_num(&args, 1, 0.)? as f32;
10656                let w0 = self.arg_num(&args, 2, 120.)? as f32;
10657                let h0 = self.arg_num(&args, 3, 40.)? as f32;
10658                let (mx, my, down) = self.mouse_now();
10659                let hover = ling_ui::holo::hit_rect(mx, my, x, y, w0, h0);
10660                let clicked = hover && down && !self.mouse_was_down;
10661                let th = self.ui_theme;
10662                let prim = self.color_at(&args, 4, th.primary);
10663                self.draw_ui(&ling_ui::widgets::button(
10664                    x,
10665                    y,
10666                    w0,
10667                    h0,
10668                    hover,
10669                    down && hover,
10670                    prim,
10671                    th.bg,
10672                ));
10673                return Ok(Value::Number(if clicked { 1.0 } else { 0.0 }));
10674            },
10675            #[cfg(not(target_arch = "wasm32"))]
10676            "ui_toggle" | "开关" | "トグル" | "토글" | "สวิตช์" | "کلید_ضامن" | "مفتاح_تبديل" | "מתג" | "ٹوگل" | "bascule_ui" | "ui_schalter" | "ui_переключатель" => {
10677                let x = self.arg_num(&args, 0, 0.)? as f32;
10678                let y = self.arg_num(&args, 1, 0.)? as f32;
10679                let w0 = self.arg_num(&args, 2, 52.)? as f32;
10680                let h0 = self.arg_num(&args, 3, 24.)? as f32;
10681                let mut state = self.arg_num(&args, 4, 0.)? > 0.5;
10682                let (mx, my, down) = self.mouse_now();
10683                let hover = ling_ui::holo::hit_rect(mx, my, x, y, w0, h0);
10684                if hover && down && !self.mouse_was_down {
10685                    state = !state;
10686                }
10687                let th = self.ui_theme;
10688                let on = self.color_at(&args, 5, th.accent);
10689                self.draw_ui(&ling_ui::widgets::toggle(x, y, w0, h0, state, on, th.track));
10690                return Ok(Value::Number(if state { 1.0 } else { 0.0 }));
10691            },
10692            #[cfg(not(target_arch = "wasm32"))]
10693            "ui_slider" | "滑块" | "スライダー" | "슬라이더" | "แถบเลื่อน" | "لغزنده" | "شريط_انزلاق" | "מחוון_החלקה" | "سلائیڈر" | "curseur_ui" | "ui_schieberegler" | "ui_ползунок" =>
10694            {
10695                let x = self.arg_num(&args, 0, 0.)? as f32;
10696                let y = self.arg_num(&args, 1, 0.)? as f32;
10697                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10698                let mut val = self.arg_num(&args, 3, 0.)? as f32;
10699                let mn = self.arg_num(&args, 4, 0.)? as f32;
10700                let mx_ = self.arg_num(&args, 5, 1.)? as f32;
10701                let (mx, my, down) = self.mouse_now();
10702                let hover = ling_ui::holo::hit_rect(mx, my, x - 8.0, y - 10.0, w0 + 16.0, 20.0);
10703                if hover && down {
10704                    let frac = ((mx - x) / w0).clamp(0.0, 1.0);
10705                    val = mn + (mx_ - mn) * frac;
10706                }
10707                let frac = ((val - mn) / (mx_ - mn).abs().max(1e-6)).clamp(0.0, 1.0);
10708                let th = self.ui_theme;
10709                let fill = self.color_at(&args, 6, th.primary);
10710                self.draw_ui(&ling_ui::widgets::slider(
10711                    x, y, w0, frac, hover, fill, th.track,
10712                ));
10713                return Ok(Value::Number(val as f64));
10714            },
10715            #[cfg(not(target_arch = "wasm32"))]
10716            "ui_checkbox" | "复选框" | "チェックボックス" | "체크박스" | "ช่องเลือก" | "جعبه_علامت" | "مربع_اختيار" | "תיבת_סימון" | "چیک_باکس" | "case_cocher_ui" | "ui_kontrollkästchen" | "ui_флажок" =>
10717            {
10718                let x = self.arg_num(&args, 0, 0.)? as f32;
10719                let y = self.arg_num(&args, 1, 0.)? as f32;
10720                let s = self.arg_num(&args, 2, 20.)? as f32;
10721                let mut checked = self.arg_num(&args, 3, 0.)? > 0.5;
10722                let (mx, my, down) = self.mouse_now();
10723                let hover = ling_ui::holo::hit_rect(mx, my, x, y, s, s);
10724                if hover && down && !self.mouse_was_down {
10725                    checked = !checked;
10726                }
10727                let th = self.ui_theme;
10728                let prim = self.color_at(&args, 4, th.primary);
10729                self.draw_ui(&ling_ui::widgets::checkbox(
10730                    x, y, s, checked, hover, prim, th.track,
10731                ));
10732                return Ok(Value::Number(if checked { 1.0 } else { 0.0 }));
10733            },
10734            #[cfg(not(target_arch = "wasm32"))]
10735            "ui_tabs" | "标签页" | "タブ" | "탭" | "แท็บ" | "برگه‌ها" | "ألسنة_الواجهة" | "לשוניות" | "ٹیبز" | "onglets_ui" | "ui_reiter" | "ui_вкладки" => {
10736                let x = self.arg_num(&args, 0, 0.)? as f32;
10737                let y = self.arg_num(&args, 1, 0.)? as f32;
10738                let w0 = self.arg_num(&args, 2, 240.)? as f32;
10739                let h0 = self.arg_num(&args, 3, 28.)? as f32;
10740                let count = self.arg_num(&args, 4, 3.)? as usize;
10741                let mut active = self.arg_num(&args, 5, 0.)? as i32;
10742                let (mx, my, down) = self.mouse_now();
10743                let mut hover = -1;
10744                if my >= y && my <= y + h0 && mx >= x && mx <= x + w0 && count > 0 {
10745                    hover = (((mx - x) / (w0 / count as f32)) as i32)
10746                        .max(0)
10747                        .min(count as i32 - 1);
10748                    if down && !self.mouse_was_down {
10749                        active = hover;
10750                    }
10751                }
10752                let th = self.ui_theme;
10753                let prim = self.color_at(&args, 6, th.primary);
10754                self.draw_ui(&ling_ui::widgets::tabs(
10755                    x,
10756                    y,
10757                    w0,
10758                    h0,
10759                    count,
10760                    active as usize,
10761                    hover,
10762                    prim,
10763                    th.track,
10764                ));
10765                return Ok(Value::Number(active as f64));
10766            },
10767            #[cfg(not(target_arch = "wasm32"))]
10768            "ui_progress" | "进度" | "プログレス" | "진행바" | "ความคืบหน้า" | "نوار_پیشرفت" | "شريط_التقدم" | "פס_התקדמות" | "پیش_رفت_بار" | "progression_ui" | "ui_fortschritt" | "ui_прогресс" =>
10769            {
10770                let x = self.arg_num(&args, 0, 0.)? as f32;
10771                let y = self.arg_num(&args, 1, 0.)? as f32;
10772                let w0 = self.arg_num(&args, 2, 200.)? as f32;
10773                let h0 = self.arg_num(&args, 3, 12.)? as f32;
10774                let frac = self.arg_num(&args, 4, 0.)? as f32;
10775                let th = self.ui_theme;
10776                let fill = self.color_at(&args, 5, th.accent);
10777                self.draw_ui(&ling_ui::widgets::progress(
10778                    x, y, w0, h0, frac, fill, th.track,
10779                ));
10780                return Ok(Value::Unit);
10781            },
10782            #[cfg(not(target_arch = "wasm32"))]
10783            "ui_tooltip" | "提示框" | "ツールチップ" | "툴팁" | "คำแนะนำ" | "راهنمای_شناور" | "تلميح_الواجهة" | "חלונית_עזרה" | "ٹول_ٹپ" | "infobulle_ui" | "ui_подсказка" =>
10784            {
10785                let x = self.arg_num(&args, 0, 0.)? as f32;
10786                let y = self.arg_num(&args, 1, 0.)? as f32;
10787                let w0 = self.arg_num(&args, 2, 120.)? as f32;
10788                let h0 = self.arg_num(&args, 3, 28.)? as f32;
10789                let th = self.ui_theme;
10790                let prim = self.color_at(&args, 4, th.primary);
10791                self.draw_ui(&ling_ui::widgets::tooltip(x, y, w0, h0, prim, th.bg));
10792                return Ok(Value::Unit);
10793            },
10794            #[cfg(not(target_arch = "wasm32"))]
10795            "ui_stepper" | "步进器" | "ステッパー" | "스테퍼" | "ตัวปรับค่า" | "پله‌گر" | "زر_خطوات" | "בורר_מדורג" | "اسٹیپر" | "pas_à_pas_ui" | "ui_schrittsteuerung" | "ui_степпер" =>
10796            {
10797                let x = self.arg_num(&args, 0, 0.)? as f32;
10798                let y = self.arg_num(&args, 1, 0.)? as f32;
10799                let w0 = self.arg_num(&args, 2, 120.)? as f32;
10800                let h0 = self.arg_num(&args, 3, 28.)? as f32;
10801                let mut val = self.arg_num(&args, 4, 0.)? as f32;
10802                let step = self.arg_num(&args, 5, 1.)? as f32;
10803                let (mx, my, down) = self.mouse_now();
10804                let hm = ling_ui::holo::hit_rect(mx, my, x, y, h0, h0);
10805                let hp = ling_ui::holo::hit_rect(mx, my, x + w0 - h0, y, h0, h0);
10806                if down && !self.mouse_was_down {
10807                    if hm {
10808                        val -= step;
10809                    }
10810                    if hp {
10811                        val += step;
10812                    }
10813                }
10814                let th = self.ui_theme;
10815                let prim = self.color_at(&args, 6, th.primary);
10816                self.draw_ui(&ling_ui::widgets::stepper(
10817                    x, y, w0, h0, hm, hp, prim, th.track,
10818                ));
10819                return Ok(Value::Number(val as f64));
10820            },
10821
10822            // ── Game UI ──────────────────────────────────────────────────────
10823            #[cfg(not(target_arch = "wasm32"))]
10824            "ui_healthbar" | "血条" | "体力バー" | "체력바" | "แถบพลังชีวิต" | "نوار_سلامتی" | "شريط_الصحة" | "פס_בריאות" | "ہیلتھ_بار" | "barre_vie_ui" | "ui_lebensbalken" | "ui_полоса_здоровья" =>
10825            {
10826                let x = self.arg_num(&args, 0, 0.)? as f32;
10827                let y = self.arg_num(&args, 1, 0.)? as f32;
10828                let w0 = self.arg_num(&args, 2, 180.)? as f32;
10829                let h0 = self.arg_num(&args, 3, 16.)? as f32;
10830                let val = self.arg_num(&args, 4, 1.)? as f32;
10831                let max = self.arg_num(&args, 5, 1.)? as f32;
10832                let pulse = self.arg_num(&args, 6, 0.)? as f32;
10833                let th = self.ui_theme;
10834                let full = self.color_at(&args, 7, th.accent);
10835                self.draw_ui(&ling_ui::widgets::healthbar(
10836                    x,
10837                    y,
10838                    w0,
10839                    h0,
10840                    val / max.max(1e-6),
10841                    pulse,
10842                    full,
10843                    th.warn,
10844                    th.track,
10845                ));
10846                return Ok(Value::Unit);
10847            },
10848            #[cfg(not(target_arch = "wasm32"))]
10849            "ui_cooldown" | "冷却" | "クールダウン" | "쿨다운" | "คูลดาวน์" | "زمان_خنک‌سازی" | "مؤقت_التهدئة" | "זמן_קירור" | "کول_ڈاؤن" | "recharge_ui" | "ui_abklingzeit" | "ui_перезарядка" =>
10850            {
10851                let cx = self.arg_num(&args, 0, 0.)? as f32;
10852                let cy = self.arg_num(&args, 1, 0.)? as f32;
10853                let r = self.arg_num(&args, 2, 28.)? as f32;
10854                let frac = self.arg_num(&args, 3, 0.)? as f32;
10855                let th = self.ui_theme;
10856                let fill = self.color_at(&args, 4, th.primary);
10857                self.draw_ui(&ling_ui::widgets::cooldown(cx, cy, r, frac, fill, th.track));
10858                return Ok(Value::Unit);
10859            },
10860            #[cfg(not(target_arch = "wasm32"))]
10861            "ui_counter" | "计数器" | "カウンター" | "카운터" | "ตัวนับ" | "شمارشگر" | "عداد_الواجهة" | "מונה_ממשק" | "کاؤنٹر" | "compteur_ui" | "ui_zähler" | "ui_счётчик" => {
10862                let x = self.arg_num(&args, 0, 0.)? as f32;
10863                let y = self.arg_num(&args, 1, 0.)? as f32;
10864                let dw = self.arg_num(&args, 2, 14.)? as f32;
10865                let dh = self.arg_num(&args, 3, 24.)? as f32;
10866                let val = self.arg_num(&args, 4, 0.)? as i64;
10867                let digits = self.arg_num(&args, 5, 4.)? as usize;
10868                let th = self.ui_theme;
10869                let on = self.color_at(&args, 6, th.primary);
10870                let off = ling_ui::widgets::shade(th.track, 0.5);
10871                self.draw_ui(&ling_ui::widgets::counter(
10872                    x, y, dw, dh, val, digits, on, off,
10873                ));
10874                return Ok(Value::Unit);
10875            },
10876            #[cfg(not(target_arch = "wasm32"))]
10877            "ui_minimap" | "小地图" | "ミニマップ" | "미니맵" | "แผนที่ย่อ" | "نقشه_کوچک" | "خريطة_مصغرة" | "מפה_מוקטנת" | "منی_میپ" | "minicarte_ui" | "ui_minikarte" | "ui_миникарта" =>
10878            {
10879                let x = self.arg_num(&args, 0, 0.)? as f32;
10880                let y = self.arg_num(&args, 1, 0.)? as f32;
10881                let w0 = self.arg_num(&args, 2, 140.)? as f32;
10882                let h0 = self.arg_num(&args, 3, 140.)? as f32;
10883                let th = self.ui_theme;
10884                let prim = self.color_at(&args, 4, th.primary);
10885                self.draw_ui(&ling_ui::widgets::minimap(x, y, w0, h0, prim, th.bg));
10886                return Ok(Value::Unit);
10887            },
10888            #[cfg(not(target_arch = "wasm32"))]
10889            "ui_dpad" | "方向键" | "方向パッド" | "방향패드" | "ปุ่มทิศทาง" | "دسته_جهت‌دار" | "لوحة_الاتجاهات" | "לוח_כיוונים" | "ڈی_پیڈ" | "croix_direction_ui" | "ui_steuerkreuz" | "ui_крестовина" =>
10890            {
10891                let cx = self.arg_num(&args, 0, 0.)? as f32;
10892                let cy = self.arg_num(&args, 1, 0.)? as f32;
10893                let r = self.arg_num(&args, 2, 50.)? as f32;
10894                let (mx, my, down) = self.mouse_now();
10895                let mut dir = 0;
10896                if down {
10897                    let (dx, dy) = (mx - cx, my - cy);
10898                    if dx * dx + dy * dy <= r * r {
10899                        if dx.abs() > dy.abs() {
10900                            dir = if dx > 0.0 { 2 } else { 4 };
10901                        } else {
10902                            dir = if dy > 0.0 { 3 } else { 1 };
10903                        }
10904                    }
10905                }
10906                let th = self.ui_theme;
10907                let prim = self.color_at(&args, 3, th.primary);
10908                self.draw_ui(&ling_ui::widgets::dpad(cx, cy, r, dir, prim, th.track));
10909                return Ok(Value::Number(dir as f64));
10910            },
10911            #[cfg(not(target_arch = "wasm32"))]
10912            "ui_slotgrid" | "物品格" | "スロットグリッド" | "슬롯격자" | "ช่องไอเทม" | "شبکه_شیار" | "شبكة_الفتحات" | "רשת_חריצים" | "سلاٹ_گرڈ" | "grille_emplacements_ui" | "ui_slotraster" | "ui_сетка_слотов" =>
10913            {
10914                let x = self.arg_num(&args, 0, 0.)? as f32;
10915                let y = self.arg_num(&args, 1, 0.)? as f32;
10916                let cols = self.arg_num(&args, 2, 4.)? as usize;
10917                let rows = self.arg_num(&args, 3, 1.)? as usize;
10918                let cell = self.arg_num(&args, 4, 36.)? as f32;
10919                let sel = self.arg_num(&args, 5, -1.)? as i32;
10920                let th = self.ui_theme;
10921                let prim = self.color_at(&args, 6, th.primary);
10922                self.draw_ui(&ling_ui::widgets::slotgrid(
10923                    x, y, cols, rows, cell, sel, prim, th.track,
10924                ));
10925                return Ok(Value::Unit);
10926            },
10927            #[cfg(not(target_arch = "wasm32"))]
10928            "ui_vignette" | "暗角" | "ビネット" | "비네트" | "ขอบมืด" | "سایه‌گرد_کادر" | "تظليل_الحواف" | "הצללת_מסגרת" | "ویگنیٹ" | "vignette_ui" | "ui_виньетка" => {
10929                let intensity = self.arg_num(&args, 0, 0.5)? as f32;
10930                let (w, h) = {
10931                    let g = self.gfx.borrow();
10932                    (g.width as f32, g.height as f32)
10933                };
10934                let th = self.ui_theme;
10935                let col = self.color_at(&args, 1, th.warn);
10936                self.draw_ui(&ling_ui::widgets::vignette(w, h, intensity, col));
10937                return Ok(Value::Unit);
10938            },
10939
10940            // ── Faux-3D in 2D space ──────────────────────────────────────────
10941            #[cfg(not(target_arch = "wasm32"))]
10942            "ui_gauge3d" | "立体仪表" | "立体ゲージ" | "입체게이지" | "มาตรวัด3มิติ" | "گیج_سه‌بعدی" | "مقياس_ثلاثي_الأبعاد" | "מד_תלת_ממדי" | "تھری_ڈی_گیج" | "jauge_3d_ui" | "ui_anzeige_3d" | "ui_индикатор_3d" =>
10943            {
10944                let cx = self.arg_num(&args, 0, 0.)? as f32;
10945                let cy = self.arg_num(&args, 1, 0.)? as f32;
10946                let r = self.arg_num(&args, 2, 50.)? as f32;
10947                let val = self.arg_num(&args, 3, 0.)? as f32;
10948                let max = self.arg_num(&args, 4, 1.)? as f32;
10949                let spin = self.arg_num(&args, 5, 0.)? as f32;
10950                let th = self.ui_theme;
10951                let fill = self.color_at(&args, 6, th.primary);
10952                self.draw_ui(&ling_ui::widgets::gauge3d(
10953                    cx,
10954                    cy,
10955                    r,
10956                    val / max.max(1e-6),
10957                    spin,
10958                    fill,
10959                    th.track,
10960                ));
10961                return Ok(Value::Unit);
10962            },
10963            #[cfg(not(target_arch = "wasm32"))]
10964            "ui_panel3d" | "立体面板" | "立体パネル" | "입체패널" | "แผง3มิติ" | "پنل_سه‌بعدی" | "لوحة_ثلاثية_الأبعاد" | "לוח_תלת_ממדי" | "تھری_ڈی_پینل" | "panneau_3d_ui" | "ui_feld_3d" | "ui_панель_3d" =>
10965            {
10966                let x = self.arg_num(&args, 0, 0.)? as f32;
10967                let y = self.arg_num(&args, 1, 0.)? as f32;
10968                let w0 = self.arg_num(&args, 2, 200.)? as f32;
10969                let h0 = self.arg_num(&args, 3, 120.)? as f32;
10970                let depth = self.arg_num(&args, 4, 14.)? as f32;
10971                let th = self.ui_theme;
10972                let prim = self.color_at(&args, 5, th.primary);
10973                self.draw_ui(&ling_ui::widgets::panel3d(x, y, w0, h0, depth, prim, th.bg));
10974                return Ok(Value::Unit);
10975            },
10976            #[cfg(not(target_arch = "wasm32"))]
10977            "ui_radar3d" | "立体雷达" | "立体レーダー" | "입체레이더" | "เรดาร์3มิติ" | "رادار_سه‌بعدی" | "رادار_ثلاثي_الأبعاد" | "מכ״ם_תלת_ממדי" | "تھری_ڈی_ریڈار" | "radar_3d_ui" | "ui_radar_3d" | "ui_радар_3d" =>
10978            {
10979                let cx = self.arg_num(&args, 0, 0.)? as f32;
10980                let cy = self.arg_num(&args, 1, 0.)? as f32;
10981                let r = self.arg_num(&args, 2, 60.)? as f32;
10982                let tilt = self.arg_num(&args, 3, 0.9)? as f32;
10983                let sweep = self.arg_num(&args, 4, 0.)? as f32;
10984                let th = self.ui_theme;
10985                let prim = self.color_at(&args, 5, th.primary);
10986                self.draw_ui(&ling_ui::widgets::radar3d(
10987                    cx, cy, r, tilt, sweep, prim, th.track,
10988                ));
10989                return Ok(Value::Unit);
10990            },
10991
10992            // ── Interface sounds ─────────────────────────────────────────────
10993            #[cfg(not(target_arch = "wasm32"))]
10994            "audio_blip" | "提示音" | "ビープ音" | "효과음" | "เสียงบี๊บ" | "بوق_کوتاه" | "نغمة_قصيرة" | "ביפ" | "بلپ_آواز" | "bip_audio" | "звук_бип" =>
10995            {
10996                let freq = self.arg_num(&args, 0, 660.)? as f32;
10997                let dur = self.arg_num(&args, 1, 0.08)? as f32;
10998                let wave = Wave::from_name(&self.arg_str(&args, 2, "sine"));
10999                let amp = self.arg_num(&args, 3, 0.25)? as f32;
11000                if let Some(audio) = &self.audio {
11001                    audio.blip(freq, amp, dur, wave);
11002                }
11003                return Ok(Value::Unit);
11004            },
11005            #[cfg(not(target_arch = "wasm32"))]
11006            "ui_sound" | "界面音" | "UI音" | "인터페이스음" | "เสียงปุ่ม" | "صدای_رابط" | "صوت_الواجهة" | "צליל_ממשק" | "یو_آئی_آواز" | "son_ui" | "ui_klang" | "ui_звук" =>
11007            {
11008                let name = self.arg_str(&args, 0, "click");
11009                if let Some(audio) = &self.audio {
11010                    match name.as_str() {
11011                        "hover" => audio.blip(880.0, 0.10, 0.04, Wave::Sine),
11012                        "confirm" => {
11013                            audio.blip(660.0, 0.22, 0.07, Wave::Square);
11014                            audio.blip(990.0, 0.18, 0.10, Wave::Square);
11015                        },
11016                        "error" => {
11017                            audio.blip(180.0, 0.30, 0.16, Wave::Saw);
11018                            audio.blip(140.0, 0.30, 0.18, Wave::Saw);
11019                        },
11020                        "toggle" => audio.blip(520.0, 0.22, 0.05, Wave::Triangle),
11021                        "tick" => audio.blip(1500.0, 0.12, 0.02, Wave::Square),
11022                        _ => audio.blip(720.0, 0.26, 0.05, Wave::Square), // "click"
11023                    }
11024                }
11025                return Ok(Value::Unit);
11026            },
11027
11028            // ══════════════════════════════════════════════════════════════════
11029            // MUSIC TOOLKIT  (crates/ling-music) — decode · analysis · GM synth ·
11030            // rhythm · karaoke. Analysis/decoding need no audio device; playback
11031            // and synthesis lazily start a dedicated music engine.
11032            // ══════════════════════════════════════════════════════════════════
11033
11034            // music_load(path) -> track handle (decodes WAV/FLAC/OGG/MP3/AAC)
11035            #[cfg(not(target_arch = "wasm32"))]
11036            "music_load" | "载入音乐" | "音楽読込" | "음악로드" | "โหลดเพลง" | "بارگذاری_موسیقی" | "تحميل_الموسيقى" | "טעינת_מוזיקה" | "موسیقی_لوڈ" | "charger_musique" | "musik_laden" | "загрузить_музыку" =>
11037            {
11038                let path = self.arg_str(&args, 0, "");
11039                let resolved = if std::path::Path::new(&path).exists() {
11040                    path.clone()
11041                } else if let Some(d) = &self.source_dir {
11042                    d.join(&path).to_string_lossy().into_owned()
11043                } else {
11044                    path.clone()
11045                };
11046                match ling_music::load(&resolved) {
11047                    Ok(t) => {
11048                        let id = self.tracks.len();
11049                        self.tracks.push(t);
11050                        return Ok(Value::Number(id as f64));
11051                    },
11052                    Err(e) => {
11053                        eprintln!("music_load failed ({path}): {e}");
11054                        return Ok(Value::Number(-1.0));
11055                    },
11056                }
11057            },
11058            #[cfg(not(target_arch = "wasm32"))]
11059            "music_duration" | "音乐时长" | "音楽長さ" | "음악길이" | "ความยาวเพลง" | "مدت_موسیقی" | "مدة_الموسيقى" | "משך_מוזיקה" | "موسیقی_دورانیہ" | "durée_musique" | "musik_dauer" | "длительность_музыки" =>
11060            {
11061                let id = self.arg_num(&args, 0, 0.0)? as i64;
11062                let d = self
11063                    .tracks
11064                    .get(id as usize)
11065                    .map(|t| t.duration)
11066                    .unwrap_or(0.0);
11067                return Ok(Value::Number(d as f64));
11068            },
11069            #[cfg(not(target_arch = "wasm32"))]
11070            "music_bpm" | "节拍速度" | "テンポ" | "템포" | "จังหวะต่อนาที" | "ضربان_در_دقیقه" | "نبضات_بالدقيقة" | "פעימות_לדקה" | "بی_پی_ایم" | "bpm_musique" | "musik_bpm" | "музыка_bpm" =>
11071            {
11072                let id = self.arg_num(&args, 0, 0.0)? as i64;
11073                let b = self
11074                    .tracks
11075                    .get(id as usize)
11076                    .map(|t| ling_music::analysis::bpm(&t.mono, t.rate))
11077                    .unwrap_or(0.0);
11078                return Ok(Value::Number(b as f64));
11079            },
11080            #[cfg(not(target_arch = "wasm32"))]
11081            "music_key" | "调性" | "調性" | "조성" | "คีย์เพลง" | "گام_موسیقی" | "مقام_الموسيقى" | "סולם_מוזיקלי" | "موسیقی_کلید" | "tonalité_musique" | "musik_tonart" | "тональность_музыки" => {
11082                let id = self.arg_num(&args, 0, 0.0)? as i64;
11083                let k = self
11084                    .tracks
11085                    .get(id as usize)
11086                    .map(|t| ling_music::analysis::key_name(&t.mono, t.rate))
11087                    .unwrap_or_default();
11088                return Ok(Value::Str(k));
11089            },
11090            #[cfg(not(target_arch = "wasm32"))]
11091            "music_onsets" | "音符起点" | "オンセット" | "온셋" | "จุดเริ่มเสียง" | "آغازهای_نت" | "بدايات_النغمات" | "התחלות_תווים" | "نوٹ_شروعات" | "attaques_musique" | "musik_einsätze" | "атаки_музыки" =>
11092            {
11093                let id = self.arg_num(&args, 0, 0.0)? as i64;
11094                let v = self
11095                    .tracks
11096                    .get(id as usize)
11097                    .map(|t| ling_music::analysis::onsets(&t.mono, t.rate))
11098                    .unwrap_or_default();
11099                return Ok(Value::List(Rc::new(
11100                    v.into_iter().map(|x| Value::Number(x as f64)).collect(),
11101                )));
11102            },
11103            #[cfg(not(target_arch = "wasm32"))]
11104            "music_beat_grid" | "节拍网格" | "ビートグリッド" | "비트그리드" | "กริดจังหวะ" | "شبکه_ضرب" | "شبكة_الإيقاع" | "רשת_פעימות" | "بیٹ_گرڈ" | "grille_temps_musique" | "musik_taktraster" | "сетка_ритма_музыки" =>
11105            {
11106                let id = self.arg_num(&args, 0, 0.0)? as i64;
11107                let beats = self
11108                    .tracks
11109                    .get(id as usize)
11110                    .map(|t| {
11111                        let b = ling_music::analysis::bpm(&t.mono, t.rate);
11112                        ling_music::analysis::beat_grid(&t.mono, t.rate, b)
11113                    })
11114                    .unwrap_or_default();
11115                return Ok(Value::List(Rc::new(
11116                    beats.into_iter().map(|x| Value::Number(x as f64)).collect(),
11117                )));
11118            },
11119
11120            // ── playback ──
11121            #[cfg(not(target_arch = "wasm32"))]
11122            "music_play" | "播放音乐" | "音楽再生" | "음악재생" | "เล่นเพลง" | "پخش_موسیقی" | "شغّل_الموسيقى" | "נגן_מוזיקה" | "موسیقی_چلاؤ" | "jouer_musique" | "musik_abspielen" | "играть_музыку" =>
11123            {
11124                let id = self.arg_num(&args, 0, 0.0)? as i64;
11125                if self.ensure_music() {
11126                    let track = self
11127                        .tracks
11128                        .get(id as usize)
11129                        .map(|t| (t.stereo.clone(), t.rate));
11130                    if let (Some((st, rate)), Some(m)) = (track, &self.music) {
11131                        m.set_track(st, rate);
11132                        m.play();
11133                    } else if let Some(m) = &self.music {
11134                        m.play();
11135                    }
11136                }
11137                return Ok(Value::Unit);
11138            },
11139            #[cfg(not(target_arch = "wasm32"))]
11140            "music_pause" | "暂停音乐" | "音楽一時停止" | "음악일시정지" | "หยุดเพลงชั่วคราว" | "مکث_موسیقی" | "ألبث_الموسيقى" | "השהה_מוזיקה" | "موسیقی_روکو_مؤقت" | "pause_musique" | "musik_pausieren" | "пауза_музыки" =>
11141            {
11142                if let Some(m) = &self.music {
11143                    m.pause();
11144                }
11145                return Ok(Value::Unit);
11146            },
11147            #[cfg(not(target_arch = "wasm32"))]
11148            "music_stop" | "停止音乐" | "音楽停止" | "음악정지" | "หยุดเพลง" | "توقف_موسیقی" | "أوقف_الموسيقى" | "עצור_מוזיקה" | "موسیقی_روکو" | "arrêter_musique" | "musik_stoppen" | "остановить_музыку" =>
11149            {
11150                if let Some(m) = &self.music {
11151                    m.stop();
11152                }
11153                return Ok(Value::Unit);
11154            },
11155            #[cfg(not(target_arch = "wasm32"))]
11156            "music_seek" | "定位音乐" | "音楽シーク" | "음악탐색" | "ค้นหาเพลง" | "جستجوی_موسیقی" | "ابحث_في_الموسيقى" | "חפש_במוזיקה" | "موسیقی_تلاش" | "chercher_musique" | "musik_suchen" | "перемотать_музыку" =>
11157            {
11158                let sec = self.arg_num(&args, 0, 0.0)? as f32;
11159                if let Some(m) = &self.music {
11160                    m.seek(sec);
11161                }
11162                return Ok(Value::Unit);
11163            },
11164            #[cfg(not(target_arch = "wasm32"))]
11165            "music_pos" | "音乐位置" | "音楽位置" | "음악위치" | "ตำแหน่งเพลง" | "موقعیت_موسیقی" | "موضع_الموسيقى" | "מיקום_מוזיקה" | "موسیقی_مقام" | "position_musique" | "musik_position" | "позиция_музыки" =>
11166            {
11167                let p = self.music.as_ref().map(|m| m.position()).unwrap_or(0.0);
11168                return Ok(Value::Number(p as f64));
11169            },
11170            #[cfg(not(target_arch = "wasm32"))]
11171            "music_volume" | "音乐音量" | "音楽音量" | "음악음량" | "ระดับเพลง" | "بلندی_موسیقی" | "مستوى_الموسيقى" | "עוצמת_מוזיקה" | "موسیقی_شدت" | "volume_musique" | "musik_lautstärke" | "громкость_музыки" =>
11172            {
11173                let v = self.arg_num(&args, 0, 0.8)? as f32;
11174                if self.ensure_music() {
11175                    if let Some(m) = &self.music {
11176                        m.set_volume(v);
11177                    }
11178                }
11179                return Ok(Value::Unit);
11180            },
11181
11182            // ── synthesis (GM-capable, patches from .ling files) ──
11183            #[cfg(not(target_arch = "wasm32"))]
11184            "music_patch" | "乐器音色" | "音色読込" | "악기패치" | "แพตช์เครื่องดนตรี" | "پچ_موسیقی" | "آلة_الموسيقى" | "תיקון_כלי_נגינה" | "میوزک_پیچ" | "patch_musique" | "musik_patch" | "патч_музыки" =>
11185            {
11186                let path = self.arg_str(&args, 0, "");
11187                let resolved = if std::path::Path::new(&path).exists() {
11188                    path.clone()
11189                } else if let Some(d) = &self.source_dir {
11190                    d.join(&path).to_string_lossy().into_owned()
11191                } else {
11192                    path.clone()
11193                };
11194                if !self.ensure_music() {
11195                    return Ok(Value::Number(-1.0));
11196                }
11197                match ling_music::patch::from_path(&resolved) {
11198                    Ok(p) => {
11199                        let id = self.music.as_ref().unwrap().add_patch(p);
11200                        return Ok(Value::Number(id as f64));
11201                    },
11202                    Err(e) => {
11203                        eprintln!("music_patch failed ({path}): {e}");
11204                        return Ok(Value::Number(-1.0));
11205                    },
11206                }
11207            },
11208            #[cfg(not(target_arch = "wasm32"))]
11209            "music_note" | "弹音符" | "音符演奏" | "음표연주" | "เล่นโน้ต" | "نواختن_نت" | "عزف_نغمة" | "נגן_תו" | "نوٹ_بجاؤ" | "note_musique" | "musik_note" | "нота_музыки" =>
11210            {
11211                let inst = self.arg_num(&args, 0, 0.0)? as usize;
11212                let midi = self.pitch_arg(&args, 1, 60);
11213                let dur = self.arg_num(&args, 2, 0.5)? as f32;
11214                let vel = self.arg_num(&args, 3, 0.9)? as f32;
11215                if self.ensure_music() {
11216                    if let Some(m) = &self.music {
11217                        m.note(inst, midi, vel, dur);
11218                    }
11219                }
11220                return Ok(Value::Unit);
11221            },
11222            #[cfg(not(target_arch = "wasm32"))]
11223            "music_note_on" | "音符开始" | "音符オン" | "음표켜기" | "โน้ตเริ่ม" | "شروع_نت" | "بدء_النغمة" | "התחלת_תו" | "نوٹ_شروع" | "note_musique_on" | "musik_note_an" | "нота_музыки_вкл" =>
11224            {
11225                let inst = self.arg_num(&args, 0, 0.0)? as usize;
11226                let midi = self.pitch_arg(&args, 1, 60);
11227                let vel = self.arg_num(&args, 2, 0.9)? as f32;
11228                if self.ensure_music() {
11229                    if let Some(m) = &self.music {
11230                        m.note_on(inst, midi, vel);
11231                    }
11232                }
11233                return Ok(Value::Unit);
11234            },
11235            #[cfg(not(target_arch = "wasm32"))]
11236            "music_note_off" | "音符结束" | "音符オフ" | "음표끄기" | "โน้ตจบ" | "پایان_نت" | "إيقاف_النغمة" | "סיום_תו" | "نوٹ_ختم" | "note_musique_off" | "musik_note_aus" | "нота_музыки_выкл" =>
11237            {
11238                let inst = self.arg_num(&args, 0, 0.0)? as usize;
11239                let midi = self.pitch_arg(&args, 1, 60);
11240                if let Some(m) = &self.music {
11241                    m.note_off(inst, midi);
11242                }
11243                return Ok(Value::Unit);
11244            },
11245
11246            // ── rhythm-game judging ──
11247            #[cfg(not(target_arch = "wasm32"))]
11248            "music_judge" | "判定" | "判定する" | "판정" | "ตัดสินจังหวะ" | "داوری_ضرب" | "حكم_الإيقاع" | "שיפוט_קצב" | "بیٹ_فیصلہ" | "juger_musique" | "musik_bewerten" | "оценить_музыку" =>
11249            {
11250                let delta_ms = self.arg_num(&args, 0, 9999.0)? as f32;
11251                return Ok(Value::Number(
11252                    ling_music::Grade::judge(delta_ms).index() as f64
11253                ));
11254            },
11255            #[cfg(not(target_arch = "wasm32"))]
11256            "music_grade_name" | "判定名" | "判定名称" | "판정이름" | "ชื่อการตัดสิน" | "نام_رتبه" | "اسم_التقييم" | "שם_דירוג" | "گریڈ_نام" | "nom_grade_musique" | "musik_bewertungsname" | "имя_оценки_музыки" =>
11257            {
11258                let idx = self.arg_num(&args, 0, 4.0)? as i32;
11259                return Ok(Value::Str(
11260                    ling_music::Grade::from_index(idx).name().to_string(),
11261                ));
11262            },
11263
11264            // ── karaoke ──
11265            #[cfg(not(target_arch = "wasm32"))]
11266            "music_lrc" | "载入歌词" | "歌詞読込" | "가사로드" | "โหลดเนื้อเพลง" | "بارگذاری_متن_ترانه" | "تحميل_كلمات_الأغنية" | "טעינת_מילות_שיר" | "گیت_متن_لوڈ" | "lrc_musique" | "musik_lrc" | "lrc_музыки" =>
11267            {
11268                let path = self.arg_str(&args, 0, "");
11269                let resolved = if std::path::Path::new(&path).exists() {
11270                    path.clone()
11271                } else if let Some(d) = &self.source_dir {
11272                    d.join(&path).to_string_lossy().into_owned()
11273                } else {
11274                    path.clone()
11275                };
11276                match std::fs::read_to_string(&resolved) {
11277                    Ok(text) => {
11278                        let id = self.lyrics.len();
11279                        self.lyrics.push(ling_music::Lyrics::parse(&text));
11280                        return Ok(Value::Number(id as f64));
11281                    },
11282                    Err(e) => {
11283                        eprintln!("music_lrc failed ({path}): {e}");
11284                        return Ok(Value::Number(-1.0));
11285                    },
11286                }
11287            },
11288            #[cfg(not(target_arch = "wasm32"))]
11289            "music_lyric" | "当前歌词" | "現在歌詞" | "현재가사" | "เนื้อเพลงปัจจุบัน" | "متن_ترانه_فعلی" | "كلمات_الأغنية_الحالية" | "מילות_שיר_נוכחיות" | "موجودہ_گیت_متن" | "paroles_musique" | "musik_liedtext" | "текст_песни" =>
11290            {
11291                let id = self.arg_num(&args, 0, 0.0)? as i64;
11292                let t = self.arg_num(&args, 1, 0.0)? as f32;
11293                let line = self
11294                    .lyrics
11295                    .get(id as usize)
11296                    .map(|l| l.line_at(t).to_string())
11297                    .unwrap_or_default();
11298                return Ok(Value::Str(line));
11299            },
11300            #[cfg(not(target_arch = "wasm32"))]
11301            "music_mic_pitch" | "麦克风音高" | "マイク音程" | "마이크음정" | "ระดับเสียงไมค์" | "زیروبمی_میکروفون" | "طبقة_صوت_الميكروفون" | "גובה_צליל_מיקרופון" | "مائیکروفون_پچ" | "hauteur_micro_musique" | "musik_mikrofon_tonhöhe" | "высота_тона_микрофона" =>
11302            {
11303                let hz = if let Some(mic) = self.mic.as_ref() {
11304                    let s = mic.latest_samples();
11305                    let rate = mic.sample_rate();
11306                    ling_music::pitch::detect(&s, rate).unwrap_or(0.0)
11307                } else {
11308                    0.0
11309                };
11310                return Ok(Value::Number(hz as f64));
11311            },
11312            #[cfg(not(target_arch = "wasm32"))]
11313            "music_note_name" | "音名" | "音名称" | "음이름" | "ชื่อโน้ต" | "نام_نت" | "اسم_النغمة" | "שם_תו" | "نوٹ_نام" | "nom_note_musique" | "musik_notenname" | "имя_ноты_музыки" =>
11314            {
11315                let hz = self.arg_num(&args, 0, 0.0)? as f32;
11316                return Ok(Value::Str(ling_music::note::hz_to_name(hz)));
11317            },
11318            #[cfg(not(target_arch = "wasm32"))]
11319            "music_hz" | "音符频率" | "音符周波数" | "음표주파수" | "ความถี่โน้ต" | "فرکانس_نت" | "تردد_النغمة" | "תדר_תו" | "نوٹ_ہرٹز" | "hz_musique" | "musik_hz" | "музыка_гц" =>
11320            {
11321                let midi = self.pitch_arg(&args, 0, 69);
11322                return Ok(Value::Number(
11323                    ling_music::note::midi_to_hz(midi as f32) as f64
11324                ));
11325            },
11326            #[cfg(not(target_arch = "wasm32"))]
11327            "music_pitch_score" | "音准评分" | "音程スコア" | "음정점수" | "คะแนนเสียง" | "امتیاز_زیروبمی" | "درجة_طبقة_الصوت" | "ציון_גובה_צליל" | "پچ_اسکور" | "score_hauteur_musique" | "musik_tonhöhen_punktzahl" | "счёт_высоты_тона" =>
11328            {
11329                let hz = self.arg_num(&args, 0, 0.0)? as f32;
11330                let target = self.arg_num(&args, 1, 0.0)? as f32;
11331                return Ok(Value::Number(
11332                    ling_music::karaoke::pitch_score(hz, target) as f64
11333                ));
11334            },
11335
11336            // ── MIDI (inaudible note source: drive coins, cues, etc.) ──
11337            #[cfg(not(target_arch = "wasm32"))]
11338            "music_midi_load" | "载入MIDI" | "MIDI読込" | "미디로드" | "โหลดมิดี" | "بارگذاری_MIDI" | "تحميل_MIDI" | "טעינת_MIDI" | "MIDI_لوڈ" | "charger_midi_musique" | "musik_midi_laden" | "загрузить_midi_музыки" =>
11339            {
11340                let path = self.arg_str(&args, 0, "");
11341                let resolved = if std::path::Path::new(&path).exists() {
11342                    path.clone()
11343                } else if let Some(d) = &self.source_dir {
11344                    d.join(&path).to_string_lossy().into_owned()
11345                } else {
11346                    path.clone()
11347                };
11348                match ling_music::midi::load(&resolved) {
11349                    Ok(m) => {
11350                        let id = self.midis.len();
11351                        self.midis.push(m);
11352                        return Ok(Value::Number(id as f64));
11353                    },
11354                    Err(e) => {
11355                        eprintln!("music_midi_load failed ({path}): {e}");
11356                        return Ok(Value::Number(-1.0));
11357                    },
11358                }
11359            },
11360            #[cfg(not(target_arch = "wasm32"))]
11361            "music_midi_count" | "MIDI数量" | "MIDI数" | "미디수" | "จำนวนมิดี" | "تعداد_MIDI" | "عدد_MIDI" | "מספר_MIDI" | "MIDI_تعداد" | "nombre_midi_musique" | "musik_midi_anzahl" | "число_midi_музыки" =>
11362            {
11363                let id = self.arg_num(&args, 0, 0.0)? as i64;
11364                let n = self
11365                    .midis
11366                    .get(id as usize)
11367                    .map(|m| m.notes.len())
11368                    .unwrap_or(0);
11369                return Ok(Value::Number(n as f64));
11370            },
11371            // music_midi_notes(id) -> flat [time, midi, time, midi, …]
11372            #[cfg(not(target_arch = "wasm32"))]
11373            "music_midi_notes" | "MIDI音符" | "MIDIノート" | "미디음표" | "โน้ตมิดี" | "نت‌های_MIDI" | "نغمات_MIDI" | "תווי_MIDI" | "MIDI_نوٹس" | "notes_midi_musique" | "musik_midi_noten" | "ноты_midi_музыки" =>
11374            {
11375                let id = self.arg_num(&args, 0, 0.0)? as i64;
11376                let mut out = Vec::new();
11377                if let Some(m) = self.midis.get(id as usize) {
11378                    for n in &m.notes {
11379                        out.push(Value::Number(n.time as f64));
11380                        out.push(Value::Number(n.midi as f64));
11381                    }
11382                }
11383                return Ok(Value::List(Rc::new(out)));
11384            },
11385            // music_midi_bars(id) -> flat [time, midi, dur, …] (for karaoke note bars)
11386            #[cfg(not(target_arch = "wasm32"))]
11387            "music_midi_bars" | "MIDI音条" | "MIDIバー" | "미디바" | "แท่งมิดี" | "میله‌های_MIDI" | "أعمدة_MIDI" | "עמודות_MIDI" | "MIDI_بارز" | "mesures_midi_musique" | "musik_midi_takte" | "такты_midi_музыки" =>
11388            {
11389                let id = self.arg_num(&args, 0, 0.0)? as i64;
11390                let mut out = Vec::new();
11391                if let Some(m) = self.midis.get(id as usize) {
11392                    for n in &m.notes {
11393                        out.push(Value::Number(n.time as f64));
11394                        out.push(Value::Number(n.midi as f64));
11395                        out.push(Value::Number(n.dur as f64));
11396                    }
11397                }
11398                return Ok(Value::List(Rc::new(out)));
11399            },
11400
11401            // music_fft(track_id, nbands) -> spectrum at the current playback position
11402            #[cfg(not(target_arch = "wasm32"))]
11403            "music_fft" | "音乐频谱" | "音楽スペクトル" | "음악스펙트럼" | "สเปกตรัมเพลง" | "طیف_موسیقی" | "طيف_الموسيقى" | "ספקטרום_מוזיקה" | "میوزک_اسپیکٹرم" | "fft_musique" | "musik_fft" | "fft_музыки" =>
11404            {
11405                let id = self.arg_num(&args, 0, 0.0)? as i64;
11406                let nbands = self.arg_num(&args, 1, 16.0)? as usize;
11407                let pos = self.music.as_ref().map(|m| m.position()).unwrap_or(0.0);
11408                if let Some(t) = self.tracks.get(id as usize) {
11409                    let idx = (pos * t.rate as f32) as usize;
11410                    let end = (idx + 2048).min(t.mono.len());
11411                    if end > idx + 64 {
11412                        self.fft.borrow_mut().push_samples(&t.mono[idx..end]);
11413                    }
11414                }
11415                let bands = self.fft.borrow().freq_bands(nbands);
11416                return Ok(Value::List(Rc::new(
11417                    bands.into_iter().map(|x| Value::Number(x as f64)).collect(),
11418                )));
11419            },
11420
11421            // ── stop every one-shot SFX/morph/sample voice (scene cleanup) ──
11422            #[cfg(not(target_arch = "wasm32"))]
11423            "audio_stop_sfx" | "停止音效" | "効果音停止" | "효과음정지" | "หยุดเอฟเฟกต์ทั้งหมด" | "توقف_همه_جلوه‌ها" | "أوقف_كل_المؤثرات" | "עצור_כל_האפקטים" | "تمام_ایفیکٹ_روکو" =>
11424            {
11425                if let Some(a) = &self.audio {
11426                    a.stop_all_sfx();
11427                }
11428                return Ok(Value::Unit);
11429            },
11430            // ── spatial (2D/3D/4D) one-shot SFX ──
11431            #[cfg(not(target_arch = "wasm32"))]
11432            "audio_sfx" | "音效" | "空間効果音" | "공간효과음" | "เสียงเอฟเฟกต์" | "جلوه_صوتی" | "مؤثرات_صوتية" | "אפקט_קול" | "آواز_ایفیکٹ" | "effet_sonore" | "klangeffekt" | "звуковой_эффект" =>
11433            {
11434                let x = self.arg_num(&args, 0, 0.0)? as f32;
11435                let y = self.arg_num(&args, 1, 0.0)? as f32;
11436                let z = self.arg_num(&args, 2, 0.0)? as f32;
11437                let w = self.arg_num(&args, 3, 1.0)? as f32;
11438                let freq = self.arg_num(&args, 4, 440.0)? as f32;
11439                let amp = self.arg_num(&args, 5, 0.3)? as f32;
11440                let dur = self.arg_num(&args, 6, 0.15)? as f32;
11441                let wave = Wave::from_name(&self.arg_str(&args, 7, "sine"));
11442                if let Some(a) = &self.audio {
11443                    a.sfx(x, y, z, w, freq, amp, dur, wave);
11444                }
11445                return Ok(Value::Unit);
11446            },
11447            // ── YIN-YANG morph synth note: physical-model(light) ↔ FM/crush(dark) ──
11448            // โน้ตมอร์ฟ(x,y,z,w, freq, amp, dur, material, morph)
11449            //   material: 0 bowed-string · 1 plucked · 2 blown · 3 struck-metal
11450            //   morph:    0.0 light/acoustic .. 1.0 dark/digital
11451            #[cfg(not(target_arch = "wasm32"))]
11452            "morph_note" | "โน้ตมอร์ฟ" | "变形音" | "モーフ音" | "모프음" | "نت_مورف" | "نغمة_متحولة" | "תו_מורף" | "مورف_نوٹ" =>
11453            {
11454                let x = self.arg_num(&args, 0, 0.0)? as f32;
11455                let y = self.arg_num(&args, 1, 0.0)? as f32;
11456                let z = self.arg_num(&args, 2, 0.0)? as f32;
11457                let w = self.arg_num(&args, 3, 1.0)? as f32;
11458                let freq = self.arg_num(&args, 4, 220.0)? as f32;
11459                let amp = self.arg_num(&args, 5, 0.3)? as f32;
11460                let dur = self.arg_num(&args, 6, 0.6)? as f32;
11461                let material = self.arg_num(&args, 7, 0.0)?.clamp(0.0, 3.0) as u8;
11462                let morph = self.arg_num(&args, 8, 0.0)? as f32;
11463                if let Some(a) = &self.audio {
11464                    a.morph_note(x, y, z, w, freq, amp, dur, material, morph);
11465                }
11466                return Ok(Value::Unit);
11467            },
11468            // ── sample load / positional play / loop / stop ──
11469            #[cfg(not(target_arch = "wasm32"))]
11470            "audio_sample_load" | "载入采样" | "サンプル読込" | "샘플로드" | "โหลดตัวอย่างเสียง" | "بارگذاری_نمونه_صدا" | "تحميل_عينة_صوتية" | "טעינת_דגימת_קול" | "آواز_نمونہ_لوڈ" | "charger_échantillon" | "sample_laden" | "загрузить_семпл" =>
11471            {
11472                let path = self.arg_str(&args, 0, "");
11473                let resolved = if std::path::Path::new(&path).exists() {
11474                    path.clone()
11475                } else if let Some(d) = &self.source_dir {
11476                    d.join(&path).to_string_lossy().into_owned()
11477                } else {
11478                    path.clone()
11479                };
11480                match ling_music::load(&resolved) {
11481                    Ok(t) => {
11482                        if let Some(a) = &self.audio {
11483                            return Ok(Value::Number(a.add_sample(t.mono, t.rate) as f64));
11484                        }
11485                        return Ok(Value::Number(-1.0));
11486                    },
11487                    Err(e) => {
11488                        eprintln!("audio_sample_load failed ({path}): {e}");
11489                        return Ok(Value::Number(-1.0));
11490                    },
11491                }
11492            },
11493            #[cfg(not(target_arch = "wasm32"))]
11494            "audio_sample_play" | "播放采样" | "サンプル再生" | "샘플재생" | "เล่นตัวอย่างเสียง" | "پخش_نمونه_صدا" | "تشغيل_عينة_صوتية" | "נגינת_דגימת_קול" | "آواز_نمونہ_چلاؤ" | "jouer_échantillon" | "sample_abspielen" | "играть_семпл" =>
11495            {
11496                let id = self.arg_num(&args, 0, 0.0)? as usize;
11497                let x = self.arg_num(&args, 1, 0.0)? as f32;
11498                let y = self.arg_num(&args, 2, 0.0)? as f32;
11499                let z = self.arg_num(&args, 3, 0.0)? as f32;
11500                let w = self.arg_num(&args, 4, 1.0)? as f32;
11501                let vol = self.arg_num(&args, 5, 1.0)? as f32;
11502                let looping = self.arg_num(&args, 6, 0.0)? > 0.5;
11503                let v = self
11504                    .audio
11505                    .as_ref()
11506                    .map(|a| a.play_sample(id, x, y, z, w, vol, looping))
11507                    .unwrap_or(0);
11508                return Ok(Value::Number(v as f64));
11509            },
11510            #[cfg(not(target_arch = "wasm32"))]
11511            "audio_sample_stop" | "停止采样" | "サンプル停止" | "샘플정지" | "หยุดตัวอย่างเสียง" | "توقف_نمونه_صدا" | "إيقاف_عينة_صوتية" | "עצירת_דגימת_קול" | "آواز_نمونہ_روکو" | "arrêter_échantillon" | "sample_stoppen" | "остановить_семпл" =>
11512            {
11513                let v = self.arg_num(&args, 0, 0.0)? as u32;
11514                if let Some(a) = &self.audio {
11515                    a.stop_sample(v);
11516                }
11517                return Ok(Value::Unit);
11518            },
11519            // ── master FX: delay / reverb / low-pass (underwater) ──
11520            #[cfg(not(target_arch = "wasm32"))]
11521            "audio_fx_delay" | "回声" | "ディレイ効果" | "딜레이" | "เสียงสะท้อน" | "افکت_تاخیر" | "صدى_تأخير" | "אפקט_עיכוב" | "تاخیر_ایفیکٹ" | "délai_audio" | "audio_verzögerung" | "звук_задержка" =>
11522            {
11523                let time = self.arg_num(&args, 0, 0.3)? as f32;
11524                let fb = self.arg_num(&args, 1, 0.3)? as f32;
11525                let mix = self.arg_num(&args, 2, 0.3)? as f32;
11526                if let Some(a) = &self.audio {
11527                    a.fx_delay(time, fb, mix);
11528                }
11529                return Ok(Value::Unit);
11530            },
11531            #[cfg(not(target_arch = "wasm32"))]
11532            "audio_fx_reverb" | "混响" | "リバーブ" | "리버브" | "เสียงก้อง" | "افکت_پژواک" | "صدى_ارتداد" | "אפקט_הדהוד" | "بازگشت_آواز_ایفیکٹ" | "réverbération_audio" | "audio_nachhall" | "звук_реверберация" =>
11533            {
11534                let mix = self.arg_num(&args, 0, 0.3)? as f32;
11535                if let Some(a) = &self.audio {
11536                    a.fx_reverb(mix);
11537                }
11538                return Ok(Value::Unit);
11539            },
11540            #[cfg(not(target_arch = "wasm32"))]
11541            "audio_fx_lowpass" | "低通滤波" | "ローパス" | "저역통과" | "กรองความถี่ต่ำ" | "فیلتر_پایین‌گذر" | "مرشح_تمرير_منخفض" | "מסנן_תדר_נמוך" | "لو_پاس_فلٹر" | "passe_bas_audio" | "audio_tiefpass" | "звук_фнч" =>
11542            {
11543                let cutoff = self.arg_num(&args, 0, 1.0)? as f32;
11544                if let Some(a) = &self.audio {
11545                    a.fx_lowpass(cutoff);
11546                }
11547                return Ok(Value::Unit);
11548            },
11549
11550            // ══════════════════════════════════════════════════════════════════
11551            // PHYSICS BUILTINS  (crates/ling-physics) — soft bodies, rigid+angular,
11552            // and a fast 2-D water/oil liquid sim mappable onto 3-D surfaces.
11553            // ══════════════════════════════════════════════════════════════════
11554
11555            // ── soft bodies (deformable bouncy balls) ──
11556            #[cfg(not(target_arch = "wasm32"))]
11557            "soft_ball" | "软球" | "ソフトボール" | "소프트볼" | "ลูกบอลนุ่ม" | "توپ_نرم" | "كرة_ناعمة" | "כדור_רך" | "نرم_گیند" | "balle_molle" | "weicher_ball" | "мягкий_шар" =>
11558            {
11559                let x = self.arg_num(&args, 0, 0.)? as f32;
11560                let y = self.arg_num(&args, 1, 0.)? as f32;
11561                let z = self.arg_num(&args, 2, 0.)? as f32;
11562                let r = self.arg_num(&args, 3, 1.0)? as f32;
11563                let b = ling_physics::soft::SoftBody::sphere(
11564                    ling_physics::Vec3::new(x, y, z),
11565                    r,
11566                    8,
11567                    12,
11568                    1.0,
11569                );
11570                let id = self.soft_bodies.len();
11571                self.soft_bodies.push(b);
11572                return Ok(Value::Number(id as f64));
11573            },
11574            #[cfg(not(target_arch = "wasm32"))]
11575            "soft_step" | "软体步进" | "ソフト更新" | "소프트스텝" | "ก้าวนุ่ม" | "گام_نرم_جسم" | "خطوة_ناعمة" | "צעד_רך" | "نرم_قدم" | "pas_mou" | "weicher_schritt" | "мягкий_шаг" =>
11576            {
11577                let id = self.arg_num(&args, 0, 0.)? as usize;
11578                let dt = self.arg_num(&args, 1, 0.016)? as f32;
11579                let gy = self.arg_num(&args, 2, 15.0)? as f32;
11580                if let Some(b) = self.soft_bodies.get_mut(id) {
11581                    b.integrate(dt, ling_physics::Vec3::new(0.0, gy, 0.0), 4);
11582                }
11583                return Ok(Value::Unit);
11584            },
11585            #[cfg(not(target_arch = "wasm32"))]
11586            "soft_bounce" | "软体落地" | "ソフト着地" | "소프트바운스" | "เด้งนุ่ม" | "جهش_نرم" | "ارتداد_ناعم" | "קפיצה_רכה" | "نرم_اچھال" | "rebond_mou" | "weicher_abprall" | "мягкий_отскок" =>
11587            {
11588                let id = self.arg_num(&args, 0, 0.)? as usize;
11589                let fy = self.arg_num(&args, 1, 0.)? as f32;
11590                let rest = self.arg_num(&args, 2, 0.5)? as f32;
11591                if let Some(b) = self.soft_bodies.get_mut(id) {
11592                    b.floor_collision(fy, rest);
11593                }
11594                return Ok(Value::Unit);
11595            },
11596            #[cfg(not(target_arch = "wasm32"))]
11597            "soft_contain" | "软体边界" | "ソフト箱" | "소프트경계" | "กล่องนุ่ม" | "محفظه_نرم" | "احتواء_ناعم" | "הכלה_רכה" | "نرم_احاطہ" | "contenir_mou" | "weiche_eindämmung" | "мягкое_сдерживание" =>
11598            {
11599                let id = self.arg_num(&args, 0, 0.)? as usize;
11600                let nx = self.arg_num(&args, 1, -5.)? as f32;
11601                let ny = self.arg_num(&args, 2, -5.)? as f32;
11602                let nz = self.arg_num(&args, 3, -5.)? as f32;
11603                let mx = self.arg_num(&args, 4, 5.)? as f32;
11604                let my = self.arg_num(&args, 5, 5.)? as f32;
11605                let mz = self.arg_num(&args, 6, 5.)? as f32;
11606                let rest = self.arg_num(&args, 7, 0.6)? as f32;
11607                if let Some(b) = self.soft_bodies.get_mut(id) {
11608                    b.contain(
11609                        ling_physics::Vec3::new(nx, ny, nz),
11610                        ling_physics::Vec3::new(mx, my, mz),
11611                        rest,
11612                    );
11613                }
11614                return Ok(Value::Unit);
11615            },
11616            #[cfg(not(target_arch = "wasm32"))]
11617            "soft_kick" | "软体踢" | "ソフト衝撃" | "소프트킥" | "เตะนุ่ม" | "ضربه_نرم" | "ركلة_ناعمة" | "בעיטה_רכה" | "نرم_ٹھوکر" | "coup_mou" | "weicher_stoß" | "мягкий_удар" =>
11618            {
11619                let id = self.arg_num(&args, 0, 0.)? as usize;
11620                let dx = self.arg_num(&args, 1, 0.)? as f32;
11621                let dy = self.arg_num(&args, 2, 0.)? as f32;
11622                let dz = self.arg_num(&args, 3, 0.)? as f32;
11623                let s = self.arg_num(&args, 4, 0.1)? as f32;
11624                if let Some(b) = self.soft_bodies.get_mut(id) {
11625                    b.kick(ling_physics::Vec3::new(dx, dy, dz), s);
11626                }
11627                return Ok(Value::Unit);
11628            },
11629            // soft_spin(id, ax, ay, az, rate) — add angular velocity about the axis
11630            // through the centroid (rate = rad/step; ≈ surface_speed / radius to roll)
11631            #[cfg(not(target_arch = "wasm32"))]
11632            "soft_spin" | "软体自旋" | "ソフト回転" | "소프트회전" | "หมุนนุ่ม" | "چرخش_نرم" | "دوران_ناعم" | "סיבוב_רך" | "نرم_گھماؤ" | "rotation_molle" | "weicher_spin" | "мягкое_вращение" =>
11633            {
11634                let id = self.arg_num(&args, 0, 0.)? as usize;
11635                let ax = self.arg_num(&args, 1, 0.)? as f32;
11636                let ay = self.arg_num(&args, 2, 0.)? as f32;
11637                let az = self.arg_num(&args, 3, 0.)? as f32;
11638                let rate = self.arg_num(&args, 4, 0.1)? as f32;
11639                if let Some(b) = self.soft_bodies.get_mut(id) {
11640                    b.spin(ling_physics::Vec3::new(ax, ay, az), rate);
11641                }
11642                return Ok(Value::Unit);
11643            },
11644            #[cfg(not(target_arch = "wasm32"))]
11645            "soft_deform" | "形变量" | "変形量" | "변형량" | "ความบิดเบี้ยว" | "تغییرشکل_نرم" | "تشوه_ناعم" | "עיוות_רך" | "نرم_بگاڑ" | "déformer_mou" | "weiches_verformen" | "мягкая_деформация" =>
11646            {
11647                let id = self.arg_num(&args, 0, 0.)? as usize;
11648                let d = self
11649                    .soft_bodies
11650                    .get(id)
11651                    .map(|b| b.deformation())
11652                    .unwrap_or(0.0);
11653                return Ok(Value::Number(d as f64));
11654            },
11655            // soft_angular_speed(id) -> magnitude of the body's angular velocity
11656            // (how fast it is tumbling/rolling), derived from its node velocities.
11657            #[cfg(not(target_arch = "wasm32"))]
11658            "soft_angular_speed"
11659            | "软体角速"
11660            | "ソフト角速度"
11661            | "소프트각속도"
11662            | "ความเร็วเชิงมุมนุ่ม" | "سرعت_زاویه‌ای_نرم" | "سرعة_زاوية_ناعمة" | "מהירות_זוויתית_רכה" | "نرم_زاویائی_رفتار" | "vitesse_angulaire_molle" | "weiche_winkelgeschwindigkeit" | "мягкая_угловая_скорость" => {
11663                let id = self.arg_num(&args, 0, 0.)? as usize;
11664                let w = self
11665                    .soft_bodies
11666                    .get(id)
11667                    .map(|b| b.angular_speed())
11668                    .unwrap_or(0.0);
11669                return Ok(Value::Number(w as f64));
11670            },
11671            #[cfg(not(target_arch = "wasm32"))]
11672            "soft_centroid" | "软体质心" | "ソフト重心" | "소프트중심" | "จุดศูนย์กลางนุ่ม" | "مرکز_جرم_نرم" | "مركز_ثقل_ناعم" | "מרכז_כובד_רך" | "نرم_مرکز_ثقل" | "centroïde_mou" | "weicher_schwerpunkt" | "мягкий_центроид" =>
11673            {
11674                let id = self.arg_num(&args, 0, 0.)? as usize;
11675                let c = self
11676                    .soft_bodies
11677                    .get(id)
11678                    .map(|b| b.centroid())
11679                    .unwrap_or(ling_physics::Vec3::ZERO);
11680                return Ok(Value::List(Rc::new(vec![
11681                    Value::Number(c.x as f64),
11682                    Value::Number(c.y as f64),
11683                    Value::Number(c.z as f64),
11684                ])));
11685            },
11686            // soft_nodes(id) -> flat [x,y,z, x,y,z, …] for rendering the deformed mesh
11687            #[cfg(not(target_arch = "wasm32"))]
11688            "soft_nodes" | "软体节点" | "ソフト節点" | "소프트노드" | "จุดนุ่ม" | "گره‌های_نرم" | "عقد_ناعمة" | "צמתי_רך" | "نرم_نوڈز" | "nœuds_mous" | "weiche_knoten" | "мягкие_узлы" =>
11689            {
11690                let id = self.arg_num(&args, 0, 0.)? as usize;
11691                let mut out = Vec::new();
11692                if let Some(b) = self.soft_bodies.get(id) {
11693                    for n in &b.nodes {
11694                        out.push(Value::Number(n.pos.x as f64));
11695                        out.push(Value::Number(n.pos.y as f64));
11696                        out.push(Value::Number(n.pos.z as f64));
11697                    }
11698                }
11699                return Ok(Value::List(Rc::new(out)));
11700            },
11701
11702            // ── rigid bodies with angular dynamics ──
11703            #[cfg(not(target_arch = "wasm32"))]
11704            "rb_add" | "刚体添加" | "剛体追加" | "강체추가" | "เพิ่มวัตถุแข็ง" | "افزودن_جسم_صلب" | "أضف_جسما_صلبا" | "הוסף_גוף_קשיח" | "سخت_جسم_شامل_کرو" | "ajouter_corps_rigide" | "starrkörper_hinzufügen" | "добавить_твёрдое_тело" =>
11705            {
11706                let x = self.arg_num(&args, 0, 0.)? as f32;
11707                let y = self.arg_num(&args, 1, 0.)? as f32;
11708                let z = self.arg_num(&args, 2, 0.)? as f32;
11709                let mass = self.arg_num(&args, 3, 1.0)? as f32;
11710                let mut b =
11711                    ling_physics::rigid::RigidBody::new(ling_physics::Vec3::new(x, y, z), mass);
11712                b.restitution = 0.6;
11713                return Ok(Value::Number(self.rigid_world.add(b) as f64));
11714            },
11715            #[cfg(not(target_arch = "wasm32"))]
11716            "rb_torque" | "扭矩" | "トルク" | "토크" | "แรงบิด" | "گشتاور" | "عزم_دوران" | "מומנט" | "ٹارک" | "couple_corps_rigide" | "starrkörper_drehmoment" | "крутящий_момент_твёрдого_тела" => {
11717                let i = self.arg_num(&args, 0, 0.)? as usize;
11718                let tx = self.arg_num(&args, 1, 0.)? as f32;
11719                let ty = self.arg_num(&args, 2, 0.)? as f32;
11720                let tz = self.arg_num(&args, 3, 0.)? as f32;
11721                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
11722                    b.apply_torque(ling_physics::Vec3::new(tx, ty, tz));
11723                }
11724                return Ok(Value::Unit);
11725            },
11726            #[cfg(not(target_arch = "wasm32"))]
11727            "rb_spin" | "自旋" | "スピン" | "스핀" | "หมุน" | "چرخش_جسم_صلب" | "دوران_جسم_صلب" | "סיבוב_גוף_קשיח" | "سخت_جسم_گھماؤ" | "spin_corps_rigide" | "starrkörper_spin" | "вращение_твёрдого_тела" => {
11728                let i = self.arg_num(&args, 0, 0.)? as usize;
11729                let wx = self.arg_num(&args, 1, 0.)? as f32;
11730                let wy = self.arg_num(&args, 2, 0.)? as f32;
11731                let wz = self.arg_num(&args, 3, 0.)? as f32;
11732                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
11733                    b.apply_spin(ling_physics::Vec3::new(wx, wy, wz));
11734                }
11735                return Ok(Value::Unit);
11736            },
11737            #[cfg(not(target_arch = "wasm32"))]
11738            "rb_impulse" | "刚体冲量" | "剛体インパルス" | "강체충격" | "แรงดลแข็ง" | "ضربه_جسم_صلب" | "دفعة_جسم_صلب" | "דחף_גוף_קשיח" | "سخت_جسم_دھکا" | "impulsion_corps_rigide" | "starrkörper_impuls" | "импульс_твёрдого_тела" =>
11739            {
11740                let i = self.arg_num(&args, 0, 0.)? as usize;
11741                let ix = self.arg_num(&args, 1, 0.)? as f32;
11742                let iy = self.arg_num(&args, 2, 0.)? as f32;
11743                let iz = self.arg_num(&args, 3, 0.)? as f32;
11744                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
11745                    b.apply_impulse(ling_physics::Vec3::new(ix, iy, iz));
11746                }
11747                return Ok(Value::Unit);
11748            },
11749            #[cfg(not(target_arch = "wasm32"))]
11750            "rb_floor" | "刚体落地" | "剛体着地" | "강체바닥" | "พื้นแข็ง" | "کف_جسم_صلب" | "أرضية_جسم_صلب" | "רצפת_גוף_קשיח" | "سخت_جسم_فرش" | "sol_corps_rigide" | "starrkörper_boden" | "пол_твёрдого_тела" =>
11751            {
11752                let i = self.arg_num(&args, 0, 0.)? as usize;
11753                let fy = self.arg_num(&args, 1, 0.)? as f32;
11754                let rest = self.arg_num(&args, 2, 0.6)? as f32;
11755                let fric = self.arg_num(&args, 3, 0.6)? as f32;
11756                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
11757                    b.bounce_floor(fy, rest, fric);
11758                }
11759                return Ok(Value::Unit);
11760            },
11761            #[cfg(not(target_arch = "wasm32"))]
11762            "rb_gravity" | "刚体重力" | "剛体重力" | "강체중력" | "แรงโน้มถ่วงแข็ง" | "گرانش_جسم_صلب" | "جاذبية_جسم_صلب" | "כבידת_גוף_קשיח" | "سخت_جسم_کشش_ثقل" | "gravité_corps_rigide" | "starrkörper_schwerkraft" | "гравитация_твёрдого_тела" =>
11763            {
11764                let gx = self.arg_num(&args, 0, 0.)? as f32;
11765                let gy = self.arg_num(&args, 1, 9.81)? as f32;
11766                let gz = self.arg_num(&args, 2, 0.)? as f32;
11767                self.rigid_world.gravity = ling_physics::Vec3::new(gx, gy, gz);
11768                return Ok(Value::Unit);
11769            },
11770            #[cfg(not(target_arch = "wasm32"))]
11771            "rb_step" | "刚体步进" | "剛体更新" | "강체스텝" | "ก้าวแข็ง" | "گام_جسم_صلب" | "خطوة_جسم_صلب" | "צעד_גוף_קשיח" | "سخت_جسم_قدم" | "pas_corps_rigide" | "starrkörper_schritt" | "шаг_твёрдого_тела" =>
11772            {
11773                let dt = self.arg_num(&args, 0, 0.016)? as f32;
11774                self.rigid_world.step(dt);
11775                return Ok(Value::Unit);
11776            },
11777            #[cfg(not(target_arch = "wasm32"))]
11778            "rb_pos" | "刚体位置" | "剛体位置" | "강체위치" | "ตำแหน่งแข็ง" | "موقعیت_جسم_صلب" | "موضع_جسم_صلب" | "מיקום_גוף_קשיח" | "سخت_جسم_مقام" | "position_corps_rigide" | "starrkörper_position" | "позиция_твёрдого_тела" =>
11779            {
11780                let i = self.arg_num(&args, 0, 0.)? as usize;
11781                let p = self
11782                    .rigid_world
11783                    .bodies
11784                    .get(i)
11785                    .map(|b| b.pos)
11786                    .unwrap_or(ling_physics::Vec3::ZERO);
11787                return Ok(Value::List(Rc::new(vec![
11788                    Value::Number(p.x as f64),
11789                    Value::Number(p.y as f64),
11790                    Value::Number(p.z as f64),
11791                ])));
11792            },
11793            #[cfg(not(target_arch = "wasm32"))]
11794            "rb_rot" | "刚体旋转" | "剛体回転" | "강체회전" | "การหมุนแข็ง" | "چرخش_وضعية_جسم_صلب" | "دوران_وضعية_جسم_صلب" | "סיבוב_זווית_גוף_קשיח" | "سخت_جسم_گردش" | "rotation_corps_rigide" | "starrkörper_rotation" | "поворот_твёрдого_тела" =>
11795            {
11796                let i = self.arg_num(&args, 0, 0.)? as usize;
11797                let q = self
11798                    .rigid_world
11799                    .bodies
11800                    .get(i)
11801                    .map(|b| b.orientation)
11802                    .unwrap_or(ling_physics::Quat::IDENTITY);
11803                return Ok(Value::List(Rc::new(vec![
11804                    Value::Number(q.x as f64),
11805                    Value::Number(q.y as f64),
11806                    Value::Number(q.z as f64),
11807                    Value::Number(q.w as f64),
11808                ])));
11809            },
11810
11811            // ── native-res mesh (.lmesh): load once, draw fast (unlit, per-tri colour) ──
11812            #[cfg(not(target_arch = "wasm32"))]
11813            "mesh_load" | "โหลดเมช" | "载入网格" | "メッシュ読込" | "메시로드" | "بارگذاری_مش" | "حمّل_شبكة" | "טען_מש" | "میش_لوڈ" =>
11814            {
11815                let path = self.arg_str(&args, 0, "");
11816                let resolved = if std::path::Path::new(&path).exists() {
11817                    path.clone()
11818                } else if let Some(d) = &self.source_dir {
11819                    d.join(&path).to_string_lossy().into_owned()
11820                } else {
11821                    path.clone()
11822                };
11823                let bytes = match std::fs::read(&resolved) {
11824                    Ok(b) => b,
11825                    Err(e) => {
11826                        eprintln!("mesh_load failed ({path}): {e}");
11827                        return Ok(Value::Number(-1.0));
11828                    },
11829                };
11830                if bytes.len() < 16 || &bytes[0..4] != b"LMSH" {
11831                    eprintln!("mesh_load: bad header ({path})");
11832                    return Ok(Value::Number(-1.0));
11833                }
11834                let rd4 =
11835                    |o: usize| -> [u8; 4] { [bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]] };
11836                let height = f32::from_le_bytes(rd4(8));
11837                let ntri = u32::from_le_bytes(rd4(12)) as usize;
11838                let need = 16usize.saturating_add(ntri.saturating_mul(9 * 4 + 3));
11839                if bytes.len() < need {
11840                    eprintln!("mesh_load: truncated ({path})");
11841                    return Ok(Value::Number(-1.0));
11842                }
11843                let mut pos = Vec::with_capacity(ntri * 3);
11844                let mut col = Vec::with_capacity(ntri);
11845                let mut off = 16usize;
11846                for _ in 0..ntri {
11847                    for _k in 0..3 {
11848                        let x = f32::from_le_bytes(rd4(off));
11849                        let y = f32::from_le_bytes(rd4(off + 4));
11850                        let z = f32::from_le_bytes(rd4(off + 8));
11851                        off += 12;
11852                        pos.push([x, y, z]);
11853                    }
11854                    col.push([bytes[off], bytes[off + 1], bytes[off + 2]]);
11855                    off += 3;
11856                }
11857                eprintln!("mesh_load: {} ({} tris, h={:.2})", path, ntri, height);
11858                let id = self.meshes.len();
11859                self.meshes
11860                    .push(crate::gfx::shapes::ColorMesh { pos, col, height });
11861                return Ok(Value::Number(id as f64));
11862            },
11863            #[cfg(target_arch = "wasm32")]
11864            "mesh_load" | "โหลดเมช" | "载入网格" | "メッシュ読込" | "메시로드" | "بارگذاری_مش" | "حمّل_شبكة" | "טען_רשת" | "میش_لوڈ" =>
11865            {
11866                // Native .lmesh loading is file-system based and not wired for wasm yet.
11867                // Return an invalid handle so scripts can choose a fallback path.
11868                return Ok(Value::Number(-1.0));
11869            },
11870            #[cfg(not(target_arch = "wasm32"))]
11871            "mesh_draw" | "วาดเมชสี" | "绘制网格" | "メッシュ描画" | "메시그리기" | "رسم_مش_رنگی" | "ارسم_شبكة_ملونة" | "צייר_רשת_צבעונית" | "رنگین_میش_کھینچو" =>
11872            {
11873                // ('วาดเมช' is taken by draw_mesh — use a distinct Thai alias)
11874                let id = self.arg_num(&args, 0, 0.)? as usize;
11875                let cx = self.arg_num(&args, 1, 0.)? as f32;
11876                let cy = self.arg_num(&args, 2, 0.)? as f32;
11877                let cz = self.arg_num(&args, 3, 0.)? as f32;
11878                let sc = self.arg_num(&args, 4, 1.)? as f32;
11879                let yaw = self.arg_num(&args, 5, 0.)? as f32;
11880                let sway = self.arg_num(&args, 6, 0.)? as f32;
11881                let arm = self.arg_num(&args, 7, 0.)? as f32;
11882                let lean = self.arg_num(&args, 8, 0.)? as f32;
11883                let leg = self.arg_num(&args, 9, 0.)? as f32;
11884                let tuck = self.arg_num(&args, 10, 0.)? as f32;
11885                if id < self.meshes.len() {
11886                    let m = &self.meshes[id];
11887                    let mut gfx = self.gfx.borrow_mut();
11888                    gfx.draw_color_mesh(m, cx, cy, cz, sc, yaw, sway, arm, lean, leg, tuck);
11889                }
11890                return Ok(Value::Unit);
11891            },
11892            #[cfg(target_arch = "wasm32")]
11893            "mesh_draw" | "วาดเมชสี" | "绘制网格" | "メッシュ描画" | "메시그리기" | "رسم_مش_رنگی" | "ارسم_شبكة_ملونة" | "צייר_רשת_צבעונית" | "رنگین_میش_کھینچو" =>
11894            {
11895                return Ok(Value::Unit);
11896            },
11897
11898            // ── liquid sim (water + oil, immiscible) ──
11899            "liquid_new" | "新建液体" | "液体新規" | "액체생성" | "สร้างของเหลว" | "مایع_جدید" | "سائل_جديد" | "נוזל_חדש" | "نیا_مائع" | "nouveau_liquide" | "neue_flüssigkeit" | "новая_жидкость" =>
11900            {
11901                let w = self.arg_num(&args, 0, 64.)? as usize;
11902                let h = self.arg_num(&args, 1, 64.)? as usize;
11903                let id = self.liquids.len();
11904                self.liquids
11905                    .push(ling_physics::liquid::LiquidGrid::new(w, h));
11906                return Ok(Value::Number(id as f64));
11907            },
11908            "liquid_set_colors" | "液体颜色" | "液体配色" | "액체색상" | "สีของเหลว" | "تنظیم_رنگ_مایع" | "عيّن_ألوان_السائل" | "קבע_צבעי_נוזל" | "مائع_رنگ_مقرر_کرو" =>
11909            {
11910                let id = self.arg_num(&args, 0, 0.)? as usize;
11911                let wr = self.arg_num(&args, 1, 40.)? as f32;
11912                let wg = self.arg_num(&args, 2, 110.)? as f32;
11913                let wb = self.arg_num(&args, 3, 235.)? as f32;
11914                let or_ = self.arg_num(&args, 4, 240.)? as f32;
11915                let og = self.arg_num(&args, 5, 175.)? as f32;
11916                let ob = self.arg_num(&args, 6, 45.)? as f32;
11917                if let Some(g) = self.liquids.get_mut(id) {
11918                    g.set_colors(wr, wg, wb, or_, og, ob);
11919                }
11920                return Ok(Value::Unit);
11921            },
11922            "liquid_splat" | "液体注入" | "液体追加" | "액체분사" | "หยดของเหลว" | "پاشش_مایع" | "بقعة_سائل" | "התזת_נוזל" | "مائع_چھینٹا" | "éclaboussure_liquide" | "flüssigkeit_spritzer" | "брызги_жидкости" =>
11923            {
11924                let id = self.arg_num(&args, 0, 0.)? as usize;
11925                let x = self.arg_num(&args, 1, 0.)? as f32;
11926                let y = self.arg_num(&args, 2, 0.)? as f32;
11927                let kind = self.arg_num(&args, 3, 0.)? as i32;
11928                let amt = self.arg_num(&args, 4, 1.0)? as f32;
11929                let rad = self.arg_num(&args, 5, 4.0)? as f32;
11930                if let Some(g) = self.liquids.get_mut(id) {
11931                    g.splat(x, y, kind, amt, rad);
11932                }
11933                return Ok(Value::Unit);
11934            },
11935            "liquid_gravity" | "液体重力" | "液体重力ベクトル" | "액체중력" | "แรงโน้มถ่วงเหลว" | "گرانش_مایع" | "جاذبية_السائل" | "כבידת_נוזל" | "مائع_کشش_ثقل" | "gravité_liquide" | "flüssigkeit_schwerkraft" | "гравитация_жидкости" =>
11936            {
11937                let id = self.arg_num(&args, 0, 0.)? as usize;
11938                let gx = self.arg_num(&args, 1, 0.)? as f32;
11939                let gy = self.arg_num(&args, 2, 60.)? as f32;
11940                if let Some(g) = self.liquids.get_mut(id) {
11941                    g.set_gravity(gx, gy);
11942                }
11943                return Ok(Value::Unit);
11944            },
11945            "liquid_step" | "液体步进" | "液体更新" | "액체스텝" | "ก้าวของเหลว" | "گام_مایع" | "خطوة_السائل" | "צעד_נוזל" | "مائع_قدم" | "pas_liquide" | "flüssigkeit_schritt" | "шаг_жидкости" =>
11946            {
11947                let id = self.arg_num(&args, 0, 0.)? as usize;
11948                let dt = self.arg_num(&args, 1, 0.016)? as f32;
11949                if let Some(g) = self.liquids.get_mut(id) {
11950                    g.step(dt);
11951                }
11952                return Ok(Value::Unit);
11953            },
11954            // liquid_step_all(dt) — advance EVERY liquid grid one tick, in parallel
11955            // across instances (rayon). Independent grids share no state, so this is
11956            // an embarrassingly-parallel batch: a scene with many liquid surfaces
11957            // steps in one call that scales across cores instead of N serial
11958            // `liquid_step` calls.
11959            "liquid_step_all"
11960            | "液体全步进"
11961            | "液体全更新"
11962            | "전체액체스텝"
11963            | "ก้าวของเหลวทั้งหมด" | "گام_همه_مایعات" | "خطوة_كل_السوائل" | "צעד_כל_הנוזלים" | "تمام_مائع_قدم" => {
11964                let dt = self.arg_num(&args, 0, 0.016)? as f32;
11965                ling_physics::liquid::step_all(&mut self.liquids, dt);
11966                return Ok(Value::Unit);
11967            },
11968            // liquid_rainbow(id, on) — colour the fluid as a flowing ROYGBIV marble
11969            "liquid_rainbow" | "液体彩虹" | "液体虹" | "액체무지개" | "ของเหลวสายรุ้ง" | "مایع_رنگین‌کمان" | "سائل_قوس_قزح" | "נוזל_קשת" | "قوس_قزح_مائع" | "arc_en_ciel_liquide" | "flüssigkeit_regenbogen" | "радуга_жидкости" =>
11970            {
11971                let id = self.arg_num(&args, 0, 0.)? as usize;
11972                let on = self.arg_num(&args, 1, 1.0)? > 0.5;
11973                if let Some(g) = self.liquids.get_mut(id) {
11974                    g.rainbow = on;
11975                }
11976                return Ok(Value::Unit);
11977            },
11978            // liquid_mix(id) -> 0 (oil/water separated) .. 1 (fully intermixed)
11979            "liquid_mix" | "液体混合" | "液体混合度" | "액체혼합" | "การผสมของเหลว" | "ترکیب_مایع" | "مزج_سائل" | "ערבוב_נוזל" | "مائع_ملاؤ" | "mélanger_liquide" | "flüssigkeit_mischen" | "смешать_жидкость" =>
11980            {
11981                let id = self.arg_num(&args, 0, 0.)? as usize;
11982                let m = self.liquids.get(id).map(|g| g.mix_amount()).unwrap_or(0.0);
11983                return Ok(Value::Number(m as f64));
11984            },
11985            // liquid_draw(id, sx, sy, scale) — fast flat 2-D blit of the colour field
11986            #[cfg(not(target_arch = "wasm32"))]
11987            "liquid_draw" | "绘制液体" | "液体描画" | "액체그리기" | "วาดของเหลว" | "رسم_مایع" | "ارسم_سائلا" | "צייר_נוזל" | "مائع_کھینچو" | "dessiner_liquide" | "flüssigkeit_zeichnen" | "рисовать_жидкость" =>
11988            {
11989                let id = self.arg_num(&args, 0, 0.)? as usize;
11990                let sx = self.arg_num(&args, 1, 0.)? as i32;
11991                let sy = self.arg_num(&args, 2, 0.)? as i32;
11992                let scale = (self.arg_num(&args, 3, 4.)? as i32).max(1);
11993                if id < self.liquids.len() {
11994                    let (gw, gh) = {
11995                        let g = &self.liquids[id];
11996                        (g.w, g.h)
11997                    };
11998                    let mut gfx = self.gfx.borrow_mut();
11999                    let (w, h) = (gfx.width as i32, gfx.height as i32);
12000                    let g = &self.liquids[id];
12001                    for cy in 0..gh {
12002                        for cx in 0..gw {
12003                            let col = g.sample_rgb(cx, cy);
12004                            let bx = sx + cx as i32 * scale;
12005                            let by = sy + cy as i32 * scale;
12006                            for dy in 0..scale {
12007                                for dx in 0..scale {
12008                                    let px = bx + dx;
12009                                    let py = by + dy;
12010                                    if px >= 0 && py >= 0 && px < w && py < h {
12011                                        gfx.buffer[(py * w + px) as usize] = col;
12012                                    }
12013                                }
12014                            }
12015                        }
12016                    }
12017                }
12018                return Ok(Value::Unit);
12019            },
12020            // liquid_draw_surface(id, kind, cx,cy,cz, radius, height)
12021            //   kind: 0 plane · 1 sphere · 2 cylinder · 3 cone · 4 dome
12022            "liquid_draw_surface" | "液体贴面" | "液体曲面" | "액체곡면" | "ของเหลวบนพื้นผิว" | "رسم_سطح_مایع" | "ارسم_سطح_السائل" | "צייר_משטח_נוזל" | "مائع_سطح_کھینچو" | "dessiner_surface_liquide" | "flüssigkeit_oberfläche_zeichnen" | "рисовать_поверхность_жидкости" =>
12023            {
12024                #[cfg(not(target_arch = "wasm32"))]
12025                {
12026                    let id = self.arg_num(&args, 0, 0.)? as usize;
12027                    let kind = self.arg_num(&args, 1, 1.)? as i32;
12028                    let cx = self.arg_num(&args, 2, 0.)? as f32;
12029                    let cy = self.arg_num(&args, 3, 0.)? as f32;
12030                    let cz = self.arg_num(&args, 4, 0.)? as f32;
12031                    let radius = self.arg_num(&args, 5, 2.0)? as f32;
12032                    let height = self.arg_num(&args, 6, 3.0)? as f32;
12033                    if id < self.liquids.len() {
12034                        let (gw, gh) = {
12035                            let g = &self.liquids[id];
12036                            (g.w, g.h)
12037                        };
12038                        let mut gfx = self.gfx.borrow_mut();
12039                        let (w, h, add) = (gfx.width, gfx.height, gfx.blend == 1);
12040                        let cam = gfx.camera.clone();
12041                        let near = -cam.zdist + 0.05;
12042                        let g = &self.liquids[id];
12043                        let tau = std::f32::consts::TAU;
12044                        let pi = std::f32::consts::PI;
12045                        // surface point for a (u,v) in [0,1] on the chosen primitive
12046                        let sp = |u: f32, v: f32| -> [f32; 3] {
12047                            if kind == 0 {
12048                                [
12049                                    cx + (u - 0.5) * 2.0 * radius,
12050                                    cy,
12051                                    cz + (v - 0.5) * 2.0 * radius,
12052                                ]
12053                            } else if kind == 2 {
12054                                let th = u * tau;
12055                                [
12056                                    cx + th.cos() * radius,
12057                                    cy + (v - 0.5) * height,
12058                                    cz + th.sin() * radius,
12059                                ]
12060                            } else if kind == 3 {
12061                                let th = u * tau;
12062                                let rr = radius * (1.0 - v);
12063                                [
12064                                    cx + th.cos() * rr,
12065                                    cy + (v - 0.5) * height,
12066                                    cz + th.sin() * rr,
12067                                ]
12068                            } else if kind == 4 {
12069                                let th = u * tau;
12070                                let ph = v * pi * 0.5;
12071                                [
12072                                    cx + ph.sin() * th.cos() * radius,
12073                                    cy - ph.cos() * radius,
12074                                    cz + ph.sin() * th.sin() * radius,
12075                                ]
12076                            } else {
12077                                let th = u * tau;
12078                                let ph = v * pi;
12079                                [
12080                                    cx + ph.sin() * th.cos() * radius,
12081                                    cy + ph.cos() * radius,
12082                                    cz + ph.sin() * th.sin() * radius,
12083                                ]
12084                            }
12085                        };
12086                        let nrm = |u: f32, v: f32| -> [f32; 3] {
12087                            if kind == 0 {
12088                                [0.0, -1.0, 0.0]
12089                            } else if kind == 2 {
12090                                let th = u * tau;
12091                                [th.cos(), 0.0, th.sin()]
12092                            } else if kind == 3 {
12093                                let th = u * tau;
12094                                let s = (radius / height.max(0.01)).atan();
12095                                [th.cos() * s.cos(), s.sin(), th.sin() * s.cos()]
12096                            } else if kind == 4 {
12097                                let th = u * tau;
12098                                let ph = v * pi * 0.5;
12099                                [ph.sin() * th.cos(), -ph.cos(), ph.sin() * th.sin()]
12100                            } else {
12101                                let th = u * tau;
12102                                let ph = v * pi;
12103                                [ph.sin() * th.cos(), ph.cos(), ph.sin() * th.sin()]
12104                            }
12105                        };
12106                        let gwf = gw as f32;
12107                        let ghf = gh as f32;
12108                        let mut cyc = 0usize;
12109                        while cyc < gh {
12110                            let mut cxc = 0usize;
12111                            while cxc < gw {
12112                                // cull by the cell centre's outward normal
12113                                let uc = (cxc as f32 + 0.5) / gwf;
12114                                let vc = (cyc as f32 + 0.5) / ghf;
12115                                let c = sp(uc, vc);
12116                                let n = nrm(uc, vc);
12117                                let dc = cam.depth(c[0], c[1], c[2]);
12118                                if dc > near {
12119                                    let cull = kind != 0
12120                                        && cam.depth(
12121                                            c[0] + n[0] * 0.06,
12122                                            c[1] + n[1] * 0.06,
12123                                            c[2] + n[2] * 0.06,
12124                                        ) > dc;
12125                                    if !cull {
12126                                        // project the 4 cell corners → a filled AA vector quad
12127                                        let u0 = cxc as f32 / gwf;
12128                                        let u1 = (cxc + 1) as f32 / gwf;
12129                                        let v0 = cyc as f32 / ghf;
12130                                        let v1 = (cyc + 1) as f32 / ghf;
12131                                        let q = [sp(u0, v0), sp(u1, v0), sp(u1, v1), sp(u0, v1)];
12132                                        let mut poly: Vec<[f32; 2]> = Vec::with_capacity(5);
12133                                        let mut ok = true;
12134                                        for p in &q {
12135                                            if cam.depth(p[0], p[1], p[2]) <= near {
12136                                                ok = false;
12137                                                break;
12138                                            }
12139                                            let (sx, sy, _) = cam.project(p[0], p[1], p[2]);
12140                                            poly.push([sx, sy]);
12141                                        }
12142                                        if ok {
12143                                            let p0 = poly[0];
12144                                            poly.push(p0);
12145                                            let col = g.sample_rgb(cxc, cyc);
12146                                            crate::gfx::raster::fill_contours_aa(
12147                                                &mut gfx.buffer,
12148                                                w,
12149                                                h,
12150                                                col,
12151                                                add,
12152                                                std::slice::from_ref(&poly),
12153                                            );
12154                                        }
12155                                    }
12156                                }
12157                                cxc += 1;
12158                            }
12159                            cyc += 1;
12160                        }
12161                    }
12162                }
12163                #[cfg(target_arch = "wasm32")]
12164                {
12165                    // WASM: liquid_draw_surface is a no-op for now (would need WebGL shader)
12166                    // The liquid simulation still runs, just not rendered to 3D surfaces
12167                }
12168                return Ok(Value::Unit);
12169            },
12170            // sparkle(x, y, w, h, count [, t]) — scatter twinkling vector star-sparkles
12171            // in a rect (snowglobe effect) in the current colour + blend mode.
12172            #[cfg(not(target_arch = "wasm32"))]
12173            "sparkle" | "闪光" | "きらめき" | "반짝임" | "ประกาย" | "درخشش" | "بريق" | "נצנוץ" | "چمک" | "scintillement" | "funkeln" | "искриться" => {
12174                let x = self.arg_num(&args, 0, 0.)? as f32;
12175                let y = self.arg_num(&args, 1, 0.)? as f32;
12176                let ww = self.arg_num(&args, 2, 200.)? as f32;
12177                let hh = self.arg_num(&args, 3, 200.)? as f32;
12178                let count = self.arg_num(&args, 4, 40.)? as i32;
12179                let t = self.arg_num(&args, 5, 0.)? as f32;
12180                let mut gfx = self.gfx.borrow_mut();
12181                let (w, h, add, color) = (gfx.width, gfx.height, gfx.blend == 1, gfx.color);
12182                let (cr, cg, cb) = (
12183                    (color >> 16 & 0xFF) as f32,
12184                    (color >> 8 & 0xFF) as f32,
12185                    (color & 0xFF) as f32,
12186                );
12187                let mut n = 0i32;
12188                while n < count {
12189                    let hsh = (n as u32).wrapping_mul(2654435761).wrapping_add(0x9E3779B9);
12190                    let u = ((hsh >> 8) & 1023) as f32 / 1023.0;
12191                    let v = ((hsh >> 18) & 1023) as f32 / 1023.0;
12192                    let phase = (hsh & 255) as f32 / 255.0;
12193                    let tw = (t * 3.0 + phase * std::f32::consts::TAU + n as f32).sin() * 0.5 + 0.5;
12194                    let sz = 1.5 + tw * 5.0;
12195                    let px = x + u * ww;
12196                    let py = y + v * hh;
12197                    let b = tw * tw; // sharp twinkle
12198                    let col =
12199                        (((cr * b) as u32) << 16) | (((cg * b) as u32) << 8) | ((cb * b) as u32);
12200                    crate::gfx::raster::draw_line_aa(
12201                        &mut gfx.buffer,
12202                        w,
12203                        h,
12204                        col,
12205                        add,
12206                        px - sz,
12207                        py,
12208                        px + sz,
12209                        py,
12210                    );
12211                    crate::gfx::raster::draw_line_aa(
12212                        &mut gfx.buffer,
12213                        w,
12214                        h,
12215                        col,
12216                        add,
12217                        px,
12218                        py - sz,
12219                        px,
12220                        py + sz,
12221                    );
12222                    let d = sz * 0.55;
12223                    crate::gfx::raster::draw_line_aa(
12224                        &mut gfx.buffer,
12225                        w,
12226                        h,
12227                        col,
12228                        add,
12229                        px - d,
12230                        py - d,
12231                        px + d,
12232                        py + d,
12233                    );
12234                    crate::gfx::raster::draw_line_aa(
12235                        &mut gfx.buffer,
12236                        w,
12237                        h,
12238                        col,
12239                        add,
12240                        px - d,
12241                        py + d,
12242                        px + d,
12243                        py - d,
12244                    );
12245                    n += 1;
12246                }
12247                return Ok(Value::Unit);
12248            },
12249
12250            // ══════════════════════════════════════════════════════════════════
12251            // DIALOG BUILTINS  (crates/ling-game/src/dialog.rs) — cinematic,
12252            // typed-out, colour-coded text boxes. Markup: {n}name{/} {p}place{/}
12253            // {i}item{/}, \n newline, || page break.
12254            // ══════════════════════════════════════════════════════════════════
12255            #[cfg(not(target_arch = "wasm32"))]
12256            "dialog_show" | "对话显示" | "会話表示" | "대화표시" | "แสดงบทสนทนา" | "نمایش_گفتگو" | "اعرض_الحوار" | "הצג_דיאלוג" | "مکالمہ_دکھاؤ" | "afficher_dialogue" | "dialog_anzeigen" | "показать_диалог" =>
12257            {
12258                let text = self.arg_str(&args, 0, "");
12259                let cps = self.arg_num(&args, 1, 32.0)? as f32;
12260                self.dialog = Some(ling_game::dialog::Dialog::new(&text, cps));
12261                return Ok(Value::Unit);
12262            },
12263            #[cfg(not(target_arch = "wasm32"))]
12264            "dialog_step" | "对话步进" | "会話更新" | "대화스텝" | "ก้าวบทสนทนา" | "گام_گفتگو" | "خطوة_الحوار" | "צעד_דיאלוג" | "مکالمہ_قدم" | "pas_dialogue" | "dialog_schritt" | "шаг_диалога" =>
12265            {
12266                let dt = self.arg_num(&args, 0, 0.016)? as f32;
12267                if let Some(d) = self.dialog.as_mut() {
12268                    d.update(dt);
12269                }
12270                return Ok(Value::Unit);
12271            },
12272            #[cfg(not(target_arch = "wasm32"))]
12273            "dialog_advance" | "对话推进" | "会話送り" | "대화진행" | "เลื่อนบทสนทนา" | "پیشروی_گفتگو" | "تقدّم_الحوار" | "קדם_דיאלוג" | "مکالمہ_آگے_بڑھاؤ" | "avancer_dialogue" | "dialog_weiter" | "продолжить_диалог" =>
12274            {
12275                if let Some(d) = self.dialog.as_mut() {
12276                    d.advance();
12277                }
12278                return Ok(Value::Unit);
12279            },
12280            #[cfg(not(target_arch = "wasm32"))]
12281            "dialog_active" | "对话激活" | "会話中" | "대화중" | "บทสนทนาทำงาน" | "گفتگو_فعال" | "الحوار_نشط" | "דיאלוג_פעיל" | "مکالمہ_فعال" | "dialogue_actif" | "dialog_aktiv" | "диалог_активен" =>
12282            {
12283                let a = self
12284                    .dialog
12285                    .as_ref()
12286                    .map(|d| !d.is_closed())
12287                    .unwrap_or(false);
12288                return Ok(Value::Bool(a));
12289            },
12290            #[cfg(not(target_arch = "wasm32"))]
12291            "dialog_typing" | "对话打字" | "会話タイプ中" | "대화타이핑" | "กำลังพิมพ์บทสนทนา" | "گفتگو_در_حال_تایپ" | "الحوار_يكتب" | "דיאלוג_מקליד" | "مکالمہ_ٹائپنگ" | "dialogue_frappe" | "dialog_tippen" | "диалог_печатает" =>
12292            {
12293                use ling_game::dialog::Dialog;
12294
12295                let a = self
12296                    .dialog
12297                    .as_ref()
12298                    .map(|d: &Dialog| !d.is_closed() && d.is_typing())
12299                    .unwrap_or(false);
12300                return Ok(Value::Bool(a));
12301            },
12302            #[cfg(not(target_arch = "wasm32"))]
12303            "dialog_close" | "对话关闭" | "会話閉じる" | "대화닫기" | "ปิดบทสนทนา" | "بستن_گفتگو" | "أغلق_الحوار" | "סגור_דיאלוג" | "مکالمہ_بند" | "fermer_dialogue" | "dialog_schließen" | "закрыть_диалог" =>
12304            {
12305                self.dialog = None;
12306                return Ok(Value::Unit);
12307            },
12308            // dialog_color(role, r, g, b) — role: 0 text · 1 name · 2 place · 3 item
12309            #[cfg(not(target_arch = "wasm32"))]
12310            "dialog_color" | "对话颜色" | "会話色" | "대화색" | "สีบทสนทนา" | "رنگ_گفتگو" | "لون_الحوار" | "צבע_דיאלוג" | "مکالمہ_رنگ" | "couleur_dialogue" | "dialog_farbe" | "цвет_диалога" =>
12311            {
12312                let role = (self.arg_num(&args, 0, 0.0)? as usize).min(3);
12313                let r = self.arg_num(&args, 1, 255.0)? as u32 & 0xFF;
12314                let g = self.arg_num(&args, 2, 255.0)? as u32 & 0xFF;
12315                let b = self.arg_num(&args, 3, 255.0)? as u32 & 0xFF;
12316                self.dialog_colors[role] = (r << 16) | (g << 8) | b;
12317                return Ok(Value::Unit);
12318            },
12319            // dialog_draw(x, y, w, h [, font_handle]) — draw the box + typed text
12320            #[cfg(not(target_arch = "wasm32"))]
12321            "dialog_draw" | "对话绘制" | "会話描画" | "대화그리기" | "วาดบทสนทนา" | "رسم_گفتگو" | "ارسم_الحوار" | "צייר_דיאלוג" | "مکالمہ_کھینچو" | "dessiner_dialogue" | "dialog_zeichnen" | "рисовать_диалог" =>
12322            {
12323                let x = self.arg_num(&args, 0, 40.0)? as f32;
12324                let y = self.arg_num(&args, 1, 0.0)? as f32;
12325                let ww = self.arg_num(&args, 2, 720.0)? as f32;
12326                let hh = self.arg_num(&args, 3, 150.0)? as f32;
12327                let font = self.arg_num(&args, 4, -1.0)? as i64;
12328                let t = (crate::runtime::now_secs() - self.start_time_secs) as f32;
12329                self.render_dialog(x, y, ww, hh, font, t);
12330                return Ok(Value::Unit);
12331            },
12332
12333            // text_poll() — fold newly-typed keys into the input buffer, return it.
12334            // Repeat is enabled (KeyRepeat::Yes) so holding a key/Backspace behaves
12335            // like a normal text field; length is capped so a stuck key or a runaway
12336            // script can't grow the buffer without bound.
12337            #[cfg(not(target_arch = "wasm32"))]
12338            "text_poll" => {
12339                const TEXT_BUFFER_MAX: usize = 240;
12340                // See key_down/key_pressed: our topmost fullscreen window can
12341                // be visually in front without real Win32 keyboard focus, so
12342                // WM_KEYDOWN/WM_CHAR (what minifb's get_keys_pressed reads)
12343                // never arrive. Poll the OS key-state table directly instead
12344                // — no focus required — with our own repeat-aware edge
12345                // detection (key_repeat_fire) so holding a key behaves like
12346                // the KeyRepeat::Yes path below: one char on press, then
12347                // repeats after a short hold delay.
12348                #[cfg(windows)]
12349                {
12350                    let topmost = self.gfx.borrow().topmost_window;
12351                    if topmost {
12352                        if !window_is_foreground(self.gfx.borrow().hwnd) {
12353                            return Ok(Value::Str(self.text_buffer.clone()));
12354                        }
12355                        let shift = os_key_down(VK_SHIFT);
12356                        let now = crate::runtime::now_secs();
12357                        let mut gfx = self.gfx.borrow_mut();
12358                        let back_idx = VK_BACK as usize;
12359                        let back_down = os_key_down(VK_BACK);
12360                        let back_was = gfx.raw_keys_prev[back_idx];
12361                        let (mut back_since, mut back_fire) = (
12362                            gfx.raw_keys_down_since[back_idx],
12363                            gfx.raw_keys_last_fire[back_idx],
12364                        );
12365                        if key_repeat_fire(now, back_down, back_was, &mut back_since, &mut back_fire) {
12366                            self.text_buffer.pop();
12367                        }
12368                        gfx.raw_keys_down_since[back_idx] = back_since;
12369                        gfx.raw_keys_last_fire[back_idx] = back_fire;
12370                        gfx.raw_keys_prev[back_idx] = back_down;
12371                        for &vk in TEXT_POLL_VKS {
12372                            let idx = (vk as usize) & 0xFF;
12373                            let down = os_key_down(vk);
12374                            let was = gfx.raw_keys_prev[idx];
12375                            let (mut since, mut fire) =
12376                                (gfx.raw_keys_down_since[idx], gfx.raw_keys_last_fire[idx]);
12377                            if key_repeat_fire(now, down, was, &mut since, &mut fire) {
12378                                if let Some(c) = vk_char(vk, shift) {
12379                                    if self.text_buffer.chars().count() < TEXT_BUFFER_MAX {
12380                                        self.text_buffer.push(c);
12381                                    }
12382                                }
12383                            }
12384                            gfx.raw_keys_down_since[idx] = since;
12385                            gfx.raw_keys_last_fire[idx] = fire;
12386                            gfx.raw_keys_prev[idx] = down;
12387                        }
12388                        return Ok(Value::Str(self.text_buffer.clone()));
12389                    }
12390                }
12391                let (keys, shift) = {
12392                    let gfx = self.gfx.borrow();
12393                    match gfx.window.as_ref() {
12394                        Some(w) => (
12395                            w.get_keys_pressed(minifb::KeyRepeat::Yes),
12396                            w.is_key_down(minifb::Key::LeftShift)
12397                                || w.is_key_down(minifb::Key::RightShift),
12398                        ),
12399                        None => (Vec::new(), false),
12400                    }
12401                };
12402                for k in keys {
12403                    if k == minifb::Key::Backspace {
12404                        self.text_buffer.pop();
12405                    } else if let Some(c) = key_char(k, shift) {
12406                        if self.text_buffer.chars().count() < TEXT_BUFFER_MAX {
12407                            self.text_buffer.push(c);
12408                        }
12409                    }
12410                }
12411                return Ok(Value::Str(self.text_buffer.clone()));
12412            },
12413            #[cfg(target_arch = "wasm32")]
12414            "text_poll" => {
12415                return Ok(Value::Str(self.text_buffer.clone()));
12416            },
12417            "text_get" => return Ok(Value::Str(self.text_buffer.clone())),
12418            "text_set" => {
12419                self.text_buffer = self.arg_str(&args, 0, "");
12420                return Ok(Value::Unit);
12421            },
12422            "text_clear" => {
12423                self.text_buffer.clear();
12424                return Ok(Value::Unit);
12425            },
12426            // record_frame() — append the current framebuffer as a PPM, return frame #
12427            #[cfg(not(target_arch = "wasm32"))]
12428            "record_frame" => {
12429                let n = self.record_n;
12430                let (buf, w, h) = {
12431                    let gfx = self.gfx.borrow();
12432                    (gfx.buffer.clone(), gfx.width, gfx.height)
12433                };
12434                let _ = std::fs::create_dir_all("recordings");
12435                let mut out = Vec::with_capacity(w * h * 3 + 32);
12436                out.extend_from_slice(format!("P6\n{w} {h}\n255\n").as_bytes());
12437                for px in &buf {
12438                    let p = *px;
12439                    out.push((p >> 16) as u8);
12440                    out.push((p >> 8) as u8);
12441                    out.push(p as u8);
12442                }
12443                let _ = std::fs::write(format!("recordings/frame_{n:05}.ppm"), out);
12444                self.record_n += 1;
12445                return Ok(Value::Number(n as f64));
12446            },
12447            "record_count" => return Ok(Value::Number(self.record_n as f64)),
12448            // ── screenshot(mode) → PNG in ./screenshots/ with timestamp + mode + size ──
12449            #[cfg(not(target_arch = "wasm32"))]
12450            "screenshot" | "บันทึกภาพ" | "عکس‌صفحه" | "لقطة_شاشة" | "צילום_מסך" | "اسکرین_شاٹ" => {
12451                let mode = self.arg_str(&args, 0, "game");
12452                let (buf, w, h) = {
12453                    let gfx = self.gfx.borrow();
12454                    (gfx.buffer.clone(), gfx.width, gfx.height)
12455                };
12456                let _ = std::fs::create_dir_all("screenshots");
12457                let ts = std::time::SystemTime::now()
12458                    .duration_since(std::time::UNIX_EPOCH)
12459                    .map(|d| d.as_secs())
12460                    .unwrap_or(0);
12461                let safe: String = mode
12462                    .chars()
12463                    .map(|c| if c.is_alphanumeric() { c } else { '_' })
12464                    .collect();
12465                let path = format!("screenshots/ss_{ts}_{safe}_{w}x{h}.png");
12466                let mut rgb = Vec::with_capacity(w * h * 3);
12467                for px in &buf {
12468                    let p = *px;
12469                    rgb.push((p >> 16) as u8);
12470                    rgb.push((p >> 8) as u8);
12471                    rgb.push(p as u8);
12472                }
12473                if let Some(img) = image::RgbImage::from_raw(w as u32, h as u32, rgb) {
12474                    let _ = img.save(&path);
12475                }
12476                return Ok(Value::Str(path));
12477            },
12478            // ── microphone → crypto donut ──
12479            // mic_capture() — append the latest mic samples to the record buffer
12480            // (call each frame while recording). Returns the buffer length.
12481            #[cfg(not(target_arch = "wasm32"))]
12482            "mic_capture" => {
12483                if let Some(mic) = self.mic.as_ref() {
12484                    let s = mic.latest_samples();
12485                    self.mic_buffer.extend_from_slice(&s);
12486                    let cap = 96_000usize; // ~2 s @ 48 kHz
12487                    if self.mic_buffer.len() > cap {
12488                        let drop = self.mic_buffer.len() - cap;
12489                        self.mic_buffer.drain(0..drop);
12490                    }
12491                }
12492                return Ok(Value::Number(self.mic_buffer.len() as f64));
12493            },
12494            // mic_seed() — SHA3-256 hex of the recorded audio, usable as a donut seed
12495            #[cfg(not(target_arch = "wasm32"))]
12496            "mic_seed" => {
12497                let mut bytes = Vec::with_capacity(self.mic_buffer.len() * 4);
12498                for f in &self.mic_buffer {
12499                    bytes.extend_from_slice(&f.to_le_bytes());
12500                }
12501                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(&bytes))));
12502            },
12503            #[cfg(not(target_arch = "wasm32"))]
12504            "mic_clear" => {
12505                self.mic_buffer.clear();
12506                return Ok(Value::Number(0.0));
12507            },
12508            // flush the 3-D depth queue onto the framebuffer WITHOUT presenting,
12509            // so 2-D UI drawn afterwards overlays the 3-D scene.
12510            #[cfg(not(target_arch = "wasm32"))]
12511            "flush_3d" | "render_3d" => {
12512                let mut gfx = self.gfx.borrow_mut();
12513                if !gfx.depth_queue.is_empty() {
12514                    let w = gfx.width;
12515                    let h = gfx.height;
12516                    let dt = gfx.depth_test;
12517                    let reset_z = gfx.zbuf_needs_clear;
12518                    let (bm, ba) = (gfx.blend, gfx.alpha);
12519                    let aa = gfx.antialias;
12520                    let queue = std::mem::take(&mut gfx.depth_queue);
12521                    {
12522                        let g = &mut *gfx;
12523                        let z = if dt { Some(&mut g.depth_buf) } else { None };
12524                        queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
12525                    }
12526                    gfx.zbuf_needs_clear = false;
12527                    gfx.depth_queue.set_state(bm, ba); // keep active blend/alpha across the mid-frame flush
12528                }
12529                return Ok(Value::Unit);
12530            },
12531            #[cfg(target_arch = "wasm32")]
12532            "flush_3d" | "render_3d" => {
12533                let mut gfx = self.gfx.borrow_mut();
12534                if !gfx.depth_queue.is_empty() {
12535                    let w = gfx.width;
12536                    let h = gfx.height;
12537                    let dt = gfx.depth_test;
12538                    let reset_z = gfx.zbuf_needs_clear;
12539                    let (bm, ba) = (gfx.blend, gfx.alpha);
12540                    let aa = gfx.antialias;
12541                    let queue = std::mem::take(&mut gfx.depth_queue);
12542                    {
12543                        let g = &mut *gfx;
12544                        let z = if dt { Some(&mut g.depth_buf) } else { None };
12545                        queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
12546                    }
12547                    gfx.zbuf_needs_clear = false;
12548                    gfx.depth_queue.set_state(bm, ba);
12549                }
12550                return Ok(Value::Unit);
12551            },
12552
12553            // flush_post() — flush the 3-D queue like `flush_3d`, then run the
12554            // toon post-chain (SSAO → outlines → tone ramp → bloom → FXAA) over
12555            // the SCENE immediately. `present` skips the chain this frame, so
12556            // 2-D UI drawn after this call stays exact — no bloom/blur on HUDs.
12557            "flush_post" | "post_now" | "포스트플러시" | "后期冲刷" => {
12558                let mut gfx = self.gfx.borrow_mut();
12559                if !gfx.depth_queue.is_empty() {
12560                    let w = gfx.width;
12561                    let h = gfx.height;
12562                    let dt = gfx.depth_test;
12563                    let reset_z = gfx.zbuf_needs_clear;
12564                    let (bm, ba) = (gfx.blend, gfx.alpha);
12565                    let aa = gfx.antialias;
12566                    let queue = std::mem::take(&mut gfx.depth_queue);
12567                    {
12568                        let g = &mut *gfx;
12569                        let z = if dt { Some(&mut g.depth_buf) } else { None };
12570                        queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
12571                    }
12572                    gfx.zbuf_needs_clear = false;
12573                    gfx.depth_queue.set_state(bm, ba);
12574                }
12575                gfx.toon_post_process();
12576                gfx.post_done = true;
12577                return Ok(Value::Unit);
12578            },
12579
12580            // Viscous full-screen distortion (warp/pucker/bloat, edge-wrapped). Call
12581            // after the 3-D flush and before the UI so only the world layer warps.
12582            #[cfg(not(target_arch = "wasm32"))]
12583            "screen_distort" | "บิดจอ" | "屏幕扭曲" | "画面歪み" | "화면왜곡" | "اعوجاج_صفحه" | "شوّه_الشاشة" | "עוות_מסך" | "اسکرین_ڈسٹورٹ" =>
12584            {
12585                let amount = self.arg_num(&args, 0, 8.0)? as f32;
12586                let t = self.arg_num(&args, 1, 0.0)? as f32;
12587                // optional `step` (default 1 = full res): 2 = half-res block warp
12588                // (~4× fewer warp computes, slightly softer — suits a liquid look).
12589                let step = self.arg_num(&args, 2, 1.0)?.max(1.0) as usize;
12590                let _d = std::time::Instant::now();
12591                self.gfx.borrow_mut().distort(amount, t, step);
12592                ling_phase_add(phase::DISTORT, _d.elapsed().as_nanos());
12593                return Ok(Value::Unit);
12594            },
12595
12596            "set_rim" | "设置边缘光" | "リム設定" | "림라이트" | "ตั้งขอบเรือง" | "تنظیم_نور_لبه" | "عيّن_إضاءة_الحافة" | "קבע_תאורת_קצה" | "رم_لائٹ_مقرر_کرو" | "définir_contour_lumineux" | "rimlicht_setzen" | "задать_контурный_свет" =>
12597            {
12598                let s = self.arg_num(&args, 0, 0.6)? as f32;
12599                let r = self.arg_num(&args, 1, 115.)? as f32 / 255.0;
12600                let g = self.arg_num(&args, 2, 217.)? as f32 / 255.0;
12601                let b = self.arg_num(&args, 3, 255.)? as f32 / 255.0;
12602                let mut gfx = self.gfx.borrow_mut();
12603                gfx.shade.rim = s;
12604                gfx.shade.rim_color = [r, g, b];
12605                return Ok(Value::Unit);
12606            },
12607
12608            // ══════════════════════════════════════════════════════════════════
12609            // 3-D PRIMITIVES  (src/gfx/shapes.rs)  — "Inkscape for 3-D"
12610            //   shape(cx,cy,cz,  sx,sy,sz,  rx,ry,rz,  mode,  e0,e1,e2)
12611            //     centre (cx,cy,cz), per-axis scale, Euler rotation (radians),
12612            //     mode: 0 filled · 1 wireframe · 2 both,
12613            //     e0..e2: shape-specific (segments / sides / ratio …).
12614            //   Pen colour (set_color) drives fill lighting and wireframe colour.
12615            // ══════════════════════════════════════════════════════════════════
12616            n if crate::gfx::shapes::canon(n).is_some() => {
12617                let kind = crate::gfx::shapes::canon(n).unwrap();
12618                let cx = self.arg_num(&args, 0, 0.)? as f32;
12619                let cy = self.arg_num(&args, 1, 0.)? as f32;
12620                let cz = self.arg_num(&args, 2, 0.)? as f32;
12621                let sx = self.arg_num(&args, 3, 1.)? as f32;
12622                let sy = self.arg_num(&args, 4, 1.)? as f32;
12623                let sz = self.arg_num(&args, 5, 1.)? as f32;
12624                let rx = self.arg_num(&args, 6, 0.)? as f32;
12625                let ry = self.arg_num(&args, 7, 0.)? as f32;
12626                let rz = self.arg_num(&args, 8, 0.)? as f32;
12627                let mode = self.arg_num(&args, 9, 0.)? as i32;
12628                let e0 = self.arg_num(&args, 10, 0.)? as f32;
12629                let e1 = self.arg_num(&args, 11, 0.)? as f32;
12630                let e2 = self.arg_num(&args, 12, 0.)? as f32;
12631                if let Some(mesh) = crate::gfx::shapes::build(
12632                    kind,
12633                    [cx, cy, cz, sx, sy, sz, rx, ry, rz],
12634                    e0,
12635                    e1,
12636                    e2,
12637                ) {
12638                    let mut gfx = self.gfx.borrow_mut();
12639                    gfx.emit_mesh(&mesh, mode);
12640                }
12641                return Ok(Value::Unit);
12642            },
12643
12644            _ => {},
12645        }
12646
12647        // `form` struct constructor: positional `Name(v0, v1, ...)`.
12648        if let Some(field_names) = self.structs.get(name).cloned() {
12649            if args.len() != field_names.len() {
12650                return Err(EvalErr::from(format!(
12651                    "{name} expects {} field(s), got {}",
12652                    field_names.len(),
12653                    args.len()
12654                )));
12655            }
12656            let fields = field_names.into_iter().zip(args).collect();
12657            return Ok(Value::Struct { name: name.to_string(), fields });
12658        }
12659
12660        // `choose` enum variant constructor: `Variant(...)` or `Enum::Variant(...)`.
12661        if let Some((enum_name, arity)) = self.enum_variants.get(name).cloned() {
12662            if args.len() != arity {
12663                return Err(EvalErr::from(format!(
12664                    "{name} expects {arity} value(s), got {}",
12665                    args.len()
12666                )));
12667            }
12668            let variant = name.rsplit("::").next().unwrap_or(name).to_string();
12669            return Ok(Value::Variant { enum_name, variant, payload: args });
12670        }
12671
12672        #[cfg(target_arch = "wasm32")]
12673        if let Some(v) = wasm_unsupported_builtin(name) {
12674            return Ok(v);
12675        }
12676
12677        Err(EvalErr::from(format!("unknown function '{name}'")))
12678    }
12679
12680    fn call_value(&mut self, v: Value, args: Vec<Value>) -> EvalResult {
12681        match v {
12682            Value::Fn(params, body, mut captured) => {
12683                for (p, a) in params.iter().zip(args) {
12684                    captured.insert(p.clone(), a);
12685                }
12686                match self.framed("<closure>", |me| me.exec_block(&body, &mut captured)) {
12687                    Ok(v) => Ok(v.unwrap_or(Value::Unit)),
12688                    Err(EvalErr::Return(v)) => Ok(v),
12689                    Err(e) => Err(e),
12690                }
12691            },
12692            other => Err(EvalErr::from(format!("cannot call {:?}", other))),
12693        }
12694    }
12695
12696    fn call_method(&self, recv: Value, method: &str, args: Vec<Value>) -> EvalResult {
12697        match (&recv, method) {
12698            (Value::Str(s), "is_empty" | "是空") => Ok(Value::Bool(s.is_empty())),
12699            // All of `lingfu normalize`'s per-language spellings of len/push
12700            // (see ling-fu normalize.rs alias table), not just the Chinese
12701            // ones — normalize rewrites method calls into whichever language
12702            // the project is normalized to, and any spelling missing here
12703            // makes those calls un-callable post-normalize (first hit with
12704            // `.长度()`, then again with Thai `.ความยาว()`).
12705            (Value::Str(s), "len" | "长" | "长度" | "長さ" | "길이" | "ความยาว") => Ok(Value::Number(s.len() as f64)),
12706            (Value::Str(s), "to_string" | "转文") => Ok(Value::Str(s.clone())),
12707            (Value::Str(s), "contains" | "包含") => {
12708                if let Some(Value::Str(sub)) = args.first() {
12709                    Ok(Value::Bool(s.contains(sub.as_str())))
12710                } else {
12711                    Ok(Value::Bool(false))
12712                }
12713            },
12714            (Value::Str(s), "push_str" | "推_文") => {
12715                let mut s2 = s.clone();
12716                if let Some(Value::Str(a)) = args.first() {
12717                    s2.push_str(a);
12718                }
12719                Ok(Value::Str(s2))
12720            },
12721            (Value::List(v), "len" | "长" | "长度" | "長さ" | "길이" | "ความยาว") => Ok(Value::Number(v.len() as f64)),
12722            (Value::List(v), "push" | "推" | "添加" | "追加" | "추가" | "เพิ่ม") => {
12723                let mut v2: Vec<Value> = (**v).clone();
12724                if let Some(a) = args.first() {
12725                    v2.push(a.clone());
12726                }
12727                Ok(Value::List(Rc::new(v2)))
12728            },
12729            // `form` field access: `point.x` (no-arg method == field read).
12730            (Value::Struct { fields, .. }, _) if args.is_empty() => fields
12731                .iter()
12732                .find(|(k, _)| k == method)
12733                .map(|(_, v)| v.clone())
12734                .ok_or_else(|| EvalErr::from(format!("no field '{method}' on {recv}"))),
12735            // Enum introspection: `.tag` → variant name, `.is(Name)` not needed for now.
12736            (Value::Variant { variant, .. }, "tag" | "标签" | "タグ" | "태그" | "ป้าย")
12737                if args.is_empty() =>
12738            {
12739                Ok(Value::Str(variant.clone()))
12740            },
12741            (Value::Ok(inner), _) | (Value::Err(inner), _) => Ok(*inner.clone()),
12742            _ => Err(EvalErr::from(format!("no method '{method}' on {recv}"))),
12743        }
12744    }
12745
12746    // ─── Pattern matching ─────────────────────────────────────────────────────
12747
12748    fn match_pattern(&self, pat: &Pattern, val: &Value) -> Option<Env> {
12749        match (pat, val) {
12750            (Pattern::Wildcard, _) => Some(new_env()),
12751            (Pattern::Str(s), Value::Str(v)) if s == v => Some(new_env()),
12752            (Pattern::Number(n), Value::Number(v)) if (n - v).abs() < 1e-12 => Some(new_env()),
12753            (Pattern::Bool(b), Value::Bool(v)) if b == v => Some(new_env()),
12754            (Pattern::Ident(name), _) => {
12755                let mut e = new_env();
12756                e.insert(name.clone(), val.clone());
12757                Some(e)
12758            },
12759            (Pattern::Constructor(ctor, inner_pat), _) => {
12760                let (matches, inner_val) = match (ctor.as_str(), val) {
12761                    ("ok" | "好", Value::Ok(v)) => (true, Some(v.as_ref().clone())),
12762                    ("bad" | "坏", Value::Err(v)) => (true, Some(v.as_ref().clone())),
12763                    ("ok" | "好", v) if !matches!(v, Value::Err(_)) => (true, Some(v.clone())),
12764                    _ => (false, None),
12765                };
12766                if !matches {
12767                    return None;
12768                }
12769                match (inner_pat, inner_val) {
12770                    (Some(p), Some(v)) => self.match_pattern(p, &v),
12771                    (None, _) => Some(new_env()),
12772                    (Some(p), None) => self.match_pattern(p, &Value::Unit),
12773                }
12774            },
12775            // User enum variant pattern: `Circle(r)`, `Pair(a, b)`, nullary `Origin()`.
12776            (Pattern::Variant(vname, sub_pats), Value::Variant { variant, payload, .. }) => {
12777                if vname != variant || sub_pats.len() != payload.len() {
12778                    return None;
12779                }
12780                let mut bindings = new_env();
12781                for (p, v) in sub_pats.iter().zip(payload.iter()) {
12782                    bindings.extend(self.match_pattern(p, v)?);
12783                }
12784                Some(bindings)
12785            },
12786            // A zero-payload variant pattern also matches the bare result-style `ok`/`bad`
12787            // values so `Ok()`-style patterns keep working uniformly.
12788            (Pattern::Variant(vname, sub), Value::Ok(v)) if (vname == "ok" || vname == "好") => {
12789                match sub.as_slice() {
12790                    [] => Some(new_env()),
12791                    [p] => self.match_pattern(p, v),
12792                    _ => None,
12793                }
12794            },
12795            (Pattern::Variant(vname, sub), Value::Err(v))
12796                if (vname == "bad" || vname == "坏" || vname == "err") =>
12797            {
12798                match sub.as_slice() {
12799                    [] => Some(new_env()),
12800                    [p] => self.match_pattern(p, v),
12801                    _ => None,
12802                }
12803            },
12804            _ => None,
12805        }
12806    }
12807
12808    // ─── Utilities ───────────────────────────────────────────────────────────
12809
12810    fn value_to_iter(&self, val: Value) -> Result<Vec<Value>, EvalErr> {
12811        match val {
12812            Value::List(v) => Ok(Rc::try_unwrap(v).unwrap_or_else(|rc| (*rc).clone())),
12813            Value::Str(s) => Ok(s.chars().map(|c| Value::Str(c.to_string())).collect()),
12814            Value::Number(n) => Ok((0..n as i64).map(|i| Value::Number(i as f64)).collect()),
12815            other => Err(EvalErr::from(format!("cannot iterate over {:?}", other))),
12816        }
12817    }
12818
12819    pub(crate) fn is_truthy(&self, val: &Value) -> bool {
12820        match val {
12821            Value::Bool(b) => *b,
12822            Value::Unit => false,
12823            Value::Number(n) => *n != 0.0,
12824            Value::Str(s) => !s.is_empty(),
12825            Value::List(v) => !v.is_empty(),
12826            Value::Ok(_) => true,
12827            Value::Err(_) => false,
12828            Value::Fn(_, _, _) => true,
12829            Value::Struct { .. } => true,
12830            Value::Variant { .. } => true,
12831        }
12832    }
12833
12834    fn to_number(&self, val: &Value) -> Result<f64, EvalErr> {
12835        match val {
12836            Value::Number(n) => Ok(*n),
12837            Value::Str(s) => s
12838                .parse()
12839                .map_err(|_| EvalErr::from(format!("cannot convert '{s}' to number"))),
12840            other => Err(EvalErr::from(format!("expected number, got {:?}", other))),
12841        }
12842    }
12843
12844    /// Get the n-th argument as f64, falling back to `default` if missing.
12845    fn arg_num(&self, args: &[Value], n: usize, default: f64) -> Result<f64, EvalErr> {
12846        match args.get(n) {
12847            Some(v) => self.to_number(v),
12848            None => Ok(default),
12849        }
12850    }
12851
12852    fn arg_str(&self, args: &[Value], n: usize, default: &str) -> String {
12853        args.get(n)
12854            .map(|v| v.to_string())
12855            .unwrap_or_else(|| default.to_string())
12856    }
12857
12858    /// Read a list-of-numbers argument as `Vec<f32>` (empty if absent/not a list).
12859    #[allow(dead_code)]
12860    fn arg_list_f32(&self, args: &[Value], n: usize) -> Vec<f32> {
12861        match args.get(n) {
12862            Some(Value::List(v)) => v
12863                .iter()
12864                .filter_map(|x| match x {
12865                    Value::Number(n) => Some(*n as f32),
12866                    _ => None,
12867                })
12868                .collect(),
12869            _ => Vec::new(),
12870        }
12871    }
12872
12873    /// Optional `r,g,b` colour override starting at arg `i` → packed 0x00RRGGBB,
12874    /// or `default` if those three numeric args aren't present.
12875    #[cfg(not(target_arch = "wasm32"))]
12876    fn color_at(&self, args: &[Value], i: usize, default: u32) -> u32 {
12877        match (args.get(i), args.get(i + 1), args.get(i + 2)) {
12878            (Some(a), Some(b), Some(c)) => {
12879                match (self.to_number(a), self.to_number(b), self.to_number(c)) {
12880                    (Ok(r), Ok(g), Ok(bl)) => {
12881                        ((r as u32 & 0xFF) << 16) | ((g as u32 & 0xFF) << 8) | (bl as u32 & 0xFF)
12882                    },
12883                    _ => default,
12884                }
12885            },
12886            _ => default,
12887        }
12888    }
12889
12890    /// A pitch argument: a note-name string (`"C4"`, `"A#3"`) or a numeric MIDI value.
12891    #[cfg(not(target_arch = "wasm32"))]
12892    fn pitch_arg(&self, args: &[Value], i: usize, default: i32) -> i32 {
12893        match args.get(i) {
12894            Some(Value::Str(s)) => ling_music::note::parse_pitch(s).unwrap_or(default),
12895            Some(Value::Number(n)) => *n as i32,
12896            _ => default,
12897        }
12898    }
12899
12900    /// Current mouse position + left-button-down (native window only).
12901    #[cfg(not(target_arch = "wasm32"))]
12902    fn mouse_now(&self) -> (f32, f32, bool) {
12903        let gfx = self.gfx.borrow();
12904        let (mx, my) = gfx
12905            .window
12906            .as_ref()
12907            .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
12908            .unwrap_or((0.0, 0.0));
12909        let down = gfx
12910            .window
12911            .as_ref()
12912            .map(|w| w.get_mouse_down(minifb::MouseButton::Left))
12913            .unwrap_or(false);
12914        (mx, my, down)
12915    }
12916
12917    /// Rasterize a UI [`ling_ui::widgets::Draw`] into the framebuffer: filled
12918    /// polygons via the AA scanline fill, polylines via AA lines, honouring the
12919    /// current blend mode.
12920    #[cfg(not(target_arch = "wasm32"))]
12921    fn draw_ui(&self, d: &ling_ui::widgets::Draw) {
12922        let mut gfx = self.gfx.borrow_mut();
12923        let (w, h, add) = (gfx.width, gfx.height, gfx.blend == 1);
12924        for (c, poly) in &d.fills {
12925            crate::gfx::raster::fill_contours_aa(
12926                &mut gfx.buffer,
12927                w,
12928                h,
12929                *c,
12930                add,
12931                std::slice::from_ref(poly),
12932            );
12933        }
12934        for (c, pl) in &d.strokes {
12935            for s in pl.windows(2) {
12936                crate::gfx::raster::draw_line_aa(
12937                    &mut gfx.buffer,
12938                    w,
12939                    h,
12940                    *c,
12941                    add,
12942                    s[0][0],
12943                    s[0][1],
12944                    s[1][0],
12945                    s[1][1],
12946                );
12947            }
12948        }
12949    }
12950
12951    /// Parse (dst_x, dst_y, width, height) from the first four args of a tex_* builtin.
12952    fn tex_rect(&self, args: &[Value]) -> Result<(usize, usize, usize, usize), EvalErr> {
12953        let tx = self.arg_num(args, 0, 0.0)? as usize;
12954        let ty = self.arg_num(args, 1, 0.0)? as usize;
12955        let tw = self.arg_num(args, 2, 256.0)? as usize;
12956        let th = self.arg_num(args, 3, 256.0)? as usize;
12957        Ok((tx, ty, tw.max(1), th.max(1)))
12958    }
12959
12960    pub(crate) fn apply_binop(&self, op: &BinOp, l: Value, r: Value) -> EvalResult {
12961        match op {
12962            BinOp::Add => match (l, r) {
12963                (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a + b)),
12964                (Value::Str(a), Value::Str(b)) => Ok(Value::Str(a + &b)),
12965                (Value::Str(a), b) => Ok(Value::Str(a + &b.to_string())),
12966                (a, Value::Str(b)) => Ok(Value::Str(a.to_string() + &b)),
12967                (a, b) => Err(EvalErr::from(format!("cannot add {:?} and {:?}", a, b))),
12968            },
12969            BinOp::Sub => Ok(Value::Number(self.to_number(&l)? - self.to_number(&r)?)),
12970            BinOp::Mul => Ok(Value::Number(self.to_number(&l)? * self.to_number(&r)?)),
12971            BinOp::Div => Ok(Value::Number(self.to_number(&l)? / self.to_number(&r)?)),
12972            BinOp::Rem => Ok(Value::Number(self.to_number(&l)? % self.to_number(&r)?)),
12973            BinOp::Eq => Ok(Value::Bool(values_equal(&l, &r))),
12974            BinOp::Ne => Ok(Value::Bool(!values_equal(&l, &r))),
12975            BinOp::Lt => Ok(Value::Bool(self.to_number(&l)? < self.to_number(&r)?)),
12976            BinOp::Gt => Ok(Value::Bool(self.to_number(&l)? > self.to_number(&r)?)),
12977            BinOp::Le => Ok(Value::Bool(self.to_number(&l)? <= self.to_number(&r)?)),
12978            BinOp::Ge => Ok(Value::Bool(self.to_number(&l)? >= self.to_number(&r)?)),
12979            BinOp::And => Ok(Value::Bool(self.is_truthy(&l) && self.is_truthy(&r))),
12980            BinOp::Or => Ok(Value::Bool(self.is_truthy(&l) || self.is_truthy(&r))),
12981        }
12982    }
12983
12984    fn builtin_format(&self, args: &[Value]) -> Result<String, EvalErr> {
12985        if args.is_empty() {
12986            return Ok(String::new());
12987        }
12988        let fmt = match &args[0] {
12989            Value::Str(s) => s.clone(),
12990            other => return Ok(other.to_string()),
12991        };
12992
12993        let mut result = String::new();
12994        let mut arg_idx = 1usize;
12995        let mut chars = fmt.chars().peekable();
12996        while let Some(c) = chars.next() {
12997            if c == '{' {
12998                if chars.peek() == Some(&'}') {
12999                    chars.next();
13000                    if arg_idx < args.len() {
13001                        result.push_str(&args[arg_idx].to_string());
13002                        arg_idx += 1;
13003                    }
13004                } else {
13005                    let mut spec = String::new();
13006                    for ch in chars.by_ref() {
13007                        if ch == '}' {
13008                            break;
13009                        }
13010                        spec.push(ch);
13011                    }
13012                    if arg_idx < args.len() {
13013                        if let Some(suffix) = spec.strip_prefix(":.") {
13014                            if let Value::Number(n) = &args[arg_idx] {
13015                                let prec: usize =
13016                                    suffix.trim_end_matches('f').parse().unwrap_or(2);
13017                                result.push_str(&format!("{:.prec$}", n));
13018                                arg_idx += 1;
13019                                continue;
13020                            }
13021                        }
13022                        result.push_str(&args[arg_idx].to_string());
13023                        arg_idx += 1;
13024                    }
13025                }
13026            } else {
13027                result.push(c);
13028            }
13029        }
13030        Ok(result)
13031    }
13032}
13033
13034#[cfg(not(target_arch = "wasm32"))]
13035/// Map a friendly button name (any vendor / d-pad alias) to a gamepad button.
13036#[cfg(not(target_arch = "wasm32"))]
13037fn parse_pad_button(name: &str) -> Option<ling_input::GamepadButton> {
13038    use ling_input::GamepadButton as B;
13039    Some(match name.to_ascii_lowercase().as_str() {
13040        "a" | "south" | "cross" => B::South,
13041        "b" | "east" | "circle" => B::East,
13042        "x" | "west" | "square" => B::West,
13043        "y" | "north" | "triangle" => B::North,
13044        "lb" | "l1" | "left_shoulder" => B::LeftShoulder,
13045        "rb" | "r1" | "right_shoulder" => B::RightShoulder,
13046        "lt" | "l2" | "left_trigger" => B::LeftTrigger,
13047        "rt" | "r2" | "right_trigger" => B::RightTrigger,
13048        "start" | "menu" | "options" | "démarrer" | "начать" => B::Start,
13049        "select" | "back" | "share" | "view" => B::Select,
13050        "guide" | "home" => B::Guide,
13051        "l3" | "left_stick" => B::LeftStick,
13052        "r3" | "right_stick" => B::RightStick,
13053        "up" | "dpad_up" => B::DpadUp,
13054        "down" | "dpad_down" => B::DpadDown,
13055        "left" | "dpad_left" => B::DpadLeft,
13056        "right" | "dpad_right" => B::DpadRight,
13057        _ => return None,
13058    })
13059}
13060
13061#[cfg(not(target_arch = "wasm32"))]
13062fn str_to_minifb_key(name: &str) -> Option<minifb::Key> {
13063    use minifb::Key;
13064    Some(match name {
13065        "numpad0" | "kp0" => Key::NumPad0,
13066        "numpad1" | "kp1" => Key::NumPad1,
13067        "numpad2" | "kp2" => Key::NumPad2,
13068        "numpad3" | "kp3" => Key::NumPad3,
13069        "numpad4" | "kp4" => Key::NumPad4,
13070        "numpad5" | "kp5" => Key::NumPad5,
13071        "numpad6" | "kp6" => Key::NumPad6,
13072        "numpad7" | "kp7" => Key::NumPad7,
13073        "numpad8" | "kp8" => Key::NumPad8,
13074        "numpad9" | "kp9" => Key::NumPad9,
13075        "numpad+" | "kp+" => Key::NumPadPlus,
13076        "numpad-" | "kp-" => Key::NumPadMinus,
13077        "numpad*" | "kp*" => Key::NumPadAsterisk,
13078        "numpad/" | "kp/" => Key::NumPadSlash,
13079        "left" => Key::Left,
13080        "right" => Key::Right,
13081        "up" => Key::Up,
13082        "down" => Key::Down,
13083        "space" => Key::Space,
13084        "enter" => Key::Enter,
13085        "escape" => Key::Escape,
13086        "pageup" => Key::PageUp,
13087        "pagedown" => Key::PageDown,
13088        "lshift" | "leftshift" => Key::LeftShift,
13089        "rshift" | "rightshift" => Key::RightShift,
13090        "lctrl" | "leftctrl" => Key::LeftCtrl,
13091        "rctrl" | "rightctrl" => Key::RightCtrl,
13092        "lalt" | "leftalt" => Key::LeftAlt,
13093        "ralt" | "rightalt" => Key::RightAlt,
13094        "tab" => Key::Tab,
13095        "backspace" => Key::Backspace,
13096        "delete" => Key::Delete,
13097        "insert" => Key::Insert,
13098        "home" => Key::Home,
13099        "end" => Key::End,
13100        "a" => Key::A,
13101        "b" => Key::B,
13102        "c" => Key::C,
13103        "d" => Key::D,
13104        "e" => Key::E,
13105        "f" => Key::F,
13106        "g" => Key::G,
13107        "h" => Key::H,
13108        "i" => Key::I,
13109        "j" => Key::J,
13110        "k" => Key::K,
13111        "l" => Key::L,
13112        "m" => Key::M,
13113        "n" => Key::N,
13114        "o" => Key::O,
13115        "p" => Key::P,
13116        "q" => Key::Q,
13117        "r" => Key::R,
13118        "s" => Key::S,
13119        "t" => Key::T,
13120        "u" => Key::U,
13121        "v" => Key::V,
13122        "w" => Key::W,
13123        "x" => Key::X,
13124        "y" => Key::Y,
13125        "z" => Key::Z,
13126        "0" => Key::Key0,
13127        "1" => Key::Key1,
13128        "2" => Key::Key2,
13129        "3" => Key::Key3,
13130        "4" => Key::Key4,
13131        "5" => Key::Key5,
13132        "6" => Key::Key6,
13133        "7" => Key::Key7,
13134        "8" => Key::Key8,
13135        "9" => Key::Key9,
13136        _ => return None,
13137    })
13138}
13139
13140pub(crate) fn values_equal(a: &Value, b: &Value) -> bool {
13141    match (a, b) {
13142        (Value::Number(x), Value::Number(y)) => (x - y).abs() < 1e-12,
13143        (Value::Str(x), Value::Str(y)) => x == y,
13144        (Value::Bool(x), Value::Bool(y)) => x == y,
13145        (Value::Unit, Value::Unit) => true,
13146        _ => false,
13147    }
13148}
13149
13150// Rasteriser functions live in crate::gfx::raster — imported at top of file.
13151
13152// ── Window platform helpers ────────────────────────────────────────────────────
13153
13154/// Strip *all* window chrome from `hwnd` and make it cover the whole primary
13155/// monitor (0,0 → screen_w × screen_h), above the taskbar. This turns the
13156/// minifb window into a true borderless-fullscreen surface: no title bar, no
13157/// frame, no resize grips — there is no visible window "handle" left.
13158#[cfg(all(not(target_arch = "wasm32"), windows))]
13159fn make_borderless_fullscreen(hwnd: isize, screen_w: i32, screen_h: i32) {
13160    if hwnd == 0 {
13161        return;
13162    }
13163    unsafe {
13164        extern "system" {
13165            fn SetWindowLongPtrW(hwnd: isize, index: i32, new: isize) -> isize;
13166            fn SetWindowPos(
13167                hwnd: isize,
13168                insert_after: isize,
13169                x: i32,
13170                y: i32,
13171                cx: i32,
13172                cy: i32,
13173                flags: u32,
13174            ) -> i32;
13175            fn ShowWindow(hwnd: isize, cmd: i32) -> i32;
13176        }
13177        const GWL_STYLE: i32 = -16;
13178        const GWL_EXSTYLE: i32 = -20;
13179        // WS_POPUP (0x80000000) | WS_VISIBLE (0x10000000) — a bare top-level
13180        // window with no caption, border, or system menu.
13181        SetWindowLongPtrW(hwnd, GWL_STYLE, 0x9000_0000isize);
13182        // Clear extended edges (WS_EX_WINDOWEDGE / CLIENTEDGE / DLGMODALFRAME).
13183        SetWindowLongPtrW(hwnd, GWL_EXSTYLE, 0);
13184        // HWND_TOPMOST = -1; SWP_FRAMECHANGED (0x0020) | SWP_SHOWWINDOW (0x0040).
13185        SetWindowPos(hwnd, -1isize, 0, 0, screen_w, screen_h, 0x0020 | 0x0040);
13186        ShowWindow(hwnd, 3); // SW_MAXIMIZE-equivalent paint; 3 = SW_SHOWMAXIMIZED
13187    }
13188}
13189
13190/// Force real OS keyboard focus onto `hwnd`, not just Z-order prominence.
13191/// Windows' foreground-lock can leave a freshly-created window topmost — so
13192/// VISUALLY it covers everything — without actually handing it keyboard
13193/// focus, e.g. when launched from a terminal that still holds real focus:
13194/// clicks can nudge focus over (a more "user-driven" event) but typed keys
13195/// silently keep going to whatever app really has it, which looks exactly
13196/// like "clicking a text field doesn't focus it". AttachThreadInput is the
13197/// standard documented workaround — it lets SetForegroundWindow succeed even
13198/// under the lock by sharing input state with whichever thread currently
13199/// owns the foreground window. Call this LAST, after every other
13200/// window-visibility change for this launch (anything that shows/hides a
13201/// window afterward — e.g. hiding the launching console — can itself
13202/// reassign the foreground window and undo an earlier focus claim).
13203#[cfg(all(not(target_arch = "wasm32"), windows))]
13204fn force_window_focus(hwnd: isize) {
13205    if hwnd == 0 {
13206        return;
13207    }
13208    unsafe {
13209        extern "system" {
13210            fn GetForegroundWindow() -> isize;
13211            fn GetWindowThreadProcessId(hwnd: isize, pid: *mut u32) -> u32;
13212            fn GetCurrentThreadId() -> u32;
13213            fn AttachThreadInput(id_attach: u32, id_attach_to: u32, attach: i32) -> i32;
13214            fn SetForegroundWindow(hwnd: isize) -> i32;
13215            fn BringWindowToTop(hwnd: isize) -> i32;
13216            fn SetFocus(hwnd: isize) -> isize;
13217            fn SetActiveWindow(hwnd: isize) -> isize;
13218        }
13219        let fg = GetForegroundWindow();
13220        if fg != 0 && fg != hwnd {
13221            let mut fg_pid: u32 = 0;
13222            let fg_tid = GetWindowThreadProcessId(fg, &mut fg_pid);
13223            let my_tid = GetCurrentThreadId();
13224            if fg_tid != 0 && fg_tid != my_tid {
13225                AttachThreadInput(my_tid, fg_tid, 1);
13226                SetForegroundWindow(hwnd);
13227                BringWindowToTop(hwnd);
13228                SetFocus(hwnd);
13229                SetActiveWindow(hwnd);
13230                AttachThreadInput(my_tid, fg_tid, 0);
13231                return;
13232            }
13233        }
13234        SetForegroundWindow(hwnd);
13235        BringWindowToTop(hwnd);
13236        SetFocus(hwnd);
13237        SetActiveWindow(hwnd);
13238    }
13239}
13240
13241/// Toggle `hwnd`'s HWND_TOPMOST z-order style without moving/resizing/
13242/// activating it — used to drop the borderless-fullscreen window's topmost
13243/// flag on alt-tab (so it stops covering whatever the user switched to) and
13244/// restore it when the user switches back.
13245#[cfg(all(not(target_arch = "wasm32"), windows))]
13246fn set_window_topmost(hwnd: isize, topmost: bool) {
13247    if hwnd == 0 {
13248        return;
13249    }
13250    unsafe {
13251        extern "system" {
13252            fn SetWindowPos(
13253                hwnd: isize,
13254                insert_after: isize,
13255                x: i32,
13256                y: i32,
13257                cx: i32,
13258                cy: i32,
13259                flags: u32,
13260            ) -> i32;
13261        }
13262        let insert_after: isize = if topmost { -1 } else { -2 }; // HWND_TOPMOST / HWND_NOTOPMOST
13263        // SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE — pure z-order change,
13264        // must not steal focus back when restoring topmost on refocus.
13265        SetWindowPos(hwnd, insert_after, 0, 0, 0, 0, 0x0002 | 0x0001 | 0x0010);
13266    }
13267}
13268
13269/// Pace `win` to `vsync`'s target rate. `LING_FPS_CAP` (0 = uncapped, else an
13270/// explicit fps) always overrides; otherwise vsync-on paces to the monitor's
13271/// real refresh rate and vsync-off runs uncapped. minifb has no swap-interval
13272/// vsync (it owns no GPU present queue), so this is frame-rate pacing to the
13273/// refresh rate, not a tear-free guarantee.
13274#[cfg(not(target_arch = "wasm32"))]
13275fn apply_frame_pacing(win: &mut minifb::Window, vsync: bool) {
13276    match std::env::var("LING_FPS_CAP")
13277        .ok()
13278        .and_then(|v| v.parse::<usize>().ok())
13279    {
13280        Some(0) => win.set_target_fps(100_000),
13281        Some(cap) => win.set_target_fps(cap),
13282        None if vsync => win.set_target_fps(monitor_info().2.max(30) as usize),
13283        None => win.set_target_fps(100_000),
13284    }
13285}
13286
13287/// Primary-monitor resolution and refresh rate as `(width, height, hz)`.
13288/// `hz` falls back to 60 when the driver reports an unknown/`default` rate.
13289#[cfg(all(not(target_arch = "wasm32"), windows))]
13290fn monitor_info() -> (i32, i32, i32) {
13291    unsafe {
13292        extern "system" {
13293            fn GetSystemMetrics(index: i32) -> i32;
13294            fn GetDC(hwnd: isize) -> isize;
13295            fn ReleaseDC(hwnd: isize, hdc: isize) -> i32;
13296            fn GetDeviceCaps(hdc: isize, index: i32) -> i32;
13297        }
13298        let w = GetSystemMetrics(0).max(1); // SM_CXSCREEN
13299        let h = GetSystemMetrics(1).max(1); // SM_CYSCREEN
13300        let hdc = GetDC(0);
13301        let mut hz = if hdc != 0 { GetDeviceCaps(hdc, 116) } else { 0 }; // VREFRESH
13302        if hdc != 0 {
13303            ReleaseDC(0, hdc);
13304        }
13305        if hz <= 1 {
13306            hz = 60; // 0 or 1 means "device default" → assume 60 Hz
13307        }
13308        (w, h, hz)
13309    }
13310}
13311
13312/// Non-Windows native fallback: resolution from [`native_screen_size`]; refresh
13313/// from the active X11/RandR mode (so a 144 Hz panel drives the loop at 144),
13314/// falling back to 60 Hz when it can't be detected (Wayland, headless, macOS).
13315#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
13316fn monitor_info() -> (i32, i32, i32) {
13317    let (w, h) = native_screen_size();
13318    (w as i32, h as i32, linux_refresh_hz().unwrap_or(60))
13319}
13320
13321/// Active display refresh rate via `xrandr`. Each connected output's active
13322/// mode is the token flagged with `*` (e.g. `1920x1080 144.00*+`); we take the
13323/// max across all active outputs so a multi-monitor rig drives the loop at
13324/// its fastest panel.
13325#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
13326fn linux_refresh_hz() -> Option<i32> {
13327    let out = std::process::Command::new("xrandr")
13328        .arg("--current")
13329        .output()
13330        .ok()?;
13331    if !out.status.success() {
13332        return None;
13333    }
13334    parse_xrandr_max_hz(&String::from_utf8_lossy(&out.stdout))
13335}
13336
13337/// Pure parse used by [`linux_refresh_hz`]: the highest `*`-flagged refresh
13338/// rate across all active outputs in `xrandr --current` output.
13339#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
13340fn parse_xrandr_max_hz(text: &str) -> Option<i32> {
13341    text.split_whitespace()
13342        .filter(|tok| tok.contains('*'))
13343        .filter_map(|tok| {
13344            tok.trim_matches(|c: char| !c.is_ascii_digit() && c != '.')
13345                .parse::<f64>()
13346                .ok()
13347        })
13348        .map(|hz| hz.round() as i32)
13349        .filter(|&hz| (24..=1000).contains(&hz))
13350        .max()
13351}
13352
13353/// WASM fallback: the canvas is the display surface; assume 60 Hz.
13354#[cfg(target_arch = "wasm32")]
13355fn monitor_info() -> (i32, i32, i32) {
13356    let (w, h) = crate::gfx::webgl::canvas_size();
13357    (w as i32, h as i32, 60)
13358}
13359
13360#[cfg(all(test, not(target_arch = "wasm32"), not(windows)))]
13361mod xrandr_tests {
13362    use super::parse_xrandr_max_hz;
13363
13364    #[test]
13365    fn picks_highest_active_output() {
13366        let text = "\
13367eDP-1 connected primary 1920x1080+0+0
13368   1920x1080     60.00*+  59.94
13369DP-1 connected 2560x1440+1920+0
13370   2560x1440    144.00*+  120.00  60.00
13371";
13372        assert_eq!(parse_xrandr_max_hz(text), Some(144));
13373    }
13374
13375    #[test]
13376    fn single_output() {
13377        let text = "   1920x1080     75.00*+  60.00\n";
13378        assert_eq!(parse_xrandr_max_hz(text), Some(75));
13379    }
13380
13381    #[test]
13382    fn no_active_mode_returns_none() {
13383        let text = "eDP-1 disconnected\n";
13384        assert_eq!(parse_xrandr_max_hz(text), None);
13385    }
13386
13387    #[test]
13388    fn out_of_range_hz_filtered() {
13389        let text = "   1x1     5000.00*+\n";
13390        assert_eq!(parse_xrandr_max_hz(text), None);
13391    }
13392}
13393
13394/// Query the primary display resolution on non-Windows platforms.
13395/// Falls back to 1920×1080 if the size cannot be determined.
13396#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
13397fn native_screen_size() -> (f64, f64) {
13398    // On Linux/macOS we don't have an easy dependency-free call; return a
13399    // sensible default. Callers can always pass explicit dimensions.
13400    (1920.0, 1080.0)
13401}
13402
13403// ════════════════════════════════════════════════════════════════════════════
13404// Builtin call profiler  (env-gated, near-zero cost when off)
13405//
13406//   LING_PROFILE=1            enable per-builtin call-count + inclusive-time tally
13407//   LING_PROFILE_EVERY=N      print the report every N frames (default 240)
13408//
13409// Every builtin call funnels through `Interp::call_named` (JIT via `ling_builtin`,
13410// tree-walker directly), so this captures the full render/physics/audio builtin
13411// hot-path. Report is sorted by total time and prints calls, calls/frame,
13412// total_ms and ms/frame — the top-down "what's making so many calls" view.
13413// ════════════════════════════════════════════════════════════════════════════
13414struct LingProfileState {
13415    enabled: bool,
13416    every: u64,
13417    frames: u64,
13418    calls: std::collections::HashMap<String, (u64, u128)>, // name -> (count, nanos)
13419}
13420
13421thread_local! {
13422    static LING_PROFILE: std::cell::RefCell<LingProfileState> = std::cell::RefCell::new({
13423        let enabled = std::env::var("LING_PROFILE").map(|v| v != "0" && !v.is_empty()).unwrap_or(false);
13424        let every = std::env::var("LING_PROFILE_EVERY").ok()
13425            .and_then(|v| v.parse::<u64>().ok()).filter(|&n| n > 0).unwrap_or(240);
13426        if enabled {
13427            eprintln!("[ling-profile] ON — report every {every} frames (set LING_PROFILE_EVERY to change)");
13428        }
13429        LingProfileState { enabled, every, frames: 0, calls: std::collections::HashMap::new() }
13430    });
13431}
13432
13433#[inline]
13434fn ling_profile_enabled() -> bool {
13435    LING_PROFILE.with(|p| p.borrow().enabled)
13436}
13437
13438thread_local! {
13439    static LING_FPS: std::cell::RefCell<(bool, f64, u32, f64)> = std::cell::RefCell::new(
13440        (std::env::var("LING_FPS").map(|v| v != "0" && !v.is_empty()).unwrap_or(false), 0.0, 0, 0.0)
13441    );
13442}
13443
13444#[cfg(not(target_arch = "wasm32"))]
13445fn ling_fps_tick() {
13446    LING_FPS.with(|s| {
13447        let mut s = s.borrow_mut();
13448        if !s.0 {
13449            return;
13450        }
13451        let now = crate::runtime::now_secs();
13452        if s.1 > 0.0 {
13453            s.3 += now - s.1;
13454            s.2 += 1;
13455            if s.2 >= 120 {
13456                let avg = s.3 / s.2 as f64;
13457                eprintln!(
13458                    "[fps] {:.1} fps  ({:.2} ms/frame, wall, {} frames)",
13459                    1.0 / avg,
13460                    avg * 1000.0,
13461                    s.2
13462                );
13463                s.2 = 0;
13464                s.3 = 0.0;
13465            }
13466        }
13467        s.1 = now;
13468    });
13469}
13470
13471// Coarse render-pipeline timers (set LING_PHASE=1). Each accumulates wall-time
13472// per frame at flush/present granularity, so the cost is negligible. Reports the
13473// software-rasteriser breakdown the builtin profiler can't separate (the work is
13474// all inside the `present`/`flush_3d` builtins).
13475thread_local! {
13476    static LING_PHASE: std::cell::RefCell<(bool, u64, [u128; 5])> = std::cell::RefCell::new(
13477        (std::env::var_os("LING_PHASE").is_some(), 0, [0; 5])
13478    );
13479}
13480
13481/// Phase indices for [`ling_phase_add`].
13482pub mod phase {
13483    pub const FLUSH: usize = 0;
13484    pub const TOON: usize = 1;
13485    pub const BLIT: usize = 2;
13486    pub const DISTORT: usize = 3;
13487    pub const SORT: usize = 4;
13488}
13489
13490#[cfg(not(target_arch = "wasm32"))]
13491#[inline]
13492pub fn ling_phase_add(idx: usize, nanos: u128) {
13493    LING_PHASE.with(|p| {
13494        let mut p = p.borrow_mut();
13495        if p.0 {
13496            p.2[idx] += nanos;
13497        }
13498    });
13499}
13500
13501#[cfg(not(target_arch = "wasm32"))]
13502fn ling_phase_frame() {
13503    LING_PHASE.with(|p| {
13504        let mut p = p.borrow_mut();
13505        if !p.0 {
13506            return;
13507        }
13508        p.1 += 1;
13509        if p.1 >= 120 {
13510            let f = p.1 as f64;
13511            let ms = |i: usize| p.2[i] as f64 / 1e6 / f;
13512            eprintln!(
13513                "[phase] sort={:.2} flush={:.2} toon={:.2} blit={:.2} distort={:.2} ms/frame",
13514                ms(phase::SORT),
13515                ms(phase::FLUSH),
13516                ms(phase::TOON),
13517                ms(phase::BLIT),
13518                ms(phase::DISTORT)
13519            );
13520            p.1 = 0;
13521            p.2 = [0; 5];
13522        }
13523    });
13524}
13525
13526fn ling_profile_record(name: &str, nanos: u128) {
13527    // Frame boundary = a present() call.
13528    let is_frame = matches!(
13529        name,
13530        "present" | "แสดงผล" | "gfx_present" | "show" | "显" | "呈现" | "表示" | "표시"
13531    );
13532    LING_PROFILE.with(|p| {
13533        let mut p = p.borrow_mut();
13534        let e = p.calls.entry(name.to_string()).or_insert((0, 0));
13535        e.0 += 1;
13536        e.1 += nanos;
13537        if is_frame {
13538            p.frames += 1;
13539            if p.frames % p.every == 0 {
13540                ling_profile_print(&p);
13541            }
13542        }
13543    });
13544}
13545
13546fn ling_profile_print(p: &LingProfileState) {
13547    let mut rows: Vec<(&String, u64, u128)> =
13548        p.calls.iter().map(|(n, (c, ns))| (n, *c, *ns)).collect();
13549    use std::cmp::Reverse;
13550    rows.sort_by_key(|x| Reverse(x.2)); // by total time desc
13551    let total_ns: u128 = p.calls.values().map(|(_, ns)| *ns).sum();
13552    let total_calls: u64 = p.calls.values().map(|(c, _)| *c).sum();
13553    let fr = p.frames.max(1) as f64;
13554    eprintln!(
13555        "\n┌─ LING PROFILE ── frames={} ─ builtin calls by total inclusive time ─────────────",
13556        p.frames
13557    );
13558    eprintln!(
13559        "│ {:<24} {:>9} {:>9} {:>10} {:>9} {:>6}",
13560        "builtin", "calls", "calls/fr", "total_ms", "ms/frame", "%time"
13561    );
13562    eprintln!("├──────────────────────────────────────────────────────────────────────────────");
13563    for (name, count, ns) in rows.iter().take(30) {
13564        let ms = *ns as f64 / 1e6;
13565        let pct = if total_ns > 0 {
13566            *ns as f64 / total_ns as f64 * 100.0
13567        } else {
13568            0.0
13569        };
13570        eprintln!(
13571            "│ {:<24} {:>9} {:>9.1} {:>10.1} {:>9.3} {:>5.1}%",
13572            truncate_name(name),
13573            count,
13574            *count as f64 / fr,
13575            ms,
13576            ms / fr,
13577            pct
13578        );
13579    }
13580    eprintln!("├──────────────────────────────────────────────────────────────────────────────");
13581    eprintln!(
13582        "│ TOTAL {} builtin calls, {:.1} ms over {} frames  →  {:.0} calls/frame, {:.2} ms/frame in builtins",
13583        total_calls,
13584        total_ns as f64 / 1e6,
13585        p.frames,
13586        total_calls as f64 / fr,
13587        total_ns as f64 / 1e6 / fr
13588    );
13589    eprintln!("└──────────────────────────────────────────────────────────────────────────────");
13590}
13591
13592/// Trim a builtin name to fit the report column (counts chars, good enough for
13593/// the mixed-script names).
13594fn truncate_name(s: &str) -> String {
13595    let max = 24;
13596    if s.chars().count() <= max {
13597        s.to_string()
13598    } else {
13599        let mut t: String = s.chars().take(max - 1).collect();
13600        t.push('…');
13601        t
13602    }
13603}