Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
667 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
from enable.gadgets.ctf.piecewise import PiecewiseFunction | ||
from enable.gadgets.ctf.utils import ( | ||
FunctionUIAdapter, AlphaFunctionUIAdapter, ColorFunctionUIAdapter, | ||
clip_to_unit | ||
) | ||
from enable.tools.api import ValueDragTool | ||
from traits.api import Event, Instance, Tuple, Type | ||
|
||
|
||
class ValueMapper(object): | ||
""" A simple mapper for ValueDragTool objects. | ||
""" | ||
def __init__(self, obj, attr_name): | ||
self.obj = obj | ||
self.attr_name = attr_name | ||
|
||
def map_data(self, screen): | ||
size = getattr(self.obj, self.attr_name) | ||
return clip_to_unit(screen / size) | ||
|
||
def map_screen(self, data): | ||
size = getattr(self.obj, self.attr_name) | ||
return clip_to_unit(data) * size | ||
|
||
|
||
class FunctionEditorTool(ValueDragTool): | ||
""" A value drag tool for editing a PiecewiseFunction. | ||
""" | ||
|
||
# The function being edited | ||
function = Instance(PiecewiseFunction) | ||
|
||
# An event to trigger when the function is updated | ||
function_updated = Event | ||
|
||
# A tuple containing the index and starting value of the item being edited | ||
edited_item = Tuple | ||
|
||
# A factory for the FunctionUIAdapter to use | ||
ui_adapter_klass = Type | ||
|
||
# The helper object for screen <=> function translation | ||
_ui_adapter = Instance(FunctionUIAdapter) | ||
|
||
#------------------------------------------------------------------------ | ||
# Traits handlers | ||
#------------------------------------------------------------------------ | ||
|
||
def _x_mapper_default(self): | ||
return ValueMapper(self.component, 'width') | ||
|
||
def _y_mapper_default(self): | ||
return ValueMapper(self.component, 'height') | ||
|
||
def __ui_adapter_default(self): | ||
return self.ui_adapter_klass(component=self.component, | ||
function=self.function) | ||
|
||
#------------------------------------------------------------------------ | ||
# ValueDragTool methods | ||
#------------------------------------------------------------------------ | ||
|
||
def is_draggable(self, x, y): | ||
""" Returns whether the (x,y) position is in a region that is OK to | ||
drag. | ||
Used by the tool to determine when to start a drag. | ||
""" | ||
index = self._ui_adapter.function_index_at_position(x, y) | ||
if index is not None: | ||
self.edited_item = (index, self.function.value_at(index)) | ||
return True | ||
|
||
return False | ||
|
||
def get_value(self): | ||
""" Return the current value that is being modified. """ | ||
return self.edited_item | ||
|
||
|
||
class AlphaFunctionEditorTool(FunctionEditorTool): | ||
""" A FuctionEditorTool for an opacity function. | ||
""" | ||
ui_adapter_klass = AlphaFunctionUIAdapter | ||
|
||
def set_delta(self, value, delta_x, delta_y): | ||
""" Set the value that is being modified | ||
""" | ||
index, start_value = value | ||
if index == 0: | ||
x_limits = (0.0, 0.0) | ||
elif index == (self.function.size() - 1): | ||
x_limits = (start_value[0], start_value[0]) | ||
else: | ||
value_at = self.function.value_at | ||
x_limits = tuple([value_at(i)[0] for i in (index-1, index+1)]) | ||
|
||
x_val = min(max(x_limits[0], start_value[0] + delta_x), x_limits[1]) | ||
new_value = (x_val, start_value[1] + delta_y) | ||
self.function.update(index, new_value) | ||
self.function_updated = True | ||
|
||
|
||
class ColorFunctionEditorTool(FunctionEditorTool): | ||
""" A FuctionEditorTool for a color function. | ||
""" | ||
ui_adapter_klass = ColorFunctionUIAdapter | ||
|
||
def set_delta(self, value, delta_x, delta_y): | ||
""" Set the value that is being modified | ||
""" | ||
index, start_value = value | ||
if index == 0: | ||
x_limits = (0.0, 0.0) | ||
elif index == (self.function.size() - 1): | ||
x_limits = (start_value[0], start_value[0]) | ||
else: | ||
value_at = self.function.value_at | ||
x_limits = tuple([value_at(i)[0] for i in (index-1, index+1)]) | ||
|
||
x_pos = min(max(x_limits[0], start_value[0] + delta_x), x_limits[1]) | ||
new_value = (x_pos,) + start_value[1:] | ||
self.function.update(index, new_value) | ||
self.function_updated = True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
import json | ||
|
||
from enable.component import Component | ||
from enable.gadgets.ctf.piecewise import PiecewiseFunction, verify_values | ||
from enable.gadgets.ctf.utils import ( | ||
FunctionUIAdapter, AlphaFunctionUIAdapter, ColorFunctionUIAdapter | ||
) | ||
from enable.tools.pyface.context_menu_tool import ContextMenuTool | ||
from pyface.action.api import Action, Group, MenuManager, Separator | ||
from traits.api import Callable, Instance, List, Type | ||
|
||
|
||
class BaseCtfAction(Action): | ||
component = Instance(Component) | ||
function = Instance(PiecewiseFunction) | ||
ui_adaptor = Instance(FunctionUIAdapter) | ||
ui_adaptor_klass = Type | ||
|
||
def _ui_adaptor_default(self): | ||
return self.ui_adaptor_klass(component=self.component, | ||
function=self.function) | ||
|
||
def _get_relative_event_position(self, event): | ||
return self.ui_adaptor.screen_to_function((event.x, event.y)) | ||
|
||
|
||
class AddColorAction(BaseCtfAction): | ||
name = 'Add Color...' | ||
ui_adaptor_klass = ColorFunctionUIAdapter | ||
|
||
# A callable which prompts the user for a color | ||
prompt_color = Callable | ||
|
||
def perform(self, event): | ||
pos = self._get_relative_event_position(event.enable_event) | ||
color_val = (pos[0],) + self.prompt_color() | ||
self.component.add_function_node(self.function, color_val) | ||
|
||
|
||
class AddOpacityAction(BaseCtfAction): | ||
name = 'Add Opacity' | ||
ui_adaptor_klass = AlphaFunctionUIAdapter | ||
|
||
def perform(self, event): | ||
pos = self._get_relative_event_position(event.enable_event) | ||
self.component.add_function_node(self.function, pos) | ||
|
||
|
||
class EditColorAction(BaseCtfAction): | ||
name = 'Edit Color...' | ||
ui_adaptor_klass = ColorFunctionUIAdapter | ||
|
||
# A callable which prompts the user for a color | ||
prompt_color = Callable | ||
|
||
def perform(self, event): | ||
mouse_pos = (event.enable_event.x, event.enable_event.y) | ||
index = self.ui_adaptor.function_index_at_position(*mouse_pos) | ||
if index is not None: | ||
color_val = self.function.value_at(index) | ||
new_value = (color_val[0],) + self.prompt_color(color_val[1:]) | ||
self.component.edit_function_node(self.function, index, new_value) | ||
|
||
|
||
class RemoveNodeAction(Action): | ||
""" Removes a node from one of the functions. | ||
""" | ||
name = 'Remove Node' | ||
component = Instance(Component) | ||
alpha_func = Instance(PiecewiseFunction) | ||
color_func = Instance(PiecewiseFunction) | ||
ui_adaptors = List(Instance(FunctionUIAdapter)) | ||
|
||
def _ui_adaptors_default(self): | ||
# Alpha function first so that it will take precedence in removals. | ||
return [AlphaFunctionUIAdapter(component=self.component, | ||
function=self.alpha_func), | ||
ColorFunctionUIAdapter(component=self.component, | ||
function=self.color_func)] | ||
|
||
def perform(self, event): | ||
mouse_pos = (event.enable_event.x, event.enable_event.y) | ||
for adaptor in self.ui_adaptors: | ||
index = adaptor.function_index_at_position(*mouse_pos) | ||
if index is not None: | ||
self.component.remove_function_node(adaptor.function, index) | ||
return | ||
|
||
|
||
class LoadFunctionAction(Action): | ||
name = 'Load Function...' | ||
component = Instance(Component) | ||
alpha_func = Instance(PiecewiseFunction) | ||
color_func = Instance(PiecewiseFunction) | ||
|
||
# A callable which prompts the user for a filename | ||
prompt_filename = Callable | ||
|
||
def perform(self, event): | ||
filename = self.prompt_filename(action='open') | ||
with open(filename, 'r') as fp: | ||
loaded_data = json.load(fp) | ||
|
||
# Sanity check | ||
if not self._verify_loaded_data(loaded_data): | ||
return | ||
|
||
parts = (('alpha', self.alpha_func), ('color', self.color_func)) | ||
for name, func in parts: | ||
func.clear() | ||
for value in loaded_data[name]: | ||
func.insert(tuple(value)) | ||
self.component.update_function() | ||
|
||
def _verify_loaded_data(self, data): | ||
keys = ('alpha', 'color') | ||
has_values = all(k in data for k in keys) | ||
return has_values and all(verify_values(data[k]) for k in keys) | ||
|
||
|
||
class SaveFunctionAction(Action): | ||
name = 'Save Function...' | ||
component = Instance(Component) | ||
alpha_func = Instance(PiecewiseFunction) | ||
color_func = Instance(PiecewiseFunction) | ||
|
||
# A callable which prompts the user for a filename | ||
prompt_filename = Callable | ||
|
||
def perform(self, event): | ||
filename = self.prompt_filename(action='save') | ||
function = {'alpha': self.alpha_func.values(), | ||
'color': self.color_func.values()} | ||
with open(filename, 'w') as fp: | ||
json.dump(function, fp, indent=1) | ||
|
||
|
||
class FunctionMenuTool(ContextMenuTool): | ||
def _menu_manager_default(self): | ||
component = self.component | ||
alpha_func = component.opacities | ||
color_func = component.colors | ||
prompt_color = component.prompt_color_selection | ||
prompt_filename = component.prompt_file_selection | ||
return MenuManager( | ||
Group( | ||
AddColorAction(component=component, function=color_func, | ||
prompt_color=prompt_color), | ||
AddOpacityAction(component=component, function=alpha_func), | ||
id='AddGroup', | ||
), | ||
Separator(), | ||
Group( | ||
EditColorAction(component=component, function=color_func, | ||
prompt_color=prompt_color), | ||
id='EditGroup', | ||
), | ||
Separator(), | ||
Group( | ||
RemoveNodeAction(component=component, alpha_func=alpha_func, | ||
color_func=color_func), | ||
id='RemoveGroup', | ||
), | ||
Separator(), | ||
Group( | ||
LoadFunctionAction(component=component, alpha_func=alpha_func, | ||
color_func=color_func, | ||
prompt_filename=prompt_filename), | ||
SaveFunctionAction(component=component, alpha_func=alpha_func, | ||
color_func=color_func, | ||
prompt_filename=prompt_filename), | ||
id='IOGroup', | ||
), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
from bisect import bisect | ||
from types import FloatType | ||
|
||
|
||
class PiecewiseFunction(object): | ||
""" A piecewise linear function. | ||
""" | ||
def __init__(self, key=None): | ||
self.keyfunc = key or (lambda x: id(x)) | ||
self.clear() | ||
|
||
def clear(self): | ||
self._keys = [] | ||
self._values = [] | ||
|
||
def insert(self, value): | ||
key = self.keyfunc(value) | ||
index = bisect(self._keys, key) | ||
self._keys.insert(index, key) | ||
self._values.insert(index, value) | ||
|
||
def items(self): | ||
return self._values | ||
|
||
def neighbor_indices(self, key_value): | ||
index = bisect(self._keys, key_value) | ||
value = self._keys[index] | ||
if key_value < value: | ||
return (max(0, index-1), index) | ||
return (index, min(index+1, self.size()-1)) | ||
|
||
def remove(self, index): | ||
del self._keys[index] | ||
del self._values[index] | ||
|
||
def size(self): | ||
return len(self._values) | ||
|
||
def update(self, index, value): | ||
key = self.keyfunc(value) | ||
self._keys[index] = key | ||
self._values[index] = value | ||
|
||
def value_at(self, index): | ||
return self._values[index] | ||
|
||
def values(self): | ||
# Return a copy | ||
return self._values[:] | ||
|
||
|
||
def verify_values(function_values): | ||
"""Make sure a sequence of values are valid function values. | ||
- Function values must be sequences of length 2 or greater | ||
- All values in a function must have the same length | ||
- All subvalues in a value sequence must be floating point numbers | ||
between 0 and 1, inclusive. | ||
""" | ||
sub_size = -1 | ||
for value in function_values: | ||
try: | ||
if sub_size < 0: | ||
sub_size = len(value) | ||
if sub_size < 2: | ||
return False | ||
elif len(value) != sub_size: | ||
return False | ||
for sub_val in value: | ||
if not isinstance(sub_val, FloatType): | ||
return False | ||
if not (0.0 <= sub_val <= 1.0): | ||
return False | ||
except TypeError: | ||
return False | ||
|
||
return True |
Oops, something went wrong.