diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e83b41ab..e016bd4e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,8 +6,9 @@ repos: rev: 'v4.6.0' hooks: - id: end-of-file-fixer + exclude: '^tests/control/.*' - id: trailing-whitespace - exclude: '(?:setup.cfg.*|paper/.*)' + exclude: '(?:setup.cfg.*|paper/.*)|^tests/control/.*' - id: debug-statements - id: check-builtin-literals - id: check-executables-have-shebangs diff --git a/coxeter/__init__.py b/coxeter/__init__.py index 5199402f..6ccb9fc9 100644 --- a/coxeter/__init__.py +++ b/coxeter/__init__.py @@ -16,9 +16,9 @@ applications such as inertia tensors. """ -from . import families, shapes -from .shape_getters import from_gsd_type_shapes +__version__ = "0.8.0" -__all__ = ["families", "shapes", "from_gsd_type_shapes"] +from . import families, io, shapes +from .shape_getters import from_gsd_type_shapes -__version__ = "0.8.0" +__all__ = ["families", "shapes", "from_gsd_type_shapes", "io"] diff --git a/coxeter/io.py b/coxeter/io.py new file mode 100644 index 00000000..3bc6c6ec --- /dev/null +++ b/coxeter/io.py @@ -0,0 +1,311 @@ +# Copyright (c) 2015-2024 The Regents of the University of Michigan. +# This file is from the coxeter project, released under the BSD 3-Clause License. + +"""Import/Export utilities for shape classes. + +This module contains functions for saving shapes to disk and creating shapes from +local files. Currently, the following formats are supported: +- Export: OBJ, OFF, STL, PLY, VTK, X3D, HTML + +These functions currently only work with `Polyhedron` and its subclasses. +""" + +import os +from copy import deepcopy +from xml.etree import ElementTree + +import numpy as np + +from coxeter import __version__ + + +def to_obj(shape, filename): + """Save shape to a wavefront OBJ file. + + Args: + filename (str, pathlib.Path, or os.PathLike): + The name or path of the output file, including the extension. + + Note: + In OBJ files, vertices in face definitions are indexed from one. + + Raises + ------ + OSError: If open() encounters a problem. + """ + content = "" + content += ( + f"# wavefront obj file written by Coxeter " + f"version {__version__}\n" + f"# {shape.__class__.__name__}\n\n" + ) + + for v in shape.vertices: + content += f"v {' '.join([str(coord) for coord in v])}\n" + + content += "\n" + + for f in shape.faces: + content += f"f {' '.join([str(v_index+1) for v_index in f])}\n" + + content = content[:-1] + + with open(filename, "w") as file: + file.write(content) + + +def to_off(shape, filename): + """Save shape to an Object File Format (OFF) file. + + Args: + filename (str, pathlib.Path, or os.PathLike): + The name or path of the output file, including the extension. + + Raises + ------ + OSError: If open() encounters a problem. + """ + content = "" + content += ( + f"OFF\n# OFF file written by Coxeter " + f"version {__version__}\n" + f"# {shape.__class__.__name__}\n" + ) + + content += f"{len(shape.vertices)} f{len(shape.faces)} " f"{len(shape.edges)}\n" + + for v in shape.vertices: + content += f"{' '.join([str(coord) for coord in v])}\n" + + for f in shape.faces: + content += f"{len(f)} {' '.join([str(v_index) for v_index in f])}\n" + + content = content[:-1] + + with open(filename, "w") as file: + file.write(content) + + +def to_stl(shape, filename): + """Save shape to a stereolithography (STL) file. + + Args: + filename (str, pathlib.Path, or os.PathLike): + The name or path of the output file, including the extension. + + Note: + The output file is ASCII-encoded. + + Raises + ------ + OSError: If open() encounters a problem. + """ + with open(filename, "w") as file: + # Ensure shape is not mutated + shape = deepcopy(shape) + + # Shift vertices so all coordinates are positive + mins = np.amin(a=shape.vertices, axis=0) + for i, m in enumerate(mins): + if m < 0: + shape.centroid[i] -= m + + # Write data + vs = shape.vertices + file.write(f"solid {shape.__class__.__name__}\n") + + for f in shape.faces: + # Decompose face into triangles + # ref: https://stackoverflow.com/a/66586936/15426433 + triangles = [[vs[f[0]], vs[b], vs[c]] for b, c in zip(f[1:], f[2:])] + + for t in triangles: + n = np.cross(t[1] - t[0], t[2] - t[1]) # order? + + file.write(f"facet normal {n[0]} {n[1]} {n[2]}\n" f"\touter loop\n") + for point in t: + file.write(f"\t\tvertex {point[0]} {point[1]} {point[2]}\n") + + file.write("\tendloop\nendfacet\n") + + file.write(f"endsolid {shape.__class__.__name__}") + + +def to_ply(shape, filename): + """Save shape to a Polygon File Format (PLY) file. + + Args: + filename (str, pathlib.Path, or os.PathLike): + The name or path of the output file, including the extension. + + Note: + The output file is ASCII-encoded. + + Raises + ------ + OSError: If open() encounters a problem. + """ + content = "" + content += ( + f"ply\nformat ascii 1.0\n" + f"comment PLY file written by Coxeter version {__version__}\n" + f"comment {shape.__class__.__name__}\n" + f"element vertex {len(shape.vertices)}\n" + f"property float x\nproperty float y\nproperty float z\n" + f"element face {len(shape.faces)}\n" + f"property list uchar uint vertex_indices\n" + f"end_header\n" + ) + + for v in shape.vertices: + content += f"{' '.join([str(coord) for coord in v])}\n" + + for f in shape.faces: + content += f"{len(f)} {' '.join([str(int(v_index)) for v_index in f])}\n" + + content = content[:-1] + + with open(filename, "w") as file: + file.write(content) + + +def to_x3d(shape, filename): + """Save shape to an Extensible 3D (X3D) file. + + Args: + filename (str, pathlib.Path, or os.PathLike): + The name or path of the output file, including the extension. + + Raises + ------ + OSError: If open() encounters a problem. + """ + # TODO: translate shape so that its centroid is at the origin + + # Parent elements + root = ElementTree.Element( + "x3d", + attrib={ + "profile": "Interchange", + "version": "4.0", + "xmlns:xsd": "http://www.w3.org/2001/XMLSchema-instance", + "xsd:schemaLocation": "http://www.web3d.org/specifications/x3d-4.0.xsd", + }, + ) + x3d_scene = ElementTree.SubElement(root, "Scene") + x3d_shape = ElementTree.SubElement( + x3d_scene, "shape", attrib={"DEF": f"{shape.__class__.__name__}"} + ) + + x3d_appearance = ElementTree.SubElement(x3d_shape, "Appearance") + ElementTree.SubElement( + x3d_appearance, "Material", attrib={"diffuseColor": "#6495ED"} + ) + + # Geometry data + point_indices = list(range(sum([len(f) for f in shape.faces]))) + prev_index = 0 + for f in shape.faces: + point_indices.insert(len(f) + prev_index, -1) + prev_index += len(f) + 1 + + points = [v for f in shape.faces for v_index in f for v in shape.vertices[v_index]] + + x3d_indexedfaceset = ElementTree.SubElement( + x3d_shape, + "IndexedFaceSet", + attrib={"coordIndex": " ".join([str(c_index) for c_index in point_indices])}, + ) + ElementTree.SubElement( + x3d_indexedfaceset, + "Coordinate", + attrib={"point": " ".join([str(p) for p in points])}, + ) + + # Write to file + ElementTree.ElementTree(root).write(filename, encoding="UTF-8") + + +def to_vtk(shape, filename): + """Save shape to a legacy VTK file. + + Args: + filename (str, pathlib.Path, or os.PathLike): + The name or path of the output file, including the extension. + + Raises + ------ + OSError: If open() encounters a problem. + """ + content = "" + # Title and Header + content += ( + f"# vtk DataFile Version 3.0\n" + f"{shape.__class__.__name__} created by " + f"Coxeter version {__version__}\n" + f"ASCII\n" + ) + + # Geometry + content += f"DATASET POLYDATA\n" f"POINTS {len(shape.vertices)} float\n" + for v in shape.vertices: + content += f"{v[0]} {v[1]} {v[2]}\n" + + num_points = len(shape.faces) + num_connections = sum([len(f) for f in shape.faces]) + content += f"POLYGONS {num_points} {num_points + num_connections}\n" + for f in shape.faces: + content += f"{len(f)} {' '.join([str(v_index) for v_index in f])}\n" + content = content.rstrip("\n") + + # Write file + with open(filename, "wb") as file: + file.write(content.encode("ascii")) + + +def to_html(shape, filename): + """Save shape to an HTML file. + + This method calls shape.to_x3d to create a temporary X3D file, then + parses that X3D file and creates an HTML file in its place. + + Args: + filename (str, pathlib.Path, or os.PathLike): + The name or path of the output file, including the extension. + + Raises + ------ + OSError: If open() encounters a problem. + """ + # Create, parse, and remove x3d file + to_x3d(shape, filename) + x3d = ElementTree.parse(filename) + os.remove(filename) + + # HTML Head + html = ElementTree.Element("html", attrib={"xmlns": "http://www.w3.org/1999/xhtml"}) + head = ElementTree.SubElement(html, "head") + script = ElementTree.SubElement( + head, + "script", + attrib={"type": "text/javascript", "src": "http://x3dom.org/release/x3dom.js"}, + ) + script.text = " " # ensures the tag is not shape-closing + ElementTree.SubElement( + head, + "link", + attrib={ + "rel": "stylesheet", + "type": "text/css", + "href": "http://x3dom.org/release/x3dom.css", + }, + ) + + # HTML body + body = ElementTree.SubElement(html, "body") + body.append(x3d.getroot()) + + # Write file + with open(filename, "w") as file: + file.write("") + file.write(ElementTree.tostring(html, encoding="unicode")) diff --git a/coxeter/shapes/polyhedron.py b/coxeter/shapes/polyhedron.py index 6a3290e8..54c1081d 100644 --- a/coxeter/shapes/polyhedron.py +++ b/coxeter/shapes/polyhedron.py @@ -10,6 +10,7 @@ import rowan from scipy.sparse.csgraph import connected_components +from .. import io from ..extern.polytri import polytri from .base_classes import Shape3D from .convex_polygon import ConvexPolygon, _is_convex @@ -1020,3 +1021,39 @@ def to_hoomd(self): self.centroid = old_centroid return hoomd_dict + + def save(self, filetype, filename): + """Save the polyhedron object to a file using methods from ``coxeter.io``. + + Args: + filetype (str): + The file format to export polyhedron to. Must be one of the following: + OBJ, OFF, STL, PLY, VTK, X3D, HTML. + + filename (str, pathlib.Path, or os.PathLike): + The name or path of the output file, including the extension. + + Raises + ------ + ValueError: If filetype is not one of the required strings. + OSError: If open() encounters a problem. + """ + if filetype == "OBJ": + io.to_obj(self, filename) + elif filetype == "OFF": + io.to_off(self, filename) + elif filetype == "STL": + io.to_stl(self, filename) + elif filetype == "PLY": + io.to_ply(self, filename) + elif filetype == "VTK": + io.to_vtk(self, filename) + elif filetype == "X3D": + io.to_x3d(self, filename) + elif filetype == "HTML": + io.to_html(self, filename) + else: + raise ValueError( + "filetype must be one of the following: OBJ, OFF, " + "STL, PLY, VTK, X3D, HTML" + ) diff --git a/tests/control/convex_polyhedron.html b/tests/control/convex_polyhedron.html new file mode 100644 index 00000000..f27b53d1 --- /dev/null +++ b/tests/control/convex_polyhedron.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/control/convex_polyhedron.obj b/tests/control/convex_polyhedron.obj new file mode 100644 index 00000000..7a3ba776 --- /dev/null +++ b/tests/control/convex_polyhedron.obj @@ -0,0 +1,18 @@ +# wavefront obj file written by Coxeter version 0.8.0 +# ConvexPolyhedron + +v -1.0 -1.0 -1.0 +v -1.0 -1.0 1.0 +v -1.0 1.0 -1.0 +v -1.0 1.0 1.0 +v 1.0 -1.0 -1.0 +v 1.0 -1.0 1.0 +v 1.0 1.0 -1.0 +v 1.0 1.0 1.0 + +f 1 3 7 5 +f 1 5 6 2 +f 5 7 8 6 +f 1 2 4 3 +f 3 4 8 7 +f 2 6 8 4 \ No newline at end of file diff --git a/tests/control/convex_polyhedron.off b/tests/control/convex_polyhedron.off new file mode 100644 index 00000000..abbdbbdf --- /dev/null +++ b/tests/control/convex_polyhedron.off @@ -0,0 +1,18 @@ +OFF +# OFF file written by Coxeter version 0.8.0 +# ConvexPolyhedron +8 f6 12 +-1.0 -1.0 -1.0 +-1.0 -1.0 1.0 +-1.0 1.0 -1.0 +-1.0 1.0 1.0 +1.0 -1.0 -1.0 +1.0 -1.0 1.0 +1.0 1.0 -1.0 +1.0 1.0 1.0 +4 0 2 6 4 +4 0 4 5 1 +4 4 6 7 5 +4 0 1 3 2 +4 2 3 7 6 +4 1 5 7 3 \ No newline at end of file diff --git a/tests/control/convex_polyhedron.ply b/tests/control/convex_polyhedron.ply new file mode 100644 index 00000000..f295bac9 --- /dev/null +++ b/tests/control/convex_polyhedron.ply @@ -0,0 +1,25 @@ +ply +format ascii 1.0 +comment PLY file written by Coxeter version 0.8.0 +comment ConvexPolyhedron +element vertex 8 +property float x +property float y +property float z +element face 6 +property list uchar uint vertex_indices +end_header +-1.0 -1.0 -1.0 +-1.0 -1.0 1.0 +-1.0 1.0 -1.0 +-1.0 1.0 1.0 +1.0 -1.0 -1.0 +1.0 -1.0 1.0 +1.0 1.0 -1.0 +1.0 1.0 1.0 +4 0 2 6 4 +4 0 4 5 1 +4 4 6 7 5 +4 0 1 3 2 +4 2 3 7 6 +4 1 5 7 3 \ No newline at end of file diff --git a/tests/control/convex_polyhedron.stl b/tests/control/convex_polyhedron.stl new file mode 100644 index 00000000..9d9a416e --- /dev/null +++ b/tests/control/convex_polyhedron.stl @@ -0,0 +1,86 @@ +solid ConvexPolyhedron +facet normal 0.0 0.0 -4.0 + outer loop + vertex -1.0 -1.0 -1.0 + vertex -1.0 1.0 -1.0 + vertex 1.0 1.0 -1.0 + endloop +endfacet +facet normal 0.0 0.0 -4.0 + outer loop + vertex -1.0 -1.0 -1.0 + vertex 1.0 1.0 -1.0 + vertex 1.0 -1.0 -1.0 + endloop +endfacet +facet normal 0.0 -4.0 0.0 + outer loop + vertex -1.0 -1.0 -1.0 + vertex 1.0 -1.0 -1.0 + vertex 1.0 -1.0 1.0 + endloop +endfacet +facet normal 0.0 -4.0 0.0 + outer loop + vertex -1.0 -1.0 -1.0 + vertex 1.0 -1.0 1.0 + vertex -1.0 -1.0 1.0 + endloop +endfacet +facet normal 4.0 0.0 0.0 + outer loop + vertex 1.0 -1.0 -1.0 + vertex 1.0 1.0 -1.0 + vertex 1.0 1.0 1.0 + endloop +endfacet +facet normal 4.0 0.0 -0.0 + outer loop + vertex 1.0 -1.0 -1.0 + vertex 1.0 1.0 1.0 + vertex 1.0 -1.0 1.0 + endloop +endfacet +facet normal -4.0 0.0 0.0 + outer loop + vertex -1.0 -1.0 -1.0 + vertex -1.0 -1.0 1.0 + vertex -1.0 1.0 1.0 + endloop +endfacet +facet normal -4.0 0.0 0.0 + outer loop + vertex -1.0 -1.0 -1.0 + vertex -1.0 1.0 1.0 + vertex -1.0 1.0 -1.0 + endloop +endfacet +facet normal 0.0 4.0 0.0 + outer loop + vertex -1.0 1.0 -1.0 + vertex -1.0 1.0 1.0 + vertex 1.0 1.0 1.0 + endloop +endfacet +facet normal -0.0 4.0 0.0 + outer loop + vertex -1.0 1.0 -1.0 + vertex 1.0 1.0 1.0 + vertex 1.0 1.0 -1.0 + endloop +endfacet +facet normal 0.0 0.0 4.0 + outer loop + vertex -1.0 -1.0 1.0 + vertex 1.0 -1.0 1.0 + vertex 1.0 1.0 1.0 + endloop +endfacet +facet normal 0.0 -0.0 4.0 + outer loop + vertex -1.0 -1.0 1.0 + vertex 1.0 1.0 1.0 + vertex -1.0 1.0 1.0 + endloop +endfacet +endsolid ConvexPolyhedron \ No newline at end of file diff --git a/tests/control/convex_polyhedron.vtk b/tests/control/convex_polyhedron.vtk new file mode 100644 index 00000000..5cdef7f5 --- /dev/null +++ b/tests/control/convex_polyhedron.vtk @@ -0,0 +1,20 @@ +# vtk DataFile Version 3.0 +ConvexPolyhedron created by Coxeter version 0.8.0 +ASCII +DATASET POLYDATA +POINTS 8 float +-1.0 -1.0 -1.0 +-1.0 -1.0 1.0 +-1.0 1.0 -1.0 +-1.0 1.0 1.0 +1.0 -1.0 -1.0 +1.0 -1.0 1.0 +1.0 1.0 -1.0 +1.0 1.0 1.0 +POLYGONS 6 30 +4 0 2 6 4 +4 0 4 5 1 +4 4 6 7 5 +4 0 1 3 2 +4 2 3 7 6 +4 1 5 7 3 \ No newline at end of file diff --git a/tests/control/convex_polyhedron.x3d b/tests/control/convex_polyhedron.x3d new file mode 100644 index 00000000..d673d9e4 --- /dev/null +++ b/tests/control/convex_polyhedron.x3d @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/control/polyhedron.html b/tests/control/polyhedron.html new file mode 100644 index 00000000..3c4d4187 --- /dev/null +++ b/tests/control/polyhedron.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/control/polyhedron.obj b/tests/control/polyhedron.obj new file mode 100644 index 00000000..73d79f00 --- /dev/null +++ b/tests/control/polyhedron.obj @@ -0,0 +1,18 @@ +# wavefront obj file written by Coxeter version 0.8.0 +# Polyhedron + +v -1.0 -1.0 -1.0 +v -1.0 -1.0 1.0 +v -1.0 1.0 -1.0 +v -1.0 1.0 1.0 +v 1.0 -1.0 -1.0 +v 1.0 -1.0 1.0 +v 1.0 1.0 -1.0 +v 1.0 1.0 1.0 + +f 1 2 4 3 +f 1 3 7 5 +f 5 7 8 6 +f 2 6 8 4 +f 1 5 6 2 +f 7 3 4 8 \ No newline at end of file diff --git a/tests/control/polyhedron.off b/tests/control/polyhedron.off new file mode 100644 index 00000000..bfe2e196 --- /dev/null +++ b/tests/control/polyhedron.off @@ -0,0 +1,18 @@ +OFF +# OFF file written by Coxeter version 0.8.0 +# Polyhedron +8 f6 12 +-1.0 -1.0 -1.0 +-1.0 -1.0 1.0 +-1.0 1.0 -1.0 +-1.0 1.0 1.0 +1.0 -1.0 -1.0 +1.0 -1.0 1.0 +1.0 1.0 -1.0 +1.0 1.0 1.0 +4 0 1 3 2 +4 0 2 6 4 +4 4 6 7 5 +4 1 5 7 3 +4 0 4 5 1 +4 6 2 3 7 \ No newline at end of file diff --git a/tests/control/polyhedron.ply b/tests/control/polyhedron.ply new file mode 100644 index 00000000..2de634b2 --- /dev/null +++ b/tests/control/polyhedron.ply @@ -0,0 +1,25 @@ +ply +format ascii 1.0 +comment PLY file written by Coxeter version 0.8.0 +comment Polyhedron +element vertex 8 +property float x +property float y +property float z +element face 6 +property list uchar uint vertex_indices +end_header +-1.0 -1.0 -1.0 +-1.0 -1.0 1.0 +-1.0 1.0 -1.0 +-1.0 1.0 1.0 +1.0 -1.0 -1.0 +1.0 -1.0 1.0 +1.0 1.0 -1.0 +1.0 1.0 1.0 +4 0 1 3 2 +4 0 2 6 4 +4 4 6 7 5 +4 1 5 7 3 +4 0 4 5 1 +4 6 2 3 7 \ No newline at end of file diff --git a/tests/control/polyhedron.stl b/tests/control/polyhedron.stl new file mode 100644 index 00000000..36280262 --- /dev/null +++ b/tests/control/polyhedron.stl @@ -0,0 +1,86 @@ +solid Polyhedron +facet normal -4.0 0.0 0.0 + outer loop + vertex -1.0 -1.0 -1.0 + vertex -1.0 -1.0 1.0 + vertex -1.0 1.0 1.0 + endloop +endfacet +facet normal -4.0 0.0 0.0 + outer loop + vertex -1.0 -1.0 -1.0 + vertex -1.0 1.0 1.0 + vertex -1.0 1.0 -1.0 + endloop +endfacet +facet normal 0.0 0.0 -4.0 + outer loop + vertex -1.0 -1.0 -1.0 + vertex -1.0 1.0 -1.0 + vertex 1.0 1.0 -1.0 + endloop +endfacet +facet normal 0.0 0.0 -4.0 + outer loop + vertex -1.0 -1.0 -1.0 + vertex 1.0 1.0 -1.0 + vertex 1.0 -1.0 -1.0 + endloop +endfacet +facet normal 4.0 0.0 0.0 + outer loop + vertex 1.0 -1.0 -1.0 + vertex 1.0 1.0 -1.0 + vertex 1.0 1.0 1.0 + endloop +endfacet +facet normal 4.0 0.0 -0.0 + outer loop + vertex 1.0 -1.0 -1.0 + vertex 1.0 1.0 1.0 + vertex 1.0 -1.0 1.0 + endloop +endfacet +facet normal 0.0 0.0 4.0 + outer loop + vertex -1.0 -1.0 1.0 + vertex 1.0 -1.0 1.0 + vertex 1.0 1.0 1.0 + endloop +endfacet +facet normal 0.0 -0.0 4.0 + outer loop + vertex -1.0 -1.0 1.0 + vertex 1.0 1.0 1.0 + vertex -1.0 1.0 1.0 + endloop +endfacet +facet normal 0.0 -4.0 0.0 + outer loop + vertex -1.0 -1.0 -1.0 + vertex 1.0 -1.0 -1.0 + vertex 1.0 -1.0 1.0 + endloop +endfacet +facet normal 0.0 -4.0 0.0 + outer loop + vertex -1.0 -1.0 -1.0 + vertex 1.0 -1.0 1.0 + vertex -1.0 -1.0 1.0 + endloop +endfacet +facet normal 0.0 4.0 -0.0 + outer loop + vertex 1.0 1.0 -1.0 + vertex -1.0 1.0 -1.0 + vertex -1.0 1.0 1.0 + endloop +endfacet +facet normal 0.0 4.0 -0.0 + outer loop + vertex 1.0 1.0 -1.0 + vertex -1.0 1.0 1.0 + vertex 1.0 1.0 1.0 + endloop +endfacet +endsolid Polyhedron \ No newline at end of file diff --git a/tests/control/polyhedron.vtk b/tests/control/polyhedron.vtk new file mode 100644 index 00000000..a8d8ce23 --- /dev/null +++ b/tests/control/polyhedron.vtk @@ -0,0 +1,20 @@ +# vtk DataFile Version 3.0 +Polyhedron created by Coxeter version 0.8.0 +ASCII +DATASET POLYDATA +POINTS 8 float +-1.0 -1.0 -1.0 +-1.0 -1.0 1.0 +-1.0 1.0 -1.0 +-1.0 1.0 1.0 +1.0 -1.0 -1.0 +1.0 -1.0 1.0 +1.0 1.0 -1.0 +1.0 1.0 1.0 +POLYGONS 6 30 +4 0 1 3 2 +4 0 2 6 4 +4 4 6 7 5 +4 1 5 7 3 +4 0 4 5 1 +4 6 2 3 7 \ No newline at end of file diff --git a/tests/control/polyhedron.x3d b/tests/control/polyhedron.x3d new file mode 100644 index 00000000..727efcb4 --- /dev/null +++ b/tests/control/polyhedron.x3d @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/test_io.py b/tests/test_io.py new file mode 100644 index 00000000..5e588a09 --- /dev/null +++ b/tests/test_io.py @@ -0,0 +1,99 @@ +# Copyright (c) 2015-2024 The Regents of the University of Michigan. +# This file is from the coxeter project, released under the BSD 3-Clause License. + +# Copyright (c) 2024 The Regents of the University of Michigan +# All rights reserved. +# This software is licensed under the BSD 3-Clause License. +"""Regression tests for the I/O module.""" + +import tempfile +from pathlib import Path + +import pytest + +from coxeter import io +from coxeter.shapes import ConvexPolyhedron, Polyhedron + + +def compare_text_files(file_path_1, file_path_2): + """Check whether two text files have identical contents, ignoring different + newline characters.""" + with open(file_path_1) as file1, open(file_path_2) as file2: + file1_contents, file2_contents = file1.readlines(), file2.readlines() + assert file1_contents == file2_contents + + +EXPORT_FUNCS_BY_FILE_TYPE = { + "obj": io.to_obj, + "off": io.to_off, + "stl": io.to_stl, + "ply": io.to_ply, + "x3d": io.to_x3d, + "vtk": io.to_vtk, + "html": io.to_html, +} + +SHAPES_BY_NAME = { + "polyhedron": Polyhedron( + vertices=[ + [-1, -1, -1], + [-1, -1, 1], + [-1, 1, -1], + [-1, 1, 1], + [1, -1, -1], + [1, -1, 1], + [1, 1, -1], + [1, 1, 1], + ], + faces=[ + [0, 1, 3, 2], + [0, 2, 6, 4], + [4, 6, 7, 5], + [1, 5, 7, 3], + [0, 4, 5, 1], + [6, 2, 3, 7], + ], + ), + "convex_polyhedron": ConvexPolyhedron( + vertices=[ + [-1, -1, -1], + [-1, -1, 1], + [-1, 1, -1], + [-1, 1, 1], + [1, -1, -1], + [1, -1, 1], + [1, 1, -1], + [1, 1, 1], + ] + ), +} + +CONTROL_DIR = Path("tests/control") + + +@pytest.mark.parametrize( + "file_type,export_func,shape_name", + [ + (ft, func, name) + for ft, func in EXPORT_FUNCS_BY_FILE_TYPE.items() + for name in SHAPES_BY_NAME.keys() + ], +) +def test_regression(file_type, export_func, shape_name): + """Check that export functions yield files identical to the control.""" + control_file_path = CONTROL_DIR / f"{shape_name}.{file_type}" + shape = SHAPES_BY_NAME[shape_name] + + with tempfile.TemporaryDirectory(dir=CONTROL_DIR) as tempdir: + test_file_path = Path(tempdir) / f"test_{shape_name}.{file_type}" + export_func(shape=shape, filename=test_file_path) + + compare_text_files(control_file_path, test_file_path) + + +if __name__ == "__main__": + # Generate new control files + for name in SHAPES_BY_NAME.keys(): + for ft, func in EXPORT_FUNCS_BY_FILE_TYPE.items(): + control_file_path = CONTROL_DIR / f"{name}.{ft}" + func(shape=SHAPES_BY_NAME[name], filename=control_file_path)