hexagram/py/hexagram/path.py
2024-01-03 13:13:54 -05:00

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