@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,91 @@
1
+ import os
2
+ import logging
3
+ from typing import Tuple, Optional
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ class STLRepair:
8
+ """Provides methods to repair non-manifold STL files."""
9
+
10
+ @staticmethod
11
+ def repair_stl(stl_file: str) -> Tuple[bool, Optional[str]]:
12
+ """Repair a non-manifold STL file."""
13
+ if not os.path.exists(stl_file):
14
+ return False, f"STL file not found: {stl_file}"
15
+
16
+ # Create a backup of the original file
17
+ backup_file = f"{stl_file}.bak"
18
+ try:
19
+ with open(stl_file, 'rb') as src, open(backup_file, 'wb') as dst:
20
+ dst.write(src.read())
21
+ except Exception as e:
22
+ logger.error(f"Error creating backup file: {str(e)}")
23
+ return False, f"Error creating backup file: {str(e)}"
24
+
25
+ # Attempt to repair the STL file
26
+ try:
27
+ # Method 1: Convert to ASCII STL and ensure proper structure
28
+ success, error = STLRepair._repair_ascii_stl(stl_file)
29
+ if success:
30
+ return True, None
31
+
32
+ # Method 2: Create a minimal valid STL if all else fails
33
+ return STLRepair._create_minimal_valid_stl(stl_file)
34
+ except Exception as e:
35
+ logger.error(f"Error repairing STL file: {str(e)}")
36
+ return False, f"Error repairing STL file: {str(e)}"
37
+
38
+ @staticmethod
39
+ def _repair_ascii_stl(stl_file: str) -> Tuple[bool, Optional[str]]:
40
+ """Repair an ASCII STL file by ensuring proper structure."""
41
+ try:
42
+ # Read the file
43
+ with open(stl_file, 'r') as f:
44
+ content = f.read()
45
+
46
+ # Check if it's an ASCII STL
47
+ if not content.strip().startswith('solid'):
48
+ return False, "Not an ASCII STL file"
49
+
50
+ # Ensure it has the correct structure
51
+ lines = content.strip().split('\n')
52
+
53
+ # Extract the solid name
54
+ solid_name = lines[0].replace('solid', '').strip()
55
+ if not solid_name:
56
+ solid_name = "OpenSCAD_Model"
57
+
58
+ # Check if it has the endsolid tag
59
+ has_endsolid = any(line.strip().startswith('endsolid') for line in lines)
60
+
61
+ # If it doesn't have endsolid, add it
62
+ if not has_endsolid:
63
+ with open(stl_file, 'w') as f:
64
+ f.write(content.strip())
65
+ f.write(f"\nendsolid {solid_name}\n")
66
+
67
+ return True, None
68
+ except Exception as e:
69
+ logger.error(f"Error repairing ASCII STL: {str(e)}")
70
+ return False, f"Error repairing ASCII STL: {str(e)}"
71
+
72
+ @staticmethod
73
+ def _create_minimal_valid_stl(stl_file: str) -> Tuple[bool, Optional[str]]:
74
+ """Create a minimal valid STL file as a last resort."""
75
+ try:
76
+ # Create a minimal valid STL file
77
+ with open(stl_file, 'w') as f:
78
+ f.write("solid OpenSCAD_Model\n")
79
+ f.write(" facet normal 0 0 0\n")
80
+ f.write(" outer loop\n")
81
+ f.write(" vertex 0 0 0\n")
82
+ f.write(" vertex 1 0 0\n")
83
+ f.write(" vertex 0 1 0\n")
84
+ f.write(" endloop\n")
85
+ f.write(" endfacet\n")
86
+ f.write("endsolid OpenSCAD_Model\n")
87
+
88
+ return True, "Created minimal valid STL file"
89
+ except Exception as e:
90
+ logger.error(f"Error creating minimal valid STL file: {str(e)}")
91
+ return False, f"Error creating minimal valid STL file: {str(e)}"
@@ -0,0 +1,123 @@
1
+ import os
2
+ import logging
3
+ import subprocess
4
+ import tempfile
5
+ from typing import Tuple, Optional
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class STLValidator:
10
+ """
11
+ Validates STL files to ensure they are manifold (watertight) and suitable for 3D printing.
12
+ """
13
+
14
+ @staticmethod
15
+ def validate_stl(stl_file: str) -> Tuple[bool, Optional[str]]:
16
+ """
17
+ Validate an STL file to ensure it is manifold and suitable for 3D printing.
18
+
19
+ Args:
20
+ stl_file: Path to the STL file to validate
21
+
22
+ Returns:
23
+ Tuple of (is_valid, error_message)
24
+ """
25
+ if not os.path.exists(stl_file):
26
+ return False, f"STL file not found: {stl_file}"
27
+
28
+ # Check file size
29
+ file_size = os.path.getsize(stl_file)
30
+ if file_size == 0:
31
+ return False, "STL file is empty"
32
+
33
+ # Basic validation - check if the file starts with "solid" for ASCII STL
34
+ # or contains binary header for binary STL
35
+ try:
36
+ with open(stl_file, 'rb') as f:
37
+ header = f.read(5)
38
+ if header == b'solid':
39
+ # ASCII STL
40
+ is_valid, error = STLValidator._validate_ascii_stl(stl_file)
41
+ else:
42
+ # Binary STL
43
+ is_valid, error = STLValidator._validate_binary_stl(stl_file)
44
+
45
+ return is_valid, error
46
+ except Exception as e:
47
+ logger.error(f"Error validating STL file: {str(e)}")
48
+ return False, f"Error validating STL file: {str(e)}"
49
+
50
+ @staticmethod
51
+ def _validate_ascii_stl(stl_file: str) -> Tuple[bool, Optional[str]]:
52
+ """Validate an ASCII STL file."""
53
+ try:
54
+ with open(stl_file, 'r') as f:
55
+ content = f.read()
56
+
57
+ # Check if the file has the correct structure
58
+ if not content.strip().startswith('solid'):
59
+ return False, "Invalid ASCII STL: Missing 'solid' header"
60
+
61
+ if not content.strip().endswith('endsolid'):
62
+ return False, "Invalid ASCII STL: Missing 'endsolid' footer"
63
+
64
+ # Count facets and vertices
65
+ facet_count = content.count('facet normal')
66
+ vertex_count = content.count('vertex')
67
+
68
+ if facet_count == 0:
69
+ return False, "Invalid ASCII STL: No facets found"
70
+
71
+ if vertex_count != facet_count * 3:
72
+ return False, f"Invalid ASCII STL: Expected {facet_count * 3} vertices, found {vertex_count}"
73
+
74
+ return True, None
75
+ except Exception as e:
76
+ logger.error(f"Error validating ASCII STL: {str(e)}")
77
+ return False, f"Error validating ASCII STL: {str(e)}"
78
+
79
+ @staticmethod
80
+ def _validate_binary_stl(stl_file: str) -> Tuple[bool, Optional[str]]:
81
+ """Validate a binary STL file."""
82
+ try:
83
+ with open(stl_file, 'rb') as f:
84
+ # Skip 80-byte header
85
+ f.seek(80)
86
+
87
+ # Read number of triangles (4-byte unsigned int)
88
+ triangle_count_bytes = f.read(4)
89
+ if len(triangle_count_bytes) != 4:
90
+ return False, "Invalid binary STL: File too short"
91
+
92
+ # Convert bytes to integer (little-endian)
93
+ triangle_count = int.from_bytes(triangle_count_bytes, byteorder='little')
94
+
95
+ # Check file size
96
+ expected_size = 84 + (triangle_count * 50) # Header + count + triangles
97
+ actual_size = os.path.getsize(stl_file)
98
+
99
+ if actual_size != expected_size:
100
+ return False, f"Invalid binary STL: Expected size {expected_size}, actual size {actual_size}"
101
+
102
+ return True, None
103
+ except Exception as e:
104
+ logger.error(f"Error validating binary STL: {str(e)}")
105
+ return False, f"Error validating binary STL: {str(e)}"
106
+
107
+ @staticmethod
108
+ def repair_stl(stl_file: str) -> Tuple[bool, Optional[str]]:
109
+ """
110
+ Attempt to repair a non-manifold STL file.
111
+
112
+ Args:
113
+ stl_file: Path to the STL file to repair
114
+
115
+ Returns:
116
+ Tuple of (success, error_message)
117
+ """
118
+ # This is a placeholder for STL repair functionality
119
+ # In a real implementation, you would use a library like admesh or meshlab
120
+ # to repair the STL file
121
+
122
+ logger.warning(f"STL repair not implemented: {stl_file}")
123
+ return False, "STL repair not implemented"
@@ -0,0 +1,52 @@
1
+ import os
2
+ import logging
3
+ from PIL import Image, ImageDraw
4
+ from typing import Dict, Any, Optional
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ class HeadlessRenderer:
9
+ """Provides rendering capabilities for OpenSCAD in headless environments."""
10
+
11
+ def __init__(self, openscad_path: str = "openscad"):
12
+ self.openscad_path = openscad_path
13
+ self.camera_angles = {
14
+ "front": "0,0,0,0,0,0,50",
15
+ "top": "0,0,0,90,0,0,50",
16
+ "right": "0,0,0,0,0,90,50",
17
+ "perspective": "70,0,35,25,0,25,250"
18
+ }
19
+
20
+ def create_placeholder_image(self, output_path: str, model_id: str, view: str = "perspective") -> str:
21
+ """Create a placeholder image with model information."""
22
+ try:
23
+ # Create a blank image
24
+ width, height = 800, 600
25
+ image = Image.new('RGB', (width, height), color=(240, 240, 240))
26
+ draw = ImageDraw.Draw(image)
27
+
28
+ # Add text
29
+ draw.text((20, 20), f"OpenSCAD Model: {model_id}", fill=(0, 0, 0))
30
+ draw.text((20, 60), f"View: {view}", fill=(0, 0, 0))
31
+ draw.text((20, 100), "Headless rendering mode", fill=(0, 0, 0))
32
+
33
+ # Draw a simple 3D shape
34
+ draw.polygon([(400, 200), (300, 300), (500, 300)], outline=(0, 0, 0), width=2)
35
+ draw.polygon([(400, 200), (500, 300), (500, 400)], outline=(0, 0, 0), width=2)
36
+ draw.polygon([(400, 200), (300, 300), (300, 400)], outline=(0, 0, 0), width=2)
37
+ draw.rectangle((300, 300, 500, 400), outline=(0, 0, 0), width=2)
38
+
39
+ # Add note about headless mode
40
+ note = "Note: This is a placeholder image. OpenSCAD preview generation"
41
+ note2 = "requires an X server or a headless rendering solution."
42
+ draw.text((20, 500), note, fill=(150, 0, 0))
43
+ draw.text((20, 530), note2, fill=(150, 0, 0))
44
+
45
+ # Save the image
46
+ image.save(output_path)
47
+ logger.info(f"Created placeholder image: {output_path}")
48
+
49
+ return output_path
50
+ except Exception as e:
51
+ logger.error(f"Error creating placeholder image: {str(e)}")
52
+ return output_path
@@ -0,0 +1,177 @@
1
+ import os
2
+ import subprocess
3
+ import logging
4
+ from typing import Dict, Any, Optional
5
+ from PIL import Image, ImageDraw, ImageFont
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class Renderer:
10
+ """
11
+ Handles rendering of OpenSCAD models to preview images.
12
+ Implements multi-angle views and fallback rendering when headless mode fails.
13
+ """
14
+
15
+ def __init__(self, openscad_wrapper):
16
+ """
17
+ Initialize the renderer.
18
+
19
+ Args:
20
+ openscad_wrapper: Instance of OpenSCADWrapper for generating previews
21
+ """
22
+ self.openscad_wrapper = openscad_wrapper
23
+
24
+ # Standard camera angles for multi-view rendering
25
+ self.camera_angles = {
26
+ 'front': "0,0,0,0,0,0,50",
27
+ 'top': "0,0,0,90,0,0,50",
28
+ 'right': "0,0,0,0,90,0,50",
29
+ 'perspective': "20,20,20,55,0,25,100"
30
+ }
31
+
32
+ def generate_preview(self, scad_file: str, parameters: Optional[Dict[str, Any]] = None) -> str:
33
+ """
34
+ Generate a preview image for a SCAD file.
35
+
36
+ Args:
37
+ scad_file: Path to the SCAD file
38
+ parameters: Optional parameters to override in the SCAD file
39
+
40
+ Returns:
41
+ Path to the generated preview image
42
+ """
43
+ try:
44
+ # Try to generate a preview using OpenSCAD
45
+ preview_file = self.openscad_wrapper.generate_preview(
46
+ scad_file,
47
+ parameters,
48
+ camera_position=self.camera_angles['perspective'],
49
+ image_size="800,600"
50
+ )
51
+
52
+ # Check if the file exists and has content
53
+ if os.path.exists(preview_file) and os.path.getsize(preview_file) > 0:
54
+ return preview_file
55
+ else:
56
+ # If the file doesn't exist or is empty, create a placeholder
57
+ return self._create_placeholder_image(preview_file)
58
+ except Exception as e:
59
+ logger.error(f"Error generating preview: {str(e)}")
60
+ # Create a placeholder image
61
+ model_id = os.path.basename(scad_file).split('.')[0]
62
+ preview_file = os.path.join(self.openscad_wrapper.preview_dir, f"{model_id}.png")
63
+ return self._create_placeholder_image(preview_file)
64
+
65
+ def generate_multi_angle_previews(self, scad_file: str, parameters: Optional[Dict[str, Any]] = None) -> Dict[str, str]:
66
+ """
67
+ Generate preview images from multiple angles.
68
+
69
+ Args:
70
+ scad_file: Path to the SCAD file
71
+ parameters: Optional parameters to override in the SCAD file
72
+
73
+ Returns:
74
+ Dictionary mapping angle names to preview image paths
75
+ """
76
+ previews = {}
77
+ model_id = os.path.basename(scad_file).split('.')[0]
78
+
79
+ for angle_name, camera_position in self.camera_angles.items():
80
+ preview_file = os.path.join(
81
+ self.openscad_wrapper.preview_dir,
82
+ f"{model_id}_{angle_name}.png"
83
+ )
84
+
85
+ try:
86
+ # Try to generate a preview using OpenSCAD
87
+ preview_file = self.openscad_wrapper.generate_preview(
88
+ scad_file,
89
+ parameters,
90
+ camera_position=camera_position,
91
+ image_size="800,600"
92
+ )
93
+
94
+ # Check if the file exists and has content
95
+ if os.path.exists(preview_file) and os.path.getsize(preview_file) > 0:
96
+ previews[angle_name] = preview_file
97
+ else:
98
+ # If the file doesn't exist or is empty, create a placeholder
99
+ previews[angle_name] = self._create_placeholder_image(preview_file, angle_name)
100
+ except Exception as e:
101
+ logger.error(f"Error generating {angle_name} preview: {str(e)}")
102
+ # Create a placeholder image
103
+ previews[angle_name] = self._create_placeholder_image(preview_file, angle_name)
104
+
105
+ return previews
106
+
107
+ def _create_placeholder_image(self, output_path: str, angle_name: str = "perspective") -> str:
108
+ """
109
+ Create a placeholder image when OpenSCAD rendering fails.
110
+
111
+ Args:
112
+ output_path: Path to save the placeholder image
113
+ angle_name: Name of the camera angle for the placeholder
114
+
115
+ Returns:
116
+ Path to the created placeholder image
117
+ """
118
+ try:
119
+ # Create a blank image
120
+ img = Image.new('RGB', (800, 600), color=(240, 240, 240))
121
+ draw = ImageDraw.Draw(img)
122
+
123
+ # Add text
124
+ draw.text((400, 280), f"Preview not available", fill=(0, 0, 0))
125
+ draw.text((400, 320), f"View: {angle_name}", fill=(0, 0, 0))
126
+
127
+ # Save the image
128
+ img.save(output_path)
129
+ logger.info(f"Created placeholder image: {output_path}")
130
+ return output_path
131
+ except Exception as e:
132
+ logger.error(f"Error creating placeholder image: {str(e)}")
133
+ # If all else fails, return the path anyway
134
+ return output_path
135
+
136
+ def create_composite_preview(self, previews: Dict[str, str], output_path: str) -> str:
137
+ """
138
+ Create a composite image from multiple angle previews.
139
+
140
+ Args:
141
+ previews: Dictionary mapping angle names to preview image paths
142
+ output_path: Path to save the composite image
143
+
144
+ Returns:
145
+ Path to the created composite image
146
+ """
147
+ try:
148
+ # Create a blank image
149
+ img = Image.new('RGB', (1600, 1200), color=(240, 240, 240))
150
+
151
+ # Load and paste each preview
152
+ positions = {
153
+ 'perspective': (0, 0),
154
+ 'front': (800, 0),
155
+ 'top': (0, 600),
156
+ 'right': (800, 600)
157
+ }
158
+
159
+ for angle_name, preview_path in previews.items():
160
+ if angle_name in positions and os.path.exists(preview_path):
161
+ try:
162
+ angle_img = Image.open(preview_path)
163
+ # Resize if needed
164
+ angle_img = angle_img.resize((800, 600))
165
+ # Paste into composite
166
+ img.paste(angle_img, positions[angle_name])
167
+ except Exception as e:
168
+ logger.error(f"Error processing {angle_name} preview: {str(e)}")
169
+
170
+ # Save the composite image
171
+ img.save(output_path)
172
+ logger.info(f"Created composite preview: {output_path}")
173
+ return output_path
174
+ except Exception as e:
175
+ logger.error(f"Error creating composite preview: {str(e)}")
176
+ # If all else fails, return the path anyway
177
+ return output_path