← Commit history

1.1.11: DIP walk for ungrouped left-right; chip outline fitted to the free rectangle between pin-name extents

John Lauer ·470cc848cf ·1mo ago ·parent dce6916
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.