hexagram/py/hexagram/path.py

200 lines
5.7 KiB
Python

from typing import Iterable
import enum
import cairo
class State(enum.Enum):
NONE = 0
COMMAND = 1
INT = 2
FLOAT = 3
class ParseError(Exception):
pass
class PathCommand():
__slots__ = 'command', 'args', 'arg',
def __init__(self):
self.command = None
self.args = list()
self.arg = ''
def set_command(self, command: str):
self.command = command
def arg_addch(self, ch: str):
self.arg += ch
def arg_next(self, ch: str):
if len(self.arg) > 0:
self.args.append(self.arg)
self.arg = ch
def arg_finish(self):
if len(self.arg) > 0:
self.args.append(self.arg)
self.arg = ''
def finish(self):
if len(self.arg) > 0:
self.args.append(self.arg)
ret = [self.command, self.args]
self.command = None
self.args = list()
self.arg = ''
return ret
class Path():
__slots__ = 'commands',
def __init__(self, commands: Iterable):
self.commands = commands
@staticmethod
def is_command(c: int):
return (c >= ord('a') and c <= ord('z')) or (c >= ord('A') and c <= ord('Z'))
@staticmethod
def is_number(c: int):
return (c >= ord('0') and c <= ord('9'))
@staticmethod
def parse(text: str) -> list:
commands = list()
command = PathCommand()
state = State.NONE
for ch in text:
if ch == '\r' or ch == '\n' or ch == '\t':
continue
c = ord(ch)
if state is State.NONE:
if Path.is_command(c):
command.set_command(ch)
state = State.COMMAND
elif ch == ' ':
continue
else:
raise ParseError()
elif state is State.COMMAND:
if Path.is_command(c):
commands.append(command.finish())
command.set_command(ch)
elif Path.is_number(c) or ch == '-':
state = State.INT
command.arg_addch(ch)
elif ch == '.':
state = State.FLOAT
command.arg_addch(ch)
elif ch == ' ':
continue
else:
raise ParseError()
elif state is State.INT:
if Path.is_command(c):
commands.append(command.finish())
command.set_command(ch)
elif Path.is_number(c):
command.arg_addch(ch)
elif ch == '.':
command.arg_addch(ch)
state = State.FLOAT
elif ch == '-':
command.arg_next(ch)
elif ch == ' ' or ch == ',':
command.arg_finish()
else:
raise ParseError()
elif state is State.FLOAT:
if Path.is_command(c):
commands.append(command.finish())
command.set_command(ch)
elif Path.is_number(c):
command.arg_addch(ch)
elif ch == '-':
command.arg_next(ch)
elif ch == ' ' or ch == ',':
state = State.INT
command.arg_finish()
else:
raise ParseError(ch)
if command.command is not None:
commands.append(command.finish())
return Path(commands)
def horiz_to(self, cr: cairo.Context, x2: float):
_, y = cr.get_current_point()
return cr.line_to(x2, y)
def rel_horiz_to(self, cr: cairo.Context, x2: float):
x, y = cr.get_current_point()
return cr.line_to(x + x2, y)
def vert_to(self, cr: cairo.Context, y2: float):
x, _ = cr.get_current_point()
return cr.line_to(x, y2)
def rel_vert_to(self, cr: cairo.Context, y2: float):
x, y = cr.get_current_point()
return cr.line_to(x, y + y2)
def draw(self, cr: cairo.Context):
last = None
for item in self.commands:
command, args = item
if command == 'M':
for i in range(0, len(args), 2):
if i == 0:
cr.move_to(args[i], args[i+1])
else:
cr.line_to(args[i], args[i+1])
elif command == 'm':
for i in range(0, len(args), 2):
if i == 0:
cr.rel_move_to(args[i], args[i+1])
else:
cr.rel_line_to(args[i], args[i+1])
elif command == 'L':
for i in range(0, len(args), 2):
cr.line_to(args[i], args[i+1])
elif command == 'l':
for i in range(0, len(args), 2):
cr.rel_line_to(args[i], args[i+1])
elif command == 'H':
for arg in args:
self.horiz_to(cr, arg)
elif command == 'h':
for arg in args:
self.rel_horiz_to(cr, arg)
elif command == 'V':
for arg in args:
self.vert_to(cr, arg)
elif command == 'v':
for arg in args:
self.rel_vert_to(cr, arg)
elif command == 'C':
for arg in args:
cr.curve_to(*arg)
elif command == 'c':
for arg in args:
cr.rel_curve_to(*arg)
elif command == 'Z' or command == 'z':
cr.close_path()
last = command