12
12
13
13
import pygfx
14
14
15
- from ._features import GraphicFeature , BufferManager , GraphicFeatureDescriptor , Deleted , PointsDataFeature , ColorFeature , PointsSizesFeature , Name , Offset , Rotation , Visible
16
-
15
+ from ._features import GraphicFeature , BufferManager , Deleted , VertexPositions , VertexColors , PointsSizesFeature , Name , Offset , Rotation , Visible , UniformColor
16
+ from .. utils import parse_cmap_values
17
17
18
18
HexStr : TypeAlias = str
19
19
38
38
]
39
39
40
40
41
- class BaseGraphic :
41
+ class Graphic :
42
+ features = {}
43
+
44
+ @property
45
+ def name (self ) -> str | None :
46
+ """Graphic name"""
47
+ return self ._name .value
48
+
49
+ @name .setter
50
+ def name (self , value : str ):
51
+ self ._name .set_value (self , value )
52
+
53
+ @property
54
+ def offset (self ) -> tuple :
55
+ """Offset position of the graphic, [x, y, z]"""
56
+ return self ._offset .value
57
+
58
+ @offset .setter
59
+ def offset (self , value : tuple [float , float , float ]):
60
+ self ._offset .set_value (self , value )
61
+
62
+ @property
63
+ def rotation (self ) -> np .ndarray :
64
+ """Orientation of the graphic as a quaternion"""
65
+ return self ._rotation .value
66
+
67
+ @rotation .setter
68
+ def rotation (self , value : tuple [float , float , float , float ]):
69
+ self ._rotation .set_value (self , value )
70
+
71
+ @property
72
+ def visible (self ) -> bool :
73
+ """Whether the graphic is visible"""
74
+ return self ._visible .value
75
+
76
+ @visible .setter
77
+ def visible (self , value : bool ):
78
+ self ._visible .set_value (self , value )
79
+
80
+ @property
81
+ def deleted (self ) -> bool :
82
+ """used to emit an event after the graphic is deleted"""
83
+ return self ._deleted .value
84
+
85
+ @deleted .setter
86
+ def deleted (self , value : bool ):
87
+ self ._deleted .set_value (self , value )
88
+
42
89
def __init_subclass__ (cls , ** kwargs ):
43
- """ set the type of the graphic in lower case like "image", "line_collection", etc."""
90
+ # set the type of the graphic in lower case like "image", "line_collection", etc.
44
91
cls .type = (
45
92
cls .__name__ .lower ()
46
93
.replace ("graphic" , "" )
47
94
.replace ("collection" , "_collection" )
48
95
.replace ("stack" , "_stack" )
49
96
)
50
97
51
- super ().__init_subclass__ (** kwargs )
52
-
53
-
54
- class Graphic (BaseGraphic ):
55
- features = {}
56
-
57
- def __init_subclass__ (cls , ** kwargs ):
58
- super ().__init_subclass__ (** kwargs )
98
+ # set of all features
59
99
cls .features = {* cls .features , "name" , "offset" , "rotation" , "visible" , "deleted" }
60
-
61
- # graphic feature class attributes
62
- for f in cls .features :
63
- setattr (cls , f , GraphicFeatureDescriptor (f ))
100
+ super ().__init_subclass__ (** kwargs )
64
101
65
102
def __init__ (
66
103
self ,
67
104
name : str = None ,
105
+ offset : tuple [float ] = (0. , 0. , 0. ),
68
106
metadata : Any = None ,
69
107
collection_index : int = None ,
70
108
):
@@ -82,7 +120,6 @@ def __init__(
82
120
if (name is not None ) and (not isinstance (name , str )):
83
121
raise TypeError ("Graphic `name` must be of type <str>" )
84
122
85
- self ._name = Name (name )
86
123
self .metadata = metadata
87
124
self .collection_index = collection_index
88
125
self .registered_callbacks = dict ()
@@ -91,8 +128,6 @@ def __init__(
91
128
# store hex id str of Graphic instance mem location
92
129
self ._fpl_address : HexStr = hex (id (self ))
93
130
94
- self ._deleted = Deleted (False )
95
-
96
131
self ._plot_area = None
97
132
98
133
# event handlers
@@ -101,6 +136,13 @@ def __init__(
101
136
# maps callbacks to their partials
102
137
self ._event_handler_wrappers = defaultdict (set )
103
138
139
+ # all the common features
140
+ self ._name = Name (name )
141
+ self ._deleted = Deleted (False )
142
+ self ._rotation = None # set later when world object is set
143
+ self ._offset = Offset (offset )
144
+ self ._visible = Visible (True )
145
+
104
146
@property
105
147
def world_object (self ) -> pygfx .WorldObject :
106
148
"""Associated pygfx WorldObject. Always returns a proxy, real object cannot be accessed directly."""
@@ -110,6 +152,8 @@ def world_object(self) -> pygfx.WorldObject:
110
152
def _set_world_object (self , wo : pygfx .WorldObject ):
111
153
WORLD_OBJECTS [self ._fpl_address ] = wo
112
154
155
+ self ._rotation = Rotation (self .world_object .world .rotation [:])
156
+
113
157
def detach_feature (self , feature : str ):
114
158
raise NotImplementedError
115
159
@@ -203,7 +247,8 @@ def _handle_event(self, callback, event: pygfx.Event):
203
247
# for feature events
204
248
event ._target = self .world_object
205
249
206
- callback (event )
250
+ with log_exception (f"Error during handling { event .type } event" ):
251
+ callback (event )
207
252
208
253
def remove_event_handler (self , callback , * types ):
209
254
# remove from our record first
@@ -315,6 +360,77 @@ def rotate(self, alpha: float, axis: Literal["x", "y", "z"] = "y"):
315
360
class PositionsGraphic (Graphic ):
316
361
"""Base class for LineGraphic and ScatterGraphic"""
317
362
363
+ @property
364
+ def data (self ) -> VertexPositions :
365
+ """Get or set the vertex positions data"""
366
+ return self ._data
367
+
368
+ @data .setter
369
+ def data (self , value ):
370
+ self ._data [:] = value
371
+
372
+ @property
373
+ def colors (self ) -> VertexColors | pygfx .Color :
374
+ """Get or set the colors data"""
375
+ if isinstance (self ._colors , VertexColors ):
376
+ return self ._colors
377
+
378
+ elif isinstance (self ._colors , UniformColor ):
379
+ return self ._colors .value
380
+
381
+ @colors .setter
382
+ def colors (self , value ):
383
+ if isinstance (self ._colors , VertexColors ):
384
+ self ._colors [:] = value
385
+
386
+ elif isinstance (self ._colors , UniformColor ):
387
+ self ._colors .set_value (self , value )
388
+
389
+ def __init__ (
390
+ self ,
391
+ data : Any ,
392
+ colors : str | np .ndarray | tuple [float ] | list [float ] | list [str ] = "w" ,
393
+ uniform_colors : bool = False ,
394
+ alpha : float = 1.0 ,
395
+ cmap : str = None ,
396
+ cmap_values : np .ndarray = None ,
397
+ isolated_buffer : bool = True ,
398
+ * args ,
399
+ ** kwargs ,
400
+ ):
401
+ self ._data = VertexPositions (data , isolated_buffer = isolated_buffer )
402
+
403
+ if cmap is not None :
404
+ if uniform_colors :
405
+ raise TypeError (
406
+ "Cannot use cmap if uniform_colors=True"
407
+ )
408
+
409
+ n_datapoints = self ._data .value .shape [0 ]
410
+
411
+ colors = parse_cmap_values (
412
+ n_colors = n_datapoints , cmap_name = cmap , cmap_values = cmap_values
413
+ )
414
+
415
+ if isinstance (colors , VertexColors ):
416
+ if uniform_colors :
417
+ raise TypeError (
418
+ "Cannot use vertex colors from existing instance if uniform_colors=True"
419
+ )
420
+ self ._colors = colors
421
+ self ._colors ._shared += 1
422
+ else :
423
+ if uniform_colors :
424
+ self ._colors = UniformColor (colors )
425
+ else :
426
+ self ._colors = VertexColors (
427
+ colors ,
428
+ n_colors = self ._data .value .shape [0 ],
429
+ alpha = alpha ,
430
+ )
431
+
432
+ super ().__init__ (* args , ** kwargs )
433
+
318
434
def detach_feature (self , feature : str ):
319
435
if not isinstance (feature , str ):
320
436
raise TypeError
@@ -323,7 +439,7 @@ def detach_feature(self, feature: str):
323
439
if f .shared == 0 :
324
440
raise BufferError ("Cannot detach an independent buffer" )
325
441
326
- if feature == "colors" :
442
+ if feature == "colors" and isinstance ( feature , VertexColors ) :
327
443
self ._colors ._buffer = pygfx .Buffer (self ._colors .value .copy ())
328
444
self .world_object .geometry .colors = self ._colors .buffer
329
445
self ._colors ._shared -= 1
@@ -338,16 +454,16 @@ def detach_feature(self, feature: str):
338
454
self .world_object .geometry .positions = self ._sizes .buffer
339
455
self ._sizes ._shared -= 1
340
456
341
- def attach_feature (self , feature : PointsDataFeature | ColorFeature | PointsSizesFeature ):
342
- if isinstance (feature , PointsDataFeature ):
457
+ def attach_feature (self , feature : VertexPositions | VertexColors | PointsSizesFeature ):
458
+ if isinstance (feature , VertexPositions ):
343
459
# TODO: check if this causes a memory leak
344
460
self ._data ._shared -= 1
345
461
346
462
self ._data = feature
347
463
self ._data ._shared += 1
348
464
self .world_object .geometry .positions = self ._data .buffer
349
465
350
- elif isinstance (feature , ColorFeature ):
466
+ elif isinstance (feature , VertexColors ):
351
467
self ._colors ._shared -= 1
352
468
353
469
self ._colors = feature
0 commit comments