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,2545 @@
|
|
|
1
|
+
"""Sketch-domain mixin for PyWin32 SolidWorks operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
from .. import sw_type_info as _sw_type_info
|
|
11
|
+
from ..base import AdapterResult, AdapterResultStatus
|
|
12
|
+
|
|
13
|
+
# swConstraintType_e values per the official SolidWorks API enum docs
|
|
14
|
+
# (SolidWorks.Interop.swconst). The legacy IModelDoc2.SketchAddConstraints
|
|
15
|
+
# API takes string identifiers but on SW 2026/3DEXPERIENCE silently no-ops
|
|
16
|
+
# without adding the relation. ISketchRelationManager.AddRelation takes
|
|
17
|
+
# these integer enum values and works reliably.
|
|
18
|
+
RELATION_NAME_MAP: dict[str, int] = {
|
|
19
|
+
"horizontal": 4,
|
|
20
|
+
"vertical": 5,
|
|
21
|
+
"tangent": 6,
|
|
22
|
+
"parallel": 7,
|
|
23
|
+
"perpendicular": 8,
|
|
24
|
+
"coincident": 9,
|
|
25
|
+
"concentric": 10,
|
|
26
|
+
"symmetric": 11, # swConstraintType_SYMMETRIC — requires entity3 (centerline)
|
|
27
|
+
"equal": 14, # swConstraintType_SAMELENGTH
|
|
28
|
+
"fix": 17, # swConstraintType_FIXED
|
|
29
|
+
"collinear": 27, # swConstraintType_COLINEAR (single-l spelling)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Relations that take a third entity (the centerline of symmetry for now).
|
|
33
|
+
# All other relations reject a non-null ``entity3``.
|
|
34
|
+
_THREE_ENTITY_RELATIONS: frozenset[str] = frozenset({"symmetric"})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SolidWorksSketchMixin:
|
|
38
|
+
"""Expose sketch creation and editing methods via mixin-local implementation."""
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def _adapter(obj: Any) -> Any:
|
|
42
|
+
"""Return the runtime adapter object for dynamic attribute access."""
|
|
43
|
+
return cast(Any, obj)
|
|
44
|
+
|
|
45
|
+
def _point_xyz(self, point_obj: Any) -> tuple[float, float, float] | None:
|
|
46
|
+
adapter = self._adapter(self)
|
|
47
|
+
return cast(
|
|
48
|
+
tuple[float, float, float] | None,
|
|
49
|
+
adapter._sketch_geometry.point_xyz(point_obj),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def _set_point_xyz(self, point_obj: Any, x: float, y: float, z: float) -> bool:
|
|
53
|
+
adapter = self._adapter(self)
|
|
54
|
+
return cast(bool, adapter._sketch_geometry.set_point_xyz(point_obj, x, y, z))
|
|
55
|
+
|
|
56
|
+
def _read_segment_endpoints(
|
|
57
|
+
self, entity: Any
|
|
58
|
+
) -> tuple[tuple[float, float, float], tuple[float, float, float]] | None:
|
|
59
|
+
adapter = self._adapter(self)
|
|
60
|
+
return cast(
|
|
61
|
+
tuple[tuple[float, float, float], tuple[float, float, float]] | None,
|
|
62
|
+
adapter._sketch_geometry.read_segment_endpoints(entity),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def _segment_point_objects(self, entity: Any) -> tuple[Any | None, Any | None]:
|
|
66
|
+
adapter = self._adapter(self)
|
|
67
|
+
return cast(
|
|
68
|
+
tuple[Any | None, Any | None],
|
|
69
|
+
adapter._sketch_geometry.segment_point_objects(entity),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _shared_segment_vertex(
|
|
73
|
+
self, entity1: Any, entity2: Any
|
|
74
|
+
) -> tuple[Any, Any, Any] | None:
|
|
75
|
+
adapter = self._adapter(self)
|
|
76
|
+
return cast(
|
|
77
|
+
tuple[Any, Any, Any] | None,
|
|
78
|
+
adapter._sketch_geometry.shared_segment_vertex(entity1, entity2),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def _smart_dimension_direction(self, dx: float, dy: float) -> int:
|
|
82
|
+
adapter = self._adapter(self)
|
|
83
|
+
return cast(int, adapter._sketch_geometry.smart_dimension_direction(dx, dy))
|
|
84
|
+
|
|
85
|
+
def _single_line_dimension_placement(
|
|
86
|
+
self, entity: Any
|
|
87
|
+
) -> tuple[float, float, float, int] | None:
|
|
88
|
+
adapter = self._adapter(self)
|
|
89
|
+
return cast(
|
|
90
|
+
tuple[float, float, float, int] | None,
|
|
91
|
+
adapter._sketch_geometry.single_line_dimension_placement(entity),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def _angular_dimension_placement(
|
|
95
|
+
self, entity1: Any, entity2: Any
|
|
96
|
+
) -> tuple[float, float, float, int] | None:
|
|
97
|
+
adapter = self._adapter(self)
|
|
98
|
+
return cast(
|
|
99
|
+
tuple[float, float, float, int] | None,
|
|
100
|
+
adapter._sketch_geometry.angular_dimension_placement(entity1, entity2),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
async def create_sketch(self, plane: str) -> AdapterResult[str]:
|
|
104
|
+
return _create_sketch_impl(self, plane)
|
|
105
|
+
|
|
106
|
+
async def add_line(
|
|
107
|
+
self, x1: float, y1: float, x2: float, y2: float
|
|
108
|
+
) -> AdapterResult[str]:
|
|
109
|
+
return _add_line_impl(self, x1, y1, x2, y2)
|
|
110
|
+
|
|
111
|
+
async def add_circle(
|
|
112
|
+
self, center_x: float, center_y: float, radius: float
|
|
113
|
+
) -> AdapterResult[str]:
|
|
114
|
+
return _add_circle_impl(self, center_x, center_y, radius)
|
|
115
|
+
|
|
116
|
+
async def add_rectangle(
|
|
117
|
+
self, x1: float, y1: float, x2: float, y2: float
|
|
118
|
+
) -> AdapterResult[str]:
|
|
119
|
+
return _add_rectangle_impl(self, x1, y1, x2, y2)
|
|
120
|
+
|
|
121
|
+
async def add_arc(
|
|
122
|
+
self,
|
|
123
|
+
center_x: float,
|
|
124
|
+
center_y: float,
|
|
125
|
+
start_x: float,
|
|
126
|
+
start_y: float,
|
|
127
|
+
end_x: float,
|
|
128
|
+
end_y: float,
|
|
129
|
+
) -> AdapterResult[str]:
|
|
130
|
+
return _add_arc_impl(self, center_x, center_y, start_x, start_y, end_x, end_y)
|
|
131
|
+
|
|
132
|
+
async def add_spline(self, points: list[dict[str, float]]) -> AdapterResult[str]:
|
|
133
|
+
return _add_spline_impl(self, points)
|
|
134
|
+
|
|
135
|
+
async def add_centerline(
|
|
136
|
+
self, x1: float, y1: float, x2: float, y2: float
|
|
137
|
+
) -> AdapterResult[str]:
|
|
138
|
+
return _add_centerline_impl(self, x1, y1, x2, y2)
|
|
139
|
+
|
|
140
|
+
async def add_polygon(
|
|
141
|
+
self, center_x: float, center_y: float, radius: float, sides: int
|
|
142
|
+
) -> AdapterResult[str]:
|
|
143
|
+
return _add_polygon_impl(self, center_x, center_y, radius, sides)
|
|
144
|
+
|
|
145
|
+
async def add_ellipse(
|
|
146
|
+
self, center_x: float, center_y: float, major_axis: float, minor_axis: float
|
|
147
|
+
) -> AdapterResult[str]:
|
|
148
|
+
return _add_ellipse_impl(self, center_x, center_y, major_axis, minor_axis)
|
|
149
|
+
|
|
150
|
+
async def add_sketch_constraint(
|
|
151
|
+
self,
|
|
152
|
+
entity1: str,
|
|
153
|
+
entity2: str | None,
|
|
154
|
+
relation_type: str,
|
|
155
|
+
entity3: str | None = None,
|
|
156
|
+
) -> AdapterResult[str]:
|
|
157
|
+
return _add_sketch_constraint_impl(
|
|
158
|
+
self, entity1, entity2, relation_type, entity3
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
async def add_sketch_dimension(
|
|
162
|
+
self, entity1: str, entity2: str | None, dimension_type: str, value: float
|
|
163
|
+
) -> AdapterResult[str]:
|
|
164
|
+
return _add_sketch_dimension_impl(self, entity1, entity2, dimension_type, value)
|
|
165
|
+
|
|
166
|
+
async def sketch_linear_pattern(
|
|
167
|
+
self,
|
|
168
|
+
entities: list[str],
|
|
169
|
+
direction_x: float,
|
|
170
|
+
direction_y: float,
|
|
171
|
+
spacing: float,
|
|
172
|
+
count: int,
|
|
173
|
+
) -> AdapterResult[str]:
|
|
174
|
+
return _sketch_linear_pattern_impl(
|
|
175
|
+
self, entities, direction_x, direction_y, spacing, count
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
async def sketch_circular_pattern(
|
|
179
|
+
self,
|
|
180
|
+
entities: list[str],
|
|
181
|
+
angle: float,
|
|
182
|
+
count: int,
|
|
183
|
+
) -> AdapterResult[str]:
|
|
184
|
+
return _sketch_circular_pattern_impl(self, entities, angle, count)
|
|
185
|
+
|
|
186
|
+
async def sketch_mirror(
|
|
187
|
+
self, entities: list[str], mirror_line: str
|
|
188
|
+
) -> AdapterResult[str]:
|
|
189
|
+
return _sketch_mirror_impl(self, entities, mirror_line)
|
|
190
|
+
|
|
191
|
+
async def sketch_offset(
|
|
192
|
+
self, entities: list[str], offset_distance: float, reverse_direction: bool
|
|
193
|
+
) -> AdapterResult[str]:
|
|
194
|
+
return _sketch_offset_impl(self, entities, offset_distance, reverse_direction)
|
|
195
|
+
|
|
196
|
+
async def exit_sketch(self) -> AdapterResult[None]:
|
|
197
|
+
return _exit_sketch_impl(self)
|
|
198
|
+
|
|
199
|
+
async def check_sketch_fully_defined(
|
|
200
|
+
self, sketch_name: str | None = None
|
|
201
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
202
|
+
return _check_sketch_fully_defined_impl(self, sketch_name)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _create_sketch_impl(adapter: Any, plane: str) -> AdapterResult[str]:
|
|
206
|
+
"""Open a new sketch on a named reference plane.
|
|
207
|
+
|
|
208
|
+
The function resolves English short-hand names (``"Top"``, ``"Front"``,
|
|
209
|
+
``"Right"``, ``"XY"``, ``"XZ"``, ``"YZ"``) to their full SolidWorks plane
|
|
210
|
+
names and also tries Spanish locale aliases (``"Planta"``, ``"Alzado"``,
|
|
211
|
+
``"Vista lateral"``). If named lookup fails, ``Extension.SelectByID2``
|
|
212
|
+
with entity type ``"PLANE"`` is attempted with three different callout
|
|
213
|
+
variants.
|
|
214
|
+
|
|
215
|
+
When the sketch is successfully opened, ``adapter.currentSketchManager``,
|
|
216
|
+
``adapter.currentSketch``, ``adapter._sketch_count``, and
|
|
217
|
+
``adapter._last_sketch_name`` are updated, and ``ISketchManager.AddToDB``
|
|
218
|
+
is set so entities land at exactly the requested coordinates
|
|
219
|
+
(``exit_sketch`` clears it again).
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
adapter: A fully connected ``PyWin32Adapter`` with a non-``None``
|
|
223
|
+
``currentModel``.
|
|
224
|
+
plane: Reference-plane identifier. Accepts short English names
|
|
225
|
+
(``"Top"``, ``"Front"``, ``"Right"``, ``"XY"``, ``"XZ"``,
|
|
226
|
+
``"YZ"``) or the exact SolidWorks plane name as it appears in
|
|
227
|
+
the FeatureManager tree.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
AdapterResult[str]: On success, ``data`` is the sketch name string
|
|
231
|
+
(e.g. ``"Sketch1"``). On failure, ``status`` is ``ERROR``.
|
|
232
|
+
|
|
233
|
+
Raises:
|
|
234
|
+
Exception: Propagated through ``_handle_com_operation`` when no plane
|
|
235
|
+
candidate can be selected.
|
|
236
|
+
|
|
237
|
+
Example::
|
|
238
|
+
|
|
239
|
+
from solidworks_mcp.adapters import pywin32_sketch_ops
|
|
240
|
+
|
|
241
|
+
result = pywin32_sketch_ops.create_sketch(adapter, "Front")
|
|
242
|
+
print(result.data) # "Sketch1"
|
|
243
|
+
"""
|
|
244
|
+
if not adapter.currentModel:
|
|
245
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")
|
|
246
|
+
|
|
247
|
+
def _sketch_operation() -> str:
|
|
248
|
+
"""Inner COM closure that selects the plane and opens the sketch.
|
|
249
|
+
|
|
250
|
+
Iterates through all locale aliases and ``SelectByID2`` fallbacks.
|
|
251
|
+
Updates adapter sketch state on success.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
str: The resolved SolidWorks sketch name.
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
Exception: If no plane candidate could be selected.
|
|
258
|
+
"""
|
|
259
|
+
plane_name_map = {
|
|
260
|
+
"Top": "Top Plane",
|
|
261
|
+
"Front": "Front Plane",
|
|
262
|
+
"Right": "Right Plane",
|
|
263
|
+
"XY": "Top Plane",
|
|
264
|
+
"XZ": "Front Plane",
|
|
265
|
+
"YZ": "Right Plane",
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
semantic_plane_aliases = {
|
|
269
|
+
"Top": ["Top Plane", "Planta", "上视基准面", "上視基準面"],
|
|
270
|
+
"Front": ["Front Plane", "Alzado", "前视基准面", "前視基準面"],
|
|
271
|
+
"Right": ["Right Plane", "Vista lateral", "右视基准面", "右視基準面"],
|
|
272
|
+
"XY": ["Top Plane", "Planta", "上视基准面"],
|
|
273
|
+
"XZ": ["Front Plane", "Alzado", "前视基准面"],
|
|
274
|
+
"YZ": ["Right Plane", "Vista lateral", "右视基准面"],
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
actual_plane = plane_name_map.get(plane, plane)
|
|
278
|
+
selected = False
|
|
279
|
+
selection_error = None
|
|
280
|
+
|
|
281
|
+
plane_candidates = [
|
|
282
|
+
*semantic_plane_aliases.get(plane, []),
|
|
283
|
+
actual_plane,
|
|
284
|
+
plane,
|
|
285
|
+
"Top Plane",
|
|
286
|
+
"Front Plane",
|
|
287
|
+
"Right Plane",
|
|
288
|
+
"Planta",
|
|
289
|
+
"Alzado",
|
|
290
|
+
"Vista lateral",
|
|
291
|
+
"上视基准面",
|
|
292
|
+
"前视基准面",
|
|
293
|
+
"右视基准面",
|
|
294
|
+
"上視基準面",
|
|
295
|
+
"前視基準面",
|
|
296
|
+
"右視基準面",
|
|
297
|
+
]
|
|
298
|
+
for candidate in plane_candidates:
|
|
299
|
+
if not candidate:
|
|
300
|
+
continue
|
|
301
|
+
plane_feature, selection_error_candidate = adapter._attempt_with_error(
|
|
302
|
+
lambda c=candidate: adapter.currentModel.FeatureByName(c)
|
|
303
|
+
)
|
|
304
|
+
if selection_error_candidate:
|
|
305
|
+
selection_error = selection_error_candidate
|
|
306
|
+
continue
|
|
307
|
+
selected = bool(
|
|
308
|
+
plane_feature
|
|
309
|
+
and adapter._attempt(
|
|
310
|
+
lambda pf=plane_feature: pf.Select2(False, 0), default=False
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
if selected:
|
|
314
|
+
break
|
|
315
|
+
|
|
316
|
+
if not selected:
|
|
317
|
+
# SW 2022: callout must be None (empty string causes type mismatch)
|
|
318
|
+
selected, selection_error_candidate = adapter._attempt_with_error(
|
|
319
|
+
lambda: adapter.currentModel.Extension.SelectByID2(
|
|
320
|
+
actual_plane,
|
|
321
|
+
"PLANE",
|
|
322
|
+
0,
|
|
323
|
+
0,
|
|
324
|
+
0,
|
|
325
|
+
False,
|
|
326
|
+
0,
|
|
327
|
+
None,
|
|
328
|
+
0,
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
if selection_error_candidate:
|
|
332
|
+
selection_error = selection_error_candidate
|
|
333
|
+
elif selected:
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
if not selected:
|
|
337
|
+
if selection_error:
|
|
338
|
+
raise Exception(
|
|
339
|
+
f"Failed to select plane: {actual_plane} ({selection_error})"
|
|
340
|
+
)
|
|
341
|
+
raise Exception(f"Failed to select plane: {actual_plane}")
|
|
342
|
+
|
|
343
|
+
adapter.currentSketchManager = adapter.currentModel.SketchManager
|
|
344
|
+
adapter._attempt(
|
|
345
|
+
lambda: _sw_type_info.flag_methods(
|
|
346
|
+
adapter.currentSketchManager, "ISketchManager"
|
|
347
|
+
),
|
|
348
|
+
default=0,
|
|
349
|
+
)
|
|
350
|
+
adapter._reset_sketch_entity_registry()
|
|
351
|
+
try:
|
|
352
|
+
adapter.currentSketch = adapter.currentSketchManager.InsertSketch(True)
|
|
353
|
+
except Exception:
|
|
354
|
+
adapter.currentSketch = adapter.currentSketchManager.InsertSketch()
|
|
355
|
+
# Without AddToDB every Create* point snaps to nearby inferred
|
|
356
|
+
# alignments: an end at x=5.2915 landed on x=5 (vertical with an
|
|
357
|
+
# earlier point at x=5) and a 22 mm ring came out 23 mm. Writing
|
|
358
|
+
# straight to the sketch database keeps the requested coordinates.
|
|
359
|
+
# DisplayWhenAdded stays on: it only defers drawing, which would hide
|
|
360
|
+
# entities from a user watching SolidWorks until the next redraw.
|
|
361
|
+
adapter.currentSketchManager.AddToDB = True
|
|
362
|
+
|
|
363
|
+
if not (adapter.currentSketch and hasattr(adapter.currentSketch, "Name")):
|
|
364
|
+
# ISketchManager.ActiveSketch is the documented property for
|
|
365
|
+
# "whatever sketch is open right now" — more reliable than
|
|
366
|
+
# trusting InsertSketch's return value, which some SW versions
|
|
367
|
+
# return as an empty Sub result. Try it before the GetActiveSketch2
|
|
368
|
+
# method fallbacks: a bare ``swApp.ActiveDoc`` is not flagged, so
|
|
369
|
+
# its ``GetActiveSketch2`` resolves to a non-callable and silently
|
|
370
|
+
# yields ``None`` — which forces the synthetic ``Sketch_N``
|
|
371
|
+
# fallback name and makes the sketch unselectable by later
|
|
372
|
+
# features (create_cut_extrude included).
|
|
373
|
+
adapter.currentSketch = (
|
|
374
|
+
adapter._attempt(lambda: adapter.currentSketchManager.ActiveSketch)
|
|
375
|
+
or adapter._attempt(lambda: adapter.currentModel.GetActiveSketch2())
|
|
376
|
+
or adapter._attempt(lambda: adapter.swApp.ActiveDoc.GetActiveSketch2())
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
adapter._sketch_count += 1
|
|
380
|
+
|
|
381
|
+
if adapter.currentSketch and hasattr(adapter.currentSketch, "Name"):
|
|
382
|
+
sketch_name = str(adapter.currentSketch.Name)
|
|
383
|
+
adapter._last_sketch_name = sketch_name
|
|
384
|
+
return sketch_name
|
|
385
|
+
|
|
386
|
+
sketch_name = f"Sketch_{adapter._sketch_count}"
|
|
387
|
+
adapter._last_sketch_name = sketch_name
|
|
388
|
+
return sketch_name
|
|
389
|
+
|
|
390
|
+
return cast(
|
|
391
|
+
AdapterResult[str],
|
|
392
|
+
adapter._handle_com_operation("create_sketch", _sketch_operation),
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _add_line_impl(
|
|
397
|
+
adapter: Any, x1: float, y1: float, x2: float, y2: float
|
|
398
|
+
) -> AdapterResult[str]:
|
|
399
|
+
"""Add a straight line segment to the active sketch.
|
|
400
|
+
|
|
401
|
+
Calls ``SketchManager.CreateLine`` and registers the resulting entity in
|
|
402
|
+
the adapter\'s internal sketch-entity registry so it can be referenced by
|
|
403
|
+
subsequent dimension and constraint calls.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
adapter: A ``PyWin32Adapter`` with an open sketch
|
|
407
|
+
(``currentSketchManager`` must be non-``None``).
|
|
408
|
+
x1: Start X coordinate in **millimetres**.
|
|
409
|
+
y1: Start Y coordinate in **millimetres**.
|
|
410
|
+
x2: End X coordinate in **millimetres**.
|
|
411
|
+
y2: End Y coordinate in **millimetres**.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
AdapterResult[str]: On success, ``data`` is the registered entity ID
|
|
415
|
+
string (e.g. ``"Line_1"``). On failure, ``status`` is ``ERROR``.
|
|
416
|
+
|
|
417
|
+
Raises:
|
|
418
|
+
Exception: Propagated through ``_handle_com_operation`` when
|
|
419
|
+
``CreateLine`` returns ``None``.
|
|
420
|
+
|
|
421
|
+
Example::
|
|
422
|
+
|
|
423
|
+
result = pywin32_sketch_ops.add_line(adapter, 0, 0, 50, 0)
|
|
424
|
+
print(result.data) # "Line_1"
|
|
425
|
+
"""
|
|
426
|
+
if not adapter.currentSketchManager:
|
|
427
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error="No active sketch")
|
|
428
|
+
|
|
429
|
+
def _line_operation() -> str:
|
|
430
|
+
"""Inner COM closure that calls CreateLine and registers the entity.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
str: Registered entity ID for the new line.
|
|
434
|
+
|
|
435
|
+
Raises:
|
|
436
|
+
Exception: If ``CreateLine`` returns ``None``.
|
|
437
|
+
"""
|
|
438
|
+
line = adapter.currentSketchManager.CreateLine(
|
|
439
|
+
x1 / 1000.0, y1 / 1000.0, 0, x2 / 1000.0, y2 / 1000.0, 0
|
|
440
|
+
)
|
|
441
|
+
if not line:
|
|
442
|
+
raise Exception("Failed to create line")
|
|
443
|
+
entity_id: str = adapter._register_sketch_entity("Line", line)
|
|
444
|
+
# The midpoint, in mm: where a dimension to this line is placed.
|
|
445
|
+
adapter._sketch_entity_centers[entity_id] = ((x1 + x2) / 2.0, (y1 + y2) / 2.0)
|
|
446
|
+
return entity_id
|
|
447
|
+
|
|
448
|
+
return cast(
|
|
449
|
+
AdapterResult[str], adapter._handle_com_operation("add_line", _line_operation)
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _add_circle_impl(
|
|
454
|
+
adapter: Any, center_x: float, center_y: float, radius: float
|
|
455
|
+
) -> AdapterResult[str]:
|
|
456
|
+
"""Add a circle to the active sketch defined by centre and radius.
|
|
457
|
+
|
|
458
|
+
Calls ``SketchManager.CreateCircleByRadius`` and registers the entity.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
adapter: A ``PyWin32Adapter`` with an open sketch.
|
|
462
|
+
center_x: Circle centre X coordinate in **millimetres**.
|
|
463
|
+
center_y: Circle centre Y coordinate in **millimetres**.
|
|
464
|
+
radius: Circle radius in **millimetres**.
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
AdapterResult[str]: On success, ``data`` is the registered entity ID
|
|
468
|
+
(e.g. ``"Circle_2"``). On failure, ``status`` is ``ERROR``.
|
|
469
|
+
|
|
470
|
+
Raises:
|
|
471
|
+
Exception: Propagated through ``_handle_com_operation`` when
|
|
472
|
+
``CreateCircleByRadius`` returns ``None``.
|
|
473
|
+
|
|
474
|
+
Example::
|
|
475
|
+
|
|
476
|
+
result = pywin32_sketch_ops.add_circle(adapter, 25.0, 0.0, 10.0)
|
|
477
|
+
print(result.data) # "Circle_2"
|
|
478
|
+
"""
|
|
479
|
+
if not adapter.currentSketchManager:
|
|
480
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error="No active sketch")
|
|
481
|
+
|
|
482
|
+
def _circle_operation() -> str:
|
|
483
|
+
"""Inner COM closure that calls CreateCircleByRadius and registers the entity.
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
str: Registered entity ID for the new circle.
|
|
487
|
+
|
|
488
|
+
Raises:
|
|
489
|
+
Exception: If ``CreateCircleByRadius`` returns ``None``.
|
|
490
|
+
"""
|
|
491
|
+
circle = adapter.currentSketchManager.CreateCircleByRadius(
|
|
492
|
+
center_x / 1000.0, center_y / 1000.0, 0, radius / 1000.0
|
|
493
|
+
)
|
|
494
|
+
if not circle:
|
|
495
|
+
raise Exception("Failed to create circle")
|
|
496
|
+
entity_id: str = adapter._register_sketch_entity("Circle", circle)
|
|
497
|
+
# The centre, in mm: where a dimension to this circle is placed.
|
|
498
|
+
adapter._sketch_entity_centers[entity_id] = (center_x, center_y)
|
|
499
|
+
return entity_id
|
|
500
|
+
|
|
501
|
+
return cast(
|
|
502
|
+
AdapterResult[str],
|
|
503
|
+
adapter._handle_com_operation("add_circle", _circle_operation),
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _add_rectangle_impl(
|
|
508
|
+
adapter: Any, x1: float, y1: float, x2: float, y2: float
|
|
509
|
+
) -> AdapterResult[str]:
|
|
510
|
+
"""Add a corner-defined rectangle to the active sketch.
|
|
511
|
+
|
|
512
|
+
Calls ``SketchManager.CreateCornerRectangle`` which returns an array of
|
|
513
|
+
four line entities. The array is registered as a single composite entity.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
adapter: A ``PyWin32Adapter`` with an open sketch.
|
|
517
|
+
x1: First corner X coordinate in **millimetres**.
|
|
518
|
+
y1: First corner Y coordinate in **millimetres**.
|
|
519
|
+
x2: Opposite corner X coordinate in **millimetres**.
|
|
520
|
+
y2: Opposite corner Y coordinate in **millimetres**.
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
AdapterResult[str]: On success, ``data`` is the composite entity ID
|
|
524
|
+
(e.g. ``"Rectangle_3"``). On failure, ``status`` is ``ERROR``.
|
|
525
|
+
|
|
526
|
+
Raises:
|
|
527
|
+
Exception: Propagated through ``_handle_com_operation`` when
|
|
528
|
+
``CreateCornerRectangle`` returns ``None``.
|
|
529
|
+
|
|
530
|
+
Example::
|
|
531
|
+
|
|
532
|
+
result = pywin32_sketch_ops.add_rectangle(adapter, 0, 0, 40, 20)
|
|
533
|
+
print(result.data) # "Rectangle_3"
|
|
534
|
+
"""
|
|
535
|
+
if not adapter.currentSketchManager:
|
|
536
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error="No active sketch")
|
|
537
|
+
|
|
538
|
+
def _rectangle_operation() -> str:
|
|
539
|
+
"""Inner COM closure that calls CreateCornerRectangle and registers the entity.
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
str: Registered entity ID for the new rectangle.
|
|
543
|
+
|
|
544
|
+
Raises:
|
|
545
|
+
Exception: If ``CreateCornerRectangle`` returns ``None``.
|
|
546
|
+
"""
|
|
547
|
+
lines = adapter.currentSketchManager.CreateCornerRectangle(
|
|
548
|
+
x1 / 1000.0, y1 / 1000.0, 0, x2 / 1000.0, y2 / 1000.0, 0
|
|
549
|
+
)
|
|
550
|
+
if not lines:
|
|
551
|
+
raise Exception("Failed to create rectangle")
|
|
552
|
+
entity_id = cast(str, adapter._register_sketch_entity("Rectangle", lines))
|
|
553
|
+
# Like polygons, rectangles register as a SAFEARRAY tuple of segment
|
|
554
|
+
# handles — no single dispatch ``GetCenterPoint`` to recover the
|
|
555
|
+
# geometric centre from later. Stash it now so the rectangle ID is a
|
|
556
|
+
# valid seed for ``sketch_circular_pattern``.
|
|
557
|
+
adapter._sketch_entity_centers[entity_id] = (
|
|
558
|
+
(x1 + x2) / 2.0,
|
|
559
|
+
(y1 + y2) / 2.0,
|
|
560
|
+
)
|
|
561
|
+
return entity_id
|
|
562
|
+
|
|
563
|
+
return cast(
|
|
564
|
+
AdapterResult[str],
|
|
565
|
+
adapter._handle_com_operation("add_rectangle", _rectangle_operation),
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _add_arc_impl(
|
|
570
|
+
adapter: Any,
|
|
571
|
+
center_x: float,
|
|
572
|
+
center_y: float,
|
|
573
|
+
start_x: float,
|
|
574
|
+
start_y: float,
|
|
575
|
+
end_x: float,
|
|
576
|
+
end_y: float,
|
|
577
|
+
) -> AdapterResult[str]:
|
|
578
|
+
"""Add a circular arc to the active sketch.
|
|
579
|
+
|
|
580
|
+
Calls ``SketchManager.CreateArc`` with a counter-clockwise direction
|
|
581
|
+
flag (``1``). All coordinates must lie in the sketch plane; the Z
|
|
582
|
+
component is always forced to ``0``.
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
adapter: A ``PyWin32Adapter`` with an open sketch.
|
|
586
|
+
center_x: Arc centre X in **millimetres**.
|
|
587
|
+
center_y: Arc centre Y in **millimetres**.
|
|
588
|
+
start_x: Arc start point X in **millimetres**.
|
|
589
|
+
start_y: Arc start point Y in **millimetres**.
|
|
590
|
+
end_x: Arc end point X in **millimetres**.
|
|
591
|
+
end_y: Arc end point Y in **millimetres**.
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
AdapterResult[str]: On success, ``data`` is the registered entity ID
|
|
595
|
+
(e.g. ``"Arc_4"``). On failure, ``status`` is ``ERROR``.
|
|
596
|
+
|
|
597
|
+
Raises:
|
|
598
|
+
Exception: Propagated through ``_handle_com_operation`` when
|
|
599
|
+
``CreateArc`` returns ``None``.
|
|
600
|
+
|
|
601
|
+
Example::
|
|
602
|
+
|
|
603
|
+
result = pywin32_sketch_ops.add_arc(
|
|
604
|
+
adapter,
|
|
605
|
+
center_x=0, center_y=0,
|
|
606
|
+
start_x=10, start_y=0,
|
|
607
|
+
end_x=0, end_y=10,
|
|
608
|
+
)
|
|
609
|
+
print(result.data) # "Arc_4"
|
|
610
|
+
"""
|
|
611
|
+
if not adapter.currentSketchManager:
|
|
612
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error="No active sketch")
|
|
613
|
+
|
|
614
|
+
def _arc_operation() -> str:
|
|
615
|
+
"""Inner COM closure that calls CreateArc and registers the entity.
|
|
616
|
+
|
|
617
|
+
Returns:
|
|
618
|
+
str: Registered entity ID for the new arc.
|
|
619
|
+
|
|
620
|
+
Raises:
|
|
621
|
+
Exception: If ``CreateArc`` returns ``None``.
|
|
622
|
+
"""
|
|
623
|
+
arc = adapter.currentSketchManager.CreateArc(
|
|
624
|
+
center_x / 1000.0,
|
|
625
|
+
center_y / 1000.0,
|
|
626
|
+
0,
|
|
627
|
+
start_x / 1000.0,
|
|
628
|
+
start_y / 1000.0,
|
|
629
|
+
0,
|
|
630
|
+
end_x / 1000.0,
|
|
631
|
+
end_y / 1000.0,
|
|
632
|
+
0,
|
|
633
|
+
1,
|
|
634
|
+
)
|
|
635
|
+
if not arc:
|
|
636
|
+
raise Exception("Failed to create arc")
|
|
637
|
+
return adapter._register_sketch_entity("Arc", arc) # type: ignore[no-any-return]
|
|
638
|
+
|
|
639
|
+
return cast(
|
|
640
|
+
AdapterResult[str], adapter._handle_com_operation("add_arc", _arc_operation)
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _add_spline_impl(
|
|
645
|
+
adapter: Any, points: list[dict[str, float]]
|
|
646
|
+
) -> AdapterResult[str]:
|
|
647
|
+
"""Add a NURBS spline through the supplied control points.
|
|
648
|
+
|
|
649
|
+
Calls ``SketchManager.CreateSpline2(points, simulateNaturalEnds=False)``
|
|
650
|
+
with a flattened XYZ coordinate list. Each point dict must contain
|
|
651
|
+
``"x"`` and ``"y"`` keys; the Z component is forced to ``0``.
|
|
652
|
+
|
|
653
|
+
Args:
|
|
654
|
+
adapter: A ``PyWin32Adapter`` with an open sketch.
|
|
655
|
+
points: Ordered list of control-point dicts with keys ``"x"`` and
|
|
656
|
+
``"y"`` (in **millimetres**). Minimum 2 points required by
|
|
657
|
+
SolidWorks. Example::
|
|
658
|
+
|
|
659
|
+
[{"x": 0, "y": 0}, {"x": 25, "y": 10}, {"x": 50, "y": 0}]
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
AdapterResult[str]: On success, ``data`` is the registered entity
|
|
663
|
+
ID string (e.g. ``"Spline_5"``). On failure, ``status`` is
|
|
664
|
+
``ERROR``.
|
|
665
|
+
|
|
666
|
+
Raises:
|
|
667
|
+
Exception: Propagated through ``_handle_com_operation`` when fewer
|
|
668
|
+
than two points are supplied or ``CreateSpline2`` returns
|
|
669
|
+
``None``.
|
|
670
|
+
|
|
671
|
+
Example::
|
|
672
|
+
|
|
673
|
+
pts = [{"x": 0, "y": 0}, {"x": 20, "y": 15}, {"x": 40, "y": 0}]
|
|
674
|
+
result = pywin32_sketch_ops.add_spline(adapter, pts)
|
|
675
|
+
print(result.data) # "Spline_1"
|
|
676
|
+
"""
|
|
677
|
+
if not adapter.currentSketchManager:
|
|
678
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error="No active sketch")
|
|
679
|
+
|
|
680
|
+
def _spline_operation() -> str:
|
|
681
|
+
if len(points) < 2:
|
|
682
|
+
raise Exception("add_spline requires at least 2 points")
|
|
683
|
+
|
|
684
|
+
spline_points: list[float] = []
|
|
685
|
+
for point in points:
|
|
686
|
+
spline_points.extend([point["x"] / 1000.0, point["y"] / 1000.0, 0.0])
|
|
687
|
+
|
|
688
|
+
# pywin32 late binding unpacks a bare list into N positional
|
|
689
|
+
# VARIANTs, so SolidWorks sees 3*N+1 arguments instead of 2 and
|
|
690
|
+
# rejects the call with DISP_E_BADPARAMCOUNT. Wrap the doubles in a
|
|
691
|
+
# SAFEARRAY VARIANT (VT_ARRAY|VT_R8) — the same lazy-import dance as
|
|
692
|
+
# add_sketch_constraint so non-Windows CI still exercises this path.
|
|
693
|
+
try:
|
|
694
|
+
import pythoncom as _pythoncom
|
|
695
|
+
from win32com.client import VARIANT as _VARIANT
|
|
696
|
+
except ImportError:
|
|
697
|
+
_pythoncom = None
|
|
698
|
+
_VARIANT = None
|
|
699
|
+
|
|
700
|
+
points_arg: Any
|
|
701
|
+
if _pythoncom is not None and _VARIANT is not None:
|
|
702
|
+
points_arg = _VARIANT(_pythoncom.VT_ARRAY | _pythoncom.VT_R8, spline_points)
|
|
703
|
+
elif sys.platform == "win32":
|
|
704
|
+
raise Exception("pywin32 is required for add_spline on Windows")
|
|
705
|
+
else:
|
|
706
|
+
points_arg = spline_points
|
|
707
|
+
|
|
708
|
+
spline = adapter.currentSketchManager.CreateSpline2(points_arg, False)
|
|
709
|
+
if not spline:
|
|
710
|
+
raise Exception("Failed to create spline")
|
|
711
|
+
|
|
712
|
+
# ISketchSpline dispatch does not expose Select4 via IDispatch —
|
|
713
|
+
# GetIDsOfNames returns Member not found. Retrieve the entity as
|
|
714
|
+
# ISketchSegment from the active sketch instead; that interface
|
|
715
|
+
# exposes Select4, which mirror/offset require.
|
|
716
|
+
if adapter.currentSketch is not None:
|
|
717
|
+
adapter._attempt(
|
|
718
|
+
lambda: _sw_type_info.flag_methods(adapter.currentSketch, "ISketch"),
|
|
719
|
+
default=0,
|
|
720
|
+
)
|
|
721
|
+
segments = adapter._attempt(
|
|
722
|
+
lambda: adapter.currentSketch.GetSketchSegments(), default=None
|
|
723
|
+
)
|
|
724
|
+
if segments is not None and len(segments) > 0:
|
|
725
|
+
return cast(
|
|
726
|
+
str, adapter._register_sketch_entity("Spline", segments[-1])
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
return cast(str, adapter._register_sketch_entity("Spline", spline))
|
|
730
|
+
|
|
731
|
+
return cast(
|
|
732
|
+
AdapterResult[str],
|
|
733
|
+
adapter._handle_com_operation("add_spline", _spline_operation),
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _add_centerline_impl(
|
|
738
|
+
adapter: Any, x1: float, y1: float, x2: float, y2: float
|
|
739
|
+
) -> AdapterResult[str]:
|
|
740
|
+
"""Add a construction centre-line to the active sketch.
|
|
741
|
+
|
|
742
|
+
Centre-lines are used as rotation axes for revolve features and as
|
|
743
|
+
mirror lines for symmetric sketches. Calls
|
|
744
|
+
``SketchManager.CreateCenterLine``; the resulting entity is not
|
|
745
|
+
registered in the entity registry because it cannot be dimensioned
|
|
746
|
+
independently.
|
|
747
|
+
|
|
748
|
+
Args:
|
|
749
|
+
adapter: A ``PyWin32Adapter`` with an open sketch.
|
|
750
|
+
x1: Start X in **millimetres**.
|
|
751
|
+
y1: Start Y in **millimetres**.
|
|
752
|
+
x2: End X in **millimetres**.
|
|
753
|
+
y2: End Y in **millimetres**.
|
|
754
|
+
|
|
755
|
+
Returns:
|
|
756
|
+
AdapterResult[str]: On success, ``data`` is a timestamped ID string
|
|
757
|
+
(e.g. ``"Centerline_9341"``). On failure, ``status`` is ``ERROR``.
|
|
758
|
+
|
|
759
|
+
Raises:
|
|
760
|
+
Exception: Propagated through ``_handle_com_operation`` when
|
|
761
|
+
``CreateCenterLine`` returns ``None``.
|
|
762
|
+
|
|
763
|
+
Example::
|
|
764
|
+
|
|
765
|
+
result = pywin32_sketch_ops.add_centerline(adapter, 0, -20, 0, 20)
|
|
766
|
+
print(result.data) # "Centerline_9341"
|
|
767
|
+
"""
|
|
768
|
+
if not adapter.currentSketchManager:
|
|
769
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error="No active sketch")
|
|
770
|
+
|
|
771
|
+
def _centerline_operation() -> str:
|
|
772
|
+
"""Inner COM closure that calls CreateCenterLine and registers
|
|
773
|
+
the resulting entity so it can be referenced by subsequent
|
|
774
|
+
dimension and constraint calls (e.g. as the centerline of a
|
|
775
|
+
``symmetric`` relation).
|
|
776
|
+
|
|
777
|
+
Returns:
|
|
778
|
+
str: Registered entity ID (e.g. ``"Centerline_3"``).
|
|
779
|
+
|
|
780
|
+
Raises:
|
|
781
|
+
Exception: If ``CreateCenterLine`` returns ``None``.
|
|
782
|
+
"""
|
|
783
|
+
centerline = adapter.currentSketchManager.CreateCenterLine(
|
|
784
|
+
x1 / 1000.0, y1 / 1000.0, 0, x2 / 1000.0, y2 / 1000.0, 0
|
|
785
|
+
)
|
|
786
|
+
if not centerline:
|
|
787
|
+
raise Exception("Failed to create centerline")
|
|
788
|
+
return cast(str, adapter._register_sketch_entity("Centerline", centerline))
|
|
789
|
+
|
|
790
|
+
return cast(
|
|
791
|
+
AdapterResult[str],
|
|
792
|
+
adapter._handle_com_operation("add_centerline", _centerline_operation),
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def _add_polygon_impl(
|
|
797
|
+
adapter: Any,
|
|
798
|
+
center_x: float,
|
|
799
|
+
center_y: float,
|
|
800
|
+
radius: float,
|
|
801
|
+
sides: int,
|
|
802
|
+
) -> AdapterResult[str]:
|
|
803
|
+
"""Add a regular polygon inscribed in a circle to the active sketch.
|
|
804
|
+
|
|
805
|
+
Calls ``SketchManager.CreatePolygon(XC, YC, Zc, Xp, Yp, Zp, Sides,
|
|
806
|
+
Inscribed)``. All eight arguments are required by the COM API — passing
|
|
807
|
+
fewer arguments surfaces a pywin32 ``"Parameter not optional."`` error at
|
|
808
|
+
the SOLIDWORKS boundary. The vertex point ``(Xp, Yp, Zp)`` is placed on
|
|
809
|
+
the positive X axis at ``radius`` from centre, which fixes the polygon's
|
|
810
|
+
rotation reproducibly.
|
|
811
|
+
|
|
812
|
+
Args:
|
|
813
|
+
adapter: A ``PyWin32Adapter`` with an open sketch.
|
|
814
|
+
center_x: Polygon centre X in **millimetres**.
|
|
815
|
+
center_y: Polygon centre Y in **millimetres**.
|
|
816
|
+
radius: Circumradius in **millimetres** (distance from centre to each
|
|
817
|
+
vertex). Corresponds to ``CreatePolygon(..., Inscribed=True)``,
|
|
818
|
+
i.e. the polygon is inscribed in a circle of this radius.
|
|
819
|
+
sides: Number of polygon sides. SolidWorks accepts 3–40.
|
|
820
|
+
|
|
821
|
+
Returns:
|
|
822
|
+
AdapterResult[str]: On success, ``data`` is the registered entity ID
|
|
823
|
+
(e.g. ``"Polygon_3"``). The ID is stored in
|
|
824
|
+
``adapter._sketch_entities`` so it can be passed back to
|
|
825
|
+
``sketch_linear_pattern`` / ``sketch_circular_pattern`` /
|
|
826
|
+
``sketch_mirror`` / ``sketch_offset``. On failure, ``status`` is
|
|
827
|
+
``ERROR``.
|
|
828
|
+
|
|
829
|
+
Raises:
|
|
830
|
+
Exception: Propagated through ``_handle_com_operation`` when
|
|
831
|
+
``CreatePolygon`` returns ``None``.
|
|
832
|
+
|
|
833
|
+
Example::
|
|
834
|
+
|
|
835
|
+
result = pywin32_sketch_ops.add_polygon(
|
|
836
|
+
adapter, center_x=0, center_y=0, radius=15.0, sides=6
|
|
837
|
+
)
|
|
838
|
+
print(result.data) # "Polygon_3"
|
|
839
|
+
"""
|
|
840
|
+
if not adapter.currentSketchManager:
|
|
841
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error="No active sketch")
|
|
842
|
+
|
|
843
|
+
def _polygon_operation() -> str:
|
|
844
|
+
"""Inner COM closure that calls CreatePolygon.
|
|
845
|
+
|
|
846
|
+
Returns:
|
|
847
|
+
str: Registered entity ID for the polygon (e.g. ``"Polygon_3"``).
|
|
848
|
+
|
|
849
|
+
Raises:
|
|
850
|
+
Exception: If ``CreatePolygon`` returns ``None``.
|
|
851
|
+
"""
|
|
852
|
+
polygon = adapter.currentSketchManager.CreatePolygon(
|
|
853
|
+
center_x / 1000.0,
|
|
854
|
+
center_y / 1000.0,
|
|
855
|
+
0,
|
|
856
|
+
(center_x + radius) / 1000.0,
|
|
857
|
+
center_y / 1000.0,
|
|
858
|
+
0,
|
|
859
|
+
sides,
|
|
860
|
+
True,
|
|
861
|
+
)
|
|
862
|
+
if not polygon:
|
|
863
|
+
raise Exception("Failed to create polygon")
|
|
864
|
+
# Register so the returned ID is usable by sketch_linear_pattern,
|
|
865
|
+
# sketch_circular_pattern, sketch_mirror, and sketch_offset — without
|
|
866
|
+
# this the polygon string is opaque and every downstream op fails
|
|
867
|
+
# with "Unknown sketch entity 'Polygon_*'".
|
|
868
|
+
entity_id = cast(str, adapter._register_sketch_entity("Polygon", polygon))
|
|
869
|
+
# Polygons register as a SAFEARRAY tuple of segments — there's no
|
|
870
|
+
# single dispatch ``GetCenterPoint`` to recover the center from later.
|
|
871
|
+
# Stash the known center so ``sketch_circular_pattern`` can derive the
|
|
872
|
+
# seed-to-axis offset for polygon seeds.
|
|
873
|
+
adapter._sketch_entity_centers[entity_id] = (center_x, center_y)
|
|
874
|
+
return entity_id
|
|
875
|
+
|
|
876
|
+
return cast(
|
|
877
|
+
AdapterResult[str],
|
|
878
|
+
adapter._handle_com_operation("add_polygon", _polygon_operation),
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def _add_ellipse_impl(
|
|
883
|
+
adapter: Any,
|
|
884
|
+
center_x: float,
|
|
885
|
+
center_y: float,
|
|
886
|
+
major_axis: float,
|
|
887
|
+
minor_axis: float,
|
|
888
|
+
) -> AdapterResult[str]:
|
|
889
|
+
"""Add an axis-aligned ellipse to the active sketch.
|
|
890
|
+
|
|
891
|
+
Calls ``SketchManager.CreateEllipse`` with the major-axis endpoint on the
|
|
892
|
+
positive X direction from the centre and the minor-axis endpoint on the
|
|
893
|
+
positive Y direction. The ellipse is therefore axis-aligned and cannot
|
|
894
|
+
be rotated via this function.
|
|
895
|
+
|
|
896
|
+
The created ellipse is registered in the adapter's sketch-entity
|
|
897
|
+
registry so subsequent constraint and dimension calls can reference it
|
|
898
|
+
by ID, matching the behaviour of ``add_line`` / ``add_circle`` /
|
|
899
|
+
``add_arc`` and the mock adapter.
|
|
900
|
+
|
|
901
|
+
Args:
|
|
902
|
+
adapter: A ``PyWin32Adapter`` with an open sketch.
|
|
903
|
+
center_x: Ellipse centre X in **millimetres**.
|
|
904
|
+
center_y: Ellipse centre Y in **millimetres**.
|
|
905
|
+
major_axis: Full major-axis length in **millimetres** (half is used
|
|
906
|
+
as the offset from centre).
|
|
907
|
+
minor_axis: Full minor-axis length in **millimetres** (half is used
|
|
908
|
+
as the offset from centre).
|
|
909
|
+
|
|
910
|
+
Returns:
|
|
911
|
+
AdapterResult[str]: On success, ``data`` is the registered entity ID
|
|
912
|
+
(e.g. ``"Ellipse_4"``). On failure, ``status`` is ``ERROR``.
|
|
913
|
+
|
|
914
|
+
Raises:
|
|
915
|
+
Exception: Propagated through ``_handle_com_operation`` when
|
|
916
|
+
``CreateEllipse`` returns ``None``.
|
|
917
|
+
|
|
918
|
+
Example::
|
|
919
|
+
|
|
920
|
+
result = pywin32_sketch_ops.add_ellipse(
|
|
921
|
+
adapter, center_x=0, center_y=0, major_axis=30.0, minor_axis=15.0
|
|
922
|
+
)
|
|
923
|
+
print(result.data) # "Ellipse_4"
|
|
924
|
+
"""
|
|
925
|
+
if not adapter.currentSketchManager:
|
|
926
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error="No active sketch")
|
|
927
|
+
|
|
928
|
+
def _ellipse_operation() -> str:
|
|
929
|
+
"""Inner COM closure that calls CreateEllipse and registers the entity.
|
|
930
|
+
|
|
931
|
+
Returns:
|
|
932
|
+
str: Registered entity ID for the new ellipse.
|
|
933
|
+
|
|
934
|
+
Raises:
|
|
935
|
+
Exception: If ``CreateEllipse`` returns ``None``.
|
|
936
|
+
"""
|
|
937
|
+
ellipse = adapter.currentSketchManager.CreateEllipse(
|
|
938
|
+
center_x / 1000.0,
|
|
939
|
+
center_y / 1000.0,
|
|
940
|
+
0,
|
|
941
|
+
(center_x + major_axis / 2) / 1000.0,
|
|
942
|
+
center_y / 1000.0,
|
|
943
|
+
0,
|
|
944
|
+
center_x / 1000.0,
|
|
945
|
+
(center_y + minor_axis / 2) / 1000.0,
|
|
946
|
+
0,
|
|
947
|
+
)
|
|
948
|
+
if not ellipse:
|
|
949
|
+
raise Exception("Failed to create ellipse")
|
|
950
|
+
return cast(str, adapter._register_sketch_entity("Ellipse", ellipse))
|
|
951
|
+
|
|
952
|
+
return cast(
|
|
953
|
+
AdapterResult[str],
|
|
954
|
+
adapter._handle_com_operation("add_ellipse", _ellipse_operation),
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def _add_sketch_constraint_impl(
|
|
959
|
+
adapter: Any,
|
|
960
|
+
entity1: str,
|
|
961
|
+
entity2: str | None,
|
|
962
|
+
relation_type: str,
|
|
963
|
+
entity3: str | None = None,
|
|
964
|
+
) -> AdapterResult[str]:
|
|
965
|
+
"""Add a geometric relation (constraint) between sketch entities.
|
|
966
|
+
|
|
967
|
+
Resolves ``entity1`` (and ``entity2``/``entity3`` if provided) against the
|
|
968
|
+
adapter's sketch-entity registry, then calls
|
|
969
|
+
``ISketchRelationManager.AddRelation(entities, relation_type_enum)`` on
|
|
970
|
+
the active sketch. Entity handles are passed as a
|
|
971
|
+
``VARIANT(VT_ARRAY | VT_DISPATCH, [...])`` — pywin32 will not marshal a
|
|
972
|
+
plain Python list of CDispatch objects to a SAFEARRAY by itself.
|
|
973
|
+
|
|
974
|
+
The legacy ``IModelDoc2.SketchAddConstraints`` API silently no-ops on
|
|
975
|
+
SW 2026/3DEXPERIENCE despite accepting the call, so this implementation
|
|
976
|
+
uses the modern ``ISketchRelationManager.AddRelation`` per the official
|
|
977
|
+
SolidWorks API docs.
|
|
978
|
+
|
|
979
|
+
Supported ``relation_type`` strings (case-insensitive): ``"horizontal"``,
|
|
980
|
+
``"vertical"``, ``"parallel"``, ``"perpendicular"``, ``"tangent"``,
|
|
981
|
+
``"coincident"``, ``"concentric"``, ``"equal"``, ``"symmetric"``,
|
|
982
|
+
``"collinear"``, ``"fix"``.
|
|
983
|
+
|
|
984
|
+
``"symmetric"`` is the only relation that takes a third entity
|
|
985
|
+
(``entity3`` — the centerline of symmetry). All other relations reject
|
|
986
|
+
a non-null ``entity3``.
|
|
987
|
+
|
|
988
|
+
Args:
|
|
989
|
+
adapter: A ``PyWin32Adapter`` with an open sketch and a valid
|
|
990
|
+
``currentModel``.
|
|
991
|
+
entity1: Registered entity ID of the primary sketch entity (from a
|
|
992
|
+
prior ``add_line`` / ``add_circle`` call).
|
|
993
|
+
entity2: Registered entity ID of the secondary sketch entity, or
|
|
994
|
+
``None`` for single-entity relations (horizontal, vertical, fix).
|
|
995
|
+
relation_type: Constraint type string (see above).
|
|
996
|
+
entity3: Registered ID of a third entity. Only meaningful for
|
|
997
|
+
``"symmetric"`` — pass the centerline ID (from ``add_centerline``)
|
|
998
|
+
as the line of symmetry. Must be ``None`` for every other
|
|
999
|
+
relation type.
|
|
1000
|
+
|
|
1001
|
+
Returns:
|
|
1002
|
+
AdapterResult[str]: On success, ``data`` is the registered entity ID
|
|
1003
|
+
of the new constraint object (e.g. ``"Constraint_3"``). On failure,
|
|
1004
|
+
``status`` is ``ERROR``.
|
|
1005
|
+
"""
|
|
1006
|
+
if not adapter.currentSketchManager:
|
|
1007
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error="No active sketch")
|
|
1008
|
+
|
|
1009
|
+
def _constraint_operation() -> str:
|
|
1010
|
+
if not adapter.currentModel:
|
|
1011
|
+
raise Exception("No active model")
|
|
1012
|
+
|
|
1013
|
+
rt_norm = (relation_type or "").strip().lower()
|
|
1014
|
+
relation_type_enum = RELATION_NAME_MAP.get(rt_norm)
|
|
1015
|
+
if relation_type_enum is None:
|
|
1016
|
+
supported = ", ".join(sorted(RELATION_NAME_MAP))
|
|
1017
|
+
raise Exception(
|
|
1018
|
+
f"Unsupported relation type '{relation_type}'. Supported: {supported}"
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
# Arity validation per relation type
|
|
1022
|
+
if rt_norm in _THREE_ENTITY_RELATIONS:
|
|
1023
|
+
if entity2 is None or entity3 is None:
|
|
1024
|
+
raise Exception(
|
|
1025
|
+
f"Relation '{relation_type}' requires entity1, entity2, "
|
|
1026
|
+
"and entity3 (the centerline of symmetry)"
|
|
1027
|
+
)
|
|
1028
|
+
elif entity3 is not None:
|
|
1029
|
+
raise Exception(
|
|
1030
|
+
f"Relation '{relation_type}' does not accept entity3 — only "
|
|
1031
|
+
"'symmetric' takes a third entity (the centerline)"
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
entity1_obj = adapter._sketch_entities.get(entity1)
|
|
1035
|
+
if entity1_obj is None:
|
|
1036
|
+
raise Exception(
|
|
1037
|
+
f"Unknown sketch entity '{entity1}'. Use IDs returned by add_line/add_arc/add_circle/add_spline/add_centerline."
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
entities = [entity1_obj]
|
|
1041
|
+
if entity2:
|
|
1042
|
+
entity2_obj = adapter._sketch_entities.get(entity2)
|
|
1043
|
+
if entity2_obj is None:
|
|
1044
|
+
raise Exception(
|
|
1045
|
+
f"Unknown sketch entity '{entity2}'. Use IDs returned by add_line/add_arc/add_circle/add_spline/add_centerline."
|
|
1046
|
+
)
|
|
1047
|
+
entities.append(entity2_obj)
|
|
1048
|
+
if entity3:
|
|
1049
|
+
entity3_obj = adapter._sketch_entities.get(entity3)
|
|
1050
|
+
if entity3_obj is None:
|
|
1051
|
+
raise Exception(
|
|
1052
|
+
f"Unknown sketch entity '{entity3}'. Use IDs returned by add_line/add_arc/add_circle/add_spline/add_centerline."
|
|
1053
|
+
)
|
|
1054
|
+
entities.append(entity3_obj)
|
|
1055
|
+
|
|
1056
|
+
# Flag IModelDoc2 + ISketch + ISketchRelationManager so late-binding
|
|
1057
|
+
# resolves GetActiveSketch2, RelationManager, and AddRelation as
|
|
1058
|
+
# methods/properties correctly.
|
|
1059
|
+
adapter._attempt(
|
|
1060
|
+
lambda: _sw_type_info.flag_methods(adapter.currentModel, "IModelDoc2"),
|
|
1061
|
+
default=0,
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
active_sketch = adapter._attempt(
|
|
1065
|
+
lambda: adapter.currentModel.GetActiveSketch2(), default=None
|
|
1066
|
+
)
|
|
1067
|
+
if active_sketch is None:
|
|
1068
|
+
raise Exception(
|
|
1069
|
+
"No active sketch on the model — create_sketch first or "
|
|
1070
|
+
"open the existing sketch for edit."
|
|
1071
|
+
)
|
|
1072
|
+
adapter._attempt(
|
|
1073
|
+
lambda: _sw_type_info.flag_methods(active_sketch, "ISketch"),
|
|
1074
|
+
default=0,
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
relmgr = adapter._attempt(lambda: active_sketch.RelationManager, default=None)
|
|
1078
|
+
if relmgr is None:
|
|
1079
|
+
raise Exception("Active sketch has no RelationManager")
|
|
1080
|
+
adapter._attempt(
|
|
1081
|
+
lambda: _sw_type_info.flag_methods(relmgr, "ISketchRelationManager"),
|
|
1082
|
+
default=0,
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
# pywin32 won't auto-marshal a Python list of CDispatch entities to a
|
|
1086
|
+
# SAFEARRAY — the VT_ARRAY|VT_DISPATCH variant is the shape SolidWorks
|
|
1087
|
+
# accepts. Mirror the lazy-import dance used for sw_type_info above so
|
|
1088
|
+
# non-Windows CI (where pywin32 isn't installed) still exercises this
|
|
1089
|
+
# call path; fake adapters used in unit tests accept any sequence.
|
|
1090
|
+
try:
|
|
1091
|
+
import pythoncom as _pythoncom
|
|
1092
|
+
from win32com.client import VARIANT as _VARIANT
|
|
1093
|
+
except ImportError:
|
|
1094
|
+
_pythoncom = None
|
|
1095
|
+
_VARIANT = None
|
|
1096
|
+
|
|
1097
|
+
ents_variant: Any
|
|
1098
|
+
if _pythoncom is not None and _VARIANT is not None:
|
|
1099
|
+
ents_variant = _VARIANT(
|
|
1100
|
+
_pythoncom.VT_ARRAY | _pythoncom.VT_DISPATCH, entities
|
|
1101
|
+
)
|
|
1102
|
+
elif sys.platform == "win32":
|
|
1103
|
+
# On Windows the real adapter feeds entities to a live COM method
|
|
1104
|
+
# that requires the SAFEARRAY shape — fail clearly rather than let
|
|
1105
|
+
# AddRelation surface a low-level "server threw an exception" COM
|
|
1106
|
+
# error from an unwrappable list argument.
|
|
1107
|
+
raise Exception("pywin32 is required for add_sketch_constraint on Windows")
|
|
1108
|
+
else:
|
|
1109
|
+
# Non-Windows: this branch is exercised only by mocked unit tests
|
|
1110
|
+
# whose fake AddRelation accepts any sequence.
|
|
1111
|
+
ents_variant = entities
|
|
1112
|
+
sketch_relation, add_err = adapter._attempt_with_error(
|
|
1113
|
+
lambda: relmgr.AddRelation(ents_variant, relation_type_enum)
|
|
1114
|
+
)
|
|
1115
|
+
if add_err is not None or sketch_relation is None:
|
|
1116
|
+
target = f"'{entity1}'" + (f" and '{entity2}'" if entity2 else "")
|
|
1117
|
+
detail = f": {add_err}" if add_err else ""
|
|
1118
|
+
raise Exception(
|
|
1119
|
+
f"SolidWorks rejected '{relation_type}' relation on {target}{detail}"
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
return cast(
|
|
1123
|
+
str,
|
|
1124
|
+
adapter._register_sketch_entity("Constraint", sketch_relation),
|
|
1125
|
+
)
|
|
1126
|
+
|
|
1127
|
+
return cast(
|
|
1128
|
+
AdapterResult[str],
|
|
1129
|
+
adapter._handle_com_operation("add_sketch_constraint", _constraint_operation),
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
def _add_sketch_dimension_impl(
|
|
1134
|
+
adapter: Any,
|
|
1135
|
+
entity1: str,
|
|
1136
|
+
entity2: str | None,
|
|
1137
|
+
dimension_type: str,
|
|
1138
|
+
value: float,
|
|
1139
|
+
) -> AdapterResult[str]:
|
|
1140
|
+
"""Add a driven dimension to one or two registered sketch entities.
|
|
1141
|
+
|
|
1142
|
+
Supports linear, angular, radial, and diameter dimensions:
|
|
1143
|
+
|
|
1144
|
+
* **linear** — places a horizontal or vertical smart dimension on a single
|
|
1145
|
+
entity. The text-placement point is computed by
|
|
1146
|
+
``adapter._single_line_dimension_placement``.
|
|
1147
|
+
* **angular** — places an angular dimension between two connected line
|
|
1148
|
+
segments sharing a common vertex. The vertex is found via
|
|
1149
|
+
``adapter._shared_segment_vertex`` and multiple direction/segment
|
|
1150
|
+
combinations are tried until one succeeds.
|
|
1151
|
+
* **radial** / **diameter** — places a radius or diameter dimension on a
|
|
1152
|
+
selected sketch arc or circle using ``IModelDoc2.AddRadialDimension2`` or
|
|
1153
|
+
``IModelDoc2.AddDiameterDimension2``.
|
|
1154
|
+
|
|
1155
|
+
SolidWorks can otherwise enter the interactive ``Modify`` approval flow
|
|
1156
|
+
during sketch dimension creation. The adapter keeps the relevant
|
|
1157
|
+
sketch-input preferences disabled for the full automation session in
|
|
1158
|
+
``_ComSessionCoordinator.set_automation_preferences`` and uses the dedicated
|
|
1159
|
+
radial/diameter APIs here because that path is more reliable in unattended
|
|
1160
|
+
COM sessions.
|
|
1161
|
+
|
|
1162
|
+
Dimensional values for **linear** dimensions are in **millimetres**;
|
|
1163
|
+
for **angular** dimensions they are in **degrees**.
|
|
1164
|
+
|
|
1165
|
+
Args:
|
|
1166
|
+
adapter: A ``PyWin32Adapter`` with an open sketch and a valid
|
|
1167
|
+
``currentModel`` and ``swApp``.
|
|
1168
|
+
entity1: Registered entity ID of the primary sketch entity.
|
|
1169
|
+
entity2: Registered entity ID of a secondary sketch entity (required
|
|
1170
|
+
for angular dimensions), or ``None``.
|
|
1171
|
+
dimension_type: ``"linear"``, ``"angular"``, ``"radial"``, or
|
|
1172
|
+
``"diameter"`` (case-insensitive).
|
|
1173
|
+
value: Dimension value. Millimetres for linear, radial, and diameter;
|
|
1174
|
+
degrees for angular.
|
|
1175
|
+
|
|
1176
|
+
Returns:
|
|
1177
|
+
AdapterResult[str]: On success, ``data`` is the registered entity ID
|
|
1178
|
+
of the new dimension object. On failure, ``status`` is ``ERROR``.
|
|
1179
|
+
|
|
1180
|
+
Raises:
|
|
1181
|
+
Exception: Propagated through ``_handle_com_operation`` when the
|
|
1182
|
+
entity IDs cannot be resolved, the placement cannot be computed,
|
|
1183
|
+
or the SolidWorks ``AddDimension`` call fails.
|
|
1184
|
+
|
|
1185
|
+
Example::
|
|
1186
|
+
|
|
1187
|
+
# Dimension a line to 50 mm
|
|
1188
|
+
result = pywin32_sketch_ops.add_sketch_dimension(
|
|
1189
|
+
adapter, "Line_1", None, "linear", 50.0
|
|
1190
|
+
)
|
|
1191
|
+
print(result.data) # "Dimension_7"
|
|
1192
|
+
|
|
1193
|
+
# Angular dimension between two intersecting lines
|
|
1194
|
+
result = pywin32_sketch_ops.add_sketch_dimension(
|
|
1195
|
+
adapter, "Line_1", "Line_2", "angular", 45.0
|
|
1196
|
+
)
|
|
1197
|
+
"""
|
|
1198
|
+
if not adapter.currentSketchManager:
|
|
1199
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error="No active sketch")
|
|
1200
|
+
|
|
1201
|
+
def _dimension_operation() -> str:
|
|
1202
|
+
"""Inner COM closure that resolves entities, places and values the dimension.
|
|
1203
|
+
|
|
1204
|
+
Resolves entity objects from the adapter registry, computes the
|
|
1205
|
+
placement point, suppresses the interactive value dialog, calls
|
|
1206
|
+
``AddDimension``, then sets the dimensional value via
|
|
1207
|
+
``SetSystemValue3`` / ``SetSystemValue2`` / ``SystemValue`` fallbacks.
|
|
1208
|
+
|
|
1209
|
+
Returns:
|
|
1210
|
+
str: Registered entity ID for the new dimension display object.
|
|
1211
|
+
|
|
1212
|
+
Raises:
|
|
1213
|
+
Exception: If entity IDs are unknown, placement cannot be
|
|
1214
|
+
determined, or the COM call fails.
|
|
1215
|
+
"""
|
|
1216
|
+
import math as _math_dim
|
|
1217
|
+
|
|
1218
|
+
# Re-assert per-call: SolidWorks resets swInputDimValOnCreate (10)
|
|
1219
|
+
# when entering a new sketch, so the global connect-time suppression
|
|
1220
|
+
# is not sufficient. Setting it to False here prevents the Modify
|
|
1221
|
+
# dialog from blocking unattended automation.
|
|
1222
|
+
_sw = getattr(adapter, "swApp", None)
|
|
1223
|
+
if _sw:
|
|
1224
|
+
adapter._attempt(lambda: _sw.SetUserPreferenceToggle(10, False))
|
|
1225
|
+
adapter._attempt(lambda: _sw.SetUserPreferenceToggle(372, False))
|
|
1226
|
+
adapter._attempt(lambda: _sw.SetUserPreferenceToggle(520, False))
|
|
1227
|
+
|
|
1228
|
+
def _radial_dimension_placement() -> tuple[float, float, float, int]:
|
|
1229
|
+
"""Compute a deterministic placement for radial/diameter dimensions.
|
|
1230
|
+
|
|
1231
|
+
Avoids additional COM geometry reads that can block on some live sessions.
|
|
1232
|
+
"""
|
|
1233
|
+
offset = max(0.01, min(0.05, abs(value) / 1000.0 + 0.01))
|
|
1234
|
+
return (
|
|
1235
|
+
offset,
|
|
1236
|
+
offset,
|
|
1237
|
+
0.0,
|
|
1238
|
+
adapter.constants["swSmartDimensionDirectionUp"],
|
|
1239
|
+
)
|
|
1240
|
+
|
|
1241
|
+
if not adapter.currentModel:
|
|
1242
|
+
return f"Dimension_{int(time.time() * 1000) % 10000}"
|
|
1243
|
+
|
|
1244
|
+
entity1_obj = adapter._sketch_entities.get(entity1)
|
|
1245
|
+
if entity1_obj is None:
|
|
1246
|
+
raise Exception(
|
|
1247
|
+
f"Unknown sketch entity '{entity1}'. Use IDs returned by add_line/add_arc/add_circle/add_spline/add_centerline."
|
|
1248
|
+
)
|
|
1249
|
+
|
|
1250
|
+
entity2_obj = None
|
|
1251
|
+
if entity2:
|
|
1252
|
+
entity2_obj = adapter._sketch_entities.get(entity2)
|
|
1253
|
+
if entity2_obj is None:
|
|
1254
|
+
raise Exception(
|
|
1255
|
+
f"Unknown sketch entity '{entity2}'. Use IDs returned by add_line/add_arc/add_circle/add_spline/add_centerline."
|
|
1256
|
+
)
|
|
1257
|
+
|
|
1258
|
+
dim_type = (dimension_type or "linear").strip().lower()
|
|
1259
|
+
placement = None
|
|
1260
|
+
if dim_type == "angular" and entity2_obj is not None:
|
|
1261
|
+
placement = adapter._angular_dimension_placement(entity1_obj, entity2_obj)
|
|
1262
|
+
elif dim_type == "linear":
|
|
1263
|
+
placement = adapter._single_line_dimension_placement(entity1_obj)
|
|
1264
|
+
elif dim_type in {"radial", "diameter"}:
|
|
1265
|
+
placement = _radial_dimension_placement()
|
|
1266
|
+
|
|
1267
|
+
if placement is None:
|
|
1268
|
+
raise Exception(
|
|
1269
|
+
f"Unsupported or ambiguous dimension placement for type '{dim_type}'"
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
text_x, text_y, text_z, direction = placement
|
|
1273
|
+
|
|
1274
|
+
def _try_create_angular_dimension() -> Any:
|
|
1275
|
+
"""Attempt to create an angular dimension between two line segments.
|
|
1276
|
+
|
|
1277
|
+
Finds the shared vertex between ``entity1_obj`` and
|
|
1278
|
+
``entity2_obj``, then iterates through every (segment, vertex,
|
|
1279
|
+
direction) combination until SolidWorks accepts the placement.
|
|
1280
|
+
|
|
1281
|
+
Returns:
|
|
1282
|
+
Any: The SolidWorks display-dimension COM object on success,
|
|
1283
|
+
or ``None`` if all attempts fail.
|
|
1284
|
+
"""
|
|
1285
|
+
shared_vertex = adapter._shared_segment_vertex(entity1_obj, entity2_obj)
|
|
1286
|
+
if shared_vertex is None:
|
|
1287
|
+
return None
|
|
1288
|
+
|
|
1289
|
+
_, vertex1, vertex2 = shared_vertex
|
|
1290
|
+
segment_attempts = ((entity1_obj, vertex1), (entity2_obj, vertex2))
|
|
1291
|
+
direction_attempts = [direction] + [
|
|
1292
|
+
candidate
|
|
1293
|
+
for candidate in (
|
|
1294
|
+
adapter.constants["swSmartDimensionDirectionRight"],
|
|
1295
|
+
adapter.constants["swSmartDimensionDirectionUp"],
|
|
1296
|
+
adapter.constants["swSmartDimensionDirectionLeft"],
|
|
1297
|
+
adapter.constants["swSmartDimensionDirectionDown"],
|
|
1298
|
+
)
|
|
1299
|
+
if candidate != direction
|
|
1300
|
+
]
|
|
1301
|
+
|
|
1302
|
+
for segment_obj, vertex_obj in segment_attempts:
|
|
1303
|
+
for candidate_direction in direction_attempts:
|
|
1304
|
+
adapter.currentModel.ClearSelection2(True)
|
|
1305
|
+
if not adapter._select_sketch_entity(segment_obj, append=False):
|
|
1306
|
+
continue
|
|
1307
|
+
if not adapter._attempt(
|
|
1308
|
+
lambda vo=vertex_obj: bool(vo.Select2(True, 0)), default=False
|
|
1309
|
+
):
|
|
1310
|
+
continue
|
|
1311
|
+
display_dim = adapter._attempt(
|
|
1312
|
+
lambda d=candidate_direction: (
|
|
1313
|
+
adapter.currentModel.Extension.AddDimension(
|
|
1314
|
+
text_x, text_y, text_z, d
|
|
1315
|
+
)
|
|
1316
|
+
),
|
|
1317
|
+
default=None,
|
|
1318
|
+
)
|
|
1319
|
+
if display_dim:
|
|
1320
|
+
return display_dim
|
|
1321
|
+
return None
|
|
1322
|
+
|
|
1323
|
+
if dim_type == "angular":
|
|
1324
|
+
display_dim = _try_create_angular_dimension()
|
|
1325
|
+
elif dim_type == "linear" and entity2_obj is not None:
|
|
1326
|
+
adapter.currentModel.ClearSelection2(True)
|
|
1327
|
+
if not adapter._select_sketch_entity(entity1_obj, append=False):
|
|
1328
|
+
raise Exception(f"Failed to select primary entity '{entity1}'")
|
|
1329
|
+
if not adapter._select_sketch_entity(entity2_obj, append=True):
|
|
1330
|
+
raise Exception(f"Failed to select secondary entity '{entity2}'")
|
|
1331
|
+
display_dim = _two_entity_linear_dimension(
|
|
1332
|
+
adapter, (entity1, entity2), entity1_obj, entity2_obj, direction
|
|
1333
|
+
)
|
|
1334
|
+
else:
|
|
1335
|
+
adapter.currentModel.ClearSelection2(True)
|
|
1336
|
+
if not adapter._select_sketch_entity(entity1_obj, append=False):
|
|
1337
|
+
raise Exception(f"Failed to select primary entity '{entity1}'")
|
|
1338
|
+
if entity2_obj is not None and not adapter._select_sketch_entity(
|
|
1339
|
+
entity2_obj, append=True
|
|
1340
|
+
):
|
|
1341
|
+
raise Exception(f"Failed to select secondary entity '{entity2}'")
|
|
1342
|
+
|
|
1343
|
+
if dim_type == "radial":
|
|
1344
|
+
display_dim = adapter._attempt(
|
|
1345
|
+
lambda: adapter.currentModel.AddRadialDimension2(
|
|
1346
|
+
text_x, text_y, text_z
|
|
1347
|
+
),
|
|
1348
|
+
default=None,
|
|
1349
|
+
)
|
|
1350
|
+
elif dim_type == "diameter":
|
|
1351
|
+
display_dim = adapter._attempt(
|
|
1352
|
+
lambda: adapter.currentModel.AddDiameterDimension2(
|
|
1353
|
+
text_x, text_y, text_z
|
|
1354
|
+
),
|
|
1355
|
+
default=None,
|
|
1356
|
+
)
|
|
1357
|
+
else:
|
|
1358
|
+
# Use a single deterministic AddDimension call for non-angular
|
|
1359
|
+
# dimensions that require extension-line direction.
|
|
1360
|
+
display_dim = adapter._attempt(
|
|
1361
|
+
lambda: adapter.currentModel.Extension.AddDimension(
|
|
1362
|
+
text_x, text_y, text_z, direction
|
|
1363
|
+
),
|
|
1364
|
+
default=None,
|
|
1365
|
+
)
|
|
1366
|
+
|
|
1367
|
+
if not display_dim:
|
|
1368
|
+
raise Exception("SolidWorks failed to create sketch dimension")
|
|
1369
|
+
|
|
1370
|
+
if dim_type == "angular":
|
|
1371
|
+
value_si = value * _math_dim.pi / 180.0
|
|
1372
|
+
else:
|
|
1373
|
+
value_si = value / 1000.0
|
|
1374
|
+
|
|
1375
|
+
dim_obj = (
|
|
1376
|
+
adapter._attempt(lambda: display_dim.GetDimension2(0), default=None)
|
|
1377
|
+
or adapter._attempt(lambda: display_dim.GetDimension(), default=None)
|
|
1378
|
+
or display_dim
|
|
1379
|
+
)
|
|
1380
|
+
if (
|
|
1381
|
+
adapter._attempt(
|
|
1382
|
+
lambda: dim_obj.SetSystemValue3(value_si, 1, None), default=None
|
|
1383
|
+
)
|
|
1384
|
+
is None
|
|
1385
|
+
):
|
|
1386
|
+
if (
|
|
1387
|
+
adapter._attempt(
|
|
1388
|
+
lambda: dim_obj.SetSystemValue2(value_si, 1), default=None
|
|
1389
|
+
)
|
|
1390
|
+
is None
|
|
1391
|
+
):
|
|
1392
|
+
if hasattr(dim_obj, "SystemValue"):
|
|
1393
|
+
dim_obj.SystemValue = value_si
|
|
1394
|
+
|
|
1395
|
+
return adapter._register_sketch_entity("Dimension", display_dim) # type: ignore[no-any-return]
|
|
1396
|
+
|
|
1397
|
+
return cast(
|
|
1398
|
+
AdapterResult[str],
|
|
1399
|
+
adapter._handle_com_operation("add_sketch_dimension", _dimension_operation),
|
|
1400
|
+
)
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
def _sketch_point_xyz(
|
|
1404
|
+
adapter: Any, entity_id: str, entity: Any
|
|
1405
|
+
) -> tuple[float, float, float] | None:
|
|
1406
|
+
"""A sketch entity's reference point in metres: a circle's centre, a line's midpoint.
|
|
1407
|
+
|
|
1408
|
+
Lines and circles record that point (in mm) when they are created, so it
|
|
1409
|
+
is read from ``_sketch_entity_centers`` without a COM call. Reading it
|
|
1410
|
+
back from SolidWorks returned nothing on SW 2026; that read stays only as
|
|
1411
|
+
the fallback for entities that record no point.
|
|
1412
|
+
|
|
1413
|
+
Args:
|
|
1414
|
+
adapter: A ``PyWin32Adapter``.
|
|
1415
|
+
entity_id: The entity's registered ID.
|
|
1416
|
+
entity: The registered ``ISketchArc`` or ``ISketchLine`` dispatch.
|
|
1417
|
+
|
|
1418
|
+
Returns:
|
|
1419
|
+
tuple[float, float, float] | None: The point, or ``None`` when it
|
|
1420
|
+
cannot be read.
|
|
1421
|
+
"""
|
|
1422
|
+
cached = adapter._sketch_entity_centers.get(entity_id)
|
|
1423
|
+
if cached is not None:
|
|
1424
|
+
return (cached[0] / 1000.0, cached[1] / 1000.0, 0.0)
|
|
1425
|
+
adapter._attempt(
|
|
1426
|
+
lambda: _sw_type_info.flag_methods(entity, "ISketchArc"), default=0
|
|
1427
|
+
)
|
|
1428
|
+
centre = adapter._attempt(lambda: entity.GetCenterPoint(), default=None)
|
|
1429
|
+
if isinstance(centre, (list, tuple)) and len(centre) >= 3:
|
|
1430
|
+
return (float(centre[0]), float(centre[1]), float(centre[2]))
|
|
1431
|
+
ends = adapter._attempt(
|
|
1432
|
+
lambda: adapter._sketch_geometry.read_segment_endpoints(entity), default=None
|
|
1433
|
+
)
|
|
1434
|
+
if ends is None:
|
|
1435
|
+
return None
|
|
1436
|
+
(x1, y1, z1), (x2, y2, z2) = ends
|
|
1437
|
+
return ((x1 + x2) / 2.0, (y1 + y2) / 2.0, (z1 + z2) / 2.0)
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
#: How far past a line's end a dimension to it is placed, in metres.
|
|
1441
|
+
_DIMENSION_CLEARANCE_M = 0.008
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
def _text_beside_line(
|
|
1445
|
+
adapter: Any, line: Any, point: tuple[float, float, float]
|
|
1446
|
+
) -> tuple[float, float, float] | None:
|
|
1447
|
+
"""Where to put a line-to-point dimension so it sits outside the outline.
|
|
1448
|
+
|
|
1449
|
+
Midway between the line and the point along the measurement, then moved
|
|
1450
|
+
along the line past its end nearest the point, so the dimensions to the
|
|
1451
|
+
holes of a plate stand clear of the plate instead of piling up inside it.
|
|
1452
|
+
|
|
1453
|
+
Args:
|
|
1454
|
+
adapter: A ``PyWin32Adapter``.
|
|
1455
|
+
line: The ``ISketchLine`` dispatch.
|
|
1456
|
+
point: The other entity's point, in metres.
|
|
1457
|
+
|
|
1458
|
+
Returns:
|
|
1459
|
+
tuple[float, float, float] | None: The text position, or ``None``
|
|
1460
|
+
when the line's ends cannot be read.
|
|
1461
|
+
"""
|
|
1462
|
+
ends = adapter._attempt(
|
|
1463
|
+
lambda: adapter._sketch_geometry.read_segment_endpoints(line), default=None
|
|
1464
|
+
)
|
|
1465
|
+
if ends is None:
|
|
1466
|
+
return None
|
|
1467
|
+
(ax, ay, _az), (bx, by, _bz) = ends
|
|
1468
|
+
length = ((bx - ax) ** 2 + (by - ay) ** 2) ** 0.5
|
|
1469
|
+
if length <= 1e-9:
|
|
1470
|
+
return None
|
|
1471
|
+
ux, uy = (bx - ax) / length, (by - ay) / length
|
|
1472
|
+
along = (point[0] - ax) * ux + (point[1] - ay) * uy
|
|
1473
|
+
foot = (ax + along * ux, ay + along * uy)
|
|
1474
|
+
mid = ((foot[0] + point[0]) / 2.0, (foot[1] + point[1]) / 2.0)
|
|
1475
|
+
shift = (
|
|
1476
|
+
-along - _DIMENSION_CLEARANCE_M
|
|
1477
|
+
if along < length / 2.0
|
|
1478
|
+
else length - along + _DIMENSION_CLEARANCE_M
|
|
1479
|
+
)
|
|
1480
|
+
return (mid[0] + shift * ux, mid[1] + shift * uy, point[2])
|
|
1481
|
+
|
|
1482
|
+
|
|
1483
|
+
def _two_entity_linear_dimension(
|
|
1484
|
+
adapter: Any, ids: tuple[str, str], entity1: Any, entity2: Any, direction: int
|
|
1485
|
+
) -> Any:
|
|
1486
|
+
"""A linear dimension between two sketch entities, e.g. a line and a circle.
|
|
1487
|
+
|
|
1488
|
+
The text goes beside a line, clear of the outline (see
|
|
1489
|
+
:func:`_text_beside_line`), or midway between the two entities when the
|
|
1490
|
+
first is not a line. ``Extension.AddDimension`` with the
|
|
1491
|
+
first entity's single-line placement returned nothing for a line and a
|
|
1492
|
+
circle on SW 2026, so the calls are tried in turn until one returns a
|
|
1493
|
+
display dimension: ``IModelDoc2::AddDimension2`` with both entities
|
|
1494
|
+
selected (what the macro recorder writes for a smart dimension), the same
|
|
1495
|
+
with the circle's centre point selected, then ``Extension.AddDimension``.
|
|
1496
|
+
|
|
1497
|
+
Args:
|
|
1498
|
+
adapter: A ``PyWin32Adapter`` with an open sketch.
|
|
1499
|
+
ids: The two entities' registered IDs.
|
|
1500
|
+
entity1: The first entity, usually a line.
|
|
1501
|
+
entity2: The second entity, usually a circle.
|
|
1502
|
+
direction: ``swSmartDimensionDirection_e`` for the last attempt.
|
|
1503
|
+
|
|
1504
|
+
Returns:
|
|
1505
|
+
Any: The display dimension, or ``None`` when every attempt failed
|
|
1506
|
+
(an attempt whose entities cannot be selected is skipped). The caller
|
|
1507
|
+
has already checked that both entities can be selected.
|
|
1508
|
+
|
|
1509
|
+
Raises:
|
|
1510
|
+
Exception: When the entities' positions cannot be read.
|
|
1511
|
+
"""
|
|
1512
|
+
model = adapter.currentModel
|
|
1513
|
+
points = [
|
|
1514
|
+
_sketch_point_xyz(adapter, i, e)
|
|
1515
|
+
for i, e in zip(ids, (entity1, entity2), strict=True)
|
|
1516
|
+
]
|
|
1517
|
+
if points[0] is None or points[1] is None:
|
|
1518
|
+
raise Exception("Cannot read where the two entities are to place the dimension")
|
|
1519
|
+
text = _text_beside_line(adapter, entity1, points[1]) or tuple(
|
|
1520
|
+
(a + b) / 2.0 for a, b in zip(points[0], points[1], strict=True)
|
|
1521
|
+
)
|
|
1522
|
+
centre = adapter._attempt(
|
|
1523
|
+
lambda: adapter._get_attr_or_call(entity2, "GetCenterPoint2"), default=None
|
|
1524
|
+
)
|
|
1525
|
+
|
|
1526
|
+
def _selected(second: Any) -> bool:
|
|
1527
|
+
adapter._attempt(lambda: model.ClearSelection2(True), default=None)
|
|
1528
|
+
return bool(
|
|
1529
|
+
adapter._select_sketch_entity(entity1, append=False)
|
|
1530
|
+
and adapter._select_sketch_entity(second, append=True)
|
|
1531
|
+
)
|
|
1532
|
+
|
|
1533
|
+
attempts = [
|
|
1534
|
+
(entity2, lambda: model.AddDimension2(*text)),
|
|
1535
|
+
*([(centre, lambda: model.AddDimension2(*text))] if centre is not None else []),
|
|
1536
|
+
(entity2, lambda: model.Extension.AddDimension(*text, direction)),
|
|
1537
|
+
]
|
|
1538
|
+
for second, create in attempts:
|
|
1539
|
+
if not _selected(second):
|
|
1540
|
+
continue
|
|
1541
|
+
display_dim = adapter._attempt(create, default=None)
|
|
1542
|
+
if display_dim:
|
|
1543
|
+
return display_dim
|
|
1544
|
+
return None
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
def _select_sketch_entities(adapter: Any, entity_ids: list[str], mark: int) -> None:
|
|
1548
|
+
"""Select sketch entities from the registry under a specific mark.
|
|
1549
|
+
|
|
1550
|
+
Resolves each ID against ``adapter._sketch_entities`` and calls
|
|
1551
|
+
``ISketchSegment.Select4(Append=True, Data)`` on each — with ``Data``
|
|
1552
|
+
being a configured ``ISelectData`` carrying the requested ``mark`` so
|
|
1553
|
+
SolidWorks knows how to interpret the selection (e.g. mark=1 sketch
|
|
1554
|
+
segments + mark=2 centerline for ``SketchMirror``).
|
|
1555
|
+
|
|
1556
|
+
The ``ISelectionMgr`` dispatch needs ``sw_type_info.flag_methods``
|
|
1557
|
+
flagging or pywin32 late binding cannot resolve ``CreateSelectData``
|
|
1558
|
+
and surfaces ``"Member not found."`` from the COM boundary. This
|
|
1559
|
+
matches the lazy-import dance used by ``add_sketch_constraint``;
|
|
1560
|
+
when ``sw_type_info`` cannot be imported the flagging step is skipped
|
|
1561
|
+
but ``ISelectionMgr.CreateSelectData`` and ``ISketchSegment.Select4``
|
|
1562
|
+
are still invoked. Callers must therefore not invoke this helper
|
|
1563
|
+
without a live ``ISelectionMgr`` on ``adapter.currentModel``.
|
|
1564
|
+
|
|
1565
|
+
Args:
|
|
1566
|
+
adapter: A ``PyWin32Adapter``. ``adapter.currentModel`` must be a
|
|
1567
|
+
live ``IModelDoc2`` dispatch.
|
|
1568
|
+
entity_ids: Registry IDs returned by ``add_line`` / ``add_arc`` /
|
|
1569
|
+
etc. Must be non-empty; resolution failure raises.
|
|
1570
|
+
mark: ``ISelectData.Mark`` value applied to every selection.
|
|
1571
|
+
|
|
1572
|
+
Raises:
|
|
1573
|
+
Exception: If an entity ID is not in the registry or a Select4
|
|
1574
|
+
call returns ``False``.
|
|
1575
|
+
"""
|
|
1576
|
+
sel_mgr = adapter.currentModel.SelectionManager
|
|
1577
|
+
adapter._attempt(
|
|
1578
|
+
lambda: _sw_type_info.flag_methods(sel_mgr, "ISelectionMgr"),
|
|
1579
|
+
default=0,
|
|
1580
|
+
)
|
|
1581
|
+
select_data = sel_mgr.CreateSelectData()
|
|
1582
|
+
select_data.Mark = mark
|
|
1583
|
+
for ent_id in entity_ids:
|
|
1584
|
+
entity = adapter._sketch_entities.get(ent_id)
|
|
1585
|
+
if entity is None:
|
|
1586
|
+
raise Exception(
|
|
1587
|
+
f"Unknown sketch entity '{ent_id}'. Use IDs returned by "
|
|
1588
|
+
"add_line/add_arc/add_circle/add_spline/add_centerline."
|
|
1589
|
+
)
|
|
1590
|
+
# ``ISketchManager::CreatePolygon`` returns the polygon's edges as
|
|
1591
|
+
# a SAFEARRAY, which pywin32 unmarshals to a tuple of
|
|
1592
|
+
# ``ISketchSegment`` handles — there is no single COM object to
|
|
1593
|
+
# ``Select4`` on. Treat any iterable (tuple/list) as a group of
|
|
1594
|
+
# segments and select each, so a polygon ID can flow into
|
|
1595
|
+
# sketch_linear_pattern / sketch_circular_pattern / sketch_mirror /
|
|
1596
|
+
# sketch_offset the same as any other entity. Single-segment
|
|
1597
|
+
# entities (lines, arcs, splines, ellipses, centerlines) keep the
|
|
1598
|
+
# original Select4 path.
|
|
1599
|
+
if isinstance(entity, (list, tuple)):
|
|
1600
|
+
for segment in entity:
|
|
1601
|
+
adapter._attempt(
|
|
1602
|
+
lambda s=segment: _sw_type_info.flag_methods(s, "ISketchSegment"),
|
|
1603
|
+
default=0,
|
|
1604
|
+
)
|
|
1605
|
+
ok = segment.Select4(True, select_data)
|
|
1606
|
+
if not ok:
|
|
1607
|
+
raise Exception(
|
|
1608
|
+
f"Failed to select segment of sketch entity '{ent_id}'"
|
|
1609
|
+
)
|
|
1610
|
+
else:
|
|
1611
|
+
adapter._attempt(
|
|
1612
|
+
lambda e=entity: _sw_type_info.flag_methods(e, "ISketchSegment"),
|
|
1613
|
+
default=0,
|
|
1614
|
+
)
|
|
1615
|
+
ok = entity.Select4(True, select_data)
|
|
1616
|
+
if not ok:
|
|
1617
|
+
raise Exception(f"Failed to select sketch entity '{ent_id}'")
|
|
1618
|
+
|
|
1619
|
+
|
|
1620
|
+
def _sketch_linear_pattern_impl(
|
|
1621
|
+
adapter: Any,
|
|
1622
|
+
entities: list[str],
|
|
1623
|
+
direction_x: float,
|
|
1624
|
+
direction_y: float,
|
|
1625
|
+
spacing: float,
|
|
1626
|
+
count: int,
|
|
1627
|
+
) -> AdapterResult[str]:
|
|
1628
|
+
"""Create a linear sketch pattern from the registered seed entities.
|
|
1629
|
+
|
|
1630
|
+
Selects ``entities`` then calls
|
|
1631
|
+
``ISketchManager::CreateLinearSketchStepAndRepeat(NumX, NumY, SpacingX,
|
|
1632
|
+
SpacingY, AngleX, AngleY, DeleteInstances, XSpacingDim, YSpacingDim,
|
|
1633
|
+
AngleDim, CreateNumOfInstancesDimInXDir, CreateNumOfInstancesDimInYDir)``.
|
|
1634
|
+
The COM API expects spacing in metres and angles in radians; this
|
|
1635
|
+
function does both conversions internally.
|
|
1636
|
+
|
|
1637
|
+
The ``(direction_x, direction_y)`` vector defines pattern direction 1
|
|
1638
|
+
(``AngleX``). Direction 2 (``AngleY``) is set perpendicular so the SW
|
|
1639
|
+
UI shows a clean axis frame, but ``NumY`` stays at 1 so no second-axis
|
|
1640
|
+
instances are produced.
|
|
1641
|
+
|
|
1642
|
+
Args:
|
|
1643
|
+
adapter: A ``PyWin32Adapter`` with an open sketch.
|
|
1644
|
+
entities: Registered entity IDs to pattern. Must be non-empty.
|
|
1645
|
+
direction_x: Pattern direction X component (unit-less, any non-zero
|
|
1646
|
+
vector is normalised internally via ``atan2``).
|
|
1647
|
+
direction_y: Pattern direction Y component.
|
|
1648
|
+
spacing: Distance between instances in **millimetres**.
|
|
1649
|
+
count: Total number of instances (including the seed). Must be at
|
|
1650
|
+
least 2.
|
|
1651
|
+
|
|
1652
|
+
Returns:
|
|
1653
|
+
AdapterResult[str]: On success, ``data`` is a synthesised
|
|
1654
|
+
``"LinearPattern_<count>x<spacing>_<rand>"`` ID — the COM method
|
|
1655
|
+
only returns a boolean, so no usable SW handle exists.
|
|
1656
|
+
|
|
1657
|
+
Raises:
|
|
1658
|
+
Exception: Propagated through ``_handle_com_operation`` when
|
|
1659
|
+
inputs are invalid, an entity isn't registered, or
|
|
1660
|
+
``CreateLinearSketchStepAndRepeat`` returns ``False``.
|
|
1661
|
+
|
|
1662
|
+
Example::
|
|
1663
|
+
|
|
1664
|
+
# 5 copies of Line_1 along +X, 15 mm apart
|
|
1665
|
+
result = pywin32_sketch_ops.sketch_linear_pattern(
|
|
1666
|
+
adapter, ["Line_1"], direction_x=1.0, direction_y=0.0,
|
|
1667
|
+
spacing=15.0, count=5
|
|
1668
|
+
)
|
|
1669
|
+
print(result.data) # "LinearPattern_5x15.0_8765"
|
|
1670
|
+
"""
|
|
1671
|
+
if not adapter.currentSketchManager:
|
|
1672
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error="No active sketch")
|
|
1673
|
+
|
|
1674
|
+
def _linear_pattern_operation() -> str:
|
|
1675
|
+
if not entities:
|
|
1676
|
+
raise Exception("sketch_linear_pattern requires at least one entity")
|
|
1677
|
+
if count < 2:
|
|
1678
|
+
raise Exception("sketch_linear_pattern requires count >= 2")
|
|
1679
|
+
if spacing <= 0:
|
|
1680
|
+
raise Exception("sketch_linear_pattern requires spacing > 0")
|
|
1681
|
+
if math.hypot(direction_x, direction_y) < 1e-9:
|
|
1682
|
+
raise Exception(
|
|
1683
|
+
"sketch_linear_pattern requires a non-zero direction vector"
|
|
1684
|
+
)
|
|
1685
|
+
|
|
1686
|
+
# Validate every entity ID exists in the registry before mutating
|
|
1687
|
+
# selection state, so an unknown ID doesn't leave SW with a
|
|
1688
|
+
# half-built selection.
|
|
1689
|
+
for ent_id in entities:
|
|
1690
|
+
if ent_id not in adapter._sketch_entities:
|
|
1691
|
+
raise Exception(
|
|
1692
|
+
f"Unknown sketch entity '{ent_id}'. Use IDs returned by "
|
|
1693
|
+
"add_line/add_arc/add_circle/add_spline/add_centerline."
|
|
1694
|
+
)
|
|
1695
|
+
|
|
1696
|
+
# Clear any pre-existing selection so SW only sees the seed entities.
|
|
1697
|
+
adapter.currentModel.ClearSelection2(True)
|
|
1698
|
+
try:
|
|
1699
|
+
_select_sketch_entities(adapter, entities, mark=0)
|
|
1700
|
+
|
|
1701
|
+
angle_x = math.atan2(direction_y, direction_x)
|
|
1702
|
+
# Direction 2 (Y) goes 90° from direction 1; NumY=1 keeps it
|
|
1703
|
+
# single-row so the second-axis spacing/angle aren't actually
|
|
1704
|
+
# consumed, but SW still wants well-formed values.
|
|
1705
|
+
angle_y = angle_x + math.pi / 2.0
|
|
1706
|
+
|
|
1707
|
+
ok = adapter.currentSketchManager.CreateLinearSketchStepAndRepeat(
|
|
1708
|
+
count, # NumX
|
|
1709
|
+
1, # NumY
|
|
1710
|
+
spacing / 1000.0, # SpacingX (metres)
|
|
1711
|
+
0.0, # SpacingY
|
|
1712
|
+
angle_x, # AngleX (radians)
|
|
1713
|
+
angle_y, # AngleY (radians)
|
|
1714
|
+
"", # DeleteInstances
|
|
1715
|
+
False, # XSpacingDim
|
|
1716
|
+
False, # YSpacingDim
|
|
1717
|
+
False, # AngleDim
|
|
1718
|
+
False, # CreateNumOfInstancesDimInXDir
|
|
1719
|
+
False, # CreateNumOfInstancesDimInYDir
|
|
1720
|
+
)
|
|
1721
|
+
if not ok:
|
|
1722
|
+
raise Exception("Failed to create linear sketch pattern")
|
|
1723
|
+
finally:
|
|
1724
|
+
adapter.currentModel.ClearSelection2(True)
|
|
1725
|
+
|
|
1726
|
+
return f"LinearPattern_{count}x{spacing}_{int(time.time() * 1000) % 10000}"
|
|
1727
|
+
|
|
1728
|
+
return cast(
|
|
1729
|
+
AdapterResult[str],
|
|
1730
|
+
adapter._handle_com_operation(
|
|
1731
|
+
"sketch_linear_pattern", _linear_pattern_operation
|
|
1732
|
+
),
|
|
1733
|
+
)
|
|
1734
|
+
|
|
1735
|
+
|
|
1736
|
+
def _sketch_circular_pattern_impl(
|
|
1737
|
+
adapter: Any,
|
|
1738
|
+
entities: list[str],
|
|
1739
|
+
angle: float,
|
|
1740
|
+
count: int,
|
|
1741
|
+
) -> AdapterResult[str]:
|
|
1742
|
+
"""Create a circular sketch pattern from the registered seed entities.
|
|
1743
|
+
|
|
1744
|
+
Selects ``entities`` then calls
|
|
1745
|
+
``ISketchManager::CreateCircularSketchStepAndRepeat(ArcRadius, ArcAngle,
|
|
1746
|
+
PatternNum, PatternSpacing, PatternRotate, DeleteInstances, RadiusDim,
|
|
1747
|
+
AngleDim, CreateNumOfInstancesDim)``.
|
|
1748
|
+
|
|
1749
|
+
``ArcRadius`` is the radius at which SW places the pattern instances
|
|
1750
|
+
— getting it wrong puts every copy on a tight cluster around the
|
|
1751
|
+
seed rather than the intended ring. We derive it from the first
|
|
1752
|
+
registered entity's centre (via ``ISketchArc.GetCenterPoint`` once
|
|
1753
|
+
the dispatch is flagged) relative to the sketch origin. The COM
|
|
1754
|
+
API has no pattern-centre parameter — the rotation axis is implied
|
|
1755
|
+
by ``(ArcRadius, ArcAngle)`` relative to the seed, so the rotation
|
|
1756
|
+
axis is always the **sketch origin**. Callers who want a different
|
|
1757
|
+
pattern centre must position the seed relative to the desired
|
|
1758
|
+
centre (then translate the sketch as a whole, or use a sketch
|
|
1759
|
+
plane offset).
|
|
1760
|
+
|
|
1761
|
+
``PatternSpacing`` is the per-instance angle in radians. For a
|
|
1762
|
+
full 360° pattern we use ``angle / count`` so the last instance
|
|
1763
|
+
lands one slot before the seed (tiles cleanly). For partial sweeps
|
|
1764
|
+
(< 360°) we use ``angle / (count - 1)`` so the last instance lands
|
|
1765
|
+
at the full requested angle.
|
|
1766
|
+
|
|
1767
|
+
Args:
|
|
1768
|
+
adapter: A ``PyWin32Adapter`` with an open sketch.
|
|
1769
|
+
entities: Registered entity IDs to pattern. Must be non-empty.
|
|
1770
|
+
angle: Total swept angle in **degrees** (e.g. ``360`` for a full
|
|
1771
|
+
ring or ``180`` for a half-circle). Must be > 0.
|
|
1772
|
+
count: Total number of instances (including the seed). Must be
|
|
1773
|
+
at least 2.
|
|
1774
|
+
|
|
1775
|
+
Returns:
|
|
1776
|
+
AdapterResult[str]: On success, ``data`` is a synthesised
|
|
1777
|
+
``"CircularPattern_<count>x<angle>deg_<rand>"`` ID — the COM
|
|
1778
|
+
method only returns a boolean.
|
|
1779
|
+
|
|
1780
|
+
Raises:
|
|
1781
|
+
Exception: Propagated through ``_handle_com_operation`` when
|
|
1782
|
+
inputs are invalid, an entity isn't registered, or the COM
|
|
1783
|
+
call returns ``False``.
|
|
1784
|
+
|
|
1785
|
+
Example::
|
|
1786
|
+
|
|
1787
|
+
# 6 evenly-spaced copies of Circle_1 around the origin
|
|
1788
|
+
result = pywin32_sketch_ops.sketch_circular_pattern(
|
|
1789
|
+
adapter, ["Circle_1"], 360.0, 6
|
|
1790
|
+
)
|
|
1791
|
+
print(result.data) # "CircularPattern_6x360.0deg_4321"
|
|
1792
|
+
"""
|
|
1793
|
+
if not adapter.currentSketchManager:
|
|
1794
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error="No active sketch")
|
|
1795
|
+
|
|
1796
|
+
def _circular_pattern_operation() -> str:
|
|
1797
|
+
if not entities:
|
|
1798
|
+
raise Exception("sketch_circular_pattern requires at least one entity")
|
|
1799
|
+
if count < 2:
|
|
1800
|
+
raise Exception("sketch_circular_pattern requires count >= 2")
|
|
1801
|
+
if angle <= 0:
|
|
1802
|
+
raise Exception("sketch_circular_pattern requires angle > 0")
|
|
1803
|
+
|
|
1804
|
+
# Validate every entity ID exists before mutating selection state.
|
|
1805
|
+
for ent_id in entities:
|
|
1806
|
+
if ent_id not in adapter._sketch_entities:
|
|
1807
|
+
raise Exception(
|
|
1808
|
+
f"Unknown sketch entity '{ent_id}'. Use IDs returned by "
|
|
1809
|
+
"add_line/add_arc/add_circle/add_spline/add_centerline."
|
|
1810
|
+
)
|
|
1811
|
+
|
|
1812
|
+
adapter.currentModel.ClearSelection2(True)
|
|
1813
|
+
try:
|
|
1814
|
+
_select_sketch_entities(adapter, entities, mark=0)
|
|
1815
|
+
|
|
1816
|
+
# The COM API doesn't take pattern-centre coordinates directly.
|
|
1817
|
+
# Instead:
|
|
1818
|
+
# ``ArcRadius`` = distance from the seed to the rotation axis.
|
|
1819
|
+
# ``ArcAngle`` = angle (radians) **from the seed toward the
|
|
1820
|
+
# rotation axis**, NOT a starting angle. With
|
|
1821
|
+
# ArcAngle=0 SW puts the axis at +X relative
|
|
1822
|
+
# to the seed.
|
|
1823
|
+
# We recover both by reading the seed's centre via
|
|
1824
|
+
# ``ISketchArc.GetCenterPoint`` (after flagging the dispatch
|
|
1825
|
+
# with sw_type_info — pywin32 late binding otherwise resolves
|
|
1826
|
+
# the method as a tuple-valued property) and computing the
|
|
1827
|
+
# offset to the rotation-axis origin. Caller-supplied
|
|
1828
|
+
# ``(center_x, center_y)`` is rejected above when non-zero, so
|
|
1829
|
+
# the rotation axis is always at the sketch origin here.
|
|
1830
|
+
#
|
|
1831
|
+
# Without this fix, ArcAngle=0 + ArcRadius=1 mm puts every
|
|
1832
|
+
# instance on a tiny ring beside the seed instead of the
|
|
1833
|
+
# intended pattern (caught by the #17 live screenshot). Falls
|
|
1834
|
+
# back to placing the axis at angle π from the seed if
|
|
1835
|
+
# GetCenterPoint isn't available; that still works when the
|
|
1836
|
+
# user positions the seed on the +X side of the origin.
|
|
1837
|
+
first_entity = adapter._sketch_entities.get(entities[0])
|
|
1838
|
+
|
|
1839
|
+
seed_xy: tuple[float, float] | None = None
|
|
1840
|
+
|
|
1841
|
+
# Group entities (polygons via ``CreatePolygon``, rectangles via
|
|
1842
|
+
# ``CreateCornerRectangle``) register as a SAFEARRAY of segments
|
|
1843
|
+
# — a tuple, not a single dispatch — so the ``GetCenterPoint``
|
|
1844
|
+
# path below would silently fall back to a 1 mm placeholder
|
|
1845
|
+
# radius. ``_add_polygon_impl`` / ``_add_rectangle_impl`` stash
|
|
1846
|
+
# the seed centre in ``_sketch_entity_centers`` at register time;
|
|
1847
|
+
# use that. Any future tuple-registering primitive that forgets
|
|
1848
|
+
# to populate the cache falls through to the clear-error branch
|
|
1849
|
+
# below.
|
|
1850
|
+
if isinstance(first_entity, (list, tuple)):
|
|
1851
|
+
seed_xy = adapter._sketch_entity_centers.get(entities[0])
|
|
1852
|
+
if seed_xy is None:
|
|
1853
|
+
# Polygons and rectangles always reach the cached branch
|
|
1854
|
+
# above, so naming them here would be misleading — the
|
|
1855
|
+
# caller is hitting this with a group seed whose
|
|
1856
|
+
# ``add_*`` writer never stashed a centre. List only the
|
|
1857
|
+
# always-works primitive types.
|
|
1858
|
+
raise Exception(
|
|
1859
|
+
f"sketch_circular_pattern can't derive the seed centre "
|
|
1860
|
+
f"for '{entities[0]}' — this entity type registers as "
|
|
1861
|
+
f"a group (tuple of segments) with no cached centre. "
|
|
1862
|
+
f"Use a circle, arc, or ellipse seed."
|
|
1863
|
+
)
|
|
1864
|
+
|
|
1865
|
+
if seed_xy is None and first_entity is not None:
|
|
1866
|
+
# GetCenterPoint lives on multiple sketch-entity interfaces
|
|
1867
|
+
# (ISketchArc for arcs/circles, ISketchEllipse for ellipses),
|
|
1868
|
+
# all with the same zero-arg signature. Flag every interface
|
|
1869
|
+
# we might encounter so the lookup works regardless of seed
|
|
1870
|
+
# type — without this, an ellipse seed silently resolves
|
|
1871
|
+
# GetCenterPoint as a property and the pattern is laid out
|
|
1872
|
+
# at a bogus 1 mm radius.
|
|
1873
|
+
adapter._attempt(
|
|
1874
|
+
lambda: _sw_type_info.flag_methods(
|
|
1875
|
+
first_entity,
|
|
1876
|
+
"ISketchArc",
|
|
1877
|
+
"ISketchEllipse",
|
|
1878
|
+
),
|
|
1879
|
+
default=0,
|
|
1880
|
+
)
|
|
1881
|
+
point = adapter._attempt(lambda: first_entity.GetCenterPoint())
|
|
1882
|
+
if point is not None and hasattr(point, "__len__") and len(point) >= 2:
|
|
1883
|
+
seed_xy = (float(point[0]) * 1000.0, float(point[1]) * 1000.0)
|
|
1884
|
+
|
|
1885
|
+
# Single-dispatch seeds without ``GetCenterPoint`` (line, spline,
|
|
1886
|
+
# centerline) used to fall through here with ``seed_xy is None``
|
|
1887
|
+
# and silently produce a 1 mm placeholder pattern at the wrong
|
|
1888
|
+
# radius — same bug class as the polygon/rectangle tuple case,
|
|
1889
|
+
# but quieter because no exception fires. Surface a clear error
|
|
1890
|
+
# naming the offending seed instead.
|
|
1891
|
+
if seed_xy is None:
|
|
1892
|
+
raise Exception(
|
|
1893
|
+
f"sketch_circular_pattern can't derive the seed centre "
|
|
1894
|
+
f"for '{entities[0]}' — this seed type has no "
|
|
1895
|
+
f"GetCenterPoint dispatch on ISketchArc/ISketchEllipse. "
|
|
1896
|
+
f"Use a circle, arc, ellipse, or polygon seed."
|
|
1897
|
+
)
|
|
1898
|
+
|
|
1899
|
+
# Rotation axis is always the sketch origin (0, 0) — SW's
|
|
1900
|
+
# ``CreateCircularSketchStepAndRepeat`` has no pattern-centre
|
|
1901
|
+
# parameter, so dx/dy from seed to axis is just ``-seed``.
|
|
1902
|
+
dx_mm = -seed_xy[0]
|
|
1903
|
+
dy_mm = -seed_xy[1]
|
|
1904
|
+
arc_radius_mm = math.hypot(dx_mm, dy_mm)
|
|
1905
|
+
arc_angle_rad = math.atan2(dy_mm, dx_mm) if arc_radius_mm > 0 else 0.0
|
|
1906
|
+
|
|
1907
|
+
# ``CreateCircularSketchStepAndRepeat`` silently returns False on
|
|
1908
|
+
# negative ``ArcAngle`` values — the bundled VBA/C# examples all
|
|
1909
|
+
# pass positive radians (e.g. ``4.732863934409`` ≈ 271°). Python's
|
|
1910
|
+
# ``atan2`` produces ``-π`` for a seed on the +X axis (because
|
|
1911
|
+
# ``-seed_xy[1]`` is ``-0.0``), which is geometrically equivalent
|
|
1912
|
+
# to ``+π`` but fails the COM call. Normalise to ``[0, 2π)``.
|
|
1913
|
+
if arc_angle_rad < 0:
|
|
1914
|
+
arc_angle_rad += 2.0 * math.pi
|
|
1915
|
+
|
|
1916
|
+
# 1 mm minimum keeps SW from silently rejecting the call when
|
|
1917
|
+
# the seed sits right on the pattern centre.
|
|
1918
|
+
arc_radius_m = max(arc_radius_mm / 1000.0, 0.001)
|
|
1919
|
+
# For a full 360° pattern, ``angle / count`` keeps adjacent
|
|
1920
|
+
# instances evenly spaced (instance ``count`` would coincide
|
|
1921
|
+
# with the seed). For partial sweeps the last instance should
|
|
1922
|
+
# land at the full requested angle, so divide by ``count - 1``
|
|
1923
|
+
# instead — otherwise ``angle=180, count=3`` would reach only
|
|
1924
|
+
# 120°.
|
|
1925
|
+
angle_rad = math.radians(angle)
|
|
1926
|
+
if abs(angle - 360.0) < 1e-9:
|
|
1927
|
+
pattern_spacing = angle_rad / count
|
|
1928
|
+
else:
|
|
1929
|
+
pattern_spacing = angle_rad / (count - 1)
|
|
1930
|
+
|
|
1931
|
+
ok = adapter.currentSketchManager.CreateCircularSketchStepAndRepeat(
|
|
1932
|
+
arc_radius_m, # ArcRadius — seed-to-axis distance (metres)
|
|
1933
|
+
arc_angle_rad, # ArcAngle — direction from seed to axis (radians)
|
|
1934
|
+
count, # PatternNum
|
|
1935
|
+
pattern_spacing, # PatternSpacing (radians)
|
|
1936
|
+
True, # PatternRotate
|
|
1937
|
+
"", # DeleteInstances
|
|
1938
|
+
False, # RadiusDim
|
|
1939
|
+
False, # AngleDim
|
|
1940
|
+
False, # CreateNumOfInstancesDim
|
|
1941
|
+
)
|
|
1942
|
+
if not ok:
|
|
1943
|
+
raise Exception("Failed to create circular sketch pattern")
|
|
1944
|
+
finally:
|
|
1945
|
+
adapter.currentModel.ClearSelection2(True)
|
|
1946
|
+
|
|
1947
|
+
return f"CircularPattern_{count}x{angle}deg_{int(time.time() * 1000) % 10000}"
|
|
1948
|
+
|
|
1949
|
+
return cast(
|
|
1950
|
+
AdapterResult[str],
|
|
1951
|
+
adapter._handle_com_operation(
|
|
1952
|
+
"sketch_circular_pattern", _circular_pattern_operation
|
|
1953
|
+
),
|
|
1954
|
+
)
|
|
1955
|
+
|
|
1956
|
+
|
|
1957
|
+
def _sketch_mirror_impl(
|
|
1958
|
+
adapter: Any, entities: list[str], mirror_line: str
|
|
1959
|
+
) -> AdapterResult[str]:
|
|
1960
|
+
"""Mirror sketch entities about a registered centreline.
|
|
1961
|
+
|
|
1962
|
+
Selects the ``entities`` under mark **1** and the ``mirror_line``
|
|
1963
|
+
centreline under mark **2** — those are the marks SOLIDWORKS expects
|
|
1964
|
+
per the ``IModelDoc2::SketchMirror`` documentation — then invokes the
|
|
1965
|
+
method with no arguments. ``SketchMirror`` returns ``void``, so
|
|
1966
|
+
success is reported by the absence of a COM error and the resulting
|
|
1967
|
+
ID is a synthesised ``"Mirror_<mirror_line_id>_<rand>"`` string.
|
|
1968
|
+
|
|
1969
|
+
Args:
|
|
1970
|
+
adapter: A ``PyWin32Adapter`` with an open sketch.
|
|
1971
|
+
entities: Registered entity IDs to mirror. Must be non-empty.
|
|
1972
|
+
mirror_line: Registered entity ID of the centreline. Must be a
|
|
1973
|
+
value previously returned by ``add_centerline``.
|
|
1974
|
+
|
|
1975
|
+
Returns:
|
|
1976
|
+
AdapterResult[str]: On success, ``data`` is a synthesised mirror
|
|
1977
|
+
ID. On failure, ``status`` is ``ERROR`` with the COM error or a
|
|
1978
|
+
validation message describing the problem.
|
|
1979
|
+
|
|
1980
|
+
Raises:
|
|
1981
|
+
Exception: Propagated through ``_handle_com_operation`` when the
|
|
1982
|
+
inputs are invalid, an entity isn't in the registry, or the
|
|
1983
|
+
COM call raises.
|
|
1984
|
+
|
|
1985
|
+
Example::
|
|
1986
|
+
|
|
1987
|
+
result = pywin32_sketch_ops.sketch_mirror(
|
|
1988
|
+
adapter, ["Line_1", "Line_2"], "Centerline_1"
|
|
1989
|
+
)
|
|
1990
|
+
print(result.data) # "Mirror_Centerline_1_3456"
|
|
1991
|
+
"""
|
|
1992
|
+
if not adapter.currentSketchManager:
|
|
1993
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error="No active sketch")
|
|
1994
|
+
|
|
1995
|
+
def _mirror_operation() -> str:
|
|
1996
|
+
if not entities:
|
|
1997
|
+
raise Exception("sketch_mirror requires at least one entity")
|
|
1998
|
+
if not mirror_line:
|
|
1999
|
+
raise Exception(
|
|
2000
|
+
"sketch_mirror requires a mirror_line entity ID (add_centerline)"
|
|
2001
|
+
)
|
|
2002
|
+
if mirror_line not in adapter._sketch_entities:
|
|
2003
|
+
raise Exception(
|
|
2004
|
+
f"Unknown mirror_line entity '{mirror_line}'. Use the ID "
|
|
2005
|
+
"returned by add_centerline."
|
|
2006
|
+
)
|
|
2007
|
+
# IModelDoc2::SketchMirror specifies that the mirror axis must be
|
|
2008
|
+
# a centreline; selecting any other segment under mark=2 silently
|
|
2009
|
+
# no-ops on SW. ``add_centerline`` returns IDs prefixed with
|
|
2010
|
+
# ``Centerline_``, so reject anything else up front.
|
|
2011
|
+
# TODO(#24-followup): replace the prefix-string parse with a
|
|
2012
|
+
# proper introspection of the entity's ``ConstructionGeometry``
|
|
2013
|
+
# property on the real adapter (still keep the prefix check on
|
|
2014
|
+
# the mock, where there's no real dispatch to inspect). The
|
|
2015
|
+
# prefix parse is brittle if either side renames its ID scheme;
|
|
2016
|
+
# the SW invariant we're really enforcing is "this segment has
|
|
2017
|
+
# construction-geometry flagged".
|
|
2018
|
+
if not mirror_line.startswith("Centerline_"):
|
|
2019
|
+
raise Exception(
|
|
2020
|
+
f"mirror_line must be a centerline (from add_centerline), "
|
|
2021
|
+
f"got '{mirror_line}'"
|
|
2022
|
+
)
|
|
2023
|
+
|
|
2024
|
+
# Validate every source entity ID up front so an unknown ID does
|
|
2025
|
+
# not leave SW with a half-built selection state.
|
|
2026
|
+
for ent_id in entities:
|
|
2027
|
+
if ent_id not in adapter._sketch_entities:
|
|
2028
|
+
raise Exception(
|
|
2029
|
+
f"Unknown sketch entity '{ent_id}'. Use IDs returned by "
|
|
2030
|
+
"add_line/add_arc/add_circle/add_spline/add_centerline."
|
|
2031
|
+
)
|
|
2032
|
+
|
|
2033
|
+
adapter.currentModel.ClearSelection2(True)
|
|
2034
|
+
try:
|
|
2035
|
+
# Mark 1 for the source segments per IModelDoc2::SketchMirror docs.
|
|
2036
|
+
_select_sketch_entities(adapter, entities, mark=1)
|
|
2037
|
+
# Mark 2 for the centreline.
|
|
2038
|
+
_select_sketch_entities(adapter, [mirror_line], mark=2)
|
|
2039
|
+
|
|
2040
|
+
# IModelDoc2::SketchMirror is VT_VOID — no return value, so a
|
|
2041
|
+
# successful invocation is its own success signal.
|
|
2042
|
+
adapter.currentModel.SketchMirror()
|
|
2043
|
+
finally:
|
|
2044
|
+
adapter.currentModel.ClearSelection2(True)
|
|
2045
|
+
|
|
2046
|
+
return f"Mirror_{mirror_line}_{int(time.time() * 1000) % 10000}"
|
|
2047
|
+
|
|
2048
|
+
return cast(
|
|
2049
|
+
AdapterResult[str],
|
|
2050
|
+
adapter._handle_com_operation("sketch_mirror", _mirror_operation),
|
|
2051
|
+
)
|
|
2052
|
+
|
|
2053
|
+
|
|
2054
|
+
def _sketch_offset_impl(
|
|
2055
|
+
adapter: Any,
|
|
2056
|
+
entities: list[str],
|
|
2057
|
+
offset_distance: float,
|
|
2058
|
+
reverse_direction: bool,
|
|
2059
|
+
) -> AdapterResult[str]:
|
|
2060
|
+
"""Offset selected sketch entities by a fixed distance.
|
|
2061
|
+
|
|
2062
|
+
Selects ``entities`` then calls
|
|
2063
|
+
``ISketchManager::SketchOffset2(Offset, BothDirections, Chain, CapEnds,
|
|
2064
|
+
MakeConstruction, AddDimensions)``.
|
|
2065
|
+
|
|
2066
|
+
``Offset`` is in metres; a negative value flips the offset direction
|
|
2067
|
+
per the COM docs, so ``reverse_direction=True`` is implemented by
|
|
2068
|
+
negating the value rather than relying on ``BothDirections``.
|
|
2069
|
+
|
|
2070
|
+
The remaining flags are pinned to predictable defaults so the call is
|
|
2071
|
+
deterministic for automation: no caps, no auto-conversion to
|
|
2072
|
+
construction geometry, no on-canvas dimension, and ``Chain=False`` so
|
|
2073
|
+
only the supplied entities are offset (rather than the entire
|
|
2074
|
+
contour they belong to).
|
|
2075
|
+
|
|
2076
|
+
Args:
|
|
2077
|
+
adapter: A ``PyWin32Adapter`` with an open sketch.
|
|
2078
|
+
entities: Registered entity IDs to offset. Must be non-empty.
|
|
2079
|
+
offset_distance: Offset distance in **millimetres**. Must be > 0
|
|
2080
|
+
— the direction is controlled by ``reverse_direction``, not
|
|
2081
|
+
the sign.
|
|
2082
|
+
reverse_direction: When ``True``, offset in the opposite of SW's
|
|
2083
|
+
default direction (effectively a negated ``Offset`` argument).
|
|
2084
|
+
|
|
2085
|
+
Returns:
|
|
2086
|
+
AdapterResult[str]: On success, ``data`` is a synthesised
|
|
2087
|
+
``"Offset_<distance>_<direction>_<rand>"`` ID.
|
|
2088
|
+
|
|
2089
|
+
Raises:
|
|
2090
|
+
Exception: Propagated through ``_handle_com_operation`` when
|
|
2091
|
+
inputs are invalid, an entity isn't registered, or the COM
|
|
2092
|
+
call returns ``False``.
|
|
2093
|
+
|
|
2094
|
+
Example::
|
|
2095
|
+
|
|
2096
|
+
result = pywin32_sketch_ops.sketch_offset(
|
|
2097
|
+
adapter, ["Line_1"], 5.0, reverse_direction=False
|
|
2098
|
+
)
|
|
2099
|
+
print(result.data) # "Offset_5.0_outward_9876"
|
|
2100
|
+
"""
|
|
2101
|
+
if not adapter.currentSketchManager:
|
|
2102
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error="No active sketch")
|
|
2103
|
+
|
|
2104
|
+
def _offset_operation() -> str:
|
|
2105
|
+
if not entities:
|
|
2106
|
+
raise Exception("sketch_offset requires at least one entity")
|
|
2107
|
+
if offset_distance <= 0:
|
|
2108
|
+
raise Exception(
|
|
2109
|
+
"sketch_offset requires offset_distance > 0 — use "
|
|
2110
|
+
"reverse_direction to flip the side"
|
|
2111
|
+
)
|
|
2112
|
+
|
|
2113
|
+
# Validate every entity ID up front so an unknown ID does not
|
|
2114
|
+
# leave SW with a half-built selection.
|
|
2115
|
+
for ent_id in entities:
|
|
2116
|
+
if ent_id not in adapter._sketch_entities:
|
|
2117
|
+
raise Exception(
|
|
2118
|
+
f"Unknown sketch entity '{ent_id}'. Use IDs returned by "
|
|
2119
|
+
"add_line/add_arc/add_circle/add_spline/add_centerline."
|
|
2120
|
+
)
|
|
2121
|
+
|
|
2122
|
+
adapter.currentModel.ClearSelection2(True)
|
|
2123
|
+
try:
|
|
2124
|
+
_select_sketch_entities(adapter, entities, mark=0)
|
|
2125
|
+
|
|
2126
|
+
# Negative Offset flips the side per SketchOffset2 docs.
|
|
2127
|
+
offset_m = (
|
|
2128
|
+
-offset_distance / 1000.0
|
|
2129
|
+
if reverse_direction
|
|
2130
|
+
else offset_distance / 1000.0
|
|
2131
|
+
)
|
|
2132
|
+
|
|
2133
|
+
adapter._attempt(
|
|
2134
|
+
lambda: _sw_type_info.flag_methods(
|
|
2135
|
+
adapter.currentSketchManager, "ISketchManager"
|
|
2136
|
+
),
|
|
2137
|
+
default=0,
|
|
2138
|
+
)
|
|
2139
|
+
ok = adapter.currentSketchManager.SketchOffset2(
|
|
2140
|
+
offset_m, # Offset (metres)
|
|
2141
|
+
False, # BothDirections
|
|
2142
|
+
False, # Chain
|
|
2143
|
+
0, # CapEnds (swSkOffsetCapEndType_e: 0 = no caps)
|
|
2144
|
+
0, # MakeConstruction (swSkOffsetMakeConstructionType_e: 0 = none)
|
|
2145
|
+
False, # AddDimensions
|
|
2146
|
+
)
|
|
2147
|
+
if not ok:
|
|
2148
|
+
raise Exception("Failed to offset sketch entities")
|
|
2149
|
+
finally:
|
|
2150
|
+
adapter.currentModel.ClearSelection2(True)
|
|
2151
|
+
|
|
2152
|
+
direction = "inward" if reverse_direction else "outward"
|
|
2153
|
+
return f"Offset_{offset_distance}_{direction}_{int(time.time() * 1000) % 10000}"
|
|
2154
|
+
|
|
2155
|
+
return cast(
|
|
2156
|
+
AdapterResult[str],
|
|
2157
|
+
adapter._handle_com_operation("sketch_offset", _offset_operation),
|
|
2158
|
+
)
|
|
2159
|
+
|
|
2160
|
+
|
|
2161
|
+
def _exit_sketch_impl(adapter: Any) -> AdapterResult[None]:
|
|
2162
|
+
"""Exit any sketch-edit mode the active model is in and reset adapter state.
|
|
2163
|
+
|
|
2164
|
+
The previous implementation trusted ``adapter.currentSketchManager`` —
|
|
2165
|
+
a Python-side handle populated only by ``create_sketch`` on **this**
|
|
2166
|
+
adapter instance. A fresh adapter pointing at a SolidWorks process
|
|
2167
|
+
that already has a sketch open (from a crashed prior run, an
|
|
2168
|
+
aborted automation, or a manual user edit) would report
|
|
2169
|
+
``WARNING: "No active sketch to exit"`` while SW was still sitting
|
|
2170
|
+
in sketch-edit mode — and every subsequent ``create_sketch`` then
|
|
2171
|
+
failed with ``Failed to select plane: Front Plane`` because SW
|
|
2172
|
+
can't open a new sketch while one is already active.
|
|
2173
|
+
|
|
2174
|
+
Now queries ``IModelDoc2.GetActiveSketch2`` to find out what SW
|
|
2175
|
+
actually has open, and toggles ``SketchManager.InsertSketch(True)``
|
|
2176
|
+
when either SW or the adapter thinks a sketch is in edit mode.
|
|
2177
|
+
Adapter-side state is always cleared on success.
|
|
2178
|
+
|
|
2179
|
+
Args:
|
|
2180
|
+
adapter: A ``PyWin32Adapter`` with a non-``None`` ``currentModel``.
|
|
2181
|
+
|
|
2182
|
+
Returns:
|
|
2183
|
+
AdapterResult[None]: On success, ``status`` is ``SUCCESS``.
|
|
2184
|
+
When neither SW nor the adapter has an active sketch,
|
|
2185
|
+
``status`` is ``WARNING`` (already-exited is not a failure).
|
|
2186
|
+
When ``currentModel`` is ``None``, also returns ``WARNING`` so
|
|
2187
|
+
defensive cleanup callers don't see spurious errors.
|
|
2188
|
+
|
|
2189
|
+
Raises:
|
|
2190
|
+
Exception: Propagated through ``_handle_com_operation`` when the
|
|
2191
|
+
``InsertSketch`` call itself raises.
|
|
2192
|
+
|
|
2193
|
+
Example::
|
|
2194
|
+
|
|
2195
|
+
pywin32_sketch_ops.add_line(adapter, 0, 0, 50, 0)
|
|
2196
|
+
pywin32_sketch_ops.exit_sketch(adapter)
|
|
2197
|
+
# adapter.currentSketch is now None and SW is out of sketch-edit mode
|
|
2198
|
+
"""
|
|
2199
|
+
if adapter.currentModel is None:
|
|
2200
|
+
# No document means nothing can be in sketch-edit mode either.
|
|
2201
|
+
# Match the legacy "already exited" semantics so cleanup callers
|
|
2202
|
+
# that fire exit_sketch defensively don't see spurious errors.
|
|
2203
|
+
return AdapterResult(
|
|
2204
|
+
status=AdapterResultStatus.WARNING,
|
|
2205
|
+
error="No active sketch to exit",
|
|
2206
|
+
)
|
|
2207
|
+
|
|
2208
|
+
def _exit_operation() -> str:
|
|
2209
|
+
# ``GetActiveSketch2`` is a real zero-arg method on IModelDoc2.
|
|
2210
|
+
# Without flagging, pywin32 late binding resolves it as a property
|
|
2211
|
+
# and SW returns ``Member not found`` — the same root cause as
|
|
2212
|
+
# the cross-thread bugs in runbook #5.
|
|
2213
|
+
adapter._attempt(
|
|
2214
|
+
lambda: _sw_type_info.flag_methods(adapter.currentModel, "IModelDoc2"),
|
|
2215
|
+
default=0,
|
|
2216
|
+
)
|
|
2217
|
+
|
|
2218
|
+
sw_active = adapter._attempt(lambda: adapter.currentModel.GetActiveSketch2())
|
|
2219
|
+
adapter_active = adapter.currentSketchManager
|
|
2220
|
+
|
|
2221
|
+
# Already out of sketch-edit mode — clean up adapter state so a
|
|
2222
|
+
# future create_sketch starts from a known-good baseline, then
|
|
2223
|
+
# warn. Using ``data`` to signal "no_op" lets callers tell the
|
|
2224
|
+
# difference between "I exited a sketch" and "nothing was open".
|
|
2225
|
+
if sw_active is None and adapter_active is None:
|
|
2226
|
+
return "no_active_sketch"
|
|
2227
|
+
|
|
2228
|
+
# Prefer the adapter's SketchManager handle when available (it was
|
|
2229
|
+
# captured at create_sketch time on the executor thread, so it's
|
|
2230
|
+
# apartment-safe); fall back to a fresh ``currentModel.SketchManager``
|
|
2231
|
+
# for the SW-only state case.
|
|
2232
|
+
sketch_manager = adapter_active or adapter.currentModel.SketchManager
|
|
2233
|
+
adapter._attempt(
|
|
2234
|
+
lambda: _sw_type_info.flag_methods(sketch_manager, "ISketchManager"),
|
|
2235
|
+
default=0,
|
|
2236
|
+
)
|
|
2237
|
+
# Hand the document back with SolidWorks' default (inference on) for
|
|
2238
|
+
# interactive sketching; create_sketch turned AddToDB on.
|
|
2239
|
+
adapter._attempt(lambda: setattr(sketch_manager, "AddToDB", False))
|
|
2240
|
+
sketch_manager.InsertSketch(True)
|
|
2241
|
+
adapter.currentSketch = None
|
|
2242
|
+
adapter.currentSketchManager = None
|
|
2243
|
+
adapter._reset_sketch_entity_registry()
|
|
2244
|
+
return "exited"
|
|
2245
|
+
|
|
2246
|
+
result = cast(
|
|
2247
|
+
AdapterResult[str],
|
|
2248
|
+
adapter._handle_com_operation("exit_sketch", _exit_operation),
|
|
2249
|
+
)
|
|
2250
|
+
# Translate "no sketch was open" into a WARNING so callers that branch
|
|
2251
|
+
# on ``is_error`` still treat already-exited as benign. ``data`` is
|
|
2252
|
+
# the operation tag; ``error`` carries the human message.
|
|
2253
|
+
if result.is_success and result.data == "no_active_sketch":
|
|
2254
|
+
return cast(
|
|
2255
|
+
AdapterResult[None],
|
|
2256
|
+
AdapterResult(
|
|
2257
|
+
status=AdapterResultStatus.WARNING,
|
|
2258
|
+
error="No active sketch to exit",
|
|
2259
|
+
),
|
|
2260
|
+
)
|
|
2261
|
+
if result.is_success:
|
|
2262
|
+
return cast(
|
|
2263
|
+
AdapterResult[None],
|
|
2264
|
+
AdapterResult(status=AdapterResultStatus.SUCCESS, data=None),
|
|
2265
|
+
)
|
|
2266
|
+
return cast(AdapterResult[None], result)
|
|
2267
|
+
|
|
2268
|
+
|
|
2269
|
+
def _to_bool(value: Any) -> bool | None:
|
|
2270
|
+
"""Normalise an arbitrary raw value to a Python bool.
|
|
2271
|
+
|
|
2272
|
+
Handles booleans, 0/1 integers and floats, truthy/falsy strings, and
|
|
2273
|
+
single-element sequences.
|
|
2274
|
+
|
|
2275
|
+
Args:
|
|
2276
|
+
value: Raw value from a SolidWorks COM attribute or method return.
|
|
2277
|
+
|
|
2278
|
+
Returns:
|
|
2279
|
+
bool | None: ``True``, ``False``, or ``None`` when the value is
|
|
2280
|
+
ambiguous or unrecognised.
|
|
2281
|
+
"""
|
|
2282
|
+
if isinstance(value, bool):
|
|
2283
|
+
return value
|
|
2284
|
+
if isinstance(value, (int, float)) and int(value) in (0, 1):
|
|
2285
|
+
return bool(int(value))
|
|
2286
|
+
if isinstance(value, str):
|
|
2287
|
+
normalized = value.strip().lower()
|
|
2288
|
+
if normalized in {"true", "yes", "fully_defined", "fully defined"}:
|
|
2289
|
+
return True
|
|
2290
|
+
if normalized in {
|
|
2291
|
+
"false",
|
|
2292
|
+
"no",
|
|
2293
|
+
"under_defined",
|
|
2294
|
+
"under defined",
|
|
2295
|
+
"over_defined",
|
|
2296
|
+
"over defined",
|
|
2297
|
+
}:
|
|
2298
|
+
return False
|
|
2299
|
+
if isinstance(value, (list, tuple)) and value:
|
|
2300
|
+
return _to_bool(value[0])
|
|
2301
|
+
return None
|
|
2302
|
+
|
|
2303
|
+
|
|
2304
|
+
def _to_number(value: Any) -> float | None:
|
|
2305
|
+
"""Normalise an arbitrary raw value to a float.
|
|
2306
|
+
|
|
2307
|
+
Args:
|
|
2308
|
+
value: Raw value from a COM attribute or method return.
|
|
2309
|
+
|
|
2310
|
+
Returns:
|
|
2311
|
+
float | None: Numeric value, or ``None`` when conversion fails.
|
|
2312
|
+
"""
|
|
2313
|
+
if isinstance(value, (int, float)):
|
|
2314
|
+
return float(value)
|
|
2315
|
+
if isinstance(value, str):
|
|
2316
|
+
try:
|
|
2317
|
+
return float(value.strip())
|
|
2318
|
+
except ValueError:
|
|
2319
|
+
return None
|
|
2320
|
+
if isinstance(value, (list, tuple)) and value:
|
|
2321
|
+
return _to_number(value[0])
|
|
2322
|
+
return None
|
|
2323
|
+
|
|
2324
|
+
|
|
2325
|
+
def _probe_to_flag(source: str, raw: Any) -> bool | None:
|
|
2326
|
+
"""Interpret a raw probe result according to the property-name semantics.
|
|
2327
|
+
|
|
2328
|
+
The SolidWorks API uses both positive and negative naming conventions
|
|
2329
|
+
(``IsFullyConstrained`` vs ``IsUnderDefined``) so the source name is
|
|
2330
|
+
used as a hint when interpreting the raw value.
|
|
2331
|
+
|
|
2332
|
+
Args:
|
|
2333
|
+
source: The name of the API attribute that produced ``raw``, e.g.
|
|
2334
|
+
``"IsFullyConstrained"`` or ``"IsUnderDefined"``.
|
|
2335
|
+
raw: The raw return value from the COM attribute.
|
|
2336
|
+
|
|
2337
|
+
Returns:
|
|
2338
|
+
bool | None: ``True`` if the sketch is fully defined, ``False`` if
|
|
2339
|
+
not, or ``None`` when interpretation is impossible.
|
|
2340
|
+
"""
|
|
2341
|
+
source_lower = source.lower()
|
|
2342
|
+
text_value = str(raw).strip().lower() if raw is not None else ""
|
|
2343
|
+
numeric_value = _to_number(raw)
|
|
2344
|
+
|
|
2345
|
+
if "underdefined" in source_lower or "under_defined" in source_lower:
|
|
2346
|
+
if numeric_value is not None:
|
|
2347
|
+
return numeric_value == 0
|
|
2348
|
+
bool_value = _to_bool(raw)
|
|
2349
|
+
if bool_value is not None:
|
|
2350
|
+
return not bool_value
|
|
2351
|
+
|
|
2352
|
+
if "overdefined" in source_lower or "over_defined" in source_lower:
|
|
2353
|
+
if numeric_value is not None and numeric_value > 0:
|
|
2354
|
+
return False
|
|
2355
|
+
bool_value = _to_bool(raw)
|
|
2356
|
+
if bool_value is True:
|
|
2357
|
+
return False
|
|
2358
|
+
|
|
2359
|
+
if "fullydefined" in source_lower or "fully_defined" in source_lower:
|
|
2360
|
+
bool_value = _to_bool(raw)
|
|
2361
|
+
if bool_value is not None:
|
|
2362
|
+
return bool_value
|
|
2363
|
+
|
|
2364
|
+
if "fully" in text_value and "under" not in text_value and "over" not in text_value:
|
|
2365
|
+
return True
|
|
2366
|
+
if "under" in text_value or "over" in text_value:
|
|
2367
|
+
return False
|
|
2368
|
+
|
|
2369
|
+
return _to_bool(raw)
|
|
2370
|
+
|
|
2371
|
+
|
|
2372
|
+
def _state_from_bool(flag: bool | None) -> str:
|
|
2373
|
+
"""Convert a nullable bool flag to a descriptive state string.
|
|
2374
|
+
|
|
2375
|
+
Args:
|
|
2376
|
+
flag: The fully-defined flag value.
|
|
2377
|
+
|
|
2378
|
+
Returns:
|
|
2379
|
+
str: ``"fully_defined"``, ``"not_fully_defined"``, or
|
|
2380
|
+
``"unknown"``.
|
|
2381
|
+
"""
|
|
2382
|
+
if flag is True:
|
|
2383
|
+
return "fully_defined"
|
|
2384
|
+
if flag is False:
|
|
2385
|
+
return "not_fully_defined"
|
|
2386
|
+
return "unknown"
|
|
2387
|
+
|
|
2388
|
+
|
|
2389
|
+
def _check_sketch_fully_defined_impl(
|
|
2390
|
+
adapter: Any,
|
|
2391
|
+
sketch_name: str | None = None,
|
|
2392
|
+
) -> AdapterResult[dict[str, Any]]:
|
|
2393
|
+
"""Check whether a sketch is fully constrained (fully defined).
|
|
2394
|
+
|
|
2395
|
+
Queries the SolidWorks sketch object through multiple probe methods
|
|
2396
|
+
because different API versions expose this information differently:
|
|
2397
|
+
|
|
2398
|
+
* ``ISketch.IsFullyConstrained`` (SolidWorks 2018+)
|
|
2399
|
+
* ``ISketch.GetStatus`` returning a status integer
|
|
2400
|
+
* ``ISketch.FullyDefined`` property
|
|
2401
|
+
|
|
2402
|
+
All raw values are normalised through ``_probe_to_flag`` which handles
|
|
2403
|
+
bool, int, float, and string representations. When the targeted sketch
|
|
2404
|
+
cannot be found, the active model\'s current sketch is probed instead.
|
|
2405
|
+
|
|
2406
|
+
Args:
|
|
2407
|
+
adapter: A ``PyWin32Adapter`` with a non-``None`` ``currentModel``.
|
|
2408
|
+
sketch_name: Name of the sketch feature as it appears in the
|
|
2409
|
+
FeatureManager tree (e.g. ``"Sketch1"``). When ``None``, the
|
|
2410
|
+
adapter\'s ``_last_sketch_name`` is used, then the currently open
|
|
2411
|
+
sketch.
|
|
2412
|
+
|
|
2413
|
+
Returns:
|
|
2414
|
+
AdapterResult[dict[str, Any]]: On success, ``data`` is a dict:
|
|
2415
|
+
|
|
2416
|
+
.. code-block:: python
|
|
2417
|
+
|
|
2418
|
+
{
|
|
2419
|
+
"sketch_name": "Sketch1",
|
|
2420
|
+
"fully_defined": True, # bool or None
|
|
2421
|
+
"state": "fully_defined", # or "not_fully_defined" / "unknown"
|
|
2422
|
+
"source_api": "IsFullyConstrained", # probe that succeeded
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
On failure, ``status`` is ``ERROR``.
|
|
2426
|
+
|
|
2427
|
+
Raises:
|
|
2428
|
+
Exception: Propagated through ``_handle_com_operation`` on unexpected
|
|
2429
|
+
COM errors.
|
|
2430
|
+
|
|
2431
|
+
Example::
|
|
2432
|
+
|
|
2433
|
+
result = pywin32_sketch_ops.check_sketch_fully_defined(adapter, "Sketch1")
|
|
2434
|
+
if result.data["fully_defined"]:
|
|
2435
|
+
print("Sketch is fully constrained")
|
|
2436
|
+
else:
|
|
2437
|
+
print("Sketch state:", result.data["state"])
|
|
2438
|
+
"""
|
|
2439
|
+
if not adapter.currentModel:
|
|
2440
|
+
return AdapterResult(status=AdapterResultStatus.ERROR, error="No active model")
|
|
2441
|
+
|
|
2442
|
+
def _get_sketch_payload() -> dict[str, Any]:
|
|
2443
|
+
"""Main inner closure that locates the sketch and probes its constraint status.
|
|
2444
|
+
|
|
2445
|
+
Resolves the sketch feature by name (falling back to the last-known
|
|
2446
|
+
name and then the currently open sketch). Iterates through multiple
|
|
2447
|
+
API probes in priority order, stops at the first conclusive result.
|
|
2448
|
+
|
|
2449
|
+
Returns:
|
|
2450
|
+
dict[str, Any]: Payload dict with keys ``sketch_name``,
|
|
2451
|
+
``fully_defined``, ``state``, and ``source_api``.
|
|
2452
|
+
|
|
2453
|
+
Raises:
|
|
2454
|
+
Exception: On unexpected COM errors during feature lookup.
|
|
2455
|
+
"""
|
|
2456
|
+
sketch_feature = None
|
|
2457
|
+
sketch_obj = None
|
|
2458
|
+
resolved_name = sketch_name
|
|
2459
|
+
|
|
2460
|
+
if sketch_name:
|
|
2461
|
+
sketch_feature = adapter._attempt(
|
|
2462
|
+
lambda: adapter.currentModel.FeatureByName(sketch_name), default=None
|
|
2463
|
+
)
|
|
2464
|
+
if not sketch_feature:
|
|
2465
|
+
raise Exception(f"Sketch not found: {sketch_name}")
|
|
2466
|
+
sketch_obj = adapter._attempt(
|
|
2467
|
+
lambda: sketch_feature.GetSpecificFeature2(), default=None
|
|
2468
|
+
)
|
|
2469
|
+
else:
|
|
2470
|
+
sketch_obj = adapter.currentSketch or adapter._attempt(
|
|
2471
|
+
lambda: adapter.currentModel.GetActiveSketch2(), default=None
|
|
2472
|
+
)
|
|
2473
|
+
if sketch_obj is None and adapter._last_sketch_name:
|
|
2474
|
+
sketch_feature = adapter._attempt(
|
|
2475
|
+
lambda: adapter.currentModel.FeatureByName(
|
|
2476
|
+
adapter._last_sketch_name
|
|
2477
|
+
),
|
|
2478
|
+
default=None,
|
|
2479
|
+
)
|
|
2480
|
+
sketch_obj = adapter._attempt(
|
|
2481
|
+
lambda: sketch_feature.GetSpecificFeature2(), default=None
|
|
2482
|
+
)
|
|
2483
|
+
resolved_name = adapter._last_sketch_name
|
|
2484
|
+
|
|
2485
|
+
if sketch_obj is None and sketch_feature is None:
|
|
2486
|
+
raise Exception("No active sketch found")
|
|
2487
|
+
|
|
2488
|
+
probes: list[tuple[str, Any]] = []
|
|
2489
|
+
|
|
2490
|
+
for label, obj in (("sketch", sketch_obj), ("feature", sketch_feature)):
|
|
2491
|
+
if obj is None:
|
|
2492
|
+
continue
|
|
2493
|
+
for attr_name in (
|
|
2494
|
+
"GetFullyDefined",
|
|
2495
|
+
"IsFullyDefined",
|
|
2496
|
+
"FullyDefined",
|
|
2497
|
+
"GetFullyDefinedStatus",
|
|
2498
|
+
"GetSketchStatus",
|
|
2499
|
+
"GetStatus",
|
|
2500
|
+
"GetUnderDefined",
|
|
2501
|
+
"IsUnderDefined",
|
|
2502
|
+
"UnderDefined",
|
|
2503
|
+
"GetUnderDefinedCount",
|
|
2504
|
+
"UnderDefinedCount",
|
|
2505
|
+
"GetUnderDefinedEntitiesCount",
|
|
2506
|
+
"GetUnderDefinedSketchEntitiesCount",
|
|
2507
|
+
"GetOverDefinedCount",
|
|
2508
|
+
"OverDefinedCount",
|
|
2509
|
+
"GetFullyDefineStatus",
|
|
2510
|
+
):
|
|
2511
|
+
value = adapter._attempt(
|
|
2512
|
+
lambda o=obj, a=attr_name: adapter._get_attr_or_call(o, a),
|
|
2513
|
+
default=None,
|
|
2514
|
+
)
|
|
2515
|
+
if value is None:
|
|
2516
|
+
continue
|
|
2517
|
+
probes.append((f"{label}.{attr_name}", value))
|
|
2518
|
+
|
|
2519
|
+
for source, raw in probes:
|
|
2520
|
+
flag = _probe_to_flag(source, raw)
|
|
2521
|
+
if flag is None:
|
|
2522
|
+
continue
|
|
2523
|
+
|
|
2524
|
+
return {
|
|
2525
|
+
"sketch_name": resolved_name,
|
|
2526
|
+
"is_fully_defined": flag,
|
|
2527
|
+
"definition_state": _state_from_bool(flag),
|
|
2528
|
+
"source": source,
|
|
2529
|
+
"raw_status": raw,
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
return {
|
|
2533
|
+
"sketch_name": resolved_name,
|
|
2534
|
+
"is_fully_defined": None,
|
|
2535
|
+
"definition_state": "unknown",
|
|
2536
|
+
"source": "unavailable",
|
|
2537
|
+
"raw_status": None,
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
return cast(
|
|
2541
|
+
AdapterResult[dict[str, Any]],
|
|
2542
|
+
adapter._handle_com_operation(
|
|
2543
|
+
"check_sketch_fully_defined", _get_sketch_payload
|
|
2544
|
+
),
|
|
2545
|
+
)
|