diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py
index f6fa3165d8..cffbb3a347 100644
--- a/mathics/builtin/graphics.py
+++ b/mathics/builtin/graphics.py
@@ -8,9 +8,11 @@
from math import floor, ceil, log10, sin, cos, pi, sqrt, atan2, degrees, radians, exp
+import re
import json
import base64
from itertools import chain
+from sympy.matrices import Matrix
from mathics.builtin.base import (
Builtin,
@@ -19,6 +21,7 @@
BoxConstructError,
)
from mathics.builtin.options import options_to_rules
+from mathics.layout.client import WebEngineUnavailable
from mathics.core.expression import (
Expression,
Integer,
@@ -45,6 +48,11 @@
"ImageSize": "Automatic",
"Background": "Automatic",
"$OptionSyntax": "Ignore",
+ # Mathics specific; used internally to enable stuff like
+ # Plot[x + 1e-20 * x, {x, 0, 1}] that without precomputing transformations inside Mathics
+ # will hit SVGs numerical accuracy abilities in some browsers as strokes with width < 1e-6
+ # will get rounded to 0 and thus won't get scale transformed in SVG and vanish.
+ "Transformation": "Automatic",
}
@@ -69,6 +77,21 @@ def get_class(name):
# like return globals().get(name)
+
+# def expr_to_coords(value):
+# if not value.has_form('List', 2):
+# raise CoordinatesError
+# x, y = value.leaves[0].to_mpmath(), value.leaves[1].to_mpmath()
+# if x is None or y is None:
+# raise CoordinatesError
+# return x, y
+
+
+# def add_coords(a, b):
+# x1, y1 = a
+# x2, y2 = b
+# return x1 + x2, y1 + y2
+
def coords(value):
if value.has_form("List", 2):
x, y = value.leaves[0].round_to_float(), value.leaves[1].round_to_float()
@@ -95,7 +118,7 @@ def __init__(self, graphics, expr=None, pos=None, d=None):
def pos(self):
p = self.graphics.translate(self.p)
- p = (cut(p[0]), cut(p[1]))
+ p = (cut_global(p[0]), cut_global(p[1]))
if self.d is not None:
d = self.graphics.translate_absolute(self.d)
return (p[0] + d[0], p[1] + d[1])
@@ -106,7 +129,16 @@ def add(self, x, y):
return Coords(self.graphics, pos=p, d=self.d)
-def cut(value):
+
+def axis_coords(graphics, pos, d=None):
+ p = graphics.translate(pos)
+ if d is not None:
+ d = graphics.translate_absolute_in_pixels(d)
+ return p[0] + d[0], p[1] + d[1]
+ else:
+ return p
+
+def cut_global(value):
"Cut values in graphics primitives (not displayed otherwise in SVG)"
border = 10 ** 8
if value < -border:
@@ -115,7 +147,6 @@ def cut(value):
value = border
return value
-
def create_css(edge_color=None, face_color=None, stroke_width=None, font_color=None):
css = []
if edge_color is not None:
@@ -261,6 +292,7 @@ def _cie2000_distance(lab1, lab2):
)
+
def _CMC_distance(lab1, lab2, l, c):
# reference https://en.wikipedia.org/wiki/Color_difference#CMC_l:c_.281984.29
L1, L2 = lab1[0], lab2[0]
@@ -289,30 +321,22 @@ def _CMC_distance(lab1, lab2, l, c):
return sqrt((dL / (l * SL)) ** 2 + (dC / (c * SC)) ** 2 + dH2 / SH ** 2)
+
def _extract_graphics(graphics, format, evaluation):
graphics_box = Expression("MakeBoxes", graphics).evaluate(evaluation)
builtin = GraphicsBox(expression=False)
+
elements, calc_dimensions = builtin._prepare_elements(
graphics_box.leaves, {"evaluation": evaluation}, neg_y=True
)
xmin, xmax, ymin, ymax, _, _, _, _ = calc_dimensions()
- # xmin, xmax have always been moved to 0 here. the untransformed
- # and unscaled bounds are found in elements.xmin, elements.ymin,
- # elements.extent_width, elements.extent_height.
-
- # now compute the position of origin (0, 0) in the transformed
- # coordinate space.
-
- ex = elements.extent_width
- ey = elements.extent_height
-
- sx = (xmax - xmin) / ex
- sy = (ymax - ymin) / ey
-
- ox = -elements.xmin * sx + xmin
- oy = -elements.ymin * sy + ymin
+ if not isinstance(elements.elements[0], GeometricTransformationBox):
+ raise ValueError('expected GeometricTransformationBox')
+ # mmatera: in master, contents = elements.elements. Not sure what is de best
+ contents = elements.elements[0].contents
+
# generate code for svg or asy.
if format == "asy":
@@ -322,9 +346,127 @@ def _extract_graphics(graphics, format, evaluation):
else:
raise NotImplementedError
- return xmin, xmax, ymin, ymax, ox, oy, ex, ey, code
+ return code
+
+# This function and class were defined by poke1024.
+def _to_float(x):
+ if isinstance(x, Integer):
+ return x.get_int_value()
+ else:
+ y = x.round_to_float()
+ if y is None:
+ raise BoxConstructError
+ return y
+
+
+class _Transform:
+ def __init__(self, f):
+ if not isinstance(f, Expression):
+ self.matrix = f
+ return
+
+ if f.get_head_name() != 'System`TransformationFunction':
+ raise BoxConstructError
+
+ if len(f.leaves) != 1 or f.leaves[0].get_head_name() != 'System`List':
+ raise BoxConstructError
+
+ rows = f.leaves[0].leaves
+ if len(rows) != 3:
+ raise BoxConstructError
+ if any(row.get_head_name() != 'System`List' for row in rows):
+ raise BoxConstructError
+ if any(len(row.leaves) != 3 for row in rows):
+ raise BoxConstructError
+
+ self.matrix = [[_to_float(x) for x in row.leaves] for row in rows]
+
+ def combine(self, transform0):
+ if isinstance(transform0, _Transform):
+ return self.multiply(transform0)
+ else:
+ t = self
+
+ def combined(*p, w=1):
+ return transform0(*t(*p, w=w), w=w)
+ return combined
+
+ def inverse(self):
+ return _Transform(Matrix(self.matrix).inv().tolist())
+
+ def multiply(self, other):
+ a = self.matrix
+ b = other.matrix
+ return _Transform([[sum(a[i][k] * b[k][j] for k in range(3)) for j in range(3)] for i in range(3)])
+
+ def __call__(self, *p, w=1):
+ m = self.matrix
+ m11 = m[0][0]
+ m12 = m[0][1]
+ m13 = m[0][2]
+ m21 = m[1][0]
+ m22 = m[1][1]
+ m23 = m[1][2]
+
+ if w == 1:
+ for x, y in p:
+ yield m11 * x + m12 * y + m13, m21 * x + m22 * y + m23
+ elif w == 0:
+ for x, y in p:
+ yield m11 * x + m12 * y, m21 * x + m22 * y
+ else:
+ raise NotImplementedError("w not in (0, 1)")
+
+ def to_svg(self, svg):
+ m = self.matrix
+
+ a = m[0][0]
+ b = m[1][0]
+ c = m[0][1]
+ d = m[1][1]
+ e = m[0][2]
+ f = m[1][2]
+
+ if m[2][0] != 0. or m[2][1] != 0. or m[2][2] != 1.:
+ raise BoxConstructError
+
+ # a c e
+ # b d f
+ # 0 0 1
+
+ t = 'matrix(%f, %f, %f, %f, %f, %f)' % (a, b, c, d, e, f)
+ return '%s' % (t, svg)
+
+ def to_asy(self, asy):
+ m = self.matrix
+
+ a = m[0][0]
+ b = m[1][0]
+ c = m[0][1]
+ d = m[1][1]
+ e = m[0][2]
+ f = m[1][2]
+
+ if m[2][0] != 0. or m[2][1] != 0. or m[2][2] != 1.:
+ raise BoxConstructError
+
+ # a c e
+ # b d f
+ # 0 0 1
+ # see http://asymptote.sourceforge.net/doc/Transforms.html#Transforms
+ t = ','.join(map(asy_number, (e, f, a, c, b, d)))
+
+ return ''.join(("add((", t, ")*(new picture(){",
+ "picture s=currentpicture,t=new picture;currentpicture=t;", asy,
+ "currentpicture=s;return t;})());"))
+
+
+def _no_transform(*p, w=None):
+ return p
+
+# This is what we have now in master
class _SVGTransform:
def __init__(self):
self.transforms = []
@@ -348,6 +490,7 @@ def apply(self, svg):
return '%s' % (" ".join(self.transforms), svg)
+
class _ASYTransform:
_template = """
add(%s * (new picture() {
@@ -360,10 +503,19 @@ class _ASYTransform:
})());
"""
- def __init__(self):
- self.transforms = []
+ def to_svg(self, svg):
+ m = self.matrix
+
+ a = m[0][0]
+ b = m[1][0]
+ c = m[0][1]
+ d = m[1][1]
+ e = m[0][2]
+ f = m[1][2]
+
+ if m[2][0] != 0. or m[2][1] != 0. or m[2][2] != 1.:
+ raise BoxConstructError
- def matrix(self, a, b, c, d, e, f):
# a c e
# b d f
# 0 0 1
@@ -458,9 +610,13 @@ class Graphics(Builtin):
. \begin{asy}
. usepackage("amsmath");
. size(5.8556cm, 5.8333cm);
- . draw(ellipse((175,175),175,175), rgb(0, 0, 0)+linewidth(0.66667));
+ . add((175,175,175,0,0,175)*(new picture(){picture s=currentpicture,t=new picture;currentpicture=t;draw(ellipse((0,0),1,1), rgb(0, 0, 0)+linewidth(0.0038095));currentpicture=s;return t;})());
. clip(box((-0.33333,0.33333), (350.33,349.67)));
. \end{asy}
+
+ Invalid graphics directives yield invalid box structures:
+ >> Graphics[Circle[{a, b}]]
+ : GraphicsBox[CircleBox[List[a, b]], Rule[$OptionSyntax, Ignore], Rule[AspectRatio, Automatic], Rule[Axes, False], Rule[AxesStyle, List[]], Rule[Background, Automatic], Rule[ImageSize, Automatic], Rule[LabelStyle, List[]], Rule[PlotRange, Automatic], Rule[PlotRangePadding, Automatic], Rule[TicksStyle, List[]], Rule[Transformation, Automatic]] is not a valid box structure.
"""
options = GRAPHICS_OPTIONS
@@ -480,6 +636,8 @@ def convert(content):
return Expression(
"StyleBox", *[convert(item) for item in content.leaves]
)
+ elif head == "System`GeometricTransformation" and len(content.leaves) == 2:
+ return Expression("GeometricTransformationBox", convert(content.leaves[0]), content.leaves[1])
if head in element_heads:
if head == "System`Text":
@@ -562,8 +720,8 @@ def init(self, item=None, components=None):
# become RGBColor[0, 0, 0, 1]. does not seem the right thing
# to do in this general context. poke1024
- if len(components) < 3:
- components.extend(self.default_components[len(components) :])
+ # if len(components) < 3:
+ # components.extend(self.default_components[len(components) :])
self.components = components
else:
@@ -1082,7 +1240,7 @@ class PointSize(_Size):
"""
def get_size(self):
- return self.graphics.view_width * self.value
+ return self.graphics.extent_width * self.value
class FontColor(Builtin):
@@ -1096,6 +1254,54 @@ class FontColor(Builtin):
pass
+class FontSize(_GraphicsElement):
+ """
+
+ - 'FontSize[$s$]'
+
- sets the font size to $s$ printer's points.
+
+ """
+
+ def init(self, graphics, item=None, value=None):
+ super(FontSize, self).init(graphics, item)
+
+ self.scaled = False
+ if item is not None and len(item.leaves) == 1:
+ if item.leaves[0].get_head_name() == 'System`Scaled':
+ scaled = item.leaves[0]
+ if len(scaled.leaves) == 1:
+ self.scaled = True
+ self.value = scaled.leaves[0].round_to_float()
+
+ if self.scaled:
+ pass
+ elif item is not None:
+ self.value = item.leaves[0].round_to_float()
+ elif value is not None:
+ self.value = value
+ else:
+ raise BoxConstructError
+
+ if self.value < 0:
+ raise BoxConstructError
+
+ def get_size(self):
+ if self.scaled:
+ if self.graphics.extent_width is None:
+ return 1.
+ else:
+ return self.graphics.extent_width * self.value
+ else:
+ if self.graphics.extent_width is None or self.graphics.pixel_width is None:
+ return 1.
+ else:
+ return (96. / 72.) * (self.value * self.graphics.extent_width / self.graphics.pixel_width)
+
+
+class Scaled(Builtin):
+ pass
+
+
class Offset(Builtin):
pass
@@ -1202,27 +1408,43 @@ def init(self, graphics, style, item):
super(RectangleBox, self).init(graphics, item, style)
if len(item.leaves) not in (1, 2):
raise BoxConstructError
- self.edge_color, self.face_color = style.get_style(_Color, face_element=True)
+ self.edge_color, self.face_color = style.get_style(
+ _Color, face_element=True)
+
self.p1 = Coords(graphics, item.leaves[0])
+ # Poke1024 used tuples for coordinates. Now we are using a specific class
if len(item.leaves) == 1:
- self.p2 = self.p1.add(1, 1)
+ self.p2 = self.p1
+ self.p2.add(1,1)
elif len(item.leaves) == 2:
self.p2 = Coords(graphics, item.leaves[1])
def extent(self):
l = self.style.get_line_width(face_element=True) / 2
result = []
+
+# This was what poke1024 did instead of the next loop:
+# tx1, ty1 = self.p1
+# tx2, ty2 = self.p2
+# x1 = min(tx1, tx2) - l
+# x2 = max(tx1, tx2) + l
+# y1 = min(ty1, ty2) - l
+# y2 = max(ty1, ty2) + l
+#
+# result.extend([(x1, y1), (x1, y2), (x2, y1), (x2, y2)])
+
for p in [self.p1, self.p2]:
- x, y = p.pos()
+ x, y = p.p #pos()
result.extend(
[(x - l, y - l), (x - l, y + l), (x + l, y - l), (x + l, y + l)]
)
+
return result
- def to_svg(self):
+ def to_svg(self, transform):
l = self.style.get_line_width(face_element=True)
- x1, y1 = self.p1.pos()
- x2, y2 = self.p2.pos()
+ x1, y1 = self.p1.p # pos()
+ x2, y2 = self.p2.p # pos()
xmin = min(x1, x2)
ymin = min(y1, y2)
w = max(x1, x2) - xmin
@@ -1236,10 +1458,10 @@ def to_svg(self):
style,
)
- def to_asy(self):
+ def to_asy(self, transform):
l = self.style.get_line_width(face_element=True)
- x1, y1 = self.p1.pos()
- x2, y2 = self.p2.pos()
+ x1, y1 = self.p1.p #.pos()
+ x2, y2 = self.p2.p #.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);" % (
@@ -1279,19 +1501,20 @@ def init(self, graphics, style, item):
def extent(self):
l = self.style.get_line_width(face_element=self.face_element) / 2
- x, y = self.c.pos()
- rx, ry = self.r.pos()
+ x, y = self.c.p #pos()
+ rx, ry = self.r.p #pos()
rx -= x
ry = y - ry
rx += l
ry += l
return [(x - rx, y - ry), (x - rx, y + ry), (x + rx, y - ry), (x + rx, y + ry)]
- def to_svg(self):
- x, y = self.c.pos()
- rx, ry = self.r.pos()
+ def to_svg(self, transform):
+ c, r = transform(self.c.p, self.r.p)
+ x, y = c
+ rx, ry = r
rx -= x
- ry = y - ry
+ ry = abs(y - ry)
l = self.style.get_line_width(face_element=self.face_element)
style = create_css(self.edge_color, self.face_color, stroke_width=l)
return '' % (
@@ -1302,11 +1525,12 @@ def to_svg(self):
style,
)
- def to_asy(self):
- x, y = self.c.pos()
- rx, ry = self.r.pos()
+ def to_asy(self, transform):
+ c, r = transform(self.c, self.r)
+ x, y = c.p
+ rx, ry = r.p
rx -= x
- ry -= y
+ ry = abs(ry - y)
l = self.style.get_line_width(face_element=self.face_element)
pen = create_pens(
edge_color=self.edge_color,
@@ -1352,9 +1576,10 @@ def init(self, graphics, style, item):
self.arc = None
super(_ArcBox, self).init(graphics, style, item)
- def _arc_params(self):
- x, y = self.c.pos()
- rx, ry = self.r.pos()
+ def _arc_params(self, transform):
+ c, r = transform(self.c, self.r)
+ x, y = c
+ rx, ry = r
rx -= x
ry -= y
@@ -1374,11 +1599,11 @@ def _arc_params(self):
return x, y, abs(rx), abs(ry), sx, sy, ex, ey, large_arc
- def to_svg(self):
+ def to_svg(self, transform):
if self.arc is None:
- return super(_ArcBox, self).to_svg()
+ return super(_ArcBox, self).to_svg(transform)
- x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params()
+ x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params(transform)
def path(closed):
if closed:
@@ -1396,11 +1621,11 @@ def path(closed):
style = create_css(self.edge_color, self.face_color, stroke_width=l)
return '' % (" ".join(path(self.face_element)), style)
- def to_asy(self):
+ def to_asy(self, transform):
if self.arc is None:
- return super(_ArcBox, self).to_asy()
+ return super(_ArcBox, self).to_asy(transform)
- x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params()
+ x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params(transform)
def path(closed):
if closed:
@@ -1454,9 +1679,18 @@ def do_init(self, graphics, points):
lines.append(leaf.leaves)
else:
raise BoxConstructError
- self.lines = [
- [graphics.coords(graphics, point) for point in line] for line in lines
- ]
+
+ make_coords = graphics.make_coords # for Graphics and Graphics3D support
+ # I am not sure about why the next line is needed for the next one,
+ # but it seems to work....
+ self.lines = []
+ lines = [make_coords(graphics, line) for line in lines]
+ self.lines = lines
+
+# self.lines = [
+# [graphics.coords(graphics, point) for point in line] for line in lines
+# ]
+
def extent(self):
l = self.style.get_line_width(face_element=False)
@@ -1507,33 +1741,39 @@ def init(self, graphics, style, item=None):
else:
raise BoxConstructError
- def to_svg(self):
+ def to_svg(self, transform):
point_size, _ = self.style.get_style(PointSize, face_element=False)
if point_size is None:
point_size = PointSize(self.graphics, value=0.005)
size = point_size.get_size()
-
style = create_css(
edge_color=self.edge_color, stroke_width=0, face_color=self.face_color
)
svg = ""
+ graphics = self.graphics
+ size_x = size
+ size_y = size_x * (graphics.extent_height / graphics.extent_width) * (graphics.pixel_width / graphics.pixel_height)
+
for line in self.lines:
- for coords in line:
- svg += '' % (
- coords.pos()[0],
- coords.pos()[1],
- size,
- style,
- )
+ for x, y in transform(*line):
+ svg += '' % (
+ x, y, size_x, size_y, style)
+# for line in self.lines:
+# for coords in line:
+# svg += '' % (
+# coords.pos()[0],
+# coords.pos()[1],
+# size,
+# style,
+# )
return svg
- def to_asy(self):
+ def to_asy(self, transform):
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)
+ for x, y in transform(*line):
+ asy += 'dot(%s, %s);' % ((x, y), pen)
return asy
@@ -1571,24 +1811,35 @@ def init(self, graphics, style, item=None, lines=None):
else:
raise BoxConstructError
- def to_svg(self):
+ def to_svg(self, transform):
l = self.style.get_line_width(face_element=False)
+ l = list(transform((l, l), w=0))[0][0]
style = create_css(edge_color=self.edge_color, stroke_width=l)
svg = ""
+ graphics = self.graphics
for line in self.lines:
- svg += '' % (
- " ".join(["%f,%f" % coords.pos() for coords in line]),
- style,
- )
+ tc = transform(*[r.p for r in line])
+ path = ' '.join(['%f,%f' % c for c in tc])
+ svg += '' % (path, style)
+
+ #for line in self.lines:
+ # svg += '' % (
+ # " ".join(["%f,%f" % coords.pos() for coords in line]),
+ # style,
+ # )
return svg
- def to_asy(self):
+ def to_asy(self, transform):
l = self.style.get_line_width(face_element=False)
+ l = list(transform((l, l), w=0))[0][0]
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)
+ path = '--'.join(['(%.5g,%5g)' % c for c in transform(*line)])
+ asy += 'draw(%s, %s);' % (path, pen)
+# for line in self.lines:
+# path = "--".join(["(%.5g,%5g)" % coords.pos() for coords in line])
+# asy += "draw(%s, %s);" % (path, pen)
return asy
@@ -1727,26 +1978,30 @@ def init(self, graphics, style, item, options):
raise BoxConstructError
self.spline_degree = spline_degree.get_int_value()
- def to_svg(self):
+ def to_svg(self, transform):
l = self.style.get_line_width(face_element=False)
style = create_css(edge_color=self.edge_color, stroke_width=l)
svg = ""
for line in self.lines:
- s = " ".join(_svg_bezier((self.spline_degree, [xy.pos() for xy in line])))
+ s = " ".join(_svg_bezier((self.spline_degree, transform(*line))))
+ # s = " ".join(_svg_bezier((self.spline_degree, [xy.pos() for xy in line])))
svg += '' % (s, style)
return svg
- def to_asy(self):
+ def to_asy(self, transform):
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)
+ for path in _asy_bezier((self.spline_degree, transform(*line))):
+ asy += 'draw(%s, %s);' % (path, pen)
+# 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
@@ -1796,16 +2051,13 @@ def parse_component(segments):
else:
raise BoxConstructError
- coords = []
-
+ c = []
for part in parts:
if part.get_head_name() != "System`List":
raise BoxConstructError
- coords.extend(
- [graphics.coords(graphics, xy) for xy in part.leaves]
- )
+ c.extend([Coords(graphics, xy) for xy in part.leaves])
- yield k, coords
+ yield k, c
if all(x.get_head_name() == "System`List" for x in leaves):
self.components = [list(parse_component(x)) for x in leaves]
@@ -1814,7 +2066,7 @@ def parse_component(segments):
else:
raise BoxConstructError
- def to_svg(self):
+ def to_svg(self, transform):
l = self.style.get_line_width(face_element=False)
style = create_css(
edge_color=self.edge_color, face_color=self.face_color, stroke_width=l
@@ -1822,15 +2074,19 @@ def to_svg(self):
def components():
for component in self.components:
- transformed = [(k, [xy.pos() for xy in p]) for k, p in component]
- yield " ".join(_svg_bezier(*transformed)) + " Z"
+
+ transformed = [(k, transform(*p)) for k, p in component]
+ yield ' '.join(_svg_bezier(*transformed)) + ' Z'
+# transformed = [(k, [xy.pos() for xy in p]) for k, p in component]
+# yield " ".join(_svg_bezier(*transformed)) + " Z"
+
return '' % (
" ".join(components()),
style,
)
- def to_asy(self):
+ def to_asy(self, transform):
l = self.style.get_line_width(face_element=False)
pen = create_pens(edge_color=self.edge_color, stroke_width=l)
@@ -1839,8 +2095,12 @@ def to_asy(self):
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)
+
+ transformed = [(k, transform(*p)) for k, p in component]
+ yield 'fill(%s--cycle, %s);' % (''.join(_asy_bezier(*transformed)), pen)
+# 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())
@@ -1851,9 +2111,7 @@ def extent(self):
for _, points in component:
for p in points:
x, y = p.pos()
- result.extend(
- [(x - l, y - l), (x - l, y + l), (x + l, y - l), (x + l, y + l)]
- )
+ result.extend([(x - l, y - l), (x - l, y + l), (x + l, y - l), (x + l, y + l)])
return result
@@ -1919,7 +2177,7 @@ def process_option(self, name, value):
else:
raise BoxConstructError
- def to_svg(self):
+ def to_svg(self, transform):
l = self.style.get_line_width(face_element=True)
if self.vertex_colors is None:
face_color = self.face_color
@@ -1932,20 +2190,26 @@ def to_svg(self):
if self.vertex_colors is not None:
mesh = []
for index, line in enumerate(self.lines):
+ tc = transform(*[r.p for r in line])
data = [
- [coords.pos(), color.to_js()]
- for coords, color in zip(line, self.vertex_colors[index])
- ]
+ [coords, color.to_js()] for coords, color in zip(
+ tc, self.vertex_colors[index])
+# [coords.pos(), color.to_js()] for coords, color in zip(
+# line, self.vertex_colors[index])
+ ]
+
mesh.append(data)
svg += '' % json.dumps(mesh)
for line in self.lines:
+ tc = transform(*[r.p for r in line])
svg += '' % (
- " ".join("%f,%f" % coords.pos() for coords in line),
+ " ".join('%f,%f' % c for c in tc),
+# " ".join("%f,%f" % coords.pos() for coords in line),
style,
)
return svg
- def to_asy(self):
+ def to_asy(self, transform):
l = self.style.get_line_width(face_element=True)
if self.vertex_colors is None:
face_color = self.face_color
@@ -1963,9 +2227,11 @@ def to_asy(self):
colors = []
edges = []
for index, line in enumerate(self.lines):
+
paths.append(
- "--".join(["(%.5g,%.5g)" % coords.pos() for coords in line])
- + "--cycle"
+ "--".join([
+ "(%.5g,%.5g)" % c for c in transform(*line)]) + "--cycle"
+# "(%.5g,%.5g)" % coords.pos() for coords in line])+ "--cycle"
)
# ignore opacity
@@ -1984,8 +2250,11 @@ def to_asy(self):
)
if pens and pens != "nullpen":
for line in self.lines:
+
path = (
- "--".join(["(%.5g,%.5g)" % coords.pos() for coords in line])
+ '--'.join(
+ ['(%.5g,%.5g)' % c for c in transform(*line)])
+# ["(%.5g,%.5g)" % coords.pos() for coords in line])
+ "--cycle"
)
asy += "filldraw(%s, %s);" % (path, pens)
@@ -2189,8 +2458,8 @@ def heads(self, extent, default_arrow, custom_arrow):
def _norm(p, q):
- px, py = p
- qx, qy = q
+ px, py = p.p
+ qx, qy = q.p
dx = qx - px
dy = qy - py
@@ -2219,8 +2488,8 @@ def draw(points):
def arrows(self, points, heads): # heads has to be sorted by pos
def segments(points):
for i in range(len(points) - 1):
- px, py = points[i]
- dx, dy, dl = _norm((px, py), points[i + 1])
+ px, py = points[i].p
+ dx, dy, dl = _norm(points[i], points[i + 1])
yield dl, px, py, dx, dy
seg = list(segments(points))
@@ -2446,7 +2715,7 @@ def shrink_one_end(line, s):
while s > 0.0:
if len(line) < 2:
return []
- xy, length = setback(line[0].p, line[1].p, s)
+ xy, length = setback(line[0], line[1], s)
if xy is not None:
line[0] = line[0].add(*xy)
else:
@@ -2468,7 +2737,7 @@ def shrink(line, s1, s2):
# note that shrinking needs to happen in the Graphics[] coordinate space, whereas the
# subsequent position calculation needs to happen in pixel space.
- transformed_points = [xy.pos() for xy in shrink(line, *self.setback)]
+ transformed_points = shrink(line, *self.setback)
for s in polyline(transformed_points):
yield s
@@ -2476,31 +2745,39 @@ def shrink(line, s1, s2):
for s in self.curve.arrows(transformed_points, heads):
yield s
- def _custom_arrow(self, format, format_transform):
+ def _custom_arrow(self, format, transform):
def make(graphics):
- xmin, xmax, ymin, ymax, ox, oy, ex, ey, code = _extract_graphics(
- graphics, format, self.graphics.evaluation
- )
- boxw = xmax - xmin
- boxh = ymax - ymin
+ code = _extract_graphics(
+ graphics, format, self.graphics.evaluation)
+
+ half_pi = pi / 2.
+# xmin, xmax, ymin, ymax, ox, oy, ex, ey, code = _extract_graphics(
+# graphics, format, self.graphics.evaluation
+# )
+# boxw = xmax - xmin
+# boxh = ymax - ymin
def draw(px, py, vx, vy, t1, s):
t0 = t1
- cx = px + t0 * vx
- cy = py + t0 * vy
- transform = format_transform()
- transform.translate(cx, cy)
- transform.scale(-s / boxw * ex, -s / boxh * ey)
- transform.rotate(90 + degrees(atan2(vy, vx)))
- transform.translate(-ox, -oy)
- yield transform.apply(code)
+ tx = px + t0 * vx
+ ty = py + t0 * vy
+
+ r = half_pi + atan2(vy, vx)
+
+ s = -s
+
+ cos_r = cos(r)
+ sin_r = sin(r)
+
+ # see TranslationTransform[{tx,ty}].ScalingTransform[{s,s}].RotationTransform[r]
+ yield transform([[s * cos_r, -s * sin_r, tx], [s * sin_r, s * cos_r, ty], [0, 0, 1]], code)
return draw
return make
- def to_svg(self):
+ def to_svg(self, transform):
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)
@@ -2509,15 +2786,18 @@ def to_svg(self):
def polygon(points):
yield '' % arrow_style
- extent = self.graphics.view_width or 0
+ def svg_transform(m, code):
+ return _Transform(m).to_svg(code)
+
+ extent = self.graphics.extent_width or 0
default_arrow = self._default_arrow(polygon)
- custom_arrow = self._custom_arrow("svg", _SVGTransform)
+ custom_arrow = self._custom_arrow('svg', svg_transform)
return "".join(self._draw(polyline, default_arrow, custom_arrow, extent))
- def to_asy(self):
+ def to_asy(self, transform):
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)
@@ -2525,21 +2805,24 @@ def to_asy(self):
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
+ yield 'filldraw('
+ yield '--'.join(['(%.5g,%5g)' % xy for xy in transform(*points)])
+ yield '--cycle, % s);' % arrow_pen
+
+ def asy_transform(m, code):
+ return _Transform(m).to_asy(code)
- extent = self.graphics.view_width or 0
+ extent = self.graphics.extent_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))
+ custom_arrow = self._custom_arrow('asy', asy_transform)
+ return ''.join(self._draw(polyline, default_arrow, custom_arrow, extent))
def extent(self):
width = self.style.get_line_width(face_element=False)
def polyline(points):
for p in points:
- x, y = p
+ x, y = p.p
yield x - width, y - width
yield x - width, y + width
yield x + width, y - width
@@ -2555,14 +2838,196 @@ def default_arrow(px, py, vx, vy, t1, s):
return list(self._draw(polyline, default_arrow, None, 0))
+class TransformationFunction(Builtin):
+ """
+ >> RotationTransform[Pi].TranslationTransform[{1, -1}]
+ = TransformationFunction[{{-1, 0, -1}, {0, -1, 1}, {0, 0, 1}}]
+
+ >> TranslationTransform[{1, -1}].RotationTransform[Pi]
+ = TransformationFunction[{{-1, 0, 1}, {0, -1, -1}, {0, 0, 1}}]
+ """
+
+ rules = {
+ 'Dot[TransformationFunction[a_], TransformationFunction[b_]]': 'TransformationFunction[a . b]',
+ 'TransformationFunction[m_][v_]': 'Take[m . Join[v, {0}], Length[v]]',
+ }
+
+
+class TranslationTransform(Builtin):
+ """
+
+ - 'TranslationTransform[v]'
+
- gives the translation by the vector $v$.
+
+
+ >> TranslationTransform[{1, 2}]
+ = TransformationFunction[{{1, 0, 1}, {0, 1, 2}, {0, 0, 1}}]
+ """
+
+ rules = {
+ 'TranslationTransform[v_]':
+ 'TransformationFunction[IdentityMatrix[Length[v] + 1] + '
+ '(Join[ConstantArray[0, Length[v]], {#}]& /@ Join[v, {0}])]',
+ }
+
+
+class RotationTransform(Builtin):
+ rules = {
+ 'RotationTransform[phi_]':
+ 'TransformationFunction[{{Cos[phi], -Sin[phi], 0}, {Sin[phi], Cos[phi], 0}, {0, 0, 1}}]',
+ 'RotationTransform[phi_, p_]':
+ 'TranslationTransform[-p] . RotationTransform[phi] . TranslationTransform[p]',
+ }
+
+
+class ScalingTransform(Builtin):
+ rules = {
+ 'ScalingTransform[v_]':
+ 'TransformationFunction[DiagonalMatrix[Join[v, {1}]]]',
+ 'ScalingTransform[v_, p_]':
+ 'TranslationTransform[-p] . ScalingTransform[v] . TranslationTransform[p]',
+ }
+
+
+class Translate(Builtin):
+ """
+
+ - 'Translate[g, {x, y}]'
+
- translates an object by the specified amount.
+
- 'Translate[g, {{x1, y1}, {x2, y2}, ...}]'
+
- creates multiple instances of object translated by the specified amounts.
+
+
+ >> Graphics[{Circle[], Translate[Circle[], {1, 0}]}]
+ = -Graphics-
+ """
+
+ rules = {
+ 'Translate[g_, v_?(Depth[#] > 2&)]': 'GeometricTransformation[g, TranslationTransform /@ v]',
+ 'Translate[g_, v_?(Depth[#] == 2&)]': 'GeometricTransformation[g, TranslationTransform[v]]',
+ }
+
+
+class Rotate(Builtin):
+ """
+
+ - 'Rotate[g, phi]'
+
- rotates an object by the specified amount.
+
+
+ >> Graphics[Rotate[Rectangle[], Pi / 3]]
+ = -Graphics-
+
+ >> Graphics[{Rotate[Rectangle[{0, 0}, {0.2, 0.2}], 1.2, {0.1, 0.1}], Red, Disk[{0.1, 0.1}, 0.05]}]
+ = -Graphics-
+
+ >> Graphics[Table[Rotate[Scale[{RGBColor[i,1-i,1],Rectangle[],Black,Text["ABC",{0.5,0.5}]},1-i],Pi*i], {i,0,1,0.2}]]
+ = -Graphics-
+ """
+
+ rules = {
+ 'Rotate[g_, phi_]': 'GeometricTransformation[g, RotationTransform[phi]]',
+ 'Rotate[g_, phi_, p_]': 'GeometricTransformation[g, RotationTransform[phi, p]]',
+ }
+
+
+class Scale(Builtin):
+ """
+
+ - 'Scale[g, phi]'
+
- scales an object by the specified amount.
+
+
+ >> Graphics[Rotate[Rectangle[], Pi / 3]]
+ = -Graphics-
+
+ >> Graphics[{Scale[Rectangle[{0, 0}, {0.2, 0.2}], 3, {0.1, 0.1}], Red, Disk[{0.1, 0.1}, 0.05]}]
+ = -Graphics-
+ """
+
+ rules = {
+ 'Scale[g_, s_?ListQ]': 'GeometricTransformation[g, ScalingTransform[s]]',
+ 'Scale[g_, s_]': 'GeometricTransformation[g, ScalingTransform[{s, s}]]',
+ 'Scale[g_, s_?ListQ, p_]': 'GeometricTransformation[g, ScalingTransform[s, p]]',
+ 'Scale[g_, s_, p_]': 'GeometricTransformation[g, ScalingTransform[{s, s}, p]]',
+ }
+
+
+class GeometricTransformation(Builtin):
+ """
+
+ - 'GeometricTransformation[$g$, $tfm$]'
+
- transforms an object $g$ with the transformation $tfm$.
+
+ """
+ pass
+
+
+class GeometricTransformationBox(_GraphicsElement):
+ def init(self, graphics, style, contents, transform):
+ super(GeometricTransformationBox, self).init(graphics, None, style)
+ self.contents = contents
+ if transform.get_head_name() == 'System`List':
+ functions = transform.leaves
+ else:
+ functions = [transform]
+ evaluation = graphics.evaluation
+ self.transforms = [_Transform(Expression('N', f).evaluate(evaluation)) for f in functions]
+ self.precompute = graphics.precompute_transformations
+
+ def patch_transforms(self, transforms):
+ self.transforms = transforms
+
+ def extent(self):
+ def points():
+ for content in self.contents:
+ p = content.extent()
+ for transform in self.transforms:
+ for q in transform(*p):
+ yield q
+ return list(points())
+
+ def to_svg(self, transform0):
+ if self.precompute:
+ def instances():
+ for transform in self.transforms:
+ t = transform.combine(transform0)
+ for content in self.contents:
+ yield content.to_svg(t)
+ else:
+ def instances():
+ for content in self.contents:
+ content_svg = content.to_svg(transform0)
+ for transform in self.transforms:
+ res = transform.to_svg(content_svg)
+ yield res
+ return ''.join(instances())
+
+ def to_asy(self, transform0):
+ def instances():
+ for content in self.contents:
+ content_asy = content.to_asy(transform0)
+ for transform in self.transforms:
+ yield transform.to_asy(content_asy)
+ return ''.join(instances())
+
+
class InsetBox(_GraphicsElement):
- def init(self, graphics, style, item=None, content=None, pos=None, opos=(0, 0)):
+ def init(self, graphics, style, item=None, content=None, pos=None,
+ opos=(0, 0), font_size=None, is_absolute=False):
super(InsetBox, self).init(graphics, item, style)
self.color = self.style.get_option("System`FontColor")
if self.color is None:
self.color, _ = style.get_style(_Color, face_element=False)
+ if font_size is not None:
+ self.font_size = FontSize(self.graphics, value=font_size)
+ else:
+ self.font_size, _ = self.style.get_style(FontSize, face_element=False)
+ if self.font_size is None:
+ self.font_size = FontSize(self.graphics, value=10.)
+
if item is not None:
if len(item.leaves) not in (1, 2, 3):
raise BoxConstructError
@@ -2571,41 +3036,143 @@ def init(self, graphics, style, item=None, content=None, pos=None, opos=(0, 0)):
if len(item.leaves) > 1:
self.pos = Coords(graphics, item.leaves[1])
else:
- self.pos = Coords(graphics, pos=(0, 0))
+ self.pos = (0, 0)
if len(item.leaves) > 2:
- self.opos = coords(item.leaves[2])
+ self.opos = Coords(graphics, item.leaves[1])
else:
- self.opos = (0, 0)
+ self.opos = Coords(graphics, pos=(0, 0))
else:
self.content = content
self.pos = pos
self.opos = opos
- self.content_text = self.content.boxes_to_text(
- evaluation=self.graphics.evaluation
- )
+
+
+ self.is_absolute = is_absolute
+
+ try:
+# self.content_text = self.content.boxes_to_text(
+# evaluation=self.graphics.evaluation
+# ) # In HEAD
+ self._prepare_text_svg()
+ except WebEngineUnavailable as e:
+ self.svg = None
+
+ self.content_text = self.content.boxes_to_text(
+ evaluation=self.graphics.evaluation)
+
+ if self.graphics.evaluation.output.warn_about_web_engine():
+ self.graphics.evaluation.message(
+ 'General', 'nowebeng', str(e), once=True)
+ except Exception as e:
+ self.svg = None
+
+ self.graphics.evaluation.message(
+ 'General', 'nowebeng', str(e), once=True)
def extent(self):
- p = self.pos.pos()
- h = 25
- w = len(self.content_text) * 7 # rough approximation by numbers of characters
+ p = self.pos.p
+
+ if not self.svg:
+ h = 25
+ if not hasattr(self, 'content_text'):
+ self.content_text = self.content.to_python()
+ w = len(self.content_text) * 7 # rough approximation by numbers of characters
+ else:
+ _, w, h = self.svg
+ scale = self._text_svg_scale(h)
+ w *= scale
+ h *= scale
opos = self.opos
x = p[0] - w / 2.0 - opos[0] * w / 2.0
y = p[1] - h / 2.0 + opos[1] * h / 2.0
return [(x, y), (x + w, y + h)]
- def to_svg(self):
- x, y = self.pos.pos()
- content = self.content.boxes_to_xml(evaluation=self.graphics.evaluation)
+ def _prepare_text_svg(self):
+ self.graphics.evaluation.output.assume_web_engine()
+
+ content = self.content.boxes_to_xml(
+ evaluation=self.graphics.evaluation)
+
+ svg = self.graphics.evaluation.output.mathml_to_svg(
+ '' % content)
+
+ svg = svg.replace('style', 'data-style', 1) # HACK
+
+ # we could parse the svg and edit it. using regexps here should be
+ # a lot faster though.
+
+ def extract_dimension(svg, name):
+ values = [0.]
+
+ def replace(m):
+ value = m.group(1)
+ values.append(float(value))
+ return '%s="%s"' % (name, value)
+
+ svg = re.sub(name + r'="([0-9\.]+)ex"', replace, svg, 1)
+ return svg, values[-1]
+
+ svg, width = extract_dimension(svg, 'width')
+ svg, height = extract_dimension(svg, 'height')
+
+ self.svg = (svg, width, height)
+
+ def _text_svg_scale(self, height):
+ size = self.font_size.get_size()
+ return size / height
+
+ def _text_svg_xml(self, style, x, y, absolute):
+ svg, width, height = self.svg
+ svg = re.sub(r'