app
Adom Symbol
Public Made by Adomby adom
KiCad symbol creator with interactive viewer and delivery to KiCad/Fusion 360
← Commit history
1.1.11: DIP walk for ungrouped left-right; chip outline fitted to the free rectangle between pin-name extents
8 files changed
+91−25
Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-symbol"-version = "1.1.10"+version = "1.1.11" dependencies = [ "anyhow", "clap",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-symbol"-version = "1.1.10"+version = "1.1.11" edition = "2021" description = "Create and preview KiCad/Fusion 360 schematic symbols with interactive viewer"
SKILL.md+2@@ -283,6 +283,8 @@ If no `side` is specified (or pins within a group have mixed/no `side` values), #### `pinLayout: "left-right"` (default) +With **no groups and no explicit sides** this is the classic DIP walk: the first half of the pins go down the left in package order, the second half up the right (pin n/2+1 bottom right, pin n top right), same height on both sides. With groups, the type-based left/right heuristic below applies.+ Place pins ONLY on the left and right sides: - **Left side**: Inputs (analog sense, ADC, sensors, data in), ground (VSS/GND)
VERSION+1−1@@ -1 +1 @@-1.1.10+1.1.11
bin/adom-symbol⋯ 1 unchanged line ⋯
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-symbol",- "version": "1.1.10",+ "version": "1.1.11", "type": "app", "description": "KiCad symbol creator with interactive viewer and delivery to KiCad/Fusion 360", "dependencies": {
src/render.rs+58−20@@ -265,14 +265,8 @@ pub fn composite_iso_into_symbol(svg: &str, block: &str, _name: &str, iso_path: 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 * frac, bh * frac);+ let (cx_k, cy_k, w, h, _rect) = outline_box(block, frac).ok_or_else(|| anyhow!("no body geometry to place iso"))?;+ let (cx, cy) = (PX + cx_k, PY - cy_k); 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}\" \@@ -336,6 +330,61 @@ fn inline_outline_markup(block: &str, overlay_svg: &str, frac: f64) -> Option<St Some(out) } +/// The rectangle inside the body that is FREE of pin-name text: the body rect+/// pulled in on each side by the longest pin name entering from that side+/// (plus KiCad's 1.016 mm name offset and a small margin). On square layouts+/// names come in from all four sides, so a fixed body fraction collides with+/// the top/bottom names; the chip outline is fitted to this box instead.+/// Returns [left, bottom, right, top] in symbol-local mm (Y-up).+pub fn free_body_rect(block: &str) -> Option<[f64; 4]> {+ let (sx, sy, ex, ey) = largest_body_rect(block)?;+ let (mut l, mut b, mut r, mut t) = (sx.min(ex), sy.min(ey), sx.max(ex), sy.max(ey));+ const CHAR_W: f64 = 1.0; // ~1.27 mm font advance, average glyph+ const OFFSET: f64 = 1.016 + 0.6;+ let re = Regex::new(r#"(?s)\(pin\s+\w+\s+\w+\s*\(at (-?[\d.]+) (-?[\d.]+) (-?[\d.]+)\)\s*\(length (-?[\d.]+)\).*?\(name "((?:[^"\\]|\\.)*)""#).unwrap();+ for c in re.captures_iter(block) {+ let a = c[3].parse::<f64>().unwrap_or(0.0).rem_euclid(360.0);+ let name = c[5].replace("~{", "").replace('}', "");+ let n = name.chars().count() as f64;+ if n == 0.0 { continue; }+ let ext = n * CHAR_W + OFFSET;+ match a as i64 {+ 0 => l = l.max(sx.min(ex) + ext),+ 180 => r = r.min(sx.max(ex) - ext),+ 270 => t = t.min(sy.max(ey) - ext),+ 90 => b = b.max(sy.min(ey) + ext),+ _ => {}+ }+ }+ if r - l < 2.0 || t - b < 2.0 {+ // Names meet in the middle: fall back to the body itself.+ return Some([sx.min(ex), sy.min(ey), sx.max(ex), sy.max(ey)]);+ }+ Some([l, b, r, t])+}++/// Where the chip outline goes: centre + box (w, h) in symbol-local mm. The box+/// is `frac` of the body (the historical size) but never wider/taller than the+/// free rectangle between the pin-name extents (minus a 1 mm margin), and it+/// is centred on that free rectangle.+pub fn outline_box(block: &str, frac: f64) -> Option<(f64, f64, f64, f64, [f64; 4])> {+ let rect = match largest_body_rect(block) {+ Some((sx, sy, ex, ey)) => [sx.min(ex), sy.min(ey), sx.max(ex), sy.max(ey)],+ None => {+ let (minx, maxx, miny, maxy) = symbol_local_bbox(block)?;+ [minx, miny, maxx, maxy]+ }+ };+ let (bw, bh) = (rect[2] - rect[0], rect[3] - rect[1]);+ let free = free_body_rect(block).unwrap_or(rect);+ const M: f64 = 1.0;+ let (fw, fh) = ((free[2] - free[0] - 2.0 * M).max(1.0), (free[3] - free[1] - 2.0 * M).max(1.0));+ let w = (bw * frac).min(fw);+ let h = (bh * frac).min(fh);+ let (cx, cy) = ((free[0] + free[2]) / 2.0, (free[1] + free[3]) / 2.0);+ Some((cx, cy, w, h, rect))+}+ /// The fraction of the symbol body the composited 3D-chip outline covers. /// MUST stay in sync with the `frac` used in `composite_iso_into_symbol` — the /// emitted outline contract (mm.svg + sidecar) describes exactly that placement.@@ -429,19 +478,8 @@ pub fn chip_size_frac(size: &str) -> f64 { pub fn outline_placement(block: &str, outline_svg: &str, frac: f64) -> Option<OutlinePlacement> { const PX: f64 = 100.0; const PY: f64 = 100.0;- let (cx_k, cy_k, bw, bh, rect) = match largest_body_rect(block) {- Some((sx, sy, ex, ey)) => (- (sx + ex) / 2.0, (sy + ey) / 2.0, (ex - sx).abs(), (ey - sy).abs(),- [sx.min(ex), sy.min(ey), sx.max(ex), sy.max(ey)],- ),- None => {- let (minx, maxx, miny, maxy) = symbol_local_bbox(block)?;- ((minx + maxx) / 2.0, (miny + maxy) / 2.0, (maxx - minx).abs(), (maxy - miny).abs(),- [minx, miny, maxx, maxy])- }- };+ let (cx_k, cy_k, w, h, rect) = outline_box(block, frac)?; let vb = parse_viewbox(outline_svg)?;- let (w, h) = (bw * frac, bh * frac); // preserveAspectRatio=meet: the WHOLE source canvas fits, smaller ratio wins. let scale = (w / vb[2]).min(h / vb[3]); if !(scale.is_finite() && scale > 0.0) {
src/sym_gen.rs+27−1@@ -266,6 +266,18 @@ pub fn infer_square_sides(pins: &[Pin], grouped: bool) -> Vec<Pin> { out } +/// DIP walk for an ungrouped two-column symbol: pins 1..=ceil(n/2) down the+/// left, the rest up the right (reversed, since the engine lays the right+/// column top→down).+pub fn infer_dip_sides(pins: &[Pin]) -> Vec<Pin> {+ let half = (pins.len() + 1) / 2;+ let mut left: Vec<Pin> = pins[..half].iter().cloned().map(|mut p| { p.side = Some("left".into()); p }).collect();+ let mut right: Vec<Pin> = pins[half..].iter().cloned().map(|mut p| { p.side = Some("right".into()); p }).collect();+ right.reverse();+ left.append(&mut right);+ left+}+ pub fn generate_ic_sym(input: &IcInput) -> String { let pin_layout = if input.pin_layout.is_empty() { "left-right" } else { input.pin_layout.as_str() }; let has_explicit_side = input.pins.iter().any(|p| p.side.is_some());@@ -274,7 +286,21 @@ pub fn generate_ic_sym(input: &IcInput) -> String { "square-grouped" | "all-sides" if !has_explicit_side => Some(infer_square_sides(&input.pins, true)), _ => None, };- let input_pins: &[Pin] = square_pins.as_deref().unwrap_or(&input.pins);+ // Ungrouped left-right = the classic DIP walk: first half of the pins down+ // the LEFT in package order, second half UP the right (pin n/2+1 bottom+ // right, pin n top right), same height on both sides. Without this every+ // pin piled onto the left as one tall column (fusion-bridge, 2026-08-29).+ let dip_pins: Option<Vec<Pin>> = if square_pins.is_none()+ && pin_layout == "left-right"+ && !has_explicit_side+ && !input.pins.iter().any(|p| p.group.is_some())+ && input.pins.len() > 1+ {+ Some(infer_dip_sides(&input.pins))+ } else {+ None+ };+ let input_pins: &[Pin] = square_pins.as_deref().or(dip_pins.as_deref()).unwrap_or(&input.pins); let pin_layout = if square_pins.is_some() { "all-sides" } else { pin_layout }; // Group pins by group name, preserving first-seen order.