Skip to content

Commit

Permalink
handle box errors in graphics and graphics3d (#966)
Browse files Browse the repository at this point in the history
Here we go with a proposal to address #964. The idea is that when an
invalid graphics primitive is processed, a pink rectangle is drawn
instead. Notice that in WMA no error message is shown in the client.
Just the pink background is shown in the image, plus a tooltip in the
notebook interface.
  • Loading branch information
mmatera authored Jan 19, 2024
1 parent 8027c56 commit d4f8b6b
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 11 deletions.
5 changes: 4 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ Compatibility
* ``*Plot`` does not show messages during the evaluation.
* ``Range[]`` now handles a negative ``di`` PR #951
* Improved support for ``DirectedInfinity`` and ``Indeterminate``.

* ``Graphics`` and ``Graphics3D`` including wrong primitives and directives
are shown with a pink background. In the Mathics-Django interface, a tooltip
error message is also shown.

Internals
---
Expand All @@ -41,6 +43,7 @@ Bugs

* ``Definitions`` is compatible with ``pickle``.
* Improved support for ``Quantity`` expressions, including conversions, formatting and arithmetic operations.
* ``Background`` option for ``Graphics`` and ``Graphics3D`` is operative again.
* ``Switch[]`` involving ``Infinity`` Issue #956
* ``Outer[]`` on ``SparseArray`` Issue #939
* ``ArrayQ[]`` detects ``SparseArray`` PR #947
Expand Down
5 changes: 5 additions & 0 deletions mathics/builtin/box/graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,11 @@ def _prepare_elements(self, elements, options, neg_y=False, max_width=None):
if evaluation is None:
evaluation = self.evaluation
elements = GraphicsElements(elements[0], evaluation, neg_y)
if hasattr(elements, "background_color"):
self.background_color = elements.background_color
if hasattr(elements, "tooltip_text"):
self.tooltip_text = elements.tooltip_text

axes = [] # to be filled further down

def calc_dimensions(final_pass=True):
Expand Down
31 changes: 29 additions & 2 deletions mathics/builtin/box/graphics3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

import json
import logging
import numbers

from mathics.builtin.box.graphics import (
Expand All @@ -13,7 +14,12 @@
PointBox,
PolygonBox,
)
from mathics.builtin.colors.color_directives import Opacity, RGBColor, _ColorObject
from mathics.builtin.colors.color_directives import (
ColorError,
Opacity,
RGBColor,
_ColorObject,
)
from mathics.builtin.drawing.graphics3d import Coords3D, Graphics3DElements, Style3D
from mathics.builtin.drawing.graphics_internals import (
GLOBALS3D,
Expand Down Expand Up @@ -52,7 +58,11 @@ def _prepare_elements(self, elements, options, max_width=None):
):
self.background_color = None
else:
self.background_color = _ColorObject.create(background)
try:
self.background_color = _ColorObject.create(background)
except ColorError:
logging.warning(f"{str(background)} is not a valid color spec.")
self.background_color = None

evaluation = options["evaluation"]

Expand Down Expand Up @@ -228,6 +238,11 @@ def _prepare_elements(self, elements, options, max_width=None):
raise BoxExpressionError

elements = Graphics3DElements(elements[0], evaluation)
# If one of the primitives or directives fails to be
# converted into a box expression, then the background color
# is set to pink, overwritting the options.
if hasattr(elements, "background_color"):
self.background_color = elements.background_color

def calc_dimensions(final_pass=True):
if "System`Automatic" in plot_range:
Expand Down Expand Up @@ -357,6 +372,16 @@ def boxes_to_json(self, elements=None, **options):
boxscale,
) = self._prepare_elements(elements, options)

# TODO: Handle alpha channel
background = (
self.background_color.to_css()[:-1]
if self.background_color is not None
else "rgbcolor(100%,100%,100%)"
)
tooltip_text = (
elements.tooltip_text if hasattr(elements, "tooltip_text") else ""
)

js_ticks_style = [s.to_js() for s in ticks_style]

elements._apply_boxscaling(boxscale)
Expand All @@ -371,6 +396,8 @@ def boxes_to_json(self, elements=None, **options):
json_repr = json.dumps(
{
"elements": format_fn(elements, **options),
"background_color": background,
"tooltip_text": tooltip_text,
"axes": {
"hasaxes": axes,
"ticks": ticks,
Expand Down
4 changes: 4 additions & 0 deletions mathics/builtin/drawing/graphics3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ class Graphics3D(Graphics):
>> Graphics3D[Polygon[{{0,0,0}, {0,1,1}, {1,0,0}}]]
= -Graphics3D-
The 'Background' option allows to set the color of the background:
>> Graphics3D[Sphere[], Background->RGBColor[.6, .7, 1.]]
= -Graphics3D-
In 'TeXForm', 'Graphics3D' creates Asymptote figures:
>> Graphics3D[Sphere[]] // TeXForm
= #<--#
Expand Down
47 changes: 42 additions & 5 deletions mathics/builtin/graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
Drawing Graphics
"""

import logging
from math import sqrt

from mathics.builtin.colors.color_directives import (
CMYKColor,
ColorError,
GrayLevel,
Hue,
LABColor,
Expand Down Expand Up @@ -69,6 +71,9 @@
DEFAULT_POINT_FACTOR = 0.005


ERROR_BACKGROUND_COLOR = RGBColor(components=[1, 0.3, 0.3, 0.25])


class CoordinatesError(BoxExpressionError):
pass

Expand Down Expand Up @@ -262,6 +267,10 @@ class Graphics(Builtin):
>> Graphics[Rectangle[]] // ToBoxes // Head
= GraphicsBox
The 'Background' option allows to set the color of the background:
>> Graphics[{Green, Disk[]}, Background->RGBColor[.6, .7, 1.]]
= -Graphics-
In 'TeXForm', 'Graphics' produces Asymptote figures:
>> Graphics[Circle[]] // TeXForm
= #<--#
Expand Down Expand Up @@ -1087,6 +1096,8 @@ def stylebox_style(style, specs):
raise BoxExpressionError
return new_style

failed = []

def convert(content, style):
if content.has_form("List", None):
items = content.elements
Expand All @@ -1098,31 +1109,57 @@ def convert(content, style):
continue
head = item.get_head()
if head in style_and_form_heads:
style.append(item)
try:
style.append(item)
except ColorError:
failed.append(head)
elif head is Symbol("System`StyleBox"):
if len(item.elements) < 1:
raise BoxExpressionError
failed.append(item.head)
for element in convert(
item.elements[0], stylebox_style(style, item.elements[1:])
):
yield element
elif head.name[-3:] == "Box": # and head[:-3] in element_heads:
element_class = get_class(head)
if element_class is None:
failed.append(head)
continue
options = get_options(head.name[:-3])
if options:
data, options = _data_and_options(item.elements, options)
new_item = Expression(head, *data)
element = element_class(self, style, new_item, options)
try:
element = element_class(self, style, new_item, options)
except (BoxExpressionError, CoordinatesError):
failed.append(head)
continue
else:
element = element_class(self, style, item)
try:
element = element_class(self, style, item)
except (BoxExpressionError, CoordinatesError):
failed.append(head)
continue
yield element
elif head is SymbolList:
for element in convert(item, style):
yield element
else:
raise BoxExpressionError
failed.append(head)
continue

# if failed:
# yield build_error_box2(style)
# raise BoxExpressionError(messages)

self.elements = list(convert(content, self.style_class(self)))
if failed:
messages = "\n".join(
[f"{str(h)} is not a valid primitive or directive." for h in failed]
)
self.tooltip_text = messages
self.background_color = ERROR_BACKGROUND_COLOR
logging.warn(messages)

def create_style(self, expr):
style = self.style_class(self)
Expand Down
11 changes: 10 additions & 1 deletion mathics/format/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,13 +551,21 @@ def graphics3dbox(self, elements=None, **options) -> str:
boundbox_asy += "draw(({0}), {1});\n".format(path, pen)

(height, width) = (400, 400) # TODO: Proper size

# Background color
if self.background_color:
bg_color, opacity = asy_color(self.background_color)
background_directive = "background=" + bg_color + ", "
else:
background_directive = ""

tex = r"""
\begin{{asy}}
import three;
import solids;
size({0}cm, {1}cm);
currentprojection=perspective({2[0]},{2[1]},{2[2]});
currentlight=light(rgb(0.5,0.5,1), specular=red, (2,0,2), (2,2,2), (0,2,2));
currentlight=light(rgb(0.5,0.5,1), {5}specular=red, (2,0,2), (2,2,2), (0,2,2));
{3}
{4}
\end{{asy}}
Expand All @@ -568,6 +576,7 @@ def graphics3dbox(self, elements=None, **options) -> str:
[vp * max([xmax - xmin, ymax - ymin, zmax - zmin]) for vp in self.viewpoint],
asy,
boundbox_asy,
background_directive,
)
return tex

Expand Down
6 changes: 4 additions & 2 deletions mathics/format/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,17 +308,19 @@ def graphics_box(self, elements=None, **options: dict) -> str:
self.boxwidth = options.get("width", self.boxwidth)
self.boxheight = options.get("height", self.boxheight)

tooltip_text = self.tooltip_text if hasattr(self, "tooltip_text") else ""
if self.background_color is not None:
# FIXME: tests don't seem to cover this secton of code.
# Wrap svg_elements in a rectangle
svg_body = f"""
<rect x="{xmin:f}" y="{ymin:f}" width="{self.boxwidth:f}" height="{self.boxheight:f}" style="fill: {self.background_color}"></rect>
<rect
x="{xmin:f}" y="{ymin:f}"
width="{self.boxwidth:f}"
height="{self.boxheight:f}"
style="fill:{self.background_color.to_css()[0]}
style="fill:{self.background_color.to_css()[0]}"><title>{tooltip_text}</title></rect>
{svg_body}
/>"""
"""

if options.get("noheader", False):
return svg_body
Expand Down
39 changes: 39 additions & 0 deletions test/builtin/drawing/test_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@
),
("Plot[x*y, {x, -1, 1}]", None, "-Graphics-", None),
("Plot3D[z, {x, 1, 20}, {y, 1, 10}]", None, "-Graphics3D-", None),
(
"Graphics[{Disk[]}, Background->RGBColor[1,.1,.1]]//TeXForm//ToString",
None,
(
'\n\\begin{asy}\nusepackage("amsmath");\nsize(5.8333cm, 5.8333cm);\n'
"filldraw(box((0,0), (350,350)), rgb(1, 0.1, 0.1));\n"
"filldraw(ellipse((175,175),175,175), rgb(0, 0, 0), nullpen);\n"
"clip(box((0,0), (350,350)));\n\\end{asy}\n"
),
"Background 2D",
),
## MaxRecursion Option
(
"Plot3D[0, {x, -2, 2}, {y, -2, 2}, MaxRecursion -> 0]",
Expand Down Expand Up @@ -86,6 +97,34 @@
"\n\\begin{asy}\nimport three;\nimport solids;\nsize(6.6667cm, 6.6667cm);",
None,
),
(
"Graphics3D[{Sphere[]}, Background->RGBColor[1,.1,.1]]//TeXForm//ToString",
None,
(
"\n\\begin{asy}\n"
"import three;\n"
"import solids;\n"
"size(6.6667cm, 6.6667cm);\n"
"currentprojection=perspective(2.6,-4.8,4.0);\n"
"currentlight=light(rgb(0.5,0.5,1), background=rgb(1, 0.1, 0.1), specular=red, (2,0,2), (2,2,2), (0,2,2));\n"
"// Sphere3DBox\n"
"draw(surface(sphere((0, 0, 0), 1)), rgb(1,1,1)+opacity(1));\n"
"draw(((-1,-1,-1)--(1,-1,-1)), rgb(0.4, 0.4, 0.4)+linewidth(1));\n"
"draw(((-1,1,-1)--(1,1,-1)), rgb(0.4, 0.4, 0.4)+linewidth(1));\n"
"draw(((-1,-1,1)--(1,-1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1));\n"
"draw(((-1,1,1)--(1,1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1));\n"
"draw(((-1,-1,-1)--(-1,1,-1)), rgb(0.4, 0.4, 0.4)+linewidth(1));\n"
"draw(((1,-1,-1)--(1,1,-1)), rgb(0.4, 0.4, 0.4)+linewidth(1));\n"
"draw(((-1,-1,1)--(-1,1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1));\n"
"draw(((1,-1,1)--(1,1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1));\n"
"draw(((-1,-1,-1)--(-1,-1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1));\n"
"draw(((1,-1,-1)--(1,-1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1));\n"
"draw(((-1,1,-1)--(-1,1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1));\n"
"draw(((1,1,-1)--(1,1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1));\n"
"\\end{asy}\n"
),
"Background 3D",
),
(
"Graphics3D[Point[Table[{Sin[t], Cos[t], 0}, {t, 0, 2. Pi, Pi / 15.}]]] // TeXForm//ToString",
None,
Expand Down

0 comments on commit d4f8b6b

Please sign in to comment.