forgekernel 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.
- forgekernel/__init__.py +7 -0
- forgekernel/brep.py +624 -0
- forgekernel/bsolid.py +295 -0
- forgekernel/csg.py +158 -0
- forgekernel/curve.py +166 -0
- forgekernel/exact.py +121 -0
- forgekernel/interval.py +141 -0
- forgekernel/io.py +56 -0
- forgekernel/kernel.py +93 -0
- forgekernel/loft.py +185 -0
- forgekernel/nurbs.py +481 -0
- forgekernel/profile2d.py +228 -0
- forgekernel/quadric.py +979 -0
- forgekernel/ssi.py +524 -0
- forgekernel/stepio.py +460 -0
- forgekernel/surd.py +104 -0
- forgekernel/surfacing.py +341 -0
- forgekernel/tess.py +74 -0
- forgekernel/trim.py +193 -0
- forgekernel-0.1.0.dist-info/METADATA +82 -0
- forgekernel-0.1.0.dist-info/RECORD +24 -0
- forgekernel-0.1.0.dist-info/WHEEL +5 -0
- forgekernel-0.1.0.dist-info/licenses/LICENSE +201 -0
- forgekernel-0.1.0.dist-info/top_level.txt +1 -0
forgekernel/ssi.py
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
"""K3.3 — surface–surface intersection with complete branch detection.
|
|
2
|
+
|
|
3
|
+
The crown jewel of K3 (coverage plan): where float kernels genuinely
|
|
4
|
+
miss branches, this finds them all — provably, at a stated resolution.
|
|
5
|
+
|
|
6
|
+
Structure (all geometry exact rational; Bézier patches, weights 1):
|
|
7
|
+
|
|
8
|
+
1. **Subdivision branch detection.** De Casteljau subdivision of both
|
|
9
|
+
patches (exact — convex combinations only) with bounding-box pruning
|
|
10
|
+
via the convex-hull property: a Bézier patch lies inside the bbox of
|
|
11
|
+
its control net, so a pair whose control-net boxes are disjoint
|
|
12
|
+
*provably* does not intersect. Pruning therefore never discards a
|
|
13
|
+
real intersection: at subdivision depth ``d`` every intersection
|
|
14
|
+
branch is guaranteed to be hit by at least one surviving leaf pair —
|
|
15
|
+
completeness at resolution 2^-d, stated, not hoped.
|
|
16
|
+
|
|
17
|
+
2. **Branch counting.** Surviving leaves are cells in A's parameter
|
|
18
|
+
square; connected components (8-neighbour union-find) = branches.
|
|
19
|
+
|
|
20
|
+
3. **Certified refinement.** Per cell, a float Newton solve lands a
|
|
21
|
+
point on the intersection; the *certificate* is exact: the rational
|
|
22
|
+
residual |A(u,v) − B(s,t)|² is computed in ℚ and must be below tol².
|
|
23
|
+
Points that fail to certify are dropped and reported honestly.
|
|
24
|
+
|
|
25
|
+
The empty case is a genuine differentiator: bbox-disjointness at the
|
|
26
|
+
top level *proves* non-intersection — a float sampler can only fail to
|
|
27
|
+
find what it never proves absent.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from fractions import Fraction
|
|
33
|
+
|
|
34
|
+
F = Fraction
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BezierPatch:
|
|
38
|
+
"""A Bézier patch: (p+1)×(q+1) exact rational control net over a
|
|
39
|
+
parameter box [u0,u1]×[v0,v1] of the original surface.
|
|
40
|
+
|
|
41
|
+
Net entries are 3-tuples (polynomial) or homogeneous 4-tuples
|
|
42
|
+
(wx, wy, wz, w) for a rational patch. The convex-hull property that
|
|
43
|
+
bbox pruning relies on holds for rational patches with POSITIVE
|
|
44
|
+
weights over the *cartesian* control points — enforced here."""
|
|
45
|
+
|
|
46
|
+
__slots__ = ("net", "u0", "u1", "v0", "v1", "dim")
|
|
47
|
+
|
|
48
|
+
def __init__(self, net, u0=F(0), u1=F(1), v0=F(0), v1=F(1)) -> None:
|
|
49
|
+
self.net = [[tuple(F(c) for c in pt) for pt in row] for row in net]
|
|
50
|
+
self.dim = len(self.net[0][0])
|
|
51
|
+
if self.dim == 4:
|
|
52
|
+
for row in self.net:
|
|
53
|
+
for pt in row:
|
|
54
|
+
if pt[3] <= 0:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
"rational patch: convex-hull pruning needs "
|
|
57
|
+
"positive weights (K3.7 for sign-varying)")
|
|
58
|
+
self.u0, self.u1, self.v0, self.v1 = F(u0), F(u1), F(v0), F(v1)
|
|
59
|
+
|
|
60
|
+
def bbox(self):
|
|
61
|
+
"""Cartesian control-net box — contains the patch (convex hull
|
|
62
|
+
property; for rational nets, over points x/w with w>0)."""
|
|
63
|
+
if self.dim == 3:
|
|
64
|
+
xs = [pt for row in self.net for pt in row]
|
|
65
|
+
else:
|
|
66
|
+
xs = [(pt[0] / pt[3], pt[1] / pt[3], pt[2] / pt[3])
|
|
67
|
+
for row in self.net for pt in row]
|
|
68
|
+
lo = tuple(min(p[c] for p in xs) for c in range(3))
|
|
69
|
+
hi = tuple(max(p[c] for p in xs) for c in range(3))
|
|
70
|
+
return lo, hi
|
|
71
|
+
|
|
72
|
+
def _split_rows(self, rows):
|
|
73
|
+
"""De Casteljau at 1/2 along a list of control rows; exact.
|
|
74
|
+
Dimension-agnostic: homogeneous 4-vectors subdivide identically."""
|
|
75
|
+
dim = self.dim
|
|
76
|
+
left, right = [], []
|
|
77
|
+
for row in rows:
|
|
78
|
+
pts = [list(p) for p in row]
|
|
79
|
+
lo = [tuple(pts[0])]
|
|
80
|
+
hi = [tuple(pts[-1])]
|
|
81
|
+
n = len(pts)
|
|
82
|
+
for r in range(1, n):
|
|
83
|
+
for i in range(n - r):
|
|
84
|
+
pts[i] = [(pts[i][c] + pts[i + 1][c]) / 2 for c in range(dim)]
|
|
85
|
+
lo.append(tuple(pts[0]))
|
|
86
|
+
hi.append(tuple(pts[n - r - 1]))
|
|
87
|
+
left.append(lo)
|
|
88
|
+
right.append(list(reversed(hi)))
|
|
89
|
+
return left, right
|
|
90
|
+
|
|
91
|
+
def split_u(self):
|
|
92
|
+
"""Split at the u-midpoint (rows of the net run along u)."""
|
|
93
|
+
cols = list(map(list, zip(*self.net))) # transpose: u-rows
|
|
94
|
+
l, r = self._split_rows(cols)
|
|
95
|
+
um = (self.u0 + self.u1) / 2
|
|
96
|
+
return (BezierPatch(list(map(list, zip(*l))), self.u0, um, self.v0, self.v1),
|
|
97
|
+
BezierPatch(list(map(list, zip(*r))), um, self.u1, self.v0, self.v1))
|
|
98
|
+
|
|
99
|
+
def split_v(self):
|
|
100
|
+
l, r = self._split_rows(self.net)
|
|
101
|
+
vm = (self.v0 + self.v1) / 2
|
|
102
|
+
return (BezierPatch(l, self.u0, self.u1, self.v0, vm),
|
|
103
|
+
BezierPatch(r, self.u0, self.u1, vm, self.v1))
|
|
104
|
+
|
|
105
|
+
def split4(self):
|
|
106
|
+
a, b = self.split_u()
|
|
107
|
+
return a.split_v() + b.split_v()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _boxes_overlap(a, b) -> bool:
|
|
111
|
+
(alo, ahi), (blo, bhi) = a, b
|
|
112
|
+
return all(alo[c] <= bhi[c] and blo[c] <= ahi[c] for c in range(3))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def ssi_branches(A: BezierPatch, B: BezierPatch, depth: int = 5):
|
|
116
|
+
"""Complete branch detection at resolution 2^-depth.
|
|
117
|
+
|
|
118
|
+
Returns ``(branches, leaf_pairs)`` where ``branches`` is a list of
|
|
119
|
+
cell-sets on A's parameter square (each a connected component = one
|
|
120
|
+
intersection branch) and ``leaf_pairs`` the surviving (cellA, cellB)
|
|
121
|
+
parameter boxes. Empty list = *certified* non-intersection."""
|
|
122
|
+
pairs = [(A, B)]
|
|
123
|
+
for _ in range(depth):
|
|
124
|
+
nxt = []
|
|
125
|
+
for a, b in pairs:
|
|
126
|
+
if not _boxes_overlap(a.bbox(), b.bbox()):
|
|
127
|
+
continue # proven disjoint
|
|
128
|
+
for sa in a.split4():
|
|
129
|
+
ba = sa.bbox()
|
|
130
|
+
for sb in b.split4():
|
|
131
|
+
if _boxes_overlap(ba, sb.bbox()):
|
|
132
|
+
nxt.append((sa, sb))
|
|
133
|
+
pairs = nxt
|
|
134
|
+
if not pairs:
|
|
135
|
+
return [], [] # certified empty
|
|
136
|
+
|
|
137
|
+
# cluster surviving A-cells into branches (8-neighbour union-find)
|
|
138
|
+
cells = {}
|
|
139
|
+
for a, _ in pairs:
|
|
140
|
+
cells.setdefault((a.u0, a.u1, a.v0, a.v1), []).append(a)
|
|
141
|
+
keys = list(cells)
|
|
142
|
+
parent = list(range(len(keys)))
|
|
143
|
+
|
|
144
|
+
def find(i):
|
|
145
|
+
while parent[i] != i:
|
|
146
|
+
parent[i] = parent[parent[i]]
|
|
147
|
+
i = parent[i]
|
|
148
|
+
return i
|
|
149
|
+
|
|
150
|
+
def touch(k1, k2):
|
|
151
|
+
return (k1[0] <= k2[1] and k2[0] <= k1[1]
|
|
152
|
+
and k1[2] <= k2[3] and k2[2] <= k1[3])
|
|
153
|
+
|
|
154
|
+
for i in range(len(keys)):
|
|
155
|
+
for j in range(i + 1, len(keys)):
|
|
156
|
+
if touch(keys[i], keys[j]):
|
|
157
|
+
ri, rj = find(i), find(j)
|
|
158
|
+
if ri != rj:
|
|
159
|
+
parent[ri] = rj
|
|
160
|
+
groups = {}
|
|
161
|
+
for i, k in enumerate(keys):
|
|
162
|
+
groups.setdefault(find(i), []).append(k)
|
|
163
|
+
return list(groups.values()), pairs
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# -- certified point refinement ------------------------------------------------
|
|
167
|
+
|
|
168
|
+
def _eval_patch(net, u, v):
|
|
169
|
+
"""De Casteljau evaluation of a Bézier net at exact (u, v) → ℚ³."""
|
|
170
|
+
rows = [_dc1(row, v) for row in net]
|
|
171
|
+
return _dc1(rows, u)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _dc1(pts, t):
|
|
175
|
+
pts = [tuple(p) for p in pts]
|
|
176
|
+
n = len(pts)
|
|
177
|
+
for r in range(1, n):
|
|
178
|
+
pts = [tuple((1 - t) * pts[i][c] + t * pts[i + 1][c] for c in range(3))
|
|
179
|
+
for i in range(n - r)]
|
|
180
|
+
return pts[0]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _partials_patch(net, u, v):
|
|
184
|
+
"""(S, S_u, S_v) of a Bézier net, exact (hodograph differences)."""
|
|
185
|
+
rows = [_dc1(row, v) for row in net]
|
|
186
|
+
S = _dc1(rows, u)
|
|
187
|
+
p = len(rows) - 1
|
|
188
|
+
du = [tuple(F(p) * (rows[i + 1][c] - rows[i][c]) for c in range(3))
|
|
189
|
+
for i in range(p)]
|
|
190
|
+
S_u = _dc1(du, u) if du else (F(0),) * 3
|
|
191
|
+
q = len(net[0]) - 1
|
|
192
|
+
dv_rows = []
|
|
193
|
+
for row in net:
|
|
194
|
+
dv_rows.append([tuple(F(q) * (row[j + 1][c] - row[j][c]) for c in range(3))
|
|
195
|
+
for j in range(q)])
|
|
196
|
+
cols = [_dc1(r, v) for r in dv_rows]
|
|
197
|
+
S_v = _dc1(cols, u) if cols and cols[0] else (F(0),) * 3
|
|
198
|
+
return S, S_u, S_v
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def refine_point(Anet, Bnet, u, v, s, t, iters: int = 12):
|
|
202
|
+
"""Float Newton on A(u,v) − B(s,t) = 0 (3 eqs, 4 unknowns; smallest-
|
|
203
|
+
norm step via normal equations), then an EXACT residual certificate.
|
|
204
|
+
|
|
205
|
+
Returns (u, v, s, t, ok, res2) — ``ok`` iff the exact rational
|
|
206
|
+
|A−B|² is below 1e-20 (distance < 1e-10)."""
|
|
207
|
+
import itertools
|
|
208
|
+
|
|
209
|
+
uf, vf, sf, tf = (float(x) for x in (u, v, s, t))
|
|
210
|
+
for _ in range(iters):
|
|
211
|
+
ur, vr = F(uf).limit_denominator(10 ** 12), F(vf).limit_denominator(10 ** 12)
|
|
212
|
+
sr, tr = F(sf).limit_denominator(10 ** 12), F(tf).limit_denominator(10 ** 12)
|
|
213
|
+
Sa, Au, Av = _partials_patch(Anet, ur, vr)
|
|
214
|
+
Sb, Bu, Bv = _partials_patch(Bnet, sr, tr)
|
|
215
|
+
r = [float(Sa[c] - Sb[c]) for c in range(3)]
|
|
216
|
+
if max(abs(x) for x in r) < 1e-14:
|
|
217
|
+
break
|
|
218
|
+
# J is 3x4: [Au Av -Bu -Bv]; solve J J^T y = -r, step = J^T y
|
|
219
|
+
J = [[float(Au[c]), float(Av[c]), -float(Bu[c]), -float(Bv[c])]
|
|
220
|
+
for c in range(3)]
|
|
221
|
+
JJT = [[sum(J[i][k] * J[j][k] for k in range(4)) for j in range(3)]
|
|
222
|
+
for i in range(3)]
|
|
223
|
+
y = _solve3f(JJT, [-x for x in r])
|
|
224
|
+
if y is None:
|
|
225
|
+
break
|
|
226
|
+
step = [sum(J[i][k] * y[i] for i in range(3)) for k in range(4)]
|
|
227
|
+
uf, vf, sf, tf = uf + step[0], vf + step[1], sf + step[2], tf + step[3]
|
|
228
|
+
uf = min(1.0, max(0.0, uf)); vf = min(1.0, max(0.0, vf))
|
|
229
|
+
sf = min(1.0, max(0.0, sf)); tf = min(1.0, max(0.0, tf))
|
|
230
|
+
ur, vr = F(uf).limit_denominator(10 ** 12), F(vf).limit_denominator(10 ** 12)
|
|
231
|
+
sr, tr = F(sf).limit_denominator(10 ** 12), F(tf).limit_denominator(10 ** 12)
|
|
232
|
+
pa = _eval_patch(Anet, ur, vr)
|
|
233
|
+
pb = _eval_patch(Bnet, sr, tr)
|
|
234
|
+
res2 = sum((pa[c] - pb[c]) ** 2 for c in range(3)) # EXACT rational
|
|
235
|
+
return ur, vr, sr, tr, res2 < F(1, 10 ** 20), res2
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _solve3f(M, b):
|
|
239
|
+
import copy
|
|
240
|
+
a = [row[:] + [b[i]] for i, row in enumerate(copy.deepcopy(M))]
|
|
241
|
+
for col in range(3):
|
|
242
|
+
piv = max(range(col, 3), key=lambda r: abs(a[r][col]))
|
|
243
|
+
if abs(a[piv][col]) < 1e-30:
|
|
244
|
+
return None
|
|
245
|
+
a[col], a[piv] = a[piv], a[col]
|
|
246
|
+
for r in range(3):
|
|
247
|
+
if r != col:
|
|
248
|
+
f = a[r][col] / a[col][col]
|
|
249
|
+
a[r] = [a[r][k] - f * a[col][k] for k in range(4)]
|
|
250
|
+
return [a[i][3] / a[i][i] for i in range(3)]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def ssi(A: BezierPatch, B: BezierPatch, depth: int = 5):
|
|
254
|
+
"""Full SSI: branches + one certified point per surviving cell.
|
|
255
|
+
|
|
256
|
+
Returns {"branches": n, "points": [(u,v,s,t)...] certified,
|
|
257
|
+
"uncertified": count, "empty_certified": bool}."""
|
|
258
|
+
branches, pairs = ssi_branches(A, B, depth)
|
|
259
|
+
if not pairs:
|
|
260
|
+
return {"branches": 0, "points": [], "uncertified": 0,
|
|
261
|
+
"empty_certified": True}
|
|
262
|
+
pts, bad = [], 0
|
|
263
|
+
seen = set()
|
|
264
|
+
for a, b in pairs:
|
|
265
|
+
key = (a.u0, a.v0)
|
|
266
|
+
if key in seen:
|
|
267
|
+
continue
|
|
268
|
+
seen.add(key)
|
|
269
|
+
um, vm = (a.u0 + a.u1) / 2, (a.v0 + a.v1) / 2
|
|
270
|
+
sm, tm = (b.u0 + b.u1) / 2, (b.v0 + b.v1) / 2
|
|
271
|
+
u, v, s, t, ok, _ = refine_point(A.net, B.net, um, vm, sm, tm)
|
|
272
|
+
if ok:
|
|
273
|
+
pts.append((u, v, s, t))
|
|
274
|
+
else:
|
|
275
|
+
bad += 1
|
|
276
|
+
return {"branches": len(branches), "points": pts, "uncertified": bad,
|
|
277
|
+
"empty_certified": False}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# -- K3.5: SSI over B-spline surfaces + ordered polylines ---------------------
|
|
281
|
+
|
|
282
|
+
def _cluster(keys):
|
|
283
|
+
"""Union-find over parameter boxes (closed-touch adjacency)."""
|
|
284
|
+
parent = list(range(len(keys)))
|
|
285
|
+
|
|
286
|
+
def find(i):
|
|
287
|
+
while parent[i] != i:
|
|
288
|
+
parent[i] = parent[parent[i]]
|
|
289
|
+
i = parent[i]
|
|
290
|
+
return i
|
|
291
|
+
|
|
292
|
+
def touch(k1, k2):
|
|
293
|
+
return (k1[0] <= k2[1] and k2[0] <= k1[1]
|
|
294
|
+
and k1[2] <= k2[3] and k2[2] <= k1[3])
|
|
295
|
+
|
|
296
|
+
for i in range(len(keys)):
|
|
297
|
+
for j in range(i + 1, len(keys)):
|
|
298
|
+
if touch(keys[i], keys[j]):
|
|
299
|
+
ri, rj = find(i), find(j)
|
|
300
|
+
if ri != rj:
|
|
301
|
+
parent[ri] = rj
|
|
302
|
+
groups = {}
|
|
303
|
+
for i, k in enumerate(keys):
|
|
304
|
+
groups.setdefault(find(i), []).append(k)
|
|
305
|
+
return list(groups.values())
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _rstr(x) -> str:
|
|
309
|
+
f = F(x)
|
|
310
|
+
return f"{f.numerator}/{f.denominator}"
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _net_str(net):
|
|
314
|
+
return [[[_rstr(c) for c in pt] for pt in row] for row in net]
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _ssi_cells(A, B, depth: int, use_rust: bool | None):
|
|
318
|
+
"""Shared detection for the surface SSI entry points: exact Bézier
|
|
319
|
+
extraction + pairwise subdivision detection, deduplicated into a
|
|
320
|
+
surviving ``{A-cell: B-cell}`` map, plus the branch clustering of the
|
|
321
|
+
A-cells (connected components in A's parameter domain). Returns
|
|
322
|
+
``(cells, branches)`` — ``({}, [])`` means certified-empty.
|
|
323
|
+
|
|
324
|
+
Detection — the hot loop — runs in the Rust port (``ssi_pairs``,
|
|
325
|
+
oracle-verified bit-identical) when the extension is built; the Python
|
|
326
|
+
path is the fallback and stays the executable spec."""
|
|
327
|
+
from forgekernel.nurbs import bezier_patches
|
|
328
|
+
|
|
329
|
+
rs = None
|
|
330
|
+
if use_rust is not False:
|
|
331
|
+
try:
|
|
332
|
+
import forgekernel_rs as rs
|
|
333
|
+
except ImportError:
|
|
334
|
+
if use_rust is True:
|
|
335
|
+
raise
|
|
336
|
+
rs = None
|
|
337
|
+
|
|
338
|
+
pa = [(u0, u1, v0, v1, net) for u0, u1, v0, v1, net in bezier_patches(A)]
|
|
339
|
+
pb = [(u0, u1, v0, v1, net) for u0, u1, v0, v1, net in bezier_patches(B)]
|
|
340
|
+
# (a_box, b_box) surviving leaf pairs, as Fraction 4-tuples
|
|
341
|
+
boxes: list[tuple[tuple, tuple]] = []
|
|
342
|
+
for au0, au1, av0, av1, na in pa:
|
|
343
|
+
patch_a = BezierPatch(na, au0, au1, av0, av1)
|
|
344
|
+
ba = patch_a.bbox()
|
|
345
|
+
for bu0, bu1, bv0, bv1, nb in pb:
|
|
346
|
+
patch_b = BezierPatch(nb, bu0, bu1, bv0, bv1)
|
|
347
|
+
if not _boxes_overlap(ba, patch_b.bbox()):
|
|
348
|
+
continue # proven disjoint
|
|
349
|
+
if rs is not None:
|
|
350
|
+
rows = rs.ssi_pairs(
|
|
351
|
+
_net_str(na), _net_str(nb), depth,
|
|
352
|
+
[_rstr(au0), _rstr(au1), _rstr(av0), _rstr(av1)],
|
|
353
|
+
[_rstr(bu0), _rstr(bu1), _rstr(bv0), _rstr(bv1)])
|
|
354
|
+
for row in rows:
|
|
355
|
+
vals = [F(x) for x in row]
|
|
356
|
+
boxes.append((tuple(vals[:4]), tuple(vals[4:])))
|
|
357
|
+
else:
|
|
358
|
+
_, pairs = ssi_branches(patch_a, patch_b, depth)
|
|
359
|
+
boxes.extend(((p.u0, p.u1, p.v0, p.v1),
|
|
360
|
+
(q.u0, q.u1, q.v0, q.v1)) for p, q in pairs)
|
|
361
|
+
if not boxes:
|
|
362
|
+
return {}, []
|
|
363
|
+
cells: dict = {}
|
|
364
|
+
for abox, bbox_ in boxes:
|
|
365
|
+
cells.setdefault(abox, bbox_)
|
|
366
|
+
branches = _cluster(list(cells))
|
|
367
|
+
return cells, branches
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def ssi_surfaces(A, B, depth: int = 4, use_rust: bool | None = None):
|
|
371
|
+
"""SSI between two B-spline surfaces: certified points in global
|
|
372
|
+
parameters plus the branch count. See :func:`ssi_curves` for the same
|
|
373
|
+
points chained into ordered per-branch polylines."""
|
|
374
|
+
cells, branches = _ssi_cells(A, B, depth, use_rust)
|
|
375
|
+
if not cells:
|
|
376
|
+
return {"branches": 0, "points": [], "uncertified": 0,
|
|
377
|
+
"empty_certified": True}
|
|
378
|
+
pts, bad = [], 0
|
|
379
|
+
for abox, bbox_ in cells.items():
|
|
380
|
+
um, vm = (abox[0] + abox[1]) / 2, (abox[2] + abox[3]) / 2
|
|
381
|
+
sm, tm = (bbox_[0] + bbox_[1]) / 2, (bbox_[2] + bbox_[3]) / 2
|
|
382
|
+
u, v, s, t, ok, _ = _refine_global(A, B, um, vm, sm, tm)
|
|
383
|
+
if ok:
|
|
384
|
+
pts.append((u, v, s, t))
|
|
385
|
+
else:
|
|
386
|
+
bad += 1
|
|
387
|
+
return {"branches": len(branches), "points": pts, "uncertified": bad,
|
|
388
|
+
"empty_certified": False}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _order_branch(points):
|
|
392
|
+
"""Chain a branch's certified points into an ordered polyline in A's
|
|
393
|
+
(u, v) parameter space by greedy nearest-neighbour, started from an
|
|
394
|
+
endpoint via the double-sweep diameter heuristic (furthest point from
|
|
395
|
+
an arbitrary sample is an endpoint of an open arc). Reports whether the
|
|
396
|
+
branch closes on itself. Float ordering over exact points — a
|
|
397
|
+
render/report artifact; the points themselves are the certified data."""
|
|
398
|
+
n = len(points)
|
|
399
|
+
if n <= 2:
|
|
400
|
+
return list(points), False
|
|
401
|
+
uv = [(float(p[0]), float(p[1])) for p in points]
|
|
402
|
+
|
|
403
|
+
def d2(i, j):
|
|
404
|
+
return (uv[i][0] - uv[j][0]) ** 2 + (uv[i][1] - uv[j][1]) ** 2
|
|
405
|
+
|
|
406
|
+
start = max(range(n), key=lambda i: d2(i, 0)) # an extreme end
|
|
407
|
+
todo = set(range(n))
|
|
408
|
+
todo.discard(start)
|
|
409
|
+
order = [start]
|
|
410
|
+
gaps = []
|
|
411
|
+
while todo:
|
|
412
|
+
last = order[-1]
|
|
413
|
+
nxt = min(todo, key=lambda i: d2(i, last))
|
|
414
|
+
gaps.append(d2(nxt, last) ** 0.5)
|
|
415
|
+
order.append(nxt)
|
|
416
|
+
todo.discard(nxt)
|
|
417
|
+
ordered = [points[i] for i in order]
|
|
418
|
+
# Closure needs enough samples to be decidable: with only 3 points the
|
|
419
|
+
# median-of-two collapses to the LARGER gap, and the triangle inequality
|
|
420
|
+
# then forces wrap ≤ g1+g2 ≤ 2·median for ANY three points — so the test
|
|
421
|
+
# would call every 3-point arc closed. A genuine median needs ≥3 gaps
|
|
422
|
+
# (n ≥ 4); below that, report open (closure is undecidable from the
|
|
423
|
+
# samples) rather than let a float heuristic mis-decide the topology.
|
|
424
|
+
if n < 4:
|
|
425
|
+
return ordered, False
|
|
426
|
+
wrap = d2(order[0], order[-1]) ** 0.5
|
|
427
|
+
med = sorted(gaps)[len(gaps) // 2]
|
|
428
|
+
closed = med > 0 and wrap <= 2.0 * med
|
|
429
|
+
return ordered, closed
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def ssi_curves(A, B, depth: int = 4, use_rust: bool | None = None):
|
|
433
|
+
"""Ordered SSI output: the certified intersection points chained into
|
|
434
|
+
one parameter-space polyline **per branch** (a branch = a connected
|
|
435
|
+
component of surviving cells in A's parameter domain). Within a branch
|
|
436
|
+
the points are ordered along the curve; a branch that returns to its
|
|
437
|
+
start is flagged ``closed``.
|
|
438
|
+
|
|
439
|
+
Returns ``{"curves": [{"points": [(u,v,s,t)…] (exact ℚ),
|
|
440
|
+
"xyz": [(x,y,z)…] (float, for render), "closed": bool}, …],
|
|
441
|
+
"uncertified": n, "empty_certified": bool}``. The points are the
|
|
442
|
+
certified objects (residual < 1e-20); the ordering is a float
|
|
443
|
+
convenience layered on top."""
|
|
444
|
+
cells, branches = _ssi_cells(A, B, depth, use_rust)
|
|
445
|
+
if not cells:
|
|
446
|
+
return {"curves": [], "uncertified": 0, "empty_certified": True}
|
|
447
|
+
branch_of = {abox: bi for bi, group in enumerate(branches) for abox in group}
|
|
448
|
+
per_branch: dict[int, list] = {bi: [] for bi in range(len(branches))}
|
|
449
|
+
bad = 0
|
|
450
|
+
for abox, bbox_ in cells.items():
|
|
451
|
+
um, vm = (abox[0] + abox[1]) / 2, (abox[2] + abox[3]) / 2
|
|
452
|
+
sm, tm = (bbox_[0] + bbox_[1]) / 2, (bbox_[2] + bbox_[3]) / 2
|
|
453
|
+
u, v, s, t, ok, _ = _refine_global(A, B, um, vm, sm, tm)
|
|
454
|
+
if ok:
|
|
455
|
+
per_branch[branch_of[abox]].append((u, v, s, t))
|
|
456
|
+
else:
|
|
457
|
+
bad += 1
|
|
458
|
+
curves = []
|
|
459
|
+
for bi in range(len(branches)):
|
|
460
|
+
bpts = per_branch[bi]
|
|
461
|
+
if not bpts:
|
|
462
|
+
continue
|
|
463
|
+
ordered, closed = _order_branch(bpts)
|
|
464
|
+
xyz = [tuple(float(c) for c in A.eval(p[0], p[1])) for p in ordered]
|
|
465
|
+
curves.append({"points": ordered, "xyz": xyz, "closed": closed})
|
|
466
|
+
return {"curves": curves, "uncertified": bad, "empty_certified": False}
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _refine_global(A, B, u, v, s, t, iters: int = 12):
|
|
470
|
+
"""refine_point against full B-spline surfaces (global parameters)."""
|
|
471
|
+
uf, vf, sf, tf = (float(x) for x in (u, v, s, t))
|
|
472
|
+
(au0, au1), (av0, av1) = A.domain()
|
|
473
|
+
(bu0, bu1), (bv0, bv1) = B.domain()
|
|
474
|
+
for _ in range(iters):
|
|
475
|
+
ur = F(uf).limit_denominator(10 ** 12)
|
|
476
|
+
vr = F(vf).limit_denominator(10 ** 12)
|
|
477
|
+
sr = F(sf).limit_denominator(10 ** 12)
|
|
478
|
+
tr = F(tf).limit_denominator(10 ** 12)
|
|
479
|
+
Sa, Au, Av = A.partials(ur, vr)
|
|
480
|
+
Sb, Bu, Bv = B.partials(sr, tr)
|
|
481
|
+
r = [float(Sa[c] - Sb[c]) for c in range(3)]
|
|
482
|
+
if max(abs(x) for x in r) < 1e-14:
|
|
483
|
+
break
|
|
484
|
+
J = [[float(Au[c]), float(Av[c]), -float(Bu[c]), -float(Bv[c])]
|
|
485
|
+
for c in range(3)]
|
|
486
|
+
JJT = [[sum(J[i][k] * J[j][k] for k in range(4)) for j in range(3)]
|
|
487
|
+
for i in range(3)]
|
|
488
|
+
y = _solve3f(JJT, [-x for x in r])
|
|
489
|
+
if y is None:
|
|
490
|
+
break
|
|
491
|
+
step = [sum(J[i][k] * y[i] for i in range(3)) for k in range(4)]
|
|
492
|
+
uf = min(au1, max(au0, uf + step[0]))
|
|
493
|
+
vf = min(av1, max(av0, vf + step[1]))
|
|
494
|
+
sf = min(bu1, max(bu0, sf + step[2]))
|
|
495
|
+
tf = min(bv1, max(bv0, tf + step[3]))
|
|
496
|
+
ur = F(uf).limit_denominator(10 ** 12)
|
|
497
|
+
vr = F(vf).limit_denominator(10 ** 12)
|
|
498
|
+
sr = F(sf).limit_denominator(10 ** 12)
|
|
499
|
+
tr = F(tf).limit_denominator(10 ** 12)
|
|
500
|
+
pa_ = A.eval(ur, vr)
|
|
501
|
+
pb_ = B.eval(sr, tr)
|
|
502
|
+
res2 = sum((pa_[c] - pb_[c]) ** 2 for c in range(3))
|
|
503
|
+
return ur, vr, sr, tr, res2 < F(1, 10 ** 20), res2
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def polyline(points_xyz):
|
|
507
|
+
"""Order 3D points into a polyline by greedy nearest-neighbour
|
|
508
|
+
chaining from an extreme point (float; a render/report artifact —
|
|
509
|
+
the certified objects are the points themselves)."""
|
|
510
|
+
if not points_xyz:
|
|
511
|
+
return []
|
|
512
|
+
pts = [tuple(float(c) for c in p) for p in points_xyz]
|
|
513
|
+
start = max(range(len(pts)),
|
|
514
|
+
key=lambda i: sum((pts[i][c] - pts[0][c]) ** 2 for c in range(3)))
|
|
515
|
+
todo = set(range(len(pts)))
|
|
516
|
+
order = [start]
|
|
517
|
+
todo.discard(start)
|
|
518
|
+
while todo:
|
|
519
|
+
last = pts[order[-1]]
|
|
520
|
+
nxt = min(todo, key=lambda i: sum((pts[i][c] - last[c]) ** 2
|
|
521
|
+
for c in range(3)))
|
|
522
|
+
order.append(nxt)
|
|
523
|
+
todo.discard(nxt)
|
|
524
|
+
return [pts[i] for i in order]
|