stdraw 0.7.13__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.
- stdraw/__init__.py +32 -0
- stdraw/__main__.py +350 -0
- stdraw/_interactive_js.py +2030 -0
- stdraw/anchors.py +103 -0
- stdraw/bowtielayout.py +350 -0
- stdraw/check.py +651 -0
- stdraw/columnlayout.py +531 -0
- stdraw/comblayout.py +533 -0
- stdraw/crossing_exact.py +1004 -0
- stdraw/delivery.py +81 -0
- stdraw/dsl.py +99 -0
- stdraw/layout3d.py +167 -0
- stdraw/layout_auto.py +1827 -0
- stdraw/layoutdiff.py +424 -0
- stdraw/loops.py +145 -0
- stdraw/metrics.py +1225 -0
- stdraw/model.py +134 -0
- stdraw/placement.py +220 -0
- stdraw/planar.py +625 -0
- stdraw/recipes.py +68 -0
- stdraw/regularity.py +176 -0
- stdraw/render.py +361 -0
- stdraw/render3d.py +165 -0
- stdraw/rendercheck.py +90 -0
- stdraw/ringlayout.py +475 -0
- stdraw/routing.py +2630 -0
- stdraw/spinewinglayout.py +329 -0
- stdraw/symlayout.py +245 -0
- stdraw/textmetrics.py +97 -0
- stdraw/visibility.py +382 -0
- stdraw-0.7.13.dist-info/METADATA +383 -0
- stdraw-0.7.13.dist-info/RECORD +36 -0
- stdraw-0.7.13.dist-info/WHEEL +5 -0
- stdraw-0.7.13.dist-info/entry_points.txt +2 -0
- stdraw-0.7.13.dist-info/licenses/LICENSE +21 -0
- stdraw-0.7.13.dist-info/top_level.txt +1 -0
stdraw/visibility.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""WS3 routing math layer: visibility-graph shortest straight-line paths + clearance-constrained minimum-curvature arcs.
|
|
3
|
+
|
|
4
|
+
Mathematical accounting (honest statement, consistent with the certificate wording):
|
|
5
|
+
1. Straight-line obstacle avoidance = **Dijkstra shortest path on the visibility graph over inflated obstacle corners**
|
|
6
|
+
(exactly optimal on the graph, a classic computational-geometry theorem). Obstacles are box-grown by clearance c: the inflated
|
|
7
|
+
rectangle >= the rounded-corner Minkowski region (|p-rect| < c), hence "segment misses the inflated rectangle => Euclidean
|
|
8
|
+
clearance to the original rectangle >= c". Versus the true shortest path in the continuous space of rounded obstacles, each
|
|
9
|
+
hugged corner adds at most (2-pi/2)*c ~= 0.43c px -- recovered by the corner-cutting effect of arc smoothing.
|
|
10
|
+
2. Every visibility edge is independently rechecked with the **exact segment-rectangle distance** (Liang-Barsky intersection
|
|
11
|
+
test + four-border segment-segment distances); clearance is an analytic guarantee, not a coincidence of discrete sampling.
|
|
12
|
+
3. Arc smoothing = tangent-inscribed circular arcs at bends: G1 tangent continuity (no inflection), curvature bounded
|
|
13
|
+
throughout (<= 1/r_min). Radii shrink along a deterministic sequence; each arc rechecks clearance at every sampled point;
|
|
14
|
+
this module builds the graph and rechecks at the "target clearance c"; the caller passes c = CLEARANCE + 1.5, leaving
|
|
15
|
+
a >= +1.4px margin at delivery-coordinate recheck, so the >=16px hard gate is never grazed.
|
|
16
|
+
4. Deterministic: no RNG. Node order, candidate order, radius sequence all fixed; same input gives byte-for-byte the same output.
|
|
17
|
+
|
|
18
|
+
This module is stdlib-only, with no networkx/ortools dependency.
|
|
19
|
+
"""
|
|
20
|
+
import math
|
|
21
|
+
from heapq import heappush, heappop
|
|
22
|
+
|
|
23
|
+
VIS_EPS = 1e-6 # float tolerance for visibility tests
|
|
24
|
+
ARC_SAMPLES = 16 # sampled segments per circular arc (for geometry/crossing rechecks)
|
|
25
|
+
FILLET_R_MAX = 36.0 # smoothing-arc maximum radius px
|
|
26
|
+
FILLET_R_SHRINK = (1.0, 0.75, 0.55, 0.40, 0.28, 0.18, 0.10) # deterministic shrink sequence
|
|
27
|
+
FILLET_TAU_MAX = 0.45 # upper bound on tangent-segment length as a fraction of the adjacent segment length
|
|
28
|
+
DEFLECT_SKIP = 0.03 # bend deflection < ~1.7 degrees is treated as straight, no arc
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# -- exact distance primitives -----------------------------------------
|
|
32
|
+
def _pt_seg_dist(px, py, a, b):
|
|
33
|
+
"""Minimum distance from a point to a segment."""
|
|
34
|
+
vx, vy = b[0] - a[0], b[1] - a[1]
|
|
35
|
+
L2 = vx * vx + vy * vy
|
|
36
|
+
if L2 < VIS_EPS:
|
|
37
|
+
return math.hypot(px - a[0], py - a[1])
|
|
38
|
+
t = max(0.0, min(1.0, ((px - a[0]) * vx + (py - a[1]) * vy) / L2))
|
|
39
|
+
return math.hypot(px - (a[0] + t * vx), py - (a[1] + t * vy))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def seg_seg_dist(p1, p2, p3, p4):
|
|
43
|
+
"""Distance between two segments: 0 if intersecting; otherwise the minimum of the four endpoint-to-other-segment distances."""
|
|
44
|
+
def orient(a, b, c):
|
|
45
|
+
v = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
|
|
46
|
+
return 0 if abs(v) < VIS_EPS else (1 if v > 0 else -1)
|
|
47
|
+
d1, d2 = orient(p3, p4, p1), orient(p3, p4, p2)
|
|
48
|
+
d3, d4 = orient(p1, p2, p3), orient(p1, p2, p4)
|
|
49
|
+
if d1 != d2 and d3 != d4 and d1 != 0 and d3 != 0:
|
|
50
|
+
return 0.0
|
|
51
|
+
return min(_pt_seg_dist(p1[0], p1[1], p3, p4),
|
|
52
|
+
_pt_seg_dist(p2[0], p2[1], p3, p4),
|
|
53
|
+
_pt_seg_dist(p3[0], p3[1], p1, p2),
|
|
54
|
+
_pt_seg_dist(p4[0], p4[1], p1, p2))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _seg_hits_rect(x1, y1, x2, y2, cx, cy, hw, hh):
|
|
58
|
+
"""Liang-Barsky: whether a segment enters the rectangle with positive length (border-touching does not count). Same accounting as metrics.
|
|
59
|
+
|
|
60
|
+
Self-contained implementation (metrics -> routing -> this module would be a circular import, so no back-reference).
|
|
61
|
+
"""
|
|
62
|
+
dx, dy = x2 - x1, y2 - y1
|
|
63
|
+
t0, t1 = 0.0, 1.0
|
|
64
|
+
for p, q in ((-dx, x1 - (cx - hw)), (dx, (cx + hw) - x1),
|
|
65
|
+
(-dy, y1 - (cy - hh)), (dy, (cy + hh) - y1)):
|
|
66
|
+
if abs(p) < VIS_EPS:
|
|
67
|
+
if q < 0:
|
|
68
|
+
return False
|
|
69
|
+
else:
|
|
70
|
+
r = q / p
|
|
71
|
+
if p < 0:
|
|
72
|
+
if r > t1:
|
|
73
|
+
return False
|
|
74
|
+
if r > t0:
|
|
75
|
+
t0 = r
|
|
76
|
+
else:
|
|
77
|
+
if r < t0:
|
|
78
|
+
return False
|
|
79
|
+
if r < t1:
|
|
80
|
+
t1 = r
|
|
81
|
+
return t1 - t0 > VIS_EPS
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def seg_rect_distance(p1, p2, rect):
|
|
85
|
+
"""Exact minimum distance from a segment to a rectangle (center + half-width/half-height): 0 if intersecting.
|
|
86
|
+
|
|
87
|
+
Intersection uses Liang-Barsky (positive-length entry counts, border-touching does not); when disjoint, take the minimum
|
|
88
|
+
segment-segment distance over the rectangle's four borders.
|
|
89
|
+
"""
|
|
90
|
+
cx, cy, hw, hh = rect
|
|
91
|
+
if _seg_hits_rect(p1[0], p1[1], p2[0], p2[1], cx, cy, hw, hh):
|
|
92
|
+
return 0.0
|
|
93
|
+
corners = ((cx - hw, cy - hh), (cx + hw, cy - hh),
|
|
94
|
+
(cx + hw, cy + hh), (cx - hw, cy + hh))
|
|
95
|
+
return min(seg_seg_dist(p1, p2, corners[k], corners[(k + 1) % 4])
|
|
96
|
+
for k in range(4))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def pt_rect_distance(px, py, rect):
|
|
100
|
+
"""Euclidean distance from a point to a rectangle (0 when inside)."""
|
|
101
|
+
cx, cy, hw, hh = rect
|
|
102
|
+
dx = max(abs(px - cx) - hw, 0.0)
|
|
103
|
+
dy = max(abs(py - cy) - hh, 0.0)
|
|
104
|
+
return math.hypot(dx, dy)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# -- visibility graph + Dijkstra ----------------------------------------
|
|
108
|
+
def _graph_nodes(src, dst, rects, c):
|
|
109
|
+
"""Nodes = source/sink + the 4 corners of every inflated rectangle (inflated corners sit exactly c from the original rectangle)."""
|
|
110
|
+
nodes = [tuple(src), tuple(dst)]
|
|
111
|
+
for (cx, cy, hw, hh) in rects:
|
|
112
|
+
gx, gy = hw + c, hh + c
|
|
113
|
+
nodes.extend([(cx - gx, cy - gy), (cx + gx, cy - gy),
|
|
114
|
+
(cx + gx, cy + gy), (cx - gx, cy + gy)])
|
|
115
|
+
return nodes
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _visible(p1, p2, rects, c):
|
|
119
|
+
"""Segment keeps clearance >= c - epsilon against all obstacles (analytic recheck, no sampling).
|
|
120
|
+
|
|
121
|
+
Performance (WS5): first a lower-bound quick filter via the "segment bbox x rectangle bbox" distance -- any point on the
|
|
122
|
+
segment is at a distance from the rectangle >= the bbox distance, so when the bbox distance >= c - epsilon the exact distance
|
|
123
|
+
is necessarily >= c - epsilon and the exact computation can be skipped (exactly equivalent, changes no test outcome).
|
|
124
|
+
"""
|
|
125
|
+
t = c - VIS_EPS
|
|
126
|
+
t2 = t * t
|
|
127
|
+
x1l, x1h = (p1[0], p2[0]) if p1[0] <= p2[0] else (p2[0], p1[0])
|
|
128
|
+
y1l, y1h = (p1[1], p2[1]) if p1[1] <= p2[1] else (p2[1], p1[1])
|
|
129
|
+
for rect in rects:
|
|
130
|
+
cx, cy, hw, hh = rect
|
|
131
|
+
gx = cx - hw - x1h
|
|
132
|
+
g = x1l - cx - hw
|
|
133
|
+
if g > gx:
|
|
134
|
+
gx = g
|
|
135
|
+
if gx < 0.0:
|
|
136
|
+
gx = 0.0
|
|
137
|
+
if gx < t:
|
|
138
|
+
gy = cy - hh - y1h
|
|
139
|
+
g = y1l - cy - hh
|
|
140
|
+
if g > gy:
|
|
141
|
+
gy = g
|
|
142
|
+
if gy < 0.0:
|
|
143
|
+
gy = 0.0
|
|
144
|
+
# gx/gy already clamped to 0: hypot(gx,gy) = the bbox distance; only < t can possibly block the line
|
|
145
|
+
if gy < t and (gx == 0.0 or gy == 0.0 or gx * gx + gy * gy < t2):
|
|
146
|
+
if seg_rect_distance(p1, p2, rect) < t:
|
|
147
|
+
return False
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _dijkstra(nodes, adj, src_idx, dst_idx):
|
|
152
|
+
"""Deterministic Dijkstra: heap key (dist, node_idx); node_idx is unique -> fixed pop order."""
|
|
153
|
+
INF = float("inf")
|
|
154
|
+
dist = [INF] * len(nodes)
|
|
155
|
+
prev = [-1] * len(nodes)
|
|
156
|
+
dist[src_idx] = 0.0
|
|
157
|
+
heap = [(0.0, src_idx)]
|
|
158
|
+
while heap:
|
|
159
|
+
d, u = heappop(heap)
|
|
160
|
+
if d > dist[u] + VIS_EPS:
|
|
161
|
+
continue
|
|
162
|
+
if u == dst_idx:
|
|
163
|
+
break
|
|
164
|
+
# adj[u] generated in ascending node_idx order -> fixed relaxation order; ties not replaced (first come, first kept)
|
|
165
|
+
for v, w in adj[u]:
|
|
166
|
+
nd = d + w
|
|
167
|
+
if nd < dist[v] - VIS_EPS:
|
|
168
|
+
dist[v] = nd
|
|
169
|
+
prev[v] = u
|
|
170
|
+
heappush(heap, (nd, v))
|
|
171
|
+
if dist[dst_idx] >= INF:
|
|
172
|
+
return None
|
|
173
|
+
path, cur = [], dst_idx
|
|
174
|
+
while cur != -1:
|
|
175
|
+
path.append(nodes[cur])
|
|
176
|
+
cur = prev[cur]
|
|
177
|
+
path.reverse()
|
|
178
|
+
return path
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def shortest_path(src, dst, rects, c):
|
|
182
|
+
"""Shortest path on the visibility graph over inflated obstacle corners. Returns None when unreachable.
|
|
183
|
+
|
|
184
|
+
Returns the polyline [src, corner*, ..., dst] (exactly optimal on the graph; optimality bounds in the module docstring).
|
|
185
|
+
"""
|
|
186
|
+
if not rects:
|
|
187
|
+
return [tuple(src), tuple(dst)]
|
|
188
|
+
nodes = _graph_nodes(src, dst, rects, c)
|
|
189
|
+
n = len(nodes)
|
|
190
|
+
# Adjacency list: O(n^2) visibility rechecks, generated in ascending (i,j) order -> deterministic
|
|
191
|
+
adj = [[] for _ in range(n)]
|
|
192
|
+
for i in range(n):
|
|
193
|
+
for j in range(i + 1, n):
|
|
194
|
+
if _visible(nodes[i], nodes[j], rects, c):
|
|
195
|
+
w = math.hypot(nodes[i][0] - nodes[j][0], nodes[i][1] - nodes[j][1])
|
|
196
|
+
adj[i].append((j, w))
|
|
197
|
+
adj[j].append((i, w))
|
|
198
|
+
return _dijkstra(nodes, adj, 0, 1)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _shortcut(path, rects, c, rounds=3):
|
|
202
|
+
"""Visible-chord shortcuts (string pulling): try skipping intermediate points in order, stop on convergence. Deterministic."""
|
|
203
|
+
pts = list(path)
|
|
204
|
+
for _ in range(rounds):
|
|
205
|
+
changed = False
|
|
206
|
+
i = 1
|
|
207
|
+
while i < len(pts) - 1:
|
|
208
|
+
if _visible(pts[i - 1], pts[i + 1], rects, c):
|
|
209
|
+
del pts[i]
|
|
210
|
+
changed = True
|
|
211
|
+
else:
|
|
212
|
+
i += 1
|
|
213
|
+
if not changed:
|
|
214
|
+
break
|
|
215
|
+
return pts
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# -- arc smoothing (clearance-constrained minimum curvature) -------------
|
|
219
|
+
def _fillet(a, q, b, rects, c):
|
|
220
|
+
"""Tangent arc at bend q. Returns None (no arc wanted / no arc possible) or an arc-parameter dict.
|
|
221
|
+
|
|
222
|
+
G1: the arc is tangent-continuous with the adjacent segments at ta/tb; curvature = 1/r is bounded.
|
|
223
|
+
Radii shrink deterministically; clearance is rechecked at every sampled point as >= c - 0.005 (chord height on the
|
|
224
|
+
<=0.05px scale; the delivery-coordinate 16px hard gate keeps the c-16=1.5px inflation margin as a fallback).
|
|
225
|
+
Geometric bound (honest): the tangent arc at a convex corner dips r*(1/cos(theta/2)-1) toward the obstacle side;
|
|
226
|
+
shallow corners admit large arcs while right angles, constrained by the 1.5px margin, admit only a tiny fillet (r <= ~3.6).
|
|
227
|
+
"""
|
|
228
|
+
ux, uy = q[0] - a[0], q[1] - a[1]
|
|
229
|
+
wx, wy = b[0] - q[0], b[1] - q[1]
|
|
230
|
+
lu, lw = math.hypot(ux, uy), math.hypot(wx, wy)
|
|
231
|
+
if lu < VIS_EPS or lw < VIS_EPS:
|
|
232
|
+
return None
|
|
233
|
+
ux, uy, wx, wy = ux / lu, uy / lu, wx / lw, wy / lw
|
|
234
|
+
# Deflection angle theta = angle between u and w (theta=0 straight continuation; theta=pi U-turn)
|
|
235
|
+
theta = math.acos(max(-1.0, min(1.0, ux * wx + uy * wy)))
|
|
236
|
+
if theta < DEFLECT_SKIP or theta > math.pi - DEFLECT_SKIP:
|
|
237
|
+
return None
|
|
238
|
+
cos_half = math.cos(theta / 2)
|
|
239
|
+
tan_half = math.tan(theta / 2)
|
|
240
|
+
# Tangent-segment length t = r*tan(theta/2), center distance d = r/cos(theta/2) (the center sits inside the bend wedge,
|
|
241
|
+
# tangent to both adjacent segment lines). Radius upper bound = the smaller of the tangent-segment clamp and the absolute cap.
|
|
242
|
+
tau_max = FILLET_TAU_MAX * min(lu, lw)
|
|
243
|
+
r0 = min(FILLET_R_MAX, tau_max / tan_half)
|
|
244
|
+
for f in FILLET_R_SHRINK:
|
|
245
|
+
r = r0 * f
|
|
246
|
+
if r < 1.5:
|
|
247
|
+
break
|
|
248
|
+
t = min(tau_max, r * tan_half)
|
|
249
|
+
r = t / tan_half
|
|
250
|
+
if r < 1.5:
|
|
251
|
+
continue
|
|
252
|
+
# Interior angle-bisector direction (the sum of the unit vectors -u and w)
|
|
253
|
+
bx, by = -ux + wx, -uy + wy
|
|
254
|
+
lb = math.hypot(bx, by)
|
|
255
|
+
if lb < VIS_EPS:
|
|
256
|
+
continue
|
|
257
|
+
bx, by = bx / lb, by / lb
|
|
258
|
+
d = r / cos_half
|
|
259
|
+
center = (q[0] + bx * d, q[1] + by * d)
|
|
260
|
+
ta = (q[0] - ux * t, q[1] - uy * t)
|
|
261
|
+
tb = (q[0] + wx * t, q[1] + wy * t)
|
|
262
|
+
samples = _arc_samples(center, r, ta, tb)
|
|
263
|
+
# Defense in depth: tangent-perpendicularity self-check (the ta radius perpendicular to the incoming segment, the tb radius to the outgoing segment -- the analytic G1 condition)
|
|
264
|
+
rada = (ta[0] - center[0], ta[1] - center[1])
|
|
265
|
+
radb = (tb[0] - center[0], tb[1] - center[1])
|
|
266
|
+
if (abs(rada[0] * ux + rada[1] * uy) > 1e-6 * r or
|
|
267
|
+
abs(radb[0] * wx + radb[1] * wy) > 1e-6 * r):
|
|
268
|
+
continue
|
|
269
|
+
if all(pt_rect_distance(px, py, rect) >= c - 0.005
|
|
270
|
+
for (px, py) in samples for rect in rects):
|
|
271
|
+
cross = ((ta[0] - center[0]) * (tb[1] - center[1]) -
|
|
272
|
+
(ta[1] - center[1]) * (tb[0] - center[0]))
|
|
273
|
+
return {"ta": list(ta), "tb": list(tb), "center": list(center),
|
|
274
|
+
"r": round(r, 2), "sweep": 1 if cross > 0 else 0}
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _arc_samples(center, r, ta, tb, n=ARC_SAMPLES):
|
|
279
|
+
"""n+1 sampled points of the arc ta->tb (along the short arc, angular interpolation)."""
|
|
280
|
+
a0 = math.atan2(ta[1] - center[1], ta[0] - center[0])
|
|
281
|
+
a1 = math.atan2(tb[1] - center[1], tb[0] - center[0])
|
|
282
|
+
da = a1 - a0
|
|
283
|
+
while da > math.pi:
|
|
284
|
+
da -= 2 * math.pi
|
|
285
|
+
while da < -math.pi:
|
|
286
|
+
da += 2 * math.pi
|
|
287
|
+
return [(center[0] + r * math.cos(a0 + da * k / n),
|
|
288
|
+
center[1] + r * math.sin(a0 + da * k / n)) for k in range(n + 1)]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def smooth_corners(pts, rects, c):
|
|
292
|
+
"""Arc smoothing of bends. Returns (fillets, corner_pts).
|
|
293
|
+
|
|
294
|
+
fillets[i] = {"i": index of the corner in corner_pts, ta/tb/center/r/sweep}.
|
|
295
|
+
Corners that cannot take an arc stay sharp (clearance is still >= c by construction of the graph).
|
|
296
|
+
"""
|
|
297
|
+
fillets = []
|
|
298
|
+
for i in range(1, len(pts) - 1):
|
|
299
|
+
f = _fillet(pts[i - 1], pts[i], pts[i + 1], rects, c)
|
|
300
|
+
if f:
|
|
301
|
+
f["i"] = i
|
|
302
|
+
fillets.append(f)
|
|
303
|
+
return fillets, list(pts)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def dense_geometry(corner_pts, fillets, arc_n=ARC_SAMPLES):
|
|
307
|
+
"""Polyline + arcs -> densely sampled point sequence (the geometric ground truth for clearance/crossing rechecks)."""
|
|
308
|
+
fmap = {f["i"]: f for f in fillets}
|
|
309
|
+
out = [tuple(corner_pts[0])]
|
|
310
|
+
for i in range(1, len(corner_pts)):
|
|
311
|
+
q = tuple(corner_pts[i])
|
|
312
|
+
f = fmap.get(i)
|
|
313
|
+
if f:
|
|
314
|
+
out.extend(tuple(p) for p in _arc_samples(f["center"], f["r"],
|
|
315
|
+
f["ta"], f["tb"], arc_n)[1:])
|
|
316
|
+
else:
|
|
317
|
+
out.append(q)
|
|
318
|
+
return out
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def route_svg_d(corner_pts, fillets, nd=1):
|
|
322
|
+
"""Delivery coordinates -> SVG path d string: straight segments L + arc segments A (exact rendering, not a polyline approximation)."""
|
|
323
|
+
fmap = {f["i"]: f for f in fillets}
|
|
324
|
+
r6 = lambda v: round(float(v), nd)
|
|
325
|
+
parts = [f"M {r6(corner_pts[0][0])} {r6(corner_pts[0][1])}"]
|
|
326
|
+
for i in range(1, len(corner_pts)):
|
|
327
|
+
q = corner_pts[i]
|
|
328
|
+
f = fmap.get(i)
|
|
329
|
+
if f:
|
|
330
|
+
# t_cf6d6978 #15: a corner with a fillet is replaced by ta->arc->tb and must not be drawn back to
|
|
331
|
+
# the corner itself (an unconditional L q once caused tb->q->ta_next zigzag glitches).
|
|
332
|
+
parts.append(f"L {r6(f['ta'][0])} {r6(f['ta'][1])}")
|
|
333
|
+
parts.append(f"A {r6(f['r'])} {r6(f['r'])} 0 0 {f['sweep']} "
|
|
334
|
+
f"{r6(f['tb'][0])} {r6(f['tb'][1])}")
|
|
335
|
+
else:
|
|
336
|
+
parts.append(f"L {r6(q[0])} {r6(q[1])}")
|
|
337
|
+
return " ".join(parts)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def plan_route(src, dst, rects, c):
|
|
341
|
+
"""High-level entry: the obstacle-avoiding route from src to dst (with a deterministic cache -- a pure function, same input same output).
|
|
342
|
+
|
|
343
|
+
t_7fe64f08 cache hits return a **deep copy**: callers reuse the dense list in place as delivery
|
|
344
|
+
points (midband_reanchor/fan_spread mutate anchors in place); returning the cached object directly
|
|
345
|
+
would poison the global cache with those mutations and break cross-run determinism (measured: vg route drift on n20).
|
|
346
|
+
"""
|
|
347
|
+
key = ((round(float(src[0]), 3), round(float(src[1]), 3)),
|
|
348
|
+
(round(float(dst[0]), 3), round(float(dst[1]), 3)),
|
|
349
|
+
tuple((round(r[0], 3), round(r[1], 3), round(r[2], 3), round(r[3], 3))
|
|
350
|
+
for r in rects), round(float(c), 3))
|
|
351
|
+
hit = _ROUTE_CACHE.get(key)
|
|
352
|
+
if hit is not None:
|
|
353
|
+
return {"corner_pts": [list(p) for p in hit["corner_pts"]],
|
|
354
|
+
"fillets": [dict(f) for f in hit["fillets"]],
|
|
355
|
+
"dense": [tuple(p) for p in hit["dense"]],
|
|
356
|
+
"path_d": hit["path_d"]}
|
|
357
|
+
result = _plan_route(src, dst, rects, c)
|
|
358
|
+
if len(_ROUTE_CACHE) > 8192:
|
|
359
|
+
_ROUTE_CACHE.clear()
|
|
360
|
+
_ROUTE_CACHE[key] = result
|
|
361
|
+
if result is None:
|
|
362
|
+
return None
|
|
363
|
+
return {"corner_pts": [list(p) for p in result["corner_pts"]],
|
|
364
|
+
"fillets": [dict(f) for f in result["fillets"]],
|
|
365
|
+
"dense": [tuple(p) for p in result["dense"]],
|
|
366
|
+
"path_d": result["path_d"]}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
_ROUTE_CACHE = {}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _plan_route(src, dst, rects, c):
|
|
373
|
+
"""plan_route implementation (no cache). Returns a dict, or None when unreachable."""
|
|
374
|
+
src, dst = tuple(src), tuple(dst)
|
|
375
|
+
path = shortest_path(src, dst, rects, c)
|
|
376
|
+
if path is None:
|
|
377
|
+
return None
|
|
378
|
+
path = _shortcut(path, rects, c)
|
|
379
|
+
fillets, corners = smooth_corners(path, rects, c)
|
|
380
|
+
dense = dense_geometry(corners, fillets)
|
|
381
|
+
return {"corner_pts": corners, "fillets": fillets,
|
|
382
|
+
"dense": dense, "path_d": route_svg_d(corners, fillets)}
|