diff --git a/src/element.py b/src/element.py new file mode 100644 index 0000000..f356c5a --- /dev/null +++ b/src/element.py @@ -0,0 +1,164 @@ +## elements that polyominoes are made of +import math +import sys + +from point import Point,l_infinity +from tools import isint_nonzero,sgn,in_interval + +# parent class of all elements +class Element(): + def __init__(self,x,y,size,**kwargs): + self.pos=Point(x,y) + self.size=size + + # set position + def setpos(self,x,y): + self.pos.x=x + self.pos.y=y + + + # override in each subclass + # check whether an element interacts with square + def check_interaction(self,element): + return False + + # override in each subclass + # whether x is in the support of the element + def in_support(self,x): + return False + + # override in each subclass + # check whether an element is touching self + def check_touch(self,element): + return False + + # override in each subclass + # find position along a line that comes in contact with the line going through element.pos in direction v + def move_on_line_to_stick(self,element,v): + return Point(0,0) + + # override in each subclass + # move along edge of element + def move_along(self,delta,element): + return element + + +# square elements +class Element_square(Element): + + # check whether an element interacts with square + def check_interaction(self,element): + # allow for error + return l_infinity(element.pos-self.pos)<(self.size+element.size)/2-1e-11 + + # whether x is in the support of the element + def in_support(self,x): + return l_infinity(self.pos-x)<=1/2 + + # check whether an element is touching self + def check_touch(self,element): + # allow for error + if in_interval(l_infinity(element.pos-self.pos),(self.size+element.size)/2-1e-11,(self.size+element.size)/2+1e-11): + return True + return False + + # find position along a line that comes in contact with the line going through element.pos in direction v + def move_on_line_to_stick(self,element,v): + # compute intersections with four lines making up square + if v.x!=0: + if v.y!=0: + intersections=[\ + Point(self.pos.x+(self.size+element.size)/2,element.pos.y+v.y/v.x*(self.pos.x+(self.size+element.size)/2-element.pos.x)),\ + Point(self.pos.x-(self.size+element.size)/2,element.pos.y+v.y/v.x*(self.pos.x-(self.size+element.size)/2-element.pos.x)),\ + Point(element.pos.x+v.x/v.y*(self.pos.y+(self.size+element.size)/2-element.pos.y),self.pos.y+(self.size+element.size)/2),\ + Point(element.pos.x+v.x/v.y*(self.pos.y-(self.size+element.size)/2-element.pos.y),self.pos.y-(self.size+element.size)/2)\ + ] + else: + intersections=[\ + Point(self.pos.x+(self.size+element.size)/2,element.pos.y+v.y/v.x*(self.pos.x+(self.size+element.size)/2-element.pos.x)),\ + Point(self.pos.x-(self.size+element.size)/2,element.pos.y+v.y/v.x*(self.pos.x-(self.size+element.size)/2-element.pos.x)) + ] + else: + if v.y!=0: + intersections=[\ + Point(element.pos.x+v.x/v.y*(self.pos.y+(self.size+element.size)/2-element.pos.y),self.pos.y+(self.size+element.size)/2),\ + Point(element.pos.x+v.x/v.y*(self.pos.y-(self.size+element.size)/2-element.pos.y),self.pos.y-(self.size+element.size)/2)\ + ] + else: + print("error: move_on_line_to_stick called with v=0, please file a bug report with the developer",file=sys.stderr) + exit(-1) + + # compute closest one, on square + closest=None + dist=math.inf + for i in range(0,len(intersections)): + # check that it is on square + if abs(intersections[i].x-self.pos.x)<=(self.size+element.size)/2+1e-11 and abs(intersections[i].y-self.pos.y)<=(self.size+element.size)/2+1e-11: + if (intersections[i]-element.pos)**20: + if relmath.ceil(rel/((self.size+element.size)/2))*((self.size+element.size)/2)+1e-11 and math.ceil(rel/((self.size+element.size)/2))*((self.size+element.size)/2)!=0: + # stick to corner + candidate.y=math.ceil(rel/((self.size+element.size)/2))*((self.size+element.size)/2)+self.pos.y-element.pos.y + else: + if rel>math.floor(rel/((self.size+element.size)/2))*((self.size+element.size)/2)+1e-11 and delta.y+rel0: + if relmath.ceil(rel/((self.size+element.size)/2))*((self.size+element.size)/2)+1e-11 and math.ceil(rel/((self.size+element.size)/2))*((self.size+element.size)/2)!=0: + # stick to corner + candidate.x=math.ceil(rel/((self.size+element.size)/2))*((self.size+element.size)/2)+self.pos.x-element.pos.x + else: + if rel>math.floor(rel/((self.size+element.size)/2))*((self.size+element.size)/2)+1e-11 and delta.x+rel0: - self.draw_grid(particle.squares[0].pos,particle.grid) + self.draw_grid(particle.elements[0].pos,particle.grid) for particle in self.particles: particle.draw(self,alpha=0.5) @@ -217,7 +216,7 @@ class Painter(Widget): new=Cross(touchx,touchy) # snap to lattice if self.lattice!=None: - new.move(self.lattice.nearest_delta(new.squares[0].pos)) + new.move(self.lattice.nearest_delta(new.elements[0].pos)) if not self.check_interaction_any(new,Point(0,0)): # add to list @@ -238,7 +237,7 @@ class Painter(Widget): # record relative position of click with respect to reference if self.undermouse!=None: - self.offset=Point(touchx,touchy)-self.undermouse.squares[0].pos + self.offset=Point(touchx,touchy)-self.undermouse.elements[0].pos # no modifiers if self.modifiers==[]: @@ -291,7 +290,7 @@ class Painter(Widget): # only move on left click if touch.button=="left" and self.modifiers==[] and self.undermouse!=None: # attempted move determined by the relative position to the relative position of click within self.undermouse - delta=self.adjust_move(Point(touchx,touchy)-(self.offset+self.undermouse.squares[0].pos),0) + delta=self.adjust_move(Point(touchx,touchy)-(self.offset+self.undermouse.elements[0].pos),0) # snap to lattice if self.lattice!=None: @@ -347,10 +346,10 @@ class Painter(Widget): # check whether a candidate particle element with any of the unselected particles def check_interaction_unselected_element(self,element,offset): for particle in self.unselected: - for square in particle.squares: + for elt in particle.elements: # add offset element.pos+=offset - if square.check_interaction(element): + if elt.check_interaction(element): # reset offset element.pos-=offset return True @@ -365,7 +364,7 @@ class Painter(Widget): # actual_delta is the smallest (componentwise) of all the computed delta's actual_delta=Point(math.inf,math.inf) for particle in self.selected: - for element in particle.squares: + for element in particle.elements: # compute adjustment move due to unselected obstacles adjusted_delta=self.adjust_move_element(delta,element,0) # only keep the smallest delta's (in absolute value) @@ -391,7 +390,7 @@ class Painter(Widget): # whether newpos is acceptable accept_newpos=True for other in self.unselected: - for obstacle in other.squares: + for obstacle in other.elements: # move would make element overlap with obstacle element.pos+=delta if obstacle.check_interaction(element): @@ -448,7 +447,7 @@ class Painter(Widget): for particle in self.particles: if type(particle)==Cross: ff.write("{:d};".format(CROSS_INDEX)) - ff.write("{:05.2f},{:05.2f};{:3.1f},{:3.1f},{:3.1f}\n".format(particle.squares[0].pos.x,particle.squares[0].pos.y,particle.color[0],particle.color[1],particle.color[2])) + ff.write("{:05.2f},{:05.2f};{:3.1f},{:3.1f},{:3.1f}\n".format(particle.elements[0].pos.x,particle.elements[0].pos.y,particle.color[0],particle.color[1],particle.color[2])) ff.close() # read configuration from file @@ -561,7 +560,7 @@ class Painter(Widget): for particle in self.particles: if type(particle)==Cross: ff.write("\cross{"+colors.closest_color(particle.color,colors.xcolor_names)+"}") - ff.write("{{({:05.2f},{:05.2f})}};\n".format(particle.squares[0].pos.x-self.particles[0].squares[0].pos.x,particle.squares[0].pos.y-self.particles[0].squares[0].pos.y)) + ff.write("{{({:05.2f},{:05.2f})}};\n".format(particle.elements[0].pos.x-self.particles[0].elements[0].pos.x,particle.elements[0].pos.y-self.particles[0].elements[0].pos.y)) ff.write("\\end{tikzpicture}\n") ff.write("\\end{document}\n") diff --git a/src/polyomino.py b/src/polyomino.py index 71ece63..2d5c550 100644 --- a/src/polyomino.py +++ b/src/polyomino.py @@ -1,15 +1,14 @@ -import math -import sys +# a polyomino is a collection of elements, defined in elements.py from kivy.graphics import Color,Line,Rectangle -from point import Point,l_infinity -from tools import isint_nonzero,sgn,in_interval +from point import l_infinity +from element import Element_square # parent class of all polyominos class Polyomino(): def __init__(self,**kwargs): - # square elements that make up the polyomino - self.squares=kwargs.get("squares",[]) + # elements that make up the polyomino + self.elements=kwargs.get("elements",[]) self.color=kwargs.get("color",(0,0,1)) self.selected=False @@ -28,8 +27,8 @@ class Polyomino(): # darken selected Color(r/2,g/2,b/2,alpha) - for square in self.squares: - Rectangle(pos=(painter.pos_tocoord_x(square.pos.x-0.5*square.size),painter.pos_tocoord_y(square.pos.y-0.5*square.size)),size=(square.size*painter.base_size,square.size*painter.base_size)) + for element in self.elements: + Rectangle(pos=(painter.pos_tocoord_x(element.pos.x-0.5*element.size),painter.pos_tocoord_y(element.pos.y-0.5*element.size)),size=(element.size*painter.base_size,element.size*painter.base_size)) # draw boundary self.stroke(painter) @@ -42,62 +41,62 @@ class Polyomino(): # white Color(1,1,1) - for square in self.squares: + for element in self.elements: Line(points=( - *(coordx-0.5*square.size*painter.base_size,coordy-0.5*square.size*painter.base_size), - *(coordx-0.5*square.size*painter.base_size,coordy+0.5*square.size*painter.base_size), - *(coordx+0.5*square.size*painter.base_size,coordy+0.5*square.size*painter.base_size), - *(coordx+0.5*square.size*painter.base_size,coordy-0.5*square.size*painter.base_size), - *(coordx-0.5*square.size*painter.base_size,coordy-0.5*square.size*painter.base_size) + *(coordx-0.5*element.size*painter.base_size,coordy-0.5*element.size*painter.base_size), + *(coordx-0.5*element.size*painter.base_size,coordy+0.5*element.size*painter.base_size), + *(coordx+0.5*element.size*painter.base_size,coordy+0.5*element.size*painter.base_size), + *(coordx+0.5*element.size*painter.base_size,coordy-0.5*element.size*painter.base_size), + *(coordx-0.5*element.size*painter.base_size,coordy-0.5*element.size*painter.base_size) )) # move by delta def move(self,delta): - for square in self.squares: - square.pos+=delta + for element in self.elements: + element.pos+=delta # whether x is in the support of the polyomino def in_support(self,x): - for square in self.squares: - if l_infinity(square.pos-x)<=1/2: + for element in self.elements: + if element.in_support(x): return True return False # check whether self interacts with candidate if candidate were moved by offset def check_interaction(self,candidate,offset): - for square1 in self.squares: - for square2 in candidate.squares: + for element1 in self.elements: + for element2 in candidate.elements: # add offset - square2.pos+=offset - if square1.check_interaction(square2): + element2.pos+=offset + if element1.check_interaction(element2): # reset offset - square2.pos-=offset + element2.pos-=offset return True # reset offset - square2.pos-=offset + element2.pos-=offset return False # square class Square(Polyomino): def __init__(self,x,y,**kwargs): - super(Square,self).__init__(**kwargs,squares=[Square_element(x,y,size=kwargs.get("size",1.0))]) + super(Square,self).__init__(**kwargs,elements=[Element_square(x,y,size=kwargs.get("size",1.0))]) # cross class Cross(Polyomino): def __init__(self,x,y,**kwargs): - super(Cross,self).__init__(**kwargs,squares=[\ - Square_element(x,y,1),\ - Square_element(x+1,y,1),\ - Square_element(x-1,y,1),\ - Square_element(x,y+1,1),\ - Square_element(x,y-1,1)\ + super(Cross,self).__init__(**kwargs,elements=[\ + Element_square(x,y,1),\ + Element_square(x+1,y,1),\ + Element_square(x-1,y,1),\ + Element_square(x,y+1,1),\ + Element_square(x,y-1,1)\ ]) - # redefine stroke to avoid lines between touching squares + # redefine stroke to avoid lines between touching elements def stroke(self,painter): # convert to graphical coordinates - coordx=painter.pos_tocoord_x(self.squares[0].pos.x) - coordy=painter.pos_tocoord_y(self.squares[0].pos.y) + coordx=painter.pos_tocoord_x(self.elements[0].pos.x) + coordy=painter.pos_tocoord_y(self.elements[0].pos.y) Color(1,1,1) Line(points=( @@ -116,130 +115,3 @@ class Cross(Polyomino): *(coordx-0.5*painter.base_size,coordy-0.5*painter.base_size), )) - - -# square building block of polyominos -class Square_element(): - - def __init__(self,x,y,size,**kwargs): - self.pos=Point(x,y) - self.size=size - - # set position - def setpos(self,x,y): - self.pos.x=x - self.pos.y=y - - - # check whether an element interacts with square - def check_interaction(self,element): - # allow for error - return l_infinity(element.pos-self.pos)<(self.size+element.size)/2-1e-11 - - # check whether an element is touching self - def check_touch(self,element): - # allow for error - if in_interval(l_infinity(element.pos-self.pos),(self.size+element.size)/2-1e-11,(self.size+element.size)/2+1e-11): - return True - return False - - # find position along a line that comes in contact with the line going through element.pos in direction v - def move_on_line_to_stick(self,element,v): - # compute intersections with four lines making up square - if v.x!=0: - if v.y!=0: - intersections=[\ - Point(self.pos.x+(self.size+element.size)/2,element.pos.y+v.y/v.x*(self.pos.x+(self.size+element.size)/2-element.pos.x)),\ - Point(self.pos.x-(self.size+element.size)/2,element.pos.y+v.y/v.x*(self.pos.x-(self.size+element.size)/2-element.pos.x)),\ - Point(element.pos.x+v.x/v.y*(self.pos.y+(self.size+element.size)/2-element.pos.y),self.pos.y+(self.size+element.size)/2),\ - Point(element.pos.x+v.x/v.y*(self.pos.y-(self.size+element.size)/2-element.pos.y),self.pos.y-(self.size+element.size)/2)\ - ] - else: - intersections=[\ - Point(self.pos.x+(self.size+element.size)/2,element.pos.y+v.y/v.x*(self.pos.x+(self.size+element.size)/2-element.pos.x)),\ - Point(self.pos.x-(self.size+element.size)/2,element.pos.y+v.y/v.x*(self.pos.x-(self.size+element.size)/2-element.pos.x)) - ] - else: - if v.y!=0: - intersections=[\ - Point(element.pos.x+v.x/v.y*(self.pos.y+(self.size+element.size)/2-element.pos.y),self.pos.y+(self.size+element.size)/2),\ - Point(element.pos.x+v.x/v.y*(self.pos.y-(self.size+element.size)/2-element.pos.y),self.pos.y-(self.size+element.size)/2)\ - ] - else: - print("error: move_on_line_to_stick called with v=0, please file a bug report with the developer",file=sys.stderr) - exit(-1) - - # compute closest one, on square - closest=None - dist=math.inf - for i in range(0,len(intersections)): - # check that it is on square - if abs(intersections[i].x-self.pos.x)<=(self.size+element.size)/2+1e-11 and abs(intersections[i].y-self.pos.y)<=(self.size+element.size)/2+1e-11: - if (intersections[i]-element.pos)**20: - if relmath.ceil(rel/((self.size+element.size)/2))*((self.size+element.size)/2)+1e-11 and math.ceil(rel/((self.size+element.size)/2))*((self.size+element.size)/2)!=0: - # stick to corner - candidate.y=math.ceil(rel/((self.size+element.size)/2))*((self.size+element.size)/2)+self.pos.y-element.pos.y - else: - if rel>math.floor(rel/((self.size+element.size)/2))*((self.size+element.size)/2)+1e-11 and delta.y+rel0: - if relmath.ceil(rel/((self.size+element.size)/2))*((self.size+element.size)/2)+1e-11 and math.ceil(rel/((self.size+element.size)/2))*((self.size+element.size)/2)!=0: - # stick to corner - candidate.x=math.ceil(rel/((self.size+element.size)/2))*((self.size+element.size)/2)+self.pos.x-element.pos.x - else: - if rel>math.floor(rel/((self.size+element.size)/2))*((self.size+element.size)/2)+1e-11 and delta.x+rel0: if self.app.painter.reference==None: - self.raw_text+=" "*spaces+"({:05.2f},{:05.2f})\n".format(self.app.painter.selected[0].squares[0].pos.x,self.app.painter.selected[0].squares[0].pos.y) + self.raw_text+=" "*spaces+"({:05.2f},{:05.2f})\n".format(self.app.painter.selected[0].elements[0].pos.x,self.app.painter.selected[0].elements[0].pos.y) else: - self.raw_text+=" "*spaces+"({:05.2f},{:05.2f})\n".format(self.app.painter.selected[0].squares[0].pos.x-self.app.painter.reference.squares[0].pos.x,self.app.painter.selected[0].squares[0].pos.y-self.app.painter.reference.squares[0].pos.y) + self.raw_text+=" "*spaces+"({:05.2f},{:05.2f})\n".format(self.app.painter.selected[0].elements[0].pos.x-self.app.painter.reference.elements[0].pos.x,self.app.painter.selected[0].elements[0].pos.y-self.app.painter.reference.elements[0].pos.y) # do not wrap self.text=self.raw_text[:min(len(self.raw_text),int(self.width/self.char_width))]