diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py
index 6f75ba18e5..eb1cdfdb93 100644
--- a/mathics/builtin/graphics.py
+++ b/mathics/builtin/graphics.py
@@ -17,6 +17,7 @@
from six.moves import zip
from itertools import chain
from math import sin, cos, pi
+from sympy.matrices import Matrix
from mathics.builtin.base import (
Builtin, InstancableBuiltin, BoxConstruct, BoxConstructError)
@@ -59,10 +60,9 @@ def coords(value):
class Coords(object):
- def __init__(self, graphics, expr=None, pos=None, d=None):
+ def __init__(self, graphics, expr=None, pos=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])
@@ -74,16 +74,38 @@ def __init__(self, graphics, expr=None, pos=None, d=None):
self.p = coords(expr)
def pos(self):
- p = self.graphics.translate(self.p)
+ p = 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)
+ return Coords(self.graphics, pos=p)
+
+ def is_absolute(self):
+ return False
+
+
+class AxisCoords(Coords):
+ def __init__(self, graphics, expr=None, pos=None, d=None):
+ super(AxisCoords, self).__init__(graphics, expr=expr, pos=pos)
+ self.d = d
+
+ def pos(self):
+ p = self.p
+ p = self.graphics.translate(p)
+ p = (cut(p[0]), cut(p[1]))
+ if self.d is not None:
+ d = self.graphics.translate_absolute_in_pixels(self.d)
+ return p[0] + d[0], p[1] + d[1]
+ else:
+ return p
+
+ def add(self, x, y):
+ raise NotImplementedError
+
+ def is_absolute(self):
+ return True
def cut(value):
@@ -129,6 +151,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 = []
@@ -179,48 +202,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)))
@@ -230,19 +253,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);
@@ -252,94 +275,123 @@ 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 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)])
-class _ASYTransform():
- _template = """
- add(%s * (new picture() {
- picture saved = currentpicture;
- picture transformed = new picture;
- currentpicture = transformed;
- %s
- currentpicture = saved;
- return transformed;
- })());
- """
+ def transform(self, p):
+ 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]
+
+ for x, y in p:
+ yield m11 * x + m12 * y + m13, m21 * x + m22 * y + m23
+
+ 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;})());"))
class Graphics(Builtin):
@@ -368,7 +420,7 @@ 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}
@@ -402,6 +454,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':
@@ -746,7 +800,7 @@ class ColorDistance(Builtin):
= 0.557976
#> ColorDistance[Red, Black, DistanceFunction -> (Abs[#1[[1]] - #2[[1]]] &)]
= 0.542917
-
+
"""
options = {
@@ -757,17 +811,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 +846,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 +982,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):
@@ -1058,10 +1112,17 @@ def init(self, graphics, style, item):
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.pos()
+ tx2, ty2 = self.p2.pos()
+
+ 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):
@@ -1124,7 +1185,7 @@ def to_svg(self):
x, y = self.c.pos()
rx, ry = self.r.pos()
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 '' % (
@@ -1134,7 +1195,7 @@ def to_asy(self):
x, y = self.c.pos()
rx, ry = self.r.pos()
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,
@@ -2128,24 +2189,28 @@ def render(points, heads): # heads has to be sorted by pos
for s in render(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
@@ -2166,9 +2231,12 @@ def polygon(points):
yield ' '.join('%f,%f' % xy for xy in points)
yield '" style="%s" />' % 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):
@@ -2186,9 +2254,12 @@ def polygon(points):
yield '--'.join(['(%.5g,%5g)' % xy for xy in 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):
@@ -2212,9 +2283,174 @@ 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]
+
+ def patch_transforms(self, transforms):
+ self.transforms = transforms
+
+ def extent(self):
+ def points():
+ for content in self.contents:
+ for transform in self.transforms:
+ p = content.extent()
+ for q in transform.transform(p):
+ yield q
+ return list(points())
+
+ def to_svg(self):
+ def instances():
+ for content in self.contents:
+ content_svg = content.to_svg()
+ for transform in self.transforms:
+ yield transform.to_svg(content_svg)
+ return ''.join(instances())
+
+ def to_asy(self):
+ def instances():
+ # graphics = self.graphics
+ for content in self.contents:
+ content_asy = content.to_asy()
+ 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)):
super(InsetBox, self).init(graphics, item, style)
self.color = self.style.get_option('System`FontColor')
@@ -2244,9 +2480,10 @@ def init(self, graphics, style, item=None, content=None, pos=None,
def extent(self):
p = self.pos.pos()
- h = 25
+ s = 0.01 # .1 / (self.graphics.pixel_width or 1.)
+ h = s * 25
w = len(self.content_text) * \
- 7 # rough approximation by numbers of characters
+ s * 7 # rough approximation by numbers of characters
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
@@ -2254,13 +2491,23 @@ def extent(self):
def to_svg(self):
x, y = self.pos.pos()
+ absolute = self.pos.is_absolute()
+
content = self.content.boxes_to_xml(
evaluation=self.graphics.evaluation)
style = create_css(font_color=self.color)
+
+ if not absolute:
+ x, y = list(self.graphics.local_to_screen.transform([(x, y)]))[0]
+
svg = (
''
'') % (
x, y, self.opos[0], self.opos[1], style, content)
+
+ if not absolute:
+ svg = self.graphics.inverse_local_to_screen.to_svg(svg)
+
return svg
def to_asy(self):
@@ -2393,7 +2640,7 @@ def get_option(self, name):
return self.options.get(name, None)
def get_line_width(self, face_element=True):
- if self.graphics.pixel_width is None:
+ if self.graphics.local_to_screen is None:
return 0
edge_style, _ = self.get_style(
_Thickness, default_to_faces=face_element,
@@ -2402,6 +2649,35 @@ def get_line_width(self, face_element=True):
return 0
return edge_style.get_thickness()
+ def to_axis_style(self):
+ return AxisStyle(self)
+
+
+class AxisStyle(object):
+ # used exclusively for graphics generated inside GraphicsBox.create_axes().
+ # wraps a Style instance for graphics and does not operate on local but on
+ # screen space, i.e. has to apply "local_to_screen" to all widths, sizes, ...
+
+ def __init__(self, style):
+ self.base = Style(style.graphics)
+ self.base.extend(style)
+ self.sx = style.graphics.local_to_screen.matrix[0][0]
+
+ def extend(self, style, pre=True):
+ self.base.extend(style.base, pre)
+
+ def clone(self):
+ return AxisStyle(self.base.clone())
+
+ def get_style(self, *args, **kwargs):
+ return self.base.get_style(*args, **kwargs)
+
+ def get_option(self, name):
+ return self.base.get_option(name)
+
+ def get_line_width(self, face_element=True):
+ return self.base.get_line_width(face_element) * self.sx
+
def _flatten(leaves):
for leaf in leaves:
@@ -2419,7 +2695,6 @@ def _flatten(leaves):
class _GraphicsElements(object):
def __init__(self, content, evaluation):
self.evaluation = evaluation
- self.elements = []
builtins = evaluation.definitions.builtin
def get_options(name):
@@ -2466,6 +2741,8 @@ def convert(content, style):
raise BoxConstructError
for element in convert(item.leaves[0], stylebox_style(style, item.leaves[1:])):
yield element
+ elif head == 'System`GeometricTransformationBox':
+ yield GeometricTransformationBox(self, style, list(convert(item.leaves[0], style)), item.leaves[1])
elif head[-3:] == 'Box': # and head[:-3] in element_heads:
element_class = get_class(head)
if element_class is not None:
@@ -2510,34 +2787,71 @@ class GraphicsElements(_GraphicsElements):
def __init__(self, content, evaluation, neg_y=False):
super(GraphicsElements, self).__init__(content, evaluation)
self.neg_y = neg_y
- self.xmin = self.ymin = self.pixel_width = None
- self.pixel_height = self.extent_width = self.extent_height = None
- self.view_width = None
+ self.pixel_width = None
+ self.extent_width = self.extent_height = None
+ self.local_to_screen = None
+
+ def set_size(self, xmin, ymin, extent_width, extent_height, pixel_width, pixel_height):
+ self.pixel_width = pixel_width
+ self.extent_width = extent_width
+ self.extent_height = extent_height
+
+ tx = -xmin
+ ty = -ymin
+
+ w = extent_width if extent_width > 0 else 1
+ h = extent_height if extent_height > 0 else 1
+
+ sx = pixel_width / w
+ sy = pixel_height / h
+
+ qx = 0
+ if self.neg_y:
+ sy = -sy
+ qy = pixel_height
+ else:
+ qy = 0
+
+ # now build a transform matrix that mimics what used to happen in GraphicsElements.translate().
+ # m = TranslationTransform[{qx, qy}].ScalingTransform[{sx, sy}].TranslationTransform[{tx, ty}]
+
+ m = [[sx, 0, sx * tx + qx], [0, sy, sy * ty + qy], [0, 0, 1]]
+ transform = _Transform(m)
+
+ # update the GeometricTransformationBox, that always has to be the root element.
+
+ self.elements[0].patch_transforms([transform])
+ self.local_to_screen = transform
+ self.inverse_local_to_screen = transform.inverse()
+
+ def add_axis_element(self, e):
+ # axis elements are added after the GeometricTransformationBox and are thus not
+ # subject to the transformation from local to pixel space.
+ self.elements.append(e)
def translate(self, coords):
- if self.pixel_width is not None:
- w = self.extent_width if self.extent_width > 0 else 1
- h = self.extent_height if self.extent_height > 0 else 1
- result = [(coords[0] - self.xmin) * self.pixel_width / w,
- (coords[1] - self.ymin) * self.pixel_height / h]
- if self.neg_y:
- result[1] = self.pixel_height - result[1]
- return tuple(result)
+ if self.local_to_screen:
+ return list(self.local_to_screen.transform([coords]))[0]
else:
- return (coords[0], coords[1])
+ return coords[0], coords[1]
def translate_absolute(self, d):
- if self.pixel_width is None:
- return (0, 0)
+ s = self.extent_width / self.pixel_width
+ x, y = self.translate_absolute_in_pixels(d)
+ return x * s, y * s
+
+ def translate_absolute_in_pixels(self, d):
+ if self.local_to_screen is None:
+ return 0, 0
else:
- l = 96.0 / 72
- return (d[0] * l, (-1 if self.neg_y else 1) * d[1] * l)
+ l = 96.0 / 72 # d is measured in printer's points
+ return d[0] * l, (-1 if self.neg_y else 1) * d[1] * l
def translate_relative(self, x):
- if self.pixel_width is None:
+ if self.local_to_screen is None:
return 0
else:
- return x * self.pixel_width
+ return x * self.extent_width
def extent(self, completely_visible_only=False):
if completely_visible_only:
@@ -2560,13 +2874,6 @@ def to_svg(self):
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):
-
- self.xmin, self.ymin = xmin, ymin
- self.extent_width, self.extent_height = extent_width, extent_height
- self.pixel_width, self.pixel_height = pixel_width, pixel_height
-
class GraphicsBox(BoxConstruct):
options = Graphics.options
@@ -2646,7 +2953,12 @@ def _prepare_elements(self, leaves, options, neg_y=False, max_width=None):
if not isinstance(plot_range, list) or len(plot_range) != 2:
raise BoxConstructError
- elements = GraphicsElements(leaves[0], options['evaluation'], neg_y)
+ transformation = Expression('System`TransformationFunction', [[1, 0, 0], [0, 1, 0], [0, 0, 1]])
+
+ elements = GraphicsElements(
+ Expression('System`GeometricTransformationBox', leaves[0], transformation),
+ options['evaluation'], neg_y)
+
axes = [] # to be filled further down
def calc_dimensions(final_pass=True):
@@ -2774,7 +3086,6 @@ def boxes_to_tex(self, leaves, **options):
leaves, options, max_width=450)
xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions()
- elements.view_width = w
asy_completely_visible = '\n'.join(
element.to_asy() for element in elements.elements
@@ -2814,7 +3125,6 @@ def boxes_to_xml(self, leaves, **options):
leaves, options, neg_y=True)
xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions()
- elements.view_width = w
svg = elements.to_svg()
@@ -2924,12 +3234,17 @@ def create_axes(self, elements, graphics_options, xmin, xmax, ymin, ymax):
ticks_style = [elements.create_style(s) for s in ticks_style]
axes_style = [elements.create_style(s) for s in axes_style]
label_style = elements.create_style(label_style)
+
+ ticks_style = [s.to_axis_style() for s in ticks_style]
+ axes_style = [s.to_axis_style() for s in axes_style]
+ label_style = label_style.to_axis_style()
+
ticks_style[0].extend(axes_style[0])
ticks_style[1].extend(axes_style[1])
def add_element(element):
element.is_completely_visible = True
- elements.elements.append(element)
+ elements.add_axis_element(element)
ticks_x, ticks_x_small, origin_x = self.axis_ticks(xmin, xmax)
ticks_y, ticks_y_small, origin_y = self.axis_ticks(ymin, ymax)
@@ -2952,16 +3267,14 @@ def add_element(element):
if axes[index]:
add_element(LineBox(
elements, axes_style[index],
- lines=[[Coords(elements, pos=p_origin(min),
- d=p_other0(-axes_extra)),
- Coords(elements, pos=p_origin(max),
- d=p_other0(axes_extra))]]))
+ lines=[[AxisCoords(elements, pos=p_origin(min), d=p_other0(-axes_extra)),
+ AxisCoords(elements, pos=p_origin(max), d=p_other0(axes_extra))]]))
ticks_lines = []
tick_label_style = ticks_style[index].clone()
tick_label_style.extend(label_style)
for x in ticks:
- ticks_lines.append([Coords(elements, pos=p_origin(x)),
- Coords(elements, pos=p_origin(x),
+ ticks_lines.append([AxisCoords(elements, pos=p_origin(x)),
+ AxisCoords(elements, pos=p_origin(x),
d=p_self0(tick_large_size))])
if ticks_int:
content = String(str(int(x)))
@@ -2972,12 +3285,12 @@ def add_element(element):
add_element(InsetBox(
elements, tick_label_style,
content=content,
- pos=Coords(elements, pos=p_origin(x),
- d=p_self0(-tick_label_d)), opos=p_self0(1)))
+ pos=AxisCoords(elements, pos=p_origin(x),
+ d=p_self0(-tick_label_d)), opos=p_self0(1)))
for x in ticks_small:
pos = p_origin(x)
- ticks_lines.append([Coords(elements, pos=pos),
- Coords(elements, pos=pos,
+ ticks_lines.append([AxisCoords(elements, pos=pos),
+ AxisCoords(elements, pos=pos,
d=p_self0(tick_small_size))])
add_element(LineBox(elements, axes_style[0],
lines=ticks_lines))
diff --git a/mathics/builtin/tensors.py b/mathics/builtin/tensors.py
index 80a1e7eaca..1f94be0276 100644
--- a/mathics/builtin/tensors.py
+++ b/mathics/builtin/tensors.py
@@ -475,3 +475,100 @@ def is_boolean(x):
return 'ColorDistance'
return None
+
+
+
+class TransformationFunction(Builtin):
+ """
+
+ - 'TransformationFunction[$m$]'
+
- represents a transformation.
+
+
+ >> 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, {1}], 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):
+ """
+
+ - 'RotationTransform[$phi$]'
+
- gives a rotation by $phi$.
+
- 'RotationTransform[$phi$, $p$]'
+
- gives a rotation by $phi$ around the point $p$.
+
+ """
+
+ 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):
+ """
+
+ - 'ScalingTransform[$v$]'
+
- gives a scaling transform of $v$. $v$ may be a scalar or a vector.
+
- 'ScalingTransform[$phi$, $p$]'
+
- gives a scaling transform of $v$ that is centered at the point $p$.
+
+ """
+
+ rules = {
+ 'ScalingTransform[v_]':
+ 'TransformationFunction[DiagonalMatrix[Join[v, {1}]]]',
+ 'ScalingTransform[v_, p_]':
+ 'TranslationTransform[p] . ScalingTransform[v] . TranslationTransform[-p]',
+ }
+
+
+class ShearingTransform(Builtin):
+ """
+
+ - 'ShearingTransform[$phi$, {1, 0}, {0, 1}]'
+
- gives a horizontal shear by the angle $phi$.
+
- 'ShearingTransform[$phi$, {0, 1}, {1, 0}]'
+
- gives a vertical shear by the angle $phi$.
+
- 'ShearingTransform[$phi$, $u$, $u$, $p$]'
+
- gives a shear centered at the point $p$.
+
+ """
+
+ rules = {
+ 'ShearingTransform[phi_, {1, 0}, {0, 1}]':
+ 'TransformationFunction[{{1, Tan[phi], 0}, {0, 1, 0}, {0, 0, 1}}]',
+ 'ShearingTransform[phi_, {0, 1}, {1, 0}]':
+ 'TransformationFunction[{{1, 0, 0}, {Tan[phi], 1, 0}, {0, 0, 1}}]',
+ 'ShearingTransform[phi_, u_, v_, p_]':
+ 'TranslationTransform[p] . ShearingTransform[phi, u, v] . TranslationTransform[-p]',
+ }