diff --git a/src/build123d/exporters.py b/src/build123d/exporters.py index a229fa2..687e2f1 100644 --- a/src/build123d/exporters.py +++ b/src/build123d/exporters.py @@ -34,6 +34,7 @@ import math import xml.etree.ElementTree as ET from copy import copy from enum import Enum, auto +from io import BytesIO from os import PathLike, fsdecode from typing import Any, TypeAlias from warnings import warn @@ -636,13 +637,13 @@ class ExportDXF(Export2D): # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - def write(self, file_name: PathLike | str | bytes): + def write(self, file_name: PathLike | str | bytes | BytesIO): """write Writes the DXF data to the specified file name. Args: - file_name (PathLike | str | bytes): The file name (including path) where + file_name (PathLike | str | bytes | BytesIO): The file name (including path) where the DXF data will be written. """ # Reset the main CAD viewport of the model space to the @@ -650,7 +651,12 @@ class ExportDXF(Export2D): # https://github.com/gumyr/build123d/issues/382 tracks # exposing viewport control to the user. zoom.extents(self._modelspace) - self._document.saveas(fsdecode(file_name)) + + if not isinstance(file_name, BytesIO): + file_name = fsdecode(file_name) + self._document.saveas(file_name) + else: + self._document.write(file_name, fmt="bin") # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1497,13 +1503,13 @@ class ExportSVG(Export2D): # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - def write(self, path: PathLike | str | bytes): + def write(self, path: PathLike | str | bytes | BytesIO): """write Writes the SVG data to the specified file path. Args: - path (PathLike | str | bytes): The file path where the SVG data will be written. + path (PathLike | str | bytes | BytesIO): The file path where the SVG data will be written. """ # pylint: disable=too-many-locals bb = self._bounds @@ -1549,5 +1555,9 @@ class ExportSVG(Export2D): xml = ET.ElementTree(svg) ET.indent(xml, " ") + + if not isinstance(path, BytesIO): + path = fsdecode(path) + # xml.write(path, encoding="utf-8", xml_declaration=True, default_namespace=False) xml.write(path, encoding="utf-8", xml_declaration=True, default_namespace=None) diff --git a/src/build123d/exporters3d.py b/src/build123d/exporters3d.py index 505d4aa..759464a 100644 --- a/src/build123d/exporters3d.py +++ b/src/build123d/exporters3d.py @@ -182,7 +182,7 @@ def export_brep( def export_gltf( to_export: Shape, - file_path: PathLike | str | bytes | BytesIO, + file_path: PathLike | str | bytes, unit: Unit = Unit.MM, binary: bool = False, linear_deflection: float = 0.001, @@ -198,7 +198,7 @@ def export_gltf( Args: to_export (Shape): object or assembly - file_path (Union[PathLike, str, bytes, BytesIO]): glTF file path + file_path (Union[PathLike, str, bytes]): glTF file path unit (Unit, optional): shape units. Defaults to Unit.MM. binary (bool, optional): output format. Defaults to False. linear_deflection (float, optional): A linear deflection setting which limits @@ -234,12 +234,9 @@ def export_gltf( # Create the XCAF document doc: TDocStd_Document = _create_xde(to_export, unit) - if not isinstance(file_path, BytesIO): - file_path = fsdecode(file_path) - # Write the glTF file writer = RWGltf_CafWriter( - theFile=TCollection_AsciiString(file_path, theIsBinary=binary + theFile=TCollection_AsciiString(fsdecode(file_path)), theIsBinary=binary ) writer.SetParallel(True) index_map = TColStd_IndexedDataMapOfStringString() @@ -330,9 +327,12 @@ def export_step( writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs) if not isinstance(file_path, BytesIO): - file_path = fspath(file_path) + status = ( + writer.Write(fspath(file_path)) == IFSelect_ReturnStatus.IFSelect_RetDone + ) + else: + status = writer.WriteStream(file_path) == IFSelect_ReturnStatus.IFSelect_RetDone - status = writer.Write(file_path) == IFSelect_ReturnStatus.IFSelect_RetDone if not status: raise RuntimeError("Failed to write STEP file") @@ -341,7 +341,7 @@ def export_step( def export_stl( to_export: Shape, - file_path: PathLike | str | bytes | BytesIO, + file_path: PathLike | str | bytes, tolerance: float = 1e-3, angular_tolerance: float = 0.1, ascii_format: bool = False, @@ -352,7 +352,7 @@ def export_stl( Args: to_export (Shape): object or assembly - file_path (Union[PathLike, str, bytes, BytesIO]): The path and file name to write the STL output to. + file_path (Union[PathLike, str, bytes]): The path and file name to write the STL output to. tolerance (float, optional): A linear deflection setting which limits the distance between a curve and its tessellation. Setting this value too low will result in large meshes that can consume computing resources. Setting the value too high can @@ -374,8 +374,4 @@ def export_stl( writer = StlAPI_Writer() writer.ASCIIMode = ascii_format - - if not isinstance(file_path, BytesIO): - file_path = fsdecode(file_path) - - return writer.Write(to_export.wrapped, file_path) + return writer.Write(to_export.wrapped, fsdecode(file_path)) diff --git a/src/build123d/mesher.py b/src/build123d/mesher.py index 5433848..8c4ce42 100644 --- a/src/build123d/mesher.py +++ b/src/build123d/mesher.py @@ -83,6 +83,7 @@ license: # pylint: disable=no-name-in-module, import-error import copy as copy_module import ctypes +from io import BytesIO import math import os import sys @@ -312,12 +313,12 @@ class Mesher: # Round off the vertices to avoid vertices within tolerance being # considered as different vertices digits = -int(round(math.log(TOLERANCE, 10), 1)) - + # Create vertex to index mapping directly vertex_to_idx = {} next_idx = 0 vert_table = {} - + # First pass - create mapping for i, (x, y, z) in enumerate(ocp_mesh_vertices): key = (round(x, digits), round(y, digits), round(z, digits)) @@ -325,17 +326,16 @@ class Mesher: vertex_to_idx[key] = next_idx next_idx += 1 vert_table[i] = vertex_to_idx[key] - + # Create vertices array in one shot vertices_3mf = [ - Lib3MF.Position((ctypes.c_float * 3)(*v)) - for v in vertex_to_idx.keys() + Lib3MF.Position((ctypes.c_float * 3)(*v)) for v in vertex_to_idx.keys() ] - + # Pre-allocate triangles array and process in bulk c_uint3 = ctypes.c_uint * 3 triangles_3mf = [] - + # Process triangles in bulk for tri in triangles: # Map indices directly without list comprehension @@ -343,11 +343,13 @@ class Mesher: mapped_a = vert_table[a] mapped_b = vert_table[b] mapped_c = vert_table[c] - + # Quick degenerate check without set creation if mapped_a != mapped_b and mapped_b != mapped_c and mapped_c != mapped_a: - triangles_3mf.append(Lib3MF.Triangle(c_uint3(mapped_a, mapped_b, mapped_c))) - + triangles_3mf.append( + Lib3MF.Triangle(c_uint3(mapped_a, mapped_b, mapped_c)) + ) + return (vertices_3mf, triangles_3mf) def _add_color(self, b3d_shape: Shape, mesh_3mf: Lib3MF.MeshObject): @@ -540,7 +542,7 @@ class Mesher: """write Args: - file_name Union[Pathlike, str, bytes]: file path + file_name Union[Pathlike, str, bytes, BytesIO]: file path Raises: ValueError: Unknown file format - must be 3mf or stl @@ -551,3 +553,8 @@ class Mesher: raise ValueError(f"Unknown file format {output_file_extension}") writer = self.model.QueryWriter(output_file_extension[1:]) writer.WriteToFile(file_name) + + def write_stream(self, stream: BytesIO, file_type: str): + writer = self.model.QueryWriter(file_type) + result = bytes(writer.WriteToBuffer()) + stream.write(result) diff --git a/tests/test_exporters.py b/tests/test_exporters.py index f95a92e..11c63da 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -1,3 +1,4 @@ +from io import BytesIO from os import fsdecode, fsencode from typing import Union, Iterable import math @@ -194,7 +195,9 @@ class ExportersTestCase(unittest.TestCase): @pytest.mark.parametrize( - "format", (Path, fsencode, fsdecode), ids=["path", "bytes", "str"] + "format", + (Path, fsencode, fsdecode), + ids=["path", "bytes", "str"], ) @pytest.mark.parametrize("Exporter", (ExportSVG, ExportDXF)) def test_pathlike_exporters(tmp_path, format, Exporter): @@ -205,5 +208,14 @@ def test_pathlike_exporters(tmp_path, format, Exporter): exporter.write(path) +@pytest.mark.parametrize("Exporter", (ExportSVG, ExportDXF)) +def test_exporters_in_memory(Exporter): + buffer = BytesIO() + sketch = ExportersTestCase.create_test_sketch() + exporter = Exporter() + exporter.add_shape(sketch) + exporter.write(buffer) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_exporters3d.py b/tests/test_exporters3d.py index 644ac3e..bf1c1bd 100644 --- a/tests/test_exporters3d.py +++ b/tests/test_exporters3d.py @@ -206,10 +206,11 @@ def test_pathlike_exporters(tmp_path, format, exporter): exporter(box, path) -def test_export_brep_in_memory(): +@pytest.mark.parametrize("exporter", (export_step, export_brep)) +def test_exporters_in_memory(exporter): buffer = io.BytesIO() box = Box(1, 1, 1).locate(Pos(-1, -2, -3)) - export_brep(box, buffer) + exporter(box, buffer) if __name__ == "__main__": diff --git a/tests/test_mesher.py b/tests/test_mesher.py index 0be4ccb..59b7214 100644 --- a/tests/test_mesher.py +++ b/tests/test_mesher.py @@ -1,4 +1,5 @@ import unittest, uuid +from io import BytesIO from packaging.specifiers import SpecifierSet from pathlib import Path from os import fsdecode, fsencode @@ -237,5 +238,13 @@ def test_pathlike_mesher(tmp_path, format): importer.read(path) +@pytest.mark.parametrize("file_type", ("3mf", "stl")) +def test_in_memory_mesher(file_type): + stream = BytesIO() + exporter = Mesher() + exporter.add_shape(Solid.make_box(1, 1, 1)) + exporter.write_stream(stream, file_type) + + if __name__ == "__main__": unittest.main()