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,4016 @@
|
|
|
1
|
+
"""Mock SolidWorks adapter for testing and development.
|
|
2
|
+
|
|
3
|
+
This adapter simulates SolidWorks operations for testing on non-Windows platforms or
|
|
4
|
+
when SolidWorks is not available.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import math
|
|
11
|
+
import os
|
|
12
|
+
import random
|
|
13
|
+
import uuid
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from ..exceptions import SolidWorksOperationError
|
|
19
|
+
from .base import (
|
|
20
|
+
AdapterHealth,
|
|
21
|
+
AdapterResult,
|
|
22
|
+
AdapterResultStatus,
|
|
23
|
+
ExtrusionParameters,
|
|
24
|
+
LoftParameters,
|
|
25
|
+
MassProperties,
|
|
26
|
+
RevolveParameters,
|
|
27
|
+
SolidWorksAdapter,
|
|
28
|
+
SolidWorksFeature,
|
|
29
|
+
SolidWorksModel,
|
|
30
|
+
SweepParameters,
|
|
31
|
+
)
|
|
32
|
+
from .solidworks.drawing_standard import gtol_problem
|
|
33
|
+
from .solidworks.features import _AXIS_PLANE_PAIRS
|
|
34
|
+
from .solidworks.io import _normalise_unit_system as _normalise_mock_unit_system
|
|
35
|
+
from .solidworks.sketch import RELATION_NAME_MAP
|
|
36
|
+
|
|
37
|
+
#: Orientations the mock accepts for a drawing view. Kept in step with
|
|
38
|
+
#: ``_NAMED_VIEWS`` in ``adapters/solidworks/io.py`` by a test — the two lists
|
|
39
|
+
#: are maintained separately, and an orientation accepted live but rejected
|
|
40
|
+
#: here could never be covered, since the suite runs against the mock.
|
|
41
|
+
_MOCK_NAMED_VIEWS: frozenset[str] = frozenset(
|
|
42
|
+
{
|
|
43
|
+
"front",
|
|
44
|
+
"back",
|
|
45
|
+
"left",
|
|
46
|
+
"right",
|
|
47
|
+
"top",
|
|
48
|
+
"bottom",
|
|
49
|
+
"isometric",
|
|
50
|
+
"iso",
|
|
51
|
+
"trimetric",
|
|
52
|
+
"dimetric",
|
|
53
|
+
"current",
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
#: Canned assembly component tree used by ``list_features`` when a mock
|
|
59
|
+
#: Assembly's ``_assembly_components`` hasn't been configured by a test.
|
|
60
|
+
#: Two top-level Part components plus one nested sub-assembly one level
|
|
61
|
+
#: deep, per ``docs/agents`` / OpenSpec change ``assembly-aware-list-features``.
|
|
62
|
+
_DEFAULT_ASSEMBLY_COMPONENTS: dict[str, dict[str, Any]] = {
|
|
63
|
+
"PartA": {
|
|
64
|
+
"type": "Part",
|
|
65
|
+
"path": "Mock://PartA.sldprt",
|
|
66
|
+
"features": [
|
|
67
|
+
{"name": "Boss-Extrude1", "type": "Extrusion", "suppressed": False},
|
|
68
|
+
{"name": "Fillet1", "type": "Fillet", "suppressed": False},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
"PartB": {
|
|
72
|
+
"type": "Part",
|
|
73
|
+
"path": "Mock://PartB.sldprt",
|
|
74
|
+
"features": [
|
|
75
|
+
{"name": "Cut-Extrude1", "type": "Cut", "suppressed": False},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
"SubAssembly1": {
|
|
79
|
+
"type": "Assembly",
|
|
80
|
+
"path": "Mock://SubAssembly1.sldasm",
|
|
81
|
+
"features": [
|
|
82
|
+
{"name": "Mate1", "type": "Mate", "suppressed": False},
|
|
83
|
+
],
|
|
84
|
+
"components": {
|
|
85
|
+
"PartC": {
|
|
86
|
+
"type": "Part",
|
|
87
|
+
"path": "Mock://PartC.sldprt",
|
|
88
|
+
"features": [
|
|
89
|
+
{"name": "Boss-Extrude1", "type": "Extrusion", "suppressed": False},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class _BoolCallable:
|
|
98
|
+
"""Compatibility shim that behaves as both bool and callable.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
getter (Callable[[], bool]): The getter value.
|
|
102
|
+
|
|
103
|
+
Attributes:
|
|
104
|
+
_getter (Any): The getter value.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, getter: Callable[[], bool]) -> None:
|
|
108
|
+
"""Initialize the bool callable.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
getter (Callable[[], bool]): The getter value.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
None: None.
|
|
115
|
+
"""
|
|
116
|
+
self._getter = getter
|
|
117
|
+
|
|
118
|
+
def __call__(self) -> bool:
|
|
119
|
+
"""Build internal call.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
bool: True if call, otherwise False.
|
|
123
|
+
"""
|
|
124
|
+
return bool(self._getter())
|
|
125
|
+
|
|
126
|
+
def __bool__(self) -> bool:
|
|
127
|
+
"""Build internal bool.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
bool: True if bool, otherwise False.
|
|
131
|
+
"""
|
|
132
|
+
return bool(self._getter())
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class MockSolidWorksAdapter(SolidWorksAdapter):
|
|
136
|
+
"""Mock adapter that simulates SolidWorks operations.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
config (object | None): Configuration values for the operation. Defaults to None.
|
|
140
|
+
|
|
141
|
+
Attributes:
|
|
142
|
+
_connected (Any): The connected value.
|
|
143
|
+
_delays (Any): The delays value.
|
|
144
|
+
_is_connected_proxy (Any): The is connected proxy value.
|
|
145
|
+
_operation_count (Any): The operation count value.
|
|
146
|
+
_simulate_errors (Any): The simulate errors value.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def _current_model(self) -> SolidWorksModel | None:
|
|
151
|
+
"""The active mock document."""
|
|
152
|
+
return self.__dict__.get("_active_model")
|
|
153
|
+
|
|
154
|
+
@_current_model.setter
|
|
155
|
+
def _current_model(self, model: SolidWorksModel | None) -> None:
|
|
156
|
+
# Switching documents drops the selection, as in real SolidWorks:
|
|
157
|
+
# edges selected on one part must never feed an empty-edge_names
|
|
158
|
+
# fillet/chamfer on another.
|
|
159
|
+
if model is not self.__dict__.get("_active_model"):
|
|
160
|
+
self.__dict__.get("_selection", []).clear()
|
|
161
|
+
self.__dict__["_active_model"] = model
|
|
162
|
+
|
|
163
|
+
def __init__(self, config: object | None = None) -> None:
|
|
164
|
+
"""Initialize the mock solid works adapter.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
config (object | None): Configuration values for the operation. Defaults to None.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
None: None.
|
|
171
|
+
"""
|
|
172
|
+
super().__init__(config)
|
|
173
|
+
cfg: dict[str, Any] = dict(self.config_dict)
|
|
174
|
+
self._connected = False
|
|
175
|
+
self._current_model = None
|
|
176
|
+
self._models: dict[str, SolidWorksModel] = {}
|
|
177
|
+
self._features: dict[str, SolidWorksFeature] = {}
|
|
178
|
+
# Names suppressed via suppress_feature. Kept beside _features rather
|
|
179
|
+
# than on SolidWorksFeature, which has no suppression field.
|
|
180
|
+
self._suppressed_features: set[str] = set()
|
|
181
|
+
# Component tree for an Assembly-type current model, keyed by
|
|
182
|
+
# component name. See ``_flatten_assembly_components`` for shape.
|
|
183
|
+
# Empty means "use the canned default fixture" (see list_features).
|
|
184
|
+
self._assembly_components: dict[str, dict[str, Any]] = {}
|
|
185
|
+
self._sketches: dict[str, str] = {}
|
|
186
|
+
self._current_sketch: str | None = None
|
|
187
|
+
# Names currently "selected" via select_feature / select_face_edges,
|
|
188
|
+
# so append and selection_count behave like the real selection set.
|
|
189
|
+
self._selection: list[str] = []
|
|
190
|
+
# Test hook: RawRebuildIssue objects rebuild_model should report, so
|
|
191
|
+
# the error/warning paths can be exercised without real geometry.
|
|
192
|
+
self.injected_rebuild_issues: list[Any] = []
|
|
193
|
+
# Tracks IDs returned by add_line/add_arc/add_circle/add_centerline/
|
|
194
|
+
# add_rectangle/add_spline so add_sketch_constraint can validate
|
|
195
|
+
# entity1/entity2 the same way the real adapter validates against
|
|
196
|
+
# its sketch-entity registry.
|
|
197
|
+
self._sketch_entity_ids: set[str] = set()
|
|
198
|
+
self._dimensions: dict[str, float] = {}
|
|
199
|
+
# Assembly components inserted via insert_component, keyed by
|
|
200
|
+
# insertion order. See insert_component/list_components/add_mate.
|
|
201
|
+
self._components: list[str] = []
|
|
202
|
+
# Mates added via add_mate, in order, so the geometry summary can
|
|
203
|
+
# report a mate count without real geometry.
|
|
204
|
+
self._mates: list[dict[str, Any]] = []
|
|
205
|
+
# Drawing views placed via create_drawing_view / add_drawing_view /
|
|
206
|
+
# create_technical_drawing, in sheet order. See list_drawing_views.
|
|
207
|
+
self._drawing_views: list[str] = []
|
|
208
|
+
# Reference planes created via create_reference_plane, in creation
|
|
209
|
+
# order, used to invent sequential "PlaneN" names. Shared feature
|
|
210
|
+
# tree counter with create_axis mirrors the live adapter's
|
|
211
|
+
# features_before/features_after pair (see _feature_count in
|
|
212
|
+
# solidworks/features.py).
|
|
213
|
+
self._reference_planes: list[str] = []
|
|
214
|
+
# Axes created via create_axis, in creation order, used to invent
|
|
215
|
+
# sequential "AxisN" names the same way self._reference_planes
|
|
216
|
+
# invents "PlaneN" names, and to validate the axis name passed to
|
|
217
|
+
# pattern_circular the same way known_sources validates mirror
|
|
218
|
+
# sources.
|
|
219
|
+
self._axes: list[str] = []
|
|
220
|
+
# Reference points created via create_reference_point, in creation
|
|
221
|
+
# order, invented "PointN" names on the same pattern as _axes.
|
|
222
|
+
self._reference_points: list[str] = []
|
|
223
|
+
# Centre-mark count per drawing-view name, grown by auto_center_marks.
|
|
224
|
+
self._center_marks: dict[str, int] = {}
|
|
225
|
+
self._feature_tree_count = 0
|
|
226
|
+
# Volume tracked for mirror_feature, in mm^3. Seeded at the
|
|
227
|
+
# live-measured 1819569.1 mm^3 wing volume from
|
|
228
|
+
# _mirror_feature_impl in solidworks/features.py, then doubled on
|
|
229
|
+
# each successful mirror so volume_before/volume_after/volume_ratio
|
|
230
|
+
# stay self-consistent without a real geometry engine.
|
|
231
|
+
self._mirror_volume: float = 1819569.1
|
|
232
|
+
# Volume tracked for pattern_circular, in mm^3. Seeded at the
|
|
233
|
+
# live-measured 48431.5 mm^3 hub+blade volume from
|
|
234
|
+
# _pattern_circular_impl in solidworks/features.py, then grown by
|
|
235
|
+
# _PATTERN_BLADE_VOLUME per extra instance on each successful
|
|
236
|
+
# pattern so volume_before/volume_after/volume_ratio stay
|
|
237
|
+
# self-consistent without a real geometry engine.
|
|
238
|
+
self._pattern_volume: float = 48431.5
|
|
239
|
+
self._operation_count = 0
|
|
240
|
+
|
|
241
|
+
# Configurable simulation delays (in seconds)
|
|
242
|
+
self._delays = {
|
|
243
|
+
"connect": cfg.get("mock_connect_delay", 0.1),
|
|
244
|
+
"model_operation": cfg.get("mock_model_delay", 0.02),
|
|
245
|
+
"feature_operation": cfg.get("mock_feature_delay", 0.03) if cfg else 0.03,
|
|
246
|
+
"sketch_operation": cfg.get("mock_sketch_delay", 0.1),
|
|
247
|
+
}
|
|
248
|
+
self._is_connected_proxy = _BoolCallable(lambda: self._connected)
|
|
249
|
+
self._simulate_errors = bool(cfg.get("simulate_errors", False))
|
|
250
|
+
|
|
251
|
+
def __getattribute__(self, name: str) -> Any:
|
|
252
|
+
"""Build internal getattribute.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
name (str): The name value.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Any: The result produced by the operation.
|
|
259
|
+
"""
|
|
260
|
+
if name == "is_connected":
|
|
261
|
+
return object.__getattribute__(self, "_is_connected_proxy")
|
|
262
|
+
return object.__getattribute__(self, name)
|
|
263
|
+
|
|
264
|
+
async def connect(self) -> None:
|
|
265
|
+
"""Mock connection to SolidWorks.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
None: None.
|
|
269
|
+
"""
|
|
270
|
+
await asyncio.sleep(self._delays["connect"])
|
|
271
|
+
self._connected = True
|
|
272
|
+
|
|
273
|
+
# Initialize some sample data
|
|
274
|
+
self._dimensions = {
|
|
275
|
+
"D1@Sketch1": 10.0,
|
|
276
|
+
"D2@Sketch1": 20.0,
|
|
277
|
+
"D1@Boss-Extrude1": 50.0,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async def disconnect(self) -> None:
|
|
281
|
+
"""Mock disconnection from SolidWorks.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
None: None.
|
|
285
|
+
"""
|
|
286
|
+
self._connected = False
|
|
287
|
+
self._current_model = None
|
|
288
|
+
self._models.clear()
|
|
289
|
+
self._features.clear()
|
|
290
|
+
self._sketches.clear()
|
|
291
|
+
self._current_sketch = None
|
|
292
|
+
self._selection.clear()
|
|
293
|
+
|
|
294
|
+
def is_connected(self) -> bool:
|
|
295
|
+
"""Check if mock connection is active.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
bool: True if connected, otherwise False.
|
|
299
|
+
"""
|
|
300
|
+
return self._connected
|
|
301
|
+
|
|
302
|
+
async def health_check(self) -> AdapterHealth:
|
|
303
|
+
"""Get mock health status.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
AdapterHealth: The result produced by the operation.
|
|
307
|
+
"""
|
|
308
|
+
error_count = int(self._metrics["errors_count"])
|
|
309
|
+
success_count = int(
|
|
310
|
+
self._metrics["operations_count"] - self._metrics["errors_count"]
|
|
311
|
+
)
|
|
312
|
+
return AdapterHealth(
|
|
313
|
+
healthy=self._connected,
|
|
314
|
+
last_check=datetime.now(),
|
|
315
|
+
error_count=error_count,
|
|
316
|
+
success_count=success_count,
|
|
317
|
+
average_response_time=self._metrics["average_response_time"],
|
|
318
|
+
connection_status="connected" if self._connected else "disconnected",
|
|
319
|
+
metrics={
|
|
320
|
+
"adapter_type": "mock",
|
|
321
|
+
"operations_count": self._operation_count,
|
|
322
|
+
"models_count": len(self._models),
|
|
323
|
+
"features_count": len(self._features),
|
|
324
|
+
"sketches_count": len(self._sketches),
|
|
325
|
+
},
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
async def open_model(self, file_path: str) -> AdapterResult[SolidWorksModel]:
|
|
329
|
+
"""Mock opening a SolidWorks model.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
file_path (str): Path to the target file.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
AdapterResult[SolidWorksModel]: The result produced by the operation.
|
|
336
|
+
|
|
337
|
+
Raises:
|
|
338
|
+
SolidWorksOperationError: Simulated adapter failure.
|
|
339
|
+
"""
|
|
340
|
+
if not self._connected:
|
|
341
|
+
return AdapterResult(
|
|
342
|
+
status=AdapterResultStatus.ERROR, error="Not connected to SolidWorks"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
if self._simulate_errors:
|
|
346
|
+
raise SolidWorksOperationError("Simulated adapter failure")
|
|
347
|
+
|
|
348
|
+
await asyncio.sleep(self._delays["model_operation"])
|
|
349
|
+
self._operation_count += 1
|
|
350
|
+
|
|
351
|
+
# Determine model type from file extension
|
|
352
|
+
file_path_lower = file_path.lower()
|
|
353
|
+
if file_path_lower.endswith(".sldprt"):
|
|
354
|
+
model_type = "Part"
|
|
355
|
+
elif file_path_lower.endswith(".sldasm"):
|
|
356
|
+
model_type = "Assembly"
|
|
357
|
+
elif file_path_lower.endswith(".slddrw"):
|
|
358
|
+
model_type = "Drawing"
|
|
359
|
+
else:
|
|
360
|
+
return AdapterResult(
|
|
361
|
+
status=AdapterResultStatus.ERROR,
|
|
362
|
+
error=f"Unsupported file type: {file_path}",
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Create mock model
|
|
366
|
+
model_name = file_path.split("/")[-1].split("\\")[-1]
|
|
367
|
+
model = SolidWorksModel(
|
|
368
|
+
path=file_path,
|
|
369
|
+
name=model_name,
|
|
370
|
+
type=model_type,
|
|
371
|
+
is_active=True,
|
|
372
|
+
configuration="Default",
|
|
373
|
+
properties={
|
|
374
|
+
"created": datetime.now().isoformat(),
|
|
375
|
+
"model_id": model_name or f"Part{len(self._models) + 1}",
|
|
376
|
+
"file_size": random.randint(100000, 5000000),
|
|
377
|
+
},
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
self._current_model = model
|
|
381
|
+
self._models[file_path] = model
|
|
382
|
+
|
|
383
|
+
return AdapterResult(
|
|
384
|
+
status=AdapterResultStatus.SUCCESS,
|
|
385
|
+
data=model,
|
|
386
|
+
execution_time=self._delays["model_operation"],
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
async def close_model(self, save: bool = False) -> AdapterResult[None]:
|
|
390
|
+
"""Mock closing the current model.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
save (bool): The save value. Defaults to False.
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
AdapterResult[None]: The result produced by the operation.
|
|
397
|
+
"""
|
|
398
|
+
if not self._current_model:
|
|
399
|
+
return AdapterResult(
|
|
400
|
+
status=AdapterResultStatus.WARNING, error="No active model to close"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
await asyncio.sleep(self._delays["model_operation"] / 2)
|
|
404
|
+
self._operation_count += 1
|
|
405
|
+
|
|
406
|
+
if save:
|
|
407
|
+
# Simulate save operation
|
|
408
|
+
await asyncio.sleep(self._delays["model_operation"])
|
|
409
|
+
|
|
410
|
+
self._current_model = None
|
|
411
|
+
|
|
412
|
+
return AdapterResult(
|
|
413
|
+
status=AdapterResultStatus.SUCCESS,
|
|
414
|
+
data=None,
|
|
415
|
+
execution_time=self._delays["model_operation"],
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
async def get_model_info(self) -> AdapterResult[dict[str, Any]]:
|
|
419
|
+
"""Mock metadata for the currently active model.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
423
|
+
"""
|
|
424
|
+
if not self._current_model:
|
|
425
|
+
return AdapterResult(
|
|
426
|
+
status=AdapterResultStatus.ERROR,
|
|
427
|
+
error="No active model",
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
await asyncio.sleep(self._delays["model_operation"] / 2)
|
|
431
|
+
self._operation_count += 1
|
|
432
|
+
|
|
433
|
+
model = self._current_model
|
|
434
|
+
feature_count = len(self._features)
|
|
435
|
+
info = {
|
|
436
|
+
"title": model.name,
|
|
437
|
+
"path": model.path,
|
|
438
|
+
"type": model.type,
|
|
439
|
+
"configuration": model.configuration or "Default",
|
|
440
|
+
"is_dirty": False,
|
|
441
|
+
"feature_count": feature_count,
|
|
442
|
+
"rebuild_status": 0,
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return AdapterResult(
|
|
446
|
+
status=AdapterResultStatus.SUCCESS,
|
|
447
|
+
data=info,
|
|
448
|
+
execution_time=self._delays["model_operation"] / 2,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
async def list_features(
|
|
452
|
+
self, include_suppressed: bool = False, max_assembly_depth: int = 2
|
|
453
|
+
) -> AdapterResult[list[dict[str, Any]]]:
|
|
454
|
+
"""Mock feature tree listing for the active model.
|
|
455
|
+
|
|
456
|
+
For an Assembly model, flattens in every configured component's
|
|
457
|
+
features (see ``_assembly_components``) alongside the document's own
|
|
458
|
+
features, tagged with ``component``/``component_path`` the same way
|
|
459
|
+
the real adapter does. Falls back to a small canned two-component
|
|
460
|
+
fixture (plus one nested sub-assembly) when no components have been
|
|
461
|
+
configured, mirroring the existing "seed realistic feature names"
|
|
462
|
+
behavior for an empty Part.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
include_suppressed (bool): The include suppressed value. Defaults to False.
|
|
466
|
+
max_assembly_depth (int): Sub-assembly recursion budget. Ignored
|
|
467
|
+
for non-Assembly models. Defaults to 2.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
AdapterResult[list[dict[str, Any]]]: The result produced by the operation.
|
|
471
|
+
"""
|
|
472
|
+
if not self._current_model:
|
|
473
|
+
return AdapterResult(
|
|
474
|
+
status=AdapterResultStatus.ERROR,
|
|
475
|
+
error="No active model",
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
await asyncio.sleep(self._delays["feature_operation"] / 2)
|
|
479
|
+
self._operation_count += 1
|
|
480
|
+
|
|
481
|
+
is_assembly = self._current_model.type == "Assembly"
|
|
482
|
+
|
|
483
|
+
# Seed realistic feature names for empty mock state.
|
|
484
|
+
if not self._features:
|
|
485
|
+
seeded: list[dict[str, Any]] = [
|
|
486
|
+
{
|
|
487
|
+
"name": "Origin",
|
|
488
|
+
"type": "OriginProfileFeature",
|
|
489
|
+
"suppressed": False,
|
|
490
|
+
"component": None,
|
|
491
|
+
"component_path": None,
|
|
492
|
+
"component_parent": None,
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
"name": "Front Plane",
|
|
496
|
+
"type": "RefPlane",
|
|
497
|
+
"suppressed": False,
|
|
498
|
+
"component": None,
|
|
499
|
+
"component_path": None,
|
|
500
|
+
"component_parent": None,
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
"name": "Right Plane",
|
|
504
|
+
"type": "RefPlane",
|
|
505
|
+
"suppressed": False,
|
|
506
|
+
"component": None,
|
|
507
|
+
"component_path": None,
|
|
508
|
+
"component_parent": None,
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
"name": "Top Plane",
|
|
512
|
+
"type": "RefPlane",
|
|
513
|
+
"suppressed": False,
|
|
514
|
+
"component": None,
|
|
515
|
+
"component_path": None,
|
|
516
|
+
"component_parent": None,
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
"name": "Sketch1",
|
|
520
|
+
"type": "ProfileFeature",
|
|
521
|
+
"suppressed": False,
|
|
522
|
+
"component": None,
|
|
523
|
+
"component_path": None,
|
|
524
|
+
"component_parent": None,
|
|
525
|
+
},
|
|
526
|
+
]
|
|
527
|
+
if is_assembly:
|
|
528
|
+
components = self._assembly_components or _DEFAULT_ASSEMBLY_COMPONENTS
|
|
529
|
+
seeded.extend(
|
|
530
|
+
self._flatten_assembly_components(
|
|
531
|
+
components, include_suppressed, max_assembly_depth - 1, None
|
|
532
|
+
)
|
|
533
|
+
)
|
|
534
|
+
return AdapterResult(
|
|
535
|
+
status=AdapterResultStatus.SUCCESS,
|
|
536
|
+
data=seeded,
|
|
537
|
+
execution_time=self._delays["feature_operation"] / 2,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
feature_rows: list[dict[str, Any]] = []
|
|
541
|
+
for i, feature in enumerate(self._features.values()):
|
|
542
|
+
row = {
|
|
543
|
+
"name": feature.name,
|
|
544
|
+
"type": feature.type,
|
|
545
|
+
"suppressed": bool((feature.properties or {}).get("suppressed", False)),
|
|
546
|
+
"position": i,
|
|
547
|
+
"component": None,
|
|
548
|
+
"component_path": None,
|
|
549
|
+
"component_parent": None,
|
|
550
|
+
}
|
|
551
|
+
if include_suppressed or not row["suppressed"]:
|
|
552
|
+
feature_rows.append(row)
|
|
553
|
+
|
|
554
|
+
if is_assembly:
|
|
555
|
+
components = self._assembly_components or _DEFAULT_ASSEMBLY_COMPONENTS
|
|
556
|
+
feature_rows.extend(
|
|
557
|
+
self._flatten_assembly_components(
|
|
558
|
+
components,
|
|
559
|
+
include_suppressed,
|
|
560
|
+
max_assembly_depth - 1,
|
|
561
|
+
None,
|
|
562
|
+
)
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
return AdapterResult(
|
|
566
|
+
status=AdapterResultStatus.SUCCESS,
|
|
567
|
+
data=feature_rows,
|
|
568
|
+
execution_time=self._delays["feature_operation"] / 2,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
def _flatten_assembly_components(
|
|
572
|
+
self,
|
|
573
|
+
components: dict[str, dict[str, Any]],
|
|
574
|
+
include_suppressed: bool,
|
|
575
|
+
depth_remaining: int,
|
|
576
|
+
parent_component: str | None,
|
|
577
|
+
) -> list[dict[str, Any]]:
|
|
578
|
+
"""Flatten a mock component tree into tagged feature descriptors.
|
|
579
|
+
|
|
580
|
+
Mirrors the real adapter's recursion rules: a sub-assembly component
|
|
581
|
+
is expanded (its own features plus a recursive pass over its
|
|
582
|
+
components) only while ``depth_remaining > 0``; beyond that it is
|
|
583
|
+
represented by a single bare descriptor. An unresolvable component
|
|
584
|
+
(``type: "Unresolved"`` in the fixture) always produces one
|
|
585
|
+
``UnresolvedComponent`` descriptor.
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
components: Mapping of component name to a fixture dict with
|
|
589
|
+
``type`` ("Part", "Assembly", or "Unresolved"), ``path``,
|
|
590
|
+
``features`` (list of ``{name, type, suppressed}``), and,
|
|
591
|
+
for "Assembly" components, a nested ``components`` mapping.
|
|
592
|
+
include_suppressed: Include suppressed entries when ``True``.
|
|
593
|
+
depth_remaining: Sub-assembly recursion budget remaining.
|
|
594
|
+
parent_component: Name of the component this level of
|
|
595
|
+
``components`` is nested inside, or ``None`` for top-level.
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
list[dict[str, Any]]: Flattened, component-tagged descriptors.
|
|
599
|
+
"""
|
|
600
|
+
results: list[dict[str, Any]] = []
|
|
601
|
+
for name, comp in components.items():
|
|
602
|
+
comp_type = comp.get("type")
|
|
603
|
+
path = comp.get("path")
|
|
604
|
+
|
|
605
|
+
if comp_type == "Unresolved":
|
|
606
|
+
results.append(
|
|
607
|
+
{
|
|
608
|
+
"name": name,
|
|
609
|
+
"type": "UnresolvedComponent",
|
|
610
|
+
"suppressed": False,
|
|
611
|
+
"position": -1,
|
|
612
|
+
"component": name,
|
|
613
|
+
"component_path": None,
|
|
614
|
+
"component_parent": parent_component,
|
|
615
|
+
}
|
|
616
|
+
)
|
|
617
|
+
continue
|
|
618
|
+
|
|
619
|
+
if comp_type == "Assembly" and depth_remaining <= 0:
|
|
620
|
+
results.append(
|
|
621
|
+
{
|
|
622
|
+
"name": name,
|
|
623
|
+
"type": "Component",
|
|
624
|
+
"suppressed": False,
|
|
625
|
+
"position": -1,
|
|
626
|
+
"component": name,
|
|
627
|
+
"component_path": path,
|
|
628
|
+
"component_parent": parent_component,
|
|
629
|
+
}
|
|
630
|
+
)
|
|
631
|
+
continue
|
|
632
|
+
|
|
633
|
+
for i, feature in enumerate(comp.get("features", [])):
|
|
634
|
+
suppressed = bool(feature.get("suppressed", False))
|
|
635
|
+
if not include_suppressed and suppressed:
|
|
636
|
+
continue
|
|
637
|
+
results.append(
|
|
638
|
+
{
|
|
639
|
+
"name": feature["name"],
|
|
640
|
+
"type": feature["type"],
|
|
641
|
+
"suppressed": suppressed,
|
|
642
|
+
"position": i,
|
|
643
|
+
"component": name,
|
|
644
|
+
"component_path": path,
|
|
645
|
+
"component_parent": parent_component,
|
|
646
|
+
}
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
if comp_type == "Assembly":
|
|
650
|
+
results.extend(
|
|
651
|
+
self._flatten_assembly_components(
|
|
652
|
+
comp.get("components", {}),
|
|
653
|
+
include_suppressed,
|
|
654
|
+
depth_remaining - 1,
|
|
655
|
+
name,
|
|
656
|
+
)
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
return results
|
|
660
|
+
|
|
661
|
+
#: Default tree items every new part has, selectable like the real ones.
|
|
662
|
+
_DEFAULT_SELECTABLE = ("Front Plane", "Top Plane", "Right Plane", "Origin")
|
|
663
|
+
|
|
664
|
+
async def select_feature(
|
|
665
|
+
self, feature_name: str, append: bool = False
|
|
666
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
667
|
+
"""Mock feature selection, mirroring the real adapter's contract.
|
|
668
|
+
|
|
669
|
+
Succeeds only for names the mock is tracking for the active model
|
|
670
|
+
(created features, sketches, default planes); anything else is an
|
|
671
|
+
ERROR naming the feature - never SUCCESS with ``selected: False``.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
feature_name (str): The feature name, optionally ``@doc`` qualified.
|
|
675
|
+
append (bool): Add to the current selection. Defaults to False.
|
|
676
|
+
|
|
677
|
+
Returns:
|
|
678
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
679
|
+
"""
|
|
680
|
+
if not self._current_model:
|
|
681
|
+
return AdapterResult(
|
|
682
|
+
status=AdapterResultStatus.ERROR,
|
|
683
|
+
error="No active model",
|
|
684
|
+
)
|
|
685
|
+
await asyncio.sleep(self._delays["feature_operation"] / 4)
|
|
686
|
+
self._operation_count += 1
|
|
687
|
+
|
|
688
|
+
bare = feature_name.split("@", 1)[0].strip()
|
|
689
|
+
known = (
|
|
690
|
+
{f.name for f in self._features.values()}
|
|
691
|
+
| set(self._sketches)
|
|
692
|
+
| set(self._DEFAULT_SELECTABLE)
|
|
693
|
+
)
|
|
694
|
+
if bare not in known:
|
|
695
|
+
return AdapterResult(
|
|
696
|
+
status=AdapterResultStatus.ERROR,
|
|
697
|
+
error=f"Feature not found or could not be selected: {feature_name}",
|
|
698
|
+
)
|
|
699
|
+
if not append:
|
|
700
|
+
self._selection.clear()
|
|
701
|
+
if bare not in self._selection:
|
|
702
|
+
self._selection.append(bare)
|
|
703
|
+
return AdapterResult(
|
|
704
|
+
status=AdapterResultStatus.SUCCESS,
|
|
705
|
+
data={
|
|
706
|
+
"selected": True,
|
|
707
|
+
"feature_name": feature_name,
|
|
708
|
+
"selected_name": bare,
|
|
709
|
+
"entity_type": "mock",
|
|
710
|
+
"selection_count": len(self._selection),
|
|
711
|
+
},
|
|
712
|
+
execution_time=self._delays["feature_operation"] / 4,
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
async def get_selected_entities(self) -> AdapterResult[list[dict[str, Any]]]:
|
|
716
|
+
"""Mock UI selection — always reports one canned face and one canned edge.
|
|
717
|
+
|
|
718
|
+
There's no real UI to read a selection from in mock mode, so this
|
|
719
|
+
returns a stable, plausible-shaped fixture rather than an empty list,
|
|
720
|
+
so callers exercising this tool in tests get realistic field names.
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
AdapterResult[list[dict[str, Any]]]: The result produced by the operation.
|
|
724
|
+
"""
|
|
725
|
+
if not self._current_model:
|
|
726
|
+
return AdapterResult(
|
|
727
|
+
status=AdapterResultStatus.ERROR,
|
|
728
|
+
error="No active model",
|
|
729
|
+
)
|
|
730
|
+
await asyncio.sleep(self._delays["feature_operation"] / 4)
|
|
731
|
+
return AdapterResult(
|
|
732
|
+
status=AdapterResultStatus.SUCCESS,
|
|
733
|
+
data=[
|
|
734
|
+
{
|
|
735
|
+
"index": 1,
|
|
736
|
+
"type": "FACE",
|
|
737
|
+
"bounding_box_mm": {
|
|
738
|
+
"min": [0.0, 0.0, 0.0],
|
|
739
|
+
"max": [30.0, 30.0, 0.0],
|
|
740
|
+
},
|
|
741
|
+
"center_mm": [15.0, 15.0, 0.0],
|
|
742
|
+
"area_mm2": 900.0,
|
|
743
|
+
"normal": [0.0, 0.0, 1.0],
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
"index": 2,
|
|
747
|
+
"type": "EDGE",
|
|
748
|
+
"start_mm": [0.0, 0.0, -15.0],
|
|
749
|
+
"end_mm": [30.0, 0.0, -15.0],
|
|
750
|
+
"length_mm": 30.0,
|
|
751
|
+
},
|
|
752
|
+
],
|
|
753
|
+
execution_time=self._delays["feature_operation"] / 4,
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
async def select_face_edges(
|
|
757
|
+
self,
|
|
758
|
+
point: list[float] | None = None,
|
|
759
|
+
side: str | None = None,
|
|
760
|
+
plane: str | None = None,
|
|
761
|
+
edge_type: str = "all",
|
|
762
|
+
append: bool = False,
|
|
763
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
764
|
+
"""Mock face-edge multi-selection on a canned 10 mm block.
|
|
765
|
+
|
|
766
|
+
Mirrors the real adapter's contract: the same input validation, the
|
|
767
|
+
same payload shape, and the same failure cases. The block spans
|
|
768
|
+
0..10 mm on every axis; the +Z/-Z faces carry a 2 mm radius hole rim
|
|
769
|
+
once any circle has been sketched, so ``edge_type="circular"``
|
|
770
|
+
behaves like a real drilled part. ``Front Plane`` (z = 0) is the only
|
|
771
|
+
reference plane with a face on it.
|
|
772
|
+
"""
|
|
773
|
+
from .solidworks.face_selection import side_vector, validate_face_query
|
|
774
|
+
|
|
775
|
+
problem = validate_face_query(point, side, plane, edge_type)
|
|
776
|
+
if problem:
|
|
777
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error=problem)
|
|
778
|
+
if not self._current_model:
|
|
779
|
+
return AdapterResult(
|
|
780
|
+
status=AdapterResultStatus.ERROR, error="No active model"
|
|
781
|
+
)
|
|
782
|
+
if getattr(self._current_model, "type", "Part") != "Part":
|
|
783
|
+
return AdapterResult(
|
|
784
|
+
status=AdapterResultStatus.ERROR,
|
|
785
|
+
error="select_face_edges requires an active part document.",
|
|
786
|
+
)
|
|
787
|
+
await asyncio.sleep(self._delays["feature_operation"] / 4)
|
|
788
|
+
|
|
789
|
+
if point is not None:
|
|
790
|
+
normal = self._mock_face_at(point)
|
|
791
|
+
if normal is None:
|
|
792
|
+
return AdapterResult(
|
|
793
|
+
status=AdapterResultStatus.ERROR,
|
|
794
|
+
error=f"No face found at point {list(point)} mm.",
|
|
795
|
+
)
|
|
796
|
+
elif side is not None:
|
|
797
|
+
normal = side_vector(side)
|
|
798
|
+
elif plane == "Front Plane":
|
|
799
|
+
normal = (0.0, 0.0, -1.0)
|
|
800
|
+
else:
|
|
801
|
+
return AdapterResult(
|
|
802
|
+
status=AdapterResultStatus.ERROR,
|
|
803
|
+
error=f"No body face lies on {plane!r}.",
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
center = [5.0 + 5.0 * c for c in normal]
|
|
807
|
+
has_hole = any(e.startswith("Circle") for e in self._sketch_entity_ids)
|
|
808
|
+
edges: list[dict[str, Any]] = [
|
|
809
|
+
{"curve": "linear", "length_mm": 10.0, "face_index": 0} for _ in range(4)
|
|
810
|
+
]
|
|
811
|
+
if has_hole and normal[2] != 0:
|
|
812
|
+
edges.append(
|
|
813
|
+
{
|
|
814
|
+
"curve": "circular",
|
|
815
|
+
"center_mm": [5.0, 5.0, center[2]],
|
|
816
|
+
"axis": [0.0, 0.0, 1.0],
|
|
817
|
+
"radius_mm": 2.0,
|
|
818
|
+
"face_index": 0,
|
|
819
|
+
}
|
|
820
|
+
)
|
|
821
|
+
edges = [e for e in edges if edge_type == "all" or e["curve"] == edge_type]
|
|
822
|
+
if not edges:
|
|
823
|
+
kind = "" if edge_type == "all" else f"{edge_type} "
|
|
824
|
+
return AdapterResult(
|
|
825
|
+
status=AdapterResultStatus.ERROR,
|
|
826
|
+
error=f"The selected face has no {kind}edges (face centre {center} mm).",
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
if not append:
|
|
830
|
+
self._selection.clear()
|
|
831
|
+
self._selection.extend(f"Edge{i}" for i in range(len(edges)))
|
|
832
|
+
face = {
|
|
833
|
+
"index": 0,
|
|
834
|
+
"center_mm": center,
|
|
835
|
+
"normal": [float(c) for c in normal],
|
|
836
|
+
"area_mm2": 100.0,
|
|
837
|
+
"planar": True,
|
|
838
|
+
}
|
|
839
|
+
return AdapterResult(
|
|
840
|
+
status=AdapterResultStatus.SUCCESS,
|
|
841
|
+
data={
|
|
842
|
+
"face": face,
|
|
843
|
+
"faces": [face],
|
|
844
|
+
"edge_type": edge_type,
|
|
845
|
+
"edges": edges,
|
|
846
|
+
"selected_edges": len(edges),
|
|
847
|
+
"selection_count": len(self._selection),
|
|
848
|
+
},
|
|
849
|
+
execution_time=self._delays["feature_operation"] / 4,
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
async def add_fillet(
|
|
853
|
+
self, radius: float, edge_names: list[str]
|
|
854
|
+
) -> AdapterResult[SolidWorksFeature]:
|
|
855
|
+
"""Mock constant-radius fillet (see :meth:`_mock_edge_feature`)."""
|
|
856
|
+
return await self._mock_edge_feature("Fillet", radius, edge_names)
|
|
857
|
+
|
|
858
|
+
async def add_chamfer(
|
|
859
|
+
self, distance: float, edge_names: list[str]
|
|
860
|
+
) -> AdapterResult[SolidWorksFeature]:
|
|
861
|
+
"""Mock equal-distance chamfer (see :meth:`_mock_edge_feature`)."""
|
|
862
|
+
return await self._mock_edge_feature("Chamfer", distance, edge_names)
|
|
863
|
+
|
|
864
|
+
async def _mock_edge_feature(
|
|
865
|
+
self, kind: str, size: float, edge_names: list[str]
|
|
866
|
+
) -> AdapterResult[SolidWorksFeature]:
|
|
867
|
+
"""Create a mock fillet/chamfer on named edges or the live selection.
|
|
868
|
+
|
|
869
|
+
Mirrors the real contract: an empty ``edge_names`` acts on whatever is
|
|
870
|
+
currently selected (e.g. after ``select_face_edges``), and is an error
|
|
871
|
+
when nothing is.
|
|
872
|
+
"""
|
|
873
|
+
if not self._current_model:
|
|
874
|
+
return AdapterResult(
|
|
875
|
+
status=AdapterResultStatus.ERROR, error="No active model"
|
|
876
|
+
)
|
|
877
|
+
if size <= 0:
|
|
878
|
+
return AdapterResult(
|
|
879
|
+
status=AdapterResultStatus.ERROR,
|
|
880
|
+
error=f"{kind} size must be positive",
|
|
881
|
+
)
|
|
882
|
+
edges = list(edge_names) or list(self._selection)
|
|
883
|
+
if not edges:
|
|
884
|
+
return AdapterResult(
|
|
885
|
+
status=AdapterResultStatus.ERROR,
|
|
886
|
+
error=f"No edges given and nothing is selected for the {kind.lower()}",
|
|
887
|
+
)
|
|
888
|
+
await asyncio.sleep(self._delays["feature_operation"] / 4)
|
|
889
|
+
count = sum(1 for f in self._features.values() if f.type == kind) + 1
|
|
890
|
+
feature = SolidWorksFeature(
|
|
891
|
+
name=f"{kind}{count}",
|
|
892
|
+
type=kind,
|
|
893
|
+
id=f"{kind.lower()}_{uuid.uuid4().hex[:8]}",
|
|
894
|
+
parameters={"size": size, "edges": edges},
|
|
895
|
+
properties={"created": datetime.now().isoformat(), "mock": True},
|
|
896
|
+
)
|
|
897
|
+
self._features[feature.id or feature.name] = feature
|
|
898
|
+
self._selection.clear()
|
|
899
|
+
return AdapterResult(
|
|
900
|
+
status=AdapterResultStatus.SUCCESS,
|
|
901
|
+
data=feature,
|
|
902
|
+
execution_time=self._delays["feature_operation"] / 4,
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
async def rebuild_model(
|
|
906
|
+
self, top_level_only: bool = False
|
|
907
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
908
|
+
"""Mock rebuild with the real adapter's diagnostics contract.
|
|
909
|
+
|
|
910
|
+
Clean by default; issues placed in ``injected_rebuild_issues`` are
|
|
911
|
+
explained with the same ``rebuild_diagnostics`` logic the real
|
|
912
|
+
adapter uses, so ERROR-with-data / warnings-only behave identically.
|
|
913
|
+
"""
|
|
914
|
+
from .solidworks.rebuild_diagnostics import explain, summarize
|
|
915
|
+
|
|
916
|
+
if not self._current_model:
|
|
917
|
+
return AdapterResult(
|
|
918
|
+
status=AdapterResultStatus.ERROR, error="No active model"
|
|
919
|
+
)
|
|
920
|
+
await asyncio.sleep(self._delays["model_operation"] / 4)
|
|
921
|
+
entries = [explain(i) for i in self.injected_rebuild_issues]
|
|
922
|
+
error_count, warning_count, summary = summarize(entries)
|
|
923
|
+
data = {
|
|
924
|
+
"rebuilt": True,
|
|
925
|
+
"top_level_only": top_level_only,
|
|
926
|
+
"errors": entries,
|
|
927
|
+
"error_count": error_count,
|
|
928
|
+
"warning_count": warning_count,
|
|
929
|
+
"summary": summary,
|
|
930
|
+
}
|
|
931
|
+
return AdapterResult(
|
|
932
|
+
status=(
|
|
933
|
+
AdapterResultStatus.ERROR
|
|
934
|
+
if error_count
|
|
935
|
+
else AdapterResultStatus.SUCCESS
|
|
936
|
+
),
|
|
937
|
+
data=data,
|
|
938
|
+
error=summary if error_count else None,
|
|
939
|
+
execution_time=self._delays["model_operation"] / 4,
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
@staticmethod
|
|
943
|
+
def _mock_face_at(point: list[float]) -> tuple[float, float, float] | None:
|
|
944
|
+
"""Return the outward normal of the mock block face containing ``point``."""
|
|
945
|
+
if not all(-1e-6 <= c <= 10.0 + 1e-6 for c in point):
|
|
946
|
+
return None
|
|
947
|
+
for axis in range(3):
|
|
948
|
+
for value, sign in ((0.0, -1.0), (10.0, 1.0)):
|
|
949
|
+
if abs(point[axis] - value) <= 1e-6:
|
|
950
|
+
normal = [0.0, 0.0, 0.0]
|
|
951
|
+
normal[axis] = sign
|
|
952
|
+
return (normal[0], normal[1], normal[2])
|
|
953
|
+
return None
|
|
954
|
+
|
|
955
|
+
async def list_configurations(self) -> AdapterResult[list[str]]:
|
|
956
|
+
"""Mock configuration listing for the active model.
|
|
957
|
+
|
|
958
|
+
Returns:
|
|
959
|
+
AdapterResult[list[str]]: The result produced by the operation.
|
|
960
|
+
"""
|
|
961
|
+
if not self._current_model:
|
|
962
|
+
return AdapterResult(
|
|
963
|
+
status=AdapterResultStatus.ERROR,
|
|
964
|
+
error="No active model",
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
await asyncio.sleep(self._delays["model_operation"] / 2)
|
|
968
|
+
self._operation_count += 1
|
|
969
|
+
|
|
970
|
+
active = self._current_model.configuration or "Default"
|
|
971
|
+
configs = [active] if active == "Default" else ["Default", active]
|
|
972
|
+
|
|
973
|
+
return AdapterResult(
|
|
974
|
+
status=AdapterResultStatus.SUCCESS,
|
|
975
|
+
data=configs,
|
|
976
|
+
execution_time=self._delays["model_operation"] / 2,
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
async def create_part(
|
|
980
|
+
self,
|
|
981
|
+
name: str | None = None,
|
|
982
|
+
units: str | None = None,
|
|
983
|
+
template: str | None = None,
|
|
984
|
+
) -> AdapterResult[SolidWorksModel]:
|
|
985
|
+
"""Mock creating a new part.
|
|
986
|
+
|
|
987
|
+
Args:
|
|
988
|
+
name (str | None): The name value. Defaults to None.
|
|
989
|
+
units (str | None): The units value. Defaults to None.
|
|
990
|
+
template (str | None): Template file path. Defaults to None.
|
|
991
|
+
|
|
992
|
+
Returns:
|
|
993
|
+
AdapterResult[SolidWorksModel]: The result produced by the operation.
|
|
994
|
+
"""
|
|
995
|
+
if not self._connected:
|
|
996
|
+
return AdapterResult(
|
|
997
|
+
status=AdapterResultStatus.ERROR, error="Not connected to SolidWorks"
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
token = "mm"
|
|
1001
|
+
if units is not None and str(units).strip():
|
|
1002
|
+
token = _normalise_mock_unit_system(str(units))
|
|
1003
|
+
if token is None:
|
|
1004
|
+
return AdapterResult(
|
|
1005
|
+
status=AdapterResultStatus.ERROR,
|
|
1006
|
+
error=(
|
|
1007
|
+
f"Unrecognised units {units!r}. "
|
|
1008
|
+
"Expected one of: mm, cm, m, in, ft."
|
|
1009
|
+
),
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
await asyncio.sleep(self._delays["model_operation"])
|
|
1013
|
+
self._operation_count += 1
|
|
1014
|
+
|
|
1015
|
+
model_id = name or f"Part{len(self._models) + 1}"
|
|
1016
|
+
model = SolidWorksModel(
|
|
1017
|
+
path=f"Mock://{model_id}.sldprt",
|
|
1018
|
+
name=f"{model_id}",
|
|
1019
|
+
type="Part",
|
|
1020
|
+
is_active=True,
|
|
1021
|
+
configuration="Default",
|
|
1022
|
+
properties={
|
|
1023
|
+
"created": datetime.now().isoformat(),
|
|
1024
|
+
"mock": True,
|
|
1025
|
+
"units": token,
|
|
1026
|
+
"template": template,
|
|
1027
|
+
},
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
self._current_model = model
|
|
1031
|
+
self._models[model.path] = model
|
|
1032
|
+
|
|
1033
|
+
return AdapterResult(
|
|
1034
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1035
|
+
data=model,
|
|
1036
|
+
execution_time=self._delays["model_operation"],
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
async def create_assembly(
|
|
1040
|
+
self, name: str | None = None, template: str | None = None
|
|
1041
|
+
) -> AdapterResult[SolidWorksModel]:
|
|
1042
|
+
"""Mock creating a new assembly.
|
|
1043
|
+
|
|
1044
|
+
Args:
|
|
1045
|
+
name (str | None): The name value. Defaults to None.
|
|
1046
|
+
template (str | None): Template file path. Defaults to None.
|
|
1047
|
+
|
|
1048
|
+
Returns:
|
|
1049
|
+
AdapterResult[SolidWorksModel]: The result produced by the operation.
|
|
1050
|
+
"""
|
|
1051
|
+
if not self._connected:
|
|
1052
|
+
return AdapterResult(
|
|
1053
|
+
status=AdapterResultStatus.ERROR, error="Not connected to SolidWorks"
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
await asyncio.sleep(self._delays["model_operation"])
|
|
1057
|
+
self._operation_count += 1
|
|
1058
|
+
|
|
1059
|
+
model_id = name or f"Assembly{len(self._models) + 1}"
|
|
1060
|
+
model = SolidWorksModel(
|
|
1061
|
+
path=f"Mock://{model_id}.sldasm",
|
|
1062
|
+
name=f"{model_id}",
|
|
1063
|
+
type="Assembly",
|
|
1064
|
+
is_active=True,
|
|
1065
|
+
configuration="Default",
|
|
1066
|
+
properties={
|
|
1067
|
+
"created": datetime.now().isoformat(),
|
|
1068
|
+
"mock": True,
|
|
1069
|
+
"template": template,
|
|
1070
|
+
},
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
self._current_model = model
|
|
1074
|
+
self._models[model.path] = model
|
|
1075
|
+
|
|
1076
|
+
return AdapterResult(
|
|
1077
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1078
|
+
data=model,
|
|
1079
|
+
execution_time=self._delays["model_operation"],
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
async def create_drawing(
|
|
1083
|
+
self, name: str | None = None, template: str | None = None
|
|
1084
|
+
) -> AdapterResult[SolidWorksModel]:
|
|
1085
|
+
"""Mock creating a new drawing.
|
|
1086
|
+
|
|
1087
|
+
Args:
|
|
1088
|
+
name (str | None): The name value. Defaults to None.
|
|
1089
|
+
template (str | None): Template file path. Defaults to None.
|
|
1090
|
+
|
|
1091
|
+
Returns:
|
|
1092
|
+
AdapterResult[SolidWorksModel]: The result produced by the operation.
|
|
1093
|
+
"""
|
|
1094
|
+
if not self._connected:
|
|
1095
|
+
return AdapterResult(
|
|
1096
|
+
status=AdapterResultStatus.ERROR, error="Not connected to SolidWorks"
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
await asyncio.sleep(self._delays["model_operation"])
|
|
1100
|
+
self._operation_count += 1
|
|
1101
|
+
|
|
1102
|
+
model_id = name or f"Drawing{len(self._models) + 1}"
|
|
1103
|
+
model = SolidWorksModel(
|
|
1104
|
+
path=f"Mock://{model_id}.slddrw",
|
|
1105
|
+
name=f"{model_id}",
|
|
1106
|
+
type="Drawing",
|
|
1107
|
+
is_active=True,
|
|
1108
|
+
configuration="Default",
|
|
1109
|
+
properties={
|
|
1110
|
+
"created": datetime.now().isoformat(),
|
|
1111
|
+
"mock": True,
|
|
1112
|
+
"template": template,
|
|
1113
|
+
},
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
self._current_model = model
|
|
1117
|
+
self._models[model.path] = model
|
|
1118
|
+
|
|
1119
|
+
return AdapterResult(
|
|
1120
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1121
|
+
data=model,
|
|
1122
|
+
execution_time=self._delays["model_operation"],
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
async def create_extrusion(
|
|
1126
|
+
self,
|
|
1127
|
+
params: ExtrusionParameters | str,
|
|
1128
|
+
depth: float | None = None,
|
|
1129
|
+
direction: str | None = None,
|
|
1130
|
+
) -> AdapterResult[SolidWorksFeature]:
|
|
1131
|
+
"""Mock creating an extrusion feature.
|
|
1132
|
+
|
|
1133
|
+
Args:
|
|
1134
|
+
params (ExtrusionParameters | str): The params value.
|
|
1135
|
+
depth (float | None): The depth value. Defaults to None.
|
|
1136
|
+
direction (str | None): The direction value. Defaults to None.
|
|
1137
|
+
|
|
1138
|
+
Returns:
|
|
1139
|
+
AdapterResult[SolidWorksFeature]: The result produced by the operation.
|
|
1140
|
+
"""
|
|
1141
|
+
if not self._current_model:
|
|
1142
|
+
return AdapterResult(
|
|
1143
|
+
status=AdapterResultStatus.ERROR, error="No active model"
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
if isinstance(params, str):
|
|
1147
|
+
params = ExtrusionParameters(depth=depth or 0.0)
|
|
1148
|
+
|
|
1149
|
+
await asyncio.sleep(self._delays["feature_operation"])
|
|
1150
|
+
self._operation_count += 1
|
|
1151
|
+
|
|
1152
|
+
feature_id = f"Boss-Extrude{len(self._features) + 1}"
|
|
1153
|
+
feature = SolidWorksFeature(
|
|
1154
|
+
name=feature_id,
|
|
1155
|
+
type="Extrusion",
|
|
1156
|
+
id=str(uuid.uuid4()),
|
|
1157
|
+
parameters={
|
|
1158
|
+
"depth": params.depth,
|
|
1159
|
+
"direction": direction or "blind",
|
|
1160
|
+
"draft_angle": params.draft_angle,
|
|
1161
|
+
"reverse_direction": params.reverse_direction,
|
|
1162
|
+
"both_directions": params.both_directions,
|
|
1163
|
+
"thin_feature": params.thin_feature,
|
|
1164
|
+
"thin_thickness": params.thin_thickness,
|
|
1165
|
+
},
|
|
1166
|
+
properties={"created": datetime.now().isoformat(), "mock": True},
|
|
1167
|
+
)
|
|
1168
|
+
|
|
1169
|
+
feature_key = feature.id or feature.name
|
|
1170
|
+
self._features[feature_key] = feature
|
|
1171
|
+
|
|
1172
|
+
return AdapterResult(
|
|
1173
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1174
|
+
data=feature,
|
|
1175
|
+
execution_time=self._delays["feature_operation"],
|
|
1176
|
+
)
|
|
1177
|
+
|
|
1178
|
+
async def create_revolve(
|
|
1179
|
+
self, params: RevolveParameters
|
|
1180
|
+
) -> AdapterResult[SolidWorksFeature]:
|
|
1181
|
+
"""Mock creating a revolve feature.
|
|
1182
|
+
|
|
1183
|
+
Args:
|
|
1184
|
+
params (RevolveParameters): The params value.
|
|
1185
|
+
|
|
1186
|
+
Returns:
|
|
1187
|
+
AdapterResult[SolidWorksFeature]: The result produced by the operation.
|
|
1188
|
+
"""
|
|
1189
|
+
if not self._current_model:
|
|
1190
|
+
return AdapterResult(
|
|
1191
|
+
status=AdapterResultStatus.ERROR, error="No active model"
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
await asyncio.sleep(self._delays["feature_operation"])
|
|
1195
|
+
self._operation_count += 1
|
|
1196
|
+
|
|
1197
|
+
feature_id = f"Boss-Revolve{len(self._features) + 1}"
|
|
1198
|
+
feature = SolidWorksFeature(
|
|
1199
|
+
name=feature_id,
|
|
1200
|
+
type="Revolve",
|
|
1201
|
+
id=str(uuid.uuid4()),
|
|
1202
|
+
parameters={
|
|
1203
|
+
"angle": params.angle,
|
|
1204
|
+
"reverse_direction": params.reverse_direction,
|
|
1205
|
+
"both_directions": params.both_directions,
|
|
1206
|
+
"thin_feature": params.thin_feature,
|
|
1207
|
+
"thin_thickness": params.thin_thickness,
|
|
1208
|
+
},
|
|
1209
|
+
properties={"created": datetime.now().isoformat(), "mock": True},
|
|
1210
|
+
)
|
|
1211
|
+
|
|
1212
|
+
feature_key = feature.id or feature.name
|
|
1213
|
+
self._features[feature_key] = feature
|
|
1214
|
+
|
|
1215
|
+
return AdapterResult(
|
|
1216
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1217
|
+
data=feature,
|
|
1218
|
+
execution_time=self._delays["feature_operation"],
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
async def create_sweep(
|
|
1222
|
+
self, params: SweepParameters
|
|
1223
|
+
) -> AdapterResult[SolidWorksFeature]:
|
|
1224
|
+
"""Mock creating a sweep feature.
|
|
1225
|
+
|
|
1226
|
+
Args:
|
|
1227
|
+
params (SweepParameters): The params value.
|
|
1228
|
+
|
|
1229
|
+
Returns:
|
|
1230
|
+
AdapterResult[SolidWorksFeature]: The result produced by the operation.
|
|
1231
|
+
"""
|
|
1232
|
+
if not self._current_model:
|
|
1233
|
+
return AdapterResult(
|
|
1234
|
+
status=AdapterResultStatus.ERROR, error="No active model"
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
await asyncio.sleep(self._delays["feature_operation"])
|
|
1238
|
+
self._operation_count += 1
|
|
1239
|
+
|
|
1240
|
+
feature_id = f"Boss-Sweep{len(self._features) + 1}"
|
|
1241
|
+
feature = SolidWorksFeature(
|
|
1242
|
+
name=feature_id,
|
|
1243
|
+
type="Sweep",
|
|
1244
|
+
id=str(uuid.uuid4()),
|
|
1245
|
+
parameters={
|
|
1246
|
+
"path": params.path,
|
|
1247
|
+
"twist_along_path": params.twist_along_path,
|
|
1248
|
+
"twist_angle": params.twist_angle,
|
|
1249
|
+
},
|
|
1250
|
+
properties={"created": datetime.now().isoformat(), "mock": True},
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
feature_key = feature.id or feature.name
|
|
1254
|
+
self._features[feature_key] = feature
|
|
1255
|
+
|
|
1256
|
+
return AdapterResult(
|
|
1257
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1258
|
+
data=feature,
|
|
1259
|
+
execution_time=self._delays["feature_operation"],
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
async def create_loft(
|
|
1263
|
+
self, params: LoftParameters
|
|
1264
|
+
) -> AdapterResult[SolidWorksFeature]:
|
|
1265
|
+
"""Mock creating a loft feature.
|
|
1266
|
+
|
|
1267
|
+
Args:
|
|
1268
|
+
params (LoftParameters): The params value.
|
|
1269
|
+
|
|
1270
|
+
Returns:
|
|
1271
|
+
AdapterResult[SolidWorksFeature]: The result produced by the operation.
|
|
1272
|
+
"""
|
|
1273
|
+
if not self._current_model:
|
|
1274
|
+
return AdapterResult(
|
|
1275
|
+
status=AdapterResultStatus.ERROR, error="No active model"
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
await asyncio.sleep(self._delays["feature_operation"])
|
|
1279
|
+
self._operation_count += 1
|
|
1280
|
+
|
|
1281
|
+
feature_id = f"Boss-Loft{len(self._features) + 1}"
|
|
1282
|
+
feature = SolidWorksFeature(
|
|
1283
|
+
name=feature_id,
|
|
1284
|
+
type="Loft",
|
|
1285
|
+
id=str(uuid.uuid4()),
|
|
1286
|
+
parameters={
|
|
1287
|
+
"profiles": params.profiles,
|
|
1288
|
+
"guide_curves": params.guide_curves,
|
|
1289
|
+
"start_tangent": params.start_tangent,
|
|
1290
|
+
"end_tangent": params.end_tangent,
|
|
1291
|
+
},
|
|
1292
|
+
properties={"created": datetime.now().isoformat(), "mock": True},
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
feature_key = feature.id or feature.name
|
|
1296
|
+
self._features[feature_key] = feature
|
|
1297
|
+
|
|
1298
|
+
return AdapterResult(
|
|
1299
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1300
|
+
data=feature,
|
|
1301
|
+
execution_time=self._delays["feature_operation"],
|
|
1302
|
+
)
|
|
1303
|
+
|
|
1304
|
+
async def create_sketch(self, plane: str) -> AdapterResult[dict[str, Any]]: # type: ignore[override]
|
|
1305
|
+
"""Mock creating a sketch.
|
|
1306
|
+
|
|
1307
|
+
Args:
|
|
1308
|
+
plane (str): The plane value.
|
|
1309
|
+
|
|
1310
|
+
Returns:
|
|
1311
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
1312
|
+
"""
|
|
1313
|
+
if not self._current_model:
|
|
1314
|
+
# Legacy tests start sketching immediately after connect.
|
|
1315
|
+
await self.create_part()
|
|
1316
|
+
|
|
1317
|
+
await asyncio.sleep(self._delays["sketch_operation"])
|
|
1318
|
+
self._operation_count += 1
|
|
1319
|
+
|
|
1320
|
+
sketch_id = f"Sketch{len(self._sketches) + 1}"
|
|
1321
|
+
self._sketches[sketch_id] = plane
|
|
1322
|
+
self._current_sketch = sketch_id
|
|
1323
|
+
|
|
1324
|
+
return AdapterResult(
|
|
1325
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1326
|
+
data={
|
|
1327
|
+
"id": sketch_id,
|
|
1328
|
+
"name": sketch_id,
|
|
1329
|
+
"sketch_name": sketch_id,
|
|
1330
|
+
"plane": plane,
|
|
1331
|
+
},
|
|
1332
|
+
execution_time=self._delays["sketch_operation"],
|
|
1333
|
+
)
|
|
1334
|
+
|
|
1335
|
+
async def add_sketch_line(
|
|
1336
|
+
self,
|
|
1337
|
+
x1: float,
|
|
1338
|
+
y1: float,
|
|
1339
|
+
x2: float,
|
|
1340
|
+
y2: float,
|
|
1341
|
+
construction: bool = False,
|
|
1342
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
1343
|
+
"""Legacy compatibility wrapper around add_line.
|
|
1344
|
+
|
|
1345
|
+
Args:
|
|
1346
|
+
x1 (float): The x1 value.
|
|
1347
|
+
y1 (float): The y1 value.
|
|
1348
|
+
x2 (float): The x2 value.
|
|
1349
|
+
y2 (float): The y2 value.
|
|
1350
|
+
construction (bool): The construction value. Defaults to False.
|
|
1351
|
+
|
|
1352
|
+
Returns:
|
|
1353
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
1354
|
+
"""
|
|
1355
|
+
result = await self.add_line(x1, y1, x2, y2)
|
|
1356
|
+
if not result.is_success:
|
|
1357
|
+
return AdapterResult(
|
|
1358
|
+
status=result.status,
|
|
1359
|
+
error=result.error,
|
|
1360
|
+
execution_time=result.execution_time,
|
|
1361
|
+
)
|
|
1362
|
+
return AdapterResult(
|
|
1363
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1364
|
+
data={
|
|
1365
|
+
"id": result.data,
|
|
1366
|
+
"start": {"x": x1, "y": y1},
|
|
1367
|
+
"end": {"x": x2, "y": y2},
|
|
1368
|
+
"construction": construction,
|
|
1369
|
+
},
|
|
1370
|
+
execution_time=result.execution_time,
|
|
1371
|
+
)
|
|
1372
|
+
|
|
1373
|
+
async def save_file(
|
|
1374
|
+
self, file_path: str | None = None
|
|
1375
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
1376
|
+
"""Legacy compatibility save operation for tests.
|
|
1377
|
+
|
|
1378
|
+
Args:
|
|
1379
|
+
file_path (str | None): Path to the target file. Defaults to None.
|
|
1380
|
+
|
|
1381
|
+
Returns:
|
|
1382
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
1383
|
+
"""
|
|
1384
|
+
if not self._current_model:
|
|
1385
|
+
return AdapterResult(
|
|
1386
|
+
status=AdapterResultStatus.ERROR,
|
|
1387
|
+
error="No active model",
|
|
1388
|
+
)
|
|
1389
|
+
await asyncio.sleep(0.02)
|
|
1390
|
+
self._operation_count += 1
|
|
1391
|
+
resolved_path = file_path or self._current_model.path
|
|
1392
|
+
return AdapterResult(
|
|
1393
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1394
|
+
data={"file_path": resolved_path, "saved": True},
|
|
1395
|
+
execution_time=0.02,
|
|
1396
|
+
)
|
|
1397
|
+
|
|
1398
|
+
async def add_line(
|
|
1399
|
+
self, x1: float, y1: float, x2: float, y2: float
|
|
1400
|
+
) -> AdapterResult[str]:
|
|
1401
|
+
"""Mock adding a line to sketch.
|
|
1402
|
+
|
|
1403
|
+
Args:
|
|
1404
|
+
x1 (float): The x1 value.
|
|
1405
|
+
y1 (float): The y1 value.
|
|
1406
|
+
x2 (float): The x2 value.
|
|
1407
|
+
y2 (float): The y2 value.
|
|
1408
|
+
|
|
1409
|
+
Returns:
|
|
1410
|
+
AdapterResult[str]: The result produced by the operation.
|
|
1411
|
+
"""
|
|
1412
|
+
if not self._current_sketch:
|
|
1413
|
+
return AdapterResult(
|
|
1414
|
+
status=AdapterResultStatus.ERROR, error="No active sketch"
|
|
1415
|
+
)
|
|
1416
|
+
|
|
1417
|
+
await asyncio.sleep(self._delays["sketch_operation"] / 2)
|
|
1418
|
+
self._operation_count += 1
|
|
1419
|
+
|
|
1420
|
+
line_id = f"Line{random.randint(1000, 9999)}"
|
|
1421
|
+
self._sketch_entity_ids.add(line_id)
|
|
1422
|
+
|
|
1423
|
+
return AdapterResult(
|
|
1424
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1425
|
+
data=line_id,
|
|
1426
|
+
execution_time=self._delays["sketch_operation"] / 2,
|
|
1427
|
+
)
|
|
1428
|
+
|
|
1429
|
+
async def add_centerline(
|
|
1430
|
+
self, x1: float, y1: float, x2: float, y2: float
|
|
1431
|
+
) -> AdapterResult[str]:
|
|
1432
|
+
"""Mock adding a construction centerline to sketch.
|
|
1433
|
+
|
|
1434
|
+
Args:
|
|
1435
|
+
x1 (float): The x1 value.
|
|
1436
|
+
y1 (float): The y1 value.
|
|
1437
|
+
x2 (float): The x2 value.
|
|
1438
|
+
y2 (float): The y2 value.
|
|
1439
|
+
|
|
1440
|
+
Returns:
|
|
1441
|
+
AdapterResult[str]: The result produced by the operation.
|
|
1442
|
+
"""
|
|
1443
|
+
if not self._current_sketch:
|
|
1444
|
+
return AdapterResult(
|
|
1445
|
+
status=AdapterResultStatus.ERROR, error="No active sketch"
|
|
1446
|
+
)
|
|
1447
|
+
|
|
1448
|
+
await asyncio.sleep(self._delays["sketch_operation"] / 2)
|
|
1449
|
+
self._operation_count += 1
|
|
1450
|
+
|
|
1451
|
+
centerline_id = f"Centerline_{random.randint(1000, 9999)}"
|
|
1452
|
+
self._sketch_entity_ids.add(centerline_id)
|
|
1453
|
+
|
|
1454
|
+
return AdapterResult(
|
|
1455
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1456
|
+
data=centerline_id,
|
|
1457
|
+
execution_time=self._delays["sketch_operation"] / 2,
|
|
1458
|
+
)
|
|
1459
|
+
|
|
1460
|
+
async def add_spline(self, points: list[dict[str, float]]) -> AdapterResult[str]:
|
|
1461
|
+
"""Mock adding a NURBS spline through the supplied points.
|
|
1462
|
+
|
|
1463
|
+
Args:
|
|
1464
|
+
points (list[dict[str, float]]): Ordered control points with
|
|
1465
|
+
``"x"`` / ``"y"`` keys. Minimum 2 points.
|
|
1466
|
+
|
|
1467
|
+
Returns:
|
|
1468
|
+
AdapterResult[str]: The result produced by the operation.
|
|
1469
|
+
"""
|
|
1470
|
+
if not self._current_sketch:
|
|
1471
|
+
return AdapterResult(
|
|
1472
|
+
status=AdapterResultStatus.ERROR, error="No active sketch"
|
|
1473
|
+
)
|
|
1474
|
+
if len(points) < 2:
|
|
1475
|
+
return AdapterResult(
|
|
1476
|
+
status=AdapterResultStatus.ERROR,
|
|
1477
|
+
error="add_spline requires at least 2 points",
|
|
1478
|
+
)
|
|
1479
|
+
# Match the real adapter's point contract: each dict must carry
|
|
1480
|
+
# both ``x`` and ``y`` keys, or the live impl raises KeyError on
|
|
1481
|
+
# ``point["x"]``. Validating here keeps mock/live parity so tests
|
|
1482
|
+
# that pass against the mock won't fail live on malformed input.
|
|
1483
|
+
for index, point in enumerate(points):
|
|
1484
|
+
if not isinstance(point, dict) or "x" not in point or "y" not in point:
|
|
1485
|
+
return AdapterResult(
|
|
1486
|
+
status=AdapterResultStatus.ERROR,
|
|
1487
|
+
error=(
|
|
1488
|
+
f"add_spline point at index {index} is missing "
|
|
1489
|
+
"required 'x' and/or 'y' keys"
|
|
1490
|
+
),
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
await asyncio.sleep(self._delays["sketch_operation"] / 2)
|
|
1494
|
+
self._operation_count += 1
|
|
1495
|
+
|
|
1496
|
+
spline_id = f"Spline_{random.randint(1000, 9999)}"
|
|
1497
|
+
self._sketch_entity_ids.add(spline_id)
|
|
1498
|
+
|
|
1499
|
+
return AdapterResult(
|
|
1500
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1501
|
+
data=spline_id,
|
|
1502
|
+
execution_time=self._delays["sketch_operation"] / 2,
|
|
1503
|
+
)
|
|
1504
|
+
|
|
1505
|
+
async def add_circle(
|
|
1506
|
+
self, center_x: float, center_y: float, radius: float
|
|
1507
|
+
) -> AdapterResult[str]:
|
|
1508
|
+
"""Mock adding a circle to sketch.
|
|
1509
|
+
|
|
1510
|
+
Args:
|
|
1511
|
+
center_x (float): The center x value.
|
|
1512
|
+
center_y (float): The center y value.
|
|
1513
|
+
radius (float): The radius value.
|
|
1514
|
+
|
|
1515
|
+
Returns:
|
|
1516
|
+
AdapterResult[str]: The result produced by the operation.
|
|
1517
|
+
"""
|
|
1518
|
+
if not self._current_sketch:
|
|
1519
|
+
return AdapterResult(
|
|
1520
|
+
status=AdapterResultStatus.ERROR, error="No active sketch"
|
|
1521
|
+
)
|
|
1522
|
+
|
|
1523
|
+
await asyncio.sleep(self._delays["sketch_operation"] / 2)
|
|
1524
|
+
self._operation_count += 1
|
|
1525
|
+
|
|
1526
|
+
circle_id = f"Circle{random.randint(1000, 9999)}"
|
|
1527
|
+
self._sketch_entity_ids.add(circle_id)
|
|
1528
|
+
|
|
1529
|
+
return AdapterResult(
|
|
1530
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1531
|
+
data=circle_id,
|
|
1532
|
+
execution_time=self._delays["sketch_operation"] / 2,
|
|
1533
|
+
)
|
|
1534
|
+
|
|
1535
|
+
async def add_arc(
|
|
1536
|
+
self,
|
|
1537
|
+
center_x: float,
|
|
1538
|
+
center_y: float,
|
|
1539
|
+
start_x: float,
|
|
1540
|
+
start_y: float,
|
|
1541
|
+
end_x: float,
|
|
1542
|
+
end_y: float,
|
|
1543
|
+
) -> AdapterResult[str]:
|
|
1544
|
+
"""Mock adding a circular arc to sketch.
|
|
1545
|
+
|
|
1546
|
+
Args:
|
|
1547
|
+
center_x (float): Arc centre X.
|
|
1548
|
+
center_y (float): Arc centre Y.
|
|
1549
|
+
start_x (float): Arc start point X.
|
|
1550
|
+
start_y (float): Arc start point Y.
|
|
1551
|
+
end_x (float): Arc end point X.
|
|
1552
|
+
end_y (float): Arc end point Y.
|
|
1553
|
+
|
|
1554
|
+
Returns:
|
|
1555
|
+
AdapterResult[str]: The result produced by the operation.
|
|
1556
|
+
"""
|
|
1557
|
+
if not self._current_sketch:
|
|
1558
|
+
return AdapterResult(
|
|
1559
|
+
status=AdapterResultStatus.ERROR, error="No active sketch"
|
|
1560
|
+
)
|
|
1561
|
+
|
|
1562
|
+
await asyncio.sleep(self._delays["sketch_operation"] / 2)
|
|
1563
|
+
self._operation_count += 1
|
|
1564
|
+
|
|
1565
|
+
arc_id = f"Arc_{random.randint(1000, 9999)}"
|
|
1566
|
+
self._sketch_entity_ids.add(arc_id)
|
|
1567
|
+
|
|
1568
|
+
return AdapterResult(
|
|
1569
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1570
|
+
data=arc_id,
|
|
1571
|
+
execution_time=self._delays["sketch_operation"] / 2,
|
|
1572
|
+
)
|
|
1573
|
+
|
|
1574
|
+
async def add_rectangle(
|
|
1575
|
+
self, x1: float, y1: float, x2: float, y2: float
|
|
1576
|
+
) -> AdapterResult[str]:
|
|
1577
|
+
"""Mock adding a rectangle to sketch.
|
|
1578
|
+
|
|
1579
|
+
Args:
|
|
1580
|
+
x1 (float): The x1 value.
|
|
1581
|
+
y1 (float): The y1 value.
|
|
1582
|
+
x2 (float): The x2 value.
|
|
1583
|
+
y2 (float): The y2 value.
|
|
1584
|
+
|
|
1585
|
+
Returns:
|
|
1586
|
+
AdapterResult[str]: The result produced by the operation.
|
|
1587
|
+
"""
|
|
1588
|
+
if not self._current_sketch:
|
|
1589
|
+
return AdapterResult(
|
|
1590
|
+
status=AdapterResultStatus.ERROR, error="No active sketch"
|
|
1591
|
+
)
|
|
1592
|
+
|
|
1593
|
+
await asyncio.sleep(self._delays["sketch_operation"])
|
|
1594
|
+
self._operation_count += 1
|
|
1595
|
+
|
|
1596
|
+
rect_id = f"Rectangle{random.randint(1000, 9999)}"
|
|
1597
|
+
self._sketch_entity_ids.add(rect_id)
|
|
1598
|
+
|
|
1599
|
+
return AdapterResult(
|
|
1600
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1601
|
+
data=rect_id,
|
|
1602
|
+
execution_time=self._delays["sketch_operation"],
|
|
1603
|
+
)
|
|
1604
|
+
|
|
1605
|
+
async def add_ellipse(
|
|
1606
|
+
self,
|
|
1607
|
+
center_x: float,
|
|
1608
|
+
center_y: float,
|
|
1609
|
+
major_axis: float,
|
|
1610
|
+
minor_axis: float,
|
|
1611
|
+
) -> AdapterResult[str]:
|
|
1612
|
+
"""Mock adding an axis-aligned ellipse to sketch.
|
|
1613
|
+
|
|
1614
|
+
Args:
|
|
1615
|
+
center_x (float): Ellipse centre X in millimetres.
|
|
1616
|
+
center_y (float): Ellipse centre Y in millimetres.
|
|
1617
|
+
major_axis (float): Full major-axis length in millimetres.
|
|
1618
|
+
minor_axis (float): Full minor-axis length in millimetres.
|
|
1619
|
+
|
|
1620
|
+
Returns:
|
|
1621
|
+
AdapterResult[str]: The result produced by the operation.
|
|
1622
|
+
"""
|
|
1623
|
+
if not self._current_sketch:
|
|
1624
|
+
return AdapterResult(
|
|
1625
|
+
status=AdapterResultStatus.ERROR, error="No active sketch"
|
|
1626
|
+
)
|
|
1627
|
+
|
|
1628
|
+
await asyncio.sleep(self._delays["sketch_operation"] / 2)
|
|
1629
|
+
self._operation_count += 1
|
|
1630
|
+
|
|
1631
|
+
ellipse_id = f"Ellipse_{random.randint(1000, 9999)}"
|
|
1632
|
+
self._sketch_entity_ids.add(ellipse_id)
|
|
1633
|
+
|
|
1634
|
+
return AdapterResult(
|
|
1635
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1636
|
+
data=ellipse_id,
|
|
1637
|
+
execution_time=self._delays["sketch_operation"] / 2,
|
|
1638
|
+
)
|
|
1639
|
+
|
|
1640
|
+
async def add_polygon(
|
|
1641
|
+
self, center_x: float, center_y: float, radius: float, sides: int
|
|
1642
|
+
) -> AdapterResult[str]:
|
|
1643
|
+
"""Mock adding a regular polygon to sketch.
|
|
1644
|
+
|
|
1645
|
+
Args:
|
|
1646
|
+
center_x (float): Polygon centre X in millimetres.
|
|
1647
|
+
center_y (float): Polygon centre Y in millimetres.
|
|
1648
|
+
radius (float): Circumradius in millimetres (distance from centre
|
|
1649
|
+
to each vertex; matches the real adapter's
|
|
1650
|
+
``CreatePolygon(..., Inscribed=True)`` semantics).
|
|
1651
|
+
sides (int): Number of polygon sides.
|
|
1652
|
+
|
|
1653
|
+
Returns:
|
|
1654
|
+
AdapterResult[str]: The result produced by the operation.
|
|
1655
|
+
"""
|
|
1656
|
+
if not self._current_sketch:
|
|
1657
|
+
return AdapterResult(
|
|
1658
|
+
status=AdapterResultStatus.ERROR, error="No active sketch"
|
|
1659
|
+
)
|
|
1660
|
+
|
|
1661
|
+
await asyncio.sleep(self._delays["sketch_operation"] / 2)
|
|
1662
|
+
self._operation_count += 1
|
|
1663
|
+
|
|
1664
|
+
# ID format mirrors the real PyWin32 adapter, which uses
|
|
1665
|
+
# ``_register_sketch_entity("Polygon", ...)`` (counter-based) so the
|
|
1666
|
+
# returned ID is a valid input to downstream sketch_linear_pattern /
|
|
1667
|
+
# sketch_circular_pattern / sketch_mirror / sketch_offset calls.
|
|
1668
|
+
polygon_id = f"Polygon_{len(self._sketch_entity_ids) + 1}"
|
|
1669
|
+
self._sketch_entity_ids.add(polygon_id)
|
|
1670
|
+
_ = sides # informational only; the sides count is no longer part of the ID
|
|
1671
|
+
|
|
1672
|
+
return AdapterResult(
|
|
1673
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1674
|
+
data=polygon_id,
|
|
1675
|
+
execution_time=self._delays["sketch_operation"] / 2,
|
|
1676
|
+
)
|
|
1677
|
+
|
|
1678
|
+
async def sketch_linear_pattern(
|
|
1679
|
+
self,
|
|
1680
|
+
entities: list[str],
|
|
1681
|
+
direction_x: float,
|
|
1682
|
+
direction_y: float,
|
|
1683
|
+
spacing: float,
|
|
1684
|
+
count: int,
|
|
1685
|
+
) -> AdapterResult[str]:
|
|
1686
|
+
"""Mock creating a linear sketch pattern.
|
|
1687
|
+
|
|
1688
|
+
Mirrors the real adapter's validation rules — empty entities,
|
|
1689
|
+
count < 2, non-positive spacing, and a zero direction vector each
|
|
1690
|
+
produce a clear error without "creating" a pattern.
|
|
1691
|
+
|
|
1692
|
+
Args:
|
|
1693
|
+
entities (list[str]): Seed entity IDs.
|
|
1694
|
+
direction_x (float): Pattern direction X component.
|
|
1695
|
+
direction_y (float): Pattern direction Y component.
|
|
1696
|
+
spacing (float): Distance between instances in millimetres.
|
|
1697
|
+
count (int): Total number of instances (including the seed).
|
|
1698
|
+
|
|
1699
|
+
Returns:
|
|
1700
|
+
AdapterResult[str]: The result produced by the operation.
|
|
1701
|
+
"""
|
|
1702
|
+
if not self._current_sketch:
|
|
1703
|
+
return AdapterResult(
|
|
1704
|
+
status=AdapterResultStatus.ERROR, error="No active sketch"
|
|
1705
|
+
)
|
|
1706
|
+
if not entities:
|
|
1707
|
+
return AdapterResult(
|
|
1708
|
+
status=AdapterResultStatus.ERROR,
|
|
1709
|
+
error="sketch_linear_pattern requires at least one entity",
|
|
1710
|
+
)
|
|
1711
|
+
if count < 2:
|
|
1712
|
+
return AdapterResult(
|
|
1713
|
+
status=AdapterResultStatus.ERROR,
|
|
1714
|
+
error="sketch_linear_pattern requires count >= 2",
|
|
1715
|
+
)
|
|
1716
|
+
if spacing <= 0:
|
|
1717
|
+
return AdapterResult(
|
|
1718
|
+
status=AdapterResultStatus.ERROR,
|
|
1719
|
+
error="sketch_linear_pattern requires spacing > 0",
|
|
1720
|
+
)
|
|
1721
|
+
if math.hypot(direction_x, direction_y) < 1e-9:
|
|
1722
|
+
return AdapterResult(
|
|
1723
|
+
status=AdapterResultStatus.ERROR,
|
|
1724
|
+
error=("sketch_linear_pattern requires a non-zero direction vector"),
|
|
1725
|
+
)
|
|
1726
|
+
for ent in entities:
|
|
1727
|
+
if ent not in self._sketch_entity_ids:
|
|
1728
|
+
return AdapterResult(
|
|
1729
|
+
status=AdapterResultStatus.ERROR,
|
|
1730
|
+
error=(
|
|
1731
|
+
f"Unknown sketch entity '{ent}'. Use IDs returned by "
|
|
1732
|
+
"add_line/add_arc/add_circle/add_spline/add_centerline."
|
|
1733
|
+
),
|
|
1734
|
+
)
|
|
1735
|
+
|
|
1736
|
+
await asyncio.sleep(self._delays["sketch_operation"] / 2)
|
|
1737
|
+
self._operation_count += 1
|
|
1738
|
+
|
|
1739
|
+
pattern_id = f"LinearPattern_{count}x{spacing}_{random.randint(1000, 9999)}"
|
|
1740
|
+
return AdapterResult(
|
|
1741
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1742
|
+
data=pattern_id,
|
|
1743
|
+
execution_time=self._delays["sketch_operation"] / 2,
|
|
1744
|
+
)
|
|
1745
|
+
|
|
1746
|
+
async def sketch_circular_pattern(
|
|
1747
|
+
self,
|
|
1748
|
+
entities: list[str],
|
|
1749
|
+
angle: float,
|
|
1750
|
+
count: int,
|
|
1751
|
+
) -> AdapterResult[str]:
|
|
1752
|
+
"""Mock creating a circular sketch pattern around the sketch origin.
|
|
1753
|
+
|
|
1754
|
+
Args:
|
|
1755
|
+
entities (list[str]): Seed entity IDs.
|
|
1756
|
+
angle (float): Total swept angle in degrees.
|
|
1757
|
+
count (int): Total number of instances (including the seed).
|
|
1758
|
+
|
|
1759
|
+
Returns:
|
|
1760
|
+
AdapterResult[str]: The result produced by the operation.
|
|
1761
|
+
"""
|
|
1762
|
+
if not self._current_sketch:
|
|
1763
|
+
return AdapterResult(
|
|
1764
|
+
status=AdapterResultStatus.ERROR, error="No active sketch"
|
|
1765
|
+
)
|
|
1766
|
+
if not entities:
|
|
1767
|
+
return AdapterResult(
|
|
1768
|
+
status=AdapterResultStatus.ERROR,
|
|
1769
|
+
error="sketch_circular_pattern requires at least one entity",
|
|
1770
|
+
)
|
|
1771
|
+
if count < 2:
|
|
1772
|
+
return AdapterResult(
|
|
1773
|
+
status=AdapterResultStatus.ERROR,
|
|
1774
|
+
error="sketch_circular_pattern requires count >= 2",
|
|
1775
|
+
)
|
|
1776
|
+
if angle <= 0:
|
|
1777
|
+
return AdapterResult(
|
|
1778
|
+
status=AdapterResultStatus.ERROR,
|
|
1779
|
+
error="sketch_circular_pattern requires angle > 0",
|
|
1780
|
+
)
|
|
1781
|
+
for ent in entities:
|
|
1782
|
+
if ent not in self._sketch_entity_ids:
|
|
1783
|
+
return AdapterResult(
|
|
1784
|
+
status=AdapterResultStatus.ERROR,
|
|
1785
|
+
error=(
|
|
1786
|
+
f"Unknown sketch entity '{ent}'. Use IDs returned by "
|
|
1787
|
+
"add_line/add_arc/add_circle/add_spline/add_centerline."
|
|
1788
|
+
),
|
|
1789
|
+
)
|
|
1790
|
+
|
|
1791
|
+
await asyncio.sleep(self._delays["sketch_operation"] / 2)
|
|
1792
|
+
self._operation_count += 1
|
|
1793
|
+
|
|
1794
|
+
pattern_id = f"CircularPattern_{count}x{angle}deg_{random.randint(1000, 9999)}"
|
|
1795
|
+
return AdapterResult(
|
|
1796
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1797
|
+
data=pattern_id,
|
|
1798
|
+
execution_time=self._delays["sketch_operation"] / 2,
|
|
1799
|
+
)
|
|
1800
|
+
|
|
1801
|
+
async def sketch_mirror(
|
|
1802
|
+
self, entities: list[str], mirror_line: str
|
|
1803
|
+
) -> AdapterResult[str]:
|
|
1804
|
+
"""Mock mirroring sketch entities about a centerline.
|
|
1805
|
+
|
|
1806
|
+
Args:
|
|
1807
|
+
entities (list[str]): IDs of segments to mirror.
|
|
1808
|
+
mirror_line (str): ID of the centerline to mirror across.
|
|
1809
|
+
|
|
1810
|
+
Returns:
|
|
1811
|
+
AdapterResult[str]: The result produced by the operation.
|
|
1812
|
+
"""
|
|
1813
|
+
if not self._current_sketch:
|
|
1814
|
+
return AdapterResult(
|
|
1815
|
+
status=AdapterResultStatus.ERROR, error="No active sketch"
|
|
1816
|
+
)
|
|
1817
|
+
if not entities:
|
|
1818
|
+
return AdapterResult(
|
|
1819
|
+
status=AdapterResultStatus.ERROR,
|
|
1820
|
+
error="sketch_mirror requires at least one entity",
|
|
1821
|
+
)
|
|
1822
|
+
if not mirror_line:
|
|
1823
|
+
return AdapterResult(
|
|
1824
|
+
status=AdapterResultStatus.ERROR,
|
|
1825
|
+
error=(
|
|
1826
|
+
"sketch_mirror requires a mirror_line entity ID (add_centerline)"
|
|
1827
|
+
),
|
|
1828
|
+
)
|
|
1829
|
+
if mirror_line not in self._sketch_entity_ids:
|
|
1830
|
+
return AdapterResult(
|
|
1831
|
+
status=AdapterResultStatus.ERROR,
|
|
1832
|
+
error=(
|
|
1833
|
+
f"Unknown mirror_line entity '{mirror_line}'. Use the "
|
|
1834
|
+
"ID returned by add_centerline."
|
|
1835
|
+
),
|
|
1836
|
+
)
|
|
1837
|
+
# Mirror the real adapter — IModelDoc2::SketchMirror needs a
|
|
1838
|
+
# centreline as the mirror axis; other segments silently no-op.
|
|
1839
|
+
if not mirror_line.startswith("Centerline_"):
|
|
1840
|
+
return AdapterResult(
|
|
1841
|
+
status=AdapterResultStatus.ERROR,
|
|
1842
|
+
error=(
|
|
1843
|
+
f"mirror_line must be a centerline (from add_centerline), "
|
|
1844
|
+
f"got '{mirror_line}'"
|
|
1845
|
+
),
|
|
1846
|
+
)
|
|
1847
|
+
for ent in entities:
|
|
1848
|
+
if ent not in self._sketch_entity_ids:
|
|
1849
|
+
return AdapterResult(
|
|
1850
|
+
status=AdapterResultStatus.ERROR,
|
|
1851
|
+
error=(
|
|
1852
|
+
f"Unknown sketch entity '{ent}'. Use IDs returned by "
|
|
1853
|
+
"add_line/add_arc/add_circle/add_spline/add_centerline."
|
|
1854
|
+
),
|
|
1855
|
+
)
|
|
1856
|
+
|
|
1857
|
+
await asyncio.sleep(self._delays["sketch_operation"] / 2)
|
|
1858
|
+
self._operation_count += 1
|
|
1859
|
+
|
|
1860
|
+
mirror_id = f"Mirror_{mirror_line}_{random.randint(1000, 9999)}"
|
|
1861
|
+
return AdapterResult(
|
|
1862
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1863
|
+
data=mirror_id,
|
|
1864
|
+
execution_time=self._delays["sketch_operation"] / 2,
|
|
1865
|
+
)
|
|
1866
|
+
|
|
1867
|
+
async def sketch_offset(
|
|
1868
|
+
self,
|
|
1869
|
+
entities: list[str],
|
|
1870
|
+
offset_distance: float,
|
|
1871
|
+
reverse_direction: bool,
|
|
1872
|
+
) -> AdapterResult[str]:
|
|
1873
|
+
"""Mock offsetting sketch entities by a fixed distance.
|
|
1874
|
+
|
|
1875
|
+
Args:
|
|
1876
|
+
entities (list[str]): IDs of segments to offset.
|
|
1877
|
+
offset_distance (float): Distance in millimetres. Must be > 0.
|
|
1878
|
+
reverse_direction (bool): Flip the offset direction.
|
|
1879
|
+
|
|
1880
|
+
Returns:
|
|
1881
|
+
AdapterResult[str]: The result produced by the operation.
|
|
1882
|
+
"""
|
|
1883
|
+
if not self._current_sketch:
|
|
1884
|
+
return AdapterResult(
|
|
1885
|
+
status=AdapterResultStatus.ERROR, error="No active sketch"
|
|
1886
|
+
)
|
|
1887
|
+
if not entities:
|
|
1888
|
+
return AdapterResult(
|
|
1889
|
+
status=AdapterResultStatus.ERROR,
|
|
1890
|
+
error="sketch_offset requires at least one entity",
|
|
1891
|
+
)
|
|
1892
|
+
if offset_distance <= 0:
|
|
1893
|
+
return AdapterResult(
|
|
1894
|
+
status=AdapterResultStatus.ERROR,
|
|
1895
|
+
error=(
|
|
1896
|
+
"sketch_offset requires offset_distance > 0 — use "
|
|
1897
|
+
"reverse_direction to flip the side"
|
|
1898
|
+
),
|
|
1899
|
+
)
|
|
1900
|
+
for ent in entities:
|
|
1901
|
+
if ent not in self._sketch_entity_ids:
|
|
1902
|
+
return AdapterResult(
|
|
1903
|
+
status=AdapterResultStatus.ERROR,
|
|
1904
|
+
error=(
|
|
1905
|
+
f"Unknown sketch entity '{ent}'. Use IDs returned by "
|
|
1906
|
+
"add_line/add_arc/add_circle/add_spline/add_centerline."
|
|
1907
|
+
),
|
|
1908
|
+
)
|
|
1909
|
+
|
|
1910
|
+
await asyncio.sleep(self._delays["sketch_operation"] / 2)
|
|
1911
|
+
self._operation_count += 1
|
|
1912
|
+
|
|
1913
|
+
direction = "inward" if reverse_direction else "outward"
|
|
1914
|
+
offset_id = f"Offset_{offset_distance}_{direction}_{random.randint(1000, 9999)}"
|
|
1915
|
+
return AdapterResult(
|
|
1916
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1917
|
+
data=offset_id,
|
|
1918
|
+
execution_time=self._delays["sketch_operation"] / 2,
|
|
1919
|
+
)
|
|
1920
|
+
|
|
1921
|
+
async def add_sketch_constraint(
|
|
1922
|
+
self,
|
|
1923
|
+
entity1: str,
|
|
1924
|
+
entity2: str | None,
|
|
1925
|
+
relation_type: str,
|
|
1926
|
+
entity3: str | None = None,
|
|
1927
|
+
) -> AdapterResult[str]:
|
|
1928
|
+
"""Mock adding a geometric constraint between sketch entities.
|
|
1929
|
+
|
|
1930
|
+
Args:
|
|
1931
|
+
entity1 (str): Primary sketch-entity ID.
|
|
1932
|
+
entity2 (str | None): Secondary sketch-entity ID, or None for
|
|
1933
|
+
single-entity relations (horizontal, vertical, fix).
|
|
1934
|
+
relation_type (str): Constraint name (see RELATION_NAME_MAP).
|
|
1935
|
+
entity3 (str | None): Third entity ID — only used by the
|
|
1936
|
+
``symmetric`` relation (the centerline of symmetry). All
|
|
1937
|
+
other relation types reject a non-null ``entity3``.
|
|
1938
|
+
|
|
1939
|
+
Returns:
|
|
1940
|
+
AdapterResult[str]: SUCCESS with a placeholder constraint ID, or
|
|
1941
|
+
ERROR if no active sketch, the relation_type is unsupported,
|
|
1942
|
+
arity is wrong, or an entity ID is unknown.
|
|
1943
|
+
"""
|
|
1944
|
+
if not self._current_sketch:
|
|
1945
|
+
return AdapterResult(
|
|
1946
|
+
status=AdapterResultStatus.ERROR, error="No active sketch"
|
|
1947
|
+
)
|
|
1948
|
+
|
|
1949
|
+
rt_norm = (relation_type or "").strip().lower()
|
|
1950
|
+
if rt_norm not in RELATION_NAME_MAP:
|
|
1951
|
+
supported = ", ".join(sorted(RELATION_NAME_MAP))
|
|
1952
|
+
return AdapterResult(
|
|
1953
|
+
status=AdapterResultStatus.ERROR,
|
|
1954
|
+
error=(
|
|
1955
|
+
f"Unsupported relation type '{relation_type}'. "
|
|
1956
|
+
f"Supported: {supported}"
|
|
1957
|
+
),
|
|
1958
|
+
)
|
|
1959
|
+
|
|
1960
|
+
# Arity validation — mirror the real adapter
|
|
1961
|
+
if rt_norm == "symmetric":
|
|
1962
|
+
if entity2 is None or entity3 is None:
|
|
1963
|
+
return AdapterResult(
|
|
1964
|
+
status=AdapterResultStatus.ERROR,
|
|
1965
|
+
error=(
|
|
1966
|
+
f"Relation '{relation_type}' requires entity1, "
|
|
1967
|
+
"entity2, and entity3 (the centerline of symmetry)"
|
|
1968
|
+
),
|
|
1969
|
+
)
|
|
1970
|
+
elif entity3 is not None:
|
|
1971
|
+
return AdapterResult(
|
|
1972
|
+
status=AdapterResultStatus.ERROR,
|
|
1973
|
+
error=(
|
|
1974
|
+
f"Relation '{relation_type}' does not accept entity3 — "
|
|
1975
|
+
"only 'symmetric' takes a third entity (the centerline)"
|
|
1976
|
+
),
|
|
1977
|
+
)
|
|
1978
|
+
|
|
1979
|
+
# Validate entity IDs against the in-process sketch-entity registry
|
|
1980
|
+
# so the mock surfaces the same "Unknown sketch entity" error as the
|
|
1981
|
+
# real adapter (which checks adapter._sketch_entities).
|
|
1982
|
+
for ent in (entity1, entity2, entity3):
|
|
1983
|
+
if ent is not None and ent not in self._sketch_entity_ids:
|
|
1984
|
+
return AdapterResult(
|
|
1985
|
+
status=AdapterResultStatus.ERROR,
|
|
1986
|
+
error=(
|
|
1987
|
+
f"Unknown sketch entity '{ent}'. Use IDs returned by "
|
|
1988
|
+
"add_line/add_arc/add_circle/add_spline/add_centerline."
|
|
1989
|
+
),
|
|
1990
|
+
)
|
|
1991
|
+
|
|
1992
|
+
await asyncio.sleep(self._delays["sketch_operation"] / 2)
|
|
1993
|
+
self._operation_count += 1
|
|
1994
|
+
|
|
1995
|
+
constraint_id = f"Constraint_{relation_type}_{random.randint(1000, 9999)}"
|
|
1996
|
+
|
|
1997
|
+
return AdapterResult(
|
|
1998
|
+
status=AdapterResultStatus.SUCCESS,
|
|
1999
|
+
data=constraint_id,
|
|
2000
|
+
execution_time=self._delays["sketch_operation"] / 2,
|
|
2001
|
+
)
|
|
2002
|
+
|
|
2003
|
+
async def exit_sketch(self) -> AdapterResult[None]:
|
|
2004
|
+
"""Mock exiting sketch mode.
|
|
2005
|
+
|
|
2006
|
+
Returns:
|
|
2007
|
+
AdapterResult[None]: The result produced by the operation.
|
|
2008
|
+
"""
|
|
2009
|
+
if not self._current_sketch:
|
|
2010
|
+
return AdapterResult(
|
|
2011
|
+
status=AdapterResultStatus.WARNING, error="No active sketch to exit"
|
|
2012
|
+
)
|
|
2013
|
+
|
|
2014
|
+
await asyncio.sleep(self._delays["sketch_operation"] / 2)
|
|
2015
|
+
self._current_sketch = None
|
|
2016
|
+
|
|
2017
|
+
return AdapterResult(
|
|
2018
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2019
|
+
data=None,
|
|
2020
|
+
execution_time=self._delays["sketch_operation"] / 2,
|
|
2021
|
+
)
|
|
2022
|
+
|
|
2023
|
+
async def check_sketch_fully_defined(
|
|
2024
|
+
self, sketch_name: str | None = None
|
|
2025
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
2026
|
+
"""Mock check for sketch definition status.
|
|
2027
|
+
|
|
2028
|
+
Args:
|
|
2029
|
+
sketch_name (str | None): Optional sketch name to inspect. Defaults to None.
|
|
2030
|
+
|
|
2031
|
+
Returns:
|
|
2032
|
+
AdapterResult[dict[str, Any]]: Definition status payload.
|
|
2033
|
+
"""
|
|
2034
|
+
if not self._current_model:
|
|
2035
|
+
return AdapterResult(
|
|
2036
|
+
status=AdapterResultStatus.ERROR,
|
|
2037
|
+
error="No active model",
|
|
2038
|
+
)
|
|
2039
|
+
|
|
2040
|
+
await asyncio.sleep(self._delays["sketch_operation"] / 3)
|
|
2041
|
+
self._operation_count += 1
|
|
2042
|
+
|
|
2043
|
+
resolved_name = sketch_name or self._current_sketch or "Sketch1"
|
|
2044
|
+
if sketch_name and sketch_name not in self._sketches:
|
|
2045
|
+
return AdapterResult(
|
|
2046
|
+
status=AdapterResultStatus.ERROR,
|
|
2047
|
+
error=f"Sketch not found: {sketch_name}",
|
|
2048
|
+
)
|
|
2049
|
+
|
|
2050
|
+
return AdapterResult(
|
|
2051
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2052
|
+
data={
|
|
2053
|
+
"sketch_name": resolved_name,
|
|
2054
|
+
"is_fully_defined": True,
|
|
2055
|
+
"definition_state": "fully_defined",
|
|
2056
|
+
"source": "mock",
|
|
2057
|
+
"raw_status": True,
|
|
2058
|
+
},
|
|
2059
|
+
execution_time=self._delays["sketch_operation"] / 3,
|
|
2060
|
+
)
|
|
2061
|
+
|
|
2062
|
+
async def get_mass_properties(self) -> AdapterResult[MassProperties]:
|
|
2063
|
+
"""Mock getting mass properties.
|
|
2064
|
+
|
|
2065
|
+
Returns:
|
|
2066
|
+
AdapterResult[MassProperties]: The result produced by the operation.
|
|
2067
|
+
"""
|
|
2068
|
+
if not self._current_model:
|
|
2069
|
+
return AdapterResult(
|
|
2070
|
+
status=AdapterResultStatus.ERROR, error="No active model"
|
|
2071
|
+
)
|
|
2072
|
+
|
|
2073
|
+
await asyncio.sleep(self._delays["feature_operation"])
|
|
2074
|
+
self._operation_count += 1
|
|
2075
|
+
|
|
2076
|
+
# Generate realistic mock data
|
|
2077
|
+
properties = MassProperties(
|
|
2078
|
+
volume=random.uniform(1000, 100000), # mm³
|
|
2079
|
+
surface_area=random.uniform(1000, 50000), # mm²
|
|
2080
|
+
mass=random.uniform(0.1, 50.0), # kg (assuming typical density)
|
|
2081
|
+
center_of_mass=[
|
|
2082
|
+
random.uniform(-100, 100),
|
|
2083
|
+
random.uniform(-100, 100),
|
|
2084
|
+
random.uniform(-100, 100),
|
|
2085
|
+
],
|
|
2086
|
+
moments_of_inertia={
|
|
2087
|
+
"Ixx": random.uniform(1e6, 1e9),
|
|
2088
|
+
"Iyy": random.uniform(1e6, 1e9),
|
|
2089
|
+
"Izz": random.uniform(1e6, 1e9),
|
|
2090
|
+
"Ixy": random.uniform(-1e8, 1e8),
|
|
2091
|
+
"Ixz": random.uniform(-1e8, 1e8),
|
|
2092
|
+
"Iyz": random.uniform(-1e8, 1e8),
|
|
2093
|
+
},
|
|
2094
|
+
)
|
|
2095
|
+
|
|
2096
|
+
return AdapterResult(
|
|
2097
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2098
|
+
data=properties,
|
|
2099
|
+
execution_time=self._delays["feature_operation"],
|
|
2100
|
+
)
|
|
2101
|
+
|
|
2102
|
+
async def export_image(self, payload: dict) -> AdapterResult[dict]:
|
|
2103
|
+
"""Mock export of a PNG/JPG viewport screenshot.
|
|
2104
|
+
|
|
2105
|
+
Args:
|
|
2106
|
+
payload (dict): The payload value.
|
|
2107
|
+
|
|
2108
|
+
Returns:
|
|
2109
|
+
AdapterResult[dict]: The result produced by the operation.
|
|
2110
|
+
"""
|
|
2111
|
+
if not self._current_model:
|
|
2112
|
+
return AdapterResult(
|
|
2113
|
+
status=AdapterResultStatus.ERROR, error="No active model"
|
|
2114
|
+
)
|
|
2115
|
+
|
|
2116
|
+
import pathlib
|
|
2117
|
+
|
|
2118
|
+
file_path = payload.get("file_path", "")
|
|
2119
|
+
width = int(payload.get("width", 1280))
|
|
2120
|
+
height = int(payload.get("height", 720))
|
|
2121
|
+
orientation = str(payload.get("view_orientation", "current"))
|
|
2122
|
+
|
|
2123
|
+
await asyncio.sleep(0.3)
|
|
2124
|
+
self._operation_count += 1
|
|
2125
|
+
|
|
2126
|
+
out = pathlib.Path(file_path)
|
|
2127
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
2128
|
+
|
|
2129
|
+
# Write a valid 1×1 white PNG binary so UI image tags don't break
|
|
2130
|
+
_PNG_1X1_WHITE = (
|
|
2131
|
+
b"\x89PNG\r\n\x1a\n" # PNG signature
|
|
2132
|
+
b"\x00\x00\x00\rIHDR" # IHDR chunk length+type
|
|
2133
|
+
b"\x00\x00\x00\x01\x00\x00\x00\x01" # 1x1
|
|
2134
|
+
b"\x08\x02\x00\x00\x00\x90wS\xde" # 8-bit RGB + CRC
|
|
2135
|
+
b"\x00\x00\x00\x0cIDATx\x9cc\xf8\xff\xff\xff\x00\x05\xfe\x02\xfe\xdc\xcaY\xe7" # IDAT
|
|
2136
|
+
b"\x00\x00\x00\x00IEND\xaeB`\x82" # IEND
|
|
2137
|
+
)
|
|
2138
|
+
out.write_bytes(_PNG_1X1_WHITE)
|
|
2139
|
+
|
|
2140
|
+
return AdapterResult(
|
|
2141
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2142
|
+
data={
|
|
2143
|
+
"file_path": file_path,
|
|
2144
|
+
"format": "PNG",
|
|
2145
|
+
"dimensions": f"{width}x{height}",
|
|
2146
|
+
"view": orientation,
|
|
2147
|
+
},
|
|
2148
|
+
execution_time=0.3,
|
|
2149
|
+
metadata={"mock": True, "orientation": orientation},
|
|
2150
|
+
)
|
|
2151
|
+
|
|
2152
|
+
async def pack_and_go_assembly(
|
|
2153
|
+
self,
|
|
2154
|
+
source_path: str,
|
|
2155
|
+
target_dir: str,
|
|
2156
|
+
) -> AdapterResult[dict]:
|
|
2157
|
+
"""Mock Pack-and-Go: simulate copying an assembly to a target directory.
|
|
2158
|
+
|
|
2159
|
+
Args:
|
|
2160
|
+
source_path (str): Path to the source .sldasm file.
|
|
2161
|
+
target_dir (str): Directory to copy files into.
|
|
2162
|
+
|
|
2163
|
+
Returns:
|
|
2164
|
+
AdapterResult[dict]: Simulated pack-and-go result.
|
|
2165
|
+
"""
|
|
2166
|
+
import pathlib
|
|
2167
|
+
|
|
2168
|
+
await asyncio.sleep(0.05)
|
|
2169
|
+
self._operation_count += 1
|
|
2170
|
+
|
|
2171
|
+
source = pathlib.Path(source_path)
|
|
2172
|
+
out_dir = pathlib.Path(target_dir)
|
|
2173
|
+
mock_parts = [f"MockPart{i}.SLDPRT" for i in range(1, 3)]
|
|
2174
|
+
source_files = [str(source)] + [str(source.parent / p) for p in mock_parts]
|
|
2175
|
+
copied_files = [str(out_dir / source.name)] + [
|
|
2176
|
+
str(out_dir / p) for p in mock_parts
|
|
2177
|
+
]
|
|
2178
|
+
return AdapterResult(
|
|
2179
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2180
|
+
data={
|
|
2181
|
+
"source_assembly": str(source),
|
|
2182
|
+
"target_dir": str(out_dir),
|
|
2183
|
+
"copied_files": copied_files,
|
|
2184
|
+
"source_files": source_files,
|
|
2185
|
+
"save_statuses": [0] * (len(mock_parts) + 1),
|
|
2186
|
+
"all_files_saved": True,
|
|
2187
|
+
},
|
|
2188
|
+
execution_time=0.05,
|
|
2189
|
+
metadata={"mock": True},
|
|
2190
|
+
)
|
|
2191
|
+
|
|
2192
|
+
async def export_file(
|
|
2193
|
+
self, file_path: str, format_type: str
|
|
2194
|
+
) -> AdapterResult[None]:
|
|
2195
|
+
"""Mock exporting a file.
|
|
2196
|
+
|
|
2197
|
+
Args:
|
|
2198
|
+
file_path (str): Path to the target file.
|
|
2199
|
+
format_type (str): The format type value.
|
|
2200
|
+
|
|
2201
|
+
Returns:
|
|
2202
|
+
AdapterResult[None]: The result produced by the operation.
|
|
2203
|
+
"""
|
|
2204
|
+
if not self._current_model:
|
|
2205
|
+
return AdapterResult(
|
|
2206
|
+
status=AdapterResultStatus.ERROR, error="No active model"
|
|
2207
|
+
)
|
|
2208
|
+
|
|
2209
|
+
# Simulate export time based on format
|
|
2210
|
+
export_times = {
|
|
2211
|
+
"step": 2.0,
|
|
2212
|
+
"iges": 1.5,
|
|
2213
|
+
"stl": 1.0,
|
|
2214
|
+
"pdf": 0.5,
|
|
2215
|
+
"jpg": 0.3,
|
|
2216
|
+
"glb": 1.2,
|
|
2217
|
+
"gltf": 1.2,
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
format_lower = format_type.lower()
|
|
2221
|
+
delay = export_times.get(format_lower, 1.0)
|
|
2222
|
+
|
|
2223
|
+
await asyncio.sleep(delay)
|
|
2224
|
+
self._operation_count += 1
|
|
2225
|
+
|
|
2226
|
+
# Write a minimal placeholder file so callers that check existence get a valid result
|
|
2227
|
+
if format_lower == "stl":
|
|
2228
|
+
import pathlib
|
|
2229
|
+
|
|
2230
|
+
out = pathlib.Path(file_path)
|
|
2231
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
2232
|
+
# Minimal ASCII STL — a single unit triangle (mock geometry placeholder)
|
|
2233
|
+
out.write_text(
|
|
2234
|
+
"solid mock\n"
|
|
2235
|
+
" facet normal 0 0 1\n"
|
|
2236
|
+
" outer loop\n"
|
|
2237
|
+
" vertex 0 0 0\n"
|
|
2238
|
+
" vertex 10 0 0\n"
|
|
2239
|
+
" vertex 5 10 0\n"
|
|
2240
|
+
" endloop\n"
|
|
2241
|
+
" endfacet\n"
|
|
2242
|
+
" facet normal 0 0 1\n"
|
|
2243
|
+
" outer loop\n"
|
|
2244
|
+
" vertex 0 0 0\n"
|
|
2245
|
+
" vertex 10 0 0\n"
|
|
2246
|
+
" vertex 5 0 10\n"
|
|
2247
|
+
" endloop\n"
|
|
2248
|
+
" endfacet\n"
|
|
2249
|
+
"endsolid mock\n",
|
|
2250
|
+
encoding="utf-8",
|
|
2251
|
+
)
|
|
2252
|
+
|
|
2253
|
+
return AdapterResult(
|
|
2254
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2255
|
+
data=None,
|
|
2256
|
+
execution_time=delay,
|
|
2257
|
+
metadata={"exported_format": format_type, "file_path": file_path},
|
|
2258
|
+
)
|
|
2259
|
+
|
|
2260
|
+
async def get_dimension(self, name: str) -> AdapterResult[float]:
|
|
2261
|
+
"""Mock getting a dimension value.
|
|
2262
|
+
|
|
2263
|
+
Args:
|
|
2264
|
+
name (str): The name value.
|
|
2265
|
+
|
|
2266
|
+
Returns:
|
|
2267
|
+
AdapterResult[float]: The result produced by the operation.
|
|
2268
|
+
"""
|
|
2269
|
+
await asyncio.sleep(0.05) # Very fast operation
|
|
2270
|
+
self._operation_count += 1
|
|
2271
|
+
|
|
2272
|
+
if name in self._dimensions:
|
|
2273
|
+
return AdapterResult(
|
|
2274
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2275
|
+
data=self._dimensions[name],
|
|
2276
|
+
execution_time=0.05,
|
|
2277
|
+
)
|
|
2278
|
+
else:
|
|
2279
|
+
return AdapterResult(
|
|
2280
|
+
status=AdapterResultStatus.ERROR, error=f"Dimension '{name}' not found"
|
|
2281
|
+
)
|
|
2282
|
+
|
|
2283
|
+
async def set_dimension(self, name: str, value: float) -> AdapterResult[None]:
|
|
2284
|
+
"""Mock setting a dimension value.
|
|
2285
|
+
|
|
2286
|
+
Args:
|
|
2287
|
+
name (str): The name value.
|
|
2288
|
+
value (float): The value value.
|
|
2289
|
+
|
|
2290
|
+
Returns:
|
|
2291
|
+
AdapterResult[None]: The result produced by the operation.
|
|
2292
|
+
"""
|
|
2293
|
+
await asyncio.sleep(0.1) # Fast operation
|
|
2294
|
+
self._operation_count += 1
|
|
2295
|
+
|
|
2296
|
+
self._dimensions[name] = value
|
|
2297
|
+
|
|
2298
|
+
return AdapterResult(
|
|
2299
|
+
status=AdapterResultStatus.SUCCESS, data=None, execution_time=0.1
|
|
2300
|
+
)
|
|
2301
|
+
|
|
2302
|
+
async def set_units(self, unit_system: str) -> AdapterResult[dict[str, Any]]:
|
|
2303
|
+
"""Mock setting the active document's linear unit system.
|
|
2304
|
+
|
|
2305
|
+
Mirrors the real adapter: an unrecognised token is an error, and the
|
|
2306
|
+
applied token is recorded on the current model's ``properties``.
|
|
2307
|
+
|
|
2308
|
+
Args:
|
|
2309
|
+
unit_system (str): One of ``mm``, ``cm``, ``m``, ``in``, ``ft``
|
|
2310
|
+
(aliases such as ``inch`` / ``millimeters`` accepted).
|
|
2311
|
+
|
|
2312
|
+
Returns:
|
|
2313
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
2314
|
+
"""
|
|
2315
|
+
await asyncio.sleep(0.1)
|
|
2316
|
+
|
|
2317
|
+
token = _normalise_mock_unit_system(unit_system)
|
|
2318
|
+
if token is None:
|
|
2319
|
+
return AdapterResult(
|
|
2320
|
+
status=AdapterResultStatus.ERROR,
|
|
2321
|
+
error=(
|
|
2322
|
+
f"Unrecognised unit system {unit_system!r}. "
|
|
2323
|
+
"Expected one of: mm, cm, m, in, ft."
|
|
2324
|
+
),
|
|
2325
|
+
)
|
|
2326
|
+
if self._current_model is None:
|
|
2327
|
+
return AdapterResult(
|
|
2328
|
+
status=AdapterResultStatus.ERROR, error="No active model"
|
|
2329
|
+
)
|
|
2330
|
+
|
|
2331
|
+
self._operation_count += 1
|
|
2332
|
+
props = dict(self._current_model.properties or {})
|
|
2333
|
+
props["units"] = token
|
|
2334
|
+
self._current_model.properties = props
|
|
2335
|
+
|
|
2336
|
+
return AdapterResult(
|
|
2337
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2338
|
+
data={"unit_system": token, "verified": True},
|
|
2339
|
+
execution_time=0.1,
|
|
2340
|
+
)
|
|
2341
|
+
|
|
2342
|
+
async def list_open_documents(self) -> AdapterResult[list[dict[str, Any]]]:
|
|
2343
|
+
"""Mock enumerating the currently open documents.
|
|
2344
|
+
|
|
2345
|
+
Returns every model the mock has created or opened this session, with
|
|
2346
|
+
``is_active`` reflecting ``self._current_model``.
|
|
2347
|
+
|
|
2348
|
+
Returns:
|
|
2349
|
+
AdapterResult[list[dict[str, Any]]]: The result produced by the
|
|
2350
|
+
operation.
|
|
2351
|
+
"""
|
|
2352
|
+
await asyncio.sleep(0.05)
|
|
2353
|
+
if not self._connected:
|
|
2354
|
+
return AdapterResult(
|
|
2355
|
+
status=AdapterResultStatus.ERROR, error="Not connected to SolidWorks"
|
|
2356
|
+
)
|
|
2357
|
+
|
|
2358
|
+
active_path = (
|
|
2359
|
+
self._current_model.path if self._current_model is not None else None
|
|
2360
|
+
)
|
|
2361
|
+
docs = [
|
|
2362
|
+
{
|
|
2363
|
+
"title": model.name,
|
|
2364
|
+
"path": model.path,
|
|
2365
|
+
"type": model.type,
|
|
2366
|
+
"is_active": model.path == active_path,
|
|
2367
|
+
}
|
|
2368
|
+
for model in self._models.values()
|
|
2369
|
+
]
|
|
2370
|
+
return AdapterResult(
|
|
2371
|
+
status=AdapterResultStatus.SUCCESS, data=docs, execution_time=0.05
|
|
2372
|
+
)
|
|
2373
|
+
|
|
2374
|
+
async def activate_document(
|
|
2375
|
+
self, title_or_path: str
|
|
2376
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
2377
|
+
"""Mock making an already-open document active.
|
|
2378
|
+
|
|
2379
|
+
Args:
|
|
2380
|
+
title_or_path (str): Title, full path, or file name of an open
|
|
2381
|
+
document.
|
|
2382
|
+
|
|
2383
|
+
Returns:
|
|
2384
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
2385
|
+
"""
|
|
2386
|
+
await asyncio.sleep(0.05)
|
|
2387
|
+
if not self._connected:
|
|
2388
|
+
return AdapterResult(
|
|
2389
|
+
status=AdapterResultStatus.ERROR, error="Not connected to SolidWorks"
|
|
2390
|
+
)
|
|
2391
|
+
target = (title_or_path or "").strip()
|
|
2392
|
+
if not target:
|
|
2393
|
+
return AdapterResult(
|
|
2394
|
+
status=AdapterResultStatus.ERROR, error="title_or_path is required"
|
|
2395
|
+
)
|
|
2396
|
+
|
|
2397
|
+
needle = target.lower()
|
|
2398
|
+
match = None
|
|
2399
|
+
for model in self._models.values():
|
|
2400
|
+
names = {
|
|
2401
|
+
model.name.lower(),
|
|
2402
|
+
model.path.lower(),
|
|
2403
|
+
model.path.rsplit("/", 1)[-1].lower(),
|
|
2404
|
+
}
|
|
2405
|
+
if needle in names:
|
|
2406
|
+
match = model
|
|
2407
|
+
break
|
|
2408
|
+
if match is None:
|
|
2409
|
+
open_titles = ", ".join(m.name for m in self._models.values()) or "none"
|
|
2410
|
+
return AdapterResult(
|
|
2411
|
+
status=AdapterResultStatus.ERROR,
|
|
2412
|
+
error=f"No open document matches {target!r}. Open: {open_titles}",
|
|
2413
|
+
)
|
|
2414
|
+
|
|
2415
|
+
self._current_model = match
|
|
2416
|
+
self._operation_count += 1
|
|
2417
|
+
return AdapterResult(
|
|
2418
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2419
|
+
data={"activated": match.name, "verified": True},
|
|
2420
|
+
execution_time=0.05,
|
|
2421
|
+
)
|
|
2422
|
+
|
|
2423
|
+
async def insert_component(
|
|
2424
|
+
self, file_path: str, x: float = 0.0, y: float = 0.0, z: float = 0.0
|
|
2425
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
2426
|
+
"""Mock inserting a component into the active assembly.
|
|
2427
|
+
|
|
2428
|
+
Records the component in ``self._components`` so repeated inserts
|
|
2429
|
+
accumulate and ``list_components`` reflects them.
|
|
2430
|
+
|
|
2431
|
+
Args:
|
|
2432
|
+
file_path (str): Path to the part or sub-assembly to insert.
|
|
2433
|
+
x (float): X position in millimetres. Defaults to 0.0.
|
|
2434
|
+
y (float): Y position in millimetres. Defaults to 0.0.
|
|
2435
|
+
z (float): Z position in millimetres. Defaults to 0.0.
|
|
2436
|
+
|
|
2437
|
+
Returns:
|
|
2438
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
2439
|
+
"""
|
|
2440
|
+
await asyncio.sleep(self._delays["model_operation"])
|
|
2441
|
+
self._operation_count += 1
|
|
2442
|
+
|
|
2443
|
+
self._components.append(f"component-{len(self._components) + 1}")
|
|
2444
|
+
|
|
2445
|
+
return AdapterResult(
|
|
2446
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2447
|
+
data={
|
|
2448
|
+
"component": self._components[-1],
|
|
2449
|
+
"file_path": file_path,
|
|
2450
|
+
"position": {"x": x, "y": y, "z": z},
|
|
2451
|
+
"components_before": len(self._components) - 1,
|
|
2452
|
+
"components_after": len(self._components),
|
|
2453
|
+
},
|
|
2454
|
+
execution_time=self._delays["model_operation"],
|
|
2455
|
+
)
|
|
2456
|
+
|
|
2457
|
+
async def list_components(self) -> AdapterResult[list[str]]:
|
|
2458
|
+
"""Mock listing the top-level components of the active assembly.
|
|
2459
|
+
|
|
2460
|
+
Returns:
|
|
2461
|
+
AdapterResult[list[str]]: The result produced by the operation.
|
|
2462
|
+
"""
|
|
2463
|
+
await asyncio.sleep(self._delays["model_operation"] / 2)
|
|
2464
|
+
self._operation_count += 1
|
|
2465
|
+
|
|
2466
|
+
return AdapterResult(
|
|
2467
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2468
|
+
data=list(self._components),
|
|
2469
|
+
execution_time=self._delays["model_operation"] / 2,
|
|
2470
|
+
)
|
|
2471
|
+
|
|
2472
|
+
async def get_model_geometry_summary(self) -> AdapterResult[dict[str, Any]]:
|
|
2473
|
+
"""Mock summary of the active part or assembly.
|
|
2474
|
+
|
|
2475
|
+
The mock has no geometry, so volume, bounding box and body count are
|
|
2476
|
+
``None`` ("not determinable") rather than invented numbers a caller
|
|
2477
|
+
could mistake for a measurement. Component and mate counts are real:
|
|
2478
|
+
they come from ``insert_component`` and ``add_mate`` this session.
|
|
2479
|
+
|
|
2480
|
+
Returns:
|
|
2481
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
2482
|
+
"""
|
|
2483
|
+
guard = self._mock_doc_error({"Part", "Assembly"}, "get_model_geometry_summary")
|
|
2484
|
+
if guard is not None:
|
|
2485
|
+
return guard
|
|
2486
|
+
await asyncio.sleep(self._delays["model_operation"] / 2)
|
|
2487
|
+
self._operation_count += 1
|
|
2488
|
+
|
|
2489
|
+
is_assembly = self._current_model.type == "Assembly"
|
|
2490
|
+
summary: dict[str, Any] = {
|
|
2491
|
+
"document_type": "assembly" if is_assembly else "part",
|
|
2492
|
+
"volume_mm3": None,
|
|
2493
|
+
"bounding_box_mm": None,
|
|
2494
|
+
"bounding_box_method": None,
|
|
2495
|
+
"solid_body_count": None,
|
|
2496
|
+
}
|
|
2497
|
+
if is_assembly:
|
|
2498
|
+
summary["component_count"] = len(self._components)
|
|
2499
|
+
summary["mate_count"] = len(self._mates)
|
|
2500
|
+
return AdapterResult(
|
|
2501
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2502
|
+
data=summary,
|
|
2503
|
+
execution_time=self._delays["model_operation"] / 2,
|
|
2504
|
+
)
|
|
2505
|
+
|
|
2506
|
+
# Drawing Operations
|
|
2507
|
+
def _mock_place_view(self, payload: Any) -> AdapterResult[dict[str, Any]]:
|
|
2508
|
+
"""Record a drawing view so ``list_drawing_views`` reflects it.
|
|
2509
|
+
|
|
2510
|
+
Args:
|
|
2511
|
+
payload (Any): Tool payload, as the real adapter receives it.
|
|
2512
|
+
|
|
2513
|
+
Returns:
|
|
2514
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
2515
|
+
"""
|
|
2516
|
+
data = payload if isinstance(payload, dict) else {}
|
|
2517
|
+
model_path = data.get("model_path") or data.get("model_file")
|
|
2518
|
+
if not model_path:
|
|
2519
|
+
return AdapterResult(
|
|
2520
|
+
status=AdapterResultStatus.ERROR,
|
|
2521
|
+
error="A model path is required (model_path or model_file)",
|
|
2522
|
+
)
|
|
2523
|
+
|
|
2524
|
+
orientation = str(data.get("orientation") or data.get("view_type") or "front")
|
|
2525
|
+
if orientation.strip().lower() not in _MOCK_NAMED_VIEWS:
|
|
2526
|
+
return AdapterResult(
|
|
2527
|
+
status=AdapterResultStatus.ERROR,
|
|
2528
|
+
error=(
|
|
2529
|
+
f"Unknown orientation '{orientation}'. Use one of: "
|
|
2530
|
+
f"{', '.join(sorted(_MOCK_NAMED_VIEWS))}."
|
|
2531
|
+
),
|
|
2532
|
+
)
|
|
2533
|
+
|
|
2534
|
+
before = len(self._drawing_views)
|
|
2535
|
+
self._drawing_views.append(f"Drawing View{before + 1}")
|
|
2536
|
+
self._operation_count += 1
|
|
2537
|
+
|
|
2538
|
+
return AdapterResult(
|
|
2539
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2540
|
+
data={
|
|
2541
|
+
"name": self._drawing_views[-1],
|
|
2542
|
+
"model_path": model_path,
|
|
2543
|
+
"orientation": orientation,
|
|
2544
|
+
"position": {
|
|
2545
|
+
"x": float(data.get("position_x", 100.0)),
|
|
2546
|
+
"y": float(data.get("position_y", 150.0)),
|
|
2547
|
+
},
|
|
2548
|
+
"views_before": before,
|
|
2549
|
+
"views_after": len(self._drawing_views),
|
|
2550
|
+
},
|
|
2551
|
+
execution_time=self._delays["model_operation"],
|
|
2552
|
+
)
|
|
2553
|
+
|
|
2554
|
+
async def create_drawing_view(
|
|
2555
|
+
self, payload: Any = None
|
|
2556
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
2557
|
+
"""Mock placing a view of a model on the active drawing sheet.
|
|
2558
|
+
|
|
2559
|
+
Args:
|
|
2560
|
+
payload (Any): Tool payload carrying the model path and orientation.
|
|
2561
|
+
|
|
2562
|
+
Returns:
|
|
2563
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
2564
|
+
"""
|
|
2565
|
+
await asyncio.sleep(self._delays["model_operation"])
|
|
2566
|
+
return self._mock_place_view(payload)
|
|
2567
|
+
|
|
2568
|
+
async def add_drawing_view(
|
|
2569
|
+
self, payload: Any = None
|
|
2570
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
2571
|
+
"""Mock adding a view of a model to the active drawing sheet.
|
|
2572
|
+
|
|
2573
|
+
Args:
|
|
2574
|
+
payload (Any): Tool payload carrying the model path and orientation.
|
|
2575
|
+
|
|
2576
|
+
Returns:
|
|
2577
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
2578
|
+
"""
|
|
2579
|
+
await asyncio.sleep(self._delays["model_operation"])
|
|
2580
|
+
return self._mock_place_view(payload)
|
|
2581
|
+
|
|
2582
|
+
async def create_technical_drawing(
|
|
2583
|
+
self, payload: Any = None
|
|
2584
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
2585
|
+
"""Mock laying out the three standard views of a model.
|
|
2586
|
+
|
|
2587
|
+
Args:
|
|
2588
|
+
payload (Any): Tool payload carrying the model path and projection.
|
|
2589
|
+
|
|
2590
|
+
Returns:
|
|
2591
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
2592
|
+
"""
|
|
2593
|
+
await asyncio.sleep(self._delays["model_operation"])
|
|
2594
|
+
data = payload if isinstance(payload, dict) else {}
|
|
2595
|
+
model_path = data.get("model_path") or data.get("model_file")
|
|
2596
|
+
if not model_path:
|
|
2597
|
+
return AdapterResult(
|
|
2598
|
+
status=AdapterResultStatus.ERROR,
|
|
2599
|
+
error="A model path is required (model_path or model_file)",
|
|
2600
|
+
)
|
|
2601
|
+
|
|
2602
|
+
third_angle = bool(data.get("third_angle", True))
|
|
2603
|
+
if str(data.get("projection", "")).lower() in {"first", "first_angle"}:
|
|
2604
|
+
third_angle = False
|
|
2605
|
+
|
|
2606
|
+
before = len(self._drawing_views)
|
|
2607
|
+
added = [f"Drawing View{before + offset}" for offset in (1, 2, 3)]
|
|
2608
|
+
self._drawing_views.extend(added)
|
|
2609
|
+
self._operation_count += 1
|
|
2610
|
+
|
|
2611
|
+
return AdapterResult(
|
|
2612
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2613
|
+
data={
|
|
2614
|
+
"views": added,
|
|
2615
|
+
"model_path": model_path,
|
|
2616
|
+
"projection": "third_angle" if third_angle else "first_angle",
|
|
2617
|
+
"views_before": before,
|
|
2618
|
+
"views_after": len(self._drawing_views),
|
|
2619
|
+
},
|
|
2620
|
+
execution_time=self._delays["model_operation"],
|
|
2621
|
+
)
|
|
2622
|
+
|
|
2623
|
+
async def add_note(self, payload: Any = None) -> AdapterResult[dict[str, Any]]:
|
|
2624
|
+
"""Mock placing a text note on the active drawing sheet.
|
|
2625
|
+
|
|
2626
|
+
Args:
|
|
2627
|
+
payload (Any): Tool payload carrying the text and position.
|
|
2628
|
+
|
|
2629
|
+
Returns:
|
|
2630
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
2631
|
+
"""
|
|
2632
|
+
await asyncio.sleep(self._delays["model_operation"])
|
|
2633
|
+
data = payload if isinstance(payload, dict) else {}
|
|
2634
|
+
text = data.get("text")
|
|
2635
|
+
if not text:
|
|
2636
|
+
return AdapterResult(
|
|
2637
|
+
status=AdapterResultStatus.ERROR, error="add_note requires text"
|
|
2638
|
+
)
|
|
2639
|
+
|
|
2640
|
+
self._operation_count += 1
|
|
2641
|
+
font_points = float(data.get("font_size") or 0.0)
|
|
2642
|
+
return AdapterResult(
|
|
2643
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2644
|
+
data={
|
|
2645
|
+
"text": text,
|
|
2646
|
+
"position": {
|
|
2647
|
+
"x": float(data.get("position_x", 100.0)),
|
|
2648
|
+
"y": float(data.get("position_y", 50.0)),
|
|
2649
|
+
},
|
|
2650
|
+
# The real adapter sets this from IAnnotation::SetPosition's
|
|
2651
|
+
# return. Mock mode has no sheet to place anything on, so it
|
|
2652
|
+
# reports "not determinable" rather than claiming success at a
|
|
2653
|
+
# check it never performed.
|
|
2654
|
+
"positioned": None,
|
|
2655
|
+
"font_size_points": font_points or None,
|
|
2656
|
+
},
|
|
2657
|
+
execution_time=self._delays["model_operation"],
|
|
2658
|
+
)
|
|
2659
|
+
|
|
2660
|
+
async def list_drawing_views(self) -> AdapterResult[list[str]]:
|
|
2661
|
+
"""Mock listing the views on the active drawing.
|
|
2662
|
+
|
|
2663
|
+
Returns:
|
|
2664
|
+
AdapterResult[list[str]]: The result produced by the operation.
|
|
2665
|
+
"""
|
|
2666
|
+
await asyncio.sleep(self._delays["model_operation"] / 2)
|
|
2667
|
+
self._operation_count += 1
|
|
2668
|
+
|
|
2669
|
+
return AdapterResult(
|
|
2670
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2671
|
+
data=list(self._drawing_views),
|
|
2672
|
+
execution_time=self._delays["model_operation"] / 2,
|
|
2673
|
+
)
|
|
2674
|
+
|
|
2675
|
+
async def auto_center_marks(
|
|
2676
|
+
self,
|
|
2677
|
+
view_name: str,
|
|
2678
|
+
mark_holes: bool = True,
|
|
2679
|
+
mark_fillets: bool = False,
|
|
2680
|
+
mark_slots: bool = True,
|
|
2681
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
2682
|
+
"""Mock auto-inserting centre marks on a drawing view.
|
|
2683
|
+
|
|
2684
|
+
The mock has no geometry, so it cannot know how many circular
|
|
2685
|
+
features a view holds - it mirrors the live adapter's refusals (not
|
|
2686
|
+
a drawing, no feature type selected, unknown view) and, on a valid
|
|
2687
|
+
request, records a nominal centre-mark increase per view so
|
|
2688
|
+
before/after deltas stay monotonic across calls.
|
|
2689
|
+
|
|
2690
|
+
Args:
|
|
2691
|
+
view_name (str): Name of a view on the active drawing.
|
|
2692
|
+
mark_holes (bool): Mark holes / bores.
|
|
2693
|
+
mark_fillets (bool): Mark fillets.
|
|
2694
|
+
mark_slots (bool): Mark slots.
|
|
2695
|
+
|
|
2696
|
+
Returns:
|
|
2697
|
+
AdapterResult[dict[str, Any]]: The view, the feature types acted
|
|
2698
|
+
on, and before/after centre-mark counts, or an error.
|
|
2699
|
+
"""
|
|
2700
|
+
await asyncio.sleep(self._delays["model_operation"] / 2)
|
|
2701
|
+
|
|
2702
|
+
model = self._current_model
|
|
2703
|
+
if model is None or getattr(model, "type", None) != "Drawing":
|
|
2704
|
+
return AdapterResult(
|
|
2705
|
+
status=AdapterResultStatus.ERROR,
|
|
2706
|
+
error="auto_center_marks requires an active drawing document",
|
|
2707
|
+
)
|
|
2708
|
+
if not (mark_holes or mark_fillets or mark_slots):
|
|
2709
|
+
return AdapterResult(
|
|
2710
|
+
status=AdapterResultStatus.ERROR,
|
|
2711
|
+
error=(
|
|
2712
|
+
"Select at least one feature type: mark_holes, "
|
|
2713
|
+
"mark_fillets or mark_slots."
|
|
2714
|
+
),
|
|
2715
|
+
)
|
|
2716
|
+
if view_name not in self._drawing_views:
|
|
2717
|
+
return AdapterResult(
|
|
2718
|
+
status=AdapterResultStatus.ERROR,
|
|
2719
|
+
error=(
|
|
2720
|
+
f"No view named {view_name!r} on the active drawing. "
|
|
2721
|
+
f"Views: {', '.join(self._drawing_views) or 'none'}"
|
|
2722
|
+
),
|
|
2723
|
+
)
|
|
2724
|
+
|
|
2725
|
+
self._operation_count += 1
|
|
2726
|
+
before = self._center_marks.get(view_name, 0)
|
|
2727
|
+
# Nominal: two marks per selected feature type, added once.
|
|
2728
|
+
added = 0 if before else 2 * sum((mark_holes, mark_fillets, mark_slots))
|
|
2729
|
+
after = before + added
|
|
2730
|
+
self._center_marks[view_name] = after
|
|
2731
|
+
|
|
2732
|
+
return AdapterResult(
|
|
2733
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2734
|
+
data={
|
|
2735
|
+
"view": view_name,
|
|
2736
|
+
"feature_types": [
|
|
2737
|
+
label
|
|
2738
|
+
for label, on in (
|
|
2739
|
+
("holes", mark_holes),
|
|
2740
|
+
("fillets", mark_fillets),
|
|
2741
|
+
("slots", mark_slots),
|
|
2742
|
+
)
|
|
2743
|
+
if on
|
|
2744
|
+
],
|
|
2745
|
+
"center_marks_before": before,
|
|
2746
|
+
"center_marks_after": after,
|
|
2747
|
+
"center_marks_added": added,
|
|
2748
|
+
},
|
|
2749
|
+
execution_time=self._delays["model_operation"] / 2,
|
|
2750
|
+
)
|
|
2751
|
+
|
|
2752
|
+
async def save_body_as_part(
|
|
2753
|
+
self, body_name: str, file_path: str
|
|
2754
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
2755
|
+
"""Mock extracting a solid body from the active part to a new file.
|
|
2756
|
+
|
|
2757
|
+
The mock has no bodies, so it cannot validate ``body_name`` against
|
|
2758
|
+
real geometry - it mirrors the live adapter's other refusals (no
|
|
2759
|
+
model, not a part, blank arguments, missing parent directory) and, on
|
|
2760
|
+
a valid request, writes an empty placeholder file so callers and
|
|
2761
|
+
tests can observe the side effect.
|
|
2762
|
+
|
|
2763
|
+
Args:
|
|
2764
|
+
body_name (str): Name of a solid body in the active part.
|
|
2765
|
+
file_path (str): Absolute path for the new ``.sldprt``.
|
|
2766
|
+
|
|
2767
|
+
Returns:
|
|
2768
|
+
AdapterResult[dict[str, Any]]: The body, the written path, and a
|
|
2769
|
+
single-element body list, or an error.
|
|
2770
|
+
"""
|
|
2771
|
+
import os as _os
|
|
2772
|
+
|
|
2773
|
+
await asyncio.sleep(self._delays["model_operation"])
|
|
2774
|
+
|
|
2775
|
+
model = self._current_model
|
|
2776
|
+
if model is None:
|
|
2777
|
+
return AdapterResult(
|
|
2778
|
+
status=AdapterResultStatus.ERROR, error="No active model"
|
|
2779
|
+
)
|
|
2780
|
+
if getattr(model, "type", None) != "Part":
|
|
2781
|
+
return AdapterResult(
|
|
2782
|
+
status=AdapterResultStatus.ERROR,
|
|
2783
|
+
error="save_body_as_part requires an active part document",
|
|
2784
|
+
)
|
|
2785
|
+
if not str(body_name or "").strip():
|
|
2786
|
+
return AdapterResult(
|
|
2787
|
+
status=AdapterResultStatus.ERROR, error="body_name is required"
|
|
2788
|
+
)
|
|
2789
|
+
target = str(file_path or "").strip()
|
|
2790
|
+
if not target:
|
|
2791
|
+
return AdapterResult(
|
|
2792
|
+
status=AdapterResultStatus.ERROR, error="file_path is required"
|
|
2793
|
+
)
|
|
2794
|
+
target = _os.path.abspath(target)
|
|
2795
|
+
parent = _os.path.dirname(target)
|
|
2796
|
+
if not _os.path.isdir(parent):
|
|
2797
|
+
return AdapterResult(
|
|
2798
|
+
status=AdapterResultStatus.ERROR,
|
|
2799
|
+
error=f"Parent directory does not exist: {parent}",
|
|
2800
|
+
)
|
|
2801
|
+
|
|
2802
|
+
with open(target, "w", encoding="utf-8"):
|
|
2803
|
+
pass
|
|
2804
|
+
self._operation_count += 1
|
|
2805
|
+
|
|
2806
|
+
return AdapterResult(
|
|
2807
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2808
|
+
data={
|
|
2809
|
+
"body": body_name,
|
|
2810
|
+
"file_path": target,
|
|
2811
|
+
"feature": f"Save Bodies{self._operation_count}",
|
|
2812
|
+
"solid_bodies": [body_name],
|
|
2813
|
+
},
|
|
2814
|
+
execution_time=self._delays["model_operation"],
|
|
2815
|
+
)
|
|
2816
|
+
|
|
2817
|
+
async def check_interference(
|
|
2818
|
+
self, params: Any = None
|
|
2819
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
2820
|
+
"""Mock checking the active assembly for interfering components.
|
|
2821
|
+
|
|
2822
|
+
Args:
|
|
2823
|
+
params (Any): Optional settings; ``coincident``, ``components``.
|
|
2824
|
+
|
|
2825
|
+
Returns:
|
|
2826
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
2827
|
+
"""
|
|
2828
|
+
await asyncio.sleep(self._delays["model_operation"])
|
|
2829
|
+
options = params if isinstance(params, dict) else {}
|
|
2830
|
+
|
|
2831
|
+
if len(self._components) < 2:
|
|
2832
|
+
return AdapterResult(
|
|
2833
|
+
status=AdapterResultStatus.ERROR,
|
|
2834
|
+
error=(
|
|
2835
|
+
"Interference detection needs at least two components; the "
|
|
2836
|
+
f"assembly has {len(self._components)}."
|
|
2837
|
+
),
|
|
2838
|
+
)
|
|
2839
|
+
|
|
2840
|
+
self._operation_count += 1
|
|
2841
|
+
return AdapterResult(
|
|
2842
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2843
|
+
data={
|
|
2844
|
+
# The real adapter gets this from SolidWorks' interference
|
|
2845
|
+
# engine. The mock has no geometry, so it cannot know whether
|
|
2846
|
+
# anything overlaps and says so rather than reporting a clean
|
|
2847
|
+
# assembly it never checked - a mock that always answered
|
|
2848
|
+
# False would make this tool useless wherever mock mode runs.
|
|
2849
|
+
"interference_found": None,
|
|
2850
|
+
"interference_count": None,
|
|
2851
|
+
"interferences": [],
|
|
2852
|
+
"coincident_treated_as_interference": bool(
|
|
2853
|
+
options.get("coincident", False)
|
|
2854
|
+
),
|
|
2855
|
+
"scope": "whole assembly",
|
|
2856
|
+
"tolerance_applied": None,
|
|
2857
|
+
},
|
|
2858
|
+
execution_time=self._delays["model_operation"],
|
|
2859
|
+
)
|
|
2860
|
+
|
|
2861
|
+
# Feature Editing
|
|
2862
|
+
async def delete_feature(self, name: str) -> AdapterResult[dict[str, Any]]:
|
|
2863
|
+
"""Mock deleting a named feature from the active model.
|
|
2864
|
+
|
|
2865
|
+
Args:
|
|
2866
|
+
name (str): Feature or sketch name.
|
|
2867
|
+
|
|
2868
|
+
Returns:
|
|
2869
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
2870
|
+
"""
|
|
2871
|
+
await asyncio.sleep(self._delays["model_operation"])
|
|
2872
|
+
|
|
2873
|
+
# _features is keyed by an internal id, so resolve by feature name.
|
|
2874
|
+
before = [f.name for f in self._features.values()]
|
|
2875
|
+
key = next((k for k, f in self._features.items() if f.name == name), None)
|
|
2876
|
+
if key is None:
|
|
2877
|
+
return AdapterResult(
|
|
2878
|
+
status=AdapterResultStatus.ERROR,
|
|
2879
|
+
error=(
|
|
2880
|
+
f"Feature not found: {name}. "
|
|
2881
|
+
f"Available: {', '.join(before) or 'none'}"
|
|
2882
|
+
),
|
|
2883
|
+
)
|
|
2884
|
+
|
|
2885
|
+
del self._features[key]
|
|
2886
|
+
self._suppressed_features.discard(name)
|
|
2887
|
+
self._operation_count += 1
|
|
2888
|
+
after = [f.name for f in self._features.values()]
|
|
2889
|
+
|
|
2890
|
+
return AdapterResult(
|
|
2891
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2892
|
+
data={
|
|
2893
|
+
"deleted": name,
|
|
2894
|
+
"removed_features": [n for n in before if n not in after],
|
|
2895
|
+
"features_before": len(before),
|
|
2896
|
+
"features_after": len(after),
|
|
2897
|
+
},
|
|
2898
|
+
execution_time=self._delays["model_operation"],
|
|
2899
|
+
)
|
|
2900
|
+
|
|
2901
|
+
async def suppress_feature(
|
|
2902
|
+
self, name: str, suppress: bool = True
|
|
2903
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
2904
|
+
"""Mock suppressing or unsuppressing a named feature.
|
|
2905
|
+
|
|
2906
|
+
Args:
|
|
2907
|
+
name (str): Feature name.
|
|
2908
|
+
suppress (bool): True to suppress, False to unsuppress.
|
|
2909
|
+
|
|
2910
|
+
Returns:
|
|
2911
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
2912
|
+
"""
|
|
2913
|
+
await asyncio.sleep(self._delays["model_operation"])
|
|
2914
|
+
|
|
2915
|
+
known = any(f.name == name for f in self._features.values())
|
|
2916
|
+
if not known:
|
|
2917
|
+
return AdapterResult(
|
|
2918
|
+
status=AdapterResultStatus.ERROR, error=f"Feature not found: {name}"
|
|
2919
|
+
)
|
|
2920
|
+
|
|
2921
|
+
was = name in self._suppressed_features
|
|
2922
|
+
if suppress:
|
|
2923
|
+
self._suppressed_features.add(name)
|
|
2924
|
+
else:
|
|
2925
|
+
self._suppressed_features.discard(name)
|
|
2926
|
+
self._operation_count += 1
|
|
2927
|
+
|
|
2928
|
+
return AdapterResult(
|
|
2929
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2930
|
+
data={
|
|
2931
|
+
"feature": name,
|
|
2932
|
+
"requested": suppress,
|
|
2933
|
+
"suppressed": bool(suppress),
|
|
2934
|
+
"was_suppressed": was,
|
|
2935
|
+
},
|
|
2936
|
+
execution_time=self._delays["model_operation"],
|
|
2937
|
+
)
|
|
2938
|
+
|
|
2939
|
+
async def rename_feature(
|
|
2940
|
+
self, old_name: str, new_name: str
|
|
2941
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
2942
|
+
"""Mock renaming a named feature in the active model.
|
|
2943
|
+
|
|
2944
|
+
Mirrors the real adapter: an empty target name, an unknown feature, or
|
|
2945
|
+
a target name already in use are all errors; renaming to the current
|
|
2946
|
+
name is a no-op success.
|
|
2947
|
+
|
|
2948
|
+
Args:
|
|
2949
|
+
old_name (str): Current feature name.
|
|
2950
|
+
new_name (str): New feature name.
|
|
2951
|
+
|
|
2952
|
+
Returns:
|
|
2953
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
2954
|
+
"""
|
|
2955
|
+
await asyncio.sleep(self._delays["model_operation"])
|
|
2956
|
+
|
|
2957
|
+
new = new_name.split("@", 1)[0].strip()
|
|
2958
|
+
if not new:
|
|
2959
|
+
return AdapterResult(
|
|
2960
|
+
status=AdapterResultStatus.ERROR, error="new_name must not be empty"
|
|
2961
|
+
)
|
|
2962
|
+
|
|
2963
|
+
key = next((k for k, f in self._features.items() if f.name == old_name), None)
|
|
2964
|
+
if key is None:
|
|
2965
|
+
return AdapterResult(
|
|
2966
|
+
status=AdapterResultStatus.ERROR,
|
|
2967
|
+
error=f"Feature not found: {old_name}",
|
|
2968
|
+
)
|
|
2969
|
+
if new == old_name:
|
|
2970
|
+
return AdapterResult(
|
|
2971
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2972
|
+
data={
|
|
2973
|
+
"renamed": False,
|
|
2974
|
+
"old_name": old_name,
|
|
2975
|
+
"new_name": new,
|
|
2976
|
+
"reason": "old and new names are the same",
|
|
2977
|
+
},
|
|
2978
|
+
execution_time=self._delays["model_operation"],
|
|
2979
|
+
)
|
|
2980
|
+
if any(f.name == new for f in self._features.values()):
|
|
2981
|
+
return AdapterResult(
|
|
2982
|
+
status=AdapterResultStatus.ERROR,
|
|
2983
|
+
error=(
|
|
2984
|
+
f"Cannot rename to '{new}': a feature with that name already exists"
|
|
2985
|
+
),
|
|
2986
|
+
)
|
|
2987
|
+
|
|
2988
|
+
self._features[key].name = new
|
|
2989
|
+
if old_name in self._suppressed_features:
|
|
2990
|
+
self._suppressed_features.discard(old_name)
|
|
2991
|
+
self._suppressed_features.add(new)
|
|
2992
|
+
self._operation_count += 1
|
|
2993
|
+
|
|
2994
|
+
return AdapterResult(
|
|
2995
|
+
status=AdapterResultStatus.SUCCESS,
|
|
2996
|
+
data={"renamed": True, "old_name": old_name, "new_name": new},
|
|
2997
|
+
execution_time=self._delays["model_operation"],
|
|
2998
|
+
)
|
|
2999
|
+
|
|
3000
|
+
async def undo(self, count: int = 1) -> AdapterResult[dict[str, Any]]:
|
|
3001
|
+
"""Mock undoing the last operations in the active model.
|
|
3002
|
+
|
|
3003
|
+
Removes the most recently added features, mirroring what an undo of
|
|
3004
|
+
feature-creation steps does to the tree. ``tree_changed`` is ``False``
|
|
3005
|
+
when there was nothing left to undo, which is what the real adapter
|
|
3006
|
+
reports too - SolidWorks accepts an undo on an empty stack quietly.
|
|
3007
|
+
|
|
3008
|
+
Args:
|
|
3009
|
+
count (int): Number of steps to undo.
|
|
3010
|
+
|
|
3011
|
+
Returns:
|
|
3012
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
3013
|
+
"""
|
|
3014
|
+
await asyncio.sleep(self._delays["model_operation"])
|
|
3015
|
+
steps = max(1, int(count))
|
|
3016
|
+
|
|
3017
|
+
keys = list(self._features)
|
|
3018
|
+
before = [self._features[k].name for k in keys]
|
|
3019
|
+
doomed = keys[-steps:] if steps <= len(keys) else list(keys)
|
|
3020
|
+
removed = [self._features[k].name for k in doomed]
|
|
3021
|
+
for key in doomed:
|
|
3022
|
+
self._suppressed_features.discard(self._features[key].name)
|
|
3023
|
+
del self._features[key]
|
|
3024
|
+
self._operation_count += 1
|
|
3025
|
+
after = [f.name for f in self._features.values()]
|
|
3026
|
+
|
|
3027
|
+
return AdapterResult(
|
|
3028
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3029
|
+
data={
|
|
3030
|
+
"requested_steps": steps,
|
|
3031
|
+
"features_before": len(before),
|
|
3032
|
+
"features_after": len(after),
|
|
3033
|
+
"removed_features": removed,
|
|
3034
|
+
"tree_changed": before != after,
|
|
3035
|
+
},
|
|
3036
|
+
execution_time=self._delays["model_operation"],
|
|
3037
|
+
)
|
|
3038
|
+
|
|
3039
|
+
async def add_mate(
|
|
3040
|
+
self,
|
|
3041
|
+
component_a: str,
|
|
3042
|
+
component_b: str,
|
|
3043
|
+
entity_a: str = "Front Plane",
|
|
3044
|
+
entity_b: str = "Front Plane",
|
|
3045
|
+
mate_type: str = "coincident",
|
|
3046
|
+
alignment: str = "aligned",
|
|
3047
|
+
distance: float = 0.0,
|
|
3048
|
+
angle: float = 0.0,
|
|
3049
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3050
|
+
"""Mock mating two components in the active assembly.
|
|
3051
|
+
|
|
3052
|
+
Args:
|
|
3053
|
+
component_a (str): First component instance name.
|
|
3054
|
+
component_b (str): Second component instance name.
|
|
3055
|
+
entity_a (str): Named feature on the first component. Defaults to
|
|
3056
|
+
"Front Plane".
|
|
3057
|
+
entity_b (str): Named feature on the second component. Defaults to
|
|
3058
|
+
"Front Plane".
|
|
3059
|
+
mate_type (str): Mate type. Defaults to "coincident".
|
|
3060
|
+
alignment (str): Mate alignment. Defaults to "aligned".
|
|
3061
|
+
distance (float): Distance in millimetres, for a distance mate.
|
|
3062
|
+
Defaults to 0.0.
|
|
3063
|
+
angle (float): Angle in degrees, for an angle mate. Defaults to 0.0.
|
|
3064
|
+
|
|
3065
|
+
Returns:
|
|
3066
|
+
AdapterResult[dict[str, Any]]: The result produced by the operation.
|
|
3067
|
+
"""
|
|
3068
|
+
known_mate_types = {
|
|
3069
|
+
"coincident",
|
|
3070
|
+
"concentric",
|
|
3071
|
+
"perpendicular",
|
|
3072
|
+
"parallel",
|
|
3073
|
+
"tangent",
|
|
3074
|
+
"distance",
|
|
3075
|
+
"angle",
|
|
3076
|
+
}
|
|
3077
|
+
if mate_type not in known_mate_types:
|
|
3078
|
+
return AdapterResult(
|
|
3079
|
+
status=AdapterResultStatus.ERROR,
|
|
3080
|
+
error=(
|
|
3081
|
+
f"Unknown mate type '{mate_type}'. "
|
|
3082
|
+
f"Use one of: {', '.join(sorted(known_mate_types))}."
|
|
3083
|
+
),
|
|
3084
|
+
)
|
|
3085
|
+
|
|
3086
|
+
await asyncio.sleep(self._delays["model_operation"])
|
|
3087
|
+
self._operation_count += 1
|
|
3088
|
+
self._mates.append(
|
|
3089
|
+
{"mate_type": mate_type, "components": [component_a, component_b]}
|
|
3090
|
+
)
|
|
3091
|
+
|
|
3092
|
+
return AdapterResult(
|
|
3093
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3094
|
+
data={
|
|
3095
|
+
"mate_type": mate_type,
|
|
3096
|
+
"alignment": alignment,
|
|
3097
|
+
"components": [component_a, component_b],
|
|
3098
|
+
"entities": [entity_a, entity_b],
|
|
3099
|
+
"distance": distance or None,
|
|
3100
|
+
"angle": angle or None,
|
|
3101
|
+
# The real adapter fills these by comparing every component's
|
|
3102
|
+
# Transform2 before and after the mate. The mock has no
|
|
3103
|
+
# geometry to move, so it reports "not determinable" rather
|
|
3104
|
+
# than True: this field exists precisely so a caller can tell
|
|
3105
|
+
# a mate that positioned something from one SolidWorks
|
|
3106
|
+
# accepted and ignored, and a mock that always answers True
|
|
3107
|
+
# would make that check useless wherever mock mode is used.
|
|
3108
|
+
"moved_components": [],
|
|
3109
|
+
"geometry_moved": None,
|
|
3110
|
+
},
|
|
3111
|
+
execution_time=self._delays["model_operation"],
|
|
3112
|
+
)
|
|
3113
|
+
|
|
3114
|
+
async def create_reference_plane(
|
|
3115
|
+
self,
|
|
3116
|
+
reference: str,
|
|
3117
|
+
offset: float = 0.0,
|
|
3118
|
+
angle: float = 0.0,
|
|
3119
|
+
flip: bool = False,
|
|
3120
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3121
|
+
"""Mock creating a reference plane offset from an existing plane.
|
|
3122
|
+
|
|
3123
|
+
Mirrors the live adapter's refusals rather than fabricating a
|
|
3124
|
+
result it cannot know: an angled plane needs a second reference this
|
|
3125
|
+
signature cannot supply, and an offset of zero would be coincident
|
|
3126
|
+
with its reference. See ``_create_reference_plane_impl`` in
|
|
3127
|
+
``solidworks/features.py`` for the live contract this mirrors.
|
|
3128
|
+
|
|
3129
|
+
On success, invents a sequential ``PlaneN`` name tracked on
|
|
3130
|
+
``self._reference_planes`` and advances the shared mock feature-tree
|
|
3131
|
+
counter so ``features_before``/``features_after`` differ by exactly
|
|
3132
|
+
one, the same signal the live adapter derives from
|
|
3133
|
+
``IFeatureManager::GetFeatureCount``.
|
|
3134
|
+
|
|
3135
|
+
Args:
|
|
3136
|
+
reference (str): Name of the reference plane or planar face.
|
|
3137
|
+
offset (float): Offset distance in millimetres. Defaults to 0.0.
|
|
3138
|
+
angle (float): Angle in degrees; unsupported (non-zero errors).
|
|
3139
|
+
Defaults to 0.0.
|
|
3140
|
+
flip (bool): Reverse the offset direction. Defaults to False.
|
|
3141
|
+
|
|
3142
|
+
Returns:
|
|
3143
|
+
AdapterResult[dict[str, Any]]: The new plane's name and
|
|
3144
|
+
parameters, or error.
|
|
3145
|
+
"""
|
|
3146
|
+
if angle:
|
|
3147
|
+
return AdapterResult(
|
|
3148
|
+
status=AdapterResultStatus.ERROR,
|
|
3149
|
+
error=(
|
|
3150
|
+
"create_reference_plane does not support 'angle' yet: an "
|
|
3151
|
+
"angled plane needs a second reference (an axis or edge "
|
|
3152
|
+
"to rotate about) which this signature cannot take. Use "
|
|
3153
|
+
"'offset' for parallel planes."
|
|
3154
|
+
),
|
|
3155
|
+
)
|
|
3156
|
+
|
|
3157
|
+
if not offset:
|
|
3158
|
+
return AdapterResult(
|
|
3159
|
+
status=AdapterResultStatus.ERROR,
|
|
3160
|
+
error=(
|
|
3161
|
+
"create_reference_plane requires a non-zero offset - a "
|
|
3162
|
+
"plane coincident with its reference is not useful"
|
|
3163
|
+
),
|
|
3164
|
+
)
|
|
3165
|
+
|
|
3166
|
+
await asyncio.sleep(self._delays["feature_operation"])
|
|
3167
|
+
self._operation_count += 1
|
|
3168
|
+
|
|
3169
|
+
before = self._feature_tree_count
|
|
3170
|
+
self._feature_tree_count += 1
|
|
3171
|
+
after = self._feature_tree_count
|
|
3172
|
+
|
|
3173
|
+
self._reference_planes.append(f"Plane{len(self._reference_planes) + 1}")
|
|
3174
|
+
|
|
3175
|
+
return AdapterResult(
|
|
3176
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3177
|
+
data={
|
|
3178
|
+
"name": self._reference_planes[-1],
|
|
3179
|
+
"reference": reference,
|
|
3180
|
+
"offset": offset,
|
|
3181
|
+
"angle": None,
|
|
3182
|
+
"flip": flip,
|
|
3183
|
+
"features_before": before,
|
|
3184
|
+
"features_after": after,
|
|
3185
|
+
},
|
|
3186
|
+
execution_time=self._delays["feature_operation"],
|
|
3187
|
+
)
|
|
3188
|
+
|
|
3189
|
+
async def create_axis(self, reference: str) -> AdapterResult[dict[str, Any]]:
|
|
3190
|
+
"""Mock creating a reference axis along a principal direction.
|
|
3191
|
+
|
|
3192
|
+
Mirrors the live adapter's plane-pair mapping (``_AXIS_PLANE_PAIRS``
|
|
3193
|
+
in ``solidworks/features.py``, imported here directly so the two can
|
|
3194
|
+
never drift apart) and validation: only ``x``/``y``/``z``
|
|
3195
|
+
(case-insensitive, leading +/- stripped) are accepted.
|
|
3196
|
+
|
|
3197
|
+
Args:
|
|
3198
|
+
reference (str): "x", "y" or "z" (case-insensitive; leading +/-
|
|
3199
|
+
stripped).
|
|
3200
|
+
|
|
3201
|
+
Returns:
|
|
3202
|
+
AdapterResult[dict[str, Any]]: The plane pair used and the
|
|
3203
|
+
feature counter before/after, or error for an unknown reference.
|
|
3204
|
+
"""
|
|
3205
|
+
key = str(reference or "").strip().lower().lstrip("+-")
|
|
3206
|
+
if key not in _AXIS_PLANE_PAIRS:
|
|
3207
|
+
return AdapterResult(
|
|
3208
|
+
status=AdapterResultStatus.ERROR,
|
|
3209
|
+
error=(
|
|
3210
|
+
f"Unknown axis reference '{reference}'. "
|
|
3211
|
+
f"Use one of: {', '.join(sorted(_AXIS_PLANE_PAIRS))}."
|
|
3212
|
+
),
|
|
3213
|
+
)
|
|
3214
|
+
|
|
3215
|
+
await asyncio.sleep(self._delays["feature_operation"])
|
|
3216
|
+
self._operation_count += 1
|
|
3217
|
+
|
|
3218
|
+
before = self._feature_tree_count
|
|
3219
|
+
self._feature_tree_count += 1
|
|
3220
|
+
after = self._feature_tree_count
|
|
3221
|
+
|
|
3222
|
+
plane_a, plane_b = _AXIS_PLANE_PAIRS[key]
|
|
3223
|
+
|
|
3224
|
+
# Invent a sequential "AxisN" name, tracked on self._axes, the same
|
|
3225
|
+
# way create_reference_plane invents "PlaneN" names on
|
|
3226
|
+
# self._reference_planes. InsertAxis2 returns only a bool, so the
|
|
3227
|
+
# live adapter cannot report a name either - but pattern_circular
|
|
3228
|
+
# needs some name to validate the axis argument against, and this
|
|
3229
|
+
# keeps the mock's bookkeeping consistent with the plane convention.
|
|
3230
|
+
self._axes.append(f"Axis{len(self._axes) + 1}")
|
|
3231
|
+
|
|
3232
|
+
return AdapterResult(
|
|
3233
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3234
|
+
data={
|
|
3235
|
+
"name": self._axes[-1],
|
|
3236
|
+
"reference": key,
|
|
3237
|
+
"planes": [plane_a, plane_b],
|
|
3238
|
+
"features_before": before,
|
|
3239
|
+
"features_after": after,
|
|
3240
|
+
},
|
|
3241
|
+
execution_time=self._delays["feature_operation"],
|
|
3242
|
+
)
|
|
3243
|
+
|
|
3244
|
+
async def create_reference_point(
|
|
3245
|
+
self,
|
|
3246
|
+
mode: str,
|
|
3247
|
+
x: float,
|
|
3248
|
+
y: float,
|
|
3249
|
+
z: float,
|
|
3250
|
+
distance: float | None = None,
|
|
3251
|
+
percent: float | None = None,
|
|
3252
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3253
|
+
"""Mock creating a reference point on the active part.
|
|
3254
|
+
|
|
3255
|
+
The mock has no geometry, so it cannot resolve a coordinate to an
|
|
3256
|
+
edge/face - it mirrors the live adapter's *validation* (unknown mode,
|
|
3257
|
+
the distance/percent XOR, range checks) and, on a valid request,
|
|
3258
|
+
bumps the feature counter the way ``InsertReferencePoint`` would.
|
|
3259
|
+
|
|
3260
|
+
Args:
|
|
3261
|
+
mode (str): "along_curve" or "face_center".
|
|
3262
|
+
x (float): X of a point on the target entity, in millimetres.
|
|
3263
|
+
y (float): Y of a point on the target entity, in millimetres.
|
|
3264
|
+
z (float): Z of a point on the target entity, in millimetres.
|
|
3265
|
+
distance (float | None): For "along_curve", offset from the edge
|
|
3266
|
+
start in millimetres.
|
|
3267
|
+
percent (float | None): For "along_curve", 0-100 of edge length.
|
|
3268
|
+
|
|
3269
|
+
Returns:
|
|
3270
|
+
AdapterResult[dict[str, Any]]: The mode and feature counts, or an
|
|
3271
|
+
error matching the live adapter's refusals.
|
|
3272
|
+
"""
|
|
3273
|
+
key = str(mode or "").strip().lower()
|
|
3274
|
+
if key not in ("along_curve", "face_center"):
|
|
3275
|
+
return AdapterResult(
|
|
3276
|
+
status=AdapterResultStatus.ERROR,
|
|
3277
|
+
error=(f"Unknown mode {mode!r}. Use 'along_curve' or 'face_center'."),
|
|
3278
|
+
)
|
|
3279
|
+
if key == "along_curve":
|
|
3280
|
+
if (distance is None) == (percent is None):
|
|
3281
|
+
return AdapterResult(
|
|
3282
|
+
status=AdapterResultStatus.ERROR,
|
|
3283
|
+
error="along_curve needs exactly one of distance or percent",
|
|
3284
|
+
)
|
|
3285
|
+
if distance is not None and float(distance) <= 0.0:
|
|
3286
|
+
return AdapterResult(
|
|
3287
|
+
status=AdapterResultStatus.ERROR,
|
|
3288
|
+
error="distance must be positive",
|
|
3289
|
+
)
|
|
3290
|
+
if percent is not None and not 0.0 < float(percent) < 100.0:
|
|
3291
|
+
return AdapterResult(
|
|
3292
|
+
status=AdapterResultStatus.ERROR,
|
|
3293
|
+
error="percent must be between 0 and 100 (exclusive)",
|
|
3294
|
+
)
|
|
3295
|
+
|
|
3296
|
+
await asyncio.sleep(self._delays["feature_operation"])
|
|
3297
|
+
self._operation_count += 1
|
|
3298
|
+
before = self._feature_tree_count
|
|
3299
|
+
self._feature_tree_count += 1
|
|
3300
|
+
self._reference_points.append(f"Point{len(self._reference_points) + 1}")
|
|
3301
|
+
|
|
3302
|
+
return AdapterResult(
|
|
3303
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3304
|
+
data={
|
|
3305
|
+
"name": self._reference_points[-1],
|
|
3306
|
+
"mode": key,
|
|
3307
|
+
"at_mm": [x, y, z],
|
|
3308
|
+
"distance_mm": distance if key == "along_curve" else None,
|
|
3309
|
+
"percent": percent if key == "along_curve" else None,
|
|
3310
|
+
"features_before": before,
|
|
3311
|
+
"features_after": self._feature_tree_count,
|
|
3312
|
+
},
|
|
3313
|
+
execution_time=self._delays["feature_operation"],
|
|
3314
|
+
)
|
|
3315
|
+
|
|
3316
|
+
#: Built-in planes every SolidWorks part starts with. Combined with
|
|
3317
|
+
#: ``self._reference_planes`` (planes created via create_reference_plane)
|
|
3318
|
+
#: this is the set of plane names ``mirror_feature`` will accept.
|
|
3319
|
+
_BUILTIN_PLANES = frozenset({"Front Plane", "Top Plane", "Right Plane"})
|
|
3320
|
+
|
|
3321
|
+
async def mirror_feature(
|
|
3322
|
+
self,
|
|
3323
|
+
features: list[str],
|
|
3324
|
+
mirror_plane: str,
|
|
3325
|
+
merge: bool = True,
|
|
3326
|
+
mirror_bodies: bool = True,
|
|
3327
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3328
|
+
"""Mock mirroring solid bodies or features about a plane.
|
|
3329
|
+
|
|
3330
|
+
The mock has no geometry engine, so it cannot measure a real volume
|
|
3331
|
+
change - it mirrors the live adapter's refusals instead of
|
|
3332
|
+
fabricating a result it cannot know. An empty source list or plane
|
|
3333
|
+
name is rejected outright (matching ``_mirror_feature_impl`` in
|
|
3334
|
+
``solidworks/features.py`` verbatim), and a source or plane name
|
|
3335
|
+
that was never created in this session is rejected the same way
|
|
3336
|
+
``_select_named_feature`` / ``_select_reference_entity`` would fail
|
|
3337
|
+
to select it there.
|
|
3338
|
+
|
|
3339
|
+
On success, the tracked ``self._mirror_volume`` figure (seeded at
|
|
3340
|
+
the live-measured 1819569.1 mm^3 wing volume) is doubled, matching
|
|
3341
|
+
the ~2.0 ratio a correct mirror produces (measured live: 1.998528
|
|
3342
|
+
for a lofted wing, exactly 2.0 for a boss whose sketch sits on the
|
|
3343
|
+
plane).
|
|
3344
|
+
|
|
3345
|
+
Args:
|
|
3346
|
+
features (list[str]): Body or feature names to mirror.
|
|
3347
|
+
mirror_plane (str): Plane name to mirror about.
|
|
3348
|
+
merge (bool): Merge the mirrored result with the original. Defaults to True.
|
|
3349
|
+
mirror_bodies (bool): Mirror whole bodies rather than features. Defaults to True.
|
|
3350
|
+
|
|
3351
|
+
Returns:
|
|
3352
|
+
AdapterResult[dict[str, Any]]: The mirror feature's name, inputs
|
|
3353
|
+
and volume before/after, or error.
|
|
3354
|
+
"""
|
|
3355
|
+
names = [n for n in (features or []) if n]
|
|
3356
|
+
if not names:
|
|
3357
|
+
return AdapterResult(
|
|
3358
|
+
status=AdapterResultStatus.ERROR,
|
|
3359
|
+
error="mirror_feature requires at least one body or feature name",
|
|
3360
|
+
)
|
|
3361
|
+
if not mirror_plane:
|
|
3362
|
+
return AdapterResult(
|
|
3363
|
+
status=AdapterResultStatus.ERROR,
|
|
3364
|
+
error="mirror_feature requires a mirror plane name",
|
|
3365
|
+
)
|
|
3366
|
+
|
|
3367
|
+
known_sources = {feature.name for feature in self._features.values()} | set(
|
|
3368
|
+
self._components
|
|
3369
|
+
)
|
|
3370
|
+
for name in names:
|
|
3371
|
+
if name not in known_sources:
|
|
3372
|
+
kind = "body" if mirror_bodies else "feature"
|
|
3373
|
+
return AdapterResult(
|
|
3374
|
+
status=AdapterResultStatus.ERROR,
|
|
3375
|
+
error=f"Failed to select {kind} to mirror: {name}",
|
|
3376
|
+
)
|
|
3377
|
+
|
|
3378
|
+
known_planes = self._BUILTIN_PLANES | set(self._reference_planes)
|
|
3379
|
+
if mirror_plane not in known_planes:
|
|
3380
|
+
return AdapterResult(
|
|
3381
|
+
status=AdapterResultStatus.ERROR,
|
|
3382
|
+
error=f"Failed to select mirror plane: {mirror_plane}",
|
|
3383
|
+
)
|
|
3384
|
+
|
|
3385
|
+
await asyncio.sleep(self._delays["feature_operation"])
|
|
3386
|
+
self._operation_count += 1
|
|
3387
|
+
|
|
3388
|
+
volume_before = self._mirror_volume
|
|
3389
|
+
volume_after = volume_before * 2
|
|
3390
|
+
self._mirror_volume = volume_after
|
|
3391
|
+
|
|
3392
|
+
self._feature_tree_count += 1
|
|
3393
|
+
feature_name = f"Mirror{self._feature_tree_count}"
|
|
3394
|
+
|
|
3395
|
+
return AdapterResult(
|
|
3396
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3397
|
+
data={
|
|
3398
|
+
"name": feature_name,
|
|
3399
|
+
"mirrored": names,
|
|
3400
|
+
"mirror_plane": mirror_plane,
|
|
3401
|
+
"merge": bool(merge),
|
|
3402
|
+
"mirror_bodies": bool(mirror_bodies),
|
|
3403
|
+
"volume_before": volume_before,
|
|
3404
|
+
"volume_after": volume_after,
|
|
3405
|
+
"volume_ratio": round(volume_after / volume_before, 6),
|
|
3406
|
+
},
|
|
3407
|
+
execution_time=self._delays["feature_operation"],
|
|
3408
|
+
)
|
|
3409
|
+
|
|
3410
|
+
#: Live-measured "volume added per extra circular-pattern instance", in
|
|
3411
|
+
#: mm^3: a hub+blade part measured 48431.5 mm3 for hub + 1 blade, and
|
|
3412
|
+
#: 75916.4 mm3 once patterned to 4 blades (3 extra instances) - see
|
|
3413
|
+
#: ``_pattern_circular_impl`` in ``solidworks/features.py``. Dividing the
|
|
3414
|
+
#: growth by the 3 extra instances gives the per-instance figure used
|
|
3415
|
+
#: below to scale ``self._pattern_volume`` by roughly ``count`` without a
|
|
3416
|
+
#: real geometry engine.
|
|
3417
|
+
_PATTERN_BLADE_VOLUME = (75916.4 - 48431.5) / 3
|
|
3418
|
+
|
|
3419
|
+
async def pattern_circular(
|
|
3420
|
+
self,
|
|
3421
|
+
features: list[str],
|
|
3422
|
+
axis: str,
|
|
3423
|
+
count: int,
|
|
3424
|
+
angle: float = 360.0,
|
|
3425
|
+
equal_spacing: bool = True,
|
|
3426
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3427
|
+
"""Mock patterning features around an axis.
|
|
3428
|
+
|
|
3429
|
+
The mock has no geometry engine, so it cannot measure a real volume
|
|
3430
|
+
change - it mirrors the live adapter's refusals instead of
|
|
3431
|
+
fabricating a result it cannot know (matching
|
|
3432
|
+
``_pattern_circular_impl`` in ``solidworks/features.py`` verbatim
|
|
3433
|
+
for the three argument checks below). An axis name that was never
|
|
3434
|
+
created via ``create_axis`` in this session is rejected the same
|
|
3435
|
+
way ``SelectByID2`` would fail to select it there.
|
|
3436
|
+
|
|
3437
|
+
On success, the tracked ``self._pattern_volume`` figure (seeded at
|
|
3438
|
+
the live-measured 48431.5 mm^3 hub+blade volume) grows by
|
|
3439
|
+
``_PATTERN_BLADE_VOLUME * (count - 1)``, the live-derived
|
|
3440
|
+
per-instance volume, so volume_before/volume_after/volume_ratio
|
|
3441
|
+
stay self-consistent and scale with ``count`` roughly the way
|
|
3442
|
+
adding more blades to the hub would - reproducing the live
|
|
3443
|
+
48431.5 -> 75916.4 mm^3 measurement exactly for ``count=4``.
|
|
3444
|
+
|
|
3445
|
+
Args:
|
|
3446
|
+
features (list[str]): Names of the features to pattern.
|
|
3447
|
+
axis (str): Name of the axis to rotate about.
|
|
3448
|
+
count (int): Total number of instances including the original.
|
|
3449
|
+
angle (float): Total angle in degrees to spread them over. Defaults to 360.0.
|
|
3450
|
+
equal_spacing (bool): Space instances evenly across ``angle``. Defaults to True.
|
|
3451
|
+
|
|
3452
|
+
Returns:
|
|
3453
|
+
AdapterResult[dict[str, Any]]: The pattern's name, inputs and
|
|
3454
|
+
volume before/after, or error.
|
|
3455
|
+
"""
|
|
3456
|
+
names = [n for n in (features or []) if n]
|
|
3457
|
+
if not names:
|
|
3458
|
+
return AdapterResult(
|
|
3459
|
+
status=AdapterResultStatus.ERROR,
|
|
3460
|
+
error="pattern_circular requires at least one feature name",
|
|
3461
|
+
)
|
|
3462
|
+
if not axis:
|
|
3463
|
+
return AdapterResult(
|
|
3464
|
+
status=AdapterResultStatus.ERROR,
|
|
3465
|
+
error="pattern_circular requires an axis name to rotate about",
|
|
3466
|
+
)
|
|
3467
|
+
if int(count) < 2:
|
|
3468
|
+
return AdapterResult(
|
|
3469
|
+
status=AdapterResultStatus.ERROR,
|
|
3470
|
+
error=(
|
|
3471
|
+
f"pattern_circular needs a count of at least 2 (got {count}); "
|
|
3472
|
+
"a pattern of one is just the original feature"
|
|
3473
|
+
),
|
|
3474
|
+
)
|
|
3475
|
+
|
|
3476
|
+
known_axes = set(self._axes)
|
|
3477
|
+
if axis not in known_axes:
|
|
3478
|
+
return AdapterResult(
|
|
3479
|
+
status=AdapterResultStatus.ERROR,
|
|
3480
|
+
error=(
|
|
3481
|
+
f"Failed to select axis '{axis}'. Create one with "
|
|
3482
|
+
"create_axis first, and pass the name it returns."
|
|
3483
|
+
),
|
|
3484
|
+
)
|
|
3485
|
+
|
|
3486
|
+
await asyncio.sleep(self._delays["feature_operation"])
|
|
3487
|
+
self._operation_count += 1
|
|
3488
|
+
|
|
3489
|
+
volume_before = self._pattern_volume
|
|
3490
|
+
volume_after = volume_before + self._PATTERN_BLADE_VOLUME * (int(count) - 1)
|
|
3491
|
+
self._pattern_volume = volume_after
|
|
3492
|
+
|
|
3493
|
+
self._feature_tree_count += 1
|
|
3494
|
+
pattern_name = f"CirPattern{self._feature_tree_count}"
|
|
3495
|
+
|
|
3496
|
+
return AdapterResult(
|
|
3497
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3498
|
+
data={
|
|
3499
|
+
"name": pattern_name,
|
|
3500
|
+
"patterned": names,
|
|
3501
|
+
"axis": axis,
|
|
3502
|
+
"count": int(count),
|
|
3503
|
+
"angle": float(angle),
|
|
3504
|
+
"equal_spacing": bool(equal_spacing),
|
|
3505
|
+
"volume_before": volume_before,
|
|
3506
|
+
"volume_after": volume_after,
|
|
3507
|
+
"volume_ratio": round(volume_after / volume_before, 6),
|
|
3508
|
+
},
|
|
3509
|
+
execution_time=self._delays["feature_operation"],
|
|
3510
|
+
)
|
|
3511
|
+
|
|
3512
|
+
# --- Engineering-data and drawing-documentation methods ---
|
|
3513
|
+
# Mirrors SolidWorksDocumentationMixin (solidworks/documentation.py): the
|
|
3514
|
+
# same refusals for wrong document types and unknown views, with
|
|
3515
|
+
# self-consistent state so set/get pairs round-trip.
|
|
3516
|
+
|
|
3517
|
+
_MOCK_MATERIALS: tuple[dict[str, str], ...] = (
|
|
3518
|
+
{
|
|
3519
|
+
"name": "AISI 1020",
|
|
3520
|
+
"classification": "Steel",
|
|
3521
|
+
"database": "solidworks materials",
|
|
3522
|
+
},
|
|
3523
|
+
{
|
|
3524
|
+
"name": "AISI 304",
|
|
3525
|
+
"classification": "Steel",
|
|
3526
|
+
"database": "solidworks materials",
|
|
3527
|
+
},
|
|
3528
|
+
{
|
|
3529
|
+
"name": "6061-T6 (SS)",
|
|
3530
|
+
"classification": "Aluminium Alloys",
|
|
3531
|
+
"database": "solidworks materials",
|
|
3532
|
+
},
|
|
3533
|
+
{
|
|
3534
|
+
"name": "ABS",
|
|
3535
|
+
"classification": "Plastics",
|
|
3536
|
+
"database": "solidworks materials",
|
|
3537
|
+
},
|
|
3538
|
+
)
|
|
3539
|
+
_MOCK_MATERIAL_FOLDER = "mock://sldmaterials"
|
|
3540
|
+
|
|
3541
|
+
def _mock_doc_error(
|
|
3542
|
+
self, allowed: set[str], operation: str
|
|
3543
|
+
) -> AdapterResult[Any] | None:
|
|
3544
|
+
"""Return an error unless the active mock document type is allowed."""
|
|
3545
|
+
model = self._current_model
|
|
3546
|
+
doc_type = getattr(model, "type", None)
|
|
3547
|
+
if model is None or doc_type not in allowed:
|
|
3548
|
+
wanted = " or ".join(sorted(t.lower() for t in allowed))
|
|
3549
|
+
return AdapterResult(
|
|
3550
|
+
status=AdapterResultStatus.ERROR,
|
|
3551
|
+
error=f"{operation} needs an active {wanted} document",
|
|
3552
|
+
)
|
|
3553
|
+
return None
|
|
3554
|
+
|
|
3555
|
+
def _mock_doc_key(self) -> str:
|
|
3556
|
+
"""Key per-document mock state by path, falling back to name."""
|
|
3557
|
+
model = self._current_model
|
|
3558
|
+
return str(
|
|
3559
|
+
getattr(model, "path", "") or getattr(model, "name", "") or "untitled"
|
|
3560
|
+
)
|
|
3561
|
+
|
|
3562
|
+
async def list_model_dimensions(
|
|
3563
|
+
self, feature_name: str | None = None
|
|
3564
|
+
) -> AdapterResult[list[dict[str, Any]]]:
|
|
3565
|
+
"""Mock listing model dimensions from the tracked dimension table."""
|
|
3566
|
+
await asyncio.sleep(self._delays["model_operation"] / 2)
|
|
3567
|
+
guard = self._mock_doc_error({"Part", "Assembly"}, "list_model_dimensions")
|
|
3568
|
+
if guard is not None:
|
|
3569
|
+
return guard
|
|
3570
|
+
self._operation_count += 1
|
|
3571
|
+
dims = []
|
|
3572
|
+
for full_name, value in self._dimensions.items():
|
|
3573
|
+
feature = full_name.split("@", 1)[1] if "@" in full_name else ""
|
|
3574
|
+
if feature_name and feature.lower() != feature_name.lower():
|
|
3575
|
+
continue
|
|
3576
|
+
dims.append(
|
|
3577
|
+
{
|
|
3578
|
+
"full_name": full_name,
|
|
3579
|
+
"feature": feature,
|
|
3580
|
+
"value": value,
|
|
3581
|
+
"system_value": value / 1000.0,
|
|
3582
|
+
"read_only": False,
|
|
3583
|
+
}
|
|
3584
|
+
)
|
|
3585
|
+
if feature_name and not dims:
|
|
3586
|
+
return AdapterResult(
|
|
3587
|
+
status=AdapterResultStatus.ERROR,
|
|
3588
|
+
error=f"No dimensions found on feature {feature_name!r}",
|
|
3589
|
+
)
|
|
3590
|
+
return AdapterResult(status=AdapterResultStatus.SUCCESS, data=dims)
|
|
3591
|
+
|
|
3592
|
+
async def measure_entities(
|
|
3593
|
+
self, entities: list[str]
|
|
3594
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3595
|
+
"""Mock measuring: nominal values with the live adapter's validation."""
|
|
3596
|
+
await asyncio.sleep(self._delays["model_operation"] / 2)
|
|
3597
|
+
guard = self._mock_doc_error({"Part", "Assembly"}, "measure_entities")
|
|
3598
|
+
if guard is not None:
|
|
3599
|
+
return guard
|
|
3600
|
+
if not entities or len(entities) > 2:
|
|
3601
|
+
return AdapterResult(
|
|
3602
|
+
status=AdapterResultStatus.ERROR,
|
|
3603
|
+
error="measure_entities takes one or two entity specs",
|
|
3604
|
+
)
|
|
3605
|
+
self._operation_count += 1
|
|
3606
|
+
data: dict[str, Any] = {"selected": list(entities)}
|
|
3607
|
+
if len(entities) == 1:
|
|
3608
|
+
data.update({"area_m2": 0.0025, "perimeter_m": 0.2})
|
|
3609
|
+
else:
|
|
3610
|
+
data.update({"distance_m": 0.05, "angle_rad": 0.0, "is_parallel": True})
|
|
3611
|
+
return AdapterResult(status=AdapterResultStatus.SUCCESS, data=data)
|
|
3612
|
+
|
|
3613
|
+
async def get_material(
|
|
3614
|
+
self, configuration: str | None = None
|
|
3615
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3616
|
+
"""Mock reading the active part material."""
|
|
3617
|
+
guard = self._mock_doc_error({"Part"}, "get_material")
|
|
3618
|
+
if guard is not None:
|
|
3619
|
+
return guard
|
|
3620
|
+
assigned = getattr(self, "_materials", {}).get(self._mock_doc_key())
|
|
3621
|
+
return AdapterResult(
|
|
3622
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3623
|
+
data={
|
|
3624
|
+
"material": assigned["name"] if assigned else None,
|
|
3625
|
+
"database": assigned["database"] if assigned else None,
|
|
3626
|
+
"configuration": configuration,
|
|
3627
|
+
},
|
|
3628
|
+
)
|
|
3629
|
+
|
|
3630
|
+
async def set_material(
|
|
3631
|
+
self,
|
|
3632
|
+
material_name: str,
|
|
3633
|
+
database: str | None = None,
|
|
3634
|
+
configuration: str | None = None,
|
|
3635
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3636
|
+
"""Mock assigning a material; unknown names fail as they do live."""
|
|
3637
|
+
guard = self._mock_doc_error({"Part"}, "set_material")
|
|
3638
|
+
if guard is not None:
|
|
3639
|
+
return guard
|
|
3640
|
+
wanted = material_name.strip().lower()
|
|
3641
|
+
match = next(
|
|
3642
|
+
(m for m in self._MOCK_MATERIALS if m["name"].lower() == wanted), None
|
|
3643
|
+
)
|
|
3644
|
+
if match is None:
|
|
3645
|
+
return AdapterResult(
|
|
3646
|
+
status=AdapterResultStatus.ERROR,
|
|
3647
|
+
error=(
|
|
3648
|
+
f"Material {material_name!r} not found in "
|
|
3649
|
+
f"{database or 'SOLIDWORKS Materials'!r}; use "
|
|
3650
|
+
"list_available_materials for the exact name and library."
|
|
3651
|
+
),
|
|
3652
|
+
)
|
|
3653
|
+
if not hasattr(self, "_materials"):
|
|
3654
|
+
self._materials: dict[str, dict[str, str]] = {}
|
|
3655
|
+
self._materials[self._mock_doc_key()] = match
|
|
3656
|
+
self._operation_count += 1
|
|
3657
|
+
return AdapterResult(
|
|
3658
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3659
|
+
data={
|
|
3660
|
+
"material": match["name"],
|
|
3661
|
+
"database": database or "SOLIDWORKS Materials",
|
|
3662
|
+
"configuration": configuration,
|
|
3663
|
+
},
|
|
3664
|
+
)
|
|
3665
|
+
|
|
3666
|
+
async def list_available_materials(
|
|
3667
|
+
self, search: str | None = None
|
|
3668
|
+
) -> AdapterResult[list[dict[str, str]]]:
|
|
3669
|
+
"""Mock listing the material library, with the live ``metadata``."""
|
|
3670
|
+
needle = (search or "").strip().lower()
|
|
3671
|
+
items = [
|
|
3672
|
+
dict(m)
|
|
3673
|
+
for m in self._MOCK_MATERIALS
|
|
3674
|
+
if not needle
|
|
3675
|
+
or needle in m["name"].lower()
|
|
3676
|
+
or needle in m["classification"].lower()
|
|
3677
|
+
]
|
|
3678
|
+
return AdapterResult(
|
|
3679
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3680
|
+
data=items,
|
|
3681
|
+
metadata={
|
|
3682
|
+
"folders": [self._MOCK_MATERIAL_FOLDER],
|
|
3683
|
+
"total": len(self._MOCK_MATERIALS),
|
|
3684
|
+
},
|
|
3685
|
+
)
|
|
3686
|
+
|
|
3687
|
+
async def get_custom_properties(
|
|
3688
|
+
self, configuration: str | None = None
|
|
3689
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3690
|
+
"""Mock reading custom properties."""
|
|
3691
|
+
if self._current_model is None:
|
|
3692
|
+
return AdapterResult(
|
|
3693
|
+
status=AdapterResultStatus.ERROR, error="No active model"
|
|
3694
|
+
)
|
|
3695
|
+
store = getattr(self, "_custom_properties", {})
|
|
3696
|
+
props = store.get((self._mock_doc_key(), configuration or ""), {})
|
|
3697
|
+
return AdapterResult(
|
|
3698
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3699
|
+
data={
|
|
3700
|
+
"configuration": configuration or "",
|
|
3701
|
+
"properties": {
|
|
3702
|
+
k: {"value": v, "resolved": v} for k, v in props.items()
|
|
3703
|
+
},
|
|
3704
|
+
},
|
|
3705
|
+
)
|
|
3706
|
+
|
|
3707
|
+
async def set_custom_property(
|
|
3708
|
+
self, name: str, value: str, configuration: str | None = None
|
|
3709
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3710
|
+
"""Mock writing one custom property."""
|
|
3711
|
+
if self._current_model is None:
|
|
3712
|
+
return AdapterResult(
|
|
3713
|
+
status=AdapterResultStatus.ERROR, error="No active model"
|
|
3714
|
+
)
|
|
3715
|
+
if not name.strip():
|
|
3716
|
+
return AdapterResult(
|
|
3717
|
+
status=AdapterResultStatus.ERROR, error="name is required"
|
|
3718
|
+
)
|
|
3719
|
+
if not hasattr(self, "_custom_properties"):
|
|
3720
|
+
self._custom_properties: dict[tuple[str, str], dict[str, str]] = {}
|
|
3721
|
+
key = (self._mock_doc_key(), configuration or "")
|
|
3722
|
+
self._custom_properties.setdefault(key, {})[name] = str(value)
|
|
3723
|
+
self._operation_count += 1
|
|
3724
|
+
return AdapterResult(
|
|
3725
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3726
|
+
data={
|
|
3727
|
+
"name": name,
|
|
3728
|
+
"value": str(value),
|
|
3729
|
+
"resolved": str(value),
|
|
3730
|
+
"configuration": configuration or "",
|
|
3731
|
+
},
|
|
3732
|
+
)
|
|
3733
|
+
|
|
3734
|
+
async def export_flat_pattern(
|
|
3735
|
+
self, file_path: str
|
|
3736
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3737
|
+
"""Mock flat-pattern export: validates the target, writes nothing."""
|
|
3738
|
+
guard = self._mock_doc_error({"Part"}, "export_flat_pattern")
|
|
3739
|
+
if guard is not None:
|
|
3740
|
+
return guard
|
|
3741
|
+
if not file_path.lower().endswith((".dxf", ".dwg")):
|
|
3742
|
+
return AdapterResult(
|
|
3743
|
+
status=AdapterResultStatus.ERROR,
|
|
3744
|
+
error="file_path must end in .dxf or .dwg",
|
|
3745
|
+
)
|
|
3746
|
+
return AdapterResult(
|
|
3747
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3748
|
+
data={"file_path": file_path, "size_bytes": 0, "mock": True},
|
|
3749
|
+
)
|
|
3750
|
+
|
|
3751
|
+
async def insert_model_dimensions(
|
|
3752
|
+
self, view_name: str | None = None, include_hole_wizard: bool = True
|
|
3753
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3754
|
+
"""Mock importing model dimensions: three per view, never twice.
|
|
3755
|
+
|
|
3756
|
+
Mirrors the live result shape (``views``, ``dimensions_added``,
|
|
3757
|
+
``unique_dimensions``, ``skipped_pictorial_views``, ``method``,
|
|
3758
|
+
``autodimension_status``) and its refusals.
|
|
3759
|
+
"""
|
|
3760
|
+
await asyncio.sleep(self._delays["model_operation"] / 2)
|
|
3761
|
+
guard = self._mock_doc_error({"Drawing"}, "insert_model_dimensions")
|
|
3762
|
+
if guard is not None:
|
|
3763
|
+
return guard
|
|
3764
|
+
names = [view_name] if view_name else list(self._drawing_views)
|
|
3765
|
+
unknown = [n for n in names if n not in self._drawing_views]
|
|
3766
|
+
if unknown or not names:
|
|
3767
|
+
return AdapterResult(
|
|
3768
|
+
status=AdapterResultStatus.ERROR,
|
|
3769
|
+
error=(
|
|
3770
|
+
f"No view named {unknown[0]!r}. Views: "
|
|
3771
|
+
f"{', '.join(self._drawing_views) or 'none'}"
|
|
3772
|
+
if unknown
|
|
3773
|
+
else "No drawing views found to dimension; add views with "
|
|
3774
|
+
"create_technical_drawing or create_drawing_view"
|
|
3775
|
+
),
|
|
3776
|
+
)
|
|
3777
|
+
if not hasattr(self, "_view_dimensions"):
|
|
3778
|
+
self._view_dimensions: dict[str, int] = {}
|
|
3779
|
+
per_view = []
|
|
3780
|
+
for name in names:
|
|
3781
|
+
before = self._view_dimensions.get(name, 0)
|
|
3782
|
+
after = before or 3
|
|
3783
|
+
self._view_dimensions[name] = after
|
|
3784
|
+
per_view.append(
|
|
3785
|
+
{
|
|
3786
|
+
"view": name,
|
|
3787
|
+
"dimensions_before": before,
|
|
3788
|
+
"dimensions_after": after,
|
|
3789
|
+
"dimensions_added": after - before,
|
|
3790
|
+
"duplicates_removed": 0,
|
|
3791
|
+
}
|
|
3792
|
+
)
|
|
3793
|
+
self._operation_count += 1
|
|
3794
|
+
return AdapterResult(
|
|
3795
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3796
|
+
data={
|
|
3797
|
+
"views": per_view,
|
|
3798
|
+
"dimensions_added": sum(v["dimensions_added"] for v in per_view),
|
|
3799
|
+
"unique_dimensions": sum(self._view_dimensions.values()),
|
|
3800
|
+
"skipped_pictorial_views": [],
|
|
3801
|
+
"method": "insert_model_annotations",
|
|
3802
|
+
"autodimension_status": None,
|
|
3803
|
+
},
|
|
3804
|
+
)
|
|
3805
|
+
|
|
3806
|
+
async def arrange_dimensions(
|
|
3807
|
+
self, view_name: str | None = None
|
|
3808
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3809
|
+
"""Mock arranging drawing dimensions: reports a clean layout."""
|
|
3810
|
+
guard = self._mock_doc_error({"Drawing"}, "arrange_dimensions")
|
|
3811
|
+
if guard is not None:
|
|
3812
|
+
return guard
|
|
3813
|
+
if view_name and view_name not in self._drawing_views:
|
|
3814
|
+
return AdapterResult(
|
|
3815
|
+
status=AdapterResultStatus.ERROR,
|
|
3816
|
+
error=(
|
|
3817
|
+
f"No view named {view_name!r}. Views: "
|
|
3818
|
+
f"{', '.join(self._drawing_views) or 'none'}"
|
|
3819
|
+
),
|
|
3820
|
+
)
|
|
3821
|
+
counts = getattr(self, "_view_dimensions", {})
|
|
3822
|
+
total = sum(n for v, n in counts.items() if not view_name or v == view_name)
|
|
3823
|
+
return AdapterResult(
|
|
3824
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3825
|
+
data={
|
|
3826
|
+
"dimensions": total,
|
|
3827
|
+
"moved": total,
|
|
3828
|
+
"unplaced": [],
|
|
3829
|
+
"overlaps_before": total,
|
|
3830
|
+
"overlaps_after": 0,
|
|
3831
|
+
"remaining_overlaps": [],
|
|
3832
|
+
"sheet_size_m": [0.42, 0.297],
|
|
3833
|
+
},
|
|
3834
|
+
)
|
|
3835
|
+
|
|
3836
|
+
async def insert_bom_table(
|
|
3837
|
+
self,
|
|
3838
|
+
view_name: str,
|
|
3839
|
+
template_path: str | None = None,
|
|
3840
|
+
bom_type: str = "top_level_only",
|
|
3841
|
+
x_mm: float | None = None,
|
|
3842
|
+
y_mm: float | None = None,
|
|
3843
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3844
|
+
"""Mock inserting a BOM table."""
|
|
3845
|
+
guard = self._mock_doc_error({"Drawing"}, "insert_bom_table")
|
|
3846
|
+
if guard is not None:
|
|
3847
|
+
return guard
|
|
3848
|
+
if bom_type not in {"top_level_only", "parts_only", "indented"}:
|
|
3849
|
+
return AdapterResult(
|
|
3850
|
+
status=AdapterResultStatus.ERROR,
|
|
3851
|
+
error="bom_type must be top_level_only, parts_only or indented",
|
|
3852
|
+
)
|
|
3853
|
+
if view_name not in self._drawing_views:
|
|
3854
|
+
return AdapterResult(
|
|
3855
|
+
status=AdapterResultStatus.ERROR,
|
|
3856
|
+
error=(
|
|
3857
|
+
f"No view named {view_name!r}. Views: "
|
|
3858
|
+
f"{', '.join(self._drawing_views) or 'none'}"
|
|
3859
|
+
),
|
|
3860
|
+
)
|
|
3861
|
+
self._operation_count += 1
|
|
3862
|
+
return AdapterResult(
|
|
3863
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3864
|
+
data={
|
|
3865
|
+
"view": view_name,
|
|
3866
|
+
"bom_type": bom_type,
|
|
3867
|
+
"template": template_path,
|
|
3868
|
+
"row_count": 2,
|
|
3869
|
+
"table_box_mm": [0.0, 0.0, 60.0, 15.0],
|
|
3870
|
+
"moved_onto_sheet": False,
|
|
3871
|
+
},
|
|
3872
|
+
)
|
|
3873
|
+
|
|
3874
|
+
async def add_gtol(
|
|
3875
|
+
self,
|
|
3876
|
+
characteristic: str,
|
|
3877
|
+
tolerance: float,
|
|
3878
|
+
x_mm: float,
|
|
3879
|
+
y_mm: float,
|
|
3880
|
+
diameter_zone: bool = False,
|
|
3881
|
+
material_condition: str = "none",
|
|
3882
|
+
datums: list[str] | None = None,
|
|
3883
|
+
attach_at_mm: tuple[float, float] | None = None,
|
|
3884
|
+
dimension: tuple[str, str] | None = None,
|
|
3885
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3886
|
+
"""Mock adding a feature control frame; refuses what the real one refuses."""
|
|
3887
|
+
guard = self._mock_doc_error({"Drawing"}, "add_gtol")
|
|
3888
|
+
if guard is not None:
|
|
3889
|
+
return guard
|
|
3890
|
+
problem = gtol_problem(characteristic, tolerance, material_condition, datums)
|
|
3891
|
+
if problem is not None:
|
|
3892
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error=problem)
|
|
3893
|
+
self._operation_count += 1
|
|
3894
|
+
return AdapterResult(
|
|
3895
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3896
|
+
data={
|
|
3897
|
+
"characteristic": characteristic,
|
|
3898
|
+
"tolerance": f"{tolerance:g}",
|
|
3899
|
+
"diameter_zone": diameter_zone,
|
|
3900
|
+
"material_condition": material_condition,
|
|
3901
|
+
"datums": [d.strip().upper() for d in datums or []],
|
|
3902
|
+
"attached": (
|
|
3903
|
+
dimension[1] if dimension is not None else attach_at_mm is not None
|
|
3904
|
+
),
|
|
3905
|
+
"position_mm": None if dimension is not None else [x_mm, y_mm],
|
|
3906
|
+
"layout": None,
|
|
3907
|
+
},
|
|
3908
|
+
)
|
|
3909
|
+
|
|
3910
|
+
async def add_datum_tag(
|
|
3911
|
+
self,
|
|
3912
|
+
label: str,
|
|
3913
|
+
attach_at_mm: tuple[float, float],
|
|
3914
|
+
x_mm: float,
|
|
3915
|
+
y_mm: float,
|
|
3916
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3917
|
+
"""Mock adding a datum tag; refuses what the real one refuses."""
|
|
3918
|
+
guard = self._mock_doc_error({"Drawing"}, "add_datum_tag")
|
|
3919
|
+
if guard is not None:
|
|
3920
|
+
return guard
|
|
3921
|
+
letter = label.strip().upper()
|
|
3922
|
+
if len(letter) != 1 or not letter.isalpha():
|
|
3923
|
+
return AdapterResult(
|
|
3924
|
+
status=AdapterResultStatus.ERROR,
|
|
3925
|
+
error="label must be a single letter, e.g. 'A'",
|
|
3926
|
+
)
|
|
3927
|
+
self._operation_count += 1
|
|
3928
|
+
return AdapterResult(
|
|
3929
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3930
|
+
data={"label": letter, "position_mm": [x_mm, y_mm]},
|
|
3931
|
+
)
|
|
3932
|
+
|
|
3933
|
+
async def set_sheet_scale(
|
|
3934
|
+
self, numerator: float, denominator: float
|
|
3935
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3936
|
+
"""Mock setting the sheet scale."""
|
|
3937
|
+
guard = self._mock_doc_error({"Drawing"}, "set_sheet_scale")
|
|
3938
|
+
if guard is not None:
|
|
3939
|
+
return guard
|
|
3940
|
+
if numerator <= 0 or denominator <= 0:
|
|
3941
|
+
return AdapterResult(
|
|
3942
|
+
status=AdapterResultStatus.ERROR, error="Scale values must be positive"
|
|
3943
|
+
)
|
|
3944
|
+
return AdapterResult(
|
|
3945
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3946
|
+
data={
|
|
3947
|
+
"sheet": "Sheet1",
|
|
3948
|
+
"scale": f"{numerator:g}:{denominator:g}",
|
|
3949
|
+
"requested": f"{numerator:g}:{denominator:g}",
|
|
3950
|
+
},
|
|
3951
|
+
)
|
|
3952
|
+
|
|
3953
|
+
async def create_standard_drawing(
|
|
3954
|
+
self,
|
|
3955
|
+
model_path: str,
|
|
3956
|
+
output_dir: str | None = None,
|
|
3957
|
+
title: str | None = None,
|
|
3958
|
+
subtitle: str | None = None,
|
|
3959
|
+
drawing_number: str | None = None,
|
|
3960
|
+
author: str = "",
|
|
3961
|
+
revision: str = "A",
|
|
3962
|
+
company: str = "",
|
|
3963
|
+
drawn_date: str | None = None,
|
|
3964
|
+
notes: list[str] | None = None,
|
|
3965
|
+
include_isometric: bool = True,
|
|
3966
|
+
write_model_properties: bool = True,
|
|
3967
|
+
title_block_text_height_mm: float | None = None,
|
|
3968
|
+
include_bom: bool = False,
|
|
3969
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
3970
|
+
"""Mock standard A3 drawing: validates inputs, writes nothing to disk."""
|
|
3971
|
+
if not os.path.isfile(model_path):
|
|
3972
|
+
return AdapterResult(
|
|
3973
|
+
status=AdapterResultStatus.ERROR,
|
|
3974
|
+
error=f"Model file not found: {model_path}",
|
|
3975
|
+
)
|
|
3976
|
+
stem = os.path.splitext(os.path.basename(model_path))[0]
|
|
3977
|
+
number = drawing_number or f"{stem.upper().replace(' ', '-')}-Rev{revision}-01"
|
|
3978
|
+
out_dir = output_dir or os.path.dirname(model_path)
|
|
3979
|
+
views = {
|
|
3980
|
+
"Drawing View1": {"role": "front", "caption": "FRONT VIEW"},
|
|
3981
|
+
"Drawing View2": {"role": "plan", "caption": "TOP VIEW"},
|
|
3982
|
+
"Drawing View3": {"role": "side", "caption": "SIDE VIEW"},
|
|
3983
|
+
}
|
|
3984
|
+
if include_isometric:
|
|
3985
|
+
views["Drawing View4"] = {"role": "iso", "caption": "ISOMETRIC VIEW"}
|
|
3986
|
+
self._operation_count += 1
|
|
3987
|
+
return AdapterResult(
|
|
3988
|
+
status=AdapterResultStatus.SUCCESS,
|
|
3989
|
+
data={
|
|
3990
|
+
"scale": "1:1",
|
|
3991
|
+
"isometric_scale": "1:1" if include_isometric else None,
|
|
3992
|
+
"views": views,
|
|
3993
|
+
"hidden_lines_visible": {
|
|
3994
|
+
n: True for n, v in views.items() if v["role"] != "iso"
|
|
3995
|
+
},
|
|
3996
|
+
"bom": {"bom_type": "top_level_only", "table_box_mm": None}
|
|
3997
|
+
if include_bom
|
|
3998
|
+
else None,
|
|
3999
|
+
"overlap_check": {"overlaps_after": 0, "annotation_overlaps": []},
|
|
4000
|
+
"drawing_number": number,
|
|
4001
|
+
"drawing_path": os.path.join(out_dir, f"{number}.SLDDRW"),
|
|
4002
|
+
"pdf_path": os.path.join(out_dir, f"{number}.pdf"),
|
|
4003
|
+
"properties_written": {
|
|
4004
|
+
"model": {"Description": title or stem}
|
|
4005
|
+
if write_model_properties
|
|
4006
|
+
else {},
|
|
4007
|
+
"drawing": {"DrawnBy": author, "Revision": revision},
|
|
4008
|
+
},
|
|
4009
|
+
"notes": ["NOTES", *[n.upper() for n in (notes or [])]],
|
|
4010
|
+
"title_block": {
|
|
4011
|
+
"drawing number": {"found": True, "fits": True},
|
|
4012
|
+
"title": {"found": True, "fits": True},
|
|
4013
|
+
},
|
|
4014
|
+
"mock": True,
|
|
4015
|
+
},
|
|
4016
|
+
)
|