1111import numpy as np
1212import 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+
85192def 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+
93213def deriv_ctrl (P ): # first derivative control net
94214 n = P .shape [0 ] - 1
95215 return n * (P [1 :] - P [:- 1 ])
96216
97217
98218def 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 ----------
199311def 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+
209324def 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
278393def 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
299412def 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
824936from typing import Tuple , TypedDict , List , Dict , Any
0 commit comments