|
17 | 17 | import matplotlib as mpl
|
18 | 18 | from . import _path, cbook
|
19 | 19 | from .cbook import _to_unmasked_float_array, simple_linear_interpolation
|
| 20 | +from .bezier import BezierSegment |
| 21 | + |
| 22 | + |
| 23 | +def _update_extents(extents, point): |
| 24 | + dim = len(point) |
| 25 | + for i, xi in enumerate(point): |
| 26 | + if xi < extents[i]: |
| 27 | + extents[i] = xi |
| 28 | + # elif here would fail to correctly update from "null" extents of |
| 29 | + # np.array([np.inf, np.inf, -np.inf, -np.inf]) |
| 30 | + if extents[i+dim] < xi: |
| 31 | + extents[i+dim] = xi |
20 | 32 |
|
21 | 33 |
|
22 | 34 | class Path:
|
@@ -420,6 +432,53 @@ def iter_segments(self, transform=None, remove_nans=True, clip=None,
|
420 | 432 | curr_vertices = np.append(curr_vertices, next(vertices))
|
421 | 433 | yield curr_vertices, code
|
422 | 434 |
|
| 435 | + def iter_bezier(self, **kwargs): |
| 436 | + """ |
| 437 | + Iterate over each bezier curve (lines included) in a Path. |
| 438 | +
|
| 439 | + Parameters |
| 440 | + ---------- |
| 441 | + **kwargs |
| 442 | + Forwarded to `.iter_segments`. |
| 443 | +
|
| 444 | + Yields |
| 445 | + ------ |
| 446 | + B : matplotlib.bezier.BezierSegment |
| 447 | + The bezier curves that make up the current path. Note in particular |
| 448 | + that freestanding points are bezier curves of order 0, and lines |
| 449 | + are bezier curves of order 1 (with two control points). |
| 450 | + code : Path.code_type |
| 451 | + The code describing what kind of curve is being returned. |
| 452 | + Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE4 correspond to |
| 453 | + bezier curves with 1, 2, 3, and 4 control points (respectively). |
| 454 | + Path.CLOSEPOLY is a Path.LINETO with the control points correctly |
| 455 | + chosen based on the start/end points of the current stroke. |
| 456 | + """ |
| 457 | + first_vert = None |
| 458 | + prev_vert = None |
| 459 | + for verts, code in self.iter_segments(**kwargs): |
| 460 | + if first_vert is None: |
| 461 | + if code != Path.MOVETO: |
| 462 | + raise ValueError("Malformed path, must start with MOVETO.") |
| 463 | + if code == Path.MOVETO: # a point is like "CURVE1" |
| 464 | + first_vert = verts |
| 465 | + yield BezierSegment(np.array([first_vert])), code |
| 466 | + elif code == Path.LINETO: # "CURVE2" |
| 467 | + yield BezierSegment(np.array([prev_vert, verts])), code |
| 468 | + elif code == Path.CURVE3: |
| 469 | + yield BezierSegment(np.array([prev_vert, verts[:2], |
| 470 | + verts[2:]])), code |
| 471 | + elif code == Path.CURVE4: |
| 472 | + yield BezierSegment(np.array([prev_vert, verts[:2], |
| 473 | + verts[2:4], verts[4:]])), code |
| 474 | + elif code == Path.CLOSEPOLY: |
| 475 | + yield BezierSegment(np.array([prev_vert, first_vert])), code |
| 476 | + elif code == Path.STOP: |
| 477 | + return |
| 478 | + else: |
| 479 | + raise ValueError("Invalid Path.code_type: " + str(code)) |
| 480 | + prev_vert = verts[-2:] |
| 481 | + |
423 | 482 | @cbook._delete_parameter("3.3", "quantize")
|
424 | 483 | def cleaned(self, transform=None, remove_nans=False, clip=None,
|
425 | 484 | quantize=False, simplify=False, curves=False,
|
@@ -528,22 +587,39 @@ def contains_path(self, path, transform=None):
|
528 | 587 | transform = transform.frozen()
|
529 | 588 | return _path.path_in_path(self, None, path, transform)
|
530 | 589 |
|
531 |
| - def get_extents(self, transform=None): |
| 590 | + def get_extents(self, transform=None, **kwargs): |
532 | 591 | """
|
533 |
| - Return the extents (*xmin*, *ymin*, *xmax*, *ymax*) of the path. |
| 592 | + Get Bbox of the path. |
534 | 593 |
|
535 |
| - Unlike computing the extents on the *vertices* alone, this |
536 |
| - algorithm will take into account the curves and deal with |
537 |
| - control points appropriately. |
| 594 | + Parameters |
| 595 | + ---------- |
| 596 | + transform : matplotlib.transforms.Transform, optional |
| 597 | + Transform to apply to path before computing extents, if any. |
| 598 | + **kwargs |
| 599 | + Forwarded to `.iter_bezier`. |
| 600 | +
|
| 601 | + Returns |
| 602 | + ------- |
| 603 | + matplotlib.transforms.Bbox |
| 604 | + The extents of the path Bbox([[xmin, ymin], [xmax, ymax]]) |
538 | 605 | """
|
539 | 606 | from .transforms import Bbox
|
540 |
| - path = self |
541 | 607 | if transform is not None:
|
542 |
| - transform = transform.frozen() |
543 |
| - if not transform.is_affine: |
544 |
| - path = self.transformed(transform) |
545 |
| - transform = None |
546 |
| - return Bbox(_path.get_path_extents(path, transform)) |
| 608 | + self = transform.transform_path(self) |
| 609 | + # return value for empty paths to match what used to be done in _path.h |
| 610 | + extents = np.array([np.inf, np.inf, -np.inf, -np.inf]) |
| 611 | + for curve, code in self.iter_bezier(**kwargs): |
| 612 | + # start and endpoints can be extrema of the curve |
| 613 | + _update_extents(extents, curve.point_at_t(0)) # start point |
| 614 | + _update_extents(extents, curve.point_at_t(1)) # end point |
| 615 | + # interior extrema where d/ds B(s) == 0 |
| 616 | + _, dzeros = curve.axis_aligned_extrema |
| 617 | + if len(dzeros) == 0: |
| 618 | + continue |
| 619 | + for ti in dzeros: |
| 620 | + potential_extrema = curve.point_at_t(ti) |
| 621 | + _update_extents(extents, potential_extrema) |
| 622 | + return Bbox.from_extents(extents) |
547 | 623 |
|
548 | 624 | def intersects_path(self, other, filled=True):
|
549 | 625 | """
|
|
0 commit comments