diff --git a/crates/altium-codec/src/altium_decode.rs b/crates/altium-codec/src/altium_decode.rs index 5cb462b..e343d3d 100644 --- a/crates/altium-codec/src/altium_decode.rs +++ b/crates/altium-codec/src/altium_decode.rs @@ -354,6 +354,7 @@ fn decode_sym_graphic(txt: &str, rec: &str) -> Option { y2: coord("Corner.Y"), width, filled: txt.contains("|IsSolid=T"), + fill_style: None, }), "11" => Some(Graphic::Circle { cx: coord("Location.X"), diff --git a/crates/altium-codec/src/kicad.rs b/crates/altium-codec/src/kicad.rs index 97ec9e4..5130db3 100644 --- a/crates/altium-codec/src/kicad.rs +++ b/crates/altium-codec/src/kicad.rs @@ -186,7 +186,14 @@ fn import_symbol(text: &str, wanted: &str) -> Result { texts: import_symbol_texts(drawing), designator_prefix: Some(prefix), footprint: None, - parameters: Vec::new(), + parameters: props.iter().filter_map(|p| { + let name = p.atom_at(1)?; + if matches!(name, "Reference" | "Footprint") { return None; } + let effects = p.child("effects"); + let hidden = effects.map(|e| e.child("hide").and_then(|h| h.atom_at(1)) == Some("yes") + || e.items().iter().any(|i| matches!(i, Sx::Atom(a) if a == "hide"))).unwrap_or(false); + Some(crate::model::Parameter { name: name.into(), value: p.atom_at(2)?.into(), visible: !hidden }) + }).collect(), pin_numbers_hidden: Some(has_hide("pin_numbers")), pin_names_hidden: Some(has_hide("pin_names")), }) @@ -259,6 +266,7 @@ fn import_symbol_graphics(root: &Sx) -> Vec { out.push(Graphic::Rect { x1: to_mil(s[0]), y1: to_mil(s[1]), x2: to_mil(e[0]), y2: to_mil(e[1]), width: width_of(g), filled: filled_of(g), + fill_style: g.child("fill").and_then(|f| f.child("type")).and_then(|t| t.atom_at(1)).map(str::to_owned), }); } } diff --git a/crates/altium-codec/src/kicad_export.rs b/crates/altium-codec/src/kicad_export.rs index 7fb021d..76d3a44 100644 --- a/crates/altium-codec/src/kicad_export.rs +++ b/crates/altium-codec/src/kicad_export.rs @@ -151,7 +151,7 @@ pub fn export_kicad_symbol_for(sym: &Symbol, ver: KicadVersion) -> String { s.push_str("\t\t(exclude_from_sim no)\n"); } s.push_str("\t\t(in_bom yes)\n\t\t(on_board yes)\n"); - // properties (Reference above the symbol, Value below; metadata hidden at origin) + // Keep hidden metadata outside the body: native library editors can display hidden fields. let prop = |k: &str, v: &str, x: f64, y: f64, hidden: bool| { let h = if hidden { hide } else { "" }; format!( @@ -161,13 +161,16 @@ pub fn export_kicad_symbol_for(sym: &Symbol, ver: KicadVersion) -> String { }; s.push_str(&prop("Reference", &reference, cx, top + 1.27, false)); s.push_str(&prop("Value", &value, cx, bottom - 1.27, false)); - s.push_str(&prop("Footprint", &footprint, 0.0, 0.0, true)); + s.push_str(&prop("Footprint", &footprint, cx, bottom - 3.81, true)); // "" not "~": since format 20250318 a tilde is a literal tilde. - s.push_str(&prop("Datasheet", "", 0.0, 0.0, true)); - // any extra parameters become hidden properties (at origin) + let datasheet = sym.parameters.iter().find(|p| p.name.eq_ignore_ascii_case("Datasheet")); + s.push_str(&prop("Datasheet", datasheet.map(|p| p.value.as_str()).unwrap_or(""), cx, bottom - 6.35, true)); + // Preserve visibility and space metadata rows for the native editor's hidden-field view. + let mut metadata_y = bottom - 8.89; for p in &sym.parameters { - if !p.name.eq_ignore_ascii_case("Value") { - s.push_str(&prop(&p.name, &p.value, 0.0, 0.0, !p.visible)); + if !["Value", "Reference", "Footprint", "Datasheet"].iter().any(|key| p.name.eq_ignore_ascii_case(key)) { + s.push_str(&prop(&p.name, &p.value, cx, metadata_y, !p.visible)); + metadata_y -= 2.54; } } @@ -212,27 +215,31 @@ pub fn export_kicad_symbol_for(sym: &Symbol, ver: KicadVersion) -> String { /// One symbol body graphic → KiCad s-expr (mils→mm via `m`). fn sym_graphic(g: &Graphic, m: &dyn Fn(f64) -> String) -> String { - let stroke_fill = |w: f64, filled: bool| { - let fill = if filled { "outline" } else { "none" }; + let stroke_fill = |w: f64, fill: &str| { format!( "\t\t\t\t(stroke (width {}) (type default))\n\t\t\t\t(fill (type {fill}))\n", n((w * MM_PER_MIL).max(0.0)) ) }; match g { - Graphic::Rect { x1, y1, x2, y2, width, filled } => format!( + Graphic::Rect { x1, y1, x2, y2, width, filled, fill_style } => format!( "\t\t\t(rectangle\n\t\t\t\t(start {} {})\n\t\t\t\t(end {} {})\n{}\t\t\t)\n", - m(*x1), m(*y1), m(*x2), m(*y2), stroke_fill(*width, *filled) + m(*x1), m(*y1), m(*x2), m(*y2), stroke_fill(*width, match fill_style.as_deref() { + Some("background") => "background", + Some("outline") => "outline", + Some("none") => "none", + _ => if *filled { "outline" } else { "none" }, + }) ), Graphic::Circle { cx, cy, r, width } => format!( "\t\t\t(circle\n\t\t\t\t(center {} {})\n\t\t\t\t(radius {})\n{}\t\t\t)\n", - m(*cx), m(*cy), m(*r), stroke_fill(*width, false) + m(*cx), m(*cy), m(*r), stroke_fill(*width, "none") ), Graphic::Arc { cx, cy, r, start, end, width } => { let (s, mid, e) = arc_points(*cx, *cy, *r, *start, *end); format!( "\t\t\t(arc\n\t\t\t\t(start {} {})\n\t\t\t\t(mid {} {})\n\t\t\t\t(end {} {})\n{}\t\t\t)\n", - m(s.0), m(s.1), m(mid.0), m(mid.1), m(e.0), m(e.1), stroke_fill(*width, false) + m(s.0), m(s.1), m(mid.0), m(mid.1), m(e.0), m(e.1), stroke_fill(*width, "none") ) } Graphic::Polyline { points, width } => { @@ -242,7 +249,7 @@ fn sym_graphic(g: &Graphic, m: &dyn Fn(f64) -> String) -> String { } format!( "\t\t\t(polyline\n\t\t\t\t(pts\n{pts}\t\t\t\t)\n{}\t\t\t)\n", - stroke_fill(*width, false) + stroke_fill(*width, "none") ) } } diff --git a/crates/altium-codec/src/lib.rs b/crates/altium-codec/src/lib.rs index fda14c7..65572b7 100644 --- a/crates/altium-codec/src/lib.rs +++ b/crates/altium-codec/src/lib.rs @@ -48,6 +48,46 @@ mod tests { } } + #[test] + fn background_and_outline_fill_remain_distinct_through_canonical_json() { + for style in ["background", "outline", "none"] { + let source = format!(r#"(kicad_symbol_lib (version 20231120) (generator "test") + (symbol "FILL" (symbol "FILL_0_1" + (rectangle (start -2.54 2.54) (end 2.54 -2.54) + (stroke (width 0.254) (type default)) (fill (type {style}))))))"#); + let sym = import_kicad(&source, r#"(footprint "FILL" (layer "F.Cu"))"#, "FILL").unwrap().symbol; + let json = serde_json::to_string(&sym).unwrap(); + let canonical: Symbol = serde_json::from_str(&json).unwrap(); + let out = export_kicad_symbol(&canonical); + assert!(out.contains(&format!("(fill (type {style}))")), "{out}"); + } + // Older libraries have only the boolean; retain their established rendering. + let legacy: Graphic = serde_json::from_str(r#"{"kind":"rect","x1":0,"y1":0,"x2":100,"y2":100,"filled":true}"#).unwrap(); + let mut sym = two_pin_symbol(); + sym.graphics.push(legacy); + assert!(export_kicad_symbol(&sym).contains("(fill (type outline))")); + } + + #[test] + fn datasheet_and_metadata_survive_kicad_without_duplicate_properties() { + let source = r#"(kicad_symbol_lib (version 20231120) (generator "test") + (symbol "META" (property "Reference" "U" (at 0 0 0)) + (property "Datasheet" "https://manufacturer.example/datasheet.pdf" (at 0 0 0) (effects (font (size 1 1)) hide)) + (property "Manufacturer" "Maker" (at 0 0 0) (effects (font (size 1 1)) (hide yes))) + (property "VisibleNote" "Read me" (at 0 0 0) (effects (font (size 1 1))))))"#; + let sym = import_kicad(source, r#"(footprint "META" (layer "F.Cu"))"#, "META").unwrap().symbol; + assert_eq!(sym.parameters.len(), 3); + assert!(!sym.parameters[0].visible); + assert!(!sym.parameters[1].visible); + assert!(sym.parameters[2].visible); + let out = export_kicad_symbol(&sym); + assert_eq!(out.matches("(property \"Datasheet\"").count(), 1); + assert!(out.contains("https://manufacturer.example/datasheet.pdf")); + let back = import_kicad(&out, r#"(footprint "META" (layer "F.Cu"))"#, "META").unwrap().symbol; + assert_eq!(back.parameters.iter().find(|p| p.name == "Manufacturer").unwrap().value, "Maker"); + assert!(back.parameters.iter().find(|p| p.name == "VisibleNote").unwrap().visible); + } + #[test] fn grouped_labels_survive_kicad_and_refuse_lossy_altium_export() { let source = r#"(kicad_symbol_lib (version 20231120) (generator "test") diff --git a/crates/altium-codec/src/model.rs b/crates/altium-codec/src/model.rs index da126fd..705aa81 100644 --- a/crates/altium-codec/src/model.rs +++ b/crates/altium-codec/src/model.rs @@ -104,7 +104,9 @@ pub enum Graphic { /// Connected line segments through `points` (mils). Polyline { points: Vec<[f64; 2]>, #[serde(default)] width: f64 }, /// Axis-aligned rectangle (mils). - Rect { x1: f64, y1: f64, x2: f64, y2: f64, #[serde(default)] width: f64, #[serde(default)] filled: bool }, + Rect { x1: f64, y1: f64, x2: f64, y2: f64, #[serde(default)] width: f64, #[serde(default)] filled: bool, + /// Preserve background versus outline fill through canonical JSON. + #[serde(default, skip_serializing_if = "Option::is_none")] fill_style: Option }, /// Circle (mils). Circle { cx: f64, cy: f64, r: f64, #[serde(default)] width: f64 }, /// Arc, angles in degrees (mils).