Skip to content

Commit e81648e

Browse files
add transforms (#39)
* start chunk selection * fix circular import * add transform tests * remove old transform implementation * move chunked image utils * add init * rename utils * rename utils * update chunk * add transform to visual * add transform setting * implement set_transform
1 parent a247c63 commit e81648e

30 files changed

Lines changed: 2575 additions & 116 deletions

‎examples/oblique_slicing.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from qtpy.QtWidgets import QApplication, QGridLayout, QLabel, QWidget
99
from wgpu.gui.qt import WgpuCanvas
1010

11-
from cellier.slicer.transforms import AffineTransform
11+
from cellier.transform import AffineTransform
1212

1313

1414
def make_plane_mesh(

‎examples/tiled_rendering/octree_pyfx.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
from skimage.transform import resize
88
from wgpu.gui.qt import WgpuCanvas
99

10-
from cellier.util.chunk import ChunkedArray3D, MultiScaleChunkedArray3D
11-
from cellier.util.geometry import (
10+
from cellier.utils.chunk import ChunkedArray3D, MultiScaleChunkedArray3D
11+
from cellier.utils.geometry import (
1212
frustum_planes_from_corners,
1313
)
1414

‎examples/tiled_rendering/tiled_render_zarr.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
from wgpu.gui.qt import WgpuCanvas
1414

1515
from cellier.models.data_stores.image import MultiScaleImageZarrStore
16-
from cellier.util.chunk import (
16+
from cellier.utils.chunk import (
1717
ChunkedArray3D,
1818
ImageDataStoreChunk,
1919
MultiScaleChunkedArray3D,
2020
)
21-
from cellier.util.geometry import (
21+
from cellier.utils.geometry import (
2222
frustum_planes_from_corners,
2323
)
2424

‎src/cellier/models/data_stores/image.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
TilingMethod,
2525
VisualId,
2626
)
27-
from cellier.util.geometry import generate_2d_grid
27+
from cellier.utils.geometry import generate_2d_grid
2828

2929

3030
class BaseImageDataStore(BaseDataStore):

‎src/cellier/models/viewer.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
from cellier.models.data_manager import DataManager
99
from cellier.models.scene import DimsManager, DimsState, Scene
10+
from cellier.models.visuals.base import BaseVisual
11+
from cellier.types import VisualId
1012

1113

1214
class SceneManager(EventedModel):
@@ -52,6 +54,31 @@ class ViewerModel(EventedModel):
5254
data: DataManager
5355
scenes: SceneManager
5456

57+
def get_visual_by_id(self, visual_id: VisualId) -> tuple[Scene, BaseVisual]:
58+
"""Get a visual model by its id.
59+
60+
This iterates through all scenes. If you know the scene the visual
61+
is in, it is likely more efficient to get the scene first and then
62+
get the visual from the scene.
63+
64+
Parameters
65+
----------
66+
visual_id : VisualId
67+
The ID of the visual model to retrieve.
68+
69+
Returns
70+
-------
71+
Scene
72+
The scene containing the visual model.
73+
BaseVisual
74+
The visual model with the specified ID.
75+
"""
76+
for scene in self.scenes.scenes.values():
77+
visual_model = scene.get_visual_by_id(visual_id)
78+
if visual_model is not None:
79+
return scene, visual_model
80+
raise ValueError(f"Visual model with id {visual_id} not found.")
81+
5582
def to_json_file(self, file_path: str, indent: int = 2) -> None:
5683
"""Save the viewer state as a JSON file."""
5784
with open(file_path, "w") as f:

‎src/cellier/models/visuals/base.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,17 @@
33
from typing import Any
44
from uuid import uuid4
55

6+
import numpy as np
67
from psygnal import EmissionInfo, EventedModel
78
from pydantic import Field
89

10+
from cellier.transform import AffineTransform
11+
12+
13+
def default_transform() -> AffineTransform:
14+
"""Create a default identity transform."""
15+
return AffineTransform(matrix=np.eye(4))
16+
917

1018
class BaseAppearance(EventedModel):
1119
"""Base model for all materials.
@@ -31,13 +39,17 @@ class BaseVisual(EventedModel):
3139
The appearance of the visual.
3240
This should be overridden with the visual-specific
3341
implementation in the subclasses.
42+
transform : AffineTransform
43+
The transform of the visual from data space to world space.
44+
Default is the identity.
3445
pick_write : bool
3546
If True, the visual can be picked.
3647
Default value is True.
3748
"""
3849

3950
name: str
4051
appearance: BaseAppearance
52+
transform: AffineTransform = Field(default_factory=default_transform)
4153
pick_write: bool = True
4254

4355
# store a UUID to identify this specific scene.

‎src/cellier/render/_render_manager.py‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
)
2424
from cellier.render.cameras import construct_pygfx_camera_from_model
2525
from cellier.render.utils import construct_pygfx_object
26+
from cellier.transform import BaseTransform
2627
from cellier.types import (
2728
CameraControlsUpdateEvent,
2829
CameraId,
@@ -575,14 +576,42 @@ def _on_visual_model_update(self, new_state: dict):
575576

576577
visual = self._visuals.get(visual_id)
577578
if visual is None:
578-
logger.warning(f"Visual with id {visual_id} not found.")
579+
logger.warning(
580+
f"RenderManager attempted to update visual with id {visual_id},"
581+
" but it wasn't found."
582+
)
579583
return
580584

581585
# update the visual with the new state
582586
if "appearance" in new_state:
583587
visual.update_appearance(new_state["appearance"])
584588

589+
if "transform" in new_state:
590+
transform = new_state["transform"]
591+
visual.set_transform(transform)
592+
585593
# emit a redraw request for the canvas
586594
scene_id = new_state.get("scene_id")
587595
if scene_id:
588596
self.events.redraw_canvas.emit(CanvasRedrawRequest(scene_id=scene_id))
597+
598+
def _on_visual_transform_update(
599+
self,
600+
scene_id: SceneId,
601+
visual_id: VisualId,
602+
transform: BaseTransform,
603+
redraw_canvas: bool = True,
604+
):
605+
"""Update the visual transform based on a change to the model."""
606+
visual = self._visuals.get(visual_id)
607+
if visual is None:
608+
logger.warning(
609+
f"RenderManager attempted to update visual with id {visual_id},"
610+
"but it wasn't found."
611+
)
612+
return
613+
614+
visual.set_transform(transform)
615+
616+
if redraw_canvas:
617+
self.events.redraw_canvas.emit(CanvasRedrawRequest(scene_id=scene_id))

‎src/cellier/render/image.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import wgpu
88

99
from cellier.models.visuals import ImageAppearance, MultiscaleImageVisual
10+
from cellier.transform import BaseTransform
1011
from cellier.types import ImageDataResponse
1112

1213

@@ -193,3 +194,14 @@ def update_appearance(self, new_state: dict):
193194
if "visible" in new_state:
194195
# update the visibility
195196
self.node.visible = new_state["visible"]
197+
198+
def set_transform(self, transform: BaseTransform):
199+
"""Set the local transform of the node.
200+
201+
Parameters
202+
----------
203+
transform : np.ndarray
204+
The 4x4 affine transform matrix to set.
205+
"""
206+
# set the local transform
207+
self.node.local.matrix = transform.matrix

‎src/cellier/render/labels.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from cellier.models.visuals import LabelsAppearance, MultiscaleLabelsVisual
1010
from cellier.render.shaders import LabelImageMaterial, LabelIsoMaterial
11+
from cellier.transform import BaseTransform
1112
from cellier.types import ImageDataResponse
1213

1314

@@ -194,3 +195,14 @@ def update_appearance(self, new_state: dict):
194195
if "visible" in new_state:
195196
# update the visibility
196197
self.node.visible = new_state["visible"]
198+
199+
def set_transform(self, transform: BaseTransform):
200+
"""Set the local transform of the node.
201+
202+
Parameters
203+
----------
204+
transform : np.ndarray
205+
The 4x4 affine transform matrix to set.
206+
"""
207+
# set the local transform
208+
self.node.local.matrix = transform.matrix

‎src/cellier/render/lines.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
LinesVisual,
1212
)
1313
from cellier.render.constants import cellier_to_gfx_coordinate_space
14+
from cellier.transform import BaseTransform
1415
from cellier.types import LinesDataResponse
1516

1617

@@ -155,3 +156,14 @@ def update_appearance(self, new_state: dict):
155156
if "visible" in new_state:
156157
# update the visibility
157158
self.node.visible = new_state["visible"]
159+
160+
def set_transform(self, transform: BaseTransform):
161+
"""Set the local transform of the node.
162+
163+
Parameters
164+
----------
165+
transform : np.ndarray
166+
The 4x4 affine transform matrix to set.
167+
"""
168+
# set the local transform
169+
self.node.local.matrix = transform.matrix

0 commit comments

Comments
 (0)