Repository: vispy/vispy

Issue:
minor: border width bug in rectangleVisual
Hi,

the border width argument of the rectangle.py (visuals) is not passed to the polygon class.
The last lines of code in the file should be:

`if not self._border_color.is_blank:`
`   self.border.set_data(pos=vertices[1:, ..., :2],`
`   color=self._border_color.rgba,`
 **`width=self._border_width`**)


Discussion:
The border width is provided to the `PolygonVisual.__init__` class. It is not currently possible to change the border width after init so it shouldn't need to be passed from what I understand.

https://github.com/vispy/vispy/blob/master/vispy/visuals/rectangle.py#L62

Do you have some example code that doesn't work as you would expect?
In this code the border width of the rectangles is ignored (at least in my setup vispy-0.5.0dev).
with the above change in the rectangle.py code, the border width can be set...

`


    import numpy as np
    from vispy import gloo
    from vispy import app
    from vispy.util.transforms import perspective, translate, rotate
    from vispy import color
    from vispy import visuals
    from vispy.visuals import TextVisual


    class Canvas(app.Canvas):

        def __init__(self):
            app.Canvas.__init__(self, title='Test', keys='interactive', decorate=False, size=(1024, 800))
            self.mainVP = (0, 0, 1024, 800)
        
            self.textClose = visuals.TextVisual('Close', bold=False, color='white', font_size=18, pos=[self.mainVP[2]/2 , self.mainVP[3]/2])
            self.textClose.transforms.configure(canvas=self, viewport=self.mainVP)
            self.boxClose = visuals.RectangleVisual(center=[self.mainVP[2]/2, self.mainVP[3]/2], color=None, border_color='gray', border_width=6, height=50.0, width=100.0, radius=[10.0, 10.0, 0.0, 0.0])
            self.boxClose.transforms.configure(canvas=self, viewport=self.mainVP)
    
            self.show()

        def on_timer(self, event):        
                self.update()

        def on_resize(self, event):
            self.apply_zoom()

        def on_mouse_press(self, event):
            self.mousePress = event.pos
            clickRatio = self.mousePress / self.size
            if clickRatio[0] > 0.45  and clickRatio[0] < 0.55 and clickRatio[1] < 0.55  and clickRatio[1] > 0.45:
                print("close button")
            
        def on_draw(self, event):
            gloo.clear(color=True, depth=True,  stencil=True)      
            gloo.set_viewport(*self.mainVP)
            self.boxClose.draw()
            self.textClose.draw()

        def apply_zoom(self):
            gloo.set_viewport(0, 0, *self.physical_size)

    if __name__ == '__main__':
            c = Canvas()
    app.run()

`
@cstrub Looks good and makes sense. Looks like this was caused by some changes a long time ago when some non-related updates were made to the RectangleVisual. While I'm not super happy about how those changes were done, your change is the simplest solution. Would you mind making a pull request?
@Eric89GXL If they make a pull request and add a test for this, should they make a pull request for the `test-data` repository (https://github.com/vispy/test-data/tree/master/visuals)?
I think that's how it's supposed to work, yeah

@davidh-ssec Could you replicate the problem and the "fix"?
In my setup the "fix" only works if I have pyopengl installed (3.1.1), rather than just pyqt (4.10.4).
Without pyopengl the border width is always ignored.
I am not deep enough into vispy to understand this effect... I would prefer if someone else could take a look at this and eventually make the changes in the repository(?)

Resolving patch:
diff --git a/vispy/visuals/ellipse.py b/vispy/visuals/ellipse.py
--- a/vispy/visuals/ellipse.py
+++ b/vispy/visuals/ellipse.py
@@ -27,6 +27,8 @@ class EllipseVisual(PolygonVisual):
         The border color to use.
     border_width: float
         The width of the border in pixels
+        Line widths > 1px are only
+        guaranteed to work when using `border_method='agg'` method.
     radius : float | tuple
         Radius or radii of the ellipse
         Defaults to  (0.1, 0.1)
@@ -35,10 +37,12 @@ class EllipseVisual(PolygonVisual):
         Defaults to 0.
     span_angle : float
         Span angle of the ellipse in degrees
-        Defaults to 0.
+        Defaults to 360.
     num_segments : int
         Number of segments to be used to draw the ellipse
         Defaults to 100
+    **kwargs : dict
+        Keyword arguments to pass to `PolygonVisual`.
     """
     def __init__(self, center=None, color='black', border_color=None,
                  border_width=1, radius=(0.1, 0.1), start_angle=0.,
@@ -49,11 +53,14 @@ def __init__(self, center=None, color='black', border_color=None,
         self._span_angle = span_angle
         self._num_segments = num_segments
 
+        # triangulation can be very slow
+        kwargs.setdefault('triangulate', False)
         PolygonVisual.__init__(self, pos=None, color=color,
                                border_color=border_color,
                                border_width=border_width, **kwargs)
 
         self._mesh.mode = "triangle_fan"
+        self._regen_pos()
         self._update()
 
     @staticmethod
@@ -79,11 +86,11 @@ def _generate_vertices(center, radius, start_angle, span_angle,
                             num_segments + 1)
 
         # PolarProjection
-        vertices[:-1, 0] = center[0] + xr * np.cos(theta)
-        vertices[:-1, 1] = center[1] + yr * np.sin(theta)
+        vertices[1:, 0] = center[0] + xr * np.cos(theta)
+        vertices[1:, 1] = center[1] + yr * np.sin(theta)
 
-        # close the curve
-        vertices[num_segments + 1] = np.float32([center[0], center[1]])
+        # specify center point (not used in border)
+        vertices[0] = np.float32([center[0], center[1]])
 
         return vertices
 
@@ -98,6 +105,7 @@ def center(self, center):
         """ The center of the ellipse
         """
         self._center = center
+        self._regen_pos()
         self._update()
 
     @property
@@ -109,6 +117,7 @@ def radius(self):
     @radius.setter
     def radius(self, radius):
         self._radius = radius
+        self._regen_pos()
         self._update()
 
     @property
@@ -120,6 +129,7 @@ def start_angle(self):
     @start_angle.setter
     def start_angle(self, start_angle):
         self._start_angle = start_angle
+        self._regen_pos()
         self._update()
 
     @property
@@ -131,6 +141,7 @@ def span_angle(self):
     @span_angle.setter
     def span_angle(self, span_angle):
         self._span_angle = span_angle
+        self._regen_pos()
         self._update()
 
     @property
@@ -146,34 +157,14 @@ def num_segments(self, num_segments):
             raise ValueError('EllipseVisual must consist of more than 1 '
                              'segment')
         self._num_segments = num_segments
+        self._regen_pos()
         self._update()
 
-    def _update(self):
-        if self._center is None:
-            return
-
+    def _regen_pos(self):
         vertices = self._generate_vertices(center=self._center,
                                            radius=self._radius,
                                            start_angle=self._start_angle,
                                            span_angle=self._span_angle,
                                            num_segments=self._num_segments)
-
-        # NOTE: we do not use PolygonVisual's
-        # inbuilt update() because the triangulation method
-        # it uses is expensive. See discussion on
-        # (campagnola/vispy #2) for more details
-        if not self._color.is_blank:
-            self._mesh.set_data(vertices=vertices,
-                                color=self._color.rgba)
-
-        # connect vertices for a closed loop when
-        # drawing the border. However, delete the last
-        # vertex so there is no line connecting the center
-        # to the border
-        if not self._border_color.is_blank:
-            border_pos = vertices[:-1]
-
-            self._border.set_data(pos=border_pos,
-                                  color=self._border_color.rgba,
-                                  width=self._border_width,
-                                  connect='strip')
+        # don't use the center point
+        self._pos = vertices[1:]
diff --git a/vispy/visuals/polygon.py b/vispy/visuals/polygon.py
--- a/vispy/visuals/polygon.py
+++ b/vispy/visuals/polygon.py
@@ -34,17 +34,32 @@ class PolygonVisual(CompoundVisual):
         Border color of the polygon.
     border_width : int
         Border width in pixels.
+        Line widths > 1px are only
+        guaranteed to work when using `border_method='agg'` method.
+    border_method : str
+        Mode to use for drawing the border line (see `LineVisual`).
+
+            * "agg" uses anti-grain geometry to draw nicely antialiased lines
+              with proper joins and endcaps.
+            * "gl" uses OpenGL's built-in line rendering. This is much faster,
+              but produces much lower-quality results and is not guaranteed to
+              obey the requested line width or join/endcap styles.
+
+    triangulate : boolean
+        Triangulate the set of vertices
     **kwargs : dict
-        Keyword arguments to pass to `PolygonVisual`.
+        Keyword arguments to pass to `CompoundVisual`.
     """
     def __init__(self, pos=None, color='black',
-                 border_color=None, border_width=1, **kwargs):
+                 border_color=None, border_width=1, border_method='gl',
+                 triangulate=True, **kwargs):
         self._mesh = MeshVisual()
-        self._border = LineVisual()
+        self._border = LineVisual(method=border_method)
         self._pos = pos
         self._color = Color(color)
         self._border_width = border_width
         self._border_color = Color(border_color)
+        self._triangulate = triangulate
 
         self._update()
         CompoundVisual.__init__(self, [self._mesh, self._border], **kwargs)
@@ -53,25 +68,27 @@ def __init__(self, pos=None, color='black',
         self.freeze()
 
     def _update(self):
-        self.data = PolygonData(vertices=np.array(self._pos, dtype=np.float32))
         if self._pos is None:
             return
-        if not self._color.is_blank:
-            pts, tris = self.data.triangulate()
+        if not self._color.is_blank and self._triangulate:
+            data = PolygonData(vertices=np.array(self._pos, dtype=np.float32))
+            pts, tris = data.triangulate()
             set_state(polygon_offset_fill=False)
             self._mesh.set_data(vertices=pts, faces=tris.astype(np.uint32),
                                 color=self._color.rgba)
+        elif not self._color.is_blank:
+            self.mesh.set_data(vertices=self._pos,
+                               color=self._color.rgba)
 
         if not self._border_color.is_blank:
             # Close border if it is not already.
             border_pos = self._pos
-            if np.any(border_pos[0] != border_pos[1]):
+            if np.any(border_pos[0] != border_pos[-1]):
                 border_pos = np.concatenate([border_pos, border_pos[:1]],
                                             axis=0)
             self._border.set_data(pos=border_pos,
                                   color=self._border_color.rgba,
-                                  width=self._border_width,
-                                  connect='strip')
+                                  width=self._border_width)
 
             self._border.update()
 
diff --git a/vispy/visuals/rectangle.py b/vispy/visuals/rectangle.py
--- a/vispy/visuals/rectangle.py
+++ b/vispy/visuals/rectangle.py
@@ -28,6 +28,8 @@ class RectangleVisual(PolygonVisual):
         The border color to use.
     border_width : int
         Border width in pixels.
+        Line widths > 1px are only
+        guaranteed to work when using `border_method='agg'` method.
     height : float
         Length of the rectangle along y-axis
         Defaults to 1.0
@@ -37,6 +39,8 @@ class RectangleVisual(PolygonVisual):
     radius : float | array
         Radii of curvatures of corners in clockwise order from top-left
         Defaults to 0.
+    **kwargs : dict
+        Keyword arguments to pass to `PolygonVisual`.
     """
     def __init__(self, center=None, color='black', border_color=None,
                  border_width=1, height=1.0, width=1.0,
@@ -47,32 +51,42 @@ def __init__(self, center=None, color='black', border_color=None,
         self._color = Color(color)
         self._border_color = Color(border_color)
         self._border_width = border_width
+        self._radius = radius
+        self._center = center
 
-        # HACK, NOTE: this order is _intentional_
-        # the self._radius is set to none to block all
-        # calls to self._update(). This is because
-        # self._update() depends on having self.mesh and
-        # self.border which are instantiated by PolygonVisual's
-        # __init__
-        self._radius = None
-        self.center = None
-
+        # triangulation can be very slow
+        kwargs.setdefault('triangulate', False)
         PolygonVisual.__init__(self, pos=None, color=color,
                                border_color=border_color,
                                border_width=border_width, **kwargs)
 
-        self.radius = radius
-        self.center = center
-
         self._mesh.mode = 'triangle_fan'
+        self._regen_pos()
         self._update()
 
-    def _generate_vertices(self, center, radius, height, width):
-
-        half_height = self._height / 2.
-        half_width = self._width / 2.
+    @staticmethod
+    def _generate_vertices(center, radius, height, width):
+        half_height = height / 2.
+        half_width = width / 2.
         hw = min(half_height, half_width)
 
+        if isinstance(radius, (list, tuple)):
+            if len(radius) != 4:
+                raise ValueError("radius must be float or 4 value tuple/list"
+                                 " (got %s of length %d)" % (type(radius),
+                                                             len(radius)))
+
+            if (radius > np.ones(4) * hw).all():
+                raise ValueError('Radius of curvature cannot be greater than\
+                                  half of min(width, height)')
+            radius = np.array(radius, dtype=np.float32)
+
+        else:
+            if radius > hw:
+                raise ValueError('Radius of curvature cannot be greater than\
+                                  half of min(width, height)')
+            radius = np.ones(4) * radius
+
         num_segments = (radius / hw * 500.).astype(int)
 
         bias1 = np.ones(4) * half_width - radius
@@ -135,6 +149,7 @@ def center(self, center):
         """ The center of the ellipse
         """
         self._center = center
+        self._regen_pos()
         self._update()
 
     @property
@@ -148,6 +163,7 @@ def height(self, height):
         if height <= 0.:
             raise ValueError('Height must be positive')
         self._height = height
+        self._regen_pos()
         self._update()
 
     @property
@@ -161,6 +177,7 @@ def width(self, width):
         if width <= 0.:
             raise ValueError('Width must be positive')
         self._width = width
+        self._regen_pos()
         self._update()
 
     @property
@@ -171,55 +188,15 @@ def radius(self):
 
     @radius.setter
     def radius(self, radius):
-        half_height = self._height / 2.
-        half_width = self._width / 2.
-        hw = min(half_height, half_width)
-
-        if isinstance(radius, (list, tuple)):
-            if len(radius) != 4:
-                raise ValueError("radius must be float or 4 value tuple/list"
-                                 " (got %s of length %d)" % (type(radius),
-                                                             len(radius)))
-
-            if (radius > np.ones(4) * hw).all():
-                raise ValueError('Radius of curvature cannot be greater than\
-                                  half of min(width, height)')
-            radius = np.array(radius, dtype=np.float32)
-
-        else:
-            if radius > hw:
-                raise ValueError('Radius of curvature cannot be greater than\
-                                  half of min(width, height)')
-            radius = np.ones(4) * radius
-
         self._radius = radius
+        self._regen_pos()
         self._update()
 
-    def _update(self):
-        if not self._center:
-            return
-
-        if self._radius is None:
-            return
-
+    def _regen_pos(self):
         vertices = self._generate_vertices(center=self._center,
                                            radius=self._radius,
                                            height=self._height,
                                            width=self._width)
-
+        # don't use the center point and only use X/Y coordinates
+        vertices = vertices[1:, ..., :2]
         self._pos = vertices
-
-        # NOTE: Do not call PolygonVisual's _update() here because it
-        # uses triangulation which is super slow
-        if not self._color.is_blank:
-            self.mesh.set_data(vertices=vertices,
-                               color=self._color.rgba)
-
-        # when passing vertices to the Border, delete the
-        # vertices that creates loops since PolygonVisual
-        # closes loops. However, closing the loop of a solid figure
-        # will create a solid line from the center of the solid figure
-        # to the edge
-        if not self._border_color.is_blank:
-            self.border.set_data(pos=vertices[1:, ..., :2],
-                                 color=self._border_color.rgba)


Test patch:
diff --git a/vispy/testing/image_tester.py b/vispy/testing/image_tester.py
--- a/vispy/testing/image_tester.py
+++ b/vispy/testing/image_tester.py
@@ -149,7 +149,8 @@ def assert_image_approved(image, standard_file, message=None, **kwargs):
             if std_image is None:
                 raise Exception("Test standard %s does not exist." % std_file)
             else:
-                if os.getenv('TRAVIS') is not None:
+                if os.getenv('TRAVIS') is not None or \
+                        os.getenv('APPVEYOR') is not None:
                     _save_failed_test(image, std_image, standard_file)
                 raise
 
@@ -375,7 +376,7 @@ def get_test_data_repo():
     # This tag marks the test-data commit that this version of vispy should
     # be tested against. When adding or changing test images, create
     # and push a new tag and update this variable.
-    test_data_tag = 'test-data-6'
+    test_data_tag = 'test-data-7'
 
     data_path = config['test_data_path']
     git_path = 'http://github.com/vispy/test-data'
diff --git a/vispy/visuals/tests/test_rectangle.py b/vispy/visuals/tests/test_rectangle.py
--- a/vispy/visuals/tests/test_rectangle.py
+++ b/vispy/visuals/tests/test_rectangle.py
@@ -53,6 +53,15 @@ def test_rectangle_draw():
 
         assert_image_approved(c.render(), 'visuals/rectpolygon5.png')
 
+        rectpolygon.parent = None
+        rectpolygon = visuals.Rectangle(center=(50, 50, 0), height=60.,
+                                        width=80., radius=[25, 10, 0, 15],
+                                        color='red', border_color=(0, 1, 1, 1),
+                                        border_width=5, border_method='agg',
+                                        parent=c.scene)
+
+        assert_image_approved(c.render(), 'visuals/rectpolygon10.png')
+
 
 @requires_application()
 def test_rectpolygon_draw():