Source code for ginga.canvas.DrawingMixin

#
# DrawingMixin.py -- enable drawing capabilities.
#
# This is open-source software licensed under a BSD license.
# Please see the file LICENSE.txt for details.
#
import time

from ginga import trcalc
from ginga.misc.Bunch import Bunch

from .CanvasMixin import CanvasMixin

__all__ = ['DrawingMixin']


[docs]class DrawingMixin(object): """The DrawingMixin is a mixin class that adds drawing capability for some of the basic CanvasObject-derived types. The set_surface method is used to associate a CanvasView object for layering on. """ def __init__(self): assert isinstance(self, CanvasMixin), "Missing CanvasMixin class" from .CanvasObject import drawCatalog # For interactive drawing self.candraw = False self.dc = drawCatalog # canvas objects which we know how to draw have an "idraw" # class method self.drawtypes = [key for key in self.dc.keys() if hasattr(self.dc[key], 'idraw')] self.drawtypes.sort() self.t_drawtype = 'point' self.t_drawparams = {} # holds the drawing context self._draw_cxt = None # For interactive editing self.canedit = False # Set to False to disable drag moves except from move control pt self.easymove = True self._start_x = 0 self._start_y = 0 self._cp_index = None self._edit_obj = None self._edit_status = False self._edit_detail = {} self._pick_cur_objs = set([]) self._pick_sel_objs = set([]) # For modes self._mode = 'draw' self._mode_tbl = Bunch() self.add_draw_mode(None) self.add_draw_mode('draw', down=self.draw_start, move=self.draw_motion, up=self.draw_stop, poly_add=self.draw_poly_add, poly_delete=self.draw_poly_delete) self.add_draw_mode('edit', down=self.edit_start, move=self.edit_motion, up=self.edit_stop, poly_add=self.edit_poly_add, poly_delete=self.edit_poly_delete) self.add_draw_mode('pick', down=self.pick_start, move=self.pick_motion, up=self.pick_stop, hover=self.pick_hover, key=self.pick_key, poly_add=self.edit_poly_add, poly_delete=self.edit_poly_delete) # For selection self._selected = [] self.multi_select_ok = False # this controls whether an object is automatically selected for # editing immediately after being drawn self.edit_follows_draw = False self._process_time = 0.0 # time delta threshold for deciding whether to update the image self._delta_time = 0.020 self._draw_obj = None # NOTE: must be mixed in with a Callback.Callbacks for name in ('draw-event', 'draw-down', 'draw-move', 'draw-up', 'cursor-down', 'cursor-up', 'cursor-move', 'draw-scroll', 'keydown-poly_add', 'keydown-poly_del', 'keydown-edit_del', 'edit-event', 'edit-select', 'drag-drop'): self.enable_callback(name) for name in ['key-down', 'key-up', 'btn-down', 'btn-move', 'btn-up', 'scroll', 'pinch', 'pan']: self.enable_callback('%s-none' % (name))
[docs] def set_surface(self, viewer): self.viewer = viewer # Register this canvas for events of interest. # Assumes we are mixed in with a canvas canvas = self # for legacy drawing via draw mode in Bindmap canvas.add_callback('draw-down', self.draw_start, viewer) canvas.add_callback('draw-move', self.draw_motion, viewer) canvas.add_callback('draw-up', self.draw_stop, viewer) canvas.add_callback('key-down-none', self._draw_op, 'key', viewer) canvas.add_callback('keydown-poly_add', self._draw_op, 'poly_add', viewer) canvas.add_callback('keydown-poly_del', self._draw_op, 'poly_delete', viewer) canvas.add_callback('keydown-edit_del', self.edit_delete_cb, viewer)
#canvas.add_callback('draw-scroll', self._edit_rotate_cb, viewer) #canvas.add_callback('draw-scroll', self._edit_scale_cb, viewer)
[docs] def register_for_cursor_drawing(self, viewer): canvas = self canvas.add_callback('cursor-down', self._draw_op, 'down', viewer) canvas.add_callback('cursor-move', self._draw_op, 'move', viewer) canvas.add_callback('cursor-up', self._draw_op, 'up', viewer) canvas.set_callback('none-move', self._draw_op, 'hover', viewer)
##### MODE LOGIC #####
[docs] def add_draw_mode(self, name, **kwargs): try: bnch = self._mode_tbl[name] except KeyError: bnch = Bunch(name=name, **kwargs) self._mode_tbl[name] = bnch return bnch
[docs] def set_draw_mode(self, mode): if mode not in self._mode_tbl: modes = list(self._mode_tbl.keys()) raise ValueError("mode must be one of: %s" % (str(modes))) self._mode = mode if mode != 'edit': self.clear_selected() self.update_canvas()
[docs] def get_draw_mode(self): return self._mode
def _draw_op(self, canvas, event, data_x, data_y, opn, viewer): if viewer != event.viewer: return False mode = self._mode # Hack to handle legacy drawing using draw mode in Bindmap if self.is_drawing(): mode = 'draw' try: method = self._mode_tbl[mode][opn] except KeyError: return False if method is not None: return method(canvas, event, data_x, data_y, viewer) return False ##### DRAWING LOGIC ##### def _draw_update(self, data_x, data_y, cxt, force_update=False): obj = None # update the context with current position x, y = cxt.crdmap.data_to((data_x, data_y)) cxt.setvals(x=x, y=y, data_x=data_x, data_y=data_y) draw_class = cxt.draw_class if draw_class is None: return False obj = draw_class.idraw(self, cxt) # update display every delta_time secs if obj is not None: obj.initialize(self, cxt.viewer, self.logger) self._draw_obj = obj if force_update or (time.time() - self._process_time > self._delta_time): self.process_drawing() return True
[docs] def draw_start(self, canvas, event, data_x, data_y, viewer): if not self.candraw: return False self._draw_obj = None self.clear_selected() # get the drawing coordinate type (default 'data') crdtype = self.t_drawparams.get('coord', 'data') crdmap = viewer.get_coordmap(crdtype) x, y = crdmap.data_to((data_x, data_y)) klass = self.dc.get(self.t_drawtype, None) # create the drawing context self._draw_cxt = Bunch(start_x=x, start_y=y, points=[(x, y)], x=x, y=y, data_x=data_x, data_y=data_y, drawparams=self.t_drawparams, crdmap=crdmap, viewer=viewer, draw_class=klass, logger=self.logger) self._draw_update(data_x, data_y, self._draw_cxt, force_update=True) return True
[docs] def draw_stop(self, canvas, event, data_x, data_y, viewer): if not self.candraw: return False self._draw_update(data_x, data_y, self._draw_cxt) obj, self._draw_obj = self._draw_obj, None if obj is not None: objtag = self.add(obj) self.make_callback('draw-event', objtag) if self.edit_follows_draw: #self.set_draw_mode('edit') self.edit_select(obj) self.make_callback('edit-select', self._edit_obj) return True else: self.process_drawing()
[docs] def draw_motion(self, canvas, event, data_x, data_y, viewer): if not self.candraw: return False self._draw_update(data_x, data_y, self._draw_cxt) return True
[docs] def draw_poly_add(self, canvas, event, data_x, data_y, viewer): if not self.candraw: return False cxt = self._draw_cxt if self.t_drawtype in ('polygon', 'freepolygon', 'path', 'freepath'): x, y = cxt.crdmap.data_to((data_x, data_y)) cxt.points.append((x, y)) elif self.t_drawtype == 'beziercurve' and len(cxt.points) < 3: x, y = cxt.crdmap.data_to((data_x, data_y)) cxt.points.append((x, y)) self._draw_update(data_x, data_y, cxt, force_update=True) return True
[docs] def draw_poly_delete(self, canvas, event, data_x, data_y, viewer): if not self.candraw: return False cxt = self._draw_cxt if self.t_drawtype in ('polygon', 'freepolygon', 'path', 'freepath', 'beziercurve'): if len(cxt.points) > 0: cxt.points.pop() self._draw_update(data_x, data_y, cxt, force_update=True) return True
[docs] def is_drawing(self): return self._draw_obj is not None
[docs] def enable_draw(self, tf): self.candraw = tf
[docs] def set_drawcolor(self, colorname): self.t_drawparams['color'] = colorname
[docs] def set_drawtype(self, drawtype, **drawparams): if drawtype is not None: drawtype = drawtype.lower() assert drawtype in self.drawtypes, \ ValueError("Bad drawing type '%s': must be one of %s" % ( drawtype, self.drawtypes)) self.t_drawtype = drawtype self.t_drawparams = drawparams.copy()
[docs] def get_drawtypes(self): return self.drawtypes
[docs] def get_drawtype(self): return self.t_drawtype
[docs] def get_draw_class(self, drawtype): drawtype = drawtype.lower() klass = self.dc[drawtype] return klass
[docs] def get_draw_classes(self): return self.dc
[docs] def get_drawparams(self): return self.t_drawparams.copy()
[docs] def process_drawing(self): self._process_time = time.time() #self.redraw(whence=3) self.update_canvas()
[docs] def register_canvas_type(self, name, klass): drawtype = name.lower() self.dc[drawtype] = klass if drawtype not in self.drawtypes: self.drawtypes.append(drawtype) self.drawtypes.sort()
##### EDITING LOGIC #####
[docs] def get_edit_object(self): return self._edit_obj
[docs] def is_editing(self): return self.get_edit_obj() is not None
[docs] def enable_edit(self, tf): self.canedit = tf
def _rot_xlate(self, obj, data_x, data_y): # translate point back into non-rotated form rot_deg = - obj.rot_deg xoff, yoff = obj.get_center_pt() data_x, data_y = trcalc.rotate_pt(data_x, data_y, rot_deg, xoff=xoff, yoff=yoff) return data_x, data_y def _edit_update(self, data_x, data_y, viewer): if (not self.canedit) or (self._cp_index is None): return False x, y = data_x, data_y if self._cp_index < 0: if self.easymove: self._edit_obj.set_edit_point(0, (x - self._start_x, y - self._start_y), self._edit_detail) else: # special hack for objects that have rot_deg attribute if hasattr(self._edit_obj, 'rot_deg') and (self._cp_index > 0): x, y = self._rot_xlate(self._edit_obj, x, y) self._edit_obj.set_edit_point(self._cp_index, (x, y), self._edit_detail) #self._edit_obj.sync_state() if time.time() - self._process_time > self._delta_time: self.process_drawing() return True def _is_editable(self, obj, pt, is_inside): return is_inside and obj.editable def _prepare_to_move(self, obj, data_x, data_y): #print(("moving an object", obj.editable)) self.edit_select(obj) self._cp_index = -1 ref_x, ref_y = self._edit_obj.get_reference_pt() self._start_x, self._start_y = data_x - ref_x, data_y - ref_y #print(("end moving an object", obj.editable))
[docs] def edit_start(self, canvas, event, data_x, data_y, viewer): if not self.canedit: return False self._edit_tmp = self._edit_obj self._edit_status = False self._edit_detail = Bunch(viewer=viewer) self._cp_index = None #shift_held = 'shift' in event.modifiers shift_held = False selects = self.get_selected() if len(selects) == 0: #print("no objects already selected") # <-- no objects already selected # check for objects at this location #print("getting items") objs = canvas.select_items_at(viewer, (data_x, data_y), test=self._is_editable) #print("items: %s" % (str(objs))) if len(objs) == 0: # <-- no objects under cursor return False # pick top object obj = objs[-1] self._prepare_to_move(obj, data_x, data_y) else: self._edit_status = True # Ugh. Check each selected object's control points # for a match contains = [] for obj in selects: #print("editing: checking for cp") edit_pts = obj.get_edit_points(viewer) #print((self._edit_obj, edit_pts)) idx = obj.get_pt(viewer, edit_pts, (data_x, data_y), obj.cap_radius) if len(idx) > 0: i = idx[0] #print("editing cp #%d" % (i)) # editing a control point from an existing object self._edit_obj = obj self._cp_index = i if hasattr(obj, 'rot_deg'): x, y = self._rot_xlate(self._edit_obj, data_x, data_y) else: x, y = data_x, data_y self._edit_detail.start_pos = (x, y) obj.setup_edit(self._edit_detail) self._edit_update(data_x, data_y, viewer) return True i = None ## if obj.contains_pt((data_x, data_y)): ## contains.append(obj) # update: check if objects bbox contains this point x1, y1, x2, y2 = obj.get_llur() if (x1 <= data_x <= x2) and (y1 <= data_y <= y2): contains.append(obj) # <-- no control points match, is there an object that contains # this point? if len(contains) > 0: # TODO?: make a compound object of contains and move it? obj = contains[-1] if self.is_selected(obj) and shift_held: # deselecting object self.select_remove(obj) else: self._prepare_to_move(obj, data_x, data_y) ## Compound = self.get_draw_class('compoundobject') ## c_obj = Compound(*self.get_selected()) ## c_obj.inherit_from(obj) ## self._prepare_to_move(c_obj, data_x, data_y) else: # <-- user clicked outside any selected item's control pt # and outside any selected item if not shift_held: self.clear_selected() # see now if there is an unselected item at this location objs = canvas.select_items_at(viewer, (data_x, data_y), test=self._is_editable) #print("new items: %s" % (str(objs))) if len(objs) > 0: # pick top object obj = objs[-1] #print(("top object", obj)) if self.num_selected() > 0: #print("there are previously selected items") # if there are already some selected items, then # add this object to the selection, make a compound # object self.edit_select(obj) Compound = self.get_draw_class('compoundobject') c_obj = Compound(*self.get_selected()) c_obj.inherit_from(obj) self._prepare_to_move(c_obj, data_x, data_y) else: # otherwise just start over with this new object #print(("starting over")) self._prepare_to_move(obj, data_x, data_y) self.process_drawing() return True
[docs] def edit_stop(self, canvas, event, data_x, data_y, viewer): if not self.canedit: return False if (self._edit_tmp != self._edit_obj) or ( (self._edit_obj is not None) and (self._edit_status != self.is_selected(self._edit_obj))): # <-- editing status has changed #print("making edit-select callback") self.make_callback('edit-select', self._edit_obj) if (self._edit_obj is not None) and (self._cp_index is not None): # <-- an object has been edited self._edit_update(data_x, data_y, viewer) self._cp_index = None self.make_callback('edit-event', self._edit_obj) self._edit_obj.make_callback('edited') return True
[docs] def edit_motion(self, canvas, event, data_x, data_y, viewer): if not self.canedit: return False if (self._edit_obj is not None) and (self._cp_index is not None): self._edit_update(data_x, data_y, viewer) return True return False
[docs] def edit_poly_add(self, canvas, event, data_x, data_y, viewer): if not self.canedit: return False obj = self._edit_obj if ((obj is not None) and self.is_selected(obj) and (obj.kind in ('polygon', 'path'))): self.logger.debug("checking points") # determine which line we are adding a point to points = list(obj.get_data_points()) if obj.kind == 'polygon': points = points + [points[0]] x0, y0 = points[0] insert = None for i in range(1, len(points[1:]) + 1): x1, y1 = points[i] self.logger.debug("checking line %d" % (i)) if obj.within_line(viewer, (data_x, data_y), (x0, y0), (x1, y1), 8): insert = i break x0, y0 = x1, y1 if insert is not None: self.logger.debug("inserting point") # Point near a line pt = obj.crdmap.data_to((data_x, data_y)) obj.insert_pt(insert, pt) self.process_drawing() else: self.logger.debug("cursor not near a line") return True
[docs] def edit_poly_delete(self, canvas, event, data_x, data_y, viewer): if not self.canedit: return False obj = self._edit_obj if ((obj is not None) and self.is_selected(obj) and (obj.kind in ('polygon', 'path'))): self.logger.debug("checking points") # determine which point we are deleting points = list(obj.get_data_points()) delete = None for i in range(len(points)): x1, y1 = points[i] self.logger.debug("checking vertex %d" % (i)) if obj.within_radius(viewer, (data_x, data_y), (x1, y1), 8): delete = i break if delete is not None: self.logger.debug("deleting point") obj.delete_pt(delete) self.process_drawing() else: self.logger.debug("cursor not near a point") return True
[docs] def edit_rotate(self, delta_deg, viewer): if self._edit_obj is None: return False self._edit_obj.rotate_by_deg([delta_deg]) self.process_drawing() self.make_callback('edit-event', self._edit_obj) return True
def _edit_rotate_cb(self, canvas, event, viewer, msg=True): if not self.canedit or (viewer != event.viewer): return False bd = viewer.get_bindings() amount = event.amount if bd.get_direction(event.direction) == 'down': amount = - amount return self.edit_rotate(amount)
[docs] def edit_scale(self, delta_x, delta_y, viewer): if self._edit_obj is None: return False self._edit_obj.scale_by_factors((delta_x, delta_y)) self.process_drawing() self.make_callback('edit-event', self._edit_obj) return True
def _edit_scale_cb(self, canvas, event, viewer, msg=True): if not self.canedit or (viewer != event.viewer): return False bd = viewer.get_bindings() if bd.get_direction(event.direction) == 'down': amount = 0.9 else: amount = 1.1 return self.edit_scale(amount, amount)
[docs] def edit_delete(self): if (self._edit_obj is not None) and self.is_selected(self._edit_obj): self.select_remove(self._edit_obj) obj, self._edit_obj = self._edit_obj, None self.delete_object(obj) self.make_callback('edit-event', self._edit_obj) return True
[docs] def edit_delete_cb(self, canvas, event, data_x, data_y, viewer): if not self.canedit or (viewer != event.viewer): return False return self.edit_delete()
[docs] def edit_select(self, newobj): if not self.canedit: return False if not self.multi_select_ok: self.clear_selected() # add new object to selection self.select_add(newobj) self._edit_obj = newobj return True
##### SELECTION LOGIC ##### def _is_selectable(self, obj, x, y, is_inside): return is_inside and obj.editable #return is_inside
[docs] def is_selected(self, obj): return obj in self._selected
[docs] def get_selected(self): return self._selected
[docs] def num_selected(self): return len(self._selected)
[docs] def clear_selected(self): self._selected = []
[docs] def select_remove(self, obj): try: self._selected.remove(obj) except Exception: pass
[docs] def select_add(self, obj): if obj not in self._selected: self._selected.append(obj)
##### PICK LOGIC ##### def _do_pick(self, canvas, event, data_x, data_y, ptype, viewer): # check for objects at this location objs = canvas.select_items_at(viewer, (data_x, data_y)) picked = set(filter(lambda obj: obj.pickable, objs)) newly_out = self._pick_cur_objs - picked newly_in = picked - self._pick_cur_objs self._pick_cur_objs = picked if ptype not in ('move', 'up'): self._pick_sel_objs = picked # leaving an object for obj in newly_out: pt = obj.crdmap.data_to((data_x, data_y)) obj.make_callback('pick-leave', canvas, event, pt) # entering an object for obj in newly_in: pt = obj.crdmap.data_to((data_x, data_y)) obj.make_callback('pick-enter', canvas, event, pt) # pick down/up res = False for obj in self._pick_sel_objs: cb_name = 'pick-%s' % (ptype) self.logger.debug("%s event in %s obj at x, y = %d, %d" % ( cb_name, obj.kind, data_x, data_y)) pt = obj.crdmap.data_to((data_x, data_y)) if obj.make_callback(cb_name, canvas, event, pt): res = True return res
[docs] def pick_start(self, canvas, event, data_x, data_y, viewer): return self._do_pick(canvas, event, data_x, data_y, 'down', viewer)
[docs] def pick_motion(self, canvas, event, data_x, data_y, viewer): return self._do_pick(canvas, event, data_x, data_y, 'move', viewer)
[docs] def pick_hover(self, canvas, event, data_x, data_y, viewer): return self._do_pick(canvas, event, data_x, data_y, 'hover', viewer)
[docs] def pick_key(self, canvas, event, data_x, data_y, viewer): return self._do_pick(canvas, event, data_x, data_y, 'key', viewer)
[docs] def pick_stop(self, canvas, event, data_x, data_y, viewer): return self._do_pick(canvas, event, data_x, data_y, 'up', viewer)
# The canvas drawing
[docs] def draw(self, viewer): # Draw everything else as usual super(DrawingMixin, self).draw(viewer) # Draw our current drawing object, if any if self._draw_obj: self._draw_obj.draw(viewer) # Draw control points on edited objects selected = list(self.get_selected()) if len(selected) > 0: for obj in selected: cr = viewer.renderer.setup_cr(obj) obj.draw_edit(cr, viewer)