Skip to main content

altium_format/edit/
session.rs

1//! Edit session for tracking changes and managing schematic modifications.
2
3use 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
22/// An editing session for a schematic document.
23pub struct EditSession {
24    /// The schematic document being edited.
25    pub doc: SchDoc,
26    /// Layout engine for placement.
27    layout: LayoutEngine,
28    /// Routing engine for wire connections.
29    routing: RoutingEngine,
30    /// Library manager for component instantiation.
31    library: LibraryManager,
32    /// Netlist builder for connectivity analysis.
33    netlist_builder: NetlistBuilder,
34    /// Operation history for undo.
35    history: Vec<EditOperation>,
36    /// Redo stack.
37    redo_stack: Vec<EditOperation>,
38    /// Path to the source file (if loaded from file).
39    source_path: Option<String>,
40    /// Whether the document has been modified.
41    modified: bool,
42}
43
44impl EditSession {
45    /// Create a new empty editing session.
46    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    /// Open an existing schematic file for editing.
61    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    /// Save the schematic to a file.
75    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    /// Save to the original file (if opened from file).
83    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    /// Check if the document has been modified.
95    pub fn is_modified(&self) -> bool {
96        self.modified
97    }
98
99    /// Get the source file path.
100    pub fn source_path(&self) -> Option<&str> {
101        self.source_path.as_deref()
102    }
103
104    /// Get the layout engine.
105    pub fn layout(&self) -> &LayoutEngine {
106        &self.layout
107    }
108
109    /// Get mutable access to the layout engine.
110    pub fn layout_mut(&mut self) -> &mut LayoutEngine {
111        &mut self.layout
112    }
113
114    /// Get the library manager.
115    pub fn library(&self) -> &LibraryManager {
116        &self.library
117    }
118
119    /// Get mutable access to the library manager.
120    pub fn library_mut(&mut self) -> &mut LibraryManager {
121        &mut self.library
122    }
123
124    /// Update routing obstacles from current document.
125    fn update_routing_obstacles(&mut self) {
126        self.routing
127            .update_obstacles(&self.doc.primitives, &self.layout);
128    }
129
130    /// Set the grid configuration.
131    pub fn set_grid(&mut self, grid: Grid) {
132        self.layout.set_grid(grid);
133        self.routing.set_grid(grid);
134    }
135
136    // ═══════════════════════════════════════════════════════════════════════
137    // COMPONENT OPERATIONS
138    // ═══════════════════════════════════════════════════════════════════════
139
140    /// Load a SchLib library file.
141    pub fn load_library<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
142        self.library.load_library(path)
143    }
144
145    /// Add a component from a loaded library.
146    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    /// Find the best position for a new component.
177    pub fn suggest_component_placement(
178        &self,
179        lib_reference: &str,
180        near_component: Option<&str>,
181    ) -> Vec<PlacementSuggestion> {
182        // Estimate component bounds (default if not in library)
183        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    /// Move a component to a new location.
204    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        // Move all child primitives
229        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    /// Translate a primitive by dx, dy.
266    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    /// Delete a component and all its child primitives.
325    pub fn delete_component(&mut self, index: usize) -> Result<()> {
326        // Verify it's a component
327        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        // Find all primitives owned by this component
335        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        // Remove in reverse order to maintain indices
357        to_remove.sort_unstable();
358        to_remove.reverse();
359
360        for i in &to_remove {
361            self.doc.primitives.remove(*i);
362        }
363
364        // Update owner indices for remaining primitives
365        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    /// Update owner indices after removing primitives.
377    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                // Count how many removed indices are below this owner
395                let offset = removed.iter().filter(|&&r| (r as i32) < *owner).count();
396                *owner -= offset as i32;
397            }
398        }
399    }
400
401    /// Find the designator for a component.
402    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    // ═══════════════════════════════════════════════════════════════════════
414    // WIRE OPERATIONS
415    // ═══════════════════════════════════════════════════════════════════════
416
417    /// Add a wire with specified vertices.
418    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, // LINEWIDTH=1 (standard wire width)
436            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    /// Route a wire between two points automatically.
453    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        // Add wire segments
463        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        // Add junctions if needed
471        for junction in self.routing.find_junctions(&path) {
472            self.add_junction(junction)?;
473        }
474
475        Ok(index)
476    }
477
478    /// Connect two component pins with automatic routing.
479    pub fn connect_pins(
480        &mut self,
481        component1: &str,
482        pin1: &str,
483        component2: &str,
484        pin2: &str,
485    ) -> Result<usize> {
486        // Find pin locations
487        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    /// Find the endpoint location of a pin.
494    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    /// Delete a wire by index.
512    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    // ═══════════════════════════════════════════════════════════════════════
530    // BUS OPERATIONS
531    // ═══════════════════════════════════════════════════════════════════════
532
533    /// Add a bus with specified vertices.
534    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    /// Add a bus entry connecting a bus to a wire.
566    ///
567    /// * `bus_point` - Point on the bus where the entry connects
568    /// * `wire_point` - Point where the wire will connect (typically 10 mil offset)
569    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    /// Find buses in the document.
594    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    /// Find a bus that contains or is near a point.
607    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; // 10 mil tolerance
611
612        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                // Check with tolerance
618                for window in bus.vertices.windows(2) {
619                    let (x1, y1) = window[0];
620                    let (x2, y2) = window[1];
621
622                    // Check vertical segment
623                    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                    // Check horizontal segment
633                    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    /// Route a wire to a bus, automatically creating a bus entry.
649    ///
650    /// Creates a bus entry at the nearest point on the bus to the wire endpoint,
651    /// then routes the wire to the bus entry's wire-side point.
652    pub fn route_to_bus(
653        &mut self,
654        wire_start: CoordPoint,
655        bus_point: CoordPoint,
656    ) -> Result<(usize, usize)> {
657        // Find the bus at or near the specified point
658        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        // Find the nearest point on the bus
663        let target_x = bus_point.x.to_raw();
664        let target_y = bus_point.y.to_raw();
665
666        // Snap to bus (find closest point on bus)
667        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            // Project point onto line segment
675            let (proj_x, proj_y) = if x1 == x2 {
676                // Vertical segment
677                let clamped_y = target_y.max(y1.min(y2)).min(y1.max(y2));
678                (x1, clamped_y)
679            } else if y1 == y2 {
680                // Horizontal segment
681                let clamped_x = target_x.max(x1.min(x2)).min(x1.max(x2));
682                (clamped_x, y1)
683            } else {
684                // Diagonal - use endpoint
685                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        // Calculate wire-side point (offset by 10 mil diagonally)
699        let offset = 100000; // 10 mil
700        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        // Create bus entry
708        let entry_idx = self.add_bus_entry(bus_entry_point, wire_entry_point)?;
709
710        // Route wire to bus entry
711        let wire_idx = self.route_wire(wire_start, wire_entry_point)?;
712
713        Ok((wire_idx, entry_idx))
714    }
715
716    // ═══════════════════════════════════════════════════════════════════════
717    // NET LABEL AND POWER OPERATIONS
718    // ═══════════════════════════════════════════════════════════════════════
719
720    /// Add a net label at a location.
721    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, // FONTID=1 (standard font)
733            ..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    /// Add a power port at a location.
755    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        // Power objects don't have area_color
768        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, // SHOWNETNAME=T (default)
776            font_id: 1,          // FONTID=1 (standard font)
777            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    /// Smart-wire a pin: create a wire stub from the pin endpoint and attach a net label or power port.
794    ///
795    /// Returns (wire_index, label_or_power_index).
796    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        // Find the pin location and direction
807        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        // Calculate wire end point: extend from pin endpoint in pin's direction
825        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        // Add wire stub from pin endpoint to wire end
833        let wire_idx = self.add_wire(&[endpoint, wire_end])?;
834
835        // Add net label or power port at wire end
836        let label_idx = if let Some(style) = power_style {
837            // Determine power port orientation based on pin direction
838            // Power port connection point is at its origin, with the symbol extending
839            // in the orientation direction. We want the symbol to point AWAY from the wire.
840            let orient = match direction {
841                Direction::Up => TextOrientations::NONE,           // Symbol points up
842                Direction::Down => TextOrientations::FLIPPED,      // Symbol points down
843                Direction::Left => TextOrientations::ROTATED,      // Symbol points left
844                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    /// Add a junction at a location.
857    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        // Junctions use INDEXINSHEET=-1 and don't have area_color
864        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    /// Add a port at a location.
886    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        // Ports don't have area_color
898        graphical.area_color = 0;
899
900        let port = SchPort {
901            graphical,
902            style: PortStyle::Right, // Default right-facing port
903            io_type,
904            name: name.to_string(),
905            width: 40,  // Standard port width (400 mils)
906            height: 10, // Standard port height (100 mils)
907            font_id: 1, // FONTID=1 (standard font)
908            ..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    // ═══════════════════════════════════════════════════════════════════════
919    // ANALYSIS AND VALIDATION
920    // ═══════════════════════════════════════════════════════════════════════
921
922    /// Build the netlist for the current document.
923    pub fn build_netlist(&self) -> Netlist {
924        self.netlist_builder.build(&self.doc.primitives)
925    }
926
927    /// Find unconnected pins.
928    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    /// Find missing junctions.
934    pub fn find_missing_junctions(&self) -> Vec<(i32, i32)> {
935        self.netlist_builder
936            .find_missing_junctions(&self.doc.primitives)
937    }
938
939    /// Add missing junctions automatically.
940    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    /// Validate the schematic and return any errors.
952    pub fn validate(&self) -> Vec<ValidationError> {
953        let mut errors = Vec::new();
954
955        // Check for component overlaps
956        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        // Check for duplicate designators
977        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        // Check for unconnected pins
996        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        // Check for missing junctions
1007        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    // ═══════════════════════════════════════════════════════════════════════
1021    // UNDO/REDO (placeholder - full implementation would require snapshots)
1022    // ═══════════════════════════════════════════════════════════════════════
1023
1024    /// Get the number of operations in history.
1025    pub fn history_count(&self) -> usize {
1026        self.history.len()
1027    }
1028
1029    /// Check if undo is available.
1030    pub fn can_undo(&self) -> bool {
1031        !self.history.is_empty()
1032    }
1033
1034    /// Check if redo is available.
1035    pub fn can_redo(&self) -> bool {
1036        !self.redo_stack.is_empty()
1037    }
1038
1039    /// Get a summary of recent operations.
1040    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}