From: NeilBrown Date: Sat, 21 Apr 2012 10:48:01 +0000 (+1000) Subject: Add scrawl.py X-Git-Url: http://git.neil.brown.name/?a=commitdiff_plain;h=14c7be0d70f95e309205c3ebd96b3eaa7afe242a;p=plato.git Add scrawl.py 'scrawl' converts mouse/finger movements into letters and numbers. Signed-off-by: NeilBrown --- diff --git a/lib/scrawl.py b/lib/scrawl.py new file mode 100644 index 0000000..098a6e1 --- /dev/null +++ b/lib/scrawl.py @@ -0,0 +1,1111 @@ +#! /usr/bin/env python + +# Copyright (C) 2011-2012 Neil Brown +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#TODO +# - Check combinations of not-enabled + +# "Scrawl" is a module for processing mouse movements and converting +# them to ascii characters. +# +# Given a widget-window it collects mouse events and reports them +# as some of the following: +# 'tap' - press/release with minimal movement is a tap +# 'sym' - press-draw-release is interpreted as a sym where possible +# 'drag' - press-hold-move is interpretted as a drag. A callout provides +# start and current points, and a 'finish' signal +# 'select' press-draw-hold is interpretted as a selection. On the final +# release, a list of points is passed to the callout +# +# Each of these can be disabled by clearing the relevant '*call' handler +# If 'sym' is None, it is passed to 'select' +# If 'drag' is none, an initial hold is ignored +# + +import gtk, gobject, time, math + +class Scrawl: + def __init__(self, win, sym = None, tap=None, drag=None, select=None): + self.window = win + self.symcall = sym + self.tapcall = tap + self.dragcall = drag + self.selectcall = select + self.collectcall = None + + self.dragging = None + self.selecting = None + self.dragtime = 500 + self.selecttime = 500 + self.timer = None + + self.line = None + self.colour = None + + self.dict = Dictionary() + LoadDict(self.dict) + + if win: + win.add_events(gtk.gdk.POINTER_MOTION_MASK + | gtk.gdk.BUTTON_PRESS_MASK + | gtk.gdk.BUTTON_RELEASE_MASK + ) + + self.presshan = win.connect("button_press_event", self.press) + self.releasehan = win.connect("button_release_event", self.release) + self.motionhan = win.connect("motion_notify_event", self.motion) + + def press(self, c, ev): + # Start new line + c.stop_emission("button_press_event") + self.first_point(int(ev.x), int(ev.y)) + self.dragging = False + self.selecting = False + if self.timer: + gobject.source_remove(self.timer) + if self.dragcall: + self.timer = gobject.timeout_add(self.dragtime, self.set_drag) + if self.collectcall: + self.collectcall(None, None, None) + def first_point(self, x, y): + self.line = [ [x, y] ] + self.bbox = BBox(Point(x,y)) + + def set_drag(self): + self.dragging = True + self.dragcall(self.line[0], self.line[-1], False) + def set_select(self): + if self.selectcall: + self.selecting = True + self.selectcall(self.line, False) + + def release(self, c, ev): + c.stop_emission("button_release_event") + if self.timer: + gobject.source_remove(self.timer) + self.timer = None + + line = self.line + self.line = None + if line == None: + return + if self.dragging: + if len(line) == 1: + # this must be treated like a select, but close the drag + self.dragcall(line[0], line[0], True) + if self.selectcall: + self.selectcall(line, False) + self.selectcall(line, True) + elif self.tapcall: + c.handler_block(self.presshan) + c.handler_block(self.releasehan) + self.tapcall(line[0]) + c.handler_unblock(self.presshan) + c.handler_unblock(self.releasehan) + return + self.dragcall(line[0], line[-1], True) + return + + # send an expose event to clean up the line + bb = self.bbox + if self.window: + self.window.window.invalidate_rect( + gtk.gdk.Rectangle(bb.minx, bb.miny, + bb.width()+1, bb.height()+1), + True) + + if len(line) == 1 and self.tapcall: + c.handler_block(self.presshan) + c.handler_block(self.releasehan) + self.tapcall(line[0]) + c.handler_unblock(self.presshan) + c.handler_unblock(self.releasehan) + return + if self.selectcall and self.selecting: + self.selectcall(line, True) + return + + if not self.symcall: + if self.selectcall: + self.selectcall(line) + return + + alloc = self.window.get_allocation() + self.line = line + sym = self.last_point(int(ev.x), int(ev.y), + alloc.width, alloc.height) + + if self.collectcall: + self.collectcall(line, p, self.bbox) + if sym: + self.symcall(sym) + + def last_point(self, x, y, width, height): + # look for a symbol match + pagebb = BBox(Point(0,0)) + pagebb.add(Point(width, height)) + pagebb.finish(div = 2) + + line = self.line + self.line = None + if len(line) < 2: + return None + + p = PPath(line[0][0], line[0][1]) + for pp in line[1:]: + p.add(pp[0], pp[1]) + p.close() + patn = p.text() + pos = pagebb.relpos(p.bbox) + tpos = "mid" + if pos < 3: + tpos = "top" + if pos >= 6: + tpos = "bot" + sym = self.dict.match(patn, tpos) + if sym == None: + print "Failed to match pattern:", patn + return sym + + def motion(self, c, ev): + if self.line: + if ev.is_hint: + x, y, state = ev.window.get_pointer() + else: + x = ev.x + y = ev.y + x = int(x) + y = int(y) + + if not self.next_point(x,y): + return + + if self.timer: + gobject.source_remove(self.timer) + self.timer = None + if self.colour: + prev = self.line[-2] + c.window.draw_line(self.colour, prev[0], prev[1], x, y) + if self.dragging: + self.dragcall(self.line[0], self.line[-1], False) + else: + if not self.symcall: + if self.selectcall: + self.selecting = True + if self.selecting: + self.selectcall(self.line, False) + else: + self.timer = gobject.timeout_add(self.selecttime, self.set_select) + + def next_point(self, x, y): + if self.line: + self.bbox.add(Point(x,y)) + prev = self.line[-1] + if abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10: + return False + self.line.append([x,y]) + return True + return False + + def set_colour(self, col): + if type(col) == str: + c = gtk.gdk.color_parse(col) + gc = self.window.window.new_gc() + gc.set_foreground(self.window.get_colormap().alloc_color(c)) + self.colour = gc + else: + self.colour = col + + def set_collect(self, collect): + self.collectcall = collect + +def LoadDict(dict): + # Upper case. + # Where they are like lowercase, we either double + # the last stroke (L, J, I) or draw backwards (S, Z, X) + # U V are a special case + + dict.add('A', "R(4)6,8") + dict.add('B', "R(4)6,4.R(7)1,6") + dict.add('B', "R(4)6,4.L(4)2,8.R(7)1,6") + dict.add('B', "S(6)7,1.R(4)6,4.R(7)0,6") + dict.add('C', "R(4)8,2") + dict.add('D', "R(4)6,6") + dict.add('E', "L(1)2,8.L(7)2,8") + dict.add('E', "L(1)2,8.R(4)0,6.L(7)2,8") + # double the stem for F + dict.add('F', "L(4)2,6.S(3)7,1") + dict.add('F', "L(4)2,6.S(3)1,1") + dict.add('F', "S(1)5,3.S(3)1,7.S(3)7,1") + + dict.add('G', "L(4)2,5.S(8)1,7") + dict.add('G', "L(4)2,5.R(8)6,8") + # FIXME I need better straight-curve alignment + dict.add('H', "S(3)1,7.R(7)6,8.S(5)7,1") + dict.add('H', "L(3)0,5.R(7)6,8.S(5)7,1") + # capital I is down/up + dict.add('I', "S(4)1,7.S(4)7,1") + + # Capital J has a left/right tail + dict.add('J', "R(4)2,6.S(7)3,5") + + dict.add('K', "L(3)0,2.L(7)2,8") + dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8") + dict.add('K', "S(3)1,7.S(4)6,2.L(5)1,8") + + # Capital L, like J, doubles the foot + dict.add('L', "L(4)0,8.S(7)5,3") + + dict.add('M', "R(3)6,5.R(5)3,8") + dict.add('M', "R(3)6,5.L(1)0,2.R(5)3,8") + + dict.add('N', "R(3)6,8.L(5)0,2") + + # Capital O is CW, but can be CCW in special dict + dict.add('O', "R(4)1,1", bot='0') + + dict.add('P', "R(4)6,3") + dict.add('Q', "R(4)7,7.S(8)0,8") + + dict.add('R', "R(4)6,4.S(8)0,8") + + # S is drawn bottom to top. + dict.add('S', "L(7)6,1.R(1)7,2") + + # Double the stem for capital T + dict.add('T', "R(4)0,8.S(5)7,1") + + # U is L to R, V is R to L for now + dict.add('U', "L(4)0,2") + # backwards U with extra stroke + dict.add('V', "R(4)2,0.S(3)0,8") + # If extra stroke look more like a loop + dict.add('V', "R(4)2,0.S(3)8,8") + + dict.add('W', "R(5)2,3.L(7)8,6.R(3)5,0") + dict.add('W', "R(5)2,3.R(3)5,0") + + dict.add('X', "R(4)6,0") + + dict.add('Y',"L(1)0,2.R(5)4,6.S(5)6,2") + dict.add('Y',"L(1)0,2.S(5)2,7.S(5)7,2") + + dict.add('Z', "R(4)8,2.L(4)6,0") + + # Lower case + dict.add('a', "L(4)2,2.L(5)1,7") + dict.add('a', "L(4)2,2.L(5)0,8") + dict.add('a', "L(4)2,2.S(5)0,8") + dict.add('b', "S(3)1,7.R(7)6,3") + dict.add('c', "L(4)2,8", top='C') + dict.add('d', "L(4)5,2.S(5)1,7") + dict.add('d', "L(4)5,2.L(5)0,8") + dict.add('e', "S(4)3,5.L(4)5,8") + dict.add('e', "L(4)3,8") + dict.add('f', "L(4)2,6", top='F') + dict.add('f', "S(1)5,3.S(3)1,7", top='F') + dict.add('g', "L(1)2,2.R(4)1,6") + dict.add('h', "S(3)1,7.R(7)6,8") + dict.add('h', "L(3)0,5.R(7)6,8") + dict.add('i', "S(4)1,7", top='I', bot='1') + dict.add('j', "R(4)2,6", top='J') + dict.add('k', "L(3)0,5.L(7)2,8") + dict.add('k', "L(4)0,5.R(7)6,6.L(7)1,8") + dict.add('k', "S(3)1,7.S(6)6,2.L(7)1,8") + dict.add('l', "L(4)0,8", top='L') + dict.add('l', "S(3)1,7.S(7)3,5", top='L') + dict.add('m', "S(3)1,7.R(3)6,8.R(5)6,8") + dict.add('m', "S(3)1,7.R(4)6,5.L(4)0,2.R(5)6,8") + dict.add('m', "L(3)0,2.R(3)6,8.R(5)6,8") + dict.add('n', "S(3)1,7.R(4)6,8") + dict.add('o', "L(4)1,1", top='O', bot='0') + dict.add('p', "S(3)1,7.R(4)6,3") + dict.add('q', "L(1)2,2.L(5)1,5") + dict.add('q', "L(1)2,2.S(5)1,7.S(8)6,2") + dict.add('q', "L(1)2,2.S(5)1,7.S(5)1,7") + # FIXME this double 1,7 is due to a gentle where the + # second looks like a line because it is narrow.?? + dict.add('r', "S(3)1,7.R(4)6,2") + dict.add('s', "L(1)2,7.R(7)1,6", top='S', bot='5') + dict.add('t', "R(4)0,8", top='T', bot='7') + dict.add('t', "S(1)3,5.S(5)1,7", top='T', bot='7') + dict.add('u', "L(4)0,2.S(5)1,7") + dict.add('v', "R(4)2,0") + dict.add('v', "L(3)0,2.S(2)3,5") + dict.add('v', "L(3)0,2.R(5)6,2") + dict.add('v', "L(3)0,2.L(2)0,2") + dict.add('w', "L(3)0,2.L(5)0,2", top='W') + dict.add('w', "L(3)0,5.R(7)6,8.L(5)3,2", top='W') + dict.add('w', "L(3)0,5.L(5)3,2", top='W') + dict.add('x', "L(4)0,6", top='X') + dict.add('y', "L(1)0,2.R(5)4,6", top='Y') # if curved + dict.add('y', "L(1)0,2.S(5)2,7", top='Y') + dict.add('z', "R(4)0,6.L(4)2,8", top='Z', bot='2') + + # Digits + dict.add('0', "L(4)7,7") + dict.add('0', "R(4)7,7") + dict.add('1', "S(4)7,1") + dict.add('2', "R(4)0,6.S(7)3,5") + dict.add('2', "R(4)3,6.L(4)2,8") + dict.add('3', "R(1)0,6.R(7)1,6") + dict.add('4', "L(4)7,5") + dict.add('5', "L(1)2,6.R(7)0,3") + dict.add('5', "L(1)2,6.L(4)0,8.R(7)0,3") + dict.add('6', "L(4)2,3") + dict.add('7', "S(1)3,5.R(4)1,6") + dict.add('7', "R(4)0,6") + dict.add('7', "R(4)0,7") + dict.add('8', "L(1)2,7.R(4)4,2") + dict.add('8', "L(1)2,7.R(4)0,2") + dict.add('8', "L(5)2,8.R(4)1,2") + dict.add('8', "L(4)2,8.R(4)4,2.L(3)6,1") + dict.add('8', "L(4)2,8.R(4)0,2.L(3)6,1") + dict.add('8', "L(1)2,8.R(7)2,0.L(1)6,1") + dict.add('8', "L(0)2,6.R(7)0,1.L(2)6,0") + dict.add('8', "R(4)2,6.L(4)4,2.R(5)8,1") + dict.add('9', "L(1)2,2.S(5)1,7") + + dict.add(' ', "S(4)3,5") + dict.add('', "S(4)5,3") + dict.add('-', "S(4)3,5.S(4)5,3") + dict.add('_', "S(4)3,5.S(4)5,3.S(4)3,5") + dict.add("", "S(4)5,3.S(3)3,5") + dict.add("","S(4)3,5.S(5)5,3") + dict.add("", "S(4)7,1.S(1)1,7") # "" + dict.add("","S(4)1,7.S(7)7,1") # "" + dict.add("", "S(4)2,6") + dict.add("", "S(4)8,0") + + # cw loop from BR is period + dict.add(".", "R(4)8,8") + # reverse is ":" + dict.add(":", "L(4)8,8") + # ccw from LR is , - reverse is ';' + dict.add(",", "L(4)2,2") + dict.add(";", "R(4)2,2") + + # slash - stoke down and back up + dict.add("/", "S(4)2,6.S(4)6,2") + dict.add("\\", "S(4)8,0.S(4)0,8") + dict.add("@", "L(4)2,2.L(4)5,5") # needs work + dict.add("@", "L(4)2,5.S(5)0,8.L(4)5,8") + ##dict.add("#", "L(3)0,2.R(3)6,8.L(6)8,0.S(7)6,2.S(5)5,3") + # $ is S with up/down stroke + dict.add("$", "L(1)2,8.R(4)3,1.S(4)1,7") + dict.add("$", "S(1)2,6.R(4)0,1.S(4)1,7") + dict.add("$", "L(4)2,6.R(4)0,7") + # ^ is A and reverse + dict.add("^", "R(4)6,8.L(4)8,6") + dict.add("^", "R(4)6,8.L(4)1,6") + dict.add("&", "R(4)8,6.L(4)1,5") + # * is a 5-point star, start at top - no different from 'o'!! + # Maybe 4 points with concave joins. + dict.add("*", "R(0)2,6.R(6)0,8.R(8)6,2.S(2)8,0") + # % ccw, down, cw + dict.add("%", "L(1)2,5.S(4)2,6.R(7)6,6") + dict.add("%", "L(1)2,5.R(4)3,6.R(7)6,6") + # < - concave curves + dict.add("<", "R(1)2,6.R(7)0,8") + dict.add(">", "L(1)0,8.L(7)2,6") + # = - dash above dash ?? - too many options... + dict.add("=", "S(1)3,5.S(1)5,3.R(4)0,6.S(7)4,5") + dict.add("=", "S(1)3,4.L(4)2,8.R(4)0,6.S(7)5,5") + dict.add("=", "S(1)3,4.L(4)2,8.R(4)0,6.S(7)3,5") + dict.add("=", "S(1)3,4.L(4)2,8.L(4)8,3.S(7)3,5") + # + - backwards 4 ... too similar to 0 or D + # try a concave join down, up-left, across + dict.add("+", "S(4)1,7.L(6)8,0.S(4)3,5") + # ( [ { } ] ) .... too hard + # "!" is like L but the foot extends both sides. + dict.add("!", "L(5)0,8.S(7)5,3") + # .. or in other directions + dict.add("!", "R(3)2,6.S(7)3,5") + # "?" like "!" starting like '7' + dict.add("?", "R(4)0,7.L(8)1,8.S(7)5,3") + + # ' is a 'j' and back up again + dict.add("'", "R(4)2,6.L(4)6,2") + dict.add("'", "R(4)2,6.L(4)2,1") + dict.add('"', "R(4)2,6.L(4)6,2.R(4)2,6") + + # '|' -- not i or I or ! but '|' + # up/down + dict.add("|", "S(4)7,1.S(4)1,7") + +class DictSegment: + # Each segment has for elements: + # direction: Right Straight Left (R=cw, L=ccw) + # location: 0-8. + # start: 0-8 + # finish: 0-8 + # Segments match if the difference at each element + # is 0, 1, or 3 (RSL coded as 012) + # A difference of 1 requires both to be same / 3 + # On a match, return number of 0s + # On non-match, return -1 + def __init__(self, str): + # D(L)S,R + # 0123456 + self.e = [0,0,0,0] + if len(str) != 7: + raise ValueError + if str[1] != '(' or str[3] != ')' or str[5] != ',': + raise ValueError + if str[0] == 'R': + self.e[0] = 0 + elif str[0] == 'L': + self.e[0] = 2 + elif str[0] == 'S': + self.e[0] = 1 + else: + raise ValueError + + self.e[1] = int(str[2]) + self.e[2] = int(str[4]) + self.e[3] = int(str[6]) + + def match(self, other): + cnt = 0 + for i in range(0,4): + diff = abs(self.e[i] - other.e[i]) + if diff == 0: + cnt += 1 + elif diff == 3: + pass + elif diff == 1 and (self.e[i]/3 == other.e[i]/3): + pass + else: + return -1 + return cnt + +class DictPattern: + # A Dict Pattern is a list of segments. + # A parsed pattern matches a dict pattern if + # there are the same nubmer of segments and they + # all match. The value of the match is the sum + # of the individual matches. + # A DictPattern is printed as segments joined by periods. + # + def __init__(self, str): + self.segs = map(DictSegment, str.split(".")) + def match(self,other): + if len(self.segs) != len(other.segs): + return -1 + cnt = 0 + for i in range(0,len(self.segs)): + m = self.segs[i].match(other.segs[i]) + if m < 0: + return m + cnt += m + return cnt + + +class Dictionary: + # The dictionary holds all the patterns for symbols and + # performs lookup + # Each pattern in the directionary can be associated + # with 3 symbols. One when drawn in middle of screen, + # one for top of screen, one for bottom. + # Often these will all be the same. + # This allows e.g. s, S and 5 to have the same pattern in different + # locations on the touchscreen. + # A match requires a unique entry with a match that is better + # than any other entry. + # + def __init__(self): + self.dict = [] + def add(self, sym, pat, top = None, bot = None): + if top == None: top = sym + if bot == None: bot = sym + self.dict.append((DictPattern(pat), sym, top, bot, pat)) + + def _match(self, p): + max = -1 + val = None + for (ptn, sym, top, bot, s) in self.dict: + cnt = ptn.match(p) + if cnt > max: + max = cnt + val = (sym, top, bot) + elif cnt == max and val != (sym, top, bot): + val = None + return val + + def match(self, str, pos = "mid"): + p = DictPattern(str) + m = self._match(p) + if m == None: + return m + (mid, top, bot) = self._match(p) + if pos == "top": return top + if pos == "bot": return bot + return mid + + def matches(self, str): + p = DictPattern(str) + max = -1 + m = [] + for (ptn, sym, top, bot, s) in self.dict: + cnt = ptn.match(p) + if cnt > max: + max = cnt + m = [] + if cnt >= 0 and cnt == max: + m.append((s,sym)) + return max, len(p.segs)*4, m + +class Point: + # This represents a point in the path and all the points leading + # up to it. It allows us to find the direction and curvature from + # one point to another + # We store x,y, and sum/cnt of points so far + # This allows us to find the mean of a set of points. In particular, + # given 2 points we can find the mean of all the points between which + # will be on the same side of the joining line as most of the points. + # This can tell us if the curve is clockwise or counterclockwise, or + # (nearly) straight. + def __init__(self,x,y): + x = int(x); y = int(y) + self.xsum = x + self.ysum = y + self.x = x + self.y = y + self.cnt = 1 + + def copy(self): + n = Point(0,0) + n.xsum = self.xsum + n.ysum = self.ysum + n.x = self.x + n.y = self.y + n.cnt = self.cnt + return n + + def add(self,x,y): + if self.x == x and self.y == y: + return + self.x = x + self.y = y + self.xsum += x + self.ysum += y + self.cnt += 1 + + def xlen(self,p): + return abs(self.x - p.x) + def ylen(self,p): + return abs(self.y - p.y) + def sqlen(self,p): + x = self.x - p.x + y = self.y - p.y + return x*x + y*y + + def xdir(self,p): + if self.x > p.x: + return 1 + if self.x < p.x: + return -1 + return 0 + def ydir(self,p): + if self.y > p.y: + return 1 + if self.y < p.y: + return -1 + return 0 + + # We calculate the 'curve' by measuring the + # distance of the mean point from the line between + # start and end, and dividing by the length of the + # line. We don't bother taking sqroot so result is + # a square and is multiplied by 100 as we are using + # ints. + # We then desire if cw or ccw or s by comparing with + # arbitrary number '+/-6' + def curve(self,p): + if self.cnt == p.cnt: + return 0 + x1 = p.x ; y1 = p.y + (x2,y2) = self.meanpoint(p) + x3 = self.x; y3 = self.y + + curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1) + curve = curve * 100 / ((y3-y1)*(y3-y1) + + (x3-x1)*(x3-x1)) + if curve > 6: + return 1 + if curve < -6: + return -1 + return 0 + + # Vcurve is the raw curve number and is only used + # to see if a curve is getting tigher..... WHY? + def Vcurve(self,p): + if self.cnt == p.cnt: + return 0 + x1 = p.x ; y1 = p.y + (x2,y2) = self.meanpoint(p) + x3 = self.x; y3 = self.y + + curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1) + curve = curve * 100 / ((y3-y1)*(y3-y1) + + (x3-x1)*(x3-x1)) + return curve + + def meanpoint(self,p): + x = (self.xsum - p.xsum) / (self.cnt - p.cnt) + y = (self.ysum - p.ysum) / (self.cnt - p.cnt) + return (x,y) + + def is_sharp(self,A,C): + # Measure the cosine at self between A and C + # as A and C could be curve, we take the mean point on + # self.A and self.C as the points to find cosine between + # If the cosine is fairly close to 1 we assume there is + # a sharp turn at 'self' + (ax,ay) = self.meanpoint(A) + (cx,cy) = self.meanpoint(C) + a = ax-self.x; b=ay-self.y + c = cx-self.x; d=cy-self.y + x = a*c + b*d + y = a*d - b*c + h = math.sqrt(x*x+y*y) + if h > 0: + cs = x*1000/h + else: + cs = 0 + return (cs > 900) + + def slope(self, p): + # cannot divide safely, so return an x,y pair + return (self.x-p.x, self.y-p.y) + + def is_reversal(self,start, mid): + # self comes after start->mid. Is it a 'reversal' of direction. + # This means a change in direction of more than 45deg. + # The slope to mid is y/x + # The slopes at 45deg to this are -(x-y)/(x+y) and (x+y)/(x-y) + # we check if the slop from mid to self is not between those. + # The slope isn't complete info though. If the new direction is + # more than 135 degrees we get a compatible slope but a changed + # direction. In that case relative direction is reversed. + # We can check that if ax+by < 0 where slope at self is b/a + # + # return 0 if not a reversal +/- 45deg + # return 1 if single reversal: +-135deg + # return 2 if double reversal: >135deg + cnt = 0 + x,y = mid.slope(start) + new = self.slope(mid) + min = (x+y, -(x-y)) + max = (x-y, x+y) + if slope_lt(min, max): + if slope_lt(new, min) or slope_lt(max, new): + cnt = 1 + else: + if slope_lt(new, min) and slope_lt(max, new): + cnt = 1 + if new[0]*x + new[1]*y < 0: + # complete reversal + cnt = 2 - cnt + return cnt + +def slope_lt(a,b): + ax,ay = a + bx,by = b + # is ay/ax < by/bx - without dividing as they could be zero + want_lt = True + if ax < 0: + want_lt = not want_lt + if bx < 0: + want_lt = not want_lt + if ay * bx < by * ax: + return want_lt + else: + return not want_lt + +class BBox: + # a BBox records min/max x/y of some Points and + # can subsequently report row, column, pos of each point + # can also locate one bbox in another + + def __init__(self, p): + self.minx = p.x + self.maxx = p.x + self.miny = p.y + self.maxy = p.y + + def width(self): + return self.maxx - self.minx + def height(self): + return self.maxy - self.miny + + def add(self, p): + if p.x > self.maxx: + self.maxx = p.x + if p.x < self.minx: + self.minx = p.x + + if p.y > self.maxy: + self.maxy = p.y + if p.y < self.miny: + self.miny = p.y + def finish(self, div = 3): + # if aspect ratio is bad, we adjust max/min accordingly + # before setting [xy][12]. We don't change self.min/max + # as they are used to place stroke in bigger bbox. + # Normally divisions are at 1/3 and 2/3. They can be moved + # by setting div e.g. 2 = 1/2 and 1/2 + # This allows us to adjust to shape of drawing, but if very + # narrow we treat it a genuinely narrow. + (minx,miny,maxx,maxy) = (self.minx,self.miny,self.maxx,self.maxy) + if (maxx - minx) * 3 < (maxy - miny) * 2: + # too narrow - make it a square + mid = int((maxx + minx)/2) + halfwidth = int ((maxy - miny)/3) + minx = mid - halfwidth + maxx = mid + halfwidth + if (maxy - miny) * 3 < (maxx - minx) * 2: + # too wide - increase hight to make a square + mid = int((maxy + miny)/2) + halfheight = int ((maxx - minx)/3) + miny = mid - halfheight + maxy = mid + halfheight + + div1 = div - 1 + self.x1 = int((div1*minx + maxx)/div) + self.x2 = int((minx + div1*maxx)/div) + self.y1 = int((div1*miny + maxy)/div) + self.y2 = int((miny + div1*maxy)/div) + + def row(self, p): + # 0, 1, 2 - top to bottom + if p.y <= self.y1: + return 0 + if p.y < self.y2: + return 1 + return 2 + def col(self, p): + if p.x <= self.x1: + return 0 + if p.x < self.x2: + return 1 + return 2 + def box(self, p): + # 0 to 8 + return self.row(p) * 3 + self.col(p) + + def relpos(self,b): + # b is a box within self. find location 0-8 + if b.maxx < self.x2 and b.minx < self.x1: + x = 0 + elif b.minx > self.x1 and b.maxx > self.x2: + x = 2 + else: + x = 1 + if b.maxy < self.y2 and b.miny < self.y1: + y = 0 + elif b.miny > self.y1 and b.maxy > self.y2: + y = 2 + else: + y = 1 + return y*3 + x + + +# check if a list of curvatures are compatible. +# i.e. if those that aren't straight are either +# all cw or all ccw +def different(*args): + cur = 0 + for i in args: + if cur != 0 and i != 0 and cur != i: + return True + if cur == 0: + cur = i + return False + +# Given a set of non-different curvatures, find the +# overall curvature +def maxcurve(*args): + for i in args: + if i != 0: + return i + return 0 + +class PPath: + # a PPath refines a list of x,y points into a list of Points + # The Points mark out segments which end at significant Points + # such as inflections and reversals. + # In the first stage we try to find points where the path + # reverses direction or where curvature starts to decrease implying + # an inflection. Once we get 4 pixels beyond such a point and it appears + # stable, we confirm it and start looking for the next point. + # This gives a smaller list of Points that are reversals or inflections. + # We then analyse these short segments for curvature and gather groups + # with the same curvature an create a sectlist where each entry holds + # a curvature number, and a list of Points. It is possible that two + # adjacent entries share a Point if they have a gentle inflection + # between them. + def __init__(self, x,y): + + self.start = Point(x,y) + self.mid = Point(x,y) + self.curr = Point(x,y) + self.list = [ self.start ] + + def add(self, x, y): + self.curr.add(x,y) + + if ( self.curr.is_reversal(self.start, self.mid) or + (abs(self.curr.Vcurve(self.start))+2 < abs(self.mid.Vcurve(self.start)))): + pass + else: + self.mid = self.curr.copy() + + if self.curr.xlen(self.mid) > 4 or self.curr.ylen(self.mid) > 4: + self.start = self.mid.copy() + self.list.append(self.start) + self.mid = self.curr.copy() + + def close(self): + self.list.append(self.curr) + + def get_sectlist(self): + if len(self.list) < 2: + return [[0,self.list]] + l = [] + A = self.list[0] + B = self.list[1] + s = [A,B] + reversals = 0 + curcurve = B.curve(A) + for C in self.list[2:]: + cabc = C.curve(A) + cab = B.curve(A) + cbc = C.curve(B) + r = reversals + C.is_reversal(A,B) + reversals = 0 + if B.is_sharp(A,C) and not different(cabc, cab, cbc, curcurve): + # B is too pointy, must break here + l.append([curcurve, s]) + s = [B, C] + curcurve = cbc + elif not different(cabc, cab, cbc, curcurve) and r <= 4: + # all happy + reversals = r + s.append(C) + if curcurve == 0: + curcurve = maxcurve(cab, cbc, cabc) + elif not different(cabc, cab, cbc) : + # gentle inflection along AB + # was: AB goes in old and new section + # now: AB only in old section, but curcurve + # preseved. + l.append([curcurve,s]) + s = [A, B, C] + curcurve =maxcurve(cab, cbc, cabc) + else: + # Change of direction at B + l.append([curcurve,s]) + s = [B, C] + curcurve = cbc + + A = B + B = C + l.append([curcurve,s]) + + return l + + def remove_shorts(self, bbox): + # in self.list, if a point is close to the previous point, + # remove it. + # "close" is relative to overall size of path. If the square + # on the distance is less that a 1/4 of the bounding box, it is close. + # If the last point is close to it's previous point, we remove + # the second last point. + if len(self.list) <= 2: + return + w = bbox.width()/10 + h = bbox.height()/10 + n = [self.list[0]] + leng = w*h*2*2 + for p in self.list[1:-1]: + l = p.sqlen(n[-1]) + if l > leng: + n.append(p) + # and check the last one separately + p = self.list[-1] + l = p.sqlen(n[-1]) + if l < leng: + # too short - remove the second last + n.pop() + n.append(p) + self.list = n + + def text(self): + # OK, we have a list of points with curvature between. + # want to divide this into sections. + # for each 3 consectutive points ABC curve of ABC and AB and BC + # If all the same, they are all in a section. + # If not B starts a new section and the old ends on B or C... + # This produces the summary of the path - a number of segments joine + # with periods. + # Each segment has a curvature R L S and a location in the whole. + # It also has a start and end position relative to itself. + BB = BBox(self.list[0]) + for p in self.list: + BB.add(p) + BB.finish() + self.bbox = BB + self.remove_shorts(BB) + sectlist = self.get_sectlist() + t = "" + for c, s in sectlist: + if c > 0: + dr = "R" # clockwise is to the Right + elif c < 0: + dr = "L" # counterclockwise to the Left + else: + dr = "S" # straight + bb = BBox(s[0]) + for p in s: + bb.add(p) + bb.finish() + # If all points are in same row or column, then + # line is S - unless other changes direction + rwinc = False; rwdec = False + clinc = False; cldec = False + + rw = bb.row(s[0]); cl=bb.col(s[0]) + for p in s: + if bb.row(p) < rw: rwdec = True + if bb.row(p) > rw: rwinc = True + if bb.col(p) < cl: cldec = True + if bb.col(p) > cl: clinc = True + rw = bb.row(p) + cl = bb.col(p) + if (rwdec and rwinc) or (cldec and clinc): + # reversal so cannot be straight + pass + else: + if not (rwinc or rwdec) or not (clinc or cldec): + # one didn't change, other monotonic, so 'S' + dr = "S" + t1 = dr + t1 += "(%d)" % BB.relpos(bb) + t1 += "%d,%d" % (bb.box(s[0]), bb.box(s[-1])) + t += t1 + '.' + return t[:-1] + + +if __name__ == "__main__": + # test app for Scrawl. + # Create a window with a ListSelect and when a symbol is + # entered, select the first element with that letter + from listselect import ListSelect + + w = gtk.Window(gtk.WINDOW_TOPLEVEL) + w.connect("destroy", lambda w: gtk.main_quit()) + w.set_title("Scrawl Test") + w.show() + + v = gtk.VBox(); v.show() + w.add(v) + + s = ListSelect() + list = [ "The", "Quick", "Brown", "Fox", "jumps", "over", + "the", "lazy", "Dog"] + el = [] + for a in list: + el.append((a, ["blue", False, False, None, "white"])) + el[4] = (el[4][0], ["red",True,True,"black","white"]) + s.list = el + s.selected = 2 + v.pack_end(s, expand = True) + s.show() + + def sel(s, n): + print n, s.list[n], "selected" + s.connect('selected', sel) + + global sc + + def gotsym(sym): + global sc + print "got sym:", sym + if sym == '-': + s.set_zoom(s.zoom-1) + elif sym == '+': + s.set_zoom(s.zoom+1) + elif sym == '1': + sc.symcall = None + else: + for i in range(len(list)): + if list[i].lower().find(sym.lower()) >= 0: + print 'sel', i, list[i].lower(), sym.lower() + s.select(i) + break + + def gottap(p): + x,y = p + s.tap(x,y) + + global dragsource + dragsource = None + def gotdrag(start, here, done): + global dragsource + if dragsource == None: + dragsource = s.map_pos(start[0], start[1]) + if dragsource != None: + s.list[dragsource][1][0] = 'black' + s.item_changed(dragsource) + dragdest = s.map_pos(here[0], here[1]) + if dragsource != None and dragdest != None and dragsource != dragdest: + # swap and update dragsource + s.list[dragsource], s.list[dragdest] = \ + s.list[dragdest], s.list[dragsource] + list[dragsource], list[dragdest] = \ + list[dragdest], list[dragsource] + dragsource = dragdest + s.list_changed() + if done and dragsource != None: + s.list[dragsource][1][0] = 'blue' + s.item_changed(dragsource) + dragsource = None + + + def gotsel(line, done): + if not done: + # set background to yellow + for p in line: + ind = s.map_pos(p[0],p[1]) + if ind != None and s.list[ind][1][3] != 'yellow': + s.list[ind][1][3] = "yellow" + s.item_changed(ind) + else: + for e in s.list: + if e[1][3] == 'yellow': + e[1][3] = None + e[1][2] = not e[1][2] + s.list_changed() + + sc = Scrawl(s, gotsym, gottap, gotdrag, gotsel) + sc.set_colour('red') + print "Write letters to select word containing the letter" + print "click to select directly" + print "click-hold to draw words around" + print "drag-hold to select a range" + gtk.main()