Skip to main content

ling/convert/
mod.rs

1//! `ling convert <asset> [-o out.ling] [--no-compression]`
2//!
3//! Detects an asset's type by extension and transcodes it into a **self-contained,
4//! importable `.ling` file** that reconstructs the asset with the engine's own
5//! builtins — preserving geometry, names, and structure. Bulk numeric data is
6//! embedded **losslessly**: by default deflate-compressed + base64 behind the
7//! `blob_f32` / `blob_i32` builtins; `--no-compression` emits plain `.ling` arrays.
8//!
9//! Supported: `.gltf`/`.glb` (meshes + node names/transforms), `.wav`/`.ogg`/`.flac`
10//! (PCM), `.mid` (note events), `.svg` (paths/primitives → vector strokes).
11//! `.blend` is detected and routed through Blender's glTF exporter when available.
12
13use std::io::Write;
14use std::path::Path;
15
16use base64::Engine as _;
17
18// ── public entry ────────────────────────────────────────────────────────────
19
20pub fn run(args: &[String]) -> i32 {
21    // args = ["convert", <input>, ...flags]
22    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
67/// Dispatch on extension. Returns the number of bytes written.
68pub 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
93// ── shared emitters ───────────────────────────────────────────────────────────
94
95fn 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
106/// Emit `bind <name> = …` for a float array — compressed blob or plain list.
107fn 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
168// ── glTF / GLB → mesh data + node hierarchy ───────────────────────────────────
169
170fn 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    // node hierarchy + names + transforms, preserved as data
175    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    // per-mesh: positions (3/vert), normals (3/vert), uvs (2/vert), indices, + a draw fn
187    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        // wireframe draw: each triangle's 3 edges via draw_line_3d
219        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    // a convenience that draws the whole model
238    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// ── audio (wav/ogg/flac/mp3) → PCM + a play function ──────────────────────────
250
251#[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    // mono downmix is enough to reconstruct/play losslessly at the source rate
265    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// ── MIDI → note events ────────────────────────────────────────────────────────
279
280#[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    // flat note list: [time, dur, midi, vel, channel] per note
291    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
313// ── SVG → vector strokes (paths + basic primitives) ──────────────────────────
314
315fn 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    // primitives → polylines
322    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    // flatten to a single coordinate stream + per-polyline lengths
358    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
393// ── .blend → route through Blender's glTF exporter when available ─────────────
394
395fn conv_blend(input: &str, output: &str, compress: bool) -> Result<String, String> {
396    // Try a headless Blender export to glTF, then convert that.
397    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
451// ── tiny SVG helpers ──────────────────────────────────────────────────────────
452
453/// Collect the value of `attr` from every `<tag …>` element.
454fn 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
461/// Return the raw attribute-region string of each `<tag …>` occurrence.
462fn 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
494/// Minimal SVG path → polylines. Handles M/L/H/V/Z (abs+rel) and flattens
495/// C/Q curves coarsely; good enough to preserve the shape of typical vector art.
496fn 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}