Skip to main content

altium_format/ops/
schlib.rs

1// SPDX-License-Identifier: GPL-3.0-only
2// SPDX-FileCopyrightText: 2026 Alexander Kiselev <alex@akiselev.com>
3//
4//! Schematic library operations.
5//!
6//! High-level operations for exploring and manipulating Altium schematic library (.SchLib) files.
7
8// cmd_* functions mix presentation and business logic; separation punted until usage patterns clarify abstraction boundaries (premature abstraction risk)
9
10use std::collections::HashMap;
11use std::fs::File;
12use std::io::{BufReader, Cursor};
13use std::path::Path;
14
15use serde::{Deserialize, Serialize};
16use serde_json;
17
18use super::util::alphanumeric_sort;
19use crate::dump::fmt_coord;
20use crate::io::{SchLib, SchLibComponent};
21use crate::ops::categorization::categorize_component;
22use crate::ops::output::*;
23use crate::records::sch::{
24    LineWidth, PinConglomerateFlags, PinElectricalType, PinSymbol, SchArc, SchComponent,
25    SchEllipse, SchGraphicalBase, SchLabel, SchLine, SchPin, SchPolygon, SchPolyline, SchRecord,
26    SchRectangle, TextJustification, TextOrientations,
27};
28use crate::types::Unit;
29
30fn open_schlib(path: &Path) -> Result<SchLib, Box<dyn std::error::Error>> {
31    let file = File::open(path)?;
32    Ok(SchLib::open(BufReader::new(file))?)
33}
34
35/// Get electrical type name.
36fn electrical_type_name(et: &PinElectricalType) -> &'static str {
37    match et {
38        PinElectricalType::Input => "Input",
39        PinElectricalType::InputOutput => "I/O",
40        PinElectricalType::Output => "Output",
41        PinElectricalType::OpenCollector => "Open Collector",
42        PinElectricalType::Passive => "Passive",
43        PinElectricalType::HiZ => "Hi-Z",
44        PinElectricalType::OpenEmitter => "Open Emitter",
45        PinElectricalType::Power => "Power",
46    }
47}
48
49/// Get record type name.
50fn record_type_name(record: &SchRecord) -> &'static str {
51    match record {
52        SchRecord::Component(_) => "Component",
53        SchRecord::Pin(_) => "Pin",
54        SchRecord::Symbol(_) => "Symbol",
55        SchRecord::Label(_) => "Label",
56        SchRecord::Bezier(_) => "Bezier",
57        SchRecord::Polyline(_) => "Polyline",
58        SchRecord::Polygon(_) => "Polygon",
59        SchRecord::Ellipse(_) => "Ellipse",
60        SchRecord::Pie(_) => "Pie",
61        SchRecord::EllipticalArc(_) => "EllipticalArc",
62        SchRecord::Arc(_) => "Arc",
63        SchRecord::Line(_) => "Line",
64        SchRecord::Rectangle(_) => "Rectangle",
65        SchRecord::PowerObject(_) => "PowerObject",
66        SchRecord::Port(_) => "Port",
67        SchRecord::NoErc(_) => "NoERC",
68        SchRecord::NetLabel(_) => "NetLabel",
69        SchRecord::Bus(_) => "Bus",
70        SchRecord::Wire(_) => "Wire",
71        SchRecord::TextFrame(_) => "TextFrame",
72        SchRecord::TextFrameVariant(_) => "TextFrameVariant",
73        SchRecord::Junction(_) => "Junction",
74        SchRecord::Image(_) => "Image",
75        SchRecord::SheetHeader(_) => "SheetHeader",
76        SchRecord::Designator(_) => "Designator",
77        SchRecord::BusEntry(_) => "BusEntry",
78        SchRecord::Parameter(_) => "Parameter",
79        SchRecord::WarningSign(_) => "WarningSign",
80        SchRecord::ImplementationList(_) => "ImplementationList",
81        SchRecord::Implementation(_) => "Implementation",
82        SchRecord::MapDefinerList(_) => "MapDefinerList",
83        SchRecord::MapDefiner(_) => "MapDefiner",
84        SchRecord::ImplementationParameters(_) => "ImplementationParameters",
85        SchRecord::Unknown { .. } => "Unknown",
86    }
87}
88
89// ═══════════════════════════════════════════════════════════════════════════
90// HIGH-LEVEL COMMANDS
91// ═══════════════════════════════════════════════════════════════════════════
92
93/// Complete library overview.
94pub fn cmd_overview(path: &Path, full: bool) -> Result<SchLibOverview, Box<dyn std::error::Error>> {
95    let lib = open_schlib(path)?;
96
97    // ─────────────────────────────────────────────────────────────────────────
98    // 1. COMPONENTS BY CATEGORY
99    // ─────────────────────────────────────────────────────────────────────────
100    let mut categories: HashMap<&'static str, Vec<ComponentSummary>> = HashMap::new();
101
102    for comp in lib.iter() {
103        let category = categorize_component(
104            &comp.component.lib_reference,
105            &comp.component.component_description,
106        );
107        categories
108            .entry(category)
109            .or_default()
110            .push(ComponentSummary {
111                name: comp.component.lib_reference.clone(),
112                description: comp.component.component_description.clone(),
113                pin_count: comp.pin_count(),
114                part_count: comp.component.part_count,
115            });
116    }
117
118    // Sort categories by importance
119    let category_order = [
120        "Microcontroller",
121        "FPGA/CPLD",
122        "Memory",
123        "ADC",
124        "DAC",
125        "Transceiver/PHY",
126        "Clock/Oscillator",
127        "Power Supply",
128        "Amplifier",
129        "Mux/Switch",
130        "Buffer/Driver",
131        "Other IC",
132        "Transistor",
133        "Diode/Protection",
134        "LED",
135        "Capacitor",
136        "Resistor",
137        "Inductor/Ferrite",
138        "Connector",
139        "Test Point",
140    ];
141
142    let mut components_by_category = Vec::new();
143    for category in category_order.iter() {
144        if let Some(comps) = categories.remove(*category) {
145            components_by_category.push((category.to_string(), comps));
146        }
147    }
148
149    // Add any uncategorized
150    for (category, comps) in categories {
151        if !comps.is_empty() {
152            components_by_category.push((category.to_string(), comps));
153        }
154    }
155
156    // ─────────────────────────────────────────────────────────────────────────
157    // 2. PIN STATISTICS
158    // ─────────────────────────────────────────────────────────────────────────
159    let mut total_pins = 0;
160    let mut pin_types: HashMap<String, usize> = HashMap::new();
161
162    for comp in lib.iter() {
163        for prim in &comp.primitives {
164            if let SchRecord::Pin(pin) = prim {
165                total_pins += 1;
166                *pin_types
167                    .entry(electrical_type_name(&pin.electrical).to_string())
168                    .or_insert(0) += 1;
169            }
170        }
171    }
172
173    let mut sorted_types: Vec<_> = pin_types.into_iter().collect();
174    sorted_types.sort_by(|a, b| b.1.cmp(&a.1));
175
176    let pin_statistics = PinStatistics {
177        total_pins,
178        pin_types: sorted_types,
179    };
180
181    // ─────────────────────────────────────────────────────────────────────────
182    // 3. MULTI-PART COMPONENTS
183    // ─────────────────────────────────────────────────────────────────────────
184    let multi_part_components: Vec<ComponentSummary> = lib
185        .iter()
186        .filter(|c| c.component.part_count > 1)
187        .map(|comp| ComponentSummary {
188            name: comp.component.lib_reference.clone(),
189            description: comp.component.component_description.clone(),
190            pin_count: comp.pin_count(),
191            part_count: comp.component.part_count,
192        })
193        .collect();
194
195    // ─────────────────────────────────────────────────────────────────────────
196    // 4. LARGEST COMPONENTS
197    // ─────────────────────────────────────────────────────────────────────────
198    let mut largest_components: Vec<ComponentSummary> = lib
199        .iter()
200        .map(|comp| ComponentSummary {
201            name: comp.component.lib_reference.clone(),
202            description: comp.component.component_description.clone(),
203            pin_count: comp.pin_count(),
204            part_count: comp.component.part_count,
205        })
206        .collect();
207    largest_components.sort_by(|a, b| b.pin_count.cmp(&a.pin_count));
208    largest_components.truncate(10);
209
210    // ─────────────────────────────────────────────────────────────────────────
211    // 5. FULL COMPONENT DETAILS (if requested)
212    // ─────────────────────────────────────────────────────────────────────────
213    let component_details = if full {
214        Some(
215            lib.iter()
216                .map(|comp| {
217                    let pins = comp
218                        .primitives
219                        .iter()
220                        .filter_map(|prim| {
221                            if let SchRecord::Pin(pin) = prim {
222                                Some(PinDetail {
223                                    designator: pin.designator.clone(),
224                                    name: pin.name.clone(),
225                                    electrical_type: electrical_type_name(&pin.electrical)
226                                        .to_string(),
227                                    description: pin.description.clone(),
228                                })
229                            } else {
230                                None
231                            }
232                        })
233                        .collect();
234
235                    SchLibComponentDetail {
236                        name: comp.component.lib_reference.clone(),
237                        description: comp.component.component_description.clone(),
238                        part_count: comp.component.part_count,
239                        display_mode_count: comp.component.display_mode_count,
240                        pin_count: comp.pin_count(),
241                        total_primitives: comp.primitives.len(),
242                        pins,
243                        primitive_counts: None,
244                    }
245                })
246                .collect(),
247        )
248    } else {
249        None
250    };
251
252    Ok(SchLibOverview {
253        path: path.display().to_string(),
254        total_components: lib.components.len(),
255        components_by_category,
256        pin_statistics,
257        multi_part_components,
258        largest_components,
259        component_details,
260    })
261}
262
263/// List all components.
264pub fn cmd_list(path: &Path) -> Result<SchLibComponentList, Box<dyn std::error::Error>> {
265    let lib = open_schlib(path)?;
266
267    let components: Vec<ComponentSummary> = lib
268        .iter()
269        .map(|comp| ComponentSummary {
270            name: comp.component.lib_reference.clone(),
271            description: comp.component.component_description.clone(),
272            pin_count: comp.pin_count(),
273            part_count: comp.component.part_count,
274        })
275        .collect();
276
277    Ok(SchLibComponentList {
278        path: path.display().to_string(),
279        total_components: lib.components.len(),
280        components,
281    })
282}
283
284/// Search for components.
285pub fn cmd_search(
286    path: &Path,
287    query: &str,
288    limit: Option<usize>,
289) -> Result<SchLibSearchResults, Box<dyn std::error::Error>> {
290    let lib = open_schlib(path)?;
291
292    let query_lower = query.to_lowercase();
293    let has_wildcard = query.contains('*');
294
295    let matches: Vec<ComponentSummary> = lib
296        .iter()
297        .filter(|comp| {
298            let name = comp.component.lib_reference.to_lowercase();
299            let desc = comp.component.component_description.to_lowercase();
300
301            if has_wildcard {
302                let pattern = query_lower.replace('*', "");
303                name.contains(&pattern) || desc.contains(&pattern)
304            } else {
305                name.contains(&query_lower) || desc.contains(&query_lower)
306            }
307        })
308        .map(|comp| ComponentSummary {
309            name: comp.component.lib_reference.clone(),
310            description: comp.component.component_description.clone(),
311            pin_count: comp.pin_count(),
312            part_count: comp.component.part_count,
313        })
314        .collect();
315
316    let total_matches = matches.len();
317    let results = if let Some(limit) = limit {
318        matches.into_iter().take(limit).collect()
319    } else {
320        matches
321    };
322
323    Ok(SchLibSearchResults {
324        query: query.to_string(),
325        total_matches,
326        results,
327    })
328}
329
330// ═══════════════════════════════════════════════════════════════════════════
331// DETAILED COMMANDS
332// ═══════════════════════════════════════════════════════════════════════════
333
334/// Library info and statistics.
335pub fn cmd_info(path: &Path) -> Result<SchLibInfo, Box<dyn std::error::Error>> {
336    let lib = open_schlib(path)?;
337
338    // Count primitive types across all components
339    let mut primitive_counts: HashMap<String, usize> = HashMap::new();
340    let mut total_primitives = 0;
341
342    for comp in lib.iter() {
343        for prim in &comp.primitives {
344            let name = record_type_name(prim).to_string();
345            *primitive_counts.entry(name).or_insert(0) += 1;
346            total_primitives += 1;
347        }
348    }
349
350    let mut sorted: Vec<_> = primitive_counts.into_iter().collect();
351    sorted.sort_by(|a, b| b.1.cmp(&a.1));
352
353    // Multi-part component count
354    let multi_part_count = lib.iter().filter(|c| c.component.part_count > 1).count();
355
356    Ok(SchLibInfo {
357        path: path.display().to_string(),
358        component_count: lib.components.len(),
359        total_primitives,
360        primitive_types: sorted,
361        multi_part_count,
362    })
363}
364
365/// Component details.
366pub fn cmd_component(
367    path: &Path,
368    name: &str,
369    show_primitives: bool,
370) -> Result<SchLibComponentDetail, Box<dyn std::error::Error>> {
371    let lib = open_schlib(path)?;
372
373    let name_lower = name.to_lowercase();
374    let comp = lib
375        .iter()
376        .find(|c| c.component.lib_reference.to_lowercase() == name_lower)
377        .ok_or_else(|| format!("Component '{}' not found", name))?;
378
379    // List pins
380    let mut pins: Vec<&SchPin> = comp
381        .primitives
382        .iter()
383        .filter_map(|p| {
384            if let SchRecord::Pin(pin) = p {
385                Some(pin)
386            } else {
387                None
388            }
389        })
390        .collect();
391
392    pins.sort_by(|a, b| alphanumeric_sort(&a.designator, &b.designator));
393
394    let pins_detail: Vec<PinDetail> = pins
395        .iter()
396        .map(|pin| PinDetail {
397            designator: pin.designator.clone(),
398            name: pin.name.clone(),
399            electrical_type: electrical_type_name(&pin.electrical).to_string(),
400            description: pin.description.clone(),
401        })
402        .collect();
403
404    let primitive_counts = if show_primitives {
405        let mut prim_counts: HashMap<String, usize> = HashMap::new();
406        for prim in &comp.primitives {
407            *prim_counts
408                .entry(record_type_name(prim).to_string())
409                .or_insert(0) += 1;
410        }
411        Some(prim_counts.into_iter().collect())
412    } else {
413        None
414    };
415
416    Ok(SchLibComponentDetail {
417        name: comp.component.lib_reference.clone(),
418        description: comp.component.component_description.clone(),
419        part_count: comp.component.part_count,
420        display_mode_count: comp.component.display_mode_count,
421        pin_count: comp.pin_count(),
422        total_primitives: comp.primitive_count(),
423        pins: pins_detail,
424        primitive_counts,
425    })
426}
427
428/// List pins.
429pub fn cmd_pins(
430    path: &Path,
431    component_filter: Option<String>,
432    by_type: bool,
433) -> Result<SchLibPinList, Box<dyn std::error::Error>> {
434    let lib = open_schlib(path)?;
435
436    let filter_lower = component_filter.as_ref().map(|s| s.to_lowercase());
437
438    let mut all_pins: Vec<PinWithComponent> = Vec::new();
439
440    for comp in lib.iter() {
441        if let Some(ref filter) = filter_lower {
442            if !comp.component.lib_reference.to_lowercase().contains(filter) {
443                continue;
444            }
445        }
446
447        for prim in &comp.primitives {
448            if let SchRecord::Pin(pin) = prim {
449                all_pins.push(PinWithComponent {
450                    component_name: comp.component.lib_reference.clone(),
451                    designator: pin.designator.clone(),
452                    name: pin.name.clone(),
453                    electrical_type: electrical_type_name(&pin.electrical).to_string(),
454                });
455            }
456        }
457    }
458
459    let pins_by_type = if by_type {
460        let mut by_type: HashMap<String, Vec<PinWithComponent>> = HashMap::new();
461        for pin in &all_pins {
462            by_type
463                .entry(pin.electrical_type.clone())
464                .or_default()
465                .push(pin.clone());
466        }
467
468        let type_order = [
469            "Input",
470            "Output",
471            "I/O",
472            "Passive",
473            "Power",
474            "Open Collector",
475            "Open Emitter",
476            "Hi-Z",
477        ];
478        let mut ordered: Vec<(String, Vec<PinWithComponent>)> = Vec::new();
479
480        for etype in type_order {
481            if let Some(pins) = by_type.remove(etype) {
482                ordered.push((etype.to_string(), pins));
483            }
484        }
485
486        // Add any remaining types not in the order
487        for (etype, pins) in by_type {
488            ordered.push((etype, pins));
489        }
490
491        Some(ordered)
492    } else {
493        None
494    };
495
496    Ok(SchLibPinList {
497        path: path.display().to_string(),
498        total_pins: all_pins.len(),
499        pins: all_pins,
500        pins_by_type,
501    })
502}
503
504/// Show primitives for a component.
505pub fn cmd_primitives(
506    path: &Path,
507    name: &str,
508) -> Result<SchLibPrimitiveList, Box<dyn std::error::Error>> {
509    let lib = open_schlib(path)?;
510
511    let name_lower = name.to_lowercase();
512    let comp = lib
513        .iter()
514        .find(|c| c.component.lib_reference.to_lowercase() == name_lower)
515        .ok_or_else(|| format!("Component '{}' not found", name))?;
516
517    // Skip the first primitive (component record itself)
518    let primitives: Vec<PrimitiveInfo> = comp
519        .primitives
520        .iter()
521        .skip(1)
522        .map(|prim| match prim {
523            SchRecord::Pin(p) => PrimitiveInfo::Pin {
524                designator: p.designator.clone(),
525                name: p.name.clone(),
526                electrical_type: electrical_type_name(&p.electrical).to_string(),
527                x: fmt_coord(p.graphical.location_x),
528                y: fmt_coord(p.graphical.location_y),
529            },
530            SchRecord::Rectangle(r) => PrimitiveInfo::Rectangle {
531                x1: fmt_coord(r.graphical.location_x),
532                y1: fmt_coord(r.graphical.location_y),
533                x2: fmt_coord(r.corner_x),
534                y2: fmt_coord(r.corner_y),
535            },
536            SchRecord::Line(l) => PrimitiveInfo::Line {
537                x1: fmt_coord(l.graphical.location_x),
538                y1: fmt_coord(l.graphical.location_y),
539                x2: fmt_coord(l.corner_x),
540                y2: fmt_coord(l.corner_y),
541            },
542            SchRecord::Arc(a) => PrimitiveInfo::Arc {
543                center_x: fmt_coord(a.graphical.location_x),
544                center_y: fmt_coord(a.graphical.location_y),
545                radius: fmt_coord(a.radius),
546                start_angle: a.start_angle,
547                end_angle: a.end_angle,
548            },
549            SchRecord::Polygon(p) => PrimitiveInfo::Polygon {
550                vertex_count: p.vertices.len(),
551            },
552            SchRecord::Polyline(p) => PrimitiveInfo::Polyline {
553                vertex_count: p.vertices.len(),
554            },
555            SchRecord::Label(l) => PrimitiveInfo::Label {
556                text: l.text.clone(),
557                x: fmt_coord(l.graphical.location_x),
558                y: fmt_coord(l.graphical.location_y),
559            },
560            _ => PrimitiveInfo::Other {
561                primitive_type: record_type_name(prim).to_string(),
562            },
563        })
564        .collect();
565
566    Ok(SchLibPrimitiveList {
567        component_name: comp.component.lib_reference.clone(),
568        total_primitives: comp.primitive_count(),
569        primitives,
570    })
571}
572
573/// Export as JSON - returns component list (let presentation layer handle JSON serialization).
574pub fn cmd_json(path: &Path) -> Result<SchLibComponentList, Box<dyn std::error::Error>> {
575    cmd_list(path)
576}
577
578// ═══════════════════════════════════════════════════════════════════════════
579// CREATION COMMAND IMPLEMENTATIONS
580// ═══════════════════════════════════════════════════════════════════════════
581
582/// Embedded blank SchLib template.
583const BLANK_SCHLIB_TEMPLATE: &[u8] = include_bytes!("../../data/blank/Schlib1.SchLib");
584
585/// Create a new empty SchLib file.
586pub fn cmd_create(path: &Path) -> Result<String, Box<dyn std::error::Error>> {
587    if path.exists() {
588        return Err(format!("File already exists: {}", path.display()).into());
589    }
590
591    std::fs::write(path, BLANK_SCHLIB_TEMPLATE)?;
592
593    Ok(format!("Created empty SchLib: {}", path.display()))
594}
595
596fn load_blank_schlib() -> Result<SchLib, Box<dyn std::error::Error>> {
597    Ok(SchLib::open(Cursor::new(BLANK_SCHLIB_TEMPLATE))?)
598}
599
600/// Open or create a SchLib file.
601fn open_or_create_schlib(path: &Path) -> Result<SchLib, Box<dyn std::error::Error>> {
602    if path.exists() {
603        open_schlib(path)
604    } else {
605        load_blank_schlib()
606    }
607}
608
609/// Save a SchLib file.
610fn save_schlib(path: &Path, lib: &SchLib) -> Result<(), Box<dyn std::error::Error>> {
611    Ok(lib.save_to_file(path)?)
612}
613
614/// Parse a hex color string to Win32 COLORREF (BGR format).
615fn parse_color(hex: &str) -> Result<i32, Box<dyn std::error::Error>> {
616    // Remove leading # if present
617    let hex = hex.trim_start_matches('#');
618
619    if hex.len() != 6 {
620        return Err(format!(
621            "Invalid color format: {}. Expected 6 hex digits (RRGGBB)",
622            hex
623        )
624        .into());
625    }
626
627    let r = u8::from_str_radix(&hex[0..2], 16)
628        .map_err(|_| format!("Invalid red component in color: {}", hex))?;
629    let g = u8::from_str_radix(&hex[2..4], 16)
630        .map_err(|_| format!("Invalid green component in color: {}", hex))?;
631    let b = u8::from_str_radix(&hex[4..6], 16)
632        .map_err(|_| format!("Invalid blue component in color: {}", hex))?;
633
634    // Win32 COLORREF is 0x00BBGGRR
635    Ok((b as i32) << 16 | (g as i32) << 8 | (r as i32))
636}
637
638/// Parse electrical type string to PinElectricalType.
639fn parse_electrical_type(s: &str) -> Result<PinElectricalType, Box<dyn std::error::Error>> {
640    match s.to_lowercase().as_str() {
641        "input" | "in" => Ok(PinElectricalType::Input),
642        "output" | "out" => Ok(PinElectricalType::Output),
643        "io" | "inputoutput" | "bidirectional" | "bidir" => Ok(PinElectricalType::InputOutput),
644        "passive" | "pas" => Ok(PinElectricalType::Passive),
645        "power" | "pwr" => Ok(PinElectricalType::Power),
646        "oc" | "opencollector" => Ok(PinElectricalType::OpenCollector),
647        "oe" | "openemitter" => Ok(PinElectricalType::OpenEmitter),
648        "hiz" | "tristate" | "3state" => Ok(PinElectricalType::HiZ),
649        _ => Err(format!(
650            "Unknown electrical type: {}. Use: input, output, io, passive, power, oc, oe, hiz",
651            s
652        )
653        .into()),
654    }
655}
656
657/// Parse pin orientation to conglomerate flags.
658fn parse_pin_orientation(s: &str) -> Result<PinConglomerateFlags, Box<dyn std::error::Error>> {
659    match s.to_lowercase().as_str() {
660        "right" => Ok(PinConglomerateFlags::empty()), // Pin pointing right (default)
661        "left" => Ok(PinConglomerateFlags::FLIPPED),  // Pin pointing left
662        "up" => Ok(PinConglomerateFlags::ROTATED),    // Pin pointing up
663        "down" => Ok(PinConglomerateFlags::ROTATED | PinConglomerateFlags::FLIPPED), // Pin pointing down
664        _ => Err(format!("Unknown orientation: {}. Use: left, right, up, down", s).into()),
665    }
666}
667
668/// Convert mils to raw coordinate value.
669fn mils_to_raw(mils: i32) -> i32 {
670    mils * 10000
671}
672
673/// Convert mils (f64) to raw coordinate value.
674fn mils_f64_to_raw(mils: f64) -> i32 {
675    (mils * 10000.0).round() as i32
676}
677
678// ═══════════════════════════════════════════════════════════════════════════
679// UNIT PARSING HELPERS
680// ═══════════════════════════════════════════════════════════════════════════
681
682/// Parse a value with unit suffix and return mils (e.g., "100mil", "2.54mm", "0.1in").
683/// Returns mils as f64 for coordinate values.
684#[allow(dead_code)] // Reserved for future unit parsing scenarios
685fn parse_unit_value_to_mils(s: &str) -> Result<f64, Box<dyn std::error::Error>> {
686    let (coord, _unit) =
687        Unit::parse_with_unit(s).map_err(|e| format!("Invalid value '{}': {:?}", s, e))?;
688    Ok(coord.to_mils())
689}
690
691/// Parse a value with optional unit suffix, defaulting to mils for plain numbers.
692/// Handles: "100mil", "2.54mm", "0.1in", "100" (interpreted as mils)
693fn parse_unit_value_or_mil(s: &str) -> Result<f64, Box<dyn std::error::Error>> {
694    let s = s.trim();
695
696    // Try parsing with unit suffix first
697    if let Ok((coord, _unit)) = Unit::parse_with_unit(s) {
698        return Ok(coord.to_mils());
699    }
700
701    // If no unit suffix, try as plain number (interpreted as mils)
702    s.parse::<f64>().map_err(|_| {
703        format!(
704            "Invalid value '{}': expected number with optional unit (e.g., '100mil', '2.54mm')",
705            s
706        )
707        .into()
708    })
709}
710
711// ═══════════════════════════════════════════════════════════════════════════
712// JSON COORDINATE VALUE TYPE (supports both numbers and strings with units)
713// ═══════════════════════════════════════════════════════════════════════════
714
715/// A coordinate value that can be deserialized from either a number (mils) or a string with units.
716/// Examples: 100, "100", "100mil", "2.54mm", "0.1in"
717#[derive(Debug, Clone)]
718pub struct CoordValue(pub f64);
719
720impl CoordValue {
721    /// Get the value in mils.
722    pub fn to_mils(&self) -> f64 {
723        self.0
724    }
725
726    /// Convert to raw internal coordinate value.
727    pub fn to_raw(&self) -> i32 {
728        mils_f64_to_raw(self.0)
729    }
730}
731
732impl Default for CoordValue {
733    fn default() -> Self {
734        CoordValue(0.0)
735    }
736}
737
738impl<'de> Deserialize<'de> for CoordValue {
739    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
740    where
741        D: serde::Deserializer<'de>,
742    {
743        use serde::de::{self, Visitor};
744
745        struct CoordValueVisitor;
746
747        impl<'de> Visitor<'de> for CoordValueVisitor {
748            type Value = CoordValue;
749
750            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
751                formatter.write_str(
752                    "a number (mils) or a string with unit (e.g., \"100mil\", \"2.54mm\")",
753                )
754            }
755
756            fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
757            where
758                E: de::Error,
759            {
760                Ok(CoordValue(value as f64))
761            }
762
763            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
764            where
765                E: de::Error,
766            {
767                Ok(CoordValue(value as f64))
768            }
769
770            fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
771            where
772                E: de::Error,
773            {
774                Ok(CoordValue(value))
775            }
776
777            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
778            where
779                E: de::Error,
780            {
781                parse_unit_value_or_mil(value)
782                    .map(CoordValue)
783                    .map_err(de::Error::custom)
784            }
785        }
786
787        deserializer.deserialize_any(CoordValueVisitor)
788    }
789}
790
791impl Serialize for CoordValue {
792    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
793    where
794        S: serde::Serializer,
795    {
796        // Serialize as a number (mils) for backward compatibility
797        serializer.serialize_f64(self.0)
798    }
799}
800
801/// Add a new component to the library.
802pub fn cmd_add_component(
803    path: &Path,
804    name: &str,
805    description: Option<String>,
806) -> Result<String, Box<dyn std::error::Error>> {
807    let mut lib = open_or_create_schlib(path)?;
808
809    // Check if component already exists
810    if lib
811        .components
812        .iter()
813        .any(|c| c.component.lib_reference == name)
814    {
815        return Err(format!("Component '{}' already exists", name).into());
816    }
817
818    // Create the component record
819    let component = SchComponent {
820        lib_reference: name.to_string(),
821        component_description: description.unwrap_or_default(),
822        part_count: 1,
823        display_mode_count: 1,
824        current_part_id: 1,
825        ..Default::default()
826    };
827
828    // Create SchLibComponent with the component record as first primitive
829    let lib_component = SchLibComponent {
830        component: component.clone(),
831        primitives: vec![SchRecord::Component(component)],
832    };
833
834    lib.components.push(lib_component);
835    save_schlib(path, &lib)?;
836
837    Ok(format!("Added component '{}' to {}", name, path.display()))
838}
839
840/// Add a pin to a component.
841#[allow(clippy::too_many_arguments)]
842pub fn cmd_add_pin(
843    path: &Path,
844    component_name: &str,
845    designator: &str,
846    name: &str,
847    x: &str,
848    y: &str,
849    length: &str,
850    electrical: &str,
851    orientation: &str,
852    hidden: bool,
853) -> Result<String, Box<dyn std::error::Error>> {
854    let mut lib = open_schlib(path)?;
855
856    // Parse coordinate values with units
857    let x_mils = parse_unit_value_or_mil(x)?;
858    let y_mils = parse_unit_value_or_mil(y)?;
859    let length_mils = parse_unit_value_or_mil(length)?;
860
861    // Find the component
862    let component = lib
863        .components
864        .iter_mut()
865        .find(|c| c.component.lib_reference == component_name)
866        .ok_or_else(|| format!("Component '{}' not found", component_name))?;
867
868    // Parse electrical type and orientation
869    let electrical_type = parse_electrical_type(electrical)?;
870    let mut conglomerate = parse_pin_orientation(orientation)?;
871
872    // Set visibility flags
873    conglomerate |= PinConglomerateFlags::DISPLAY_NAME_VISIBLE;
874    conglomerate |= PinConglomerateFlags::DESIGNATOR_VISIBLE;
875
876    if hidden {
877        conglomerate |= PinConglomerateFlags::HIDE;
878    }
879
880    // Create the pin
881    let mut graphical = SchGraphicalBase::default();
882    graphical.base.owner_part_id = Some(1);
883    graphical.location_x = mils_f64_to_raw(x_mils);
884    graphical.location_y = mils_f64_to_raw(y_mils);
885    graphical.color = 0x000080; // Dark blue default
886
887    let pin = SchPin {
888        graphical,
889        designator: designator.to_string(),
890        name: name.to_string(),
891        electrical: electrical_type,
892        pin_conglomerate: conglomerate,
893        pin_length: mils_f64_to_raw(length_mils),
894        symbol_inner_edge: PinSymbol::None,
895        symbol_outer_edge: PinSymbol::None,
896        symbol_inside: PinSymbol::None,
897        symbol_outside: PinSymbol::None,
898        ..Default::default()
899    };
900
901    component.primitives.push(SchRecord::Pin(pin));
902    save_schlib(path, &lib)?;
903
904    Ok(format!(
905        "Added pin '{}' ({}) to component '{}'",
906        designator, name, component_name
907    ))
908}
909
910/// Add a rectangle to a component.
911#[allow(clippy::too_many_arguments)]
912pub fn cmd_add_rectangle(
913    path: &Path,
914    component_name: &str,
915    x1: &str,
916    y1: &str,
917    x2: &str,
918    y2: &str,
919    filled: bool,
920    fill_color: &str,
921    border_color: &str,
922) -> Result<String, Box<dyn std::error::Error>> {
923    let mut lib = open_schlib(path)?;
924
925    // Parse coordinate values with units
926    let x1_mils = parse_unit_value_or_mil(x1)?;
927    let y1_mils = parse_unit_value_or_mil(y1)?;
928    let x2_mils = parse_unit_value_or_mil(x2)?;
929    let y2_mils = parse_unit_value_or_mil(y2)?;
930
931    // Find the component
932    let component = lib
933        .components
934        .iter_mut()
935        .find(|c| c.component.lib_reference == component_name)
936        .ok_or_else(|| format!("Component '{}' not found", component_name))?;
937
938    // Parse colors
939    let fill_color_val = parse_color(fill_color)?;
940    let border_color_val = parse_color(border_color)?;
941
942    // Create the rectangle
943    let mut graphical = SchGraphicalBase::default();
944    graphical.base.owner_part_id = Some(1);
945    graphical.location_x = mils_f64_to_raw(x1_mils);
946    graphical.location_y = mils_f64_to_raw(y1_mils);
947    graphical.color = border_color_val;
948    graphical.area_color = fill_color_val;
949
950    let rect = SchRectangle {
951        graphical,
952        corner_x: mils_f64_to_raw(x2_mils),
953        corner_y: mils_f64_to_raw(y2_mils),
954        line_width: LineWidth::Small,
955        is_solid: filled,
956        transparent: !filled,
957        ..Default::default()
958    };
959
960    component.primitives.push(SchRecord::Rectangle(rect));
961    save_schlib(path, &lib)?;
962
963    Ok(format!("Added rectangle to component '{}'", component_name))
964}
965
966/// Add a line to a component.
967pub fn cmd_add_line(
968    path: &Path,
969    component_name: &str,
970    x1: &str,
971    y1: &str,
972    x2: &str,
973    y2: &str,
974    color: &str,
975) -> Result<String, Box<dyn std::error::Error>> {
976    let mut lib = open_schlib(path)?;
977
978    // Parse coordinate values with units
979    let x1_mils = parse_unit_value_or_mil(x1)?;
980    let y1_mils = parse_unit_value_or_mil(y1)?;
981    let x2_mils = parse_unit_value_or_mil(x2)?;
982    let y2_mils = parse_unit_value_or_mil(y2)?;
983
984    // Find the component
985    let component = lib
986        .components
987        .iter_mut()
988        .find(|c| c.component.lib_reference == component_name)
989        .ok_or_else(|| format!("Component '{}' not found", component_name))?;
990
991    // Parse color
992    let color_val = parse_color(color)?;
993
994    // Create the line
995    let mut graphical = SchGraphicalBase::default();
996    graphical.base.owner_part_id = Some(1);
997    graphical.location_x = mils_f64_to_raw(x1_mils);
998    graphical.location_y = mils_f64_to_raw(y1_mils);
999    graphical.color = color_val;
1000
1001    let line = SchLine {
1002        graphical,
1003        corner_x: mils_f64_to_raw(x2_mils),
1004        corner_y: mils_f64_to_raw(y2_mils),
1005        line_width: LineWidth::Small,
1006        ..Default::default()
1007    };
1008
1009    component.primitives.push(SchRecord::Line(line));
1010    save_schlib(path, &lib)?;
1011
1012    Ok(format!("Added line to component '{}'", component_name))
1013}
1014
1015/// Add a polygon to a component.
1016pub fn cmd_add_polygon(
1017    path: &Path,
1018    component_name: &str,
1019    vertices_str: &str,
1020    filled: bool,
1021    fill_color: &str,
1022    border_color: &str,
1023) -> Result<String, Box<dyn std::error::Error>> {
1024    let mut lib = open_schlib(path)?;
1025
1026    // Find the component
1027    let component = lib
1028        .components
1029        .iter_mut()
1030        .find(|c| c.component.lib_reference == component_name)
1031        .ok_or_else(|| format!("Component '{}' not found", component_name))?;
1032
1033    // Parse vertices with unit support
1034    let values: Vec<f64> = vertices_str
1035        .split(',')
1036        .map(|s| parse_unit_value_or_mil(s))
1037        .collect::<Result<Vec<_>, _>>()?;
1038
1039    if values.len() < 6 || values.len() % 2 != 0 {
1040        return Err("Need at least 3 vertex pairs (6 values)".into());
1041    }
1042
1043    let vertices: Vec<(i32, i32)> = values
1044        .chunks(2)
1045        .map(|chunk| (mils_f64_to_raw(chunk[0]), mils_f64_to_raw(chunk[1])))
1046        .collect();
1047
1048    // Parse colors
1049    let fill_color_val = parse_color(fill_color)?;
1050    let border_color_val = parse_color(border_color)?;
1051
1052    // Create the polygon
1053    let mut graphical = SchGraphicalBase::default();
1054    graphical.base.owner_part_id = Some(1);
1055    graphical.location_x = vertices[0].0;
1056    graphical.location_y = vertices[0].1;
1057    graphical.color = border_color_val;
1058    graphical.area_color = fill_color_val;
1059
1060    let polygon = SchPolygon {
1061        graphical,
1062        vertices,
1063        line_width: LineWidth::Small,
1064        is_solid: filled,
1065        transparent: !filled,
1066        ..Default::default()
1067    };
1068
1069    component.primitives.push(SchRecord::Polygon(polygon));
1070    save_schlib(path, &lib)?;
1071
1072    Ok(format!(
1073        "Added polygon with {} vertices to component '{}'",
1074        values.len() / 2,
1075        component_name
1076    ))
1077}
1078
1079/// Pin definition for gen-ic command.
1080struct PinDef {
1081    designator: String,
1082    name: String,
1083    electrical: PinElectricalType,
1084    side: String,
1085}
1086
1087/// Parse pin definitions from string.
1088fn parse_pin_defs(pins_str: &str) -> Result<Vec<PinDef>, Box<dyn std::error::Error>> {
1089    let mut pins = Vec::new();
1090
1091    for pin_spec in pins_str.split(',') {
1092        let parts: Vec<&str> = pin_spec.trim().split(':').collect();
1093        if parts.len() < 3 {
1094            return Err(format!(
1095                "Invalid pin spec '{}'. Format: designator:name:type[:side]",
1096                pin_spec
1097            )
1098            .into());
1099        }
1100
1101        let electrical = parse_electrical_type(parts[2])?;
1102        let side = if parts.len() > 3 {
1103            parts[3].to_lowercase()
1104        } else {
1105            "left".to_string()
1106        };
1107
1108        pins.push(PinDef {
1109            designator: parts[0].to_string(),
1110            name: parts[1].to_string(),
1111            electrical,
1112            side,
1113        });
1114    }
1115
1116    Ok(pins)
1117}
1118
1119/// Generate a standard IC symbol.
1120pub fn cmd_gen_ic(
1121    path: &Path,
1122    name: &str,
1123    pins_str: &str,
1124    description: Option<String>,
1125    width: &str,
1126    pin_length: &str,
1127    pin_spacing: &str,
1128) -> Result<String, Box<dyn std::error::Error>> {
1129    let mut lib = open_or_create_schlib(path)?;
1130
1131    // Parse dimension values with units
1132    let width_mils = parse_unit_value_or_mil(width)?;
1133    let pin_length_mils = parse_unit_value_or_mil(pin_length)?;
1134    let pin_spacing_mils = parse_unit_value_or_mil(pin_spacing)?;
1135
1136    // Check if component already exists
1137    if lib
1138        .components
1139        .iter()
1140        .any(|c| c.component.lib_reference == name)
1141    {
1142        return Err(format!("Component '{}' already exists", name).into());
1143    }
1144
1145    // Parse pin definitions
1146    let pin_defs = parse_pin_defs(pins_str)?;
1147
1148    // Separate pins by side
1149    let left_pins: Vec<_> = pin_defs.iter().filter(|p| p.side == "left").collect();
1150    let right_pins: Vec<_> = pin_defs.iter().filter(|p| p.side == "right").collect();
1151    let top_pins: Vec<_> = pin_defs.iter().filter(|p| p.side == "top").collect();
1152    let bottom_pins: Vec<_> = pin_defs.iter().filter(|p| p.side == "bottom").collect();
1153
1154    // Calculate body dimensions based on pin count
1155    let left_count = left_pins.len();
1156    let right_count = right_pins.len();
1157    let top_count = top_pins.len();
1158    let bottom_count = bottom_pins.len();
1159    let max_vertical_pins = left_count.max(right_count);
1160    let max_horizontal_pins = top_count.max(bottom_count);
1161    let body_height_mils = (max_vertical_pins + 1) as f64 * pin_spacing_mils;
1162    // Widen body if top/bottom pins need more space
1163    let min_width_for_tb = if max_horizontal_pins > 0 {
1164        (max_horizontal_pins + 1) as f64 * pin_spacing_mils
1165    } else {
1166        0.0
1167    };
1168    let width_mils = width_mils.max(min_width_for_tb);
1169
1170    // Create component
1171    let component = SchComponent {
1172        lib_reference: name.to_string(),
1173        component_description: description.unwrap_or_default(),
1174        part_count: 1,
1175        display_mode_count: 1,
1176        current_part_id: 1,
1177        ..Default::default()
1178    };
1179
1180    let mut primitives = vec![SchRecord::Component(component.clone())];
1181
1182    // Add body rectangle
1183    let mut rect_graphical = SchGraphicalBase::default();
1184    rect_graphical.base.owner_part_id = Some(1);
1185    rect_graphical.location_x = mils_to_raw(0);
1186    rect_graphical.location_y = mils_to_raw(0);
1187    rect_graphical.color = parse_color("800000")?; // Dark red border
1188    rect_graphical.area_color = parse_color("FFFFB0")?; // Light yellow fill
1189
1190    let rect = SchRectangle {
1191        graphical: rect_graphical,
1192        corner_x: mils_f64_to_raw(width_mils),
1193        corner_y: mils_f64_to_raw(body_height_mils),
1194        line_width: LineWidth::Small,
1195        is_solid: true,
1196        transparent: false,
1197        ..Default::default()
1198    };
1199    primitives.push(SchRecord::Rectangle(rect));
1200
1201    // Add left pins (pointing right into body)
1202    for (i, pin_def) in left_pins.iter().enumerate() {
1203        let y_mils = body_height_mils - (i + 1) as f64 * pin_spacing_mils;
1204
1205        let mut graphical = SchGraphicalBase::default();
1206        graphical.base.owner_part_id = Some(1);
1207        graphical.location_x = mils_f64_to_raw(-pin_length_mils);
1208        graphical.location_y = mils_f64_to_raw(y_mils);
1209        graphical.color = 0x000080;
1210
1211        let pin = SchPin {
1212            graphical,
1213            designator: pin_def.designator.clone(),
1214            name: pin_def.name.clone(),
1215            electrical: pin_def.electrical,
1216            pin_conglomerate: PinConglomerateFlags::DISPLAY_NAME_VISIBLE
1217                | PinConglomerateFlags::DESIGNATOR_VISIBLE,
1218            pin_length: mils_f64_to_raw(pin_length_mils),
1219            ..Default::default()
1220        };
1221        primitives.push(SchRecord::Pin(pin));
1222    }
1223
1224    // Add right pins (pointing left into body)
1225    for (i, pin_def) in right_pins.iter().enumerate() {
1226        let y_mils = body_height_mils - (i + 1) as f64 * pin_spacing_mils;
1227
1228        let mut graphical = SchGraphicalBase::default();
1229        graphical.base.owner_part_id = Some(1);
1230        graphical.location_x = mils_f64_to_raw(width_mils + pin_length_mils);
1231        graphical.location_y = mils_f64_to_raw(y_mils);
1232        graphical.color = 0x000080;
1233
1234        let pin = SchPin {
1235            graphical,
1236            designator: pin_def.designator.clone(),
1237            name: pin_def.name.clone(),
1238            electrical: pin_def.electrical,
1239            pin_conglomerate: PinConglomerateFlags::DISPLAY_NAME_VISIBLE
1240                | PinConglomerateFlags::DESIGNATOR_VISIBLE
1241                | PinConglomerateFlags::FLIPPED,
1242            pin_length: mils_f64_to_raw(pin_length_mils),
1243            ..Default::default()
1244        };
1245        primitives.push(SchRecord::Pin(pin));
1246    }
1247
1248    // Add top pins (pointing down into body)
1249    for (i, pin_def) in top_pins.iter().enumerate() {
1250        let x_mils = (i + 1) as f64 * pin_spacing_mils;
1251
1252        let mut graphical = SchGraphicalBase::default();
1253        graphical.base.owner_part_id = Some(1);
1254        graphical.location_x = mils_f64_to_raw(x_mils);
1255        graphical.location_y = mils_f64_to_raw(body_height_mils + pin_length_mils);
1256        graphical.color = 0x000080;
1257
1258        let pin = SchPin {
1259            graphical,
1260            designator: pin_def.designator.clone(),
1261            name: pin_def.name.clone(),
1262            electrical: pin_def.electrical,
1263            pin_conglomerate: PinConglomerateFlags::DISPLAY_NAME_VISIBLE
1264                | PinConglomerateFlags::DESIGNATOR_VISIBLE
1265                | PinConglomerateFlags::ROTATED,
1266            pin_length: mils_f64_to_raw(pin_length_mils),
1267            ..Default::default()
1268        };
1269        primitives.push(SchRecord::Pin(pin));
1270    }
1271
1272    // Add bottom pins (pointing up into body)
1273    for (i, pin_def) in bottom_pins.iter().enumerate() {
1274        let x_mils = (i + 1) as f64 * pin_spacing_mils;
1275
1276        let mut graphical = SchGraphicalBase::default();
1277        graphical.base.owner_part_id = Some(1);
1278        graphical.location_x = mils_f64_to_raw(x_mils);
1279        graphical.location_y = mils_f64_to_raw(-pin_length_mils);
1280        graphical.color = 0x000080;
1281
1282        let pin = SchPin {
1283            graphical,
1284            designator: pin_def.designator.clone(),
1285            name: pin_def.name.clone(),
1286            electrical: pin_def.electrical,
1287            pin_conglomerate: PinConglomerateFlags::DISPLAY_NAME_VISIBLE
1288                | PinConglomerateFlags::DESIGNATOR_VISIBLE
1289                | PinConglomerateFlags::ROTATED
1290                | PinConglomerateFlags::FLIPPED,
1291            pin_length: mils_f64_to_raw(pin_length_mils),
1292            ..Default::default()
1293        };
1294        primitives.push(SchRecord::Pin(pin));
1295    }
1296
1297    let lib_component = SchLibComponent {
1298        component,
1299        primitives,
1300    };
1301
1302    lib.components.push(lib_component);
1303    save_schlib(path, &lib)?;
1304
1305    Ok(format!(
1306        "Generated IC symbol '{}' with {} pins ({} left, {} right, {} top, {} bottom)",
1307        name,
1308        pin_defs.len(),
1309        left_count,
1310        right_count,
1311        top_count,
1312        bottom_count,
1313    ))
1314}
1315
1316/// Render a component symbol as ASCII art.
1317pub fn cmd_render_ascii(
1318    path: &Path,
1319    component_name: &str,
1320    max_width: usize,
1321    max_height: usize,
1322) -> Result<String, Box<dyn std::error::Error>> {
1323    let lib = open_schlib(path)?;
1324
1325    let name_lower = component_name.to_lowercase();
1326    let component = lib
1327        .components
1328        .iter()
1329        .find(|c| c.component.lib_reference.to_lowercase() == name_lower)
1330        .ok_or_else(|| format!("Component '{}' not found", component_name))?;
1331
1332    // Find bounds
1333    let mut min_x = i32::MAX;
1334    let mut min_y = i32::MAX;
1335    let mut max_x = i32::MIN;
1336    let mut max_y = i32::MIN;
1337
1338    for prim in &component.primitives {
1339        match prim {
1340            SchRecord::Pin(p) => {
1341                let (cx, cy) = p.get_corner();
1342                min_x = min_x.min(p.graphical.location_x).min(cx);
1343                min_y = min_y.min(p.graphical.location_y).min(cy);
1344                max_x = max_x.max(p.graphical.location_x).max(cx);
1345                max_y = max_y.max(p.graphical.location_y).max(cy);
1346            }
1347            SchRecord::Rectangle(r) => {
1348                min_x = min_x.min(r.graphical.location_x).min(r.corner_x);
1349                min_y = min_y.min(r.graphical.location_y).min(r.corner_y);
1350                max_x = max_x.max(r.graphical.location_x).max(r.corner_x);
1351                max_y = max_y.max(r.graphical.location_y).max(r.corner_y);
1352            }
1353            SchRecord::Line(l) => {
1354                min_x = min_x.min(l.graphical.location_x).min(l.corner_x);
1355                min_y = min_y.min(l.graphical.location_y).min(l.corner_y);
1356                max_x = max_x.max(l.graphical.location_x).max(l.corner_x);
1357                max_y = max_y.max(l.graphical.location_y).max(l.corner_y);
1358            }
1359            _ => {}
1360        }
1361    }
1362
1363    if min_x == i32::MAX {
1364        return Ok("No renderable primitives found.".to_string());
1365    }
1366
1367    let width_raw = (max_x - min_x) as f64;
1368    let height_raw = (max_y - min_y) as f64;
1369
1370    // Scale to fit
1371    let scale_x = (max_width as f64 - 2.0) / width_raw;
1372    let scale_y = (max_height as f64 - 2.0) / height_raw;
1373    let scale = scale_x.min(scale_y);
1374
1375    let canvas_width = ((width_raw * scale) as usize + 2).min(max_width);
1376    let canvas_height = ((height_raw * scale) as usize + 2).min(max_height);
1377
1378    // Create canvas
1379    let mut canvas: Vec<Vec<char>> = vec![vec![' '; canvas_width]; canvas_height];
1380
1381    // Helper to convert coords
1382    let to_canvas = |x: i32, y: i32| -> (usize, usize) {
1383        let cx = ((x - min_x) as f64 * scale) as usize;
1384        let cy = canvas_height - 1 - (((y - min_y) as f64 * scale) as usize);
1385        (cx.min(canvas_width - 1), cy.min(canvas_height - 1))
1386    };
1387
1388    // Draw rectangles
1389    for prim in &component.primitives {
1390        if let SchRecord::Rectangle(r) = prim {
1391            let (x1, y1) = to_canvas(r.graphical.location_x, r.graphical.location_y);
1392            let (x2, y2) = to_canvas(r.corner_x, r.corner_y);
1393            let (x1, x2) = (x1.min(x2), x1.max(x2));
1394            let (y1, y2) = (y1.min(y2), y1.max(y2));
1395
1396            // Draw rectangle border
1397            for x in x1..=x2 {
1398                if y1 < canvas_height {
1399                    canvas[y1][x.min(canvas_width - 1)] = '-';
1400                }
1401                if y2 < canvas_height {
1402                    canvas[y2][x.min(canvas_width - 1)] = '-';
1403                }
1404            }
1405            for y in y1..=y2 {
1406                if x1 < canvas_width {
1407                    canvas[y.min(canvas_height - 1)][x1] = '|';
1408                }
1409                if x2 < canvas_width {
1410                    canvas[y.min(canvas_height - 1)][x2] = '|';
1411                }
1412            }
1413            // Corners
1414            if y1 < canvas_height && x1 < canvas_width {
1415                canvas[y1][x1] = '+';
1416            }
1417            if y1 < canvas_height && x2 < canvas_width {
1418                canvas[y1][x2] = '+';
1419            }
1420            if y2 < canvas_height && x1 < canvas_width {
1421                canvas[y2][x1] = '+';
1422            }
1423            if y2 < canvas_height && x2 < canvas_width {
1424                canvas[y2][x2] = '+';
1425            }
1426        }
1427    }
1428
1429    // Draw pins
1430    for prim in &component.primitives {
1431        if let SchRecord::Pin(p) = prim {
1432            let (px, py) = to_canvas(p.graphical.location_x, p.graphical.location_y);
1433            let (cx, cy) = p.get_corner();
1434            let (ex, ey) = to_canvas(cx, cy);
1435
1436            // Draw pin line
1437            if px == ex {
1438                let y_start = py.min(ey);
1439                let y_end = py.max(ey);
1440                for row in canvas.iter_mut().take(y_end + 1).skip(y_start) {
1441                    if let Some(cell) = row.get_mut(px) {
1442                        *cell = '|';
1443                    }
1444                }
1445            } else if let Some(row) = canvas.get_mut(py) {
1446                let x_start = px.min(ex);
1447                let x_end = px.max(ex);
1448                for cell in row.iter_mut().take(x_end + 1).skip(x_start) {
1449                    *cell = '-';
1450                }
1451            }
1452
1453            // Draw pin endpoint marker
1454            if py < canvas_height && px < canvas_width {
1455                canvas[py][px] = 'o';
1456            }
1457        }
1458    }
1459
1460    // Build output string
1461    let mut output = String::new();
1462    output.push_str(&format!("\n{}\n", component.component.lib_reference));
1463    output.push_str(&format!(
1464        "{}\n",
1465        "=".repeat(component.component.lib_reference.len())
1466    ));
1467    for row in &canvas {
1468        output.push_str(&format!("{}\n", row.iter().collect::<String>()));
1469    }
1470
1471    // Add pin list
1472    output.push_str("\nPins:\n");
1473    let mut pins: Vec<&SchPin> = component
1474        .primitives
1475        .iter()
1476        .filter_map(|p| {
1477            if let SchRecord::Pin(pin) = p {
1478                Some(pin)
1479            } else {
1480                None
1481            }
1482        })
1483        .collect();
1484    pins.sort_by(|a, b| alphanumeric_sort(&a.designator, &b.designator));
1485
1486    for pin in pins {
1487        output.push_str(&format!(
1488            "  {} - {} ({})\n",
1489            pin.designator,
1490            pin.name,
1491            electrical_type_name(&pin.electrical)
1492        ));
1493    }
1494
1495    Ok(output)
1496}
1497
1498// ═══════════════════════════════════════════════════════════════════════════
1499// JSON INPUT STRUCTURES (for LLM tool calling and structured output)
1500// ═══════════════════════════════════════════════════════════════════════════
1501
1502/// JSON schema for a pin in a schematic component.
1503/// Coordinates accept numbers (mils) or strings with units (e.g., "100mil", "2.54mm", "0.1in").
1504#[derive(Debug, Clone, Deserialize, Serialize)]
1505pub struct SchPinJson {
1506    /// Pin designator (e.g., "1", "2", "A1")
1507    pub designator: String,
1508    /// Pin name (e.g., "VCC", "GND", "DATA0")
1509    pub name: String,
1510    /// X position: number (mils) or string with unit (e.g., "100mil", "2.54mm")
1511    pub x: CoordValue,
1512    /// Y position: number (mils) or string with unit (e.g., "100mil", "2.54mm")
1513    pub y: CoordValue,
1514    /// Pin length: number (mils) or string with unit (default: 200mil)
1515    #[serde(default = "default_pin_length")]
1516    pub length: CoordValue,
1517    /// Electrical type: "input", "output", "io", "passive", "power", "oc", "oe", "hiz"
1518    #[serde(default = "default_electrical")]
1519    pub electrical: String,
1520    /// Pin orientation: "right", "left", "up", "down"
1521    #[serde(default = "default_orientation")]
1522    pub orientation: String,
1523    /// Hide the pin (optional)
1524    #[serde(default)]
1525    pub hidden: bool,
1526    /// Pin description (optional)
1527    #[serde(default)]
1528    pub description: String,
1529}
1530
1531fn default_pin_length() -> CoordValue {
1532    CoordValue(200.0)
1533}
1534
1535fn default_electrical() -> String {
1536    "passive".to_string()
1537}
1538
1539fn default_orientation() -> String {
1540    "right".to_string()
1541}
1542
1543/// JSON schema for a rectangle in a schematic component.
1544/// Coordinates accept numbers (mils) or strings with units (e.g., "100mil", "2.54mm", "0.1in").
1545#[derive(Debug, Clone, Deserialize, Serialize)]
1546pub struct SchRectangleJson {
1547    /// Corner 1 X: number (mils) or string with unit
1548    pub x1: CoordValue,
1549    /// Corner 1 Y: number (mils) or string with unit
1550    pub y1: CoordValue,
1551    /// Corner 2 X: number (mils) or string with unit
1552    pub x2: CoordValue,
1553    /// Corner 2 Y: number (mils) or string with unit
1554    pub y2: CoordValue,
1555    /// Fill the rectangle
1556    #[serde(default)]
1557    pub filled: bool,
1558    /// Fill color in hex (RRGGBB), default light yellow
1559    #[serde(default = "default_fill_color")]
1560    pub fill_color: String,
1561    /// Border color in hex (RRGGBB), default dark blue
1562    #[serde(default = "default_border_color")]
1563    pub border_color: String,
1564}
1565
1566fn default_fill_color() -> String {
1567    "FFFFB0".to_string()
1568}
1569
1570fn default_border_color() -> String {
1571    "000080".to_string()
1572}
1573
1574/// JSON schema for a line in a schematic component.
1575/// Coordinates accept numbers (mils) or strings with units (e.g., "100mil", "2.54mm", "0.1in").
1576#[derive(Debug, Clone, Deserialize, Serialize)]
1577pub struct SchLineJson {
1578    /// Start X: number (mils) or string with unit
1579    pub x1: CoordValue,
1580    /// Start Y: number (mils) or string with unit
1581    pub y1: CoordValue,
1582    /// End X: number (mils) or string with unit
1583    pub x2: CoordValue,
1584    /// End Y: number (mils) or string with unit
1585    pub y2: CoordValue,
1586    /// Line color in hex (RRGGBB)
1587    #[serde(default = "default_border_color")]
1588    pub color: String,
1589}
1590
1591/// JSON schema for a polygon in a schematic component.
1592/// Vertices accept numbers (mils) or strings with units.
1593#[derive(Debug, Clone, Deserialize, Serialize)]
1594pub struct SchPolygonJson {
1595    /// Vertices as array of [x, y] pairs: numbers (mils) or strings with units
1596    pub vertices: Vec<[CoordValue; 2]>,
1597    /// Fill the polygon
1598    #[serde(default)]
1599    pub filled: bool,
1600    /// Fill color in hex (RRGGBB)
1601    #[serde(default = "default_fill_color")]
1602    pub fill_color: String,
1603    /// Border color in hex (RRGGBB)
1604    #[serde(default = "default_border_color")]
1605    pub border_color: String,
1606}
1607
1608/// JSON schema for a text label in a schematic component.
1609/// Coordinates accept numbers (mils) or strings with units (e.g., "100mil", "2.54mm", "0.1in").
1610#[derive(Debug, Clone, Deserialize, Serialize)]
1611pub struct SchLabelJson {
1612    /// X position: number (mils) or string with unit
1613    pub x: CoordValue,
1614    /// Y position: number (mils) or string with unit
1615    pub y: CoordValue,
1616    /// Label text
1617    pub text: String,
1618    /// Text orientation: "horizontal", "vertical_up", "vertical_down", "90", "180", "270"
1619    #[serde(default = "default_label_orientation")]
1620    pub orientation: String,
1621    /// Text justification: "bottom_left", "bottom_center", "bottom_right",
1622    /// "center_left", "center", "center_right", "top_left", "top_center", "top_right"
1623    #[serde(default = "default_justification")]
1624    pub justification: String,
1625    /// Text color in hex (RRGGBB)
1626    #[serde(default = "default_border_color")]
1627    pub color: String,
1628    /// Font ID (optional, default 1)
1629    #[serde(default = "default_font_id")]
1630    pub font_id: i32,
1631    /// Hide the label
1632    #[serde(default)]
1633    pub hidden: bool,
1634}
1635
1636fn default_label_orientation() -> String {
1637    "horizontal".to_string()
1638}
1639
1640fn default_justification() -> String {
1641    "bottom_left".to_string()
1642}
1643
1644fn default_font_id() -> i32 {
1645    1
1646}
1647
1648/// JSON schema for an arc in a schematic component.
1649/// Coordinates accept numbers (mils) or strings with units (e.g., "100mil", "2.54mm", "0.1in").
1650#[derive(Debug, Clone, Deserialize, Serialize)]
1651pub struct SchArcJson {
1652    /// Center X: number (mils) or string with unit
1653    pub x: CoordValue,
1654    /// Center Y: number (mils) or string with unit
1655    pub y: CoordValue,
1656    /// Radius: number (mils) or string with unit
1657    pub radius: CoordValue,
1658    /// Start angle in degrees (0 = right, 90 = up)
1659    #[serde(default)]
1660    pub start_angle: f64,
1661    /// End angle in degrees
1662    #[serde(default = "default_end_angle")]
1663    pub end_angle: f64,
1664    /// Line color in hex (RRGGBB)
1665    #[serde(default = "default_border_color")]
1666    pub color: String,
1667}
1668
1669fn default_end_angle() -> f64 {
1670    360.0
1671}
1672
1673/// JSON schema for a polyline in a schematic component.
1674/// Vertices accept numbers (mils) or strings with units.
1675#[derive(Debug, Clone, Deserialize, Serialize)]
1676pub struct SchPolylineJson {
1677    /// Vertices as array of [x, y] pairs: numbers (mils) or strings with units
1678    pub vertices: Vec<[CoordValue; 2]>,
1679    /// Line color in hex (RRGGBB)
1680    #[serde(default = "default_border_color")]
1681    pub color: String,
1682}
1683
1684/// JSON schema for an ellipse in a schematic component.
1685/// Coordinates accept numbers (mils) or strings with units (e.g., "100mil", "2.54mm", "0.1in").
1686#[derive(Debug, Clone, Deserialize, Serialize)]
1687pub struct SchEllipseJson {
1688    /// Center X: number (mils) or string with unit
1689    pub x: CoordValue,
1690    /// Center Y: number (mils) or string with unit
1691    pub y: CoordValue,
1692    /// X radius: number (mils) or string with unit
1693    pub radius_x: CoordValue,
1694    /// Y radius: number (mils) or string with unit
1695    pub radius_y: CoordValue,
1696    /// Fill the ellipse
1697    #[serde(default)]
1698    pub filled: bool,
1699    /// Fill color in hex (RRGGBB)
1700    #[serde(default = "default_fill_color")]
1701    pub fill_color: String,
1702    /// Border color in hex (RRGGBB)
1703    #[serde(default = "default_border_color")]
1704    pub border_color: String,
1705}
1706
1707/// JSON schema for a complete schematic component definition.
1708/// This is the top-level structure for the add-json command.
1709#[derive(Debug, Clone, Deserialize, Serialize)]
1710pub struct SchComponentJson {
1711    /// Component name (LIBREFERENCE)
1712    pub name: String,
1713    /// Component description (optional)
1714    #[serde(default)]
1715    pub description: String,
1716    /// Number of parts (for multi-part components, default 1)
1717    #[serde(default = "default_part_count")]
1718    pub part_count: i32,
1719    /// List of pins
1720    #[serde(default)]
1721    pub pins: Vec<SchPinJson>,
1722    /// List of rectangles (typically for the body)
1723    #[serde(default)]
1724    pub rectangles: Vec<SchRectangleJson>,
1725    /// List of lines
1726    #[serde(default)]
1727    pub lines: Vec<SchLineJson>,
1728    /// List of polygons
1729    #[serde(default)]
1730    pub polygons: Vec<SchPolygonJson>,
1731    /// List of text labels
1732    #[serde(default)]
1733    pub labels: Vec<SchLabelJson>,
1734    /// List of arcs
1735    #[serde(default)]
1736    pub arcs: Vec<SchArcJson>,
1737    /// List of polylines (multi-segment lines)
1738    #[serde(default)]
1739    pub polylines: Vec<SchPolylineJson>,
1740    /// List of ellipses
1741    #[serde(default)]
1742    pub ellipses: Vec<SchEllipseJson>,
1743}
1744
1745fn default_part_count() -> i32 {
1746    1
1747}
1748
1749/// Add a complete component from JSON input.
1750pub fn cmd_add_json(
1751    path: &Path,
1752    json_file: Option<String>,
1753    json_str: Option<String>,
1754) -> Result<String, Box<dyn std::error::Error>> {
1755    use std::io::{self, Read as IoRead};
1756
1757    // Read JSON from file, stdin, or command line
1758    let json_content = match (json_file, json_str) {
1759        (_, Some(s)) => s,
1760        (Some(ref path), None) if path == "-" => {
1761            let mut buffer = String::new();
1762            io::stdin().read_to_string(&mut buffer)?;
1763            buffer
1764        }
1765        (Some(ref file_path), None) => std::fs::read_to_string(file_path)?,
1766        (None, None) => {
1767            return Err("Must provide either --file <file> or --json <string>".into());
1768        }
1769    };
1770
1771    // Parse JSON
1772    let component_def: SchComponentJson = serde_json::from_str(&json_content)?;
1773
1774    // Open or create library
1775    let mut lib = open_or_create_schlib(path)?;
1776
1777    // Check if component already exists
1778    if lib
1779        .components
1780        .iter()
1781        .any(|c| c.component.lib_reference == component_def.name)
1782    {
1783        return Err(format!("Component '{}' already exists", component_def.name).into());
1784    }
1785
1786    // Create component record
1787    let component = SchComponent {
1788        lib_reference: component_def.name.clone(),
1789        component_description: component_def.description.clone(),
1790        part_count: component_def.part_count,
1791        display_mode_count: 1,
1792        current_part_id: 1,
1793        ..Default::default()
1794    };
1795
1796    // Start with the component record as first primitive
1797    let mut primitives = vec![SchRecord::Component(component.clone())];
1798
1799    // Add rectangles
1800    for rect in &component_def.rectangles {
1801        let fill_color_val = parse_color(&rect.fill_color)?;
1802        let border_color_val = parse_color(&rect.border_color)?;
1803
1804        let mut graphical = SchGraphicalBase::default();
1805        graphical.base.owner_part_id = Some(1);
1806        graphical.location_x = rect.x1.to_raw();
1807        graphical.location_y = rect.y1.to_raw();
1808        graphical.color = border_color_val;
1809        graphical.area_color = fill_color_val;
1810
1811        let sch_rect = SchRectangle {
1812            graphical,
1813            corner_x: rect.x2.to_raw(),
1814            corner_y: rect.y2.to_raw(),
1815            line_width: LineWidth::Small,
1816            is_solid: rect.filled,
1817            transparent: !rect.filled,
1818            ..Default::default()
1819        };
1820        primitives.push(SchRecord::Rectangle(sch_rect));
1821    }
1822
1823    // Add lines
1824    for line in &component_def.lines {
1825        let color_val = parse_color(&line.color)?;
1826
1827        let mut graphical = SchGraphicalBase::default();
1828        graphical.base.owner_part_id = Some(1);
1829        graphical.location_x = line.x1.to_raw();
1830        graphical.location_y = line.y1.to_raw();
1831        graphical.color = color_val;
1832
1833        let sch_line = SchLine {
1834            graphical,
1835            corner_x: line.x2.to_raw(),
1836            corner_y: line.y2.to_raw(),
1837            line_width: LineWidth::Small,
1838            ..Default::default()
1839        };
1840        primitives.push(SchRecord::Line(sch_line));
1841    }
1842
1843    // Add polygons
1844    for polygon in &component_def.polygons {
1845        if polygon.vertices.len() < 3 {
1846            return Err("Polygon must have at least 3 vertices".into());
1847        }
1848
1849        let fill_color_val = parse_color(&polygon.fill_color)?;
1850        let border_color_val = parse_color(&polygon.border_color)?;
1851
1852        let vertices: Vec<(i32, i32)> = polygon
1853            .vertices
1854            .iter()
1855            .map(|v| (v[0].to_raw(), v[1].to_raw()))
1856            .collect();
1857
1858        let mut graphical = SchGraphicalBase::default();
1859        graphical.base.owner_part_id = Some(1);
1860        graphical.location_x = vertices[0].0;
1861        graphical.location_y = vertices[0].1;
1862        graphical.color = border_color_val;
1863        graphical.area_color = fill_color_val;
1864
1865        let sch_polygon = SchPolygon {
1866            graphical,
1867            vertices,
1868            line_width: LineWidth::Small,
1869            is_solid: polygon.filled,
1870            transparent: !polygon.filled,
1871            ..Default::default()
1872        };
1873        primitives.push(SchRecord::Polygon(sch_polygon));
1874    }
1875
1876    // Add labels (text)
1877    for label in &component_def.labels {
1878        let color_val = parse_color(&label.color)?;
1879        let orientation = parse_text_orientation(&label.orientation)?;
1880        let justification = parse_text_justification(&label.justification)?;
1881
1882        let mut graphical = SchGraphicalBase::default();
1883        graphical.base.owner_part_id = Some(1);
1884        graphical.location_x = label.x.to_raw();
1885        graphical.location_y = label.y.to_raw();
1886        graphical.color = color_val;
1887
1888        let sch_label = SchLabel {
1889            graphical,
1890            text: label.text.clone(),
1891            orientation,
1892            justification,
1893            font_id: label.font_id,
1894            is_hidden: label.hidden,
1895            ..Default::default()
1896        };
1897        primitives.push(SchRecord::Label(sch_label));
1898    }
1899
1900    // Add arcs
1901    for arc in &component_def.arcs {
1902        let color_val = parse_color(&arc.color)?;
1903
1904        let mut graphical = SchGraphicalBase::default();
1905        graphical.base.owner_part_id = Some(1);
1906        graphical.location_x = arc.x.to_raw();
1907        graphical.location_y = arc.y.to_raw();
1908        graphical.color = color_val;
1909
1910        let sch_arc = SchArc {
1911            graphical,
1912            radius: arc.radius.to_raw(),
1913            secondary_radius: arc.radius.to_raw(), // Same as radius for circular arcs
1914            start_angle: arc.start_angle,
1915            end_angle: arc.end_angle,
1916            line_width: LineWidth::Small,
1917            ..Default::default()
1918        };
1919        primitives.push(SchRecord::Arc(sch_arc));
1920    }
1921
1922    // Add polylines
1923    for polyline in &component_def.polylines {
1924        if polyline.vertices.len() < 2 {
1925            return Err("Polyline must have at least 2 vertices".into());
1926        }
1927
1928        let color_val = parse_color(&polyline.color)?;
1929
1930        let vertices: Vec<(i32, i32)> = polyline
1931            .vertices
1932            .iter()
1933            .map(|v| (v[0].to_raw(), v[1].to_raw()))
1934            .collect();
1935
1936        let mut graphical = SchGraphicalBase::default();
1937        graphical.base.owner_part_id = Some(1);
1938        graphical.location_x = vertices[0].0;
1939        graphical.location_y = vertices[0].1;
1940        graphical.color = color_val;
1941
1942        let sch_polyline = SchPolyline {
1943            graphical,
1944            vertices,
1945            line_width: LineWidth::Small,
1946            ..Default::default()
1947        };
1948        primitives.push(SchRecord::Polyline(sch_polyline));
1949    }
1950
1951    // Add ellipses
1952    for ellipse in &component_def.ellipses {
1953        let fill_color_val = parse_color(&ellipse.fill_color)?;
1954        let border_color_val = parse_color(&ellipse.border_color)?;
1955
1956        let mut graphical = SchGraphicalBase::default();
1957        graphical.base.owner_part_id = Some(1);
1958        graphical.location_x = ellipse.x.to_raw();
1959        graphical.location_y = ellipse.y.to_raw();
1960        graphical.color = border_color_val;
1961        graphical.area_color = fill_color_val;
1962
1963        let sch_ellipse = SchEllipse {
1964            graphical,
1965            radius_x: ellipse.radius_x.to_raw(),
1966            radius_y: ellipse.radius_y.to_raw(),
1967            is_solid: ellipse.filled,
1968            transparent: !ellipse.filled,
1969            line_width: LineWidth::Small,
1970            ..Default::default()
1971        };
1972        primitives.push(SchRecord::Ellipse(sch_ellipse));
1973    }
1974
1975    // Add pins
1976    for pin_def in &component_def.pins {
1977        let electrical_type = parse_electrical_type(&pin_def.electrical)?;
1978        let mut conglomerate = parse_pin_orientation(&pin_def.orientation)?;
1979
1980        conglomerate |= PinConglomerateFlags::DISPLAY_NAME_VISIBLE;
1981        conglomerate |= PinConglomerateFlags::DESIGNATOR_VISIBLE;
1982
1983        if pin_def.hidden {
1984            conglomerate |= PinConglomerateFlags::HIDE;
1985        }
1986
1987        let mut graphical = SchGraphicalBase::default();
1988        graphical.base.owner_part_id = Some(1);
1989        graphical.location_x = pin_def.x.to_raw();
1990        graphical.location_y = pin_def.y.to_raw();
1991        graphical.color = 0x000080;
1992
1993        let pin = SchPin {
1994            graphical,
1995            designator: pin_def.designator.clone(),
1996            name: pin_def.name.clone(),
1997            electrical: electrical_type,
1998            pin_conglomerate: conglomerate,
1999            pin_length: pin_def.length.to_raw(),
2000            description: pin_def.description.clone(),
2001            symbol_inner_edge: PinSymbol::None,
2002            symbol_outer_edge: PinSymbol::None,
2003            symbol_inside: PinSymbol::None,
2004            symbol_outside: PinSymbol::None,
2005            ..Default::default()
2006        };
2007        primitives.push(SchRecord::Pin(pin));
2008    }
2009
2010    let pin_count = component_def.pins.len();
2011    let rect_count = component_def.rectangles.len();
2012    let line_count = component_def.lines.len();
2013    let polygon_count = component_def.polygons.len();
2014    let label_count = component_def.labels.len();
2015    let arc_count = component_def.arcs.len();
2016    let polyline_count = component_def.polylines.len();
2017    let ellipse_count = component_def.ellipses.len();
2018
2019    let lib_component = SchLibComponent {
2020        component,
2021        primitives,
2022    };
2023
2024    lib.components.push(lib_component);
2025    save_schlib(path, &lib)?;
2026
2027    // Build summary of added primitives
2028    let mut parts = vec![format!("{} pins", pin_count)];
2029    if rect_count > 0 {
2030        parts.push(format!("{} rectangles", rect_count));
2031    }
2032    if line_count > 0 {
2033        parts.push(format!("{} lines", line_count));
2034    }
2035    if polygon_count > 0 {
2036        parts.push(format!("{} polygons", polygon_count));
2037    }
2038    if label_count > 0 {
2039        parts.push(format!("{} labels", label_count));
2040    }
2041    if arc_count > 0 {
2042        parts.push(format!("{} arcs", arc_count));
2043    }
2044    if polyline_count > 0 {
2045        parts.push(format!("{} polylines", polyline_count));
2046    }
2047    if ellipse_count > 0 {
2048        parts.push(format!("{} ellipses", ellipse_count));
2049    }
2050
2051    Ok(format!(
2052        "Added component '{}' with {} to {}",
2053        component_def.name,
2054        parts.join(", "),
2055        path.display()
2056    ))
2057}
2058
2059/// Parse text orientation string.
2060fn parse_text_orientation(s: &str) -> Result<TextOrientations, Box<dyn std::error::Error>> {
2061    match s.to_lowercase().as_str() {
2062        "horizontal" | "0" => Ok(TextOrientations::NONE),
2063        "vertical_up" | "90" | "up" => Ok(TextOrientations::ROTATED),
2064        "vertical_down" | "270" | "down" => Ok(TextOrientations::ROTATED | TextOrientations::FLIPPED),
2065        "180" | "flipped" => Ok(TextOrientations::FLIPPED),
2066        _ => Err(format!("Unknown text orientation: {}. Use: horizontal, vertical_up, vertical_down, 90, 180, 270", s).into()),
2067    }
2068}
2069
2070/// Parse text justification string.
2071fn parse_text_justification(s: &str) -> Result<TextJustification, Box<dyn std::error::Error>> {
2072    match s.to_lowercase().replace('_', "").as_str() {
2073        "bottomleft" | "bl" => Ok(TextJustification::BOTTOM_LEFT),
2074        "bottomcenter" | "bc" => Ok(TextJustification::BOTTOM_CENTER),
2075        "bottomright" | "br" => Ok(TextJustification::BOTTOM_RIGHT),
2076        "centerleft" | "cl" | "middleleft" | "ml" => Ok(TextJustification::MIDDLE_LEFT),
2077        "center" | "c" | "middle" | "m" => Ok(TextJustification::MIDDLE_CENTER),
2078        "centerright" | "cr" | "middleright" | "mr" => Ok(TextJustification::MIDDLE_RIGHT),
2079        "topleft" | "tl" => Ok(TextJustification::TOP_LEFT),
2080        "topcenter" | "tc" => Ok(TextJustification::TOP_CENTER),
2081        "topright" | "tr" => Ok(TextJustification::TOP_RIGHT),
2082        _ => Err(format!("Unknown justification: {}. Use: bottom_left, bottom_center, bottom_right, center_left, center, center_right, top_left, top_center, top_right", s).into()),
2083    }
2084}
2085
2086#[cfg(test)]
2087mod tests {
2088    use super::*;
2089    use std::path::PathBuf;
2090
2091    fn temp_schlib() -> PathBuf {
2092        let id = uuid::Uuid::new_v4();
2093        std::env::temp_dir().join(format!("test_{}.SchLib", id))
2094    }
2095
2096    /// Regression: gen-ic must place pins on all four sides (left, right, top, bottom).
2097    /// Previously, top and bottom pins were silently dropped.
2098    #[test]
2099    fn test_gen_ic_all_four_sides() {
2100        let path = temp_schlib();
2101        cmd_create(&path).unwrap();
2102
2103        let result = cmd_gen_ic(
2104            &path,
2105            "TEST_4SIDE",
2106            "1:VCC:power:top,2:GND:power:bottom,3:IN:input:left,4:OUT:output:right",
2107            Some("4-side test".to_string()),
2108            "600mil",
2109            "200mil",
2110            "100mil",
2111        )
2112        .unwrap();
2113
2114        assert!(result.contains("4 pins"), "Expected 4 pins, got: {}", result);
2115        assert!(result.contains("1 left"), "Expected 1 left pin: {}", result);
2116        assert!(result.contains("1 right"), "Expected 1 right pin: {}", result);
2117        assert!(result.contains("1 top"), "Expected 1 top pin: {}", result);
2118        assert!(result.contains("1 bottom"), "Expected 1 bottom pin: {}", result);
2119
2120        // Verify all 4 pins are in the library
2121        let lib = open_or_create_schlib(&path).unwrap();
2122        let comp = lib
2123            .components
2124            .iter()
2125            .find(|c| c.component.lib_reference == "TEST_4SIDE")
2126            .expect("Component must exist");
2127
2128        let pin_count = comp
2129            .primitives
2130            .iter()
2131            .filter(|r| matches!(r, SchRecord::Pin(_)))
2132            .count();
2133        assert_eq!(pin_count, 4, "All 4 pins must be saved, got {}", pin_count);
2134
2135        std::fs::remove_file(&path).ok();
2136    }
2137
2138    /// Regression: gen-ic with only top/bottom pins must not produce 0-pin component.
2139    #[test]
2140    fn test_gen_ic_only_top_bottom() {
2141        let path = temp_schlib();
2142        cmd_create(&path).unwrap();
2143
2144        let result = cmd_gen_ic(
2145            &path,
2146            "PWR_2PIN",
2147            "1:VCC:power:top,2:GND:power:bottom",
2148            None,
2149            "400mil",
2150            "200mil",
2151            "100mil",
2152        )
2153        .unwrap();
2154
2155        assert!(result.contains("2 pins"), "Expected 2 pins, got: {}", result);
2156        assert!(result.contains("1 top"), "Expected 1 top: {}", result);
2157        assert!(result.contains("1 bottom"), "Expected 1 bottom: {}", result);
2158
2159        std::fs::remove_file(&path).ok();
2160    }
2161
2162    /// gen-ic with many pins per side places all of them.
2163    #[test]
2164    fn test_gen_ic_multi_pin_sides() {
2165        let path = temp_schlib();
2166        cmd_create(&path).unwrap();
2167
2168        // 3 left, 4 right, 2 top, 2 bottom = 11 pins
2169        let pins = [
2170            "1:A:io:left", "2:B:io:left", "3:C:io:left",
2171            "4:D:io:right", "5:E:io:right", "6:F:io:right", "7:G:io:right",
2172            "8:VCC:power:top", "9:VCC2:power:top",
2173            "10:GND:power:bottom", "11:GND2:power:bottom",
2174        ]
2175        .join(",");
2176
2177        let result = cmd_gen_ic(
2178            &path,
2179            "MULTI_PIN",
2180            &pins,
2181            None,
2182            "800mil",
2183            "200mil",
2184            "100mil",
2185        )
2186        .unwrap();
2187
2188        assert!(result.contains("11 pins"), "Expected 11 pins, got: {}", result);
2189        assert!(result.contains("3 left"), "Got: {}", result);
2190        assert!(result.contains("4 right"), "Got: {}", result);
2191        assert!(result.contains("2 top"), "Got: {}", result);
2192        assert!(result.contains("2 bottom"), "Got: {}", result);
2193
2194        // Verify pin count in library
2195        let lib = open_or_create_schlib(&path).unwrap();
2196        let comp = lib
2197            .components
2198            .iter()
2199            .find(|c| c.component.lib_reference == "MULTI_PIN")
2200            .unwrap();
2201        let pin_count = comp
2202            .primitives
2203            .iter()
2204            .filter(|r| matches!(r, SchRecord::Pin(_)))
2205            .count();
2206        assert_eq!(pin_count, 11);
2207
2208        std::fs::remove_file(&path).ok();
2209    }
2210
2211    /// gen-ic body width auto-expands when top/bottom pins need more space.
2212    #[test]
2213    fn test_gen_ic_body_widens_for_top_bottom() {
2214        let path = temp_schlib();
2215        cmd_create(&path).unwrap();
2216
2217        // 5 top pins at 100mil spacing need 600mil. Requested width is 400mil.
2218        // Body should expand to accommodate.
2219        let pins = "1:A:io:top,2:B:io:top,3:C:io:top,4:D:io:top,5:E:io:top,6:IN:input:left";
2220
2221        cmd_gen_ic(
2222            &path,
2223            "WIDE_TOP",
2224            pins,
2225            None,
2226            "400mil",  // narrower than needed
2227            "200mil",
2228            "100mil",
2229        )
2230        .unwrap();
2231
2232        let lib = open_or_create_schlib(&path).unwrap();
2233        let comp = lib
2234            .components
2235            .iter()
2236            .find(|c| c.component.lib_reference == "WIDE_TOP")
2237            .unwrap();
2238
2239        // Find the rectangle (body)
2240        let rect = comp.primitives.iter().find_map(|r| {
2241            if let SchRecord::Rectangle(rect) = r {
2242                Some(rect)
2243            } else {
2244                None
2245            }
2246        }).expect("Must have body rectangle");
2247
2248        // Body width = corner_x (origin is 0). Must be >= 600mil (5+1 * 100mil spacing)
2249        let body_width_mils = rect.corner_x as f64 / 10000.0;
2250        assert!(
2251            body_width_mils >= 600.0,
2252            "Body width should expand to at least 600mil for 5 top pins, got {}mil",
2253            body_width_mils
2254        );
2255
2256        std::fs::remove_file(&path).ok();
2257    }
2258
2259    /// Pin electrical type parsing rejects invalid values with clear error.
2260    #[test]
2261    fn test_gen_ic_invalid_pin_type() {
2262        let path = temp_schlib();
2263        cmd_create(&path).unwrap();
2264
2265        let result = cmd_gen_ic(
2266            &path,
2267            "BAD",
2268            "1:VCC:W:left",  // "W" is not valid, must be "power"
2269            None,
2270            "400mil",
2271            "200mil",
2272            "100mil",
2273        );
2274
2275        assert!(result.is_err(), "Invalid pin type should fail");
2276        let err = result.unwrap_err().to_string();
2277        assert!(
2278            err.contains("Unknown electrical type"),
2279            "Error should mention electrical type, got: {}",
2280            err
2281        );
2282
2283        std::fs::remove_file(&path).ok();
2284    }
2285}