polyforge 0.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. polyforge/__init__.py +154 -0
  2. polyforge/clearance/__init__.py +62 -0
  3. polyforge/clearance/fix_clearance.py +474 -0
  4. polyforge/core/__init__.py +69 -0
  5. polyforge/core/cleanup.py +20 -0
  6. polyforge/core/constraints.py +297 -0
  7. polyforge/core/errors.py +259 -0
  8. polyforge/core/geometry_utils.py +395 -0
  9. polyforge/core/iterative_utils.py +134 -0
  10. polyforge/core/spatial_utils.py +334 -0
  11. polyforge/core/types.py +165 -0
  12. polyforge/merge/__init__.py +10 -0
  13. polyforge/merge/core.py +252 -0
  14. polyforge/metrics.py +133 -0
  15. polyforge/ops/__init__.py +10 -0
  16. polyforge/ops/cleanup_ops.py +129 -0
  17. polyforge/ops/clearance/__init__.py +36 -0
  18. polyforge/ops/clearance/holes.py +177 -0
  19. polyforge/ops/clearance/passages.py +760 -0
  20. polyforge/ops/clearance/protrusions.py +283 -0
  21. polyforge/ops/clearance/remove_protrusions.py +177 -0
  22. polyforge/ops/clearance/utils.py +251 -0
  23. polyforge/ops/merge/__init__.py +33 -0
  24. polyforge/ops/merge_boundary_extension.py +270 -0
  25. polyforge/ops/merge_common.py +101 -0
  26. polyforge/ops/merge_convex_bridges.py +124 -0
  27. polyforge/ops/merge_edge_detection.py +156 -0
  28. polyforge/ops/merge_ops.py +174 -0
  29. polyforge/ops/merge_selective_buffer.py +86 -0
  30. polyforge/ops/merge_simple_buffer.py +52 -0
  31. polyforge/ops/merge_vertex_movement.py +83 -0
  32. polyforge/ops/simplify_ops.py +143 -0
  33. polyforge/overlap.py +412 -0
  34. polyforge/pipeline.py +133 -0
  35. polyforge/process.py +140 -0
  36. polyforge/repair/__init__.py +10 -0
  37. polyforge/repair/analysis.py +84 -0
  38. polyforge/repair/core.py +149 -0
  39. polyforge/repair/robust.py +735 -0
  40. polyforge/repair/strategies/__init__.py +15 -0
  41. polyforge/repair/strategies/auto.py +87 -0
  42. polyforge/repair/strategies/buffer.py +29 -0
  43. polyforge/repair/strategies/reconstruct.py +46 -0
  44. polyforge/repair/strategies/simplify.py +39 -0
  45. polyforge/repair/strategies/strict.py +28 -0
  46. polyforge/repair/utils.py +99 -0
  47. polyforge/simplify.py +203 -0
  48. polyforge/tile.py +89 -0
  49. polyforge/topology.py +274 -0
  50. polyforge-0.1.0a1.dist-info/METADATA +153 -0
  51. polyforge-0.1.0a1.dist-info/RECORD +54 -0
  52. polyforge-0.1.0a1.dist-info/WHEEL +5 -0
  53. polyforge-0.1.0a1.dist-info/licenses/LICENSE +21 -0
  54. polyforge-0.1.0a1.dist-info/top_level.txt +1 -0
polyforge/__init__.py ADDED
@@ -0,0 +1,154 @@
1
+ """Polyforge - Polygon processing and manipulation library.
2
+
3
+ This library provides utilities for processing, simplifying, and manipulating
4
+ polygon geometries using Shapely.
5
+ """
6
+
7
+ __version__ = "0.1.0a1"
8
+
9
+ # Simplification functions
10
+ from .simplify import (
11
+ simplify_rdp,
12
+ simplify_vw,
13
+ simplify_vwp,
14
+ collapse_short_edges,
15
+ deduplicate_vertices,
16
+ remove_small_holes,
17
+ remove_narrow_holes,
18
+ )
19
+
20
+ # Clearance fixing functions
21
+ from .clearance import (
22
+ fix_clearance,
23
+ fix_hole_too_close,
24
+ fix_narrow_protrusion,
25
+ remove_narrow_protrusions,
26
+ fix_sharp_intrusion,
27
+ fix_narrow_passage,
28
+ fix_near_self_intersection,
29
+ fix_parallel_close_edges,
30
+ )
31
+
32
+ # Overlap handling functions
33
+ from .overlap import (
34
+ split_overlap,
35
+ remove_overlaps,
36
+ count_overlaps,
37
+ find_overlapping_groups,
38
+ resolve_overlap_pair,
39
+ )
40
+
41
+ # Merge functions
42
+ from .merge import merge_close_polygons
43
+
44
+ # Topology functions
45
+ from .topology import align_boundaries
46
+
47
+ # Geometry repair functions
48
+ from .repair import (
49
+ repair_geometry,
50
+ analyze_geometry,
51
+ batch_repair_geometries,
52
+ )
53
+
54
+ # Robust constraint-aware repair
55
+ from .repair.robust import (
56
+ robust_fix_geometry,
57
+ robust_fix_batch,
58
+ )
59
+
60
+ # Core types (enums)
61
+ from .core import (
62
+ OverlapStrategy,
63
+ MergeStrategy,
64
+ RepairStrategy,
65
+ CollapseMode,
66
+ )
67
+
68
+ # Core exceptions
69
+ from .core import (
70
+ PolyforgeError,
71
+ ValidationError,
72
+ RepairError,
73
+ OverlapResolutionError,
74
+ MergeError,
75
+ ClearanceError,
76
+ ConfigurationError,
77
+ FixWarning,
78
+ )
79
+
80
+ # Constraint framework
81
+ from .core import (
82
+ GeometryConstraints,
83
+ ConstraintStatus,
84
+ ConstraintViolation,
85
+ ConstraintType,
86
+ MergeConstraints,
87
+ )
88
+
89
+ __all__ = [
90
+
91
+ # Simplification
92
+ 'simplify_rdp',
93
+ 'simplify_vw',
94
+ 'simplify_vwp',
95
+ 'collapse_short_edges',
96
+ 'deduplicate_vertices',
97
+ 'remove_small_holes',
98
+ 'remove_narrow_holes',
99
+
100
+ # Clearance fixing
101
+ 'fix_clearance',
102
+ 'fix_hole_too_close',
103
+ 'fix_narrow_protrusion',
104
+ 'remove_narrow_protrusions',
105
+ 'fix_sharp_intrusion',
106
+ 'fix_narrow_passage',
107
+ 'fix_near_self_intersection',
108
+ 'fix_parallel_close_edges',
109
+
110
+ # Overlap handling
111
+ 'split_overlap',
112
+ 'remove_overlaps',
113
+ 'count_overlaps',
114
+ 'find_overlapping_groups',
115
+ 'resolve_overlap_pair',
116
+
117
+ # Merge
118
+ 'merge_close_polygons',
119
+
120
+ # Topology
121
+ 'align_boundaries',
122
+
123
+ # Geometry repair
124
+ 'repair_geometry',
125
+ 'analyze_geometry',
126
+ 'batch_repair_geometries',
127
+
128
+ # Robust constraint-aware repair
129
+ 'robust_fix_geometry',
130
+ 'robust_fix_batch',
131
+
132
+ # Core types (enums)
133
+ 'OverlapStrategy',
134
+ 'MergeStrategy',
135
+ 'RepairStrategy',
136
+ 'CollapseMode',
137
+
138
+ # Core exceptions
139
+ 'PolyforgeError',
140
+ 'ValidationError',
141
+ 'RepairError',
142
+ 'OverlapResolutionError',
143
+ 'MergeError',
144
+ 'ClearanceError',
145
+ 'ConfigurationError',
146
+ 'FixWarning',
147
+
148
+ # Constraint framework
149
+ 'GeometryConstraints',
150
+ 'ConstraintStatus',
151
+ 'ConstraintViolation',
152
+ 'ConstraintType',
153
+ 'MergeConstraints',
154
+ ]
@@ -0,0 +1,62 @@
1
+ """Geometry clearance fixing functions.
2
+
3
+ This module provides functions for fixing geometries with low minimum clearance
4
+ by applying minimal geometric modifications. Each function targets a specific
5
+ type of clearance issue.
6
+
7
+ The minimum_clearance (from Shapely) represents the smallest distance by which
8
+ a vertex could be moved to create an invalid geometry. Higher values indicate
9
+ more robust, stable geometries.
10
+ """
11
+
12
+ # Import public API functions from the ops layer
13
+ from polyforge.ops.clearance import (
14
+ fix_hole_too_close,
15
+ fix_narrow_protrusion,
16
+ fix_sharp_intrusion,
17
+ remove_narrow_protrusions,
18
+ fix_narrow_passage,
19
+ fix_near_self_intersection,
20
+ fix_parallel_close_edges,
21
+ )
22
+ from .fix_clearance import (
23
+ fix_clearance,
24
+ diagnose_clearance,
25
+ ClearanceIssue,
26
+ ClearanceDiagnosis,
27
+ ClearanceFixSummary,
28
+ )
29
+
30
+ # Import utility functions from ops
31
+ from polyforge.ops.clearance import (
32
+ _find_nearest_vertex_index,
33
+ _find_nearest_edge_index,
34
+ _point_to_segment_distance,
35
+ _point_to_line_perpendicular_distance,
36
+ _get_vertex_neighborhood,
37
+ _calculate_curvature_at_vertex,
38
+ _remove_vertices_between,
39
+ )
40
+
41
+ __all__ = [
42
+ # Main public API
43
+ 'fix_clearance', # Auto-detection and fixing
44
+ 'diagnose_clearance', # Diagnosis without fixing
45
+ 'ClearanceIssue',
46
+ 'ClearanceDiagnosis',
47
+ 'ClearanceFixSummary',
48
+ 'fix_hole_too_close',
49
+ 'fix_narrow_protrusion',
50
+ 'remove_narrow_protrusions',
51
+ 'fix_sharp_intrusion',
52
+ 'fix_narrow_passage',
53
+ 'fix_near_self_intersection',
54
+ 'fix_parallel_close_edges',
55
+ # Utility functions (for advanced users and tests)
56
+ # '_find_nearest_vertex_index',
57
+ # '_find_nearest_edge_index',
58
+ # '_point_to_segment_distance',
59
+ # '_get_vertex_neighborhood',
60
+ # '_calculate_curvature_at_vertex',
61
+ # '_remove_vertices_between',
62
+ ]
@@ -0,0 +1,474 @@
1
+ """Automatic clearance detection and fixing.
2
+
3
+ This module provides an intelligent function that automatically diagnoses
4
+ clearance issues and applies the most appropriate fixing strategy.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from typing import Callable, Dict, List, Optional, Tuple, Union
10
+ import numpy as np
11
+ import shapely
12
+ from shapely.geometry import Polygon, MultiPolygon, Point, LineString, LinearRing
13
+ from shapely.geometry.base import BaseGeometry
14
+
15
+ from polyforge.ops.clearance import (
16
+ fix_hole_too_close,
17
+ fix_narrow_protrusion,
18
+ remove_narrow_protrusions,
19
+ fix_narrow_passage,
20
+ fix_near_self_intersection,
21
+ fix_parallel_close_edges,
22
+ _find_nearest_vertex_index,
23
+ _calculate_curvature_at_vertex,
24
+ )
25
+ from polyforge.core.geometry_utils import to_single_polygon
26
+ from polyforge.core.types import HoleStrategy, PassageStrategy, IntersectionStrategy
27
+ from polyforge.core.iterative_utils import iterative_improve
28
+
29
+
30
+ class ClearanceIssue(Enum):
31
+ """Enumerates the types of clearance problems we can detect."""
32
+
33
+ NONE = "none"
34
+ HOLE_TOO_CLOSE = "hole_too_close"
35
+ NARROW_PROTRUSION = "narrow_protrusion"
36
+ NARROW_PASSAGE = "narrow_passage"
37
+ NEAR_SELF_INTERSECTION = "near_self_intersection"
38
+ PARALLEL_CLOSE_EDGES = "parallel_close_edges"
39
+ UNKNOWN = "unknown"
40
+
41
+
42
+ @dataclass
43
+ class ClearanceDiagnosis:
44
+ """
45
+ Result of analyzing a polygon's clearance.
46
+
47
+ Attributes:
48
+ issue: Detected issue type.
49
+ meets_requirement: Whether min_clearance is already satisfied.
50
+ current_clearance: Measured minimum clearance.
51
+ clearance_ratio: current_clearance / min_clearance.
52
+ clearance_line: Location of the bottleneck if available.
53
+ recommended_fix: Name of the suggested fix function.
54
+ """
55
+
56
+ issue: ClearanceIssue
57
+ meets_requirement: bool
58
+ current_clearance: float
59
+ clearance_ratio: float
60
+ clearance_line: Optional[LineString]
61
+ recommended_fix: str
62
+
63
+ @property
64
+ def has_issues(self) -> bool:
65
+ return not self.meets_requirement
66
+
67
+
68
+ @dataclass
69
+ class ClearanceFixSummary:
70
+ """Metadata describing the fix process."""
71
+
72
+ initial_clearance: float
73
+ final_clearance: float
74
+ area_ratio: float
75
+ iterations: int
76
+ issue: ClearanceIssue
77
+ fixed: bool
78
+ valid: bool
79
+ history: List[ClearanceIssue]
80
+
81
+
82
+ def fix_clearance(
83
+ geometry: Polygon,
84
+ min_clearance: float,
85
+ max_iterations: int = 10,
86
+ min_area_ratio: float = 0.9,
87
+ return_diagnosis: bool = False,
88
+ ) -> Union[Polygon, Tuple[Polygon, ClearanceFixSummary]]:
89
+ """Automatically diagnose and fix low minimum clearance in a polygon."""
90
+ if not isinstance(geometry, Polygon):
91
+ raise TypeError(f"Expected Polygon, got {type(geometry).__name__}")
92
+
93
+ if min_area_ratio <= 0 or min_area_ratio > 1.0:
94
+ raise ValueError("min_area_ratio must be in (0, 1].")
95
+
96
+ initial_clearance = geometry.minimum_clearance
97
+ original_area = geometry.area
98
+ summary = ClearanceFixSummary(
99
+ initial_clearance=initial_clearance,
100
+ final_clearance=initial_clearance,
101
+ area_ratio=1.0,
102
+ iterations=0,
103
+ issue=ClearanceIssue.NONE,
104
+ fixed=initial_clearance >= min_clearance,
105
+ valid=geometry.is_valid,
106
+ history=[ClearanceIssue.NONE],
107
+ )
108
+
109
+ if summary.fixed:
110
+ return (geometry, summary) if return_diagnosis else geometry
111
+
112
+ issue_history: List[ClearanceIssue] = []
113
+ best_valid = geometry
114
+
115
+ def improve(poly: Polygon, target: float) -> Optional[Polygon]:
116
+ diagnosis = diagnose_clearance(poly, target)
117
+ issue_history.append(diagnosis.issue)
118
+ if diagnosis.issue == ClearanceIssue.NONE:
119
+ return None
120
+ candidate = _apply_clearance_strategy(poly, target, diagnosis)
121
+ if candidate is None or not candidate.is_valid or candidate.is_empty:
122
+ return None
123
+ nonlocal best_valid
124
+ if candidate.area < min_area_ratio * original_area:
125
+ return None
126
+ if _safe_clearance(candidate) > _safe_clearance(best_valid):
127
+ best_valid = candidate
128
+ return candidate
129
+
130
+ metric = lambda poly: _safe_clearance(poly) # noqa: E731
131
+ improved = iterative_improve(
132
+ geometry,
133
+ target_value=min_clearance,
134
+ improve_func=improve,
135
+ metric_func=metric,
136
+ max_iterations=max_iterations,
137
+ )
138
+
139
+ if improved is None or not improved.is_valid or improved.is_empty:
140
+ improved = best_valid
141
+ if improved.area < min_area_ratio * original_area:
142
+ improved = best_valid
143
+
144
+ final_clearance = _safe_clearance(improved)
145
+ final_area_ratio = improved.area / original_area if original_area > 0 else float("inf")
146
+ final_diag = diagnose_clearance(improved, min_clearance)
147
+ summary = ClearanceFixSummary(
148
+ initial_clearance=initial_clearance,
149
+ final_clearance=final_clearance,
150
+ area_ratio=final_area_ratio,
151
+ iterations=len(issue_history),
152
+ issue=final_diag.issue,
153
+ fixed=final_diag.meets_requirement,
154
+ valid=improved.is_valid,
155
+ history=issue_history or [final_diag.issue],
156
+ )
157
+
158
+ return (improved, summary) if return_diagnosis else improved
159
+
160
+
161
+ StrategyFunc = Callable[[Polygon, float, ClearanceDiagnosis], Optional[Polygon]]
162
+
163
+ RECOMMENDED_FIXES: Dict[ClearanceIssue, str] = {
164
+ ClearanceIssue.NONE: "none",
165
+ ClearanceIssue.HOLE_TOO_CLOSE: "fix_hole_too_close",
166
+ ClearanceIssue.NARROW_PROTRUSION: "remove_narrow_protrusions",
167
+ ClearanceIssue.NARROW_PASSAGE: "fix_narrow_passage",
168
+ ClearanceIssue.NEAR_SELF_INTERSECTION: "fix_near_self_intersection",
169
+ ClearanceIssue.PARALLEL_CLOSE_EDGES: "fix_parallel_close_edges",
170
+ ClearanceIssue.UNKNOWN: "fix_narrow_passage",
171
+ }
172
+
173
+ STRATEGY_REGISTRY: Dict[ClearanceIssue, StrategyFunc] = {}
174
+
175
+
176
+ def _strategy(issue: ClearanceIssue) -> StrategyFunc:
177
+ return STRATEGY_REGISTRY.get(issue, _strategy_default)
178
+
179
+
180
+ def _apply_clearance_strategy(
181
+ geometry: Polygon,
182
+ min_clearance: float,
183
+ diagnosis: ClearanceDiagnosis,
184
+ ) -> Optional[Polygon]:
185
+ handler = _strategy(diagnosis.issue)
186
+ candidate = handler(geometry, min_clearance, diagnosis)
187
+ return _normalize_polygon(candidate)
188
+
189
+
190
+ def _register_strategy(issue: ClearanceIssue):
191
+ def decorator(func: StrategyFunc) -> StrategyFunc:
192
+ STRATEGY_REGISTRY[issue] = func
193
+ return func
194
+
195
+ return decorator
196
+
197
+
198
+ @_register_strategy(ClearanceIssue.HOLE_TOO_CLOSE)
199
+ def _strategy_hole_too_close(
200
+ geometry: Polygon,
201
+ min_clearance: float,
202
+ _: ClearanceDiagnosis,
203
+ ) -> Optional[Polygon]:
204
+ fixed = fix_hole_too_close(geometry, min_clearance, strategy=HoleStrategy.REMOVE)
205
+ return fixed
206
+
207
+
208
+ @_register_strategy(ClearanceIssue.NARROW_PROTRUSION)
209
+ def _strategy_narrow_protrusion(
210
+ geometry: Polygon,
211
+ min_clearance: float,
212
+ diagnosis: ClearanceDiagnosis,
213
+ ) -> Optional[Polygon]:
214
+ baseline = diagnosis.current_clearance
215
+ first_pass = remove_narrow_protrusions(geometry, aspect_ratio_threshold=10.0)
216
+ if first_pass.is_valid and _safe_clearance(first_pass) > baseline:
217
+ return first_pass
218
+ return fix_narrow_protrusion(geometry, min_clearance)
219
+
220
+
221
+ @_register_strategy(ClearanceIssue.NARROW_PASSAGE)
222
+ def _strategy_narrow_passage(
223
+ geometry: Polygon,
224
+ min_clearance: float,
225
+ _: ClearanceDiagnosis,
226
+ ) -> Optional[Polygon]:
227
+ return fix_narrow_passage(geometry, min_clearance, strategy=PassageStrategy.WIDEN)
228
+
229
+
230
+ @_register_strategy(ClearanceIssue.NEAR_SELF_INTERSECTION)
231
+ def _strategy_near_self_intersection(
232
+ geometry: Polygon,
233
+ min_clearance: float,
234
+ _: ClearanceDiagnosis,
235
+ ) -> Optional[Polygon]:
236
+ return fix_near_self_intersection(
237
+ geometry,
238
+ min_clearance,
239
+ strategy=IntersectionStrategy.SIMPLIFY,
240
+ )
241
+
242
+
243
+ @_register_strategy(ClearanceIssue.PARALLEL_CLOSE_EDGES)
244
+ def _strategy_parallel_edges(
245
+ geometry: Polygon,
246
+ min_clearance: float,
247
+ _: ClearanceDiagnosis,
248
+ ) -> Optional[Polygon]:
249
+ return fix_parallel_close_edges(
250
+ geometry,
251
+ min_clearance,
252
+ strategy=IntersectionStrategy.SIMPLIFY,
253
+ )
254
+
255
+
256
+ def _strategy_default(
257
+ geometry: Polygon,
258
+ min_clearance: float,
259
+ _: ClearanceDiagnosis,
260
+ ) -> Optional[Polygon]:
261
+ return fix_narrow_passage(geometry, min_clearance, strategy=PassageStrategy.WIDEN)
262
+
263
+
264
+ def _normalize_polygon(candidate: Optional[BaseGeometry]) -> Optional[Polygon]:
265
+ if candidate is None:
266
+ return None
267
+ if candidate.is_empty or not candidate.is_valid:
268
+ return None
269
+ polygon = to_single_polygon(candidate)
270
+ return polygon if polygon.is_valid and not polygon.is_empty else None
271
+
272
+
273
+ def _safe_clearance(geometry: Polygon) -> float:
274
+ try:
275
+ return float(geometry.minimum_clearance)
276
+ except Exception:
277
+ return 0.0
278
+
279
+
280
+ def _diagnose_clearance_issue(
281
+ geometry: Polygon,
282
+ min_clearance: float
283
+ ) -> ClearanceIssue:
284
+ """Diagnose the type of clearance issue in a polygon.
285
+
286
+ Examines the geometry's minimum_clearance_line and surrounding geometry
287
+ to determine what type of issue is causing low clearance.
288
+
289
+ Args:
290
+ geometry: Input polygon
291
+ min_clearance: Target minimum clearance
292
+
293
+ Returns:
294
+ A :class:`ClearanceIssue` describing the detected problem.
295
+ """
296
+ if _has_close_hole(geometry, min_clearance):
297
+ return ClearanceIssue.HOLE_TOO_CLOSE
298
+
299
+ context = _build_clearance_context(geometry)
300
+ if context is None:
301
+ return ClearanceIssue.UNKNOWN
302
+
303
+ if "hole" in context.ring_types:
304
+ return ClearanceIssue.HOLE_TOO_CLOSE
305
+
306
+ for heuristic in (
307
+ _looks_like_protrusion,
308
+ _looks_like_near_self_intersection,
309
+ _looks_like_parallel_edges,
310
+ _default_clearance_issue,
311
+ ):
312
+ issue = heuristic(context, min_clearance)
313
+ if issue is not None:
314
+ return issue
315
+ return ClearanceIssue.UNKNOWN
316
+
317
+
318
+ def _classify_ring_types(geometry: Polygon, pt1: Tuple[float, float], pt2: Tuple[float, float]) -> Tuple[str, str]:
319
+ """Return ring type ('exterior' or 'hole') for each endpoint of the clearance line."""
320
+ def classify(point: Tuple[float, float]) -> Tuple[str, Optional[int]]:
321
+ p = Point(point)
322
+ exterior_ring = LinearRing(geometry.exterior.coords)
323
+ d_exterior = p.distance(exterior_ring)
324
+
325
+ min_hole_dist = float("inf")
326
+ hole_idx = None
327
+ for idx, hole in enumerate(geometry.interiors):
328
+ d = p.distance(LinearRing(hole.coords))
329
+ if d < min_hole_dist:
330
+ min_hole_dist = d
331
+ hole_idx = idx
332
+
333
+ if min_hole_dist < d_exterior:
334
+ return "hole", hole_idx
335
+ return "exterior", None
336
+
337
+ t1, _ = classify(pt1)
338
+ t2, _ = classify(pt2)
339
+ return t1, t2
340
+
341
+
342
+ def _has_close_hole(geometry: Polygon, min_clearance: float) -> bool:
343
+ if not geometry.interiors:
344
+ return False
345
+ exterior_ring = LinearRing(geometry.exterior.coords)
346
+ for hole in geometry.interiors:
347
+ hole_ring = LinearRing(hole.coords)
348
+ if hole_ring.distance(exterior_ring) < min_clearance:
349
+ return True
350
+ return False
351
+
352
+
353
+ def _build_clearance_context(geometry: Polygon) -> Optional["ClearanceContext"]:
354
+ try:
355
+ clearance_line = shapely.minimum_clearance_line(geometry)
356
+ except Exception:
357
+ return None
358
+
359
+ if clearance_line.is_empty:
360
+ return None
361
+
362
+ coords_2d = np.array(clearance_line.coords)
363
+ if len(coords_2d) < 2:
364
+ return None
365
+
366
+ pt1, pt2 = coords_2d[:2]
367
+ exterior_coords = np.array(geometry.exterior.coords)
368
+ idx1 = _find_nearest_vertex_index(exterior_coords, pt1)
369
+ idx2 = _find_nearest_vertex_index(exterior_coords, pt2)
370
+ curvature1 = _calculate_curvature_at_vertex(exterior_coords, idx1)
371
+ curvature2 = _calculate_curvature_at_vertex(exterior_coords, idx2)
372
+ n = len(exterior_coords) - 1
373
+ separation = min(abs(idx2 - idx1), n - abs(idx2 - idx1))
374
+ ring_types = _classify_ring_types(geometry, pt1, pt2)
375
+
376
+ return ClearanceContext(
377
+ curvature=(curvature1, curvature2),
378
+ separation=separation,
379
+ vertex_count=n,
380
+ ring_types=ring_types,
381
+ )
382
+
383
+
384
+ @dataclass
385
+ class ClearanceContext:
386
+ curvature: Tuple[float, float]
387
+ separation: int
388
+ vertex_count: int
389
+ ring_types: Tuple[str, str]
390
+
391
+
392
+ def _looks_like_protrusion(context: ClearanceContext, _: float) -> Optional[ClearanceIssue]:
393
+ if "hole" in context.ring_types:
394
+ return None
395
+ curvature1, curvature2 = context.curvature
396
+ sharp_angle_threshold = 135.0
397
+ if curvature1 > sharp_angle_threshold or curvature2 > sharp_angle_threshold:
398
+ return ClearanceIssue.NARROW_PROTRUSION
399
+
400
+ if context.separation <= 3 and (curvature1 > 90 or curvature2 > 90):
401
+ return ClearanceIssue.NARROW_PROTRUSION
402
+ return None
403
+
404
+
405
+ def _looks_like_near_self_intersection(context: ClearanceContext, _: float) -> Optional[ClearanceIssue]:
406
+ if "hole" in context.ring_types:
407
+ return None
408
+ if context.separation <= 3:
409
+ curvature1, curvature2 = context.curvature
410
+ if curvature1 <= 90 and curvature2 <= 90:
411
+ return ClearanceIssue.NEAR_SELF_INTERSECTION
412
+ return None
413
+
414
+
415
+ def _looks_like_parallel_edges(context: ClearanceContext, _: float) -> Optional[ClearanceIssue]:
416
+ if "hole" in context.ring_types:
417
+ return None
418
+ if context.vertex_count <= 0:
419
+ return None
420
+ if context.separation >= context.vertex_count // 3:
421
+ return ClearanceIssue.PARALLEL_CLOSE_EDGES
422
+ return None
423
+
424
+
425
+ def _default_clearance_issue(_: ClearanceContext, __: float) -> ClearanceIssue:
426
+ return ClearanceIssue.NARROW_PASSAGE
427
+
428
+
429
+ def diagnose_clearance(
430
+ geometry: Polygon,
431
+ min_clearance: float
432
+ ) -> ClearanceDiagnosis:
433
+ """Diagnose clearance issues without fixing them."""
434
+ if not isinstance(geometry, Polygon):
435
+ raise TypeError(f"Expected Polygon, got {type(geometry).__name__}")
436
+
437
+ current_clearance = geometry.minimum_clearance
438
+ meets_requirement = current_clearance >= min_clearance
439
+ ratio = current_clearance / min_clearance if min_clearance > 0 else float("inf")
440
+
441
+ try:
442
+ clearance_line = shapely.minimum_clearance_line(geometry)
443
+ except Exception:
444
+ clearance_line = None
445
+
446
+ if meets_requirement:
447
+ return ClearanceDiagnosis(
448
+ issue=ClearanceIssue.NONE,
449
+ meets_requirement=True,
450
+ current_clearance=current_clearance,
451
+ clearance_ratio=ratio,
452
+ clearance_line=clearance_line,
453
+ recommended_fix=RECOMMENDED_FIXES[ClearanceIssue.NONE],
454
+ )
455
+
456
+ issue = _diagnose_clearance_issue(geometry, min_clearance)
457
+ recommended = RECOMMENDED_FIXES.get(issue, RECOMMENDED_FIXES[ClearanceIssue.UNKNOWN])
458
+ return ClearanceDiagnosis(
459
+ issue=issue,
460
+ meets_requirement=False,
461
+ current_clearance=current_clearance,
462
+ clearance_ratio=ratio,
463
+ clearance_line=clearance_line,
464
+ recommended_fix=recommended,
465
+ )
466
+
467
+
468
+ __all__ = [
469
+ 'fix_clearance',
470
+ 'diagnose_clearance',
471
+ 'ClearanceIssue',
472
+ 'ClearanceDiagnosis',
473
+ 'ClearanceFixSummary',
474
+ ]