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/nurbs.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"""K3.1 — NURBS / B-spline curve evaluation via de Boor (ADR-0018/0019).
|
|
2
|
+
|
|
3
|
+
The groundwork for free-form geometry: STEP AP214 curves, general
|
|
4
|
+
sweeps/lofts, and — the crown jewel — surface–surface intersection.
|
|
5
|
+
|
|
6
|
+
The exactness charter reaches further here than one might expect. A
|
|
7
|
+
B-spline with **rational** control points, knots, and weights, evaluated
|
|
8
|
+
at a **rational** parameter, comes out *exactly rational*: de Boor's
|
|
9
|
+
recurrence is nothing but convex combinations — ``+``, ``×``, and ``÷``
|
|
10
|
+
by rationals — so no irrationality enters. Such a curve is ``exact``,
|
|
11
|
+
not merely certified, and ``eval`` returns a point in ℚ³ that OCCT can
|
|
12
|
+
only approximate.
|
|
13
|
+
|
|
14
|
+
The certified-interval path (ADR-0019) is reserved for the genuinely
|
|
15
|
+
irrational cases: an irrational weight (the √2/2 of a true circular
|
|
16
|
+
NURBS arc) or an irrational parameter. There ``eval_ci`` carries each
|
|
17
|
+
homogeneous coordinate as a ``CInterval`` and the division by the
|
|
18
|
+
weight stays enclosure-preserving.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from fractions import Fraction
|
|
24
|
+
|
|
25
|
+
from forgekernel.interval import CInterval
|
|
26
|
+
|
|
27
|
+
F = Fraction
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BSplineCurve:
|
|
31
|
+
"""A NURBS curve: degree ``p``, control points, clamped/open knot
|
|
32
|
+
vector, optional weights (default 1 → a polynomial B-spline).
|
|
33
|
+
|
|
34
|
+
Control points are 3-tuples; knots and weights are scalars. Anything
|
|
35
|
+
rational stays exact through evaluation."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, degree: int, control_points, knots, weights=None) -> None:
|
|
38
|
+
self.p = int(degree)
|
|
39
|
+
self.cp = [tuple(F(c) for c in pt) for pt in control_points]
|
|
40
|
+
self.U = [F(u) for u in knots]
|
|
41
|
+
n = len(self.cp)
|
|
42
|
+
if weights is None:
|
|
43
|
+
self.w = [F(1)] * n
|
|
44
|
+
self.rational = False
|
|
45
|
+
self.exact_weights = True
|
|
46
|
+
else:
|
|
47
|
+
# weights may be rational (exact eval available) or CInterval
|
|
48
|
+
# (a genuinely irrational weight, e.g. √2/2 for a true circle —
|
|
49
|
+
# only the certified path eval_ci applies)
|
|
50
|
+
self.w = [x if isinstance(x, CInterval) else F(x) for x in weights]
|
|
51
|
+
self.exact_weights = all(not isinstance(x, CInterval) for x in self.w)
|
|
52
|
+
self.rational = not self.exact_weights or \
|
|
53
|
+
any(x != self.w[0] for x in self.w)
|
|
54
|
+
if len(self.w) != n:
|
|
55
|
+
raise ValueError("weights and control points differ in count")
|
|
56
|
+
if len(self.U) != n + self.p + 1:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"knot vector must have n+p+1={n + self.p + 1} entries, "
|
|
59
|
+
f"got {len(self.U)}")
|
|
60
|
+
|
|
61
|
+
# -- span location --------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
def _span(self, u: F) -> int:
|
|
64
|
+
"""Knot span index k with U[k] <= u < U[k+1] (clamped to the last
|
|
65
|
+
non-empty span at the right end). Exact rational comparisons."""
|
|
66
|
+
n = len(self.cp) - 1
|
|
67
|
+
if u >= self.U[n + 1]:
|
|
68
|
+
return n
|
|
69
|
+
if u <= self.U[self.p]:
|
|
70
|
+
return self.p
|
|
71
|
+
lo, hi = self.p, n + 1
|
|
72
|
+
while hi - lo > 1: # binary search, exact
|
|
73
|
+
mid = (lo + hi) // 2
|
|
74
|
+
if u < self.U[mid]:
|
|
75
|
+
hi = mid
|
|
76
|
+
else:
|
|
77
|
+
lo = mid
|
|
78
|
+
return lo
|
|
79
|
+
|
|
80
|
+
# -- exact rational evaluation (de Boor in homogeneous coords) ------------
|
|
81
|
+
|
|
82
|
+
def eval(self, t):
|
|
83
|
+
"""Exact point in ℚ³ at parameter ``t`` (rational in, rational out).
|
|
84
|
+
Raises if any weight is irrational — use :meth:`eval_ci` for that."""
|
|
85
|
+
if not self.exact_weights:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
"curve has irrational weights — use eval_ci (certified)")
|
|
88
|
+
u = F(t)
|
|
89
|
+
k = self._span(u)
|
|
90
|
+
p = self.p
|
|
91
|
+
# homogeneous control points (w·x, w·y, w·z, w) for the active span
|
|
92
|
+
d = []
|
|
93
|
+
for j in range(p + 1):
|
|
94
|
+
i = k - p + j
|
|
95
|
+
wi = self.w[i]
|
|
96
|
+
x, y, z = self.cp[i]
|
|
97
|
+
d.append([wi * x, wi * y, wi * z, wi])
|
|
98
|
+
for r in range(1, p + 1):
|
|
99
|
+
for j in range(p, r - 1, -1):
|
|
100
|
+
i = k - p + j
|
|
101
|
+
denom = self.U[i + p - r + 1] - self.U[i]
|
|
102
|
+
a = F(0) if denom == 0 else (u - self.U[i]) / denom
|
|
103
|
+
b = 1 - a
|
|
104
|
+
d[j] = [b * d[j - 1][c] + a * d[j][c] for c in range(4)]
|
|
105
|
+
hx, hy, hz, hw = d[p]
|
|
106
|
+
return (hx / hw, hy / hw, hz / hw)
|
|
107
|
+
|
|
108
|
+
# -- certified evaluation (for irrational weights/parameters) -------------
|
|
109
|
+
|
|
110
|
+
def eval_ci(self, t):
|
|
111
|
+
"""Certified point (three ``CInterval``s) — the enclosure-preserving
|
|
112
|
+
path for irrational weights or parameters. Accepts rational or
|
|
113
|
+
``CInterval`` inputs and never loses the bracket."""
|
|
114
|
+
u = t if isinstance(t, CInterval) else CInterval.exact(F(t))
|
|
115
|
+
# span from the interval midpoint (a location choice, not a decision
|
|
116
|
+
# that affects the certified value — the recurrence is continuous)
|
|
117
|
+
k = self._span(u.mid)
|
|
118
|
+
p = self.p
|
|
119
|
+
d = []
|
|
120
|
+
for j in range(p + 1):
|
|
121
|
+
i = k - p + j
|
|
122
|
+
wi = _ci(self.w[i])
|
|
123
|
+
x, y, z = (_ci(v) for v in self.cp[i])
|
|
124
|
+
d.append([wi * x, wi * y, wi * z, wi])
|
|
125
|
+
for r in range(1, p + 1):
|
|
126
|
+
for j in range(p, r - 1, -1):
|
|
127
|
+
i = k - p + j
|
|
128
|
+
denom = self.U[i + p - r + 1] - self.U[i]
|
|
129
|
+
if denom == 0:
|
|
130
|
+
continue
|
|
131
|
+
a = (u - _ci(self.U[i])) * _ci(F(1) / denom)
|
|
132
|
+
b = _ci(1) - a
|
|
133
|
+
d[j] = [b * d[j - 1][c] + a * d[j][c] for c in range(4)]
|
|
134
|
+
hx, hy, hz, hw = d[p]
|
|
135
|
+
inv = _ci_reciprocal(hw)
|
|
136
|
+
return (hx * inv, hy * inv, hz * inv)
|
|
137
|
+
|
|
138
|
+
# -- float evaluation (tessellation) --------------------------------------
|
|
139
|
+
|
|
140
|
+
def eval_f(self, t: float):
|
|
141
|
+
x, y, z = self.eval(F(t).limit_denominator(10 ** 9))
|
|
142
|
+
return (float(x), float(y), float(z))
|
|
143
|
+
|
|
144
|
+
def domain(self):
|
|
145
|
+
return (float(self.U[self.p]), float(self.U[len(self.cp)]))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# -- constructors -------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
def bezier(control_points, weights=None) -> BSplineCurve:
|
|
151
|
+
"""A Bézier curve as a clamped B-spline on [0, 1]."""
|
|
152
|
+
n = len(control_points)
|
|
153
|
+
p = n - 1
|
|
154
|
+
knots = [F(0)] * (p + 1) + [F(1)] * (p + 1)
|
|
155
|
+
return BSplineCurve(p, control_points, knots, weights)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _ci(x) -> CInterval:
|
|
159
|
+
return x if isinstance(x, CInterval) else CInterval.exact(F(x))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _ci_reciprocal(x: CInterval) -> CInterval:
|
|
163
|
+
"""1/x for an interval that strictly excludes zero (certified)."""
|
|
164
|
+
s = x.sign() # raises if straddles 0
|
|
165
|
+
lo, hi = (F(1) / x.hi, F(1) / x.lo) if s > 0 else (F(1) / x.hi, F(1) / x.lo)
|
|
166
|
+
return CInterval(min(lo, hi), max(lo, hi))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# -- K3.2: tensor-product NURBS surfaces --------------------------------------
|
|
170
|
+
|
|
171
|
+
def _deboor4(p: int, U, pts, u):
|
|
172
|
+
"""De Boor on homogeneous 4-vectors (exact rational). ``pts`` spans one
|
|
173
|
+
curve; returns the 4-vector at parameter ``u``."""
|
|
174
|
+
n = len(pts) - 1
|
|
175
|
+
# span
|
|
176
|
+
if u >= U[n + 1]:
|
|
177
|
+
k = n
|
|
178
|
+
elif u <= U[p]:
|
|
179
|
+
k = p
|
|
180
|
+
else:
|
|
181
|
+
lo, hi = p, n + 1
|
|
182
|
+
while hi - lo > 1:
|
|
183
|
+
mid = (lo + hi) // 2
|
|
184
|
+
if u < U[mid]:
|
|
185
|
+
hi = mid
|
|
186
|
+
else:
|
|
187
|
+
lo = mid
|
|
188
|
+
k = lo
|
|
189
|
+
d = [list(pts[k - p + j]) for j in range(p + 1)]
|
|
190
|
+
dim = len(d[0])
|
|
191
|
+
for r in range(1, p + 1):
|
|
192
|
+
for j in range(p, r - 1, -1):
|
|
193
|
+
i = k - p + j
|
|
194
|
+
denom = U[i + p - r + 1] - U[i]
|
|
195
|
+
a = F(0) if denom == 0 else (u - U[i]) / denom
|
|
196
|
+
b = 1 - a
|
|
197
|
+
d[j] = [b * d[j - 1][c] + a * d[j][c] for c in range(dim)]
|
|
198
|
+
return tuple(d[p])
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _hodograph4(p: int, U, pts):
|
|
202
|
+
"""Derivative curve of a (homogeneous) B-spline: degree p-1, control
|
|
203
|
+
points p·(P[i+1]−P[i])/(U[i+p+1]−U[i+1]), knots U[1:-1]. Exact."""
|
|
204
|
+
D = []
|
|
205
|
+
for i in range(len(pts) - 1):
|
|
206
|
+
denom = U[i + p + 1] - U[i + 1]
|
|
207
|
+
s = F(0) if denom == 0 else F(p) / denom
|
|
208
|
+
D.append(tuple(s * (pts[i + 1][c] - pts[i][c]) for c in range(4)))
|
|
209
|
+
return p - 1, U[1:-1], D
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class BSplineSurface:
|
|
213
|
+
"""A tensor-product NURBS surface: degrees (p, q), an nu×nv control
|
|
214
|
+
net, clamped knot vectors U (nu+p+1) and V (nv+q+1), optional
|
|
215
|
+
weights. Rational data at rational parameters evaluates exactly."""
|
|
216
|
+
|
|
217
|
+
def __init__(self, degree_u: int, degree_v: int, control_net,
|
|
218
|
+
knots_u, knots_v, weights=None) -> None:
|
|
219
|
+
self.p = int(degree_u)
|
|
220
|
+
self.q = int(degree_v)
|
|
221
|
+
self.cp = [[tuple(F(c) for c in pt) for pt in row] for row in control_net]
|
|
222
|
+
self.nu = len(self.cp)
|
|
223
|
+
self.nv = len(self.cp[0])
|
|
224
|
+
if any(len(r) != self.nv for r in self.cp):
|
|
225
|
+
raise ValueError("ragged control net")
|
|
226
|
+
self.U = [F(u) for u in knots_u]
|
|
227
|
+
self.V = [F(v) for v in knots_v]
|
|
228
|
+
if len(self.U) != self.nu + self.p + 1:
|
|
229
|
+
raise ValueError(f"knots_u wants {self.nu + self.p + 1} entries")
|
|
230
|
+
if len(self.V) != self.nv + self.q + 1:
|
|
231
|
+
raise ValueError(f"knots_v wants {self.nv + self.q + 1} entries")
|
|
232
|
+
if weights is None:
|
|
233
|
+
self.w = [[F(1)] * self.nv for _ in range(self.nu)]
|
|
234
|
+
else:
|
|
235
|
+
self.w = [[F(x) for x in row] for row in weights]
|
|
236
|
+
# homogeneous net
|
|
237
|
+
self.H = [[(self.w[i][j] * self.cp[i][j][0],
|
|
238
|
+
self.w[i][j] * self.cp[i][j][1],
|
|
239
|
+
self.w[i][j] * self.cp[i][j][2],
|
|
240
|
+
self.w[i][j]) for j in range(self.nv)]
|
|
241
|
+
for i in range(self.nu)]
|
|
242
|
+
|
|
243
|
+
# -- exact evaluation ------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
def _eval_h(self, u, v):
|
|
246
|
+
"""Homogeneous 4-vector at (u, v): de Boor down v then across u."""
|
|
247
|
+
u, v = F(u), F(v)
|
|
248
|
+
col = [_deboor4(self.q, self.V, row, v) for row in self.H]
|
|
249
|
+
return _deboor4(self.p, self.U, col, u)
|
|
250
|
+
|
|
251
|
+
def eval(self, u, v):
|
|
252
|
+
"""Exact surface point in ℚ³ (rational in → rational out)."""
|
|
253
|
+
hx, hy, hz, hw = self._eval_h(u, v)
|
|
254
|
+
return (hx / hw, hy / hw, hz / hw)
|
|
255
|
+
|
|
256
|
+
def eval_f(self, u: float, v: float):
|
|
257
|
+
x, y, z = self.eval(F(u).limit_denominator(10 ** 9),
|
|
258
|
+
F(v).limit_denominator(10 ** 9))
|
|
259
|
+
return (float(x), float(y), float(z))
|
|
260
|
+
|
|
261
|
+
# -- exact partial derivatives (quotient rule in homogeneous space) -------
|
|
262
|
+
|
|
263
|
+
def _partials_h(self, u, v):
|
|
264
|
+
"""(H, H_u, H_v) homogeneous 4-vectors, all exact."""
|
|
265
|
+
u, v = F(u), F(v)
|
|
266
|
+
col = [_deboor4(self.q, self.V, row, v) for row in self.H]
|
|
267
|
+
H = _deboor4(self.p, self.U, col, u)
|
|
268
|
+
pu, Uu, Du = _hodograph4(self.p, self.U, col)
|
|
269
|
+
H_u = _deboor4(pu, Uu, Du, u) if pu >= 0 and Du else (F(0),) * 4
|
|
270
|
+
# v-partial: hodograph each row in v, evaluate, then de Boor in u
|
|
271
|
+
rows_dv = []
|
|
272
|
+
for row in self.H:
|
|
273
|
+
qv, Vv, Dv = _hodograph4(self.q, self.V, row)
|
|
274
|
+
rows_dv.append(_deboor4(qv, Vv, Dv, v) if qv >= 0 and Dv
|
|
275
|
+
else (F(0),) * 4)
|
|
276
|
+
H_v = _deboor4(self.p, self.U, rows_dv, u)
|
|
277
|
+
return H, H_u, H_v
|
|
278
|
+
|
|
279
|
+
def partials(self, u, v):
|
|
280
|
+
"""(S, S_u, S_v) in ℚ³ — the exact tangent plane data SSI needs.
|
|
281
|
+
Quotient rule: S_u = (A_u·w − A·w_u)/w² with H = (A, w)."""
|
|
282
|
+
H, H_u, H_v = self._partials_h(u, v)
|
|
283
|
+
w, wu, wv = H[3], H_u[3], H_v[3]
|
|
284
|
+
S = tuple(H[c] / w for c in range(3))
|
|
285
|
+
S_u = tuple((H_u[c] * w - H[c] * wu) / (w * w) for c in range(3))
|
|
286
|
+
S_v = tuple((H_v[c] * w - H[c] * wv) / (w * w) for c in range(3))
|
|
287
|
+
return S, S_u, S_v
|
|
288
|
+
|
|
289
|
+
def normal(self, u, v):
|
|
290
|
+
"""Exact (unnormalized) normal S_u × S_v in ℚ³."""
|
|
291
|
+
_, su, sv = self.partials(u, v)
|
|
292
|
+
return (su[1] * sv[2] - su[2] * sv[1],
|
|
293
|
+
su[2] * sv[0] - su[0] * sv[2],
|
|
294
|
+
su[0] * sv[1] - su[1] * sv[0])
|
|
295
|
+
|
|
296
|
+
def domain(self):
|
|
297
|
+
return ((float(self.U[self.p]), float(self.U[self.nu])),
|
|
298
|
+
(float(self.V[self.q]), float(self.V[self.nv])))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def bezier_surface(control_net, weights=None) -> BSplineSurface:
|
|
302
|
+
"""A Bézier patch as a clamped B-spline surface on [0,1]²."""
|
|
303
|
+
nu, nv = len(control_net), len(control_net[0])
|
|
304
|
+
p, q = nu - 1, nv - 1
|
|
305
|
+
ku = [F(0)] * (p + 1) + [F(1)] * (p + 1)
|
|
306
|
+
kv = [F(0)] * (q + 1) + [F(1)] * (q + 1)
|
|
307
|
+
return BSplineSurface(p, q, control_net, ku, kv, weights)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# -- K3.5: exact Bézier extraction (knot insertion to full multiplicity) ------
|
|
311
|
+
|
|
312
|
+
def _insert_knot_once(p: int, U, pts, u):
|
|
313
|
+
"""Boehm insertion of knot ``u`` into a (homogeneous or cartesian)
|
|
314
|
+
control sequence. Exact: convex combinations only. Returns (U', pts')."""
|
|
315
|
+
n = len(pts) - 1
|
|
316
|
+
# span k: U[k] <= u < U[k+1]
|
|
317
|
+
k = p
|
|
318
|
+
while k < n + 1 and not (U[k] <= u < U[k + 1]):
|
|
319
|
+
k += 1
|
|
320
|
+
if k == n + 1:
|
|
321
|
+
k = n
|
|
322
|
+
dim = len(pts[0])
|
|
323
|
+
out = [pts[0]]
|
|
324
|
+
for i in range(1, len(pts) + 1):
|
|
325
|
+
if i <= k - p:
|
|
326
|
+
out.append(pts[i] if i < len(pts) else pts[-1])
|
|
327
|
+
elif i <= k:
|
|
328
|
+
denom = U[i + p] - U[i]
|
|
329
|
+
a = F(0) if denom == 0 else (u - U[i]) / denom
|
|
330
|
+
out.append(tuple((1 - a) * pts[i - 1][c] + a * pts[i][c]
|
|
331
|
+
for c in range(dim)))
|
|
332
|
+
else:
|
|
333
|
+
out.append(pts[i - 1])
|
|
334
|
+
newU = sorted(list(U) + [u])
|
|
335
|
+
return newU, out[:n + 2]
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def bezier_segments(curve: "BSplineCurve"):
|
|
339
|
+
"""Split a (polynomial) B-spline curve into exact Bézier segments:
|
|
340
|
+
[(u0, u1, [P0..Pp]), ...]. Knot insertion to full multiplicity."""
|
|
341
|
+
if not curve.exact_weights or curve.rational:
|
|
342
|
+
raise ValueError("bezier extraction: polynomial curves only (K3.6)")
|
|
343
|
+
p = curve.p
|
|
344
|
+
U = list(curve.U)
|
|
345
|
+
pts = [tuple(pt) for pt in curve.cp]
|
|
346
|
+
# insert every interior knot up to multiplicity p
|
|
347
|
+
lo, hi = U[p], U[len(pts)]
|
|
348
|
+
interior = sorted({u for u in U if lo < u < hi})
|
|
349
|
+
for u in interior:
|
|
350
|
+
while U.count(u) < p:
|
|
351
|
+
U, pts = _insert_knot_once(p, U, pts, u)
|
|
352
|
+
breaks = [lo] + interior + [hi]
|
|
353
|
+
segs = []
|
|
354
|
+
for j in range(len(breaks) - 1):
|
|
355
|
+
segs.append((breaks[j], breaks[j + 1], pts[j * p: j * p + p + 1]))
|
|
356
|
+
return segs
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def bezier_patches(surface: "BSplineSurface"):
|
|
360
|
+
"""Split a B-spline surface into exact Bézier patches:
|
|
361
|
+
[(u0, u1, v0, v1, net), ...] — insertion along u then along v.
|
|
362
|
+
|
|
363
|
+
Polynomial surfaces yield 3-tuple cartesian nets; rational surfaces
|
|
364
|
+
yield homogeneous 4-tuple nets (wx, wy, wz, w) — knot insertion is
|
|
365
|
+
the same convex-combination recurrence in either space."""
|
|
366
|
+
rational = any(w != F(1) for row in surface.w for w in row)
|
|
367
|
+
p, q = surface.p, surface.q
|
|
368
|
+
# --- u direction: treat each v-column of the net as a u-curve
|
|
369
|
+
U = list(surface.U)
|
|
370
|
+
src = surface.H if rational else surface.cp
|
|
371
|
+
cols = [[src[i][j] for i in range(surface.nu)]
|
|
372
|
+
for j in range(surface.nv)]
|
|
373
|
+
lo_u, hi_u = U[p], U[surface.nu]
|
|
374
|
+
int_u = sorted({u for u in U if lo_u < u < hi_u})
|
|
375
|
+
for u in int_u:
|
|
376
|
+
while U.count(u) < p:
|
|
377
|
+
newU = None
|
|
378
|
+
for j in range(len(cols)):
|
|
379
|
+
nu2, cols[j] = _insert_knot_once(p, U, cols[j], u)
|
|
380
|
+
newU = nu2
|
|
381
|
+
U = newU
|
|
382
|
+
# --- v direction on the refined net
|
|
383
|
+
V = list(surface.V)
|
|
384
|
+
rows = [[cols[j][i] for j in range(len(cols))]
|
|
385
|
+
for i in range(len(cols[0]))]
|
|
386
|
+
lo_v, hi_v = V[q], V[surface.nv]
|
|
387
|
+
int_v = sorted({v for v in V if lo_v < v < hi_v})
|
|
388
|
+
for v in int_v:
|
|
389
|
+
while V.count(v) < q:
|
|
390
|
+
newV = None
|
|
391
|
+
for i in range(len(rows)):
|
|
392
|
+
nv2, rows[i] = _insert_knot_once(q, V, rows[i], v)
|
|
393
|
+
newV = nv2
|
|
394
|
+
V = newV
|
|
395
|
+
ub = [lo_u] + int_u + [hi_u]
|
|
396
|
+
vb = [lo_v] + int_v + [hi_v]
|
|
397
|
+
patches = []
|
|
398
|
+
for a in range(len(ub) - 1):
|
|
399
|
+
for b in range(len(vb) - 1):
|
|
400
|
+
net = [[rows[a * p + i][b * q + j] for j in range(q + 1)]
|
|
401
|
+
for i in range(p + 1)]
|
|
402
|
+
patches.append((ub[a], ub[a + 1], vb[b], vb[b + 1], net))
|
|
403
|
+
return patches
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
# -- K6.0: second partials (polynomial surfaces; rational → K6.1) -------------
|
|
407
|
+
|
|
408
|
+
def _hodo_list(p, U, pts):
|
|
409
|
+
"""Hodograph as plain lists (degree p-1, knots U[1:-1])."""
|
|
410
|
+
D = []
|
|
411
|
+
for i in range(len(pts) - 1):
|
|
412
|
+
denom = U[i + p + 1] - U[i + 1]
|
|
413
|
+
s = F(0) if denom == 0 else F(p) / denom
|
|
414
|
+
D.append(tuple(s * (pts[i + 1][c] - pts[i][c])
|
|
415
|
+
for c in range(len(pts[0]))))
|
|
416
|
+
return p - 1, U[1:-1], D
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _h_partials2(surface: "BSplineSurface", u, v):
|
|
420
|
+
"""Homogeneous 4-vector derivatives (H, H_u, H_v, H_uu, H_uv, H_vv)
|
|
421
|
+
of the weighted net — exact via double hodographs."""
|
|
422
|
+
u, v = F(u), F(v)
|
|
423
|
+
p, q, U, V = surface.p, surface.q, surface.U, surface.V
|
|
424
|
+
zero4 = (F(0),) * 4
|
|
425
|
+
|
|
426
|
+
def eval_curve(pp, UU, pts, t):
|
|
427
|
+
return _deboor4(pp, UU, pts, t) if pp >= 0 and pts else zero4
|
|
428
|
+
|
|
429
|
+
rows_v = [tuple(_deboor4(q, V, [tuple(pt) for pt in row], v))
|
|
430
|
+
for row in surface.H]
|
|
431
|
+
H = _deboor4(p, U, rows_v, u)
|
|
432
|
+
pu, Uu, Du = _hodo_list(p, U, rows_v)
|
|
433
|
+
H_u = eval_curve(pu, Uu, Du, u)
|
|
434
|
+
puu, Uuu, Duu = _hodo_list(pu, Uu, Du) if pu >= 1 else (-1, [], [])
|
|
435
|
+
H_uu = eval_curve(puu, Uuu, Duu, u)
|
|
436
|
+
rows_dv, rows_dvv = [], []
|
|
437
|
+
for r in surface.H:
|
|
438
|
+
qv, Vv, Dv = _hodo_list(q, V, [tuple(pt) for pt in r])
|
|
439
|
+
rows_dv.append(eval_curve(qv, Vv, Dv, v))
|
|
440
|
+
if qv >= 1:
|
|
441
|
+
qvv, Vvv, Dvv = _hodo_list(qv, Vv, Dv)
|
|
442
|
+
rows_dvv.append(eval_curve(qvv, Vvv, Dvv, v))
|
|
443
|
+
else:
|
|
444
|
+
rows_dvv.append(zero4)
|
|
445
|
+
H_v = _deboor4(p, U, rows_dv, u)
|
|
446
|
+
H_vv = _deboor4(p, U, rows_dvv, u)
|
|
447
|
+
puv, Uuv, Duv = _hodo_list(p, U, rows_dv)
|
|
448
|
+
H_uv = eval_curve(puv, Uuv, Duv, u)
|
|
449
|
+
return H, H_u, H_v, H_uu, H_uv, H_vv
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def surface_partials2(surface: "BSplineSurface", u, v):
|
|
453
|
+
"""(S, S_u, S_v, S_uu, S_uv, S_vv) — all exact ℚ³, polynomial OR
|
|
454
|
+
rational (K6.1). Recursive quotient rule in homogeneous space:
|
|
455
|
+
|
|
456
|
+
S = A/w
|
|
457
|
+
S_x = (A_x − S·w_x)/w
|
|
458
|
+
S_xy = (A_xy − S_x·w_y − S_y·w_x − S·w_xy)/w
|
|
459
|
+
|
|
460
|
+
— only +, ×, ÷ by rationals, so the result is exact whenever the
|
|
461
|
+
weights are."""
|
|
462
|
+
H, H_u, H_v, H_uu, H_uv, H_vv = _h_partials2(surface, u, v)
|
|
463
|
+
w = H[3]
|
|
464
|
+
|
|
465
|
+
def sub3(A, *terms):
|
|
466
|
+
"""(A[0:3] − Σ coeff·vec)/w componentwise."""
|
|
467
|
+
out = []
|
|
468
|
+
for c in range(3):
|
|
469
|
+
acc = A[c]
|
|
470
|
+
for coeff, vec in terms:
|
|
471
|
+
acc -= coeff * vec[c]
|
|
472
|
+
out.append(acc / w)
|
|
473
|
+
return tuple(out)
|
|
474
|
+
|
|
475
|
+
S = (H[0] / w, H[1] / w, H[2] / w)
|
|
476
|
+
S_u = sub3(H_u, (H_u[3], S))
|
|
477
|
+
S_v = sub3(H_v, (H_v[3], S))
|
|
478
|
+
S_uu = sub3(H_uu, (2 * H_u[3], S_u), (H_uu[3], S))
|
|
479
|
+
S_vv = sub3(H_vv, (2 * H_v[3], S_v), (H_vv[3], S))
|
|
480
|
+
S_uv = sub3(H_uv, (H_u[3], S_v), (H_v[3], S_u), (H_uv[3], S))
|
|
481
|
+
return S, S_u, S_v, S_uu, S_uv, S_vv
|