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 left and right pins
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
1152    // Calculate body height based on pin count
1153    let left_count = left_pins.len();
1154    let right_count = right_pins.len();
1155    let max_pins = left_count.max(right_count);
1156    let body_height_mils = (max_pins + 1) as f64 * pin_spacing_mils;
1157
1158    // Create component
1159    let component = SchComponent {
1160        lib_reference: name.to_string(),
1161        component_description: description.unwrap_or_default(),
1162        part_count: 1,
1163        display_mode_count: 1,
1164        current_part_id: 1,
1165        ..Default::default()
1166    };
1167
1168    let mut primitives = vec![SchRecord::Component(component.clone())];
1169
1170    // Add body rectangle
1171    let mut rect_graphical = SchGraphicalBase::default();
1172    rect_graphical.base.owner_part_id = Some(1);
1173    rect_graphical.location_x = mils_to_raw(0);
1174    rect_graphical.location_y = mils_to_raw(0);
1175    rect_graphical.color = parse_color("800000")?; // Dark red border
1176    rect_graphical.area_color = parse_color("FFFFB0")?; // Light yellow fill
1177
1178    let rect = SchRectangle {
1179        graphical: rect_graphical,
1180        corner_x: mils_f64_to_raw(width_mils),
1181        corner_y: mils_f64_to_raw(body_height_mils),
1182        line_width: LineWidth::Small,
1183        is_solid: true,
1184        transparent: false,
1185        ..Default::default()
1186    };
1187    primitives.push(SchRecord::Rectangle(rect));
1188
1189    // Add left pins (pointing right into body)
1190    for (i, pin_def) in left_pins.iter().enumerate() {
1191        let y_mils = body_height_mils - (i + 1) as f64 * pin_spacing_mils;
1192
1193        let mut graphical = SchGraphicalBase::default();
1194        graphical.base.owner_part_id = Some(1);
1195        graphical.location_x = mils_f64_to_raw(-pin_length_mils);
1196        graphical.location_y = mils_f64_to_raw(y_mils);
1197        graphical.color = 0x000080;
1198
1199        let pin = SchPin {
1200            graphical,
1201            designator: pin_def.designator.clone(),
1202            name: pin_def.name.clone(),
1203            electrical: pin_def.electrical,
1204            pin_conglomerate: PinConglomerateFlags::DISPLAY_NAME_VISIBLE
1205                | PinConglomerateFlags::DESIGNATOR_VISIBLE,
1206            pin_length: mils_f64_to_raw(pin_length_mils),
1207            ..Default::default()
1208        };
1209        primitives.push(SchRecord::Pin(pin));
1210    }
1211
1212    // Add right pins (pointing left into body)
1213    for (i, pin_def) in right_pins.iter().enumerate() {
1214        let y_mils = body_height_mils - (i + 1) as f64 * pin_spacing_mils;
1215
1216        let mut graphical = SchGraphicalBase::default();
1217        graphical.base.owner_part_id = Some(1);
1218        graphical.location_x = mils_f64_to_raw(width_mils + pin_length_mils);
1219        graphical.location_y = mils_f64_to_raw(y_mils);
1220        graphical.color = 0x000080;
1221
1222        let pin = SchPin {
1223            graphical,
1224            designator: pin_def.designator.clone(),
1225            name: pin_def.name.clone(),
1226            electrical: pin_def.electrical,
1227            pin_conglomerate: PinConglomerateFlags::DISPLAY_NAME_VISIBLE
1228                | PinConglomerateFlags::DESIGNATOR_VISIBLE
1229                | PinConglomerateFlags::FLIPPED,
1230            pin_length: mils_f64_to_raw(pin_length_mils),
1231            ..Default::default()
1232        };
1233        primitives.push(SchRecord::Pin(pin));
1234    }
1235
1236    let lib_component = SchLibComponent {
1237        component,
1238        primitives,
1239    };
1240
1241    lib.components.push(lib_component);
1242    save_schlib(path, &lib)?;
1243
1244    Ok(format!(
1245        "Generated IC symbol '{}' with {} pins ({} left, {} right)",
1246        name,
1247        pin_defs.len(),
1248        left_count,
1249        right_count
1250    ))
1251}
1252
1253/// Render a component symbol as ASCII art.
1254pub fn cmd_render_ascii(
1255    path: &Path,
1256    component_name: &str,
1257    max_width: usize,
1258    max_height: usize,
1259) -> Result<String, Box<dyn std::error::Error>> {
1260    let lib = open_schlib(path)?;
1261
1262    let name_lower = component_name.to_lowercase();
1263    let component = lib
1264        .components
1265        .iter()
1266        .find(|c| c.component.lib_reference.to_lowercase() == name_lower)
1267        .ok_or_else(|| format!("Component '{}' not found", component_name))?;
1268
1269    // Find bounds
1270    let mut min_x = i32::MAX;
1271    let mut min_y = i32::MAX;
1272    let mut max_x = i32::MIN;
1273    let mut max_y = i32::MIN;
1274
1275    for prim in &component.primitives {
1276        match prim {
1277            SchRecord::Pin(p) => {
1278                let (cx, cy) = p.get_corner();
1279                min_x = min_x.min(p.graphical.location_x).min(cx);
1280                min_y = min_y.min(p.graphical.location_y).min(cy);
1281                max_x = max_x.max(p.graphical.location_x).max(cx);
1282                max_y = max_y.max(p.graphical.location_y).max(cy);
1283            }
1284            SchRecord::Rectangle(r) => {
1285                min_x = min_x.min(r.graphical.location_x).min(r.corner_x);
1286                min_y = min_y.min(r.graphical.location_y).min(r.corner_y);
1287                max_x = max_x.max(r.graphical.location_x).max(r.corner_x);
1288                max_y = max_y.max(r.graphical.location_y).max(r.corner_y);
1289            }
1290            SchRecord::Line(l) => {
1291                min_x = min_x.min(l.graphical.location_x).min(l.corner_x);
1292                min_y = min_y.min(l.graphical.location_y).min(l.corner_y);
1293                max_x = max_x.max(l.graphical.location_x).max(l.corner_x);
1294                max_y = max_y.max(l.graphical.location_y).max(l.corner_y);
1295            }
1296            _ => {}
1297        }
1298    }
1299
1300    if min_x == i32::MAX {
1301        return Ok("No renderable primitives found.".to_string());
1302    }
1303
1304    let width_raw = (max_x - min_x) as f64;
1305    let height_raw = (max_y - min_y) as f64;
1306
1307    // Scale to fit
1308    let scale_x = (max_width as f64 - 2.0) / width_raw;
1309    let scale_y = (max_height as f64 - 2.0) / height_raw;
1310    let scale = scale_x.min(scale_y);
1311
1312    let canvas_width = ((width_raw * scale) as usize + 2).min(max_width);
1313    let canvas_height = ((height_raw * scale) as usize + 2).min(max_height);
1314
1315    // Create canvas
1316    let mut canvas: Vec<Vec<char>> = vec![vec![' '; canvas_width]; canvas_height];
1317
1318    // Helper to convert coords
1319    let to_canvas = |x: i32, y: i32| -> (usize, usize) {
1320        let cx = ((x - min_x) as f64 * scale) as usize;
1321        let cy = canvas_height - 1 - (((y - min_y) as f64 * scale) as usize);
1322        (cx.min(canvas_width - 1), cy.min(canvas_height - 1))
1323    };
1324
1325    // Draw rectangles
1326    for prim in &component.primitives {
1327        if let SchRecord::Rectangle(r) = prim {
1328            let (x1, y1) = to_canvas(r.graphical.location_x, r.graphical.location_y);
1329            let (x2, y2) = to_canvas(r.corner_x, r.corner_y);
1330            let (x1, x2) = (x1.min(x2), x1.max(x2));
1331            let (y1, y2) = (y1.min(y2), y1.max(y2));
1332
1333            // Draw rectangle border
1334            for x in x1..=x2 {
1335                if y1 < canvas_height {
1336                    canvas[y1][x.min(canvas_width - 1)] = '-';
1337                }
1338                if y2 < canvas_height {
1339                    canvas[y2][x.min(canvas_width - 1)] = '-';
1340                }
1341            }
1342            for y in y1..=y2 {
1343                if x1 < canvas_width {
1344                    canvas[y.min(canvas_height - 1)][x1] = '|';
1345                }
1346                if x2 < canvas_width {
1347                    canvas[y.min(canvas_height - 1)][x2] = '|';
1348                }
1349            }
1350            // Corners
1351            if y1 < canvas_height && x1 < canvas_width {
1352                canvas[y1][x1] = '+';
1353            }
1354            if y1 < canvas_height && x2 < canvas_width {
1355                canvas[y1][x2] = '+';
1356            }
1357            if y2 < canvas_height && x1 < canvas_width {
1358                canvas[y2][x1] = '+';
1359            }
1360            if y2 < canvas_height && x2 < canvas_width {
1361                canvas[y2][x2] = '+';
1362            }
1363        }
1364    }
1365
1366    // Draw pins
1367    for prim in &component.primitives {
1368        if let SchRecord::Pin(p) = prim {
1369            let (px, py) = to_canvas(p.graphical.location_x, p.graphical.location_y);
1370            let (cx, cy) = p.get_corner();
1371            let (ex, ey) = to_canvas(cx, cy);
1372
1373            // Draw pin line
1374            if px == ex {
1375                let y_start = py.min(ey);
1376                let y_end = py.max(ey);
1377                for row in canvas.iter_mut().take(y_end + 1).skip(y_start) {
1378                    if let Some(cell) = row.get_mut(px) {
1379                        *cell = '|';
1380                    }
1381                }
1382            } else if let Some(row) = canvas.get_mut(py) {
1383                let x_start = px.min(ex);
1384                let x_end = px.max(ex);
1385                for cell in row.iter_mut().take(x_end + 1).skip(x_start) {
1386                    *cell = '-';
1387                }
1388            }
1389
1390            // Draw pin endpoint marker
1391            if py < canvas_height && px < canvas_width {
1392                canvas[py][px] = 'o';
1393            }
1394        }
1395    }
1396
1397    // Build output string
1398    let mut output = String::new();
1399    output.push_str(&format!("\n{}\n", component.component.lib_reference));
1400    output.push_str(&format!(
1401        "{}\n",
1402        "=".repeat(component.component.lib_reference.len())
1403    ));
1404    for row in &canvas {
1405        output.push_str(&format!("{}\n", row.iter().collect::<String>()));
1406    }
1407
1408    // Add pin list
1409    output.push_str("\nPins:\n");
1410    let mut pins: Vec<&SchPin> = component
1411        .primitives
1412        .iter()
1413        .filter_map(|p| {
1414            if let SchRecord::Pin(pin) = p {
1415                Some(pin)
1416            } else {
1417                None
1418            }
1419        })
1420        .collect();
1421    pins.sort_by(|a, b| alphanumeric_sort(&a.designator, &b.designator));
1422
1423    for pin in pins {
1424        output.push_str(&format!(
1425            "  {} - {} ({})\n",
1426            pin.designator,
1427            pin.name,
1428            electrical_type_name(&pin.electrical)
1429        ));
1430    }
1431
1432    Ok(output)
1433}
1434
1435// ═══════════════════════════════════════════════════════════════════════════
1436// JSON INPUT STRUCTURES (for LLM tool calling and structured output)
1437// ═══════════════════════════════════════════════════════════════════════════
1438
1439/// JSON schema for a pin in a schematic component.
1440/// Coordinates accept numbers (mils) or strings with units (e.g., "100mil", "2.54mm", "0.1in").
1441#[derive(Debug, Clone, Deserialize, Serialize)]
1442pub struct SchPinJson {
1443    /// Pin designator (e.g., "1", "2", "A1")
1444    pub designator: String,
1445    /// Pin name (e.g., "VCC", "GND", "DATA0")
1446    pub name: String,
1447    /// X position: number (mils) or string with unit (e.g., "100mil", "2.54mm")
1448    pub x: CoordValue,
1449    /// Y position: number (mils) or string with unit (e.g., "100mil", "2.54mm")
1450    pub y: CoordValue,
1451    /// Pin length: number (mils) or string with unit (default: 200mil)
1452    #[serde(default = "default_pin_length")]
1453    pub length: CoordValue,
1454    /// Electrical type: "input", "output", "io", "passive", "power", "oc", "oe", "hiz"
1455    #[serde(default = "default_electrical")]
1456    pub electrical: String,
1457    /// Pin orientation: "right", "left", "up", "down"
1458    #[serde(default = "default_orientation")]
1459    pub orientation: String,
1460    /// Hide the pin (optional)
1461    #[serde(default)]
1462    pub hidden: bool,
1463    /// Pin description (optional)
1464    #[serde(default)]
1465    pub description: String,
1466}
1467
1468fn default_pin_length() -> CoordValue {
1469    CoordValue(200.0)
1470}
1471
1472fn default_electrical() -> String {
1473    "passive".to_string()
1474}
1475
1476fn default_orientation() -> String {
1477    "right".to_string()
1478}
1479
1480/// JSON schema for a rectangle in a schematic component.
1481/// Coordinates accept numbers (mils) or strings with units (e.g., "100mil", "2.54mm", "0.1in").
1482#[derive(Debug, Clone, Deserialize, Serialize)]
1483pub struct SchRectangleJson {
1484    /// Corner 1 X: number (mils) or string with unit
1485    pub x1: CoordValue,
1486    /// Corner 1 Y: number (mils) or string with unit
1487    pub y1: CoordValue,
1488    /// Corner 2 X: number (mils) or string with unit
1489    pub x2: CoordValue,
1490    /// Corner 2 Y: number (mils) or string with unit
1491    pub y2: CoordValue,
1492    /// Fill the rectangle
1493    #[serde(default)]
1494    pub filled: bool,
1495    /// Fill color in hex (RRGGBB), default light yellow
1496    #[serde(default = "default_fill_color")]
1497    pub fill_color: String,
1498    /// Border color in hex (RRGGBB), default dark blue
1499    #[serde(default = "default_border_color")]
1500    pub border_color: String,
1501}
1502
1503fn default_fill_color() -> String {
1504    "FFFFB0".to_string()
1505}
1506
1507fn default_border_color() -> String {
1508    "000080".to_string()
1509}
1510
1511/// JSON schema for a line in a schematic component.
1512/// Coordinates accept numbers (mils) or strings with units (e.g., "100mil", "2.54mm", "0.1in").
1513#[derive(Debug, Clone, Deserialize, Serialize)]
1514pub struct SchLineJson {
1515    /// Start X: number (mils) or string with unit
1516    pub x1: CoordValue,
1517    /// Start Y: number (mils) or string with unit
1518    pub y1: CoordValue,
1519    /// End X: number (mils) or string with unit
1520    pub x2: CoordValue,
1521    /// End Y: number (mils) or string with unit
1522    pub y2: CoordValue,
1523    /// Line color in hex (RRGGBB)
1524    #[serde(default = "default_border_color")]
1525    pub color: String,
1526}
1527
1528/// JSON schema for a polygon in a schematic component.
1529/// Vertices accept numbers (mils) or strings with units.
1530#[derive(Debug, Clone, Deserialize, Serialize)]
1531pub struct SchPolygonJson {
1532    /// Vertices as array of [x, y] pairs: numbers (mils) or strings with units
1533    pub vertices: Vec<[CoordValue; 2]>,
1534    /// Fill the polygon
1535    #[serde(default)]
1536    pub filled: bool,
1537    /// Fill color in hex (RRGGBB)
1538    #[serde(default = "default_fill_color")]
1539    pub fill_color: String,
1540    /// Border color in hex (RRGGBB)
1541    #[serde(default = "default_border_color")]
1542    pub border_color: String,
1543}
1544
1545/// JSON schema for a text label in a schematic component.
1546/// Coordinates accept numbers (mils) or strings with units (e.g., "100mil", "2.54mm", "0.1in").
1547#[derive(Debug, Clone, Deserialize, Serialize)]
1548pub struct SchLabelJson {
1549    /// X position: number (mils) or string with unit
1550    pub x: CoordValue,
1551    /// Y position: number (mils) or string with unit
1552    pub y: CoordValue,
1553    /// Label text
1554    pub text: String,
1555    /// Text orientation: "horizontal", "vertical_up", "vertical_down", "90", "180", "270"
1556    #[serde(default = "default_label_orientation")]
1557    pub orientation: String,
1558    /// Text justification: "bottom_left", "bottom_center", "bottom_right",
1559    /// "center_left", "center", "center_right", "top_left", "top_center", "top_right"
1560    #[serde(default = "default_justification")]
1561    pub justification: String,
1562    /// Text color in hex (RRGGBB)
1563    #[serde(default = "default_border_color")]
1564    pub color: String,
1565    /// Font ID (optional, default 1)
1566    #[serde(default = "default_font_id")]
1567    pub font_id: i32,
1568    /// Hide the label
1569    #[serde(default)]
1570    pub hidden: bool,
1571}
1572
1573fn default_label_orientation() -> String {
1574    "horizontal".to_string()
1575}
1576
1577fn default_justification() -> String {
1578    "bottom_left".to_string()
1579}
1580
1581fn default_font_id() -> i32 {
1582    1
1583}
1584
1585/// JSON schema for an arc in a schematic component.
1586/// Coordinates accept numbers (mils) or strings with units (e.g., "100mil", "2.54mm", "0.1in").
1587#[derive(Debug, Clone, Deserialize, Serialize)]
1588pub struct SchArcJson {
1589    /// Center X: number (mils) or string with unit
1590    pub x: CoordValue,
1591    /// Center Y: number (mils) or string with unit
1592    pub y: CoordValue,
1593    /// Radius: number (mils) or string with unit
1594    pub radius: CoordValue,
1595    /// Start angle in degrees (0 = right, 90 = up)
1596    #[serde(default)]
1597    pub start_angle: f64,
1598    /// End angle in degrees
1599    #[serde(default = "default_end_angle")]
1600    pub end_angle: f64,
1601    /// Line color in hex (RRGGBB)
1602    #[serde(default = "default_border_color")]
1603    pub color: String,
1604}
1605
1606fn default_end_angle() -> f64 {
1607    360.0
1608}
1609
1610/// JSON schema for a polyline in a schematic component.
1611/// Vertices accept numbers (mils) or strings with units.
1612#[derive(Debug, Clone, Deserialize, Serialize)]
1613pub struct SchPolylineJson {
1614    /// Vertices as array of [x, y] pairs: numbers (mils) or strings with units
1615    pub vertices: Vec<[CoordValue; 2]>,
1616    /// Line color in hex (RRGGBB)
1617    #[serde(default = "default_border_color")]
1618    pub color: String,
1619}
1620
1621/// JSON schema for an ellipse in a schematic component.
1622/// Coordinates accept numbers (mils) or strings with units (e.g., "100mil", "2.54mm", "0.1in").
1623#[derive(Debug, Clone, Deserialize, Serialize)]
1624pub struct SchEllipseJson {
1625    /// Center X: number (mils) or string with unit
1626    pub x: CoordValue,
1627    /// Center Y: number (mils) or string with unit
1628    pub y: CoordValue,
1629    /// X radius: number (mils) or string with unit
1630    pub radius_x: CoordValue,
1631    /// Y radius: number (mils) or string with unit
1632    pub radius_y: CoordValue,
1633    /// Fill the ellipse
1634    #[serde(default)]
1635    pub filled: bool,
1636    /// Fill color in hex (RRGGBB)
1637    #[serde(default = "default_fill_color")]
1638    pub fill_color: String,
1639    /// Border color in hex (RRGGBB)
1640    #[serde(default = "default_border_color")]
1641    pub border_color: String,
1642}
1643
1644/// JSON schema for a complete schematic component definition.
1645/// This is the top-level structure for the add-json command.
1646#[derive(Debug, Clone, Deserialize, Serialize)]
1647pub struct SchComponentJson {
1648    /// Component name (LIBREFERENCE)
1649    pub name: String,
1650    /// Component description (optional)
1651    #[serde(default)]
1652    pub description: String,
1653    /// Number of parts (for multi-part components, default 1)
1654    #[serde(default = "default_part_count")]
1655    pub part_count: i32,
1656    /// List of pins
1657    #[serde(default)]
1658    pub pins: Vec<SchPinJson>,
1659    /// List of rectangles (typically for the body)
1660    #[serde(default)]
1661    pub rectangles: Vec<SchRectangleJson>,
1662    /// List of lines
1663    #[serde(default)]
1664    pub lines: Vec<SchLineJson>,
1665    /// List of polygons
1666    #[serde(default)]
1667    pub polygons: Vec<SchPolygonJson>,
1668    /// List of text labels
1669    #[serde(default)]
1670    pub labels: Vec<SchLabelJson>,
1671    /// List of arcs
1672    #[serde(default)]
1673    pub arcs: Vec<SchArcJson>,
1674    /// List of polylines (multi-segment lines)
1675    #[serde(default)]
1676    pub polylines: Vec<SchPolylineJson>,
1677    /// List of ellipses
1678    #[serde(default)]
1679    pub ellipses: Vec<SchEllipseJson>,
1680}
1681
1682fn default_part_count() -> i32 {
1683    1
1684}
1685
1686/// Add a complete component from JSON input.
1687pub fn cmd_add_json(
1688    path: &Path,
1689    json_file: Option<String>,
1690    json_str: Option<String>,
1691) -> Result<String, Box<dyn std::error::Error>> {
1692    use std::io::{self, Read as IoRead};
1693
1694    // Read JSON from file, stdin, or command line
1695    let json_content = match (json_file, json_str) {
1696        (_, Some(s)) => s,
1697        (Some(ref path), None) if path == "-" => {
1698            let mut buffer = String::new();
1699            io::stdin().read_to_string(&mut buffer)?;
1700            buffer
1701        }
1702        (Some(ref file_path), None) => std::fs::read_to_string(file_path)?,
1703        (None, None) => {
1704            return Err("Must provide either --file <file> or --json <string>".into());
1705        }
1706    };
1707
1708    // Parse JSON
1709    let component_def: SchComponentJson = serde_json::from_str(&json_content)?;
1710
1711    // Open or create library
1712    let mut lib = open_or_create_schlib(path)?;
1713
1714    // Check if component already exists
1715    if lib
1716        .components
1717        .iter()
1718        .any(|c| c.component.lib_reference == component_def.name)
1719    {
1720        return Err(format!("Component '{}' already exists", component_def.name).into());
1721    }
1722
1723    // Create component record
1724    let component = SchComponent {
1725        lib_reference: component_def.name.clone(),
1726        component_description: component_def.description.clone(),
1727        part_count: component_def.part_count,
1728        display_mode_count: 1,
1729        current_part_id: 1,
1730        ..Default::default()
1731    };
1732
1733    // Start with the component record as first primitive
1734    let mut primitives = vec![SchRecord::Component(component.clone())];
1735
1736    // Add rectangles
1737    for rect in &component_def.rectangles {
1738        let fill_color_val = parse_color(&rect.fill_color)?;
1739        let border_color_val = parse_color(&rect.border_color)?;
1740
1741        let mut graphical = SchGraphicalBase::default();
1742        graphical.base.owner_part_id = Some(1);
1743        graphical.location_x = rect.x1.to_raw();
1744        graphical.location_y = rect.y1.to_raw();
1745        graphical.color = border_color_val;
1746        graphical.area_color = fill_color_val;
1747
1748        let sch_rect = SchRectangle {
1749            graphical,
1750            corner_x: rect.x2.to_raw(),
1751            corner_y: rect.y2.to_raw(),
1752            line_width: LineWidth::Small,
1753            is_solid: rect.filled,
1754            transparent: !rect.filled,
1755            ..Default::default()
1756        };
1757        primitives.push(SchRecord::Rectangle(sch_rect));
1758    }
1759
1760    // Add lines
1761    for line in &component_def.lines {
1762        let color_val = parse_color(&line.color)?;
1763
1764        let mut graphical = SchGraphicalBase::default();
1765        graphical.base.owner_part_id = Some(1);
1766        graphical.location_x = line.x1.to_raw();
1767        graphical.location_y = line.y1.to_raw();
1768        graphical.color = color_val;
1769
1770        let sch_line = SchLine {
1771            graphical,
1772            corner_x: line.x2.to_raw(),
1773            corner_y: line.y2.to_raw(),
1774            line_width: LineWidth::Small,
1775            ..Default::default()
1776        };
1777        primitives.push(SchRecord::Line(sch_line));
1778    }
1779
1780    // Add polygons
1781    for polygon in &component_def.polygons {
1782        if polygon.vertices.len() < 3 {
1783            return Err("Polygon must have at least 3 vertices".into());
1784        }
1785
1786        let fill_color_val = parse_color(&polygon.fill_color)?;
1787        let border_color_val = parse_color(&polygon.border_color)?;
1788
1789        let vertices: Vec<(i32, i32)> = polygon
1790            .vertices
1791            .iter()
1792            .map(|v| (v[0].to_raw(), v[1].to_raw()))
1793            .collect();
1794
1795        let mut graphical = SchGraphicalBase::default();
1796        graphical.base.owner_part_id = Some(1);
1797        graphical.location_x = vertices[0].0;
1798        graphical.location_y = vertices[0].1;
1799        graphical.color = border_color_val;
1800        graphical.area_color = fill_color_val;
1801
1802        let sch_polygon = SchPolygon {
1803            graphical,
1804            vertices,
1805            line_width: LineWidth::Small,
1806            is_solid: polygon.filled,
1807            transparent: !polygon.filled,
1808            ..Default::default()
1809        };
1810        primitives.push(SchRecord::Polygon(sch_polygon));
1811    }
1812
1813    // Add labels (text)
1814    for label in &component_def.labels {
1815        let color_val = parse_color(&label.color)?;
1816        let orientation = parse_text_orientation(&label.orientation)?;
1817        let justification = parse_text_justification(&label.justification)?;
1818
1819        let mut graphical = SchGraphicalBase::default();
1820        graphical.base.owner_part_id = Some(1);
1821        graphical.location_x = label.x.to_raw();
1822        graphical.location_y = label.y.to_raw();
1823        graphical.color = color_val;
1824
1825        let sch_label = SchLabel {
1826            graphical,
1827            text: label.text.clone(),
1828            orientation,
1829            justification,
1830            font_id: label.font_id,
1831            is_hidden: label.hidden,
1832            ..Default::default()
1833        };
1834        primitives.push(SchRecord::Label(sch_label));
1835    }
1836
1837    // Add arcs
1838    for arc in &component_def.arcs {
1839        let color_val = parse_color(&arc.color)?;
1840
1841        let mut graphical = SchGraphicalBase::default();
1842        graphical.base.owner_part_id = Some(1);
1843        graphical.location_x = arc.x.to_raw();
1844        graphical.location_y = arc.y.to_raw();
1845        graphical.color = color_val;
1846
1847        let sch_arc = SchArc {
1848            graphical,
1849            radius: arc.radius.to_raw(),
1850            secondary_radius: arc.radius.to_raw(), // Same as radius for circular arcs
1851            start_angle: arc.start_angle,
1852            end_angle: arc.end_angle,
1853            line_width: LineWidth::Small,
1854            ..Default::default()
1855        };
1856        primitives.push(SchRecord::Arc(sch_arc));
1857    }
1858
1859    // Add polylines
1860    for polyline in &component_def.polylines {
1861        if polyline.vertices.len() < 2 {
1862            return Err("Polyline must have at least 2 vertices".into());
1863        }
1864
1865        let color_val = parse_color(&polyline.color)?;
1866
1867        let vertices: Vec<(i32, i32)> = polyline
1868            .vertices
1869            .iter()
1870            .map(|v| (v[0].to_raw(), v[1].to_raw()))
1871            .collect();
1872
1873        let mut graphical = SchGraphicalBase::default();
1874        graphical.base.owner_part_id = Some(1);
1875        graphical.location_x = vertices[0].0;
1876        graphical.location_y = vertices[0].1;
1877        graphical.color = color_val;
1878
1879        let sch_polyline = SchPolyline {
1880            graphical,
1881            vertices,
1882            line_width: LineWidth::Small,
1883            ..Default::default()
1884        };
1885        primitives.push(SchRecord::Polyline(sch_polyline));
1886    }
1887
1888    // Add ellipses
1889    for ellipse in &component_def.ellipses {
1890        let fill_color_val = parse_color(&ellipse.fill_color)?;
1891        let border_color_val = parse_color(&ellipse.border_color)?;
1892
1893        let mut graphical = SchGraphicalBase::default();
1894        graphical.base.owner_part_id = Some(1);
1895        graphical.location_x = ellipse.x.to_raw();
1896        graphical.location_y = ellipse.y.to_raw();
1897        graphical.color = border_color_val;
1898        graphical.area_color = fill_color_val;
1899
1900        let sch_ellipse = SchEllipse {
1901            graphical,
1902            radius_x: ellipse.radius_x.to_raw(),
1903            radius_y: ellipse.radius_y.to_raw(),
1904            is_solid: ellipse.filled,
1905            transparent: !ellipse.filled,
1906            line_width: LineWidth::Small,
1907            ..Default::default()
1908        };
1909        primitives.push(SchRecord::Ellipse(sch_ellipse));
1910    }
1911
1912    // Add pins
1913    for pin_def in &component_def.pins {
1914        let electrical_type = parse_electrical_type(&pin_def.electrical)?;
1915        let mut conglomerate = parse_pin_orientation(&pin_def.orientation)?;
1916
1917        conglomerate |= PinConglomerateFlags::DISPLAY_NAME_VISIBLE;
1918        conglomerate |= PinConglomerateFlags::DESIGNATOR_VISIBLE;
1919
1920        if pin_def.hidden {
1921            conglomerate |= PinConglomerateFlags::HIDE;
1922        }
1923
1924        let mut graphical = SchGraphicalBase::default();
1925        graphical.base.owner_part_id = Some(1);
1926        graphical.location_x = pin_def.x.to_raw();
1927        graphical.location_y = pin_def.y.to_raw();
1928        graphical.color = 0x000080;
1929
1930        let pin = SchPin {
1931            graphical,
1932            designator: pin_def.designator.clone(),
1933            name: pin_def.name.clone(),
1934            electrical: electrical_type,
1935            pin_conglomerate: conglomerate,
1936            pin_length: pin_def.length.to_raw(),
1937            description: pin_def.description.clone(),
1938            symbol_inner_edge: PinSymbol::None,
1939            symbol_outer_edge: PinSymbol::None,
1940            symbol_inside: PinSymbol::None,
1941            symbol_outside: PinSymbol::None,
1942            ..Default::default()
1943        };
1944        primitives.push(SchRecord::Pin(pin));
1945    }
1946
1947    let pin_count = component_def.pins.len();
1948    let rect_count = component_def.rectangles.len();
1949    let line_count = component_def.lines.len();
1950    let polygon_count = component_def.polygons.len();
1951    let label_count = component_def.labels.len();
1952    let arc_count = component_def.arcs.len();
1953    let polyline_count = component_def.polylines.len();
1954    let ellipse_count = component_def.ellipses.len();
1955
1956    let lib_component = SchLibComponent {
1957        component,
1958        primitives,
1959    };
1960
1961    lib.components.push(lib_component);
1962    save_schlib(path, &lib)?;
1963
1964    // Build summary of added primitives
1965    let mut parts = vec![format!("{} pins", pin_count)];
1966    if rect_count > 0 {
1967        parts.push(format!("{} rectangles", rect_count));
1968    }
1969    if line_count > 0 {
1970        parts.push(format!("{} lines", line_count));
1971    }
1972    if polygon_count > 0 {
1973        parts.push(format!("{} polygons", polygon_count));
1974    }
1975    if label_count > 0 {
1976        parts.push(format!("{} labels", label_count));
1977    }
1978    if arc_count > 0 {
1979        parts.push(format!("{} arcs", arc_count));
1980    }
1981    if polyline_count > 0 {
1982        parts.push(format!("{} polylines", polyline_count));
1983    }
1984    if ellipse_count > 0 {
1985        parts.push(format!("{} ellipses", ellipse_count));
1986    }
1987
1988    Ok(format!(
1989        "Added component '{}' with {} to {}",
1990        component_def.name,
1991        parts.join(", "),
1992        path.display()
1993    ))
1994}
1995
1996/// Parse text orientation string.
1997fn parse_text_orientation(s: &str) -> Result<TextOrientations, Box<dyn std::error::Error>> {
1998    match s.to_lowercase().as_str() {
1999        "horizontal" | "0" => Ok(TextOrientations::NONE),
2000        "vertical_up" | "90" | "up" => Ok(TextOrientations::ROTATED),
2001        "vertical_down" | "270" | "down" => Ok(TextOrientations::ROTATED | TextOrientations::FLIPPED),
2002        "180" | "flipped" => Ok(TextOrientations::FLIPPED),
2003        _ => Err(format!("Unknown text orientation: {}. Use: horizontal, vertical_up, vertical_down, 90, 180, 270", s).into()),
2004    }
2005}
2006
2007/// Parse text justification string.
2008fn parse_text_justification(s: &str) -> Result<TextJustification, Box<dyn std::error::Error>> {
2009    match s.to_lowercase().replace('_', "").as_str() {
2010        "bottomleft" | "bl" => Ok(TextJustification::BOTTOM_LEFT),
2011        "bottomcenter" | "bc" => Ok(TextJustification::BOTTOM_CENTER),
2012        "bottomright" | "br" => Ok(TextJustification::BOTTOM_RIGHT),
2013        "centerleft" | "cl" | "middleleft" | "ml" => Ok(TextJustification::MIDDLE_LEFT),
2014        "center" | "c" | "middle" | "m" => Ok(TextJustification::MIDDLE_CENTER),
2015        "centerright" | "cr" | "middleright" | "mr" => Ok(TextJustification::MIDDLE_RIGHT),
2016        "topleft" | "tl" => Ok(TextJustification::TOP_LEFT),
2017        "topcenter" | "tc" => Ok(TextJustification::TOP_CENTER),
2018        "topright" | "tr" => Ok(TextJustification::TOP_RIGHT),
2019        _ => Err(format!("Unknown justification: {}. Use: bottom_left, bottom_center, bottom_right, center_left, center, center_right, top_left, top_center, top_right", s).into()),
2020    }
2021}