Skip to content

Commit 6a3e01a

Browse files
committed
Add circular_cone and nurbs_surface utilities, refactor imports:
- Introduce `circular_cone` function extending cone modeling capabilities. - Add `nurbs_surface` for NURBS surface construction with customizable parameters. - Refactor imports in `__init__.py` to include new utilities and improve consistency. - Update `nurbs_curve` to use `from_homogeneous_1d` for enhanced rational handling.
1 parent aa0c0a7 commit 6a3e01a

2 files changed

Lines changed: 145 additions & 33 deletions

File tree

mmcore/geom/bvh/lbvh.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ def get_leaves(self, node=None):
368368
stack=[node
369369
]
370370
while stack:
371-
current=stack.pop()
371+
current=stack.pop(0)
372372
if current.is_leaf():
373373
yield current
374374
else:

mmcore/numeric/intersection/ccx/_bez_ccx3.py

Lines changed: 144 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@
1111
import numpy as np
1212
import time
1313

14+
try:
15+
from ._bezier_eval import (
16+
eval_bezier_raw as _eval_bezier_raw_fast,
17+
eval_bezier_raw_d1 as _eval_bezier_raw_d1_fast,
18+
eval_bezier_raw_d2 as _eval_bezier_raw_d2_fast,
19+
)
20+
except Exception: # pragma: no cover - optional Cython acceleration
21+
_eval_bezier_raw_fast = None
22+
_eval_bezier_raw_d1_fast = None
23+
_eval_bezier_raw_d2_fast = None
24+
1425
# ---------------------------------------------------------------------------
1526
# Homogeneous helpers
1627
# ---------------------------------------------------------------------------
@@ -73,15 +84,111 @@ def de_casteljau_split(ctrl, prms=None): # (n+1,d)
7384
return left, right
7485

7586

76-
def eval_bezier_raw(P, t):
77-
"""Plain De Casteljau evaluation in the control-space (homogeneous safe)."""
78-
Q = P.copy()
87+
def _eval_bezier_raw_py(P, t):
88+
"""Plain Bézier evaluation in control-space (homogeneous safe)."""
89+
P = np.asarray(P, dtype=np.float64)
7990
n = P.shape[0] - 1
80-
for _ in range(n):
81-
Q = (1.0 - t) * Q[:-1] + t * Q[1:]
91+
if n <= 0:
92+
return P[0]
93+
94+
omt = 1.0 - t
95+
# Fast paths for degrees we hit most (2/3 in practice).
96+
if n == 1:
97+
return omt * P[0] + t * P[1]
98+
if n == 2:
99+
omt2 = omt * omt
100+
t2 = t * t
101+
return omt2 * P[0] + (2.0 * omt * t) * P[1] + t2 * P[2]
102+
if n == 3:
103+
omt2 = omt * omt
104+
t2 = t * t
105+
return (omt2 * omt) * P[0] + (3.0 * omt2 * t) * P[1] + (3.0 * omt * t2) * P[2] + (t2 * t) * P[3]
106+
107+
# Generic De Casteljau (small degrees: numpy allocations are acceptable).
108+
Q = P.copy()
109+
for r in range(1, n + 1):
110+
m = n + 1 - r
111+
Q[:m] = (1.0 - t) * Q[:m] + t * Q[1 : m + 1]
82112
return Q[0]
83113

84114

115+
def _eval_bezier_raw_d1_py(P, t):
116+
"""Return (P(t), P'(t)) in control-space (homogeneous safe)."""
117+
P = np.asarray(P, dtype=np.float64)
118+
n = P.shape[0] - 1
119+
if n <= 0:
120+
return P[0], np.zeros_like(P[0])
121+
122+
omt = 1.0 - t
123+
if n == 1:
124+
return omt * P[0] + t * P[1], (P[1] - P[0])
125+
if n == 2:
126+
pt = _eval_bezier_raw_py(P, t)
127+
dpt = 2.0 * (omt * (P[1] - P[0]) + t * (P[2] - P[1]))
128+
return pt, dpt
129+
if n == 3:
130+
omt2 = omt * omt
131+
t2 = t * t
132+
pt = _eval_bezier_raw_py(P, t)
133+
dpt = 3.0 * (omt2 * (P[1] - P[0]) + (2.0 * omt * t) * (P[2] - P[1]) + t2 * (P[3] - P[2]))
134+
return pt, dpt
135+
136+
Q = P.copy()
137+
for r in range(1, n):
138+
m = n + 1 - r
139+
Q[:m] = (1.0 - t) * Q[:m] + t * Q[1 : m + 1]
140+
q0, q1 = Q[0], Q[1]
141+
dpt = n * (q1 - q0)
142+
pt = (1.0 - t) * q0 + t * q1
143+
return pt, dpt
144+
145+
146+
def _eval_bezier_raw_d2_py(P, t):
147+
"""Return (P(t), P'(t), P''(t)) in control-space (homogeneous safe)."""
148+
P = np.asarray(P, dtype=np.float64)
149+
n = P.shape[0] - 1
150+
if n <= 0:
151+
z = np.zeros_like(P[0])
152+
return P[0], z, z
153+
154+
omt = 1.0 - t
155+
if n == 1:
156+
pt, dpt = _eval_bezier_raw_d1_py(P, t)
157+
return pt, dpt, np.zeros_like(pt)
158+
if n == 2:
159+
pt, dpt = _eval_bezier_raw_d1_py(P, t)
160+
ddpt = 2.0 * (P[2] - 2.0 * P[1] + P[0])
161+
return pt, dpt, ddpt
162+
if n == 3:
163+
pt, dpt = _eval_bezier_raw_d1_py(P, t)
164+
ddpt = 6.0 * (omt * (P[2] - 2.0 * P[1] + P[0]) + t * (P[3] - 2.0 * P[2] + P[1]))
165+
return pt, dpt, ddpt
166+
167+
Q = P.copy()
168+
ddpt = np.zeros_like(P[0])
169+
for r in range(1, n - 1):
170+
m = n + 1 - r
171+
Q[:m] = (1.0 - t) * Q[:m] + t * Q[1 : m + 1]
172+
if m == 3:
173+
ddpt = n * (n - 1) * (Q[2] - 2.0 * Q[1] + Q[0])
174+
# One more step to m==2
175+
Q[:2] = (1.0 - t) * Q[:2] + t * Q[1:3]
176+
q0, q1 = Q[0], Q[1]
177+
dpt = n * (q1 - q0)
178+
pt = (1.0 - t) * q0 + t * q1
179+
return pt, dpt, ddpt
180+
181+
182+
if _eval_bezier_raw_fast is None:
183+
eval_bezier_raw = _eval_bezier_raw_py
184+
eval_bezier_raw_d1 = _eval_bezier_raw_d1_py
185+
eval_bezier_raw_d2 = _eval_bezier_raw_d2_py
186+
else:
187+
eval_bezier_raw = _eval_bezier_raw_fast
188+
eval_bezier_raw_d1 = _eval_bezier_raw_d1_fast
189+
eval_bezier_raw_d2 = _eval_bezier_raw_d2_fast
190+
191+
85192
def eval_bezier(P, t, rational: bool | None = None):
86193
"""Return Euclidean point; dehomogenize if weights are present."""
87194
Ph = eval_bezier_raw(P, t)
@@ -90,21 +197,26 @@ def eval_bezier(P, t, rational: bool | None = None):
90197
return Ph
91198

92199

200+
def eval_bezier_and_deriv(P, t, rational: bool | None = None):
201+
"""Return (point, first derivative) in Euclidean space."""
202+
if is_homogeneous_ctrl(P, rational=rational):
203+
Ph, dPh = eval_bezier_raw_d1(P, t)
204+
w = float(Ph[-1])
205+
dw = float(dPh[-1])
206+
denom = w * w + 1e-30
207+
p = Ph[:-1] / w
208+
dp = (dPh[:-1] * w - Ph[:-1] * dw) / denom
209+
return p, dp
210+
return eval_bezier_raw_d1(P, t)
211+
212+
93213
def deriv_ctrl(P): # first derivative control net
94214
n = P.shape[0] - 1
95215
return n * (P[1:] - P[:-1])
96216

97217

98218
def eval_bezier_deriv(P, t, rational: bool | None = None): # first derivative at t
99-
if is_homogeneous_ctrl(P, rational=rational):
100-
Ph = eval_bezier_raw(P, t)
101-
dPh = eval_bezier_raw(deriv_ctrl(P), t)
102-
w = float(Ph[-1])
103-
dw = float(dPh[-1])
104-
denom = w * w + 1e-30
105-
num = dPh[:-1] * w - Ph[:-1] * dw
106-
return num / denom
107-
return eval_bezier_raw(deriv_ctrl(P), t)
219+
return eval_bezier_and_deriv(P, t, rational=rational)[1]
108220

109221

110222
# ---------- Distance net envelope (pruning) ----------
@@ -197,15 +309,18 @@ def bernstein_envelope_min(dnet):
197309

198310
# ---------- Vector system G(u,v)=C1(u)-C2(v)=0 ----------
199311
def G_and_J(C1, C2, u, v, rational: bool | None = None):
200-
p1 = eval_bezier(C1, u, rational=rational)
201-
t1 = eval_bezier_deriv(C1, u, rational=rational)
202-
p2 = eval_bezier(C2, v, rational=rational)
203-
t2 = eval_bezier_deriv(C2, v, rational=rational)
312+
p1, t1 = eval_bezier_and_deriv(C1, u, rational=rational)
313+
p2, t2 = eval_bezier_and_deriv(C2, v, rational=rational)
204314
G = p1 - p2 # d-vector
205315
J = np.stack([t1, -t2], axis=1) # d x 2
206316
return G, J
207317

208318

319+
def G_only(C1, C2, u, v, rational: bool | None = None):
320+
"""Compute G(u,v)=C1(u)-C2(v) without building J (used in line-search)."""
321+
return eval_bezier(C1, u, rational=rational) - eval_bezier(C2, v, rational=rational)
322+
323+
209324
def newton_project_G0(C1, C2, u0, v0, tol=1e-12, it=13, lm_damp=1e-12, step_tol=1e-9,delta_tol=1e-10, rational: bool | None = None):
210325
"""Levenberg–Marquardt corrector to G(u,v)=0; clamps to [0,1]^2."""
211326
u, v = float(u0), float(v0)
@@ -222,16 +337,16 @@ def newton_project_G0(C1, C2, u0, v0, tol=1e-12, it=13, lm_damp=1e-12, step_tol=
222337
delta = np.zeros(2)
223338
# line search with clamping
224339
step = 1.0
340+
g2 = float(np.dot(G, G))
225341
for _ls in range(8):
226342
un = np.clip(u + step * delta[0], 0.0, 1.0)
227343
vn = np.clip(v + step * delta[1], 0.0, 1.0)
228-
dgj=G_and_J(C1, C2, un, vn, rational=rational)[0]
229-
230-
if np.dot( dgj, dgj) <= np.dot(G,G):
344+
dgj = G_only(C1, C2, un, vn, rational=rational)
345+
if float(np.dot(dgj, dgj)) <= g2:
231346
u, v = un, vn
232347
break
233348
step *= 0.5
234-
if np.dot(G,G) < tol_sq:
349+
if g2 < tol_sq:
235350
break
236351
if step < step_tol and np.dot(delta,delta) < delta_tol_sq:
237352
break
@@ -277,9 +392,7 @@ def second_deriv_ctrl(P):
277392

278393
def eval_bezier_second_deriv(P, t, rational: bool | None = None):
279394
if is_homogeneous_ctrl(P, rational=rational):
280-
Ph = eval_bezier_raw(P, t)
281-
dPh = eval_bezier_raw(deriv_ctrl(P), t)
282-
ddPh = eval_bezier_raw(second_deriv_ctrl(P), t)
395+
Ph, dPh, ddPh = eval_bezier_raw_d2(P, t)
283396
w = float(Ph[-1])
284397
dw = float(dPh[-1])
285398
ddw = float(ddPh[-1])
@@ -293,15 +406,15 @@ def eval_bezier_second_deriv(P, t, rational: bool | None = None):
293406
num = term1 + term2 + term3 + term4
294407
return num / w3
295408

296-
return eval_bezier_raw(second_deriv_ctrl(P), t)
409+
return eval_bezier_raw_d2(P, t)[2]
297410

298411

299412
def curvature_and_speed(C, u, rational: bool | None = None):
300413
t = eval_bezier_deriv(C, u, rational=rational)
301414
s = np.linalg.norm(t)
302415
if s < 1e-16:
303416
return 0.0, 0.0, t # degenerate speed
304-
tt = eval_bezier_second_deriv(C, u)
417+
tt = eval_bezier_second_deriv(C, u, rational=rational)
305418
d = C.shape[1]
306419
if d == 2:
307420
kappa = abs(t[0] * tt[1] - t[1] * tt[0]) / (s**3 + 1e-30)
@@ -355,7 +468,7 @@ def project_G0_fixed_u(C1, C2, u_fixed, v0, tol=1e-12, it=40, lm_damp=1e-12, rat
355468
Returns v, G (residual vector), success(bool).
356469
"""
357470
sq_tol=tol*tol
358-
p1 = eval_bezier(C1, u_fixed)
471+
p1 = eval_bezier(C1, u_fixed, rational=rational)
359472
v = float(v0)
360473
for _ in range(it):
361474
p2 = eval_bezier(C2, v, rational=rational)
@@ -458,7 +571,7 @@ def sigma_flip_alpha(C1, C2, u, v, du, dv, tol_alpha=1e-12, max_it=40, rational:
458571
slo, shi = s0, s1
459572
for _ in range(max_it):
460573
mid = 0.5 * (lo + hi)
461-
sm = dot_sigma(C1, C2, u + mid * du, v + mid * dv)
574+
sm = dot_sigma(C1, C2, u + mid * du, v + mid * dv, rational=rational)
462575
if sm == 0.0 or (hi - lo) < tol_alpha:
463576
return mid
464577
if slo * sm <= 0.0:
@@ -547,7 +660,7 @@ def bbox_diag_len(C1, C2):
547660
# --- classify; if not overlap, return isolated
548661
cls0 = classify_contact(J0, sv_thresh)
549662
if cls0["type"] != "overlap":
550-
x0 = eval_bezier(C1, u0)
663+
x0 = eval_bezier(C1, u0, rational=rational)
551664
return {"kind": cls0["type"], "points": [(u0, v0)], "xyz": [x0], "start": "seed", "end": "seed"}
552665

553666
# --- snap to boundary if very close (exact 0/1)
@@ -817,8 +930,7 @@ def contact_detect_and_extract(C1, C2, seed_uv=(0.5, 0.5), envelope_prune=None,
817930
if res["kind"] != "overlap" or len(res["points"]) < 2:
818931
return {"type": "none"}
819932

820-
xyz = [eval_bezier(C1, uu) for (uu, vv) in res["points"]]
821-
return {"type": "overlap", "uv_path": res["points"], "xyz_path": xyz, "start": res["start"], "end": res["end"]}
933+
return {"type": "overlap", "uv_path": res["points"], "xyz_path": res["xyz"], "start": res["start"], "end": res["end"]}
822934

823935

824936
from typing import Tuple, TypedDict, List, Dict, Any

0 commit comments

Comments
 (0)