//! Fusion 360 Electronics / EAGLE `.sch` schematic parser + renderer.
//!
//! Fusion Electronics is EAGLE underneath, so one parser serves both. Unlike the
//! KiCad path there is no headless renderer to produce the visual base, so we
//! draw the sheet ourselves in EAGLE's own colour convention (green nets, red
//! symbol outlines, dark-red pins on a light sheet).
//!
//! The upside of self-rendering: we KNOW which primitive belongs to which
//! instance, so every element is tagged `data-cref` directly instead of being
//! inferred by bbox containment the way the KiCad path has to. The frontend's
//! halo highlight then works unchanged.
//!
//! Coordinates: EAGLE is mm, Y-up; we emit Y-down, so every Y is negated.

use crate::sch_parse::{Comp, Sheet};
use roxmltree::{Document, Node};
use std::collections::HashMap;
use std::fmt::Write as _;

pub fn looks_like_eagle(text: &str) -> bool {
    let head = &text[..text.len().min(4096)];
    head.contains("<eagle") && text.contains("<schematic")
}

fn at(n: Node, k: &str) -> String { n.attribute(k).unwrap_or("").to_string() }
fn num(n: Node, k: &str) -> f64 { n.attribute(k).and_then(|v| v.parse().ok()).unwrap_or(0.0) }

fn rot_of(s: &str) -> (f64, bool) {
    let mirror = s.contains('M');
    let deg: f64 = s.chars().filter(|c| c.is_ascii_digit() || *c == '.').collect::<String>().parse().unwrap_or(0.0);
    (deg, mirror)
}

/// Place a symbol-local point onto the sheet (and flip Y for SVG).
fn place(px: f64, py: f64, ix: f64, iy: f64, deg: f64, mirror: bool) -> (f64, f64) {
    let x = if mirror { -px } else { px };
    let r = deg.to_radians();
    let (s, c) = (r.sin(), r.cos());
    (ix + x * c - py * s, -(iy + x * s + py * c))
}

// EAGLE layer colours, as the EDA itself draws them
const C_NET: &str = "#1a9648";     // layer 91 Nets — Fusion green
const C_SYM: &str = "#b11313";     // layer 94 Symbols — Fusion red body outline
const C_PIN: &str = "#1a9648";     // layer 93 Pins — green stub
const C_PINEND: &str = "#e01010";  // unconnected pin-end tick — red
const C_NAME: &str = "#141414";    // layer 95 Names — near-black
const C_VALUE: &str = "#7d828b";   // layer 96 Values — gray
const C_TEXT: &str = "#444444";
const C_INFO: &str = "#8a8f98";    // layer 97/98 Info/Guide — EAGLE draws these gray

fn esc(s: &str) -> String {
    s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;")
}

/// EAGLE layer number -> the colour the EDA draws it, so a symbol's own primitives
/// keep their layer identity (94 body dark-red, 93/91 green, 95/96 name/value,
/// 97/98 info gray) instead of being flattened to one colour.
fn layer_color(n: i32) -> Option<&'static str> {
    match n {
        91 => Some(C_NET), 93 => Some(C_PIN), 94 => Some(C_SYM),
        95 => Some(C_NAME), 96 => Some(C_VALUE), 97 | 98 => Some(C_INFO),
        _ => None,
    }
}

/// Emit one EAGLE text primitive FAITHFULLY: honour size, alignment and rotation
/// with EAGLE's "keep readable" rule — text whose angle lands in (90,270] is
/// un-flipped and BOTH alignments mirrored, so it never renders upside down. This
/// is what stops smashed NAME/VALUE labels (often rot=R180) from flowing back over
/// their own symbol. `x,y` are already sheet coords (Y-down).
#[allow(clippy::too_many_arguments)]
fn draw_text(out: &mut String, tag: &str, x: f64, y: f64, text: &str, size: f64,
             color: &str, rot: f64, align: &str, mirror: bool) {
    if text.is_empty() { return; }
    // EAGLE align is "<v>-<h>" (bottom-left is the default), or a bare "center".
    let a = if align.is_empty() { "bottom-left" } else { align };
    let (mut valign, mut halign) = ("bottom", "left");
    if a == "center" { valign = "center"; halign = "center"; }
    else {
        for part in a.split('-') {
            match part { "top" | "center" | "bottom" => valign = part, "left" | "right" => halign = part, _ => {} }
        }
    }
    let mut r = ((rot % 360.0) + 360.0) % 360.0;
    if mirror {
        halign = if halign == "left" { "right" } else if halign == "right" { "left" } else { halign };
        r = (360.0 - r) % 360.0;
    }
    // Readability: 90 < r <= 270 → drop 180deg of glyph rotation, mirror both aligns.
    let mut glyph_rot = r;
    if r > 90.0 && r <= 270.0 {
        glyph_rot = r - 180.0;
        halign = if halign == "left" { "right" } else if halign == "right" { "left" } else { halign };
        valign = if valign == "top" { "bottom" } else if valign == "bottom" { "top" } else { valign };
    }
    let anchor = match halign { "center" => "middle", "right" => "end", _ => "start" };
    // Y is flipped, so EAGLE "bottom" (text sits above the anchor) is the alphabetic
    // baseline; "top" hangs below; "center" is central.
    let baseline = match valign { "center" => "central", "top" => "hanging", _ => "auto" };
    let transform = if glyph_rot.abs() > 0.01 {
        format!(r##" transform="rotate({:.2} {:.3} {:.3})""##, glyph_rot, x, y)
    } else { String::new() };
    let _ = write!(out, r##"<text{tag} x="{:.3}" y="{:.3}" font-size="{:.3}" fill="{}" text-anchor="{}" dominant-baseline="{}"{}>{}</text>"##,
        x, y, size, color, anchor, baseline, transform, esc(text));
}

struct Sym<'a, 'i> { nodes: Vec<Node<'a, 'i>> }

/// Parse + render in one pass. Returns (svg, sheet).
pub fn render_sheet(xml: &str) -> Result<(String, Sheet), String> {
    let opts = roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() };
    let doc = Document::parse_with_options(xml, opts).map_err(|e| format!("eagle .sch: {e}"))?;
    let root = doc.root_element();
    let schem = root.descendants().find(|n| n.has_tag_name("schematic")).ok_or("eagle: no <schematic>")?;

    // symbols, keyed "library:symbol"
    let mut syms: HashMap<String, Sym> = HashMap::new();
    // deviceset -> (gate name -> symbol name), and deviceset+device -> package
    let mut gate_sym: HashMap<String, String> = HashMap::new();
    let mut dev_pkg: HashMap<String, String> = HashMap::new();
    let mut dev_desc: HashMap<String, String> = HashMap::new();
    for lib in schem.descendants().filter(|n| n.has_tag_name("library")) {
        let lname = at(lib, "name");
        for s in lib.descendants().filter(|n| n.has_tag_name("symbol")) {
            syms.insert(format!("{}:{}", lname, at(s, "name")), Sym { nodes: s.children().filter(|c| c.is_element()).collect() });
        }
        for ds in lib.descendants().filter(|n| n.has_tag_name("deviceset")) {
            let dsn = at(ds, "name");
            if let Some(d) = ds.children().find(|c| c.has_tag_name("description")) {
                dev_desc.insert(format!("{}:{}", lname, dsn), d.text().unwrap_or("").trim().to_string());
            }
            for g in ds.descendants().filter(|n| n.has_tag_name("gate")) {
                gate_sym.insert(format!("{}:{}:{}", lname, dsn, at(g, "name")), format!("{}:{}", lname, at(g, "symbol")));
            }
            for d in ds.descendants().filter(|n| n.has_tag_name("device")) {
                dev_pkg.insert(format!("{}:{}:{}", lname, dsn, at(d, "name")), at(d, "package"));
            }
        }
    }

    // parts
    struct Part { library: String, deviceset: String, device: String, value: String, lcsc: String, datasheet: String }
    let mut parts: HashMap<String, Part> = HashMap::new();
    for p in schem.descendants().filter(|n| n.has_tag_name("part")) {
        let mut lcsc = String::new();
        let mut datasheet = String::new();
        for a in p.children().filter(|c| c.has_tag_name("attribute")) {
            let k = at(a, "name").to_uppercase();
            let v = at(a, "value");
            if k.contains("LCSC") || k.contains("JLC") { lcsc = v; }
            else if k.contains("DATASHEET") { datasheet = v; }
        }
        parts.insert(at(p, "name"), Part {
            library: at(p, "library"), deviceset: at(p, "deviceset"), device: at(p, "device"),
            value: at(p, "value"), lcsc, datasheet,
        });
    }

    let mut body = String::new();  // sheet graphics (wires, symbols)
    let mut comps: Vec<Comp> = Vec::new();
    let mut ext: Vec<(f64, f64)> = Vec::new();

    // ── nets: wires, junctions, labels ──────────────────────────────────────
    for net in schem.descendants().filter(|n| n.has_tag_name("net")) {
        let nname = at(net, "name");
        for seg in net.children().filter(|c| c.has_tag_name("segment")) {
            for w in seg.children().filter(|c| c.has_tag_name("wire")) {
                let (x1, y1) = (num(w, "x1"), -num(w, "y1"));
                let (x2, y2) = (num(w, "x2"), -num(w, "y2"));
                ext.push((x1, y1)); ext.push((x2, y2));
                let _ = write!(body, r##"<line class="adom-wire" data-net="{}" x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="{}" stroke-width="{:.3}" stroke-linecap="round"/>"##,
                    esc(&nname), x1, y1, x2, y2, C_NET, num(w, "width").max(0.15));
            }
            for j in seg.children().filter(|c| c.has_tag_name("junction")) {
                let _ = write!(body, r##"<circle cx="{:.3}" cy="{:.3}" r="0.5" fill="{}"/>"##, num(j, "x"), -num(j, "y"), C_NET);
            }
            for l in seg.children().filter(|c| c.has_tag_name("label")) {
                let (x, y) = (num(l, "x"), -num(l, "y"));
                let size = if num(l, "size") > 0.0 { num(l, "size") } else { 1.778 };
                let (deg, _) = rot_of(&at(l, "rot"));
                ext.push((x - 12.0, y - 3.0)); ext.push((x + 12.0, y + 3.0));
                if at(l, "xref").eq_ignore_ascii_case("yes") {
                    draw_xref_flag(&mut body, "", x, y, &nname, size, deg);
                } else {
                    draw_text(&mut body, "", x + 0.4, y, &nname, size, C_NET, deg, "bottom-left", false);
                }
            }
        }
    }

    // ── free graphics on the sheet (frame, notes) ───────────────────────────
    for pl in schem.descendants().filter(|n| n.has_tag_name("plain")) {
        for n in pl.children().filter(|c| c.is_element()) {
            draw_prim(&mut body, n, 0.0, 0.0, 0.0, false, C_TEXT, "", &mut ext, "");
        }
    }

    // ── instances: the actual components ────────────────────────────────────
    for inst in schem.descendants().filter(|n| n.has_tag_name("instance")) {
        let pname = at(inst, "part");
        let (ix, iy) = (num(inst, "x"), num(inst, "y"));
        let (deg, mirror) = rot_of(&at(inst, "rot"));
        let part = match parts.get(&pname) { Some(p) => p, None => continue };
        let skey = format!("{}:{}:{}", part.library, part.deviceset, at(inst, "gate"));
        let sym = gate_sym.get(&skey).and_then(|k| syms.get(k));

        let mut my: Vec<(f64, f64)> = Vec::new();
        let mut pin_count = 0usize;
        let cref = esc(&pname);
        // The symbol's >NAME / >VALUE placeholders (local x, y, size, align, rot) —
        // used to place the label when the instance didn't smash it out explicitly.
        let mut name_ph: Option<(f64, f64, f64, String, f64)> = None;
        let mut value_ph: Option<(f64, f64, f64, String, f64)> = None;
        let ctag = format!(r##" data-cref="{}""##, cref);
        let _ = write!(body, r##"<g class="adom-sym" data-cref="{}">"##, cref);
        if let Some(sym) = sym {
            for n in &sym.nodes {
                let n = *n;
                if n.has_tag_name("pin") {
                    pin_count += 1;
                    let len = match at(n, "length").as_str() { "point" => 0.0, "short" => 2.54, "long" => 7.62, _ => 5.08 };
                    let (pdeg, _) = rot_of(&at(n, "rot"));
                    let (x0, y0) = (num(n, "x"), num(n, "y"));
                    let a = pdeg.to_radians();
                    let (x1, y1) = (x0 + len * a.cos(), y0 + len * a.sin());
                    let (sx, sy) = place(x0, y0, ix, iy, deg, mirror);
                    let (ex2, ey2) = place(x1, y1, ix, iy, deg, mirror);
                    my.push((sx, sy)); my.push((ex2, ey2));
                    let _ = write!(body, r##"<line data-cref="{}" x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="{}" stroke-width="0.15"/>"##,
                        cref, sx, sy, ex2, ey2, C_PIN);
                    // EAGLE draws the pin junction as a small red mark at the connect point.
                    let _ = write!(body, r##"<circle data-cref="{}" cx="{:.3}" cy="{:.3}" r="0.35" fill="none" stroke="{}" stroke-width="0.18"/>"##,
                        cref, sx, sy, C_PINEND);
                    continue;
                }
                if n.has_tag_name("text") {
                    let t = n.text().unwrap_or("").trim().to_string();
                    let ph = (num(n, "x"), num(n, "y"), if num(n, "size") > 0.0 { num(n, "size") } else { 1.4224 }, at(n, "align"), rot_of(&at(n, "rot")).0);
                    if t.eq_ignore_ascii_case(">NAME") { name_ph = Some(ph); continue; }
                    if t.eq_ignore_ascii_case(">VALUE") { value_ph = Some(ph); continue; }
                }
                draw_prim(&mut body, n, ix, iy, deg, mirror, C_SYM, &cref, &mut my, &pname);
            }
        }
        // NAME / VALUE. Priority: a smashed instance <attribute> (ABSOLUTE coords +
        // its own size/align/rot) → the symbol's >NAME/>VALUE placeholder placed by
        // the instance → a sensible default. This is what stops labels flowing back
        // over their own symbol.
        let inst_attr = |want: &str| inst.children().find(|c| c.has_tag_name("attribute") && at(*c, "name").eq_ignore_ascii_case(want));
        let (nx, ny) = if let Some(a) = inst_attr("NAME") {
            let sz = if num(a, "size") > 0.0 { num(a, "size") } else { 1.778 };
            let (x, y) = (num(a, "x"), -num(a, "y"));
            draw_text(&mut body, &ctag, x, y, &pname, sz, C_NAME, rot_of(&at(a, "rot")).0, &at(a, "align"), false);
            (x, y)
        } else if let Some((px, py, sz, al, pr)) = name_ph.clone() {
            let (x, y) = place(px, py, ix, iy, deg, mirror);
            draw_text(&mut body, &ctag, x, y, &pname, sz, C_NAME, deg + pr, &al, mirror);
            (x, y)
        } else {
            let (x, y) = place(0.0, 2.2, ix, iy, deg, mirror);
            draw_text(&mut body, &ctag, x, y, &pname, 1.778, C_NAME, 0.0, "bottom-left", false);
            (x, y)
        };
        let (vx, vy) = if part.value.is_empty() { (nx, ny) } else if let Some(a) = inst_attr("VALUE") {
            let sz = if num(a, "size") > 0.0 { num(a, "size") } else { 1.778 };
            let (x, y) = (num(a, "x"), -num(a, "y"));
            draw_text(&mut body, &ctag, x, y, &part.value, sz, C_VALUE, rot_of(&at(a, "rot")).0, &at(a, "align"), false);
            (x, y)
        } else if let Some((px, py, sz, al, pr)) = value_ph.clone() {
            let (x, y) = place(px, py, ix, iy, deg, mirror);
            draw_text(&mut body, &ctag, x, y, &part.value, sz, C_VALUE, deg + pr, &al, mirror);
            (x, y)
        } else {
            let (x, y) = place(0.0, -2.2, ix, iy, deg, mirror);
            draw_text(&mut body, &ctag, x, y, &part.value, 1.778, C_VALUE, 0.0, "bottom-left", false);
            (x, y)
        };
        body.push_str("</g>");
        my.push((nx - 3.0, ny - 2.0)); my.push((nx + 8.0, ny + 2.0));
        my.push((vx - 3.0, vy - 2.0)); my.push((vx + 8.0, vy + 2.0));

        if my.is_empty() { my.push((ix - 2.54, -iy - 2.54)); my.push((ix + 2.54, -iy + 2.54)); }
        let (mut mnx, mut mny, mut mxx, mut mxy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
        for (x, y) in &my { mnx = mnx.min(*x); mxx = mxx.max(*x); mny = mny.min(*y); mxy = mxy.max(*y); }
        ext.extend(my.iter().copied());

        let pkgkey = format!("{}:{}:{}", part.library, part.deviceset, part.device);
        comps.push(Comp {
            reference: pname.clone(),
            value: part.value.clone(),
            footprint: dev_pkg.get(&pkgkey).cloned().unwrap_or_default(),
            lcsc: part.lcsc.clone(),
            description: dev_desc.get(&format!("{}:{}", part.library, part.deviceset)).cloned().unwrap_or_default(),
            datasheet: part.datasheet.clone(),
            x: ix, y: -iy,
            bbox: [mnx, mny, mxx, mxy],
            pin_count,
            regions: vec![[mnx, mny, mxx, mxy]],
        });
    }

    // ── compose ─────────────────────────────────────────────────────────────
    if ext.is_empty() { ext.push((0.0, 0.0)); ext.push((100.0, 100.0)); }
    let m = 5.0;
    let minx = ext.iter().map(|p| p.0).fold(f64::INFINITY, f64::min) - m;
    let miny = ext.iter().map(|p| p.1).fold(f64::INFINITY, f64::min) - m;
    let maxx = ext.iter().map(|p| p.0).fold(f64::NEG_INFINITY, f64::max) + m;
    let maxy = ext.iter().map(|p| p.1).fold(f64::NEG_INFINITY, f64::max) + m;
    let (w, h) = (maxx - minx, maxy - miny);
    let sheet = Sheet { comps, sub_sheets: Vec::new(), content_bbox: (minx, miny, w, h) };

    let mut s = String::new();
    let _ = write!(s, r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{:.3} {:.3} {:.3} {:.3}" preserveAspectRatio="xMidYMid meet" font-family="ui-monospace,Menlo,monospace">"##, minx, miny, w, h);
    let _ = write!(s, r##"<rect class="adom-sheet-bg" x="{:.3}" y="{:.3}" width="{:.3}" height="{:.3}" fill="#fbfbf6"/>"##, minx, miny, w, h);
    let _ = write!(s, r##"<g class="adom-sheet">{}</g>"##, body);
    s.push_str(&crate::sch_parse::build_overlay(&sheet));
    s.push_str("</svg>");
    Ok((s, sheet))
}

/// Draw an EAGLE cross-reference net LABEL as the gray pointed pennant Fusion
/// draws (a rectangle with a triangular point at the net-attachment end). `x,y`
/// are already sheet coords (Y-down); `rot` selects the point direction
/// (R0 right, R90 up, R180 left, R270 down).
fn draw_xref_flag(out: &mut String, tag: &str, x: f64, y: f64, name: &str, size: f64, rot: f64) {
    // Fusion draws the net-label text noticeably larger than the plain label size,
    // filling the flag; bump it and fit the pennant tightly around it.
    let ts = size * 1.4;                                           // flag text size
    let w = name.chars().count() as f64 * ts * 0.62 + ts * 0.7;    // body width
    let h = ts * 1.35;                                             // body height
    let pl = h * 0.5;                                              // point length
    // Local pennant: attach tip at (0,0) pointing -x (LEFT), body to the RIGHT — so
    // an R0 label points left toward its net and its text sits in clear space; R180
    // rotates it to point right (e.g. into U$1), matching how Fusion draws them.
    let local = [(0.0, 0.0), (pl, -h / 2.0), (pl + w, -h / 2.0), (pl + w, h / 2.0), (pl, h / 2.0)];
    // EAGLE rot is CCW in Y-up; on our Y-down sheet that's a CW screen rotation of
    // the same magnitude, so screen theta = -rot puts R90 pointing up.
    let th = (-rot).to_radians();
    let (s, c) = (th.sin(), th.cos());
    let mut d = String::new();
    for (i, (lx, ly)) in local.iter().enumerate() {
        let (rx, ry) = (lx * c - ly * s + x, lx * s + ly * c + y);
        let _ = write!(d, "{} {:.3} {:.3} ", if i == 0 { "M" } else { "L" }, rx, ry);
    }
    d.push('Z');
    let _ = write!(out, r##"<path{tag} d="{}" fill="none" stroke="{}" stroke-width="0.18"/>"##, d, C_INFO);
    // Net name centred in the body, rotated with the flag.
    let (bx, by) = (pl + w / 2.0, 0.0);
    let (tx, ty) = (bx * c - by * s + x, bx * s + by * c + y);
    let mut r = ((rot % 360.0) + 360.0) % 360.0;
    if r > 90.0 && r <= 270.0 { r -= 180.0; }
    let transform = if r.abs() > 0.01 { format!(r##" transform="rotate({:.2} {:.3} {:.3})""##, r, tx, ty) } else { String::new() };
    let _ = write!(out, r##"<text{tag} x="{:.3}" y="{:.3}" font-size="{:.3}" fill="{}" text-anchor="middle" dominant-baseline="central"{}>{}</text>"##,
        tx, ty, ts, C_NAME, transform, esc(name));
}

/// Draw one EAGLE primitive (wire/rect/circle/polygon/text) into `out`.
#[allow(clippy::too_many_arguments)]
fn draw_prim(out: &mut String, n: Node, ix: f64, iy: f64, deg: f64, mirror: bool,
             color: &str, cref: &str, ext: &mut Vec<(f64, f64)>, _owner: &str) {
    let p = |x: f64, y: f64| place(x, y, ix, iy, deg, mirror);
    let tag = if cref.is_empty() { String::new() } else { format!(r##" data-cref="{}""##, cref) };
    let layer = num(n, "layer") as i32;
    // Each primitive keeps its EAGLE layer colour (94 body red, 97 info gray, …),
    // falling back to the caller's colour for unmapped layers.
    let lc = layer_color(layer).unwrap_or(color);
    match n.tag_name().name() {
        "wire" => {
            let (x1, y1) = p(num(n, "x1"), num(n, "y1"));
            let (x2, y2) = p(num(n, "x2"), num(n, "y2"));
            ext.push((x1, y1)); ext.push((x2, y2));
            let _ = write!(out, r##"<line{} x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="{}" stroke-width="{:.3}" stroke-linecap="round"/>"##,
                tag, x1, y1, x2, y2, lc, num(n, "width").max(0.15));
        }
        "rectangle" => {
            let (x1, y1) = p(num(n, "x1"), num(n, "y1"));
            let (x2, y2) = p(num(n, "x2"), num(n, "y2"));
            ext.push((x1, y1)); ext.push((x2, y2));
            let _ = write!(out, r##"<rect{} x="{:.3}" y="{:.3}" width="{:.3}" height="{:.3}" fill="none" stroke="{}" stroke-width="0.2"/>"##,
                tag, x1.min(x2), y1.min(y2), (x2 - x1).abs(), (y2 - y1).abs(), lc);
        }
        "circle" => {
            let (cx, cy) = p(num(n, "x"), num(n, "y"));
            let r = num(n, "radius");
            ext.push((cx - r, cy - r)); ext.push((cx + r, cy + r));
            let _ = write!(out, r##"<circle{} cx="{:.3}" cy="{:.3}" r="{:.3}" fill="none" stroke="{}" stroke-width="{:.3}"/>"##,
                tag, cx, cy, r, lc, num(n, "width").max(0.15));
        }
        "polygon" | "polygonshape" => {
            let pts: Vec<(f64, f64)> = n.children().filter(|c| c.has_tag_name("vertex"))
                .map(|v| p(num(v, "x"), num(v, "y"))).collect();
            if pts.len() < 2 { return; }
            let mut d = String::new();
            for (i, (x, y)) in pts.iter().enumerate() {
                let _ = write!(d, "{} {:.3} {:.3} ", if i == 0 { "M" } else { "L" }, x, y);
            }
            d.push('Z');
            ext.extend(pts.iter().copied());
            // A pour=solid polygon (the mechanical-pin connector shapes on the Info
            // layer) fills solidly like Fusion draws it; other polygons stay light.
            let opacity = if at(n, "pour") == "solid" { 0.7 } else { 0.25 };
            let _ = write!(out, r##"<path{} d="{}" fill="{}" fill-opacity="{}" stroke="{}" stroke-width="0.15"/>"##, tag, d, lc, opacity, lc);
        }
        "text" => {
            let (x, y) = p(num(n, "x"), num(n, "y"));
            let t = n.text().unwrap_or("").trim().to_string();
            // >NAME / >VALUE placeholders are drawn from the instance instead
            if t.is_empty() || t.starts_with('>') { return; }
            let size = if num(n, "size") > 0.0 { num(n, "size") } else { 1.4 };
            ext.push((x, y - size)); ext.push((x + t.chars().count() as f64 * size * 0.6, y));
            draw_text(out, &tag, x, y, &t, size, lc, deg + rot_of(&at(n, "rot")).0, &at(n, "align"), mirror);
        }
        _ => {}
    }
}