Skip to content

Commit df29e2a

Browse files
Fix pygfx and pyside compatibility
1 parent 0599c54 commit df29e2a

8 files changed

Lines changed: 110 additions & 48 deletions

File tree

collagraph/renderers/pygfx_renderer.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Callable
2+
13
import pygfx as gfx
24

35
from . import Renderer
@@ -9,17 +11,42 @@
911
class PygfxRenderer(Renderer):
1012
"""Renderer for Pygfx objects"""
1113

14+
def __init__(self, *args, **kwargs):
15+
super().__init__(*args, **kwargs)
16+
self._on_change_handlers = set()
17+
18+
def add_on_change_handler(self, handler: Callable):
19+
self._on_change_handlers.add(handler)
20+
21+
def remove_on_change_handler(self, handler: Callable):
22+
self._on_change_handlers.remove(handler)
23+
24+
def _trigger(self):
25+
for handler in self._on_change_handlers:
26+
handler()
27+
28+
def register_asyncio(self):
29+
import asyncio
30+
31+
from PySide6.QtAsyncio import QAsyncioEventLoopPolicy
32+
33+
policy = asyncio.get_event_loop_policy()
34+
if not isinstance(policy, QAsyncioEventLoopPolicy):
35+
asyncio.set_event_loop_policy(QAsyncioEventLoopPolicy())
36+
1237
def create_element(self, type: str) -> gfx.WorldObject:
1338
"""Create pygfx element for the given type"""
1439
type = type.lower().replace("-", "")
1540
if element_type := ELEMENT_TYPE_CACHE.get(type):
41+
self._trigger()
1642
return element_type()
1743

1844
attrs = dir(gfx)
1945
for attr in attrs:
2046
if attr.lower() == type:
2147
element_type = getattr(gfx, attr)
2248
ELEMENT_TYPE_CACHE[type] = element_type
49+
self._trigger()
2350
return element_type()
2451

2552
raise ValueError(f"Can't create element of type: {type}")
@@ -34,9 +61,11 @@ def insert(
3461
anchor: gfx.WorldObject = None,
3562
):
3663
parent.add(el, before=anchor)
64+
self._trigger()
3765

3866
def remove(self, el: gfx.WorldObject, parent: gfx.WorldObject):
3967
parent.remove(el)
68+
self._trigger()
4069

4170
def set_element_text(self, el, value: str):
4271
raise NotImplementedError
@@ -58,7 +87,14 @@ def set_attribute(self, obj, attr, value):
5887
else:
5988
DEFAULT_ATTR_CACHE[key] = default_value
6089

61-
setattr(obj, attr, value)
90+
# Note: this check can be removed when the following PR
91+
# has been included in a new release of observ (>0.15):
92+
# https://github.com/fork-tongue/observ/pull/130
93+
if val := getattr(value, "__target__", None):
94+
setattr(obj, attr, val)
95+
else:
96+
setattr(obj, attr, value)
97+
self._trigger()
6298

6399
def remove_attribute(self, obj, attr, value):
64100
key = f"{type(obj).__name__}.{attr}"
@@ -78,6 +114,7 @@ def remove_attribute(self, obj, attr, value):
78114
setattr(obj, attr, val)
79115
else:
80116
delattr(obj, attr)
117+
self._trigger()
81118

82119
def add_event_listener(self, el, event_type, value):
83120
el.add_event_handler(value, event_type)

collagraph/renderers/pyside_renderer.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections import defaultdict
2-
from functools import lru_cache
2+
from functools import lru_cache, partial
33
import logging
44
from typing import Any, Callable
55
from warnings import warn
@@ -402,6 +402,11 @@ def add_event_listener(self, el: Any, event_type: str, value: Callable):
402402
# a QObject?) results in a SystemError. Lambdas though _can_ function
403403
# as a slot, so when creating a slot of the value fails, retry with
404404
# a simple lambda.
405+
if isinstance(value, partial):
406+
# In the case that value is a partial object, Pyside 6.9.2 spits
407+
# out a warning 'PytestUnraisableExceptionWarning'. Wrapping the
408+
# partial in a lambda seems to do the trick
409+
raise SystemError
405410
slot = QtCore.Slot()(value)
406411
except SystemError:
407412
# TODO: with some inspection we might be able to figure out the

examples/pygfx/combined-example.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
Example of how to render lists, tables and trees.
33
"""
44

5-
from observ import reactive
6-
from point_cloud import materials, PointCloud, sphere_geom
75
import pygfx as gfx
6+
from observ import reactive
7+
from point_cloud import PointCloud, materials
88
from PySide6 import QtWidgets
99
from wgpu.gui.qt import WgpuCanvas
1010

@@ -41,10 +41,10 @@ def mounted(self):
4141
h(
4242
"Mesh",
4343
{
44+
"geometry": gfx.sphere_geometry(),
45+
"material": materials["other"],
4446
"name": "Hip",
4547
"local.position": [2, 2, 2],
46-
"geometry": sphere_geom,
47-
"material": materials["other"],
4848
},
4949
),
5050
)
@@ -54,9 +54,8 @@ def mounted(self):
5454
def animate():
5555
renderer.render(container, camera)
5656

57-
self.gui.render(
58-
element, container, callback=lambda: self.element.request_draw(animate)
59-
)
57+
self.gui.renderer.add_on_change_handler(lambda: self.element.request_draw(animate))
58+
self.gui.render(element, container)
6059

6160
def render(self):
6261
return h("WgpuCanvas", {"minimum_height": 400, "minimum_width": 600})
@@ -103,7 +102,7 @@ def remove(event):
103102
renderer.register_element("WgpuCanvas", WgpuCanvas)
104103
gui = cg.Collagraph(renderer=renderer)
105104

106-
state = reactive({"count": 50})
105+
state = reactive({"count": 1000})
107106

108107
# Define Qt structure and map state to the structure
109108
element = h(Example, state)

examples/pygfx/component-example.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
class Button(cg.Component):
99
geometry = gfx.box_geometry()
1010
materials = {
11-
"default": gfx.MeshPhongMaterial(color=[1.0, 0.5, 0.0]),
12-
"pressed": gfx.MeshPhongMaterial(color=[0.0, 0.5, 0.0]),
13-
"hovered": gfx.MeshPhongMaterial(color=[1.0, 0.2, 0.0]),
11+
"default": gfx.MeshPhongMaterial(color=[1.0, 0.5, 0.0], pick_write=True),
12+
"pressed": gfx.MeshPhongMaterial(color=[0.0, 0.5, 0.0], pick_write=True),
13+
"hovered": gfx.MeshPhongMaterial(color=[1.0, 0.2, 0.0], pick_write=True),
1414
}
1515

1616
def __init__(self, *args, **kwargs):

examples/pygfx/point_cloud.py

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,47 @@
66

77
sphere_geom = gfx.sphere_geometry(radius=0.5)
88
materials = {
9-
"default": gfx.MeshPhongMaterial(color=[1, 1, 1]),
10-
"selected": gfx.MeshPhongMaterial(color=[1, 0, 0]),
11-
"hovered": gfx.MeshPhongMaterial(color=[1, 0.6, 0]),
12-
"other": gfx.MeshPhongMaterial(color=[1, 0, 0.5]),
9+
"default": gfx.MeshPhongMaterial(color=[1, 1, 1], pick_write=True),
10+
"selected": gfx.MeshPhongMaterial(color=[1, 0, 0], pick_write=True),
11+
"hovered": gfx.MeshPhongMaterial(color=[1, 0.6, 0], pick_write=True),
12+
"other": gfx.MeshPhongMaterial(color=[1, 0, 0.5], pick_write=True),
1313
}
1414

1515

16+
def rand_point():
17+
return (
18+
random.randint(-20, 20),
19+
random.randint(-20, 20),
20+
random.randint(-20, 20),
21+
)
22+
23+
24+
def point(index, selected, hovered, set_selected, set_hovered):
25+
material = (
26+
"selected"
27+
if index == selected
28+
else "hovered"
29+
if index == hovered
30+
else "default"
31+
)
32+
return h(
33+
"Mesh",
34+
{
35+
"geometry": sphere_geom,
36+
"material": materials[material],
37+
"local.position": positions[index],
38+
"key": index,
39+
"on_click": lambda event: set_selected(index),
40+
"on_pointer_move": lambda event: set_hovered(index),
41+
},
42+
)
43+
44+
45+
positions = []
46+
47+
1648
def PointCloud(props):
17-
random.seed(0)
49+
global positions
1850

1951
def set_hovered(index):
2052
props["hovered"] = index
@@ -25,34 +57,23 @@ def set_selected(index):
2557
else:
2658
props["selected"] = index
2759

28-
def random_point(index, selected, hovered):
29-
material = materials["default"]
30-
if index == selected:
31-
material = materials["selected"]
32-
elif index == hovered:
33-
material = materials["hovered"]
34-
return h(
35-
"Mesh",
36-
{
37-
"geometry": sphere_geom,
38-
"material": material,
39-
"local.position": [
40-
random.randint(-10, 10),
41-
random.randint(-10, 10),
42-
random.randint(-10, 10),
43-
],
44-
"key": index,
45-
"on_click": lambda event: set_selected(index),
46-
"on_pointer_move": lambda event: set_hovered(index),
47-
},
48-
)
4960

5061
selected = props.get("selected", -1)
5162
hovered = props.get("hovered", -1)
5263
number_of_points = props.get("count", 50)
5364

65+
if len(positions) > number_of_points:
66+
positions = positions[:number_of_points]
67+
elif len(positions) < number_of_points:
68+
positions.extend(
69+
[rand_point() for _ in range(number_of_points - len(positions))]
70+
)
71+
5472
return h(
5573
"Group",
5674
{},
57-
*[random_point(i, selected, hovered) for i in range(number_of_points)],
75+
*[
76+
point(i, selected, hovered, set_selected, set_hovered)
77+
for i in range(number_of_points)
78+
],
5879
)

examples/pygfx/pygfx-example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
# When increasing this number, it will take longer
3232
# and longer for pygfx to create the render pipeline
3333
# (compiling shaders and such), so be careful...
34-
{"count": 50},
34+
{"count": 500},
3535
),
3636
h(
3737
"Mesh",

examples/pygfx/pygfx-slider-example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def render(self):
3232

3333
class Scrubber(cg.Component):
3434
scrubber_geometry = gfx.sphere_geometry()
35-
scrubber_material = gfx.MeshPhongMaterial(color=[0.5, 1, 0.5])
35+
scrubber_material = gfx.MeshPhongMaterial(color=[0.5, 1, 0.5], pick_write=True)
3636

3737
def __init__(self, *args, **kwargs):
3838
super().__init__(*args, **kwargs)

tests/test_pygfx_renderer.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,12 @@ def test_pygfx_attributes():
7979
# Set and unset attributes on Mesh
8080
mesh = gfx.Mesh()
8181

82-
# Custom attribute 'name'
83-
renderer.set_attribute(mesh, "name", "foo")
84-
assert mesh.name == "foo"
82+
# Custom attribute 'foo'
83+
renderer.set_attribute(mesh, "foo", "foo")
84+
assert mesh.foo == "foo"
8585

86-
renderer.remove_attribute(mesh, "name", "foo")
87-
assert not hasattr(mesh, "name")
86+
renderer.remove_attribute(mesh, "foo", "foo")
87+
assert not hasattr(mesh, "foo")
8888

8989
# Position attribute
9090
original_position_contents = mesh.local.position.tolist()
@@ -105,7 +105,7 @@ def test_pygfx_attributes():
105105

106106
# Matrix attribute
107107
original_matrix_contents = mesh.local.matrix.tolist()
108-
matrix = [[2, 0, 0, 0], [0, 2, 0, 0], [0, 0, 2, 0], [0, 0, 0, 2]]
108+
matrix = [[2, 0, 0, 0], [0, 2, 0, 0], [0, 0, 2, 0], [0, 0, 0, 1]]
109109
renderer.set_attribute(mesh, "local.matrix", matrix)
110110
assert mesh.local.matrix.tolist() == matrix
111111

0 commit comments

Comments
 (0)