From 2c7287338c77445cff3fb70f41bd38b8544a8b87 Mon Sep 17 00:00:00 2001 From: Lephe Date: Sun, 6 Oct 2019 00:06:04 +0200 Subject: [PATCH] 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. --- ast.py | 2 +- drawing.py | 50 +++++++++++++++++++++++++++--- fx92.py | 14 +++++---- interpreter.py | 68 ++++++++++++++++------------------------- lexer.py | 8 +++++ tests/line-patterns.txt | 20 ++++++++++-- 6 files changed, 106 insertions(+), 56 deletions(-) diff --git a/ast.py b/ast.py index 9130ed0..df7b432 100644 --- a/ast.py +++ b/ast.py @@ -12,7 +12,7 @@ def auto_builder(): try: enum.auto() -except AttributeError: +except AttributeError: enum.auto = auto_builder() del auto_builder diff --git a/drawing.py b/drawing.py index 3b8dfe0..55ef608 100644 --- a/drawing.py +++ b/drawing.py @@ -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 diff --git a/fx92.py b/fx92.py index b468ef3..344e162 100755 --- a/fx92.py +++ b/fx92.py @@ -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 diff --git a/interpreter.py b/interpreter.py index 88f7415..df4ea01 100644 --- a/interpreter.py +++ b/interpreter.py @@ -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: diff --git a/lexer.py b/lexer.py index ec52e33..7057d6a 100644 --- a/lexer.py +++ b/lexer.py @@ -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): diff --git a/tests/line-patterns.txt b/tests/line-patterns.txt index 1bdee82..c87e603 100644 --- a/tests/line-patterns.txt +++ b/tests/line-patterns.txt @@ -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