app
Adom Symbol
Public Made by Adomby adom
KiCad symbol creator with interactive viewer and delivery to KiCad/Fusion 360
← 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
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")) )