From a1ba4764624f7f39fdb114de04cb066040b547f0 Mon Sep 17 00:00:00 2001 From: r3kste <138380708+r3kste@users.noreply.github.com> Date: Thu, 4 Dec 2025 22:19:00 +0530 Subject: [PATCH] Revamp `draw_path_collection()` API. Co-authored-by: Pranav Co-authored-by: AdwaithBatchu Co-authored-by: Sreekanth-M8 Co-authored-by: Sujal Kumar --- lib/matplotlib/backend_bases.py | 355 ++++++++++++++++++--- lib/matplotlib/backend_bases.pyi | 74 ++++- lib/matplotlib/backends/backend_pdf.py | 50 +-- lib/matplotlib/backends/backend_ps.py | 29 +- lib/matplotlib/backends/backend_svg.py | 31 +- lib/matplotlib/collections.py | 154 +++++---- lib/matplotlib/patheffects.py | 6 +- lib/matplotlib/patheffects.pyi | 4 +- lib/matplotlib/tests/test_backend_bases.py | 11 +- lib/matplotlib/tests/test_collections.py | 4 +- src/_backend_agg.h | 219 ++++++++----- src/_backend_agg_basic_types.h | 66 ++++ src/_backend_agg_wrapper.cpp | 54 +++- 13 files changed, 772 insertions(+), 285 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index fc7d651a6eb4..f65e1dd0271b 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -207,10 +207,10 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transforms.Affine2D().translate(x, y), rgbFace) - def draw_path_collection(self, gc, master_transform, paths, all_transforms, - offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, - offset_position, *, hatchcolors=None): + def draw_path_collection(self, vgc, master_transform, paths, all_transforms, + offsets, offset_trans, facecolors=None, edgecolors=None, + linewidths=None, linestyles=None, antialiaseds=None, + urls=None, offset_position=None, hatchcolors=None): """ Draw a collection of *paths*. @@ -243,10 +243,14 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, if hatchcolors is None: hatchcolors = [] + if isinstance(gc := vgc, GraphicsContextBase): + vgc = VectorizedGraphicsContextBase() + vgc.copy_properties(gc, facecolors, edgecolors, linewidths, linestyles, + antialiaseds, urls, hatchcolors) + for xo, yo, path_id, gc0, rgbFace in self._iter_collection( - gc, list(path_ids), offsets, offset_trans, - facecolors, edgecolors, linewidths, linestyles, - antialiaseds, urls, offset_position, hatchcolors=hatchcolors): + vgc, list(path_ids), offsets, offset_trans + ): path, transform = path_id # Only apply another translation if we have an offset, else we # reuse the initial transform. @@ -343,9 +347,7 @@ def _iter_collection_uses_per_path(self, paths, all_transforms, N = max(Npath_ids, len(offsets)) return (N + Npath_ids - 1) // Npath_ids - def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors, - edgecolors, linewidths, linestyles, - antialiaseds, urls, offset_position, *, hatchcolors): + def _iter_collection(self, vgc, path_ids, offsets, offset_trans): """ Helper method (along with `_iter_collection_raw_paths`) to implement `draw_path_collection` in a memory-efficient manner. @@ -371,18 +373,30 @@ def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors, Npaths = len(path_ids) Noffsets = len(offsets) N = max(Npaths, Noffsets) - Nfacecolors = len(facecolors) - Nedgecolors = len(edgecolors) - Nhatchcolors = len(hatchcolors) - Nlinewidths = len(linewidths) - Nlinestyles = len(linestyles) - Nurls = len(urls) + + Nalphas = len(vgc._alphas) + Nforced_alphas = len(vgc._forced_alphas) + Nantialiaseds = len(vgc._antialiaseds) + Ncapstyles = len(vgc._capstyles) + Ndashes = len(vgc._dashes) + Njoinstyles = len(vgc._joinstyles) + Nlinewidths = len(vgc._linewidths) + Nedgecolors = len(vgc._edgecolors) + Nfacecolors = len(vgc._facecolors) + Nhatches = len(vgc._hatches) + Nhatchcolors = len(vgc._hatchcolors) + Nhatch_linewidths = len(vgc._hatch_linewidths) + Nurls = len(vgc._urls) + Ngids = len(vgc._gids) + Nsnaps = len(vgc._snaps) + Nsketches = len(vgc._sketches) if (Nfacecolors == 0 and Nedgecolors == 0 and Nhatchcolors == 0) or Npaths == 0: return - gc0 = self.new_gc() - gc0.copy_properties(gc) + gc = self.new_gc() + gc.set_clip_path(vgc._clippath) + gc.set_clip_rectangle(vgc._cliprect) def cycle_or_default(seq, default=None): # Cycle over *seq* if it is not empty; else always yield *default*. @@ -391,39 +405,76 @@ def cycle_or_default(seq, default=None): pathids = cycle_or_default(path_ids) toffsets = cycle_or_default(offset_trans.transform(offsets), (0, 0)) - fcs = cycle_or_default(facecolors) - ecs = cycle_or_default(edgecolors) - hcs = cycle_or_default(hatchcolors) - lws = cycle_or_default(linewidths) - lss = cycle_or_default(linestyles) - aas = cycle_or_default(antialiaseds) - urls = cycle_or_default(urls) - if Nedgecolors == 0: - gc0.set_linewidth(0.0) + alphas = cycle_or_default(vgc._alphas) + forced_alphas = cycle_or_default(vgc._forced_alphas) + antialiaseds = cycle_or_default(vgc._antialiaseds) + capstyles = cycle_or_default(vgc._capstyles) + dashes = cycle_or_default(vgc._dashes) + joinstyles = cycle_or_default(vgc._joinstyles) + linewidths = cycle_or_default(vgc._linewidths) + edgecolors = cycle_or_default(vgc._edgecolors) + facecolors = cycle_or_default(vgc._facecolors) + hatches = cycle_or_default(vgc._hatches) + hatchcolors = cycle_or_default(vgc._hatchcolors) + hatch_linewidths = cycle_or_default(vgc._hatch_linewidths) + urls = cycle_or_default(vgc._urls) + gids = cycle_or_default(vgc._gids) + snaps = cycle_or_default(vgc._snaps) + sketches = cycle_or_default(vgc._sketches) - for pathid, (xo, yo), fc, ec, hc, lw, ls, aa, url in itertools.islice( - zip(pathids, toffsets, fcs, ecs, hcs, lws, lss, aas, urls), N): + if Nedgecolors == 0: + gc.set_linewidth(0.0) + + for (pathid, (xo, yo), alpha, forced_alpha, antialiased, capstyle, dash, + joinstyle, linewidth, edgecolor, facecolor, hatch, hatchcolor, + hatch_linewidth, url, gid, snap, sketch) in itertools.islice( + zip(pathids, toffsets, alphas, forced_alphas, antialiaseds, capstyles, + dashes, joinstyles, linewidths, edgecolors, facecolors, hatches, + hatchcolors, hatch_linewidths, urls, gids, snaps, sketches), + N): if not (np.isfinite(xo) and np.isfinite(yo)): continue + + if forced_alpha is False: + gc.set_alpha(None) + elif Nalphas: + gc.set_alpha(alpha) + if Nantialiaseds: + gc.set_antialiased(antialiased) + if Ncapstyles: + gc.set_capstyle(CapStyle(capstyle)) + if Njoinstyles: + gc.set_joinstyle(JoinStyle(joinstyle)) if Nedgecolors: if Nlinewidths: - gc0.set_linewidth(lw) - if Nlinestyles: - gc0.set_dashes(*ls) - if len(ec) == 4 and ec[3] == 0.0: - gc0.set_linewidth(0) + gc.set_linewidth(linewidth) + if Ndashes: + gc.set_dashes(*dash) + if len(edgecolor) == 4 and edgecolor[3] == 0.0: + gc.set_linewidth(0) else: - gc0.set_foreground(ec) + gc.set_foreground(edgecolor) + if facecolor is not None and len(facecolor) == 4 and facecolor[3] == 0: + facecolor = None + if Nhatches: + gc.set_hatch(hatch) if Nhatchcolors: - gc0.set_hatch_color(hc) - if fc is not None and len(fc) == 4 and fc[3] == 0: - fc = None - gc0.set_antialiased(aa) + gc.set_hatch_color(hatchcolor) + if Nhatch_linewidths: + gc.set_hatch_linewidth(hatch_linewidth) if Nurls: - gc0.set_url(url) - yield xo, yo, pathid, gc0, fc - gc0.restore() + gc.set_url(url) + if Ngids: + gc.set_gid(gid) + if Nsnaps: + gc.set_snap(snap) + if Nsketches: + if sketch is not None: + gc.set_sketch_params(*sketch) + + yield xo, yo, pathid, gc, facecolor + gc.restore() def get_image_magnification(self): """ @@ -620,6 +671,9 @@ def new_gc(self): """Return an instance of a `.GraphicsContextBase`.""" return GraphicsContextBase() + def new_vgc(self): + return VectorizedGraphicsContextBase() + def points_to_pixels(self, points): """ Convert points to display units. @@ -1021,6 +1075,223 @@ def set_sketch_params(self, scale=None, length=None, randomness=None): else (scale, length or 128., randomness or 16.)) +class VectorizedGraphicsContextBase: + def __init__(self): + self._alphas = [] + self._forced_alphas = [] + self._antialiaseds = [] + self._capstyles = [] + self._cliprect = None + self._clippath = None + self._dashes = [] + self._joinstyles = [] + self._linewidths = [] + self._edgecolors = [] + self._facecolors = [] + self._hatches = [] + self._hatchcolors = [] + self._hatch_linewidths = [] + self._urls = [] + self._gids = [] + self._snaps = [] + self._sketches = [None] + + def copy_properties(self, gc, facecolors, edgecolors, linewidths, linestyles, + antialiaseds, urls, hatchcolors): + self._alphas = [gc._alpha] + self._forced_alphas = [gc._forced_alpha] + self._antialiaseds = antialiaseds + self._capstyles = [gc._capstyle] + self._cliprect = gc._cliprect + self._clippath = gc._clippath + self._dashes = linestyles + self._joinstyles = [gc._joinstyle] + self._linewidths = linewidths + self._edgecolors = edgecolors + self._facecolors = facecolors + self._hatches = [gc._hatch] + self._hatchcolors = hatchcolors + self._hatch_linewidths = [gc._hatch_linewidth] + self._urls = urls + self._gids = [gc._gid] + self._snaps = [gc._snap] + self._sketches = [gc._sketch] + + def get_alphas(self): + return self._alphas + + def get_forced_alphas(self): + return self._forced_alphas + + def get_antialiaseds(self): + return self._antialiaseds + + def get_capstyles(self): + return self._capstyles + + def get_clip_rectangle(self): + return self._cliprect + + def get_clip_path(self): + if self._clippath is not None: + tpath, tr = self._clippath.get_transformed_path_and_affine() + if np.all(np.isfinite(tpath.vertices)): + return tpath, tr + else: + _log.warning("Ill-defined clip_path detected. Returning None.") + return None, None + return None, None + + def get_dashes(self): + return self._dashes + + def get_joinstyles(self): + return self._joinstyles + + def get_linewidths(self): + return self._linewidths + + def get_edgecolors(self): + return self._edgecolors + + def get_facecolors(self): + return self._facecolors + + def get_hatches(self): + return self._hatches + + def get_hatch_paths(self, density=6.0): + hatches = self.get_hatches() + if hatches == []: + return [] + hatch_paths = [None] * len(hatches) + for i, hatch in enumerate(hatches): + hatch_paths[i] = Path.hatch(hatch, density) + return hatch_paths + + def get_hatch_colors(self): + return self._hatchcolors + + def get_hatch_linewidths(self): + return self._hatch_linewidths + + def get_urls(self): + return self._urls + + def get_gids(self): + return self._gids + + def get_snaps(self): + return self._snaps + + def get_sketches_params(self): + return self._sketches + + def set_alphas(self, alphas): + n = len(alphas) + new_alphas = [None] * n + new_forced_alphas = [None] * n + for i, alpha in enumerate(alphas): + if alpha is not None: + new_alphas[i] = alpha + new_forced_alphas[i] = True + else: + new_alphas[i] = 1.0 + new_forced_alphas[i] = False + self._alphas = new_alphas.copy() + self._forced_alphas = new_forced_alphas.copy() + + isRGBA = [True] * n + self.set_edgecolors(self._edgecolors, isRGBA=isRGBA) + + def set_antialiaseds(self, antialiaseds): + self._antialiaseds = [int(bool(b)) for b in antialiaseds] + + def set_capstyles(self, capstyles): + self._capstyles = [CapStyle(cs) for cs in capstyles] + + def set_clip_rectangle(self, rectangle): + self._cliprect = rectangle + + def set_clip_path(self, path): + _api.check_isinstance((transforms.TransformedPath, None), path=path) + self._clippath = path + + def set_dashes(self, dashes): + n = len(dashes) + new_dashes = [None] * n + for i, (dash_offset, dash_list) in enumerate(dashes): + if dash_list is not None: + dl = np.asarray(dash_list) + if np.any(dl < 0.0): + raise ValueError( + "All values in the dash list must be non-negative.") + if dl.size and not np.any(dl > 0.0): + raise ValueError( + "At least one value in the dash list must be positive.") + new_dashes[i] = (dash_offset, dash_list) + self._dashes = new_dashes.copy() + + def set_joinstyles(self, joinstyles): + self._joinstyles = [JoinStyle(js) for js in joinstyles] + + def set_linewidths(self, linewidths): + self._linewidths = linewidths.copy() + + def set_edgecolors(self, edgecolors, isRGBA=None): + n = len(edgecolors) + if isRGBA is None: + isRGBA = [False] * n + elif len(isRGBA) < n: + isRGBA += [False] * (n - len(isRGBA)) + + if len(self._forced_alphas) < n: + self._forced_alphas += [False] * (n - len(self._forced_alphas)) + + new_edgecolors = [None] * n + for i, edgecolor in enumerate(edgecolors): + if self._forced_alphas[i] and isRGBA[i]: + new_edgecolors[i] = edgecolor[:3] + (self._alphas[i],) + elif self._forced_alphas[i]: + new_edgecolors[i] = colors.to_rgba(edgecolor, self._alphas[i]) + elif isRGBA[i]: + new_edgecolors[i] = edgecolor + else: + new_edgecolors[i] = colors.to_rgba(edgecolor) + self._edgecolors = new_edgecolors.copy() + + def set_facecolors(self, facecolors): + self._facecolors = facecolors.copy() + + def set_urls(self, urls): + self._urls = urls.copy() + + def set_gids(self, gids): + self._gids = gids.copy() + + def set_snaps(self, snaps): + self._snaps = snaps.copy() + + def set_hatches(self, hatches): + self._hatches = hatches.copy() + + def set_hatch_colors(self, hatchcolors): + self._hatchcolors = hatchcolors.copy() + + def set_hatch_linewidths(self, hatch_linewidths): + self._hatch_linewidths = hatch_linewidths.copy() + + def set_sketches_params(self, sketches): + n = len(sketches) + new_sketches = [None] * n + for i, (scale, length, randomness) in enumerate(sketches): + if scale is None: + new_sketches[i] = None + else: + new_sketches[i] = (scale, length or 128.0, randomness or 16.0) + self._sketches = new_sketches.copy() + + class TimerBase: """ A base class for providing timer events, useful for things animations. diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index 7a2b28262249..c1ee396ad138 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -61,21 +61,20 @@ class RendererBase: ) -> None: ... def draw_path_collection( self, - gc: GraphicsContextBase, + vgc: GraphicsContextBase | VectorizedGraphicsContextBase, master_transform: Transform, paths: Sequence[Path], all_transforms: Sequence[ArrayLike], offsets: ArrayLike | Sequence[ArrayLike], offset_trans: Transform, - facecolors: ColorType | Sequence[ColorType], - edgecolors: ColorType | Sequence[ColorType], - linewidths: float | Sequence[float], - linestyles: LineStyleType | Sequence[LineStyleType], - antialiaseds: bool | Sequence[bool], - urls: str | Sequence[str], - offset_position: Any, - *, - hatchcolors: ColorType | Sequence[ColorType] | None = None, + facecolors: ColorType | Sequence[ColorType] | None = ..., + edgecolors: ColorType | Sequence[ColorType] | None = ..., + linewidths: float | Sequence[float] | None = ..., + linestyles: LineStyleType | Sequence[LineStyleType] | None = ..., + antialiaseds: bool | Sequence[bool] | None = ..., + urls: str | Sequence[str] | None = ..., + offset_position: Any = ..., + hatchcolors: ColorType | Sequence[ColorType] | None = ..., ) -> None: ... def draw_quad_mesh( self, @@ -137,6 +136,7 @@ class RendererBase: def get_canvas_width_height(self) -> tuple[float, float]: ... def get_texmanager(self) -> TexManager: ... def new_gc(self) -> GraphicsContextBase: ... + def new_vgc(self) -> VectorizedGraphicsContextBase: ... def points_to_pixels(self, points: ArrayLike) -> ArrayLike: ... def start_rasterizing(self) -> None: ... def stop_rasterizing(self) -> None: ... @@ -189,6 +189,60 @@ class GraphicsContextBase: randomness: float | None = ..., ) -> None: ... +class VectorizedGraphicsContextBase: + def __init__(self) -> None: ... + def copy_properties( + self, + gc: GraphicsContextBase, + facecolors: ColorType | Sequence[ColorType] | None, + edgecolors: ColorType | Sequence[ColorType] | None, + linewidths: float | Sequence[float] | None, + linestyles: LineStyleType | Sequence[LineStyleType] | None, + antialiaseds: bool | Sequence[bool] | None, + urls: str | Sequence[str] | None, + hatchcolors: ColorType | Sequence[ColorType] | None, + ) -> None: ... + def get_alphas(self) -> list[float]: ... + def get_forced_alphas(self) -> list[bool]: ... + def get_antialiaseds(self) -> list[int]: ... + def get_capstyles(self) -> list[Literal["butt", "projecting", "round"]]: ... + def get_clip_rectangle(self) -> Bbox | None: ... + def get_clip_path( + self, + ) -> tuple[TransformedPath, Transform] | tuple[None, None]: ... + def get_dashes(self) -> list[LineStyleType]: ... + def get_joinstyles(self) -> list[Literal["miter", "round", "bevel"]]: ... + def get_linewidths(self) -> list[float]: ... + def get_edgecolors(self) -> list[ColorType]: ... + def get_facecolors(self) -> list[ColorType]: ... + def get_hatches(self) -> list[str | None]: ... + def get_hatch_paths(self, density: float = ...) -> list[Path]: ... + def get_hatch_colors(self) -> list[ColorType]: ... + def get_hatch_linewidths(self) -> list[float]: ... + def get_urls(self) -> list[str | None]: ... + def get_gids(self) -> list[int | None]: ... + def get_snaps(self) -> list[bool | None]: ... + def get_sketches_params(self) -> list[tuple[float, float, float] | None]: ... + def set_alphas(self, alphas: list[float]) -> None: ... + def set_antialiaseds(self, antialiaseds: list[int]) -> None: ... + def set_capstyles(self, capstyles: list[CapStyleType]) -> None: ... + def set_clip_rectangle(self, rectangle: Bbox | None) -> None: ... + def set_clip_path(self, path: TransformedPath | None) -> None: ... + def set_dashes(self, dashes: list[tuple[float, ArrayLike | None]]) -> None: ... + def set_joinstyles(self, joinstyles: list[JoinStyleType]) -> None: ... + def set_linewidths(self, linewidths: list[float]) -> None: ... + def set_edgecolors(self, edgecolors: list[ColorType], isRGBA: None = ...) -> None: ... + def set_facecolors(self, facecolors: list[ColorType]) -> None: ... + def set_urls(self, urls: list[str | None]) -> None: ... + def set_gids(self, gids: list[int | None]) -> None: ... + def set_snaps(self, snaps: list[bool | None]) -> None: ... + def set_hatches(self, hatches: list[str | None]) -> None: ... + def set_hatch_colors(self, hatchcolors: list[ColorType]) -> None: ... + def set_hatch_linewidths(self, hatch_linewidths: list[float]) -> None: ... + def set_sketches_params( + self, sketches: list[tuple[float | None, float | None, float | None]] + ) -> None: ... + class TimerBase: callbacks: list[tuple[Callable, tuple, dict[str, Any]]] def __init__( diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index d63808eb3925..565588dcd3a5 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -30,7 +30,7 @@ from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, - RendererBase) + RendererBase, VectorizedGraphicsContextBase) from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.figure import Figure from matplotlib.font_manager import get_font, fontManager as _fontManager @@ -2052,23 +2052,29 @@ def draw_path(self, gc, path, transform, rgbFace=None): gc.get_sketch_params()) self.file.output(self.gc.paint()) - def draw_path_collection(self, gc, master_transform, paths, all_transforms, - offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, - offset_position, *, hatchcolors=None): + def draw_path_collection(self, vgc, master_transform, paths, all_transforms, + offsets, offset_trans, facecolors=None, edgecolors=None, + linewidths=None, linestyles=None, antialiaseds=None, + urls=None, offset_position=None, hatchcolors=None): + + if hatchcolors is None: + hatchcolors = [] + + if isinstance(gc := vgc, GraphicsContextBase): + vgc = VectorizedGraphicsContextBase() + vgc.copy_properties(gc, facecolors, edgecolors, linewidths, linestyles, + antialiaseds, urls, hatchcolors) + # We can only reuse the objects if the presence of fill and # stroke (and the amount of alpha for each) is the same for # all of them can_do_optimization = True - facecolors = np.asarray(facecolors) - edgecolors = np.asarray(edgecolors) - - if hatchcolors is None: - hatchcolors = [] + facecolors = np.asarray(vgc.get_facecolors()) + edgecolors = np.asarray(vgc.get_edgecolors()) if not len(facecolors): filled = False - can_do_optimization = not gc.get_hatch() + can_do_optimization = not vgc.get_hatches() else: if np.all(facecolors[:, 3] == facecolors[0, 3]): filled = facecolors[0, 3] != 0.0 @@ -2091,21 +2097,27 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, # uses_per_path for the uses len_path = len(paths[0].vertices) if len(paths) > 0 else 0 uses_per_path = self._iter_collection_uses_per_path( - paths, all_transforms, offsets, facecolors, edgecolors) + paths, all_transforms, offsets, vgc.get_facecolors(), vgc.get_edgecolors()) should_do_optimization = \ len_path + uses_per_path + 5 < len_path * uses_per_path if (not can_do_optimization) or (not should_do_optimization): return RendererBase.draw_path_collection( - self, gc, master_transform, paths, all_transforms, - offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, - offset_position, hatchcolors=hatchcolors) + self, vgc, master_transform, paths, all_transforms, + offsets, offset_trans) + + Njoinstyles = len(vgc._joinstyles) + Ncapstyles = len(vgc._capstyles) - padding = np.max(linewidths) + padding = np.max(vgc.get_linewidths()) path_codes = [] + gc = self.new_gc() for i, (path, transform) in enumerate(self._iter_collection_raw_paths( master_transform, paths, all_transforms)): + if Njoinstyles: + gc.set_joinstyle(vgc.get_joinstyles()[i % Njoinstyles]) + if Ncapstyles: + gc.set_capstyle(vgc.get_capstyles()[i % Ncapstyles]) name = self.file.pathCollectionObject( gc, path, transform, padding, filled, stroked) path_codes.append(name) @@ -2114,9 +2126,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, output(*self.gc.push()) lastx, lasty = 0, 0 for xo, yo, path_id, gc0, rgbFace in self._iter_collection( - gc, path_codes, offsets, offset_trans, - facecolors, edgecolors, linewidths, linestyles, - antialiaseds, urls, offset_position, hatchcolors=hatchcolors): + vgc, path_codes, offsets, offset_trans): self.check_gc(gc0, rgbFace) dx, dy = xo - lastx, yo - lasty diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index ea5868387918..84b900f214d0 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -25,7 +25,8 @@ import matplotlib as mpl from matplotlib import _api, cbook, _path, _text_helpers from matplotlib.backend_bases import ( - _Backend, FigureCanvasBase, FigureManagerBase, RendererBase) + _Backend, FigureCanvasBase, FigureManagerBase, RendererBase, GraphicsContextBase, + VectorizedGraphicsContextBase) from matplotlib.cbook import is_writable_file_like, file_requires_unicode from matplotlib.font_manager import get_font from matplotlib.ft2font import LoadFlags @@ -670,12 +671,18 @@ def draw_markers( self._draw_ps(ps, gc, rgbFace, fill=False, stroke=False) @_log_if_debug_on - def draw_path_collection(self, gc, master_transform, paths, all_transforms, - offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, - offset_position, *, hatchcolors=None): + def draw_path_collection(self, vgc, master_transform, paths, all_transforms, + offsets, offset_trans, facecolors=None, edgecolors=None, + linewidths=None, linestyles=None, antialiaseds=None, + urls=None, offset_position=None, hatchcolors=None): if hatchcolors is None: hatchcolors = [] + + if isinstance(gc := vgc, GraphicsContextBase): + vgc = VectorizedGraphicsContextBase() + vgc.copy_properties(gc, facecolors, edgecolors, linewidths, linestyles, + antialiaseds, urls, hatchcolors) + # Is the optimization worth it? Rough calculation: # cost of emitting a path in-line is # (len_path + 2) * uses_per_path @@ -683,15 +690,13 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, # (len_path + 3) + 3 * uses_per_path len_path = len(paths[0].vertices) if len(paths) > 0 else 0 uses_per_path = self._iter_collection_uses_per_path( - paths, all_transforms, offsets, facecolors, edgecolors) + paths, all_transforms, offsets, vgc.get_facecolors(), vgc.get_edgecolors()) should_do_optimization = \ len_path + 3 * uses_per_path + 3 < (len_path + 2) * uses_per_path if not should_do_optimization: return RendererBase.draw_path_collection( - self, gc, master_transform, paths, all_transforms, - offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, - offset_position, hatchcolors=hatchcolors) + self, vgc, master_transform, paths, all_transforms, + offsets, offset_trans) path_codes = [] for i, (path, transform) in enumerate(self._iter_collection_raw_paths( @@ -708,9 +713,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, path_codes.append(name) for xo, yo, path_id, gc0, rgbFace in self._iter_collection( - gc, path_codes, offsets, offset_trans, - facecolors, edgecolors, linewidths, linestyles, - antialiaseds, urls, offset_position, hatchcolors=hatchcolors): + vgc, path_codes, offsets, offset_trans): ps = f"{xo:g} {yo:g} {path_id}" self._draw_ps(ps, gc0, rgbFace) diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 0cb6430ec823..f9adf5e39c9c 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -16,7 +16,8 @@ import matplotlib as mpl from matplotlib import cbook, font_manager as fm from matplotlib.backend_bases import ( - _Backend, FigureCanvasBase, FigureManagerBase, RendererBase) + _Backend, FigureCanvasBase, FigureManagerBase, RendererBase, GraphicsContextBase, + VectorizedGraphicsContextBase) from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.colors import rgb2hex from matplotlib.dates import UTC @@ -733,12 +734,19 @@ def draw_markers( self.writer.end('a') writer.end('g') - def draw_path_collection(self, gc, master_transform, paths, all_transforms, - offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, - offset_position, *, hatchcolors=None): + def draw_path_collection(self, vgc, master_transform, paths, all_transforms, + offsets, offset_trans, facecolors=None, edgecolors=None, + linewidths=None, linestyles=None, antialiaseds=None, + urls=None, offset_position=None, hatchcolors=None): + if hatchcolors is None: hatchcolors = [] + + if isinstance(gc := vgc, GraphicsContextBase): + vgc = VectorizedGraphicsContextBase() + vgc.copy_properties(gc, facecolors, edgecolors, linewidths, linestyles, + antialiaseds, urls, hatchcolors) + # Is the optimization worth it? Rough calculation: # cost of emitting a path in-line is # (len_path + 5) * uses_per_path @@ -746,15 +754,14 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, # (len_path + 3) + 9 * uses_per_path len_path = len(paths[0].vertices) if len(paths) > 0 else 0 uses_per_path = self._iter_collection_uses_per_path( - paths, all_transforms, offsets, facecolors, edgecolors) + paths, all_transforms, offsets, vgc.get_facecolors(), vgc.get_edgecolors()) should_do_optimization = \ len_path + 9 * uses_per_path + 3 < (len_path + 5) * uses_per_path + if not should_do_optimization: return super().draw_path_collection( - gc, master_transform, paths, all_transforms, - offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, - offset_position, hatchcolors=hatchcolors) + vgc, master_transform, paths, all_transforms, + offsets, offset_trans) writer = self.writer path_codes = [] @@ -770,9 +777,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, writer.end('defs') for xo, yo, path_id, gc0, rgbFace in self._iter_collection( - gc, path_codes, offsets, offset_trans, - facecolors, edgecolors, linewidths, linestyles, - antialiaseds, urls, offset_position, hatchcolors=hatchcolors): + vgc, path_codes, offsets, offset_trans): url = gc0.get_url() if url is not None: writer.start('a', attrib={'xlink:href': url}) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 684e15cdf854..6193938191b9 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -364,17 +364,6 @@ def draw(self, renderer): transform, offset_trf, offsets, paths = self._prepare_points() - gc = renderer.new_gc() - self._set_gc_clip(gc) - gc.set_snap(self.get_snap()) - - if self._hatch: - gc.set_hatch(self._hatch) - gc.set_hatch_linewidth(self._hatch_linewidth) - - if self.get_sketch_params() is not None: - gc.set_sketch_params(*self.get_sketch_params()) - if self.get_path_effects(): from matplotlib.patheffects import PathEffectRenderer renderer = PathEffectRenderer(self.get_path_effects(), renderer) @@ -404,13 +393,24 @@ def draw(self, renderer): and extents.height < self.get_figure(root=True).bbox.height): do_single_path_optimization = True - if self._joinstyle: - gc.set_joinstyle(self._joinstyle) + if do_single_path_optimization: + gc = renderer.new_gc() + self._set_gc_clip(gc) + gc.set_snap(self.get_snap()) - if self._capstyle: - gc.set_capstyle(self._capstyle) + if self._hatch: + gc.set_hatch(self._hatch) + gc.set_hatch_linewidth(self._hatch_linewidth) + + if self.get_sketch_params() is not None: + gc.set_sketch_params(*self.get_sketch_params()) + + if self._joinstyle: + gc.set_joinstyle(self._joinstyle) + + if self._capstyle: + gc.set_capstyle(self._capstyle) - if do_single_path_optimization: gc.set_foreground(tuple(edgecolors[0])) gc.set_linewidth(self._linewidths[0]) gc.set_dashes(*self._linestyles[0]) @@ -419,81 +419,53 @@ def draw(self, renderer): renderer.draw_markers( gc, paths[0], combined_transform.frozen(), mpath.Path(offsets), offset_trf, tuple(facecolors[0])) + gc.restore() else: - # The current new API of draw_path_collection() is provisional - # and will be changed in a future PR. - - # Find whether renderer.draw_path_collection() takes hatchcolor parameter. - # Since third-party implementations of draw_path_collection() may not be - # introspectable, e.g. with inspect.signature, the only way is to try and - # call this with the hatchcolors parameter. - hatchcolors_arg_supported = True + vgc = renderer.new_vgc() + + vgc_supported = True try: - renderer.draw_path_collection( - gc, transform.frozen(), [], - self.get_transforms(), offsets, offset_trf, - self.get_facecolor(), self.get_edgecolor(), - self._linewidths, self._linestyles, - self._antialiaseds, self._urls, - "screen", hatchcolors=self.get_hatchcolor() - ) + renderer.draw_path_collection(vgc, transform.frozen(), [], + self.get_transforms(), offsets, + offset_trf) except TypeError: - # If the renderer does not support the hatchcolors argument, - # it will raise a TypeError. In this case, we will - # iterate over all paths and draw them one by one. - hatchcolors_arg_supported = False + vgc_supported = False + + self._set_gc_clip(vgc) + vgc.set_snaps([self.get_snap()]) + + if self._hatch: + vgc.set_hatches([self.get_hatch()]) + vgc.set_hatch_linewidths([self.get_hatch_linewidth()]) + vgc.set_hatch_colors(self.get_hatchcolor()) - # If the hatchcolors argument is not needed or not passed - # then we can skip the iteration over paths in case the - # argument is not supported by the renderer. - hatchcolors_not_needed = (self.get_hatch() is None or - self._original_hatchcolor is None) + if self.get_sketch_params() is not None: + vgc.set_sketches_params([self.get_sketch_params()]) + + if self._joinstyle: + vgc.set_joinstyles([self._joinstyle]) + if self._capstyle: + vgc.set_capstyles([self._capstyle]) + + vgc.set_linewidths(np.atleast_1d(self.get_linewidth())) + vgc.set_antialiaseds(self.get_antialiased()) + vgc.set_urls(self.get_urls()) if self._gapcolor is not None: - # First draw paths within the gaps. ipaths, ilinestyles = self._get_inverse_paths_linestyles() - args = [offsets, offset_trf, [mcolors.to_rgba("none")], self._gapcolor, - self._linewidths, ilinestyles, self._antialiaseds, self._urls, - "screen"] - - if hatchcolors_arg_supported: - renderer.draw_path_collection(gc, transform.frozen(), ipaths, - self.get_transforms(), *args, - hatchcolors=self.get_hatchcolor()) - else: - if hatchcolors_not_needed: - renderer.draw_path_collection(gc, transform.frozen(), ipaths, - self.get_transforms(), *args) - else: - path_ids = renderer._iter_collection_raw_paths( - transform.frozen(), ipaths, self.get_transforms()) - for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection( - gc, list(path_ids), *args, - hatchcolors=self.get_hatchcolor(), - ): - path, transform = path_id - if xo != 0 or yo != 0: - transform = transform.frozen() - transform.translate(xo, yo) - renderer.draw_path(gc0, path, transform, rgbFace) - - args = [offsets, offset_trf, self.get_facecolor(), self.get_edgecolor(), - self._linewidths, self._linestyles, self._antialiaseds, self._urls, - "screen"] - - if hatchcolors_arg_supported: - renderer.draw_path_collection(gc, transform.frozen(), paths, - self.get_transforms(), *args, - hatchcolors=self.get_hatchcolor()) - else: - if hatchcolors_not_needed: - renderer.draw_path_collection(gc, transform.frozen(), paths, - self.get_transforms(), *args) + vgc.set_facecolors([mcolors.to_rgba("none")]) + vgc.set_edgecolors(self._gapcolor) + vgc.set_dashes(ilinestyles) + + if vgc_supported: + renderer.draw_path_collection(vgc, transform.frozen(), ipaths, + self.get_transforms(), offsets, + offset_trf) else: path_ids = renderer._iter_collection_raw_paths( - transform.frozen(), paths, self.get_transforms()) + transform.frozen(), ipaths, self.get_transforms()) for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection( - gc, list(path_ids), *args, hatchcolors=self.get_hatchcolor(), + vgc, list(path_ids), offsets, offset_trf ): path, transform = path_id if xo != 0 or yo != 0: @@ -501,7 +473,25 @@ def draw(self, renderer): transform.translate(xo, yo) renderer.draw_path(gc0, path, transform, rgbFace) - gc.restore() + vgc.set_facecolors(self.get_facecolor()) + vgc.set_edgecolors(self.get_edgecolor()) + vgc.set_dashes(self.get_linestyle()) + + if vgc_supported: + renderer.draw_path_collection(vgc, transform.frozen(), paths, + self.get_transforms(), offsets, offset_trf) + else: + path_ids = renderer._iter_collection_raw_paths( + transform.frozen(), paths, self.get_transforms()) + for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection( + vgc, list(path_ids), offsets, offset_trf + ): + path, transform = path_id + if xo != 0 or yo != 0: + transform = transform.frozen() + transform.translate(xo, yo) + renderer.draw_path(gc0, path, transform, rgbFace) + renderer.close_group(self.__class__.__name__) self.stale = False diff --git a/lib/matplotlib/patheffects.py b/lib/matplotlib/patheffects.py index e12713e2796e..9f177fb3b934 100644 --- a/lib/matplotlib/patheffects.py +++ b/lib/matplotlib/patheffects.py @@ -126,7 +126,7 @@ def draw_markers( renderer.draw_markers(gc, marker_path, marker_trans, path, *args, **kwargs) - def draw_path_collection(self, gc, master_transform, paths, *args, + def draw_path_collection(self, vgc, master_transform, paths, *args, **kwargs): # We do a little shimmy so that all paths are drawn for each path # effect in turn. Essentially, we induce recursion (depth 1) which is @@ -134,14 +134,14 @@ def draw_path_collection(self, gc, master_transform, paths, *args, if len(self._path_effects) == 1: # Call the base path effect function - this uses the unoptimised # approach of calling "draw_path" multiple times. - return super().draw_path_collection(gc, master_transform, paths, + return super().draw_path_collection(vgc, master_transform, paths, *args, **kwargs) for path_effect in self._path_effects: renderer = self.copy_with_path_effect([path_effect]) # Recursively call this method, only next time we will only have # one path effect. - renderer.draw_path_collection(gc, master_transform, paths, + renderer.draw_path_collection(vgc, master_transform, paths, *args, **kwargs) def open_group(self, s, gid=None): diff --git a/lib/matplotlib/patheffects.pyi b/lib/matplotlib/patheffects.pyi index 2c1634ca9314..47d90fff1e90 100644 --- a/lib/matplotlib/patheffects.pyi +++ b/lib/matplotlib/patheffects.pyi @@ -1,7 +1,7 @@ from collections.abc import Iterable, Sequence from typing import Any -from matplotlib.backend_bases import RendererBase, GraphicsContextBase +from matplotlib.backend_bases import RendererBase, GraphicsContextBase, VectorizedGraphicsContextBase from matplotlib.path import Path from matplotlib.patches import Patch from matplotlib.transforms import Transform @@ -42,7 +42,7 @@ class PathEffectRenderer(RendererBase): ) -> None: ... def draw_path_collection( self, - gc: GraphicsContextBase, + vgc: GraphicsContextBase | VectorizedGraphicsContextBase, master_transform: Transform, paths: Sequence[Path], *args, diff --git a/lib/matplotlib/tests/test_backend_bases.py b/lib/matplotlib/tests/test_backend_bases.py index 0205eac42fb3..57efd8dce26e 100644 --- a/lib/matplotlib/tests/test_backend_bases.py +++ b/lib/matplotlib/tests/test_backend_bases.py @@ -32,13 +32,14 @@ def check(master_transform, paths, all_transforms, rb = RendererBase() raw_paths = list(rb._iter_collection_raw_paths( master_transform, paths, all_transforms)) - gc = rb.new_gc() + vgc = rb.new_vgc() + vgc._edgecolors = edgecolors + vgc._facecolors = facecolors + vgc._antialiaseds = [False] ids = [path_id for xo, yo, path_id, gc0, rgbFace in rb._iter_collection( - gc, range(len(raw_paths)), offsets, - transforms.AffineDeltaTransform(master_transform), - facecolors, edgecolors, [], [], [False], - [], 'screen', hatchcolors=[])] + vgc, range(len(raw_paths)), offsets, + transforms.AffineDeltaTransform(master_transform))] uses = rb._iter_collection_uses_per_path( paths, all_transforms, offsets, facecolors, edgecolors) if raw_paths: diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index c062e8c12b9c..d7b541895c28 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -1515,13 +1515,13 @@ def test_draw_path_collection_no_hatchcolor(backend): ) -def test_third_party_backend_hatchcolors_arg_fallback(monkeypatch): +def test_draw_path_collection_third_party_backend_fallback(monkeypatch): fig, ax = plt.subplots() canvas = fig.canvas renderer = canvas.get_renderer() # monkeypatch the `draw_path_collection` method to simulate a third-party backend - # that does not support the `hatchcolors` argument. + # that uses the old draw_path_collection signature. def mock_draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, diff --git a/src/_backend_agg.h b/src/_backend_agg.h index 1ac3d4c06b13..0ff93d6d1fb4 100644 --- a/src/_backend_agg.h +++ b/src/_backend_agg.h @@ -42,6 +42,7 @@ #include "_backend_agg_basic_types.h" #include "path_converters.h" +#include "py_converters.h" #include "array.h" #include "agg_workaround.h" @@ -177,6 +178,16 @@ class RendererAgg AntialiasedArray &antialiaseds, ColorArray &hatchcolors); + template + void draw_path_collection(VGCAgg &vgc, + agg::trans_affine &master_transform, + PathGenerator &path, + TransformArray &transforms, + OffsetArray &offsets, + agg::trans_affine &offset_trans); + template void draw_quad_mesh(GCAgg &gc, agg::trans_affine &master_transform, @@ -248,30 +259,17 @@ class RendererAgg template void _draw_path(PathIteratorType &path, bool has_clippath, const std::optional &face, GCAgg &gc); - template - void _draw_path_collection_generic(GCAgg &gc, + class OffsetArray> + void _draw_path_collection_generic(VGCAgg &vgc, agg::trans_affine master_transform, - const agg::rect_d &cliprect, - PathIterator &clippath, - const agg::trans_affine &clippath_trans, PathGenerator &path_generator, TransformArray &transforms, OffsetArray &offsets, const agg::trans_affine &offset_trans, - ColorArray &facecolors, - ColorArray &edgecolors, - LineWidthArray &linewidths, - DashesVector &linestyles, - AntialiasedArray &antialiaseds, bool check_snap, - bool has_codes, - ColorArray &hatchcolors); + bool has_codes); template void _draw_gouraud_triangle(PointArray &points, @@ -880,30 +878,17 @@ inline void RendererAgg::draw_image(GCAgg &gc, rendererBase.reset_clipping(true); } -template -inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, + class OffsetArray> +inline void RendererAgg::_draw_path_collection_generic(VGCAgg &vgc, agg::trans_affine master_transform, - const agg::rect_d &cliprect, - PathIterator &clippath, - const agg::trans_affine &clippath_trans, PathGenerator &path_generator, TransformArray &transforms, OffsetArray &offsets, const agg::trans_affine &offset_trans, - ColorArray &facecolors, - ColorArray &edgecolors, - LineWidthArray &linewidths, - DashesVector &linestyles, - AntialiasedArray &antialiaseds, bool check_snap, - bool has_codes, - ColorArray &hatchcolors) + bool has_codes) { typedef agg::conv_transform transformed_path_t; typedef PathNanRemover nan_removed_t; @@ -920,13 +905,30 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, size_t Noffsets = safe_first_shape(offsets); size_t N = std::max(Npaths, Noffsets); + auto facecolors = convert_colors(vgc.facecolors); + auto edgecolors = convert_colors(vgc.edgecolors); + auto hatch_colors = convert_colors(vgc.hatch_colors); + auto alphas = vgc.alphas.unchecked<1>(); + auto forced_alphas = vgc.forced_alphas.unchecked<1>(); + auto antialiaseds = vgc.antialiaseds.unchecked<1>(); + auto linewidths = vgc.linewidths.unchecked<1>(); + auto hatch_linewidths = vgc.hatch_linewidths.unchecked<1>(); + size_t Ntransforms = safe_first_shape(transforms); - size_t Nfacecolors = safe_first_shape(facecolors); - size_t Nedgecolors = safe_first_shape(edgecolors); - size_t Nhatchcolors = safe_first_shape(hatchcolors); + size_t Nalphas = safe_first_shape(alphas); + size_t Nforced_alphas = safe_first_shape(forced_alphas); + size_t Nantialiaseds = safe_first_shape(antialiaseds); + size_t Ncapstyles = vgc.capstyles.size(); + size_t Ndashes = vgc.dashes.size(); + size_t Njoinstyles = vgc.joinstyles.size(); size_t Nlinewidths = safe_first_shape(linewidths); - size_t Nlinestyles = std::min(linestyles.size(), N); - size_t Naa = safe_first_shape(antialiaseds); + size_t Nedgecolors = safe_first_shape(edgecolors); + size_t Nfacecolors = safe_first_shape(facecolors); + size_t Nhatchpaths = vgc.hatchpaths.size(); + size_t Nhatchcolors = safe_first_shape(hatch_colors); + size_t Nhatch_linewidths = safe_first_shape(hatch_linewidths); + size_t Nsnap_modes = vgc.snap_modes.size(); + size_t Nsketches = vgc.sketches.size(); if ((Nfacecolors == 0 && Nedgecolors == 0 && Nhatchcolors == 0) || Npaths == 0) { return; @@ -935,14 +937,15 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, // Handle any clipping globally theRasterizer.reset_clipping(); rendererBase.reset_clipping(true); - set_clipbox(cliprect, theRasterizer); - bool has_clippath = render_clippath(clippath, clippath_trans, gc.snap_mode); + set_clipbox(vgc.cliprect, theRasterizer); // Set some defaults, assuming no face or edge + GCAgg gc; gc.linewidth = 0.0; std::optional face; agg::trans_affine trans; - bool do_clip = Nfacecolors == 0 && !gc.has_hatchpath(); + gc.clippath = vgc.clippath; + gc.cliprect = vgc.cliprect; for (int i = 0; i < (int)N; ++i) { typename PathGenerator::path_iterator path = path_generator(i); @@ -966,16 +969,19 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, offset_trans.transform(&xo, &yo); trans *= agg::trans_affine_translation(xo, yo); } - // These transformations must be done post-offsets trans *= agg::trans_affine_scaling(1.0, -1.0); trans *= agg::trans_affine_translation(0.0, (double)height); - if (Nfacecolors) { - int ic = i % Nfacecolors; - face.emplace(facecolors(ic, 0), facecolors(ic, 1), facecolors(ic, 2), facecolors(ic, 3)); + if(Nalphas){ + gc.alpha = alphas(i % Nalphas); + } + if(Nforced_alphas){ + gc.forced_alpha = forced_alphas(i % Nforced_alphas); + } + if(Nantialiaseds){ + gc.isaa = antialiaseds(i % Nantialiaseds); } - if (Nedgecolors) { int ic = i % Nedgecolors; gc.color = agg::rgba(edgecolors(ic, 0), edgecolors(ic, 1), edgecolors(ic, 2), edgecolors(ic, 3)); @@ -985,17 +991,40 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, } else { gc.linewidth = 1.0; } - if (Nlinestyles) { - gc.dashes = linestyles[i % Nlinestyles]; + if (Ndashes) { + gc.dashes = vgc.dashes[i % Ndashes]; } } - + if (Nfacecolors) { + int ic = i % Nfacecolors; + face.emplace(facecolors(ic, 0), facecolors(ic, 1), facecolors(ic, 2), facecolors(ic, 3)); + } + if(Ncapstyles){ + gc.cap = vgc.capstyles[i % Ncapstyles]; + } + if(Njoinstyles){ + gc.join = vgc.joinstyles[i % Njoinstyles]; + } + if (Nhatchpaths) { + int ic = i % Nhatchpaths; + gc.hatchpath = vgc.hatchpaths[ic]; + } if(Nhatchcolors) { int ic = i % Nhatchcolors; - gc.hatch_color = agg::rgba(hatchcolors(ic, 0), hatchcolors(ic, 1), hatchcolors(ic, 2), hatchcolors(ic, 3)); + gc.hatch_color = agg::rgba(hatch_colors(ic, 0), hatch_colors(ic, 1), hatch_colors(ic, 2), hatch_colors(ic, 3)); + } + if(Nhatch_linewidths){ + gc.hatch_linewidth = hatch_linewidths(i % Nhatch_linewidths); + } + if(Nsnap_modes){ + gc.snap_mode = vgc.snap_modes[i % Nsnap_modes]; + } + if(Nsketches){ + gc.sketch = vgc.sketches[i % Nsketches]; } - gc.isaa = antialiaseds(i % Naa); + bool has_clippath = render_clippath((gc.clippath).path, (gc.clippath).trans, gc.snap_mode); + bool do_clip = Nfacecolors == 0 && !gc.has_hatchpath(); transformed_path_t tpath(path, trans); nan_removed_t nan_removed(tpath, true, has_codes); clipped_t clipped(nan_removed, do_clip, width, height); @@ -1042,25 +1071,53 @@ inline void RendererAgg::draw_path_collection(GCAgg &gc, AntialiasedArray &antialiaseds, ColorArray &hatchcolors) { - _draw_path_collection_generic(gc, + VGCAgg vgc; + + vgc.alphas = py::array_t({1}, &gc.alpha); + vgc.forced_alphas = py::array_t({1}, reinterpret_cast(&gc.forced_alpha)); + vgc.antialiaseds = antialiaseds; + vgc.linewidths = linewidths; + vgc.edgecolors = edgecolors; + vgc.facecolors = facecolors; + vgc.capstyles = {gc.cap}; + vgc.joinstyles = {gc.join}; + vgc.clippath = gc.clippath; + vgc.cliprect = gc.cliprect; + vgc.dashes = linestyles; + vgc.hatchpaths = {gc.hatchpath}; + vgc.hatch_colors = hatchcolors; + vgc.hatch_linewidths = py::array_t({1}, &gc.hatch_linewidth); + vgc.snap_modes = {gc.snap_mode}; + vgc.sketches = {gc.sketch}; + + _draw_path_collection_generic(vgc, master_transform, - gc.cliprect, - gc.clippath.path, - gc.clippath.trans, path, transforms, offsets, offset_trans, - facecolors, - edgecolors, - linewidths, - linestyles, - antialiaseds, true, + true); +} +template +inline void RendererAgg::draw_path_collection(VGCAgg &vgc, + agg::trans_affine &master_transform, + PathGenerator &path, + TransformArray &transforms, + OffsetArray &offsets, + agg::trans_affine &offset_trans) +{ + _draw_path_collection_generic(vgc, + master_transform, + path, + transforms, + offsets, + offset_trans, true, - hatchcolors); + true); } - template class QuadMeshGenerator { @@ -1149,28 +1206,34 @@ inline void RendererAgg::draw_quad_mesh(GCAgg &gc, QuadMeshGenerator path_generator(mesh_width, mesh_height, coordinates); array::empty transforms; - array::scalar linewidths(gc.linewidth); - array::scalar antialiaseds(antialiased); - DashesVector linestyles; - ColorArray hatchcolors = py::array_t().reshape({0, 4}).unchecked(); - _draw_path_collection_generic(gc, + VGCAgg vgc; + + vgc.alphas = py::array_t({1}, &gc.alpha); + vgc.forced_alphas = py::array_t({1}, reinterpret_cast(&gc.forced_alpha)); + vgc.antialiaseds = py::array_t({1}, reinterpret_cast(&antialiased)); + vgc.linewidths = py::array_t({1}, &gc.linewidth); + vgc.edgecolors = edgecolors; + vgc.facecolors = facecolors; + vgc.capstyles = {gc.cap}; + vgc.joinstyles = {gc.join}; + vgc.clippath = gc.clippath; + vgc.cliprect = gc.cliprect; + vgc.dashes = {}; + vgc.hatchpaths = {gc.hatchpath}; + vgc.hatch_colors = py::array_t().reshape({0, 4}); + vgc.hatch_linewidths = py::array_t({1}, &gc.hatch_linewidth); + vgc.snap_modes = {gc.snap_mode}; + vgc.sketches = {gc.sketch}; + + _draw_path_collection_generic(vgc, master_transform, - gc.cliprect, - gc.clippath.path, - gc.clippath.trans, path_generator, transforms, offsets, offset_trans, - facecolors, - edgecolors, - linewidths, - linestyles, - antialiaseds, true, // check_snap - false, - hatchcolors); + false); } template diff --git a/src/_backend_agg_basic_types.h b/src/_backend_agg_basic_types.h index b424419ec99e..ed1131e79100 100644 --- a/src/_backend_agg_basic_types.h +++ b/src/_backend_agg_basic_types.h @@ -124,6 +124,46 @@ class GCAgg GCAgg &operator=(const GCAgg &); }; +class VGCAgg +{ + public: + VGCAgg() + { + } + ~VGCAgg() + { + } + + py::array_t alphas; + py::array_t forced_alphas; + py::array_t antialiaseds; + py::array_t linewidths; + py::array_t edgecolors; + py::array_t facecolors; + + std::vector capstyles; + std::vector joinstyles; + + agg::rect_d cliprect; + + ClipPath clippath; + + std::vector dashes; + + std::vector hatchpaths; + py::array_t hatch_colors; + py::array_t hatch_linewidths; + + std::vector snap_modes; + + std::vector sketches; + + private: + // prevent copying + VGCAgg(const VGCAgg &); + VGCAgg &operator=(const VGCAgg &); +}; + namespace PYBIND11_NAMESPACE { namespace detail { template <> struct type_caster { public: @@ -250,6 +290,32 @@ namespace PYBIND11_NAMESPACE { namespace detail { return true; } }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(VGCAgg, const_name("VGCAgg")); + + bool load(handle src, bool) { + value.alphas = src.attr("get_alphas")().cast>(); + value.forced_alphas = src.attr("get_forced_alphas")().cast>(); + value.antialiaseds = src.attr("get_antialiaseds")().cast>(); + value.capstyles = src.attr("get_capstyles")().cast>(); + value.dashes = src.attr("get_dashes")().cast>(); + value.joinstyles = src.attr("get_joinstyles")().cast>(); + value.linewidths = src.attr("get_linewidths")().cast>(); + value.edgecolors = src.attr("get_edgecolors")().cast>().reshape({-1, 4}); + value.facecolors = src.attr("get_facecolors")().cast>().reshape({-1, 4}); + value.hatchpaths = src.attr("get_hatch_paths")().cast>(); + value.hatch_colors = src.attr("get_hatch_colors")().cast>().reshape({-1, 4}); + value.hatch_linewidths = src.attr("get_hatch_linewidths")().cast>(); + value.snap_modes = src.attr("get_snaps")().cast>(); + value.sketches = src.attr("get_sketches_params")().cast>(); + value.cliprect = src.attr("get_clip_rectangle")().cast(); + value.clippath = src.attr("get_clip_path")().cast(); + + return true; + } + }; }} // namespace PYBIND11_NAMESPACE::detail #endif diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 3dd50b31f64a..7f008c0daa86 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -139,23 +139,18 @@ PyRendererAgg_draw_path_collection(RendererAgg *self, py::array_t transforms_obj, py::array_t offsets_obj, agg::trans_affine offset_trans, - py::array_t facecolors_obj, - py::array_t edgecolors_obj, - py::array_t linewidths_obj, + py::array_t facecolors, + py::array_t edgecolors, + py::array_t linewidths, DashesVector dashes, - py::array_t antialiaseds_obj, + py::array_t antialiaseds, py::object Py_UNUSED(ignored_obj), // offset position is no longer used py::object Py_UNUSED(offset_position_obj), - py::array_t hatchcolors_obj) + py::array_t hatchcolors) { auto transforms = convert_transforms(transforms_obj); auto offsets = convert_points(offsets_obj); - auto facecolors = convert_colors(facecolors_obj); - auto edgecolors = convert_colors(edgecolors_obj); - auto hatchcolors = convert_colors(hatchcolors_obj); - auto linewidths = linewidths_obj.unchecked<1>(); - auto antialiaseds = antialiaseds_obj.unchecked<1>(); self->draw_path_collection(gc, master_transform, @@ -171,6 +166,28 @@ PyRendererAgg_draw_path_collection(RendererAgg *self, hatchcolors); } +static void +PyRendererAgg_draw_path_collection(RendererAgg *self, + VGCAgg &vgc, + agg::trans_affine master_transform, + mpl::PathGenerator paths, + py::array_t transforms_obj, + py::array_t offsets_obj, + agg::trans_affine offset_trans, + py::object Py_UNUSED(ignored_obj)) +{ + auto transforms = convert_transforms(transforms_obj); + auto offsets = convert_points(offsets_obj); + + self->draw_path_collection(vgc, + master_transform, + paths, + transforms, + offsets, + offset_trans + ); +} + static void PyRendererAgg_draw_quad_mesh(RendererAgg *self, GCAgg &gc, @@ -180,14 +197,12 @@ PyRendererAgg_draw_quad_mesh(RendererAgg *self, py::array_t coordinates_obj, py::array_t offsets_obj, agg::trans_affine offset_trans, - py::array_t facecolors_obj, + py::array_t facecolors, bool antialiased, - py::array_t edgecolors_obj) + py::array_t edgecolors) { auto coordinates = coordinates_obj.mutable_unchecked<3>(); auto offsets = convert_points(offsets_obj); - auto facecolors = convert_colors(facecolors_obj); - auto edgecolors = convert_colors(edgecolors_obj); self->draw_quad_mesh(gc, master_transform, @@ -229,11 +244,20 @@ PYBIND11_MODULE(_backend_agg, m, py::mod_gil_not_used()) "image"_a, "x"_a, "y"_a, "angle"_a, "gc"_a) .def("draw_image", &PyRendererAgg_draw_image, "gc"_a, "x"_a, "y"_a, "image"_a) - .def("draw_path_collection", &PyRendererAgg_draw_path_collection, + .def("draw_path_collection", py::overload_cast, py::array_t, agg::trans_affine, + py::array_t, py::array_t, py::array_t, + DashesVector, py::array_t, py::object, py::object, + py::array_t>(&PyRendererAgg_draw_path_collection), "gc"_a, "master_transform"_a, "paths"_a, "transforms"_a, "offsets"_a, "offset_trans"_a, "facecolors"_a, "edgecolors"_a, "linewidths"_a, "dashes"_a, "antialiaseds"_a, "ignored"_a, "offset_position"_a, py::kw_only(), "hatchcolors"_a = py::array_t().reshape({0, 4})) + .def("draw_path_collection", py::overload_cast, py::array_t, agg::trans_affine, + py::object>(&PyRendererAgg_draw_path_collection), + "vgc"_a, "master_transform"_a, "paths"_a, "transforms"_a, "offsets"_a, + "offset_trans"_a,"ignored"_a = py::array_t()) .def("draw_quad_mesh", &PyRendererAgg_draw_quad_mesh, "gc"_a, "master_transform"_a, "mesh_width"_a, "mesh_height"_a, "coordinates"_a, "offsets"_a, "offset_trans"_a, "facecolors"_a,