Skip to content
Draft
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
14 changes: 10 additions & 4 deletions examples/basic_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from scenex.app.events import (
Event,
MouseButton,
MouseEvent,
MouseMoveEvent,
MousePressEvent,
MouseReleaseEvent,
Expand Down Expand Up @@ -42,15 +43,20 @@ def _create_line_data(angle: float = 0) -> np.ndarray:


def _view_event_filter(event: Event) -> bool:
global pressed
"""Interactive mesh manipulation based on mouse events."""
global pressed

if not isinstance(event, MouseEvent):
return False
if not (ray := view.to_ray(event.pos)):
return False
if isinstance(event, MouseMoveEvent):
if pressed and event.buttons & MouseButton.LEFT:
x, y, _z = event.world_ray.origin
x, y, _z = ray.origin
y = max(-1, min(1, y))
line.vertices = _create_line_data(angle=np.asin(y) - x)
return True
if intersections := event.world_ray.intersections(view.scene):
if intersections := ray.intersections(view.scene):
# Find mesh intersection
for node, _distance in intersections:
if isinstance(node, snx.Line):
Expand All @@ -59,7 +65,7 @@ def _view_event_filter(event: Event) -> bool:
line.color = line_color_model
elif isinstance(event, MousePressEvent):
if event.buttons & MouseButton.LEFT:
if intersections := event.world_ray.intersections(view.scene):
if intersections := ray.intersections(view.scene):
# Find line intersection
for node, _distance in intersections:
if isinstance(node, snx.Line):
Expand Down
7 changes: 4 additions & 3 deletions examples/basic_mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,15 @@ def create_grid_mesh(

def event_filter(event: Event) -> bool:
"""Interactive mesh manipulation based on mouse events."""
global per_face_model
if isinstance(event, MouseMoveEvent):
if intersections := event.world_ray.intersections(view.scene):
if not (ray := view.to_ray(event.pos)):
return False
if intersections := ray.intersections(view.scene):
# Find mesh intersection
for node, _distance in intersections:
if isinstance(node, snx.Mesh):
# Remove the intersected face
indices = [i for i, _d in node.intersecting_faces(event.world_ray)]
indices = [i for i, _d in node.intersecting_faces(ray)]
node.faces = np.delete(node.faces, indices, axis=0)
return True
elif isinstance(event, MousePressEvent):
Expand Down
4 changes: 3 additions & 1 deletion examples/basic_points.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@

def _on_view_event(event: Event) -> bool:
if isinstance(event, MouseMoveEvent):
intersections = event.world_ray.intersections(view.scene)
if (ray := view.to_ray(event.pos)) is None:
return False
intersections = ray.intersections(view.scene)
Comment on lines 72 to +76
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really left wanting for something better than this. We need a way to go from "here's an event" to "here are all of the nodes that pertain to this event"

Will keep thinking on it. Suggestions appreciated too by any passers-by!

if points in [n for n, _ in intersections]:
points.face_color = snx.UniformColor(color=cmap.Color("white"))
points.edge_color = snx.VertexColors(color=colors)
Expand Down
4 changes: 3 additions & 1 deletion examples/blending.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ def change_blend_mode(event: Event) -> bool:
"""Change the blend mode of a volume when it is clicked."""
if not isinstance(event, (MousePressEvent)):
return False
intersected_nodes = [node for node, _ in event.world_ray.intersections(view.scene)]
if not (ray := view.to_ray(event.pos)):
return False
intersected_nodes = [node for node, _ in ray.intersections(view.scene)]
if volume1 not in intersected_nodes:
return False
idx = blend_modes.index(volume1.blending)
Expand Down
4 changes: 3 additions & 1 deletion examples/cursor_points.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@

def _cursor_filter(event: Event) -> bool:
if isinstance(event, MouseMoveEvent):
intersections = event.world_ray.intersections(view.scene)
if not (ray := view.to_ray(event.pos)):
return False
intersections = ray.intersections(view.scene)
if points in [n for n, _ in intersections]:
app().set_cursor(canvas, CursorType.CROSS)
else:
Expand Down
6 changes: 4 additions & 2 deletions examples/event_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@
def _view_filter(event: Event) -> bool:
"""Example event drawing a square that reacts to the cursor."""
if isinstance(event, MouseMoveEvent):
intersections = event.world_ray.intersections(view.scene)
if not (ray := view.to_ray(event.pos)):
return False
intersections = ray.intersections(view.scene)
if not intersections:
# Clear the image if the mouse is not over it
img.data = np.zeros((200, 200), dtype=np.uint8)
return True
for node, distance in intersections:
if not isinstance(node, snx.Image):
continue
intersection = event.world_ray.point_at_distance(distance)
intersection = ray.point_at_distance(distance)
data = np.zeros((200, 200), dtype=np.uint8)
x = int(intersection[0])
min_x = max(0, x - 5)
Expand Down
16 changes: 10 additions & 6 deletions examples/histogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,14 @@ def _init_main_view(self) -> None:
self.view.set_event_filter(self._on_main_view)

def _on_main_view(self, event: events.Event) -> bool:
if not isinstance(event, events.MouseEvent):
return False
if not (ray := self.view.to_ray(event.pos)):
return False
if isinstance(event, events.MousePressEvent):
intersections = [
node
for node, _dist in event.world_ray.intersections(self.controls)
for node, _dist in ray.intersections(self.controls)
if node.interactive
]
if len(intersections):
Expand All @@ -234,32 +238,32 @@ def _on_main_view(self, event: events.Event) -> bool:
elif isinstance(event, events.MouseDoublePressEvent):
intersections = [
node
for node, _dist in event.world_ray.intersections(self.controls)
for node, _dist in ray.intersections(self.controls)
if node.interactive
]
if self.gamma_handle in intersections:
self.set_gamma(1.0)
if isinstance(event, events.MouseMoveEvent):
if self._grabbed is self.left_clim:
# The left clim must stay to the left of the right clim
new_left = min(event.world_ray.origin[0], self._clims[1])
new_left = min(ray.origin[0], self._clims[1])
# ...and no less than the minimum value
if self._bins is not None:
new_left = max(new_left, self._bins[0])
self.set_clims((new_left, self._clims[1]))
elif self._grabbed is self.right_clim:
# The right clim must stay to the right of the left clim
new_right = max(self._clims[0], event.world_ray.origin[0])
new_right = max(self._clims[0], ray.origin[0])
# ...and no more than the maximum value
if self._bins is not None:
new_right = min(new_right, self._bins[-1])
self.set_clims((self._clims[0], new_right))
elif self._grabbed is self.gamma_handle:
self.set_gamma(-np.log2(event.world_ray.origin[1]))
self.set_gamma(-np.log2(ray.origin[1]))
elif self._grabbed is None:
intersections = [
node
for node, _dist in event.world_ray.intersections(self.controls)
for node, _dist in ray.intersections(self.controls)
if node.interactive
]
if self.right_clim in intersections or self.left_clim in intersections:
Expand Down
67 changes: 67 additions & 0 deletions examples/keyboard_pan_zoom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Demonstrates keyboard-driven pan and zoom on top of the PanZoom controller.

Arrow keys pan the view; + and - zoom in and out. Mouse drag and scroll
continue to work as normal via the PanZoom controller.
"""

import numpy as np
from app_model.types import KeyBinding, KeyCode

import scenex as snx
from scenex.app.events import Event, KeyPressEvent

# Build a recognisable test image: a grid of bright dots on a dark background.
rng = np.random.default_rng(0)
data = np.zeros((512, 512), dtype=np.uint8)
coords = rng.integers(8, 504, size=(200, 2))
for y, x in coords:
data[y - 3 : y + 3, x - 3 : x + 3] = 255

view = snx.View(
scene=snx.Scene(children=[snx.Image(data=data)]),
camera=snx.Camera(controller=snx.PanZoom(), interactive=True),
)
canvas = snx.Canvas(views=[view])

_PAN_STEP = 20.0 # world units per arrow-key press
_ZOOM_STEP = 1.25 # multiplicative factor per +/- press

LEFT = KeyBinding.validate(KeyCode.LeftArrow)
RIGHT = KeyBinding.validate(KeyCode.RightArrow)
UP = KeyBinding.validate(KeyCode.UpArrow)
DOWN = KeyBinding.validate(KeyCode.DownArrow)
ZOOM_IN = KeyBinding.validate(KeyCode.NumpadAdd) # + key
ZOOM_OUT = KeyBinding.validate(KeyCode.NumpadSubtract) # - key


def _key_filter(event: Event) -> bool:
"""Pan with arrow keys; zoom with + / -."""
if not isinstance(event, KeyPressEvent):
return False

key = event.key # KeyCode (or KeyCombo for modified keys)
print(f"key_down: {key}")
cam = view.camera

if key == UP:
cam.transform = cam.transform.translated((0, -_PAN_STEP))
elif key == DOWN:
cam.transform = cam.transform.translated((0, _PAN_STEP))
elif key == LEFT:
cam.transform = cam.transform.translated((_PAN_STEP, 0))
elif key == RIGHT:
cam.transform = cam.transform.translated((-_PAN_STEP, 0))
elif key == ZOOM_IN:
s = _ZOOM_STEP
cam.projection = cam.projection.scaled((s, s, 1.0))
elif key == ZOOM_OUT:
s = 1.0 / _ZOOM_STEP
cam.projection = cam.projection.scaled((s, s, 1.0))

return True


canvas.set_event_filter(_key_filter)

snx.show(canvas)
snx.run()
6 changes: 4 additions & 2 deletions examples/multi_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,11 @@ def _make_scene() -> snx.Scene:
# -z axis and has no mouse interaction.
def _view1_event_filter(event: Event) -> bool:
if isinstance(event, MouseMoveEvent):
for node, distance in event.world_ray.intersections(view1.scene):
if not (ray := view1.to_ray(event.pos)):
return False
for node, distance in ray.intersections(view1.scene):
if node in vols:
intersection = event.world_ray.point_at_distance(distance)
intersection = ray.point_at_distance(distance)
idx = max(0, min(59, round(intersection[2])))
for img in imgs:
img.data = img_data[idx]
Expand Down
4 changes: 3 additions & 1 deletion examples/rgb.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@

def _event_filter(event: events.Event) -> bool:
if isinstance(event, events.MousePressEvent):
for node, _distance in event.world_ray.intersections(view.scene):
if not (ray := view.to_ray(event.pos)):
return False
for node, _distance in ray.intersections(view.scene):
if node == img:
global idx
img.data = data[:, :, idx % 3]
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ classifiers = [
"Programming Language :: Python :: 3.13",
"Typing :: Typed",
]
dependencies = ["cmap>=0.5", "numpy>=1.24", "psygnal>=0.11.1", "pydantic>=2.10", "pylinalg"]
dependencies = ["app-model>=0.3", "cmap>=0.5", "numpy>=1.24", "psygnal>=0.11.1", "pydantic>=2.10", "pylinalg"]

[project.optional-dependencies]
jupyter = [
Expand Down
Loading
Loading