← Commit history

1.1.12: Reference/Value inside the body on square layouts (2.54 mm breathing room, corner fallback); contract json free_rect/icon_rect/designators

John Lauer ·2ca177e332 ·1mo ago ·parent 16cb80d
9 files changed +138−23
Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-symbol"-version = "1.1.11"+version = "1.1.12" dependencies = [  "anyhow",  "clap",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-symbol"-version = "1.1.11"+version = "1.1.12" edition = "2021" description = "Create and preview KiCad/Fusion 360 schematic symbols with interactive viewer" 
SKILL.md+4
@@ -300,6 +300,10 @@ These are the two square variants a component page ships (John, 2026-08-29: stan - `square-grouped`: **whole groups per side**. `Power` on top; ground pins (GND/VSS/AGND/DGND/PGND/EP) are split out of Power onto the bottom as a `Ground` group; any group that is all-ground goes to the bottom, all-power to the top; the remaining groups are placed largest-first onto the side with the fewest pins. - Explicit `side` on any pin disables the inference for the whole symbol (explicit sides always win, so ds2sf and studio relayouts are untouched). +#### Reference and Value on square layouts (v1.1.12+)++Any layout with pins on the top or bottom puts **Reference and Value inside the body**, in the text-free rectangle (the body pulled in by each side's longest pin name and group caption): Reference 2.54 mm below the free top, Value 2.54 mm above the free bottom, the 3D-chip icon between them. Outside placement would sit on the top/bottom pin stubs, so every wire to those pins would run through the label. If the free rect cannot hold both labels with a text height between them, they fall back to the outside top-left / bottom-left corners beyond the pin stubs. Two-column layouts keep the above/below placement. The outline contract `.json` carries `free_rect`, `icon_rect`, and `designators` (reference/value positions, `inside_body`, `inset_mm`) so adom-lbr places `>NAME`/`>VALUE` identically.+ #### `pinLayout: "all-sides"` (legacy name)  With no explicit sides this is now the same as `square-grouped`. With explicit sides it is the four-side placement engine described below.
VERSION+1−1
@@ -1 +1 @@-1.1.11+1.1.12
bin/adom-symbol
⋯ 1 unchanged line ⋯
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-symbol",-  "version": "1.1.11",+  "version": "1.1.12",   "type": "app",   "description": "KiCad symbol creator with interactive viewer and delivery to KiCad/Fusion 360",   "dependencies": {
src/render.rs+45−3
@@ -339,8 +339,8 @@ fn inline_outline_markup(block: &str, overlay_svg: &str, frac: f64) -> Option<St 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;+    const CHAR_W: f64 = crate::sym_gen::NAME_CHAR_W;+    const OFFSET: f64 = crate::sym_gen::NAME_EXT_OFFSET;     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);@@ -356,6 +356,22 @@ pub fn free_body_rect(block: &str) -> Option<[f64; 4]> {             _ => {}         }     }+    // Group labels (0.762 mm, 1.02 mm inside the edge): rot 0 + justify left =+    // left side, justify right = right side; rot 900 + justify right = top+    // (extends down), justify left = bottom (extends up).+    let lre = Regex::new(r#"(?s)\(text "((?:[^"\\]|\\.)*)"\s*\(at (-?[\d.]+) (-?[\d.]+) (-?[\d.]+)\)\s*\(effects.*?\(justify (left|right)\)"#).unwrap();+    for c in lre.captures_iter(block) {+        let n = c[1].chars().count() as f64;+        let ext = n * crate::sym_gen::LABEL_CHAR_W + crate::sym_gen::LABEL_INSET;+        let rot = c[4].parse::<f64>().unwrap_or(0.0);+        match (rot as i64, &c[5]) {+            (0, "left") => l = l.max(sx.min(ex) + ext),+            (0, "right") => r = r.min(sx.max(ex) - ext),+            (900, "right") => t = t.min(sy.max(ey) - ext),+            (900, "left") => 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)]);@@ -376,7 +392,18 @@ pub fn outline_box(block: &str, frac: f64) -> Option<(f64, f64, f64, f64, [f64;         }     };     let (bw, bh) = (rect[2] - rect[0], rect[3] - rect[1]);-    let free = free_body_rect(block).unwrap_or(rect);+    let mut free = free_body_rect(block).unwrap_or(rect);+    // A visible Reference / Value placed INSIDE the body (square layouts) takes+    // the top / bottom band of the free rect; the icon goes between them.+    for field in ["Reference", "Value"] {+        if prop_hidden(block, field) { continue; }+        if let Some((x, y)) = prop_at(block, field) {+            if x > rect[0] && x < rect[2] && y > rect[1] && y < rect[3] {+                let cy = (free[1] + free[3]) / 2.0;+                if y >= cy { free[3] = free[3].min(y - 1.3); } else { free[1] = free[1].max(y + 1.3); }+            }+        }+    }     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);@@ -735,6 +762,21 @@ pub fn build_outline_contract(         "default_variant": variant,         "body_fraction": round4(place.frac),         "segments": segs,+        // Text-free rectangle inside the body (kicad-symbol-local mm, Y-up):+        // [left, bottom, right, top]. Reference/Value go inside it on layouts+        // with top/bottom pins (Reference at its top, Value at its bottom,+        // 1.6 mm in); the chip icon is fitted to `icon_rect` between them.+        "free_rect": free_body_rect(block).map(|r| r.iter().map(|v| round4(*v)).collect::<Vec<_>>()),+        // Where adom-symbol put the Reference / Value (kicad-symbol-local mm,+        // Y-up, text centre). `inside_body` = the square-layout placement;+        // adom-lbr reads these for >NAME / >VALUE so both sides agree.+        "designators": {+            "reference": if prop_hidden(block, "Reference") { None } else { prop_at(block, "Reference").map(|(x, y)| vec![round4(x), round4(y)]) },+            "value": if prop_hidden(block, "Value") { None } else { prop_at(block, "Value").map(|(x, y)| vec![round4(x), round4(y)]) },+            "inside_body": prop_at(block, "Reference").map(|(x, y)| x > place.body_rect[0] && x < place.body_rect[2] && y > place.body_rect[1] && y < place.body_rect[3]).unwrap_or(false),+            "inset_mm": crate::sym_gen::DESIGNATOR_INSET,+        },+        "icon_rect": outline_box(block, frac).map(|(cx, cy, w, h, _)| vec![round4(cx - w / 2.0), round4(cy - h / 2.0), round4(cx + w / 2.0), round4(cy + h / 2.0)]),         "preserve_aspect_ratio": "xMidYMid meet (the SQUARE source canvas fits the body box — not the ink bbox)",         // The frame adom-lbr wants: KiCad/EAGLE symbol-local mm, Y-UP, origin at         // the symbol origin (same numbers as the .kicad_sym body rectangle).
src/sym_gen.rs+75−6
@@ -24,6 +24,16 @@ const BODY_PADDING: f64 = 4.0; const FS_PIN_NAME: f64 = 1.27; const FS_GROUP_LABEL: f64 = 0.762; +/// Free-rectangle formula, shared with `render::free_body_rect` so the+/// generator, the chip-icon fit, and adom-lbr (via the outline contract)+/// all agree on the text-free area inside the body.+pub const NAME_CHAR_W: f64 = 1.5;          // measured glyph advance of KiCad's 1.27 mm pin-name font+pub const NAME_EXT_OFFSET: f64 = 1.016 + 0.6; // KiCad pin-name offset + margin+pub const LABEL_CHAR_W: f64 = 0.9;         // measured glyph advance of the 0.762 mm group-label font+pub const LABEL_INSET: f64 = 1.02;         // group labels sit 1.02 mm inside the body edge+/// Reference/Value inside the free rect: distance from the free edge to the text centre.+pub const DESIGNATOR_INSET: f64 = 2.54; // one text height (1.27) + one text height of gap+ #[derive(Clone)] pub struct Pin {     pub name: String,@@ -147,10 +157,20 @@ struct Clear { /// `clr` for the "out" positions so they don't collide with pins. /// Positions: out-top / in-top / out-bot / in-bot / out-left / in-left / /// out-right / in-right / center. Returns (x, y, justify-suffix).-fn designator_at(pos: &str, bl: f64, br: f64, bt: f64, bb: f64, clr: &Clear) -> (f64, f64, &'static str) {+fn designator_at(pos: &str, bl: f64, br: f64, bt: f64, bb: f64, clr: &Clear, free: &[f64; 4]) -> (f64, f64, &'static str) {     let cy = (bt + bb) / 2.0;+    let fcx = (free[0] + free[2]) / 2.0;     match pos {         "center" => (0.0, cy, ""),+        // Inside the body, in the text-free rectangle (layouts with top/bottom+        // pins: outside placement would sit on the pin stubs, so every wire to+        // those pins would run through the label).+        "free-top" => (fcx, free[3] - DESIGNATOR_INSET, ""),+        "free-bot" | "free-bottom" => (fcx, free[1] + DESIGNATOR_INSET, ""),+        // Outside corners beyond the top/bottom pin stubs (fallback when the+        // free rect is too short for both labels).+        "corner-top" => (bl + 1.0, clr.top, " (justify left)"),+        "corner-bot" | "corner-bottom" => (bl + 1.0, clr.bot, " (justify left)"),         "in-top" => (0.0, bt - 2.2, ""),         "out-bot" | "out-bottom" => (0.0, clr.bot, ""),         "in-bot" | "in-bottom" => (0.0, bb + 2.2, ""),@@ -393,6 +413,14 @@ pub fn generate_ic_sym(input: &IcInput) -> String {     let top_width = top_pin_count as f64 * PIN_PITCH + top_group_gaps + top_label_offsets + BODY_PADDING * 2.0;     let bottom_width = bottom_pin_count as f64 * PIN_PITCH + bottom_group_gaps + bottom_label_offsets + BODY_PADDING * 2.0; +    // Layouts with top/bottom pins put Reference + Value INSIDE the body, so+    // the text-free rectangle must be wide enough for the Value text between+    // the left and right pin-name columns.+    if !top.is_empty() || !bottom.is_empty() {+        let value_len = input.symbol_name.chars().count().max(input.reference_prefix.chars().count() + 1) as f64;+        let need = (longest_left + longest_right + value_len) * NAME_CHAR_W + 2.0 * NAME_EXT_OFFSET + 2.0;+        name_width = name_width.max(need);+    }     let body_width = name_width.max(MIN_BODY_WIDTH).max(top_width).max(bottom_width);     let half_width = body_width / 2.0; @@ -433,6 +461,34 @@ pub fn generate_ic_sym(input: &IcInput) -> String {     let lowest_pin_y = if all_pin_ys.is_empty() { -10.0 } else { all_pin_ys.iter().cloned().fold(f64::INFINITY, f64::min) };     let body_bottom = lowest_pin_y - BODY_BOTTOM_MARGIN - bottom_pin_clearance; +    // Text-free rectangle inside the body: body pulled in on each side by that+    // side's longest pin name (and group label). Same formula as+    // render::free_body_rect; exposed to adom-lbr through the outline contract.+    let free: [f64; 4] = {+        let ext_name = |n: usize| n as f64 * NAME_CHAR_W + NAME_EXT_OFFSET;+        let ext_label = |n: usize| n as f64 * LABEL_CHAR_W + LABEL_INSET;+        let strip = |s: &str| s.replace("~{", "").replace('}', "").chars().count();+        let mut fl = body_left;+        let mut fr = body_right;+        let mut ft = body_top;+        let mut fb = body_bottom;+        for p in &left_pins { fl = fl.max(body_left + ext_name(strip(&p.name))); }+        for l in &left_labels { fl = fl.max(body_left + ext_label(l.name.chars().count())); }+        for p in &right_pins { fr = fr.min(body_right - ext_name(strip(&p.name))); }+        for l in &right_labels { fr = fr.min(body_right - ext_label(l.name.chars().count())); }+        for p in &top_pins { ft = ft.min(body_top - ext_name(strip(&p.name))); }+        for l in &top_labels { ft = ft.min(body_top - ext_label(l.name.chars().count())); }+        for p in &bottom_pins { fb = fb.max(body_bottom + ext_name(strip(&p.name))); }+        for l in &bottom_labels { fb = fb.max(body_bottom + ext_label(l.name.chars().count())); }+        if fr - fl < 2.0 || ft - fb < 2.0 { [body_left, body_bottom, body_right, body_top] } else { [fl, fb, fr, ft] }+    };+    // Inside placement needs room for both labels with their margins; if the+    // free rect is too short, fall back to the outside corners beyond the+    // top/bottom pin stubs (left corner, so wires to the centred stubs stay clear).+    let has_vertical_pins = (!top_pins.is_empty() || !bottom_pins.is_empty())+        && (free[3] - free[1]) >= 2.0 * DESIGNATOR_INSET + 2.0 * FS_PIN_NAME; // two labels + a text height between them+    let corner_fallback = (!top_pins.is_empty() || !bottom_pins.is_empty()) && !has_vertical_pins;+     let mut lines: Vec<String> = Vec::new();     lines.push("(kicad_symbol_lib".to_string());     lines.push("  (version 20231120)".to_string());@@ -456,8 +512,16 @@ pub fn generate_ic_sym(input: &IcInput) -> String {      lines.push(format!("    (property \"Reference\" \"{}\"", input.reference_prefix));     {-        let pos = if input.ref_pos.is_empty() { "out-top" } else { input.ref_pos.as_str() };-        let (rx, ry, just) = designator_at(pos, body_left, body_right, body_top, body_bottom, &clr);+        // Default placement: above the body, EXCEPT when pins sit on the top or+        // bottom (square layouts): then inside the free rect, so nothing but+        // pins is outside the body and wires stay clear (John, 2026-08-30).+        let pos = match input.ref_pos.as_str() {+            "" | "top" | "out-top" if has_vertical_pins => "free-top",+            "" | "top" | "out-top" if corner_fallback => "corner-top",+            "" => "out-top",+            p => p,+        };+        let (rx, ry, just) = designator_at(pos, body_left, body_right, body_top, body_bottom, &clr, &free);         lines.push(format!("      (at {} {} 0)", fx(rx), fx(ry)));         lines.push(format!("      (effects (font (size 1.27 1.27)){just})"));     }@@ -468,12 +532,17 @@ pub fn generate_ic_sym(input: &IcInput) -> String {         // "none" hides the Value (e.g. the chip name is laser-marked on the 3D         // chip instead). KiCad requires the property to exist, so emit it hidden.         if input.value_pos == "none" {-            let (vx, vy, _) = designator_at("out-bot", body_left, body_right, body_top, body_bottom, &clr);+            let (vx, vy, _) = designator_at("out-bot", body_left, body_right, body_top, body_bottom, &clr, &free);             lines.push(format!("      (at {} {} 0)", fx(vx), fx(vy)));             lines.push("      (effects (font (size 1.27 1.27)) hide)".to_string());         } else {-            let pos = if input.value_pos.is_empty() { "out-bot" } else { input.value_pos.as_str() };-            let (vx, vy, just) = designator_at(pos, body_left, body_right, body_top, body_bottom, &clr);+            let pos = match input.value_pos.as_str() {+                "" | "out-bot" | "out-bottom" if has_vertical_pins => "free-bot",+                "" | "out-bot" | "out-bottom" if corner_fallback => "corner-bot",+                "" => "out-bot",+                p => p,+            };+            let (vx, vy, just) = designator_at(pos, body_left, body_right, body_top, body_bottom, &clr, &free);             lines.push(format!("      (at {} {} 0)", fx(vx), fx(vy)));             lines.push(format!("      (effects (font (size 1.27 1.27)){just})"));         }
tests/golden/TST-ALL.kicad_sym+10−10
@@ -7,12 +7,12 @@     (in_bom yes)     (on_board yes)     (property "Reference" "U"-      (at 0.00 16.64 0)-      (effects (font (size 1.27 1.27)))+      (at -11.70 16.64 0)+      (effects (font (size 1.27 1.27)) (justify left))     )     (property "Value" "TST-ALL"-      (at 0.00 -20.79 0)-      (effects (font (size 1.27 1.27)))+      (at -11.70 -20.79 0)+      (effects (font (size 1.27 1.27)) (justify left))     )     (property "Footprint" "QFN-16"       (at 0 0 0)@@ -35,14 +35,14 @@       (effects (font (size 1.27 1.27)) (hide yes))     )     (symbol "TST-ALL_0_1"-      (rectangle (start -10.16 8.10) (end 10.16 -12.25)+      (rectangle (start -12.70 8.10) (end 12.70 -12.25)         (stroke (width 0.254) (type default))         (fill (type background))       )-      (text "IO" (at -9.14 0.00 0)+      (text "IO" (at -11.68 0.00 0)         (effects (font (size 0.762 0.762) (color 132 0 0 1)) (justify left))       )-      (text "Out" (at 9.14 0.00 0)+      (text "Out" (at 11.68 0.00 0)         (effects (font (size 0.762 0.762) (color 132 0 0 1)) (justify right))       )       (text "Power" (at -2.27 7.08 900)@@ -53,9 +53,9 @@       )     )     (symbol "TST-ALL_1_1"-      (pin bidirectional line (at -12.70 -2.00 0) (length 2.54) (name "SDA") (number "1"))-      (pin bidirectional line (at -12.70 -4.54 0) (length 2.54) (name "SCL") (number "2"))-      (pin output line (at 12.70 -2.00 180) (length 2.54) (name "TX") (number "5"))+      (pin bidirectional line (at -15.24 -2.00 0) (length 2.54) (name "SDA") (number "1"))+      (pin bidirectional line (at -15.24 -4.54 0) (length 2.54) (name "SCL") (number "2"))+      (pin output line (at 15.24 -2.00 180) (length 2.54) (name "TX") (number "5"))       (pin power_in line (at -0.27 10.54 270) (length 2.54) (name "V3V3") (number "16"))       (pin power_in line (at -0.27 -14.69 90) (length 2.54) (name "GND") (number "8"))     )