diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index f6fa3165d8..cffbb3a347 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -8,9 +8,11 @@ from math import floor, ceil, log10, sin, cos, pi, sqrt, atan2, degrees, radians, exp +import re import json import base64 from itertools import chain +from sympy.matrices import Matrix from mathics.builtin.base import ( Builtin, @@ -19,6 +21,7 @@ BoxConstructError, ) from mathics.builtin.options import options_to_rules +from mathics.layout.client import WebEngineUnavailable from mathics.core.expression import ( Expression, Integer, @@ -45,6 +48,11 @@ "ImageSize": "Automatic", "Background": "Automatic", "$OptionSyntax": "Ignore", + # Mathics specific; used internally to enable stuff like + # Plot[x + 1e-20 * x, {x, 0, 1}] that without precomputing transformations inside Mathics + # will hit SVGs numerical accuracy abilities in some browsers as strokes with width < 1e-6 + # will get rounded to 0 and thus won't get scale transformed in SVG and vanish. + "Transformation": "Automatic", } @@ -69,6 +77,21 @@ def get_class(name): # like return globals().get(name) + +# def expr_to_coords(value): +# if not value.has_form('List', 2): +# raise CoordinatesError +# x, y = value.leaves[0].to_mpmath(), value.leaves[1].to_mpmath() +# if x is None or y is None: +# raise CoordinatesError +# return x, y + + +# def add_coords(a, b): +# x1, y1 = a +# x2, y2 = b +# return x1 + x2, y1 + y2 + def coords(value): if value.has_form("List", 2): x, y = value.leaves[0].round_to_float(), value.leaves[1].round_to_float() @@ -95,7 +118,7 @@ def __init__(self, graphics, expr=None, pos=None, d=None): def pos(self): p = self.graphics.translate(self.p) - p = (cut(p[0]), cut(p[1])) + p = (cut_global(p[0]), cut_global(p[1])) if self.d is not None: d = self.graphics.translate_absolute(self.d) return (p[0] + d[0], p[1] + d[1]) @@ -106,7 +129,16 @@ def add(self, x, y): return Coords(self.graphics, pos=p, d=self.d) -def cut(value): + +def axis_coords(graphics, pos, d=None): + p = graphics.translate(pos) + if d is not None: + d = graphics.translate_absolute_in_pixels(d) + return p[0] + d[0], p[1] + d[1] + else: + return p + +def cut_global(value): "Cut values in graphics primitives (not displayed otherwise in SVG)" border = 10 ** 8 if value < -border: @@ -115,7 +147,6 @@ def cut(value): value = border return value - def create_css(edge_color=None, face_color=None, stroke_width=None, font_color=None): css = [] if edge_color is not None: @@ -261,6 +292,7 @@ def _cie2000_distance(lab1, lab2): ) + def _CMC_distance(lab1, lab2, l, c): # reference https://en.wikipedia.org/wiki/Color_difference#CMC_l:c_.281984.29 L1, L2 = lab1[0], lab2[0] @@ -289,30 +321,22 @@ def _CMC_distance(lab1, lab2, l, c): return sqrt((dL / (l * SL)) ** 2 + (dC / (c * SC)) ** 2 + dH2 / SH ** 2) + def _extract_graphics(graphics, format, evaluation): graphics_box = Expression("MakeBoxes", graphics).evaluate(evaluation) builtin = GraphicsBox(expression=False) + elements, calc_dimensions = builtin._prepare_elements( graphics_box.leaves, {"evaluation": evaluation}, neg_y=True ) xmin, xmax, ymin, ymax, _, _, _, _ = calc_dimensions() - # xmin, xmax have always been moved to 0 here. the untransformed - # and unscaled bounds are found in elements.xmin, elements.ymin, - # elements.extent_width, elements.extent_height. - - # now compute the position of origin (0, 0) in the transformed - # coordinate space. - - ex = elements.extent_width - ey = elements.extent_height - - sx = (xmax - xmin) / ex - sy = (ymax - ymin) / ey - - ox = -elements.xmin * sx + xmin - oy = -elements.ymin * sy + ymin + if not isinstance(elements.elements[0], GeometricTransformationBox): + raise ValueError('expected GeometricTransformationBox') + # mmatera: in master, contents = elements.elements. Not sure what is de best + contents = elements.elements[0].contents + # generate code for svg or asy. if format == "asy": @@ -322,9 +346,127 @@ def _extract_graphics(graphics, format, evaluation): else: raise NotImplementedError - return xmin, xmax, ymin, ymax, ox, oy, ex, ey, code + return code + +# This function and class were defined by poke1024. +def _to_float(x): + if isinstance(x, Integer): + return x.get_int_value() + else: + y = x.round_to_float() + if y is None: + raise BoxConstructError + return y + + +class _Transform: + def __init__(self, f): + if not isinstance(f, Expression): + self.matrix = f + return + + if f.get_head_name() != 'System`TransformationFunction': + raise BoxConstructError + + if len(f.leaves) != 1 or f.leaves[0].get_head_name() != 'System`List': + raise BoxConstructError + + rows = f.leaves[0].leaves + if len(rows) != 3: + raise BoxConstructError + if any(row.get_head_name() != 'System`List' for row in rows): + raise BoxConstructError + if any(len(row.leaves) != 3 for row in rows): + raise BoxConstructError + + self.matrix = [[_to_float(x) for x in row.leaves] for row in rows] + + def combine(self, transform0): + if isinstance(transform0, _Transform): + return self.multiply(transform0) + else: + t = self + + def combined(*p, w=1): + return transform0(*t(*p, w=w), w=w) + return combined + + def inverse(self): + return _Transform(Matrix(self.matrix).inv().tolist()) + + def multiply(self, other): + a = self.matrix + b = other.matrix + return _Transform([[sum(a[i][k] * b[k][j] for k in range(3)) for j in range(3)] for i in range(3)]) + + def __call__(self, *p, w=1): + m = self.matrix + m11 = m[0][0] + m12 = m[0][1] + m13 = m[0][2] + m21 = m[1][0] + m22 = m[1][1] + m23 = m[1][2] + + if w == 1: + for x, y in p: + yield m11 * x + m12 * y + m13, m21 * x + m22 * y + m23 + elif w == 0: + for x, y in p: + yield m11 * x + m12 * y, m21 * x + m22 * y + else: + raise NotImplementedError("w not in (0, 1)") + + def to_svg(self, svg): + m = self.matrix + + a = m[0][0] + b = m[1][0] + c = m[0][1] + d = m[1][1] + e = m[0][2] + f = m[1][2] + + if m[2][0] != 0. or m[2][1] != 0. or m[2][2] != 1.: + raise BoxConstructError + + # a c e + # b d f + # 0 0 1 + + t = 'matrix(%f, %f, %f, %f, %f, %f)' % (a, b, c, d, e, f) + return '%s' % (t, svg) + + def to_asy(self, asy): + m = self.matrix + + a = m[0][0] + b = m[1][0] + c = m[0][1] + d = m[1][1] + e = m[0][2] + f = m[1][2] + + if m[2][0] != 0. or m[2][1] != 0. or m[2][2] != 1.: + raise BoxConstructError + + # a c e + # b d f + # 0 0 1 + # see http://asymptote.sourceforge.net/doc/Transforms.html#Transforms + t = ','.join(map(asy_number, (e, f, a, c, b, d))) + + return ''.join(("add((", t, ")*(new picture(){", + "picture s=currentpicture,t=new picture;currentpicture=t;", asy, + "currentpicture=s;return t;})());")) + + +def _no_transform(*p, w=None): + return p + +# This is what we have now in master class _SVGTransform: def __init__(self): self.transforms = [] @@ -348,6 +490,7 @@ def apply(self, svg): return '%s' % (" ".join(self.transforms), svg) + class _ASYTransform: _template = """ add(%s * (new picture() { @@ -360,10 +503,19 @@ class _ASYTransform: })()); """ - def __init__(self): - self.transforms = [] + def to_svg(self, svg): + m = self.matrix + + a = m[0][0] + b = m[1][0] + c = m[0][1] + d = m[1][1] + e = m[0][2] + f = m[1][2] + + if m[2][0] != 0. or m[2][1] != 0. or m[2][2] != 1.: + raise BoxConstructError - def matrix(self, a, b, c, d, e, f): # a c e # b d f # 0 0 1 @@ -458,9 +610,13 @@ class Graphics(Builtin): . \begin{asy} . usepackage("amsmath"); . size(5.8556cm, 5.8333cm); - . draw(ellipse((175,175),175,175), rgb(0, 0, 0)+linewidth(0.66667)); + . add((175,175,175,0,0,175)*(new picture(){picture s=currentpicture,t=new picture;currentpicture=t;draw(ellipse((0,0),1,1), rgb(0, 0, 0)+linewidth(0.0038095));currentpicture=s;return t;})()); . clip(box((-0.33333,0.33333), (350.33,349.67))); . \end{asy} + + Invalid graphics directives yield invalid box structures: + >> Graphics[Circle[{a, b}]] + : GraphicsBox[CircleBox[List[a, b]], Rule[$OptionSyntax, Ignore], Rule[AspectRatio, Automatic], Rule[Axes, False], Rule[AxesStyle, List[]], Rule[Background, Automatic], Rule[ImageSize, Automatic], Rule[LabelStyle, List[]], Rule[PlotRange, Automatic], Rule[PlotRangePadding, Automatic], Rule[TicksStyle, List[]], Rule[Transformation, Automatic]] is not a valid box structure. """ options = GRAPHICS_OPTIONS @@ -480,6 +636,8 @@ def convert(content): return Expression( "StyleBox", *[convert(item) for item in content.leaves] ) + elif head == "System`GeometricTransformation" and len(content.leaves) == 2: + return Expression("GeometricTransformationBox", convert(content.leaves[0]), content.leaves[1]) if head in element_heads: if head == "System`Text": @@ -562,8 +720,8 @@ def init(self, item=None, components=None): # become RGBColor[0, 0, 0, 1]. does not seem the right thing # to do in this general context. poke1024 - if len(components) < 3: - components.extend(self.default_components[len(components) :]) + # if len(components) < 3: + # components.extend(self.default_components[len(components) :]) self.components = components else: @@ -1082,7 +1240,7 @@ class PointSize(_Size): """ def get_size(self): - return self.graphics.view_width * self.value + return self.graphics.extent_width * self.value class FontColor(Builtin): @@ -1096,6 +1254,54 @@ class FontColor(Builtin): pass +class FontSize(_GraphicsElement): + """ +
+
'FontSize[$s$]' +
sets the font size to $s$ printer's points. +
+ """ + + def init(self, graphics, item=None, value=None): + super(FontSize, self).init(graphics, item) + + self.scaled = False + if item is not None and len(item.leaves) == 1: + if item.leaves[0].get_head_name() == 'System`Scaled': + scaled = item.leaves[0] + if len(scaled.leaves) == 1: + self.scaled = True + self.value = scaled.leaves[0].round_to_float() + + if self.scaled: + pass + elif item is not None: + self.value = item.leaves[0].round_to_float() + elif value is not None: + self.value = value + else: + raise BoxConstructError + + if self.value < 0: + raise BoxConstructError + + def get_size(self): + if self.scaled: + if self.graphics.extent_width is None: + return 1. + else: + return self.graphics.extent_width * self.value + else: + if self.graphics.extent_width is None or self.graphics.pixel_width is None: + return 1. + else: + return (96. / 72.) * (self.value * self.graphics.extent_width / self.graphics.pixel_width) + + +class Scaled(Builtin): + pass + + class Offset(Builtin): pass @@ -1202,27 +1408,43 @@ def init(self, graphics, style, item): super(RectangleBox, self).init(graphics, item, style) if len(item.leaves) not in (1, 2): raise BoxConstructError - self.edge_color, self.face_color = style.get_style(_Color, face_element=True) + self.edge_color, self.face_color = style.get_style( + _Color, face_element=True) + self.p1 = Coords(graphics, item.leaves[0]) + # Poke1024 used tuples for coordinates. Now we are using a specific class if len(item.leaves) == 1: - self.p2 = self.p1.add(1, 1) + self.p2 = self.p1 + self.p2.add(1,1) elif len(item.leaves) == 2: self.p2 = Coords(graphics, item.leaves[1]) def extent(self): l = self.style.get_line_width(face_element=True) / 2 result = [] + +# This was what poke1024 did instead of the next loop: +# tx1, ty1 = self.p1 +# tx2, ty2 = self.p2 +# x1 = min(tx1, tx2) - l +# x2 = max(tx1, tx2) + l +# y1 = min(ty1, ty2) - l +# y2 = max(ty1, ty2) + l +# +# result.extend([(x1, y1), (x1, y2), (x2, y1), (x2, y2)]) + for p in [self.p1, self.p2]: - x, y = p.pos() + x, y = p.p #pos() result.extend( [(x - l, y - l), (x - l, y + l), (x + l, y - l), (x + l, y + l)] ) + return result - def to_svg(self): + def to_svg(self, transform): l = self.style.get_line_width(face_element=True) - x1, y1 = self.p1.pos() - x2, y2 = self.p2.pos() + x1, y1 = self.p1.p # pos() + x2, y2 = self.p2.p # pos() xmin = min(x1, x2) ymin = min(y1, y2) w = max(x1, x2) - xmin @@ -1236,10 +1458,10 @@ def to_svg(self): style, ) - def to_asy(self): + def to_asy(self, transform): l = self.style.get_line_width(face_element=True) - x1, y1 = self.p1.pos() - x2, y2 = self.p2.pos() + x1, y1 = self.p1.p #.pos() + x2, y2 = self.p2.p #.pos() pens = create_pens(self.edge_color, self.face_color, l, is_face_element=True) x1, x2, y1, y2 = asy_number(x1), asy_number(x2), asy_number(y1), asy_number(y2) return "filldraw((%s,%s)--(%s,%s)--(%s,%s)--(%s,%s)--cycle, %s);" % ( @@ -1279,19 +1501,20 @@ def init(self, graphics, style, item): def extent(self): l = self.style.get_line_width(face_element=self.face_element) / 2 - x, y = self.c.pos() - rx, ry = self.r.pos() + x, y = self.c.p #pos() + rx, ry = self.r.p #pos() rx -= x ry = y - ry rx += l ry += l return [(x - rx, y - ry), (x - rx, y + ry), (x + rx, y - ry), (x + rx, y + ry)] - def to_svg(self): - x, y = self.c.pos() - rx, ry = self.r.pos() + def to_svg(self, transform): + c, r = transform(self.c.p, self.r.p) + x, y = c + rx, ry = r rx -= x - ry = y - ry + ry = abs(y - ry) l = self.style.get_line_width(face_element=self.face_element) style = create_css(self.edge_color, self.face_color, stroke_width=l) return '' % ( @@ -1302,11 +1525,12 @@ def to_svg(self): style, ) - def to_asy(self): - x, y = self.c.pos() - rx, ry = self.r.pos() + def to_asy(self, transform): + c, r = transform(self.c, self.r) + x, y = c.p + rx, ry = r.p rx -= x - ry -= y + ry = abs(ry - y) l = self.style.get_line_width(face_element=self.face_element) pen = create_pens( edge_color=self.edge_color, @@ -1352,9 +1576,10 @@ def init(self, graphics, style, item): self.arc = None super(_ArcBox, self).init(graphics, style, item) - def _arc_params(self): - x, y = self.c.pos() - rx, ry = self.r.pos() + def _arc_params(self, transform): + c, r = transform(self.c, self.r) + x, y = c + rx, ry = r rx -= x ry -= y @@ -1374,11 +1599,11 @@ def _arc_params(self): return x, y, abs(rx), abs(ry), sx, sy, ex, ey, large_arc - def to_svg(self): + def to_svg(self, transform): if self.arc is None: - return super(_ArcBox, self).to_svg() + return super(_ArcBox, self).to_svg(transform) - x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params() + x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params(transform) def path(closed): if closed: @@ -1396,11 +1621,11 @@ def path(closed): style = create_css(self.edge_color, self.face_color, stroke_width=l) return '' % (" ".join(path(self.face_element)), style) - def to_asy(self): + def to_asy(self, transform): if self.arc is None: - return super(_ArcBox, self).to_asy() + return super(_ArcBox, self).to_asy(transform) - x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params() + x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params(transform) def path(closed): if closed: @@ -1454,9 +1679,18 @@ def do_init(self, graphics, points): lines.append(leaf.leaves) else: raise BoxConstructError - self.lines = [ - [graphics.coords(graphics, point) for point in line] for line in lines - ] + + make_coords = graphics.make_coords # for Graphics and Graphics3D support + # I am not sure about why the next line is needed for the next one, + # but it seems to work.... + self.lines = [] + lines = [make_coords(graphics, line) for line in lines] + self.lines = lines + +# self.lines = [ +# [graphics.coords(graphics, point) for point in line] for line in lines +# ] + def extent(self): l = self.style.get_line_width(face_element=False) @@ -1507,33 +1741,39 @@ def init(self, graphics, style, item=None): else: raise BoxConstructError - def to_svg(self): + def to_svg(self, transform): point_size, _ = self.style.get_style(PointSize, face_element=False) if point_size is None: point_size = PointSize(self.graphics, value=0.005) size = point_size.get_size() - style = create_css( edge_color=self.edge_color, stroke_width=0, face_color=self.face_color ) svg = "" + graphics = self.graphics + size_x = size + size_y = size_x * (graphics.extent_height / graphics.extent_width) * (graphics.pixel_width / graphics.pixel_height) + for line in self.lines: - for coords in line: - svg += '' % ( - coords.pos()[0], - coords.pos()[1], - size, - style, - ) + for x, y in transform(*line): + svg += '' % ( + x, y, size_x, size_y, style) +# for line in self.lines: +# for coords in line: +# svg += '' % ( +# coords.pos()[0], +# coords.pos()[1], +# size, +# style, +# ) return svg - def to_asy(self): + def to_asy(self, transform): pen = create_pens(face_color=self.face_color, is_face_element=False) - asy = "" for line in self.lines: - for coords in line: - asy += "dot(%s, %s);" % (coords.pos(), pen) + for x, y in transform(*line): + asy += 'dot(%s, %s);' % ((x, y), pen) return asy @@ -1571,24 +1811,35 @@ def init(self, graphics, style, item=None, lines=None): else: raise BoxConstructError - def to_svg(self): + def to_svg(self, transform): l = self.style.get_line_width(face_element=False) + l = list(transform((l, l), w=0))[0][0] style = create_css(edge_color=self.edge_color, stroke_width=l) svg = "" + graphics = self.graphics for line in self.lines: - svg += '' % ( - " ".join(["%f,%f" % coords.pos() for coords in line]), - style, - ) + tc = transform(*[r.p for r in line]) + path = ' '.join(['%f,%f' % c for c in tc]) + svg += '' % (path, style) + + #for line in self.lines: + # svg += '' % ( + # " ".join(["%f,%f" % coords.pos() for coords in line]), + # style, + # ) return svg - def to_asy(self): + def to_asy(self, transform): l = self.style.get_line_width(face_element=False) + l = list(transform((l, l), w=0))[0][0] pen = create_pens(edge_color=self.edge_color, stroke_width=l) asy = "" for line in self.lines: - path = "--".join(["(%.5g,%5g)" % coords.pos() for coords in line]) - asy += "draw(%s, %s);" % (path, pen) + path = '--'.join(['(%.5g,%5g)' % c for c in transform(*line)]) + asy += 'draw(%s, %s);' % (path, pen) +# for line in self.lines: +# path = "--".join(["(%.5g,%5g)" % coords.pos() for coords in line]) +# asy += "draw(%s, %s);" % (path, pen) return asy @@ -1727,26 +1978,30 @@ def init(self, graphics, style, item, options): raise BoxConstructError self.spline_degree = spline_degree.get_int_value() - def to_svg(self): + def to_svg(self, transform): l = self.style.get_line_width(face_element=False) style = create_css(edge_color=self.edge_color, stroke_width=l) svg = "" for line in self.lines: - s = " ".join(_svg_bezier((self.spline_degree, [xy.pos() for xy in line]))) + s = " ".join(_svg_bezier((self.spline_degree, transform(*line)))) + # s = " ".join(_svg_bezier((self.spline_degree, [xy.pos() for xy in line]))) svg += '' % (s, style) return svg - def to_asy(self): + def to_asy(self, transform): l = self.style.get_line_width(face_element=False) pen = create_pens(edge_color=self.edge_color, stroke_width=l) asy = "" for line in self.lines: - for path in _asy_bezier((self.spline_degree, [xy.pos() for xy in line])): - if path[:2] == "..": - path = "(0.,0.)" + path - asy += "draw(%s, %s);" % (path, pen) + for path in _asy_bezier((self.spline_degree, transform(*line))): + asy += 'draw(%s, %s);' % (path, pen) +# for path in _asy_bezier((self.spline_degree, [xy.pos() for xy in line])): +# if path[:2] == "..": +# path = "(0.,0.)" + path +# asy += "draw(%s, %s);" % (path, pen) + return asy @@ -1796,16 +2051,13 @@ def parse_component(segments): else: raise BoxConstructError - coords = [] - + c = [] for part in parts: if part.get_head_name() != "System`List": raise BoxConstructError - coords.extend( - [graphics.coords(graphics, xy) for xy in part.leaves] - ) + c.extend([Coords(graphics, xy) for xy in part.leaves]) - yield k, coords + yield k, c if all(x.get_head_name() == "System`List" for x in leaves): self.components = [list(parse_component(x)) for x in leaves] @@ -1814,7 +2066,7 @@ def parse_component(segments): else: raise BoxConstructError - def to_svg(self): + def to_svg(self, transform): l = self.style.get_line_width(face_element=False) style = create_css( edge_color=self.edge_color, face_color=self.face_color, stroke_width=l @@ -1822,15 +2074,19 @@ def to_svg(self): def components(): for component in self.components: - transformed = [(k, [xy.pos() for xy in p]) for k, p in component] - yield " ".join(_svg_bezier(*transformed)) + " Z" + + transformed = [(k, transform(*p)) for k, p in component] + yield ' '.join(_svg_bezier(*transformed)) + ' Z' +# transformed = [(k, [xy.pos() for xy in p]) for k, p in component] +# yield " ".join(_svg_bezier(*transformed)) + " Z" + return '' % ( " ".join(components()), style, ) - def to_asy(self): + def to_asy(self, transform): l = self.style.get_line_width(face_element=False) pen = create_pens(edge_color=self.edge_color, stroke_width=l) @@ -1839,8 +2095,12 @@ def to_asy(self): def components(): for component in self.components: - transformed = [(k, [xy.pos() for xy in p]) for k, p in component] - yield "fill(%s--cycle, %s);" % ("".join(_asy_bezier(*transformed)), pen) + + transformed = [(k, transform(*p)) for k, p in component] + yield 'fill(%s--cycle, %s);' % (''.join(_asy_bezier(*transformed)), pen) +# transformed = [(k, [xy.pos() for xy in p]) for k, p in component] +# yield "fill(%s--cycle, %s);" % ("".join(_asy_bezier(*transformed)), pen) + return "".join(components()) @@ -1851,9 +2111,7 @@ def extent(self): for _, points in component: for p in points: x, y = p.pos() - result.extend( - [(x - l, y - l), (x - l, y + l), (x + l, y - l), (x + l, y + l)] - ) + result.extend([(x - l, y - l), (x - l, y + l), (x + l, y - l), (x + l, y + l)]) return result @@ -1919,7 +2177,7 @@ def process_option(self, name, value): else: raise BoxConstructError - def to_svg(self): + def to_svg(self, transform): l = self.style.get_line_width(face_element=True) if self.vertex_colors is None: face_color = self.face_color @@ -1932,20 +2190,26 @@ def to_svg(self): if self.vertex_colors is not None: mesh = [] for index, line in enumerate(self.lines): + tc = transform(*[r.p for r in line]) data = [ - [coords.pos(), color.to_js()] - for coords, color in zip(line, self.vertex_colors[index]) - ] + [coords, color.to_js()] for coords, color in zip( + tc, self.vertex_colors[index]) +# [coords.pos(), color.to_js()] for coords, color in zip( +# line, self.vertex_colors[index]) + ] + mesh.append(data) svg += '' % json.dumps(mesh) for line in self.lines: + tc = transform(*[r.p for r in line]) svg += '' % ( - " ".join("%f,%f" % coords.pos() for coords in line), + " ".join('%f,%f' % c for c in tc), +# " ".join("%f,%f" % coords.pos() for coords in line), style, ) return svg - def to_asy(self): + def to_asy(self, transform): l = self.style.get_line_width(face_element=True) if self.vertex_colors is None: face_color = self.face_color @@ -1963,9 +2227,11 @@ def to_asy(self): colors = [] edges = [] for index, line in enumerate(self.lines): + paths.append( - "--".join(["(%.5g,%.5g)" % coords.pos() for coords in line]) - + "--cycle" + "--".join([ + "(%.5g,%.5g)" % c for c in transform(*line)]) + "--cycle" +# "(%.5g,%.5g)" % coords.pos() for coords in line])+ "--cycle" ) # ignore opacity @@ -1984,8 +2250,11 @@ def to_asy(self): ) if pens and pens != "nullpen": for line in self.lines: + path = ( - "--".join(["(%.5g,%.5g)" % coords.pos() for coords in line]) + '--'.join( + ['(%.5g,%.5g)' % c for c in transform(*line)]) +# ["(%.5g,%.5g)" % coords.pos() for coords in line]) + "--cycle" ) asy += "filldraw(%s, %s);" % (path, pens) @@ -2189,8 +2458,8 @@ def heads(self, extent, default_arrow, custom_arrow): def _norm(p, q): - px, py = p - qx, qy = q + px, py = p.p + qx, qy = q.p dx = qx - px dy = qy - py @@ -2219,8 +2488,8 @@ def draw(points): def arrows(self, points, heads): # heads has to be sorted by pos def segments(points): for i in range(len(points) - 1): - px, py = points[i] - dx, dy, dl = _norm((px, py), points[i + 1]) + px, py = points[i].p + dx, dy, dl = _norm(points[i], points[i + 1]) yield dl, px, py, dx, dy seg = list(segments(points)) @@ -2446,7 +2715,7 @@ def shrink_one_end(line, s): while s > 0.0: if len(line) < 2: return [] - xy, length = setback(line[0].p, line[1].p, s) + xy, length = setback(line[0], line[1], s) if xy is not None: line[0] = line[0].add(*xy) else: @@ -2468,7 +2737,7 @@ def shrink(line, s1, s2): # note that shrinking needs to happen in the Graphics[] coordinate space, whereas the # subsequent position calculation needs to happen in pixel space. - transformed_points = [xy.pos() for xy in shrink(line, *self.setback)] + transformed_points = shrink(line, *self.setback) for s in polyline(transformed_points): yield s @@ -2476,31 +2745,39 @@ def shrink(line, s1, s2): for s in self.curve.arrows(transformed_points, heads): yield s - def _custom_arrow(self, format, format_transform): + def _custom_arrow(self, format, transform): def make(graphics): - xmin, xmax, ymin, ymax, ox, oy, ex, ey, code = _extract_graphics( - graphics, format, self.graphics.evaluation - ) - boxw = xmax - xmin - boxh = ymax - ymin + code = _extract_graphics( + graphics, format, self.graphics.evaluation) + + half_pi = pi / 2. +# xmin, xmax, ymin, ymax, ox, oy, ex, ey, code = _extract_graphics( +# graphics, format, self.graphics.evaluation +# ) +# boxw = xmax - xmin +# boxh = ymax - ymin def draw(px, py, vx, vy, t1, s): t0 = t1 - cx = px + t0 * vx - cy = py + t0 * vy - transform = format_transform() - transform.translate(cx, cy) - transform.scale(-s / boxw * ex, -s / boxh * ey) - transform.rotate(90 + degrees(atan2(vy, vx))) - transform.translate(-ox, -oy) - yield transform.apply(code) + tx = px + t0 * vx + ty = py + t0 * vy + + r = half_pi + atan2(vy, vx) + + s = -s + + cos_r = cos(r) + sin_r = sin(r) + + # see TranslationTransform[{tx,ty}].ScalingTransform[{s,s}].RotationTransform[r] + yield transform([[s * cos_r, -s * sin_r, tx], [s * sin_r, s * cos_r, ty], [0, 0, 1]], code) return draw return make - def to_svg(self): + def to_svg(self, transform): width = self.style.get_line_width(face_element=False) style = create_css(edge_color=self.edge_color, stroke_width=width) polyline = self.curve.make_draw_svg(style) @@ -2509,15 +2786,18 @@ def to_svg(self): def polygon(points): yield '' % arrow_style - extent = self.graphics.view_width or 0 + def svg_transform(m, code): + return _Transform(m).to_svg(code) + + extent = self.graphics.extent_width or 0 default_arrow = self._default_arrow(polygon) - custom_arrow = self._custom_arrow("svg", _SVGTransform) + custom_arrow = self._custom_arrow('svg', svg_transform) return "".join(self._draw(polyline, default_arrow, custom_arrow, extent)) - def to_asy(self): + def to_asy(self, transform): width = self.style.get_line_width(face_element=False) pen = create_pens(edge_color=self.edge_color, stroke_width=width) polyline = self.curve.make_draw_asy(pen) @@ -2525,21 +2805,24 @@ def to_asy(self): arrow_pen = create_pens(face_color=self.edge_color, stroke_width=width) def polygon(points): - yield "filldraw(" - yield "--".join(["(%.5g,%5g)" % xy for xy in points]) - yield "--cycle, % s);" % arrow_pen + yield 'filldraw(' + yield '--'.join(['(%.5g,%5g)' % xy for xy in transform(*points)]) + yield '--cycle, % s);' % arrow_pen + + def asy_transform(m, code): + return _Transform(m).to_asy(code) - extent = self.graphics.view_width or 0 + extent = self.graphics.extent_width or 0 default_arrow = self._default_arrow(polygon) - custom_arrow = self._custom_arrow("asy", _ASYTransform) - return "".join(self._draw(polyline, default_arrow, custom_arrow, extent)) + custom_arrow = self._custom_arrow('asy', asy_transform) + return ''.join(self._draw(polyline, default_arrow, custom_arrow, extent)) def extent(self): width = self.style.get_line_width(face_element=False) def polyline(points): for p in points: - x, y = p + x, y = p.p yield x - width, y - width yield x - width, y + width yield x + width, y - width @@ -2555,14 +2838,196 @@ def default_arrow(px, py, vx, vy, t1, s): return list(self._draw(polyline, default_arrow, None, 0)) +class TransformationFunction(Builtin): + """ + >> RotationTransform[Pi].TranslationTransform[{1, -1}] + = TransformationFunction[{{-1, 0, -1}, {0, -1, 1}, {0, 0, 1}}] + + >> TranslationTransform[{1, -1}].RotationTransform[Pi] + = TransformationFunction[{{-1, 0, 1}, {0, -1, -1}, {0, 0, 1}}] + """ + + rules = { + 'Dot[TransformationFunction[a_], TransformationFunction[b_]]': 'TransformationFunction[a . b]', + 'TransformationFunction[m_][v_]': 'Take[m . Join[v, {0}], Length[v]]', + } + + +class TranslationTransform(Builtin): + """ +
+
'TranslationTransform[v]' +
gives the translation by the vector $v$. +
+ + >> TranslationTransform[{1, 2}] + = TransformationFunction[{{1, 0, 1}, {0, 1, 2}, {0, 0, 1}}] + """ + + rules = { + 'TranslationTransform[v_]': + 'TransformationFunction[IdentityMatrix[Length[v] + 1] + ' + '(Join[ConstantArray[0, Length[v]], {#}]& /@ Join[v, {0}])]', + } + + +class RotationTransform(Builtin): + rules = { + 'RotationTransform[phi_]': + 'TransformationFunction[{{Cos[phi], -Sin[phi], 0}, {Sin[phi], Cos[phi], 0}, {0, 0, 1}}]', + 'RotationTransform[phi_, p_]': + 'TranslationTransform[-p] . RotationTransform[phi] . TranslationTransform[p]', + } + + +class ScalingTransform(Builtin): + rules = { + 'ScalingTransform[v_]': + 'TransformationFunction[DiagonalMatrix[Join[v, {1}]]]', + 'ScalingTransform[v_, p_]': + 'TranslationTransform[-p] . ScalingTransform[v] . TranslationTransform[p]', + } + + +class Translate(Builtin): + """ +
+
'Translate[g, {x, y}]' +
translates an object by the specified amount. +
'Translate[g, {{x1, y1}, {x2, y2}, ...}]' +
creates multiple instances of object translated by the specified amounts. +
+ + >> Graphics[{Circle[], Translate[Circle[], {1, 0}]}] + = -Graphics- + """ + + rules = { + 'Translate[g_, v_?(Depth[#] > 2&)]': 'GeometricTransformation[g, TranslationTransform /@ v]', + 'Translate[g_, v_?(Depth[#] == 2&)]': 'GeometricTransformation[g, TranslationTransform[v]]', + } + + +class Rotate(Builtin): + """ +
+
'Rotate[g, phi]' +
rotates an object by the specified amount. +
+ + >> Graphics[Rotate[Rectangle[], Pi / 3]] + = -Graphics- + + >> Graphics[{Rotate[Rectangle[{0, 0}, {0.2, 0.2}], 1.2, {0.1, 0.1}], Red, Disk[{0.1, 0.1}, 0.05]}] + = -Graphics- + + >> Graphics[Table[Rotate[Scale[{RGBColor[i,1-i,1],Rectangle[],Black,Text["ABC",{0.5,0.5}]},1-i],Pi*i], {i,0,1,0.2}]] + = -Graphics- + """ + + rules = { + 'Rotate[g_, phi_]': 'GeometricTransformation[g, RotationTransform[phi]]', + 'Rotate[g_, phi_, p_]': 'GeometricTransformation[g, RotationTransform[phi, p]]', + } + + +class Scale(Builtin): + """ +
+
'Scale[g, phi]' +
scales an object by the specified amount. +
+ + >> Graphics[Rotate[Rectangle[], Pi / 3]] + = -Graphics- + + >> Graphics[{Scale[Rectangle[{0, 0}, {0.2, 0.2}], 3, {0.1, 0.1}], Red, Disk[{0.1, 0.1}, 0.05]}] + = -Graphics- + """ + + rules = { + 'Scale[g_, s_?ListQ]': 'GeometricTransformation[g, ScalingTransform[s]]', + 'Scale[g_, s_]': 'GeometricTransformation[g, ScalingTransform[{s, s}]]', + 'Scale[g_, s_?ListQ, p_]': 'GeometricTransformation[g, ScalingTransform[s, p]]', + 'Scale[g_, s_, p_]': 'GeometricTransformation[g, ScalingTransform[{s, s}, p]]', + } + + +class GeometricTransformation(Builtin): + """ +
+
'GeometricTransformation[$g$, $tfm$]' +
transforms an object $g$ with the transformation $tfm$. +
+ """ + pass + + +class GeometricTransformationBox(_GraphicsElement): + def init(self, graphics, style, contents, transform): + super(GeometricTransformationBox, self).init(graphics, None, style) + self.contents = contents + if transform.get_head_name() == 'System`List': + functions = transform.leaves + else: + functions = [transform] + evaluation = graphics.evaluation + self.transforms = [_Transform(Expression('N', f).evaluate(evaluation)) for f in functions] + self.precompute = graphics.precompute_transformations + + def patch_transforms(self, transforms): + self.transforms = transforms + + def extent(self): + def points(): + for content in self.contents: + p = content.extent() + for transform in self.transforms: + for q in transform(*p): + yield q + return list(points()) + + def to_svg(self, transform0): + if self.precompute: + def instances(): + for transform in self.transforms: + t = transform.combine(transform0) + for content in self.contents: + yield content.to_svg(t) + else: + def instances(): + for content in self.contents: + content_svg = content.to_svg(transform0) + for transform in self.transforms: + res = transform.to_svg(content_svg) + yield res + return ''.join(instances()) + + def to_asy(self, transform0): + def instances(): + for content in self.contents: + content_asy = content.to_asy(transform0) + for transform in self.transforms: + yield transform.to_asy(content_asy) + return ''.join(instances()) + + class InsetBox(_GraphicsElement): - def init(self, graphics, style, item=None, content=None, pos=None, opos=(0, 0)): + def init(self, graphics, style, item=None, content=None, pos=None, + opos=(0, 0), font_size=None, is_absolute=False): super(InsetBox, self).init(graphics, item, style) self.color = self.style.get_option("System`FontColor") if self.color is None: self.color, _ = style.get_style(_Color, face_element=False) + if font_size is not None: + self.font_size = FontSize(self.graphics, value=font_size) + else: + self.font_size, _ = self.style.get_style(FontSize, face_element=False) + if self.font_size is None: + self.font_size = FontSize(self.graphics, value=10.) + if item is not None: if len(item.leaves) not in (1, 2, 3): raise BoxConstructError @@ -2571,41 +3036,143 @@ def init(self, graphics, style, item=None, content=None, pos=None, opos=(0, 0)): if len(item.leaves) > 1: self.pos = Coords(graphics, item.leaves[1]) else: - self.pos = Coords(graphics, pos=(0, 0)) + self.pos = (0, 0) if len(item.leaves) > 2: - self.opos = coords(item.leaves[2]) + self.opos = Coords(graphics, item.leaves[1]) else: - self.opos = (0, 0) + self.opos = Coords(graphics, pos=(0, 0)) else: self.content = content self.pos = pos self.opos = opos - self.content_text = self.content.boxes_to_text( - evaluation=self.graphics.evaluation - ) + + + self.is_absolute = is_absolute + + try: +# self.content_text = self.content.boxes_to_text( +# evaluation=self.graphics.evaluation +# ) # In HEAD + self._prepare_text_svg() + except WebEngineUnavailable as e: + self.svg = None + + self.content_text = self.content.boxes_to_text( + evaluation=self.graphics.evaluation) + + if self.graphics.evaluation.output.warn_about_web_engine(): + self.graphics.evaluation.message( + 'General', 'nowebeng', str(e), once=True) + except Exception as e: + self.svg = None + + self.graphics.evaluation.message( + 'General', 'nowebeng', str(e), once=True) def extent(self): - p = self.pos.pos() - h = 25 - w = len(self.content_text) * 7 # rough approximation by numbers of characters + p = self.pos.p + + if not self.svg: + h = 25 + if not hasattr(self, 'content_text'): + self.content_text = self.content.to_python() + w = len(self.content_text) * 7 # rough approximation by numbers of characters + else: + _, w, h = self.svg + scale = self._text_svg_scale(h) + w *= scale + h *= scale opos = self.opos x = p[0] - w / 2.0 - opos[0] * w / 2.0 y = p[1] - h / 2.0 + opos[1] * h / 2.0 return [(x, y), (x + w, y + h)] - def to_svg(self): - x, y = self.pos.pos() - content = self.content.boxes_to_xml(evaluation=self.graphics.evaluation) + def _prepare_text_svg(self): + self.graphics.evaluation.output.assume_web_engine() + + content = self.content.boxes_to_xml( + evaluation=self.graphics.evaluation) + + svg = self.graphics.evaluation.output.mathml_to_svg( + '%s' % content) + + svg = svg.replace('style', 'data-style', 1) # HACK + + # we could parse the svg and edit it. using regexps here should be + # a lot faster though. + + def extract_dimension(svg, name): + values = [0.] + + def replace(m): + value = m.group(1) + values.append(float(value)) + return '%s="%s"' % (name, value) + + svg = re.sub(name + r'="([0-9\.]+)ex"', replace, svg, 1) + return svg, values[-1] + + svg, width = extract_dimension(svg, 'width') + svg, height = extract_dimension(svg, 'height') + + self.svg = (svg, width, height) + + def _text_svg_scale(self, height): + size = self.font_size.get_size() + return size / height + + def _text_svg_xml(self, style, x, y, absolute): + svg, width, height = self.svg + svg = re.sub(r'%s' % ( + x, + y, + scale * tx, + scale * ty, + -width / 2 - ox * width / 2, + -height / 2 + oy * height / 2, + svg) + + def to_svg(self, transform): + evaluation = self.graphics.evaluation + x, y = transform(self.pos.p)[0] + + content = self.content.boxes_to_xml( + evaluation=evaluation) style = create_css(font_color=self.color) - svg = ( - '' - "%s" - ) % (x, y, self.opos[0], self.opos[1], style, content) + + is_absolute = self.is_absolute + + if not self.svg: + if not is_absolute: + x, y = list(self.graphics.local_to_screen((x, y)))[0] + + svg = ( + '' + '%s') % ( + x, y, self.opos[0], self.opos[1], style, content) + + if not is_absolute: + svg = self.graphics.inverse_local_to_screen.to_svg(svg) + else: + svg = self._text_svg_xml(style, x, y, is_absolute) + return svg - def to_asy(self): - x, y = self.pos.pos() - content = self.content.boxes_to_tex(evaluation=self.graphics.evaluation) + def to_asy(self, transform): +# x, y = self.pos.pos() + x, y = transform(self.pos.pos())[0] + content = self.content.boxes_to_tex( + evaluation=self.graphics.evaluation) pen = create_pens(edge_color=self.color) asy = 'label("$%s$", (%s,%s), (%s,%s), %s);' % ( content, @@ -2741,7 +3308,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, consider_forms=face_element @@ -2750,6 +3317,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: @@ -2767,7 +3347,7 @@ def _flatten(leaves): class _GraphicsElements(object): def __init__(self, content, evaluation): self.evaluation = evaluation - self.elements = [] + self.web_engine_warning_issued = False builtins = evaluation.definitions.builtin @@ -2817,7 +3397,9 @@ def convert(content, style): item.leaves[0], stylebox_style(style, item.leaves[1:]) ): yield element - elif head[-3:] == "Box": # and head[:-3] in element_heads: + 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: options = get_options(head[:-3]) @@ -2838,6 +3420,10 @@ def convert(content, style): self.elements = list(convert(content, self.get_style_class()(self))) + def make_coords(self, graphics, points): # overriden by Graphics3DElements + #return [expr_to_coords(p) for p in points] + return [Coords(graphics, p) for p in points] + def create_style(self, expr): style = self.get_style_class()(self) @@ -2856,41 +3442,90 @@ def get_style_class(self): class GraphicsElements(_GraphicsElements): - coords = Coords - - def __init__(self, content, evaluation, neg_y=False): + def __init__(self, content, evaluation, neg_y=False, precompute_transformations=False): + self.precompute_transformations = precompute_transformations 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): + # this is in master + # return '\n'.join(element.to_svg(cut) for element in self.elements) + self.pixel_width = pixel_width + self.pixel_height = pixel_height + 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() + self.text_rescale = (1., -1. if self.neg_y else 1.) + + 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: + if self.local_to_screen: # this if is in poke1024's + return list(self.local_to_screen(coords))[0] + elif self.pixel_width is not None: # this is in master w = self.extent_width if self.extent_width > 0 else 1 h = self.extent_height if self.extent_height > 0 else 1 + x, y = coords result = [ - (coords[0] - self.xmin) * self.pixel_width / w, - (coords[1] - self.ymin) * self.pixel_height / h, + (x - self.xmin) * self.pixel_width / w, + (y - self.ymin) * self.pixel_height / h, ] if self.neg_y: result[1] = self.pixel_height - result[1] return tuple(result) else: - return (coords[0], coords[1]) + return coords 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: @@ -2917,18 +3552,23 @@ def extent(self, completely_visible_only=False): return xmin, xmax, ymin, ymax def to_svg(self): - return "\n".join(element.to_svg() for element in self.elements) + border = 10 ** 8 - def to_asy(self): - return "\n".join(element.to_asy() for element in self.elements) + def cut_coords(xy): + "Cut values in graphics primitives (not displayed otherwise in SVG)" + return min(max(xy[0], -border), border), min(max(xy[1], -border), border) - def set_size( - self, xmin, ymin, extent_width, extent_height, pixel_width, pixel_height - ): + def cut_(*p, w=None): + result = [cut_coords(q) for q in p] + return result + + return '\n'.join(element.to_svg(cut_) for element in self.elements) + - 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 + + + def to_asy(self): + return '\n'.join(element.to_asy(_no_transform) for element in self.elements) class GraphicsBox(BoxConstruct): @@ -3015,7 +3655,15 @@ 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]]) + + precompute_transformations = graphics_options['System`Transformation'].get_string_value() == 'Precomputed' + + elements = GraphicsElements( + Expression('System`GeometricTransformationBox', leaves[0], transformation), + options['evaluation'], neg_y, precompute_transformations) + axes = [] # to be filled further down def calc_dimensions(final_pass=True): @@ -3148,16 +3796,15 @@ def boxes_to_tex(self, leaves, **options): ) xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() - elements.view_width = w asy_completely_visible = "\n".join( - element.to_asy() + element.to_asy(_no_transform) # in master, this goes without the parameter for element in elements.elements if element.is_completely_visible ) asy_regular = "\n".join( - element.to_asy() + element.to_asy(_no_transform) # the same for element in elements.elements if not element.is_completely_visible ) @@ -3199,7 +3846,6 @@ def boxes_to_xml(self, leaves, **options): elements, calc_dimensions = self._prepare_elements(leaves, options, neg_y=True) xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() - elements.view_width = w svg = elements.to_svg() @@ -3322,12 +3968,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) @@ -3337,6 +3988,9 @@ def add_element(element): tick_large_size = 5 tick_label_d = 2 + # hack: work around the local to screen scaling in class FontSize + font_size = tick_large_size * 2. / (elements.extent_width / elements.pixel_width) + ticks_x_int = all(floor(x) == x for x in ticks_x) ticks_y_int = all(floor(x) == x for x in ticks_y) @@ -3372,7 +4026,7 @@ def add_element(element): ticks_y_int, ), ] - ): + ): if axes[index]: add_element( LineBox( @@ -3408,14 +4062,15 @@ def add_element(element): content = String("%.1f" % x) # e.g. 1.0 (instead of 1.) else: content = String("%g" % x) # fix e.g. 0.6000000000000001 + tickpos = Coords( + elements, pos=p_origin(x), d=p_self0(-tick_label_d) + ) add_element( InsetBox( elements, tick_label_style, content=content, - pos=Coords( - elements, pos=p_origin(x), d=p_self0(-tick_label_d) - ), + pos=tickpos, opos=p_self0(1), ) ) @@ -3779,10 +4434,11 @@ class Automatic(Builtin): graphical options: >> Cases[Options[Plot], HoldPattern[_ :> Automatic]] - = {Background :> Automatic, Exclusions :> Automatic, ImageSize :> Automatic, MaxRecursion :> Automatic, PlotRange :> Automatic, PlotRangePadding :> Automatic} + = {Background :> Automatic, Exclusions :> Automatic, ImageSize :> Automatic, MaxRecursion :> Automatic, PlotRange :> Automatic, PlotRangePadding :> Automatic, Transformation :> Automatic} """ + class Tiny(Builtin): """
@@ -3818,7 +4474,6 @@ class Large(Builtin):
""" - element_heads = frozenset( system_symbols( "Rectangle", @@ -3838,6 +4493,7 @@ class Large(Builtin): ) ) + styles = system_symbols_dict( { "RGBColor": RGBColor, diff --git a/mathics/builtin/graphics3d.py b/mathics/builtin/graphics3d.py index 43f30ad687..f237d10e1e 100644 --- a/mathics/builtin/graphics3d.py +++ b/mathics/builtin/graphics3d.py @@ -114,21 +114,20 @@ class Graphics3D(Graphics): . import three; . import solids; . size(6.6667cm, 6.6667cm); - . currentprojection=perspective(2.6,-4.8,4.0); + . currentprojection=perspective(1.3,-2.4,2.0); . currentlight=light(rgb(0.5,0.5,1), specular=red, (2,0,2), (2,2,2), (0,2,2)); - . path3 g=(0,1,0)--(0.20791,0.97815,0)--(0.40674,0.91355,0)--(0.58779,0.80902,0)--(0.74314,0.66913,0)--(0.86603,0.5,0)--(0.95106,0.30902,0)--(0.99452,0.10453,0)--(0.99452,-0.10453,0)--(0.95106,-0.30902,0)--(0.86603,-0.5,0)--(0.74314,-0.66913,0)--(0.58779,-0.80902,0)--(0.40674,-0.91355,0)--(0.20791,-0.97815,0)--(5.6655e-16,-1,0)--(-0.20791,-0.97815,0)--(-0.40674,-0.91355,0)--(-0.58779,-0.80902,0)--(-0.74314,-0.66913,0)--(-0.86603,-0.5,0)--(-0.95106,-0.30902,0)--(-0.99452,-0.10453,0)--(-0.99452,0.10453,0)--(-0.95106,0.30902,0)--(-0.86603,0.5,0)--(-0.74314,0.66913,0)--(-0.58779,0.80902,0)--(-0.40674,0.91355,0)--(-0.20791,0.97815,0)--(1.5314e-15,1,0)--cycle;dot(g, rgb(0, 0, 0)); - . draw(((-0.99452,-1,-1)--(0.99452,-1,-1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); - . draw(((-0.99452,1,-1)--(0.99452,1,-1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); - . draw(((-0.99452,-1,1)--(0.99452,-1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); - . draw(((-0.99452,1,1)--(0.99452,1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); - . draw(((-0.99452,-1,-1)--(-0.99452,1,-1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); - . draw(((0.99452,-1,-1)--(0.99452,1,-1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); - . draw(((-0.99452,-1,1)--(-0.99452,1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); - . draw(((0.99452,-1,1)--(0.99452,1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); - . draw(((-0.99452,-1,-1)--(-0.99452,-1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); - . draw(((0.99452,-1,-1)--(0.99452,-1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); - . draw(((-0.99452,1,-1)--(-0.99452,1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); - . draw(((0.99452,1,-1)--(0.99452,1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); + . draw(((0,0,0)--(1,0,0)), rgb(0.4, 0.4, 0.4)+linewidth(1)); + . draw(((0,1,0)--(1,1,0)), rgb(0.4, 0.4, 0.4)+linewidth(1)); + . draw(((0,0,1)--(1,0,1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); + . draw(((0,1,1)--(1,1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); + . draw(((0,0,0)--(0,1,0)), rgb(0.4, 0.4, 0.4)+linewidth(1)); + . draw(((1,0,0)--(1,1,0)), rgb(0.4, 0.4, 0.4)+linewidth(1)); + . draw(((0,0,1)--(0,1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); + . draw(((1,0,1)--(1,1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); + . draw(((0,0,0)--(0,0,1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); + . draw(((1,0,0)--(1,0,1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); + . draw(((0,1,0)--(0,1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); + . draw(((1,1,0)--(1,1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); . \end{asy} """ @@ -763,8 +762,6 @@ def total_extent_3d(extents): class Graphics3DElements(_GraphicsElements): - coords = Coords3D - def __init__(self, content, evaluation, neg_y=False): super(Graphics3DElements, self).__init__(content, evaluation) self.neg_y = neg_y @@ -774,6 +771,10 @@ def __init__(self, content, evaluation, neg_y=False): self.pixel_width ) = self.pixel_height = self.extent_width = self.extent_height = None + def make_coords(self, points): + print("make_coords3d",points) + return [Coords3D(self, p) for p in points] + def extent(self, completely_visible_only=False): return total_extent_3d([element.extent() for element in self.elements]) @@ -799,6 +800,7 @@ def get_style_class(self): class Point3DBox(PointBox): def init(self, *args, **kwargs): + print("Point3DBox.init") super(Point3DBox, self).init(*args, **kwargs) def process_option(self, name, value): @@ -902,6 +904,7 @@ def _apply_boxscaling(self, boxscale): class Polygon3DBox(PolygonBox): def init(self, *args, **kwargs): self.vertex_normals = None + self.vertex_colors = None super(Polygon3DBox, self).init(*args, **kwargs) def process_option(self, name, value): diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index b8aa12ea64..9adca892e6 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -20,6 +20,7 @@ convert as convert_color, colorspaces as known_colorspaces, ) +#from mathics.layout.client import WebEngineError import base64 import functools @@ -2569,4 +2570,43 @@ def color_func( wc.generate_from_frequencies(freq) image = wc.to_image() - return Image(numpy.array(image), "RGB") + return Image(numpy.array(image), 'RGB') + + +class Rasterize(Builtin): + requires = _image_requires + + options = { + 'RasterSize': '300', + } + + def apply(self, expr, evaluation, options): + 'Rasterize[expr_, OptionsPattern[%(name)s]]' + + raster_size = self.get_option(options, 'RasterSize', evaluation) + if isinstance(raster_size, Integer): + s = raster_size.get_int_value() + py_raster_size = (s, s) + elif raster_size.has_form('List', 2) and all(isinstance(s, Integer) for s in raster_size.leaves): + py_raster_size = tuple(s.get_int_value for s in raster_size.leaves) + else: + return + + mathml = evaluation.format_output(expr, 'xml') + try: + svg = evaluation.output.mathml_to_svg(mathml) + png = evaluation.output.rasterize(svg, py_raster_size) + + stream = BytesIO() + stream.write(png) + stream.seek(0) + im = PIL.Image.open(stream) + # note that we need to get these pixels as long as stream is still open, + # otherwise PIL will generate an IO error. + pixels = numpy.array(im) + stream.close() + + return Image(pixels, 'RGB') + except WebEngineError as e: + evaluation.message( + 'General', 'nowebeng', 'Rasterize[] did not succeed: ' + str(e), once=True) diff --git a/mathics/builtin/inout.py b/mathics/builtin/inout.py index c8e20f50f3..dfe2751927 100644 --- a/mathics/builtin/inout.py +++ b/mathics/builtin/inout.py @@ -1878,6 +1878,7 @@ class General(Builtin): 'notboxes': "`1` is not a valid box structure.", 'pyimport': "`1`[] is not available. Your Python installation misses the \"`2`\" module.", + 'nowebeng': "Web Engine is not available: `1`", } diff --git a/mathics/builtin/plot.py b/mathics/builtin/plot.py index 5c409d7bef..34a4653c5d 100644 --- a/mathics/builtin/plot.py +++ b/mathics/builtin/plot.py @@ -818,6 +818,14 @@ def find_excl(excl): meshpoints = [Expression("List", xx, yy) for xx, yy in points] graphics.append(Expression("Point", Expression("List", *meshpoints))) + # we need the PrecomputeTransformations option here. to understand why, try Plot[1+x*0.000001, {x, 0, 1}] + # without it. in Graphics[], we set up a transformation that scales a very tiny area to a very large area. + # unfortunately, most browsers seem to have problems with scaling stroke width properly. since we scale a + # very tiny area, we specify a very small stroke width (e.g. 1e-6) which is then scaled. but most browsers + # simply round this stroke width to 0 before scaling, so we end up with an empty plot. in order to fix this, + # Transformation -> Precomputed simply gets rid of the SVG transformations and passes the scaled coordinates + # into the SVG. this also has the advantage that we can precompute with arbitrary precision using mpmath. + options['System`Transformation'] = String('Precomputed') return Expression( "Graphics", Expression("List", *graphics), *options_to_rules(options) ) @@ -1243,6 +1251,11 @@ def labels(names): ) + + + + + class Histogram(Builtin): """
diff --git a/mathics/builtin/tensors.py b/mathics/builtin/tensors.py index bdac582079..6e8082eae4 100644 --- a/mathics/builtin/tensors.py +++ b/mathics/builtin/tensors.py @@ -482,3 +482,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]', + } diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py index b78afe0928..b525100fec 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -13,8 +13,11 @@ from mathics_scanner import TranslateError from mathics import settings + +from mathics.layout.client import NoWebEngine from mathics.core.expression import ensure_context, KeyComparable, SymbolAborted + FORMATS = [ "StandardForm", "FullForm", @@ -213,6 +216,9 @@ def get_data(self): class Output(object): + def __init__(self, web_engine=NoWebEngine()): + self.web_engine = web_engine + def max_stored_size(self, settings) -> int: return settings.MAX_STORED_SIZE @@ -225,6 +231,18 @@ def clear(self, wait): def display(self, data, metadata): raise NotImplementedError + def warn_about_web_engine(self): + return False + + def assume_web_engine(self): + return self.web_engine.assume_is_available() + + def mathml_to_svg(self, mathml): + return self.web_engine.mathml_to_svg(mathml) + + def rasterize(self, svg, *args, **kwargs): + return self.web_engine.rasterize(svg, *args, **kwargs) + class Evaluation(object): def __init__( @@ -249,6 +267,7 @@ def __init__( self.quiet_all = False self.format = format self.catch_interrupt = catch_interrupt + self.once_messages = set() self.SymbolNull = Symbol("Null") @@ -474,7 +493,7 @@ def get_quiet_messages(self): return [] return value.leaves - def message(self, symbol, tag, *args) -> None: + def message(self, symbol, tag, *args, **kwargs) -> None: from mathics.core.expression import String, Symbol, Expression, from_python # Allow evaluation.message('MyBuiltin', ...) (assume @@ -484,6 +503,11 @@ def message(self, symbol, tag, *args) -> None: pattern = Expression("MessageName", Symbol(symbol), String(tag)) + if kwargs.get('once', False): + if pattern in self.once_messages: + return + self.once_messages.add(pattern) + if pattern in quiet_messages or self.quiet_all: return diff --git a/mathics/layout/__init__.py b/mathics/layout/__init__.py new file mode 100644 index 0000000000..faa18be5bb --- /dev/null +++ b/mathics/layout/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- diff --git a/mathics/layout/client.py b/mathics/layout/client.py new file mode 100644 index 0000000000..776bf763ca --- /dev/null +++ b/mathics/layout/client.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Your installation of nodejs with the following packages: mathjax-node svg2png (install them using +# npm). + +# Tips for troubleshooting: +# https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally +# export NODE_PATH=$(npm root --quiet -g) + +import subprocess +from subprocess import Popen +import os + +import socket +import json +import struct + + +class WebEngineError(RuntimeError): + pass + + +class WebEngineUnavailable(WebEngineError): + pass + + +class Pipe: + def __init__(self, sock): + self.sock = sock + + # the following three functions are taken from + # http://stackoverflow.com/questions/17667903/python-socket-receive-large-amount-of-data + + def _recvall(self, n): + # Helper function to recv n bytes or return None if EOF is hit + data = b'' + sock = self.sock + while len(data) < n: + packet = sock.recv(n - len(data)) + if not packet: + return None + data += packet + return data + + def put(self, msg): + msg = json.dumps(msg).encode('utf8') + # Prefix each message with a 4-byte length (network byte order) + msg = struct.pack('>I', len(msg)) + msg + self.sock.sendall(msg) + + def get(self): + # Read message length and unpack it into an integer + raw_msglen = self._recvall(4) + if not raw_msglen: + return None + msglen = struct.unpack('>I', raw_msglen)[0] + # Read the message data + return json.loads(self._recvall(msglen).decode('utf8')) + + +class RemoteMethod: + def __init__(self, socket, name): + self.pipe = Pipe(socket) + self.name = name + + def __call__(self, *args): + self.pipe.put({'call': self.name, 'args': args}) + reply = self.pipe.get() + + error = reply.get('error') + if error: + raise WebEngineError(str(error)) + else: + return reply.get('data') + + +class Client: + def __init__(self, ip, port): + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.connect((ip, port)) + + def __getattr__(self, name): + return RemoteMethod(self.socket, name) + + def close(self): + return self.socket.close() + + +# Why WebEngine? Well, QT calls its class for similar stuff "web engine", an engine +# that "provides functionality for rendering regions of dynamic web content". This +# is not about web servers but layout (http://doc.qt.io/qt-5/qtwebengine-index.html). + +class NoWebEngine: + def assume_is_available(self): + raise WebEngineUnavailable + + def mathml_to_svg(self, mathml): + raise WebEngineUnavailable + + def rasterize(self, svg, *args, **kwargs): + raise WebEngineUnavailable + + +def _normalize_svg(svg): + import xml.etree.ElementTree as ET + import base64 + import re + + ET.register_namespace('', 'http://www.w3.org/2000/svg') + root = ET.fromstring(svg) + prefix = 'data:image/svg+xml;base64,' + + def rewrite(up): + changes = [] + + for i, node in enumerate(up): + if node.tag == '{http://www.w3.org/2000/svg}image': + src = node.attrib.get('src', '') + if src.startswith(prefix): + attrib = node.attrib + + if 'width' in attrib and 'height' in attrib: + target_width = float(attrib['width']) + target_height = float(attrib['height']) + target_transform = attrib.get('transform', '') + + image_svg = _normalize_svg(base64.b64decode(src[len(prefix):])) + root = ET.fromstring(image_svg) + + view_box = re.split('\s+', root.attrib.get('viewBox', '')) + + if len(view_box) == 4: + x, y, w, h = (float(t) for t in view_box) + root.tag = '{http://www.w3.org/2000/svg}g' + root.attrib = {'transform': '%s scale(%f, %f) translate(%f, %f)' % ( + target_transform, target_width / w, target_height / h, -x, -y)} + + changes.append((i, node, root)) + else: + rewrite(node) + + for i, node, new_node in reversed(changes): + up.remove(node) + up.insert(i, new_node) + + rewrite(root) + + return ET.tostring(root, 'utf8').decode('utf8') + + +class WebEngine: + def __init__(self): + self.process = None + self.client = None + self.unavailable = None + + def _create_client(self): + try: + popen_env = os.environ.copy() + + server_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'server.js') + + if False: + # fixes problems on Windows network drives + import tempfile + fd, copied_path = tempfile.mkstemp(suffix='js') + with open(server_path, 'rb') as f: + os.write(fd, f.read()) + os.fsync(fd) + server_path = copied_path + + def abort(message): + error_text = '\n'.join([ + '', + 'Node.js failed to start Mathics server.', + 'You might need to run: npm install -g mathjax-node svg2png', + '', + '']) + raise WebEngineUnavailable(error_text + message) + + process = Popen( + ['node', server_path], + stdout=subprocess.PIPE, + env=popen_env) + + hello = 'HELLO:' # agreed upon "all ok" hello message. + + status = process.stdout.readline().decode('utf8').strip() + if not status.startswith(hello): + error = '' + while True: + line = process.stdout.readline().decode('utf8') + if not line: + break + error += ' ' + line + + process.terminate() + abort(error) + + port = int(status[len(hello):]) + except OSError as e: + abort(str(e)) + + try: + self.client = Client('127.0.0.1', port) + self.process = process + except Exception as e: + self.client = None + self.process = None + process.terminate() + abort(str(e)) + + def _ensure_client(self): + if not self.client: + if self.unavailable is not None: + raise WebEngineUnavailable(self.unavailable) + try: + self._create_client() + except WebEngineUnavailable as e: + self.unavailable = str(e) + raise e + + return self.client + + def assume_is_available(self): + if self.unavailable is not None: + raise WebEngineUnavailable(self.unavailable) + + def mathml_to_svg(self, mathml): + return self._ensure_client().mathml_to_svg(mathml) + + def rasterize(self, svg, size): + buffer = self._ensure_client().rasterize(_normalize_svg(svg), size) + return bytearray(buffer['data']) + + def terminate(self): + if self.process: + self.process.terminate() + self.process = None + self.client = None + diff --git a/mathics/layout/server.js b/mathics/layout/server.js new file mode 100644 index 0000000000..5a33f4be63 --- /dev/null +++ b/mathics/layout/server.js @@ -0,0 +1,114 @@ +// to install: npm install -g mathjax-node svg2png + +try { + function server(methods) { + net = require('net'); + + var uint32 = { + parse: function(buffer) { + return (buffer[0] << 24) | + (buffer[1] << 16) | + (buffer[2] << 8) | + (buffer[3] << 0); + }, + make: function(x) { + var buffer = new Buffer(4); + buffer[0] = x >> 24; + buffer[1] = x >> 16; + buffer[2] = x >> 8; + buffer[3] = x >> 0; + return buffer; + } + }; + + var server = net.createServer(function (socket) { + function write(data) { + var json = JSON.stringify(data); + var size = json.length; + socket.write(Buffer.concat([uint32.make(size), new Buffer(json)])); + } + + var state = { + buffer: new Buffer(0) + }; + + function rpc(size) { + var json = JSON.parse(state.buffer.slice(4, size + 4)); + state.buffer = state.buffer.slice(size + 4) + var method = methods[json.call]; + if (method) { + try { + method.apply(null, json.args.concat([write])); + } catch(e) { + write({error: e.toString() + '; ' + e.stack}); + } + } + } + + socket.on('close', function() { + // means our Python client has lost us. quit. + process.exit(); + }); + + socket.on('data', function(data) { + state.buffer = Buffer.concat( + [state.buffer, data]); + + if (state.buffer.length >= 4) { + var buffer = state.buffer; + var size = uint32.parse(buffer); + if (buffer.length >= size + 4) { + rpc(size); + } + } + }); + }); + + server.on('listening', function() { + var port = server.address().port; + process.stdout.write('HELLO:' + port.toString() + '\n'); + }); + + server.listen(0); // pick a free port + } + + var mathjax = require("mathjax-node"); + mathjax.config({ + MathJax: { + // traditional MathJax configuration + } + }); + mathjax.start(); + + server({ + mathml_to_svg: function(mathml, reply) { + mathjax.typeset({ + math: mathml, + format: "MathML", + svg: true + }, function (data) { + if (!data.errors) { + reply({data: data.svg}); + } else { + reply({error: data.errors}); + } + }); + }, + rasterize: function(svg, size, reply) { + var svg2png = require("svg2png"); + + svg2png(Buffer.from(svg, 'utf8'), { + width: size[0], + height: size[1] + }) + .then(function(buffer) { + reply({data: buffer}); + }) + .catch(function(e) { + reply({error: e.toString()}); + }); + } + }); +} catch (ex) { + process.stdout.write('FAIL.' + '\n' + ex.toString() + '\n'); +} diff --git a/mathics/main.py b/mathics/main.py index e9193d6887..f408b3df72 100755 --- a/mathics/main.py +++ b/mathics/main.py @@ -168,6 +168,7 @@ def max_stored_size(self, settings): return None def __init__(self, shell): + super(TerminalOutput, self).__init__() self.shell = shell def out(self, out): diff --git a/mathics/web/media/js/mathics.js b/mathics/web/media/js/mathics.js new file mode 100644 index 0000000000..1a52284297 --- /dev/null +++ b/mathics/web/media/js/mathics.js @@ -0,0 +1,766 @@ +var deleting; +var blurredElement; + +var movedItem; + +var clickedQuery; + +var lastFocus = null; + +var welcome = true; + +function getLetterWidth(element) { + var letter = $E('span', $T('m')); + letter.setStyle({ + fontFamily: element.getStyle('font-family'), + fontSize: element.getStyle('font-size') + }); + var parent = $$('body')[0]; + parent.appendChild(letter); + var width = letter.getWidth(); + parent.removeChild(letter); + delete letter; + return width; +} + +function refreshInputSize(textarea) { + var letterWidth = getLetterWidth(textarea); + var width = textarea.getWidth() - 15; + var lines = textarea.value.split('\n'); + var lineCount = 0; + for (var index = 0; index < lines.length; ++index) { + var line = lines[index]; + lineCount += Math.ceil(1.0 * (line.length + 1) * letterWidth / width); + } + textarea.rows = lineCount; +} + +function refreshInputSizes() { + $$('textarea.request').each(function(textarea) { + refreshInputSize(textarea); + }); + + $$('#queries ul').each(function(ul) { + afterProcessResult(ul, 'Rerender'); + }); +} + +function inputChange(event) { + refreshInputSize(this); +} + +function isEmpty(textarea) { + return textarea.value.strip() == '' && !textarea.submitted; +} + +function prepareText(text) { + if (text == '') { + text = String.fromCharCode(160); + } + return text; + + /* + // Place ­ between every two characters. + // Problem: Copy & paste yields weird results! + var result = ''; + for (var index = 0; index < text.length; ++index) { + result += text.charAt(index); + if (index < text.length - 1) + result += String.fromCharCode(173); // ­ + } + return result; + */ +} + +function getDimensions(math, callback) { + var all = $('calc_all').cloneNode(true); + all.id = null; + var body = $$('body')[0]; + body.appendChild(all); + var container = all.select('.calc_container')[0]; + container.appendChild(translateDOMElement(math)); + + MathJax.Hub.Queue(["Typeset", MathJax.Hub, container]); + MathJax.Hub.Queue(function() { + var pos = container.cumulativeOffset(); + var next = all.select('.calc_next')[0].cumulativeOffset(); + var below = all.select('.calc_below')[0].cumulativeOffset(); + var width = next.left - pos.left + 4; + var height = below.top - pos.top + 20; + body.removeChild(all); + callback(width, height); + }); +} + +function drawMeshGradient(ctx, points) { + function color(c, a) { + var result = 'rgba(' + Math.round(c[0]*255) + ', ' + Math.round(c[1]*255) + ', ' + + Math.round(c[2]*255) + ', ' + a + ')'; + return result; + } + + var grad1 = ctx.createLinearGradient(0, 0, 0.5, 0.5); + grad1.addColorStop(0, color(points[0][1], 1)); + grad1.addColorStop(1, color(points[0][1], 0)); + var grad2 = ctx.createLinearGradient(1, 0, 0, 0); + grad2.addColorStop(0, color(points[1][1], 1)); + grad2.addColorStop(1, color(points[1][1], 0)); + var grad3 = ctx.createLinearGradient(0, 1, 0, 0); + grad3.addColorStop(0, color(points[2][1], 1)); + grad3.addColorStop(1, color(points[2][1], 0)); + + ctx.save(); + ctx.setTransform(points[1][0][0]-points[0][0][0], points[1][0][1]-points[0][0][1], + points[2][0][0]-points[0][0][0], points[2][0][1]-points[0][0][1], points[0][0][0], points[0][0][1]); + + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(1, 0); + ctx.lineTo(0, 1); + ctx.closePath(); + + ctx.globalCompositeOperation = "lighter"; + ctx.fillStyle = grad1; + ctx.fill(); + ctx.fillStyle = grad2; + ctx.fill(); + ctx.fillStyle = grad3; + ctx.fill(); + ctx.restore(); +} + +function createMathNode(nodeName) { + if (['svg', 'g', 'rect', 'circle', 'polyline', 'polygon', 'path', 'ellipse', 'foreignObject'].include(nodeName)) + return document.createElementNS("http://www.w3.org/2000/svg", nodeName); + else { + return document.createElement(nodeName); + } +} + +var objectsPrefix = 'math_object_'; +var objectsCount = 0; +var objects = {}; + +function translateDOMElement(element, svg) { + if (element.nodeType == 3) { + var text = element.nodeValue; + return $T(text); + } + + if (svg && element.nodeName == 'svg') { + // leave s embedded in s alone, if they are + // not > > . this fixes the + // node.js web engine svg rendering, which embeds text + // as in the Graphics . + var node = element; + var ok = false; + while (node != svg && node.parentNode) { + if (node.nodeName == 'foreignObject') { + ok = true; + break; + } + node = node.parentNode; + } + if (!ok) { + return element; + } + } + + var dom = null; + var nodeName = element.nodeName; + if (nodeName != 'meshgradient' && nodeName != 'graphics3d') { + dom = createMathNode(element.nodeName); + for (var i = 0; i < element.attributes.length; ++i) { + var attr = element.attributes[i]; + if (attr.nodeName != 'ox' && attr.nodeName != 'oy') + dom.setAttribute(attr.nodeName, attr.nodeValue); + } + } + if (nodeName == 'foreignObject') { + dom.setAttribute('width', svg.getAttribute('width')); + dom.setAttribute('height', svg.getAttribute('height')); + dom.setAttribute('style', dom.getAttribute('style') + '; text-align: left; padding-left: 2px; padding-right: 2px;'); + var ox = parseFloat(element.getAttribute('ox')); + var oy = parseFloat(element.getAttribute('oy')); + dom.setAttribute('ox', ox); + dom.setAttribute('oy', oy); + } + if (nodeName == 'mo') { + var op = element.childNodes[0].nodeValue; + if (op == '[' || op == ']' || op == '{' || op == '}' || op == String.fromCharCode(12314) || op == String.fromCharCode(12315)) + dom.setAttribute('maxsize', '3'); + } + if (nodeName == 'meshgradient') { + if (!MathJax.Hub.Browser.isOpera) { + var data = element.getAttribute('data').evalJSON(); + var div = document.createElementNS('http://www.w3.org/1999/xhtml', 'div'); + var foreign = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'); + foreign.setAttribute('width', svg.getAttribute('width')); + foreign.setAttribute('height', svg.getAttribute('height')); + foreign.setAttribute('x', '0px'); + foreign.setAttribute('y', '0px'); + foreign.appendChild(div); + + var canvas = createMathNode('canvas'); + canvas.setAttribute('width', svg.getAttribute('width')); + canvas.setAttribute('height', svg.getAttribute('height')); + div.appendChild(canvas); + + var ctx = canvas.getContext('2d'); + for (var index = 0; index < data.length; ++index) { + var points = data[index]; + if (points.length == 3) { + drawMeshGradient(ctx, points); + } + } + + dom = foreign; + } + } + var object = null; + if (nodeName == 'graphics3d') { + var data = element.getAttribute('data').evalJSON(); + var div = document.createElement('div'); + drawGraphics3D(div, data); + dom = div; + } + if (nodeName == 'svg' || nodeName == 'graphics3d' || nodeName.toLowerCase() == 'img') { + // create that will contain the graphics + object = createMathNode('mspace'); + var width, height; + if (nodeName == 'svg' || nodeName.toLowerCase() == 'img') { + width = dom.getAttribute('width'); + height = dom.getAttribute('height'); + } else { + // TODO: calculate appropriate height and recalculate on every view change + width = height = '400'; + } + object.setAttribute('width', width + 'px'); + object.setAttribute('height', height + 'px'); + } + if (nodeName == 'svg') + svg = dom; + var rows = [[]]; + $A(element.childNodes).each(function(child) { + if (child.nodeName == 'mspace' && child.getAttribute('linebreak') == 'newline') + rows.push([]); + else + rows[rows.length - 1].push(child); + }); + var childParent = dom; + if (nodeName == 'math') { + var mstyle = createMathNode('mstyle'); + mstyle.setAttribute('displaystyle', 'true'); + dom.appendChild(mstyle); + childParent = mstyle; + } + if (rows.length > 1) { + var mtable = createMathNode('mtable'); + mtable.setAttribute('rowspacing', '0'); + mtable.setAttribute('columnalign', 'left'); + var nospace = 'cell-spacing: 0; cell-padding: 0; row-spacing: 0; row-padding: 0; border-spacing: 0; padding: 0; margin: 0'; + mtable.setAttribute('style', nospace); + rows.each(function(row) { + var mtr = createMathNode('mtr'); + mtr.setAttribute('style', nospace); + var mtd = createMathNode('mtd'); + mtd.setAttribute('style', nospace); + row.each(function(element) { + var elmt = translateDOMElement(element, svg); + if (nodeName == 'mtext') { + // wrap element in mtext + var outer = createMathNode('mtext'); + outer.appendChild(elmt); + elmt = outer; + } + mtd.appendChild(elmt); + }); + mtr.appendChild(mtd); + mtable.appendChild(mtr); + }); + if (nodeName == 'mtext') { + // no mtable inside mtext, but mtable instead of mtext + dom = mtable; + } else + childParent.appendChild(mtable); + } else { + rows[0].each(function(element) { + childParent.appendChild(translateDOMElement(element, svg)); + }); + } + if (object) { + var id = objectsCount++; + object.setAttribute('id', objectsPrefix + id); + objects[id] = dom; + return object; + } + return dom; +} + +function convertMathGlyphs(dom) { + // convert mglyphs to their classic representation ( or ), so the new mglyph logic does not make + // anything worse in the classic Mathics frontend for now. In the long run, this code should vanish. + + var MML = "http://www.w3.org/1998/Math/MathML"; + var glyphs = dom.getElementsByTagName("mglyph"); + for (var i = 0; i < glyphs.length; i++) { + var glyph = glyphs[i]; + var src = glyph.getAttribute('src'); + if (src.startsWith('data:image/svg+xml;base64,')) { + var svgText = atob(src.substring(src.indexOf(",") + 1)); + var mtable = document.createElementNS(MML, "mtable"); + mtable.innerHTML = '' + svgText + ''; + var svg = mtable.getElementsByTagNameNS("*", "svg")[0]; + svg.setAttribute('width', glyph.getAttribute('width')); + svg.setAttribute('height', glyph.getAttribute('height')); + svg.setAttribute('data-mathics', 'format'); + glyph.parentNode.replaceChild(mtable, glyph); + } else if (src.startsWith('data:image/')) { + var img = document.createElement('img'); + img.setAttribute('src', src) + img.setAttribute('width', glyph.getAttribute('width')); + img.setAttribute('height', glyph.getAttribute('height')); + img.setAttribute('data-mathics', 'format'); + glyph.parentNode.replaceChild(img, glyph); + } + } +} + +function createLine(value) { + if (value.startsWith('s + ul.select('.mspace').each(function(mspace) { + var id = mspace.getAttribute('id').substr(objectsPrefix.length); + var object = objects[id]; + mspace.appendChild(object); + }); + }); + if (!MathJax.Hub.Browser.isOpera) { + // Opera 11.01 Build 1190 on Mac OS X 10.5.8 crashes on this call for Plot[x,{x,0,1}] + // => leave inner MathML untouched + MathJax.Hub.Queue(['Typeset', MathJax.Hub, ul]); + } + MathJax.Hub.Queue(function() { + ul.select('foreignObject >span >nobr >span.math').each(function(math) { + var content = math.childNodes[0].childNodes[0].childNodes[0]; + math.removeChild(math.childNodes[0]); + math.insertBefore(content, math.childNodes[0]); + + if (command == 'Typeset') { + // recalculate positions of insets based on ox/oy properties + var foreignObject = math.parentNode.parentNode.parentNode; + var dimensions = math.getDimensions(); + var w = dimensions.width + 4; + var h = dimensions.height + 4; + var x = parseFloat(foreignObject.getAttribute('x').substr()); + var y = parseFloat(foreignObject.getAttribute('y')); + var ox = parseFloat(foreignObject.getAttribute('ox')); + var oy = parseFloat(foreignObject.getAttribute('oy')); + x = x - w/2.0 - ox*w/2.0; + y = y - h/2.0 + oy*h/2.0; + foreignObject.setAttribute('x', x + 'px'); + foreignObject.setAttribute('y', y + 'px'); + } + }); + }); +} + +function setResult(ul, results) { + results.each(function(result) { + var resultUl = $E('ul', {'class': 'out'}); + result.out.each(function(out) { + var li = $E('li', {'class': (out.message ? 'message' : 'print')}); + if (out.message) + li.appendChild($T(out.prefix + ': ')); + li.appendChild(createLine(out.text)); + resultUl.appendChild(li); + }); + if (result.result != null) { + var li = $E('li', {'class': 'result'}, createLine(result.result)); + resultUl.appendChild(li); + } + ul.appendChild($E('li', {'class': 'out'}, resultUl)); + }); + afterProcessResult(ul); +} + +function submitQuery(textarea, onfinish) { + if (welcome) { + $('welcomeContainer').fade({duration: 0.2}); + if ($('hideStartupMsg').checked) localStorage.setItem('hideMathicsStartupMsg', 'true'); + welcome = false; + $('logo').removeClassName('load'); + } + + textarea.li.addClassName('loading'); + $('logo').addClassName('working'); + new Ajax.Request('/ajax/query/', { + method: 'post', + parameters: { + query: textarea.value + }, + onSuccess: function(transport) { + textarea.ul.select('li[class!=request][class!=submitbutton]').invoke('deleteElement'); + if (!transport.responseText) { + // A fatal Python error has occurred, e.g. on 4.4329408320439^43214234345 + // ("Fatal Python error: mp_reallocate failure") + // -> print overflow message + transport.responseText = '{"results": [{"out": [{"prefix": "General::noserver", "message": true, "tag": "noserver", "symbol": "General", "text": "No server running."}]}]}'; + } + var response = transport.responseText.evalJSON(); + setResult(textarea.ul, response.results); + textarea.submitted = true; + textarea.results = response.results; + var next = textarea.li.nextSibling; + if (next) + next.textarea.focus(); + else + createQuery(); + }, + onFailure: function(transport) { + textarea.ul.select('li[class!=request]').invoke('deleteElement'); + var li = $E('li', {'class': 'serverError'}, $T("Sorry, an error occurred while processing your request!")); + textarea.ul.appendChild(li); + textarea.submitted = true; + }, + onComplete: function() { + textarea.li.removeClassName('loading'); + $('logo').removeClassName('working'); + if (onfinish) + onfinish(); + } + }); +} + +function getSelection() { + // TODO +} + +function keyDown(event) { + var textarea = lastFocus; + if (!textarea) + return; + refreshInputSize(textarea); + + if (event.keyCode == Event.KEY_RETURN && (event.shiftKey || event.location == 3)) { + if (!Prototype.Browser.IE) + event.stop(); + + var query = textarea.value.strip(); + if (query) { + submitQuery(textarea); + } + } else if (event.keyCode == Event.KEY_UP) { + if (textarea.selectionStart == 0 && textarea.selectionEnd == 0) { + if (isEmpty(textarea)) { + if (textarea.li.previousSibling) + textarea.li.previousSibling.textarea.focus(); + } else + createQuery(textarea.li); + } + } else if (event.keyCode == Event.KEY_DOWN) { + if (textarea.selectionStart == textarea.value.length && textarea.selectionEnd == textarea.selectionStart) { + if (isEmpty(textarea)) { + if (textarea.li.nextSibling) + textarea.li.nextSibling.textarea.focus(); + } else + createQuery(textarea.li.nextSibling); + } + } else + if (isGlobalKey(event)) + event.stop(); +} + +function deleteMouseDown(event) { + if (event.isLeftClick()) + deleting = true; +} + +function deleteClick(event) { + if (lastFocus == this.li.textarea) + lastFocus = null; + this.li.deleteElement(); + deleting = false; + if (blurredElement) { + blurredElement.focus(); + blurredElement = null; + } + if ($('queries').childElements().length == 0) + createQuery(); + +} + +function moveMouseDown(event) { + movedItem = this.li; + movedItem.addClassName('moving'); +} + +function moveMouseUp(event) { + if (movedItem) { + movedItem.removeClassName('moving'); + movedItem.textarea.focus(); + movedItem = null; + } +} + +function onFocus(event) { + var textarea = this; + textarea.li.addClassName('focused'); + lastFocus = textarea; +} + +function onBlur(event) { + var textarea = this; + blurredElement = textarea; + if (!deleting && textarea.li != movedItem && isEmpty(textarea) && $('queries').childElements().length > 1) { + textarea.li.hide(); + if (textarea == lastFocus) + lastFocus = null; + window.setTimeout(function() { + textarea.li.deleteElement(); + }, 10); + } + textarea.li.removeClassName('focused'); +} + +function createSortable() { + Position.includeScrollOffsets = true; + Sortable.create('queries', { + handle: 'move', + scroll: 'document', + scrollSensitivity: 1 // otherwise strange flying-away of item at top + }); +} + +var queryIndex = 0; + +function createQuery(before, noFocus, updatingAll) { + var ul, textarea, moveHandle, deleteHandle, submitButton; + // Items need id in order for Sortable.onUpdate to work. + var li = $E('li', {'id': 'query_' + queryIndex++, 'class': 'query'}, + ul = $E('ul', {'class': 'query'}, + $E('li', {'class': 'request'}, + textarea = $E('textarea', {'class': 'request', 'spellcheck': 'false'}), + $E('span', {'class': 'submitbutton', 'title': "Evaluate [Shift+Return]"}, + submitButton = $E('span', $T('=')) + ) + ) + ), + moveHandle = $E('span', {'class': 'move'}), + deleteHandle = $E('span', {'class': 'delete', 'title': "Delete"}, $T(String.fromCharCode(215))) + ); + textarea.rows = 1; + textarea.ul = ul; + textarea.li = li; + textarea.submitted = false; + moveHandle.li = li; + deleteHandle.li = li; + li.textarea = textarea; + li.ul = ul; + if (before) + $('queries').insertBefore(li, before); + else + $('queries').appendChild(li); + if (!updatingAll) + refreshInputSize(textarea); + new Form.Element.Observer(textarea, 0.2, inputChange.bindAsEventListener(textarea)); + textarea.observe('focus', onFocus.bindAsEventListener(textarea)); + textarea.observe('blur', onBlur.bindAsEventListener(textarea)); + li.observe('mousedown', queryMouseDown.bindAsEventListener(li)); + deleteHandle.observe('click', deleteClick.bindAsEventListener(deleteHandle)); + deleteHandle.observe('mousedown', deleteMouseDown.bindAsEventListener(deleteHandle)); + moveHandle.observe('mousedown', moveMouseDown.bindAsEventListener(moveHandle)); + moveHandle.observe('mouseup', moveMouseUp.bindAsEventListener(moveHandle)); + $(document).observe('mouseup', moveMouseUp.bindAsEventListener($(document))); + submitButton.observe('mousedown', function() { + if (textarea.value.strip()) + submitQuery(textarea); + else + window.setTimeout(function() { + textarea.focus(); + }, 10); + }); + if (!updatingAll) { + createSortable(); + // calling directly fails in Safari on document loading + //window.setTimeout(createSortable, 10); + } + // Immediately setting focus doesn't work in IE. + if (!noFocus) + window.setTimeout(function() { + textarea.focus(); + }, 10); + return li; +} + +var mouseDownEvent = null; + +function documentMouseDown(event) { + if (event.isLeftClick()) { + if (clickedQuery) { + clickedQuery = null; + mouseDownEvent = null; + return; + } + event.stop(); // strangely, doesn't work otherwise + mouseDownEvent = event; + } +} + +function documentClick(event) { + // In Firefox, mousedown also fires when user clicks scrollbars. + // -> listen to click + event = mouseDownEvent; + if (!event) + return; + if ($('queries').childElements().length == 1 && isEmpty($('queries').childElements()[0].textarea)) { + $('queries').childElements()[0].textarea.focus(); + return; + } + var offset = $('document').cumulativeOffset(); + var y = event.pointerY() - offset.top + $('document').scrollTop; + var element = null; + $('queries').childElements().each(function(li) { + var offset = li.positionedOffset(); // margin-top: 10px + if (offset.top + 20 > y) { + element = li; + throw $break; + } + }); + createQuery(element); +} + +function queryMouseDown(event) { + clickedQuery = this; +} + +function focusLast() { + if (lastFocus) + lastFocus.focus(); + else + createQuery(); +} + +function isGlobalKey(event) { + if (event.ctrlKey) { + switch(event.keyCode) { + case 68: + // case 67: + case 83: + case 79: + return true; + } + } + return false; +} + +function globalKeyUp(event) { + if (!popup && event.ctrlKey) { + switch (event.keyCode) { + case 68: // D + showDoc(); + $('search').select(); + event.stop(); + break; + // case 67: // C + // focusLast(); + // event.stop(); + // break; + case 83: // S + showSave(); + break; + case 79: // O + showOpen(); + break; + } + } +} + +function domLoaded() { + MathJax.Hub.Config({ + "HTML-CSS": { + imageFont: null, + linebreaks: { automatic: true } + }, + MMLorHTML: { + // + // The output jax that is to be preferred when both are possible + // (set to "MML" for native MathML, "HTML" for MathJax's HTML-CSS output jax). + // + prefer: { + MSIE: "HTML", + Firefox: "HTML", + Opera: "HTML", + other: "HTML" + } + } + }); + MathJax.Hub.Configured(); + + if (localStorage.getItem('hideMathicsStartupMsg') === 'true') { + $('welcome').hide(); + } + + if ($('welcomeBrowser')) + if (!(Prototype.Browser.WebKit || Prototype.Browser.MobileSafari || Prototype.Browser.Gecko)) + $('welcomeBrowser').show(); + + $$('body')[0].observe('resize', refreshInputSizes); + + if ($('queriesContainer')) { + $('queriesContainer').appendChild($E('ul', {'id': 'queries'})); + + $('document').observe('mousedown', documentMouseDown.bindAsEventListener($('document'))); + $('document').observe('click', documentClick.bindAsEventListener($('document'))); + + $(document).observe('keydown', keyDown.bindAsEventListener()); + if (Prototype.Browser.IE) { + document.body.addEventListener('keydown', function(event) { + if (event.keyCode == Event.KEY_RETURN && event.shiftKey) { + event.stopPropagation(); + event.preventDefault(); + keyDown(event); + } + }, true); + } + if (Prototype.Browser.Opera || Prototype.Browser.IE) { + // Opera needs another hook so it doesn't insert newlines after Shift+Return + $(document).observe('keypress', function(event) { + if (event.keyCode == Event.KEY_RETURN && event.shiftKey) + event.stop(); + }.bindAsEventListener()); + } + + $(document).observe('keyup', globalKeyUp.bindAsEventListener($('document'))); + + if (!loadLink()) + createQuery(); + } +} + +$(document).observe('dom:loaded', domLoaded); +// Konqueror won't fire dom:loaded, so we still need body.onload. + +window.onresize = refreshInputSizes;