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, out, bytes / 1024,
39 if compress { "deflate+base64 lossless" } else { "uncompressed" }
40 );
41 0
42 }
43 Err(e) => {
44 eprintln!("[convert] error: {e}");
45 1
46 }
47 }
48}
49
50fn default_out(input: &str) -> String {
51 let p = Path::new(input);
52 p.with_extension("ling").to_string_lossy().into_owned()
53}
54
55fn flag_value(args: &[String], flag: &str) -> Option<String> {
56 args.iter().position(|a| a == flag).and_then(|i| args.get(i + 1).cloned())
57}
58
59pub fn convert(input: &str, output: &str, compress: bool) -> Result<usize, String> {
61 let ext = Path::new(input)
62 .extension()
63 .map(|e| e.to_string_lossy().to_lowercase())
64 .unwrap_or_default();
65 let stem = Path::new(input)
66 .file_stem()
67 .map(|s| s.to_string_lossy().into_owned())
68 .unwrap_or_else(|| "asset".into());
69 let name = sanitize(&stem);
70
71 let ling = match ext.as_str() {
72 "gltf" | "glb" => conv_gltf(input, &name, compress)?,
73 "wav" | "ogg" | "flac" | "mp3" => conv_audio(input, &name, compress)?,
74 "mid" | "midi" => conv_midi(input, &name, compress)?,
75 "svg" => conv_svg(input, &name, compress)?,
76 "blend" => conv_blend(input, output, compress)?,
77 other => return Err(format!("unsupported extension '.{other}'")),
78 };
79
80 let mut f = std::fs::File::create(output).map_err(|e| format!("{output}: {e}"))?;
81 f.write_all(ling.as_bytes()).map_err(|e| e.to_string())?;
82 Ok(ling.len())
83}
84
85fn deflate(bytes: &[u8]) -> Vec<u8> {
88 use flate2::{write::ZlibEncoder, Compression};
89 let mut e = ZlibEncoder::new(Vec::new(), Compression::best());
90 let _ = e.write_all(bytes);
91 e.finish().unwrap_or_default()
92}
93
94fn b64(bytes: &[u8]) -> String {
95 base64::engine::general_purpose::STANDARD.encode(bytes)
96}
97
98fn emit_f32(name: &str, data: &[f32], compress: bool) -> String {
100 if compress && data.len() > 8 {
101 let mut bytes = Vec::with_capacity(data.len() * 4);
102 for v in data { bytes.extend_from_slice(&v.to_le_bytes()); }
103 format!("bind {name} = blob_f32(\"{}\")\n", b64(&deflate(&bytes)))
104 } else {
105 let body: Vec<String> = data.iter().map(|v| fmt_f32(*v)).collect();
106 format!("bind {name} = [{}]\n", body.join(", "))
107 }
108}
109
110fn emit_i32(name: &str, data: &[u32], compress: bool) -> String {
111 if compress && data.len() > 8 {
112 let mut bytes = Vec::with_capacity(data.len() * 4);
113 for v in data { bytes.extend_from_slice(&(*v as i32).to_le_bytes()); }
114 format!("bind {name} = blob_i32(\"{}\")\n", b64(&deflate(&bytes)))
115 } else {
116 let body: Vec<String> = data.iter().map(|v| v.to_string()).collect();
117 format!("bind {name} = [{}]\n", body.join(", "))
118 }
119}
120
121fn fmt_f32(v: f32) -> String {
122 if v == v.trunc() && v.abs() < 1e7 { format!("{:.1}", v) } else { format!("{}", v) }
123}
124
125fn sanitize(s: &str) -> String {
126 let mut out: String = s.chars().map(|c| if c.is_alphanumeric() { c } else { '_' }).collect();
127 if out.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(true) { out.insert(0, '_'); }
128 out
129}
130
131fn header(kind: &str, src: &str) -> String {
132 format!(
133 "# ───────────────────────────────────────────────────────────────────────────\n\
134 # Auto-generated by `ling convert` — {kind}\n\
135 # source: {src}\n\
136 # Lossless: bulk data is deflate+base64 behind blob_f32/blob_i32 (or plain\n\
137 # arrays with --no-compression). Import this file and call its draw/play fn.\n\
138 # ───────────────────────────────────────────────────────────────────────────\n\n"
139 )
140}
141
142fn conv_gltf(input: &str, name: &str, compress: bool) -> Result<String, String> {
145 let model = ling_physics::gltf::GltfModel::load(input)?;
146 let mut s = header("glTF model (geometry + nodes)", input);
147
148 s.push_str("# ── nodes (name, mesh index, world-ish transform rows) ──\n");
150 for (i, n) in model.nodes.iter().enumerate() {
151 let m = n.transform.to_cols_array();
152 s.push_str(&format!(
153 "# node[{i}] \"{}\" mesh={:?} T=[{:.3},{:.3},{:.3},{:.3} / {:.3},{:.3},{:.3},{:.3} / {:.3},{:.3},{:.3},{:.3} / {:.3},{:.3},{:.3},{:.3}]\n",
154 n.name, n.mesh_idx,
155 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],
156 ));
157 }
158 s.push('\n');
159
160 let mut draw_calls = Vec::new();
162 for (mi, mesh) in model.meshes.iter().enumerate() {
163 let raw_name = if mesh.name.is_empty() { format!("mesh{mi}") } else { mesh.name.clone() };
164 let mname = sanitize(&raw_name);
165 let mut pos = Vec::with_capacity(mesh.verts.len() * 3);
166 let mut nrm = Vec::with_capacity(mesh.verts.len() * 3);
167 let mut uv = Vec::with_capacity(mesh.verts.len() * 2);
168 for v in &mesh.verts {
169 pos.extend_from_slice(&[v.pos.x, v.pos.y, v.pos.z]);
170 nrm.extend_from_slice(&[v.normal.x, v.normal.y, v.normal.z]);
171 uv.extend_from_slice(&[v.uv.x, v.uv.y]);
172 }
173 s.push_str(&format!("# mesh \"{}\" — {} verts, {} tris, material={:?}\n",
174 mesh.name, mesh.verts.len(), mesh.indices.len() / 3, mesh.mat_idx));
175 s.push_str(&emit_f32(&format!("{name}_{mname}_pos"), &pos, compress));
176 s.push_str(&emit_f32(&format!("{name}_{mname}_nrm"), &nrm, compress));
177 s.push_str(&emit_f32(&format!("{name}_{mname}_uv"), &uv, compress));
178 s.push_str(&emit_i32(&format!("{name}_{mname}_idx"), &mesh.indices, compress));
179 s.push_str(&format!(
181 "\nฟังก์ชัน draw_{name}_{mname}(ox, oy, oz, scale) {{\n\
182 \x20 bind P = {name}_{mname}_pos bind I = {name}_{mname}_idx\n\
183 \x20 bind n = len(I)\n\
184 \x20 bind k = 0\n\
185 \x20 while k + 2 < n + 1 {{\n\
186 \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\
187 \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\
188 \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\
189 \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\
190 \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\
191 \x20 bind k = k + 3\n\
192 \x20 }}\n\
193 }}\n\n"
194 ));
195 draw_calls.push(format!("draw_{name}_{mname}(ox, oy, oz, scale)"));
196 }
197
198 s.push_str(&format!("ฟังก์ชัน draw_{name}(ox, oy, oz, scale) {{\n"));
200 for c in &draw_calls { s.push_str(&format!(" {c}\n")); }
201 s.push_str("}\n\n");
202 s.push_str(&format!(
203 "# Example:\n# ใช้ \"{name}.ling\"\n# … inside your loop: draw_{name}(0,0,0, 1.0) flush_3d()\n"
204 ));
205 Ok(s)
206}
207
208#[cfg(not(target_arch = "wasm32"))]
211fn conv_audio(input: &str, name: &str, compress: bool) -> Result<String, String> {
212 let a = ling_music::decode::load(input)?;
213 let mut s = header("audio (PCM samples)", input);
214 s.push_str(&format!(
215 "# rate={} Hz, channels={}, duration={:.3}s, mono samples={}\n",
216 a.rate, a.channels, a.duration, a.mono.len()
217 ));
218 s.push_str(&format!("bind {name}_rate = {}.0\n", a.rate));
219 s.push_str(&format!("bind {name}_dur = {}\n", fmt_f32(a.duration)));
220 s.push_str(&emit_f32(&format!("{name}_pcm"), &a.mono, compress));
222 s.push_str(&format!(
223 "\n# {name}_pcm holds the lossless mono PCM at {name}_rate.\n\
224 # Feed it to your audio path (e.g. a sample-playback builtin) or analyse it.\n"
225 ));
226 Ok(s)
227}
228
229#[cfg(target_arch = "wasm32")]
230fn conv_audio(_: &str, _: &str, _: bool) -> Result<String, String> {
231 Err("audio conversion is unavailable on wasm".into())
232}
233
234#[cfg(not(target_arch = "wasm32"))]
237fn conv_midi(input: &str, name: &str, compress: bool) -> Result<String, String> {
238 let song = ling_music::midi::load(input)?;
239 let mut s = header("MIDI song (note events)", input);
240 s.push_str(&format!("# {} notes, duration {:.3}s\n", song.notes.len(), song.duration));
241 s.push_str(&format!("bind {name}_dur = {}\n", fmt_f32(song.duration)));
242 let mut flat = Vec::with_capacity(song.notes.len() * 5);
244 for n in &song.notes {
245 flat.push(n.time); flat.push(n.dur);
246 flat.push(n.midi as f32); flat.push(n.vel as f32); flat.push(n.channel as f32);
247 }
248 s.push_str(&emit_f32(&format!("{name}_notes"), &flat, compress));
249 s.push_str(&format!(
250 "\n# {name}_notes is flat [time,dur,midi,vel,channel] × {} — step it against a\n\
251 # clock and trigger tones (e.g. music_note / audio_tone) per event.\n",
252 song.notes.len()
253 ));
254 Ok(s)
255}
256
257#[cfg(target_arch = "wasm32")]
258fn conv_midi(_: &str, _: &str, _: bool) -> Result<String, String> {
259 Err("MIDI conversion is unavailable on wasm".into())
260}
261
262fn conv_svg(input: &str, name: &str, compress: bool) -> Result<String, String> {
265 let xml = std::fs::read_to_string(input).map_err(|e| format!("{input}: {e}"))?;
266 let mut polylines: Vec<Vec<[f32; 2]>> = Vec::new();
267 for d in attr_values(&xml, "path", "d") {
268 polylines.extend(svg_path_to_polylines(&d));
269 }
270 for r in elements(&xml, "line") {
272 if let (Some(x1), Some(y1), Some(x2), Some(y2)) =
273 (num(&r, "x1"), num(&r, "y1"), num(&r, "x2"), num(&r, "y2")) {
274 polylines.push(vec![[x1, y1], [x2, y2]]);
275 }
276 }
277 for r in elements(&xml, "rect") {
278 if let (Some(x), Some(y), Some(w), Some(h)) =
279 (num(&r, "x"), num(&r, "y"), num(&r, "width"), num(&r, "height")) {
280 polylines.push(vec![[x, y], [x + w, y], [x + w, y + h], [x, y + h], [x, y]]);
281 }
282 }
283 for r in elements(&xml, "polyline").into_iter().chain(elements(&xml, "polygon")) {
284 if let Some(pts) = attr(&r, "points") {
285 let nums: Vec<f32> = pts.split(|c: char| c == ',' || c.is_whitespace())
286 .filter_map(|t| t.trim().parse().ok()).collect();
287 let pl: Vec<[f32; 2]> = nums.chunks_exact(2).map(|c| [c[0], c[1]]).collect();
288 if pl.len() >= 2 { polylines.push(pl); }
289 }
290 }
291 if polylines.is_empty() {
292 return Err("no <path>/line/rect/poly geometry found in SVG".into());
293 }
294 let mut coords: Vec<f32> = Vec::new();
296 let mut lens: Vec<u32> = Vec::new();
297 for pl in &polylines {
298 lens.push(pl.len() as u32);
299 for p in pl { coords.push(p[0]); coords.push(p[1]); }
300 }
301 let mut s = header("SVG vector art (polylines)", input);
302 s.push_str(&format!("# {} polylines, {} points\n", polylines.len(), coords.len() / 2));
303 s.push_str(&emit_f32(&format!("{name}_xy"), &coords, compress));
304 s.push_str(&emit_i32(&format!("{name}_lens"), &lens, compress));
305 s.push_str(&format!(
306 "\nฟังก์ชัน draw_{name}(ox, oy, scale) {{\n\
307 \x20 bind L = {name}_lens bind P = {name}_xy\n\
308 \x20 bind li = 0 bind base = 0\n\
309 \x20 while li < len(L) {{\n\
310 \x20 bind cnt = list_get(L, li) bind j = 0\n\
311 \x20 while j + 1 < cnt {{\n\
312 \x20 bind a = (base + j) * 2 bind b = (base + j + 1) * 2\n\
313 \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\
314 \x20 bind j = j + 1\n\
315 \x20 }}\n\
316 \x20 bind base = base + cnt bind li = li + 1\n\
317 \x20 }}\n\
318 }}\n"
319 ));
320 Ok(s)
321}
322
323fn conv_blend(input: &str, output: &str, compress: bool) -> Result<String, String> {
326 let tmp = std::env::temp_dir().join("ling_blend_export.glb");
328 let tmp_s = tmp.to_string_lossy().to_string();
329 let script = format!(
330 "import bpy; bpy.ops.export_scene.gltf(filepath=r'{}', export_format='GLB')",
331 tmp_s
332 );
333 let blender = which_blender();
334 match blender {
335 Some(bin) => {
336 let status = std::process::Command::new(&bin)
337 .args(["-b", input, "--python-expr", &script])
338 .status()
339 .map_err(|e| format!("failed to run Blender ({bin}): {e}"))?;
340 if !status.success() || !tmp.exists() {
341 return Err("Blender ran but produced no glTF export".into());
342 }
343 let name = sanitize(
344 &Path::new(input).file_stem().map(|s| s.to_string_lossy().into_owned())
345 .unwrap_or_else(|| "asset".into()),
346 );
347 let s = conv_gltf(&tmp_s, &name, compress)?;
348 let _ = std::fs::remove_file(&tmp);
349 let _ = output;
350 Ok(s)
351 }
352 None => Err(
353 ".blend needs Blender on PATH (set $BLENDER or install it). \
354 Or export the model to .glb/.gltf in Blender and run `ling convert model.glb`."
355 .into(),
356 ),
357 }
358}
359
360fn which_blender() -> Option<String> {
361 if let Ok(b) = std::env::var("BLENDER") {
362 if !b.is_empty() { return Some(b); }
363 }
364 for cand in ["blender", "blender.exe"] {
365 if std::process::Command::new(cand).arg("--version").output().map(|o| o.status.success()).unwrap_or(false) {
366 return Some(cand.to_string());
367 }
368 }
369 None
370}
371
372fn attr_values(xml: &str, tag: &str, attr_name: &str) -> Vec<String> {
376 elements(xml, tag).iter().filter_map(|e| attr(e, attr_name)).collect()
377}
378
379fn elements(xml: &str, tag: &str) -> Vec<String> {
381 let needle = format!("<{tag}");
382 let mut out = Vec::new();
383 let mut i = 0;
384 while let Some(p) = xml[i..].find(&needle) {
385 let start = i + p + needle.len();
386 if let Some(end) = xml[start..].find('>') {
387 out.push(xml[start..start + end].to_string());
388 i = start + end;
389 } else { break; }
390 }
391 out
392}
393
394fn attr(el: &str, key: &str) -> Option<String> {
395 let pat = format!("{key}=\"");
396 let p = el.find(&pat)? + pat.len();
397 let end = el[p..].find('"')? + p;
398 Some(el[p..end].to_string())
399}
400
401fn num(el: &str, key: &str) -> Option<f32> {
402 attr(el, key).and_then(|v| v.trim().trim_end_matches(|c: char| !c.is_ascii_digit() && c != '.' && c != '-').parse().ok())
403}
404
405fn svg_path_to_polylines(d: &str) -> Vec<Vec<[f32; 2]>> {
408 let mut polys: Vec<Vec<[f32; 2]>> = Vec::new();
409 let mut cur: Vec<[f32; 2]> = Vec::new();
410 let (mut x, mut y) = (0.0f32, 0.0f32);
411 let (mut start_x, mut start_y) = (0.0f32, 0.0f32);
412 let mut toks = tokenize_path(d);
413 let mut i = 0;
414 let mut cmd = ' ';
415 while i < toks.len() {
416 if let Tok::Cmd(c) = toks[i] { cmd = c; i += 1; }
417 let rel = cmd.is_ascii_lowercase();
418 let uc = cmd.to_ascii_uppercase();
419 let mut next = |i: &mut usize| -> f32 {
420 while *i < toks.len() { if let Tok::Num(n) = toks[*i] { *i += 1; return n; } else { break; } }
421 0.0
422 };
423 match uc {
424 'M' => {
425 if !cur.is_empty() { polys.push(std::mem::take(&mut cur)); }
426 let (nx, ny) = (next(&mut i), next(&mut i));
427 x = if rel { x + nx } else { nx }; y = if rel { y + ny } else { ny };
428 start_x = x; start_y = y; cur.push([x, y]); cmd = if rel { 'l' } else { 'L' };
429 }
430 'L' => { let (nx, ny) = (next(&mut i), next(&mut i));
431 x = if rel { x + nx } else { nx }; y = if rel { y + ny } else { ny }; cur.push([x, y]); }
432 'H' => { let nx = next(&mut i); x = if rel { x + nx } else { nx }; cur.push([x, y]); }
433 'V' => { let ny = next(&mut i); y = if rel { y + ny } else { ny }; cur.push([x, y]); }
434 'C' => {
435 let (x1, y1) = (next(&mut i), next(&mut i));
436 let (x2, y2) = (next(&mut i), next(&mut i));
437 let (ex, ey) = (next(&mut i), next(&mut i));
438 let (p0, p1, p2, p3);
439 if rel { p1 = [x + x1, y + y1]; p2 = [x + x2, y + y2]; p3 = [x + ex, y + ey]; }
440 else { p1 = [x1, y1]; p2 = [x2, y2]; p3 = [ex, ey]; }
441 p0 = [x, y];
442 flatten_cubic(p0, p1, p2, p3, &mut cur);
443 x = p3[0]; y = p3[1];
444 }
445 'Q' => {
446 let (x1, y1) = (next(&mut i), next(&mut i));
447 let (ex, ey) = (next(&mut i), next(&mut i));
448 let (p0, p1, p2);
449 if rel { p1 = [x + x1, y + y1]; p2 = [x + ex, y + ey]; }
450 else { p1 = [x1, y1]; p2 = [ex, ey]; }
451 p0 = [x, y];
452 flatten_quad(p0, p1, p2, &mut cur);
453 x = p2[0]; y = p2[1];
454 }
455 'Z' => { cur.push([start_x, start_y]); x = start_x; y = start_y;
456 if !cur.is_empty() { polys.push(std::mem::take(&mut cur)); } }
457 _ => { i += 1; }
458 }
459 }
460 if cur.len() >= 2 { polys.push(cur); }
461 polys
462}
463
464#[derive(Clone, Copy)]
465enum Tok { Cmd(char), Num(f32) }
466
467fn tokenize_path(d: &str) -> Vec<Tok> {
468 let mut out = Vec::new();
469 let mut numbuf = String::new();
470 let flush = |b: &mut String, o: &mut Vec<Tok>| {
471 if !b.is_empty() { if let Ok(n) = b.parse::<f32>() { o.push(Tok::Num(n)); } b.clear(); }
472 };
473 for c in d.chars() {
474 if c.is_ascii_alphabetic() {
475 flush(&mut numbuf, &mut out);
476 out.push(Tok::Cmd(c));
477 } else if c == '-' && !numbuf.is_empty() && !numbuf.ends_with('e') && !numbuf.ends_with('E') {
478 flush(&mut numbuf, &mut out);
479 numbuf.push(c);
480 } else if c == ',' || c.is_whitespace() {
481 flush(&mut numbuf, &mut out);
482 } else {
483 numbuf.push(c);
484 }
485 }
486 flush(&mut numbuf, &mut out);
487 out
488}
489
490fn flatten_cubic(p0: [f32; 2], p1: [f32; 2], p2: [f32; 2], p3: [f32; 2], out: &mut Vec<[f32; 2]>) {
491 let steps = 16;
492 for s in 1..=steps {
493 let t = s as f32 / steps as f32; let u = 1.0 - t;
494 let b = [
495 u*u*u*p0[0] + 3.0*u*u*t*p1[0] + 3.0*u*t*t*p2[0] + t*t*t*p3[0],
496 u*u*u*p0[1] + 3.0*u*u*t*p1[1] + 3.0*u*t*t*p2[1] + t*t*t*p3[1],
497 ];
498 out.push(b);
499 }
500}
501
502fn flatten_quad(p0: [f32; 2], p1: [f32; 2], p2: [f32; 2], out: &mut Vec<[f32; 2]>) {
503 let steps = 12;
504 for s in 1..=steps {
505 let t = s as f32 / steps as f32; let u = 1.0 - t;
506 out.push([u*u*p0[0] + 2.0*u*t*p1[0] + t*t*p2[0], u*u*p0[1] + 2.0*u*t*p1[1] + t*t*p2[1]]);
507 }
508}