|
4 | 4 |
|
5 | 5 | import math
|
6 | 6 | import warnings
|
| 7 | +from collections import deque |
7 | 8 |
|
8 | 9 | import numpy as np
|
9 | 10 |
|
@@ -206,6 +207,96 @@ def point_at_t(self, t):
|
206 | 207 | """Return the point on the Bezier curve for parameter *t*."""
|
207 | 208 | return tuple(self(t))
|
208 | 209 |
|
| 210 | + def split_at_t(self, t): |
| 211 | + """Split into two Bezier curves using de casteljau's algorithm. |
| 212 | +
|
| 213 | + Parameters |
| 214 | + ---------- |
| 215 | + t : float |
| 216 | + Point in [0,1] at which to split into two curves |
| 217 | +
|
| 218 | + Returns |
| 219 | + ------- |
| 220 | + B1, B2 : BezierSegment |
| 221 | + The two sub-curves. |
| 222 | + """ |
| 223 | + new_cpoints = split_de_casteljau(self._cpoints, t) |
| 224 | + return BezierSegment(new_cpoints[0]), BezierSegment(new_cpoints[1]) |
| 225 | + |
| 226 | + def control_net_length(self): |
| 227 | + """Sum of lengths between control points""" |
| 228 | + L = 0 |
| 229 | + N, d = self._cpoints.shape |
| 230 | + for i in range(N - 1): |
| 231 | + L += np.linalg.norm(self._cpoints[i+1] - self._cpoints[i]) |
| 232 | + return L |
| 233 | + |
| 234 | + def arc_length(self, rtol=None, atol=None): |
| 235 | + """Estimate the length using iterative refinement. |
| 236 | +
|
| 237 | + Our estimate is just the average between the length of the chord and |
| 238 | + the length of the control net. |
| 239 | +
|
| 240 | + Since the chord length and control net give lower and upper bounds |
| 241 | + (respectively) on the length, this maximum possible error is tested |
| 242 | + against an absolute tolerance threshold at each subdivision. |
| 243 | +
|
| 244 | + However, sometimes this estimator converges much faster than this error |
| 245 | + esimate would suggest. Therefore, the relative change in the length |
| 246 | + estimate between subdivisions is compared to a relative error tolerance |
| 247 | + after each set of subdivisions. |
| 248 | +
|
| 249 | + Parameters |
| 250 | + ---------- |
| 251 | + rtol : float, default 1e-4 |
| 252 | + If :code:`abs(est[i+1] - est[i]) <= rtol * est[i+1]`, we return |
| 253 | + :code:`est[i+1]`. |
| 254 | + atol : float, default 1e-6 |
| 255 | + If the distance between chord length and control length at any |
| 256 | + point falls below this number, iteration is terminated. |
| 257 | + """ |
| 258 | + if rtol is None: |
| 259 | + rtol = 1e-4 |
| 260 | + if atol is None: |
| 261 | + atol = 1e-6 |
| 262 | + |
| 263 | + chord = np.linalg.norm(self._cpoints[-1] - self._cpoints[0]) |
| 264 | + net = self.control_net_length() |
| 265 | + max_err = (net - chord)/2 |
| 266 | + curr_est = chord + max_err |
| 267 | + # early exit so we don't try to "split" paths of zero length |
| 268 | + if max_err < atol: |
| 269 | + return curr_est |
| 270 | + |
| 271 | + prev_est = np.inf |
| 272 | + curves = deque([self]) |
| 273 | + errs = deque([max_err]) |
| 274 | + lengths = deque([curr_est]) |
| 275 | + while np.abs(curr_est - prev_est) > rtol * curr_est: |
| 276 | + # subdivide the *whole* curve before checking relative convergence |
| 277 | + # again |
| 278 | + prev_est = curr_est |
| 279 | + num_curves = len(curves) |
| 280 | + for i in range(num_curves): |
| 281 | + curve = curves.popleft() |
| 282 | + new_curves = curve.split_at_t(0.5) |
| 283 | + max_err -= errs.popleft() |
| 284 | + curr_est -= lengths.popleft() |
| 285 | + for ncurve in new_curves: |
| 286 | + chord = np.linalg.norm( |
| 287 | + ncurve._cpoints[-1] - ncurve._cpoints[0]) |
| 288 | + net = ncurve.control_net_length() |
| 289 | + nerr = (net - chord)/2 |
| 290 | + nlength = chord + nerr |
| 291 | + max_err += nerr |
| 292 | + curr_est += nlength |
| 293 | + curves.append(ncurve) |
| 294 | + errs.append(nerr) |
| 295 | + lengths.append(nlength) |
| 296 | + if max_err < atol: |
| 297 | + return curr_est |
| 298 | + return curr_est |
| 299 | + |
209 | 300 | def arc_area(self):
|
210 | 301 | r"""
|
211 | 302 | (Signed) area swept out by ray from origin to curve.
|
|
0 commit comments