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,753 @@
|
|
|
1
|
+
"""Overlap-free placement of drawing dimensions (pure geometry, no COM).
|
|
2
|
+
|
|
3
|
+
Rule enforced: **nothing on the sheet may overlap.** A dimension's text box must
|
|
4
|
+
not intersect another dimension's text, be crossed by another dimension's lines,
|
|
5
|
+
sit on any view's geometry, touch an obstacle (title block, view captions, note
|
|
6
|
+
blocks) or leave the working area of the frame. A dimension's lines must not
|
|
7
|
+
cross another view, and its extension lines must not run across its own view to
|
|
8
|
+
the far side.
|
|
9
|
+
|
|
10
|
+
``InsertModelAnnotations3`` places imported dimensions where they sit in the
|
|
11
|
+
model sketch, which on a drawing view lands them *inside* the view outline, on
|
|
12
|
+
top of the part and of each other (measured live on SW 2025).
|
|
13
|
+
|
|
14
|
+
:func:`plan_layout` moves each dimension outside its view, following the
|
|
15
|
+
A3 drafting standard
|
|
16
|
+
(``drawing_standard.py``): horizontal dimensions in rows above/below the part, rows 7 mm
|
|
17
|
+
apart; vertical dimensions in columns left/right, 9 mm apart; the first row
|
|
18
|
+
8 mm from the outline, on the side its extension lines start from. Others
|
|
19
|
+
(diameters, radii, angles) keep their leader on the feature and step their
|
|
20
|
+
text outward from it. Rows are filled shortest span first, so the extension lines of longer
|
|
21
|
+
outer dimensions clear the inner text. Every candidate position is checked
|
|
22
|
+
against everything already placed; the first free one wins.
|
|
23
|
+
|
|
24
|
+
All coordinates are sheet metres; boxes and segments are ``(x0, y0, x1, y1)``.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import math
|
|
30
|
+
import re
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
|
|
33
|
+
Box = tuple[float, float, float, float]
|
|
34
|
+
Segment = tuple[float, float, float, float]
|
|
35
|
+
|
|
36
|
+
#: Clearance kept around every text box when testing overlaps (0.75 mm).
|
|
37
|
+
TEXT_PADDING = 0.00075
|
|
38
|
+
#: First row of dimensions: 8 mm from the part outline.
|
|
39
|
+
FIRST_OFFSET = 0.008
|
|
40
|
+
#: Horizontal-dimension rows 7 mm apart.
|
|
41
|
+
ROW_PITCH = 0.007
|
|
42
|
+
#: Vertical-dimension columns 9 mm apart.
|
|
43
|
+
COLUMN_PITCH = 0.009
|
|
44
|
+
#: Gap used when a text box is bigger than the standard pitch allows.
|
|
45
|
+
STACK_GAP = 0.0035
|
|
46
|
+
#: Outward steps tried per side before giving up on that side.
|
|
47
|
+
MAX_STEPS = 12
|
|
48
|
+
#: A leader's text is tried every 4 mm outward from its feature.
|
|
49
|
+
LEADER_STEP = 0.004
|
|
50
|
+
#: Another view's outline is grown by 0.2 mm, so a line along its edge counts.
|
|
51
|
+
EDGE_TOLERANCE = 0.0002
|
|
52
|
+
#: More than 1 mm of a line on another view is a crossing; touching is not.
|
|
53
|
+
MIN_CROSSING = 0.001
|
|
54
|
+
#: A leader is planned at least 2 mm from any text. SolidWorks attaches it to
|
|
55
|
+
#: its text a little differently from the model here, and a leader planned to
|
|
56
|
+
#: just miss a "10" grazed it (seen live on SW 2026); the check stays exact.
|
|
57
|
+
LEADER_CLEARANCE = 0.002
|
|
58
|
+
#: A leader's text may sit inside its own view (a hole note on a plate) when
|
|
59
|
+
#: it is this far inside the view's outline ...
|
|
60
|
+
INSIDE_MARGIN = 0.008
|
|
61
|
+
#: ... and this far from every point a dimension starts from (hole centres,
|
|
62
|
+
#: edge points, the leader's own hole), the features the view shows there.
|
|
63
|
+
FEATURE_CLEARANCE = 0.006
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _grown(box: Box, by: float) -> Box:
|
|
67
|
+
"""``box`` grown by ``by`` on every side."""
|
|
68
|
+
return (box[0] - by, box[1] - by, box[2] + by, box[3] + by)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def shown_length(text: str) -> int:
|
|
72
|
+
"""Characters a dimension's text shows: each SolidWorks code is one symbol.
|
|
73
|
+
|
|
74
|
+
``IDisplayData::GetTextAtIndex`` returns codes such as ``<MOD-DIAM>``
|
|
75
|
+
for the Ø sign; counted as typed, they make the text look three times
|
|
76
|
+
wider than it is drawn.
|
|
77
|
+
"""
|
|
78
|
+
return len(re.sub(r"<[^<>]*>", "X", text).strip())
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def text_box(x: float, y: float, width: float, height: float, angle: float) -> Box:
|
|
82
|
+
"""Sheet box of a text run anchored at its lower-left corner and turned.
|
|
83
|
+
|
|
84
|
+
``IDisplayData`` gives each text's lower-left corner, length, height and
|
|
85
|
+
angle. A vertical dimension's text is turned 90 degrees, so it runs up the
|
|
86
|
+
sheet from that corner and lies to its left (measured live on SW 2026).
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
x: Lower-left corner x.
|
|
90
|
+
y: Lower-left corner y.
|
|
91
|
+
width: Text length along its baseline.
|
|
92
|
+
height: Text height.
|
|
93
|
+
angle: Baseline angle in radians, counter-clockwise.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Box: The axis-aligned box around the turned text.
|
|
97
|
+
"""
|
|
98
|
+
c, s = math.cos(angle), math.sin(angle)
|
|
99
|
+
corners = [(0.0, 0.0), (width, 0.0), (0.0, height), (width, height)]
|
|
100
|
+
xs = [x + u * c - v * s for u, v in corners]
|
|
101
|
+
ys = [y + u * s + v * c for u, v in corners]
|
|
102
|
+
return (min(xs), min(ys), max(xs), max(ys))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def crosses_other_view(
|
|
106
|
+
lines: list[Segment], own_view: str, outlines: dict[str, Box]
|
|
107
|
+
) -> str | None:
|
|
108
|
+
"""Return the first other view any of ``lines`` runs across or along.
|
|
109
|
+
|
|
110
|
+
A dimension's extension lines start on its own view's geometry, so its own
|
|
111
|
+
outline is exempt. On any other view, more than :data:`MIN_CROSSING` of a
|
|
112
|
+
line inside that outline (grown by :data:`EDGE_TOLERANCE`, so a line lying
|
|
113
|
+
along its edge counts) is a crossing; a line ending at the edge is not.
|
|
114
|
+
Measured live on SW 2026: a front view's height dimension parked beyond
|
|
115
|
+
the side view drew its extension lines along the side view's top and
|
|
116
|
+
bottom edges, straight across it.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
lines: The dimension's line segments.
|
|
120
|
+
own_view: The view the dimension belongs to.
|
|
121
|
+
outlines: View name -> view outline.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
str | None: The crossed view's name, or ``None`` when clear.
|
|
125
|
+
"""
|
|
126
|
+
t = EDGE_TOLERANCE
|
|
127
|
+
for view, o in outlines.items():
|
|
128
|
+
if view == own_view:
|
|
129
|
+
continue
|
|
130
|
+
grown = (o[0] - t, o[1] - t, o[2] + t, o[3] + t)
|
|
131
|
+
if any(length_inside(s, grown) > MIN_CROSSING for s in lines):
|
|
132
|
+
return view
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class DimensionGeometry:
|
|
138
|
+
"""One drawing dimension as read from ``IAnnotation::GetDisplayData``.
|
|
139
|
+
|
|
140
|
+
Attributes:
|
|
141
|
+
key: Stable identifier (view name + index).
|
|
142
|
+
view: Owning view name.
|
|
143
|
+
text_box: Current text box on the sheet.
|
|
144
|
+
position: ``IAnnotation::GetPosition`` (x, y) - what ``SetPosition`` moves.
|
|
145
|
+
lines: Every line segment of the dimension (extension + dimension lines).
|
|
146
|
+
orientation: ``"h"``, ``"v"`` or ``None``; set by :func:`plan_layout`.
|
|
147
|
+
span: Distance between the extension lines (0 when unknown).
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
key: str
|
|
151
|
+
view: str
|
|
152
|
+
text_box: Box
|
|
153
|
+
position: tuple[float, float]
|
|
154
|
+
lines: list[Segment] = field(default_factory=list)
|
|
155
|
+
orientation: str | None = None
|
|
156
|
+
span: float = 0.0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def attach_boxes(dims: list[DimensionGeometry], attached: dict[str, Box]) -> None:
|
|
160
|
+
"""Grow each dimension's text box by what hangs off it, such as a GD&T frame.
|
|
161
|
+
|
|
162
|
+
A frame attached to a dimension moves with it, so its box is given
|
|
163
|
+
relative to the dimension's position and counts as part of its text.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
dims: Dimensions as read; updated in place.
|
|
167
|
+
attached: Dimension key -> box relative to that dimension's position.
|
|
168
|
+
"""
|
|
169
|
+
for dim in dims:
|
|
170
|
+
rel = attached.get(dim.key)
|
|
171
|
+
if rel is None:
|
|
172
|
+
continue
|
|
173
|
+
x, y = dim.position
|
|
174
|
+
tb = dim.text_box
|
|
175
|
+
dim.text_box = (
|
|
176
|
+
min(tb[0], x + rel[0]),
|
|
177
|
+
min(tb[1], y + rel[1]),
|
|
178
|
+
max(tb[2], x + rel[2]),
|
|
179
|
+
max(tb[3], y + rel[3]),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def box_overlap(a: Box, b: Box, pad: float = 0.0) -> bool:
|
|
184
|
+
"""Return True when two boxes intersect (after growing each by ``pad``)."""
|
|
185
|
+
return not (
|
|
186
|
+
a[2] + pad <= b[0] - pad
|
|
187
|
+
or b[2] + pad <= a[0] - pad
|
|
188
|
+
or a[3] + pad <= b[1] - pad
|
|
189
|
+
or b[3] + pad <= a[1] - pad
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def inside(box: Box, area: Box) -> bool:
|
|
194
|
+
"""Return True when ``box`` lies entirely within ``area``."""
|
|
195
|
+
return (
|
|
196
|
+
area[0] <= box[0]
|
|
197
|
+
and box[2] <= area[2]
|
|
198
|
+
and area[1] <= box[1]
|
|
199
|
+
and box[3] <= area[3]
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def segment_hits_box(seg: Segment, box: Box) -> bool:
|
|
204
|
+
"""Liang-Barsky clip: True when the segment passes through the box."""
|
|
205
|
+
x0, y0, x1, y1 = seg
|
|
206
|
+
dx, dy = x1 - x0, y1 - y0
|
|
207
|
+
t0, t1 = 0.0, 1.0
|
|
208
|
+
for p, q in (
|
|
209
|
+
(-dx, x0 - box[0]),
|
|
210
|
+
(dx, box[2] - x0),
|
|
211
|
+
(-dy, y0 - box[1]),
|
|
212
|
+
(dy, box[3] - y0),
|
|
213
|
+
):
|
|
214
|
+
if abs(p) < 1e-12:
|
|
215
|
+
if q < 0:
|
|
216
|
+
return False
|
|
217
|
+
continue
|
|
218
|
+
t = q / p
|
|
219
|
+
if p < 0:
|
|
220
|
+
t0 = max(t0, t)
|
|
221
|
+
else:
|
|
222
|
+
t1 = min(t1, t)
|
|
223
|
+
if t0 > t1:
|
|
224
|
+
return False
|
|
225
|
+
return True
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def length_inside(seg: Segment, box: Box) -> float:
|
|
229
|
+
"""Length of the part of a segment that lies inside a box (Liang-Barsky).
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
seg: The segment.
|
|
233
|
+
box: The box.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
float: 0.0 when the segment misses the box.
|
|
237
|
+
"""
|
|
238
|
+
x0, y0, x1, y1 = seg
|
|
239
|
+
dx, dy = x1 - x0, y1 - y0
|
|
240
|
+
t0, t1 = 0.0, 1.0
|
|
241
|
+
for p, q in (
|
|
242
|
+
(-dx, x0 - box[0]),
|
|
243
|
+
(dx, box[2] - x0),
|
|
244
|
+
(-dy, y0 - box[1]),
|
|
245
|
+
(dy, box[3] - y0),
|
|
246
|
+
):
|
|
247
|
+
if abs(p) < 1e-12:
|
|
248
|
+
if q < 0:
|
|
249
|
+
return 0.0
|
|
250
|
+
continue
|
|
251
|
+
t = q / p
|
|
252
|
+
if p < 0:
|
|
253
|
+
t0 = max(t0, t)
|
|
254
|
+
else:
|
|
255
|
+
t1 = min(t1, t)
|
|
256
|
+
if t0 > t1:
|
|
257
|
+
return 0.0
|
|
258
|
+
return (t1 - t0) * (dx * dx + dy * dy) ** 0.5
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def shift_box(box: Box, dx: float, dy: float) -> Box:
|
|
262
|
+
"""Translate a box."""
|
|
263
|
+
return (box[0] + dx, box[1] + dy, box[2] + dx, box[3] + dy)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def classify(lines: list[Segment]) -> tuple[str | None, float, list[float]]:
|
|
267
|
+
"""Work out orientation, span and extension-line coordinates.
|
|
268
|
+
|
|
269
|
+
SolidWorks returns a linear dimension's two extension lines first. Both
|
|
270
|
+
vertical means a horizontal dimension (their x values are the span ends);
|
|
271
|
+
both horizontal means a vertical one.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
tuple: orientation, span, and the two extension-line coordinates.
|
|
275
|
+
"""
|
|
276
|
+
if len(lines) >= 2:
|
|
277
|
+
a, b = lines[0], lines[1]
|
|
278
|
+
tol = 1e-6
|
|
279
|
+
if abs(a[0] - a[2]) < tol and abs(b[0] - b[2]) < tol and abs(a[0] - b[0]) > tol:
|
|
280
|
+
return "h", abs(a[0] - b[0]), sorted([a[0], b[0]])
|
|
281
|
+
if abs(a[1] - a[3]) < tol and abs(b[1] - b[3]) < tol and abs(a[1] - b[1]) > tol:
|
|
282
|
+
return "v", abs(a[1] - b[1]), sorted([a[1], b[1]])
|
|
283
|
+
return None, 0.0, []
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def extension_origins(dim: DimensionGeometry) -> list[float]:
|
|
287
|
+
"""Where each extension line starts on the part, across the dimension.
|
|
288
|
+
|
|
289
|
+
An extension line runs from the part to just past the dimension line, so
|
|
290
|
+
its start is the end farther from the text: a y for a horizontal
|
|
291
|
+
dimension, an x for a vertical one.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
dim: The dimension.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
list[float]: One coordinate per extension line; empty for a leader.
|
|
298
|
+
"""
|
|
299
|
+
orient = classify(dim.lines)[0]
|
|
300
|
+
if orient is None:
|
|
301
|
+
return []
|
|
302
|
+
axis = 1 if orient == "h" else 0
|
|
303
|
+
centre = (dim.text_box[axis] + dim.text_box[axis + 2]) / 2.0
|
|
304
|
+
return [
|
|
305
|
+
max((s[axis], s[axis + 2]), key=lambda c: abs(c - centre))
|
|
306
|
+
for s in dim.lines[:2]
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def crosses_own_view(dim: DimensionGeometry, outline: Box) -> bool:
|
|
311
|
+
"""True when an extension line runs across its own view to the far side.
|
|
312
|
+
|
|
313
|
+
Measured live on SW 2026: a height dimension moved from the left of the
|
|
314
|
+
top view to its right drew both extension lines straight across the view.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
dim: The dimension.
|
|
318
|
+
outline: Its view's outline.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
bool: ``False`` for leaders, which have no extension lines.
|
|
322
|
+
"""
|
|
323
|
+
axis = 1 if classify(dim.lines)[0] == "h" else 0
|
|
324
|
+
low, high = outline[axis], outline[axis + 2]
|
|
325
|
+
middle = (low + high) / 2.0
|
|
326
|
+
for start, line in zip(extension_origins(dim), dim.lines, strict=False):
|
|
327
|
+
end = line[axis + 2] if line[axis] == start else line[axis]
|
|
328
|
+
if (start < middle and end > high) or (start > middle and end < low):
|
|
329
|
+
return True
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def leader_anchor(dim: DimensionGeometry) -> tuple[float, float] | None:
|
|
334
|
+
"""Where a leader (diameter, radius) meets the part: the line end farthest from the text.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
dim: The dimension.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
tuple[float, float] | None: The anchor, or ``None`` with no lines.
|
|
341
|
+
"""
|
|
342
|
+
cx = (dim.text_box[0] + dim.text_box[2]) / 2.0
|
|
343
|
+
cy = (dim.text_box[1] + dim.text_box[3]) / 2.0
|
|
344
|
+
ends = [(s[0], s[1]) for s in dim.lines] + [(s[2], s[3]) for s in dim.lines]
|
|
345
|
+
if not ends:
|
|
346
|
+
return None
|
|
347
|
+
return max(ends, key=lambda p: (p[0] - cx) ** 2 + (p[1] - cy) ** 2)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def feature_points(
|
|
351
|
+
dims: list[DimensionGeometry], view: str, outline: Box, skip: str | None = None
|
|
352
|
+
) -> list[tuple[float, float]]:
|
|
353
|
+
"""Points inside a view where its dimensions start: its features.
|
|
354
|
+
|
|
355
|
+
Every line end of the view's dimensions (but ``skip``'s) that lies inside
|
|
356
|
+
the outline: extension lines start on holes and edges, leaders on the
|
|
357
|
+
feature they point at.
|
|
358
|
+
"""
|
|
359
|
+
inner = _grown(outline, -0.0005)
|
|
360
|
+
return [
|
|
361
|
+
p
|
|
362
|
+
for d in dims
|
|
363
|
+
if d.view == view and d.key != skip
|
|
364
|
+
for s in d.lines
|
|
365
|
+
for p in ((s[0], s[1]), (s[2], s[3]))
|
|
366
|
+
if inner[0] < p[0] < inner[2] and inner[1] < p[1] < inner[3]
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def clear_inside(box: Box, outline: Box, points: list[tuple[float, float]]) -> bool:
|
|
371
|
+
"""True when ``box`` sits well inside ``outline`` and clear of ``points``."""
|
|
372
|
+
room = _grown(outline, -INSIDE_MARGIN)
|
|
373
|
+
if not (
|
|
374
|
+
room[0] <= box[0]
|
|
375
|
+
and room[1] <= box[1]
|
|
376
|
+
and box[2] <= room[2]
|
|
377
|
+
and box[3] <= room[3]
|
|
378
|
+
):
|
|
379
|
+
return False
|
|
380
|
+
near = _grown(box, FEATURE_CLEARANCE)
|
|
381
|
+
return not any(
|
|
382
|
+
near[0] <= x <= near[2] and near[1] <= y <= near[3] for x, y in points
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _leader_inside(
|
|
387
|
+
dim: DimensionGeometry, dims: list[DimensionGeometry], outline: Box
|
|
388
|
+
) -> bool:
|
|
389
|
+
"""A leader whose text sits clear inside its own view (see :func:`clear_inside`)."""
|
|
390
|
+
anchor = leader_anchor(dim)
|
|
391
|
+
if classify(dim.lines)[0] is not None or anchor is None:
|
|
392
|
+
return False
|
|
393
|
+
points = [*feature_points(dims, dim.view, outline, skip=dim.key), anchor]
|
|
394
|
+
return clear_inside(dim.text_box, outline, points)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _inside_candidates(
|
|
398
|
+
dim: DimensionGeometry, outline: Box
|
|
399
|
+
) -> list[tuple[str, float, float]]:
|
|
400
|
+
"""Moves putting a leader's text inside its view, toward the view's centre."""
|
|
401
|
+
anchor = leader_anchor(dim)
|
|
402
|
+
tb = dim.text_box
|
|
403
|
+
w, h = tb[2] - tb[0], tb[3] - tb[1]
|
|
404
|
+
cx, cy = (tb[0] + tb[2]) / 2.0, (tb[1] + tb[3]) / 2.0
|
|
405
|
+
ox, oy = (outline[0] + outline[2]) / 2.0, (outline[1] + outline[3]) / 2.0
|
|
406
|
+
ux, uy = ox - anchor[0], oy - anchor[1]
|
|
407
|
+
length = math.hypot(ux, uy) or 1.0
|
|
408
|
+
ux, uy = ux / length, uy / length
|
|
409
|
+
out = []
|
|
410
|
+
for step in range(2 * MAX_STEPS):
|
|
411
|
+
reach = FIRST_OFFSET + step * LEADER_STEP
|
|
412
|
+
tx = anchor[0] + ux * reach + math.copysign(w / 2.0, ux)
|
|
413
|
+
ty = anchor[1] + uy * reach + math.copysign(h / 2.0, uy)
|
|
414
|
+
out.append(("inside", tx - cx, ty - cy))
|
|
415
|
+
return out
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _leader_to(anchor: tuple[float, float], box: Box) -> Segment:
|
|
419
|
+
"""The leader from ``anchor`` to the nearest point of the text ``box``."""
|
|
420
|
+
x = min(max(anchor[0], box[0]), box[2])
|
|
421
|
+
y = min(max(anchor[1], box[1]), box[3])
|
|
422
|
+
return (anchor[0], anchor[1], x, y)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _projected_segments(
|
|
426
|
+
dim: DimensionGeometry, dx: float, dy: float, outline: Box
|
|
427
|
+
) -> list[Segment]:
|
|
428
|
+
"""Approximate a moved dimension's new dimension and extension lines.
|
|
429
|
+
|
|
430
|
+
The extension lines still start where they did on the part and now reach
|
|
431
|
+
the moved dimension line. A leader stays fixed to the feature it points
|
|
432
|
+
at and runs from there to the moved text; moving it with the text, as
|
|
433
|
+
before, hid a diameter leader drawn across the part and two neighbouring
|
|
434
|
+
dimensions (measured live on SW 2026).
|
|
435
|
+
"""
|
|
436
|
+
orient, _span, ends = classify(dim.lines)
|
|
437
|
+
box = shift_box(dim.text_box, dx, dy)
|
|
438
|
+
origins = extension_origins(dim)
|
|
439
|
+
if orient == "h":
|
|
440
|
+
y = (box[1] + box[3]) / 2.0
|
|
441
|
+
return [(ends[0], y, ends[1], y)] + [
|
|
442
|
+
(s[0], o, s[0], y) for s, o in zip(dim.lines, origins, strict=False)
|
|
443
|
+
]
|
|
444
|
+
if orient == "v":
|
|
445
|
+
x = (box[0] + box[2]) / 2.0
|
|
446
|
+
return [(x, ends[0], x, ends[1])] + [
|
|
447
|
+
(o, s[1], x, s[1]) for s, o in zip(dim.lines, origins, strict=False)
|
|
448
|
+
]
|
|
449
|
+
anchor = leader_anchor(dim)
|
|
450
|
+
return [] if anchor is None else [_leader_to(anchor, box)]
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def find_overlaps(
|
|
454
|
+
dims: list[DimensionGeometry],
|
|
455
|
+
outlines: dict[str, Box],
|
|
456
|
+
sheet: Box | None = None,
|
|
457
|
+
obstacles: dict[str, Box] | None = None,
|
|
458
|
+
) -> list[dict[str, str]]:
|
|
459
|
+
"""List every rule violation among dimensions as they currently sit.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
dims: Dimensions with their real (read-back) geometry.
|
|
463
|
+
outlines: View name -> view outline box.
|
|
464
|
+
sheet: Working area of the frame, when known.
|
|
465
|
+
obstacles: Named boxes nothing may touch (title block, captions, notes).
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
list[dict[str, str]]: ``{"dimension", "conflicts_with", "kind"}`` entries.
|
|
469
|
+
"""
|
|
470
|
+
problems: list[dict[str, str]] = []
|
|
471
|
+
for i, dim in enumerate(dims):
|
|
472
|
+
for view, outline in outlines.items():
|
|
473
|
+
if view == dim.view and _leader_inside(dim, dims, outline):
|
|
474
|
+
continue
|
|
475
|
+
if box_overlap(dim.text_box, outline, -TEXT_PADDING / 2):
|
|
476
|
+
problems.append(
|
|
477
|
+
{
|
|
478
|
+
"dimension": dim.key,
|
|
479
|
+
"conflicts_with": view,
|
|
480
|
+
"kind": "on_view_geometry",
|
|
481
|
+
}
|
|
482
|
+
)
|
|
483
|
+
crossed = crosses_other_view(dim.lines, dim.view, outlines)
|
|
484
|
+
if crossed is not None:
|
|
485
|
+
problems.append(
|
|
486
|
+
{
|
|
487
|
+
"dimension": dim.key,
|
|
488
|
+
"conflicts_with": crossed,
|
|
489
|
+
"kind": "line_through_view",
|
|
490
|
+
}
|
|
491
|
+
)
|
|
492
|
+
own = outlines.get(dim.view)
|
|
493
|
+
if own is not None and crosses_own_view(dim, own):
|
|
494
|
+
problems.append(
|
|
495
|
+
{
|
|
496
|
+
"dimension": dim.key,
|
|
497
|
+
"conflicts_with": dim.view,
|
|
498
|
+
"kind": "line_through_own_view",
|
|
499
|
+
}
|
|
500
|
+
)
|
|
501
|
+
for label, obstacle in (obstacles or {}).items():
|
|
502
|
+
if box_overlap(dim.text_box, obstacle, TEXT_PADDING / 2) or any(
|
|
503
|
+
segment_hits_box(s, obstacle) for s in dim.lines
|
|
504
|
+
):
|
|
505
|
+
problems.append(
|
|
506
|
+
{
|
|
507
|
+
"dimension": dim.key,
|
|
508
|
+
"conflicts_with": label,
|
|
509
|
+
"kind": "on_obstacle",
|
|
510
|
+
}
|
|
511
|
+
)
|
|
512
|
+
if sheet is not None and not inside(dim.text_box, sheet):
|
|
513
|
+
problems.append(
|
|
514
|
+
{
|
|
515
|
+
"dimension": dim.key,
|
|
516
|
+
"conflicts_with": "frame",
|
|
517
|
+
"kind": "outside_working_area",
|
|
518
|
+
}
|
|
519
|
+
)
|
|
520
|
+
for j, other in enumerate(dims):
|
|
521
|
+
if j == i:
|
|
522
|
+
continue
|
|
523
|
+
if j > i and box_overlap(dim.text_box, other.text_box, TEXT_PADDING):
|
|
524
|
+
problems.append(
|
|
525
|
+
{
|
|
526
|
+
"dimension": dim.key,
|
|
527
|
+
"conflicts_with": other.key,
|
|
528
|
+
"kind": "text_on_text",
|
|
529
|
+
}
|
|
530
|
+
)
|
|
531
|
+
if any(segment_hits_box(s, dim.text_box) for s in other.lines):
|
|
532
|
+
problems.append(
|
|
533
|
+
{
|
|
534
|
+
"dimension": dim.key,
|
|
535
|
+
"conflicts_with": other.key,
|
|
536
|
+
"kind": "line_through_text",
|
|
537
|
+
}
|
|
538
|
+
)
|
|
539
|
+
return problems
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _candidates(dim: DimensionGeometry, outline: Box) -> list[tuple[str, float, float]]:
|
|
543
|
+
"""Ordered (side, dx, dy) moves to try for one dimension.
|
|
544
|
+
|
|
545
|
+
A linear dimension only goes to the side its extension lines start from;
|
|
546
|
+
on the far side they would cross its own view. Rows and columns are tried
|
|
547
|
+
from the innermost out, so dimensions that do not touch share a row; the
|
|
548
|
+
caller's checks push one outward when its text or lines would collide.
|
|
549
|
+
"""
|
|
550
|
+
tb = dim.text_box
|
|
551
|
+
w, h = tb[2] - tb[0], tb[3] - tb[1]
|
|
552
|
+
cx, cy = (tb[0] + tb[2]) / 2.0, (tb[1] + tb[3]) / 2.0
|
|
553
|
+
ox, oy = (outline[0] + outline[2]) / 2.0, (outline[1] + outline[3]) / 2.0
|
|
554
|
+
origins = extension_origins(dim)
|
|
555
|
+
out: list[tuple[str, float, float]] = []
|
|
556
|
+
if dim.orientation == "h":
|
|
557
|
+
sides = list(dict.fromkeys("above" if o >= oy else "below" for o in origins))
|
|
558
|
+
pitch = max(ROW_PITCH, h + STACK_GAP)
|
|
559
|
+
for side in sides:
|
|
560
|
+
for step in range(MAX_STEPS):
|
|
561
|
+
off = FIRST_OFFSET + step * pitch
|
|
562
|
+
target = outline[3] + off if side == "above" else outline[1] - off
|
|
563
|
+
out.append((side, 0.0, target - cy))
|
|
564
|
+
elif dim.orientation == "v":
|
|
565
|
+
sides = list(dict.fromkeys("right" if o >= ox else "left" for o in origins))
|
|
566
|
+
pitch = max(COLUMN_PITCH, w + STACK_GAP)
|
|
567
|
+
for side in sides:
|
|
568
|
+
for step in range(MAX_STEPS):
|
|
569
|
+
off = FIRST_OFFSET + step * pitch
|
|
570
|
+
target = outline[2] + off if side == "right" else outline[0] - off
|
|
571
|
+
out.append((side, target - cx, 0.0))
|
|
572
|
+
else:
|
|
573
|
+
# A leader: step the text outward from the feature it points at,
|
|
574
|
+
# along each of eight directions, away from the view's centre first.
|
|
575
|
+
ax, ay = leader_anchor(dim) or (cx, cy)
|
|
576
|
+
root = 0.5**0.5
|
|
577
|
+
directions = [
|
|
578
|
+
(1.0, 0.0), (-1.0, 0.0), (0.0, 1.0), (0.0, -1.0),
|
|
579
|
+
(root, root), (-root, root), (root, -root), (-root, -root),
|
|
580
|
+
] # fmt: skip
|
|
581
|
+
directions.sort(key=lambda d: -((ax - ox) * d[0] + (ay - oy) * d[1]))
|
|
582
|
+
for ux, uy in directions:
|
|
583
|
+
for step in range(2 * MAX_STEPS):
|
|
584
|
+
reach = FIRST_OFFSET / 2.0 + step * LEADER_STEP
|
|
585
|
+
tx = ax + ux * reach + math.copysign(w / 2.0, ux or 1.0) * abs(ux)
|
|
586
|
+
ty = ay + uy * reach + math.copysign(h / 2.0, uy or 1.0) * abs(uy)
|
|
587
|
+
out.append((f"leader{ux:+.1f}{uy:+.1f}", tx - cx, ty - cy))
|
|
588
|
+
return out
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def plan_layout(
|
|
592
|
+
dims: list[DimensionGeometry],
|
|
593
|
+
outlines: dict[str, Box],
|
|
594
|
+
sheet: Box | None = None,
|
|
595
|
+
obstacles: dict[str, Box] | None = None,
|
|
596
|
+
fixed: set[str] | None = None,
|
|
597
|
+
inside_first: set[str] | None = None,
|
|
598
|
+
) -> tuple[dict[str, tuple[float, float]], list[str]]:
|
|
599
|
+
"""Choose a move for every dimension so the overlap rule holds.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
dims: Dimensions with their current geometry.
|
|
603
|
+
outlines: View name -> view outline.
|
|
604
|
+
sheet: Working area of the frame, when known.
|
|
605
|
+
obstacles: Named boxes nothing may touch.
|
|
606
|
+
fixed: Keys of dimensions that stay where they are: their real text
|
|
607
|
+
and lines are obstacles. Used to re-plan only what a first pass
|
|
608
|
+
left in conflict.
|
|
609
|
+
inside_first: Leader dimensions to try inside their own view first,
|
|
610
|
+
near the feature (a hole note with its GD&T frame), before
|
|
611
|
+
outside it; see :func:`clear_inside`.
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
tuple: ``{key: (dx, dy)}`` moves, and keys that found no free spot
|
|
615
|
+
(left in place and reported by the caller's final check).
|
|
616
|
+
"""
|
|
617
|
+
blocked = list((obstacles or {}).values())
|
|
618
|
+
for dim in dims:
|
|
619
|
+
dim.orientation, dim.span, _ = classify(dim.lines)
|
|
620
|
+
keep = fixed or set()
|
|
621
|
+
placed_boxes: list[Box] = []
|
|
622
|
+
placed_lines: list[Segment] = []
|
|
623
|
+
for dim in dims:
|
|
624
|
+
if dim.key in keep:
|
|
625
|
+
placed_boxes.append(dim.text_box)
|
|
626
|
+
placed_lines.extend(dim.lines)
|
|
627
|
+
order = sorted(
|
|
628
|
+
(d for d in dims if d.key not in keep),
|
|
629
|
+
key=lambda d: (d.view, d.orientation or "z", d.span),
|
|
630
|
+
)
|
|
631
|
+
moves: dict[str, tuple[float, float]] = {}
|
|
632
|
+
unplaced: list[str] = []
|
|
633
|
+
for dim in order:
|
|
634
|
+
outline = outlines.get(dim.view)
|
|
635
|
+
if outline is None:
|
|
636
|
+
unplaced.append(dim.key)
|
|
637
|
+
continue
|
|
638
|
+
chosen = None
|
|
639
|
+
candidates = _candidates(dim, outline)
|
|
640
|
+
if dim.key in (inside_first or set()) and dim.orientation is None:
|
|
641
|
+
candidates = _inside_candidates(dim, outline) + candidates
|
|
642
|
+
features = [
|
|
643
|
+
*feature_points(dims, dim.view, outline, skip=dim.key),
|
|
644
|
+
leader_anchor(dim),
|
|
645
|
+
]
|
|
646
|
+
for side, dx, dy in candidates:
|
|
647
|
+
box = shift_box(dim.text_box, dx, dy)
|
|
648
|
+
others = [
|
|
649
|
+
o for v, o in outlines.items() if side != "inside" or v != dim.view
|
|
650
|
+
]
|
|
651
|
+
if any(box_overlap(box, o, TEXT_PADDING) for o in others):
|
|
652
|
+
continue
|
|
653
|
+
if side == "inside" and not clear_inside(box, outline, features):
|
|
654
|
+
continue
|
|
655
|
+
if any(box_overlap(box, o, TEXT_PADDING) for o in blocked):
|
|
656
|
+
continue
|
|
657
|
+
if sheet is not None and not inside(box, sheet):
|
|
658
|
+
continue
|
|
659
|
+
if any(box_overlap(box, p, TEXT_PADDING) for p in placed_boxes):
|
|
660
|
+
continue
|
|
661
|
+
if any(segment_hits_box(s, box) for s in placed_lines):
|
|
662
|
+
continue
|
|
663
|
+
segs = _projected_segments(dim, dx, dy, outline)
|
|
664
|
+
if crosses_other_view(segs, dim.view, outlines) is not None:
|
|
665
|
+
continue
|
|
666
|
+
clear = LEADER_CLEARANCE if dim.orientation is None else 0.0
|
|
667
|
+
if any(
|
|
668
|
+
segment_hits_box(s, _grown(p, clear))
|
|
669
|
+
for s in segs
|
|
670
|
+
for p in placed_boxes + blocked
|
|
671
|
+
):
|
|
672
|
+
continue
|
|
673
|
+
chosen = (side, dx, dy, box, segs)
|
|
674
|
+
break
|
|
675
|
+
if chosen is None:
|
|
676
|
+
unplaced.append(dim.key)
|
|
677
|
+
placed_boxes.append(dim.text_box)
|
|
678
|
+
placed_lines.extend(dim.lines)
|
|
679
|
+
continue
|
|
680
|
+
_side, dx, dy, box, segs = chosen
|
|
681
|
+
moves[dim.key] = (dx, dy)
|
|
682
|
+
placed_boxes.append(box)
|
|
683
|
+
placed_lines.extend(segs)
|
|
684
|
+
return moves, unplaced
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def find_annotation_overlaps(
|
|
688
|
+
notes: dict[str, Box],
|
|
689
|
+
dims: list[DimensionGeometry],
|
|
690
|
+
outlines: dict[str, Box],
|
|
691
|
+
sheet: Box | None = None,
|
|
692
|
+
obstacles: dict[str, Box] | None = None,
|
|
693
|
+
) -> list[dict[str, str]]:
|
|
694
|
+
"""Check sheet notes and captions against everything else on the sheet.
|
|
695
|
+
|
|
696
|
+
Args:
|
|
697
|
+
notes: Label -> measured text box of each note or caption.
|
|
698
|
+
dims: Dimensions with their real geometry.
|
|
699
|
+
outlines: View outlines.
|
|
700
|
+
sheet: Working area of the frame.
|
|
701
|
+
obstacles: Named boxes notes may not touch (title block).
|
|
702
|
+
|
|
703
|
+
Returns:
|
|
704
|
+
list[dict[str, str]]: ``{"dimension", "conflicts_with", "kind"}`` entries
|
|
705
|
+
(``dimension`` holds the note label, for one report format).
|
|
706
|
+
"""
|
|
707
|
+
problems: list[dict[str, str]] = []
|
|
708
|
+
labels = list(notes)
|
|
709
|
+
for i, label in enumerate(labels):
|
|
710
|
+
box = notes[label]
|
|
711
|
+
for other in labels[i + 1 :]:
|
|
712
|
+
if box_overlap(box, notes[other], TEXT_PADDING / 2):
|
|
713
|
+
problems.append(
|
|
714
|
+
{
|
|
715
|
+
"dimension": label,
|
|
716
|
+
"conflicts_with": other,
|
|
717
|
+
"kind": "note_on_note",
|
|
718
|
+
}
|
|
719
|
+
)
|
|
720
|
+
for view, outline in outlines.items():
|
|
721
|
+
if box_overlap(box, outline, 0.0):
|
|
722
|
+
problems.append(
|
|
723
|
+
{"dimension": label, "conflicts_with": view, "kind": "note_on_view"}
|
|
724
|
+
)
|
|
725
|
+
for name, obstacle in (obstacles or {}).items():
|
|
726
|
+
if box_overlap(box, obstacle, 0.0):
|
|
727
|
+
problems.append(
|
|
728
|
+
{
|
|
729
|
+
"dimension": label,
|
|
730
|
+
"conflicts_with": name,
|
|
731
|
+
"kind": "note_on_obstacle",
|
|
732
|
+
}
|
|
733
|
+
)
|
|
734
|
+
if sheet is not None and not inside(box, sheet):
|
|
735
|
+
problems.append(
|
|
736
|
+
{
|
|
737
|
+
"dimension": label,
|
|
738
|
+
"conflicts_with": "frame",
|
|
739
|
+
"kind": "note_outside_working_area",
|
|
740
|
+
}
|
|
741
|
+
)
|
|
742
|
+
for dim in dims:
|
|
743
|
+
if box_overlap(box, dim.text_box, TEXT_PADDING / 2) or any(
|
|
744
|
+
segment_hits_box(s, box) for s in dim.lines
|
|
745
|
+
):
|
|
746
|
+
problems.append(
|
|
747
|
+
{
|
|
748
|
+
"dimension": label,
|
|
749
|
+
"conflicts_with": dim.key,
|
|
750
|
+
"kind": "note_on_dimension",
|
|
751
|
+
}
|
|
752
|
+
)
|
|
753
|
+
return problems
|