@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,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 @@
|
|
|
1
|
+
# Utils package
|
|
@@ -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 ""
|