Rename to src
This commit is contained in:
463
src/command_prompt.py
Normal file
463
src/command_prompt.py
Normal file
@ -0,0 +1,463 @@
|
||||
from kivy.uix.label import Label
|
||||
from kivy.core.window import Window
|
||||
from kivy.graphics import Color,Rectangle
|
||||
import glob
|
||||
import os.path
|
||||
|
||||
import filecheck
|
||||
|
||||
class Command_prompt(Label):
|
||||
|
||||
def __init__(self,app,**kwargs):
|
||||
# app is used to share information between widgets
|
||||
self.app=app
|
||||
|
||||
# insert mode
|
||||
self.insert=False
|
||||
|
||||
# width of a letter
|
||||
self.char_width=9
|
||||
|
||||
# cursor position
|
||||
self.cursor=0
|
||||
|
||||
# the text, with no color information
|
||||
self.raw_text=""
|
||||
|
||||
# a one-time message that shows up over the bar
|
||||
self.message=""
|
||||
|
||||
# array of command with arguments
|
||||
self.argv=[]
|
||||
|
||||
# display characters from window_offset onwards (for text wrapping)
|
||||
self.window_offset=0
|
||||
|
||||
# init Label
|
||||
super(Command_prompt,self).__init__(**kwargs)
|
||||
|
||||
self.keyboard = Window.request_keyboard(None,self,"text")
|
||||
self.keyboard.bind(on_textinput=self.on_textinput,on_key_down=self.on_key_down)
|
||||
|
||||
self.draw()
|
||||
|
||||
|
||||
def draw(self):
|
||||
self.canvas.before.clear()
|
||||
|
||||
# background
|
||||
with self.canvas.before:
|
||||
Color(0,0,0)
|
||||
Rectangle(pos=self.pos,size=self.size)
|
||||
|
||||
# if message is not empty, draw message instead
|
||||
if self.message!="":
|
||||
# do not wrap
|
||||
self.text=self.message[:min(len(self.message),int(self.width/self.char_width))]
|
||||
self.message=""
|
||||
return
|
||||
|
||||
# wrap text
|
||||
window_size=int(self.width/self.char_width)
|
||||
if self.cursor>=self.window_offset+window_size:
|
||||
self.window_offset=self.cursor-window_size+1
|
||||
elif self.cursor<self.window_offset:
|
||||
self.window_offset=self.cursor
|
||||
# do not stop right before ':'
|
||||
if self.window_offset==1:
|
||||
self.window_offset=0
|
||||
|
||||
# cursor
|
||||
with self.canvas.before:
|
||||
Color(1,1,1)
|
||||
# wrap cursor position
|
||||
Rectangle(pos=(self.pos[0]+(self.cursor-self.window_offset)*self.char_width,self.pos[1]),size=(self.char_width,self.height))
|
||||
|
||||
# make text under cursor black
|
||||
end=min(len(self.raw_text),self.window_offset+window_size)
|
||||
if self.cursor<end:
|
||||
self.text=self.raw_text[self.window_offset:self.cursor]+"[color=000]"+self.raw_text[self.cursor]+"[/color]"+self.raw_text[self.cursor+1:end]
|
||||
else:
|
||||
self.text=self.raw_text[self.window_offset:end]
|
||||
|
||||
|
||||
def on_key_down(self, keyboard, keycode, text, modifiers):
|
||||
#print(keycode,text,modifiers)
|
||||
if self.insert:
|
||||
# process modifiers
|
||||
mods=self.process_modifiers(modifiers)
|
||||
|
||||
# process command
|
||||
if keycode[1]=="enter":
|
||||
self.insert=False
|
||||
self.run_command()
|
||||
self.set_text("")
|
||||
elif keycode[1]=="escape":
|
||||
self.insert=False
|
||||
self.set_text("")
|
||||
|
||||
# move
|
||||
elif keycode[1]=="left":
|
||||
self.move_cursor_relative(-1)
|
||||
elif keycode[1]=="right":
|
||||
self.move_cursor_relative(1)
|
||||
elif mods==['c'] and text=="a":
|
||||
self.move_cursor(1)
|
||||
elif mods==['c'] and text=="e":
|
||||
self.move_cursor(len(self.raw_text))
|
||||
|
||||
# delete
|
||||
elif keycode[1]=="backspace":
|
||||
self.backspace()
|
||||
elif keycode[1]=="delete":
|
||||
self.delete_forward()
|
||||
elif mods==['c'] and text=="k":
|
||||
self.delete_all_forward()
|
||||
|
||||
# tab completion
|
||||
elif keycode[1]=="tab":
|
||||
self.tab_complete()
|
||||
|
||||
def on_textinput(self,window,text):
|
||||
# reset status_bar
|
||||
self.app.status_bar.draw()
|
||||
if self.insert:
|
||||
self.append_text_at_cursor(text)
|
||||
elif text==':':
|
||||
self.append_text_at_cursor(text)
|
||||
self.insert=True
|
||||
|
||||
|
||||
# get modifiers from list
|
||||
# returns an alphabetically ordered array of characters
|
||||
# 'a' is alt
|
||||
# 'c' is ctrl
|
||||
# 'm' is meta
|
||||
# 's' is shift
|
||||
# ignore all others
|
||||
def process_modifiers(self,modifiers):
|
||||
out=[]
|
||||
for mod in modifiers:
|
||||
if mod=="alt":
|
||||
out.append('a')
|
||||
elif mod=="ctrl":
|
||||
out.append('c')
|
||||
elif mod=="meta":
|
||||
out.append('m')
|
||||
elif mod=="shift":
|
||||
out.append('s')
|
||||
out.sort()
|
||||
return out
|
||||
|
||||
|
||||
|
||||
# append to text at position of cursor
|
||||
def append_text_at_cursor(self,text):
|
||||
if self.cursor==len(self.raw_text):
|
||||
self.raw_text+=text
|
||||
else:
|
||||
self.raw_text=self.raw_text[:self.cursor]+text+self.raw_text[self.cursor:]
|
||||
self.cursor+=len(text)
|
||||
self.draw()
|
||||
|
||||
# delete before cursor
|
||||
def backspace(self):
|
||||
# do not delete leading ':'
|
||||
if self.cursor==1:
|
||||
return
|
||||
if self.cursor==len(self.raw_text):
|
||||
self.raw_text=self.raw_text[:self.cursor-1]
|
||||
else:
|
||||
self.raw_text=self.raw_text[:self.cursor-1]+self.raw_text[self.cursor:]
|
||||
self.cursor-=1
|
||||
self.draw()
|
||||
# delete after cursor
|
||||
def delete_forward(self):
|
||||
if self.cursor==len(self.raw_text):
|
||||
return
|
||||
else:
|
||||
self.raw_text=self.raw_text[:self.cursor]+self.raw_text[self.cursor+1:]
|
||||
self.draw()
|
||||
# delete until end of line(self):
|
||||
def delete_all_forward(self):
|
||||
if self.cursor==len(self.raw_text):
|
||||
return
|
||||
else:
|
||||
self.raw_text=self.raw_text[:self.cursor]
|
||||
self.draw()
|
||||
|
||||
|
||||
# set text in prompt
|
||||
def set_text(self,text):
|
||||
self.raw_text=text
|
||||
self.cursor=len(text)
|
||||
self.draw()
|
||||
|
||||
|
||||
# move cursor n steps to right (left if negative)
|
||||
def move_cursor_relative(self,n):
|
||||
if n==0 or (n>0 and self.cursor==len(self.raw_text)) or (n<0 and self.cursor==0):
|
||||
return
|
||||
self.cursor=max(1,min(len(self.raw_text),self.cursor+n))
|
||||
self.draw()
|
||||
|
||||
# move cursor to absolute position
|
||||
def move_cursor(self,n):
|
||||
self.cursor=max(1,min(len(self.raw_text),n))
|
||||
self.draw()
|
||||
|
||||
|
||||
# parse text as argv
|
||||
def parse_argv(self):
|
||||
# init
|
||||
self.argv=[""]
|
||||
|
||||
# return position of cursor
|
||||
cursor=(0,0)
|
||||
|
||||
# whether inside quotes
|
||||
single_quoted=False
|
||||
double_quoted=False
|
||||
# whether after '\'
|
||||
backslashed=False
|
||||
|
||||
# start after ':'
|
||||
# proceed one character at a time
|
||||
for i in range(1,len(self.raw_text)):
|
||||
char=self.raw_text[i]
|
||||
|
||||
# split argument
|
||||
if single_quoted==False and double_quoted==False and backslashed==False and char==' ':
|
||||
# new argv
|
||||
self.argv.append("")
|
||||
|
||||
# quotes or '\'
|
||||
elif double_quoted==False and backslashed==False and char=='\'':
|
||||
single_quoted=not single_quoted
|
||||
elif single_quoted==False and backslashed==False and char=='"':
|
||||
double_quoted=not double_quoted
|
||||
elif single_quoted==False and backslashed==False and char=='\\':
|
||||
backslashed=True
|
||||
|
||||
# write character
|
||||
else:
|
||||
self.argv[len(self.argv)-1]+=char
|
||||
# reset backslash
|
||||
backslashed=False
|
||||
|
||||
# record position of cursor
|
||||
if self.cursor==i+1:
|
||||
cursor=(len(self.argv)-1,len(self.argv[len(self.argv)-1]))
|
||||
|
||||
|
||||
return cursor
|
||||
|
||||
|
||||
# tab completion
|
||||
def tab_complete(self):
|
||||
# parse argv
|
||||
cursor=self.parse_argv()
|
||||
|
||||
# write and edit commands
|
||||
if self.argv[0]=="w" or self.argv[0]=="e":
|
||||
# check that cursor is in first argument
|
||||
if cursor[0]==1:
|
||||
# complete filesystem path
|
||||
self.append_text_at_cursor(self.complete_path(self.argv[cursor[0]],cursor[1]))
|
||||
|
||||
# complete filesystem path
|
||||
def complete_path(self,base,end):
|
||||
paths=glob.glob(glob.escape(base[:end])+"*")
|
||||
print(glob.escape(base[:end])+"*", paths)
|
||||
if len(paths)==0:
|
||||
return ""
|
||||
elif len(paths)==1:
|
||||
# append '/' to directories
|
||||
if os.path.isdir(paths[0]):
|
||||
return paths[0][end:]+"/"
|
||||
else:
|
||||
return paths[0][end:]
|
||||
else:
|
||||
# display in status bar
|
||||
self.app.status_bar.message=""
|
||||
for path in paths:
|
||||
name=os.path.basename(path)
|
||||
# add quotes if needed
|
||||
if " " in name:
|
||||
name="\""+name+"\""
|
||||
self.app.status_bar.message+=name+" "
|
||||
self.app.status_bar.draw()
|
||||
return os.path.commonprefix(paths)[end:]
|
||||
|
||||
|
||||
# run command
|
||||
def run_command(self):
|
||||
# parse command line
|
||||
self.parse_argv()
|
||||
|
||||
# commands that cannot be compounded
|
||||
if self.argv[0]=="set":
|
||||
self.run_set(self.argv)
|
||||
return
|
||||
|
||||
# single letter commands (which can be combined)
|
||||
# write
|
||||
if self.argv[0]=="w" or self.argv[0]=="w!" or self.argv[0]=="wq":
|
||||
self.run_write(self.argv)
|
||||
# edit file
|
||||
if self.argv[0]=="e":
|
||||
self.run_edit(self.argv)
|
||||
# quit
|
||||
if self.argv[0]=="q" or self.argv=="wq":
|
||||
self.app.stop()
|
||||
|
||||
# set properties of particles
|
||||
def run_set(self,argv):
|
||||
if len(argv)<2:
|
||||
self.message="error: 'set' command was run with too few arguments -- usage: 'set <property> <value>'"
|
||||
return
|
||||
|
||||
if argv[1]=="color":
|
||||
self.run_set_color(argv)
|
||||
return
|
||||
elif argv[1]=="grid":
|
||||
self.run_set_grid(argv)
|
||||
return
|
||||
elif argv[1]=="zoom":
|
||||
self.run_set_zoom(argv)
|
||||
else:
|
||||
self.message="error: unrecognized command '"+argv[1]+"'"
|
||||
return
|
||||
|
||||
# set color
|
||||
def run_set_color(self,argv):
|
||||
if len(argv)<3:
|
||||
self.message="error: 'set color' command was run with without anargument -- usage: 'set color <color_descriptor>'"
|
||||
return
|
||||
if argv[2]=="blue":
|
||||
self.app.painter.set_color((0,0,1))
|
||||
elif argv[2]=="green":
|
||||
self.app.painter.set_color((0,1,0))
|
||||
elif argv[2]=="red":
|
||||
self.app.painter.set_color((1,0,0))
|
||||
elif argv[2]=="cyan":
|
||||
self.app.painter.set_color((0,1,1))
|
||||
elif argv[2]=="magenta":
|
||||
self.app.painter.set_color((1,0,1))
|
||||
elif argv[2]=="yellow":
|
||||
self.app.painter.set_color((1,1,0))
|
||||
elif argv[2]=="white":
|
||||
self.app.painter.set_color((1,1,1))
|
||||
elif argv[2]=="black":
|
||||
self.app.painter.set_color((0,0,0))
|
||||
elif argv[2]=="gray":
|
||||
self.app.painter.set_color((0.5,0.5,0.5))
|
||||
else:
|
||||
color_str=argv[2].split(",")
|
||||
# error if improperly formatted
|
||||
if len(color_str)!=3:
|
||||
self.message="error: unrecognized color specification '"+argv[2]+"'; supported format is 'r,g,b' or one of red|green|blue|cyan|magenta|yellow|white|black|gray"
|
||||
return
|
||||
try:
|
||||
color=(float(color_str[0]),float(color_str[1]),float(color_str[2]))
|
||||
except:
|
||||
self.message="error: unrecognized color specification '"+argv[2]+"'; supported format is 'r,g,b' or one of red|green|blue|cyan|magenta|yellow|white|black|gray"
|
||||
return
|
||||
self.app.painter.set_color(color)
|
||||
|
||||
# toggle grid
|
||||
def run_set_grid(self,argv):
|
||||
if len(argv)==2:
|
||||
# no argument: set to toggle
|
||||
self.app.painter.set_grid(-1)
|
||||
elif argv[2]=="on":
|
||||
self.app.painter.set_grid(1)
|
||||
elif argv[2]=="off":
|
||||
self.app.painter.set_grid(0)
|
||||
else:
|
||||
try:
|
||||
mesh=float(argv[2])
|
||||
except:
|
||||
self.message="error: unrecognized argument '"+argv[2]+"' -- usage 'set grid [on|off|<float>]'"
|
||||
return
|
||||
if mesh<0:
|
||||
self.message="error: grid size cannot be negative: '"+argv[2]+"'"
|
||||
return
|
||||
self.app.painter.set_grid(mesh)
|
||||
|
||||
# set zoom level (changes size of elements)
|
||||
def run_set_zoom(self,argv):
|
||||
if len(argv)==2:
|
||||
self.message="error: missing argument in 'set zoom'"
|
||||
return
|
||||
else:
|
||||
try:
|
||||
zoom_level=float(argv[2])
|
||||
except:
|
||||
self.message="error: unrecognized argument '"+argv[2]+"' -- usage 'set zoom <zoom_level>'"
|
||||
return
|
||||
if zoom_level<0:
|
||||
self.message="error: zoom level cannot be negative: '"+argv[2]+"'"
|
||||
return
|
||||
self.app.painter.set_zoom(zoom_level)
|
||||
|
||||
|
||||
|
||||
# write to file
|
||||
def run_write(self,argv):
|
||||
if len(argv)>2:
|
||||
self.message="error: could not write to file: too many arguments -- usage: ':w [path to file]'"
|
||||
return
|
||||
elif len(argv)==1:
|
||||
if self.app.openfile!="":
|
||||
if self.app.readonly:
|
||||
self.message="error: open file is readonly"
|
||||
return
|
||||
self.app.painter.write(self.app.openfile)
|
||||
return
|
||||
else:
|
||||
self.message="error: no file is open for editing, specify a path"
|
||||
return
|
||||
|
||||
(ret,self.message)=filecheck.check_write(argv[1],argv[0]=="w!")
|
||||
# add comment if no overwrite
|
||||
if ret==-2:
|
||||
self.message+=" (use ':w!' to overwrite)"
|
||||
if ret<0:
|
||||
return
|
||||
|
||||
self.app.openfile=argv[1]
|
||||
self.app.readonly=False
|
||||
self.app.painter.write(argv[1])
|
||||
# update status bar
|
||||
self.app.status_bar.draw()
|
||||
|
||||
# edit file
|
||||
def run_edit(self,argv):
|
||||
if len(argv)>2:
|
||||
self.message="error: could not open file: too many arguments -- usage: ':e <path to file>'"
|
||||
return
|
||||
elif len(argv)==1:
|
||||
self.message="error: could not open file: no argument found -- usage: ':e <path to file>'"
|
||||
return
|
||||
|
||||
# check that the file can be edited
|
||||
(ret,self.message)=filecheck.check_edit(argv[1])
|
||||
# error
|
||||
if ret<0:
|
||||
return
|
||||
|
||||
if os.path.isfile(argv[1]):
|
||||
# read the file
|
||||
self.app.painter.read(argv[1])
|
||||
# set readonly mode
|
||||
self.app.readonly=not os.access(argv[1],os.W_OK)
|
||||
else:
|
||||
# new file, reset painter
|
||||
self.app.painter.reset()
|
||||
self.app.readonly=False
|
||||
|
||||
# select openfile in app
|
||||
self.app.openfile=argv[1]
|
||||
# update status bar
|
||||
self.app.status_bar.draw()
|
46
src/filecheck.py
Normal file
46
src/filecheck.py
Normal file
@ -0,0 +1,46 @@
|
||||
import os.path
|
||||
|
||||
# check that a file can be edited
|
||||
def check_edit(file):
|
||||
# check that the file is not a directory
|
||||
if os.path.isdir(file):
|
||||
return(-1,"error: '"+file+"' is a directory")
|
||||
if os.path.isfile(file):
|
||||
# check permissions
|
||||
if not os.access(file,os.R_OK):
|
||||
return(-2,"error: permission denied: cannot read file '"+file+"'")
|
||||
else:
|
||||
# check write to directory
|
||||
if len(os.path.dirname(file))>0:
|
||||
if not os.access(os.path.dirname(file),os.W_OK):
|
||||
return(-3,"error: permission denied: cannot write to parent directory '"+os.path.dirname(file)+"'")
|
||||
else:
|
||||
if not os.access(os.getcwd(),os.W_OK):
|
||||
return(-4,"error: permission denied: cannot write to parent directory '"+os.getcwd()+"'")
|
||||
return(0,"")
|
||||
|
||||
|
||||
# check that a file can be written
|
||||
def check_write(file,overwrite):
|
||||
# check that the file is not a directory
|
||||
if os.path.isdir(file):
|
||||
return(-1,"error: '"+file+"' is a directory")
|
||||
# check that file does not already exist
|
||||
if not overwrite and os.path.isfile(file):
|
||||
return(-2,"error: '"+file+"' already exists")
|
||||
# check that the containing directory exists
|
||||
if len(os.path.dirname(file))>0 and not os.path.isdir(os.path.dirname(file)):
|
||||
return(-3,"error: could not find directory '"+os.path.dirname(file)+"'")
|
||||
# check permissions
|
||||
if os.path.isfile(file):
|
||||
if not os.access(file,os.W_OK):
|
||||
return(-4,"error: permission denied: cannot write to file '"+file+"'")
|
||||
else:
|
||||
# check write to directory
|
||||
if len(os.path.dirname(file))>0:
|
||||
if not os.access(os.path.dirname(file),os.W_OK):
|
||||
return(-5,"error: permission denied: cannot write to parent directory '"+os.path.dirname(file)+"'")
|
||||
else:
|
||||
if not os.access(os.getcwd(),os.W_OK):
|
||||
return(-6,"error: permission denied: cannot write to parent directory '"+os.getcwd()+"'")
|
||||
return(0,"")
|
71
src/jam
Executable file
71
src/jam
Executable file
@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.config import Config
|
||||
import sys
|
||||
import os.path
|
||||
|
||||
from painter import Painter
|
||||
from status_bar import Status_bar
|
||||
from command_prompt import Command_prompt
|
||||
import filecheck
|
||||
|
||||
# App class
|
||||
class Jam_app(App):
|
||||
# name of .kv file for main interface
|
||||
kv_file="jam.kv"
|
||||
|
||||
def __init__(self,**kwargs):
|
||||
# the file open for editing
|
||||
self.openfile=kwargs.get("openfile","")
|
||||
|
||||
# readonly mode
|
||||
self.readonly=False
|
||||
|
||||
super(Jam_app,self).__init__()
|
||||
|
||||
|
||||
def build(self):
|
||||
layout=BoxLayout(orientation="vertical")
|
||||
|
||||
# painter
|
||||
self.painter=Painter(self)
|
||||
# status bar
|
||||
self.status_bar=Status_bar(self)
|
||||
# command prompt
|
||||
self.command_prompt=Command_prompt(self)
|
||||
|
||||
layout.add_widget(self.painter)
|
||||
layout.add_widget(self.status_bar)
|
||||
layout.add_widget(self.command_prompt)
|
||||
|
||||
# read openfile
|
||||
if os.path.isfile(self.openfile):
|
||||
# read the file
|
||||
self.painter.read(self.openfile)
|
||||
# set readonly mode
|
||||
self.readonly=not os.access(self.openfile,os.W_OK)
|
||||
|
||||
return layout
|
||||
|
||||
|
||||
# disable red circles on right click
|
||||
Config.set('input', 'mouse', 'mouse,disable_multitouch')
|
||||
# do not exit on escape
|
||||
Config.set('kivy', 'exit_on_escape', 0)
|
||||
|
||||
# read cli
|
||||
openfile=""
|
||||
if len(sys.argv)==2:
|
||||
openfile=sys.argv[1]
|
||||
# check file
|
||||
(ret,message)=filecheck.check_edit(openfile)
|
||||
if ret<0:
|
||||
print(message,file=sys.stderr)
|
||||
exit(-1)
|
||||
|
||||
# run
|
||||
if __name__ == '__main__':
|
||||
Jam_app(openfile=openfile).run()
|
26
src/jam.kv
Normal file
26
src/jam.kv
Normal file
@ -0,0 +1,26 @@
|
||||
#: kivy 2.0.0
|
||||
|
||||
<Status_bar>:
|
||||
canvas.before:
|
||||
Color:
|
||||
rgb: 1,1,1
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
size_hint_y: None
|
||||
height: 20
|
||||
text_size: self.size
|
||||
markup: True
|
||||
color: (0,0,0,1)
|
||||
font_name: "RobotoMono-Regular"
|
||||
font_size: "15px"
|
||||
|
||||
|
||||
<Command_prompt>:
|
||||
size_hint_y: None
|
||||
height: 20
|
||||
markup: True
|
||||
text_size: self.size
|
||||
font_name: "RobotoMono-Regular"
|
||||
font_size: "15px"
|
||||
|
463
src/painter.py
Normal file
463
src/painter.py
Normal file
@ -0,0 +1,463 @@
|
||||
import sys
|
||||
import math
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.core.window import Window
|
||||
from kivy.graphics import Color,Line
|
||||
|
||||
from point import Point
|
||||
from polyomino import Cross
|
||||
from polyomino import Square_element
|
||||
|
||||
from tools import remove_fromlist
|
||||
|
||||
# painter class
|
||||
class Painter(Widget):
|
||||
|
||||
def __init__(self,app,**kwargs):
|
||||
|
||||
# list of particles
|
||||
self.particles=[]
|
||||
|
||||
# particle under mouse
|
||||
self.undermouse=None
|
||||
|
||||
# list of selected particles
|
||||
self.selected=[]
|
||||
# complement
|
||||
self.unselected=[]
|
||||
|
||||
# relative position of mouse when moving
|
||||
self.offset=Point(0,0)
|
||||
|
||||
# reference particle from which positions should be computed
|
||||
self.reference=None
|
||||
|
||||
# app is used to share information between widgets
|
||||
self.app=app
|
||||
|
||||
# modifiers
|
||||
self.modifiers=[]
|
||||
|
||||
# init Widget
|
||||
super(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,on_textinput=self.on_textinput)
|
||||
|
||||
|
||||
def reset(self):
|
||||
self.particles=[]
|
||||
self.undermouse=None
|
||||
self.draw()
|
||||
|
||||
|
||||
# draw all particles
|
||||
def draw(self):
|
||||
self.canvas.clear()
|
||||
with self.canvas:
|
||||
# draw order: particles, then grids, then transparent particles
|
||||
|
||||
for particle in self.particles:
|
||||
particle.draw()
|
||||
|
||||
# draw grids
|
||||
for particle in self.particles:
|
||||
if particle.grid>0:
|
||||
self.draw_grid(particle.squares[0].pos,particle.grid)
|
||||
|
||||
for particle in self.particles:
|
||||
particle.draw(alpha=0.5)
|
||||
|
||||
def draw_grid(self,pos,mesh):
|
||||
# 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
|
||||
offset=(pos.x-0.5)%mesh
|
||||
for i in range(math.floor((self.width/Square_element.size-offset)/mesh)+1):
|
||||
Color(1,1,1)
|
||||
Line(points=((i*mesh+offset)*Square_element.size,height_offset,(i*mesh+offset)*Square_element.size,self.height+height_offset))
|
||||
# horizontal lines
|
||||
# offset wrt 0
|
||||
offset=(pos.y-0.5)%1-height_offset/Square_element.size
|
||||
for i in range(math.floor((self.height/Square_element.size-offset)/mesh)+1):
|
||||
Color(1,1,1)
|
||||
Line(points=(0,(i*mesh+offset)*Square_element.size+height_offset,self.width,(i*mesh+offset)*Square_element.size+height_offset))
|
||||
|
||||
|
||||
# respond to keyboard
|
||||
def on_key_down(self, keyboard, keycode, text, modifiers):
|
||||
# check the 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()
|
||||
# remove particles
|
||||
elif keycode[1]=="backspace":
|
||||
for particle in self.selected:
|
||||
self.particles=remove_fromlist(self.particles,particle)
|
||||
self.selected=[]
|
||||
self.draw()
|
||||
|
||||
# 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:
|
||||
# select all
|
||||
if text=="a":
|
||||
for particle in self.particles:
|
||||
particle.selected=True
|
||||
self.selected=self.particles.copy()
|
||||
self.unselected=[]
|
||||
self.draw()
|
||||
|
||||
# toggle grid
|
||||
elif text=="g":
|
||||
for particle in self.selected:
|
||||
if particle.grid==0:
|
||||
particle.grid=1
|
||||
else:
|
||||
particle.grid=-particle.grid
|
||||
self.draw()
|
||||
|
||||
# zoom
|
||||
elif text=="+":
|
||||
# increment by 10%
|
||||
self.set_zoom(Square_element.size/50*1.1)
|
||||
elif text=="-":
|
||||
# decrease by 10%
|
||||
self.set_zoom(Square_element.size/50*0.9)
|
||||
elif text=="=":
|
||||
# reset
|
||||
self.set_zoom(1)
|
||||
|
||||
# set reference
|
||||
elif text=="r":
|
||||
if len(self.selected)>0:
|
||||
self.reference=self.selected[0]
|
||||
else:
|
||||
self.reference=None
|
||||
self.app.status_bar.draw()
|
||||
|
||||
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 drawing area
|
||||
if self.collide_point(*touch.pos):
|
||||
|
||||
# create new cross
|
||||
if touch.button=="right":
|
||||
new=Cross(touch.x/Square_element.size,touch.y/Square_element.size)
|
||||
if not self.check_interaction_any(new,Point(0,0)):
|
||||
# add to list
|
||||
self.particles.append(new)
|
||||
|
||||
# unselect all particles
|
||||
for sel in self.selected:
|
||||
sel.selected=False
|
||||
self.selected=[]
|
||||
self.unselected=self.particles
|
||||
|
||||
self.draw()
|
||||
|
||||
# select particle
|
||||
if touch.button=="left":
|
||||
# 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:
|
||||
# unselect all particles
|
||||
for sel in self.selected:
|
||||
sel.selected=False
|
||||
self.selected=[]
|
||||
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
|
||||
|
||||
|
||||
# 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
|
||||
# remove from unselected
|
||||
self.unselected=remove_fromlist(self.unselected,self.undermouse)
|
||||
else:
|
||||
# remove
|
||||
self.selected=remove_fromlist(self.selected,self.undermouse)
|
||||
self.undermouse.selected=False
|
||||
# add to unselected
|
||||
self.unselected.append(self.undermouse)
|
||||
|
||||
self.draw()
|
||||
|
||||
# draw status bar
|
||||
self.app.status_bar.draw()
|
||||
|
||||
|
||||
# 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:
|
||||
# attempted move determined by the relative position to the relative position of click within self.undermouse
|
||||
delta=self.adjust_move(Point(touch.x/Square_element.size,touch.y/Square_element.size)-(self.offset+self.undermouse.squares[0].pos),0)
|
||||
for particle in self.selected:
|
||||
particle.move(delta)
|
||||
|
||||
# redraw
|
||||
self.draw()
|
||||
|
||||
# draw status bar
|
||||
self.app.status_bar.draw()
|
||||
|
||||
|
||||
# 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 candidate particle intersects with any of the particles
|
||||
def check_interaction_any(self,candidate,offset):
|
||||
for particle in self.particles:
|
||||
# do not check interaction if candidate=particle
|
||||
if candidate!=particle and particle.check_interaction(candidate,offset):
|
||||
return True
|
||||
return False
|
||||
|
||||
# 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
|
||||
def adjust_move(self,delta,recursion_depth):
|
||||
# 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
|
||||
adjusted_delta=self.adjust_move_element(delta,element,0)
|
||||
# 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
|
||||
# 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)
|
||||
|
||||
|
||||
# trying to move a single element by delta, adjust if needed to avoid overlap with unselected particles
|
||||
def adjust_move_element(self,delta,element,recursion_depth):
|
||||
# whether newpos is acceptable
|
||||
accept_newpos=True
|
||||
for other in self.unselected:
|
||||
for obstacle in other.squares:
|
||||
# move would make element overlap with obstacle
|
||||
if obstacle.check_interaction(element.pos+delta):
|
||||
accept_newpos=False
|
||||
# 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)
|
||||
else:
|
||||
newdelta=obstacle.move_on_line_to_stick(element.pos,delta)
|
||||
if not self.check_interaction_unselected_element(element,newdelta):
|
||||
return newdelta
|
||||
if accept_newpos:
|
||||
return delta
|
||||
else:
|
||||
# cannot move particle at all, try again
|
||||
# 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)
|
||||
|
||||
|
||||
# set color of selected particles
|
||||
def set_color(self,color):
|
||||
for particle in self.selected:
|
||||
particle.color=color
|
||||
# redraw
|
||||
self.draw()
|
||||
|
||||
# set grid for selected particles
|
||||
# set mesh to -1 to toggle on/off
|
||||
def set_grid(self,mesh):
|
||||
for particle in self.selected:
|
||||
if mesh==-1:
|
||||
if particle.grid==0:
|
||||
particle.grid=1
|
||||
else:
|
||||
particle.grid=-particle.grid
|
||||
else:
|
||||
particle.grid=mesh
|
||||
# redraw
|
||||
self.draw()
|
||||
|
||||
|
||||
# write configuration to file
|
||||
def write(self,file):
|
||||
ff=open(file,"w")
|
||||
for particle in self.particles:
|
||||
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]))
|
||||
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)>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)
|
||||
continue
|
||||
|
||||
# position
|
||||
pos_str=entries[1].split(",")
|
||||
# skip line if improperly formatted
|
||||
if len(pos_str)!=2:
|
||||
print("warning: ignoring line "+str(i)+" in file '"+file+"': position '"+entries[1]+"' 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[1]+"' cannot be read",file=sys.stderr)
|
||||
continue
|
||||
|
||||
|
||||
# color
|
||||
color=(0,0,1)
|
||||
if len(entries)==3:
|
||||
color_str=entries[2].split(",")
|
||||
# skip line if improperly formatted
|
||||
if len(color_str)!=3:
|
||||
print("warning: ignoring line "+str(i)+" in file '"+file+"': color '"+entries[2]+"' 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[2]+"' cannot be read",file=sys.stderr)
|
||||
continue
|
||||
|
||||
# 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)):
|
||||
# add to list
|
||||
self.particles.append(candidate)
|
||||
self.unselected.append(candidate)
|
||||
else:
|
||||
print("warning: ignoring line "+str(i)+" in file '"+file+"': particle overlaps with existing particles",file=sys.stderr)
|
||||
|
||||
ff.close()
|
||||
self.draw()
|
||||
|
||||
# set zoom level
|
||||
def set_zoom(self,level):
|
||||
Square_element.size=level*50
|
||||
self.draw()
|
||||
|
||||
|
||||
# global variables (used like precompiler variables)
|
||||
CROSS_INDEX=1
|
||||
|
||||
|
49
src/point.py
Normal file
49
src/point.py
Normal file
@ -0,0 +1,49 @@
|
||||
import math
|
||||
|
||||
# point in two dimensions
|
||||
class Point:
|
||||
def __init__(self,x,y):
|
||||
self.x=x
|
||||
self.y=y
|
||||
|
||||
def __add__(self,point):
|
||||
return Point(self.x+point.x,self.y+point.y)
|
||||
|
||||
def __sub__(self,point):
|
||||
return Point(self.x-point.x,self.y-point.y)
|
||||
|
||||
def __neg__(self):
|
||||
return Point(-self.x,-self.y)
|
||||
|
||||
def __mul__(self,a):
|
||||
return Point(a*self.x,a*self.y)
|
||||
|
||||
def __truediv__(self,a):
|
||||
return Point(self.x/a,self.y/a)
|
||||
|
||||
def __pow__(self,a):
|
||||
if a==2:
|
||||
return self.dot(self)
|
||||
|
||||
# dot product
|
||||
def dot(self,x):
|
||||
return self.x*x.x+self.y*x.y
|
||||
|
||||
# integer part
|
||||
def int(self):
|
||||
return Point(int(self.x),int(self.y))
|
||||
|
||||
# find the closest among a list of points
|
||||
def closest(self,points):
|
||||
dist=math.inf
|
||||
closest=None
|
||||
for point in points:
|
||||
if (self-point)**2<dist:
|
||||
closest=point
|
||||
dist=(self-point)**2
|
||||
return closest
|
||||
|
||||
|
||||
# L infinity norm
|
||||
def l_infinity(x):
|
||||
return max(abs(x.x),abs(x.y))
|
231
src/polyomino.py
Normal file
231
src/polyomino.py
Normal file
@ -0,0 +1,231 @@
|
||||
import math
|
||||
import sys
|
||||
from kivy.graphics import Color,Line,Rectangle
|
||||
|
||||
from point import Point,l_infinity
|
||||
from tools import isint_nonzero,sgn,in_interval
|
||||
|
||||
# parent class of all polyominos
|
||||
class Polyomino():
|
||||
def __init__(self,**kwargs):
|
||||
# square elements that maje up the polyomino
|
||||
self.squares=kwargs.get("squares",[])
|
||||
|
||||
self.color=kwargs.get("color",(0,0,1))
|
||||
self.selected=False
|
||||
|
||||
# mesh of background grid (no grid for mesh size 0)
|
||||
self.grid=kwargs.get("grid",0)
|
||||
|
||||
# draw function
|
||||
def draw(self,**kwargs):
|
||||
alpha=kwargs.get("alpha",1)
|
||||
# set color
|
||||
if not self.selected:
|
||||
Color(*self.color,alpha)
|
||||
else:
|
||||
(r,g,b)=self.color
|
||||
# darken selected
|
||||
Color(r/2,g/2,b/2,alpha)
|
||||
|
||||
for square in self.squares:
|
||||
Rectangle(pos=((square.pos.x-0.5)*square.size,(square.pos.y-0.5)*square.size),size=(square.size,square.size))
|
||||
|
||||
# draw boundary
|
||||
self.stroke()
|
||||
|
||||
# draw boundary (override for connected polyominos)
|
||||
def stroke(self):
|
||||
# white
|
||||
Color(1,1,1)
|
||||
for square in self.squares:
|
||||
Line(points=(
|
||||
*((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),
|
||||
*((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),
|
||||
*((square.pos.x-0.5)*square.size,(square.pos.y-0.5)*square.size)
|
||||
))
|
||||
|
||||
# move by delta
|
||||
def move(self,delta):
|
||||
for square in self.squares:
|
||||
square.pos+=delta
|
||||
|
||||
# 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
|
||||
|
||||
# square
|
||||
class Square(Polyomino):
|
||||
def __init__(self,x,y,**kwargs):
|
||||
super(Square,self).__init__(**kwargs,squares=[Square_element(x,y)])
|
||||
|
||||
# cross
|
||||
class Cross(Polyomino):
|
||||
def __init__(self,x,y,**kwargs):
|
||||
super(Cross,self).__init__(**kwargs,squares=[\
|
||||
Square_element(x,y),\
|
||||
Square_element(x+1,y),\
|
||||
Square_element(x-1,y),\
|
||||
Square_element(x,y+1),\
|
||||
Square_element(x,y-1)\
|
||||
])
|
||||
|
||||
# redefine stroke to avoid lines between touching squares
|
||||
def stroke(self):
|
||||
Color(1,1,1)
|
||||
Line(points=(
|
||||
*((self.squares[0].pos.x-0.5)*Square_element.size,(self.squares[0].pos.y-0.5)*Square_element.size),
|
||||
*((self.squares[0].pos.x-0.5)*Square_element.size,(self.squares[0].pos.y-1.5)*Square_element.size),
|
||||
*((self.squares[0].pos.x+0.5)*Square_element.size,(self.squares[0].pos.y-1.5)*Square_element.size),
|
||||
*((self.squares[0].pos.x+0.5)*Square_element.size,(self.squares[0].pos.y-0.5)*Square_element.size),
|
||||
*((self.squares[0].pos.x+1.5)*Square_element.size,(self.squares[0].pos.y-0.5)*Square_element.size),
|
||||
*((self.squares[0].pos.x+1.5)*Square_element.size,(self.squares[0].pos.y+0.5)*Square_element.size),
|
||||
*((self.squares[0].pos.x+0.5)*Square_element.size,(self.squares[0].pos.y+0.5)*Square_element.size),
|
||||
*((self.squares[0].pos.x+0.5)*Square_element.size,(self.squares[0].pos.y+1.5)*Square_element.size),
|
||||
*((self.squares[0].pos.x-0.5)*Square_element.size,(self.squares[0].pos.y+1.5)*Square_element.size),
|
||||
*((self.squares[0].pos.x-0.5)*Square_element.size,(self.squares[0].pos.y+0.5)*Square_element.size),
|
||||
*((self.squares[0].pos.x-1.5)*Square_element.size,(self.squares[0].pos.y+0.5)*Square_element.size),
|
||||
*((self.squares[0].pos.x-1.5)*Square_element.size,(self.squares[0].pos.y-0.5)*Square_element.size),
|
||||
*((self.squares[0].pos.x-0.5)*Square_element.size,(self.squares[0].pos.y-0.5)*Square_element.size),
|
||||
))
|
||||
|
||||
|
||||
|
||||
# square building block of polyominos
|
||||
class Square_element():
|
||||
# size
|
||||
size=50
|
||||
|
||||
def __init__(self,x,y,**kwargs):
|
||||
self.pos=Point(x,y)
|
||||
|
||||
# set position
|
||||
def setpos(self,x,y):
|
||||
self.pos.x=x
|
||||
self.pos.y=y
|
||||
|
||||
|
||||
# check whether a square at pos interacts with square
|
||||
def check_interaction(self,pos):
|
||||
return l_infinity(pos-self.pos)<1
|
||||
|
||||
# check whether a square at position pos is touching self
|
||||
def check_touch(self,pos):
|
||||
# allow for error
|
||||
if in_interval(l_infinity(pos-self.pos),1-1e-11,1+1e-11):
|
||||
return True
|
||||
return False
|
||||
|
||||
# find position along a line that comes in contact with the line going through pos in direction v
|
||||
def move_on_line_to_stick(self,pos,v):
|
||||
# compute intersections with four lines making up square
|
||||
if v.x!=0:
|
||||
if v.y!=0:
|
||||
intersections=[\
|
||||
Point(self.pos.x+1,pos.y+v.y/v.x*(self.pos.x+1-pos.x)),\
|
||||
Point(self.pos.x-1,pos.y+v.y/v.x*(self.pos.x-1-pos.x)),\
|
||||
Point(pos.x+v.x/v.y*(self.pos.y+1-pos.y),self.pos.y+1),\
|
||||
Point(pos.x+v.x/v.y*(self.pos.y-1-pos.y),self.pos.y-1)\
|
||||
]
|
||||
else:
|
||||
intersections=[\
|
||||
Point(self.pos.x+1,pos.y+v.y/v.x*(self.pos.x+1-pos.x)),\
|
||||
Point(self.pos.x-1,pos.y+v.y/v.x*(self.pos.x-1-pos.x))
|
||||
]
|
||||
else:
|
||||
if v.y!=0:
|
||||
intersections=[\
|
||||
Point(pos.x+v.x/v.y*(self.pos.y+1-pos.y),self.pos.y+1),\
|
||||
Point(pos.x+v.x/v.y*(self.pos.y-1-pos.y),self.pos.y-1)\
|
||||
]
|
||||
else:
|
||||
print("error: move_on_line_to_stick called with v=0, please file a bug report with the developer",file=sys.stderr)
|
||||
exit(-1)
|
||||
|
||||
# compute closest one, on square
|
||||
closest=None
|
||||
dist=math.inf
|
||||
for i in range(0,len(intersections)):
|
||||
# check that it is on square
|
||||
if abs(intersections[i].x-self.pos.x)<=1+1e-11 and abs(intersections[i].y-self.pos.y)<=1+1e-11:
|
||||
if (intersections[i]-pos)**2<dist:
|
||||
closest=intersections[i]
|
||||
dist=(intersections[i]-pos)**2
|
||||
|
||||
if closest==None:
|
||||
print("error: cannot move particle at (",pos.x,",",pos.y,") to the boundary of (",self.pos.x,",",self.pos.y,") in direction (",v.x,",",v.y,")",file=sys.stderr)
|
||||
exit(-1)
|
||||
|
||||
# return difference to pos
|
||||
return closest-pos
|
||||
|
||||
# move along edge of square
|
||||
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(delta.y)==-sgn(rel.y):
|
||||
# stuck in x direction
|
||||
return self.move_stuck_x(delta,pos)
|
||||
elif sgn(delta.x)==-sgn(rel.x):
|
||||
# stuck in y direction
|
||||
return self.move_stuck_y(delta,pos)
|
||||
# stuck in both directions
|
||||
return pos
|
||||
else:
|
||||
# stuck in x direction
|
||||
return self.move_stuck_x(delta,pos)
|
||||
elif isint_nonzero(rel.y):
|
||||
# stuck in y direction
|
||||
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,delta,pos):
|
||||
# only move in y direction
|
||||
candidate=Point(0,delta.y)
|
||||
# do not move past corners
|
||||
rel=pos.y-self.pos.y
|
||||
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-pos.y
|
||||
else:
|
||||
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-pos.y
|
||||
return candidate
|
||||
# move when stuck in the y direction
|
||||
def move_stuck_y(self,delta,pos):
|
||||
# onlx move in x direction
|
||||
candidate=Point(delta.x,0)
|
||||
# do not move past corners
|
||||
rel=pos.x-self.pos.x
|
||||
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-pos.x
|
||||
else:
|
||||
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-pos.x
|
||||
return candidate
|
||||
|
53
src/status_bar.py
Normal file
53
src/status_bar.py
Normal file
@ -0,0 +1,53 @@
|
||||
from kivy.uix.label import Label
|
||||
from kivy.graphics import Color,Rectangle
|
||||
from kivy.utils import escape_markup
|
||||
|
||||
class Status_bar(Label):
|
||||
|
||||
def __init__(self,app,**kwargs):
|
||||
# app is used to share information between widgets
|
||||
self.app=app
|
||||
|
||||
# width of a character
|
||||
self.char_width=9
|
||||
|
||||
# unformatted text
|
||||
self.raw_text=""
|
||||
|
||||
# a one-time message that shows up over the bar
|
||||
self.message=""
|
||||
|
||||
# init Label
|
||||
super(Status_bar,self).__init__(**kwargs)
|
||||
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
# if message is not empty, draw message instead
|
||||
if self.message!="":
|
||||
self.text=self.message[:min(len(self.message),int(self.width/self.char_width))]
|
||||
self.message=""
|
||||
return
|
||||
|
||||
# list openfile
|
||||
if self.app.openfile!="":
|
||||
self.raw_text=self.app.openfile
|
||||
else:
|
||||
self.raw_text="[no file]"
|
||||
|
||||
# readonly
|
||||
if self.app.readonly:
|
||||
self.raw_text+=" [RO]"
|
||||
|
||||
# coordinates of selected cross
|
||||
if len(self.app.painter.selected)>0:
|
||||
# number of spaces to align right (use 13 characters to print position)
|
||||
spaces=int(self.width/self.char_width)-len(self.raw_text)-13
|
||||
if spaces>0:
|
||||
if self.app.painter.reference==None:
|
||||
self.raw_text+=" "*spaces+"({:05.2f},{:05.2f})\n".format(self.app.painter.selected[0].squares[0].pos.x,self.app.painter.selected[0].squares[0].pos.y)
|
||||
else:
|
||||
self.raw_text+=" "*spaces+"({:05.2f},{:05.2f})\n".format(self.app.painter.selected[0].squares[0].pos.x-self.app.painter.reference.squares[0].pos.x,self.app.painter.selected[0].squares[0].pos.y-self.app.painter.reference.squares[0].pos.y)
|
||||
|
||||
# do not wrap
|
||||
self.text=self.raw_text[:min(len(self.raw_text),int(self.width/self.char_width))]
|
22
src/tools.py
Normal file
22
src/tools.py
Normal file
@ -0,0 +1,22 @@
|
||||
# sign function
|
||||
def sgn(x):
|
||||
if x>=0:
|
||||
return 1
|
||||
return -1
|
||||
|
||||
# check whether a number is an integer (with tolerance)
|
||||
def isint_nonzero(x):
|
||||
if abs(x)<1e-11:
|
||||
return False
|
||||
return abs(round(x)-x)<1e-11
|
||||
|
||||
# check that a number is in an interval
|
||||
def in_interval(x,a,b):
|
||||
return x>=a and x<=b
|
||||
|
||||
# remove x from list a
|
||||
def remove_fromlist(a,x):
|
||||
if x in a:
|
||||
a[a.index(x)]=a[len(a)-1]
|
||||
a=a[:len(a)-1]
|
||||
return a
|
Reference in New Issue
Block a user