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/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]