1use std::io::Write;
14use std::path::Path;
15
16use base64::Engine as _;
17
18pub fn run(args: &[String]) -> i32 {
21 let input = match args.get(1) {
23 Some(s) if !s.starts_with('-') => s.clone(),
24 _ => {
25 eprintln!("Usage: ling convert <file.(gltf|glb|wav|ogg|flac|mid|svg|blend)> [-o out.ling] [--no-compression]");
26 return 1;
27 },
28 };
29 let compress = !args.iter().any(|a| a == "--no-compression");
30 let out = flag_value(args, "-o")
31 .or_else(|| flag_value(args, "--out"))
32 .unwrap_or_else(|| default_out(&input));
33
34 match convert(&input, &out, compress) {
35 Ok(bytes) => {
36 eprintln!(
37 "[convert] {} → {} ({} KB, {})",
38 input,
39 out,
40 bytes / 1024,
41 if compress {
42 "deflate+base64 lossless"
43 } else {
44 "uncompressed"
45 }
46 );
47 0
48 },
49 Err(e) => {
50 eprintln!("[convert] error: {e}");
51 1
52 },
53 }
54}
55
56fn default_out(input: &str) -> String {
57 let p = Path::new(input);
58 p.with_extension("ling").to_string_lossy().into_owned()
59}
60
61fn flag_value(args: &[String], flag: &str) -> Option<String> {
62 args.iter()
63 .position(|a| a == flag)
64 .and_then(|i| args.get(i + 1).cloned())
65}
66
67pub fn convert(input: &str, output: &str, compress: bool) -> Result<usize, String> {
69 let ext = Path::new(input)
70 .extension()
71 .map(|e| e.to_string_lossy().to_lowercase())
72 .unwrap_or_default();
73 let stem = Path::new(input)
74 .file_stem()
75 .map(|s| s.to_string_lossy().into_owned())
76 .unwrap_or_else(|| "asset".into());
77 let name = sanitize(&stem);
78
79 let ling = match ext.as_str() {
80 "gltf" | "glb" => conv_gltf(input, &name, compress)?,
81 "wav" | "ogg" | "flac" | "mp3" => conv_audio(input, &name, compress)?,
82 "mid" | "midi" => conv_midi(input, &name, compress)?,
83 "svg" => conv_svg(input, &name, compress)?,
84 "blend" => conv_blend(input, output, compress)?,
85 other => return Err(format!("unsupported extension '.{other}'")),
86 };
87
88 let mut f = std::fs::File::create(output).map_err(|e| format!("{output}: {e}"))?;
89 f.write_all(ling.as_bytes()).map_err(|e| e.to_string())?;
90 Ok(ling.len())
91}
92
93fn deflate(bytes: &[u8]) -> Vec<u8> {
96 use flate2::{write::ZlibEncoder, Compression};
97 let mut e = ZlibEncoder::new(Vec::new(), Compression::best());
98 let _ = e.write_all(bytes);
99 e.finish().unwrap_or_default()
100}
101
102fn b64(bytes: &[u8]) -> String {
103 base64::engine::general_purpose::STANDARD.encode(bytes)
104}
105
106fn emit_f32(name: &str, data: &[f32], compress: bool) -> String {
108 if compress && data.len() > 8 {
109 let mut bytes = Vec::with_capacity(data.len() * 4);
110 for v in data {
111 bytes.extend_from_slice(&v.to_le_bytes());
112 }
113 format!("bind {name} = blob_f32(\"{}\")\n", b64(&deflate(&bytes)))
114 } else {
115 let body: Vec<String> = data.iter().map(|v| fmt_f32(*v)).collect();
116 format!("bind {name} = [{}]\n", body.join(", "))
117 }
118}
119
120fn emit_i32(name: &str, data: &[u32], compress: bool) -> String {
121 if compress && data.len() > 8 {
122 let mut bytes = Vec::with_capacity(data.len() * 4);
123 for v in data {
124 bytes.extend_from_slice(&(*v as i32).to_le_bytes());
125 }
126 format!("bind {name} = blob_i32(\"{}\")\n", b64(&deflate(&bytes)))
127 } else {
128 let body: Vec<String> = data.iter().map(|v| v.to_string()).collect();
129 format!("bind {name} = [{}]\n", body.join(", "))
130 }
131}
132
133fn fmt_f32(v: f32) -> String {
134 if v == v.trunc() && v.abs() < 1e7 {
135 format!("{:.1}", v)
136 } else {
137 format!("{}", v)
138 }
139}
140
141fn sanitize(s: &str) -> String {
142 let mut out: String = s
143 .chars()
144 .map(|c| if c.is_alphanumeric() { c } else { '_' })
145 .collect();
146 if out
147 .chars()
148 .next()
149 .map(|c| c.is_ascii_digit())
150 .unwrap_or(true)
151 {
152 out.insert(0, '_');
153 }
154 out
155}
156
157fn header(kind: &str, src: &str) -> String {
158 format!(
159 "# ───────────────────────────────────────────────────────────────────────────\n\
160 # Auto-generated by `ling convert` — {kind}\n\
161 # source: {src}\n\
162 # Lossless: bulk data is deflate+base64 behind blob_f32/blob_i32 (or plain\n\
163 # arrays with --no-compression). Import this file and call its draw/play fn.\n\
164 # ───────────────────────────────────────────────────────────────────────────\n\n"
165 )
166}
167
168fn conv_gltf(input: &str, name: &str, compress: bool) -> Result<String, String> {
171 let model = ling_physics::gltf::GltfModel::load(input)?;
172 let mut s = header("glTF model (geometry + nodes)", input);
173
174 s.push_str("# ── nodes (name, mesh index, world-ish transform rows) ──\n");
176 for (i, n) in model.nodes.iter().enumerate() {
177 let m = n.transform.to_cols_array();
178 s.push_str(&format!(
179 "# node[{i}] \"{}\" mesh={:?} T=[{:.3},{:.3},{:.3},{:.3} / {:.3},{:.3},{:.3},{:.3} / {:.3},{:.3},{:.3},{:.3} / {:.3},{:.3},{:.3},{:.3}]\n",
180 n.name, n.mesh_idx,
181 m[0],m[1],m[2],m[3], m[4],m[5],m[6],m[7], m[8],m[9],m[10],m[11], m[12],m[13],m[14],m[15],
182 ));
183 }
184 s.push('\n');
185
186 let mut draw_calls = Vec::new();
188 for (mi, mesh) in model.meshes.iter().enumerate() {
189 let raw_name = if mesh.name.is_empty() {
190 format!("mesh{mi}")
191 } else {
192 mesh.name.clone()
193 };
194 let mname = sanitize(&raw_name);
195 let mut pos = Vec::with_capacity(mesh.verts.len() * 3);
196 let mut nrm = Vec::with_capacity(mesh.verts.len() * 3);
197 let mut uv = Vec::with_capacity(mesh.verts.len() * 2);
198 for v in &mesh.verts {
199 pos.extend_from_slice(&[v.pos.x, v.pos.y, v.pos.z]);
200 nrm.extend_from_slice(&[v.normal.x, v.normal.y, v.normal.z]);
201 uv.extend_from_slice(&[v.uv.x, v.uv.y]);
202 }
203 s.push_str(&format!(
204 "# mesh \"{}\" — {} verts, {} tris, material={:?}\n",
205 mesh.name,
206 mesh.verts.len(),
207 mesh.indices.len() / 3,
208 mesh.mat_idx
209 ));
210 s.push_str(&emit_f32(&format!("{name}_{mname}_pos"), &pos, compress));
211 s.push_str(&emit_f32(&format!("{name}_{mname}_nrm"), &nrm, compress));
212 s.push_str(&emit_f32(&format!("{name}_{mname}_uv"), &uv, compress));
213 s.push_str(&emit_i32(
214 &format!("{name}_{mname}_idx"),
215 &mesh.indices,
216 compress,
217 ));
218 s.push_str(&format!(
220 "\nฟังก์ชัน draw_{name}_{mname}(ox, oy, oz, scale) {{\n\
221 \x20 bind P = {name}_{mname}_pos bind I = {name}_{mname}_idx\n\
222 \x20 bind n = len(I)\n\
223 \x20 bind k = 0\n\
224 \x20 while k + 2 < n + 1 {{\n\
225 \x20 bind a = list_get(I, k) * 3 bind b = list_get(I, k+1) * 3 bind c = list_get(I, k+2) * 3\n\
226 \x20 bind ax = ox + list_get(P,a)*scale bind ay = oy + list_get(P,a+1)*scale bind az = oz + list_get(P,a+2)*scale\n\
227 \x20 bind bx = ox + list_get(P,b)*scale bind by = oy + list_get(P,b+1)*scale bind bz = oz + list_get(P,b+2)*scale\n\
228 \x20 bind cx = ox + list_get(P,c)*scale bind cy = oy + list_get(P,c+1)*scale bind cz = oz + list_get(P,c+2)*scale\n\
229 \x20 draw_line_3d(ax,ay,az, bx,by,bz) draw_line_3d(bx,by,bz, cx,cy,cz) draw_line_3d(cx,cy,cz, ax,ay,az)\n\
230 \x20 bind k = k + 3\n\
231 \x20 }}\n\
232 }}\n\n"
233 ));
234 draw_calls.push(format!("draw_{name}_{mname}(ox, oy, oz, scale)"));
235 }
236
237 s.push_str(&format!("ฟังก์ชัน draw_{name}(ox, oy, oz, scale) {{\n"));
239 for c in &draw_calls {
240 s.push_str(&format!(" {c}\n"));
241 }
242 s.push_str("}\n\n");
243 s.push_str(&format!(
244 "# Example:\n# ใช้ \"{name}.ling\"\n# … inside your loop: draw_{name}(0,0,0, 1.0) flush_3d()\n"
245 ));
246 Ok(s)
247}
248
249#[cfg(not(target_arch = "wasm32"))]
252fn conv_audio(input: &str, name: &str, compress: bool) -> Result<String, String> {
253 let a = ling_music::decode::load(input)?;
254 let mut s = header("audio (PCM samples)", input);
255 s.push_str(&format!(
256 "# rate={} Hz, channels={}, duration={:.3}s, mono samples={}\n",
257 a.rate,
258 a.channels,
259 a.duration,
260 a.mono.len()
261 ));
262 s.push_str(&format!("bind {name}_rate = {}.0\n", a.rate));
263 s.push_str(&format!("bind {name}_dur = {}\n", fmt_f32(a.duration)));
264 s.push_str(&emit_f32(&format!("{name}_pcm"), &a.mono, compress));
266 s.push_str(&format!(
267 "\n# {name}_pcm holds the lossless mono PCM at {name}_rate.\n\
268 # Feed it to your audio path (e.g. a sample-playback builtin) or analyse it.\n"
269 ));
270 Ok(s)
271}
272
273#[cfg(target_arch = "wasm32")]
274fn conv_audio(_: &str, _: &str, _: bool) -> Result<String, String> {
275 Err("audio conversion is unavailable on wasm".into())
276}
277
278#[cfg(not(target_arch = "wasm32"))]
281fn conv_midi(input: &str, name: &str, compress: bool) -> Result<String, String> {
282 let song = ling_music::midi::load(input)?;
283 let mut s = header("MIDI song (note events)", input);
284 s.push_str(&format!(
285 "# {} notes, duration {:.3}s\n",
286 song.notes.len(),
287 song.duration
288 ));
289 s.push_str(&format!("bind {name}_dur = {}\n", fmt_f32(song.duration)));
290 let mut flat = Vec::with_capacity(song.notes.len() * 5);
292 for n in &song.notes {
293 flat.push(n.time);
294 flat.push(n.dur);
295 flat.push(n.midi as f32);
296 flat.push(n.vel as f32);
297 flat.push(n.channel as f32);
298 }
299 s.push_str(&emit_f32(&format!("{name}_notes"), &flat, compress));
300 s.push_str(&format!(
301 "\n# {name}_notes is flat [time,dur,midi,vel,channel] × {} — step it against a\n\
302 # clock and trigger tones (e.g. music_note / audio_tone) per event.\n",
303 song.notes.len()
304 ));
305 Ok(s)
306}
307
308#[cfg(target_arch = "wasm32")]
309fn conv_midi(_: &str, _: &str, _: bool) -> Result<String, String> {
310 Err("MIDI conversion is unavailable on wasm".into())
311}
312
313fn conv_svg(input: &str, name: &str, compress: bool) -> Result<String, String> {
316 let xml = std::fs::read_to_string(input).map_err(|e| format!("{input}: {e}"))?;
317 let mut polylines: Vec<Vec<[f32; 2]>> = Vec::new();
318 for d in attr_values(&xml, "path", "d") {
319 polylines.extend(svg_path_to_polylines(&d));
320 }
321 for r in elements(&xml, "line") {
323 if let (Some(x1), Some(y1), Some(x2), Some(y2)) =
324 (num(&r, "x1"), num(&r, "y1"), num(&r, "x2"), num(&r, "y2"))
325 {
326 polylines.push(vec![[x1, y1], [x2, y2]]);
327 }
328 }
329 for r in elements(&xml, "rect") {
330 if let (Some(x), Some(y), Some(w), Some(h)) = (
331 num(&r, "x"),
332 num(&r, "y"),
333 num(&r, "width"),
334 num(&r, "height"),
335 ) {
336 polylines.push(vec![[x, y], [x + w, y], [x + w, y + h], [x, y + h], [x, y]]);
337 }
338 }
339 for r in elements(&xml, "polyline")
340 .into_iter()
341 .chain(elements(&xml, "polygon"))
342 {
343 if let Some(pts) = attr(&r, "points") {
344 let nums: Vec<f32> = pts
345 .split(|c: char| c == ',' || c.is_whitespace())
346 .filter_map(|t| t.trim().parse().ok())
347 .collect();
348 let pl: Vec<[f32; 2]> = nums.chunks_exact(2).map(|c| [c[0], c[1]]).collect();
349 if pl.len() >= 2 {
350 polylines.push(pl);
351 }
352 }
353 }
354 if polylines.is_empty() {
355 return Err("no <path>/line/rect/poly geometry found in SVG".into());
356 }
357 let mut coords: Vec<f32> = Vec::new();
359 let mut lens: Vec<u32> = Vec::new();
360 for pl in &polylines {
361 lens.push(pl.len() as u32);
362 for p in pl {
363 coords.push(p[0]);
364 coords.push(p[1]);
365 }
366 }
367 let mut s = header("SVG vector art (polylines)", input);
368 s.push_str(&format!(
369 "# {} polylines, {} points\n",
370 polylines.len(),
371 coords.len() / 2
372 ));
373 s.push_str(&emit_f32(&format!("{name}_xy"), &coords, compress));
374 s.push_str(&emit_i32(&format!("{name}_lens"), &lens, compress));
375 s.push_str(&format!(
376 "\nฟังก์ชัน draw_{name}(ox, oy, scale) {{\n\
377 \x20 bind L = {name}_lens bind P = {name}_xy\n\
378 \x20 bind li = 0 bind base = 0\n\
379 \x20 while li < len(L) {{\n\
380 \x20 bind cnt = list_get(L, li) bind j = 0\n\
381 \x20 while j + 1 < cnt {{\n\
382 \x20 bind a = (base + j) * 2 bind b = (base + j + 1) * 2\n\
383 \x20 draw_line(ox + list_get(P,a)*scale, oy + list_get(P,a+1)*scale, ox + list_get(P,b)*scale, oy + list_get(P,b+1)*scale)\n\
384 \x20 bind j = j + 1\n\
385 \x20 }}\n\
386 \x20 bind base = base + cnt bind li = li + 1\n\
387 \x20 }}\n\
388 }}\n"
389 ));
390 Ok(s)
391}
392
393fn conv_blend(input: &str, output: &str, compress: bool) -> Result<String, String> {
396 let tmp = std::env::temp_dir().join("ling_blend_export.glb");
398 let tmp_s = tmp.to_string_lossy().to_string();
399 let script = format!(
400 "import bpy; bpy.ops.export_scene.gltf(filepath=r'{}', export_format='GLB')",
401 tmp_s
402 );
403 let blender = which_blender();
404 match blender {
405 Some(bin) => {
406 let status = std::process::Command::new(&bin)
407 .args(["-b", input, "--python-expr", &script])
408 .status()
409 .map_err(|e| format!("failed to run Blender ({bin}): {e}"))?;
410 if !status.success() || !tmp.exists() {
411 return Err("Blender ran but produced no glTF export".into());
412 }
413 let name = sanitize(
414 &Path::new(input)
415 .file_stem()
416 .map(|s| s.to_string_lossy().into_owned())
417 .unwrap_or_else(|| "asset".into()),
418 );
419 let s = conv_gltf(&tmp_s, &name, compress)?;
420 let _ = std::fs::remove_file(&tmp);
421 let _ = output;
422 Ok(s)
423 },
424 None => Err(
425 ".blend needs Blender on PATH (set $BLENDER or install it). \
426 Or export the model to .glb/.gltf in Blender and run `ling convert model.glb`."
427 .into(),
428 ),
429 }
430}
431
432fn which_blender() -> Option<String> {
433 if let Ok(b) = std::env::var("BLENDER") {
434 if !b.is_empty() {
435 return Some(b);
436 }
437 }
438 for cand in ["blender", "blender.exe"] {
439 if std::process::Command::new(cand)
440 .arg("--version")
441 .output()
442 .map(|o| o.status.success())
443 .unwrap_or(false)
444 {
445 return Some(cand.to_string());
446 }
447 }
448 None
449}
450
451fn attr_values(xml: &str, tag: &str, attr_name: &str) -> Vec<String> {
455 elements(xml, tag)
456 .iter()
457 .filter_map(|e| attr(e, attr_name))
458 .collect()
459}
460
461fn elements(xml: &str, tag: &str) -> Vec<String> {
463 let needle = format!("<{tag}");
464 let mut out = Vec::new();
465 let mut i = 0;
466 while let Some(p) = xml[i..].find(&needle) {
467 let start = i + p + needle.len();
468 if let Some(end) = xml[start..].find('>') {
469 out.push(xml[start..start + end].to_string());
470 i = start + end;
471 } else {
472 break;
473 }
474 }
475 out
476}
477
478fn attr(el: &str, key: &str) -> Option<String> {
479 let pat = format!("{key}=\"");
480 let p = el.find(&pat)? + pat.len();
481 let end = el[p..].find('"')? + p;
482 Some(el[p..end].to_string())
483}
484
485fn num(el: &str, key: &str) -> Option<f32> {
486 attr(el, key).and_then(|v| {
487 v.trim()
488 .trim_end_matches(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
489 .parse()
490 .ok()
491 })
492}
493
494fn svg_path_to_polylines(d: &str) -> Vec<Vec<[f32; 2]>> {
497 let mut polys: Vec<Vec<[f32; 2]>> = Vec::new();
498 let mut cur: Vec<[f32; 2]> = Vec::new();
499 let (mut x, mut y) = (0.0f32, 0.0f32);
500 let (mut start_x, mut start_y) = (0.0f32, 0.0f32);
501 let toks = tokenize_path(d);
502 let mut i = 0;
503 let mut cmd = ' ';
504 while i < toks.len() {
505 if let Tok::Cmd(c) = toks[i] {
506 cmd = c;
507 i += 1;
508 }
509 let rel = cmd.is_ascii_lowercase();
510 let uc = cmd.to_ascii_uppercase();
511 let next = |i: &mut usize| -> f32 {
512 while *i < toks.len() {
513 if let Tok::Num(n) = toks[*i] {
514 *i += 1;
515 return n;
516 } else {
517 break;
518 }
519 }
520 0.0
521 };
522 match uc {
523 'M' => {
524 if !cur.is_empty() {
525 polys.push(std::mem::take(&mut cur));
526 }
527 let (nx, ny) = (next(&mut i), next(&mut i));
528 x = if rel { x + nx } else { nx };
529 y = if rel { y + ny } else { ny };
530 start_x = x;
531 start_y = y;
532 cur.push([x, y]);
533 cmd = if rel { 'l' } else { 'L' };
534 },
535 'L' => {
536 let (nx, ny) = (next(&mut i), next(&mut i));
537 x = if rel { x + nx } else { nx };
538 y = if rel { y + ny } else { ny };
539 cur.push([x, y]);
540 },
541 'H' => {
542 let nx = next(&mut i);
543 x = if rel { x + nx } else { nx };
544 cur.push([x, y]);
545 },
546 'V' => {
547 let ny = next(&mut i);
548 y = if rel { y + ny } else { ny };
549 cur.push([x, y]);
550 },
551 'C' => {
552 let (x1, y1) = (next(&mut i), next(&mut i));
553 let (x2, y2) = (next(&mut i), next(&mut i));
554 let (ex, ey) = (next(&mut i), next(&mut i));
555 let (p0, p1, p2, p3);
556 if rel {
557 p1 = [x + x1, y + y1];
558 p2 = [x + x2, y + y2];
559 p3 = [x + ex, y + ey];
560 } else {
561 p1 = [x1, y1];
562 p2 = [x2, y2];
563 p3 = [ex, ey];
564 }
565 p0 = [x, y];
566 flatten_cubic(p0, p1, p2, p3, &mut cur);
567 x = p3[0];
568 y = p3[1];
569 },
570 'Q' => {
571 let (x1, y1) = (next(&mut i), next(&mut i));
572 let (ex, ey) = (next(&mut i), next(&mut i));
573 let (p0, p1, p2);
574 if rel {
575 p1 = [x + x1, y + y1];
576 p2 = [x + ex, y + ey];
577 } else {
578 p1 = [x1, y1];
579 p2 = [ex, ey];
580 }
581 p0 = [x, y];
582 flatten_quad(p0, p1, p2, &mut cur);
583 x = p2[0];
584 y = p2[1];
585 },
586 'Z' => {
587 cur.push([start_x, start_y]);
588 x = start_x;
589 y = start_y;
590 if !cur.is_empty() {
591 polys.push(std::mem::take(&mut cur));
592 }
593 },
594 _ => {
595 i += 1;
596 },
597 }
598 }
599 if cur.len() >= 2 {
600 polys.push(cur);
601 }
602 polys
603}
604
605#[derive(Clone, Copy)]
606enum Tok {
607 Cmd(char),
608 Num(f32),
609}
610
611fn tokenize_path(d: &str) -> Vec<Tok> {
612 let mut out = Vec::new();
613 let mut numbuf = String::new();
614 let flush = |b: &mut String, o: &mut Vec<Tok>| {
615 if !b.is_empty() {
616 if let Ok(n) = b.parse::<f32>() {
617 o.push(Tok::Num(n));
618 }
619 b.clear();
620 }
621 };
622 for c in d.chars() {
623 if c.is_ascii_alphabetic() {
624 flush(&mut numbuf, &mut out);
625 out.push(Tok::Cmd(c));
626 } else if c == '-' && !numbuf.is_empty() && !numbuf.ends_with('e') && !numbuf.ends_with('E')
627 {
628 flush(&mut numbuf, &mut out);
629 numbuf.push(c);
630 } else if c == ',' || c.is_whitespace() {
631 flush(&mut numbuf, &mut out);
632 } else {
633 numbuf.push(c);
634 }
635 }
636 flush(&mut numbuf, &mut out);
637 out
638}
639
640fn flatten_cubic(p0: [f32; 2], p1: [f32; 2], p2: [f32; 2], p3: [f32; 2], out: &mut Vec<[f32; 2]>) {
641 let steps = 16;
642 for s in 1..=steps {
643 let t = s as f32 / steps as f32;
644 let u = 1.0 - t;
645 let b = [
646 u * u * u * p0[0]
647 + 3.0 * u * u * t * p1[0]
648 + 3.0 * u * t * t * p2[0]
649 + t * t * t * p3[0],
650 u * u * u * p0[1]
651 + 3.0 * u * u * t * p1[1]
652 + 3.0 * u * t * t * p2[1]
653 + t * t * t * p3[1],
654 ];
655 out.push(b);
656 }
657}
658
659fn flatten_quad(p0: [f32; 2], p1: [f32; 2], p2: [f32; 2], out: &mut Vec<[f32; 2]>) {
660 let steps = 12;
661 for s in 1..=steps {
662 let t = s as f32 / steps as f32;
663 let u = 1.0 - t;
664 out.push([
665 u * u * p0[0] + 2.0 * u * t * p1[0] + t * t * p2[0],
666 u * u * p0[1] + 2.0 * u * t * p1[1] + t * t * p2[1],
667 ]);
668 }
669}