99import numpy as np
1010
1111from matplotlib import _api
12+ from numpy .polynomial .polynomial import polyval as _polyval
13+
14+
15+ def _bisect_root_finder (f , a , b , tol = 1e-12 , max_iter = 64 ):
16+ """Find root of f in [a, b] using bisection. Assumes sign change exists."""
17+ fa = f (a )
18+ for _ in range (max_iter ):
19+ mid = (a + b ) * 0.5
20+ fm = f (mid )
21+ if abs (fm ) < tol or (b - a ) < tol :
22+ return mid
23+ if fa * fm < 0 :
24+ b = mid
25+ else :
26+ a , fa = mid , fm
27+ return (a + b ) * 0.5
28+
29+
30+ def _real_roots_in_01 (coeffs ):
31+ """
32+ Find real roots of polynomial in [0, 1] using sampling and bisection.
33+ coeffs in ascending order: c0 + c1*x + c2*x**2 + ...
34+ """
35+ deg = len (coeffs ) - 1
36+ n_samples = max (8 , deg * 2 )
37+ ts = np .linspace (0 , 1 , n_samples )
38+ vals = _polyval (ts , coeffs )
39+
40+ signs = np .sign (vals )
41+ sign_changes = np .where ((signs [:- 1 ] != signs [1 :]) & (signs [:- 1 ] != 0 ))[0 ]
42+
43+ roots = []
44+
45+ def f (t ):
46+ return _polyval (t , coeffs )
47+
48+ max_iter = 53 # float64 fractional precision for [0, 1] interval
49+ for i in sign_changes :
50+ roots .append (_bisect_root_finder (f , ts [i ], ts [i + 1 ], max_iter = max_iter ))
51+
52+ # Check endpoints
53+ if abs (vals [0 ]) < 1e-12 :
54+ roots .insert (0 , 0.0 )
55+ if abs (vals [- 1 ]) < 1e-12 and (not roots or abs (roots [- 1 ] - 1.0 ) > 1e-10 ):
56+ roots .append (1.0 )
57+
58+ return np .asarray (roots )
59+
60+
61+ def _quadratic_roots_in_01 (c0 , c1 , c2 ):
62+ """Real roots of c0 + c1*x + c2*x**2 in [0, 1]."""
63+ if abs (c2 ) < 1e-12 : # Linear
64+ if abs (c1 ) < 1e-12 :
65+ return np .array ([])
66+ root = - c0 / c1
67+ return np .array ([root ]) if 0 <= root <= 1 else np .array ([])
68+
69+ disc = c1 * c1 - 4 * c2 * c0
70+ if disc < 0 :
71+ return np .array ([])
72+
73+ sqrt_disc = np .sqrt (disc )
74+ # Numerically stable quadratic formula
75+ if c1 >= 0 :
76+ q = - 0.5 * (c1 + sqrt_disc )
77+ else :
78+ q = - 0.5 * (c1 - sqrt_disc )
79+
80+ roots = []
81+ if abs (q ) > 1e-12 :
82+ roots .append (c0 / q )
83+ if abs (c2 ) > 1e-12 :
84+ roots .append (q / c2 )
85+
86+ roots = np .asarray (roots )
87+ return roots [(roots >= 0 ) & (roots <= 1 )]
1288
1389
1490# same algorithm as 3.8's math.comb
@@ -22,7 +98,7 @@ def _comb(n, k):
2298 return np .prod ((n + 1 - i )/ i ).astype (int )
2399
24100
25- # Precomputed matrices for converting Bézier control points to polynomial
101+ # Precomputed matrices for converting Bezier control points to polynomial
26102# coefficients. _COEFF_MATRICES[n] @ control_points gives coefficients.
27103# These avoid the slow _comb vectorized function for common cases.
28104_COEFF_MATRICES = {
@@ -322,17 +398,35 @@ def axis_aligned_extrema(self):
322398 if n <= 1 :
323399 return np .array ([]), np .array ([])
324400 Cj = self .polynomial_coefficients
325- dCj = np .arange (1 , n + 1 )[:, None ] * Cj [1 :]
326- dims = []
327- roots = []
401+ dCj = np .arange (1 , n + 1 )[:, None ] * Cj [1 :]
402+
403+ all_dims = []
404+ all_roots = []
405+
328406 for i , pi in enumerate (dCj .T ):
329- r = np .roots (pi [::- 1 ])
330- roots .append (r )
331- dims .append (np .full_like (r , i ))
332- roots = np .concatenate (roots )
333- dims = np .concatenate (dims )
334- in_range = np .isreal (roots ) & (roots >= 0 ) & (roots <= 1 )
335- return dims [in_range ], np .real (roots )[in_range ]
407+ # Trim trailing near-zeros to get actual degree
408+ deg = len (pi ) - 1
409+ while deg > 0 and abs (pi [deg ]) < 1e-12 :
410+ deg -= 1
411+
412+ if deg == 0 :
413+ continue
414+ elif deg == 1 :
415+ root = - pi [0 ] / pi [1 ]
416+ r = np .array ([root ]) if 0 <= root <= 1 else np .array ([])
417+ elif deg == 2 :
418+ r = _quadratic_roots_in_01 (pi [0 ], pi [1 ], pi [2 ])
419+ else :
420+ r = _real_roots_in_01 (pi [:deg + 1 ])
421+
422+ if len (r ) > 0 :
423+ all_roots .append (r )
424+ all_dims .append (np .full (len (r ), i ))
425+
426+ if not all_roots :
427+ return np .array ([]), np .array ([])
428+
429+ return np .concatenate (all_dims ), np .concatenate (all_roots )
336430
337431
338432def split_bezier_intersecting_with_closedpath (
0 commit comments