@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,639 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import base64
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Dict, Any, List, Optional
|
|
5
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
6
|
+
from fastapi.responses import HTMLResponse
|
|
7
|
+
from fastapi.templating import Jinja2Templates
|
|
8
|
+
from fastapi.staticfiles import StaticFiles
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
class WebInterface:
|
|
13
|
+
"""
|
|
14
|
+
Web interface for displaying model previews and managing 3D models.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, app, static_dir: str, templates_dir: str, output_dir: str):
|
|
18
|
+
"""
|
|
19
|
+
Initialize the web interface.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
app: FastAPI application
|
|
23
|
+
static_dir: Directory for static files
|
|
24
|
+
templates_dir: Directory for templates
|
|
25
|
+
output_dir: Directory containing output files (STL, PNG)
|
|
26
|
+
"""
|
|
27
|
+
self.app = app
|
|
28
|
+
self.static_dir = static_dir
|
|
29
|
+
self.templates_dir = templates_dir
|
|
30
|
+
self.output_dir = output_dir
|
|
31
|
+
self.preview_dir = os.path.join(output_dir, "preview")
|
|
32
|
+
self.stl_dir = os.path.join(output_dir, "stl")
|
|
33
|
+
|
|
34
|
+
# Create directories if they don't exist
|
|
35
|
+
os.makedirs(self.static_dir, exist_ok=True)
|
|
36
|
+
os.makedirs(self.templates_dir, exist_ok=True)
|
|
37
|
+
|
|
38
|
+
# Create router
|
|
39
|
+
self.router = APIRouter(prefix="/ui", tags=["UI"])
|
|
40
|
+
|
|
41
|
+
# Set up static files
|
|
42
|
+
self.app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
|
43
|
+
|
|
44
|
+
# Set up templates
|
|
45
|
+
self.templates = Jinja2Templates(directory=templates_dir)
|
|
46
|
+
|
|
47
|
+
# Register routes
|
|
48
|
+
self._register_routes()
|
|
49
|
+
|
|
50
|
+
# Create template files
|
|
51
|
+
self._create_template_files()
|
|
52
|
+
|
|
53
|
+
# Create static files
|
|
54
|
+
self._create_static_files()
|
|
55
|
+
|
|
56
|
+
def _register_routes(self):
|
|
57
|
+
"""Register routes for the web interface."""
|
|
58
|
+
# Home page
|
|
59
|
+
@self.router.get("/", response_class=HTMLResponse)
|
|
60
|
+
async def home(request: Request):
|
|
61
|
+
return self.templates.TemplateResponse("index.html", {"request": request})
|
|
62
|
+
|
|
63
|
+
# Model preview page
|
|
64
|
+
@self.router.get("/preview/{model_id}", response_class=HTMLResponse)
|
|
65
|
+
async def preview(request: Request, model_id: str):
|
|
66
|
+
# Check if preview exists
|
|
67
|
+
preview_file = os.path.join(self.preview_dir, f"{model_id}.png")
|
|
68
|
+
if not os.path.exists(preview_file):
|
|
69
|
+
raise HTTPException(status_code=404, detail="Preview not found")
|
|
70
|
+
|
|
71
|
+
# Get multi-angle previews if they exist
|
|
72
|
+
angles = ["front", "top", "right", "perspective"]
|
|
73
|
+
previews = {}
|
|
74
|
+
|
|
75
|
+
for angle in angles:
|
|
76
|
+
angle_file = os.path.join(self.preview_dir, f"{model_id}_{angle}.png")
|
|
77
|
+
if os.path.exists(angle_file):
|
|
78
|
+
previews[angle] = f"/api/preview/{model_id}_{angle}"
|
|
79
|
+
|
|
80
|
+
# If no multi-angle previews, use the main preview
|
|
81
|
+
if not previews:
|
|
82
|
+
previews["main"] = f"/api/preview/{model_id}"
|
|
83
|
+
|
|
84
|
+
# Check if STL exists
|
|
85
|
+
stl_file = os.path.join(self.stl_dir, f"{model_id}.stl")
|
|
86
|
+
stl_url = f"/api/stl/{model_id}" if os.path.exists(stl_file) else None
|
|
87
|
+
|
|
88
|
+
return self.templates.TemplateResponse(
|
|
89
|
+
"preview.html",
|
|
90
|
+
{
|
|
91
|
+
"request": request,
|
|
92
|
+
"model_id": model_id,
|
|
93
|
+
"previews": previews,
|
|
94
|
+
"stl_url": stl_url
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# List all models
|
|
99
|
+
@self.router.get("/models", response_class=HTMLResponse)
|
|
100
|
+
async def list_models(request: Request):
|
|
101
|
+
# Get all STL files
|
|
102
|
+
stl_files = []
|
|
103
|
+
if os.path.exists(self.stl_dir):
|
|
104
|
+
stl_files = [f for f in os.listdir(self.stl_dir) if f.endswith(".stl")]
|
|
105
|
+
|
|
106
|
+
# Extract model IDs
|
|
107
|
+
model_ids = [os.path.splitext(f)[0] for f in stl_files]
|
|
108
|
+
|
|
109
|
+
# Get preview URLs
|
|
110
|
+
models = []
|
|
111
|
+
for model_id in model_ids:
|
|
112
|
+
preview_file = os.path.join(self.preview_dir, f"{model_id}.png")
|
|
113
|
+
preview_url = f"/api/preview/{model_id}" if os.path.exists(preview_file) else None
|
|
114
|
+
stl_url = f"/api/stl/{model_id}"
|
|
115
|
+
|
|
116
|
+
models.append({
|
|
117
|
+
"id": model_id,
|
|
118
|
+
"preview_url": preview_url,
|
|
119
|
+
"stl_url": stl_url
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return self.templates.TemplateResponse(
|
|
123
|
+
"models.html",
|
|
124
|
+
{"request": request, "models": models}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# API endpoints for serving files
|
|
128
|
+
|
|
129
|
+
# Serve preview image
|
|
130
|
+
@self.app.get("/api/preview/{preview_id}")
|
|
131
|
+
async def get_preview(preview_id: str):
|
|
132
|
+
preview_file = os.path.join(self.preview_dir, f"{preview_id}.png")
|
|
133
|
+
if not os.path.exists(preview_file):
|
|
134
|
+
raise HTTPException(status_code=404, detail="Preview not found")
|
|
135
|
+
|
|
136
|
+
# Return the file
|
|
137
|
+
with open(preview_file, "rb") as f:
|
|
138
|
+
content = f.read()
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
"content": base64.b64encode(content).decode("utf-8"),
|
|
142
|
+
"content_type": "image/png"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Serve STL file
|
|
146
|
+
@self.app.get("/api/stl/{model_id}")
|
|
147
|
+
async def get_stl(model_id: str):
|
|
148
|
+
stl_file = os.path.join(self.stl_dir, f"{model_id}.stl")
|
|
149
|
+
if not os.path.exists(stl_file):
|
|
150
|
+
raise HTTPException(status_code=404, detail="STL file not found")
|
|
151
|
+
|
|
152
|
+
# Return the file
|
|
153
|
+
with open(stl_file, "rb") as f:
|
|
154
|
+
content = f.read()
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
"content": base64.b64encode(content).decode("utf-8"),
|
|
158
|
+
"content_type": "application/octet-stream",
|
|
159
|
+
"filename": f"{model_id}.stl"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Register the router with the app
|
|
163
|
+
self.app.include_router(self.router)
|
|
164
|
+
|
|
165
|
+
def _create_template_files(self):
|
|
166
|
+
"""Create template files for the web interface."""
|
|
167
|
+
# Create base template
|
|
168
|
+
base_template = """<!DOCTYPE html>
|
|
169
|
+
<html>
|
|
170
|
+
<head>
|
|
171
|
+
<meta charset="utf-8">
|
|
172
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
173
|
+
<title>{% block title %}OpenSCAD MCP Server{% endblock %}</title>
|
|
174
|
+
<link rel="stylesheet" href="/static/styles.css">
|
|
175
|
+
</head>
|
|
176
|
+
<body>
|
|
177
|
+
<header>
|
|
178
|
+
<h1>OpenSCAD MCP Server</h1>
|
|
179
|
+
<nav>
|
|
180
|
+
<ul>
|
|
181
|
+
<li><a href="/ui/">Home</a></li>
|
|
182
|
+
<li><a href="/ui/models">Models</a></li>
|
|
183
|
+
</ul>
|
|
184
|
+
</nav>
|
|
185
|
+
</header>
|
|
186
|
+
|
|
187
|
+
<main>
|
|
188
|
+
{% block content %}{% endblock %}
|
|
189
|
+
</main>
|
|
190
|
+
|
|
191
|
+
<footer>
|
|
192
|
+
<p>OpenSCAD MCP Server - Model Context Protocol Implementation</p>
|
|
193
|
+
</footer>
|
|
194
|
+
|
|
195
|
+
<script src="/static/script.js"></script>
|
|
196
|
+
</body>
|
|
197
|
+
</html>
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
# Create index template
|
|
201
|
+
index_template = """{% extends "base.html" %}
|
|
202
|
+
|
|
203
|
+
{% block title %}OpenSCAD MCP Server - Home{% endblock %}
|
|
204
|
+
|
|
205
|
+
{% block content %}
|
|
206
|
+
<section class="hero">
|
|
207
|
+
<h2>Welcome to OpenSCAD MCP Server</h2>
|
|
208
|
+
<p>A Model Context Protocol server for generating 3D models with OpenSCAD</p>
|
|
209
|
+
</section>
|
|
210
|
+
|
|
211
|
+
<section class="features">
|
|
212
|
+
<div class="feature">
|
|
213
|
+
<h3>Natural Language Processing</h3>
|
|
214
|
+
<p>Describe 3D objects in natural language and get parametric models</p>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div class="feature">
|
|
218
|
+
<h3>Preview Generation</h3>
|
|
219
|
+
<p>See your models from multiple angles before exporting</p>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div class="feature">
|
|
223
|
+
<h3>STL Export</h3>
|
|
224
|
+
<p>Generate STL files ready for 3D printing</p>
|
|
225
|
+
</div>
|
|
226
|
+
</section>
|
|
227
|
+
{% endblock %}
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
# Create preview template
|
|
231
|
+
preview_template = """{% extends "base.html" %}
|
|
232
|
+
|
|
233
|
+
{% block title %}Model Preview - {{ model_id }}{% endblock %}
|
|
234
|
+
|
|
235
|
+
{% block content %}
|
|
236
|
+
<section class="model-preview">
|
|
237
|
+
<h2>Model Preview: {{ model_id }}</h2>
|
|
238
|
+
|
|
239
|
+
<div class="preview-container">
|
|
240
|
+
{% for angle, url in previews.items() %}
|
|
241
|
+
<div class="preview-angle">
|
|
242
|
+
<h3>{{ angle|title }} View</h3>
|
|
243
|
+
<img src="{{ url }}" alt="{{ angle }} view of {{ model_id }}" class="preview-image" data-angle="{{ angle }}">
|
|
244
|
+
</div>
|
|
245
|
+
{% endfor %}
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
{% if stl_url %}
|
|
249
|
+
<div class="download-container">
|
|
250
|
+
<a href="{{ stl_url }}" class="download-button" download="{{ model_id }}.stl">Download STL</a>
|
|
251
|
+
</div>
|
|
252
|
+
{% endif %}
|
|
253
|
+
</section>
|
|
254
|
+
{% endblock %}
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
# Create models template
|
|
258
|
+
models_template = """{% extends "base.html" %}
|
|
259
|
+
|
|
260
|
+
{% block title %}All Models{% endblock %}
|
|
261
|
+
|
|
262
|
+
{% block content %}
|
|
263
|
+
<section class="models-list">
|
|
264
|
+
<h2>All Models</h2>
|
|
265
|
+
|
|
266
|
+
{% if models %}
|
|
267
|
+
<div class="models-grid">
|
|
268
|
+
{% for model in models %}
|
|
269
|
+
<div class="model-card">
|
|
270
|
+
<h3>{{ model.id }}</h3>
|
|
271
|
+
{% if model.preview_url %}
|
|
272
|
+
<img src="{{ model.preview_url }}" alt="Preview of {{ model.id }}" class="model-thumbnail">
|
|
273
|
+
{% else %}
|
|
274
|
+
<div class="no-preview">No preview available</div>
|
|
275
|
+
{% endif %}
|
|
276
|
+
<div class="model-actions">
|
|
277
|
+
<a href="/ui/preview/{{ model.id }}" class="view-button">View</a>
|
|
278
|
+
<a href="{{ model.stl_url }}" class="download-button" download="{{ model.id }}.stl">Download</a>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
{% endfor %}
|
|
282
|
+
</div>
|
|
283
|
+
{% else %}
|
|
284
|
+
<p>No models found.</p>
|
|
285
|
+
{% endif %}
|
|
286
|
+
</section>
|
|
287
|
+
{% endblock %}
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
# Write templates to files
|
|
291
|
+
os.makedirs(self.templates_dir, exist_ok=True)
|
|
292
|
+
|
|
293
|
+
with open(os.path.join(self.templates_dir, "base.html"), "w") as f:
|
|
294
|
+
f.write(base_template)
|
|
295
|
+
|
|
296
|
+
with open(os.path.join(self.templates_dir, "index.html"), "w") as f:
|
|
297
|
+
f.write(index_template)
|
|
298
|
+
|
|
299
|
+
with open(os.path.join(self.templates_dir, "preview.html"), "w") as f:
|
|
300
|
+
f.write(preview_template)
|
|
301
|
+
|
|
302
|
+
with open(os.path.join(self.templates_dir, "models.html"), "w") as f:
|
|
303
|
+
f.write(models_template)
|
|
304
|
+
|
|
305
|
+
def _create_static_files(self):
|
|
306
|
+
"""Create static files for the web interface."""
|
|
307
|
+
# Create CSS file
|
|
308
|
+
css = """/* Base styles */
|
|
309
|
+
* {
|
|
310
|
+
box-sizing: border-box;
|
|
311
|
+
margin: 0;
|
|
312
|
+
padding: 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
body {
|
|
316
|
+
font-family: Arial, sans-serif;
|
|
317
|
+
line-height: 1.6;
|
|
318
|
+
color: #333;
|
|
319
|
+
background-color: #f4f4f4;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
header {
|
|
323
|
+
background-color: #333;
|
|
324
|
+
color: #fff;
|
|
325
|
+
padding: 1rem;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
header h1 {
|
|
329
|
+
margin-bottom: 0.5rem;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
nav ul {
|
|
333
|
+
display: flex;
|
|
334
|
+
list-style: none;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
nav ul li {
|
|
338
|
+
margin-right: 1rem;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
nav ul li a {
|
|
342
|
+
color: #fff;
|
|
343
|
+
text-decoration: none;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
nav ul li a:hover {
|
|
347
|
+
text-decoration: underline;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
main {
|
|
351
|
+
max-width: 1200px;
|
|
352
|
+
margin: 0 auto;
|
|
353
|
+
padding: 2rem;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
footer {
|
|
357
|
+
background-color: #333;
|
|
358
|
+
color: #fff;
|
|
359
|
+
text-align: center;
|
|
360
|
+
padding: 1rem;
|
|
361
|
+
margin-top: 2rem;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* Home page */
|
|
365
|
+
.hero {
|
|
366
|
+
text-align: center;
|
|
367
|
+
margin-bottom: 2rem;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.hero h2 {
|
|
371
|
+
font-size: 2rem;
|
|
372
|
+
margin-bottom: 1rem;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.features {
|
|
376
|
+
display: flex;
|
|
377
|
+
justify-content: space-between;
|
|
378
|
+
flex-wrap: wrap;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.feature {
|
|
382
|
+
flex: 1;
|
|
383
|
+
background-color: #fff;
|
|
384
|
+
padding: 1.5rem;
|
|
385
|
+
margin: 0.5rem;
|
|
386
|
+
border-radius: 5px;
|
|
387
|
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.feature h3 {
|
|
391
|
+
margin-bottom: 1rem;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/* Preview page */
|
|
395
|
+
.model-preview h2 {
|
|
396
|
+
margin-bottom: 1.5rem;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.preview-container {
|
|
400
|
+
display: flex;
|
|
401
|
+
flex-wrap: wrap;
|
|
402
|
+
gap: 1rem;
|
|
403
|
+
margin-bottom: 1.5rem;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.preview-angle {
|
|
407
|
+
flex: 1;
|
|
408
|
+
min-width: 300px;
|
|
409
|
+
background-color: #fff;
|
|
410
|
+
padding: 1rem;
|
|
411
|
+
border-radius: 5px;
|
|
412
|
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.preview-angle h3 {
|
|
416
|
+
margin-bottom: 0.5rem;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.preview-image {
|
|
420
|
+
width: 100%;
|
|
421
|
+
height: auto;
|
|
422
|
+
border: 1px solid #ddd;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.download-container {
|
|
426
|
+
text-align: center;
|
|
427
|
+
margin-top: 1rem;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.download-button {
|
|
431
|
+
display: inline-block;
|
|
432
|
+
background-color: #4CAF50;
|
|
433
|
+
color: white;
|
|
434
|
+
padding: 0.5rem 1rem;
|
|
435
|
+
text-decoration: none;
|
|
436
|
+
border-radius: 4px;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.download-button:hover {
|
|
440
|
+
background-color: #45a049;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* Models page */
|
|
444
|
+
.models-grid {
|
|
445
|
+
display: grid;
|
|
446
|
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
447
|
+
gap: 1.5rem;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.model-card {
|
|
451
|
+
background-color: #fff;
|
|
452
|
+
border-radius: 5px;
|
|
453
|
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
454
|
+
overflow: hidden;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.model-card h3 {
|
|
458
|
+
padding: 1rem;
|
|
459
|
+
background-color: #f8f8f8;
|
|
460
|
+
border-bottom: 1px solid #eee;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.model-thumbnail {
|
|
464
|
+
width: 100%;
|
|
465
|
+
height: 200px;
|
|
466
|
+
object-fit: contain;
|
|
467
|
+
background-color: #f4f4f4;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.no-preview {
|
|
471
|
+
width: 100%;
|
|
472
|
+
height: 200px;
|
|
473
|
+
display: flex;
|
|
474
|
+
align-items: center;
|
|
475
|
+
justify-content: center;
|
|
476
|
+
background-color: #f4f4f4;
|
|
477
|
+
color: #999;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.model-actions {
|
|
481
|
+
display: flex;
|
|
482
|
+
padding: 1rem;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.view-button, .download-button {
|
|
486
|
+
flex: 1;
|
|
487
|
+
text-align: center;
|
|
488
|
+
padding: 0.5rem;
|
|
489
|
+
text-decoration: none;
|
|
490
|
+
border-radius: 4px;
|
|
491
|
+
margin: 0 0.25rem;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.view-button {
|
|
495
|
+
background-color: #2196F3;
|
|
496
|
+
color: white;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.view-button:hover {
|
|
500
|
+
background-color: #0b7dda;
|
|
501
|
+
}
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
# Create JavaScript file
|
|
505
|
+
js = """// JavaScript for OpenSCAD MCP Server web interface
|
|
506
|
+
|
|
507
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
508
|
+
// Handle image loading errors
|
|
509
|
+
const images = document.querySelectorAll('img');
|
|
510
|
+
images.forEach(img => {
|
|
511
|
+
img.onerror = function() {
|
|
512
|
+
this.src = '/static/placeholder.png';
|
|
513
|
+
};
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// Handle STL download
|
|
517
|
+
const downloadButtons = document.querySelectorAll('.download-button');
|
|
518
|
+
downloadButtons.forEach(button => {
|
|
519
|
+
button.addEventListener('click', async function(e) {
|
|
520
|
+
e.preventDefault();
|
|
521
|
+
|
|
522
|
+
const url = this.getAttribute('href');
|
|
523
|
+
const filename = this.hasAttribute('download') ? this.getAttribute('download') : 'model.stl';
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
const response = await fetch(url);
|
|
527
|
+
const data = await response.json();
|
|
528
|
+
|
|
529
|
+
// Decode base64 content
|
|
530
|
+
const content = atob(data.content);
|
|
531
|
+
|
|
532
|
+
// Convert to Blob
|
|
533
|
+
const bytes = new Uint8Array(content.length);
|
|
534
|
+
for (let i = 0; i < content.length; i++) {
|
|
535
|
+
bytes[i] = content.charCodeAt(i);
|
|
536
|
+
}
|
|
537
|
+
const blob = new Blob([bytes], { type: data.content_type });
|
|
538
|
+
|
|
539
|
+
// Create download link
|
|
540
|
+
const downloadLink = document.createElement('a');
|
|
541
|
+
downloadLink.href = URL.createObjectURL(blob);
|
|
542
|
+
downloadLink.download = data.filename || filename;
|
|
543
|
+
|
|
544
|
+
// Trigger download
|
|
545
|
+
document.body.appendChild(downloadLink);
|
|
546
|
+
downloadLink.click();
|
|
547
|
+
document.body.removeChild(downloadLink);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
console.error('Error downloading file:', error);
|
|
550
|
+
alert('Error downloading file. Please try again.');
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// Handle preview images
|
|
556
|
+
const previewImages = document.querySelectorAll('.preview-image');
|
|
557
|
+
previewImages.forEach(img => {
|
|
558
|
+
img.addEventListener('click', function() {
|
|
559
|
+
const url = this.getAttribute('src');
|
|
560
|
+
const angle = this.getAttribute('data-angle');
|
|
561
|
+
|
|
562
|
+
// Create modal for larger view
|
|
563
|
+
const modal = document.createElement('div');
|
|
564
|
+
modal.className = 'preview-modal';
|
|
565
|
+
modal.innerHTML = `
|
|
566
|
+
<div class="modal-content">
|
|
567
|
+
<span class="close-button">×</span>
|
|
568
|
+
<h3>${angle ? angle.charAt(0).toUpperCase() + angle.slice(1) + ' View' : 'Preview'}</h3>
|
|
569
|
+
<img src="${url}" alt="Preview">
|
|
570
|
+
</div>
|
|
571
|
+
`;
|
|
572
|
+
|
|
573
|
+
// Add modal styles
|
|
574
|
+
modal.style.position = 'fixed';
|
|
575
|
+
modal.style.top = '0';
|
|
576
|
+
modal.style.left = '0';
|
|
577
|
+
modal.style.width = '100%';
|
|
578
|
+
modal.style.height = '100%';
|
|
579
|
+
modal.style.backgroundColor = 'rgba(0,0,0,0.7)';
|
|
580
|
+
modal.style.display = 'flex';
|
|
581
|
+
modal.style.alignItems = 'center';
|
|
582
|
+
modal.style.justifyContent = 'center';
|
|
583
|
+
modal.style.zIndex = '1000';
|
|
584
|
+
|
|
585
|
+
const modalContent = modal.querySelector('.modal-content');
|
|
586
|
+
modalContent.style.backgroundColor = '#fff';
|
|
587
|
+
modalContent.style.padding = '20px';
|
|
588
|
+
modalContent.style.borderRadius = '5px';
|
|
589
|
+
modalContent.style.maxWidth = '90%';
|
|
590
|
+
modalContent.style.maxHeight = '90%';
|
|
591
|
+
modalContent.style.overflow = 'auto';
|
|
592
|
+
|
|
593
|
+
const closeButton = modal.querySelector('.close-button');
|
|
594
|
+
closeButton.style.float = 'right';
|
|
595
|
+
closeButton.style.fontSize = '1.5rem';
|
|
596
|
+
closeButton.style.fontWeight = 'bold';
|
|
597
|
+
closeButton.style.cursor = 'pointer';
|
|
598
|
+
|
|
599
|
+
const modalImg = modal.querySelector('img');
|
|
600
|
+
modalImg.style.maxWidth = '100%';
|
|
601
|
+
modalImg.style.maxHeight = '70vh';
|
|
602
|
+
modalImg.style.display = 'block';
|
|
603
|
+
modalImg.style.margin = '0 auto';
|
|
604
|
+
|
|
605
|
+
// Add modal to body
|
|
606
|
+
document.body.appendChild(modal);
|
|
607
|
+
|
|
608
|
+
// Close modal when clicking close button or outside the modal
|
|
609
|
+
closeButton.addEventListener('click', function() {
|
|
610
|
+
document.body.removeChild(modal);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
modal.addEventListener('click', function(e) {
|
|
614
|
+
if (e.target === modal) {
|
|
615
|
+
document.body.removeChild(modal);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
"""
|
|
622
|
+
|
|
623
|
+
# Create placeholder image
|
|
624
|
+
placeholder_svg = """<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600" viewBox="0 0 800 600">
|
|
625
|
+
<rect width="800" height="600" fill="#f0f0f0"/>
|
|
626
|
+
<text x="400" y="300" font-family="Arial" font-size="24" text-anchor="middle" fill="#999">Preview not available</text>
|
|
627
|
+
</svg>"""
|
|
628
|
+
|
|
629
|
+
# Write static files
|
|
630
|
+
os.makedirs(self.static_dir, exist_ok=True)
|
|
631
|
+
|
|
632
|
+
with open(os.path.join(self.static_dir, "styles.css"), "w") as f:
|
|
633
|
+
f.write(css)
|
|
634
|
+
|
|
635
|
+
with open(os.path.join(self.static_dir, "script.js"), "w") as f:
|
|
636
|
+
f.write(js)
|
|
637
|
+
|
|
638
|
+
with open(os.path.join(self.static_dir, "placeholder.svg"), "w") as f:
|
|
639
|
+
f.write(placeholder_svg)
|