diff --git a/cross.py b/cross.py new file mode 100644 index 0000000..4e69c8d --- /dev/null +++ b/cross.py @@ -0,0 +1,200 @@ +import math +from kivy.graphics import Color,Line,Rectangle +from kivy.uix.widget import Widget + +from point import Point + +class Cross(): + # size of central square + size=50 + + def __init__(self,x,y,**kwargs): + self.pos=Point(x,y) + self.color=kwargs.get("color",(0,0,1)) + self.selected=False + + # set position + def setpos(self,x,y): + self.pos.x=x + self.pos.y=y + + def draw(self): + # fill + #if not self.selected: + # Color(*(self.color)) + #else: + # Color(1,0,0) + Color(*(self.color)) + Rectangle(pos=(self.pos.x-self.size*1.5,self.pos.y-self.size*0.5),size=(3*self.size,self.size)) + Rectangle(pos=(self.pos.x-self.size*0.5,self.pos.y-self.size*1.5),size=(self.size,3*self.size)) + + # stroke + Color(1,1,1) + Line(points=( + *(self.pos.x-self.size*0.5,self.pos.y-self.size*0.5), + *(self.pos.x-self.size*0.5,self.pos.y-self.size*1.5), + *(self.pos.x+self.size*0.5,self.pos.y-self.size*1.5), + *(self.pos.x+self.size*0.5,self.pos.y-self.size*0.5), + *(self.pos.x+self.size*1.5,self.pos.y-self.size*0.5), + *(self.pos.x+self.size*1.5,self.pos.y+self.size*0.5), + *(self.pos.x+self.size*0.5,self.pos.y+self.size*0.5), + *(self.pos.x+self.size*0.5,self.pos.y+self.size*1.5), + *(self.pos.x-self.size*0.5,self.pos.y+self.size*1.5), + *(self.pos.x-self.size*0.5,self.pos.y+self.size*0.5), + *(self.pos.x-self.size*1.5,self.pos.y+self.size*0.5), + *(self.pos.x-self.size*1.5,self.pos.y-self.size*0.5), + *(self.pos.x-self.size*0.5,self.pos.y-self.size*0.5), + )) + + # check whether a cross at pos interacts with cross + def check_interaction(self,pos): + return ((pos-self.pos)/self.size).int()**2>=5 + + # find position along a line that comes in contact with the line going through pos in direction v + def move_on_line_to_stick(self,pos,v): + # relative to cross + return self.move_on_line_to_stick_relative((pos-self.pos)/self.size,v/self.size)*self.size+self.pos + def move_on_line_to_stick_relative(self,x,v): + + # if x is in the right quadrant + if abs(x.y)<=x.x: + # find all stuck positions on lines + stuck=[] + + # check intersections with vertical lines + if v.x!=0: + for i in range(1,4): + # candidate + y=Point(i,x.y+(i-x.x)*v.y/v.x) + # check that it is in the right range + if abs(y.y)<=4-i and abs(y.y)>=3-i: + stuck.append(y) + # check intersections with horizontal lines + if v.y!=0: + for i in [-3,-2,-1,1,2,3]: + # candidate + y=Point(x.x+(i-x.y)*v.x/v.y,i) + # check that it is in the right range + if y.x<=4-abs(i) and y.x>=3-abs(i): + stuck.append(y) + + return x.closest(stuck) + + # reflect other quadrants to the right one + # top quadrant + elif abs(x.x)<=x.y: + closest=self.move_on_line_to_stick_relative(Point(x.y,x.x),Point(v.y,v.x)) + return Point(closest.y,closest.x) + # bottom quadrant + elif abs(x.x)<=-x.y: + closest=self.move_on_line_to_stick_relative(Point(-x.y,x.x),Point(-v.y,v.x)) + return Point(closest.y,-closest.x) + # left quadrant + else: + closest=self.move_on_line_to_stick_relative(Point(-x.x,x.y),Point(-v.x,v.y)) + return Point(-closest.x,closest.y) + + +# L_infinity distance rescalled by 3 in the x direction +def cross_distx(x,y): + return max(abs(x.x-y.x)/3,abs(x.y-y.y)) +# L_infinity distance rescalled by 3 in the y direction +def cross_disty(x,y): + return max(abs(x.x-y.x),abs(x.y-y.y)/3) + +# polar description of touching cross +def cross_polar(t): + # by symmetry, put angle in interval (-pi/4,pi/4), and take absolute value + tt=abs((t+math.pi/4)%(math.pi/2)-math.pi/4) + if tt: +# canvas: +# Rectangle: +# pos: 0,0 +# size: self.width,self.height diff --git a/main.py b/main.py index d34b69f..f22cccb 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,9 @@ -import math from kivy.app import App from kivy.uix.widget import Widget -from kivy.graphics import Color,Line,Rectangle from kivy.config import Config +from cross import Cross,Cross_painter + # App class class Jam_app(App): # name of .kv file for main interface @@ -15,245 +15,6 @@ class Jam_app(App): parent.add_widget(self.cross_painter) return parent -# cross painter -class Cross_painter(Widget): - - def __init__(self,**kwargs): - - # list of crosses - self.crosses=[] - - # selected cross - self.selected=None - - # init Widget - super(Cross_painter,self).__init__(**kwargs) - - - # draw all crosses - def draw(self): - with self.canvas: - for cross in self.crosses: - cross.draw() - - - # respond to mouse down - def on_touch_down(self,touch): - # create new cross - if touch.button=="right": - if self.check_add(Point(touch.x,touch.y)): - new=Cross(touch.x,touch.y) - with self.canvas: - new.draw() - # add to list - self.crosses.append(new) - - # select cross - if touch.button=="left": - # unselect - if self.selected!=None: - self.selected.selected=False - - # find cross under touch - self.selected=self.find_cross(Point(touch.x,touch.y)) - # select - if self.selected!=None: - self.selected.selected=True - - - # respond to drag - def on_touch_move(self,touch): - # only move on left click - if touch.button=="left" and self.selected!=None: - self.selected.pos=self.check_move(Point(touch.x,touch.y),self.selected) - # redraw - self.canvas.clear() - self.draw() - - - # find the cross at position pos - def find_cross(self,pos): - for cross in self.crosses: - if cross_distx(pos,cross.pos)<=cross.size/2 or cross_disty(pos,cross.pos)<=cross.size/2: - return cross - # none found - return None - - # check that a cross can move to new position - def check_move(self,newpos,cross): - for other in self.crosses: - # do not compare a cross to itself - if other!=cross: - if other.check_interaction(newpos)==False: - # stick to the cross - return other.move_on_line_to_stick(cross.pos,newpos-cross.pos) - return newpos - - # check that a cross can be added at position - def check_add(self,pos): - for cross in self.crosses: - if cross.check_interaction(pos)==False: - return False - return True - -# cross -class Cross(): - # size of central square - size=50 - - def __init__(self,x,y,**kwargs): - self.pos=Point(x,y) - self.color=kwargs.get("color",(0,0,1)) - self.selected=False - - # set position - def setpos(self,x,y): - self.pos.x=x - self.pos.y=y - - def draw(self): - # fill - #if not self.selected: - # Color(*(self.color)) - #else: - # Color(1,0,0) - Color(*(self.color)) - Rectangle(pos=(self.pos.x-self.size*1.5,self.pos.y-self.size*0.5),size=(3*self.size,self.size)) - Rectangle(pos=(self.pos.x-self.size*0.5,self.pos.y-self.size*1.5),size=(self.size,3*self.size)) - - # stroke - Color(1,1,1) - Line(points=( - *(self.pos.x-self.size*0.5,self.pos.y-self.size*0.5), - *(self.pos.x-self.size*0.5,self.pos.y-self.size*1.5), - *(self.pos.x+self.size*0.5,self.pos.y-self.size*1.5), - *(self.pos.x+self.size*0.5,self.pos.y-self.size*0.5), - *(self.pos.x+self.size*1.5,self.pos.y-self.size*0.5), - *(self.pos.x+self.size*1.5,self.pos.y+self.size*0.5), - *(self.pos.x+self.size*0.5,self.pos.y+self.size*0.5), - *(self.pos.x+self.size*0.5,self.pos.y+self.size*1.5), - *(self.pos.x-self.size*0.5,self.pos.y+self.size*1.5), - *(self.pos.x-self.size*0.5,self.pos.y+self.size*0.5), - *(self.pos.x-self.size*1.5,self.pos.y+self.size*0.5), - *(self.pos.x-self.size*1.5,self.pos.y-self.size*0.5), - *(self.pos.x-self.size*0.5,self.pos.y-self.size*0.5), - )) - - # check whether a cross at pos interacts with cross - def check_interaction(self,pos): - return ((pos-self.pos)/self.size).int()**2>=5 - - # find position along a line that comes in contact with the line going through pos in direction v - def move_on_line_to_stick(self,pos,v): - # relative to cross - return self.move_on_line_to_stick_relative((pos-self.pos)/self.size,v/self.size)*self.size+self.pos - def move_on_line_to_stick_relative(self,x,v): - - # if x is in the right quadrant - if abs(x.y)<=x.x: - # find all stuck positions on lines - stuck=[] - - # check intersections with vertical lines - if v.x!=0: - for i in range(1,4): - # candidate - y=Point(i,x.y+(i-x.x)*v.y/v.x) - # check that it is in the right range - if abs(y.y)<=4-i and abs(y.y)>=3-i: - stuck.append(y) - # check intersections with horizontal lines - if v.y!=0: - for i in [-3,-2,-1,1,2,3]: - # candidate - y=Point(x.x+(i-x.y)*v.x/v.y,i) - # check that it is in the right range - if y.x<=4-abs(i) and y.x>=3-abs(i): - stuck.append(y) - - return x.closest(stuck) - - # reflect other quadrants to the right one - # top quadrant - elif abs(x.x)<=x.y: - closest=self.move_on_line_to_stick_relative(Point(x.y,x.x),Point(v.y,v.x)) - return Point(closest.y,closest.x) - # bottom quadrant - elif abs(x.x)<=-x.y: - closest=self.move_on_line_to_stick_relative(Point(-x.y,x.x),Point(-v.y,v.x)) - return Point(closest.y,-closest.x) - # left quadrant - else: - closest=self.move_on_line_to_stick_relative(Point(-x.x,x.y),Point(-v.x,v.y)) - return Point(-closest.x,closest.y) - - -class Point: - def __init__(self,x,y): - self.x=x - self.y=y - - def __add__(self,point): - return Point(self.x+point.x,self.y+point.y) - - def __sub__(self,point): - return Point(self.x-point.x,self.y-point.y) - - def __neg__(self): - return Point(-self.x,-self.y) - - def __mul__(self,a): - return Point(a*self.x,a*self.y) - - def __truediv__(self,a): - return Point(self.x/a,self.y/a) - - def __pow__(self,a): - if a==2: - return self.dot(self) - - # dot product - def dot(self,x): - return self.x*x.x+self.y*x.y - - # integer part - def int(self): - return Point(int(self.x),int(self.y)) - - # find the closest among a list of points - def closest(self,points): - dist=math.inf - closest=None - for point in points: - if (self-point)**2=0: - return 1 - return -1 # disable red circles on right click diff --git a/point.py b/point.py new file mode 100644 index 0000000..60ef0f3 --- /dev/null +++ b/point.py @@ -0,0 +1,45 @@ +import math + +# point in two dimensions +class Point: + def __init__(self,x,y): + self.x=x + self.y=y + + def __add__(self,point): + return Point(self.x+point.x,self.y+point.y) + + def __sub__(self,point): + return Point(self.x-point.x,self.y-point.y) + + def __neg__(self): + return Point(-self.x,-self.y) + + def __mul__(self,a): + return Point(a*self.x,a*self.y) + + def __truediv__(self,a): + return Point(self.x/a,self.y/a) + + def __pow__(self,a): + if a==2: + return self.dot(self) + + # dot product + def dot(self,x): + return self.x*x.x+self.y*x.y + + # integer part + def int(self): + return Point(int(self.x),int(self.y)) + + # find the closest among a list of points + def closest(self,points): + dist=math.inf + closest=None + for point in points: + if (self-point)**2=0: + return 1 + return -1