Skip to content

Commit

Permalink
Merge 40b83a2 into a5b8cb9
Browse files Browse the repository at this point in the history
  • Loading branch information
jwiggins committed Mar 4, 2014
2 parents a5b8cb9 + 40b83a2 commit c051c35
Show file tree
Hide file tree
Showing 7 changed files with 667 additions and 0 deletions.
Empty file added enable/gadgets/ctf/__init__.py
Empty file.
124 changes: 124 additions & 0 deletions enable/gadgets/ctf/editor_tools.py
@@ -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
174 changes: 174 additions & 0 deletions enable/gadgets/ctf/menu_tool.py
@@ -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',
),
)
76 changes: 76 additions & 0 deletions enable/gadgets/ctf/piecewise.py
@@ -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

0 comments on commit c051c35

Please sign in to comment.