← Commit history
Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-symbol"-version = "0.1.7"+version = "0.1.8" dependencies = [  "anyhow",  "clap",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-symbol"-version = "0.1.7"+version = "0.1.8" edition = "2021" description = "Create and preview KiCad/Fusion 360 schematic symbols with interactive viewer" 
README.md+16−12
@@ -15,8 +15,9 @@ viewer. No Node, no gallia, no local KiCad install. - **Render** the symbol to a cropped, dark-themed SVG via service-kicad   (`POST /kicad/sch/export/svg`) — the same render path the chip-fetcher   dashboard uses, so the thumbnail and the app match exactly. The default render-  composites the part's 3D-chip **vector outline** (with the part number lasered-  on) into the body centre.+  **inlines** the part's 3D-chip **vector outline** (with the part number lasered+  on) into the body centre as real `<polyline>`s — crisp in every renderer+  (browser, librsvg thumbnailer, anything), not a rasterisable `<image>`. - **Emit an outline contract** — alongside `<mpn>-symbol.svg`, `render` writes   `<mpn>-symbol-outline.mm.svg` + `<mpn>-symbol-outline.json` so **adom-lbr** can   draw the exact same chip outline onto the EAGLE/Fusion 360 symbol with **zero@@ -103,20 +104,23 @@ verbatim into the sidecar, so a consumer never has to reconstruct them.  ## Screenshots -### Default symbol render — 3D-chip outline + lasered part number-![adom-symbol render of a 64-pin MCU with the heavy 3D outline composited into the body](screenshots/outline-symbol.png)+### Default symbol render — inline-vector 3D outline + lasered part number+The BQ25798 QFN's 3D outline inlined into the symbol body as crisp vector, with+the part number lasered on the top face.+![adom-symbol render of the BQ25798 QFN with the heavy 3D outline inlined into the body](screenshots/outline-symbol.png) -### The outline contract overlays the symbol 1:1-The emitted `.mm.svg` (shown here recoloured magenta) painted over the live-`-symbol.svg` — it traces the chip body, pin-1 chamfer, every lead and the-lasered name exactly. Zero drift.-![Magenta outline contract overlaying the white composited outline, perfectly aligned](screenshots/outline-alignment.png)+### The outline contract lands exactly where adom-symbol draws it+The emitted `.mm.svg` (shown here recoloured magenta) placed over the live+`-symbol.svg` — it sits dead-centre in the body, framed by the real pins, tracing+the chip body, side pads and lasered name. Zero drift.+![Magenta outline contract centered in the symbol body, framed by the pins](screenshots/outline-alignment.png)  ### Real EAGLE `.lbr` output — contract vs the old guess adom-lbr drawing the outline as layer-94 wires: **left** transcribed 1:1 from the-contract (exact 0.62 fit); **right** the legacy 0.38 centre+scale guess (~30%-too small). Both round-tripped back through `adom-sfconvert`.-![Side-by-side EAGLE symbols showing the contract fit vs the smaller legacy guess](screenshots/outline-lbr-contract-vs-legacy.png)+contract (exact 0.62 fit, centred); **right** the legacy guess — too small *and*+shoved off-centre, because it fit the source's ink bbox instead of the canvas.+Both round-tripped back through `adom-sfconvert`.+![Side-by-side EAGLE symbols: the contract fit centred vs the smaller, off-centre legacy guess](screenshots/outline-lbr-contract-vs-legacy.png)  ### The lite `.mm.svg` outline Decimated to a few hundred segments for small libraries, in symbol mm-space.
VERSION+1−1
@@ -1 +1 @@-1.0.8+1.0.9
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-symbol",-  "version": "1.0.8",+  "version": "1.0.9",   "type": "app",   "description": "KiCad symbol creator with interactive viewer and delivery to KiCad/Fusion 360",   "dependencies": {
screenshots/outline-alignment.png
⋯ 1 unchanged line ⋯
screenshots/outline-contract-json.png
⋯ 1 unchanged line ⋯
screenshots/outline-lbr-contract-vs-legacy.png
⋯ 1 unchanged line ⋯
screenshots/outline-mm-lite.png
⋯ 1 unchanged line ⋯
screenshots/outline-symbol.png
⋯ 1 unchanged line ⋯
src/render.rs+84−41
@@ -135,57 +135,100 @@ fn prop_justify(block: &str, name: &str) -> String {     re.captures(block).map(|c| format!(" {}", &c[1])).unwrap_or_default() } -/// Composite an iso 3D chip PNG into the symbol body centre — the "default-/// symbol with the 3D chip in it" the chip-fetcher dashboard card shows. We-/// place the image centred in the symbol's largest body rectangle (the chip-/// outline), sized to ~62% of it so pin names/numbers near the edges stay-/// readable, matching the live app's centred-3D default.+/// Composite the 3D-chip overlay into the symbol body centre — the "default+/// symbol with the 3D chip in it" the chip-fetcher dashboard card shows, centred+/// in the symbol's largest body rectangle and sized to ~62% of it so pin+/// names/numbers near the edges stay readable.+///+/// For a **vector** overlay (`.svg` from chip-thumbnailer) we INLINE the real+/// strokes as `<polyline>`s (full resolution, each layer keeping its own+/// colour/width/dash + any glow `<defs>`), so the symbol stays crisp vector in+/// EVERY renderer — browser, librsvg thumbnailer, anything. (Wrapping the SVG in+/// an `<image data:svg+xml>` rasterised it in non-browser renderers.) For a+/// **raster** overlay (`--iso` PNG) we fall back to an `<image>` element. pub fn composite_iso_into_symbol(svg: &str, block: &str, _name: &str, iso_path: &std::path::Path) -> Result<String> {-    let bytes = std::fs::read(iso_path)-        .with_context(|| format!("reading iso {}", iso_path.display()))?;-    let mime = if iso_path.extension().and_then(|e| e.to_str()) == Some("svg") {-        "image/svg+xml"-    } else { "image/png" };-    let data = format!("data:{mime};base64,{}", b64(&bytes));+    let is_svg = iso_path.extension().and_then(|e| e.to_str()) == Some("svg"); -    // Body rectangle in symbol-local coords → SVG coords (PX/PY origin, Y flip).-    const PX: f64 = 100.0;-    const PY: f64 = 100.0;-    let (cx, cy, bw, bh) = match largest_body_rect(block) {-        Some((sx, sy, ex, ey)) => {-            let cx = PX + (sx + ex) / 2.0;-            let cy = PY - (sy + ey) / 2.0; // Y flip-            (cx, cy, (ex - sx).abs(), (ey - sy).abs())-        }-        None => match symbol_local_bbox(block) {-            Some((minx, maxx, miny, maxy)) => {-                let cx = PX + (minx + maxx) / 2.0;-                let cy = PY - (miny + maxy) / 2.0;-                (cx, cy, (maxx - minx).abs(), (maxy - miny).abs())-            }-            None => return Err(anyhow!("no body geometry to place iso")),-        },+    let markup = if is_svg {+        let overlay = std::fs::read_to_string(iso_path)+            .with_context(|| format!("reading outline {}", iso_path.display()))?;+        inline_outline_markup(block, &overlay)+            .ok_or_else(|| anyhow!("no body geometry / outline strokes to inline"))?+    } else {+        let bytes = std::fs::read(iso_path)+            .with_context(|| format!("reading iso {}", iso_path.display()))?;+        let data = format!("data:image/png;base64,{}", b64(&bytes));+        const PX: f64 = 100.0;+        const PY: f64 = 100.0;+        let (cx, cy, bw, bh) = match largest_body_rect(block) {+            Some((sx, sy, ex, ey)) => (PX + (sx + ex) / 2.0, PY - (sy + ey) / 2.0, (ex - sx).abs(), (ey - sy).abs()),+            None => match symbol_local_bbox(block) {+                Some((minx, maxx, miny, maxy)) => (PX + (minx + maxx) / 2.0, PY - (miny + maxy) / 2.0, (maxx - minx).abs(), (maxy - miny).abs()),+                None => return Err(anyhow!("no body geometry to place iso")),+            },+        };+        let (w, h) = (bw * OUTLINE_BODY_FRAC, bh * OUTLINE_BODY_FRAC);+        let (x, y) = (cx - w / 2.0, cy - h / 2.0);+        format!(+            "<image x=\"{x:.3}\" y=\"{y:.3}\" width=\"{w:.3}\" height=\"{h:.3}\" \+             preserveAspectRatio=\"xMidYMid meet\" opacity=\"0.96\" href=\"{data}\" />"+        )     };-    // Cover ~62% of the body; the image keeps its own aspect ratio (meet).-    let frac = 0.62;-    let w = bw * frac;-    let h = bh * frac;-    let x = cx - w / 2.0;-    let y = cy - h / 2.0;-    let image = format!(-        "<image x=\"{x:.3}\" y=\"{y:.3}\" width=\"{w:.3}\" height=\"{h:.3}\" \-         preserveAspectRatio=\"xMidYMid meet\" opacity=\"0.96\" href=\"{data}\" />"-    );+     // Inject just before </svg> so it paints on top of the symbol body.     if let Some(pos) = svg.rfind("</svg>") {-        let mut out = String::with_capacity(svg.len() + image.len() + 8);+        let mut out = String::with_capacity(svg.len() + markup.len() + 8);         out.push_str(&svg[..pos]);-        out.push_str(&image);+        out.push_str(&markup);         out.push_str(&svg[pos..]);         Ok(out)     } else {-        Err(anyhow!("no </svg> close tag to inject iso into"))+        Err(anyhow!("no </svg> close tag to inject overlay into"))+    }+}++/// Build the inline vector markup (`<defs>` + a `<g>` of `<polyline>`s) that+/// places a chip-thumbnailer outline SVG into the symbol body at FULL resolution,+/// in the `<mpn>-symbol.svg` frame, faithful to each layer's stroke style. This+/// is the crisp-vector twin of the lite `.mm.svg` the outline contract emits.+fn inline_outline_markup(block: &str, overlay_svg: &str) -> Option<String> {+    let place = outline_placement(block, overlay_svg)?;+    let elems = parse_poly_elems(overlay_svg);+    if elems.is_empty() {+        return None;+    }+    let mut out = String::with_capacity(overlay_svg.len());+    let defs = scaled_defs(overlay_svg, place.scale);+    if !defs.is_empty() {+        out.push_str(&defs);+    }+    out.push_str("<g fill=\"none\" stroke-linejoin=\"round\" stroke-linecap=\"round\" opacity=\"0.96\">");+    for e in &elems {+        let w = (e.stroke_width * place.scale).max(0.02);+        out.push_str(&format!("<polyline stroke=\"{}\" stroke-width=\"{:.4}\"", e.stroke, w));+        if let Some(d) = &e.dash {+            let scaled: Vec<String> = d+                .split(|c| c == ',' || c == ' ')+                .filter(|s| !s.is_empty())+                .map(|s| s.parse::<f64>().map(|v| format!("{:.4}", v * place.scale)).unwrap_or_else(|_| s.to_string()))+                .collect();+            out.push_str(&format!(" stroke-dasharray=\"{}\"", scaled.join(",")));+        }+        if let Some(f) = &e.filter {+            out.push_str(&format!(" filter=\"{f}\""));+        }+        out.push_str(" points=\"");+        for (i, &(px, py)) in e.pts.iter().enumerate() {+            let (x, y) = place.to_svg(px, py);+            if i > 0 {+                out.push(' ');+            }+            out.push_str(&format!("{x:.4},{y:.4}"));+        }+        out.push_str("\"/>");     }+    out.push_str("</g>");+    Some(out) }  /// The fraction of the symbol body the composited 3D-chip outline covers.