|
5 | 5 | import numpy as np
|
6 | 6 |
|
7 | 7 | from traitlets import (
|
8 |
| - Bool, Dict, Unicode, List, Instance, CFloat, Tuple, Union, default, validate |
| 8 | + Bool, Dict, Unicode, List, Instance, CFloat, Tuple, TraitError, Union, default, validate |
9 | 9 | )
|
10 | 10 | from traittypes import Array
|
11 | 11 | from ipywidgets import (
|
@@ -70,6 +70,15 @@ def __init__(self, name, components, **kwargs):
|
70 | 70 | """Create a new Data instance given its name and components."""
|
71 | 71 | super(Data, self).__init__(name=name, components=components, **kwargs)
|
72 | 72 |
|
| 73 | + @property |
| 74 | + def dim(self): |
| 75 | + """Get the data dimension.""" |
| 76 | + return len(self.components) |
| 77 | + |
| 78 | + def as_input(self): |
| 79 | + """Internal method of ipygany. Do not use this.""" |
| 80 | + return [(self.name, comp.name) for comp in self.components] |
| 81 | + |
73 | 82 | def __getitem__(self, key):
|
74 | 83 | """Get a component by name or index."""
|
75 | 84 | if isinstance(key, str):
|
@@ -97,13 +106,10 @@ def _grid_data_to_data_widget(grid_data):
|
97 | 106 | """Turn a vtk grid into Data widgets."""
|
98 | 107 | data = []
|
99 | 108 | for key, value in grid_data.items():
|
100 |
| - d = Data( |
101 |
| - name=key, |
102 |
| - components=[ |
103 |
| - Component(name=comp_name, array=comp['array']) |
104 |
| - for comp_name, comp in value.items() |
105 |
| - ] |
106 |
| - ) |
| 109 | + d = Data(key, [ |
| 110 | + Component(comp_name, comp['array']) |
| 111 | + for comp_name, comp in value.items() |
| 112 | + ]) |
107 | 113 | data.append(d)
|
108 | 114 |
|
109 | 115 | return data
|
@@ -135,20 +141,32 @@ class Block(_GanyWidgetBase):
|
135 | 141 |
|
136 | 142 | def __getitem__(self, key):
|
137 | 143 | """Get a component by name or index."""
|
138 |
| - if not isinstance(key, tuple) or len(key) != 2: |
139 |
| - raise KeyError('You can only access data by (data_name, component_name) tuple.') |
| 144 | + if not (isinstance(key, str) or (isinstance(key, tuple) and len(key) == 2)): |
| 145 | + raise KeyError('You can only access data by (data_name, component_name) tuple or data_name string.') |
| 146 | + |
| 147 | + # This prevents failures when this method is called in the constructor |
| 148 | + # self.data is not yet initialized |
| 149 | + actual_data = self.data if len(self.data) else (self.parent.data if self.parent else []) |
140 | 150 |
|
| 151 | + # If the key is a string, we assume it's the data name |
| 152 | + if isinstance(key, str): |
| 153 | + for data in actual_data: |
| 154 | + if data.name == key: |
| 155 | + return data |
| 156 | + raise KeyError('Data {} not found.'.format(key)) |
| 157 | + |
| 158 | + # Otherwise it's a (data name, component name) tuple |
141 | 159 | data_name = key[0]
|
142 | 160 | component_name = key[1]
|
143 | 161 |
|
144 | 162 | if isinstance(data_name, str):
|
145 |
| - for data in self.data: |
| 163 | + for data in actual_data: |
146 | 164 | if data.name == data_name:
|
147 | 165 | return data[component_name]
|
148 | 166 | raise KeyError('Data {} not found.'.format(data_name))
|
149 | 167 |
|
150 | 168 | if isinstance(data_name, int):
|
151 |
| - return self.data[data_name][component_name] |
| 169 | + return actual_data[data_name][component_name] |
152 | 170 |
|
153 | 171 | raise KeyError('Invalid key {}.'.format(key))
|
154 | 172 |
|
@@ -397,110 +415,204 @@ class Effect(Block):
|
397 | 415 |
|
398 | 416 | _model_name = Unicode('EffectModel').tag(sync=True)
|
399 | 417 |
|
| 418 | + input = Union((Tuple(), Unicode(), CFloat())).tag(sync=True) |
| 419 | + |
400 | 420 | parent = Instance(Block).tag(sync=True, **widget_serialization)
|
401 | 421 |
|
402 | 422 | def __init__(self, parent, **kwargs):
|
403 | 423 | """Create an Effect on the given Mesh or Effect output."""
|
404 | 424 | super(Effect, self).__init__(parent=parent, **kwargs)
|
405 | 425 |
|
| 426 | + @property |
| 427 | + def data(self): |
| 428 | + """Get data.""" |
| 429 | + return self.parent.data |
| 430 | + |
| 431 | + @property |
| 432 | + def input_dim(self): |
| 433 | + """Input dimension.""" |
| 434 | + return 0 |
| 435 | + |
| 436 | + @default('input') |
| 437 | + def _default_input(self): |
| 438 | + if not len(self.data): |
| 439 | + if self.input_dim == 0 or self.input_dim == 1: |
| 440 | + return 0 |
| 441 | + return tuple(0 for _ in range(self.input_dim)) |
| 442 | + |
| 443 | + return self._validate_input_impl(self.data[0].name) |
| 444 | + |
| 445 | + @validate('input') |
| 446 | + def _validate_input(self, proposal): |
| 447 | + return self._validate_input_impl(proposal['value']) |
| 448 | + |
| 449 | + def _validate_input_impl(self, value): |
| 450 | + # Input is a data name |
| 451 | + if isinstance(value, str): |
| 452 | + input_data = self[value] |
| 453 | + |
| 454 | + # Simply use this data |
| 455 | + if input_data.dim == self.input_dim: |
| 456 | + return input_data.name |
| 457 | + |
| 458 | + # Take all the components and fill in with zeros |
| 459 | + if input_data.dim < self.input_dim: |
| 460 | + chosen_input = input_data.as_input() |
| 461 | + |
| 462 | + while len(chosen_input) != self.input_dim: |
| 463 | + chosen_input.append(0.) |
| 464 | + |
| 465 | + return chosen_input |
| 466 | + |
| 467 | + # input_data.dim > self.input_dim, take only the first self.input_dim components |
| 468 | + return input_data.as_input()[:self.input_dim] |
| 469 | + |
| 470 | + # Input as a tuple |
| 471 | + if isinstance(value, (tuple, list)): |
| 472 | + if self.input_dim == 1 and len(value) == 2: |
| 473 | + return self._validate_input_component(value) |
| 474 | + |
| 475 | + if len(value) != self.input_dim: |
| 476 | + raise TraitError('input is of dimension {} but expected input dimension is {}'.format(len(value), self.input_dim)) |
| 477 | + |
| 478 | + # Check all elements in the tuple |
| 479 | + return tuple(self._validate_input_component(el) for el in value) |
| 480 | + |
| 481 | + # Input is a number |
| 482 | + if isinstance(value, (float, int)) and self.input_dim == 1: |
| 483 | + return value |
| 484 | + |
| 485 | + raise TraitError('{} is not a valid input'.format(value)) |
| 486 | + |
| 487 | + def _validate_input_component(self, value): |
| 488 | + # Component selection by name |
| 489 | + if isinstance(value, (tuple, list)): |
| 490 | + if len(value) != 2: |
| 491 | + raise TraitError('{} is not a valid component'.format(value)) |
| 492 | + |
| 493 | + try: |
| 494 | + self[value[0], value[1]] |
| 495 | + except KeyError: |
| 496 | + raise TraitError('{} is not a valid component'.format(value)) |
| 497 | + |
| 498 | + return value |
| 499 | + |
| 500 | + # Data selection by name |
| 501 | + if isinstance(value, str): |
| 502 | + try: |
| 503 | + data = self[value] |
| 504 | + except KeyError: |
| 505 | + raise TraitError('{} is not a valid data'.format(value)) |
| 506 | + |
| 507 | + if data.dim != 1: |
| 508 | + raise TraitError('{} is ambiguous, please select a component'.format(value)) |
| 509 | + |
| 510 | + return (data.name, data.components[0].name) |
| 511 | + |
| 512 | + if isinstance(value, (float, int)): |
| 513 | + return value |
| 514 | + |
| 515 | + raise TraitError('{} is not a valid input'.format(value)) |
| 516 | + |
406 | 517 |
|
407 | 518 | class Warp(Effect):
|
408 | 519 | """A warp effect to another block."""
|
409 | 520 |
|
410 | 521 | _model_name = Unicode('WarpModel').tag(sync=True)
|
411 | 522 |
|
412 |
| - input = Union((Tuple(trait=Unicode, minlen=2, maxlen=2), Unicode(), CFloat(0.))).tag(sync=True) |
413 |
| - |
414 | 523 | offset = Union((Tuple(trait=Unicode, minlen=3, maxlen=3), CFloat(0.)), default_value=0.).tag(sync=True)
|
415 | 524 | factor = Union((Tuple(trait=Unicode, minlen=3, maxlen=3), CFloat(0.)), default_value=1.).tag(sync=True)
|
416 | 525 |
|
417 |
| - @default('input') |
418 |
| - def _default_input(self): |
419 |
| - return self.parent.data[0].name |
| 526 | + @property |
| 527 | + def input_dim(self): |
| 528 | + """Input dimension.""" |
| 529 | + return 3 |
420 | 530 |
|
421 | 531 |
|
422 | 532 | class Alpha(Effect):
|
423 | 533 | """An transparency effect to another block."""
|
424 | 534 |
|
425 | 535 | _model_name = Unicode('AlphaModel').tag(sync=True)
|
426 | 536 |
|
427 |
| - input = Union((Tuple(trait=Unicode, minlen=2, maxlen=2), Unicode(), CFloat(0.))).tag(sync=True) |
428 |
| - |
429 | 537 | @default('input')
|
430 | 538 | def _default_input(self):
|
431 | 539 | return 0.7
|
432 | 540 |
|
| 541 | + @property |
| 542 | + def input_dim(self): |
| 543 | + """Input dimension.""" |
| 544 | + return 1 |
| 545 | + |
433 | 546 |
|
434 | 547 | class RGB(Effect):
|
435 | 548 | """A color effect to another block."""
|
436 | 549 |
|
437 | 550 | _model_name = Unicode('RGBModel').tag(sync=True)
|
438 | 551 |
|
439 |
| - input = Union((Tuple(trait=Unicode, minlen=2, maxlen=2), Unicode(), CFloat(0.))).tag(sync=True) |
| 552 | + @property |
| 553 | + def input_dim(self): |
| 554 | + """Input dimension.""" |
| 555 | + return 3 |
440 | 556 |
|
441 | 557 |
|
442 | 558 | class IsoColor(Effect):
|
443 | 559 | """An IsoColor effect to another block."""
|
444 | 560 |
|
445 | 561 | _model_name = Unicode('IsoColorModel').tag(sync=True)
|
446 | 562 |
|
447 |
| - input = Union((Tuple(trait=Unicode, minlen=2, maxlen=2), Unicode(), CFloat(0.))).tag(sync=True) |
448 |
| - |
449 | 563 | min = CFloat(0.).tag(sync=True)
|
450 | 564 | max = CFloat(0.).tag(sync=True)
|
451 | 565 |
|
452 |
| - @default('input') |
453 |
| - def _default_input(self): |
454 |
| - return self.parent.data[0].name |
| 566 | + @property |
| 567 | + def input_dim(self): |
| 568 | + """Input dimension.""" |
| 569 | + return 1 |
455 | 570 |
|
456 | 571 |
|
457 | 572 | class IsoSurface(Effect):
|
458 | 573 | """An IsoSurface effect to another block."""
|
459 | 574 |
|
460 | 575 | _model_name = Unicode('IsoSurfaceModel').tag(sync=True)
|
461 | 576 |
|
462 |
| - input = Union((Tuple(trait=Unicode, minlen=2, maxlen=2), Unicode(), CFloat(0.))).tag(sync=True) |
463 |
| - |
464 | 577 | value = CFloat(0.).tag(sync=True)
|
465 | 578 | dynamic = Bool(False).tag(sync=True)
|
466 | 579 |
|
467 |
| - @default('input') |
468 |
| - def _default_input(self): |
469 |
| - return self.parent.data[0].name |
| 580 | + @property |
| 581 | + def input_dim(self): |
| 582 | + """Input dimension.""" |
| 583 | + return 1 |
470 | 584 |
|
471 | 585 |
|
472 | 586 | class Threshold(Effect):
|
473 | 587 | """An Threshold effect to another block."""
|
474 | 588 |
|
475 | 589 | _model_name = Unicode('ThresholdModel').tag(sync=True)
|
476 | 590 |
|
477 |
| - input = Union((Tuple(trait=Unicode, minlen=2, maxlen=2), Unicode(), CFloat(0.))).tag(sync=True) |
478 |
| - |
479 | 591 | min = CFloat(0.).tag(sync=True)
|
480 | 592 | max = CFloat(0.).tag(sync=True)
|
481 | 593 | dynamic = Bool(False).tag(sync=True)
|
482 | 594 | inclusive = Bool(True).tag(sync=True)
|
483 | 595 |
|
484 |
| - @default('input') |
485 |
| - def _default_input(self): |
486 |
| - return self.parent.data[0].name |
| 596 | + @property |
| 597 | + def input_dim(self): |
| 598 | + """Input dimension.""" |
| 599 | + return 1 |
487 | 600 |
|
488 | 601 |
|
489 | 602 | class UnderWater(Effect):
|
490 | 603 | """An nice UnderWater effect to another block."""
|
491 | 604 |
|
492 | 605 | _model_name = Unicode('UnderWaterModel').tag(sync=True)
|
493 | 606 |
|
494 |
| - input = Union((Tuple(trait=Unicode, minlen=2, maxlen=2), Unicode(), CFloat(0.))).tag(sync=True) |
495 |
| - |
496 | 607 | default_color = Color('#F2FFD2').tag(sync=True)
|
497 | 608 | texture = Instance(Image, allow_none=True, default_value=None).tag(sync=True, **widget_serialization)
|
498 | 609 | texture_scale = CFloat(2.).tag(sync=True)
|
499 | 610 | texture_position = Tuple(minlen=2, maxlen=2, default_value=(1., 1., 0.)).tag(sync=True)
|
500 | 611 |
|
501 |
| - @default('input') |
502 |
| - def _default_input(self): |
503 |
| - return self.parent.data[0].name |
| 612 | + @property |
| 613 | + def input_dim(self): |
| 614 | + """Input dimension.""" |
| 615 | + return 1 |
504 | 616 |
|
505 | 617 |
|
506 | 618 | class Water(Effect):
|
|
0 commit comments