diff --git a/Packages/vcs/Lib/configurator.py b/Packages/vcs/Lib/configurator.py index 7bcd1e56ff..1615bc64b3 100644 --- a/Packages/vcs/Lib/configurator.py +++ b/Packages/vcs/Lib/configurator.py @@ -2,8 +2,10 @@ import datetime import editors import vtk_ui -import os, sys +import os +import sys import vtk +from vcs2vtk import vtkIterate CREATING_FILL = "fill" CREATING_LINE = "line" @@ -19,128 +21,61 @@ import copy -def sync_template(src, target): - target.orientation=src.orientation - target.file=copy.copy(src.file) - target.function=copy.copy(src.function) - target.logicalmask=copy.copy(src.logicalmask) - target.transformation=copy.copy(src.transformation) - target.source=copy.copy(src.source) - target.dataname=copy.copy(src.dataname) - target.title=copy.copy(src.title) - target.units=copy.copy(src.units) - target.crdate=copy.copy(src.crdate) - target.crtime=copy.copy(src.crtime) - target.comment1=copy.copy(src.comment1) - target.comment2=copy.copy(src.comment2) - target.comment3=copy.copy(src.comment3) - target.comment4=copy.copy(src.comment4) - target.xname=copy.copy(src.xname) - target.yname=copy.copy(src.yname) - target.zname=copy.copy(src.zname) - target.tname=copy.copy(src.tname) - target.xunits=copy.copy(src.xunits) - target.yunits=copy.copy(src.yunits) - target.zunits=copy.copy(src.zunits) - target.tunits=copy.copy(src.tunits) - target.xvalue=copy.copy(src.xvalue) - target.yvalue=copy.copy(src.yvalue) - target.zvalue=copy.copy(src.zvalue) - target.tvalue=copy.copy(src.tvalue) - target.mean=copy.copy(src.mean) - target.min=copy.copy(src.min) - target.max=copy.copy(src.max) - target.xtic1=copy.copy(src.xtic1) - target.xtic2=copy.copy(src.xtic2) - target.xmintic1=copy.copy(src.xmintic1) - target.xmintic2=copy.copy(src.xmintic2) - target.ytic1=copy.copy(src.ytic1) - target.ytic2=copy.copy(src.ytic2) - target.ymintic1=copy.copy(src.ymintic1) - target.ymintic2=copy.copy(src.ymintic2) - target.xlabel1=copy.copy(src.xlabel1) - target.xlabel2=copy.copy(src.xlabel2) - target.ylabel1=copy.copy(src.ylabel1) - target.ylabel2=copy.copy(src.ylabel2) - target.box1=copy.copy(src.box1) - target.box2=copy.copy(src.box2) - target.box3=copy.copy(src.box3) - target.box4=copy.copy(src.box4) - target.line1=copy.copy(src.line1) - target.line2=copy.copy(src.line2) - target.line3=copy.copy(src.line3) - target.line4=copy.copy(src.line4) - target.legend=copy.copy(src.legend) - target.data=copy.copy(src.data) - -def array_strings(template, array): - attrs = [ - "file", - "function", - "logicalmask", - "transformation", - "source", - "dataname", - "title", - "units", - "crdate", - "crtime", - "comment1", - "comment2", - "comment3", - "comment4", - "xname", - "yname", - "zname", - "tname", - "xunits", - "yunits", - "zunits", - "tunits", - "xvalue", - "yvalue", - "zvalue", - "tvalue", - "mean", - "min", - "max", - "xtic1", - "xtic2", - "xmintic1", - "xmintic2", - "ytic1", - "ytic2", - "ymintic1", - "ymintic2", - "xlabel1", - "xlabel2", - "ylabel1", - "ylabel2", - "box1", - "box2", - "box3", - "box4", - "line1", - "line2", - "line3", - "line4", - "legend", - "data", - ] - - template = t(template) - - strings = {} - for attr in attrs: - try: - attribute = getattr(template, attr) - if is_label(attribute): - strings[attr] = editors.label.get_label_text(attribute, array) - except AttributeError: - pass +def sync_template(src, target): + target.orientation = src.orientation + target.file = copy.copy(src.file) + target.function = copy.copy(src.function) + target.logicalmask = copy.copy(src.logicalmask) + target.transformation = copy.copy(src.transformation) + target.source = copy.copy(src.source) + target.dataname = copy.copy(src.dataname) + target.title = copy.copy(src.title) + target.units = copy.copy(src.units) + target.crdate = copy.copy(src.crdate) + target.crtime = copy.copy(src.crtime) + target.comment1 = copy.copy(src.comment1) + target.comment2 = copy.copy(src.comment2) + target.comment3 = copy.copy(src.comment3) + target.comment4 = copy.copy(src.comment4) + target.xname = copy.copy(src.xname) + target.yname = copy.copy(src.yname) + target.zname = copy.copy(src.zname) + target.tname = copy.copy(src.tname) + target.xunits = copy.copy(src.xunits) + target.yunits = copy.copy(src.yunits) + target.zunits = copy.copy(src.zunits) + target.tunits = copy.copy(src.tunits) + target.xvalue = copy.copy(src.xvalue) + target.yvalue = copy.copy(src.yvalue) + target.zvalue = copy.copy(src.zvalue) + target.tvalue = copy.copy(src.tvalue) + target.mean = copy.copy(src.mean) + target.min = copy.copy(src.min) + target.max = copy.copy(src.max) + target.xtic1 = copy.copy(src.xtic1) + target.xtic2 = copy.copy(src.xtic2) + target.xmintic1 = copy.copy(src.xmintic1) + target.xmintic2 = copy.copy(src.xmintic2) + target.ytic1 = copy.copy(src.ytic1) + target.ytic2 = copy.copy(src.ytic2) + target.ymintic1 = copy.copy(src.ymintic1) + target.ymintic2 = copy.copy(src.ymintic2) + target.xlabel1 = copy.copy(src.xlabel1) + target.xlabel2 = copy.copy(src.xlabel2) + target.ylabel1 = copy.copy(src.ylabel1) + target.ylabel2 = copy.copy(src.ylabel2) + target.box1 = copy.copy(src.box1) + target.box2 = copy.copy(src.box2) + target.box3 = copy.copy(src.box3) + target.box4 = copy.copy(src.box4) + target.line1 = copy.copy(src.line1) + target.line2 = copy.copy(src.line2) + target.line3 = copy.copy(src.line3) + target.line4 = copy.copy(src.line4) + target.legend = copy.copy(src.legend) + target.data = copy.copy(src.data) - return strings class Configurator(object): def __init__(self, canvas): @@ -167,12 +102,18 @@ def __init__(self, canvas): self.anim_button = None self.listeners = [] self.animation_last_frame_time = datetime.datetime.now() + self.picker = vtk.vtkPropPicker() # Map custom templates to their source template self.templates = {} self.creating = False self.click_locations = None + @property + def render_window(self): + if self.interactor is not None: + return self.interactor.GetRenderWindow() + return self.backend.renWin def get_save_path(self, default_name='', dialog_name="Save File"): import os.path @@ -258,82 +199,130 @@ def detach(self): # if all of the widgets have been cleaned up correctly, this will delete the manager vtk_ui.manager.delete_manager(self.interactor) - self.interactor.GetRenderWindow().Render() + self.render_window.Render() def release(self, object, event): if self.clicking is None: return if datetime.datetime.now() - self.clicking[1] < datetime.timedelta(0, .5): - point = self.clicking[0] self.clicking = None + if self.creating: self.click_locations.append(point) if len(self.click_locations) == CLICKS_TO_CREATE[self.creating]: self.create() return - if self.target and self.shift() == False and self.target.handle_click(point): + if self.target and self.shift() is False and self.target.handle_click(point): return if self.shift() and type(self.target) != editors.group.GroupEditor: self.target = editors.group.GroupEditor(self.interactor, (self.target,)) - clicked = None - display_clicked = None - for display in self.displays: - obj = self.in_display_plot(point, display) - if obj is not None and (clicked is None or obj.priority >= clicked.priority): - clicked = obj - display_clicked = display - if clicked: - if self.target is None or self.target.is_object(clicked) == False: - self.activate(clicked, display_clicked) - elif self.shift() and self.target.contains_object(clicked): - self.target.remove_object(clicked) - if len(self.target.targets) == 0: - self.target.detach() - self.target = None - self.save() - return - elif self.target is not None and self.shift() == False: + clicked_actor = self.actor_at_point(*point) + + if clicked_actor is None and self.target: self.deactivate(self.target) return + if clicked_actor is None: + return + + display, key = self.display_and_key_for_actor(clicked_actor) + if editable_type(display, key): + # Some methods (markers) have more than one actor per displayed item + if clicked_actor != display.backend[key] and clicked_actor not in display.backend[key]: + for group in display.backend[key]: + if clicked_actor in group: + clicked_actor = group + break + + self.activate(display, clicked_actor, key) + self.clicking = None + def display_and_key_for_actor(self, actor): + """ + Iterates across the displays, and tries to find the actor in the backend. + """ + for display in self.displays: + for key in display.backend: + try: + """ + display.backend[key] will contain either: + A) An actor + B) A list of actors + C) A list of tuples that include actors + """ + actor_in_backend = actor == display.backend[key] + if not actor_in_backend: + actor_in_backend = actor in display.backend[key] + if not actor_in_backend: + actor_in_backend = len([True for group in display.backend[key] if actor in group]) > 0 + except TypeError: + actor_in_backend = False + if actor_in_backend: + return display, key + else: + continue + break + else: + return None, None + + + def actor_at_point(self, x, y): + """ + Iterates all renderers, checks if there's an actor at the point + """ + obj = None + layer_obj = 0 + + for ren in vtkIterate(self.render_window.GetRenderers()): + layer = ren.GetLayer() + + if self.interactor is not None: + manager = vcs.vtk_ui.manager.get_manager(self.interactor) + if ren == manager.actor_renderer or ren == manager.renderer: + continue + + if obj is not None and layer < layer_obj: + continue + + if self.picker.PickProp(x, y, ren): + obj = self.picker.GetViewProp() + layer_obj = layer + + return obj + def hover(self, object,event): if self.clicking is not None: return point = self.interactor.GetEventPosition() - cursor = self.interactor.GetRenderWindow().GetCurrentCursor() - if self.target: - if self.target.handle_click(point): + + actor = self.actor_at_point(*point) + window = self.render_window + + new_cursor = vtk.VTK_CURSOR_DEFAULT + if actor is None: + man = vtk_ui.manager.get_manager(self.interactor) + if man.widget_at_point(*point): + new_cursor = vtk.VTK_CURSOR_HAND + else: + if self.target and self.target.handle_click(point): if self.interactor.GetControlKey() == 1 and type(self.target) in (editors.marker.MarkerEditor, editors.text.TextEditor): - if cursor != vtk.VTK_CURSOR_CROSSHAIR: - self.interactor.GetRenderWindow().SetCurrentCursor(vtk.VTK_CURSOR_CROSSHAIR) + new_cursor = vtk.VTK_CURSOR_CROSSHAIR else: - if cursor != vtk.VTK_CURSOR_HAND: - self.interactor.GetRenderWindow().SetCurrentCursor(vtk.VTK_CURSOR_HAND) - return - else: - if self.toolbar.in_toolbar(*point): - return + new_cursor = vtk.VTK_CURSOR_HAND + else: + display, key = self.display_and_key_for_actor(actor) - if self.marker_button.in_bounds(*point) or self.text_button.in_bounds(*point): - return - - for display in self.displays: - obj = self.in_display_plot(point, display) - if obj is not None: - if cursor != vtk.VTK_CURSOR_HAND: - self.interactor.GetRenderWindow().SetCurrentCursor(vtk.VTK_CURSOR_HAND) - return - if cursor != vtk.VTK_CURSOR_DEFAULT: - self.interactor.GetRenderWindow().SetCurrentCursor(vtk.VTK_CURSOR_DEFAULT) + if display is not None and editable_type(display, key): + new_cursor = vtk.VTK_CURSOR_HAND + window.SetCurrentCursor(new_cursor) def click(self, object, event): self.clicking = (self.interactor.GetEventPosition(), datetime.datetime.now()) @@ -381,35 +370,26 @@ def place(self): if self.target: self.target.place() - def activate(self, obj, display): - if self.target is not None and self.shift() == False: + def activate(self, display, actor, key): + if self.target is not None and self.shift() is False: self.deactivate(self.target) self.toolbar.hide() - if display.g_type == "fillarea": - editor = editors.fillarea.FillEditor(self.interactor, obj, self.clicked_info, self) - elif display.g_type == "line": - editor = editors.line.LineEditor(self.interactor, obj, self.clicked_info, self) - elif display.g_type == "marker": - editor = editors.marker.MarkerEditor(self.interactor, obj, self.clicked_info, display, self) + if display.g_type == "marker": + l = display.backend[key] + # Actor is actually a group of VTK objects + index = l.index(actor) + editor = editors.marker.MarkerEditor(self.interactor, vcs.getmarker(display.g_name), index, display, self) elif display.g_type == "text": - editor = editors.text.TextEditor(self.interactor, obj, self.clicked_info, display, self) + l = display.backend[key] + index = l.index(actor) + editor = editors.text.TextEditor(self.interactor, vcs.gettext(display.g_name), index, display, self) + elif is_label(key): + obj = get_attribute(display, key) + editor = editors.label.LabelEditor(self.interactor, obj, display, self) else: - if is_box(obj): - if obj.member == "legend": - editor = editors.legend.LegendEditor(self.interactor, t(display.template), self) - elif obj.member == "data": - editor = editors.data.DataEditor(self.interactor, vcs.getgraphicsmethod(display.g_type, display.g_name), t(display.template), self) - else: - editor = editors.box.BoxEditor(self.interactor, obj, self) - else: - if is_label(obj): - editor = editors.label.LabelEditor(self.interactor, obj, display, self) - elif is_point(obj): - editor = editors.point.PointEditor(self.interactor, obj, self) - else: - editor = None + editor = None if self.target: self.target.add_target(editor) @@ -417,50 +397,12 @@ def activate(self, obj, display): self.target = editor - - def in_display_plot(self, point, dp): - #Normalize the point - x, y = point - w, h = self.interactor.GetRenderWindow().GetSize() - if x > 1 or y > 1: - point = (x / float(w), y / float(h)) - x, y = point - - if dp.g_type == "fillarea": - fill = vcs.getfillarea(dp.g_name) - info = editors.fillarea.inside_fillarea(fill, *point) - if info is not None: - self.clicked_info = info - return fill - elif dp.g_type == "line": - l = vcs.getline(dp.g_name) - # Uses screen_height to determine how much buffer space there is around the line - info = editors.line.inside_line(l, *point, screen_height=h) - if info is not None: - self.clicked_info = info - return l - elif dp.g_type == "marker": - m = vcs.getmarker(dp.g_name) - info = editors.marker.inside_marker(m, point[0], point[1], w, h) - if info is not None: - self.clicked_info = info - return m - elif dp.g_type == "text": - tc = vcs.gettextcombined(dp.g_name) - info = editors.text.inside_text(tc, point[0], point[1], w, h) - if info is not None: - self.clicked_info = info - return tc - else: - fudge = 5 / float(w) - return in_template(point, t(dp.template), dp, (w, h), fudge=fudge) - def save(self): if self.changed: self.canvas.update() self.changed = False else: - self.interactor.GetRenderWindow().Render() + self.render_window.Render() self.canvas.animate.reset() def init_toolbar(self): @@ -510,12 +452,13 @@ def reset_template_changes(state): logo_button.set_state(1) def setup_animation(self): - if self.initialized == False: + if self.initialized is False and [True for d in self.displays if display_supports_animation(d)]: self.canvas.animate.create() anim_toolbar = self.toolbar.add_toolbar("Animation") self.anim_button = anim_toolbar.add_toggle_button("Animation", on=self.start_animating, off=self.stop_animating, on_prefix="Run", off_prefix="Stop") anim_toolbar.add_button(["Step Forward"], action=self.step_forward) anim_toolbar.add_button(["Step Backward"], action=self.step_back) + def get_frame(): return self.canvas.animate.frame_num anim_toolbar.add_slider_button(get_frame, 0, self.canvas.animate.number_of_frames(), "Time Slider", update=self.set_animation_frame) @@ -659,11 +602,7 @@ def init_buttons(self): prop.SetBackgroundOpacity(1) prop.SetColor(0, 0, 0) - #self.fill_button = vtk_ui.button.Button(self.interactor, states=states, image=os.path.join(sys.prefix,"share","vcs","fill_icon.png"), top=10, left=10, halign=vtk_ui.button.RIGHT_ALIGN, action=self.fill_click) self.text_button = vtk_ui.button.Button(self.interactor, states=states, image=os.path.join(sys.prefix, "share", "vcs", "text_icon.png"), top=10, left=10, halign=vtk_ui.button.RIGHT_ALIGN, action=self.text_click, tooltip="Create Text: click to place.", tooltip_property=prop) - - #self.line_button = vtk_ui.button.Button(self.interactor, states=states, image=os.path.join(sys.prefix, "share", "vcs", "line_icon.png"), top=10, left=116, halign=vtk_ui.button.RIGHT_ALIGN, action=self.line_click) - self.marker_button = vtk_ui.button.Button(self.interactor, states=states, image=os.path.join(sys.prefix, "share", "vcs", "marker_icon.png"), top=10, left=63, halign=vtk_ui.button.RIGHT_ALIGN, action=self.marker_click, tooltip="Create Marker: click to place.", tooltip_property=prop) @@ -721,7 +660,7 @@ def fill_click(self, index): self.creator_disabled(self.fill_button) def create(self): - w, h = self.interactor.GetRenderWindow().GetSize() + w, h = self.render_window.GetSize() x = [] y = [] @@ -730,155 +669,51 @@ def create(self): x.append(point[0] / float(w)) y.append(point[1] / float(h)) - created = None - - if self.creating == CREATING_FILL: - fill = self.canvas.createfillarea() - fill.x = x - fill.y = y - created = fill - self.fill_button.set_state(0) - dp = self.canvas.fillarea(fill) - elif self.creating == CREATING_TEXT: + if self.creating == CREATING_TEXT: t = self.canvas.createtextcombined() t.x = x t.y = y - t.string = ["New Text"] - created = t + t.string = ["Click to Edit"] self.text_button.set_state(0) dp = self.canvas.text(t) + key = "vtk_backend_text_actors" elif self.creating == CREATING_MARKER: m = self.canvas.createmarker() m.x = x m.y = y m.size = [10] - created = m self.marker_button.set_state(0) dp = self.canvas.marker(m) - elif self.creating == CREATING_LINE: - l = self.canvas.createline() - l.x = x - l.y = y - created = l - self.line_button.set_state(0) - dp = self.canvas.line(l) + key = "vtk_backend_marker_actors" # Activate an editor for the new object - self.clicked_info = 0 - self.activate(created, dp) + self.activate(dp, dp.backend[key][0], key) self.creating = False self.click_locations = None +def get_attribute(display, backend_key): + key = backend_key.split("_")[2] + template = vcstemp(display.template) + if key in ("Min", "Max", "Mean"): + return getattr(template, key.lower()) + else: + return getattr(template, key) + -def t(name): +def vcstemp(name): + """ + Retrieves template by name + """ return vcs.gettemplate(name) -def is_label(obj): - return type(obj) in (vcs.Pformat.Pf, vcs.Ptext.Pt) -def in_template(point, template, dp, window_size, fudge=None): - x, y = point +def display_supports_animation(d): + return d.g_type not in ("marker", "text", "textcombined", "fillarea", "line", "textorientation", "texttable") - attrs = [ - "file", - "function", - "logicalmask", - "transformation", - "source", - "dataname", - "title", - "units", - "crdate", - "crtime", - "comment1", - "comment2", - "comment3", - "comment4", - "xname", - "yname", - "zname", - "tname", - "xunits", - "yunits", - "zunits", - "tunits", - "xvalue", - "yvalue", - "zvalue", - "tvalue", - "mean", - "min", - "max", - "xtic1", - "xtic2", - "xmintic1", - "xmintic2", - "ytic1", - "ytic2", - "ymintic1", - "ymintic2", - "xlabel1", - "xlabel2", - "ylabel1", - "ylabel2", - "box1", - "box2", - "box3", - "box4", - "line1", - "line2", - "line3", - "line4", - "legend", - "data", - ] - - intersecting = None - - for attr in attrs: - attribute = getattr(template, attr) - if attribute.priority == 0 or (intersecting is not None and intersecting.priority > attribute.priority): - # 0 is turned off - continue - t_x = safe_get(attribute, "x") - t_y = safe_get(attribute, "y") - - if t_x is not None or t_y is not None: - if t_x is not None and t_y is not None: - # It's probably a text blob - if is_label(attribute): - text = editors.label.get_label_text(attribute, dp) - if text == '': - continue - if editors.label.inside_label(attribute, text, x, y, *window_size): - intersecting = attribute - elif is_point_in_box((x, y), ((t_x - fudge, t_y - fudge), (t_x + fudge, t_y + fudge))): - intersecting = attribute - else: - pass - """ Uncomment to enable axis editors - # It's probably the labels for an axis - if t_x is not None and t_x < x + fudge and t_x > x - fudge: - intersecting = attribute - elif t_y is not None and t_y < y + fudge and t_y > y - fudge: - intersecting = attribute - """ - else: - pass - """ Uncomment to reenable the box editors - x1 = safe_get(attribute, "x1") - y1 = safe_get(attribute, "y1") - x2 = safe_get(attribute, "x2") - y2 = safe_get(attribute, "y2") - if None in (x1, y1, x2, y2): - continue - else: - if is_point_in_box((x, y), ((min(x1, x2), min(y1, y2)), (max(x1, x2), max(y1, y2)))): - intersecting = attribute - """ - return intersecting +def is_label(key): + return "_text_actor" == key[-11:] def is_box(member): x1 = safe_get(member, "x1") @@ -915,3 +750,6 @@ def safe_get(obj, attr, sentinel=None): return getattr(obj, attr) except AttributeError: return sentinel + +def editable_type(display, key): + return display.g_type in ("marker", "text") or is_label(key) diff --git a/Packages/vcs/Lib/editors/label.py b/Packages/vcs/Lib/editors/label.py index 203b0f75d2..f2e9fb0423 100644 --- a/Packages/vcs/Lib/editors/label.py +++ b/Packages/vcs/Lib/editors/label.py @@ -1,9 +1,9 @@ import point import vcs -import text -#import vtk +import vtk import vcs.vcs2vtk from font import FontEditor +from vcs.vtk_ui.text import contrasting_color __valign_map__ = { 0: 0, @@ -19,6 +19,7 @@ "max": "vtk_backend_Max_text_actor", } + def get_actor(member, dp): if member.member in __backend_actor_names__: actor = dp.backend[__backend_actor_names__[member.member]] @@ -28,6 +29,7 @@ def get_actor(member, dp): actor = None return actor + class LabelEditor(point.PointEditor): """ An editor for text items that have data-provided text (template.min, template.max, etc.) @@ -44,6 +46,12 @@ def __init__(self, interactor, label, dp, configurator): self.actor = get_actor(self.label, self.display) + tprop = self.actor.GetTextProperty() + self.real_bg = tprop.GetBackgroundColor() + self.real_bg_opacity = tprop.GetBackgroundOpacity() + + tprop.SetBackgroundColor(contrasting_color(*tprop.GetColor())) + tprop.SetBackgroundOpacity(.85) text_types_name = template.name + "_" + label.member @@ -83,7 +91,7 @@ def sync_actor(self): if self.actor: p = self.actor.GetTextProperty() winSize = self.interactor.GetRenderWindow().GetSize() - vcs.vcs2vtk.prepTextProperty(p,winSize,to=self.to,tt=self.tt,cmap=None) + vcs.vcs2vtk.prepTextProperty(p, winSize, to=self.to, tt=self.tt, cmap=None) def set_font(self, font): self.tt.font = font @@ -123,13 +131,15 @@ def set_color(self, cmap, color): self.tt.color = color self.picker = None self.save() + tprop = self.actor.GetTextProperty() + tprop.SetBackgroundColor(contrasting_color(*tprop.GetColor())) + tprop.SetBackgroundOpacity(.85) #text colormap is currently not in place, will be later. #self.text.colormap = cmap def cancel_color(self): self.picker = None - def get_text(self): return get_label_text(self.label, self.display) @@ -140,9 +150,19 @@ def is_object(self, label): return self.label == label def in_bounds(self, x, y): - t = self.get_text() - swidth, sheight = self.interactor.GetRenderWindow().GetSize() - return inside_label(self.label, t, x, y, swidth, sheight) + w, h = self.interactor.GetRenderWindow().GetSize() + x, y = x * w, y * h + picker = vtk.vtkPropPicker() + for ren in vcs.vcs2vtk.vtkIterate(self.interactor.GetRenderWindow().GetRenderers()): + if ren.HasViewProp(self.actor): + break + else: + return False + + if picker.PickProp(x, y, ren) and picker.GetViewProp() == self.actor: + return True + else: + return False def delete(self): super(LabelEditor, self).delete() @@ -152,6 +172,9 @@ def delete(self): def detach(self): super(LabelEditor, self).detach() self.toolbar.detach() + tprop = self.actor.GetTextProperty() + tprop.SetBackgroundColor(*self.real_bg) + tprop.SetBackgroundOpacity(self.real_bg_opacity) def update_priority(self): maxLayers = self.interactor.GetRenderWindow().GetNumberOfLayers() @@ -184,14 +207,3 @@ def get_label_text(label, display): except AttributeError: t = '' return t - -def inside_label(label, t, x, y, screen_width, screen_height): - tt = label.texttable - to = label.textorientation - - tc = vcs.createtextcombined(Tt_source=tt, To_source=to) - tc.string = [t] - tc.x = [label.x] - tc.y = [label.y] - - return text.inside_text(tc, x, y, screen_width, screen_height) is not None diff --git a/Packages/vcs/Lib/editors/point.py b/Packages/vcs/Lib/editors/point.py index 8228ad1f07..cfa6603ebc 100644 --- a/Packages/vcs/Lib/editors/point.py +++ b/Packages/vcs/Lib/editors/point.py @@ -1,6 +1,7 @@ -from vcs.vtk_ui import behaviors, Handle +from vcs.vtk_ui import behaviors import priority + class PointEditor(behaviors.ClickableMixin, behaviors.DraggableMixin, priority.PriorityEditor): """ Base editor for anything with a single "x" and "y" coordinate (so mostly labels) @@ -30,10 +31,13 @@ def is_object(self, point): def handle_click(self, point): x, y = point + w, h = self.interactor.GetRenderWindow().GetSize() + + adjusted_x, adjusted_y = float(x) / w, float(y) / h try: - return self.in_bounds(x, y) or self.toolbar.in_toolbar(x, y) + return self.in_bounds(adjusted_x, adjusted_y) or self.toolbar.in_toolbar(x, y) except AttributeError: - return self.in_bounds(x, y) + return self.in_bounds(adjusted_x, adjusted_y) def render(self): from vcs.vtk_ui.manager import get_manager @@ -47,7 +51,6 @@ def drag_move(self, d_x, d_y): w, h = self.interactor.GetRenderWindow().GetSize() x, y = self.actor.GetPosition() self.actor.SetPosition(x + w * d_x, y + h * d_y) - self.actor.GetMapper().Update() self.render() except AttributeError: self.configurator.changed = True @@ -64,6 +67,7 @@ def detach(self): def deactivate(self): self.configurator.deactivate(self) + def in_point(point, x, y): if x < point.x + .001 and x > point.x - .001 and y > point.y - .001 and y < point.y + .001: return True diff --git a/Packages/vcs/Lib/editors/priority.py b/Packages/vcs/Lib/editors/priority.py index 0247713985..f33c079388 100644 --- a/Packages/vcs/Lib/editors/priority.py +++ b/Packages/vcs/Lib/editors/priority.py @@ -9,7 +9,7 @@ def key_pressed(self, key, shift=False, alt=False, control=False): self.raise_priority() elif key == "Down": self.lower_priority() - elif (len(key) == 1 and ord(key) == 127) or key == "Delete": + elif (len(key) == 1 and ord(key) == 127) or key in ("Delete", "Backspace"): self.delete() def get_object(self): diff --git a/Packages/vcs/Lib/editors/text.py b/Packages/vcs/Lib/editors/text.py index 27263f4837..cdc0ed7baa 100644 --- a/Packages/vcs/Lib/editors/text.py +++ b/Packages/vcs/Lib/editors/text.py @@ -1,11 +1,11 @@ from vcs.vtk_ui import Textbox, Toolbar, Label import vcs.vtk_ui.text from vcs.colorpicker import ColorPicker -from vtk import vtkTextProperty +from vtk import vtkTextProperty, vtkPropPicker, vtkPropCollection from vcs.vtk_ui.behaviors import ClickableMixin import priority import vcs -from vcs.vcs2vtk import genTextActor +from vcs.vcs2vtk import genTextActor, vtkIterate from font import FontEditor import sys @@ -17,6 +17,7 @@ 4: 2, } + class TextEditor(ClickableMixin, priority.PriorityEditor): """ Editor for `textcombined` objects @@ -59,6 +60,7 @@ def __init__(self, interactor, text, index, dp, configurator): prop.SetBackgroundColor(.87, .79, .55) prop.SetBackgroundOpacity(1) prop.SetColor(0, 0, 0) + prop.SetVerticalJustificationToTop() self.tooltip = Label(self.interactor, "%s + Click to place new text." % ("Cmd" if sys.platform == "darwin" else "Ctrl"), textproperty=prop) self.tooltip.left = 0 self.tooltip.top = self.interactor.GetRenderWindow().GetSize()[1] - self.tooltip.get_dimensions()[1] @@ -105,25 +107,21 @@ def update(self): string = self.text.string[ind] text_width, text_height = text_dimensions(self.text, ind, (w, h)) - x = x * w - y = h - y * h # mirror the y axis for widgets - if self.text.halign in ("right", 2): - x -= text_width - elif self.text.halign in ("center", 1): - x -= text_width / 2.0 - - if self.text.valign in ("half", 2): - y -= text_height / 2.0 - elif self.text.valign in ("top", 0): - y -= text_height + x = x * w + y = y * h box_prop = vtkTextProperty() vcs.vcs2vtk.prepTextProperty(box_prop, (w, h), to=self.text, tt=self.text, cmap=cmap) box_prop.SetOrientation(-1 * self.text.angle) + text_color = box_prop.GetColor() + highlight_color = vcs.vtk_ui.text.contrasting_color(*text_color) - textbox = Textbox(self.interactor, string, left=x, top=y, movable=True, on_editing_end=self.finished_editing, on_drag=self.moved_textbox, textproperty=box_prop, on_click=self.textbox_clicked) + textbox = Textbox(self.interactor, string, highlight_color=highlight_color, highlight_opacity=.8, movable=True, on_editing_end=self.finished_editing, on_drag=self.moved_textbox, textproperty=box_prop, on_click=self.textbox_clicked) + textbox.x = x + textbox.y = y textbox.show() + textbox.show_highlight() if ind == self.index: textbox.start_editing() @@ -132,15 +130,35 @@ def update(self): def finished_editing(self, textbox): index = self.textboxes.index(textbox) - self.text.string[index] = textbox.text - self.actors[index].SetInput(textbox.text) + if textbox.text == "": + del self.text.string[index] + del self.text.x[index] + del self.text.y[index] + del self.actors[index] + textbox.detach() + del self.textboxes[index] + if len(self.text.string) == 0: + self.deactivate() + return + else: + self.text.string[index] = textbox.text + self.actors[index].SetInput(textbox.text) + + def get_box_at_point(self, x, y): + for box in self.textboxes: + if box.in_bounds(x, y): + return box + return None def in_bounds(self, x, y): - return inside_text(self.text, x, y, *self.interactor.GetRenderWindow().GetSize(), index=self.index) is not None + return self.get_box_at_point(x, y) is not None def click_release(self): x, y = self.event_position() - text_index = inside_text(self.text, x, y, *self.interactor.GetRenderWindow().GetSize()) + w, h = self.interactor.GetRenderWindow().GetSize() + box = self.get_box_at_point(x * w, y * h) + + text_index = None if box is None else self.textboxes.index(box) self.process_click(text_index, x, y) @@ -162,7 +180,6 @@ def process_click(self, text_index, x, y): return else: self.textboxes[self.index].stop_editing() - if text_index is not None: # Change which one we're editing self.index = text_index @@ -177,9 +194,9 @@ def process_click(self, text_index, x, y): self.text.x.append(x) self.text.y.append(y) - self.text.string.append("New Text") + self.text.string.append("Click to Edit") - new_actor = genTextActor(self.actors[0].GetConsumer(0), string=["New Text"], x=[x], y=[y],to=self.text,tt=self.text,cmap=vcs.getcolormap())[0] + new_actor = genTextActor(self.actors[0].GetConsumer(0), string=["Click to Edit"], x=[x], y=[y],to=self.text,tt=self.text,cmap=vcs.getcolormap())[0] new_actor.SetVisibility(0) self.actors.append(new_actor) self.index = new_index @@ -187,12 +204,11 @@ def process_click(self, text_index, x, y): self.update() def textbox_clicked(self, point): - x, y = point - - winsize = self.interactor.GetRenderWindow().GetSize() + for ind, box in enumerate(self.textboxes): + if box.in_bounds(*point): + clicked_on = ind - clicked_on = inside_text(self.text, x, y, *winsize) - self.process_click(clicked_on, x, y) + self.process_click(clicked_on, *point) def deactivate(self): self.configurator.deactivate(self) @@ -241,7 +257,7 @@ def valign(self, state): elif state == 1: self.text.valign = 2 elif state == 2: - self.text.valign = 3 + self.text.valign = 4 self.update() def update_angle(self, value): @@ -279,51 +295,3 @@ def text_dimensions(text, index, winsize): prop = vtkTextProperty() vcs.vcs2vtk.prepTextProperty(prop, winsize, text, text, vcs.getcolormap()) return vcs.vtk_ui.text.text_dimensions(text.string[index], prop) - -def inside_text(text, x, y, screen_width, screen_height, index=None): - import math - - winsize = (screen_width, screen_height) - - if x > 1: - x = x / float(screen_width) - if y > 1: - y = y / float(screen_height) - - for ind, xcoord in enumerate(text.x): - if index is not None: - if ind != index: - continue - - ycoord = text.y[ind] - text_width, text_height = text_dimensions(text, ind, winsize) - text_width = text_width / float(screen_width) - text_height = text_height / float(screen_height) - - local_x, local_y = x, y - # Adjust X, Y for angle - if text.angle != 0: - # Translate to the origin - translated_x, translated_y = x - xcoord, y - ycoord - # Rotate about the origin - theta = math.radians(text.angle) - txrot = translated_x * math.cos(theta) - translated_y * math.sin(theta) - tyrot = translated_x * math.sin(theta) + translated_y * math.cos(theta) - # Translate back to the point - local_x, local_y = txrot + xcoord, tyrot + ycoord - - # Adjust for alignments - if text.valign in ("half", 2): - ycoord -= text_height / 2.0 - elif text.valign in ("top", 0): - ycoord -= text_height - - if text.halign in ("right", 2): - xcoord -= text_width - elif text.halign in ("center", 1): - xcoord -= text_width / 2.0 - - if local_x > xcoord and local_x < xcoord + text_width and local_y < ycoord + text_height and local_y > ycoord: - return ind - - return None diff --git a/Packages/vcs/Lib/vcs2vtk.py b/Packages/vcs/Lib/vcs2vtk.py index 13e5dde859..5fae266b9b 100644 --- a/Packages/vcs/Lib/vcs2vtk.py +++ b/Packages/vcs/Lib/vcs2vtk.py @@ -1450,3 +1450,10 @@ def stripGrid(vtk_grid): thresh.Update() vtk_grid = thresh.GetOutput() return vtk_grid + +def vtkIterate(iterator): + iterator.InitTraversal() + obj = iterator.GetNextItem() + while obj is not None: + yield obj + obj = iterator.GetNextItem() diff --git a/Packages/vcs/Lib/vtk_ui/behaviors/draggable.py b/Packages/vcs/Lib/vtk_ui/behaviors/draggable.py index da04f3715d..1bcbb238a9 100644 --- a/Packages/vcs/Lib/vtk_ui/behaviors/draggable.py +++ b/Packages/vcs/Lib/vtk_ui/behaviors/draggable.py @@ -1,5 +1,7 @@ from behavior import Behavior import datetime + + class DraggableMixin(Behavior): def __init__(self): @@ -7,7 +9,8 @@ def __init__(self): self.drag_origin = None self.drag_position = None self.drag_started = None - self.drag_buffer = 10 # in pixels + self.drag_buffer = 10 # in pixels + self.drag_interval = datetime.timedelta(0, .25) self.add_event_handler("LeftButtonPressEvent", self.drag_clicked) self.add_event_handler("MouseMoveEvent", self.drag_moved) self.add_event_handler("LeftButtonReleaseEvent", self.drag_released) @@ -27,7 +30,6 @@ def drag_stop(self): def drag_clicked(self, obj, event): x, y = self.event_position() - if self.in_bounds(x, y): self.drag_origin = self.event_position() self.drag_position = None @@ -37,7 +39,7 @@ def drag_clicked(self, obj, event): def drag_moved(self, obj, event): x, y = self.event_position() - if self.drag_origin is None or datetime.datetime.now() - self.drag_started < datetime.timedelta(0, .25): + if self.drag_origin is None or datetime.datetime.now() - self.drag_started < self.drag_interval: return if self.drag_position is None: diff --git a/Packages/vcs/Lib/vtk_ui/button.py b/Packages/vcs/Lib/vtk_ui/button.py index 9915b72fb5..7d790c6f32 100644 --- a/Packages/vcs/Lib/vtk_ui/button.py +++ b/Packages/vcs/Lib/vtk_ui/button.py @@ -95,6 +95,8 @@ def __init__(self, interactor, action=None, corner_radius=5, width=None, font="A super(Button, self).__init__(interactor, widget) if tooltip: + if tooltip_property is not None: + tooltip_property.SetVerticalJustificationToTop() self.tooltip_label = Label(interactor, tooltip, textproperty=tooltip_property) self.hover_handler = self.interactor.AddObserver("MouseMoveEvent", self.hover) self.hover_timer = None @@ -120,7 +122,6 @@ def hover(self, obj, event): if self.tooltip_label.showing(): self.tooltip_label.hide() - def still_hovering(self, obj, event): if self.hover_timer: self.tooltip_label.place() @@ -256,7 +257,7 @@ def set_state(self, state): self.place() def show(self): - self.widget.On() + super(Button, self).show() self.text_widget.show() self.place() @@ -273,12 +274,12 @@ def detach(self): super(Button, self).detach() def hide(self): + super(Button, self).hide() try: self.tooltip_label.hide() except AttributeError: pass self.text_widget.hide() - self.widget.Off() def in_bounds(self, x, y): w, h = self.get_dimensions() diff --git a/Packages/vcs/Lib/vtk_ui/handle.py b/Packages/vcs/Lib/vtk_ui/handle.py index 208edb3a57..82f94d6f3d 100644 --- a/Packages/vcs/Lib/vtk_ui/handle.py +++ b/Packages/vcs/Lib/vtk_ui/handle.py @@ -48,15 +48,6 @@ def __init__(self, interactor, point, width=10, height=10, opacity=1, color=(0, vtkCommand::InteractionEvent (on vtkWidgetEvent::Move) """ - def show(self): - if self.widget.GetEnabled() == False: - self.widget.On() - self.place() - - def hide(self): - if self.widget.GetEnabled(): - self.widget.Off() - def __get_position__(self): if self.normalize: w, h = self.interactor.GetRenderWindow().GetSize() diff --git a/Packages/vcs/Lib/vtk_ui/line.py b/Packages/vcs/Lib/vtk_ui/line.py new file mode 100644 index 0000000000..66bb10caa2 --- /dev/null +++ b/Packages/vcs/Lib/vtk_ui/line.py @@ -0,0 +1,115 @@ +import vtk + +class Line(object): + def __init__(self, point_1, point_2, color=(0,0,0), width=1, renderer=None): + self.line = vtk.vtkLineSource() + mapper = vtk.vtkPolyDataMapper2D() + mapper.SetInputConnection(self.line.GetOutputPort()) + self.actor = vtk.vtkActor2D() + self.actor.SetMapper(mapper) + self._renderer = None + + # Default to hidden + self.hide() + + # Actor and everything need to be set up before magic properties work + self.point_1 = point_1 + self.point_2 = point_2 + self.color = color + self.width = width + self.renderer = renderer + + def hide(self): + self.actor.VisibilityOff() + + def show(self): + self.actor.VisibilityOn() + + @property + def showing(self): + return self.actor.GetVisibility() == 1 + + @property + def point_1(self): + return self.line.GetPoint1()[:2] + + @point_1.setter + def point_1(self, value): + self.line.SetPoint1(value[0], value[1], 10) + + @property + def point_2(self): + return self.line.GetPoint2()[:2] + + @point_2.setter + def point_2(self, value): + self.line.SetPoint2(value[0], value[1], 10) + + @property + def x1(self): + return self.point_1[0] + + @x1.setter + def x1(self, value): + p1 = self.point_1 + self.point_1 = (value, p1[1], 10) + + @property + def x2(self): + return self.point_2[0] + + @x2.setter + def x2(self, value): + p = self.point_2 + self.point_2 = (value, p[1], 10) + + @property + def y1(self): + return self.point_1[1] + + @y1.setter + def y1(self, value): + p = self.point_1 + self.point_1 = (p[0], value, 10) + + @property + def y2(self): + return self.point_2[1] + + @y2.setter + def y2(self, value): + p = self.point_2 + self.point_2 = (p[0], value, 10) + + @property + def width(self): + return self.actor.GetProperty().GetLineWidth() + + @width.setter + def width(self, value): + self.actor.GetProperty().SetLineWidth(value) + + @property + def color(self): + return self.actor.GetProperty().GetColor() + + @color.setter + def color(self, value): + self.actor.GetProperty().SetColor(value) + + @property + def renderer(self): + return self._renderer + + @renderer.setter + def renderer(self, value): + if self._renderer: + self._renderer.RemoveActor(self.actor) + self._renderer = value + if value is not None: + self._renderer.AddActor(self.actor) + + def detach(self): + self.renderer = None + del self.actor + del self.line \ No newline at end of file diff --git a/Packages/vcs/Lib/vtk_ui/manager.py b/Packages/vcs/Lib/vtk_ui/manager.py index 4836979f66..3aae864fd8 100644 --- a/Packages/vcs/Lib/vtk_ui/manager.py +++ b/Packages/vcs/Lib/vtk_ui/manager.py @@ -14,7 +14,15 @@ def __init__(self, interactor): self.renderer = vtk.vtkRenderer() self.renderer.SetViewport(0, 0, 1, 1) self.renderer.SetBackground(1, 1, 1) + + # Used to overlay actors above widgets + self.actor_renderer = vtk.vtkRenderer() + self.actor_renderer.SetViewport(0, 0, 1, 1) + self.actor_renderer.SetBackground(1, 1, 1) + self.window.AddRenderer(self.renderer) + self.window.AddRenderer(self.actor_renderer) + self.widgets = [] self.timer_listener = self.interactor.AddObserver("TimerEvent", self.__render) self.window_mod = self.window.AddObserver("ModifiedEvent", self.__place, 30) @@ -41,6 +49,14 @@ def __render(self, obj, event): self.timer = None self.window.Render() + def widget_at_point(self, x, y): + picker = vtk.vtkPropPicker() + if picker.PickProp(x, y, self.renderer): + return True + if picker.PickProp(x, y, self.actor_renderer): + return True + return False + def queue_render(self): if not self.interactor.GetInitialized(): self.timer = 1 @@ -54,11 +70,24 @@ def queue_render(self): def elevate(self): # Raise to top layer of render window layer = self.window.GetNumberOfLayers() - self.window.SetNumberOfLayers(layer + 1) + + if layer > 1 and self.window.HasRenderer(self.renderer) and self.renderer.GetLayer() == layer - 1: + # We don't need to mess with anything and send out ModifiedEvents on the render window if + # we're already at the top layer. + return + + self.window.SetNumberOfLayers(layer + 2) + + # To get the layer to change appropriately, have to remove first. if self.window.HasRenderer(self.renderer): self.window.RemoveRenderer(self.renderer) + if self.window.HasRenderer(self.actor_renderer): + self.window.RemoveRenderer(self.actor_renderer) + self.renderer.SetLayer(layer) + self.actor_renderer.SetLayer(layer + 1) self.window.AddRenderer(self.renderer) + self.window.AddRenderer(self.actor_renderer) def add_widget(self, widget): @@ -74,6 +103,7 @@ def add_widget(self, widget): def remove_widget(self, widget): if widget in self.widgets: self.widgets.remove(widget) + del widget.manager if len(self.widgets) == 0: del ui_managers[self.interactor] self.detach() @@ -83,8 +113,16 @@ def detach(self): w.detach() if self.window.HasRenderer(self.renderer): self.window.RemoveRenderer(self.renderer) + + if self.window.HasRenderer(self.actor_renderer): + self.window.RemoveRenderer(self.actor_renderer) + self.renderer.RemoveAllViewProps() + self.actor_renderer.RemoveAllViewProps() + self.renderer = None + self.actor_renderer = None + self.interactor.RemoveObserver(self.timer_listener) self.window.RemoveObserver(self.window_mod) self.window.RemoveObserver(self.render_listener) @@ -111,4 +149,4 @@ def get_manager(inter): if ui_managers.get(inter, None) is None: ui_managers[inter] = InterfaceManager(inter) - return ui_managers[inter] \ No newline at end of file + return ui_managers[inter] diff --git a/Packages/vcs/Lib/vtk_ui/slider.py b/Packages/vcs/Lib/vtk_ui/slider.py index 71aa7fead7..007e4360e9 100644 --- a/Packages/vcs/Lib/vtk_ui/slider.py +++ b/Packages/vcs/Lib/vtk_ui/slider.py @@ -65,14 +65,8 @@ def place(self): def show(self): if self.value_func: self.repr.SetValue(float(self.value_func())) - self.widget.EnabledOn() - self.widget.On() + super(Slider, self).show() - def hide(self): - self.widget.Off() - - def is_showing(self): - return self.widget.GetEnabled() == 1 def end_slide(self, obj, event): value = self.repr.GetValue() diff --git a/Packages/vcs/Lib/vtk_ui/text.py b/Packages/vcs/Lib/vtk_ui/text.py index 8f3fcb0ef4..16e0eb4404 100644 --- a/Packages/vcs/Lib/vtk_ui/text.py +++ b/Packages/vcs/Lib/vtk_ui/text.py @@ -1,5 +1,7 @@ -from vtk import vtkTextActor, vtkTextWidget, vtkTextRenderer -import datetime +from vtk import vtkTextActor, vtkTextRenderer, vtkTextProperty, vtkPropPicker +import math + + def __set_font(font, text_props): """ Font selection logic for text properties @@ -17,11 +19,131 @@ def __set_font(font, text_props): text_props.SetFontFamily(VTK_FONT_FILE) text_props.SetFontFile(font) + +def lum_normalize(component): + if component <= .03928: + return component / 12.92 + else: + return ((component + .055) / 1.055) ** 2.4 + + +def luminance(color): + r, g, b = [lum_normalize(c) for c in color] + lum = .2126 * r + .7152 * g + .0722 * b + return lum + + +def contrast_ratio(fg, bg): + lum_fg = luminance(fg) + lum_bg = luminance(bg) + l1 = max(lum_fg, lum_bg) + l2 = min(lum_fg, lum_bg) + + return (l1 + .05) / (l2 + .05) + + def white_or_black(red, green, blue): """ Returns white or black to choose most contrasting color for provided color """ # Convert to YIQ colorspace - yiq = (red * 255 * 299 + green * 255 * 587 + blue * 255 * 114) / 1000 - return (0,0,0) if yiq >= 128 else (1, 1, 1) + lum = luminance((red, green, blue)) + return (0, 0, 0) if lum >= .5 else (1, 1, 1) + + +def contrasting_color(red, green, blue): + hue, saturation, value = rgb_to_hsv(red, green, blue) + + #saturation /= 2. + r, g, b = red, green, blue + phi = .61803398875 + + iterations = 0 + change_key_count = 5 + + hsv = {"hue": hue, "value": value, "saturation": saturation} + var_keys = hsv.keys() + key = "value" + + best_color = None + best_contrast = 0 + contrast = 0 + + maximum_iterations = 1000 + while contrast < 4.5 and iterations < maximum_iterations: + + contrast = contrast_ratio((red, green, blue), (r, g, b)) + # Allows us to jump out in case of infinite loop, and still use the best contrast we've seen + if contrast > best_contrast: + best_color = r, g, b + best_contrast = contrast + + iterations += 1 + + if iterations % change_key_count == 0: + key = var_keys[(var_keys.index(key) + 1) % 3] + + var_value = hsv[key] - phi + if var_value < 0: + var_value += 1 + hsv[key] = var_value + r, g, b = hsv_to_rgb(hsv["hue"], hsv["saturation"], hsv["value"]) + + return best_color + + +def hsv_to_rgb(h, s, v): + if s == 0: + # grayscale + return v, v, v + + h = h / 60. + i = math.floor(h) + f = h - i + p = v * (1 - s) + q = v * (1 - s * f) + t = v * (1 - s * (1 - f)) + + if i == 0: + r, g, b = v, t, p + elif i == 1: + r, g, b = q, v, p + elif i == 2: + r, g, b = p, v, t + elif i == 3: + r, g, b = p, q, v + elif i == 4: + r, g, b = t, p, v + else: + r, g, b = v, p, q + return r, g, b + + +def rgb_to_hsv(r, g, b): + + minimum = min(r, g, b) + maximum = max(r, g, b) + v = maximum + + delta = float(maximum - minimum) + + if r == g == b: + s = 0 + h = 0 + return h, s, v + else: + s = delta / maximum + + if r == maximum: + h = (g - b) / delta + elif g == maximum: + h = 2 + (b - r) / delta + else: + h = 4 + (r - g) / delta + + h = h * 60 + if h < 0: + h += 360 + return h, s, v + def text_actor(string, fgcolor, size, font): """ @@ -40,7 +162,7 @@ def text_actor(string, fgcolor, size, font): # Sane defaults. props.SetJustificationToCentered() - props.SetVerticalJustificationToCentered() + props.SetVerticalJustificationToTop() if string.find("\n") != -1: lines = string.split("\n") @@ -49,34 +171,24 @@ def text_actor(string, fgcolor, size, font): return actor -def text_dimensions(text, text_prop): - ren = vtkTextRenderer() - bounds = [0,0,0,0] - ren.GetBoundingBox(text_prop, text, bounds) - return bounds[1] - bounds[0], bounds[3] - bounds[2] -def baseline_offsets(origin, new_string, text_prop): +def text_dimensions(text, text_prop, at_angle=0): ren = vtkTextRenderer() + bounds = [0, 0, 0, 0] + p = vtkTextProperty() + p.ShallowCopy(text_prop) + p.SetOrientation(at_angle) + ren.GetBoundingBox(p, text, bounds) + return bounds[1] - bounds[0] + 1, bounds[3] - bounds[2] + 1 - bounds_origin = [0,0,0,0] - ren.GetBoundingBox(text_prop, origin, bounds_origin) - - bounds_new = [0,0,0,0] - ren.GetBoundingBox(text_prop, new_string, bounds_new) - - below_offset = bounds_origin[2] - bounds_new[2] - above_offset = bounds_origin[3] - bounds_new[3] +from widget import Widget, WidgetReprShim +from behaviors import DraggableMixin, ClickableMixin - return below_offset, above_offset -from widget import Widget -from behaviors import DraggableMixin +class Label(Widget, DraggableMixin, ClickableMixin): -class Label(Widget, DraggableMixin): - - def __init__(self, interactor, string, movable=False, on_move=None, on_drag=None, on_click=None, on_release=None, fgcolor=(1,1,1), size=24, font="Arial", left=0, top=0, textproperty=None): - widget = vtkTextWidget() + def __init__(self, interactor, string, movable=False, on_move=None, on_drag=None, on_click=None, fgcolor=(1, 1, 1), size=24, font="Arial", left=0, top=0, textproperty=None): if textproperty is not None: self.actor = vtkTextActor() @@ -87,45 +199,126 @@ def __init__(self, interactor, string, movable=False, on_move=None, on_drag=None else: self.actor = text_actor(string, fgcolor, size, font) - widget.SetTextActor(self.actor) + widget = WidgetReprShim(interactor, self.actor) super(Label, self).__init__(interactor, widget) - #self.widget.ResizableOff() self.movable = movable self.action = on_click - self.release_action = on_release self.move_action = on_move self.dragged = on_drag - self.left, self.top = left, top - self.top_offset = 0 - - # Assigned by Widget.__init__ - self.repr.MovingOff() - self.repr.PickableOff() - self.repr.SetShowBorderToOff() - self.actor.SetTextScaleModeToNone() + self.actor.SetUseBorderAlign(False) + self.actor.VisibilityOff() - # Map events to draggable actions, because standard events aren't propagated - self.add_event_handler("StartInteractionEvent", self.drag_clicked) - self.add_event_handler("InteractionEvent", self.drag_moved) - self.add_event_handler("EndInteractionEvent", self.drag_released) - self.add_event_handler("StartInteractionEvent", self.click) - self.add_event_handler("EndInteractionEvent", self.release) + self.left = left + self.top = top self.register() - def get_text(self): - return self.repr.GetText() + @property + def text(self): + """ + Get/Set the text on the actor + """ + return self.get_text() + + @text.setter + def text(self, value): + self.set_text(value) + + @property + def x(self): + """The x coordinate of the text actor.""" + return self.actor.GetPosition()[0] + + @x.setter + def x(self, value): + self.actor.SetPosition(value, self.y) + + @property + def y(self): + """The y coordinate of the text actor.""" + return self.actor.GetPosition()[1] + + @y.setter + def y(self, value): + self.actor.SetPosition(self.x, value) + + @property + def left(self): + """The left side of the text actor, adjusted by width/justification""" + halign = self.actor.GetTextProperty().GetJustificationAsString() + if halign == "Left": + return self.x + + w, h = text_dimensions(self.text, self.actor.GetTextProperty()) + if halign == "Centered": + return self.x - math.floor(w / 2.) + + if halign == "Right": + return self.x - w + + @left.setter + def left(self, l): + halign = self.actor.GetTextProperty().GetJustificationAsString() + if halign == "Left": + self.x = l + + w, h = text_dimensions(self.text, self.actor.GetTextProperty()) + if halign == "Centered": + self.x = l + math.floor(w / 2.) + + if halign == "Right": + self.x = l + w + + @property + def top(self): + """The top side of the text actor, adjusted by height/justification""" + w, h = text_dimensions(self.text, self.actor.GetTextProperty()) + valign = self.actor.GetTextProperty().GetVerticalJustificationAsString() + y = self.y + # Adjust from alignment point to top of the actor + if valign == "Top": + pass + if valign == "Centered": + y += math.floor(h / 2.) + 1 + if valign == "Bottom": + y += h + # Transform from y position to distance from top of screen to top of actor + w, h = self.interactor.GetRenderWindow().GetSize() + return h - y + + @top.setter + def top(self, t): + """ + Sets actor y using distance in pixels from top of window to top of actor + """ + # Get the text's size to adjust for alignment + w, h = text_dimensions(self.text, self.actor.GetTextProperty()) + + valign = self.actor.GetTextProperty().GetVerticalJustificationAsString() + # Adjust position based on alignment + if valign == "Top": + y = t + # Since it's not top-aligned, alignment point will be lower (and we're in units from top) + elif valign == "Centered": + y = t + math.floor(h / 2.) + 1 + elif valign == "Bottom": + y = t + h + # Convert the y from pixels from top to pixels from bottom + w, h = self.interactor.GetRenderWindow().GetSize() + self.y = h - y - def set_text(self, string): + def __repr__(self): + return "