@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,415 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Error handling and retry mechanisms for remote CUDA MVS processing.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for handling network errors, implementing
|
|
5
|
+
retry mechanisms, and ensuring robust communication with remote CUDA MVS servers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
import random
|
|
10
|
+
import logging
|
|
11
|
+
import functools
|
|
12
|
+
from typing import Callable, Any, Type, Union, List, Dict, Optional, Tuple
|
|
13
|
+
import requests
|
|
14
|
+
|
|
15
|
+
# Configure logging
|
|
16
|
+
logging.basicConfig(level=logging.INFO)
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Define common exception types for network operations
|
|
20
|
+
NETWORK_EXCEPTIONS = (
|
|
21
|
+
requests.exceptions.ConnectionError,
|
|
22
|
+
requests.exceptions.Timeout,
|
|
23
|
+
requests.exceptions.HTTPError,
|
|
24
|
+
requests.exceptions.RequestException,
|
|
25
|
+
ConnectionRefusedError,
|
|
26
|
+
TimeoutError,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def retry_with_backoff(
|
|
30
|
+
max_retries: int = 3,
|
|
31
|
+
base_delay: float = 1.0,
|
|
32
|
+
max_delay: float = 60.0,
|
|
33
|
+
exception_types: Tuple[Type[Exception], ...] = NETWORK_EXCEPTIONS,
|
|
34
|
+
jitter_factor: float = 0.1,
|
|
35
|
+
logger_instance: Optional[logging.Logger] = None
|
|
36
|
+
) -> Callable:
|
|
37
|
+
"""
|
|
38
|
+
Decorator for retrying a function with exponential backoff.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
max_retries: Maximum number of retries
|
|
42
|
+
base_delay: Base delay in seconds
|
|
43
|
+
max_delay: Maximum delay in seconds
|
|
44
|
+
exception_types: Tuple of exception types to catch and retry
|
|
45
|
+
jitter_factor: Factor for random jitter (0.0 to 1.0)
|
|
46
|
+
logger_instance: Logger instance to use (uses module logger if None)
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Decorator function
|
|
50
|
+
"""
|
|
51
|
+
log = logger_instance or logger
|
|
52
|
+
|
|
53
|
+
def decorator(func: Callable) -> Callable:
|
|
54
|
+
@functools.wraps(func)
|
|
55
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
56
|
+
retries = 0
|
|
57
|
+
last_exception = None
|
|
58
|
+
|
|
59
|
+
while True:
|
|
60
|
+
try:
|
|
61
|
+
return func(*args, **kwargs)
|
|
62
|
+
except exception_types as e:
|
|
63
|
+
retries += 1
|
|
64
|
+
last_exception = e
|
|
65
|
+
|
|
66
|
+
if retries > max_retries:
|
|
67
|
+
log.error(f"Max retries ({max_retries}) exceeded: {str(e)}")
|
|
68
|
+
raise
|
|
69
|
+
|
|
70
|
+
# Calculate delay with exponential backoff
|
|
71
|
+
delay = min(max_delay, base_delay * (2 ** (retries - 1)))
|
|
72
|
+
|
|
73
|
+
# Add jitter to avoid thundering herd problem
|
|
74
|
+
jitter = random.uniform(0, jitter_factor * delay)
|
|
75
|
+
sleep_time = delay + jitter
|
|
76
|
+
|
|
77
|
+
log.warning(f"Retry {retries}/{max_retries} after {sleep_time:.2f}s: {str(e)}")
|
|
78
|
+
time.sleep(sleep_time)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
# Don't retry other exceptions
|
|
81
|
+
log.error(f"Non-retryable exception: {str(e)}")
|
|
82
|
+
raise
|
|
83
|
+
|
|
84
|
+
return wrapper
|
|
85
|
+
|
|
86
|
+
return decorator
|
|
87
|
+
|
|
88
|
+
def timeout_handler(
|
|
89
|
+
timeout: float,
|
|
90
|
+
default_value: Any = None,
|
|
91
|
+
exception_types: Tuple[Type[Exception], ...] = (TimeoutError, requests.exceptions.Timeout),
|
|
92
|
+
logger_instance: Optional[logging.Logger] = None
|
|
93
|
+
) -> Callable:
|
|
94
|
+
"""
|
|
95
|
+
Decorator for handling timeouts in function calls.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
timeout: Timeout in seconds
|
|
99
|
+
default_value: Value to return if timeout occurs
|
|
100
|
+
exception_types: Tuple of exception types to catch as timeouts
|
|
101
|
+
logger_instance: Logger instance to use (uses module logger if None)
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Decorator function
|
|
105
|
+
"""
|
|
106
|
+
log = logger_instance or logger
|
|
107
|
+
|
|
108
|
+
def decorator(func: Callable) -> Callable:
|
|
109
|
+
@functools.wraps(func)
|
|
110
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
111
|
+
import signal
|
|
112
|
+
|
|
113
|
+
def timeout_handler(signum, frame):
|
|
114
|
+
raise TimeoutError(f"Function {func.__name__} timed out after {timeout} seconds")
|
|
115
|
+
|
|
116
|
+
# Set timeout using signal
|
|
117
|
+
original_handler = signal.signal(signal.SIGALRM, timeout_handler)
|
|
118
|
+
signal.alarm(int(timeout))
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
return func(*args, **kwargs)
|
|
122
|
+
except exception_types as e:
|
|
123
|
+
log.warning(f"Timeout in {func.__name__}: {str(e)}")
|
|
124
|
+
return default_value
|
|
125
|
+
finally:
|
|
126
|
+
# Reset signal handler and alarm
|
|
127
|
+
signal.signal(signal.SIGALRM, original_handler)
|
|
128
|
+
signal.alarm(0)
|
|
129
|
+
|
|
130
|
+
return wrapper
|
|
131
|
+
|
|
132
|
+
return decorator
|
|
133
|
+
|
|
134
|
+
class NetworkErrorTracker:
|
|
135
|
+
"""
|
|
136
|
+
Tracks network errors and provides information about error patterns.
|
|
137
|
+
|
|
138
|
+
This class helps identify persistent network issues and can be used
|
|
139
|
+
to make decisions about server availability.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(
|
|
143
|
+
self,
|
|
144
|
+
error_window: int = 10,
|
|
145
|
+
error_threshold: float = 0.5,
|
|
146
|
+
reset_after: int = 100
|
|
147
|
+
):
|
|
148
|
+
"""
|
|
149
|
+
Initialize the error tracker.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
error_window: Number of recent requests to consider
|
|
153
|
+
error_threshold: Error rate threshold to consider a server problematic
|
|
154
|
+
reset_after: Number of successful requests after which to reset error count
|
|
155
|
+
"""
|
|
156
|
+
self.error_window = error_window
|
|
157
|
+
self.error_threshold = error_threshold
|
|
158
|
+
self.reset_after = reset_after
|
|
159
|
+
|
|
160
|
+
self.requests = [] # List of (timestamp, success) tuples
|
|
161
|
+
self.consecutive_successes = 0
|
|
162
|
+
self.consecutive_failures = 0
|
|
163
|
+
|
|
164
|
+
def record_request(self, success: bool) -> None:
|
|
165
|
+
"""
|
|
166
|
+
Record the result of a request.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
success: Whether the request was successful
|
|
170
|
+
"""
|
|
171
|
+
timestamp = time.time()
|
|
172
|
+
self.requests.append((timestamp, success))
|
|
173
|
+
|
|
174
|
+
# Trim old requests outside the window
|
|
175
|
+
self._trim_old_requests()
|
|
176
|
+
|
|
177
|
+
# Update consecutive counters
|
|
178
|
+
if success:
|
|
179
|
+
self.consecutive_successes += 1
|
|
180
|
+
self.consecutive_failures = 0
|
|
181
|
+
|
|
182
|
+
# Reset error count after enough consecutive successes
|
|
183
|
+
if self.consecutive_successes >= self.reset_after:
|
|
184
|
+
self.requests = [(timestamp, True)]
|
|
185
|
+
self.consecutive_successes = 1
|
|
186
|
+
else:
|
|
187
|
+
self.consecutive_failures += 1
|
|
188
|
+
self.consecutive_successes = 0
|
|
189
|
+
|
|
190
|
+
def _trim_old_requests(self) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Remove requests that are outside the current window.
|
|
193
|
+
"""
|
|
194
|
+
if len(self.requests) > self.error_window:
|
|
195
|
+
self.requests = self.requests[-self.error_window:]
|
|
196
|
+
|
|
197
|
+
def get_error_rate(self) -> float:
|
|
198
|
+
"""
|
|
199
|
+
Get the current error rate.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Error rate as a float between 0.0 and 1.0
|
|
203
|
+
"""
|
|
204
|
+
if not self.requests:
|
|
205
|
+
return 0.0
|
|
206
|
+
|
|
207
|
+
failures = sum(1 for _, success in self.requests if not success)
|
|
208
|
+
return failures / len(self.requests)
|
|
209
|
+
|
|
210
|
+
def is_server_problematic(self) -> bool:
|
|
211
|
+
"""
|
|
212
|
+
Check if the server is experiencing persistent issues.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
True if the server is problematic, False otherwise
|
|
216
|
+
"""
|
|
217
|
+
return self.get_error_rate() >= self.error_threshold
|
|
218
|
+
|
|
219
|
+
def get_status(self) -> Dict[str, Any]:
|
|
220
|
+
"""
|
|
221
|
+
Get the current status of the error tracker.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Dictionary with status information
|
|
225
|
+
"""
|
|
226
|
+
return {
|
|
227
|
+
"error_rate": self.get_error_rate(),
|
|
228
|
+
"is_problematic": self.is_server_problematic(),
|
|
229
|
+
"consecutive_successes": self.consecutive_successes,
|
|
230
|
+
"consecutive_failures": self.consecutive_failures,
|
|
231
|
+
"total_requests": len(self.requests),
|
|
232
|
+
"recent_failures": sum(1 for _, success in self.requests if not success)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
class CircuitBreaker:
|
|
236
|
+
"""
|
|
237
|
+
Circuit breaker pattern implementation for network requests.
|
|
238
|
+
|
|
239
|
+
This class helps prevent cascading failures by stopping requests
|
|
240
|
+
to a problematic server until it recovers.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
# Circuit states
|
|
244
|
+
CLOSED = "closed" # Normal operation
|
|
245
|
+
OPEN = "open" # No requests allowed
|
|
246
|
+
HALF_OPEN = "half_open" # Testing if service is back
|
|
247
|
+
|
|
248
|
+
def __init__(
|
|
249
|
+
self,
|
|
250
|
+
failure_threshold: int = 5,
|
|
251
|
+
recovery_timeout: float = 30.0,
|
|
252
|
+
reset_timeout: float = 60.0,
|
|
253
|
+
logger_instance: Optional[logging.Logger] = None
|
|
254
|
+
):
|
|
255
|
+
"""
|
|
256
|
+
Initialize the circuit breaker.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
failure_threshold: Number of consecutive failures before opening
|
|
260
|
+
recovery_timeout: Time in seconds before testing recovery
|
|
261
|
+
reset_timeout: Time in seconds before fully resetting
|
|
262
|
+
logger_instance: Logger instance to use (uses module logger if None)
|
|
263
|
+
"""
|
|
264
|
+
self.failure_threshold = failure_threshold
|
|
265
|
+
self.recovery_timeout = recovery_timeout
|
|
266
|
+
self.reset_timeout = reset_timeout
|
|
267
|
+
self.log = logger_instance or logger
|
|
268
|
+
|
|
269
|
+
self.state = self.CLOSED
|
|
270
|
+
self.failure_count = 0
|
|
271
|
+
self.last_failure_time = 0
|
|
272
|
+
self.last_success_time = 0
|
|
273
|
+
|
|
274
|
+
def record_success(self) -> None:
|
|
275
|
+
"""
|
|
276
|
+
Record a successful request.
|
|
277
|
+
"""
|
|
278
|
+
self.last_success_time = time.time()
|
|
279
|
+
|
|
280
|
+
if self.state == self.HALF_OPEN:
|
|
281
|
+
self.log.info("Circuit breaker reset to closed state after successful test request")
|
|
282
|
+
self.state = self.CLOSED
|
|
283
|
+
self.failure_count = 0
|
|
284
|
+
elif self.state == self.CLOSED:
|
|
285
|
+
# Reset failure count after a successful request
|
|
286
|
+
self.failure_count = 0
|
|
287
|
+
|
|
288
|
+
def record_failure(self) -> None:
|
|
289
|
+
"""
|
|
290
|
+
Record a failed request.
|
|
291
|
+
"""
|
|
292
|
+
self.last_failure_time = time.time()
|
|
293
|
+
|
|
294
|
+
if self.state == self.CLOSED:
|
|
295
|
+
self.failure_count += 1
|
|
296
|
+
|
|
297
|
+
if self.failure_count >= self.failure_threshold:
|
|
298
|
+
self.log.warning(f"Circuit breaker opened after {self.failure_count} consecutive failures")
|
|
299
|
+
self.state = self.OPEN
|
|
300
|
+
elif self.state == self.HALF_OPEN:
|
|
301
|
+
self.log.warning("Circuit breaker opened again after failed test request")
|
|
302
|
+
self.state = self.OPEN
|
|
303
|
+
|
|
304
|
+
def allow_request(self) -> bool:
|
|
305
|
+
"""
|
|
306
|
+
Check if a request should be allowed.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
True if the request should be allowed, False otherwise
|
|
310
|
+
"""
|
|
311
|
+
if self.state == self.CLOSED:
|
|
312
|
+
return True
|
|
313
|
+
|
|
314
|
+
if self.state == self.OPEN:
|
|
315
|
+
# Check if recovery timeout has elapsed
|
|
316
|
+
if time.time() - self.last_failure_time > self.recovery_timeout:
|
|
317
|
+
self.log.info("Circuit breaker entering half-open state to test service")
|
|
318
|
+
self.state = self.HALF_OPEN
|
|
319
|
+
return True
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
# In HALF_OPEN state, allow only one request
|
|
323
|
+
return True
|
|
324
|
+
|
|
325
|
+
def reset(self) -> None:
|
|
326
|
+
"""
|
|
327
|
+
Reset the circuit breaker to closed state.
|
|
328
|
+
"""
|
|
329
|
+
self.state = self.CLOSED
|
|
330
|
+
self.failure_count = 0
|
|
331
|
+
self.log.info("Circuit breaker manually reset to closed state")
|
|
332
|
+
|
|
333
|
+
def get_status(self) -> Dict[str, Any]:
|
|
334
|
+
"""
|
|
335
|
+
Get the current status of the circuit breaker.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Dictionary with status information
|
|
339
|
+
"""
|
|
340
|
+
now = time.time()
|
|
341
|
+
return {
|
|
342
|
+
"state": self.state,
|
|
343
|
+
"failure_count": self.failure_count,
|
|
344
|
+
"time_since_last_failure": now - self.last_failure_time if self.last_failure_time > 0 else None,
|
|
345
|
+
"time_since_last_success": now - self.last_success_time if self.last_success_time > 0 else None,
|
|
346
|
+
"recovery_timeout": self.recovery_timeout,
|
|
347
|
+
"reset_timeout": self.reset_timeout
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
def safe_request(
|
|
351
|
+
url: str,
|
|
352
|
+
method: str = "GET",
|
|
353
|
+
circuit_breaker: Optional[CircuitBreaker] = None,
|
|
354
|
+
error_tracker: Optional[NetworkErrorTracker] = None,
|
|
355
|
+
retry_count: int = 3,
|
|
356
|
+
timeout: float = 30.0,
|
|
357
|
+
**kwargs
|
|
358
|
+
) -> Optional[requests.Response]:
|
|
359
|
+
"""
|
|
360
|
+
Make a safe HTTP request with circuit breaker and retry logic.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
url: URL to request
|
|
364
|
+
method: HTTP method (GET, POST, etc.)
|
|
365
|
+
circuit_breaker: Circuit breaker instance
|
|
366
|
+
error_tracker: Error tracker instance
|
|
367
|
+
retry_count: Number of retries
|
|
368
|
+
timeout: Request timeout in seconds
|
|
369
|
+
**kwargs: Additional arguments for requests
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Response object or None if request failed
|
|
373
|
+
"""
|
|
374
|
+
# Check circuit breaker
|
|
375
|
+
if circuit_breaker and not circuit_breaker.allow_request():
|
|
376
|
+
logger.warning(f"Circuit breaker prevented request to {url}")
|
|
377
|
+
return None
|
|
378
|
+
|
|
379
|
+
# Set default timeout
|
|
380
|
+
kwargs.setdefault("timeout", timeout)
|
|
381
|
+
|
|
382
|
+
# Make request with retry
|
|
383
|
+
response = None
|
|
384
|
+
success = False
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
for attempt in range(retry_count + 1):
|
|
388
|
+
try:
|
|
389
|
+
response = requests.request(method, url, **kwargs)
|
|
390
|
+
response.raise_for_status()
|
|
391
|
+
success = True
|
|
392
|
+
break
|
|
393
|
+
except NETWORK_EXCEPTIONS as e:
|
|
394
|
+
if attempt < retry_count:
|
|
395
|
+
delay = 2 ** attempt + random.uniform(0, 1)
|
|
396
|
+
logger.warning(f"Request to {url} failed (attempt {attempt+1}/{retry_count+1}): {str(e)}. Retrying in {delay:.2f}s")
|
|
397
|
+
time.sleep(delay)
|
|
398
|
+
else:
|
|
399
|
+
logger.error(f"Request to {url} failed after {retry_count+1} attempts: {str(e)}")
|
|
400
|
+
raise
|
|
401
|
+
except Exception as e:
|
|
402
|
+
logger.error(f"Error making request to {url}: {str(e)}")
|
|
403
|
+
success = False
|
|
404
|
+
|
|
405
|
+
# Update circuit breaker and error tracker
|
|
406
|
+
if circuit_breaker:
|
|
407
|
+
if success:
|
|
408
|
+
circuit_breaker.record_success()
|
|
409
|
+
else:
|
|
410
|
+
circuit_breaker.record_failure()
|
|
411
|
+
|
|
412
|
+
if error_tracker:
|
|
413
|
+
error_tracker.record_request(success)
|
|
414
|
+
|
|
415
|
+
return response if success else None
|
|
File without changes
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from src.models.code_generator import OpenSCADCodeGenerator
|
|
6
|
+
from src.utils.cad_exporter import CADExporter
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
class PrimitiveTester:
|
|
11
|
+
"""Tests OpenSCAD primitives with different export formats."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, code_generator: OpenSCADCodeGenerator, cad_exporter: CADExporter,
|
|
14
|
+
output_dir: str = "test_output"):
|
|
15
|
+
"""
|
|
16
|
+
Initialize the primitive tester.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
code_generator: CodeGenerator instance for generating OpenSCAD code
|
|
20
|
+
cad_exporter: CADExporter instance for exporting models
|
|
21
|
+
output_dir: Directory to store test output
|
|
22
|
+
"""
|
|
23
|
+
self.code_generator = code_generator
|
|
24
|
+
self.cad_exporter = cad_exporter
|
|
25
|
+
self.output_dir = output_dir
|
|
26
|
+
|
|
27
|
+
# Create output directory
|
|
28
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
29
|
+
|
|
30
|
+
# Primitive types to test
|
|
31
|
+
self.primitives = [
|
|
32
|
+
"cube", "sphere", "cylinder", "cone", "torus",
|
|
33
|
+
"rounded_box", "hexagonal_prism", "text"
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# Export formats to test (no STL per requirements)
|
|
37
|
+
self.formats = ["3mf", "amf", "csg", "scad"]
|
|
38
|
+
|
|
39
|
+
def test_all_primitives(self) -> Dict[str, Dict[str, Any]]:
|
|
40
|
+
"""
|
|
41
|
+
Test all primitives with all formats.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Dictionary of test results for each primitive
|
|
45
|
+
"""
|
|
46
|
+
results = {}
|
|
47
|
+
|
|
48
|
+
for primitive in self.primitives:
|
|
49
|
+
results[primitive] = self.test_primitive(primitive)
|
|
50
|
+
|
|
51
|
+
return results
|
|
52
|
+
|
|
53
|
+
def test_primitive(self, primitive_type: str) -> Dict[str, Any]:
|
|
54
|
+
"""
|
|
55
|
+
Test a single primitive with all formats.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
primitive_type: Type of primitive to test
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Dictionary of test results for the primitive
|
|
62
|
+
"""
|
|
63
|
+
results = {
|
|
64
|
+
"primitive": primitive_type,
|
|
65
|
+
"formats": {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Generate default parameters for the primitive
|
|
69
|
+
params = self._get_default_parameters(primitive_type)
|
|
70
|
+
|
|
71
|
+
# Generate the SCAD code
|
|
72
|
+
scad_file = self.code_generator.generate_code(primitive_type, params)
|
|
73
|
+
|
|
74
|
+
# Test export to each format
|
|
75
|
+
for format_type in self.formats:
|
|
76
|
+
success, output_file, error = self.cad_exporter.export_model(
|
|
77
|
+
scad_file,
|
|
78
|
+
format_type,
|
|
79
|
+
params,
|
|
80
|
+
metadata={"primitive_type": primitive_type}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
results["formats"][format_type] = {
|
|
84
|
+
"success": success,
|
|
85
|
+
"output_file": output_file,
|
|
86
|
+
"error": error
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return results
|
|
90
|
+
|
|
91
|
+
def _get_default_parameters(self, primitive_type: str) -> Dict[str, Any]:
|
|
92
|
+
"""
|
|
93
|
+
Get default parameters for a primitive type.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
primitive_type: Type of primitive
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Dictionary of default parameters
|
|
100
|
+
"""
|
|
101
|
+
params = {}
|
|
102
|
+
|
|
103
|
+
if primitive_type == "cube":
|
|
104
|
+
params = {"width": 20, "depth": 20, "height": 20, "center": True}
|
|
105
|
+
elif primitive_type == "sphere":
|
|
106
|
+
params = {"radius": 10, "segments": 32}
|
|
107
|
+
elif primitive_type == "cylinder":
|
|
108
|
+
params = {"radius": 10, "height": 20, "center": True, "segments": 32}
|
|
109
|
+
elif primitive_type == "cone":
|
|
110
|
+
params = {"bottom_radius": 10, "top_radius": 0, "height": 20, "center": True}
|
|
111
|
+
elif primitive_type == "torus":
|
|
112
|
+
params = {"outer_radius": 20, "inner_radius": 5, "segments": 32}
|
|
113
|
+
elif primitive_type == "rounded_box":
|
|
114
|
+
params = {"width": 30, "depth": 20, "height": 15, "radius": 3}
|
|
115
|
+
elif primitive_type == "hexagonal_prism":
|
|
116
|
+
params = {"radius": 10, "height": 20}
|
|
117
|
+
elif primitive_type == "text":
|
|
118
|
+
params = {"text": "OpenSCAD", "size": 10, "height": 3}
|
|
119
|
+
|
|
120
|
+
return params
|
|
121
|
+
|
|
122
|
+
def test_with_parameter_variations(self, primitive_type: str) -> Dict[str, Any]:
|
|
123
|
+
"""
|
|
124
|
+
Test a primitive with variations of parameters.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
primitive_type: Type of primitive to test
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Dictionary of test results for different parameter variations
|
|
131
|
+
"""
|
|
132
|
+
results = {
|
|
133
|
+
"primitive": primitive_type,
|
|
134
|
+
"variations": {}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Define parameter variations for the primitive
|
|
138
|
+
variations = self._get_parameter_variations(primitive_type)
|
|
139
|
+
|
|
140
|
+
# Test each variation
|
|
141
|
+
for variation_name, params in variations.items():
|
|
142
|
+
# Generate the SCAD code
|
|
143
|
+
scad_file = self.code_generator.generate_code(primitive_type, params)
|
|
144
|
+
|
|
145
|
+
# Test export to each format
|
|
146
|
+
format_results = {}
|
|
147
|
+
for format_type in self.formats:
|
|
148
|
+
success, output_file, error = self.cad_exporter.export_model(
|
|
149
|
+
scad_file,
|
|
150
|
+
format_type,
|
|
151
|
+
params,
|
|
152
|
+
metadata={"primitive_type": primitive_type, "variation": variation_name}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
format_results[format_type] = {
|
|
156
|
+
"success": success,
|
|
157
|
+
"output_file": output_file,
|
|
158
|
+
"error": error
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
results["variations"][variation_name] = {
|
|
162
|
+
"parameters": params,
|
|
163
|
+
"formats": format_results
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return results
|
|
167
|
+
|
|
168
|
+
def _get_parameter_variations(self, primitive_type: str) -> Dict[str, Dict[str, Any]]:
|
|
169
|
+
"""
|
|
170
|
+
Get parameter variations for a primitive type.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
primitive_type: Type of primitive
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Dictionary of parameter variations
|
|
177
|
+
"""
|
|
178
|
+
variations = {}
|
|
179
|
+
|
|
180
|
+
if primitive_type == "cube":
|
|
181
|
+
variations = {
|
|
182
|
+
"small": {"width": 5, "depth": 5, "height": 5, "center": True},
|
|
183
|
+
"large": {"width": 50, "depth": 50, "height": 50, "center": True},
|
|
184
|
+
"flat": {"width": 50, "depth": 50, "height": 2, "center": True},
|
|
185
|
+
"tall": {"width": 10, "depth": 10, "height": 100, "center": True}
|
|
186
|
+
}
|
|
187
|
+
elif primitive_type == "sphere":
|
|
188
|
+
variations = {
|
|
189
|
+
"small": {"radius": 2, "segments": 16},
|
|
190
|
+
"large": {"radius": 30, "segments": 64},
|
|
191
|
+
"low_res": {"radius": 10, "segments": 8},
|
|
192
|
+
"high_res": {"radius": 10, "segments": 128}
|
|
193
|
+
}
|
|
194
|
+
elif primitive_type == "cylinder":
|
|
195
|
+
variations = {
|
|
196
|
+
"small": {"radius": 2, "height": 5, "center": True, "segments": 16},
|
|
197
|
+
"large": {"radius": 30, "height": 50, "center": True, "segments": 64},
|
|
198
|
+
"thin": {"radius": 1, "height": 50, "center": True, "segments": 32},
|
|
199
|
+
"disc": {"radius": 30, "height": 2, "center": True, "segments": 32}
|
|
200
|
+
}
|
|
201
|
+
# Add variations for other primitives as needed
|
|
202
|
+
|
|
203
|
+
return variations
|