mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-06 02:30:55 -08:00
Compare commits
4 commits
26caed754c
...
7a4f1f7e55
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a4f1f7e55 | ||
|
|
70764bbe08 | ||
|
|
7f4e92f0bf | ||
|
|
d329cf1094 |
6 changed files with 68 additions and 26 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -312,12 +313,12 @@ class Mesher:
|
||||||
# Round off the vertices to avoid vertices within tolerance being
|
# Round off the vertices to avoid vertices within tolerance being
|
||||||
# considered as different vertices
|
# considered as different vertices
|
||||||
digits = -int(round(math.log(TOLERANCE, 10), 1))
|
digits = -int(round(math.log(TOLERANCE, 10), 1))
|
||||||
|
|
||||||
# Create vertex to index mapping directly
|
# Create vertex to index mapping directly
|
||||||
vertex_to_idx = {}
|
vertex_to_idx = {}
|
||||||
next_idx = 0
|
next_idx = 0
|
||||||
vert_table = {}
|
vert_table = {}
|
||||||
|
|
||||||
# First pass - create mapping
|
# First pass - create mapping
|
||||||
for i, (x, y, z) in enumerate(ocp_mesh_vertices):
|
for i, (x, y, z) in enumerate(ocp_mesh_vertices):
|
||||||
key = (round(x, digits), round(y, digits), round(z, digits))
|
key = (round(x, digits), round(y, digits), round(z, digits))
|
||||||
|
|
@ -325,17 +326,16 @@ class Mesher:
|
||||||
vertex_to_idx[key] = next_idx
|
vertex_to_idx[key] = next_idx
|
||||||
next_idx += 1
|
next_idx += 1
|
||||||
vert_table[i] = vertex_to_idx[key]
|
vert_table[i] = vertex_to_idx[key]
|
||||||
|
|
||||||
# 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
|
||||||
c_uint3 = ctypes.c_uint * 3
|
c_uint3 = ctypes.c_uint * 3
|
||||||
triangles_3mf = []
|
triangles_3mf = []
|
||||||
|
|
||||||
# Process triangles in bulk
|
# Process triangles in bulk
|
||||||
for tri in triangles:
|
for tri in triangles:
|
||||||
# Map indices directly without list comprehension
|
# Map indices directly without list comprehension
|
||||||
|
|
@ -343,11 +343,13 @@ class Mesher:
|
||||||
mapped_a = vert_table[a]
|
mapped_a = vert_table[a]
|
||||||
mapped_b = vert_table[b]
|
mapped_b = vert_table[b]
|
||||||
mapped_c = vert_table[c]
|
mapped_c = vert_table[c]
|
||||||
|
|
||||||
# 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)
|
||||||
|
|
||||||
def _add_color(self, b3d_shape: Shape, mesh_3mf: Lib3MF.MeshObject):
|
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}")
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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__":
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue