Skip to main content

ling/runtime/
mod.rs

1// src/runtime/mod.rs — tree-walking interpreter with graphics support
2#[cfg(not(target_arch = "wasm32"))]
3mod ai;
4#[cfg(not(target_arch = "wasm32"))]
5mod gamepad;
6#[cfg(target_arch = "wasm32")]
7mod input_web;
8#[cfg(not(target_arch = "wasm32"))]
9pub(crate) mod jit_abi;
10
11/// Initialize the AOT/JIT runtime. Must be called before any AOT-compiled code.
12/// Creates a new interpreter instance for runtime function dispatch.
13#[cfg(not(target_arch = "wasm32"))]
14pub fn init_aot_runtime() {
15    let interp = Interpreter::new();
16    jit_abi::init(interp);
17}
18
19/// Returns seconds since Unix epoch. On wasm32 uses `js_sys::Date::now()`
20/// (milliseconds / 1000); on native uses `SystemTime`.
21pub fn now_secs() -> f64 {
22    #[cfg(target_arch = "wasm32")]
23    {
24        js_sys::Date::now() / 1000.0
25    }
26    #[cfg(not(target_arch = "wasm32"))]
27    {
28        std::time::SystemTime::now()
29            .duration_since(std::time::UNIX_EPOCH)
30            .map(|d| d.as_secs_f64())
31            .unwrap_or(0.0)
32    }
33}
34
35/// Global hue-cycle for wireframe line strokes (enabled via `set_line_hue_cycle`).
36/// Stored as f64 bits: the cycle rate (radians/sec) and the epoch-seconds baseline
37/// captured when it was last (re)enabled. A rate of 0 disables the effect.
38static LINE_HUE_RATE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
39static LINE_HUE_START: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
40
41/// Enable/disable the wireframe line hue-cycle. `rate` is in radians/sec (0 = off).
42pub fn set_line_hue_rate(rate: f64) {
43    LINE_HUE_RATE.store(rate.to_bits(), std::sync::atomic::Ordering::Relaxed);
44    LINE_HUE_START.store(now_secs().to_bits(), std::sync::atomic::Ordering::Relaxed);
45}
46
47/// Current hue phase (radians) for line strokes, or `None` when the cycle is off.
48/// Elapsed is computed in f64 (epoch seconds are huge) before the caller casts to
49/// f32, so `sin` keeps precision across a long session.
50pub fn line_hue_phase() -> Option<f64> {
51    let rate = f64::from_bits(LINE_HUE_RATE.load(std::sync::atomic::Ordering::Relaxed));
52    if rate <= 0.0 {
53        return None;
54    }
55    let start = f64::from_bits(LINE_HUE_START.load(std::sync::atomic::Ordering::Relaxed));
56    Some((now_secs() - start) * rate)
57}
58
59// Wasm-only module registry: seeded by JS before `run_program` is called so
60// that `use "path"` statements resolve without a real filesystem.
61#[cfg(target_arch = "wasm32")]
62thread_local! {
63    static WASM_MODULES: std::cell::RefCell<std::collections::HashMap<String, String>> =
64        std::cell::RefCell::new(std::collections::HashMap::new());
65}
66
67/// Register a module source for wasm32 `use` resolution.
68/// Called from JS via `wasm_bindgen` before `run_program`.
69#[cfg(target_arch = "wasm32")]
70pub fn register_wasm_module(path: &str, source: &str) {
71    WASM_MODULES.with(|m| m.borrow_mut().insert(path.to_string(), source.to_string()));
72}
73
74/// Look up a registered module source on wasm32.
75#[cfg(target_arch = "wasm32")]
76pub(crate) fn get_wasm_module(path: &str) -> Option<String> {
77    WASM_MODULES.with(|m| m.borrow().get(path).cloned())
78}
79
80#[cfg(target_arch = "wasm32")]
81#[inline]
82fn wasm_sleep_ms(ms: i32) {
83    if ms <= 0 {
84        return;
85    }
86
87    // Keep this in Rust/js_sys so wasm-bindgen doesn't emit `require(...)`
88    // snippets, which break worker/no-modules output.
89    let global = js_sys::global();
90    let has_sab = js_sys::Reflect::has(
91        &global,
92        &wasm_bindgen::JsValue::from_str("SharedArrayBuffer"),
93    )
94    .unwrap_or(false);
95    let has_atomics =
96        js_sys::Reflect::has(&global, &wasm_bindgen::JsValue::from_str("Atomics")).unwrap_or(false);
97    if has_sab && has_atomics {
98        let sab = js_sys::SharedArrayBuffer::new(4);
99        let i32a = js_sys::Int32Array::new(&sab);
100        if js_sys::Atomics::wait_with_timeout(&i32a, 0, 0, ms as f64).is_ok() {
101            return;
102        }
103    }
104
105    let end = js_sys::Date::now() + ms as f64;
106    while js_sys::Date::now() < end {}
107}
108
109#[cfg(target_arch = "wasm32")]
110fn wasm_fetch_sync(
111    path: &str,
112    response_type: &str,
113    return_expr: &str,
114) -> Result<wasm_bindgen::JsValue, String> {
115    let quoted = js_sys::JSON::stringify(&wasm_bindgen::JsValue::from_str(path))
116        .ok()
117        .and_then(|s| s.as_string())
118        .unwrap_or_else(|| "\"\"".to_string());
119
120    let script = format!(
121        "(function(){{\n  var xhr = new XMLHttpRequest();\n  xhr.open('GET', {quoted}, false);\n  xhr.responseType = '{response_type}';\n  xhr.send(null);\n  if ((xhr.status|0) !== 200 && (xhr.status|0) !== 0) {{ throw new Error('HTTP ' + xhr.status + ' for ' + {quoted}); }}\n  return {return_expr};\n}})()"
122    );
123
124    js_sys::eval(&script).map_err(|e| {
125        e.as_string()
126            .unwrap_or_else(|| format!("JS eval failed: {:?}", e))
127    })
128}
129
130#[cfg(target_arch = "wasm32")]
131fn wasm_fetch_bytes(path: &str) -> Result<Vec<u8>, String> {
132    let value = wasm_fetch_sync(
133        path,
134        "arraybuffer",
135        "new Uint8Array(xhr.response || new ArrayBuffer(0))",
136    )?;
137    let arr = js_sys::Uint8Array::new(&value);
138    let mut out = vec![0u8; arr.length() as usize];
139    arr.copy_to(&mut out);
140    Ok(out)
141}
142
143#[cfg(target_arch = "wasm32")]
144fn wasm_fetch_text(path: &str) -> Result<String, String> {
145    let value = wasm_fetch_sync(path, "text", "String(xhr.responseText || '')")?;
146    Ok(value.as_string().unwrap_or_default())
147}
148
149#[cfg(not(target_arch = "wasm32"))]
150mod net;
151#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
152pub mod web;
153use crate::gfx::{GfxState, Light};
154use crate::parser::ast::*;
155#[cfg(target_arch = "wasm32")]
156use js_sys;
157use std::cell::RefCell;
158use std::collections::HashMap;
159use std::rc::Rc;
160// `raster` is wasm-safe (pure CPU framebuffer), so `draw_line` is available on
161// web too; `fill_triangle` is only reached from native-gated 3-D fill paths.
162use crate::gfx::raster::draw_line;
163#[cfg(not(target_arch = "wasm32"))]
164use crate::gfx::raster::fill_triangle;
165#[cfg(not(target_arch = "wasm32"))]
166use ling_audio::{AudioEngine, ToneParams, Wave};
167
168#[cfg(not(target_arch = "wasm32"))]
169use ling_audio::FftAnalyzer;
170
171#[cfg(not(target_arch = "wasm32"))]
172use ling_mic;
173
174// ─── Values ──────────────────────────────────────────────────────────────────
175
176#[derive(Debug, Clone)]
177pub enum Value {
178    Str(String),
179    Number(f64),
180    Bool(bool),
181    Unit,
182    List(Rc<Vec<Value>>),
183    Ok(Box<Value>),
184    Err(Box<Value>),
185    Fn(Vec<String>, Vec<Stmt>, Env),
186    /// `form` record instance — ordered named fields.
187    Struct {
188        name: String,
189        fields: Vec<(String, Value)>,
190    },
191    /// `choose` enum instance — variant tag plus ordered payload.
192    Variant {
193        enum_name: String,
194        variant: String,
195        payload: Vec<Value>,
196    },
197}
198
199// Interpreter-hot maps use a fast non-crypto hasher: short Thai identifier keys
200// are hashed on every variable access and builtin dispatch, where SipHash dominates.
201use rustc_hash::FxHashMap;
202type Env = FxHashMap<String, Value>;
203
204#[inline]
205fn new_env() -> Env {
206    FxHashMap::default()
207}
208
209impl std::fmt::Display for Value {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        match self {
212            Value::Str(s) => write!(f, "{s}"),
213            Value::Number(n) => {
214                if n.fract() == 0.0 && n.abs() < 1e15 {
215                    write!(f, "{}", *n as i64)
216                } else {
217                    write!(f, "{n}")
218                }
219            },
220            Value::Bool(b) => write!(f, "{b}"),
221            Value::Unit => write!(f, "()"),
222            Value::List(v) => {
223                write!(f, "[")?;
224                for (i, x) in v.iter().enumerate() {
225                    if i > 0 {
226                        write!(f, ", ")?;
227                    }
228                    write!(f, "{x}")?;
229                }
230                write!(f, "]")
231            },
232            Value::Ok(v) => write!(f, "Ok({v})"),
233            Value::Err(v) => write!(f, "Err({v})"),
234            Value::Fn(_, _, _) => write!(f, "<fn>"),
235            Value::Struct { name, fields } => {
236                write!(f, "{name} {{ ")?;
237                for (i, (k, v)) in fields.iter().enumerate() {
238                    if i > 0 {
239                        write!(f, ", ")?;
240                    }
241                    write!(f, "{k}: {v}")?;
242                }
243                write!(f, " }}")
244            },
245            Value::Variant { variant, payload, .. } => {
246                write!(f, "{variant}")?;
247                if !payload.is_empty() {
248                    write!(f, "(")?;
249                    for (i, v) in payload.iter().enumerate() {
250                        if i > 0 {
251                            write!(f, ", ")?;
252                        }
253                        write!(f, "{v}")?;
254                    }
255                    write!(f, ")")?;
256                }
257                Ok(())
258            },
259        }
260    }
261}
262
263#[cfg(target_arch = "wasm32")]
264fn wasm_unsupported_builtin(name: &str) -> Option<Value> {
265    Some(match name {
266        // interface blips (native-only today)
267        "audio_blip"
268        | "提示音"
269        | "ビープ音"
270        | "효과음"
271        | "เสียงบี๊บ"
272        | "ui_sound"
273        | "界面音"
274        | "UI音"
275        | "인터페이스음"
276        | "เสียงปุ่ม"
277        | "audio_stop_sfx"
278        | "停止音效"
279        | "効果音停止"
280        | "효과음정지"
281        | "หยุดเอฟเฟกต์ทั้งหมด" => Value::Unit,
282
283        // music loading / analysis / playback / midi (native-only today)
284        "music_load"
285        | "载入音乐"
286        | "音楽読込"
287        | "음악로드"
288        | "โหลดเพลง"
289        | "music_patch"
290        | "乐器音色"
291        | "音色読込"
292        | "악기패치"
293        | "แพตช์เครื่องดนตรี"
294        | "music_lrc"
295        | "载入歌词"
296        | "歌詞読込"
297        | "가사로드"
298        | "โหลดเนื้อเพลง"
299        | "music_midi_load"
300        | "载入MIDI"
301        | "MIDI読込"
302        | "미디로드"
303        | "โหลดมิดี" => Value::Number(-1.0),
304
305        "music_duration"
306        | "音乐时长"
307        | "音楽長さ"
308        | "음악길이"
309        | "ความยาวเพลง"
310        | "music_bpm"
311        | "节拍速度"
312        | "テンポ"
313        | "템포"
314        | "จังหวะต่อนาที"
315        | "music_pos"
316        | "音乐位置"
317        | "音楽位置"
318        | "음악위치"
319        | "ตำแหน่งเพลง"
320        | "music_mic_pitch"
321        | "麦克风音高"
322        | "マイク音程"
323        | "마이크음정"
324        | "ระดับเสียงไมค์"
325        | "music_hz"
326        | "音符频率"
327        | "音符周波数"
328        | "음표주파수"
329        | "ความถี่โน้ต"
330        | "music_pitch_score"
331        | "音准评分"
332        | "音程スコア"
333        | "음정점수"
334        | "คะแนนเสียง"
335        | "music_judge"
336        | "判定"
337        | "判定する"
338        | "판정"
339        | "ตัดสินจังหวะ"
340        | "music_midi_count"
341        | "MIDI数量"
342        | "MIDI数"
343        | "미디수"
344        | "จำนวนมิดี" => Value::Number(0.0),
345
346        "music_key"
347        | "调性"
348        | "調性"
349        | "조성"
350        | "คีย์เพลง"
351        | "music_lyric"
352        | "当前歌词"
353        | "現在歌詞"
354        | "현재가사"
355        | "เนื้อเพลงปัจจุบัน"
356        | "music_note_name"
357        | "音名"
358        | "音名称"
359        | "음이름"
360        | "ชื่อโน้ต"
361        | "music_grade_name"
362        | "判定名"
363        | "判定名称"
364        | "판정이름"
365        | "ชื่อการตัดสิน" => Value::Str(String::new()),
366
367        "music_onsets"
368        | "音符起点"
369        | "オンセット"
370        | "온셋"
371        | "จุดเริ่มเสียง"
372        | "music_beat_grid"
373        | "节拍网格"
374        | "ビートグリッド"
375        | "비트그리드"
376        | "กริดจังหวะ"
377        | "music_midi_notes"
378        | "MIDI音符"
379        | "MIDIノート"
380        | "미디음표"
381        | "โน้ตมิดี"
382        | "music_midi_bars"
383        | "MIDI音条"
384        | "MIDIバー"
385        | "미디바"
386        | "แท่งมิดี"
387        | "music_fft"
388        | "音乐频谱"
389        | "音楽スペクトル"
390        | "음악스펙트럼"
391        | "สเปกตรัมเพลง" => Value::List(Vec::new().into()),
392
393        "music_play"
394        | "播放音乐"
395        | "音楽再生"
396        | "음악재생"
397        | "เล่นเพลง"
398        | "music_pause"
399        | "暂停音乐"
400        | "音楽一時停止"
401        | "음악일시정지"
402        | "หยุดเพลงชั่วคราว"
403        | "music_stop"
404        | "停止音乐"
405        | "音楽停止"
406        | "음악정지"
407        | "หยุดเพลง"
408        | "music_seek"
409        | "定位音乐"
410        | "音楽シーク"
411        | "음악탐색"
412        | "ค้นหาเพลง"
413        | "music_volume"
414        | "音乐音量"
415        | "音楽音量"
416        | "음악음량"
417        | "ระดับเพลง"
418        | "music_note"
419        | "弹音符"
420        | "音符演奏"
421        | "음표연주"
422        | "เล่นโน้ต"
423        | "music_note_on"
424        | "音符开始"
425        | "音符オン"
426        | "음표켜기"
427        | "โน้ตเริ่ม"
428        | "music_note_off"
429        | "音符结束"
430        | "音符オフ"
431        | "음표끄기"
432        | "โน้ตจบ" => Value::Unit,
433
434        // liquid sim — return handle 0 for new, Unit for everything else
435        "liquid_new" | "新建液体" | "液体新規" | "액체생성" | "สร้างของเหลว" => {
436            Value::Number(0.0)
437        },
438        "liquid_mix" | "液体混合" | "液体混合度" | "액체혼합" | "การผสมของเหลว" => {
439            Value::Number(0.0)
440        },
441        "liquid_set_colors"
442        | "液体颜色"
443        | "液体配色"
444        | "액체색상"
445        | "สีของเหลว"
446        | "liquid_splat"
447        | "液体注入"
448        | "液体追加"
449        | "액체분사"
450        | "หยดของเหลว"
451        | "liquid_gravity"
452        | "液体重力"
453        | "液体重力ベクトル"
454        | "액체중력"
455        | "แรงโน้มถ่วงเหลว"
456        | "liquid_step"
457        | "液体步进"
458        | "液体更新"
459        | "액체스텝"
460        | "ก้าวของเหลว"
461        | "liquid_step_all"
462        | "液体全步进"
463        | "液体全更新"
464        | "전체액체스텝"
465        | "ก้าวของเหลวทั้งหมด"
466        | "liquid_rainbow"
467        | "液体彩虹"
468        | "液体虹"
469        | "액체무지개"
470        | "ของเหลวสายรุ้ง"
471        | "liquid_draw"
472        | "绘制液体"
473        | "液体描画"
474        | "액체그리기"
475        | "วาดของเหลว"
476        | "liquid_draw_surface"
477        | "液体贴面"
478        | "液体曲面"
479        | "액체곡면"
480        | "ของเหลวบนพื้นผิว" => Value::Unit,
481
482        // ── game AI: neural networks ─────────────────────────────────────────
483        "nn_new"
484        | "建神经网"
485        | "ニューラル作成"
486        | "신경망생성"
487        | "สร้างโครงข่าย"
488        | "nn_load"
489        | "载入网"
490        | "網読込"
491        | "신경망불러오기"
492        | "โหลดโครงข่าย" => Value::Number(-1.0),
493        "nn_forward" | "神经前向" | "順伝播" | "순전파" | "ส่งต่อโครงข่าย" => {
494            Value::List(Vec::new().into())
495        },
496        "nn_train"
497        | "训练网"
498        | "ニューラル学習"
499        | "신경망학습"
500        | "ฝึกโครงข่าย"
501        | "nn_dense"
502        | "密集层"
503        | "密層追加"
504        | "밀집층"
505        | "ชั้นหนาแน่น" => Value::Number(0.0),
506        "nn_save" | "保存网" | "網保存" | "신경망저장" | "บันทึกโครงข่าย" => {
507            Value::Bool(false)
508        },
509
510        // ── game AI: behavior trees ─────────────────────────────────────────
511        "bt_build" | "建行为树" | "行動木構築" | "행동트리구성" | "สร้างต้นไม้พฤติกรรม" => {
512            Value::Number(-1.0)
513        },
514        "bt_tick" | "行为树滴答" | "行動木更新" | "행동트리틱" | "เดินต้นไม้พฤติกรรม" => {
515            Value::Str(String::new())
516        },
517        "bt_status" | "行为树状态" | "行動木状態" | "행동트리상태" | "สถานะต้นไม้พฤติกรรม" => {
518            Value::Number(0.0)
519        },
520        "bt_set" | "设事实" | "事実設定" | "사실설정" | "ตั้งข้อเท็จจริง" => {
521            Value::Unit
522        },
523
524        // ── game AI: dialog LLM ─────────────────────────────────────────────
525        "dialog_new"
526        | "建对话模型"
527        | "対話モデル作成"
528        | "대화모델생성"
529        | "สร้างโมเดลสนทนา"
530        | "dialog_load_model"
531        | "对话载模"
532        | "対話モデル読込"
533        | "대화모델불러오기"
534        | "โหลดโมเดลสนทนา"
535        | "dialog_train"
536        | "对话训练"
537        | "対話訓練"
538        | "대화훈련"
539        | "ฝึกสนทนา"
540        | "dialog_load"
541        | "对话载入"
542        | "対話読込"
543        | "대화불러오기"
544        | "โหลดชุดสนทนา" => Value::Number(-1.0),
545        "dialog_say" | "对话生成" | "対話生成" | "대화생성" | "พูดสนทนา" => {
546            Value::Str(String::new())
547        },
548        "dialog_save" | "对话存模" | "対話モデル保存" | "대화모델저장" | "บันทึกโมเดลสนทนา" => {
549            Value::Bool(false)
550        },
551        "dialog_learn" | "对话学习" | "対話学習" | "대화학습" | "เรียนรู้สนทนา" => {
552            Value::Unit
553        },
554
555        // ── networking ──────────────────────────────────────────────────────
556        "net_connect"
557        | "联网"
558        | "ネット接続"
559        | "네트연결"
560        | "เชื่อมเน็ต"
561        | "net_listen"
562        | "监听"
563        | "待機"
564        | "리슨"
565        | "รอรับ"
566        | "net_send"
567        | "发送"
568        | "送信"
569        | "전송"
570        | "ส่ง" => Value::Number(-1.0),
571        "net_recv"
572        | "接收"
573        | "受信"
574        | "수신"
575        | "รับ"
576        | "net_status"
577        | "连接状态"
578        | "接続状態"
579        | "연결상태"
580        | "สถานะการเชื่อม" => Value::Str(String::new()),
581        "net_discover" | "发现" | "探索" | "검색" | "ค้นหาเครือข่าย" => {
582            Value::List(Vec::new().into())
583        },
584        "net_close"
585        | "断开"
586        | "切断"
587        | "연결종료"
588        | "ตัดเชื่อม"
589        | "net_test"
590        | "测连接"
591        | "接続テスト"
592        | "연결테스트"
593        | "ทดสอบเน็ต" => Value::Number(0.0),
594
595        // catch-all: any other native-only builtin silently no-ops on wasm32
596        _ => Value::Unit,
597    })
598}
599
600// ─── Control flow ────────────────────────────────────────────────────────────
601
602#[derive(Debug)]
603pub(crate) enum EvalErr {
604    Runtime(String),
605    Return(Value),
606    #[allow(dead_code)] // reserved for future `break` statement support
607    Break,
608}
609
610impl From<String> for EvalErr {
611    fn from(s: String) -> Self {
612        EvalErr::Runtime(s)
613    }
614}
615
616type EvalResult = Result<Value, EvalErr>;
617
618/// RFC 4648 base32 encode (no padding) — used for TOTP secrets.
619#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
620fn base32_encode(data: &[u8]) -> String {
621    const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
622    let mut out = String::new();
623    let mut buffer: u32 = 0;
624    let mut bits = 0u32;
625    for &b in data {
626        buffer = (buffer << 8) | b as u32;
627        bits += 8;
628        while bits >= 5 {
629            bits -= 5;
630            out.push(ALPHABET[((buffer >> bits) & 0x1f) as usize] as char);
631        }
632    }
633    if bits > 0 {
634        out.push(ALPHABET[((buffer << (5 - bits)) & 0x1f) as usize] as char);
635    }
636    out
637}
638
639/// RFC 4648 base32 decode (case-insensitive, ignores padding/whitespace).
640#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
641fn base32_decode(s: &str) -> Option<Vec<u8>> {
642    let mut buffer: u32 = 0;
643    let mut bits = 0u32;
644    let mut out = Vec::new();
645    for c in s.chars() {
646        if c == '=' || c.is_whitespace() {
647            continue;
648        }
649        let v = match c.to_ascii_uppercase() {
650            'A'..='Z' => c.to_ascii_uppercase() as u32 - 'A' as u32,
651            '2'..='7' => c as u32 - '2' as u32 + 26,
652            _ => return None,
653        };
654        buffer = (buffer << 5) | v;
655        bits += 5;
656        if bits >= 8 {
657            bits -= 8;
658            out.push((buffer >> bits) as u8);
659        }
660    }
661    Some(out)
662}
663
664/// One RFC 6238 TOTP code (HMAC-SHA1, 6 digits) for the given 30s time step.
665#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
666fn totp_code(secret_b32: &str, step: u64) -> Option<String> {
667    let key = base32_decode(secret_b32)?;
668    let msg = step.to_be_bytes();
669    let mac = hmac_sha1(&key, &msg);
670    let offset = (mac[19] & 0x0f) as usize;
671    let bin = ((mac[offset] as u32 & 0x7f) << 24)
672        | ((mac[offset + 1] as u32) << 16)
673        | ((mac[offset + 2] as u32) << 8)
674        | (mac[offset + 3] as u32);
675    Some(format!("{:06}", bin % 1_000_000))
676}
677
678/// TOTP verify with a ±1 step window (tolerates minor clock skew).
679#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
680fn totp_check(secret_b32: &str, code: &str) -> bool {
681    if code.len() != 6 || !code.bytes().all(|b| b.is_ascii_digit()) {
682        return false;
683    }
684    let now_step = (crate::runtime::now_secs() as u64) / 30;
685    for delta in [-1i64, 0, 1] {
686        let step = (now_step as i64 + delta) as u64;
687        if let Some(expected) = totp_code(secret_b32, step) {
688            // constant-time-ish compare (fixed 6-char length)
689            if expected.as_bytes().ct_eq_str(code.as_bytes()) {
690                return true;
691            }
692        }
693    }
694    false
695}
696
697/// HMAC-SHA1 built on the `hmac`+`sha1` crates (both via the web feature).
698#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
699fn hmac_sha1(key: &[u8], msg: &[u8]) -> [u8; 20] {
700    use hmac::{Mac, SimpleHmac};
701    let mut mac = SimpleHmac::<sha1::Sha1>::new_from_slice(key).expect("hmac key");
702    mac.update(msg);
703    let out = mac.finalize().into_bytes();
704    let mut arr = [0u8; 20];
705    arr.copy_from_slice(&out);
706    arr
707}
708
709/// Tiny fixed-length constant-time byte compare helper for TOTP codes.
710#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
711trait CtEqStr {
712    fn ct_eq_str(&self, other: &[u8]) -> bool;
713}
714#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
715impl CtEqStr for [u8] {
716    fn ct_eq_str(&self, other: &[u8]) -> bool {
717        if self.len() != other.len() {
718            return false;
719        }
720        let mut diff = 0u8;
721        for (a, b) in self.iter().zip(other.iter()) {
722            diff |= a ^ b;
723        }
724        diff == 0
725    }
726}
727
728/// Percent-decodes a URL query component (`+` → space, `%41` → `A`).
729fn url_decode(s: &str) -> String {
730    let bytes = s.as_bytes();
731    let mut out = Vec::with_capacity(bytes.len());
732    let mut i = 0;
733    while i < bytes.len() {
734        match bytes[i] {
735            b'+' => {
736                out.push(b' ');
737                i += 1;
738            },
739            b'%' if i + 2 < bytes.len() => {
740                let hi = (bytes[i + 1] as char).to_digit(16);
741                let lo = (bytes[i + 2] as char).to_digit(16);
742                if let (Some(h), Some(l)) = (hi, lo) {
743                    out.push((h * 16 + l) as u8);
744                    i += 3;
745                } else {
746                    out.push(bytes[i]);
747                    i += 1;
748                }
749            },
750            b => {
751                out.push(b);
752                i += 1;
753            },
754        }
755    }
756    String::from_utf8_lossy(&out).into_owned()
757}
758
759/// Maps Ling values to owned rusqlite parameter values for `db_exec`/`db_query`.
760#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
761fn values_to_sql_params(args: &[Value]) -> Vec<ling_http::rusqlite::types::Value> {
762    use ling_http::rusqlite::types::Value as Sql;
763    args.iter()
764        .map(|v| match v {
765            Value::Number(n) if n.fract() == 0.0 && n.abs() < 9e15 => Sql::Integer(*n as i64),
766            Value::Number(n) => Sql::Real(*n),
767            Value::Bool(b) => Sql::Integer(*b as i64),
768            other => Sql::Text(other.to_string()),
769        })
770        .collect()
771}
772
773// GfxState is now defined in crate::gfx — see src/gfx/mod.rs.
774
775// ─── SVG writer ───────────────────────────────────────────────────────────────
776
777struct SvgWriter {
778    path: String,
779    width: f64,
780    height: f64,
781    elements: Vec<String>,
782}
783
784impl SvgWriter {
785    fn new(path: String, width: f64, height: f64) -> Self {
786        let bg = format!("<rect width=\"{width}\" height=\"{height}\" fill=\"#0a0a0a\"/>");
787        Self { path, width, height, elements: vec![bg] }
788    }
789
790    fn save(&self) -> std::io::Result<()> {
791        let w = self.width;
792        let h = self.height;
793        let mut out = format!(
794            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
795             <svg xmlns=\"http://www.w3.org/2000/svg\" \
796             width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\">\n"
797        );
798        for elem in &self.elements {
799            out.push_str("  ");
800            out.push_str(elem);
801            out.push('\n');
802        }
803        out.push_str("</svg>\n");
804        // Create parent directory if it doesn't exist.
805        if let Some(parent) = std::path::Path::new(&self.path).parent() {
806            if !parent.as_os_str().is_empty() {
807                let _ = std::fs::create_dir_all(parent);
808            }
809        }
810        std::fs::write(&self.path, out.as_bytes())
811    }
812}
813
814fn hsl_to_hex(h: f64, s: f64, l: f64) -> String {
815    let s = s / 100.0;
816    let l = l / 100.0;
817    let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
818    let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
819    let m = l - c / 2.0;
820    let (r1, g1, b1) = if h < 60.0 {
821        (c, x, 0.0)
822    } else if h < 120.0 {
823        (x, c, 0.0)
824    } else if h < 180.0 {
825        (0.0, c, x)
826    } else if h < 240.0 {
827        (0.0, x, c)
828    } else if h < 300.0 {
829        (x, 0.0, c)
830    } else {
831        (c, 0.0, x)
832    };
833    let r = ((r1 + m) * 255.0).round() as u8;
834    let g = ((g1 + m) * 255.0).round() as u8;
835    let b = ((b1 + m) * 255.0).round() as u8;
836    format!("#{r:02x}{g:02x}{b:02x}")
837}
838
839// ─── Procedural texture helpers ───────────────────────────────────────────────
840
841fn tex_hash(x: i32, y: i32, seed: u32) -> f32 {
842    let mut h = (x as u32)
843        .wrapping_add((y as u32).wrapping_mul(2654435769))
844        .wrapping_add(seed.wrapping_mul(1234567891));
845    h ^= h >> 16;
846    h = h.wrapping_mul(0x45d9f3b);
847    h ^= h >> 16;
848    h as f32 / u32::MAX as f32
849}
850
851fn tex_vnoise(x: f32, y: f32, seed: u32) -> f32 {
852    let xi = x.floor() as i32;
853    let yi = y.floor() as i32;
854    let sm = |t: f32| t * t * (3.0 - 2.0 * t);
855    let xf = sm(x - xi as f32);
856    let yf = sm(y - yi as f32);
857    let a = tex_hash(xi, yi, seed);
858    let b = tex_hash(xi + 1, yi, seed);
859    let c = tex_hash(xi, yi + 1, seed);
860    let d = tex_hash(xi + 1, yi + 1, seed);
861    a + (b - a) * xf + (c - a) * yf + (a - b - c + d) * xf * yf
862}
863
864fn tex_fbm(x: f32, y: f32, octaves: u32, seed: u32) -> f32 {
865    let mut v = 0.0f32;
866    let mut amp = 0.5f32;
867    let mut f = 1.0f32;
868    for i in 0..octaves {
869        v += tex_vnoise(x * f, y * f, seed.wrapping_add(i * 7919)) * amp;
870        amp *= 0.5;
871        f *= 2.0;
872    }
873    v
874}
875
876fn tex_palette(name: &str, t: f32) -> [f32; 3] {
877    let (a, b, c, d): ([f32; 3], [f32; 3], [f32; 3], [f32; 3]) = match name {
878        "fire" => (
879            [0.8, 0.4, 0.1],
880            [0.7, 0.3, 0.1],
881            [1.0, 0.5, 0.3],
882            [0.0, 0.5, 0.8],
883        ),
884        "ocean" => (
885            [0.1, 0.4, 0.7],
886            [0.3, 0.3, 0.4],
887            [0.8, 1.0, 0.5],
888            [0.3, 0.0, 0.6],
889        ),
890        "psychedelic" => (
891            [0.5, 0.5, 0.5],
892            [0.8, 0.8, 0.8],
893            [1.0, 1.3, 0.7],
894            [0.0, 0.15, 0.3],
895        ),
896        "neon" => (
897            [0.5, 0.5, 0.5],
898            [0.5, 0.5, 0.5],
899            [2.0, 1.0, 0.0],
900            [0.5, 0.2, 0.25],
901        ),
902        "forest" => (
903            [0.3, 0.5, 0.2],
904            [0.2, 0.3, 0.1],
905            [1.0, 0.5, 0.8],
906            [0.1, 0.3, 0.6],
907        ),
908        _ => (
909            [0.5, 0.5, 0.5],
910            [0.5, 0.5, 0.5],
911            [1.0, 1.0, 1.0],
912            [0.0, 0.333, 0.667],
913        ),
914    };
915    [0, 1, 2]
916        .map(|i| (a[i] + b[i] * (std::f32::consts::TAU * (c[i] * t + d[i])).cos()).clamp(0.0, 1.0))
917}
918
919/// Map a physical key to a typed character for ling-ui text input (lowercase).
920#[cfg(not(target_arch = "wasm32"))]
921// Full US-QWERTY keyboard → char, shift-aware. `key_char` only ever emits
922// printable ASCII (never Tab/Enter/control bytes — those keys have no case
923// here at all) so anything read through `text_poll` is safe by construction
924// to drop straight into the game's tab/comma/semicolon/pipe-framed wire
925// protocols without needing per-keystroke sanitization.
926fn key_char(k: minifb::Key, shift: bool) -> Option<char> {
927    use minifb::Key::*;
928    let base = match k {
929        A => 'a',
930        B => 'b',
931        C => 'c',
932        D => 'd',
933        E => 'e',
934        F => 'f',
935        G => 'g',
936        H => 'h',
937        I => 'i',
938        J => 'j',
939        K => 'k',
940        L => 'l',
941        M => 'm',
942        N => 'n',
943        O => 'o',
944        P => 'p',
945        Q => 'q',
946        R => 'r',
947        S => 's',
948        T => 't',
949        U => 'u',
950        V => 'v',
951        W => 'w',
952        X => 'x',
953        Y => 'y',
954        Z => 'z',
955        Key0 => '0',
956        Key1 => '1',
957        Key2 => '2',
958        Key3 => '3',
959        Key4 => '4',
960        Key5 => '5',
961        Key6 => '6',
962        Key7 => '7',
963        Key8 => '8',
964        Key9 => '9',
965        Space => ' ',
966        Minus => '-',
967        Equal => '=',
968        Period => '.',
969        Comma => ',',
970        Slash => '/',
971        Backslash => '\\',
972        Semicolon => ';',
973        Apostrophe => '\'',
974        LeftBracket => '[',
975        RightBracket => ']',
976        Backquote => '`',
977        _ => return None,
978    };
979    if !shift {
980        return Some(base);
981    }
982    Some(match base {
983        'a'..='z' => base.to_ascii_uppercase(),
984        '0' => ')',
985        '1' => '!',
986        '2' => '@',
987        '3' => '#',
988        '4' => '$',
989        '5' => '%',
990        '6' => '^',
991        '7' => '&',
992        '8' => '*',
993        '9' => '(',
994        '-' => '_',
995        '=' => '+',
996        '.' => '>',
997        ',' => '<',
998        '/' => '?',
999        '\\' => '|',
1000        ';' => ':',
1001        '\'' => '"',
1002        '[' => '{',
1003        ']' => '}',
1004        '`' => '~',
1005        other => other,
1006    })
1007}
1008
1009/// Win32 virtual-key code → char, mirroring `key_char` exactly but keyed by
1010/// VK code instead of `minifb::Key` — used by the `GetAsyncKeyState` input
1011/// path (see `os_key_down` / the `topmost_window` fallback in `text_poll`).
1012#[cfg(all(not(target_arch = "wasm32"), windows))]
1013fn vk_char(vk: i32, shift: bool) -> Option<char> {
1014    let base = match vk {
1015        0x41..=0x5A => (b'a' + (vk - 0x41) as u8) as char, // 'A'..'Z'
1016        0x30..=0x39 => (b'0' + (vk - 0x30) as u8) as char, // '0'..'9'
1017        0x20 => ' ',  // VK_SPACE
1018        0xBD => '-',  // VK_OEM_MINUS
1019        0xBB => '=',  // VK_OEM_PLUS (unshifted '=')
1020        0xBE => '.',  // VK_OEM_PERIOD
1021        0xBC => ',',  // VK_OEM_COMMA
1022        0xBF => '/',  // VK_OEM_2
1023        0xDC => '\\', // VK_OEM_5
1024        0xBA => ';',  // VK_OEM_1
1025        0xDE => '\'', // VK_OEM_7
1026        0xDB => '[',  // VK_OEM_4
1027        0xDD => ']',  // VK_OEM_6
1028        0xC0 => '`',  // VK_OEM_3
1029        _ => return None,
1030    };
1031    if !shift {
1032        return Some(base);
1033    }
1034    Some(match base {
1035        'a'..='z' => base.to_ascii_uppercase(),
1036        '0' => ')',
1037        '1' => '!',
1038        '2' => '@',
1039        '3' => '#',
1040        '4' => '$',
1041        '5' => '%',
1042        '6' => '^',
1043        '7' => '&',
1044        '8' => '*',
1045        '9' => '(',
1046        '-' => '_',
1047        '=' => '+',
1048        '.' => '>',
1049        ',' => '<',
1050        '/' => '?',
1051        '\\' => '|',
1052        ';' => ':',
1053        '\'' => '"',
1054        '[' => '{',
1055        ']' => '}',
1056        '`' => '~',
1057        other => other,
1058    })
1059}
1060
1061/// The VK codes `text_poll`'s `GetAsyncKeyState` fallback scans each frame —
1062/// every key `vk_char` can turn into a character.
1063#[cfg(all(not(target_arch = "wasm32"), windows))]
1064const TEXT_POLL_VKS: &[i32] = &[
1065    0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
1066    0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x30, 0x31, 0x32, 0x33,
1067    0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x20, 0xBD, 0xBB, 0xBE, 0xBC, 0xBF, 0xDC, 0xBA, 0xDE,
1068    0xDB, 0xDD, 0xC0,
1069];
1070/// VK_BACK — polled separately from `TEXT_POLL_VKS` since it edits (pops)
1071/// rather than appends.
1072#[cfg(all(not(target_arch = "wasm32"), windows))]
1073const VK_BACK: i32 = 0x08;
1074#[cfg(all(not(target_arch = "wasm32"), windows))]
1075const VK_SHIFT: i32 = 0x10;
1076
1077/// Read the OS's live key-state table directly — unlike minifb's
1078/// `is_key_down`/`get_keys_pressed` (populated from `WM_KEYDOWN`, which only
1079/// arrives at a window holding real Win32 keyboard focus), this works even
1080/// when the borderless-fullscreen window is topmost/visually in front but
1081/// didn't actually win the OS focus fight (Windows' foreground-lock — see
1082/// `force_window_focus`). High bit of `GetAsyncKeyState` = currently down.
1083#[cfg(all(not(target_arch = "wasm32"), windows))]
1084fn os_key_down(vk: i32) -> bool {
1085    unsafe {
1086        extern "system" {
1087            fn GetAsyncKeyState(vkey: i32) -> i16;
1088        }
1089        (GetAsyncKeyState(vk) as u16 & 0x8000) != 0
1090    }
1091}
1092
1093/// True when `hwnd` is the actual OS foreground window. `GetAsyncKeyState`
1094/// (see `os_key_down`) reads the global key-state table regardless of which
1095/// window is focused, so the `topmost_window` input fallback must check this
1096/// before trusting it — otherwise alt-tabbing away from the topmost/
1097/// fullscreen window to type in some other app would still feed keystrokes
1098/// into the game sitting behind it, since nothing about GetAsyncKeyState
1099/// itself would notice the window lost focus.
1100#[cfg(all(not(target_arch = "wasm32"), windows))]
1101fn window_is_foreground(hwnd: isize) -> bool {
1102    unsafe {
1103        extern "system" {
1104            fn GetForegroundWindow() -> isize;
1105        }
1106        hwnd != 0 && GetForegroundWindow() == hwnd
1107    }
1108}
1109
1110/// Delay (seconds) a key must stay held before it starts auto-repeating —
1111/// matches the initial "hesitation" of a normal OS text field.
1112#[cfg(all(not(target_arch = "wasm32"), windows))]
1113const KEY_REPEAT_DELAY: f64 = 0.45;
1114/// Interval (seconds) between repeats once a held key is auto-repeating.
1115#[cfg(all(not(target_arch = "wasm32"), windows))]
1116const KEY_REPEAT_RATE: f64 = 0.045;
1117
1118/// Edge/repeat detector for the `GetAsyncKeyState` text-input fallback:
1119/// fires true on the initial press, then again every `KEY_REPEAT_RATE`
1120/// seconds once the key has been held past `KEY_REPEAT_DELAY` — the same
1121/// press-then-hold-to-repeat behavior minifb's `KeyRepeat::Yes` gives the
1122/// normal (focused-window) path, which this fallback otherwise lacks since
1123/// it does its own from-scratch edge detection per VK code.
1124#[cfg(all(not(target_arch = "wasm32"), windows))]
1125fn key_repeat_fire(now: f64, down: bool, was_down: bool, down_since: &mut f64, last_fire: &mut f64) -> bool {
1126    if !down {
1127        return false;
1128    }
1129    if !was_down {
1130        *down_since = now;
1131        *last_fire = now;
1132        return true;
1133    }
1134    if now - *down_since >= KEY_REPEAT_DELAY && now - *last_fire >= KEY_REPEAT_RATE {
1135        *last_fire = now;
1136        return true;
1137    }
1138    false
1139}
1140
1141/// Map the same key-name strings `key_down`/`key_pressed` already accept
1142/// (see `str_to_minifb_key`) to Win32 virtual-key codes, for the
1143/// `GetAsyncKeyState` fallback path.
1144#[cfg(all(not(target_arch = "wasm32"), windows))]
1145fn str_to_vk(name: &str) -> Option<i32> {
1146    Some(match name {
1147        "numpad0" | "kp0" => 0x60,
1148        "numpad1" | "kp1" => 0x61,
1149        "numpad2" | "kp2" => 0x62,
1150        "numpad3" | "kp3" => 0x63,
1151        "numpad4" | "kp4" => 0x64,
1152        "numpad5" | "kp5" => 0x65,
1153        "numpad6" | "kp6" => 0x66,
1154        "numpad7" | "kp7" => 0x67,
1155        "numpad8" | "kp8" => 0x68,
1156        "numpad9" | "kp9" => 0x69,
1157        "numpad+" | "kp+" => 0x6B,
1158        "numpad-" | "kp-" => 0x6D,
1159        "numpad*" | "kp*" => 0x6A,
1160        "numpad/" | "kp/" => 0x6F,
1161        "left" => 0x25,
1162        "up" => 0x26,
1163        "right" => 0x27,
1164        "down" => 0x28,
1165        "space" => 0x20,
1166        "enter" => 0x0D,
1167        "escape" => 0x1B,
1168        "pageup" => 0x21,
1169        "pagedown" => 0x22,
1170        "lshift" | "leftshift" => 0xA0,
1171        "rshift" | "rightshift" => 0xA1,
1172        "lctrl" | "leftctrl" => 0xA2,
1173        "rctrl" | "rightctrl" => 0xA3,
1174        "lalt" | "leftalt" => 0xA4,
1175        "ralt" | "rightalt" => 0xA5,
1176        "tab" => 0x09,
1177        "backspace" => VK_BACK,
1178        "delete" => 0x2E,
1179        "insert" => 0x2D,
1180        "home" => 0x24,
1181        "end" => 0x23,
1182        "a" => 0x41,
1183        "b" => 0x42,
1184        "c" => 0x43,
1185        "d" => 0x44,
1186        "e" => 0x45,
1187        "f" => 0x46,
1188        "g" => 0x47,
1189        "h" => 0x48,
1190        "i" => 0x49,
1191        "j" => 0x4A,
1192        "k" => 0x4B,
1193        "l" => 0x4C,
1194        "m" => 0x4D,
1195        "n" => 0x4E,
1196        "o" => 0x4F,
1197        "p" => 0x50,
1198        "q" => 0x51,
1199        "r" => 0x52,
1200        "s" => 0x53,
1201        "t" => 0x54,
1202        "u" => 0x55,
1203        "v" => 0x56,
1204        "w" => 0x57,
1205        "x" => 0x58,
1206        "y" => 0x59,
1207        "z" => 0x5A,
1208        "0" => 0x30,
1209        "1" => 0x31,
1210        "2" => 0x32,
1211        "3" => 0x33,
1212        "4" => 0x34,
1213        "5" => 0x35,
1214        "6" => 0x36,
1215        "7" => 0x37,
1216        "8" => 0x38,
1217        "9" => 0x39,
1218        _ => return None,
1219    })
1220}
1221
1222/// Lowercase-hex encode bytes (the wire format for crypto values in Ling).
1223fn hex_encode(bytes: &[u8]) -> String {
1224    let mut s = String::with_capacity(bytes.len() * 2);
1225    for b in bytes {
1226        s.push_str(&format!("{b:02x}"));
1227    }
1228    s
1229}
1230
1231/// Decode a `ling convert` blob: base64 → zlib-inflate → raw little-endian bytes.
1232#[cfg(not(target_arch = "wasm32"))]
1233fn decode_blob(s: &str) -> Result<Vec<u8>, String> {
1234    use base64::Engine as _;
1235    use std::io::Read as _;
1236    let comp = base64::engine::general_purpose::STANDARD
1237        .decode(s.trim())
1238        .map_err(|e| format!("base64: {e}"))?;
1239    let mut out = Vec::new();
1240    flate2::read::ZlibDecoder::new(&comp[..])
1241        .read_to_end(&mut out)
1242        .map_err(|e| format!("inflate: {e}"))?;
1243    Ok(out)
1244}
1245
1246/// Decode a lowercase/uppercase hex string to bytes (ignores malformed tail).
1247#[cfg(not(target_arch = "wasm32"))]
1248fn hex_decode(s: &str) -> Vec<u8> {
1249    let s = s.trim();
1250    (0..s.len() / 2)
1251        .filter_map(|i| u8::from_str_radix(s.get(i * 2..i * 2 + 2)?, 16).ok())
1252        .collect()
1253}
1254
1255/// Decode a hex string into a fixed 32-byte key (zero-padded / truncated).
1256#[cfg(not(target_arch = "wasm32"))]
1257fn hex_to_32(s: &str) -> [u8; 32] {
1258    let v = hex_decode(s);
1259    let mut out = [0u8; 32];
1260    let n = v.len().min(32);
1261    out[..n].copy_from_slice(&v[..n]);
1262    out
1263}
1264
1265/// Builds a minimal, valid PDF with one page per input image (decoded via
1266/// `image`, page sized to the image's own pixel dimensions), backing the
1267/// `pdf_from_images` builtin. No PDF crate: each page is three objects
1268/// (Page / Contents / Image XObject) hand-written directly, with the image
1269/// stream flate-compressed raw RGB8 (`/Filter /FlateDecode /ColorSpace
1270/// /DeviceRGB /BitsPerComponent 8`) — no separate re-encoding step beyond
1271/// what `flate2` (already a dependency) does for the stream itself.
1272#[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1273fn build_pdf_from_images(paths: &[String], out_path: &str) -> Result<(), Box<dyn std::error::Error>> {
1274    use std::io::Write as _;
1275
1276    struct PageImg {
1277        w: u32,
1278        h: u32,
1279        compressed: Vec<u8>,
1280    }
1281
1282    let mut pages = Vec::with_capacity(paths.len());
1283    for p in paths {
1284        let img = image::open(p)?.to_rgb8();
1285        let (w, h) = (img.width(), img.height());
1286        let mut enc = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
1287        enc.write_all(img.as_raw())?;
1288        pages.push(PageImg { w, h, compressed: enc.finish()? });
1289    }
1290
1291    let n = pages.len();
1292    // Object numbers: 1=Catalog, 2=Pages, then per page i (0-indexed):
1293    // 3+3i=Page, 4+3i=Contents, 5+3i=Image XObject.
1294    let page_nums: Vec<u32> = (0..n).map(|i| 3 + (i as u32) * 3).collect();
1295    let total_objs = 2 + n * 3;
1296    let mut off = vec![0usize; total_objs + 1]; // 1-based; off[0] unused
1297
1298    let mut buf: Vec<u8> = Vec::new();
1299    buf.extend_from_slice(b"%PDF-1.4\n%\xE2\xE3\xCF\xD3\n");
1300
1301    off[1] = buf.len();
1302    buf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
1303
1304    off[2] = buf.len();
1305    let kids = page_nums.iter().map(|n| format!("{n} 0 R")).collect::<Vec<_>>().join(" ");
1306    buf.extend_from_slice(format!("2 0 obj\n<< /Type /Pages /Kids [{kids}] /Count {n} >>\nendobj\n").as_bytes());
1307
1308    for (i, page) in pages.iter().enumerate() {
1309        let page_num = page_nums[i];
1310        let content_num = page_num + 1;
1311        let image_num = page_num + 2;
1312        let (w, h) = (page.w, page.h);
1313
1314        off[page_num as usize] = buf.len();
1315        buf.extend_from_slice(format!(
1316            "{page_num} 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {w} {h}] /Resources << /XObject << /Im0 {image_num} 0 R >> >> /Contents {content_num} 0 R >>\nendobj\n"
1317        ).as_bytes());
1318
1319        let content = format!("q {w} 0 0 {h} 0 0 cm /Im0 Do Q");
1320        off[content_num as usize] = buf.len();
1321        buf.extend_from_slice(format!(
1322            "{content_num} 0 obj\n<< /Length {} >>\nstream\n{content}\nendstream\nendobj\n",
1323            content.len()
1324        ).as_bytes());
1325
1326        off[image_num as usize] = buf.len();
1327        buf.extend_from_slice(format!(
1328            "{image_num} 0 obj\n<< /Type /XObject /Subtype /Image /Width {w} /Height {h} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode /Length {} >>\nstream\n",
1329            page.compressed.len()
1330        ).as_bytes());
1331        buf.extend_from_slice(&page.compressed);
1332        buf.extend_from_slice(b"\nendstream\nendobj\n");
1333    }
1334
1335    let xref_offset = buf.len();
1336    buf.extend_from_slice(format!("xref\n0 {}\n", total_objs + 1).as_bytes());
1337    buf.extend_from_slice(b"0000000000 65535 f \n");
1338    for entry in off.iter().skip(1) {
1339        buf.extend_from_slice(format!("{entry:010} 00000 n \n").as_bytes());
1340    }
1341    buf.extend_from_slice(
1342        format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref_offset}\n%%EOF", total_objs + 1).as_bytes(),
1343    );
1344
1345    std::fs::write(out_path, buf)?;
1346    Ok(())
1347}
1348
1349fn tex_rgb(r: f32, g: f32, b: f32) -> u32 {
1350    ((r * 255.0) as u32) << 16 | ((g * 255.0) as u32) << 8 | (b * 255.0) as u32
1351}
1352
1353// ─── 3D Perlin Noise (Improved Perlin 2002) ───────────────────────────────────
1354
1355const PERM: [u8; 512] = [
1356    151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69,
1357    142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219,
1358    203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 35, 63, 189, 114, 56, 42, 123,
1359    165, 38, 72, 93, 69, 139, 138, 78, 149, 159, 56, 89, 152, 78, 61, 140, 63, 26, 142, 76, 124,
1360    132, 72, 11, 90, 44, 82, 59, 96, 41, 148, 126, 157, 13, 49, 27, 176, 33, 47, 14, 97, 78, 71,
1361    40, 87, 183, 4, 122, 92, 7, 72, 3, 246, 17, 225, 87, 91, 106, 203, 190, 57, 74, 76, 88, 207,
1362    208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146,
1363    157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196,
1364    167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94,
1365    11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55,
1366    109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, 186, 120, 37, 46, 28, 166, 180,
1367    198, 232, 221, 116, 31, 75, 189, 139, 138, 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185,
1368    134, 193, 29, 158, 225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223,
1369    140, 161, 137, 13, 191, 230, 66, 104, 153, 199, 167, 147, 99, 179, 92,
1370    // Duplicate for wrap-around indexing
1371    151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69,
1372    142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219,
1373    203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 35, 63, 189, 114, 56, 42, 123,
1374    165, 38, 72, 93, 69, 139, 138, 78, 149, 159, 56, 89, 152, 78, 61, 140, 63, 26, 142, 76, 124,
1375    132, 72, 11, 90, 44, 82, 59, 96, 41, 148, 126, 157, 13, 49, 27, 176, 33, 47, 14, 97, 78, 71,
1376    40, 87, 183, 4, 122, 92, 7, 72, 3, 246, 17, 225, 87, 91, 106, 203, 190, 57, 74, 76, 88, 207,
1377    208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146,
1378    157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196,
1379    167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94,
1380    11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55,
1381    109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174,
1382];
1383
1384fn fade(t: f32) -> f32 {
1385    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
1386}
1387
1388fn grad(hash: u8, x: f32, y: f32, z: f32) -> f32 {
1389    let h = hash & 15;
1390    let u = if h < 8 { x } else { y };
1391    let v = if h < 8 { y } else { z };
1392    (if (h & 1) == 0 { u } else { -u }) + (if (h & 2) == 0 { v } else { -v })
1393}
1394
1395fn perlin3(x: f32, y: f32, z: f32) -> f32 {
1396    let xi = (x.floor() as i32) & 255;
1397    let yi = (y.floor() as i32) & 255;
1398    let zi = (z.floor() as i32) & 255;
1399
1400    let xf = x - x.floor();
1401    let yf = y - y.floor();
1402    let zf = z - z.floor();
1403
1404    let u = fade(xf);
1405    let v = fade(yf);
1406    let w = fade(zf);
1407
1408    let p0 = PERM[xi as usize] as usize;
1409    let p1 = PERM[((xi + 1) & 255) as usize] as usize;
1410    let pa = PERM[(p0 + yi as usize) & 255] as usize;
1411    let pb = PERM[(p0 + ((yi + 1) & 255) as usize) & 255] as usize;
1412    let pc = PERM[(p1 + yi as usize) & 255] as usize;
1413    let pd = PERM[(p1 + ((yi + 1) & 255) as usize) & 255] as usize;
1414
1415    let g000 = grad(PERM[(pa + zi as usize) & 255], xf, yf, zf);
1416    let g001 = grad(
1417        PERM[(pa + ((zi + 1) & 255) as usize) & 255],
1418        xf,
1419        yf,
1420        zf - 1.0,
1421    );
1422    let g010 = grad(PERM[(pb + zi as usize) & 255], xf, yf - 1.0, zf);
1423    let g011 = grad(
1424        PERM[(pb + ((zi + 1) & 255) as usize) & 255],
1425        xf,
1426        yf - 1.0,
1427        zf - 1.0,
1428    );
1429    let g100 = grad(PERM[(pc + zi as usize) & 255], xf - 1.0, yf, zf);
1430    let g101 = grad(
1431        PERM[(pc + ((zi + 1) & 255) as usize) & 255],
1432        xf - 1.0,
1433        yf,
1434        zf - 1.0,
1435    );
1436    let g110 = grad(PERM[(pd + zi as usize) & 255], xf - 1.0, yf - 1.0, zf);
1437    let g111 = grad(
1438        PERM[(pd + ((zi + 1) & 255) as usize) & 255],
1439        xf - 1.0,
1440        yf - 1.0,
1441        zf - 1.0,
1442    );
1443
1444    let l00 = g000 + u * (g100 - g000);
1445    let l01 = g001 + u * (g101 - g001);
1446    let l10 = g010 + u * (g110 - g010);
1447    let l11 = g011 + u * (g111 - g011);
1448
1449    let l0 = l00 + v * (l10 - l00);
1450    let l1 = l01 + v * (l11 - l01);
1451
1452    l0 + w * (l1 - l0)
1453}
1454
1455fn fast_rand_f64(state: &mut u64) -> f64 {
1456    *state = state
1457        .wrapping_mul(6364136223846793005)
1458        .wrapping_add(1442695040888963407);
1459    ((*state >> 32) as u32) as f64 / 4294967296.0
1460}
1461
1462// ─── Circle Drawing Primitives ────────────────────────────────────────────────
1463
1464/// Write one pixel into the framebuffer (normal or additive blend).
1465#[inline]
1466fn put_px(buf: &mut [u32], idx: usize, color: u32, blend: u8) {
1467    if idx >= buf.len() {
1468        return;
1469    }
1470    if blend == 0 {
1471        buf[idx] = color;
1472    } else {
1473        let old = buf[idx];
1474        let r = (((old >> 16) & 255) + ((color >> 16) & 255)).min(255);
1475        let g = (((old >> 8) & 255) + ((color >> 8) & 255)).min(255);
1476        let b = ((old & 255) + (color & 255)).min(255);
1477        buf[idx] = (r << 16) | (g << 8) | b;
1478    }
1479}
1480
1481/// Pack three float colour channels (0..255) into a 0x00RRGGBB word, clamping.
1482#[inline]
1483fn rgb(r: f64, g: f64, b: f64) -> u32 {
1484    let r = (r as i64).clamp(0, 255) as u32;
1485    let g = (g as i64).clamp(0, 255) as u32;
1486    let b = (b as i64).clamp(0, 255) as u32;
1487    (r << 16) | (g << 8) | b
1488}
1489
1490#[allow(clippy::too_many_arguments)]
1491fn draw_circle_outline(
1492    buf: &mut [u32],
1493    w: i32,
1494    h: i32,
1495    cx: i32,
1496    cy: i32,
1497    r: i32,
1498    color: u32,
1499    blend: u8,
1500) {
1501    let r = r.clamp(0, 20000); // guard against overflow / runaway from tiny depths
1502    if r == 0 {
1503        return;
1504    }
1505    let mut x = 0;
1506    let mut y = r;
1507    let mut d = 3 - 2 * r;
1508    while x <= y {
1509        plot_circle_points(buf, w, h, cx, cy, x, y, color, blend);
1510        if d < 0 {
1511            d += 4 * x + 6;
1512        } else {
1513            d += 4 * (x - y) + 10;
1514            y -= 1;
1515        }
1516        x += 1;
1517    }
1518}
1519
1520#[allow(clippy::too_many_arguments)]
1521fn plot_circle_points(
1522    buf: &mut [u32],
1523    w: i32,
1524    h: i32,
1525    cx: i32,
1526    cy: i32,
1527    x: i32,
1528    y: i32,
1529    color: u32,
1530    blend: u8,
1531) {
1532    let points = [
1533        (cx + x, cy + y),
1534        (cx - x, cy + y),
1535        (cx + x, cy - y),
1536        (cx - x, cy - y),
1537        (cx + y, cy + x),
1538        (cx - y, cy + x),
1539        (cx + y, cy - x),
1540        (cx - y, cy - x),
1541    ];
1542    for &(px, py) in &points {
1543        if px >= 0 && px < w && py >= 0 && py < h {
1544            put_px(buf, (py * w + px) as usize, color, blend);
1545        }
1546    }
1547}
1548
1549#[allow(clippy::too_many_arguments)]
1550fn draw_circle_filled(
1551    buf: &mut [u32],
1552    w: i32,
1553    h: i32,
1554    cx: i32,
1555    cy: i32,
1556    r: i32,
1557    color: u32,
1558    blend: u8,
1559) {
1560    if r <= 0 {
1561        return;
1562    }
1563    for dy in -r..=r {
1564        let dx_max = ((r * r - dy * dy) as f64).sqrt() as i32;
1565        let py = cy + dy;
1566        if py < 0 || py >= h {
1567            continue;
1568        }
1569        for dx in -dx_max..=dx_max {
1570            let px = cx + dx;
1571            if px >= 0 && px < w {
1572                put_px(buf, (py * w + px) as usize, color, blend);
1573            }
1574        }
1575    }
1576}
1577
1578#[cfg(test)]
1579mod draw_tests {
1580    use super::*;
1581
1582    #[test]
1583    fn filled_circle_actually_writes_pixels() {
1584        let mut buf = vec![0u32; 100 * 100];
1585        draw_circle_filled(&mut buf, 100, 100, 50, 50, 10, 0xFF00FF, 0);
1586        assert_eq!(buf[50 * 100 + 50], 0xFF00FF, "centre pixel must be filled");
1587        assert_eq!(buf[0], 0, "far corner must stay clear");
1588        let n = buf.iter().filter(|&&p| p != 0).count();
1589        assert!(n > 200 && n < 500, "r=10 disc area ≈ 314, got {n}");
1590    }
1591
1592    #[test]
1593    fn circle_outline_writes_a_ring() {
1594        let mut buf = vec![0u32; 100 * 100];
1595        draw_circle_outline(&mut buf, 100, 100, 50, 50, 20, 0x00FF00, 0);
1596        assert_eq!(buf[50 * 100 + 50], 0, "outline must NOT fill the centre");
1597        assert!(
1598            buf.iter().any(|&p| p == 0x00FF00),
1599            "outline must draw a ring"
1600        );
1601    }
1602
1603    #[test]
1604    fn additive_blend_accumulates_channels() {
1605        let mut buf = vec![0x202020u32; 1];
1606        put_px(&mut buf, 0, 0x404040, 1);
1607        assert_eq!(buf[0], 0x606060);
1608    }
1609}
1610
1611// ─── Interpreter ─────────────────────────────────────────────────────────────
1612
1613/// Customizable colour palette for the vector UI toolkit (packed 0x00RRGGBB).
1614/// `ui_theme(...)` sets it; every widget falls back to these and accepts a
1615/// trailing `r,g,b` override.
1616#[derive(Clone, Copy)]
1617pub struct UiTheme {
1618    pub primary: u32,
1619    pub accent: u32,
1620    pub track: u32,
1621    pub warn: u32,
1622    pub text: u32,
1623    pub bg: u32,
1624}
1625
1626impl Default for UiTheme {
1627    fn default() -> Self {
1628        Self {
1629            primary: 0x00D2FF, // holographic cyan
1630            accent: 0x28FFB4,  // mint
1631            track: 0x2C3E64,   // dim slate
1632            warn: 0xFF5A5A,    // alert red
1633            text: 0xBEEBFF,    // pale cyan
1634            bg: 0x0A1018,      // near-black panel
1635        }
1636    }
1637}
1638
1639#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
1640pub struct Interpreter {
1641    globals: HashMap<String, Expr>,
1642    /// Globals evaluated ONCE at program start (immutable after load).
1643    /// call_named clones this instead of re-evaluating every global per call.
1644    global_seed: Env,
1645    functions: FxHashMap<String, Rc<FnDef>>,
1646    /// `form` definitions: struct name → ordered field names.
1647    pub(crate) structs: HashMap<String, Vec<String>>,
1648    /// `choose` variants: variant name (bare and `Enum::Variant`) → (enum name, arity).
1649    enum_variants: HashMap<String, (String, usize)>,
1650    _modules: HashMap<String, Vec<FnDef>>,
1651    gfx: RefCell<GfxState>,
1652    svg: RefCell<Option<SvgWriter>>,
1653    /// Directory of the primary source file, for relative `use` resolution.
1654    pub source_dir: Option<std::path::PathBuf>,
1655    /// Files already loaded — prevents circular imports.
1656    loaded_files: std::collections::HashSet<String>,
1657    /// Optional audio engine — `None` if no audio device is available.
1658    #[cfg(not(target_arch = "wasm32"))]
1659    audio: Option<AudioEngine>,
1660    #[cfg(not(target_arch = "wasm32"))]
1661    fft: RefCell<FftAnalyzer>,
1662    fft_bands_cache: RefCell<Vec<f32>>,
1663    /// Real-time clock — seconds since Unix epoch at startup (f64 works on both
1664    /// native and wasm32; Instant is not available on wasm32).
1665    start_time_secs: f64,
1666    /// Frame counter — incremented at each present()
1667    frame_num: u64,
1668    /// Target framerate used to pace `present()` on wasm32.
1669    #[cfg(target_arch = "wasm32")]
1670    wasm_target_fps: f64,
1671    /// Next frame deadline (ms since epoch) for wasm frame pacing.
1672    #[cfg(target_arch = "wasm32")]
1673    wasm_next_present_ms: f64,
1674    /// Random state for rand() builtin (xorshift)
1675    rand_state: u64,
1676    /// Microphone input (Phase 1 audio reactivity)
1677    #[cfg(not(target_arch = "wasm32"))]
1678    mic: Option<ling_mic::MicInput>,
1679    /// Persistent KEM keypairs (knot / hybrid identities), referenced by handle.
1680    #[cfg(not(target_arch = "wasm32"))]
1681    crypto_ids: Vec<ling_crypto::KnotIdentity>,
1682    /// Persistent Ed25519 signing keypairs, referenced by handle.
1683    #[cfg(not(target_arch = "wasm32"))]
1684    ed25519_ids: Vec<ling_crypto::Ed25519Keypair>,
1685    /// Editable text-input buffer (ling-ui text fields).
1686    text_buffer: String,
1687    /// Frame counter for record_frame().
1688    record_n: u32,
1689    /// Accumulated microphone samples (for turning sound into crypto donuts).
1690    #[cfg(not(target_arch = "wasm32"))]
1691    mic_buffer: Vec<f32>,
1692    /// Loaded vector UI fonts, referenced by handle (index) from `font_load`.
1693    #[cfg(not(target_arch = "wasm32"))]
1694    fonts: Vec<ling_graphics::VectorFont>,
1695    /// Loaded raster images (PNG/etc.), referenced by handle (index) from
1696    /// `image_load` — read pixel-by-pixel via `image_pixel_r/g/b/a`.
1697    images: Vec<image::RgbaImage>,
1698    /// Customizable UI colour palette (set via `ui_theme`).
1699    ui_theme: UiTheme,
1700    /// Left-mouse state on the previous frame — for widget click-edge detection.
1701    mouse_was_down: bool,
1702    /// Live music engine (decode playback + GM synth) — lazily initialised.
1703    #[cfg(not(target_arch = "wasm32"))]
1704    music: Option<ling_music::MusicEngine>,
1705    #[cfg(not(target_arch = "wasm32"))]
1706    music_init: bool,
1707    /// Decoded tracks (for analysis + playback), by `music_load` handle.
1708    tracks: Vec<ling_music::DecodedAudio>,
1709    /// Parsed `.lrc` lyrics, by `music_lrc` handle.
1710    lyrics: Vec<ling_music::Lyrics>,
1711    /// Parsed MIDI songs, by `music_midi_load` handle.
1712    midis: Vec<ling_music::MidiSong>,
1713    /// Soft bodies (deformable balls), by `soft_ball` handle.
1714    soft_bodies: Vec<ling_physics::soft::SoftBody>,
1715    /// Rigid-body world (angular dynamics), shared by `rb_*`.
1716    rigid_world: ling_physics::rigid::PhysicsWorld,
1717    /// Liquid grids (water/oil), by `liquid_new` handle.
1718    liquids: Vec<ling_physics::liquid::LiquidGrid>,
1719    meshes: Vec<crate::gfx::shapes::ColorMesh>,
1720    /// Loaded glTF models (skeleton + skin weights + animations), by `mesh_load` handle.
1721    gltf_models: std::cell::RefCell<Vec<ling_physics::gltf::GltfModel>>,
1722    /// Active cinematic dialog box (Ocarina/Majora-style), if any.
1723    dialog: Option<ling_game::dialog::Dialog>,
1724    /// Dialog highlight colours by role: text, name, place, item (0x00RRGGBB).
1725    dialog_colors: [u32; 4],
1726    /// Active user-function call frames (names), for error tracebacks.
1727    frames: Vec<String>,
1728    /// Snapshot of `frames` captured the moment a runtime error first arose
1729    /// (the deepest call). Consumed by `take_error_trace`.
1730    error_trace: Option<Vec<String>>,
1731    /// Unified input (gamepads/joysticks/VR/touch via the ling-input
1732    /// "Sensorium"). Lazily initialised on the first `pad_*` builtin call;
1733    /// `None` if no native input backend is available.
1734    #[cfg(not(target_arch = "wasm32"))]
1735    input: RefCell<Option<InputState>>,
1736    /// Routes registered by `http_route(method, path, handler)`, consumed
1737    /// by `http_serve`. Lives here (not a global, unlike `net`) because the
1738    /// handler is a `Value::Fn` closure — `Value` holds `Rc` and so can't
1739    /// safely live in a `static`.
1740    #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1741    http_routes: Vec<(String, String, Value)>,
1742    /// SQLite handle opened by `db_open` — plain rusqlite Connection, no
1743    /// pool: the interpreter is single-threaded, so one connection is both
1744    /// sufficient and contention-free.
1745    #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1746    db: Option<ling_http::rusqlite::Connection>,
1747    /// `(url_prefix, disk_dir)` pairs registered by `http_static`, consumed by
1748    /// `http_serve` — served as raw bytes, bypassing the String-only Value bridge.
1749    #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1750    http_static_dirs: Vec<(String, String)>,
1751    /// Background jobs started by `http_post_async`, polled by `http_job_poll`.
1752    /// A plain `Arc<Mutex<..>>` handle (not `Value`), so it's fine to touch from
1753    /// the background tokio task that fills in each job's result.
1754    #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1755    async_jobs: web::AsyncJobs,
1756}
1757
1758/// Live gamepad input state: a ling-input hub fed by the native `gilrs` backend.
1759#[cfg(not(target_arch = "wasm32"))]
1760struct InputState {
1761    sensorium: ling_input::Sensorium,
1762    backend: ling_input::backend::GilrsBackend,
1763}
1764
1765impl Default for Interpreter {
1766    fn default() -> Self {
1767        Self::new()
1768    }
1769}
1770
1771impl Interpreter {
1772    pub fn new() -> Self {
1773        #[cfg(not(target_arch = "wasm32"))]
1774        let audio = AudioEngine::new()
1775            .map_err(|e| eprintln!("audio init failed (no sound): {e}"))
1776            .ok();
1777        Self {
1778            globals: HashMap::new(),
1779            global_seed: new_env(),
1780            functions: FxHashMap::default(),
1781            structs: HashMap::new(),
1782            enum_variants: HashMap::new(),
1783            _modules: HashMap::new(),
1784            gfx: RefCell::new(GfxState::new()),
1785            svg: RefCell::new(None),
1786            source_dir: None,
1787            loaded_files: std::collections::HashSet::new(),
1788            #[cfg(not(target_arch = "wasm32"))]
1789            audio,
1790            #[cfg(not(target_arch = "wasm32"))]
1791            fft: RefCell::new(FftAnalyzer::new(2048, 44100)),
1792            fft_bands_cache: RefCell::new(vec![]),
1793            start_time_secs: crate::runtime::now_secs(),
1794            frame_num: 0,
1795            #[cfg(target_arch = "wasm32")]
1796            wasm_target_fps: 60.0,
1797            #[cfg(target_arch = "wasm32")]
1798            wasm_next_present_ms: 0.0,
1799            rand_state: 0x123456789ABCDEF,
1800            #[cfg(not(target_arch = "wasm32"))]
1801            mic: None,
1802            #[cfg(not(target_arch = "wasm32"))]
1803            crypto_ids: Vec::new(),
1804            #[cfg(not(target_arch = "wasm32"))]
1805            ed25519_ids: Vec::new(),
1806            text_buffer: String::new(),
1807            record_n: 0,
1808            #[cfg(not(target_arch = "wasm32"))]
1809            mic_buffer: Vec::new(),
1810            #[cfg(not(target_arch = "wasm32"))]
1811            fonts: Vec::new(),
1812            images: Vec::new(),
1813            ui_theme: UiTheme::default(),
1814            mouse_was_down: false,
1815            #[cfg(not(target_arch = "wasm32"))]
1816            music: None,
1817            #[cfg(not(target_arch = "wasm32"))]
1818            music_init: false,
1819            tracks: Vec::new(),
1820            lyrics: Vec::new(),
1821            midis: Vec::new(),
1822            soft_bodies: Vec::new(),
1823            rigid_world: ling_physics::rigid::PhysicsWorld::new(),
1824            liquids: Vec::new(),
1825            meshes: Vec::new(),
1826            gltf_models: std::cell::RefCell::new(Vec::new()),
1827            dialog: None,
1828            dialog_colors: [0xE6F2FF, 0xFFD24A, 0x4AD2FF, 0x6CFF8C], // text · name · place · item
1829            frames: Vec::new(),
1830            error_trace: None,
1831            #[cfg(not(target_arch = "wasm32"))]
1832            input: RefCell::new(None),
1833            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1834            http_routes: Vec::new(),
1835            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1836            db: None,
1837            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1838            http_static_dirs: Vec::new(),
1839            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
1840            async_jobs: web::AsyncJobs::new(),
1841        }
1842    }
1843
1844    /// Lazily initialise the input system and advance it one frame; returns the
1845    /// number of connected gamepads. Call once per game-loop iteration (like a
1846    /// window update) before reading `pad_*` state.
1847    #[cfg(not(target_arch = "wasm32"))]
1848    fn pad_poll(&self) -> usize {
1849        let mut slot = self.input.borrow_mut();
1850        if slot.is_none() {
1851            match ling_input::backend::GilrsBackend::new() {
1852                Ok(backend) => {
1853                    *slot = Some(InputState { sensorium: ling_input::Sensorium::new(4), backend });
1854                },
1855                Err(_) => return 0,
1856            }
1857        }
1858        let st = slot.as_mut().unwrap();
1859        st.sensorium.begin_frame();
1860        st.sensorium.pump(&mut st.backend);
1861        st.sensorium.update(1.0 / 60.0);
1862        st.sensorium.devices.count()
1863    }
1864
1865    /// Read player `slot`'s gamepad with `f`, or return `default` if there is no
1866    /// input system / no such pad.
1867    #[cfg(not(target_arch = "wasm32"))]
1868    fn with_pad<T>(&self, slot: usize, default: T, f: impl FnOnce(&ling_input::Gamepad) -> T) -> T {
1869        let inp = self.input.borrow();
1870        match inp.as_ref().and_then(|s| s.sensorium.player(slot)) {
1871            Some(p) => f(p),
1872            None => default,
1873        }
1874    }
1875
1876    /// Take the call-stack snapshot captured at the deepest runtime error, if any.
1877    /// Frames are ordered outermost-first (entry point first, failing call last).
1878    pub fn take_error_trace(&mut self) -> Vec<String> {
1879        self.error_trace.take().unwrap_or_default()
1880    }
1881
1882    #[cfg(target_arch = "wasm32")]
1883    fn wasm_pace_frame(&mut self) {
1884        let fps = self.wasm_target_fps.max(1.0);
1885        let frame_ms = 1000.0 / fps;
1886        let now = js_sys::Date::now();
1887        if self.wasm_next_present_ms <= 0.0 {
1888            self.wasm_next_present_ms = now + frame_ms;
1889            return;
1890        }
1891
1892        let wait_ms = (self.wasm_next_present_ms - now).floor() as i32;
1893        if wait_ms > 0 {
1894            wasm_sleep_ms(wait_ms);
1895        }
1896
1897        let after = js_sys::Date::now();
1898        if after > self.wasm_next_present_ms + frame_ms * 3.0 {
1899            self.wasm_next_present_ms = after + frame_ms;
1900        } else {
1901            self.wasm_next_present_ms += frame_ms;
1902        }
1903    }
1904
1905    /// Run `body`, recording `name` as a call frame and snapshotting the stack on
1906    /// the first runtime error so a traceback can be reported.
1907    fn framed<T, F>(&mut self, name: &str, body: F) -> Result<T, EvalErr>
1908    where
1909        F: FnOnce(&mut Self) -> Result<T, EvalErr>,
1910    {
1911        self.frames.push(name.to_string());
1912        let result = body(self);
1913        if matches!(result, Err(EvalErr::Runtime(_))) && self.error_trace.is_none() {
1914            self.error_trace = Some(self.frames.clone());
1915        }
1916        self.frames.pop();
1917        result
1918    }
1919
1920    /// Render the active dialog box: beveled frame + dark fill, then the visible
1921    /// (typewriter-revealed) text word-wrapped with colour-coded runs, plus a
1922    /// blinking advance arrow once the page is fully typed.
1923    #[cfg(not(target_arch = "wasm32"))]
1924    fn render_dialog(&mut self, x: f32, y: f32, w: f32, h: f32, font: i64, t: f32) {
1925        let (runs, typing) = match &self.dialog {
1926            Some(d) if !d.is_closed() => {
1927                let runs: Vec<(String, usize, bool)> = d
1928                    .visible_runs()
1929                    .into_iter()
1930                    .map(|r| (r.text, r.role.index(), r.newline_before))
1931                    .collect();
1932                (runs, d.is_typing())
1933            },
1934            _ => return,
1935        };
1936        let colors = self.dialog_colors;
1937        // ── frame + fill ──
1938        let b = 12.0;
1939        let corners: Vec<[f32; 2]> = vec![
1940            [x + b, y],
1941            [x + w - b, y],
1942            [x + w, y + b],
1943            [x + w, y + h - b],
1944            [x + w - b, y + h],
1945            [x + b, y + h],
1946            [x, y + h - b],
1947            [x, y + b],
1948            [x + b, y],
1949        ];
1950        {
1951            let mut gfx = self.gfx.borrow_mut();
1952            let (bw, bh) = (gfx.width, gfx.height);
1953            crate::gfx::raster::fill_contours_aa(
1954                &mut gfx.buffer,
1955                bw,
1956                bh,
1957                0x0A1018,
1958                false,
1959                std::slice::from_ref(&corners),
1960            );
1961            for seg in corners.windows(2) {
1962                crate::gfx::raster::draw_line_aa(
1963                    &mut gfx.buffer,
1964                    bw,
1965                    bh,
1966                    0x00D2FF,
1967                    false,
1968                    seg[0][0],
1969                    seg[0][1],
1970                    seg[1][0],
1971                    seg[1][1],
1972                );
1973            }
1974        }
1975        // ── word-wrapped, colour-coded text ──
1976        let px = 22.0f32;
1977        let pad = 20.0f32;
1978        let line_h = px * 1.45;
1979        let mut cx = x + pad;
1980        let mut cy = y + pad;
1981        let use_font = font >= 0 && (font as usize) < self.fonts.len();
1982        for (text, role, nl) in &runs {
1983            if *nl {
1984                cx = x + pad;
1985                cy += line_h;
1986            }
1987            for word in text.split_inclusive(' ') {
1988                let wpx = if use_font {
1989                    self.fonts[font as usize].measure(word, px)
1990                } else {
1991                    ling_ui::holo::text_width(word, px * 0.6, px * 0.24)
1992                };
1993                if cx + wpx > x + w - pad && cx > x + pad + 1.0 {
1994                    cx = x + pad;
1995                    cy += line_h;
1996                }
1997                if cy + line_h > y + h {
1998                    break;
1999                }
2000                let col = colors[(*role).min(3)];
2001                if use_font {
2002                    let glyphs = self.font_layout_2d_glyphs(font as usize, cx, cy, px, word);
2003                    let mut gfx = self.gfx.borrow_mut();
2004                    let (bw, bh, add) = (gfx.width, gfx.height, gfx.blend == 1);
2005                    for contours in &glyphs {
2006                        crate::gfx::raster::fill_contours_aa(
2007                            &mut gfx.buffer,
2008                            bw,
2009                            bh,
2010                            col,
2011                            add,
2012                            contours,
2013                        );
2014                    }
2015                } else {
2016                    let segs = ling_ui::holo::text_lines(word, cx, cy, px * 0.6, px, px * 0.24);
2017                    let mut gfx = self.gfx.borrow_mut();
2018                    let (bw, bh) = (gfx.width, gfx.height);
2019                    for s in segs {
2020                        draw_line(&mut gfx.buffer, bw, bh, col, s[0], s[1], s[2], s[3]);
2021                    }
2022                }
2023                cx += wpx;
2024            }
2025        }
2026        // ── blinking advance arrow when fully typed ──
2027        if !typing && (t * 3.0).sin() > 0.0 {
2028            let ax = x + w - 26.0;
2029            let ay = y + h - 22.0;
2030            let mut gfx = self.gfx.borrow_mut();
2031            let (bw, bh) = (gfx.width, gfx.height);
2032            crate::gfx::raster::fill_contours_aa(
2033                &mut gfx.buffer,
2034                bw,
2035                bh,
2036                0x00D2FF,
2037                false,
2038                std::slice::from_ref(&vec![
2039                    [ax - 7.0, ay],
2040                    [ax + 7.0, ay],
2041                    [ax, ay + 9.0],
2042                    [ax - 7.0, ay],
2043                ]),
2044            );
2045        }
2046    }
2047
2048    /// Lazily start the music engine on first use (playback/synth need a device;
2049    /// analysis/decoding do not). Returns `false` if no audio device is available.
2050    #[cfg(not(target_arch = "wasm32"))]
2051    fn ensure_music(&mut self) -> bool {
2052        if self.music.is_some() {
2053            return true;
2054        }
2055        if self.music_init {
2056            return false;
2057        }
2058        self.music_init = true;
2059        match ling_music::MusicEngine::new() {
2060            Ok(m) => {
2061                self.music = Some(m);
2062                true
2063            },
2064            Err(e) => {
2065                eprintln!("music engine init failed (no music playback): {e}");
2066                false
2067            },
2068        }
2069    }
2070
2071    #[cfg(target_arch = "wasm32")]
2072    fn wasm_resolve_source_path(&self, path: &str) -> String {
2073        let p = path.trim();
2074        if p.is_empty() {
2075            return String::new();
2076        }
2077        if p.contains("://") || p.starts_with('/') || p.starts_with("./") || p.starts_with("../") {
2078            return p.to_string();
2079        }
2080        if let Some(d) = &self.source_dir {
2081            let base = d.to_string_lossy().replace('\\', "/");
2082            if !base.is_empty() {
2083                return format!(
2084                    "{}/{}",
2085                    base.trim_end_matches('/'),
2086                    p.trim_start_matches("./")
2087                );
2088            }
2089        }
2090        p.to_string()
2091    }
2092
2093    #[cfg(target_arch = "wasm32")]
2094    fn wasm_music_builtin(&mut self, name: &str, args: &[Value]) -> Result<Option<Value>, EvalErr> {
2095        match name {
2096            // music_load(path) -> track handle (decode from fetched bytes)
2097            "music_load" | "载入音乐" | "音楽読込" | "음악로드" | "โหลดเพลง" =>
2098            {
2099                let path = self.arg_str(args, 0, "");
2100                let resolved = self.wasm_resolve_source_path(&path);
2101                match wasm_fetch_bytes(&resolved)
2102                    .and_then(|bytes| ling_music::from_bytes(&bytes).map_err(|e| e.to_string()))
2103                {
2104                    Ok(t) => {
2105                        let id = self.tracks.len();
2106                        self.tracks.push(t);
2107                        return Ok(Some(Value::Number(id as f64)));
2108                    },
2109                    Err(e) => {
2110                        eprintln!("music_load failed ({path}): {e}");
2111                        return Ok(Some(Value::Number(-1.0)));
2112                    },
2113                }
2114            },
2115            "music_duration" | "音乐时长" | "音楽長さ" | "음악길이" | "ความยาวเพลง" =>
2116            {
2117                let id = self.arg_num(args, 0, 0.0)? as i64;
2118                let d = self
2119                    .tracks
2120                    .get(id as usize)
2121                    .map(|t| t.duration)
2122                    .unwrap_or(0.0);
2123                return Ok(Some(Value::Number(d as f64)));
2124            },
2125            "music_bpm" | "节拍速度" | "テンポ" | "템포" | "จังหวะต่อนาที" =>
2126            {
2127                let id = self.arg_num(args, 0, 0.0)? as i64;
2128                let b = self
2129                    .tracks
2130                    .get(id as usize)
2131                    .map(|t| ling_music::analysis::bpm(&t.mono, t.rate))
2132                    .unwrap_or(0.0);
2133                return Ok(Some(Value::Number(b as f64)));
2134            },
2135            "music_key" | "调性" | "調性" | "조성" | "คีย์เพลง" => {
2136                let id = self.arg_num(args, 0, 0.0)? as i64;
2137                let k = self
2138                    .tracks
2139                    .get(id as usize)
2140                    .map(|t| ling_music::analysis::key_name(&t.mono, t.rate))
2141                    .unwrap_or_default();
2142                return Ok(Some(Value::Str(k)));
2143            },
2144            "music_onsets" | "音符起点" | "オンセット" | "온셋" | "จุดเริ่มเสียง" =>
2145            {
2146                let id = self.arg_num(args, 0, 0.0)? as i64;
2147                let v = self
2148                    .tracks
2149                    .get(id as usize)
2150                    .map(|t| ling_music::analysis::onsets(&t.mono, t.rate))
2151                    .unwrap_or_default();
2152                return Ok(Some(Value::List(
2153                    v.into_iter().map(|x| Value::Number(x as f64)).collect::<Vec<_>>().into(),
2154                )));
2155            },
2156            "music_beat_grid" | "节拍网格" | "ビートグリッド" | "비트그리드" | "กริดจังหวะ" =>
2157            {
2158                let id = self.arg_num(args, 0, 0.0)? as i64;
2159                let beats = self
2160                    .tracks
2161                    .get(id as usize)
2162                    .map(|t| {
2163                        let b = ling_music::analysis::bpm(&t.mono, t.rate);
2164                        ling_music::analysis::beat_grid(&t.mono, t.rate, b)
2165                    })
2166                    .unwrap_or_default();
2167                return Ok(Some(Value::List(
2168                    beats.into_iter().map(|x| Value::Number(x as f64)).collect::<Vec<_>>().into(),
2169                )));
2170            },
2171            "music_lrc" | "载入歌词" | "歌詞読込" | "가사로드" | "โหลดเนื้อเพลง" =>
2172            {
2173                let path = self.arg_str(args, 0, "");
2174                let resolved = self.wasm_resolve_source_path(&path);
2175                match wasm_fetch_text(&resolved) {
2176                    Ok(text) => {
2177                        let id = self.lyrics.len();
2178                        self.lyrics.push(ling_music::Lyrics::parse(&text));
2179                        return Ok(Some(Value::Number(id as f64)));
2180                    },
2181                    Err(e) => {
2182                        eprintln!("music_lrc failed ({path}): {e}");
2183                        return Ok(Some(Value::Number(-1.0)));
2184                    },
2185                }
2186            },
2187            "music_lyric" | "当前歌词" | "現在歌詞" | "현재가사" | "เนื้อเพลงปัจจุบัน" =>
2188            {
2189                let id = self.arg_num(args, 0, 0.0)? as i64;
2190                let t = self.arg_num(args, 1, 0.0)? as f32;
2191                let line = self
2192                    .lyrics
2193                    .get(id as usize)
2194                    .map(|l| l.line_at(t).to_string())
2195                    .unwrap_or_default();
2196                return Ok(Some(Value::Str(line)));
2197            },
2198            "music_midi_load" | "载入MIDI" | "MIDI読込" | "미디로드" | "โหลดมิดี" =>
2199            {
2200                let path = self.arg_str(args, 0, "");
2201                let resolved = self.wasm_resolve_source_path(&path);
2202                match wasm_fetch_bytes(&resolved).and_then(|bytes| {
2203                    ling_music::midi::from_bytes(&bytes).map_err(|e| e.to_string())
2204                }) {
2205                    Ok(m) => {
2206                        let id = self.midis.len();
2207                        self.midis.push(m);
2208                        return Ok(Some(Value::Number(id as f64)));
2209                    },
2210                    Err(e) => {
2211                        eprintln!("music_midi_load failed ({path}): {e}");
2212                        return Ok(Some(Value::Number(-1.0)));
2213                    },
2214                }
2215            },
2216            "music_midi_count" | "MIDI数量" | "MIDI数" | "미디수" | "จำนวนมิดี" =>
2217            {
2218                let id = self.arg_num(args, 0, 0.0)? as i64;
2219                let n = self
2220                    .midis
2221                    .get(id as usize)
2222                    .map(|m| m.notes.len())
2223                    .unwrap_or(0);
2224                return Ok(Some(Value::Number(n as f64)));
2225            },
2226            "music_midi_notes" | "MIDI音符" | "MIDIノート" | "미디음표" | "โน้ตมิดี" =>
2227            {
2228                let id = self.arg_num(args, 0, 0.0)? as i64;
2229                let mut out = Vec::new();
2230                if let Some(m) = self.midis.get(id as usize) {
2231                    for n in &m.notes {
2232                        out.push(Value::Number(n.time as f64));
2233                        out.push(Value::Number(n.midi as f64));
2234                    }
2235                }
2236                return Ok(Some(Value::List(out.into())));
2237            },
2238            "music_midi_bars" | "MIDI音条" | "MIDIバー" | "미디바" | "แท่งมิดี" =>
2239            {
2240                let id = self.arg_num(args, 0, 0.0)? as i64;
2241                let mut out = Vec::new();
2242                if let Some(m) = self.midis.get(id as usize) {
2243                    for n in &m.notes {
2244                        out.push(Value::Number(n.time as f64));
2245                        out.push(Value::Number(n.midi as f64));
2246                        out.push(Value::Number(n.dur as f64));
2247                    }
2248                }
2249                return Ok(Some(Value::List(out.into())));
2250            },
2251            "music_judge" | "判定" | "判定する" | "판정" | "ตัดสินจังหวะ" =>
2252            {
2253                let delta_ms = self.arg_num(args, 0, 9999.0)? as f32;
2254                return Ok(Some(Value::Number(
2255                    ling_music::Grade::judge(delta_ms).index() as f64,
2256                )));
2257            },
2258            "music_grade_name" | "判定名" | "判定名称" | "판정이름" | "ชื่อการตัดสิน" =>
2259            {
2260                let idx = self.arg_num(args, 0, 4.0)? as i32;
2261                return Ok(Some(Value::Str(
2262                    ling_music::Grade::from_index(idx).name().to_string(),
2263                )));
2264            },
2265            "music_note_name" | "音名" | "音名称" | "음이름" | "ชื่อโน้ต" =>
2266            {
2267                let hz = self.arg_num(args, 0, 0.0)? as f32;
2268                return Ok(Some(Value::Str(ling_music::note::hz_to_name(hz))));
2269            },
2270            "music_hz" | "音符频率" | "音符周波数" | "음표주파수" | "ความถี่โน้ต" =>
2271            {
2272                let midi = match args.get(0) {
2273                    Some(Value::Str(s)) => ling_music::note::parse_pitch(s).unwrap_or(69),
2274                    Some(Value::Number(n)) => *n as i32,
2275                    _ => 69,
2276                };
2277                return Ok(Some(Value::Number(
2278                    ling_music::note::midi_to_hz(midi as f32) as f64,
2279                )));
2280            },
2281            "music_pitch_score" | "音准评分" | "音程スコア" | "음정점수" | "คะแนนเสียง" =>
2282            {
2283                let hz = self.arg_num(args, 0, 0.0)? as f32;
2284                let target = self.arg_num(args, 1, 0.0)? as f32;
2285                return Ok(Some(Value::Number(
2286                    ling_music::karaoke::pitch_score(hz, target) as f64,
2287                )));
2288            },
2289
2290            // ── Playback ──────────────────────────────────────────────────────
2291            "music_play" | "播放音乐" | "音楽再生" | "음악재생" | "เล่นเพลง" =>
2292            {
2293                let id = self.arg_num(args, 0, 0.0)? as usize;
2294                if let Some(t) = self.tracks.get(id) {
2295                    crate::gfx::audio_web::play_music(id, &t.stereo, t.channels, t.rate, 1.0);
2296                }
2297                return Ok(Some(Value::Unit));
2298            },
2299            "music_pause"
2300            | "暂停音乐"
2301            | "音楽一時停止"
2302            | "음악일시정지"
2303            | "หยุดเพลงชั่วคราว"
2304            | "music_stop"
2305            | "停止音乐"
2306            | "音楽停止"
2307            | "음악정지"
2308            | "หยุดเพลง" => {
2309                let id = self.arg_num(args, 0, 0.0)? as usize;
2310                crate::gfx::audio_web::stop_music(id);
2311                return Ok(Some(Value::Unit));
2312            },
2313            "music_seek" | "定位音乐" | "音楽シーク" | "음악탐색" | "ค้นหาเพลง" =>
2314            {
2315                // Seek is not straightforward on AudioBufferSourceNode; no-op for now.
2316                return Ok(Some(Value::Unit));
2317            },
2318            "music_pos" | "音乐位置" | "音楽位置" | "음악위치" | "ตำแหน่งเพลง" =>
2319            {
2320                return Ok(Some(Value::Number(
2321                    crate::gfx::audio_web::current_music_position(),
2322                )));
2323            },
2324            "music_volume" | "音乐音量" | "音楽音量" | "음악음량" | "ระดับเพลง" =>
2325            {
2326                let vol = self.arg_num(args, 0, 0.8)? as f32;
2327                // Apply to the most-recently started slot (slot 0 is typical).
2328                crate::gfx::audio_web::set_music_volume(0, vol);
2329                return Ok(Some(Value::Unit));
2330            },
2331
2332            // ── FFT bands at current playback position ─────────────────────
2333            "music_fft" | "音乐频谱" | "音楽スペクトル" | "음악스펙트럼" | "สเปกตรัมเพลง" =>
2334            {
2335                let id = self.arg_num(args, 0, 0.0)? as usize;
2336                let nbands = self.arg_num(args, 1, 16.0)? as usize;
2337                let pos = crate::gfx::audio_web::current_music_position() as f32;
2338                let bands = if let Some(t) = self.tracks.get(id) {
2339                    ling_music::analysis::fft_bands_at_pos(&t.mono, t.rate, pos, nbands)
2340                } else {
2341                    vec![0.0f32; nbands]
2342                };
2343                return Ok(Some(Value::List(
2344                    bands.into_iter().map(|x| Value::Number(x as f64)).collect::<Vec<_>>().into(),
2345                )));
2346            },
2347
2348            _ => {},
2349        }
2350        Ok(None)
2351    }
2352
2353    /// Lay out `text` for font `id` at size `px`, returning every glyph contour as
2354    /// a screen-space polyline (x→right, y→down). `(x, y)` is the text box top-left;
2355    /// the baseline is placed `ascent*px` below it. Curves are flattened to 0.3 px.
2356    #[cfg(not(target_arch = "wasm32"))]
2357    fn font_layout_2d(
2358        &mut self,
2359        id: usize,
2360        x: f32,
2361        y: f32,
2362        px: f32,
2363        text: &str,
2364    ) -> Vec<Vec<[f32; 2]>> {
2365        let mut out = Vec::new();
2366        for g in self.font_layout_2d_glyphs(id, x, y, px, text) {
2367            out.extend(g);
2368        }
2369        out
2370    }
2371
2372    /// Same as [`font_layout_2d`] but grouped per glyph (so a fill can apply the
2373    /// non-zero winding rule within each glyph, preserving interior holes).
2374    #[cfg(not(target_arch = "wasm32"))]
2375    fn font_layout_2d_glyphs(
2376        &mut self,
2377        id: usize,
2378        x: f32,
2379        y: f32,
2380        px: f32,
2381        text: &str,
2382    ) -> Vec<Vec<Vec<[f32; 2]>>> {
2383        let font = &mut self.fonts[id];
2384        let asc = font.ascent();
2385        let tol = 0.3 / px;
2386        let mut pen = 0.0f32;
2387        let mut glyphs = Vec::new();
2388        for ch in text.chars() {
2389            let go = font.glyph_outline(ch, tol);
2390            let mut contours = Vec::with_capacity(go.polylines.len());
2391            for pl in &go.polylines {
2392                let mapped: Vec<[f32; 2]> = pl
2393                    .iter()
2394                    .map(|p| [x + (pen + p[0]) * px, y + (asc - p[1]) * px])
2395                    .collect();
2396                contours.push(mapped);
2397            }
2398            glyphs.push(contours);
2399            pen += go.advance;
2400        }
2401        glyphs
2402    }
2403
2404    /// Register every item (functions, structs, globals) and evaluate the
2405    /// non-`do` globals into `global_seed`, WITHOUT running the entry. Used to
2406    /// prime the JIT's fallback interpreter so cranelift-skipped (oversized)
2407    /// functions can still be interpreted with full access to globals + peers.
2408    pub fn register_program(&mut self, program: &Program) -> Result<(), String> {
2409        for item in &program.items {
2410            self.register_item("", item)?;
2411        }
2412        let mut env = new_env();
2413        let non_do: Vec<_> = self
2414            .globals
2415            .iter()
2416            .filter(|(_, e)| !matches!(e, Expr::Do(_)))
2417            .map(|(k, e)| (k.clone(), e.clone()))
2418            .collect();
2419        let mut pending: Vec<(String, Expr)> = Vec::new();
2420        for (k, expr) in &non_do {
2421            let mut tmp = new_env();
2422            if let Ok(v) = self.eval_expr(expr, &mut tmp) {
2423                env.insert(k.clone(), v);
2424            } else {
2425                pending.push((k.clone(), expr.clone()));
2426            }
2427        }
2428        for (k, expr) in &pending {
2429            let mut tmp = env.clone();
2430            if let Ok(v) = self.eval_expr(expr, &mut tmp) {
2431                env.insert(k.clone(), v);
2432            }
2433        }
2434        self.global_seed = env;
2435        Ok(())
2436    }
2437
2438    pub fn run_program(&mut self, program: &Program) -> Result<(), String> {
2439        self.register_program(program)?;
2440        let entry = self
2441            .find_entry()
2442            .ok_or("no entry point — need `bind start = do {...}` or `ผูก เริ่ม = ทำ {...}`")?;
2443        let mut env = self.global_seed.clone();
2444        self.framed("start", |me| me.eval_expr(&entry, &mut env))
2445            .map(|_| ())
2446            .map_err(|e| match e {
2447                EvalErr::Runtime(s) => s,
2448                EvalErr::Return(_) => "unexpected top-level return".to_string(),
2449                EvalErr::Break => "unexpected break at top level".to_string(),
2450            })
2451    }
2452
2453    fn register_item(&mut self, ns: &str, item: &Item) -> Result<(), String> {
2454        match item {
2455            Item::Bind(name, expr) => {
2456                let key = if ns.is_empty() {
2457                    name.clone()
2458                } else {
2459                    format!("{ns}::{name}")
2460                };
2461                self.globals.insert(key, expr.clone());
2462            },
2463            Item::Fn(def) => {
2464                let key = if ns.is_empty() {
2465                    def.name.clone()
2466                } else {
2467                    format!("{ns}::{}", def.name)
2468                };
2469                self.functions.insert(key, Rc::new(def.clone()));
2470            },
2471            Item::Mod(name, body) => {
2472                let child_ns = if ns.is_empty() {
2473                    name.clone()
2474                } else {
2475                    format!("{ns}::{name}")
2476                };
2477                for child in body {
2478                    self.register_item(&child_ns, child)?;
2479                }
2480            },
2481            Item::TypeAlias(_, _) => {},
2482            Item::Struct(name, fields) => {
2483                self.structs.insert(name.clone(), fields.clone());
2484                if !ns.is_empty() {
2485                    self.structs.insert(format!("{ns}::{name}"), fields.clone());
2486                }
2487            },
2488            Item::Enum(name, variants) => {
2489                for v in variants {
2490                    self.enum_variants
2491                        .insert(v.name.clone(), (name.clone(), v.arity));
2492                    self.enum_variants
2493                        .insert(format!("{name}::{}", v.name), (name.clone(), v.arity));
2494                    if !ns.is_empty() {
2495                        self.enum_variants
2496                            .insert(format!("{ns}::{name}::{}", v.name), (name.clone(), v.arity));
2497                    }
2498                }
2499            },
2500            Item::Use { path, alias } => {
2501                self.load_module(path, alias.as_deref(), ns)?;
2502            },
2503        }
2504        Ok(())
2505    }
2506
2507    /// Resolve `path` relative to `source_dir`, load and parse it, then
2508    /// register all its definitions.  If `alias` is given, every name is
2509    /// prefixed with `<parent_ns>::<alias>`.  Circular imports are silently
2510    /// skipped.
2511    fn load_module(
2512        &mut self,
2513        path: &str,
2514        alias: Option<&str>,
2515        parent_ns: &str,
2516    ) -> Result<(), String> {
2517        // ── Wasm32: no filesystem — use the pre-registered module registry ──
2518        #[cfg(target_arch = "wasm32")]
2519        let (source, sub_dir) = {
2520            // Skip if already loaded (circular import guard)
2521            if self.loaded_files.contains(path) {
2522                return Ok(());
2523            }
2524            self.loaded_files.insert(path.to_string());
2525
2526            let src = crate::runtime::get_wasm_module(path)
2527                .or_else(|| crate::runtime::get_wasm_module(&format!("{}.ling", path)))
2528                .ok_or_else(|| format!("use: cannot find module '{path}'"))?;
2529            (src, None::<std::path::PathBuf>)
2530        };
2531
2532        // ── Native: resolve against filesystem ──
2533        #[cfg(not(target_arch = "wasm32"))]
2534        let (source, sub_dir) = {
2535            let base_dir = self
2536                .source_dir
2537                .clone()
2538                .unwrap_or_else(|| std::path::PathBuf::from("."));
2539            let raw = std::path::Path::new(path);
2540            let candidates: Vec<std::path::PathBuf> = vec![
2541                base_dir.join(format!("{}.ling", path)),
2542                base_dir.join(format!("{}.灵", path)),
2543                base_dir.join(format!("{}.령", path)),
2544                base_dir.join(format!("{}.霊", path)),
2545                base_dir.join(format!("{}.ลิง", path)),
2546                base_dir.join(raw),
2547                std::path::PathBuf::from(format!("{}.ling", path)),
2548                std::path::PathBuf::from(path),
2549            ];
2550
2551            let resolved = candidates
2552                .into_iter()
2553                .find(|p| p.exists())
2554                .ok_or_else(|| format!("use: cannot find module '{path}'"))?;
2555
2556            let canonical = resolved
2557                .canonicalize()
2558                .unwrap_or_else(|_| resolved.clone())
2559                .to_string_lossy()
2560                .to_string();
2561
2562            // Skip if already loaded (circular import guard)
2563            if self.loaded_files.contains(&canonical) {
2564                return Ok(());
2565            }
2566            self.loaded_files.insert(canonical.clone());
2567
2568            let src = std::fs::read_to_string(&resolved)
2569                .map_err(|e| format!("use: failed to read '{path}': {e}"))?;
2570            let dir = resolved.parent().map(|p| p.to_path_buf());
2571            (src, dir)
2572        };
2573
2574        let program = crate::parser::parse(&source)
2575            .map_err(|e| format!("use: parse error in '{path}': {e}"))?;
2576
2577        // Compute target namespace: parent_ns :: alias (or just alias, or just parent_ns)
2578        let target_ns = match (parent_ns.is_empty(), alias) {
2579            (_, Some(a)) if !parent_ns.is_empty() => format!("{parent_ns}::{a}"),
2580            (_, Some(a)) => a.to_string(),
2581            (false, None) => parent_ns.to_string(),
2582            (true, None) => String::new(),
2583        };
2584
2585        // Save/restore source_dir for nested relative imports
2586        let prev_dir = self.source_dir.clone();
2587        self.source_dir = sub_dir;
2588
2589        for item in &program.items {
2590            self.register_item(&target_ns, item)?;
2591        }
2592
2593        self.source_dir = prev_dir;
2594        Ok(())
2595    }
2596
2597    fn find_entry(&self) -> Option<Expr> {
2598        // Known entry-point names across supported human languages.
2599        for key in crate::entry::ENTRY_NAMES {
2600            if let Some(e) = self.globals.get(*key) {
2601                return Some(e.clone());
2602            }
2603        }
2604        self.globals
2605            .values()
2606            .find(|e| matches!(e, Expr::Do(_)))
2607            .cloned()
2608    }
2609
2610    // ─── Expression evaluation ────────────────────────────────────────────────
2611
2612    fn eval_expr(&mut self, expr: &Expr, env: &mut Env) -> EvalResult {
2613        match expr {
2614            Expr::Str(s) => Ok(Value::Str(s.clone())),
2615            Expr::Number(n) => Ok(Value::Number(*n)),
2616            Expr::Bool(b) => Ok(Value::Bool(*b)),
2617            Expr::Unit => Ok(Value::Unit),
2618            Expr::Array(elems) => {
2619                let vs: Vec<_> = elems
2620                    .iter()
2621                    .map(|e| self.eval_expr(e, env))
2622                    .collect::<Result<_, _>>()?;
2623                Ok(Value::List(Rc::new(vs)))
2624            },
2625
2626            Expr::Ident(name) => self.lookup(name, env),
2627
2628            Expr::Path(segs) => {
2629                if segs.len() == 1 {
2630                    return self.lookup(&segs[0], env);
2631                }
2632                Ok(Value::Str(segs.join("::")))
2633            },
2634
2635            Expr::Ref(inner) => self.eval_expr(inner, env),
2636            Expr::Await(inner) => self.eval_expr(inner, env),
2637
2638            Expr::Do(stmts) => {
2639                let mut local = env.clone();
2640                Ok(self.exec_block(stmts, &mut local)?.unwrap_or(Value::Unit))
2641            },
2642
2643            Expr::BinOp(op, lhs, rhs) => {
2644                let l = self.eval_expr(lhs, env)?;
2645                let r = self.eval_expr(rhs, env)?;
2646                self.apply_binop(op, l, r)
2647            },
2648
2649            Expr::If { cond, then, elseifs, else_body } => {
2650                let cond_val = self.eval_expr(cond, env)?;
2651                if self.is_truthy(&cond_val) {
2652                    return Ok(self.exec_block(then, env)?.unwrap_or(Value::Unit));
2653                }
2654                for (ei_cond, ei_body) in elseifs {
2655                    let ei_cond_val = self.eval_expr(ei_cond, env)?;
2656                    if self.is_truthy(&ei_cond_val) {
2657                        return Ok(self.exec_block(ei_body, env)?.unwrap_or(Value::Unit));
2658                    }
2659                }
2660                if let Some(eb) = else_body {
2661                    return Ok(self.exec_block(eb, env)?.unwrap_or(Value::Unit));
2662                }
2663                Ok(Value::Unit)
2664            },
2665
2666            Expr::While { cond, body } => {
2667                // Run the body directly in the *outer* env so that
2668                // `bind counter = counter + 1` persists across iterations,
2669                // which is the expected behaviour in a scripting language.
2670                loop {
2671                    let cv = self.eval_expr(cond, env)?;
2672                    if !self.is_truthy(&cv) {
2673                        break;
2674                    }
2675                    match self.exec_block(body, env) {
2676                        Ok(_) => {},
2677                        Err(EvalErr::Break) => break,
2678                        Err(e) => return Err(e),
2679                    }
2680                }
2681                Ok(Value::Unit)
2682            },
2683
2684            Expr::For { var, iter, body } => {
2685                let iter_val = self.eval_expr(iter, env)?;
2686                let items = self.value_to_iter(iter_val)?;
2687                for item in items {
2688                    let mut local = env.clone();
2689                    local.insert(var.clone(), item);
2690                    match self.exec_block(body, &mut local) {
2691                        Ok(_) => {},
2692                        Err(EvalErr::Break) => break,
2693                        Err(e) => return Err(e),
2694                    }
2695                }
2696                Ok(Value::Unit)
2697            },
2698
2699            Expr::Match(subject, arms) => {
2700                let subj = self.eval_expr(subject, env)?;
2701                for arm in arms {
2702                    if let Some(bindings) = self.match_pattern(&arm.pattern, &subj) {
2703                        let mut local = env.clone();
2704                        local.extend(bindings);
2705                        return self.eval_expr(&arm.body, &mut local);
2706                    }
2707                }
2708                Ok(Value::Unit)
2709            },
2710
2711            Expr::Range(lo, hi) => {
2712                let lo_v = self.eval_expr(lo, env)?;
2713                let hi_v = self.eval_expr(hi, env)?;
2714                let lo_n = self.to_number(&lo_v)? as i64;
2715                let hi_n = self.to_number(&hi_v)? as i64;
2716                Ok(Value::List(Rc::new(
2717                    (lo_n..hi_n).map(|i| Value::Number(i as f64)).collect(),
2718                )))
2719            },
2720
2721            Expr::Index(base, idx) => {
2722                let b = self.eval_expr(base, env)?;
2723                let i = self.eval_expr(idx, env)?;
2724                let n = self.to_number(&i)? as usize;
2725                match b {
2726                    Value::List(v) => v
2727                        .get(n)
2728                        .cloned()
2729                        .ok_or_else(|| EvalErr::from(format!("index {n} out of bounds"))),
2730                    Value::Str(s) => s
2731                        .chars()
2732                        .nth(n)
2733                        .map(|c| Value::Str(c.to_string()))
2734                        .ok_or_else(|| EvalErr::from(format!("index {n} out of bounds"))),
2735                    other => Err(EvalErr::from(format!("cannot index {:?}", other))),
2736                }
2737            },
2738
2739            Expr::Call(callee, args) => {
2740                let arg_vals: Vec<Value> = args
2741                    .iter()
2742                    .map(|a| self.eval_expr(a, env))
2743                    .collect::<Result<_, _>>()?;
2744                match callee.as_ref() {
2745                    Expr::Ident(name) => self.call_named(name, arg_vals, env),
2746                    Expr::Path(segs) => self.call_named(&segs.join("::"), arg_vals, env),
2747                    _ => {
2748                        let v = self.eval_expr(callee, env)?;
2749                        self.call_value(v, arg_vals)
2750                    },
2751                }
2752            },
2753
2754            Expr::MethodCall { receiver, method, args } => {
2755                let recv = self.eval_expr(receiver, env)?;
2756                let arg_vals: Vec<Value> = args
2757                    .iter()
2758                    .map(|a| self.eval_expr(a, env))
2759                    .collect::<Result<_, _>>()?;
2760                self.call_method(recv, method, arg_vals)
2761            },
2762
2763            Expr::Closure(params, body) => Ok(Value::Fn(
2764                params.clone(),
2765                vec![Stmt::Expr(*body.clone())],
2766                env.clone(),
2767            )),
2768
2769            Expr::Asm(_) => Ok(Value::Unit),
2770        }
2771    }
2772
2773    // ─── Block execution ─────────────────────────────────────────────────────
2774
2775    fn exec_block(&mut self, stmts: &[Stmt], env: &mut Env) -> Result<Option<Value>, EvalErr> {
2776        let mut last: Option<Value> = None;
2777        for stmt in stmts {
2778            match stmt {
2779                Stmt::Bind(name, expr) => {
2780                    match self.try_inplace_list_update(name, expr, env)? {
2781                        Some(v) => env.insert(name.clone(), v),
2782                        None => {
2783                            let v = self.eval_expr(expr, env)?;
2784                            env.insert(name.clone(), v)
2785                        },
2786                    };
2787                    last = None;
2788                },
2789                Stmt::Return(expr) => {
2790                    let v = self.eval_expr(expr, env)?;
2791                    return Err(EvalErr::Return(v));
2792                },
2793                Stmt::Expr(expr) => {
2794                    last = Some(self.eval_expr(expr, env)?);
2795                },
2796            }
2797        }
2798        Ok(last)
2799    }
2800
2801    /// Fast path for `bind v = list_push(v, x)` / `bind v = list_set(v, i, x)`:
2802    /// the binding aliases the same list being rebuilt, so the env copy keeps the
2803    /// `Rc` shared and `make_mut` copies the whole vector every call. Taking the
2804    /// value out of env first leaves the `Rc` unique (unless truly aliased
2805    /// elsewhere, where copy-on-write still applies), turning O(n) into O(1).
2806    /// Returns `None` to fall back to normal evaluation.
2807    fn try_inplace_list_update(
2808        &mut self,
2809        name: &str,
2810        expr: &Expr,
2811        env: &mut Env,
2812    ) -> Result<Option<Value>, EvalErr> {
2813        let Expr::Call(callee, args) = expr else { return Ok(None) };
2814        let Expr::Ident(fname) = callee.as_ref() else { return Ok(None) };
2815        let is_push = matches!(
2816            fname.as_str(),
2817            "list_push" | "เพิ่มรายการ" | "列表添加" | "リスト追加" | "목록추가"
2818        );
2819        let is_set = matches!(
2820            fname.as_str(),
2821            "list_set" | "ตั้งรายการ" | "设元素" | "要素設定" | "요소설정"
2822        );
2823        if !is_push && !is_set {
2824            return Ok(None);
2825        }
2826        // First arg must be the same variable we are binding, and the builtin
2827        // must not be shadowed by a user function.
2828        match args.first() {
2829            Some(Expr::Ident(a0)) if a0 == name => {},
2830            _ => return Ok(None),
2831        }
2832        if self.functions.contains_key(fname.as_str()) {
2833            return Ok(None);
2834        }
2835        if is_push {
2836            if args.len() != 2 {
2837                return Ok(None);
2838            }
2839            let val = self.eval_expr(&args[1], env)?;
2840            match env.remove(name) {
2841                Some(Value::List(mut v)) => {
2842                    Rc::make_mut(&mut v).push(val);
2843                    Ok(Some(Value::List(v)))
2844                },
2845                other => {
2846                    if let Some(o) = other {
2847                        env.insert(name.to_string(), o);
2848                    }
2849                    Ok(None)
2850                },
2851            }
2852        } else {
2853            if args.len() != 3 {
2854                return Ok(None);
2855            }
2856            let idx_v = self.eval_expr(&args[1], env)?;
2857            let idx = self.to_number(&idx_v).unwrap_or(0.0) as usize;
2858            let val = self.eval_expr(&args[2], env)?;
2859            match env.remove(name) {
2860                Some(Value::List(mut v)) => {
2861                    if idx < v.len() {
2862                        Rc::make_mut(&mut v)[idx] = val;
2863                    }
2864                    Ok(Some(Value::List(v)))
2865                },
2866                other => {
2867                    if let Some(o) = other {
2868                        env.insert(name.to_string(), o);
2869                    }
2870                    Ok(None)
2871                },
2872            }
2873        }
2874    }
2875
2876    // ─── Dispatch helpers ─────────────────────────────────────────────────────
2877
2878    fn lookup(&self, name: &str, env: &Env) -> EvalResult {
2879        if let Some(v) = env.get(name) {
2880            return Ok(v.clone());
2881        }
2882        // Globals are an immutable load-time snapshot shared by every call frame;
2883        // a function reads them here instead of receiving a per-call clone.
2884        if let Some(v) = self.global_seed.get(name) {
2885            return Ok(v.clone());
2886        }
2887        if self.functions.contains_key(name) {
2888            let def = &self.functions[name];
2889            return Ok(Value::Fn(def.params.clone(), def.body.clone(), new_env()));
2890        }
2891        // Bare nullary enum variant used as a value (e.g. `bind p = Origin`).
2892        if let Some((enum_name, 0)) = self.enum_variants.get(name).cloned() {
2893            let variant = name.rsplit("::").next().unwrap_or(name).to_string();
2894            return Ok(Value::Variant { enum_name, variant, payload: Vec::new() });
2895        }
2896        // Math constants usable as plain identifiers (e.g. `sin(pi)`)
2897        match name {
2898            "pi" | "π" | "พาย" | "圆周率" | "円周率" | "파이" => {
2899                return Ok(Value::Number(std::f64::consts::PI))
2900            },
2901            "tau" | "τ" | "双周率" | "タウ" | "타우" | "ทาว" => {
2902                return Ok(Value::Number(std::f64::consts::TAU))
2903            },
2904            _ => {},
2905        }
2906        Err(EvalErr::from(format!("undefined: '{name}'")))
2907    }
2908
2909    /// Profiling wrapper around the real dispatch. Zero overhead unless
2910    /// `LING_PROFILE` is set (one thread-local bool check per call). When on,
2911    /// it tallies per-name call count + inclusive time and, on each frame
2912    /// boundary (`present`), prints a sorted top-down report every
2913    /// `LING_PROFILE_EVERY` frames (default 240). Both the JIT (`ling_builtin` →
2914    /// here) and the tree-walker route through this, so it sees every builtin —
2915    /// in JIT mode user fns are native, so it's a clean builtin/render/physics
2916    /// profile with no nesting double-count.
2917    pub(crate) fn call_named(&mut self, name: &str, args: Vec<Value>, env: &Env) -> EvalResult {
2918        if !ling_profile_enabled() {
2919            return self.call_named_inner(name, args, env);
2920        }
2921        let t0 = crate::runtime::now_secs();
2922        let r = self.call_named_inner(name, args, env);
2923        ling_profile_record(
2924            name,
2925            ((crate::runtime::now_secs() - t0) * 1_000_000_000.0) as u128,
2926        );
2927        r
2928    }
2929
2930    fn call_named_inner(&mut self, name: &str, args: Vec<Value>, env: &Env) -> EvalResult {
2931        // A user-defined function shadows any builtin of the same name, matching
2932        // the JIT/AOT backends (which always resolve a defined function first).
2933        if let Some(def) = self.functions.get(name).cloned() {
2934            let mut call_env =
2935                FxHashMap::with_capacity_and_hasher(def.params.len(), Default::default());
2936            let _ = env; // call-site locals are intentionally NOT visible to fns
2937            for (param, arg) in def.params.iter().zip(args) {
2938                call_env.insert(param.clone(), arg);
2939            }
2940            return match self.framed(name, |me| me.exec_block(&def.body, &mut call_env)) {
2941                Ok(v) => Ok(v.unwrap_or(Value::Unit)),
2942                Err(EvalErr::Return(v)) => Ok(v),
2943                Err(e) => Err(e),
2944            };
2945        }
2946
2947        #[cfg(target_arch = "wasm32")]
2948        if let Some(v) = self.wasm_music_builtin(name, &args)? {
2949            return Ok(v);
2950        }
2951
2952        match name {
2953            // Module global read emitted by the MIR backend: resolve against the
2954            // evaluated global snapshot (functions see globals read-only).
2955            "__ling_global" => {
2956                if let Some(Value::Str(g)) = args.first() {
2957                    if let Some(v) = self.global_seed.get(g.as_str()) {
2958                        return Ok(v.clone());
2959                    }
2960                }
2961                return Ok(Value::Unit);
2962            },
2963            // ── Print ──
2964            "print" | "println" | "印" | "打印" | "印刷" | "พิมพ์" | "출력" | "вывести"
2965            | "imprimir" | "afficher" => {
2966                let s = args
2967                    .iter()
2968                    .map(|v| v.to_string())
2969                    .collect::<Vec<_>>()
2970                    .join("");
2971                println!("{s}");
2972                return Ok(Value::Unit);
2973            },
2974            // print_color(colorIdx, text...) — ANSI-coloured console line.
2975            //   colorIdx 0..7 → bright fg (90+idx): 1=red 2=green 3=yellow 4=blue 6=cyan 7=white.
2976            "print_color" | "พิมพ์สี" => {
2977                #[cfg(windows)]
2978                {
2979                    use std::sync::Once;
2980                    static VT: Once = Once::new();
2981                    VT.call_once(|| {
2982                        extern "system" {
2983                            fn GetStdHandle(n: u32) -> *mut std::ffi::c_void;
2984                            fn GetConsoleMode(h: *mut std::ffi::c_void, m: *mut u32) -> i32;
2985                            fn SetConsoleMode(h: *mut std::ffi::c_void, m: u32) -> i32;
2986                        }
2987                        unsafe {
2988                            let h = GetStdHandle(0xFFFF_FFF5u32); // STD_OUTPUT_HANDLE (-11)
2989                            let mut mode = 0u32;
2990                            if GetConsoleMode(h, &mut mode) != 0 {
2991                                SetConsoleMode(h, mode | 0x0004); // ENABLE_VIRTUAL_TERMINAL_PROCESSING
2992                            }
2993                        }
2994                    });
2995                }
2996                let col = self.arg_num(&args, 0, 7.0)? as i64;
2997                let s = args
2998                    .iter()
2999                    .skip(1)
3000                    .map(|v| v.to_string())
3001                    .collect::<Vec<_>>()
3002                    .join("");
3003                let code = 90 + col.clamp(0, 7);
3004                println!("\x1b[1;{code}m{s}\x1b[0m");
3005                return Ok(Value::Unit);
3006            },
3007            // ── Format ──
3008            "format"
3009            | "格式"
3010            | "フォーマット"
3011            | "서식"
3012            | "รูปแบบ"
3013            | "форматировать"
3014            | "formatear"
3015            | "formater" => {
3016                return Ok(Value::Str(self.builtin_format(&args)?));
3017            },
3018            // ── String join / concatenation ──
3019            "格式::拼接" | "format::join" => match args.first() {
3020                Some(Value::List(items)) => {
3021                    return Ok(Value::Str(items.iter().map(|v| v.to_string()).collect()));
3022                },
3023                _ => return Ok(Value::Str(self.builtin_format(&args)?)),
3024            },
3025            // ── Result constructors ──
3026            "ok" | "好" | "良し" | "좋아" | "โอเค" => {
3027                let val = args.into_iter().next().unwrap_or(Value::Unit);
3028                return Ok(Value::Ok(Box::new(val)));
3029            },
3030            "bad" | "坏" | "err" | "悪い" | "나쁨" | "ผิด" => {
3031                let val = args.into_iter().next().unwrap_or(Value::Unit);
3032                return Ok(Value::Err(Box::new(val)));
3033            },
3034            // ── Vec constructors ──
3035            "向量::从" | "Vec::from" => {
3036                if let Some(Value::List(v)) = args.first() {
3037                    return Ok(Value::List(v.clone()));
3038                }
3039                return Ok(Value::List(Rc::new(args)));
3040            },
3041            "向量::有容量" | "Vec::with_capacity" => {
3042                return Ok(Value::List(Rc::new(Vec::new())))
3043            },
3044            // ── Timer stubs ──
3045            "计时::获取当前小时" | "Timer::hour" => return Ok(Value::Number(14.0)),
3046            "计时::现在" | "Timer::now" => return Ok(Value::Number(1000.0)),
3047            // ── Sleep ──
3048            "sleep" | "หยุด" | "นอน" | "sleep_ms" | "睡眠" | "眠る" | "スリープ" | "잠자기"
3049            | "잠" | "流水::睡眠" | "Flow::sleep" => {
3050                if let Some(ms_val) = args.first() {
3051                    if let Ok(ms) = self.to_number(ms_val) {
3052                        #[cfg(target_arch = "wasm32")]
3053                        wasm_sleep_ms(ms.max(0.0) as i32);
3054                        #[cfg(not(target_arch = "wasm32"))]
3055                        std::thread::sleep(std::time::Duration::from_millis(ms as u64));
3056                    }
3057                }
3058                return Ok(Value::Unit);
3059            },
3060            // ── Flow::parallel stub ──
3061            "流水::并行" | "Flow::parallel" => {
3062                if let Some(Value::Fn(params, body, mut cap)) = args.first().cloned() {
3063                    let _ = params;
3064                    match self.exec_block(&body, &mut cap) {
3065                        Ok(Some(v)) => return Ok(v),
3066                        Ok(None) => return Ok(Value::Unit),
3067                        Err(EvalErr::Return(v)) => return Ok(v),
3068                        Err(e) => return Err(e),
3069                    }
3070                }
3071                return Ok(Value::Unit);
3072            },
3073
3074            // ══════════════════════════════════════════════════════════════════
3075            // MATH BUILTINS  (all args and results are f64)
3076            // Thai aliases: ไซน์ โคไซน์ แทนเจนต์ รากที่สอง ค่าสัมบูรณ์
3077            //               ปัดลง ปัดขึ้น ปัดเศษ ตัดทศนิยม ต่ำสุด สูงสุด
3078            //               จำกัด ยกกำลัง ลอการิทึม พาย
3079            // ══════════════════════════════════════════════════════════════════
3080
3081            // ── Trigonometry (input in radians) ──
3082            "sin" | "ไซน์" | "正弦" | "サイン" | "사인" => {
3083                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.sin()));
3084            },
3085            "cos" | "โคไซน์" | "余弦" | "コサイン" | "코사인" => {
3086                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.cos()));
3087            },
3088
3089            // ── Hyperbolic functions ──
3090            // Hyperbolic tangent
3091            "tanh" | "tanhf" | "双曲正切" | "双曲線正接" | "쌍곡탄젠트" => {
3092                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.tanh()));
3093            },
3094
3095            "tan" | "แทนเจนต์" | "正切" | "タンジェント" | "탄젠트" => {
3096                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.tan()));
3097            },
3098            "asin" | "arcsin" | "反正弦" | "アークサイン" | "아크사인" | "อาร์กไซน์" =>
3099            {
3100                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.asin()));
3101            },
3102            "acos" | "arccos" | "反余弦" | "アークコサイン" | "아크코사인" | "อาร์กโคไซน์" =>
3103            {
3104                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.acos()));
3105            },
3106            "atan" | "arctan" | "反正切" | "アークタンジェント" | "아크탄젠트" | "อาร์กแทนเจนต์" =>
3107            {
3108                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.atan()));
3109            },
3110            "atan2" | "arctan2" | "反正切2" | "アークタンジェント2" | "아크탄젠트2" =>
3111            {
3112                let y = self.arg_num(&args, 0, 0.0)?;
3113                let x = self.arg_num(&args, 1, 1.0)?;
3114                return Ok(Value::Number(y.atan2(x)));
3115            },
3116
3117            // ── Roots / powers ──
3118            "sqrt" | "รากที่สอง" | "平方根" | "根" | "제곱근" => {
3119                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.sqrt()));
3120            },
3121            "cbrt" | "立方根" | "세제곱근" | "รากที่สาม" => {
3122                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.cbrt()));
3123            },
3124            "pow" | "ยกกำลัง" | "幂" | "べき乗" | "거듭제곱" => {
3125                let base = self.arg_num(&args, 0, 0.0)?;
3126                let exp = self.arg_num(&args, 1, 1.0)?;
3127                return Ok(Value::Number(base.powf(exp)));
3128            },
3129            "exp" | "指数" | "指数関数" | "지수" => {
3130                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.exp()));
3131            },
3132            "hypot" | "斜边" | "斜辺" | "빗변" => {
3133                let x = self.arg_num(&args, 0, 0.0)?;
3134                let y = self.arg_num(&args, 1, 0.0)?;
3135                return Ok(Value::Number(x.hypot(y)));
3136            },
3137
3138            // ── Logarithms ──
3139            "ln" | "log" | "ลอการิทึม" | "对数" | "対数" | "로그" => {
3140                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.ln()));
3141            },
3142            "log2" | "对数2" | "対数2" | "로그2" => {
3143                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.log2()));
3144            },
3145            "log10" | "对数10" | "対数10" | "로그10" => {
3146                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.log10()));
3147            },
3148
3149            // ── Rounding / truncation ──
3150            "abs" | "ค่าสัมบูรณ์" | "绝对值" | "绝对" | "絶対値" | "절댓값" | "절대값" =>
3151            {
3152                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.abs()));
3153            },
3154            "floor" | "ปัดลง" | "向下取整" | "下整" | "床関数" | "내림" => {
3155                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.floor()));
3156            },
3157            "ceil" | "ปัดขึ้น" | "向上取整" | "上整" | "天井関数" | "올림" =>
3158            {
3159                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.ceil()));
3160            },
3161            "round" | "ปัดเศษ" | "四舍五入" | "四舍" | "四捨五入" | "반올림" =>
3162            {
3163                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.round()));
3164            },
3165            "trunc"
3166            | "int"
3167            | "ตัดทศนิยม"
3168            | "取整"
3169            | "整数化"
3170            | "整数"
3171            | "截整"
3172            | "정수화"
3173            | "정수"
3174            | "切り捨て"
3175            | "버림" => {
3176                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.trunc()));
3177            },
3178            "fract" | "小数部分" | "小数部" | "소수부" => {
3179                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.fract()));
3180            },
3181
3182            // ── min / max / clamp ──
3183            "min" | "ต่ำสุด" | "最小" | "최솟값" => {
3184                let a = self.arg_num(&args, 0, 0.0)?;
3185                let b = self.arg_num(&args, 1, 0.0)?;
3186                return Ok(Value::Number(a.min(b)));
3187            },
3188            "max" | "สูงสุด" | "最大" | "최댓값" => {
3189                let a = self.arg_num(&args, 0, 0.0)?;
3190                let b = self.arg_num(&args, 1, 0.0)?;
3191                return Ok(Value::Number(a.max(b)));
3192            },
3193            "clamp" | "จำกัด" | "截取" | "範囲制限" | "범위제한" => {
3194                let x = self.arg_num(&args, 0, 0.0)?;
3195                let lo = self.arg_num(&args, 1, 0.0)?;
3196                let hi = self.arg_num(&args, 2, 1.0)?;
3197                return Ok(Value::Number(x.clamp(lo, hi)));
3198            },
3199
3200            // ── Constants (also accessible as plain identifiers via lookup) ──
3201            "pi" | "π" | "พาย" | "圆周率" | "円周率" | "파이" => {
3202                return Ok(Value::Number(std::f64::consts::PI))
3203            },
3204            "tau" | "τ" | "双周率" | "タウ" | "타우" | "ทาว" => {
3205                return Ok(Value::Number(std::f64::consts::TAU))
3206            },
3207
3208            // ══════════════════════════════════════════════════════════════════
3209            // PHASE 1: DMT TRIP CODER FEATURES
3210            // ══════════════════════════════════════════════════════════════════
3211
3212            // ── Step 1: Noise Functions ──
3213            "vnoise" | "noise2" | "นอยส์2ดี" | "柏林噪声2D" | "バリューノイズ2D" | "값노이즈2D" =>
3214            {
3215                let x = self.arg_num(&args, 0, 0.0)? as f32;
3216                let y = self.arg_num(&args, 1, 0.0)? as f32;
3217                let seed = self.arg_num(&args, 2, 0.0)? as u32;
3218                return Ok(Value::Number(tex_vnoise(x, y, seed) as f64));
3219            },
3220
3221            "fbm" | "นอยส์ออร์แกนิก" | "分形噪声" | "フラクタルノイズ" | "프랙탈노이즈" =>
3222            {
3223                let x = self.arg_num(&args, 0, 0.0)? as f32;
3224                let y = self.arg_num(&args, 1, 0.0)? as f32;
3225                let octaves = self.arg_num(&args, 2, 4.0)? as u32;
3226                let seed = self.arg_num(&args, 3, 0.0)? as u32;
3227                return Ok(Value::Number(tex_fbm(x, y, octaves, seed) as f64));
3228            },
3229
3230            "perlin"
3231            | "perlin3"
3232            | "เพอร์ลิน3ดี"
3233            | "柏林噪声3D"
3234            | "パーリンノイズ3D"
3235            | "펄린노이즈3D" => {
3236                let x = self.arg_num(&args, 0, 0.0)? as f32;
3237                let y = self.arg_num(&args, 1, 0.0)? as f32;
3238                let z = self.arg_num(&args, 2, 0.0)? as f32;
3239                return Ok(Value::Number(perlin3(x, y, z) as f64));
3240            },
3241
3242            // ── Step 2: Math Ergonomics ──
3243            "lerp" | "ค่าระหว่าง" | "线性插值" | "線形補間" | "선형보간" =>
3244            {
3245                let a = self.arg_num(&args, 0, 0.0)?;
3246                let b = self.arg_num(&args, 1, 1.0)?;
3247                let t = self.arg_num(&args, 2, 0.0)?;
3248                return Ok(Value::Number(a + (b - a) * t));
3249            },
3250
3251            "smoothstep" | "เปลี่ยนแบบนุ่ม" | "平滑步进" | "スムーズステップ" | "스무스스텝" =>
3252            {
3253                let lo = self.arg_num(&args, 0, 0.0)?;
3254                let hi = self.arg_num(&args, 1, 1.0)?;
3255                let x = self.arg_num(&args, 2, 0.5)?;
3256                let t = ((x - lo) / (hi - lo)).clamp(0.0, 1.0);
3257                return Ok(Value::Number(t * t * (3.0 - 2.0 * t)));
3258            },
3259
3260            "rand" | "สุ่ม" | "随机" | "乱数" | "난수" => {
3261                let val = fast_rand_f64(&mut self.rand_state);
3262                return Ok(Value::Number(val));
3263            },
3264
3265            "sign" | "เครื่องหมาย" | "符号" | "符号関数" | "부호" => {
3266                let x = self.arg_num(&args, 0, 0.0)?;
3267                return Ok(Value::Number(x.signum()));
3268            },
3269
3270            "hsv_to_rgb" | "เอชเอสวีเป็นRGB" | "HSV转RGB" | "HSV変換RGB" | "HSV변환RGB" =>
3271            {
3272                let h = self.arg_num(&args, 0, 0.0)?; // 0-360
3273                let s = self.arg_num(&args, 1, 1.0)?; // 0-1
3274                let v = self.arg_num(&args, 2, 1.0)?; // 0-1
3275                let c = v * s;
3276                let x = c * (1.0 - (((h / 60.0) % 2.0) - 1.0).abs());
3277                let m = v - c;
3278                let (r1, g1, b1) = if h < 60.0 {
3279                    (c, x, 0.0)
3280                } else if h < 120.0 {
3281                    (x, c, 0.0)
3282                } else if h < 180.0 {
3283                    (0.0, c, x)
3284                } else if h < 240.0 {
3285                    (0.0, x, c)
3286                } else if h < 300.0 {
3287                    (x, 0.0, c)
3288                } else {
3289                    (c, 0.0, x)
3290                };
3291                let r = ((r1 + m) * 255.0).round();
3292                let g = ((g1 + m) * 255.0).round();
3293                let b = ((b1 + m) * 255.0).round();
3294                return Ok(Value::List(Rc::new(vec![
3295                    Value::Number(r),
3296                    Value::Number(g),
3297                    Value::Number(b),
3298                ])));
3299            },
3300
3301            "lerp_color" | "ไล่สี" | "颜色插值" | "色補間" | "색보간" => {
3302                let r1 = self.arg_num(&args, 0, 0.0)?;
3303                let g1 = self.arg_num(&args, 1, 0.0)?;
3304                let b1 = self.arg_num(&args, 2, 0.0)?;
3305                let r2 = self.arg_num(&args, 3, 255.0)?;
3306                let g2 = self.arg_num(&args, 4, 255.0)?;
3307                let b2 = self.arg_num(&args, 5, 255.0)?;
3308                let t = self.arg_num(&args, 6, 0.0)?;
3309                let r = r1 + (r2 - r1) * t;
3310                let g = g1 + (g2 - g1) * t;
3311                let b = b1 + (b2 - b1) * t;
3312                let c = ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
3313                self.gfx.borrow_mut().color = c;
3314                return Ok(Value::Unit);
3315            },
3316
3317            // ── Step 3: Real-Time Clock ──
3318            "time_now" | "เวลาปัจจุบัน" | "当前时间" | "経過時間" | "현재시간" =>
3319            {
3320                return Ok(Value::Number(
3321                    crate::runtime::now_secs() - self.start_time_secs,
3322                ));
3323            },
3324
3325            // Wall-clock seconds since the Unix epoch (real date/time). Lets a
3326            // program defer deterministic-yet-evolving generation to the actual
3327            // datetime — same clock → same world, advancing as real time passes.
3328            "epoch_now" | "เวลาโลก" | "datetime" | "现在时刻" | "現在時刻" | "현재시각" =>
3329            {
3330                return Ok(Value::Number(crate::runtime::now_secs()));
3331            },
3332
3333            "frame_count" | "เฟรม" | "帧数" | "フレーム数" | "프레임수" => {
3334                return Ok(Value::Number(self.frame_num as f64));
3335            },
3336
3337            // ── Step 4: Microphone Input ──
3338            "mic_open" | "เปิดไมค์" | "开麦克风" | "マイク開く" | "마이크열기" =>
3339            {
3340                #[cfg(not(target_arch = "wasm32"))]
3341                {
3342                    match ling_mic::MicInput::open(Default::default()) {
3343                        Ok(mic) => {
3344                            let _ = mic.start(|_samples: &[f32]| {}); // No-op callback
3345                            self.mic = Some(mic);
3346                            return Ok(Value::Number(1.0)); // opened
3347                        },
3348                        // No device / permission denied → graceful: don't crash the game loop.
3349                        // Returns 0.0; mic_rms/mic_peak return 0.0 while self.mic is None.
3350                        Err(_e) => {
3351                            self.mic = None;
3352                            return Ok(Value::Number(0.0));
3353                        },
3354                    }
3355                }
3356                #[cfg(target_arch = "wasm32")]
3357                return Ok(Value::Unit);
3358            },
3359
3360            "mic_rms" | "เสียงRMS" | "麦克风音量" | "マイクRMS" | "마이크RMS" =>
3361            {
3362                #[cfg(not(target_arch = "wasm32"))]
3363                {
3364                    let rms = self
3365                        .mic
3366                        .as_ref()
3367                        .map(|m: &ling_mic::MicInput| m.rms())
3368                        .unwrap_or(0.0);
3369                    return Ok(Value::Number(rms as f64));
3370                }
3371                #[cfg(target_arch = "wasm32")]
3372                return Ok(Value::Number(0.0));
3373            },
3374
3375            "mic_peak" | "เสียงพีค" | "麦克风峰值" | "マイクピーク" | "마이크피크" =>
3376            {
3377                #[cfg(not(target_arch = "wasm32"))]
3378                {
3379                    let peak = self
3380                        .mic
3381                        .as_ref()
3382                        .map(|m: &ling_mic::MicInput| m.peak())
3383                        .unwrap_or(0.0);
3384                    return Ok(Value::Number(peak as f64));
3385                }
3386                #[cfg(target_arch = "wasm32")]
3387                return Ok(Value::Number(0.0));
3388            },
3389
3390            "mic_fft" | "วิเคราะห์เสียงสด" | "实时频谱" | "リアルタイムFFT" | "실시간FFT" =>
3391            {
3392                #[cfg(not(target_arch = "wasm32"))]
3393                {
3394                    let n = self.arg_num(&args, 0, 8.0)? as usize;
3395                    if let Some(mic) = self.mic.as_ref() {
3396                        let samples = mic.latest_samples();
3397                        self.fft.borrow_mut().push_samples(&samples);
3398                    }
3399                    let bands = self.fft.borrow().freq_bands(n);
3400                    let result: Vec<Value> =
3401                        bands.iter().map(|&v| Value::Number(v as f64)).collect();
3402                    return Ok(Value::List(Rc::new(result)));
3403                }
3404                #[cfg(target_arch = "wasm32")]
3405                return Ok(Value::List(Vec::new().into()));
3406            },
3407
3408            // ── Step 5: Additive Blend Mode ──
3409            "set_blend" | "โหมดผสม" | "混合模式" | "ブレンドモード" | "블렌드모드" =>
3410            {
3411                let mode = self.arg_num(&args, 0, 0.0)? as u8;
3412                let mut gfx = self.gfx.borrow_mut();
3413                gfx.blend = mode;
3414                let a = gfx.alpha;
3415                gfx.depth_queue.set_state(mode, a); // 3-D queue captures blend for subsequent pushes
3416                return Ok(Value::Unit);
3417            },
3418
3419            // set_antialias(on) — smooth wireframe strokes (lines / edges / arcs /
3420            // circle outlines) via Xiaolin-Wu coverage. Default OFF = crisp,
3421            // opaque, aliased pixels; pass 1 to opt into smooth edges.
3422            "set_antialias" | "ตั้งลบรอยหยัก" | "抗锯齿" | "アンチエイリアス" | "안티에일리어싱" =>
3423            {
3424                let on = self.arg_num(&args, 0, 1.0)? > 0.5;
3425                self.gfx.borrow_mut().antialias = on;
3426                return Ok(Value::Unit);
3427            },
3428            // get_antialias() -> bool — current wireframe anti-aliasing state.
3429            "get_antialias"
3430            | "อ่านลบรอยหยัก"
3431            | "读取抗锯齿"
3432            | "アンチエイリアス取得"
3433            | "안티에일리어싱상태" => {
3434                return Ok(Value::Bool(self.gfx.borrow().antialias));
3435            },
3436
3437            // set_font_antialias(on) — smooth `font_text`/`font_text_fill` glyph
3438            // edges, independent of `set_antialias` (which only covers wireframe
3439            // strokes). Default OFF = crisp, hard-edged text; pass 1 to opt in.
3440            "set_font_antialias" | "글꼴안티에일리어싱" => {
3441                let on = self.arg_num(&args, 0, 1.0)? > 0.5;
3442                self.gfx.borrow_mut().font_antialias = on;
3443                return Ok(Value::Unit);
3444            },
3445            // get_font_antialias() -> bool — current font anti-aliasing state.
3446            "get_font_antialias" | "글꼴안티에일리어싱상태" => {
3447                return Ok(Value::Bool(self.gfx.borrow().font_antialias));
3448            },
3449
3450            // ── Step 6: Circle Primitives ──
3451            "draw_circle" | "วาดวงกลม" | "画圆" | "円描画" | "원그리기" =>
3452            {
3453                let cx = self.arg_num(&args, 0, 0.0)? as i32;
3454                let cy = self.arg_num(&args, 1, 0.0)? as i32;
3455                let r = self.arg_num(&args, 2, 10.0)? as i32;
3456                let mut gfx = self.gfx.borrow_mut();
3457                let (w, h, color, blend) =
3458                    (gfx.width as i32, gfx.height as i32, gfx.color, gfx.blend);
3459                if gfx.antialias {
3460                    let (uw, uh) = (gfx.width, gfx.height);
3461                    let segs = ((r.max(1) as u32) * 4).clamp(24, 512);
3462                    crate::gfx::raster::draw_arc(
3463                        &mut gfx.buffer,
3464                        uw,
3465                        uh,
3466                        color,
3467                        true,
3468                        blend == 1,
3469                        cx as f32,
3470                        cy as f32,
3471                        r as f32,
3472                        0.0,
3473                        std::f32::consts::TAU,
3474                        segs,
3475                    );
3476                } else {
3477                    draw_circle_outline(&mut gfx.buffer, w, h, cx, cy, r, color, blend);
3478                }
3479                return Ok(Value::Unit);
3480            },
3481
3482            "draw_filled_circle"
3483            | "draw_disc"
3484            | "วาดวงกลมทึบ"
3485            | "画实心圆"
3486            | "塗りつぶし円"
3487            | "원채우기" => {
3488                let cx = self.arg_num(&args, 0, 0.0)? as i32;
3489                let cy = self.arg_num(&args, 1, 0.0)? as i32;
3490                let r = self.arg_num(&args, 2, 10.0)? as i32;
3491                let mut gfx = self.gfx.borrow_mut();
3492                let (w, h, color, blend) =
3493                    (gfx.width as i32, gfx.height as i32, gfx.color, gfx.blend);
3494                draw_circle_filled(&mut gfx.buffer, w, h, cx, cy, r, color, blend);
3495                return Ok(Value::Unit);
3496            },
3497
3498            // draw_arc(cx, cy, r, a0, a1 [, segments]) — stroke a circular arc in
3499            // the pen colour (full circle when a1-a0 = TAU). Honors the antialias
3500            // flag; opaque by default (additive when blend = 1).
3501            "draw_arc" | "arc" | "วาดส่วนโค้ง" | "画弧" | "円弧描画" | "호그리기" =>
3502            {
3503                let cx = self.arg_num(&args, 0, 0.0)? as f32;
3504                let cy = self.arg_num(&args, 1, 0.0)? as f32;
3505                let r = self.arg_num(&args, 2, 10.0)? as f32;
3506                let a0 = self.arg_num(&args, 3, 0.0)? as f32;
3507                let a1 = self.arg_num(&args, 4, std::f64::consts::TAU)? as f32;
3508                let default_segs = ((r.abs() * (a1 - a0).abs()).ceil() as u32).clamp(8, 1024);
3509                let segs = self.arg_num(&args, 5, default_segs as f64)? as u32;
3510                let mut gfx = self.gfx.borrow_mut();
3511                let color = gfx.color;
3512                #[cfg(not(target_arch = "wasm32"))]
3513                {
3514                    let (uw, uh, aa, add) = (gfx.width, gfx.height, gfx.antialias, gfx.blend == 1);
3515                    crate::gfx::raster::draw_arc(
3516                        &mut gfx.buffer,
3517                        uw,
3518                        uh,
3519                        color,
3520                        aa,
3521                        add,
3522                        cx,
3523                        cy,
3524                        r,
3525                        a0,
3526                        a1,
3527                        segs,
3528                    );
3529                }
3530                #[cfg(target_arch = "wasm32")]
3531                {
3532                    let segs_f = segs.max(1);
3533                    let step = (a1 - a0) / segs_f as f32;
3534                    let mut px = cx + r * a0.cos();
3535                    let mut py = cy + r * a0.sin();
3536                    let mut i = 1u32;
3537                    while i <= segs_f {
3538                        let a = a0 + step * i as f32;
3539                        let nx = cx + r * a.cos();
3540                        let ny = cy + r * a.sin();
3541                        gfx.depth_queue.push_line(0.0, color, px, py, nx, ny);
3542                        px = nx;
3543                        py = ny;
3544                        i += 1;
3545                    }
3546                }
3547                return Ok(Value::Unit);
3548            },
3549
3550            // ── Step 7: Transparent fills, gradient surfaces & colored shadows ──
3551            // These all write straight into the software framebuffer (gfx.buffer)
3552            // on both native and web, so no target gating is needed.
3553
3554            // set_alpha(a) — pen opacity 0..1 for the alpha-blended fills below.
3555            "set_alpha" | "ตั้งความโปร่งใส" | "设透明" | "アルファ設定" | "투명도설정" =>
3556            {
3557                let a = self.arg_num(&args, 0, 1.0)? as f32;
3558                let mut gfx = self.gfx.borrow_mut();
3559                gfx.alpha = a.clamp(0.0, 1.0);
3560                let (m, al) = (gfx.blend, gfx.alpha);
3561                gfx.depth_queue.set_state(m, al); // 3-D queue captures alpha for subsequent pushes
3562                return Ok(Value::Unit);
3563            },
3564
3565            // mesh_hue(radians) — hue-rotate the baked per-tri colours of every
3566            // subsequent mesh_draw (.lmesh). 0 resets. Cheap: one matrix per call.
3567            "mesh_hue" | "หมุนสีเมช" =>
3568            {
3569                let h = self.arg_num(&args, 0, 0.0)? as f32;
3570                let g = self.arg_num(&args, 1, 1.0)? as f32;
3571                let mut gfx = self.gfx.borrow_mut();
3572                gfx.mesh_hue = h;
3573                gfx.mesh_hue_gain = g.max(0.0);
3574                return Ok(Value::Unit);
3575            },
3576
3577            // set_frame_blur(amount 0..0.95) — afterimage trails: blend the previous
3578            // presented frame into each new one. 0 = off (also frees the ghost buffer).
3579            "set_frame_blur" | "frame_blur" | "เบลอเฟรม" =>
3580            {
3581                let a = self.arg_num(&args, 0, 0.0)? as f32;
3582                let mut gfx = self.gfx.borrow_mut();
3583                gfx.frame_blur = a.clamp(0.0, 0.95);
3584                if gfx.frame_blur <= 0.0 {
3585                    gfx.prev_frame = Vec::new();
3586                }
3587                return Ok(Value::Unit);
3588            },
3589
3590            // set_line_hue_cycle(rate) — rapidly cycle the hue of ALL wireframe line
3591            // strokes (draw_line / draw_line_3d). `rate` in radians/sec; 0 = off.
3592            // Process-global so a single call covers every stroke, every frame.
3593            "set_line_hue_cycle" | "ตั้งวนสีเส้น" => {
3594                let rate = self.arg_num(&args, 0, 0.0)?;
3595                crate::runtime::set_line_hue_rate(rate);
3596                return Ok(Value::Unit);
3597            },
3598
3599            // set_color_space(mode) — 0 = legacy sRGB compositing (default),
3600            // 1 = gamma-correct linear-light compositing (blend in linear, store
3601            // sRGB) so alpha and gradients don't darken/shift hue.
3602            "set_color_space" | "ปริภูมิสี" | "色彩空间" | "色空間" | "색공간" =>
3603            {
3604                let m = self.arg_num(&args, 0, 0.0)? as i64;
3605                self.gfx.borrow_mut().linear_blend = m != 0;
3606                return Ok(Value::Unit);
3607            },
3608
3609            // set_gradient_space(mode) — 1 = perceptual OkLab gradient interp
3610            // (default), 0 = legacy sRGB. Affects grad_triangle / grad_rect.
3611            "set_gradient_space" | "ปริภูมิไล่สี" | "渐变空间" | "グラデ空間" | "그라데이션공간" =>
3612            {
3613                let m = self.arg_num(&args, 0, 1.0)? as i64;
3614                self.gfx.borrow_mut().grad_oklab = m != 0;
3615                return Ok(Value::Unit);
3616            },
3617
3618            // mix_color(r0,g0,b0, r1,g1,b1, t) — set the pen colour to the
3619            // perceptual OkLab blend of two colours (t in 0..1). Far nicer
3620            // mid-tones than a raw RGB lerp.
3621            "mix_color" | "ผสมสี" | "混合颜色" | "色混合" | "색혼합" => {
3622                let c0 = rgb(
3623                    self.arg_num(&args, 0, 0.0)?,
3624                    self.arg_num(&args, 1, 0.0)?,
3625                    self.arg_num(&args, 2, 0.0)?,
3626                );
3627                let c1 = rgb(
3628                    self.arg_num(&args, 3, 255.0)?,
3629                    self.arg_num(&args, 4, 255.0)?,
3630                    self.arg_num(&args, 5, 255.0)?,
3631                );
3632                let t = self.arg_num(&args, 6, 0.5)? as f32;
3633                self.gfx.borrow_mut().color = crate::gfx::color::mix_oklab(c0, c1, t);
3634                return Ok(Value::Unit);
3635            },
3636
3637            // set_depth_test(on) — enable the per-pixel z-buffer for the deferred
3638            // 3-D/queued draws (correct interpenetration) instead of painter's-
3639            // only sort. 0 = off (default), non-zero = on.
3640            "set_depth_test" | "ทดสอบความลึก" | "深度测试" | "深度テスト" | "깊이테스트" =>
3641            {
3642                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
3643                self.gfx.borrow_mut().depth_test = on;
3644                return Ok(Value::Unit);
3645            },
3646
3647            // set_flat_shade(on) / ตั้งแฟลตเชด — perf test: skip all per-triangle/mesh
3648            // lighting (compute_lit_color) and draw with the raw pen colour.
3649            "set_flat_shade" | "ตั้งแฟลตเชด" | "平面着色" | "フラット着色" | "평면음영" =>
3650            {
3651                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
3652                self.gfx.borrow_mut().flat_shade = on;
3653                return Ok(Value::Unit);
3654            },
3655
3656            // set_normal_override(x,y,z) - force subsequent triangle/mesh lighting
3657            // to use a stylized world-space normal until reset_normal_override().
3658            "set_normal_override" =>
3659            {
3660                let x = self.arg_num(&args, 0, 0.0)? as f32;
3661                let y = self.arg_num(&args, 1, -1.0)? as f32;
3662                let z = self.arg_num(&args, 2, 0.0)? as f32;
3663                self.gfx.borrow_mut().normal_override = Some([x, y, z]);
3664                return Ok(Value::Unit);
3665            },
3666
3667            "reset_normal_override" =>
3668            {
3669                self.gfx.borrow_mut().normal_override = None;
3670                return Ok(Value::Unit);
3671            },
3672
3673            // clear_depth() / ล้างความลึก — force the z-buffer to clear on the next
3674            // flush. `เติม` already does this; call explicitly to start a fresh
3675            // depth pass mid-frame (e.g. a separate overlay scene).
3676            "clear_depth" | "ล้างความลึก" | "清深度" | "深度クリア" | "깊이지우기" =>
3677            {
3678                self.gfx.borrow_mut().zbuf_needs_clear = true;
3679                return Ok(Value::Unit);
3680            },
3681
3682            // depth_blur(focus, range, radius) / เบลอความลึก — depth-of-field post
3683            // pass over the framebuffer using the z-buffer: sharp at camera-space
3684            // depth `focus`, blurred up to `radius` px as depth departs by `range`.
3685            // Background (no geometry) blurs fully. Call AFTER `flush_3d` (so the
3686            // z-buffer is populated) and BEFORE `present`. Needs `set_depth_test(1)`.
3687            "depth_blur" | "เบลอความลึก" | "dof" | "depth_of_field" | "景深" =>
3688            {
3689                let focus = self.arg_num(&args, 0, 30.0)? as f32;
3690                let range = self.arg_num(&args, 1, 60.0)? as f32;
3691                let radius = self.arg_num(&args, 2, 3.0)?.max(0.0) as usize;
3692                // oil [0..1] — oil-slick treatment of the blurred zone:
3693                // iridescent chroma fringe + hue swirl (water / heat haze).
3694                let oil = self.arg_num(&args, 3, 0.0)? as f32;
3695                let mut gfx = self.gfx.borrow_mut();
3696                let w = gfx.width;
3697                let h = gfx.height;
3698                if gfx.depth_buf.len() == w * h {
3699                    let g = &mut *gfx;
3700                    crate::gfx::raster::depth_of_field(
3701                        &mut g.buffer,
3702                        &g.depth_buf,
3703                        w,
3704                        h,
3705                        focus,
3706                        range,
3707                        radius,
3708                        oil,
3709                    );
3710                }
3711                return Ok(Value::Unit);
3712            },
3713
3714            // light_pool(x, y, z, radius, r, g, b, intensity) / แอ่งแสง —
3715            // volumetric light splash: a soft additive radial vector gradient on
3716            // the floor at height y — the coloured pool a light throws on the
3717            // ground (underwater-light look). Smooth transparent edge, distance-
3718            // fog aware. Colours 0-255; intensity ~0.2-1.5.
3719            "light_pool" | "แอ่งแสง" | "光池" | "ライトプール" | "빛웅덩이" =>
3720            {
3721                let x = self.arg_num(&args, 0, 0.0)? as f32;
3722                let y = self.arg_num(&args, 1, 0.0)? as f32;
3723                let z = self.arg_num(&args, 2, 0.0)? as f32;
3724                let radius = self.arg_num(&args, 3, 20.0)? as f32;
3725                let r = self.arg_num(&args, 4, 255.0)? as f32 / 255.0;
3726                let g = self.arg_num(&args, 5, 255.0)? as f32 / 255.0;
3727                let b = self.arg_num(&args, 6, 255.0)? as f32 / 255.0;
3728                let inten = self.arg_num(&args, 7, 1.0)? as f32;
3729                self.gfx
3730                    .borrow_mut()
3731                    .emit_light_pool(x, y, z, radius, [r, g, b], inten);
3732                return Ok(Value::Unit);
3733            },
3734
3735            // light_beam(x, y, z, floor_y, radius, r, g, b, intensity) / ลำแสงไฟ —
3736            // volumetric god-ray shaft: a soft additive double-cone from the
3737            // light position down to the floor plane, spreading to `radius`.
3738            // Pair with light_pool at the base. Colours 0-255.
3739            "light_beam" | "ลำแสงไฟ" | "光柱" | "ライトビーム" | "빛기둥" =>
3740            {
3741                let x = self.arg_num(&args, 0, 0.0)? as f32;
3742                let y = self.arg_num(&args, 1, 0.0)? as f32;
3743                let z = self.arg_num(&args, 2, 0.0)? as f32;
3744                let fy = self.arg_num(&args, 3, 0.0)? as f32;
3745                let radius = self.arg_num(&args, 4, 14.0)? as f32;
3746                let r = self.arg_num(&args, 5, 255.0)? as f32 / 255.0;
3747                let g = self.arg_num(&args, 6, 255.0)? as f32 / 255.0;
3748                let b = self.arg_num(&args, 7, 255.0)? as f32 / 255.0;
3749                let inten = self.arg_num(&args, 8, 1.0)? as f32;
3750                self.gfx
3751                    .borrow_mut()
3752                    .emit_light_beam(x, y, z, fy, radius, [r, g, b], inten);
3753                return Ok(Value::Unit);
3754            },
3755
3756            // grad_triangle(x0,y0,r0,g0,b0, x1,y1,r1,g1,b1, x2,y2,r2,g2,b2)
3757            // Smooth per-vertex gradient triangle — a cheap lit surface: put the
3758            // bright colour on the vertex facing the light. Honours set_alpha.
3759            "grad_triangle" | "สามเหลี่ยมไล่สี" | "渐变三角" | "グラデ三角" | "그라데삼각" =>
3760            {
3761                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
3762                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
3763                let c0 = rgb(
3764                    self.arg_num(&args, 2, 255.0)?,
3765                    self.arg_num(&args, 3, 255.0)?,
3766                    self.arg_num(&args, 4, 255.0)?,
3767                );
3768                let x1 = self.arg_num(&args, 5, 0.0)? as f32;
3769                let y1 = self.arg_num(&args, 6, 0.0)? as f32;
3770                let c1 = rgb(
3771                    self.arg_num(&args, 7, 255.0)?,
3772                    self.arg_num(&args, 8, 255.0)?,
3773                    self.arg_num(&args, 9, 255.0)?,
3774                );
3775                let x2 = self.arg_num(&args, 10, 0.0)? as f32;
3776                let y2 = self.arg_num(&args, 11, 0.0)? as f32;
3777                let c2 = rgb(
3778                    self.arg_num(&args, 12, 255.0)?,
3779                    self.arg_num(&args, 13, 255.0)?,
3780                    self.arg_num(&args, 14, 255.0)?,
3781                );
3782                let mut gfx = self.gfx.borrow_mut();
3783                let (w, h, alpha, mode, lin, ok) = (
3784                    gfx.width,
3785                    gfx.height,
3786                    gfx.alpha,
3787                    gfx.blend,
3788                    gfx.linear_blend,
3789                    gfx.grad_oklab,
3790                );
3791                crate::gfx::raster::fill_triangle_grad(
3792                    &mut gfx.buffer,
3793                    w,
3794                    h,
3795                    alpha,
3796                    mode,
3797                    lin,
3798                    ok,
3799                    x0,
3800                    y0,
3801                    c0,
3802                    x1,
3803                    y1,
3804                    c1,
3805                    x2,
3806                    y2,
3807                    c2,
3808                );
3809                return Ok(Value::Unit);
3810            },
3811
3812            // grad_rect(x,y,w,h, r0,g0,b0, r1,g1,b1, dir) — linear-gradient rect.
3813            // dir 0 = horizontal (left→right), else vertical (top→bottom).
3814            "grad_rect" | "สี่เหลี่ยมไล่สี" | "渐变矩形" | "グラデ矩形" | "그라데사각" =>
3815            {
3816                let x = self.arg_num(&args, 0, 0.0)? as f32;
3817                let y = self.arg_num(&args, 1, 0.0)? as f32;
3818                let rw = self.arg_num(&args, 2, 0.0)? as f32;
3819                let rh = self.arg_num(&args, 3, 0.0)? as f32;
3820                let c0 = rgb(
3821                    self.arg_num(&args, 4, 255.0)?,
3822                    self.arg_num(&args, 5, 255.0)?,
3823                    self.arg_num(&args, 6, 255.0)?,
3824                );
3825                let c1 = rgb(
3826                    self.arg_num(&args, 7, 0.0)?,
3827                    self.arg_num(&args, 8, 0.0)?,
3828                    self.arg_num(&args, 9, 0.0)?,
3829                );
3830                let dir = self.arg_num(&args, 10, 1.0)? as u8;
3831                let mut gfx = self.gfx.borrow_mut();
3832                let (w, h, alpha, mode, lin, ok) = (
3833                    gfx.width,
3834                    gfx.height,
3835                    gfx.alpha,
3836                    gfx.blend,
3837                    gfx.linear_blend,
3838                    gfx.grad_oklab,
3839                );
3840                crate::gfx::raster::fill_rect_grad(
3841                    &mut gfx.buffer,
3842                    w,
3843                    h,
3844                    alpha,
3845                    mode,
3846                    lin,
3847                    ok,
3848                    x,
3849                    y,
3850                    rw,
3851                    rh,
3852                    c0,
3853                    c1,
3854                    dir,
3855                );
3856                return Ok(Value::Unit);
3857            },
3858
3859            // shadow_blob(cx,cy, rx,ry, alpha) — soft colored shadow ellipse in
3860            // the current pen colour. Dark colour = normal shadow; any hue = a
3861            // tinted/coloured shadow. Edge softness comes from shadow_params.
3862            "shadow_blob" | "เงาวงรี" | "阴影斑" | "影ブロブ" | "그림자블롭" =>
3863            {
3864                let cx = self.arg_num(&args, 0, 0.0)? as f32;
3865                let cy = self.arg_num(&args, 1, 0.0)? as f32;
3866                let rx = self.arg_num(&args, 2, 16.0)? as f32;
3867                let ry = self.arg_num(&args, 3, 8.0)? as f32;
3868                let a = self.arg_num(&args, 4, 0.5)? as f32;
3869                let mut gfx = self.gfx.borrow_mut();
3870                let (w, h, color, soft, mode, lin) = (
3871                    gfx.width,
3872                    gfx.height,
3873                    gfx.color,
3874                    gfx.shadow.soft,
3875                    gfx.blend,
3876                    gfx.linear_blend,
3877                );
3878                crate::gfx::raster::fill_disc_soft(
3879                    &mut gfx.buffer,
3880                    w,
3881                    h,
3882                    cx,
3883                    cy,
3884                    rx,
3885                    ry,
3886                    color,
3887                    a,
3888                    soft,
3889                    mode,
3890                    lin,
3891                );
3892                return Ok(Value::Unit);
3893            },
3894
3895            // cast_shadow(cx,cy, height) — height-driven contact shadow in the
3896            // current pen colour. Closer to the surface (small height) = smaller,
3897            // darker, sharper; farther (large height) = bigger, fainter, softer.
3898            // Tune the ramp with shadow_params.
3899            "cast_shadow" | "ทอดเงา" | "投射阴影" | "影を落とす" | "그림자드리우기" =>
3900            {
3901                let cx = self.arg_num(&args, 0, 0.0)? as f32;
3902                let cy = self.arg_num(&args, 1, 0.0)? as f32;
3903                let height = (self.arg_num(&args, 2, 0.0)? as f32).max(0.0);
3904                let mut gfx = self.gfx.borrow_mut();
3905                let sp = gfx.shadow;
3906                let radius = (sp.base + sp.grow * height).max(0.5);
3907                let alpha = (sp.alpha - sp.fade * height).clamp(0.04, 1.0);
3908                let soft = (sp.soft + height * 0.004).clamp(0.0, 0.95);
3909                let (w, h, color, mode, lin) = (
3910                    gfx.width,
3911                    gfx.height,
3912                    gfx.color,
3913                    gfx.blend,
3914                    gfx.linear_blend,
3915                );
3916                crate::gfx::raster::fill_disc_soft(
3917                    &mut gfx.buffer,
3918                    w,
3919                    h,
3920                    cx,
3921                    cy,
3922                    radius,
3923                    radius * 0.62,
3924                    color,
3925                    alpha,
3926                    soft,
3927                    mode,
3928                    lin,
3929                );
3930                return Ok(Value::Unit);
3931            },
3932
3933            // shadow_params(base, grow, alpha, fade, soft) — tune cast_shadow.
3934            // Each arg defaults to the current value, so you can set just one.
3935            "shadow_params" | "ตั้งค่าเงา" | "阴影参数" | "影設定" | "그림자설정" =>
3936            {
3937                let cur = self.gfx.borrow().shadow;
3938                let base = self.arg_num(&args, 0, cur.base as f64)? as f32;
3939                let grow = self.arg_num(&args, 1, cur.grow as f64)? as f32;
3940                let alpha = self.arg_num(&args, 2, cur.alpha as f64)? as f32;
3941                let fade = self.arg_num(&args, 3, cur.fade as f64)? as f32;
3942                let soft = self.arg_num(&args, 4, cur.soft as f64)? as f32;
3943                self.gfx.borrow_mut().shadow =
3944                    crate::gfx::ShadowParams { base, grow, alpha, fade, soft };
3945                return Ok(Value::Unit);
3946            },
3947
3948            // depth_triangle(x0,y0, x1,y1, x2,y2, z) — queue a depth-sorted tri in
3949            // the current colour. Drawn back-to-front (painter's algorithm) at
3950            // present(); larger z = farther away. Lets 2-D sprites/quads sort by
3951            // depth the same way 3-D faces do.
3952            "depth_triangle" | "สามเหลี่ยมเรียงลึก" | "深度三角" | "深度三角形" | "깊이삼각" =>
3953            {
3954                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
3955                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
3956                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
3957                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
3958                let x2 = self.arg_num(&args, 4, 0.0)? as f32;
3959                let y2 = self.arg_num(&args, 5, 0.0)? as f32;
3960                let z = self.arg_num(&args, 6, 0.0)? as f32;
3961                let mut gfx = self.gfx.borrow_mut();
3962                let color = gfx.color;
3963                gfx.depth_queue
3964                    .push_triangle(z, color, x0, y0, x1, y1, x2, y2);
3965                return Ok(Value::Unit);
3966            },
3967
3968            // depth_line(x0,y0, x1,y1, z) — queue a depth-sorted line in the
3969            // current colour (same painter's queue as depth_triangle).
3970            "depth_line" | "เส้นเรียงลึก" | "深度线" | "深度線" | "깊이선" =>
3971            {
3972                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
3973                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
3974                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
3975                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
3976                let z = self.arg_num(&args, 4, 0.0)? as f32;
3977                let mut gfx = self.gfx.borrow_mut();
3978                let color = gfx.color;
3979                gfx.depth_queue.push_line(z, color, x0, y0, x1, y1);
3980                return Ok(Value::Unit);
3981            },
3982
3983            // ══════════════════════════════════════════════════════════════════
3984            // GRAPHICS BUILTINS
3985            // Thai names first, then English aliases.
3986            // ══════════════════════════════════════════════════════════════════
3987
3988            // ── เปิดหน้าต่าง(width, height, title) — open_window ──
3989            "เปิดหน้าต่าง" | "open_window" | "gfx_window" | "开窗" | "ウィンドウ開く" | "창열기" =>
3990            {
3991                let w = self.arg_num(&args, 0, 800.0)? as usize;
3992                let h = self.arg_num(&args, 1, 600.0)? as usize;
3993                #[cfg(not(target_arch = "wasm32"))]
3994                {
3995                    let title = args
3996                        .get(2)
3997                        .map(|v| v.to_string())
3998                        .unwrap_or_else(|| "Ling".into());
3999                    let mut gfx = self.gfx.borrow_mut();
4000                    let mut win = minifb::Window::new(
4001                        &title,
4002                        w,
4003                        h,
4004                        minifb::WindowOptions {
4005                            resize: false,
4006                            scale: minifb::Scale::X1,
4007                            ..Default::default()
4008                        },
4009                    )
4010                    .map_err(|e| EvalErr::from(format!("cannot open window: {e}")))?;
4011                    apply_frame_pacing(&mut win, gfx.vsync);
4012                    gfx.buffer = vec![0u32; w * h];
4013                    gfx.width = w;
4014                    gfx.height = h;
4015                    gfx.window = Some(win);
4016                    gfx.topmost_window = false;
4017                    gfx.sync_projection();
4018                    hide_console_window();
4019                }
4020                #[cfg(target_arch = "wasm32")]
4021                {
4022                    let mut gfx = self.gfx.borrow_mut();
4023                    gfx.width = w;
4024                    gfx.height = h;
4025                    gfx.buffer.resize(w * h, 0); // keep the CPU framebuffer in sync
4026                    gfx.sync_projection();
4027                    crate::gfx::webgl::resize(w as u32, h as u32);
4028                }
4029                return Ok(Value::Unit);
4030            },
4031
4032            // ── เติม(r, g, b) — fill / clear screen with colour ──
4033            "เติม" | "fill" | "gfx_fill" | "clear" | "填" | "塗り潰し" | "채우기" | "清"
4034            | "消去" | "지우기" => {
4035                let r = self.arg_num(&args, 0, 0.0)? as u32;
4036                let g = self.arg_num(&args, 1, 0.0)? as u32;
4037                let b = self.arg_num(&args, 2, 0.0)? as u32;
4038                #[cfg(not(target_arch = "wasm32"))]
4039                {
4040                    let c = (r << 16) | (g << 8) | b;
4041                    let mut gfx = self.gfx.borrow_mut();
4042                    gfx.buffer.fill(c);
4043                    gfx.zbuf_needs_clear = true; // clear color ⇒ clear depth next flush
4044                    gfx.edge_set.clear(); // reset shared-edge dedup for new frame
4045                }
4046                #[cfg(target_arch = "wasm32")]
4047                {
4048                    let mut gfx = self.gfx.borrow_mut();
4049                    gfx.fill_r = r as f32 / 255.0;
4050                    gfx.fill_g = g as f32 / 255.0;
4051                    gfx.fill_b = b as f32 / 255.0;
4052                    let c = (r << 16) | (g << 8) | b;
4053                    gfx.buffer.fill(c);
4054                    gfx.zbuf_needs_clear = true;
4055                    gfx.edge_set.clear();
4056                }
4057                return Ok(Value::Unit);
4058            },
4059
4060            // ── set_color_hsl(h, s, l) — set drawing colour from HSL ──
4061            // h: 0–360 degrees, s: 0–100 saturation, l: 0–100 lightness
4062            "set_color_hsl" | "颜色HSL" | "色相" | "HSL色" | "HSL색설정" | "สีHSLวาด" =>
4063            {
4064                let h = self.arg_num(&args, 0, 0.0)?;
4065                let s = self.arg_num(&args, 1, 70.0)?;
4066                let l = self.arg_num(&args, 2, 50.0)?;
4067                let hex = hsl_to_hex(h, s, l);
4068                let r = u32::from_str_radix(&hex[1..3], 16).unwrap_or(255);
4069                let g = u32::from_str_radix(&hex[3..5], 16).unwrap_or(255);
4070                let b = u32::from_str_radix(&hex[5..7], 16).unwrap_or(255);
4071                self.gfx.borrow_mut().color = (r << 16) | (g << 8) | b;
4072                return Ok(Value::Unit);
4073            },
4074
4075            // ── สีดินสอ(r, g, b) — set drawing colour ──
4076            "สีดินสอ" | "set_color" | "gfx_color" | "color" | "设色" | "色設定" | "색설정" =>
4077            {
4078                let r = self.arg_num(&args, 0, 255.0)? as u32;
4079                let g = self.arg_num(&args, 1, 255.0)? as u32;
4080                let b = self.arg_num(&args, 2, 255.0)? as u32;
4081                self.gfx.borrow_mut().color = (r << 16) | (g << 8) | b;
4082                return Ok(Value::Unit);
4083            },
4084
4085            // ── วาดสามเหลี่ยม(x1,y1, x2,y2, x3,y3) — draw filled triangle ──
4086            "วาดสามเหลี่ยม"
4087            | "draw_triangle"
4088            | "gfx_triangle"
4089            | "triangle"
4090            | "画三角"
4091            | "三角形描画"
4092            | "삼각형그리기" => {
4093                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
4094                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
4095                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
4096                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
4097                let x2 = self.arg_num(&args, 4, 0.0)? as f32;
4098                let y2 = self.arg_num(&args, 5, 0.0)? as f32;
4099                let mut gfx = self.gfx.borrow_mut();
4100                let color = gfx.color;
4101                #[cfg(not(target_arch = "wasm32"))]
4102                {
4103                    let w = gfx.width;
4104                    let h = gfx.height;
4105                    fill_triangle(&mut gfx.buffer, w, h, color, x0, y0, x1, y1, x2, y2);
4106                }
4107                #[cfg(target_arch = "wasm32")]
4108                gfx.depth_queue
4109                    .push_triangle(0.0, color, x0, y0, x1, y1, x2, y2);
4110                return Ok(Value::Unit);
4111            },
4112
4113            // ── วาดเส้น(x1,y1, x2,y2) — draw line ──
4114            "วาดเส้น" | "draw_line" | "gfx_line" | "line" | "画线" | "線描く" | "선그리기" =>
4115            {
4116                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
4117                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
4118                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
4119                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
4120                let mut gfx = self.gfx.borrow_mut();
4121                let color = gfx.color;
4122                #[cfg(not(target_arch = "wasm32"))]
4123                {
4124                    let w = gfx.width;
4125                    let h = gfx.height;
4126                    let aa = gfx.antialias;
4127                    let add = gfx.blend == 1;
4128                    if aa {
4129                        crate::gfx::raster::draw_line_aa(
4130                            &mut gfx.buffer,
4131                            w,
4132                            h,
4133                            color,
4134                            add,
4135                            x0,
4136                            y0,
4137                            x1,
4138                            y1,
4139                        );
4140                    } else {
4141                        draw_line(&mut gfx.buffer, w, h, color, x0, y0, x1, y1);
4142                    }
4143                }
4144                #[cfg(target_arch = "wasm32")]
4145                gfx.depth_queue.push_line(0.0, color, x0, y0, x1, y1);
4146                return Ok(Value::Unit);
4147            },
4148
4149            // ── วาดจุด(x, y) — plot a single pixel ──
4150            "วาดจุด" | "draw_pixel" | "gfx_pixel" | "pixel" | "画点" | "点描く" | "점그리기" =>
4151            {
4152                let px = self.arg_num(&args, 0, 0.0)? as i32;
4153                let py = self.arg_num(&args, 1, 0.0)? as i32;
4154                #[cfg(not(target_arch = "wasm32"))]
4155                {
4156                    let mut gfx = self.gfx.borrow_mut();
4157                    let color = gfx.color;
4158                    let w = gfx.width;
4159                    let h = gfx.height;
4160                    if px >= 0 && py >= 0 && (px as usize) < w && (py as usize) < h {
4161                        gfx.buffer[py as usize * w + px as usize] = color;
4162                    }
4163                }
4164                #[cfg(target_arch = "wasm32")]
4165                {
4166                    // Render pixel as a 1×1 square via two triangles.
4167                    let mut gfx = self.gfx.borrow_mut();
4168                    let color = gfx.color;
4169                    let x = px as f32;
4170                    let y = py as f32;
4171                    gfx.depth_queue
4172                        .push_triangle(0.0, color, x, y, x + 1.0, y, x + 1.0, y + 1.0);
4173                    gfx.depth_queue
4174                        .push_triangle(0.0, color, x, y, x + 1.0, y + 1.0, x, y + 1.0);
4175                }
4176                return Ok(Value::Unit);
4177            },
4178
4179            // ── แสดงผล() — flush depth queue, then present frame to screen ──
4180            "แสดงผล" | "present" | "gfx_present" | "show" | "显" | "呈现" | "表示" | "표시" =>
4181            {
4182                // Click-edge widgets (ui_button etc.) compare THIS frame's
4183                // mouse_now() against `mouse_was_down` to detect a fresh
4184                // press. That comparison only works if mouse_was_down
4185                // reflects what the script itself observed this frame — i.e.
4186                // the state from BEFORE update_with_buffer below pulls in new
4187                // OS events. Capturing it after (as the "freshest" read)
4188                // would mean a just-arrived click is already baked into
4189                // mouse_was_down by the time next frame's ui_button compares
4190                // against it, so `down && !mouse_was_down` is never true and
4191                // clicks never register at all. Declared at the top of the
4192                // match arm (not inside the block below) so it survives past
4193                // the wasm32/non-wasm32 split further down.
4194                #[cfg(not(target_arch = "wasm32"))]
4195                let pre_update_mouse_down = self
4196                    .gfx
4197                    .borrow()
4198                    .window
4199                    .as_ref()
4200                    .map(|w| w.get_mouse_down(minifb::MouseButton::Left))
4201                    .unwrap_or(false);
4202                #[cfg(not(target_arch = "wasm32"))]
4203                {
4204                    ling_fps_tick();
4205                    ling_phase_frame();
4206                    // Flush depth queue and present — release borrow before reading mouse.
4207                    {
4208                        let mut gfx = self.gfx.borrow_mut();
4209                        if !gfx.depth_queue.is_empty() {
4210                            let w = gfx.width;
4211                            let h = gfx.height;
4212                            let dt = gfx.depth_test;
4213                            let reset_z = gfx.zbuf_needs_clear;
4214                            let (bm, ba) = (gfx.blend, gfx.alpha);
4215                            let aa = gfx.antialias;
4216                            let queue = std::mem::take(&mut gfx.depth_queue);
4217                            {
4218                                let g = &mut *gfx;
4219                                let z = if dt { Some(&mut g.depth_buf) } else { None };
4220                                queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
4221                            }
4222                            gfx.depth_queue.set_state(bm, ba);
4223                            gfx.zbuf_needs_clear = false;
4224                        }
4225                        let _t = std::time::Instant::now();
4226                        if !gfx.post_done {
4227                            gfx.toon_post_process();
4228                        }
4229                        gfx.post_done = false;
4230                        ling_phase_add(phase::TOON, _t.elapsed().as_nanos());
4231                        let w = gfx.width;
4232                        let h = gfx.height;
4233                        let g = &mut *gfx;
4234                        if g.frame_blur > 0.0 {
4235                            // Afterimage trails: previous frame decays by `frame_blur`
4236                            // per frame and composites with MAX — fresh content stays
4237                            // full-brightness, ghosts fade out over time.
4238                            // retention 0.98 @60fps ≈ trails last ~2.6 s.
4239                            let a = (g.frame_blur.clamp(0.0, 0.995) * 256.0) as u32;
4240                            if g.prev_frame.len() != g.buffer.len() {
4241                                g.prev_frame = g.buffer.clone();
4242                            }
4243                            for (dst, prev) in g.buffer.iter_mut().zip(g.prev_frame.iter_mut()) {
4244                                let c = *dst;
4245                                let pv = *prev;
4246                                let pr = (((pv >> 16) & 0xFF) * a) >> 8;
4247                                let pg = (((pv >> 8) & 0xFF) * a) >> 8;
4248                                let pb = ((pv & 0xFF) * a) >> 8;
4249                                let cr = (c >> 16) & 0xFF;
4250                                let cg = (c >> 8) & 0xFF;
4251                                let cb = c & 0xFF;
4252                                let outp = (cr.max(pr) << 16) | (cg.max(pg) << 8) | cb.max(pb);
4253                                *dst = outp;
4254                                *prev = outp;
4255                            }
4256                        }
4257                        if let Some(win) = g.window.as_mut() {
4258                            let _b = std::time::Instant::now();
4259                            win.update_with_buffer(&g.buffer, w, h)
4260                                .map_err(|e| EvalErr::from(format!("present error: {e}")))?;
4261                            ling_phase_add(phase::BLIT, _b.elapsed().as_nanos());
4262                        }
4263                    }
4264                    // Read mouse AFTER update_with_buffer so events are processed.
4265                    let mouse_pos = {
4266                        let gfx = self.gfx.borrow();
4267                        gfx.window
4268                            .as_ref()
4269                            .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
4270                    };
4271                    let mut gfx = self.gfx.borrow_mut();
4272                    if gfx.mouse_captured {
4273                        let w = gfx.width as f32;
4274                        let h = gfx.height as f32;
4275                        if let Some((mx, my)) = mouse_pos {
4276                            if gfx.last_mx.is_nan() {
4277                                gfx.mouse_dx = 0.0;
4278                                gfx.mouse_dy = 0.0;
4279                                gfx.last_mx = mx;
4280                                gfx.last_my = my;
4281                            } else {
4282                                gfx.mouse_dx = mx - gfx.last_mx;
4283                                gfx.mouse_dy = my - gfx.last_my;
4284                                // Wrap the cursor at every edge (L/R/U/D) → infinite look
4285                                // on both axes, and the cursor is NOT trapped (alt-tab works).
4286                                let margin = 6.0;
4287                                let (mut nx, mut ny, mut warp) = (mx, my, false);
4288                                if mx < margin {
4289                                    nx = w - margin - 2.0;
4290                                    warp = true;
4291                                } else if mx > w - margin {
4292                                    nx = margin + 2.0;
4293                                    warp = true;
4294                                }
4295                                if my < margin {
4296                                    ny = h - margin - 2.0;
4297                                    warp = true;
4298                                } else if my > h - margin {
4299                                    ny = margin + 2.0;
4300                                    warp = true;
4301                                }
4302                                if warp {
4303                                    #[cfg(windows)]
4304                                    unsafe {
4305                                        #[repr(C)]
4306                                        struct RECT {
4307                                            left: i32,
4308                                            top: i32,
4309                                            right: i32,
4310                                            bottom: i32,
4311                                        }
4312                                        extern "system" {
4313                                            fn GetForegroundWindow() -> isize;
4314                                            fn GetWindowRect(hwnd: isize, lpRect: *mut RECT)
4315                                                -> i32;
4316                                            fn SetCursorPos(x: i32, y: i32) -> i32;
4317                                        }
4318                                        let hwnd = GetForegroundWindow();
4319                                        let mut rect =
4320                                            RECT { left: 0, top: 0, right: 0, bottom: 0 };
4321                                        if GetWindowRect(hwnd, &mut rect) != 0 {
4322                                            SetCursorPos(
4323                                                rect.left + nx as i32,
4324                                                rect.top + ny as i32,
4325                                            );
4326                                        }
4327                                    }
4328                                    gfx.last_mx = nx;
4329                                    gfx.last_my = ny;
4330                                } else {
4331                                    gfx.last_mx = mx;
4332                                    gfx.last_my = my;
4333                                }
4334                            }
4335                        } else {
4336                            gfx.mouse_dx = 0.0;
4337                            gfx.mouse_dy = 0.0;
4338                        }
4339                    } else if let Some((mx, my)) = mouse_pos {
4340                        if gfx.last_mx.is_nan() {
4341                            gfx.mouse_dx = 0.0;
4342                            gfx.mouse_dy = 0.0;
4343                        } else {
4344                            gfx.mouse_dx = mx - gfx.last_mx;
4345                            gfx.mouse_dy = my - gfx.last_my;
4346                        }
4347                        gfx.last_mx = mx;
4348                        gfx.last_my = my;
4349                    } else {
4350                        gfx.mouse_dx = 0.0;
4351                        gfx.mouse_dy = 0.0;
4352                    }
4353
4354                    // Alt-tab support: minifb has no WM_KILLFOCUS handler on Windows,
4355                    // so a key/button released while another window was focused can
4356                    // still read as "down" for one stale frame right after the user
4357                    // alt-tabs back. Detect the unfocused→focused transition and
4358                    // swallow raw input for a short grace window afterward instead of
4359                    // letting a phantom held key jerk the camera. See key_down /
4360                    // mouse_down* below (they early-out on gfx.input_suppressed()).
4361                    let is_active = gfx.window.as_mut().map(|w| w.is_active()).unwrap_or(true);
4362                    if is_active && !gfx.was_active {
4363                        gfx.focus_grace_frames = 5;
4364                        // Regained focus: restore HWND_TOPMOST so the borderless-
4365                        // fullscreen window covers the taskbar again.
4366                        #[cfg(windows)]
4367                        if gfx.topmost_window {
4368                            if let Some(w) = gfx.window.as_ref() {
4369                                set_window_topmost(w.get_window_handle() as isize, true);
4370                            }
4371                        }
4372                    } else if !is_active && gfx.was_active {
4373                        // Lost focus (alt-tab): drop topmost so the game stops
4374                        // covering whatever window the user just switched to —
4375                        // otherwise a topmost borderless window visually "wins"
4376                        // even though it's no longer focused, making alt-tab
4377                        // look broken.
4378                        #[cfg(windows)]
4379                        if gfx.topmost_window {
4380                            if let Some(w) = gfx.window.as_ref() {
4381                                set_window_topmost(w.get_window_handle() as isize, false);
4382                            }
4383                        }
4384                    }
4385                    gfx.was_active = is_active;
4386                    if gfx.focus_grace_frames > 0 {
4387                        gfx.focus_grace_frames -= 1;
4388                    }
4389                }
4390                #[cfg(target_arch = "wasm32")]
4391                {
4392                    {
4393                        // Software-render everything (3-D depth queue + 2-D vtex/ui that
4394                        // already wrote into the buffer) into the framebuffer, exactly
4395                        // like native, then upload that buffer to the canvas in one blit.
4396                        let mut gfx = self.gfx.borrow_mut();
4397                        let w = gfx.width;
4398                        let h = gfx.height;
4399                        if gfx.buffer.len() != w * h {
4400                            gfx.buffer.resize(w * h, 0);
4401                        }
4402                        if !gfx.depth_queue.is_empty() {
4403                            let dt = gfx.depth_test;
4404                            let reset_z = gfx.zbuf_needs_clear;
4405                            let aa = gfx.antialias;
4406                            let queue = std::mem::take(&mut gfx.depth_queue);
4407                            {
4408                                let g = &mut *gfx;
4409                                let z = if dt { Some(&mut g.depth_buf) } else { None };
4410                                queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
4411                            }
4412                            gfx.zbuf_needs_clear = false;
4413                        }
4414                        if !gfx.post_done {
4415                            gfx.toon_post_process();
4416                        }
4417                        gfx.post_done = false;
4418                        crate::gfx::webgl::blit_rgb(&gfx.buffer, w, h);
4419                    }
4420                    self.wasm_pace_frame();
4421                }
4422                // Update the click-edge latch for interactive UI widgets —
4423                // using the PRE-update_with_buffer snapshot captured at the
4424                // top of this function (see the comment there for why).
4425                #[cfg(not(target_arch = "wasm32"))]
4426                {
4427                    self.mouse_was_down = pre_update_mouse_down;
4428                }
4429                // Increment frame counter
4430                self.frame_num += 1;
4431                return Ok(Value::Unit);
4432            },
4433
4434            // ── เปิดหน้าต่างเต็มจอ(title) — true native-res fullscreen window ──
4435            "เปิดหน้าต่างเต็มจอ"
4436            | "open_fullscreen"
4437            | "fullscreen"
4438            | "全屏"
4439            | "全画面"
4440            | "전체화면" => {
4441                // In WASM the canvas defines the viewport; use its current size
4442                // as the default so the projection matches what's actually visible.
4443                #[cfg(target_arch = "wasm32")]
4444                let (default_w, default_h) = {
4445                    let (cw, ch) = crate::gfx::webgl::canvas_size();
4446                    (cw as f64, ch as f64)
4447                };
4448                // On native: query the actual primary monitor resolution.
4449                #[cfg(all(not(target_arch = "wasm32"), windows))]
4450                let (default_w, default_h) = unsafe {
4451                    extern "system" {
4452                        fn GetSystemMetrics(nIndex: i32) -> i32;
4453                    }
4454                    (GetSystemMetrics(0) as f64, GetSystemMetrics(1) as f64)
4455                };
4456                #[cfg(all(not(target_arch = "wasm32"), not(windows)))]
4457                let (default_w, default_h) = native_screen_size();
4458
4459                let w = args
4460                    .get(1)
4461                    .map(|v| self.to_number(v).unwrap_or(default_w) as usize)
4462                    .unwrap_or(default_w as usize);
4463                let h = args
4464                    .get(2)
4465                    .map(|v| self.to_number(v).unwrap_or(default_h) as usize)
4466                    .unwrap_or(default_h as usize);
4467                #[cfg(not(target_arch = "wasm32"))]
4468                {
4469                    let title = args
4470                        .first()
4471                        .map(|v| v.to_string())
4472                        .unwrap_or_else(|| "Ling".into());
4473                    let mut gfx = self.gfx.borrow_mut();
4474                    let mut win = minifb::Window::new(
4475                        &title,
4476                        w,
4477                        h,
4478                        minifb::WindowOptions {
4479                            borderless: true,
4480                            title: false,
4481                            resize: false,
4482                            topmost: true,
4483                            scale: minifb::Scale::X1,
4484                            ..Default::default()
4485                        },
4486                    )
4487                    .map_err(|e| EvalErr::from(format!("cannot open fullscreen: {e}")))?;
4488                    apply_frame_pacing(&mut win, gfx.vsync);
4489                    // Grab the native handle *before* moving the window into gfx.
4490                    #[cfg(windows)]
4491                    let hwnd = win.get_window_handle() as isize;
4492                    gfx.buffer = vec![0u32; w * h];
4493                    gfx.width = w;
4494                    gfx.height = h;
4495                    gfx.window = Some(win);
4496                    gfx.topmost_window = true;
4497                    #[cfg(windows)]
4498                    {
4499                        gfx.hwnd = hwnd;
4500                    }
4501                    gfx.sync_projection();
4502                    // Strip all chrome and cover the full screen, above the taskbar.
4503                    #[cfg(windows)]
4504                    make_borderless_fullscreen(hwnd, w as i32, h as i32);
4505                    hide_console_window();
4506                    // hide_console_window() can itself reassign the OS foreground
4507                    // window (hiding whatever previously had it, e.g. the terminal
4508                    // that ran `ling run`, can hand focus to something other than
4509                    // this window) — so the focus claim has to be the LAST word,
4510                    // after every other window-visibility change, not just inside
4511                    // make_borderless_fullscreen further up.
4512                    #[cfg(windows)]
4513                    force_window_focus(hwnd);
4514                }
4515                #[cfg(target_arch = "wasm32")]
4516                {
4517                    let mut gfx = self.gfx.borrow_mut();
4518                    gfx.width = w;
4519                    gfx.height = h;
4520                    gfx.buffer.resize(w * h, 0); // keep the CPU framebuffer in sync
4521                    gfx.sync_projection();
4522                    crate::gfx::webgl::resize(w as u32, h as u32);
4523                }
4524                return Ok(Value::Unit);
4525            },
4526
4527            // ── ความกว้าง() / ความสูง() — current framebuffer size ──
4528            "get_width" | "ความกว้าง" | "宽" | "幅取得" | "너비" => {
4529                return Ok(Value::Number(self.gfx.borrow().width as f64));
4530            },
4531            "get_height" | "ความสูง" | "高" | "高取得" | "높이" => {
4532                return Ok(Value::Number(self.gfx.borrow().height as f64));
4533            },
4534
4535            // ── monitor detection: physical display, not the framebuffer ──────
4536            // monitor_width() → primary-monitor pixel width
4537            "monitor_width" | "screen_width" | "屏宽" | "画面幅" | "화면너비" | "ความกว้างจอ" =>
4538            {
4539                return Ok(Value::Number(monitor_info().0 as f64));
4540            },
4541            // monitor_height() → primary-monitor pixel height
4542            "monitor_height" | "screen_height" | "屏高" | "画面高" | "화면높이" | "ความสูงจอ" =>
4543            {
4544                return Ok(Value::Number(monitor_info().1 as f64));
4545            },
4546            // monitor_refresh() → refresh rate in Hz (a.k.a. the monitor framerate)
4547            "monitor_refresh"
4548            | "monitor_hz"
4549            | "monitor_fps"
4550            | "refresh_rate"
4551            | "刷新率"
4552            | "リフレッシュレート"
4553            | "주사율"
4554            | "อัตรารีเฟรช" => {
4555                return Ok(Value::Number(monitor_info().2 as f64));
4556            },
4557            // monitor_info() → [width, height, refresh_hz]
4558            "monitor_info" | "screen_info" | "屏幕信息" | "画面情報" | "화면정보" | "ข้อมูลจอ" =>
4559            {
4560                let (w, h, hz) = monitor_info();
4561                return Ok(Value::List(Rc::new(vec![
4562                    Value::Number(w as f64),
4563                    Value::Number(h as f64),
4564                    Value::Number(hz as f64),
4565                ])));
4566            },
4567            // set_fps(n) → cap the render loop at n frames per second
4568            "set_fps"
4569            | "set_target_fps"
4570            | "target_fps"
4571            | "设帧率"
4572            | "フレームレート設定"
4573            | "프레임설정"
4574            | "ตั้งเฟรมเรต" => {
4575                #[cfg(not(target_arch = "wasm32"))]
4576                {
4577                    let fps = self.arg_num(&args, 0, 60.0)?.max(1.0) as usize;
4578                    let mut gfx = self.gfx.borrow_mut();
4579                    if let Some(win) = gfx.window.as_mut() {
4580                        win.set_target_fps(fps);
4581                    }
4582                }
4583                #[cfg(target_arch = "wasm32")]
4584                {
4585                    self.wasm_target_fps = self.arg_num(&args, 0, 60.0)?.max(1.0);
4586                    self.wasm_next_present_ms = 0.0;
4587                }
4588                return Ok(Value::Unit);
4589            },
4590
4591            // set_vsync(on) → pace the window to the monitor's refresh rate.
4592            // Frame-rate pacing (minifb has no swap-interval), not tear-free
4593            // vsync; `LING_FPS_CAP` and an explicit `set_fps` call still win.
4594            "set_vsync" | "vsync" | "垂直同步" | "垂直同期" | "수직동기" | "ตั้งวีซิงก์" =>
4595            {
4596                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
4597                #[cfg(not(target_arch = "wasm32"))]
4598                {
4599                    let mut gfx = self.gfx.borrow_mut();
4600                    gfx.vsync = on;
4601                    if let Some(win) = gfx.window.as_mut() {
4602                        apply_frame_pacing(win, on);
4603                    }
4604                }
4605                #[cfg(target_arch = "wasm32")]
4606                {
4607                    self.wasm_target_fps = if on { monitor_info().2 as f64 } else { 240.0 };
4608                    self.wasm_next_present_ms = 0.0;
4609                }
4610                return Ok(Value::Unit);
4611            },
4612
4613            // ── หน้าต่างเปิดอยู่() → bool — is the window still open? ──
4614            "หน้าต่างเปิดอยู่"
4615            | "window_is_open"
4616            | "gfx_is_open"
4617            | "is_open"
4618            | "窗开"
4619            | "開いている"
4620            | "창열림" => {
4621                #[cfg(not(target_arch = "wasm32"))]
4622                {
4623                    let gfx = self.gfx.borrow();
4624                    if gfx.want_quit {
4625                        return Ok(Value::Bool(false));
4626                    }
4627                    // Escape-to-quit needs the same GetAsyncKeyState fallback
4628                    // as key_down/key_pressed/text_poll (see those) — raw
4629                    // w.is_key_down(Escape) is WM_KEYDOWN-based and silently
4630                    // never fires if this topmost window didn't actually win
4631                    // real Win32 keyboard focus.
4632                    #[cfg(windows)]
4633                    let escape_down = if gfx.topmost_window {
4634                        window_is_foreground(gfx.hwnd) && os_key_down(0x1B) // VK_ESCAPE
4635                    } else {
4636                        gfx.window
4637                            .as_ref()
4638                            .map(|w| w.is_key_down(minifb::Key::Escape))
4639                            .unwrap_or(false)
4640                    };
4641                    #[cfg(not(windows))]
4642                    let escape_down = gfx
4643                        .window
4644                        .as_ref()
4645                        .map(|w| w.is_key_down(minifb::Key::Escape))
4646                        .unwrap_or(false);
4647                    let open = gfx.window.as_ref().map(|w| w.is_open()).unwrap_or(false)
4648                        && !escape_down;
4649                    return Ok(Value::Bool(open));
4650                }
4651                #[cfg(target_arch = "wasm32")]
4652                return Ok(Value::Bool(true));
4653            },
4654
4655            // quit() — close the window the same way Escape does, for a
4656            // script-drawn UI element (an exit button) to call.
4657            "quit" | "exit_game" | "close_window" => {
4658                #[cfg(not(target_arch = "wasm32"))]
4659                {
4660                    self.gfx.borrow_mut().want_quit = true;
4661                }
4662                return Ok(Value::Unit);
4663            },
4664
4665            // ── key_down(name) → bool — is a key held? ──
4666            "key_down" | "กดค้าง" | "按键" | "キー押す" | "키누름" => {
4667                #[cfg(not(target_arch = "wasm32"))]
4668                {
4669                    let name = self.arg_str(&args, 0, "");
4670                    let mut gfx = self.gfx.borrow_mut();
4671                    // The borderless-fullscreen/topmost window can be
4672                    // visually in front without ever winning real Win32
4673                    // keyboard focus (Windows' foreground-lock) — minifb's
4674                    // is_key_down is populated from WM_KEYDOWN, which then
4675                    // never arrives. GetAsyncKeyState reads the OS key-state
4676                    // table directly and doesn't need focus, so use it
4677                    // whenever this is that window (see force_window_focus).
4678                    #[cfg(windows)]
4679                    if gfx.topmost_window {
4680                        if !window_is_foreground(gfx.hwnd) {
4681                            return Ok(Value::Bool(false));
4682                        }
4683                        return Ok(Value::Bool(
4684                            str_to_vk(&name).map(os_key_down).unwrap_or(false),
4685                        ));
4686                    }
4687                    if gfx.input_suppressed() {
4688                        return Ok(Value::Bool(false));
4689                    }
4690                    let down = gfx
4691                        .window
4692                        .as_ref()
4693                        .and_then(|w| str_to_minifb_key(&name).map(|k| w.is_key_down(k)))
4694                        .unwrap_or(false);
4695                    return Ok(Value::Bool(down));
4696                }
4697                #[cfg(target_arch = "wasm32")]
4698                {
4699                    let name = self.arg_str(&args, 0, "");
4700                    return Ok(Value::Bool(crate::gfx::wasm_is_key_down(&name)));
4701                }
4702            },
4703
4704            // ── key_pressed(name) → bool — was a key pressed this frame? ──
4705            "key_pressed" | "กดปุ่ม" | "键按" | "キー押した" | "키눌림" => {
4706                #[cfg(not(target_arch = "wasm32"))]
4707                {
4708                    let name = self.arg_str(&args, 0, "");
4709                    let pressed = {
4710                        let mut gfx = self.gfx.borrow_mut();
4711                        #[cfg(windows)]
4712                        let topmost = gfx.topmost_window;
4713                        #[cfg(not(windows))]
4714                        let topmost = false;
4715                        if topmost {
4716                            #[cfg(windows)]
4717                            {
4718                                if !window_is_foreground(gfx.hwnd) {
4719                                    false
4720                                } else {
4721                                    match str_to_vk(&name) {
4722                                        Some(vk) => {
4723                                            let idx = (vk as usize) & 0xFF;
4724                                            let down = os_key_down(vk);
4725                                            let was = gfx.raw_keys_prev[idx];
4726                                            gfx.raw_keys_prev[idx] = down;
4727                                            down && !was
4728                                        },
4729                                        None => false,
4730                                    }
4731                                }
4732                            }
4733                            #[cfg(not(windows))]
4734                            {
4735                                false
4736                            }
4737                        } else if gfx.input_suppressed() {
4738                            false
4739                        } else {
4740                            gfx.window
4741                                .as_ref()
4742                                .and_then(|w| {
4743                                    str_to_minifb_key(&name)
4744                                        .map(|k| w.is_key_pressed(k, minifb::KeyRepeat::No))
4745                                })
4746                                .unwrap_or(false)
4747                        }
4748                    };
4749                    // gamepad Start behaves like Enter everywhere
4750                    let pressed =
4751                        pressed || ((name == "enter" || name == "return") && gamepad::start_edge());
4752                    return Ok(Value::Bool(pressed));
4753                }
4754                #[cfg(target_arch = "wasm32")]
4755                {
4756                    let name = self.arg_str(&args, 0, "");
4757                    let pressed = crate::gfx::wasm_is_key_pressed(&name);
4758                    return Ok(Value::Bool(pressed));
4759                }
4760            },
4761
4762            // ── mouse_dx() / mouse_dy() → f64 — delta since last frame ──
4763            "mouse_dx" | "เมาส์X" | "鼠ΔX" | "マウスΔX" | "마우스ΔX" => {
4764                #[cfg(not(target_arch = "wasm32"))]
4765                return Ok(Value::Number(self.gfx.borrow().mouse_dx as f64));
4766                #[cfg(target_arch = "wasm32")]
4767                return Ok(Value::Number(crate::gfx::wasm_mouse_dx() as f64));
4768            },
4769            // ── mouse_scroll() → f64 — vertical scroll-wheel delta this frame ──
4770            #[cfg(not(target_arch = "wasm32"))]
4771            "mouse_scroll" | "ล้อเมาส์" | "滚轮" | "ホイール" | "스크롤" =>
4772            {
4773                let gfx = self.gfx.borrow();
4774                let s = gfx
4775                    .window
4776                    .as_ref()
4777                    .and_then(|w| w.get_scroll_wheel())
4778                    .map(|(_, y)| y as f64)
4779                    .unwrap_or(0.0);
4780                return Ok(Value::Number(s));
4781            },
4782            #[cfg(target_arch = "wasm32")]
4783            "mouse_scroll" | "ล้อเมาส์" | "滚轮" | "ホイール" | "스크롤" =>
4784            {
4785                return Ok(Value::Number(0.0));
4786            },
4787            "mouse_dy" | "เมาส์Y" | "鼠ΔY" | "マウスΔY" | "마우스ΔY" => {
4788                #[cfg(not(target_arch = "wasm32"))]
4789                return Ok(Value::Number(self.gfx.borrow().mouse_dy as f64));
4790                #[cfg(target_arch = "wasm32")]
4791                return Ok(Value::Number(crate::gfx::wasm_mouse_dy() as f64));
4792            },
4793
4794            // ── Gamepad / joystick input (ling-input "Sensorium" + gilrs) ──
4795            // pad_poll() → number — advance input one frame; returns # connected pads.
4796            "pad_poll" | "手柄轮询" | "パッド更新" | "패드폴링" | "อัปเดตแพด" =>
4797            {
4798                #[cfg(not(target_arch = "wasm32"))]
4799                return Ok(Value::Number(self.pad_poll() as f64));
4800                #[cfg(target_arch = "wasm32")]
4801                return Ok(Value::Number(input_web::poll() as f64));
4802            },
4803            // pad_count() → number — connected gamepads.
4804            "pad_count" | "手柄数" | "パッド数" | "패드수" | "จำนวนแพด" =>
4805            {
4806                #[cfg(not(target_arch = "wasm32"))]
4807                {
4808                    let inp = self.input.borrow();
4809                    let n = inp.as_ref().map_or(0, |s| s.sensorium.devices.count());
4810                    return Ok(Value::Number(n as f64));
4811                }
4812                #[cfg(target_arch = "wasm32")]
4813                return Ok(Value::Number(input_web::count() as f64));
4814            },
4815            // pad_connected(i) → bool.
4816            "pad_connected" | "手柄连接" | "パッド接続" | "패드연결" | "แพดเชื่อม" =>
4817            {
4818                #[cfg(not(target_arch = "wasm32"))]
4819                {
4820                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4821                    let inp = self.input.borrow();
4822                    let c = inp
4823                        .as_ref()
4824                        .is_some_and(|s| s.sensorium.devices.for_player(i as u8).is_some());
4825                    return Ok(Value::Bool(c));
4826                }
4827                #[cfg(target_arch = "wasm32")]
4828                {
4829                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4830                    return Ok(Value::Bool(input_web::is_connected(i)));
4831                }
4832            },
4833            // pad_button(i, name) → bool — is the button held?
4834            "pad_button" | "手柄按键" | "パッドボタン" | "패드버튼" | "ปุ่มแพด" =>
4835            {
4836                #[cfg(not(target_arch = "wasm32"))]
4837                {
4838                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4839                    let name = self.arg_str(&args, 1, "");
4840                    let down = parse_pad_button(&name)
4841                        .is_some_and(|b| self.with_pad(i, false, |p| p.is_down(b)));
4842                    return Ok(Value::Bool(down));
4843                }
4844                #[cfg(target_arch = "wasm32")]
4845                {
4846                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4847                    let name = self.arg_str(&args, 1, "");
4848                    return Ok(Value::Bool(input_web::button_down(i, &name)));
4849                }
4850            },
4851            // pad_pressed(i, name) → bool — pressed this frame?
4852            // On WASM we only have the current snapshot, so treat as button_down.
4853            "pad_pressed" | "手柄按下" | "パッド押下" | "패드눌림" | "แพดกด" =>
4854            {
4855                #[cfg(not(target_arch = "wasm32"))]
4856                {
4857                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4858                    let name = self.arg_str(&args, 1, "");
4859                    let p = parse_pad_button(&name)
4860                        .is_some_and(|b| self.with_pad(i, false, |g| g.just_pressed(b)));
4861                    return Ok(Value::Bool(p));
4862                }
4863                #[cfg(target_arch = "wasm32")]
4864                {
4865                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4866                    let name = self.arg_str(&args, 1, "");
4867                    return Ok(Value::Bool(input_web::button_down(i, &name)));
4868                }
4869            },
4870            // pad_lx(i)/pad_ly(i)/pad_rx(i)/pad_ry(i) → number — stick axes (−1..=1).
4871            "pad_lx" | "手柄左X" | "パッド左X" | "패드왼X" | "แพดซ้ายX" => {
4872                #[cfg(not(target_arch = "wasm32"))]
4873                {
4874                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4875                    return Ok(Value::Number(
4876                        self.with_pad(i, 0.0, |p| p.left_stick.x as f64),
4877                    ));
4878                }
4879                #[cfg(target_arch = "wasm32")]
4880                {
4881                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4882                    return Ok(Value::Number(input_web::axis_lx(i) as f64));
4883                }
4884            },
4885            "pad_ly" | "手柄左Y" | "パッド左Y" | "패드왼Y" | "แพดซ้ายY" => {
4886                #[cfg(not(target_arch = "wasm32"))]
4887                {
4888                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4889                    return Ok(Value::Number(
4890                        self.with_pad(i, 0.0, |p| p.left_stick.y as f64),
4891                    ));
4892                }
4893                #[cfg(target_arch = "wasm32")]
4894                {
4895                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4896                    return Ok(Value::Number(input_web::axis_ly(i) as f64));
4897                }
4898            },
4899            "pad_rx" | "手柄右X" | "パッド右X" | "패드오X" | "แพดขวาX" => {
4900                #[cfg(not(target_arch = "wasm32"))]
4901                {
4902                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4903                    return Ok(Value::Number(
4904                        self.with_pad(i, 0.0, |p| p.right_stick.x as f64),
4905                    ));
4906                }
4907                #[cfg(target_arch = "wasm32")]
4908                {
4909                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4910                    return Ok(Value::Number(input_web::axis_rx(i) as f64));
4911                }
4912            },
4913            "pad_ry" | "手柄右Y" | "パッド右Y" | "패드오Y" | "แพดขวาY" => {
4914                #[cfg(not(target_arch = "wasm32"))]
4915                {
4916                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4917                    return Ok(Value::Number(
4918                        self.with_pad(i, 0.0, |p| p.right_stick.y as f64),
4919                    ));
4920                }
4921                #[cfg(target_arch = "wasm32")]
4922                {
4923                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4924                    return Ok(Value::Number(input_web::axis_ry(i) as f64));
4925                }
4926            },
4927            // pad_lt(i)/pad_rt(i) → number — analog triggers (0..=1).
4928            "pad_lt" | "手柄左扳机" | "パッド左トリガー" | "패드왼트리거" | "ไกแพดซ้าย" =>
4929            {
4930                #[cfg(not(target_arch = "wasm32"))]
4931                {
4932                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4933                    return Ok(Value::Number(
4934                        self.with_pad(i, 0.0, |p| p.left_trigger as f64),
4935                    ));
4936                }
4937                #[cfg(target_arch = "wasm32")]
4938                {
4939                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4940                    return Ok(Value::Number(input_web::trigger_lt(i) as f64));
4941                }
4942            },
4943            "pad_rt" | "手柄右扳机" | "パッド右トリガー" | "패드오트리거" | "ไกแพดขวา" =>
4944            {
4945                #[cfg(not(target_arch = "wasm32"))]
4946                {
4947                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4948                    return Ok(Value::Number(
4949                        self.with_pad(i, 0.0, |p| p.right_trigger as f64),
4950                    ));
4951                }
4952                #[cfg(target_arch = "wasm32")]
4953                {
4954                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4955                    return Ok(Value::Number(input_web::trigger_rt(i) as f64));
4956                }
4957            },
4958            // pad_rumble(i, lo, hi) → unit — set rumble motor amplitudes (0..=1).
4959            "pad_rumble" | "手柄震动" | "パッド振動" | "패드진동" | "แพดสั่น" =>
4960            {
4961                #[cfg(not(target_arch = "wasm32"))]
4962                {
4963                    use ling_input::backend::InputBackend;
4964                    let i = self.arg_num(&args, 0, 0.0)? as usize;
4965                    let lo = self.arg_num(&args, 1, 0.0)? as f32;
4966                    let hi = self.arg_num(&args, 2, lo as f64)? as f32;
4967                    let mut inp = self.input.borrow_mut();
4968                    if let Some(s) = inp.as_mut() {
4969                        if let Some(dev) = s.sensorium.devices.for_player(i as u8).map(|d| d.id) {
4970                            s.backend.set_rumble(
4971                                dev,
4972                                ling_input::Rumble { low: lo, high: hi, ..Default::default() },
4973                            );
4974                        }
4975                    }
4976                    return Ok(Value::Unit);
4977                }
4978                #[cfg(target_arch = "wasm32")]
4979                return Ok(Value::Unit);
4980            },
4981
4982            // ── set_camera_pos(x, y, z) — move camera to world position ──
4983            "set_camera_pos" | "ตั้งตำแหน่งกล้อง" | "镜坐标" | "カメラ座標" | "카메라좌표" =>
4984            {
4985                let x = self.arg_num(&args, 0, 0.0)? as f32;
4986                let y = self.arg_num(&args, 1, 0.0)? as f32;
4987                let z = self.arg_num(&args, 2, 0.0)? as f32;
4988                {
4989                    let mut gfx = self.gfx.borrow_mut();
4990                    gfx.camera.tx = x;
4991                    gfx.camera.ty = y;
4992                    gfx.camera.tz = z;
4993                }
4994                #[cfg(not(target_arch = "wasm32"))]
4995                if let Some(audio) = &self.audio {
4996                    audio.set_listener_pos(x, y, z);
4997                }
4998                return Ok(Value::Unit);
4999            },
5000
5001            // ── move_camera(dx, dy, dz) — translate camera by delta ──
5002            "move_camera" => {
5003                let dx = self.arg_num(&args, 0, 0.0)? as f32;
5004                let dy = self.arg_num(&args, 1, 0.0)? as f32;
5005                let dz = self.arg_num(&args, 2, 0.0)? as f32;
5006                let mut gfx = self.gfx.borrow_mut();
5007                gfx.camera.tx += dx;
5008                gfx.camera.ty += dy;
5009                gfx.camera.tz += dz;
5010                return Ok(Value::Unit);
5011            },
5012
5013            // ── set_zdist(d) — set perspective z-offset (field-of-view taper) ──
5014            "set_zdist" | "ตั้งระยะห่าง" | "镜距" | "Z距離設定" | "Z거리설정" =>
5015            {
5016                let d = self.arg_num(&args, 0, 5.0)? as f32;
5017                self.gfx.borrow_mut().camera.zdist = d;
5018                return Ok(Value::Unit);
5019            },
5020
5021            // ── capture_mouse() — hide cursor and warp to centre each frame ──
5022            "capture_mouse" | "จับเมาส์" | "捕鼠" | "マウス捕捉" | "마우스잡기" =>
5023            {
5024                #[cfg(not(target_arch = "wasm32"))]
5025                {
5026                    let mut gfx = self.gfx.borrow_mut();
5027                    gfx.mouse_captured = true;
5028                    gfx.last_mx = f32::NAN;
5029                    if let Some(win) = gfx.window.as_mut() {
5030                        win.set_cursor_visibility(false);
5031                    }
5032                }
5033                return Ok(Value::Unit);
5034            },
5035
5036            // ── release_mouse() — restore cursor and remove clip region ──
5037            "release_mouse" => {
5038                #[cfg(not(target_arch = "wasm32"))]
5039                {
5040                    let mut gfx = self.gfx.borrow_mut();
5041                    gfx.mouse_captured = false;
5042                    gfx.last_mx = f32::NAN;
5043                    if let Some(win) = gfx.window.as_mut() {
5044                        win.set_cursor_visibility(true);
5045                    }
5046                    #[cfg(windows)]
5047                    unsafe {
5048                        // Null releases the clip; reuse the RECT-typed declaration above.
5049                        extern "system" {
5050                            fn ClipCursor(lpRect: *const std::ffi::c_void) -> i32;
5051                        }
5052                        ClipCursor(std::ptr::null());
5053                    }
5054                }
5055                return Ok(Value::Unit);
5056            },
5057
5058            // ── cursor_hide() / cursor_show() — just the OS cursor's visibility,
5059            // no warp-to-centre or clip region (unlike capture_mouse/release_mouse,
5060            // which are for FPS-style look-around). For point-and-click play where
5061            // the cursor still needs to move freely and mouse_x()/mouse_y() still
5062            // need to track real position, just hide the system pointer glyph.
5063            "cursor_hide" => {
5064                #[cfg(not(target_arch = "wasm32"))]
5065                if let Some(win) = self.gfx.borrow_mut().window.as_mut() {
5066                    win.set_cursor_visibility(false);
5067                }
5068                return Ok(Value::Unit);
5069            },
5070            "cursor_show" => {
5071                #[cfg(not(target_arch = "wasm32"))]
5072                if let Some(win) = self.gfx.borrow_mut().window.as_mut() {
5073                    win.set_cursor_visibility(true);
5074                }
5075                return Ok(Value::Unit);
5076            },
5077
5078            // ══════════════════════════════════════════════════════════════════
5079            // 3-D / 4-D DRAWING — camera, lights, depth-sorted geometry
5080            // ══════════════════════════════════════════════════════════════════
5081
5082            // ── set_camera(cry, sry, crx, srx) — store precomputed camera trig ──
5083            // Call once per frame after computing cos/sin of your rotation angles.
5084            "set_camera" | "ตั้งกล้อง" | "设镜" | "设置摄像机" | "カメラ設定" | "카메라설정" =>
5085            {
5086                let cry = self.arg_num(&args, 0, 1.0)? as f32;
5087                let sry = self.arg_num(&args, 1, 0.0)? as f32;
5088                let crx = self.arg_num(&args, 2, 1.0)? as f32;
5089                let srx = self.arg_num(&args, 3, 0.0)? as f32;
5090                let mut gfx = self.gfx.borrow_mut();
5091                gfx.camera.cry = cry;
5092                gfx.camera.sry = sry;
5093                gfx.camera.crx = crx;
5094                gfx.camera.srx = srx;
5095                return Ok(Value::Unit);
5096            },
5097
5098            // ── set_projection(cx, cy, focal, zdist) — override projection params ──
5099            // Automatically set when the window opens; override only if needed.
5100            "set_projection" | "ตั้งโปรเจกชัน" | "投影" | "投影設定" | "투영설정" =>
5101            {
5102                let cx = self.arg_num(&args, 0, 960.0)? as f32;
5103                let cy = self.arg_num(&args, 1, 540.0)? as f32;
5104                let focal = self.arg_num(&args, 2, 1080.0)? as f32;
5105                let zdist = self.arg_num(&args, 3, 5.0)? as f32;
5106                let mut gfx = self.gfx.borrow_mut();
5107                gfx.camera.cx = cx;
5108                gfx.camera.cy = cy;
5109                gfx.camera.focal = focal;
5110                gfx.camera.zdist = zdist;
5111                return Ok(Value::Unit);
5112            },
5113
5114            // ── mesh_load(path) → handle · loads a glb/gltf (skeleton + skin + animation) ──
5115            "gltf_load" => {
5116                let path = self.arg_str(&args, 0, "");
5117                match ling_physics::gltf::GltfModel::load(&path) {
5118                    Ok(m) => {
5119                        self.gltf_models.borrow_mut().push(m);
5120                        let h = self.gltf_models.borrow().len() - 1;
5121                        return Ok(Value::Number(h as f64));
5122                    }
5123                    Err(e) => {
5124                        eprintln!("mesh_load failed ({path}): {e}");
5125                        return Ok(Value::Number(-1.0));
5126                    }
5127                }
5128            },
5129            // mesh_anim_count(handle) → number of animation clips
5130            "gltf_anim_count" => {
5131                let h = self.arg_num(&args, 0, -1.0)? as i64;
5132                let n = self
5133                    .gltf_models
5134                    .borrow()
5135                    .get(h as usize)
5136                    .map(|m| m.animations.len())
5137                    .unwrap_or(0);
5138                return Ok(Value::Number(n as f64));
5139            },
5140            // mesh_anim_name(handle, i) → clip name
5141            "gltf_anim_name" => {
5142                let h = self.arg_num(&args, 0, -1.0)? as i64;
5143                let i = self.arg_num(&args, 1, 0.0)? as usize;
5144                let s = self
5145                    .gltf_models
5146                    .borrow()
5147                    .get(h as usize)
5148                    .and_then(|m| m.animations.get(i))
5149                    .map(|a| a.name.clone())
5150                    .unwrap_or_default();
5151                return Ok(Value::Str(s));
5152            },
5153            // mesh_anim_dur(handle, i) → clip duration (seconds)
5154            "gltf_anim_dur" => {
5155                let h = self.arg_num(&args, 0, -1.0)? as i64;
5156                let i = self.arg_num(&args, 1, 0.0)? as usize;
5157                let d = self
5158                    .gltf_models
5159                    .borrow()
5160                    .get(h as usize)
5161                    .and_then(|m| m.animations.get(i))
5162                    .map(|a| a.duration)
5163                    .unwrap_or(0.0);
5164                return Ok(Value::Number(d as f64));
5165            },
5166            // mesh_tris(handle) → total triangle count (perf sanity check)
5167            "gltf_tris" => {
5168                let h = self.arg_num(&args, 0, -1.0)? as i64;
5169                let n: usize = self
5170                    .gltf_models
5171                    .borrow()
5172                    .get(h as usize)
5173                    .map(|m| m.meshes.iter().map(|mm| mm.indices.len()).sum::<usize>() / 3)
5174                    .unwrap_or(0);
5175                return Ok(Value::Number(n as f64));
5176            },
5177
5178            // gltf_joint_count(handle) → number of skin joints (bones)
5179            "gltf_joint_count" => {
5180                let h = self.arg_num(&args, 0, -1.0)? as i64;
5181                let n = self
5182                    .gltf_models
5183                    .borrow()
5184                    .get(h as usize)
5185                    .and_then(|m| m.skins.first())
5186                    .map(|s| s.joints.len())
5187                    .unwrap_or(0);
5188                return Ok(Value::Number(n as f64));
5189            },
5190            // gltf_joint_name(handle, j) → bone name (its node's name)
5191            "gltf_joint_name" => {
5192                let h = self.arg_num(&args, 0, -1.0)? as i64;
5193                let j = self.arg_num(&args, 1, 0.0)? as usize;
5194                let models = self.gltf_models.borrow();
5195                let s = models
5196                    .get(h as usize)
5197                    .and_then(|m| {
5198                        m.skins
5199                            .first()
5200                            .and_then(|sk| sk.joints.get(j))
5201                            .and_then(|jt| m.nodes.get(jt.node_idx))
5202                            .map(|n| n.name.clone())
5203                    })
5204                    .unwrap_or_default();
5205                return Ok(Value::Str(s));
5206            },
5207
5208            // ── gltf_draw(handle, ox,oy,oz, scale, yaw) — filled render of a loaded model ──
5209            //   glTF is Y-up / -Z-forward; the engine is Y-down, so we flip Y and Z, then
5210            //   yaw about Y, scale, translate. Per-part colour by mesh name. Lit + depth-queued
5211            //   exactly like draw_mesh, so it shares the camera + z-buffer.
5212            "gltf_draw" => {
5213                let hh = self.arg_num(&args, 0, -1.0)? as i64;
5214                let ox = self.arg_num(&args, 1, 0.0)? as f32;
5215                let oy = self.arg_num(&args, 2, 0.0)? as f32;
5216                let oz = self.arg_num(&args, 3, 0.0)? as f32;
5217                let scale = self.arg_num(&args, 4, 1.0)? as f32;
5218                let yaw = self.arg_num(&args, 5, 0.0)? as f32;
5219                let (sy, cyy) = yaw.sin_cos();
5220                let models = self.gltf_models.borrow();
5221                let model = match models.get(hh as usize) {
5222                    Some(m) => m,
5223                    None => return Ok(Value::Unit),
5224                };
5225                let mut gfx = self.gfx.borrow_mut();
5226                let cp = {
5227                    let c = &gfx.camera;
5228                    ling_gpu::CameraParams {
5229                        cry: c.cry, sry: c.sry, crx: c.crx, srx: c.srx,
5230                        cx: c.cx, cy: c.cy, focal: c.focal, zdist: c.zdist,
5231                        tx: c.tx, ty: c.ty, tz: c.tz,
5232                    }
5233                };
5234                let near = -gfx.camera.zdist + 0.02;
5235                let ambient = gfx.ambient;
5236                for mesh in &model.meshes {
5237                    let nlow = mesh.name.to_lowercase();
5238                    let base: u32 = if nlow.contains("hair") {
5239                        0x7a4a28
5240                    } else if nlow.contains("cloth") || nlow.contains("top") {
5241                        0x4a86e0
5242                    } else if nlow.contains("wing") {
5243                        0xe6ecf5
5244                    } else if nlow.contains("star") {
5245                        0xffd24d
5246                    } else {
5247                        0xf2d6b8
5248                    };
5249                    let nv = mesh.verts.len();
5250                    if nv == 0 {
5251                        continue;
5252                    }
5253                    let mut world = vec![0.0f32; nv * 3];
5254                    for (i, v) in mesh.verts.iter().enumerate() {
5255                        let gx = v.pos.x * scale;
5256                        let gy = -v.pos.y * scale;
5257                        let gz = -v.pos.z * scale;
5258                        let rx = gx * cyy + gz * sy;
5259                        let rz = -gx * sy + gz * cyy;
5260                        world[i * 3] = ox + rx;
5261                        world[i * 3 + 1] = oy + gy;
5262                        world[i * 3 + 2] = oz + rz;
5263                    }
5264                    let mut proj = vec![0.0f32; nv * 3];
5265                    ling_gpu::backend().project_points(&world, &cp, &mut proj);
5266                    let idx = &mesh.indices;
5267                    let nt = idx.len() / 3;
5268                    for t in 0..nt {
5269                        let ia = idx[t * 3] as usize;
5270                        let ib = idx[t * 3 + 1] as usize;
5271                        let ic = idx[t * 3 + 2] as usize;
5272                        if ia >= nv || ib >= nv || ic >= nv {
5273                            continue;
5274                        }
5275                        let (da, db, dc) = (proj[ia * 3 + 2], proj[ib * 3 + 2], proj[ic * 3 + 2]);
5276                        if (da + db + dc) / 3.0 <= near {
5277                            continue;
5278                        }
5279                        let col = {
5280                            let (ax, ay, az) = (world[ia * 3], world[ia * 3 + 1], world[ia * 3 + 2]);
5281                            let (bx, by, bz) = (world[ib * 3], world[ib * 3 + 1], world[ib * 3 + 2]);
5282                            let (px, py, pz) = (world[ic * 3], world[ic * 3 + 1], world[ic * 3 + 2]);
5283                            let (ux, uy, uz) = (bx - ax, by - ay, bz - az);
5284                            let (vx, vy, vz) = (px - ax, py - ay, pz - az);
5285                            let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
5286                            let centroid =
5287                                [(ax + bx + px) / 3.0, (ay + by + py) / 3.0, (az + bz + pz) / 3.0];
5288                            if gfx.flat_shade {
5289                                base
5290                            } else {
5291                                crate::gfx::light::compute_lit_color(
5292                                    base, normal, centroid, &gfx.lights, ambient,
5293                                )
5294                            }
5295                        };
5296                        let depth = (da + db + dc) / 3.0;
5297                        let col = gfx.fog_apply(col, depth);
5298                        gfx.depth_queue.push_triangle_zv(
5299                            col,
5300                            proj[ia * 3], proj[ia * 3 + 1], da,
5301                            proj[ib * 3], proj[ib * 3 + 1], db,
5302                            proj[ic * 3], proj[ic * 3 + 1], dc,
5303                        );
5304                    }
5305                }
5306                return Ok(Value::Unit);
5307            },
5308
5309            // ── gltf_autorig(handle) → synthesize a humanoid skeleton + skin weights ──
5310            "gltf_autorig" => {
5311                let hh = self.arg_num(&args, 0, -1.0)? as i64;
5312                let mut models = self.gltf_models.borrow_mut();
5313                if let Some(m) = models.get_mut(hh as usize) {
5314                    return Ok(Value::Number(m.autorig() as f64));
5315                }
5316                return Ok(Value::Number(0.0));
5317            },
5318
5319            // ── gltf_pose_draw(handle, ox,oy,oz, scale, yaw, poseList) ──
5320            //   Like gltf_draw, but linear-blend-skins the mesh by `poseList` first.
5321            //   poseList = flat XYZ-euler radians, 3 per bone (12 bones → 36 values).
5322            "gltf_pose_draw" => {
5323                let hh = self.arg_num(&args, 0, -1.0)? as i64;
5324                let ox = self.arg_num(&args, 1, 0.0)? as f32;
5325                let oy = self.arg_num(&args, 2, 0.0)? as f32;
5326                let oz = self.arg_num(&args, 3, 0.0)? as f32;
5327                let scale = self.arg_num(&args, 4, 1.0)? as f32;
5328                let yaw = self.arg_num(&args, 5, 0.0)? as f32;
5329                let euler: Vec<f32> = match args.get(6) {
5330                    Some(Value::List(v)) => {
5331                        v.iter().map(|x| self.to_number(x).unwrap_or(0.0) as f32).collect()
5332                    },
5333                    _ => Vec::new(),
5334                };
5335                let (sy, cyy) = yaw.sin_cos();
5336                let models = self.gltf_models.borrow();
5337                let model = match models.get(hh as usize) {
5338                    Some(m) => m,
5339                    None => return Ok(Value::Unit),
5340                };
5341                let skinned = model.skin_local(&euler);
5342                let mut gfx = self.gfx.borrow_mut();
5343                let cp = {
5344                    let c = &gfx.camera;
5345                    ling_gpu::CameraParams {
5346                        cry: c.cry, sry: c.sry, crx: c.crx, srx: c.srx,
5347                        cx: c.cx, cy: c.cy, focal: c.focal, zdist: c.zdist,
5348                        tx: c.tx, ty: c.ty, tz: c.tz,
5349                    }
5350                };
5351                let near = -gfx.camera.zdist + 0.02;
5352                let ambient = gfx.ambient;
5353                for (mi, mesh) in model.meshes.iter().enumerate() {
5354                    let nlow = mesh.name.to_lowercase();
5355                    let base: u32 = if nlow.contains("hair") {
5356                        0x7a4a28
5357                    } else if nlow.contains("cloth") || nlow.contains("top") {
5358                        0x4a86e0
5359                    } else if nlow.contains("wing") {
5360                        0xe6ecf5
5361                    } else if nlow.contains("star") {
5362                        0xffd24d
5363                    } else {
5364                        0xf2d6b8
5365                    };
5366                    let sk = match skinned.get(mi) {
5367                        Some(s) => s,
5368                        None => continue,
5369                    };
5370                    let nv = sk.len();
5371                    if nv == 0 {
5372                        continue;
5373                    }
5374                    let mut world = vec![0.0f32; nv * 3];
5375                    for i in 0..nv {
5376                        let gx = sk[i][0] * scale;
5377                        let gy = -sk[i][1] * scale;
5378                        let gz = -sk[i][2] * scale;
5379                        let rx = gx * cyy + gz * sy;
5380                        let rz = -gx * sy + gz * cyy;
5381                        world[i * 3] = ox + rx;
5382                        world[i * 3 + 1] = oy + gy;
5383                        world[i * 3 + 2] = oz + rz;
5384                    }
5385                    let mut proj = vec![0.0f32; nv * 3];
5386                    ling_gpu::backend().project_points(&world, &cp, &mut proj);
5387                    let idx = &mesh.indices;
5388                    let nt = idx.len() / 3;
5389                    for t in 0..nt {
5390                        let ia = idx[t * 3] as usize;
5391                        let ib = idx[t * 3 + 1] as usize;
5392                        let ic = idx[t * 3 + 2] as usize;
5393                        if ia >= nv || ib >= nv || ic >= nv {
5394                            continue;
5395                        }
5396                        let (da, db, dc) = (proj[ia * 3 + 2], proj[ib * 3 + 2], proj[ic * 3 + 2]);
5397                        if (da + db + dc) / 3.0 <= near {
5398                            continue;
5399                        }
5400                        let col = {
5401                            let (ax, ay, az) = (world[ia * 3], world[ia * 3 + 1], world[ia * 3 + 2]);
5402                            let (bx, by, bz) = (world[ib * 3], world[ib * 3 + 1], world[ib * 3 + 2]);
5403                            let (px, py, pz) = (world[ic * 3], world[ic * 3 + 1], world[ic * 3 + 2]);
5404                            let (ux, uy, uz) = (bx - ax, by - ay, bz - az);
5405                            let (vx, vy, vz) = (px - ax, py - ay, pz - az);
5406                            let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
5407                            let centroid =
5408                                [(ax + bx + px) / 3.0, (ay + by + py) / 3.0, (az + bz + pz) / 3.0];
5409                            if gfx.flat_shade {
5410                                base
5411                            } else {
5412                                crate::gfx::light::compute_lit_color(
5413                                    base, normal, centroid, &gfx.lights, ambient,
5414                                )
5415                            }
5416                        };
5417                        let depth = (da + db + dc) / 3.0;
5418                        let col = gfx.fog_apply(col, depth);
5419                        gfx.depth_queue.push_triangle_zv(
5420                            col,
5421                            proj[ia * 3], proj[ia * 3 + 1], da,
5422                            proj[ib * 3], proj[ib * 3 + 1], db,
5423                            proj[ic * 3], proj[ic * 3 + 1], dc,
5424                        );
5425                    }
5426                }
5427                return Ok(Value::Unit);
5428            },
5429
5430            // ── draw_mesh(pos, idx, ox, oy, oz, scale, mode) ──
5431            //   Native batched triangle mesh. pos = flat [x,y,z,…], idx = flat tri indices.
5432            //   mode 0 = lit with current pen colour; 1 = per-face hue cycle.
5433            //   Vertices are batch-projected via ling-gpu (CPU fallback, or CUDA when the
5434            //   `cuda` feature is on); the per-triangle loop runs natively (not in the
5435            //   interpreter) so dense meshes (imported glTF, grids) stay fast.
5436            "draw_mesh" | "วาดเมช" => {
5437                let pos = match args.first() {
5438                    Some(Value::List(v)) => v,
5439                    _ => return Ok(Value::Unit),
5440                };
5441                let idx = match args.get(1) {
5442                    Some(Value::List(v)) => v,
5443                    _ => return Ok(Value::Unit),
5444                };
5445                let ox = self.arg_num(&args, 2, 0.0)? as f32;
5446                let oy = self.arg_num(&args, 3, 0.0)? as f32;
5447                let oz = self.arg_num(&args, 4, 0.0)? as f32;
5448                let scale = self.arg_num(&args, 5, 1.0)? as f32;
5449                let mode = self.arg_num(&args, 6, 0.0)? as i64;
5450                let nv = pos.len() / 3;
5451                if nv == 0 {
5452                    return Ok(Value::Unit);
5453                }
5454                let mut world = vec![0.0f32; nv * 3];
5455                for i in 0..nv {
5456                    world[i * 3] = ox + self.to_number(&pos[i * 3]).unwrap_or(0.0) as f32 * scale;
5457                    world[i * 3 + 1] =
5458                        oy + self.to_number(&pos[i * 3 + 1]).unwrap_or(0.0) as f32 * scale;
5459                    world[i * 3 + 2] =
5460                        oz + self.to_number(&pos[i * 3 + 2]).unwrap_or(0.0) as f32 * scale;
5461                }
5462                let mut gfx = self.gfx.borrow_mut();
5463                let cp = {
5464                    let c = &gfx.camera;
5465                    ling_gpu::CameraParams {
5466                        cry: c.cry,
5467                        sry: c.sry,
5468                        crx: c.crx,
5469                        srx: c.srx,
5470                        cx: c.cx,
5471                        cy: c.cy,
5472                        focal: c.focal,
5473                        zdist: c.zdist,
5474                        tx: c.tx,
5475                        ty: c.ty,
5476                        tz: c.tz,
5477                    }
5478                };
5479                let near = -gfx.camera.zdist + 0.02;
5480                let base = gfx.color;
5481                let ambient = gfx.ambient;
5482                let mut proj = vec![0.0f32; nv * 3]; // (sx, sy, depth) per vertex
5483                ling_gpu::backend().project_points(&world, &cp, &mut proj);
5484                let nt = idx.len() / 3;
5485                for t in 0..nt {
5486                    let ia = self.to_number(&idx[t * 3]).unwrap_or(0.0) as usize;
5487                    let ib = self.to_number(&idx[t * 3 + 1]).unwrap_or(0.0) as usize;
5488                    let ic = self.to_number(&idx[t * 3 + 2]).unwrap_or(0.0) as usize;
5489                    if ia >= nv || ib >= nv || ic >= nv {
5490                        continue;
5491                    }
5492                    let (da, db, dc) = (proj[ia * 3 + 2], proj[ib * 3 + 2], proj[ic * 3 + 2]);
5493                    if (da + db + dc) / 3.0 <= near {
5494                        continue;
5495                    } // near-plane cull (centroid)
5496                    let col = if mode == 1 {
5497                        let h = t as f32 * 0.6;
5498                        let r = ((h.sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
5499                        let g = (((h + 2.094).sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
5500                        let b = (((h + 4.189).sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
5501                        (r << 16) | (g << 8) | b
5502                    } else {
5503                        let (ax, ay, az) = (world[ia * 3], world[ia * 3 + 1], world[ia * 3 + 2]);
5504                        let (bx, by, bz) = (world[ib * 3], world[ib * 3 + 1], world[ib * 3 + 2]);
5505                        let (px, py, pz) = (world[ic * 3], world[ic * 3 + 1], world[ic * 3 + 2]);
5506                        let (ux, uy, uz) = (bx - ax, by - ay, bz - az);
5507                        let (vx, vy, vz) = (px - ax, py - ay, pz - az);
5508                        let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
5509                        let centroid = [
5510                            (ax + bx + px) / 3.0,
5511                            (ay + by + py) / 3.0,
5512                            (az + bz + pz) / 3.0,
5513                        ];
5514                        if gfx.flat_shade {
5515                            base
5516                        } else {
5517                            crate::gfx::light::compute_lit_color(
5518                                base,
5519                                normal,
5520                                centroid,
5521                                &gfx.lights,
5522                                ambient,
5523                            )
5524                        }
5525                    };
5526                    let depth = (da + db + dc) / 3.0;
5527                    let col = gfx.fog_apply(col, depth);
5528                    // True per-vertex depth so the z-buffer resolves mesh
5529                    // self-occlusion (when depth_test is off, the flush ignores
5530                    // z and uses the screen x/y exactly as before).
5531                    gfx.depth_queue.push_triangle_zv(
5532                        col,
5533                        proj[ia * 3],
5534                        proj[ia * 3 + 1],
5535                        da,
5536                        proj[ib * 3],
5537                        proj[ib * 3 + 1],
5538                        db,
5539                        proj[ic * 3],
5540                        proj[ic * 3 + 1],
5541                        dc,
5542                    );
5543                }
5544                return Ok(Value::Unit);
5545            },
5546
5547            // ── add_light(x, y, z, r, g, b, intensity, radius) ──
5548            // Adds a point light in world space.  r/g/b in [0..1].
5549            // radius == 0 → no distance falloff.
5550            "add_light" | "เพิ่มแสง" | "加灯" | "ライト追加" | "조명추가" =>
5551            {
5552                let x = self.arg_num(&args, 0, 0.0)? as f32;
5553                let y = self.arg_num(&args, 1, -3.0)? as f32;
5554                let z = self.arg_num(&args, 2, 3.0)? as f32;
5555                let mut r = self.arg_num(&args, 3, 1.0)? as f32;
5556                let mut g = self.arg_num(&args, 4, 1.0)? as f32;
5557                let mut b = self.arg_num(&args, 5, 1.0)? as f32;
5558                // Forgive 0-255 colour values: if any channel is clearly > 1,
5559                // treat the triple as 0-255 and normalise. Keeps 0-1 callers exact.
5560                if r > 1.5 || g > 1.5 || b > 1.5 {
5561                    r /= 255.0;
5562                    g /= 255.0;
5563                    b /= 255.0;
5564                }
5565                let intensity = self.arg_num(&args, 6, 1.0)? as f32;
5566                let radius = self.arg_num(&args, 7, 0.0)? as f32;
5567                self.gfx
5568                    .borrow_mut()
5569                    .lights
5570                    .push(Light { x, y, z, r, g, b, intensity, radius });
5571                return Ok(Value::Unit);
5572            },
5573
5574            // ── clear_lights() — remove all lights ──
5575            "clear_lights" | "ล้างแสง" | "清灯" | "ライト消去" | "조명초기화" =>
5576            {
5577                self.gfx.borrow_mut().lights.clear();
5578                return Ok(Value::Unit);
5579            },
5580
5581            // ── set_material(key, value) — configure LingMaterial field ──
5582            // Activates the material BSDF for subsequent polygon/triangle draws.
5583            // Keys (string): "albedo" "roughness" "metallic" "emission"
5584            //   "emission_strength" "specular" "specular_tint" "subsurface"
5585            //   "subsurface_color" "clearcoat" "clearcoat_roughness"
5586            //   "transmission" "ior" "iridescence" "sheen" "anisotropy"
5587            //   "anisotropy_angle" "toon_bands" "shadow_softness"
5588            //   "outline_px" "outline_color" "highlight_color"
5589            // Value: number (or packed 0xRRGGBB for colour fields)
5590            "set_material" | "ตั้งวัสดุ" | "设置材质" | "マテリアル設定" | "재질설정" =>
5591            {
5592                let key = self.arg_str(&args, 0, "");
5593                let val = self.arg_num(&args, 1, 0.0)?;
5594                let mut gfx = self.gfx.borrow_mut();
5595                let mat = gfx
5596                    .material
5597                    .get_or_insert_with(crate::gfx::LingMaterial::default);
5598                match key.as_str() {
5599                    "albedo" => mat.albedo = val as u32,
5600                    "roughness" => mat.roughness = val as f32,
5601                    "metallic" => mat.metallic = val as f32,
5602                    "emission" => mat.emission = val as u32,
5603                    "emission_strength" => mat.emission_strength = val as f32,
5604                    "specular" => mat.specular = val as f32,
5605                    "specular_tint" => mat.specular_tint = val as f32,
5606                    "subsurface" => mat.subsurface = val as f32,
5607                    "subsurface_color" => mat.subsurface_color = val as u32,
5608                    "clearcoat" => mat.clearcoat = val as f32,
5609                    "clearcoat_roughness" => mat.clearcoat_roughness = val as f32,
5610                    "transmission" => mat.transmission = val as f32,
5611                    "ior" => mat.ior = val as f32,
5612                    "iridescence" => mat.iridescence = val as f32,
5613                    "sheen" => mat.sheen = val as f32,
5614                    "anisotropy" => mat.anisotropy = val as f32,
5615                    "anisotropy_angle" => mat.anisotropy_angle = val as f32,
5616                    "toon_bands" => mat.toon_bands = val as u32,
5617                    "shadow_softness" => mat.shadow_softness = val as f32,
5618                    "outline_px" => mat.outline_px = val as f32,
5619                    "outline_color" => mat.outline_color = val as u32,
5620                    "highlight_color" => mat.highlight_color = val as u32,
5621                    _ => {},
5622                }
5623                return Ok(Value::Unit);
5624            },
5625
5626            // ── reset_material() — disable material override ──
5627            // After this call, draws use the legacy compute_lit_color_linear path.
5628            "reset_material" | "รีเซ็ตวัสดุ" | "重置材质" | "マテリアルリセット" | "재질초기화" =>
5629            {
5630                self.gfx.borrow_mut().material = None;
5631                return Ok(Value::Unit);
5632            },
5633
5634            // ── toon_outlines(thickness, color, threshold) ──
5635            // Enable vector-smooth ink outlines on depth discontinuities.
5636            //   thickness  — ink-line half-width in pixels (0 = off, 1.5 = anime default)
5637            //   color      — 0xRRGGBB ink colour (default black = 0)
5638            //   threshold  — depth delta that triggers an edge (0.05 recommended)
5639            "toon_outlines"
5640            | "ตั้งเส้นขอบการ์ตูน"
5641            | "卡通轮廓"
5642            | "トゥーンアウトライン"
5643            | "툰아웃라인" => {
5644                let px = self.arg_num(&args, 0, 0.0)? as f32;
5645                let color = self.arg_num(&args, 1, 0.0)? as u32;
5646                let thresh = self.arg_num(&args, 2, 0.05)? as f32;
5647                let mut gfx = self.gfx.borrow_mut();
5648                gfx.toon.outline_px = px;
5649                gfx.toon.outline_color = color;
5650                gfx.toon.outline_thresh = thresh;
5651                return Ok(Value::Unit);
5652            },
5653
5654            // ── tone_stop(t, value) ──
5655            // Add a stop to the tone ramp.
5656            //   t      — input luminance position [0..1]
5657            //   value  — output brightness [0..1]
5658            // Stops are automatically sorted; call tone_ramp_reset() first to clear.
5659            "tone_stop" | "ตั้งจุดโทน" | "色调停止" | "トーンストップ" | "톤스톱" =>
5660            {
5661                let t = self.arg_num(&args, 0, 0.0)? as f32;
5662                let val = self.arg_num(&args, 1, 1.0)? as f32;
5663                let mut gfx = self.gfx.borrow_mut();
5664                gfx.toon.ramp.stops.push(crate::gfx::toon::ToneStop {
5665                    t: t.clamp(0.0, 1.0),
5666                    value: val.clamp(0.0, 1.0),
5667                });
5668                gfx.toon
5669                    .ramp
5670                    .stops
5671                    .sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap_or(std::cmp::Ordering::Equal));
5672                return Ok(Value::Unit);
5673            },
5674
5675            // ── tone_smooth(enabled) ──
5676            // 0 = hard cel snap between stops (default); 1 = smooth gradient lerp.
5677            "tone_smooth" | "ตั้งโทนนุ่ม" | "色调平滑" | "トーンスムーズ" | "톤스무스" =>
5678            {
5679                let v = self.arg_num(&args, 0, 0.0)? as f32;
5680                self.gfx.borrow_mut().toon.ramp.smooth = v > 0.5;
5681                return Ok(Value::Unit);
5682            },
5683
5684            // ── tone_bezier(y1, y2) ──
5685            // Apply a cubic Bézier remap to the input luminance before stop lookup.
5686            //   y1, y2 — control-point y-values (identity: y1=0.333 y2=0.667)
5687            //   0 args or tone_bezier(0, 0)  → ease-in (shadow-heavy)
5688            //   tone_bezier(1, 1)            → ease-out (highlight-heavy)
5689            //   tone_bezier(0.1, 0.9)        → S-curve (smooth both ends)
5690            //   tone_bezier_off()            → disable (back to linear)
5691            "tone_bezier" | "ตั้งโทนเบซิเยร์" | "色调贝塞尔" | "トーンベジェ" | "톤베지어" =>
5692            {
5693                let y1 = self.arg_num(&args, 0, 1.0 / 3.0)? as f32;
5694                let y2 = self.arg_num(&args, 1, 2.0 / 3.0)? as f32;
5695                self.gfx.borrow_mut().toon.ramp.bezier = Some([y1, y2]);
5696                return Ok(Value::Unit);
5697            },
5698
5699            // ── tone_bezier_off() — disable Bézier remap ──
5700            "tone_bezier_off"
5701            | "ปิดโทนเบซิเยร์"
5702            | "关闭色调贝塞尔"
5703            | "トーンベジェオフ"
5704            | "톤베지어끄기" => {
5705                self.gfx.borrow_mut().toon.ramp.bezier = None;
5706                return Ok(Value::Unit);
5707            },
5708
5709            // ── tone_ramp_reset() — restore default 3-band cel ramp ──
5710            "tone_ramp_reset"
5711            | "รีเซ็ตการไล่โทน"
5712            | "重置色调渐变"
5713            | "トーンランプリセット"
5714            | "톤램프리셋" => {
5715                self.gfx.borrow_mut().toon.ramp = crate::gfx::toon::ToneRamp::default();
5716                return Ok(Value::Unit);
5717            },
5718
5719            // ── tone_ramp_clear() — clear all stops (build your own ramp) ──
5720            "tone_ramp_clear"
5721            | "ล้างการไล่โทน"
5722            | "清除色调渐变"
5723            | "トーンランプクリア"
5724            | "톤램프클리어" => {
5725                self.gfx.borrow_mut().toon.ramp.stops.clear();
5726                return Ok(Value::Unit);
5727            },
5728
5729            // ── tone_soft(soft, sheen) — band-edge softness + highlight sheen ──
5730            //   soft  [0..1] — fraction of each band gap that blends smoothly
5731            //                  across the boundary (0 = crisp cel, ~0.3 = soft
5732            //                  Wind Waker shadow edges). Default 0.32.
5733            //   sheen [0..1] — bright pixels keep their smooth gradient instead
5734            //                  of being quantised (clean specular/rim sheen
5735            //                  rather than scratchy banded highlights). 0.65.
5736            "tone_soft" | "โทนขอบนุ่ม" | "色调柔边" | "トーンソフト" | "톤소프트" => {
5737                let s = self.arg_num(&args, 0, 0.32)? as f32;
5738                let sh = self.arg_num(&args, 1, 0.65)? as f32;
5739                let mut gfx = self.gfx.borrow_mut();
5740                gfx.toon.ramp.soft = s.clamp(0.0, 1.0);
5741                gfx.toon.ramp.sheen = sh.clamp(0.0, 1.0);
5742                return Ok(Value::Unit);
5743            },
5744
5745            // ── set_ssao(strength, radius_px, zrange) — ambient occlusion ──
5746            // Depth-buffer contact shading: soft darkening in corners/under
5747            // objects, computed half-res + smoothed (no grain). Needs
5748            // set_depth_test(1). strength 0 disables. Defaults (0.35, 6, 12).
5749            "set_ssao" | "ตั้งเงาสัมผัส" | "环境光遮蔽" | "アンビエントオクルージョン"
5750            | "앰비언트오클루전" => {
5751                let s = self.arg_num(&args, 0, 0.35)? as f32;
5752                let r = self.arg_num(&args, 1, 6.0)? as f32;
5753                let z = self.arg_num(&args, 2, 12.0)? as f32;
5754                let mut gfx = self.gfx.borrow_mut();
5755                gfx.toon.ao_strength = s.clamp(0.0, 1.0);
5756                gfx.toon.ao_radius = r.max(1.0);
5757                gfx.toon.ao_range = z.max(0.01);
5758                return Ok(Value::Unit);
5759            },
5760
5761            // ── set_fxaa(on) — FXAA-lite screen-space edge anti-aliasing ──
5762            // Softens polygon stair-steps and ink-line jaggies over the whole
5763            // frame; flat fills are untouched. Applied last in the present
5764            // post-chain. (set_antialias smooths wireframe STROKES; this pass
5765            // smooths the composited IMAGE.)
5766            "set_fxaa" | "ลบรอยหยัก" | "屏幕抗锯齿" | "画面アンチエイリアス" | "화면안티앨리어싱" => {
5767                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
5768                self.gfx.borrow_mut().toon.fxaa = on;
5769                return Ok(Value::Unit);
5770            },
5771
5772            // ── set_bloom(strength, threshold) — soft HDR-style glow ──
5773            // Bright pixels (rim sheen, emissive, additive FX) bleed a soft
5774            // quarter-res glow — the "HDR material" feel for toon/vector art.
5775            // strength 0 disables; threshold = luminance cutoff [0..1].
5776            "set_bloom" | "ตั้งบลูม" | "泛光" | "ブルーム" | "블룸" => {
5777                let s = self.arg_num(&args, 0, 0.45)? as f32;
5778                let t = self.arg_num(&args, 1, 0.74)? as f32;
5779                let mut gfx = self.gfx.borrow_mut();
5780                gfx.toon.bloom_strength = s.max(0.0);
5781                gfx.toon.bloom_thresh = t.clamp(0.0, 0.99);
5782                return Ok(Value::Unit);
5783            },
5784
5785            // ── shadow_smooth(softness) [compat] → tone_smooth + tone_bezier ──
5786            // Deprecated: use tone_smooth + tone_bezier instead.
5787            "shadow_smooth" | "ตั้งเงานุ่ม" | "柔化阴影" | "影ソフト" | "그림자부드럽게" =>
5788            {
5789                let s = self.arg_num(&args, 0, 0.0)? as f32;
5790                let mut gfx = self.gfx.borrow_mut();
5791                gfx.toon.ramp.smooth = s > 0.05;
5792                if s > 0.05 {
5793                    let y1 = (0.333 + s * 0.2).clamp(0.0, 1.0);
5794                    let y2 = (0.667 - s * 0.2).clamp(0.0, 1.0);
5795                    gfx.toon.ramp.bezier = Some([y1, y2]);
5796                } else {
5797                    gfx.toon.ramp.bezier = None;
5798                }
5799                return Ok(Value::Unit);
5800            },
5801
5802            // ── toon_highlight [compat] — no-op, use tone_stop instead ──
5803            "toon_highlight"
5804            | "ตั้งไฮไลท์การ์ตูน"
5805            | "卡通高光"
5806            | "トゥーンハイライト"
5807            | "툰하이라이트" => {
5808                // Remap as a lit-band brightness boost: adds a stop near the highlight threshold.
5809                let _strength = self.arg_num(&args, 0, 0.0)? as f32;
5810                let _thresh = self.arg_num(&args, 2, 0.78)? as f32;
5811                // No-op: configure via tone_stop() for precise control.
5812                return Ok(Value::Unit);
5813            },
5814
5815            // ── set_ambient(v) — ambient light level [0..1] ──
5816            "set_ambient" | "ตั้งแสงรอบข้าง" | "环境光" | "環境光設定" | "환경광설정" =>
5817            {
5818                let v = self.arg_num(&args, 0, 0.15)? as f32;
5819                self.gfx.borrow_mut().ambient = v;
5820                return Ok(Value::Unit);
5821            },
5822
5823            // ── set_fog(r,g,b, start, end) — distance fog toward (r,g,b).
5824            //    triangles/lines fade from `start`..`end` camera depth. end<=0 = off.
5825            "set_fog" | "ตั้งหมอก" | "雾" | "霧設定" | "안개설정" => {
5826                let r = self.arg_num(&args, 0, 0.0)?.clamp(0.0, 255.0) as u32;
5827                let g = self.arg_num(&args, 1, 0.0)?.clamp(0.0, 255.0) as u32;
5828                let b = self.arg_num(&args, 2, 0.0)?.clamp(0.0, 255.0) as u32;
5829                let start = self.arg_num(&args, 3, 0.0)? as f32;
5830                let end = self.arg_num(&args, 4, 0.0)? as f32;
5831                let mut gfx = self.gfx.borrow_mut();
5832                gfx.fog_color = (r << 16) | (g << 8) | b;
5833                gfx.fog_start = start;
5834                gfx.fog_end = end;
5835                return Ok(Value::Unit);
5836            },
5837
5838            // ── วาดสามเหลี่ยม3มิติ(ax,ay,az, bx,by,bz, cx,cy,cz) ──
5839            // Computes lighting from world-space normal + active lights (cel shading),
5840            // projects via the stored camera, and pushes to the depth queue.
5841            "วาดสามเหลี่ยม3มิติ" | "draw_triangle_3d" | "triangle3d" =>
5842            {
5843                let ax = self.arg_num(&args, 0, 0.0)? as f32;
5844                let ay = self.arg_num(&args, 1, 0.0)? as f32;
5845                let az = self.arg_num(&args, 2, 0.0)? as f32;
5846                let bx = self.arg_num(&args, 3, 0.0)? as f32;
5847                let by = self.arg_num(&args, 4, 0.0)? as f32;
5848                let bz = self.arg_num(&args, 5, 0.0)? as f32;
5849                let cx = self.arg_num(&args, 6, 0.0)? as f32;
5850                let cy = self.arg_num(&args, 7, 0.0)? as f32;
5851                let cz = self.arg_num(&args, 8, 0.0)? as f32;
5852
5853                let mut gfx = self.gfx.borrow_mut();
5854
5855                // Mesh capture: record raw local coords + pen colour, skip submit.
5856                if gfx.mesh_capture.is_some() {
5857                    let col = gfx.color;
5858                    gfx.mesh_capture
5859                        .as_mut()
5860                        .unwrap()
5861                        .push(([ax, ay, az, bx, by, bz, cx, cy, cz], col));
5862                    return Ok(Value::Unit);
5863                }
5864
5865                gfx.submit_triangle(ax, ay, az, bx, by, bz, cx, cy, cz);
5866                return Ok(Value::Unit);
5867            },
5868
5869            // ── เริ่มอบเมช() — begin capturing 3-D triangles into a display list ──
5870            "เริ่มอบเมช" | "mesh_bake_begin" => {
5871                self.gfx.borrow_mut().mesh_capture = Some(Vec::new());
5872                return Ok(Value::Unit);
5873            },
5874
5875            // ── เมชแคชรับ(key) — keyed display-list cache lookup (-1 = miss) ──
5876            "เมชแคชรับ" | "mesh_cache_get" => {
5877                let key = self.arg_num(&args, 0, 0.0)? as i64;
5878                let h = self.gfx.borrow().mesh_cache.get(&key).copied();
5879                return Ok(Value::Number(h.map(|x| x as f64).unwrap_or(-1.0)));
5880            },
5881
5882            // ── เมชแคชตั้ง(key, handle) — store a baked mesh under key (bounded) ──
5883            "เมชแคชตั้ง" | "mesh_cache_put" => {
5884                let key = self.arg_num(&args, 0, 0.0)? as i64;
5885                let h = self.arg_num(&args, 1, 0.0)? as usize;
5886                let mut gfx = self.gfx.borrow_mut();
5887                const CAP: usize = 256;
5888                if gfx.mesh_cache.len() >= CAP {
5889                    let evict: Vec<usize> = gfx.mesh_cache.values().copied().collect();
5890                    gfx.mesh_cache.clear();
5891                    for id in evict {
5892                        if id < gfx.meshes.len() {
5893                            gfx.meshes[id].clear();
5894                            gfx.mesh_free.push(id);
5895                        }
5896                    }
5897                }
5898                gfx.mesh_cache.insert(key, h);
5899                return Ok(Value::Unit);
5900            },
5901
5902            // ── เมชแคชล้าง() — drop the keyed cache (e.g. on level change) ──
5903            "เมชแคชล้าง" | "mesh_cache_clear" => {
5904                let mut gfx = self.gfx.borrow_mut();
5905                let evict: Vec<usize> = gfx.mesh_cache.values().copied().collect();
5906                gfx.mesh_cache.clear();
5907                for id in evict {
5908                    if id < gfx.meshes.len() {
5909                        gfx.meshes[id].clear();
5910                        gfx.mesh_free.push(id);
5911                    }
5912                }
5913                return Ok(Value::Unit);
5914            },
5915
5916            // ── จบอบเมช() — bake captured triangles, return mesh handle ──
5917            "จบอบเมช" | "mesh_bake_end" => {
5918                let mut gfx = self.gfx.borrow_mut();
5919                let tris = gfx.mesh_capture.take().unwrap_or_default();
5920                let id = gfx.mesh_register(tris);
5921                return Ok(Value::Number(id as f64));
5922            },
5923
5924            // ── วาดอบเมช[สี](id, ox,oy,oz, rx,ry,rz, ux,uy,uz, s) — draw a baked mesh ──
5925            //   วาดอบเมช: current pen colour (tinted glyphs)
5926            //   วาดอบเมชสี: per-triangle baked colour (multi-colour models)
5927            "วาดอบเมช" | "mesh_bake_draw" | "วาดอบเมชสี" | "mesh_bake_draw_col" =>
5928            {
5929                let baked_col = matches!(name, "วาดอบเมชสี" | "mesh_bake_draw_col");
5930                let id = self.arg_num(&args, 0, 0.0)? as usize;
5931                let ox = self.arg_num(&args, 1, 0.0)? as f32;
5932                let oy = self.arg_num(&args, 2, 0.0)? as f32;
5933                let oz = self.arg_num(&args, 3, 0.0)? as f32;
5934                let rx = self.arg_num(&args, 4, 1.0)? as f32;
5935                let ry = self.arg_num(&args, 5, 0.0)? as f32;
5936                let rz = self.arg_num(&args, 6, 0.0)? as f32;
5937                let ux = self.arg_num(&args, 7, 0.0)? as f32;
5938                let uy = self.arg_num(&args, 8, 1.0)? as f32;
5939                let uz = self.arg_num(&args, 9, 0.0)? as f32;
5940                let s = self.arg_num(&args, 10, 1.0)? as f32;
5941                self.gfx
5942                    .borrow_mut()
5943                    .mesh_draw(id, ox, oy, oz, rx, ry, rz, ux, uy, uz, s, baked_col);
5944                return Ok(Value::Unit);
5945            },
5946
5947            // ── draw_quad_3d / draw_pent_3d / draw_hex_3d / draw_polygon_3d ──
5948            // Fan-triangulate convex n-gons.  Lighting, near-plane clip, fog, and
5949            // Gouraud shading all mirror draw_triangle_3d exactly.
5950            "draw_quad_3d"
5951            | "quad3d"
5952            | "วาดสี่เหลี่ยม3มิติ"
5953            | "draw_pent_3d"
5954            | "pent3d"
5955            | "วาดห้าเหลี่ยม3มิติ"
5956            | "draw_hex_3d"
5957            | "hex3d"
5958            | "วาดหกเหลี่ยม3มิติ"
5959            | "draw_polygon_3d"
5960            | "polygon3d"
5961            | "วาดรูปหลายเหลี่ยม3มิติ" => {
5962                // Collect (wx, wy, wz) triples from args or list
5963                let mut wxs: [f32; 8] = [0.0; 8];
5964                let mut wys: [f32; 8] = [0.0; 8];
5965                let mut wzs: [f32; 8] = [0.0; 8];
5966                let n_verts;
5967
5968                if args.len() == 1 {
5969                    // draw_polygon_3d([x0,y0,z0, x1,y1,z1, ...])
5970                    let list = match &args[0] {
5971                        Value::List(l) => l.clone(),
5972                        _ => {
5973                            return Err(EvalErr::from("draw_polygon_3d: expected list".to_string()))
5974                        },
5975                    };
5976                    let coords: Vec<f32> = list
5977                        .iter()
5978                        .map(|v| match v {
5979                            Value::Number(n) => *n as f32,
5980                            _ => 0.0,
5981                        })
5982                        .collect();
5983                    n_verts = (coords.len() / 3).min(8);
5984                    for i in 0..n_verts {
5985                        wxs[i] = coords[i * 3];
5986                        wys[i] = coords[i * 3 + 1];
5987                        wzs[i] = coords[i * 3 + 2];
5988                    }
5989                } else {
5990                    // draw_quad/pent/hex_3d(x0,y0,z0, x1,y1,z1, ...)
5991                    n_verts = (args.len() / 3).min(8);
5992                    for i in 0..n_verts {
5993                        wxs[i] = self.arg_num(&args, i * 3, 0.0)? as f32;
5994                        wys[i] = self.arg_num(&args, i * 3 + 1, 0.0)? as f32;
5995                        wzs[i] = self.arg_num(&args, i * 3 + 2, 0.0)? as f32;
5996                    }
5997                }
5998                if n_verts < 3 {
5999                    return Ok(Value::Unit);
6000                }
6001
6002                let mut gfx = self.gfx.borrow_mut();
6003
6004                // Mesh capture: fan-triangulate and record raw local coords +
6005                // pen colour, exactly like วาดสามเหลี่ยม3มิติ. Quads used to
6006                // fall through here and bake EMPTY display lists — the 3-D
6007                // glyph fonts (letter pickups) are built from draw_quad_3d,
6008                // which made every baked glyph invisible.
6009                if gfx.mesh_capture.is_some() {
6010                    let col = gfx.color;
6011                    let cap = gfx.mesh_capture.as_mut().unwrap();
6012                    for i in 1..n_verts - 1 {
6013                        cap.push((
6014                            [
6015                                wxs[0], wys[0], wzs[0],
6016                                wxs[i], wys[i], wzs[i],
6017                                wxs[i + 1], wys[i + 1], wzs[i + 1],
6018                            ],
6019                            col,
6020                        ));
6021                    }
6022                    return Ok(Value::Unit);
6023                }
6024
6025                // Face normal from first triangle of the fan
6026                let normal = crate::gfx::poly::face_normal(
6027                    wxs[0], wys[0], wzs[0], wxs[1], wys[1], wzs[1], wxs[2], wys[2], wzs[2],
6028                );
6029
6030                // Per-vertex lit colours
6031                let mut wcs: [u32; 8] = [0; 8];
6032                if gfx.flat_shade {
6033                    let c = gfx.color;
6034                    for wc in wcs.iter_mut().take(n_verts) {
6035                        *wc = c;
6036                    }
6037                } else if let Some(ref mat) = gfx.material.clone() {
6038                    let cam = [gfx.camera.cx, gfx.camera.cy, gfx.camera.zdist];
6039                    let lights: Vec<_> = gfx.lights.clone();
6040                    let ambient = gfx.ambient;
6041                    for i in 0..n_verts {
6042                        let v = [wxs[i], wys[i], wzs[i]];
6043                        let vd = [cam[0] - v[0], cam[1] - v[1], cam[2] - v[2]];
6044                        wcs[i] = crate::gfx::material::shade(mat, normal, vd, v, &lights, ambient);
6045                    }
6046                } else {
6047                    let base = gfx.color;
6048                    let lights: Vec<_> = gfx.lights.clone();
6049                    let ambient = gfx.ambient;
6050                    for i in 0..n_verts {
6051                        wcs[i] = crate::gfx::light::compute_lit_color_linear(
6052                            base,
6053                            normal,
6054                            [wxs[i], wys[i], wzs[i]],
6055                            &lights,
6056                            ambient,
6057                        );
6058                    }
6059                }
6060
6061                // Near-plane clip (Sutherland-Hodgman per vertex)
6062                let near = -gfx.camera.zdist + 0.05;
6063                let mut clip_in: [(f32, f32, f32, f32, u32); crate::gfx::poly::MAX_CLIP_VERTS] =
6064                    [(0.0, 0.0, 0.0, 0.0, 0); crate::gfx::poly::MAX_CLIP_VERTS];
6065                for i in 0..n_verts {
6066                    let d = gfx.camera.depth(wxs[i], wys[i], wzs[i]);
6067                    clip_in[i] = (wxs[i], wys[i], wzs[i], d, wcs[i]);
6068                }
6069                let mut clip_out: [(f32, f32, f32, f32, u32); crate::gfx::poly::MAX_CLIP_VERTS] =
6070                    [(0.0, 0.0, 0.0, 0.0, 0); crate::gfx::poly::MAX_CLIP_VERTS];
6071                let pn = crate::gfx::poly::clip_near(&clip_in, n_verts, near, &mut clip_out);
6072                if pn < 3 {
6073                    return Ok(Value::Unit);
6074                }
6075
6076                // Project + fog
6077                let mut proj: [(f32, f32, f32, u32); crate::gfx::poly::MAX_CLIP_VERTS] =
6078                    [(0.0, 0.0, 0.0, 0); crate::gfx::poly::MAX_CLIP_VERTS];
6079                for i in 0..pn {
6080                    let (sx, sy, sz) =
6081                        gfx.camera
6082                            .project(clip_out[i].0, clip_out[i].1, clip_out[i].2);
6083                    let fc = gfx.fog_apply(clip_out[i].4, sz);
6084                    proj[i] = (sx, sy, sz, fc);
6085                }
6086
6087                // Fan-triangulate and push
6088                let unlit = gfx.flat_shade;
6089                crate::gfx::poly::fan_emit_proj(
6090                    &proj,
6091                    pn,
6092                    |x0, y0, z0, c0, x1, y1, z1, c1, x2, y2, z2, c2| {
6093                        gfx.depth_queue.push_triangle_g_zv(
6094                            x0, y0, z0, c0, x1, y1, z1, c1, x2, y2, z2, c2, 3, unlit,
6095                        );
6096                    },
6097                );
6098                return Ok(Value::Unit);
6099            },
6100
6101            // ── วาดเส้น3มิติ(ax,ay,az, bx,by,bz) ──
6102            // Projects two world-space points via the stored camera and pushes
6103            // a line to the depth queue.
6104            "วาดเส้น3มิติ" | "draw_line_3d" | "line3d" | "画3D线" | "3D線描く" | "3D선그리기" =>
6105            {
6106                let ax = self.arg_num(&args, 0, 0.0)? as f32;
6107                let ay = self.arg_num(&args, 1, 0.0)? as f32;
6108                let az = self.arg_num(&args, 2, 0.0)? as f32;
6109                let bx = self.arg_num(&args, 3, 0.0)? as f32;
6110                let by = self.arg_num(&args, 4, 0.0)? as f32;
6111                let bz = self.arg_num(&args, 5, 0.0)? as f32;
6112
6113                let mut gfx = self.gfx.borrow_mut();
6114                let color = gfx.color;
6115                // Near-plane clip in 3-D before perspective divide
6116                let near = -gfx.camera.zdist + 0.05;
6117                let mut lax = ax;
6118                let mut lay = ay;
6119                let mut laz = az;
6120                let mut lbx = bx;
6121                let mut lby = by;
6122                let mut lbz = bz;
6123                let da_raw = gfx.camera.depth(lax, lay, laz);
6124                let db_raw = gfx.camera.depth(lbx, lby, lbz);
6125                if da_raw <= near && db_raw <= near {
6126                    return Ok(Value::Unit);
6127                }
6128                if da_raw <= near {
6129                    let t = (near - da_raw) / (db_raw - da_raw);
6130                    lax += t * (lbx - lax);
6131                    lay += t * (lby - lay);
6132                    laz += t * (lbz - laz);
6133                } else if db_raw <= near {
6134                    let t = (near - da_raw) / (db_raw - da_raw);
6135                    lbx = lax + t * (lbx - lax);
6136                    lby = lay + t * (lby - lay);
6137                    lbz = laz + t * (lbz - laz);
6138                }
6139                // Shared-edge dedup: skip if this world-space edge was already queued.
6140                if !gfx.edge_set.try_insert(lax, lay, laz, lbx, lby, lbz) {
6141                    return Ok(Value::Unit);
6142                }
6143                let (sax, say, da) = gfx.camera.project(lax, lay, laz);
6144                let (sbx, sby, db) = gfx.camera.project(lbx, lby, lbz);
6145                let depth = (da + db) / 2.0;
6146                let color = gfx.fog_apply(color, depth);
6147                gfx.depth_queue.push_line(depth, color, sax, say, sbx, sby);
6148                return Ok(Value::Unit);
6149            },
6150
6151            // orb_shell(cx,cy,cz, radius, rot_y, rot_x, density, r,g,b)
6152            //   A single trippy, grayscale, depth-faded vector pattern wound around
6153            //   a sphere — two families of interleaved spherical spirals (a guilloché
6154            //   weave), NOT a lat/long cage. Each segment's brightness follows its
6155            //   facing (front bright, back dim), so it reads as a translucent
6156            //   grayscale "texture" with alpha rather than a hard wireframe; the
6157            //   inner marble shows through. `rot_y`/`rot_x` roll the texture around
6158            //   the orb; `density` = spirals per winding direction. r,g,b tint it
6159            //   (pass a gray like 230,230,230 for pure grayscale).
6160            #[cfg(not(target_arch = "wasm32"))]
6161            "orb_shell" | "球壳" | "オーブ殻" | "오브껍질" | "เปลือกทรงกลม" =>
6162            {
6163                let cx = self.arg_num(&args, 0, 0.)? as f32;
6164                let cy = self.arg_num(&args, 1, 0.)? as f32;
6165                let cz = self.arg_num(&args, 2, 0.)? as f32;
6166                let radius = self.arg_num(&args, 3, 1.0)? as f32;
6167                let ry = self.arg_num(&args, 4, 0.)? as f32;
6168                let rx = self.arg_num(&args, 5, 0.)? as f32;
6169                let density = (self.arg_num(&args, 6, 10.)? as i32).clamp(1, 48);
6170                let tr = (self.arg_num(&args, 7, 230.)? as f32).clamp(0., 255.);
6171                let tg = (self.arg_num(&args, 8, 230.)? as f32).clamp(0., 255.);
6172                let tb = (self.arg_num(&args, 9, 235.)? as f32).clamp(0., 255.);
6173                let (cyr, syr) = (ry.cos(), ry.sin());
6174                let (cxr, sxr) = (rx.cos(), rx.sin());
6175                let tau = std::f32::consts::TAU;
6176                let pi = std::f32::consts::PI;
6177                let turns = 6.0_f32; // how many times each spiral wraps pole→pole
6178                let nseg = 96; // segments per spiral (smoothness)
6179                let inv_r = if radius.abs() > 1e-5 {
6180                    1.0 / radius
6181                } else {
6182                    0.0
6183                };
6184                // a point along a spiral (param u 0..1, start angle theta0, winding dir),
6185                // spun by ry/rx — returns (world point, facing 0..1 where 1 = toward camera)
6186                let pt = |u: f32, theta0: f32, dir: f32| -> ([f32; 3], f32) {
6187                    let phi = pi * u; // 0..pi  (north → south)
6188                    let th = dir * turns * tau * u + theta0;
6189                    let (mut x, y, mut z) = (
6190                        phi.sin() * th.cos() * radius,
6191                        phi.cos() * radius,
6192                        phi.sin() * th.sin() * radius,
6193                    );
6194                    let x1 = x * cyr + z * syr; // yaw about Y
6195                    let z1 = -x * syr + z * cyr;
6196                    x = x1;
6197                    z = z1;
6198                    let y2 = y * cxr - z * sxr; // pitch about X
6199                    let z2 = y * sxr + z * cxr;
6200                    // facing: camera sits at -zdist looking +z, so smaller z2 = nearer = brighter
6201                    let facing = (0.5 - 0.5 * z2 * inv_r).clamp(0.0, 1.0);
6202                    ([cx + x, cy + y2, cz + z2], facing)
6203                };
6204                let mut gfx = self.gfx.borrow_mut();
6205                let near = -gfx.camera.zdist + 0.05;
6206                // draw one segment (near-clipped) in a grayscale tint scaled by `lum`
6207                let seg = |gfx: &mut crate::gfx::GfxState, a: [f32; 3], b: [f32; 3], lum: f32| {
6208                    let (mut lax, mut lay, mut laz) = (a[0], a[1], a[2]);
6209                    let (mut lbx, mut lby, mut lbz) = (b[0], b[1], b[2]);
6210                    let da = gfx.camera.depth(lax, lay, laz);
6211                    let db = gfx.camera.depth(lbx, lby, lbz);
6212                    if da <= near && db <= near {
6213                        return;
6214                    }
6215                    if da <= near {
6216                        let t = (near - da) / (db - da);
6217                        lax += t * (lbx - lax);
6218                        lay += t * (lby - lay);
6219                        laz += t * (lbz - laz);
6220                    } else if db <= near {
6221                        let t = (near - da) / (db - da);
6222                        lbx = lax + t * (lbx - lax);
6223                        lby = lay + t * (lby - lay);
6224                        lbz = laz + t * (lbz - laz);
6225                    }
6226                    let (sax, say, da2) = gfx.camera.project(lax, lay, laz);
6227                    let (sbx, sby, db2) = gfx.camera.project(lbx, lby, lbz);
6228                    // grayscale-alpha: front-facing bright, back faded toward black
6229                    let l = (0.12 + 0.88 * lum).clamp(0.0, 1.0);
6230                    let cr = (tr * l) as u32;
6231                    let cg = (tg * l) as u32;
6232                    let cb = (tb * l) as u32;
6233                    let color = (cr << 16) | (cg << 8) | cb;
6234                    gfx.depth_queue
6235                        .push_line((da2 + db2) * 0.5, color, sax, say, sbx, sby);
6236                };
6237                // two opposite winding directions → a soft guilloché weave (not a cage)
6238                for &dir in &[1.0_f32, -1.0_f32] {
6239                    for s in 0..density {
6240                        let theta0 = s as f32 * tau / density as f32;
6241                        let mut prev = pt(0.0, theta0, dir);
6242                        for k in 1..=nseg {
6243                            let cur = pt(k as f32 / nseg as f32, theta0, dir);
6244                            seg(&mut gfx, prev.0, cur.0, (prev.1 + cur.1) * 0.5);
6245                            prev = cur;
6246                        }
6247                    }
6248                }
6249                return Ok(Value::Unit);
6250            },
6251
6252            // orb_particles(cx,cy,cz, radius, count, t, r,g,b)
6253            //   Fills the VOLUME of a sphere with `count` swirling vector points —
6254            //   like motes suspended inside a snow-globe orb. Points are distributed
6255            //   uniformly through the ball, slowly tumble as a cloud + wobble
6256            //   individually over time `t`, and are depth-shaded (near = bright,
6257            //   far = dim) so the cloud has real volume. Additive, so it layers under
6258            //   a shell / over a liquid marble.
6259            #[cfg(not(target_arch = "wasm32"))]
6260            "orb_particles" | "球内粒子" | "オーブ粒子" | "오브입자" | "อนุภาคทรงกลม" =>
6261            {
6262                let cx = self.arg_num(&args, 0, 0.)? as f32;
6263                let cy = self.arg_num(&args, 1, 0.)? as f32;
6264                let cz = self.arg_num(&args, 2, 0.)? as f32;
6265                let radius = self.arg_num(&args, 3, 1.0)? as f32;
6266                let count = (self.arg_num(&args, 4, 160.)? as i32).clamp(1, 4000);
6267                let t = self.arg_num(&args, 5, 0.)? as f32;
6268                let tr = (self.arg_num(&args, 6, 255.)? as f32).clamp(0., 255.);
6269                let tg = (self.arg_num(&args, 7, 255.)? as f32).clamp(0., 255.);
6270                let tb = (self.arg_num(&args, 8, 255.)? as f32).clamp(0., 255.);
6271                let inv_r = if radius.abs() > 1e-5 {
6272                    1.0 / radius
6273                } else {
6274                    0.0
6275                };
6276                // cheap deterministic hash → [0,1)
6277                let h = |mut x: u32| -> f32 {
6278                    x = x.wrapping_mul(747796405).wrapping_add(2891336453);
6279                    x = ((x >> ((x >> 28).wrapping_add(4))) ^ x).wrapping_mul(277803737);
6280                    (((x >> 22) ^ x) & 0xFFFFFF) as f32 / 16_777_216.0
6281                };
6282                let tau = std::f32::consts::TAU;
6283                // slow tumble of the whole cloud
6284                let (cyr, syr) = ((t * 0.5).cos(), (t * 0.5).sin());
6285                let (cxr, sxr) = ((t * 0.23).cos(), (t * 0.23).sin());
6286                let mut gfx = self.gfx.borrow_mut();
6287                let near = -gfx.camera.zdist + 0.05;
6288                let (sw, sh) = (gfx.width as i32, gfx.height as i32);
6289                for i in 0..count {
6290                    let i = i as u32;
6291                    // uniform-in-volume: r = cbrt(u) * radius; direction from two hashes
6292                    let u = h(i.wrapping_mul(3) + 1);
6293                    let rr = u.cbrt() * radius * (0.85 + 0.15 * (t * 1.3 + i as f32).sin()); // gentle pulse
6294                    let th = h(i.wrapping_mul(3) + 2) * tau + t * (0.3 + 0.5 * h(i * 7 + 5)); // per-mote orbit
6295                    let ph = (h(i.wrapping_mul(3) + 3) * 2.0 - 1.0).acos(); // uniform cos(phi)
6296                    let (mut x, y, mut z) = (
6297                        rr * ph.sin() * th.cos(),
6298                        rr * ph.cos(),
6299                        rr * ph.sin() * th.sin(),
6300                    );
6301                    // tumble the cloud (yaw then pitch)
6302                    let x1 = x * cyr + z * syr;
6303                    let z1 = -x * syr + z * cyr;
6304                    x = x1;
6305                    z = z1;
6306                    let y2 = y * cxr - z * sxr;
6307                    let z2 = y * sxr + z * cxr;
6308                    let (wx, wy, wz) = (cx + x, cy + y2, cz + z2);
6309                    if gfx.camera.depth(wx, wy, wz) <= near {
6310                        continue;
6311                    }
6312                    let (sx, sy, dep) = gfx.camera.project(wx, wy, wz);
6313                    let sxi = sx as i32;
6314                    let syi = sy as i32;
6315                    if sxi < 0 || syi < 0 || sxi >= sw || syi >= sh {
6316                        continue;
6317                    }
6318                    // depth-shade: nearer (smaller z2) = brighter
6319                    let facing = (0.5 - 0.5 * z2 * inv_r).clamp(0.15, 1.0);
6320                    let l = facing;
6321                    let cr = (tr * l) as u32;
6322                    let cg = (tg * l) as u32;
6323                    let cb = (tb * l) as u32;
6324                    let color = (cr << 16) | (cg << 8) | cb;
6325                    // a 1–2px dot (bigger when near) as a short segment in the depth queue
6326                    let len = if facing > 0.7 { 1.0 } else { 0.0 };
6327                    gfx.depth_queue.push_line(dep, color, sx, sy, sx + len, sy);
6328                }
6329                return Ok(Value::Unit);
6330            },
6331
6332            // project_3d(x,y,z) -> [screen_x, screen_y, depth]; behind the camera
6333            // returns a sentinel ([-99999,-99999, depth]) so scripts can skip it.
6334            // Lets scripts place 2-D overlays (e.g. filled teardrop flames) onto 3-D points.
6335            "project_3d" | "投影3D" | "3D投影" | "3D투영" | "ฉาย3มิติ" => {
6336                let x = self.arg_num(&args, 0, 0.0)? as f32;
6337                let y = self.arg_num(&args, 1, 0.0)? as f32;
6338                let z = self.arg_num(&args, 2, 0.0)? as f32;
6339                let gfx = self.gfx.borrow();
6340                let near = -gfx.camera.zdist + 0.05;
6341                let d = gfx.camera.depth(x, y, z);
6342                if d <= near {
6343                    return Ok(Value::List(Rc::new(vec![
6344                        Value::Number(-99999.0),
6345                        Value::Number(-99999.0),
6346                        Value::Number(d as f64),
6347                    ])));
6348                }
6349                let (sx, sy, depth) = gfx.camera.project(x, y, z);
6350                return Ok(Value::List(Rc::new(vec![
6351                    Value::Number(sx as f64),
6352                    Value::Number(sy as f64),
6353                    Value::Number(depth as f64),
6354                ])));
6355            },
6356
6357            // mouse_ray() -> [ox,oy,oz, dx,dy,dz] — world-space ray from the eye
6358            // through the actual mouse cursor pixel, exact inverse of project_3d's
6359            // pipeline (translate → Y-rotate → X-rotate → perspective divide by
6360            // rz+zdist). Scripts previously marched a ray along the CENTRE-SCREEN
6361            // forward vector regardless of where the cursor was — accurate only by
6362            // coincidence when the cursor happened to sit near the crosshair.
6363            #[cfg(not(target_arch = "wasm32"))]
6364            "mouse_ray" => {
6365                let gfx = self.gfx.borrow();
6366                let (mx, my) = gfx
6367                    .window
6368                    .as_ref()
6369                    .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
6370                    .unwrap_or((gfx.camera.cx, gfx.camera.cy));
6371                let cam = &gfx.camera;
6372                // Eye-relative pinhole direction for this pixel, in rotation-space
6373                // (before undoing the Y-then-X rotation project() applied).
6374                let dcx = (mx - cam.cx) / cam.focal;
6375                let dcy = (my - cam.cy) / cam.focal;
6376                // Undo the X-rotation, then the Y-rotation (reverse of project()'s
6377                // forward order), on the direction vector (dcx, dcy, 1.0).
6378                let a_x = dcx;
6379                let a_y = cam.crx * dcy + cam.srx * 1.0;
6380                let a_z = 0.0 - cam.srx * dcy + cam.crx * 1.0;
6381                let dir_x = cam.cry * a_x + cam.sry * a_z;
6382                let dir_y = a_y;
6383                let dir_z = 0.0 - cam.sry * a_x + cam.cry * a_z;
6384                let dlen = (dir_x * dir_x + dir_y * dir_y + dir_z * dir_z)
6385                    .sqrt()
6386                    .max(1e-6);
6387                let (dir_x, dir_y, dir_z) = (dir_x / dlen, dir_y / dlen, dir_z / dlen);
6388                // Origin: the camera's rotation pivot (tx,ty,tz) — i.e. wherever
6389                // the script last put it with set_camera_pos, NOT the "true"
6390                // pinhole eye zdist further back. The pivot is what scripts
6391                // already keep clear of the ground (a ground-collision pull-in
6392                // loop is standard practice for orbit cameras); the true eye
6393                // would need its own separate ground clearance since zdist is
6394                // often large relative to a close-in camera distance, and
6395                // starting a ray underground makes it hit "ground" instantly
6396                // regardless of aim. The zdist offset only matters for the
6397                // near-field parallax, which a click-to-move ray (aimed at
6398                // terrain many units out) doesn't need.
6399                let ox = cam.tx;
6400                let oy = cam.ty;
6401                let oz = cam.tz;
6402                return Ok(Value::List(Rc::new(vec![
6403                    Value::Number(ox as f64),
6404                    Value::Number(oy as f64),
6405                    Value::Number(oz as f64),
6406                    Value::Number(dir_x as f64),
6407                    Value::Number(dir_y as f64),
6408                    Value::Number(dir_z as f64),
6409                ])));
6410            },
6411            #[cfg(target_arch = "wasm32")]
6412            "mouse_ray" => {
6413                let gfx = self.gfx.borrow();
6414                let mx = crate::gfx::wasm_mouse_x();
6415                let my = crate::gfx::wasm_mouse_y();
6416                let cam = &gfx.camera;
6417                let dcx = (mx - cam.cx) / cam.focal;
6418                let dcy = (my - cam.cy) / cam.focal;
6419                let a_x = dcx;
6420                let a_y = cam.crx * dcy + cam.srx * 1.0;
6421                let a_z = 0.0 - cam.srx * dcy + cam.crx * 1.0;
6422                let dir_x = cam.cry * a_x + cam.sry * a_z;
6423                let dir_y = a_y;
6424                let dir_z = 0.0 - cam.sry * a_x + cam.cry * a_z;
6425                let dlen = (dir_x * dir_x + dir_y * dir_y + dir_z * dir_z)
6426                    .sqrt()
6427                    .max(1e-6);
6428                let (dir_x, dir_y, dir_z) = (dir_x / dlen, dir_y / dlen, dir_z / dlen);
6429                let ox = cam.tx;
6430                let oy = cam.ty;
6431                let oz = cam.tz;
6432                return Ok(Value::List(Rc::new(vec![
6433                    Value::Number(ox as f64),
6434                    Value::Number(oy as f64),
6435                    Value::Number(oz as f64),
6436                    Value::Number(dir_x as f64),
6437                    Value::Number(dir_y as f64),
6438                    Value::Number(dir_z as f64),
6439                ])));
6440            },
6441            // draw_poly([x0,y0,x1,y1,…]) — filled 2-D polygon in the current colour,
6442            // honouring the blend mode (additive → translucent glow). Auto-closes.
6443            #[cfg(not(target_arch = "wasm32"))]
6444            "draw_poly" | "填充多边形" | "ポリゴン塗り" | "다각형채우기" | "เติมรูปหลายเหลี่ยม" =>
6445            {
6446                let mut pts: Vec<[f32; 2]> = Vec::new();
6447                if let Some(Value::List(v)) = args.first() {
6448                    let mut i = 0;
6449                    while i + 1 < v.len() {
6450                        let x = self.to_number(&v[i]).unwrap_or(0.0) as f32;
6451                        let y = self.to_number(&v[i + 1]).unwrap_or(0.0) as f32;
6452                        pts.push([x, y]);
6453                        i += 2;
6454                    }
6455                }
6456                if pts.len() >= 3 {
6457                    if pts[0] != pts[pts.len() - 1] {
6458                        let p0 = pts[0];
6459                        pts.push(p0);
6460                    } // close
6461                    let mut gfx = self.gfx.borrow_mut();
6462                    let (w, h, color, add) = (gfx.width, gfx.height, gfx.color, gfx.blend == 1);
6463                    crate::gfx::raster::fill_contours_aa(
6464                        &mut gfx.buffer,
6465                        w,
6466                        h,
6467                        color,
6468                        add,
6469                        std::slice::from_ref(&pts),
6470                    );
6471                }
6472                return Ok(Value::Unit);
6473            },
6474
6475            // ══════════════════════════════════════════════════════════════════
6476            // VECTOR TEXTURE BUILTINS  (src/gfx/vtex.rs)
6477            // All patterns are depth-biased so they appear on top of surfaces.
6478            // Plane defined by: centre (cx,cy,cz) + U tangent + V tangent.
6479            // Last two args always: fr (frame f32), hue (phase offset f32).
6480            // ══════════════════════════════════════════════════════════════════
6481
6482            // vtex_grid(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cw,ch, fr,hue)
6483            "vtex_grid" | "ลายตาราง" | "纹格" | "格子模様" | "격자무늬" =>
6484            {
6485                let cx = self.arg_num(&args, 0, 0.)? as f32;
6486                let cy = self.arg_num(&args, 1, 0.)? as f32;
6487                let cz = self.arg_num(&args, 2, 0.)? as f32;
6488                let ux = self.arg_num(&args, 3, 1.)? as f32;
6489                let uy = self.arg_num(&args, 4, 0.)? as f32;
6490                let uz = self.arg_num(&args, 5, 0.)? as f32;
6491                let vx = self.arg_num(&args, 6, 0.)? as f32;
6492                let vy = self.arg_num(&args, 7, 0.)? as f32;
6493                let vz = self.arg_num(&args, 8, 1.)? as f32;
6494                let cols = self.arg_num(&args, 9, 10.)? as usize;
6495                let rows = self.arg_num(&args, 10, 10.)? as usize;
6496                let cw = self.arg_num(&args, 11, 1.)? as f32;
6497                let ch = self.arg_num(&args, 12, 1.)? as f32;
6498                let fr = self.arg_num(&args, 13, 0.)? as f32;
6499                let hue = self.arg_num(&args, 14, 0.)? as f32;
6500                let mut gfx = self.gfx.borrow_mut();
6501                let cam = gfx.camera.clone();
6502                crate::gfx::vtex::draw_grid(
6503                    &mut gfx.depth_queue,
6504                    &cam,
6505                    cx,
6506                    cy,
6507                    cz,
6508                    ux,
6509                    uy,
6510                    uz,
6511                    vx,
6512                    vy,
6513                    vz,
6514                    cols,
6515                    rows,
6516                    cw,
6517                    ch,
6518                    fr,
6519                    hue,
6520                );
6521                return Ok(Value::Unit);
6522            },
6523
6524            // vtex_rings(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_rings,n_sides, max_r,twist, fr,hue)
6525            "vtex_rings" | "ลายวงซ้อน" | "纹环" | "同心円" | "동심원" => {
6526                let cx = self.arg_num(&args, 0, 0.)? as f32;
6527                let cy = self.arg_num(&args, 1, 0.)? as f32;
6528                let cz = self.arg_num(&args, 2, 0.)? as f32;
6529                let ux = self.arg_num(&args, 3, 1.)? as f32;
6530                let uy = self.arg_num(&args, 4, 0.)? as f32;
6531                let uz = self.arg_num(&args, 5, 0.)? as f32;
6532                let vx = self.arg_num(&args, 6, 0.)? as f32;
6533                let vy = self.arg_num(&args, 7, 0.)? as f32;
6534                let vz = self.arg_num(&args, 8, 1.)? as f32;
6535                let nr = self.arg_num(&args, 9, 6.)? as usize;
6536                let ns = self.arg_num(&args, 10, 6.)? as usize;
6537                let mr = self.arg_num(&args, 11, 3.)? as f32;
6538                let tw = self.arg_num(&args, 12, 0.)? as f32;
6539                let fr = self.arg_num(&args, 13, 0.)? as f32;
6540                let hue = self.arg_num(&args, 14, 0.)? as f32;
6541                let mut gfx = self.gfx.borrow_mut();
6542                let cam = gfx.camera.clone();
6543                crate::gfx::vtex::draw_rings(
6544                    &mut gfx.depth_queue,
6545                    &cam,
6546                    cx,
6547                    cy,
6548                    cz,
6549                    ux,
6550                    uy,
6551                    uz,
6552                    vx,
6553                    vy,
6554                    vz,
6555                    nr,
6556                    ns,
6557                    mr,
6558                    tw,
6559                    fr,
6560                    hue,
6561                );
6562                return Ok(Value::Unit);
6563            },
6564
6565            // vtex_star(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_pts,r_out,r_in, rot_speed, fr,hue)
6566            "vtex_star" | "ลายดาว" | "纹星" | "星模様" | "별무늬" => {
6567                let cx = self.arg_num(&args, 0, 0.)? as f32;
6568                let cy = self.arg_num(&args, 1, 0.)? as f32;
6569                let cz = self.arg_num(&args, 2, 0.)? as f32;
6570                let ux = self.arg_num(&args, 3, 1.)? as f32;
6571                let uy = self.arg_num(&args, 4, 0.)? as f32;
6572                let uz = self.arg_num(&args, 5, 0.)? as f32;
6573                let vx = self.arg_num(&args, 6, 0.)? as f32;
6574                let vy = self.arg_num(&args, 7, 0.)? as f32;
6575                let vz = self.arg_num(&args, 8, 1.)? as f32;
6576                let np = self.arg_num(&args, 9, 6.)? as usize;
6577                let ro = self.arg_num(&args, 10, 2.)? as f32;
6578                let ri = self.arg_num(&args, 11, 1.)? as f32;
6579                let rs = self.arg_num(&args, 12, 0.01)? as f32;
6580                let fr = self.arg_num(&args, 13, 0.)? as f32;
6581                let hue = self.arg_num(&args, 14, 0.)? as f32;
6582                let mut gfx = self.gfx.borrow_mut();
6583                let cam = gfx.camera.clone();
6584                crate::gfx::vtex::draw_star(
6585                    &mut gfx.depth_queue,
6586                    &cam,
6587                    cx,
6588                    cy,
6589                    cz,
6590                    ux,
6591                    uy,
6592                    uz,
6593                    vx,
6594                    vy,
6595                    vz,
6596                    np,
6597                    ro,
6598                    ri,
6599                    rs,
6600                    fr,
6601                    hue,
6602                );
6603                return Ok(Value::Unit);
6604            },
6605
6606            // vtex_spiral(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_turns,max_r,steps, fr,hue)
6607            "vtex_spiral" | "ลายเกลียว" | "纹螺" | "螺旋" | "나선" => {
6608                let cx = self.arg_num(&args, 0, 0.)? as f32;
6609                let cy = self.arg_num(&args, 1, 0.)? as f32;
6610                let cz = self.arg_num(&args, 2, 0.)? as f32;
6611                let ux = self.arg_num(&args, 3, 1.)? as f32;
6612                let uy = self.arg_num(&args, 4, 0.)? as f32;
6613                let uz = self.arg_num(&args, 5, 0.)? as f32;
6614                let vx = self.arg_num(&args, 6, 0.)? as f32;
6615                let vy = self.arg_num(&args, 7, 0.)? as f32;
6616                let vz = self.arg_num(&args, 8, 1.)? as f32;
6617                let nt = self.arg_num(&args, 9, 3.)? as f32;
6618                let mr = self.arg_num(&args, 10, 3.)? as f32;
6619                let st = self.arg_num(&args, 11, 120.)? as usize;
6620                let fr = self.arg_num(&args, 12, 0.)? as f32;
6621                let hue = self.arg_num(&args, 13, 0.)? as f32;
6622                let mut gfx = self.gfx.borrow_mut();
6623                let cam = gfx.camera.clone();
6624                crate::gfx::vtex::draw_spiral(
6625                    &mut gfx.depth_queue,
6626                    &cam,
6627                    cx,
6628                    cy,
6629                    cz,
6630                    ux,
6631                    uy,
6632                    uz,
6633                    vx,
6634                    vy,
6635                    vz,
6636                    nt,
6637                    mr,
6638                    st,
6639                    fr,
6640                    hue,
6641                );
6642                return Ok(Value::Unit);
6643            },
6644
6645            // vtex_flower(cx,cy,cz, ux,uy,uz, vx,vy,vz, radius,n_sides, fr,hue)
6646            "vtex_flower" | "ลายดอก" | "纹花" | "花模様" | "꽃무늬" => {
6647                let cx = self.arg_num(&args, 0, 0.)? as f32;
6648                let cy = self.arg_num(&args, 1, 0.)? as f32;
6649                let cz = self.arg_num(&args, 2, 0.)? as f32;
6650                let ux = self.arg_num(&args, 3, 1.)? as f32;
6651                let uy = self.arg_num(&args, 4, 0.)? as f32;
6652                let uz = self.arg_num(&args, 5, 0.)? as f32;
6653                let vx = self.arg_num(&args, 6, 0.)? as f32;
6654                let vy = self.arg_num(&args, 7, 0.)? as f32;
6655                let vz = self.arg_num(&args, 8, 1.)? as f32;
6656                let r = self.arg_num(&args, 9, 1.)? as f32;
6657                let ns = self.arg_num(&args, 10, 24.)? as usize;
6658                let fr = self.arg_num(&args, 11, 0.)? as f32;
6659                let hue = self.arg_num(&args, 12, 0.)? as f32;
6660                let mut gfx = self.gfx.borrow_mut();
6661                let cam = gfx.camera.clone();
6662                crate::gfx::vtex::draw_flower(
6663                    &mut gfx.depth_queue,
6664                    &cam,
6665                    cx,
6666                    cy,
6667                    cz,
6668                    ux,
6669                    uy,
6670                    uz,
6671                    vx,
6672                    vy,
6673                    vz,
6674                    r,
6675                    ns,
6676                    fr,
6677                    hue,
6678                );
6679                return Ok(Value::Unit);
6680            },
6681
6682            // vtex_letter_rain(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_cols,n_vis, col_w,row_h, speed, fr,hue)
6683            "vtex_letter_rain" | "ลายอักษรไหล" | "纹字雨" | "文字雨" | "글자비" =>
6684            {
6685                let cx = self.arg_num(&args, 0, 0.)? as f32;
6686                let cy = self.arg_num(&args, 1, 0.)? as f32;
6687                let cz = self.arg_num(&args, 2, 0.)? as f32;
6688                let ux = self.arg_num(&args, 3, 1.)? as f32;
6689                let uy = self.arg_num(&args, 4, 0.)? as f32;
6690                let uz = self.arg_num(&args, 5, 0.)? as f32;
6691                let vx = self.arg_num(&args, 6, 0.)? as f32;
6692                let vy = self.arg_num(&args, 7, 0.)? as f32;
6693                let vz = self.arg_num(&args, 8, 1.)? as f32;
6694                let nc = self.arg_num(&args, 9, 16.)? as usize;
6695                let nv = self.arg_num(&args, 10, 14.)? as usize;
6696                let cw = self.arg_num(&args, 11, 0.65)? as f32;
6697                let rh = self.arg_num(&args, 12, 0.60)? as f32;
6698                let sp = self.arg_num(&args, 13, 0.025)? as f32;
6699                let fr = self.arg_num(&args, 14, 0.)? as f32;
6700                let hue = self.arg_num(&args, 15, 0.)? as f32;
6701                let mut gfx = self.gfx.borrow_mut();
6702                let cam = gfx.camera.clone();
6703                crate::gfx::vtex::draw_letter_rain(
6704                    &mut gfx.depth_queue,
6705                    &cam,
6706                    cx,
6707                    cy,
6708                    cz,
6709                    ux,
6710                    uy,
6711                    uz,
6712                    vx,
6713                    vy,
6714                    vz,
6715                    nc,
6716                    nv,
6717                    cw,
6718                    rh,
6719                    sp,
6720                    fr,
6721                    hue,
6722                );
6723                return Ok(Value::Unit);
6724            },
6725
6726            // vtex_hyperbolic_uv(cx,cy,cz, ux,uy,uz, vx,vy,vz, max_r,n_circles,n_rays, fr,hue)
6727            "vtex_hyperbolic_uv" | "ลายไฮเพอร์โบลิก" | "纹曲面" | "双曲線" | "쌍곡선" =>
6728            {
6729                let cx = self.arg_num(&args, 0, 0.)? as f32;
6730                let cy = self.arg_num(&args, 1, 0.)? as f32;
6731                let cz = self.arg_num(&args, 2, 0.)? as f32;
6732                let ux = self.arg_num(&args, 3, 1.)? as f32;
6733                let uy = self.arg_num(&args, 4, 0.)? as f32;
6734                let uz = self.arg_num(&args, 5, 0.)? as f32;
6735                let vx = self.arg_num(&args, 6, 0.)? as f32;
6736                let vy = self.arg_num(&args, 7, 0.)? as f32;
6737                let vz = self.arg_num(&args, 8, 1.)? as f32;
6738                let mr = self.arg_num(&args, 9, 5.)? as f32;
6739                let nc = self.arg_num(&args, 10, 12.)? as usize;
6740                let nr = self.arg_num(&args, 11, 18.)? as usize;
6741                let fr = self.arg_num(&args, 12, 0.)? as f32;
6742                let hue = self.arg_num(&args, 13, 0.)? as f32;
6743                let mut gfx = self.gfx.borrow_mut();
6744                let cam = gfx.camera.clone();
6745                crate::gfx::vtex::draw_hyperbolic_uv(
6746                    &mut gfx.depth_queue,
6747                    &cam,
6748                    cx,
6749                    cy,
6750                    cz,
6751                    ux,
6752                    uy,
6753                    uz,
6754                    vx,
6755                    vy,
6756                    vz,
6757                    mr,
6758                    nc,
6759                    nr,
6760                    fr,
6761                    hue,
6762                );
6763                return Ok(Value::Unit);
6764            },
6765
6766            // vtex_halftone(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cell_w,cell_h, density, fr,hue)
6767            "vtex_halftone" | "ลายจุด" | "纹半调" | "網点模様" | "망점" => {
6768                let cx = self.arg_num(&args, 0, 0.)? as f32;
6769                let cy = self.arg_num(&args, 1, 0.)? as f32;
6770                let cz = self.arg_num(&args, 2, 0.)? as f32;
6771                let ux = self.arg_num(&args, 3, 1.)? as f32;
6772                let uy = self.arg_num(&args, 4, 0.)? as f32;
6773                let uz = self.arg_num(&args, 5, 0.)? as f32;
6774                let vx = self.arg_num(&args, 6, 0.)? as f32;
6775                let vy = self.arg_num(&args, 7, 0.)? as f32;
6776                let vz = self.arg_num(&args, 8, 1.)? as f32;
6777                let cols = self.arg_num(&args, 9, 16.)? as usize;
6778                let rows = self.arg_num(&args, 10, 12.)? as usize;
6779                let cw = self.arg_num(&args, 11, 0.5)? as f32;
6780                let ch = self.arg_num(&args, 12, 0.5)? as f32;
6781                let dens = self.arg_num(&args, 13, 0.4)? as f32;
6782                let fr = self.arg_num(&args, 14, 0.)? as f32;
6783                let hue = self.arg_num(&args, 15, 0.)? as f32;
6784                let mut gfx = self.gfx.borrow_mut();
6785                let cam = gfx.camera.clone();
6786                crate::gfx::vtex::draw_halftone(
6787                    &mut gfx.depth_queue,
6788                    &cam,
6789                    cx,
6790                    cy,
6791                    cz,
6792                    ux,
6793                    uy,
6794                    uz,
6795                    vx,
6796                    vy,
6797                    vz,
6798                    cols,
6799                    rows,
6800                    cw,
6801                    ch,
6802                    dens,
6803                    fr,
6804                    hue,
6805                );
6806                return Ok(Value::Unit);
6807            },
6808
6809            // vtex_tessellated(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cell, amplitude,freq, fr,hue)
6810            "vtex_tessellated" | "ลายตาข่าย" | "纹镶嵌" | "網目模様" | "격자망" =>
6811            {
6812                let cx = self.arg_num(&args, 0, 0.)? as f32;
6813                let cy = self.arg_num(&args, 1, 0.)? as f32;
6814                let cz = self.arg_num(&args, 2, 0.)? as f32;
6815                let ux = self.arg_num(&args, 3, 1.)? as f32;
6816                let uy = self.arg_num(&args, 4, 0.)? as f32;
6817                let uz = self.arg_num(&args, 5, 0.)? as f32;
6818                let vx = self.arg_num(&args, 6, 0.)? as f32;
6819                let vy = self.arg_num(&args, 7, 0.)? as f32;
6820                let vz = self.arg_num(&args, 8, 1.)? as f32;
6821                let cols = self.arg_num(&args, 9, 14.)? as usize;
6822                let rows = self.arg_num(&args, 10, 10.)? as usize;
6823                let cell = self.arg_num(&args, 11, 0.6)? as f32;
6824                let amp = self.arg_num(&args, 12, 0.25)? as f32;
6825                let freq = self.arg_num(&args, 13, 4.)? as f32;
6826                let fr = self.arg_num(&args, 14, 0.)? as f32;
6827                let hue = self.arg_num(&args, 15, 0.)? as f32;
6828                let mut gfx = self.gfx.borrow_mut();
6829                let cam = gfx.camera.clone();
6830                crate::gfx::vtex::draw_tessellated(
6831                    &mut gfx.depth_queue,
6832                    &cam,
6833                    cx,
6834                    cy,
6835                    cz,
6836                    ux,
6837                    uy,
6838                    uz,
6839                    vx,
6840                    vy,
6841                    vz,
6842                    cols,
6843                    rows,
6844                    cell,
6845                    amp,
6846                    freq,
6847                    fr,
6848                    hue,
6849                );
6850                return Ok(Value::Unit);
6851            },
6852
6853            // vtex_lotus(cx,cy,cz, ux,uy,uz, vx,vy,vz, r_inner,r_outer,n_petals, fr,hue)
6854            "vtex_lotus" | "ลายดอกบัว" | "纹莲" | "蓮模様" | "연꽃무늬" =>
6855            {
6856                let cx = self.arg_num(&args, 0, 0.)? as f32;
6857                let cy = self.arg_num(&args, 1, 0.)? as f32;
6858                let cz = self.arg_num(&args, 2, 0.)? as f32;
6859                let ux = self.arg_num(&args, 3, 1.)? as f32;
6860                let uy = self.arg_num(&args, 4, 0.)? as f32;
6861                let uz = self.arg_num(&args, 5, 0.)? as f32;
6862                let vx = self.arg_num(&args, 6, 0.)? as f32;
6863                let vy = self.arg_num(&args, 7, 0.)? as f32;
6864                let vz = self.arg_num(&args, 8, 1.)? as f32;
6865                let ri = self.arg_num(&args, 9, 1.)? as f32;
6866                let ro = self.arg_num(&args, 10, 2.)? as f32;
6867                let np = self.arg_num(&args, 11, 12.)? as usize;
6868                let fr = self.arg_num(&args, 12, 0.)? as f32;
6869                let hue = self.arg_num(&args, 13, 0.)? as f32;
6870                let mut gfx = self.gfx.borrow_mut();
6871                let cam = gfx.camera.clone();
6872                crate::gfx::vtex::draw_lotus(
6873                    &mut gfx.depth_queue,
6874                    &cam,
6875                    cx,
6876                    cy,
6877                    cz,
6878                    ux,
6879                    uy,
6880                    uz,
6881                    vx,
6882                    vy,
6883                    vz,
6884                    ri,
6885                    ro,
6886                    np,
6887                    fr,
6888                    hue,
6889                );
6890                return Ok(Value::Unit);
6891            },
6892
6893            // vtex_chakra(cx,cy,cz, ux,uy,uz, vx,vy,vz, r,n_spokes, fr,hue)
6894            "vtex_chakra" | "ลายจักร" | "纹轮" | "輪模様" | "바퀴무늬" => {
6895                let cx = self.arg_num(&args, 0, 0.)? as f32;
6896                let cy = self.arg_num(&args, 1, 0.)? as f32;
6897                let cz = self.arg_num(&args, 2, 0.)? as f32;
6898                let ux = self.arg_num(&args, 3, 1.)? as f32;
6899                let uy = self.arg_num(&args, 4, 0.)? as f32;
6900                let uz = self.arg_num(&args, 5, 0.)? as f32;
6901                let vx = self.arg_num(&args, 6, 0.)? as f32;
6902                let vy = self.arg_num(&args, 7, 0.)? as f32;
6903                let vz = self.arg_num(&args, 8, 1.)? as f32;
6904                let r = self.arg_num(&args, 9, 2.)? as f32;
6905                let ns = self.arg_num(&args, 10, 8.)? as usize;
6906                let fr = self.arg_num(&args, 11, 0.)? as f32;
6907                let hue = self.arg_num(&args, 12, 0.)? as f32;
6908                let mut gfx = self.gfx.borrow_mut();
6909                let cam = gfx.camera.clone();
6910                crate::gfx::vtex::draw_chakra(
6911                    &mut gfx.depth_queue,
6912                    &cam,
6913                    cx,
6914                    cy,
6915                    cz,
6916                    ux,
6917                    uy,
6918                    uz,
6919                    vx,
6920                    vy,
6921                    vz,
6922                    r,
6923                    ns,
6924                    fr,
6925                    hue,
6926                );
6927                return Ok(Value::Unit);
6928            },
6929
6930            // vtex_yantra(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_layers,max_r, fr,hue)
6931            "vtex_yantra" | "ลายยันต์" | "纹咒" | "護符模様" | "부적무늬" =>
6932            {
6933                let cx = self.arg_num(&args, 0, 0.)? as f32;
6934                let cy = self.arg_num(&args, 1, 0.)? as f32;
6935                let cz = self.arg_num(&args, 2, 0.)? as f32;
6936                let ux = self.arg_num(&args, 3, 1.)? as f32;
6937                let uy = self.arg_num(&args, 4, 0.)? as f32;
6938                let uz = self.arg_num(&args, 5, 0.)? as f32;
6939                let vx = self.arg_num(&args, 6, 0.)? as f32;
6940                let vy = self.arg_num(&args, 7, 0.)? as f32;
6941                let vz = self.arg_num(&args, 8, 1.)? as f32;
6942                let nl = self.arg_num(&args, 9, 4.)? as usize;
6943                let mr = self.arg_num(&args, 10, 3.)? as f32;
6944                let fr = self.arg_num(&args, 11, 0.)? as f32;
6945                let hue = self.arg_num(&args, 12, 0.)? as f32;
6946                let mut gfx = self.gfx.borrow_mut();
6947                let cam = gfx.camera.clone();
6948                crate::gfx::vtex::draw_yantra(
6949                    &mut gfx.depth_queue,
6950                    &cam,
6951                    cx,
6952                    cy,
6953                    cz,
6954                    ux,
6955                    uy,
6956                    uz,
6957                    vx,
6958                    vy,
6959                    vz,
6960                    nl,
6961                    mr,
6962                    fr,
6963                    hue,
6964                );
6965                return Ok(Value::Unit);
6966            },
6967
6968            // vtex_spiked_cog(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_teeth,r_body,r_spike,r_hub,n_spokes, fr,hue)
6969            "vtex_spiked_cog" | "ฟันเฟืองหนาม" | "纹棘轮" | "歯車模様" | "톱니바퀴" =>
6970            {
6971                let cx = self.arg_num(&args, 0, 0.)? as f32;
6972                let cy = self.arg_num(&args, 1, 0.)? as f32;
6973                let cz = self.arg_num(&args, 2, 0.)? as f32;
6974                let ux = self.arg_num(&args, 3, 1.)? as f32;
6975                let uy = self.arg_num(&args, 4, 0.)? as f32;
6976                let uz = self.arg_num(&args, 5, 0.)? as f32;
6977                let vx = self.arg_num(&args, 6, 0.)? as f32;
6978                let vy = self.arg_num(&args, 7, 0.)? as f32;
6979                let vz = self.arg_num(&args, 8, 1.)? as f32;
6980                let nt = self.arg_num(&args, 9, 12.)? as usize;
6981                let rb = self.arg_num(&args, 10, 1.)? as f32;
6982                let rs = self.arg_num(&args, 11, 1.3)? as f32;
6983                let rh = self.arg_num(&args, 12, 0.2)? as f32;
6984                let ns = self.arg_num(&args, 13, 6.)? as usize;
6985                let fr = self.arg_num(&args, 14, 0.)? as f32;
6986                let hue = self.arg_num(&args, 15, 0.)? as f32;
6987                let mut gfx = self.gfx.borrow_mut();
6988                let cam = gfx.camera.clone();
6989                crate::gfx::vtex::draw_spiked_cog(
6990                    &mut gfx.depth_queue,
6991                    &cam,
6992                    cx,
6993                    cy,
6994                    cz,
6995                    ux,
6996                    uy,
6997                    uz,
6998                    vx,
6999                    vy,
7000                    vz,
7001                    nt,
7002                    rb,
7003                    rs,
7004                    rh,
7005                    ns,
7006                    fr,
7007                    hue,
7008                );
7009                return Ok(Value::Unit);
7010            },
7011
7012            // vtex_torii(cx,cy,cz, ux,uy,uz, vx,vy,vz, width,height, fr,hue)
7013            "vtex_torii" | "ประตูโทริอิ" | "纹鸟居" | "鳥居" | "도리이" =>
7014            {
7015                let cx = self.arg_num(&args, 0, 0.)? as f32;
7016                let cy = self.arg_num(&args, 1, 0.)? as f32;
7017                let cz = self.arg_num(&args, 2, 0.)? as f32;
7018                let ux = self.arg_num(&args, 3, 1.)? as f32;
7019                let uy = self.arg_num(&args, 4, 0.)? as f32;
7020                let uz = self.arg_num(&args, 5, 0.)? as f32;
7021                let vx = self.arg_num(&args, 6, 0.)? as f32;
7022                let vy = self.arg_num(&args, 7, 0.)? as f32;
7023                let vz = self.arg_num(&args, 8, 1.)? as f32;
7024                let w = self.arg_num(&args, 9, 4.)? as f32;
7025                let h = self.arg_num(&args, 10, 5.)? as f32;
7026                let fr = self.arg_num(&args, 11, 0.)? as f32;
7027                let hue = self.arg_num(&args, 12, 0.)? as f32;
7028                let mut gfx = self.gfx.borrow_mut();
7029                let cam = gfx.camera.clone();
7030                crate::gfx::vtex::draw_torii(
7031                    &mut gfx.depth_queue,
7032                    &cam,
7033                    cx,
7034                    cy,
7035                    cz,
7036                    ux,
7037                    uy,
7038                    uz,
7039                    vx,
7040                    vy,
7041                    vz,
7042                    w,
7043                    h,
7044                    fr,
7045                    hue,
7046                );
7047                return Ok(Value::Unit);
7048            },
7049
7050            // vtex_pagoda(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_tiers,base_w,tier_h,taper,eave_out, fr,hue)
7051            "vtex_pagoda" | "เจดีย์" | "纹塔" | "塔" | "탑" => {
7052                let cx = self.arg_num(&args, 0, 0.)? as f32;
7053                let cy = self.arg_num(&args, 1, 0.)? as f32;
7054                let cz = self.arg_num(&args, 2, 0.)? as f32;
7055                let ux = self.arg_num(&args, 3, 1.)? as f32;
7056                let uy = self.arg_num(&args, 4, 0.)? as f32;
7057                let uz = self.arg_num(&args, 5, 0.)? as f32;
7058                let vx = self.arg_num(&args, 6, 0.)? as f32;
7059                let vy = self.arg_num(&args, 7, 0.)? as f32;
7060                let vz = self.arg_num(&args, 8, 1.)? as f32;
7061                let nt = self.arg_num(&args, 9, 5.)? as usize;
7062                let bw = self.arg_num(&args, 10, 2.)? as f32;
7063                let th = self.arg_num(&args, 11, 1.)? as f32;
7064                let tp = self.arg_num(&args, 12, 0.72)? as f32;
7065                let eo = self.arg_num(&args, 13, 0.28)? as f32;
7066                let fr = self.arg_num(&args, 14, 0.)? as f32;
7067                let hue = self.arg_num(&args, 15, 0.)? as f32;
7068                let mut gfx = self.gfx.borrow_mut();
7069                let cam = gfx.camera.clone();
7070                crate::gfx::vtex::draw_pagoda(
7071                    &mut gfx.depth_queue,
7072                    &cam,
7073                    cx,
7074                    cy,
7075                    cz,
7076                    ux,
7077                    uy,
7078                    uz,
7079                    vx,
7080                    vy,
7081                    vz,
7082                    nt,
7083                    bw,
7084                    th,
7085                    tp,
7086                    eo,
7087                    fr,
7088                    hue,
7089                );
7090                return Ok(Value::Unit);
7091            },
7092
7093            // ══════════════════════════════════════════════════════════════════
7094            // AUDIO BUILTINS
7095            // ══════════════════════════════════════════════════════════════════
7096
7097            // audio_tone(idx, x, y, z, w, freq, amp, lfo_rate, lfo_depth)
7098            #[cfg(not(target_arch = "wasm32"))]
7099            "audio_tone"
7100            | "เสียงโทน"
7101            | "音调"
7102            | "音調"
7103            | "음조"
7104            | "空间音"
7105            | "空間音"
7106            | "공간음" => {
7107                let idx = self.arg_num(&args, 0, 0.0)? as usize;
7108                let x = self.arg_num(&args, 1, 0.0)? as f32;
7109                let y = self.arg_num(&args, 2, 0.0)? as f32;
7110                let z = self.arg_num(&args, 3, 0.0)? as f32;
7111                let w = self.arg_num(&args, 4, 1.0)? as f32;
7112                let freq = self.arg_num(&args, 5, 220.0)? as f32;
7113                let amp = self.arg_num(&args, 6, 0.15)? as f32;
7114                let lfo_rate = self.arg_num(&args, 7, 0.5)? as f32;
7115                let lfo_depth = self.arg_num(&args, 8, 0.02)? as f32;
7116                if let Some(audio) = &self.audio {
7117                    audio.set_tone(
7118                        idx,
7119                        ToneParams { x, y, z, w, freq, amp, lfo_rate, lfo_depth },
7120                    );
7121                }
7122                return Ok(Value::Unit);
7123            },
7124
7125            #[cfg(not(target_arch = "wasm32"))]
7126            "audio_listener" | "ผู้ฟัง" | "音频监听" | "音声リスナー" | "오디오리스너" =>
7127            {
7128                let cry = self.arg_num(&args, 0, 1.0)? as f32;
7129                let sry = self.arg_num(&args, 1, 0.0)? as f32;
7130                let crx = self.arg_num(&args, 2, 1.0)? as f32;
7131                let srx = self.arg_num(&args, 3, 0.0)? as f32;
7132                if let Some(audio) = &self.audio {
7133                    audio.set_listener(cry, sry, crx, srx);
7134                }
7135                return Ok(Value::Unit);
7136            },
7137
7138            #[cfg(not(target_arch = "wasm32"))]
7139            "audio_bgm" | "เพลงพื้นหลัง" | "เพลงประกอบ" | "背景乐" | "BGM" | "배경음악" =>
7140            {
7141                let path = match args.first() {
7142                    Some(Value::Str(s)) => s.clone(),
7143                    _ => return Ok(Value::Unit),
7144                };
7145                let vol = self.arg_num(&args, 1, 0.5)? as f32;
7146                if let Some(audio) = &self.audio {
7147                    audio.load_bgm(&path, vol);
7148                }
7149                return Ok(Value::Unit);
7150            },
7151
7152            #[cfg(not(target_arch = "wasm32"))]
7153            "audio_bgm_volume"
7154            | "ระดับเสียงพื้นหลัง"
7155            | "ระดับเพลงประกอบ"
7156            | "背景乐音量"
7157            | "BGM音量"
7158            | "배경음악음량" => {
7159                let vol = self.arg_num(&args, 0, 0.5)? as f32;
7160                if let Some(audio) = &self.audio {
7161                    audio.set_bgm_volume(vol);
7162                }
7163                return Ok(Value::Unit);
7164            },
7165
7166            #[cfg(not(target_arch = "wasm32"))]
7167            "audio_volume" | "ระดับเสียง" | "音量" | "음량" => {
7168                let vol = self.arg_num(&args, 0, 0.7)? as f32;
7169                if let Some(audio) = &self.audio {
7170                    audio.set_master_volume(vol);
7171                }
7172                return Ok(Value::Unit);
7173            },
7174
7175            // WASM audio builtins — delegate to Web Audio API
7176            #[cfg(target_arch = "wasm32")]
7177            "audio_tone"
7178            | "เสียงโทน"
7179            | "音调"
7180            | "音調"
7181            | "음조"
7182            | "空间音"
7183            | "空間音"
7184            | "공간음" => {
7185                let idx = self.arg_num(&args, 0, 0.0)? as usize;
7186                let x = self.arg_num(&args, 1, 0.0)? as f32;
7187                let y = self.arg_num(&args, 2, 0.0)? as f32;
7188                let z = self.arg_num(&args, 3, 0.0)? as f32;
7189                let w = self.arg_num(&args, 4, 1.0)? as f32;
7190                let freq = self.arg_num(&args, 5, 220.0)? as f32;
7191                let amp = self.arg_num(&args, 6, 0.15)? as f32;
7192                let lfo_rate = self.arg_num(&args, 7, 0.5)? as f32;
7193                let lfo_depth = self.arg_num(&args, 8, 0.02)? as f32;
7194                crate::gfx::audio_web::set_tone(idx, x, y, z, w, freq, amp, lfo_rate, lfo_depth);
7195                return Ok(Value::Unit);
7196            },
7197
7198            #[cfg(target_arch = "wasm32")]
7199            "audio_listener" | "ผู้ฟัง" | "音频监听" | "音声リスナー" | "오디오리스너" =>
7200            {
7201                let cry = self.arg_num(&args, 0, 1.0)? as f32;
7202                let sry = self.arg_num(&args, 1, 0.0)? as f32;
7203                let crx = self.arg_num(&args, 2, 1.0)? as f32;
7204                let srx = self.arg_num(&args, 3, 0.0)? as f32;
7205                crate::gfx::audio_web::set_listener(cry, sry, crx, srx);
7206                return Ok(Value::Unit);
7207            },
7208
7209            #[cfg(target_arch = "wasm32")]
7210            "audio_bgm" | "เพลงพื้นหลัง" | "เพลงประกอบ" | "背景乐" | "BGM" | "배경음악" =>
7211            {
7212                let path = self.arg_str(&args, 0, "");
7213                let vol = self.arg_num(&args, 1, 0.5)? as f32;
7214                crate::gfx::audio_web::load_bgm(&path, vol);
7215                return Ok(Value::Unit);
7216            },
7217
7218            #[cfg(target_arch = "wasm32")]
7219            "audio_bgm_volume"
7220            | "ระดับเสียงพื้นหลัง"
7221            | "ระดับเพลงประกอบ"
7222            | "背景乐音量"
7223            | "BGM音量"
7224            | "배경음악음량" => {
7225                let vol = self.arg_num(&args, 0, 0.5)? as f32;
7226                crate::gfx::audio_web::set_bgm_volume(vol);
7227                return Ok(Value::Unit);
7228            },
7229
7230            #[cfg(target_arch = "wasm32")]
7231            "audio_volume" | "ระดับเสียง" | "音量" | "음량" => {
7232                let vol = self.arg_num(&args, 0, 0.7)? as f32;
7233                crate::gfx::audio_web::set_master_volume(vol);
7234                return Ok(Value::Unit);
7235            },
7236
7237            // ── WASM sample load / play / stop / FX (Web Audio pool) ─────────
7238            #[cfg(target_arch = "wasm32")]
7239            "audio_sample_load" | "载入采样" | "サンプル読込" | "샘플로드" | "โหลดตัวอย่างเสียง" =>
7240            {
7241                let path = self.arg_str(&args, 0, "");
7242                let resolved = self.wasm_resolve_source_path(&path);
7243                match wasm_fetch_bytes(&resolved)
7244                    .and_then(|bytes| ling_music::from_bytes(&bytes).map_err(|e| e.to_string()))
7245                {
7246                    Ok(t) => {
7247                        let id = crate::gfx::audio_web::add_sample(&t.stereo, t.channels, t.rate);
7248                        return Ok(Value::Number(id as f64));
7249                    },
7250                    Err(e) => {
7251                        eprintln!("audio_sample_load failed ({path}): {e}");
7252                        return Ok(Value::Number(-1.0));
7253                    },
7254                }
7255            },
7256            #[cfg(target_arch = "wasm32")]
7257            "audio_sample_play" | "播放采样" | "サンプル再生" | "샘플재생" | "เล่นตัวอย่างเสียง" =>
7258            {
7259                let id = self.arg_num(&args, 0, 0.0)? as usize;
7260                let x = self.arg_num(&args, 1, 0.0)? as f32;
7261                let y = self.arg_num(&args, 2, 0.0)? as f32;
7262                let z = self.arg_num(&args, 3, 0.0)? as f32;
7263                // arg 4 is w (4th spatial dim) — ignored for 3-D panner
7264                let vol = self.arg_num(&args, 5, 1.0)? as f32;
7265                let looping = self.arg_num(&args, 6, 0.0)? > 0.5;
7266                crate::gfx::audio_web::play_sample(id, x, y, z, vol, looping);
7267                return Ok(Value::Number(0.0));
7268            },
7269            #[cfg(target_arch = "wasm32")]
7270            "audio_sample_stop"
7271            | "停止采样"
7272            | "サンプル停止"
7273            | "샘플정지"
7274            | "หยุดตัวอย่างเสียง"
7275            | "audio_fx_reverb"
7276            | "混响"
7277            | "リバーブ"
7278            | "리버브"
7279            | "เสียงก้อง"
7280            | "audio_fx_delay"
7281            | "回声"
7282            | "ディレイ効果"
7283            | "딜레이"
7284            | "เสียงสะท้อน"
7285            | "audio_fx_lowpass"
7286            | "低通滤波"
7287            | "ローパス"
7288            | "저역통과"
7289            | "กรองความถี่ต่ำ" => {
7290                return Ok(Value::Unit);
7291            },
7292
7293            // ── รอหน้าต่าง() — block until window closed / Escape ──
7294            "รอหน้าต่าง" | "wait_window" | "gfx_wait" => {
7295                #[cfg(not(target_arch = "wasm32"))]
7296                loop {
7297                    let still_open = {
7298                        let gfx = self.gfx.borrow();
7299                        gfx.window
7300                            .as_ref()
7301                            .map(|w| w.is_open() && !w.is_key_down(minifb::Key::Escape))
7302                            .unwrap_or(false)
7303                    };
7304                    if !still_open {
7305                        break;
7306                    }
7307                    let (buf, w, h) = {
7308                        let gfx = self.gfx.borrow();
7309                        (gfx.buffer.clone(), gfx.width, gfx.height)
7310                    };
7311                    let mut gfx = self.gfx.borrow_mut();
7312                    if let Some(win) = gfx.window.as_mut() {
7313                        if win.update_with_buffer(&buf, w, h).is_err() {
7314                            break;
7315                        }
7316                    }
7317                }
7318                return Ok(Value::Unit);
7319            },
7320
7321            // ── File I/O ──────────────────────────────────────────────────────
7322            "read_file" | "อ่านไฟล์" => {
7323                #[cfg(target_arch = "wasm32")]
7324                return Ok(Value::Str(String::new()));
7325                #[cfg(not(target_arch = "wasm32"))]
7326                {
7327                    let path = self.arg_str(&args, 0, "");
7328                    return std::fs::read_to_string(&path)
7329                        .map(Value::Str)
7330                        .map_err(|e| EvalErr::from(format!("read_file '{path}': {e}")));
7331                }
7332            },
7333            // ── networking (TCP, 2-peer co-op) ───────────────────────────────
7334            #[cfg(not(target_arch = "wasm32"))]
7335            "net_host" | "เน็ตโฮสต์" => {
7336                let port = self.arg_num(&args, 0, 7777.0)? as u16;
7337                net::host(port);
7338                return Ok(Value::Unit);
7339            },
7340            #[cfg(not(target_arch = "wasm32"))]
7341            "net_join" | "เน็ตจอย" => {
7342                let ip = self.arg_str(&args, 0, "127.0.0.1");
7343                let port = self.arg_num(&args, 1, 7777.0)? as u16;
7344                net::join(&ip, port);
7345                return Ok(Value::Unit);
7346            },
7347            #[cfg(not(target_arch = "wasm32"))]
7348            "net_send" | "เน็ตส่ง" => {
7349                let s = self.arg_str(&args, 0, "");
7350                net::send(&s);
7351                return Ok(Value::Unit);
7352            },
7353            #[cfg(not(target_arch = "wasm32"))]
7354            "net_recv" | "เน็ตรับ" => {
7355                return Ok(Value::Str(net::recv()));
7356            },
7357            #[cfg(not(target_arch = "wasm32"))]
7358            "net_status" | "เน็ตสถานะ" => {
7359                return Ok(Value::Number(net::status() as f64));
7360            },
7361            #[cfg(not(target_arch = "wasm32"))]
7362            "net_recv_from" => {
7363                return Ok(Value::Str(net::recv_from()));
7364            },
7365            #[cfg(not(target_arch = "wasm32"))]
7366            "net_send_to" => {
7367                let id = self.arg_num(&args, 0, 0.0)? as u64;
7368                let s = self.arg_str(&args, 1, "");
7369                net::send_to(id, &s);
7370                return Ok(Value::Unit);
7371            },
7372            #[cfg(not(target_arch = "wasm32"))]
7373            "net_close" | "연결종료" => {
7374                net::close();
7375                return Ok(Value::Unit);
7376            },
7377            // ── LAN lobby discovery (UDP broadcast) ──
7378            #[cfg(not(target_arch = "wasm32"))]
7379            "net_announce" | "เน็ตประกาศ" => {
7380                let port = self.arg_num(&args, 0, 7778.0)? as u16;
7381                let info = self.arg_str(&args, 1, "");
7382                net::announce(port, &info);
7383                return Ok(Value::Unit);
7384            },
7385            #[cfg(not(target_arch = "wasm32"))]
7386            "net_announce_stop" | "เน็ตหยุดประกาศ" => {
7387                net::announce_stop();
7388                return Ok(Value::Unit);
7389            },
7390            #[cfg(not(target_arch = "wasm32"))]
7391            "net_discover" | "เน็ตค้นหา" => {
7392                let port = self.arg_num(&args, 0, 7778.0)? as u16;
7393                return Ok(Value::Str(net::discover(port)));
7394            },
7395            #[cfg(not(target_arch = "wasm32"))]
7396            "net_test" | "เน็ตทดสอบ" => {
7397                let port = self.arg_num(&args, 0, 7777.0)? as u16;
7398                return Ok(Value::Str(net::test_bind(port)));
7399            },
7400            // ── HTTP server (interpreter <-> async bridge, see runtime::web) ──
7401            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7402            "http_route" | "เว็บเส้นทาง" => {
7403                let method = self.arg_str(&args, 0, "GET").to_uppercase();
7404                let path = self.arg_str(&args, 1, "/");
7405                let handler = args.get(2).cloned().unwrap_or(Value::Unit);
7406                self.http_routes.push((method, path, handler));
7407                return Ok(Value::Unit);
7408            },
7409            // Registers a directory to be served as raw bytes at `prefix` (fonts,
7410            // images, generated zips/PDFs) — bypasses the String-only Request/
7411            // Response bridge entirely, so binary files come through intact.
7412            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7413            "http_static" | "เว็บสแตติก" => {
7414                let prefix = self.arg_str(&args, 0, "/static");
7415                let dir = self.arg_str(&args, 1, "static");
7416                self.http_static_dirs.push((prefix, dir));
7417                return Ok(Value::Unit);
7418            },
7419            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7420            "http_serve" | "เว็บเสิร์ฟ" => {
7421                let host = self.arg_str(&args, 0, "127.0.0.1");
7422                let port = self.arg_num(&args, 1, 8080.0)? as u16;
7423                let routes = std::mem::take(&mut self.http_routes);
7424                let static_dirs = std::mem::take(&mut self.http_static_dirs);
7425                // No premature "listening" print here: ling_http::serve_http
7426                // (called from spawn_server's background thread) now prints
7427                // its own banner, but only after the socket is actually
7428                // bound — a more honest signal than printing right after
7429                // requesting the background thread be spawned.
7430                let rx = web::spawn_server(host.clone(), port, static_dirs);
7431                for pending in rx {
7432                    let matched = routes
7433                        .iter()
7434                        .find(|(m, p, _)| m == &pending.method && p == &pending.path);
7435                    let response = match matched {
7436                        Some((_, _, handler)) => {
7437                            let req_value = Value::Struct {
7438                                name: "Request".to_string(),
7439                                fields: vec![
7440                                    ("method".to_string(), Value::Str(pending.method.clone())),
7441                                    ("path".to_string(), Value::Str(pending.path.clone())),
7442                                    ("query".to_string(), Value::Str(pending.query.clone())),
7443                                    ("body".to_string(), Value::Str(pending.body.clone())),
7444                                    ("cookie".to_string(), Value::Str(pending.cookie.clone())),
7445                                    (
7446                                        "authorization".to_string(),
7447                                        Value::Str(pending.authorization.clone()),
7448                                    ),
7449                                    (
7450                                        "client_ip".to_string(),
7451                                        Value::Str(pending.client_ip.clone()),
7452                                    ),
7453                                ],
7454                            };
7455                            match self.call_value(handler.clone(), vec![req_value]) {
7456                                Ok(v) => web::value_to_response(&v),
7457                                Err(e) => web::HttpResponse {
7458                                    status: 500,
7459                                    content_type: "text/plain; charset=utf-8".to_string(),
7460                                    body: format!("handler error: {e:?}"),
7461                                    set_cookie: None,
7462                                    location: None,
7463                                },
7464                            }
7465                        },
7466                        None => web::HttpResponse {
7467                            status: 404,
7468                            content_type: "text/plain; charset=utf-8".to_string(),
7469                            body: "not found".to_string(),
7470                            set_cookie: None,
7471                            location: None,
7472                        },
7473                    };
7474                    let _ = pending.respond_to.send(response);
7475                }
7476                return Ok(Value::Unit);
7477            },
7478            // Fires a POST request on a background async runtime and returns a job
7479            // id immediately — for slow external calls (e.g. local Stable Diffusion
7480            // generation) that must not block http_serve's single-threaded loop.
7481            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7482            "http_post_async" | "เว็บโพสต์ไม่บล็อก" => {
7483                let url = self.arg_str(&args, 0, "");
7484                let body = self.arg_str(&args, 1, "");
7485                let content_type = self.arg_str(&args, 2, "application/json");
7486                let id = self.async_jobs.start_post(url, content_type, body);
7487                return Ok(Value::Str(id));
7488            },
7489            // Non-blocking poll: "" while the job named by http_post_async (or
7490            // sdai_generate_start) is still running, the result once it
7491            // completes — same job table, same builtin polls both.
7492            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7493            "http_job_poll" | "เว็บงานสำรวจ" => {
7494                let id = self.arg_str(&args, 0, "");
7495                return Ok(Value::Str(self.async_jobs.poll(&id).unwrap_or_default()));
7496            },
7497            // Starts an AUTOMATIC1111-compatible txt2img generation in the
7498            // background against `base_url` (e.g. "http://127.0.0.1:1342").
7499            // Poll with http_job_poll: the result is the plain base64 PNG
7500            // once ready, or a string starting with "ERROR:" on failure —
7501            // the JSON response itself is parsed in Rust (see
7502            // AsyncJobs::start_sdai_txt2img), since `.ling` has no JSON parser.
7503            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7504            "sdai_generate_start" | "เอสดีเอไอเริ่มสร้าง" => {
7505                let base_url = self.arg_str(&args, 0, "http://127.0.0.1:1342");
7506                let prompt = self.arg_str(&args, 1, "");
7507                let width = self.arg_num(&args, 2, 512.0)? as u32;
7508                let height = self.arg_num(&args, 3, 512.0)? as u32;
7509                let id = self.async_jobs.start_sdai_txt2img(base_url, prompt, width, height);
7510                return Ok(Value::Str(id));
7511            },
7512            // ── query_param("q=a&page=2", "q", "") → "a" (URL-decoded) ──
7513            "query_param" | "พารามิเตอร์" => {
7514                let qs = self.arg_str(&args, 0, "");
7515                let name = self.arg_str(&args, 1, "");
7516                let default = self.arg_str(&args, 2, "");
7517                let mut found = default;
7518                for pair in qs.split('&') {
7519                    let mut it = pair.splitn(2, '=');
7520                    if it.next().unwrap_or("") == name {
7521                        found = url_decode(it.next().unwrap_or(""));
7522                        break;
7523                    }
7524                }
7525                return Ok(Value::Str(found));
7526            },
7527            // ── cookie_get("sid=abc; x=1", "sid", "") → "abc" ──
7528            "cookie_get" | "รับคุกกี้" => {
7529                let header = self.arg_str(&args, 0, "");
7530                let name = self.arg_str(&args, 1, "");
7531                let default = self.arg_str(&args, 2, "");
7532                let mut found = default;
7533                for pair in header.split(';') {
7534                    let p = pair.trim();
7535                    let mut it = p.splitn(2, '=');
7536                    if it.next().unwrap_or("") == name {
7537                        found = it.next().unwrap_or("").to_string();
7538                        break;
7539                    }
7540                }
7541                return Ok(Value::Str(found));
7542            },
7543            // ── html_escape(s) — & < > " ' → entities, for echoing user input ──
7544            "html_escape" | "กันเอชทีเอ็มแอล" => {
7545                let s = self.arg_str(&args, 0, "");
7546                return Ok(Value::Str(
7547                    s.replace('&', "&amp;")
7548                        .replace('<', "&lt;")
7549                        .replace('>', "&gt;")
7550                        .replace('"', "&quot;")
7551                        .replace('\'', "&#39;"),
7552                ));
7553            },
7554            // json_escape(s) — escape a string for embedding inside a JSON
7555            // string literal (", \, and control chars). Needed because the
7556            // registry builds JSON API responses by concatenation; without
7557            // this a value containing " or \ breaks or injects into the JSON.
7558            "json_escape" | "หนีเจสัน" => {
7559                let s = self.arg_str(&args, 0, "");
7560                let mut out = String::with_capacity(s.len() + 8);
7561                for c in s.chars() {
7562                    match c {
7563                        '"' => out.push_str("\\\""),
7564                        '\\' => out.push_str("\\\\"),
7565                        '\n' => out.push_str("\\n"),
7566                        '\r' => out.push_str("\\r"),
7567                        '\t' => out.push_str("\\t"),
7568                        c if (c as u32) < 0x20 => {
7569                            out.push_str(&format!("\\u{:04x}", c as u32))
7570                        },
7571                        c => out.push(c),
7572                    }
7573                }
7574                return Ok(Value::Str(out));
7575            },
7576            // ── CLI arguments: cli_arg("port", "8080") reads `--port 6688` ──
7577            "cli_arg" | "อาร์กิวเมนต์" => {
7578                let name = self.arg_str(&args, 0, "");
7579                let default = self.arg_str(&args, 1, "");
7580                let flag = format!("--{name}");
7581                let argv: Vec<String> = std::env::args().collect();
7582                let found = argv
7583                    .iter()
7584                    .position(|a| a == &flag)
7585                    .and_then(|i| argv.get(i + 1))
7586                    .cloned()
7587                    .unwrap_or(default);
7588                return Ok(Value::Str(found));
7589            },
7590            // ── SQLite (rusqlite, synchronous — matches the interpreter) ──
7591            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7592            "db_open" | "ฐานข้อมูลเปิด" => {
7593                let path = self.arg_str(&args, 0, "app.db");
7594                let conn = ling_http::rusqlite::Connection::open(&path)
7595                    .map_err(|e| EvalErr::from(format!("db_open '{path}': {e}")))?;
7596                let _ = conn.execute_batch("PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL;");
7597                self.db = Some(conn);
7598                return Ok(Value::Unit);
7599            },
7600            // db_exec(sql, ...params) → rows affected. Params bind positionally
7601            // (?1, ?2, ...): numbers as REAL, bools as 0/1, everything else TEXT.
7602            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7603            "db_exec" | "ฐานข้อมูลรัน" => {
7604                let sql = self.arg_str(&args, 0, "");
7605                let params = values_to_sql_params(&args[1.min(args.len())..]);
7606                let conn = self
7607                    .db
7608                    .as_ref()
7609                    .ok_or_else(|| EvalErr::from("db_exec: call db_open first".to_string()))?;
7610                let n = conn
7611                    .execute(
7612                        &sql,
7613                        ling_http::rusqlite::params_from_iter(params.iter()),
7614                    )
7615                    .map_err(|e| EvalErr::from(format!("db_exec: {e}\n  sql: {sql}")))?;
7616                return Ok(Value::Number(n as f64));
7617            },
7618            // db_query(sql, ...params) → List of Row structs (row.column_name).
7619            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
7620            "db_query" | "ฐานข้อมูลถาม" => {
7621                let sql = self.arg_str(&args, 0, "");
7622                let params = values_to_sql_params(&args[1.min(args.len())..]);
7623                let conn = self
7624                    .db
7625                    .as_ref()
7626                    .ok_or_else(|| EvalErr::from("db_query: call db_open first".to_string()))?;
7627                let mut stmt = conn
7628                    .prepare(&sql)
7629                    .map_err(|e| EvalErr::from(format!("db_query: {e}\n  sql: {sql}")))?;
7630                let col_names: Vec<String> =
7631                    stmt.column_names().iter().map(|s| s.to_string()).collect();
7632                let mut rows = stmt
7633                    .query(ling_http::rusqlite::params_from_iter(params.iter()))
7634                    .map_err(|e| EvalErr::from(format!("db_query: {e}")))?;
7635                let mut out = Vec::new();
7636                while let Some(row) = rows
7637                    .next()
7638                    .map_err(|e| EvalErr::from(format!("db_query row: {e}")))?
7639                {
7640                    let mut fields = Vec::with_capacity(col_names.len());
7641                    for (i, col) in col_names.iter().enumerate() {
7642                        use ling_http::rusqlite::types::ValueRef;
7643                        let v = match row.get_ref(i) {
7644                            Ok(ValueRef::Null) => Value::Str(String::new()),
7645                            Ok(ValueRef::Integer(n)) => Value::Number(n as f64),
7646                            Ok(ValueRef::Real(n)) => Value::Number(n),
7647                            Ok(ValueRef::Text(t)) => {
7648                                Value::Str(String::from_utf8_lossy(t).into_owned())
7649                            },
7650                            Ok(ValueRef::Blob(b)) =>
7651
7652                            {
7653                                use base64::Engine as _;
7654                                Value::Str(base64::engine::general_purpose::STANDARD.encode(b))
7655                            },
7656                            Err(_) => Value::Str(String::new()),
7657                        };
7658                        fields.push((col.clone(), v));
7659                    }
7660                    out.push(Value::Struct { name: "Row".to_string(), fields });
7661                }
7662                return Ok(Value::List(Rc::new(out)));
7663            },
7664            // ── gamepad (gilrs) ──
7665            #[cfg(not(target_arch = "wasm32"))]
7666            "gamepad_poll" | "จอยโพล" => {
7667                gamepad::poll();
7668                return Ok(Value::Unit);
7669            },
7670            #[cfg(not(target_arch = "wasm32"))]
7671            "gamepad_button" | "จอยปุ่ม" => {
7672                let name = self.arg_str(&args, 0, "");
7673                return Ok(Value::Number(if gamepad::button(&name) {
7674                    1.0
7675                } else {
7676                    0.0
7677                }));
7678            },
7679            #[cfg(not(target_arch = "wasm32"))]
7680            "gamepad_axis" | "จอยแกน" => {
7681                let name = self.arg_str(&args, 0, "");
7682                return Ok(Value::Number(gamepad::axis(&name) as f64));
7683            },
7684            #[cfg(not(target_arch = "wasm32"))]
7685            "gamepad_rumble" | "จอยสั่น" => {
7686                let low = self.arg_num(&args, 0, 0.0)? as f32;
7687                let high = self.arg_num(&args, 1, 0.0)? as f32;
7688                let ms = self.arg_num(&args, 2, 200.0)? as u32;
7689                gamepad::rumble(low, high, ms);
7690                return Ok(Value::Unit);
7691            },
7692            #[cfg(not(target_arch = "wasm32"))]
7693            "gamepad_list" | "จอยรายการ" => {
7694                return Ok(Value::Str(gamepad::list()));
7695            },
7696            #[cfg(not(target_arch = "wasm32"))]
7697            "gamepad_any" | "จอยใดๆ" => {
7698                return Ok(Value::Number(if gamepad::any_button() { 1.0 } else { 0.0 }));
7699            },
7700            // wasm32: gamepad not available — return safe no-op values
7701            #[cfg(target_arch = "wasm32")]
7702            "gamepad_poll" | "จอยโพล" | "gamepad_rumble" | "จอยสั่น" => {
7703                return Ok(Value::Unit);
7704            },
7705            #[cfg(target_arch = "wasm32")]
7706            "gamepad_button" | "จอยปุ่ม" | "gamepad_axis" | "จอยแกน" | "gamepad_any" | "จอยใดๆ" =>
7707            {
7708                return Ok(Value::Number(0.0));
7709            },
7710            #[cfg(target_arch = "wasm32")]
7711            "gamepad_list" | "จอยรายการ" => {
7712                return Ok(Value::Str(String::new()));
7713            },
7714
7715            // ── game AI: neural networks ─────────────────────────────────────
7716            // nn_new(inputs[, seed]) → handle
7717            #[cfg(not(target_arch = "wasm32"))]
7718            "nn_new" | "建神经网" | "ニューラル作成" | "신경망생성" | "สร้างโครงข่าย" =>
7719            {
7720                let n_in = self.arg_num(&args, 0, 1.0)?.max(0.0) as usize;
7721                let seed = self.arg_num(&args, 1, 1.0)? as u64;
7722                return Ok(Value::Number(ai::nn_new(n_in, seed) as f64));
7723            },
7724            // nn_dense(handle, units[, activation]) — append a layer
7725            #[cfg(not(target_arch = "wasm32"))]
7726            "nn_dense" | "密集层" | "密層追加" | "밀집층" | "ชั้นหนาแน่น" =>
7727            {
7728                let id = self.arg_num(&args, 0, -1.0)? as i64;
7729                let units = self.arg_num(&args, 1, 1.0)?.max(1.0) as usize;
7730                let act = self.arg_str(&args, 2, "relu");
7731                ai::nn_dense(id, units, &act);
7732                return Ok(Value::Unit);
7733            },
7734            // nn_forward(handle, [inputs]) → [outputs]
7735            #[cfg(not(target_arch = "wasm32"))]
7736            "nn_forward" | "神经前向" | "順伝播" | "순전파" | "ส่งต่อโครงข่าย" =>
7737            {
7738                let id = self.arg_num(&args, 0, -1.0)? as i64;
7739                let input = self.arg_list_f32(&args, 1);
7740                let out = ai::nn_forward(id, &input);
7741                return Ok(Value::List(Rc::new(
7742                    out.into_iter().map(|v| Value::Number(v as f64)).collect(),
7743                )));
7744            },
7745            // nn_train(handle, [inputs], [targets][, lr]) → loss
7746            #[cfg(not(target_arch = "wasm32"))]
7747            "nn_train" | "训练网" | "ニューラル学習" | "신경망학습" | "ฝึกโครงข่าย" =>
7748            {
7749                let id = self.arg_num(&args, 0, -1.0)? as i64;
7750                let input = self.arg_list_f32(&args, 1);
7751                let target = self.arg_list_f32(&args, 2);
7752                let lr = self.arg_num(&args, 3, 0.01)? as f32;
7753                return Ok(Value::Number(ai::nn_train(id, &input, &target, lr) as f64));
7754            },
7755            // nn_save(handle, path) → bool
7756            #[cfg(not(target_arch = "wasm32"))]
7757            "nn_save" | "保存网" | "網保存" | "신경망저장" | "บันทึกโครงข่าย" =>
7758            {
7759                let id = self.arg_num(&args, 0, -1.0)? as i64;
7760                let path = self.arg_str(&args, 1, "model.lnn");
7761                return Ok(Value::Bool(ai::nn_save(id, &path)));
7762            },
7763            // nn_load(path) → handle (-1 on failure)
7764            #[cfg(not(target_arch = "wasm32"))]
7765            "nn_load" | "载入网" | "網読込" | "신경망불러오기" | "โหลดโครงข่าย" =>
7766            {
7767                let path = self.arg_str(&args, 0, "model.lnn");
7768                return Ok(Value::Number(ai::nn_load(&path) as f64));
7769            },
7770
7771            // ── game AI: behavior trees ──────────────────────────────────────
7772            // bt_build(dsl_string) → handle
7773            #[cfg(not(target_arch = "wasm32"))]
7774            "bt_build" | "建行为树" | "行動木構築" | "행동트리구성" | "สร้างต้นไม้พฤติกรรม" =>
7775            {
7776                let spec = self.arg_str(&args, 0, "");
7777                return Ok(Value::Number(ai::bt_build(&spec) as f64));
7778            },
7779            // bt_set(handle, key, value) — set a blackboard fact
7780            #[cfg(not(target_arch = "wasm32"))]
7781            "bt_set" | "设事实" | "事実設定" | "사실설정" | "ตั้งข้อเท็จจริง" =>
7782            {
7783                let id = self.arg_num(&args, 0, -1.0)? as i64;
7784                let key = self.arg_str(&args, 1, "");
7785                let val = self.arg_num(&args, 2, 0.0)? as f32;
7786                ai::bt_set(id, &key, val);
7787                return Ok(Value::Unit);
7788            },
7789            // bt_tick(handle) → chosen action name ("" if none)
7790            #[cfg(not(target_arch = "wasm32"))]
7791            "bt_tick" | "行为树滴答" | "行動木更新" | "행동트리틱" | "เดินต้นไม้พฤติกรรม" =>
7792            {
7793                let id = self.arg_num(&args, 0, -1.0)? as i64;
7794                return Ok(Value::Str(ai::bt_tick(id)));
7795            },
7796            // bt_status(handle) → 0 fail / 1 success / 2 running
7797            #[cfg(not(target_arch = "wasm32"))]
7798            "bt_status" | "行为树状态" | "行動木状態" | "행동트리상태" | "สถานะต้นไม้พฤติกรรม" =>
7799            {
7800                let id = self.arg_num(&args, 0, -1.0)? as i64;
7801                return Ok(Value::Number(ai::bt_status(id) as f64));
7802            },
7803
7804            // ── game AI: miniature dialog LLM ────────────────────────────────
7805            // dialog_new([ctx, embed, hidden, seed]) → handle
7806            #[cfg(not(target_arch = "wasm32"))]
7807            "dialog_new" | "建对话模型" | "対話モデル作成" | "대화모델생성" | "สร้างโมเดลสนทนา" =>
7808            {
7809                let ctx = self.arg_num(&args, 0, 3.0)?.max(1.0) as usize;
7810                let embed = self.arg_num(&args, 1, 32.0)?.max(1.0) as usize;
7811                let hidden = self.arg_num(&args, 2, 64.0)?.max(1.0) as usize;
7812                let seed = self.arg_num(&args, 3, 1.0)? as u64;
7813                return Ok(Value::Number(
7814                    ai::dialog_new(ctx, embed, hidden, seed) as f64
7815                ));
7816            },
7817            // dialog_learn(handle, text) — add one utterance to the corpus
7818            #[cfg(not(target_arch = "wasm32"))]
7819            "dialog_learn" | "对话学习" | "対話学習" | "대화학습" | "เรียนรู้สนทนา" =>
7820            {
7821                let id = self.arg_num(&args, 0, -1.0)? as i64;
7822                let text = self.arg_str(&args, 1, "");
7823                ai::dialog_learn(id, &text);
7824                return Ok(Value::Unit);
7825            },
7826            // dialog_load(handle, path) → lines added (-1 on error)
7827            #[cfg(not(target_arch = "wasm32"))]
7828            "dialog_load" | "对话载入" | "対話読込" | "대화불러오기" | "โหลดชุดสนทนา" =>
7829            {
7830                let id = self.arg_num(&args, 0, -1.0)? as i64;
7831                let path = self.arg_str(&args, 1, "");
7832                return Ok(Value::Number(ai::dialog_load(id, &path) as f64));
7833            },
7834            // dialog_train(handle[, epochs, lr]) → loss
7835            #[cfg(not(target_arch = "wasm32"))]
7836            "dialog_train" | "对话训练" | "対話訓練" | "대화훈련" | "ฝึกสนทนา" =>
7837            {
7838                let id = self.arg_num(&args, 0, -1.0)? as i64;
7839                let epochs = self.arg_num(&args, 1, 20.0)?.max(1.0) as usize;
7840                let lr = self.arg_num(&args, 2, 0.1)? as f32;
7841                return Ok(Value::Number(ai::dialog_train(id, epochs, lr) as f64));
7842            },
7843            // dialog_say(handle, prompt[, max_tokens, temperature]) → reply text
7844            #[cfg(not(target_arch = "wasm32"))]
7845            "dialog_say" | "对话生成" | "対話生成" | "대화생성" | "พูดสนทนา" =>
7846            {
7847                let id = self.arg_num(&args, 0, -1.0)? as i64;
7848                let prompt = self.arg_str(&args, 1, "");
7849                let max = self.arg_num(&args, 2, 24.0)?.max(1.0) as usize;
7850                let temp = self.arg_num(&args, 3, 0.8)? as f32;
7851                return Ok(Value::Str(ai::dialog_say(id, &prompt, max, temp)));
7852            },
7853            // dialog_save(handle, path) → bool
7854            #[cfg(not(target_arch = "wasm32"))]
7855            "dialog_save" | "对话存模" | "対話モデル保存" | "대화모델저장" | "บันทึกโมเดลสนทนา" =>
7856            {
7857                let id = self.arg_num(&args, 0, -1.0)? as i64;
7858                let path = self.arg_str(&args, 1, "model.llm");
7859                return Ok(Value::Bool(ai::dialog_save(id, &path)));
7860            },
7861            // dialog_load_model(path) → handle (-1 on failure)
7862            #[cfg(not(target_arch = "wasm32"))]
7863            "dialog_load_model"
7864            | "对话载模"
7865            | "対話モデル読込"
7866            | "대화모델불러오기"
7867            | "โหลดโมเดลสนทนา" => {
7868                let path = self.arg_str(&args, 0, "model.llm");
7869                return Ok(Value::Number(ai::dialog_load_model(&path) as f64));
7870            },
7871
7872            // Decodes `application/x-www-form-urlencoded` text: '+' -> space,
7873            // '%XX' -> byte. Needed to read plain HTML `<form>` POST bodies.
7874            "url_decode" | "网址解码" => {
7875                let s = self.arg_str(&args, 0, "");
7876                let bytes = s.as_bytes();
7877                let mut out = Vec::with_capacity(bytes.len());
7878                let mut i = 0;
7879                while i < bytes.len() {
7880                    match bytes[i] {
7881                        b'+' => {
7882                            out.push(b' ');
7883                            i += 1;
7884                        },
7885                        b'%' if i + 2 < bytes.len() => {
7886                            let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or("");
7887                            match u8::from_str_radix(hex, 16) {
7888                                Ok(b) => {
7889                                    out.push(b);
7890                                    i += 3;
7891                                },
7892                                Err(_) => {
7893                                    out.push(bytes[i]);
7894                                    i += 1;
7895                                },
7896                            }
7897                        },
7898                        b => {
7899                            out.push(b);
7900                            i += 1;
7901                        },
7902                    }
7903                }
7904                return Ok(Value::Str(String::from_utf8_lossy(&out).into_owned()));
7905            },
7906            // Seconds since Unix epoch (float — sub-second precision). No ISO/date
7907            // formatting builtin exists yet; `.ling` code that wants a display
7908            // string currently just uses the raw number.
7909            "now_unix" | "现在时间" => {
7910                return Ok(Value::Number(now_secs()));
7911            },
7912            "file_exists" | "文件存在" => {
7913                #[cfg(target_arch = "wasm32")]
7914                return Ok(Value::Bool(false));
7915                #[cfg(not(target_arch = "wasm32"))]
7916                {
7917                    let path = self.arg_str(&args, 0, "");
7918                    return Ok(Value::Bool(std::path::Path::new(&path).exists()));
7919                }
7920            },
7921            "write_file" | "เขียนไฟล์" => {
7922                #[cfg(target_arch = "wasm32")]
7923                return Ok(Value::Unit);
7924                #[cfg(not(target_arch = "wasm32"))]
7925                {
7926                    let path = self.arg_str(&args, 0, "");
7927                    let content = self.arg_str(&args, 1, "");
7928                    std::fs::write(&path, content.as_bytes())
7929                        .map_err(|e| EvalErr::from(format!("write_file '{path}': {e}")))?;
7930                    return Ok(Value::Unit);
7931                }
7932            },
7933            "print_file" | "พิมพ์ไฟล์" => {
7934                let content = self.arg_str(&args, 0, "");
7935                print!("{content}");
7936                return Ok(Value::Unit);
7937            },
7938
7939            // ── CLI arguments ─────────────────────────────────────────────────
7940            "get_args" | "รับอาร์กิวเมนต์" => {
7941                let v: Vec<Value> = std::env::args().map(Value::Str).collect();
7942                return Ok(Value::List(Rc::new(v)));
7943            },
7944
7945            // ── Filesystem: directory walking, stat, content hashing (native) ──
7946            // These power headless batch tools (asset pipelines, indexers). Errors
7947            // degrade gracefully (empty list / 0 / "") so a walk never aborts on one
7948            // unreadable entry.
7949            #[cfg(not(target_arch = "wasm32"))]
7950            "list_dir" | "รายการไดเรกทอรี" => {
7951                let path = self.arg_str(&args, 0, ".");
7952                let mut paths: Vec<String> = Vec::new();
7953                if let Ok(rd) = std::fs::read_dir(&path) {
7954                    for e in rd.flatten() {
7955                        paths.push(e.path().to_string_lossy().into_owned());
7956                    }
7957                }
7958                paths.sort();
7959                let out: Vec<Value> = paths.into_iter().map(Value::Str).collect();
7960                return Ok(Value::List(Rc::new(out)));
7961            },
7962            #[cfg(not(target_arch = "wasm32"))]
7963            "is_dir" | "เป็นไดเรกทอรี" => {
7964                let path = self.arg_str(&args, 0, "");
7965                return Ok(Value::Bool(std::path::Path::new(&path).is_dir()));
7966            },
7967            #[cfg(not(target_arch = "wasm32"))]
7968            "is_file" | "เป็นไฟล์" => {
7969                let path = self.arg_str(&args, 0, "");
7970                return Ok(Value::Bool(std::path::Path::new(&path).is_file()));
7971            },
7972            #[cfg(not(target_arch = "wasm32"))]
7973            "path_name" | "ชื่อไฟล์" => {
7974                let path = self.arg_str(&args, 0, "");
7975                let name = std::path::Path::new(&path)
7976                    .file_name()
7977                    .map(|s| s.to_string_lossy().into_owned())
7978                    .unwrap_or_default();
7979                return Ok(Value::Str(name));
7980            },
7981            #[cfg(not(target_arch = "wasm32"))]
7982            "path_ext" | "นามสกุลไฟล์" => {
7983                let path = self.arg_str(&args, 0, "");
7984                let ext = std::path::Path::new(&path)
7985                    .extension()
7986                    .map(|s| s.to_string_lossy().to_lowercase())
7987                    .unwrap_or_default();
7988                return Ok(Value::Str(ext));
7989            },
7990            #[cfg(not(target_arch = "wasm32"))]
7991            "file_size" | "ขนาดไฟล์" => {
7992                let path = self.arg_str(&args, 0, "");
7993                let sz = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
7994                return Ok(Value::Number(sz as f64));
7995            },
7996            #[cfg(not(target_arch = "wasm32"))]
7997            "file_modified" | "เวลาที่แก้ไข" => {
7998                let path = self.arg_str(&args, 0, "");
7999                let secs = std::fs::metadata(&path)
8000                    .and_then(|m| m.modified())
8001                    .ok()
8002                    .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
8003                    .map(|d| d.as_secs_f64())
8004                    .unwrap_or(0.0);
8005                return Ok(Value::Number(secs));
8006            },
8007            #[cfg(not(target_arch = "wasm32"))]
8008            "file_created" | "เวลาที่สร้าง" => {
8009                let path = self.arg_str(&args, 0, "");
8010                let secs = std::fs::metadata(&path)
8011                    .ok()
8012                    .and_then(|m| m.created().or_else(|_| m.modified()).ok())
8013                    .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
8014                    .map(|d| d.as_secs_f64())
8015                    .unwrap_or(0.0);
8016                return Ok(Value::Number(secs));
8017            },
8018            #[cfg(not(target_arch = "wasm32"))]
8019            "make_dir" | "สร้างไดเรกทอรี" => {
8020                let path = self.arg_str(&args, 0, "");
8021                return Ok(Value::Bool(std::fs::create_dir_all(&path).is_ok()));
8022            },
8023            // str_strip_prefix("Bearer x", "Bearer ") → "x" (unchanged if absent).
8024            "str_strip_prefix" | "ตัดคำนำหน้า" => {
8025                let s = self.arg_str(&args, 0, "");
8026                let prefix = self.arg_str(&args, 1, "");
8027                return Ok(Value::Str(
8028                    s.strip_prefix(&prefix).unwrap_or(&s).to_string(),
8029                ));
8030            },
8031            // Classify a file by magic bytes: "gzip" | "zip" | "other" | "missing".
8032            // The build-verification gate: only real built archives may publish.
8033            #[cfg(not(target_arch = "wasm32"))]
8034            "file_magic" | "มายาไฟล์" => {
8035                let path = self.arg_str(&args, 0, "");
8036                let kind = match std::fs::File::open(&path) {
8037                    Ok(mut f) => {
8038                        use std::io::Read;
8039                        let mut buf = [0u8; 4];
8040                        let n = f.read(&mut buf).unwrap_or(0);
8041                        if n >= 2 && buf[0] == 0x1f && buf[1] == 0x8b {
8042                            "gzip"
8043                        } else if n >= 4 && &buf[0..2] == b"PK" {
8044                            "zip"
8045                        } else {
8046                            "other"
8047                        }
8048                    },
8049                    Err(_) => "missing",
8050                };
8051                return Ok(Value::Str(kind.to_string()));
8052            },
8053            // Binary-safe file copy (backups): copy_file(src, dst) → bool.
8054            #[cfg(not(target_arch = "wasm32"))]
8055            "copy_file" | "คัดลอกไฟล์" => {
8056                let src = self.arg_str(&args, 0, "");
8057                let dst = self.arg_str(&args, 1, "");
8058                if let Some(parent) = std::path::Path::new(&dst).parent() {
8059                    let _ = std::fs::create_dir_all(parent);
8060                }
8061                return Ok(Value::Bool(std::fs::copy(&src, &dst).is_ok()));
8062            },
8063            // ── Read a .tgz (gzip tarball): list file entries / read one file ──
8064            // Powers the GitHub-style "Code / Files" browser: tar_gz_list gives
8065            // the file tree, tar_gz_read pulls one file's text for the viewer.
8066            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8067            "tar_gz_list" | "รายการทาร์" => {
8068                let path = self.arg_str(&args, 0, "");
8069                let mut names: Vec<String> = Vec::new();
8070                if let Ok(file) = std::fs::File::open(&path) {
8071                    let gz = flate2::read::GzDecoder::new(file);
8072                    let mut ar = tar::Archive::new(gz);
8073                    if let Ok(entries) = ar.entries() {
8074                        for entry in entries.flatten() {
8075                            if entry.header().entry_type().is_file() {
8076                                if let Ok(p) = entry.path() {
8077                                    names.push(
8078                                        p.to_string_lossy()
8079                                            .trim_start_matches("./")
8080                                            .replace('\\', "/"),
8081                                    );
8082                                }
8083                            }
8084                        }
8085                    }
8086                }
8087                names.sort();
8088                names.dedup();
8089                let out: Vec<Value> = names.into_iter().map(Value::Str).collect();
8090                return Ok(Value::List(Rc::new(out)));
8091            },
8092            // tar_gz_read(archive, entry) → that file's text (utf-8 lossy,
8093            // capped at 256 KiB). Only reads entries that exist in the archive,
8094            // so a caller can't traverse outside it. "" if not found/unreadable.
8095            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8096            "tar_gz_read" | "อ่านทาร์" => {
8097                let path = self.arg_str(&args, 0, "");
8098                let want = self.arg_str(&args, 1, "");
8099                let want = want.trim_start_matches("./").replace('\\', "/");
8100                let mut content = String::new();
8101                if let Ok(file) = std::fs::File::open(&path) {
8102                    let gz = flate2::read::GzDecoder::new(file);
8103                    let mut ar = tar::Archive::new(gz);
8104                    if let Ok(entries) = ar.entries() {
8105                        for entry in entries.flatten() {
8106                            let mut entry = entry;
8107                            let name = match entry.path() {
8108                                Ok(p) => p
8109                                    .to_string_lossy()
8110                                    .trim_start_matches("./")
8111                                    .replace('\\', "/"),
8112                                Err(_) => continue,
8113                            };
8114                            if name == want {
8115                                use std::io::Read;
8116                                let mut buf = Vec::new();
8117                                let cap = 256 * 1024;
8118                                if entry.take(cap as u64 + 1).read_to_end(&mut buf).is_ok() {
8119                                    let slice = if buf.len() > cap { &buf[..cap] } else { &buf[..] };
8120                                    content = String::from_utf8_lossy(slice).into_owned();
8121                                }
8122                                break;
8123                            }
8124                        }
8125                    }
8126                }
8127                return Ok(Value::Str(content));
8128            },
8129            // ── TOTP (RFC 6238, HMAC-SHA1, 6 digits, 30s) for 2FA ──
8130            // Base32 secret compatible with Google Authenticator / Authy etc.
8131            // `base32_encode`/`totp_code`/`totp_check` only exist under this
8132            // same `feature = "web"` gate (see their definitions above) — a
8133            // build without it must skip these arms too, not just fail to
8134            // link; matches how `file_hash` etc. gate their own arms below.
8135            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8136            "totp_secret" | "โทเทนลับ" => {
8137                let mut bytes = [0u8; 20];
8138                rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut bytes);
8139                return Ok(Value::Str(base32_encode(&bytes)));
8140            },
8141            // otpauth:// URI to paste into an authenticator app (or make a QR of).
8142            // No cfg gate needed — pure string formatting, no dependency on
8143            // the web-only TOTP helpers.
8144            "totp_uri" | "โทเทนยูอาร์ไอ" => {
8145                let secret = self.arg_str(&args, 0, "");
8146                let account = self.arg_str(&args, 1, "user");
8147                let issuer = self.arg_str(&args, 2, "lingfu");
8148                return Ok(Value::Str(format!(
8149                    "otpauth://totp/{issuer}:{account}?secret={secret}&issuer={issuer}&algorithm=SHA1&digits=6&period=30"
8150                )));
8151            },
8152            // Verify a 6-digit code against the secret, allowing ±1 time step.
8153            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8154            "totp_verify" | "โทเทนตรวจ" => {
8155                let secret = self.arg_str(&args, 0, "");
8156                let code = self.arg_str(&args, 1, "");
8157                let ok = totp_check(&secret, code.trim());
8158                return Ok(Value::Bool(ok));
8159            },
8160            // The current valid code, for tests/tools.
8161            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
8162            "totp_now" | "โทเทนตอนนี้" => {
8163                let secret = self.arg_str(&args, 0, "");
8164                let step = (crate::runtime::now_secs() as u64) / 30;
8165                return Ok(Value::Str(
8166                    totp_code(&secret, step).unwrap_or_default(),
8167                ));
8168            },
8169            // BLAKE3 hex of a file's bytes (binary-safe content fingerprint).
8170            #[cfg(not(target_arch = "wasm32"))]
8171            "file_hash" | "แฮชไฟล์" => {
8172                let path = self.arg_str(&args, 0, "");
8173                match std::fs::read(&path) {
8174                    Ok(bytes) => {
8175                        return Ok(Value::Str(hex_encode(&ling_crypto::Blake3::hash(&bytes))))
8176                    },
8177                    Err(_) => return Ok(Value::Str(String::new())),
8178                }
8179            },
8180            // BLAKE3 hex of an arbitrary string (deterministic id/colour/role seed).
8181            "hash_hex" | "แฮชสตริง" => {
8182                let s = self.arg_str(&args, 0, "");
8183                return Ok(Value::Str(hex_encode(&ling_crypto::Blake3::hash(
8184                    s.as_bytes(),
8185                ))));
8186            },
8187            // Read an environment variable, falling back to a default.
8188            #[cfg(not(target_arch = "wasm32"))]
8189            "env_get" | "รับตัวแปรแวดล้อม" => {
8190                let name = self.arg_str(&args, 0, "");
8191                let dflt = self.arg_str(&args, 1, "");
8192                return Ok(Value::Str(std::env::var(&name).unwrap_or(dflt)));
8193            },
8194
8195            // ── String utilities ──────────────────────────────────────────────
8196            // Parses a string to a number (0 on failure — degrades gracefully,
8197            // like the other filesystem/parsing builtins in this file).
8198            "to_number" | "转数字" => {
8199                let s = self.arg_str(&args, 0, "");
8200                return Ok(Value::Number(s.trim().parse().unwrap_or(0.0)));
8201            },
8202            // Plain SHA-256 hex — matches the browser's native SubtleCrypto
8203            // digest("SHA-256", ...), which is what proof-of-work mining uses
8204            // client-side (Web Crypto has no Blake3/SHA-3, so this is the one
8205            // hash both sides can compute natively and fast).
8206            "sha256_hex" | "SHA256哈希" => {
8207                use sha2::Digest;
8208                let s = self.arg_str(&args, 0, "");
8209                let mut h = sha2::Sha256::new();
8210                h.update(s.as_bytes());
8211                return Ok(Value::Str(hex_encode(&h.finalize())));
8212            },
8213            // Parses a hex string (no "0x" prefix) to a number — `to_number`
8214            // uses Rust's plain f64 parser, which doesn't understand hex.
8215            "hex_to_number" | "十六进制转数字" => {
8216                let s = self.arg_str(&args, 0, "");
8217                let v = u64::from_str_radix(s.trim(), 16).unwrap_or(0);
8218                return Ok(Value::Number(v as f64));
8219            },
8220            "split" | "str_split" | "แยก" => {
8221                let s = self.arg_str(&args, 0, "");
8222                let sep = self.arg_str(&args, 1, "\n");
8223                let sep = if sep.is_empty() { "\n".into() } else { sep };
8224                let parts: Vec<Value> = s
8225                    .split(sep.as_str())
8226                    .map(|p| Value::Str(p.to_string()))
8227                    .collect();
8228                return Ok(Value::List(Rc::new(parts)));
8229            },
8230            "trim" | "str_trim" | "ตัดช่องว่าง" => {
8231                let s = self.arg_str(&args, 0, "");
8232                return Ok(Value::Str(s.trim().to_string()));
8233            },
8234            "starts_with" | "str_starts_with" | "เริ่มด้วย" => {
8235                let s = self.arg_str(&args, 0, "");
8236                let prefix = self.arg_str(&args, 1, "");
8237                return Ok(Value::Bool(s.starts_with(prefix.as_str())));
8238            },
8239            "ends_with" | "str_ends_with" | "ลงท้ายด้วย" => {
8240                let s = self.arg_str(&args, 0, "");
8241                let suffix = self.arg_str(&args, 1, "");
8242                return Ok(Value::Bool(s.ends_with(suffix.as_str())));
8243            },
8244            "str_replace" | "แทนสตริง" => {
8245                let s = self.arg_str(&args, 0, "");
8246                let from = self.arg_str(&args, 1, "");
8247                let to = self.arg_str(&args, 2, "");
8248                return Ok(Value::Str(s.replace(from.as_str(), to.as_str())));
8249            },
8250            "str_find" | "หาในสตริง" => {
8251                let s = self.arg_str(&args, 0, "");
8252                let needle = self.arg_str(&args, 1, "");
8253                // Return char index (not byte index) for consistency with substr
8254                let pos = s
8255                    .find(needle.as_str())
8256                    .map(|byte_i| s[..byte_i].chars().count() as f64)
8257                    .unwrap_or(-1.0);
8258                return Ok(Value::Number(pos));
8259            },
8260            "substr" | "str_slice" | "ส่วนสตริง" => {
8261                let s = self.arg_str(&args, 0, "");
8262                let start = self.arg_num(&args, 1, 0.0)? as usize;
8263                let len = args
8264                    .get(2)
8265                    .map(|v| self.to_number(v).unwrap_or(999999.0) as usize)
8266                    .unwrap_or_else(|| s.chars().count().saturating_sub(start));
8267                let chars: Vec<char> = s.chars().collect();
8268                let end = (start + len).min(chars.len());
8269                let slice: String = chars.get(start..end).unwrap_or(&[]).iter().collect();
8270                return Ok(Value::Str(slice));
8271            },
8272            "to_str" | "str" | "num_str" | "แปลงสตริง" => {
8273                let v = args.into_iter().next().unwrap_or(Value::Unit);
8274                return Ok(Value::Str(v.to_string()));
8275            },
8276            "str_repeat" | "ทำซ้ำสตริง" => {
8277                let s = self.arg_str(&args, 0, "");
8278                let n = self.arg_num(&args, 1, 1.0)? as usize;
8279                return Ok(Value::Str(s.repeat(n)));
8280            },
8281            "str_upper" => {
8282                let s = self.arg_str(&args, 0, "");
8283                return Ok(Value::Str(s.to_uppercase()));
8284            },
8285            "str_lower" => {
8286                let s = self.arg_str(&args, 0, "");
8287                return Ok(Value::Str(s.to_lowercase()));
8288            },
8289            "str_len" | "len" | "ความยาว" | "长度" | "長さ" | "길이" => {
8290                match args.first() {
8291                    Some(Value::Str(s)) => return Ok(Value::Number(s.chars().count() as f64)),
8292                    Some(Value::List(v)) => return Ok(Value::Number(v.len() as f64)),
8293                    _ => return Ok(Value::Number(0.0)),
8294                }
8295            },
8296
8297            // ── FNV-1a hash (deterministic, normalized 0.0–1.0) ──────────────
8298            "hash_str" | "แฮช" => {
8299                let s = self.arg_str(&args, 0, "");
8300                let mut h: u64 = 14695981039346656037_u64;
8301                for b in s.bytes() {
8302                    h ^= b as u64;
8303                    h = h.wrapping_mul(1099511628211);
8304                }
8305                return Ok(Value::Number((h & 0xFFFFFF) as f64 / 16777215.0));
8306            },
8307            "hash_int" | "แฮชจำนวน" => {
8308                let s = self.arg_str(&args, 0, "");
8309                let n = self.arg_num(&args, 1, 100.0)? as u64;
8310                let mut h: u64 = 14695981039346656037_u64;
8311                for b in s.bytes() {
8312                    h ^= b as u64;
8313                    h = h.wrapping_mul(1099511628211);
8314                }
8315                return Ok(Value::Number((h % n.max(1)) as f64));
8316            },
8317
8318            // ── List utilities ────────────────────────────────────────────────
8319            "list_new" | "รายการใหม่" | "新建列表" | "新規リスト" | "새목록" =>
8320            {
8321                return Ok(Value::List(Rc::new(Vec::new())));
8322            },
8323            "list_push" | "เพิ่มรายการ" | "列表添加" | "リスト追加" | "목록추가" =>
8324            {
8325                let lst = args
8326                    .first()
8327                    .cloned()
8328                    .unwrap_or(Value::List(Rc::new(vec![])));
8329                let val = args.get(1).cloned().unwrap_or(Value::Unit);
8330                if let Value::List(mut v) = lst {
8331                    Rc::make_mut(&mut v).push(val);
8332                    return Ok(Value::List(v));
8333                }
8334                return Ok(Value::List(Rc::new(vec![val])));
8335            },
8336            "list_get" | "รับรายการ" | "取元素" | "要素取得" | "요소가져오기" =>
8337            {
8338                // Borrow the list; clone only the element (was cloning the whole list).
8339                let i = self.arg_num(&args, 1, 0.0)? as usize;
8340                if let Some(Value::List(v)) = args.first() {
8341                    return Ok(v.get(i).cloned().unwrap_or(Value::Str(String::new())));
8342                }
8343                return Ok(Value::Str(String::new()));
8344            },
8345            // list_max(numbers, default) / list_min(numbers, default) — `default`
8346            // is returned for an empty list (there's no numeric identity element
8347            // to fall back to otherwise).
8348            "list_max" | "列表最大值" => {
8349                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8350                let default = self.arg_num(&args, 1, 0.0)?;
8351                if let Value::List(v) = lst {
8352                    let mut best = default;
8353                    let mut any = false;
8354                    for item in v.iter() {
8355                        if let Value::Number(n) = item {
8356                            if !any || *n > best {
8357                                best = *n;
8358                                any = true;
8359                            }
8360                        }
8361                    }
8362                    return Ok(Value::Number(best));
8363                }
8364                return Ok(Value::Number(default));
8365            },
8366            "list_min" | "列表最小值" => {
8367                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8368                let default = self.arg_num(&args, 1, 0.0)?;
8369                if let Value::List(v) = lst {
8370                    let mut best = default;
8371                    let mut any = false;
8372                    for item in v.iter() {
8373                        if let Value::Number(n) = item {
8374                            if !any || *n < best {
8375                                best = *n;
8376                                any = true;
8377                            }
8378                        }
8379                    }
8380                    return Ok(Value::Number(best));
8381                }
8382                return Ok(Value::Number(default));
8383            },
8384            // list_set(lst, idx, val) → new list with index replaced. Engine builtin
8385            // (O(n) one copy) to replace the O(n²) ling `ตั้งรายการ` that looped
8386            // list_push + list_get (each of which copied the whole list).
8387            "list_set" | "ตั้งรายการ" | "设元素" | "要素設定" | "요소설정" =>
8388            {
8389                let idx = self.arg_num(&args, 1, 0.0)? as usize;
8390                let mut ai = args.into_iter();
8391                let lst = ai.next().unwrap_or(Value::List(Rc::new(vec![])));
8392                ai.next(); // skip idx
8393                let val = ai.next().unwrap_or(Value::Unit);
8394                if let Value::List(mut v) = lst {
8395                    if idx < v.len() {
8396                        Rc::make_mut(&mut v)[idx] = val;
8397                    }
8398                    return Ok(Value::List(v));
8399                }
8400                return Ok(Value::List(Rc::new(vec![])));
8401            },
8402            "list_join" | "join" | "รวมรายการ" | "连接" | "連結" | "연결" =>
8403            {
8404                let lst = args
8405                    .first()
8406                    .cloned()
8407                    .unwrap_or(Value::List(Rc::new(vec![])));
8408                let sep = args.get(1).map(|v| v.to_string()).unwrap_or_default();
8409                if let Value::List(v) = lst {
8410                    return Ok(Value::Str(
8411                        v.iter()
8412                            .map(|x| x.to_string())
8413                            .collect::<Vec<_>>()
8414                            .join(&sep),
8415                    ));
8416                }
8417                return Ok(Value::Str(String::new()));
8418            },
8419            // list_map/list_filter/list_find — take a closure. Necessary as real
8420            // builtins (not expressible in `.ling` itself): a bare-identifier call
8421            // `f(x)` where `f` is a local variable always resolves through
8422            // `call_named`, which only looks at top-level `fn` definitions by
8423            // design ("call-site locals are intentionally NOT visible to fns") —
8424            // so a closure held in a variable/parameter can't be invoked from
8425            // `.ling` source directly. These call it from the Rust side instead,
8426            // the same way `http_serve` already invokes route-handler closures.
8427            "list_map" | "映射列表" => {
8428                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8429                let f = args.get(1).cloned().unwrap_or(Value::Unit);
8430                if let Value::List(v) = lst {
8431                    let mut out = Vec::with_capacity(v.len());
8432                    for item in v.iter() {
8433                        out.push(self.call_value(f.clone(), vec![item.clone()])?);
8434                    }
8435                    return Ok(Value::List(Rc::new(out)));
8436                }
8437                return Ok(Value::List(Rc::new(vec![])));
8438            },
8439            "list_filter" | "过滤列表" => {
8440                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8441                let f = args.get(1).cloned().unwrap_or(Value::Unit);
8442                if let Value::List(v) = lst {
8443                    let mut out = Vec::new();
8444                    for item in v.iter() {
8445                        if matches!(self.call_value(f.clone(), vec![item.clone()])?, Value::Bool(true)) {
8446                            out.push(item.clone());
8447                        }
8448                    }
8449                    return Ok(Value::List(Rc::new(out)));
8450                }
8451                return Ok(Value::List(Rc::new(vec![])));
8452            },
8453            // First element for which `f` returns true, or Unit if none match.
8454            "list_find" | "查找列表" => {
8455                let lst = args.first().cloned().unwrap_or(Value::List(Rc::new(vec![])));
8456                let f = args.get(1).cloned().unwrap_or(Value::Unit);
8457                if let Value::List(v) = lst {
8458                    for item in v.iter() {
8459                        if matches!(self.call_value(f.clone(), vec![item.clone()])?, Value::Bool(true)) {
8460                            return Ok(item.clone());
8461                        }
8462                    }
8463                }
8464                return Ok(Value::Unit);
8465            },
8466            // blob_f32("<deflate+base64>") / blob_i32(...) — decode an embedded,
8467            // losslessly-compressed numeric blob into a list. Produced by
8468            // `ling convert`; lets converted assets carry geometry/PCM/etc. compactly.
8469            #[cfg(not(target_arch = "wasm32"))]
8470            "blob_f32" | "blob_i32" => {
8471                let s = self.arg_str(&args, 0, "");
8472                let is_i32 = name == "blob_i32";
8473                match decode_blob(&s) {
8474                    Ok(bytes) => {
8475                        let mut out = Vec::with_capacity(bytes.len() / 4);
8476                        for ch in bytes.chunks_exact(4) {
8477                            let arr = [ch[0], ch[1], ch[2], ch[3]];
8478                            let n = if is_i32 {
8479                                i32::from_le_bytes(arr) as f64
8480                            } else {
8481                                f32::from_le_bytes(arr) as f64
8482                            };
8483                            out.push(Value::Number(n));
8484                        }
8485                        return Ok(Value::List(Rc::new(out)));
8486                    },
8487                    Err(e) => {
8488                        eprintln!("blob decode failed: {e}");
8489                        return Ok(Value::List(Rc::new(vec![])));
8490                    },
8491                }
8492            },
8493
8494            // ══════════════════════════════════════════════════════════════════
8495            // SVG EXPORT  (svg_begin / svg_rect / svg_circle / svg_line /
8496            //              svg_polyline / svg_text / svg_end / hsl_color)
8497            // Chinese aliases: 开始SVG 结束SVG SVG矩形 SVG圆形 SVG线段 SVG折线 SVG文本 HSL颜色
8498            // Thai aliases:    เริ่มSVG จบSVG SVGสี่เหลี่ยม SVGวงกลม SVGเส้น SVGเส้นหัก SVGข้อความ สีHSL
8499            // ══════════════════════════════════════════════════════════════════
8500            "svg_begin" | "开始SVG" | "เริ่มSVG" => {
8501                let path = self.arg_str(&args, 0, "output.svg");
8502                let width = self.arg_num(&args, 1, 800.0)?;
8503                let height = self.arg_num(&args, 2, 600.0)?;
8504                *self.svg.borrow_mut() = Some(SvgWriter::new(path, width, height));
8505                return Ok(Value::Unit);
8506            },
8507
8508            "svg_rect" | "SVG矩形" | "SVGสี่เหลี่ยม" => {
8509                let x = self.arg_num(&args, 0, 0.0)?;
8510                let y = self.arg_num(&args, 1, 0.0)?;
8511                let w = self.arg_num(&args, 2, 10.0)?;
8512                let h = self.arg_num(&args, 3, 10.0)?;
8513                let fill = self.arg_str(&args, 4, "#ffffff");
8514                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8515                    svg.elements.push(format!(
8516                        "<rect x=\"{x:.1}\" y=\"{y:.1}\" width=\"{w:.1}\" \
8517                         height=\"{h:.1}\" fill=\"{fill}\"/>"
8518                    ));
8519                }
8520                return Ok(Value::Unit);
8521            },
8522
8523            "svg_circle" | "SVG圆形" | "SVGวงกลม" => {
8524                let cx = self.arg_num(&args, 0, 0.0)?;
8525                let cy = self.arg_num(&args, 1, 0.0)?;
8526                let r = self.arg_num(&args, 2, 5.0)?;
8527                let fill = self.arg_str(&args, 3, "#ffffff");
8528                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8529                    svg.elements.push(format!(
8530                        "<circle cx=\"{cx:.1}\" cy=\"{cy:.1}\" r=\"{r:.1}\" fill=\"{fill}\"/>"
8531                    ));
8532                }
8533                return Ok(Value::Unit);
8534            },
8535
8536            "svg_line" | "SVG线段" | "SVGเส้น" => {
8537                let x1 = self.arg_num(&args, 0, 0.0)?;
8538                let y1 = self.arg_num(&args, 1, 0.0)?;
8539                let x2 = self.arg_num(&args, 2, 0.0)?;
8540                let y2 = self.arg_num(&args, 3, 0.0)?;
8541                let stroke = self.arg_str(&args, 4, "#ffffff");
8542                let sw = self.arg_num(&args, 5, 1.0)?;
8543                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8544                    svg.elements.push(format!(
8545                        "<line x1=\"{x1:.1}\" y1=\"{y1:.1}\" x2=\"{x2:.1}\" y2=\"{y2:.1}\" \
8546                         stroke=\"{stroke}\" stroke-width=\"{sw:.1}\"/>"
8547                    ));
8548                }
8549                return Ok(Value::Unit);
8550            },
8551
8552            "svg_polyline" | "SVG折线" | "SVGเส้นหัก" => {
8553                let pts = self.arg_str(&args, 0, "");
8554                let stroke = self.arg_str(&args, 1, "#ffffff");
8555                let sw = self.arg_num(&args, 2, 1.0)?;
8556                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8557                    svg.elements.push(format!(
8558                        "<polyline points=\"{pts}\" fill=\"none\" \
8559                         stroke=\"{stroke}\" stroke-width=\"{sw:.1}\"/>"
8560                    ));
8561                }
8562                return Ok(Value::Unit);
8563            },
8564
8565            "svg_text" | "SVG文本" | "SVGข้อความ" => {
8566                let x = self.arg_num(&args, 0, 0.0)?;
8567                let y = self.arg_num(&args, 1, 0.0)?;
8568                let text = self.arg_str(&args, 2, "");
8569                let fill = self.arg_str(&args, 3, "#ffffff");
8570                let size = self.arg_num(&args, 4, 12.0)?;
8571                if let Some(svg) = self.svg.borrow_mut().as_mut() {
8572                    let safe = text
8573                        .replace('&', "&amp;")
8574                        .replace('<', "&lt;")
8575                        .replace('>', "&gt;");
8576                    svg.elements.push(format!(
8577                        "<text x=\"{x:.1}\" y=\"{y:.1}\" fill=\"{fill}\" \
8578                         font-family=\"monospace\" font-size=\"{size:.0}\">{safe}</text>"
8579                    ));
8580                }
8581                return Ok(Value::Unit);
8582            },
8583
8584            "svg_end" | "结束SVG" | "จบSVG" => {
8585                {
8586                    let borrow = self.svg.borrow();
8587                    if let Some(svg) = borrow.as_ref() {
8588                        svg.save()
8589                            .map_err(|e| EvalErr::from(format!("svg_end: {e}")))?;
8590                    }
8591                }
8592                *self.svg.borrow_mut() = None;
8593                return Ok(Value::Unit);
8594            },
8595
8596            "hsl_color" | "HSL颜色" | "สีHSL" => {
8597                let h = self.arg_num(&args, 0, 0.0)?;
8598                let s = self.arg_num(&args, 1, 70.0)?;
8599                let l = self.arg_num(&args, 2, 50.0)?;
8600                return Ok(Value::Str(hsl_to_hex(h, s, l)));
8601            },
8602
8603            // ══════════════════════════════════════════════════════════════════
8604            // FFT / AUDIO ANALYSIS BUILTINS  (native only)
8605            // ══════════════════════════════════════════════════════════════════
8606
8607            // fft_push(samples_list) — feed raw audio samples and run FFT
8608            #[cfg(not(target_arch = "wasm32"))]
8609            "fft_push" | "วิเคราะห์เสียง" | "频谱输入" | "FFT入力" | "FFT입력" =>
8610            {
8611                if let Some(Value::List(v)) = args.first() {
8612                    let samples: Vec<f32> = v
8613                        .iter()
8614                        .filter_map(|x| {
8615                            if let Value::Number(n) = x {
8616                                Some(*n as f32)
8617                            } else {
8618                                None
8619                            }
8620                        })
8621                        .collect();
8622                    self.fft.borrow_mut().push_samples(&samples);
8623                }
8624                return Ok(Value::Unit);
8625            },
8626
8627            // fft_bands(n) → list of n log-spaced magnitude bands (0..1)
8628            #[cfg(not(target_arch = "wasm32"))]
8629            "fft_bands" | "แถบความถี่" | "频段" | "周波数帯" | "주파수대" =>
8630            {
8631                let n = self.arg_num(&args, 0, 32.0)? as usize;
8632                let bands = self.fft.borrow().freq_bands(n);
8633                *self.fft_bands_cache.borrow_mut() = bands.clone();
8634                return Ok(Value::List(Rc::new(
8635                    bands.into_iter().map(|v| Value::Number(v as f64)).collect(),
8636                )));
8637            },
8638
8639            // fft_beat() → bool
8640            #[cfg(not(target_arch = "wasm32"))]
8641            "fft_beat" | "จังหวะเสียง" | "节拍检测" | "ビート検出" | "비트" =>
8642            {
8643                return Ok(Value::Bool(self.fft.borrow().is_beat()));
8644            },
8645
8646            // fft_beat_ratio() → f64  (1.0 = at threshold, >1 = strong beat)
8647            #[cfg(not(target_arch = "wasm32"))]
8648            "fft_beat_ratio" | "อัตราจังหวะ" | "节拍比" | "ビート比" | "비트비율" =>
8649            {
8650                return Ok(Value::Number(self.fft.borrow().beat_ratio() as f64));
8651            },
8652
8653            // fft_rms() → f64
8654            #[cfg(not(target_arch = "wasm32"))]
8655            "fft_rms" | "ระดับRMS" | "均方根" | "二乗平均" | "RMS레벨" => {
8656                return Ok(Value::Number(self.fft.borrow().rms() as f64));
8657            },
8658
8659            // fft_dominant_freq() → f64  in Hz
8660            #[cfg(not(target_arch = "wasm32"))]
8661            "fft_dominant_freq" | "ความถี่หลัก" | "主频" | "主要周波数" | "주파수" =>
8662            {
8663                return Ok(Value::Number(self.fft.borrow().dominant_freq() as f64));
8664            },
8665
8666            // ── wasm32 stubs: fft builtins are no-ops on web ───────────────
8667            #[cfg(target_arch = "wasm32")]
8668            "fft_push" | "วิเคราะห์เสียง" | "频谱输入" | "FFT入力" | "FFT입력" =>
8669            {
8670                return Ok(Value::Unit);
8671            },
8672            #[cfg(target_arch = "wasm32")]
8673            "fft_bands" | "แถบความถี่" | "频段" | "周波数帯" | "주파수대" =>
8674            {
8675                let n = self.arg_num(&args, 0, 32.0)? as usize;
8676                return Ok(Value::List(vec![Value::Number(0.0); n].into()));
8677            },
8678            #[cfg(target_arch = "wasm32")]
8679            "fft_beat" | "จังหวะเสียง" | "节拍检测" | "ビート検出" | "비트" =>
8680            {
8681                return Ok(Value::Bool(false));
8682            },
8683            #[cfg(target_arch = "wasm32")]
8684            "fft_beat_ratio" | "อัตราจังหวะ" | "节拍比" | "ビート比" | "비트비율" =>
8685            {
8686                return Ok(Value::Number(1.0));
8687            },
8688            #[cfg(target_arch = "wasm32")]
8689            "fft_rms" | "ระดับRMS" | "均方根" | "二乗平均" | "RMS레벨" => {
8690                return Ok(Value::Number(0.0));
8691            },
8692            #[cfg(target_arch = "wasm32")]
8693            "fft_dominant_freq" | "ความถี่หลัก" | "主频" | "主要周波数" | "주파수" =>
8694            {
8695                return Ok(Value::Number(0.0));
8696            },
8697
8698            // ══════════════════════════════════════════════════════════════════
8699            // PROCEDURAL TEXTURE BLIT BUILTINS  (screen-space)
8700            // All: name(dst_x, dst_y, width, height, ...params, palette)
8701            // palette: "rainbow" | "fire" | "ocean" | "psychedelic" | "neon" | "forest"
8702            // ══════════════════════════════════════════════════════════════════
8703
8704            // tex_checkerboard(x, y, w, h, tiles, r1,g1,b1, r2,g2,b2)
8705            "tex_checkerboard" | "ลายตารางหมากรุก" => {
8706                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8707                let tiles = self.arg_num(&args, 4, 8.0)? as u32;
8708                let (r1, g1, b1) = (
8709                    self.arg_num(&args, 5, 255.)? as u32,
8710                    self.arg_num(&args, 6, 255.)? as u32,
8711                    self.arg_num(&args, 7, 255.)? as u32,
8712                );
8713                let (r2, g2, b2) = (
8714                    self.arg_num(&args, 8, 0.)? as u32,
8715                    self.arg_num(&args, 9, 0.)? as u32,
8716                    self.arg_num(&args, 10, 0.)? as u32,
8717                );
8718                let c1 = (r1 << 16) | (g1 << 8) | b1;
8719                let c2 = (r2 << 16) | (g2 << 8) | b2;
8720                let mut gfx = self.gfx.borrow_mut();
8721                let (bw, bh) = (gfx.width, gfx.height);
8722                for row in 0..th {
8723                    for col in 0..tw {
8724                        let cx = col as u32 * tiles / tw as u32;
8725                        let cy = row as u32 * tiles / th as u32;
8726                        let (dx, dy) = (tx + col, ty + row);
8727                        if dx < bw && dy < bh {
8728                            gfx.buffer[dy * bw + dx] = if (cx + cy) % 2 == 0 { c1 } else { c2 };
8729                        }
8730                    }
8731                }
8732                return Ok(Value::Unit);
8733            },
8734
8735            // tex_gradient(x, y, w, h, angle_deg, r1,g1,b1, r2,g2,b2)
8736            "tex_gradient" | "ลายไล่สี" => {
8737                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8738                let angle = self.arg_num(&args, 4, 0.0)? as f32;
8739                let (r1, g1, b1) = (
8740                    self.arg_num(&args, 5, 0.)? as f32 / 255.,
8741                    self.arg_num(&args, 6, 0.)? as f32 / 255.,
8742                    self.arg_num(&args, 7, 0.)? as f32 / 255.,
8743                );
8744                let (r2, g2, b2) = (
8745                    self.arg_num(&args, 8, 255.)? as f32 / 255.,
8746                    self.arg_num(&args, 9, 255.)? as f32 / 255.,
8747                    self.arg_num(&args, 10, 255.)? as f32 / 255.,
8748                );
8749                let (ca, sa) = (angle.to_radians().cos(), angle.to_radians().sin());
8750                let mut gfx = self.gfx.borrow_mut();
8751                let (bw, bh) = (gfx.width, gfx.height);
8752                for row in 0..th {
8753                    for col in 0..tw {
8754                        let nx = col as f32 / tw as f32 - 0.5;
8755                        let ny = row as f32 / th as f32 - 0.5;
8756                        let t = ((nx * ca + ny * sa + 0.707) / 1.414).clamp(0., 1.);
8757                        let (dx, dy) = (tx + col, ty + row);
8758                        if dx < bw && dy < bh {
8759                            gfx.buffer[dy * bw + dx] =
8760                                tex_rgb(r1 + (r2 - r1) * t, g1 + (g2 - g1) * t, b1 + (b2 - b1) * t);
8761                        }
8762                    }
8763                }
8764                return Ok(Value::Unit);
8765            },
8766
8767            // tex_noise(x, y, w, h, scale, octaves, seed, palette)
8768            "tex_noise" | "ลายนอยส์" => {
8769                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8770                let scale = self.arg_num(&args, 4, 4.0)? as f32;
8771                let octaves = self.arg_num(&args, 5, 4.0)? as u32;
8772                let seed = self.arg_num(&args, 6, 0.0)? as u32;
8773                let palette = self.arg_str(&args, 7, "rainbow");
8774                let mut gfx = self.gfx.borrow_mut();
8775                let (bw, bh) = (gfx.width, gfx.height);
8776                for row in 0..th {
8777                    for col in 0..tw {
8778                        let v = tex_fbm(
8779                            col as f32 * scale / tw as f32,
8780                            row as f32 * scale / th as f32,
8781                            octaves,
8782                            seed,
8783                        );
8784                        let [r, g, b] = tex_palette(&palette, v);
8785                        let (dx, dy) = (tx + col, ty + row);
8786                        if dx < bw && dy < bh {
8787                            gfx.buffer[dy * bw + dx] = tex_rgb(r, g, b);
8788                        }
8789                    }
8790                }
8791                return Ok(Value::Unit);
8792            },
8793
8794            // tex_freq_map(x, y, w, h, time, speed, palette)
8795            // Uses bands written by the last fft_bands() call.
8796            "tex_freq_map" | "ลายความถี่" => {
8797                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8798                let time = self.arg_num(&args, 4, 0.0)? as f32;
8799                let speed = self.arg_num(&args, 5, 0.3)? as f32;
8800                let palette = self.arg_str(&args, 6, "rainbow");
8801                let bands: Vec<f32> = {
8802                    let c = self.fft_bands_cache.borrow();
8803                    if c.is_empty() {
8804                        vec![0.0; 32]
8805                    } else {
8806                        c.clone()
8807                    }
8808                };
8809                let n = bands.len().max(1);
8810                let mut gfx = self.gfx.borrow_mut();
8811                let (bw, bh) = (gfx.width, gfx.height);
8812                for row in 0..th {
8813                    for col in 0..tw {
8814                        let band_idx = (col * n / tw.max(1)).min(n - 1);
8815                        let mag = bands[band_idx].clamp(0., 1.);
8816                        let fill_y = (mag * th as f32) as usize;
8817                        if row >= th.saturating_sub(fill_y) {
8818                            let t = (col as f32 / tw as f32 + time * speed) % 1.0;
8819                            let [r, g, b] = tex_palette(&palette, t);
8820                            let bright = mag * (1.0 - row as f32 / th as f32 * 0.5);
8821                            let (dx, dy) = (tx + col, ty + row);
8822                            if dx < bw && dy < bh {
8823                                gfx.buffer[dy * bw + dx] =
8824                                    tex_rgb(r * bright, g * bright, b * bright);
8825                            }
8826                        }
8827                    }
8828                }
8829                return Ok(Value::Unit);
8830            },
8831
8832            // tex_spiral(x, y, w, h, freq, bands, time, palette)
8833            "tex_spiral" | "ลายเกลียวหมุน" => {
8834                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8835                let freq = self.arg_num(&args, 4, 5.0)? as f32;
8836                let n_bands = self.arg_num(&args, 5, 8.0)? as f32;
8837                let time = self.arg_num(&args, 6, 0.0)? as f32;
8838                let palette = self.arg_str(&args, 7, "rainbow");
8839                let mut gfx = self.gfx.borrow_mut();
8840                let (bw, bh) = (gfx.width, gfx.height);
8841                for row in 0..th {
8842                    for col in 0..tw {
8843                        let nx = col as f32 / tw as f32 - 0.5;
8844                        let ny = row as f32 / th as f32 - 0.5;
8845                        let r = (nx * nx + ny * ny).sqrt();
8846                        let theta = ny.atan2(nx);
8847                        let t = ((r * freq - theta / std::f32::consts::TAU + time * 0.5) * n_bands
8848                            % 1.0)
8849                            .abs();
8850                        let [cr, cg, cb] = tex_palette(&palette, t);
8851                        let (dx, dy) = (tx + col, ty + row);
8852                        if dx < bw && dy < bh {
8853                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
8854                        }
8855                    }
8856                }
8857                return Ok(Value::Unit);
8858            },
8859
8860            // tex_ripple(x, y, w, h, freq, cx, cy, time, palette)
8861            "tex_ripple" | "ลายระลอก" => {
8862                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8863                let freq = self.arg_num(&args, 4, 10.0)? as f32;
8864                let rcx = self.arg_num(&args, 5, 0.5)? as f32;
8865                let rcy = self.arg_num(&args, 6, 0.5)? as f32;
8866                let time = self.arg_num(&args, 7, 0.0)? as f32;
8867                let palette = self.arg_str(&args, 8, "ocean");
8868                let mut gfx = self.gfx.borrow_mut();
8869                let (bw, bh) = (gfx.width, gfx.height);
8870                for row in 0..th {
8871                    for col in 0..tw {
8872                        let nx = col as f32 / tw as f32 - rcx;
8873                        let ny = row as f32 / th as f32 - rcy;
8874                        let r = (nx * nx + ny * ny).sqrt();
8875                        let t = ((r * freq - time) % 1.0).abs();
8876                        let [cr, cg, cb] = tex_palette(&palette, t);
8877                        let (dx, dy) = (tx + col, ty + row);
8878                        if dx < bw && dy < bh {
8879                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
8880                        }
8881                    }
8882                }
8883                return Ok(Value::Unit);
8884            },
8885
8886            // tex_mandelbrot(x, y, w, h, zoom, cx, cy, max_iter, palette)
8887            "tex_mandelbrot" | "ลายแมนเดลบรอต" => {
8888                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8889                let zoom = self.arg_num(&args, 4, 1.0)?;
8890                let mcx = self.arg_num(&args, 5, -0.5)?;
8891                let mcy = self.arg_num(&args, 6, 0.0)?;
8892                let max_iter = self.arg_num(&args, 7, 64.0)? as u32;
8893                let palette = self.arg_str(&args, 8, "psychedelic");
8894                let mut gfx = self.gfx.borrow_mut();
8895                let (bw, bh) = (gfx.width, gfx.height);
8896                for row in 0..th {
8897                    for col in 0..tw {
8898                        let zx0 = (col as f64 / tw as f64 - 0.5) / zoom + mcx;
8899                        let zy0 = (row as f64 / th as f64 - 0.5) / zoom + mcy;
8900                        let mut x = 0.0f64;
8901                        let mut y = 0.0f64;
8902                        let mut i = 0u32;
8903                        while i < max_iter && x * x + y * y < 4.0 {
8904                            let t = x * x - y * y + zx0;
8905                            y = 2.0 * x * y + zy0;
8906                            x = t;
8907                            i += 1;
8908                        }
8909                        let t = if i == max_iter {
8910                            0.0f32
8911                        } else {
8912                            (i as f32
8913                                - (x as f32 * x as f32 + y as f32 * y as f32).ln().ln()
8914                                    / 2.0f32.ln())
8915                                / max_iter as f32
8916                        };
8917                        let [cr, cg, cb] = tex_palette(&palette, t.clamp(0., 1.));
8918                        let (dx, dy) = (tx + col, ty + row);
8919                        if dx < bw && dy < bh {
8920                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
8921                        }
8922                    }
8923                }
8924                return Ok(Value::Unit);
8925            },
8926
8927            // tex_julia(x, y, w, h, c_re, c_im, max_iter, palette)
8928            "tex_julia" | "ลายจูเลีย" => {
8929                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8930                let c_re = self.arg_num(&args, 4, -0.7)?;
8931                let c_im = self.arg_num(&args, 5, 0.27)?;
8932                let max_iter = self.arg_num(&args, 6, 64.0)? as u32;
8933                let palette = self.arg_str(&args, 7, "neon");
8934                let mut gfx = self.gfx.borrow_mut();
8935                let (bw, bh) = (gfx.width, gfx.height);
8936                for row in 0..th {
8937                    for col in 0..tw {
8938                        let mut zx = (col as f64 / tw as f64 - 0.5) * 3.5;
8939                        let mut zy = (row as f64 / th as f64 - 0.5) * 3.5;
8940                        let mut i = 0u32;
8941                        while i < max_iter && zx * zx + zy * zy < 4.0 {
8942                            let t = zx * zx - zy * zy + c_re;
8943                            zy = 2.0 * zx * zy + c_im;
8944                            zx = t;
8945                            i += 1;
8946                        }
8947                        let t = i as f32 / max_iter as f32;
8948                        let [cr, cg, cb] = tex_palette(&palette, t);
8949                        let (dx, dy) = (tx + col, ty + row);
8950                        if dx < bw && dy < bh {
8951                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
8952                        }
8953                    }
8954                }
8955                return Ok(Value::Unit);
8956            },
8957
8958            // tex_voronoi(x, y, w, h, cells, seed, palette)
8959            "tex_voronoi" | "ลายโวโรนอย" => {
8960                let (tx, ty, tw, th) = self.tex_rect(&args)?;
8961                let cells = self.arg_num(&args, 4, 16.0)? as u32;
8962                let seed = self.arg_num(&args, 5, 42.0)? as u32;
8963                let palette = self.arg_str(&args, 6, "rainbow");
8964                let pts: Vec<[f32; 2]> = (0..cells)
8965                    .map(|i| {
8966                        [
8967                            tex_hash(i as i32, 0, seed),
8968                            tex_hash(i as i32, 1, seed + 999),
8969                        ]
8970                    })
8971                    .collect();
8972                let mut gfx = self.gfx.borrow_mut();
8973                let (bw, bh) = (gfx.width, gfx.height);
8974                for row in 0..th {
8975                    for col in 0..tw {
8976                        let (fx, fy) = (col as f32 / tw as f32, row as f32 / th as f32);
8977                        let (min_d, nearest) = pts.iter().enumerate().fold(
8978                            (f32::MAX, 0usize),
8979                            |(d, idx), (i, &[cx, cy])| {
8980                                let dd = (fx - cx).powi(2) + (fy - cy).powi(2);
8981                                if dd < d {
8982                                    (dd, i)
8983                                } else {
8984                                    (d, idx)
8985                                }
8986                            },
8987                        );
8988                        let t = (nearest as f32 / cells as f32 + min_d * 4.0) % 1.0;
8989                        let [cr, cg, cb] = tex_palette(&palette, t);
8990                        let (dx, dy) = (tx + col, ty + row);
8991                        if dx < bw && dy < bh {
8992                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
8993                        }
8994                    }
8995                }
8996                return Ok(Value::Unit);
8997            },
8998
8999            // tex_halftone(x, y, w, h, dot_size, time, palette)
9000            "tex_halftone" | "ลายฮาล์ฟโทน" => {
9001                let (tx, ty, tw, th) = self.tex_rect(&args)?;
9002                let dot_size = self.arg_num(&args, 4, 0.05)? as f32;
9003                let time = self.arg_num(&args, 5, 0.0)? as f32;
9004                let palette = self.arg_str(&args, 6, "rainbow");
9005                let mut gfx = self.gfx.borrow_mut();
9006                let (bw, bh) = (gfx.width, gfx.height);
9007                for row in 0..th {
9008                    for col in 0..tw {
9009                        let (fx, fy) = (col as f32 / tw as f32, row as f32 / th as f32);
9010                        let gx = (fx / dot_size).floor();
9011                        let gy = (fy / dot_size).floor();
9012                        let lx = (fx / dot_size - gx - 0.5) * 2.0;
9013                        let ly = (fy / dot_size - gy - 0.5) * 2.0;
9014                        let r = (lx * lx + ly * ly).sqrt();
9015                        let t = (gx / (1.0 / dot_size) + time * 0.1) % 1.0;
9016                        let a = if r < 0.7 {
9017                            ((0.7 - r) / 0.7).clamp(0., 1.)
9018                        } else {
9019                            0.0
9020                        };
9021                        if a > 0.0 {
9022                            let [cr, cg, cb] = tex_palette(&palette, t);
9023                            let (dx, dy) = (tx + col, ty + row);
9024                            if dx < bw && dy < bh {
9025                                gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
9026                            }
9027                        }
9028                    }
9029                }
9030                return Ok(Value::Unit);
9031            },
9032
9033            // ══════════════════════════════════════════════════════════════════
9034            // RENDER / LIGHTING MODES  (holographic cel shading)
9035            // ══════════════════════════════════════════════════════════════════
9036            // set_shade_mode(m) — 0 flat · 1 cel · 2 holo (default)
9037            "set_shade_mode" | "设置着色" | "シェード設定" | "셰이드모드" | "ตั้งการแรเงา" =>
9038            {
9039                let m = self.arg_num(&args, 0, 2.0)? as u8;
9040                self.gfx.borrow_mut().shade_mode = m;
9041                return Ok(Value::Unit);
9042            },
9043            // set_cel_bands(n) — number of posterisation bands (>=2)
9044            "set_cel_bands" | "设置色阶" | "セル段数" | "셀밴드" | "ตั้งระดับสี" =>
9045            {
9046                let n = (self.arg_num(&args, 0, 4.0)? as u32).max(2);
9047                self.gfx.borrow_mut().shade.bands = n;
9048                return Ok(Value::Unit);
9049            },
9050            // set_shadow_color(r,g,b) — coloured-shadow tint, 0-255
9051            "set_shadow_color" | "设置阴影色" | "影の色" | "그림자색" | "ตั้งสีเงา" =>
9052            {
9053                let r = self.arg_num(&args, 0, 26.)? as f32 / 255.0;
9054                let g = self.arg_num(&args, 1, 33.)? as f32 / 255.0;
9055                let b = self.arg_num(&args, 2, 77.)? as f32 / 255.0;
9056                self.gfx.borrow_mut().shade.shadow = [r, g, b];
9057                return Ok(Value::Unit);
9058            },
9059            // set_rim(strength, r,g,b) — holographic fresnel edge glow
9060            // ══════════════════════════════════════════════════════════════════
9061            // CRYPTOGRAPHY (ling-crypto) — geo suite, hybrid PQ KEM, holographic
9062            // Bytes cross the language boundary as lowercase hex strings.
9063            // ══════════════════════════════════════════════════════════════════
9064            #[cfg(not(target_arch = "wasm32"))]
9065            "crypto_hash" | "แฮชเข้ารหัส" | "几何哈希" | "幾何ハッシュ" | "기하해시" =>
9066            {
9067                let s = self.arg_str(&args, 0, "");
9068                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(
9069                    s.as_bytes(),
9070                ))));
9071            },
9072            #[cfg(target_arch = "wasm32")]
9073            "crypto_hash" | "แฮชเข้ารหัส" | "几何哈希" | "幾何ハッシュ" | "기하해시" =>
9074            {
9075                let s = self.arg_str(&args, 0, "");
9076                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(
9077                    s.as_bytes(),
9078                ))));
9079            },
9080            // 3-D torus-knot fingerprint of any text/key → flat [x,y,z, x,y,z, …]
9081            #[cfg(not(target_arch = "wasm32"))]
9082            "knot_points" | "จุดปม" | "结点坐标" | "結び目点" | "매듭점" => {
9083                let s = self.arg_str(&args, 0, "");
9084                let shape = ling_crypto::geo::KnotShape::from_bytes(s.as_bytes());
9085                let mut out = Vec::with_capacity(shape.points.len() * 3);
9086                for p in &shape.points {
9087                    out.push(Value::Number(p[0] as f64));
9088                    out.push(Value::Number(p[1] as f64));
9089                    out.push(Value::Number(p[2] as f64));
9090                }
9091                return Ok(Value::List(Rc::new(out)));
9092            },
9093            #[cfg(target_arch = "wasm32")]
9094            "knot_points" | "จุดปม" | "结点坐标" | "結び目点" | "매듭점" => {
9095                let s = self.arg_str(&args, 0, "");
9096                let shape = ling_crypto::geo::KnotShape::from_bytes(s.as_bytes());
9097                let mut out = Vec::with_capacity(shape.points.len() * 3);
9098                for p in &shape.points {
9099                    out.push(Value::Number(p[0] as f64));
9100                    out.push(Value::Number(p[1] as f64));
9101                    out.push(Value::Number(p[2] as f64));
9102                }
9103                return Ok(Value::List(out.into()));
9104            },
9105            #[cfg(not(target_arch = "wasm32"))]
9106            "knot_label" | "ป้ายปม" | "结点标签" | "結び目ラベル" | "매듭라벨" =>
9107            {
9108                let s = self.arg_str(&args, 0, "");
9109                return Ok(Value::Str(
9110                    ling_crypto::geo::KnotShape::from_bytes(s.as_bytes()).label(),
9111                ));
9112            },
9113            #[cfg(target_arch = "wasm32")]
9114            "knot_label" | "ป้ายปม" | "结点标签" | "結び目ラベル" | "매듭라벨" =>
9115            {
9116                let s = self.arg_str(&args, 0, "");
9117                return Ok(Value::Str(
9118                    ling_crypto::geo::KnotShape::from_bytes(s.as_bytes()).label(),
9119                ));
9120            },
9121            // KEM keypair (hybrid X25519+ML-KEM-768) → integer handle
9122            #[cfg(not(target_arch = "wasm32"))]
9123            "knot_keygen" | "hybrid_keygen" | "สร้างกุญแจปม" | "生成密钥" | "鍵生成" | "키생성" =>
9124            {
9125                self.crypto_ids.push(ling_crypto::KnotIdentity::generate());
9126                return Ok(Value::Number((self.crypto_ids.len() - 1) as f64));
9127            },
9128            #[cfg(not(target_arch = "wasm32"))]
9129            "knot_public" | "hybrid_public" | "กุญแจสาธารณะปม" | "公钥" | "公開鍵" | "공개키" =>
9130            {
9131                let h = self.arg_num(&args, 0, 0.0)? as usize;
9132                let pk = self
9133                    .crypto_ids
9134                    .get(h)
9135                    .map(|id| hex_encode(id.public_key()))
9136                    .unwrap_or_default();
9137                return Ok(Value::Str(pk));
9138            },
9139            // encapsulate(pubkey_hex) → [ciphertext_hex, shared_secret_hex]
9140            #[cfg(not(target_arch = "wasm32"))]
9141            "knot_encapsulate"
9142            | "hybrid_encapsulate"
9143            | "ห่อกุญแจปม"
9144            | "封装密钥"
9145            | "カプセル化"
9146            | "캡슐화" => {
9147                let pk = hex_decode(&self.arg_str(&args, 0, ""));
9148                match ling_crypto::geo::knot_encapsulate(&pk) {
9149                    Ok((ct, ss)) => {
9150                        return Ok(Value::List(Rc::new(vec![
9151                            Value::Str(hex_encode(&ct)),
9152                            Value::Str(hex_encode(&ss)),
9153                        ])))
9154                    },
9155                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
9156                }
9157            },
9158            // decapsulate(handle, ciphertext_hex) → shared_secret_hex
9159            #[cfg(not(target_arch = "wasm32"))]
9160            "knot_decapsulate"
9161            | "hybrid_decapsulate"
9162            | "แกะกุญแจปม"
9163            | "解封装密钥"
9164            | "カプセル解除"
9165            | "캡슐해제" => {
9166                let h = self.arg_num(&args, 0, 0.0)? as usize;
9167                let ct = hex_decode(&self.arg_str(&args, 1, ""));
9168                let ss = self
9169                    .crypto_ids
9170                    .get(h)
9171                    .and_then(|id| id.decapsulate(&ct).ok())
9172                    .map(|s| hex_encode(&s))
9173                    .unwrap_or_default();
9174                return Ok(Value::Str(ss));
9175            },
9176            // Authenticated encryption (XChaCha20-Poly1305) — seal(key_hex, text) → ct_hex
9177            #[cfg(not(target_arch = "wasm32"))]
9178            "crypto_seal" | "ผนึก" | "封印" | "封印する" | "봉인" => {
9179                let key = hex_to_32(&self.arg_str(&args, 0, ""));
9180                let pt = self.arg_str(&args, 1, "");
9181                match ling_crypto::geo::holo_seal(key, pt.as_bytes()) {
9182                    Ok(ct) => return Ok(Value::Str(hex_encode(&ct))),
9183                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
9184                }
9185            },
9186            #[cfg(not(target_arch = "wasm32"))]
9187            "crypto_open" | "เปิดผนึก" | "解封" | "封印解除" | "봉인해제" =>
9188            {
9189                let key = hex_to_32(&self.arg_str(&args, 0, ""));
9190                let ct = hex_decode(&self.arg_str(&args, 1, ""));
9191                match ling_crypto::geo::holo_open(key, &ct) {
9192                    Ok(pt) => return Ok(Value::Str(String::from_utf8_lossy(&pt).into_owned())),
9193                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
9194                }
9195            },
9196            // Holographic all-or-nothing transform — 4-D fragment coords [a,b,c,d, …]
9197            #[cfg(not(target_arch = "wasm32"))]
9198            "holo_points" | "จุดโฮโลแกรม" | "全息点" | "ホログラム点" | "홀로그램점" =>
9199            {
9200                let s = self.arg_str(&args, 0, "");
9201                let frags = ling_crypto::geo::scatter(s.as_bytes());
9202                let mut out = Vec::with_capacity(frags.len() * 4);
9203                for f in &frags {
9204                    for c in f.coord {
9205                        out.push(Value::Number(c as f64));
9206                    }
9207                }
9208                return Ok(Value::List(Rc::new(out)));
9209            },
9210            #[cfg(not(target_arch = "wasm32"))]
9211            "holo_fragment_count"
9212            | "จำนวนชิ้นโฮโลแกรม"
9213            | "全息碎片数"
9214            | "ホログラム断片数"
9215            | "홀로그램조각수" => {
9216                let s = self.arg_str(&args, 0, "");
9217                return Ok(Value::Number(
9218                    ling_crypto::geo::scatter(s.as_bytes()).len() as f64
9219                ));
9220            },
9221            // SHAKE-256 XOF, squeezed to an arbitrary output length in bytes
9222            // (`shake_hex(s, 128)` = a 1024-bit seal digest).
9223            #[cfg(not(target_arch = "wasm32"))]
9224            "shake_hex" | "SHAKE哈希" => {
9225                let s = self.arg_str(&args, 0, "");
9226                let len = self.arg_num(&args, 1, 32.0)?.max(0.0) as usize;
9227                return Ok(Value::Str(hex_encode(&ling_crypto::Shake256::hash(s.as_bytes(), len))));
9228            },
9229            // Ed25519 signing keypair (issuer identity) → integer handle.
9230            #[cfg(not(target_arch = "wasm32"))]
9231            "ed25519_keygen" | "생성서명키" => {
9232                self.ed25519_ids.push(ling_crypto::Ed25519Keypair::generate());
9233                return Ok(Value::Number((self.ed25519_ids.len() - 1) as f64));
9234            },
9235            // Deterministic keypair from a 32-byte hex seed — the same seed
9236            // always yields the same keypair, so a program can persist just the
9237            // seed (e.g. a bank's issuer identity) and rederive identical keys
9238            // across restarts instead of every run minting a fresh, unrelated one.
9239            #[cfg(not(target_arch = "wasm32"))]
9240            "ed25519_keygen_from_seed" | "씨앗에서생성서명키" => {
9241                let seed = hex_to_32(&self.arg_str(&args, 0, ""));
9242                self.ed25519_ids.push(ling_crypto::Ed25519Keypair::from_seed(seed));
9243                return Ok(Value::Number((self.ed25519_ids.len() - 1) as f64));
9244            },
9245            #[cfg(not(target_arch = "wasm32"))]
9246            "ed25519_public" | "서명공개키" => {
9247                let h = self.arg_num(&args, 0, 0.0)? as usize;
9248                let pk = self
9249                    .ed25519_ids
9250                    .get(h)
9251                    .map(|kp| hex_encode(&kp.public_key()))
9252                    .unwrap_or_default();
9253                return Ok(Value::Str(pk));
9254            },
9255            // ed25519_sign(handle, message) → signature hex (64 bytes)
9256            #[cfg(not(target_arch = "wasm32"))]
9257            "ed25519_sign" | "서명하다" => {
9258                let h = self.arg_num(&args, 0, 0.0)? as usize;
9259                let msg = self.arg_str(&args, 1, "");
9260                let sig = self
9261                    .ed25519_ids
9262                    .get(h)
9263                    .map(|kp| hex_encode(&kp.sign(msg.as_bytes())))
9264                    .unwrap_or_default();
9265                return Ok(Value::Str(sig));
9266            },
9267            // ed25519_verify(pubkey_hex, message, signature_hex) → bool
9268            #[cfg(not(target_arch = "wasm32"))]
9269            "ed25519_verify" | "서명확인" => {
9270                let pk_hex = self.arg_str(&args, 0, "");
9271                let msg = self.arg_str(&args, 1, "");
9272                let sig_hex = self.arg_str(&args, 2, "");
9273                let pk_bytes = hex_decode(&pk_hex);
9274                let sig_bytes = hex_decode(&sig_hex);
9275                let ok = (|| {
9276                    let pk: [u8; 32] = pk_bytes.try_into().ok()?;
9277                    let sig: [u8; 64] = sig_bytes.try_into().ok()?;
9278                    Some(ling_crypto::Ed25519Keypair::verify(&pk, msg.as_bytes(), &sig).is_ok())
9279                })()
9280                .unwrap_or(false);
9281                return Ok(Value::Bool(ok));
9282            },
9283            // Argon2id password hashing — password_hash(pw) → PHC string,
9284            // password_verify(pw, phc_string) → bool.
9285            #[cfg(not(target_arch = "wasm32"))]
9286            "password_hash" | "비밀번호해시" => {
9287                let pw = self.arg_str(&args, 0, "");
9288                let hash = ling_crypto::Argon2idParams::default()
9289                    .hash_password(pw.as_bytes())
9290                    .unwrap_or_default();
9291                return Ok(Value::Str(hash));
9292            },
9293            #[cfg(not(target_arch = "wasm32"))]
9294            "password_verify" | "비밀번호확인" => {
9295                let pw = self.arg_str(&args, 0, "");
9296                let hash = self.arg_str(&args, 1, "");
9297                let ok = ling_crypto::Argon2idParams::verify_password(pw.as_bytes(), &hash).is_ok();
9298                return Ok(Value::Bool(ok));
9299            },
9300            // OS-CSPRNG random bytes as hex — session ids / nonces (not the
9301            // xorshift `rand` builtin, which is for game logic, not security).
9302            #[cfg(not(target_arch = "wasm32"))]
9303            "random_hex" | "무작위16진수" => {
9304                use rand::RngCore;
9305                let n = self.arg_num(&args, 0, 16.0)?.max(0.0) as usize;
9306                let mut buf = vec![0u8; n];
9307                rand::rngs::OsRng.fill_bytes(&mut buf);
9308                return Ok(Value::Str(hex_encode(&buf)));
9309            },
9310            // base64_encode(s) — text -> base64 (matches what canvas.toDataURL()
9311            // already produces client-side, so PNG uploads never need a binary
9312            // request body).
9313            #[cfg(not(target_arch = "wasm32"))]
9314            "base64_encode" | "base64인코딩" => {
9315                use base64::Engine as _;
9316                let s = self.arg_str(&args, 0, "");
9317                return Ok(Value::Str(
9318                    base64::engine::general_purpose::STANDARD.encode(s.as_bytes()),
9319                ));
9320            },
9321            // base64_decode_to_file(b64, path) — writes decoded bytes straight to
9322            // disk; returns true on success. The only way binary data (an
9323            // uploaded/rendered PNG) reaches the filesystem from `.ling` source.
9324            #[cfg(not(target_arch = "wasm32"))]
9325            "base64_decode_to_file" | "base64파일로저장" => {
9326                use base64::Engine as _;
9327                let b64 = self.arg_str(&args, 0, "");
9328                let path = self.arg_str(&args, 1, "");
9329                let b64 = b64
9330                    .split(',')
9331                    .next_back()
9332                    .unwrap_or(&b64); // tolerate a "data:image/png;base64,..." prefix
9333                let ok = base64::engine::general_purpose::STANDARD
9334                    .decode(b64)
9335                    .ok()
9336                    .and_then(|bytes| std::fs::write(&path, bytes).ok())
9337                    .is_some();
9338                return Ok(Value::Bool(ok));
9339            },
9340            // qr_svg(text) — a scannable QR code as an inline <svg>...</svg>
9341            // string (e.g. for an otpauth:// 2FA enrollment URI). Kept as SVG
9342            // rather than a rasterized image so it fits the same "everything
9343            // stays vector" theme as the banknote seal art.
9344            #[cfg(not(target_arch = "wasm32"))]
9345            "qr_svg" | "QR코드" => {
9346                let text = self.arg_str(&args, 0, "");
9347                let svg = qrcode::QrCode::new(text.as_bytes())
9348                    .map(|code| {
9349                        code.render::<qrcode::render::svg::Color>()
9350                            .min_dimensions(240, 240)
9351                            .dark_color(qrcode::render::svg::Color("#1a0f3d"))
9352                            .light_color(qrcode::render::svg::Color("#ffffff"))
9353                            .build()
9354                    })
9355                    .unwrap_or_default();
9356                return Ok(Value::Str(svg));
9357            },
9358            // zip_files(paths_list, out_path) — bundles files into a zip archive
9359            // (used by the "render" step to package a note's PNG/SVG/PDF).
9360            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
9361            "zip_files" | "압축파일" => {
9362                let paths = match args.first() {
9363                    Some(Value::List(l)) => l.iter().map(|v| v.to_string()).collect::<Vec<_>>(),
9364                    _ => Vec::new(),
9365                };
9366                let out_path = self.arg_str(&args, 1, "out.zip");
9367                let ok = (|| -> std::io::Result<()> {
9368                    let file = std::fs::File::create(&out_path)?;
9369                    let mut writer = zip::ZipWriter::new(file);
9370                    let options: zip::write::FileOptions<'_, ()> = zip::write::FileOptions::default()
9371                        .compression_method(zip::CompressionMethod::Deflated);
9372                    for p in &paths {
9373                        let name = std::path::Path::new(p)
9374                            .file_name()
9375                            .map(|n| n.to_string_lossy().into_owned())
9376                            .unwrap_or_else(|| p.clone());
9377                        let bytes = std::fs::read(p)?;
9378                        writer.start_file(name, options)?;
9379                        std::io::Write::write_all(&mut writer, &bytes)?;
9380                    }
9381                    writer.finish()?;
9382                    Ok(())
9383                })()
9384                .is_ok();
9385                return Ok(Value::Bool(ok));
9386            },
9387            // pdf_from_images(png_paths_list, out_path) — one page per image,
9388            // sized to its pixel dimensions. No PDF crate dependency: `image`
9389            // (decode) and `flate2` (deflate the page's raw RGB stream) are
9390            // already unconditional deps, so this hand-writes the handful of
9391            // PDF objects (Catalog/Pages/Page/Contents/Image XObject) directly.
9392            // `build_pdf_from_images` itself only exists under this same gate.
9393            #[cfg(all(not(target_arch = "wasm32"), feature = "web"))]
9394            "pdf_from_images" | "PDF来自图片" => {
9395                let paths = match args.first() {
9396                    Some(Value::List(l)) => l.iter().map(|v| v.to_string()).collect::<Vec<_>>(),
9397                    _ => Vec::new(),
9398                };
9399                let out_path = self.arg_str(&args, 1, "out.pdf");
9400                let ok = build_pdf_from_images(&paths, &out_path).is_ok();
9401                return Ok(Value::Bool(ok));
9402            },
9403
9404            // ══════════════════════════════════════════════════════════════════
9405            // ling-ui — animation easings + holographic vector widgets + text I/O
9406            // ══════════════════════════════════════════════════════════════════
9407            "ease" => {
9408                let name = self.arg_str(&args, 0, "ease");
9409                let t = self.arg_num(&args, 1, 0.0)? as f32;
9410                return Ok(Value::Number(
9411                    ling_ui::Easing::from_name(&name).apply(t) as f64
9412                ));
9413            },
9414
9415            // ══════════════════════════════════════════════════════════════════
9416            // Anima — unified animation drivers (ling-animation). Organic 灵 +
9417            // mechanical 机 scalar drivers, callable per frame from a script.
9418            // ══════════════════════════════════════════════════════════════════
9419            "tween" | "补间" | "補間" | "트윈" | "แทรกค่า" => {
9420                let a = self.arg_num(&args, 0, 0.0)?;
9421                let b = self.arg_num(&args, 1, 0.0)?;
9422                let t = self.arg_num(&args, 2, 0.0)?.clamp(0.0, 1.0);
9423                return Ok(Value::Number(a + (b - a) * t));
9424            },
9425            "tween_ease" | "缓动补间" | "緩和補間" | "이징트윈" | "แทรกนุ่ม" =>
9426            {
9427                let a = self.arg_num(&args, 0, 0.0)? as f32;
9428                let b = self.arg_num(&args, 1, 0.0)? as f32;
9429                let t = self.arg_num(&args, 2, 0.0)? as f32;
9430                let kind = self.arg_str(&args, 3, "linear");
9431                let e = ling_animation::EaseFunction::from_name(&kind);
9432                return Ok(Value::Number(
9433                    ling_animation::ease::tween_ease(&a, &b, t, e) as f64,
9434                ));
9435            },
9436            // ── Organic 灵 ──
9437            "breathe" | "呼吸" | "호흡" | "หายใจ" => {
9438                let t = self.arg_num(&args, 0, 0.0)? as f32;
9439                let rate = self.arg_num(&args, 1, 1.0)? as f32;
9440                let depth = self.arg_num(&args, 2, 0.1)? as f32;
9441                return Ok(Value::Number(
9442                    ling_animation::scalar::breathe(t, rate, depth) as f64,
9443                ));
9444            },
9445            "wobble" | "摆动" | "揺れ" | "흔들림" | "โยก" => {
9446                let t = self.arg_num(&args, 0, 0.0)? as f32;
9447                let freq = self.arg_num(&args, 1, 1.0)? as f32;
9448                let amp = self.arg_num(&args, 2, 1.0)? as f32;
9449                let phase = self.arg_num(&args, 3, 0.0)? as f32;
9450                return Ok(Value::Number(
9451                    ling_animation::scalar::wobble(t, freq, amp, phase) as f64,
9452                ));
9453            },
9454            "gait_phase" | "步相" | "歩相" | "걸음위상" | "เฟสก้าว" => {
9455                let t = self.arg_num(&args, 0, 0.0)? as f32;
9456                let speed = self.arg_num(&args, 1, 1.0)? as f32;
9457                return Ok(Value::Number(
9458                    ling_animation::scalar::gait_phase(t, speed) as f64
9459                ));
9460            },
9461            "gait_swing" | "步摆" | "歩振り" | "걸음흔들" | "ก้าวแกว่ง" =>
9462            {
9463                let t = self.arg_num(&args, 0, 0.0)? as f32;
9464                let speed = self.arg_num(&args, 1, 1.0)? as f32;
9465                let stride = self.arg_num(&args, 2, 1.0)? as f32;
9466                return Ok(Value::Number(
9467                    ling_animation::scalar::gait_swing(t, speed, stride) as f64,
9468                ));
9469            },
9470            "gait_lift" | "抬脚" | "足上げ" | "발들기" | "ยกเท้า" => {
9471                let t = self.arg_num(&args, 0, 0.0)? as f32;
9472                let speed = self.arg_num(&args, 1, 1.0)? as f32;
9473                let height = self.arg_num(&args, 2, 1.0)? as f32;
9474                return Ok(Value::Number(
9475                    ling_animation::scalar::gait_lift(t, speed, height) as f64,
9476                ));
9477            },
9478            "spring_to" | "弹向" | "バネ寄せ" | "스프링이동" | "สปริงไป" =>
9479            {
9480                let pos = self.arg_num(&args, 0, 0.0)? as f32;
9481                let vel = self.arg_num(&args, 1, 0.0)? as f32;
9482                let target = self.arg_num(&args, 2, 0.0)? as f32;
9483                let stiffness = self.arg_num(&args, 3, 120.0)? as f32;
9484                let damping = self.arg_num(&args, 4, 14.0)? as f32;
9485                let dt = self.arg_num(&args, 5, 1.0 / 60.0)? as f32;
9486                let (np, nv) =
9487                    ling_animation::scalar::spring_step(pos, vel, target, stiffness, damping, dt);
9488                return Ok(Value::List(Rc::new(vec![
9489                    Value::Number(np as f64),
9490                    Value::Number(nv as f64),
9491                ])));
9492            },
9493            "ik2" | "反解" | "逆運動" | "역운동" | "ไอเค2" => {
9494                let l1 = self.arg_num(&args, 0, 1.0)? as f32;
9495                let l2 = self.arg_num(&args, 1, 1.0)? as f32;
9496                let tx = self.arg_num(&args, 2, 0.0)? as f32;
9497                let ty = self.arg_num(&args, 3, 0.0)? as f32;
9498                let (sh, el) = ling_animation::scalar::two_bone_ik(l1, l2, tx, ty);
9499                return Ok(Value::List(Rc::new(vec![
9500                    Value::Number(sh as f64),
9501                    Value::Number(el as f64),
9502                ])));
9503            },
9504            // ── Mechanical 机 ──
9505            "gear_couple" | "齿轮联动" | "歯車連動" | "기어연동" | "เฟืองทด" =>
9506            {
9507                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9508                let ti = self.arg_num(&args, 1, 1.0)? as f32;
9509                let to = self.arg_num(&args, 2, 1.0)? as f32;
9510                return Ok(Value::Number(
9511                    ling_animation::scalar::gear(angle, ti, to) as f64
9512                ));
9513            },
9514            "gear_train" | "齿轮组" | "歯車列" | "기어열" | "ชุดเฟือง" => {
9515                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9516                let teeth: Vec<f32> = match args.get(1) {
9517                    Some(Value::List(items)) => items
9518                        .iter()
9519                        .filter_map(|v| {
9520                            if let Value::Number(n) = v {
9521                                Some(*n as f32)
9522                            } else {
9523                                None
9524                            }
9525                        })
9526                        .collect(),
9527                    _ => Vec::new(),
9528                };
9529                let out = ling_animation::mechanism::gear_train(angle, &teeth);
9530                return Ok(Value::List(Rc::new(
9531                    out.into_iter().map(|a| Value::Number(a as f64)).collect(),
9532                )));
9533            },
9534            "cam_lift" | "凸轮升程" | "カム揚程" | "캠리프트" | "ยกลูกเบี้ยว" =>
9535            {
9536                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9537                let lift = self.arg_num(&args, 1, 1.0)? as f32;
9538                return Ok(Value::Number(
9539                    ling_animation::scalar::cam_lift(angle, lift) as f64
9540                ));
9541            },
9542            "piston" | "活塞" | "ピストン" | "피스톤" | "ลูกสูบ" => {
9543                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9544                let crank = self.arg_num(&args, 1, 1.0)? as f32;
9545                let rod = self.arg_num(&args, 2, 2.0)? as f32;
9546                return Ok(Value::Number(
9547                    ling_animation::scalar::piston(angle, crank, rod) as f64,
9548                ));
9549            },
9550            "rack" | "齿条" | "ラック" | "랙" | "แร็ค" => {
9551                let angle = self.arg_num(&args, 0, 0.0)? as f32;
9552                let radius = self.arg_num(&args, 1, 1.0)? as f32;
9553                return Ok(Value::Number(
9554                    ling_animation::scalar::rack(angle, radius) as f64
9555                ));
9556            },
9557            #[cfg(not(target_arch = "wasm32"))]
9558            "mouse_x" => {
9559                let gfx = self.gfx.borrow();
9560                let v = gfx
9561                    .window
9562                    .as_ref()
9563                    .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
9564                    .map(|p| p.0 as f64)
9565                    .unwrap_or(0.0);
9566                return Ok(Value::Number(v));
9567            },
9568            #[cfg(target_arch = "wasm32")]
9569            "mouse_x" => {
9570                return Ok(Value::Number(crate::gfx::wasm_mouse_x() as f64));
9571            },
9572            #[cfg(not(target_arch = "wasm32"))]
9573            "mouse_y" => {
9574                let gfx = self.gfx.borrow();
9575                let v = gfx
9576                    .window
9577                    .as_ref()
9578                    .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
9579                    .map(|p| p.1 as f64)
9580                    .unwrap_or(0.0);
9581                return Ok(Value::Number(v));
9582            },
9583            #[cfg(target_arch = "wasm32")]
9584            "mouse_y" => {
9585                return Ok(Value::Number(crate::gfx::wasm_mouse_y() as f64));
9586            },
9587            #[cfg(not(target_arch = "wasm32"))]
9588            "mouse_down" => {
9589                let mut gfx = self.gfx.borrow_mut();
9590                let d = !gfx.input_suppressed()
9591                    && gfx
9592                        .window
9593                        .as_ref()
9594                        .map(|w| w.get_mouse_down(minifb::MouseButton::Left))
9595                        .unwrap_or(false);
9596                return Ok(Value::Bool(d));
9597            },
9598            #[cfg(target_arch = "wasm32")]
9599            "mouse_down" => {
9600                return Ok(Value::Bool(crate::gfx::wasm_mouse_down()));
9601            },
9602            #[cfg(not(target_arch = "wasm32"))]
9603            "mouse_down_right" | "เมาส์ขวา" => {
9604                let mut gfx = self.gfx.borrow_mut();
9605                let d = !gfx.input_suppressed()
9606                    && gfx
9607                        .window
9608                        .as_ref()
9609                        .map(|w| w.get_mouse_down(minifb::MouseButton::Right))
9610                        .unwrap_or(false);
9611                return Ok(Value::Bool(d));
9612            },
9613            #[cfg(target_arch = "wasm32")]
9614            "mouse_down_right" | "เมาส์ขวา" => {
9615                return Ok(Value::Bool(crate::gfx::wasm_mouse_down_right()));
9616            },
9617            #[cfg(not(target_arch = "wasm32"))]
9618            "mouse_down_middle" | "เมาส์กลาง" => {
9619                let mut gfx = self.gfx.borrow_mut();
9620                let d = !gfx.input_suppressed()
9621                    && gfx
9622                        .window
9623                        .as_ref()
9624                        .map(|w| w.get_mouse_down(minifb::MouseButton::Middle))
9625                        .unwrap_or(false);
9626                return Ok(Value::Bool(d));
9627            },
9628            #[cfg(target_arch = "wasm32")]
9629            "mouse_down_middle" | "เมาส์กลาง" => {
9630                return Ok(Value::Bool(crate::gfx::wasm_mouse_down_middle()));
9631            },
9632            #[cfg(not(target_arch = "wasm32"))]
9633            "ui_hot" | "热区" | "ホットエリア" | "핫존" | "พื้นที่สัมผัส" =>
9634            {
9635                let x = self.arg_num(&args, 0, 0.0)? as f32;
9636                let y = self.arg_num(&args, 1, 0.0)? as f32;
9637                let w = self.arg_num(&args, 2, 0.0)? as f32;
9638                let h = self.arg_num(&args, 3, 0.0)? as f32;
9639                let gfx = self.gfx.borrow();
9640                let (mx, my) = gfx
9641                    .window
9642                    .as_ref()
9643                    .and_then(|win| win.get_mouse_pos(minifb::MouseMode::Clamp))
9644                    .unwrap_or((0.0, 0.0));
9645                return Ok(Value::Bool(ling_ui::holo::hit_rect(mx, my, x, y, w, h)));
9646            },
9647            #[cfg(target_arch = "wasm32")]
9648            "ui_hot" | "热区" | "ホットエリア" | "핫존" | "พื้นที่สัมผัส" =>
9649            {
9650                return Ok(Value::Bool(false));
9651            },
9652            // ui_text(x, y, scale, "string") — holographic vector text
9653            "ui_text" | "界面文字" | "UI文字" | "UI텍스트" | "ข้อความหน้าจอ" =>
9654            {
9655                let x = self.arg_num(&args, 0, 0.0)? as f32;
9656                let y = self.arg_num(&args, 1, 0.0)? as f32;
9657                let scale = self.arg_num(&args, 2, 16.0)? as f32;
9658                let s = self.arg_str(&args, 3, "");
9659                let segs = ling_ui::holo::text_lines(&s, x, y, scale * 0.62, scale, scale * 0.24);
9660                let mut gfx = self.gfx.borrow_mut();
9661                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
9662                for sg in segs {
9663                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
9664                }
9665                return Ok(Value::Unit);
9666            },
9667            // font_load("path.ttf") — load a vector font (outlines cached lazily as
9668            // cache/fonts/<stem>/<codepoint>.ling). Returns a handle, or -1 on failure.
9669            #[cfg(not(target_arch = "wasm32"))]
9670            "font_load" | "โหลดฟอนต์" | "加载字体" | "フォント読込" | "글꼴로드" =>
9671            {
9672                let path = self.arg_str(&args, 0, "");
9673                // Optional 2nd arg: variable-font weight (e.g. 600 for a solid, bold UI).
9674                let weight = match self.arg_num(&args, 1, 0.0)? {
9675                    w if w > 0.0 => Some(w as f32),
9676                    _ => None,
9677                };
9678                // Try the path as given, then relative to the script's directory.
9679                let mut loaded = ling_graphics::VectorFont::from_path_weight(&path, weight);
9680                if loaded.is_err() {
9681                    if let Some(dir) = &self.source_dir {
9682                        let joined = dir.join(&path);
9683                        loaded = ling_graphics::VectorFont::from_path_weight(
9684                            &joined.to_string_lossy(),
9685                            weight,
9686                        );
9687                    }
9688                }
9689                match loaded {
9690                    Ok(f) => {
9691                        let id = self.fonts.len();
9692                        self.fonts.push(f);
9693                        return Ok(Value::Number(id as f64));
9694                    },
9695                    Err(e) => {
9696                        eprintln!("font_load failed ({path}): {e}");
9697                        return Ok(Value::Number(-1.0));
9698                    },
9699                }
9700            },
9701            #[cfg(target_arch = "wasm32")]
9702            "font_load" | "โหลดฟอนต์" | "加载字体" | "フォント読込" | "글꼴로드" =>
9703            {
9704                // Web runtime does not load host TTF/OTF files yet.
9705                // Return -1 so scripts can fall back to ui_text.
9706                return Ok(Value::Number(-1.0));
9707            },
9708            // image_load("path.png") — decode a raster image (via the `image` crate)
9709            // for pixel sampling (image_width/image_height/image_pixel_r/g/b/a) —
9710            // used by the coin-stamp mosaic tool to read a source photo's
9711            // colour/darkness. Returns a handle, or -1 on failure.
9712            #[cfg(not(target_arch = "wasm32"))]
9713            "image_load" =>
9714            {
9715                let path = self.arg_str(&args, 0, "");
9716                let mut loaded = image::open(&path);
9717                if loaded.is_err() {
9718                    if let Some(dir) = &self.source_dir {
9719                        let joined = dir.join(&path);
9720                        loaded = image::open(&joined);
9721                    }
9722                }
9723                match loaded {
9724                    Ok(img) => {
9725                        let id = self.images.len();
9726                        self.images.push(img.to_rgba8());
9727                        return Ok(Value::Number(id as f64));
9728                    },
9729                    Err(e) => {
9730                        eprintln!("image_load failed ({path}): {e}");
9731                        return Ok(Value::Number(-1.0));
9732                    },
9733                }
9734            },
9735            #[cfg(target_arch = "wasm32")]
9736            "image_load" =>
9737            {
9738                // Web runtime does not load host image files yet.
9739                return Ok(Value::Number(-1.0));
9740            },
9741            "image_width" =>
9742            {
9743                let id = self.arg_num(&args, 0, -1.0)? as i64;
9744                if id >= 0 && (id as usize) < self.images.len() {
9745                    return Ok(Value::Number(self.images[id as usize].width() as f64));
9746                }
9747                return Ok(Value::Number(0.0));
9748            },
9749            "image_height" =>
9750            {
9751                let id = self.arg_num(&args, 0, -1.0)? as i64;
9752                if id >= 0 && (id as usize) < self.images.len() {
9753                    return Ok(Value::Number(self.images[id as usize].height() as f64));
9754                }
9755                return Ok(Value::Number(0.0));
9756            },
9757            "image_pixel_r" | "image_pixel_g" | "image_pixel_b" | "image_pixel_a" =>
9758            {
9759                let id = self.arg_num(&args, 0, -1.0)? as i64;
9760                let px = self.arg_num(&args, 1, 0.0)? as i64;
9761                let py = self.arg_num(&args, 2, 0.0)? as i64;
9762                if id >= 0 && (id as usize) < self.images.len() {
9763                    let img = &self.images[id as usize];
9764                    if px >= 0 && py >= 0 && (px as u32) < img.width() && (py as u32) < img.height() {
9765                        let p = img.get_pixel(px as u32, py as u32);
9766                        let ch = match name {
9767                            "image_pixel_r" => p[0],
9768                            "image_pixel_g" => p[1],
9769                            "image_pixel_b" => p[2],
9770                            _ => p[3],
9771                        };
9772                        return Ok(Value::Number(ch as f64));
9773                    }
9774                }
9775                return Ok(Value::Number(0.0));
9776            },
9777            // image_new(w, h) — a new blank (fully transparent) RGBA image the
9778            // script can paint into with image_set_pixel and write out with
9779            // image_save. Lives in the same self.images table as image_load,
9780            // so image_width/image_height/image_pixel_* all work on it too.
9781            // Used by the coin-stamp tool to build cropped, physically-sized
9782            // (mm x DPI) PNG exports — something a raw window screenshot()
9783            // can't do, since it always captures the whole on-screen
9784            // framebuffer at whatever size the window happens to be.
9785            "image_new" =>
9786            {
9787                let w = self.arg_num(&args, 0, 1.0)?.max(1.0) as u32;
9788                let h = self.arg_num(&args, 1, 1.0)?.max(1.0) as u32;
9789                let id = self.images.len();
9790                self.images.push(image::RgbaImage::new(w, h));
9791                return Ok(Value::Number(id as f64));
9792            },
9793            // image_set_pixel(id, x, y, r, g, b, a) — paint one pixel of an
9794            // image created with image_new (0..255 channels; out-of-bounds is
9795            // a silent no-op, matching image_pixel_*'s own out-of-bounds
9796            // behaviour).
9797            "image_set_pixel" =>
9798            {
9799                let id = self.arg_num(&args, 0, -1.0)? as i64;
9800                let px = self.arg_num(&args, 1, 0.0)? as i64;
9801                let py = self.arg_num(&args, 2, 0.0)? as i64;
9802                let r = self.arg_num(&args, 3, 0.0)?.clamp(0.0, 255.0) as u8;
9803                let g = self.arg_num(&args, 4, 0.0)?.clamp(0.0, 255.0) as u8;
9804                let b = self.arg_num(&args, 5, 0.0)?.clamp(0.0, 255.0) as u8;
9805                let a = self.arg_num(&args, 6, 255.0)?.clamp(0.0, 255.0) as u8;
9806                if id >= 0 && (id as usize) < self.images.len() {
9807                    let img = &mut self.images[id as usize];
9808                    if px >= 0 && py >= 0 && (px as u32) < img.width() && (py as u32) < img.height() {
9809                        img.put_pixel(px as u32, py as u32, image::Rgba([r, g, b, a]));
9810                    }
9811                }
9812                return Ok(Value::Unit);
9813            },
9814            // image_save(id, "path.png") — encode an image (from image_new or
9815            // image_load) to disk, alpha preserved. Returns 1 on success, -1
9816            // on failure (bad id or write error), mirroring image_load's own
9817            // -1-on-failure convention. Path resolves the same way
9818            // write_file/copy_file's outputs do: relative to the script's own
9819            // working directory (typically the app dir the launcher cd's
9820            // into), not source_dir.
9821            #[cfg(not(target_arch = "wasm32"))]
9822            "image_save" =>
9823            {
9824                let id = self.arg_num(&args, 0, -1.0)? as i64;
9825                let path = self.arg_str(&args, 1, "");
9826                if id >= 0 && (id as usize) < self.images.len() {
9827                    if let Some(parent) = std::path::Path::new(&path).parent() {
9828                        if !parent.as_os_str().is_empty() {
9829                            let _ = std::fs::create_dir_all(parent);
9830                        }
9831                    }
9832                    if self.images[id as usize].save(&path).is_ok() {
9833                        return Ok(Value::Number(1.0));
9834                    }
9835                }
9836                return Ok(Value::Number(-1.0));
9837            },
9838            #[cfg(target_arch = "wasm32")]
9839            "image_save" =>
9840            {
9841                return Ok(Value::Number(-1.0));
9842            },
9843            // font_text(handle, x, y, px, "string") — anti-aliased *stroked* vector outline
9844            // in the current set_color / set_blend. (x,y) is the text box top-left.
9845            #[cfg(not(target_arch = "wasm32"))]
9846            "font_text" | "ข้อความฟอนต์" | "字体文本" | "フォント文字" | "글꼴텍스트" =>
9847            {
9848                let id = self.arg_num(&args, 0, 0.0)? as i64;
9849                let x = self.arg_num(&args, 1, 0.0)? as f32;
9850                let y = self.arg_num(&args, 2, 0.0)? as f32;
9851                let px = self.arg_num(&args, 3, 16.0)? as f32;
9852                let s = self.arg_str(&args, 4, "");
9853                if id >= 0 && (id as usize) < self.fonts.len() && px > 0.0 {
9854                    let strokes = self.font_layout_2d(id as usize, x, y, px, &s);
9855                    let mut gfx = self.gfx.borrow_mut();
9856                    let (w, h, color, add, aa) =
9857                        (gfx.width, gfx.height, gfx.color, gfx.blend == 1, gfx.font_antialias);
9858                    for pl in &strokes {
9859                        for seg in pl.windows(2) {
9860                            if aa {
9861                                crate::gfx::raster::draw_line_aa(
9862                                    &mut gfx.buffer,
9863                                    w,
9864                                    h,
9865                                    color,
9866                                    add,
9867                                    seg[0][0],
9868                                    seg[0][1],
9869                                    seg[1][0],
9870                                    seg[1][1],
9871                                );
9872                            } else {
9873                                crate::gfx::raster::draw_line(
9874                                    &mut gfx.buffer,
9875                                    w,
9876                                    h,
9877                                    color,
9878                                    seg[0][0],
9879                                    seg[0][1],
9880                                    seg[1][0],
9881                                    seg[1][1],
9882                                );
9883                            }
9884                        }
9885                    }
9886                }
9887                return Ok(Value::Unit);
9888            },
9889            #[cfg(target_arch = "wasm32")]
9890            "font_text" | "ข้อความฟอนต์" | "字体文本" | "フォント文字" | "글꼴텍스트" =>
9891            {
9892                return Ok(Value::Unit);
9893            },
9894            // font_text_fill(handle, x, y, px, "string") — filled vector glyphs;
9895            // anti-aliased when `set_font_antialias(1)` is on (default off = crisp).
9896            #[cfg(not(target_arch = "wasm32"))]
9897            "font_text_fill" | "เติมฟอนต์" | "填充字体" | "フォント塗り" | "글꼴채움" =>
9898            {
9899                let id = self.arg_num(&args, 0, 0.0)? as i64;
9900                let x = self.arg_num(&args, 1, 0.0)? as f32;
9901                let y = self.arg_num(&args, 2, 0.0)? as f32;
9902                let px = self.arg_num(&args, 3, 16.0)? as f32;
9903                let s = self.arg_str(&args, 4, "");
9904                if id >= 0 && (id as usize) < self.fonts.len() && px > 0.0 {
9905                    // fill each glyph independently so interior holes (winding) stay correct
9906                    let glyphs = self.font_layout_2d_glyphs(id as usize, x, y, px, &s);
9907                    let mut gfx = self.gfx.borrow_mut();
9908                    let (w, h, color, add, aa) =
9909                        (gfx.width, gfx.height, gfx.color, gfx.blend == 1, gfx.font_antialias);
9910                    for contours in &glyphs {
9911                        if aa {
9912                            crate::gfx::raster::fill_contours_aa(
9913                                &mut gfx.buffer,
9914                                w,
9915                                h,
9916                                color,
9917                                add,
9918                                contours,
9919                            );
9920                        } else {
9921                            crate::gfx::raster::fill_contours(
9922                                &mut gfx.buffer,
9923                                w,
9924                                h,
9925                                color,
9926                                add,
9927                                contours,
9928                            );
9929                        }
9930                    }
9931                }
9932                return Ok(Value::Unit);
9933            },
9934            #[cfg(target_arch = "wasm32")]
9935            "font_text_fill" | "เติมฟอนต์" | "填充字体" | "フォント塗り" | "글꼴채움" =>
9936            {
9937                return Ok(Value::Unit);
9938            },
9939            // font_text_3d(handle, cx,cy,cz, ux,uy,uz, vx,vy,vz, size, "string")
9940            // — stroked vector text on a 3D plane: u = advance dir, v = up dir, size = world/em.
9941            //   Flows through the depth-sorted line pipeline, so it rotates with the camera (and 4D).
9942            #[cfg(not(target_arch = "wasm32"))]
9943            "font_text_3d" | "ข้อความฟอนต์3มิติ" | "字体3D" | "フォント3D" | "글꼴3D" =>
9944            {
9945                let id = self.arg_num(&args, 0, 0.0)? as i64;
9946                let cx = self.arg_num(&args, 1, 0.0)? as f32;
9947                let cy = self.arg_num(&args, 2, 0.0)? as f32;
9948                let cz = self.arg_num(&args, 3, 0.0)? as f32;
9949                let ux = self.arg_num(&args, 4, 1.0)? as f32;
9950                let uy = self.arg_num(&args, 5, 0.0)? as f32;
9951                let uz = self.arg_num(&args, 6, 0.0)? as f32;
9952                let vx = self.arg_num(&args, 7, 0.0)? as f32;
9953                let vy = self.arg_num(&args, 8, 1.0)? as f32;
9954                let vz = self.arg_num(&args, 9, 0.0)? as f32;
9955                let size = self.arg_num(&args, 10, 1.0)? as f32;
9956                let s = self.arg_str(&args, 11, "");
9957                // Optional arg 12: fill_rows — when > 0, each glyph interior is
9958                // filled with that many even-odd scanline spans (true filled
9959                // letterforms, not a bounding box). 0/omitted = outline only.
9960                let fill_rows = self.arg_num(&args, 12, 0.0)? as i32;
9961                if id >= 0 && (id as usize) < self.fonts.len() && size > 0.0 {
9962                    // Build world-space polylines: world = C + (pen+ex)*size*U + ey*size*V
9963                    let font = &mut self.fonts[id as usize];
9964                    let asc = font.ascent();
9965                    let mut pen = 0.0f32;
9966                    let mut lines: Vec<[f32; 6]> = Vec::new();
9967                    for ch in s.chars() {
9968                        let go = font.glyph_outline(ch, 0.01);
9969                        let map = |p: [f32; 2], pen: f32| {
9970                            let a = pen + p[0];
9971                            let b = p[1] - asc; // shift so the top of the cap sits near C
9972                            [
9973                                cx + a * size * ux + b * size * vx,
9974                                cy + a * size * uy + b * size * vy,
9975                                cz + a * size * uz + b * size * vz,
9976                            ]
9977                        };
9978                        for pl in &go.polylines {
9979                            for seg in pl.windows(2) {
9980                                let p0 = map(seg[0], pen);
9981                                let p1 = map(seg[1], pen);
9982                                lines.push([p0[0], p0[1], p0[2], p1[0], p1[1], p1[2]]);
9983                            }
9984                        }
9985                        if fill_rows > 0 {
9986                            // Even-odd scanline fill in glyph space. Contours may
9987                            // omit their closing edge, so the implicit last→first
9988                            // segment is scanned too (skipped when degenerate).
9989                            let (mut ymin, mut ymax) = (f32::MAX, f32::MIN);
9990                            for pl in &go.polylines {
9991                                for p in pl {
9992                                    ymin = ymin.min(p[1]);
9993                                    ymax = ymax.max(p[1]);
9994                                }
9995                            }
9996                            if ymax > ymin {
9997                                for r in 0..fill_rows {
9998                                    let y =
9999                                        ymin + (r as f32 + 0.5) * (ymax - ymin) / fill_rows as f32;
10000                                    let mut xs: Vec<f32> = Vec::new();
10001                                    for pl in &go.polylines {
10002                                        let n = pl.len();
10003                                        if n < 2 {
10004                                            continue;
10005                                        }
10006                                        for k in 0..n {
10007                                            let p0 = pl[k];
10008                                            let p1 = pl[(k + 1) % n];
10009                                            if k + 1 == n
10010                                                && (p1[0] - p0[0]).abs() < 1e-6
10011                                                && (p1[1] - p0[1]).abs() < 1e-6
10012                                            {
10013                                                continue; // contour already closed
10014                                            }
10015                                            let (y0, y1) = (p0[1], p1[1]);
10016                                            if (y0 <= y && y1 > y) || (y1 <= y && y0 > y) {
10017                                                let t = (y - y0) / (y1 - y0);
10018                                                xs.push(p0[0] + t * (p1[0] - p0[0]));
10019                                            }
10020                                        }
10021                                    }
10022                                    xs.sort_by(|a, b| {
10023                                        a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
10024                                    });
10025                                    let mut k = 0;
10026                                    while k + 1 < xs.len() {
10027                                        let a = map([xs[k], y], pen);
10028                                        let b = map([xs[k + 1], y], pen);
10029                                        lines.push([a[0], a[1], a[2], b[0], b[1], b[2]]);
10030                                        k += 2;
10031                                    }
10032                                }
10033                            }
10034                        }
10035                        pen += go.advance;
10036                    }
10037                    let mut gfx = self.gfx.borrow_mut();
10038                    let color = gfx.color;
10039                    let near = -gfx.camera.zdist + 0.05;
10040                    for l in &lines {
10041                        let (mut ax, mut ay, mut az) = (l[0], l[1], l[2]);
10042                        let (mut bx, mut by, mut bz) = (l[3], l[4], l[5]);
10043                        let da = gfx.camera.depth(ax, ay, az);
10044                        let db = gfx.camera.depth(bx, by, bz);
10045                        if da <= near && db <= near {
10046                            continue;
10047                        }
10048                        if da <= near {
10049                            let t = (near - da) / (db - da);
10050                            ax += t * (bx - ax);
10051                            ay += t * (by - ay);
10052                            az += t * (bz - az);
10053                        } else if db <= near {
10054                            let t = (near - da) / (db - da);
10055                            bx = ax + t * (bx - ax);
10056                            by = ay + t * (by - ay);
10057                            bz = az + t * (bz - az);
10058                        }
10059                        let (sax, say, da2) = gfx.camera.project(ax, ay, az);
10060                        let (sbx, sby, db2) = gfx.camera.project(bx, by, bz);
10061                        let depth = (da2 + db2) / 2.0;
10062                        gfx.depth_queue.push_line(depth, color, sax, say, sbx, sby);
10063                    }
10064                }
10065                return Ok(Value::Unit);
10066            },
10067            #[cfg(target_arch = "wasm32")]
10068            "font_text_3d" | "ข้อความฟอนต์3มิติ" | "字体3D" | "フォント3D" | "글꼴3D" =>
10069            {
10070                return Ok(Value::Unit);
10071            },
10072            // font_width(handle, px, "string") — pixel width of a string in a loaded font.
10073            #[cfg(not(target_arch = "wasm32"))]
10074            "font_width" | "ความกว้างฟอนต์" | "字体宽度" | "フォント幅" | "글꼴너비" =>
10075            {
10076                let id = self.arg_num(&args, 0, 0.0)? as i64;
10077                let px = self.arg_num(&args, 1, 16.0)? as f32;
10078                let s = self.arg_str(&args, 2, "");
10079                if id >= 0 && (id as usize) < self.fonts.len() {
10080                    return Ok(Value::Number(self.fonts[id as usize].measure(&s, px) as f64));
10081                }
10082                return Ok(Value::Number(0.0));
10083            },
10084            #[cfg(target_arch = "wasm32")]
10085            "font_width" | "ความกว้างฟอนต์" | "字体宽度" | "フォント幅" | "글꼴너비" =>
10086            {
10087                return Ok(Value::Number(0.0));
10088            },
10089            // font_glyph_outline(handle, "char", tol_em) — flattened vector outline of
10090            // ONE glyph in normalized em space (x→right, y→up, baseline at 0). Returns a
10091            // list of contours; each contour is a flat list [x0,y0,x1,y1,…]. Curves are
10092            // subdivided so deviation stays under tol_em (default 0.01). Empty on failure.
10093            #[cfg(not(target_arch = "wasm32"))]
10094            "font_glyph_outline" | "font_outline" | "เส้นขอบฟอนต์" | "字体轮廓"
10095            | "フォント輪郭" | "글꼴윤곽" => {
10096                let id = self.arg_num(&args, 0, 0.0)? as i64;
10097                let s = self.arg_str(&args, 1, "");
10098                let tol = self.arg_num(&args, 2, 0.01)? as f32;
10099                let ch = s.chars().next().unwrap_or(' ');
10100                if id >= 0 && (id as usize) < self.fonts.len() {
10101                    let go = self.fonts[id as usize].glyph_outline(ch, tol.max(1e-4));
10102                    let mut contours: Vec<Value> = Vec::with_capacity(go.polylines.len());
10103                    for pl in &go.polylines {
10104                        let mut flat: Vec<Value> = Vec::with_capacity(pl.len() * 2);
10105                        for p in pl {
10106                            flat.push(Value::Number(p[0] as f64));
10107                            flat.push(Value::Number(p[1] as f64));
10108                        }
10109                        contours.push(Value::List(Rc::new(flat)));
10110                    }
10111                    return Ok(Value::List(Rc::new(contours)));
10112                }
10113                return Ok(Value::List(Rc::new(vec![])));
10114            },
10115            #[cfg(target_arch = "wasm32")]
10116            "font_glyph_outline" | "font_outline" | "เส้นขอบฟอนต์" | "字体轮廓"
10117            | "フォント輪郭" | "글꼴윤곽" => {
10118                return Ok(Value::List(Rc::new(vec![])));
10119            },
10120            // font_advance(handle, "char") — normalized em advance width of ONE glyph
10121            // (baseline metric, ignores side bearings). Multiply by px for pixels.
10122            #[cfg(not(target_arch = "wasm32"))]
10123            "font_advance" | "ระยะฟอนต์" | "字体步进" | "フォント送り" | "글꼴전진" => {
10124                let id = self.arg_num(&args, 0, 0.0)? as i64;
10125                let s = self.arg_str(&args, 1, "");
10126                let ch = s.chars().next().unwrap_or(' ');
10127                if id >= 0 && (id as usize) < self.fonts.len() {
10128                    return Ok(Value::Number(self.fonts[id as usize].advance(ch) as f64));
10129                }
10130                return Ok(Value::Number(0.0));
10131            },
10132            #[cfg(target_arch = "wasm32")]
10133            "font_advance" | "ระยะฟอนต์" | "字体步进" | "フォント送り" | "글꼴전진" => {
10134                return Ok(Value::Number(0.0));
10135            },
10136
10137            // ui_frame(x,y,w,h, bracketLen) — sci-fi corner brackets
10138            "ui_frame" | "边框" | "フレーム枠" | "프레임틀" | "กรอบ" => {
10139                let x = self.arg_num(&args, 0, 0.0)? as f32;
10140                let y = self.arg_num(&args, 1, 0.0)? as f32;
10141                let w0 = self.arg_num(&args, 2, 0.0)? as f32;
10142                let h0 = self.arg_num(&args, 3, 0.0)? as f32;
10143                let l = self.arg_num(&args, 4, 14.0)? as f32;
10144                let segs = ling_ui::holo::corner_brackets(x, y, w0, h0, l);
10145                let mut gfx = self.gfx.borrow_mut();
10146                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
10147                for sg in segs {
10148                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
10149                }
10150                return Ok(Value::Unit);
10151            },
10152            // ui_bevel(x,y,w,h, bevel) — beveled holographic panel outline
10153            "ui_bevel" | "斜角框" | "ベベル枠" | "베벨틀" | "กรอบเฉียง" =>
10154            {
10155                let x = self.arg_num(&args, 0, 0.0)? as f32;
10156                let y = self.arg_num(&args, 1, 0.0)? as f32;
10157                let w0 = self.arg_num(&args, 2, 0.0)? as f32;
10158                let h0 = self.arg_num(&args, 3, 0.0)? as f32;
10159                let bv = self.arg_num(&args, 4, 10.0)? as f32;
10160                let segs = ling_ui::holo::beveled_rect(x, y, w0, h0, bv);
10161                let mut gfx = self.gfx.borrow_mut();
10162                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
10163                for sg in segs {
10164                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
10165                }
10166                return Ok(Value::Unit);
10167            },
10168
10169            // ══════════════════════════════════════════════════════════════════
10170            // VECTOR UI TOOLKIT  (crates/ling-ui/src/widgets.rs)
10171            // All widgets are vector + theme-coloured with an optional trailing
10172            // r,g,b override; interactive ones read the mouse and return state.
10173            // ══════════════════════════════════════════════════════════════════
10174            #[cfg(not(target_arch = "wasm32"))]
10175            "ui_theme" | "界面主题" | "UIテーマ" | "인터페이스테마" | "ธีมส่วนติดต่อ" =>
10176            {
10177                let cur = self.ui_theme;
10178                let primary = self.color_at(&args, 0, cur.primary);
10179                let accent = self.color_at(&args, 3, cur.accent);
10180                let track = self.color_at(&args, 6, cur.track);
10181                let warn = self.color_at(&args, 9, cur.warn);
10182                let text = self.color_at(&args, 12, cur.text);
10183                let bg = self.color_at(&args, 15, cur.bg);
10184                self.ui_theme = UiTheme { primary, accent, track, warn, text, bg };
10185                return Ok(Value::Unit);
10186            },
10187
10188            // ui_theme_colors() -> [pr,pg,pb, ar,ag,ab, tr,tg,tb, wr,wg,wb,
10189            // xr,xg,xb, br,bg,bb] — the live theme every ui_* widget already
10190            // draws from (primary/accent/track/warn/text/bg, each 0-255),
10191            // so script-drawn UI (e.g. a hand-rolled text field) can match it
10192            // instead of guessing its own colours.
10193            "ui_theme_colors" | "인터페이스테마색상" => {
10194                let th = self.ui_theme;
10195                let mut out = Vec::with_capacity(18);
10196                for c in [th.primary, th.accent, th.track, th.warn, th.text, th.bg] {
10197                    out.push(Value::Number(((c >> 16) & 0xFF) as f64));
10198                    out.push(Value::Number(((c >> 8) & 0xFF) as f64));
10199                    out.push(Value::Number((c & 0xFF) as f64));
10200                }
10201                return Ok(Value::List(Rc::new(out)));
10202            },
10203
10204            // ── HUD ──────────────────────────────────────────────────────────
10205            #[cfg(not(target_arch = "wasm32"))]
10206            "ui_radar" | "雷达" | "レーダー" | "레이더" | "เรดาร์" => {
10207                let cx = self.arg_num(&args, 0, 0.)? as f32;
10208                let cy = self.arg_num(&args, 1, 0.)? as f32;
10209                let r = self.arg_num(&args, 2, 60.)? as f32;
10210                let sweep = self.arg_num(&args, 3, 0.)? as f32;
10211                let th = self.ui_theme;
10212                let prim = self.color_at(&args, 4, th.primary);
10213                self.draw_ui(&ling_ui::widgets::radar(
10214                    cx, cy, r, sweep, prim, th.accent, th.track,
10215                ));
10216                return Ok(Value::Unit);
10217            },
10218            #[cfg(not(target_arch = "wasm32"))]
10219            "ui_compass" | "罗盘" | "コンパス" | "나침반" | "เข็มทิศ" => {
10220                let x = self.arg_num(&args, 0, 0.)? as f32;
10221                let y = self.arg_num(&args, 1, 0.)? as f32;
10222                let w0 = self.arg_num(&args, 2, 300.)? as f32;
10223                let h0 = self.arg_num(&args, 3, 24.)? as f32;
10224                let head = self.arg_num(&args, 4, 0.)? as f32;
10225                let th = self.ui_theme;
10226                let prim = self.color_at(&args, 5, th.primary);
10227                self.draw_ui(&ling_ui::widgets::compass(
10228                    x, y, w0, h0, head, prim, th.track,
10229                ));
10230                return Ok(Value::Unit);
10231            },
10232            #[cfg(not(target_arch = "wasm32"))]
10233            "ui_reticle" | "准星" | "照準" | "조준선" | "เป้าเล็ง" => {
10234                let cx = self.arg_num(&args, 0, 0.)? as f32;
10235                let cy = self.arg_num(&args, 1, 0.)? as f32;
10236                let r = self.arg_num(&args, 2, 30.)? as f32;
10237                let spread = self.arg_num(&args, 3, 0.)? as f32;
10238                let th = self.ui_theme;
10239                let prim = self.color_at(&args, 4, th.primary);
10240                self.draw_ui(&ling_ui::widgets::reticle(cx, cy, r, spread, prim));
10241                return Ok(Value::Unit);
10242            },
10243            #[cfg(not(target_arch = "wasm32"))]
10244            "ui_target" | "锁定框" | "ターゲット" | "표적" | "กรอบเป้า" =>
10245            {
10246                let x = self.arg_num(&args, 0, 0.)? as f32;
10247                let y = self.arg_num(&args, 1, 0.)? as f32;
10248                let w0 = self.arg_num(&args, 2, 80.)? as f32;
10249                let h0 = self.arg_num(&args, 3, 80.)? as f32;
10250                let lock = self.arg_num(&args, 4, 0.)? as f32;
10251                let th = self.ui_theme;
10252                let prim = self.color_at(&args, 5, th.primary);
10253                self.draw_ui(&ling_ui::widgets::target(
10254                    x, y, w0, h0, lock, prim, th.accent,
10255                ));
10256                return Ok(Value::Unit);
10257            },
10258            #[cfg(not(target_arch = "wasm32"))]
10259            "ui_panel" | "面板" | "パネル" | "패널" | "แผง" => {
10260                let x = self.arg_num(&args, 0, 0.)? as f32;
10261                let y = self.arg_num(&args, 1, 0.)? as f32;
10262                let w0 = self.arg_num(&args, 2, 200.)? as f32;
10263                let h0 = self.arg_num(&args, 3, 120.)? as f32;
10264                let bv = self.arg_num(&args, 4, 12.)? as f32;
10265                let th = self.ui_theme;
10266                let prim = self.color_at(&args, 5, th.primary);
10267                self.draw_ui(&ling_ui::widgets::panel(x, y, w0, h0, bv, prim, th.bg));
10268                return Ok(Value::Unit);
10269            },
10270            #[cfg(not(target_arch = "wasm32"))]
10271            "ui_scanlines" | "扫描线" | "走査線" | "스캔라인" | "เส้นสแกน" =>
10272            {
10273                let x = self.arg_num(&args, 0, 0.)? as f32;
10274                let y = self.arg_num(&args, 1, 0.)? as f32;
10275                let w0 = self.arg_num(&args, 2, 200.)? as f32;
10276                let h0 = self.arg_num(&args, 3, 120.)? as f32;
10277                let dens = self.arg_num(&args, 4, 24.)? as usize;
10278                let th = self.ui_theme;
10279                let line = self.color_at(&args, 5, th.track);
10280                self.draw_ui(&ling_ui::widgets::scanlines(x, y, w0, h0, dens, line));
10281                return Ok(Value::Unit);
10282            },
10283
10284            // ── Meters ───────────────────────────────────────────────────────
10285            #[cfg(not(target_arch = "wasm32"))]
10286            "ui_bar" | "进度条" | "バー" | "막대" | "แถบ" => {
10287                let x = self.arg_num(&args, 0, 0.)? as f32;
10288                let y = self.arg_num(&args, 1, 0.)? as f32;
10289                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10290                let h0 = self.arg_num(&args, 3, 16.)? as f32;
10291                let val = self.arg_num(&args, 4, 0.)? as f32;
10292                let max = self.arg_num(&args, 5, 1.)? as f32;
10293                let th = self.ui_theme;
10294                let fill = self.color_at(&args, 6, th.primary);
10295                self.draw_ui(&ling_ui::widgets::bar(
10296                    x,
10297                    y,
10298                    w0,
10299                    h0,
10300                    val / max.max(1e-6),
10301                    fill,
10302                    th.track,
10303                ));
10304                return Ok(Value::Unit);
10305            },
10306            #[cfg(not(target_arch = "wasm32"))]
10307            "ui_segbar" | "分段条" | "分割バー" | "분할막대" | "แถบแบ่ง" =>
10308            {
10309                let x = self.arg_num(&args, 0, 0.)? as f32;
10310                let y = self.arg_num(&args, 1, 0.)? as f32;
10311                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10312                let h0 = self.arg_num(&args, 3, 16.)? as f32;
10313                let val = self.arg_num(&args, 4, 0.)? as f32;
10314                let max = self.arg_num(&args, 5, 1.)? as f32;
10315                let segs = self.arg_num(&args, 6, 10.)? as usize;
10316                let th = self.ui_theme;
10317                let fill = self.color_at(&args, 7, th.primary);
10318                self.draw_ui(&ling_ui::widgets::segbar(
10319                    x,
10320                    y,
10321                    w0,
10322                    h0,
10323                    val / max.max(1e-6),
10324                    segs,
10325                    fill,
10326                    th.track,
10327                ));
10328                return Ok(Value::Unit);
10329            },
10330            #[cfg(not(target_arch = "wasm32"))]
10331            "ui_gauge" | "仪表" | "ゲージ" | "게이지" | "มาตรวัด" => {
10332                let cx = self.arg_num(&args, 0, 0.)? as f32;
10333                let cy = self.arg_num(&args, 1, 0.)? as f32;
10334                let r = self.arg_num(&args, 2, 50.)? as f32;
10335                let val = self.arg_num(&args, 3, 0.)? as f32;
10336                let max = self.arg_num(&args, 4, 1.)? as f32;
10337                let th = self.ui_theme;
10338                let needle = self.color_at(&args, 5, th.warn);
10339                self.draw_ui(&ling_ui::widgets::gauge(
10340                    cx,
10341                    cy,
10342                    r,
10343                    val / max.max(1e-6),
10344                    needle,
10345                    th.accent,
10346                    th.track,
10347                ));
10348                return Ok(Value::Unit);
10349            },
10350            #[cfg(not(target_arch = "wasm32"))]
10351            "ui_ring" | "环表" | "リングメーター" | "링미터" | "วงแหวนวัด" =>
10352            {
10353                let cx = self.arg_num(&args, 0, 0.)? as f32;
10354                let cy = self.arg_num(&args, 1, 0.)? as f32;
10355                let r = self.arg_num(&args, 2, 40.)? as f32;
10356                let val = self.arg_num(&args, 3, 0.)? as f32;
10357                let max = self.arg_num(&args, 4, 1.)? as f32;
10358                let th = self.ui_theme;
10359                let fill = self.color_at(&args, 5, th.primary);
10360                self.draw_ui(&ling_ui::widgets::ring(
10361                    cx,
10362                    cy,
10363                    r,
10364                    val / max.max(1e-6),
10365                    fill,
10366                    th.track,
10367                ));
10368                return Ok(Value::Unit);
10369            },
10370            #[cfg(not(target_arch = "wasm32"))]
10371            "ui_vu" | "音量条" | "VUメーター" | "음량막대" | "มาตรเสียง" =>
10372            {
10373                let x = self.arg_num(&args, 0, 0.)? as f32;
10374                let y = self.arg_num(&args, 1, 0.)? as f32;
10375                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10376                let h0 = self.arg_num(&args, 3, 60.)? as f32;
10377                let levels = self.arg_list_f32(&args, 4);
10378                let th = self.ui_theme;
10379                let fill = self.color_at(&args, 5, th.primary);
10380                self.draw_ui(&ling_ui::widgets::vu(x, y, w0, h0, &levels, fill, th.warn));
10381                return Ok(Value::Unit);
10382            },
10383            #[cfg(not(target_arch = "wasm32"))]
10384            "ui_spark" | "迷你图" | "スパークライン" | "스파크라인" | "กราฟจิ๋ว" =>
10385            {
10386                let x = self.arg_num(&args, 0, 0.)? as f32;
10387                let y = self.arg_num(&args, 1, 0.)? as f32;
10388                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10389                let h0 = self.arg_num(&args, 3, 40.)? as f32;
10390                let vals = self.arg_list_f32(&args, 4);
10391                let th = self.ui_theme;
10392                let line = self.color_at(&args, 5, th.accent);
10393                self.draw_ui(&ling_ui::widgets::spark(x, y, w0, h0, &vals, line));
10394                return Ok(Value::Unit);
10395            },
10396            #[cfg(not(target_arch = "wasm32"))]
10397            "ui_battery" | "电池" | "バッテリー" | "배터리" | "แบตเตอรี่" =>
10398            {
10399                let x = self.arg_num(&args, 0, 0.)? as f32;
10400                let y = self.arg_num(&args, 1, 0.)? as f32;
10401                let w0 = self.arg_num(&args, 2, 50.)? as f32;
10402                let h0 = self.arg_num(&args, 3, 22.)? as f32;
10403                let val = self.arg_num(&args, 4, 1.)? as f32;
10404                let max = self.arg_num(&args, 5, 1.)? as f32;
10405                let th = self.ui_theme;
10406                let fill = self.color_at(&args, 6, th.accent);
10407                self.draw_ui(&ling_ui::widgets::battery(
10408                    x,
10409                    y,
10410                    w0,
10411                    h0,
10412                    val / max.max(1e-6),
10413                    fill,
10414                    th.track,
10415                    th.warn,
10416                ));
10417                return Ok(Value::Unit);
10418            },
10419
10420            // ── Interface controls (interactive → return state) ──────────────
10421            #[cfg(not(target_arch = "wasm32"))]
10422            "ui_button" | "按钮" | "ボタン" | "버튼" | "ปุ่ม" => {
10423                let x = self.arg_num(&args, 0, 0.)? as f32;
10424                let y = self.arg_num(&args, 1, 0.)? as f32;
10425                let w0 = self.arg_num(&args, 2, 120.)? as f32;
10426                let h0 = self.arg_num(&args, 3, 40.)? as f32;
10427                let (mx, my, down) = self.mouse_now();
10428                let hover = ling_ui::holo::hit_rect(mx, my, x, y, w0, h0);
10429                let clicked = hover && down && !self.mouse_was_down;
10430                let th = self.ui_theme;
10431                let prim = self.color_at(&args, 4, th.primary);
10432                self.draw_ui(&ling_ui::widgets::button(
10433                    x,
10434                    y,
10435                    w0,
10436                    h0,
10437                    hover,
10438                    down && hover,
10439                    prim,
10440                    th.bg,
10441                ));
10442                return Ok(Value::Number(if clicked { 1.0 } else { 0.0 }));
10443            },
10444            #[cfg(not(target_arch = "wasm32"))]
10445            "ui_toggle" | "开关" | "トグル" | "토글" | "สวิตช์" => {
10446                let x = self.arg_num(&args, 0, 0.)? as f32;
10447                let y = self.arg_num(&args, 1, 0.)? as f32;
10448                let w0 = self.arg_num(&args, 2, 52.)? as f32;
10449                let h0 = self.arg_num(&args, 3, 24.)? as f32;
10450                let mut state = self.arg_num(&args, 4, 0.)? > 0.5;
10451                let (mx, my, down) = self.mouse_now();
10452                let hover = ling_ui::holo::hit_rect(mx, my, x, y, w0, h0);
10453                if hover && down && !self.mouse_was_down {
10454                    state = !state;
10455                }
10456                let th = self.ui_theme;
10457                let on = self.color_at(&args, 5, th.accent);
10458                self.draw_ui(&ling_ui::widgets::toggle(x, y, w0, h0, state, on, th.track));
10459                return Ok(Value::Number(if state { 1.0 } else { 0.0 }));
10460            },
10461            #[cfg(not(target_arch = "wasm32"))]
10462            "ui_slider" | "滑块" | "スライダー" | "슬라이더" | "แถบเลื่อน" =>
10463            {
10464                let x = self.arg_num(&args, 0, 0.)? as f32;
10465                let y = self.arg_num(&args, 1, 0.)? as f32;
10466                let w0 = self.arg_num(&args, 2, 160.)? as f32;
10467                let mut val = self.arg_num(&args, 3, 0.)? as f32;
10468                let mn = self.arg_num(&args, 4, 0.)? as f32;
10469                let mx_ = self.arg_num(&args, 5, 1.)? as f32;
10470                let (mx, my, down) = self.mouse_now();
10471                let hover = ling_ui::holo::hit_rect(mx, my, x - 8.0, y - 10.0, w0 + 16.0, 20.0);
10472                if hover && down {
10473                    let frac = ((mx - x) / w0).clamp(0.0, 1.0);
10474                    val = mn + (mx_ - mn) * frac;
10475                }
10476                let frac = ((val - mn) / (mx_ - mn).abs().max(1e-6)).clamp(0.0, 1.0);
10477                let th = self.ui_theme;
10478                let fill = self.color_at(&args, 6, th.primary);
10479                self.draw_ui(&ling_ui::widgets::slider(
10480                    x, y, w0, frac, hover, fill, th.track,
10481                ));
10482                return Ok(Value::Number(val as f64));
10483            },
10484            #[cfg(not(target_arch = "wasm32"))]
10485            "ui_checkbox" | "复选框" | "チェックボックス" | "체크박스" | "ช่องเลือก" =>
10486            {
10487                let x = self.arg_num(&args, 0, 0.)? as f32;
10488                let y = self.arg_num(&args, 1, 0.)? as f32;
10489                let s = self.arg_num(&args, 2, 20.)? as f32;
10490                let mut checked = self.arg_num(&args, 3, 0.)? > 0.5;
10491                let (mx, my, down) = self.mouse_now();
10492                let hover = ling_ui::holo::hit_rect(mx, my, x, y, s, s);
10493                if hover && down && !self.mouse_was_down {
10494                    checked = !checked;
10495                }
10496                let th = self.ui_theme;
10497                let prim = self.color_at(&args, 4, th.primary);
10498                self.draw_ui(&ling_ui::widgets::checkbox(
10499                    x, y, s, checked, hover, prim, th.track,
10500                ));
10501                return Ok(Value::Number(if checked { 1.0 } else { 0.0 }));
10502            },
10503            #[cfg(not(target_arch = "wasm32"))]
10504            "ui_tabs" | "标签页" | "タブ" | "탭" | "แท็บ" => {
10505                let x = self.arg_num(&args, 0, 0.)? as f32;
10506                let y = self.arg_num(&args, 1, 0.)? as f32;
10507                let w0 = self.arg_num(&args, 2, 240.)? as f32;
10508                let h0 = self.arg_num(&args, 3, 28.)? as f32;
10509                let count = self.arg_num(&args, 4, 3.)? as usize;
10510                let mut active = self.arg_num(&args, 5, 0.)? as i32;
10511                let (mx, my, down) = self.mouse_now();
10512                let mut hover = -1;
10513                if my >= y && my <= y + h0 && mx >= x && mx <= x + w0 && count > 0 {
10514                    hover = (((mx - x) / (w0 / count as f32)) as i32)
10515                        .max(0)
10516                        .min(count as i32 - 1);
10517                    if down && !self.mouse_was_down {
10518                        active = hover;
10519                    }
10520                }
10521                let th = self.ui_theme;
10522                let prim = self.color_at(&args, 6, th.primary);
10523                self.draw_ui(&ling_ui::widgets::tabs(
10524                    x,
10525                    y,
10526                    w0,
10527                    h0,
10528                    count,
10529                    active as usize,
10530                    hover,
10531                    prim,
10532                    th.track,
10533                ));
10534                return Ok(Value::Number(active as f64));
10535            },
10536            #[cfg(not(target_arch = "wasm32"))]
10537            "ui_progress" | "进度" | "プログレス" | "진행바" | "ความคืบหน้า" =>
10538            {
10539                let x = self.arg_num(&args, 0, 0.)? as f32;
10540                let y = self.arg_num(&args, 1, 0.)? as f32;
10541                let w0 = self.arg_num(&args, 2, 200.)? as f32;
10542                let h0 = self.arg_num(&args, 3, 12.)? as f32;
10543                let frac = self.arg_num(&args, 4, 0.)? as f32;
10544                let th = self.ui_theme;
10545                let fill = self.color_at(&args, 5, th.accent);
10546                self.draw_ui(&ling_ui::widgets::progress(
10547                    x, y, w0, h0, frac, fill, th.track,
10548                ));
10549                return Ok(Value::Unit);
10550            },
10551            #[cfg(not(target_arch = "wasm32"))]
10552            "ui_tooltip" | "提示框" | "ツールチップ" | "툴팁" | "คำแนะนำ" =>
10553            {
10554                let x = self.arg_num(&args, 0, 0.)? as f32;
10555                let y = self.arg_num(&args, 1, 0.)? as f32;
10556                let w0 = self.arg_num(&args, 2, 120.)? as f32;
10557                let h0 = self.arg_num(&args, 3, 28.)? as f32;
10558                let th = self.ui_theme;
10559                let prim = self.color_at(&args, 4, th.primary);
10560                self.draw_ui(&ling_ui::widgets::tooltip(x, y, w0, h0, prim, th.bg));
10561                return Ok(Value::Unit);
10562            },
10563            #[cfg(not(target_arch = "wasm32"))]
10564            "ui_stepper" | "步进器" | "ステッパー" | "스테퍼" | "ตัวปรับค่า" =>
10565            {
10566                let x = self.arg_num(&args, 0, 0.)? as f32;
10567                let y = self.arg_num(&args, 1, 0.)? as f32;
10568                let w0 = self.arg_num(&args, 2, 120.)? as f32;
10569                let h0 = self.arg_num(&args, 3, 28.)? as f32;
10570                let mut val = self.arg_num(&args, 4, 0.)? as f32;
10571                let step = self.arg_num(&args, 5, 1.)? as f32;
10572                let (mx, my, down) = self.mouse_now();
10573                let hm = ling_ui::holo::hit_rect(mx, my, x, y, h0, h0);
10574                let hp = ling_ui::holo::hit_rect(mx, my, x + w0 - h0, y, h0, h0);
10575                if down && !self.mouse_was_down {
10576                    if hm {
10577                        val -= step;
10578                    }
10579                    if hp {
10580                        val += step;
10581                    }
10582                }
10583                let th = self.ui_theme;
10584                let prim = self.color_at(&args, 6, th.primary);
10585                self.draw_ui(&ling_ui::widgets::stepper(
10586                    x, y, w0, h0, hm, hp, prim, th.track,
10587                ));
10588                return Ok(Value::Number(val as f64));
10589            },
10590
10591            // ── Game UI ──────────────────────────────────────────────────────
10592            #[cfg(not(target_arch = "wasm32"))]
10593            "ui_healthbar" | "血条" | "体力バー" | "체력바" | "แถบพลังชีวิต" =>
10594            {
10595                let x = self.arg_num(&args, 0, 0.)? as f32;
10596                let y = self.arg_num(&args, 1, 0.)? as f32;
10597                let w0 = self.arg_num(&args, 2, 180.)? as f32;
10598                let h0 = self.arg_num(&args, 3, 16.)? as f32;
10599                let val = self.arg_num(&args, 4, 1.)? as f32;
10600                let max = self.arg_num(&args, 5, 1.)? as f32;
10601                let pulse = self.arg_num(&args, 6, 0.)? as f32;
10602                let th = self.ui_theme;
10603                let full = self.color_at(&args, 7, th.accent);
10604                self.draw_ui(&ling_ui::widgets::healthbar(
10605                    x,
10606                    y,
10607                    w0,
10608                    h0,
10609                    val / max.max(1e-6),
10610                    pulse,
10611                    full,
10612                    th.warn,
10613                    th.track,
10614                ));
10615                return Ok(Value::Unit);
10616            },
10617            #[cfg(not(target_arch = "wasm32"))]
10618            "ui_cooldown" | "冷却" | "クールダウン" | "쿨다운" | "คูลดาวน์" =>
10619            {
10620                let cx = self.arg_num(&args, 0, 0.)? as f32;
10621                let cy = self.arg_num(&args, 1, 0.)? as f32;
10622                let r = self.arg_num(&args, 2, 28.)? as f32;
10623                let frac = self.arg_num(&args, 3, 0.)? as f32;
10624                let th = self.ui_theme;
10625                let fill = self.color_at(&args, 4, th.primary);
10626                self.draw_ui(&ling_ui::widgets::cooldown(cx, cy, r, frac, fill, th.track));
10627                return Ok(Value::Unit);
10628            },
10629            #[cfg(not(target_arch = "wasm32"))]
10630            "ui_counter" | "计数器" | "カウンター" | "카운터" | "ตัวนับ" => {
10631                let x = self.arg_num(&args, 0, 0.)? as f32;
10632                let y = self.arg_num(&args, 1, 0.)? as f32;
10633                let dw = self.arg_num(&args, 2, 14.)? as f32;
10634                let dh = self.arg_num(&args, 3, 24.)? as f32;
10635                let val = self.arg_num(&args, 4, 0.)? as i64;
10636                let digits = self.arg_num(&args, 5, 4.)? as usize;
10637                let th = self.ui_theme;
10638                let on = self.color_at(&args, 6, th.primary);
10639                let off = ling_ui::widgets::shade(th.track, 0.5);
10640                self.draw_ui(&ling_ui::widgets::counter(
10641                    x, y, dw, dh, val, digits, on, off,
10642                ));
10643                return Ok(Value::Unit);
10644            },
10645            #[cfg(not(target_arch = "wasm32"))]
10646            "ui_minimap" | "小地图" | "ミニマップ" | "미니맵" | "แผนที่ย่อ" =>
10647            {
10648                let x = self.arg_num(&args, 0, 0.)? as f32;
10649                let y = self.arg_num(&args, 1, 0.)? as f32;
10650                let w0 = self.arg_num(&args, 2, 140.)? as f32;
10651                let h0 = self.arg_num(&args, 3, 140.)? as f32;
10652                let th = self.ui_theme;
10653                let prim = self.color_at(&args, 4, th.primary);
10654                self.draw_ui(&ling_ui::widgets::minimap(x, y, w0, h0, prim, th.bg));
10655                return Ok(Value::Unit);
10656            },
10657            #[cfg(not(target_arch = "wasm32"))]
10658            "ui_dpad" | "方向键" | "方向パッド" | "방향패드" | "ปุ่มทิศทาง" =>
10659            {
10660                let cx = self.arg_num(&args, 0, 0.)? as f32;
10661                let cy = self.arg_num(&args, 1, 0.)? as f32;
10662                let r = self.arg_num(&args, 2, 50.)? as f32;
10663                let (mx, my, down) = self.mouse_now();
10664                let mut dir = 0;
10665                if down {
10666                    let (dx, dy) = (mx - cx, my - cy);
10667                    if dx * dx + dy * dy <= r * r {
10668                        if dx.abs() > dy.abs() {
10669                            dir = if dx > 0.0 { 2 } else { 4 };
10670                        } else {
10671                            dir = if dy > 0.0 { 3 } else { 1 };
10672                        }
10673                    }
10674                }
10675                let th = self.ui_theme;
10676                let prim = self.color_at(&args, 3, th.primary);
10677                self.draw_ui(&ling_ui::widgets::dpad(cx, cy, r, dir, prim, th.track));
10678                return Ok(Value::Number(dir as f64));
10679            },
10680            #[cfg(not(target_arch = "wasm32"))]
10681            "ui_slotgrid" | "物品格" | "スロットグリッド" | "슬롯격자" | "ช่องไอเทม" =>
10682            {
10683                let x = self.arg_num(&args, 0, 0.)? as f32;
10684                let y = self.arg_num(&args, 1, 0.)? as f32;
10685                let cols = self.arg_num(&args, 2, 4.)? as usize;
10686                let rows = self.arg_num(&args, 3, 1.)? as usize;
10687                let cell = self.arg_num(&args, 4, 36.)? as f32;
10688                let sel = self.arg_num(&args, 5, -1.)? as i32;
10689                let th = self.ui_theme;
10690                let prim = self.color_at(&args, 6, th.primary);
10691                self.draw_ui(&ling_ui::widgets::slotgrid(
10692                    x, y, cols, rows, cell, sel, prim, th.track,
10693                ));
10694                return Ok(Value::Unit);
10695            },
10696            #[cfg(not(target_arch = "wasm32"))]
10697            "ui_vignette" | "暗角" | "ビネット" | "비네트" | "ขอบมืด" => {
10698                let intensity = self.arg_num(&args, 0, 0.5)? as f32;
10699                let (w, h) = {
10700                    let g = self.gfx.borrow();
10701                    (g.width as f32, g.height as f32)
10702                };
10703                let th = self.ui_theme;
10704                let col = self.color_at(&args, 1, th.warn);
10705                self.draw_ui(&ling_ui::widgets::vignette(w, h, intensity, col));
10706                return Ok(Value::Unit);
10707            },
10708
10709            // ── Faux-3D in 2D space ──────────────────────────────────────────
10710            #[cfg(not(target_arch = "wasm32"))]
10711            "ui_gauge3d" | "立体仪表" | "立体ゲージ" | "입체게이지" | "มาตรวัด3มิติ" =>
10712            {
10713                let cx = self.arg_num(&args, 0, 0.)? as f32;
10714                let cy = self.arg_num(&args, 1, 0.)? as f32;
10715                let r = self.arg_num(&args, 2, 50.)? as f32;
10716                let val = self.arg_num(&args, 3, 0.)? as f32;
10717                let max = self.arg_num(&args, 4, 1.)? as f32;
10718                let spin = self.arg_num(&args, 5, 0.)? as f32;
10719                let th = self.ui_theme;
10720                let fill = self.color_at(&args, 6, th.primary);
10721                self.draw_ui(&ling_ui::widgets::gauge3d(
10722                    cx,
10723                    cy,
10724                    r,
10725                    val / max.max(1e-6),
10726                    spin,
10727                    fill,
10728                    th.track,
10729                ));
10730                return Ok(Value::Unit);
10731            },
10732            #[cfg(not(target_arch = "wasm32"))]
10733            "ui_panel3d" | "立体面板" | "立体パネル" | "입체패널" | "แผง3มิติ" =>
10734            {
10735                let x = self.arg_num(&args, 0, 0.)? as f32;
10736                let y = self.arg_num(&args, 1, 0.)? as f32;
10737                let w0 = self.arg_num(&args, 2, 200.)? as f32;
10738                let h0 = self.arg_num(&args, 3, 120.)? as f32;
10739                let depth = self.arg_num(&args, 4, 14.)? as f32;
10740                let th = self.ui_theme;
10741                let prim = self.color_at(&args, 5, th.primary);
10742                self.draw_ui(&ling_ui::widgets::panel3d(x, y, w0, h0, depth, prim, th.bg));
10743                return Ok(Value::Unit);
10744            },
10745            #[cfg(not(target_arch = "wasm32"))]
10746            "ui_radar3d" | "立体雷达" | "立体レーダー" | "입체레이더" | "เรดาร์3มิติ" =>
10747            {
10748                let cx = self.arg_num(&args, 0, 0.)? as f32;
10749                let cy = self.arg_num(&args, 1, 0.)? as f32;
10750                let r = self.arg_num(&args, 2, 60.)? as f32;
10751                let tilt = self.arg_num(&args, 3, 0.9)? as f32;
10752                let sweep = self.arg_num(&args, 4, 0.)? as f32;
10753                let th = self.ui_theme;
10754                let prim = self.color_at(&args, 5, th.primary);
10755                self.draw_ui(&ling_ui::widgets::radar3d(
10756                    cx, cy, r, tilt, sweep, prim, th.track,
10757                ));
10758                return Ok(Value::Unit);
10759            },
10760
10761            // ── Interface sounds ─────────────────────────────────────────────
10762            #[cfg(not(target_arch = "wasm32"))]
10763            "audio_blip" | "提示音" | "ビープ音" | "효과음" | "เสียงบี๊บ" =>
10764            {
10765                let freq = self.arg_num(&args, 0, 660.)? as f32;
10766                let dur = self.arg_num(&args, 1, 0.08)? as f32;
10767                let wave = Wave::from_name(&self.arg_str(&args, 2, "sine"));
10768                let amp = self.arg_num(&args, 3, 0.25)? as f32;
10769                if let Some(audio) = &self.audio {
10770                    audio.blip(freq, amp, dur, wave);
10771                }
10772                return Ok(Value::Unit);
10773            },
10774            #[cfg(not(target_arch = "wasm32"))]
10775            "ui_sound" | "界面音" | "UI音" | "인터페이스음" | "เสียงปุ่ม" =>
10776            {
10777                let name = self.arg_str(&args, 0, "click");
10778                if let Some(audio) = &self.audio {
10779                    match name.as_str() {
10780                        "hover" => audio.blip(880.0, 0.10, 0.04, Wave::Sine),
10781                        "confirm" => {
10782                            audio.blip(660.0, 0.22, 0.07, Wave::Square);
10783                            audio.blip(990.0, 0.18, 0.10, Wave::Square);
10784                        },
10785                        "error" => {
10786                            audio.blip(180.0, 0.30, 0.16, Wave::Saw);
10787                            audio.blip(140.0, 0.30, 0.18, Wave::Saw);
10788                        },
10789                        "toggle" => audio.blip(520.0, 0.22, 0.05, Wave::Triangle),
10790                        "tick" => audio.blip(1500.0, 0.12, 0.02, Wave::Square),
10791                        _ => audio.blip(720.0, 0.26, 0.05, Wave::Square), // "click"
10792                    }
10793                }
10794                return Ok(Value::Unit);
10795            },
10796
10797            // ══════════════════════════════════════════════════════════════════
10798            // MUSIC TOOLKIT  (crates/ling-music) — decode · analysis · GM synth ·
10799            // rhythm · karaoke. Analysis/decoding need no audio device; playback
10800            // and synthesis lazily start a dedicated music engine.
10801            // ══════════════════════════════════════════════════════════════════
10802
10803            // music_load(path) -> track handle (decodes WAV/FLAC/OGG/MP3/AAC)
10804            #[cfg(not(target_arch = "wasm32"))]
10805            "music_load" | "载入音乐" | "音楽読込" | "음악로드" | "โหลดเพลง" =>
10806            {
10807                let path = self.arg_str(&args, 0, "");
10808                let resolved = if std::path::Path::new(&path).exists() {
10809                    path.clone()
10810                } else if let Some(d) = &self.source_dir {
10811                    d.join(&path).to_string_lossy().into_owned()
10812                } else {
10813                    path.clone()
10814                };
10815                match ling_music::load(&resolved) {
10816                    Ok(t) => {
10817                        let id = self.tracks.len();
10818                        self.tracks.push(t);
10819                        return Ok(Value::Number(id as f64));
10820                    },
10821                    Err(e) => {
10822                        eprintln!("music_load failed ({path}): {e}");
10823                        return Ok(Value::Number(-1.0));
10824                    },
10825                }
10826            },
10827            #[cfg(not(target_arch = "wasm32"))]
10828            "music_duration" | "音乐时长" | "音楽長さ" | "음악길이" | "ความยาวเพลง" =>
10829            {
10830                let id = self.arg_num(&args, 0, 0.0)? as i64;
10831                let d = self
10832                    .tracks
10833                    .get(id as usize)
10834                    .map(|t| t.duration)
10835                    .unwrap_or(0.0);
10836                return Ok(Value::Number(d as f64));
10837            },
10838            #[cfg(not(target_arch = "wasm32"))]
10839            "music_bpm" | "节拍速度" | "テンポ" | "템포" | "จังหวะต่อนาที" =>
10840            {
10841                let id = self.arg_num(&args, 0, 0.0)? as i64;
10842                let b = self
10843                    .tracks
10844                    .get(id as usize)
10845                    .map(|t| ling_music::analysis::bpm(&t.mono, t.rate))
10846                    .unwrap_or(0.0);
10847                return Ok(Value::Number(b as f64));
10848            },
10849            #[cfg(not(target_arch = "wasm32"))]
10850            "music_key" | "调性" | "調性" | "조성" | "คีย์เพลง" => {
10851                let id = self.arg_num(&args, 0, 0.0)? as i64;
10852                let k = self
10853                    .tracks
10854                    .get(id as usize)
10855                    .map(|t| ling_music::analysis::key_name(&t.mono, t.rate))
10856                    .unwrap_or_default();
10857                return Ok(Value::Str(k));
10858            },
10859            #[cfg(not(target_arch = "wasm32"))]
10860            "music_onsets" | "音符起点" | "オンセット" | "온셋" | "จุดเริ่มเสียง" =>
10861            {
10862                let id = self.arg_num(&args, 0, 0.0)? as i64;
10863                let v = self
10864                    .tracks
10865                    .get(id as usize)
10866                    .map(|t| ling_music::analysis::onsets(&t.mono, t.rate))
10867                    .unwrap_or_default();
10868                return Ok(Value::List(Rc::new(
10869                    v.into_iter().map(|x| Value::Number(x as f64)).collect(),
10870                )));
10871            },
10872            #[cfg(not(target_arch = "wasm32"))]
10873            "music_beat_grid" | "节拍网格" | "ビートグリッド" | "비트그리드" | "กริดจังหวะ" =>
10874            {
10875                let id = self.arg_num(&args, 0, 0.0)? as i64;
10876                let beats = self
10877                    .tracks
10878                    .get(id as usize)
10879                    .map(|t| {
10880                        let b = ling_music::analysis::bpm(&t.mono, t.rate);
10881                        ling_music::analysis::beat_grid(&t.mono, t.rate, b)
10882                    })
10883                    .unwrap_or_default();
10884                return Ok(Value::List(Rc::new(
10885                    beats.into_iter().map(|x| Value::Number(x as f64)).collect(),
10886                )));
10887            },
10888
10889            // ── playback ──
10890            #[cfg(not(target_arch = "wasm32"))]
10891            "music_play" | "播放音乐" | "音楽再生" | "음악재생" | "เล่นเพลง" =>
10892            {
10893                let id = self.arg_num(&args, 0, 0.0)? as i64;
10894                if self.ensure_music() {
10895                    let track = self
10896                        .tracks
10897                        .get(id as usize)
10898                        .map(|t| (t.stereo.clone(), t.rate));
10899                    if let (Some((st, rate)), Some(m)) = (track, &self.music) {
10900                        m.set_track(st, rate);
10901                        m.play();
10902                    } else if let Some(m) = &self.music {
10903                        m.play();
10904                    }
10905                }
10906                return Ok(Value::Unit);
10907            },
10908            #[cfg(not(target_arch = "wasm32"))]
10909            "music_pause" | "暂停音乐" | "音楽一時停止" | "음악일시정지" | "หยุดเพลงชั่วคราว" =>
10910            {
10911                if let Some(m) = &self.music {
10912                    m.pause();
10913                }
10914                return Ok(Value::Unit);
10915            },
10916            #[cfg(not(target_arch = "wasm32"))]
10917            "music_stop" | "停止音乐" | "音楽停止" | "음악정지" | "หยุดเพลง" =>
10918            {
10919                if let Some(m) = &self.music {
10920                    m.stop();
10921                }
10922                return Ok(Value::Unit);
10923            },
10924            #[cfg(not(target_arch = "wasm32"))]
10925            "music_seek" | "定位音乐" | "音楽シーク" | "음악탐색" | "ค้นหาเพลง" =>
10926            {
10927                let sec = self.arg_num(&args, 0, 0.0)? as f32;
10928                if let Some(m) = &self.music {
10929                    m.seek(sec);
10930                }
10931                return Ok(Value::Unit);
10932            },
10933            #[cfg(not(target_arch = "wasm32"))]
10934            "music_pos" | "音乐位置" | "音楽位置" | "음악위치" | "ตำแหน่งเพลง" =>
10935            {
10936                let p = self.music.as_ref().map(|m| m.position()).unwrap_or(0.0);
10937                return Ok(Value::Number(p as f64));
10938            },
10939            #[cfg(not(target_arch = "wasm32"))]
10940            "music_volume" | "音乐音量" | "音楽音量" | "음악음량" | "ระดับเพลง" =>
10941            {
10942                let v = self.arg_num(&args, 0, 0.8)? as f32;
10943                if self.ensure_music() {
10944                    if let Some(m) = &self.music {
10945                        m.set_volume(v);
10946                    }
10947                }
10948                return Ok(Value::Unit);
10949            },
10950
10951            // ── synthesis (GM-capable, patches from .ling files) ──
10952            #[cfg(not(target_arch = "wasm32"))]
10953            "music_patch" | "乐器音色" | "音色読込" | "악기패치" | "แพตช์เครื่องดนตรี" =>
10954            {
10955                let path = self.arg_str(&args, 0, "");
10956                let resolved = if std::path::Path::new(&path).exists() {
10957                    path.clone()
10958                } else if let Some(d) = &self.source_dir {
10959                    d.join(&path).to_string_lossy().into_owned()
10960                } else {
10961                    path.clone()
10962                };
10963                if !self.ensure_music() {
10964                    return Ok(Value::Number(-1.0));
10965                }
10966                match ling_music::patch::from_path(&resolved) {
10967                    Ok(p) => {
10968                        let id = self.music.as_ref().unwrap().add_patch(p);
10969                        return Ok(Value::Number(id as f64));
10970                    },
10971                    Err(e) => {
10972                        eprintln!("music_patch failed ({path}): {e}");
10973                        return Ok(Value::Number(-1.0));
10974                    },
10975                }
10976            },
10977            #[cfg(not(target_arch = "wasm32"))]
10978            "music_note" | "弹音符" | "音符演奏" | "음표연주" | "เล่นโน้ต" =>
10979            {
10980                let inst = self.arg_num(&args, 0, 0.0)? as usize;
10981                let midi = self.pitch_arg(&args, 1, 60);
10982                let dur = self.arg_num(&args, 2, 0.5)? as f32;
10983                let vel = self.arg_num(&args, 3, 0.9)? as f32;
10984                if self.ensure_music() {
10985                    if let Some(m) = &self.music {
10986                        m.note(inst, midi, vel, dur);
10987                    }
10988                }
10989                return Ok(Value::Unit);
10990            },
10991            #[cfg(not(target_arch = "wasm32"))]
10992            "music_note_on" | "音符开始" | "音符オン" | "음표켜기" | "โน้ตเริ่ม" =>
10993            {
10994                let inst = self.arg_num(&args, 0, 0.0)? as usize;
10995                let midi = self.pitch_arg(&args, 1, 60);
10996                let vel = self.arg_num(&args, 2, 0.9)? as f32;
10997                if self.ensure_music() {
10998                    if let Some(m) = &self.music {
10999                        m.note_on(inst, midi, vel);
11000                    }
11001                }
11002                return Ok(Value::Unit);
11003            },
11004            #[cfg(not(target_arch = "wasm32"))]
11005            "music_note_off" | "音符结束" | "音符オフ" | "음표끄기" | "โน้ตจบ" =>
11006            {
11007                let inst = self.arg_num(&args, 0, 0.0)? as usize;
11008                let midi = self.pitch_arg(&args, 1, 60);
11009                if let Some(m) = &self.music {
11010                    m.note_off(inst, midi);
11011                }
11012                return Ok(Value::Unit);
11013            },
11014
11015            // ── rhythm-game judging ──
11016            #[cfg(not(target_arch = "wasm32"))]
11017            "music_judge" | "判定" | "判定する" | "판정" | "ตัดสินจังหวะ" =>
11018            {
11019                let delta_ms = self.arg_num(&args, 0, 9999.0)? as f32;
11020                return Ok(Value::Number(
11021                    ling_music::Grade::judge(delta_ms).index() as f64
11022                ));
11023            },
11024            #[cfg(not(target_arch = "wasm32"))]
11025            "music_grade_name" | "判定名" | "判定名称" | "판정이름" | "ชื่อการตัดสิน" =>
11026            {
11027                let idx = self.arg_num(&args, 0, 4.0)? as i32;
11028                return Ok(Value::Str(
11029                    ling_music::Grade::from_index(idx).name().to_string(),
11030                ));
11031            },
11032
11033            // ── karaoke ──
11034            #[cfg(not(target_arch = "wasm32"))]
11035            "music_lrc" | "载入歌词" | "歌詞読込" | "가사로드" | "โหลดเนื้อเพลง" =>
11036            {
11037                let path = self.arg_str(&args, 0, "");
11038                let resolved = if std::path::Path::new(&path).exists() {
11039                    path.clone()
11040                } else if let Some(d) = &self.source_dir {
11041                    d.join(&path).to_string_lossy().into_owned()
11042                } else {
11043                    path.clone()
11044                };
11045                match std::fs::read_to_string(&resolved) {
11046                    Ok(text) => {
11047                        let id = self.lyrics.len();
11048                        self.lyrics.push(ling_music::Lyrics::parse(&text));
11049                        return Ok(Value::Number(id as f64));
11050                    },
11051                    Err(e) => {
11052                        eprintln!("music_lrc failed ({path}): {e}");
11053                        return Ok(Value::Number(-1.0));
11054                    },
11055                }
11056            },
11057            #[cfg(not(target_arch = "wasm32"))]
11058            "music_lyric" | "当前歌词" | "現在歌詞" | "현재가사" | "เนื้อเพลงปัจจุบัน" =>
11059            {
11060                let id = self.arg_num(&args, 0, 0.0)? as i64;
11061                let t = self.arg_num(&args, 1, 0.0)? as f32;
11062                let line = self
11063                    .lyrics
11064                    .get(id as usize)
11065                    .map(|l| l.line_at(t).to_string())
11066                    .unwrap_or_default();
11067                return Ok(Value::Str(line));
11068            },
11069            #[cfg(not(target_arch = "wasm32"))]
11070            "music_mic_pitch" | "麦克风音高" | "マイク音程" | "마이크음정" | "ระดับเสียงไมค์" =>
11071            {
11072                let hz = if let Some(mic) = self.mic.as_ref() {
11073                    let s = mic.latest_samples();
11074                    let rate = mic.sample_rate();
11075                    ling_music::pitch::detect(&s, rate).unwrap_or(0.0)
11076                } else {
11077                    0.0
11078                };
11079                return Ok(Value::Number(hz as f64));
11080            },
11081            #[cfg(not(target_arch = "wasm32"))]
11082            "music_note_name" | "音名" | "音名称" | "음이름" | "ชื่อโน้ต" =>
11083            {
11084                let hz = self.arg_num(&args, 0, 0.0)? as f32;
11085                return Ok(Value::Str(ling_music::note::hz_to_name(hz)));
11086            },
11087            #[cfg(not(target_arch = "wasm32"))]
11088            "music_hz" | "音符频率" | "音符周波数" | "음표주파수" | "ความถี่โน้ต" =>
11089            {
11090                let midi = self.pitch_arg(&args, 0, 69);
11091                return Ok(Value::Number(
11092                    ling_music::note::midi_to_hz(midi as f32) as f64
11093                ));
11094            },
11095            #[cfg(not(target_arch = "wasm32"))]
11096            "music_pitch_score" | "音准评分" | "音程スコア" | "음정점수" | "คะแนนเสียง" =>
11097            {
11098                let hz = self.arg_num(&args, 0, 0.0)? as f32;
11099                let target = self.arg_num(&args, 1, 0.0)? as f32;
11100                return Ok(Value::Number(
11101                    ling_music::karaoke::pitch_score(hz, target) as f64
11102                ));
11103            },
11104
11105            // ── MIDI (inaudible note source: drive coins, cues, etc.) ──
11106            #[cfg(not(target_arch = "wasm32"))]
11107            "music_midi_load" | "载入MIDI" | "MIDI読込" | "미디로드" | "โหลดมิดี" =>
11108            {
11109                let path = self.arg_str(&args, 0, "");
11110                let resolved = if std::path::Path::new(&path).exists() {
11111                    path.clone()
11112                } else if let Some(d) = &self.source_dir {
11113                    d.join(&path).to_string_lossy().into_owned()
11114                } else {
11115                    path.clone()
11116                };
11117                match ling_music::midi::load(&resolved) {
11118                    Ok(m) => {
11119                        let id = self.midis.len();
11120                        self.midis.push(m);
11121                        return Ok(Value::Number(id as f64));
11122                    },
11123                    Err(e) => {
11124                        eprintln!("music_midi_load failed ({path}): {e}");
11125                        return Ok(Value::Number(-1.0));
11126                    },
11127                }
11128            },
11129            #[cfg(not(target_arch = "wasm32"))]
11130            "music_midi_count" | "MIDI数量" | "MIDI数" | "미디수" | "จำนวนมิดี" =>
11131            {
11132                let id = self.arg_num(&args, 0, 0.0)? as i64;
11133                let n = self
11134                    .midis
11135                    .get(id as usize)
11136                    .map(|m| m.notes.len())
11137                    .unwrap_or(0);
11138                return Ok(Value::Number(n as f64));
11139            },
11140            // music_midi_notes(id) -> flat [time, midi, time, midi, …]
11141            #[cfg(not(target_arch = "wasm32"))]
11142            "music_midi_notes" | "MIDI音符" | "MIDIノート" | "미디음표" | "โน้ตมิดี" =>
11143            {
11144                let id = self.arg_num(&args, 0, 0.0)? as i64;
11145                let mut out = Vec::new();
11146                if let Some(m) = self.midis.get(id as usize) {
11147                    for n in &m.notes {
11148                        out.push(Value::Number(n.time as f64));
11149                        out.push(Value::Number(n.midi as f64));
11150                    }
11151                }
11152                return Ok(Value::List(Rc::new(out)));
11153            },
11154            // music_midi_bars(id) -> flat [time, midi, dur, …] (for karaoke note bars)
11155            #[cfg(not(target_arch = "wasm32"))]
11156            "music_midi_bars" | "MIDI音条" | "MIDIバー" | "미디바" | "แท่งมิดี" =>
11157            {
11158                let id = self.arg_num(&args, 0, 0.0)? as i64;
11159                let mut out = Vec::new();
11160                if let Some(m) = self.midis.get(id as usize) {
11161                    for n in &m.notes {
11162                        out.push(Value::Number(n.time as f64));
11163                        out.push(Value::Number(n.midi as f64));
11164                        out.push(Value::Number(n.dur as f64));
11165                    }
11166                }
11167                return Ok(Value::List(Rc::new(out)));
11168            },
11169
11170            // music_fft(track_id, nbands) -> spectrum at the current playback position
11171            #[cfg(not(target_arch = "wasm32"))]
11172            "music_fft" | "音乐频谱" | "音楽スペクトル" | "음악스펙트럼" | "สเปกตรัมเพลง" =>
11173            {
11174                let id = self.arg_num(&args, 0, 0.0)? as i64;
11175                let nbands = self.arg_num(&args, 1, 16.0)? as usize;
11176                let pos = self.music.as_ref().map(|m| m.position()).unwrap_or(0.0);
11177                if let Some(t) = self.tracks.get(id as usize) {
11178                    let idx = (pos * t.rate as f32) as usize;
11179                    let end = (idx + 2048).min(t.mono.len());
11180                    if end > idx + 64 {
11181                        self.fft.borrow_mut().push_samples(&t.mono[idx..end]);
11182                    }
11183                }
11184                let bands = self.fft.borrow().freq_bands(nbands);
11185                return Ok(Value::List(Rc::new(
11186                    bands.into_iter().map(|x| Value::Number(x as f64)).collect(),
11187                )));
11188            },
11189
11190            // ── stop every one-shot SFX/morph/sample voice (scene cleanup) ──
11191            #[cfg(not(target_arch = "wasm32"))]
11192            "audio_stop_sfx" | "停止音效" | "効果音停止" | "효과음정지" | "หยุดเอฟเฟกต์ทั้งหมด" =>
11193            {
11194                if let Some(a) = &self.audio {
11195                    a.stop_all_sfx();
11196                }
11197                return Ok(Value::Unit);
11198            },
11199            // ── spatial (2D/3D/4D) one-shot SFX ──
11200            #[cfg(not(target_arch = "wasm32"))]
11201            "audio_sfx" | "音效" | "空間効果音" | "공간효과음" | "เสียงเอฟเฟกต์" =>
11202            {
11203                let x = self.arg_num(&args, 0, 0.0)? as f32;
11204                let y = self.arg_num(&args, 1, 0.0)? as f32;
11205                let z = self.arg_num(&args, 2, 0.0)? as f32;
11206                let w = self.arg_num(&args, 3, 1.0)? as f32;
11207                let freq = self.arg_num(&args, 4, 440.0)? as f32;
11208                let amp = self.arg_num(&args, 5, 0.3)? as f32;
11209                let dur = self.arg_num(&args, 6, 0.15)? as f32;
11210                let wave = Wave::from_name(&self.arg_str(&args, 7, "sine"));
11211                if let Some(a) = &self.audio {
11212                    a.sfx(x, y, z, w, freq, amp, dur, wave);
11213                }
11214                return Ok(Value::Unit);
11215            },
11216            // ── YIN-YANG morph synth note: physical-model(light) ↔ FM/crush(dark) ──
11217            // โน้ตมอร์ฟ(x,y,z,w, freq, amp, dur, material, morph)
11218            //   material: 0 bowed-string · 1 plucked · 2 blown · 3 struck-metal
11219            //   morph:    0.0 light/acoustic .. 1.0 dark/digital
11220            #[cfg(not(target_arch = "wasm32"))]
11221            "morph_note" | "โน้ตมอร์ฟ" | "变形音" | "モーフ音" | "모프음" =>
11222            {
11223                let x = self.arg_num(&args, 0, 0.0)? as f32;
11224                let y = self.arg_num(&args, 1, 0.0)? as f32;
11225                let z = self.arg_num(&args, 2, 0.0)? as f32;
11226                let w = self.arg_num(&args, 3, 1.0)? as f32;
11227                let freq = self.arg_num(&args, 4, 220.0)? as f32;
11228                let amp = self.arg_num(&args, 5, 0.3)? as f32;
11229                let dur = self.arg_num(&args, 6, 0.6)? as f32;
11230                let material = self.arg_num(&args, 7, 0.0)?.clamp(0.0, 3.0) as u8;
11231                let morph = self.arg_num(&args, 8, 0.0)? as f32;
11232                if let Some(a) = &self.audio {
11233                    a.morph_note(x, y, z, w, freq, amp, dur, material, morph);
11234                }
11235                return Ok(Value::Unit);
11236            },
11237            // ── sample load / positional play / loop / stop ──
11238            #[cfg(not(target_arch = "wasm32"))]
11239            "audio_sample_load" | "载入采样" | "サンプル読込" | "샘플로드" | "โหลดตัวอย่างเสียง" =>
11240            {
11241                let path = self.arg_str(&args, 0, "");
11242                let resolved = if std::path::Path::new(&path).exists() {
11243                    path.clone()
11244                } else if let Some(d) = &self.source_dir {
11245                    d.join(&path).to_string_lossy().into_owned()
11246                } else {
11247                    path.clone()
11248                };
11249                match ling_music::load(&resolved) {
11250                    Ok(t) => {
11251                        if let Some(a) = &self.audio {
11252                            return Ok(Value::Number(a.add_sample(t.mono, t.rate) as f64));
11253                        }
11254                        return Ok(Value::Number(-1.0));
11255                    },
11256                    Err(e) => {
11257                        eprintln!("audio_sample_load failed ({path}): {e}");
11258                        return Ok(Value::Number(-1.0));
11259                    },
11260                }
11261            },
11262            #[cfg(not(target_arch = "wasm32"))]
11263            "audio_sample_play" | "播放采样" | "サンプル再生" | "샘플재생" | "เล่นตัวอย่างเสียง" =>
11264            {
11265                let id = self.arg_num(&args, 0, 0.0)? as usize;
11266                let x = self.arg_num(&args, 1, 0.0)? as f32;
11267                let y = self.arg_num(&args, 2, 0.0)? as f32;
11268                let z = self.arg_num(&args, 3, 0.0)? as f32;
11269                let w = self.arg_num(&args, 4, 1.0)? as f32;
11270                let vol = self.arg_num(&args, 5, 1.0)? as f32;
11271                let looping = self.arg_num(&args, 6, 0.0)? > 0.5;
11272                let v = self
11273                    .audio
11274                    .as_ref()
11275                    .map(|a| a.play_sample(id, x, y, z, w, vol, looping))
11276                    .unwrap_or(0);
11277                return Ok(Value::Number(v as f64));
11278            },
11279            #[cfg(not(target_arch = "wasm32"))]
11280            "audio_sample_stop" | "停止采样" | "サンプル停止" | "샘플정지" | "หยุดตัวอย่างเสียง" =>
11281            {
11282                let v = self.arg_num(&args, 0, 0.0)? as u32;
11283                if let Some(a) = &self.audio {
11284                    a.stop_sample(v);
11285                }
11286                return Ok(Value::Unit);
11287            },
11288            // ── master FX: delay / reverb / low-pass (underwater) ──
11289            #[cfg(not(target_arch = "wasm32"))]
11290            "audio_fx_delay" | "回声" | "ディレイ効果" | "딜레이" | "เสียงสะท้อน" =>
11291            {
11292                let time = self.arg_num(&args, 0, 0.3)? as f32;
11293                let fb = self.arg_num(&args, 1, 0.3)? as f32;
11294                let mix = self.arg_num(&args, 2, 0.3)? as f32;
11295                if let Some(a) = &self.audio {
11296                    a.fx_delay(time, fb, mix);
11297                }
11298                return Ok(Value::Unit);
11299            },
11300            #[cfg(not(target_arch = "wasm32"))]
11301            "audio_fx_reverb" | "混响" | "リバーブ" | "리버브" | "เสียงก้อง" =>
11302            {
11303                let mix = self.arg_num(&args, 0, 0.3)? as f32;
11304                if let Some(a) = &self.audio {
11305                    a.fx_reverb(mix);
11306                }
11307                return Ok(Value::Unit);
11308            },
11309            #[cfg(not(target_arch = "wasm32"))]
11310            "audio_fx_lowpass" | "低通滤波" | "ローパス" | "저역통과" | "กรองความถี่ต่ำ" =>
11311            {
11312                let cutoff = self.arg_num(&args, 0, 1.0)? as f32;
11313                if let Some(a) = &self.audio {
11314                    a.fx_lowpass(cutoff);
11315                }
11316                return Ok(Value::Unit);
11317            },
11318
11319            // ══════════════════════════════════════════════════════════════════
11320            // PHYSICS BUILTINS  (crates/ling-physics) — soft bodies, rigid+angular,
11321            // and a fast 2-D water/oil liquid sim mappable onto 3-D surfaces.
11322            // ══════════════════════════════════════════════════════════════════
11323
11324            // ── soft bodies (deformable bouncy balls) ──
11325            #[cfg(not(target_arch = "wasm32"))]
11326            "soft_ball" | "软球" | "ソフトボール" | "소프트볼" | "ลูกบอลนุ่ม" =>
11327            {
11328                let x = self.arg_num(&args, 0, 0.)? as f32;
11329                let y = self.arg_num(&args, 1, 0.)? as f32;
11330                let z = self.arg_num(&args, 2, 0.)? as f32;
11331                let r = self.arg_num(&args, 3, 1.0)? as f32;
11332                let b = ling_physics::soft::SoftBody::sphere(
11333                    ling_physics::Vec3::new(x, y, z),
11334                    r,
11335                    8,
11336                    12,
11337                    1.0,
11338                );
11339                let id = self.soft_bodies.len();
11340                self.soft_bodies.push(b);
11341                return Ok(Value::Number(id as f64));
11342            },
11343            #[cfg(not(target_arch = "wasm32"))]
11344            "soft_step" | "软体步进" | "ソフト更新" | "소프트스텝" | "ก้าวนุ่ม" =>
11345            {
11346                let id = self.arg_num(&args, 0, 0.)? as usize;
11347                let dt = self.arg_num(&args, 1, 0.016)? as f32;
11348                let gy = self.arg_num(&args, 2, 15.0)? as f32;
11349                if let Some(b) = self.soft_bodies.get_mut(id) {
11350                    b.integrate(dt, ling_physics::Vec3::new(0.0, gy, 0.0), 4);
11351                }
11352                return Ok(Value::Unit);
11353            },
11354            #[cfg(not(target_arch = "wasm32"))]
11355            "soft_bounce" | "软体落地" | "ソフト着地" | "소프트바운스" | "เด้งนุ่ม" =>
11356            {
11357                let id = self.arg_num(&args, 0, 0.)? as usize;
11358                let fy = self.arg_num(&args, 1, 0.)? as f32;
11359                let rest = self.arg_num(&args, 2, 0.5)? as f32;
11360                if let Some(b) = self.soft_bodies.get_mut(id) {
11361                    b.floor_collision(fy, rest);
11362                }
11363                return Ok(Value::Unit);
11364            },
11365            #[cfg(not(target_arch = "wasm32"))]
11366            "soft_contain" | "软体边界" | "ソフト箱" | "소프트경계" | "กล่องนุ่ม" =>
11367            {
11368                let id = self.arg_num(&args, 0, 0.)? as usize;
11369                let nx = self.arg_num(&args, 1, -5.)? as f32;
11370                let ny = self.arg_num(&args, 2, -5.)? as f32;
11371                let nz = self.arg_num(&args, 3, -5.)? as f32;
11372                let mx = self.arg_num(&args, 4, 5.)? as f32;
11373                let my = self.arg_num(&args, 5, 5.)? as f32;
11374                let mz = self.arg_num(&args, 6, 5.)? as f32;
11375                let rest = self.arg_num(&args, 7, 0.6)? as f32;
11376                if let Some(b) = self.soft_bodies.get_mut(id) {
11377                    b.contain(
11378                        ling_physics::Vec3::new(nx, ny, nz),
11379                        ling_physics::Vec3::new(mx, my, mz),
11380                        rest,
11381                    );
11382                }
11383                return Ok(Value::Unit);
11384            },
11385            #[cfg(not(target_arch = "wasm32"))]
11386            "soft_kick" | "软体踢" | "ソフト衝撃" | "소프트킥" | "เตะนุ่ม" =>
11387            {
11388                let id = self.arg_num(&args, 0, 0.)? as usize;
11389                let dx = self.arg_num(&args, 1, 0.)? as f32;
11390                let dy = self.arg_num(&args, 2, 0.)? as f32;
11391                let dz = self.arg_num(&args, 3, 0.)? as f32;
11392                let s = self.arg_num(&args, 4, 0.1)? as f32;
11393                if let Some(b) = self.soft_bodies.get_mut(id) {
11394                    b.kick(ling_physics::Vec3::new(dx, dy, dz), s);
11395                }
11396                return Ok(Value::Unit);
11397            },
11398            // soft_spin(id, ax, ay, az, rate) — add angular velocity about the axis
11399            // through the centroid (rate = rad/step; ≈ surface_speed / radius to roll)
11400            #[cfg(not(target_arch = "wasm32"))]
11401            "soft_spin" | "软体自旋" | "ソフト回転" | "소프트회전" | "หมุนนุ่ม" =>
11402            {
11403                let id = self.arg_num(&args, 0, 0.)? as usize;
11404                let ax = self.arg_num(&args, 1, 0.)? as f32;
11405                let ay = self.arg_num(&args, 2, 0.)? as f32;
11406                let az = self.arg_num(&args, 3, 0.)? as f32;
11407                let rate = self.arg_num(&args, 4, 0.1)? as f32;
11408                if let Some(b) = self.soft_bodies.get_mut(id) {
11409                    b.spin(ling_physics::Vec3::new(ax, ay, az), rate);
11410                }
11411                return Ok(Value::Unit);
11412            },
11413            #[cfg(not(target_arch = "wasm32"))]
11414            "soft_deform" | "形变量" | "変形量" | "변형량" | "ความบิดเบี้ยว" =>
11415            {
11416                let id = self.arg_num(&args, 0, 0.)? as usize;
11417                let d = self
11418                    .soft_bodies
11419                    .get(id)
11420                    .map(|b| b.deformation())
11421                    .unwrap_or(0.0);
11422                return Ok(Value::Number(d as f64));
11423            },
11424            // soft_angular_speed(id) -> magnitude of the body's angular velocity
11425            // (how fast it is tumbling/rolling), derived from its node velocities.
11426            #[cfg(not(target_arch = "wasm32"))]
11427            "soft_angular_speed"
11428            | "软体角速"
11429            | "ソフト角速度"
11430            | "소프트각속도"
11431            | "ความเร็วเชิงมุมนุ่ม" => {
11432                let id = self.arg_num(&args, 0, 0.)? as usize;
11433                let w = self
11434                    .soft_bodies
11435                    .get(id)
11436                    .map(|b| b.angular_speed())
11437                    .unwrap_or(0.0);
11438                return Ok(Value::Number(w as f64));
11439            },
11440            #[cfg(not(target_arch = "wasm32"))]
11441            "soft_centroid" | "软体质心" | "ソフト重心" | "소프트중심" | "จุดศูนย์กลางนุ่ม" =>
11442            {
11443                let id = self.arg_num(&args, 0, 0.)? as usize;
11444                let c = self
11445                    .soft_bodies
11446                    .get(id)
11447                    .map(|b| b.centroid())
11448                    .unwrap_or(ling_physics::Vec3::ZERO);
11449                return Ok(Value::List(Rc::new(vec![
11450                    Value::Number(c.x as f64),
11451                    Value::Number(c.y as f64),
11452                    Value::Number(c.z as f64),
11453                ])));
11454            },
11455            // soft_nodes(id) -> flat [x,y,z, x,y,z, …] for rendering the deformed mesh
11456            #[cfg(not(target_arch = "wasm32"))]
11457            "soft_nodes" | "软体节点" | "ソフト節点" | "소프트노드" | "จุดนุ่ม" =>
11458            {
11459                let id = self.arg_num(&args, 0, 0.)? as usize;
11460                let mut out = Vec::new();
11461                if let Some(b) = self.soft_bodies.get(id) {
11462                    for n in &b.nodes {
11463                        out.push(Value::Number(n.pos.x as f64));
11464                        out.push(Value::Number(n.pos.y as f64));
11465                        out.push(Value::Number(n.pos.z as f64));
11466                    }
11467                }
11468                return Ok(Value::List(Rc::new(out)));
11469            },
11470
11471            // ── rigid bodies with angular dynamics ──
11472            #[cfg(not(target_arch = "wasm32"))]
11473            "rb_add" | "刚体添加" | "剛体追加" | "강체추가" | "เพิ่มวัตถุแข็ง" =>
11474            {
11475                let x = self.arg_num(&args, 0, 0.)? as f32;
11476                let y = self.arg_num(&args, 1, 0.)? as f32;
11477                let z = self.arg_num(&args, 2, 0.)? as f32;
11478                let mass = self.arg_num(&args, 3, 1.0)? as f32;
11479                let mut b =
11480                    ling_physics::rigid::RigidBody::new(ling_physics::Vec3::new(x, y, z), mass);
11481                b.restitution = 0.6;
11482                return Ok(Value::Number(self.rigid_world.add(b) as f64));
11483            },
11484            #[cfg(not(target_arch = "wasm32"))]
11485            "rb_torque" | "扭矩" | "トルク" | "토크" | "แรงบิด" => {
11486                let i = self.arg_num(&args, 0, 0.)? as usize;
11487                let tx = self.arg_num(&args, 1, 0.)? as f32;
11488                let ty = self.arg_num(&args, 2, 0.)? as f32;
11489                let tz = self.arg_num(&args, 3, 0.)? as f32;
11490                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
11491                    b.apply_torque(ling_physics::Vec3::new(tx, ty, tz));
11492                }
11493                return Ok(Value::Unit);
11494            },
11495            #[cfg(not(target_arch = "wasm32"))]
11496            "rb_spin" | "自旋" | "スピン" | "스핀" | "หมุน" => {
11497                let i = self.arg_num(&args, 0, 0.)? as usize;
11498                let wx = self.arg_num(&args, 1, 0.)? as f32;
11499                let wy = self.arg_num(&args, 2, 0.)? as f32;
11500                let wz = self.arg_num(&args, 3, 0.)? as f32;
11501                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
11502                    b.apply_spin(ling_physics::Vec3::new(wx, wy, wz));
11503                }
11504                return Ok(Value::Unit);
11505            },
11506            #[cfg(not(target_arch = "wasm32"))]
11507            "rb_impulse" | "刚体冲量" | "剛体インパルス" | "강체충격" | "แรงดลแข็ง" =>
11508            {
11509                let i = self.arg_num(&args, 0, 0.)? as usize;
11510                let ix = self.arg_num(&args, 1, 0.)? as f32;
11511                let iy = self.arg_num(&args, 2, 0.)? as f32;
11512                let iz = self.arg_num(&args, 3, 0.)? as f32;
11513                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
11514                    b.apply_impulse(ling_physics::Vec3::new(ix, iy, iz));
11515                }
11516                return Ok(Value::Unit);
11517            },
11518            #[cfg(not(target_arch = "wasm32"))]
11519            "rb_floor" | "刚体落地" | "剛体着地" | "강체바닥" | "พื้นแข็ง" =>
11520            {
11521                let i = self.arg_num(&args, 0, 0.)? as usize;
11522                let fy = self.arg_num(&args, 1, 0.)? as f32;
11523                let rest = self.arg_num(&args, 2, 0.6)? as f32;
11524                let fric = self.arg_num(&args, 3, 0.6)? as f32;
11525                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
11526                    b.bounce_floor(fy, rest, fric);
11527                }
11528                return Ok(Value::Unit);
11529            },
11530            #[cfg(not(target_arch = "wasm32"))]
11531            "rb_gravity" | "刚体重力" | "剛体重力" | "강체중력" | "แรงโน้มถ่วงแข็ง" =>
11532            {
11533                let gx = self.arg_num(&args, 0, 0.)? as f32;
11534                let gy = self.arg_num(&args, 1, 9.81)? as f32;
11535                let gz = self.arg_num(&args, 2, 0.)? as f32;
11536                self.rigid_world.gravity = ling_physics::Vec3::new(gx, gy, gz);
11537                return Ok(Value::Unit);
11538            },
11539            #[cfg(not(target_arch = "wasm32"))]
11540            "rb_step" | "刚体步进" | "剛体更新" | "강체스텝" | "ก้าวแข็ง" =>
11541            {
11542                let dt = self.arg_num(&args, 0, 0.016)? as f32;
11543                self.rigid_world.step(dt);
11544                return Ok(Value::Unit);
11545            },
11546            #[cfg(not(target_arch = "wasm32"))]
11547            "rb_pos" | "刚体位置" | "剛体位置" | "강체위치" | "ตำแหน่งแข็ง" =>
11548            {
11549                let i = self.arg_num(&args, 0, 0.)? as usize;
11550                let p = self
11551                    .rigid_world
11552                    .bodies
11553                    .get(i)
11554                    .map(|b| b.pos)
11555                    .unwrap_or(ling_physics::Vec3::ZERO);
11556                return Ok(Value::List(Rc::new(vec![
11557                    Value::Number(p.x as f64),
11558                    Value::Number(p.y as f64),
11559                    Value::Number(p.z as f64),
11560                ])));
11561            },
11562            #[cfg(not(target_arch = "wasm32"))]
11563            "rb_rot" | "刚体旋转" | "剛体回転" | "강체회전" | "การหมุนแข็ง" =>
11564            {
11565                let i = self.arg_num(&args, 0, 0.)? as usize;
11566                let q = self
11567                    .rigid_world
11568                    .bodies
11569                    .get(i)
11570                    .map(|b| b.orientation)
11571                    .unwrap_or(ling_physics::Quat::IDENTITY);
11572                return Ok(Value::List(Rc::new(vec![
11573                    Value::Number(q.x as f64),
11574                    Value::Number(q.y as f64),
11575                    Value::Number(q.z as f64),
11576                    Value::Number(q.w as f64),
11577                ])));
11578            },
11579
11580            // ── native-res mesh (.lmesh): load once, draw fast (unlit, per-tri colour) ──
11581            #[cfg(not(target_arch = "wasm32"))]
11582            "mesh_load" | "โหลดเมช" | "载入网格" | "メッシュ読込" | "메시로드" =>
11583            {
11584                let path = self.arg_str(&args, 0, "");
11585                let resolved = if std::path::Path::new(&path).exists() {
11586                    path.clone()
11587                } else if let Some(d) = &self.source_dir {
11588                    d.join(&path).to_string_lossy().into_owned()
11589                } else {
11590                    path.clone()
11591                };
11592                let bytes = match std::fs::read(&resolved) {
11593                    Ok(b) => b,
11594                    Err(e) => {
11595                        eprintln!("mesh_load failed ({path}): {e}");
11596                        return Ok(Value::Number(-1.0));
11597                    },
11598                };
11599                if bytes.len() < 16 || &bytes[0..4] != b"LMSH" {
11600                    eprintln!("mesh_load: bad header ({path})");
11601                    return Ok(Value::Number(-1.0));
11602                }
11603                let rd4 =
11604                    |o: usize| -> [u8; 4] { [bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]] };
11605                let height = f32::from_le_bytes(rd4(8));
11606                let ntri = u32::from_le_bytes(rd4(12)) as usize;
11607                let need = 16usize.saturating_add(ntri.saturating_mul(9 * 4 + 3));
11608                if bytes.len() < need {
11609                    eprintln!("mesh_load: truncated ({path})");
11610                    return Ok(Value::Number(-1.0));
11611                }
11612                let mut pos = Vec::with_capacity(ntri * 3);
11613                let mut col = Vec::with_capacity(ntri);
11614                let mut off = 16usize;
11615                for _ in 0..ntri {
11616                    for _k in 0..3 {
11617                        let x = f32::from_le_bytes(rd4(off));
11618                        let y = f32::from_le_bytes(rd4(off + 4));
11619                        let z = f32::from_le_bytes(rd4(off + 8));
11620                        off += 12;
11621                        pos.push([x, y, z]);
11622                    }
11623                    col.push([bytes[off], bytes[off + 1], bytes[off + 2]]);
11624                    off += 3;
11625                }
11626                eprintln!("mesh_load: {} ({} tris, h={:.2})", path, ntri, height);
11627                let id = self.meshes.len();
11628                self.meshes
11629                    .push(crate::gfx::shapes::ColorMesh { pos, col, height });
11630                return Ok(Value::Number(id as f64));
11631            },
11632            #[cfg(target_arch = "wasm32")]
11633            "mesh_load" | "โหลดเมช" | "载入网格" | "メッシュ読込" | "메시로드" =>
11634            {
11635                // Native .lmesh loading is file-system based and not wired for wasm yet.
11636                // Return an invalid handle so scripts can choose a fallback path.
11637                return Ok(Value::Number(-1.0));
11638            },
11639            #[cfg(not(target_arch = "wasm32"))]
11640            "mesh_draw" | "วาดเมชสี" | "绘制网格" | "メッシュ描画" | "메시그리기" =>
11641            {
11642                // ('วาดเมช' is taken by draw_mesh — use a distinct Thai alias)
11643                let id = self.arg_num(&args, 0, 0.)? as usize;
11644                let cx = self.arg_num(&args, 1, 0.)? as f32;
11645                let cy = self.arg_num(&args, 2, 0.)? as f32;
11646                let cz = self.arg_num(&args, 3, 0.)? as f32;
11647                let sc = self.arg_num(&args, 4, 1.)? as f32;
11648                let yaw = self.arg_num(&args, 5, 0.)? as f32;
11649                let sway = self.arg_num(&args, 6, 0.)? as f32;
11650                let arm = self.arg_num(&args, 7, 0.)? as f32;
11651                let lean = self.arg_num(&args, 8, 0.)? as f32;
11652                let leg = self.arg_num(&args, 9, 0.)? as f32;
11653                let tuck = self.arg_num(&args, 10, 0.)? as f32;
11654                if id < self.meshes.len() {
11655                    let m = &self.meshes[id];
11656                    let mut gfx = self.gfx.borrow_mut();
11657                    gfx.draw_color_mesh(m, cx, cy, cz, sc, yaw, sway, arm, lean, leg, tuck);
11658                }
11659                return Ok(Value::Unit);
11660            },
11661            #[cfg(target_arch = "wasm32")]
11662            "mesh_draw" | "วาดเมชสี" | "绘制网格" | "メッシュ描画" | "메시그리기" =>
11663            {
11664                return Ok(Value::Unit);
11665            },
11666
11667            // ── liquid sim (water + oil, immiscible) ──
11668            "liquid_new" | "新建液体" | "液体新規" | "액체생성" | "สร้างของเหลว" =>
11669            {
11670                let w = self.arg_num(&args, 0, 64.)? as usize;
11671                let h = self.arg_num(&args, 1, 64.)? as usize;
11672                let id = self.liquids.len();
11673                self.liquids
11674                    .push(ling_physics::liquid::LiquidGrid::new(w, h));
11675                return Ok(Value::Number(id as f64));
11676            },
11677            "liquid_set_colors" | "液体颜色" | "液体配色" | "액체색상" | "สีของเหลว" =>
11678            {
11679                let id = self.arg_num(&args, 0, 0.)? as usize;
11680                let wr = self.arg_num(&args, 1, 40.)? as f32;
11681                let wg = self.arg_num(&args, 2, 110.)? as f32;
11682                let wb = self.arg_num(&args, 3, 235.)? as f32;
11683                let or_ = self.arg_num(&args, 4, 240.)? as f32;
11684                let og = self.arg_num(&args, 5, 175.)? as f32;
11685                let ob = self.arg_num(&args, 6, 45.)? as f32;
11686                if let Some(g) = self.liquids.get_mut(id) {
11687                    g.set_colors(wr, wg, wb, or_, og, ob);
11688                }
11689                return Ok(Value::Unit);
11690            },
11691            "liquid_splat" | "液体注入" | "液体追加" | "액체분사" | "หยดของเหลว" =>
11692            {
11693                let id = self.arg_num(&args, 0, 0.)? as usize;
11694                let x = self.arg_num(&args, 1, 0.)? as f32;
11695                let y = self.arg_num(&args, 2, 0.)? as f32;
11696                let kind = self.arg_num(&args, 3, 0.)? as i32;
11697                let amt = self.arg_num(&args, 4, 1.0)? as f32;
11698                let rad = self.arg_num(&args, 5, 4.0)? as f32;
11699                if let Some(g) = self.liquids.get_mut(id) {
11700                    g.splat(x, y, kind, amt, rad);
11701                }
11702                return Ok(Value::Unit);
11703            },
11704            "liquid_gravity" | "液体重力" | "液体重力ベクトル" | "액체중력" | "แรงโน้มถ่วงเหลว" =>
11705            {
11706                let id = self.arg_num(&args, 0, 0.)? as usize;
11707                let gx = self.arg_num(&args, 1, 0.)? as f32;
11708                let gy = self.arg_num(&args, 2, 60.)? as f32;
11709                if let Some(g) = self.liquids.get_mut(id) {
11710                    g.set_gravity(gx, gy);
11711                }
11712                return Ok(Value::Unit);
11713            },
11714            "liquid_step" | "液体步进" | "液体更新" | "액체스텝" | "ก้าวของเหลว" =>
11715            {
11716                let id = self.arg_num(&args, 0, 0.)? as usize;
11717                let dt = self.arg_num(&args, 1, 0.016)? as f32;
11718                if let Some(g) = self.liquids.get_mut(id) {
11719                    g.step(dt);
11720                }
11721                return Ok(Value::Unit);
11722            },
11723            // liquid_step_all(dt) — advance EVERY liquid grid one tick, in parallel
11724            // across instances (rayon). Independent grids share no state, so this is
11725            // an embarrassingly-parallel batch: a scene with many liquid surfaces
11726            // steps in one call that scales across cores instead of N serial
11727            // `liquid_step` calls.
11728            "liquid_step_all"
11729            | "液体全步进"
11730            | "液体全更新"
11731            | "전체액체스텝"
11732            | "ก้าวของเหลวทั้งหมด" => {
11733                let dt = self.arg_num(&args, 0, 0.016)? as f32;
11734                ling_physics::liquid::step_all(&mut self.liquids, dt);
11735                return Ok(Value::Unit);
11736            },
11737            // liquid_rainbow(id, on) — colour the fluid as a flowing ROYGBIV marble
11738            "liquid_rainbow" | "液体彩虹" | "液体虹" | "액체무지개" | "ของเหลวสายรุ้ง" =>
11739            {
11740                let id = self.arg_num(&args, 0, 0.)? as usize;
11741                let on = self.arg_num(&args, 1, 1.0)? > 0.5;
11742                if let Some(g) = self.liquids.get_mut(id) {
11743                    g.rainbow = on;
11744                }
11745                return Ok(Value::Unit);
11746            },
11747            // liquid_mix(id) -> 0 (oil/water separated) .. 1 (fully intermixed)
11748            "liquid_mix" | "液体混合" | "液体混合度" | "액체혼합" | "การผสมของเหลว" =>
11749            {
11750                let id = self.arg_num(&args, 0, 0.)? as usize;
11751                let m = self.liquids.get(id).map(|g| g.mix_amount()).unwrap_or(0.0);
11752                return Ok(Value::Number(m as f64));
11753            },
11754            // liquid_draw(id, sx, sy, scale) — fast flat 2-D blit of the colour field
11755            #[cfg(not(target_arch = "wasm32"))]
11756            "liquid_draw" | "绘制液体" | "液体描画" | "액체그리기" | "วาดของเหลว" =>
11757            {
11758                let id = self.arg_num(&args, 0, 0.)? as usize;
11759                let sx = self.arg_num(&args, 1, 0.)? as i32;
11760                let sy = self.arg_num(&args, 2, 0.)? as i32;
11761                let scale = (self.arg_num(&args, 3, 4.)? as i32).max(1);
11762                if id < self.liquids.len() {
11763                    let (gw, gh) = {
11764                        let g = &self.liquids[id];
11765                        (g.w, g.h)
11766                    };
11767                    let mut gfx = self.gfx.borrow_mut();
11768                    let (w, h) = (gfx.width as i32, gfx.height as i32);
11769                    let g = &self.liquids[id];
11770                    for cy in 0..gh {
11771                        for cx in 0..gw {
11772                            let col = g.sample_rgb(cx, cy);
11773                            let bx = sx + cx as i32 * scale;
11774                            let by = sy + cy as i32 * scale;
11775                            for dy in 0..scale {
11776                                for dx in 0..scale {
11777                                    let px = bx + dx;
11778                                    let py = by + dy;
11779                                    if px >= 0 && py >= 0 && px < w && py < h {
11780                                        gfx.buffer[(py * w + px) as usize] = col;
11781                                    }
11782                                }
11783                            }
11784                        }
11785                    }
11786                }
11787                return Ok(Value::Unit);
11788            },
11789            // liquid_draw_surface(id, kind, cx,cy,cz, radius, height)
11790            //   kind: 0 plane · 1 sphere · 2 cylinder · 3 cone · 4 dome
11791            "liquid_draw_surface" | "液体贴面" | "液体曲面" | "액체곡면" | "ของเหลวบนพื้นผิว" =>
11792            {
11793                #[cfg(not(target_arch = "wasm32"))]
11794                {
11795                    let id = self.arg_num(&args, 0, 0.)? as usize;
11796                    let kind = self.arg_num(&args, 1, 1.)? as i32;
11797                    let cx = self.arg_num(&args, 2, 0.)? as f32;
11798                    let cy = self.arg_num(&args, 3, 0.)? as f32;
11799                    let cz = self.arg_num(&args, 4, 0.)? as f32;
11800                    let radius = self.arg_num(&args, 5, 2.0)? as f32;
11801                    let height = self.arg_num(&args, 6, 3.0)? as f32;
11802                    if id < self.liquids.len() {
11803                        let (gw, gh) = {
11804                            let g = &self.liquids[id];
11805                            (g.w, g.h)
11806                        };
11807                        let mut gfx = self.gfx.borrow_mut();
11808                        let (w, h, add) = (gfx.width, gfx.height, gfx.blend == 1);
11809                        let cam = gfx.camera.clone();
11810                        let near = -cam.zdist + 0.05;
11811                        let g = &self.liquids[id];
11812                        let tau = std::f32::consts::TAU;
11813                        let pi = std::f32::consts::PI;
11814                        // surface point for a (u,v) in [0,1] on the chosen primitive
11815                        let sp = |u: f32, v: f32| -> [f32; 3] {
11816                            if kind == 0 {
11817                                [
11818                                    cx + (u - 0.5) * 2.0 * radius,
11819                                    cy,
11820                                    cz + (v - 0.5) * 2.0 * radius,
11821                                ]
11822                            } else if kind == 2 {
11823                                let th = u * tau;
11824                                [
11825                                    cx + th.cos() * radius,
11826                                    cy + (v - 0.5) * height,
11827                                    cz + th.sin() * radius,
11828                                ]
11829                            } else if kind == 3 {
11830                                let th = u * tau;
11831                                let rr = radius * (1.0 - v);
11832                                [
11833                                    cx + th.cos() * rr,
11834                                    cy + (v - 0.5) * height,
11835                                    cz + th.sin() * rr,
11836                                ]
11837                            } else if kind == 4 {
11838                                let th = u * tau;
11839                                let ph = v * pi * 0.5;
11840                                [
11841                                    cx + ph.sin() * th.cos() * radius,
11842                                    cy - ph.cos() * radius,
11843                                    cz + ph.sin() * th.sin() * radius,
11844                                ]
11845                            } else {
11846                                let th = u * tau;
11847                                let ph = v * pi;
11848                                [
11849                                    cx + ph.sin() * th.cos() * radius,
11850                                    cy + ph.cos() * radius,
11851                                    cz + ph.sin() * th.sin() * radius,
11852                                ]
11853                            }
11854                        };
11855                        let nrm = |u: f32, v: f32| -> [f32; 3] {
11856                            if kind == 0 {
11857                                [0.0, -1.0, 0.0]
11858                            } else if kind == 2 {
11859                                let th = u * tau;
11860                                [th.cos(), 0.0, th.sin()]
11861                            } else if kind == 3 {
11862                                let th = u * tau;
11863                                let s = (radius / height.max(0.01)).atan();
11864                                [th.cos() * s.cos(), s.sin(), th.sin() * s.cos()]
11865                            } else if kind == 4 {
11866                                let th = u * tau;
11867                                let ph = v * pi * 0.5;
11868                                [ph.sin() * th.cos(), -ph.cos(), ph.sin() * th.sin()]
11869                            } else {
11870                                let th = u * tau;
11871                                let ph = v * pi;
11872                                [ph.sin() * th.cos(), ph.cos(), ph.sin() * th.sin()]
11873                            }
11874                        };
11875                        let gwf = gw as f32;
11876                        let ghf = gh as f32;
11877                        let mut cyc = 0usize;
11878                        while cyc < gh {
11879                            let mut cxc = 0usize;
11880                            while cxc < gw {
11881                                // cull by the cell centre's outward normal
11882                                let uc = (cxc as f32 + 0.5) / gwf;
11883                                let vc = (cyc as f32 + 0.5) / ghf;
11884                                let c = sp(uc, vc);
11885                                let n = nrm(uc, vc);
11886                                let dc = cam.depth(c[0], c[1], c[2]);
11887                                if dc > near {
11888                                    let cull = kind != 0
11889                                        && cam.depth(
11890                                            c[0] + n[0] * 0.06,
11891                                            c[1] + n[1] * 0.06,
11892                                            c[2] + n[2] * 0.06,
11893                                        ) > dc;
11894                                    if !cull {
11895                                        // project the 4 cell corners → a filled AA vector quad
11896                                        let u0 = cxc as f32 / gwf;
11897                                        let u1 = (cxc + 1) as f32 / gwf;
11898                                        let v0 = cyc as f32 / ghf;
11899                                        let v1 = (cyc + 1) as f32 / ghf;
11900                                        let q = [sp(u0, v0), sp(u1, v0), sp(u1, v1), sp(u0, v1)];
11901                                        let mut poly: Vec<[f32; 2]> = Vec::with_capacity(5);
11902                                        let mut ok = true;
11903                                        for p in &q {
11904                                            if cam.depth(p[0], p[1], p[2]) <= near {
11905                                                ok = false;
11906                                                break;
11907                                            }
11908                                            let (sx, sy, _) = cam.project(p[0], p[1], p[2]);
11909                                            poly.push([sx, sy]);
11910                                        }
11911                                        if ok {
11912                                            let p0 = poly[0];
11913                                            poly.push(p0);
11914                                            let col = g.sample_rgb(cxc, cyc);
11915                                            crate::gfx::raster::fill_contours_aa(
11916                                                &mut gfx.buffer,
11917                                                w,
11918                                                h,
11919                                                col,
11920                                                add,
11921                                                std::slice::from_ref(&poly),
11922                                            );
11923                                        }
11924                                    }
11925                                }
11926                                cxc += 1;
11927                            }
11928                            cyc += 1;
11929                        }
11930                    }
11931                }
11932                #[cfg(target_arch = "wasm32")]
11933                {
11934                    // WASM: liquid_draw_surface is a no-op for now (would need WebGL shader)
11935                    // The liquid simulation still runs, just not rendered to 3D surfaces
11936                }
11937                return Ok(Value::Unit);
11938            },
11939            // sparkle(x, y, w, h, count [, t]) — scatter twinkling vector star-sparkles
11940            // in a rect (snowglobe effect) in the current colour + blend mode.
11941            #[cfg(not(target_arch = "wasm32"))]
11942            "sparkle" | "闪光" | "きらめき" | "반짝임" | "ประกาย" => {
11943                let x = self.arg_num(&args, 0, 0.)? as f32;
11944                let y = self.arg_num(&args, 1, 0.)? as f32;
11945                let ww = self.arg_num(&args, 2, 200.)? as f32;
11946                let hh = self.arg_num(&args, 3, 200.)? as f32;
11947                let count = self.arg_num(&args, 4, 40.)? as i32;
11948                let t = self.arg_num(&args, 5, 0.)? as f32;
11949                let mut gfx = self.gfx.borrow_mut();
11950                let (w, h, add, color) = (gfx.width, gfx.height, gfx.blend == 1, gfx.color);
11951                let (cr, cg, cb) = (
11952                    (color >> 16 & 0xFF) as f32,
11953                    (color >> 8 & 0xFF) as f32,
11954                    (color & 0xFF) as f32,
11955                );
11956                let mut n = 0i32;
11957                while n < count {
11958                    let hsh = (n as u32).wrapping_mul(2654435761).wrapping_add(0x9E3779B9);
11959                    let u = ((hsh >> 8) & 1023) as f32 / 1023.0;
11960                    let v = ((hsh >> 18) & 1023) as f32 / 1023.0;
11961                    let phase = (hsh & 255) as f32 / 255.0;
11962                    let tw = (t * 3.0 + phase * std::f32::consts::TAU + n as f32).sin() * 0.5 + 0.5;
11963                    let sz = 1.5 + tw * 5.0;
11964                    let px = x + u * ww;
11965                    let py = y + v * hh;
11966                    let b = tw * tw; // sharp twinkle
11967                    let col =
11968                        (((cr * b) as u32) << 16) | (((cg * b) as u32) << 8) | ((cb * b) as u32);
11969                    crate::gfx::raster::draw_line_aa(
11970                        &mut gfx.buffer,
11971                        w,
11972                        h,
11973                        col,
11974                        add,
11975                        px - sz,
11976                        py,
11977                        px + sz,
11978                        py,
11979                    );
11980                    crate::gfx::raster::draw_line_aa(
11981                        &mut gfx.buffer,
11982                        w,
11983                        h,
11984                        col,
11985                        add,
11986                        px,
11987                        py - sz,
11988                        px,
11989                        py + sz,
11990                    );
11991                    let d = sz * 0.55;
11992                    crate::gfx::raster::draw_line_aa(
11993                        &mut gfx.buffer,
11994                        w,
11995                        h,
11996                        col,
11997                        add,
11998                        px - d,
11999                        py - d,
12000                        px + d,
12001                        py + d,
12002                    );
12003                    crate::gfx::raster::draw_line_aa(
12004                        &mut gfx.buffer,
12005                        w,
12006                        h,
12007                        col,
12008                        add,
12009                        px - d,
12010                        py + d,
12011                        px + d,
12012                        py - d,
12013                    );
12014                    n += 1;
12015                }
12016                return Ok(Value::Unit);
12017            },
12018
12019            // ══════════════════════════════════════════════════════════════════
12020            // DIALOG BUILTINS  (crates/ling-game/src/dialog.rs) — cinematic,
12021            // typed-out, colour-coded text boxes. Markup: {n}name{/} {p}place{/}
12022            // {i}item{/}, \n newline, || page break.
12023            // ══════════════════════════════════════════════════════════════════
12024            #[cfg(not(target_arch = "wasm32"))]
12025            "dialog_show" | "对话显示" | "会話表示" | "대화표시" | "แสดงบทสนทนา" =>
12026            {
12027                let text = self.arg_str(&args, 0, "");
12028                let cps = self.arg_num(&args, 1, 32.0)? as f32;
12029                self.dialog = Some(ling_game::dialog::Dialog::new(&text, cps));
12030                return Ok(Value::Unit);
12031            },
12032            #[cfg(not(target_arch = "wasm32"))]
12033            "dialog_step" | "对话步进" | "会話更新" | "대화스텝" | "ก้าวบทสนทนา" =>
12034            {
12035                let dt = self.arg_num(&args, 0, 0.016)? as f32;
12036                if let Some(d) = self.dialog.as_mut() {
12037                    d.update(dt);
12038                }
12039                return Ok(Value::Unit);
12040            },
12041            #[cfg(not(target_arch = "wasm32"))]
12042            "dialog_advance" | "对话推进" | "会話送り" | "대화진행" | "เลื่อนบทสนทนา" =>
12043            {
12044                if let Some(d) = self.dialog.as_mut() {
12045                    d.advance();
12046                }
12047                return Ok(Value::Unit);
12048            },
12049            #[cfg(not(target_arch = "wasm32"))]
12050            "dialog_active" | "对话激活" | "会話中" | "대화중" | "บทสนทนาทำงาน" =>
12051            {
12052                let a = self
12053                    .dialog
12054                    .as_ref()
12055                    .map(|d| !d.is_closed())
12056                    .unwrap_or(false);
12057                return Ok(Value::Bool(a));
12058            },
12059            #[cfg(not(target_arch = "wasm32"))]
12060            "dialog_typing" | "对话打字" | "会話タイプ中" | "대화타이핑" | "กำลังพิมพ์บทสนทนา" =>
12061            {
12062                use ling_game::dialog::Dialog;
12063
12064                let a = self
12065                    .dialog
12066                    .as_ref()
12067                    .map(|d: &Dialog| !d.is_closed() && d.is_typing())
12068                    .unwrap_or(false);
12069                return Ok(Value::Bool(a));
12070            },
12071            #[cfg(not(target_arch = "wasm32"))]
12072            "dialog_close" | "对话关闭" | "会話閉じる" | "대화닫기" | "ปิดบทสนทนา" =>
12073            {
12074                self.dialog = None;
12075                return Ok(Value::Unit);
12076            },
12077            // dialog_color(role, r, g, b) — role: 0 text · 1 name · 2 place · 3 item
12078            #[cfg(not(target_arch = "wasm32"))]
12079            "dialog_color" | "对话颜色" | "会話色" | "대화색" | "สีบทสนทนา" =>
12080            {
12081                let role = (self.arg_num(&args, 0, 0.0)? as usize).min(3);
12082                let r = self.arg_num(&args, 1, 255.0)? as u32 & 0xFF;
12083                let g = self.arg_num(&args, 2, 255.0)? as u32 & 0xFF;
12084                let b = self.arg_num(&args, 3, 255.0)? as u32 & 0xFF;
12085                self.dialog_colors[role] = (r << 16) | (g << 8) | b;
12086                return Ok(Value::Unit);
12087            },
12088            // dialog_draw(x, y, w, h [, font_handle]) — draw the box + typed text
12089            #[cfg(not(target_arch = "wasm32"))]
12090            "dialog_draw" | "对话绘制" | "会話描画" | "대화그리기" | "วาดบทสนทนา" =>
12091            {
12092                let x = self.arg_num(&args, 0, 40.0)? as f32;
12093                let y = self.arg_num(&args, 1, 0.0)? as f32;
12094                let ww = self.arg_num(&args, 2, 720.0)? as f32;
12095                let hh = self.arg_num(&args, 3, 150.0)? as f32;
12096                let font = self.arg_num(&args, 4, -1.0)? as i64;
12097                let t = (crate::runtime::now_secs() - self.start_time_secs) as f32;
12098                self.render_dialog(x, y, ww, hh, font, t);
12099                return Ok(Value::Unit);
12100            },
12101
12102            // text_poll() — fold newly-typed keys into the input buffer, return it.
12103            // Repeat is enabled (KeyRepeat::Yes) so holding a key/Backspace behaves
12104            // like a normal text field; length is capped so a stuck key or a runaway
12105            // script can't grow the buffer without bound.
12106            #[cfg(not(target_arch = "wasm32"))]
12107            "text_poll" => {
12108                const TEXT_BUFFER_MAX: usize = 240;
12109                // See key_down/key_pressed: our topmost fullscreen window can
12110                // be visually in front without real Win32 keyboard focus, so
12111                // WM_KEYDOWN/WM_CHAR (what minifb's get_keys_pressed reads)
12112                // never arrive. Poll the OS key-state table directly instead
12113                // — no focus required — with our own repeat-aware edge
12114                // detection (key_repeat_fire) so holding a key behaves like
12115                // the KeyRepeat::Yes path below: one char on press, then
12116                // repeats after a short hold delay.
12117                #[cfg(windows)]
12118                {
12119                    let topmost = self.gfx.borrow().topmost_window;
12120                    if topmost {
12121                        if !window_is_foreground(self.gfx.borrow().hwnd) {
12122                            return Ok(Value::Str(self.text_buffer.clone()));
12123                        }
12124                        let shift = os_key_down(VK_SHIFT);
12125                        let now = crate::runtime::now_secs();
12126                        let mut gfx = self.gfx.borrow_mut();
12127                        let back_idx = VK_BACK as usize;
12128                        let back_down = os_key_down(VK_BACK);
12129                        let back_was = gfx.raw_keys_prev[back_idx];
12130                        let (mut back_since, mut back_fire) = (
12131                            gfx.raw_keys_down_since[back_idx],
12132                            gfx.raw_keys_last_fire[back_idx],
12133                        );
12134                        if key_repeat_fire(now, back_down, back_was, &mut back_since, &mut back_fire) {
12135                            self.text_buffer.pop();
12136                        }
12137                        gfx.raw_keys_down_since[back_idx] = back_since;
12138                        gfx.raw_keys_last_fire[back_idx] = back_fire;
12139                        gfx.raw_keys_prev[back_idx] = back_down;
12140                        for &vk in TEXT_POLL_VKS {
12141                            let idx = (vk as usize) & 0xFF;
12142                            let down = os_key_down(vk);
12143                            let was = gfx.raw_keys_prev[idx];
12144                            let (mut since, mut fire) =
12145                                (gfx.raw_keys_down_since[idx], gfx.raw_keys_last_fire[idx]);
12146                            if key_repeat_fire(now, down, was, &mut since, &mut fire) {
12147                                if let Some(c) = vk_char(vk, shift) {
12148                                    if self.text_buffer.chars().count() < TEXT_BUFFER_MAX {
12149                                        self.text_buffer.push(c);
12150                                    }
12151                                }
12152                            }
12153                            gfx.raw_keys_down_since[idx] = since;
12154                            gfx.raw_keys_last_fire[idx] = fire;
12155                            gfx.raw_keys_prev[idx] = down;
12156                        }
12157                        return Ok(Value::Str(self.text_buffer.clone()));
12158                    }
12159                }
12160                let (keys, shift) = {
12161                    let gfx = self.gfx.borrow();
12162                    match gfx.window.as_ref() {
12163                        Some(w) => (
12164                            w.get_keys_pressed(minifb::KeyRepeat::Yes),
12165                            w.is_key_down(minifb::Key::LeftShift)
12166                                || w.is_key_down(minifb::Key::RightShift),
12167                        ),
12168                        None => (Vec::new(), false),
12169                    }
12170                };
12171                for k in keys {
12172                    if k == minifb::Key::Backspace {
12173                        self.text_buffer.pop();
12174                    } else if let Some(c) = key_char(k, shift) {
12175                        if self.text_buffer.chars().count() < TEXT_BUFFER_MAX {
12176                            self.text_buffer.push(c);
12177                        }
12178                    }
12179                }
12180                return Ok(Value::Str(self.text_buffer.clone()));
12181            },
12182            #[cfg(target_arch = "wasm32")]
12183            "text_poll" => {
12184                return Ok(Value::Str(self.text_buffer.clone()));
12185            },
12186            "text_get" => return Ok(Value::Str(self.text_buffer.clone())),
12187            "text_set" => {
12188                self.text_buffer = self.arg_str(&args, 0, "");
12189                return Ok(Value::Unit);
12190            },
12191            "text_clear" => {
12192                self.text_buffer.clear();
12193                return Ok(Value::Unit);
12194            },
12195            // record_frame() — append the current framebuffer as a PPM, return frame #
12196            #[cfg(not(target_arch = "wasm32"))]
12197            "record_frame" => {
12198                let n = self.record_n;
12199                let (buf, w, h) = {
12200                    let gfx = self.gfx.borrow();
12201                    (gfx.buffer.clone(), gfx.width, gfx.height)
12202                };
12203                let _ = std::fs::create_dir_all("recordings");
12204                let mut out = Vec::with_capacity(w * h * 3 + 32);
12205                out.extend_from_slice(format!("P6\n{w} {h}\n255\n").as_bytes());
12206                for px in &buf {
12207                    let p = *px;
12208                    out.push((p >> 16) as u8);
12209                    out.push((p >> 8) as u8);
12210                    out.push(p as u8);
12211                }
12212                let _ = std::fs::write(format!("recordings/frame_{n:05}.ppm"), out);
12213                self.record_n += 1;
12214                return Ok(Value::Number(n as f64));
12215            },
12216            "record_count" => return Ok(Value::Number(self.record_n as f64)),
12217            // ── screenshot(mode) → PNG in ./screenshots/ with timestamp + mode + size ──
12218            #[cfg(not(target_arch = "wasm32"))]
12219            "screenshot" | "บันทึกภาพ" => {
12220                let mode = self.arg_str(&args, 0, "game");
12221                let (buf, w, h) = {
12222                    let gfx = self.gfx.borrow();
12223                    (gfx.buffer.clone(), gfx.width, gfx.height)
12224                };
12225                let _ = std::fs::create_dir_all("screenshots");
12226                let ts = std::time::SystemTime::now()
12227                    .duration_since(std::time::UNIX_EPOCH)
12228                    .map(|d| d.as_secs())
12229                    .unwrap_or(0);
12230                let safe: String = mode
12231                    .chars()
12232                    .map(|c| if c.is_alphanumeric() { c } else { '_' })
12233                    .collect();
12234                let path = format!("screenshots/ss_{ts}_{safe}_{w}x{h}.png");
12235                let mut rgb = Vec::with_capacity(w * h * 3);
12236                for px in &buf {
12237                    let p = *px;
12238                    rgb.push((p >> 16) as u8);
12239                    rgb.push((p >> 8) as u8);
12240                    rgb.push(p as u8);
12241                }
12242                if let Some(img) = image::RgbImage::from_raw(w as u32, h as u32, rgb) {
12243                    let _ = img.save(&path);
12244                }
12245                return Ok(Value::Str(path));
12246            },
12247            // ── microphone → crypto donut ──
12248            // mic_capture() — append the latest mic samples to the record buffer
12249            // (call each frame while recording). Returns the buffer length.
12250            #[cfg(not(target_arch = "wasm32"))]
12251            "mic_capture" => {
12252                if let Some(mic) = self.mic.as_ref() {
12253                    let s = mic.latest_samples();
12254                    self.mic_buffer.extend_from_slice(&s);
12255                    let cap = 96_000usize; // ~2 s @ 48 kHz
12256                    if self.mic_buffer.len() > cap {
12257                        let drop = self.mic_buffer.len() - cap;
12258                        self.mic_buffer.drain(0..drop);
12259                    }
12260                }
12261                return Ok(Value::Number(self.mic_buffer.len() as f64));
12262            },
12263            // mic_seed() — SHA3-256 hex of the recorded audio, usable as a donut seed
12264            #[cfg(not(target_arch = "wasm32"))]
12265            "mic_seed" => {
12266                let mut bytes = Vec::with_capacity(self.mic_buffer.len() * 4);
12267                for f in &self.mic_buffer {
12268                    bytes.extend_from_slice(&f.to_le_bytes());
12269                }
12270                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(&bytes))));
12271            },
12272            #[cfg(not(target_arch = "wasm32"))]
12273            "mic_clear" => {
12274                self.mic_buffer.clear();
12275                return Ok(Value::Number(0.0));
12276            },
12277            // flush the 3-D depth queue onto the framebuffer WITHOUT presenting,
12278            // so 2-D UI drawn afterwards overlays the 3-D scene.
12279            #[cfg(not(target_arch = "wasm32"))]
12280            "flush_3d" | "render_3d" => {
12281                let mut gfx = self.gfx.borrow_mut();
12282                if !gfx.depth_queue.is_empty() {
12283                    let w = gfx.width;
12284                    let h = gfx.height;
12285                    let dt = gfx.depth_test;
12286                    let reset_z = gfx.zbuf_needs_clear;
12287                    let (bm, ba) = (gfx.blend, gfx.alpha);
12288                    let aa = gfx.antialias;
12289                    let queue = std::mem::take(&mut gfx.depth_queue);
12290                    {
12291                        let g = &mut *gfx;
12292                        let z = if dt { Some(&mut g.depth_buf) } else { None };
12293                        queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
12294                    }
12295                    gfx.zbuf_needs_clear = false;
12296                    gfx.depth_queue.set_state(bm, ba); // keep active blend/alpha across the mid-frame flush
12297                }
12298                return Ok(Value::Unit);
12299            },
12300            #[cfg(target_arch = "wasm32")]
12301            "flush_3d" | "render_3d" => {
12302                let mut gfx = self.gfx.borrow_mut();
12303                if !gfx.depth_queue.is_empty() {
12304                    let w = gfx.width;
12305                    let h = gfx.height;
12306                    let dt = gfx.depth_test;
12307                    let reset_z = gfx.zbuf_needs_clear;
12308                    let (bm, ba) = (gfx.blend, gfx.alpha);
12309                    let aa = gfx.antialias;
12310                    let queue = std::mem::take(&mut gfx.depth_queue);
12311                    {
12312                        let g = &mut *gfx;
12313                        let z = if dt { Some(&mut g.depth_buf) } else { None };
12314                        queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
12315                    }
12316                    gfx.zbuf_needs_clear = false;
12317                    gfx.depth_queue.set_state(bm, ba);
12318                }
12319                return Ok(Value::Unit);
12320            },
12321
12322            // flush_post() — flush the 3-D queue like `flush_3d`, then run the
12323            // toon post-chain (SSAO → outlines → tone ramp → bloom → FXAA) over
12324            // the SCENE immediately. `present` skips the chain this frame, so
12325            // 2-D UI drawn after this call stays exact — no bloom/blur on HUDs.
12326            "flush_post" | "post_now" | "포스트플러시" | "后期冲刷" => {
12327                let mut gfx = self.gfx.borrow_mut();
12328                if !gfx.depth_queue.is_empty() {
12329                    let w = gfx.width;
12330                    let h = gfx.height;
12331                    let dt = gfx.depth_test;
12332                    let reset_z = gfx.zbuf_needs_clear;
12333                    let (bm, ba) = (gfx.blend, gfx.alpha);
12334                    let aa = gfx.antialias;
12335                    let queue = std::mem::take(&mut gfx.depth_queue);
12336                    {
12337                        let g = &mut *gfx;
12338                        let z = if dt { Some(&mut g.depth_buf) } else { None };
12339                        queue.flush(&mut g.buffer, z, reset_z, w, h, aa);
12340                    }
12341                    gfx.zbuf_needs_clear = false;
12342                    gfx.depth_queue.set_state(bm, ba);
12343                }
12344                gfx.toon_post_process();
12345                gfx.post_done = true;
12346                return Ok(Value::Unit);
12347            },
12348
12349            // Viscous full-screen distortion (warp/pucker/bloat, edge-wrapped). Call
12350            // after the 3-D flush and before the UI so only the world layer warps.
12351            #[cfg(not(target_arch = "wasm32"))]
12352            "screen_distort" | "บิดจอ" | "屏幕扭曲" | "画面歪み" | "화면왜곡" =>
12353            {
12354                let amount = self.arg_num(&args, 0, 8.0)? as f32;
12355                let t = self.arg_num(&args, 1, 0.0)? as f32;
12356                // optional `step` (default 1 = full res): 2 = half-res block warp
12357                // (~4× fewer warp computes, slightly softer — suits a liquid look).
12358                let step = self.arg_num(&args, 2, 1.0)?.max(1.0) as usize;
12359                let _d = std::time::Instant::now();
12360                self.gfx.borrow_mut().distort(amount, t, step);
12361                ling_phase_add(phase::DISTORT, _d.elapsed().as_nanos());
12362                return Ok(Value::Unit);
12363            },
12364
12365            "set_rim" | "设置边缘光" | "リム設定" | "림라이트" | "ตั้งขอบเรือง" =>
12366            {
12367                let s = self.arg_num(&args, 0, 0.6)? as f32;
12368                let r = self.arg_num(&args, 1, 115.)? as f32 / 255.0;
12369                let g = self.arg_num(&args, 2, 217.)? as f32 / 255.0;
12370                let b = self.arg_num(&args, 3, 255.)? as f32 / 255.0;
12371                let mut gfx = self.gfx.borrow_mut();
12372                gfx.shade.rim = s;
12373                gfx.shade.rim_color = [r, g, b];
12374                return Ok(Value::Unit);
12375            },
12376
12377            // ══════════════════════════════════════════════════════════════════
12378            // 3-D PRIMITIVES  (src/gfx/shapes.rs)  — "Inkscape for 3-D"
12379            //   shape(cx,cy,cz,  sx,sy,sz,  rx,ry,rz,  mode,  e0,e1,e2)
12380            //     centre (cx,cy,cz), per-axis scale, Euler rotation (radians),
12381            //     mode: 0 filled · 1 wireframe · 2 both,
12382            //     e0..e2: shape-specific (segments / sides / ratio …).
12383            //   Pen colour (set_color) drives fill lighting and wireframe colour.
12384            // ══════════════════════════════════════════════════════════════════
12385            n if crate::gfx::shapes::canon(n).is_some() => {
12386                let kind = crate::gfx::shapes::canon(n).unwrap();
12387                let cx = self.arg_num(&args, 0, 0.)? as f32;
12388                let cy = self.arg_num(&args, 1, 0.)? as f32;
12389                let cz = self.arg_num(&args, 2, 0.)? as f32;
12390                let sx = self.arg_num(&args, 3, 1.)? as f32;
12391                let sy = self.arg_num(&args, 4, 1.)? as f32;
12392                let sz = self.arg_num(&args, 5, 1.)? as f32;
12393                let rx = self.arg_num(&args, 6, 0.)? as f32;
12394                let ry = self.arg_num(&args, 7, 0.)? as f32;
12395                let rz = self.arg_num(&args, 8, 0.)? as f32;
12396                let mode = self.arg_num(&args, 9, 0.)? as i32;
12397                let e0 = self.arg_num(&args, 10, 0.)? as f32;
12398                let e1 = self.arg_num(&args, 11, 0.)? as f32;
12399                let e2 = self.arg_num(&args, 12, 0.)? as f32;
12400                if let Some(mesh) = crate::gfx::shapes::build(
12401                    kind,
12402                    [cx, cy, cz, sx, sy, sz, rx, ry, rz],
12403                    e0,
12404                    e1,
12405                    e2,
12406                ) {
12407                    let mut gfx = self.gfx.borrow_mut();
12408                    gfx.emit_mesh(&mesh, mode);
12409                }
12410                return Ok(Value::Unit);
12411            },
12412
12413            _ => {},
12414        }
12415
12416        // `form` struct constructor: positional `Name(v0, v1, ...)`.
12417        if let Some(field_names) = self.structs.get(name).cloned() {
12418            if args.len() != field_names.len() {
12419                return Err(EvalErr::from(format!(
12420                    "{name} expects {} field(s), got {}",
12421                    field_names.len(),
12422                    args.len()
12423                )));
12424            }
12425            let fields = field_names.into_iter().zip(args).collect();
12426            return Ok(Value::Struct { name: name.to_string(), fields });
12427        }
12428
12429        // `choose` enum variant constructor: `Variant(...)` or `Enum::Variant(...)`.
12430        if let Some((enum_name, arity)) = self.enum_variants.get(name).cloned() {
12431            if args.len() != arity {
12432                return Err(EvalErr::from(format!(
12433                    "{name} expects {arity} value(s), got {}",
12434                    args.len()
12435                )));
12436            }
12437            let variant = name.rsplit("::").next().unwrap_or(name).to_string();
12438            return Ok(Value::Variant { enum_name, variant, payload: args });
12439        }
12440
12441        #[cfg(target_arch = "wasm32")]
12442        if let Some(v) = wasm_unsupported_builtin(name) {
12443            return Ok(v);
12444        }
12445
12446        Err(EvalErr::from(format!("unknown function '{name}'")))
12447    }
12448
12449    fn call_value(&mut self, v: Value, args: Vec<Value>) -> EvalResult {
12450        match v {
12451            Value::Fn(params, body, mut captured) => {
12452                for (p, a) in params.iter().zip(args) {
12453                    captured.insert(p.clone(), a);
12454                }
12455                match self.framed("<closure>", |me| me.exec_block(&body, &mut captured)) {
12456                    Ok(v) => Ok(v.unwrap_or(Value::Unit)),
12457                    Err(EvalErr::Return(v)) => Ok(v),
12458                    Err(e) => Err(e),
12459                }
12460            },
12461            other => Err(EvalErr::from(format!("cannot call {:?}", other))),
12462        }
12463    }
12464
12465    fn call_method(&self, recv: Value, method: &str, args: Vec<Value>) -> EvalResult {
12466        match (&recv, method) {
12467            (Value::Str(s), "is_empty" | "是空") => Ok(Value::Bool(s.is_empty())),
12468            // All of `lingfu normalize`'s per-language spellings of len/push
12469            // (see ling-fu normalize.rs alias table), not just the Chinese
12470            // ones — normalize rewrites method calls into whichever language
12471            // the project is normalized to, and any spelling missing here
12472            // makes those calls un-callable post-normalize (first hit with
12473            // `.长度()`, then again with Thai `.ความยาว()`).
12474            (Value::Str(s), "len" | "长" | "长度" | "長さ" | "길이" | "ความยาว") => Ok(Value::Number(s.len() as f64)),
12475            (Value::Str(s), "to_string" | "转文") => Ok(Value::Str(s.clone())),
12476            (Value::Str(s), "contains" | "包含") => {
12477                if let Some(Value::Str(sub)) = args.first() {
12478                    Ok(Value::Bool(s.contains(sub.as_str())))
12479                } else {
12480                    Ok(Value::Bool(false))
12481                }
12482            },
12483            (Value::Str(s), "push_str" | "推_文") => {
12484                let mut s2 = s.clone();
12485                if let Some(Value::Str(a)) = args.first() {
12486                    s2.push_str(a);
12487                }
12488                Ok(Value::Str(s2))
12489            },
12490            (Value::List(v), "len" | "长" | "长度" | "長さ" | "길이" | "ความยาว") => Ok(Value::Number(v.len() as f64)),
12491            (Value::List(v), "push" | "推" | "添加" | "追加" | "추가" | "เพิ่ม") => {
12492                let mut v2: Vec<Value> = (**v).clone();
12493                if let Some(a) = args.first() {
12494                    v2.push(a.clone());
12495                }
12496                Ok(Value::List(Rc::new(v2)))
12497            },
12498            // `form` field access: `point.x` (no-arg method == field read).
12499            (Value::Struct { fields, .. }, _) if args.is_empty() => fields
12500                .iter()
12501                .find(|(k, _)| k == method)
12502                .map(|(_, v)| v.clone())
12503                .ok_or_else(|| EvalErr::from(format!("no field '{method}' on {recv}"))),
12504            // Enum introspection: `.tag` → variant name, `.is(Name)` not needed for now.
12505            (Value::Variant { variant, .. }, "tag" | "标签" | "タグ" | "태그" | "ป้าย")
12506                if args.is_empty() =>
12507            {
12508                Ok(Value::Str(variant.clone()))
12509            },
12510            (Value::Ok(inner), _) | (Value::Err(inner), _) => Ok(*inner.clone()),
12511            _ => Err(EvalErr::from(format!("no method '{method}' on {recv}"))),
12512        }
12513    }
12514
12515    // ─── Pattern matching ─────────────────────────────────────────────────────
12516
12517    fn match_pattern(&self, pat: &Pattern, val: &Value) -> Option<Env> {
12518        match (pat, val) {
12519            (Pattern::Wildcard, _) => Some(new_env()),
12520            (Pattern::Str(s), Value::Str(v)) if s == v => Some(new_env()),
12521            (Pattern::Number(n), Value::Number(v)) if (n - v).abs() < 1e-12 => Some(new_env()),
12522            (Pattern::Bool(b), Value::Bool(v)) if b == v => Some(new_env()),
12523            (Pattern::Ident(name), _) => {
12524                let mut e = new_env();
12525                e.insert(name.clone(), val.clone());
12526                Some(e)
12527            },
12528            (Pattern::Constructor(ctor, inner_pat), _) => {
12529                let (matches, inner_val) = match (ctor.as_str(), val) {
12530                    ("ok" | "好", Value::Ok(v)) => (true, Some(v.as_ref().clone())),
12531                    ("bad" | "坏", Value::Err(v)) => (true, Some(v.as_ref().clone())),
12532                    ("ok" | "好", v) if !matches!(v, Value::Err(_)) => (true, Some(v.clone())),
12533                    _ => (false, None),
12534                };
12535                if !matches {
12536                    return None;
12537                }
12538                match (inner_pat, inner_val) {
12539                    (Some(p), Some(v)) => self.match_pattern(p, &v),
12540                    (None, _) => Some(new_env()),
12541                    (Some(p), None) => self.match_pattern(p, &Value::Unit),
12542                }
12543            },
12544            // User enum variant pattern: `Circle(r)`, `Pair(a, b)`, nullary `Origin()`.
12545            (Pattern::Variant(vname, sub_pats), Value::Variant { variant, payload, .. }) => {
12546                if vname != variant || sub_pats.len() != payload.len() {
12547                    return None;
12548                }
12549                let mut bindings = new_env();
12550                for (p, v) in sub_pats.iter().zip(payload.iter()) {
12551                    bindings.extend(self.match_pattern(p, v)?);
12552                }
12553                Some(bindings)
12554            },
12555            // A zero-payload variant pattern also matches the bare result-style `ok`/`bad`
12556            // values so `Ok()`-style patterns keep working uniformly.
12557            (Pattern::Variant(vname, sub), Value::Ok(v)) if (vname == "ok" || vname == "好") => {
12558                match sub.as_slice() {
12559                    [] => Some(new_env()),
12560                    [p] => self.match_pattern(p, v),
12561                    _ => None,
12562                }
12563            },
12564            (Pattern::Variant(vname, sub), Value::Err(v))
12565                if (vname == "bad" || vname == "坏" || vname == "err") =>
12566            {
12567                match sub.as_slice() {
12568                    [] => Some(new_env()),
12569                    [p] => self.match_pattern(p, v),
12570                    _ => None,
12571                }
12572            },
12573            _ => None,
12574        }
12575    }
12576
12577    // ─── Utilities ───────────────────────────────────────────────────────────
12578
12579    fn value_to_iter(&self, val: Value) -> Result<Vec<Value>, EvalErr> {
12580        match val {
12581            Value::List(v) => Ok(Rc::try_unwrap(v).unwrap_or_else(|rc| (*rc).clone())),
12582            Value::Str(s) => Ok(s.chars().map(|c| Value::Str(c.to_string())).collect()),
12583            Value::Number(n) => Ok((0..n as i64).map(|i| Value::Number(i as f64)).collect()),
12584            other => Err(EvalErr::from(format!("cannot iterate over {:?}", other))),
12585        }
12586    }
12587
12588    pub(crate) fn is_truthy(&self, val: &Value) -> bool {
12589        match val {
12590            Value::Bool(b) => *b,
12591            Value::Unit => false,
12592            Value::Number(n) => *n != 0.0,
12593            Value::Str(s) => !s.is_empty(),
12594            Value::List(v) => !v.is_empty(),
12595            Value::Ok(_) => true,
12596            Value::Err(_) => false,
12597            Value::Fn(_, _, _) => true,
12598            Value::Struct { .. } => true,
12599            Value::Variant { .. } => true,
12600        }
12601    }
12602
12603    fn to_number(&self, val: &Value) -> Result<f64, EvalErr> {
12604        match val {
12605            Value::Number(n) => Ok(*n),
12606            Value::Str(s) => s
12607                .parse()
12608                .map_err(|_| EvalErr::from(format!("cannot convert '{s}' to number"))),
12609            other => Err(EvalErr::from(format!("expected number, got {:?}", other))),
12610        }
12611    }
12612
12613    /// Get the n-th argument as f64, falling back to `default` if missing.
12614    fn arg_num(&self, args: &[Value], n: usize, default: f64) -> Result<f64, EvalErr> {
12615        match args.get(n) {
12616            Some(v) => self.to_number(v),
12617            None => Ok(default),
12618        }
12619    }
12620
12621    fn arg_str(&self, args: &[Value], n: usize, default: &str) -> String {
12622        args.get(n)
12623            .map(|v| v.to_string())
12624            .unwrap_or_else(|| default.to_string())
12625    }
12626
12627    /// Read a list-of-numbers argument as `Vec<f32>` (empty if absent/not a list).
12628    #[allow(dead_code)]
12629    fn arg_list_f32(&self, args: &[Value], n: usize) -> Vec<f32> {
12630        match args.get(n) {
12631            Some(Value::List(v)) => v
12632                .iter()
12633                .filter_map(|x| match x {
12634                    Value::Number(n) => Some(*n as f32),
12635                    _ => None,
12636                })
12637                .collect(),
12638            _ => Vec::new(),
12639        }
12640    }
12641
12642    /// Optional `r,g,b` colour override starting at arg `i` → packed 0x00RRGGBB,
12643    /// or `default` if those three numeric args aren't present.
12644    #[cfg(not(target_arch = "wasm32"))]
12645    fn color_at(&self, args: &[Value], i: usize, default: u32) -> u32 {
12646        match (args.get(i), args.get(i + 1), args.get(i + 2)) {
12647            (Some(a), Some(b), Some(c)) => {
12648                match (self.to_number(a), self.to_number(b), self.to_number(c)) {
12649                    (Ok(r), Ok(g), Ok(bl)) => {
12650                        ((r as u32 & 0xFF) << 16) | ((g as u32 & 0xFF) << 8) | (bl as u32 & 0xFF)
12651                    },
12652                    _ => default,
12653                }
12654            },
12655            _ => default,
12656        }
12657    }
12658
12659    /// A pitch argument: a note-name string (`"C4"`, `"A#3"`) or a numeric MIDI value.
12660    #[cfg(not(target_arch = "wasm32"))]
12661    fn pitch_arg(&self, args: &[Value], i: usize, default: i32) -> i32 {
12662        match args.get(i) {
12663            Some(Value::Str(s)) => ling_music::note::parse_pitch(s).unwrap_or(default),
12664            Some(Value::Number(n)) => *n as i32,
12665            _ => default,
12666        }
12667    }
12668
12669    /// Current mouse position + left-button-down (native window only).
12670    #[cfg(not(target_arch = "wasm32"))]
12671    fn mouse_now(&self) -> (f32, f32, bool) {
12672        let gfx = self.gfx.borrow();
12673        let (mx, my) = gfx
12674            .window
12675            .as_ref()
12676            .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
12677            .unwrap_or((0.0, 0.0));
12678        let down = gfx
12679            .window
12680            .as_ref()
12681            .map(|w| w.get_mouse_down(minifb::MouseButton::Left))
12682            .unwrap_or(false);
12683        (mx, my, down)
12684    }
12685
12686    /// Rasterize a UI [`ling_ui::widgets::Draw`] into the framebuffer: filled
12687    /// polygons via the AA scanline fill, polylines via AA lines, honouring the
12688    /// current blend mode.
12689    #[cfg(not(target_arch = "wasm32"))]
12690    fn draw_ui(&self, d: &ling_ui::widgets::Draw) {
12691        let mut gfx = self.gfx.borrow_mut();
12692        let (w, h, add) = (gfx.width, gfx.height, gfx.blend == 1);
12693        for (c, poly) in &d.fills {
12694            crate::gfx::raster::fill_contours_aa(
12695                &mut gfx.buffer,
12696                w,
12697                h,
12698                *c,
12699                add,
12700                std::slice::from_ref(poly),
12701            );
12702        }
12703        for (c, pl) in &d.strokes {
12704            for s in pl.windows(2) {
12705                crate::gfx::raster::draw_line_aa(
12706                    &mut gfx.buffer,
12707                    w,
12708                    h,
12709                    *c,
12710                    add,
12711                    s[0][0],
12712                    s[0][1],
12713                    s[1][0],
12714                    s[1][1],
12715                );
12716            }
12717        }
12718    }
12719
12720    /// Parse (dst_x, dst_y, width, height) from the first four args of a tex_* builtin.
12721    fn tex_rect(&self, args: &[Value]) -> Result<(usize, usize, usize, usize), EvalErr> {
12722        let tx = self.arg_num(args, 0, 0.0)? as usize;
12723        let ty = self.arg_num(args, 1, 0.0)? as usize;
12724        let tw = self.arg_num(args, 2, 256.0)? as usize;
12725        let th = self.arg_num(args, 3, 256.0)? as usize;
12726        Ok((tx, ty, tw.max(1), th.max(1)))
12727    }
12728
12729    pub(crate) fn apply_binop(&self, op: &BinOp, l: Value, r: Value) -> EvalResult {
12730        match op {
12731            BinOp::Add => match (l, r) {
12732                (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a + b)),
12733                (Value::Str(a), Value::Str(b)) => Ok(Value::Str(a + &b)),
12734                (Value::Str(a), b) => Ok(Value::Str(a + &b.to_string())),
12735                (a, Value::Str(b)) => Ok(Value::Str(a.to_string() + &b)),
12736                (a, b) => Err(EvalErr::from(format!("cannot add {:?} and {:?}", a, b))),
12737            },
12738            BinOp::Sub => Ok(Value::Number(self.to_number(&l)? - self.to_number(&r)?)),
12739            BinOp::Mul => Ok(Value::Number(self.to_number(&l)? * self.to_number(&r)?)),
12740            BinOp::Div => Ok(Value::Number(self.to_number(&l)? / self.to_number(&r)?)),
12741            BinOp::Rem => Ok(Value::Number(self.to_number(&l)? % self.to_number(&r)?)),
12742            BinOp::Eq => Ok(Value::Bool(values_equal(&l, &r))),
12743            BinOp::Ne => Ok(Value::Bool(!values_equal(&l, &r))),
12744            BinOp::Lt => Ok(Value::Bool(self.to_number(&l)? < self.to_number(&r)?)),
12745            BinOp::Gt => Ok(Value::Bool(self.to_number(&l)? > self.to_number(&r)?)),
12746            BinOp::Le => Ok(Value::Bool(self.to_number(&l)? <= self.to_number(&r)?)),
12747            BinOp::Ge => Ok(Value::Bool(self.to_number(&l)? >= self.to_number(&r)?)),
12748            BinOp::And => Ok(Value::Bool(self.is_truthy(&l) && self.is_truthy(&r))),
12749            BinOp::Or => Ok(Value::Bool(self.is_truthy(&l) || self.is_truthy(&r))),
12750        }
12751    }
12752
12753    fn builtin_format(&self, args: &[Value]) -> Result<String, EvalErr> {
12754        if args.is_empty() {
12755            return Ok(String::new());
12756        }
12757        let fmt = match &args[0] {
12758            Value::Str(s) => s.clone(),
12759            other => return Ok(other.to_string()),
12760        };
12761
12762        let mut result = String::new();
12763        let mut arg_idx = 1usize;
12764        let mut chars = fmt.chars().peekable();
12765        while let Some(c) = chars.next() {
12766            if c == '{' {
12767                if chars.peek() == Some(&'}') {
12768                    chars.next();
12769                    if arg_idx < args.len() {
12770                        result.push_str(&args[arg_idx].to_string());
12771                        arg_idx += 1;
12772                    }
12773                } else {
12774                    let mut spec = String::new();
12775                    for ch in chars.by_ref() {
12776                        if ch == '}' {
12777                            break;
12778                        }
12779                        spec.push(ch);
12780                    }
12781                    if arg_idx < args.len() {
12782                        if let Some(suffix) = spec.strip_prefix(":.") {
12783                            if let Value::Number(n) = &args[arg_idx] {
12784                                let prec: usize =
12785                                    suffix.trim_end_matches('f').parse().unwrap_or(2);
12786                                result.push_str(&format!("{:.prec$}", n));
12787                                arg_idx += 1;
12788                                continue;
12789                            }
12790                        }
12791                        result.push_str(&args[arg_idx].to_string());
12792                        arg_idx += 1;
12793                    }
12794                }
12795            } else {
12796                result.push(c);
12797            }
12798        }
12799        Ok(result)
12800    }
12801}
12802
12803#[cfg(not(target_arch = "wasm32"))]
12804/// Map a friendly button name (any vendor / d-pad alias) to a gamepad button.
12805#[cfg(not(target_arch = "wasm32"))]
12806fn parse_pad_button(name: &str) -> Option<ling_input::GamepadButton> {
12807    use ling_input::GamepadButton as B;
12808    Some(match name.to_ascii_lowercase().as_str() {
12809        "a" | "south" | "cross" => B::South,
12810        "b" | "east" | "circle" => B::East,
12811        "x" | "west" | "square" => B::West,
12812        "y" | "north" | "triangle" => B::North,
12813        "lb" | "l1" | "left_shoulder" => B::LeftShoulder,
12814        "rb" | "r1" | "right_shoulder" => B::RightShoulder,
12815        "lt" | "l2" | "left_trigger" => B::LeftTrigger,
12816        "rt" | "r2" | "right_trigger" => B::RightTrigger,
12817        "start" | "menu" | "options" => B::Start,
12818        "select" | "back" | "share" | "view" => B::Select,
12819        "guide" | "home" => B::Guide,
12820        "l3" | "left_stick" => B::LeftStick,
12821        "r3" | "right_stick" => B::RightStick,
12822        "up" | "dpad_up" => B::DpadUp,
12823        "down" | "dpad_down" => B::DpadDown,
12824        "left" | "dpad_left" => B::DpadLeft,
12825        "right" | "dpad_right" => B::DpadRight,
12826        _ => return None,
12827    })
12828}
12829
12830#[cfg(not(target_arch = "wasm32"))]
12831fn str_to_minifb_key(name: &str) -> Option<minifb::Key> {
12832    use minifb::Key;
12833    Some(match name {
12834        "numpad0" | "kp0" => Key::NumPad0,
12835        "numpad1" | "kp1" => Key::NumPad1,
12836        "numpad2" | "kp2" => Key::NumPad2,
12837        "numpad3" | "kp3" => Key::NumPad3,
12838        "numpad4" | "kp4" => Key::NumPad4,
12839        "numpad5" | "kp5" => Key::NumPad5,
12840        "numpad6" | "kp6" => Key::NumPad6,
12841        "numpad7" | "kp7" => Key::NumPad7,
12842        "numpad8" | "kp8" => Key::NumPad8,
12843        "numpad9" | "kp9" => Key::NumPad9,
12844        "numpad+" | "kp+" => Key::NumPadPlus,
12845        "numpad-" | "kp-" => Key::NumPadMinus,
12846        "numpad*" | "kp*" => Key::NumPadAsterisk,
12847        "numpad/" | "kp/" => Key::NumPadSlash,
12848        "left" => Key::Left,
12849        "right" => Key::Right,
12850        "up" => Key::Up,
12851        "down" => Key::Down,
12852        "space" => Key::Space,
12853        "enter" => Key::Enter,
12854        "escape" => Key::Escape,
12855        "pageup" => Key::PageUp,
12856        "pagedown" => Key::PageDown,
12857        "lshift" | "leftshift" => Key::LeftShift,
12858        "rshift" | "rightshift" => Key::RightShift,
12859        "lctrl" | "leftctrl" => Key::LeftCtrl,
12860        "rctrl" | "rightctrl" => Key::RightCtrl,
12861        "lalt" | "leftalt" => Key::LeftAlt,
12862        "ralt" | "rightalt" => Key::RightAlt,
12863        "tab" => Key::Tab,
12864        "backspace" => Key::Backspace,
12865        "delete" => Key::Delete,
12866        "insert" => Key::Insert,
12867        "home" => Key::Home,
12868        "end" => Key::End,
12869        "a" => Key::A,
12870        "b" => Key::B,
12871        "c" => Key::C,
12872        "d" => Key::D,
12873        "e" => Key::E,
12874        "f" => Key::F,
12875        "g" => Key::G,
12876        "h" => Key::H,
12877        "i" => Key::I,
12878        "j" => Key::J,
12879        "k" => Key::K,
12880        "l" => Key::L,
12881        "m" => Key::M,
12882        "n" => Key::N,
12883        "o" => Key::O,
12884        "p" => Key::P,
12885        "q" => Key::Q,
12886        "r" => Key::R,
12887        "s" => Key::S,
12888        "t" => Key::T,
12889        "u" => Key::U,
12890        "v" => Key::V,
12891        "w" => Key::W,
12892        "x" => Key::X,
12893        "y" => Key::Y,
12894        "z" => Key::Z,
12895        "0" => Key::Key0,
12896        "1" => Key::Key1,
12897        "2" => Key::Key2,
12898        "3" => Key::Key3,
12899        "4" => Key::Key4,
12900        "5" => Key::Key5,
12901        "6" => Key::Key6,
12902        "7" => Key::Key7,
12903        "8" => Key::Key8,
12904        "9" => Key::Key9,
12905        _ => return None,
12906    })
12907}
12908
12909pub(crate) fn values_equal(a: &Value, b: &Value) -> bool {
12910    match (a, b) {
12911        (Value::Number(x), Value::Number(y)) => (x - y).abs() < 1e-12,
12912        (Value::Str(x), Value::Str(y)) => x == y,
12913        (Value::Bool(x), Value::Bool(y)) => x == y,
12914        (Value::Unit, Value::Unit) => true,
12915        _ => false,
12916    }
12917}
12918
12919// Rasteriser functions live in crate::gfx::raster — imported at top of file.
12920
12921// ── Window platform helpers ────────────────────────────────────────────────────
12922
12923/// Hide the console window that the OS auto-attaches to console-subsystem
12924/// processes. No-op on non-Windows and when no console is present.
12925#[cfg(not(target_arch = "wasm32"))]
12926fn hide_console_window() {
12927    #[cfg(windows)]
12928    unsafe {
12929        extern "system" {
12930            fn GetConsoleWindow() -> isize;
12931            fn ShowWindow(hwnd: isize, nCmdShow: i32) -> i32;
12932        }
12933        let hwnd = GetConsoleWindow();
12934        if hwnd != 0 {
12935            ShowWindow(hwnd, 0); // SW_HIDE = 0
12936        }
12937    }
12938}
12939
12940/// Strip *all* window chrome from `hwnd` and make it cover the whole primary
12941/// monitor (0,0 → screen_w × screen_h), above the taskbar. This turns the
12942/// minifb window into a true borderless-fullscreen surface: no title bar, no
12943/// frame, no resize grips — there is no visible window "handle" left.
12944#[cfg(all(not(target_arch = "wasm32"), windows))]
12945fn make_borderless_fullscreen(hwnd: isize, screen_w: i32, screen_h: i32) {
12946    if hwnd == 0 {
12947        return;
12948    }
12949    unsafe {
12950        extern "system" {
12951            fn SetWindowLongPtrW(hwnd: isize, index: i32, new: isize) -> isize;
12952            fn SetWindowPos(
12953                hwnd: isize,
12954                insert_after: isize,
12955                x: i32,
12956                y: i32,
12957                cx: i32,
12958                cy: i32,
12959                flags: u32,
12960            ) -> i32;
12961            fn ShowWindow(hwnd: isize, cmd: i32) -> i32;
12962        }
12963        const GWL_STYLE: i32 = -16;
12964        const GWL_EXSTYLE: i32 = -20;
12965        // WS_POPUP (0x80000000) | WS_VISIBLE (0x10000000) — a bare top-level
12966        // window with no caption, border, or system menu.
12967        SetWindowLongPtrW(hwnd, GWL_STYLE, 0x9000_0000isize);
12968        // Clear extended edges (WS_EX_WINDOWEDGE / CLIENTEDGE / DLGMODALFRAME).
12969        SetWindowLongPtrW(hwnd, GWL_EXSTYLE, 0);
12970        // HWND_TOPMOST = -1; SWP_FRAMECHANGED (0x0020) | SWP_SHOWWINDOW (0x0040).
12971        SetWindowPos(hwnd, -1isize, 0, 0, screen_w, screen_h, 0x0020 | 0x0040);
12972        ShowWindow(hwnd, 3); // SW_MAXIMIZE-equivalent paint; 3 = SW_SHOWMAXIMIZED
12973    }
12974}
12975
12976/// Force real OS keyboard focus onto `hwnd`, not just Z-order prominence.
12977/// Windows' foreground-lock can leave a freshly-created window topmost — so
12978/// VISUALLY it covers everything — without actually handing it keyboard
12979/// focus, e.g. when launched from a terminal that still holds real focus:
12980/// clicks can nudge focus over (a more "user-driven" event) but typed keys
12981/// silently keep going to whatever app really has it, which looks exactly
12982/// like "clicking a text field doesn't focus it". AttachThreadInput is the
12983/// standard documented workaround — it lets SetForegroundWindow succeed even
12984/// under the lock by sharing input state with whichever thread currently
12985/// owns the foreground window. Call this LAST, after every other
12986/// window-visibility change for this launch (anything that shows/hides a
12987/// window afterward — e.g. hiding the launching console — can itself
12988/// reassign the foreground window and undo an earlier focus claim).
12989#[cfg(all(not(target_arch = "wasm32"), windows))]
12990fn force_window_focus(hwnd: isize) {
12991    if hwnd == 0 {
12992        return;
12993    }
12994    unsafe {
12995        extern "system" {
12996            fn GetForegroundWindow() -> isize;
12997            fn GetWindowThreadProcessId(hwnd: isize, pid: *mut u32) -> u32;
12998            fn GetCurrentThreadId() -> u32;
12999            fn AttachThreadInput(id_attach: u32, id_attach_to: u32, attach: i32) -> i32;
13000            fn SetForegroundWindow(hwnd: isize) -> i32;
13001            fn BringWindowToTop(hwnd: isize) -> i32;
13002            fn SetFocus(hwnd: isize) -> isize;
13003            fn SetActiveWindow(hwnd: isize) -> isize;
13004        }
13005        let fg = GetForegroundWindow();
13006        if fg != 0 && fg != hwnd {
13007            let mut fg_pid: u32 = 0;
13008            let fg_tid = GetWindowThreadProcessId(fg, &mut fg_pid);
13009            let my_tid = GetCurrentThreadId();
13010            if fg_tid != 0 && fg_tid != my_tid {
13011                AttachThreadInput(my_tid, fg_tid, 1);
13012                SetForegroundWindow(hwnd);
13013                BringWindowToTop(hwnd);
13014                SetFocus(hwnd);
13015                SetActiveWindow(hwnd);
13016                AttachThreadInput(my_tid, fg_tid, 0);
13017                return;
13018            }
13019        }
13020        SetForegroundWindow(hwnd);
13021        BringWindowToTop(hwnd);
13022        SetFocus(hwnd);
13023        SetActiveWindow(hwnd);
13024    }
13025}
13026
13027/// Toggle `hwnd`'s HWND_TOPMOST z-order style without moving/resizing/
13028/// activating it — used to drop the borderless-fullscreen window's topmost
13029/// flag on alt-tab (so it stops covering whatever the user switched to) and
13030/// restore it when the user switches back.
13031#[cfg(all(not(target_arch = "wasm32"), windows))]
13032fn set_window_topmost(hwnd: isize, topmost: bool) {
13033    if hwnd == 0 {
13034        return;
13035    }
13036    unsafe {
13037        extern "system" {
13038            fn SetWindowPos(
13039                hwnd: isize,
13040                insert_after: isize,
13041                x: i32,
13042                y: i32,
13043                cx: i32,
13044                cy: i32,
13045                flags: u32,
13046            ) -> i32;
13047        }
13048        let insert_after: isize = if topmost { -1 } else { -2 }; // HWND_TOPMOST / HWND_NOTOPMOST
13049        // SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE — pure z-order change,
13050        // must not steal focus back when restoring topmost on refocus.
13051        SetWindowPos(hwnd, insert_after, 0, 0, 0, 0, 0x0002 | 0x0001 | 0x0010);
13052    }
13053}
13054
13055/// Pace `win` to `vsync`'s target rate. `LING_FPS_CAP` (0 = uncapped, else an
13056/// explicit fps) always overrides; otherwise vsync-on paces to the monitor's
13057/// real refresh rate and vsync-off runs uncapped. minifb has no swap-interval
13058/// vsync (it owns no GPU present queue), so this is frame-rate pacing to the
13059/// refresh rate, not a tear-free guarantee.
13060#[cfg(not(target_arch = "wasm32"))]
13061fn apply_frame_pacing(win: &mut minifb::Window, vsync: bool) {
13062    match std::env::var("LING_FPS_CAP")
13063        .ok()
13064        .and_then(|v| v.parse::<usize>().ok())
13065    {
13066        Some(0) => win.set_target_fps(100_000),
13067        Some(cap) => win.set_target_fps(cap),
13068        None if vsync => win.set_target_fps(monitor_info().2.max(30) as usize),
13069        None => win.set_target_fps(100_000),
13070    }
13071}
13072
13073/// Primary-monitor resolution and refresh rate as `(width, height, hz)`.
13074/// `hz` falls back to 60 when the driver reports an unknown/`default` rate.
13075#[cfg(all(not(target_arch = "wasm32"), windows))]
13076fn monitor_info() -> (i32, i32, i32) {
13077    unsafe {
13078        extern "system" {
13079            fn GetSystemMetrics(index: i32) -> i32;
13080            fn GetDC(hwnd: isize) -> isize;
13081            fn ReleaseDC(hwnd: isize, hdc: isize) -> i32;
13082            fn GetDeviceCaps(hdc: isize, index: i32) -> i32;
13083        }
13084        let w = GetSystemMetrics(0).max(1); // SM_CXSCREEN
13085        let h = GetSystemMetrics(1).max(1); // SM_CYSCREEN
13086        let hdc = GetDC(0);
13087        let mut hz = if hdc != 0 { GetDeviceCaps(hdc, 116) } else { 0 }; // VREFRESH
13088        if hdc != 0 {
13089            ReleaseDC(0, hdc);
13090        }
13091        if hz <= 1 {
13092            hz = 60; // 0 or 1 means "device default" → assume 60 Hz
13093        }
13094        (w, h, hz)
13095    }
13096}
13097
13098/// Non-Windows native fallback: resolution from [`native_screen_size`]; refresh
13099/// from the active X11/RandR mode (so a 144 Hz panel drives the loop at 144),
13100/// falling back to 60 Hz when it can't be detected (Wayland, headless, macOS).
13101#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
13102fn monitor_info() -> (i32, i32, i32) {
13103    let (w, h) = native_screen_size();
13104    (w as i32, h as i32, linux_refresh_hz().unwrap_or(60))
13105}
13106
13107/// Active display refresh rate via `xrandr`. Each connected output's active
13108/// mode is the token flagged with `*` (e.g. `1920x1080 144.00*+`); we take the
13109/// max across all active outputs so a multi-monitor rig drives the loop at
13110/// its fastest panel.
13111#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
13112fn linux_refresh_hz() -> Option<i32> {
13113    let out = std::process::Command::new("xrandr")
13114        .arg("--current")
13115        .output()
13116        .ok()?;
13117    if !out.status.success() {
13118        return None;
13119    }
13120    parse_xrandr_max_hz(&String::from_utf8_lossy(&out.stdout))
13121}
13122
13123/// Pure parse used by [`linux_refresh_hz`]: the highest `*`-flagged refresh
13124/// rate across all active outputs in `xrandr --current` output.
13125#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
13126fn parse_xrandr_max_hz(text: &str) -> Option<i32> {
13127    text.split_whitespace()
13128        .filter(|tok| tok.contains('*'))
13129        .filter_map(|tok| {
13130            tok.trim_matches(|c: char| !c.is_ascii_digit() && c != '.')
13131                .parse::<f64>()
13132                .ok()
13133        })
13134        .map(|hz| hz.round() as i32)
13135        .filter(|&hz| (24..=1000).contains(&hz))
13136        .max()
13137}
13138
13139/// WASM fallback: the canvas is the display surface; assume 60 Hz.
13140#[cfg(target_arch = "wasm32")]
13141fn monitor_info() -> (i32, i32, i32) {
13142    let (w, h) = crate::gfx::webgl::canvas_size();
13143    (w as i32, h as i32, 60)
13144}
13145
13146#[cfg(all(test, not(target_arch = "wasm32"), not(windows)))]
13147mod xrandr_tests {
13148    use super::parse_xrandr_max_hz;
13149
13150    #[test]
13151    fn picks_highest_active_output() {
13152        let text = "\
13153eDP-1 connected primary 1920x1080+0+0
13154   1920x1080     60.00*+  59.94
13155DP-1 connected 2560x1440+1920+0
13156   2560x1440    144.00*+  120.00  60.00
13157";
13158        assert_eq!(parse_xrandr_max_hz(text), Some(144));
13159    }
13160
13161    #[test]
13162    fn single_output() {
13163        let text = "   1920x1080     75.00*+  60.00\n";
13164        assert_eq!(parse_xrandr_max_hz(text), Some(75));
13165    }
13166
13167    #[test]
13168    fn no_active_mode_returns_none() {
13169        let text = "eDP-1 disconnected\n";
13170        assert_eq!(parse_xrandr_max_hz(text), None);
13171    }
13172
13173    #[test]
13174    fn out_of_range_hz_filtered() {
13175        let text = "   1x1     5000.00*+\n";
13176        assert_eq!(parse_xrandr_max_hz(text), None);
13177    }
13178}
13179
13180/// Query the primary display resolution on non-Windows platforms.
13181/// Falls back to 1920×1080 if the size cannot be determined.
13182#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
13183fn native_screen_size() -> (f64, f64) {
13184    // On Linux/macOS we don't have an easy dependency-free call; return a
13185    // sensible default. Callers can always pass explicit dimensions.
13186    (1920.0, 1080.0)
13187}
13188
13189// ════════════════════════════════════════════════════════════════════════════
13190// Builtin call profiler  (env-gated, near-zero cost when off)
13191//
13192//   LING_PROFILE=1            enable per-builtin call-count + inclusive-time tally
13193//   LING_PROFILE_EVERY=N      print the report every N frames (default 240)
13194//
13195// Every builtin call funnels through `Interp::call_named` (JIT via `ling_builtin`,
13196// tree-walker directly), so this captures the full render/physics/audio builtin
13197// hot-path. Report is sorted by total time and prints calls, calls/frame,
13198// total_ms and ms/frame — the top-down "what's making so many calls" view.
13199// ════════════════════════════════════════════════════════════════════════════
13200struct LingProfileState {
13201    enabled: bool,
13202    every: u64,
13203    frames: u64,
13204    calls: std::collections::HashMap<String, (u64, u128)>, // name -> (count, nanos)
13205}
13206
13207thread_local! {
13208    static LING_PROFILE: std::cell::RefCell<LingProfileState> = std::cell::RefCell::new({
13209        let enabled = std::env::var("LING_PROFILE").map(|v| v != "0" && !v.is_empty()).unwrap_or(false);
13210        let every = std::env::var("LING_PROFILE_EVERY").ok()
13211            .and_then(|v| v.parse::<u64>().ok()).filter(|&n| n > 0).unwrap_or(240);
13212        if enabled {
13213            eprintln!("[ling-profile] ON — report every {every} frames (set LING_PROFILE_EVERY to change)");
13214        }
13215        LingProfileState { enabled, every, frames: 0, calls: std::collections::HashMap::new() }
13216    });
13217}
13218
13219#[inline]
13220fn ling_profile_enabled() -> bool {
13221    LING_PROFILE.with(|p| p.borrow().enabled)
13222}
13223
13224thread_local! {
13225    static LING_FPS: std::cell::RefCell<(bool, f64, u32, f64)> = std::cell::RefCell::new(
13226        (std::env::var("LING_FPS").map(|v| v != "0" && !v.is_empty()).unwrap_or(false), 0.0, 0, 0.0)
13227    );
13228}
13229
13230#[cfg(not(target_arch = "wasm32"))]
13231fn ling_fps_tick() {
13232    LING_FPS.with(|s| {
13233        let mut s = s.borrow_mut();
13234        if !s.0 {
13235            return;
13236        }
13237        let now = crate::runtime::now_secs();
13238        if s.1 > 0.0 {
13239            s.3 += now - s.1;
13240            s.2 += 1;
13241            if s.2 >= 120 {
13242                let avg = s.3 / s.2 as f64;
13243                eprintln!(
13244                    "[fps] {:.1} fps  ({:.2} ms/frame, wall, {} frames)",
13245                    1.0 / avg,
13246                    avg * 1000.0,
13247                    s.2
13248                );
13249                s.2 = 0;
13250                s.3 = 0.0;
13251            }
13252        }
13253        s.1 = now;
13254    });
13255}
13256
13257// Coarse render-pipeline timers (set LING_PHASE=1). Each accumulates wall-time
13258// per frame at flush/present granularity, so the cost is negligible. Reports the
13259// software-rasteriser breakdown the builtin profiler can't separate (the work is
13260// all inside the `present`/`flush_3d` builtins).
13261thread_local! {
13262    static LING_PHASE: std::cell::RefCell<(bool, u64, [u128; 5])> = std::cell::RefCell::new(
13263        (std::env::var_os("LING_PHASE").is_some(), 0, [0; 5])
13264    );
13265}
13266
13267/// Phase indices for [`ling_phase_add`].
13268pub mod phase {
13269    pub const FLUSH: usize = 0;
13270    pub const TOON: usize = 1;
13271    pub const BLIT: usize = 2;
13272    pub const DISTORT: usize = 3;
13273    pub const SORT: usize = 4;
13274}
13275
13276#[cfg(not(target_arch = "wasm32"))]
13277#[inline]
13278pub fn ling_phase_add(idx: usize, nanos: u128) {
13279    LING_PHASE.with(|p| {
13280        let mut p = p.borrow_mut();
13281        if p.0 {
13282            p.2[idx] += nanos;
13283        }
13284    });
13285}
13286
13287#[cfg(not(target_arch = "wasm32"))]
13288fn ling_phase_frame() {
13289    LING_PHASE.with(|p| {
13290        let mut p = p.borrow_mut();
13291        if !p.0 {
13292            return;
13293        }
13294        p.1 += 1;
13295        if p.1 >= 120 {
13296            let f = p.1 as f64;
13297            let ms = |i: usize| p.2[i] as f64 / 1e6 / f;
13298            eprintln!(
13299                "[phase] sort={:.2} flush={:.2} toon={:.2} blit={:.2} distort={:.2} ms/frame",
13300                ms(phase::SORT),
13301                ms(phase::FLUSH),
13302                ms(phase::TOON),
13303                ms(phase::BLIT),
13304                ms(phase::DISTORT)
13305            );
13306            p.1 = 0;
13307            p.2 = [0; 5];
13308        }
13309    });
13310}
13311
13312fn ling_profile_record(name: &str, nanos: u128) {
13313    // Frame boundary = a present() call.
13314    let is_frame = matches!(
13315        name,
13316        "present" | "แสดงผล" | "gfx_present" | "show" | "显" | "呈现" | "表示" | "표시"
13317    );
13318    LING_PROFILE.with(|p| {
13319        let mut p = p.borrow_mut();
13320        let e = p.calls.entry(name.to_string()).or_insert((0, 0));
13321        e.0 += 1;
13322        e.1 += nanos;
13323        if is_frame {
13324            p.frames += 1;
13325            if p.frames % p.every == 0 {
13326                ling_profile_print(&p);
13327            }
13328        }
13329    });
13330}
13331
13332fn ling_profile_print(p: &LingProfileState) {
13333    let mut rows: Vec<(&String, u64, u128)> =
13334        p.calls.iter().map(|(n, (c, ns))| (n, *c, *ns)).collect();
13335    use std::cmp::Reverse;
13336    rows.sort_by_key(|x| Reverse(x.2)); // by total time desc
13337    let total_ns: u128 = p.calls.values().map(|(_, ns)| *ns).sum();
13338    let total_calls: u64 = p.calls.values().map(|(c, _)| *c).sum();
13339    let fr = p.frames.max(1) as f64;
13340    eprintln!(
13341        "\n┌─ LING PROFILE ── frames={} ─ builtin calls by total inclusive time ─────────────",
13342        p.frames
13343    );
13344    eprintln!(
13345        "│ {:<24} {:>9} {:>9} {:>10} {:>9} {:>6}",
13346        "builtin", "calls", "calls/fr", "total_ms", "ms/frame", "%time"
13347    );
13348    eprintln!("├──────────────────────────────────────────────────────────────────────────────");
13349    for (name, count, ns) in rows.iter().take(30) {
13350        let ms = *ns as f64 / 1e6;
13351        let pct = if total_ns > 0 {
13352            *ns as f64 / total_ns as f64 * 100.0
13353        } else {
13354            0.0
13355        };
13356        eprintln!(
13357            "│ {:<24} {:>9} {:>9.1} {:>10.1} {:>9.3} {:>5.1}%",
13358            truncate_name(name),
13359            count,
13360            *count as f64 / fr,
13361            ms,
13362            ms / fr,
13363            pct
13364        );
13365    }
13366    eprintln!("├──────────────────────────────────────────────────────────────────────────────");
13367    eprintln!(
13368        "│ TOTAL {} builtin calls, {:.1} ms over {} frames  →  {:.0} calls/frame, {:.2} ms/frame in builtins",
13369        total_calls,
13370        total_ns as f64 / 1e6,
13371        p.frames,
13372        total_calls as f64 / fr,
13373        total_ns as f64 / 1e6 / fr
13374    );
13375    eprintln!("└──────────────────────────────────────────────────────────────────────────────");
13376}
13377
13378/// Trim a builtin name to fit the report column (counts chars, good enough for
13379/// the mixed-script names).
13380fn truncate_name(s: &str) -> String {
13381    let max = 24;
13382    if s.chars().count() <= max {
13383        s.to_string()
13384    } else {
13385        let mut t: String = s.chars().take(max - 1).collect();
13386        t.push('…');
13387        t
13388    }
13389}