1use std::collections::HashMap;
9use std::fs::File;
10use std::io::BufReader;
11use std::path::{Path, PathBuf};
12
13use crate::edit::pcb_placement::{
14 parse_coord as placement_parse_coord, parse_offset, parse_position,
15};
16use crate::edit::{BoardEdge, PcbPlacementEngine, PlacementAnchor};
17use crate::io::PcbDoc;
18use crate::ops::output::*;
19use crate::records::pcb::{
20 HatchStyle, PcbBoard, PcbPolygon, PcbRule, PolygonVertex, PolygonVertexKind, RuleKind,
21};
22use crate::types::{Coord, CoordPoint, Layer};
23
24fn open_pcbdoc(path: &Path) -> Result<PcbDoc, String> {
26 let file = File::open(path).map_err(|e| format!("Error opening file: {}", e))?;
27 PcbDoc::open(BufReader::new(file)).map_err(|e| format!("Error parsing PcbDoc: {:?}", e))
28}
29
30fn parse_coord(s: &str) -> Result<Coord, String> {
32 let s = s.trim().to_lowercase();
33
34 if s.ends_with("mil") {
35 let val: f64 = s
36 .trim_end_matches("mil")
37 .trim()
38 .parse()
39 .map_err(|_| format!("Invalid coordinate: {}", s))?;
40 Ok(Coord::from_mils(val))
41 } else if s.ends_with("mm") {
42 let val: f64 = s
43 .trim_end_matches("mm")
44 .trim()
45 .parse()
46 .map_err(|_| format!("Invalid coordinate: {}", s))?;
47 Ok(Coord::from_mms(val))
48 } else {
49 let val: f64 = s
51 .parse()
52 .map_err(|_| format!("Invalid coordinate: {} (use '10mil' or '0.254mm')", s))?;
53 Ok(Coord::from_mils(val))
54 }
55}
56
57fn rule_kind_display(kind: &RuleKind) -> String {
59 format!("{}", kind)
60}
61
62pub fn cmd_overview(path: &Path) -> Result<PcbDocOverview, Box<dyn std::error::Error>> {
68 let pcb = open_pcbdoc(path)?;
69
70 let summary = PcbDocSummary {
72 components: pcb.components.len(),
73 nets: pcb.nets.len(),
74 rules: pcb.rules.len(),
75 primitives: pcb.primitives.len(),
76 tracks: pcb.track_count(),
77 vias: pcb.via_count(),
78 };
79
80 let mut rules_by_kind: HashMap<String, Vec<&PcbRule>> = HashMap::new();
82 for rule in &pcb.rules {
83 rules_by_kind
84 .entry(rule_kind_display(&rule.kind))
85 .or_default()
86 .push(rule);
87 }
88
89 let mut rules_by_category: Vec<(String, Vec<RuleSummary>)> = Vec::new();
90 let mut categories: Vec<_> = rules_by_kind.keys().cloned().collect();
91 categories.sort();
92
93 for category in categories {
94 let rules = &rules_by_kind[&category];
95 let rule_summaries: Vec<RuleSummary> = rules
96 .iter()
97 .map(|rule| RuleSummary {
98 name: rule.name.clone(),
99 priority: rule.priority,
100 enabled: rule.enabled,
101 })
102 .collect();
103 rules_by_category.push((category, rule_summaries));
104 }
105
106 let components_preview: Vec<ComponentPreview> = pcb
108 .components
109 .iter()
110 .take(10)
111 .map(|comp| ComponentPreview {
112 designator: comp.designator.clone(),
113 pattern: comp.pattern.clone(),
114 comment: comp.comment.clone(),
115 })
116 .collect();
117
118 let nets_preview: Vec<String> = pcb.nets.iter().take(10).cloned().collect();
120
121 Ok(PcbDocOverview {
122 path: path.display().to_string(),
123 summary,
124 rules_by_category,
125 components_preview,
126 nets_preview,
127 })
128}
129
130pub fn cmd_info(path: &Path) -> Result<PcbDocInfo, Box<dyn std::error::Error>> {
132 let pcb = open_pcbdoc(path)?;
133
134 Ok(PcbDocInfo {
135 path: path.display().to_string(),
136 component_count: pcb.components.len(),
137 net_count: pcb.nets.len(),
138 rule_count: pcb.rules.len(),
139 primitive_count: pcb.primitives.len(),
140 track_count: pcb.track_count(),
141 via_count: pcb.via_count(),
142 })
143}
144
145pub fn cmd_rules(
151 path: &Path,
152 kind_filter: Option<String>,
153 verbose: bool,
154) -> Result<PcbDocRuleList, Box<dyn std::error::Error>> {
155 let pcb = open_pcbdoc(path)?;
156
157 let kind_filter_lower = kind_filter.as_ref().map(|s| s.to_lowercase());
158
159 let filtered_rules: Vec<_> = pcb
160 .rules
161 .iter()
162 .filter(|rule| {
163 if let Some(ref filter) = kind_filter_lower {
164 rule_kind_display(&rule.kind)
165 .to_lowercase()
166 .contains(filter)
167 } else {
168 true
169 }
170 })
171 .collect();
172
173 let rules: Vec<RuleInfo> = filtered_rules
174 .iter()
175 .map(|rule| {
176 let parameters = if verbose {
177 Some(
178 rule.params
179 .iter()
180 .map(|(k, v)| (k.to_string(), v.to_string()))
181 .collect(),
182 )
183 } else {
184 None
185 };
186
187 RuleInfo {
188 name: rule.name.clone(),
189 kind: rule_kind_display(&rule.kind),
190 enabled: rule.enabled,
191 priority: rule.priority,
192 scope1_expression: rule.scope1_expression.clone(),
193 scope2_expression: rule.scope2_expression.clone(),
194 comment: rule.comment.clone(),
195 parameters,
196 }
197 })
198 .collect();
199
200 Ok(PcbDocRuleList {
201 path: path.display().to_string(),
202 filter: kind_filter,
203 total_rules: rules.len(),
204 rules,
205 })
206}
207
208pub fn cmd_rule(
210 path: &Path,
211 name: &str,
212 _show_params: bool,
213) -> Result<PcbDocRuleDetail, Box<dyn std::error::Error>> {
214 let pcb = open_pcbdoc(path)?;
215
216 let name_lower = name.to_lowercase();
217 let rule = pcb
218 .rules
219 .iter()
220 .find(|r| r.name.to_lowercase() == name_lower)
221 .ok_or_else(|| format!("Rule '{}' not found", name))?;
222
223 Ok(PcbDocRuleDetail {
224 name: rule.name.clone(),
225 kind: rule_kind_display(&rule.kind),
226 enabled: rule.enabled,
227 priority: rule.priority,
228 scope1_expression: rule.scope1_expression.clone(),
229 scope2_expression: rule.scope2_expression.clone(),
230 comment: rule.comment.clone(),
231 parameters: rule
232 .params
233 .iter()
234 .map(|(k, v)| (k.to_string(), v.to_string()))
235 .collect(),
236 })
237}
238
239#[allow(clippy::too_many_arguments)]
241pub fn cmd_add_rule(
242 path: &Path,
243 kind_str: &str,
244 name: &str,
245 priority: i32,
246 scope1: &str,
247 scope2: &str,
248 gap: Option<String>,
249 min_width: Option<String>,
250 max_width: Option<String>,
251 pref_width: Option<String>,
252 comment: Option<String>,
253 disabled: bool,
254) -> Result<(), String> {
255 let mut pcb = open_pcbdoc(path)?;
256
257 if pcb
259 .rules
260 .iter()
261 .any(|r| r.name.to_lowercase() == name.to_lowercase())
262 {
263 return Err(format!("Rule '{}' already exists", name));
264 }
265
266 let kind = RuleKind::from_name(kind_str)
268 .ok_or_else(|| format!("Unknown rule kind: '{}'. Valid kinds: Clearance, Width, RoutingLayers, RoutingVias, etc.", kind_str))?;
269
270 let mut rule = PcbRule::new(kind, name);
272 rule.enabled = !disabled;
273 rule.priority = priority;
274 rule.scope1_expression = scope1.to_string();
275 rule.scope2_expression = scope2.to_string();
276
277 rule.unique_id = format!(
279 "{:08X}",
280 std::time::SystemTime::now()
281 .duration_since(std::time::UNIX_EPOCH)
282 .unwrap_or_default()
283 .as_secs() as u32
284 );
285
286 if let Some(ref c) = comment {
287 rule.comment = c.clone();
288 }
289
290 if let Some(ref gap_str) = gap {
292 let coord = parse_coord(gap_str)?;
293 rule.params.add_coord("GAP", coord);
294 }
295 if let Some(ref min_str) = min_width {
296 let coord = parse_coord(min_str)?;
297 rule.params.add_coord("MINWIDTH", coord);
298 }
299 if let Some(ref max_str) = max_width {
300 let coord = parse_coord(max_str)?;
301 rule.params.add_coord("MAXWIDTH", coord);
302 }
303 if let Some(ref pref_str) = pref_width {
304 let coord = parse_coord(pref_str)?;
305 rule.params.add_coord("PREFWIDTH", coord);
306 }
307
308 rule.params.add("SELECTION", "FALSE");
310 rule.params.add("LAYER", "UNKNOWN");
311 rule.params.add("LOCKED", "FALSE");
312 rule.params.add("POLYGONOUTLINE", "FALSE");
313 rule.params.add("USERROUTED", "TRUE");
314 rule.params.add("KEEPOUT", "FALSE");
315 rule.params.add("UNIONINDEX", "0");
316
317 pcb.add_rule(rule);
318
319 pcb.save_to_file(path)
321 .map_err(|e| format!("Error saving file: {:?}", e))?;
322
323 println!("Added rule '{}' ({}) to {}", name, kind_str, path.display());
324 println!("Total rules: {}", pcb.rules.len());
325
326 Ok(())
327}
328
329#[allow(clippy::too_many_arguments)]
331pub fn cmd_modify_rule(
332 path: &Path,
333 name: &str,
334 priority: Option<i32>,
335 gap: Option<String>,
336 min_width: Option<String>,
337 max_width: Option<String>,
338 pref_width: Option<String>,
339 comment: Option<String>,
340 enable: bool,
341 disable: bool,
342) -> Result<(), String> {
343 let mut pcb = open_pcbdoc(path)?;
344
345 let name_lower = name.to_lowercase();
346 let rule = pcb
347 .rules
348 .iter_mut()
349 .find(|r| r.name.to_lowercase() == name_lower)
350 .ok_or_else(|| format!("Rule '{}' not found", name))?;
351
352 let mut changes = Vec::new();
353
354 if let Some(p) = priority {
355 rule.priority = p;
356 changes.push(format!("priority={}", p));
357 }
358
359 if enable && disable {
360 return Err("Cannot use both --enable and --disable".to_string());
361 }
362 if enable {
363 rule.enabled = true;
364 changes.push("enabled=true".to_string());
365 }
366 if disable {
367 rule.enabled = false;
368 changes.push("enabled=false".to_string());
369 }
370
371 if let Some(ref c) = comment {
372 rule.comment = c.clone();
373 changes.push(format!("comment={}", c));
374 }
375
376 if let Some(ref gap_str) = gap {
377 let coord = parse_coord(gap_str)?;
378 rule.params.add_coord("GAP", coord);
379 changes.push(format!("GAP={}", gap_str));
380 }
381 if let Some(ref min_str) = min_width {
382 let coord = parse_coord(min_str)?;
383 rule.params.add_coord("MINWIDTH", coord);
384 changes.push(format!("MINWIDTH={}", min_str));
385 }
386 if let Some(ref max_str) = max_width {
387 let coord = parse_coord(max_str)?;
388 rule.params.add_coord("MAXWIDTH", coord);
389 changes.push(format!("MAXWIDTH={}", max_str));
390 }
391 if let Some(ref pref_str) = pref_width {
392 let coord = parse_coord(pref_str)?;
393 rule.params.add_coord("PREFWIDTH", coord);
394 changes.push(format!("PREFWIDTH={}", pref_str));
395 }
396
397 if changes.is_empty() {
398 println!("No changes specified");
399 return Ok(());
400 }
401
402 pcb.save_to_file(path)
404 .map_err(|e| format!("Error saving file: {:?}", e))?;
405
406 println!("Modified rule '{}' in {}", name, path.display());
407 for change in changes {
408 println!(" {}", change);
409 }
410
411 Ok(())
412}
413
414pub fn cmd_delete_rule(path: &Path, name: &str) -> Result<(), String> {
416 let mut pcb = open_pcbdoc(path)?;
417
418 let name_lower = name.to_lowercase();
419 let original_count = pcb.rules.len();
420
421 pcb.rules.retain(|r| r.name.to_lowercase() != name_lower);
422
423 if pcb.rules.len() == original_count {
424 return Err(format!("Rule '{}' not found", name));
425 }
426
427 pcb.save_to_file(path)
429 .map_err(|e| format!("Error saving file: {:?}", e))?;
430
431 println!("Deleted rule '{}' from {}", name, path.display());
432 println!("Remaining rules: {}", pcb.rules.len());
433
434 Ok(())
435}
436
437pub fn cmd_json(
439 path: &Path,
440 full: bool,
441 _pretty: bool,
442) -> Result<PcbDocJson, Box<dyn std::error::Error>> {
443 let pcb = open_pcbdoc(path)?;
444
445 let summary = PcbDocSummary {
446 components: pcb.components.len(),
447 nets: pcb.nets.len(),
448 rules: pcb.rules.len(),
449 primitives: pcb.primitives.len(),
450 tracks: pcb.track_count(),
451 vias: pcb.via_count(),
452 };
453
454 let rules: Option<Vec<RuleInfo>> = if full {
455 Some(
456 pcb.rules
457 .iter()
458 .map(|rule| RuleInfo {
459 name: rule.name.clone(),
460 kind: rule_kind_display(&rule.kind),
461 enabled: rule.enabled,
462 priority: rule.priority,
463 scope1_expression: rule.scope1_expression.clone(),
464 scope2_expression: rule.scope2_expression.clone(),
465 comment: rule.comment.clone(),
466 parameters: Some(
467 rule.params
468 .iter()
469 .map(|(k, v)| (k.to_string(), v.to_string()))
470 .collect(),
471 ),
472 })
473 .collect(),
474 )
475 } else {
476 None
477 };
478
479 let components: Option<Vec<PcbComponentInfo>> = if full {
480 Some(
481 pcb.components
482 .iter()
483 .map(|c| {
484 let locked = c
485 .params
486 .get("LOCKED")
487 .map(|v| v.to_string() == "T")
488 .unwrap_or(false);
489 PcbComponentInfo {
490 designator: c.designator.clone(),
491 pattern: c.pattern.clone(),
492 comment: c.comment.clone(),
493 x: c.x().map(|coord| format!("{:.3}mm", coord.to_mms())),
494 y: c.y().map(|coord| format!("{:.3}mm", coord.to_mms())),
495 rotation: c.rotation(),
496 layer: c.layer().name().to_string(),
497 locked,
498 }
499 })
500 .collect(),
501 )
502 } else {
503 None
504 };
505
506 let nets = if full { Some(pcb.nets.clone()) } else { None };
507
508 Ok(PcbDocJson {
509 file: path.display().to_string(),
510 summary,
511 rules,
512 components,
513 nets,
514 layers: None, })
516}
517
518pub fn cmd_components(
524 path: &Path,
525 _verbose: bool,
526 layer_filter: Option<String>,
527) -> Result<PcbDocComponentList, Box<dyn std::error::Error>> {
528 let pcb = open_pcbdoc(path)?;
529
530 let layer_filter_lower = layer_filter.as_ref().map(|s| s.to_lowercase());
532
533 let components: Vec<PcbComponentInfo> = pcb
534 .components
535 .iter()
536 .filter(|component| {
537 if let Some(ref filter) = layer_filter_lower {
538 let layer_name = component.layer().name();
539 layer_name.to_lowercase().contains(filter)
540 } else {
541 true
542 }
543 })
544 .map(|component| {
545 let locked = component
546 .params
547 .get("LOCKED")
548 .map(|v| v.to_string() == "T")
549 .unwrap_or(false);
550 PcbComponentInfo {
551 designator: component.designator.clone(),
552 pattern: component.pattern.clone(),
553 comment: component.comment.clone(),
554 x: component.x().map(|c| format!("{:.3}mm", c.to_mms())),
555 y: component.y().map(|c| format!("{:.3}mm", c.to_mms())),
556 rotation: component.rotation(),
557 layer: component.layer().name().to_string(),
558 locked,
559 }
560 })
561 .collect();
562
563 Ok(PcbDocComponentList {
564 path: path.display().to_string(),
565 total_components: components.len(),
566 layer_filter,
567 components,
568 })
569}
570
571pub fn cmd_component(
573 path: &Path,
574 designator: &str,
575 _show_params: bool,
576) -> Result<PcbDocComponentDetail, Box<dyn std::error::Error>> {
577 let pcb = open_pcbdoc(path)?;
578
579 let component = pcb
580 .find_component(designator)
581 .ok_or_else(|| format!("Component '{}' not found", designator))?;
582
583 let pad_count = component
585 .primitives
586 .iter()
587 .filter(|p| matches!(p, crate::records::pcb::PcbRecord::Pad(_)))
588 .count();
589
590 let locked = component
591 .params
592 .get("LOCKED")
593 .map(|v| v.to_string() == "T")
594 .unwrap_or(false);
595 let source_designator = component
596 .params
597 .get("SOURCEDESIGNATOR")
598 .map(|v| v.to_string())
599 .unwrap_or_default();
600 let source_footprint = component
601 .params
602 .get("SOURCEFOOTPRINTLIBRARY")
603 .map(|v| v.to_string())
604 .unwrap_or_default();
605 let unique_id = component
606 .params
607 .get("UNIQUEID")
608 .map(|v| v.to_string())
609 .unwrap_or_default();
610
611 Ok(PcbDocComponentDetail {
612 designator: component.designator.clone(),
613 pattern: component.pattern.clone(),
614 comment: component.comment.clone(),
615 source_designator,
616 source_footprint,
617 x: component.x().map(|c| format!("{:.4}mm", c.to_mms())),
618 y: component.y().map(|c| format!("{:.4}mm", c.to_mms())),
619 rotation: component.rotation(),
620 layer: component.layer().name().to_string(),
621 locked,
622 pad_count,
623 unique_id,
624 })
625}
626
627#[allow(clippy::too_many_arguments)]
629pub fn cmd_place_component(
630 path: &Path,
631 designator: &str,
632 at: Option<String>,
633 near: Option<String>,
634 align_x: Option<String>,
635 align_y: Option<String>,
636 edge: Option<String>,
637 offset: Option<String>,
638 rotation: Option<f64>,
639 layer: Option<String>,
640 grid: Option<String>,
641 force: bool,
642) -> Result<(), String> {
643 let mut pcb = open_pcbdoc(path)?;
644
645 if pcb.find_component(designator).is_none() {
647 return Err(format!("Component '{}' not found", designator));
648 }
649
650 let mut engine = PcbPlacementEngine::new();
652
653 if let Some(ref grid_str) = grid {
655 let grid_coord = placement_parse_coord(grid_str)?;
656 engine.set_grid(crate::edit::types::Grid {
657 spacing: grid_coord,
658 snap_enabled: true,
659 });
660 }
661
662 engine.calculate_board_bounds(&pcb);
664
665 let connected = engine.find_connected_routes(&pcb, designator);
667 if connected.has_connections() && !force {
668 return Err(format!(
669 "Component '{}' has {} connected routes ({} tracks, {} vias). Use --force to move anyway.",
670 designator,
671 connected.count(),
672 connected.tracks.len(),
673 connected.vias.len()
674 ));
675 }
676
677 let current_pos = engine.get_component_position(&pcb, designator);
679
680 let offset_point = if let Some(ref offset_str) = offset {
682 parse_offset(offset_str)?
683 } else {
684 CoordPoint::ZERO
685 };
686
687 let anchor = if let Some(ref at_str) = at {
689 PlacementAnchor::Absolute(parse_position(at_str)?)
690 } else if let Some(ref near_str) = near {
691 PlacementAnchor::NearComponent {
692 designator: near_str.clone(),
693 offset: offset_point,
694 }
695 } else if let Some(ref align_x_str) = align_x {
696 PlacementAnchor::AlignX {
697 designator: align_x_str.clone(),
698 offset: offset_point.x,
699 }
700 } else if let Some(ref align_y_str) = align_y {
701 PlacementAnchor::AlignY {
702 designator: align_y_str.clone(),
703 offset: offset_point.y,
704 }
705 } else if let Some(ref edge_str) = edge {
706 let board_edge = BoardEdge::try_parse(edge_str).ok_or_else(|| {
707 format!(
708 "Invalid edge: '{}'. Use: left, right, top, bottom",
709 edge_str
710 )
711 })?;
712 PlacementAnchor::BoardEdge {
713 edge: board_edge,
714 offset: offset_point.x, }
716 } else if rotation.is_some() || layer.is_some() {
717 if let Some(ref pos) = current_pos {
719 PlacementAnchor::Absolute(CoordPoint::new(pos.x, pos.y))
720 } else {
721 return Err("No position specified and component has no current position".to_string());
722 }
723 } else {
724 return Err(
725 "No position specified. Use --at, --near, --align-x, --align-y, or --edge".to_string(),
726 );
727 };
728
729 let target_pos = engine.resolve_anchor(&pcb, &anchor, current_pos.as_ref())?;
731
732 {
734 let component = pcb
735 .find_component_mut(designator)
736 .ok_or_else(|| format!("Component '{}' not found", designator))?;
737
738 component.set_position(target_pos.x, target_pos.y);
739
740 if let Some(rot) = rotation {
741 component.set_rotation(rot);
742 }
743
744 if let Some(ref layer_str) = layer {
745 let new_layer = Layer::from_name(layer_str).ok_or_else(|| {
746 format!(
747 "Invalid layer: '{}'. Use: TopLayer, BottomLayer, etc.",
748 layer_str
749 )
750 })?;
751 component.set_layer(new_layer);
752 }
753 }
754
755 pcb.save_with_components(path)
757 .map_err(|e| format!("Error saving file: {:?}", e))?;
758
759 println!("Moved component '{}' in {}", designator, path.display());
761 println!(
762 " New position: {:.4}mm, {:.4}mm",
763 target_pos.x.to_mms(),
764 target_pos.y.to_mms()
765 );
766 if let Some(rot) = rotation {
767 println!(" Rotation: {:.1} degrees", rot);
768 }
769 if let Some(ref layer_str) = layer {
770 println!(" Layer: {}", layer_str);
771 }
772 if connected.has_connections() && force {
773 println!(
774 "\n Warning: {} connected routes may need to be re-routed.",
775 connected.count()
776 );
777 }
778
779 Ok(())
780}
781
782pub fn cmd_add_component(
784 path: &Path,
785 schematic: &Path,
786 designator: &str,
787 _footprint_lib: Option<PathBuf>,
788 footprint: Option<String>,
789 at: Option<String>,
790 layer: &str,
791) -> Result<(), String> {
792 let sch_file = File::open(schematic).map_err(|e| format!("Error opening schematic: {}", e))?;
794 let sch = crate::io::SchDoc::open(BufReader::new(sch_file))
795 .map_err(|e| format!("Error parsing schematic: {:?}", e))?;
796
797 let mut found_component: Option<&crate::records::sch::SchComponent> = None;
799 let mut component_comment = String::new();
800
801 for record in &sch.primitives {
803 if let crate::records::sch::SchRecord::Designator(d) = record {
804 if d.text().eq_ignore_ascii_case(designator) {
805 let owner_index = d.param.label.graphical.base.owner_index;
807 if owner_index >= 0 && (owner_index as usize) < sch.primitives.len() {
808 if let crate::records::sch::SchRecord::Component(c) =
809 &sch.primitives[owner_index as usize]
810 {
811 found_component = Some(c);
812 }
813 }
814 break;
815 }
816 }
817 }
818
819 for record in &sch.primitives {
821 if let crate::records::sch::SchRecord::Parameter(p) = record {
822 if p.name.to_uppercase() == "VALUE" || p.name.to_uppercase() == "COMMENT" {
823 if let Some(comp) = found_component {
824 if p.label.graphical.base.owner_index == comp.graphical.base.owner_index {
825 component_comment = p.value().to_string();
826 }
827 }
828 }
829 }
830 }
831
832 let sch_component = found_component
833 .ok_or_else(|| format!("Component '{}' not found in schematic", designator))?;
834
835 let footprint_name = footprint.ok_or_else(|| {
837 format!(
838 "No footprint specified for component '{}'. Use --footprint.",
839 designator
840 )
841 })?;
842
843 let mut pcb = open_pcbdoc(path)?;
845
846 if pcb.find_component(designator).is_some() {
848 return Err(format!("Component '{}' already exists in PCB", designator));
849 }
850
851 let position = if let Some(ref at_str) = at {
853 parse_position(at_str)?
854 } else {
855 CoordPoint::from_mms(25.0, 25.0) };
857
858 let pcb_layer = Layer::from_name(layer)
860 .or_else(|| match layer.to_lowercase().as_str() {
861 "top" => Some(Layer::TOP_LAYER),
862 "bottom" => Some(Layer::BOTTOM_LAYER),
863 _ => None,
864 })
865 .ok_or_else(|| {
866 format!(
867 "Invalid layer: '{}'. Use: TOP, BOTTOM, TopLayer, BottomLayer",
868 layer
869 )
870 })?;
871
872 let mut params = crate::types::ParameterCollection::new();
874 params.add("SELECTION", "FALSE");
875 params.add("LAYER", pcb_layer.name());
876 params.add("LOCKED", "FALSE");
877 params.add("POLYGONOUTLINE", "FALSE");
878 params.add("USERROUTED", "TRUE");
879 params.add("KEEPOUT", "FALSE");
880 params.add("PRIMITIVELOCK", "FALSE");
881 params.add_coord("X", position.x);
882 params.add_coord("Y", position.y);
883 params.add("PATTERN", &footprint_name);
884 params.add("NAMEON", "TRUE");
885 params.add("COMMENTON", "TRUE");
886 params.add("GROUPNUM", "0");
887 params.add("COUNT", "0");
888 params.add("ROTATION", "0.00000000000000E+0000");
889 params.add("SOURCEDESIGNATOR", designator);
890 params.add("SOURCELIBREFERENCE", &sch_component.lib_reference);
891 params.add(
892 "UNIQUEID",
893 &format!(
894 "{:08X}",
895 std::time::SystemTime::now()
896 .duration_since(std::time::UNIX_EPOCH)
897 .unwrap_or_default()
898 .as_secs() as u32
899 ),
900 );
901
902 if !component_comment.is_empty() {
903 params.add("COMMENT", &component_comment);
904 }
905
906 let new_component = crate::io::PcbDocComponent {
907 designator: designator.to_string(),
908 pattern: footprint_name.clone(),
909 comment: component_comment.clone(),
910 params,
911 primitives: Vec::new(),
912 };
913
914 pcb.components.push(new_component);
915
916 pcb.save_with_components(path)
918 .map_err(|e| format!("Error saving file: {:?}", e))?;
919
920 println!("Added component '{}' to {}", designator, path.display());
921 println!(" Footprint: {}", footprint_name);
922 println!(
923 " Position: {:.4}mm, {:.4}mm",
924 position.x.to_mms(),
925 position.y.to_mms()
926 );
927 println!(" Layer: {}", pcb_layer.name());
928 if !component_comment.is_empty() {
929 println!(" Comment: {}", component_comment);
930 }
931 println!("\nNote: Component pads need to be populated from a footprint library.");
932
933 Ok(())
934}
935
936const BLANK_PCBDOC_TEMPLATE: &[u8] = include_bytes!("../../data/PCB1.PcbDoc");
943
944pub fn cmd_create(path: &Path, template: Option<PathBuf>) -> Result<(), String> {
946 if path.exists() {
947 return Err(format!("File already exists: {}", path.display()));
948 }
949
950 match template {
951 Some(template_path) => {
952 std::fs::copy(&template_path, path)
954 .map_err(|e| format!("Error copying template: {}", e))?;
955 println!("Created PcbDoc from template: {}", path.display());
956 println!(" Template: {}", template_path.display());
957 }
958 None => {
959 std::fs::write(path, BLANK_PCBDOC_TEMPLATE)
961 .map_err(|e| format!("Error creating file: {}", e))?;
962 println!("Created empty PcbDoc: {}", path.display());
963 }
964 }
965
966 let pcb = open_pcbdoc(path)?;
968 println!(" Rules: {}", pcb.rules.len());
969 println!(" Classes: {}", pcb.classes.len());
970
971 Ok(())
972}
973
974pub fn cmd_outline(path: &Path, _json: bool) -> Result<PcbDocOutline, Box<dyn std::error::Error>> {
980 let pcb = open_pcbdoc(path)?;
981 let board = PcbBoard::from_params(&pcb.board_params);
982
983 let vertices: Vec<OutlineVertex> = board
984 .outline
985 .iter()
986 .map(|v| {
987 let is_arc = matches!(v.kind, PolygonVertexKind::Arc);
988 OutlineVertex {
989 x_mm: v.x.to_mms(),
990 y_mm: v.y.to_mms(),
991 kind: match v.kind {
992 PolygonVertexKind::Line => "line".to_string(),
993 PolygonVertexKind::Arc => "arc".to_string(),
994 },
995 center_x_mm: if is_arc {
996 Some(v.center_x.to_mms())
997 } else {
998 None
999 },
1000 center_y_mm: if is_arc {
1001 Some(v.center_y.to_mms())
1002 } else {
1003 None
1004 },
1005 radius_mm: if is_arc {
1006 Some(v.radius.to_mms())
1007 } else {
1008 None
1009 },
1010 }
1011 })
1012 .collect();
1013
1014 let (width, height) = calculate_outline_dimensions(&board.outline);
1015
1016 Ok(PcbDocOutline {
1017 vertex_count: board.outline.len(),
1018 width_mm: width,
1019 height_mm: height,
1020 vertices,
1021 })
1022}
1023
1024fn calculate_outline_dimensions(vertices: &[PolygonVertex]) -> (f64, f64) {
1026 if vertices.is_empty() {
1027 return (0.0, 0.0);
1028 }
1029
1030 let min_x = vertices
1031 .iter()
1032 .map(|v| v.x.to_mms())
1033 .fold(f64::INFINITY, f64::min);
1034 let max_x = vertices
1035 .iter()
1036 .map(|v| v.x.to_mms())
1037 .fold(f64::NEG_INFINITY, f64::max);
1038 let min_y = vertices
1039 .iter()
1040 .map(|v| v.y.to_mms())
1041 .fold(f64::INFINITY, f64::min);
1042 let max_y = vertices
1043 .iter()
1044 .map(|v| v.y.to_mms())
1045 .fold(f64::NEG_INFINITY, f64::max);
1046
1047 (max_x - min_x, max_y - min_y)
1048}
1049
1050pub fn cmd_set_outline_rect(
1052 path: &Path,
1053 width: &str,
1054 height: &str,
1055 origin_x: &str,
1056 origin_y: &str,
1057) -> Result<(), String> {
1058 let w = parse_coord(width)?;
1059 let h = parse_coord(height)?;
1060 let ox = parse_coord(origin_x)?;
1061 let oy = parse_coord(origin_y)?;
1062
1063 let mut pcb = open_pcbdoc(path)?;
1064
1065 let vertices = vec![
1067 PolygonVertex {
1068 kind: PolygonVertexKind::Line,
1069 x: ox,
1070 y: oy,
1071 ..Default::default()
1072 },
1073 PolygonVertex {
1074 kind: PolygonVertexKind::Line,
1075 x: Coord::from_raw(ox.to_raw() + w.to_raw()),
1076 y: oy,
1077 ..Default::default()
1078 },
1079 PolygonVertex {
1080 kind: PolygonVertexKind::Line,
1081 x: Coord::from_raw(ox.to_raw() + w.to_raw()),
1082 y: Coord::from_raw(oy.to_raw() + h.to_raw()),
1083 ..Default::default()
1084 },
1085 PolygonVertex {
1086 kind: PolygonVertexKind::Line,
1087 x: ox,
1088 y: Coord::from_raw(oy.to_raw() + h.to_raw()),
1089 ..Default::default()
1090 },
1091 ];
1092
1093 update_board_outline(&mut pcb.board_params, &vertices);
1095
1096 pcb.save_board_to_file(path)
1098 .map_err(|e| format!("Error saving file: {:?}", e))?;
1099
1100 println!(
1101 "Set board outline to rectangle: {:.3}mm x {:.3}mm",
1102 w.to_mms(),
1103 h.to_mms()
1104 );
1105 println!(" Origin: ({:.3}mm, {:.3}mm)", ox.to_mms(), oy.to_mms());
1106
1107 Ok(())
1108}
1109
1110pub fn cmd_set_outline(path: &Path, vertices_str: &str) -> Result<(), String> {
1112 let mut vertices = Vec::new();
1113
1114 for part in vertices_str.split_whitespace() {
1115 let coords: Vec<&str> = part.split(',').collect();
1116 if coords.len() != 2 {
1117 return Err(format!(
1118 "Invalid vertex format: '{}'. Use 'x,y' format.",
1119 part
1120 ));
1121 }
1122
1123 let x = parse_coord(coords[0])?;
1124 let y = parse_coord(coords[1])?;
1125
1126 vertices.push(PolygonVertex {
1127 kind: PolygonVertexKind::Line,
1128 x,
1129 y,
1130 ..Default::default()
1131 });
1132 }
1133
1134 if vertices.len() < 3 {
1135 return Err("Board outline requires at least 3 vertices.".to_string());
1136 }
1137
1138 let mut pcb = open_pcbdoc(path)?;
1139 update_board_outline(&mut pcb.board_params, &vertices);
1140 pcb.save_board_to_file(path)
1141 .map_err(|e| format!("Error saving file: {:?}", e))?;
1142
1143 println!("Set board outline with {} vertices", vertices.len());
1144
1145 Ok(())
1146}
1147
1148fn update_board_outline(
1150 params: &mut crate::types::ParameterCollection,
1151 vertices: &[PolygonVertex],
1152) {
1153 let mut idx = 0;
1155 loop {
1156 let vx_key = format!("VX{}", idx);
1157 if !params.contains(&vx_key) {
1158 break;
1159 }
1160 params.remove(&vx_key);
1161 params.remove(&format!("VY{}", idx));
1162 params.remove(&format!("KIND{}", idx));
1163 params.remove(&format!("CX{}", idx));
1164 params.remove(&format!("CY{}", idx));
1165 params.remove(&format!("SA{}", idx));
1166 params.remove(&format!("EA{}", idx));
1167 params.remove(&format!("R{}", idx));
1168 idx += 1;
1169 }
1170
1171 for (i, v) in vertices.iter().enumerate() {
1173 params.add_int(&format!("KIND{}", i), v.kind.to_int());
1174 params.add_coord(&format!("VX{}", i), v.x);
1175 params.add_coord(&format!("VY{}", i), v.y);
1176 params.add_coord(&format!("CX{}", i), v.center_x);
1177 params.add_coord(&format!("CY{}", i), v.center_y);
1178 params.add_double(&format!("SA{}", i), v.start_angle, 14);
1179 params.add_double(&format!("EA{}", i), v.end_angle, 14);
1180 params.add_coord(&format!("R{}", i), v.radius);
1181 }
1182}
1183
1184pub fn cmd_settings(
1190 path: &Path,
1191 _json: bool,
1192) -> Result<PcbDocSettings, Box<dyn std::error::Error>> {
1193 let pcb = open_pcbdoc(path)?;
1194 let board = PcbBoard::from_params(&pcb.board_params);
1195
1196 let unit = if board.is_metric() { "mm" } else { "mil" };
1197
1198 Ok(PcbDocSettings {
1199 display_unit: unit.to_string(),
1200 snap_grid: format!("{:.6}{}", board.snap_grid_size, unit),
1201 visible_grid: format!("{:.6}{}", board.visible_grid_size, unit),
1202 component_grid: format!("{:.6}{}", board.component_grid_size, unit),
1203 track_grid: Some(format!("{:.6}{}", board.track_grid_size, unit)),
1204 via_grid: Some(format!("{:.6}{}", board.via_grid_size, unit)),
1205 track_width: Some(format!("{:.3}mm", board.track_width.to_mms())),
1206 origin_x: format!("{:.3}mm", board.origin_x.to_mms()),
1207 origin_y: format!("{:.3}mm", board.origin_y.to_mms()),
1208 })
1209}
1210
1211#[allow(clippy::too_many_arguments)]
1213pub fn cmd_set_settings(
1214 path: &Path,
1215 metric: bool,
1216 imperial: bool,
1217 snap_grid: Option<String>,
1218 visible_grid: Option<String>,
1219 component_grid: Option<String>,
1220 track_grid: Option<String>,
1221 via_grid: Option<String>,
1222 track_width: Option<String>,
1223 origin_x: Option<String>,
1224 origin_y: Option<String>,
1225) -> Result<(), String> {
1226 use crate::records::pcb::DisplayUnit;
1227
1228 let mut pcb = open_pcbdoc(path)?;
1229 let mut changes = Vec::new();
1230
1231 if metric && imperial {
1232 return Err("Cannot use both --metric and --imperial".to_string());
1233 }
1234
1235 if metric {
1236 pcb.board_params
1237 .add_int("DISPLAYUNIT", DisplayUnit::Metric.to_int());
1238 changes.push("display_unit=metric".to_string());
1239 }
1240 if imperial {
1241 pcb.board_params
1242 .add_int("DISPLAYUNIT", DisplayUnit::Imperial.to_int());
1243 changes.push("display_unit=imperial".to_string());
1244 }
1245
1246 if let Some(ref v) = snap_grid {
1247 let coord = parse_coord(v)?;
1248 let val = if v.contains("mm") {
1249 coord.to_mms()
1250 } else {
1251 coord.to_mils()
1252 };
1253 pcb.board_params.add_double("SNAPGRIDSIZE", val, 6);
1254 pcb.board_params.add_double("SNAPGRIDSIZEX", val, 6);
1255 pcb.board_params.add_double("SNAPGRIDSIZEY", val, 6);
1256 changes.push(format!("snap_grid={}", v));
1257 }
1258
1259 if let Some(ref v) = visible_grid {
1260 let coord = parse_coord(v)?;
1261 let val = if v.contains("mm") {
1262 coord.to_mms()
1263 } else {
1264 coord.to_mils()
1265 };
1266 pcb.board_params.add_double("VISIBLEGRIDSIZE", val, 6);
1267 changes.push(format!("visible_grid={}", v));
1268 }
1269
1270 if let Some(ref v) = component_grid {
1271 let coord = parse_coord(v)?;
1272 let val = if v.contains("mm") {
1273 coord.to_mms()
1274 } else {
1275 coord.to_mils()
1276 };
1277 pcb.board_params.add_double("COMPONENTGRIDSIZE", val, 6);
1278 changes.push(format!("component_grid={}", v));
1279 }
1280
1281 if let Some(ref v) = track_grid {
1282 let coord = parse_coord(v)?;
1283 let val = if v.contains("mm") {
1284 coord.to_mms()
1285 } else {
1286 coord.to_mils()
1287 };
1288 pcb.board_params.add_double("TRACKGRIDSIZE", val, 6);
1289 changes.push(format!("track_grid={}", v));
1290 }
1291
1292 if let Some(ref v) = via_grid {
1293 let coord = parse_coord(v)?;
1294 let val = if v.contains("mm") {
1295 coord.to_mms()
1296 } else {
1297 coord.to_mils()
1298 };
1299 pcb.board_params.add_double("VIAGRIDSIZE", val, 6);
1300 changes.push(format!("via_grid={}", v));
1301 }
1302
1303 if let Some(ref v) = track_width {
1304 let coord = parse_coord(v)?;
1305 pcb.board_params.add_coord("TRACKWIDTH", coord);
1306 changes.push(format!("track_width={}", v));
1307 }
1308
1309 if let Some(ref v) = origin_x {
1310 let coord = parse_coord(v)?;
1311 pcb.board_params.add_coord("ORIGINX", coord);
1312 changes.push(format!("origin_x={}", v));
1313 }
1314
1315 if let Some(ref v) = origin_y {
1316 let coord = parse_coord(v)?;
1317 pcb.board_params.add_coord("ORIGINY", coord);
1318 changes.push(format!("origin_y={}", v));
1319 }
1320
1321 if changes.is_empty() {
1322 println!("No changes specified");
1323 return Ok(());
1324 }
1325
1326 pcb.save_board_to_file(path)
1327 .map_err(|e| format!("Error saving file: {:?}", e))?;
1328
1329 println!("Modified board settings in {}", path.display());
1330 for change in changes {
1331 println!(" {}", change);
1332 }
1333
1334 Ok(())
1335}
1336
1337pub fn cmd_layers(path: &Path, all: bool) -> Result<PcbDocLayers, Box<dyn std::error::Error>> {
1343 let pcb = open_pcbdoc(path)?;
1344
1345 let mut used_layers: std::collections::HashSet<u8> = std::collections::HashSet::new();
1347 for prim in &pcb.primitives {
1348 match prim {
1349 crate::records::pcb::PcbRecord::Track(t) => {
1350 used_layers.insert(t.common.layer.to_byte());
1351 }
1352 crate::records::pcb::PcbRecord::Arc(a) => {
1353 used_layers.insert(a.common.layer.to_byte());
1354 }
1355 crate::records::pcb::PcbRecord::Via(_) => {
1356 used_layers.insert(Layer::MULTI_LAYER.to_byte());
1357 }
1358 crate::records::pcb::PcbRecord::Fill(f) => {
1359 used_layers.insert(f.base.common.layer.to_byte());
1360 }
1361 crate::records::pcb::PcbRecord::Region(r) => {
1362 used_layers.insert(r.common.layer.to_byte());
1363 }
1364 _ => {}
1365 }
1366 }
1367
1368 let signal_layers = [
1370 (Layer::TOP_LAYER, "Top Layer", "signal"),
1371 (Layer::MID_LAYER_1, "Mid Layer 1", "signal"),
1372 (Layer::MID_LAYER_2, "Mid Layer 2", "signal"),
1373 (Layer::BOTTOM_LAYER, "Bottom Layer", "signal"),
1374 ];
1375
1376 let plane_layers = [
1377 (Layer::INTERNAL_PLANE_1, "Internal Plane 1", "plane"),
1378 (Layer::INTERNAL_PLANE_2, "Internal Plane 2", "plane"),
1379 ];
1380
1381 let mask_layers = [
1382 (Layer::TOP_SOLDER, "Top Solder Mask", "mask"),
1383 (Layer::BOTTOM_SOLDER, "Bottom Solder Mask", "mask"),
1384 (Layer::TOP_PASTE, "Top Paste", "mask"),
1385 (Layer::BOTTOM_PASTE, "Bottom Paste", "mask"),
1386 ];
1387
1388 let silk_layers = [
1389 (Layer::TOP_OVERLAY, "Top Silkscreen", "silkscreen"),
1390 (Layer::BOTTOM_OVERLAY, "Bottom Silkscreen", "silkscreen"),
1391 ];
1392
1393 let mech_layers = [
1394 (Layer::MECHANICAL_1, "Mechanical 1", "mechanical"),
1395 (Layer::MECHANICAL_2, "Mechanical 2", "mechanical"),
1396 (Layer::MECHANICAL_3, "Mechanical 3", "mechanical"),
1397 (Layer::MECHANICAL_4, "Mechanical 4", "mechanical"),
1398 ];
1399
1400 let special_layers = [
1401 (Layer::KEEP_OUT_LAYER, "Keep-Out Layer", "special"),
1402 (Layer::MULTI_LAYER, "Multi-Layer", "special"),
1403 (Layer::DRILL_GUIDE, "Drill Guide", "special"),
1404 (Layer::DRILL_DRAWING, "Drill Drawing", "special"),
1405 ];
1406
1407 let mut layers: Vec<LayerInfo> = Vec::new();
1408
1409 for (layer, name, layer_type) in signal_layers
1410 .iter()
1411 .chain(plane_layers.iter())
1412 .chain(mask_layers.iter())
1413 .chain(silk_layers.iter())
1414 .chain(mech_layers.iter())
1415 .chain(special_layers.iter())
1416 {
1417 let is_used = used_layers.contains(&layer.to_byte());
1418 if all || is_used {
1419 layers.push(LayerInfo {
1420 id: layer.to_byte(),
1421 name: name.to_string(),
1422 layer_type: layer_type.to_string(),
1423 used: is_used,
1424 enabled: true,
1425 copper_thickness: None,
1426 dielectric_constant: None,
1427 dielectric_thickness: None,
1428 });
1429 }
1430 }
1431
1432 Ok(PcbDocLayers {
1433 path: path.display().to_string(),
1434 total_layers: layers.len(),
1435 show_all: all,
1436 layers,
1437 })
1438}
1439
1440pub fn cmd_keepouts(
1446 path: &Path,
1447 layer_filter: Option<String>,
1448) -> Result<PcbDocKeepouts, Box<dyn std::error::Error>> {
1449 let pcb = open_pcbdoc(path)?;
1450
1451 let mut keepouts: Vec<(usize, &crate::records::pcb::PcbRegion)> = Vec::new();
1453 for (i, prim) in pcb.primitives.iter().enumerate() {
1454 if let crate::records::pcb::PcbRecord::Region(r) = prim {
1455 if r.common.layer == Layer::KEEP_OUT_LAYER || r.common.is_keepout() {
1457 if let Some(ref filter) = layer_filter {
1458 let filter_layer = Layer::from_name(filter);
1459 if let Some(fl) = filter_layer {
1460 if r.common.layer != fl {
1461 continue;
1462 }
1463 }
1464 }
1465 keepouts.push((i, r));
1466 }
1467 }
1468 }
1469
1470 let keepout_infos: Vec<KeepoutInfo> = keepouts
1471 .iter()
1472 .map(|(i, r)| {
1473 let bounds = r.calculate_bounds();
1474 KeepoutInfo {
1475 index: *i,
1476 layer: r.common.layer.name().to_string(),
1477 x1: format!("{:.4}mm", bounds.location1.x.to_mms()),
1478 y1: format!("{:.4}mm", bounds.location1.y.to_mms()),
1479 x2: format!("{:.4}mm", bounds.location2.x.to_mms()),
1480 y2: format!("{:.4}mm", bounds.location2.y.to_mms()),
1481 kind: "region".to_string(),
1482 }
1483 })
1484 .collect();
1485
1486 Ok(PcbDocKeepouts {
1487 path: path.display().to_string(),
1488 total_keepouts: keepout_infos.len(),
1489 layer_filter,
1490 keepouts: keepout_infos,
1491 })
1492}
1493
1494pub fn cmd_add_keepout(
1496 path: &Path,
1497 layer_str: &str,
1498 x1: &str,
1499 y1: &str,
1500 x2: &str,
1501 y2: &str,
1502) -> Result<(), String> {
1503 let layer = Layer::from_name(layer_str)
1504 .ok_or_else(|| format!("Unknown layer: '{}'. Valid layers: TopLayer, BottomLayer, KeepOutLayer, MultiLayer, etc.", layer_str))?;
1505
1506 let x1_coord = parse_coord(x1)?;
1507 let y1_coord = parse_coord(y1)?;
1508 let x2_coord = parse_coord(x2)?;
1509 let y2_coord = parse_coord(y2)?;
1510
1511 let mut pcb = open_pcbdoc(path)?;
1512
1513 let region = crate::records::pcb::PcbRegion {
1515 common: crate::records::pcb::PcbPrimitiveCommon {
1516 layer,
1517 flags: crate::records::pcb::PcbFlags::KEEPOUT,
1518 ..Default::default()
1519 },
1520 parameters: crate::types::ParameterCollection::new(),
1521 outline: vec![
1522 crate::types::CoordPoint {
1523 x: x1_coord,
1524 y: y1_coord,
1525 },
1526 crate::types::CoordPoint {
1527 x: x2_coord,
1528 y: y1_coord,
1529 },
1530 crate::types::CoordPoint {
1531 x: x2_coord,
1532 y: y2_coord,
1533 },
1534 crate::types::CoordPoint {
1535 x: x1_coord,
1536 y: y2_coord,
1537 },
1538 ],
1539 };
1540
1541 pcb.primitives
1542 .push(crate::records::pcb::PcbRecord::Region(region));
1543
1544 pcb.save_regions_to_file(path)
1546 .map_err(|e| format!("Error saving file: {:?}", e))?;
1547
1548 println!(
1549 "Added keepout region on {} at ({:.3}mm, {:.3}mm) to ({:.3}mm, {:.3}mm)",
1550 layer.name(),
1551 x1_coord.to_mms(),
1552 y1_coord.to_mms(),
1553 x2_coord.to_mms(),
1554 y2_coord.to_mms()
1555 );
1556
1557 Ok(())
1558}
1559
1560pub fn cmd_cutouts(path: &Path) -> Result<PcbDocCutouts, Box<dyn std::error::Error>> {
1566 let pcb = open_pcbdoc(path)?;
1567
1568 let mut cutouts: Vec<(usize, &crate::records::pcb::PcbRegion)> = Vec::new();
1570 for (i, prim) in pcb.primitives.iter().enumerate() {
1571 if let crate::records::pcb::PcbRecord::Region(r) = prim {
1572 if r.common.layer == Layer::MULTI_LAYER && !r.common.is_keepout() {
1575 cutouts.push((i, r));
1576 }
1577 }
1578 }
1579
1580 let cutout_infos: Vec<CutoutInfo> = cutouts
1581 .iter()
1582 .map(|(i, r)| {
1583 let bounds = r.calculate_bounds();
1584 CutoutInfo {
1585 index: *i,
1586 vertex_count: r.outline.len(),
1587 bounds: BoundsInfo {
1588 x1: format!("{:.4}mm", bounds.location1.x.to_mms()),
1589 y1: format!("{:.4}mm", bounds.location1.y.to_mms()),
1590 x2: format!("{:.4}mm", bounds.location2.x.to_mms()),
1591 y2: format!("{:.4}mm", bounds.location2.y.to_mms()),
1592 },
1593 }
1594 })
1595 .collect();
1596
1597 Ok(PcbDocCutouts {
1598 path: path.display().to_string(),
1599 total_cutouts: cutout_infos.len(),
1600 cutouts: cutout_infos,
1601 })
1602}
1603
1604pub fn cmd_add_cutout(path: &Path, x1: &str, y1: &str, x2: &str, y2: &str) -> Result<(), String> {
1606 let x1_coord = parse_coord(x1)?;
1607 let y1_coord = parse_coord(y1)?;
1608 let x2_coord = parse_coord(x2)?;
1609 let y2_coord = parse_coord(y2)?;
1610
1611 let mut pcb = open_pcbdoc(path)?;
1612
1613 let region = crate::records::pcb::PcbRegion {
1615 common: crate::records::pcb::PcbPrimitiveCommon {
1616 layer: Layer::MULTI_LAYER,
1617 ..Default::default()
1618 },
1619 parameters: crate::types::ParameterCollection::new(),
1620 outline: vec![
1621 crate::types::CoordPoint {
1622 x: x1_coord,
1623 y: y1_coord,
1624 },
1625 crate::types::CoordPoint {
1626 x: x2_coord,
1627 y: y1_coord,
1628 },
1629 crate::types::CoordPoint {
1630 x: x2_coord,
1631 y: y2_coord,
1632 },
1633 crate::types::CoordPoint {
1634 x: x1_coord,
1635 y: y2_coord,
1636 },
1637 ],
1638 };
1639
1640 pcb.primitives
1641 .push(crate::records::pcb::PcbRecord::Region(region));
1642
1643 pcb.save_regions_to_file(path)
1645 .map_err(|e| format!("Error saving file: {:?}", e))?;
1646
1647 println!(
1648 "Added board cutout at ({:.3}mm, {:.3}mm) to ({:.3}mm, {:.3}mm)",
1649 x1_coord.to_mms(),
1650 y1_coord.to_mms(),
1651 x2_coord.to_mms(),
1652 y2_coord.to_mms()
1653 );
1654 let width_mm = ((x2_coord.to_raw() - x1_coord.to_raw()).abs() as f64) / 10000.0 * 0.0254;
1655 let height_mm = ((y2_coord.to_raw() - y1_coord.to_raw()).abs() as f64) / 10000.0 * 0.0254;
1656 println!(" Size: {:.3}mm x {:.3}mm", width_mm, height_mm);
1657
1658 Ok(())
1659}
1660
1661pub fn cmd_polygons(
1667 path: &Path,
1668 layer_filter: Option<String>,
1669 net_filter: Option<String>,
1670) -> Result<PcbDocPolygons, Box<dyn std::error::Error>> {
1671 let pcb = open_pcbdoc(path)?;
1672
1673 let mut polygons: Vec<(usize, &PcbPolygon)> = Vec::new();
1675 for (i, prim) in pcb.primitives.iter().enumerate() {
1676 if let crate::records::pcb::PcbRecord::Polygon(p) = prim {
1677 if let Some(ref filter) = layer_filter {
1679 let filter_layer = Layer::from_name(filter);
1680 if let Some(fl) = filter_layer {
1681 if p.layer != fl {
1682 continue;
1683 }
1684 }
1685 }
1686 if let Some(ref filter) = net_filter {
1687 if !p.net_name.eq_ignore_ascii_case(filter) {
1688 continue;
1689 }
1690 }
1691 polygons.push((i, p));
1692 }
1693 }
1694
1695 let polygon_summaries: Vec<PolygonSummary> = polygons
1696 .iter()
1697 .map(|(i, p)| PolygonSummary {
1698 index: *i,
1699 layer: p.layer.name().to_string(),
1700 net: p.net_name.clone(),
1701 vertex_count: p.vertices.len(),
1702 pour_over: p.pour_over,
1703 remove_dead: p.remove_dead,
1704 hatch_style: p.hatch_style.as_str().to_string(),
1705 })
1706 .collect();
1707
1708 Ok(PcbDocPolygons {
1709 path: path.display().to_string(),
1710 total_polygons: polygon_summaries.len(),
1711 layer_filter,
1712 net_filter,
1713 polygons: polygon_summaries,
1714 })
1715}
1716
1717pub fn cmd_polygon(
1719 path: &Path,
1720 index: usize,
1721) -> Result<PcbDocPolygonDetail, Box<dyn std::error::Error>> {
1722 let pcb = open_pcbdoc(path)?;
1723
1724 let mut polygon: Option<&PcbPolygon> = None;
1726 let mut poly_index = 0;
1727 for prim in &pcb.primitives {
1728 if let crate::records::pcb::PcbRecord::Polygon(p) = prim {
1729 if poly_index == index {
1730 polygon = Some(p);
1731 break;
1732 }
1733 poly_index += 1;
1734 }
1735 }
1736
1737 let p = polygon.ok_or_else(|| format!("Polygon index {} not found", index))?;
1738
1739 let vertices: Vec<PolygonVertexInfo> = p
1740 .vertices
1741 .iter()
1742 .map(|v| PolygonVertexInfo {
1743 x: format!("{:.4}mm", v.x.to_mms()),
1744 y: format!("{:.4}mm", v.y.to_mms()),
1745 kind: match v.kind {
1746 PolygonVertexKind::Line => "line".to_string(),
1747 PolygonVertexKind::Arc => "arc".to_string(),
1748 },
1749 })
1750 .collect();
1751
1752 Ok(PcbDocPolygonDetail {
1753 index,
1754 layer: p.layer.name().to_string(),
1755 net: p.net_name.clone(),
1756 vertex_count: p.vertices.len(),
1757 pour_over: p.pour_over,
1758 remove_dead: p.remove_dead,
1759 hatch_style: p.hatch_style.as_str().to_string(),
1760 vertices,
1761 })
1762}
1763
1764pub fn cmd_add_polygon(
1766 path: &Path,
1767 layer_str: &str,
1768 net: &str,
1769 vertices_str: &str,
1770 pour_over: bool,
1771 remove_dead: bool,
1772 hatch_style_str: &str,
1773) -> Result<(), String> {
1774 let layer = Layer::from_name(layer_str).ok_or_else(|| {
1775 format!(
1776 "Unknown layer: '{}'. Valid layers: TopLayer, BottomLayer, InternalPlane1, etc.",
1777 layer_str
1778 )
1779 })?;
1780
1781 let hatch_style = HatchStyle::parse(hatch_style_str);
1782
1783 let mut vertices: Vec<PolygonVertex> = Vec::new();
1785 for vertex_str in vertices_str.split_whitespace() {
1786 let parts: Vec<&str> = vertex_str.split(',').collect();
1787 if parts.len() != 2 {
1788 return Err(format!(
1789 "Invalid vertex format: '{}'. Expected 'x,y'",
1790 vertex_str
1791 ));
1792 }
1793 let x = parse_coord(parts[0])?;
1794 let y = parse_coord(parts[1])?;
1795 vertices.push(PolygonVertex {
1796 kind: PolygonVertexKind::Line,
1797 x,
1798 y,
1799 center_x: Coord::default(),
1800 center_y: Coord::default(),
1801 start_angle: 0.0,
1802 end_angle: 0.0,
1803 radius: Coord::default(),
1804 });
1805 }
1806
1807 if vertices.len() < 3 {
1808 return Err("At least 3 vertices are required to create a polygon".to_string());
1809 }
1810
1811 let vertex_count = vertices.len();
1812
1813 let mut pcb = open_pcbdoc(path)?;
1814
1815 let board = PcbBoard::from_params(&pcb.board_params);
1817
1818 let polygon = PcbPolygon {
1820 layer,
1821 net_name: net.to_string(),
1822 vertices,
1823 polygon_type: crate::records::pcb::PolygonType::Polygon,
1824 hatch_style,
1825 pour_over,
1826 remove_dead,
1827 grid_size: board.grid_size,
1828 track_width: board.track_width,
1829 use_octagons: board.use_octagons,
1830 min_prim_length: board.min_prim_length,
1831 locked: false,
1832 polygon_outline: false,
1833 user_routed: true,
1834 keepout: false,
1835 union_index: -1,
1836 primitive_lock: false,
1837 unique_id: String::new(),
1838 params: crate::types::ParameterCollection::new(),
1839 };
1840
1841 pcb.primitives
1842 .push(crate::records::pcb::PcbRecord::Polygon(polygon));
1843
1844 pcb.save_polygons_to_file(path)
1846 .map_err(|e| format!("Error saving file: {:?}", e))?;
1847
1848 println!(
1849 "Added polygon (copper pour) on {} for net '{}'",
1850 layer.name(),
1851 net
1852 );
1853 println!(" Vertices: {}", vertex_count);
1854 println!(" Hatch Style: {}", hatch_style_str);
1855 println!(" Pour Over: {}", pour_over);
1856 println!(" Remove Dead: {}", remove_dead);
1857
1858 Ok(())
1859}
1860
1861pub fn cmd_tracks(
1867 path: &Path,
1868 layer_filter: Option<String>,
1869) -> Result<PcbDocTracks, Box<dyn std::error::Error>> {
1870 let pcb = open_pcbdoc(path)?;
1871
1872 let layer = layer_filter.as_ref().and_then(|l| Layer::from_name(l));
1873
1874 let tracks: Vec<_> = pcb
1875 .iter_tracks()
1876 .filter(|t| layer.is_none_or(|l| t.common.layer == l))
1877 .collect();
1878
1879 let track_infos: Vec<TrackInfo> = tracks
1880 .iter()
1881 .enumerate()
1882 .map(|(i, t)| TrackInfo {
1883 index: i,
1884 layer: t.common.layer.name().to_string(),
1885 start_x: format!("{:.4}mm", t.start.x.to_mms()),
1886 start_y: format!("{:.4}mm", t.start.y.to_mms()),
1887 end_x: format!("{:.4}mm", t.end.x.to_mms()),
1888 end_y: format!("{:.4}mm", t.end.y.to_mms()),
1889 width: format!("{:.4}mm", t.width.to_mms()),
1890 net: String::new(),
1891 })
1892 .collect();
1893
1894 Ok(PcbDocTracks {
1895 path: path.display().to_string(),
1896 total_tracks: track_infos.len(),
1897 layer_filter,
1898 tracks: track_infos,
1899 })
1900}
1901
1902#[allow(clippy::too_many_arguments)]
1904pub fn cmd_add_track(
1905 path: &Path,
1906 start: Option<String>,
1907 end: Option<String>,
1908 start_pad: Option<String>,
1909 end_pad: Option<String>,
1910 width: Option<String>,
1911 layer_str: &str,
1912 net: Option<String>,
1913) -> Result<(), String> {
1914 use crate::edit::PcbEditSession;
1915
1916 let mut session =
1917 PcbEditSession::open(path).map_err(|e| format!("Error opening file: {:?}", e))?;
1918
1919 let layer =
1920 Layer::from_name(layer_str).ok_or_else(|| format!("Invalid layer: {}", layer_str))?;
1921 session.set_default_layer(layer);
1922
1923 let track_width = width.as_ref().map(|w| parse_coord(w)).transpose()?;
1924
1925 let start_point = if let Some(pad_ref) = &start_pad {
1927 let parts: Vec<&str> = pad_ref.split('.').collect();
1928 if parts.len() != 2 {
1929 return Err("Pad reference must be in format 'U1.1'".to_string());
1930 }
1931 session
1932 .resolve_position(&crate::edit::Position::RelativeToPad {
1933 component: parts[0].to_string(),
1934 pad: parts[1].to_string(),
1935 offset: CoordPoint::default(),
1936 })
1937 .map_err(|e| format!("Error resolving start pad: {:?}", e))?
1938 } else if let Some(start_str) = &start {
1939 let parts: Vec<&str> = start_str.split(',').collect();
1940 if parts.len() != 2 {
1941 return Err("Start position must be in format 'x,y'".to_string());
1942 }
1943 CoordPoint::new(parse_coord(parts[0])?, parse_coord(parts[1])?)
1944 } else {
1945 return Err("Either --start or --start-pad must be specified".to_string());
1946 };
1947
1948 let end_point = if let Some(pad_ref) = &end_pad {
1950 let parts: Vec<&str> = pad_ref.split('.').collect();
1951 if parts.len() != 2 {
1952 return Err("Pad reference must be in format 'U1.1'".to_string());
1953 }
1954 session
1955 .resolve_position(&crate::edit::Position::RelativeToPad {
1956 component: parts[0].to_string(),
1957 pad: parts[1].to_string(),
1958 offset: CoordPoint::default(),
1959 })
1960 .map_err(|e| format!("Error resolving end pad: {:?}", e))?
1961 } else if let Some(end_str) = &end {
1962 let parts: Vec<&str> = end_str.split(',').collect();
1963 if parts.len() != 2 {
1964 return Err("End position must be in format 'x,y'".to_string());
1965 }
1966 CoordPoint::new(parse_coord(parts[0])?, parse_coord(parts[1])?)
1967 } else {
1968 return Err("Either --end or --end-pad must be specified".to_string());
1969 };
1970
1971 let idx = session
1972 .add_track(
1973 start_point,
1974 end_point,
1975 track_width,
1976 Some(layer),
1977 net.as_deref(),
1978 )
1979 .map_err(|e| format!("Error adding track: {:?}", e))?;
1980
1981 session
1982 .save_to_original()
1983 .map_err(|e| format!("Error saving file: {:?}", e))?;
1984
1985 println!("Added track at index {}", idx);
1986 println!(" Layer: {}", layer.name());
1987 println!(
1988 " Start: ({:.3}mm, {:.3}mm)",
1989 start_point.x.to_mms(),
1990 start_point.y.to_mms()
1991 );
1992 println!(
1993 " End: ({:.3}mm, {:.3}mm)",
1994 end_point.x.to_mms(),
1995 end_point.y.to_mms()
1996 );
1997 println!(
1998 " Width: {:.3}mm",
1999 track_width.unwrap_or(Coord::from_mils(10.0)).to_mms()
2000 );
2001 if let Some(n) = &net {
2002 println!(" Net: {}", n);
2003 }
2004
2005 Ok(())
2006}
2007
2008pub fn cmd_add_track_path(
2010 path: &Path,
2011 vertices_str: &str,
2012 width: Option<String>,
2013 layer_str: &str,
2014 net: Option<String>,
2015) -> Result<(), String> {
2016 use crate::edit::PcbEditSession;
2017
2018 let mut session =
2019 PcbEditSession::open(path).map_err(|e| format!("Error opening file: {:?}", e))?;
2020
2021 let layer =
2022 Layer::from_name(layer_str).ok_or_else(|| format!("Invalid layer: {}", layer_str))?;
2023 session.set_default_layer(layer);
2024
2025 let track_width = width.as_ref().map(|w| parse_coord(w)).transpose()?;
2026
2027 let mut vertices = Vec::new();
2028 for vertex_str in vertices_str.split_whitespace() {
2029 let parts: Vec<&str> = vertex_str.split(',').collect();
2030 if parts.len() != 2 {
2031 return Err(format!(
2032 "Invalid vertex format '{}', expected 'x,y'",
2033 vertex_str
2034 ));
2035 }
2036 vertices.push(CoordPoint::new(
2037 parse_coord(parts[0])?,
2038 parse_coord(parts[1])?,
2039 ));
2040 }
2041
2042 if vertices.len() < 2 {
2043 return Err("At least 2 vertices are required for a track path".to_string());
2044 }
2045
2046 let indices = session
2047 .add_track_path(&vertices, track_width, Some(layer), net.as_deref())
2048 .map_err(|e| format!("Error adding track path: {:?}", e))?;
2049
2050 session
2051 .save_to_original()
2052 .map_err(|e| format!("Error saving file: {:?}", e))?;
2053
2054 println!(
2055 "Added {} track segments (indices {:?})",
2056 indices.len(),
2057 indices
2058 );
2059 println!(" Layer: {}", layer.name());
2060 println!(" Vertices: {}", vertices.len());
2061 println!(
2062 " Width: {:.3}mm",
2063 track_width.unwrap_or(Coord::from_mils(10.0)).to_mms()
2064 );
2065
2066 Ok(())
2067}
2068
2069pub fn cmd_vias(path: &Path) -> Result<PcbDocVias, Box<dyn std::error::Error>> {
2075 let pcb = open_pcbdoc(path)?;
2076
2077 let vias: Vec<_> = pcb.iter_vias().collect();
2078
2079 let via_infos: Vec<ViaInfo> = vias
2080 .iter()
2081 .enumerate()
2082 .map(|(i, v)| ViaInfo {
2083 index: i,
2084 x: format!("{:.4}mm", v.location.x.to_mms()),
2085 y: format!("{:.4}mm", v.location.y.to_mms()),
2086 diameter: format!("{:.4}mm", v.diameter().to_mms()),
2087 hole_size: format!("{:.4}mm", v.hole_size.to_mms()),
2088 from_layer: v.from_layer.name().to_string(),
2089 to_layer: v.to_layer.name().to_string(),
2090 net: String::new(),
2091 })
2092 .collect();
2093
2094 Ok(PcbDocVias {
2095 path: path.display().to_string(),
2096 total_vias: via_infos.len(),
2097 vias: via_infos,
2098 })
2099}
2100
2101#[allow(clippy::too_many_arguments)]
2103pub fn cmd_add_via(
2104 path: &Path,
2105 at: Option<String>,
2106 at_pad: Option<String>,
2107 diameter: Option<String>,
2108 hole: Option<String>,
2109 from_layer_str: &str,
2110 to_layer_str: &str,
2111 net: Option<String>,
2112) -> Result<(), String> {
2113 use crate::edit::PcbEditSession;
2114
2115 let mut session =
2116 PcbEditSession::open(path).map_err(|e| format!("Error opening file: {:?}", e))?;
2117
2118 let from_layer = Layer::from_name(from_layer_str)
2119 .ok_or_else(|| format!("Invalid from layer: {}", from_layer_str))?;
2120 let to_layer = Layer::from_name(to_layer_str)
2121 .ok_or_else(|| format!("Invalid to layer: {}", to_layer_str))?;
2122
2123 let via_diameter = diameter.as_ref().map(|d| parse_coord(d)).transpose()?;
2124 let via_hole = hole.as_ref().map(|h| parse_coord(h)).transpose()?;
2125
2126 let location = if let Some(pad_ref) = &at_pad {
2127 let parts: Vec<&str> = pad_ref.split('.').collect();
2128 if parts.len() != 2 {
2129 return Err("Pad reference must be in format 'U1.1'".to_string());
2130 }
2131 session
2132 .resolve_position(&crate::edit::Position::RelativeToPad {
2133 component: parts[0].to_string(),
2134 pad: parts[1].to_string(),
2135 offset: CoordPoint::default(),
2136 })
2137 .map_err(|e| format!("Error resolving pad: {:?}", e))?
2138 } else if let Some(at_str) = &at {
2139 let parts: Vec<&str> = at_str.split(',').collect();
2140 if parts.len() != 2 {
2141 return Err("Position must be in format 'x,y'".to_string());
2142 }
2143 CoordPoint::new(parse_coord(parts[0])?, parse_coord(parts[1])?)
2144 } else {
2145 return Err("Either --at or --at-pad must be specified".to_string());
2146 };
2147
2148 let idx = session
2149 .add_via(
2150 location,
2151 via_diameter,
2152 via_hole,
2153 Some(from_layer),
2154 Some(to_layer),
2155 net.as_deref(),
2156 )
2157 .map_err(|e| format!("Error adding via: {:?}", e))?;
2158
2159 session
2160 .save_to_original()
2161 .map_err(|e| format!("Error saving file: {:?}", e))?;
2162
2163 println!("Added via at index {}", idx);
2164 println!(
2165 " Position: ({:.3}mm, {:.3}mm)",
2166 location.x.to_mms(),
2167 location.y.to_mms()
2168 );
2169 println!(
2170 " Diameter: {:.3}mm",
2171 via_diameter.unwrap_or(Coord::from_mils(50.0)).to_mms()
2172 );
2173 println!(
2174 " Hole: {:.3}mm",
2175 via_hole.unwrap_or(Coord::from_mils(28.0)).to_mms()
2176 );
2177 println!(" Layers: {} -> {}", from_layer.name(), to_layer.name());
2178
2179 Ok(())
2180}
2181
2182pub fn cmd_arcs(
2188 path: &Path,
2189 layer_filter: Option<String>,
2190) -> Result<PcbDocArcs, Box<dyn std::error::Error>> {
2191 let pcb = open_pcbdoc(path)?;
2192
2193 let layer = layer_filter.as_ref().and_then(|l| Layer::from_name(l));
2194
2195 let arcs: Vec<_> = pcb
2196 .iter_arcs()
2197 .filter(|a| layer.is_none_or(|l| a.common.layer == l))
2198 .collect();
2199
2200 let arc_infos: Vec<ArcInfo> = arcs
2201 .iter()
2202 .enumerate()
2203 .map(|(i, a)| ArcInfo {
2204 index: i,
2205 layer: a.common.layer.name().to_string(),
2206 center_x: format!("{:.4}mm", a.location.x.to_mms()),
2207 center_y: format!("{:.4}mm", a.location.y.to_mms()),
2208 radius: format!("{:.4}mm", a.radius.to_mms()),
2209 start_angle: a.start_angle,
2210 end_angle: a.end_angle,
2211 width: format!("{:.4}mm", a.width.to_mms()),
2212 net: String::new(),
2213 })
2214 .collect();
2215
2216 Ok(PcbDocArcs {
2217 path: path.display().to_string(),
2218 total_arcs: arc_infos.len(),
2219 layer_filter,
2220 arcs: arc_infos,
2221 })
2222}
2223
2224#[allow(clippy::too_many_arguments)]
2226pub fn cmd_add_arc(
2227 path: &Path,
2228 center_str: &str,
2229 radius_str: &str,
2230 start_angle: f64,
2231 end_angle: f64,
2232 width: Option<String>,
2233 layer_str: &str,
2234 net: Option<String>,
2235) -> Result<(), String> {
2236 use crate::edit::PcbEditSession;
2237
2238 let mut session =
2239 PcbEditSession::open(path).map_err(|e| format!("Error opening file: {:?}", e))?;
2240
2241 let layer =
2242 Layer::from_name(layer_str).ok_or_else(|| format!("Invalid layer: {}", layer_str))?;
2243 session.set_default_layer(layer);
2244
2245 let center_parts: Vec<&str> = center_str.split(',').collect();
2246 if center_parts.len() != 2 {
2247 return Err("Center must be in format 'x,y'".to_string());
2248 }
2249 let center = CoordPoint::new(parse_coord(center_parts[0])?, parse_coord(center_parts[1])?);
2250 let radius = parse_coord(radius_str)?;
2251 let arc_width = width.as_ref().map(|w| parse_coord(w)).transpose()?;
2252
2253 let idx = session
2254 .add_arc(
2255 center,
2256 radius,
2257 start_angle,
2258 end_angle,
2259 arc_width,
2260 Some(layer),
2261 net.as_deref(),
2262 )
2263 .map_err(|e| format!("Error adding arc: {:?}", e))?;
2264
2265 session
2266 .save_to_original()
2267 .map_err(|e| format!("Error saving file: {:?}", e))?;
2268
2269 println!("Added arc at index {}", idx);
2270 println!(" Layer: {}", layer.name());
2271 println!(
2272 " Center: ({:.3}mm, {:.3}mm)",
2273 center.x.to_mms(),
2274 center.y.to_mms()
2275 );
2276 println!(" Radius: {:.3}mm", radius.to_mms());
2277 println!(" Angles: {:.1}deg - {:.1}deg", start_angle, end_angle);
2278 println!(
2279 " Width: {:.3}mm",
2280 arc_width.unwrap_or(Coord::from_mils(10.0)).to_mms()
2281 );
2282
2283 Ok(())
2284}
2285
2286pub fn cmd_fills(
2292 path: &Path,
2293 layer_filter: Option<String>,
2294) -> Result<PcbDocFills, Box<dyn std::error::Error>> {
2295 let pcb = open_pcbdoc(path)?;
2296
2297 let layer = layer_filter.as_ref().and_then(|l| Layer::from_name(l));
2298
2299 let fills: Vec<_> = pcb
2300 .iter_fills()
2301 .filter(|f| layer.is_none_or(|l| f.base.common.layer == l))
2302 .collect();
2303
2304 let fill_infos: Vec<FillInfo> = fills
2305 .iter()
2306 .enumerate()
2307 .map(|(i, f)| FillInfo {
2308 index: i,
2309 layer: f.base.common.layer.name().to_string(),
2310 x1: format!("{:.4}mm", f.base.corner1.x.to_mms()),
2311 y1: format!("{:.4}mm", f.base.corner1.y.to_mms()),
2312 x2: format!("{:.4}mm", f.base.corner2.x.to_mms()),
2313 y2: format!("{:.4}mm", f.base.corner2.y.to_mms()),
2314 rotation: f.base.rotation,
2315 net: String::new(),
2316 })
2317 .collect();
2318
2319 Ok(PcbDocFills {
2320 path: path.display().to_string(),
2321 total_fills: fill_infos.len(),
2322 layer_filter,
2323 fills: fill_infos,
2324 })
2325}
2326
2327pub fn cmd_add_fill(
2329 path: &Path,
2330 x1y1_str: &str,
2331 x2y2_str: &str,
2332 layer_str: &str,
2333 rotation: f64,
2334 net: Option<String>,
2335) -> Result<(), String> {
2336 use crate::edit::PcbEditSession;
2337
2338 let mut session =
2339 PcbEditSession::open(path).map_err(|e| format!("Error opening file: {:?}", e))?;
2340
2341 let layer =
2342 Layer::from_name(layer_str).ok_or_else(|| format!("Invalid layer: {}", layer_str))?;
2343
2344 let parts1: Vec<&str> = x1y1_str.split(',').collect();
2345 let parts2: Vec<&str> = x2y2_str.split(',').collect();
2346 if parts1.len() != 2 || parts2.len() != 2 {
2347 return Err("Coordinates must be in format 'x,y'".to_string());
2348 }
2349
2350 let corner1 = CoordPoint::new(parse_coord(parts1[0])?, parse_coord(parts1[1])?);
2351 let corner2 = CoordPoint::new(parse_coord(parts2[0])?, parse_coord(parts2[1])?);
2352
2353 let idx = session
2354 .add_fill(
2355 corner1,
2356 corner2,
2357 Some(layer),
2358 Some(rotation),
2359 net.as_deref(),
2360 )
2361 .map_err(|e| format!("Error adding fill: {:?}", e))?;
2362
2363 session
2364 .save_to_original()
2365 .map_err(|e| format!("Error saving file: {:?}", e))?;
2366
2367 println!("Added fill at index {}", idx);
2368 println!(" Layer: {}", layer.name());
2369 println!(
2370 " Corner 1: ({:.3}mm, {:.3}mm)",
2371 corner1.x.to_mms(),
2372 corner1.y.to_mms()
2373 );
2374 println!(
2375 " Corner 2: ({:.3}mm, {:.3}mm)",
2376 corner2.x.to_mms(),
2377 corner2.y.to_mms()
2378 );
2379 println!(" Rotation: {:.1}deg", rotation);
2380
2381 Ok(())
2382}
2383
2384pub fn cmd_texts(
2390 path: &Path,
2391 layer_filter: Option<String>,
2392) -> Result<PcbDocTexts, Box<dyn std::error::Error>> {
2393 let pcb = open_pcbdoc(path)?;
2394
2395 let layer = layer_filter.as_ref().and_then(|l| Layer::from_name(l));
2396
2397 let texts: Vec<_> = pcb
2398 .primitives
2399 .iter()
2400 .filter_map(|p| {
2401 if let crate::records::pcb::PcbRecord::Text(t) = p {
2402 if layer.is_none_or(|l| t.base.common.layer == l) {
2403 Some(t)
2404 } else {
2405 None
2406 }
2407 } else {
2408 None
2409 }
2410 })
2411 .collect();
2412
2413 let text_infos: Vec<TextInfo> = texts
2414 .iter()
2415 .enumerate()
2416 .map(|(i, t)| TextInfo {
2417 index: i,
2418 text: t.text.clone(),
2419 layer: t.base.common.layer.name().to_string(),
2420 x: format!("{:.4}mm", t.base.corner1.x.to_mms()),
2421 y: format!("{:.4}mm", t.base.corner1.y.to_mms()),
2422 height: format!("{:.4}mm", t.height().to_mms()),
2423 rotation: t.base.rotation,
2424 })
2425 .collect();
2426
2427 Ok(PcbDocTexts {
2428 path: path.display().to_string(),
2429 total_texts: text_infos.len(),
2430 layer_filter,
2431 texts: text_infos,
2432 })
2433}
2434
2435pub fn cmd_add_text(
2437 path: &Path,
2438 text: &str,
2439 at_str: &str,
2440 height: Option<String>,
2441 layer_str: &str,
2442 rotation: f64,
2443) -> Result<(), String> {
2444 use crate::edit::PcbEditSession;
2445
2446 let mut session =
2447 PcbEditSession::open(path).map_err(|e| format!("Error opening file: {:?}", e))?;
2448
2449 let layer =
2450 Layer::from_name(layer_str).ok_or_else(|| format!("Invalid layer: {}", layer_str))?;
2451
2452 let parts: Vec<&str> = at_str.split(',').collect();
2453 if parts.len() != 2 {
2454 return Err("Position must be in format 'x,y'".to_string());
2455 }
2456
2457 let location = CoordPoint::new(parse_coord(parts[0])?, parse_coord(parts[1])?);
2458 let text_height = height
2459 .as_ref()
2460 .map(|h| parse_coord(h))
2461 .transpose()?
2462 .unwrap_or(Coord::from_mms(1.0));
2463
2464 let idx = session
2465 .add_text(
2466 text,
2467 location,
2468 text_height,
2469 Some(layer),
2470 Some(rotation),
2471 None,
2472 )
2473 .map_err(|e| format!("Error adding text: {:?}", e))?;
2474
2475 session
2476 .save_to_original()
2477 .map_err(|e| format!("Error saving file: {:?}", e))?;
2478
2479 println!("Added text at index {}", idx);
2480 println!(" Text: \"{}\"", text);
2481 println!(" Layer: {}", layer.name());
2482 println!(
2483 " Position: ({:.3}mm, {:.3}mm)",
2484 location.x.to_mms(),
2485 location.y.to_mms()
2486 );
2487 println!(" Height: {:.3}mm", text_height.to_mms());
2488 println!(" Rotation: {:.1}deg", rotation);
2489
2490 Ok(())
2491}
2492
2493pub fn cmd_regions(
2499 path: &Path,
2500 layer_filter: Option<String>,
2501) -> Result<PcbDocRegions, Box<dyn std::error::Error>> {
2502 let pcb = open_pcbdoc(path)?;
2503
2504 let layer = layer_filter.as_ref().and_then(|l| Layer::from_name(l));
2505
2506 let regions: Vec<_> = pcb
2507 .iter_regions()
2508 .filter(|r| layer.is_none_or(|l| r.common.layer == l))
2509 .collect();
2510
2511 let region_infos: Vec<RegionInfo> = regions
2512 .iter()
2513 .enumerate()
2514 .map(|(i, r)| {
2515 RegionInfo {
2516 index: i,
2517 layer: r.common.layer.name().to_string(),
2518 vertex_count: r.outline.len(),
2519 is_keepout: r.common.is_keepout(),
2520 net: String::new(), }
2522 })
2523 .collect();
2524
2525 Ok(PcbDocRegions {
2526 path: path.display().to_string(),
2527 total_regions: region_infos.len(),
2528 layer_filter,
2529 regions: region_infos,
2530 })
2531}
2532
2533pub fn cmd_add_region(
2535 path: &Path,
2536 vertices_str: &str,
2537 layer_str: &str,
2538 keepout: bool,
2539 net: Option<String>,
2540) -> Result<(), String> {
2541 use crate::edit::PcbEditSession;
2542
2543 let mut session =
2544 PcbEditSession::open(path).map_err(|e| format!("Error opening file: {:?}", e))?;
2545
2546 let layer =
2547 Layer::from_name(layer_str).ok_or_else(|| format!("Invalid layer: {}", layer_str))?;
2548
2549 let mut vertices = Vec::new();
2550 for vertex_str in vertices_str.split_whitespace() {
2551 let parts: Vec<&str> = vertex_str.split(',').collect();
2552 if parts.len() != 2 {
2553 return Err(format!(
2554 "Invalid vertex format '{}', expected 'x,y'",
2555 vertex_str
2556 ));
2557 }
2558 vertices.push(CoordPoint::new(
2559 parse_coord(parts[0])?,
2560 parse_coord(parts[1])?,
2561 ));
2562 }
2563
2564 if vertices.len() < 3 {
2565 return Err("At least 3 vertices are required for a region".to_string());
2566 }
2567
2568 let idx = session
2569 .add_region(&vertices, layer, keepout, net.as_deref())
2570 .map_err(|e| format!("Error adding region: {:?}", e))?;
2571
2572 session
2573 .save_to_original()
2574 .map_err(|e| format!("Error saving file: {:?}", e))?;
2575
2576 let region_type = if keepout {
2577 "keepout region"
2578 } else {
2579 "copper region"
2580 };
2581 println!("Added {} at index {}", region_type, idx);
2582 println!(" Layer: {}", layer.name());
2583 println!(" Vertices: {}", vertices.len());
2584
2585 Ok(())
2586}
2587
2588pub fn cmd_delete_primitive(path: &Path, index: usize) -> Result<(), String> {
2594 use crate::edit::PcbEditSession;
2595
2596 let mut session =
2597 PcbEditSession::open(path).map_err(|e| format!("Error opening file: {:?}", e))?;
2598
2599 session
2600 .delete_primitive(index)
2601 .map_err(|e| format!("Error deleting primitive: {:?}", e))?;
2602
2603 session
2604 .save_to_original()
2605 .map_err(|e| format!("Error saving file: {:?}", e))?;
2606
2607 println!("Deleted primitive at index {}", index);
2608
2609 Ok(())
2610}
2611
2612pub fn cmd_delete_tracks(path: &Path, layer_str: &str) -> Result<(), String> {
2614 use crate::edit::PcbEditSession;
2615
2616 let mut session =
2617 PcbEditSession::open(path).map_err(|e| format!("Error opening file: {:?}", e))?;
2618
2619 let layer =
2620 Layer::from_name(layer_str).ok_or_else(|| format!("Invalid layer: {}", layer_str))?;
2621
2622 let count = session
2623 .delete_tracks_on_layer(layer)
2624 .map_err(|e| format!("Error deleting tracks: {:?}", e))?;
2625
2626 session
2627 .save_to_original()
2628 .map_err(|e| format!("Error saving file: {:?}", e))?;
2629
2630 println!("Deleted {} tracks on {}", count, layer.name());
2631
2632 Ok(())
2633}
2634
2635pub fn cmd_delete_vias(path: &Path) -> Result<(), String> {
2637 use crate::edit::PcbEditSession;
2638
2639 let mut session =
2640 PcbEditSession::open(path).map_err(|e| format!("Error opening file: {:?}", e))?;
2641
2642 let count = session
2643 .delete_all_vias()
2644 .map_err(|e| format!("Error deleting vias: {:?}", e))?;
2645
2646 session
2647 .save_to_original()
2648 .map_err(|e| format!("Error saving file: {:?}", e))?;
2649
2650 println!("Deleted {} vias", count);
2651
2652 Ok(())
2653}
2654
2655pub fn cmd_nets(path: &Path) -> Result<PcbDocNets, Box<dyn std::error::Error>> {
2661 let pcb = open_pcbdoc(path)?;
2662
2663 Ok(PcbDocNets {
2664 path: path.display().to_string(),
2665 total_nets: pcb.nets.len(),
2666 nets: pcb.nets.clone(),
2667 })
2668}
2669
2670pub fn cmd_add_net(path: &Path, name: &str) -> Result<(), String> {
2672 use crate::edit::PcbEditSession;
2673
2674 let mut session =
2675 PcbEditSession::open(path).map_err(|e| format!("Error opening file: {:?}", e))?;
2676
2677 session
2678 .add_net(name)
2679 .map_err(|e| format!("Error adding net: {:?}", e))?;
2680
2681 session
2682 .save_to_original()
2683 .map_err(|e| format!("Error saving file: {:?}", e))?;
2684
2685 println!("Added net '{}'", name);
2686
2687 Ok(())
2688}