mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-06 02:30:55 -08:00
Merge pull request #1141 from jdegenstein/bytesio
Some checks are pending
benchmarks / benchmarks (macos-14, 3.12) (push) Waiting to run
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Waiting to run
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Waiting to run
benchmarks / benchmarks (windows-latest, 3.12) (push) Waiting to run
Upload coverage reports to Codecov / run (push) Waiting to run
pylint / lint (3.10) (push) Waiting to run
Run type checker / typecheck (3.10) (push) Waiting to run
Run type checker / typecheck (3.13) (push) Waiting to run
Wheel building and publishing / Build wheel on ubuntu-latest (push) Waiting to run
Wheel building and publishing / upload_pypi (push) Blocked by required conditions
tests / tests (macos-14, 3.10) (push) Waiting to run
tests / tests (macos-14, 3.13) (push) Waiting to run
tests / tests (macos-15-intel, 3.10) (push) Waiting to run
tests / tests (macos-15-intel, 3.13) (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Waiting to run
tests / tests (ubuntu-latest, 3.13) (push) Waiting to run
tests / tests (windows-latest, 3.10) (push) Waiting to run
tests / tests (windows-latest, 3.13) (push) Waiting to run
Some checks are pending
benchmarks / benchmarks (macos-14, 3.12) (push) Waiting to run
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Waiting to run
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Waiting to run
benchmarks / benchmarks (windows-latest, 3.12) (push) Waiting to run
Upload coverage reports to Codecov / run (push) Waiting to run
pylint / lint (3.10) (push) Waiting to run
Run type checker / typecheck (3.10) (push) Waiting to run
Run type checker / typecheck (3.13) (push) Waiting to run
Wheel building and publishing / Build wheel on ubuntu-latest (push) Waiting to run
Wheel building and publishing / upload_pypi (push) Blocked by required conditions
tests / tests (macos-14, 3.10) (push) Waiting to run
tests / tests (macos-14, 3.13) (push) Waiting to run
tests / tests (macos-15-intel, 3.10) (push) Waiting to run
tests / tests (macos-15-intel, 3.13) (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Waiting to run
tests / tests (ubuntu-latest, 3.13) (push) Waiting to run
tests / tests (windows-latest, 3.10) (push) Waiting to run
tests / tests (windows-latest, 3.13) (push) Waiting to run
Add BytesIO export support to SVG, DXF, STEP, and STL/3MF via a lib3mf/Mesher workaround
This commit is contained in:
commit
7a4f1f7e55
6 changed files with 68 additions and 26 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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__":
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue