Repository: celiagg/celiagg (language: python)

Issue:
Rethink drawing multiple instances of a path at points
Currently, `ShapeAtPoints` is the fastest way to draw a single `Path` object at multiple points. However, with a combined fill and stroke drawing mode (`DrawingMode.DrawFillStroke`), the rasterized result is not quite correct. First the paths are drawn with a fill, then drawn again with a stroke. That results in some stroked areas, which would normally have been overdrawn by filled areas, showing in the image.


Discussion / hints:

Fix patch:
diff --git a/celiagg/_ndarray_canvas.pxd b/celiagg/_ndarray_canvas.pxd
index bf12944..65749cd 100644
--- a/celiagg/_ndarray_canvas.pxd
+++ b/celiagg/_ndarray_canvas.pxd
@@ -68,6 +68,12 @@ cdef extern from "ndarray_canvas.h":
                         const _transform.trans_affine& transform,
                         _paint.Paint& linePaint, _paint.Paint& fillPaint,
                         const _graphics_state.GraphicsState& gs)
+        void draw_shape_at_points(_vertex_source.VertexSource& shape,
+                                  const double* points,
+                                  const size_t point_count,
+                                  const _transform.trans_affine& transform,
+                                  _paint.Paint& linePaint, _paint.Paint& fillPaint,
+                                  const _graphics_state.GraphicsState& gs)
         void draw_text(const char* text, _font.Font& font,
                        const _transform.trans_affine& transform,
                        _paint.Paint& linePaint, _paint.Paint& fillPaint,
diff --git a/celiagg/ndarray_canvas.h b/celiagg/ndarray_canvas.h
index 1b21d89..b9a627f 100644
--- a/celiagg/ndarray_canvas.h
+++ b/celiagg/ndarray_canvas.h
@@ -79,6 +79,12 @@ class ndarray_canvas_base
                             const agg::trans_affine& transform,
                             Paint& linePaint, Paint& fillPaint,
                             const GraphicsState& gs) = 0;
+    virtual void draw_shape_at_points(VertexSource& shape,
+                                      const double* points,
+                                      const size_t point_count,
+                                      const agg::trans_affine& transform,
+                                      Paint& linePaint, Paint& fillPaint,
+                                      const GraphicsState& gs) = 0;
     virtual void draw_text(const char* text, Font& font,
                            const agg::trans_affine& transform,
                            Paint& linePaint, Paint& fillPaint,
@@ -108,6 +114,12 @@ class ndarray_canvas : public ndarray_canvas_base
                     const agg::trans_affine& transform,
                     Paint& linePaint, Paint& fillPaint,
                     const GraphicsState& gs);
+    void draw_shape_at_points(VertexSource& shape,
+                              const double* points,
+                              const size_t point_count,
+                              const agg::trans_affine& transform,
+                              Paint& linePaint, Paint& fillPaint,
+                              const GraphicsState& gs);
     void draw_text(const char* text, Font& font,
                    const agg::trans_affine& transform,
                    Paint& linePaint, Paint& fillPaint,
diff --git a/celiagg/ndarray_canvas.hxx b/celiagg/ndarray_canvas.hxx
index a881185..efcac22 100644
--- a/celiagg/ndarray_canvas.hxx
+++ b/celiagg/ndarray_canvas.hxx
@@ -117,6 +117,43 @@ void ndarray_canvas<pixfmt_t>::draw_shape(VertexSource& shape,
     }
 }
 
+template<typename pixfmt_t>
+void ndarray_canvas<pixfmt_t>::draw_shape_at_points(VertexSource& shape,
+    const double* points, const size_t point_count,
+    const agg::trans_affine& transform, Paint& linePaint, Paint& fillPaint,
+    const GraphicsState& gs)
+{
+    agg::simple_polygon_vertex_source _points(points, point_count, false, false);
+    agg::trans_affine pt_trans;
+    unsigned cmd;
+
+    _set_clipping(gs.clip_box());
+    linePaint.master_alpha(gs.master_alpha());
+    fillPaint.master_alpha(gs.master_alpha());
+
+    if (gs.stencil() == NULL)
+    {
+        for (;;)
+        {
+            cmd = _points.vertex(&pt_trans.tx, &pt_trans.ty);
+            if (cmd == agg::path_cmd_end_poly) break;
+
+            _draw_shape_internal(shape, pt_trans * transform, linePaint, fillPaint, gs, m_renderer);
+        }
+    }
+    else
+    {
+        _WITH_MASKED_RENDERER(gs, renderer)
+        for (;;)
+        {
+            cmd = _points.vertex(&pt_trans.tx, &pt_trans.ty);
+            if (cmd == agg::path_cmd_end_poly) break;
+
+            _draw_shape_internal(shape, pt_trans * transform, linePaint, fillPaint, gs, renderer);
+        }
+    }
+}
+
 template<typename pixfmt_t>
 void ndarray_canvas<pixfmt_t>::draw_text(const char* text,
     Font& font, const agg::trans_affine& transform,
diff --git a/celiagg/ndarray_canvas.pxi b/celiagg/ndarray_canvas.pxi
index ec5e6aa..98d9106 100644
--- a/celiagg/ndarray_canvas.pxi
+++ b/celiagg/ndarray_canvas.pxi
@@ -198,6 +198,57 @@ cdef class CanvasBase:
                               dereference(fill_paint._this),
                               dereference(gs._this))
 
+    def draw_shape_at_points(self, shape, points, transform, state, stroke=None, fill=None):
+        """draw_shape_at_points(shape, points, transform, state, stroke=SolidColor(0, 0, 0), fill=SolidColor(0, 0, 0))
+        Draw a shape at multiple points on the canvas.
+
+        .. note::
+           Use ``GraphicsState.drawing_mode`` to enable/disable stroke or fill
+           drawing.
+
+        :param shape: A ``VertexSource`` object
+        :param points: A sequence of (x, y) pairs where the ``shape`` should be drawn.
+        :param transform: A ``Transform`` object
+        :param state: A ``GraphicsState`` object
+        :param stroke: The ``Paint`` to use for outlines. Defaults to black.
+        :param fill: The ``Paint`` to use for fills. Defaults to black.
+        """
+        if not isinstance(shape, VertexSource):
+            raise TypeError("shape must be a VertexSource (Path, BSpline, etc)")
+        if not isinstance(transform, Transform):
+            raise TypeError("transform must be a Transform instance")
+        if not isinstance(state, GraphicsState):
+            raise TypeError("state must be a GraphicsState instance")
+        if stroke is not None and not isinstance(stroke, Paint):
+            raise TypeError("stroke must be a Paint instance")
+        if fill is not None and not isinstance(fill, Paint):
+            raise TypeError("fill must be a Paint instance")
+
+        cdef:
+            VertexSource shp = <VertexSource>shape
+            double[:,::1] _points = numpy.asarray(points, dtype=numpy.float64,
+                                                  order='c')
+            GraphicsState gs = <GraphicsState>state
+            Transform trans = <Transform>transform
+            PixelFormat fmt = self.pixel_format
+            Paint stroke_paint
+            Paint fill_paint
+
+        if _points.shape[1] != 2:
+            msg = 'Points argument must be an iterable of (x, y) pairs.'
+            raise ValueError(msg)
+
+        self._check_stencil(gs)
+        stroke_paint = self._get_native_paint(stroke, fmt)
+        fill_paint = self._get_native_paint(fill, fmt)
+
+        self._this.draw_shape_at_points(dereference(shp._this),
+                                        &_points[0][0], _points.shape[0],
+                                        dereference(trans._this),
+                                        dereference(stroke_paint._this),
+                                        dereference(fill_paint._this),
+                                        dereference(gs._this))
+
     def draw_text(self, text, font, transform, state, stroke=None, fill=None):
         """draw_text(text, font, transform, state, stroke=SolidColor(0, 0, 0), fill=SolidColor(0, 0, 0))
         Draw a line of text on the canvas.


Test patch:
diff --git a/celiagg/tests/test_drawing.py b/celiagg/tests/test_drawing.py
index b41b7e6..8f15046 100644
--- a/celiagg/tests/test_drawing.py
+++ b/celiagg/tests/test_drawing.py
@@ -132,3 +132,19 @@ def test_ellipse(self):
             path, self.transform, self.state, stroke=self.paint
         )
         assert_equal(expected, self.canvas.array)
+
+    def test_draw_shape_at_points(self):
+        path = agg.Path()
+        path.ellipse(0, 0, 0.5, 0.5)
+        points = [(0.0, 0.0), (0.0, 5.0), (5.0, 0.0), (5.0, 5.0), (2.5, 2.5)]
+        expected = [
+            [1, 0, 0, 0, 1],
+            [0, 0, 0, 0, 0],
+            [0, 0, 1, 0, 0],
+            [0, 0, 0, 0, 0],
+            [1, 0, 0, 0, 1],
+        ]
+        self.canvas.draw_shape_at_points(
+            path, points, self.transform, self.state, stroke=self.paint
+        )
+        assert_equal(expected, self.canvas.array)