cellier 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. cellier/__init__.py +10 -0
  2. cellier/convenience/__init__.py +4 -0
  3. cellier/convenience/qt/__init__.py +1 -0
  4. cellier/convenience/qt/qt_viewer.py +43 -0
  5. cellier/events/__init__.py +5 -0
  6. cellier/events/_event_bus.py +25 -0
  7. cellier/events/_visual.py +175 -0
  8. cellier/gui/__init__.py +1 -0
  9. cellier/gui/constants.py +9 -0
  10. cellier/gui/qt/__init__.py +1 -0
  11. cellier/gui/qt/utils.py +36 -0
  12. cellier/models/__init__.py +1 -0
  13. cellier/models/data_manager.py +45 -0
  14. cellier/models/data_stores/__init__.py +6 -0
  15. cellier/models/data_stores/base_data_store.py +36 -0
  16. cellier/models/data_stores/lines.py +84 -0
  17. cellier/models/data_stores/points.py +84 -0
  18. cellier/models/scene/__init__.py +15 -0
  19. cellier/models/scene/cameras.py +106 -0
  20. cellier/models/scene/canvas.py +22 -0
  21. cellier/models/scene/dims_manager.py +106 -0
  22. cellier/models/scene/scene.py +49 -0
  23. cellier/models/viewer.py +43 -0
  24. cellier/models/visuals/__init__.py +11 -0
  25. cellier/models/visuals/base.py +43 -0
  26. cellier/models/visuals/lines.py +63 -0
  27. cellier/models/visuals/points.py +57 -0
  28. cellier/py.typed +5 -0
  29. cellier/qt_canvas.py +14 -0
  30. cellier/render/__init__.py +5 -0
  31. cellier/render/_render_manager.py +284 -0
  32. cellier/render/cameras.py +26 -0
  33. cellier/render/constants.py +8 -0
  34. cellier/render/lines.py +101 -0
  35. cellier/render/points.py +110 -0
  36. cellier/render/utils.py +20 -0
  37. cellier/slicer/__init__.py +12 -0
  38. cellier/slicer/data_slice.py +170 -0
  39. cellier/slicer/slicer.py +104 -0
  40. cellier/slicer/transforms.py +169 -0
  41. cellier/slicer/utils.py +33 -0
  42. cellier/slicer/world_slice.py +203 -0
  43. cellier/types.py +14 -0
  44. cellier/util/__init__.py +1 -0
  45. cellier/util/chunk.py +407 -0
  46. cellier/util/geometry.py +202 -0
  47. cellier/v1/__init__.py +10 -0
  48. cellier/v1/convenience/__init__.py +4 -0
  49. cellier/v1/convenience/qt/__init__.py +1 -0
  50. cellier/v1/convenience/qt/qt_viewer.py +43 -0
  51. cellier/v1/gui/__init__.py +1 -0
  52. cellier/v1/gui/constants.py +9 -0
  53. cellier/v1/gui/qt/__init__.py +1 -0
  54. cellier/v1/gui/qt/utils.py +36 -0
  55. cellier/v1/models/__init__.py +1 -0
  56. cellier/v1/models/data_manager.py +73 -0
  57. cellier/v1/models/data_stores/__init__.py +1 -0
  58. cellier/v1/models/data_stores/base_data_store.py +36 -0
  59. cellier/v1/models/data_stores/image.py +298 -0
  60. cellier/v1/models/data_stores/mesh.py +81 -0
  61. cellier/v1/models/data_stores/points.py +119 -0
  62. cellier/v1/models/data_streams/__init__.py +1 -0
  63. cellier/v1/models/data_streams/base_data_stream.py +39 -0
  64. cellier/v1/models/data_streams/image.py +80 -0
  65. cellier/v1/models/data_streams/mesh.py +43 -0
  66. cellier/v1/models/data_streams/points.py +46 -0
  67. cellier/v1/models/nodes/__init__.py +1 -0
  68. cellier/v1/models/nodes/base_node.py +21 -0
  69. cellier/v1/models/nodes/image_node.py +89 -0
  70. cellier/v1/models/nodes/mesh_node.py +96 -0
  71. cellier/v1/models/nodes/points_node.py +54 -0
  72. cellier/v1/models/scene/__init__.py +15 -0
  73. cellier/v1/models/scene/cameras.py +106 -0
  74. cellier/v1/models/scene/canvas.py +22 -0
  75. cellier/v1/models/scene/dims_manager.py +106 -0
  76. cellier/v1/models/scene/scene.py +57 -0
  77. cellier/v1/models/viewer.py +42 -0
  78. cellier/v1/py.typed +5 -0
  79. cellier/v1/qt_canvas.py +14 -0
  80. cellier/v1/render/__init__.py +1 -0
  81. cellier/v1/render/cameras.py +26 -0
  82. cellier/v1/render/image.py +279 -0
  83. cellier/v1/render/mesh.py +107 -0
  84. cellier/v1/render/points.py +115 -0
  85. cellier/v1/render/render_manager.py +237 -0
  86. cellier/v1/render/utils.py +27 -0
  87. cellier/v1/slicer/__init__.py +6 -0
  88. cellier/v1/slicer/data_slice.py +150 -0
  89. cellier/v1/slicer/slicer.py +182 -0
  90. cellier/v1/slicer/transforms.py +169 -0
  91. cellier/v1/slicer/utils.py +33 -0
  92. cellier/v1/slicer/world_slice.py +203 -0
  93. cellier/v1/util/__init__.py +1 -0
  94. cellier/v1/util/chunk.py +407 -0
  95. cellier/v1/util/geometry.py +202 -0
  96. cellier/v1/viewer_controller.py +266 -0
  97. cellier/viewer_controller.py +348 -0
  98. cellier-0.0.2.dist-info/METADATA +46 -0
  99. cellier-0.0.2.dist-info/RECORD +101 -0
  100. cellier-0.0.2.dist-info/WHEEL +4 -0
  101. cellier-0.0.2.dist-info/licenses/LICENSE +28 -0
cellier/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """A workshop for rendering cellular models."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("cellier")
7
+ except PackageNotFoundError:
8
+ __version__ = "uninstalled"
9
+ __author__ = "Kevin Yamauchi"
10
+ __email__ = "kevin.yamauchi@gmail.com"
@@ -0,0 +1,4 @@
1
+ """Functionalities to make building viewers easier.
2
+
3
+ This module will likely get pulled out into a separate package.
4
+ """
@@ -0,0 +1 @@
1
+ """Convenience module for making Qt GUIs."""
@@ -0,0 +1,43 @@
1
+ """Qt widget for the viewer."""
2
+
3
+ from typing import Dict
4
+
5
+ from qtpy.QtWidgets import QVBoxLayout, QWidget
6
+ from wgpu.gui.qt import WgpuCanvas
7
+
8
+ from cellier.models.viewer import ViewerModel
9
+
10
+
11
+ class QtViewer(QWidget):
12
+ """Qt widget for the viewer.
13
+
14
+ This contains all the canvases and the dim sliders.
15
+ """
16
+
17
+ def __init__(self, viewer_model: ViewerModel, parent=None) -> None:
18
+ super().__init__(parent=parent)
19
+
20
+ # make the canvas widgets
21
+ canvas_widgets = {}
22
+ for scene_model in viewer_model.scenes.scenes:
23
+ for canvas_model in scene_model.canvases:
24
+ canvas_widgets[canvas_model.id] = WgpuCanvas(parent=self)
25
+ self._canvases = canvas_widgets
26
+
27
+ # make the layout
28
+ self.setLayout(QVBoxLayout())
29
+ for canvas_widget in self._canvases.values():
30
+ # add the canvas widgets
31
+ self.layout().addWidget(canvas_widget)
32
+
33
+ @property
34
+ def canvases(self) -> Dict[str, WgpuCanvas]:
35
+ """The canvas widgets.
36
+
37
+ Returns
38
+ -------
39
+ Dict[str, WgpuCanvas]
40
+ Dictionary where the keys are the canvas IDs and
41
+ the values are the canvas widgets.
42
+ """
43
+ return self._canvases
@@ -0,0 +1,5 @@
1
+ """Infrastructure for the events system."""
2
+
3
+ from cellier.events._event_bus import EventBus
4
+
5
+ __all__ = ["EventBus"]
@@ -0,0 +1,25 @@
1
+ """Class to manage connecting events between the model and the view."""
2
+
3
+ import logging
4
+
5
+ from cellier.events._visual import VisualEventBus
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class EventBus:
11
+ """Class to manage connecting events between the model and the view.
12
+
13
+ There are three types of events:
14
+ - visual: communicate changes to the visual model state.
15
+ - visual_controls: communicate changes to the visual gui state.
16
+ """
17
+
18
+ def __init__(self):
19
+ # the signals for each visual model
20
+ self._visual_bus = VisualEventBus()
21
+
22
+ @property
23
+ def visual(self) -> VisualEventBus:
24
+ """Return the visual events."""
25
+ return self._visual_bus
@@ -0,0 +1,175 @@
1
+ """Class to manage connecting events between the visual model and the view."""
2
+
3
+ import logging
4
+ from typing import Any, Callable
5
+
6
+ from psygnal import EmissionInfo, Signal, SignalInstance
7
+
8
+ from cellier.types import VisualType
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class VisualEventBus:
14
+ """Class to manage connecting events for the visual models.
15
+
16
+ There are three types of events:
17
+ - visual: communicate changes to the visual model state.
18
+ - visual_controls: communicate changes to the visual gui state.
19
+ """
20
+
21
+ def __init__(self):
22
+ # the signals for each visual model that has been registered
23
+ self._visual_model_signals: dict[str, SignalInstance] = {}
24
+
25
+ # the signals for each visual control that has been registered
26
+ self._visual_control_signals: dict[str, SignalInstance] = {}
27
+
28
+ @property
29
+ def model_signals(self) -> dict[str, SignalInstance]:
30
+ """Return the signals for each registered visual model.
31
+
32
+ The dictionary key is the visual model ID and the value is the SignalInstance.
33
+ """
34
+ return self._visual_model_signals
35
+
36
+ @property
37
+ def control_signals(self) -> dict[str, SignalInstance]:
38
+ """Return the signals for each registered visual control.
39
+
40
+ The dictionary key is the visual model ID and the value is
41
+ the SignalInstance.
42
+ """
43
+ return self._visual_control_signals
44
+
45
+ def register_visual(self, visual: VisualType):
46
+ """Register a visual with the event bus.
47
+
48
+ This will create a signal on the event bus that will be
49
+ emitted when the visual model updates. Other components (e.g., GUI)
50
+ can register to this signal to be notified of changes via the
51
+ subscribe_to_visual() method.
52
+
53
+ Parameters
54
+ ----------
55
+ visual : VisualType
56
+ The visual model to register.
57
+ """
58
+ if visual.id in self.model_signals:
59
+ logging.info(f"Visual {visual.id} is already registered.")
60
+ return
61
+ # connect all events to the visual model update handler
62
+ visual.events.all.connect(self._on_visual_model_update)
63
+
64
+ # initialize the visual model callbacks
65
+ self.model_signals[visual.id] = SignalInstance(
66
+ name=visual.id, check_nargs_on_connect=False, check_types_on_connect=False
67
+ )
68
+
69
+ def subscribe_to_visual(self, visual_id: str, callback: Callable):
70
+ """Subscribe to an event on a visual model.
71
+
72
+ Parameters
73
+ ----------
74
+ visual_id : str
75
+ The ID of the visual model to subscribe to.
76
+ callback : Callable
77
+ The callback to call when the visual model updates.
78
+ """
79
+ try:
80
+ visual_model_signal = self.model_signals[visual_id]
81
+ except KeyError:
82
+ raise ValueError(f"Visual {visual_id} is not registered.") from None
83
+
84
+ # connect the visual model signal
85
+ visual_model_signal.connect(callback)
86
+
87
+ def register_controls(self, visual_id: str, signal: SignalInstance):
88
+ """Register a visual control with the event bus.
89
+
90
+ This creates a signal on the event bus that will be emitted when
91
+ the visual control updates. Visual models can subscribe to this
92
+ signal to be notified of changes.
93
+
94
+ Parameters
95
+ ----------
96
+ visual_id : str
97
+ The ID of the visual model to register the control for.
98
+ signal : SignalInstance
99
+ The signal to register.
100
+ """
101
+ if visual_id not in self.control_signals:
102
+ # If we haven't registered this visual_id yet, create the signal
103
+ self.control_signals[visual_id] = SignalInstance(
104
+ name=visual_id,
105
+ check_nargs_on_connect=False,
106
+ check_types_on_connect=False,
107
+ )
108
+ # connect the visual control signal
109
+ signal.connect(self._on_visual_control_update)
110
+
111
+ def subscribe_to_controls(self, visual_id: str, callback: Callable):
112
+ """Subscribe to the event emitted when the visual controls are updated."""
113
+ try:
114
+ visual_control_signal = self.control_signals[visual_id]
115
+ except KeyError:
116
+ raise ValueError(f"Visual {visual_id} is not registered.") from None
117
+
118
+ # connect the visual control signal
119
+ visual_control_signal.connect(callback)
120
+
121
+ def _on_visual_model_update(self, event: EmissionInfo):
122
+ """Handle a visual model update event.
123
+
124
+ This emits a dictionary containing the visual id and
125
+ the updated property values.
126
+ """
127
+ # get the sender
128
+ emitter_object = Signal.sender()
129
+
130
+ try:
131
+ signal = self.model_signals[emitter_object.id]
132
+ except KeyError:
133
+ logger.debug(
134
+ f"EventBus received event from visual model {emitter_object.id},"
135
+ "but the model is not registered"
136
+ )
137
+
138
+ # dictionary with the updated state
139
+ property_name = event.signal.name
140
+ property_value = event.args[0]
141
+ new_state = {"id": emitter_object.id, property_name: property_value}
142
+ signal.emit(new_state)
143
+
144
+ def _on_visual_control_update(self, event: Any | None = None):
145
+ """Handle a visual control update event.
146
+
147
+ The update dictionary must include the following keys:
148
+ - id: the id of the visual model
149
+ - controls_update_callback: the visual control's update callback that
150
+ should be blocked when updating the visual model.
151
+ this prevents the update from bouncing back
152
+ to the GUI.
153
+ """
154
+ visual_id = event["id"]
155
+ try:
156
+ control_signal = self.control_signals[visual_id]
157
+ except KeyError:
158
+ logger.debug(
159
+ f"EventBus received event from visual model {visual_id},"
160
+ "but the model is not registered"
161
+ )
162
+
163
+ callback_to_block = event.pop("controls_update_callback")
164
+
165
+ # update the visual model
166
+ if visual_id in self._visual_control_signals:
167
+ # block the gui update callback so the signal doesn't bounce back
168
+ visual_signal = self.model_signals[visual_id]
169
+
170
+ # emit the event and temporarily disconnect the callback
171
+ visual_signal.disconnect(callback_to_block, missing_ok=True)
172
+ control_signal.emit(event)
173
+ visual_signal.connect(callback_to_block)
174
+ else:
175
+ control_signal.emit(event)
@@ -0,0 +1 @@
1
+ """GUI related functions."""
@@ -0,0 +1,9 @@
1
+ """Constants for the GUI."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class GuiFramework(Enum):
7
+ """Enum for supported GUI frameworks."""
8
+
9
+ QT = "qt"
@@ -0,0 +1 @@
1
+ """Qt GUI related functions."""
@@ -0,0 +1,36 @@
1
+ """Utility functions for generating Qt widgets."""
2
+
3
+ from typing import Dict, Optional
4
+
5
+ from qtpy.QtWidgets import QWidget
6
+ from wgpu.gui.qt import WgpuCanvas
7
+
8
+ from cellier.models.viewer import ViewerModel
9
+
10
+
11
+ def construct_qt_canvases_from_model(
12
+ viewer_model: ViewerModel, parent: Optional[QWidget] = None
13
+ ) -> Dict[str, WgpuCanvas]:
14
+ """Construct Qt PyGFX canvas widgets from the viewer model.
15
+
16
+ Parameters
17
+ ----------
18
+ viewer_model : ViewerModel
19
+ The viewer model to construct the Qt canvases from.
20
+
21
+ parent : Optional[QWidget]
22
+ The parent for the constructed Qt canvases.
23
+ Default value is None.
24
+
25
+ Returns
26
+ -------
27
+ Dict[str, WgpuCanvas]
28
+ A dictionary where the canvas IDs are the keys and
29
+ The constructed Qt canvases are the values.
30
+ """
31
+ canvas_widgets = {}
32
+ for scene_model in viewer_model.scenes.scenes.values():
33
+ for canvas_model in scene_model.canvases.values():
34
+ canvas_widgets[canvas_model.id] = WgpuCanvas(parent=parent)
35
+
36
+ return canvas_widgets
@@ -0,0 +1 @@
1
+ """Models to express the viewer state."""
@@ -0,0 +1,45 @@
1
+ """Class to hold all of the data stores and streams."""
2
+
3
+ from typing import Dict, Union
4
+
5
+ from psygnal import EventedModel
6
+ from pydantic import Field
7
+ from typing_extensions import Annotated
8
+
9
+ from cellier.models.data_stores.points import PointsMemoryStore
10
+
11
+ # types for discrimitive unions
12
+ DataStoreType = Annotated[
13
+ Union[PointsMemoryStore,],
14
+ Field(discriminator="store_type"),
15
+ ]
16
+
17
+
18
+ class DataManager(EventedModel):
19
+ """Class to model all data_stores in the viewer.
20
+
21
+ todo: add discrimitive union
22
+
23
+ Attributes
24
+ ----------
25
+ stores : Dict[str, DataStoreType]
26
+ The data stores in the viewer.
27
+ The key to the store is the data store id.
28
+
29
+ """
30
+
31
+ stores: Dict[str, DataStoreType]
32
+
33
+ def add_data_store(self, data_store: DataStoreType):
34
+ """Add a data store to the viewer.
35
+
36
+ Parameters
37
+ ----------
38
+ data_store : DataStoreType
39
+ The data store to add to the viewer.
40
+
41
+ """
42
+ self.stores[data_store.id] = data_store
43
+
44
+ # emit event to signal that the data has been updated
45
+ self.events.stores.emit()
@@ -0,0 +1,6 @@
1
+ """Models for the DataStore classes."""
2
+
3
+ from cellier.models.data_stores.lines import LinesMemoryStore
4
+ from cellier.models.data_stores.points import PointsMemoryStore
5
+
6
+ __all__ = ["LinesMemoryStore", "PointsMemoryStore"]
@@ -0,0 +1,36 @@
1
+ """Base Data Store classes."""
2
+
3
+ from dataclasses import dataclass
4
+ from uuid import uuid4
5
+
6
+ from psygnal import EventedModel
7
+ from pydantic import Field
8
+
9
+
10
+ class BaseDataStore(EventedModel):
11
+ """The base class for all DataStores.
12
+
13
+ Parameters
14
+ ----------
15
+ id : str
16
+ The unique identifier for the data store.
17
+ The default value is a UUID4 generated hex string.
18
+
19
+ Attributes
20
+ ----------
21
+ id : str
22
+ The unique identifier for the data store.
23
+ """
24
+
25
+ # store a UUID to identify this specific scene.
26
+ id: str = Field(default_factory=lambda: uuid4().hex)
27
+ name: str = "data store"
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class DataStoreSlice:
32
+ """Base class for all data store slices."""
33
+
34
+ scene_id: str
35
+ visual_id: str
36
+ resolution_level: int
@@ -0,0 +1,84 @@
1
+ """Classes for lines DataStores."""
2
+
3
+ from typing import Literal
4
+
5
+ import numpy as np
6
+ from pydantic import ConfigDict, field_serializer, field_validator
7
+ from pydantic_core.core_schema import ValidationInfo
8
+
9
+ from cellier.models.data_stores.base_data_store import BaseDataStore
10
+ from cellier.slicer.data_slice import DataSliceRequest, RenderedLinesDataSlice
11
+
12
+
13
+ class BaseLinesDataStore(BaseDataStore):
14
+ """The base class for all lines data_stores.
15
+
16
+ todo: properly set up. this shouldn't specify ndarrays.
17
+ """
18
+
19
+ coordinates: np.ndarray
20
+
21
+ model_config = ConfigDict(arbitrary_types_allowed=True)
22
+
23
+ @field_validator("coordinates", mode="before")
24
+ @classmethod
25
+ def coerce_to_ndarray_float32(cls, v: str, info: ValidationInfo):
26
+ """Coerce to a float32 numpy array."""
27
+ if not isinstance(v, np.ndarray):
28
+ v = np.asarray(v, dtype=np.float32)
29
+ return v.astype(np.float32)
30
+
31
+
32
+ class LinesMemoryStore(BaseLinesDataStore):
33
+ """Point data_stores store for arrays stored in memory."""
34
+
35
+ # this is used for a discriminated union
36
+ store_type: Literal["lines_memory"] = "lines_memory"
37
+
38
+ @field_serializer("coordinates")
39
+ def serialize_ndarray(self, array: np.ndarray, _info) -> list:
40
+ """Coerce numpy arrays into lists for serialization."""
41
+ return array.tolist()
42
+
43
+ def get_slice(self, slice_data: DataSliceRequest) -> RenderedLinesDataSlice:
44
+ """Get the data required to render a slice of the mesh.
45
+
46
+ todo: generalize to oblique slicing
47
+ """
48
+ displayed_dimensions = list(slice_data.world_slice.displayed_dimensions)
49
+ points_ndim = self.coordinates.shape[1]
50
+
51
+ # get a mask for the not displayed dimensions
52
+ not_displayed_mask = np.ones((points_ndim,), dtype=bool)
53
+ not_displayed_mask[displayed_dimensions] = False
54
+
55
+ # get the range to include
56
+ point = np.asarray(slice_data.world_slice.point)
57
+ margin_negative = np.asarray(slice_data.world_slice.margin_negative)
58
+ margin_positive = np.asarray(slice_data.world_slice.margin_positive)
59
+ low = point - margin_negative
60
+ high = point + margin_positive
61
+
62
+ # get the components of the range from the not displayed dimensions
63
+ not_displayed_low = low[not_displayed_mask]
64
+ not_displayed_high = high[not_displayed_mask]
65
+
66
+ # find the coordinates inside the slice
67
+ not_displayed_coordinates = self.coordinates[:, not_displayed_mask]
68
+
69
+ inside_slice_mask = np.all(
70
+ (not_displayed_coordinates >= not_displayed_low)
71
+ & (not_displayed_coordinates <= not_displayed_high),
72
+ axis=1,
73
+ )
74
+
75
+ in_slice_coordinates = np.atleast_2d(
76
+ self.coordinates[inside_slice_mask, :][:, displayed_dimensions]
77
+ )
78
+
79
+ return RenderedLinesDataSlice(
80
+ scene_id=slice_data.scene_id,
81
+ visual_id=slice_data.visual_id,
82
+ resolution_level=slice_data.resolution_level,
83
+ coordinates=in_slice_coordinates,
84
+ )
@@ -0,0 +1,84 @@
1
+ """Classes for Point DataStores."""
2
+
3
+ from typing import Literal
4
+
5
+ import numpy as np
6
+ from pydantic import ConfigDict, field_serializer, field_validator
7
+ from pydantic_core.core_schema import ValidationInfo
8
+
9
+ from cellier.models.data_stores.base_data_store import BaseDataStore
10
+ from cellier.slicer.data_slice import DataSliceRequest, RenderedPointsDataSlice
11
+
12
+
13
+ class BasePointsDataStore(BaseDataStore):
14
+ """The base class for all point data_stores.
15
+
16
+ todo: properly set up. this shouldn't specify ndarrays.
17
+ """
18
+
19
+ coordinates: np.ndarray
20
+
21
+ model_config = ConfigDict(arbitrary_types_allowed=True)
22
+
23
+ @field_validator("coordinates", mode="before")
24
+ @classmethod
25
+ def coerce_to_ndarray_float32(cls, v: str, info: ValidationInfo):
26
+ """Coerce to a float32 numpy array."""
27
+ if not isinstance(v, np.ndarray):
28
+ v = np.asarray(v, dtype=np.float32)
29
+ return v.astype(np.float32)
30
+
31
+
32
+ class PointsMemoryStore(BasePointsDataStore):
33
+ """Point data_stores store for arrays stored in memory."""
34
+
35
+ # this is used for a discriminated union
36
+ store_type: Literal["points_memory"] = "points_memory"
37
+
38
+ @field_serializer("coordinates")
39
+ def serialize_ndarray(self, array: np.ndarray, _info) -> list:
40
+ """Coerce numpy arrays into lists for serialization."""
41
+ return array.tolist()
42
+
43
+ def get_slice(self, slice_data: DataSliceRequest) -> RenderedPointsDataSlice:
44
+ """Get the data required to render a slice of the mesh.
45
+
46
+ todo: generalize to oblique slicing
47
+ """
48
+ displayed_dimensions = list(slice_data.world_slice.displayed_dimensions)
49
+ points_ndim = self.coordinates.shape[1]
50
+
51
+ # get a mask for the not displayed dimensions
52
+ not_displayed_mask = np.ones((points_ndim,), dtype=bool)
53
+ not_displayed_mask[displayed_dimensions] = False
54
+
55
+ # get the range to include
56
+ point = np.asarray(slice_data.world_slice.point)
57
+ margin_negative = np.asarray(slice_data.world_slice.margin_negative)
58
+ margin_positive = np.asarray(slice_data.world_slice.margin_positive)
59
+ low = point - margin_negative
60
+ high = point + margin_positive
61
+
62
+ # get the components of the range from the not displayed dimensions
63
+ not_displayed_low = low[not_displayed_mask]
64
+ not_displayed_high = high[not_displayed_mask]
65
+
66
+ # find the coordinates inside the slice
67
+ not_displayed_coordinates = self.coordinates[:, not_displayed_mask]
68
+
69
+ inside_slice_mask = np.all(
70
+ (not_displayed_coordinates >= not_displayed_low)
71
+ & (not_displayed_coordinates <= not_displayed_high),
72
+ axis=1,
73
+ )
74
+
75
+ in_slice_coordinates = np.atleast_2d(
76
+ self.coordinates[inside_slice_mask, :][:, displayed_dimensions]
77
+ )
78
+
79
+ return RenderedPointsDataSlice(
80
+ scene_id=slice_data.scene_id,
81
+ visual_id=slice_data.visual_id,
82
+ resolution_level=slice_data.resolution_level,
83
+ coordinates=in_slice_coordinates,
84
+ )
@@ -0,0 +1,15 @@
1
+ """Models for the scene objects."""
2
+
3
+ from cellier.models.scene.cameras import OrthographicCamera, PerspectiveCamera
4
+ from cellier.models.scene.canvas import Canvas
5
+ from cellier.models.scene.dims_manager import CoordinateSystem, DimsManager
6
+ from cellier.models.scene.scene import Scene
7
+
8
+ __all__ = [
9
+ "Canvas",
10
+ "CoordinateSystem",
11
+ "DimsManager",
12
+ "OrthographicCamera",
13
+ "PerspectiveCamera",
14
+ "Scene",
15
+ ]