#!/usr/bin/python import gobject import gtk from gtk import gdk import gtk.glade import cairo import cairo.gtk import math import sys def d_point_segment (p, p1, p2): segx = float(p2[0] - p1[0]) segy = float(p2[1] - p1[1]) denom = (segx*segx + segy*segy) if denom == 0: return abs(p[1] - p1[1]) t = ((p[0] - p1[0]) * segx + (p[1] - p1[1]) * segy) / (segx*segx + segy*segy) dx = p[0] - (p1[0] + t * segx) dy = p[1] - (p1[1] + t * segy) return math.sqrt (dx*dx + dy*dy) def d_point_point (p1, p2): segx = float(p2[0] - p1[0]) segy = float(p2[1] - p1[1]) return math.sqrt (segx*segx + segy*segy) SIMPLIFY_NONE = 0 SIMPLIFY_FIXED = 1 SIMPLIFY_VELOCITY = 2 JOIN_ANGLE = 0 JOIN_CURVATURE = 1 class StrokerConfig(gobject.GObject): __gsignals__ = { 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()) } def __init__(self): gobject.GObject.__init__(self) self.update_immediately = False self.annotate = True self.simplify_type = SIMPLIFY_VELOCITY self.fixed_threshold = 4 self.min_threshold = 2 self.base_threshold = 5 self.base_velocity = 300 self.join_type = JOIN_CURVATURE self.join_angle = 50 self.curvature_multiplier = 2.0 def threshold(self, velocity): if self.simplify_type == SIMPLIFY_FIXED: return self.fixed_threshold else: return self.min_threshold + (self.base_threshold - self.min_threshold) * math.log(1+velocity/self.base_velocity) / (1 + math.log(2)) gobject.type_register(StrokerConfig) class Simplifier: def __init__(self, config): self.config = config def smooth_junction(self, p1, p2, p3): d12 = d_point_point(p1, p2) d23 = d_point_point(p2, p3) d13 = d_point_point(p1, p3) if self.config.join_type == JOIN_ANGLE: dot = (p2[0] - p1[0]) * (p3[0] - p2[0]) + (p2[1] - p1[1]) * (p3[1] - p2[1]) angle = math.acos(dot / (d12 * d23)) join_smooth = angle < math.pi * self.config.join_angle / 180. else: try: c12 = (d13*d13 - d12*d12 + d23*d23) / (2*d13*d23) s12 = math.sqrt(1 - c12*c12) extra_12 = d12 * (1 - c12) / (2 * s12) c23 = (d13*d13 - d23*d23 + d12*d12) / (2*d13*d12) s23 = math.sqrt(1 - c23*c23) extra_23 = d12 * (1 - c23) / (2 * s23) join_smooth = extra_12 < p2[2] * self.config.curvature_multiplier and \ extra_23 < p3[2] * self.config.curvature_multiplier except ZeroDivisionError: join_smooth = False if join_smooth: seg13x = (p3[0] - p1[0]) / d13 seg13y = (p3[1] - p1[1]) / d13 return ((p2[0] - seg13x * d12 * 0.33, p2[1] - seg13y * d12 * 0.33), (p2[0] + seg13x * d23 * 0.33, p2[1] + seg13y * d23 * 0.33), 1) else: return (p2, p2, 0) # Alternate method of generating control points for sharp segments ... use a tangent to the # line from the segment; simply using a control point at the segment as above should give less # lumpy results. # # return ((0.5 * (p1[0] + p2[0]), 0.5 * (p1[1] + p2[1])), # (0.5 * (p2[0] + p3[0]), 0.5 * (p2[1] + p3[1])), # 0) # returns a list of points which is a simplification # of the set of line segments from start to end within # a distance threshold. start is not included in the # result. # def do_simplify(self, points, start, end): max_point = None max_distance = 0 for i in range(start+1, end): distance = d_point_segment(points[i], points[start], points[end]) if distance > max_distance: max_point = i max_distance = distance velocity = 1000*(points[end][2] - points[start][2]) / (points[end][3] - points[start][3]) threshold = self.config.threshold(velocity) if max_distance < threshold: return [(points[end][0],points[end][1],threshold)] return self.do_simplify(points, start, max_point) + \ self.do_simplify(points, max_point, end) def simplify(self, points): if self.config.simplify_type == SIMPLIFY_NONE or len(points) < 2: return points else: return [points[0]] + self.do_simplify(points, 0, len(points) - 1) class Stroke: def __init__(self, config, simplifier): self.config = config def config_changed_cb(cfg): self.simplified = None self.config.connect('changed', config_changed_cb) self.simplifier = simplifier self.points = [] self.done = False self.bbox = None def add_point(self, point): self.points.append(point) self.simplified = None def finish(self): self.done = True def draw_smooth_points(self, context, points): context.set_rgb_color(1, 0, 0) context.set_alpha(0.5) for point in points: context.arc (point[0], point[1], 4, 0, 2 * math.pi) context.fill() def draw_sharp_points(self, context, points): context.set_rgb_color(0, 0, 1) context.set_alpha(0.5) for point in points: context.rectangle (point[0] - 3, point[1] - 3, 6, 6) context.fill() def draw_original_points(self, context, points): context.set_rgb_color(0, 0, 1) context.set_alpha(0.5) for point in points: context.arc (point[0], point[1], 4, 0, 2 * math.pi) context.fill() def draw_lines(self, context, points): context.set_rgb_color(0, 0, 0) context.set_alpha(1) context.move_to(points[0][0], points[0][1]) last = points[0] tentative = None for point in points[1:]: if tentative and ((tentative[0] > last[0] and tentative[0] > point[0]) or (tentative[0] < last[0] and tentative[0] < point[0]) or (tentative[1] > last[1] and tentative[1] > point[1]) or (tentative[1] < last[1] and tentative[1] < point[1])): context.line_to(tentative[0], tentative[1]) if abs(point[0] - last[0]) > 0 and \ abs(point[1] - last[1]) > 0: context.line_to(point[0], point[1]) last = point tentative = None else: tentative = point if tentative: context.line_to(tentative[0], tentative[1]) context.stroke() if self.config.annotate and self.done: self.draw_original_points(context, self.points) def draw_smooth(self, context, points): smooth_points = [] sharp_points = [] context.set_rgb_color(0, 0, 0) context.set_alpha(1) context.move_to(points[0][0], points[0][1]) last = points[0] point = points[1] prev_control = (0.5 * (point[0] + last[0]), 0.5 * (point[1] + last[1])) for next in points[2:]: back_control, next_control, smooth = self.simplifier.smooth_junction (last, point, next) if smooth: smooth_points.append(point) else: sharp_points.append(point) context.curve_to(prev_control[0], prev_control[1], back_control[0], back_control[1], point[0], point[1]) last = point point = next prev_control = next_control back_control = (0.5 * (point[0] + last[0]), 0.5 * (point[1] + last[1])) context.curve_to(prev_control[0], prev_control[1], back_control[0], back_control[1], point[0], point[1]) context.stroke() if self.config.annotate: self.draw_smooth_points(context, smooth_points) sharp_points.append(points[0]) sharp_points.append(points[-1]) self.draw_sharp_points(context, sharp_points) def draw(self, context): if (self.config.simplify_type != SIMPLIFY_NONE and (self.config.update_immediately or self.done)): if not self.simplified: self.simplified = self.simplifier.simplify(self.points) self.draw_smooth(context, self.simplified) else: self.draw_lines(context, self.points) def draw_velocity(self, context): start_point = self.points[-1] end_point = None for i in range(len(self.points) - 2, -1, -1): end_point = self.points[i] if start_point[3] - end_point[3] > 500: break if not end_point: return dt = end_point[3] - start_point[3] if dt == 0: return velocity = 1000*(end_point[2] - start_point[2]) / dt threshold = self.config.threshold(velocity) context.set_rgb_color(0, 0.5, 0) context.set_alpha(0.75) context.select_font("sans-serif", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) context.scale_font(20) context.move_to(5, 20) context.show_text("%.0f" % velocity) context.move_to(5, 45) context.show_text("%.1f" % threshold) class Stroker(gtk.DrawingArea): __gsignals__ = { 'expose-event' : 'override', 'button-press-event' : 'override', 'button-release-event' : 'override', 'motion-notify-event' : 'override', 'size-allocate' : 'override', 'unrealize' : 'override'} def __init__(self): gtk.DrawingArea.__init__(self) self.set_events(gdk.EXPOSURE_MASK | gdk.BUTTON_PRESS_MASK | gdk.BUTTON_RELEASE_MASK | gdk.POINTER_MOTION_MASK) self.current_stroke = None self.backing_store = None self.strokes = [] self.config = StrokerConfig() def config_changed_cb(cfg): self.backing_store = None self.queue_draw() self.config.connect('changed', config_changed_cb) self.simplifier = Simplifier(self.config) def _make_backing_store (self): width = self.allocation.width height = self.allocation.height self.backing_store = gdk.Pixmap (self.window, width, height) self.backing_store.draw_rectangle(self.style.base_gc[self.state], True, 0, 0, width, height) context = cairo.Context() cairo.gtk.set_target_drawable (context, self.backing_store) for stroke in self.strokes: stroke.draw(context) def do_expose_event(self, event): width = self.allocation.width height = self.allocation.height if not self.backing_store: self._make_backing_store() self.window.draw_drawable(self.style.base_gc[self.state], self.backing_store, event.area.x, event.area.y, event.area.x, event.area.y, event.area.width, event.area.height) if self.current_stroke: context = cairo.Context() cairo.gtk.set_target_drawable (context, self.window) self.current_stroke.draw(context) if self.config.simplify_type == SIMPLIFY_VELOCITY: self.current_stroke.draw_velocity(context) return False def _add_point(self, event): if len(self.current_stroke.points) > 0: last_point = self.current_stroke.points[-1] distance = last_point[2] + d_point_point (last_point, (event.x, event.y)) else: distance = 0 self.current_stroke.add_point((event.x, event.y, distance, event.time)) def do_button_press_event(self, event): self.current_stroke = Stroke(self.config, self.simplifier) self._add_point(event) self.queue_draw() return True def do_button_release_event(self, event): if not self.current_stroke: return self._add_point(event) self.current_stroke.finish() if self.backing_store: context = cairo.Context() cairo.gtk.set_target_drawable (context, self.backing_store) self.current_stroke.draw(context) self.strokes.append(self.current_stroke) self.current_stroke = None self.queue_draw() return False def do_motion_notify_event(self, event): if not self.current_stroke: return self._add_point(event) self.queue_draw() return False def do_size_allocate(self, allocation): self.chain(allocation) self.backing_store = None def do_unrealize(self): self.chain() self.backing_store = None def clear(self): self.strokes = [] self.current_stroke = None self.backing_store = None self.queue_draw() class StrokerOptions: def _get_widget(self, name): widget = self.xml.get_widget(name) if not widget: raise KeyError("No such widget %s" % name) return widget def _connect_checkbutton(self, attr, name): checkbutton = self._get_widget (name) checkbutton.set_active (self.stroker.config.__dict__[attr]) def toggled_cb(widget): self.stroker.config.__dict__[attr] = widget.get_active() self.stroker.config.emit('changed') checkbutton.connect('toggled', toggled_cb) def _connect_scale(self, attr, name): scale = self._get_widget(name) scale.set_value (self.stroker.config.__dict__[attr]) def value_changed_cb(widget): self.stroker.config.__dict__[attr] = widget.get_value() self.stroker.config.emit('changed') scale.connect('value-changed', value_changed_cb) def _connect_radio(self, attr, options): for name, value, sensitive_name in options: active = (value == self.stroker.config.__dict__[attr]) if sensitive_name: self._get_widget(sensitive_name).set_sensitive(active) if active: self._get_widget(name).set_active (True) def toggled_cb(widget, value, sensitive): if sensitive: sensitive.set_sensitive(widget.get_active()) if widget.get_active(): self.stroker.config.__dict__[attr] = value self.stroker.config.emit('changed') for name, value, sensitive_name in options: if sensitive_name: sensitive = self._get_widget(sensitive_name) else: sensitive = None self._get_widget(name).connect ('toggled', toggled_cb, value, sensitive) def __init__(self, stroker): self.stroker = stroker self.xml = gtk.glade.XML('stroker.glade') self._connect_checkbutton('update_immediately', 'update_checkbutton') self._connect_checkbutton('annotate', 'annotate_checkbutton') self._connect_radio('simplify_type', [ ('none_radiobutton', SIMPLIFY_NONE, None), ('fixed_radiobutton', SIMPLIFY_FIXED, 'fixed_alignment'), ('velocity_radiobutton', SIMPLIFY_VELOCITY, 'velocity_alignment') ]) self._connect_scale('fixed_threshold', 'threshold_scale') self._connect_scale('min_threshold', 'min_threshold_scale') self._connect_scale('base_threshold', 'base_threshold_scale') self._connect_scale('base_velocity', 'velocity_scale') min_scale = self._get_widget ('min_threshold_scale') base_scale = self._get_widget ('base_threshold_scale') def make_below_cb(widget, other): if widget.get_value() > other.get_value(): other.set_value(widget.get_value()) def make_above_cb(widget, other): if widget.get_value() < other.get_value(): other.set_value(widget.get_value()) min_scale.connect ('value-changed', make_below_cb, base_scale) base_scale.connect ('value-changed', make_above_cb, min_scale) self._connect_radio('join_type', [ ('angle_radiobutton', JOIN_ANGLE, 'angle_hbox'), ('curvature_radiobutton', JOIN_CURVATURE, 'curvature_hbox') ]) self._connect_scale('join_angle', 'angle_scale') self._connect_scale('curvature_multiplier', 'multiplier_scale') window = self.xml.get_widget('options_window') window.show() gobject.type_register(Stroker) window = gtk.Window(gtk.WINDOW_TOPLEVEL) window.set_default_size (500, 500) vbox = gtk.VBox(False, 0) window.add(vbox) stroker = Stroker() vbox.pack_start(stroker, True, True, 0) button = gtk.Button("Clear") def do_clear(button): stroker.clear() button.connect('clicked', do_clear) vbox.pack_start(button, False, False, 0) window.show_all() window.connect ("destroy", gtk.main_quit) options = StrokerOptions(stroker) gtk.main ()