app
Adom Symbol
Public Made by Adomby adom
KiCad symbol creator with interactive viewer and delivery to KiCad/Fusion 360
← Commit history
Inline outline as crisp vector (no <image> raster); new landscape screenshots; v1.0.9
11 files changed
+104−57
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-+### 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.+ -### 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.-+### 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.+ ### 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`.-+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`.+ ### 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.