← Commit history

v1.1.7: discrete symbols — short Value + baked offset 3D-chip icon; render.rs uses Value not MPN

John Lauer ·829912d191 ·3mo ago ·parent 493bf81
4 files changed +166−7
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-symbol"-version = "1.1.6"+version = "1.1.7" edition = "2021" description = "Create and preview KiCad/Fusion 360 schematic symbols with interactive viewer" 
VERSION+1−1
@@ -1 +1 @@-1.1.6+1.1.7
src/discrete_gen.rs+103−5
@@ -24,6 +24,15 @@ pub struct DiscreteInput {     pub baseline_symbol: String,     /// (old_number,new_number) and/or (old_name,new_name)     pub pin_overrides: Vec<PinOverride>,+    /// Short, human on-canvas Value (e.g. "33Ω", "22nF", "Red"). Empty → fall+    /// back to the symbol name (the old behavior). The MPN always survives in the+    /// symbol name + a hidden `MPN` property, so BOM/adom-lbr still get it.+    pub value: String,+    /// Pre-scaled, pre-offset 3D-chip outline polylines in KiCad mm (y-up),+    /// ready to bake beside the symbol shape as thin `(polyline …)` geometry.+    /// Empty → no icon. This is the small (~30%) physical-package thumbnail so+    /// SMD-vs-through-hole and part type read at a glance on the schematic.+    pub outline_polylines: Vec<Vec<(f64, f64)>>, }  #[derive(Default)]@@ -71,7 +80,13 @@ pub fn generate_discrete_sym(     };      // Properties come from the requested symbol's block; input overrides win.-    let prop_value = input.symbol_name.clone();+    // Value is the SHORT human value (33Ω/22nF/Red) when provided — never the+    // MPN. The MPN stays as the symbol name + a hidden `MPN` property below.+    let prop_value = if input.value.is_empty() {+        input.symbol_name.clone()+    } else {+        input.value.clone()+    };     let prop_footprint = first_nonempty(&[         Some(input.package.clone()),         extract_property(&baseline, "Footprint"),@@ -81,10 +96,27 @@ pub fn generate_discrete_sym(     let prop_description = first_nonempty(&[Some(input.description.clone()), extract_property(&baseline, "Description")]);     let prop_manufacturer = first_nonempty(&[Some(input.manufacturer.clone()), extract_property(&baseline, "Manufacturer")]); -    let ref_at = extract_property_at(&graphics_source, "Reference").unwrap_or_else(|| "0 2.54 0".to_string());-    let val_at = extract_property_at(&graphics_source, "Value").unwrap_or_else(|| "0 -2.54 0".to_string());-    let ref_effects = extract_property_effects(&graphics_source, "Reference").unwrap_or_else(|| "(effects (font (size 1.27 1.27)))".to_string());-    let val_effects = extract_property_effects(&graphics_source, "Value").unwrap_or_else(|| "(effects (font (size 1.27 1.27)))".to_string());+    // Clean-discrete mode (a short value or an outline icon was supplied): place+    // Reference + Value at consistent spots CLEAR of the body — Reference+    // upper-right, Value lower-right, left-justified — instead of the KiCad+    // baseline's `(0 0 90)` Value that sits on top of the shape. Zero overlap,+    // identical placement on every discrete.+    let clean = !input.value.is_empty() || !input.outline_polylines.is_empty();+    let (ref_at, val_at, ref_effects, val_effects) = if clean {+        (+            "2.54 1.0 0".to_string(),+            "2.54 -1.0 0".to_string(),+            "(effects (font (size 1.27 1.27)) (justify left))".to_string(),+            "(effects (font (size 1.27 1.27)) (justify left))".to_string(),+        )+    } else {+        (+            extract_property_at(&graphics_source, "Reference").unwrap_or_else(|| "0 2.54 0".to_string()),+            extract_property_at(&graphics_source, "Value").unwrap_or_else(|| "0 -2.54 0".to_string()),+            extract_property_effects(&graphics_source, "Reference").unwrap_or_else(|| "(effects (font (size 1.27 1.27)))".to_string()),+            extract_property_effects(&graphics_source, "Value").unwrap_or_else(|| "(effects (font (size 1.27 1.27)))".to_string()),+        )+    };      // Rename the sub-symbol blocks to the new symbol name.     let mut renamed_graphics = graphics_block.replacen(@@ -154,6 +186,18 @@ pub fn generate_discrete_sym(         lines.push("      (effects (font (size 1.27 1.27)) (hide yes))".to_string());         lines.push("    )".to_string());     }+    // Keep the full MPN discoverable (BOM / adom-lbr) even though the on-canvas+    // Value is now the short human value.+    lines.push(format!("    (property \"MPN\" \"{}\"", input.symbol_name));+    lines.push("      (at 0 0 0)".to_string());+    lines.push("      (effects (font (size 1.27 1.27)) (hide yes))".to_string());+    lines.push("    )".to_string());++    // Bake the small offset 3D-chip outline into the _0_1 graphics sub-symbol as+    // real thin polyline geometry (survives to KiCad + adom-lbr — not an <image>).+    if !input.outline_polylines.is_empty() {+        renamed_graphics = inject_outline(&renamed_graphics, &input.outline_polylines);+    }      lines.push(renamed_graphics);     lines.push(renamed_pins);@@ -164,6 +208,33 @@ pub fn generate_discrete_sym(     Ok(lines.join("\n")) } +/// Insert the outline polylines into the `(symbol "..._0_1" …)` graphics block,+/// just before its closing paren, so KiCad renders them alongside the shape.+fn inject_outline(graphics: &str, polys: &[Vec<(f64, f64)>]) -> String {+    let mut inject = String::new();+    for pl in polys {+        if pl.len() < 2 {+            continue;+        }+        let pts: String = pl+            .iter()+            .map(|(x, y)| format!("(xy {:.3} {:.3})", x, y))+            .collect::<Vec<_>>()+            .join(" ");+        inject.push_str(&format!(+            "\n      (polyline (pts {}) (stroke (width 0.12) (type default)) (fill (type none)))",+            pts+        ));+    }+    if inject.is_empty() {+        return graphics.to_string();+    }+    match graphics.rfind(')') {+        Some(idx) => format!("{}{}\n     )", graphics[..idx].trim_end(), inject),+        None => graphics.to_string(),+    }+}+ fn first_nonempty(opts: &[Option<String>]) -> String {     for o in opts {         if let Some(s) = o {@@ -300,6 +371,8 @@ mod tests {             baseline_library: "Device".into(),             baseline_symbol: "R".into(),             pin_overrides: vec![],+            value: String::new(),+            outline_polylines: vec![],         };         let out = generate_discrete_sym(&input, &fixture_fetch).unwrap(); @@ -308,9 +381,34 @@ mod tests {         assert!(out.contains("(symbol \"R-TEST-10K_0_1\""), "renamed graphics sub-symbol");         assert!(out.contains("(symbol \"R-TEST-10K_1_1\""), "renamed pins sub-symbol");         assert!(out.contains("(property \"Reference\" \"R\""), "reference prefix R");+        // value empty → falls back to the symbol name (MPN)         assert!(out.contains("(property \"Value\" \"R-TEST-10K\""));         assert!(out.contains("(property \"Footprint\" \"R_0402_1005Metric\""));         assert!(!out.contains("C_Feedthrough"), "must not leak the C_Feedthrough baseline");         assert!(out.starts_with("(kicad_symbol_lib"));     }++    #[test]+    fn short_value_and_outline_are_applied() {+        let input = DiscreteInput {+            symbol_name: "0603WAF330JT5E".into(),+            manufacturer: "UNI-ROYAL".into(),+            package: "R_0603_1608Metric".into(),+            description: "33Ω 1% 0603".into(),+            datasheet_url: "".into(),+            reference_prefix: Some("R".into()),+            baseline_library: "Device".into(),+            baseline_symbol: "R".into(),+            pin_overrides: vec![],+            value: "33Ω".into(),+            outline_polylines: vec![vec![(-4.0, 1.0), (-3.0, 1.0), (-3.0, -1.0)]],+        };+        let out = generate_discrete_sym(&input, &fixture_fetch).unwrap();+        // short human value on canvas, MPN preserved in a hidden property+        assert!(out.contains("(property \"Value\" \"33Ω\""), "on-canvas value is short");+        assert!(!out.contains("(property \"Value\" \"0603WAF330JT5E\""), "MPN not the value");+        assert!(out.contains("(property \"MPN\" \"0603WAF330JT5E\""), "MPN preserved hidden");+        // the offset 3D-chip outline is baked as real polyline geometry+        assert!(out.contains("(polyline (pts (xy -4.000 1.000)"), "outline baked into graphics");+    } }
src/main.rs+61
@@ -143,6 +143,13 @@ struct CreateInput {     pin_overrides: Vec<JsonPinOverride>,     #[serde(default)]     preferences: Preferences,+    /// Short human value for discretes (e.g. "33Ω"). Empty → symbol name.+    #[serde(default)]+    value: String,+    /// Path to a `<mpn>-3d-outline-*.svg` wireframe to bake as a small offset+    /// 3D-chip icon beside a discrete symbol.+    #[serde(default, rename = "outlineSvg")]+    outline_svg: Option<String>, }  fn default_category() -> String {@@ -264,6 +271,55 @@ fn run() -> Result<()> {     } } +/// Parse a `<mpn>-3d-outline-*.svg` (polyline wireframe in a 0..N viewBox) into+/// KiCad-mm polylines scaled to ~30% (a ~3 mm box), offset to the LEFT of the+/// symbol body, y-flipped — the small 3D-chip icon baked beside a discrete.+fn parse_outline_svg(path: &str) -> Result<Vec<Vec<(f64, f64)>>> {+    const OUT_BOX: f64 = 3.0;+    const OUT_CX: f64 = -3.7;+    const OUT_CY: f64 = 0.0;+    let svg = fs::read_to_string(path).with_context(|| format!("reading {path}"))?;+    let re = regex::Regex::new(r#"points="([^"]+)""#).unwrap();+    let mut polys: Vec<Vec<(f64, f64)>> = Vec::new();+    for c in re.captures_iter(&svg) {+        let mut pts = Vec::new();+        for pair in c[1].split_whitespace() {+            let mut it = pair.split(',');+            if let (Some(a), Some(b)) = (it.next(), it.next()) {+                if let (Ok(x), Ok(y)) = (a.parse::<f64>(), b.parse::<f64>()) {+                    pts.push((x, y));+                }+            }+        }+        if pts.len() >= 2 {+            polys.push(pts);+        }+    }+    if polys.is_empty() {+        return Ok(vec![]);+    }+    let (mut minx, mut maxx, mut miny, mut maxy) = (f64::MAX, f64::MIN, f64::MAX, f64::MIN);+    for pl in &polys {+        for &(x, y) in pl {+            minx = minx.min(x);+            maxx = maxx.max(x);+            miny = miny.min(y);+            maxy = maxy.max(y);+        }+    }+    let (w, h) = (maxx - minx, maxy - miny);+    let s = OUT_BOX / w.max(h).max(1e-6);+    let (cx, cy) = ((minx + maxx) / 2.0, (miny + maxy) / 2.0);+    Ok(polys+        .into_iter()+        .map(|pl| {+            pl.into_iter()+                .map(|(x, y)| (OUT_CX + (x - cx) * s, OUT_CY - (y - cy) * s))+                .collect()+        })+        .collect())+}+ fn cmd_create(file: Option<PathBuf>, out: Option<PathBuf>) -> Result<()> {     let body = match file {         Some(p) => fs::read_to_string(&p).with_context(|| format!("reading {}", p.display()))?,@@ -303,6 +359,11 @@ fn cmd_create(file: Option<PathBuf>, out: Option<PathBuf>) -> Result<()> {                     new_name: o.new_name.clone(),                 })                 .collect(),+            value: input.value.clone(),+            outline_polylines: match &input.outline_svg {+                Some(p) => parse_outline_svg(p).unwrap_or_default(),+                None => vec![],+            },         };         discrete_gen::generate_discrete_sym(&dinput, &fetch_lib_from_service)?     } else {