From a4fe964019867cebd2a1679e5ef982a9bed38b45 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 19 Aug 2016 09:32:21 +0200 Subject: [PATCH 01/17] transforms basics --- mathics/builtin/graphics.py | 146 +++++++++++++++++++++++++++++++++++- 1 file changed, 145 insertions(+), 1 deletion(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 6f75ba18e5..5b64abf66a 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -342,6 +342,137 @@ def apply(self, asy): return self._template % (' * '.join(self.transforms), asy) +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 scaled(self, x, y): + # we compute AB, where A is the scale matrix (x, y, 1) and B is + # self.matrix + m = self.matrix + return _Transform([[t * x for t in m[0]], [t * y for t in m[1]], m[2]]) + + def transform(self, p): + 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] + + 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 + + # 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) + + +class _SVGTransform(): + def __init__(self): + self.transforms = [] + + 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)) + + def scale(self, x, y): + self.transforms.append('scale(%f, %f)' % (x, y)) + + def rotate(self, x): + self.transforms.append('rotate(%f)' % x) + + def apply(self, svg): + return '%s' % (' '.join(self.transforms), svg) + + +class _ASYTransform(): + _template = """ + add(%s * (new picture() { + picture saved = currentpicture; + picture transformed = new picture; + currentpicture = transformed; + %s + currentpicture = saved; + return transformed; + })()); + """ + + def __init__(self): + self.transforms = [] + + def matrix(self, a, b, c, d, e, f): + # a c e + # b d f + # 0 0 1 + # see http://asymptote.sourceforge.net/doc/Transforms.html#Transforms + self.transforms.append('(%f, %f, %f, %f, %f, %f)' % (e, f, a, c, b, d)) + + def translate(self, x, y): + self.transforms.append('shift(%f, %f)' % (x, y)) + + def scale(self, x, y): + self.transforms.append('scale(%f, %f)' % (x, y)) + + def rotate(self, x): + self.transforms.append('rotate(%f)' % x) + + def apply(self, asy): + return self._template % (' * '.join(self.transforms), asy) + + class Graphics(Builtin): r"""
@@ -402,6 +533,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': @@ -2419,7 +2552,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 +2598,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: @@ -2514,6 +2648,16 @@ def __init__(self, content, evaluation, neg_y=False): self.pixel_height = self.extent_width = self.extent_height = None self.view_width = None + def fix_transform(self, transform): + 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 + x = self.pixel_width / w + y = self.pixel_height / h + return transform.scaled(x, y) + else: + return transform + def translate(self, coords): if self.pixel_width is not None: w = self.extent_width if self.extent_width > 0 else 1 From 0fa843fc1279457355a9c04edd2ef2bc9f11297c Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 19 Aug 2016 09:33:35 +0200 Subject: [PATCH 02/17] more transforms --- mathics/builtin/graphics.py | 196 +++++++++++++++++++++++++++++++++++- 1 file changed, 193 insertions(+), 3 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 5b64abf66a..b2ac1f1d66 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -375,10 +375,12 @@ def __init__(self, f): self.matrix = [[_to_float(x) for x in row.leaves] for row in rows] def scaled(self, x, y): - # we compute AB, where A is the scale matrix (x, y, 1) and B is - # self.matrix + # we compute ABC, where A is the scale matrix (x, y, 1), C is the + # scale matrix (1 / x, 1 / y, 1) and B is self.matrix. m = self.matrix - return _Transform([[t * x for t in m[0]], [t * y for t in m[1]], m[2]]) + ab = [[t * x for t in m[0]], [t * y for t in m[1]], m[2]] + s = (1. / x, 1. / y, 1.) + return _Transform([[s[i] * t for i, t in enumerate(row)] for row in ab]) def transform(self, p): m = self.matrix @@ -414,6 +416,38 @@ def to_svg(self, svg): 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 = '(%f, %f, %f, %f, %f, %f)' % (e, f, a, c, b, d) + + template = """ + add(%s * (new picture() { + picture saved = currentpicture; + picture transformed = new picture; + currentpicture = transformed; + %s + currentpicture = saved; + return transformed; + })()); + """ + + return template % (t, asy) + class _SVGTransform(): def __init__(self): @@ -2345,6 +2379,162 @@ 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- + """ + + 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- + """ + + 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 by the given transformation. +
+ """ + 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 extent(self): + def points(): + graphics = self.graphics + for content in self.contents: + for transform in self.transforms: + p = content.extent() + for q in graphics.fix_transform(transform).transform(p): + yield q + return list(points()) + + def to_svg(self): + def instances(): + graphics = self.graphics + for content in self.contents: + content_svg = content.to_svg() + for transform in self.transforms: + yield graphics.fix_transform(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 graphics.fix_transform(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)): From cc91d9c2efb8e9523afd8c383eb02ba830c10308 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 19 Aug 2016 12:03:08 +0200 Subject: [PATCH 03/17] transforms cleanup --- mathics/builtin/graphics.py | 137 +++++++++++++++--------------------- mathics/builtin/tensors.py | 97 +++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 80 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index b2ac1f1d66..1e8f36917c 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -374,13 +374,10 @@ def __init__(self, f): self.matrix = [[_to_float(x) for x in row.leaves] for row in rows] - def scaled(self, x, y): - # we compute ABC, where A is the scale matrix (x, y, 1), C is the - # scale matrix (1 / x, 1 / y, 1) and B is self.matrix. - m = self.matrix - ab = [[t * x for t in m[0]], [t * y for t in m[1]], m[2]] - s = (1. / x, 1. / y, 1.) - return _Transform([[s[i] * t for i, t in enumerate(row)] for row in ab]) + 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 transform(self, p): m = self.matrix @@ -449,64 +446,6 @@ def to_asy(self, asy): return template % (t, asy) -class _SVGTransform(): - def __init__(self): - self.transforms = [] - - 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)) - - def scale(self, x, y): - self.transforms.append('scale(%f, %f)' % (x, y)) - - def rotate(self, x): - self.transforms.append('rotate(%f)' % x) - - def apply(self, svg): - return '%s' % (' '.join(self.transforms), svg) - - -class _ASYTransform(): - _template = """ - add(%s * (new picture() { - picture saved = currentpicture; - picture transformed = new picture; - currentpicture = transformed; - %s - currentpicture = saved; - return transformed; - })()); - """ - - def __init__(self): - self.transforms = [] - - def matrix(self, a, b, c, d, e, f): - # a c e - # b d f - # 0 0 1 - # see http://asymptote.sourceforge.net/doc/Transforms.html#Transforms - self.transforms.append('(%f, %f, %f, %f, %f, %f)' % (e, f, a, c, b, d)) - - def translate(self, x, y): - self.transforms.append('shift(%f, %f)' % (x, y)) - - def scale(self, x, y): - self.transforms.append('scale(%f, %f)' % (x, y)) - - def rotate(self, x): - self.transforms.append('rotate(%f)' % x) - - def apply(self, asy): - return self._template % (' * '.join(self.transforms), asy) - - class Graphics(Builtin): r"""
@@ -1225,10 +1164,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): @@ -2458,6 +2404,9 @@ class Rotate(Builtin): >> 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- """ rules = { @@ -2475,6 +2424,9 @@ class Scale(Builtin): >> 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 = { @@ -2488,8 +2440,8 @@ class Scale(Builtin): class GeometricTransformation(Builtin): """
-
'GeometricTransformation[g, tfm]' -
transforms an object by the given transformation. +
'GeometricTransformation[$g$, $tfm$]' +
transforms an object $g$ with the transformation $tfm$.
""" pass @@ -2508,21 +2460,21 @@ def init(self, graphics, style, contents, transform): def extent(self): def points(): - graphics = self.graphics + fixed_transforms = [self.graphics.fix_transform(transform) for transform in self.transforms] for content in self.contents: - for transform in self.transforms: + for transform in fixed_transforms: p = content.extent() - for q in graphics.fix_transform(transform).transform(p): + for q in transform.transform(p): yield q return list(points()) def to_svg(self): def instances(): - graphics = self.graphics + fixed_transforms = [self.graphics.fix_transform(transform) for transform in self.transforms] for content in self.contents: content_svg = content.to_svg() - for transform in self.transforms: - yield graphics.fix_transform(transform).to_svg(content_svg) + for transform in fixed_transforms: + yield transform.to_svg(content_svg) return ''.join(instances()) def to_asy(self): @@ -2839,12 +2791,37 @@ def __init__(self, content, evaluation, neg_y=False): self.view_width = None def fix_transform(self, transform): + # mirror what happens in GraphicsElements.translate() in order to get out transformation matrices right. + if self.pixel_width is not None: + tx = -self.xmin + ty = -self.ymin + w = self.extent_width if self.extent_width > 0 else 1 h = self.extent_height if self.extent_height > 0 else 1 - x = self.pixel_width / w - y = self.pixel_height / h - return transform.scaled(x, y) + + sx = self.pixel_width / w + sy = self.pixel_height / h + + if self.neg_y: + sy = -sy + + qx = 0 + if self.neg_y: + qy = self.pixel_height + else: + qy = 0 + + # we compute M1 = TranslationTransform[{qx, qy}].ScalingTransform[{sx, sy}].TranslationTransform[{tx, ty}] + # and its inverse M2 = Inverse[M1] + + m1 = [[sx, 0, sx * tx + qx], [0, sy, sy * ty + qy], [0, 0, 1]] + + m2 = [[1. / sx, 0, (-qx * sy - sx * sy * tx) / (sx * sy)], + [0, 1. / sy, (-qy * sx - sx * sy * ty) / (sx * sy)], + [0, 0, 1]] + + return _Transform(m1).multiply(transform).multiply(_Transform(m2)) else: return transform 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]', + } From b4afe3907fa492fa1e17fdcca02259d8438cf3c7 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 19 Aug 2016 14:42:37 +0200 Subject: [PATCH 04/17] coordinate transform overhaul part 1 (plot is currently broken) --- mathics/builtin/graphics.py | 116 +++++++++++++++++------------------- 1 file changed, 55 insertions(+), 61 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 1e8f36917c..628adf7a2e 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -74,12 +74,13 @@ 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 + return p[0] + d[0], p[1] + d[1] + else: + return p def add(self, x, y): p = (self.p[0] + x, self.p[1] + y) @@ -1237,7 +1238,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 '' % ( @@ -1247,7 +1248,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, @@ -2458,11 +2459,14 @@ def init(self, graphics, style, contents, 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(): - fixed_transforms = [self.graphics.fix_transform(transform) for transform in self.transforms] + #fixed_transforms = [self.graphics.fix_transform(transform) for transform in self.transforms] for content in self.contents: - for transform in fixed_transforms: + for transform in self.transforms: p = content.extent() for q in transform.transform(p): yield q @@ -2470,20 +2474,20 @@ def points(): def to_svg(self): def instances(): - fixed_transforms = [self.graphics.fix_transform(transform) for transform in self.transforms] + # fixed_transforms = [self.graphics.fix_transform(transform) for transform in self.transforms] for content in self.contents: content_svg = content.to_svg() - for transform in fixed_transforms: + for transform in self.transforms: yield transform.to_svg(content_svg) return ''.join(instances()) def to_asy(self): def instances(): - graphics = self.graphics + # graphics = self.graphics for content in self.contents: content_asy = content.to_asy() for transform in self.transforms: - yield graphics.fix_transform(transform).to_asy(content_asy) + yield transform.to_asy(content_asy) return ''.join(instances()) @@ -2668,7 +2672,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_world is None: return 0 edge_style, _ = self.get_style( _Thickness, default_to_faces=face_element, @@ -2789,66 +2793,58 @@ def __init__(self, content, evaluation, neg_y=False): self.xmin = self.ymin = self.pixel_width = None self.pixel_height = self.extent_width = self.extent_height = None self.view_width = None + self.local_to_world = None - def fix_transform(self, transform): - # mirror what happens in GraphicsElements.translate() in order to get out transformation matrices right. - - if self.pixel_width is not None: - tx = -self.xmin - ty = -self.ymin + def set_size(self, xmin, ymin, extent_width, extent_height, pixel_width, pixel_height): + self.pixel_width = pixel_width + self.extent_width = extent_width - w = self.extent_width if self.extent_width > 0 else 1 - h = self.extent_height if self.extent_height > 0 else 1 + tx = -xmin + ty = -ymin - sx = self.pixel_width / w - sy = self.pixel_height / h + w = extent_width if extent_width > 0 else 1 + h = extent_height if extent_height > 0 else 1 - if self.neg_y: - sy = -sy + sx = pixel_width / w + sy = pixel_height / h - qx = 0 - if self.neg_y: - qy = self.pixel_height - else: - qy = 0 + qx = 0 + if self.neg_y: + sy = -sy + qy = pixel_height + else: + qy = 0 - # we compute M1 = TranslationTransform[{qx, qy}].ScalingTransform[{sx, sy}].TranslationTransform[{tx, ty}] - # and its inverse M2 = Inverse[M1] + # now build a transform matrix that mimics what used to happen in GraphicsElements.translate(). + # m = TranslationTransform[{qx, qy}].ScalingTransform[{sx, sy}].TranslationTransform[{tx, ty}] - m1 = [[sx, 0, sx * tx + qx], [0, sy, sy * ty + qy], [0, 0, 1]] + m = [[sx, 0, sx * tx + qx], [0, sy, sy * ty + qy], [0, 0, 1]] + transform = _Transform(m) - m2 = [[1. / sx, 0, (-qx * sy - sx * sy * tx) / (sx * sy)], - [0, 1. / sy, (-qy * sx - sx * sy * ty) / (sx * sy)], - [0, 0, 1]] + # update the GeometricTransformationBox, that always has to be the root element. - return _Transform(m1).multiply(transform).multiply(_Transform(m2)) - else: - return transform + self.elements[0].patch_transforms([transform]) + self.local_to_world = transform 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_world: + return list(self.local_to_world.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) + if self.local_to_world is None: + return 0, 0 else: - l = 96.0 / 72 - return (d[0] * l, (-1 if self.neg_y else 1) * d[1] * l) + s = self.extent_width / self.pixel_width # d is a pixel size + l = s * 96.0 / 72 + 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_world 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: @@ -2871,13 +2867,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 @@ -2957,7 +2946,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): From c6202572a49ca2f0e16d9acd5d30f4181aa460d6 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Mon, 22 Aug 2016 10:13:02 +0200 Subject: [PATCH 05/17] fixes two test cases --- mathics/builtin/graphics.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 628adf7a2e..59f29eb1e9 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -431,20 +431,11 @@ def to_asy(self, asy): # b d f # 0 0 1 # see http://asymptote.sourceforge.net/doc/Transforms.html#Transforms - t = '(%f, %f, %f, %f, %f, %f)' % (e, f, a, c, b, d) + t = ','.join(map(asy_number, (e, f, a, c, b, d))) - template = """ - add(%s * (new picture() { - picture saved = currentpicture; - picture transformed = new picture; - currentpicture = transformed; - %s - currentpicture = saved; - return transformed; - })()); - """ - - return template % (t, asy) + return ''.join(("add((", t, ")*(new picture(){", + "picture s=currentpicture,t=new picture;currentpicture=t;", asy, + "currentpicture=s;return t;})());")) class Graphics(Builtin): @@ -473,7 +464,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} From 2f43d09e97d8839571fec8b1c13beb591b441989 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Mon, 22 Aug 2016 11:16:22 +0200 Subject: [PATCH 06/17] fixes for Plot[] --- mathics/builtin/graphics.py | 58 +++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 59f29eb1e9..e36d68c272 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -59,10 +59,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]) @@ -76,17 +75,29 @@ def __init__(self, graphics, expr=None, pos=None, d=None): def pos(self): 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] - else: - return p + 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) +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 cut(value): "Cut values in graphics primitives (not displayed otherwise in SVG)" border = 10 ** 8 @@ -2455,7 +2466,6 @@ def patch_transforms(self, transforms): def extent(self): def points(): - #fixed_transforms = [self.graphics.fix_transform(transform) for transform in self.transforms] for content in self.contents: for transform in self.transforms: p = content.extent() @@ -2465,7 +2475,6 @@ def points(): def to_svg(self): def instances(): - # fixed_transforms = [self.graphics.fix_transform(transform) for transform in self.transforms] for content in self.contents: content_svg = content.to_svg() for transform in self.transforms: @@ -2817,6 +2826,11 @@ def set_size(self, xmin, ymin, extent_width, extent_height, pixel_width, pixel_h self.elements[0].patch_transforms([transform]) self.local_to_world = transform + 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.local_to_world: return list(self.local_to_world.transform([coords]))[0] @@ -2824,11 +2838,15 @@ def translate(self, coords): return coords[0], coords[1] def translate_absolute(self, d): + 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_world is None: return 0, 0 else: - s = self.extent_width / self.pixel_width # d is a pixel size - l = s * 96.0 / 72 + 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): @@ -3225,7 +3243,7 @@ def create_axes(self, elements, graphics_options, xmin, xmax, ymin, ymax): 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) @@ -3248,16 +3266,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))) @@ -3268,12 +3284,12 @@ def add_element(element): add_element(InsetBox( elements, tick_label_style, content=content, - pos=Coords(elements, pos=p_origin(x), + 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)) From c2af2168fdc13c58848659fcf2e70761b579e162 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Mon, 22 Aug 2016 13:30:10 +0200 Subject: [PATCH 07/17] fix for Coords.add() --- mathics/builtin/graphics.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index e36d68c272..0160ef0c2e 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -79,7 +79,7 @@ def pos(self): 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) class AxisCoords(Coords): @@ -97,6 +97,9 @@ def pos(self): else: return p + def add(self, x, y): + raise NotImplementedError + def cut(value): "Cut values in graphics primitives (not displayed otherwise in SVG)" From 1a54d2710559e054c8540fd84b999ed925fe36cc Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 9 Sep 2016 12:45:28 +0200 Subject: [PATCH 08/17] fixes wrong scaling with Text[] --- mathics/builtin/graphics.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 0160ef0c2e..2a1ca19e47 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) @@ -389,6 +390,9 @@ def __init__(self, f): self.matrix = [[_to_float(x) for x in row.leaves] for row in rows] + def inverse(self): + return _Transform(Matrix(self.matrix).inv().tolist()) + def multiply(self, other): a = self.matrix b = other.matrix @@ -475,7 +479,7 @@ class Graphics(Builtin): In 'TeXForm', 'Graphics' produces Asymptote figures: >> Graphics[Circle[]] // TeXForm - = + = . \begin{asy} . size(5.8556cm, 5.8333cm); . 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;})()); @@ -2496,12 +2500,13 @@ def instances(): class InsetBox(_GraphicsElement): def init(self, graphics, style, item=None, content=None, pos=None, - opos=(0, 0)): + opos=(0, 0), absolute_coordinates=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) + self.absolute_coordinates = absolute_coordinates if item is not None: if len(item.leaves) not in (1, 2, 3): @@ -2526,9 +2531,10 @@ def init(self, graphics, style, item=None, content=None, pos=None, def extent(self): p = self.pos.pos() - h = 25 + s = 0.01 + 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 @@ -2543,6 +2549,8 @@ def to_svg(self): '' '%s') % ( x, y, self.opos[0], self.opos[1], style, content) + if not self.absolute_coordinates: + svg = self.graphics.text_matrix.to_svg(svg) return svg def to_asy(self): @@ -2675,7 +2683,7 @@ def get_option(self, name): return self.options.get(name, None) def get_line_width(self, face_element=True): - if self.graphics.local_to_world is None: + if self.graphics.local_to_screen is None: return 0 edge_style, _ = self.get_style( _Thickness, default_to_faces=face_element, @@ -2796,7 +2804,7 @@ def __init__(self, content, evaluation, neg_y=False): self.xmin = self.ymin = self.pixel_width = None self.pixel_height = self.extent_width = self.extent_height = None self.view_width = None - self.local_to_world = 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 @@ -2827,7 +2835,8 @@ def set_size(self, xmin, ymin, extent_width, extent_height, pixel_width, pixel_h # update the GeometricTransformationBox, that always has to be the root element. self.elements[0].patch_transforms([transform]) - self.local_to_world = transform + self.local_to_screen = transform + self.text_matrix = _Transform([[1. / sx, 0, 0], [0, 1. / sy, 0], [0, 0, 1]]) def add_axis_element(self, e): # axis elements are added after the GeometricTransformationBox and are thus not @@ -2835,8 +2844,8 @@ def add_axis_element(self, e): self.elements.append(e) def translate(self, coords): - if self.local_to_world: - return list(self.local_to_world.transform([coords]))[0] + if self.local_to_screen: + return list(self.local_to_screen.transform([coords]))[0] else: return coords[0], coords[1] @@ -2846,14 +2855,14 @@ def translate_absolute(self, d): return x * s, y * s def translate_absolute_in_pixels(self, d): - if self.local_to_world is None: + if self.local_to_screen is None: return 0, 0 else: 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.local_to_world is None: + if self.local_to_screen is None: return 0 else: return x * self.extent_width @@ -3288,7 +3297,7 @@ def add_element(element): elements, tick_label_style, content=content, pos=AxisCoords(elements, pos=p_origin(x), - d=p_self0(-tick_label_d)), opos=p_self0(1))) + d=p_self0(-tick_label_d)), opos=p_self0(1), absolute_coordinates=True)) for x in ticks_small: pos = p_origin(x) ticks_lines.append([AxisCoords(elements, pos=pos), From cb5f80b87e11c9a2224a599a273dfdcd9087f05b Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 9 Sep 2016 12:54:10 +0200 Subject: [PATCH 09/17] got rid of view_width --- mathics/builtin/graphics.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 2a1ca19e47..c2196fafc6 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -1044,7 +1044,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): @@ -2289,7 +2289,7 @@ def polygon(points): yield ' '.join('%f,%f' % xy for xy in points) yield '" style="%s" />' % arrow_style - 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('svg', _SVGTransform) return ''.join(self._draw(polyline, default_arrow, custom_arrow, extent)) @@ -2309,7 +2309,7 @@ def polygon(points): yield '--'.join(['(%.5g,%5g)' % xy for xy in points]) yield '--cycle, % s);' % arrow_pen - 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)) @@ -2801,9 +2801,8 @@ 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): @@ -3100,7 +3099,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 @@ -3140,7 +3138,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() From 35b0c4b9079dc15497b3bccd4299d034d3d0dfbe Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 9 Sep 2016 14:41:36 +0200 Subject: [PATCH 10/17] fixes _extract_graphics() --- mathics/builtin/graphics.py | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index c2196fafc6..592c1d6e1a 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -145,6 +145,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 = [] @@ -268,36 +269,25 @@ 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. - - # 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 + if not isinstance(elements.elements[0], GeometricTransformationBox): + raise ValueError('expected GeometricTransformationBox') - 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(): @@ -2253,10 +2243,8 @@ def render(points, heads): # heads has to be sorted by pos def _custom_arrow(self, format, 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 def draw(px, py, vx, vy, t1, s): t0 = t1 @@ -2265,9 +2253,8 @@ def draw(px, py, vx, vy, t1, s): transform = format_transform() transform.translate(cx, cy) - transform.scale(-s / boxw * ex, -s / boxh * ey) + transform.scale(-s, -s) transform.rotate(90 + degrees(atan2(vy, vx))) - transform.translate(-ox, -oy) yield transform.apply(code) return draw @@ -2808,6 +2795,7 @@ def __init__(self, content, evaluation, neg_y=False): 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 From 0773cdad87dad6e3af1b21f02c46e45b02933ed6 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 9 Sep 2016 14:53:06 +0200 Subject: [PATCH 11/17] got rid of _SVGTransform, _ASYTransform --- mathics/builtin/graphics.py | 135 ++++++++++++------------------------ 1 file changed, 45 insertions(+), 90 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 592c1d6e1a..a089d95f22 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -196,48 +196,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))) @@ -247,19 +247,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); @@ -290,64 +290,6 @@ def _extract_graphics(graphics, format, evaluation): return code -class _SVGTransform(): - def __init__(self): - self.transforms = [] - - 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)) - - def scale(self, x, y): - self.transforms.append('scale(%f, %f)' % (x, y)) - - def rotate(self, x): - self.transforms.append('rotate(%f)' % x) - - def apply(self, svg): - return '%s' % (' '.join(self.transforms), svg) - - -class _ASYTransform(): - _template = """ - add(%s * (new picture() { - picture saved = currentpicture; - picture transformed = new picture; - currentpicture = transformed; - %s - currentpicture = saved; - return transformed; - })()); - """ - - def __init__(self): - self.transforms = [] - - def matrix(self, a, b, c, d, e, f): - # a c e - # b d f - # 0 0 1 - # see http://asymptote.sourceforge.net/doc/Transforms.html#Transforms - self.transforms.append('(%f, %f, %f, %f, %f, %f)' % (e, f, a, c, b, d)) - - def translate(self, x, y): - self.transforms.append('shift(%f, %f)' % (x, y)) - - def scale(self, x, y): - self.transforms.append('scale(%f, %f)' % (x, y)) - - def rotate(self, x): - self.transforms.append('rotate(%f)' % x) - - def apply(self, asy): - return self._template % (' * '.join(self.transforms), asy) - - def _to_float(x): if isinstance(x, Integer): return x.get_int_value() @@ -852,7 +794,7 @@ class ColorDistance(Builtin): = 0.557976 #> ColorDistance[Red, Black, DistanceFunction -> (Abs[#1[[1]] - #2[[1]]] &)] = 0.542917 - + """ options = { @@ -863,17 +805,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), @@ -898,7 +840,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)): @@ -2241,21 +2183,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): code = _extract_graphics( graphics, format, self.graphics.evaluation) + 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, -s) - transform.rotate(90 + degrees(atan2(vy, vx))) - 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 @@ -2276,9 +2225,12 @@ def polygon(points): yield ' '.join('%f,%f' % xy for xy in points) yield '" style="%s" />' % arrow_style + 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): @@ -2296,9 +2248,12 @@ def polygon(points): yield '--'.join(['(%.5g,%5g)' % xy for xy in points]) yield '--cycle, % s);' % arrow_pen + 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): From e0851dc9350a1c20013b3cff5feb6a31b3fd337f Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 9 Sep 2016 17:23:18 +0200 Subject: [PATCH 12/17] better transform for Text[] --- mathics/builtin/graphics.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index a089d95f22..39a7778ee0 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -2359,6 +2359,9 @@ class Rotate(Builtin): >> 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 = { @@ -2473,7 +2476,7 @@ def init(self, graphics, style, item=None, content=None, pos=None, def extent(self): p = self.pos.pos() - s = 0.01 + s = 0.01 # .1 / (self.graphics.pixel_width or 1.) h = s * 25 w = len(self.content_text) * \ s * 7 # rough approximation by numbers of characters @@ -2487,12 +2490,18 @@ def to_svg(self): content = self.content.boxes_to_xml( evaluation=self.graphics.evaluation) style = create_css(font_color=self.color) + + if not self.absolute_coordinates: + x, y = list(self.graphics.local_to_screen.transform([(x, y)]))[0] + svg = ( '' '%s') % ( x, y, self.opos[0], self.opos[1], style, content) + if not self.absolute_coordinates: - svg = self.graphics.text_matrix.to_svg(svg) + svg = self.graphics.inverse_local_to_screen.to_svg(svg) + return svg def to_asy(self): @@ -2778,7 +2787,7 @@ def set_size(self, xmin, ymin, extent_width, extent_height, pixel_width, pixel_h self.elements[0].patch_transforms([transform]) self.local_to_screen = transform - self.text_matrix = _Transform([[1. / sx, 0, 0], [0, 1. / sy, 0], [0, 0, 1]]) + self.inverse_local_to_screen = transform.inverse() def add_axis_element(self, e): # axis elements are added after the GeometricTransformationBox and are thus not From b544a486d097e4eed328d1233d54990d2daf2b36 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 9 Sep 2016 19:05:32 +0200 Subject: [PATCH 13/17] should fix Travis --- mathics/builtin/graphics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 39a7778ee0..c250a03815 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -411,7 +411,7 @@ class Graphics(Builtin): In 'TeXForm', 'Graphics' produces Asymptote figures: >> Graphics[Circle[]] // TeXForm - = + = . \begin{asy} . size(5.8556cm, 5.8333cm); . 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;})()); From 1eca554a633dc38d74ba84ae7708732c5f746038 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sat, 10 Sep 2016 10:24:09 +0200 Subject: [PATCH 14/17] cleaner handling of AxisCoords in InsetBox --- mathics/builtin/graphics.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index c250a03815..3c2a8cb053 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -82,6 +82,9 @@ def add(self, x, y): p = (self.p[0] + x, self.p[1] + y) 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): @@ -101,6 +104,9 @@ def pos(self): def add(self, x, y): raise NotImplementedError + def is_absolute(self): + return True + def cut(value): "Cut values in graphics primitives (not displayed otherwise in SVG)" @@ -2444,14 +2450,12 @@ def instances(): class InsetBox(_GraphicsElement): - def init(self, graphics, style, item=None, content=None, pos=None, - opos=(0, 0), absolute_coordinates=False): + 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') if self.color is None: self.color, _ = style.get_style(_Color, face_element=False) - self.absolute_coordinates = absolute_coordinates if item is not None: if len(item.leaves) not in (1, 2, 3): @@ -2487,11 +2491,13 @@ 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 self.absolute_coordinates: + if not absolute: x, y = list(self.graphics.local_to_screen.transform([(x, y)]))[0] svg = ( @@ -2499,7 +2505,7 @@ def to_svg(self): '%s') % ( x, y, self.opos[0], self.opos[1], style, content) - if not self.absolute_coordinates: + if not absolute: svg = self.graphics.inverse_local_to_screen.to_svg(svg) return svg @@ -3246,7 +3252,7 @@ def add_element(element): elements, tick_label_style, content=content, pos=AxisCoords(elements, pos=p_origin(x), - d=p_self0(-tick_label_d)), opos=p_self0(1), absolute_coordinates=True)) + d=p_self0(-tick_label_d)), opos=p_self0(1))) for x in ticks_small: pos = p_origin(x) ticks_lines.append([AxisCoords(elements, pos=pos), From d05f06ca81673d6bf4c79aa8a9b32a5c9f6e4681 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 16 Sep 2016 18:29:12 +0200 Subject: [PATCH 15/17] added a work around for the axis scaling problems --- mathics/builtin/graphics.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 3c2a8cb053..c16f24c0a7 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -417,7 +417,7 @@ class Graphics(Builtin): In 'TeXForm', 'Graphics' produces Asymptote figures: >> Graphics[Circle[]] // TeXForm - = + = . \begin{asy} . size(5.8556cm, 5.8333cm); . 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;})()); @@ -2649,6 +2649,19 @@ 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(Style): + def __init__(self, style): + super(AxisStyle, self).__init__(style.graphics, style.edge, style.face) + self.styles = style.styles + self.options = style.options + + def get_line_width(self, face_element=True): + return 0.5 + def _flatten(leaves): for leaf in leaves: @@ -3205,6 +3218,11 @@ 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]) From 5062f1aa89ced1cdd1d3dbcb83329cf41adbc63a Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 28 Sep 2016 19:22:28 +0200 Subject: [PATCH 16/17] AxisStyle line width now depends on actual given thickness --- mathics/builtin/graphics.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index c16f24c0a7..73de72fb2e 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -2653,14 +2653,30 @@ def to_axis_style(self): return AxisStyle(self) -class AxisStyle(Style): +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): - super(AxisStyle, self).__init__(style.graphics, style.edge, style.face) - self.styles = style.styles - self.options = style.options + 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 0.5 + return self.base.get_line_width(face_element) * self.sx def _flatten(leaves): From 351ce6d3a7141638224e61830a73b13159d4bd6a Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 28 Sep 2016 19:23:51 +0200 Subject: [PATCH 17/17] fixes test case --- mathics/builtin/graphics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 73de72fb2e..eb1cdfdb93 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -417,7 +417,7 @@ class Graphics(Builtin): In 'TeXForm', 'Graphics' produces Asymptote figures: >> Graphics[Circle[]] // TeXForm - = + = . \begin{asy} . size(5.8556cm, 5.8333cm); . 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;})());