Skip to main content

altium_format/edit/
library.rs

1//! Library integration for instantiating components from SchLib files.
2
3use std::collections::HashMap;
4use std::path::Path;
5
6use crate::error::{AltiumError, Result};
7use crate::io::schlib::SchLibComponent;
8use crate::io::{SchDoc, SchLib};
9use crate::records::sch::{SchDesignator, SchParameter, SchRecord, TextOrientations};
10use crate::types::{Coord, CoordPoint, UnknownFields};
11
12use super::types::Orientation;
13
14/// Manages schematic libraries and component instantiation.
15pub struct LibraryManager {
16    /// Loaded libraries (path -> library).
17    libraries: HashMap<String, SchLib>,
18    /// Designator counters by prefix.
19    designator_counters: HashMap<String, u32>,
20}
21
22impl Default for LibraryManager {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl LibraryManager {
29    /// Create a new library manager.
30    pub fn new() -> Self {
31        Self {
32            libraries: HashMap::new(),
33            designator_counters: HashMap::new(),
34        }
35    }
36
37    /// Load a SchLib file.
38    pub fn load_library<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
39        let path_str = path.as_ref().to_string_lossy().to_string();
40        if self.libraries.contains_key(&path_str) {
41            return Ok(()); // Already loaded
42        }
43
44        let lib = SchLib::open_file(&path)?;
45        self.libraries.insert(path_str, lib);
46        Ok(())
47    }
48
49    /// Get a loaded library by path.
50    pub fn get_library(&self, path: &str) -> Option<&SchLib> {
51        self.libraries.get(path)
52    }
53
54    /// List all loaded libraries.
55    pub fn list_libraries(&self) -> Vec<&str> {
56        self.libraries.keys().map(|s| s.as_str()).collect()
57    }
58
59    /// Search for a component across all loaded libraries.
60    pub fn find_component(&self, lib_reference: &str) -> Option<(&str, &SchLibComponent)> {
61        for (path, lib) in &self.libraries {
62            for component in lib.iter() {
63                if component.name() == lib_reference {
64                    return Some((path.as_str(), component));
65                }
66            }
67        }
68        None
69    }
70
71    /// List all components in all loaded libraries.
72    pub fn list_all_components(&self) -> Vec<(&str, &str, &str)> {
73        let mut components = Vec::new();
74        for (path, lib) in &self.libraries {
75            for component in lib.iter() {
76                components.push((path.as_str(), component.name(), component.description()));
77            }
78        }
79        components
80    }
81
82    /// Search components by pattern (case-insensitive substring match).
83    pub fn search_components(&self, pattern: &str) -> Vec<(&str, &str, &str)> {
84        let pattern_lower = pattern.to_lowercase();
85        self.list_all_components()
86            .into_iter()
87            .filter(|(_, name, desc)| {
88                name.to_lowercase().contains(&pattern_lower)
89                    || desc.to_lowercase().contains(&pattern_lower)
90            })
91            .collect()
92    }
93
94    /// Initialize designator counters from an existing schematic.
95    pub fn init_designators_from(&mut self, doc: &SchDoc) {
96        self.designator_counters.clear();
97
98        for record in &doc.primitives {
99            if let SchRecord::Designator(d) = record {
100                let text = d.text();
101                if let Some((prefix, num)) = Self::parse_designator(text) {
102                    let entry = self.designator_counters.entry(prefix).or_insert(0);
103                    *entry = (*entry).max(num);
104                }
105            }
106        }
107    }
108
109    /// Parse a designator into prefix and number (e.g., "R1" -> ("R", 1)).
110    fn parse_designator(designator: &str) -> Option<(String, u32)> {
111        let mut prefix = String::new();
112        let mut num_str = String::new();
113
114        for c in designator.chars() {
115            if c.is_ascii_digit() {
116                num_str.push(c);
117            } else if num_str.is_empty() {
118                prefix.push(c);
119            } else {
120                // Letters after numbers - not a simple designator
121                return None;
122            }
123        }
124
125        if prefix.is_empty() || num_str.is_empty() {
126            return None;
127        }
128
129        num_str.parse().ok().map(|n| (prefix, n))
130    }
131
132    /// Get the next designator for a given prefix.
133    pub fn next_designator(&mut self, prefix: &str) -> String {
134        let counter = self
135            .designator_counters
136            .entry(prefix.to_string())
137            .or_insert(0);
138        *counter += 1;
139        format!("{}{}", prefix, counter)
140    }
141
142    /// Suggest a designator prefix based on component name.
143    pub fn suggest_designator_prefix(&self, lib_reference: &str) -> &str {
144        let name_upper = lib_reference.to_uppercase();
145
146        // Common component type prefixes
147        if name_upper.contains("RESISTOR") || name_upper.starts_with("R") && name_upper.len() <= 6 {
148            return "R";
149        }
150        if name_upper.contains("CAPACITOR") || name_upper.starts_with("C") && name_upper.len() <= 6
151        {
152            return "C";
153        }
154        if name_upper.contains("INDUCTOR") || name_upper.starts_with("L") && name_upper.len() <= 6 {
155            return "L";
156        }
157        if name_upper.contains("DIODE") || name_upper.starts_with("D") && name_upper.len() <= 6 {
158            return "D";
159        }
160        if name_upper.contains("TRANSISTOR")
161            || name_upper.contains("MOSFET")
162            || name_upper.contains("BJT")
163        {
164            return "Q";
165        }
166        if name_upper.contains("LED") {
167            return "D";
168        }
169        if name_upper.contains("CRYSTAL") || name_upper.contains("OSCILLATOR") {
170            return "Y";
171        }
172        if name_upper.contains("CONNECTOR") || name_upper.starts_with("J") {
173            return "J";
174        }
175        if name_upper.contains("HEADER") {
176            return "J";
177        }
178        if name_upper.contains("SWITCH") {
179            return "SW";
180        }
181        if name_upper.contains("FUSE") {
182            return "F";
183        }
184        if name_upper.contains("TRANSFORMER") {
185            return "T";
186        }
187        if name_upper.contains("RELAY") {
188            return "K";
189        }
190        if name_upper.contains("OPAMP") || name_upper.contains("OP-AMP") {
191            return "U";
192        }
193
194        // Default to U for ICs/generic components
195        "U"
196    }
197
198    /// Instantiate a component from a library into a schematic.
199    pub fn instantiate_component(
200        &mut self,
201        lib_reference: &str,
202        location: CoordPoint,
203        orientation: Orientation,
204        designator: Option<&str>,
205        doc: &mut SchDoc,
206    ) -> Result<usize> {
207        // Find the component in loaded libraries and clone what we need
208        let (lib_path, lib_component_clone, lib_primitives) = {
209            let (path, lib_component) = self.find_component(lib_reference).ok_or_else(|| {
210                AltiumError::Parse(format!("Component not found: {}", lib_reference))
211            })?;
212            (
213                path.to_string(),
214                lib_component.component.clone(),
215                lib_component.primitives.clone(),
216            )
217        };
218
219        // Generate designator if not provided
220        let designator = match designator {
221            Some(d) => {
222                // Update counter if this designator is higher
223                if let Some((prefix, num)) = Self::parse_designator(d) {
224                    let entry = self.designator_counters.entry(prefix).or_insert(0);
225                    *entry = (*entry).max(num);
226                }
227                d.to_string()
228            }
229            None => {
230                let prefix = self.suggest_designator_prefix(lib_reference).to_string();
231                self.next_designator(&prefix)
232            }
233        };
234
235        // The component will be added at this index
236        let component_index = doc.primitives.len();
237
238        // Create the component record
239        let mut component = lib_component_clone;
240        component.graphical.location_x = location.x.to_raw();
241        component.graphical.location_y = location.y.to_raw();
242        component.graphical.base.owner_index = -1; // Top-level
243        component.orientation = TextOrientations::from_int(orientation.to_altium());
244        component.library_path = lib_path.clone();
245        component.source_library_name = lib_path;
246
247        // Generate a unique ID
248        let mut det = ();
249        component.unique_id = format!("{}_{}", designator, uuid_simple_deterministic(&mut det));
250
251        doc.primitives.push(SchRecord::Component(component));
252
253        // Copy all child primitives, transforming coordinates and updating owner indices
254        for record in lib_primitives.iter().skip(1) {
255            // Skip the component record itself
256            let new_record = self.transform_primitive(
257                record,
258                location,
259                orientation,
260                component_index as i32,
261                &designator,
262            );
263
264            doc.primitives.push(new_record);
265        }
266
267        // Add designator if not already present
268        let has_designator = lib_primitives
269            .iter()
270            .any(|r| matches!(r, SchRecord::Designator(_)));
271        if !has_designator {
272            let designator_record =
273                self.create_designator(&designator, location, component_index as i32);
274            doc.primitives.push(designator_record);
275        }
276
277        Ok(component_index)
278    }
279
280    /// Transform a primitive's coordinates and update owner index.
281    fn transform_primitive(
282        &self,
283        record: &SchRecord,
284        location: CoordPoint,
285        orientation: Orientation,
286        owner_index: i32,
287        designator: &str,
288    ) -> SchRecord {
289        match record {
290            SchRecord::Pin(pin) => {
291                let mut new_pin = pin.clone();
292                new_pin.graphical.base.owner_index = owner_index;
293
294                // Transform pin location
295                let local =
296                    CoordPoint::from_raw(pin.graphical.location_x, pin.graphical.location_y);
297                let absolute = self.transform_point(local, location, orientation);
298                new_pin.graphical.location_x = absolute.x.to_raw();
299                new_pin.graphical.location_y = absolute.y.to_raw();
300
301                // Update pin rotation based on orientation
302                if orientation.rotation_degrees() != 0.0 {
303                    use crate::records::sch::PinConglomerateFlags;
304
305                    // Rotate the pin direction
306                    let rotated = pin.pin_conglomerate.contains(PinConglomerateFlags::ROTATED);
307                    let flipped = pin.pin_conglomerate.contains(PinConglomerateFlags::FLIPPED);
308
309                    // For 90-degree rotation, swap rotated flag
310                    let new_rotated = match orientation {
311                        Orientation::Rotated90 | Orientation::Rotated270 => !rotated,
312                        _ => rotated,
313                    };
314
315                    // Update flags
316                    if new_rotated {
317                        new_pin
318                            .pin_conglomerate
319                            .insert(PinConglomerateFlags::ROTATED);
320                    } else {
321                        new_pin
322                            .pin_conglomerate
323                            .remove(PinConglomerateFlags::ROTATED);
324                    }
325                    if flipped {
326                        new_pin
327                            .pin_conglomerate
328                            .insert(PinConglomerateFlags::FLIPPED);
329                    }
330                }
331
332                SchRecord::Pin(new_pin)
333            }
334            SchRecord::Line(line) => {
335                let mut new_line = line.clone();
336                new_line.graphical.base.owner_index = owner_index;
337
338                let start =
339                    CoordPoint::from_raw(line.graphical.location_x, line.graphical.location_y);
340                let end = CoordPoint::from_raw(line.corner_x, line.corner_y);
341
342                let new_start = self.transform_point(start, location, orientation);
343                let new_end = self.transform_point(end, location, orientation);
344
345                new_line.graphical.location_x = new_start.x.to_raw();
346                new_line.graphical.location_y = new_start.y.to_raw();
347                new_line.corner_x = new_end.x.to_raw();
348                new_line.corner_y = new_end.y.to_raw();
349
350                SchRecord::Line(new_line)
351            }
352            SchRecord::Rectangle(rect) => {
353                let mut new_rect = rect.clone();
354                new_rect.graphical.base.owner_index = owner_index;
355
356                let loc =
357                    CoordPoint::from_raw(rect.graphical.location_x, rect.graphical.location_y);
358                let corner = CoordPoint::from_raw(rect.corner_x, rect.corner_y);
359
360                let new_loc = self.transform_point(loc, location, orientation);
361                let new_corner = self.transform_point(corner, location, orientation);
362
363                new_rect.graphical.location_x = new_loc.x.to_raw();
364                new_rect.graphical.location_y = new_loc.y.to_raw();
365                new_rect.corner_x = new_corner.x.to_raw();
366                new_rect.corner_y = new_corner.y.to_raw();
367
368                SchRecord::Rectangle(new_rect)
369            }
370            SchRecord::Polygon(poly) => {
371                let mut new_poly = poly.clone();
372                new_poly.graphical.base.owner_index = owner_index;
373
374                let loc =
375                    CoordPoint::from_raw(poly.graphical.location_x, poly.graphical.location_y);
376                let new_loc = self.transform_point(loc, location, orientation);
377                new_poly.graphical.location_x = new_loc.x.to_raw();
378                new_poly.graphical.location_y = new_loc.y.to_raw();
379
380                // Transform vertices
381                new_poly.vertices = poly
382                    .vertices
383                    .iter()
384                    .map(|(x, y)| {
385                        let pt = CoordPoint::from_raw(*x, *y);
386                        let new_pt = self.transform_point(pt, location, orientation);
387                        (new_pt.x.to_raw(), new_pt.y.to_raw())
388                    })
389                    .collect();
390
391                SchRecord::Polygon(new_poly)
392            }
393            SchRecord::Polyline(polyline) => {
394                let mut new_polyline = polyline.clone();
395                new_polyline.graphical.base.owner_index = owner_index;
396
397                let loc = CoordPoint::from_raw(
398                    polyline.graphical.location_x,
399                    polyline.graphical.location_y,
400                );
401                let new_loc = self.transform_point(loc, location, orientation);
402                new_polyline.graphical.location_x = new_loc.x.to_raw();
403                new_polyline.graphical.location_y = new_loc.y.to_raw();
404
405                new_polyline.vertices = polyline
406                    .vertices
407                    .iter()
408                    .map(|(x, y)| {
409                        let pt = CoordPoint::from_raw(*x, *y);
410                        let new_pt = self.transform_point(pt, location, orientation);
411                        (new_pt.x.to_raw(), new_pt.y.to_raw())
412                    })
413                    .collect();
414
415                SchRecord::Polyline(new_polyline)
416            }
417            SchRecord::Arc(arc) => {
418                let mut new_arc = arc.clone();
419                new_arc.graphical.base.owner_index = owner_index;
420
421                let loc = CoordPoint::from_raw(arc.graphical.location_x, arc.graphical.location_y);
422                let new_loc = self.transform_point(loc, location, orientation);
423                new_arc.graphical.location_x = new_loc.x.to_raw();
424                new_arc.graphical.location_y = new_loc.y.to_raw();
425
426                // Adjust angles for rotation
427                let rotation = orientation.rotation_degrees();
428                if rotation != 0.0 {
429                    new_arc.start_angle = (arc.start_angle + rotation) % 360.0;
430                    new_arc.end_angle = (arc.end_angle + rotation) % 360.0;
431                }
432
433                SchRecord::Arc(new_arc)
434            }
435            SchRecord::Ellipse(ellipse) => {
436                let mut new_ellipse = ellipse.clone();
437                new_ellipse.graphical.base.owner_index = owner_index;
438
439                let loc = CoordPoint::from_raw(
440                    ellipse.graphical.location_x,
441                    ellipse.graphical.location_y,
442                );
443                let new_loc = self.transform_point(loc, location, orientation);
444                new_ellipse.graphical.location_x = new_loc.x.to_raw();
445                new_ellipse.graphical.location_y = new_loc.y.to_raw();
446
447                SchRecord::Ellipse(new_ellipse)
448            }
449            SchRecord::Label(label) => {
450                let mut new_label = label.clone();
451                new_label.graphical.base.owner_index = owner_index;
452
453                let loc =
454                    CoordPoint::from_raw(label.graphical.location_x, label.graphical.location_y);
455                let new_loc = self.transform_point(loc, location, orientation);
456                new_label.graphical.location_x = new_loc.x.to_raw();
457                new_label.graphical.location_y = new_loc.y.to_raw();
458
459                SchRecord::Label(new_label)
460            }
461            SchRecord::Designator(d) => {
462                let mut new_d = d.clone();
463                new_d.param.label.graphical.base.owner_index = owner_index;
464
465                // Update the designator text
466                new_d.param.label.text = designator.to_string();
467
468                let loc = CoordPoint::from_raw(
469                    d.param.label.graphical.location_x,
470                    d.param.label.graphical.location_y,
471                );
472                let new_loc = self.transform_point(loc, location, orientation);
473                new_d.param.label.graphical.location_x = new_loc.x.to_raw();
474                new_d.param.label.graphical.location_y = new_loc.y.to_raw();
475
476                SchRecord::Designator(new_d)
477            }
478            SchRecord::Parameter(p) => {
479                let mut new_p = p.clone();
480                new_p.label.graphical.base.owner_index = owner_index;
481
482                let loc = CoordPoint::from_raw(
483                    p.label.graphical.location_x,
484                    p.label.graphical.location_y,
485                );
486                let new_loc = self.transform_point(loc, location, orientation);
487                new_p.label.graphical.location_x = new_loc.x.to_raw();
488                new_p.label.graphical.location_y = new_loc.y.to_raw();
489
490                SchRecord::Parameter(new_p)
491            }
492            _ => {
493                // For other record types, just update owner index if possible
494                record.clone()
495            }
496        }
497    }
498
499    /// Transform a point from local (library) to absolute coordinates.
500    fn transform_point(
501        &self,
502        local: CoordPoint,
503        component_location: CoordPoint,
504        orientation: Orientation,
505    ) -> CoordPoint {
506        let rotated = local.rotate(CoordPoint::ZERO, orientation.rotation_degrees());
507
508        let mirrored = if orientation.is_mirrored() {
509            CoordPoint::new(-rotated.x, rotated.y)
510        } else {
511            rotated
512        };
513
514        mirrored.translate(component_location.x, component_location.y)
515    }
516
517    /// Create a designator record.
518    fn create_designator(&self, text: &str, location: CoordPoint, owner_index: i32) -> SchRecord {
519        use crate::records::sch::{
520            SchGraphicalBase, SchLabel, SchPrimitiveBase, TextJustification,
521        };
522
523        let label = SchLabel {
524            graphical: SchGraphicalBase {
525                base: SchPrimitiveBase {
526                    owner_index,
527                    ..Default::default()
528                },
529                location_x: location.x.to_raw() + Coord::from_mils(10.0).to_raw(),
530                location_y: location.y.to_raw() + Coord::from_mils(30.0).to_raw(),
531                ..Default::default()
532            },
533            text: text.to_string(),
534            justification: TextJustification::BOTTOM_LEFT,
535            font_id: 1,
536            ..Default::default()
537        };
538
539        let param = SchParameter {
540            label,
541            name: "Designator".to_string(),
542            read_only_state: 1,
543            ..Default::default()
544        };
545
546        SchRecord::Designator(SchDesignator {
547            param,
548            unknown_params: UnknownFields::default(),
549        })
550    }
551}
552
553/// Generate a simple UUID-like string (non-deterministic).
554///
555/// **Prefer using `uuid_simple_deterministic()` for reproducible execution.**
556#[deprecated(
557    since = "0.1.0",
558    note = "Use uuid_simple_deterministic() with a DeterminismContext for reproducible execution"
559)]
560#[allow(dead_code)]
561fn uuid_simple() -> String {
562    use std::time::{SystemTime, UNIX_EPOCH};
563
564    let timestamp = SystemTime::now()
565        .duration_since(UNIX_EPOCH)
566        .unwrap_or_default()
567        .as_nanos();
568
569    format!("{:016X}", timestamp & 0xFFFFFFFFFFFFFFFF)
570}
571
572/// Generate a simple UUID-like string deterministically.
573fn uuid_simple_deterministic(_det: &mut ()) -> String {
574    uuid::Uuid::new_v4().simple().to_string()
575}