1use 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#[derive(Debug, Clone)]
27pub enum Position {
28 Absolute(CoordPoint),
30 RelativeTo {
32 reference_index: usize,
33 offset: CoordPoint,
34 },
35 RelativeToComponent {
37 designator: String,
38 offset: CoordPoint,
39 },
40 RelativeToPad {
42 component: String,
43 pad: String,
44 offset: CoordPoint,
45 },
46 RelativeToEdge { edge: BoardEdge, offset: CoordPoint },
48 BoardCenter { offset: CoordPoint },
50}
51
52impl Position {
53 pub fn absolute(x: Coord, y: Coord) -> Self {
55 Position::Absolute(CoordPoint::new(x, y))
56 }
57
58 pub fn at(point: CoordPoint) -> Self {
60 Position::Absolute(point)
61 }
62
63 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 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 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#[derive(Debug, Clone)]
91pub enum TrackPath {
92 Direct { start: Position, end: Position },
94 Manhattan {
96 start: Position,
97 end: Position,
98 prefer_horizontal_first: bool,
99 },
100 Diagonal45 { start: Position, end: Position },
102 Vertices(Vec<Position>),
104}
105
106#[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
152pub struct PcbEditSession {
154 pub doc: PcbDoc,
156 placement: PcbPlacementEngine,
158 history: Vec<PcbEditOperation>,
160 redo_stack: Vec<PcbEditOperation>,
162 source_path: Option<String>,
164 modified: bool,
166 default_track_width: Coord,
168 default_via_diameter: Coord,
170 default_via_hole: Coord,
172 default_layer: Layer,
174}
175
176impl Default for PcbEditSession {
177 fn default() -> Self {
178 Self::new()
179 }
180}
181
182impl PcbEditSession {
183 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 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 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 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 pub fn is_modified(&self) -> bool {
234 self.modified
235 }
236
237 pub fn source_path(&self) -> Option<&str> {
239 self.source_path.as_deref()
240 }
241
242 pub fn set_grid(&mut self, grid: Grid) {
244 self.placement.set_grid(grid);
245 }
246
247 pub fn set_grid_mm(&mut self, spacing_mm: f64) {
249 self.placement.set_grid_mm(spacing_mm);
250 }
251
252 pub fn set_default_track_width(&mut self, width: Coord) {
254 self.default_track_width = width;
255 }
256
257 pub fn set_default_via_diameter(&mut self, diameter: Coord) {
259 self.default_via_diameter = diameter;
260 }
261
262 pub fn set_default_via_hole(&mut self, hole: Coord) {
264 self.default_via_hole = hole;
265 }
266
267 pub fn set_default_layer(&mut self, layer: Layer) {
269 self.default_layer = layer;
270 }
271
272 pub fn default_layer(&self) -> Layer {
274 self.default_layer
275 }
276
277 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 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 fn get_pad_position(&self, component: &str, pad_designator: &str) -> Result<CoordPoint> {
396 let comp = self
398 .doc
399 .find_component(component)
400 .ok_or_else(|| AltiumError::Parse(format!("Component '{}' not found", component)))?;
401
402 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 for prim in &comp.primitives {
409 if let PcbRecord::Pad(p) = prim {
410 if p.designator.eq_ignore_ascii_case(pad_designator) {
411 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 pub fn get_board_bounds(&self) -> CoordRect {
428 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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; pad.is_plated = false;
751
752 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 #[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 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 #[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 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 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 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 let text_record = PcbText::new(
911 location.x.to_mms(),
912 location.y.to_mms(),
913 text,
914 height.to_mms(),
915 0.15, rotation.unwrap_or(0.0),
917 false, 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 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 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 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 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 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 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 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 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 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 pub fn delete_all_vias(&mut self) -> Result<usize> {
1145 self.delete_primitives_where(|p| matches!(p, PcbRecord::Via(_)))
1146 }
1147
1148 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 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 pub fn nets(&self) -> &[String] {
1184 &self.doc.nets
1185 }
1186
1187 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn clear_history(&mut self) {
1398 self.history.clear();
1399 self.redo_stack.clear();
1400 }
1401}
1402
1403#[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 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}