2021-11-04 02:36:28 +00:00
import sys
2021-11-25 03:22:19 +00:00
import math
2021-11-04 02:36:28 +00:00
from kivy . uix . widget import Widget
from kivy . core . window import Window
from point import Point
2021-11-25 03:46:47 +00:00
from polyomino import Cross
2021-11-25 01:30:51 +00:00
from polyomino import Square_element
2021-11-04 02:36:28 +00:00
2021-11-25 03:22:19 +00:00
from tools import remove_fromlist
2021-11-25 00:22:05 +00:00
# painter class
class Painter ( Widget ) :
2021-11-04 02:36:28 +00:00
def __init__ ( self , app , * * kwargs ) :
2021-11-25 00:22:05 +00:00
# list of particles
self . particles = [ ]
2021-11-04 02:36:28 +00:00
2021-11-25 00:22:05 +00:00
# particle under mouse
2021-11-04 02:36:28 +00:00
self . undermouse = None
2021-11-25 00:22:05 +00:00
# list of selected particles
2021-11-04 02:36:28 +00:00
self . selected = [ ]
2021-11-25 03:22:19 +00:00
# complement
self . unselected = self . particles
2021-11-04 02:36:28 +00:00
2021-11-25 00:22:05 +00:00
# relative position of mouse when moving
self . offset = Point ( 0 , 0 )
2021-11-04 02:36:28 +00:00
# app is used to share information between widgets
self . app = app
# modifiers
self . modifiers = [ ]
# init Widget
2021-11-25 00:22:05 +00:00
super ( Painter , self ) . __init__ ( * * kwargs )
2021-11-04 02:36:28 +00:00
# init keyboard
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 )
def reset ( self ) :
2021-11-25 00:22:05 +00:00
self . particles = [ ]
2021-11-04 02:36:28 +00:00
self . undermouse = None
self . draw ( )
2021-11-25 00:22:05 +00:00
# draw all particles
2021-11-04 02:36:28 +00:00
def draw ( self ) :
self . canvas . clear ( )
with self . canvas :
2021-11-25 00:22:05 +00:00
for particle in self . particles :
particle . draw ( )
2021-11-04 02:36:28 +00:00
# respond to keyboard
def on_key_down ( self , keyboard , keycode , text , modifiers ) :
# check that command_prompt is not focused
if not self . app . command_prompt . insert :
if keycode [ 1 ] == " shift " :
if not ' s ' in self . modifiers :
self . modifiers . append ( ' s ' )
self . modifiers . sort ( )
def on_key_up ( self , keyboard , keycode ) :
if keycode [ 1 ] == " shift " :
if ' s ' in self . modifiers :
# remove
self . modifiers [ self . modifiers . index ( ' s ' ) ] = self . modifiers [ len ( self . modifiers ) - 1 ]
self . modifiers = self . modifiers [ : len ( self . modifiers ) - 1 ]
self . modifiers . sort ( )
# respond to mouse down
def on_touch_down ( self , touch ) :
2021-11-25 00:22:05 +00:00
# only respond to touch in drawing area
2021-11-04 02:36:28 +00:00
if self . collide_point ( * touch . pos ) :
# create new cross
if touch . button == " right " :
2021-11-25 03:46:47 +00:00
new = Cross ( touch . x / Square_element . size , touch . y / Square_element . size )
2021-11-25 00:22:05 +00:00
if not self . check_interaction_any ( new , Point ( 0 , 0 ) ) :
2021-11-04 02:36:28 +00:00
# add to list
2021-11-25 00:22:05 +00:00
self . particles . append ( new )
2021-11-04 02:36:28 +00:00
2021-11-25 00:22:05 +00:00
# unselect all particles
2021-11-04 02:36:28 +00:00
for sel in self . selected :
sel . selected = False
self . selected = [ ]
2021-11-25 03:22:19 +00:00
self . unselected = self . particles
2021-11-04 02:36:28 +00:00
self . draw ( )
2021-11-25 00:22:05 +00:00
# select particle
2021-11-04 02:36:28 +00:00
if touch . button == " left " :
2021-11-25 00:22:05 +00:00
# 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
2021-11-04 02:36:28 +00:00
# no modifiers
if self . modifiers == [ ] :
2021-11-25 03:22:19 +00:00
if self . undermouse == None :
2021-11-25 00:22:05 +00:00
# unselect all particles
2021-11-04 02:36:28 +00:00
for sel in self . selected :
sel . selected = False
self . selected = [ ]
2021-11-25 03:22:19 +00:00
self . unselected = self . particles
# select undermouse
elif not self . undermouse in self . selected :
for sel in self . selected :
sel . selected = False
self . selected = [ self . undermouse ]
self . unselected = self . particles . copy ( )
self . unselected = remove_fromlist ( self . unselected , self . undermouse )
self . undermouse . selected = True
2021-11-04 02:36:28 +00:00
# shift-click
elif self . modifiers == [ ' s ' ] :
if self . undermouse != None :
if self . undermouse not in self . selected :
self . selected . append ( self . undermouse )
self . undermouse . selected = True
2021-11-25 03:22:19 +00:00
# remove from unselected
self . unselected = remove_fromlist ( self . unselected , self . undermouse )
2021-11-04 02:36:28 +00:00
else :
# remove
2021-11-25 03:22:19 +00:00
self . selected = remove_fromlist ( self . selected , self . undermouse )
2021-11-04 02:36:28 +00:00
self . undermouse . selected = False
2021-11-25 03:22:19 +00:00
# add to unselected
self . unselected . append ( self . undermouse )
2021-11-04 02:36:28 +00:00
self . draw ( )
# respond to drag
def on_touch_move ( self , touch ) :
2021-11-25 00:22:05 +00:00
# only respond to touch in drawing area
2021-11-04 02:36:28 +00:00
if self . collide_point ( * touch . pos ) :
# only move on left click
if touch . button == " left " and self . modifiers == [ ] and self . undermouse != None :
2021-11-25 03:22:19 +00:00
# attempted move determined by the relative position to the relative position of click within self.undermouse
2021-11-25 03:49:42 +00:00
delta = self . adjust_move ( Point ( touch . x / Square_element . size , touch . y / Square_element . size ) - ( self . offset + self . undermouse . squares [ 0 ] . pos ) , 0 )
2021-11-25 03:22:19 +00:00
for particle in self . selected :
particle . move ( delta )
2021-11-04 02:36:28 +00:00
# redraw
self . draw ( )
2021-11-25 00:22:05 +00:00
# find the particle at position pos
def find_particle ( self , pos ) :
for particle in self . particles :
if particle . in_support ( pos ) :
return particle
2021-11-04 02:36:28 +00:00
# none found
return None
2021-11-25 03:22:19 +00:00
# check whether a candidate particle intersects with any of the particles
2021-11-25 00:22:05 +00:00
def check_interaction_any ( self , candidate , offset ) :
for particle in self . particles :
2021-11-25 01:30:51 +00:00
# do not check interaction if candidate=particle
if candidate != particle and particle . check_interaction ( candidate , offset ) :
2021-11-25 00:22:05 +00:00
return True
return False
2021-11-04 02:36:28 +00:00
2021-11-25 03:22:19 +00:00
# check whether shifting a list of particles by offset makes them interact with all particles
def check_interaction_list ( self , array , offset ) :
for candidate in array :
if self . check_interaction_any ( candidate , offset ) :
return True
return False
# check whether shifting a list of particles by offset makes them interact with the unselected particles
def check_interaction_unselected_list ( self , array , offset ) :
for candidate in array :
for particle in self . unselected :
if particle . check_interaction ( candidate , offset ) :
return True
return False
# check whether a candidate particle element with any of the unselected particles
def check_interaction_unselected_element ( self , element , offset ) :
for particle in self . unselected :
for square in particle . squares :
if square . check_interaction ( element . pos + offset ) :
return True
return False
# try to move all selected particles by delta, adjust if needed to avoid overlap with unselected particles
# we only track whether these elements collide with unselected particles, not with each other
2021-11-25 03:49:42 +00:00
def adjust_move ( self , delta , recursion_depth ) :
2021-11-25 03:22:19 +00:00
# actual_delta is the smallest (componentwise) of all the computed delta's
actual_delta = Point ( math . inf , math . inf )
for particle in self . selected :
for element in particle . squares :
# compute adjustment move due to unselected obstacles
2021-11-25 03:49:42 +00:00
adjusted_delta = self . adjust_move_element ( delta , element , 0 )
2021-11-25 03:22:19 +00:00
# only keep the smallest delta's (in absolute value)
if abs ( adjusted_delta . x ) < abs ( actual_delta . x ) :
actual_delta . x = adjusted_delta . x
if abs ( adjusted_delta . y ) < abs ( actual_delta . y ) :
actual_delta . y = adjusted_delta . y
# try to move by actual_delta
if not self . check_interaction_unselected_list ( self . selected , actual_delta ) :
return actual_delta
else :
# cannot move particles at all, try again
2021-11-25 03:49:42 +00:00
# give up if tried too many times
if recursion_depth > 100 :
print ( " warning: recursion depth exceeded when adjusting move by delta=( " , delta . x , " , " , delta . y , " ) " , file = sys . stderr )
return Point ( 0 , 0 )
else :
return self . adjust_move ( actual_delta , recursion_depth + 1 )
2021-11-25 03:22:19 +00:00
# trying to move a single element by delta, adjust if needed to avoid overlap with unselected particles
2021-11-25 03:49:42 +00:00
def adjust_move_element ( self , delta , element , recursion_depth ) :
2021-11-04 02:36:28 +00:00
# whether newpos is acceptable
accept_newpos = True
2021-11-25 03:22:19 +00:00
for other in self . unselected :
for obstacle in other . squares :
# move would make element overlap with obstacle
if obstacle . check_interaction ( element . pos + delta ) :
2021-11-04 02:36:28 +00:00
accept_newpos = False
2021-11-25 03:22:19 +00:00
# check if particle already touches obstacle
if obstacle . check_touch ( element . pos ) :
# move along obstacle while remaining stuck
newdelta = obstacle . move_along ( delta , element . pos )
2021-11-04 02:36:28 +00:00
else :
2021-11-25 03:22:19 +00:00
newdelta = obstacle . move_on_line_to_stick ( element . pos , delta )
if not self . check_interaction_unselected_element ( element , newdelta ) :
2021-11-25 00:22:05 +00:00
return newdelta
2021-11-04 02:36:28 +00:00
if accept_newpos :
2021-11-25 00:22:05 +00:00
return delta
2021-11-04 02:36:28 +00:00
else :
2021-11-25 00:22:05 +00:00
# cannot move particle at all, try again
2021-11-25 03:49:42 +00:00
# give up if tried too many times
if recursion_depth > 100 :
print ( " warning: recursion depth exceeded when adjusting move of element at ( " , element . pos . x , " , " , element . pos . y , " ) by delta=( " , delta . x , " , " , delta . y , " ) " , file = sys . stderr )
return Point ( 0 , 0 )
else :
return self . adjust_move_element ( newdelta , element , recursion_depth + 1 )
2021-11-04 02:36:28 +00:00
2021-11-25 03:22:19 +00:00
# TODO adapt
2021-11-04 02:36:28 +00:00
# write configuration to file
def write ( self , file ) :
ff = open ( file , " w " )
2021-11-25 00:22:05 +00:00
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 ] ) )
2021-11-04 02:36:28 +00:00
ff . close ( )
2021-11-25 03:22:19 +00:00
# TODO adapt
2021-11-04 02:36:28 +00:00
# read configuration from file
def read ( self , file ) :
self . reset ( )
try :
ff = open ( file , " r " )
except :
self . app . command_prompt . message = " error: could not read file ' " + file + " ' (this should not happen and is probably a bug) "
return
# counter
i = 0
try :
lines = ff . readlines ( )
except :
self . app . command_prompt . message = " error: could not read the contents of file ' " + file + " ' "
return
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
entries = line . split ( " ; " )
# skip line if improperly formatted
if len ( entries ) > 2 :
print ( " warning: ignoring line " + str ( i ) + " in file ' " + file + " ' : more than two ' ; ' spearated entries in ' " + line + " ' " , file = sys . stderr )
continue
# position
pos_str = entries [ 0 ] . split ( " , " )
# skip line if improperly formatted
if len ( pos_str ) != 2 :
print ( " warning: ignoring line " + str ( i ) + " in file ' " + file + " ' : position ' " + entries [ 0 ] + " ' does not have two components " , file = sys . stderr )
continue
try :
pos = Point ( float ( pos_str [ 0 ] ) , float ( pos_str [ 1 ] ) )
except :
print ( " warning: ignoring line " + str ( i ) + " in file ' " + file + " ' : position ' " + entries [ 0 ] + " ' cannot be read " , file = sys . stderr )
continue
# color
color = ( 0 , 0 , 1 )
if len ( entries ) == 2 :
color_str = entries [ 1 ] . split ( " , " )
# skip line if improperly formatted
if len ( color_str ) != 3 :
print ( " warning: ignoring line " + str ( i ) + " in file ' " + file + " ' : color ' " + entries [ 1 ] + " ' does not have three components " , file = sys . stderr )
continue
try :
color = ( float ( color_str [ 0 ] ) , float ( color_str [ 1 ] ) , float ( color_str [ 2 ] ) )
except :
print ( " warning: ignoring line " + str ( i ) + " in file ' " + file + " ' : color ' " + entries [ 1 ] + " ' cannot be read " , file = sys . stderr )
continue
if self . check_interaction_any ( pos , None ) :
# add to list
2021-11-25 00:22:05 +00:00
self . particles . append ( Cross ( pos . x , pos . y , color = color ) )
2021-11-04 02:36:28 +00:00
else :
print ( " warning: ignoring line " + str ( i ) + " in file ' " + file + " ' : particle overlaps with existing particles " , file = sys . stderr )
ff . close ( )
self . draw ( )