From beae509a66f761a14eadf0c80f0b3ad8ccfccaf6 Mon Sep 17 00:00:00 2001 From: Akhil Velagapudi Date: Sat, 28 Mar 2026 21:23:55 -0400 Subject: [PATCH 1/7] Make KiCad symbol the source of truth for component metadata - Generated .zen files are now minimal wrappers (symbol, pins, pin_defs, optional skip_bom/skip_pos); no longer emit part, datasheet, or footprint. - Component() inherits footprint, datasheet, and part from the symbol and package manifest at evaluation time. - pcb import patches canonical MPN, manufacturer, and datasheet path into the KiCad symbol properties when generating components. - Deduplicate KiCad placeholder checks into shared helpers in pcb-eda. - Add regression test for local symbol datasheet inheritance. --- CHANGELOG.md | 4 + crates/pcb-component-gen/src/lib.rs | 66 +++++-------- .../templates/component.zen.jinja | 24 ++--- crates/pcb-diode-api/src/component.rs | 59 +++++------- crates/pcb-diode-api/src/datasheet.rs | 59 +++++++++--- crates/pcb-eda/src/kicad/symbol.rs | 4 +- crates/pcb-eda/src/lib.rs | 43 ++++++++- crates/pcb-zen-core/src/lang/component.rs | 95 +++++++++++++++---- crates/pcb/src/import/generate.rs | 21 ---- crates/pcb/tests/part.rs | 59 ++++++++++++ docs/pages/spec.mdx | 3 - 11 files changed, 282 insertions(+), 155 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e137bc6bd..85cba6aa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to Semantic Versioning (https://semver.org/spec/v2.0.0. ## [Unreleased] +### Changed + +- KiCad symbol is now the source of truth for component metadata (footprint, datasheet, part); generated `.zen` files are minimal wrappers. + ### Added - Warn when module `io()`s are declared but never connected to any realized ports. diff --git a/crates/pcb-component-gen/src/lib.rs b/crates/pcb-component-gen/src/lib.rs index a77198518..1df6099ac 100644 --- a/crates/pcb-component-gen/src/lib.rs +++ b/crates/pcb-component-gen/src/lib.rs @@ -93,13 +93,9 @@ pub fn sanitize_pin_name(name: &str) -> String { } pub struct GenerateComponentZenArgs<'a> { - pub mpn: &'a str, pub component_name: &'a str, pub symbol: &'a Symbol, pub symbol_filename: &'a str, - pub footprint_filename: Option<&'a str>, - pub datasheet_filename: Option<&'a str>, - pub manufacturer: Option<&'a str>, pub generated_by: &'a str, pub include_skip_bom: bool, pub include_skip_pos: bool, @@ -162,14 +158,10 @@ pub fn generate_component_zen(args: GenerateComponentZenArgs<'_>) -> Result { eprintln!("{} Resolved datasheet from symbol", "✓".green()); @@ -909,7 +911,7 @@ pub fn add_component_to_workspace( &component_dir, &identity.part_number, identity.manufacturer.as_deref(), - include_datasheet_in_zen, + datasheet_ref.as_deref(), )?; if !zen_file.exists() { @@ -949,13 +951,12 @@ fn finalize_component( component_dir: &Path, mpn: &str, manufacturer: Option<&str>, - include_datasheet_in_zen: bool, + datasheet_ref: Option<&str>, ) -> Result<()> { let sanitized_mpn = pcb_component_gen::sanitize_mpn_for_path(mpn); let symbol_path = component_dir.join(format!("{}.kicad_sym", &sanitized_mpn)); let footprint_path = component_dir.join(format!("{}.kicad_mod", &sanitized_mpn)); let step_path = component_dir.join(format!("{}.step", &sanitized_mpn)); - let datasheet_path = component_dir.join(format!("{}.pdf", &sanitized_mpn)); if footprint_path.exists() { if step_path.exists() { @@ -975,6 +976,7 @@ fn finalize_component( &symbol_source, &symbol_path, footprint_stem_if_exists(&footprint_path)?.as_deref(), + datasheet_ref, mpn, manufacturer, )?; @@ -986,18 +988,9 @@ fn finalize_component( let symbol = only_symbol_in_library(&symbol_lib, &symbol_path)?; let content = generate_zen_file( - mpn, &sanitized_mpn, symbol, &format!("{}.kicad_sym", &sanitized_mpn), - footprint_path - .exists() - .then(|| format!("{}.kicad_mod", &sanitized_mpn)) - .as_deref(), - (include_datasheet_in_zen && datasheet_path.exists()) - .then(|| format!("{}.pdf", &sanitized_mpn)) - .as_deref(), - manufacturer, )?; let zen_file = component_dir.join(format!("{}.zen", &sanitized_mpn)); @@ -1035,6 +1028,7 @@ fn rewrite_symbol_component_metadata_text( source: &str, symbol_path: &Path, footprint_ref: Option<&str>, + datasheet_ref: Option<&str>, mpn: &str, manufacturer: Option<&str>, ) -> Result { @@ -1044,10 +1038,14 @@ fn rewrite_symbol_component_metadata_text( })?; let symbol_items = only_symbol_in_library_mut(root, symbol_path)?; - let mut next_properties = symbol_properties(symbol_items); + let current_metadata = SymbolMetadata::from_property_iter(symbol_properties(symbol_items)); + let mut next_properties = current_metadata.to_properties_map(); if let Some(footprint_ref) = footprint_ref { next_properties.insert("Footprint".to_string(), footprint_ref.to_string()); } + if let Some(datasheet_ref) = datasheet_ref { + next_properties.insert("Datasheet".to_string(), datasheet_ref.to_string()); + } next_properties.insert("Manufacturer_Part_Number".to_string(), mpn.to_string()); if let Some(manufacturer) = manufacturer.filter(|m| !m.trim().is_empty()) { next_properties.insert("Manufacturer_Name".to_string(), manufacturer.to_string()); @@ -1144,22 +1142,14 @@ fn write_component_files(component_file: &Path, component_dir: &Path, content: & } fn generate_zen_file( - mpn: &str, component_name: &str, symbol: &pcb_eda::Symbol, symbol_filename: &str, - footprint_filename: Option<&str>, - datasheet_filename: Option<&str>, - manufacturer: Option<&str>, ) -> Result { pcb_component_gen::generate_component_zen(pcb_component_gen::GenerateComponentZenArgs { - mpn, component_name, symbol, symbol_filename, - footprint_filename, - datasheet_filename, - manufacturer, generated_by: "pcb search", include_skip_bom: false, include_skip_pos: false, @@ -1512,7 +1502,13 @@ fn execute_from_dir(dir: &Path, workspace_root: &Path) -> Result<()> { // Finalize: embed STEP, generate .zen file println!("{} Generating .zen file...", "→".blue().bold()); - finalize_component(&component_dir, &mpn, manufacturer.as_deref(), true)?; + let datasheet_ref = has_datasheet.then(|| format!("{}.pdf", &sanitized_mpn)); + finalize_component( + &component_dir, + &mpn, + manufacturer.as_deref(), + datasheet_ref.as_deref(), + )?; // Show result let display_path = zen_file.strip_prefix(workspace_root).unwrap_or(&zen_file); @@ -2467,16 +2463,7 @@ mod tests { ..Default::default() }; - let zen_content = generate_zen_file( - "TEST-MPN", - "TestComponent", - &symbol, - "symbol.kicad_sym", - Some("footprint.kicad_mod"), - None, - Some("TestMfr"), - ) - .unwrap(); + let zen_content = generate_zen_file("TestComponent", &symbol, "symbol.kicad_sym").unwrap(); // The pins dict should NOT have duplicate "GND" keys // Count how many times "GND" appears as a dict key @@ -2559,12 +2546,14 @@ mod tests { symbol, Path::new("TEST.kicad_sym"), Some("NewFootprint"), + Some("NEW-MPN.pdf"), "NEW-MPN", Some("NewMfr"), ) .unwrap(); assert!(updated.contains("(property \"Footprint\" \"NewFootprint\"")); assert!(!updated.contains("OldLib:OldFootprint")); + assert!(updated.contains("(property \"Datasheet\" \"NEW-MPN.pdf\"")); assert!(updated.contains("(property \"Manufacturer_Part_Number\" \"NEW-MPN\"")); assert!(updated.contains("(property \"Manufacturer_Name\" \"NewMfr\"")); } @@ -2581,7 +2570,7 @@ mod tests { nonce )); std::fs::create_dir_all(&dir).unwrap(); - let err = finalize_component(&dir, "MISSING", None, false).unwrap_err(); + let err = finalize_component(&dir, "MISSING", None, None).unwrap_err(); let _ = std::fs::remove_dir_all(&dir); assert!(err.to_string().contains("Expected symbol file not found")); } diff --git a/crates/pcb-diode-api/src/datasheet.rs b/crates/pcb-diode-api/src/datasheet.rs index a1a16af7c..9127d25d4 100644 --- a/crates/pcb-diode-api/src/datasheet.rs +++ b/crates/pcb-diode-api/src/datasheet.rs @@ -99,9 +99,17 @@ pub fn resolve_datasheet( execute_resolve_execution(&client, auth_token, execution, None) } ResolveDatasheetInput::KicadSymPath { path, symbol_name } => { - let url = extract_datasheet_url_from_kicad_sym(path, symbol_name.as_deref())?; - let canonical_url = canonicalize_url(&url)?; - resolve_source_url_datasheet(&client, auth_token, canonical_url) + let reference = + extract_datasheet_reference_from_kicad_sym(path, symbol_name.as_deref())?; + if is_http_datasheet_url(&reference) { + let canonical_url = canonicalize_url(&reference)?; + resolve_source_url_datasheet(&client, auth_token, canonical_url) + } else { + let pdf_path = resolve_local_datasheet_path_from_kicad_sym(path, &reference)?; + validate_local_pdf(&pdf_path)?; + let execution = ResolveExecution::from_pdf_path(pdf_path, None)?; + execute_resolve_execution(&client, auth_token, execution, None) + } } } } @@ -316,7 +324,10 @@ fn reset_materialized_cache(materialized_dir: &Path, images_dir: &Path, complete } } -fn extract_datasheet_url_from_kicad_sym(path: &Path, symbol_name: Option<&str>) -> Result { +fn extract_datasheet_reference_from_kicad_sym( + path: &Path, + symbol_name: Option<&str>, +) -> Result { let symbol_lib = pcb_eda::SymbolLibrary::from_file(path) .with_context(|| format!("Failed to parse KiCad symbol file {}", path.display()))?; @@ -324,10 +335,22 @@ fn extract_datasheet_url_from_kicad_sym(path: &Path, symbol_name: Option<&str>) symbol .datasheet .as_deref() - .map(str::trim) - .filter(|v| is_usable_datasheet_value(v)) + .and_then(pcb_eda::usable_kicad_field_value) .map(ToOwned::to_owned) - .ok_or_else(|| anyhow::anyhow!("No valid Datasheet URL found in {}", path.display())) + .ok_or_else(|| anyhow::anyhow!("No valid Datasheet found in {}", path.display())) +} + +fn resolve_local_datasheet_path_from_kicad_sym( + path: &Path, + datasheet_ref: &str, +) -> Result { + let datasheet_path = Path::new(datasheet_ref); + if datasheet_path.is_absolute() { + return Ok(datasheet_path.to_path_buf()); + } + + let symbol_dir = path.parent().unwrap_or_else(|| Path::new("")); + Ok(symbol_dir.join(datasheet_path)) } fn select_symbol_from_library<'a>( @@ -365,10 +388,7 @@ fn select_symbol_from_library<'a>( Ok(&symbols[0]) } -fn is_usable_datasheet_value(value: &str) -> bool { - if value.is_empty() || value == "~" { - return false; - } +fn is_http_datasheet_url(value: &str) -> bool { value.starts_with("http://") || value.starts_with("https://") } @@ -727,7 +747,7 @@ mod tests { } #[test] - fn test_extract_datasheet_url_from_symbols_uses_first_valid_value() { + fn test_extract_datasheet_reference_from_symbols_uses_first_valid_value() { let source = r#"(kicad_symbol_lib (version 20211014) (generator kicad_symbol_editor) @@ -760,6 +780,21 @@ mod tests { ); } + #[test] + fn test_resolve_local_datasheet_path_from_kicad_sym_uses_symbol_directory() { + let root = std::env::temp_dir().join(format!("datasheet-local-ref-{}", Uuid::new_v4())); + let symbol_dir = root.join("parts"); + fs::create_dir_all(&symbol_dir).unwrap(); + let symbol_path = symbol_dir.join("Part.kicad_sym"); + fs::write(&symbol_path, "(kicad_symbol_lib)").unwrap(); + + let resolved = + resolve_local_datasheet_path_from_kicad_sym(&symbol_path, "docs/Part.pdf").unwrap(); + assert_eq!(resolved, symbol_dir.join("docs/Part.pdf")); + + fs::remove_dir_all(root).unwrap(); + } + #[test] fn test_parse_request_rejects_symbol_name_without_kicad_sym_path() { let args = serde_json::json!({ diff --git a/crates/pcb-eda/src/kicad/symbol.rs b/crates/pcb-eda/src/kicad/symbol.rs index b7d2120bf..7ee463fcf 100644 --- a/crates/pcb-eda/src/kicad/symbol.rs +++ b/crates/pcb-eda/src/kicad/symbol.rs @@ -1,5 +1,5 @@ use crate::kicad::metadata::SymbolMetadata; -use crate::{Part, Pin, PinAt, Symbol}; +use crate::{Part, Pin, PinAt, Symbol, is_placeholder_kicad_pin_name}; use anyhow::Result; use pcb_sexpr::{Sexpr, SexprKind, parse}; use serde::Serialize; @@ -231,7 +231,7 @@ struct NestedStylePins { } fn is_named_pin(pin: &KicadPin) -> bool { - !pin.name.is_empty() && pin.name != "~" + !is_placeholder_kicad_pin_name(&pin.name) } // Parse pins from a nested symbol section. diff --git a/crates/pcb-eda/src/lib.rs b/crates/pcb-eda/src/lib.rs index 8c5d99810..fb025160c 100644 --- a/crates/pcb-eda/src/lib.rs +++ b/crates/pcb-eda/src/lib.rs @@ -62,13 +62,23 @@ fn is_false(v: &bool) -> bool { !*v } +/// KiCad uses `~` as a placeholder for an absent text value in several contexts. +pub fn usable_kicad_field_value(value: &str) -> Option<&str> { + let trimmed = value.trim(); + (!trimmed.is_empty() && trimmed != "~").then_some(trimmed) +} + +pub fn is_placeholder_kicad_pin_name(name: &str) -> bool { + usable_kicad_field_value(name).is_none() +} + impl Pin { /// KiCad uses `~` as a placeholder pin name for "unnamed" pins. /// /// When consuming KiCad symbols, treat unnamed pins as being identified by their number so /// connectivity mappings stay stable and match Zener's Symbol signal naming semantics. pub fn signal_name(&self) -> &str { - if self.name == "~" || self.name.is_empty() { + if is_placeholder_kicad_pin_name(&self.name) { &self.number } else { &self.name @@ -152,3 +162,34 @@ impl SymbolLibrary { self.symbols.first() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn usable_kicad_field_value_filters_placeholders() { + assert_eq!(usable_kicad_field_value("value"), Some("value")); + assert_eq!(usable_kicad_field_value(" value "), Some("value")); + assert_eq!(usable_kicad_field_value(""), None); + assert_eq!(usable_kicad_field_value(" "), None); + assert_eq!(usable_kicad_field_value("~"), None); + } + + #[test] + fn signal_name_falls_back_for_placeholder_pin_names() { + let pin = Pin { + name: "~".to_string(), + number: "42".to_string(), + ..Default::default() + }; + assert_eq!(pin.signal_name(), "42"); + + let named_pin = Pin { + name: "VCC".to_string(), + number: "1".to_string(), + ..Default::default() + }; + assert_eq!(named_pin.signal_name(), "VCC"); + } +} diff --git a/crates/pcb-zen-core/src/lang/component.rs b/crates/pcb-zen-core/src/lang/component.rs index 95d236c2f..92e644bc5 100644 --- a/crates/pcb-zen-core/src/lang/component.rs +++ b/crates/pcb-zen-core/src/lang/component.rs @@ -43,8 +43,6 @@ pub enum ComponentError { NameNotString, #[error("`footprint` must be a string")] FootprintNotString, - #[error("could not determine parent directory of current file")] - ParentDirectoryNotFound, #[error("`pins` must be a dict mapping pin names to Net")] PinsNotDict, #[error("`prefix` must be a string")] @@ -359,7 +357,8 @@ fn resolve_component_sourcing<'v>( symbol .properties() .get("Manufacturer_Part_Number") - .map(|s| s.to_owned()) + .and_then(|s| pcb_eda::usable_kicad_field_value(s)) + .map(ToOwned::to_owned) }); let manufacturer = explicit_manufacturer .or_else(|| property_string(properties_map, "manufacturer")) @@ -368,7 +367,8 @@ fn resolve_component_sourcing<'v>( symbol .properties() .get("Manufacturer_Name") - .map(|s| s.to_owned()) + .and_then(|s| pcb_eda::usable_kicad_field_value(s)) + .map(ToOwned::to_owned) }); // Manifest parts fallback: if no MPN from explicit/properties/symbol, use manifest primary @@ -388,6 +388,61 @@ fn resolve_component_sourcing<'v>( (part, vec![]) } +fn resolve_symbol_datasheet( + final_symbol: &SymbolValue, + eval_ctx: &crate::EvalContext, +) -> starlark::Result> { + let Some(datasheet_prop) = final_symbol + .properties() + .get("Datasheet") + .and_then(|value| pcb_eda::usable_kicad_field_value(value)) + else { + return Ok(None); + }; + + if datasheet_prop.starts_with("http://") + || datasheet_prop.starts_with("https://") + || datasheet_prop.starts_with(pcb_sch::PACKAGE_URI_PREFIX) + { + return Ok(Some(datasheet_prop.to_owned())); + } + + let symbol_source_uri = final_symbol.source_uri().ok_or_else(|| { + starlark::Error::new_other(anyhow!( + "`symbol` datasheet path can only be inferred when the symbol is loaded from a file" + )) + })?; + let symbol_source = eval_ctx + .resolution() + .resolve_package_uri(symbol_source_uri) + .map_err(|e| { + starlark::Error::new_other(anyhow!( + "Failed to resolve symbol library '{}': {}", + symbol_source_uri, + e + )) + })?; + + let resolved = eval_ctx + .get_config() + .resolve_path(datasheet_prop, &symbol_source) + .map_err(|e| { + starlark::Error::new_other(anyhow!( + "Failed to resolve symbol datasheet path '{}' relative to '{}': {}", + datasheet_prop, + symbol_source.display(), + e + )) + })?; + + Ok(Some( + eval_ctx + .resolution() + .format_package_uri(&resolved) + .unwrap_or_else(|| resolved.to_string_lossy().into_owned()), + )) +} + fn manifest_part_matches_symbol(part: &ManifestPart, symbol: &SymbolValue) -> bool { part.symbol_name .as_deref() @@ -504,11 +559,7 @@ fn infer_local_footprint_from_symbol_property( return Ok(None); }; - let symbol_dir = symbol_source.parent().ok_or_else(|| { - starlark::Error::new_other(anyhow!( - "Could not infer footprint: symbol source path has no parent directory" - )) - })?; + let symbol_dir = symbol_source.parent().unwrap_or_else(|| Path::new("")); let candidate = symbol_dir.join(format!("{stem}.kicad_mod")); if !eval_ctx.file_provider().exists(&candidate) { @@ -605,12 +656,12 @@ fn resolve_component_footprint( let footprint_prop = final_symbol .properties() .get("Footprint") + .and_then(|value| pcb_eda::usable_kicad_field_value(value)) .ok_or_else(|| { starlark::Error::new_other(anyhow!( "`footprint` is required unless symbol property `Footprint` can be inferred" )) - })? - .as_str(); + })?; if let Some(inferred) = infer_local_footprint_from_symbol_property(&symbol_source, footprint_prop, eval_ctx)? @@ -1448,20 +1499,22 @@ where // If datasheet is not explicitly provided, try to get it from properties, then symbol properties // Skip empty strings and "~" (KiCad's placeholder for no datasheet) - prefer None over empty - let final_datasheet = datasheet_val - .and_then(|v| v.unpack_str().map(|s| s.to_owned())) + let explicit_datasheet = datasheet_val + .and_then(|v| v.unpack_str()) + .and_then(pcb_eda::usable_kicad_field_value) + .map(ToOwned::to_owned) .or_else(|| { properties_map .get("datasheet") - .and_then(|v| v.unpack_str().map(|s| s.to_owned())) - }) - .or_else(|| { - final_symbol - .properties() - .get("Datasheet") - .filter(|s| !s.is_empty() && s.as_str() != "~") - .map(|s| s.to_owned()) + .and_then(|v| v.unpack_str()) + .and_then(pcb_eda::usable_kicad_field_value) + .map(ToOwned::to_owned) }); + let final_datasheet = if let Some(datasheet) = explicit_datasheet { + Some(normalize_path_to_package_uri(&datasheet, Some(ctx))) + } else { + resolve_symbol_datasheet(&final_symbol, ctx)? + }; // If description is not explicitly provided, try to get it from properties, then symbol properties // Skip empty strings - prefer None over empty diff --git a/crates/pcb/src/import/generate.rs b/crates/pcb/src/import/generate.rs index 1d1e6753c..0c22ffb0d 100644 --- a/crates/pcb/src/import/generate.rs +++ b/crates/pcb/src/import/generate.rs @@ -1643,10 +1643,8 @@ fn generate_imported_components( .with_context(|| format!("Failed to render footprint for {}", out_dir.display()))?; let zen = render_component_zen( &part_dir.component_dir, - component, &symbol.symbol, &symbol.filename, - Some(&footprint.filename), flags, ) .with_context(|| format!("Failed to render .zen for {}", out_dir.display()))?; @@ -2082,10 +2080,8 @@ struct RenderedComponentZen { fn render_component_zen( component_name: &str, - component: &ImportComponentData, symbol: &pcb_eda::Symbol, symbol_filename: &str, - footprint_filename: Option<&str>, flags: ImportPartFlags, ) -> Result { let pin_defs = build_pin_defs_for_symbol(symbol); @@ -2104,28 +2100,11 @@ fn render_component_zen( io_pins.entry(io_name).or_default().insert(pin_number); } } - let props = component.best_properties(); - let mpn = props - .and_then(|p| { - find_property_ci(p, &["mpn"]) - .or_else(|| find_property_ci(p, &["manufacturer_part_number"])) - .or_else(|| find_property_ci(p, &["manufacturer part number"])) - }) - .map(|s| s.to_string()) - .unwrap_or_else(|| component_name.to_string()); - let manufacturer = props - .and_then(|p| find_property_ci(p, &["manufacturer", "mfr", "mfg"])) - .map(|s| s.to_string()); - let zen_content = component_gen::generate_component_zen(component_gen::GenerateComponentZenArgs { - mpn: &mpn, component_name, symbol, symbol_filename, - footprint_filename, - datasheet_filename: None, - manufacturer: manufacturer.as_deref(), generated_by: "pcb import", include_skip_bom: flags.any_skip_bom, include_skip_pos: flags.any_skip_pos, diff --git a/crates/pcb/tests/part.rs b/crates/pcb/tests/part.rs index 7987d8692..705b5efad 100644 --- a/crates/pcb/tests/part.rs +++ b/crates/pcb/tests/part.rs @@ -387,3 +387,62 @@ parts = [ serde_json::json!(["Preferred"]) ); } + +#[test] +fn component_inherits_local_symbol_datasheet() { + let output = Sandbox::new() + .write( + "components/TestPart/Part.kicad_sym", + r#"(kicad_symbol_lib + (version 20241209) + (symbol "TestPart" + (property "Reference" "U" (at 0 0 0) (effects (font (size 1.27 1.27)))) + (property "Value" "TestPart" (at 0 -2.54 0) (effects (font (size 1.27 1.27)))) + (property "Footprint" "Part" (at 0 0 0) (effects (font (size 1.27 1.27)) hide)) + (property "Datasheet" "docs/Part.pdf" (at 0 0 0) (effects (font (size 1.27 1.27)) hide)) + (symbol "TestPart_0_1" + (pin input line (at -5.08 0 0) (length 2.54) (name "P1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27))))) + (pin input line (at 5.08 0 180) (length 2.54) (name "P2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27))))) + ) + ) +)"#, + ) + .write( + "components/TestPart/Part.kicad_mod", + r#"(footprint "Part" + (layer "F.Cu") + (pad "1" smd rect (at -1 0) (size 1 1) (layers "F.Cu")) + (pad "2" smd rect (at 1 0) (size 1 1) (layers "F.Cu")) +)"#, + ) + .write("components/TestPart/docs/Part.pdf", "%PDF-1.4\n%") + .write( + "components/TestPart/Part.zen", + r#" +P1 = io("P1", Net) +P2 = io("P2", Net) + +Component( + name = "U", + symbol = Symbol(library = "Part.kicad_sym"), + pins = {"P1": P1, "P2": P2}, +) +"#, + ) + .write( + "board.zen", + r#" +Part = Module("components/TestPart/Part.zen") + +Part(name = "U1", P1 = Net("A"), P2 = Net("B")) +"#, + ) + .snapshot_run("pcb", ["build", "board.zen", "--netlist"]); + + let netlist = parse_netlist_json(&output); + let attrs = component_attrs(&netlist); + assert_eq!( + attrs["datasheet"]["String"].as_str(), + Some("package://workspace/components/TestPart/docs/Part.pdf") + ); +} diff --git a/docs/pages/spec.mdx b/docs/pages/spec.mdx index e787ee6c2..3645de154 100644 --- a/docs/pages/spec.mdx +++ b/docs/pages/spec.mdx @@ -258,9 +258,6 @@ Component( | `properties` | no | Additional properties dict | | `dnp` | no | Do Not Populate flag | | `skip_bom` | no | Exclude from BOM | - -When `footprint` is omitted, it is inferred from the symbol's metadata. When `part` is provided, its `mpn` and `manufacturer` override any scalar `mpn`/`manufacturer` parameters or symbol metadata. If no `part` is set and no explicit/scalar `mpn` is set, `Component()` can inherit a default part from the package manifest's `parts` entries matching the symbol path. `manufacturer` without `mpn` is treated as incomplete legacy scalar sourcing and does not prevent manifest fallback; prefer `part = Part(...)` as the simplest and most robust way to specify sourcing. - ### Part `Part` specifies manufacturer sourcing for a component. It is a prelude symbol — available in all `.zen` files without `load()`. From d305d017b302d96e01566ce5e986b49dc2024bf0 Mon Sep 17 00:00:00 2001 From: Akhil Velagapudi Date: Sat, 28 Mar 2026 21:42:54 -0400 Subject: [PATCH 2/7] Inherit skip_bom from KiCad symbol in_bom flag Component() now falls back to !symbol.in_bom when skip_bom is not explicitly set. KiCad symbol parser defaults in_bom to true (matching KiCad convention) when the property is omitted. --- CHANGELOG.md | 1 + crates/pcb-eda/src/kicad/symbol.rs | 1 + crates/pcb-zen-core/src/lang/component.rs | 11 ++- crates/pcb-zen-core/src/lang/symbol.rs | 3 + crates/pcb/tests/part.rs | 95 +++++++++++++++++++++++ 5 files changed, 108 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85cba6aa6..5458e6267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to Semantic Versioning (https://semver.org/spec/v2.0.0. ### Changed - KiCad symbol is now the source of truth for component metadata (footprint, datasheet, part); generated `.zen` files are minimal wrappers. +- `Component()` inherits `skip_bom` from the KiCad symbol `in_bom` flag (inverted) when not explicitly set. ### Added diff --git a/crates/pcb-eda/src/kicad/symbol.rs b/crates/pcb-eda/src/kicad/symbol.rs index 7ee463fcf..1ae5aa061 100644 --- a/crates/pcb-eda/src/kicad/symbol.rs +++ b/crates/pcb-eda/src/kicad/symbol.rs @@ -167,6 +167,7 @@ pub(super) fn parse_symbol(symbol_data: &[Sexpr]) -> Result { let mut symbol = KicadSymbol { name, raw_sexp: Some(Sexpr::list(symbol_data.to_vec())), + in_bom: true, // KiCad default; overridden by explicit (in_bom no) ..Default::default() }; let mut nested_pin_groups: BTreeMap> = BTreeMap::new(); diff --git a/crates/pcb-zen-core/src/lang/component.rs b/crates/pcb-zen-core/src/lang/component.rs index 92e644bc5..854149e9e 100644 --- a/crates/pcb-zen-core/src/lang/component.rs +++ b/crates/pcb-zen-core/src/lang/component.rs @@ -1354,6 +1354,7 @@ where source_uri: symbol_value.source_uri.clone(), raw_sexp: symbol_value.raw_sexp.clone(), properties: symbol_value.properties.clone(), + in_bom: symbol_value.in_bom, } } else { // symbol is not a Symbol type, just use pin_defs @@ -1363,6 +1364,7 @@ where source_uri: None, raw_sexp: None, properties: SmallMap::new(), + in_bom: true, } } } else { @@ -1373,6 +1375,7 @@ where source_uri: None, raw_sexp: None, properties: SmallMap::new(), + in_bom: true, } } } else if let Some(symbol) = &symbol_val { @@ -1559,12 +1562,13 @@ where ) }; - // Consolidate skip_bom: check kwarg, then legacy properties + // Consolidate skip_bom: check kwarg, then legacy properties, then symbol in_bom (inverted) let final_skip_bom = consolidate_bool_property( skip_bom_val, &properties_map, &["Exclude_from_bom", "exclude_from_bom"], - ); + ) + .unwrap_or(!final_symbol.in_bom); // Consolidate skip_pos: check kwarg, then legacy properties let final_skip_pos = consolidate_bool_property( @@ -1608,7 +1612,7 @@ where data: RefCell::new(ComponentData { part: final_part, dnp: final_dnp.unwrap_or(false), - skip_bom: final_skip_bom.unwrap_or(false), + skip_bom: final_skip_bom, skip_pos: final_skip_pos.unwrap_or(false), properties: properties_map, }), @@ -1679,6 +1683,7 @@ mod tests { source_uri: None, raw_sexp: None, properties, + in_bom: true, } } diff --git a/crates/pcb-zen-core/src/lang/symbol.rs b/crates/pcb-zen-core/src/lang/symbol.rs index d09be07b5..ec0390d85 100644 --- a/crates/pcb-zen-core/src/lang/symbol.rs +++ b/crates/pcb-zen-core/src/lang/symbol.rs @@ -55,6 +55,7 @@ pub struct SymbolValue { pub source_uri: Option, // Stable package URI for the symbol library when available pub raw_sexp: Option, // Raw s-expression of the symbol (if loaded from file, otherwise None) pub properties: SmallMap, // Properties from the symbol definition + pub in_bom: bool, // KiCad in_bom flag (inverse of skip_bom) } impl std::fmt::Debug for SymbolValue { @@ -226,6 +227,7 @@ impl<'v> SymbolValue { source_uri: None, raw_sexp: None, properties: SmallMap::new(), + in_bom: true, }) } // Case 2: Load from library @@ -327,6 +329,7 @@ impl<'v> SymbolValue { source_uri: Some(source_uri), raw_sexp: sexpr, properties, + in_bom: symbol.in_bom, }) } else { Err(starlark::Error::new_other(anyhow!( diff --git a/crates/pcb/tests/part.rs b/crates/pcb/tests/part.rs index 705b5efad..a7534e7e7 100644 --- a/crates/pcb/tests/part.rs +++ b/crates/pcb/tests/part.rs @@ -446,3 +446,98 @@ Part(name = "U1", P1 = Net("A"), P2 = Net("B")) Some("package://workspace/components/TestPart/docs/Part.pdf") ); } + +#[test] +fn component_inherits_skip_bom_from_symbol_in_bom() { + let sym_not_in_bom = r#"(kicad_symbol_lib + (version 20241209) + (symbol "TestPart" (in_bom no) (on_board yes) + (property "Reference" "U" (at 0 0 0) (effects (font (size 1.27 1.27)))) + (property "Value" "TestPart" (at 0 -2.54 0) (effects (font (size 1.27 1.27)))) + (property "Footprint" "TestPart" (at 0 0 0) (effects (font (size 1.27 1.27)) hide)) + (symbol "TestPart_0_1" + (pin input line (at -5.08 0 0) (length 2.54) (name "P1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27))))) + (pin input line (at 5.08 0 180) (length 2.54) (name "P2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27))))) + ) + ) +)"#; + + // Symbol has in_bom=no → component should inherit skip_bom=true + let output = Sandbox::new() + .write("components/TestPart/TestPart.kicad_sym", sym_not_in_bom) + .write("components/TestPart/TestPart.kicad_mod", TEST_KICAD_MOD) + .write( + "components/TestPart/TestPart.zen", + r#" +P1 = io("P1", Net) +P2 = io("P2", Net) + +Component( + name = "U", + symbol = Symbol(library = "TestPart.kicad_sym"), + pins = {"P1": P1, "P2": P2}, +) +"#, + ) + .write( + "board.zen", + r#" +# ```pcb +# [workspace] +# pcb-version = "0.3" +# ``` + +TestPart = Module("components/TestPart/TestPart.zen") + +TestPart(name = "U1", P1 = Net("A"), P2 = Net("B")) +"#, + ) + .snapshot_run("pcb", ["build", "board.zen", "--netlist"]); + + let netlist = parse_netlist_json(&output); + let attrs = component_attrs(&netlist); + assert_eq!( + attrs["skip_bom"]["Boolean"].as_bool(), + Some(true), + "symbol with in_bom=no should set skip_bom=true" + ); + + // Symbol has in_bom=yes → component should have skip_bom=false (default) + let output2 = Sandbox::new() + .write("components/TestPart/TestPart.kicad_sym", MINIMAL_KICAD_SYM) + .write("components/TestPart/TestPart.kicad_mod", TEST_KICAD_MOD) + .write( + "components/TestPart/TestPart.zen", + r#" +P1 = io("P1", Net) +P2 = io("P2", Net) + +Component( + name = "U", + symbol = Symbol(library = "TestPart.kicad_sym"), + pins = {"P1": P1, "P2": P2}, +) +"#, + ) + .write( + "board.zen", + r#" +# ```pcb +# [workspace] +# pcb-version = "0.3" +# ``` + +TestPart = Module("components/TestPart/TestPart.zen") + +TestPart(name = "U1", P1 = Net("A"), P2 = Net("B")) +"#, + ) + .snapshot_run("pcb", ["build", "board.zen", "--netlist"]); + + let netlist2 = parse_netlist_json(&output2); + let attrs2 = component_attrs(&netlist2); + assert!( + attrs2.get("skip_bom").is_none() || attrs2["skip_bom"]["Boolean"].as_bool() == Some(false), + "symbol with in_bom=yes should not set skip_bom" + ); +} From be2b9d475aa1e1486eb6b4e87c0cc1ec81c0ba22 Mon Sep 17 00:00:00 2001 From: Akhil Velagapudi Date: Sat, 28 Mar 2026 21:46:26 -0400 Subject: [PATCH 3/7] Patch symbol Footprint property to local stem during pcb import Imported KiCad symbols retain their original Footprint property (e.g. 'lib:fp' form). Since imported components are local files rather than packages, the KiCad footprint fallback inference path cannot resolve them, causing 'Footprint property is not inferable' errors at build time. Rewrite the symbol's Footprint property to the local footprint stem after rendering, so Component() can infer the footprint file path. --- crates/pcb/src/import/generate.rs | 34 ++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/pcb/src/import/generate.rs b/crates/pcb/src/import/generate.rs index 0c22ffb0d..9147d9973 100644 --- a/crates/pcb/src/import/generate.rs +++ b/crates/pcb/src/import/generate.rs @@ -12,6 +12,10 @@ use log::debug; use pcb_component_gen as component_gen; use pcb_sexpr::Sexpr; use pcb_sexpr::find_child_list; +use pcb_sexpr::formatter::{FormatMode, format_tree}; +use pcb_sexpr::kicad::symbol::{ + kicad_symbol_lib_items_mut, rewrite_symbol_properties, symbol_names, symbol_properties, +}; use pcb_sexpr::{PatchSet, Span, board as sexpr_board}; use pcb_zen_core::lang::stackup as zen_stackup; use std::collections::{BTreeMap, BTreeSet}; @@ -1636,11 +1640,23 @@ fn generate_imported_components( // Render all artifacts first; only touch the filesystem if we can produce a complete // component package. - let symbol = + let mut symbol = render_component_symbol(&part_dir.component_dir, component, schematic_lib_symbols) .with_context(|| format!("Failed to render symbol for {}", out_dir.display()))?; let footprint = render_component_footprint(component) .with_context(|| format!("Failed to render footprint for {}", out_dir.display()))?; + + // Patch the symbol's Footprint property to the local footprint stem so + // that `Component()` can infer it during build. + let fp_stem = footprint + .filename + .strip_suffix(".kicad_mod") + .unwrap_or(&footprint.filename); + symbol.library_text = patch_symbol_footprint_property(&symbol.library_text, fp_stem) + .with_context(|| { + format!("Failed to patch symbol Footprint for {}", out_dir.display()) + })?; + let zen = render_component_zen( &part_dir.component_dir, &symbol.symbol, @@ -1974,6 +1990,22 @@ fn render_component_symbol( }) } +fn patch_symbol_footprint_property(library_text: &str, footprint_stem: &str) -> Result { + let mut parsed = pcb_sexpr::parse(library_text).map_err(|e| anyhow::anyhow!(e))?; + let root = kicad_symbol_lib_items_mut(&mut parsed).context("Not a KiCad symbol library")?; + let names = symbol_names(root); + anyhow::ensure!(!names.is_empty(), "Symbol library contains no symbols"); + let idx = + pcb_sexpr::kicad::symbol::find_symbol_index(root, &names[0]).context("Symbol not found")?; + let symbol_items = root[idx] + .as_list_mut() + .context("Invalid symbol structure")?; + let mut props = symbol_properties(symbol_items); + props.insert("Footprint".to_string(), footprint_stem.to_string()); + rewrite_symbol_properties(symbol_items, &props); + Ok(format_tree(&parsed, FormatMode::Normal)) +} + #[derive(Debug, Clone)] struct RenderedComponentFootprint { filename: String, From bc5e99f62f7fcfe762a604ced21a837a4ba97063 Mon Sep 17 00:00:00 2001 From: Akhil Velagapudi Date: Sat, 28 Mar 2026 22:03:38 -0400 Subject: [PATCH 4/7] Remove pin_defs from generated component templates Generated .zen files no longer emit pin_defs; duplicate pin signal names (e.g. multiple NC/GND pads) naturally merge into a single io() declaration. The Component(pin_defs=...) API remains available for manual use. --- crates/pcb-component-gen/src/lib.rs | 56 ++++--------- .../templates/component.zen.jinja | 7 -- crates/pcb-diode-api/src/component.rs | 1 - crates/pcb/src/import/generate.rs | 79 ++----------------- 4 files changed, 20 insertions(+), 123 deletions(-) diff --git a/crates/pcb-component-gen/src/lib.rs b/crates/pcb-component-gen/src/lib.rs index 1df6099ac..70fc4a700 100644 --- a/crates/pcb-component-gen/src/lib.rs +++ b/crates/pcb-component-gen/src/lib.rs @@ -101,37 +101,20 @@ pub struct GenerateComponentZenArgs<'a> { pub include_skip_pos: bool, pub skip_bom_default: bool, pub skip_pos_default: bool, - pub pin_defs: Option<&'a BTreeMap>, } pub fn generate_component_zen(args: GenerateComponentZenArgs<'_>) -> Result { let component_name = sanitize_mpn_for_path(args.component_name); + // Group pins by sanitized name; duplicate signal names (e.g. multiple "NC" pads) + // naturally merge into one io() declaration. let mut pin_groups: BTreeMap> = BTreeMap::new(); - let mut pin_defs_vec: Option> = None; - - if let Some(pin_defs) = args.pin_defs { - pin_defs_vec = Some( - pin_defs - .iter() - .map(|(name, pad)| serde_json::json!({"name": name, "pad": pad})) - .collect(), - ); - - for signal_name in pin_defs.keys() { - pin_groups - .entry(sanitize_pin_name(signal_name)) - .or_default() - .insert(signal_name.to_string()); - } - } else { - for pin in &args.symbol.pins { - let signal_name = pin.signal_name().to_string(); - pin_groups - .entry(sanitize_pin_name(&signal_name)) - .or_default() - .insert(signal_name); - } + for pin in &args.symbol.pins { + let signal_name = pin.signal_name().to_string(); + pin_groups + .entry(sanitize_pin_name(&signal_name)) + .or_default() + .insert(signal_name); } let pin_groups_vec: Vec<_> = pin_groups @@ -159,7 +142,6 @@ pub fn generate_component_zen(args: GenerateComponentZenArgs<'_>) -> Result = BTreeMap::new(); - pin_defs.insert("NC_6".to_string(), "6".to_string()); - pin_defs.insert("NC_7".to_string(), "7".to_string()); - let zen = generate_component_zen(GenerateComponentZenArgs { component_name: "MPN1", symbol: &symbol, @@ -373,14 +348,13 @@ mod tests { include_skip_pos: false, skip_bom_default: false, skip_pos_default: false, - pin_defs: Some(&pin_defs), }) .unwrap(); - assert!(zen.contains("pin_defs = {")); - assert!(zen.contains("\"NC_6\": \"6\"")); - assert!(zen.contains("\"NC_7\": \"7\"")); - assert!(zen.contains("\"NC_6\": Pins.NC_6")); - assert!(zen.contains("\"NC_7\": Pins.NC_7")); + // Single io() for the shared signal name + assert!(zen.contains("NC = io(\"NC\", Net)")); + assert!(zen.contains("\"NC\": Pins.NC")); + // No pin_defs needed + assert!(!zen.contains("pin_defs")); } } diff --git a/crates/pcb-component-gen/templates/component.zen.jinja b/crates/pcb-component-gen/templates/component.zen.jinja index 3b97e6244..54105b359 100644 --- a/crates/pcb-component-gen/templates/component.zen.jinja +++ b/crates/pcb-component-gen/templates/component.zen.jinja @@ -24,13 +24,6 @@ Component( {%- endif %} {%- if include_skip_pos %} skip_pos = skip_pos, -{%- endif %} -{%- if pin_defs %} - pin_defs = { -{%- for pin in pin_defs %} - "{{ pin.name }}": "{{ pin.pad }}", -{%- endfor %} - }, {%- endif %} symbol = Symbol(library = "{{ sym_path }}"), pins = { diff --git a/crates/pcb-diode-api/src/component.rs b/crates/pcb-diode-api/src/component.rs index 219acf1e4..45c71ea73 100644 --- a/crates/pcb-diode-api/src/component.rs +++ b/crates/pcb-diode-api/src/component.rs @@ -1155,7 +1155,6 @@ fn generate_zen_file( include_skip_pos: false, skip_bom_default: false, skip_pos_default: false, - pin_defs: None, }) } diff --git a/crates/pcb/src/import/generate.rs b/crates/pcb/src/import/generate.rs index 9147d9973..ec602bf30 100644 --- a/crates/pcb/src/import/generate.rs +++ b/crates/pcb/src/import/generate.rs @@ -2044,65 +2044,6 @@ fn render_component_footprint( Ok(RenderedComponentFootprint { filename, mod_text }) } -fn build_pin_defs_for_symbol(symbol: &pcb_eda::Symbol) -> Option> { - // If a symbol contains duplicate pin signal names, our generated Zener module interface - // must disambiguate them; otherwise a single IO would span multiple pin numbers and - // net assignment would become ambiguous (KiCad connectivity keys by pin number). - - let mut base_by_pad: BTreeMap = BTreeMap::new(); - for pin in &symbol.pins { - let pad = KiCadPinNumber::from(pin.number.clone()); - let base = component_gen::sanitize_pin_name(pin.signal_name()); - - base_by_pad - .entry(pad) - .and_modify(|cur| { - if base < *cur { - *cur = base.clone(); - } - }) - .or_insert(base); - } - - let mut counts: BTreeMap = BTreeMap::new(); - for base in base_by_pad.values() { - *counts.entry(base.clone()).or_insert(0) += 1; - } - - if counts.values().all(|&n| n <= 1) { - return None; - } - - let mut used: BTreeSet = BTreeSet::new(); - let mut out: BTreeMap = BTreeMap::new(); - - for (pad, base) in base_by_pad { - let mut name = if counts.get(&base).copied().unwrap_or(1) <= 1 { - base - } else { - format!("{base}_{}", pad.as_str()) - }; - - // Defensive: avoid collisions from pathological pin names or stacked pads. - if !used.insert(name.clone()) { - let base_name = name.clone(); - let mut i = 2; - loop { - let candidate = format!("{base_name}_{i}"); - if used.insert(candidate.clone()) { - name = candidate; - break; - } - i += 1; - } - } - - out.insert(name, pad.as_str().to_string()); - } - - Some(out) -} - #[derive(Debug, Clone)] struct RenderedComponentZen { filename: String, @@ -2116,22 +2057,13 @@ fn render_component_zen( symbol_filename: &str, flags: ImportPartFlags, ) -> Result { - let pin_defs = build_pin_defs_for_symbol(symbol); - let mut io_pins: BTreeMap> = BTreeMap::new(); - if let Some(pin_defs) = &pin_defs { - for (signal_name, pad) in pin_defs { - let pin_number = KiCadPinNumber::from(pad.clone()); - let io_name = component_gen::sanitize_pin_name(signal_name); - io_pins.entry(io_name).or_default().insert(pin_number); - } - } else { - for pin in &symbol.pins { - let pin_number = KiCadPinNumber::from(pin.number.clone()); - let io_name = component_gen::sanitize_pin_name(pin.signal_name()); - io_pins.entry(io_name).or_default().insert(pin_number); - } + for pin in &symbol.pins { + let pin_number = KiCadPinNumber::from(pin.number.clone()); + let io_name = component_gen::sanitize_pin_name(pin.signal_name()); + io_pins.entry(io_name).or_default().insert(pin_number); } + let zen_content = component_gen::generate_component_zen(component_gen::GenerateComponentZenArgs { component_name, @@ -2142,7 +2074,6 @@ fn render_component_zen( include_skip_pos: flags.any_skip_pos, skip_bom_default: flags.all_skip_bom, skip_pos_default: flags.all_skip_pos, - pin_defs: pin_defs.as_ref(), }) .context("Failed to generate component .zen")?; From 257028cac53111564e1e2bd4b52b1d4bba1cf6c8 Mon Sep 17 00:00:00 2001 From: Akhil Velagapudi Date: Sat, 28 Mar 2026 22:08:55 -0400 Subject: [PATCH 5/7] Remove pin_defs from stdlib generics Use symbol's native signal names directly in pins dict. The io() names (P1, P2) remain unchanged. --- crates/pcb-zen/tests/spice_model.rs | 27 ++++++--------------------- stdlib/generics/Capacitor.zen | 8 ++------ stdlib/generics/FerriteBead.zen | 8 ++------ stdlib/generics/Resistor.zen | 8 ++------ stdlib/generics/SolderJumper.zen | 8 ++------ stdlib/generics/Thermistor.zen | 10 +++------- 6 files changed, 17 insertions(+), 52 deletions(-) diff --git a/crates/pcb-zen/tests/spice_model.rs b/crates/pcb-zen/tests/spice_model.rs index 7781263fe..986fabc2d 100644 --- a/crates/pcb-zen/tests/spice_model.rs +++ b/crates/pcb-zen/tests/spice_model.rs @@ -68,7 +68,6 @@ R1 p n {RVAL} env.add_file( "myresistor.zen", r#" -load("@stdlib/generics/SolderJumper.zen", "pin_defs") load("@stdlib/config.zen", "config_properties") load("@stdlib/units.zen", "Resistance", "Voltage") load("@stdlib/utils.zen", "format_value") @@ -113,13 +112,9 @@ Component( spice_model = SpiceModel('./r.lib', 'my_resistor', nets=[P1, P2], args={"RVAL": str(value.value)}), - pin_defs = { - "P1": "1", - "P2": "2", - }, pins = { - "P1": P1, - "P2": P2, + "1": P1, + "2": P2, }, properties = properties, ) @@ -166,7 +161,6 @@ R1 p n {RVAL} env.add_file( "myresistor.zen", r#" -load("@stdlib/generics/SolderJumper.zen", "pin_defs") load("@stdlib/config.zen", "config_properties") load("@stdlib/units.zen", "Resistance", "Voltage") load("@stdlib/utils.zen", "format_value") @@ -195,13 +189,9 @@ Component( spice_model = SpiceModel('./r.lib', 'my_resistor', nets=[P1, P2], args={"RVAL": str(value.value)}), - pin_defs = { - "P1": "1", - "P2": "2", - }, pins = { - "P1": P1, - "P2": P2, + "1": P1, + "2": P2, }, properties = properties, ) @@ -247,7 +237,6 @@ R1 p n {RVAL} env.add_file( "myresistor.zen", r#" -load("@stdlib/generics/SolderJumper.zen", "pin_defs") load("@stdlib/config.zen", "config_properties") load("@stdlib/units.zen", "Resistance", "Voltage") load("@stdlib/utils.zen", "format_value") @@ -276,13 +265,9 @@ Component( spice_model = SpiceModel('./r.lib', 'my_resistor', nets=[P1, P2], args={"RVAL": str(value.value)}), - pin_defs = { - "P1": "1", - "P2": "2", - }, pins = { - "P1": P1, - "P2": P2, + "1": P1, + "2": P2, }, properties = properties, ) diff --git a/stdlib/generics/Capacitor.zen b/stdlib/generics/Capacitor.zen index 37dfe12fd..d48520225 100644 --- a/stdlib/generics/Capacitor.zen +++ b/stdlib/generics/Capacitor.zen @@ -130,13 +130,9 @@ Component( footprint=File(_footprint(mount, package)), properties=properties, type="capacitor", - pin_defs={ - "P1": "1", - "P2": "2", - }, pins={ - "P1": P1, - "P2": P2, + "1": P1, + "2": P2, }, spice_model=SpiceModel( "spice/Capacitor.lib", diff --git a/stdlib/generics/FerriteBead.zen b/stdlib/generics/FerriteBead.zen index 65299afed..eab537493 100644 --- a/stdlib/generics/FerriteBead.zen +++ b/stdlib/generics/FerriteBead.zen @@ -106,13 +106,9 @@ Component( symbol=Symbol(**_symbol(package)), footprint=File(_footprint(package)), prefix="FB", - pin_defs={ - "P1": "1", - "P2": "2", - }, pins={ - "P1": P1, - "P2": P2, + "1": P1, + "2": P2, }, spice_model=SpiceModel( "spice/FerriteBead.lib", diff --git a/stdlib/generics/Resistor.zen b/stdlib/generics/Resistor.zen index d5cc27116..f217fce21 100644 --- a/stdlib/generics/Resistor.zen +++ b/stdlib/generics/Resistor.zen @@ -111,13 +111,9 @@ Component( symbol=Symbol(**_symbol(mount, package, use_us_symbol)), footprint=File(_footprint(mount, package)), prefix="R", - pin_defs={ - "P1": "1", - "P2": "2", - }, pins={ - "P1": P1, - "P2": P2, + "1": P1, + "2": P2, }, spice_model=SpiceModel( "spice/Resistor.lib", diff --git a/stdlib/generics/SolderJumper.zen b/stdlib/generics/SolderJumper.zen index 65caa074e..0f3c01aae 100644 --- a/stdlib/generics/SolderJumper.zen +++ b/stdlib/generics/SolderJumper.zen @@ -85,20 +85,16 @@ def _symbol(pin_count): def _pins_and_connections(pin_count): - # pin_defs: mapping of pin name to pin number as string - # pins: mapping of pin name to net (if connected) - pin_defs = {} pins = {} nets = P for i in range(pin_count): pin_name = str(i + 1) - pin_defs[pin_name] = str(i + 1) net = nets[i] if net != None: pins[pin_name] = net - return pins, pin_defs + return pins -pins, pin_defs = _pins_and_connections(pin_count) +pins = _pins_and_connections(pin_count) Component( name="SJ", diff --git a/stdlib/generics/Thermistor.zen b/stdlib/generics/Thermistor.zen index 6950c9aaa..0f39c630e 100644 --- a/stdlib/generics/Thermistor.zen +++ b/stdlib/generics/Thermistor.zen @@ -1,5 +1,5 @@ load("../interfaces.zen", "Net") -load("./SolderJumper.zen", "pin_defs") + load("../units.zen", "Capacitance", "Inductance", "Resistance", "Voltage") load("../utils.zen", "format_value") @@ -95,13 +95,9 @@ Component( symbol=Symbol(**_symbol(mount, package, temperature_coefficient, use_us_symbol)), footprint=File(_footprint(mount, package)), prefix="TH", - pin_defs={ - "P1": "1", - "P2": "2", - }, pins={ - "P1": P1, - "P2": P2, + "1": P1, + "2": P2, }, spice_model=SpiceModel( "spice/Thermistor.lib", From 024f654cf67f1de2dd5a75510c77c39cd1271d84 Mon Sep 17 00:00:00 2001 From: Akhil Velagapudi Date: Sat, 28 Mar 2026 22:09:33 -0400 Subject: [PATCH 6/7] Document Component() symbol-inherited defaults in spec Update parameter table: footprint, skip_bom, and datasheet now document their symbol-derived defaults inline. --- docs/pages/spec.mdx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/pages/spec.mdx b/docs/pages/spec.mdx index 3645de154..f1ccb59cb 100644 --- a/docs/pages/spec.mdx +++ b/docs/pages/spec.mdx @@ -253,11 +253,12 @@ Component( | `prefix` | no | Reference designator prefix (default: `"U"`) | | `manufacturer` | no | Manufacturer name (legacy — prefer `part`) | | `mpn` | no | Manufacturer part number (legacy — prefer `part`) | -| `footprint` | no | PCB footprint path (usually omit — inferred from symbol) | +| `footprint` | no | PCB footprint path (default: inferred from symbol `Footprint` property) | | `type` | no | Component type string | | `properties` | no | Additional properties dict | | `dnp` | no | Do Not Populate flag | -| `skip_bom` | no | Exclude from BOM | +| `skip_bom` | no | Exclude from BOM (default: inverse of symbol `in_bom` flag) | +| `datasheet` | no | Datasheet path (default: symbol `Datasheet` property; local paths resolved relative to the `.kicad_sym` file) | ### Part `Part` specifies manufacturer sourcing for a component. It is a prelude symbol — available in all `.zen` files without `load()`. From 287252e2a372aded28370c989c7c52561c0b68b9 Mon Sep 17 00:00:00 2001 From: Akhil Velagapudi Date: Sat, 28 Mar 2026 22:26:00 -0400 Subject: [PATCH 7/7] Accept snapshot diffs --- crates/pcb/tests/snapshots/release__publish_full.snap | 2 +- crates/pcb/tests/snapshots/release__publish_source_only.snap | 2 +- .../pcb/tests/snapshots/release__publish_with_description.snap | 2 +- crates/pcb/tests/snapshots/release__publish_with_version.snap | 2 +- docs/pages/spec.mdx | 1 + 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/pcb/tests/snapshots/release__publish_full.snap b/crates/pcb/tests/snapshots/release__publish_full.snap index 98d43fa06..381d7bd07 100644 --- a/crates/pcb/tests/snapshots/release__publish_full.snap +++ b/crates/pcb/tests/snapshots/release__publish_full.snap @@ -117,7 +117,7 @@ Designator,Val,Package,Mid X,Mid Y,Rotation,Layer "user": "" } } -=== netlist.json <73038 bytes, sha256: 9e3dc0b> +=== netlist.json <73006 bytes, sha256: 0f3d886> === src/boards/TestBoard.zen load("@stdlib/interfaces.zen", "Gpio") diff --git a/crates/pcb/tests/snapshots/release__publish_source_only.snap b/crates/pcb/tests/snapshots/release__publish_source_only.snap index 42dd8ce59..ebb6a6a21 100644 --- a/crates/pcb/tests/snapshots/release__publish_source_only.snap +++ b/crates/pcb/tests/snapshots/release__publish_source_only.snap @@ -43,7 +43,7 @@ expression: sb.snapshot_dir(&staging_dir) "user": "" } } -=== netlist.json <73038 bytes, sha256: 9e3dc0b> +=== netlist.json <73006 bytes, sha256: 0f3d886> === src/boards/TestBoard.zen load("@stdlib/interfaces.zen", "Gpio") diff --git a/crates/pcb/tests/snapshots/release__publish_with_description.snap b/crates/pcb/tests/snapshots/release__publish_with_description.snap index 66b2f78ea..3b61149c5 100644 --- a/crates/pcb/tests/snapshots/release__publish_with_description.snap +++ b/crates/pcb/tests/snapshots/release__publish_with_description.snap @@ -44,7 +44,7 @@ expression: sb.snapshot_dir(&staging_dir) "user": "" } } -=== netlist.json <73038 bytes, sha256: 095564f> +=== netlist.json <73006 bytes, sha256: 42cb844> === src/boards/DescBoard.zen load("@stdlib/interfaces.zen", "Gpio") diff --git a/crates/pcb/tests/snapshots/release__publish_with_version.snap b/crates/pcb/tests/snapshots/release__publish_with_version.snap index 8fb9e1924..b718ea401 100644 --- a/crates/pcb/tests/snapshots/release__publish_with_version.snap +++ b/crates/pcb/tests/snapshots/release__publish_with_version.snap @@ -43,7 +43,7 @@ expression: sb.snapshot_dir(staging_dir) "user": "" } } -=== netlist.json <72299 bytes, sha256: 215b338> +=== netlist.json <72267 bytes, sha256: 243c2ef> === src/boards/TB0001.zen load("@stdlib/interfaces.zen", "Gpio") diff --git a/docs/pages/spec.mdx b/docs/pages/spec.mdx index f1ccb59cb..8ca2ae075 100644 --- a/docs/pages/spec.mdx +++ b/docs/pages/spec.mdx @@ -259,6 +259,7 @@ Component( | `dnp` | no | Do Not Populate flag | | `skip_bom` | no | Exclude from BOM (default: inverse of symbol `in_bom` flag) | | `datasheet` | no | Datasheet path (default: symbol `Datasheet` property; local paths resolved relative to the `.kicad_sym` file) | + ### Part `Part` specifies manufacturer sourcing for a component. It is a prelude symbol — available in all `.zen` files without `load()`.