@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.
- package/README.md +116 -0
- package/config/mcp/antigravity.json +22 -0
- package/config/mcp/claude.json +22 -0
- package/config/mcp/codex.toml +25 -0
- package/docs/adr/ADR-001-KICAD-OPENSCAD-MCP-STRATEGY.md +334 -0
- package/docs/review_documentation.md +121 -0
- package/installer.js +358 -0
- package/mcp_servers/kicad-mcp-server/.env.example +22 -0
- package/mcp_servers/kicad-mcp-server/.github/workflows/ci.yml +36 -0
- package/mcp_servers/kicad-mcp-server/CLAUDE.md +487 -0
- package/mcp_servers/kicad-mcp-server/README.md +316 -0
- package/mcp_servers/kicad-mcp-server/docs/DEVICE_TREE.md +416 -0
- package/mcp_servers/kicad-mcp-server/docs/INSTALLATION.md +332 -0
- package/mcp_servers/kicad-mcp-server/docs/PIN_ANALYSIS.md +332 -0
- package/mcp_servers/kicad-mcp-server/docs/README.md +240 -0
- package/mcp_servers/kicad-mcp-server/docs/TESTING.md +613 -0
- package/mcp_servers/kicad-mcp-server/docs/VALIDATION.md +268 -0
- package/mcp_servers/kicad-mcp-server/pyproject.toml +96 -0
- package/mcp_servers/kicad-mcp-server/requirements-dev.txt +16 -0
- package/mcp_servers/kicad-mcp-server/requirements-test.txt +24 -0
- package/mcp_servers/kicad-mcp-server/requirements.txt +13 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__init__.py +3 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__main__.py +17 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/config.py +46 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/types.py +87 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/netlist_parser.py +234 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser.py +375 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser_kicad.py +327 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/schematic_parser.py +902 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/server.py +71 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/arduino/connectivity_test.cpp.j2 +189 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/atmega.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/esp32.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/nrf52.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/stm32f4.dts.j2 +89 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/esp_idf/test_suite.c.j2 +340 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/pytest/test_connectivity.py.j2 +147 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/st_hal/hal_test.c.j2 +313 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_gpio_test.py.j2 +99 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_i2c_test.py.j2 +117 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_pinmux_test.py.j2 +43 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_spi_test.py.j2 +94 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_gpio_test.c.j2 +113 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_i2c_test.c.j2 +101 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_spi_test.c.j2 +94 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/unittest/test_schematic.py.j2 +172 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/__init__.py +36 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/device_tree.py +1187 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/hierarchical_analysis.py +211 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/netlist.py +320 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/parts_registry.py +142 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb.py +955 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb_layout.py +308 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pin_analysis.py +765 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/project.py +196 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic.py +319 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_editor.py +674 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_search.py +158 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/validation.py +866 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/visualization.py +225 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/file_handlers.py +65 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_cli.py +103 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_version.py +103 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/parts_registry.py +197 -0
- package/mcp_servers/kicad-mcp-server/tests/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/examples/ESP32S3_TEST.md +219 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/README.md +65 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/example_pcb.kicad_pcb +177 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/example_schematic.kicad_sch +145 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/child.kicad_sch +24 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/root.kicad_sch +38 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_hierarchical_labels.py +195 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_kicad_cli.py +116 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_netlist_cache_path.py +29 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic.py +189 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic_hierarchy.py +69 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_visualization.py +136 -0
- package/mcp_servers/kicad-mcp-server/uv.lock +2873 -0
- package/mcp_servers/openscad-mcp-server/.dockerignore +9 -0
- package/mcp_servers/openscad-mcp-server/.github/workflows/test.yml +40 -0
- package/mcp_servers/openscad-mcp-server/Dockerfile +29 -0
- package/mcp_servers/openscad-mcp-server/LICENSE +21 -0
- package/mcp_servers/openscad-mcp-server/README.md +154 -0
- package/mcp_servers/openscad-mcp-server/docs/audit.md +56 -0
- package/mcp_servers/openscad-mcp-server/docs/docker.md +66 -0
- package/mcp_servers/openscad-mcp-server/docs/issue-followup.md +19 -0
- package/mcp_servers/openscad-mcp-server/docs/jetson.md +17 -0
- package/mcp_servers/openscad-mcp-server/glama.json +4 -0
- package/mcp_servers/openscad-mcp-server/legacy/README.md +15 -0
- package/mcp_servers/openscad-mcp-server/legacy/README.original.md +294 -0
- package/mcp_servers/openscad-mcp-server/legacy/implementation_plan.md +100 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/download_sam2_checkpoint.py +115 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/ai/sam_segmentation.py +209 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/models/threestudio_generator.py +231 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/workflow/image_to_model_pipeline.py +260 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/test_sam2_segmentation.py +96 -0
- package/mcp_servers/openscad-mcp-server/legacy/requirements.txt +57 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/README.md +39 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/ai-driven-code-generation.md +122 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/export-formats.md +76 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/ai/ai_service.py.md +51 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/main.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/models/code_generator.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/nlp/parameter_extractor.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/ai/natural-language-processing.md +78 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/nlp/parameter-extraction.md +173 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/export-formats.md +91 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/openscad-basics.md +66 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/primitive-testing.md +79 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/ai_service.py +257 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/gemini_api.py +161 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/venice_api.py +203 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/config.py +121 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main.py +1456 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main.py.new +404 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main_remote.py +401 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/code_generator.py +321 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/cuda_mvs.py +209 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/scad_templates/basic_shapes.scad +144 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/nlp/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/nlp/parameter_extractor.py +388 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/wrapper.py +418 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/__init__.py +1 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/printer_discovery.py +471 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/connection_manager.py +537 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_client.py +435 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_server.py +787 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/error_handling.py +415 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/primitive_tester.py +203 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/test_primitives.py +98 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/__init__.py +1 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/cad_exporter.py +241 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/format_validator.py +206 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_exporter.py +140 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_repair.py +91 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_validator.py +123 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/headless_renderer.py +52 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/renderer.py +177 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/web_interface.py +639 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/workflow/image_approval.py +148 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/workflow/multi_view_to_model_pipeline.py +338 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_complete_workflow.py +374 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_cuda_mvs.py +191 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_gemini_api.py +168 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_approval.py +192 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_approval_workflow.py +251 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_to_model_pipeline.py +145 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_model_selection.py +41 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_multi_view_pipeline.py +290 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_primitives.sh +13 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_rabbit_direct.py +71 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_remote_cuda_mvs.py +283 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_venice_example.py +69 -0
- package/mcp_servers/openscad-mcp-server/pyproject.toml +33 -0
- package/mcp_servers/openscad-mcp-server/requirements.txt +2 -0
- package/mcp_servers/openscad-mcp-server/scad/simple_cube.scad +2 -0
- package/mcp_servers/openscad-mcp-server/scripts/test_docker.py +220 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__init__.py +3 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__main__.py +3 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/engine.py +94 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/geometry.py +242 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/server.py +245 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/service.py +190 -0
- package/mcp_servers/openscad-mcp-server/tests/conftest.py +17 -0
- package/mcp_servers/openscad-mcp-server/tests/test_engine.py +37 -0
- package/mcp_servers/openscad-mcp-server/tests/test_geometry.py +106 -0
- package/mcp_servers/openscad-mcp-server/tests/test_integration.py +172 -0
- package/mcp_servers/openscad-mcp-server/tests/test_transports.py +314 -0
- package/mcp_servers/openscad-mcp-server/uv.lock +1123 -0
- package/package.json +44 -0
- package/scripts/install_mcps.sh +86 -0
- package/scripts/openscad_wrapper.sh +62 -0
- package/scripts/run_kicad_mcp.sh +25 -0
- package/scripts/run_openscad_mcp.sh +41 -0
- package/scripts/test_mcp_connection.py +626 -0
- package/scripts/test_mcp_connection.sh +168 -0
- package/skills/code-first-hardware-design/SKILL.md +260 -0
- package/skills/pcb-constraint-definition/SKILL.md +236 -0
- package/skills/pcb-layout-routing-automation/SKILL.md +153 -0
- package/skills/pcb-validation-dfm-signoff/SKILL.md +194 -0
- 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"
|
|
File without changes
|
|
@@ -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
|