Compare commits

..

No commits in common. "dev" and "master" have entirely different histories.
dev ... master

12 changed files with 238 additions and 968 deletions

View File

@ -9,17 +9,10 @@ particles overlap.
Run with Run with
```bash ```bash
./src/jam [configuration_file] [-L lattice] ./src/jam [configuration_file]
``` ```
where `[configuration_file]` is an optional argument that specifies a file with
* `[configuration_file]` is an optional argument that specifies a file with a list of particle positions that will be loaded on initial execution.
a list of particle positions that will be loaded on initial execution.
* `[-L lattice]` is an optional argument that specifies a background grid that
constrains the position of the particles. So far, the `lattice` argument must
be `square` for the square lattice, or, to specify the unit length of the
lattice: `square:<unit_length>`.
# Dependencies # Dependencies
@ -66,8 +59,11 @@ Commands can be executed by typing `:` (similarly to vim).
# Current developments # Current developments
So far, Jam supports particles made of combinations of rectangles, but there is So far, Jam only supports cross-shaped particles, but work is in progress to
no infrastructure to set the shape of the particle at runtime. support arbitrary shapes consisting of rectangles, circle arcs and triangles
(check out the `dev` branch to follow the progress).
Support for lattice configurations is also ongoing.
# License # License

View File

@ -28,21 +28,3 @@
\fill[color=#1]#2++(0.5,0.5)--++(0,1)--++(-1,0)--++(0,-1)--++(-1,0)--++(0,-1)--++(1,0)--++(0,-1)--++(1,0)--++(0,1)--++(1,0)--++(0,1)--++(-1,0); \fill[color=#1]#2++(0.5,0.5)--++(0,1)--++(-1,0)--++(0,-1)--++(-1,0)--++(0,-1)--++(1,0)--++(0,-1)--++(1,0)--++(0,1)--++(1,0)--++(0,1)--++(-1,0);
\draw[color=black]#2++(0.5,0.5)--++(0,1)--++(-1,0)--++(0,-1)--++(-1,0)--++(0,-1)--++(1,0)--++(0,-1)--++(1,0)--++(0,1)--++(1,0)--++(0,1)--++(-1,0); \draw[color=black]#2++(0.5,0.5)--++(0,1)--++(-1,0)--++(0,-1)--++(-1,0)--++(0,-1)--++(1,0)--++(0,-1)--++(1,0)--++(0,1)--++(1,0)--++(0,1)--++(-1,0);
} }
% 3-staircase (color #1, position #2)
\def\staircase#1#2{
\fill[color=#1]#2++(-0.5,-0.5)--++(3,0)--++(0,1)--++(-1,0)--++(0,1)--++(-1,0)--++(0,1)--++(-1,0)--++(0,-3);
\draw[color=black]#2++(-0.5,-0.5)--++(3,0)--++(0,1)--++(-1,0)--++(0,1)--++(-1,0)--++(0,1)--++(-1,0)--++(0,-3);
}
% disk (color #1, position #2)
\def\disk#1#2{
\fill[color=#1]#2circle(2.5);
\draw[color=black]#2circle(2.5);
}
% square (color #1, position #2)
\def\square#1#2{
\fill[color=#1]#2++(-1,-1)--++(0,2)--++(2,0)--++(0,-2)--cycle;
\draw[color=black]#2++(-1,-1)--++(0,2)--++(2,0)--++(0,-2)--cycle;
}

View File

@ -20,7 +20,6 @@ import os.path
import filecheck import filecheck
import colors import colors
from polyomino import Cross,Disk,Staircase,Square2
class Command_prompt(Label): class Command_prompt(Label):
@ -339,15 +338,9 @@ class Command_prompt(Label):
if argv[1]=="color": if argv[1]=="color":
self.run_set_color(argv) self.run_set_color(argv)
return return
elif argv[1]=="shape":
self.run_set_shape(argv)
return
elif argv[1]=="grid": elif argv[1]=="grid":
self.run_set_grid(argv) self.run_set_grid(argv)
return return
elif argv[1]=="voronoi":
self.run_set_voronoi(argv)
return
elif argv[1]=="zoom": elif argv[1]=="zoom":
self.run_set_zoom(argv) self.run_set_zoom(argv)
else: else:
@ -357,7 +350,7 @@ class Command_prompt(Label):
# set color # set color
def run_set_color(self,argv): def run_set_color(self,argv):
if len(argv)<3: if len(argv)<3:
self.message="error: 'set color' command was run with without an argument -- usage: 'set color <color_descriptor>'" self.message="error: 'set color' command was run with without anargument -- usage: 'set color <color_descriptor>'"
return return
# find color name in list # find color name in list
for color in colors.xcolor_names: for color in colors.xcolor_names:
@ -379,23 +372,6 @@ class Command_prompt(Label):
return return
self.app.painter.set_color(color) self.app.painter.set_color(color)
# set particle shape
def run_set_shape(self,argv):
if len(argv)<3:
self.message="error: 'set shape' command was run with without an argument -- usage: 'set shape <shape_descriptor>'"
return
elif argv[2]=="cross":
self.app.painter.shape=Cross
elif argv[2]=="disk":
self.app.painter.shape=Disk
elif argv[2]=="staircase":
self.app.painter.shape=Staircase
elif argv[2]=="2square":
self.app.painter.shape=Square2
else:
self.message="error: unrecognized shape '"+argv[2]+"'; supported shapes are cross|disk"
return
# toggle grid # toggle grid
def run_set_grid(self,argv): def run_set_grid(self,argv):
if len(argv)==2: if len(argv)==2:
@ -416,19 +392,6 @@ class Command_prompt(Label):
return return
self.app.painter.set_grid(mesh) self.app.painter.set_grid(mesh)
# toggle Voronoi cells
def run_set_voronoi(self,argv):
if len(argv)==2:
# no argument: set to toggle
self.app.painter.set_voronoi(-1)
elif argv[2]=="on":
self.app.painter.set_voronoi(1)
elif argv[2]=="off":
self.app.painter.set_voronoi(0)
else:
self.message="error: unrecognized argument '"+argv[2]+"' -- usage 'set voronoi [on|off]'"
return
# set zoom level (changes size of elements) # set zoom level (changes size of elements)
def run_set_zoom(self,argv): def run_set_zoom(self,argv):
if len(argv)==2: if len(argv)==2:

View File

@ -1,297 +0,0 @@
# Copyright 2021-2023 Ian Jauslin
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
## 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)
))
# for use with lattices
# list of lattice points covered by square
def lattice_points(self,lattice):
out=[]
dx=math.floor(0.5*self.size/lattice.spacing)
dy=math.floor(0.5*self.size*self.aspect/lattice.spacing)
for i in range(-dx,dx+1):
for j in range(-dy,dy+1):
out.append(Point(self.pos.x+i*lattice.spacing,self.pos.y+j*lattice.spacing))
return out
# check whether an element interacts with square
# TODO: this only works if element is a rectangle!
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 rectangle!
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 rectangle!
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 rectangle!
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
# for use with lattices
# list of lattice points covered by square
def lattice_points(self,lattice):
out=[]
dx=math.floor(0.5*self.size/lattice.spacing)
for i in range(-dx,dx+1):
for j in range(-dx,dx+1):
if lattice.spacing*lattice.spacing*(i*i+j*j)<=self.size*self.size/4:
out.append(Point(self.pos.x+i*lattice.spacing,self.pos.y+j*lattice.spacing))
return out

View File

@ -12,8 +12,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# 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

128
src/jam
View File

@ -14,119 +14,18 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
import os.path,os
import filecheck
from lattice import Lattice
## read cli before loading kivy, in case there are errors
# read cli
openfile=""
lattice=""
def read_cli():
global openfile
global lattice
# init flag
flag=""
# loop over arguments
for arg in sys.argv[1:]:
# option flag
if arg[0]=='-':
# loop over options
for c in arg[1:]:
# lattice
if c=='L':
flag="lattice"
else:
print("error: unrecognized option '"+c+"'\n", file=sys.stderr)
exit(-1)
else:
# read lattice argument
if flag=="lattice":
# test the specification
(obj,message)=Lattice.new(arg)
if obj==None:
print(message,file=sys.stderr)
exit(-1)
lattice=arg
# reset flag
flag=""
# no flags
else:
openfile=arg
(ret,message)=filecheck.check_edit(openfile)
if ret<0:
print(message,file=sys.stderr)
exit(-1)
preread_conf(openfile)
# read command line arguments from configuration file
def preread_conf(file):
global lattice
try:
ff=open(file,"r")
except:
return
# counter
i=0
try:
lines=ff.readlines()
except:
print("error: could not read the contents of file '"+file+"'", file=sys.stderr)
exit(-1)
for line in lines:
i+=1
# remove newline
line=line[:len(line)-1]
# ignore comments
if '#' in line:
line=line[:line.find('#')]
# ignore empty lines
if len(line)==0:
continue
# read options
if line[0]=='%':
# ignore empty line
if len(line)==1:
continue
[key,val]=line[1:].split('=',1)
if key=="lattice":
# test the specification
(obj,message)=Lattice.new(val)
if obj==None:
print("error: line "+str(i)+" in file '"+file+"': "+message,file=sys.stderr)
exit(-1)
lattice=val
ff.close()
# read cli
read_cli()
## import kivy
# disable kivy argument parser
os.environ["KIVY_NO_ARGS"] = "1"
from kivy.app import App from kivy.app import App
from kivy.uix.widget import Widget from kivy.uix.widget import Widget
from kivy.uix.boxlayout import BoxLayout from kivy.uix.boxlayout import BoxLayout
from kivy.config import Config from kivy.config import Config
import sys
import os.path
from painter import Painter from painter import Painter
from status_bar import Status_bar from status_bar import Status_bar
from command_prompt import Command_prompt from command_prompt import Command_prompt
import filecheck
# App class # App class
class Jam_app(App): class Jam_app(App):
@ -137,9 +36,6 @@ class Jam_app(App):
# the file open for editing # the file open for editing
self.openfile=kwargs.get("openfile","") self.openfile=kwargs.get("openfile","")
# the lattice open for editing
self.lattice=kwargs.get("lattice","")
# readonly mode # readonly mode
self.readonly=False self.readonly=False
@ -167,20 +63,24 @@ class Jam_app(App):
# set readonly mode # set readonly mode
self.readonly=not os.access(self.openfile,os.W_OK) self.readonly=not os.access(self.openfile,os.W_OK)
# load lattice
if self.lattice!="":
(obj,message)=Lattice.new(self.lattice)
self.painter.set_lattice(obj)
return layout return layout
# disable red circles on right click # disable red circles on right click
Config.set('input', 'mouse', 'mouse,disable_multitouch') Config.set('input', 'mouse', 'mouse,disable_multitouch')
# do not exit on escape # do not exit on escape
Config.set('kivy', 'exit_on_escape', 0) Config.set('kivy', 'exit_on_escape', 0)
# read cli
openfile=""
if len(sys.argv)==2:
openfile=sys.argv[1]
# check file
(ret,message)=filecheck.check_edit(openfile)
if ret<0:
print(message,file=sys.stderr)
exit(-1)
# run # run
if __name__ == '__main__': if __name__ == '__main__':
Jam_app(openfile=openfile,lattice=lattice).run() Jam_app(openfile=openfile).run()

View File

@ -1,94 +0,0 @@
# Copyright 2021-2023 Ian Jauslin
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# define background lattices
from point import Point
# parent class of all lattices
class Lattice():
def __init__(self,**kwargs):
self.type=kwargs.get("type","")
# lattice point nearest to point
# overwrite in subclasses
def nearest(self,point):
return point
# delta to nearest point
def nearest_delta(self,point):
return self.nearest(point)-point
# draw lattice
# overwrite in subclasses
def draw(self,painter):
return
# return the lattice according to a specification
def new(spec):
specs=spec.split(":")
# check type of lattice
if specs[0]=="square":
return Lattice_square.new_square(specs[1:],spec)
else:
return(None,"error: unrecognized lattice type: '"+specs[0]+"'")
# square lattice
class Lattice_square(Lattice):
def __init__(self,**kwargs):
self.spacing=kwargs.get("spacing",1.)
super(Lattice_square,self).__init__(**kwargs,type="square")
# lattice point nearest to point
def nearest(self,point):
return Point(round(point.x/self.spacing)*self.spacing,round(point.y/self.spacing)*self.spacing)
# draw
def draw(self,painter):
painter.draw_grid(Point(self.spacing/2,self.spacing/2),self.spacing)
# return the lattice according to a specification
def new_square(specs,spec):
# no optional args
if len(specs)==0:
return (Lattice_square(),"")
if len(specs)>1:
return (None,"error: '"+spec+"' is not a valid specification for the square lattice: should be 'square[:spacing]'")
try:
spacing=float(specs[0])
return (Lattice_square(spacing=spacing),"")
except:
return (None,"error: '"+spec+"' is not a valid specification for the square lattice: should be 'square[:spacing]'")
# distance on the lattice between (x1,x2) and (y1,y2)
def distance(self, x1, x2, y1, y2):
return round((abs(x1-y1)+abs(x2-y2))/self.spacing)
# distance between a lattice site and a particle
def distance_to_particle(self, x1, x2, particle):
mindist=self.distance_to_element(x1, x2, particle.elements[0])
for i in range(1,len(particle.elements)):
mindist=min(self.distance_to_element(x1, x2, particle.elements[i]),mindist)
return mindist
# distance between a lattice site and an element
def distance_to_element(self, x1, x2, element):
pts=element.lattice_points(self)
mindist=self.distance(x1, x2, pts[0].x, pts[0].y)
for i in range(1,len(pts)):
mindist=min(self.distance(x1, x2, pts[i].x, pts[i].y),mindist)
return mindist

View File

@ -12,16 +12,15 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# main drawing class
import sys import sys
import math import math
from kivy.uix.widget import Widget from kivy.uix.widget import Widget
from kivy.core.window import Window from kivy.core.window import Window
from kivy.graphics import Color,Line,Rectangle from kivy.graphics import Color,Line
from point import Point from point import Point
from polyomino import Cross,Disk,Staircase,Square2 from polyomino import Cross
from polyomino import Square_element
from tools import remove_fromlist from tools import remove_fromlist
@ -35,15 +34,6 @@ class Painter(Widget):
# list of particles # list of particles
self.particles=[] self.particles=[]
# shape of particle to add next
self.shape=Cross
# color of particle to add next
self.color=(0,0,1)
# underlying lattice
self.lattice=None
# particle under mouse # particle under mouse
self.undermouse=None self.undermouse=None
@ -64,9 +54,6 @@ 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)
@ -74,38 +61,21 @@ class Painter(Widget):
self.keyboard = Window.request_keyboard(None,self,"text") self.keyboard = Window.request_keyboard(None,self,"text")
self.keyboard.bind(on_key_down=self.on_key_down,on_key_up=self.on_key_up,on_textinput=self.on_textinput) self.keyboard.bind(on_key_down=self.on_key_down,on_key_up=self.on_key_up,on_textinput=self.on_textinput)
# redraw on resize
self.bind(size=lambda obj,value: self.draw())
def reset(self): def reset(self):
self.particles=[] self.particles=[]
self.undermouse=None self.undermouse=None
self.draw() self.draw()
# set lattice
def set_lattice(self,lattice):
self.lattice=lattice
# draw
self.draw()
# snap all existing particles to grid
for particle in self.particles:
delta=self.lattice.nearest_delta(particle.elements[0].pos)
if not self.check_interaction_any(particle,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*self.base_size return self.width/2+x*Square_element.size
def pos_tocoord_y(self,y): def pos_tocoord_y(self,y):
return self.height/2+y*self.base_size return self.height/2+y*Square_element.size
def coord_topos_x(self,x): def coord_topos_x(self,x):
return (x-self.width/2)/self.base_size return (x-self.width/2)/Square_element.size
def coord_topos_y(self,y): def coord_topos_y(self,y):
return (y-self.height/2)/self.base_size return (y-self.height/2)/Square_element.size
@ -118,20 +88,10 @@ class Painter(Widget):
for particle in self.particles: for particle in self.particles:
particle.draw(self) particle.draw(self)
# draw lattice
if self.lattice!=None:
self.lattice.draw(self)
# 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.elements[0].pos,particle.grid) self.draw_grid(particle.squares[0].pos,particle.grid)
# draw Voronoi cells
if self.lattice!=None:
for particle in self.particles:
if particle.voronoi>0:
self.draw_voronoi(particle)
for particle in self.particles: for particle in self.particles:
particle.draw(self,alpha=0.5) particle.draw(self,alpha=0.5)
@ -141,79 +101,17 @@ class Painter(Widget):
# height offset due to status bar and command prompt # height offset due to status bar and command prompt
height_offset=self.app.status_bar.height+self.app.command_prompt.height height_offset=self.app.status_bar.height+self.app.command_prompt.height
# vertical lines # vertical lines
# lines right of pos # offest wrt 0
xx=pos.x+mesh/2 offset=(pos.x-0.5)%mesh
while self.pos_tocoord_x(xx)<self.width: for i in range(math.floor((self.width/Square_element.size-offset)/mesh)+1):
Color(1,1,1) Color(1,1,1)
Line(points=(self.pos_tocoord_x(xx),height_offset,self.pos_tocoord_x(xx),self.height+height_offset)) Line(points=((i*mesh+offset)*Square_element.size,height_offset,(i*mesh+offset)*Square_element.size,self.height+height_offset))
xx+=mesh # horizontal lines
# lines left of pos # offset wrt 0
xx=pos.x-mesh/2 offset=(pos.y-0.5)%1-height_offset/Square_element.size
while self.pos_tocoord_x(xx)>0: for i in range(math.floor((self.height/Square_element.size-offset)/mesh)+1):
Color(1,1,1) Color(1,1,1)
Line(points=(self.pos_tocoord_x(xx),height_offset,self.pos_tocoord_x(xx),self.height+height_offset)) Line(points=(0,(i*mesh+offset)*Square_element.size+height_offset,self.width,(i*mesh+offset)*Square_element.size+height_offset))
xx-=mesh
# lines above pos
yy=pos.y+mesh/2
while self.pos_tocoord_y(yy)<self.height:
Color(1,1,1)
Line(points=(0,self.pos_tocoord_y(yy),self.width,self.pos_tocoord_y(yy)))
yy+=mesh
# lines below pos
yy=pos.y-mesh/2
while self.pos_tocoord_y(yy)>0:
Color(1,1,1)
Line(points=(0,self.pos_tocoord_y(yy),self.width,self.pos_tocoord_y(yy)))
yy-=mesh
# draw the discrete Voronoi cell of a particle
def draw_voronoi(self,particle):
# only works for lattices
if self.lattice!=None:
pos=particle.elements[0].pos
# loop over all points
xx=pos.x
while self.pos_tocoord_x(xx)<self.width:
yy=pos.y
while self.pos_tocoord_y(yy)<self.height:
self.draw_voronoi_site(xx,yy,particle.color,self.is_in_voronoi(xx,yy,particle))
yy+=self.lattice.spacing
yy=pos.y-self.lattice.spacing
while self.pos_tocoord_y(yy)>0:
self.draw_voronoi_site(xx,yy,particle.color,self.is_in_voronoi(xx,yy,particle))
yy-=self.lattice.spacing
xx+=self.lattice.spacing
xx=pos.x-self.lattice.spacing
while self.pos_tocoord_x(xx)>0:
yy=pos.y
while self.pos_tocoord_y(yy)<self.height:
self.draw_voronoi_site(xx,yy,particle.color,self.is_in_voronoi(xx,yy,particle))
yy+=self.lattice.spacing
yy=pos.y-self.lattice.spacing
while self.pos_tocoord_y(yy)>0:
self.draw_voronoi_site(xx,yy,particle.color,self.is_in_voronoi(xx,yy,particle))
yy-=self.lattice.spacing
xx-=self.lattice.spacing
# check whether a site is in the Voronoi cell of a particle
def is_in_voronoi(self,x,y,particle):
d_to_particle=self.lattice.distance_to_particle(x,y,particle)
# count how many are in voronoi cell
count=1
# TODO: start with a particle that is close to x,y
for q in self.particles:
dd=self.lattice.distance_to_particle(x,y,q)
if q!=particle and dd<d_to_particle:
return 0
if dd==d_to_particle:
count+=1
return count
# draw a site in a Voronoi cell
def draw_voronoi_site(self,x,y,color,count):
if count==0:
return
Color(color[0],color[1],color[2],1-count*0.1)
Rectangle(pos=(self.pos_tocoord_x(x-0.5*self.lattice.spacing),self.pos_tocoord_y(y-0.5*self.lattice.spacing)),size=(self.base_size*self.lattice.spacing,self.base_size*self.lattice.spacing))
# respond to keyboard # respond to keyboard
@ -255,10 +153,10 @@ class Painter(Widget):
# zoom # zoom
elif text=="+": elif text=="+":
# increment by 10% # increment by 10%
self.set_zoom(self.base_size/50*1.1) self.set_zoom(Square_element.size/50*1.1)
elif text=="-": elif text=="-":
# decrease by 10% # decrease by 10%
self.set_zoom(self.base_size/50*0.9) self.set_zoom(Square_element.size/50*0.9)
elif text=="=": elif text=="=":
# reset # reset
self.set_zoom(1) self.set_zoom(1)
@ -288,13 +186,9 @@ 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 particle # create new cross
if touch.button=="right": if touch.button=="right":
new=self.shape(touchx,touchy,color=self.color) new=Cross(touchx,touchy)
# snap to lattice
if self.lattice!=None:
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
self.particles.append(new) self.particles.append(new)
@ -314,10 +208,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.elements[0].pos self.offset=Point(touchx,touchy)-self.undermouse.squares[0].pos
# snap to lattice
if self.lattice!=None:
self.offset=self.lattice.nearest(self.offset)
# no modifiers # no modifiers
if self.modifiers==[]: if self.modifiers==[]:
@ -360,32 +251,16 @@ class Painter(Widget):
# respond to drag # respond to drag
def on_touch_move(self,touch): def on_touch_move(self,touch):
# convert to logical
touchx=self.coord_topos_x(touch.x)
touchy=self.coord_topos_y(touch.y)
# only respond to touch in drawing area # only respond to touch in drawing area
if self.collide_point(*touch.pos): if self.collide_point(*touch.pos):
# convert to logical
touchc=Point(self.coord_topos_x(touch.x),self.coord_topos_y(touch.y))
# snap to lattice
if self.lattice!=None:
touchc=self.lattice.nearest(touchc)
# 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(touchc-(self.offset+self.undermouse.elements[0].pos),0) delta=self.adjust_move(Point(touchx,touchy)-(self.offset+self.undermouse.squares[0].pos),0)
# snap to lattice
if self.lattice!=None:
delta=self.lattice.nearest(delta)
# check that the move is possible (which is not guaranteed after snapping to lattice)
if not self.check_interaction_unselected_list(self.selected,delta):
for particle in self.selected:
particle.move(delta)
# no lattice, move is guaranteed to be acceptable
else:
for particle in self.selected: for particle in self.selected:
particle.move(delta) particle.move(delta)
@ -430,15 +305,9 @@ 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 elt in particle.elements: for square in particle.squares:
# add offset if square.check_interaction(element.pos+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
@ -448,7 +317,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.elements: for element in particle.squares:
# 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)
@ -474,23 +343,18 @@ 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.elements: for obstacle in other.squares:
# move would make element overlap with obstacle # move would make element overlap with obstacle
element.pos+=delta if obstacle.check_interaction(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): if obstacle.check_touch(element.pos):
# move along obstacle while remaining stuck # move along obstacle while remaining stuck
newdelta=obstacle.move_along(delta,element) newdelta=obstacle.move_along(delta,element.pos)
else: else:
newdelta=obstacle.move_on_line_to_stick(element,delta) newdelta=obstacle.move_on_line_to_stick(element.pos,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:
@ -505,9 +369,6 @@ class Painter(Widget):
# set color of selected particles # set color of selected particles
def set_color(self,color): def set_color(self,color):
# set color for next particles
self.color=color
# set color of selected particles
for particle in self.selected: for particle in self.selected:
particle.color=color particle.color=color
# redraw # redraw
@ -527,46 +388,14 @@ class Painter(Widget):
# redraw # redraw
self.draw() self.draw()
# set voronoi for selected particles
def set_voronoi(self, onoff):
for particle in self.selected:
if onoff==0:
particle.voronoi=False
elif onoff==1:
particle.voronoi=True
elif onoff==-1:
particle.voronoi=not particle.voronoi
# redraw
self.draw()
# write configuration to file # write configuration to file
def write(self,file): def write(self,file):
ff=open(file,"w") ff=open(file,"w")
# save state (particle shape, zoom, lattice)
if self.shape==Cross:
ff.write("%shape=cross\n")
elif self.shape==Disk:
ff.write("%shape=disk\n")
elif self.shape==Staircase:
ff.write("%shape=staircase\n")
elif self.shape==Square2:
ff.write("%shape=2square\n")
else:
print("bug: unrecognized shape in write: '"+str(self.shape)+"'")
ff.write("%zoom={:1.1f}\n".format(self.base_size/50))
ff.write("%color={:1.1f},{:1.1f},{:1.1f}\n".format(self.color[0],self.color[1],self.color[2]))
if self.lattice != None:
ff.write("%lattice="+self.lattice.type+':'+str(self.lattice.spacing)+"\n")
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))
elif type(particle)==Disk: 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("{:d};".format(DISK_INDEX))
elif type(particle)==Staircase:
ff.write("{:d};".format(STAIRCASE_INDEX))
elif type(particle)==Square2:
ff.write("{:d};".format(SQUARE2_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
@ -600,35 +429,12 @@ class Painter(Widget):
if len(line)==0: if len(line)==0:
continue continue
# read options
if line[0]=='%':
# ignore empty line
if len(line)==1:
continue
[key,val]=line[1:].split('=',1)
if key=="shape":
self.app.command_prompt.run_set_shape(["set","shape",val])
elif key=="zoom":
self.app.command_prompt.run_set_zoom(["set","zoom",val])
elif key=="color":
color_str=val.split(',')
try:
self.set_color((float(color_str[0]),float(color_str[1]),float(color_str[2])))
except:
print("warning: ignoring line "+str(i)+" in file '"+file+"': color '"+color_str+"' cannot be read",file=sys.stderr)
# lattice is handled by main function
elif key=="lattice":
continue
else:
print("warning: ignoring line "+str(i)+" in file '"+file+"': unrecognized option '"+key+"'",file=sys.stderr)
continue
entries=line.split(";") entries=line.split(";")
# skip line if improperly formatted # skip line if improperly formatted
if len(entries)>3: if len(entries)>3:
print("warning: ignoring line "+str(i)+" in file '"+file+"': more than three ';' separated entries in '"+line+"'",file=sys.stderr) print("warning: ignoring line "+str(i)+" in file '"+file+"': more than three ';' spearated entries in '"+line+"'",file=sys.stderr)
if len(entries)<2: if len(entries)<2:
print("warning: ignoring line "+str(i)+" in file '"+file+"': fewer than two ';' separated entries in '"+line+"'",file=sys.stderr) print("warning: ignoring line "+str(i)+" in file '"+file+"': fewer than two ';' spearated entries in '"+line+"'",file=sys.stderr)
continue continue
# position # position
@ -666,12 +472,6 @@ 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)
elif particle_type==STAIRCASE_INDEX:
candidate=Staircase(pos.x,pos.y,color=color)
elif particle_type==SQUARE2_INDEX:
candidate=Square2(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
@ -708,13 +508,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)+"}")
elif type(particle)==Disk: 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("\disk{"+colors.closest_color(particle.color,colors.xcolor_names)+"}")
elif type(particle)==Staircase:
ff.write("\staircase{"+colors.closest_color(particle.color,colors.xcolor_names)+"}")
elif type(particle)==Square2:
ff.write("\square{"+colors.closest_color(particle.color,colors.xcolor_names)+"}")
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")
@ -724,14 +518,11 @@ class Painter(Widget):
# set zoom level # set zoom level
def set_zoom(self,level): def set_zoom(self,level):
self.base_size=level*50 Square_element.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
STAIRCASE_INDEX=3
SQUARE2_INDEX=4

View File

@ -12,8 +12,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# two-dimensional point structure
import math import math
# point in two dimensions # point in two dimensions
@ -63,7 +61,3 @@ 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

@ -12,16 +12,18 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# a polyomino is a collection of elements, defined in elements.py import math
from kivy.graphics import Color,Line import sys
from point import l_infinity from kivy.graphics import Color,Line,Rectangle
from element import Element_square,Element_circle
from point import Point,l_infinity
from tools import isint_nonzero,sgn,in_interval
# parent class of all polyominos # parent class of all polyominos
class Polyomino(): class Polyomino():
def __init__(self,**kwargs): def __init__(self,**kwargs):
# elements that make up the polyomino # square elements that maje up the polyomino
self.elements=kwargs.get("elements",[]) self.squares=kwargs.get("squares",[])
self.color=kwargs.get("color",(0,0,1)) self.color=kwargs.get("color",(0,0,1))
self.selected=False self.selected=False
@ -29,9 +31,6 @@ class Polyomino():
# mesh of background grid (no grid for mesh size 0) # mesh of background grid (no grid for mesh size 0)
self.grid=kwargs.get("grid",0) self.grid=kwargs.get("grid",0)
# draw Voronoi cell
self.voronoi=kwargs.get("voronoi",False)
# draw function # draw function
def draw(self,painter,**kwargs): def draw(self,painter,**kwargs):
alpha=kwargs.get("alpha",1) alpha=kwargs.get("alpha",1)
@ -43,137 +42,212 @@ class Polyomino():
# darken selected # darken selected
Color(r/2,g/2,b/2,alpha) Color(r/2,g/2,b/2,alpha)
for element in self.elements: for square in self.squares:
element.draw(painter) 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))
# 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 element in self.elements: for square in self.squares:
element.stroke(painter) Line(points=(
*(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 element in self.elements: for square in self.squares:
element.pos+=delta square.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 element in self.elements: for square in self.squares:
if element.in_support(x): if l_infinity(square.pos-x)<=1/2:
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 element1 in self.elements: for square1 in self.squares:
for element2 in candidate.elements: for square2 in candidate.squares:
# add offset if square1.check_interaction(square2.pos+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,elements=[Element_square(x,y,size=kwargs.get("size",1.0))]) super(Square,self).__init__(**kwargs,squares=[Square_element(x,y)])
# 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,elements=[\ super(Cross,self).__init__(**kwargs,squares=[\
Element_square(x,y,1,aspect=3),\ Square_element(x,y),\
Element_square(x+1,y,1),\ Square_element(x+1,y),\
Element_square(x-1,y,1)\ Square_element(x-1,y),\
Square_element(x,y+1),\
Square_element(x,y-1)\
]) ])
# redefine stroke to avoid lines between touching elements # redefine stroke to avoid lines between touching squares
def stroke(self,painter): def stroke(self,painter):
# convert to graphical coordinates # convert to graphical coordinates
coordx=painter.pos_tocoord_x(self.elements[0].pos.x) coordx=painter.pos_tocoord_x(self.squares[0].pos.x)
coordy=painter.pos_tocoord_y(self.elements[0].pos.y) coordy=painter.pos_tocoord_y(self.squares[0].pos.y)
Color(1,1,1) Color(1,1,1)
Line(points=( Line(points=(
*(coordx-0.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-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-1.5*Square_element.size),
*(coordx+0.5*painter.base_size,coordy-0.5*painter.base_size), *(coordx+0.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+1.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+0.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+1.5*Square_element.size),
*(coordx-0.5*painter.base_size,coordy+0.5*painter.base_size), *(coordx-0.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-1.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-0.5*Square_element.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):
super(Disk,self).__init__(**kwargs,elements=[Element_circle(x,y,size=kwargs.get("size",1.0))]) self.pos=Point(x,y)
# 3-staircase # set position
class Staircase(Polyomino): def setpos(self,x,y):
def __init__(self,x,y,**kwargs): self.pos.x=x
super(Staircase,self).__init__(**kwargs,elements=[\ self.pos.y=y
Element_square(x,y+1,1,aspect=3),\
Element_square(x+1,y,1),\
Element_square(x+1,y+1,1),\
Element_square(x+2,y,1)\
])
# redefine stroke to avoid lines between touching elements
def stroke(self,painter):
# convert to graphical coordinates
coordx=painter.pos_tocoord_x(self.elements[0].pos.x)
coordy=painter.pos_tocoord_y(self.elements[0].pos.y)
Color(1,1,1) # check whether a square at pos interacts with square
Line(points=( def check_interaction(self,pos):
*(coordx-0.5*painter.base_size,coordy-1.5*painter.base_size), return l_infinity(pos-self.pos)<1
*(coordx+2.5*painter.base_size,coordy-1.5*painter.base_size),
*(coordx+2.5*painter.base_size,coordy-0.5*painter.base_size),
*(coordx+1.5*painter.base_size,coordy-0.5*painter.base_size),
*(coordx+1.5*painter.base_size,coordy+0.5*painter.base_size),
*(coordx+0.5*painter.base_size,coordy+0.5*painter.base_size),
*(coordx+0.5*painter.base_size,coordy+1.5*painter.base_size),
*(coordx-0.5*painter.base_size,coordy+1.5*painter.base_size),
*(coordx-0.5*painter.base_size,coordy-1.5*painter.base_size),
))
# 2-square # check whether a square at position pos is touching self
class Square2(Polyomino): def check_touch(self,pos):
def __init__(self,x,y,**kwargs): # allow for error
super(Square2,self).__init__(**kwargs,elements=[\ if in_interval(l_infinity(pos-self.pos),1-1e-11,1+1e-11):
Element_square(x,y,1),\ return True
Element_square(x+1,y,1),\ return False
Element_square(x,y+1,1),\
Element_square(x+1,y+1,1)\
])
# redefine stroke to avoid lines between touching elements # find position along a line that comes in contact with the line going through pos in direction v
def stroke(self,painter): def move_on_line_to_stick(self,pos,v):
# convert to graphical coordinates # compute intersections with four lines making up square
coordx=painter.pos_tocoord_x(self.elements[0].pos.x) if v.x!=0:
coordy=painter.pos_tocoord_y(self.elements[0].pos.y) 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
Color(1,1,1)
Line(points=(
*(coordx-0.5*painter.base_size,coordy-0.5*painter.base_size),
*(coordx+1.5*painter.base_size,coordy-0.5*painter.base_size),
*(coordx+1.5*painter.base_size,coordy+1.5*painter.base_size),
*(coordx-0.5*painter.base_size,coordy+1.5*painter.base_size),
*(coordx-0.5*painter.base_size,coordy-0.5*painter.base_size),
))

View File

@ -1,17 +1,3 @@
# Copyright 2021-2023 Ian Jauslin
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from kivy.uix.label import Label from kivy.uix.label import Label
from kivy.graphics import Color,Rectangle from kivy.graphics import Color,Rectangle
from kivy.utils import escape_markup from kivy.utils import escape_markup
@ -59,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].elements[0].pos.x,self.app.painter.selected[0].elements[0].pos.y) 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)
else: else:
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) 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)
# 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,19 +1,3 @@
# Copyright 2021-2023 Ian Jauslin
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import math
# sign function # sign function
def sgn(x): def sgn(x):
if x>=0: if x>=0:
@ -36,10 +20,3 @@ 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