1use std::path::Path;
4
5use crate::error::{AltiumError, Result};
6use crate::io::SchDoc;
7use crate::records::sch::{
8 LineStyle, LineWidth, PortIoType, PortStyle, PowerObjectStyle, SchBus, SchBusEntry,
9 SchGraphicalBase, SchJunction, SchLabel, SchNetLabel, SchPort, SchPowerObject, SchRecord,
10 SchWire, TextJustification, TextOrientations,
11};
12use crate::types::{Coord, CoordPoint, UnknownFields};
13
14use super::layout::LayoutEngine;
15use super::library::LibraryManager;
16use super::netlist::{Netlist, NetlistBuilder};
17use super::routing::RoutingEngine;
18use super::types::{
19 EditOperation, Grid, Orientation, PlacementSuggestion, ValidationError, ValidationErrorKind,
20};
21
22pub struct EditSession {
24 pub doc: SchDoc,
26 layout: LayoutEngine,
28 routing: RoutingEngine,
30 library: LibraryManager,
32 netlist_builder: NetlistBuilder,
34 history: Vec<EditOperation>,
36 redo_stack: Vec<EditOperation>,
38 source_path: Option<String>,
40 modified: bool,
42}
43
44impl EditSession {
45 pub fn new() -> Self {
47 Self {
48 doc: SchDoc::default(),
49 layout: LayoutEngine::new(),
50 routing: RoutingEngine::new(),
51 library: LibraryManager::new(),
52 netlist_builder: NetlistBuilder::new(),
53 history: Vec::new(),
54 redo_stack: Vec::new(),
55 source_path: None,
56 modified: false,
57 }
58 }
59
60 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
62 let path_str = path.as_ref().to_string_lossy().to_string();
63 let doc = SchDoc::open_file(&path)?;
64
65 let mut session = Self::new();
66 session.doc = doc;
67 session.source_path = Some(path_str);
68 session.library.init_designators_from(&session.doc);
69 session.update_routing_obstacles();
70
71 Ok(session)
72 }
73
74 pub fn save<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
76 self.doc.save_to_file(&path)?;
77 self.source_path = Some(path.as_ref().to_string_lossy().to_string());
78 self.modified = false;
79 Ok(())
80 }
81
82 pub fn save_to_original(&mut self) -> Result<()> {
84 match &self.source_path {
85 Some(path) => {
86 self.doc.save_to_file(path)?;
87 self.modified = false;
88 Ok(())
89 }
90 None => Err(AltiumError::Parse("No source file path".to_string())),
91 }
92 }
93
94 pub fn is_modified(&self) -> bool {
96 self.modified
97 }
98
99 pub fn source_path(&self) -> Option<&str> {
101 self.source_path.as_deref()
102 }
103
104 pub fn layout(&self) -> &LayoutEngine {
106 &self.layout
107 }
108
109 pub fn layout_mut(&mut self) -> &mut LayoutEngine {
111 &mut self.layout
112 }
113
114 pub fn library(&self) -> &LibraryManager {
116 &self.library
117 }
118
119 pub fn library_mut(&mut self) -> &mut LibraryManager {
121 &mut self.library
122 }
123
124 fn update_routing_obstacles(&mut self) {
126 self.routing
127 .update_obstacles(&self.doc.primitives, &self.layout);
128 }
129
130 pub fn set_grid(&mut self, grid: Grid) {
132 self.layout.set_grid(grid);
133 self.routing.set_grid(grid);
134 }
135
136 pub fn load_library<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
142 self.library.load_library(path)
143 }
144
145 pub fn add_component(
147 &mut self,
148 lib_reference: &str,
149 location: CoordPoint,
150 orientation: Orientation,
151 designator: Option<&str>,
152 ) -> Result<usize> {
153 let index = self.library.instantiate_component(
154 lib_reference,
155 location,
156 orientation,
157 designator,
158 &mut self.doc,
159 )?;
160
161 let designator_str = designator
162 .map(|s| s.to_string())
163 .unwrap_or_else(|| self.find_designator(index).unwrap_or_default());
164
165 self.history.push(EditOperation::AddComponent {
166 index,
167 designator: designator_str,
168 });
169 self.redo_stack.clear();
170 self.modified = true;
171 self.update_routing_obstacles();
172
173 Ok(index)
174 }
175
176 pub fn suggest_component_placement(
178 &self,
179 lib_reference: &str,
180 near_component: Option<&str>,
181 ) -> Vec<PlacementSuggestion> {
182 let bounds = self
184 .library
185 .find_component(lib_reference)
186 .map(|(_, comp)| {
187 self.layout
188 .calculate_component_bounds(&comp.component, &comp.primitives, 0)
189 })
190 .unwrap_or_else(|| {
191 crate::types::CoordRect::from_xywh(
192 Coord::ZERO,
193 Coord::ZERO,
194 Coord::from_mils(100.0),
195 Coord::from_mils(100.0),
196 )
197 });
198
199 self.layout
200 .suggest_placement(bounds, &self.doc.primitives, near_component)
201 }
202
203 pub fn move_component(&mut self, index: usize, new_location: CoordPoint) -> Result<()> {
205 let record = self
206 .doc
207 .primitives
208 .get_mut(index)
209 .ok_or_else(|| AltiumError::Parse("Component not found".to_string()))?;
210
211 let component = match record {
212 SchRecord::Component(c) => c,
213 _ => return Err(AltiumError::Parse("Not a component".to_string())),
214 };
215
216 let old_location = CoordPoint::from_raw(
217 component.graphical.location_x,
218 component.graphical.location_y,
219 );
220
221 let snapped = self.layout.snap_to_grid(new_location);
222 let dx = snapped.x.to_raw() - old_location.x.to_raw();
223 let dy = snapped.y.to_raw() - old_location.y.to_raw();
224
225 component.graphical.location_x = snapped.x.to_raw();
226 component.graphical.location_y = snapped.y.to_raw();
227
228 for (i, record) in self.doc.primitives.iter_mut().enumerate() {
230 if i == index {
231 continue;
232 }
233
234 let owner_index = match record {
235 SchRecord::Pin(p) => p.graphical.base.owner_index,
236 SchRecord::Line(l) => l.graphical.base.owner_index,
237 SchRecord::Rectangle(r) => r.graphical.base.owner_index,
238 SchRecord::Polygon(p) => p.graphical.base.owner_index,
239 SchRecord::Polyline(p) => p.graphical.base.owner_index,
240 SchRecord::Arc(a) => a.graphical.base.owner_index,
241 SchRecord::Ellipse(e) => e.graphical.base.owner_index,
242 SchRecord::Label(l) => l.graphical.base.owner_index,
243 SchRecord::Designator(d) => d.param.label.graphical.base.owner_index,
244 SchRecord::Parameter(p) => p.label.graphical.base.owner_index,
245 _ => -1,
246 };
247
248 if owner_index == index as i32 {
249 Self::translate_primitive(record, dx, dy);
250 }
251 }
252
253 self.history.push(EditOperation::MoveComponent {
254 index,
255 from: old_location,
256 to: snapped,
257 });
258 self.redo_stack.clear();
259 self.modified = true;
260 self.update_routing_obstacles();
261
262 Ok(())
263 }
264
265 fn translate_primitive(record: &mut SchRecord, dx: i32, dy: i32) {
267 match record {
268 SchRecord::Pin(p) => {
269 p.graphical.location_x += dx;
270 p.graphical.location_y += dy;
271 }
272 SchRecord::Line(l) => {
273 l.graphical.location_x += dx;
274 l.graphical.location_y += dy;
275 l.corner_x += dx;
276 l.corner_y += dy;
277 }
278 SchRecord::Rectangle(r) => {
279 r.graphical.location_x += dx;
280 r.graphical.location_y += dy;
281 r.corner_x += dx;
282 r.corner_y += dy;
283 }
284 SchRecord::Polygon(p) => {
285 p.graphical.location_x += dx;
286 p.graphical.location_y += dy;
287 for vertex in &mut p.vertices {
288 vertex.0 += dx;
289 vertex.1 += dy;
290 }
291 }
292 SchRecord::Polyline(p) => {
293 p.graphical.location_x += dx;
294 p.graphical.location_y += dy;
295 for vertex in &mut p.vertices {
296 vertex.0 += dx;
297 vertex.1 += dy;
298 }
299 }
300 SchRecord::Arc(a) => {
301 a.graphical.location_x += dx;
302 a.graphical.location_y += dy;
303 }
304 SchRecord::Ellipse(e) => {
305 e.graphical.location_x += dx;
306 e.graphical.location_y += dy;
307 }
308 SchRecord::Label(l) => {
309 l.graphical.location_x += dx;
310 l.graphical.location_y += dy;
311 }
312 SchRecord::Designator(d) => {
313 d.param.label.graphical.location_x += dx;
314 d.param.label.graphical.location_y += dy;
315 }
316 SchRecord::Parameter(p) => {
317 p.label.graphical.location_x += dx;
318 p.label.graphical.location_y += dy;
319 }
320 _ => {}
321 }
322 }
323
324 pub fn delete_component(&mut self, index: usize) -> Result<()> {
326 match self.doc.primitives.get(index) {
328 Some(SchRecord::Component(_)) => {}
329 _ => return Err(AltiumError::Parse("Not a component".to_string())),
330 }
331
332 let designator = self.find_designator(index).unwrap_or_default();
333
334 let mut to_remove: Vec<usize> = vec![index];
336 for (i, record) in self.doc.primitives.iter().enumerate() {
337 let owner_index = match record {
338 SchRecord::Pin(p) => p.graphical.base.owner_index,
339 SchRecord::Line(l) => l.graphical.base.owner_index,
340 SchRecord::Rectangle(r) => r.graphical.base.owner_index,
341 SchRecord::Polygon(p) => p.graphical.base.owner_index,
342 SchRecord::Polyline(p) => p.graphical.base.owner_index,
343 SchRecord::Arc(a) => a.graphical.base.owner_index,
344 SchRecord::Ellipse(e) => e.graphical.base.owner_index,
345 SchRecord::Label(l) => l.graphical.base.owner_index,
346 SchRecord::Designator(d) => d.param.label.graphical.base.owner_index,
347 SchRecord::Parameter(p) => p.label.graphical.base.owner_index,
348 _ => -1,
349 };
350
351 if owner_index == index as i32 && !to_remove.contains(&i) {
352 to_remove.push(i);
353 }
354 }
355
356 to_remove.sort_unstable();
358 to_remove.reverse();
359
360 for i in &to_remove {
361 self.doc.primitives.remove(*i);
362 }
363
364 self.update_owner_indices_after_removal(&to_remove);
366
367 self.history
368 .push(EditOperation::RemoveComponent { index, designator });
369 self.redo_stack.clear();
370 self.modified = true;
371 self.update_routing_obstacles();
372
373 Ok(())
374 }
375
376 fn update_owner_indices_after_removal(&mut self, removed: &[usize]) {
378 for record in &mut self.doc.primitives {
379 let owner = match record {
380 SchRecord::Pin(p) => &mut p.graphical.base.owner_index,
381 SchRecord::Line(l) => &mut l.graphical.base.owner_index,
382 SchRecord::Rectangle(r) => &mut r.graphical.base.owner_index,
383 SchRecord::Polygon(p) => &mut p.graphical.base.owner_index,
384 SchRecord::Polyline(p) => &mut p.graphical.base.owner_index,
385 SchRecord::Arc(a) => &mut a.graphical.base.owner_index,
386 SchRecord::Ellipse(e) => &mut e.graphical.base.owner_index,
387 SchRecord::Label(l) => &mut l.graphical.base.owner_index,
388 SchRecord::Designator(d) => &mut d.param.label.graphical.base.owner_index,
389 SchRecord::Parameter(p) => &mut p.label.graphical.base.owner_index,
390 _ => continue,
391 };
392
393 if *owner >= 0 {
394 let offset = removed.iter().filter(|&&r| (r as i32) < *owner).count();
396 *owner -= offset as i32;
397 }
398 }
399 }
400
401 fn find_designator(&self, component_index: usize) -> Option<String> {
403 for record in &self.doc.primitives {
404 if let SchRecord::Designator(d) = record {
405 if d.param.label.graphical.base.owner_index == component_index as i32 {
406 return Some(d.text().to_string());
407 }
408 }
409 }
410 None
411 }
412
413 pub fn add_wire(&mut self, vertices: &[CoordPoint]) -> Result<usize> {
419 if vertices.len() < 2 {
420 return Err(AltiumError::Parse(
421 "Wire must have at least 2 vertices".to_string(),
422 ));
423 }
424
425 let snapped: Vec<(i32, i32)> = vertices
426 .iter()
427 .map(|p| {
428 let s = self.layout.snap_to_grid(*p);
429 (s.x.to_raw(), s.y.to_raw())
430 })
431 .collect();
432
433 let wire = SchWire {
434 graphical: SchGraphicalBase::new_wire_or_text(),
435 line_width: LineWidth::Small, line_style: LineStyle::Solid,
437 vertices: snapped,
438 unknown_params: UnknownFields::default(),
439 };
440
441 let index = self.doc.primitives.len();
442 self.doc.primitives.push(SchRecord::Wire(wire));
443
444 self.history.push(EditOperation::AddWire { index });
445 self.redo_stack.clear();
446 self.modified = true;
447 self.update_routing_obstacles();
448
449 Ok(index)
450 }
451
452 pub fn route_wire(&mut self, start: CoordPoint, end: CoordPoint) -> Result<usize> {
454 self.routing
455 .update_obstacles(&self.doc.primitives, &self.layout);
456
457 let path = self
458 .routing
459 .route(start, end)
460 .ok_or_else(|| AltiumError::Parse("No route found".to_string()))?;
461
462 let vertices = path.vertices();
464 if vertices.is_empty() {
465 return Err(AltiumError::Parse("Empty route".to_string()));
466 }
467
468 let index = self.add_wire(&vertices)?;
469
470 for junction in self.routing.find_junctions(&path) {
472 self.add_junction(junction)?;
473 }
474
475 Ok(index)
476 }
477
478 pub fn connect_pins(
480 &mut self,
481 component1: &str,
482 pin1: &str,
483 component2: &str,
484 pin2: &str,
485 ) -> Result<usize> {
486 let pin1_loc = self.find_pin_location(component1, pin1)?;
488 let pin2_loc = self.find_pin_location(component2, pin2)?;
489
490 self.route_wire(pin1_loc, pin2_loc)
491 }
492
493 fn find_pin_location(&self, component: &str, pin: &str) -> Result<CoordPoint> {
495 let components = self.layout.get_placed_components(&self.doc.primitives);
496
497 let comp = components
498 .iter()
499 .find(|c| c.designator == component)
500 .ok_or_else(|| AltiumError::Parse(format!("Component not found: {}", component)))?;
501
502 let pin_loc = comp
503 .pin_locations
504 .iter()
505 .find(|p| p.designator == pin || p.name == pin)
506 .ok_or_else(|| AltiumError::Parse(format!("Pin not found: {}.{}", component, pin)))?;
507
508 Ok(pin_loc.location)
509 }
510
511 pub fn delete_wire(&mut self, index: usize) -> Result<()> {
513 match self.doc.primitives.get(index) {
514 Some(SchRecord::Wire(_)) => {}
515 _ => return Err(AltiumError::Parse("Not a wire".to_string())),
516 }
517
518 self.doc.primitives.remove(index);
519 self.update_owner_indices_after_removal(&[index]);
520
521 self.history.push(EditOperation::RemoveWire { index });
522 self.redo_stack.clear();
523 self.modified = true;
524 self.update_routing_obstacles();
525
526 Ok(())
527 }
528
529 pub fn add_bus(&mut self, vertices: &[CoordPoint]) -> Result<usize> {
535 if vertices.len() < 2 {
536 return Err(AltiumError::Parse(
537 "Bus must have at least 2 vertices".to_string(),
538 ));
539 }
540
541 let snapped: Vec<(i32, i32)> = vertices
542 .iter()
543 .map(|p| {
544 let s = self.layout.snap_to_grid(*p);
545 (s.x.to_raw(), s.y.to_raw())
546 })
547 .collect();
548
549 let bus = SchBus {
550 graphical: SchGraphicalBase::default(),
551 line_width: LineWidth::Medium,
552 vertices: snapped,
553 ..Default::default()
554 };
555
556 let index = self.doc.primitives.len();
557 self.doc.primitives.push(SchRecord::Bus(bus));
558
559 self.modified = true;
560 self.update_routing_obstacles();
561
562 Ok(index)
563 }
564
565 pub fn add_bus_entry(
570 &mut self,
571 bus_point: CoordPoint,
572 wire_point: CoordPoint,
573 ) -> Result<usize> {
574 let bus_snapped = self.layout.snap_to_grid(bus_point);
575 let wire_snapped = self.layout.snap_to_grid(wire_point);
576
577 let bus_entry = SchBusEntry::new(
578 bus_snapped.x.to_raw(),
579 bus_snapped.y.to_raw(),
580 wire_snapped.x.to_raw(),
581 wire_snapped.y.to_raw(),
582 );
583
584 let index = self.doc.primitives.len();
585 self.doc.primitives.push(SchRecord::BusEntry(bus_entry));
586
587 self.modified = true;
588 self.update_routing_obstacles();
589
590 Ok(index)
591 }
592
593 pub fn find_buses(&self) -> Vec<(usize, &SchBus)> {
595 self.doc
596 .primitives
597 .iter()
598 .enumerate()
599 .filter_map(|(i, p)| match p {
600 SchRecord::Bus(bus) => Some((i, bus)),
601 _ => None,
602 })
603 .collect()
604 }
605
606 pub fn find_bus_at(&self, point: CoordPoint) -> Option<(usize, &SchBus)> {
608 let x = point.x.to_raw();
609 let y = point.y.to_raw();
610 let tolerance = 100000; for (i, prim) in self.doc.primitives.iter().enumerate() {
613 if let SchRecord::Bus(bus) = prim {
614 if bus.contains_point(x, y) {
615 return Some((i, bus));
616 }
617 for window in bus.vertices.windows(2) {
619 let (x1, y1) = window[0];
620 let (x2, y2) = window[1];
621
622 if x1 == x2 {
624 let (min_y, max_y) = if y1 < y2 { (y1, y2) } else { (y2, y1) };
625 if (x - x1).abs() < tolerance
626 && y >= min_y - tolerance
627 && y <= max_y + tolerance
628 {
629 return Some((i, bus));
630 }
631 }
632 if y1 == y2 {
634 let (min_x, max_x) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
635 if (y - y1).abs() < tolerance
636 && x >= min_x - tolerance
637 && x <= max_x + tolerance
638 {
639 return Some((i, bus));
640 }
641 }
642 }
643 }
644 }
645 None
646 }
647
648 pub fn route_to_bus(
653 &mut self,
654 wire_start: CoordPoint,
655 bus_point: CoordPoint,
656 ) -> Result<(usize, usize)> {
657 let (_bus_idx, bus) = self
659 .find_bus_at(bus_point)
660 .ok_or_else(|| AltiumError::Parse("No bus found at specified point".to_string()))?;
661
662 let target_x = bus_point.x.to_raw();
664 let target_y = bus_point.y.to_raw();
665
666 let mut best_point = bus.vertices[0];
668 let mut best_dist = i64::MAX;
669
670 for window in bus.vertices.windows(2) {
671 let (x1, y1) = window[0];
672 let (x2, y2) = window[1];
673
674 let (proj_x, proj_y) = if x1 == x2 {
676 let clamped_y = target_y.max(y1.min(y2)).min(y1.max(y2));
678 (x1, clamped_y)
679 } else if y1 == y2 {
680 let clamped_x = target_x.max(x1.min(x2)).min(x1.max(x2));
682 (clamped_x, y1)
683 } else {
684 window[0]
686 };
687
688 let dist =
689 (proj_x as i64 - target_x as i64).pow(2) + (proj_y as i64 - target_y as i64).pow(2);
690 if dist < best_dist {
691 best_dist = dist;
692 best_point = (proj_x, proj_y);
693 }
694 }
695
696 let bus_entry_point = CoordPoint::from_raw(best_point.0, best_point.1);
697
698 let offset = 100000; let dx = if wire_start.x.to_raw() < best_point.0 {
701 -offset
702 } else {
703 offset
704 };
705 let wire_entry_point = CoordPoint::from_raw(best_point.0 + dx, best_point.1 + dx);
706
707 let entry_idx = self.add_bus_entry(bus_entry_point, wire_entry_point)?;
709
710 let wire_idx = self.route_wire(wire_start, wire_entry_point)?;
712
713 Ok((wire_idx, entry_idx))
714 }
715
716 pub fn add_net_label(&mut self, name: &str, location: CoordPoint) -> Result<usize> {
722 let snapped = self.layout.snap_to_grid(location);
723
724 let mut graphical = SchGraphicalBase::new_wire_or_text();
725 graphical.location_x = snapped.x.to_raw();
726 graphical.location_y = snapped.y.to_raw();
727
728 let label = SchLabel {
729 graphical,
730 text: name.to_string(),
731 justification: TextJustification::BOTTOM_LEFT,
732 font_id: 1, ..Default::default()
734 };
735
736 let net_label = SchNetLabel {
737 label,
738 unknown_params: UnknownFields::default(),
739 };
740
741 let index = self.doc.primitives.len();
742 self.doc.primitives.push(SchRecord::NetLabel(net_label));
743
744 self.history.push(EditOperation::AddNetLabel {
745 index,
746 net_name: name.to_string(),
747 });
748 self.redo_stack.clear();
749 self.modified = true;
750
751 Ok(index)
752 }
753
754 pub fn add_power_port(
756 &mut self,
757 net_name: &str,
758 location: CoordPoint,
759 style: PowerObjectStyle,
760 orientation: TextOrientations,
761 ) -> Result<usize> {
762 let snapped = self.layout.snap_to_grid(location);
763
764 let mut graphical = SchGraphicalBase::new_graphical();
765 graphical.location_x = snapped.x.to_raw();
766 graphical.location_y = snapped.y.to_raw();
767 graphical.area_color = 0;
769
770 let power = SchPowerObject {
771 graphical,
772 style,
773 orientation,
774 text: net_name.to_string(),
775 show_net_name: true, font_id: 1, unknown_params: UnknownFields::default(),
778 };
779
780 let index = self.doc.primitives.len();
781 self.doc.primitives.push(SchRecord::PowerObject(power));
782
783 self.history.push(EditOperation::AddPowerPort {
784 index,
785 net_name: net_name.to_string(),
786 });
787 self.redo_stack.clear();
788 self.modified = true;
789
790 Ok(index)
791 }
792
793 pub fn smart_wire_pin(
797 &mut self,
798 component: &str,
799 pin: &str,
800 net_name: &str,
801 power_style: Option<PowerObjectStyle>,
802 wire_length_mils: f64,
803 ) -> Result<(usize, usize)> {
804 use super::types::Direction;
805
806 let components = self.layout.get_placed_components(&self.doc.primitives);
808 let comp = components
809 .iter()
810 .find(|c| c.designator == component)
811 .ok_or_else(|| AltiumError::Parse(format!("Component not found: {}", component)))?;
812
813 let pin_loc = comp
814 .pin_locations
815 .iter()
816 .find(|p| p.designator == pin || p.name == pin)
817 .ok_or_else(|| {
818 AltiumError::Parse(format!("Pin not found: {}.{}", component, pin))
819 })?;
820
821 let endpoint = pin_loc.location;
822 let direction = pin_loc.direction;
823
824 let (dx, dy) = direction.unit_vector();
826 let wire_length_raw = (wire_length_mils * 10000.0) as i32;
827 let wire_end = CoordPoint::from_raw(
828 endpoint.x.to_raw() + dx * wire_length_raw,
829 endpoint.y.to_raw() + dy * wire_length_raw,
830 );
831
832 let wire_idx = self.add_wire(&[endpoint, wire_end])?;
834
835 let label_idx = if let Some(style) = power_style {
837 let orient = match direction {
841 Direction::Up => TextOrientations::NONE, Direction::Down => TextOrientations::FLIPPED, Direction::Left => TextOrientations::ROTATED, Direction::Right => {
845 TextOrientations::ROTATED | TextOrientations::FLIPPED
846 }
847 };
848 self.add_power_port(net_name, wire_end, style, orient)?
849 } else {
850 self.add_net_label(net_name, wire_end)?
851 };
852
853 Ok((wire_idx, label_idx))
854 }
855
856 pub fn add_junction(&mut self, location: CoordPoint) -> Result<usize> {
858 let snapped = self.layout.snap_to_grid(location);
859
860 let mut graphical = SchGraphicalBase::new_graphical();
861 graphical.location_x = snapped.x.to_raw();
862 graphical.location_y = snapped.y.to_raw();
863 graphical.base.owner_index = -1;
865 graphical.area_color = 0;
866
867 let junction = SchJunction {
868 graphical,
869 unknown_params: UnknownFields::default(),
870 };
871
872 let index = self.doc.primitives.len();
873 self.doc.primitives.push(SchRecord::Junction(junction));
874
875 self.history.push(EditOperation::AddJunction {
876 index,
877 location: snapped,
878 });
879 self.redo_stack.clear();
880 self.modified = true;
881
882 Ok(index)
883 }
884
885 pub fn add_port(
887 &mut self,
888 name: &str,
889 location: CoordPoint,
890 io_type: PortIoType,
891 ) -> Result<usize> {
892 let snapped = self.layout.snap_to_grid(location);
893
894 let mut graphical = SchGraphicalBase::new_graphical();
895 graphical.location_x = snapped.x.to_raw();
896 graphical.location_y = snapped.y.to_raw();
897 graphical.area_color = 0;
899
900 let port = SchPort {
901 graphical,
902 style: PortStyle::Right, io_type,
904 name: name.to_string(),
905 width: 40, height: 10, font_id: 1, ..Default::default()
909 };
910
911 let index = self.doc.primitives.len();
912 self.doc.primitives.push(SchRecord::Port(port));
913
914 self.modified = true;
915 Ok(index)
916 }
917
918 pub fn build_netlist(&self) -> Netlist {
924 self.netlist_builder.build(&self.doc.primitives)
925 }
926
927 pub fn find_unconnected_pins(&self) -> Vec<(String, String, (i32, i32))> {
929 self.netlist_builder
930 .find_unconnected_pins(&self.doc.primitives)
931 }
932
933 pub fn find_missing_junctions(&self) -> Vec<(i32, i32)> {
935 self.netlist_builder
936 .find_missing_junctions(&self.doc.primitives)
937 }
938
939 pub fn add_missing_junctions(&mut self) -> Result<usize> {
941 let missing = self.find_missing_junctions();
942 let count = missing.len();
943
944 for loc in missing {
945 self.add_junction(CoordPoint::from_raw(loc.0, loc.1))?;
946 }
947
948 Ok(count)
949 }
950
951 pub fn validate(&self) -> Vec<ValidationError> {
953 let mut errors = Vec::new();
954
955 let components = self.layout.get_placed_components(&self.doc.primitives);
957 for i in 0..components.len() {
958 for j in i + 1..components.len() {
959 if components[i].bounds.intersects(components[j].bounds) {
960 errors.push(ValidationError {
961 kind: ValidationErrorKind::ComponentOverlap,
962 message: format!(
963 "Components {} and {} overlap",
964 components[i].designator, components[j].designator
965 ),
966 location: Some(components[i].bounds.center()),
967 components: vec![
968 components[i].designator.clone(),
969 components[j].designator.clone(),
970 ],
971 });
972 }
973 }
974 }
975
976 let mut designators: std::collections::HashMap<&str, usize> =
978 std::collections::HashMap::new();
979 for comp in &components {
980 if !comp.designator.is_empty() {
981 *designators.entry(&comp.designator).or_insert(0) += 1;
982 }
983 }
984 for (des, count) in designators {
985 if count > 1 {
986 errors.push(ValidationError {
987 kind: ValidationErrorKind::DuplicateDesignator,
988 message: format!("Duplicate designator: {} (appears {} times)", des, count),
989 location: None,
990 components: vec![des.to_string()],
991 });
992 }
993 }
994
995 let unconnected = self.find_unconnected_pins();
997 for (comp, pin, loc) in unconnected {
998 errors.push(ValidationError {
999 kind: ValidationErrorKind::UnconnectedPin,
1000 message: format!("Unconnected pin: {}.{}", comp, pin),
1001 location: Some(CoordPoint::from_raw(loc.0, loc.1)),
1002 components: vec![comp],
1003 });
1004 }
1005
1006 let missing_junctions = self.find_missing_junctions();
1008 for loc in missing_junctions {
1009 errors.push(ValidationError {
1010 kind: ValidationErrorKind::MissingJunction,
1011 message: format!("Missing junction at ({}, {})", loc.0 / 10000, loc.1 / 10000),
1012 location: Some(CoordPoint::from_raw(loc.0, loc.1)),
1013 components: vec![],
1014 });
1015 }
1016
1017 errors
1018 }
1019
1020 pub fn history_count(&self) -> usize {
1026 self.history.len()
1027 }
1028
1029 pub fn can_undo(&self) -> bool {
1031 !self.history.is_empty()
1032 }
1033
1034 pub fn can_redo(&self) -> bool {
1036 !self.redo_stack.is_empty()
1037 }
1038
1039 pub fn recent_operations(&self, count: usize) -> Vec<String> {
1041 self.history
1042 .iter()
1043 .rev()
1044 .take(count)
1045 .map(|op| match op {
1046 EditOperation::AddComponent { designator, .. } => {
1047 format!("Add component {}", designator)
1048 }
1049 EditOperation::RemoveComponent { designator, .. } => {
1050 format!("Remove component {}", designator)
1051 }
1052 EditOperation::MoveComponent { index, .. } => {
1053 format!("Move component at index {}", index)
1054 }
1055 EditOperation::AddWire { .. } => "Add wire".to_string(),
1056 EditOperation::RemoveWire { .. } => "Remove wire".to_string(),
1057 EditOperation::AddJunction { .. } => "Add junction".to_string(),
1058 EditOperation::AddNetLabel { net_name, .. } => {
1059 format!("Add net label {}", net_name)
1060 }
1061 EditOperation::AddPowerPort { net_name, .. } => {
1062 format!("Add power port {}", net_name)
1063 }
1064 EditOperation::Batch { operations } => {
1065 format!("Batch ({} operations)", operations.len())
1066 }
1067 })
1068 .collect()
1069 }
1070}
1071
1072impl Default for EditSession {
1073 fn default() -> Self {
1074 Self::new()
1075 }
1076}