gdsdiff 0.1.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.
- gdsdiff/__init__.py +148 -0
- gdsdiff/__main__.py +7 -0
- gdsdiff/_environment.py +12 -0
- gdsdiff/_version.py +34 -0
- gdsdiff/acceptance_evidence.py +530 -0
- gdsdiff/api.py +2955 -0
- gdsdiff/backends/__init__.py +26 -0
- gdsdiff/backends/base.py +77 -0
- gdsdiff/backends/boundary_loop_ctypes.py +191 -0
- gdsdiff/backends/cpu.py +20 -0
- gdsdiff/backends/cpu_ctypes.py +255 -0
- gdsdiff/backends/cuda.py +40 -0
- gdsdiff/backends/cuda_ctypes.py +1297 -0
- gdsdiff/backends/exact.py +424 -0
- gdsdiff/backends/geometry_ctypes.py +252 -0
- gdsdiff/backends/identity.py +53 -0
- gdsdiff/backends/metal.py +198 -0
- gdsdiff/backends/metal_ctypes.py +866 -0
- gdsdiff/backends/polygon_extract_ctypes.py +233 -0
- gdsdiff/backends/structural_ctypes.py +130 -0
- gdsdiff/boundary_loops.py +616 -0
- gdsdiff/cache.py +544 -0
- gdsdiff/canonicalize.py +176 -0
- gdsdiff/cli.py +352 -0
- gdsdiff/config.py +257 -0
- gdsdiff/cuda_setup.py +174 -0
- gdsdiff/design_history.py +65 -0
- gdsdiff/diagnostics.py +42 -0
- gdsdiff/diff_gds.py +66 -0
- gdsdiff/exact_diff_markdown.py +979 -0
- gdsdiff/exact_oracle.py +649 -0
- gdsdiff/extract.py +376 -0
- gdsdiff/gds_metadata.py +43 -0
- gdsdiff/geometry.py +112 -0
- gdsdiff/grid.py +143 -0
- gdsdiff/hierarchy.py +215 -0
- gdsdiff/hierarchy_extract.py +263 -0
- gdsdiff/inventory.py +153 -0
- gdsdiff/multiprocessing_support.py +39 -0
- gdsdiff/native.py +135 -0
- gdsdiff/native_cache.py +265 -0
- gdsdiff/native_src/cpu_prefilter.cpp +349 -0
- gdsdiff/native_src/gds_boundary_loops.cpp +505 -0
- gdsdiff/native_src/gds_geometry_scan.cpp +601 -0
- gdsdiff/native_src/gds_polygon_extract.cpp +594 -0
- gdsdiff/native_src/gds_structural_scan.cpp +335 -0
- gdsdiff/native_src/gdsdiff/CMakeLists.txt +74 -0
- gdsdiff/native_src/gdsdiff/core.cpp +191 -0
- gdsdiff/native_src/gdsdiff/core.hpp +96 -0
- gdsdiff/native_src/gdsdiff/cuda_assignment.cu +304 -0
- gdsdiff/native_src/gdsdiff/cuda_assignment.cuh +33 -0
- gdsdiff/native_src/gdsdiff/cuda_candidates.cu +133 -0
- gdsdiff/native_src/gdsdiff/cuda_candidates.cuh +17 -0
- gdsdiff/native_src/gdsdiff/cuda_ctypes.cu +4528 -0
- gdsdiff/native_src/gdsdiff/cuda_memory.cu +194 -0
- gdsdiff/native_src/gdsdiff/cuda_memory.cuh +34 -0
- gdsdiff/native_src/gdsdiff/metal_ctypes.mm +2791 -0
- gdsdiff/native_src/gdsdiff/python_bindings.cpp +21 -0
- gdsdiff/native_src/gdsdiff/test_core.cpp +93 -0
- gdsdiff/native_src/gdsdiff/test_cuda_assignment.cu +109 -0
- gdsdiff/native_src/gdsdiff/test_cuda_candidates.cu +94 -0
- gdsdiff/native_src/gdsdiff/test_cuda_memory.cu +97 -0
- gdsdiff/policy.py +305 -0
- gdsdiff/polygon_components.py +860 -0
- gdsdiff/profiles.py +152 -0
- gdsdiff/py.typed +1 -0
- gdsdiff/rect_coverage.py +384 -0
- gdsdiff/report.py +354 -0
- gdsdiff/schemas/gdsdiff_report-v1.schema.json +92 -0
- gdsdiff/semantics.py +75 -0
- gdsdiff/source_edge_diff.py +1120 -0
- gdsdiff/spatial.py +71 -0
- gdsdiff/structural_guard.py +161 -0
- gdsdiff/surplus_coverage.py +255 -0
- gdsdiff/synthetic_suite.py +1643 -0
- gdsdiff/templates.py +709 -0
- gdsdiff/tiling.py +153 -0
- gdsdiff/windowed_exact.py +270 -0
- gdsdiff/windows.py +184 -0
- gdsdiff-0.1.0.dist-info/METADATA +135 -0
- gdsdiff-0.1.0.dist-info/RECORD +85 -0
- gdsdiff-0.1.0.dist-info/WHEEL +5 -0
- gdsdiff-0.1.0.dist-info/entry_points.txt +4 -0
- gdsdiff-0.1.0.dist-info/licenses/LICENSE +674 -0
- gdsdiff-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from math import hypot, isfinite
|
|
6
|
+
from typing import Sequence
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from .exact_oracle import DiffFragment, ExactOracleResult, LayerExactDiff
|
|
11
|
+
from .geometry import polygon_bbox, polygon_twice_area
|
|
12
|
+
from .polygon_components import (
|
|
13
|
+
LayerSideMergeStats,
|
|
14
|
+
PolygonComponent,
|
|
15
|
+
_component_sort_key,
|
|
16
|
+
_fragment_sort_key,
|
|
17
|
+
_merge_layer_side_fragments_with_stats,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
Point = tuple[int, int]
|
|
21
|
+
FloatPoint = tuple[float, float]
|
|
22
|
+
FloatSegment = tuple[FloatPoint, FloatPoint]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class SourceEdgeDiffStats:
|
|
27
|
+
component_count: int
|
|
28
|
+
boundary_edge_count: int
|
|
29
|
+
loop_count: int
|
|
30
|
+
dropped_zero_area_loop_count: int
|
|
31
|
+
gpu_loop_component_count: int = 0
|
|
32
|
+
gpu_loop_edge_count: int = 0
|
|
33
|
+
cuda_loop_component_count: int = 0
|
|
34
|
+
cuda_loop_edge_count: int = 0
|
|
35
|
+
metal_loop_component_count: int = 0
|
|
36
|
+
metal_loop_edge_count: int = 0
|
|
37
|
+
native_loop_component_count: int = 0
|
|
38
|
+
native_loop_edge_count: int = 0
|
|
39
|
+
gpu_boundary_component_count: int = 0
|
|
40
|
+
gpu_boundary_edge_count: int = 0
|
|
41
|
+
gpu_split_free_component_count: int = 0
|
|
42
|
+
gpu_mixed_boundary_component_count: int = 0
|
|
43
|
+
gpu_mixed_boundary_edge_count: int = 0
|
|
44
|
+
layer_merge_input_fragment_count: int = 0
|
|
45
|
+
layer_merge_output_fragment_count: int = 0
|
|
46
|
+
layer_merge_group_count: int = 0
|
|
47
|
+
layer_merge_assembled_group_count: int = 0
|
|
48
|
+
layer_merge_cuda_edge_cancel_s: float = 0.0
|
|
49
|
+
layer_merge_cuda_edge_cancel_kernel_count: int = 0
|
|
50
|
+
layer_merge_cuda_edge_cancel_event_elapsed_s: float = 0.0
|
|
51
|
+
layer_merge_cuda_edge_cancel_transferred_bytes: int = 0
|
|
52
|
+
layer_merge_native_edge_cancel_s: float = 0.0
|
|
53
|
+
layer_merge_cuda_loop_trace_s: float = 0.0
|
|
54
|
+
layer_merge_metal_loop_trace_s: float = 0.0
|
|
55
|
+
layer_merge_metal_loop_kernel_count: int = 0
|
|
56
|
+
layer_merge_metal_loop_event_elapsed_s: float = 0.0
|
|
57
|
+
layer_merge_metal_loop_transferred_bytes: int = 0
|
|
58
|
+
layer_merge_native_loop_trace_s: float = 0.0
|
|
59
|
+
layer_merge_python_loop_trace_s: float = 0.0
|
|
60
|
+
layer_merge_hole_stitch_s: float = 0.0
|
|
61
|
+
nested_direction_fragment_component_count: int = 0
|
|
62
|
+
|
|
63
|
+
def to_json(self) -> dict[str, int | float]:
|
|
64
|
+
return {
|
|
65
|
+
"component_count": self.component_count,
|
|
66
|
+
"boundary_edge_count": self.boundary_edge_count,
|
|
67
|
+
"loop_count": self.loop_count,
|
|
68
|
+
"dropped_zero_area_loop_count": self.dropped_zero_area_loop_count,
|
|
69
|
+
"gpu_loop_component_count": self.gpu_loop_component_count,
|
|
70
|
+
"gpu_loop_edge_count": self.gpu_loop_edge_count,
|
|
71
|
+
"cuda_loop_component_count": self.cuda_loop_component_count,
|
|
72
|
+
"cuda_loop_edge_count": self.cuda_loop_edge_count,
|
|
73
|
+
"metal_loop_component_count": self.metal_loop_component_count,
|
|
74
|
+
"metal_loop_edge_count": self.metal_loop_edge_count,
|
|
75
|
+
"native_loop_component_count": self.native_loop_component_count,
|
|
76
|
+
"native_loop_edge_count": self.native_loop_edge_count,
|
|
77
|
+
"gpu_boundary_component_count": self.gpu_boundary_component_count,
|
|
78
|
+
"gpu_boundary_edge_count": self.gpu_boundary_edge_count,
|
|
79
|
+
"gpu_split_free_component_count": self.gpu_split_free_component_count,
|
|
80
|
+
"gpu_mixed_boundary_component_count": self.gpu_mixed_boundary_component_count,
|
|
81
|
+
"gpu_mixed_boundary_edge_count": self.gpu_mixed_boundary_edge_count,
|
|
82
|
+
"layer_merge_input_fragment_count": self.layer_merge_input_fragment_count,
|
|
83
|
+
"layer_merge_output_fragment_count": self.layer_merge_output_fragment_count,
|
|
84
|
+
"layer_merge_group_count": self.layer_merge_group_count,
|
|
85
|
+
"layer_merge_assembled_group_count": self.layer_merge_assembled_group_count,
|
|
86
|
+
"layer_merge_cuda_edge_cancel_s": self.layer_merge_cuda_edge_cancel_s,
|
|
87
|
+
"layer_merge_cuda_edge_cancel_kernel_count": self.layer_merge_cuda_edge_cancel_kernel_count,
|
|
88
|
+
"layer_merge_cuda_edge_cancel_event_elapsed_s": self.layer_merge_cuda_edge_cancel_event_elapsed_s,
|
|
89
|
+
"layer_merge_cuda_edge_cancel_transferred_bytes": self.layer_merge_cuda_edge_cancel_transferred_bytes,
|
|
90
|
+
"layer_merge_native_edge_cancel_s": self.layer_merge_native_edge_cancel_s,
|
|
91
|
+
"layer_merge_cuda_loop_trace_s": self.layer_merge_cuda_loop_trace_s,
|
|
92
|
+
"layer_merge_metal_loop_trace_s": self.layer_merge_metal_loop_trace_s,
|
|
93
|
+
"layer_merge_metal_loop_kernel_count": self.layer_merge_metal_loop_kernel_count,
|
|
94
|
+
"layer_merge_metal_loop_event_elapsed_s": self.layer_merge_metal_loop_event_elapsed_s,
|
|
95
|
+
"layer_merge_metal_loop_transferred_bytes": self.layer_merge_metal_loop_transferred_bytes,
|
|
96
|
+
"layer_merge_native_loop_trace_s": self.layer_merge_native_loop_trace_s,
|
|
97
|
+
"layer_merge_python_loop_trace_s": self.layer_merge_python_loop_trace_s,
|
|
98
|
+
"layer_merge_hole_stitch_s": self.layer_merge_hole_stitch_s,
|
|
99
|
+
"nested_direction_fragment_component_count": self.nested_direction_fragment_component_count,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True)
|
|
104
|
+
class SourceEdgeDiffResult:
|
|
105
|
+
exact_result: ExactOracleResult
|
|
106
|
+
stats: SourceEdgeDiffStats
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class _SegmentRecord:
|
|
111
|
+
side: str
|
|
112
|
+
polygon_index: int
|
|
113
|
+
start: FloatPoint
|
|
114
|
+
stop: FloatPoint
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def exact_result_from_source_edges(components: tuple[PolygonComponent, ...]) -> SourceEdgeDiffResult:
|
|
118
|
+
source_components = _merge_contacting_source_components(components)
|
|
119
|
+
by_layer: dict[object, dict[str, list[DiffFragment]]] = {}
|
|
120
|
+
edge_count = 0
|
|
121
|
+
loop_count = 0
|
|
122
|
+
dropped_count = 0
|
|
123
|
+
gpu_component_count = 0
|
|
124
|
+
gpu_edge_count = 0
|
|
125
|
+
gpu_split_free_count = 0
|
|
126
|
+
gpu_mixed_component_count = 0
|
|
127
|
+
gpu_mixed_edge_count = 0
|
|
128
|
+
native_loop_component_count = 0
|
|
129
|
+
native_loop_edge_count = 0
|
|
130
|
+
gpu_loop_component_count = 0
|
|
131
|
+
gpu_loop_edge_count = 0
|
|
132
|
+
cuda_loop_component_count = 0
|
|
133
|
+
cuda_loop_edge_count = 0
|
|
134
|
+
metal_loop_component_count = 0
|
|
135
|
+
metal_loop_edge_count = 0
|
|
136
|
+
nested_direction_fragment_component_count = 0
|
|
137
|
+
layer_merge_stats = LayerSideMergeStats()
|
|
138
|
+
side_cache: dict[tuple[str, tuple[object, ...]], tuple[tuple[DiffFragment, ...], _ComponentSideStats]] = {}
|
|
139
|
+
for component in source_components:
|
|
140
|
+
layer_bucket = by_layer.setdefault(component.layer, {"old": [], "new": []})
|
|
141
|
+
old_minus, old_stats = _cached_component_side_diff(component, direction="old", cache=side_cache)
|
|
142
|
+
new_minus, new_stats = _cached_component_side_diff(component, direction="new", cache=side_cache)
|
|
143
|
+
layer_bucket["old"].extend(old_minus)
|
|
144
|
+
layer_bucket["new"].extend(new_minus)
|
|
145
|
+
edge_count += old_stats.boundary_edge_count + new_stats.boundary_edge_count
|
|
146
|
+
loop_count += old_stats.loop_count + new_stats.loop_count
|
|
147
|
+
dropped_count += old_stats.dropped_zero_area_loop_count + new_stats.dropped_zero_area_loop_count
|
|
148
|
+
gpu_component_count += old_stats.gpu_boundary_component_count + new_stats.gpu_boundary_component_count
|
|
149
|
+
gpu_edge_count += old_stats.gpu_boundary_edge_count + new_stats.gpu_boundary_edge_count
|
|
150
|
+
gpu_split_free_count += old_stats.gpu_split_free_component_count + new_stats.gpu_split_free_component_count
|
|
151
|
+
gpu_mixed_component_count += old_stats.gpu_mixed_boundary_component_count + new_stats.gpu_mixed_boundary_component_count
|
|
152
|
+
gpu_mixed_edge_count += old_stats.gpu_mixed_boundary_edge_count + new_stats.gpu_mixed_boundary_edge_count
|
|
153
|
+
native_loop_component_count += old_stats.native_loop_component_count + new_stats.native_loop_component_count
|
|
154
|
+
native_loop_edge_count += old_stats.native_loop_edge_count + new_stats.native_loop_edge_count
|
|
155
|
+
gpu_loop_component_count += old_stats.gpu_loop_component_count + new_stats.gpu_loop_component_count
|
|
156
|
+
gpu_loop_edge_count += old_stats.gpu_loop_edge_count + new_stats.gpu_loop_edge_count
|
|
157
|
+
cuda_loop_component_count += old_stats.cuda_loop_component_count + new_stats.cuda_loop_component_count
|
|
158
|
+
cuda_loop_edge_count += old_stats.cuda_loop_edge_count + new_stats.cuda_loop_edge_count
|
|
159
|
+
metal_loop_component_count += old_stats.metal_loop_component_count + new_stats.metal_loop_component_count
|
|
160
|
+
metal_loop_edge_count += old_stats.metal_loop_edge_count + new_stats.metal_loop_edge_count
|
|
161
|
+
nested_direction_fragment_component_count += (
|
|
162
|
+
old_stats.nested_direction_fragment_component_count
|
|
163
|
+
+ new_stats.nested_direction_fragment_component_count
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
layer_diffs = []
|
|
167
|
+
for layer in sorted(by_layer):
|
|
168
|
+
old_merge = _merge_layer_side_fragments_with_stats(tuple(sorted(by_layer[layer]["old"], key=_fragment_sort_key)))
|
|
169
|
+
new_merge = _merge_layer_side_fragments_with_stats(tuple(sorted(by_layer[layer]["new"], key=_fragment_sort_key)))
|
|
170
|
+
layer_merge_stats = _add_layer_merge_stats(layer_merge_stats, old_merge.stats)
|
|
171
|
+
layer_merge_stats = _add_layer_merge_stats(layer_merge_stats, new_merge.stats)
|
|
172
|
+
old_minus_new = old_merge.fragments
|
|
173
|
+
new_minus_old = new_merge.fragments
|
|
174
|
+
layer_diffs.append(
|
|
175
|
+
LayerExactDiff(
|
|
176
|
+
layer=layer,
|
|
177
|
+
old_minus_new=old_minus_new,
|
|
178
|
+
new_minus_old=new_minus_old,
|
|
179
|
+
xor=tuple(sorted(old_minus_new + new_minus_old, key=_fragment_sort_key)),
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
return SourceEdgeDiffResult(
|
|
183
|
+
exact_result=ExactOracleResult(tuple(layer_diffs)),
|
|
184
|
+
stats=SourceEdgeDiffStats(
|
|
185
|
+
component_count=len(source_components),
|
|
186
|
+
boundary_edge_count=edge_count,
|
|
187
|
+
loop_count=loop_count,
|
|
188
|
+
dropped_zero_area_loop_count=dropped_count,
|
|
189
|
+
gpu_loop_component_count=gpu_loop_component_count,
|
|
190
|
+
gpu_loop_edge_count=gpu_loop_edge_count,
|
|
191
|
+
cuda_loop_component_count=cuda_loop_component_count,
|
|
192
|
+
cuda_loop_edge_count=cuda_loop_edge_count,
|
|
193
|
+
metal_loop_component_count=metal_loop_component_count,
|
|
194
|
+
metal_loop_edge_count=metal_loop_edge_count,
|
|
195
|
+
native_loop_component_count=native_loop_component_count,
|
|
196
|
+
native_loop_edge_count=native_loop_edge_count,
|
|
197
|
+
gpu_boundary_component_count=gpu_component_count,
|
|
198
|
+
gpu_boundary_edge_count=gpu_edge_count,
|
|
199
|
+
gpu_split_free_component_count=gpu_split_free_count,
|
|
200
|
+
gpu_mixed_boundary_component_count=gpu_mixed_component_count,
|
|
201
|
+
gpu_mixed_boundary_edge_count=gpu_mixed_edge_count,
|
|
202
|
+
layer_merge_input_fragment_count=layer_merge_stats.input_fragment_count,
|
|
203
|
+
layer_merge_output_fragment_count=layer_merge_stats.output_fragment_count,
|
|
204
|
+
layer_merge_group_count=layer_merge_stats.group_count,
|
|
205
|
+
layer_merge_assembled_group_count=layer_merge_stats.assembled_group_count,
|
|
206
|
+
layer_merge_cuda_edge_cancel_s=layer_merge_stats.cuda_edge_cancel_s,
|
|
207
|
+
layer_merge_cuda_edge_cancel_kernel_count=layer_merge_stats.cuda_edge_cancel_kernel_count,
|
|
208
|
+
layer_merge_cuda_edge_cancel_event_elapsed_s=layer_merge_stats.cuda_edge_cancel_event_elapsed_s,
|
|
209
|
+
layer_merge_cuda_edge_cancel_transferred_bytes=layer_merge_stats.cuda_edge_cancel_transferred_bytes,
|
|
210
|
+
layer_merge_native_edge_cancel_s=layer_merge_stats.native_edge_cancel_s,
|
|
211
|
+
layer_merge_cuda_loop_trace_s=layer_merge_stats.cuda_loop_trace_s,
|
|
212
|
+
layer_merge_metal_loop_trace_s=layer_merge_stats.metal_loop_trace_s,
|
|
213
|
+
layer_merge_metal_loop_kernel_count=layer_merge_stats.metal_loop_kernel_count,
|
|
214
|
+
layer_merge_metal_loop_event_elapsed_s=layer_merge_stats.metal_loop_event_elapsed_s,
|
|
215
|
+
layer_merge_metal_loop_transferred_bytes=layer_merge_stats.metal_loop_transferred_bytes,
|
|
216
|
+
layer_merge_native_loop_trace_s=layer_merge_stats.native_loop_trace_s,
|
|
217
|
+
layer_merge_python_loop_trace_s=layer_merge_stats.python_loop_trace_s,
|
|
218
|
+
layer_merge_hole_stitch_s=layer_merge_stats.hole_stitch_s,
|
|
219
|
+
nested_direction_fragment_component_count=nested_direction_fragment_component_count,
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _add_layer_merge_stats(left: LayerSideMergeStats, right: LayerSideMergeStats) -> LayerSideMergeStats:
|
|
225
|
+
return LayerSideMergeStats(
|
|
226
|
+
input_fragment_count=left.input_fragment_count + right.input_fragment_count,
|
|
227
|
+
output_fragment_count=left.output_fragment_count + right.output_fragment_count,
|
|
228
|
+
group_count=left.group_count + right.group_count,
|
|
229
|
+
assembled_group_count=left.assembled_group_count + right.assembled_group_count,
|
|
230
|
+
cuda_edge_cancel_s=left.cuda_edge_cancel_s + right.cuda_edge_cancel_s,
|
|
231
|
+
cuda_edge_cancel_kernel_count=left.cuda_edge_cancel_kernel_count + right.cuda_edge_cancel_kernel_count,
|
|
232
|
+
cuda_edge_cancel_event_elapsed_s=left.cuda_edge_cancel_event_elapsed_s + right.cuda_edge_cancel_event_elapsed_s,
|
|
233
|
+
cuda_edge_cancel_transferred_bytes=left.cuda_edge_cancel_transferred_bytes + right.cuda_edge_cancel_transferred_bytes,
|
|
234
|
+
native_edge_cancel_s=left.native_edge_cancel_s + right.native_edge_cancel_s,
|
|
235
|
+
cuda_loop_trace_s=left.cuda_loop_trace_s + right.cuda_loop_trace_s,
|
|
236
|
+
metal_loop_trace_s=left.metal_loop_trace_s + right.metal_loop_trace_s,
|
|
237
|
+
metal_loop_kernel_count=left.metal_loop_kernel_count + right.metal_loop_kernel_count,
|
|
238
|
+
metal_loop_event_elapsed_s=left.metal_loop_event_elapsed_s + right.metal_loop_event_elapsed_s,
|
|
239
|
+
metal_loop_transferred_bytes=left.metal_loop_transferred_bytes + right.metal_loop_transferred_bytes,
|
|
240
|
+
native_loop_trace_s=left.native_loop_trace_s + right.native_loop_trace_s,
|
|
241
|
+
python_loop_trace_s=left.python_loop_trace_s + right.python_loop_trace_s,
|
|
242
|
+
hole_stitch_s=left.hole_stitch_s + right.hole_stitch_s,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _cached_component_side_diff(
|
|
247
|
+
component: PolygonComponent,
|
|
248
|
+
*,
|
|
249
|
+
direction: str,
|
|
250
|
+
cache: dict[tuple[str, tuple[object, ...]], tuple[tuple[DiffFragment, ...], _ComponentSideStats]],
|
|
251
|
+
) -> tuple[tuple[DiffFragment, ...], _ComponentSideStats]:
|
|
252
|
+
origin = (component.bbox[0], component.bbox[1])
|
|
253
|
+
key = (direction, _normalized_component_key(component, origin))
|
|
254
|
+
cached = cache.get(key)
|
|
255
|
+
if cached is None:
|
|
256
|
+
fragments, stats = _component_side_diff(component, direction=direction)
|
|
257
|
+
cached = (_translate_fragments(fragments, (-origin[0], -origin[1])), stats)
|
|
258
|
+
cache[key] = cached
|
|
259
|
+
fragments, stats = cached
|
|
260
|
+
return _translate_fragments(fragments, origin), stats
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _merge_contacting_source_components(components: tuple[PolygonComponent, ...]) -> tuple[PolygonComponent, ...]:
|
|
264
|
+
by_layer: dict[object, list[PolygonComponent]] = {}
|
|
265
|
+
for component in components:
|
|
266
|
+
by_layer.setdefault(component.layer, []).append(component)
|
|
267
|
+
|
|
268
|
+
merged: list[PolygonComponent] = []
|
|
269
|
+
for layer in sorted(by_layer):
|
|
270
|
+
layer_components = tuple(sorted(by_layer[layer], key=_component_sort_key))
|
|
271
|
+
for indices in _component_bbox_contact_groups(layer_components):
|
|
272
|
+
polygons = {}
|
|
273
|
+
for index in indices:
|
|
274
|
+
for polygon in layer_components[index].polygons:
|
|
275
|
+
polygons[(polygon.side, polygon.fragment.bbox, polygon.fragment.twice_area, polygon.fragment.points)] = polygon
|
|
276
|
+
merged.append(
|
|
277
|
+
PolygonComponent(
|
|
278
|
+
layer=layer,
|
|
279
|
+
polygons=tuple(sorted(polygons.values(), key=lambda polygon: (polygon.side, polygon.source_index, polygon.role, polygon.fragment.bbox))),
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
return tuple(sorted(merged, key=_component_sort_key))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _component_bbox_contact_groups(components: tuple[PolygonComponent, ...]) -> tuple[tuple[int, ...], ...]:
|
|
286
|
+
remaining = set(range(len(components)))
|
|
287
|
+
groups: list[tuple[int, ...]] = []
|
|
288
|
+
while remaining:
|
|
289
|
+
seed = remaining.pop()
|
|
290
|
+
group = {seed}
|
|
291
|
+
stack = [seed]
|
|
292
|
+
while stack:
|
|
293
|
+
left = stack.pop()
|
|
294
|
+
touched = [right for right in remaining if _bbox_contact(components[left].bbox, components[right].bbox)]
|
|
295
|
+
for right in touched:
|
|
296
|
+
remaining.remove(right)
|
|
297
|
+
group.add(right)
|
|
298
|
+
stack.append(right)
|
|
299
|
+
groups.append(tuple(sorted(group)))
|
|
300
|
+
return tuple(sorted(groups))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _bbox_contact(left: tuple[int, int, int, int], right: tuple[int, int, int, int]) -> bool:
|
|
304
|
+
return max(left[0], right[0]) <= min(left[2], right[2]) and max(left[1], right[1]) <= min(left[3], right[3])
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _normalized_component_key(component: PolygonComponent, origin: Point) -> tuple[object, ...]:
|
|
308
|
+
ox, oy = origin
|
|
309
|
+
records = []
|
|
310
|
+
for polygon in component.polygons:
|
|
311
|
+
points = tuple((point[0] - ox, point[1] - oy) for point in polygon.fragment.points)
|
|
312
|
+
records.append((polygon.side, points))
|
|
313
|
+
return tuple(sorted(records))
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _translate_fragments(fragments: tuple[DiffFragment, ...], delta: Point) -> tuple[DiffFragment, ...]:
|
|
317
|
+
dx, dy = delta
|
|
318
|
+
if dx == 0 and dy == 0:
|
|
319
|
+
return fragments
|
|
320
|
+
out = []
|
|
321
|
+
for fragment in fragments:
|
|
322
|
+
points = tuple((point[0] + dx, point[1] + dy) for point in fragment.points)
|
|
323
|
+
bbox = (
|
|
324
|
+
fragment.bbox[0] + dx,
|
|
325
|
+
fragment.bbox[1] + dy,
|
|
326
|
+
fragment.bbox[2] + dx,
|
|
327
|
+
fragment.bbox[3] + dy,
|
|
328
|
+
)
|
|
329
|
+
out.append(DiffFragment(points=points, bbox=bbox, twice_area=fragment.twice_area))
|
|
330
|
+
return tuple(out)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@dataclass(frozen=True)
|
|
334
|
+
class _ComponentSideStats:
|
|
335
|
+
boundary_edge_count: int
|
|
336
|
+
loop_count: int
|
|
337
|
+
dropped_zero_area_loop_count: int
|
|
338
|
+
gpu_loop_component_count: int = 0
|
|
339
|
+
gpu_loop_edge_count: int = 0
|
|
340
|
+
cuda_loop_component_count: int = 0
|
|
341
|
+
cuda_loop_edge_count: int = 0
|
|
342
|
+
metal_loop_component_count: int = 0
|
|
343
|
+
metal_loop_edge_count: int = 0
|
|
344
|
+
native_loop_component_count: int = 0
|
|
345
|
+
native_loop_edge_count: int = 0
|
|
346
|
+
gpu_boundary_component_count: int = 0
|
|
347
|
+
gpu_boundary_edge_count: int = 0
|
|
348
|
+
gpu_split_free_component_count: int = 0
|
|
349
|
+
gpu_mixed_boundary_component_count: int = 0
|
|
350
|
+
gpu_mixed_boundary_edge_count: int = 0
|
|
351
|
+
nested_direction_fragment_component_count: int = 0
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _component_side_diff(component: PolygonComponent, *, direction: str) -> tuple[tuple[DiffFragment, ...], _ComponentSideStats]:
|
|
355
|
+
if direction not in {"old", "new"}:
|
|
356
|
+
raise ValueError(f"unknown diff direction: {direction!r}")
|
|
357
|
+
target_fragments = tuple(polygon.fragment for polygon in component.polygons if polygon.side == direction)
|
|
358
|
+
opposite = tuple(polygon for polygon in component.polygons if polygon.side != direction)
|
|
359
|
+
if len(target_fragments) == 1 and not opposite:
|
|
360
|
+
return target_fragments, _ComponentSideStats(0, 1, 0)
|
|
361
|
+
if target_fragments and not opposite:
|
|
362
|
+
return _one_sided_union_diff(target_fragments)
|
|
363
|
+
polygons_by_side = {
|
|
364
|
+
"old": tuple(polygon.fragment.points for polygon in component.polygons if polygon.side == "old"),
|
|
365
|
+
"new": tuple(polygon.fragment.points for polygon in component.polygons if polygon.side == "new"),
|
|
366
|
+
}
|
|
367
|
+
bboxes_by_side = {
|
|
368
|
+
side: tuple(polygon_bbox(points) for points in polygons)
|
|
369
|
+
for side, polygons in polygons_by_side.items()
|
|
370
|
+
}
|
|
371
|
+
if not polygons_by_side[direction]:
|
|
372
|
+
return (), _ComponentSideStats(0, 0, 0)
|
|
373
|
+
segments = _build_segments(polygons_by_side)
|
|
374
|
+
if not segments:
|
|
375
|
+
return (), _ComponentSideStats(0, 0, 0)
|
|
376
|
+
splits = _segment_splits(segments)
|
|
377
|
+
boundary_edges, used_gpu = _maybe_cuda_boundary_edges_for_direction(
|
|
378
|
+
segments,
|
|
379
|
+
splits,
|
|
380
|
+
polygons_by_side,
|
|
381
|
+
bboxes_by_side,
|
|
382
|
+
direction=direction,
|
|
383
|
+
)
|
|
384
|
+
fragments, dropped, loop_backend = _fragments_from_boundary_edges(boundary_edges)
|
|
385
|
+
return (
|
|
386
|
+
tuple(sorted(fragments, key=_fragment_sort_key)),
|
|
387
|
+
_ComponentSideStats(
|
|
388
|
+
boundary_edge_count=len(boundary_edges),
|
|
389
|
+
loop_count=len(fragments),
|
|
390
|
+
dropped_zero_area_loop_count=dropped,
|
|
391
|
+
gpu_loop_component_count=1 if loop_backend in {"cuda", "metal"} else 0,
|
|
392
|
+
gpu_loop_edge_count=len(boundary_edges) if loop_backend in {"cuda", "metal"} else 0,
|
|
393
|
+
cuda_loop_component_count=1 if loop_backend == "cuda" else 0,
|
|
394
|
+
cuda_loop_edge_count=len(boundary_edges) if loop_backend == "cuda" else 0,
|
|
395
|
+
metal_loop_component_count=1 if loop_backend == "metal" else 0,
|
|
396
|
+
metal_loop_edge_count=len(boundary_edges) if loop_backend == "metal" else 0,
|
|
397
|
+
native_loop_component_count=1 if loop_backend == "native" else 0,
|
|
398
|
+
native_loop_edge_count=len(boundary_edges) if loop_backend == "native" else 0,
|
|
399
|
+
gpu_boundary_component_count=1 if used_gpu else 0,
|
|
400
|
+
gpu_boundary_edge_count=len(boundary_edges) if used_gpu else 0,
|
|
401
|
+
gpu_mixed_boundary_component_count=1 if used_gpu else 0,
|
|
402
|
+
gpu_mixed_boundary_edge_count=len(boundary_edges) if used_gpu else 0,
|
|
403
|
+
nested_direction_fragment_component_count=1 if _fragments_have_nested_bboxes(fragments) else 0,
|
|
404
|
+
),
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _one_sided_union_diff(target_fragments: tuple[DiffFragment, ...]) -> tuple[tuple[DiffFragment, ...], _ComponentSideStats]:
|
|
409
|
+
polygons = tuple(fragment.points for fragment in target_fragments)
|
|
410
|
+
bboxes = tuple(fragment.bbox for fragment in target_fragments)
|
|
411
|
+
segments = _build_segments({"target": polygons})
|
|
412
|
+
if not segments:
|
|
413
|
+
return (), _ComponentSideStats(0, 0, 0)
|
|
414
|
+
gpu_split_free = False
|
|
415
|
+
boundary_edges, used_gpu = _maybe_cuda_unsplit_boundary_edges_for_one_sided_union(segments, polygons, bboxes)
|
|
416
|
+
if boundary_edges is not None:
|
|
417
|
+
gpu_split_free = True
|
|
418
|
+
else:
|
|
419
|
+
splits = _segment_splits(segments)
|
|
420
|
+
boundary_edges, used_gpu = _maybe_cuda_boundary_edges_for_one_sided_union(segments, splits, polygons, bboxes)
|
|
421
|
+
fragments, dropped, loop_backend = _fragments_from_boundary_edges(boundary_edges)
|
|
422
|
+
return (
|
|
423
|
+
tuple(sorted(fragments, key=_fragment_sort_key)),
|
|
424
|
+
_ComponentSideStats(
|
|
425
|
+
boundary_edge_count=len(boundary_edges),
|
|
426
|
+
loop_count=len(fragments),
|
|
427
|
+
dropped_zero_area_loop_count=dropped,
|
|
428
|
+
gpu_loop_component_count=1 if loop_backend in {"cuda", "metal"} else 0,
|
|
429
|
+
gpu_loop_edge_count=len(boundary_edges) if loop_backend in {"cuda", "metal"} else 0,
|
|
430
|
+
cuda_loop_component_count=1 if loop_backend == "cuda" else 0,
|
|
431
|
+
cuda_loop_edge_count=len(boundary_edges) if loop_backend == "cuda" else 0,
|
|
432
|
+
metal_loop_component_count=1 if loop_backend == "metal" else 0,
|
|
433
|
+
metal_loop_edge_count=len(boundary_edges) if loop_backend == "metal" else 0,
|
|
434
|
+
native_loop_component_count=1 if loop_backend == "native" else 0,
|
|
435
|
+
native_loop_edge_count=len(boundary_edges) if loop_backend == "native" else 0,
|
|
436
|
+
gpu_boundary_component_count=1 if used_gpu else 0,
|
|
437
|
+
gpu_boundary_edge_count=len(boundary_edges) if used_gpu else 0,
|
|
438
|
+
gpu_split_free_component_count=1 if gpu_split_free else 0,
|
|
439
|
+
nested_direction_fragment_component_count=1 if _fragments_have_nested_bboxes(fragments) else 0,
|
|
440
|
+
),
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _fragments_have_nested_bboxes(fragments: Sequence[DiffFragment]) -> bool:
|
|
445
|
+
bboxes = tuple(fragment.bbox for fragment in fragments)
|
|
446
|
+
for index, outer in enumerate(bboxes):
|
|
447
|
+
for inner_index, inner in enumerate(bboxes):
|
|
448
|
+
if index != inner_index and _bbox_strictly_contains(outer, inner):
|
|
449
|
+
return True
|
|
450
|
+
return False
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _bbox_strictly_contains(
|
|
454
|
+
outer: tuple[int, int, int, int],
|
|
455
|
+
inner: tuple[int, int, int, int],
|
|
456
|
+
) -> bool:
|
|
457
|
+
return (
|
|
458
|
+
outer[0] <= inner[0]
|
|
459
|
+
and outer[1] <= inner[1]
|
|
460
|
+
and outer[2] >= inner[2]
|
|
461
|
+
and outer[3] >= inner[3]
|
|
462
|
+
and outer != inner
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _maybe_cuda_unsplit_boundary_edges_for_one_sided_union(
|
|
467
|
+
segments: tuple[_SegmentRecord, ...],
|
|
468
|
+
polygons: tuple[tuple[Point, ...], ...],
|
|
469
|
+
bboxes: tuple[tuple[int, int, int, int], ...],
|
|
470
|
+
) -> tuple[tuple[FloatSegment, ...] | None, bool]:
|
|
471
|
+
if len(segments) < 256 or len(polygons) < 4:
|
|
472
|
+
return None, False
|
|
473
|
+
try:
|
|
474
|
+
edges, safe = _cuda_unsplit_boundary_edges_for_one_sided_union(segments, polygons, bboxes)
|
|
475
|
+
except Exception:
|
|
476
|
+
return None, False
|
|
477
|
+
if not safe:
|
|
478
|
+
return None, False
|
|
479
|
+
return edges, True
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _cuda_unsplit_boundary_edges_for_one_sided_union(
|
|
483
|
+
segments: tuple[_SegmentRecord, ...],
|
|
484
|
+
polygons: tuple[tuple[Point, ...], ...],
|
|
485
|
+
bboxes: tuple[tuple[int, int, int, int], ...],
|
|
486
|
+
) -> tuple[tuple[FloatSegment, ...], bool]:
|
|
487
|
+
from .backends.cuda_ctypes import run_cuda_one_sided_boundary_edges_unsplit_if_safe
|
|
488
|
+
|
|
489
|
+
vertices, offsets, signed_areas, segment_points, segment_polygon_indices = _one_sided_cuda_arrays(
|
|
490
|
+
segments,
|
|
491
|
+
polygons,
|
|
492
|
+
)
|
|
493
|
+
edges, safe = run_cuda_one_sided_boundary_edges_unsplit_if_safe(
|
|
494
|
+
vertices,
|
|
495
|
+
offsets,
|
|
496
|
+
np.asarray(bboxes, dtype=np.int64),
|
|
497
|
+
signed_areas,
|
|
498
|
+
segment_points,
|
|
499
|
+
segment_polygon_indices,
|
|
500
|
+
)
|
|
501
|
+
return tuple(sorted(((_point_key((edge[0][0], edge[0][1])), _point_key((edge[1][0], edge[1][1]))) for edge in edges))), safe
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _maybe_cuda_boundary_edges_for_one_sided_union(
|
|
505
|
+
segments: tuple[_SegmentRecord, ...],
|
|
506
|
+
splits: tuple[tuple[float, ...], ...],
|
|
507
|
+
polygons: tuple[tuple[Point, ...], ...],
|
|
508
|
+
bboxes: tuple[tuple[int, int, int, int], ...],
|
|
509
|
+
) -> tuple[tuple[FloatSegment, ...], bool]:
|
|
510
|
+
piece_count = sum(1 for split in splits for left, right in zip(split, split[1:]) if right - left > 1e-10)
|
|
511
|
+
if piece_count < 256 or len(polygons) < 4:
|
|
512
|
+
return _boundary_edges_for_one_sided_union(segments, splits, polygons, bboxes), False
|
|
513
|
+
try:
|
|
514
|
+
return _cuda_boundary_edges_for_one_sided_union(segments, splits, polygons, bboxes), True
|
|
515
|
+
except Exception:
|
|
516
|
+
pass
|
|
517
|
+
# Keep Metal boundary sampling as a direct backend experiment for now. The
|
|
518
|
+
# production source-edge path uses the CPU reference sampler for parity on
|
|
519
|
+
# compact, overlapping undercut geometry.
|
|
520
|
+
return _boundary_edges_for_one_sided_union(segments, splits, polygons, bboxes), False
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _maybe_cuda_boundary_edges_for_direction(
|
|
524
|
+
segments: tuple[_SegmentRecord, ...],
|
|
525
|
+
splits: tuple[tuple[float, ...], ...],
|
|
526
|
+
polygons_by_side: dict[str, tuple[tuple[Point, ...], ...]],
|
|
527
|
+
bboxes_by_side: dict[str, tuple[tuple[int, int, int, int], ...]],
|
|
528
|
+
*,
|
|
529
|
+
direction: str,
|
|
530
|
+
) -> tuple[tuple[FloatSegment, ...], bool]:
|
|
531
|
+
piece_count = sum(1 for split in splits for left, right in zip(split, split[1:]) if right - left > 1e-10)
|
|
532
|
+
if piece_count < 128 or not polygons_by_side["old"] or not polygons_by_side["new"]:
|
|
533
|
+
return _boundary_edges_for_direction(segments, splits, polygons_by_side, bboxes_by_side, direction=direction), False
|
|
534
|
+
try:
|
|
535
|
+
return _cuda_boundary_edges_for_direction(segments, splits, polygons_by_side, bboxes_by_side, direction=direction), True
|
|
536
|
+
except Exception:
|
|
537
|
+
pass
|
|
538
|
+
# Keep Metal boundary sampling as a direct backend experiment for now. The
|
|
539
|
+
# production source-edge path uses the CPU reference sampler for parity on
|
|
540
|
+
# compact, overlapping undercut geometry.
|
|
541
|
+
return _boundary_edges_for_direction(segments, splits, polygons_by_side, bboxes_by_side, direction=direction), False
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _cuda_boundary_edges_for_direction(
|
|
545
|
+
segments: tuple[_SegmentRecord, ...],
|
|
546
|
+
splits: tuple[tuple[float, ...], ...],
|
|
547
|
+
polygons_by_side: dict[str, tuple[tuple[Point, ...], ...]],
|
|
548
|
+
bboxes_by_side: dict[str, tuple[tuple[int, int, int, int], ...]],
|
|
549
|
+
*,
|
|
550
|
+
direction: str,
|
|
551
|
+
) -> tuple[FloatSegment, ...]:
|
|
552
|
+
from .backends.cuda_ctypes import run_cuda_mixed_boundary_edges
|
|
553
|
+
|
|
554
|
+
vertices, offsets, bboxes, polygon_sides = _mixed_cuda_polygon_arrays(polygons_by_side, bboxes_by_side)
|
|
555
|
+
piece_rows: list[tuple[float, float, float, float]] = []
|
|
556
|
+
for segment_index, segment in enumerate(segments):
|
|
557
|
+
for left_t, right_t in zip(splits[segment_index], splits[segment_index][1:]):
|
|
558
|
+
if right_t - left_t <= 1e-10:
|
|
559
|
+
continue
|
|
560
|
+
start = _point_at(segment, left_t)
|
|
561
|
+
stop = _point_at(segment, right_t)
|
|
562
|
+
piece_rows.append((start[0], start[1], stop[0], stop[1]))
|
|
563
|
+
if not piece_rows:
|
|
564
|
+
return ()
|
|
565
|
+
edges = run_cuda_mixed_boundary_edges(
|
|
566
|
+
vertices,
|
|
567
|
+
offsets,
|
|
568
|
+
bboxes,
|
|
569
|
+
polygon_sides,
|
|
570
|
+
np.asarray(piece_rows, dtype=np.float64),
|
|
571
|
+
direction=direction,
|
|
572
|
+
)
|
|
573
|
+
return tuple(sorted(((_point_key((edge[0][0], edge[0][1])), _point_key((edge[1][0], edge[1][1]))) for edge in edges)))
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _metal_boundary_edges_for_direction(
|
|
577
|
+
segments: tuple[_SegmentRecord, ...],
|
|
578
|
+
splits: tuple[tuple[float, ...], ...],
|
|
579
|
+
polygons_by_side: dict[str, tuple[tuple[Point, ...], ...]],
|
|
580
|
+
bboxes_by_side: dict[str, tuple[tuple[int, int, int, int], ...]],
|
|
581
|
+
*,
|
|
582
|
+
direction: str,
|
|
583
|
+
) -> tuple[FloatSegment, ...]:
|
|
584
|
+
from .backends.metal_ctypes import run_metal_mixed_boundary_edges
|
|
585
|
+
|
|
586
|
+
vertices, offsets, bboxes, polygon_sides = _mixed_cuda_polygon_arrays(polygons_by_side, bboxes_by_side)
|
|
587
|
+
piece_rows: list[tuple[float, float, float, float]] = []
|
|
588
|
+
for segment_index, segment in enumerate(segments):
|
|
589
|
+
for left_t, right_t in zip(splits[segment_index], splits[segment_index][1:]):
|
|
590
|
+
if right_t - left_t <= 1e-10:
|
|
591
|
+
continue
|
|
592
|
+
start = _point_at(segment, left_t)
|
|
593
|
+
stop = _point_at(segment, right_t)
|
|
594
|
+
piece_rows.append((start[0], start[1], stop[0], stop[1]))
|
|
595
|
+
if not piece_rows:
|
|
596
|
+
return ()
|
|
597
|
+
edges = run_metal_mixed_boundary_edges(
|
|
598
|
+
vertices,
|
|
599
|
+
offsets,
|
|
600
|
+
bboxes,
|
|
601
|
+
polygon_sides,
|
|
602
|
+
np.asarray(piece_rows, dtype=np.float64),
|
|
603
|
+
direction=direction,
|
|
604
|
+
)
|
|
605
|
+
return tuple(sorted(((_point_key((edge[0][0], edge[0][1])), _point_key((edge[1][0], edge[1][1]))) for edge in edges)))
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _mixed_cuda_polygon_arrays(
|
|
609
|
+
polygons_by_side: dict[str, tuple[tuple[Point, ...], ...]],
|
|
610
|
+
bboxes_by_side: dict[str, tuple[tuple[int, int, int, int], ...]],
|
|
611
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
612
|
+
vertex_rows: list[Point] = []
|
|
613
|
+
offsets = [0]
|
|
614
|
+
bboxes: list[tuple[int, int, int, int]] = []
|
|
615
|
+
sides: list[int] = []
|
|
616
|
+
for side, side_id in (("old", 1), ("new", 2)):
|
|
617
|
+
for polygon, bbox in zip(polygons_by_side[side], bboxes_by_side[side]):
|
|
618
|
+
vertex_rows.extend(polygon)
|
|
619
|
+
offsets.append(len(vertex_rows))
|
|
620
|
+
bboxes.append(bbox)
|
|
621
|
+
sides.append(side_id)
|
|
622
|
+
return (
|
|
623
|
+
np.asarray(vertex_rows, dtype=np.int64),
|
|
624
|
+
np.asarray(offsets, dtype=np.uint64),
|
|
625
|
+
np.asarray(bboxes, dtype=np.int64),
|
|
626
|
+
np.asarray(sides, dtype=np.uint8),
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _cuda_boundary_edges_for_one_sided_union(
|
|
631
|
+
segments: tuple[_SegmentRecord, ...],
|
|
632
|
+
splits: tuple[tuple[float, ...], ...],
|
|
633
|
+
polygons: tuple[tuple[Point, ...], ...],
|
|
634
|
+
bboxes: tuple[tuple[int, int, int, int], ...],
|
|
635
|
+
) -> tuple[FloatSegment, ...]:
|
|
636
|
+
from .backends.cuda_ctypes import run_cuda_one_sided_boundary_edges
|
|
637
|
+
|
|
638
|
+
piece_rows: list[tuple[float, float, float, float]] = []
|
|
639
|
+
piece_polygon_indices: list[int] = []
|
|
640
|
+
for segment_index, segment in enumerate(segments):
|
|
641
|
+
for left_t, right_t in zip(splits[segment_index], splits[segment_index][1:]):
|
|
642
|
+
if right_t - left_t <= 1e-10:
|
|
643
|
+
continue
|
|
644
|
+
start = _point_at(segment, left_t)
|
|
645
|
+
stop = _point_at(segment, right_t)
|
|
646
|
+
piece_rows.append((start[0], start[1], stop[0], stop[1]))
|
|
647
|
+
piece_polygon_indices.append(segment.polygon_index)
|
|
648
|
+
if not piece_rows:
|
|
649
|
+
return ()
|
|
650
|
+
vertices, offsets, signed_areas, _segment_points, _segment_polygon_indices = _one_sided_cuda_arrays(
|
|
651
|
+
(),
|
|
652
|
+
polygons,
|
|
653
|
+
)
|
|
654
|
+
edges = run_cuda_one_sided_boundary_edges(
|
|
655
|
+
vertices,
|
|
656
|
+
offsets,
|
|
657
|
+
np.asarray(bboxes, dtype=np.int64),
|
|
658
|
+
signed_areas,
|
|
659
|
+
np.asarray(piece_rows, dtype=np.float64),
|
|
660
|
+
np.asarray(piece_polygon_indices, dtype=np.uint32),
|
|
661
|
+
)
|
|
662
|
+
return tuple(sorted(((_point_key((edge[0][0], edge[0][1])), _point_key((edge[1][0], edge[1][1]))) for edge in edges)))
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _metal_boundary_edges_for_one_sided_union(
|
|
666
|
+
segments: tuple[_SegmentRecord, ...],
|
|
667
|
+
splits: tuple[tuple[float, ...], ...],
|
|
668
|
+
polygons: tuple[tuple[Point, ...], ...],
|
|
669
|
+
bboxes: tuple[tuple[int, int, int, int], ...],
|
|
670
|
+
) -> tuple[FloatSegment, ...]:
|
|
671
|
+
from .backends.metal_ctypes import run_metal_one_sided_boundary_edges
|
|
672
|
+
|
|
673
|
+
piece_rows: list[tuple[float, float, float, float]] = []
|
|
674
|
+
piece_polygon_indices: list[int] = []
|
|
675
|
+
for segment_index, segment in enumerate(segments):
|
|
676
|
+
for left_t, right_t in zip(splits[segment_index], splits[segment_index][1:]):
|
|
677
|
+
if right_t - left_t <= 1e-10:
|
|
678
|
+
continue
|
|
679
|
+
start = _point_at(segment, left_t)
|
|
680
|
+
stop = _point_at(segment, right_t)
|
|
681
|
+
piece_rows.append((start[0], start[1], stop[0], stop[1]))
|
|
682
|
+
piece_polygon_indices.append(segment.polygon_index)
|
|
683
|
+
if not piece_rows:
|
|
684
|
+
return ()
|
|
685
|
+
vertices, offsets, signed_areas, _segment_points, _segment_polygon_indices = _one_sided_cuda_arrays(
|
|
686
|
+
(),
|
|
687
|
+
polygons,
|
|
688
|
+
)
|
|
689
|
+
edges = run_metal_one_sided_boundary_edges(
|
|
690
|
+
vertices,
|
|
691
|
+
offsets,
|
|
692
|
+
np.asarray(bboxes, dtype=np.int64),
|
|
693
|
+
signed_areas,
|
|
694
|
+
np.asarray(piece_rows, dtype=np.float64),
|
|
695
|
+
np.asarray(piece_polygon_indices, dtype=np.uint32),
|
|
696
|
+
)
|
|
697
|
+
return tuple(sorted(((_point_key((edge[0][0], edge[0][1])), _point_key((edge[1][0], edge[1][1]))) for edge in edges)))
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _one_sided_cuda_arrays(
|
|
701
|
+
segments: tuple[_SegmentRecord, ...],
|
|
702
|
+
polygons: tuple[tuple[Point, ...], ...],
|
|
703
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
704
|
+
vertex_rows: list[Point] = []
|
|
705
|
+
offsets = [0]
|
|
706
|
+
signed_areas: list[int] = []
|
|
707
|
+
for polygon in polygons:
|
|
708
|
+
vertex_rows.extend(polygon)
|
|
709
|
+
offsets.append(len(vertex_rows))
|
|
710
|
+
signed_areas.append(_signed_twice_area(polygon))
|
|
711
|
+
segment_rows = tuple((segment.start[0], segment.start[1], segment.stop[0], segment.stop[1]) for segment in segments)
|
|
712
|
+
segment_polygon_indices = tuple(segment.polygon_index for segment in segments)
|
|
713
|
+
return (
|
|
714
|
+
np.asarray(vertex_rows, dtype=np.int64),
|
|
715
|
+
np.asarray(offsets, dtype=np.uint64),
|
|
716
|
+
np.asarray(signed_areas, dtype=np.int64),
|
|
717
|
+
np.asarray(segment_rows, dtype=np.float64).reshape((-1, 4)),
|
|
718
|
+
np.asarray(segment_polygon_indices, dtype=np.uint32),
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _fragments_from_boundary_edges(boundary_edges: tuple[FloatSegment, ...]) -> tuple[list[DiffFragment], int, str]:
|
|
723
|
+
integer_edges = _integer_edges_array_if_exact(boundary_edges)
|
|
724
|
+
if integer_edges is not None:
|
|
725
|
+
cuda = _try_cuda_fragments_from_integer_boundary_edges(integer_edges)
|
|
726
|
+
if cuda is not None:
|
|
727
|
+
return list(cuda.fragments), cuda.dropped_zero_area_loop_count, "cuda"
|
|
728
|
+
metal = _try_metal_fragments_from_integer_boundary_edges(integer_edges)
|
|
729
|
+
if metal is not None:
|
|
730
|
+
return list(metal.fragments), metal.dropped_zero_area_loop_count, "metal"
|
|
731
|
+
native = _try_native_fragments_from_integer_boundary_edges(integer_edges)
|
|
732
|
+
if native is not None:
|
|
733
|
+
return list(native.fragments), native.dropped_zero_area_loop_count, "native"
|
|
734
|
+
loops = _trace_loops(boundary_edges)
|
|
735
|
+
fragments: list[DiffFragment] = []
|
|
736
|
+
dropped = 0
|
|
737
|
+
for loop in loops:
|
|
738
|
+
points = _clean_integer_loop(_snap_loop(loop))
|
|
739
|
+
if len(points) < 3:
|
|
740
|
+
dropped += 1
|
|
741
|
+
continue
|
|
742
|
+
twice_area = polygon_twice_area(points)
|
|
743
|
+
if twice_area == 0:
|
|
744
|
+
dropped += 1
|
|
745
|
+
continue
|
|
746
|
+
fragments.append(DiffFragment(points=points, bbox=polygon_bbox(points), twice_area=twice_area))
|
|
747
|
+
return fragments, dropped, "python"
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def _integer_edges_array_if_exact(boundary_edges: tuple[FloatSegment, ...]) -> np.ndarray | None:
|
|
751
|
+
rows: list[tuple[int, int, int, int]] = []
|
|
752
|
+
for start, stop in boundary_edges:
|
|
753
|
+
snapped = (_snap_coordinate(start[0]), _snap_coordinate(start[1]), _snap_coordinate(stop[0]), _snap_coordinate(stop[1]))
|
|
754
|
+
if snapped[0] is None or snapped[1] is None or snapped[2] is None or snapped[3] is None:
|
|
755
|
+
return None
|
|
756
|
+
rows.append((snapped[0], snapped[1], snapped[2], snapped[3]))
|
|
757
|
+
return np.asarray(rows, dtype=np.int64)
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _try_cuda_fragments_from_integer_boundary_edges(edges: np.ndarray):
|
|
761
|
+
if len(edges) > 4096:
|
|
762
|
+
return None
|
|
763
|
+
try:
|
|
764
|
+
from .backends.cuda_ctypes import run_cuda_boundary_loops_from_edges
|
|
765
|
+
|
|
766
|
+
return run_cuda_boundary_loops_from_edges(edges)
|
|
767
|
+
except Exception:
|
|
768
|
+
return None
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _try_metal_fragments_from_integer_boundary_edges(edges: np.ndarray):
|
|
772
|
+
# The Metal loop tracer is kept as a direct backend experiment, but source-edge
|
|
773
|
+
# evidence needs the native/Python loop assemblers for parity on compact
|
|
774
|
+
# overlapping undercut geometry.
|
|
775
|
+
return None
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def _try_native_fragments_from_integer_boundary_edges(edges: np.ndarray):
|
|
779
|
+
if len(edges) < 64:
|
|
780
|
+
return None
|
|
781
|
+
try:
|
|
782
|
+
from .backends.boundary_loop_ctypes import native_boundary_loops_from_edges
|
|
783
|
+
|
|
784
|
+
return native_boundary_loops_from_edges(edges)
|
|
785
|
+
except Exception:
|
|
786
|
+
return None
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _snap_coordinate(value: float) -> int | None:
|
|
790
|
+
if not isfinite(value):
|
|
791
|
+
return None
|
|
792
|
+
rounded = round(value)
|
|
793
|
+
if abs(value - rounded) > 1e-9:
|
|
794
|
+
return None
|
|
795
|
+
return int(rounded)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _build_segments(polygons_by_side: dict[str, tuple[tuple[Point, ...], ...]]) -> tuple[_SegmentRecord, ...]:
|
|
799
|
+
segments: list[_SegmentRecord] = []
|
|
800
|
+
for side, polygons in polygons_by_side.items():
|
|
801
|
+
for polygon_index, points in enumerate(polygons):
|
|
802
|
+
for index, point in enumerate(points):
|
|
803
|
+
next_point = points[(index + 1) % len(points)]
|
|
804
|
+
if point == next_point:
|
|
805
|
+
continue
|
|
806
|
+
segments.append(
|
|
807
|
+
_SegmentRecord(
|
|
808
|
+
side=side,
|
|
809
|
+
polygon_index=polygon_index,
|
|
810
|
+
start=(float(point[0]), float(point[1])),
|
|
811
|
+
stop=(float(next_point[0]), float(next_point[1])),
|
|
812
|
+
)
|
|
813
|
+
)
|
|
814
|
+
return tuple(segments)
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def _segment_splits(segments: tuple[_SegmentRecord, ...]) -> tuple[tuple[float, ...], ...]:
|
|
818
|
+
splits: list[set[float]] = [{0.0, 1.0} for _ in segments]
|
|
819
|
+
bboxes = tuple(_segment_bbox(segment) for segment in segments)
|
|
820
|
+
ordered_indices = tuple(sorted(range(len(segments)), key=lambda index: (bboxes[index][0], bboxes[index][1], index)))
|
|
821
|
+
for left_order_index, left_index in enumerate(ordered_indices):
|
|
822
|
+
left = segments[left_index]
|
|
823
|
+
left_bbox = bboxes[left_index]
|
|
824
|
+
left_dx = left.stop[0] - left.start[0]
|
|
825
|
+
left_dy = left.stop[1] - left.start[1]
|
|
826
|
+
for right_index in ordered_indices[left_order_index + 1 :]:
|
|
827
|
+
right_bbox = bboxes[right_index]
|
|
828
|
+
if right_bbox[0] > left_bbox[2]:
|
|
829
|
+
break
|
|
830
|
+
if not _float_bbox_contact(left_bbox, right_bbox):
|
|
831
|
+
continue
|
|
832
|
+
right = segments[right_index]
|
|
833
|
+
right_dx = right.stop[0] - right.start[0]
|
|
834
|
+
right_dy = right.stop[1] - right.start[1]
|
|
835
|
+
denom = _cross(left_dx, left_dy, right_dx, right_dy)
|
|
836
|
+
cx = right.start[0] - left.start[0]
|
|
837
|
+
cy = right.start[1] - left.start[1]
|
|
838
|
+
if abs(denom) <= 1e-9:
|
|
839
|
+
if abs(_cross(cx, cy, left_dx, left_dy)) <= 1e-6:
|
|
840
|
+
_add_collinear_splits(splits, left_index, right_index, left, right)
|
|
841
|
+
continue
|
|
842
|
+
left_t = _cross(cx, cy, right_dx, right_dy) / denom
|
|
843
|
+
right_t = _cross(cx, cy, left_dx, left_dy) / denom
|
|
844
|
+
if _within_unit_interval(left_t) and _within_unit_interval(right_t):
|
|
845
|
+
_add_split(splits[left_index], left_t)
|
|
846
|
+
_add_split(splits[right_index], right_t)
|
|
847
|
+
return tuple(tuple(sorted(values)) for values in splits)
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
def _segment_bbox(segment: _SegmentRecord) -> tuple[float, float, float, float]:
|
|
851
|
+
return (
|
|
852
|
+
min(segment.start[0], segment.stop[0]),
|
|
853
|
+
min(segment.start[1], segment.stop[1]),
|
|
854
|
+
max(segment.start[0], segment.stop[0]),
|
|
855
|
+
max(segment.start[1], segment.stop[1]),
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def _float_bbox_contact(left: tuple[float, float, float, float], right: tuple[float, float, float, float]) -> bool:
|
|
860
|
+
return max(left[0], right[0]) <= min(left[2], right[2]) and max(left[1], right[1]) <= min(left[3], right[3])
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def _add_collinear_splits(
|
|
864
|
+
splits: list[set[float]],
|
|
865
|
+
left_index: int,
|
|
866
|
+
right_index: int,
|
|
867
|
+
left: _SegmentRecord,
|
|
868
|
+
right: _SegmentRecord,
|
|
869
|
+
) -> None:
|
|
870
|
+
left_dx = left.stop[0] - left.start[0]
|
|
871
|
+
left_dy = left.stop[1] - left.start[1]
|
|
872
|
+
right_dx = right.stop[0] - right.start[0]
|
|
873
|
+
right_dy = right.stop[1] - right.start[1]
|
|
874
|
+
left_len2 = left_dx * left_dx + left_dy * left_dy
|
|
875
|
+
right_len2 = right_dx * right_dx + right_dy * right_dy
|
|
876
|
+
if left_len2:
|
|
877
|
+
for point in (right.start, right.stop):
|
|
878
|
+
_add_split(splits[left_index], ((point[0] - left.start[0]) * left_dx + (point[1] - left.start[1]) * left_dy) / left_len2)
|
|
879
|
+
if right_len2:
|
|
880
|
+
for point in (left.start, left.stop):
|
|
881
|
+
_add_split(splits[right_index], ((point[0] - right.start[0]) * right_dx + (point[1] - right.start[1]) * right_dy) / right_len2)
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
def _boundary_edges_for_direction(
|
|
885
|
+
segments: tuple[_SegmentRecord, ...],
|
|
886
|
+
splits: tuple[tuple[float, ...], ...],
|
|
887
|
+
polygons_by_side: dict[str, tuple[tuple[Point, ...], ...]],
|
|
888
|
+
bboxes_by_side: dict[str, tuple[tuple[int, int, int, int], ...]],
|
|
889
|
+
*,
|
|
890
|
+
direction: str,
|
|
891
|
+
) -> tuple[FloatSegment, ...]:
|
|
892
|
+
counts: dict[FloatSegment, int] = defaultdict(int)
|
|
893
|
+
for segment_index, segment in enumerate(segments):
|
|
894
|
+
dx = segment.stop[0] - segment.start[0]
|
|
895
|
+
dy = segment.stop[1] - segment.start[1]
|
|
896
|
+
length = hypot(dx, dy)
|
|
897
|
+
if length <= 1e-12:
|
|
898
|
+
continue
|
|
899
|
+
normal = (-dy / length, dx / length)
|
|
900
|
+
for left_t, right_t in zip(splits[segment_index], splits[segment_index][1:]):
|
|
901
|
+
if right_t - left_t <= 1e-10:
|
|
902
|
+
continue
|
|
903
|
+
start = _point_at(segment, left_t)
|
|
904
|
+
stop = _point_at(segment, right_t)
|
|
905
|
+
midpoint = ((start[0] + stop[0]) * 0.5, (start[1] + stop[1]) * 0.5)
|
|
906
|
+
left_sample = (midpoint[0] + normal[0] * 1e-5, midpoint[1] + normal[1] * 1e-5)
|
|
907
|
+
right_sample = (midpoint[0] - normal[0] * 1e-5, midpoint[1] - normal[1] * 1e-5)
|
|
908
|
+
left_inside = _target_inside(polygons_by_side, bboxes_by_side, left_sample, direction)
|
|
909
|
+
right_inside = _target_inside(polygons_by_side, bboxes_by_side, right_sample, direction)
|
|
910
|
+
if left_inside and not right_inside:
|
|
911
|
+
_add_boundary_edge(counts, (_point_key(start), _point_key(stop)))
|
|
912
|
+
elif right_inside and not left_inside:
|
|
913
|
+
_add_boundary_edge(counts, (_point_key(stop), _point_key(start)))
|
|
914
|
+
edges: list[FloatSegment] = []
|
|
915
|
+
for edge, count in counts.items():
|
|
916
|
+
edges.extend(edge for _ in range(count))
|
|
917
|
+
return tuple(sorted(edges))
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def _boundary_edges_for_one_sided_union(
|
|
921
|
+
segments: tuple[_SegmentRecord, ...],
|
|
922
|
+
splits: tuple[tuple[float, ...], ...],
|
|
923
|
+
polygons: tuple[tuple[Point, ...], ...],
|
|
924
|
+
bboxes: tuple[tuple[int, int, int, int], ...],
|
|
925
|
+
) -> tuple[FloatSegment, ...]:
|
|
926
|
+
signed_areas = tuple(_signed_twice_area(polygon) for polygon in polygons)
|
|
927
|
+
counts: dict[FloatSegment, int] = defaultdict(int)
|
|
928
|
+
for segment_index, segment in enumerate(segments):
|
|
929
|
+
signed_area = signed_areas[segment.polygon_index]
|
|
930
|
+
if signed_area == 0:
|
|
931
|
+
continue
|
|
932
|
+
dx = segment.stop[0] - segment.start[0]
|
|
933
|
+
dy = segment.stop[1] - segment.start[1]
|
|
934
|
+
length = hypot(dx, dy)
|
|
935
|
+
if length <= 1e-12:
|
|
936
|
+
continue
|
|
937
|
+
normal = (-dy / length, dx / length)
|
|
938
|
+
for left_t, right_t in zip(splits[segment_index], splits[segment_index][1:]):
|
|
939
|
+
if right_t - left_t <= 1e-10:
|
|
940
|
+
continue
|
|
941
|
+
start = _point_at(segment, left_t)
|
|
942
|
+
stop = _point_at(segment, right_t)
|
|
943
|
+
midpoint = ((start[0] + stop[0]) * 0.5, (start[1] + stop[1]) * 0.5)
|
|
944
|
+
if signed_area > 0:
|
|
945
|
+
outside_sample = (midpoint[0] - normal[0] * 1e-5, midpoint[1] - normal[1] * 1e-5)
|
|
946
|
+
if not _union_contains(polygons, bboxes, outside_sample):
|
|
947
|
+
_add_boundary_edge(counts, (_point_key(start), _point_key(stop)))
|
|
948
|
+
else:
|
|
949
|
+
outside_sample = (midpoint[0] + normal[0] * 1e-5, midpoint[1] + normal[1] * 1e-5)
|
|
950
|
+
if not _union_contains(polygons, bboxes, outside_sample):
|
|
951
|
+
_add_boundary_edge(counts, (_point_key(stop), _point_key(start)))
|
|
952
|
+
edges: list[FloatSegment] = []
|
|
953
|
+
for edge, count in counts.items():
|
|
954
|
+
edges.extend(edge for _ in range(count))
|
|
955
|
+
return tuple(sorted(edges))
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def _add_boundary_edge(counts: dict[FloatSegment, int], edge: FloatSegment) -> None:
|
|
959
|
+
reverse = (edge[1], edge[0])
|
|
960
|
+
if counts.get(reverse, 0):
|
|
961
|
+
counts[reverse] -= 1
|
|
962
|
+
if counts[reverse] == 0:
|
|
963
|
+
del counts[reverse]
|
|
964
|
+
else:
|
|
965
|
+
counts[edge] += 1
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def _trace_loops(edges: tuple[FloatSegment, ...]) -> tuple[tuple[FloatPoint, ...], ...]:
|
|
969
|
+
outgoing: dict[FloatPoint, list[FloatPoint]] = defaultdict(list)
|
|
970
|
+
for start, stop in edges:
|
|
971
|
+
outgoing[start].append(stop)
|
|
972
|
+
for targets in outgoing.values():
|
|
973
|
+
targets.sort()
|
|
974
|
+
|
|
975
|
+
remaining: set[FloatSegment] = set(edges)
|
|
976
|
+
loops: list[tuple[FloatPoint, ...]] = []
|
|
977
|
+
while remaining:
|
|
978
|
+
start, stop = min(remaining)
|
|
979
|
+
remaining.remove((start, stop))
|
|
980
|
+
loop = [start, stop]
|
|
981
|
+
previous = start
|
|
982
|
+
current = stop
|
|
983
|
+
while current != start:
|
|
984
|
+
candidates = [point for point in outgoing.get(current, ()) if (current, point) in remaining]
|
|
985
|
+
if not candidates:
|
|
986
|
+
break
|
|
987
|
+
in_vec = (current[0] - previous[0], current[1] - previous[1])
|
|
988
|
+
next_point = min(
|
|
989
|
+
candidates,
|
|
990
|
+
key=lambda point: (_turn_key(in_vec, (point[0] - current[0], point[1] - current[1])), point),
|
|
991
|
+
)
|
|
992
|
+
remaining.remove((current, next_point))
|
|
993
|
+
loop.append(next_point)
|
|
994
|
+
previous, current = current, next_point
|
|
995
|
+
if loop[-1] == start:
|
|
996
|
+
loop.pop()
|
|
997
|
+
loops.append(tuple(loop))
|
|
998
|
+
return tuple(sorted(loops))
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def _target_inside(
|
|
1002
|
+
polygons_by_side: dict[str, tuple[tuple[Point, ...], ...]],
|
|
1003
|
+
bboxes_by_side: dict[str, tuple[tuple[int, int, int, int], ...]],
|
|
1004
|
+
point: FloatPoint,
|
|
1005
|
+
direction: str,
|
|
1006
|
+
) -> bool:
|
|
1007
|
+
old_inside = _union_contains(polygons_by_side["old"], bboxes_by_side["old"], point)
|
|
1008
|
+
new_inside = _union_contains(polygons_by_side["new"], bboxes_by_side["new"], point)
|
|
1009
|
+
if direction == "old":
|
|
1010
|
+
return old_inside and not new_inside
|
|
1011
|
+
return new_inside and not old_inside
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def _union_contains(
|
|
1015
|
+
polygons: tuple[tuple[Point, ...], ...],
|
|
1016
|
+
bboxes: tuple[tuple[int, int, int, int], ...],
|
|
1017
|
+
point: FloatPoint,
|
|
1018
|
+
) -> bool:
|
|
1019
|
+
return any(
|
|
1020
|
+
_point_in_bbox(point, bbox) and _point_in_polygon(point, polygon)
|
|
1021
|
+
for polygon, bbox in zip(polygons, bboxes)
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def _point_in_bbox(point: FloatPoint, bbox: tuple[int, int, int, int]) -> bool:
|
|
1026
|
+
return bbox[0] <= point[0] <= bbox[2] and bbox[1] <= point[1] <= bbox[3]
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def _point_in_polygon(point: FloatPoint, polygon: tuple[Point, ...]) -> bool:
|
|
1030
|
+
x, y = point
|
|
1031
|
+
inside = False
|
|
1032
|
+
for index, start in enumerate(polygon):
|
|
1033
|
+
stop = polygon[(index + 1) % len(polygon)]
|
|
1034
|
+
x0, y0 = float(start[0]), float(start[1])
|
|
1035
|
+
x1, y1 = float(stop[0]), float(stop[1])
|
|
1036
|
+
if (y0 > y) != (y1 > y):
|
|
1037
|
+
x_intersection = x0 + (y - y0) * (x1 - x0) / (y1 - y0)
|
|
1038
|
+
if x_intersection > x:
|
|
1039
|
+
inside = not inside
|
|
1040
|
+
return inside
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def _signed_twice_area(points: tuple[Point, ...]) -> int:
|
|
1044
|
+
area = 0
|
|
1045
|
+
for index, point in enumerate(points):
|
|
1046
|
+
next_point = points[(index + 1) % len(points)]
|
|
1047
|
+
area += point[0] * next_point[1] - next_point[0] * point[1]
|
|
1048
|
+
return area
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def _snap_loop(loop: tuple[FloatPoint, ...]) -> tuple[Point, ...]:
|
|
1052
|
+
points: list[Point] = []
|
|
1053
|
+
for point in loop:
|
|
1054
|
+
if not isfinite(point[0]) or not isfinite(point[1]):
|
|
1055
|
+
continue
|
|
1056
|
+
snapped = (round(point[0]), round(point[1]))
|
|
1057
|
+
if not points or points[-1] != snapped:
|
|
1058
|
+
points.append(snapped)
|
|
1059
|
+
if len(points) > 1 and points[0] == points[-1]:
|
|
1060
|
+
points.pop()
|
|
1061
|
+
return tuple(points)
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def _clean_integer_loop(points: tuple[Point, ...]) -> tuple[Point, ...]:
|
|
1065
|
+
cleaned = list(points)
|
|
1066
|
+
changed = True
|
|
1067
|
+
while changed and len(cleaned) >= 3:
|
|
1068
|
+
changed = False
|
|
1069
|
+
next_points: list[Point] = []
|
|
1070
|
+
for index, point in enumerate(cleaned):
|
|
1071
|
+
previous = cleaned[index - 1]
|
|
1072
|
+
following = cleaned[(index + 1) % len(cleaned)]
|
|
1073
|
+
if _collinear(previous, point, following):
|
|
1074
|
+
changed = True
|
|
1075
|
+
continue
|
|
1076
|
+
next_points.append(point)
|
|
1077
|
+
cleaned = next_points
|
|
1078
|
+
return tuple(cleaned)
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
def _point_at(segment: _SegmentRecord, t: float) -> FloatPoint:
|
|
1082
|
+
return (
|
|
1083
|
+
segment.start[0] + (segment.stop[0] - segment.start[0]) * t,
|
|
1084
|
+
segment.start[1] + (segment.stop[1] - segment.start[1]) * t,
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def _point_key(point: FloatPoint) -> FloatPoint:
|
|
1089
|
+
return (round(point[0], 9), round(point[1], 9))
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
def _add_split(splits: set[float], value: float) -> None:
|
|
1093
|
+
if _within_unit_interval(value):
|
|
1094
|
+
splits.add(min(1.0, max(0.0, value)))
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def _within_unit_interval(value: float) -> bool:
|
|
1098
|
+
return -1e-9 <= value <= 1.0 + 1e-9
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def _cross(left_x: float, left_y: float, right_x: float, right_y: float) -> float:
|
|
1102
|
+
return left_x * right_y - left_y * right_x
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
def _turn_key(in_vec: FloatPoint, out_vec: FloatPoint) -> tuple[int, float, float]:
|
|
1106
|
+
cross = in_vec[0] * out_vec[1] - in_vec[1] * out_vec[0]
|
|
1107
|
+
dot = in_vec[0] * out_vec[0] + in_vec[1] * out_vec[1]
|
|
1108
|
+
if cross < 0:
|
|
1109
|
+
turn_class = 0
|
|
1110
|
+
elif cross == 0 and dot > 0:
|
|
1111
|
+
turn_class = 1
|
|
1112
|
+
elif cross > 0:
|
|
1113
|
+
turn_class = 2
|
|
1114
|
+
else:
|
|
1115
|
+
turn_class = 3
|
|
1116
|
+
return turn_class, -abs(cross), -dot
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
def _collinear(left: Point, middle: Point, right: Point) -> bool:
|
|
1120
|
+
return (middle[0] - left[0]) * (right[1] - middle[1]) == (middle[1] - left[1]) * (right[0] - middle[0])
|