diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py
index 76c259b843..b0e556789f 100644
--- a/mathics/builtin/graphics.py
+++ b/mathics/builtin/graphics.py
@@ -7,13 +7,16 @@
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, InstancableBuiltin, BoxConstruct, BoxConstructError)
from mathics.builtin.options import options_to_rules
+from mathics.layout.client import WebEngineUnavailable
from mathics.core.expression import (
Expression, Integer, Rational, Real, String, Symbol, strip_context,
system_symbols, system_symbols_dict, from_python)
@@ -42,51 +45,28 @@ def get_class(name):
# like return globals().get(name)
-def coords(value):
- if value.has_form('List', 2):
- x, y = value.leaves[0].round_to_float(), value.leaves[1].round_to_float()
- if x is None or y is None:
- raise CoordinatesError
- return (x, y)
- raise CoordinatesError
+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
-class Coords(object):
- def __init__(self, graphics, expr=None, pos=None, d=None):
- self.graphics = graphics
- self.p = pos
- self.d = d
- if expr is not None:
- if expr.has_form('Offset', 1, 2):
- self.d = coords(expr.leaves[0])
- if len(expr.leaves) > 1:
- self.p = coords(expr.leaves[1])
- else:
- self.p = None
- else:
- self.p = coords(expr)
-
- def pos(self):
- p = self.graphics.translate(self.p)
- p = (cut(p[0]), cut(p[1]))
- if self.d is not None:
- d = self.graphics.translate_absolute(self.d)
- return (p[0] + d[0], p[1] + d[1])
- return p
-
- def add(self, x, y):
- p = (self.p[0] + x, self.p[1] + y)
- return Coords(self.graphics, pos=p, d=self.d)
+def add_coords(a, b):
+ x1, y1 = a
+ x2, y2 = b
+ return x1 + x2, y1 + y2
-def cut(value):
- "Cut values in graphics primitives (not displayed otherwise in SVG)"
- border = 10 ** 8
- if value < -border:
- value = -border
- elif value > border:
- value = border
- return 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 create_css(edge_color=None, face_color=None, stroke_width=None,
@@ -122,6 +102,7 @@ def _to_float(x):
raise BoxConstructError
return x
+
def create_pens(edge_color=None, face_color=None, stroke_width=None,
is_face_element=False):
result = []
@@ -172,48 +153,48 @@ def _euclidean_distance(a, b):
def _component_distance(a, b, i):
return abs(a[i] - b[i])
-
+
def _cie2000_distance(lab1, lab2):
#reference: https://en.wikipedia.org/wiki/Color_difference#CIEDE2000
e = machine_epsilon
kL = kC = kH = 1 #common values
-
+
L1, L2 = lab1[0], lab2[0]
a1, a2 = lab1[1], lab2[1]
b1, b2 = lab1[2], lab2[2]
-
+
dL = L2 - L1
Lm = (L1 + L2)/2
C1 = sqrt(a1**2 + b1**2)
C2 = sqrt(a2**2 + b2**2)
Cm = (C1 + C2)/2;
-
+
a1 = a1 * (1 + (1 - sqrt(Cm**7/(Cm**7 + 25**7)))/2)
a2 = a2 * (1 + (1 - sqrt(Cm**7/(Cm**7 + 25**7)))/2)
-
+
C1 = sqrt(a1**2 + b1**2)
C2 = sqrt(a2**2 + b2**2)
Cm = (C1 + C2)/2
dC = C2 - C1
-
+
h1 = (180 * atan2(b1, a1 + e))/pi % 360
h2 = (180 * atan2(b2, a2 + e))/pi % 360
if abs(h2 - h1) <= 180:
- dh = h2 - h1
+ dh = h2 - h1
elif abs(h2 - h1) > 180 and h2 <= h1:
dh = h2 - h1 + 360
elif abs(h2 - h1) > 180 and h2 > h1:
dh = h2 - h1 - 360
-
+
dH = 2*sqrt(C1*C2)*sin(radians(dh)/2)
-
+
Hm = (h1 + h2)/2 if abs(h2 - h1) <= 180 else (h1 + h2 + 360)/2
T = 1 - 0.17*cos(radians(Hm - 30)) + 0.24*cos(radians(2*Hm)) + 0.32*cos(radians(3*Hm + 6)) - 0.2*cos(radians(4*Hm - 63))
-
+
SL = 1 + (0.015*(Lm - 50)**2)/sqrt(20 + (Lm - 50)**2)
SC = 1 + 0.045*Cm
SH = 1 + 0.015*Cm*T
-
+
rT = -2 * sqrt(Cm**7/(Cm**7 + 25**7))*sin(radians(60*exp(-((Hm - 275)**2 / 25**2))))
return sqrt((dL/(SL*kL))**2 + (dC/(SC*kC))**2 + (dH/(SH*kH))**2 + rT*(dC/(SC*kC))*(dH/(SH*kH)))
@@ -223,19 +204,19 @@ def _CMC_distance(lab1, lab2, l, c):
L1, L2 = lab1[0], lab2[0]
a1, a2 = lab1[1], lab2[1]
b1, b2 = lab1[2], lab2[2]
-
+
dL, da, db = L2-L1, a2-a1, b2-b1
e = machine_epsilon
-
+
C1 = sqrt(a1**2 + b1**2);
C2 = sqrt(a2**2 + b2**2);
-
+
h1 = (180 * atan2(b1, a1 + e))/pi % 360;
dC = C2 - C1;
dH2 = da**2 + db**2 - dC**2;
F = C1**2/sqrt(C1**4 + 1900);
T = 0.56 + abs(0.2*cos(radians(h1 + 168))) if (164 <= h1 and h1 <= 345) else 0.36 + abs(0.4*cos(radians(h1 + 35)));
-
+
SL = 0.511 if L1 < 16 else (0.040975*L1)/(1 + 0.01765*L1);
SC = (0.0638*C1)/(1 + 0.0131*C1) + 0.638;
SH = SC*(F*T + 1 - F);
@@ -245,94 +226,143 @@ def _CMC_distance(lab1, lab2, l, c):
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.
+ if not isinstance(elements.elements[0], GeometricTransformationBox):
+ raise ValueError('expected GeometricTransformationBox')
- # 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
+ contents = elements.elements[0].contents
# generate code for svg or asy.
if format == 'asy':
- code = '\n'.join(element.to_asy() for element in elements.elements)
+ code = '\n'.join(element.to_asy() for element in contents)
elif format == 'svg':
- code = elements.to_svg()
+ code = ''.join(element.to_svg() for element in contents)
else:
raise NotImplementedError
- return xmin, xmax, ymin, ymax, ox, oy, ex, ey, code
+ return code
-class _SVGTransform():
- def __init__(self):
- self.transforms = []
+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
- def matrix(self, a, b, c, d, e, f):
- # a c e
- # b d f
- # 0 0 1
- self.transforms.append('matrix(%f, %f, %f, %f, %f, %f)' % (a, b, c, d, e, f))
- def translate(self, x, y):
- self.transforms.append('translate(%f, %f)' % (x, 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
- def scale(self, x, y):
- self.transforms.append('scale(%f, %f)' % (x, y))
+ 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
- def rotate(self, x):
- self.transforms.append('rotate(%f)' % x)
+ self.matrix = [[_to_float(x) for x in row.leaves] for row in rows]
- def apply(self, svg):
- return '%s' % (' '.join(self.transforms), svg)
+ 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
-class _ASYTransform():
- _template = """
- add(%s * (new picture() {
- picture saved = currentpicture;
- picture transformed = new picture;
- currentpicture = transformed;
- %s
- currentpicture = saved;
- return transformed;
- })());
- """
+ 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
- def __init__(self):
- self.transforms = []
+ 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
- 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))
+ t = 'matrix(%f, %f, %f, %f, %f, %f)' % (a, b, c, d, e, f)
+ return '%s' % (t, svg)
- def scale(self, x, y):
- self.transforms.append('scale(%f, %f)' % (x, y))
+ def to_asy(self, asy):
+ m = self.matrix
- def rotate(self, x):
- self.transforms.append('rotate(%f)' % x)
+ a = m[0][0]
+ b = m[1][0]
+ c = m[0][1]
+ d = m[1][1]
+ e = m[0][2]
+ f = m[1][2]
- def apply(self, asy):
- return self._template % (' * '.join(self.transforms), asy)
+ 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
class Graphics(Builtin):
@@ -361,13 +391,13 @@ class Graphics(Builtin):
=
. \begin{asy}
. 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[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[]]] is not a valid box structure.
+ : GraphicsBox[CircleBox[List[a, b]], 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 = {
@@ -380,6 +410,11 @@ class Graphics(Builtin):
'PlotRangePadding': 'Automatic',
'ImageSize': 'Automatic',
'Background': 'Automatic',
+
+ 'Transformation': 'Automatic', # 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.
}
box_suffix = 'Box'
@@ -395,6 +430,8 @@ def convert(content):
return Expression('List', *[convert(item) for item in content.leaves])
elif head == 'System`Style':
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':
@@ -473,8 +510,9 @@ 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) < len(self.default_components):
+ # components.extend(self.default_components[
+ # len(components):])
self.components = components
else:
@@ -739,7 +777,7 @@ class ColorDistance(Builtin):
= 0.557976
#> ColorDistance[Red, Black, DistanceFunction -> (Abs[#1[[1]] - #2[[1]]] &)]
= 0.542917
-
+
"""
options = {
@@ -750,17 +788,17 @@ class ColorDistance(Builtin):
'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 ' +
'two lists of colors of the same length.'
-
+
}
-
- # the docs say LABColor's colorspace corresponds to the CIE 1976 L^* a^* b^* color space
+
+ # 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.
-
+
_distances = {
"CIE76": lambda c1, c2: _euclidean_distance(c1.to_color_space('LAB')[:3], c2.to_color_space('LAB')[:3]),
"CIE94": lambda c1, c2: _euclidean_distance(c1.to_color_space('LCH')[:3], c2.to_color_space('LCH')[:3]),
"CIE2000": lambda c1, c2: _cie2000_distance(100*c1.to_color_space('LAB')[:3], 100*c2.to_color_space('LAB')[:3])/100,
- "CIEDE2000": lambda c1, c2: _cie2000_distance(100*c1.to_color_space('LAB')[:3], 100*c2.to_color_space('LAB')[:3])/100,
+ "CIEDE2000": lambda c1, c2: _cie2000_distance(100*c1.to_color_space('LAB')[:3], 100*c2.to_color_space('LAB')[:3])/100,
"DeltaL": lambda c1, c2: _component_distance(c1.to_color_space('LCH'), c2.to_color_space('LCH'), 0),
"DeltaC": lambda c1, c2: _component_distance(c1.to_color_space('LCH'), c2.to_color_space('LCH'), 1),
"DeltaH": lambda c1, c2: _component_distance(c1.to_color_space('LCH'), c2.to_color_space('LCH'), 2),
@@ -792,7 +830,7 @@ def apply(self, c1, c2, evaluation, options):
100*c2.to_color_space('LAB')[:3], 2, 1)/100
elif distance_function.leaves[1].get_string_value() == 'Perceptibility':
compute = ColorDistance._distances.get("CMC")
-
+
elif distance_function.leaves[1].has_form('List', 2):
if (isinstance(distance_function.leaves[1].leaves[0], Integer)
and isinstance(distance_function.leaves[1].leaves[1], Integer)):
@@ -928,7 +966,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):
@@ -941,6 +979,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
@@ -1049,25 +1135,34 @@ def init(self, graphics, style, item):
raise BoxConstructError
self.edge_color, self.face_color = style.get_style(
_Color, face_element=True)
- self.p1 = Coords(graphics, item.leaves[0])
+
+ self.p1 = expr_to_coords(item.leaves[0])
if len(item.leaves) == 1:
- self.p2 = self.p1.add(1, 1)
+ self.p2 = add_coords(self.p1, (1, 1))
elif len(item.leaves) == 2:
- self.p2 = Coords(graphics, item.leaves[1])
+ self.p2 = expr_to_coords(item.leaves[1])
def extent(self):
l = self.style.get_line_width(face_element=True) / 2
result = []
- for p in [self.p1, self.p2]:
- x, y = p.pos()
- result.extend([(x - l, y - l), (
- x - l, y + l), (x + l, y - l), (x + l, y + l)])
+
+ 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)])
+
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()
+ p1, p2 = transform(self.p1, self.p2)
+ x1, y1 = p1
+ x2, y2 = p2
xmin = min(x1, x2)
ymin = min(y1, y2)
w = max(x1, x2) - xmin
@@ -1076,10 +1171,11 @@ def to_svg(self):
return '' % (
xmin, ymin, w, h, 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()
+ p1, p2 = transform(self.p1, self.p2)
+ x1, y1 = p1
+ x2, y2 = p2
pens = create_pens(
self.edge_color, self.face_color, l, is_face_element=True)
x1, x2, y1, y2 = asy_number(x1), asy_number(
@@ -1097,7 +1193,7 @@ def init(self, graphics, style, item):
raise BoxConstructError
self.edge_color, self.face_color = style.get_style(
_Color, face_element=self.face_element)
- self.c = Coords(graphics, item.leaves[0])
+ self.c = expr_to_coords(item.leaves[0])
if len(item.leaves) == 1:
rx = ry = 1
elif len(item.leaves) == 2:
@@ -1107,12 +1203,12 @@ def init(self, graphics, style, item):
ry = r.leaves[1].round_to_float()
else:
rx = ry = r.round_to_float()
- self.r = self.c.add(rx, ry)
+ self.r = add_coords(self.c, (rx, ry))
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
+ rx, ry = self.r
rx -= x
ry = y - ry
rx += l
@@ -1120,21 +1216,23 @@ def extent(self):
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, self.r)
+ 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 '' % (
x, y, rx, ry, 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
+ rx, ry = r
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,
face_color=self.face_color, stroke_width=l,
@@ -1172,9 +1270,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
@@ -1194,11 +1293,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:
@@ -1216,11 +1315,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:
@@ -1267,15 +1366,15 @@ 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
+ self.lines = [make_coords(line) for line in lines]
def extent(self):
l = self.style.get_line_width(face_element=False)
result = []
for line in self.lines:
for c in line:
- x, y = c.pos()
+ x, y = c
result.extend([(x - l, y - l), (
x - l, y + l), (x + l, y - l), (x + l, y + l)])
return result
@@ -1319,28 +1418,32 @@ 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()
+ graphics = self.graphics
+ size_x = size
+ size_y = size_x * (graphics.extent_height / graphics.extent_width) * (graphics.pixel_width / graphics.pixel_height)
+
style = create_css(edge_color=self.edge_color,
stroke_width=0, face_color=self.face_color)
svg = ''
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)
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
@@ -1377,22 +1480,28 @@ 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 = ''
for line in self.lines:
- svg += '' % (
- ' '.join(['%f,%f' % coords.pos() for coords in line]), style)
+ path = ' '.join(['%f,%f' % c for c in transform(*line)])
+ svg += '' % (path, 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])
+ path = '--'.join(['(%.5g,%5g)' % c for c in transform(*line)])
asy += 'draw(%s, %s);' % (path, pen)
+
return asy
@@ -1496,6 +1605,7 @@ class BezierFunction(Builtin):
'BezierFunction[p_]': 'Function[x, Total[p * BernsteinBasis[Length[p] - 1, Range[0, Length[p] - 1], x]]]',
}
+
class BezierCurve(Builtin):
"""
@@ -1528,23 +1638,23 @@ 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))))
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])):
+ for path in _asy_bezier((self.spline_degree, transform(*line))):
asy += 'draw(%s, %s);' % (path, pen)
return asy
@@ -1594,14 +1704,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([expr_to_coords(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]
@@ -1610,18 +1719,18 @@ 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)
def components():
for component in self.components:
- transformed = [(k, [xy.pos() for xy in p]) for k, p in component]
+ transformed = [(k, transform(*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)
@@ -1630,7 +1739,7 @@ def to_asy(self):
def components():
for component in self.components:
- transformed = [(k, [xy.pos() for xy in p]) for k, p in component]
+ transformed = [(k, transform(*p)) for k, p in component]
yield 'fill(%s--cycle, %s);' % (''.join(_asy_bezier(*transformed)), pen)
return ''.join(components())
@@ -1641,7 +1750,7 @@ def extent(self):
for component in self.components:
for _, points in component:
for p in points:
- x, y = p.pos()
+ x, y = p
result.extend([(x - l, y - l), (x - l, y + l), (x + l, y - l), (x + l, y + l)])
return result
@@ -1709,7 +1818,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
@@ -1721,16 +1830,16 @@ def to_svg(self):
if self.vertex_colors is not None:
mesh = []
for index, line in enumerate(self.lines):
- data = [[coords.pos(), color.to_js()] for coords, color in zip(
- line, self.vertex_colors[index])]
+ data = [[coords, color.to_js()] for coords, color in zip(
+ transform(*line), self.vertex_colors[index])]
mesh.append(data)
svg += '' % json.dumps(mesh)
for line in self.lines:
svg += '' % (
- ' '.join('%f,%f' % coords.pos() for coords in line), style)
+ ' '.join('%f,%f' % c for c in transform(*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
@@ -1745,7 +1854,7 @@ def to_asy(self):
edges = []
for index, line in enumerate(self.lines):
paths.append('--'.join([
- '(%.5g,%.5g)' % coords.pos() for coords in line]) + '--cycle')
+ '(%.5g,%.5g)' % c for c in transform(*line)]) + '--cycle')
# ignore opacity
colors.append(','.join([
@@ -1759,7 +1868,7 @@ def to_asy(self):
if pens and pens != 'nullpen':
for line in self.lines:
path = '--'.join(
- ['(%.5g,%.5g)' % coords.pos() for coords in line]) + '--cycle'
+ ['(%.5g,%.5g)' % c for c in transform(*line)]) + '--cycle'
asy += 'filldraw(%s, %s);' % (path, pens)
return asy
@@ -2212,9 +2321,9 @@ def shrink_one_end(line, s):
while s > 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)
+ line[0] = add_coords(line[0], xy)
else:
line = line[1:]
s -= length
@@ -2231,7 +2340,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
@@ -2239,30 +2348,34 @@ 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(
+ code = _extract_graphics(
graphics, format, self.graphics.evaluation)
- boxw = xmax - xmin
- boxh = ymax - ymin
+
+ half_pi = pi / 2.
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)
@@ -2271,15 +2384,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)
@@ -2288,12 +2404,15 @@ def to_asy(self):
def polygon(points):
yield 'filldraw('
- yield '--'.join(['(%.5g,%5g)' % xy for xy in points])
+ yield '--'.join(['(%.5g,%5g)' % xy for xy in transform(*points)])
yield '--cycle, % s);' % arrow_pen
- extent = self.graphics.view_width or 0
+ def asy_transform(m, code):
+ return _Transform(m).to_asy(code)
+
+ extent = self.graphics.extent_width or 0
default_arrow = self._default_arrow(polygon)
- custom_arrow = self._custom_arrow('asy', _ASYTransform)
+ custom_arrow = self._custom_arrow('asy', asy_transform)
return ''.join(self._draw(polyline, default_arrow, custom_arrow, extent))
def extent(self):
@@ -2317,15 +2436,195 @@ 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:
+ yield transform.to_svg(content_svg)
+ 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)):
+ 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
@@ -2333,43 +2632,138 @@ def init(self, graphics, style, item=None, content=None, pos=None,
self.content = content.format(
graphics.evaluation, 'TraditionalForm')
if len(item.leaves) > 1:
- self.pos = Coords(graphics, item.leaves[1])
+ self.pos = expr_to_coords(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 = expr_to_coords(item.leaves[2])
else:
self.opos = (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._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
+
+ if not self.svg:
+ h = 25
+ 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()
+ 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'