@hybridlabor-api/bdb-hardware-pcb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/README.md +116 -0
  2. package/config/mcp/antigravity.json +22 -0
  3. package/config/mcp/claude.json +22 -0
  4. package/config/mcp/codex.toml +25 -0
  5. package/docs/adr/ADR-001-KICAD-OPENSCAD-MCP-STRATEGY.md +334 -0
  6. package/docs/review_documentation.md +121 -0
  7. package/installer.js +358 -0
  8. package/mcp_servers/kicad-mcp-server/.env.example +22 -0
  9. package/mcp_servers/kicad-mcp-server/.github/workflows/ci.yml +36 -0
  10. package/mcp_servers/kicad-mcp-server/CLAUDE.md +487 -0
  11. package/mcp_servers/kicad-mcp-server/README.md +316 -0
  12. package/mcp_servers/kicad-mcp-server/docs/DEVICE_TREE.md +416 -0
  13. package/mcp_servers/kicad-mcp-server/docs/INSTALLATION.md +332 -0
  14. package/mcp_servers/kicad-mcp-server/docs/PIN_ANALYSIS.md +332 -0
  15. package/mcp_servers/kicad-mcp-server/docs/README.md +240 -0
  16. package/mcp_servers/kicad-mcp-server/docs/TESTING.md +613 -0
  17. package/mcp_servers/kicad-mcp-server/docs/VALIDATION.md +268 -0
  18. package/mcp_servers/kicad-mcp-server/pyproject.toml +96 -0
  19. package/mcp_servers/kicad-mcp-server/requirements-dev.txt +16 -0
  20. package/mcp_servers/kicad-mcp-server/requirements-test.txt +24 -0
  21. package/mcp_servers/kicad-mcp-server/requirements.txt +13 -0
  22. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__init__.py +3 -0
  23. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__main__.py +17 -0
  24. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/config.py +46 -0
  25. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/__init__.py +1 -0
  26. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/types.py +87 -0
  27. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/__init__.py +1 -0
  28. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/netlist_parser.py +234 -0
  29. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser.py +375 -0
  30. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser_kicad.py +327 -0
  31. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/schematic_parser.py +902 -0
  32. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/server.py +71 -0
  33. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/__init__.py +1 -0
  34. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/arduino/connectivity_test.cpp.j2 +189 -0
  35. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/atmega.dts.j2 +77 -0
  36. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/esp32.dts.j2 +77 -0
  37. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/nrf52.dts.j2 +77 -0
  38. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/stm32f4.dts.j2 +89 -0
  39. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/esp_idf/test_suite.c.j2 +340 -0
  40. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/pytest/test_connectivity.py.j2 +147 -0
  41. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/st_hal/hal_test.c.j2 +313 -0
  42. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_gpio_test.py.j2 +99 -0
  43. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_i2c_test.py.j2 +117 -0
  44. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_pinmux_test.py.j2 +43 -0
  45. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_spi_test.py.j2 +94 -0
  46. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_gpio_test.c.j2 +113 -0
  47. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_i2c_test.c.j2 +101 -0
  48. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_spi_test.c.j2 +94 -0
  49. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/unittest/test_schematic.py.j2 +172 -0
  50. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/__init__.py +36 -0
  51. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/device_tree.py +1187 -0
  52. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/hierarchical_analysis.py +211 -0
  53. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/netlist.py +320 -0
  54. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/parts_registry.py +142 -0
  55. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb.py +955 -0
  56. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb_layout.py +308 -0
  57. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pin_analysis.py +765 -0
  58. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/project.py +196 -0
  59. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic.py +319 -0
  60. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_editor.py +674 -0
  61. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_search.py +158 -0
  62. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/validation.py +866 -0
  63. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/visualization.py +225 -0
  64. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/__init__.py +1 -0
  65. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/file_handlers.py +65 -0
  66. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_cli.py +103 -0
  67. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_version.py +103 -0
  68. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/parts_registry.py +197 -0
  69. package/mcp_servers/kicad-mcp-server/tests/__init__.py +1 -0
  70. package/mcp_servers/kicad-mcp-server/tests/examples/ESP32S3_TEST.md +219 -0
  71. package/mcp_servers/kicad-mcp-server/tests/fixtures/README.md +65 -0
  72. package/mcp_servers/kicad-mcp-server/tests/fixtures/__init__.py +1 -0
  73. package/mcp_servers/kicad-mcp-server/tests/fixtures/example_pcb.kicad_pcb +177 -0
  74. package/mcp_servers/kicad-mcp-server/tests/fixtures/example_schematic.kicad_sch +145 -0
  75. package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/child.kicad_sch +24 -0
  76. package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/root.kicad_sch +38 -0
  77. package/mcp_servers/kicad-mcp-server/tests/test_tools/__init__.py +1 -0
  78. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_hierarchical_labels.py +195 -0
  79. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_kicad_cli.py +116 -0
  80. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_netlist_cache_path.py +29 -0
  81. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic.py +189 -0
  82. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic_hierarchy.py +69 -0
  83. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_visualization.py +136 -0
  84. package/mcp_servers/kicad-mcp-server/uv.lock +2873 -0
  85. package/mcp_servers/openscad-mcp-server/.dockerignore +9 -0
  86. package/mcp_servers/openscad-mcp-server/.github/workflows/test.yml +40 -0
  87. package/mcp_servers/openscad-mcp-server/Dockerfile +29 -0
  88. package/mcp_servers/openscad-mcp-server/LICENSE +21 -0
  89. package/mcp_servers/openscad-mcp-server/README.md +154 -0
  90. package/mcp_servers/openscad-mcp-server/docs/audit.md +56 -0
  91. package/mcp_servers/openscad-mcp-server/docs/docker.md +66 -0
  92. package/mcp_servers/openscad-mcp-server/docs/issue-followup.md +19 -0
  93. package/mcp_servers/openscad-mcp-server/docs/jetson.md +17 -0
  94. package/mcp_servers/openscad-mcp-server/glama.json +4 -0
  95. package/mcp_servers/openscad-mcp-server/legacy/README.md +15 -0
  96. package/mcp_servers/openscad-mcp-server/legacy/README.original.md +294 -0
  97. package/mcp_servers/openscad-mcp-server/legacy/implementation_plan.md +100 -0
  98. package/mcp_servers/openscad-mcp-server/legacy/old/download_sam2_checkpoint.py +115 -0
  99. package/mcp_servers/openscad-mcp-server/legacy/old/src/ai/sam_segmentation.py +209 -0
  100. package/mcp_servers/openscad-mcp-server/legacy/old/src/models/threestudio_generator.py +231 -0
  101. package/mcp_servers/openscad-mcp-server/legacy/old/src/workflow/image_to_model_pipeline.py +260 -0
  102. package/mcp_servers/openscad-mcp-server/legacy/old/test_sam2_segmentation.py +96 -0
  103. package/mcp_servers/openscad-mcp-server/legacy/requirements.txt +57 -0
  104. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/README.md +39 -0
  105. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/ai-driven-code-generation.md +122 -0
  106. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/export-formats.md +76 -0
  107. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/ai/ai_service.py.md +51 -0
  108. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/main.py.md +63 -0
  109. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/models/code_generator.py.md +63 -0
  110. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/nlp/parameter_extractor.py.md +63 -0
  111. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/ai/natural-language-processing.md +78 -0
  112. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/nlp/parameter-extraction.md +173 -0
  113. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/export-formats.md +91 -0
  114. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/openscad-basics.md +66 -0
  115. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/primitive-testing.md +79 -0
  116. package/mcp_servers/openscad-mcp-server/legacy/src/__init__.py +0 -0
  117. package/mcp_servers/openscad-mcp-server/legacy/src/ai/ai_service.py +257 -0
  118. package/mcp_servers/openscad-mcp-server/legacy/src/ai/gemini_api.py +161 -0
  119. package/mcp_servers/openscad-mcp-server/legacy/src/ai/venice_api.py +203 -0
  120. package/mcp_servers/openscad-mcp-server/legacy/src/config.py +121 -0
  121. package/mcp_servers/openscad-mcp-server/legacy/src/main.py +1456 -0
  122. package/mcp_servers/openscad-mcp-server/legacy/src/main.py.new +404 -0
  123. package/mcp_servers/openscad-mcp-server/legacy/src/main_remote.py +401 -0
  124. package/mcp_servers/openscad-mcp-server/legacy/src/models/__init__.py +0 -0
  125. package/mcp_servers/openscad-mcp-server/legacy/src/models/code_generator.py +321 -0
  126. package/mcp_servers/openscad-mcp-server/legacy/src/models/cuda_mvs.py +209 -0
  127. package/mcp_servers/openscad-mcp-server/legacy/src/models/scad_templates/basic_shapes.scad +144 -0
  128. package/mcp_servers/openscad-mcp-server/legacy/src/nlp/__init__.py +0 -0
  129. package/mcp_servers/openscad-mcp-server/legacy/src/nlp/parameter_extractor.py +388 -0
  130. package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/__init__.py +0 -0
  131. package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/wrapper.py +418 -0
  132. package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/__init__.py +1 -0
  133. package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/printer_discovery.py +471 -0
  134. package/mcp_servers/openscad-mcp-server/legacy/src/remote/connection_manager.py +537 -0
  135. package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_client.py +435 -0
  136. package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_server.py +787 -0
  137. package/mcp_servers/openscad-mcp-server/legacy/src/remote/error_handling.py +415 -0
  138. package/mcp_servers/openscad-mcp-server/legacy/src/testing/__init__.py +0 -0
  139. package/mcp_servers/openscad-mcp-server/legacy/src/testing/primitive_tester.py +203 -0
  140. package/mcp_servers/openscad-mcp-server/legacy/src/testing/test_primitives.py +98 -0
  141. package/mcp_servers/openscad-mcp-server/legacy/src/utils/__init__.py +1 -0
  142. package/mcp_servers/openscad-mcp-server/legacy/src/utils/cad_exporter.py +241 -0
  143. package/mcp_servers/openscad-mcp-server/legacy/src/utils/format_validator.py +206 -0
  144. package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_exporter.py +140 -0
  145. package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_repair.py +91 -0
  146. package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_validator.py +123 -0
  147. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/__init__.py +0 -0
  148. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/headless_renderer.py +52 -0
  149. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/renderer.py +177 -0
  150. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/web_interface.py +639 -0
  151. package/mcp_servers/openscad-mcp-server/legacy/src/workflow/image_approval.py +148 -0
  152. package/mcp_servers/openscad-mcp-server/legacy/src/workflow/multi_view_to_model_pipeline.py +338 -0
  153. package/mcp_servers/openscad-mcp-server/legacy/test_complete_workflow.py +374 -0
  154. package/mcp_servers/openscad-mcp-server/legacy/test_cuda_mvs.py +191 -0
  155. package/mcp_servers/openscad-mcp-server/legacy/test_gemini_api.py +168 -0
  156. package/mcp_servers/openscad-mcp-server/legacy/test_image_approval.py +192 -0
  157. package/mcp_servers/openscad-mcp-server/legacy/test_image_approval_workflow.py +251 -0
  158. package/mcp_servers/openscad-mcp-server/legacy/test_image_to_model_pipeline.py +145 -0
  159. package/mcp_servers/openscad-mcp-server/legacy/test_model_selection.py +41 -0
  160. package/mcp_servers/openscad-mcp-server/legacy/test_multi_view_pipeline.py +290 -0
  161. package/mcp_servers/openscad-mcp-server/legacy/test_primitives.sh +13 -0
  162. package/mcp_servers/openscad-mcp-server/legacy/test_rabbit_direct.py +71 -0
  163. package/mcp_servers/openscad-mcp-server/legacy/test_remote_cuda_mvs.py +283 -0
  164. package/mcp_servers/openscad-mcp-server/legacy/test_venice_example.py +69 -0
  165. package/mcp_servers/openscad-mcp-server/pyproject.toml +33 -0
  166. package/mcp_servers/openscad-mcp-server/requirements.txt +2 -0
  167. package/mcp_servers/openscad-mcp-server/scad/simple_cube.scad +2 -0
  168. package/mcp_servers/openscad-mcp-server/scripts/test_docker.py +220 -0
  169. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__init__.py +3 -0
  170. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__main__.py +3 -0
  171. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/engine.py +94 -0
  172. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/geometry.py +242 -0
  173. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/server.py +245 -0
  174. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/service.py +190 -0
  175. package/mcp_servers/openscad-mcp-server/tests/conftest.py +17 -0
  176. package/mcp_servers/openscad-mcp-server/tests/test_engine.py +37 -0
  177. package/mcp_servers/openscad-mcp-server/tests/test_geometry.py +106 -0
  178. package/mcp_servers/openscad-mcp-server/tests/test_integration.py +172 -0
  179. package/mcp_servers/openscad-mcp-server/tests/test_transports.py +314 -0
  180. package/mcp_servers/openscad-mcp-server/uv.lock +1123 -0
  181. package/package.json +44 -0
  182. package/scripts/install_mcps.sh +86 -0
  183. package/scripts/openscad_wrapper.sh +62 -0
  184. package/scripts/run_kicad_mcp.sh +25 -0
  185. package/scripts/run_openscad_mcp.sh +41 -0
  186. package/scripts/test_mcp_connection.py +626 -0
  187. package/scripts/test_mcp_connection.sh +168 -0
  188. package/skills/code-first-hardware-design/SKILL.md +260 -0
  189. package/skills/pcb-constraint-definition/SKILL.md +236 -0
  190. package/skills/pcb-layout-routing-automation/SKILL.md +153 -0
  191. package/skills/pcb-validation-dfm-signoff/SKILL.md +194 -0
  192. package/skills/schematic-datasheet-analysis/SKILL.md +202 -0
@@ -0,0 +1,327 @@
1
+ """PCB file parser using KiCad Python API (pcbnew)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+ from ..utils.file_handlers import validate_kicad_file
9
+
10
+ # pcbnew returns user-facing layer names (TOP, BOTTOM, L2 INNER, etc.)
11
+ # but users typically reference canonical names (F.Cu, B.Cu, In1.Cu, etc.)
12
+ _CANONICAL_LAYER_MAP: dict[str, str] = {
13
+ "TOP": "F.Cu",
14
+ "BOTTOM": "B.Cu",
15
+ }
16
+ _INNER_RE = __import__("re").compile(r"^L(\d+)\s+INNER$")
17
+
18
+
19
+ def _canonicalize_layer(name: str) -> str:
20
+ """Convert pcbnew layer name to canonical KiCad name."""
21
+ if name in _CANONICAL_LAYER_MAP:
22
+ return _CANONICAL_LAYER_MAP[name]
23
+ m = _INNER_RE.match(name)
24
+ if m:
25
+ # L2 INNER -> In1.Cu (pcbnew L2 = 1st inner = In1.Cu)
26
+ return f"In{int(m.group(1)) - 1}.Cu"
27
+ # Handle "L2 LNNER" typo from some KiCad versions
28
+ m2 = __import__("re").match(r"^L(\d+)\s+LNNER$", name)
29
+ if m2:
30
+ return f"In{int(m2.group(1)) - 1}.Cu"
31
+ return name
32
+
33
+
34
+ @dataclass
35
+ class PCBFootprint:
36
+ """Footprint from PCB file."""
37
+
38
+ reference: str
39
+ value: str
40
+ library: str
41
+ position: tuple[float, float]
42
+ rotation: float
43
+ pads_count: int
44
+ layer: str
45
+ properties: dict[str, str] = field(default_factory=dict)
46
+
47
+
48
+ @dataclass
49
+ class PCBNet:
50
+ """Net from PCB file."""
51
+
52
+ name: str
53
+ code: int
54
+ node_count: int = 0
55
+
56
+
57
+ @dataclass
58
+ class PCBTrack:
59
+ """Track (segment) from PCB file."""
60
+
61
+ start: tuple[float, float]
62
+ end: tuple[float, float]
63
+ width: float
64
+ layer: str
65
+ net: str = ""
66
+ length: float = 0.0
67
+
68
+
69
+ @dataclass
70
+ class PCBVia:
71
+ """Via from PCB file."""
72
+
73
+ position: tuple[float, float]
74
+ size: float
75
+ drill: float
76
+ top_layer: str
77
+ bottom_layer: str
78
+ net: str = ""
79
+
80
+
81
+ @dataclass
82
+ class PCBZone:
83
+ """Copper zone from PCB file."""
84
+
85
+ net_name: str
86
+ layer: str
87
+ filled: bool
88
+
89
+
90
+ class PCBParserKiCad:
91
+ """PCB file parser using KiCad's official Python API (pcbnew)."""
92
+
93
+ def __init__(self, file_path: str) -> None:
94
+ self.file_path = validate_kicad_file(file_path, ".kicad_pcb")
95
+
96
+ try:
97
+ import pcbnew
98
+ except ImportError as e:
99
+ raise ImportError(
100
+ "pcbnew module not found. Please install KiCad or ensure it's in PATH.\n"
101
+ f"Error: {e}"
102
+ ) from e
103
+
104
+ self.board = pcbnew.LoadBoard(str(self.file_path))
105
+ self._pcbnew = pcbnew
106
+
107
+ def _to_mm(self, iu_value: int | float) -> float:
108
+ """Convert KiCad internal units (nm) to mm."""
109
+ return float(iu_value) / 1e6
110
+
111
+ def _pos_mm(self, pos) -> tuple[float, float]:
112
+ """Convert KiCad position to (x_mm, y_mm)."""
113
+ return (pos.x / 1e6, pos.y / 1e6)
114
+
115
+ def _layer_name(self, layer_id_or_name) -> str:
116
+ """Get canonical layer name (F.Cu, B.Cu, In1.Cu, etc.)."""
117
+ raw = self.board.GetLayerName(layer_id_or_name)
118
+ return _canonicalize_layer(raw)
119
+
120
+ # ── Footprints ──────────────────────────────────────────
121
+
122
+ def get_footprints(self) -> list[PCBFootprint]:
123
+ footprints = []
124
+ for fp in self.board.GetFootprints():
125
+ pos = fp.GetPosition()
126
+ fpid = fp.GetFPID()
127
+ library = fpid.GetLibItemName() if fpid.IsValid() else ""
128
+
129
+ properties: dict[str, str] = {}
130
+ for key in ("Reference", "Value", "Footprint", "Datasheet"):
131
+ try:
132
+ value = fp.GetProperty(key)
133
+ if value:
134
+ properties[key] = value
135
+ except Exception:
136
+ pass
137
+
138
+ footprints.append(PCBFootprint(
139
+ reference=fp.GetReference(),
140
+ value=fp.GetValue(),
141
+ library=library,
142
+ position=self._pos_mm(pos),
143
+ rotation=fp.GetOrientation().AsDegrees(),
144
+ pads_count=fp.GetPadCount(),
145
+ layer=_canonicalize_layer(fp.GetLayerName()),
146
+ properties=properties,
147
+ ))
148
+ return footprints
149
+
150
+ def get_footprint_by_reference(self, reference: str) -> PCBFootprint | None:
151
+ for footprint in self.get_footprints():
152
+ if footprint.reference == reference:
153
+ return footprint
154
+ return None
155
+
156
+ # ── Nets ────────────────────────────────────────────────
157
+
158
+ def get_nets(self) -> list[PCBNet]:
159
+ nets = []
160
+ net_info = self.board.GetNetInfo()
161
+ for _name, net in net_info.NetsByName().items():
162
+ name = str(_name)
163
+ if name:
164
+ nets.append(PCBNet(name=name, code=net.GetNetCode()))
165
+ return nets
166
+
167
+ # ── Tracks ──────────────────────────────────────────────
168
+
169
+ def get_tracks(self) -> list[PCBTrack]:
170
+ """Get all track segments (excludes vias)."""
171
+ pcbnew = self._pcbnew
172
+ tracks = []
173
+ for track in self.board.GetTracks():
174
+ if isinstance(track, pcbnew.PCB_VIA):
175
+ continue
176
+ tracks.append(PCBTrack(
177
+ start=self._pos_mm(track.GetStart()),
178
+ end=self._pos_mm(track.GetEnd()),
179
+ width=self._to_mm(track.GetWidth()),
180
+ layer=self._layer_name(track.GetLayer()),
181
+ net=track.GetNetname(),
182
+ length=self._to_mm(track.GetLength()),
183
+ ))
184
+ return tracks
185
+
186
+ def get_tracks_by_net(self, net_name: str) -> list[PCBTrack]:
187
+ """Get track segments for a specific net."""
188
+ return [t for t in self.get_tracks() if t.net == net_name]
189
+
190
+ # ── Vias ────────────────────────────────────────────────
191
+
192
+ def get_vias(self) -> list[PCBVia]:
193
+ """Get all vias (uses GetWidth(layer) to avoid headless assert)."""
194
+ pcbnew = self._pcbnew
195
+ vias = []
196
+ for track in self.board.GetTracks():
197
+ if not isinstance(track, pcbnew.PCB_VIA):
198
+ continue
199
+ top_layer = track.TopLayer()
200
+ size_mm = self._to_mm(track.GetWidth(top_layer))
201
+ vias.append(PCBVia(
202
+ position=self._pos_mm(track.GetStart()),
203
+ size=size_mm,
204
+ drill=self._to_mm(track.GetDrill()),
205
+ top_layer=self._layer_name(top_layer),
206
+ bottom_layer=self._layer_name(track.BottomLayer()),
207
+ net=track.GetNetname(),
208
+ ))
209
+ return vias
210
+
211
+ def get_vias_for_net(self, net_name: str) -> list[PCBVia]:
212
+ """Get vias belonging to a specific net."""
213
+ return [v for v in self.get_vias() if v.net == net_name]
214
+
215
+ # ── Zones ───────────────────────────────────────────────
216
+
217
+ def get_zones(self) -> list[PCBZone]:
218
+ """Get all copper zones."""
219
+ zones = []
220
+ for zone in self.board.Zones():
221
+ zones.append(PCBZone(
222
+ net_name=zone.GetNetname(),
223
+ layer=_canonicalize_layer(zone.GetLayerName()),
224
+ filled=zone.IsFilled(),
225
+ ))
226
+ return zones
227
+
228
+ def get_zones_for_net(self, net_name: str) -> list[PCBZone]:
229
+ return [z for z in self.get_zones() if z.net_name == net_name]
230
+
231
+ # ── Board Info ──────────────────────────────────────────
232
+
233
+ def get_board_info(self) -> dict[str, Any]:
234
+ title_block = self.board.GetTitleBlock()
235
+ bbox = self.board.ComputeBoundingBox(True)
236
+ size = bbox.GetSize()
237
+
238
+ return {
239
+ "title": title_block.GetTitle(),
240
+ "date": title_block.GetDate(),
241
+ "revision": title_block.GetRevision(),
242
+ "company": title_block.GetCompany(),
243
+ "file_path": str(self.file_path),
244
+ "board_width_mm": self._to_mm(size.x),
245
+ "board_height_mm": self._to_mm(size.y),
246
+ "footprints_count": len(self.board.GetFootprints()),
247
+ "tracks_count": sum(
248
+ 1 for t in self.board.GetTracks()
249
+ if not isinstance(t, self._pcbnew.PCB_VIA)
250
+ ),
251
+ "vias_count": sum(
252
+ 1 for t in self.board.GetTracks()
253
+ if isinstance(t, self._pcbnew.PCB_VIA)
254
+ ),
255
+ "nets_count": self.board.GetNetCount(),
256
+ "zones_count": len(list(self.board.Zones())),
257
+ }
258
+
259
+ # ── Design Rules ────────────────────────────────────────
260
+
261
+ def get_design_rules(self) -> dict[str, Any]:
262
+ ds = self.board.GetDesignSettings()
263
+ pcbnew = self._pcbnew
264
+ return {
265
+ "board_thickness_mm": pcbnew.ToMM(ds.GetBoardThickness()),
266
+ "copper_layers": ds.GetCopperLayerCount(),
267
+ "smallest_clearance_mm": pcbnew.ToMM(ds.GetSmallestClearanceValue()),
268
+ "biggest_clearance_mm": pcbnew.ToMM(ds.GetBiggestClearanceValue()),
269
+ "current_track_width_mm": pcbnew.ToMM(ds.GetCurrentTrackWidth()),
270
+ "current_via_drill_mm": pcbnew.ToMM(ds.GetCurrentViaDrill()),
271
+ "current_via_size_mm": pcbnew.ToMM(ds.GetCurrentViaSize()),
272
+ "current_diff_pair_width_mm": pcbnew.ToMM(ds.GetCurrentDiffPairWidth()),
273
+ "current_diff_pair_gap_mm": pcbnew.ToMM(ds.GetCurrentDiffPairGap()),
274
+ "drc_epsilon_mm": pcbnew.ToMM(ds.GetDRCEpsilon()),
275
+ "hole_plating_mm": pcbnew.ToMM(ds.GetHolePlatingThickness()),
276
+ }
277
+
278
+ # ── Aggregate Stats ─────────────────────────────────────
279
+
280
+ def get_net_track_stats(self) -> dict[str, dict[str, Any]]:
281
+ """Per-net track statistics: total_length, segment_count, widths, layers."""
282
+ stats: dict[str, dict[str, Any]] = {}
283
+ for track in self.get_tracks():
284
+ net = track.net or "(unnamed)"
285
+ if net not in stats:
286
+ stats[net] = {
287
+ "total_length": 0.0,
288
+ "segment_count": 0,
289
+ "widths": [],
290
+ "layers": set(),
291
+ }
292
+ s = stats[net]
293
+ s["total_length"] += track.length
294
+ s["segment_count"] += 1
295
+ s["widths"].append(track.width)
296
+ s["layers"].add(track.layer)
297
+ # Convert sets to sorted lists for JSON
298
+ for s in stats.values():
299
+ s["layers"] = sorted(s["layers"])
300
+ return stats
301
+
302
+ def get_via_stats(self) -> dict[str, Any]:
303
+ """Aggregate via statistics."""
304
+ vias = self.get_vias()
305
+ if not vias:
306
+ return {"count": 0}
307
+
308
+ drill_counts: dict[str, int] = {}
309
+ layer_span_counts: dict[str, int] = {}
310
+ net_via_counts: dict[str, int] = {}
311
+
312
+ for v in vias:
313
+ drill_key = f"{v.drill:.3f}mm"
314
+ drill_counts[drill_key] = drill_counts.get(drill_key, 0) + 1
315
+
316
+ span = f"{v.top_layer} -> {v.bottom_layer}"
317
+ layer_span_counts[span] = layer_span_counts.get(span, 0) + 1
318
+
319
+ net = v.net or "(unnamed)"
320
+ net_via_counts[net] = net_via_counts.get(net, 0) + 1
321
+
322
+ return {
323
+ "count": len(vias),
324
+ "drill_sizes": dict(sorted(drill_counts.items(), key=lambda x: -x[1])),
325
+ "layer_spans": layer_span_counts,
326
+ "top_via_nets": dict(sorted(net_via_counts.items(), key=lambda x: -x[1])[:20]),
327
+ }