use proper line rendering

There are some edge cases to Bresenham's line drawing algorithm with
cumul=dx. The fx-92 SC+ clearly checks cumul>dx, but for dx=1 and dy=1
this results in a horizontal line.

Apparently dx=dy is the only case where the fx-92 SC+ behaves
differently than cumul>dx, as seen with dx=5 dy=4 (causes cumul=dx after
2 iterations but does not trigger the condition).

Also allow the program to be interrupted with Escape while paused.
This commit is contained in:
Lephe 2019-10-06 00:06:04 +02:00
parent ab057abddd
commit 2c7287338c
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
6 changed files with 106 additions and 56 deletions

2
ast.py
View File

@ -12,7 +12,7 @@ def auto_builder():
try:
enum.auto()
except AttributeError:
except AttributeError:
enum.auto = auto_builder()
del auto_builder

View File

@ -1,6 +1,10 @@
# fx-92 Scientifique Collège+ language interpreter: On-screen drawing
from sdl2 import *
import math
class InterruptException(Exception):
pass
class Window:
"""
@ -72,13 +76,24 @@ class Window:
SDL_Quit()
def _point(self, x, y):
# It's important to divide with //, otherwise when the width or height
# is odd this transformation loses rounding information and can alter
# the position of the final pixel.
x = self.width // 2 + x - 1
y = self.height // 2 - y
x = int(round(x))
y = int(round(y))
SDL_RenderDrawPoint(self.r, x, y)
def clear(self, color):
"""Clear screen in a uniform color."""
SDL_SetRenderDrawColor(self.r, *color)
SDL_RenderClear(self.r)
def line(self, x1, y1, x2, y2, color):
def _line(self, x1, y1, x2, y2, color):
"""Draw a straight line."""
SDL_SetRenderDrawColor(self.r, *color)
@ -92,9 +107,9 @@ class Window:
dx, dy = abs(dx), abs(dy)
cumul = 0
SDL_RenderDrawPoint(self.r, x, y)
self._point(x, y)
if dx >= dy:
if dx > dy:
cumul = dx // 2
for i in range(dx):
x += sx
@ -102,7 +117,12 @@ class Window:
if cumul > dx:
cumul -= dx
y += sy
SDL_RenderDrawPoint(self.r, x, y)
self._point(x, y)
elif dx == dy:
for i in range(dx):
x += sx
y += sy
self._point(x, y)
else:
cumul = dy // 2
for i in range(dy):
@ -111,7 +131,25 @@ class Window:
if cumul > dy:
cumul -= dy
x += sx
SDL_RenderDrawPoint(self.r, x, y)
self._point(x, y)
def line(self, x1, y1, x2, y2, color):
x1, y1, x2, y2 = round(x1), round(y1), round(x2), round(y2)
x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
self._line(x1, y1, x2, y2, color)
def linef(self, x, y, angle, dist, color):
"""Draw a line forward."""
cos = math.cos(angle * math.pi / 180)
sin = math.sin(angle * math.pi / 180)
x2 = x + dist * cos
y2 = y + dist * sin
x1, y1, x2, y2 = round(x), round(y), round(x2), round(y2)
x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
self._line(x1, y1, x2, y2, color)
def wait(self, allow_any=False):
"""Wait for the window to be closed."""
@ -125,6 +163,8 @@ class Window:
if event.type == SDL_QUIT:
break
if event.type == SDL_KEYDOWN and event.key.keysym.sym==SDLK_ESCAPE:
raise InterruptException()
if event.type == SDL_KEYDOWN and allow_any:
break

14
fx92.py
View File

@ -6,7 +6,7 @@ import getopt
from parser import Parser
from lexer import UrlLexer, TextLexer
from printer import print_ast
from drawing import Window
from drawing import Window, InterruptException
from interpreter import Context
#---
@ -97,11 +97,13 @@ def main():
with Window(width=192, height=47, scale=scale, quiet=quiet) as w:
ctx = Context(w)
ctx.run(ast)
if out is not None:
w.save(out)
w.wait()
try:
ctx.run(ast)
if out is not None:
w.save(out)
w.wait()
except InterruptException:
print("[interpreter] Interrupted.", file=sys.stderr)
return 0

View File

@ -20,17 +20,17 @@ class Context:
self.x = float(0)
self.y = float(0)
self.theta = float(0)
self.pen = False
self.angle = float(0)
self.sin = float(0)
self.cos = float(1)
# TODO: Also specify the style!
self.w.clear(self.w.WHITE)
self.w.update()
def settheta(self, newtheta):
self.theta = newtheta % 360
def run(self, n):
"""
Execute a full program tree. For expression nodes, returns a value; for
@ -47,51 +47,27 @@ class Context:
elif n.type == N.FORWARD:
dist, = self.runs(n)
dx = self.cos * dist
dy = self.sin * dist
if self.pen:
coord = self.x, self.y, self.x + dx, self.y + dy
coord = [int(round(x)) for x in coord]
x1, y1, x2, y2 = coord
x1 += self.w.width // 2
x2 += self.w.width // 2
y1 = self.w.height // 2 - y1
y2 = self.w.height // 2 - y2
self.w.line(x1, y1, x2, y2, self.w.BLACK)
self.w.linef(self.x, self.y, self.theta, dist, self.w.BLACK)
self.w.update()
self.x += dx
self.y += dy
self.x += dist * math.cos(self.theta * math.pi / 180)
self.y += dist * math.sin(self.theta * math.pi / 180)
elif n.type == N.ROTATE:
angle, = self.runs(n)
self.angle += angle
self.sin = math.sin(math.pi * self.angle / 180)
self.cos = math.cos(math.pi * self.angle / 180)
self.settheta(self.theta + angle)
elif n.type == N.ORIENT:
angle, = self.runs(n)
self.angle = angle
self.sin = math.sin(math.pi * self.angle / 180)
self.cos = math.cos(math.pi * self.angle / 180)
self.settheta(angle)
elif n.type == N.GOTO:
x, y = self.runs(n)
if self.pen:
coord = self.x, self.y, x, y
coord = [int(round(x)) for x in coord]
x1, y1, x2, y2 = coord
x1 += self.w.width // 2
x2 += self.w.width // 2
y1 = self.w.height // 2 - y1
y2 = self.w.height // 2 - y2
self.w.line(x1, y1, x2, y2, self.w.BLACK)
self.w.line(self.x, self.y, x, y, self.w.BLACK)
self.w.update()
self.x = x
@ -106,7 +82,14 @@ class Context:
val = self.run(n.args[0])
var = n.args[1]
self.vars[var] = val
if var == "x":
self.x = val
elif var == "y":
self.y = val
elif var == "theta":
self.theta = val
else:
self.vars[var] = val
elif n.type == N.INPUT:
# TODO: Input stmt should call into the lexer+parser (urg)
@ -122,11 +105,11 @@ class Context:
elif n.type == N.WAIT:
print("---- pause")
print(f"x={self.x} y={self.y} theta={self.angle}")
print(f"A={self.vars['A']} B={self.vars['B']}")
print(f"C={self.vars['C']} D={self.vars['D']}")
print(f"E={self.vars['E']} F={self.vars['F']}")
print(f"M={self.vars['M']}")
print("x={} y={} theta={}".format(self.x, self.y, self.theta))
print("A={} B={}".format(self.vars['A'], self.vars['B']))
print("C={} D={}".format(self.vars['C'], self.vars['D']))
print("E={} F={}".format(self.vars['E'], self.vars['F']))
print("M={}".format(self.vars['M']))
self.w.pause()
# Flow control
@ -181,6 +164,8 @@ class Context:
return self.x
if var == "y":
return self.y
if var == "theta":
return self.theta
return self.vars[var]
elif n.type == N.CONST:
@ -217,7 +202,8 @@ class Context:
}[n.args[0]]
if f is None:
raise Exception("Function {} not there yet x_x".format(n.args[0]))
raise Exception(
"Function {} not there yet x_x".format(n.args[0]))
return f(arg)
elif n.type == N.REL:

View File

@ -396,6 +396,14 @@ class TextLexer(LexerBase):
err = s[0]
self.code = s[1] if len(s) > 1 else ""
# Comments
if c[0] == "#":
splits = c.split('\n', maxsplit=1)
print(splits)
self.code = c[len(splits[0]):]
self.position -= 1
return self.lex()
raise Exception("Lexical error near '{}'".format(err))
def at_end(self):

View File

@ -1,4 +1,3 @@
goto -60, 12 ; pendown ; goto x+13, y+15 ; penup
goto -60, 9 ; pendown ; goto x+13, y+13 ; penup
goto -60, 6 ; pendown ; goto x+13, y+8 ; penup
goto -60, 3 ; pendown ; goto x+13, y+5 ; penup
@ -7,12 +6,11 @@ goto -60, 0 ; pendown ; goto x+13, y ; penup
goto -47, -8 ; pendown ; goto x-13, y+5 ; penup
goto -47, -14 ; pendown ; goto x-13, y+8 ; penup
goto -47, -22 ; pendown ; goto x-13, y+13 ; penup
goto -47, -27 ; pendown ; goto x-13, y+15 ; penup
setvar 0, A
repeat 6
goto -30,16-7A
goto -35,16-7A
pendown
goto x+3A+3,y+5
penup
@ -22,3 +20,19 @@ repeat 6
penup
setvar A+1, A
repeat_end
setvar 0, B
repeat 5
setvar 0, A
repeat 4
goto 3A, 24-30B
goto x+B,y+B
pendown
goto x+A,y+A
penup
setvar A+1, A
repeat_end
setvar B+0.2,B
repeat_end