Painter: draw only squares

This commit is contained in:
Ian Jauslin 2021-11-24 19:22:05 -05:00
parent 221aa0a713
commit a7c18641a6
3 changed files with 131 additions and 102 deletions

5
jam
View File

@ -7,8 +7,7 @@ from kivy.config import Config
import sys
import os.path
from cross import Cross
from painter import Cross_painter
from painter import Painter
from status_bar import Status_bar
from command_prompt import Command_prompt
import filecheck
@ -32,7 +31,7 @@ class Jam_app(App):
layout=BoxLayout(orientation="vertical")
# painter
self.painter=Cross_painter(self)
self.painter=Painter(self)
# status bar
self.status_bar=Status_bar(self)
# command prompt

View File

@ -3,21 +3,25 @@ from kivy.uix.widget import Widget
from kivy.core.window import Window
from point import Point
from polyomino import Square
# cross painter
class Cross_painter(Widget):
# painter class
class Painter(Widget):
def __init__(self,app,**kwargs):
# list of crosses
self.crosses=[]
# list of particles
self.particles=[]
# cross under mouse
# particle under mouse
self.undermouse=None
# list of selected crosses
# list of selected particles
self.selected=[]
# relative position of mouse when moving
self.offset=Point(0,0)
# app is used to share information between widgets
self.app=app
@ -25,7 +29,7 @@ class Cross_painter(Widget):
self.modifiers=[]
# init Widget
super(Cross_painter,self).__init__(**kwargs)
super(Painter,self).__init__(**kwargs)
# init keyboard
self.keyboard = Window.request_keyboard(None,self,"text")
@ -33,17 +37,17 @@ class Cross_painter(Widget):
def reset(self):
self.crosses=[]
self.particles=[]
self.undermouse=None
self.draw()
# draw all crosses
# draw all particles
def draw(self):
self.canvas.clear()
with self.canvas:
for cross in self.crosses:
cross.draw()
for particle in self.particles:
particle.draw()
# respond to keyboard
@ -66,32 +70,36 @@ class Cross_painter(Widget):
# respond to mouse down
def on_touch_down(self,touch):
# only respond to touch in area
# only respond to touch in drawing area
if self.collide_point(*touch.pos):
# create new cross
if touch.button=="right":
if self.check_interaction_any(Point(touch.x/Cross.size,touch.y/Cross.size),None):
new=Cross(touch.x/Cross.size,touch.y/Cross.size)
new=Square(touch.x/Square_element.size,touch.y/Square_element.size)
if not self.check_interaction_any(new,Point(0,0)):
# add to list
self.crosses.append(new)
self.particles.append(new)
# unselect all crosses
# unselect all particles
for sel in self.selected:
sel.selected=False
self.selected=[]
self.draw()
# select cross
# select particle
if touch.button=="left":
# find cross under touch
self.undermouse=self.find_cross(Point(touch.x/Cross.size,touch.y/Cross.size))
# find particle under touch
self.undermouse=self.find_particle(Point(touch.x/Square_element.size,touch.y/Square_element.size))
# record relative position of click with respect to reference
if self.undermouse!=None:
self.offset=Point(touch.x/Square_element.size,touch.y/Square_element.size)-self.undermouse.squares[0].pos
# no modifiers
if self.modifiers==[]:
if self.undermouse==None or not self.undermouse in self.selected:
# unselect all crosses
# unselect all particles
for sel in self.selected:
sel.selected=False
self.selected=[]
@ -114,91 +122,92 @@ class Cross_painter(Widget):
# respond to drag
def on_touch_move(self,touch):
# only respond to touch in drawing area
if self.collide_point(*touch.pos):
# only move on left click
if touch.button=="left" and self.modifiers==[] and self.undermouse!=None:
# single cross
if not self.undermouse in self.selected:
self.undermouse.pos=self.check_move(Point(touch.x/Cross.size,touch.y/Cross.size),self.undermouse)
# multiple crosses
else:
self.group_move(touch)
# change in position
delta=self.check_move(Point(touch.x/Square_element.size,touch.y/Square_element.size)-(self.offset+self.undermouse.squares[0].pos),self.undermouse)
# multiple particles
# TODO: group moves
#else:
# self.group_move(touch)
# redraw
self.draw()
# move all selected crosses
def group_move(self,touch):
# save position of undermouse (to use it after it has been reset)
relative_position=self.undermouse.pos
## move all selected particles
#def group_move(self,touch):
# # save position of undermouse (to use it after it has been reset)
# relative_position=self.undermouse.pos
# determine order in which to move
# direction of motion
direction=Point(touch.x/Cross.size,touch.y/Cross.size)-self.undermouse.pos
# sort according to scalar product with direction
self.selected.sort(key=(lambda cross: direction.dot(cross.pos-self.undermouse.pos)),reverse=True)
# # determine order in which to move
# # direction of motion
# direction=Point(touch.x/Square_element.size,touch.y/Square_element.size)-self.undermouse.pos
# # sort according to scalar product with direction
# self.selected.sort(key=(lambda particle: direction.dot(particle.pos-self.undermouse.pos)),reverse=True)
# move
for cross in self.selected:
cross.pos=self.check_move(Point(touch.x/Cross.size-relative_position.x+cross.pos.x,touch.y/Cross.size-relative_position.y+cross.pos.y),cross)
## new position of undermouse
#self.undermouse.pos=self.check_move(Point(touch.x/Cross.size,touch.y/Cross.size),self.undermouse)
## move other crosses by the same amount as undermouse
#for cross in self.selected:
# if cross!=self.undermouse:
# cross.pos=self.check_move(Point(self.undermouse.pos.x-relative_position.x+cross.pos.x,self.undermouse.pos.y-relative_position.y+cross.pos.y),cross)
#
#for cross in self.selected:
# cross.pos=self.check_move(Point(touch.x/Cross.size-relative_position.x+cross.pos.x,touch.y/Cross.size-relative_position.y+cross.pos.y),cross)
# # move
# for particle in self.selected:
# particle.pos=self.check_move(Point(touch.x/Square_element.size-relative_position.x+particle.pos.x,touch.y/Square_element.size-relative_position.y+particle.pos.y),particle)
#
# ## new position of undermouse
# #self.undermouse.pos=self.check_move(Point(touch.x/Square_element.size,touch.y/Square_element.size),self.undermouse)
# ## move other particles by the same amount as undermouse
# #for particle in self.selected:
# # if particle!=self.undermouse:
# # particle.pos=self.check_move(Point(self.undermouse.pos.x-relative_position.x+particle.pos.x,self.undermouse.pos.y-relative_position.y+particle.pos.y),particle)
# #
# #for particle in self.selected:
# # particle.pos=self.check_move(Point(touch.x/Square_element.size-relative_position.x+particle.pos.x,touch.y/Square_element.size-relative_position.y+particle.pos.y),particle)
# find the cross at position pos
def find_cross(self,pos):
for cross in self.crosses:
if cross_distx(pos,cross.pos)<=0.5 or cross_disty(pos,cross.pos)<=0.5:
return cross
# find the particle at position pos
def find_particle(self,pos):
for particle in self.particles:
if particle.in_support(pos):
return particle
# none found
return None
# check whether a position intersects with any of the crosses
def check_interaction_any(self,pos,exception):
for other in self.crosses:
if other!=exception:
if other.check_interaction(pos)==False:
return False
return True
# check whether a position intersects with any of the particles
def check_interaction_any(self,candidate,offset):
for particle in self.particles:
if particle.check_interaction(candidate,offset):
return True
return False
# check that a cross can move to new position
def check_move(self,newpos,cross):
# check that a particle can move by delta, and return the closest allowed relative motion
def check_move(self,delta,particle):
# whether newpos is acceptable
accept_newpos=True
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:
for other in self.particles:
# do not compare a particle to itself
if other!=particle:
# move would make particle overlap with other
if other.check_interaction(particle,delta):
accept_newpos=False
# check if cross touches other
if other.check_touch(cross.pos):
# check if particle already touches other
if other.check_touch(particle):
# move along other while remaining stuck
candidate=other.move_along(newpos,cross.pos)
# TODO: this assumes other is a square
newdelta=other.squares[0].move_along(delta,particle.squares[0].pos)
else:
candidate=other.move_on_line_to_stick(cross.pos,newpos-cross.pos)
if self.check_interaction_any(candidate,cross):
return candidate
newdelta=other.move_on_line_to_stick(particle.squares[0].pos,delta)
if not self.check_interaction_any(particle,newdelta):
return newdelta
if accept_newpos:
return newpos
return delta
else:
# cannot move cross at all, try again
return self.check_move(candidate,cross)
# cannot move particle at all, try again
return self.check_move(newdelta,particle)
# write configuration to file
def write(self,file):
ff=open(file,"w")
for cross in self.crosses:
ff.write("{:05.2f},{:05.2f};{:3.1f},{:3.1f},{:3.1f}\n".format(cross.pos.x,cross.pos.y,cross.color[0],cross.color[1],cross.color[2]))
for particle in self.particles:
ff.write("{:05.2f},{:05.2f};{:3.1f},{:3.1f},{:3.1f}\n".format(particle.pos.x,particle.pos.y,particle.color[0],particle.color[1],particle.color[2]))
ff.close()
# read configuration from file
@ -267,7 +276,7 @@ class Cross_painter(Widget):
if self.check_interaction_any(pos,None):
# add to list
self.crosses.append(Cross(pos.x,pos.y,color=color))
self.particles.append(Cross(pos.x,pos.y,color=color))
else:
print("warning: ignoring line "+str(i)+" in file '"+file+"': particle overlaps with existing particles",file=sys.stderr)

View File

@ -41,12 +41,35 @@ class Polyomino():
*((square.pos.x+0.5)*square.size,(square.pos.y+0.5)*square.size),
*((square.pos.x+0.5)*square.size,(square.pos.y-0.5)*square.size)
))
# whether x is in the support of the polyomino
def in_support(self,x):
for square in self.squares:
if l_infinity(square.pos-x)<=1/2:
return True
return False
# check whether self interacts with candidate if candidate were moved by offset
def check_interaction(self,candidate,offset):
for square1 in self.squares:
for square2 in candidate.squares:
if square1.check_interaction(square2.pos+offset):
return True
return False
# check whether self touches candidate
def check_touch(self,candidate):
for square1 in self.squares:
for square2 in candidate.squares:
if square1.check_touch(square2.pos):
return True
return False
# square
class Square(Polyomino):
def __init__(self,x,y,**kwargs):
super(Square,self).__init__(squares=[Square_element(x,y)],kwargs)
super(Square,self).__init__(kwargs,squares=[Square_element(x,y)])
@ -66,7 +89,7 @@ class Square_element():
# check whether a square at pos interacts with square
def check_interaction(self,pos):
return l_infinity(pos-self.pos)>=1
return l_infinity(pos-self.pos)<1
# check whether a square at position pos is touching self
def check_touch(self,pos):
@ -102,60 +125,58 @@ class Square_element():
return closest
# move along edge of square
def move_along(self,newpos,pos):
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(newpos.y-pos.y)==-sgn(rel.y):
if sgn(delta.y)==-sgn(rel.y):
# stuck in x direction
return self.move_stuck_x(newpos,pos)
elif sgn(newpos.x-pos.x)==-sgn(rel.x):
return self.move_stuck_x(delta,pos)
elif sgn(delta.x)==-sgn(rel.x):
# stuck in y direction
return self.move_stuck_y(newpos,pos)
return self.move_stuck_y(delta,pos)
# stuck in both directions
return pos
else:
# stuck in x direction
return self.move_stuck_x(newpos,pos)
return self.move_stuck_x(delta,pos)
elif isint_nonzero(rel.y):
# stuck in y direction
return self.move_stuck_y(newpos,pos)
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,newpos,pos):
def move_stuck_x(self,delta,pos):
# only move in y direction
candidate=Point(pos.x,newpos.y)
candidate=Point(pos.x,pos.x+delta.y)
# do not move past corners
rel=pos.y-self.pos.y
newrel=newpos.y-self.pos.y
if newpos.y>pos.y:
if rel<math.ceil(rel)-1e-11 and newrel>math.ceil(rel)+1e-11 and math.ceil(rel)!=0:
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
else:
if rel>math.floor(rel)+1e-11 and newrel<math.floor(rel)-1e-11 and math.floor(rel)!=0:
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
return candidate
# move when stuck in the y direction
def move_stuck_y(self,newpos,pos):
def move_stuck_y(self,delta,pos):
# onlx move in x direction
candidate=Point(pos.x,newpos.x)
# do not move past corners
rel=pos.x-self.pos.x
newrel=newpos.x-self.pos.x
if newpos.x>pos.x:
if rel<math.ceil(rel)-1e-11 and newrel>math.ceil(rel)+1e-11 and math.ceil(rel)!=0:
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
else:
if rel>math.floor(rel)+1e-11 and newrel<math.floor(rel)-1e-11 and math.floor(rel)!=0:
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
return candidate