Skip to main content

altium_format/records/pcb/
polygon.rs

1//! PCB polygon (copper pour) record type.
2//!
3//! Polygons are copper pours that fill areas on PCB layers, typically
4//! used for power planes, ground planes, and shielding.
5
6use crate::types::{Coord, Layer, ParameterCollection};
7
8/// Type of polygon.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum PolygonType {
11    /// Solid copper pour polygon.
12    #[default]
13    Polygon,
14    /// Split plane polygon.
15    SplitPlane,
16    /// Cutout region.
17    Cutout,
18}
19
20impl PolygonType {
21    /// Parse from string value.
22    pub fn parse(s: &str) -> Self {
23        match s.to_uppercase().as_str() {
24            "POLYGON" => PolygonType::Polygon,
25            "SPLITPLANE" => PolygonType::SplitPlane,
26            "CUTOUT" => PolygonType::Cutout,
27            _ => PolygonType::Polygon,
28        }
29    }
30
31    /// Convert to string value.
32    pub fn as_str(&self) -> &'static str {
33        match self {
34            PolygonType::Polygon => "Polygon",
35            PolygonType::SplitPlane => "SplitPlane",
36            PolygonType::Cutout => "Cutout",
37        }
38    }
39}
40
41/// Hatch style for polygon fill.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum HatchStyle {
44    /// No fill (outline only).
45    None,
46    /// 45-degree hatch lines.
47    Hatch45,
48    /// 90-degree hatch lines.
49    Hatch90,
50    /// Horizontal hatch lines.
51    HatchHorizontal,
52    /// Vertical hatch lines.
53    HatchVertical,
54    /// Solid fill.
55    #[default]
56    Solid,
57}
58
59impl HatchStyle {
60    /// Parse from string value.
61    pub fn parse(s: &str) -> Self {
62        match s.to_uppercase().as_str() {
63            "NONE" => HatchStyle::None,
64            "45DEGREE" | "HATCH45" => HatchStyle::Hatch45,
65            "90DEGREE" | "HATCH90" => HatchStyle::Hatch90,
66            "HORIZONTAL" => HatchStyle::HatchHorizontal,
67            "VERTICAL" => HatchStyle::HatchVertical,
68            "SOLID" => HatchStyle::Solid,
69            _ => HatchStyle::Solid,
70        }
71    }
72
73    /// Convert to string value.
74    pub fn as_str(&self) -> &'static str {
75        match self {
76            HatchStyle::None => "None",
77            HatchStyle::Hatch45 => "45Degree",
78            HatchStyle::Hatch90 => "90Degree",
79            HatchStyle::HatchHorizontal => "Horizontal",
80            HatchStyle::HatchVertical => "Vertical",
81            HatchStyle::Solid => "Solid",
82        }
83    }
84}
85
86/// Vertex kind in polygon outline.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
88#[repr(u8)]
89pub enum PolygonVertexKind {
90    /// Straight line segment.
91    #[default]
92    Line = 0,
93    /// Arc segment.
94    Arc = 1,
95}
96
97impl PolygonVertexKind {
98    /// Parse from integer value.
99    pub fn from_int(value: i32) -> Self {
100        match value {
101            1 => PolygonVertexKind::Arc,
102            _ => PolygonVertexKind::Line,
103        }
104    }
105
106    /// Convert to integer value.
107    pub fn to_int(self) -> i32 {
108        self as i32
109    }
110}
111
112/// A vertex in the polygon outline.
113#[derive(Debug, Clone, Default)]
114pub struct PolygonVertex {
115    /// Vertex kind (line or arc).
116    pub kind: PolygonVertexKind,
117    /// X coordinate of the vertex.
118    pub x: Coord,
119    /// Y coordinate of the vertex.
120    pub y: Coord,
121    /// Arc center X (for arc vertices).
122    pub center_x: Coord,
123    /// Arc center Y (for arc vertices).
124    pub center_y: Coord,
125    /// Arc start angle in degrees.
126    pub start_angle: f64,
127    /// Arc end angle in degrees.
128    pub end_angle: f64,
129    /// Arc radius.
130    pub radius: Coord,
131}
132
133/// PCB polygon (copper pour) record.
134///
135/// Polygons are copper pours that fill areas on PCB layers. They automatically
136/// avoid pads, tracks, and other obstacles while maintaining clearance rules.
137#[derive(Debug, Clone, Default)]
138pub struct PcbPolygon {
139    /// Layer the polygon is on.
140    pub layer: Layer,
141    /// Whether the polygon is locked.
142    pub locked: bool,
143    /// Whether this is a polygon outline (not filled).
144    pub polygon_outline: bool,
145    /// Whether the polygon was user-routed.
146    pub user_routed: bool,
147    /// Whether this is a keepout region.
148    pub keepout: bool,
149    /// Union index (for grouping).
150    pub union_index: i32,
151    /// Whether primitives are locked.
152    pub primitive_lock: bool,
153    /// Type of polygon (Polygon, SplitPlane, Cutout).
154    pub polygon_type: PolygonType,
155    /// Whether to pour over all same-net objects.
156    pub pour_over: bool,
157    /// Whether to remove dead copper islands.
158    pub remove_dead: bool,
159    /// Grid size for pour.
160    pub grid_size: Coord,
161    /// Track width for pour.
162    pub track_width: Coord,
163    /// Hatch style for fill.
164    pub hatch_style: HatchStyle,
165    /// Whether to use octagons for thermal relief.
166    pub use_octagons: bool,
167    /// Minimum primitive length.
168    pub min_prim_length: Coord,
169    /// Net name the polygon is connected to.
170    pub net_name: String,
171    /// Unique ID.
172    pub unique_id: String,
173    /// Polygon outline vertices.
174    pub vertices: Vec<PolygonVertex>,
175    /// All parameters for round-tripping.
176    pub params: ParameterCollection,
177}
178
179impl PcbPolygon {
180    /// Parse a polygon from parameters.
181    pub fn from_params(params: &ParameterCollection) -> Self {
182        let mut polygon = Self {
183            layer: params
184                .get("LAYER")
185                .map(|v| v.as_layer())
186                .unwrap_or_default(),
187            locked: params
188                .get("LOCKED")
189                .map(|v| v.as_bool_or(false))
190                .unwrap_or(false),
191            polygon_outline: params
192                .get("POLYGONOUTLINE")
193                .map(|v| v.as_bool_or(false))
194                .unwrap_or(false),
195            user_routed: params
196                .get("USERROUTED")
197                .map(|v| v.as_bool_or(true))
198                .unwrap_or(true),
199            keepout: params
200                .get("KEEPOUT")
201                .map(|v| v.as_bool_or(false))
202                .unwrap_or(false),
203            union_index: params
204                .get("UNIONINDEX")
205                .map(|v| v.as_int_or(0))
206                .unwrap_or(0),
207            primitive_lock: params
208                .get("PRIMITIVELOCK")
209                .map(|v| v.as_bool_or(false))
210                .unwrap_or(false),
211            polygon_type: params
212                .get("POLYGONTYPE")
213                .map(|v| PolygonType::parse(v.as_str()))
214                .unwrap_or_default(),
215            pour_over: params
216                .get("POUROVER")
217                .map(|v| v.as_bool_or(false))
218                .unwrap_or(false),
219            remove_dead: params
220                .get("REMOVEDEAD")
221                .map(|v| v.as_bool_or(false))
222                .unwrap_or(false),
223            grid_size: params
224                .get("GRIDSIZE")
225                .and_then(|v| v.as_coord().ok())
226                .unwrap_or_default(),
227            track_width: params
228                .get("TRACKWIDTH")
229                .and_then(|v| v.as_coord().ok())
230                .unwrap_or_default(),
231            hatch_style: params
232                .get("HATCHSTYLE")
233                .map(|v| HatchStyle::parse(v.as_str()))
234                .unwrap_or_default(),
235            use_octagons: params
236                .get("USEOCTAGONS")
237                .map(|v| v.as_bool_or(false))
238                .unwrap_or(false),
239            min_prim_length: params
240                .get("MINPRIMLENGTH")
241                .and_then(|v| v.as_coord().ok())
242                .unwrap_or_default(),
243            net_name: params
244                .get("NET")
245                .or_else(|| params.get("NETNAME"))
246                .map(|v| v.as_str().to_string())
247                .unwrap_or_default(),
248            unique_id: params
249                .get("UNIQUEID")
250                .map(|v| v.as_str().to_string())
251                .unwrap_or_default(),
252            vertices: Vec::new(),
253            params: params.clone(),
254        };
255
256        // Parse vertices (KIND0, VX0, VY0, CX0, CY0, SA0, EA0, R0, ...)
257        let mut idx = 0;
258        loop {
259            let kind_key = format!("KIND{}", idx);
260            let vx_key = format!("VX{}", idx);
261            let vy_key = format!("VY{}", idx);
262
263            if !params.contains(&vx_key) {
264                break;
265            }
266
267            let vertex = PolygonVertex {
268                kind: params
269                    .get(&kind_key)
270                    .map(|v| PolygonVertexKind::from_int(v.as_int_or(0)))
271                    .unwrap_or_default(),
272                x: params
273                    .get(&vx_key)
274                    .and_then(|v| v.as_coord().ok())
275                    .unwrap_or_default(),
276                y: params
277                    .get(&vy_key)
278                    .and_then(|v| v.as_coord().ok())
279                    .unwrap_or_default(),
280                center_x: params
281                    .get(&format!("CX{}", idx))
282                    .and_then(|v| v.as_coord().ok())
283                    .unwrap_or_default(),
284                center_y: params
285                    .get(&format!("CY{}", idx))
286                    .and_then(|v| v.as_coord().ok())
287                    .unwrap_or_default(),
288                start_angle: params
289                    .get(&format!("SA{}", idx))
290                    .map(|v| v.as_double_or(0.0))
291                    .unwrap_or(0.0),
292                end_angle: params
293                    .get(&format!("EA{}", idx))
294                    .map(|v| v.as_double_or(0.0))
295                    .unwrap_or(0.0),
296                radius: params
297                    .get(&format!("R{}", idx))
298                    .and_then(|v| v.as_coord().ok())
299                    .unwrap_or_default(),
300            };
301
302            polygon.vertices.push(vertex);
303            idx += 1;
304        }
305
306        polygon
307    }
308
309    /// Export to parameters.
310    pub fn to_params(&self) -> ParameterCollection {
311        let mut params = self.params.clone();
312
313        params.add("LAYER", &self.layer.to_string());
314        params.add("LOCKED", if self.locked { "TRUE" } else { "FALSE" });
315        params.add(
316            "POLYGONOUTLINE",
317            if self.polygon_outline {
318                "TRUE"
319            } else {
320                "FALSE"
321            },
322        );
323        params.add(
324            "USERROUTED",
325            if self.user_routed { "TRUE" } else { "FALSE" },
326        );
327        params.add("KEEPOUT", if self.keepout { "TRUE" } else { "FALSE" });
328        params.add_int("UNIONINDEX", self.union_index);
329        params.add(
330            "PRIMITIVELOCK",
331            if self.primitive_lock { "TRUE" } else { "FALSE" },
332        );
333        params.add("POLYGONTYPE", self.polygon_type.as_str());
334        params.add("POUROVER", if self.pour_over { "TRUE" } else { "FALSE" });
335        params.add(
336            "REMOVEDEAD",
337            if self.remove_dead { "TRUE" } else { "FALSE" },
338        );
339        params.add_coord("GRIDSIZE", self.grid_size);
340        params.add_coord("TRACKWIDTH", self.track_width);
341        params.add("HATCHSTYLE", self.hatch_style.as_str());
342        params.add(
343            "USEOCTAGONS",
344            if self.use_octagons { "TRUE" } else { "FALSE" },
345        );
346        params.add_coord("MINPRIMLENGTH", self.min_prim_length);
347
348        if !self.net_name.is_empty() {
349            params.add("NET", &self.net_name);
350        }
351        if !self.unique_id.is_empty() {
352            params.add("UNIQUEID", &self.unique_id);
353        }
354
355        // Write vertices
356        for (idx, vertex) in self.vertices.iter().enumerate() {
357            params.add_int(&format!("KIND{}", idx), vertex.kind.to_int());
358            params.add_coord(&format!("VX{}", idx), vertex.x);
359            params.add_coord(&format!("VY{}", idx), vertex.y);
360            params.add_coord(&format!("CX{}", idx), vertex.center_x);
361            params.add_coord(&format!("CY{}", idx), vertex.center_y);
362            params.add_double(&format!("SA{}", idx), vertex.start_angle, 14);
363            params.add_double(&format!("EA{}", idx), vertex.end_angle, 14);
364            params.add_coord(&format!("R{}", idx), vertex.radius);
365        }
366
367        params
368    }
369
370    /// Get the number of vertices.
371    pub fn vertex_count(&self) -> usize {
372        self.vertices.len()
373    }
374}