Split into files

This commit is contained in:
Ian Jauslin 2021-09-28 15:23:06 -04:00
parent af26517c82
commit 1eb5be5da1
5 changed files with 260 additions and 241 deletions

200
cross.py Normal file
View File

@ -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<math.atan(1/3):
return 3/math.cos(tt)
elif tt<math.atan(1/2):
return 1/math.sin(tt)
else:
return 2/math.cos(tt)
# 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:
# move would make cross overlap with other
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

7
jam.kv Normal file
View File

@ -0,0 +1,7 @@
#: kivy 2.0.0
#<jam>:
# canvas:
# Rectangle:
# pos: 0,0
# size: self.width,self.height

243
main.py
View File

@ -1,9 +1,9 @@
import math
from kivy.app import App from kivy.app import App
from kivy.uix.widget import Widget from kivy.uix.widget import Widget
from kivy.graphics import Color,Line,Rectangle
from kivy.config import Config from kivy.config import Config
from cross import Cross,Cross_painter
# App class # App class
class Jam_app(App): class Jam_app(App):
# name of .kv file for main interface # name of .kv file for main interface
@ -15,245 +15,6 @@ class Jam_app(App):
parent.add_widget(self.cross_painter) parent.add_widget(self.cross_painter)
return parent 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<dist:
closest=point
dist=(self-point)**2
return closest
# 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<math.atan(1/3):
return 3/math.cos(tt)
elif tt<math.atan(1/2):
return 1/math.sin(tt)
else:
return 2/math.cos(tt)
# sign function
def sgn(x):
if x>=0:
return 1
return -1
# disable red circles on right click # disable red circles on right click

45
point.py Normal file
View File

@ -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<dist:
closest=point
dist=(self-point)**2
return closest

6
tools.py Normal file
View File

@ -0,0 +1,6 @@
# sign function
def sgn(x):
if x>=0:
return 1
return -1