diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 2356930429f0..8192d514f4ff 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -671,19 +671,21 @@ def get_sketch_params(self): ------- tuple or None - A 3-tuple with the following elements: + A 4-tuple with the following elements: - *scale*: The amplitude of the wiggle perpendicular to the source line. - *length*: The length of the wiggle along the line. - *randomness*: The scale factor by which the length is shrunken or expanded. + - *seed*: Seed for the internal pseudo-random number + generator. Returns *None* if no sketch parameters were set. """ 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=0): """ Set the sketch parameters. @@ -702,13 +704,18 @@ 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. + seed : int, optional + Seed for the internal pseudo-random number generator. + For the same seed, the result will be exactly the same. - .. ACCEPTS: (scale: float, length: float, randomness: float) + .. versionadded:: 3.8 + + .. ACCEPTS: (scale: float, length: float, randomness: float, seed: int) """ if scale is None: self._sketch = None else: - self._sketch = (scale, length or 128.0, randomness or 16.0) + self._sketch = (scale, length or 128.0, randomness or 16.0, seed) self.stale = True def set_path_effects(self, path_effects): diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index d0ff3fec9c4d..136266c7e7f3 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1062,7 +1062,7 @@ 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=0): """ Set the sketch parameters. @@ -1076,10 +1076,12 @@ 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. """ self._sketch = ( None if scale is None - else (scale, length or 128., randomness or 16.)) + else (scale, length or 128., randomness or 16., seed)) class TimerBase: diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 8b6c3669b967..e76009315e6e 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -823,8 +823,8 @@ def draw(self, renderer): ec_rgba = ec_rgba[:3] + (fc_rgba[3],) 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) + scale, length, randomness, seed = self.get_sketch_params() + gc.set_sketch_params(scale/2, length/2, 2*randomness, seed) marker = self._marker diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 27b677195470..ceee64ed053b 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -668,7 +668,7 @@ def setp(obj, *args, **kwargs): def xkcd( - scale: float = 1, length: float = 100, randomness: float = 2 + scale: float = 1, length: float = 100, randomness: float = 2, seed: int = -1 ) -> ExitStack: """ Turn on `xkcd `_ sketch-style drawing mode. This will @@ -685,6 +685,10 @@ 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. The special + value of -1 will make the randomness different for each object + processed. Notes ----- @@ -717,7 +721,7 @@ def xkcd( 'font.family': ['xkcd', 'xkcd Script', 'Humor Sans', 'Comic Neue', 'Comic Sans MS'], 'font.size': 14.0, - 'path.sketch': (scale, length, randomness), + 'path.sketch': (scale, length, randomness, seed), 'path.effects': [ patheffects.withStroke(linewidth=4, foreground="w")], 'axes.linewidth': 1.5, diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index a9bebd209077..658df518a9e1 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -549,7 +549,14 @@ def validate_sketch(s): try: return tuple(_listify_validator(validate_float, n=3)(s)) except ValueError: - raise ValueError("Expected a (scale, length, randomness) triplet") + try: + result = tuple(_listify_validator(validate_float, n=4)(s)) + # make sure seed is an integer + return (result[0], result[1], result[2], int(result[3])) + except ValueError: + raise ValueError( + "path.sketch must be a 3-tuple (scale, length, randomness) or" + " a 4-tuple (scale, length, randomness, seed)") def _validate_greaterequal0_lessthan1(s): diff --git a/setupext.py b/setupext.py index a91c681e0f58..351bea41dd62 100644 --- a/setupext.py +++ b/setupext.py @@ -434,6 +434,7 @@ def get_extensions(self): ext = Extension( "matplotlib._path", [ "src/py_converters.cpp", + "src/path_converters.cpp", "src/_path_wrapper.cpp", ]) add_numpy_flags(ext) diff --git a/src/_backend_agg.h b/src/_backend_agg.h index f15fa05dd5fd..6ed5d519b52b 100644 --- a/src/_backend_agg.h +++ b/src/_backend_agg.h @@ -483,7 +483,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); } diff --git a/src/_backend_agg_basic_types.h b/src/_backend_agg_basic_types.h index 21a84bb6a5ae..ad0254ae04ab 100644 --- a/src/_backend_agg_basic_types.h +++ b/src/_backend_agg_basic_types.h @@ -23,6 +23,7 @@ struct SketchParams double scale; double length; double randomness; + int seed; }; class Dashes diff --git a/src/_path.h b/src/_path.h index bfbb7b49d254..aff6a0579ddf 100644 --- a/src/_path.h +++ b/src/_path.h @@ -1067,7 +1067,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); } } @@ -1232,7 +1232,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_wrapper.cpp b/src/_path_wrapper.cpp index e1cb0a4f89af..c372ca956563 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -4,6 +4,7 @@ #include "py_converters.h" #include "py_adaptors.h" +#include "path_converters.h" PyObject *convert_polygon_vector(std::vector &polygons) { @@ -842,6 +843,22 @@ static PyObject *Py_is_sorted(PyObject *self, PyObject *obj) } +const char *Py_sketch_reset_previous_seed__doc__ = "sketch_reset_previous_seed(seed)"; + +static PyObject *Py_sketch_reset_previous_seed(PyObject *self, PyObject *args, PyObject *kwds) +{ + int seed; + + if (!PyArg_ParseTuple(args,"i:sketch_reset_previous_seed", &seed)) { + return NULL; + } + + CALL_CPP("SketchBase::reset_previous_seed",(SketchBase::reset_previous_seed(seed))); + + Py_RETURN_NONE; +} + + static PyMethodDef module_functions[] = { {"point_in_path", (PyCFunction)Py_point_in_path, METH_VARARGS, Py_point_in_path__doc__}, {"points_in_path", (PyCFunction)Py_points_in_path, METH_VARARGS, Py_points_in_path__doc__}, @@ -861,6 +878,7 @@ static PyMethodDef module_functions[] = { {"cleanup_path", (PyCFunction)Py_cleanup_path, METH_VARARGS, Py_cleanup_path__doc__}, {"convert_to_string", (PyCFunction)Py_convert_to_string, METH_VARARGS, Py_convert_to_string__doc__}, {"is_sorted", (PyCFunction)Py_is_sorted, METH_O, Py_is_sorted__doc__}, + {"sketch_reset_previous_seed", (PyCFunction)Py_sketch_reset_previous_seed, METH_VARARGS, Py_sketch_reset_previous_seed__doc__}, {NULL} }; diff --git a/src/path_converters.cpp b/src/path_converters.cpp new file mode 100644 index 000000000000..70d1438c8462 --- /dev/null +++ b/src/path_converters.cpp @@ -0,0 +1,8 @@ +/* -*- mode: c++; c-basic-offset: 4 -*- */ + +#define NO_IMPORT_ARRAY + + +#include "path_converters.h" + +int SketchBase::previous_seed=0; diff --git a/src/path_converters.h b/src/path_converters.h index 8583d84855aa..ce7bd1efea90 100644 --- a/src/path_converters.h +++ b/src/path_converters.h @@ -991,8 +991,36 @@ class PathSimplifier : protected EmbeddedQueue<9> } }; +// XXX: remove!!! +// #include + +class SketchBase +{ + static int previous_seed; + protected: + /* handle the special-case value -1 which means: use the + previous seed plus one. If not a special value, only + remember it as the last one and return it. + */ + int get_prng_seed(int seed) + { + if (seed==-1) { + previous_seed++; + } else { + previous_seed = seed; + } + // std::cerr<<"Seeding PRNG with "< -class Sketch +class Sketch: SketchBase { public: /* @@ -1004,8 +1032,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), @@ -1015,9 +1045,13 @@ class Sketch m_last_y(0.0), m_has_last(false), m_p(0.0), - m_rand(0) + m_rand(0), + /* if scale==0. (e.g. with test), then seed is 0 + that would set previous_seed to 0 which is not what we want. + */ + m_seed0( scale!=0 ? get_prng_seed(seed) : 0) { - rewind(0); + rewind(0); // re-seeds PRNG with m_seed0 again const double d_M_PI = 3.14159265358979323846; m_p_scale = (2.0 * d_M_PI) / (m_length * m_randomness); m_log_randomness = 2.0 * log(m_randomness); @@ -1081,7 +1115,7 @@ class Sketch m_has_last = false; m_p = 0.0; if (m_scale != 0.0) { - m_rand.seed(0); + m_rand.seed(m_seed0); m_segmented.rewind(path_id); } else { m_source->rewind(path_id); @@ -1101,6 +1135,7 @@ class Sketch RandomNumberGenerator m_rand; double m_p_scale; double m_log_randomness; + int m_seed0; }; #endif // MPL_PATH_CONVERTERS_H diff --git a/src/py_converters.cpp b/src/py_converters.cpp index cd4013bb30ad..b472fb30668f 100644 --- a/src/py_converters.cpp +++ b/src/py_converters.cpp @@ -451,13 +451,16 @@ int convert_sketch_params(PyObject *obj, void *sketchp) { SketchParams *sketch = (SketchParams *)sketchp; + sketch->seed=0; // default + if (obj == NULL || obj == Py_None) { sketch->scale = 0.0; } else if (!PyArg_ParseTuple(obj, - "ddd:sketch_params", + "ddd|i:sketch_params", &sketch->scale, &sketch->length, - &sketch->randomness)) { + &sketch->randomness, + &sketch->seed)) { return 0; }