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,10 @@
|
|
|
1
|
+
"""FastAPI router for the ``routers`` sub-package.
|
|
2
|
+
|
|
3
|
+
Import individual routers and include them in the FastAPI app:
|
|
4
|
+
|
|
5
|
+
.. code-block:: python
|
|
6
|
+
|
|
7
|
+
from solidworks_mcp.ui.routers import session, model, preview, llm, checkpoint, docs, viewer
|
|
8
|
+
app.include_router(session.router)
|
|
9
|
+
...
|
|
10
|
+
"""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Checkpoint execution route for the Prefab CAD dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from ..services import DEFAULT_SESSION_ID, execute_next_checkpoint
|
|
11
|
+
|
|
12
|
+
router = APIRouter()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CheckpointExecuteRequest(BaseModel):
|
|
16
|
+
"""Request payload for checkpoint execution."""
|
|
17
|
+
|
|
18
|
+
session_id: str = DEFAULT_SESSION_ID
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@router.post("/api/ui/checkpoints/execute-next")
|
|
22
|
+
async def execute_checkpoint(payload: CheckpointExecuteRequest) -> dict[str, Any]:
|
|
23
|
+
"""Execute the next pending design checkpoint against the SolidWorks adapter."""
|
|
24
|
+
return await execute_next_checkpoint(payload.session_id)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Docs context and RAG ingestion routes for the Prefab CAD dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from ..services import DEFAULT_SESSION_ID, fetch_docs_context, ingest_reference_source
|
|
11
|
+
|
|
12
|
+
router = APIRouter()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DocsContextRequest(BaseModel):
|
|
16
|
+
"""Request payload for docs-context refresh."""
|
|
17
|
+
|
|
18
|
+
session_id: str = DEFAULT_SESSION_ID
|
|
19
|
+
query: str = "SolidWorks MCP endpoints"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RagIngestRequest(BaseModel):
|
|
23
|
+
"""Request payload for BYO retrieval ingestion from a local path or URL."""
|
|
24
|
+
|
|
25
|
+
session_id: str = DEFAULT_SESSION_ID
|
|
26
|
+
source_path: str
|
|
27
|
+
namespace: str = "engineering-reference"
|
|
28
|
+
chunk_size: int = 1200
|
|
29
|
+
overlap: int = 200
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@router.post("/api/ui/docs/context")
|
|
33
|
+
async def docs_context(payload: DocsContextRequest) -> dict[str, Any]:
|
|
34
|
+
"""Fetch and cache docs text from the /docs endpoint."""
|
|
35
|
+
return fetch_docs_context(payload.session_id, docs_query=payload.query)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.post("/api/ui/rag/ingest")
|
|
39
|
+
async def rag_ingest(payload: RagIngestRequest) -> dict[str, Any]:
|
|
40
|
+
"""Ingest a local file or URL into the local retrieval index."""
|
|
41
|
+
return ingest_reference_source(
|
|
42
|
+
payload.session_id,
|
|
43
|
+
source_path=payload.source_path,
|
|
44
|
+
namespace=payload.namespace,
|
|
45
|
+
chunk_size=payload.chunk_size,
|
|
46
|
+
overlap=payload.overlap,
|
|
47
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""LLM clarify / inspect / go-orchestration routes for the Prefab CAD dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from ...agents.history_db import get_design_session
|
|
11
|
+
from ..services import (
|
|
12
|
+
DEFAULT_SESSION_ID,
|
|
13
|
+
DEFAULT_USER_GOAL,
|
|
14
|
+
inspect_family,
|
|
15
|
+
request_clarifications,
|
|
16
|
+
run_go_orchestration,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
router = APIRouter()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _resolve_user_goal(session_id: str, user_goal: str) -> str:
|
|
23
|
+
"""Prefer the approved session goal when the client sends the default placeholder."""
|
|
24
|
+
|
|
25
|
+
if user_goal and user_goal != DEFAULT_USER_GOAL:
|
|
26
|
+
return user_goal
|
|
27
|
+
|
|
28
|
+
session_row = get_design_session(session_id) or {}
|
|
29
|
+
return str(session_row.get("user_goal") or DEFAULT_USER_GOAL)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ClarifyWithAnswerRequest(BaseModel):
|
|
33
|
+
"""Request payload for clarify that includes the user's typed answers."""
|
|
34
|
+
|
|
35
|
+
session_id: str = DEFAULT_SESSION_ID
|
|
36
|
+
user_goal: str = DEFAULT_USER_GOAL
|
|
37
|
+
user_answer: str = ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GoOrchestrationRequest(BaseModel):
|
|
41
|
+
"""Request payload for global Go orchestration action."""
|
|
42
|
+
|
|
43
|
+
session_id: str = DEFAULT_SESSION_ID
|
|
44
|
+
user_goal: str = DEFAULT_USER_GOAL
|
|
45
|
+
assumptions_text: str | None = None
|
|
46
|
+
user_answer: str = ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class FamilyInspectRequest(BaseModel):
|
|
50
|
+
"""Request payload for design family classification."""
|
|
51
|
+
|
|
52
|
+
session_id: str = DEFAULT_SESSION_ID
|
|
53
|
+
user_goal: str = DEFAULT_USER_GOAL
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.post("/api/ui/clarify")
|
|
57
|
+
async def clarify(payload: ClarifyWithAnswerRequest) -> dict[str, Any]:
|
|
58
|
+
"""Call the LLM to generate clarifying questions for the design goal."""
|
|
59
|
+
return await request_clarifications(
|
|
60
|
+
payload.session_id,
|
|
61
|
+
user_goal=_resolve_user_goal(payload.session_id, payload.user_goal),
|
|
62
|
+
user_answer=payload.user_answer,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@router.post("/api/ui/family/inspect")
|
|
67
|
+
async def family_inspect(payload: FamilyInspectRequest) -> dict[str, Any]:
|
|
68
|
+
"""Call the LLM to classify the design family from the current goal."""
|
|
69
|
+
return await inspect_family(
|
|
70
|
+
payload.session_id,
|
|
71
|
+
user_goal=_resolve_user_goal(payload.session_id, payload.user_goal),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@router.post("/api/ui/orchestrate/go")
|
|
76
|
+
async def orchestrate_go(payload: GoOrchestrationRequest) -> dict[str, Any]:
|
|
77
|
+
"""Run the full Go orchestration pipeline (clarify → inspect → plan checkpoints)."""
|
|
78
|
+
return await run_go_orchestration(
|
|
79
|
+
payload.session_id,
|
|
80
|
+
user_goal=_resolve_user_goal(payload.session_id, payload.user_goal),
|
|
81
|
+
assumptions_text=payload.assumptions_text,
|
|
82
|
+
user_answer=payload.user_answer,
|
|
83
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Local Ollama model probe / pull / query routes for the Prefab CAD dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter
|
|
6
|
+
|
|
7
|
+
from ..local_llm import (
|
|
8
|
+
LocalAgentResult,
|
|
9
|
+
LocalModelProbeResult,
|
|
10
|
+
LocalModelPullRequest,
|
|
11
|
+
LocalModelPullResult,
|
|
12
|
+
LocalModelQueryRequest,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
router = APIRouter()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.get("/api/ui/local-model/probe")
|
|
19
|
+
async def probe_local_model_endpoint() -> LocalModelProbeResult:
|
|
20
|
+
"""Probe for a running Ollama server and recommend the best Gemma model tier."""
|
|
21
|
+
from ..local_llm import probe_local_model # noqa: PLC0415
|
|
22
|
+
|
|
23
|
+
return await probe_local_model()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.post("/api/ui/local-model/pull")
|
|
27
|
+
async def pull_local_model_endpoint(
|
|
28
|
+
payload: LocalModelPullRequest,
|
|
29
|
+
) -> LocalModelPullResult:
|
|
30
|
+
"""Trigger an Ollama pull for the specified model name."""
|
|
31
|
+
from ..local_llm import pull_ollama_model # noqa: PLC0415
|
|
32
|
+
|
|
33
|
+
return await pull_ollama_model(model=payload.model, endpoint=payload.endpoint)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@router.post("/api/ui/local-model/query")
|
|
37
|
+
async def query_local_model_endpoint(
|
|
38
|
+
payload: LocalModelQueryRequest,
|
|
39
|
+
) -> LocalAgentResult:
|
|
40
|
+
"""Run a free-form prompt against the local Ollama model."""
|
|
41
|
+
from pydantic import BaseModel as _BaseModel
|
|
42
|
+
|
|
43
|
+
from ..local_llm import LocalLLMConfig, run_local_agent # noqa: PLC0415
|
|
44
|
+
|
|
45
|
+
class _FreeFormResponse(_BaseModel):
|
|
46
|
+
text: str
|
|
47
|
+
|
|
48
|
+
config = LocalLLMConfig.from_env()
|
|
49
|
+
if payload.endpoint:
|
|
50
|
+
config = config.model_copy(
|
|
51
|
+
update={
|
|
52
|
+
"endpoint": payload.endpoint,
|
|
53
|
+
"openai_endpoint": f"{payload.endpoint}/v1",
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
if payload.model:
|
|
57
|
+
service_model = (
|
|
58
|
+
payload.model
|
|
59
|
+
if payload.model.startswith("local:")
|
|
60
|
+
else f"local:{payload.model}"
|
|
61
|
+
)
|
|
62
|
+
config = config.model_copy(update={"service_model": service_model})
|
|
63
|
+
|
|
64
|
+
return await run_local_agent(
|
|
65
|
+
system_prompt=payload.system_prompt,
|
|
66
|
+
user_prompt=payload.prompt,
|
|
67
|
+
result_type=_FreeFormResponse,
|
|
68
|
+
config=config,
|
|
69
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Model open/connect routes for the Prefab CAD dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter
|
|
8
|
+
from pydantic import BaseModel, field_validator
|
|
9
|
+
|
|
10
|
+
from ..services import (
|
|
11
|
+
DEFAULT_SESSION_ID,
|
|
12
|
+
connect_target_model,
|
|
13
|
+
open_target_model,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
router = APIRouter()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UploadedFilePayload(BaseModel):
|
|
20
|
+
"""Browser-uploaded file payload returned by Prefab's OpenFilePicker action."""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
size: int
|
|
24
|
+
type: str
|
|
25
|
+
data: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConnectTargetModelRequest(BaseModel):
|
|
29
|
+
"""Request payload for attaching an active SolidWorks target model."""
|
|
30
|
+
|
|
31
|
+
session_id: str = DEFAULT_SESSION_ID
|
|
32
|
+
model_path: str | None = None
|
|
33
|
+
uploaded_files: list[UploadedFilePayload] | None = None
|
|
34
|
+
feature_target_text: str | None = None
|
|
35
|
+
|
|
36
|
+
@field_validator("uploaded_files", mode="before")
|
|
37
|
+
@classmethod
|
|
38
|
+
def _coerce_empty_uploaded_files(cls, v: object) -> object:
|
|
39
|
+
"""Coerce empty string or empty list to None so Pydantic accepts it."""
|
|
40
|
+
if v == "" or v == []:
|
|
41
|
+
return None
|
|
42
|
+
return v
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class OpenTargetModelRequest(BaseModel):
|
|
46
|
+
"""Request payload for opening a target model by file path."""
|
|
47
|
+
|
|
48
|
+
session_id: str = DEFAULT_SESSION_ID
|
|
49
|
+
model_path: str | None = None
|
|
50
|
+
feature_target_text: str | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.post("/api/ui/model/connect")
|
|
54
|
+
async def connect_model(payload: ConnectTargetModelRequest) -> dict[str, Any]:
|
|
55
|
+
"""Attach a target SolidWorks document and derive grounded feature-tree context."""
|
|
56
|
+
return await connect_target_model(
|
|
57
|
+
payload.session_id,
|
|
58
|
+
model_path=payload.model_path,
|
|
59
|
+
uploaded_files=(
|
|
60
|
+
[f.model_dump() for f in payload.uploaded_files]
|
|
61
|
+
if payload.uploaded_files
|
|
62
|
+
else None
|
|
63
|
+
),
|
|
64
|
+
feature_target_text=payload.feature_target_text,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@router.post("/api/ui/model/open")
|
|
69
|
+
async def open_model(payload: OpenTargetModelRequest) -> dict[str, Any]:
|
|
70
|
+
"""Open a SolidWorks model by path and reflect its feature tree into the session."""
|
|
71
|
+
return await open_target_model(
|
|
72
|
+
payload.session_id,
|
|
73
|
+
model_path=payload.model_path,
|
|
74
|
+
feature_target_text=payload.feature_target_text,
|
|
75
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Preview and feature-highlight routes for the Prefab CAD dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from ..services import DEFAULT_SESSION_ID, highlight_feature, refresh_preview
|
|
11
|
+
|
|
12
|
+
router = APIRouter()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PreviewRefreshRequest(BaseModel):
|
|
16
|
+
"""Request payload for preview refresh requests."""
|
|
17
|
+
|
|
18
|
+
session_id: str = DEFAULT_SESSION_ID
|
|
19
|
+
orientation: str = "current"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FeatureSelectRequest(BaseModel):
|
|
23
|
+
"""Request payload for highlighting a named feature in the SolidWorks model tree."""
|
|
24
|
+
|
|
25
|
+
session_id: str = DEFAULT_SESSION_ID
|
|
26
|
+
feature_name: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@router.post("/api/ui/preview/refresh")
|
|
30
|
+
async def preview_refresh(payload: PreviewRefreshRequest) -> dict[str, Any]:
|
|
31
|
+
"""Export the current SolidWorks viewport to a PNG/GLB preview."""
|
|
32
|
+
return await refresh_preview(payload.session_id, orientation=payload.orientation)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.post("/api/ui/feature/select")
|
|
36
|
+
async def feature_select(payload: FeatureSelectRequest) -> dict[str, Any]:
|
|
37
|
+
"""Select and highlight a named feature in the active SolidWorks model."""
|
|
38
|
+
return await highlight_feature(payload.session_id, payload.feature_name)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Session management routes for the Prefab CAD dashboard.
|
|
2
|
+
|
|
3
|
+
Covers: state hydration, debug snapshot, brief approval, preferences,
|
|
4
|
+
workflow selection, notes, context save/load, family accept, and manual-sync reconcile.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from fastapi import APIRouter, Query
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
from ..services import (
|
|
15
|
+
DEFAULT_API_ORIGIN,
|
|
16
|
+
DEFAULT_SESSION_ID,
|
|
17
|
+
DEFAULT_USER_GOAL,
|
|
18
|
+
accept_family_choice,
|
|
19
|
+
approve_design_brief,
|
|
20
|
+
build_dashboard_state,
|
|
21
|
+
build_dashboard_trace_payload,
|
|
22
|
+
load_session_context,
|
|
23
|
+
reconcile_manual_edits,
|
|
24
|
+
save_session_context,
|
|
25
|
+
select_workflow_mode,
|
|
26
|
+
update_session_notes,
|
|
27
|
+
update_ui_preferences,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
router = APIRouter()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Request models
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SessionRequest(BaseModel):
|
|
39
|
+
"""Base request payload containing session scope."""
|
|
40
|
+
|
|
41
|
+
session_id: str = DEFAULT_SESSION_ID
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GoalRequest(SessionRequest):
|
|
45
|
+
"""Request payload for endpoints that operate on the current design goal."""
|
|
46
|
+
|
|
47
|
+
user_goal: str = DEFAULT_USER_GOAL
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class FamilyAcceptRequest(SessionRequest):
|
|
51
|
+
"""Request payload for family acceptance."""
|
|
52
|
+
|
|
53
|
+
family: str | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PreferencesUpdateRequest(SessionRequest):
|
|
57
|
+
"""Request payload for assumptions and model preference updates."""
|
|
58
|
+
|
|
59
|
+
assumptions_text: str
|
|
60
|
+
model_provider: str = "github"
|
|
61
|
+
model_profile: str = "balanced"
|
|
62
|
+
model_name: str | None = None
|
|
63
|
+
local_endpoint: str | None = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class WorkflowSelectionRequest(SessionRequest):
|
|
67
|
+
"""Request payload for selecting the onboarding workflow branch."""
|
|
68
|
+
|
|
69
|
+
workflow_mode: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class NotesUpdateRequest(SessionRequest):
|
|
73
|
+
"""Request payload for saving free-form engineering notes."""
|
|
74
|
+
|
|
75
|
+
notes_text: str = ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ContextSaveRequest(SessionRequest):
|
|
79
|
+
"""Request payload for saving context to a plain JSON snapshot."""
|
|
80
|
+
|
|
81
|
+
context_name: str | None = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ContextLoadRequest(SessionRequest):
|
|
85
|
+
"""Request payload for loading context snapshot from plain JSON."""
|
|
86
|
+
|
|
87
|
+
context_file: str | None = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Routes
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@router.get("/api/ui/state")
|
|
96
|
+
async def get_state(session_id: str = Query(DEFAULT_SESSION_ID)) -> dict[str, Any]:
|
|
97
|
+
"""Hydrate UI state from active session database."""
|
|
98
|
+
return build_dashboard_state(session_id, api_origin=DEFAULT_API_ORIGIN)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@router.get("/api/ui/debug/session")
|
|
102
|
+
async def get_debug_session(
|
|
103
|
+
session_id: str = Query(DEFAULT_SESSION_ID),
|
|
104
|
+
) -> dict[str, Any]:
|
|
105
|
+
"""Return a verbose debug snapshot for the active UI session."""
|
|
106
|
+
return build_dashboard_trace_payload(session_id, api_origin=DEFAULT_API_ORIGIN)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@router.post("/api/ui/brief/approve")
|
|
110
|
+
async def approve_brief(payload: GoalRequest) -> dict[str, Any]:
|
|
111
|
+
"""Accept the user-provided design goal."""
|
|
112
|
+
return approve_design_brief(payload.session_id, payload.user_goal)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@router.post("/api/ui/preferences/update")
|
|
116
|
+
async def update_preferences(payload: PreferencesUpdateRequest) -> dict[str, Any]:
|
|
117
|
+
"""Persist assumptions and provider/model preferences in session metadata."""
|
|
118
|
+
return update_ui_preferences(
|
|
119
|
+
payload.session_id,
|
|
120
|
+
assumptions_text=payload.assumptions_text,
|
|
121
|
+
model_provider=payload.model_provider,
|
|
122
|
+
model_profile=payload.model_profile,
|
|
123
|
+
model_name=payload.model_name,
|
|
124
|
+
local_endpoint=payload.local_endpoint,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@router.post("/api/ui/workflow/select")
|
|
129
|
+
async def update_workflow_mode(payload: WorkflowSelectionRequest) -> dict[str, Any]:
|
|
130
|
+
"""Persist the workflow choice shown on the opening dashboard screen."""
|
|
131
|
+
return select_workflow_mode(payload.session_id, workflow_mode=payload.workflow_mode)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@router.post("/api/ui/family/accept")
|
|
135
|
+
async def accept_family(payload: FamilyAcceptRequest) -> dict[str, Any]:
|
|
136
|
+
"""Accept the proposed design family classification."""
|
|
137
|
+
return accept_family_choice(payload.session_id, family=payload.family)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@router.post("/api/ui/notes/update")
|
|
141
|
+
async def update_notes(payload: NotesUpdateRequest) -> dict[str, Any]:
|
|
142
|
+
"""Persist free-form engineering notes for the session."""
|
|
143
|
+
return update_session_notes(payload.session_id, notes_text=payload.notes_text)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@router.post("/api/ui/context/save")
|
|
147
|
+
async def context_save(payload: ContextSaveRequest) -> dict[str, Any]:
|
|
148
|
+
"""Save current session context to a JSON snapshot file."""
|
|
149
|
+
return save_session_context(payload.session_id, context_name=payload.context_name)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@router.post("/api/ui/context/load")
|
|
153
|
+
async def context_load(payload: ContextLoadRequest) -> dict[str, Any]:
|
|
154
|
+
"""Load a previously saved session context JSON snapshot."""
|
|
155
|
+
return load_session_context(payload.session_id, context_file=payload.context_file)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@router.post("/api/ui/manual-sync/reconcile")
|
|
159
|
+
async def reconcile_edits(payload: SessionRequest) -> dict[str, Any]:
|
|
160
|
+
"""Detect manual edits via snapshot diff and apply forward delta."""
|
|
161
|
+
return reconcile_manual_edits(payload.session_id)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Three.js 3D model viewer route for the Prefab CAD dashboard.
|
|
2
|
+
|
|
3
|
+
# TODO: move _VIEWER_HTML to a Jinja2 template file at templates/viewer.html
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Path
|
|
9
|
+
from fastapi.responses import HTMLResponse
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# Embedded Three.js viewer HTML
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# TODO: move to templates/viewer.html and serve via Jinja2TemplateResponse
|
|
17
|
+
_VIEWER_HTML = """\
|
|
18
|
+
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
20
|
+
<title>3D Model Viewer</title>
|
|
21
|
+
<style>
|
|
22
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
23
|
+
body { background: #0f172a; color: #94a3b8; font-family: system-ui, sans-serif; overflow: hidden; }
|
|
24
|
+
#wrap { width: 100vw; height: 100vh; }
|
|
25
|
+
#overlay { position: fixed; inset: 0; display: flex; flex-direction: column;
|
|
26
|
+
align-items: center; justify-content: center; gap: 10px; pointer-events: none; }
|
|
27
|
+
#status { font-size: 13px; text-align: center; max-width: 300px; line-height: 1.6; }
|
|
28
|
+
#hint { position: fixed; bottom: 10px; left: 50%; transform: translateX(-50%);
|
|
29
|
+
font-size: 11px; opacity: 0.35; user-select: none; }
|
|
30
|
+
#fmt-badge { position: fixed; top: 8px; right: 10px; font-size: 10px;
|
|
31
|
+
opacity: 0.4; letter-spacing: 0.05em; user-select: none; }
|
|
32
|
+
</style></head><body>
|
|
33
|
+
<div id="wrap"></div>
|
|
34
|
+
<div id="overlay">
|
|
35
|
+
<div id="icon" style="font-size:28px">⏳</div>
|
|
36
|
+
<div id="status">Loading 3D model…</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div id="hint">Drag to rotate · Scroll to zoom · Right-drag to pan</div>
|
|
39
|
+
<div id="fmt-badge"></div>
|
|
40
|
+
<script type="importmap">{"imports": {
|
|
41
|
+
"three": "https://cdn.jsdelivr.net/npm/three@0.165.0/build/three.module.js",
|
|
42
|
+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.165.0/examples/jsm/"
|
|
43
|
+
}}</script>
|
|
44
|
+
<script type="module">
|
|
45
|
+
import * as THREE from 'three';
|
|
46
|
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
47
|
+
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
48
|
+
import { STLLoader } from 'three/addons/loaders/STLLoader.js';
|
|
49
|
+
|
|
50
|
+
const params = new URLSearchParams(location.search);
|
|
51
|
+
const pathParts = location.pathname.split('/').filter(Boolean);
|
|
52
|
+
const pathSessionId = pathParts[pathParts.length - 1] || 'prefab-dashboard';
|
|
53
|
+
const sessionId = params.get('session_id') || pathSessionId;
|
|
54
|
+
const ts = params.get('t') || '0';
|
|
55
|
+
const fmt = params.get('fmt') || 'stl';
|
|
56
|
+
|
|
57
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
58
|
+
renderer.setPixelRatio(window.devicePixelRatio);
|
|
59
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
60
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
61
|
+
document.getElementById('wrap').appendChild(renderer.domElement);
|
|
62
|
+
|
|
63
|
+
const scene = new THREE.Scene();
|
|
64
|
+
scene.background = new THREE.Color(0x0f172a);
|
|
65
|
+
|
|
66
|
+
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 100000);
|
|
67
|
+
camera.position.set(0, 100, 250);
|
|
68
|
+
|
|
69
|
+
const controls = new OrbitControls(camera, renderer.domElement);
|
|
70
|
+
controls.enableDamping = true;
|
|
71
|
+
controls.dampingFactor = 0.06;
|
|
72
|
+
|
|
73
|
+
scene.add(new THREE.AmbientLight(0xffffff, 0.8));
|
|
74
|
+
const dirLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
|
75
|
+
dirLight.position.set(1, 2, 1.5);
|
|
76
|
+
scene.add(dirLight);
|
|
77
|
+
const fillLight = new THREE.DirectionalLight(0x8ab4f8, 0.4);
|
|
78
|
+
fillLight.position.set(-1, -1, -1);
|
|
79
|
+
scene.add(fillLight);
|
|
80
|
+
|
|
81
|
+
const stlMaterial = new THREE.MeshPhongMaterial({
|
|
82
|
+
color: 0x3b82f6, specular: 0x1e3a5f, shininess: 60, side: THREE.DoubleSide
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
function fitCamera(object) {
|
|
86
|
+
const box = new THREE.Box3().setFromObject(object);
|
|
87
|
+
const size = box.getSize(new THREE.Vector3());
|
|
88
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
89
|
+
const maxDim = Math.max(size.x, size.y, size.z) || 100;
|
|
90
|
+
camera.position.set(center.x, center.y + maxDim * 0.6, center.z + maxDim * 2);
|
|
91
|
+
camera.near = maxDim * 0.001;
|
|
92
|
+
camera.far = maxDim * 200;
|
|
93
|
+
camera.updateProjectionMatrix();
|
|
94
|
+
controls.target.copy(center);
|
|
95
|
+
controls.update();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function hideOverlay() { document.getElementById('overlay').style.display = 'none'; }
|
|
99
|
+
function showError(msg) {
|
|
100
|
+
document.getElementById('icon').textContent = '( )';
|
|
101
|
+
document.getElementById('status').textContent = msg;
|
|
102
|
+
}
|
|
103
|
+
function onProgress(p) {
|
|
104
|
+
const pct = p.total ? Math.round(p.loaded / p.total * 100) : 0;
|
|
105
|
+
document.getElementById('status').textContent = 'Loading\\u2026 ' + pct + '%';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (fmt === 'glb') {
|
|
109
|
+
document.getElementById('fmt-badge').textContent = 'GLB';
|
|
110
|
+
const glbUrl = location.origin + '/previews/' + sessionId + '.glb?_t=' + ts;
|
|
111
|
+
new GLTFLoader().load(glbUrl, (gltf) => {
|
|
112
|
+
hideOverlay();
|
|
113
|
+
const model = gltf.scene;
|
|
114
|
+
model.traverse((node) => {
|
|
115
|
+
if (node.isMesh && node.material) {
|
|
116
|
+
const mats = Array.isArray(node.material) ? node.material : [node.material];
|
|
117
|
+
mats.forEach((m) => { m.side = THREE.DoubleSide; });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
scene.add(model);
|
|
121
|
+
fitCamera(model);
|
|
122
|
+
}, onProgress, () => showError('No 3D model file yet. Attach a SolidWorks model, then click Refresh 3D View.'));
|
|
123
|
+
} else if (fmt === 'stl') {
|
|
124
|
+
document.getElementById('fmt-badge').textContent = 'STL';
|
|
125
|
+
const stlUrl = location.origin + '/previews/' + sessionId + '.stl?_t=' + ts;
|
|
126
|
+
new STLLoader().load(stlUrl, (geometry) => {
|
|
127
|
+
hideOverlay();
|
|
128
|
+
geometry.computeBoundingBox();
|
|
129
|
+
geometry.center();
|
|
130
|
+
geometry.computeVertexNormals();
|
|
131
|
+
const mesh = new THREE.Mesh(geometry, stlMaterial);
|
|
132
|
+
scene.add(mesh);
|
|
133
|
+
fitCamera(mesh);
|
|
134
|
+
}, onProgress, () => showError('No 3D model file yet. Attach a SolidWorks model, then click Refresh 3D View.'));
|
|
135
|
+
} else {
|
|
136
|
+
showError('No 3D model file yet. Attach a SolidWorks model, then click Refresh 3D View.');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
window.addEventListener('resize', () => {
|
|
140
|
+
camera.aspect = window.innerWidth / window.innerHeight;
|
|
141
|
+
camera.updateProjectionMatrix();
|
|
142
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
143
|
+
});
|
|
144
|
+
(function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); })();
|
|
145
|
+
</script></body></html>
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@router.get("/api/ui/viewer/{session_id}", response_class=HTMLResponse)
|
|
150
|
+
async def get_viewer(
|
|
151
|
+
session_id: str = Path(description="Session identifier for 3D model file routing"),
|
|
152
|
+
) -> HTMLResponse:
|
|
153
|
+
"""Serve the embedded Three.js 3D model viewer page."""
|
|
154
|
+
return HTMLResponse(content=_VIEWER_HTML, media_type="text/html")
|