# cubeforge/writers.py
import struct
import logging
import abc # Abstract Base Classes
logger = logging.getLogger(__name__)
[docs]
class MeshWriterBase(abc.ABC):
"""Abstract base class for mesh file writers."""
[docs]
@abc.abstractmethod
def write(self, triangles, filename, **kwargs):
"""
Writes the mesh data to a file.
Args:
triangles (list): A list of tuples, where each tuple represents a
triangle: (normal, vertex1, vertex2, vertex3).
Vertices and normals should be tuples/lists of 3 floats.
filename (str): The path to the output file.
**kwargs: Additional format-specific arguments.
"""
pass
[docs]
class StlAsciiWriter(MeshWriterBase):
"""Writes mesh data to an ASCII STL file."""
[docs]
def write(self, triangles, filename, **kwargs):
"""
Writes triangles to an ASCII STL file.
Args:
triangles (list): List of (normal, v1, v2, v3) tuples.
filename (str): Output filename.
**kwargs: Expects 'solid_name' (str, optional).
"""
solid_name = kwargs.get("solid_name", "cubeforge_model")
# Use the logger instance obtained at the module level
logger.info(f"Writing ASCII STL file: {filename} with {len(triangles)} triangles.")
try:
with open(filename, 'w') as f:
f.write(f"solid {solid_name}\n")
for normal, v1, v2, v3 in triangles:
self._write_triangle(f, normal, v1, v2, v3)
f.write(f"endsolid {solid_name}\n")
logger.info(f"Successfully wrote ASCII STL file: {filename}")
except IOError as e:
logger.error(f"Failed to write ASCII STL file {filename}: {e}")
raise
def _write_triangle(self, f, normal, v1, v2, v3):
"""Writes a single triangle in ASCII STL format."""
f.write(f" facet normal {normal[0]:.6f} {normal[1]:.6f} {normal[2]:.6f}\n")
f.write(" outer loop\n")
f.write(f" vertex {v1[0]:.6f} {v1[1]:.6f} {v1[2]:.6f}\n")
f.write(f" vertex {v2[0]:.6f} {v2[1]:.6f} {v2[2]:.6f}\n")
f.write(f" vertex {v3[0]:.6f} {v3[1]:.6f} {v3[2]:.6f}\n")
f.write(" endloop\n")
f.write(" endfacet\n")
[docs]
class StlBinaryWriter(MeshWriterBase):
"""Writes mesh data to a Binary STL file."""
[docs]
def write(self, triangles, filename, **kwargs):
"""
Writes triangles to a Binary STL file.
Args:
triangles (list): List of (normal, v1, v2, v3) tuples.
filename (str): Output filename.
**kwargs: Expects 'solid_name' (str, optional).
"""
solid_name = kwargs.get("solid_name", "cubeforge_model")
logger.info(f"Writing Binary STL file: {filename} with {len(triangles)} triangles.")
try:
with open(filename, 'wb') as f:
# Write header (80 bytes)
header_name = solid_name[:80].encode('utf-8')
header = header_name + b'\x00' * (80 - len(header_name))
f.write(header)
# Write number of triangles (4-byte unsigned integer, little-endian)
num_triangles = len(triangles)
f.write(struct.pack('<I', num_triangles)) # I = unsigned int
# Write each triangle (50 bytes each)
for normal, v1, v2, v3 in triangles:
self._write_triangle(f, normal, v1, v2, v3)
logger.info(f"Successfully wrote Binary STL file: {filename}")
except IOError as e:
logger.error(f"Failed to write Binary STL file {filename}: {e}")
raise
except struct.error as e:
logger.error(f"Failed to pack data for Binary STL file {filename}: {e}")
raise
def _write_triangle(self, f, normal, v1, v2, v3):
"""Writes a single triangle in Binary STL format."""
# Pack data as little-endian floats (f) and an unsigned short (H)
data = struct.pack('<3f 3f 3f 3f H',
normal[0], normal[1], normal[2],
v1[0], v1[1], v1[2],
v2[0], v2[1], v2[2],
v3[0], v3[1], v3[2],
0) # Attribute byte count = 0
f.write(data)
# --- Factory Function ---
_writer_map = {
'stl': StlBinaryWriter,
'stl_binary': StlBinaryWriter,
'stl_ascii': StlAsciiWriter,
}
[docs]
def get_writer(format_id):
"""
Gets an instance of the appropriate writer class based on the format ID.
Args:
format_id (str): The identifier for the desired format. Case-insensitive.
Returns:
MeshWriterBase: An instance of the corresponding writer class.
Raises:
ValueError: If the format_id is not recognized.
"""
format_id = format_id.lower()
writer_class = _writer_map.get(format_id)
if writer_class:
return writer_class()
else:
supported_formats = ", ".join(sorted(_writer_map.keys()))
raise ValueError(f"Unsupported format: '{format_id}'. Supported formats are: {supported_formats}")