app
Adom Symbol
Public Made by Adomby adom
KiCad symbol creator with interactive viewer and delivery to KiCad/Fusion 360
← Commit history
v1.1.7: discrete symbols — short Value + baked offset 3D-chip icon; render.rs uses Value not MPN
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 {