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.
Files changed (115) hide show
  1. solidworks_mcp/__init__.py +57 -0
  2. solidworks_mcp/adapters/__init__.py +40 -0
  3. solidworks_mcp/adapters/base.py +1882 -0
  4. solidworks_mcp/adapters/circuit_breaker.py +2052 -0
  5. solidworks_mcp/adapters/com_executor.py +252 -0
  6. solidworks_mcp/adapters/complexity_analyzer.py +272 -0
  7. solidworks_mcp/adapters/connection_pool.py +1884 -0
  8. solidworks_mcp/adapters/factory.py +366 -0
  9. solidworks_mcp/adapters/intelligent_router.py +218 -0
  10. solidworks_mcp/adapters/mock_adapter.py +4016 -0
  11. solidworks_mcp/adapters/pywin32_adapter.py +2842 -0
  12. solidworks_mcp/adapters/solidworks/__init__.py +17 -0
  13. solidworks_mcp/adapters/solidworks/bitmap.py +180 -0
  14. solidworks_mcp/adapters/solidworks/com_utils.py +56 -0
  15. solidworks_mcp/adapters/solidworks/dimension_layout.py +753 -0
  16. solidworks_mcp/adapters/solidworks/documentation.py +1476 -0
  17. solidworks_mcp/adapters/solidworks/drawing_standard.py +1993 -0
  18. solidworks_mcp/adapters/solidworks/face_selection.py +392 -0
  19. solidworks_mcp/adapters/solidworks/features.py +3362 -0
  20. solidworks_mcp/adapters/solidworks/io.py +3649 -0
  21. solidworks_mcp/adapters/solidworks/rebuild_diagnostics.py +269 -0
  22. solidworks_mcp/adapters/solidworks/rebuild_inspector.py +146 -0
  23. solidworks_mcp/adapters/solidworks/selection.py +308 -0
  24. solidworks_mcp/adapters/solidworks/sketch.py +2545 -0
  25. solidworks_mcp/adapters/sw_feature_errors.py +66 -0
  26. solidworks_mcp/adapters/sw_type_info.py +317 -0
  27. solidworks_mcp/adapters/vba_adapter.py +318 -0
  28. solidworks_mcp/adapters/vba_macro_executor.py +215 -0
  29. solidworks_mcp/agents/__init__.py +68 -0
  30. solidworks_mcp/agents/design_knowledge/assembly_patterns.md +127 -0
  31. solidworks_mcp/agents/design_knowledge/hinges.md +118 -0
  32. solidworks_mcp/agents/design_knowledge/snap_fits.md +93 -0
  33. solidworks_mcp/agents/design_knowledge/solidworks_basics.md +3 -0
  34. solidworks_mcp/agents/design_knowledge/tolerancing.md +128 -0
  35. solidworks_mcp/agents/harness.py +216 -0
  36. solidworks_mcp/agents/history_db.py +1508 -0
  37. solidworks_mcp/agents/retrieval_index.py +164 -0
  38. solidworks_mcp/agents/schemas.py +165 -0
  39. solidworks_mcp/agents/smoke_test.py +285 -0
  40. solidworks_mcp/agents/soc_exporter.py +650 -0
  41. solidworks_mcp/agents/soc_pickup.py +332 -0
  42. solidworks_mcp/agents/soc_rewind.py +221 -0
  43. solidworks_mcp/agents/vector_rag.py +665 -0
  44. solidworks_mcp/cache/__init__.py +5 -0
  45. solidworks_mcp/cache/response_cache.py +164 -0
  46. solidworks_mcp/config.py +729 -0
  47. solidworks_mcp/exceptions.py +37 -0
  48. solidworks_mcp/security/__init__.py +68 -0
  49. solidworks_mcp/security/auth.py +128 -0
  50. solidworks_mcp/security/cors.py +27 -0
  51. solidworks_mcp/security/rate_limiting.py +90 -0
  52. solidworks_mcp/security/runtime.py +155 -0
  53. solidworks_mcp/server.py +777 -0
  54. solidworks_mcp/server_cli_fixed.py +120 -0
  55. solidworks_mcp/setup/__init__.py +1 -0
  56. solidworks_mcp/setup/cli.py +399 -0
  57. solidworks_mcp/setup/clients.py +157 -0
  58. solidworks_mcp/setup/config_file.py +230 -0
  59. solidworks_mcp/setup/handshake.py +202 -0
  60. solidworks_mcp/setup/launcher.py +102 -0
  61. solidworks_mcp/setup/legacy.py +100 -0
  62. solidworks_mcp/setup/registration.py +193 -0
  63. solidworks_mcp/setup/solidworks.py +49 -0
  64. solidworks_mcp/tools/__init__.py +64 -0
  65. solidworks_mcp/tools/analysis.py +723 -0
  66. solidworks_mcp/tools/automation.py +578 -0
  67. solidworks_mcp/tools/docs_discovery.py +1476 -0
  68. solidworks_mcp/tools/drawing.py +1015 -0
  69. solidworks_mcp/tools/drawing_analysis.py +456 -0
  70. solidworks_mcp/tools/drawing_automation.py +627 -0
  71. solidworks_mcp/tools/engineering_data.py +466 -0
  72. solidworks_mcp/tools/error_classifier.py +232 -0
  73. solidworks_mcp/tools/export.py +833 -0
  74. solidworks_mcp/tools/file_facts.py +40 -0
  75. solidworks_mcp/tools/file_management.py +1473 -0
  76. solidworks_mcp/tools/input_compat.py +17 -0
  77. solidworks_mcp/tools/macro_recording.py +464 -0
  78. solidworks_mcp/tools/modeling.py +2360 -0
  79. solidworks_mcp/tools/sketching.py +1422 -0
  80. solidworks_mcp/tools/template_management.py +367 -0
  81. solidworks_mcp/tools/tool_boundary.py +280 -0
  82. solidworks_mcp/tools/vba_generation.py +1145 -0
  83. solidworks_mcp/ui/__init__.py +9 -0
  84. solidworks_mcp/ui/local_llm.py +725 -0
  85. solidworks_mcp/ui/routers/__init__.py +10 -0
  86. solidworks_mcp/ui/routers/checkpoint.py +24 -0
  87. solidworks_mcp/ui/routers/docs.py +47 -0
  88. solidworks_mcp/ui/routers/llm.py +83 -0
  89. solidworks_mcp/ui/routers/local_model.py +69 -0
  90. solidworks_mcp/ui/routers/model.py +75 -0
  91. solidworks_mcp/ui/routers/preview.py +38 -0
  92. solidworks_mcp/ui/routers/session.py +161 -0
  93. solidworks_mcp/ui/routers/viewer.py +154 -0
  94. solidworks_mcp/ui/schemas.py +502 -0
  95. solidworks_mcp/ui/server.py +298 -0
  96. solidworks_mcp/ui/service.py +481 -0
  97. solidworks_mcp/ui/services/__init__.py +136 -0
  98. solidworks_mcp/ui/services/_utils.py +958 -0
  99. solidworks_mcp/ui/services/checkpoint_service.py +1110 -0
  100. solidworks_mcp/ui/services/docs_service.py +282 -0
  101. solidworks_mcp/ui/services/llm_service.py +956 -0
  102. solidworks_mcp/ui/services/model_service.py +459 -0
  103. solidworks_mcp/ui/services/parameter_repair_service.py +314 -0
  104. solidworks_mcp/ui/services/preview_service.py +489 -0
  105. solidworks_mcp/ui/services/session_service.py +1389 -0
  106. solidworks_mcp/utils/__init__.py +9 -0
  107. solidworks_mcp/utils/feature_tree_classifier.py +342 -0
  108. solidworks_mcp/utils/logging.py +76 -0
  109. solidworks_mcp/utils/validation.py +77 -0
  110. solidworks_mcp/version.py +8 -0
  111. solidworks_mcp_python-1.5.0.dist-info/METADATA +410 -0
  112. solidworks_mcp_python-1.5.0.dist-info/RECORD +115 -0
  113. solidworks_mcp_python-1.5.0.dist-info/WHEEL +4 -0
  114. solidworks_mcp_python-1.5.0.dist-info/entry_points.txt +3 -0
  115. solidworks_mcp_python-1.5.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,392 @@
1
+ """Resolve a face by point / side / plane and multi-select its edges (#106).
2
+
3
+ Split into three single-purpose pieces so each can be tested on its own:
4
+
5
+ * **Pure geometry** (no COM): side-token parsing, picking the furthest
6
+ planar face facing a direction, matching faces to a reference plane, and
7
+ input validation shared by the real and mock adapters.
8
+ * **Edge description** (:func:`describe_edge`): one place that turns an
9
+ ``IEdge`` into a JSON-friendly dict - also used by ``get_selected_entities``.
10
+ * **COM access** (:class:`FaceEdgeSelector`): reads faces/edges off the
11
+ active part and performs the multi-selection.
12
+
13
+ Live behaviour this relies on (R2026x, 2026-10-13) is recorded in
14
+ ``openspec/changes/archive/2026-10-30-wire-unwired-adapter-tools/design.md``: every dispatch must
15
+ be method-flagged, ``IFace2.Normal`` is ``[0,0,0]`` for non-planar faces,
16
+ ``IFace2.GetClosestPointOn`` returns the closest point in its first three
17
+ values, a full-circle edge has no vertices, ``IRefPlane.Transform.ArrayData``
18
+ holds the plane normal at ``[6:9]`` and origin at ``[9:12]``, and
19
+ ``IEntity.Select4`` needs a real ``SelectData`` (``None`` is a type mismatch).
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from dataclasses import dataclass
25
+ from typing import Any
26
+
27
+ from .com_utils import as_list, selection_count
28
+ from .com_utils import attempt as _attempt
29
+ from .com_utils import flag as _flag
30
+
31
+ #: Outward direction for each accepted ``side`` token (case-insensitive).
32
+ #: Aliases follow SolidWorks' default orientation: Front = XY (+Z),
33
+ #: Top = XZ (+Y), Right = YZ (+X).
34
+ SIDE_VECTORS: dict[str, tuple[float, float, float]] = {
35
+ "+x": (1.0, 0.0, 0.0),
36
+ "-x": (-1.0, 0.0, 0.0),
37
+ "+y": (0.0, 1.0, 0.0),
38
+ "-y": (0.0, -1.0, 0.0),
39
+ "+z": (0.0, 0.0, 1.0),
40
+ "-z": (0.0, 0.0, -1.0),
41
+ "right": (1.0, 0.0, 0.0),
42
+ "left": (-1.0, 0.0, 0.0),
43
+ "top": (0.0, 1.0, 0.0),
44
+ "bottom": (0.0, -1.0, 0.0),
45
+ "front": (0.0, 0.0, 1.0),
46
+ "back": (0.0, 0.0, -1.0),
47
+ }
48
+ EDGE_TYPES = ("all", "linear", "circular")
49
+
50
+ _PARALLEL = 0.999 # |cos| threshold for "same direction"
51
+ _ON_PLANE_M = 1e-6 # metres
52
+ _ON_FACE_M = 1e-5 # metres (0.01 mm) for point-on-face
53
+
54
+
55
+ # ---- pure helpers ---------------------------------------------------------
56
+
57
+
58
+ def _dot(a: tuple[float, ...], b: tuple[float, ...]) -> float:
59
+ return sum(x * y for x, y in zip(a, b, strict=True))
60
+
61
+
62
+ def side_vector(side: str) -> tuple[float, float, float]:
63
+ """Return the unit vector for a side token.
64
+
65
+ Raises:
66
+ ValueError: When the token is not one of :data:`SIDE_VECTORS`.
67
+ """
68
+ try:
69
+ return SIDE_VECTORS[side.strip().lower()]
70
+ except KeyError:
71
+ raise ValueError(
72
+ f"Unknown side {side!r}; use one of: " + ", ".join(SIDE_VECTORS)
73
+ ) from None
74
+
75
+
76
+ def validate_face_query(
77
+ point: list[float] | None,
78
+ side: str | None,
79
+ plane: str | None,
80
+ edge_type: str,
81
+ ) -> str | None:
82
+ """Check a select_face_edges request; return an error message or None."""
83
+ given = [
84
+ name
85
+ for name, value in (("point", point), ("side", side), ("plane", plane))
86
+ if value not in (None, "")
87
+ ]
88
+ if len(given) != 1:
89
+ return (
90
+ "Give exactly one of point, side or plane to identify the face "
91
+ f"(got {', '.join(given) or 'none'})."
92
+ )
93
+ if point is not None and len(point) != 3:
94
+ return "point must be [x, y, z] in millimetres."
95
+ if side is not None:
96
+ try:
97
+ side_vector(side)
98
+ except ValueError as e:
99
+ return str(e)
100
+ if edge_type not in EDGE_TYPES:
101
+ return f"edge_type must be one of {', '.join(EDGE_TYPES)} (got {edge_type!r})."
102
+ return None
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class FaceInfo:
107
+ """Geometry of one body face, in metres.
108
+
109
+ Attributes:
110
+ index: Position in the body's face list (stable within one call).
111
+ normal: Outward unit normal; ``(0, 0, 0)`` for non-planar faces.
112
+ box: ``(xmin, ymin, zmin, xmax, ymax, zmax)``.
113
+ area: Face area in m^2.
114
+ handle: The ``IFace2`` dispatch (``None`` in pure tests).
115
+ """
116
+
117
+ index: int
118
+ normal: tuple[float, float, float]
119
+ box: tuple[float, float, float, float, float, float]
120
+ area: float
121
+ handle: Any = None
122
+
123
+ @property
124
+ def is_planar(self) -> bool:
125
+ """True when SolidWorks reported a non-zero normal."""
126
+ return any(abs(c) > 1e-9 for c in self.normal)
127
+
128
+ @property
129
+ def center(self) -> tuple[float, float, float]:
130
+ """Bounding-box centre (lies in the face's plane for planar faces)."""
131
+ b = self.box
132
+ return ((b[0] + b[3]) / 2, (b[1] + b[4]) / 2, (b[2] + b[5]) / 2)
133
+
134
+ def describe(self) -> dict[str, Any]:
135
+ """JSON-friendly summary in millimetres."""
136
+ return {
137
+ "index": self.index,
138
+ "center_mm": [round(c * 1000, 6) for c in self.center],
139
+ "normal": [round(c, 6) + 0.0 for c in self.normal],
140
+ "area_mm2": round(self.area * 1e6, 6),
141
+ "planar": self.is_planar,
142
+ }
143
+
144
+
145
+ def pick_side_face(
146
+ faces: list[FaceInfo], direction: tuple[float, float, float]
147
+ ) -> FaceInfo | None:
148
+ """Return the planar face facing ``direction`` that lies furthest along it.
149
+
150
+ Ties (same offset within 0.1 µm) go to the larger face.
151
+ """
152
+ facing = [
153
+ f for f in faces if f.is_planar and _dot(f.normal, direction) >= _PARALLEL
154
+ ]
155
+ if not facing:
156
+ return None
157
+ return max(facing, key=lambda f: (round(_dot(f.center, direction), 7), f.area))
158
+
159
+
160
+ def plane_from_transform(
161
+ array_data: list[float],
162
+ ) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
163
+ """Return ``(origin, normal)`` from ``IRefPlane.Transform.ArrayData``."""
164
+ normal = (float(array_data[6]), float(array_data[7]), float(array_data[8]))
165
+ origin = (float(array_data[9]), float(array_data[10]), float(array_data[11]))
166
+ return origin, normal
167
+
168
+
169
+ def faces_on_plane(
170
+ faces: list[FaceInfo],
171
+ origin: tuple[float, float, float],
172
+ normal: tuple[float, float, float],
173
+ ) -> list[FaceInfo]:
174
+ """Return the planar faces that lie in the given plane."""
175
+ out = []
176
+ for f in faces:
177
+ if not f.is_planar or abs(_dot(f.normal, normal)) < _PARALLEL:
178
+ continue
179
+ offset = tuple(c - o for c, o in zip(f.center, origin, strict=True))
180
+ if abs(_dot(offset, normal)) <= _ON_PLANE_M:
181
+ out.append(f)
182
+ return out
183
+
184
+
185
+ def edge_matches(kind: str, edge_type: str) -> bool:
186
+ """Whether an edge of ``kind`` passes the ``edge_type`` filter."""
187
+ return edge_type == "all" or kind == edge_type
188
+
189
+
190
+ # ---- COM: edge description -----------------------------------------------
191
+
192
+
193
+ #: Exactly the members read below. Flagging a whole interface costs ~27 ms
194
+ #: per fresh dispatch (see ``_WALK_MEMBERS`` in pywin32_adapter); a face can
195
+ #: hand back dozens of edges/curves/vertices, so flag just what is used.
196
+ _EDGE_MEMBERS = ("GetCurve", "GetStartVertex", "GetEndVertex", "Select4")
197
+ _FACE_MEMBERS = ("GetBox", "GetArea", "GetEdges", "GetClosestPointOn")
198
+
199
+
200
+ def describe_edge(adapter: Any, edge: Any) -> dict[str, Any]:
201
+ """Describe an ``IEdge`` in millimetres; unresolvable fields are omitted.
202
+
203
+ Returns:
204
+ dict[str, Any]: ``curve`` (``linear``/``circular``/``other``), plus
205
+ ``start_mm``/``end_mm``/``length_mm`` when the edge has vertices
206
+ (straight-line length - a curved edge is longer), and
207
+ ``center_mm``/``axis``/``radius_mm`` for circular edges.
208
+ """
209
+ _flag(adapter, edge, *_EDGE_MEMBERS)
210
+ entry: dict[str, Any] = {"curve": "other"}
211
+
212
+ curve = _attempt(adapter, lambda: edge.GetCurve())
213
+ if curve is not None:
214
+ _flag(adapter, curve, "IsCircle", "IsLine")
215
+ if _attempt(adapter, lambda: curve.IsCircle(), False):
216
+ entry["curve"] = "circular"
217
+ params = _attempt(adapter, lambda: list(curve.CircleParams))
218
+ if params and len(params) >= 7:
219
+ entry["center_mm"] = [c * 1000 for c in params[0:3]]
220
+ entry["axis"] = [round(float(c), 6) + 0.0 for c in params[3:6]]
221
+ entry["radius_mm"] = params[6] * 1000
222
+ elif _attempt(adapter, lambda: curve.IsLine(), False):
223
+ entry["curve"] = "linear"
224
+
225
+ points = []
226
+ for getter in ("GetStartVertex", "GetEndVertex"):
227
+ vertex = _attempt(adapter, lambda g=getter: getattr(edge, g)())
228
+ if vertex is None:
229
+ points.append(None)
230
+ continue
231
+ _flag(adapter, vertex, "GetPoint")
232
+ points.append(_attempt(adapter, lambda v=vertex: v.GetPoint()))
233
+ start, end = points
234
+ if start:
235
+ entry["start_mm"] = [c * 1000 for c in start]
236
+ if end:
237
+ entry["end_mm"] = [c * 1000 for c in end]
238
+ if start and end:
239
+ entry["length_mm"] = (
240
+ sum((a - b) ** 2 for a, b in zip(start, end, strict=True)) ** 0.5 * 1000
241
+ )
242
+ return entry
243
+
244
+
245
+ # ---- COM: face resolution + selection -----------------------------------
246
+
247
+
248
+ class FaceEdgeSelector:
249
+ """Resolve a face on the active part and multi-select its edges.
250
+
251
+ Args:
252
+ adapter: A connected ``PyWin32Adapter`` (needs ``currentModel`` and
253
+ ``_attempt``).
254
+ """
255
+
256
+ def __init__(self, adapter: Any) -> None:
257
+ self._adapter = adapter
258
+
259
+ @property
260
+ def _model(self) -> Any:
261
+ return self._adapter.currentModel
262
+
263
+ def faces(self) -> list[FaceInfo]:
264
+ """Return every face of every solid body on the active part."""
265
+ part = self._model
266
+ _flag(self._adapter, part, "GetBodies2")
267
+ bodies = as_list(_attempt(self._adapter, lambda: part.GetBodies2(0, False)))
268
+ out: list[FaceInfo] = []
269
+ for body in bodies:
270
+ _flag(self._adapter, body, "GetFaces")
271
+ for face in _attempt(self._adapter, lambda b=body: b.GetFaces()) or []:
272
+ _flag(self._adapter, face, *_FACE_MEMBERS)
273
+ normal = _attempt(self._adapter, lambda f=face: list(f.Normal)) or [
274
+ 0,
275
+ 0,
276
+ 0,
277
+ ]
278
+ box = (
279
+ _attempt(self._adapter, lambda f=face: list(f.GetBox())) or [0] * 6
280
+ )
281
+ area = _attempt(self._adapter, lambda f=face: f.GetArea()) or 0.0
282
+ out.append(
283
+ FaceInfo(
284
+ index=len(out),
285
+ normal=tuple(float(c) for c in normal[:3]), # type: ignore[arg-type]
286
+ box=tuple(float(c) for c in box[:6]), # type: ignore[arg-type]
287
+ area=float(area),
288
+ handle=face,
289
+ )
290
+ )
291
+ return out
292
+
293
+ def face_at_point(self, faces: list[FaceInfo], point_mm: list[float]) -> FaceInfo:
294
+ """Return the face containing ``point_mm`` (closest within 0.01 mm)."""
295
+ q = tuple(c / 1000.0 for c in point_mm)
296
+ best: tuple[float, FaceInfo] | None = None
297
+ for f in faces:
298
+ closest = _attempt(
299
+ self._adapter, lambda f=f: list(f.handle.GetClosestPointOn(*q))
300
+ )
301
+ if not closest or len(closest) < 3:
302
+ continue
303
+ d = sum((a - b) ** 2 for a, b in zip(closest[:3], q, strict=True)) ** 0.5
304
+ if best is None or d < best[0]:
305
+ best = (d, f)
306
+ if best is None or best[0] > _ON_FACE_M:
307
+ raise LookupError(f"No face found at point {list(point_mm)} mm.")
308
+ return best[1]
309
+
310
+ def plane_geometry(
311
+ self, plane: str
312
+ ) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
313
+ """Return ``(origin, normal)`` of a named reference plane."""
314
+ feature = _attempt(self._adapter, lambda: self._model.FeatureByName(plane))
315
+ if feature is None:
316
+ raise LookupError(f"No reference plane named {plane!r}.")
317
+ _flag(self._adapter, feature, "GetSpecificFeature2")
318
+ ref = _attempt(self._adapter, lambda: feature.GetSpecificFeature2())
319
+ if ref is None:
320
+ raise LookupError(f"{plane!r} is not a reference plane.")
321
+ data = _attempt(self._adapter, lambda: list(ref.Transform.ArrayData))
322
+ if not data or len(data) < 12:
323
+ raise LookupError(f"Could not read the geometry of plane {plane!r}.")
324
+ return plane_from_transform(data)
325
+
326
+ def resolve(
327
+ self, point: list[float] | None, side: str | None, plane: str | None
328
+ ) -> list[FaceInfo]:
329
+ """Resolve the request to one face (or several for ``plane``)."""
330
+ faces = self.faces()
331
+ if not faces:
332
+ raise LookupError("The active part has no solid body faces.")
333
+ if point is not None:
334
+ return [self.face_at_point(faces, point)]
335
+ if side is not None:
336
+ face = pick_side_face(faces, side_vector(side))
337
+ if face is None:
338
+ raise LookupError(f"No planar face faces {side!r}.")
339
+ return [face]
340
+ origin, normal = self.plane_geometry(str(plane))
341
+ on_plane = faces_on_plane(faces, origin, normal)
342
+ if not on_plane:
343
+ raise LookupError(f"No body face lies on {plane!r}.")
344
+ return on_plane
345
+
346
+ def edges(
347
+ self, faces: list[FaceInfo], edge_type: str
348
+ ) -> list[tuple[Any, dict[str, Any]]]:
349
+ """Return ``(edge, description)`` for the faces' edges passing the filter."""
350
+ out = []
351
+ for f in faces:
352
+ for edge in _attempt(self._adapter, lambda f=f: f.handle.GetEdges()) or []:
353
+ info = describe_edge(self._adapter, edge)
354
+ if edge_matches(info["curve"], edge_type):
355
+ info["face_index"] = f.index
356
+ out.append((edge, info))
357
+ return out
358
+
359
+ def select(
360
+ self, edges: list[tuple[Any, dict[str, Any]]], append: bool
361
+ ) -> int | None:
362
+ """Select every edge; return the selection count afterwards.
363
+
364
+ All or nothing: if any edge refuses to select, the selection is
365
+ cleared before raising, so a following ``add_chamfer`` /
366
+ ``add_fillet`` with empty ``edge_names`` cannot silently act on a
367
+ partial set.
368
+
369
+ Raises:
370
+ RuntimeError: When any edge refuses to select.
371
+ """
372
+ model = self._model
373
+ sel_mgr = model.SelectionManager
374
+ _flag(self._adapter, sel_mgr, "CreateSelectData")
375
+ if not append:
376
+ _attempt(self._adapter, lambda: model.ClearSelection2(True))
377
+ select_data = _attempt(self._adapter, lambda: sel_mgr.CreateSelectData())
378
+ failed = []
379
+ for i, (edge, _info) in enumerate(edges):
380
+ _flag(self._adapter, edge, "Select4")
381
+ if not _attempt(
382
+ self._adapter, lambda e=edge: e.Select4(True, select_data), False
383
+ ):
384
+ failed.append(i)
385
+ if failed:
386
+ _attempt(self._adapter, lambda: model.ClearSelection2(True))
387
+ raise RuntimeError(
388
+ f"{len(failed)} of {len(edges)} edge(s) could not be selected "
389
+ f"(indices {failed}); the selection was cleared so nothing acts "
390
+ "on a partial set."
391
+ )
392
+ return selection_count(self._adapter, model)