250 lines
7.6 KiB
Python
250 lines
7.6 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)
|
|
|
|
args = list()
|
|
|
|
for arg in self.args:
|
|
try:
|
|
arg.index('.')
|
|
args.append(float(arg))
|
|
except ValueError:
|
|
args.append(int(arg))
|
|
|
|
ret = [self.command, 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):
|
|
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 quad_to_cube(self, cr: cairo.Context, x1: float, y1: float, x2: float, y2: float):
|
|
x0, y0 = cr.get_current_point()
|
|
|
|
return (2 / 3 * x1 + 1 / 3 * x0,
|
|
2 / 3 * y1 + 1 / 3 * y0,
|
|
2 / 3 * x1 + 1 / 3 * x2,
|
|
2 / 3 * y1 + 1 / 3 * y2)
|
|
|
|
def draw(self, cr: cairo.Context):
|
|
last = ['X', []]
|
|
|
|
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:i+2])
|
|
else:
|
|
cr.line_to(*args[i:i+2])
|
|
elif command == 'm':
|
|
for i in range(0, len(args), 2):
|
|
if i == 0:
|
|
cr.rel_move_to(*args[i:i+2])
|
|
else:
|
|
cr.rel_line_to(*args[i:i+2])
|
|
elif command == 'L':
|
|
for i in range(0, len(args), 2):
|
|
cr.line_to(*args[i:i+2])
|
|
elif command == 'l':
|
|
for i in range(0, len(args), 2):
|
|
cr.rel_line_to(*args[i:i+2])
|
|
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 i in range(0, len(args), 6):
|
|
cr.curve_to(*args[i:i+6])
|
|
elif command == 'c':
|
|
for i in range(0, len(args), 6):
|
|
cr.rel_curve_to(*args[i:i+6])
|
|
elif command == 'S' or command == 's':
|
|
for i in range(0, len(args), 4):
|
|
x2, y2, x, y = map(float, args[i:i+4])
|
|
|
|
if last[0] == 'C' or last[0] == 'c':
|
|
x1, y1 = map(float, last[1][2:4])
|
|
elif last[0] == 'S' or last[0] == 's':
|
|
raise NotImplementedError
|
|
else:
|
|
x1, y1 = cr.get_current_point()
|
|
|
|
if command == 'S':
|
|
cr.curve_to(x1, y1, x2, y2, x, y)
|
|
elif command == 's':
|
|
cr.curve_to(x1, y1, x2, y2, x, y)
|
|
elif command == 'Q' or command == 'q':
|
|
for i in range(0, len(args), 4):
|
|
x1, y1, x2, y2 = args[i:i+4]
|
|
cube = self.quad_to_cube(cr, x1, y1, x2, y2)
|
|
|
|
if command == 'Q':
|
|
cr.curve_to(*cube)
|
|
elif command == 'q':
|
|
cr.rel_curve_to(*cube)
|
|
elif command == 'T' or command == 't':
|
|
for i in range(0, len(args), 2):
|
|
x, y = args[i:i+2]
|
|
|
|
if last[0] == 'Q' or last[0] == 'q':
|
|
x1, y1 = last[1][0:2]
|
|
elif last[0] == 'T' or last[0] == 't':
|
|
raise NotImplementedError
|
|
|
|
elif command == 'Z' or command == 'z':
|
|
cr.close_path()
|
|
|
|
last = item
|