Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handle box errors in graphics and graphics3d #966

Merged
merged 6 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading