Skip to content

Commit fbd73d5

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/kicad-variable-expansion
2 parents 69991f1 + af1f5b0 commit fbd73d5

38 files changed

+1312
-915
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ and this project adheres to Semantic Versioning (https://semver.org/spec/v2.0.0.
1010

1111
### Changed
1212

13+
- Bump stdlib to 0.5.9
1314
- `pcb layout --check` now runs layout sync against a shadow copy.
1415
- Removed `--sync-board-config`; board config sync is now always enabled for layout sync (CLI, MCP `run_layout`, and `pcb_layout::process_layout`).
1516
- `pcb fmt` now formats KiCad S-expression files (for example `.kicad_pcb`, `.kicad_sch`, `fp-lib-table`) only when an explicit file path is provided; default/discovery mode remains `.zen`-only.
1617
- Stackup/layers patching in `pcb layout` now uses structural S-expression mutation + canonical KiCad-style formatting, with unconditional patch/write.
1718
- `pcb layout` stackup sync now also patches `general (thickness ...)` from computed stackup thickness.
1819
- Removed MCP resource `zener-docs` (https://docs.pcb.new/llms.txt) from `pcb mcp`, with Zener docs now embedded in `pcb doc`.
20+
- Move board-config/title-block patching to Rust; simplify Python sync; only update `.kicad_pro` netclass patterns when assignments exist.
1921

2022
### Fixed
2123

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use std::collections::HashSet;
1+
use std::collections::BTreeMap;
22

3-
use ipc2581::types::Step;
3+
use ipc2581::types::{PlatingStatus, Step};
44
use serde::{Deserialize, Serialize};
55

66
use super::IpcAccessor;
@@ -10,42 +10,106 @@ use super::IpcAccessor;
1010
pub struct DrillStats {
1111
pub total_holes: usize,
1212
pub unique_sizes: usize,
13+
/// Per-type distribution: via, plated, non-plated
14+
pub distribution: Vec<DrillTypeDistribution>,
1315
}
1416

15-
impl DrillStats {
16-
pub fn new(total_holes: usize, unique_sizes: usize) -> Self {
17-
Self {
18-
total_holes,
19-
unique_sizes,
17+
/// Distribution of holes for a single plating type
18+
#[derive(Debug, Clone, Serialize, Deserialize)]
19+
pub struct DrillTypeDistribution {
20+
pub hole_type: DrillHoleType,
21+
pub total: usize,
22+
/// Unique diameters sorted ascending, each with count
23+
pub sizes: Vec<DrillSize>,
24+
}
25+
26+
/// Categorized hole type
27+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
28+
pub enum DrillHoleType {
29+
Via,
30+
Plated,
31+
NonPlated,
32+
}
33+
34+
impl DrillHoleType {
35+
pub fn as_str(&self) -> &'static str {
36+
match self {
37+
Self::Via => "Via",
38+
Self::Plated => "Plated (PTH)",
39+
Self::NonPlated => "Non-Plated (NPTH)",
2040
}
2141
}
2242
}
2343

44+
/// A unique drill size with its count
45+
#[derive(Debug, Clone, Serialize, Deserialize)]
46+
pub struct DrillSize {
47+
pub diameter_mm: f64,
48+
pub count: usize,
49+
}
50+
2451
impl<'a> IpcAccessor<'a> {
25-
/// Get drill hole statistics (total count and unique sizes)
52+
/// Get drill hole statistics with per-type distribution
2653
///
2754
/// Returns None if no ECAD section or no steps exist
2855
pub fn drill_stats(&self) -> Option<DrillStats> {
2956
let step = self.first_step()?;
30-
Some(count_drill_info(step))
57+
Some(collect_drill_info(step))
3158
}
3259
}
3360

34-
/// Count drill holes and unique diameters
35-
fn count_drill_info(step: &Step) -> DrillStats {
36-
let mut total_holes = 0;
37-
let mut unique_diameters = HashSet::new();
61+
/// Collect drill holes grouped by plating type, with per-diameter counts
62+
fn collect_drill_info(step: &Step) -> DrillStats {
63+
// BTreeMap<DrillHoleType, BTreeMap<diameter_mils, count>>
64+
let mut by_type: BTreeMap<DrillHoleType, BTreeMap<i32, (f64, usize)>> = BTreeMap::new();
65+
let mut total_holes = 0usize;
66+
let mut all_diameters = std::collections::HashSet::new();
3867

3968
for layer_feature in &step.layer_features {
4069
for set in &layer_feature.sets {
4170
for hole in &set.holes {
4271
total_holes += 1;
43-
// Convert diameter to integer mils to avoid floating point comparison issues
4472
let diameter_mils = (hole.diameter * 39370.0) as i32;
45-
unique_diameters.insert(diameter_mils);
73+
all_diameters.insert(diameter_mils);
74+
75+
let hole_type = match hole.plating_status {
76+
PlatingStatus::Via => DrillHoleType::Via,
77+
PlatingStatus::Plated => DrillHoleType::Plated,
78+
PlatingStatus::NonPlated => DrillHoleType::NonPlated,
79+
};
80+
81+
let entry = by_type
82+
.entry(hole_type)
83+
.or_default()
84+
.entry(diameter_mils)
85+
.or_insert((hole.diameter, 0));
86+
entry.1 += 1;
4687
}
4788
}
4889
}
4990

50-
DrillStats::new(total_holes, unique_diameters.len())
91+
let distribution = by_type
92+
.into_iter()
93+
.map(|(hole_type, sizes_map)| {
94+
let mut total = 0usize;
95+
let sizes: Vec<DrillSize> = sizes_map
96+
.into_values()
97+
.map(|(diameter_mm, count)| {
98+
total += count;
99+
DrillSize { diameter_mm, count }
100+
})
101+
.collect();
102+
DrillTypeDistribution {
103+
hole_type,
104+
total,
105+
sizes,
106+
}
107+
})
108+
.collect();
109+
110+
DrillStats {
111+
total_holes,
112+
unique_sizes: all_diameters.len(),
113+
distribution,
114+
}
51115
}

crates/pcb-ipc2581-tools/src/accessors/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ mod stackup;
1313
pub use board::{BoardDimensions, StackupInfo};
1414
pub use bom::{AvlLookup, BomStats, CharacteristicsData};
1515
pub use components::ComponentStats;
16-
pub use drills::DrillStats;
16+
pub use drills::{DrillHoleType, DrillSize, DrillStats, DrillTypeDistribution};
1717
pub use layers::{LayerStats, NetStats};
1818
pub use metadata::{FileMetadata, SoftwareInfo};
1919
pub use stackup::{
20-
ColorInfo, StackupDetails, StackupLayerInfo, StackupLayerType, SurfaceFinishInfo,
20+
ColorInfo, ImpedanceControlInfo, MaterialInfo, StackupDetails, StackupLayerInfo,
21+
StackupLayerType, SurfaceFinishInfo,
2122
};
2223

2324
/// Main accessor for IPC-2581 data extraction

crates/pcb-ipc2581-tools/src/accessors/stackup.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,86 @@ impl<'a> IpcAccessor<'a> {
416416
}
417417
}
418418

419+
/// Dielectric material information extracted from stackup
420+
#[derive(Debug, Clone, Serialize, Deserialize)]
421+
pub struct MaterialInfo {
422+
/// Distinct dielectric material names (e.g., ["FR4", "Rogers 4350B"])
423+
pub dielectric: Vec<String>,
424+
}
425+
426+
/// Impedance control information extracted from stackup
427+
#[derive(Debug, Clone, Serialize, Deserialize)]
428+
pub struct ImpedanceControlInfo {
429+
/// Whether any dielectric layer has Dk specified
430+
pub has_dielectric_constant: bool,
431+
/// Whether any dielectric layer has loss tangent specified
432+
pub has_loss_tangent: bool,
433+
/// Distinct Dk values found across dielectric layers
434+
pub dielectric_constants: Vec<f64>,
435+
/// Distinct loss tangent values found across dielectric layers
436+
pub loss_tangents: Vec<f64>,
437+
}
438+
439+
impl ImpedanceControlInfo {
440+
/// A design is likely impedance-controlled if Dk values are specified
441+
pub fn is_impedance_controlled(&self) -> bool {
442+
self.has_dielectric_constant
443+
}
444+
}
445+
446+
impl<'a> IpcAccessor<'a> {
447+
/// Extract dielectric material names from the stackup
448+
pub fn material_info(&self) -> Option<MaterialInfo> {
449+
let stackup = self.stackup_details()?;
450+
451+
let dielectric: Vec<String> = stackup
452+
.layers
453+
.iter()
454+
.filter(|l| l.layer_type.is_dielectric())
455+
.filter_map(|l| l.material.clone())
456+
.collect::<std::collections::BTreeSet<_>>()
457+
.into_iter()
458+
.collect();
459+
460+
if dielectric.is_empty() {
461+
return None;
462+
}
463+
464+
Some(MaterialInfo { dielectric })
465+
}
466+
467+
/// Extract impedance control information from the stackup
468+
pub fn impedance_control_info(&self) -> Option<ImpedanceControlInfo> {
469+
let stackup = self.stackup_details()?;
470+
471+
let mut dk_set = std::collections::BTreeSet::new();
472+
let mut df_set = std::collections::BTreeSet::new();
473+
474+
for layer in &stackup.layers {
475+
if !layer.layer_type.is_dielectric() {
476+
continue;
477+
}
478+
if let Some(dk) = layer.dielectric_constant {
479+
// Use fixed-precision key to dedup
480+
dk_set.insert((dk * 1000.0) as i64);
481+
}
482+
if let Some(df) = layer.loss_tangent {
483+
df_set.insert((df * 100000.0) as i64);
484+
}
485+
}
486+
487+
let dielectric_constants: Vec<f64> = dk_set.iter().map(|&v| v as f64 / 1000.0).collect();
488+
let loss_tangents: Vec<f64> = df_set.iter().map(|&v| v as f64 / 100000.0).collect();
489+
490+
Some(ImpedanceControlInfo {
491+
has_dielectric_constant: !dielectric_constants.is_empty(),
492+
has_loss_tangent: !loss_tangents.is_empty(),
493+
dielectric_constants,
494+
loss_tangents,
495+
})
496+
}
497+
}
498+
419499
/// Format FinishType enum to display string
420500
/// Values per IPC-6012 Table 3-3 "Final Finish and Coating Requirements"
421501
/// -N suffix = suitable for soldering (Nickel barrier critical)

0 commit comments

Comments
 (0)