@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,98 @@
1
+ import os
2
+ import argparse
3
+ import logging
4
+ import json
5
+ from typing import Dict, Any, List
6
+
7
+ from src.models.code_generator import OpenSCADCodeGenerator
8
+ from src.utils.cad_exporter import CADExporter
9
+ from src.utils.format_validator import FormatValidator
10
+ from src.testing.primitive_tester import PrimitiveTester
11
+
12
+ # Configure logging
13
+ logging.basicConfig(
14
+ level=logging.INFO,
15
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
16
+ )
17
+ logger = logging.getLogger(__name__)
18
+
19
+ def main():
20
+ parser = argparse.ArgumentParser(description='Test OpenSCAD primitives with different export formats')
21
+ parser.add_argument('--output-dir', default='test_output', help='Directory to store test output')
22
+ parser.add_argument('--formats', nargs='+', default=['3mf', 'amf', 'csg', 'scad'],
23
+ help='Formats to test (default: 3mf amf csg scad)')
24
+ parser.add_argument('--primitives', nargs='+',
25
+ help='Primitives to test (default: all)')
26
+ parser.add_argument('--validate', action='store_true',
27
+ help='Validate exported files')
28
+ parser.add_argument('--printer-type', choices=['prusa', 'bambu'], default='prusa',
29
+ help='Printer type to check compatibility with (default: prusa)')
30
+
31
+ args = parser.parse_args()
32
+
33
+ # Create directories
34
+ os.makedirs("scad", exist_ok=True)
35
+ os.makedirs(args.output_dir, exist_ok=True)
36
+
37
+ # Initialize components
38
+ # Use absolute path for templates to avoid path issues
39
+ templates_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../models/scad_templates"))
40
+ code_generator = OpenSCADCodeGenerator(templates_dir, "scad")
41
+ cad_exporter = CADExporter()
42
+
43
+ # Initialize tester
44
+ tester = PrimitiveTester(code_generator, cad_exporter, args.output_dir)
45
+
46
+ # Override formats if specified
47
+ if args.formats:
48
+ tester.formats = args.formats
49
+
50
+ # Test primitives
51
+ if args.primitives:
52
+ results = {}
53
+ for primitive in args.primitives:
54
+ results[primitive] = tester.test_primitive(primitive)
55
+ else:
56
+ results = tester.test_all_primitives()
57
+
58
+ # Print results
59
+ logger.info(f"Test results: {json.dumps(results, indent=2)}")
60
+
61
+ # Validate exported files if requested
62
+ if args.validate:
63
+ validator = FormatValidator()
64
+ validation_results = {}
65
+
66
+ for primitive, primitive_results in results.items():
67
+ validation_results[primitive] = {}
68
+
69
+ for format_type, format_results in primitive_results["formats"].items():
70
+ if format_results["success"] and format_type in ['3mf', 'amf']:
71
+ output_file = format_results["output_file"]
72
+
73
+ if format_type == '3mf':
74
+ is_valid, error = validator.validate_3mf(output_file)
75
+ elif format_type == 'amf':
76
+ is_valid, error = validator.validate_amf(output_file)
77
+ else:
78
+ is_valid, error = False, "Validation not supported for this format"
79
+
80
+ # Check printer compatibility
81
+ is_compatible, compat_error = validator.check_printer_compatibility(
82
+ output_file, args.printer_type
83
+ )
84
+
85
+ metadata = validator.extract_metadata(output_file)
86
+
87
+ validation_results[primitive][format_type] = {
88
+ "is_valid": is_valid,
89
+ "error": error,
90
+ "is_compatible_with_printer": is_compatible,
91
+ "compatibility_error": compat_error,
92
+ "metadata": metadata
93
+ }
94
+
95
+ logger.info(f"Validation results: {json.dumps(validation_results, indent=2)}")
96
+
97
+ if __name__ == "__main__":
98
+ main()
@@ -0,0 +1,241 @@
1
+ import os
2
+ import logging
3
+ import subprocess
4
+ from typing import Dict, Any, Optional, Tuple, List
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ class CADExporter:
9
+ """
10
+ Exports OpenSCAD models to various CAD formats that preserve parametric properties.
11
+ """
12
+
13
+ def __init__(self, openscad_path: str = "openscad"):
14
+ """
15
+ Initialize the CAD exporter.
16
+
17
+ Args:
18
+ openscad_path: Path to the OpenSCAD executable
19
+ """
20
+ self.openscad_path = openscad_path
21
+
22
+ # Supported export formats
23
+ self.supported_formats = {
24
+ "csg": "OpenSCAD CSG format (preserves all parametric properties)",
25
+ "amf": "Additive Manufacturing File Format (preserves some metadata)",
26
+ "3mf": "3D Manufacturing Format (modern replacement for STL with metadata)",
27
+ "scad": "OpenSCAD source code (fully parametric)",
28
+ "dxf": "Drawing Exchange Format (for 2D designs)",
29
+ "svg": "Scalable Vector Graphics (for 2D designs)"
30
+ }
31
+
32
+ def export_model(self, scad_file: str, output_format: str = "csg",
33
+ parameters: Optional[Dict[str, Any]] = None,
34
+ metadata: Optional[Dict[str, Any]] = None) -> Tuple[bool, str, Optional[str]]:
35
+ """
36
+ Export an OpenSCAD model to the specified format.
37
+
38
+ Args:
39
+ scad_file: Path to the SCAD file
40
+ output_format: Format to export to (csg, amf, 3mf, etc.)
41
+ parameters: Optional parameters to override in the SCAD file
42
+ metadata: Optional metadata to include in the export
43
+
44
+ Returns:
45
+ Tuple of (success, output_file_path, error_message)
46
+ """
47
+ if not os.path.exists(scad_file):
48
+ return False, "", f"SCAD file not found: {scad_file}"
49
+
50
+ # Create output file path
51
+ output_dir = os.path.dirname(scad_file)
52
+ model_id = os.path.basename(scad_file).split('.')[0]
53
+
54
+ # Special case for SCAD format - just copy the file with parameters embedded
55
+ if output_format.lower() == "scad" and parameters:
56
+ return self._export_parametric_scad(scad_file, parameters, metadata)
57
+
58
+ # For native OpenSCAD formats
59
+ output_file = os.path.join(output_dir, f"{model_id}.{output_format.lower()}")
60
+
61
+ # Build command
62
+ cmd = [self.openscad_path, "-o", output_file]
63
+
64
+ # Add parameters if provided
65
+ if parameters:
66
+ for key, value in parameters.items():
67
+ cmd.extend(["-D", f"{key}={value}"])
68
+
69
+ # Add input file
70
+ cmd.append(scad_file)
71
+
72
+ try:
73
+ # Run OpenSCAD
74
+ result = subprocess.run(cmd, check=True, capture_output=True, text=True)
75
+
76
+ # Check if file was created
77
+ if os.path.exists(output_file) and os.path.getsize(output_file) > 0:
78
+ logger.info(f"Exported model to {output_format}: {output_file}")
79
+
80
+ # Add metadata if supported and provided
81
+ if metadata and output_format.lower() in ["amf", "3mf"]:
82
+ self._add_metadata_to_file(output_file, metadata, output_format)
83
+
84
+ return True, output_file, None
85
+ else:
86
+ error_msg = f"Failed to export model to {output_format}"
87
+ logger.error(error_msg)
88
+ logger.error(f"OpenSCAD output: {result.stdout}")
89
+ logger.error(f"OpenSCAD error: {result.stderr}")
90
+ return False, "", error_msg
91
+ except subprocess.CalledProcessError as e:
92
+ error_msg = f"Error exporting model to {output_format}: {e.stderr}"
93
+ logger.error(error_msg)
94
+ return False, "", error_msg
95
+ except Exception as e:
96
+ error_msg = f"Error exporting model to {output_format}: {str(e)}"
97
+ logger.error(error_msg)
98
+ return False, "", error_msg
99
+
100
+ def _export_parametric_scad(self, scad_file: str, parameters: Dict[str, Any],
101
+ metadata: Optional[Dict[str, Any]] = None) -> Tuple[bool, str, Optional[str]]:
102
+ """
103
+ Create a new SCAD file with parameters embedded as variables.
104
+
105
+ Args:
106
+ scad_file: Path to the original SCAD file
107
+ parameters: Parameters to embed in the SCAD file
108
+ metadata: Optional metadata to include as comments
109
+
110
+ Returns:
111
+ Tuple of (success, output_file_path, error_message)
112
+ """
113
+ try:
114
+ # Read the original SCAD file
115
+ with open(scad_file, 'r') as f:
116
+ content = f.read()
117
+
118
+ # Create output file path
119
+ output_dir = os.path.dirname(scad_file)
120
+ model_id = os.path.basename(scad_file).split('.')[0]
121
+ output_file = os.path.join(output_dir, f"{model_id}_parametric.scad")
122
+
123
+ # Create parameter declarations
124
+ param_declarations = []
125
+ for key, value in parameters.items():
126
+ if isinstance(value, str):
127
+ param_declarations.append(f'{key} = "{value}";')
128
+ else:
129
+ param_declarations.append(f'{key} = {value};')
130
+
131
+ # Create metadata comments
132
+ metadata_comments = []
133
+ if metadata:
134
+ metadata_comments.append("// Metadata:")
135
+ for key, value in metadata.items():
136
+ metadata_comments.append(f"// {key}: {value}")
137
+
138
+ # Combine everything
139
+ new_content = "// Parametric model generated by OpenSCAD MCP Server\n"
140
+ new_content += "\n".join(metadata_comments) + "\n\n" if metadata_comments else "\n"
141
+ new_content += "// Parameters:\n"
142
+ new_content += "\n".join(param_declarations) + "\n\n"
143
+ new_content += content
144
+
145
+ # Write to the new file
146
+ with open(output_file, 'w') as f:
147
+ f.write(new_content)
148
+
149
+ logger.info(f"Exported parametric SCAD file: {output_file}")
150
+ return True, output_file, None
151
+ except Exception as e:
152
+ error_msg = f"Error creating parametric SCAD file: {str(e)}"
153
+ logger.error(error_msg)
154
+ return False, "", error_msg
155
+
156
+ def _add_metadata_to_file(self, file_path: str, metadata: Dict[str, Any], format_type: str) -> None:
157
+ """
158
+ Add metadata to supported file formats.
159
+
160
+ Args:
161
+ file_path: Path to the file
162
+ metadata: Metadata to add
163
+ format_type: File format
164
+ """
165
+ if format_type.lower() == "amf":
166
+ self._add_metadata_to_amf(file_path, metadata)
167
+ elif format_type.lower() == "3mf":
168
+ self._add_metadata_to_3mf(file_path, metadata)
169
+
170
+ def _add_metadata_to_amf(self, file_path: str, metadata: Dict[str, Any]) -> None:
171
+ """Add metadata to AMF file."""
172
+ try:
173
+ import xml.etree.ElementTree as ET
174
+
175
+ # Parse the AMF file
176
+ tree = ET.parse(file_path)
177
+ root = tree.getroot()
178
+
179
+ # Find or create metadata element
180
+ metadata_elem = root.find("metadata")
181
+ if metadata_elem is None:
182
+ metadata_elem = ET.SubElement(root, "metadata")
183
+
184
+ # Add metadata
185
+ for key, value in metadata.items():
186
+ meta = ET.SubElement(metadata_elem, "meta", name=key)
187
+ meta.text = str(value)
188
+
189
+ # Write back to file
190
+ tree.write(file_path)
191
+ logger.info(f"Added metadata to AMF file: {file_path}")
192
+ except Exception as e:
193
+ logger.error(f"Error adding metadata to AMF file: {str(e)}")
194
+
195
+ def _add_metadata_to_3mf(self, file_path: str, metadata: Dict[str, Any]) -> None:
196
+ """Add metadata to 3MF file."""
197
+ try:
198
+ import zipfile
199
+ import xml.etree.ElementTree as ET
200
+
201
+ # 3MF files are ZIP archives
202
+ with zipfile.ZipFile(file_path, 'a') as z:
203
+ # Check if metadata file exists
204
+ metadata_path = "Metadata/model_metadata.xml"
205
+ try:
206
+ z.getinfo(metadata_path)
207
+ # Extract existing metadata
208
+ with z.open(metadata_path) as f:
209
+ tree = ET.parse(f)
210
+ root = tree.getroot()
211
+ except KeyError:
212
+ # Create new metadata file
213
+ root = ET.Element("metadata")
214
+ tree = ET.ElementTree(root)
215
+
216
+ # Add metadata
217
+ for key, value in metadata.items():
218
+ meta = ET.SubElement(root, "meta", name=key)
219
+ meta.text = str(value)
220
+
221
+ # Write metadata to a temporary file
222
+ temp_path = file_path + ".metadata.tmp"
223
+ tree.write(temp_path)
224
+
225
+ # Add to ZIP
226
+ z.write(temp_path, metadata_path)
227
+
228
+ # Remove temporary file
229
+ os.remove(temp_path)
230
+
231
+ logger.info(f"Added metadata to 3MF file: {file_path}")
232
+ except Exception as e:
233
+ logger.error(f"Error adding metadata to 3MF file: {str(e)}")
234
+
235
+ def get_supported_formats(self) -> List[str]:
236
+ """Get list of supported export formats."""
237
+ return list(self.supported_formats.keys())
238
+
239
+ def get_format_description(self, format_name: str) -> str:
240
+ """Get description of a format."""
241
+ return self.supported_formats.get(format_name.lower(), "Unknown format")
@@ -0,0 +1,206 @@
1
+ import os
2
+ import logging
3
+ import zipfile
4
+ import xml.etree.ElementTree as ET
5
+ from typing import Tuple, Optional, Dict, Any
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class FormatValidator:
10
+ """Validates 3D model formats for compatibility with printers."""
11
+
12
+ @staticmethod
13
+ def validate_3mf(file_path: str) -> Tuple[bool, Optional[str]]:
14
+ """
15
+ Validate a 3MF file for compatibility with Prusa and Bambu printers.
16
+
17
+ Args:
18
+ file_path: Path to the 3MF file
19
+
20
+ Returns:
21
+ Tuple of (is_valid, error_message)
22
+ """
23
+ if not os.path.exists(file_path):
24
+ return False, f"File not found: {file_path}"
25
+
26
+ try:
27
+ # 3MF files are ZIP archives with XML content
28
+ with zipfile.ZipFile(file_path, 'r') as zip_ref:
29
+ # Check for required files
30
+ required_files = ['3D/3dmodel.model', '[Content_Types].xml']
31
+ for req_file in required_files:
32
+ try:
33
+ zip_ref.getinfo(req_file)
34
+ except KeyError:
35
+ return False, f"Missing required file in 3MF: {req_file}"
36
+
37
+ # Validate 3D model file
38
+ with zip_ref.open('3D/3dmodel.model') as model_file:
39
+ tree = ET.parse(model_file)
40
+ root = tree.getroot()
41
+
42
+ # Check for required elements
43
+ if root.tag != '{http://schemas.microsoft.com/3dmanufacturing/core/2015/02}model':
44
+ return False, "Invalid 3MF: Missing model element"
45
+
46
+ # Verify resources section exists
47
+ resources = root.find('.//{http://schemas.microsoft.com/3dmanufacturing/core/2015/02}resources')
48
+ if resources is None:
49
+ return False, "Invalid 3MF: Missing resources element"
50
+
51
+ return True, None
52
+ except Exception as e:
53
+ logger.error(f"Error validating 3MF file: {str(e)}")
54
+ return False, f"Error validating 3MF file: {str(e)}"
55
+
56
+ @staticmethod
57
+ def validate_amf(file_path: str) -> Tuple[bool, Optional[str]]:
58
+ """
59
+ Validate an AMF file for compatibility with printers.
60
+
61
+ Args:
62
+ file_path: Path to the AMF file
63
+
64
+ Returns:
65
+ Tuple of (is_valid, error_message)
66
+ """
67
+ if not os.path.exists(file_path):
68
+ return False, f"File not found: {file_path}"
69
+
70
+ try:
71
+ # Parse the AMF file (XML format)
72
+ tree = ET.parse(file_path)
73
+ root = tree.getroot()
74
+
75
+ # Check for required elements
76
+ if root.tag != 'amf':
77
+ return False, "Invalid AMF: Missing amf root element"
78
+
79
+ # Check for at least one object
80
+ objects = root.findall('./object')
81
+ if not objects:
82
+ return False, "Invalid AMF: No objects found"
83
+
84
+ # Check that each object has a mesh
85
+ for obj in objects:
86
+ mesh = obj.find('./mesh')
87
+ if mesh is None:
88
+ return False, f"Invalid AMF: Object {obj.get('id', 'unknown')} is missing a mesh"
89
+
90
+ # Check for vertices and volumes
91
+ vertices = mesh.find('./vertices')
92
+ volumes = mesh.findall('./volume')
93
+
94
+ if vertices is None:
95
+ return False, f"Invalid AMF: Mesh in object {obj.get('id', 'unknown')} is missing vertices"
96
+
97
+ if not volumes:
98
+ return False, f"Invalid AMF: Mesh in object {obj.get('id', 'unknown')} has no volumes"
99
+
100
+ return True, None
101
+ except Exception as e:
102
+ logger.error(f"Error validating AMF file: {str(e)}")
103
+ return False, f"Error validating AMF file: {str(e)}"
104
+
105
+ @staticmethod
106
+ def extract_metadata(file_path: str) -> Dict[str, Any]:
107
+ """
108
+ Extract metadata from a 3MF or AMF file.
109
+
110
+ Args:
111
+ file_path: Path to the 3D model file
112
+
113
+ Returns:
114
+ Dictionary of metadata
115
+ """
116
+ metadata = {}
117
+
118
+ # Check file extension
119
+ ext = os.path.splitext(file_path)[1].lower()
120
+
121
+ try:
122
+ if ext == '.3mf':
123
+ with zipfile.ZipFile(file_path, 'r') as zip_ref:
124
+ metadata_path = "Metadata/model_metadata.xml"
125
+ try:
126
+ with zip_ref.open(metadata_path) as f:
127
+ tree = ET.parse(f)
128
+ root = tree.getroot()
129
+
130
+ for meta in root.findall('./meta'):
131
+ name = meta.get('name')
132
+ if name:
133
+ metadata[name] = meta.text
134
+ except KeyError:
135
+ # Metadata file doesn't exist
136
+ pass
137
+
138
+ elif ext == '.amf':
139
+ tree = ET.parse(file_path)
140
+ root = tree.getroot()
141
+
142
+ for meta in root.findall('./metadata'):
143
+ name = meta.get('type')
144
+ if name:
145
+ metadata[name] = meta.text
146
+ except Exception as e:
147
+ logger.error(f"Error extracting metadata: {str(e)}")
148
+
149
+ return metadata
150
+
151
+ @staticmethod
152
+ def check_printer_compatibility(file_path: str, printer_type: str = "prusa") -> Tuple[bool, Optional[str]]:
153
+ """
154
+ Check if a 3D model file is compatible with a specific printer type.
155
+
156
+ Args:
157
+ file_path: Path to the 3D model file
158
+ printer_type: Type of printer ("prusa" or "bambu")
159
+
160
+ Returns:
161
+ Tuple of (is_compatible, error_message)
162
+ """
163
+ # Check file extension
164
+ ext = os.path.splitext(file_path)[1].lower()
165
+
166
+ # Validate based on file format
167
+ if ext == '.3mf':
168
+ is_valid, error = FormatValidator.validate_3mf(file_path)
169
+ if not is_valid:
170
+ return False, error
171
+
172
+ # Additional printer-specific checks
173
+ if printer_type.lower() == "prusa":
174
+ # Prusa-specific checks for 3MF
175
+ # For now, just basic validation is sufficient
176
+ return True, None
177
+
178
+ elif printer_type.lower() == "bambu":
179
+ # Bambu-specific checks for 3MF
180
+ # For now, just basic validation is sufficient
181
+ return True, None
182
+
183
+ else:
184
+ return False, f"Unknown printer type: {printer_type}"
185
+
186
+ elif ext == '.amf':
187
+ is_valid, error = FormatValidator.validate_amf(file_path)
188
+ if not is_valid:
189
+ return False, error
190
+
191
+ # Additional printer-specific checks
192
+ if printer_type.lower() == "prusa":
193
+ # Prusa-specific checks for AMF
194
+ # For now, just basic validation is sufficient
195
+ return True, None
196
+
197
+ elif printer_type.lower() == "bambu":
198
+ # Bambu-specific checks for AMF
199
+ # For now, just basic validation is sufficient
200
+ return True, None
201
+
202
+ else:
203
+ return False, f"Unknown printer type: {printer_type}"
204
+
205
+ else:
206
+ return False, f"Unsupported file format for printer compatibility check: {ext}"
@@ -0,0 +1,140 @@
1
+ import os
2
+ import logging
3
+ import uuid
4
+ import shutil
5
+ from typing import Dict, Any, Optional, Tuple
6
+
7
+ from src.utils.stl_validator import STLValidator
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class STLExporter:
12
+ """
13
+ Handles STL file export and validation for 3D printing.
14
+ """
15
+
16
+ def __init__(self, openscad_wrapper, output_dir: str):
17
+ """
18
+ Initialize the STL exporter.
19
+
20
+ Args:
21
+ openscad_wrapper: Instance of OpenSCADWrapper for generating STL files
22
+ output_dir: Directory to store output files
23
+ """
24
+ self.openscad_wrapper = openscad_wrapper
25
+ self.output_dir = output_dir
26
+ self.stl_dir = os.path.join(output_dir, "stl")
27
+
28
+ # Create directory if it doesn't exist
29
+ os.makedirs(self.stl_dir, exist_ok=True)
30
+
31
+ def export_stl(self, scad_file: str, parameters: Optional[Dict[str, Any]] = None) -> Tuple[str, bool, Optional[str]]:
32
+ """
33
+ Export a SCAD file to STL format.
34
+
35
+ Args:
36
+ scad_file: Path to the SCAD file
37
+ parameters: Optional parameters to override in the SCAD file
38
+
39
+ Returns:
40
+ Tuple of (stl_file_path, is_valid, error_message)
41
+ """
42
+ try:
43
+ # Generate STL file
44
+ stl_file = self.openscad_wrapper.generate_stl(scad_file, parameters)
45
+
46
+ # Validate STL file
47
+ is_valid, error = STLValidator.validate_stl(stl_file)
48
+
49
+ if not is_valid:
50
+ logger.warning(f"STL validation failed: {error}")
51
+
52
+ # Attempt to repair if validation fails
53
+ repair_success, repair_error = STLValidator.repair_stl(stl_file)
54
+ if repair_success:
55
+ # Validate again after repair
56
+ is_valid, error = STLValidator.validate_stl(stl_file)
57
+ else:
58
+ logger.error(f"STL repair failed: {repair_error}")
59
+
60
+ return stl_file, is_valid, error
61
+ except Exception as e:
62
+ logger.error(f"Error exporting STL: {str(e)}")
63
+ return "", False, str(e)
64
+
65
+ def export_stl_with_metadata(self, scad_file: str, parameters: Optional[Dict[str, Any]] = None,
66
+ metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
67
+ """
68
+ Export a SCAD file to STL format and include metadata.
69
+
70
+ Args:
71
+ scad_file: Path to the SCAD file
72
+ parameters: Optional parameters to override in the SCAD file
73
+ metadata: Optional metadata to include with the STL file
74
+
75
+ Returns:
76
+ Dictionary with STL file information
77
+ """
78
+ # Export STL file
79
+ stl_file, is_valid, error = self.export_stl(scad_file, parameters)
80
+
81
+ # Create metadata file if metadata is provided
82
+ metadata_file = None
83
+ if metadata and stl_file:
84
+ metadata_file = self._create_metadata_file(stl_file, metadata)
85
+
86
+ # Extract model ID from filename
87
+ model_id = os.path.basename(scad_file).split('.')[0] if scad_file else str(uuid.uuid4())
88
+
89
+ return {
90
+ "model_id": model_id,
91
+ "stl_file": stl_file,
92
+ "is_valid": is_valid,
93
+ "error": error,
94
+ "metadata_file": metadata_file,
95
+ "metadata": metadata
96
+ }
97
+
98
+ def _create_metadata_file(self, stl_file: str, metadata: Dict[str, Any]) -> str:
99
+ """Create a metadata file for an STL file."""
100
+ metadata_file = f"{os.path.splitext(stl_file)[0]}.json"
101
+
102
+ try:
103
+ import json
104
+ with open(metadata_file, 'w') as f:
105
+ json.dump(metadata, f, indent=2)
106
+
107
+ logger.info(f"Created metadata file: {metadata_file}")
108
+ return metadata_file
109
+ except Exception as e:
110
+ logger.error(f"Error creating metadata file: {str(e)}")
111
+ return ""
112
+
113
+ def copy_stl_to_location(self, stl_file: str, destination: str) -> str:
114
+ """
115
+ Copy an STL file to a specified location.
116
+
117
+ Args:
118
+ stl_file: Path to the STL file
119
+ destination: Destination path or directory
120
+
121
+ Returns:
122
+ Path to the copied STL file
123
+ """
124
+ try:
125
+ if not os.path.exists(stl_file):
126
+ raise FileNotFoundError(f"STL file not found: {stl_file}")
127
+
128
+ # If destination is a directory, create a filename
129
+ if os.path.isdir(destination):
130
+ filename = os.path.basename(stl_file)
131
+ destination = os.path.join(destination, filename)
132
+
133
+ # Copy the file
134
+ shutil.copy2(stl_file, destination)
135
+ logger.info(f"Copied STL file to: {destination}")
136
+
137
+ return destination
138
+ except Exception as e:
139
+ logger.error(f"Error copying STL file: {str(e)}")
140
+ return ""