#
# 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 ginga.util.six.moves import filter
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 ImageViewCanvas 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', 'cursor-changed'):
self.enable_callback(name)
for name in ['keydown', 'keyup', '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('keydown-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()
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(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)
### NON-PEP8 EQUIVALENTS -- TO BE DEPRECATED ###
setSurface = set_surface
getDrawClass = get_draw_class
# END