Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 57 additions & 3 deletions collagraph/renderers/pygfx_renderer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import warnings
from typing import Any, Callable
from weakref import ref

import pygfx as gfx

Expand All @@ -9,6 +10,22 @@
DEFAULT_ATTR_CACHE = {}


class _TextElementProxy(gfx.WorldObject):
def __init__(self):
super().__init__()
self._cg_content = ""
self._cg_parent_text_ref = None

@property
def _cg_parent_text(self) -> gfx.Text | None:
if self._cg_parent_text_ref is None:
return None
return self._cg_parent_text_ref()

def _cg_set_parent_text(self, parent: gfx.Text | None):
self._cg_parent_text_ref = ref(parent) if parent else None


class PygfxRenderer(Renderer):
"""Renderer for Pygfx objects"""

Expand Down Expand Up @@ -37,7 +54,8 @@ def register_asyncio(self):

def create_element(self, type: str) -> gfx.WorldObject:
"""Create pygfx element for the given type"""
type = type.lower().replace("-", "")
type = type.lower().replace("-", "").replace("_", "")

if element_type := ELEMENT_TYPE_CACHE.get(type):
self._trigger()
return element_type()
Expand All @@ -53,27 +71,53 @@ def create_element(self, type: str) -> gfx.WorldObject:
raise ValueError(f"Can't create element of type: {type}")

def create_text_element(self):
raise NotImplementedError
self._trigger()
return _TextElementProxy()

def insert(
self,
el: gfx.WorldObject,
parent: gfx.WorldObject,
anchor: gfx.WorldObject | None = None,
):
is_text_proxy = isinstance(el, _TextElementProxy)
try:
parent.add(el, before=anchor)
except ValueError:
warnings.warn(f"Could not find anchor in {parent}")
parent.add(el)

if is_text_proxy and isinstance(parent, gfx.Text):
el._cg_set_parent_text(parent)
self._sync_text_from_proxy_children(parent)
return

self._trigger()

def remove(self, el: gfx.WorldObject, parent: gfx.WorldObject):
parent.remove(el)

if isinstance(el, _TextElementProxy):
if isinstance(parent, gfx.Text):
self._sync_text_from_proxy_children(parent)
el._cg_set_parent_text(None)
return

self._trigger()

def set_element_text(self, el, value: str):
raise NotImplementedError
el._cg_content = value
if parent := el._cg_parent_text:
self._sync_text_from_proxy_children(parent)

def _sync_text_from_proxy_children(self, parent: gfx.Text):
content = "".join(
child._cg_content
for child in parent.children
if isinstance(child, _TextElementProxy)
)
parent.set_text(content)
self._trigger()

def set_attribute(self, el: gfx.WorldObject, attr: str, value: Any):
key = f"{type(el).__name__}.{attr}"
Expand All @@ -84,6 +128,16 @@ def set_attribute(self, el: gfx.WorldObject, attr: str, value: Any):
for attribute in attrs:
el = getattr(el, attribute)

if isinstance(el, gfx.Text):
if attr == "text":
el.set_text(value)
self._trigger()
return
if attr == "markdown":
el.set_markdown(value)
self._trigger()
return

if key not in DEFAULT_ATTR_CACHE:
if hasattr(el, attr):
default_value = getattr(el, attr)
Expand Down
9 changes: 9 additions & 0 deletions examples/pygfx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ uv run collagraph --renderer pygfx --state '{"count": 100}' examples/pygfx/point
Shows a point cloud. Adjust the count to see how far you can push it.


## Landmarks example

```shell
uv run collagraph --renderer pygfx --state '{"count": 25}' examples/pygfx/landmarks.cgx
```
Shows random 3D landmarks with labels rendered as child text nodes of `<text>` elements.
Click a landmark sphere to increment and rename its label while highlighting both sphere and text color.


## Component example

```shell
Expand Down
84 changes: 84 additions & 0 deletions examples/pygfx/landmarks.cgx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<!--
Run this example as follows:
uv run collagraph --renderer pygfx --state '{"count": 25}' examples/pygfx/landmarks.cgx
-->
<ambient-light />
<point-light :local.position="(12, 18, 10)" />
<group v-for="idx, landmark in enumerate(landmarks)">
<group :local.position="landmark['position']">
<mesh
:geometry="sphere_geometry"
:material="selected_sphere_material if idx == selected else sphere_material"
@click="lambda ev: click_landmark(idx)"
/>
<text
anchor="bottom-center"
:font_size="14"
:local.position="(0, 0.75, 0)"
:material="selected_label_material if idx == selected else label_material"
screen_space
>
{{landmark['name']}}
#{{idx}}
</text>
</group>
</group>

<script>
import random

import collagraph as cg
import pygfx as gfx


sphere_geometry = gfx.sphere_geometry(radius=0.35)
sphere_material = gfx.MeshPhongMaterial(color=[0.2, 0.8, 1.0], pick_write=True)
selected_sphere_material = gfx.MeshPhongMaterial(color=[1.0, 0.4, 0.2], pick_write=True)

label_material = gfx.TextMaterial(color="#D6F4FF")
selected_label_material = gfx.TextMaterial(color="#FFE066")

LANDMARK_NAMES = [
"Harbor",
"Bridge",
"Museum",
"Library",
"Station",
"Observatory",
"Garden",
"Square",
"Market",
"Beacon",
]


def rand_position():
return (
round(random.uniform(-12.0, 12.0), 2),
round(random.uniform(-8.0, 8.0), 2),
round(random.uniform(-12.0, 12.0), 2),
)


class Landmarks(cg.Component):
def init(self):
random.seed(42)
count = int(self.props.get("count", 20))
self.state["selected"] = -1
self.state["landmarks"] = [
{
"base_name": LANDMARK_NAMES[idx % len(LANDMARK_NAMES)],
"name": LANDMARK_NAMES[idx % len(LANDMARK_NAMES)],
"position": rand_position(),
"clicks": 0,
}
for idx in range(count)
]

def click_landmark(self, idx):
landmark = self.state["landmarks"][idx]
landmark["clicks"] += 1
landmark["name"] = f"{landmark['base_name']} ({landmark['clicks']})"
self.state["selected"] = idx
</script>

Loading
Loading