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("") + # TODO: # mmatera: please put in pytest conditionally # >> System`Convert`B64Dump`B64Encode["∫ f  x"]