Compare commits

..

10 Commits

8 changed files with 377 additions and 210 deletions

258
src/element.py Normal file
View File

@@ -0,0 +1,258 @@
## elements that polyominoes are made of
import math
import sys
from point import Point,l_infinity,l_2
from tools import isint_nonzero,sgn,in_interval,ceil_grid,floor_grid
from kivy.graphics import Rectangle,Ellipse,Line
# 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
# draw element
def draw(self,painter):
return
# override in each subclass
# draw boundary
def stroke(self,painter):
return
# 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
# delta is the impossible move that was asked for
def move_along(self,delta,element):
return element
# rectangular element
# the size of the y component is specified by an aspect ratio: size_x=size, size_y=size*aspect
class Element_square(Element):
def __init__(self,x,y,size,**kwargs):
self.pos=Point(x,y)
self.size=size
self.aspect=kwargs.get("aspect",1.0)
# draw element
def draw(self,painter):
Rectangle(pos=(painter.pos_tocoord_x(self.pos.x-0.5*self.size),painter.pos_tocoord_y(self.pos.y-0.5*self.size*self.aspect)),size=(self.size*painter.base_size,self.size*self.aspect*painter.base_size))
# draw boundary
def stroke(self,painter):
# convert to graphical coordinates
coordx=painter.pos_tocoord_x(square.pos.x)
coordy=painter.pos_tocoord_y(square.pos.y)
Line(points=(
*(coordx-0.5*self.size*painter.base_size,coordy-0.5*self.size*self.aspect*painter.base_size),
*(coordx-0.5*self.size*painter.base_size,coordy+0.5*self.size*self.aspect*painter.base_size),
*(coordx+0.5*self.size*painter.base_size,coordy+0.5*self.size*self.aspect*painter.base_size),
*(coordx+0.5*self.size*painter.base_size,coordy-0.5*self.size*self.aspect*painter.base_size),
*(coordx-0.5*self.size*painter.base_size,coordy-0.5*self.size*self.aspect*painter.base_size)
))
# check whether an element interacts with square
# TODO: this only works if element is a square!
def check_interaction(self,element):
# allow for error
return max(abs(element.pos.x-self.pos.x)/(self.size+element.size),abs(element.pos.y-self.pos.y)/(self.size*self.aspect+element.size*element.aspect))<1/2-1e-11
# whether x is in the support of the element
def in_support(self,x):
return max(abs(self.pos.x-x.x),abs(self.pos.y-x.y)/self.aspect)<=1/2
# check whether an element is touching self
# TODO: this only works if element is a square!
def check_touch(self,element):
# allow for error
if in_interval(max(abs(element.pos.x-self.pos.x)/(self.size+element.size),abs(element.pos.y-self.pos.y)/(self.size*self.aspect+element.size*element.aspect)),1/2-1e-11,1/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
# TODO: this only works if element is a square!
def move_on_line_to_stick(self,element,v):
size_x=(self.size+element.size)/2
size_y=(self.size*self.aspect+element.size*element.aspect)/2
# compute intersections with four lines making up square
if v.x!=0:
if v.y!=0:
intersections=[\
Point(self.pos.x+size_x,element.pos.y+v.y/v.x*(self.pos.x+size_x-element.pos.x)),\
Point(self.pos.x-size_x,element.pos.y+v.y/v.x*(self.pos.x-size_x-element.pos.x)),\
Point(element.pos.x+v.x/v.y*(self.pos.y+size_y-element.pos.y),self.pos.y+size_y),\
Point(element.pos.x+v.x/v.y*(self.pos.y-size_y-element.pos.y),self.pos.y-size_y)\
]
else:
intersections=[\
Point(self.pos.x+size_x,element.pos.y),\
Point(self.pos.x-size_x,element.pos.y)
]
else:
if v.y!=0:
intersections=[\
Point(element.pos.x,self.pos.y+size_y),\
Point(element.pos.x,self.pos.y-size_y)\
]
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)<=size_x+1e-11 and abs(intersections[i].y-self.pos.y)<=size_y+1e-11:
if (intersections[i]-element.pos)**2<dist:
closest=intersections[i]
dist=(intersections[i]-element.pos)**2
if closest==None:
print("error: cannot move particle at (",element.pos.x,",",element.pos.y,") to the boundary of (",self.pos.x,",",self.pos.y,") in direction (",v.x,",",v.y,")",file=sys.stderr)
exit(-1)
# return difference to pos
return closest-element.pos
# move along edge of square
# TODO: this only works if element is a square!
def move_along(self,delta,element):
size_x=(self.size+element.size)/2
size_y=(self.size*self.aspect+element.size*element.aspect)/2
rel=element.pos-self.pos
# check if the particle is stuck in the x direction
if isint_nonzero(rel.x/size_x):
# check y direction
if isint_nonzero(rel.y/size_y):
# in corner
if sgn(delta.y)==-sgn(rel.y):
# stuck in x direction
return self.move_stuck_x(delta,element)
elif sgn(delta.x)==-sgn(rel.x):
# stuck in y direction
return self.move_stuck_y(delta,element)
# stuck in both directions
return element.pos
else:
# stuck in x direction
return self.move_stuck_x(delta,element)
elif isint_nonzero(rel.y/size_y):
# stuck in y direction
return self.move_stuck_y(delta,element)
# this should never happen
else:
print("error: stuck particle has non-integer relative position: (",rel.x,",",rel.y,")",file=sys.stderr)
exit(-1)
# move when stuck in the x direction
def move_stuck_x(self,delta,element):
size_y=(self.size*self.aspect+element.size*element.aspect)/2
# only move in y direction
candidate=Point(0,delta.y)
# do not move past corners
rel=element.pos.y-self.pos.y
if delta.y>0:
if rel<ceil_grid(rel,size_y)-1e-11 and delta.y+rel>ceil_grid(rel,size_y)+1e-11 and ceil_grid(rel,size_y)!=0:
# stick to corner
candidate.y=ceil_grid(rel,size_y)+self.pos.y-element.pos.y
else:
if rel>floor_grid(rel,size_y)+1e-11 and delta.y+rel<floor_grid(rel,size_y)-1e-11 and floor_grid(rel,size_y)!=0:
# stick to corner
candidate.y=floor_grid(rel,size_y)+self.pos.y-element.pos.y
return candidate
# move when stuck in the y direction
def move_stuck_y(self,delta,element):
size_x=(self.size+element.size)/2
# onlx move in x direction
candidate=Point(delta.x,0)
# do not move past corners
rel=element.pos.x-self.pos.x
if delta.x>0:
if rel<ceil_grid(rel,size_x)-1e-11 and delta.x+rel>ceil_grid(rel,size_x)+1e-11 and ceil_grid(rel,size_x)!=0:
# stick to corner
candidate.x=ceil_grid(rel,size_x)+self.pos.x-element.pos.x
else:
if rel>floor_grid(rel,size_x)+1e-11 and delta.x+rel<floor_grid(rel,size_x)-1e-11 and floor_grid(rel,size_x)!=0:
# stick to corner
candidate.x=floor_grid(rel,size_x)+self.pos.x-element.pos.x
return candidate
# circular elements
# (size is the diameter)
class Element_circle(Element):
# draw element
def draw(self,painter):
Ellipse(pos=(painter.pos_tocoord_x(self.pos.x-0.5*self.size),painter.pos_tocoord_y(self.pos.y-0.5*self.size)),size=(self.size*painter.base_size,self.size*painter.base_size))
# draw boundary
def stroke(self,painter):
Line(circle=(painter.pos_tocoord_x(self.pos.x),painter.pos_tocoord_y(self.pos.y),self.size*0.5*painter.base_size))
# check whether an element interacts with square
# TODO: this only works if element is a circle!
def check_interaction(self,element):
# allow for error
return l_2(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_2(self.pos-x)<=1/2
# check whether an element is touching self
# TODO: this only works if element is a circle!
def check_touch(self,element):
# allow for error
if in_interval(l_2(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
# TODO: this only works if element is a circle!
def move_on_line_to_stick(self,element,v):
# relative position
x=element.pos-self.pos
# radius of collision circle
R=(element.size+self.size)/2
# smallest root of t^2 v^2+2x.v t+x^2-R^2
t=(-v.dot(x)-math.sqrt(v.dot(x)*v.dot(x)-v.dot(v)*(x.dot(x)-R*R)))/v.dot(v)
# return difference to pos
return v*t
# move along edge of circle
# TODO: this only works if element is a circle!
def move_along(self,delta,element):
x=element.pos-self.pos+delta
return x/l_2(x)*(element.size+self.size)/2+self.pos-element.pos

View File

@@ -1,3 +1,5 @@
# check that a file is creatable/writable/editable
import os.path import os.path
# check that a file can be edited # check that a file can be edited

View File

@@ -1,3 +1,5 @@
# define background lattices
from point import Point from point import Point
# parent class of all lattices # parent class of all lattices
@@ -24,18 +26,18 @@ class Lattice():
specs=spec.split(":") specs=spec.split(":")
# check type of lattice # check type of lattice
if specs[0]=="square": if specs[0]=="square":
return Square_lattice.new_square(specs[1:],spec) return Lattice_square.new_square(specs[1:],spec)
else: else:
return(None,"error: unrecognized lattice type: '"+specs[0]+"'") return(None,"error: unrecognized lattice type: '"+specs[0]+"'")
# square lattice # square lattice
class Square_lattice(Lattice): class Lattice_square(Lattice):
def __init__(self,**kwargs): def __init__(self,**kwargs):
self.spacing=kwargs.get("spacing",1.) self.spacing=kwargs.get("spacing",1.)
super(Square_lattice,self).__init__(**kwargs,type="square") super(Lattice_square,self).__init__(**kwargs,type="square")
# lattice point nearest to point # lattice point nearest to point
def nearest(self,point): def nearest(self,point):
@@ -49,12 +51,12 @@ class Square_lattice(Lattice):
def new_square(specs,spec): def new_square(specs,spec):
# no optional args # no optional args
if len(specs)==0: if len(specs)==0:
return (Square_lattice(),"") return (Lattice_square(),"")
if len(specs)>1: if len(specs)>1:
return (None,"error: '"+spec+"' is not a valid specification for the square lattice: should be 'square[:spacing]'") return (None,"error: '"+spec+"' is not a valid specification for the square lattice: should be 'square[:spacing]'")
try: try:
spacing=float(specs[0]) spacing=float(specs[0])
return (Square_lattice(spacing=spacing),"") return (Lattice_square(spacing=spacing),"")
except: except:
return (None,"error: '"+spec+"' is not a valid specification for the square lattice: should be 'square[:spacing]'") return (None,"error: '"+spec+"' is not a valid specification for the square lattice: should be 'square[:spacing]'")

View File

@@ -1,3 +1,5 @@
# main drawing class
import sys import sys
import math import math
from kivy.uix.widget import Widget from kivy.uix.widget import Widget
@@ -5,9 +7,7 @@ from kivy.core.window import Window
from kivy.graphics import Color,Line from kivy.graphics import Color,Line
from point import Point from point import Point
from polyomino import Cross from polyomino import Cross,Disk
from polyomino import Square_element
from lattice import Square_lattice
from tools import remove_fromlist from tools import remove_fromlist
@@ -44,6 +44,9 @@ class Painter(Widget):
# modifiers # modifiers
self.modifiers=[] self.modifiers=[]
# base size for all particles
self.base_size=50
# init Widget # init Widget
super(Painter,self).__init__(**kwargs) super(Painter,self).__init__(**kwargs)
@@ -69,20 +72,20 @@ class Painter(Widget):
# snap all existing particles to grid # snap all existing particles to grid
for particle in self.particles: for particle in self.particles:
delta=self.lattice.nearest_delta(particle.squares[0].pos) delta=self.lattice.nearest_delta(particle.elements[0].pos)
if not self.check_interaction_any(particle,delta): if not self.check_interaction_any(particle,delta):
particle.move(delta) particle.move(delta)
# convert logical coordinates (normalized and centered) to the ones that are plotted # convert logical coordinates (normalized and centered) to the ones that are plotted
def pos_tocoord_x(self,x): def pos_tocoord_x(self,x):
return self.width/2+x*Square_element.size return self.width/2+x*self.base_size
def pos_tocoord_y(self,y): def pos_tocoord_y(self,y):
return self.height/2+y*Square_element.size return self.height/2+y*self.base_size
def coord_topos_x(self,x): def coord_topos_x(self,x):
return (x-self.width/2)/Square_element.size return (x-self.width/2)/self.base_size
def coord_topos_y(self,y): def coord_topos_y(self,y):
return (y-self.height/2)/Square_element.size return (y-self.height/2)/self.base_size
@@ -102,7 +105,7 @@ class Painter(Widget):
# draw grids # draw grids
for particle in self.particles: for particle in self.particles:
if particle.grid>0: if particle.grid>0:
self.draw_grid(particle.squares[0].pos,particle.grid) self.draw_grid(particle.elements[0].pos,particle.grid)
for particle in self.particles: for particle in self.particles:
particle.draw(self,alpha=0.5) particle.draw(self,alpha=0.5)
@@ -177,10 +180,10 @@ class Painter(Widget):
# zoom # zoom
elif text=="+": elif text=="+":
# increment by 10% # increment by 10%
self.set_zoom(Square_element.size/50*1.1) self.set_zoom(self.base_size/50*1.1)
elif text=="-": elif text=="-":
# decrease by 10% # decrease by 10%
self.set_zoom(Square_element.size/50*0.9) self.set_zoom(self.base_size/50*0.9)
elif text=="=": elif text=="=":
# reset # reset
self.set_zoom(1) self.set_zoom(1)
@@ -210,12 +213,13 @@ class Painter(Widget):
touchx=self.coord_topos_x(touch.x) touchx=self.coord_topos_x(touch.x)
touchy=self.coord_topos_y(touch.y) touchy=self.coord_topos_y(touch.y)
# create new cross # create new particle
if touch.button=="right": if touch.button=="right":
new=Cross(touchx,touchy) new=Cross(touchx,touchy)
#new=Disk(touchx,touchy)
# snap to lattice # snap to lattice
if self.lattice!=None: 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)): if not self.check_interaction_any(new,Point(0,0)):
# add to list # add to list
@@ -236,7 +240,7 @@ class Painter(Widget):
# record relative position of click with respect to reference # record relative position of click with respect to reference
if self.undermouse!=None: 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 # no modifiers
if self.modifiers==[]: if self.modifiers==[]:
@@ -289,7 +293,7 @@ class Painter(Widget):
# only move on left click # only move on left click
if touch.button=="left" and self.modifiers==[] and self.undermouse!=None: 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 # 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 # snap to lattice
if self.lattice!=None: if self.lattice!=None:
@@ -345,9 +349,15 @@ class Painter(Widget):
# check whether a candidate particle element with any of the unselected particles # check whether a candidate particle element with any of the unselected particles
def check_interaction_unselected_element(self,element,offset): def check_interaction_unselected_element(self,element,offset):
for particle in self.unselected: for particle in self.unselected:
for square in particle.squares: for elt in particle.elements:
if square.check_interaction(element.pos+offset): # add offset
element.pos+=offset
if elt.check_interaction(element):
# reset offset
element.pos-=offset
return True return True
# reset offset
element.pos-=offset
return False return False
@@ -357,7 +367,7 @@ class Painter(Widget):
# actual_delta is the smallest (componentwise) of all the computed delta's # actual_delta is the smallest (componentwise) of all the computed delta's
actual_delta=Point(math.inf,math.inf) actual_delta=Point(math.inf,math.inf)
for particle in self.selected: for particle in self.selected:
for element in particle.squares: for element in particle.elements:
# compute adjustment move due to unselected obstacles # compute adjustment move due to unselected obstacles
adjusted_delta=self.adjust_move_element(delta,element,0) adjusted_delta=self.adjust_move_element(delta,element,0)
# only keep the smallest delta's (in absolute value) # only keep the smallest delta's (in absolute value)
@@ -383,18 +393,23 @@ class Painter(Widget):
# whether newpos is acceptable # whether newpos is acceptable
accept_newpos=True accept_newpos=True
for other in self.unselected: for other in self.unselected:
for obstacle in other.squares: for obstacle in other.elements:
# move would make element overlap with obstacle # move would make element overlap with obstacle
if obstacle.check_interaction(element.pos+delta): element.pos+=delta
if obstacle.check_interaction(element):
element.pos-=delta
accept_newpos=False accept_newpos=False
# check if particle already touches obstacle # check if particle already touches obstacle
if obstacle.check_touch(element.pos): if obstacle.check_touch(element):
# move along obstacle while remaining stuck # move along obstacle while remaining stuck
newdelta=obstacle.move_along(delta,element.pos) newdelta=obstacle.move_along(delta,element)
else: else:
newdelta=obstacle.move_on_line_to_stick(element.pos,delta) newdelta=obstacle.move_on_line_to_stick(element,delta)
if not self.check_interaction_unselected_element(element,newdelta): if not self.check_interaction_unselected_element(element,newdelta):
return newdelta return newdelta
else:
# reset offset
element.pos-=delta
if accept_newpos: if accept_newpos:
return delta return delta
else: else:
@@ -435,7 +450,9 @@ class Painter(Widget):
for particle in self.particles: for particle in self.particles:
if type(particle)==Cross: if type(particle)==Cross:
ff.write("{:d};".format(CROSS_INDEX)) 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])) elif type(particle)==Disk:
ff.write("{:d};".format(DISK_INDEX))
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() ff.close()
# read configuration from file # read configuration from file
@@ -512,6 +529,8 @@ class Painter(Widget):
continue continue
if particle_type==CROSS_INDEX: if particle_type==CROSS_INDEX:
candidate=Cross(pos.x,pos.y,color=color) candidate=Cross(pos.x,pos.y,color=color)
elif particle_type==DISK_INDEX:
candidate=Disk(pos.x,pos.y,color=color)
else: else:
print("warning: ignoring line "+str(i)+" in file '"+file+"': unrecognized particle type: '"+entries[0]+"'",file=sys.stderr) print("warning: ignoring line "+str(i)+" in file '"+file+"': unrecognized particle type: '"+entries[0]+"'",file=sys.stderr)
continue continue
@@ -548,7 +567,7 @@ class Painter(Widget):
for particle in self.particles: for particle in self.particles:
if type(particle)==Cross: if type(particle)==Cross:
ff.write("\cross{"+colors.closest_color(particle.color,colors.xcolor_names)+"}") 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{tikzpicture}\n")
ff.write("\\end{document}\n") ff.write("\\end{document}\n")
@@ -558,11 +577,12 @@ class Painter(Widget):
# set zoom level # set zoom level
def set_zoom(self,level): def set_zoom(self,level):
Square_element.size=level*50 self.base_size=level*50
self.draw() self.draw()
# global variables (used like precompiler variables) # global variables (used like precompiler variables)
CROSS_INDEX=1 CROSS_INDEX=1
DISK_INDEX=2

View File

@@ -1,3 +1,5 @@
# two-dimensional point structure
import math import math
# point in two dimensions # point in two dimensions
@@ -47,3 +49,7 @@ class Point:
# L infinity norm # L infinity norm
def l_infinity(x): def l_infinity(x):
return max(abs(x.x),abs(x.y)) return max(abs(x.x),abs(x.y))
# L 2 norm
def l_2(x):
return math.sqrt(x.x*x.x+x.y*x.y)

View File

@@ -1,15 +1,14 @@
import math # a polyomino is a collection of elements, defined in elements.py
import sys from kivy.graphics import Color,Line
from kivy.graphics import Color,Line,Rectangle
from point import Point,l_infinity from point import l_infinity
from tools import isint_nonzero,sgn,in_interval from element import Element_square,Element_circle
# parent class of all polyominos # parent class of all polyominos
class Polyomino(): class Polyomino():
def __init__(self,**kwargs): def __init__(self,**kwargs):
# square elements that make up the polyomino # elements that make up the polyomino
self.squares=kwargs.get("squares",[]) self.elements=kwargs.get("elements",[])
self.color=kwargs.get("color",(0,0,1)) self.color=kwargs.get("color",(0,0,1))
self.selected=False self.selected=False
@@ -28,212 +27,83 @@ class Polyomino():
# darken selected # darken selected
Color(r/2,g/2,b/2,alpha) Color(r/2,g/2,b/2,alpha)
for square in self.squares: for element in self.elements:
Rectangle(pos=(painter.pos_tocoord_x(square.pos.x-0.5),painter.pos_tocoord_y(square.pos.y-0.5)),size=(square.size,square.size)) element.draw(painter)
# draw boundary # draw boundary
self.stroke(painter) self.stroke(painter)
# draw boundary (override for connected polyominos) # draw boundary (override for connected polyominos)
def stroke(self,painter): def stroke(self,painter):
# convert to graphical coordinates
coordx=painter.pos_tocoord_x(square.pos.x)
coordy=painter.pos_tocoord_y(square.pos.y)
# white # white
Color(1,1,1) Color(1,1,1)
for square in self.squares: for element in self.elements:
Line(points=( element.stroke(painter)
*(coordx-0.5*square.size,coordy-0.5*square.size),
*(coordx-0.5*square.size,coordy+0.5*square.size),
*(coordx+0.5*square.size,coordy+0.5*square.size),
*(coordx+0.5*square.size,coordy-0.5*square.size),
*(coordx-0.5*square.size,coordy-0.5*square.size)
))
# move by delta # move by delta
def move(self,delta): def move(self,delta):
for square in self.squares: for element in self.elements:
square.pos+=delta element.pos+=delta
# whether x is in the support of the polyomino # whether x is in the support of the polyomino
def in_support(self,x): def in_support(self,x):
for square in self.squares: for element in self.elements:
if l_infinity(square.pos-x)<=1/2: if element.in_support(x):
return True return True
return False return False
# check whether self interacts with candidate if candidate were moved by offset # check whether self interacts with candidate if candidate were moved by offset
def check_interaction(self,candidate,offset): def check_interaction(self,candidate,offset):
for square1 in self.squares: for element1 in self.elements:
for square2 in candidate.squares: for element2 in candidate.elements:
if square1.check_interaction(square2.pos+offset): # add offset
element2.pos+=offset
if element1.check_interaction(element2):
# reset offset
element2.pos-=offset
return True return True
# reset offset
element2.pos-=offset
return False return False
# square # square
class Square(Polyomino): class Square(Polyomino):
def __init__(self,x,y,**kwargs): def __init__(self,x,y,**kwargs):
super(Square,self).__init__(**kwargs,squares=[Square_element(x,y)]) super(Square,self).__init__(**kwargs,elements=[Element_square(x,y,size=kwargs.get("size",1.0))])
# cross # cross
class Cross(Polyomino): class Cross(Polyomino):
def __init__(self,x,y,**kwargs): def __init__(self,x,y,**kwargs):
super(Cross,self).__init__(**kwargs,squares=[\ super(Cross,self).__init__(**kwargs,elements=[\
Square_element(x,y),\ Element_square(x,y,1,aspect=3),\
Square_element(x+1,y),\ Element_square(x+1,y,1),\
Square_element(x-1,y),\ Element_square(x-1,y,1)\
Square_element(x,y+1),\
Square_element(x,y-1)\
]) ])
# redefine stroke to avoid lines between touching squares # redefine stroke to avoid lines between touching elements
def stroke(self,painter): def stroke(self,painter):
# convert to graphical coordinates # convert to graphical coordinates
coordx=painter.pos_tocoord_x(self.squares[0].pos.x) coordx=painter.pos_tocoord_x(self.elements[0].pos.x)
coordy=painter.pos_tocoord_y(self.squares[0].pos.y) coordy=painter.pos_tocoord_y(self.elements[0].pos.y)
Color(1,1,1) Color(1,1,1)
Line(points=( Line(points=(
*(coordx-0.5*Square_element.size,coordy-0.5*Square_element.size), *(coordx-0.5*painter.base_size,coordy-0.5*painter.base_size),
*(coordx-0.5*Square_element.size,coordy-1.5*Square_element.size), *(coordx-0.5*painter.base_size,coordy-1.5*painter.base_size),
*(coordx+0.5*Square_element.size,coordy-1.5*Square_element.size), *(coordx+0.5*painter.base_size,coordy-1.5*painter.base_size),
*(coordx+0.5*Square_element.size,coordy-0.5*Square_element.size), *(coordx+0.5*painter.base_size,coordy-0.5*painter.base_size),
*(coordx+1.5*Square_element.size,coordy-0.5*Square_element.size), *(coordx+1.5*painter.base_size,coordy-0.5*painter.base_size),
*(coordx+1.5*Square_element.size,coordy+0.5*Square_element.size), *(coordx+1.5*painter.base_size,coordy+0.5*painter.base_size),
*(coordx+0.5*Square_element.size,coordy+0.5*Square_element.size), *(coordx+0.5*painter.base_size,coordy+0.5*painter.base_size),
*(coordx+0.5*Square_element.size,coordy+1.5*Square_element.size), *(coordx+0.5*painter.base_size,coordy+1.5*painter.base_size),
*(coordx-0.5*Square_element.size,coordy+1.5*Square_element.size), *(coordx-0.5*painter.base_size,coordy+1.5*painter.base_size),
*(coordx-0.5*Square_element.size,coordy+0.5*Square_element.size), *(coordx-0.5*painter.base_size,coordy+0.5*painter.base_size),
*(coordx-1.5*Square_element.size,coordy+0.5*Square_element.size), *(coordx-1.5*painter.base_size,coordy+0.5*painter.base_size),
*(coordx-1.5*Square_element.size,coordy-0.5*Square_element.size), *(coordx-1.5*painter.base_size,coordy-0.5*painter.base_size),
*(coordx-0.5*Square_element.size,coordy-0.5*Square_element.size), *(coordx-0.5*painter.base_size,coordy-0.5*painter.base_size),
)) ))
# disk
class Disk(Polyomino):
# square building block of polyominos
class Square_element():
# size
size=50
def __init__(self,x,y,**kwargs): def __init__(self,x,y,**kwargs):
self.pos=Point(x,y) super(Disk,self).__init__(**kwargs,elements=[Element_circle(x,y,size=kwargs.get("size",1.0))])
# set position
def setpos(self,x,y):
self.pos.x=x
self.pos.y=y
# check whether a square at pos interacts with square
def check_interaction(self,pos):
return l_infinity(pos-self.pos)<1
# check whether a square at position pos is touching self
def check_touch(self,pos):
# allow for error
if in_interval(l_infinity(pos-self.pos),1-1e-11,1+1e-11):
return True
return False
# 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):
# compute intersections with four lines making up square
if v.x!=0:
if v.y!=0:
intersections=[\
Point(self.pos.x+1,pos.y+v.y/v.x*(self.pos.x+1-pos.x)),\
Point(self.pos.x-1,pos.y+v.y/v.x*(self.pos.x-1-pos.x)),\
Point(pos.x+v.x/v.y*(self.pos.y+1-pos.y),self.pos.y+1),\
Point(pos.x+v.x/v.y*(self.pos.y-1-pos.y),self.pos.y-1)\
]
else:
intersections=[\
Point(self.pos.x+1,pos.y+v.y/v.x*(self.pos.x+1-pos.x)),\
Point(self.pos.x-1,pos.y+v.y/v.x*(self.pos.x-1-pos.x))
]
else:
if v.y!=0:
intersections=[\
Point(pos.x+v.x/v.y*(self.pos.y+1-pos.y),self.pos.y+1),\
Point(pos.x+v.x/v.y*(self.pos.y-1-pos.y),self.pos.y-1)\
]
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)<=1+1e-11 and abs(intersections[i].y-self.pos.y)<=1+1e-11:
if (intersections[i]-pos)**2<dist:
closest=intersections[i]
dist=(intersections[i]-pos)**2
if closest==None:
print("error: cannot move particle at (",pos.x,",",pos.y,") to the boundary of (",self.pos.x,",",self.pos.y,") in direction (",v.x,",",v.y,")",file=sys.stderr)
exit(-1)
# return difference to pos
return closest-pos
# move along edge of square
def move_along(self,delta,pos):
rel=pos-self.pos
# check if the particle is stuck in the x direction
if isint_nonzero(rel.x):
# check y direction
if isint_nonzero(rel.y):
# in corner
if sgn(delta.y)==-sgn(rel.y):
# stuck in x direction
return self.move_stuck_x(delta,pos)
elif sgn(delta.x)==-sgn(rel.x):
# stuck in y direction
return self.move_stuck_y(delta,pos)
# stuck in both directions
return pos
else:
# stuck in x direction
return self.move_stuck_x(delta,pos)
elif isint_nonzero(rel.y):
# stuck in y direction
return self.move_stuck_y(delta,pos)
# this should never happen
else:
print("error: stuck particle has non-integer relative position: (",rel.x,",",rel.y,")",file=sys.stderr)
exit(-1)
# move when stuck in the x direction
def move_stuck_x(self,delta,pos):
# only move in y direction
candidate=Point(0,delta.y)
# do not move past corners
rel=pos.y-self.pos.y
if delta.y>0:
if rel<math.ceil(rel)-1e-11 and delta.y+rel>math.ceil(rel)+1e-11 and math.ceil(rel)!=0:
# stick to corner
candidate.y=math.ceil(rel)+self.pos.y-pos.y
else:
if rel>math.floor(rel)+1e-11 and delta.y+rel<math.floor(rel)-1e-11 and math.floor(rel)!=0:
# stick to corner
candidate.y=math.floor(rel)+self.pos.y-pos.y
return candidate
# move when stuck in the y direction
def move_stuck_y(self,delta,pos):
# onlx move in x direction
candidate=Point(delta.x,0)
# do not move past corners
rel=pos.x-self.pos.x
if delta.x>0:
if rel<math.ceil(rel)-1e-11 and delta.x+rel>math.ceil(rel)+1e-11 and math.ceil(rel)!=0:
# stick to corner
candidate.x=math.ceil(rel)+self.pos.x-pos.x
else:
if rel>math.floor(rel)+1e-11 and delta.x+rel<math.floor(rel)-1e-11 and math.floor(rel)!=0:
# stick to corner
candidate.x=math.floor(rel)+self.pos.x-pos.x
return candidate

View File

@@ -45,9 +45,9 @@ class Status_bar(Label):
spaces=int(self.width/self.char_width)-len(self.raw_text)-13 spaces=int(self.width/self.char_width)-len(self.raw_text)-13
if spaces>0: if spaces>0:
if self.app.painter.reference==None: 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: 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 # do not wrap
self.text=self.raw_text[:min(len(self.raw_text),int(self.width/self.char_width))] self.text=self.raw_text[:min(len(self.raw_text),int(self.width/self.char_width))]

View File

@@ -1,3 +1,5 @@
import math
# sign function # sign function
def sgn(x): def sgn(x):
if x>=0: if x>=0:
@@ -20,3 +22,10 @@ def remove_fromlist(a,x):
a[a.index(x)]=a[len(a)-1] a[a.index(x)]=a[len(a)-1]
a=a[:len(a)-1] a=a[:len(a)-1]
return a return a
# snap to a grid: ceiling
def ceil_grid(x,size):
return math.ceil(x/size)*size
# snap to a grid: floor
def floor_grid(x,size):
return math.floor(x/size)*size