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

Feature/additional geometry file export methods #225

Merged
merged 64 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
9b5dc29
Import version for clearer file writing
josephburkhart Jun 20, 2024
741cf5e
Add OBJ export method
josephburkhart Jun 20, 2024
d690f4b
Add OFF export method
josephburkhart Jun 20, 2024
b422c92
Add STL export method
josephburkhart Jun 20, 2024
e63ff8d
Add PLY export method
josephburkhart Jun 20, 2024
08ce8b5
Add X3D export method
josephburkhart Jun 20, 2024
fe8bc7e
Add VTK export method
josephburkhart Jun 20, 2024
642f867
Add HTML export method
josephburkhart Jun 20, 2024
8c0ceb7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 20, 2024
dc50cca
Move export methods to new file
josephburkhart Jun 21, 2024
366544d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 21, 2024
8877438
Revise docstrings
josephburkhart Jun 21, 2024
7709cc2
Merge branch 'master' into Feature/Additional-geometry-file-export-me…
janbridley Jun 25, 2024
d86ff7c
Fix F841
janbridley Jun 27, 2024
7d4206c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 27, 2024
d9aff11
Change acronym import to full name
josephburkhart Jun 27, 2024
3853729
Fix star import
josephburkhart Jun 27, 2024
0ab6a6f
Fix assignments of unused variables
josephburkhart Jun 27, 2024
401703d
Add doctype declaration to HTML output
josephburkhart Jun 27, 2024
a971c24
Fix XML and HTML namespacing
josephburkhart Jun 27, 2024
308b7bb
Fix linting issue
josephburkhart Jun 27, 2024
4f6200e
Revise variable names for clarity
josephburkhart Jun 27, 2024
4d49bb1
Add module docstring
josephburkhart Jun 27, 2024
330efb0
Add convenience method to Polyhedron class
josephburkhart Jun 27, 2024
f0d9736
Revise docstring
josephburkhart Jun 27, 2024
927b2cd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 28, 2024
2113787
Change match/case to if/elif for backwards compatibility
josephburkhart Jun 28, 2024
92968f4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 28, 2024
d06cc4a
Revise docstring
josephburkhart Jun 28, 2024
fa739f5
Fix circular import
janbridley Jul 2, 2024
52a21e4
Update copyright in coxeter/io.py
janbridley Jul 2, 2024
85c0f52
Merge branch 'master' into Feature/Additional-geometry-file-export-me…
janbridley Jul 2, 2024
7f327de
Fix D401
janbridley Jul 2, 2024
85d90be
Merge branch 'master' into Feature/Additional-geometry-file-export-me…
janbridley Jul 18, 2024
3e3fd1f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 18, 2024
cbac8cf
Merge branch 'master' into Feature/Additional-geometry-file-export-me…
janbridley Jul 18, 2024
1a19890
Fix unintended shape mutation
josephburkhart Sep 12, 2024
999b09d
Limit imports to reduce loading time
josephburkhart Sep 12, 2024
94ab8e9
Create io control files
josephburkhart Sep 12, 2024
bc35493
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 12, 2024
46d0859
Fix incorrect control directory
josephburkhart Sep 12, 2024
f4ae897
Update control files
josephburkhart Sep 12, 2024
3c2078a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 12, 2024
4769ec5
Update .pre-commit-config.yaml
josephburkhart Sep 12, 2024
9a19e00
Update control files
josephburkhart Sep 12, 2024
391834c
Delete control files
josephburkhart Sep 12, 2024
3332048
Create new control files
josephburkhart Sep 12, 2024
bc7263c
Update .pre-commit-config.yaml
josephburkhart Sep 12, 2024
b5c05bb
Delete new control files
josephburkhart Sep 12, 2024
e9e568a
Create new control files
josephburkhart Sep 12, 2024
69c0377
Update .pre-commit-config.yaml
josephburkhart Sep 12, 2024
8955118
Delete new control files
josephburkhart Sep 12, 2024
f956ddc
Create new control files
josephburkhart Sep 12, 2024
37175d3
Prevent trailing newlines
josephburkhart Sep 12, 2024
fe03d11
Update control files
josephburkhart Sep 12, 2024
b6bf215
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 12, 2024
763a262
Fix pre-commit regex
janbridley Sep 12, 2024
b2a79d1
Switch to OS-agnostic file comparison
josephburkhart Sep 12, 2024
6e28a64
Check file equality by line
josephburkhart Sep 13, 2024
f260fd2
Merge branch 'master' into Feature/Additional-geometry-file-export-me…
josephburkhart Sep 13, 2024
7f45afa
Alternate solution to circular import
josephburkhart Sep 13, 2024
d8b7726
Update control files
josephburkhart Sep 13, 2024
43bc025
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 13, 2024
1c70ef1
Merge branch 'master' into Feature/Additional-geometry-file-export-me…
janbridley Sep 13, 2024
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
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions coxeter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
311 changes: 311 additions & 0 deletions coxeter/io.py
Original file line number Diff line number Diff line change
@@ -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:])]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice little snippet


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("<!DOCTYPE html>")
file.write(ElementTree.tostring(html, encoding="unicode"))
37 changes: 37 additions & 0 deletions coxeter/shapes/polyhedron.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
)
1 change: 1 addition & 0 deletions tests/control/convex_polyhedron.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!DOCTYPE html><html xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.w3.org/1999/xhtml"><head><script type="text/javascript" src="http://x3dom.org/release/x3dom.js"> </script><link rel="stylesheet" type="text/css" href="http://x3dom.org/release/x3dom.css" /></head><body><x3d profile="Interchange" version="4.0" xsi:schemaLocation="http://www.web3d.org/specifications/x3d-4.0.xsd"><Scene><shape DEF="ConvexPolyhedron"><Appearance><Material diffuseColor="#6495ED" /></Appearance><IndexedFaceSet coordIndex="0 1 2 3 -1 4 5 6 7 -1 8 9 10 11 -1 12 13 14 15 -1 16 17 18 19 -1 20 21 22 23 -1"><Coordinate point="-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 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 -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" /></IndexedFaceSet></shape></Scene></x3d></body></html>
Loading