diff --git a/mathics/builtin/box/graphics3d.py b/mathics/builtin/box/graphics3d.py
index 3597c7c7c..a253fca94 100644
--- a/mathics/builtin/box/graphics3d.py
+++ b/mathics/builtin/box/graphics3d.py
@@ -694,12 +694,85 @@ def get_boundbox_lines(self, xmin, xmax, ymin, ymax, zmin, zmax):
]
-class Point3DBox(PointBox):
+class Cylinder3DBox(_Graphics3DElement):
+ """
+ Internal Python class used when Boxing a 'Cylinder' object.
+ """
+
+ def init(self, graphics, style, item):
+ super(Cylinder3DBox, self).init(graphics, item, style)
+
+ self.edge_color, self.face_color = style.get_style(_Color, face_element=True)
+
+ if len(item.leaves) != 2:
+ raise BoxConstructError
+
+ points = item.leaves[0].to_python()
+ if not all(
+ len(point) == 3 and all(isinstance(p, numbers.Real) for p in point)
+ for point in points
+ ):
+ raise BoxConstructError
+
+ self.points = [Coords3D(graphics, pos=point) for point in points]
+ self.radius = item.leaves[1].to_python()
+
+ def to_asy(self):
+ # l = self.style.get_line_width(face_element=True)
+
+ if self.face_color is None:
+ face_color = (1, 1, 1)
+ else:
+ face_color = self.face_color.to_js()
+
+ rgb = f"rgb({face_color[0]}, {face_color[1]}, {face_color[2]})"
+ return "".join(
+ f"draw(surface(cylinder({tuple(coord.pos()[0])}, {self.radius}, {self.height})), {rgb});"
+ for coord in self.points
+ )
+
+ def to_json(self):
+ face_color = self.face_color
+ if face_color is not None:
+ face_color = face_color.to_js()
+ return [
+ {
+ "type": "cylinder",
+ "coords": [coords.pos() for coords in self.points],
+ "radius": self.radius,
+ "faceColor": face_color,
+ }
+ ]
+
+ def extent(self):
+ result = []
+ # FIXME: instead of `coords.add(±self.radius, ±self.radius, ±self.radius)` we should do:
+ # coords.add(transformation_vector.x * ±self.radius, transformation_vector.y * ±self.radius, transformation_vector.z * ±self.radius)
+ result.extend(
+ [
+ coords.add(self.radius, self.radius, self.radius).pos()[0]
+ for coords in self.points
+ ]
+ )
+ result.extend(
+ [
+ coords.add(-self.radius, -self.radius, -self.radius).pos()[0]
+ for coords in self.points
+ ]
+ )
+ return result
+
+ def _apply_boxscaling(self, boxscale):
+ # TODO
+ pass
+
+
+class Line3DBox(LineBox):
def init(self, *args, **kwargs):
- super(Point3DBox, self).init(*args, **kwargs)
+ super(Line3DBox, self).init(*args, **kwargs)
def process_option(self, name, value):
- super(Point3DBox, self).process_option(name, value)
+ super(Line3DBox, self).process_option(name, value)
def extent(self):
result = []
@@ -715,12 +788,12 @@ def _apply_boxscaling(self, boxscale):
coords.scale(boxscale)
-class Line3DBox(LineBox):
+class Point3DBox(PointBox):
def init(self, *args, **kwargs):
- super(Line3DBox, self).init(*args, **kwargs)
+ super(Point3DBox, self).init(*args, **kwargs)
def process_option(self, name, value):
- super(Line3DBox, self).process_option(name, value)
+ super(Point3DBox, self).process_option(name, value)
def extent(self):
result = []
@@ -805,9 +878,10 @@ def _apply_boxscaling(self, boxscale):
# FIXME: GLOBALS3D is a horrible name.
GLOBALS3D.update(
{
- "System`Polygon3DBox": Polygon3DBox,
+ "System`Cylinder3DBox": Cylinder3DBox,
"System`Line3DBox": Line3DBox,
"System`Point3DBox": Point3DBox,
+ "System`Polygon3DBox": Polygon3DBox,
"System`Sphere3DBox": Sphere3DBox,
}
)
diff --git a/mathics/builtin/drawing/graphics3d.py b/mathics/builtin/drawing/graphics3d.py
index 1ff8ca45d..3fbe85ce4 100644
--- a/mathics/builtin/drawing/graphics3d.py
+++ b/mathics/builtin/drawing/graphics3d.py
@@ -335,6 +335,43 @@ def apply_min(self, xmin, ymin, zmin, evaluation):
return self.apply_full(xmin, ymin, zmin, xmax, ymax, zmax, evaluation)
+class Cylinder(Builtin):
+ """
+
+ - 'Cylinder[{{$x1$, $y1$, $z1$}, {$x2$, $y2$, $z2$}}]'
+
- represents a cylinder of radius 1.
+
- 'Cylinder[{{$x1$, $y1$, $z1$}, {$x2$, $y2$, $z2$}}, $r$]'
+
- is a cylinder of radius $r$ starting at ($x1$, $y1$, $z1$) and ending at ($x2$, $y2$, $z2$).
+
- 'Cylinder[{{$x1$, $y1$, $z1$}, {$x2$, $y2$, $z2$}, ... }, $r$]'
+
- is a collection cylinders of radius $r$
+
+
+ >> Graphics3D[Cylinder[{{0, 0, 0}, {1, 1, 1}}, 1]]
+ = -Graphics3D-
+
+ >> Graphics3D[{Yellow, Cylinder[{{-1, 0, 0}, {1, 0, 0}, {0, 0, Sqrt[3]}, {1, 1, Sqrt[3]}}, 1]}]
+ = -Graphics3D-
+ """
+
+ rules = {
+ "Cylinder[]": "Cylinder[{{0, 0, 0}, {1, 1, 1}}, 1]",
+ "Cylinder[positions_]": "Cylinder[positions, 1]",
+ }
+
+ messages = {
+ "oddn": "The number of points must be even."
+ }
+
+ def apply_check(self, positions, radius, evaluation):
+ "Cylinder[positions_, radius_?NumericQ]"
+
+ if len(positions.get_leaves()) % 2 == 1:
+ # number of points is odd so abort
+ evaluation.error("Cylinder", "oddn", positions)
+
+ return Expression("Cylinder", positions, radius)
+
+
class _Graphics3DElement(InstanceableBuiltin):
def init(self, graphics, item=None, style=None):
if item is not None and not item.has_form(self.get_name(), None):
diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py
index fe04e21ea..0e5037ade 100644
--- a/mathics/builtin/graphics.py
+++ b/mathics/builtin/graphics.py
@@ -1361,20 +1361,21 @@ class Large(Builtin):
element_heads = frozenset(
system_symbols(
- "Rectangle",
- "Disk",
- "Line",
"Arrow",
- "FilledCurve",
"BezierCurve",
- "Point",
"Circle",
+ "Cylinder",
+ "Disk",
+ "FilledCurve",
+ "Inset",
+ "Line",
+ "Point",
"Polygon",
+ "Rectangle",
"RegularPolygon",
- "Inset",
- "Text",
"Sphere",
"Style",
+ "Text",
)
)
diff --git a/mathics/builtin/lists.py b/mathics/builtin/lists.py
index 1c96c5bc1..e51aabe32 100644
--- a/mathics/builtin/lists.py
+++ b/mathics/builtin/lists.py
@@ -477,7 +477,7 @@ def rec(cur, rest):
def set_part(varlist, indices, newval):
- " Simple part replacement. indices must be a list of python integers. "
+ "Simple part replacement. indices must be a list of python integers."
def rec(cur, rest):
if len(rest) > 1:
diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py
index 1bd88e079..224db67a5 100644
--- a/mathics/core/definitions.py
+++ b/mathics/core/definitions.py
@@ -32,7 +32,7 @@ def get_file_time(file) -> float:
def valuesname(name) -> str:
- " 'NValues' -> 'n' "
+ "'NValues' -> 'n'"
assert name.startswith("System`"), name
if name == "System`Messages":
diff --git a/mathics/core/expression.py b/mathics/core/expression.py
index 64bf459fd..ec49c97a2 100644
--- a/mathics/core/expression.py
+++ b/mathics/core/expression.py
@@ -309,7 +309,7 @@ def get_atoms(self, include_heads=True):
return []
def get_name(self):
- " Returns symbol's name if Symbol instance "
+ "Returns symbol's name if Symbol instance"
return ""
@@ -320,7 +320,7 @@ def is_machine_precision(self) -> bool:
return False
def get_lookup_name(self):
- " Returns symbol name of leftmost head "
+ "Returns symbol name of leftmost head"
return self.get_name()
@@ -1667,7 +1667,7 @@ def default_format(self, evaluation, form) -> str:
)
def sort(self, pattern=False):
- " Sort the leaves according to internal ordering. "
+ "Sort the leaves according to internal ordering."
leaves = list(self._leaves)
if pattern:
leaves.sort(key=lambda e: e.get_sort_key(pattern_sort=True))
@@ -2048,7 +2048,7 @@ def get_sort_key(self, pattern_sort=False):
]
def equal2(self, rhs: Any) -> Optional[bool]:
- """Mathics two-argument Equal (==) """
+ """Mathics two-argument Equal (==)"""
if self.sameQ(rhs):
return True
diff --git a/mathics/core/streams.py b/mathics/core/streams.py
index b4d9f17a5..cedbfd61b 100644
--- a/mathics/core/streams.py
+++ b/mathics/core/streams.py
@@ -89,13 +89,13 @@ class StreamsManager(object):
@staticmethod
def get_instance():
- """ Static access method. """
+ """Static access method."""
if StreamsManager.__instance == None:
StreamsManager()
return StreamsManager.__instance
def __init__(self):
- """ Virtually private constructor. """
+ """Virtually private constructor."""
if StreamsManager.__instance != None:
raise Exception("this class is a singleton!")
else:
diff --git a/mathics/format/asy.py b/mathics/format/asy.py
index 3ad8fa1fe..5e2a61e4b 100644
--- a/mathics/format/asy.py
+++ b/mathics/format/asy.py
@@ -19,6 +19,7 @@
from mathics.builtin.box.graphics3d import (
Graphics3DElements,
+ Cylinder3DBox,
Line3DBox,
Point3DBox,
Polygon3DBox,
@@ -151,6 +152,38 @@ def bezier_curve_box(self, **options) -> str:
add_conversion_fn(BezierCurveBox, bezier_curve_box)
+def cylinder3dbox(self, **options) -> str:
+ if self.face_color is None:
+ face_color = (1, 1, 1)
+ else:
+ face_color = self.face_color.to_js()
+
+ asy = ""
+ i = 0
+ while i < len(self.points) / 2:
+ asy += "draw(surface(cylinder({0}, {1}, {2}, {3})), rgb({2},{3},{4}));".format(
+ tuple(self.points[i * 2].pos()[0]),
+ self.radius,
+
+ # distance between start and end
+ (
+ (self.points[i * 2][0][0] - self.points[i * 2 + 1][0][0])**2 +
+ (self.points[i * 2][0][1] - self.points[i * 2 + 1][0][1])**2 +
+ (self.points[i * 2][0][2] - self.points[i * 2 + 1][0][2])**2
+ ) ** 0.5,
+
+ (1, 1, 0), # FIXME: currently always drawing around the axis X+Y
+ *face_color[:3]
+ )
+
+ i += 1
+
+ return asy
+
+
+add_conversion_fn(Cylinder3DBox)
+
+
def filled_curve_box(self, **options) -> str:
line_width = self.style.get_line_width(face_element=False)
pen = asy_create_pens(edge_color=self.edge_color, stroke_width=line_width)
@@ -310,7 +343,7 @@ def polygon3dbox(self, **options) -> str:
add_conversion_fn(Polygon3DBox)
-def polygonbox(self, **options):
+def polygonbox(self, **options) -> str:
line_width = self.style.get_line_width(face_element=True)
if self.vertex_colors is None:
face_color = self.face_color
diff --git a/mathics/format/json.py b/mathics/format/json.py
index 15fae093e..72d22dd90 100644
--- a/mathics/format/json.py
+++ b/mathics/format/json.py
@@ -10,6 +10,7 @@
)
from mathics.builtin.box.graphics3d import (
+ Cylinder3DBox,
Line3DBox,
Point3DBox,
Polygon3DBox,
@@ -39,6 +40,23 @@ def graphics_3D_elements(self, **options):
add_conversion_fn(Graphics3DElements, graphics_3D_elements)
+def cylinder_3d_box(self):
+ face_color = self.face_color
+ if face_color is not None:
+ face_color = face_color.to_js()
+ return [
+ {
+ "type": "cylinder",
+ "coords": [coords.pos() for coords in self.points],
+ "radius": self.radius,
+ "faceColor": face_color,
+ }
+ ]
+
+
+add_conversion_fn(Cylinder3DBox, cylinder_3d_box)
+
+
def line_3d_box(self):
# TODO: account for line widths and style
data = []
diff --git a/test/test_importexport.py b/test/test_importexport.py
index 4203c5d5c..7f4202952 100644
--- a/test/test_importexport.py
+++ b/test/test_importexport.py
@@ -10,25 +10,36 @@ def test_import():
eaccent = "\xe9"
for str_expr, str_expected, message in (
(
- """StringTake[Import["ExampleData/Middlemarch.txt", CharacterEncoding -> "ISO8859-1"], {49, 69}]""",
- f"des plaisirs pr{eaccent}sents",
- "accented characters in Import"
+ """StringTake[Import["ExampleData/Middlemarch.txt", CharacterEncoding -> "ISO8859-1"], {49, 69}]""",
+ f"des plaisirs pr{eaccent}sents",
+ "accented characters in Import",
),
):
check_evaluation(str_expr, str_expected, message)
-def run_export(temp_dirname: str, short_name: str, file_data:str, character_encoding):
+
+def run_export(temp_dirname: str, short_name: str, file_data: str, character_encoding):
file_path = osp.join(temp_dirname, short_name)
expr = fr'Export["{file_path}", {file_data}'
- expr += ', CharacterEncoding -> "{character_encoding}"' if character_encoding else ""
+ expr += (
+ ', CharacterEncoding -> "{character_encoding}"' if character_encoding else ""
+ )
expr += "]"
result = session.evaluate(expr)
assert result.to_python(string_quotes=False) == file_path
return file_path
-def check_data(temp_dirname: str, short_name: str, file_data:str,
- character_encoding=None, expected_data=None):
- file_path = run_export(temp_dirname, short_name, fr'"{file_data}"', character_encoding)
+
+def check_data(
+ temp_dirname: str,
+ short_name: str,
+ file_data: str,
+ character_encoding=None,
+ expected_data=None,
+):
+ file_path = run_export(
+ temp_dirname, short_name, fr'"{file_data}"', character_encoding
+ )
if expected_data is None:
expected_data = file_data
assert open(file_path, "r").read() == expected_data
@@ -38,6 +49,7 @@ def check_data(temp_dirname: str, short_name: str, file_data:str,
# a tempfile.TemporaryDirectory context manager.
# Leave out until we figure how to work around this.
if not (os.environ.get("CI", False) or sys.platform in ("win32",)):
+
def test_export():
with tempfile.TemporaryDirectory(prefix="mtest-") as temp_dirname:
# Check exporting text files (file extension ".txt")
@@ -46,16 +58,21 @@ def test_export():
check_data(temp_dirname, "AAcuteUTF.txt", "\u00C1", "UTF-8")
# Check exporting CSV files (file extension ".csv")
- file_path = run_export(temp_dirname, "csv_list.csv", "{{1, 2, 3}, {4, 5, 6}}", None)
+ file_path = run_export(
+ temp_dirname, "csv_list.csv", "{{1, 2, 3}, {4, 5, 6}}", None
+ )
assert open(file_path, "r").read() == "1,2,3\n4,5,6"
# Check exporting SVG files (file extension ".svg")
- file_path = run_export(temp_dirname, "sine.svg", "Plot[Sin[x], {x,0,1}]", None)
+ file_path = run_export(
+ temp_dirname, "sine.svg", "Plot[Sin[x], {x,0,1}]", None
+ )
data = open(file_path, "r").read().strip()
if not os.environ.get("CI", None):
assert data.startswith("