Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-vispy for openSUSE:Factory 
checked in at 2023-07-19 19:10:45
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-vispy (Old)
 and      /work/SRC/openSUSE:Factory/.python-vispy.new.5570 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-vispy"

Wed Jul 19 19:10:45 2023 rev:11 rq:1099362 version:0.13.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-vispy/python-vispy.changes        
2023-03-24 15:20:11.298815116 +0100
+++ /work/SRC/openSUSE:Factory/.python-vispy.new.5570/python-vispy.changes      
2023-07-19 19:10:47.352620607 +0200
@@ -1,0 +2,14 @@
+Wed Jul 19 04:42:21 UTC 2023 - Steve Kowalik <steven.kowa...@suse.com>
+
+- Update to 0.13.0:
+  * Switch MarkersVisual scaling option to string "fixed", "scene", or
+    "visual"
+  * Add early-termination optimization to attenuated mip
+  * Add `InstancedMeshVisual` for faster and easier rendering of repeated
+    meshes
+  * Instanced mesh example
+  * Use QNativeEventGesture for touchpad gesture input
+  * Fix TypeError with pinch-to-zoom 
+- Replace setuptools_scm_git_archive BuildRequires with setuptools_scm.
+
+-------------------------------------------------------------------

Old:
----
  vispy-0.12.2.tar.gz

New:
----
  vispy-0.13.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-vispy.spec ++++++
--- /var/tmp/diff_new_pack.GOLwY1/_old  2023-07-19 19:10:48.132625169 +0200
+++ /var/tmp/diff_new_pack.GOLwY1/_new  2023-07-19 19:10:48.136625192 +0200
@@ -18,7 +18,7 @@
 
 %bcond_without  ext_deps
 Name:           python-vispy
-Version:        0.12.2
+Version:        0.13.0
 Release:        0
 Summary:        Interactive visualization in Python
 License:        BSD-3-Clause
@@ -28,7 +28,7 @@
 BuildRequires:  %{python_module devel}
 BuildRequires:  %{python_module numpy-devel}
 BuildRequires:  %{python_module pip}
-BuildRequires:  %{python_module setuptools_scm_git_archive}
+BuildRequires:  %{python_module setuptools_scm}
 BuildRequires:  %{python_module wheel}
 BuildRequires:  fdupes
 BuildRequires:  jupyter-notebook-filesystem
@@ -118,6 +118,6 @@
 %doc *.rst *.md
 %license LICENSE.txt
 %{python_sitearch}/vispy
-%{python_sitearch}/vispy-%{version}*-info
+%{python_sitearch}/vispy-%{version}.dist-info
 
 %changelog

++++++ vispy-0.12.2.tar.gz -> vispy-0.13.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/.github/workflows/wheels.yml 
new/vispy-0.13.0/.github/workflows/wheels.yml
--- old/vispy-0.12.2/.github/workflows/wheels.yml       2023-03-20 
16:16:17.000000000 +0100
+++ new/vispy-0.13.0/.github/workflows/wheels.yml       2023-05-12 
20:06:40.000000000 +0200
@@ -27,7 +27,7 @@
       if: ${{ matrix.arch == 'aarch64' }}
       uses: docker/setup-qemu-action@v2
     - name: Build wheels
-      uses: pypa/cibuildwheel@v2.12.1
+      uses: pypa/cibuildwheel@v2.12.3
       env:
         CIBW_SKIP: "cp36-* pp* *-win32 *-manylinux_i686 *-musllinux*"
         CIBW_ARCHS_LINUX: ${{ matrix.arch }}
@@ -42,6 +42,8 @@
         CIBW_ENVIRONMENT: "GITHUB_ACTIONS=true"
         CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014
         CIBW_MANYLINUX_I686_IMAGE: manylinux2014
+        CIBW_ARCHS_MACOS: 'x86_64 arm64'
+        CIBW_TEST_SKIP: '*-macosx_arm64'
     - uses: actions/upload-artifact@v3
       with:
         path: ./wheelhouse/*.whl
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/CHANGELOG.md 
new/vispy-0.13.0/CHANGELOG.md
--- old/vispy-0.12.2/CHANGELOG.md       2023-03-20 16:16:17.000000000 +0100
+++ new/vispy-0.13.0/CHANGELOG.md       2023-05-12 20:06:40.000000000 +0200
@@ -1,5 +1,24 @@
 # Release Notes
 
+## [v0.13.0](https://github.com/vispy/vispy/tree/v0.13.0) (2023-05-12)
+
+**Enhancements:**
+
+- Switch MarkersVisual scaling option to string "fixed", "scene", or "visual" 
[\#2470](https://github.com/vispy/vispy/pull/2470) 
([djhoese](https://github.com/djhoese))
+- Add early-termination optimization to attenuated mip 
[\#2465](https://github.com/vispy/vispy/pull/2465) 
([aganders3](https://github.com/aganders3))
+- Add `InstancedMeshVisual` for faster and easier rendering of repeated meshes 
[\#2461](https://github.com/vispy/vispy/pull/2461) 
([brisvag](https://github.com/brisvag))
+- Instanced mesh example [\#2460](https://github.com/vispy/vispy/pull/2460) 
([brisvag](https://github.com/brisvag))
+- Use QNativeEventGesture for touchpad gesture input 
[\#2456](https://github.com/vispy/vispy/pull/2456) 
([aganders3](https://github.com/aganders3))
+
+**Fixed bugs:**
+
+- Fix TypeError with pinch-to-zoom 
[\#2483](https://github.com/vispy/vispy/pull/2483) 
([aganders3](https://github.com/aganders3))
+
+**Merged pull requests:**
+
+- Bump pypa/cibuildwheel from 2.12.1 to 2.12.3 
[\#2472](https://github.com/vispy/vispy/pull/2472) 
([dependabot[bot]](https://github.com/apps/dependabot))
+- Cleanup site navbar with pydata-sphinx-theme 0.10+ 
[\#2371](https://github.com/vispy/vispy/pull/2371) 
([djhoese](https://github.com/djhoese))
+
 ## [v0.12.2](https://github.com/vispy/vispy/tree/v0.12.2) (2023-03-20)
 
 **Enhancements:**
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/PKG-INFO new/vispy-0.13.0/PKG-INFO
--- old/vispy-0.12.2/PKG-INFO   2023-03-20 16:16:37.000000000 +0100
+++ new/vispy-0.13.0/PKG-INFO   2023-05-12 20:07:07.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: vispy
-Version: 0.12.2
+Version: 0.13.0
 Summary: Interactive visualization in Python
 Home-page: http://vispy.org
 Download-URL: https://pypi.python.org/pypi/vispy
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/doc/conf.py new/vispy-0.13.0/doc/conf.py
--- old/vispy-0.12.2/doc/conf.py        2023-03-20 16:16:17.000000000 +0100
+++ new/vispy-0.13.0/doc/conf.py        2023-05-12 20:06:40.000000000 +0200
@@ -186,6 +186,7 @@
     "use_edit_page_button": True,
     "github_url": "https://github.com/vispy/vispy";,
     "twitter_url": "https://twitter.com/vispyproject";,
+    "header_links_before_dropdown": 7,
 }
 
 # Add any paths that contain custom themes here, relative to this directory.
@@ -346,11 +347,11 @@
 # -----------------------------------------------------------------------------
 # intersphinx
 # -----------------------------------------------------------------------------
-_python_doc_base = 'https://docs.python.org/3.9'
+_python_doc_base = "https://docs.python.org/3";
 intersphinx_mapping = {
-    _python_doc_base: None,
-    'https://numpy.org/doc/stable/': None,
-    'https://scipy.github.io/devdocs/': None,
+    "python": (_python_doc_base, None),
+    "numpy": ("https://numpy.org/doc/stable/";, None),
+    "scipy": ("https://scipy.github.io/devdocs/";, None),
 }
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/examples/basics/visuals/markers.py 
new/vispy-0.13.0/examples/basics/visuals/markers.py
--- old/vispy-0.12.2/examples/basics/visuals/markers.py 2023-03-20 
16:16:17.000000000 +0100
+++ new/vispy-0.13.0/examples/basics/visuals/markers.py 2023-05-12 
20:06:40.000000000 +0200
@@ -4,7 +4,13 @@
 # Copyright (c) Vispy Development Team. All Rights Reserved.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
 # -----------------------------------------------------------------------------
-""" Display markers at different sizes and line thicknessess.
+"""Display markers at different sizes and line thicknesses.
+
+Keyboard options:
+* spacebar: Cycle through possible marker symbols.
+* "s": Switch between "fixed" marker scaling (initial setting) and "scene"
+  scaling.
+
 """
 
 import numpy as np
@@ -60,10 +66,11 @@
             self.markers.symbol = self.markers.symbols[self.index]
             self.update()
         elif event.text == 's':
-            self.markers.scaling = not self.markers.scaling
+            self.markers.scaling = "fixed" if self.markers.scaling != "fixed" 
else "scene"
             self.update()
 
 
 if __name__ == '__main__':
+    print(__doc__)
     canvas = Canvas()
     app.run()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/examples/scene/instanced_mesh.py 
new/vispy-0.13.0/examples/scene/instanced_mesh.py
--- old/vispy-0.12.2/examples/scene/instanced_mesh.py   1970-01-01 
01:00:00.000000000 +0100
+++ new/vispy-0.13.0/examples/scene/instanced_mesh.py   2023-05-12 
20:06:40.000000000 +0200
@@ -0,0 +1,117 @@
+# -*- coding: utf-8 -*-
+# vispy: gallery 5
+# -----------------------------------------------------------------------------
+# Copyright (c) Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+Instanced rendering of arbitrarily transformed meshes
+=====================================================
+"""
+
+from vispy import app, gloo, visuals, scene, use
+import numpy as np
+from scipy.spatial.transform import Rotation
+from vispy.io import read_mesh, load_data_file
+
+# full gl+ context is required for instanced rendering
+use(gl='gl+')
+
+
+vertex_shader = """
+// these attributes will be defined on an instance basis
+attribute vec3 shift;
+attribute vec4 color;
+attribute vec3 transform_x;
+attribute vec3 transform_y;
+attribute vec3 transform_z;
+
+varying vec4 v_color;
+
+void main() {
+    v_color = color;
+    // transform is generated from column vectors (new basis vectors)
+    // 
https://en.wikibooks.org/wiki/GLSL_Programming/Vector_and_Matrix_Operations#Constructors
+    mat3 instance_transform = mat3(transform_x, transform_y, transform_z);
+    vec3 pos_rotated = instance_transform * $position;
+    vec4 pos_shifted = vec4(pos_rotated + shift, 1);
+    gl_Position = $transform(pos_shifted);
+}
+"""
+
+fragment_shader = """
+varying vec4 v_color;
+
+void main() {
+  gl_FragColor = v_color;
+}
+"""
+
+
+class InstancedMeshVisual(visuals.Visual):
+    def __init__(self, vertices, faces, positions, colors, transforms, 
subdivisions=5):
+        visuals.Visual.__init__(self, vertex_shader, fragment_shader)
+
+        self.set_gl_state('translucent', depth_test=True, cull_face=True)
+        self._draw_mode = 'triangles'
+
+        # set up vertex and index buffer
+        self.vbo = gloo.VertexBuffer(vertices.astype(np.float32))
+        self.shared_program.vert['position'] = self.vbo
+        self._index_buffer = gloo.IndexBuffer(data=faces.astype(np.uint32))
+
+        # create a vertex buffer with a divisor argument of 1. This means that 
the
+        # attribute value is set to the next element of the array every 1 
instance.
+        # The length of the array multiplied by the divisor determines the 
number
+        # of instances
+        self.shifts = gloo.VertexBuffer(positions.astype(np.float32), 
divisor=1)
+        self.shared_program['shift'] = self.shifts
+
+        # vispy does not handle matrix attributes (likely requires some big 
changes in GLIR)
+        # so we decompose it into three vec3; (column vectors of the matrix)
+        transforms = transforms.astype(np.float32)
+        self.transforms_x = gloo.VertexBuffer(transforms[..., 0].copy(), 
divisor=1)
+        self.transforms_y = gloo.VertexBuffer(transforms[..., 1].copy(), 
divisor=1)
+        self.transforms_z = gloo.VertexBuffer(transforms[..., 2].copy(), 
divisor=1)
+        self.shared_program['transform_x'] = self.transforms_x
+        self.shared_program['transform_y'] = self.transforms_y
+        self.shared_program['transform_z'] = self.transforms_z
+
+        # we can provide additional buffers with different divisors, as long 
as the
+        # amount of instances (length * divisor) is the same. In this case, we 
will change
+        # color every 5 instances
+        self.color = gloo.VertexBuffer(colors.astype(np.float32), divisor=1)
+        self.shared_program['color'] = self.color
+
+    def _prepare_transforms(self, view):
+        view.view_program.vert['transform'] = view.get_transform()
+
+
+# create a visual node class to add it to the canvas
+InstancedMesh = scene.visuals.create_visual_node(InstancedMeshVisual)
+
+# set up vanvas
+canvas = scene.SceneCanvas(keys='interactive', show=True)
+view = canvas.central_widget.add_view()
+view.camera = 'arcball'
+view.camera.scale_factor = 1000
+
+N = 1000
+
+mesh_file = load_data_file('orig/triceratops.obj.gz')
+vertices, faces, _, _ = read_mesh(mesh_file)
+
+np.random.seed(0)
+pos = (np.random.rand(N, 3) - 0.5) * 1000
+colors = np.random.rand(N, 4)
+transforms = Rotation.random(N).as_matrix()
+
+multimesh = InstancedMesh(vertices * 10, faces, pos, colors, transforms, 
parent=view.scene)
+# global transforms are applied correctly after the individual instance 
transforms!
+multimesh.transform = visuals.transforms.STTransform(scale=(3, 2, 1))
+
+
+if __name__ == '__main__':
+    import sys
+    if sys.flags.interactive != 1:
+        app.run()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/examples/scene/instanced_mesh_visual.py 
new/vispy-0.13.0/examples/scene/instanced_mesh_visual.py
--- old/vispy-0.12.2/examples/scene/instanced_mesh_visual.py    1970-01-01 
01:00:00.000000000 +0100
+++ new/vispy-0.13.0/examples/scene/instanced_mesh_visual.py    2023-05-12 
20:06:40.000000000 +0200
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+# vispy: gallery 30
+# -----------------------------------------------------------------------------
+# Copyright (c) Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+"""
+Instanced Mesh Visual
+=====================
+
+Show usage of the InstancedMesh visual and its filters.
+"""
+
+from itertools import cycle
+
+import numpy as np
+from scipy.spatial.transform import Rotation
+from vispy import app, scene, use
+from vispy.io import imread, load_data_file, read_mesh
+from vispy.scene.visuals import InstancedMesh
+from vispy.visuals.filters import InstancedShadingFilter, WireframeFilter, 
TextureFilter
+
+# needed for instanced rendering to work
+use(gl='gl+')
+
+
+mesh_path = load_data_file('spot/spot.obj.gz')
+texture_path = load_data_file('spot/spot.png')
+vertices, faces, normals, texcoords = read_mesh(mesh_path)
+texture = np.flipud(imread(texture_path))
+
+canvas = scene.SceneCanvas(keys='interactive', bgcolor='white', show=True)
+view = canvas.central_widget.add_view()
+
+view.camera = 'arcball'
+view.camera.depth_value = 10 * (vertices.max() - vertices.min())
+
+n_instances = 100
+
+instance_colors = np.random.rand(n_instances, 3).astype(np.float32)
+instance_positions = ((np.random.rand(n_instances, 3) - 0.5) * 
10).astype(np.float32)
+face_colors = np.random.rand(len(faces), 3)
+instance_transforms = 
Rotation.random(n_instances).as_matrix().astype(np.float32)
+
+# Create a colored `MeshVisual`.
+mesh = InstancedMesh(
+    vertices,
+    faces,
+    instance_colors=instance_colors,
+    face_colors=face_colors,
+    instance_positions=instance_positions,
+    instance_transforms=instance_transforms,
+    parent=view.scene,
+)
+
+
+wireframe_filter = WireframeFilter(width=1)
+shading_filter = InstancedShadingFilter('smooth', shininess=1)
+texture_filter = TextureFilter(texture, texcoords)
+mesh.attach(wireframe_filter)
+mesh.attach(shading_filter)
+mesh.attach(texture_filter)
+
+
+def attach_headlight(view):
+    light_dir = (0, 1, 0, 0)
+    shading_filter.light_dir = light_dir[:3]
+    initial_light_dir = view.camera.transform.imap(light_dir)
+
+    @view.scene.transform.changed.connect
+    def on_transform_change(event):
+        transform = view.camera.transform
+        shading_filter.light_dir = transform.map(initial_light_dir)[:3]
+
+
+attach_headlight(view)
+
+
+shading_cycle = cycle(['flat', None, 'smooth'])
+color_cycle = cycle([None, instance_colors])
+face_color_cycle = cycle([None, face_colors])
+
+
+@canvas.events.key_press.connect
+def on_key_press(event):
+    if event.key == "t":
+        texture_filter.enabled = not texture_filter.enabled
+        canvas.update()
+    if event.key == 's':
+        shading_filter.shading = next(shading_cycle)
+        canvas.update()
+    if event.key == 'c':
+        mesh.instance_colors = next(color_cycle)
+        canvas.update()
+    if event.key == 'f':
+        mesh.set_data(
+            vertices=vertices,
+            faces=faces,
+            face_colors=next(face_color_cycle),
+        )
+        canvas.update()
+    if event.key == 'w':
+        wireframe_filter.enabled = not wireframe_filter.enabled
+        canvas.update()
+
+
+if __name__ == "__main__":
+    app.run()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy/app/backends/_qt.py 
new/vispy-0.13.0/vispy/app/backends/_qt.py
--- old/vispy-0.12.2/vispy/app/backends/_qt.py  2023-03-20 16:16:17.000000000 
+0100
+++ new/vispy-0.13.0/vispy/app/backends/_qt.py  2023-05-12 20:06:40.000000000 
+0200
@@ -22,6 +22,7 @@
 from __future__ import division
 
 from time import sleep, time
+import math
 import os
 import sys
 import atexit
@@ -410,17 +411,10 @@
             # either not PyQt5 backend or no parent window available
             pass
 
-        # Activate touch and gesture.
-        # NOTE: we only activate touch on OS X because there seems to be
-        # problems on Ubuntu computers with touchscreen.
-        # See https://github.com/vispy/vispy/pull/1143
-        if sys.platform == 'darwin':
-            if PYQT6_API:
-                
self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents)
-                self.grabGesture(QtCore.Qt.GestureType.PinchGesture)
-            else:
-                self.setAttribute(QtCore.Qt.WA_AcceptTouchEvents)
-                self.grabGesture(QtCore.Qt.PinchGesture)
+        # QNativeGestureEvent does not keep track of last or total
+        # values like QGestureEvent does
+        self._native_gesture_scale_values = []
+        self._native_gesture_rotation_values = []
 
     def screen_changed(self, new_screen):
         """Window moved from one display to another, resize canvas.
@@ -563,50 +557,81 @@
     def keyReleaseEvent(self, ev):
         self._keyEvent(self._vispy_canvas.events.key_release, ev)
 
+    def _handle_native_gesture_event(self, ev):
+        if self._vispy_canvas is None:
+            return
+        t = ev.gestureType()
+        # this is a workaround for what looks like a Qt bug where
+        # QNativeGestureEvent gives the wrong local position.
+        # See: https://bugreports.qt.io/browse/QTBUG-59595
+        try:
+            pos = self.mapFromGlobal(ev.globalPosition().toPoint())
+        except AttributeError:
+            # globalPos is deprecated in Qt6
+            pos = self.mapFromGlobal(ev.globalPos())
+        pos = pos.x(), pos.y()
+
+        if t == QtCore.Qt.NativeGestureType.BeginNativeGesture:
+            self._vispy_canvas.events.touch(
+                type='gesture_begin',
+                pos=_get_event_xy(ev),
+            )
+        elif t == QtCore.Qt.NativeGestureType.EndNativeGesture:
+            self._native_touch_total_rotation = []
+            self._native_touch_total_scale = []
+            self._vispy_canvas.events.touch(
+                type='gesture_end',
+                pos=_get_event_xy(ev),
+            )
+        elif t == QtCore.Qt.NativeGestureType.RotateNativeGesture:
+            angle = ev.value()
+            last_angle = (
+                self._native_gesture_rotation_values[-1]
+                if self._native_gesture_rotation_values
+                else None
+            )
+            self._native_gesture_rotation_values.append(angle)
+            total_rotation_angle = 
math.fsum(self._native_gesture_rotation_values)
+            self._vispy_canvas.events.touch(
+                type="gesture_rotate",
+                pos=pos,
+                rotation=angle,
+                last_rotation=last_angle,
+                total_rotation_angle=total_rotation_angle,
+            )
+        elif t == QtCore.Qt.NativeGestureType.ZoomNativeGesture:
+            scale = ev.value()
+            last_scale = (
+                self._native_gesture_scale_values[-1]
+                if self._native_gesture_scale_values
+                else None
+            )
+            self._native_gesture_scale_values.append(scale)
+            total_scale_factor = math.fsum(self._native_gesture_scale_values)
+            self._vispy_canvas.events.touch(
+                type="gesture_zoom",
+                pos=pos,
+                last_scale=last_scale,
+                scale=scale,
+                total_scale_factor=total_scale_factor,
+            )
+        # QtCore.Qt.NativeGestureType.PanNativeGesture
+        # Qt6 docs seem to imply this is only supported on Wayland but I have
+        # not been able to test it.
+        # Two finger pan events are anyway converted to scroll/wheel events.
+        # On macOS, more fingers are usually swallowed by the OS (by spaces,
+        # mission control, etc.).
+
     def event(self, ev):
         out = super(QtBaseCanvasBackend, self).event(ev)
-        t = ev.type()
 
-        qt_event_types = QtCore.QEvent.Type if PYQT6_API else QtCore.QEvent
-        # Two-finger pinch.
-        if t == qt_event_types.TouchBegin:
-            self._vispy_canvas.events.touch(type='begin')
-        if t == qt_event_types.TouchEnd:
-            self._vispy_canvas.events.touch(type='end')
-        if t == qt_event_types.Gesture:
-            pinch_gesture = QtCore.Qt.GestureType.PinchGesture if PYQT6_API 
else QtCore.Qt.PinchGesture
-            gesture = ev.gesture(pinch_gesture)
-            if gesture:
-                (x, y) = _get_qpoint_pos(gesture.centerPoint())
-                scale = gesture.scaleFactor()
-                last_scale = gesture.lastScaleFactor()
-                rotation = gesture.rotationAngle()
-                self._vispy_canvas.events.touch(
-                    type="pinch",
-                    pos=(x, y),
-                    last_pos=None,
-                    scale=scale,
-                    last_scale=last_scale,
-                    rotation=rotation,
-                    total_rotation_angle=gesture.totalRotationAngle(),
-                    total_scale_factor=gesture.totalScaleFactor(),
-                )
-        # General touch event.
-        elif t == qt_event_types.TouchUpdate:
-            if qt_lib == 'pyqt6' or qt_lib == 'pyside6':
-                points = ev.points()
-                # These variables are lists of (x, y) coordinates.
-                pos = [_get_qpoint_pos(p.position()) for p in points]
-                lpos = [_get_qpoint_pos(p.lastPosition()) for p in points]
-            else:
-                points = ev.touchPoints()
-                # These variables are lists of (x, y) coordinates.
-                pos = [_get_qpoint_pos(p.pos()) for p in points]
-                lpos = [_get_qpoint_pos(p.lastPos()) for p in points]
-            self._vispy_canvas.events.touch(type='touch',
-                                            pos=pos,
-                                            last_pos=lpos,
-                                            )
+        # QNativeGestureEvent is Qt 5+
+        if (
+            (QT5_NEW_API or PYSIDE6_API or PYQT6_API)
+            and isinstance(ev, QtGui.QNativeGestureEvent)
+        ):
+            self._handle_native_gesture_event(ev)
+
         return out
 
     def _keyEvent(self, func, ev):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy/gloo/texture.py 
new/vispy-0.13.0/vispy/gloo/texture.py
--- old/vispy-0.12.2/vispy/gloo/texture.py      2023-03-20 16:16:17.000000000 
+0100
+++ new/vispy-0.13.0/vispy/gloo/texture.py      2023-05-12 20:06:40.000000000 
+0200
@@ -37,13 +37,16 @@
         return new_data
 
 
-def downcast_to_32bit_if_needed(data, copy=False):
+def downcast_to_32bit_if_needed(data, copy=False, dtype=None):
     """Downcast to 32bit dtype if necessary."""
-    dtype = np.dtype(data.dtype)
+    if dtype is None:
+        dtype = data.dtype
+    dtype = np.dtype(dtype)
     if dtype.itemsize > 4:
         warnings.warn(
             f"GPUs can't support dtypes bigger than 32-bit, but got '{dtype}'. 
"
-            "Precision will be lost due to downcasting to 32-bit."
+            "Precision will be lost due to downcasting to 32-bit.",
+            stacklevel=2,
         )
 
     size = min(dtype.itemsize, 4)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy/scene/cameras/base_camera.py 
new/vispy-0.13.0/vispy/scene/cameras/base_camera.py
--- old/vispy-0.12.2/vispy/scene/cameras/base_camera.py 2023-03-20 
16:16:17.000000000 +0100
+++ new/vispy-0.13.0/vispy/scene/cameras/base_camera.py 2023-05-12 
20:06:40.000000000 +0200
@@ -133,6 +133,8 @@
         viewbox.events.mouse_release.connect(self.viewbox_mouse_event)
         viewbox.events.mouse_move.connect(self.viewbox_mouse_event)
         viewbox.events.mouse_wheel.connect(self.viewbox_mouse_event)
+        viewbox.events.gesture_zoom.connect(self.viewbox_mouse_event)
+        viewbox.events.gesture_rotate.connect(self.viewbox_mouse_event)
         viewbox.events.resize.connect(self.viewbox_resize_event)
         # todo: also add key events! (and also on viewbox (they're missing)
 
@@ -144,6 +146,8 @@
         viewbox.events.mouse_release.disconnect(self.viewbox_mouse_event)
         viewbox.events.mouse_move.disconnect(self.viewbox_mouse_event)
         viewbox.events.mouse_wheel.disconnect(self.viewbox_mouse_event)
+        viewbox.events.gesture_zoom.disconnect(self.viewbox_mouse_event)
+        viewbox.events.gesture_rotate.disconnect(self.viewbox_mouse_event)
         viewbox.events.resize.disconnect(self.viewbox_resize_event)
 
     @property
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy/scene/cameras/panzoom.py 
new/vispy-0.13.0/vispy/scene/cameras/panzoom.py
--- old/vispy-0.12.2/vispy/scene/cameras/panzoom.py     2023-03-20 
16:16:17.000000000 +0100
+++ new/vispy-0.13.0/vispy/scene/cameras/panzoom.py     2023-05-12 
20:06:40.000000000 +0200
@@ -207,7 +207,10 @@
             center = self._scene_transform.imap(event.pos)
             self.zoom((1 + self.zoom_factor)**(-event.delta[1] * 30), center)
             event.handled = True
-
+        elif event.type == 'gesture_zoom':
+            center = self._scene_transform.imap(event.pos)
+            self.zoom(1 - event.scale, center)
+            event.handled = True
         elif event.type == 'mouse_move':
             if event.press_event is None:
                 return
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy/scene/cameras/perspective.py 
new/vispy-0.13.0/vispy/scene/cameras/perspective.py
--- old/vispy-0.12.2/vispy/scene/cameras/perspective.py 2023-03-20 
16:16:17.000000000 +0100
+++ new/vispy-0.13.0/vispy/scene/cameras/perspective.py 2023-05-12 
20:06:40.000000000 +0200
@@ -62,6 +62,12 @@
             if self._distance is not None:
                 self._distance *= s
             self.view_changed()
+        elif event.type == 'gesture_zoom':
+            s = 1 - event.scale
+            self._scale_factor *= s
+            if self._distance is not None:
+                self._distance *= s
+            self.view_changed()
 
     @property
     def scale_factor(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/vispy-0.12.2/vispy/scene/cameras/tests/test_perspective.py 
new/vispy-0.13.0/vispy/scene/cameras/tests/test_perspective.py
--- old/vispy-0.12.2/vispy/scene/cameras/tests/test_perspective.py      
2023-03-20 16:16:17.000000000 +0100
+++ new/vispy-0.13.0/vispy/scene/cameras/tests/test_perspective.py      
2023-05-12 20:06:40.000000000 +0200
@@ -82,4 +82,41 @@
         assert v.camera.center == (-12.8, -12.8, 0)
 
 
+@requires_application()
+def test_panzoom_gesture_zoom():
+    with TestingCanvas(size=(120, 200)) as canvas:
+        view = canvas.central_widget.add_view()
+        imdata = io.load_crate().astype('float32') / 255
+        scene.visuals.Image(imdata, parent=view.scene)
+        view.camera = scene.PanZoomCamera(aspect=1)
+
+        assert view.camera.rect.size == (1, 1)
+
+        canvas.events.touch(
+            type="gesture_zoom",
+            pos=(60, 100),
+            scale=-1.0,
+        )
+
+        assert view.camera.rect.size == (2, 2)
+
+
+@requires_application()
+def test_turntable_gesture_zoom():
+    with TestingCanvas(size=(120, 200)) as canvas:
+        view = canvas.central_widget.add_view()
+        imdata = io.load_crate().astype('float32') / 255
+        scene.visuals.Image(imdata, parent=view.scene)
+        view.camera = scene.TurntableCamera()
+
+        initial_scale_factor = view.camera.scale_factor
+        canvas.events.touch(
+            type="gesture_zoom",
+            pos=(60, 100),
+            scale=-1.0,
+        )
+
+        assert view.camera.scale_factor == 2 * initial_scale_factor
+
+
 run_tests_if_main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy/scene/canvas.py 
new/vispy-0.13.0/vispy/scene/canvas.py
--- old/vispy-0.12.2/vispy/scene/canvas.py      2023-03-20 16:16:17.000000000 
+0100
+++ new/vispy-0.13.0/vispy/scene/canvas.py      2023-05-12 20:06:40.000000000 
+0200
@@ -140,6 +140,7 @@
         self.events.mouse_move.connect(self._process_mouse_event)
         self.events.mouse_release.connect(self._process_mouse_event)
         self.events.mouse_wheel.connect(self._process_mouse_event)
+        self.events.touch.connect(self._process_mouse_event)
 
         self.scene = SubScene()
         self.freeze()
@@ -344,7 +345,12 @@
 
     def _process_mouse_event(self, event):
         prof = Profiler()  # noqa
-        deliver_types = ['mouse_press', 'mouse_wheel']
+        deliver_types = [
+            'mouse_press',
+            'mouse_wheel',
+            'gesture_zoom',
+            'gesture_rotate',
+        ]
         if self._send_hover_events:
             deliver_types += ['mouse_move']
 
@@ -524,6 +530,7 @@
         self.events.mouse_move.disconnect(self._process_mouse_event)
         self.events.mouse_release.disconnect(self._process_mouse_event)
         self.events.mouse_wheel.disconnect(self._process_mouse_event)
+        self.events.touch.disconnect(self._process_mouse_event)
 
     # -------------------------------------------------- transform handling ---
     def push_viewport(self, viewport):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy/scene/events.py 
new/vispy-0.13.0/vispy/scene/events.py
--- old/vispy-0.12.2/vispy/scene/events.py      2023-03-20 16:16:17.000000000 
+0100
+++ new/vispy-0.13.0/vispy/scene/events.py      2023-05-12 20:06:40.000000000 
+0200
@@ -71,6 +71,15 @@
         """The increment by which the mouse wheel has moved."""
         return self.mouse_event.delta
 
+    @property
+    def scale(self):
+        """The scale of a gesture_zoom event"""
+        try:
+            return self.mouse_event.scale
+        except AttributeError:
+            errmsg = f"SceneMouseEvent type '{self.type}' has no scale"
+            raise TypeError(errmsg)
+
     def copy(self):
         ev = self.__class__(self.mouse_event, self.visual)
         return ev
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy/scene/node.py 
new/vispy-0.13.0/vispy/scene/node.py
--- old/vispy-0.12.2/vispy/scene/node.py        2023-03-20 16:16:17.000000000 
+0100
+++ new/vispy-0.13.0/vispy/scene/node.py        2023-05-12 20:06:40.000000000 
+0200
@@ -63,7 +63,8 @@
         # Add some events to the emitter groups:
         events = ['canvas_change', 'parent_change', 'children_change', 
                   'transform_change', 'mouse_press', 'mouse_move',
-                  'mouse_release', 'mouse_wheel', 'key_press', 'key_release']
+                  'mouse_release', 'mouse_wheel', 'key_press', 'key_release',
+                  'gesture_zoom', 'gesture_rotate']
         # Create event emitter if needed (in subclasses that inherit from
         # Visual, we already have an emitter to share)
         if not hasattr(self, 'events'):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy/scene/visuals.py 
new/vispy-0.13.0/vispy/scene/visuals.py
--- old/vispy-0.12.2/vispy/scene/visuals.py     2023-03-20 16:16:17.000000000 
+0100
+++ new/vispy-0.13.0/vispy/scene/visuals.py     2023-05-12 20:06:40.000000000 
+0200
@@ -244,6 +244,7 @@
 Image = create_visual_node(visuals.ImageVisual)
 ComplexImage = create_visual_node(visuals.ComplexImageVisual)
 InfiniteLine = create_visual_node(visuals.InfiniteLineVisual)
+InstancedMesh = create_visual_node(visuals.InstancedMeshVisual)
 Isocurve = create_visual_node(visuals.IsocurveVisual)
 Isoline = create_visual_node(visuals.IsolineVisual)
 Isosurface = create_visual_node(visuals.IsosurfaceVisual)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy/version.py 
new/vispy-0.13.0/vispy/version.py
--- old/vispy-0.12.2/vispy/version.py   2023-03-20 16:16:37.000000000 +0100
+++ new/vispy-0.13.0/vispy/version.py   2023-05-12 20:07:06.000000000 +0200
@@ -1,4 +1,4 @@
 # file generated by setuptools_scm
 # don't change, don't track in version control
-__version__ = version = '0.12.2'
-__version_tuple__ = version_tuple = (0, 12, 2)
+__version__ = version = '0.13.0'
+__version_tuple__ = version_tuple = (0, 13, 0)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy/visuals/__init__.py 
new/vispy-0.13.0/vispy/visuals/__init__.py
--- old/vispy-0.12.2/vispy/visuals/__init__.py  2023-03-20 16:16:17.000000000 
+0100
+++ new/vispy-0.13.0/vispy/visuals/__init__.py  2023-05-12 20:06:40.000000000 
+0200
@@ -21,6 +21,7 @@
 from .gridmesh import GridMeshVisual  # noqa
 from .histogram import HistogramVisual  # noqa
 from .infinite_line import InfiniteLineVisual  # noqa
+from .instanced_mesh import InstancedMeshVisual  # noqa
 from .isocurve import IsocurveVisual  # noqa
 from .isoline import IsolineVisual  # noqa
 from .isosurface import IsosurfaceVisual  # noqa
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy/visuals/filters/__init__.py 
new/vispy-0.13.0/vispy/visuals/filters/__init__.py
--- old/vispy-0.12.2/vispy/visuals/filters/__init__.py  2023-03-20 
16:16:17.000000000 +0100
+++ new/vispy-0.13.0/vispy/visuals/filters/__init__.py  2023-05-12 
20:06:40.000000000 +0200
@@ -6,4 +6,4 @@
 from .clipper import Clipper  # noqa
 from .color import Alpha, ColorFilter, IsolineFilter, ZColormapFilter  # noqa
 from .picking import PickingFilter  # noqa
-from .mesh import TextureFilter, ShadingFilter, WireframeFilter  # noqa
+from .mesh import TextureFilter, ShadingFilter, InstancedShadingFilter, 
WireframeFilter  # noqa
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy/visuals/filters/mesh.py 
new/vispy-0.13.0/vispy/visuals/filters/mesh.py
--- old/vispy-0.12.2/vispy/visuals/filters/mesh.py      2023-03-20 
16:16:17.000000000 +0100
+++ new/vispy-0.13.0/vispy/visuals/filters/mesh.py      2023-05-12 
20:06:40.000000000 +0200
@@ -387,6 +387,10 @@
     
<https://github.com/vispy/vispy/blob/main/examples/basics/scene/mesh_shading.py>`_
     example script.
     """
+    _shaders = {
+        'vertex': shading_vertex_template,
+        'fragment': shading_fragment_template,
+    }
 
     def __init__(self, shading='flat',
                  ambient_coefficient=(1, 1, 1, 1),
@@ -412,8 +416,8 @@
 
         self._enabled = enabled
 
-        vfunc = Function(shading_vertex_template)
-        ffunc = Function(shading_fragment_template)
+        vfunc = Function(self._shaders['vertex'])
+        ffunc = Function(self._shaders['fragment'])
 
         self._normals = VertexBuffer(np.zeros((0, 3), dtype=np.float32))
         vfunc['normal'] = self._normals
@@ -572,6 +576,29 @@
         super()._detach(visual)
 
 
+instanced_shading_vertex_template = shading_vertex_template.replace(
+    "$normal",
+    "mat3($instance_transform_x, $instance_transform_y, $instance_transform_z) 
* $normal"
+)
+
+
+class InstancedShadingFilter(ShadingFilter):
+    """Shading filter modified for use with 
:class:`~vispy.visuals.InstancedMeshVisual`.
+
+    See :class:`ShadingFilter` for details and usage.
+    """
+    _shaders = {
+        'vertex': instanced_shading_vertex_template,
+        'fragment': ShadingFilter._shaders['fragment'],
+    }
+
+    def _attach(self, visual):
+        super()._attach(visual)
+        self.vshader['instance_transform_x'] = 
visual._instance_transforms_vbos[0]
+        self.vshader['instance_transform_y'] = 
visual._instance_transforms_vbos[1]
+        self.vshader['instance_transform_z'] = 
visual._instance_transforms_vbos[2]
+
+
 wireframe_vertex_template = """
 varying vec3 v_bc;
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy/visuals/instanced_mesh.py 
new/vispy-0.13.0/vispy/visuals/instanced_mesh.py
--- old/vispy-0.12.2/vispy/visuals/instanced_mesh.py    1970-01-01 
01:00:00.000000000 +0100
+++ new/vispy-0.13.0/vispy/visuals/instanced_mesh.py    2023-05-12 
20:06:40.000000000 +0200
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright (c) Vispy Development Team. All Rights Reserved.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+# -----------------------------------------------------------------------------
+
+"""An instanced version of MeshVisual with arbitrary shifts, transforms, and 
colors."""
+
+from __future__ import division
+
+import numpy as np
+
+from ..gloo import VertexBuffer
+from ..gloo.texture import downcast_to_32bit_if_needed
+from ..color import ColorArray
+from .filters import InstancedShadingFilter
+from .shaders import Variable
+
+from .mesh import MeshVisual
+
+
+_VERTEX_SHADER = """
+uniform bool use_instance_colors;
+
+// these attributes will be defined on an instance basis
+attribute vec3 shift;
+attribute vec3 transform_x;
+attribute vec3 transform_y;
+attribute vec3 transform_z;
+
+varying vec4 v_base_color;
+void main() {
+
+    v_base_color = $color_transform($base_color);
+
+    // transform is generated from column vectors (new basis vectors)
+    // 
https://en.wikibooks.org/wiki/GLSL_Programming/Vector_and_Matrix_Operations#Constructors
+    mat3 instance_transform = mat3(transform_x, transform_y, transform_z);
+    vec3 pos_rotated = instance_transform * $to_vec4($position).xyz;
+    vec4 pos_shifted = $to_vec4(pos_rotated + shift);
+    gl_Position = $transform(pos_shifted);
+}
+"""
+
+
+class InstancedMeshVisual(MeshVisual):
+    """Instanced Mesh visual.
+
+    Mostly identical to MeshVisual, but additionally takes arrays of
+    of positions and transforms (optionally colors) to create multiple
+    instances of the mesh.
+
+    Instancing is a rendering technique that re-uses the same mesh data
+    by applying transformations to vertices and vertex data or textures,
+    wich can drastically improve performance compared to having many
+    simple MeshVisuals.
+
+    Parameters
+    ----------
+    instance_positions : (I, 3) array
+        Coordinates for each instance of the mesh.
+    instance_transforms : (I, 3, 3) array
+        Matrices for the transforms to apply to each instance.
+    instance_colors : ColorArray
+        Matrices of colors for each instance. Colors
+    *args : list
+        Positional arguments to pass to 
:class:`~vispy.visuals.mesh.MeshVisual`.
+    **kwargs : dict
+        Keyword arguments to pass to :class:`~vispy.visuals.mesh.MeshVisual`.
+
+    Examples
+    --------
+    See example `scene/instanced_mesh_visual.py` in the gallery.
+    """
+
+    _shaders = {
+        'vertex': _VERTEX_SHADER,
+        'fragment': MeshVisual._shaders['fragment'],
+    }
+
+    _shading_filter_class = InstancedShadingFilter
+
+    def __init__(self, *args, instance_positions, instance_transforms, 
instance_colors=None, **kwargs):
+        self._instance_positions = None
+        self._instance_positions_vbo = None
+        self._instance_transforms = None
+        self._instance_transforms_vbos = None
+        self._instance_colors = None
+        self._instance_colors_vbo = None
+        super().__init__(*args, **kwargs)
+        self.instance_positions = instance_positions
+        self.instance_transforms = instance_transforms
+        self.instance_colors = instance_colors
+
+    @property
+    def instance_positions(self):
+        return self._instance_positions
+
+    @instance_positions.setter
+    def instance_positions(self, pos):
+        pos = np.reshape(pos, (-1, 3))
+        if pos.ndim != 2 or pos.shape[-1] != 3:
+            raise ValueError(f'positions must be 3D coordinates, but provided 
data has shape {pos.shape}')
+        self._instance_positions = downcast_to_32bit_if_needed(pos, 
dtype=np.float32)
+        self._instance_positions_vbo = VertexBuffer(self._instance_positions, 
divisor=1)
+        self.mesh_data_changed()
+
+    @property
+    def instance_transforms(self):
+        return self._instance_transforms
+
+    @instance_transforms.setter
+    def instance_transforms(self, matrix):
+        matrix = np.reshape(matrix, (-1, 3, 3))
+        if matrix.ndim != 3 or matrix.shape[1:] != (3, 3):
+            raise ValueError(f'transforms must be an array of 3x3 matrices, 
but provided data has shape {matrix.shape}')
+        self._instance_transforms = downcast_to_32bit_if_needed(matrix, 
dtype=np.float32)
+        # copy if not c contiguous
+        self._instance_transforms_vbos = (
+            VertexBuffer(np.ascontiguousarray(self._instance_transforms[..., 
0]), divisor=1),
+            VertexBuffer(np.ascontiguousarray(self._instance_transforms[..., 
1]), divisor=1),
+            VertexBuffer(np.ascontiguousarray(self._instance_transforms[..., 
2]), divisor=1),
+        )
+        self.mesh_data_changed()
+
+    @property
+    def instance_colors(self):
+        return self._instance_colors
+
+    @instance_colors.setter
+    def instance_colors(self, colors):
+        if colors is not None:
+            colors = ColorArray(colors)
+            self._instance_colors_vbo = VertexBuffer(colors.rgba, divisor=1)
+        else:
+            self._instance_colors_vbo = Variable('base_color', 
self._color.rgba)
+
+        self._instance_colors = colors
+        self.mesh_data_changed()
+
+    def _update_data(self):
+        with self.events.data_updated.blocker():
+            super()._update_data()
+
+        # set instance buffers
+        self.shared_program.vert['base_color'] = self._instance_colors_vbo
+        self.shared_program['transform_x'] = self._instance_transforms_vbos[0]
+        self.shared_program['transform_y'] = self._instance_transforms_vbos[1]
+        self.shared_program['transform_z'] = self._instance_transforms_vbos[2]
+        self.shared_program['shift'] = self._instance_positions_vbo
+
+        self.events.data_updated()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy/visuals/markers.py 
new/vispy-0.13.0/vispy/visuals/markers.py
--- old/vispy-0.12.2/vispy/visuals/markers.py   2023-03-20 16:16:17.000000000 
+0100
+++ new/vispy-0.13.0/vispy/visuals/markers.py   2023-05-12 20:06:40.000000000 
+0200
@@ -16,7 +16,7 @@
 _VERTEX_SHADER = """
 uniform float u_antialias;
 uniform float u_px_scale;
-uniform bool u_scaling;
+uniform int u_scaling;
 uniform bool u_spherical;
 
 attribute vec3 a_position;
@@ -43,28 +43,38 @@
 
     vec4 pos = vec4(a_position, 1);
     vec4 fb_pos = $visual_to_framebuffer(pos);
+    vec4 x;
+    vec4 size_vec;
     gl_Position = $framebuffer_to_render(fb_pos);
 
     // NOTE: gl_stuff uses framebuffer coords!
-
-    if (u_scaling == true) {
-        // calculate point size from visual to framebuffer coords to determine 
size
+    if (u_scaling == 1) {
+        // scaling == "scene": scale marker using entire visual -> framebuffer 
set of transforms
+        x = $framebuffer_to_visual(fb_pos + vec4(big_float, 0, 0, 0));
+        x = (x - pos);
+        size_vec = $visual_to_framebuffer(pos + normalize(x) * a_size);
+        $v_size = size_vec.x / size_vec.w - fb_pos.x / fb_pos.w;
+        v_edgewidth = ($v_size / a_size) * a_edgewidth;
+    }
+    else if (u_scaling == 2) {
+        // scaling == "visual": scale marker using only the Visual's transform
         // move horizontally in framebuffer space
         // then go to scene coordinates (not visual, so scaling is accounted 
for)
-        vec4 x = $framebuffer_to_scene(fb_pos + vec4(big_float, 0, 0, 0));
+        x = $framebuffer_to_scene(fb_pos + vec4(big_float, 0, 0, 0));
         // subtract position, so we get the scene-coordinate vector describing
         // an "horizontal direction parallel to the screen"
         vec4 scene_pos = $framebuffer_to_scene(fb_pos);
         x = (x - scene_pos);
         // multiply that direction by the size (in scene space) and add it to 
the position
         // this gives us the position of the edge of the point, which we 
convert in screen space
-        vec4 size_vec = $scene_to_framebuffer(scene_pos + normalize(x) * 
a_size);
+        size_vec = $scene_to_framebuffer(scene_pos + normalize(x) * a_size);
         // divide by `w` for perspective, and subtract pos
         // this gives us the actual screen-space size of the point
         $v_size = size_vec.x / size_vec.w - fb_pos.x / fb_pos.w;
         v_edgewidth = ($v_size / a_size) * a_edgewidth;
     }
     else {
+        // scaling == "fixed": marker is always the same number of pixels
         $v_size = a_size * u_px_scale;
         v_edgewidth = a_edgewidth * u_px_scale;
     }
@@ -505,8 +515,20 @@
         The color used to draw each symbol interior.
     symbol : str or array
         The style of symbol used to draw each marker (see Notes).
-    scaling : bool
-        If set to True, marker scales when rezooming.
+    scaling : str | bool
+        Scaling method of individual markers. If set to "fixed" (default) then
+        no scaling is done and markers will always be the same number of
+        pixels on the screen. If set to "scene" then the chain of transforms
+        from the Visual's transform to the transform mapping to the OpenGL
+        framebuffer are used to scaling the marker. This has the effect of the
+        marker staying the same size in the "scene" coordinate space and
+        changing size as the visualization is zoomed in and out. If set to
+        "visual" the marker is scaled only using the transform of the Visual
+        and not the rest of the scene/camera. This means that something like
+        a camera changing the view will not affect the size of the marker, but
+        the user can still scale it using the Visual's transform. For
+        backwards compatibility this can be set to the boolean ``False`` for
+        "fixed" or ``True`` for "scene".
     alpha : float
         The opacity level of the visual.
     antialias : float
@@ -534,7 +556,7 @@
     _symbol_shader_values = symbol_shader_values
     _symbol_shader = symbol_func
 
-    def __init__(self, scaling=False, alpha=1, antialias=1, spherical=False,
+    def __init__(self, scaling="fixed", alpha=1, antialias=1, spherical=False,
                  light_color='white', light_position=(1, -1, 1), 
light_ambient=0.3, **kwargs):
         self._vbo = VertexBuffer()
         self._data = None
@@ -554,6 +576,8 @@
         if len(kwargs) > 0:
             self.set_data(**kwargs)
 
+        self._scaling = "fixed"
+        self._scaling_int = 0
         self.scaling = scaling
         self.antialias = antialias
         self.light_color = light_color
@@ -677,9 +701,20 @@
 
     @scaling.setter
     def scaling(self, value):
-        value = bool(value)
-        self.shared_program['u_scaling'] = value
+        scaling_modes = {
+            False: 0,
+            True: 1,
+            "fixed": 0,
+            "scene": 1,
+            "visual": 2,
+        }
+        if value not in scaling_modes:
+            possible_options = ", ".join(repr(opt) for opt in scaling_modes)
+            raise ValueError(f"Unknown scaling option {value!r}, expected one 
of: {possible_options}")
+        scaling_int = scaling_modes[value]
+        self.shared_program['u_scaling'] = scaling_int
         self._scaling = value
+        self._scaling_int = scaling_int
         self.update()
 
     @property
@@ -773,7 +808,7 @@
         if self._data is None:
             return False
         view.view_program['u_px_scale'] = view.transforms.pixel_scale
-        view.view_program['u_scaling'] = self.scaling
+        view.view_program['u_scaling'] = self._scaling_int
 
     def _compute_bounds(self, axis, view):
         pos = self._data['a_position']
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/vispy-0.12.2/vispy/visuals/tests/test_instanced_mesh.py 
new/vispy-0.13.0/vispy/visuals/tests/test_instanced_mesh.py
--- old/vispy-0.12.2/vispy/visuals/tests/test_instanced_mesh.py 1970-01-01 
01:00:00.000000000 +0100
+++ new/vispy-0.13.0/vispy/visuals/tests/test_instanced_mesh.py 2023-05-12 
20:06:40.000000000 +0200
@@ -0,0 +1,50 @@
+import numpy as np
+from vispy import scene, use
+
+from vispy.testing import (TestingCanvas, requires_application,
+                           run_tests_if_main, requires_pyopengl)
+
+
+def setup_module(module):
+    use(gl='gl+')
+
+
+def teardown_module(module):
+    use(gl='gl2')
+
+
+@requires_pyopengl()
+@requires_application()
+def test_mesh_with_vertex_values():
+    size = (80, 60)
+    with TestingCanvas(size=size) as c:
+        use(gl='gl+')
+        vert = np.array([[0, 0, 0], [0, 30, 0], [40, 0, 0]])
+        faces = np.array([0, 1, 2])
+        pos = np.array([[0, 0, 0], [80, 60, 0]])
+        # identity and rotate 180
+        trans = np.array([
+            [
+                [1, 0, 0],
+                [0, 1, 0],
+                [0, 0, 1],
+            ],
+            [
+                [-1, 0, 0],
+                [0, -1, 0],
+                [0, 0, 1],
+            ],
+        ])
+        colors = ['red', 'blue']
+        mesh = scene.visuals.InstancedMesh(
+            vertices=vert, faces=faces, instance_positions=pos, 
instance_transforms=trans, instance_colors=colors
+        )
+        v = c.central_widget.add_view(border_width=0)
+        v.add(mesh)
+        render = c.render()
+        assert np.allclose(render[10, 10], (255, 0, 0, 255))
+        assert np.allclose(render[-10, -10], (0, 0, 255, 255))
+        assert np.allclose(render[30, 40], (0, 0, 0, 255))
+
+
+run_tests_if_main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy/visuals/volume.py 
new/vispy-0.13.0/vispy/visuals/volume.py
--- old/vispy-0.12.2/vispy/visuals/volume.py    2023-03-20 16:16:17.000000000 
+0100
+++ new/vispy-0.13.0/vispy/visuals/volume.py    2023-05-12 20:06:40.000000000 
+0200
@@ -369,9 +369,13 @@
         int maxi = -1;  // Where the maximum value was encountered
         """,
     in_loop="""
-        if( val > maxval ) {
+        if ( val > maxval ) {
             maxval = val;
             maxi = iter;
+            if ( maxval >= clim.y ) {
+                // stop if no chance of finding a higher maxval
+                iter = nsteps;
+            }
         }
         """,
     after_loop="""
@@ -395,8 +399,7 @@
             }
             frag_depth_point = max_loc_tex * u_shape;
             gl_FragColor = applyColormap(maxval);
-        }
-        else {
+        } else {
             discard;
         }
         """,
@@ -406,7 +409,7 @@
     before_loop="""
         float maxval = u_mip_cutoff; // The maximum encountered value
         float sumval = 0.0; // The sum of the encountered values
-        float scaled = 0.0; // The scaled value
+        float scale = 0.0; // The cumulative attenuation
         int maxi = -1;  // Where the maximum value was encountered
         vec3 max_loc_tex = vec3(0.0);  // Location where the maximum value was 
encountered
         """,
@@ -415,9 +418,12 @@
         // * attenuation value does not depend on data values
         // * negative values do not amplify instead of attenuate
         sumval = sumval + clamp((val - clim.x) / (clim.y - clim.x), 0.0, 1.0);
-        scaled = val * exp(-u_attenuation * (sumval - 1) / 
u_relative_step_size);
-        if( scaled > maxval ) {
-            maxval = scaled;
+        scale = exp(-u_attenuation * (sumval - 1) / u_relative_step_size);
+        if( maxval > scale * clim.y ) {
+            // stop if no chance of finding a higher maxval
+            iter = nsteps;
+        } else if( val * scale > maxval ) {
+            maxval = val * scale;
             maxi = iter;
             max_loc_tex = loc;
         }
@@ -439,9 +445,13 @@
         int mini = -1;  // Where the minimum value was encountered
         """,
     in_loop="""
-        if( val < minval ) {
+        if ( val < minval ) {
             minval = val;
             mini = iter;
+            if ( minval <= clim.x ) {
+                // stop if no chance of finding a lower minval
+                iter = nsteps;
+            }
         }
         """,
     after_loop="""
@@ -465,8 +475,7 @@
             }
             frag_depth_point = min_loc_tex * u_shape;
             gl_FragColor = applyColormap(minval);
-        }
-        else {
+        } else {
             discard;
         }
         """,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy.egg-info/PKG-INFO 
new/vispy-0.13.0/vispy.egg-info/PKG-INFO
--- old/vispy-0.12.2/vispy.egg-info/PKG-INFO    2023-03-20 16:16:37.000000000 
+0100
+++ new/vispy-0.13.0/vispy.egg-info/PKG-INFO    2023-05-12 20:07:06.000000000 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: vispy
-Version: 0.12.2
+Version: 0.13.0
 Summary: Interactive visualization in Python
 Home-page: http://vispy.org
 Download-URL: https://pypi.python.org/pypi/vispy
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vispy-0.12.2/vispy.egg-info/SOURCES.txt 
new/vispy-0.13.0/vispy.egg-info/SOURCES.txt
--- old/vispy-0.12.2/vispy.egg-info/SOURCES.txt 2023-03-20 16:16:37.000000000 
+0100
+++ new/vispy-0.13.0/vispy.egg-info/SOURCES.txt 2023-05-12 20:07:07.000000000 
+0200
@@ -245,6 +245,8 @@
 examples/scene/image.py
 examples/scene/image_custom_kernel.py
 examples/scene/infinite_line.py
+examples/scene/instanced_mesh.py
+examples/scene/instanced_mesh_visual.py
 examples/scene/instanced_quad_visual.py
 examples/scene/isocurve.py
 examples/scene/isocurve_for_trisurface.py
@@ -693,6 +695,7 @@
 vispy/visuals/image.py
 vispy/visuals/image_complex.py
 vispy/visuals/infinite_line.py
+vispy/visuals/instanced_mesh.py
 vispy/visuals/isocurve.py
 vispy/visuals/isoline.py
 vispy/visuals/isosurface.py
@@ -784,6 +787,7 @@
 vispy/visuals/tests/test_image.py
 vispy/visuals/tests/test_image_complex.py
 vispy/visuals/tests/test_infinite_line.py
+vispy/visuals/tests/test_instanced_mesh.py
 vispy/visuals/tests/test_isosurface.py
 vispy/visuals/tests/test_linear_region.py
 vispy/visuals/tests/test_markers.py

Reply via email to