From 7d319674518025f4f103a3041ae710a7468608e2 Mon Sep 17 00:00:00 2001 From: rocky Date: Wed, 19 May 2021 08:13:04 -0400 Subject: [PATCH 1/9] Proper way to indicate ColorDistance needs numpy --- mathics/builtin/graphics.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index cef531b68f..2d49cc80ac 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -851,6 +851,10 @@ class ColorDistance(Builtin): + "two lists of colors of the same length.", } + # If numpy is not installed, 100 * c1.to_color_space returns + # a list of 100 x 3 elements, instead of doing elementwise multiplication + requires = ("numpy",) + # the docs say LABColor's colorspace corresponds to the CIE 1976 L^* a^* b^* color space # with {l,a,b}={L^*,a^*,b^*}/100. Corrections factors are put accordingly. @@ -887,13 +891,6 @@ class ColorDistance(Builtin): def apply(self, c1, c2, evaluation, options): "ColorDistance[c1_, c2_, OptionsPattern[ColorDistance]]" - # If numpy is not installed, 100 * c1.to_color_space returns - # a list of 100 x 3 elements, instead of doing elementwise multiplication - try: - import numpy as np - except: - raise RuntimeError("NumPy needs to be installed for ColorDistance") - distance_function = options.get("System`DistanceFunction") compute = None if isinstance(distance_function, String): From 1ef44077c30c666129e3ed1ca61a35f27db98628 Mon Sep 17 00:00:00 2001 From: rocky Date: Wed, 19 May 2021 06:19:41 -0400 Subject: [PATCH 2/9] WIP - start asymptote --- mathics/builtin/graphics.py | 298 ----------------------- mathics/formatter/asy.py | 457 ++++++++++++++++++++++++++++++++++++ 2 files changed, 457 insertions(+), 298 deletions(-) create mode 100644 mathics/formatter/asy.py diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 2d49cc80ac..349eee009b 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -8,7 +8,6 @@ from math import floor, ceil, log10, sin, cos, pi, sqrt, atan2, degrees, radians, exp import base64 -from itertools import chain from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import ( @@ -144,10 +143,6 @@ def create_css( return "; ".join(css) -def asy_number(value): - return "%.5g" % value - - def _to_float(x): x = x.round_to_float() if x is None: @@ -155,29 +150,6 @@ def _to_float(x): return x -def create_pens( - edge_color=None, face_color=None, stroke_width=None, is_face_element=False -): - result = [] - if face_color is not None: - brush, opacity = face_color.to_asy() - if opacity != 1: - brush += "+opacity(%s)" % asy_number(opacity) - result.append(brush) - elif is_face_element: - result.append("nullpen") - if edge_color is not None: - pen, opacity = edge_color.to_asy() - if opacity != 1: - pen += "+opacity(%s)" % asy_number(opacity) - if stroke_width is not None: - pen += "+linewidth(%s)" % asy_number(stroke_width) - result.append(pen) - elif is_face_element: - result.append("nullpen") - return ", ".join(result) - - def _data_and_options(leaves, defined_options): data = [] options = defined_options.copy() @@ -335,41 +307,6 @@ def _extract_graphics(graphics, format, evaluation): return xmin, xmax, ymin, ymax, ox, oy, ex, ey, code -class _ASYTransform: - _template = """ - add(%s * (new picture() { - picture saved = currentpicture; - picture transformed = new picture; - currentpicture = transformed; - %s - currentpicture = saved; - return transformed; - })()); - """ - - def __init__(self): - self.transforms = [] - - def matrix(self, a, b, c, d, e, f): - # a c e - # b d f - # 0 0 1 - # see http://asymptote.sourceforge.net/doc/Transforms.html#Transforms - self.transforms.append("(%f, %f, %f, %f, %f, %f)" % (e, f, a, c, b, d)) - - def translate(self, x, y): - self.transforms.append("shift(%f, %f)" % (x, y)) - - def scale(self, x, y): - self.transforms.append("scale(%f, %f)" % (x, y)) - - def rotate(self, x): - self.transforms.append("rotate(%f)" % x) - - def apply(self, asy): - return self._template % (" * ".join(self.transforms), asy) - - class Show(Builtin): """
@@ -596,15 +533,6 @@ def to_css(self): alpha, ) - def to_asy(self): - rgba = self.to_rgba() - alpha = rgba[3] if len(rgba) > 3 else 1.0 - return ( - r"rgb(%s, %s, %s)" - % (asy_number(rgba[0]), asy_number(rgba[1]), asy_number(rgba[2])), - alpha, - ) - def to_js(self): return self.to_rgba() @@ -1216,25 +1144,6 @@ def extent(self): ) return result - def to_asy(self): - l = self.style.get_line_width(face_element=True) - x1, y1 = self.p1.pos() - x2, y2 = self.p2.pos() - pens = create_pens(self.edge_color, self.face_color, l, is_face_element=True) - x1, x2, y1, y2 = asy_number(x1), asy_number(x2), asy_number(y1), asy_number(y2) - return "filldraw((%s,%s)--(%s,%s)--(%s,%s)--(%s,%s)--cycle, %s);" % ( - x1, - y1, - x2, - y1, - x2, - y2, - x1, - y2, - pens, - ) - - class _RoundBox(_GraphicsElement): face_element = None @@ -1267,27 +1176,6 @@ def extent(self): ry += l return [(x - rx, y - ry), (x - rx, y + ry), (x + rx, y - ry), (x + rx, y + ry)] - def to_asy(self): - x, y = self.c.pos() - rx, ry = self.r.pos() - rx -= x - ry -= y - l = self.style.get_line_width(face_element=self.face_element) - pen = create_pens( - edge_color=self.edge_color, - face_color=self.face_color, - stroke_width=l, - is_face_element=self.face_element, - ) - cmd = "filldraw" if self.face_element else "draw" - return "%s(ellipse((%s,%s),%s,%s), %s);" % ( - cmd, - asy_number(x), - asy_number(y), - asy_number(rx), - asy_number(ry), - pen, - ) class _ArcBox(_RoundBox): @@ -1420,17 +1308,6 @@ def init(self, graphics, style, item=None): else: raise BoxConstructError - def to_asy(self): - pen = create_pens(face_color=self.face_color, is_face_element=False) - - asy = "" - for line in self.lines: - for coords in line: - asy += "dot(%s, %s);" % (coords.pos(), pen) - - return asy - - class Line(Builtin): """
@@ -1464,16 +1341,6 @@ def init(self, graphics, style, item=None, lines=None): else: raise BoxConstructError - def to_asy(self): - l = self.style.get_line_width(face_element=False) - pen = create_pens(edge_color=self.edge_color, stroke_width=l) - asy = "" - for line in self.lines: - path = "--".join(["(%.5g,%5g)" % coords.pos() for coords in line]) - asy += "draw(%s, %s);" % (path, pen) - return asy - - def _svg_bezier(*segments): # see https://www.w3.org/TR/SVG/paths.html#PathDataCubicBezierCommands # see https://docs.webplatform.org/wiki/svg/tutorials/smarter_svg_shapes @@ -1506,65 +1373,6 @@ def path(max_degree, p): yield s -def _asy_bezier(*segments): - # see http://asymptote.sourceforge.net/doc/Bezier-curves.html#Bezier-curves - - while segments and not segments[0][1]: - segments = segments[1:] - - if not segments: - return - - def cubic(p0, p1, p2, p3): - return "..controls(%.5g,%.5g) and (%.5g,%.5g)..(%.5g,%.5g)" % tuple( - list(chain(p1, p2, p3)) - ) - - def quadratric(qp0, qp1, qp2): - # asymptote only supports cubic beziers, so we convert this quadratic - # bezier to a cubic bezier, see http://fontforge.github.io/bezier.html - - # CP0 = QP0 - # CP3 = QP2 - # CP1 = QP0 + 2 / 3 * (QP1 - QP0) - # CP2 = QP2 + 2 / 3 * (QP1 - QP2) - - qp0x, qp0y = qp0 - qp1x, qp1y = qp1 - qp2x, qp2y = qp2 - - t = 2.0 / 3.0 - cp0 = qp0 - cp1 = (qp0x + t * (qp1x - qp0x), qp0y + t * (qp1y - qp0y)) - cp2 = (qp2x + t * (qp1x - qp2x), qp2y + t * (qp1y - qp2y)) - cp3 = qp2 - - return cubic(cp0, cp1, cp2, cp3) - - def linear(p0, p1): - return "--(%.5g,%.5g)" % p1 - - forms = (linear, quadratric, cubic) - - def path(max_degree, p): - max_degree = min(max_degree, len(forms)) - while p: - n = min(max_degree, len(p) - 1) # 1, 2, or 3 - if n < 1: - break - yield forms[n - 1](*p[: n + 1]) - p = p[n:] - - k, p = segments[0] - yield "(%.5g,%.5g)" % p[0] - - connect = [] - for k, p in segments: - for s in path(k, list(chain(connect, p))): - yield s - connect = p[-1:] - - class BernsteinBasis(Builtin): attributes = ("Listable", "NumericFunction", "Protected") rules = { @@ -1608,18 +1416,6 @@ def init(self, graphics, style, item, options): raise BoxConstructError self.spline_degree = spline_degree.get_int_value() - def to_asy(self): - l = self.style.get_line_width(face_element=False) - pen = create_pens(edge_color=self.edge_color, stroke_width=l) - - asy = "" - for line in self.lines: - for path in _asy_bezier((self.spline_degree, [xy.pos() for xy in line])): - if path[:2] == "..": - path = "(0.,0.)" + path - asy += "draw(%s, %s);" % (path, pen) - return asy - class FilledCurve(Builtin): """ @@ -1685,20 +1481,6 @@ def parse_component(segments): else: raise BoxConstructError - def to_asy(self): - l = self.style.get_line_width(face_element=False) - pen = create_pens(edge_color=self.edge_color, stroke_width=l) - - if not pen: - pen = "currentpen" - - def components(): - for component in self.components: - transformed = [(k, [xy.pos() for xy in p]) for k, p in component] - yield "fill(%s--cycle, %s);" % ("".join(_asy_bezier(*transformed)), pen) - - return "".join(components()) - def extent(self): l = self.style.get_line_width(face_element=False) result = [] @@ -1774,51 +1556,6 @@ def process_option(self, name, value): else: raise BoxConstructError - def to_asy(self): - l = self.style.get_line_width(face_element=True) - if self.vertex_colors is None: - face_color = self.face_color - else: - face_color = None - pens = create_pens( - edge_color=self.edge_color, - face_color=face_color, - stroke_width=l, - is_face_element=True, - ) - asy = "" - if self.vertex_colors is not None: - paths = [] - colors = [] - edges = [] - for index, line in enumerate(self.lines): - paths.append( - "--".join(["(%.5g,%.5g)" % coords.pos() for coords in line]) - + "--cycle" - ) - - # ignore opacity - colors.append( - ",".join([color.to_asy()[0] for color in self.vertex_colors[index]]) - ) - - edges.append( - ",".join(["0"] + ["1"] * (len(self.vertex_colors[index]) - 1)) - ) - - asy += "gouraudshade(%s, new pen[] {%s}, new int[] {%s});" % ( - "^^".join(paths), - ",".join(colors), - ",".join(edges), - ) - if pens and pens != "nullpen": - for line in self.lines: - path = ( - "--".join(["(%.5g,%.5g)" % coords.pos() for coords in line]) - + "--cycle" - ) - asy += "filldraw(%s, %s);" % (path, pens) - return asy class RegularPolygon(Builtin): @@ -2329,23 +2066,6 @@ def draw(px, py, vx, vy, t1, s): return make - def to_asy(self): - width = self.style.get_line_width(face_element=False) - pen = create_pens(edge_color=self.edge_color, stroke_width=width) - polyline = self.curve.make_draw_asy(pen) - - arrow_pen = create_pens(face_color=self.edge_color, stroke_width=width) - - def polygon(points): - yield "filldraw(" - yield "--".join(["(%.5g,%5g)" % xy for xy in points]) - yield "--cycle, % s);" % arrow_pen - - extent = self.graphics.view_width or 0 - default_arrow = self._default_arrow(polygon) - custom_arrow = self._custom_arrow("asy", _ASYTransform) - return "".join(self._draw(polyline, default_arrow, custom_arrow, extent)) - def extent(self): width = self.style.get_line_width(face_element=False) @@ -2415,21 +2135,6 @@ def extent(self): y = p[1] - h / 2.0 + opos[1] * h / 2.0 return [(x, y), (x + w, y + h)] - def to_asy(self): - x, y = self.pos.pos() - content = self.content.boxes_to_tex(evaluation=self.graphics.evaluation) - pen = create_pens(edge_color=self.color) - asy = 'label("$%s$", (%s,%s), (%s,%s), %s);' % ( - content, - x, - y, - -self.opos[0], - -self.opos[1], - pen, - ) - return asy - - def total_extent(extents): xmin = xmax = ymin = ymax = None for extent in extents: @@ -2728,9 +2433,6 @@ def extent(self, completely_visible_only=False): ymax *= 2 return xmin, xmax, ymin, ymax - def to_asy(self): - return "\n".join(element.to_asy() for element in self.elements) - def set_size( self, xmin, ymin, extent_width, extent_height, pixel_width, pixel_height ): diff --git a/mathics/formatter/asy.py b/mathics/formatter/asy.py new file mode 100644 index 0000000000..2a50f20bff --- /dev/null +++ b/mathics/formatter/asy.py @@ -0,0 +1,457 @@ +# -*- coding: utf-8 -*- + +""" +Format a Mathics object as an Aymptote string +""" + +from itertools import chain + +from mathics.builtin.graphics import ( + _ArcBox, + ArrowBox, + _Color, + BezierCurveBox, + FilledCurveBox, + GraphicsBox, + GraphicsElements, + InsetBox, + LineBox, + PointBox, + PolygonBox, + RectangleBox, + _RoundBox, +) + +from mathics.core.formatter import lookup_method, add_conversion_fn + + +class _ASYTransform: + _template = """ + add(%s * (new picture() { + picture saved = currentpicture; + picture transformed = new picture; + currentpicture = transformed; + %s + currentpicture = saved; + return transformed; + })()); + """ + + def __init__(self): + self.transforms = [] + + def matrix(self, a, b, c, d, e, f): + # a c e + # b d f + # 0 0 1 + # see http://asymptote.sourceforge.net/doc/Transforms.html#Transforms + self.transforms.append("(%f, %f, %f, %f, %f, %f)" % (e, f, a, c, b, d)) + + def translate(self, x, y): + self.transforms.append("shift(%f, %f)" % (x, y)) + + def scale(self, x, y): + self.transforms.append("scale(%f, %f)" % (x, y)) + + def rotate(self, x): + self.transforms.append("rotate(%f)" % x) + + def apply(self, asy): + return self._template % (" * ".join(self.transforms), asy) + + +def _asy_bezier(*segments): + # see http://asymptote.sourceforge.net/doc/Bezier-curves.html#Bezier-curves + + while segments and not segments[0][1]: + segments = segments[1:] + + if not segments: + return + + def cubic(p0, p1, p2, p3): + return "..controls(%.5g,%.5g) and (%.5g,%.5g)..(%.5g,%.5g)" % tuple( + list(chain(p1, p2, p3)) + ) + + def quadratric(qp0, qp1, qp2): + # asymptote only supports cubic beziers, so we convert this quadratic + # bezier to a cubic bezier, see http://fontforge.github.io/bezier.html + + # CP0 = QP0 + # CP3 = QP2 + # CP1 = QP0 + 2 / 3 * (QP1 - QP0) + # CP2 = QP2 + 2 / 3 * (QP1 - QP2) + + qp0x, qp0y = qp0 + qp1x, qp1y = qp1 + qp2x, qp2y = qp2 + + t = 2.0 / 3.0 + cp0 = qp0 + cp1 = (qp0x + t * (qp1x - qp0x), qp0y + t * (qp1y - qp0y)) + cp2 = (qp2x + t * (qp1x - qp2x), qp2y + t * (qp1y - qp2y)) + cp3 = qp2 + + return cubic(cp0, cp1, cp2, cp3) + + def linear(p0, p1): + return "--(%.5g,%.5g)" % p1 + + forms = (linear, quadratric, cubic) + + def path(max_degree, p): + max_degree = min(max_degree, len(forms)) + while p: + n = min(max_degree, len(p) - 1) # 1, 2, or 3 + if n < 1: + break + yield forms[n - 1](*p[: n + 1]) + p = p[n:] + + k, p = segments[0] + yield "(%.5g,%.5g)" % p[0] + + connect = [] + for k, p in segments: + for s in path(k, list(chain(connect, p))): + yield s + connect = p[-1:] + + +def asy_number(value): + return "%.5g" % value + + +def create_pens( + edge_color=None, face_color=None, stroke_width=None, is_face_element=False +): + result = [] + if face_color is not None: + brush, opacity = _color(face_color) + if opacity != 1: + brush += "+opacity(%s)" % asy_number(opacity) + result.append(brush) + elif is_face_element: + result.append("nullpen") + if edge_color is not None: + pen, opacity = _color(edge_color) + if opacity != 1: + pen += "+opacity(%s)" % asy_number(opacity) + if stroke_width is not None: + pen += "+linewidth(%s)" % asy_number(stroke_width) + result.append(pen) + elif is_face_element: + result.append("nullpen") + return ", ".join(result) + + +def _color(self): + rgba = self.to_rgba() + alpha = rgba[3] if len(rgba) > 3 else 1.0 + return ( + r"rgb(%s, %s, %s)" + % (asy_number(rgba[0]), asy_number(rgba[1]), asy_number(rgba[2])), + alpha, + ) + + +add_conversion_fn(_Color) + + +def _arcbox(self): + if self.arc is None: + return super(_ArcBox, self).to_asy() + + x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params() + + def path(closed): + if closed: + yield "(%s,%s)--(%s,%s)--" % tuple(asy_number(t) for t in (x, y, sx, sy)) + + yield "arc((%s,%s), (%s, %s), (%s, %s))" % tuple( + asy_number(t) for t in (x, y, sx, sy, ex, ey) + ) + + if closed: + yield "--cycle" + + line_width = self.style.get_line_width(face_element=self.face_element) + pen = create_pens( + edge_color=self.edge_color, + face_color=self.face_color, + stroke_width=line_width, + is_face_element=self.face_element, + ) + command = "filldraw" if self.face_element else "draw" + return "%s(%s, %s);" % (command, "".join(path(self.face_element)), pen) + + +add_conversion_fn(_ArcBox) + + +def arrowbox(self): + width = self.style.get_line_width(face_element=False) + pen = create_pens(edge_color=self.edge_color, stroke_width=width) + polyline = self.curve.make_draw_asy(pen) + + arrow_pen = create_pens(face_color=self.edge_color, stroke_width=width) + + def polygon(points): + yield "filldraw(" + yield "--".join(["(%.5g,%5g)" % xy for xy in points]) + yield "--cycle, % s);" % arrow_pen + + extent = self.graphics.view_width or 0 + default_arrow = self._default_arrow(polygon) + custom_arrow = self._custom_arrow("asy", _ASYTransform) + return "".join(self._draw(polyline, default_arrow, custom_arrow, extent)) + + +add_conversion_fn(ArrowBox) + + +def beziercurvebox(self): + line_width = self.style.get_line_width(face_element=False) + pen = create_pens(edge_color=self.edge_color, stroke_width=line_width) + + asy = "" + for line in self.lines: + for path in _asy_bezier((self.spline_degree, [xy.pos() for xy in line])): + if path[:2] == "..": + path = "(0.,0.)" + path + asy += "draw(%s, %s);" % (path, pen) + return asy + if self.arc is None: + return super(_ArcBox, self).to_asy() + + x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params() + + +add_conversion_fn(BezierCurveBox) + + +def filledcurvebox(self): + line_width = self.style.get_line_width(face_element=False) + pen = create_pens(edge_color=self.edge_color, stroke_width=line_width) + + if not pen: + pen = "currentpen" + + def components(): + for component in self.components: + transformed = [(k, [xy.pos() for xy in p]) for k, p in component] + yield "fill(%s--cycle, %s);" % ("".join(_asy_bezier(*transformed)), pen) + + return "".join(components()) + + +add_conversion_fn(FilledCurveBox) + +# FIXME figure out how we can add this. +def graphicsbox(self, leaves=None, **options) -> str: + if not leaves: + leaves = self._leaves + + data = options.get("data", None) + if data: + elements, xmin, xmax, ymin, ymax, w, h, width, height = data + else: + elements, calc_dimensions = self._prepare_elements(leaves, options, neg_y=True) + xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() + + elements.view_width = w + + format_fn = lookup_method(elements, "svg") + if format_fn is not None: + svg_body = format_fn(elements, offset=options.get("offset", None)) + else: + svg_body = elements.to_svg(offset=options.get("offset", None)) + + if self.background_color is not None: + # Wrap svg_elements in a rectangle + svg_body = '%s' % ( + xmin, + ymin, + w, + h, + self.background_color.to_css()[0], + svg_body, + ) + + xmin -= 1 + ymin -= 1 + w += 2 + h += 2 + + if options.get("noheader", False): + return svg_body + svg_main = """ + + %s + + """ % ( + " ".join("%f" % t for t in (xmin, ymin, w, h)), + svg_body, + ) + return svg_main # , width, height + + +add_conversion_fn(GraphicsBox) + + +def graphicselements(self, offset=None): + result = [] + for element in self.elements: + format_fn = lookup_method(element, "asy") + if format_fn is None: + result.append(element.to_asy(offset)) + else: + result.append(format_fn(element, offset)) + + return "\n".join(result) + + +add_conversion_fn(GraphicsElements) + + +def insetbox(self): + x, y = self.pos.pos() + content = self.content.boxes_to_tex(evaluation=self.graphics.evaluation) + pen = create_pens(edge_color=self.color) + asy = 'label("$%s$", (%s,%s), (%s,%s), %s);' % ( + content, + x, + y, + -self.opos[0], + -self.opos[1], + pen, + ) + return asy + + +add_conversion_fn(InsetBox) + + +def linebox(self): + line_width = self.style.get_line_width(face_element=False) + pen = create_pens(edge_color=self.edge_color, stroke_width=line_width) + asy = "" + for line in self.lines: + path = "--".join(["(%.5g,%5g)" % coords.pos() for coords in line]) + asy += "draw(%s, %s);" % (path, pen) + return asy + + +add_conversion_fn(LineBox) + + +def pointbox(self): + pen = create_pens(face_color=self.face_color, is_face_element=False) + + asy = "" + for line in self.lines: + for coords in line: + asy += "dot(%s, %s);" % (coords.pos(), pen) + + return asy + + +add_conversion_fn(PointBox) + + +def polygonbox(self): + line_width = self.style.get_line_width(face_element=True) + if self.vertex_colors is None: + face_color = self.face_color + else: + face_color = None + pens = create_pens( + edge_color=self.edge_color, + face_color=face_color, + stroke_width=line_width, + is_face_element=True, + ) + asy = "" + if self.vertex_colors is not None: + paths = [] + colors = [] + edges = [] + for index, line in enumerate(self.lines): + paths.append( + "--".join(["(%.5g,%.5g)" % coords.pos() for coords in line]) + "--cycle" + ) + + # ignore opacity + colors.append( + ",".join([_color(color)[0] for color in self.vertex_colors[index]]) + ) + + edges.append(",".join(["0"] + ["1"] * (len(self.vertex_colors[index]) - 1))) + + asy += "gouraudshade(%s, new pen[] {%s}, new int[] {%s});" % ( + "^^".join(paths), + ",".join(colors), + ",".join(edges), + ) + if pens and pens != "nullpen": + for line in self.lines: + path = ( + "--".join(["(%.5g,%.5g)" % coords.pos() for coords in line]) + "--cycle" + ) + asy += "filldraw(%s, %s);" % (path, pens) + return asy + + +add_conversion_fn(PolygonBox) + + +def rectanglebox(self): + line_width = self.style.get_line_width(face_element=True) + x1, y1 = self.p1.pos() + x2, y2 = self.p2.pos() + pens = create_pens(self.edge_color, self.face_color, line_width, is_face_element=True) + x1, x2, y1, y2 = asy_number(x1), asy_number(x2), asy_number(y1), asy_number(y2) + return "filldraw((%s,%s)--(%s,%s)--(%s,%s)--(%s,%s)--cycle, %s);" % ( + x1, + y1, + x2, + y1, + x2, + y2, + x1, + y2, + pens, + ) + + +add_conversion_fn(RectangleBox) + + +def _roundbox(self): + x, y = self.c.pos() + rx, ry = self.r.pos() + rx -= x + ry -= y + line_width = self.style.get_line_width(face_element=self.face_element) + pen = create_pens( + edge_color=self.edge_color, + face_color=self.face_color, + stroke_width=line_width, + is_face_element=self.face_element, + ) + cmd = "filldraw" if self.face_element else "draw" + return "%s(ellipse((%s,%s),%s,%s), %s);" % ( + cmd, + asy_number(x), + asy_number(y), + asy_number(rx), + asy_number(ry), + pen, + ) + + +add_conversion_fn(_RoundBox) From 1806e0a78a32034635cdd4bd7b0f15991f2394b3 Mon Sep 17 00:00:00 2001 From: rocky Date: Wed, 19 May 2021 07:42:02 -0400 Subject: [PATCH 3/9] Remove _arcbox formatting It doesn't seem to be used. --- mathics/formatter/asy.py | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/mathics/formatter/asy.py b/mathics/formatter/asy.py index 2a50f20bff..3b0adeacc2 100644 --- a/mathics/formatter/asy.py +++ b/mathics/formatter/asy.py @@ -7,7 +7,6 @@ from itertools import chain from mathics.builtin.graphics import ( - _ArcBox, ArrowBox, _Color, BezierCurveBox, @@ -159,37 +158,6 @@ def _color(self): add_conversion_fn(_Color) -def _arcbox(self): - if self.arc is None: - return super(_ArcBox, self).to_asy() - - x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params() - - def path(closed): - if closed: - yield "(%s,%s)--(%s,%s)--" % tuple(asy_number(t) for t in (x, y, sx, sy)) - - yield "arc((%s,%s), (%s, %s), (%s, %s))" % tuple( - asy_number(t) for t in (x, y, sx, sy, ex, ey) - ) - - if closed: - yield "--cycle" - - line_width = self.style.get_line_width(face_element=self.face_element) - pen = create_pens( - edge_color=self.edge_color, - face_color=self.face_color, - stroke_width=line_width, - is_face_element=self.face_element, - ) - command = "filldraw" if self.face_element else "draw" - return "%s(%s, %s);" % (command, "".join(path(self.face_element)), pen) - - -add_conversion_fn(_ArcBox) - - def arrowbox(self): width = self.style.get_line_width(face_element=False) pen = create_pens(edge_color=self.edge_color, stroke_width=width) From 129317e21c2f5181fe810e72572adf145a9d79a2 Mon Sep 17 00:00:00 2001 From: rocky Date: Wed, 19 May 2021 08:09:27 -0400 Subject: [PATCH 4/9] Reinstate some asy routines graphics... until we can factor those out. --- mathics/builtin/graphics.py | 74 ++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 349eee009b..06537a0b48 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -8,6 +8,7 @@ from math import floor, ceil, log10, sin, cos, pi, sqrt, atan2, degrees, radians, exp import base64 +from itertools import chain from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import ( @@ -49,6 +50,70 @@ "$OptionSyntax": "Ignore", } +# FIXME: move out into formatter after make_draw_asy is moved +def _asy_bezier(*segments): + # see http://asymptote.sourceforge.net/doc/Bezier-curves.html#Bezier-curves + + while segments and not segments[0][1]: + segments = segments[1:] + + if not segments: + return + + def cubic(p0, p1, p2, p3): + return "..controls(%.5g,%.5g) and (%.5g,%.5g)..(%.5g,%.5g)" % tuple( + list(chain(p1, p2, p3)) + ) + + def quadratric(qp0, qp1, qp2): + # asymptote only supports cubic beziers, so we convert this quadratic + # bezier to a cubic bezier, see http://fontforge.github.io/bezier.html + + # CP0 = QP0 + # CP3 = QP2 + # CP1 = QP0 + 2 / 3 * (QP1 - QP0) + # CP2 = QP2 + 2 / 3 * (QP1 - QP2) + + qp0x, qp0y = qp0 + qp1x, qp1y = qp1 + qp2x, qp2y = qp2 + + t = 2.0 / 3.0 + cp0 = qp0 + cp1 = (qp0x + t * (qp1x - qp0x), qp0y + t * (qp1y - qp0y)) + cp2 = (qp2x + t * (qp1x - qp2x), qp2y + t * (qp1y - qp2y)) + cp3 = qp2 + + return cubic(cp0, cp1, cp2, cp3) + + def linear(p0, p1): + return "--(%.5g,%.5g)" % p1 + + forms = (linear, quadratric, cubic) + + def path(max_degree, p): + max_degree = min(max_degree, len(forms)) + while p: + n = min(max_degree, len(p) - 1) # 1, 2, or 3 + if n < 1: + break + yield forms[n - 1](*p[: n + 1]) + p = p[n:] + + k, p = segments[0] + yield "(%.5g,%.5g)" % p[0] + + connect = [] + for k, p in segments: + for s in path(k, list(chain(connect, p))): + yield s + connect = p[-1:] + + +# FIXME: move out into formatter after make_draw_asy is moved +def asy_number(value): + return "%.5g" % value + class CoordinatesError(BoxConstructError): pass @@ -773,6 +838,8 @@ class ColorDistance(Builtin): options = {"DistanceFunction": "Automatic"} + requires = ("numpy",) + messages = { "invdist": "`1` is not Automatic or a valid distance specification.", "invarg": "`1` and `2` should be two colors or a color and a lists of colors or " @@ -1144,6 +1211,7 @@ def extent(self): ) return result + class _RoundBox(_GraphicsElement): face_element = None @@ -1177,7 +1245,6 @@ def extent(self): return [(x - rx, y - ry), (x - rx, y + ry), (x + rx, y - ry), (x + rx, y + ry)] - class _ArcBox(_RoundBox): def init(self, graphics, style, item): if len(item.leaves) == 3: @@ -1227,6 +1294,7 @@ def _arc_params(self): return x, y, abs(rx), abs(ry), sx, sy, ex, ey, large_arc + class DiskBox(_ArcBox): face_element = True @@ -1308,6 +1376,7 @@ def init(self, graphics, style, item=None): else: raise BoxConstructError + class Line(Builtin): """
@@ -1341,6 +1410,7 @@ def init(self, graphics, style, item=None, lines=None): else: raise BoxConstructError + def _svg_bezier(*segments): # see https://www.w3.org/TR/SVG/paths.html#PathDataCubicBezierCommands # see https://docs.webplatform.org/wiki/svg/tutorials/smarter_svg_shapes @@ -1557,7 +1627,6 @@ def process_option(self, name, value): raise BoxConstructError - class RegularPolygon(Builtin): """
@@ -2135,6 +2204,7 @@ def extent(self): y = p[1] - h / 2.0 + opos[1] * h / 2.0 return [(x, y), (x + w, y + h)] + def total_extent(extents): xmin = xmax = ymin = ymax = None for extent in extents: From d12ad2b5ac27594840851b01f4eebb9b97a5463e Mon Sep 17 00:00:00 2001 From: rocky Date: Wed, 19 May 2021 14:36:36 -0400 Subject: [PATCH 5/9] Convert to_asy() to lookup_method(xx,"asy)(xx) Short of adding tests, this finishes the first cut at moving asy conversion methods. --- mathics/builtin/drawing/graphics3d.py | 47 ++------ mathics/builtin/graphics.py | 50 +++++++-- mathics/core/definitions.py | 3 +- mathics/formatter/asy.py | 154 ++++++++++++++++++-------- mathics/formatter/svg.py | 1 - 5 files changed, 157 insertions(+), 98 deletions(-) diff --git a/mathics/builtin/drawing/graphics3d.py b/mathics/builtin/drawing/graphics3d.py index efbf2c4cbf..93a3158c13 100644 --- a/mathics/builtin/drawing/graphics3d.py +++ b/mathics/builtin/drawing/graphics3d.py @@ -7,12 +7,14 @@ import numbers from mathics.version import __version__ # noqa used in loading to check consistency. + from mathics.core.expression import ( Expression, from_python, system_symbols_dict, SymbolList, ) +from mathics.core.formatter import lookup_method from mathics.builtin.base import BoxConstructError, Builtin, InstanceableBuiltin from mathics.builtin.graphics import ( Graphics, @@ -443,7 +445,12 @@ def boxes_to_tex(self, leaves=None, **options): elements._apply_boxscaling(boxscale) - asy = elements.to_asy() + + format_fn = lookup_method(elements, "asy") + if format_fn is not None: + asy = format_fn(elements) + else: + asy = elements.to_asy() xmin, xmax, ymin, ymax, zmin, zmax, boxscale = calc_dimensions() @@ -778,9 +785,6 @@ def __init__(self, content, evaluation, neg_y=False): def extent(self, completely_visible_only=False): return total_extent_3d([element.extent() for element in self.elements]) - def to_asy(self): - return "\n".join([element.to_asy() for element in self.elements]) - def _apply_boxscaling(self, boxscale): for element in self.elements: element._apply_boxscaling(boxscale) @@ -871,18 +875,6 @@ def to_json(self): ) return data - def to_asy(self): - # l = self.style.get_line_width(face_element=False) - pen = create_pens(edge_color=self.edge_color, stroke_width=1) - - return "".join( - "draw({0}, {1});".format( - "--".join("({0},{1},{2})".format(*coords.pos()[0]) for coords in line), - pen, - ) - for line in self.lines - ) - def extent(self): result = [] for line in self.lines: @@ -930,29 +922,6 @@ def to_json(self): ) return data - def to_asy(self): - l = self.style.get_line_width(face_element=True) - if self.vertex_colors is None: - face_color = self.face_color - else: - face_color = None - pen = create_pens( - edge_color=self.edge_color, - face_color=face_color, - stroke_width=l, - is_face_element=True, - ) - - asy = "" - for line in self.lines: - asy += ( - "path3 g=" - + "--".join(["(%.5g,%.5g,%.5g)" % coords.pos()[0] for coords in line]) - + "--cycle;" - ) - asy += "draw(surface(g), %s);" % (pen) - return asy - def extent(self): result = [] for line in self.lines: diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 06537a0b48..c93b56f516 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -114,6 +114,39 @@ def path(max_degree, p): def asy_number(value): return "%.5g" % value +# This is asy specific. Move elsewhere +def _color(self): + rgba = self.to_rgba() + alpha = rgba[3] if len(rgba) > 3 else 1.0 + return ( + r"rgb(%s, %s, %s)" + % (asy_number(rgba[0]), asy_number(rgba[1]), asy_number(rgba[2])), + alpha, + ) + + +def create_pens( + edge_color=None, face_color=None, stroke_width=None, is_face_element=False +): + result = [] + if face_color is not None: + brush, opacity = _color(face_color) + if opacity != 1: + brush += "+opacity(%s)" % asy_number(opacity) + result.append(brush) + elif is_face_element: + result.append("nullpen") + if edge_color is not None: + pen, opacity = _color(edge_color) + if opacity != 1: + pen += "+opacity(%s)" % asy_number(opacity) + if stroke_width is not None: + pen += "+linewidth(%s)" % asy_number(stroke_width) + result.append(pen) + elif is_face_element: + result.append("nullpen") + return ", ".join(result) + class CoordinatesError(BoxConstructError): pass @@ -358,14 +391,9 @@ def _extract_graphics(graphics, format, evaluation): # generate code for svg or asy. - if format == "asy": - code = "\n".join(element.to_asy() for element in elements.elements) - elif format == "svg": - format_fn = lookup_method(elements, "svg") - if format_fn is not None: - code = format_fn(elements) - else: - code = elements.to_svg() + if format in ("asy", "svg"): + format_fn = lookup_method(elements, format) + code = format_fn(elements) else: raise NotImplementedError @@ -2743,13 +2771,13 @@ def boxes_to_tex(self, leaves=None, **options): elements.view_width = w asy_completely_visible = "\n".join( - element.to_asy() + lookup_method(element, "asy")(element) for element in elements.elements if element.is_completely_visible ) asy_regular = "\n".join( - element.to_asy() + lookup_method(element, "asy")(element) for element in elements.elements if not element.is_completely_visible ) @@ -2762,7 +2790,7 @@ def boxes_to_tex(self, leaves=None, **options): ) if self.background_color is not None: - color, opacity = self.background_color.to_asy() + color, opacity = _color(self.background_color) asy_background = "filldraw(%s, %s);" % (asy_box, color) else: asy_background = "" diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py index 57d0228be8..75cb5adb58 100644 --- a/mathics/core/definitions.py +++ b/mathics/core/definitions.py @@ -125,7 +125,8 @@ def __init__( self.builtin.update(self.user) self.user = {} self.clear_cache() - import mathics.formatter.svg + import mathics.formatter.svg # noqa + import mathics.formatter.asy # noqa def load_pymathics_module(self, module, remove_on_quit=True): """ diff --git a/mathics/formatter/asy.py b/mathics/formatter/asy.py index 3b0adeacc2..9b443b6f21 100644 --- a/mathics/formatter/asy.py +++ b/mathics/formatter/asy.py @@ -7,8 +7,12 @@ from itertools import chain from mathics.builtin.graphics import ( - ArrowBox, + _color, _Color, + _ArcBox, + ArrowBox, # FIXME move elsewhere + asy_number, # FIXME move elsewhere + create_pens, BezierCurveBox, FilledCurveBox, GraphicsBox, @@ -17,10 +21,19 @@ LineBox, PointBox, PolygonBox, + RGBColor, RectangleBox, _RoundBox, ) +from mathics.builtin.drawing.graphics3d import ( + Graphics3DElements, + Line3DBox, + Point3DBox, + Polygon3DBox, + Sphere3DBox, +) + from mathics.core.formatter import lookup_method, add_conversion_fn @@ -118,47 +131,14 @@ def path(max_degree, p): connect = p[-1:] -def asy_number(value): - return "%.5g" % value - - -def create_pens( - edge_color=None, face_color=None, stroke_width=None, is_face_element=False -): - result = [] - if face_color is not None: - brush, opacity = _color(face_color) - if opacity != 1: - brush += "+opacity(%s)" % asy_number(opacity) - result.append(brush) - elif is_face_element: - result.append("nullpen") - if edge_color is not None: - pen, opacity = _color(edge_color) - if opacity != 1: - pen += "+opacity(%s)" % asy_number(opacity) - if stroke_width is not None: - pen += "+linewidth(%s)" % asy_number(stroke_width) - result.append(pen) - elif is_face_element: - result.append("nullpen") - return ", ".join(result) - - -def _color(self): - rgba = self.to_rgba() - alpha = rgba[3] if len(rgba) > 3 else 1.0 - return ( - r"rgb(%s, %s, %s)" - % (asy_number(rgba[0]), asy_number(rgba[1]), asy_number(rgba[2])), - alpha, - ) +# def asy_number(value): +# return "%.5g" % value add_conversion_fn(_Color) -def arrowbox(self): +def arrowbox(self) -> str: width = self.style.get_line_width(face_element=False) pen = create_pens(edge_color=self.edge_color, stroke_width=width) polyline = self.curve.make_draw_asy(pen) @@ -179,7 +159,7 @@ def polygon(points): add_conversion_fn(ArrowBox) -def beziercurvebox(self): +def beziercurvebox(self) -> str: line_width = self.style.get_line_width(face_element=False) pen = create_pens(edge_color=self.edge_color, stroke_width=line_width) @@ -191,7 +171,7 @@ def beziercurvebox(self): asy += "draw(%s, %s);" % (path, pen) return asy if self.arc is None: - return super(_ArcBox, self).to_asy() + return _roundbox(self) x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params() @@ -199,7 +179,7 @@ def beziercurvebox(self): add_conversion_fn(BezierCurveBox) -def filledcurvebox(self): +def filledcurvebox(self) -> str: line_width = self.style.get_line_width(face_element=False) pen = create_pens(edge_color=self.edge_color, stroke_width=line_width) @@ -271,22 +251,25 @@ def graphicsbox(self, leaves=None, **options) -> str: add_conversion_fn(GraphicsBox) -def graphicselements(self, offset=None): +def graphicselements(self) -> str: result = [] for element in self.elements: format_fn = lookup_method(element, "asy") if format_fn is None: result.append(element.to_asy(offset)) else: - result.append(format_fn(element, offset)) + result.append(format_fn(element)) return "\n".join(result) add_conversion_fn(GraphicsElements) +graphics3delements = graphicselements -def insetbox(self): +add_conversion_fn(Graphics3DElements) + +def insetbox(self) -> str: x, y = self.pos.pos() content = self.content.boxes_to_tex(evaluation=self.graphics.evaluation) pen = create_pens(edge_color=self.color) @@ -303,21 +286,54 @@ def insetbox(self): add_conversion_fn(InsetBox) +def line3dbox(self): + # l = self.style.get_line_width(face_element=False) + pen = create_pens(edge_color=self.edge_color, stroke_width=1) + + return "".join( + "draw({0}, {1});".format( + "--".join("({0},{1},{2})".format(*coords.pos()[0]) for coords in line), + pen, + ) + for line in self.lines + ) + +add_conversion_fn(Line3DBox) -def linebox(self): + +def linebox(self) -> str: line_width = self.style.get_line_width(face_element=False) pen = create_pens(edge_color=self.edge_color, stroke_width=line_width) asy = "" for line in self.lines: path = "--".join(["(%.5g,%5g)" % coords.pos() for coords in line]) asy += "draw(%s, %s);" % (path, pen) + # print("### linebox", asy) return asy add_conversion_fn(LineBox) -def pointbox(self): +def point3dbox(self) -> str: + face_color = self.face_color + + # Tempoary bug fix: default Point color should be black not white + if list(face_color.to_rgba()[:3]) == [1, 1, 1]: + face_color = RGBColor(components=(0, 0, 0, face_color.to_rgba()[3])) + + pen = create_pens(face_color=face_color, is_face_element=False) + + return "".join( + "path3 g={0}--cycle;dot(g, {1});".format( + "--".join("(%.5g,%.5g,%.5g)" % coords.pos()[0] for coords in line), pen + ) + for line in self.lines + ) + +add_conversion_fn(Point3DBox) + +def pointbox(self) -> str: pen = create_pens(face_color=self.face_color, is_face_element=False) asy = "" @@ -325,11 +341,38 @@ def pointbox(self): for coords in line: asy += "dot(%s, %s);" % (coords.pos(), pen) + # print("### pointbox", asy) return asy add_conversion_fn(PointBox) +def polygon3dbox(self) -> str: + l = self.style.get_line_width(face_element=True) + if self.vertex_colors is None: + face_color = self.face_color + else: + face_color = None + pen = create_pens( + edge_color=self.edge_color, + face_color=face_color, + stroke_width=l, + is_face_element=True, + ) + + asy = "" + for line in self.lines: + asy += ( + "path3 g=" + + "--".join(["(%.5g,%.5g,%.5g)" % coords.pos()[0] for coords in line]) + + "--cycle;" + ) + asy += "draw(surface(g), %s);" % (pen) + + # print("### polygon3dbox", asy) + return asy + +add_conversion_fn(Polygon3DBox) def polygonbox(self): line_width = self.style.get_line_width(face_element=True) @@ -371,13 +414,15 @@ def polygonbox(self): "--".join(["(%.5g,%.5g)" % coords.pos() for coords in line]) + "--cycle" ) asy += "filldraw(%s, %s);" % (path, pens) + + # print("### polygonbox", asy) return asy add_conversion_fn(PolygonBox) -def rectanglebox(self): +def rectanglebox(self) -> str: line_width = self.style.get_line_width(face_element=True) x1, y1 = self.p1.pos() x2, y2 = self.p2.pos() @@ -423,3 +468,20 @@ def _roundbox(self): add_conversion_fn(_RoundBox) + +def sphere3dbox(self): + # l = self.style.get_line_width(face_element=True) + + if self.face_color is None: + face_color = (1, 1, 1) + else: + face_color = self.face_color.to_js() + + return "".join( + "draw(surface(sphere({0}, {1})), rgb({2},{3},{4}));".format( + tuple(coord.pos()[0]), self.radius, *face_color[:3] + ) + for coord in self.points + ) + +add_conversion_fn(Sphere3DBox) diff --git a/mathics/formatter/svg.py b/mathics/formatter/svg.py index 7a741efb4b..7b3cc83038 100644 --- a/mathics/formatter/svg.py +++ b/mathics/formatter/svg.py @@ -27,7 +27,6 @@ class _SVGTransform: def __init__(self): - from trepan.api import debug; debug() self.transforms = [] def matrix(self, a, b, c, d, e, f): From 0600cac0a5e2a1479a9bc8ab75cb5bd3403d99fd Mon Sep 17 00:00:00 2001 From: rocky Date: Wed, 19 May 2021 18:13:57 -0400 Subject: [PATCH 6/9] Pull out common asy functions --- mathics/builtin/drawing/graphics3d.py | 48 ++--------- mathics/builtin/graphics.py | 107 ++--------------------- mathics/core/formatter.py | 10 ++- mathics/formatter/asy.py | 118 +++++++------------------- mathics/formatter/asy_fns.py | 105 +++++++++++++++++++++++ 5 files changed, 154 insertions(+), 234 deletions(-) create mode 100644 mathics/formatter/asy_fns.py diff --git a/mathics/builtin/drawing/graphics3d.py b/mathics/builtin/drawing/graphics3d.py index 93a3158c13..a9df10705f 100644 --- a/mathics/builtin/drawing/graphics3d.py +++ b/mathics/builtin/drawing/graphics3d.py @@ -4,9 +4,11 @@ Three-Dimensional Graphics """ +from mathics.version import __version__ # noqa used in loading to check consistency. +import html +import json import numbers -from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.core.expression import ( Expression, @@ -15,25 +17,23 @@ SymbolList, ) from mathics.core.formatter import lookup_method + from mathics.builtin.base import BoxConstructError, Builtin, InstanceableBuiltin from mathics.builtin.graphics import ( Graphics, GraphicsBox, PolygonBox, - create_pens, _Color, LineBox, PointBox, Style, RGBColor, get_class, - asy_number, CoordinatesError, _GraphicsElements, ) -import json -import html +from mathics.formatter.asy_fns import asy_create_pens, asy_number def coords3D(value): @@ -445,7 +445,6 @@ def boxes_to_tex(self, leaves=None, **options): elements._apply_boxscaling(boxscale) - format_fn = lookup_method(elements, "asy") if format_fn is not None: asy = format_fn(elements) @@ -473,11 +472,11 @@ def boxes_to_tex(self, leaves=None, **options): for i, line in enumerate(boundbox_lines): if i in axes_indices: - pen = create_pens( + pen = asy_create_pens( edge_color=RGBColor(components=(0, 0, 0, 1)), stroke_width=1.5 ) else: - pen = create_pens( + pen = asy_create_pens( edge_color=RGBColor(components=(0.4, 0.4, 0.4, 1)), stroke_width=1 ) @@ -490,7 +489,7 @@ def boxes_to_tex(self, leaves=None, **options): # Draw axes ticks ticklength = 0.05 * max([xmax - xmin, ymax - ymin, zmax - zmin]) - pen = create_pens( + pen = asy_create_pens( edge_color=RGBColor(components=(0, 0, 0, 1)), stroke_width=1.2 ) for xi in axes_indices: @@ -825,22 +824,6 @@ def to_json(self): ) return data - def to_asy(self): - face_color = self.face_color - - # Tempoary bug fix: default Point color should be black not white - if list(face_color.to_rgba()[:3]) == [1, 1, 1]: - face_color = RGBColor(components=(0, 0, 0, face_color.to_rgba()[3])) - - pen = create_pens(face_color=face_color, is_face_element=False) - - return "".join( - "path3 g={0}--cycle;dot(g, {1});".format( - "--".join("(%.5g,%.5g,%.5g)" % coords.pos()[0] for coords in line), pen - ) - for line in self.lines - ) - def extent(self): result = [] for line in self.lines: @@ -1115,21 +1098,6 @@ def init(self, graphics, style, item): self.points = [Coords3D(graphics, pos=point) for point in points] self.radius = item.leaves[1].to_python() - def to_asy(self): - # l = self.style.get_line_width(face_element=True) - - if self.face_color is None: - face_color = (1, 1, 1) - else: - face_color = self.face_color.to_js() - - return "".join( - "draw(surface(sphere({0}, {1})), rgb({2},{3},{4}));".format( - tuple(coord.pos()[0]), self.radius, *face_color[:3] - ) - for coord in self.points - ) - def to_json(self): face_color = self.face_color if face_color is not None: diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index c93b56f516..d1911f70c4 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -8,7 +8,6 @@ from math import floor, ceil, log10, sin, cos, pi, sqrt, atan2, degrees, radians, exp import base64 -from itertools import chain from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import ( @@ -32,10 +31,12 @@ system_symbols_dict, from_python, ) -from mathics.core.formatter import lookup_method from mathics.builtin.drawing.colors import convert as convert_color +from mathics.core.formatter import lookup_method from mathics.core.numbers import machine_epsilon +from mathics.formatter.asy_fns import asy_bezier, asy_color, asy_number + GRAPHICS_OPTIONS = { "AspectRatio": "Automatic", @@ -50,104 +51,6 @@ "$OptionSyntax": "Ignore", } -# FIXME: move out into formatter after make_draw_asy is moved -def _asy_bezier(*segments): - # see http://asymptote.sourceforge.net/doc/Bezier-curves.html#Bezier-curves - - while segments and not segments[0][1]: - segments = segments[1:] - - if not segments: - return - - def cubic(p0, p1, p2, p3): - return "..controls(%.5g,%.5g) and (%.5g,%.5g)..(%.5g,%.5g)" % tuple( - list(chain(p1, p2, p3)) - ) - - def quadratric(qp0, qp1, qp2): - # asymptote only supports cubic beziers, so we convert this quadratic - # bezier to a cubic bezier, see http://fontforge.github.io/bezier.html - - # CP0 = QP0 - # CP3 = QP2 - # CP1 = QP0 + 2 / 3 * (QP1 - QP0) - # CP2 = QP2 + 2 / 3 * (QP1 - QP2) - - qp0x, qp0y = qp0 - qp1x, qp1y = qp1 - qp2x, qp2y = qp2 - - t = 2.0 / 3.0 - cp0 = qp0 - cp1 = (qp0x + t * (qp1x - qp0x), qp0y + t * (qp1y - qp0y)) - cp2 = (qp2x + t * (qp1x - qp2x), qp2y + t * (qp1y - qp2y)) - cp3 = qp2 - - return cubic(cp0, cp1, cp2, cp3) - - def linear(p0, p1): - return "--(%.5g,%.5g)" % p1 - - forms = (linear, quadratric, cubic) - - def path(max_degree, p): - max_degree = min(max_degree, len(forms)) - while p: - n = min(max_degree, len(p) - 1) # 1, 2, or 3 - if n < 1: - break - yield forms[n - 1](*p[: n + 1]) - p = p[n:] - - k, p = segments[0] - yield "(%.5g,%.5g)" % p[0] - - connect = [] - for k, p in segments: - for s in path(k, list(chain(connect, p))): - yield s - connect = p[-1:] - - -# FIXME: move out into formatter after make_draw_asy is moved -def asy_number(value): - return "%.5g" % value - -# This is asy specific. Move elsewhere -def _color(self): - rgba = self.to_rgba() - alpha = rgba[3] if len(rgba) > 3 else 1.0 - return ( - r"rgb(%s, %s, %s)" - % (asy_number(rgba[0]), asy_number(rgba[1]), asy_number(rgba[2])), - alpha, - ) - - -def create_pens( - edge_color=None, face_color=None, stroke_width=None, is_face_element=False -): - result = [] - if face_color is not None: - brush, opacity = _color(face_color) - if opacity != 1: - brush += "+opacity(%s)" % asy_number(opacity) - result.append(brush) - elif is_face_element: - result.append("nullpen") - if edge_color is not None: - pen, opacity = _color(edge_color) - if opacity != 1: - pen += "+opacity(%s)" % asy_number(opacity) - if stroke_width is not None: - pen += "+linewidth(%s)" % asy_number(stroke_width) - result.append(pen) - elif is_face_element: - result.append("nullpen") - return ", ".join(result) - - class CoordinatesError(BoxConstructError): pass @@ -1960,7 +1863,7 @@ def draw(points): def make_draw_asy(self, pen): def draw(points): - for path in _asy_bezier((self.spline_degree, points)): + for path in asy_bezier((self.spline_degree, points)): yield "draw(%s, %s);" % (path, pen) return draw @@ -2790,7 +2693,7 @@ def boxes_to_tex(self, leaves=None, **options): ) if self.background_color is not None: - color, opacity = _color(self.background_color) + color, opacity = asy_color(self.background_color) asy_background = "filldraw(%s, %s);" % (asy_box, color) else: asy_background = "" diff --git a/mathics/core/formatter.py b/mathics/core/formatter.py index c39b72f69d..f3ac5699a1 100644 --- a/mathics/core/formatter.py +++ b/mathics/core/formatter.py @@ -5,7 +5,7 @@ format2fn = {} -def lookup_method(self, format: str) -> Callable: +def lookup_method(self, format: str, module_fn_name=None) -> Callable: """ Find a conversion method for `format` in self's class method resolution order. """ @@ -19,7 +19,7 @@ def lookup_method(self, format: str) -> Callable: ) -def add_conversion_fn(cls) -> None: +def add_conversion_fn(cls, module_fn_name=None) -> None: """Add to `format2fn` a mapping from a conversion type and builtin-class to a conversion method. @@ -41,8 +41,10 @@ def add_conversion_fn(cls) -> None: # The last part of the module name is expected to be the conversion routine. conversion_type = module_dict["__name__"].split(".")[-1] - # Derive the conversion function from the passed-in class argument. - module_fn_name = cls.__name__.lower() + # Derive the conversion function from the passed-in class argument, + # unless it is already set. + if module_fn_name is None: + module_fn_name = cls.__name__.lower() # Finally register the mapping: (Builtin-class, conversion name) -> conversion_function. format2fn[(conversion_type, cls)] = module_dict[module_fn_name] diff --git a/mathics/formatter/asy.py b/mathics/formatter/asy.py index 9b443b6f21..3fd6b13ac6 100644 --- a/mathics/formatter/asy.py +++ b/mathics/formatter/asy.py @@ -4,15 +4,10 @@ Format a Mathics object as an Aymptote string """ -from itertools import chain - from mathics.builtin.graphics import ( - _color, _Color, _ArcBox, - ArrowBox, # FIXME move elsewhere - asy_number, # FIXME move elsewhere - create_pens, + ArrowBox, BezierCurveBox, FilledCurveBox, GraphicsBox, @@ -35,6 +30,7 @@ ) from mathics.core.formatter import lookup_method, add_conversion_fn +from mathics.formatter.asy_fns import asy_bezier, asy_color as _color, asy_create_pens, asy_number class _ASYTransform: @@ -72,78 +68,12 @@ def apply(self, asy): return self._template % (" * ".join(self.transforms), asy) -def _asy_bezier(*segments): - # see http://asymptote.sourceforge.net/doc/Bezier-curves.html#Bezier-curves - - while segments and not segments[0][1]: - segments = segments[1:] - - if not segments: - return - - def cubic(p0, p1, p2, p3): - return "..controls(%.5g,%.5g) and (%.5g,%.5g)..(%.5g,%.5g)" % tuple( - list(chain(p1, p2, p3)) - ) - - def quadratric(qp0, qp1, qp2): - # asymptote only supports cubic beziers, so we convert this quadratic - # bezier to a cubic bezier, see http://fontforge.github.io/bezier.html - - # CP0 = QP0 - # CP3 = QP2 - # CP1 = QP0 + 2 / 3 * (QP1 - QP0) - # CP2 = QP2 + 2 / 3 * (QP1 - QP2) - - qp0x, qp0y = qp0 - qp1x, qp1y = qp1 - qp2x, qp2y = qp2 - - t = 2.0 / 3.0 - cp0 = qp0 - cp1 = (qp0x + t * (qp1x - qp0x), qp0y + t * (qp1y - qp0y)) - cp2 = (qp2x + t * (qp1x - qp2x), qp2y + t * (qp1y - qp2y)) - cp3 = qp2 - - return cubic(cp0, cp1, cp2, cp3) - - def linear(p0, p1): - return "--(%.5g,%.5g)" % p1 - - forms = (linear, quadratric, cubic) - - def path(max_degree, p): - max_degree = min(max_degree, len(forms)) - while p: - n = min(max_degree, len(p) - 1) # 1, 2, or 3 - if n < 1: - break - yield forms[n - 1](*p[: n + 1]) - p = p[n:] - - k, p = segments[0] - yield "(%.5g,%.5g)" % p[0] - - connect = [] - for k, p in segments: - for s in path(k, list(chain(connect, p))): - yield s - connect = p[-1:] - - -# def asy_number(value): -# return "%.5g" % value - - -add_conversion_fn(_Color) - - def arrowbox(self) -> str: width = self.style.get_line_width(face_element=False) - pen = create_pens(edge_color=self.edge_color, stroke_width=width) + pen = asy_create_pens(edge_color=self.edge_color, stroke_width=width) polyline = self.curve.make_draw_asy(pen) - arrow_pen = create_pens(face_color=self.edge_color, stroke_width=width) + arrow_pen = asy_create_pens(face_color=self.edge_color, stroke_width=width) def polygon(points): yield "filldraw(" @@ -161,11 +91,11 @@ def polygon(points): def beziercurvebox(self) -> str: line_width = self.style.get_line_width(face_element=False) - pen = create_pens(edge_color=self.edge_color, stroke_width=line_width) + pen = asy_create_pens(edge_color=self.edge_color, stroke_width=line_width) asy = "" for line in self.lines: - for path in _asy_bezier((self.spline_degree, [xy.pos() for xy in line])): + for path in asy_bezier((self.spline_degree, [xy.pos() for xy in line])): if path[:2] == "..": path = "(0.,0.)" + path asy += "draw(%s, %s);" % (path, pen) @@ -181,7 +111,7 @@ def beziercurvebox(self) -> str: def filledcurvebox(self) -> str: line_width = self.style.get_line_width(face_element=False) - pen = create_pens(edge_color=self.edge_color, stroke_width=line_width) + pen = asy_create_pens(edge_color=self.edge_color, stroke_width=line_width) if not pen: pen = "currentpen" @@ -189,7 +119,7 @@ def filledcurvebox(self) -> str: def components(): for component in self.components: transformed = [(k, [xy.pos() for xy in p]) for k, p in component] - yield "fill(%s--cycle, %s);" % ("".join(_asy_bezier(*transformed)), pen) + yield "fill(%s--cycle, %s);" % ("".join(asy_bezier(*transformed)), pen) return "".join(components()) @@ -269,10 +199,11 @@ def graphicselements(self) -> str: add_conversion_fn(Graphics3DElements) + def insetbox(self) -> str: x, y = self.pos.pos() content = self.content.boxes_to_tex(evaluation=self.graphics.evaluation) - pen = create_pens(edge_color=self.color) + pen = asy_create_pens(edge_color=self.color) asy = 'label("$%s$", (%s,%s), (%s,%s), %s);' % ( content, x, @@ -286,9 +217,10 @@ def insetbox(self) -> str: add_conversion_fn(InsetBox) + def line3dbox(self): # l = self.style.get_line_width(face_element=False) - pen = create_pens(edge_color=self.edge_color, stroke_width=1) + pen = asy_create_pens(edge_color=self.edge_color, stroke_width=1) return "".join( "draw({0}, {1});".format( @@ -298,12 +230,13 @@ def line3dbox(self): for line in self.lines ) + add_conversion_fn(Line3DBox) def linebox(self) -> str: line_width = self.style.get_line_width(face_element=False) - pen = create_pens(edge_color=self.edge_color, stroke_width=line_width) + pen = asy_create_pens(edge_color=self.edge_color, stroke_width=line_width) asy = "" for line in self.lines: path = "--".join(["(%.5g,%5g)" % coords.pos() for coords in line]) @@ -322,7 +255,7 @@ def point3dbox(self) -> str: if list(face_color.to_rgba()[:3]) == [1, 1, 1]: face_color = RGBColor(components=(0, 0, 0, face_color.to_rgba()[3])) - pen = create_pens(face_color=face_color, is_face_element=False) + pen = asy_create_pens(face_color=face_color, is_face_element=False) return "".join( "path3 g={0}--cycle;dot(g, {1});".format( @@ -331,10 +264,12 @@ def point3dbox(self) -> str: for line in self.lines ) + add_conversion_fn(Point3DBox) + def pointbox(self) -> str: - pen = create_pens(face_color=self.face_color, is_face_element=False) + pen = asy_create_pens(face_color=self.face_color, is_face_element=False) asy = "" for line in self.lines: @@ -347,13 +282,14 @@ def pointbox(self) -> str: add_conversion_fn(PointBox) + def polygon3dbox(self) -> str: l = self.style.get_line_width(face_element=True) if self.vertex_colors is None: face_color = self.face_color else: face_color = None - pen = create_pens( + pen = asy_create_pens( edge_color=self.edge_color, face_color=face_color, stroke_width=l, @@ -372,15 +308,17 @@ def polygon3dbox(self) -> str: # print("### polygon3dbox", asy) return asy + add_conversion_fn(Polygon3DBox) + def polygonbox(self): line_width = self.style.get_line_width(face_element=True) if self.vertex_colors is None: face_color = self.face_color else: face_color = None - pens = create_pens( + pens = asy_create_pens( edge_color=self.edge_color, face_color=face_color, stroke_width=line_width, @@ -426,7 +364,9 @@ def rectanglebox(self) -> str: line_width = self.style.get_line_width(face_element=True) x1, y1 = self.p1.pos() x2, y2 = self.p2.pos() - pens = create_pens(self.edge_color, self.face_color, line_width, is_face_element=True) + pens = asy_create_pens( + self.edge_color, self.face_color, line_width, is_face_element=True + ) x1, x2, y1, y2 = asy_number(x1), asy_number(x2), asy_number(y1), asy_number(y2) return "filldraw((%s,%s)--(%s,%s)--(%s,%s)--(%s,%s)--cycle, %s);" % ( x1, @@ -450,7 +390,7 @@ def _roundbox(self): rx -= x ry -= y line_width = self.style.get_line_width(face_element=self.face_element) - pen = create_pens( + pen = asy_create_pens( edge_color=self.edge_color, face_color=self.face_color, stroke_width=line_width, @@ -469,7 +409,8 @@ def _roundbox(self): add_conversion_fn(_RoundBox) -def sphere3dbox(self): + +def sphere3dbox(self) -> str: # l = self.style.get_line_width(face_element=True) if self.face_color is None: @@ -484,4 +425,5 @@ def sphere3dbox(self): for coord in self.points ) + add_conversion_fn(Sphere3DBox) diff --git a/mathics/formatter/asy_fns.py b/mathics/formatter/asy_fns.py new file mode 100644 index 0000000000..ca319d0e1e --- /dev/null +++ b/mathics/formatter/asy_fns.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- + +""" Asymptote-related functions""" + +from itertools import chain + +def asy_bezier(*segments): + # see http://asymptote.sourceforge.net/doc/Bezier-curves.html#Bezier-curves + + while segments and not segments[0][1]: + segments = segments[1:] + + if not segments: + return + + def cubic(p0, p1, p2, p3): + return "..controls(%.5g,%.5g) and (%.5g,%.5g)..(%.5g,%.5g)" % tuple( + list(chain(p1, p2, p3)) + ) + + def quadratric(qp0, qp1, qp2): + # asymptote only supports cubic beziers, so we convert this quadratic + # bezier to a cubic bezier, see http://fontforge.github.io/bezier.html + + # CP0 = QP0 + # CP3 = QP2 + # CP1 = QP0 + 2 / 3 * (QP1 - QP0) + # CP2 = QP2 + 2 / 3 * (QP1 - QP2) + + qp0x, qp0y = qp0 + qp1x, qp1y = qp1 + qp2x, qp2y = qp2 + + t = 2.0 / 3.0 + cp0 = qp0 + cp1 = (qp0x + t * (qp1x - qp0x), qp0y + t * (qp1y - qp0y)) + cp2 = (qp2x + t * (qp1x - qp2x), qp2y + t * (qp1y - qp2y)) + cp3 = qp2 + + return cubic(cp0, cp1, cp2, cp3) + + def linear(p0, p1): + return "--(%.5g,%.5g)" % p1 + + forms = (linear, quadratric, cubic) + + def path(max_degree, p): + max_degree = min(max_degree, len(forms)) + while p: + n = min(max_degree, len(p) - 1) # 1, 2, or 3 + if n < 1: + break + yield forms[n - 1](*p[: n + 1]) + p = p[n:] + + k, p = segments[0] + yield "(%.5g,%.5g)" % p[0] + + connect = [] + for k, p in segments: + for s in path(k, list(chain(connect, p))): + yield s + connect = p[-1:] + + +def asy_color(self) -> str: + """Return an asymptote string fragment for object's RGB or RGBA value""" + rgba = self.to_rgba() + alpha = rgba[3] if len(rgba) > 3 else 1.0 + return ( + r"rgb(%s, %s, %s)" + % (asy_number(rgba[0]), asy_number(rgba[1]), asy_number(rgba[2])), + alpha, + ) + + +def asy_create_pens( + edge_color=None, face_color=None, stroke_width=None, is_face_element=False +) -> str: + """ + Return an asymptote string fragment that creates a drawing pen. + """ + result = [] + if face_color is not None: + brush, opacity = asy_color(face_color) + if opacity != 1: + brush += "+opacity(%s)" % asy_number(opacity) + result.append(brush) + elif is_face_element: + result.append("nullpen") + if edge_color is not None: + pen, opacity = asy_color(edge_color) + if opacity != 1: + pen += "+opacity(%s)" % asy_number(opacity) + if stroke_width is not None: + pen += "+linewidth(%s)" % asy_number(stroke_width) + result.append(pen) + elif is_face_element: + result.append("nullpen") + return ", ".join(result) + + +def asy_number(value) -> str: + """Format an asymptote number""" + return "%.5g" % value From 9611d080b32a0effb5f460c733c3944d5b03e73c Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 20 May 2021 19:28:19 -0400 Subject: [PATCH 7/9] More complete SVG format testing.. This is preparation for doing the same for Asymptote. --- test/test_formatter.py | 52 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/test/test_formatter.py b/test/test_formatter.py index 998141a9b0..ed251ce20f 100644 --- a/test/test_formatter.py +++ b/test/test_formatter.py @@ -1,5 +1,5 @@ import re -from mathics.core.expression import Symbol, Integer0, Expression +from mathics.core.expression import Symbol, Integer0, Integer1, Expression from mathics.core.evaluation import Evaluation from mathics.session import MathicsSession from mathics.builtin.inout import MakeBoxes @@ -63,6 +63,7 @@ def test_svg_circle(): assert matches assert matches.group(1) == matches.group(2) == matches.group(3) + def test_svg_point(): expression = Expression( GraphicsSymbol, @@ -75,12 +76,53 @@ def test_svg_point(): # Circles are implemented as ellipses with equal major and minor axes. # Check for that. print(inner_svg) - matches = re.match( - r'^', inner_svg - ) + matches = re.match(r'^', inner_svg) assert matches assert matches.group(1) == matches.group(2) +def test_svg_arrowbox(): + expression = Expression( + GraphicsSymbol, + Expression( + "Arrow", + Expression( + ListSymbol, + Expression(ListSymbol, Integer0, Integer0), + Expression(ListSymbol, Integer1, Integer1), + ), + ), + ) + svg = get_svg(expression) + inner_svg = extract_svg_body(svg) + + matches = re.match(r'^<', inner_svg) + # TODO: Could pick endpoint of this line and match with beginnign of arrow polygon below + assert matches + arrow_polygon = inner_svg[len(matches.group(0)) - 1 :] + matches = re.match(r'^', arrow_polygon) + assert matches + + +def test_svg_bezier_curve(): + + expression = Expression( + GraphicsSymbol, + Expression( + "BezierCurve", + Expression( + ListSymbol, + Expression(ListSymbol, Integer0, Integer0), + Expression(ListSymbol, Integer1, Integer1), + ), + ), + ) + svg = get_svg(expression) + inner_svg = extract_svg_body(svg) + + matches = re.match(r'^', inner_svg) + assert matches + + if __name__ == "__main__": - test_svg_point() + test_svg_bezier_curve() From 2e5e52a287c0d92551c0797b12c94b80086224c6 Mon Sep 17 00:00:00 2001 From: rocky Date: Sat, 22 May 2021 13:13:30 -0400 Subject: [PATCH 8/9] Start asy formatter unit test --- mathics/formatter/asy.py | 54 -------- test/test_formatter/test_asy.py | 116 ++++++++++++++++++ .../test_svg.py} | 0 3 files changed, 116 insertions(+), 54 deletions(-) create mode 100644 test/test_formatter/test_asy.py rename test/{test_formatter.py => test_formatter/test_svg.py} (100%) diff --git a/mathics/formatter/asy.py b/mathics/formatter/asy.py index 3fd6b13ac6..35599eb7bb 100644 --- a/mathics/formatter/asy.py +++ b/mathics/formatter/asy.py @@ -126,60 +126,6 @@ def components(): add_conversion_fn(FilledCurveBox) -# FIXME figure out how we can add this. -def graphicsbox(self, leaves=None, **options) -> str: - if not leaves: - leaves = self._leaves - - data = options.get("data", None) - if data: - elements, xmin, xmax, ymin, ymax, w, h, width, height = data - else: - elements, calc_dimensions = self._prepare_elements(leaves, options, neg_y=True) - xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() - - elements.view_width = w - - format_fn = lookup_method(elements, "svg") - if format_fn is not None: - svg_body = format_fn(elements, offset=options.get("offset", None)) - else: - svg_body = elements.to_svg(offset=options.get("offset", None)) - - if self.background_color is not None: - # Wrap svg_elements in a rectangle - svg_body = '%s' % ( - xmin, - ymin, - w, - h, - self.background_color.to_css()[0], - svg_body, - ) - - xmin -= 1 - ymin -= 1 - w += 2 - h += 2 - - if options.get("noheader", False): - return svg_body - svg_main = """ - - %s - - """ % ( - " ".join("%f" % t for t in (xmin, ymin, w, h)), - svg_body, - ) - return svg_main # , width, height - - -add_conversion_fn(GraphicsBox) - def graphicselements(self) -> str: result = [] diff --git a/test/test_formatter/test_asy.py b/test/test_formatter/test_asy.py new file mode 100644 index 0000000000..a996de1e7c --- /dev/null +++ b/test/test_formatter/test_asy.py @@ -0,0 +1,116 @@ +import re +from mathics.core.expression import Symbol, Integer0, Integer1, Expression +from mathics.core.evaluation import Evaluation +from mathics.session import MathicsSession +from mathics.builtin.inout import MakeBoxes +from mathics.core.formatter import lookup_method + +session = MathicsSession(add_builtin=True, catch_interrupt=False) +evaluation = Evaluation(session.definitions) + +GraphicsSymbol = Symbol("Graphics") +ListSymbol = Symbol("List") + +asy_wrapper_pat = r"""^\s* +\s*\\begin{asy} +\s*usepackage\("amsmath"\); +\s*size\(.+\); +\s* +""" + + +def extract_asy_body(asy): + matches = re.match(asy_wrapper_pat, asy) + body = asy[len(matches.group(0)) :] + assert matches + print(body) + return body + + +def get_asy(expression): + options = {} + boxes = MakeBoxes(expression).evaluate(evaluation) + return boxes.boxes_to_tex() + + +def test_asy_circle(): + expression = Expression( + GraphicsSymbol, + Expression("Circle", Expression(ListSymbol, Integer0, Integer0)), + ) + + asy = get_asy(expression) + inner_asy = extract_asy_body(asy) + + # Circles are implemented as ellipses with equal major and minor axes. + # Check for that. + matches = re.match( + r'^draw\(ellipse\(\((.+),\s*(.+)\),(.*),(.*)\), .*', inner_asy + ) + + assert matches + # Check that center point is centered and + # major and minor axes are the same + assert matches.group(1) == matches.group(2) + assert matches.group(3) == matches.group(4) + + +def test_asy_point(): + expression = Expression( + GraphicsSymbol, + Expression("Point", Expression(ListSymbol, Integer0, Integer0)), + ) + + asy = get_asy(expression) + inner_asy = extract_asy_body(asy) + + matches = re.match(r'^dot\(\((.+), (.+)\), .+\);.*', inner_asy) + assert matches + # Since the x,y pont is the same, we'll check that whatever this + # coordinate mapped to, it is the same. + assert matches.group(1) == matches.group(2) + + +def test_asy_arrowbox(): + expression = Expression( + GraphicsSymbol, + Expression( + "Arrow", + Expression( + ListSymbol, + Expression(ListSymbol, Integer0, Integer0), + Expression(ListSymbol, Integer1, Integer1), + ), + ), + ) + asy = get_asy(expression) + inner_asy = extract_asy_body(asy) + + matches = re.match(r'^draw\(.*\)', inner_asy) + # TODO: Match line and arrowbox + assert matches + + +def test_asy_bezier_curve(): + + expression = Expression( + GraphicsSymbol, + Expression( + "BezierCurve", + Expression( + ListSymbol, + Expression(ListSymbol, Integer0, Integer0), + Expression(ListSymbol, Integer1, Integer1), + ), + ), + ) + asy = get_asy(expression) + inner_asy = extract_asy_body(asy) + + matches = re.match(r'^draw\(.*\)', inner_asy) + # TODO: Match line and arrowbox + assert matches + + +if __name__ == "__main__": + test_asy_bezier_curve() diff --git a/test/test_formatter.py b/test/test_formatter/test_svg.py similarity index 100% rename from test/test_formatter.py rename to test/test_formatter/test_svg.py From b4c5e03471586f44c430a02d4ff2942aab083c80 Mon Sep 17 00:00:00 2001 From: rocky Date: Sat, 22 May 2021 13:26:55 -0400 Subject: [PATCH 9/9] More flexible naming in format conversion routines --- mathics/core/formatter.py | 3 +++ mathics/formatter/asy.py | 19 +++++++++---------- mathics/formatter/svg.py | 26 +++++++++++++------------- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/mathics/core/formatter.py b/mathics/core/formatter.py index f3ac5699a1..b9514b52db 100644 --- a/mathics/core/formatter.py +++ b/mathics/core/formatter.py @@ -45,6 +45,9 @@ def add_conversion_fn(cls, module_fn_name=None) -> None: # unless it is already set. if module_fn_name is None: module_fn_name = cls.__name__.lower() + elif hasattr(module_fn_name, "__name__"): + module_fn_name = module_fn_name.__name__ + # Finally register the mapping: (Builtin-class, conversion name) -> conversion_function. format2fn[(conversion_type, cls)] = module_dict[module_fn_name] diff --git a/mathics/formatter/asy.py b/mathics/formatter/asy.py index 35599eb7bb..a2cffe1992 100644 --- a/mathics/formatter/asy.py +++ b/mathics/formatter/asy.py @@ -68,7 +68,7 @@ def apply(self, asy): return self._template % (" * ".join(self.transforms), asy) -def arrowbox(self) -> str: +def arrow_box(self) -> str: width = self.style.get_line_width(face_element=False) pen = asy_create_pens(edge_color=self.edge_color, stroke_width=width) polyline = self.curve.make_draw_asy(pen) @@ -86,10 +86,10 @@ def polygon(points): return "".join(self._draw(polyline, default_arrow, custom_arrow, extent)) -add_conversion_fn(ArrowBox) +add_conversion_fn(ArrowBox, arrow_box) -def beziercurvebox(self) -> str: +def bezier_curve_box(self) -> str: line_width = self.style.get_line_width(face_element=False) pen = asy_create_pens(edge_color=self.edge_color, stroke_width=line_width) @@ -105,11 +105,10 @@ def beziercurvebox(self) -> str: x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params() +add_conversion_fn(BezierCurveBox, bezier_curve_box) -add_conversion_fn(BezierCurveBox) - -def filledcurvebox(self) -> str: +def filled_curve_box(self) -> str: line_width = self.style.get_line_width(face_element=False) pen = asy_create_pens(edge_color=self.edge_color, stroke_width=line_width) @@ -124,10 +123,10 @@ def components(): return "".join(components()) -add_conversion_fn(FilledCurveBox) +add_conversion_fn(FilledCurveBox, filled_curve_box) -def graphicselements(self) -> str: +def graphics_elements(self) -> str: result = [] for element in self.elements: format_fn = lookup_method(element, "asy") @@ -139,8 +138,8 @@ def graphicselements(self) -> str: return "\n".join(result) -add_conversion_fn(GraphicsElements) -graphics3delements = graphicselements +add_conversion_fn(GraphicsElements, graphics_elements) +graphics3delements = graphics_elements add_conversion_fn(Graphics3DElements) diff --git a/mathics/formatter/svg.py b/mathics/formatter/svg.py index 7b3cc83038..835a0e234d 100644 --- a/mathics/formatter/svg.py +++ b/mathics/formatter/svg.py @@ -76,7 +76,7 @@ def create_css( return "; ".join(css) -def arrowbox(self, offset=None): +def arrow_box(self, offset=None): width = self.style.get_line_width(face_element=False) style = create_css(edge_color=self.edge_color, stroke_width=width) polyline = self.curve.make_draw_svg(style) @@ -94,7 +94,7 @@ def polygon(points): return "".join(self._draw(polyline, default_arrow, custom_arrow, extent)) -add_conversion_fn(ArrowBox) +add_conversion_fn(ArrowBox, arrow_box) def beziercurvebox(self, offset=None): @@ -112,7 +112,7 @@ def beziercurvebox(self, offset=None): add_conversion_fn(BezierCurveBox) -def filledcurvebox(self, offset=None): +def filled_curve_box(self, offset=None): line_width = self.style.get_line_width(face_element=False) style = create_css( edge_color=self.edge_color, face_color=self.face_color, stroke_width=line_width @@ -130,9 +130,9 @@ def components(): ) -add_conversion_fn(FilledCurveBox) +add_conversion_fn(FilledCurveBox, filled_curve_box) -def graphicsbox(self, leaves=None, **options) -> str: +def graphics_box(self, leaves=None, **options) -> str: if not leaves: leaves = self._leaves @@ -185,10 +185,10 @@ def graphicsbox(self, leaves=None, **options) -> str: return svg_main # , width, height -add_conversion_fn(GraphicsBox) +add_conversion_fn(GraphicsBox, graphics_box) -def graphicselements(self, offset=None): +def graphics_elements(self, offset=None): result = [] for element in self.elements: format_fn = lookup_method(element, "svg") @@ -200,13 +200,13 @@ def graphicselements(self, offset=None): return "\n".join(result) -add_conversion_fn(GraphicsElements) -graphics3delements = graphicselements +add_conversion_fn(GraphicsElements, graphics_elements) +graphics3delements = graphics_elements add_conversion_fn(Graphics3DElements) -def insetbox(self, offset=None): +def inset_box(self, offset=None): x, y = self.pos.pos() if offset: x = x + offset[0] @@ -237,10 +237,10 @@ def insetbox(self, offset=None): return svg -add_conversion_fn(InsetBox) +add_conversion_fn(InsetBox, inset_box) -def linebox(self, offset=None): +def line_box(self, offset=None): line_width = self.style.get_line_width(face_element=False) style = create_css(edge_color=self.edge_color, stroke_width=line_width) svg = "" @@ -253,7 +253,7 @@ def linebox(self, offset=None): return svg -add_conversion_fn(LineBox) +add_conversion_fn(LineBox, line_box) def pointbox(self, offset=None):