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
2021-12-01 20:37:07 +00:00
from kivy . graphics import Color , Line
2021-11-04 02:36:28 +00:00
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
2021-12-01 18:46:25 +00:00
self . unselected = [ ]
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 " )
2021-12-06 18:22:34 +00:00
self . keyboard . bind ( on_key_down = self . on_key_down , on_key_up = self . on_key_up , on_textinput = self . on_textinput )
2021-11-04 02:36:28 +00:00
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-12-01 20:37:07 +00:00
# draw grids
for particle in self . particles :
2021-12-01 23:49:35 +00:00
if particle . grid > 0 :
self . draw_grid ( particle . squares [ 0 ] . pos , particle . grid )
2021-12-01 20:37:07 +00:00
2021-11-25 00:22:05 +00:00
for particle in self . particles :
particle . draw ( )
2021-11-04 02:36:28 +00:00
2021-12-01 23:49:35 +00:00
def draw_grid ( self , pos , mesh ) :
2021-12-01 20:37:07 +00:00
# height offset due to status bar and command prompt
height_offset = self . app . status_bar . height + self . app . command_prompt . height
# vertical lines
# offest wrt 0
2021-12-01 23:58:24 +00:00
offset = ( pos . x - 0.5 ) % mesh
2021-12-01 23:49:35 +00:00
for i in range ( math . floor ( ( self . width / Square_element . size - offset ) / mesh ) + 1 ) :
2021-12-01 20:37:07 +00:00
Color ( 1 , 1 , 1 )
2021-12-01 23:49:35 +00:00
Line ( points = ( ( i * mesh + offset ) * Square_element . size , height_offset , ( i * mesh + offset ) * Square_element . size , self . height + height_offset ) )
2021-12-01 20:37:07 +00:00
# horizontal lines
# offset wrt 0
2021-12-01 23:58:24 +00:00
offset = ( pos . y - 0.5 ) % 1 - height_offset / Square_element . size
2021-12-01 23:49:35 +00:00
for i in range ( math . floor ( ( self . height / Square_element . size - offset ) / mesh ) + 1 ) :
2021-12-01 20:37:07 +00:00
Color ( 1 , 1 , 1 )
2021-12-01 23:49:35 +00:00
Line ( points = ( 0 , ( i * mesh + offset ) * Square_element . size + height_offset , self . width , ( i * mesh + offset ) * Square_element . size + height_offset ) )
2021-12-01 20:37:07 +00:00
2021-11-04 02:36:28 +00:00
# respond to keyboard
def on_key_down ( self , keyboard , keycode , text , modifiers ) :
2021-12-06 18:22:34 +00:00
# check the command_prompt is not focused
2021-11-04 02:36:28 +00:00
if not self . app . command_prompt . insert :
if keycode [ 1 ] == " shift " :
if not ' s ' in self . modifiers :
self . modifiers . append ( ' s ' )
self . modifiers . sort ( )
2021-12-01 23:54:37 +00:00
# remove particles
elif keycode [ 1 ] == " backspace " :
for particle in self . selected :
self . particles = remove_fromlist ( self . particles , particle )
self . selected = [ ]
self . draw ( )
2021-11-04 02:36:28 +00:00
2021-12-06 18:22:34 +00:00
# respond to keyboard (text input is modifier-sensitive, i.e. one can use shift)
def on_textinput ( self , window , text ) :
# check the command_prompt is not focused
if not self . app . command_prompt . insert :
2021-12-02 23:51:47 +00:00
# select all
2021-12-06 18:22:34 +00:00
if text == " a " :
2021-12-02 23:51:47 +00:00
for particle in self . particles :
particle . selected = True
self . selected = self . particles . copy ( )
self . unselected = [ ]
self . draw ( )
2021-12-06 18:11:39 +00:00
# toggle grid
2021-12-06 18:22:34 +00:00
elif text == " g " :
2021-12-06 18:11:39 +00:00
for particle in self . selected :
if particle . grid == 0 :
particle . grid = 1
else :
particle . grid = - particle . grid
self . draw ( )
2021-12-06 18:19:39 +00:00
# zoom
2021-12-06 18:22:34 +00:00
elif text == " + " :
2021-12-06 18:19:39 +00:00
# increment by 10%
self . set_zoom ( Square_element . size / 50 * 1.1 )
2021-12-06 18:22:34 +00:00
elif text == " - " :
2021-12-06 18:19:39 +00:00
# decrease by 10%
self . set_zoom ( Square_element . size / 50 * 0.9 )
2021-12-06 18:22:34 +00:00
elif text == " = " :
2021-12-06 18:19:39 +00:00
# reset
self . set_zoom ( 1 )
2021-11-04 02:36:28 +00:00
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-12-01 19:23:21 +00:00
# set color of selected particles
def set_color ( self , color ) :
for particle in self . selected :
particle . color = color
# redraw
self . draw ( )
2021-12-01 20:37:07 +00:00
# set grid for selected particles
2021-12-01 23:49:35 +00:00
# set mesh to -1 to toggle on/off
def set_grid ( self , mesh ) :
2021-12-01 20:37:07 +00:00
for particle in self . selected :
2021-12-01 23:49:35 +00:00
if mesh == - 1 :
if particle . grid == 0 :
particle . grid = 1
else :
particle . grid = - particle . grid
else :
particle . grid = mesh
2021-12-01 20:37:07 +00:00
# redraw
self . draw ( )
2021-12-01 19:23:21 +00:00
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 :
2021-12-01 18:46:25 +00:00
if type ( particle ) == Cross :
ff . write ( " {:d} ; " . format ( CROSS_INDEX ) )
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 ] ) )
2021-11-04 02:36:28 +00:00
ff . close ( )
# 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
2021-12-01 18:46:25 +00:00
if len ( entries ) > 3 :
print ( " warning: ignoring line " + str ( i ) + " in file ' " + file + " ' : more than three ' ; ' spearated entries in ' " + line + " ' " , file = sys . stderr )
if len ( entries ) < 2 :
print ( " warning: ignoring line " + str ( i ) + " in file ' " + file + " ' : fewer than two ' ; ' spearated entries in ' " + line + " ' " , file = sys . stderr )
2021-11-04 02:36:28 +00:00
continue
# position
2021-12-01 18:46:25 +00:00
pos_str = entries [ 1 ] . split ( " , " )
2021-11-04 02:36:28 +00:00
# skip line if improperly formatted
if len ( pos_str ) != 2 :
2021-12-01 18:46:25 +00:00
print ( " warning: ignoring line " + str ( i ) + " in file ' " + file + " ' : position ' " + entries [ 1 ] + " ' does not have two components " , file = sys . stderr )
2021-11-04 02:36:28 +00:00
continue
try :
pos = Point ( float ( pos_str [ 0 ] ) , float ( pos_str [ 1 ] ) )
except :
2021-12-01 18:46:25 +00:00
print ( " warning: ignoring line " + str ( i ) + " in file ' " + file + " ' : position ' " + entries [ 1 ] + " ' cannot be read " , file = sys . stderr )
2021-11-04 02:36:28 +00:00
continue
# color
color = ( 0 , 0 , 1 )
2021-12-01 18:46:25 +00:00
if len ( entries ) == 3 :
color_str = entries [ 2 ] . split ( " , " )
2021-11-04 02:36:28 +00:00
# skip line if improperly formatted
if len ( color_str ) != 3 :
2021-12-01 18:46:25 +00:00
print ( " warning: ignoring line " + str ( i ) + " in file ' " + file + " ' : color ' " + entries [ 2 ] + " ' does not have three components " , file = sys . stderr )
2021-11-04 02:36:28 +00:00
continue
try :
color = ( float ( color_str [ 0 ] ) , float ( color_str [ 1 ] ) , float ( color_str [ 2 ] ) )
except :
2021-12-01 18:46:25 +00:00
print ( " warning: ignoring line " + str ( i ) + " in file ' " + file + " ' : color ' " + entries [ 2 ] + " ' cannot be read " , file = sys . stderr )
2021-11-04 02:36:28 +00:00
continue
2021-12-01 18:46:25 +00:00
# candidate particle
try :
particle_type = int ( entries [ 0 ] )
except :
print ( " warning: ignoring line " + str ( i ) + " in file ' " + file + " ' : particle type ' " + entries [ 0 ] + " ' is not an integer " , file = sys . stderr )
continue
if particle_type == CROSS_INDEX :
candidate = Cross ( pos . x , pos . y , color = color )
else :
print ( " warning: ignoring line " + str ( i ) + " in file ' " + file + " ' : unrecognized particle type: ' " + entries [ 0 ] + " ' " , file = sys . stderr )
continue
if not self . check_interaction_any ( candidate , Point ( 0 , 0 ) ) :
2021-11-04 02:36:28 +00:00
# add to list
2021-12-01 18:46:25 +00:00
self . particles . append ( candidate )
self . unselected . append ( candidate )
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 ( )
2021-12-06 18:19:39 +00:00
# set zoom level
def set_zoom ( self , level ) :
Square_element . size = level * 50
self . draw ( )
2021-11-04 02:36:28 +00:00
2021-12-01 18:46:25 +00:00
# global variables (used like precompiler variables)
CROSS_INDEX = 1