Merge branch 'dev' of https://github.com/gumyr/build123d into dev
Some checks failed
benchmarks / benchmarks (macos-14, 3.12) (push) Has been cancelled
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Has been cancelled
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Has been cancelled
benchmarks / benchmarks (windows-latest, 3.12) (push) Has been cancelled
Upload coverage reports to Codecov / run (push) Has been cancelled
pylint / lint (3.10) (push) Has been cancelled
Run type checker / typecheck (3.10) (push) Has been cancelled
Run type checker / typecheck (3.13) (push) Has been cancelled
Wheel building and publishing / Build wheel on ubuntu-latest (push) Has been cancelled
tests / tests (macos-14, 3.10) (push) Has been cancelled
tests / tests (macos-14, 3.13) (push) Has been cancelled
tests / tests (macos-15-intel, 3.10) (push) Has been cancelled
tests / tests (macos-15-intel, 3.13) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
tests / tests (windows-latest, 3.10) (push) Has been cancelled
tests / tests (windows-latest, 3.13) (push) Has been cancelled
Wheel building and publishing / upload_pypi (push) Has been cancelled

This commit is contained in:
gumyr 2025-11-21 15:09:32 -05:00
commit bdad339e58
6 changed files with 68 additions and 26 deletions

View file

@ -34,6 +34,7 @@ import math
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from copy import copy from copy import copy
from enum import Enum, auto from enum import Enum, auto
from io import BytesIO
from os import PathLike, fsdecode from os import PathLike, fsdecode
from typing import Any, TypeAlias from typing import Any, TypeAlias
from warnings import warn 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 """write
Writes the DXF data to the specified file name. Writes the DXF data to the specified file name.
Args: 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. the DXF data will be written.
""" """
# Reset the main CAD viewport of the model space to the # 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 # https://github.com/gumyr/build123d/issues/382 tracks
# exposing viewport control to the user. # exposing viewport control to the user.
zoom.extents(self._modelspace) 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 """write
Writes the SVG data to the specified file path. Writes the SVG data to the specified file path.
Args: 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 # pylint: disable=too-many-locals
bb = self._bounds bb = self._bounds
@ -1549,5 +1555,9 @@ class ExportSVG(Export2D):
xml = ET.ElementTree(svg) xml = ET.ElementTree(svg)
ET.indent(xml, " ") 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=False)
xml.write(path, encoding="utf-8", xml_declaration=True, default_namespace=None) xml.write(path, encoding="utf-8", xml_declaration=True, default_namespace=None)

View file

@ -262,7 +262,7 @@ def export_gltf(
def export_step( def export_step(
to_export: Shape, to_export: Shape,
file_path: PathLike | str | bytes, file_path: PathLike | str | bytes | BytesIO,
unit: Unit = Unit.MM, unit: Unit = Unit.MM,
write_pcurves: bool = True, write_pcurves: bool = True,
precision_mode: PrecisionMode = PrecisionMode.AVERAGE, precision_mode: PrecisionMode = PrecisionMode.AVERAGE,
@ -277,7 +277,7 @@ def export_step(
Args: Args:
to_export (Shape): object or assembly to_export (Shape): object or assembly
file_path (Union[PathLike, str, bytes]): step file path file_path (Union[PathLike, str, bytes, BytesIO]): step file path
unit (Unit, optional): shape units. Defaults to Unit.MM. unit (Unit, optional): shape units. Defaults to Unit.MM.
write_pcurves (bool, optional): write parametric curves to the STEP file. write_pcurves (bool, optional): write parametric curves to the STEP file.
Defaults to True. Defaults to True.
@ -326,7 +326,13 @@ def export_step(
Interface_Static.SetIVal_s("write.precision.mode", precision_mode.value) Interface_Static.SetIVal_s("write.precision.mode", precision_mode.value)
writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs) writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs)
status = writer.Write(fspath(file_path)) == IFSelect_ReturnStatus.IFSelect_RetDone if not isinstance(file_path, BytesIO):
status = (
writer.Write(fspath(file_path)) == IFSelect_ReturnStatus.IFSelect_RetDone
)
else:
status = writer.WriteStream(file_path) == IFSelect_ReturnStatus.IFSelect_RetDone
if not status: if not status:
raise RuntimeError("Failed to write STEP file") raise RuntimeError("Failed to write STEP file")
@ -346,7 +352,7 @@ def export_stl(
Args: Args:
to_export (Shape): object or assembly to_export (Shape): object or assembly
file_path (str): 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 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 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 large meshes that can consume computing resources. Setting the value too high can
@ -368,7 +374,4 @@ def export_stl(
writer = StlAPI_Writer() writer = StlAPI_Writer()
writer.ASCIIMode = ascii_format writer.ASCIIMode = ascii_format
return writer.Write(to_export.wrapped, fsdecode(file_path))
file_path = str(file_path)
return writer.Write(to_export.wrapped, file_path)

View file

@ -83,6 +83,7 @@ license:
# pylint: disable=no-name-in-module, import-error # pylint: disable=no-name-in-module, import-error
import copy as copy_module import copy as copy_module
import ctypes import ctypes
from io import BytesIO
import math import math
import os import os
import sys import sys
@ -328,8 +329,7 @@ class Mesher:
# Create vertices array in one shot # Create vertices array in one shot
vertices_3mf = [ vertices_3mf = [
Lib3MF.Position((ctypes.c_float * 3)(*v)) Lib3MF.Position((ctypes.c_float * 3)(*v)) for v in vertex_to_idx.keys()
for v in vertex_to_idx.keys()
] ]
# Pre-allocate triangles array and process in bulk # Pre-allocate triangles array and process in bulk
@ -346,7 +346,9 @@ class Mesher:
# Quick degenerate check without set creation # Quick degenerate check without set creation
if mapped_a != mapped_b and mapped_b != mapped_c and mapped_c != mapped_a: 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) return (vertices_3mf, triangles_3mf)
@ -551,3 +553,8 @@ class Mesher:
raise ValueError(f"Unknown file format {output_file_extension}") raise ValueError(f"Unknown file format {output_file_extension}")
writer = self.model.QueryWriter(output_file_extension[1:]) writer = self.model.QueryWriter(output_file_extension[1:])
writer.WriteToFile(file_name) 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)

View file

@ -1,3 +1,4 @@
from io import BytesIO
from os import fsdecode, fsencode from os import fsdecode, fsencode
from typing import Union, Iterable from typing import Union, Iterable
import math import math
@ -194,7 +195,9 @@ class ExportersTestCase(unittest.TestCase):
@pytest.mark.parametrize( @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)) @pytest.mark.parametrize("Exporter", (ExportSVG, ExportDXF))
def test_pathlike_exporters(tmp_path, format, Exporter): def test_pathlike_exporters(tmp_path, format, Exporter):
@ -205,5 +208,14 @@ def test_pathlike_exporters(tmp_path, format, Exporter):
exporter.write(path) 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__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -206,10 +206,11 @@ def test_pathlike_exporters(tmp_path, format, exporter):
exporter(box, path) 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() buffer = io.BytesIO()
box = Box(1, 1, 1).locate(Pos(-1, -2, -3)) box = Box(1, 1, 1).locate(Pos(-1, -2, -3))
export_brep(box, buffer) exporter(box, buffer)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1,4 +1,5 @@
import unittest, uuid import unittest, uuid
from io import BytesIO
from packaging.specifiers import SpecifierSet from packaging.specifiers import SpecifierSet
from pathlib import Path from pathlib import Path
from os import fsdecode, fsencode from os import fsdecode, fsencode
@ -237,5 +238,13 @@ def test_pathlike_mesher(tmp_path, format):
importer.read(path) 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__": if __name__ == "__main__":
unittest.main() unittest.main()