solidworks-mcp-python 1.5.0__py3-none-any.whl
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.
- solidworks_mcp/__init__.py +57 -0
- solidworks_mcp/adapters/__init__.py +40 -0
- solidworks_mcp/adapters/base.py +1882 -0
- solidworks_mcp/adapters/circuit_breaker.py +2052 -0
- solidworks_mcp/adapters/com_executor.py +252 -0
- solidworks_mcp/adapters/complexity_analyzer.py +272 -0
- solidworks_mcp/adapters/connection_pool.py +1884 -0
- solidworks_mcp/adapters/factory.py +366 -0
- solidworks_mcp/adapters/intelligent_router.py +218 -0
- solidworks_mcp/adapters/mock_adapter.py +4016 -0
- solidworks_mcp/adapters/pywin32_adapter.py +2842 -0
- solidworks_mcp/adapters/solidworks/__init__.py +17 -0
- solidworks_mcp/adapters/solidworks/bitmap.py +180 -0
- solidworks_mcp/adapters/solidworks/com_utils.py +56 -0
- solidworks_mcp/adapters/solidworks/dimension_layout.py +753 -0
- solidworks_mcp/adapters/solidworks/documentation.py +1476 -0
- solidworks_mcp/adapters/solidworks/drawing_standard.py +1993 -0
- solidworks_mcp/adapters/solidworks/face_selection.py +392 -0
- solidworks_mcp/adapters/solidworks/features.py +3362 -0
- solidworks_mcp/adapters/solidworks/io.py +3649 -0
- solidworks_mcp/adapters/solidworks/rebuild_diagnostics.py +269 -0
- solidworks_mcp/adapters/solidworks/rebuild_inspector.py +146 -0
- solidworks_mcp/adapters/solidworks/selection.py +308 -0
- solidworks_mcp/adapters/solidworks/sketch.py +2545 -0
- solidworks_mcp/adapters/sw_feature_errors.py +66 -0
- solidworks_mcp/adapters/sw_type_info.py +317 -0
- solidworks_mcp/adapters/vba_adapter.py +318 -0
- solidworks_mcp/adapters/vba_macro_executor.py +215 -0
- solidworks_mcp/agents/__init__.py +68 -0
- solidworks_mcp/agents/design_knowledge/assembly_patterns.md +127 -0
- solidworks_mcp/agents/design_knowledge/hinges.md +118 -0
- solidworks_mcp/agents/design_knowledge/snap_fits.md +93 -0
- solidworks_mcp/agents/design_knowledge/solidworks_basics.md +3 -0
- solidworks_mcp/agents/design_knowledge/tolerancing.md +128 -0
- solidworks_mcp/agents/harness.py +216 -0
- solidworks_mcp/agents/history_db.py +1508 -0
- solidworks_mcp/agents/retrieval_index.py +164 -0
- solidworks_mcp/agents/schemas.py +165 -0
- solidworks_mcp/agents/smoke_test.py +285 -0
- solidworks_mcp/agents/soc_exporter.py +650 -0
- solidworks_mcp/agents/soc_pickup.py +332 -0
- solidworks_mcp/agents/soc_rewind.py +221 -0
- solidworks_mcp/agents/vector_rag.py +665 -0
- solidworks_mcp/cache/__init__.py +5 -0
- solidworks_mcp/cache/response_cache.py +164 -0
- solidworks_mcp/config.py +729 -0
- solidworks_mcp/exceptions.py +37 -0
- solidworks_mcp/security/__init__.py +68 -0
- solidworks_mcp/security/auth.py +128 -0
- solidworks_mcp/security/cors.py +27 -0
- solidworks_mcp/security/rate_limiting.py +90 -0
- solidworks_mcp/security/runtime.py +155 -0
- solidworks_mcp/server.py +777 -0
- solidworks_mcp/server_cli_fixed.py +120 -0
- solidworks_mcp/setup/__init__.py +1 -0
- solidworks_mcp/setup/cli.py +399 -0
- solidworks_mcp/setup/clients.py +157 -0
- solidworks_mcp/setup/config_file.py +230 -0
- solidworks_mcp/setup/handshake.py +202 -0
- solidworks_mcp/setup/launcher.py +102 -0
- solidworks_mcp/setup/legacy.py +100 -0
- solidworks_mcp/setup/registration.py +193 -0
- solidworks_mcp/setup/solidworks.py +49 -0
- solidworks_mcp/tools/__init__.py +64 -0
- solidworks_mcp/tools/analysis.py +723 -0
- solidworks_mcp/tools/automation.py +578 -0
- solidworks_mcp/tools/docs_discovery.py +1476 -0
- solidworks_mcp/tools/drawing.py +1015 -0
- solidworks_mcp/tools/drawing_analysis.py +456 -0
- solidworks_mcp/tools/drawing_automation.py +627 -0
- solidworks_mcp/tools/engineering_data.py +466 -0
- solidworks_mcp/tools/error_classifier.py +232 -0
- solidworks_mcp/tools/export.py +833 -0
- solidworks_mcp/tools/file_facts.py +40 -0
- solidworks_mcp/tools/file_management.py +1473 -0
- solidworks_mcp/tools/input_compat.py +17 -0
- solidworks_mcp/tools/macro_recording.py +464 -0
- solidworks_mcp/tools/modeling.py +2360 -0
- solidworks_mcp/tools/sketching.py +1422 -0
- solidworks_mcp/tools/template_management.py +367 -0
- solidworks_mcp/tools/tool_boundary.py +280 -0
- solidworks_mcp/tools/vba_generation.py +1145 -0
- solidworks_mcp/ui/__init__.py +9 -0
- solidworks_mcp/ui/local_llm.py +725 -0
- solidworks_mcp/ui/routers/__init__.py +10 -0
- solidworks_mcp/ui/routers/checkpoint.py +24 -0
- solidworks_mcp/ui/routers/docs.py +47 -0
- solidworks_mcp/ui/routers/llm.py +83 -0
- solidworks_mcp/ui/routers/local_model.py +69 -0
- solidworks_mcp/ui/routers/model.py +75 -0
- solidworks_mcp/ui/routers/preview.py +38 -0
- solidworks_mcp/ui/routers/session.py +161 -0
- solidworks_mcp/ui/routers/viewer.py +154 -0
- solidworks_mcp/ui/schemas.py +502 -0
- solidworks_mcp/ui/server.py +298 -0
- solidworks_mcp/ui/service.py +481 -0
- solidworks_mcp/ui/services/__init__.py +136 -0
- solidworks_mcp/ui/services/_utils.py +958 -0
- solidworks_mcp/ui/services/checkpoint_service.py +1110 -0
- solidworks_mcp/ui/services/docs_service.py +282 -0
- solidworks_mcp/ui/services/llm_service.py +956 -0
- solidworks_mcp/ui/services/model_service.py +459 -0
- solidworks_mcp/ui/services/parameter_repair_service.py +314 -0
- solidworks_mcp/ui/services/preview_service.py +489 -0
- solidworks_mcp/ui/services/session_service.py +1389 -0
- solidworks_mcp/utils/__init__.py +9 -0
- solidworks_mcp/utils/feature_tree_classifier.py +342 -0
- solidworks_mcp/utils/logging.py +76 -0
- solidworks_mcp/utils/validation.py +77 -0
- solidworks_mcp/version.py +8 -0
- solidworks_mcp_python-1.5.0.dist-info/METADATA +410 -0
- solidworks_mcp_python-1.5.0.dist-info/RECORD +115 -0
- solidworks_mcp_python-1.5.0.dist-info/WHEEL +4 -0
- solidworks_mcp_python-1.5.0.dist-info/entry_points.txt +3 -0
- solidworks_mcp_python-1.5.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
"""Preview and feature-highlighting service for the Prefab CAD assistant dashboard.
|
|
2
|
+
|
|
3
|
+
Exports PNG screenshots and 3D geometry (GLB/STL) from the active SolidWorks model and
|
|
4
|
+
persists preview state in the session.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from loguru import logger
|
|
15
|
+
|
|
16
|
+
from ...adapters import create_adapter
|
|
17
|
+
from ...agents.history_db import (
|
|
18
|
+
get_design_session,
|
|
19
|
+
insert_model_state_snapshot,
|
|
20
|
+
insert_tool_call_record,
|
|
21
|
+
list_model_state_snapshots,
|
|
22
|
+
)
|
|
23
|
+
from ...config import load_config
|
|
24
|
+
from ._utils import (
|
|
25
|
+
DEFAULT_API_ORIGIN,
|
|
26
|
+
DEFAULT_PREVIEW_ORIENTATION,
|
|
27
|
+
ensure_preview_dir,
|
|
28
|
+
merge_metadata,
|
|
29
|
+
parse_json_blob,
|
|
30
|
+
sanitize_preview_viewer_url,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Internal helpers
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _public_preview_url(
|
|
39
|
+
preview_path: Path,
|
|
40
|
+
*,
|
|
41
|
+
api_origin: str = DEFAULT_API_ORIGIN,
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Build a cache-busting public URL for a preview image.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
preview_path: Filesystem path to the preview image.
|
|
47
|
+
api_origin: Base URL of the running FastAPI server.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Public URL string with a ``ts`` query parameter.
|
|
51
|
+
"""
|
|
52
|
+
timestamp = (
|
|
53
|
+
int(preview_path.stat().st_mtime) if preview_path.exists() else int(time.time())
|
|
54
|
+
)
|
|
55
|
+
return f"{api_origin}/previews/{preview_path.name}?ts={timestamp}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def _reopen_target_model_for_preview(
|
|
59
|
+
adapter: Any, model_path: str, *, context: str
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Reopen the persisted target model before preview export."""
|
|
62
|
+
candidate_path = Path(str(model_path))
|
|
63
|
+
if not candidate_path.exists():
|
|
64
|
+
raise RuntimeError(
|
|
65
|
+
f"Target model path for {context} does not exist: {candidate_path}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
logger.info(
|
|
69
|
+
"[ui.refresh_preview] reopening target model for {} {}",
|
|
70
|
+
context,
|
|
71
|
+
str(candidate_path.resolve()),
|
|
72
|
+
)
|
|
73
|
+
reopen_result = await adapter.open_model(str(candidate_path.resolve()))
|
|
74
|
+
if not getattr(reopen_result, "is_success", False):
|
|
75
|
+
raise RuntimeError(
|
|
76
|
+
reopen_result.error
|
|
77
|
+
or f"Failed to reopen target model for {context}: {candidate_path.resolve()}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Public API
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def refresh_preview(
|
|
87
|
+
session_id: str,
|
|
88
|
+
*,
|
|
89
|
+
orientation: str = DEFAULT_PREVIEW_ORIENTATION,
|
|
90
|
+
db_path: Path | None = None,
|
|
91
|
+
preview_dir: Path | None = None,
|
|
92
|
+
api_origin: str = DEFAULT_API_ORIGIN,
|
|
93
|
+
adapter_override: Any | None = None,
|
|
94
|
+
active_model_path_override: str | None = None,
|
|
95
|
+
reopen_active_model: bool = True,
|
|
96
|
+
) -> dict[str, Any]:
|
|
97
|
+
"""Export the current SolidWorks viewport to a PNG preview and GLB/STL for the 3D viewer.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
session_id: Dashboard session identifier.
|
|
101
|
+
orientation: View orientation for the PNG export
|
|
102
|
+
(``"front"``, ``"top"``, ``"right"``, ``"isometric"``, ``"current"``).
|
|
103
|
+
db_path: Optional SQLite path override.
|
|
104
|
+
preview_dir: Override for the preview output directory.
|
|
105
|
+
api_origin: Base URL of the running FastAPI server.
|
|
106
|
+
adapter_override: Pre-connected adapter (avoids creating a second connection when
|
|
107
|
+
called from ``connect_target_model``).
|
|
108
|
+
active_model_path_override: Override for the model path to reopen.
|
|
109
|
+
reopen_active_model: When ``True``, reopen the persisted active model path before
|
|
110
|
+
exporting (default).
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Full dashboard state payload.
|
|
114
|
+
"""
|
|
115
|
+
from .session_service import ( # noqa: PLC0415
|
|
116
|
+
build_dashboard_state,
|
|
117
|
+
ensure_dashboard_session,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
ensure_dashboard_session(session_id, db_path=db_path)
|
|
121
|
+
logger.info(
|
|
122
|
+
"[ui.refresh_preview] session_id={} orientation={}",
|
|
123
|
+
session_id,
|
|
124
|
+
orientation,
|
|
125
|
+
)
|
|
126
|
+
session_row = get_design_session(session_id, db_path=db_path) or {}
|
|
127
|
+
metadata = parse_json_blob(session_row.get("metadata_json"))
|
|
128
|
+
preview_viewer_url = sanitize_preview_viewer_url(
|
|
129
|
+
metadata.get("preview_viewer_url"),
|
|
130
|
+
session_id=session_id,
|
|
131
|
+
api_origin=api_origin,
|
|
132
|
+
)
|
|
133
|
+
resolved_preview_dir = ensure_preview_dir(preview_dir)
|
|
134
|
+
preview_path = resolved_preview_dir / f"{session_id}.png"
|
|
135
|
+
active_model_path = active_model_path_override or metadata.get("active_model_path")
|
|
136
|
+
adapter = adapter_override
|
|
137
|
+
owns_adapter = adapter_override is None
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
if not active_model_path:
|
|
141
|
+
raise RuntimeError(
|
|
142
|
+
"No attached model path found for preview refresh. Attach a target model first."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if adapter is None:
|
|
146
|
+
config = load_config()
|
|
147
|
+
adapter = await create_adapter(config)
|
|
148
|
+
if owns_adapter:
|
|
149
|
+
await adapter.connect()
|
|
150
|
+
if reopen_active_model and hasattr(adapter, "open_model"):
|
|
151
|
+
await _reopen_target_model_for_preview(
|
|
152
|
+
adapter, str(active_model_path), context="preview"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# --- Step 1: Export 3D geometry for the Three.js viewer (GLB preferred) ---
|
|
156
|
+
glb_path = resolved_preview_dir / f"{session_id}.glb"
|
|
157
|
+
stl_path = resolved_preview_dir / f"{session_id}.stl"
|
|
158
|
+
viewer_ts = int(time.time())
|
|
159
|
+
viewer_format = "none"
|
|
160
|
+
try:
|
|
161
|
+
glb_result = await adapter.export_file(str(glb_path.resolve()), "glb")
|
|
162
|
+
if (
|
|
163
|
+
glb_result.is_success
|
|
164
|
+
and glb_path.exists()
|
|
165
|
+
and glb_path.stat().st_size > 0
|
|
166
|
+
):
|
|
167
|
+
viewer_format = "glb"
|
|
168
|
+
viewer_ts = int(glb_path.stat().st_mtime)
|
|
169
|
+
logger.info(
|
|
170
|
+
"[ui.refresh_preview] GLB export succeeded path={}",
|
|
171
|
+
str(glb_path.resolve()),
|
|
172
|
+
)
|
|
173
|
+
except Exception as _glb_exc:
|
|
174
|
+
logger.warning("[ui.refresh_preview] GLB export failed: {}", str(_glb_exc))
|
|
175
|
+
|
|
176
|
+
if viewer_format == "none":
|
|
177
|
+
try:
|
|
178
|
+
stl_result = await adapter.export_file(str(stl_path.resolve()), "stl")
|
|
179
|
+
if (
|
|
180
|
+
stl_result.is_success
|
|
181
|
+
and stl_path.exists()
|
|
182
|
+
and stl_path.stat().st_size > 0
|
|
183
|
+
):
|
|
184
|
+
viewer_format = "stl"
|
|
185
|
+
viewer_ts = int(stl_path.stat().st_mtime)
|
|
186
|
+
logger.info(
|
|
187
|
+
"[ui.refresh_preview] STL export succeeded path={}",
|
|
188
|
+
str(stl_path.resolve()),
|
|
189
|
+
)
|
|
190
|
+
except Exception:
|
|
191
|
+
logger.debug("[ui.refresh_preview] STL export skipped (adapter error)")
|
|
192
|
+
|
|
193
|
+
preview_viewer_url = (
|
|
194
|
+
f"{api_origin}/api/ui/viewer/{session_id}?t={viewer_ts}&fmt={viewer_format}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# --- Step 2: Export PNG screenshot (best-effort) ---
|
|
198
|
+
png_payload = {
|
|
199
|
+
"file_path": str(preview_path.resolve()),
|
|
200
|
+
"format_type": "png",
|
|
201
|
+
"width": 1280,
|
|
202
|
+
"height": 720,
|
|
203
|
+
"view_orientation": orientation,
|
|
204
|
+
}
|
|
205
|
+
png_ok = False
|
|
206
|
+
png_error: str = ""
|
|
207
|
+
snapshot_id: int | None = None
|
|
208
|
+
try:
|
|
209
|
+
result = await adapter.export_image(png_payload)
|
|
210
|
+
if result.is_success and preview_path.exists():
|
|
211
|
+
png_ok = True
|
|
212
|
+
logger.info(
|
|
213
|
+
"[ui.refresh_preview] PNG export succeeded file_path={}",
|
|
214
|
+
str(preview_path.resolve()),
|
|
215
|
+
)
|
|
216
|
+
snapshot_id = insert_model_state_snapshot(
|
|
217
|
+
session_id=session_id,
|
|
218
|
+
screenshot_path=str(preview_path.resolve()),
|
|
219
|
+
state_fingerprint=f"preview-{preview_path.stat().st_mtime_ns}",
|
|
220
|
+
db_path=db_path,
|
|
221
|
+
)
|
|
222
|
+
insert_tool_call_record(
|
|
223
|
+
session_id=session_id,
|
|
224
|
+
tool_name="export_image",
|
|
225
|
+
input_json=json.dumps(png_payload, ensure_ascii=True),
|
|
226
|
+
output_json=json.dumps(result.data or {}, ensure_ascii=True),
|
|
227
|
+
success=True,
|
|
228
|
+
db_path=db_path,
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
png_error = result.error or "export_image returned failure"
|
|
232
|
+
logger.warning("[ui.refresh_preview] PNG export failed: {}", png_error)
|
|
233
|
+
except Exception as png_exc:
|
|
234
|
+
png_error = str(png_exc)
|
|
235
|
+
logger.warning("[ui.refresh_preview] PNG export exception: {}", png_exc)
|
|
236
|
+
|
|
237
|
+
if owns_adapter:
|
|
238
|
+
await adapter.disconnect()
|
|
239
|
+
|
|
240
|
+
# --- Step 3: Export per-orientation PNG thumbnails ---
|
|
241
|
+
VIEW_ORIENTATIONS = ["isometric", "front", "top", "right"]
|
|
242
|
+
preview_view_urls: dict[str, str] = {}
|
|
243
|
+
try:
|
|
244
|
+
config2 = load_config()
|
|
245
|
+
adapter2 = await create_adapter(config2)
|
|
246
|
+
await adapter2.connect()
|
|
247
|
+
if hasattr(adapter2, "open_model"):
|
|
248
|
+
await _reopen_target_model_for_preview(
|
|
249
|
+
adapter2,
|
|
250
|
+
str(active_model_path),
|
|
251
|
+
context="orientation previews",
|
|
252
|
+
)
|
|
253
|
+
_sel_name = str(
|
|
254
|
+
metadata.get("selected_feature_selector_name")
|
|
255
|
+
or metadata.get("selected_feature_name")
|
|
256
|
+
or ""
|
|
257
|
+
).strip()
|
|
258
|
+
if _sel_name and hasattr(adapter2, "select_feature"):
|
|
259
|
+
try:
|
|
260
|
+
await adapter2.select_feature(_sel_name)
|
|
261
|
+
logger.info(
|
|
262
|
+
"[ui.refresh_preview] re-selected '{}' before view screenshots",
|
|
263
|
+
_sel_name,
|
|
264
|
+
)
|
|
265
|
+
except Exception as _sel_exc:
|
|
266
|
+
logger.debug(
|
|
267
|
+
"[ui.refresh_preview] re-select '{}' failed (non-fatal): {}",
|
|
268
|
+
_sel_name,
|
|
269
|
+
_sel_exc,
|
|
270
|
+
)
|
|
271
|
+
for view_name in VIEW_ORIENTATIONS:
|
|
272
|
+
view_path = resolved_preview_dir / f"{session_id}-{view_name}.png"
|
|
273
|
+
try:
|
|
274
|
+
if _sel_name and hasattr(adapter2, "select_feature"):
|
|
275
|
+
await adapter2.select_feature(_sel_name)
|
|
276
|
+
view_result = await adapter2.export_image(
|
|
277
|
+
{
|
|
278
|
+
"file_path": str(view_path.resolve()),
|
|
279
|
+
"format_type": "png",
|
|
280
|
+
"width": 640,
|
|
281
|
+
"height": 480,
|
|
282
|
+
"view_orientation": view_name,
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
if view_result.is_success and view_path.exists():
|
|
286
|
+
ts = int(view_path.stat().st_mtime)
|
|
287
|
+
preview_view_urls[view_name] = (
|
|
288
|
+
f"{api_origin}/previews/{view_path.name}?ts={ts}"
|
|
289
|
+
)
|
|
290
|
+
logger.info(
|
|
291
|
+
"[ui.refresh_preview] view PNG {} exported", view_name
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
logger.warning(
|
|
295
|
+
"[ui.refresh_preview] view PNG {} failed: {}",
|
|
296
|
+
view_name,
|
|
297
|
+
view_result.error or "no detail",
|
|
298
|
+
)
|
|
299
|
+
except Exception as _ve:
|
|
300
|
+
logger.warning(
|
|
301
|
+
"[ui.refresh_preview] view PNG {} exception: {}",
|
|
302
|
+
view_name,
|
|
303
|
+
str(_ve),
|
|
304
|
+
)
|
|
305
|
+
await adapter2.disconnect()
|
|
306
|
+
except Exception as _views_exc:
|
|
307
|
+
logger.warning(
|
|
308
|
+
"[ui.refresh_preview] multi-view export failed: {}", str(_views_exc)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Preserve existing view URLs when a refresh attempt returns no images.
|
|
312
|
+
existing_view_urls = metadata.get("preview_view_urls")
|
|
313
|
+
if isinstance(existing_view_urls, dict):
|
|
314
|
+
if not preview_view_urls:
|
|
315
|
+
preview_view_urls = dict(existing_view_urls)
|
|
316
|
+
else:
|
|
317
|
+
merged_view_urls = dict(existing_view_urls)
|
|
318
|
+
merged_view_urls.update(preview_view_urls)
|
|
319
|
+
preview_view_urls = merged_view_urls
|
|
320
|
+
|
|
321
|
+
viewer_label = (
|
|
322
|
+
f"3D viewer ({viewer_format.upper()})"
|
|
323
|
+
if viewer_format != "none"
|
|
324
|
+
else "3D viewer (no model)"
|
|
325
|
+
)
|
|
326
|
+
png_label = "PNG" if png_ok else f"no PNG ({png_error})"
|
|
327
|
+
status_msg = f"Preview refreshed ({viewer_label}, {png_label})."
|
|
328
|
+
|
|
329
|
+
merge_metadata(
|
|
330
|
+
session_id,
|
|
331
|
+
db_path=db_path,
|
|
332
|
+
preview_orientation=orientation,
|
|
333
|
+
latest_message=status_msg,
|
|
334
|
+
preview_status=status_msg,
|
|
335
|
+
latest_snapshot_id=(str(snapshot_id) if snapshot_id is not None else ""),
|
|
336
|
+
preview_viewer_url=preview_viewer_url,
|
|
337
|
+
preview_stl_ready=(viewer_format != "none"),
|
|
338
|
+
preview_png_ready=png_ok,
|
|
339
|
+
preview_view_urls=preview_view_urls,
|
|
340
|
+
latest_error_text="",
|
|
341
|
+
remediation_hint="",
|
|
342
|
+
)
|
|
343
|
+
except Exception as exc:
|
|
344
|
+
logger.exception("[ui.refresh_preview] failed: {}", exc)
|
|
345
|
+
insert_tool_call_record(
|
|
346
|
+
session_id=session_id,
|
|
347
|
+
tool_name="export_image",
|
|
348
|
+
input_json=json.dumps({"orientation": orientation}, ensure_ascii=True),
|
|
349
|
+
output_json=json.dumps({"error": str(exc)}, ensure_ascii=True),
|
|
350
|
+
success=False,
|
|
351
|
+
db_path=db_path,
|
|
352
|
+
)
|
|
353
|
+
merge_metadata(
|
|
354
|
+
session_id,
|
|
355
|
+
db_path=db_path,
|
|
356
|
+
latest_message=f"Preview refresh failed: {exc}",
|
|
357
|
+
preview_status=f"Preview refresh failed: {exc}",
|
|
358
|
+
preview_orientation=orientation,
|
|
359
|
+
latest_error_text=str(exc),
|
|
360
|
+
remediation_hint="Open a model in SolidWorks and retry preview refresh.",
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
return build_dashboard_state(session_id, db_path=db_path, api_origin=api_origin)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
async def highlight_feature(
|
|
367
|
+
session_id: str,
|
|
368
|
+
feature_name: str,
|
|
369
|
+
*,
|
|
370
|
+
db_path: Path | None = None,
|
|
371
|
+
api_origin: str = DEFAULT_API_ORIGIN,
|
|
372
|
+
) -> dict[str, Any]:
|
|
373
|
+
"""Select and highlight a named feature in the active SolidWorks model.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
session_id: Dashboard session identifier.
|
|
377
|
+
feature_name: Name of the feature to select (must match the feature-tree entry).
|
|
378
|
+
db_path: Optional SQLite path override.
|
|
379
|
+
api_origin: Base URL of the running FastAPI server.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Full dashboard state payload.
|
|
383
|
+
"""
|
|
384
|
+
from .session_service import ( # noqa: PLC0415
|
|
385
|
+
build_dashboard_state,
|
|
386
|
+
ensure_dashboard_session,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
ensure_dashboard_session(session_id, db_path=db_path)
|
|
390
|
+
session_row = get_design_session(session_id, db_path=db_path) or {}
|
|
391
|
+
metadata = parse_json_blob(session_row.get("metadata_json"))
|
|
392
|
+
active_model_path = metadata.get("active_model_path")
|
|
393
|
+
resolved_name = (feature_name or "").strip()
|
|
394
|
+
if not resolved_name:
|
|
395
|
+
merge_metadata(
|
|
396
|
+
session_id,
|
|
397
|
+
db_path=db_path,
|
|
398
|
+
latest_error_text="No feature name provided for selection.",
|
|
399
|
+
remediation_hint="Pass a non-empty feature_name.",
|
|
400
|
+
)
|
|
401
|
+
return build_dashboard_state(session_id, db_path=db_path, api_origin=api_origin)
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
known_feature_names: set[str] = set()
|
|
405
|
+
for snapshot in list_model_state_snapshots(session_id, db_path=db_path):
|
|
406
|
+
raw_tree = snapshot.get("feature_tree_json")
|
|
407
|
+
if not raw_tree:
|
|
408
|
+
continue
|
|
409
|
+
try:
|
|
410
|
+
parsed_tree = json.loads(raw_tree)
|
|
411
|
+
except Exception:
|
|
412
|
+
continue
|
|
413
|
+
if isinstance(parsed_tree, list):
|
|
414
|
+
known_feature_names.update(
|
|
415
|
+
str(item.get("name") or "").strip()
|
|
416
|
+
for item in parsed_tree
|
|
417
|
+
if str(item.get("name") or "").strip()
|
|
418
|
+
)
|
|
419
|
+
if known_feature_names:
|
|
420
|
+
break
|
|
421
|
+
|
|
422
|
+
config = load_config()
|
|
423
|
+
adapter = await create_adapter(config)
|
|
424
|
+
await adapter.connect()
|
|
425
|
+
if active_model_path and hasattr(adapter, "open_model"):
|
|
426
|
+
candidate = Path(str(active_model_path))
|
|
427
|
+
if candidate.exists():
|
|
428
|
+
await adapter.open_model(str(candidate.resolve()))
|
|
429
|
+
selected = False
|
|
430
|
+
entity_type = ""
|
|
431
|
+
selected_name = resolved_name
|
|
432
|
+
if hasattr(adapter, "select_feature"):
|
|
433
|
+
result = await adapter.select_feature(resolved_name)
|
|
434
|
+
if result.is_success and isinstance(result.data, dict):
|
|
435
|
+
selected = bool(result.data.get("selected"))
|
|
436
|
+
entity_type = str(result.data.get("entity_type") or "")
|
|
437
|
+
selected_name = str(result.data.get("selected_name") or resolved_name)
|
|
438
|
+
await adapter.disconnect()
|
|
439
|
+
tracked_only = (not selected) and (resolved_name in known_feature_names)
|
|
440
|
+
merge_metadata(
|
|
441
|
+
session_id,
|
|
442
|
+
db_path=db_path,
|
|
443
|
+
selected_feature_name=resolved_name,
|
|
444
|
+
selected_feature_selector_name=selected_name,
|
|
445
|
+
latest_message=(
|
|
446
|
+
f"Selected '{resolved_name}' ({entity_type}) in SolidWorks."
|
|
447
|
+
if selected
|
|
448
|
+
else (
|
|
449
|
+
f"Tracking '{resolved_name}' from the feature tree. "
|
|
450
|
+
"SolidWorks did not expose a direct selectable handle for that row."
|
|
451
|
+
if tracked_only
|
|
452
|
+
else f"Could not select feature '{resolved_name}' — name may not match the feature tree."
|
|
453
|
+
)
|
|
454
|
+
),
|
|
455
|
+
latest_error_text=(
|
|
456
|
+
""
|
|
457
|
+
if (selected or tracked_only)
|
|
458
|
+
else f"SelectByID2 returned False for '{resolved_name}'."
|
|
459
|
+
),
|
|
460
|
+
remediation_hint=(
|
|
461
|
+
""
|
|
462
|
+
if (selected or tracked_only)
|
|
463
|
+
else "Check that the feature name exactly matches the SolidWorks feature tree entry."
|
|
464
|
+
),
|
|
465
|
+
)
|
|
466
|
+
insert_tool_call_record(
|
|
467
|
+
session_id=session_id,
|
|
468
|
+
tool_name="ui.highlight_feature",
|
|
469
|
+
input_json=json.dumps({"feature_name": resolved_name}, ensure_ascii=True),
|
|
470
|
+
output_json=json.dumps(
|
|
471
|
+
{
|
|
472
|
+
"selected": selected,
|
|
473
|
+
"tracked_only": tracked_only,
|
|
474
|
+
"entity_type": entity_type,
|
|
475
|
+
},
|
|
476
|
+
ensure_ascii=True,
|
|
477
|
+
),
|
|
478
|
+
success=(selected or tracked_only),
|
|
479
|
+
db_path=db_path,
|
|
480
|
+
)
|
|
481
|
+
except Exception as exc:
|
|
482
|
+
logger.exception("[ui.highlight_feature] failed: {}", exc)
|
|
483
|
+
merge_metadata(
|
|
484
|
+
session_id,
|
|
485
|
+
db_path=db_path,
|
|
486
|
+
latest_error_text=str(exc),
|
|
487
|
+
remediation_hint="Ensure SolidWorks is open with the target model loaded.",
|
|
488
|
+
)
|
|
489
|
+
return build_dashboard_state(session_id, db_path=db_path, api_origin=api_origin)
|