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
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)

View file

@ -262,7 +262,7 @@ def export_gltf(
def export_step(
to_export: Shape,
file_path: PathLike | str | bytes,
file_path: PathLike | str | bytes | BytesIO,
unit: Unit = Unit.MM,
write_pcurves: bool = True,
precision_mode: PrecisionMode = PrecisionMode.AVERAGE,
@ -277,7 +277,7 @@ def export_step(
Args:
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.
write_pcurves (bool, optional): write parametric curves to the STEP file.
Defaults to True.
@ -326,7 +326,13 @@ def export_step(
Interface_Static.SetIVal_s("write.precision.mode", precision_mode.value)
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:
raise RuntimeError("Failed to write STEP file")
@ -346,7 +352,7 @@ def export_stl(
Args:
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
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
@ -368,7 +374,4 @@ def export_stl(
writer = StlAPI_Writer()
writer.ASCIIMode = ascii_format
file_path = str(file_path)
return writer.Write(to_export.wrapped, file_path)
return writer.Write(to_export.wrapped, fsdecode(file_path))

View file

@ -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):
@ -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)

View file

@ -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()

View file

@ -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__":

View file

@ -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()