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 sys
import os.path import os.path
from cross import Cross from painter import Painter
from painter import Cross_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 import filecheck
@ -32,7 +31,7 @@ class Jam_app(App):
layout=BoxLayout(orientation="vertical") layout=BoxLayout(orientation="vertical")
# painter # painter
self.painter=Cross_painter(self) self.painter=Painter(self)
# status bar # status bar
self.status_bar=Status_bar(self) self.status_bar=Status_bar(self)
# command prompt # command prompt

View File

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

View File

@ -42,11 +42,34 @@ 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 # square
class Square(Polyomino): class Square(Polyomino):
def __init__(self,x,y,**kwargs): 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 # check whether a square at pos interacts with square
def check_interaction(self,pos): 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 # check whether a square at position pos is touching self
def check_touch(self,pos): def check_touch(self,pos):
@ -102,60 +125,58 @@ class Square_element():
return closest return closest
# move along edge of square # move along edge of square
def move_along(self,newpos,pos): def move_along(self,delta,pos):
rel=pos-self.pos rel=pos-self.pos
# check if the particle is stuck in the x direction # check if the particle is stuck in the x direction
if isint_nonzero(rel.x): if isint_nonzero(rel.x):
# check y direction # check y direction
if isint_nonzero(rel.y): if isint_nonzero(rel.y):
# in corner # in corner
if sgn(newpos.y-pos.y)==-sgn(rel.y): if sgn(delta.y)==-sgn(rel.y):
# stuck in x direction # stuck in x direction
return self.move_stuck_x(newpos,pos) return self.move_stuck_x(delta,pos)
elif sgn(newpos.x-pos.x)==-sgn(rel.x): elif sgn(delta.x)==-sgn(rel.x):
# stuck in y direction # stuck in y direction
return self.move_stuck_y(newpos,pos) return self.move_stuck_y(delta,pos)
# stuck in both directions # stuck in both directions
return pos return pos
else: else:
# stuck in x direction # stuck in x direction
return self.move_stuck_x(newpos,pos) return self.move_stuck_x(delta,pos)
elif isint_nonzero(rel.y): elif isint_nonzero(rel.y):
# stuck in y direction # stuck in y direction
return self.move_stuck_y(newpos,pos) return self.move_stuck_y(delta,pos)
# this should never happen # this should never happen
else: else:
print("error: stuck particle has non-integer relative position: (",rel.x,",",rel.y,")",file=sys.stderr) print("error: stuck particle has non-integer relative position: (",rel.x,",",rel.y,")",file=sys.stderr)
exit(-1) exit(-1)
# move when stuck in the x direction # 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 # only move in y direction
candidate=Point(pos.x,newpos.y) candidate=Point(pos.x,pos.x+delta.y)
# do not move past corners # do not move past corners
rel=pos.y-self.pos.y rel=pos.y-self.pos.y
newrel=newpos.y-self.pos.y if delta.y>0:
if newpos.y>pos.y: if rel<math.ceil(rel)-1e-11 and delta.y+rel>math.ceil(rel)+1e-11 and math.ceil(rel)!=0:
if rel<math.ceil(rel)-1e-11 and newrel>math.ceil(rel)+1e-11 and math.ceil(rel)!=0:
# stick to corner # stick to corner
candidate.y=math.ceil(rel)+self.pos.y candidate.y=math.ceil(rel)+self.pos.y
else: 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 # stick to corner
candidate.y=math.floor(rel)+self.pos.y candidate.y=math.floor(rel)+self.pos.y
return candidate return candidate
# move when stuck in the y direction # 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 # onlx move in x direction
candidate=Point(pos.x,newpos.x) candidate=Point(pos.x,newpos.x)
# do not move past corners # do not move past corners
rel=pos.x-self.pos.x rel=pos.x-self.pos.x
newrel=newpos.x-self.pos.x if delta.x>0:
if newpos.x>pos.x: if rel<math.ceil(rel)-1e-11 and delta.x+rel>math.ceil(rel)+1e-11 and math.ceil(rel)!=0:
if rel<math.ceil(rel)-1e-11 and newrel>math.ceil(rel)+1e-11 and math.ceil(rel)!=0:
# stick to corner # stick to corner
candidate.x=math.ceil(rel)+self.pos.x candidate.x=math.ceil(rel)+self.pos.x
else: 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 # stick to corner
candidate.x=math.floor(rel)+self.pos.x candidate.x=math.floor(rel)+self.pos.x
return candidate return candidate