-
Notifications
You must be signed in to change notification settings - Fork 64
Expand file tree
/
Copy path_base.py
More file actions
359 lines (274 loc) · 9.89 KB
/
_base.py
File metadata and controls
359 lines (274 loc) · 9.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
from abc import ABC, abstractmethod
from inspect import getfullargspec
from warnings import warn
from typing import *
import weakref
import numpy as np
import pygfx
supported_dtypes = [
np.uint8,
np.uint16,
np.uint32,
np.int8,
np.int16,
np.int32,
np.float16,
np.float32,
]
def to_gpu_supported_dtype(array):
"""
If ``array`` is a numpy array, converts it to a supported type. GPUs don't support 64 bit dtypes.
"""
if isinstance(array, np.ndarray):
if array.dtype not in supported_dtypes:
if np.issubdtype(array.dtype, np.integer):
warn(f"converting {array.dtype} array to int32")
return array.astype(np.int32)
elif np.issubdtype(array.dtype, np.floating):
warn(f"converting {array.dtype} array to float32")
return array.astype(np.float32, copy=False)
else:
raise TypeError(
"Unsupported type, supported array types must be int or float dtypes"
)
return array
class FeatureEvent:
"""
Dataclass that holds feature event information. Has ``type`` and ``pick_info`` attributes.
Attributes
----------
type: str, example "colors"
pick_info: dict:
============== =============================================================================
key value
============== =============================================================================
"index" indices where feature data was changed, ``range`` object or ``List[int]``
"world_object" world object the feature belongs to
"new_data: the new data for this feature
============== =============================================================================
.. note::
pick info varies between features, this is just the general structure
"""
def __init__(self, type: str, pick_info: dict):
self.type = type
self.pick_info = pick_info
def __repr__(self):
return (
f"{self.__class__.__name__} @ {hex(id(self))}\n"
f"type: {self.type}\n"
f"pick_info: {self.pick_info}\n"
)
class GraphicFeature(ABC):
def __init__(self, parent, data: Any, collection_index: int = None):
# not shown as a docstring so it doesn't show up in the docs
#
# Parameters
# ----------
# parent
#
# data: Any
#
# collection_index: int
# if part of a collection, index of this graphic within the collection
self._parent = weakref.proxy(parent)
self._data = to_gpu_supported_dtype(data)
self._collection_index = collection_index
self._event_handlers = list()
self._block_events = False
def __call__(self, *args, **kwargs):
return self._data
def block_events(self, val: bool):
"""
Block all events from this feature
Parameters
----------
val: bool
``True`` or ``False``
"""
self._block_events = val
@abstractmethod
def _set(self, value):
pass
def _parse_set_value(self, value):
if isinstance(value, GraphicFeature):
return value()
return value
def add_event_handler(self, handler: callable):
"""
Add an event handler. All added event handlers are called when this feature changes.
The ``handler`` can optionally accept a :class:`.FeatureEvent` as the first and only argument.
The ``FeatureEvent`` only has two attributes, ``type`` which denotes the type of event
as a ``str`` in the form of "<feature_name>", such as "color". And ``pick_info`` which contains
information about the event and Graphic that triggered it.
Parameters
----------
handler: callable
a function to call when this feature changes
"""
if not callable(handler):
raise TypeError("event handler must be callable")
if handler in self._event_handlers:
warn(f"Event handler {handler} is already registered.")
return
self._event_handlers.append(handler)
def remove_event_handler(self, handler: callable):
"""
Remove a registered event ``handler``.
Parameters
----------
handler: callable
event handler to remove
"""
if handler not in self._event_handlers:
raise KeyError(f"event handler {handler} not registered.")
self._event_handlers.remove(handler)
def clear_event_handlers(self):
"""Clear all event handlers"""
self._event_handlers.clear()
# TODO: maybe this can be implemented right here in the base class
@abstractmethod
def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any):
"""Called whenever a feature changes, and it calls all funcs in self._event_handlers"""
pass
def _call_event_handlers(self, event_data: FeatureEvent):
if self._block_events:
return
for func in self._event_handlers:
try:
args = getfullargspec(func).args
if len(args) > 0:
if args[0] == "self" and not len(args) > 1:
func()
else:
func(event_data)
else:
func()
except TypeError:
warn(
f"Event handler {func} has an unresolvable argspec, calling it without arguments"
)
func()
@abstractmethod
def __repr__(self) -> str:
pass
def cleanup_slice(key: Union[int, slice], upper_bound) -> Union[slice, int]:
"""
If the key in an `int`, it just returns it. Otherwise,
it parses it and removes the `None` vals and replaces
them with corresponding values that can be used to
create a `range`, get `len` etc.
Parameters
----------
key
upper_bound
Returns
-------
"""
if isinstance(key, int):
return key
if isinstance(key, np.ndarray):
return cleanup_array_slice(key, upper_bound)
if isinstance(key, tuple):
# if tuple of slice we only need the first obj
# since the first obj is the datapoint indices
if isinstance(key[0], slice):
key = key[0]
else:
raise TypeError("Tuple slicing must have slice object in first position")
if not isinstance(key, slice):
raise TypeError("Must pass slice or int object")
start = key.start
stop = key.stop
step = key.step
for attr in [start, stop, step]:
if attr is None:
continue
if attr < 0:
raise IndexError("Negative indexing not supported.")
if start is None:
start = 0
if stop is None:
stop = upper_bound
elif stop > upper_bound:
raise IndexError(
f"Index: `{stop}` out of bounds for feature array of size: `{upper_bound}`"
)
step = key.step
if step is None:
step = 1
return slice(start, stop, step)
def cleanup_array_slice(key: np.ndarray, upper_bound) -> Union[np.ndarray, None]:
"""
Cleanup numpy array used for fancy indexing, make sure key[-1] <= upper_bound.
Returns None if nothing to change.
Parameters
----------
key: np.ndarray
integer or boolean array
upper_bound
Returns
-------
np.ndarray
integer indexing array
"""
if key.ndim > 1:
raise TypeError(f"Can only use 1D boolean or integer arrays for fancy indexing")
# if boolean array convert to integer array of indices
if key.dtype == bool:
key = np.nonzero(key)[0]
if key.size < 1:
return None
# make sure indices within bounds of feature buffer range
if key[-1] > upper_bound:
raise IndexError(
f"Index: `{key[-1]}` out of bounds for feature array of size: `{upper_bound}`"
)
# make sure indices are integers
if np.issubdtype(key.dtype, np.integer):
return key
raise TypeError(f"Can only use 1D boolean or integer arrays for fancy indexing")
class GraphicFeatureIndexable(GraphicFeature):
"""An indexable Graphic Feature, colors, data, sizes etc."""
def _set(self, value):
value = self._parse_set_value(value)
self[:] = value
@abstractmethod
def __getitem__(self, item):
pass
@abstractmethod
def __setitem__(self, key, value):
pass
@abstractmethod
def _update_range(self, key):
pass
@property
@abstractmethod
def buffer(self) -> Union[pygfx.Buffer, pygfx.Texture]:
"""Underlying buffer for this feature"""
pass
@property
def _upper_bound(self) -> int:
return self._data.shape[0]
def _update_range_indices(self, key):
"""Currently used by colors and positions data"""
if not isinstance(key, np.ndarray):
key = cleanup_slice(key, self._upper_bound)
if isinstance(key, int):
self.buffer.update_range(key, size=1)
return
# else if it's a slice obj
if isinstance(key, slice):
if key.step == 1: # we cleaned up the slice obj so step of None becomes 1
# update range according to size using the offset
self.buffer.update_range(offset=key.start, size=key.stop - key.start)
else:
step = key.step
# convert slice to indices
ixs = range(key.start, key.stop, step)
for ix in ixs:
self.buffer.update_range(ix, size=1)
# TODO: See how efficient this is with large indexing
elif isinstance(key, np.ndarray):
self.buffer.update_range()
else:
raise TypeError("must pass int or slice to update range")