Skip to main content

altium_format/edit/
pcb_session.rs

1//! PCB edit session for programmatic modification of Altium PCB documents.
2//!
3//! This module provides a comprehensive editing system for PCB documents including:
4//! - Adding/modifying/deleting tracks, vias, pads, arcs, fills, text, and regions
5//! - Relative positioning with support for multiple units (mm, mil, inch)
6//! - Net assignment and management
7//! - Grid snapping and board bounds awareness
8//! - Undo/redo support
9
10use std::path::Path;
11
12use crate::error::{AltiumError, Result};
13use crate::io::PcbDoc;
14use crate::records::pcb::{
15    HatchStyle, PcbArc, PcbFill, PcbFlags, PcbPad, PcbPadShape, PcbPolygon, PcbPrimitiveCommon,
16    PcbRecord, PcbRectangularBase, PcbRegion, PcbText, PcbTextJustification, PcbTrack, PcbVia,
17    PolygonVertex, PolygonVertexKind,
18};
19use crate::types::{Coord, CoordPoint, CoordRect, Layer, ParameterCollection};
20
21use super::pcb_placement::{BoardEdge, PcbPlacementEngine, PlacementAnchor};
22use super::types::Grid;
23
24/// Position specification for PCB primitives.
25/// Supports absolute coordinates and relative positioning.
26#[derive(Debug, Clone)]
27pub enum Position {
28    /// Absolute position with X,Y coordinates.
29    Absolute(CoordPoint),
30    /// Relative to another primitive by index.
31    RelativeTo {
32        reference_index: usize,
33        offset: CoordPoint,
34    },
35    /// Relative to a component by designator.
36    RelativeToComponent {
37        designator: String,
38        offset: CoordPoint,
39    },
40    /// Relative to a pad on a component.
41    RelativeToPad {
42        component: String,
43        pad: String,
44        offset: CoordPoint,
45    },
46    /// Relative to board edge.
47    RelativeToEdge { edge: BoardEdge, offset: CoordPoint },
48    /// Center of board.
49    BoardCenter { offset: CoordPoint },
50}
51
52impl Position {
53    /// Create an absolute position from coordinates.
54    pub fn absolute(x: Coord, y: Coord) -> Self {
55        Position::Absolute(CoordPoint::new(x, y))
56    }
57
58    /// Create an absolute position from a point.
59    pub fn at(point: CoordPoint) -> Self {
60        Position::Absolute(point)
61    }
62
63    /// Create a position relative to a component.
64    pub fn relative_to_component(designator: &str, dx: Coord, dy: Coord) -> Self {
65        Position::RelativeToComponent {
66            designator: designator.to_string(),
67            offset: CoordPoint::new(dx, dy),
68        }
69    }
70
71    /// Create a position relative to a component pad.
72    pub fn relative_to_pad(component: &str, pad: &str, dx: Coord, dy: Coord) -> Self {
73        Position::RelativeToPad {
74            component: component.to_string(),
75            pad: pad.to_string(),
76            offset: CoordPoint::new(dx, dy),
77        }
78    }
79
80    /// Create a position relative to a board edge.
81    pub fn from_edge(edge: BoardEdge, dx: Coord, dy: Coord) -> Self {
82        Position::RelativeToEdge {
83            edge,
84            offset: CoordPoint::new(dx, dy),
85        }
86    }
87}
88
89/// Track path specification with support for various routing styles.
90#[derive(Debug, Clone)]
91pub enum TrackPath {
92    /// Direct line from start to end.
93    Direct { start: Position, end: Position },
94    /// Manhattan routing (90-degree angles only).
95    Manhattan {
96        start: Position,
97        end: Position,
98        prefer_horizontal_first: bool,
99    },
100    /// Diagonal routing (45-degree angles).
101    Diagonal45 { start: Position, end: Position },
102    /// Explicit list of vertices.
103    Vertices(Vec<Position>),
104}
105
106/// Edit operation for undo/redo support.
107///
108/// Large variants (DeletePrimitive, ModifyPrimitive) box their PcbRecord fields
109/// to reduce enum size on the stack.
110#[derive(Debug, Clone)]
111pub enum PcbEditOperation {
112    AddTrack {
113        index: usize,
114    },
115    AddVia {
116        index: usize,
117    },
118    AddPad {
119        index: usize,
120    },
121    AddArc {
122        index: usize,
123    },
124    AddFill {
125        index: usize,
126    },
127    AddText {
128        index: usize,
129    },
130    AddRegion {
131        index: usize,
132    },
133    AddPolygon {
134        index: usize,
135    },
136    DeletePrimitive {
137        index: usize,
138        record: Box<PcbRecord>,
139    },
140    ModifyPrimitive {
141        index: usize,
142        old: Box<PcbRecord>,
143        new: Box<PcbRecord>,
144    },
145    MoveComponent {
146        designator: String,
147        old_pos: CoordPoint,
148        new_pos: CoordPoint,
149    },
150}
151
152/// PCB editing session for comprehensive document modification.
153pub struct PcbEditSession {
154    /// The PCB document being edited.
155    pub doc: PcbDoc,
156    /// Placement engine for positioning.
157    placement: PcbPlacementEngine,
158    /// Operation history for undo.
159    history: Vec<PcbEditOperation>,
160    /// Redo stack.
161    redo_stack: Vec<PcbEditOperation>,
162    /// Path to the source file.
163    source_path: Option<String>,
164    /// Whether the document has been modified.
165    modified: bool,
166    /// Default track width.
167    default_track_width: Coord,
168    /// Default via diameter.
169    default_via_diameter: Coord,
170    /// Default via hole size.
171    default_via_hole: Coord,
172    /// Default layer for new primitives.
173    default_layer: Layer,
174}
175
176impl Default for PcbEditSession {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182impl PcbEditSession {
183    /// Create a new empty editing session.
184    pub fn new() -> Self {
185        Self {
186            doc: PcbDoc::default(),
187            placement: PcbPlacementEngine::new(),
188            history: Vec::new(),
189            redo_stack: Vec::new(),
190            source_path: None,
191            modified: false,
192            default_track_width: Coord::from_mils(10.0),
193            default_via_diameter: Coord::from_mils(50.0),
194            default_via_hole: Coord::from_mils(28.0),
195            default_layer: Layer::TOP_LAYER,
196        }
197    }
198
199    /// Open an existing PCB file for editing.
200    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
201        let path_str = path.as_ref().to_string_lossy().to_string();
202        let doc = PcbDoc::open_file(&path)?;
203
204        let mut session = Self::new();
205        session.doc = doc;
206        session.source_path = Some(path_str);
207        session.placement.calculate_board_bounds(&session.doc);
208
209        Ok(session)
210    }
211
212    /// Save the PCB document to a file.
213    pub fn save<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
214        self.doc.save_all_to_file(&path)?;
215        self.source_path = Some(path.as_ref().to_string_lossy().to_string());
216        self.modified = false;
217        Ok(())
218    }
219
220    /// Save to the original file (if opened from file).
221    pub fn save_to_original(&mut self) -> Result<()> {
222        match &self.source_path {
223            Some(path) => {
224                self.doc.save_all_to_file(path)?;
225                self.modified = false;
226                Ok(())
227            }
228            None => Err(AltiumError::Parse("No source file path".to_string())),
229        }
230    }
231
232    /// Check if the document has been modified.
233    pub fn is_modified(&self) -> bool {
234        self.modified
235    }
236
237    /// Get the source file path.
238    pub fn source_path(&self) -> Option<&str> {
239        self.source_path.as_deref()
240    }
241
242    /// Set the grid configuration.
243    pub fn set_grid(&mut self, grid: Grid) {
244        self.placement.set_grid(grid);
245    }
246
247    /// Set grid from spacing in mm.
248    pub fn set_grid_mm(&mut self, spacing_mm: f64) {
249        self.placement.set_grid_mm(spacing_mm);
250    }
251
252    /// Set the default track width.
253    pub fn set_default_track_width(&mut self, width: Coord) {
254        self.default_track_width = width;
255    }
256
257    /// Set the default via diameter.
258    pub fn set_default_via_diameter(&mut self, diameter: Coord) {
259        self.default_via_diameter = diameter;
260    }
261
262    /// Set the default via hole size.
263    pub fn set_default_via_hole(&mut self, hole: Coord) {
264        self.default_via_hole = hole;
265    }
266
267    /// Set the default layer for new primitives.
268    pub fn set_default_layer(&mut self, layer: Layer) {
269        self.default_layer = layer;
270    }
271
272    /// Get the default layer.
273    pub fn default_layer(&self) -> Layer {
274        self.default_layer
275    }
276
277    // ═══════════════════════════════════════════════════════════════════════
278    // POSITION RESOLUTION
279    // ═══════════════════════════════════════════════════════════════════════
280
281    /// Resolve a Position to an absolute CoordPoint.
282    pub fn resolve_position(&self, pos: &Position) -> Result<CoordPoint> {
283        match pos {
284            Position::Absolute(point) => Ok(self.placement.snap_to_grid(*point)),
285
286            Position::RelativeTo {
287                reference_index,
288                offset,
289            } => {
290                if *reference_index >= self.doc.primitives.len() {
291                    return Err(AltiumError::Parse(format!(
292                        "Invalid primitive index: {}",
293                        reference_index
294                    )));
295                }
296                let ref_pos = self.get_primitive_center(*reference_index)?;
297                Ok(self
298                    .placement
299                    .snap_to_grid(CoordPoint::new(ref_pos.x + offset.x, ref_pos.y + offset.y)))
300            }
301
302            Position::RelativeToComponent { designator, offset } => {
303                let comp_pos = self
304                    .placement
305                    .get_component_position(&self.doc, designator)
306                    .ok_or_else(|| {
307                        AltiumError::Parse(format!("Component '{}' not found", designator))
308                    })?;
309                Ok(self.placement.snap_to_grid(CoordPoint::new(
310                    comp_pos.x + offset.x,
311                    comp_pos.y + offset.y,
312                )))
313            }
314
315            Position::RelativeToPad {
316                component,
317                pad,
318                offset,
319            } => {
320                let pad_pos = self.get_pad_position(component, pad)?;
321                Ok(self
322                    .placement
323                    .snap_to_grid(CoordPoint::new(pad_pos.x + offset.x, pad_pos.y + offset.y)))
324            }
325
326            Position::RelativeToEdge { edge, offset } => {
327                let anchor = PlacementAnchor::BoardEdge {
328                    edge: *edge,
329                    offset: Coord::ZERO,
330                };
331                let base = self
332                    .placement
333                    .resolve_anchor(&self.doc, &anchor, None)
334                    .map_err(AltiumError::Parse)?;
335                Ok(self
336                    .placement
337                    .snap_to_grid(CoordPoint::new(base.x + offset.x, base.y + offset.y)))
338            }
339
340            Position::BoardCenter { offset } => {
341                let bounds = self.get_board_bounds();
342                let center = bounds.center();
343                Ok(self
344                    .placement
345                    .snap_to_grid(CoordPoint::new(center.x + offset.x, center.y + offset.y)))
346            }
347        }
348    }
349
350    /// Get the center point of a primitive.
351    fn get_primitive_center(&self, index: usize) -> Result<CoordPoint> {
352        let prim = &self.doc.primitives[index];
353        let center = match prim {
354            PcbRecord::Track(t) => CoordPoint::new(
355                Coord::from_raw((t.start.x.to_raw() + t.end.x.to_raw()) / 2),
356                Coord::from_raw((t.start.y.to_raw() + t.end.y.to_raw()) / 2),
357            ),
358            PcbRecord::Via(v) => v.location,
359            PcbRecord::Pad(p) => p.location,
360            PcbRecord::Arc(a) => a.location,
361            PcbRecord::Fill(f) => f.base.calculate_bounds().center(),
362            PcbRecord::Text(t) => t.base.calculate_bounds().center(),
363            PcbRecord::Region(r) => {
364                if r.outline.is_empty() {
365                    CoordPoint::default()
366                } else {
367                    let sum_x: i64 = r.outline.iter().map(|p| p.x.to_raw() as i64).sum();
368                    let sum_y: i64 = r.outline.iter().map(|p| p.y.to_raw() as i64).sum();
369                    let n = r.outline.len() as i64;
370                    CoordPoint::new(
371                        Coord::from_raw((sum_x / n) as i32),
372                        Coord::from_raw((sum_y / n) as i32),
373                    )
374                }
375            }
376            PcbRecord::Polygon(p) => {
377                if p.vertices.is_empty() {
378                    CoordPoint::default()
379                } else {
380                    let sum_x: i64 = p.vertices.iter().map(|v| v.x.to_raw() as i64).sum();
381                    let sum_y: i64 = p.vertices.iter().map(|v| v.y.to_raw() as i64).sum();
382                    let n = p.vertices.len() as i64;
383                    CoordPoint::new(
384                        Coord::from_raw((sum_x / n) as i32),
385                        Coord::from_raw((sum_y / n) as i32),
386                    )
387                }
388            }
389            _ => CoordPoint::default(),
390        };
391        Ok(center)
392    }
393
394    /// Get the position of a pad on a component.
395    fn get_pad_position(&self, component: &str, pad_designator: &str) -> Result<CoordPoint> {
396        // Find the component
397        let comp = self
398            .doc
399            .find_component(component)
400            .ok_or_else(|| AltiumError::Parse(format!("Component '{}' not found", component)))?;
401
402        // Get component position
403        let comp_x = comp.x().unwrap_or(Coord::ZERO);
404        let comp_y = comp.y().unwrap_or(Coord::ZERO);
405        let _rotation = comp.rotation();
406
407        // Find the pad in the component's primitives
408        for prim in &comp.primitives {
409            if let PcbRecord::Pad(p) = prim {
410                if p.designator.eq_ignore_ascii_case(pad_designator) {
411                    // Pad location is relative to component, need to transform
412                    return Ok(CoordPoint::new(
413                        comp_x + p.location.x,
414                        comp_y + p.location.y,
415                    ));
416                }
417            }
418        }
419
420        Err(AltiumError::Parse(format!(
421            "Pad '{}' not found on component '{}'",
422            pad_designator, component
423        )))
424    }
425
426    /// Get the board bounds.
427    pub fn get_board_bounds(&self) -> CoordRect {
428        // Try to calculate from board outline or use default
429        let mut bounds = CoordRect::EMPTY;
430
431        for prim in &self.doc.primitives {
432            match prim {
433                PcbRecord::Track(t) => {
434                    bounds = bounds.union(t.calculate_bounds());
435                }
436                PcbRecord::Via(v) => {
437                    bounds = bounds.union(v.calculate_bounds());
438                }
439                PcbRecord::Region(r) if !r.outline.is_empty() => {
440                    for point in &r.outline {
441                        if bounds.is_empty() {
442                            bounds =
443                                CoordRect::from_xywh(point.x, point.y, Coord::ZERO, Coord::ZERO);
444                        } else {
445                            bounds = bounds.union(CoordRect::from_xywh(
446                                point.x,
447                                point.y,
448                                Coord::ZERO,
449                                Coord::ZERO,
450                            ));
451                        }
452                    }
453                }
454                _ => {}
455            }
456        }
457
458        if bounds.is_empty() {
459            // Default board size
460            CoordRect::from_xywh(
461                Coord::ZERO,
462                Coord::ZERO,
463                Coord::from_mils(4000.0),
464                Coord::from_mils(3000.0),
465            )
466        } else {
467            bounds
468        }
469    }
470
471    // ═══════════════════════════════════════════════════════════════════════
472    // TRACK OPERATIONS
473    // ═══════════════════════════════════════════════════════════════════════
474
475    /// Add a single track segment.
476    pub fn add_track(
477        &mut self,
478        start: CoordPoint,
479        end: CoordPoint,
480        width: Option<Coord>,
481        layer: Option<Layer>,
482        net: Option<&str>,
483    ) -> Result<usize> {
484        let track = PcbTrack {
485            common: PcbPrimitiveCommon {
486                layer: layer.unwrap_or(self.default_layer),
487                flags: PcbFlags::default(),
488                unique_id: None,
489            },
490            start: self.placement.snap_to_grid(start),
491            end: self.placement.snap_to_grid(end),
492            width: width.unwrap_or(self.default_track_width),
493            unknown: vec![0u8; 16],
494        };
495
496        let index = self.doc.primitives.len();
497        self.doc.primitives.push(PcbRecord::Track(track));
498
499        // Add to net if specified
500        if let Some(net_name) = net {
501            self.ensure_net_exists(net_name);
502        }
503
504        self.history.push(PcbEditOperation::AddTrack { index });
505        self.redo_stack.clear();
506        self.modified = true;
507
508        Ok(index)
509    }
510
511    /// Add a track using Position specifications.
512    pub fn add_track_positioned(
513        &mut self,
514        start: &Position,
515        end: &Position,
516        width: Option<Coord>,
517        layer: Option<Layer>,
518        net: Option<&str>,
519    ) -> Result<usize> {
520        let start_point = self.resolve_position(start)?;
521        let end_point = self.resolve_position(end)?;
522        self.add_track(start_point, end_point, width, layer, net)
523    }
524
525    /// Add multiple connected track segments forming a path.
526    pub fn add_track_path(
527        &mut self,
528        vertices: &[CoordPoint],
529        width: Option<Coord>,
530        layer: Option<Layer>,
531        net: Option<&str>,
532    ) -> Result<Vec<usize>> {
533        if vertices.len() < 2 {
534            return Err(AltiumError::Parse(
535                "Track path requires at least 2 vertices".to_string(),
536            ));
537        }
538
539        let mut indices = Vec::new();
540        for i in 0..vertices.len() - 1 {
541            let idx = self.add_track(vertices[i], vertices[i + 1], width, layer, net)?;
542            indices.push(idx);
543        }
544        Ok(indices)
545    }
546
547    /// Add a track path using the TrackPath specification.
548    pub fn add_track_routed(
549        &mut self,
550        path: &TrackPath,
551        width: Option<Coord>,
552        layer: Option<Layer>,
553        net: Option<&str>,
554    ) -> Result<Vec<usize>> {
555        match path {
556            TrackPath::Direct { start, end } => {
557                let idx = self.add_track_positioned(start, end, width, layer, net)?;
558                Ok(vec![idx])
559            }
560
561            TrackPath::Manhattan {
562                start,
563                end,
564                prefer_horizontal_first,
565            } => {
566                let start_pt = self.resolve_position(start)?;
567                let end_pt = self.resolve_position(end)?;
568
569                let mid = if *prefer_horizontal_first {
570                    CoordPoint::new(end_pt.x, start_pt.y)
571                } else {
572                    CoordPoint::new(start_pt.x, end_pt.y)
573                };
574
575                self.add_track_path(&[start_pt, mid, end_pt], width, layer, net)
576            }
577
578            TrackPath::Diagonal45 { start, end } => {
579                let start_pt = self.resolve_position(start)?;
580                let end_pt = self.resolve_position(end)?;
581
582                let dx = (end_pt.x.to_raw() - start_pt.x.to_raw()).abs();
583                let dy = (end_pt.y.to_raw() - start_pt.y.to_raw()).abs();
584                let diag = dx.min(dy);
585
586                let mid = if dx > dy {
587                    // More horizontal, go diagonal first then horizontal
588                    let sign_x = if end_pt.x > start_pt.x { 1 } else { -1 };
589                    let sign_y = if end_pt.y > start_pt.y { 1 } else { -1 };
590                    CoordPoint::new(
591                        Coord::from_raw(start_pt.x.to_raw() + sign_x * diag),
592                        Coord::from_raw(start_pt.y.to_raw() + sign_y * diag),
593                    )
594                } else {
595                    // More vertical, go diagonal first then vertical
596                    let sign_x = if end_pt.x > start_pt.x { 1 } else { -1 };
597                    let sign_y = if end_pt.y > start_pt.y { 1 } else { -1 };
598                    CoordPoint::new(
599                        Coord::from_raw(start_pt.x.to_raw() + sign_x * diag),
600                        Coord::from_raw(start_pt.y.to_raw() + sign_y * diag),
601                    )
602                };
603
604                self.add_track_path(&[start_pt, mid, end_pt], width, layer, net)
605            }
606
607            TrackPath::Vertices(positions) => {
608                let mut points = Vec::new();
609                for pos in positions {
610                    points.push(self.resolve_position(pos)?);
611                }
612                self.add_track_path(&points, width, layer, net)
613            }
614        }
615    }
616
617    // ═══════════════════════════════════════════════════════════════════════
618    // VIA OPERATIONS
619    // ═══════════════════════════════════════════════════════════════════════
620
621    /// Add a via at a specific location.
622    pub fn add_via(
623        &mut self,
624        location: CoordPoint,
625        diameter: Option<Coord>,
626        hole_size: Option<Coord>,
627        from_layer: Option<Layer>,
628        to_layer: Option<Layer>,
629        net: Option<&str>,
630    ) -> Result<usize> {
631        let dia = diameter.unwrap_or(self.default_via_diameter);
632        let mut via = PcbVia::default();
633        via.common.layer = Layer::MULTI_LAYER;
634        via.location = self.placement.snap_to_grid(location);
635        via.hole_size = hole_size.unwrap_or(self.default_via_hole);
636        via.from_layer = from_layer.unwrap_or(Layer::TOP_LAYER);
637        via.to_layer = to_layer.unwrap_or(Layer::BOTTOM_LAYER);
638
639        // Set all diameters to the same value for simple stack mode
640        for d in via.diameters.iter_mut() {
641            *d = dia;
642        }
643
644        let index = self.doc.primitives.len();
645        self.doc.primitives.push(PcbRecord::Via(via));
646
647        if let Some(net_name) = net {
648            self.ensure_net_exists(net_name);
649        }
650
651        self.history.push(PcbEditOperation::AddVia { index });
652        self.redo_stack.clear();
653        self.modified = true;
654
655        Ok(index)
656    }
657
658    /// Add a via using Position specification.
659    pub fn add_via_positioned(
660        &mut self,
661        position: &Position,
662        diameter: Option<Coord>,
663        hole_size: Option<Coord>,
664        from_layer: Option<Layer>,
665        to_layer: Option<Layer>,
666        net: Option<&str>,
667    ) -> Result<usize> {
668        let location = self.resolve_position(position)?;
669        self.add_via(location, diameter, hole_size, from_layer, to_layer, net)
670    }
671
672    /// Add a blind via (connects top/bottom to inner layer).
673    pub fn add_blind_via(
674        &mut self,
675        location: CoordPoint,
676        from_layer: Layer,
677        to_layer: Layer,
678        diameter: Option<Coord>,
679        hole_size: Option<Coord>,
680        net: Option<&str>,
681    ) -> Result<usize> {
682        self.add_via(
683            location,
684            diameter,
685            hole_size,
686            Some(from_layer),
687            Some(to_layer),
688            net,
689        )
690    }
691
692    // ═══════════════════════════════════════════════════════════════════════
693    // PAD OPERATIONS
694    // ═══════════════════════════════════════════════════════════════════════
695
696    /// Add a through-hole pad.
697    pub fn add_through_hole_pad(
698        &mut self,
699        location: CoordPoint,
700        designator: &str,
701        hole_size: Coord,
702        pad_size: CoordPoint,
703        shape: PcbPadShape,
704        net: Option<&str>,
705    ) -> Result<usize> {
706        let mut pad = PcbPad::default();
707        pad.common.layer = Layer::MULTI_LAYER;
708        pad.location = self.placement.snap_to_grid(location);
709        pad.designator = designator.to_string();
710        pad.hole_size = hole_size;
711        pad.is_plated = true;
712
713        // Set size and shape for all layers
714        for size in pad.size_layers.iter_mut() {
715            *size = pad_size;
716        }
717        for s in pad.shape_layers.iter_mut() {
718            *s = shape;
719        }
720
721        let index = self.doc.primitives.len();
722        self.doc.primitives.push(PcbRecord::Pad(Box::new(pad)));
723
724        if let Some(net_name) = net {
725            self.ensure_net_exists(net_name);
726        }
727
728        self.history.push(PcbEditOperation::AddPad { index });
729        self.redo_stack.clear();
730        self.modified = true;
731
732        Ok(index)
733    }
734
735    /// Add an SMD pad (surface mount).
736    pub fn add_smd_pad(
737        &mut self,
738        location: CoordPoint,
739        designator: &str,
740        size: CoordPoint,
741        shape: PcbPadShape,
742        layer: Layer,
743        net: Option<&str>,
744    ) -> Result<usize> {
745        let mut pad = PcbPad::default();
746        pad.common.layer = layer;
747        pad.location = self.placement.snap_to_grid(location);
748        pad.designator = designator.to_string();
749        pad.hole_size = Coord::ZERO; // No hole for SMD
750        pad.is_plated = false;
751
752        // Set size and shape for the specified layer
753        let layer_idx = if layer == Layer::TOP_LAYER { 0 } else { 31 };
754        pad.size_layers[layer_idx] = size;
755        pad.shape_layers[layer_idx] = shape;
756
757        let index = self.doc.primitives.len();
758        self.doc.primitives.push(PcbRecord::Pad(Box::new(pad)));
759
760        if let Some(net_name) = net {
761            self.ensure_net_exists(net_name);
762        }
763
764        self.history.push(PcbEditOperation::AddPad { index });
765        self.redo_stack.clear();
766        self.modified = true;
767
768        Ok(index)
769    }
770
771    // ═══════════════════════════════════════════════════════════════════════
772    // ARC OPERATIONS
773    // ═══════════════════════════════════════════════════════════════════════
774
775    /// Add an arc.
776    #[allow(clippy::too_many_arguments)]
777    pub fn add_arc(
778        &mut self,
779        center: CoordPoint,
780        radius: Coord,
781        start_angle: f64,
782        end_angle: f64,
783        width: Option<Coord>,
784        layer: Option<Layer>,
785        net: Option<&str>,
786    ) -> Result<usize> {
787        let arc = PcbArc {
788            common: PcbPrimitiveCommon {
789                layer: layer.unwrap_or(self.default_layer),
790                flags: PcbFlags::default(),
791                unique_id: None,
792            },
793            location: self.placement.snap_to_grid(center),
794            radius,
795            start_angle,
796            end_angle,
797            width: width.unwrap_or(self.default_track_width),
798        };
799
800        let index = self.doc.primitives.len();
801        self.doc.primitives.push(PcbRecord::Arc(arc));
802
803        if let Some(net_name) = net {
804            self.ensure_net_exists(net_name);
805        }
806
807        self.history.push(PcbEditOperation::AddArc { index });
808        self.redo_stack.clear();
809        self.modified = true;
810
811        Ok(index)
812    }
813
814    /// Add a full circle arc.
815    pub fn add_circle(
816        &mut self,
817        center: CoordPoint,
818        radius: Coord,
819        width: Option<Coord>,
820        layer: Option<Layer>,
821    ) -> Result<usize> {
822        self.add_arc(center, radius, 0.0, 360.0, width, layer, None)
823    }
824
825    /// Add an arc using Position specification.
826    #[allow(clippy::too_many_arguments)]
827    pub fn add_arc_positioned(
828        &mut self,
829        center: &Position,
830        radius: Coord,
831        start_angle: f64,
832        end_angle: f64,
833        width: Option<Coord>,
834        layer: Option<Layer>,
835        net: Option<&str>,
836    ) -> Result<usize> {
837        let center_pt = self.resolve_position(center)?;
838        self.add_arc(center_pt, radius, start_angle, end_angle, width, layer, net)
839    }
840
841    // ═══════════════════════════════════════════════════════════════════════
842    // FILL OPERATIONS
843    // ═══════════════════════════════════════════════════════════════════════
844
845    /// Add a solid fill (copper rectangle).
846    pub fn add_fill(
847        &mut self,
848        corner1: CoordPoint,
849        corner2: CoordPoint,
850        layer: Option<Layer>,
851        rotation: Option<f64>,
852        net: Option<&str>,
853    ) -> Result<usize> {
854        let fill = PcbFill {
855            base: PcbRectangularBase {
856                common: PcbPrimitiveCommon {
857                    layer: layer.unwrap_or(self.default_layer),
858                    flags: PcbFlags::default(),
859                    unique_id: None,
860                },
861                corner1: self.placement.snap_to_grid(corner1),
862                corner2: self.placement.snap_to_grid(corner2),
863                rotation: rotation.unwrap_or(0.0),
864            },
865        };
866
867        let index = self.doc.primitives.len();
868        self.doc.primitives.push(PcbRecord::Fill(fill));
869
870        if let Some(net_name) = net {
871            self.ensure_net_exists(net_name);
872        }
873
874        self.history.push(PcbEditOperation::AddFill { index });
875        self.redo_stack.clear();
876        self.modified = true;
877
878        Ok(index)
879    }
880
881    /// Add a fill with Position specifications.
882    pub fn add_fill_positioned(
883        &mut self,
884        corner1: &Position,
885        corner2: &Position,
886        layer: Option<Layer>,
887        rotation: Option<f64>,
888        net: Option<&str>,
889    ) -> Result<usize> {
890        let c1 = self.resolve_position(corner1)?;
891        let c2 = self.resolve_position(corner2)?;
892        self.add_fill(c1, c2, layer, rotation, net)
893    }
894
895    // ═══════════════════════════════════════════════════════════════════════
896    // TEXT OPERATIONS
897    // ═══════════════════════════════════════════════════════════════════════
898
899    /// Add text annotation.
900    pub fn add_text(
901        &mut self,
902        text: &str,
903        location: CoordPoint,
904        height: Coord,
905        layer: Option<Layer>,
906        rotation: Option<f64>,
907        _justification: Option<PcbTextJustification>,
908    ) -> Result<usize> {
909        // Use the PcbText::new constructor for proper initialization
910        let text_record = PcbText::new(
911            location.x.to_mms(),
912            location.y.to_mms(),
913            text,
914            height.to_mms(),
915            0.15, // Default stroke width in mm
916            rotation.unwrap_or(0.0),
917            false, // not mirrored
918            layer.unwrap_or(Layer::TOP_OVERLAY),
919        );
920
921        let index = self.doc.primitives.len();
922        self.doc.primitives.push(PcbRecord::Text(text_record));
923
924        self.history.push(PcbEditOperation::AddText { index });
925        self.redo_stack.clear();
926        self.modified = true;
927
928        Ok(index)
929    }
930
931    /// Add text using Position specification.
932    pub fn add_text_positioned(
933        &mut self,
934        text: &str,
935        position: &Position,
936        height: Coord,
937        layer: Option<Layer>,
938        rotation: Option<f64>,
939        justification: Option<PcbTextJustification>,
940    ) -> Result<usize> {
941        let location = self.resolve_position(position)?;
942        self.add_text(text, location, height, layer, rotation, justification)
943    }
944
945    // ═══════════════════════════════════════════════════════════════════════
946    // REGION OPERATIONS
947    // ═══════════════════════════════════════════════════════════════════════
948
949    /// Add a region (copper area or keepout).
950    pub fn add_region(
951        &mut self,
952        vertices: &[CoordPoint],
953        layer: Layer,
954        is_keepout: bool,
955        net: Option<&str>,
956    ) -> Result<usize> {
957        if vertices.len() < 3 {
958            return Err(AltiumError::Parse(
959                "Region requires at least 3 vertices".to_string(),
960            ));
961        }
962
963        let mut flags = PcbFlags::default();
964        if is_keepout {
965            flags |= PcbFlags::KEEPOUT;
966        }
967
968        let region = PcbRegion {
969            common: PcbPrimitiveCommon {
970                layer,
971                flags,
972                unique_id: None,
973            },
974            parameters: ParameterCollection::new(),
975            outline: vertices
976                .iter()
977                .map(|v| self.placement.snap_to_grid(*v))
978                .collect(),
979        };
980
981        let index = self.doc.primitives.len();
982        self.doc.primitives.push(PcbRecord::Region(region));
983
984        if let Some(net_name) = net {
985            self.ensure_net_exists(net_name);
986        }
987
988        self.history.push(PcbEditOperation::AddRegion { index });
989        self.redo_stack.clear();
990        self.modified = true;
991
992        Ok(index)
993    }
994
995    /// Add a rectangular region.
996    pub fn add_rectangular_region(
997        &mut self,
998        corner1: CoordPoint,
999        corner2: CoordPoint,
1000        layer: Layer,
1001        is_keepout: bool,
1002        net: Option<&str>,
1003    ) -> Result<usize> {
1004        let vertices = vec![
1005            corner1,
1006            CoordPoint::new(corner2.x, corner1.y),
1007            corner2,
1008            CoordPoint::new(corner1.x, corner2.y),
1009        ];
1010        self.add_region(&vertices, layer, is_keepout, net)
1011    }
1012
1013    // ═══════════════════════════════════════════════════════════════════════
1014    // POLYGON (COPPER POUR) OPERATIONS
1015    // ═══════════════════════════════════════════════════════════════════════
1016
1017    /// Add a polygon (copper pour).
1018    pub fn add_polygon(
1019        &mut self,
1020        vertices: &[CoordPoint],
1021        layer: Layer,
1022        net_name: &str,
1023        hatch_style: HatchStyle,
1024        pour_over_same_net: bool,
1025        remove_dead_copper: bool,
1026    ) -> Result<usize> {
1027        if vertices.len() < 3 {
1028            return Err(AltiumError::Parse(
1029                "Polygon requires at least 3 vertices".to_string(),
1030            ));
1031        }
1032
1033        let mut polygon = PcbPolygon {
1034            layer,
1035            net_name: net_name.to_string(),
1036            hatch_style,
1037            pour_over: pour_over_same_net,
1038            remove_dead: remove_dead_copper,
1039            ..Default::default()
1040        };
1041
1042        for vertex in vertices.iter() {
1043            let snapped = self.placement.snap_to_grid(*vertex);
1044            polygon.vertices.push(PolygonVertex {
1045                kind: PolygonVertexKind::Line,
1046                x: snapped.x,
1047                y: snapped.y,
1048                center_x: Coord::ZERO,
1049                center_y: Coord::ZERO,
1050                start_angle: 0.0,
1051                end_angle: 0.0,
1052                radius: Coord::ZERO,
1053            });
1054        }
1055
1056        self.ensure_net_exists(net_name);
1057
1058        let index = self.doc.primitives.len();
1059        self.doc.primitives.push(PcbRecord::Polygon(polygon));
1060
1061        self.history.push(PcbEditOperation::AddPolygon { index });
1062        self.redo_stack.clear();
1063        self.modified = true;
1064
1065        Ok(index)
1066    }
1067
1068    /// Add a rectangular copper pour.
1069    pub fn add_rectangular_polygon(
1070        &mut self,
1071        corner1: CoordPoint,
1072        corner2: CoordPoint,
1073        layer: Layer,
1074        net_name: &str,
1075        hatch_style: HatchStyle,
1076    ) -> Result<usize> {
1077        let vertices = vec![
1078            corner1,
1079            CoordPoint::new(corner2.x, corner1.y),
1080            corner2,
1081            CoordPoint::new(corner1.x, corner2.y),
1082        ];
1083        self.add_polygon(&vertices, layer, net_name, hatch_style, true, true)
1084    }
1085
1086    // ═══════════════════════════════════════════════════════════════════════
1087    // DELETE OPERATIONS
1088    // ═══════════════════════════════════════════════════════════════════════
1089
1090    /// Delete a primitive by index.
1091    pub fn delete_primitive(&mut self, index: usize) -> Result<()> {
1092        if index >= self.doc.primitives.len() {
1093            return Err(AltiumError::Parse(format!(
1094                "Invalid primitive index: {}",
1095                index
1096            )));
1097        }
1098
1099        let record = self.doc.primitives.remove(index);
1100        self.history.push(PcbEditOperation::DeletePrimitive {
1101            index,
1102            record: Box::new(record),
1103        });
1104        self.redo_stack.clear();
1105        self.modified = true;
1106
1107        Ok(())
1108    }
1109
1110    /// Delete all primitives matching a filter.
1111    pub fn delete_primitives_where<F>(&mut self, filter: F) -> Result<usize>
1112    where
1113        F: Fn(&PcbRecord) -> bool,
1114    {
1115        let mut indices: Vec<usize> = self
1116            .doc
1117            .primitives
1118            .iter()
1119            .enumerate()
1120            .filter(|(_, p)| filter(p))
1121            .map(|(i, _)| i)
1122            .collect();
1123
1124        // Delete in reverse order to maintain valid indices
1125        indices.sort();
1126        indices.reverse();
1127
1128        let count = indices.len();
1129        for idx in indices {
1130            self.delete_primitive(idx)?;
1131        }
1132
1133        Ok(count)
1134    }
1135
1136    /// Delete all tracks on a layer.
1137    pub fn delete_tracks_on_layer(&mut self, layer: Layer) -> Result<usize> {
1138        self.delete_primitives_where(
1139            |p| matches!(p, PcbRecord::Track(t) if t.common.layer == layer),
1140        )
1141    }
1142
1143    /// Delete all vias.
1144    pub fn delete_all_vias(&mut self) -> Result<usize> {
1145        self.delete_primitives_where(|p| matches!(p, PcbRecord::Via(_)))
1146    }
1147
1148    // ═══════════════════════════════════════════════════════════════════════
1149    // NET OPERATIONS
1150    // ═══════════════════════════════════════════════════════════════════════
1151
1152    /// Ensure a net exists in the document.
1153    pub fn ensure_net_exists(&mut self, net_name: &str) {
1154        if !self
1155            .doc
1156            .nets
1157            .iter()
1158            .any(|n| n.eq_ignore_ascii_case(net_name))
1159        {
1160            self.doc.nets.push(net_name.to_string());
1161        }
1162    }
1163
1164    /// Add a new net.
1165    pub fn add_net(&mut self, net_name: &str) -> Result<()> {
1166        if self
1167            .doc
1168            .nets
1169            .iter()
1170            .any(|n| n.eq_ignore_ascii_case(net_name))
1171        {
1172            return Err(AltiumError::Parse(format!(
1173                "Net '{}' already exists",
1174                net_name
1175            )));
1176        }
1177        self.doc.nets.push(net_name.to_string());
1178        self.modified = true;
1179        Ok(())
1180    }
1181
1182    /// Get all nets in the document.
1183    pub fn nets(&self) -> &[String] {
1184        &self.doc.nets
1185    }
1186
1187    // ═══════════════════════════════════════════════════════════════════════
1188    // QUERY OPERATIONS
1189    // ═══════════════════════════════════════════════════════════════════════
1190
1191    /// Get all tracks.
1192    pub fn tracks(&self) -> impl Iterator<Item = (usize, &PcbTrack)> {
1193        self.doc.primitives.iter().enumerate().filter_map(|(i, p)| {
1194            if let PcbRecord::Track(t) = p {
1195                Some((i, t))
1196            } else {
1197                None
1198            }
1199        })
1200    }
1201
1202    /// Get all vias.
1203    pub fn vias(&self) -> impl Iterator<Item = (usize, &PcbVia)> {
1204        self.doc.primitives.iter().enumerate().filter_map(|(i, p)| {
1205            if let PcbRecord::Via(v) = p {
1206                Some((i, v))
1207            } else {
1208                None
1209            }
1210        })
1211    }
1212
1213    /// Get all pads.
1214    pub fn pads(&self) -> impl Iterator<Item = (usize, &PcbPad)> {
1215        self.doc.primitives.iter().enumerate().filter_map(|(i, p)| {
1216            if let PcbRecord::Pad(p) = p {
1217                Some((i, p.as_ref()))
1218            } else {
1219                None
1220            }
1221        })
1222    }
1223
1224    /// Get all arcs.
1225    pub fn arcs(&self) -> impl Iterator<Item = (usize, &PcbArc)> {
1226        self.doc.primitives.iter().enumerate().filter_map(|(i, p)| {
1227            if let PcbRecord::Arc(a) = p {
1228                Some((i, a))
1229            } else {
1230                None
1231            }
1232        })
1233    }
1234
1235    /// Get all fills.
1236    pub fn fills(&self) -> impl Iterator<Item = (usize, &PcbFill)> {
1237        self.doc.primitives.iter().enumerate().filter_map(|(i, p)| {
1238            if let PcbRecord::Fill(f) = p {
1239                Some((i, f))
1240            } else {
1241                None
1242            }
1243        })
1244    }
1245
1246    /// Get all text annotations.
1247    pub fn texts(&self) -> impl Iterator<Item = (usize, &PcbText)> {
1248        self.doc.primitives.iter().enumerate().filter_map(|(i, p)| {
1249            if let PcbRecord::Text(t) = p {
1250                Some((i, t))
1251            } else {
1252                None
1253            }
1254        })
1255    }
1256
1257    /// Get all regions.
1258    pub fn regions(&self) -> impl Iterator<Item = (usize, &PcbRegion)> {
1259        self.doc.primitives.iter().enumerate().filter_map(|(i, p)| {
1260            if let PcbRecord::Region(r) = p {
1261                Some((i, r))
1262            } else {
1263                None
1264            }
1265        })
1266    }
1267
1268    /// Get all polygons.
1269    pub fn polygons(&self) -> impl Iterator<Item = (usize, &PcbPolygon)> {
1270        self.doc.primitives.iter().enumerate().filter_map(|(i, p)| {
1271            if let PcbRecord::Polygon(p) = p {
1272                Some((i, p))
1273            } else {
1274                None
1275            }
1276        })
1277    }
1278
1279    /// Get tracks on a specific layer.
1280    pub fn tracks_on_layer(&self, layer: Layer) -> impl Iterator<Item = (usize, &PcbTrack)> {
1281        self.tracks().filter(move |(_, t)| t.common.layer == layer)
1282    }
1283
1284    /// Count primitives by type.
1285    pub fn count_primitives(&self) -> PrimitiveCount {
1286        let mut count = PrimitiveCount::default();
1287        for p in &self.doc.primitives {
1288            match p {
1289                PcbRecord::Track(_) => count.tracks += 1,
1290                PcbRecord::Via(_) => count.vias += 1,
1291                PcbRecord::Pad(_) => count.pads += 1,
1292                PcbRecord::Arc(_) => count.arcs += 1,
1293                PcbRecord::Fill(_) => count.fills += 1,
1294                PcbRecord::Text(_) => count.texts += 1,
1295                PcbRecord::Region(_) => count.regions += 1,
1296                PcbRecord::Polygon(_) => count.polygons += 1,
1297                _ => count.other += 1,
1298            }
1299        }
1300        count
1301    }
1302
1303    // ═══════════════════════════════════════════════════════════════════════
1304    // UNDO/REDO
1305    // ═══════════════════════════════════════════════════════════════════════
1306
1307    /// Undo the last operation.
1308    pub fn undo(&mut self) -> Result<()> {
1309        let op = self
1310            .history
1311            .pop()
1312            .ok_or_else(|| AltiumError::Parse("Nothing to undo".to_string()))?;
1313
1314        match &op {
1315            PcbEditOperation::AddTrack { index }
1316            | PcbEditOperation::AddVia { index }
1317            | PcbEditOperation::AddPad { index }
1318            | PcbEditOperation::AddArc { index }
1319            | PcbEditOperation::AddFill { index }
1320            | PcbEditOperation::AddText { index }
1321            | PcbEditOperation::AddRegion { index }
1322            | PcbEditOperation::AddPolygon { index } => {
1323                if *index < self.doc.primitives.len() {
1324                    self.doc.primitives.remove(*index);
1325                }
1326            }
1327            PcbEditOperation::DeletePrimitive { index, record } => {
1328                self.doc.primitives.insert(*index, (**record).clone());
1329            }
1330            PcbEditOperation::ModifyPrimitive { index, old, .. } => {
1331                if *index < self.doc.primitives.len() {
1332                    self.doc.primitives[*index] = (**old).clone();
1333                }
1334            }
1335            PcbEditOperation::MoveComponent {
1336                designator,
1337                old_pos,
1338                ..
1339            } => {
1340                if let Some(comp) = self.doc.find_component_mut(designator) {
1341                    comp.set_position(old_pos.x, old_pos.y);
1342                }
1343            }
1344        }
1345
1346        self.redo_stack.push(op);
1347        self.modified = true;
1348        Ok(())
1349    }
1350
1351    /// Redo the last undone operation.
1352    pub fn redo(&mut self) -> Result<()> {
1353        let op = self
1354            .redo_stack
1355            .pop()
1356            .ok_or_else(|| AltiumError::Parse("Nothing to redo".to_string()))?;
1357
1358        match &op {
1359            PcbEditOperation::AddTrack { .. }
1360            | PcbEditOperation::AddVia { .. }
1361            | PcbEditOperation::AddPad { .. }
1362            | PcbEditOperation::AddArc { .. }
1363            | PcbEditOperation::AddFill { .. }
1364            | PcbEditOperation::AddText { .. }
1365            | PcbEditOperation::AddRegion { .. }
1366            | PcbEditOperation::AddPolygon { .. } => {
1367                // Can't easily redo adds without storing the record
1368                return Err(AltiumError::Parse("Cannot redo add operation".to_string()));
1369            }
1370            PcbEditOperation::DeletePrimitive { index, .. } => {
1371                if *index < self.doc.primitives.len() {
1372                    self.doc.primitives.remove(*index);
1373                }
1374            }
1375            PcbEditOperation::ModifyPrimitive { index, new, .. } => {
1376                if *index < self.doc.primitives.len() {
1377                    self.doc.primitives[*index] = (**new).clone();
1378                }
1379            }
1380            PcbEditOperation::MoveComponent {
1381                designator,
1382                new_pos,
1383                ..
1384            } => {
1385                if let Some(comp) = self.doc.find_component_mut(designator) {
1386                    comp.set_position(new_pos.x, new_pos.y);
1387                }
1388            }
1389        }
1390
1391        self.history.push(op);
1392        self.modified = true;
1393        Ok(())
1394    }
1395
1396    /// Clear all history.
1397    pub fn clear_history(&mut self) {
1398        self.history.clear();
1399        self.redo_stack.clear();
1400    }
1401}
1402
1403/// Primitive count statistics.
1404#[derive(Debug, Default, Clone)]
1405pub struct PrimitiveCount {
1406    pub tracks: usize,
1407    pub vias: usize,
1408    pub pads: usize,
1409    pub arcs: usize,
1410    pub fills: usize,
1411    pub texts: usize,
1412    pub regions: usize,
1413    pub polygons: usize,
1414    pub other: usize,
1415}
1416
1417impl PrimitiveCount {
1418    /// Total number of primitives.
1419    pub fn total(&self) -> usize {
1420        self.tracks
1421            + self.vias
1422            + self.pads
1423            + self.arcs
1424            + self.fills
1425            + self.texts
1426            + self.regions
1427            + self.polygons
1428            + self.other
1429    }
1430}
1431
1432#[cfg(test)]
1433mod tests {
1434    use super::*;
1435
1436    #[test]
1437    fn test_position_resolution() {
1438        let session = PcbEditSession::new();
1439
1440        let pos = Position::absolute(Coord::from_mils(100.0), Coord::from_mils(200.0));
1441        let point = session.resolve_position(&pos).unwrap();
1442
1443        assert!((point.x.to_mils() - 100.0).abs() < 0.1);
1444        assert!((point.y.to_mils() - 200.0).abs() < 0.1);
1445    }
1446
1447    #[test]
1448    fn test_add_track() {
1449        let mut session = PcbEditSession::new();
1450
1451        let start = CoordPoint::from_mils(0.0, 0.0);
1452        let end = CoordPoint::from_mils(100.0, 100.0);
1453
1454        let idx = session.add_track(start, end, None, None, None).unwrap();
1455        assert_eq!(idx, 0);
1456        assert_eq!(session.count_primitives().tracks, 1);
1457    }
1458
1459    #[test]
1460    fn test_add_via() {
1461        let mut session = PcbEditSession::new();
1462
1463        let location = CoordPoint::from_mils(50.0, 50.0);
1464        let idx = session
1465            .add_via(location, None, None, None, None, None)
1466            .unwrap();
1467
1468        assert_eq!(idx, 0);
1469        assert_eq!(session.count_primitives().vias, 1);
1470    }
1471
1472    #[test]
1473    fn test_primitive_count() {
1474        let mut session = PcbEditSession::new();
1475
1476        session
1477            .add_track(
1478                CoordPoint::from_mils(0.0, 0.0),
1479                CoordPoint::from_mils(100.0, 0.0),
1480                None,
1481                None,
1482                None,
1483            )
1484            .unwrap();
1485
1486        session
1487            .add_via(
1488                CoordPoint::from_mils(50.0, 50.0),
1489                None,
1490                None,
1491                None,
1492                None,
1493                None,
1494            )
1495            .unwrap();
1496
1497        let count = session.count_primitives();
1498        assert_eq!(count.tracks, 1);
1499        assert_eq!(count.vias, 1);
1500        assert_eq!(count.total(), 2);
1501    }
1502}