Skip to main content

altium_format/records/pcb/
rule.rs

1//! PCB design rule types for Altium PCB files.
2//!
3//! Design rules define constraints for PCB layout such as clearance,
4//! trace width, routing layers, etc.
5
6use std::fmt;
7use std::io::{Read, Write};
8
9use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
10
11use crate::error::Result;
12use crate::io::reader::decode_windows_1252;
13use crate::io::writer::encode_windows_1252;
14use crate::types::{Coord, ParameterCollection};
15
16/// Design rule kind identifiers.
17///
18/// These correspond to Altium's internal rule type IDs.
19/// Based on DXP API TRuleKind enumeration.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
21#[repr(u16)]
22pub enum RuleKind {
23    /// Clearance constraints between objects.
24    #[default]
25    Clearance = 0,
26    /// Parallel segment constraints for differential pairs.
27    ParallelSegment = 1,
28    /// Trace width constraints.
29    Width = 2,
30    /// Maximum/minimum trace length.
31    Length = 3,
32    /// Matched length constraints for bus signals.
33    MatchedLengths = 4,
34    /// Daisy chain stub length limits.
35    StubLength = 5,
36    /// Connection style to power planes.
37    PlaneConnect = 6,
38    /// Routing topology rules (e.g., daisy chain, star).
39    RoutingTopology = 7,
40    /// Priority of routing for specific nets.
41    RoutingPriority = 8,
42    /// Allowed routing layers.
43    RoutingLayers = 9,
44    /// Corner styles (45 deg, 90 deg, etc.).
45    RoutingCorners = 10,
46    /// Via types and sizes allowed.
47    RoutingVias = 11,
48    /// Clearance to split planes.
49    PlaneClearance = 12,
50    /// Solder mask expansion settings.
51    SolderMaskExpansion = 13,
52    /// Paste mask expansion settings.
53    PasteMaskExpansion = 14,
54    /// Short circuit allowance.
55    ShortCircuit = 15,
56    /// Checks for unrouted nets.
57    UnRoutedNet = 16,
58    /// Via placement under SMD pads.
59    ViasUnderSMD = 17,
60    /// Maximum via count per net.
61    MaximumViaCount = 18,
62    /// Minimum annular ring width.
63    MinimumAnnularRing = 19,
64    /// Polygon connection style (Relief/Direct).
65    PolygonConnect = 20,
66    /// Acute angle constraints.
67    AcuteAngle = 21,
68    /// Room definition constraints.
69    RoomDefinition = 22,
70    /// SMD to corner clearance.
71    SMDToCorner = 23,
72    /// Clearance between 3D bodies/courtyards.
73    ComponentClearance = 24,
74    /// Allowed component orientations.
75    ComponentOrientations = 25,
76    /// Permitted layers for objects.
77    PermittedLayers = 26,
78    /// Nets to ignore in DRC.
79    NetsToIgnore = 27,
80    /// Signal stimulus for simulation.
81    SignalStimulus = 28,
82    /// Overshoot on falling edge (signal integrity).
83    OvershootFalling = 29,
84    /// Overshoot on rising edge (signal integrity).
85    OvershootRising = 30,
86    /// Undershoot on falling edge (signal integrity).
87    UndershootFalling = 31,
88    /// Undershoot on rising edge (signal integrity).
89    UndershootRising = 32,
90    /// Maximum/minimum impedance (signal integrity).
91    MaxMinImpedance = 33,
92    /// Signal top voltage value (signal integrity).
93    SignalTopValue = 34,
94    /// Signal base voltage value (signal integrity).
95    SignalBaseValue = 35,
96    /// Flight time on rising edge (signal integrity).
97    FlightTimeRising = 36,
98    /// Flight time on falling edge (signal integrity).
99    FlightTimeFalling = 37,
100    /// Layer stack definition.
101    LayerStack = 38,
102    /// Maximum slope on rising edge (signal integrity).
103    SlopeRising = 39,
104    /// Maximum slope on falling edge (signal integrity).
105    SlopeFalling = 40,
106    /// Supply net definitions.
107    SupplyNets = 41,
108    /// Allowed hole size range.
109    HoleSize = 42,
110    /// Testpoint style for fabrication.
111    TestPointStyle = 43,
112    /// Testpoint usage settings.
113    TestPointUsage = 44,
114    /// Unconnected pin check.
115    UnconnectedPin = 45,
116    /// SMD to plane clearance.
117    SMDToPlane = 46,
118    /// SMD neck-down settings.
119    SMDNeckDown = 47,
120    /// Drill pair definitions.
121    LayerPairs = 48,
122    /// Fanout settings for BGAs/LCCs.
123    FanoutControl = 49,
124    /// Component height checks.
125    Height = 50,
126    /// Differential pair routing settings.
127    DiffPairsRouting = 51,
128    /// Minimum clearance between holes.
129    HoleToHoleClearance = 52,
130    /// Minimum solder mask sliver width.
131    MinimumSolderMaskSliver = 53,
132    /// Silkscreen to mask clearance.
133    SilkToSolderMaskClearance = 54,
134    /// Silkscreen to silkscreen clearance.
135    SilkToSilkClearance = 55,
136    /// Checks for stub tracks (antennae).
137    NetAntennae = 56,
138    /// Testpoint usage for assembly.
139    AssemblyTestpoint = 57,
140    /// Assembly testpoint usage settings.
141    AssemblyTestPointUsage = 58,
142    /// Silkscreen to board region clearance.
143    SilkToBoardRegionClearance = 59,
144    /// Checks for unpoured polygons.
145    UnpouredPolygon = 62,
146    /// Unknown rule kind.
147    Unknown(u16),
148}
149
150impl RuleKind {
151    /// Create a RuleKind from its numeric ID.
152    pub fn from_id(id: u16) -> Self {
153        match id {
154            0 => RuleKind::Clearance,
155            1 => RuleKind::ParallelSegment,
156            2 => RuleKind::Width,
157            3 => RuleKind::Length,
158            4 => RuleKind::MatchedLengths,
159            5 => RuleKind::StubLength,
160            6 => RuleKind::PlaneConnect,
161            7 => RuleKind::RoutingTopology,
162            8 => RuleKind::RoutingPriority,
163            9 => RuleKind::RoutingLayers,
164            10 => RuleKind::RoutingCorners,
165            11 => RuleKind::RoutingVias,
166            12 => RuleKind::PlaneClearance,
167            13 => RuleKind::SolderMaskExpansion,
168            14 => RuleKind::PasteMaskExpansion,
169            15 => RuleKind::ShortCircuit,
170            16 => RuleKind::UnRoutedNet,
171            17 => RuleKind::ViasUnderSMD,
172            18 => RuleKind::MaximumViaCount,
173            19 => RuleKind::MinimumAnnularRing,
174            20 => RuleKind::PolygonConnect,
175            21 => RuleKind::AcuteAngle,
176            22 => RuleKind::RoomDefinition,
177            23 => RuleKind::SMDToCorner,
178            24 => RuleKind::ComponentClearance,
179            25 => RuleKind::ComponentOrientations,
180            26 => RuleKind::PermittedLayers,
181            27 => RuleKind::NetsToIgnore,
182            28 => RuleKind::SignalStimulus,
183            29 => RuleKind::OvershootFalling,
184            30 => RuleKind::OvershootRising,
185            31 => RuleKind::UndershootFalling,
186            32 => RuleKind::UndershootRising,
187            33 => RuleKind::MaxMinImpedance,
188            34 => RuleKind::SignalTopValue,
189            35 => RuleKind::SignalBaseValue,
190            36 => RuleKind::FlightTimeRising,
191            37 => RuleKind::FlightTimeFalling,
192            38 => RuleKind::LayerStack,
193            39 => RuleKind::SlopeRising,
194            40 => RuleKind::SlopeFalling,
195            41 => RuleKind::SupplyNets,
196            42 => RuleKind::HoleSize,
197            43 => RuleKind::TestPointStyle,
198            44 => RuleKind::TestPointUsage,
199            45 => RuleKind::UnconnectedPin,
200            46 => RuleKind::SMDToPlane,
201            47 => RuleKind::SMDNeckDown,
202            48 => RuleKind::LayerPairs,
203            49 => RuleKind::FanoutControl,
204            50 => RuleKind::Height,
205            51 => RuleKind::DiffPairsRouting,
206            52 => RuleKind::HoleToHoleClearance,
207            53 => RuleKind::MinimumSolderMaskSliver,
208            54 => RuleKind::SilkToSolderMaskClearance,
209            55 => RuleKind::SilkToSilkClearance,
210            56 => RuleKind::NetAntennae,
211            57 => RuleKind::AssemblyTestpoint,
212            58 => RuleKind::AssemblyTestPointUsage,
213            59 => RuleKind::SilkToBoardRegionClearance,
214            62 => RuleKind::UnpouredPolygon,
215            other => RuleKind::Unknown(other),
216        }
217    }
218
219    /// Get the numeric ID of this rule kind.
220    pub fn to_id(self) -> u16 {
221        match self {
222            RuleKind::Clearance => 0,
223            RuleKind::ParallelSegment => 1,
224            RuleKind::Width => 2,
225            RuleKind::Length => 3,
226            RuleKind::MatchedLengths => 4,
227            RuleKind::StubLength => 5,
228            RuleKind::PlaneConnect => 6,
229            RuleKind::RoutingTopology => 7,
230            RuleKind::RoutingPriority => 8,
231            RuleKind::RoutingLayers => 9,
232            RuleKind::RoutingCorners => 10,
233            RuleKind::RoutingVias => 11,
234            RuleKind::PlaneClearance => 12,
235            RuleKind::SolderMaskExpansion => 13,
236            RuleKind::PasteMaskExpansion => 14,
237            RuleKind::ShortCircuit => 15,
238            RuleKind::UnRoutedNet => 16,
239            RuleKind::ViasUnderSMD => 17,
240            RuleKind::MaximumViaCount => 18,
241            RuleKind::MinimumAnnularRing => 19,
242            RuleKind::PolygonConnect => 20,
243            RuleKind::AcuteAngle => 21,
244            RuleKind::RoomDefinition => 22,
245            RuleKind::SMDToCorner => 23,
246            RuleKind::ComponentClearance => 24,
247            RuleKind::ComponentOrientations => 25,
248            RuleKind::PermittedLayers => 26,
249            RuleKind::NetsToIgnore => 27,
250            RuleKind::SignalStimulus => 28,
251            RuleKind::OvershootFalling => 29,
252            RuleKind::OvershootRising => 30,
253            RuleKind::UndershootFalling => 31,
254            RuleKind::UndershootRising => 32,
255            RuleKind::MaxMinImpedance => 33,
256            RuleKind::SignalTopValue => 34,
257            RuleKind::SignalBaseValue => 35,
258            RuleKind::FlightTimeRising => 36,
259            RuleKind::FlightTimeFalling => 37,
260            RuleKind::LayerStack => 38,
261            RuleKind::SlopeRising => 39,
262            RuleKind::SlopeFalling => 40,
263            RuleKind::SupplyNets => 41,
264            RuleKind::HoleSize => 42,
265            RuleKind::TestPointStyle => 43,
266            RuleKind::TestPointUsage => 44,
267            RuleKind::UnconnectedPin => 45,
268            RuleKind::SMDToPlane => 46,
269            RuleKind::SMDNeckDown => 47,
270            RuleKind::LayerPairs => 48,
271            RuleKind::FanoutControl => 49,
272            RuleKind::Height => 50,
273            RuleKind::DiffPairsRouting => 51,
274            RuleKind::HoleToHoleClearance => 52,
275            RuleKind::MinimumSolderMaskSliver => 53,
276            RuleKind::SilkToSolderMaskClearance => 54,
277            RuleKind::SilkToSilkClearance => 55,
278            RuleKind::NetAntennae => 56,
279            RuleKind::AssemblyTestpoint => 57,
280            RuleKind::AssemblyTestPointUsage => 58,
281            RuleKind::SilkToBoardRegionClearance => 59,
282            RuleKind::UnpouredPolygon => 62,
283            RuleKind::Unknown(id) => id,
284        }
285    }
286
287    /// Get the name of this rule kind as it appears in Altium files.
288    pub fn name(&self) -> &'static str {
289        match self {
290            RuleKind::Clearance => "Clearance",
291            RuleKind::ParallelSegment => "ParallelSegment",
292            RuleKind::Width => "Width",
293            RuleKind::Length => "Length",
294            RuleKind::MatchedLengths => "MatchedLengths",
295            RuleKind::StubLength => "StubLength",
296            RuleKind::PlaneConnect => "PlaneConnect",
297            RuleKind::RoutingTopology => "RoutingTopology",
298            RuleKind::RoutingPriority => "RoutingPriority",
299            RuleKind::RoutingLayers => "RoutingLayers",
300            RuleKind::RoutingCorners => "RoutingCorners",
301            RuleKind::RoutingVias => "RoutingVias",
302            RuleKind::PlaneClearance => "PlaneClearance",
303            RuleKind::SolderMaskExpansion => "SolderMaskExpansion",
304            RuleKind::PasteMaskExpansion => "PasteMaskExpansion",
305            RuleKind::ShortCircuit => "ShortCircuit",
306            RuleKind::UnRoutedNet => "UnRoutedNet",
307            RuleKind::ViasUnderSMD => "ViasUnderSMD",
308            RuleKind::MaximumViaCount => "MaximumViaCount",
309            RuleKind::MinimumAnnularRing => "MinimumAnnularRing",
310            RuleKind::PolygonConnect => "PolygonConnect",
311            RuleKind::AcuteAngle => "AcuteAngle",
312            RuleKind::RoomDefinition => "RoomDefinition",
313            RuleKind::SMDToCorner => "SMDToCorner",
314            RuleKind::ComponentClearance => "ComponentClearance",
315            RuleKind::ComponentOrientations => "ComponentOrientations",
316            RuleKind::PermittedLayers => "PermittedLayers",
317            RuleKind::NetsToIgnore => "NetsToIgnore",
318            RuleKind::SignalStimulus => "SignalStimulus",
319            RuleKind::OvershootFalling => "OvershootFalling",
320            RuleKind::OvershootRising => "OvershootRising",
321            RuleKind::UndershootFalling => "UndershootFalling",
322            RuleKind::UndershootRising => "UndershootRising",
323            RuleKind::MaxMinImpedance => "MaxMinImpedance",
324            RuleKind::SignalTopValue => "SignalTopValue",
325            RuleKind::SignalBaseValue => "SignalBaseValue",
326            RuleKind::FlightTimeRising => "FlightTimeRising",
327            RuleKind::FlightTimeFalling => "FlightTimeFalling",
328            RuleKind::LayerStack => "LayerStack",
329            RuleKind::SlopeRising => "SlopeRising",
330            RuleKind::SlopeFalling => "SlopeFalling",
331            RuleKind::SupplyNets => "SupplyNets",
332            RuleKind::HoleSize => "HoleSize",
333            RuleKind::TestPointStyle => "Testpoint",
334            RuleKind::TestPointUsage => "TestPointUsage",
335            RuleKind::UnconnectedPin => "UnConnectedPin",
336            RuleKind::SMDToPlane => "SMDToPlane",
337            RuleKind::SMDNeckDown => "SMDNeckDown",
338            RuleKind::LayerPairs => "LayerPairs",
339            RuleKind::FanoutControl => "FanoutControl",
340            RuleKind::Height => "Height",
341            RuleKind::DiffPairsRouting => "DiffPairsRouting",
342            RuleKind::HoleToHoleClearance => "HoleToHoleClearance",
343            RuleKind::MinimumSolderMaskSliver => "MinimumSolderMaskSliver",
344            RuleKind::SilkToSolderMaskClearance => "SilkToSolderMaskClearance",
345            RuleKind::SilkToSilkClearance => "SilkToSilkClearance",
346            RuleKind::NetAntennae => "NetAntennae",
347            RuleKind::AssemblyTestpoint => "AssemblyTestpoint",
348            RuleKind::AssemblyTestPointUsage => "AssemblyTestPointUsage",
349            RuleKind::SilkToBoardRegionClearance => "SilkToBoardRegionClearance",
350            RuleKind::UnpouredPolygon => "UnpouredPolygon",
351            RuleKind::Unknown(_) => "Unknown",
352        }
353    }
354
355    /// Parse a rule kind from its name string.
356    pub fn from_name(name: &str) -> Option<Self> {
357        match name {
358            "Clearance" => Some(RuleKind::Clearance),
359            "ParallelSegment" => Some(RuleKind::ParallelSegment),
360            "Width" => Some(RuleKind::Width),
361            "Length" => Some(RuleKind::Length),
362            "MatchedLengths" => Some(RuleKind::MatchedLengths),
363            "StubLength" => Some(RuleKind::StubLength),
364            "PlaneConnect" => Some(RuleKind::PlaneConnect),
365            "RoutingTopology" => Some(RuleKind::RoutingTopology),
366            "RoutingPriority" => Some(RuleKind::RoutingPriority),
367            "RoutingLayers" => Some(RuleKind::RoutingLayers),
368            "RoutingCorners" => Some(RuleKind::RoutingCorners),
369            "RoutingVias" => Some(RuleKind::RoutingVias),
370            "PlaneClearance" => Some(RuleKind::PlaneClearance),
371            "SolderMaskExpansion" => Some(RuleKind::SolderMaskExpansion),
372            "PasteMaskExpansion" => Some(RuleKind::PasteMaskExpansion),
373            "ShortCircuit" => Some(RuleKind::ShortCircuit),
374            "UnRoutedNet" => Some(RuleKind::UnRoutedNet),
375            "ViasUnderSMD" => Some(RuleKind::ViasUnderSMD),
376            "MaximumViaCount" => Some(RuleKind::MaximumViaCount),
377            "MinimumAnnularRing" => Some(RuleKind::MinimumAnnularRing),
378            "PolygonConnect" => Some(RuleKind::PolygonConnect),
379            "AcuteAngle" => Some(RuleKind::AcuteAngle),
380            "RoomDefinition" => Some(RuleKind::RoomDefinition),
381            "SMDToCorner" => Some(RuleKind::SMDToCorner),
382            "ComponentClearance" => Some(RuleKind::ComponentClearance),
383            "ComponentOrientations" => Some(RuleKind::ComponentOrientations),
384            "PermittedLayers" => Some(RuleKind::PermittedLayers),
385            "NetsToIgnore" => Some(RuleKind::NetsToIgnore),
386            "SignalStimulus" => Some(RuleKind::SignalStimulus),
387            "OvershootFalling" => Some(RuleKind::OvershootFalling),
388            "OvershootRising" => Some(RuleKind::OvershootRising),
389            "UndershootFalling" => Some(RuleKind::UndershootFalling),
390            "UndershootRising" => Some(RuleKind::UndershootRising),
391            "MaxMinImpedance" => Some(RuleKind::MaxMinImpedance),
392            "SignalTopValue" => Some(RuleKind::SignalTopValue),
393            "SignalBaseValue" => Some(RuleKind::SignalBaseValue),
394            "FlightTimeRising" => Some(RuleKind::FlightTimeRising),
395            "FlightTimeFalling" => Some(RuleKind::FlightTimeFalling),
396            "LayerStack" => Some(RuleKind::LayerStack),
397            "SlopeRising" => Some(RuleKind::SlopeRising),
398            "SlopeFalling" => Some(RuleKind::SlopeFalling),
399            "SupplyNets" => Some(RuleKind::SupplyNets),
400            "HoleSize" => Some(RuleKind::HoleSize),
401            "Testpoint" | "TestPointStyle" => Some(RuleKind::TestPointStyle),
402            "TestPointUsage" => Some(RuleKind::TestPointUsage),
403            "UnConnectedPin" | "UnconnectedPin" => Some(RuleKind::UnconnectedPin),
404            "SMDToPlane" => Some(RuleKind::SMDToPlane),
405            "SMDNeckDown" => Some(RuleKind::SMDNeckDown),
406            "LayerPairs" => Some(RuleKind::LayerPairs),
407            "FanoutControl" => Some(RuleKind::FanoutControl),
408            "Height" => Some(RuleKind::Height),
409            "DiffPairsRouting" => Some(RuleKind::DiffPairsRouting),
410            "HoleToHoleClearance" => Some(RuleKind::HoleToHoleClearance),
411            "MinimumSolderMaskSliver" => Some(RuleKind::MinimumSolderMaskSliver),
412            "SilkToSolderMaskClearance" => Some(RuleKind::SilkToSolderMaskClearance),
413            "SilkToSilkClearance" => Some(RuleKind::SilkToSilkClearance),
414            "NetAntennae" => Some(RuleKind::NetAntennae),
415            "AssemblyTestpoint" => Some(RuleKind::AssemblyTestpoint),
416            "AssemblyTestPointUsage" => Some(RuleKind::AssemblyTestPointUsage),
417            "SilkToBoardRegionClearance" => Some(RuleKind::SilkToBoardRegionClearance),
418            "UnpouredPolygon" => Some(RuleKind::UnpouredPolygon),
419            _ => None,
420        }
421    }
422}
423
424impl fmt::Display for RuleKind {
425    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
426        match self {
427            RuleKind::Unknown(id) => write!(f, "Unknown({})", id),
428            _ => write!(f, "{}", self.name()),
429        }
430    }
431}
432
433/// Net scope for rule application.
434#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
435pub enum NetScope {
436    #[default]
437    AnyNet,
438    InNetClass,
439    Net,
440    FromTo,
441    Board,
442    DifferentialPair,
443}
444
445impl NetScope {
446    pub fn parse(s: &str) -> Self {
447        match s {
448            "AnyNet" => NetScope::AnyNet,
449            "InNetClass" => NetScope::InNetClass,
450            "Net" => NetScope::Net,
451            "FromTo" => NetScope::FromTo,
452            "Board" => NetScope::Board,
453            "DifferentialPair" => NetScope::DifferentialPair,
454            _ => NetScope::AnyNet,
455        }
456    }
457
458    pub fn to_str(&self) -> &'static str {
459        match self {
460            NetScope::AnyNet => "AnyNet",
461            NetScope::InNetClass => "InNetClass",
462            NetScope::Net => "Net",
463            NetScope::FromTo => "FromTo",
464            NetScope::Board => "Board",
465            NetScope::DifferentialPair => "DifferentialPair",
466        }
467    }
468}
469
470/// Layer kind for rule application.
471#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
472pub enum LayerKind {
473    #[default]
474    SameLayer,
475    AnyLayer,
476    OnLayer,
477    InLayerClass,
478}
479
480impl LayerKind {
481    pub fn parse(s: &str) -> Self {
482        match s {
483            "SameLayer" => LayerKind::SameLayer,
484            "AnyLayer" => LayerKind::AnyLayer,
485            "OnLayer" => LayerKind::OnLayer,
486            "InLayerClass" => LayerKind::InLayerClass,
487            _ => LayerKind::SameLayer,
488        }
489    }
490
491    pub fn to_str(&self) -> &'static str {
492        match self {
493            LayerKind::SameLayer => "SameLayer",
494            LayerKind::AnyLayer => "AnyLayer",
495            LayerKind::OnLayer => "OnLayer",
496            LayerKind::InLayerClass => "InLayerClass",
497        }
498    }
499}
500
501/// A PCB design rule.
502///
503/// Design rules contain common metadata plus rule-specific parameters.
504/// The parameters are stored in a ParameterCollection to support
505/// all rule types and preserve unknown fields for round-tripping.
506#[derive(Debug, Clone)]
507pub struct PcbRule {
508    /// The kind of rule (Clearance, Width, etc.).
509    pub kind: RuleKind,
510    /// Rule name (user-defined or default).
511    pub name: String,
512    /// Whether the rule is enabled.
513    pub enabled: bool,
514    /// Rule priority (lower is higher priority).
515    pub priority: i32,
516    /// Rule comment/description.
517    pub comment: String,
518    /// Unique ID for the rule.
519    pub unique_id: String,
520    /// Net scope for rule application.
521    pub net_scope: NetScope,
522    /// Layer kind for rule application.
523    pub layer_kind: LayerKind,
524    /// First scope expression (e.g., "All", "IsTrack", net name).
525    pub scope1_expression: String,
526    /// Second scope expression.
527    pub scope2_expression: String,
528    /// Whether the rule is defined by a logical document.
529    pub defined_by_logical_document: bool,
530    /// All parameters (including rule-specific ones).
531    /// This preserves any unknown parameters for round-tripping.
532    pub params: ParameterCollection,
533}
534
535impl Default for PcbRule {
536    fn default() -> Self {
537        PcbRule {
538            kind: RuleKind::Clearance,
539            name: String::new(),
540            enabled: true,
541            priority: 1,
542            comment: String::new(),
543            unique_id: String::new(),
544            net_scope: NetScope::AnyNet,
545            layer_kind: LayerKind::SameLayer,
546            scope1_expression: "All".to_string(),
547            scope2_expression: "All".to_string(),
548            defined_by_logical_document: false,
549            params: ParameterCollection::new(),
550        }
551    }
552}
553
554impl PcbRule {
555    /// Create a new rule with the given kind and name.
556    pub fn new(kind: RuleKind, name: &str) -> Self {
557        PcbRule {
558            kind,
559            name: name.to_string(),
560            ..Default::default()
561        }
562    }
563
564    /// Read a design rule from a binary stream.
565    ///
566    /// Format:
567    /// - 2 bytes: rule kind ID (u16 LE)
568    /// - 4 bytes: data size (u32 LE)
569    /// - data_size bytes: pipe-delimited parameters (null-terminated)
570    pub fn read_from<R: Read>(reader: &mut R) -> Result<Self> {
571        // Read rule kind ID
572        let kind_id = reader.read_u16::<LittleEndian>()?;
573        let kind = RuleKind::from_id(kind_id);
574
575        // Read data size
576        let data_size = reader.read_u32::<LittleEndian>()? as usize;
577
578        if data_size == 0 {
579            return Ok(PcbRule {
580                kind,
581                ..Default::default()
582            });
583        }
584
585        // Read parameter data
586        let mut data = vec![0u8; data_size];
587        reader.read_exact(&mut data)?;
588
589        // Remove null terminator if present
590        let end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
591        let param_str = decode_windows_1252(&data[..end]);
592
593        // Parse parameters
594        let params = ParameterCollection::from_string(&param_str);
595
596        // Extract common fields
597        let name = params
598            .get("NAME")
599            .map(|v| v.as_str().to_string())
600            .unwrap_or_default();
601        let enabled = params
602            .get("ENABLED")
603            .map(|v| v.as_bool_or(true))
604            .unwrap_or(true);
605        let priority = params.get("PRIORITY").map(|v| v.as_int_or(1)).unwrap_or(1);
606        let comment = params
607            .get("COMMENT")
608            .map(|v| v.as_str().to_string())
609            .unwrap_or_default();
610        let unique_id = params
611            .get("UNIQUEID")
612            .map(|v| v.as_str().to_string())
613            .unwrap_or_default();
614        let net_scope = params
615            .get("NETSCOPE")
616            .map(|v| NetScope::parse(v.as_str()))
617            .unwrap_or_default();
618        let layer_kind = params
619            .get("LAYERKIND")
620            .map(|v| LayerKind::parse(v.as_str()))
621            .unwrap_or_default();
622        let scope1_expression = params
623            .get("SCOPE1EXPRESSION")
624            .map(|v| v.as_str().to_string())
625            .unwrap_or_else(|| "All".to_string());
626        let scope2_expression = params
627            .get("SCOPE2EXPRESSION")
628            .map(|v| v.as_str().to_string())
629            .unwrap_or_else(|| "All".to_string());
630        let defined_by_logical_document = params
631            .get("DEFINEDBYLOGICALDOCUMENT")
632            .map(|v| v.as_bool_or(false))
633            .unwrap_or(false);
634
635        Ok(PcbRule {
636            kind,
637            name,
638            enabled,
639            priority,
640            comment,
641            unique_id,
642            net_scope,
643            layer_kind,
644            scope1_expression,
645            scope2_expression,
646            defined_by_logical_document,
647            params,
648        })
649    }
650
651    /// Write a design rule to a binary stream.
652    pub fn write_to<W: Write>(&self, writer: &mut W) -> Result<()> {
653        // Sync common fields back to params
654        let mut params = self.params.clone();
655        params.add("RULEKIND", self.kind.name());
656        params.add("NAME", &self.name);
657        params.add("ENABLED", if self.enabled { "TRUE" } else { "FALSE" });
658        params.add("PRIORITY", &self.priority.to_string());
659        params.add("COMMENT", &self.comment);
660        params.add("UNIQUEID", &self.unique_id);
661        params.add("NETSCOPE", self.net_scope.to_str());
662        params.add("LAYERKIND", self.layer_kind.to_str());
663        params.add("SCOPE1EXPRESSION", &self.scope1_expression);
664        params.add("SCOPE2EXPRESSION", &self.scope2_expression);
665        params.add(
666            "DEFINEDBYLOGICALDOCUMENT",
667            if self.defined_by_logical_document {
668                "TRUE"
669            } else {
670                "FALSE"
671            },
672        );
673
674        // Convert to string and encode
675        let param_str = params.to_string();
676        let mut data = encode_windows_1252(&param_str);
677        data.push(0); // Null terminator
678
679        // Write rule kind ID
680        writer.write_u16::<LittleEndian>(self.kind.to_id())?;
681
682        // Write data size
683        writer.write_u32::<LittleEndian>(data.len() as u32)?;
684
685        // Write parameter data
686        writer.write_all(&data)?;
687
688        Ok(())
689    }
690
691    /// Get the binary size of this rule.
692    pub fn binary_size(&self) -> usize {
693        // 2 bytes kind + 4 bytes size + params + null terminator
694        let param_str = self.params.to_string();
695        6 + param_str.len() + 1
696    }
697
698    // ========== Convenience accessors for common rule-specific parameters ==========
699
700    /// Get a clearance value (for Clearance rules).
701    pub fn clearance(&self) -> Option<Coord> {
702        self.params.get("GAP").and_then(|v| v.as_coord().ok())
703    }
704
705    /// Set a clearance value.
706    pub fn set_clearance(&mut self, value: Coord) {
707        self.params.add_coord("GAP", value);
708    }
709
710    /// Get minimum width (for Width rules).
711    pub fn min_width(&self) -> Option<Coord> {
712        self.params.get("MINWIDTH").and_then(|v| v.as_coord().ok())
713    }
714
715    /// Set minimum width.
716    pub fn set_min_width(&mut self, value: Coord) {
717        self.params.add_coord("MINWIDTH", value);
718    }
719
720    /// Get maximum width (for Width rules).
721    pub fn max_width(&self) -> Option<Coord> {
722        self.params.get("MAXWIDTH").and_then(|v| v.as_coord().ok())
723    }
724
725    /// Set maximum width.
726    pub fn set_max_width(&mut self, value: Coord) {
727        self.params.add_coord("MAXWIDTH", value);
728    }
729
730    /// Get preferred width (for Width rules).
731    pub fn preferred_width(&self) -> Option<Coord> {
732        self.params.get("PREFWIDTH").and_then(|v| v.as_coord().ok())
733    }
734
735    /// Set preferred width.
736    pub fn set_preferred_width(&mut self, value: Coord) {
737        self.params.add_coord("PREFWIDTH", value);
738    }
739
740    /// Get minimum hole size (for HoleSize rules).
741    pub fn min_hole_size(&self) -> Option<Coord> {
742        self.params
743            .get("MINHOLESIZE")
744            .and_then(|v| v.as_coord().ok())
745    }
746
747    /// Set minimum hole size.
748    pub fn set_min_hole_size(&mut self, value: Coord) {
749        self.params.add_coord("MINHOLESIZE", value);
750    }
751
752    /// Get maximum hole size (for HoleSize rules).
753    pub fn max_hole_size(&self) -> Option<Coord> {
754        self.params
755            .get("MAXHOLESIZE")
756            .and_then(|v| v.as_coord().ok())
757    }
758
759    /// Set maximum hole size.
760    pub fn set_max_hole_size(&mut self, value: Coord) {
761        self.params.add_coord("MAXHOLESIZE", value);
762    }
763
764    /// Get solder mask expansion (for SolderMaskExpansion rules).
765    pub fn solder_mask_expansion(&self) -> Option<Coord> {
766        self.params.get("EXPANSION").and_then(|v| v.as_coord().ok())
767    }
768
769    /// Set solder mask expansion.
770    pub fn set_solder_mask_expansion(&mut self, value: Coord) {
771        self.params.add_coord("EXPANSION", value);
772    }
773
774    /// Get paste mask expansion (for PasteMaskExpansion rules).
775    pub fn paste_mask_expansion(&self) -> Option<Coord> {
776        self.params.get("EXPANSION").and_then(|v| v.as_coord().ok())
777    }
778
779    /// Set paste mask expansion.
780    pub fn set_paste_mask_expansion(&mut self, value: Coord) {
781        self.params.add_coord("EXPANSION", value);
782    }
783
784    // ========== Differential Pair Routing Parameters ==========
785
786    /// Get minimum gap for differential pairs.
787    pub fn diff_pair_min_gap(&self) -> Option<Coord> {
788        self.params.get("MINLIMIT").and_then(|v| v.as_coord().ok())
789    }
790
791    /// Set minimum gap for differential pairs.
792    pub fn set_diff_pair_min_gap(&mut self, value: Coord) {
793        self.params.add_coord("MINLIMIT", value);
794    }
795
796    /// Get maximum gap for differential pairs.
797    pub fn diff_pair_max_gap(&self) -> Option<Coord> {
798        self.params.get("MAXLIMIT").and_then(|v| v.as_coord().ok())
799    }
800
801    /// Set maximum gap for differential pairs.
802    pub fn set_diff_pair_max_gap(&mut self, value: Coord) {
803        self.params.add_coord("MAXLIMIT", value);
804    }
805
806    /// Get preferred/most frequent gap for differential pairs.
807    pub fn diff_pair_preferred_gap(&self) -> Option<Coord> {
808        self.params
809            .get("MOSTFREQGAP")
810            .and_then(|v| v.as_coord().ok())
811    }
812
813    /// Set preferred gap for differential pairs.
814    pub fn set_diff_pair_preferred_gap(&mut self, value: Coord) {
815        self.params.add_coord("MOSTFREQGAP", value);
816    }
817
818    /// Get maximum uncoupled length for differential pairs.
819    pub fn diff_pair_max_uncoupled_length(&self) -> Option<Coord> {
820        self.params
821            .get("MAXUNCOUPLEDLENGTH")
822            .and_then(|v| v.as_coord().ok())
823    }
824
825    /// Set maximum uncoupled length for differential pairs.
826    pub fn set_diff_pair_max_uncoupled_length(&mut self, value: Coord) {
827        self.params.add_coord("MAXUNCOUPLEDLENGTH", value);
828    }
829
830    // ========== Via Parameters ==========
831
832    /// Get via minimum diameter (RoutingVias rule).
833    pub fn via_min_diameter(&self) -> Option<Coord> {
834        self.params.get("MINWIDTH").and_then(|v| v.as_coord().ok())
835    }
836
837    /// Set via minimum diameter.
838    pub fn set_via_min_diameter(&mut self, value: Coord) {
839        self.params.add_coord("MINWIDTH", value);
840    }
841
842    /// Get via maximum diameter (RoutingVias rule).
843    pub fn via_max_diameter(&self) -> Option<Coord> {
844        self.params.get("MAXWIDTH").and_then(|v| v.as_coord().ok())
845    }
846
847    /// Set via maximum diameter.
848    pub fn set_via_max_diameter(&mut self, value: Coord) {
849        self.params.add_coord("MAXWIDTH", value);
850    }
851
852    /// Get via preferred diameter (RoutingVias rule).
853    pub fn via_preferred_diameter(&self) -> Option<Coord> {
854        self.params.get("PREFWIDTH").and_then(|v| v.as_coord().ok())
855    }
856
857    /// Set via preferred diameter.
858    pub fn set_via_preferred_diameter(&mut self, value: Coord) {
859        self.params.add_coord("PREFWIDTH", value);
860    }
861
862    // ========== Annular Ring Parameters ==========
863
864    /// Get minimum annular ring (MinimumAnnularRing rule).
865    pub fn annular_ring(&self) -> Option<Coord> {
866        self.params
867            .get("ANNULARRING")
868            .and_then(|v| v.as_coord().ok())
869    }
870
871    /// Set minimum annular ring.
872    pub fn set_annular_ring(&mut self, value: Coord) {
873        self.params.add_coord("ANNULARRING", value);
874    }
875
876    // ========== Hole Clearance Parameters ==========
877
878    /// Get hole-to-hole clearance (HoleToHoleClearance rule).
879    pub fn hole_to_hole_clearance(&self) -> Option<Coord> {
880        self.params.get("GAP").and_then(|v| v.as_coord().ok())
881    }
882
883    /// Set hole-to-hole clearance.
884    pub fn set_hole_to_hole_clearance(&mut self, value: Coord) {
885        self.params.add_coord("GAP", value);
886    }
887
888    // ========== Silkscreen Parameters ==========
889
890    /// Get minimum solder mask sliver.
891    pub fn min_solder_mask_sliver(&self) -> Option<Coord> {
892        self.params.get("MINLIMIT").and_then(|v| v.as_coord().ok())
893    }
894
895    /// Set minimum solder mask sliver.
896    pub fn set_min_solder_mask_sliver(&mut self, value: Coord) {
897        self.params.add_coord("MINLIMIT", value);
898    }
899
900    // ========== Plane Connection Parameters ==========
901
902    /// Get plane connect style (Relief, Direct, NoConnect).
903    pub fn plane_connect_style(&self) -> Option<String> {
904        self.params
905            .get("CONNECTSTYLE")
906            .map(|v| v.as_str().to_string())
907    }
908
909    /// Set plane connect style.
910    pub fn set_plane_connect_style(&mut self, style: &str) {
911        self.params.add("CONNECTSTYLE", style);
912    }
913
914    /// Get thermal relief air gap width.
915    pub fn thermal_relief_air_gap(&self) -> Option<Coord> {
916        self.params
917            .get("AIRGAPWIDTH")
918            .and_then(|v| v.as_coord().ok())
919    }
920
921    /// Set thermal relief air gap width.
922    pub fn set_thermal_relief_air_gap(&mut self, value: Coord) {
923        self.params.add_coord("AIRGAPWIDTH", value);
924    }
925
926    /// Get thermal relief conductor width.
927    pub fn thermal_relief_conductor_width(&self) -> Option<Coord> {
928        self.params
929            .get("CONDUCTORWIDTH")
930            .and_then(|v| v.as_coord().ok())
931    }
932
933    /// Set thermal relief conductor width.
934    pub fn set_thermal_relief_conductor_width(&mut self, value: Coord) {
935        self.params.add_coord("CONDUCTORWIDTH", value);
936    }
937
938    /// Get thermal relief conductor count.
939    pub fn thermal_relief_conductors(&self) -> Option<i32> {
940        self.params.get("CONDUCTORS").map(|v| v.as_int_or(4))
941    }
942
943    /// Set thermal relief conductor count.
944    pub fn set_thermal_relief_conductors(&mut self, count: i32) {
945        self.params.add("CONDUCTORS", &count.to_string());
946    }
947
948    // ========== Routing Corner Parameters ==========
949
950    /// Get routing corner style (45, 90, Any).
951    pub fn routing_corner_style(&self) -> Option<String> {
952        self.params.get("STYLE").map(|v| v.as_str().to_string())
953    }
954
955    /// Set routing corner style.
956    pub fn set_routing_corner_style(&mut self, style: &str) {
957        self.params.add("STYLE", style);
958    }
959
960    // ========== Routing Topology Parameters ==========
961
962    /// Get routing topology (Shortest, DaisyChain, Star, Starburst).
963    pub fn routing_topology(&self) -> Option<String> {
964        self.params.get("TOPOLOGY").map(|v| v.as_str().to_string())
965    }
966
967    /// Set routing topology.
968    pub fn set_routing_topology(&mut self, topology: &str) {
969        self.params.add("TOPOLOGY", topology);
970    }
971
972    // ========== Component Parameters ==========
973
974    /// Get component clearance.
975    pub fn component_clearance(&self) -> Option<Coord> {
976        self.params.get("GAP").and_then(|v| v.as_coord().ok())
977    }
978
979    /// Set component clearance.
980    pub fn set_component_clearance(&mut self, value: Coord) {
981        self.params.add_coord("GAP", value);
982    }
983
984    /// Get maximum component height.
985    pub fn max_height(&self) -> Option<Coord> {
986        self.params.get("MAXLIMIT").and_then(|v| v.as_coord().ok())
987    }
988
989    /// Set maximum component height.
990    pub fn set_max_height(&mut self, value: Coord) {
991        self.params.add_coord("MAXLIMIT", value);
992    }
993
994    // ========== Length Parameters ==========
995
996    /// Get minimum length (Length rule).
997    pub fn min_length(&self) -> Option<Coord> {
998        self.params.get("MINLIMIT").and_then(|v| v.as_coord().ok())
999    }
1000
1001    /// Set minimum length.
1002    pub fn set_min_length(&mut self, value: Coord) {
1003        self.params.add_coord("MINLIMIT", value);
1004    }
1005
1006    /// Get maximum length (Length rule).
1007    pub fn max_length(&self) -> Option<Coord> {
1008        self.params.get("MAXLIMIT").and_then(|v| v.as_coord().ok())
1009    }
1010
1011    /// Set maximum length.
1012    pub fn set_max_length(&mut self, value: Coord) {
1013        self.params.add_coord("MAXLIMIT", value);
1014    }
1015
1016    /// Get matched length tolerance.
1017    pub fn matched_length_tolerance(&self) -> Option<Coord> {
1018        self.params.get("TOLERANCE").and_then(|v| v.as_coord().ok())
1019    }
1020
1021    /// Set matched length tolerance.
1022    pub fn set_matched_length_tolerance(&mut self, value: Coord) {
1023        self.params.add_coord("TOLERANCE", value);
1024    }
1025
1026    /// Get maximum stub length.
1027    pub fn max_stub_length(&self) -> Option<Coord> {
1028        self.params.get("MAXLIMIT").and_then(|v| v.as_coord().ok())
1029    }
1030
1031    /// Set maximum stub length.
1032    pub fn set_max_stub_length(&mut self, value: Coord) {
1033        self.params.add_coord("MAXLIMIT", value);
1034    }
1035
1036    // ========== Signal Integrity Parameters ==========
1037
1038    /// Get minimum impedance.
1039    pub fn min_impedance(&self) -> Option<f64> {
1040        self.params.get("MINLIMIT").map(|v| v.as_double_or(0.0))
1041    }
1042
1043    /// Set minimum impedance.
1044    pub fn set_min_impedance(&mut self, value: f64) {
1045        self.params.add("MINLIMIT", &value.to_string());
1046    }
1047
1048    /// Get maximum impedance.
1049    pub fn max_impedance(&self) -> Option<f64> {
1050        self.params.get("MAXLIMIT").map(|v| v.as_double_or(0.0))
1051    }
1052
1053    /// Set maximum impedance.
1054    pub fn set_max_impedance(&mut self, value: f64) {
1055        self.params.add("MAXLIMIT", &value.to_string());
1056    }
1057
1058    // ========== Via Under SMD Parameters ==========
1059
1060    /// Check if vias under SMD are allowed.
1061    pub fn vias_under_smd_allowed(&self) -> bool {
1062        self.params
1063            .get("ALLOWVIAS")
1064            .map(|v| v.as_bool_or(false))
1065            .unwrap_or(false)
1066    }
1067
1068    /// Set whether vias under SMD are allowed.
1069    pub fn set_vias_under_smd_allowed(&mut self, allowed: bool) {
1070        self.params
1071            .add("ALLOWVIAS", if allowed { "TRUE" } else { "FALSE" });
1072    }
1073
1074    // ========== Maximum Via Count Parameters ==========
1075
1076    /// Get maximum via count.
1077    pub fn max_via_count(&self) -> Option<i32> {
1078        self.params.get("MAXVIACOUNT").map(|v| v.as_int_or(0))
1079    }
1080
1081    /// Set maximum via count.
1082    pub fn set_max_via_count(&mut self, count: i32) {
1083        self.params.add("MAXVIACOUNT", &count.to_string());
1084    }
1085
1086    // ========== Fanout Parameters ==========
1087
1088    /// Get fanout direction.
1089    pub fn fanout_direction(&self) -> Option<String> {
1090        self.params.get("DIRECTION").map(|v| v.as_str().to_string())
1091    }
1092
1093    /// Set fanout direction.
1094    pub fn set_fanout_direction(&mut self, direction: &str) {
1095        self.params.add("DIRECTION", direction);
1096    }
1097
1098    /// Get fanout style.
1099    pub fn fanout_style(&self) -> Option<String> {
1100        self.params.get("STYLE").map(|v| v.as_str().to_string())
1101    }
1102
1103    /// Set fanout style.
1104    pub fn set_fanout_style(&mut self, style: &str) {
1105        self.params.add("STYLE", style);
1106    }
1107
1108    // ========== Generic Limit Parameters ==========
1109
1110    /// Get generic minimum limit (works for various rule types).
1111    pub fn generic_min_limit(&self) -> Option<Coord> {
1112        self.params.get("MINLIMIT").and_then(|v| v.as_coord().ok())
1113    }
1114
1115    /// Get generic maximum limit (works for various rule types).
1116    pub fn generic_max_limit(&self) -> Option<Coord> {
1117        self.params.get("MAXLIMIT").and_then(|v| v.as_coord().ok())
1118    }
1119
1120    /// Get generic gap parameter (works for clearance-type rules).
1121    pub fn generic_gap(&self) -> Option<Coord> {
1122        self.params.get("GAP").and_then(|v| v.as_coord().ok())
1123    }
1124}
1125
1126/// Read all design rules from a binary stream.
1127pub fn read_rules<R: Read>(reader: &mut R, data_len: usize) -> Result<Vec<PcbRule>> {
1128    let mut rules = Vec::new();
1129    let mut bytes_read = 0;
1130
1131    while bytes_read < data_len {
1132        let start_pos = bytes_read;
1133
1134        // Check if we have at least 6 bytes for header
1135        if bytes_read + 6 > data_len {
1136            break;
1137        }
1138
1139        match PcbRule::read_from(reader) {
1140            Ok(rule) => {
1141                // Calculate bytes consumed
1142                let rule_size = rule.binary_size();
1143                bytes_read += rule_size;
1144                rules.push(rule);
1145            }
1146            Err(e) => {
1147                // If we can't read a complete rule, stop
1148                if start_pos == bytes_read {
1149                    return Err(e);
1150                }
1151                break;
1152            }
1153        }
1154    }
1155
1156    Ok(rules)
1157}
1158
1159/// Write all design rules to a binary stream.
1160pub fn write_rules<W: Write>(writer: &mut W, rules: &[PcbRule]) -> Result<()> {
1161    for rule in rules {
1162        rule.write_to(writer)?;
1163    }
1164    Ok(())
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169    use super::*;
1170    use std::io::Cursor;
1171
1172    #[test]
1173    fn test_rule_kind_roundtrip() {
1174        let kinds = [
1175            RuleKind::Clearance,
1176            RuleKind::Width,
1177            RuleKind::RoutingLayers,
1178            RuleKind::DiffPairsRouting,
1179        ];
1180
1181        for kind in &kinds {
1182            let id = kind.to_id();
1183            let parsed = RuleKind::from_id(id);
1184            assert_eq!(*kind, parsed);
1185        }
1186    }
1187
1188    #[test]
1189    fn test_rule_kind_name() {
1190        assert_eq!(RuleKind::Clearance.name(), "Clearance");
1191        assert_eq!(RuleKind::Width.name(), "Width");
1192        assert_eq!(RuleKind::DiffPairsRouting.name(), "DiffPairsRouting");
1193    }
1194
1195    #[test]
1196    fn test_rule_serialization() {
1197        let mut rule = PcbRule::new(RuleKind::Clearance, "TestClearance");
1198        rule.enabled = true;
1199        rule.priority = 1;
1200        rule.scope1_expression = "All".to_string();
1201        rule.scope2_expression = "All".to_string();
1202        rule.params.add("GAP", "10mil");
1203
1204        // Write to buffer
1205        let mut buffer = Vec::new();
1206        rule.write_to(&mut buffer).unwrap();
1207
1208        // Read back
1209        let mut cursor = Cursor::new(&buffer);
1210        let parsed = PcbRule::read_from(&mut cursor).unwrap();
1211
1212        assert_eq!(parsed.kind, RuleKind::Clearance);
1213        assert_eq!(parsed.name, "TestClearance");
1214        assert!(parsed.enabled);
1215        assert_eq!(parsed.priority, 1);
1216    }
1217}