diff --git a/doc/api/next_api_changes/behavior/26050-AS.rst b/doc/api/next_api_changes/behavior/26050-AS.rst new file mode 100644 index 000000000000..0f8424f4d047 --- /dev/null +++ b/doc/api/next_api_changes/behavior/26050-AS.rst @@ -0,0 +1,13 @@ +Seed for ``path.sketch`` will have a rolling (auto incrementing) behaviour +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The seed for the internal Pseudo number generator will now have an auto changing behavior. +This means that the C code of every artist will get a different seed every time it is called +and this will be done in a deterministic manner. + +Two figures sketched with the same parameters and different seed will look different from one another. + +``Artist.get_sketch_params()`` will now return a 4-tuple instead of a 3-tuple consisting of +(scale, length, randomness, seed) of the form (float, float, float, int). + +See 'What's new' on how to set a value to the seed and its behaviour. diff --git a/doc/release/next_whats_new/sketch_seed.rst b/doc/release/next_whats_new/sketch_seed.rst new file mode 100644 index 000000000000..97ebea87fe2c --- /dev/null +++ b/doc/release/next_whats_new/sketch_seed.rst @@ -0,0 +1,67 @@ +``sketch_seed`` parameter for rcParams +-------------------------------------- + +`~matplotlib.rcParams` now has a new parameter ``path.sketch_seed``. +Its default value is 0 and accepted values are any non negative integer. +This allows the user to set the seed for the internal pseudo random number generator in one of three ways. + +1) Directly changing the rcParam: + + rcParams['path.sketch_seed'] = 20 + +2) Passing a value to the new *seed* parameter of `~matplotlib.pyplot.xkcd` function: + + plt.xkcd(seed=20) + +3) Passing a value to the new *seed* parameter of matplotlib.artist.set_sketch_params function: + + ln = plt.plot(x, y) + ln[0].set_sketch_params(seed = 20) + +The seed will also have a changing characteristic for every artist which will be done in a deterministic manner. + + +.. plot:: + :include-source: true + + import matplotlib.pyplot as plt + from matplotlib import rcParams + + with plt.xkcd(): + rcParams['path.sketch_seed']=0 + rcParams['path.sketch']=(2,120,40) + pat,txt=plt.pie([10,20,30,40],wedgeprops={'edgecolor':'black'}) + plt.legend(pat,['first','second','third','fourth'],loc='best') + plt.title("seed = 0") + plt.show() + +.. plot:: + :include-source: true + + import matplotlib.pyplot as plt + from matplotlib import rcParams + + fig, ax = plt.subplots() + x = np.linspace(0.7, 1.42, 100) + y = x ** 2 + ln = ax.plot(x, y, color='black') + ln[0].set_sketch_params(100, 100, 20, 40) + plt.title("seed = 40") + plt.show() + +.. plot:: + :include-source: true + + import matplotlib.pyplot as plt + from matplotlib import rcParams + + with plt.xkcd(seed=19680801): + import matplotlib + from matplotlib import gridspec + + rcParams['path.sketch']=(2,120,40) + + pat,txt=plt.pie([10,20,30,40],wedgeprops={'edgecolor':'black'}) + plt.legend(pat,['first','second','third','fourth'],loc='best') + plt.title("seed = 19680801") + plt.show() diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index cb6e650a94cc..50d25f11b700 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -222,6 +222,7 @@ def __init__(self): self._gid = None self._snap = None self._sketch = mpl.rcParams['path.sketch'] + self._sketch_seed = mpl.rcParams['path.sketch_seed'] self._path_effects = mpl.rcParams['path.effects'] self._sticky_edges = _XYPair([], []) self._in_layout = True @@ -713,7 +714,8 @@ def get_sketch_params(self): """ return self._sketch - def set_sketch_params(self, scale=None, length=None, randomness=None): + def set_sketch_params(self, scale=None, length=None, randomness=None, + seed=None): """ Set the sketch parameters. @@ -733,12 +735,22 @@ def set_sketch_params(self, scale=None, length=None, randomness=None): The PGF backend uses this argument as an RNG seed and not as described above. Using the same seed yields the same random shape. - .. ACCEPTS: (scale: float, length: float, randomness: float) + seed : int, optional + Seed for the internal pseudo-random number generator. + + .. versionadded:: 3.8 + + .. ACCEPTS: (scale: float, length: float, randomness: float, seed: int) """ + if seed is not None: + self._sketch_seed = seed + if scale is None: self._sketch = None else: - self._sketch = (scale, length or 128.0, randomness or 16.0) + self._sketch = (scale,length if length is not None else 128.0, + randomness if randomness is not None else 16.0) + self.stale = True def set_path_effects(self, path_effects): diff --git a/lib/matplotlib/artist.pyi b/lib/matplotlib/artist.pyi index 7b8b0c36be69..b90b9be22d8c 100644 --- a/lib/matplotlib/artist.pyi +++ b/lib/matplotlib/artist.pyi @@ -77,12 +77,13 @@ class Artist: def set_gid(self, gid: str | None) -> None: ... def get_snap(self) -> bool | None: ... def set_snap(self, snap: bool | None) -> None: ... - def get_sketch_params(self) -> tuple[float, float, float] | None: ... + def get_sketch_params(self) -> tuple[float, float, float, int] | None: ... def set_sketch_params( self, scale: float | None = ..., length: float | None = ..., randomness: float | None = ..., + seed: int | None = ..., ) -> None: ... def set_path_effects(self, path_effects: list[AbstractPathEffect]) -> None: ... def get_path_effects(self) -> list[AbstractPathEffect]: ... diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index fc19f6108d13..59020a844041 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -691,6 +691,19 @@ def _draw_disabled(self): return _setattr_cm(self, **no_ops) + @property + def _seed_increment(self): + """ + seed increment for renderer. + It is used to implement the rolling characteristic for seed + """ + self.__seed_increment += 1 + return self.__seed_increment + + @_seed_increment.setter + def _seed_increment(self, value): + self.__seed_increment = value + class GraphicsContextBase: """An abstract base class that provides color, line styles, etc.""" @@ -1003,7 +1016,8 @@ def get_sketch_params(self): """ return self._sketch - def set_sketch_params(self, scale=None, length=None, randomness=None): + def set_sketch_params(self, scale=None, length=None, randomness=None, + seed=None): """ Set the sketch parameters. @@ -1017,10 +1031,19 @@ def set_sketch_params(self, scale=None, length=None, randomness=None): The length of the wiggle along the line, in pixels. randomness : float, default: 16 The scale factor by which the length is shrunken or expanded. + seed : int, optional + Seed for the internal pseudo-random number generator. + + .. versionadded:: 3.8 """ + self._sketch = ( None if scale is None - else (scale, length or 128., randomness or 16.)) + else (scale, + length or rcParams['path.sketch'][1], + randomness or rcParams['path.sketch'][2], + seed or rcParams['path.sketch_seed']) + ) class TimerBase: diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index a69b36093839..a3598ef83763 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -142,6 +142,10 @@ class RendererBase: def stop_rasterizing(self) -> None: ... def start_filter(self) -> None: ... def stop_filter(self, filter_func) -> None: ... + @property + def _seed_increment(self) -> int: ... + @_seed_increment.setter + def _seed_increment(self, value: int) -> None: ... class GraphicsContextBase: def __init__(self) -> None: ... @@ -187,6 +191,7 @@ class GraphicsContextBase: scale: float | None = ..., length: float | None = ..., randomness: float | None = ..., + seed:int | None = ..., ) -> None: ... class TimerBase: diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 2d2e24c3286c..dfa39f73ba86 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -598,7 +598,7 @@ def _print_pgf_path(self, gc, path, transform, rgbFace=None): # and has a separate "scale" argument for the amplitude. # -> Use "randomness" as PRNG seed to allow the user to force the # same shape on multiple sketched lines - scale, length, randomness = sketch_params + scale, length, randomness, seed = sketch_params if scale is not None: # make matplotlib and PGF rendering visually similar length *= 0.5 diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 501b3e32ebf4..9525bc2521ec 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -42,6 +42,7 @@ Artist, allow_rasterization, _finalize_rasterization) from matplotlib.backend_bases import ( DrawEvent, FigureCanvasBase, NonGuiException, MouseButton, _get_renderer) + import matplotlib._api as _api import matplotlib.cbook as cbook import matplotlib.colorbar as cbar @@ -3263,6 +3264,7 @@ def draw(self, renderer): artists = self._get_draw_artists(renderer) try: + renderer._seed_increment = 0 renderer.open_group('figure', gid=self.get_gid()) if self.axes and self.get_layout_engine() is not None: try: diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 0cfda07dd627..677378a3e346 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -838,7 +838,8 @@ def draw(self, renderer): gc.set_foreground(ec_rgba, isRGBA=True) if self.get_sketch_params() is not None: scale, length, randomness = self.get_sketch_params() - gc.set_sketch_params(scale/2, length/2, 2*randomness) + seed = self._sketch_seed + gc.set_sketch_params(scale/2, length/2, 2*randomness, seed) marker = self._marker diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc index e934109ee492..9e5903891f06 100644 --- a/lib/matplotlib/mpl-data/matplotlibrc +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -714,6 +714,7 @@ # - *randomness* is the factor by which the length is # randomly scaled. #path.effects: +#path.sketch_seed: 0 # seed for the internal pseudo number generator. ## *************************************************************************** diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 4a4bd698db04..346e81ec7a78 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -703,7 +703,9 @@ def _draw_paths_with_artist_properties( gc.set_hatch_linewidth(self._hatch_linewidth) if self.get_sketch_params() is not None: - gc.set_sketch_params(*self.get_sketch_params()) + scale, length, randomness = self.get_sketch_params() + gc.set_sketch_params(scale, length, randomness, + self._sketch_seed+renderer._seed_increment) if self.get_path_effects(): from matplotlib.patheffects import PathEffectRenderer diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index f65ade669167..6123027000f7 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -401,8 +401,8 @@ def iter_segments(self, transform=None, remove_nans=True, clip=None, If True, curve segments will be returned as curve segments. If False, all curves will be converted to line segments. sketch : None or sequence, optional - If not None, must be a 3-tuple of the form - (scale, length, randomness), representing the sketch parameters. + If not None, must be a 4-tuple of the form + (scale, length, randomness, seed), representing the sketch parameters. """ if not len(self): return diff --git a/lib/matplotlib/path.pyi b/lib/matplotlib/path.pyi index 8a5a5c03792e..7073d9117da0 100644 --- a/lib/matplotlib/path.pyi +++ b/lib/matplotlib/path.pyi @@ -61,7 +61,7 @@ class Path: stroke_width: float = ..., simplify: bool | None = ..., curves: bool = ..., - sketch: tuple[float, float, float] | None = ..., + sketch: tuple[float, float, float, int] | None = ..., ) -> Generator[tuple[np.ndarray, np.uint8], None, None]: ... def iter_bezier(self, **kwargs) -> Generator[BezierSegment, None, None]: ... def cleaned( @@ -74,7 +74,7 @@ class Path: curves: bool = ..., stroke_width: float = ..., snap: bool | None = ..., - sketch: tuple[float, float, float] | None = ... + sketch: tuple[float, float, float, int] | None = ... ) -> Path: ... def transformed(self, transform: Transform) -> Path: ... def contains_point( diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 483bc031336c..e413c6ceeebe 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -828,8 +828,8 @@ def setp(obj, *args, **kwargs): def xkcd( - scale: float = 1, length: float = 100, randomness: float = 2 -) -> ExitStack: + scale: float = 1, length: float = 100, randomness: float = 2, + seed: int | None = None) -> ExitStack: """ Turn on `xkcd `_ sketch-style drawing mode. @@ -846,6 +846,8 @@ def xkcd( The length of the wiggle along the line. randomness : float, optional The scale factor by which the length is shrunken or expanded. + seed: int, optional + Seed for the internal pseudo-random number generator. Notes ----- @@ -865,6 +867,9 @@ def xkcd( # This cannot be implemented in terms of contextmanager() or rc_context() # because this needs to work as a non-contextmanager too. + if seed is not None: + rcParams['path.sketch_seed'] = seed + if rcParams['text.usetex']: raise RuntimeError( "xkcd mode is not compatible with text.usetex = True") diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index e56da5200386..01d3fdb32ea7 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -636,6 +636,15 @@ def validate_sketch(s): raise ValueError("Expected a (scale, length, randomness) tuple") from exc +def validate_sketch_seed(s): + s = validate_int(s) + + if s >= 0: + return s + else: + raise ValueError("seed must be a non negative integer") + + def _validate_greaterthan_minushalf(s): s = validate_float(s) if s > -0.5: @@ -1388,6 +1397,7 @@ def _convert_validator_spec(key, conv): "path.simplify_threshold": _validate_greaterequal0_lessequal1, "path.snap": validate_bool, "path.sketch": validate_sketch, + "path.sketch_seed": validate_sketch_seed, "path.effects": validate_anylist, "agg.path.chunksize": validate_int, # 0 to disable chunking @@ -3004,6 +3014,13 @@ class _Subsection: "- *randomness* is the factor by which the length is randomly " " scaled." ), + _Param( + "path.sketch_seed", + default=0, + validator=validate_sketch_seed, + description="Seed for the random number generator used in sketch mode. " + "The seed is auto-incremented after each path is drawn." + ), _Param( "path.effects", default=[], diff --git a/lib/matplotlib/rcsetup.pyi b/lib/matplotlib/rcsetup.pyi index 120c0c93bec9..de1a9d219113 100644 --- a/lib/matplotlib/rcsetup.pyi +++ b/lib/matplotlib/rcsetup.pyi @@ -143,6 +143,7 @@ def _validate_linestyle_or_None(s: Any) -> LineStyleType | None: ... def validate_markeverylist(s: Any) -> list[MarkEveryType]: ... def validate_bbox(s: Any) -> Literal["tight", "standard"] | None: ... def validate_sketch(s: Any) -> None | tuple[float, float, float]: ... +def validate_sketch_seed(s: Any) -> int: ... def validate_hatch(s: Any) -> str: ... def validate_hatchlist(s: Any) -> list[str]: ... def validate_dashlist(s: Any) -> list[list[float]]: ... diff --git a/lib/matplotlib/tests/baseline_images/test_path/xkcd.png b/lib/matplotlib/tests/baseline_images/test_path/xkcd.png index fd486c42305f..f57d07aab8f2 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_path/xkcd.png and b/lib/matplotlib/tests/baseline_images/test_path/xkcd.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_path/xkcd_marker.png b/lib/matplotlib/tests/baseline_images/test_path/xkcd_marker.png index c4224f74c1ec..a163a2254217 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_path/xkcd_marker.png and b/lib/matplotlib/tests/baseline_images/test_path/xkcd_marker.png differ diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index e218a81cdceb..f0aa8413e18c 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -358,7 +358,7 @@ def test_sketch_params(): ax.set_yticks([]) ax.set_frame_on(False) handle, = ax.plot([0, 1]) - handle.set_sketch_params(scale=5, length=30, randomness=42) + handle.set_sketch_params(scale=5, length=30, randomness=42, seed=0) with BytesIO() as fd: fig.savefig(fd, format='pgf') diff --git a/lib/matplotlib/tests/test_path.py b/lib/matplotlib/tests/test_path.py index a61f01c0d48a..ba1642bb2f05 100644 --- a/lib/matplotlib/tests/test_path.py +++ b/lib/matplotlib/tests/test_path.py @@ -9,10 +9,11 @@ from matplotlib import patches from matplotlib.path import Path from matplotlib.patches import Polygon -from matplotlib.testing.decorators import image_comparison +from matplotlib.testing.decorators import image_comparison, check_figures_equal import matplotlib.pyplot as plt from matplotlib import transforms from matplotlib.backend_bases import MouseEvent +from matplotlib import rcParams def test_empty_closed_path(): @@ -242,13 +243,45 @@ def test_make_compound_path_stops(): assert np.sum(compound_path.codes == Path.STOP) == 0 +def test_path_sketch_seed(): + # the default value of path.sketch_seed should be 0 + assert rcParams['path.sketch_seed'] == 0 + + +def test_xkcd_seed_update(): + # when passing a seed to xkcd, the global rcParam should be updated + with plt.xkcd(seed=2000): + assert rcParams['path.sketch_seed'] == 2000 + + +def test_sketch_rolling_seed(): + """Same seed should produce identical output every time (deterministic)""" + + import io + x = np.linspace(0, 5, 200) + y = 20 * np.sin(x) + + with plt.xkcd(seed=100): + fig, ax = plt.subplots() + ax.plot(x, y) + ax.plot(x, y + 5) + buf1 = io.BytesIO() + buf2 = io.BytesIO() + fig.savefig(buf1, format='png') + fig.savefig(buf2, format='png') + + buf1.seek(0) + buf2.seek(0) + assert buf1.read() == buf2.read() + plt.close('all') + + @image_comparison(['xkcd.png'], remove_text=True) def test_xkcd(): np.random.seed(0) x = np.linspace(0, 2 * np.pi, 100) y = np.sin(x) - with plt.xkcd(): fig, ax = plt.subplots() ax.plot(x, y) @@ -262,7 +295,6 @@ def test_xkcd_marker(): y1 = x y2 = 5 - x y3 = 2.5 * np.ones(8) - with plt.xkcd(): fig, ax = plt.subplots() ax.plot(x, y1, '+', ms=10) @@ -270,6 +302,77 @@ def test_xkcd_marker(): ax.plot(x, y3, '^', ms=10) +@check_figures_equal(extensions=['png']) +def test_xkcd_override(fig_test, fig_ref): + x = np.linspace(0.7, 1.42, 100) + y = x ** 2 + + ln = fig_ref.add_subplot().plot(x, y) + ln_test = fig_test.add_subplot().plot(x, y) + + with plt.xkcd(): + ln[0].set_sketch_params(3, 120, 40, 420) + + # set_sketch should override seed set by xkcd + with plt.xkcd(seed=5885): + ln_test[0].set_sketch_params(3, 120, 40, 420) + + +@check_figures_equal(extensions=['png']) +def test_artist_seed(fig_test, fig_ref): + x = np.linspace(0.7, 1.42, 100) + y = [0.7]*100 + + ax_ref = fig_ref.add_subplot() + ax_test = fig_test.add_subplot() + + ln = ax_ref.plot(x, y) + ln_test = ax_test.plot(x, y) + + ln[0].set_sketch_params(3, 120, 40, 19680801) + + # set_sketch_params seed should override seed set by rcParam + # when seed is passed in set_sketch, it should be used + # else rcParam should be used as seed + rcParams['path.sketch_seed'] = 59856 + ln_test[0].set_sketch_params(3, 120, 40, 19680801) + + +@check_figures_equal(extensions=['png']) +def test_path_seed(fig_test, fig_ref): + x = x = np.linspace(0, 5, 200) + y = 20*np.sin(x) + + ax_ref = fig_ref.add_subplot() + ax_test = fig_test.add_subplot() + + rcParams['path.sketch_seed'] = 645 + rcParams['path.sketch'] = 3, 120, 40 + + ln = ax_ref.plot(x, y) + + ln_test = ax_test.plot(x, y) + ln_test[0].set_sketch_params(3, 120, 40, 645) + + +@check_figures_equal(extensions=['png']) +def test_xkcd_seed(fig_test, fig_ref): + x = x = np.linspace(0, 5, 200) + y = 20*np.sin(x) + + ax_ref = fig_ref.add_subplot() + ax_test = fig_test.add_subplot() + + with plt.xkcd(seed=20): + ln = ax_ref.plot(x, y) + ln[0].set_sketch_params(3, 120, 40) + + with plt.xkcd(seed=40): + rcParams['path.sketch_seed'] = 20 + ln_test = ax_test.plot(x, y) + ln_test[0].set_sketch_params(3, 120, 40) + + @image_comparison(['marker_paths.pdf'], remove_text=True) def test_marker_paths_pdf(): N = 7 diff --git a/lib/matplotlib/typing.py b/lib/matplotlib/typing.py index 87016984da12..ce4dcfc17554 100644 --- a/lib/matplotlib/typing.py +++ b/lib/matplotlib/typing.py @@ -418,6 +418,7 @@ "path.simplify", "path.simplify_threshold", "path.sketch", + "path.sketch_seed", "path.snap", "pcolor.shading", "pcolormesh.snap", diff --git a/src/_backend_agg.h b/src/_backend_agg.h index 1ac3d4c06b13..ce7584c11426 100644 --- a/src/_backend_agg.h +++ b/src/_backend_agg.h @@ -481,7 +481,7 @@ RendererAgg::draw_path(GCAgg &gc, PathIterator &path, agg::trans_affine &trans, snapped_t snapped(clipped, gc.snap_mode, path.total_vertices(), snapping_linewidth); simplify_t simplified(snapped, simplify, path.simplify_threshold()); curve_t curve(simplified); - sketch_t sketch(curve, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness); + sketch_t sketch(curve, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness, gc.sketch.seed); _draw_path(sketch, has_clippath, face, gc); } @@ -1004,19 +1004,18 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, clipped, gc.snap_mode, path.total_vertices(), points_to_pixels(gc.linewidth)); if (has_codes) { snapped_curve_t curve(snapped); - sketch_snapped_curve_t sketch(curve, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness); - _draw_path(sketch, has_clippath, face, gc); +sketch_snapped_curve_t sketch(curve, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness, gc.sketch.seed); _draw_path(sketch, has_clippath, face, gc); } else { - sketch_snapped_t sketch(snapped, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness); + sketch_snapped_t sketch(snapped, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness, gc.sketch.seed); _draw_path(sketch, has_clippath, face, gc); } } else { if (has_codes) { curve_t curve(clipped); - sketch_curve_t sketch(curve, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness); + sketch_curve_t sketch(curve, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness, gc.sketch.seed); _draw_path(sketch, has_clippath, face, gc); } else { - sketch_clipped_t sketch(clipped, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness); + sketch_clipped_t sketch(clipped, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness, gc.sketch.seed); _draw_path(sketch, has_clippath, face, gc); } } diff --git a/src/_backend_agg_basic_types.h b/src/_backend_agg_basic_types.h index b424419ec99e..41c610a73e51 100644 --- a/src/_backend_agg_basic_types.h +++ b/src/_backend_agg_basic_types.h @@ -29,6 +29,7 @@ struct SketchParams double scale; double length; double randomness; + int seed; }; class Dashes @@ -219,8 +220,10 @@ namespace PYBIND11_NAMESPACE { namespace detail { return true; } - auto params = src.cast>(); - std::tie(value.scale, value.length, value.randomness) = params; + + + auto params = src.cast>(); + std::tie(value.scale, value.length, value.randomness, value.seed) = params; return true; } diff --git a/src/_path.h b/src/_path.h index 226d60231682..0772f4790969 100644 --- a/src/_path.h +++ b/src/_path.h @@ -1033,7 +1033,7 @@ void cleanup_path(PathIterator &path, __cleanup_path(simplified, vertices, codes); } else { curve_t curve(simplified); - sketch_t sketch(curve, sketch_params.scale, sketch_params.length, sketch_params.randomness); + sketch_t sketch(curve, sketch_params.scale, sketch_params.length, sketch_params.randomness, sketch_params.seed); __cleanup_path(sketch, vertices, codes); } } @@ -1197,7 +1197,7 @@ bool convert_to_string(PathIterator &path, return __convert_to_string(simplified, precision, codes, postfix, buffer); } else { curve_t curve(simplified); - sketch_t sketch(curve, sketch_params.scale, sketch_params.length, sketch_params.randomness); + sketch_t sketch(curve, sketch_params.scale, sketch_params.length, sketch_params.randomness, sketch_params.seed); return __convert_to_string(sketch, precision, codes, postfix, buffer); } } diff --git a/src/path_converters.h b/src/path_converters.h index 1482aeed95f8..e89d048fc90c 100644 --- a/src/path_converters.h +++ b/src/path_converters.h @@ -1041,6 +1041,7 @@ class PathSimplifier : protected EmbeddedQueue<9> } }; + template class Sketch { @@ -1054,8 +1055,10 @@ class Sketch randomness: the factor that the sketch length will randomly shrink and expand. + + seed: seed for the built-in pseudo-random number generator. */ - Sketch(VertexSource &source, double scale, double length, double randomness) + Sketch(VertexSource &source, double scale, double length, double randomness, int seed) : m_source(&source), m_scale(scale), m_length(length), @@ -1065,9 +1068,9 @@ class Sketch m_last_y(0.0), m_has_last(false), m_p(0.0), - m_rand(0) + m_rand(seed), + m_seed(seed) { - rewind(0); const double d_M_PI = 3.14159265358979323846; // Set derived values to zero if m_length or m_randomness are zero to // avoid divide-by-zero errors when a sketch is created but not used. @@ -1141,7 +1144,7 @@ class Sketch m_has_last = false; m_p = 0.0; if (m_scale != 0.0) { - m_rand.seed(0); + m_rand.seed(m_seed); m_segmented.rewind(path_id); } else { m_source->rewind(path_id); @@ -1161,6 +1164,7 @@ class Sketch RandomNumberGenerator m_rand; double m_p_scale; double m_log_randomness; + int m_seed; }; #endif // MPL_PATH_CONVERTERS_H