app
Adom Schematic
Public Made by Adomby adom
Interactive schematic viewer: your EDA's own render plus per-symbol highlighting and live MPN, stock and price on hover
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
//! Self-render a `.kicad_sch` into the `.adom-sheet` inner SVG WITHOUT
//! service-kicad. Used as the fallback when the shared KiCad export can't render
//! a sheet — most importantly a HIERARCHICAL ROOT, which kicad-cli renders empty
//! when its child files aren't present. Draws every symbol from its `lib_symbols`
//! definition (rectangles / polylines / circles / arcs / pins) transformed to the
//! instance placement, plus wires, junctions, power symbols and labels, in
//! KiCad's own light-sheet colours. Each component is wrapped `<g data-cref>` so
//! the existing halo-on-hover works unchanged.
//!
//! Ported from adom-2dboard's sch_render.rs; here it only emits the inner
//! content (the caller wraps it in `<g class="adom-sheet">` + overlay).
use crate::sexpr::{blocks, esc, f};
use regex::Regex;
use std::collections::HashMap;
use std::fmt::Write as _;
// KiCad default schematic colours
const C_SYM: &str = "#840000"; // symbol bodies + pins (dark red)
const C_WIRE: &str = "#008484"; // wires + junctions (teal-green)
const C_NAME: &str = "#840000"; // reference designators
const C_VALUE: &str = "#008484"; // values
const C_LABEL: &str = "#840000"; // net / power labels
#[derive(Clone)]
struct Prim { kind: u8, pts: Vec<(f64, f64)>, r: f64 } // 0 poly/rect, 1 circle, 2 pin
struct LibSym { prims: Vec<Prim> }
/// lib Y-up -> screen Y-down, ROTATE CCW by `rot` (screen matrix uses -rot), then
/// MIRROR in the world/page frame, then translate to (px,py). KiCad applies the
/// mirror AFTER the rotation (in page space), not before — doing it before put a
/// rotated+mirrored symbol's body/pins on the wrong side while its axis-symmetric
/// features (e.g. a pin-1 socket arc) still matched, so a mirrored connector lit up
/// only by its text + pin-1 arc. `mirror==2` is KiCad `(mirror x)` = flip page Y;
/// `mirror==1` is `(mirror y)` = flip page X.
fn xf(lx: f64, ly: f64, px: f64, py: f64, rot: f64, mirror: u8) -> (f64, f64) {
let (x, y) = (lx, -ly);
let a = (-rot).to_radians();
let (c, s) = (a.cos(), a.sin());
let (mut rx, mut ry) = (x * c - y * s, x * s + y * c);
if mirror == 1 { rx = -rx; } // (mirror y) → flip page X
if mirror == 2 { ry = -ry; } // (mirror x) → flip page Y
(px + rx, py + ry)
}
fn quoted(s: &str) -> Option<String> {
let q1 = s.find('"')?; let rest = &s[q1 + 1..]; let q2 = rest.find('"')?; Some(rest[..q2].to_string())
}
fn capture1(s: &str, re: &str) -> Option<String> { Regex::new(re).ok()?.captures(s).map(|c| c[1].to_string()) }
/// Normalize a symbol name / lib_id to a bare key for FALLBACK lookup. KiCad
/// usually stores each `lib_symbols` entry under its full lib_id ("Device:LED"),
/// but on a name collision it renames the stored entry to a bare, dedup-suffixed
/// name ("LED_2") while INSTANCES still reference the original lib_id
/// ("Device:LED"). An exact lookup then misses and the symbol gets no geometry.
/// This drops the "Lib:" prefix and a trailing "_<digits>" so both sides collapse
/// to the same key ("LED"). Used ONLY as a fallback after the exact lookup fails.
pub fn lib_bare_key(name: &str) -> String {
let base = name.rsplit(':').next().unwrap_or(name);
if let Some(pos) = base.rfind('_') {
if pos + 1 < base.len() && base[pos + 1..].chars().all(|c| c.is_ascii_digit()) {
return base[..pos].to_string();
}
}
base.to_string()
}
/// Resolve a lib def by exact lib_id, falling back to the bare-key alias (see
/// `lib_bare_key`) so dedup-renamed symbols like "LED_2" still resolve.
fn resolve_lib<'a, T>(libs: &'a HashMap<String, T>, bare: &HashMap<String, String>, lib_id: &str) -> Option<&'a T> {
libs.get(lib_id).or_else(|| bare.get(&lib_bare_key(lib_id)).and_then(|n| libs.get(n)))
}
/// Build the bare-key → stored-name index for the fallback lookup.
fn bare_index<T>(libs: &HashMap<String, T>) -> HashMap<String, String> {
let mut m = HashMap::new();
for k in libs.keys() { m.entry(lib_bare_key(k)).or_insert_with(|| k.clone()); }
m
}
/// Top-level `(symbol …)` blocks (not nested).
fn top_symbols(s: &str) -> Vec<&str> {
let bytes = s.as_bytes();
let mut out = vec![];
let mut i = 0;
while let Some(rel) = s[i..].find("(symbol") {
let start = i + rel;
let after = start + 7;
if bytes.get(after).map(|c| !c.is_ascii_whitespace()).unwrap_or(true) { i = after; continue; }
let mut depth = 0i32; let mut end = start;
for j in start..s.len() { match bytes[j] { b'(' => depth += 1, b')' => { depth -= 1; if depth == 0 { end = j; break; } } _ => {} } }
out.push(&s[start..=end.min(s.len() - 1)]);
i = end + 1;
}
out
}
fn strip_first_block(s: &str, tag: &str) -> String {
if let Some(b) = blocks(s, tag).into_iter().next() { return s.replacen(b, "", 1); }
s.to_string()
}
fn poly_d(pts: &[(f64, f64)]) -> String {
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
}
/// A quantized line-segment key (both endpoints to 0.05mm, order-independent) so
/// export strokes can be matched to the exact symbol geometry regardless of which
/// end KiCad drew first.
pub type SegKey = (i32, i32, i32, i32);
pub fn seg_key_pub(a: (f64, f64), b: (f64, f64)) -> SegKey { seg_key(a, b) }
fn seg_key(a: (f64, f64), b: (f64, f64)) -> SegKey {
let q = |v: f64| (v * 20.0).round() as i32; // 0.05mm grid
let (ax, ay, bx, by) = (q(a.0), q(a.1), q(b.0), q(b.1));
if (ax, ay) <= (bx, by) { (ax, ay, bx, by) } else { (bx, by, ax, ay) }
}
/// The EXACT set of line segments that make up each symbol on the sheet, keyed by
/// reference. Built from `lib_symbols` (rectangle / polyline / pin edges)
/// transformed to each instance's placement. This is the ground truth for hover
/// highlighting: a stroke in the KiCad export belongs to a component IFF it
/// coincides with one of that component's segments — a wire or another part's line
/// never will. (Circles/arcs are matched separately by centre+radius.)
pub fn symbol_segments(content: &str) -> (HashMap<SegKey, String>, HashMap<(i32, i32, i32), String>) {
let at3 = Regex::new(r"\(at\s+(-?[\d.]+)\s+(-?[\d.]+)(?:\s+(-?[\d.]+))?\)").unwrap();
let se = Regex::new(r"\(start\s+(-?[\d.]+)\s+(-?[\d.]+)\)\s*\(end\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();
// arc: (start x y) (mid x y) (end x y) — capture start (1,2), mid (3,4), end (5,6)
let arc_se = Regex::new(r"\(start\s+(-?[\d.]+)\s+(-?[\d.]+)\)\s*\(mid\s+(-?[\d.]+)\s+(-?[\d.]+)\)\s*\(end\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();
let xy = Regex::new(r"\(xy\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();
let center = Regex::new(r"\(center\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();
let radius = Regex::new(r"\(radius\s+(-?[\d.]+)\)").unwrap();
let length = Regex::new(r"\(length\s+(-?[\d.]+)\)").unwrap();
let mirror_re = Regex::new(r"\(mirror\s+(x|y)\)").unwrap();
let mut libs: HashMap<String, LibSym> = HashMap::new();
if let Some(libblk) = blocks(content, "lib_symbols").into_iter().next() {
for def in top_symbols(libblk) {
let name = quoted(def).unwrap_or_default();
if name.is_empty() { continue; }
let mut prims = vec![];
for r in blocks(def, "rectangle") {
if let Some(c) = se.captures(r) {
let (x1, y1, x2, y2) = (f(c.get(1)), f(c.get(2)), f(c.get(3)), f(c.get(4)));
prims.push(Prim { kind: 0, pts: vec![(x1, y1), (x2, y1), (x2, y2), (x1, y2), (x1, y1)], r: 0.0 });
}
}
for p in blocks(def, "polyline") {
let pts: Vec<(f64, f64)> = xy.captures_iter(p).map(|c| (f(c.get(1)), f(c.get(2)))).collect();
if pts.len() >= 2 { prims.push(Prim { kind: 0, pts, r: 0.0 }); }
}
for c in blocks(def, "circle") {
if let (Some(ce), Some(ra)) = (center.captures(c), radius.captures(c)) {
prims.push(Prim { kind: 1, pts: vec![(f(ce.get(1)), f(ce.get(2)))], r: f(ra.get(1)) });
}
}
// arcs (inductor coils, curved cap plates, crystal): KiCad's SVG export
// draws each as an `A` command giving only its start+end, so match on the
// CHORD (start→end). Without this, arc-bodied parts (L, ferrite, crystal)
// never highlighted their body.
for a in blocks(def, "arc") {
if let Some(c) = arc_se.captures(a) {
let (sx, sy, ex, ey) = (f(c.get(1)), f(c.get(2)), f(c.get(5)), f(c.get(6)));
prims.push(Prim { kind: 0, pts: vec![(sx, sy), (ex, ey)], r: 0.0 });
}
}
for pin in blocks(def, "pin") {
if let Some(a) = at3.captures(pin) {
let (lx, ly, ang) = (f(a.get(1)), f(a.get(2)), f(a.get(3)));
let len = length.captures(pin).map(|c| f(c.get(1))).unwrap_or(2.54);
let (dx, dy) = (ang.to_radians().cos() * len, ang.to_radians().sin() * len);
prims.push(Prim { kind: 2, pts: vec![(lx, ly), (lx + dx, ly + dy)], r: 0.0 });
}
}
libs.insert(name, LibSym { prims });
}
}
let bare = bare_index(&libs);
let content_wo_lib = strip_first_block(content, "lib_symbols");
let mut segs: HashMap<SegKey, String> = HashMap::new();
let mut circles: HashMap<(i32, i32, i32), String> = HashMap::new();
for inst in top_symbols(&content_wo_lib) {
let lib_id = match capture1(inst, r##"\(lib_id\s+"([^"]+)"\)"##) { Some(s) => s, None => continue };
let a = match at3.captures(inst) { Some(a) => a, None => continue };
let (px, py, rot) = (f(a.get(1)), f(a.get(2)), f(a.get(3)));
let mirror = mirror_re.captures(inst).map(|c| if &c[1] == "x" { 2u8 } else { 1u8 }).unwrap_or(0);
let reference = capture1(inst, r##"\(property\s+"Reference"\s+"([^"]*)""##).unwrap_or_default();
if reference.is_empty() || lib_id.starts_with("power:") { continue; }
if let Some(lib) = resolve_lib(&libs, &bare, &lib_id) {
for pr in &lib.prims {
match pr.kind {
1 => { // circle: centre+radius key
let (cx, cy) = xf(pr.pts[0].0, pr.pts[0].1, px, py, rot, mirror);
let q = |v: f64| (v * 20.0).round() as i32;
circles.insert((q(cx), q(cy), q(pr.r)), reference.clone());
}
_ => { // polyline/rect/pin: consecutive segments
let tp: Vec<(f64, f64)> = pr.pts.iter().map(|&(x, y)| xf(x, y, px, py, rot, mirror)).collect();
for w in tp.windows(2) { segs.insert(seg_key(w[0], w[1]), reference.clone()); }
}
}
}
}
}
(segs, circles)
}
/// Anchor points of things that are NOT a component's own text: power-symbol
/// labels (power:GND, power:+3V3 …) and net / global / hierarchical labels. Text
/// in the export sitting on one of these is a NET/POWER label, never a symbol's
/// pin-name or field, so it must not be tagged to a component.
pub fn label_anchors(content: &str) -> Vec<(f64, f64)> {
let at = Regex::new(r"\(at\s+(-?[\d.]+)\s+(-?[\d.]+)").unwrap();
let mut out = Vec::new();
let cw = strip_first_block(content, "lib_symbols");
// schematic `(at)` is Y-DOWN, same frame as the export/page — do NOT negate Y.
for tag in ["label", "global_label", "hierarchical_label"] {
for l in blocks(&cw, tag) {
if let Some(c) = at.captures(l) { out.push((f(c.get(1)), f(c.get(2)))); }
}
}
// Power symbols: the VISIBLE label is the Value property's text (e.g. "GND"),
// which sits at its OWN `(at)`, NOT the symbol origin (the origin can be closer
// to a neighbouring component's pin number than to its own label — using it made
// the exclusion grab pin numbers). The Value `(at)` is exactly where the text is.
let val_at = Regex::new(r##"\(property\s+"Value"\s+"[^"]*"\s*\(at\s+(-?[\d.]+)\s+(-?[\d.]+)"##).unwrap();
for inst in top_symbols(&cw) {
if capture1(inst, r##"\(lib_id\s+"(power:[^"]+)"\)"##).is_some() {
if let Some(c) = val_at.captures(inst) { out.push((f(c.get(1)), f(c.get(2)))); }
else if let Some(c) = at.captures(inst) { out.push((f(c.get(1)), f(c.get(2)))); }
}
}
out
}
/// Page-space anchor points for each symbol's PIN TEXT (names inside the body,
/// numbers along the stubs). A pin is `(at lx ly ang) (length L)`: `(lx,ly)` is
/// the CONNECTION point (where wires / net labels attach — deliberately excluded),
/// the pin runs inward to the BODY point `B = (lx+L·cosang, ly+L·sinang)` where the
/// pin NAME sits, and the pin NUMBER sits at the stub MIDPOINT `M`. We return B and
/// M (per pin) so a text group landing on one is tagged to that symbol — while a
/// net/power label at the connection point never is. Skips power symbols.
pub fn pin_anchors(content: &str) -> Vec<(String, f64, f64)> {
let at3 = Regex::new(r"\(at\s+(-?[\d.]+)\s+(-?[\d.]+)(?:\s+(-?[\d.]+))?\)").unwrap();
let length = Regex::new(r"\(length\s+(-?[\d.]+)\)").unwrap();
let mirror_re = Regex::new(r"\(mirror\s+(x|y)\)").unwrap();
// lib_name -> Vec<(lx, ly, ang, len)>
let mut libpins: HashMap<String, Vec<(f64, f64, f64, f64)>> = HashMap::new();
if let Some(libblk) = blocks(content, "lib_symbols").into_iter().next() {
for def in top_symbols(libblk) {
let name = quoted(def).unwrap_or_default();
if name.is_empty() { continue; }
let mut pins = vec![];
for pin in blocks(def, "pin") {
if let Some(a) = at3.captures(pin) {
let (lx, ly, ang) = (f(a.get(1)), f(a.get(2)), f(a.get(3)));
let len = length.captures(pin).map(|c| f(c.get(1))).unwrap_or(2.54);
pins.push((lx, ly, ang, len));
}
}
libpins.insert(name, pins);
}
}
let bare = bare_index(&libpins);
let cw = strip_first_block(content, "lib_symbols");
let mut out = Vec::new();
for inst in top_symbols(&cw) {
let lib_id = match capture1(inst, r##"\(lib_id\s+"([^"]+)"\)"##) { Some(s) => s, None => continue };
if lib_id.starts_with("power:") { continue; }
let a = match at3.captures(inst) { Some(a) => a, None => continue };
let (px, py, rot) = (f(a.get(1)), f(a.get(2)), f(a.get(3)));
let mirror = mirror_re.captures(inst).map(|c| if &c[1] == "x" { 2u8 } else { 1u8 }).unwrap_or(0);
let reference = capture1(inst, r##"\(property\s+"Reference"\s+"([^"]*)""##).unwrap_or_default();
if reference.is_empty() { continue; }
if let Some(pins) = resolve_lib(&libpins, &bare, &lib_id) {
for &(lx, ly, ang, len) in pins {
let (dx, dy) = (ang.to_radians().cos() * len, ang.to_radians().sin() * len);
// body point (pin name) and stub midpoint (pin number), in page space
let b = xf(lx + dx, ly + dy, px, py, rot, mirror);
let m = xf(lx + dx / 2.0, ly + dy / 2.0, px, py, rot, mirror);
out.push((reference.clone(), b.0, b.1));
out.push((reference.clone(), m.0, m.1));
}
}
}
out
}
/// The ON-SHEET TEXT that belongs to each symbol: its Reference and Value fields.
/// KiCad stores each as `(property "Reference"|"Value" "<text>" (at x y …))` with
/// an ABSOLUTE page position; that's the ONLY text that is the symbol's own (pin
/// names/numbers are the symbol GRAPHIC, and net/power labels are standalone).
/// Returns (ref-designator, field text, x, y) for each VISIBLE Reference/Value.
pub fn field_anchors(content: &str) -> Vec<(String, String, f64, f64)> {
let at = Regex::new(r"\(at\s+(-?[\d.]+)\s+(-?[\d.]+)").unwrap();
let cw = strip_first_block(content, "lib_symbols");
let mut out = Vec::new();
for inst in top_symbols(&cw) {
if capture1(inst, r##"\(lib_id\s+"(power:[^"]+)"\)"##).is_some() { continue; }
let reference = capture1(inst, r##"\(property\s+"Reference"\s+"([^"]*)""##).unwrap_or_default();
if reference.is_empty() { continue; }
// each `(property "Name" "Value" (at x y ..) (effects .. [ (hide yes) ]))`
for pb in blocks(inst, "property") {
let name = quoted(pb).unwrap_or_default();
if name != "Reference" && name != "Value" { continue; }
// the field text is the SECOND quoted string
let text = nth_quoted(pb, 1).unwrap_or_default();
// skip hidden fields (not rendered on the sheet)
if pb.contains("(hide yes)") || pb.contains("hide)") { continue; }
if let Some(c) = at.captures(pb) {
out.push((reference.clone(), text, f(c.get(1)), f(c.get(2))));
}
}
}
out
}
fn nth_quoted(s: &str, n: usize) -> Option<String> {
let mut rest = s; let mut i = 0;
loop {
let q1 = rest.find('"')? + 1;
let after = &rest[q1..];
let q2 = after.find('"')?;
let val = after[..q2].to_string();
if i == n { return Some(val); }
rest = &after[q2 + 1..]; i += 1;
}
}
/// Render the sheet's drawable content (symbols + wires + power + labels) as the
/// inner SVG for `<g class="adom-sheet">`. Returns "" if there's nothing to draw.
pub fn render_inner(content: &str) -> String {
let at3 = Regex::new(r"\(at\s+(-?[\d.]+)\s+(-?[\d.]+)(?:\s+(-?[\d.]+))?\)").unwrap();
let se = Regex::new(r"\(start\s+(-?[\d.]+)\s+(-?[\d.]+)\)\s*\(end\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();
let xy = Regex::new(r"\(xy\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();
let center = Regex::new(r"\(center\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();
let radius = Regex::new(r"\(radius\s+(-?[\d.]+)\)").unwrap();
let length = Regex::new(r"\(length\s+(-?[\d.]+)\)").unwrap();
let prop = Regex::new(r##"\(property\s+"([^"]+)"\s+"([^"]*)""##).unwrap();
let mirror_re = Regex::new(r"\(mirror\s+(x|y)\)").unwrap();
// ── lib_symbols ──
let mut libs: HashMap<String, LibSym> = HashMap::new();
if let Some(libblk) = blocks(content, "lib_symbols").into_iter().next() {
for def in top_symbols(libblk) {
let name = quoted(def).unwrap_or_default();
if name.is_empty() { continue; }
let mut prims = vec![];
for r in blocks(def, "rectangle") {
if let Some(c) = se.captures(r) {
let (x1, y1, x2, y2) = (f(c.get(1)), f(c.get(2)), f(c.get(3)), f(c.get(4)));
prims.push(Prim { kind: 0, pts: vec![(x1, y1), (x2, y1), (x2, y2), (x1, y2), (x1, y1)], r: 0.0 });
}
}
for p in blocks(def, "polyline") {
let pts: Vec<(f64, f64)> = xy.captures_iter(p).map(|c| (f(c.get(1)), f(c.get(2)))).collect();
if pts.len() >= 2 { prims.push(Prim { kind: 0, pts, r: 0.0 }); }
}
for c in blocks(def, "circle") {
if let (Some(ce), Some(ra)) = (center.captures(c), radius.captures(c)) {
prims.push(Prim { kind: 1, pts: vec![(f(ce.get(1)), f(ce.get(2)))], r: f(ra.get(1)) });
}
}
for pin in blocks(def, "pin") {
if let Some(a) = at3.captures(pin) {
let (lx, ly, ang) = (f(a.get(1)), f(a.get(2)), f(a.get(3)));
let len = length.captures(pin).map(|c| f(c.get(1))).unwrap_or(2.54);
let (dx, dy) = (ang.to_radians().cos() * len, ang.to_radians().sin() * len);
prims.push(Prim { kind: 2, pts: vec![(lx, ly), (lx + dx, ly + dy)], r: 0.0 });
}
}
libs.insert(name, LibSym { prims });
}
}
let bare = bare_index(&libs);
let content_wo_lib = strip_first_block(content, "lib_symbols");
let mut out = String::new();
// ── wires + junctions ──
for w in blocks(&content_wo_lib, "wire") {
let pts: Vec<(f64, f64)> = xy.captures_iter(w).map(|c| (f(c.get(1)), f(c.get(2)))).collect();
if pts.len() >= 2 {
let _ = write!(out, r##"<line x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="{}" stroke-width="0.15" stroke-linecap="round"/>"##,
pts[0].0, pts[0].1, pts[1].0, pts[1].1, C_WIRE);
}
}
for j in blocks(&content_wo_lib, "junction") {
if let Some(a) = at3.captures(j) {
let _ = write!(out, r##"<circle cx="{:.3}" cy="{:.3}" r="0.4" fill="{}"/>"##, f(a.get(1)), f(a.get(2)), C_WIRE);
}
}
// net / global / hierarchical labels
for l in blocks(&content_wo_lib, "label").into_iter()
.chain(blocks(&content_wo_lib, "global_label"))
.chain(blocks(&content_wo_lib, "hierarchical_label")) {
if let Some(a) = at3.captures(l) {
let text = quoted(l).unwrap_or_default();
if !text.is_empty() {
let _ = write!(out, r##"<text x="{:.3}" y="{:.3}" font-size="1.4" fill="{}" font-family="monospace" dominant-baseline="middle">{}</text>"##,
f(a.get(1)) + 0.5, f(a.get(2)), C_LABEL, esc(&text));
}
}
}
// ── symbol instances ──
for inst in top_symbols(&content_wo_lib) {
let lib_id = match capture1(inst, r##"\(lib_id\s+"([^"]+)"\)"##) { Some(s) => s, None => continue };
let a = match at3.captures(inst) { Some(a) => a, None => continue };
let (px, py, rot) = (f(a.get(1)), f(a.get(2)), f(a.get(3)));
let mirror = mirror_re.captures(inst).map(|c| if &c[1] == "x" { 2u8 } else { 1u8 }).unwrap_or(0);
let (mut reference, mut value) = (String::new(), String::new());
for c in prop.captures_iter(inst) {
match &c[1] { "Reference" => reference = c[2].to_string(), "Value" => value = c[2].to_string(), _ => {} }
}
let is_power = lib_id.starts_with("power:");
let lib = resolve_lib(&libs, &bare, &lib_id);
let mut body = String::new();
let (mut minx, mut miny, mut maxx, mut maxy) = (f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
if let Some(lib) = lib {
for pr in &lib.prims {
let tp: Vec<(f64, f64)> = pr.pts.iter().map(|&(x, y)| xf(x, y, px, py, rot, mirror)).collect();
for &(x, y) in &tp { minx = minx.min(x); miny = miny.min(y); maxx = maxx.max(x); maxy = maxy.max(y); }
match pr.kind {
1 => { let _ = write!(body, r##"<circle cx="{:.3}" cy="{:.3}" r="{:.3}" fill="none" stroke="{}" stroke-width="0.15"/>"##, tp[0].0, tp[0].1, pr.r, C_SYM); }
2 => { let _ = write!(body, r##"<line x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="{}" stroke-width="0.15"/>"##, tp[0].0, tp[0].1, tp[1].0, tp[1].1, C_SYM); }
_ => { let _ = write!(body, r##"<path d="{}" fill="none" stroke="{}" stroke-width="0.15"/>"##, poly_d(&tp), C_SYM); }
}
}
}
if !minx.is_finite() { minx = px - 1.27; miny = py - 1.27; maxx = px + 1.27; maxy = py + 1.27; }
// power symbols show their net name; regular parts show ref + value
if is_power {
if !value.is_empty() {
let _ = write!(body, r##"<text x="{:.3}" y="{:.3}" font-size="1.4" fill="{}" font-family="monospace" text-anchor="middle">{}</text>"##, px, maxy + 1.6, C_LABEL, esc(&value));
}
} else if !reference.is_empty() {
let _ = write!(body, r##"<text x="{:.3}" y="{:.3}" font-size="1.3" fill="{}" font-family="monospace">{}</text>"##, maxx + 0.4, miny + 1.2, C_NAME, esc(&reference));
if !value.is_empty() { let _ = write!(body, r##"<text x="{:.3}" y="{:.3}" font-size="1.1" fill="{}" font-family="monospace">{}</text>"##, maxx + 0.4, miny + 2.7, C_VALUE, esc(&value)); }
}
let _ = write!(out, r##"<g data-cref="{}">{}</g>"##, esc(&reference), body);
}
out
}