diff --git a/cross.py b/cross.py index 86b3220..527a202 100644 --- a/cross.py +++ b/cross.py @@ -1,8 +1,6 @@ import math import sys from kivy.graphics import Color,Line,Rectangle -from kivy.uix.widget import Widget -from kivy.core.window import Window from point import Point from tools import isint_nonzero,sgn @@ -213,242 +211,3 @@ def cross_polar(t): -# cross painter -class Cross_painter(Widget): - - def __init__(self,app,**kwargs): - - # list of crosses - self.crosses=[] - - # cross under mouse - self.undermouse=None - - # list of selected crosses - self.selected=[] - - # app is used to share information between widgets - self.app=app - - # modifiers - self.modifiers=[] - - # init Widget - super(Cross_painter,self).__init__(**kwargs) - - # 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): - self.crosses=[] - self.undermouse=None - self.draw() - - - # draw all crosses - def draw(self): - self.canvas.clear() - with self.canvas: - for cross in self.crosses: - cross.draw() - - - # 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): - # only respond to touch in 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) - # add to list - self.crosses.append(new) - - # unselect all crosses - for sel in self.selected: - sel.selected=False - self.selected=[] - - self.draw() - - # select cross - if touch.button=="left": - # find cross under touch - self.undermouse=self.find_cross(Point(touch.x/Cross.size,touch.y/Cross.size)) - - # no modifiers - if self.modifiers==[]: - if self.undermouse==None or not self.undermouse in self.selected: - # unselect all crosses - for sel in self.selected: - sel.selected=False - self.selected=[] - - - # 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 - else: - # remove - self.selected[self.selected.index(self.undermouse)]=self.selected[len(self.selected)-1] - self.selected=self.selected[:len(self.selected)-1] - self.undermouse.selected=False - - self.draw() - - - # respond to drag - def on_touch_move(self,touch): - if self.collide_point(*touch.pos): - # only move on left click - if touch.button=="left" and self.undermouse!=None: - self.undermouse.pos=self.check_move(Point(touch.x/Cross.size,touch.y/Cross.size),self.undermouse) - # redraw - self.draw() - - - # 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 - # 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 that a cross can move to new position - def check_move(self,newpos,cross): - # 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: - accept_newpos=False - # check if cross touches other - if other.check_touch(cross.pos): - # move along other while remaining stuck - candidate=other.move_along(newpos,cross.pos) - else: - candidate=other.move_on_line_to_stick(cross.pos,newpos-cross.pos) - if self.check_interaction_any(candidate,cross): - return candidate - if accept_newpos: - return newpos - else: - # cannot move cross at all, try again - return self.check_move(candidate,cross) - - - # 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])) - 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 - 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 - self.crosses.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) - - ff.close() - self.draw() - diff --git a/jam b/jam index 63103f9..d824808 100755 --- a/jam +++ b/jam @@ -7,7 +7,8 @@ from kivy.config import Config import sys import os.path -from cross import Cross,Cross_painter +from cross import Cross +from painter import Cross_painter from status_bar import Status_bar from command_prompt import Command_prompt import filecheck diff --git a/painter.py b/painter.py new file mode 100644 index 0000000..c9b751b --- /dev/null +++ b/painter.py @@ -0,0 +1,277 @@ +import sys +from kivy.uix.widget import Widget +from kivy.core.window import Window + +from point import Point + +# cross painter +class Cross_painter(Widget): + + def __init__(self,app,**kwargs): + + # list of crosses + self.crosses=[] + + # cross under mouse + self.undermouse=None + + # list of selected crosses + self.selected=[] + + # app is used to share information between widgets + self.app=app + + # modifiers + self.modifiers=[] + + # init Widget + super(Cross_painter,self).__init__(**kwargs) + + # 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): + self.crosses=[] + self.undermouse=None + self.draw() + + + # draw all crosses + def draw(self): + self.canvas.clear() + with self.canvas: + for cross in self.crosses: + cross.draw() + + + # 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): + # only respond to touch in 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) + # add to list + self.crosses.append(new) + + # unselect all crosses + for sel in self.selected: + sel.selected=False + self.selected=[] + + self.draw() + + # select cross + if touch.button=="left": + # find cross under touch + self.undermouse=self.find_cross(Point(touch.x/Cross.size,touch.y/Cross.size)) + + # no modifiers + if self.modifiers==[]: + if self.undermouse==None or not self.undermouse in self.selected: + # unselect all crosses + for sel in self.selected: + sel.selected=False + self.selected=[] + + + # 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 + else: + # remove + self.selected[self.selected.index(self.undermouse)]=self.selected[len(self.selected)-1] + self.selected=self.selected[:len(self.selected)-1] + self.undermouse.selected=False + + self.draw() + + + # respond to drag + def on_touch_move(self,touch): + 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) + + # 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 + + # 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) + + # 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) + + + # 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 + # 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 that a cross can move to new position + def check_move(self,newpos,cross): + # 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: + accept_newpos=False + # check if cross touches other + if other.check_touch(cross.pos): + # move along other while remaining stuck + candidate=other.move_along(newpos,cross.pos) + else: + candidate=other.move_on_line_to_stick(cross.pos,newpos-cross.pos) + if self.check_interaction_any(candidate,cross): + return candidate + if accept_newpos: + return newpos + else: + # cannot move cross at all, try again + return self.check_move(candidate,cross) + + + # 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])) + 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 + 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 + self.crosses.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) + + ff.close() + self.draw() + +