diff --git a/src/build123d/__init__.py b/src/build123d/__init__.py index d89bdc1..925a648 100644 --- a/src/build123d/__init__.py +++ b/src/build123d/__init__.py @@ -34,6 +34,7 @@ __all__ = [ "Keep", "Kind", "LengthMode", + "MeshType", "Mode", "PositionMode", "Select", diff --git a/src/build123d/build_enums.py b/src/build123d/build_enums.py index 9204da1..f043104 100644 --- a/src/build123d/build_enums.py +++ b/src/build123d/build_enums.py @@ -163,6 +163,18 @@ class LengthMode(Enum): return f"<{self.__class__.__name__}.{self.name}>" +class MeshType(Enum): + """3MF mesh types typically for 3D printing""" + + OTHER = auto() + MODEL = auto() + SUPPORT = auto() + SOLIDSUPPORT = auto() + + def __repr__(self): + return f"<{self.__class__.__name__}.{self.name}>" + + class PositionMode(Enum): """Position along curve mode""" diff --git a/src/build123d/mesh_tools.py b/src/build123d/mesher.py similarity index 64% rename from src/build123d/mesh_tools.py rename to src/build123d/mesher.py index 72d419d..63caf38 100644 --- a/src/build123d/mesh_tools.py +++ b/src/build123d/mesher.py @@ -1,90 +1,83 @@ -# pylint: skip-file -"""++ - -Copyright (C) 2019 3MF Consortium (Original Author) - -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this -list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, -this list of conditions and the following disclaimer in the documentation -and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -This file has been generated by the Automatic Component Toolkit (ACT) version 1.6.0-develop. - -Abstract: This is an autogenerated Python application that demonstrates the - usage of the Python bindings of the 3MF Library - -Interface version: 2.2.0 - """ -""" -Creating a 3MF object involves constructing a valid 3D model conforming to -the 3MF specification. The resource hierarchy represents the various -components that make up a 3MF object. The main components required to create -a 3MF object are: +build123d exporter/import for 3MF and STL - Wrapper: The wrapper is the highest-level component representing the -entire 3MF model. It serves as a container for all other resources and -provides access to the complete 3D model. The wrapper is the starting point -for creating and managing the 3MF model. +name: mesher.py +by: Gumyr +date: Aug 9th 2023 - Model: The model is a core component that contains the geometric and -non-geometric resources of the 3D object. It represents the actual 3D -content, including geometry, materials, colors, textures, and other model -information. +desc: + This module provides the Mesher class that implements exporting and importing + both 3MF and STL mesh files. It uses the 3MF Consortium's Lib3MF library + (see https://github.com/3MFConsortium/lib3mf). - Resources: Within the model, various resources are used to define -different aspects of the 3D object. Some essential resources are: + Creating a 3MF object involves constructing a valid 3D model conforming to + the 3MF specification. The resource hierarchy represents the various + components that make up a 3MF object. The main components required to create + a 3MF object are: - a. Mesh: The mesh resource defines the geometry of the 3D object. It -contains a collection of vertices, triangles, and other geometric -information that describes the shape. + Wrapper: The wrapper is the highest-level component representing the + entire 3MF model. It serves as a container for all other resources and + provides access to the complete 3D model. The wrapper is the starting point + for creating and managing the 3MF model. - b. Components: Components allow you to define complex structures by -combining multiple meshes together. They are useful for hierarchical -assemblies and instances. + Model: The model is a core component that contains the geometric and + non-geometric resources of the 3D object. It represents the actual 3D + content, including geometry, materials, colors, textures, and other model + information. - c. Materials: Materials define the appearance properties of the -surfaces, such as color, texture, or surface finish. + Resources: Within the model, various resources are used to define + different aspects of the 3D object. Some essential resources are: - d. Textures: Textures are images applied to the surfaces of the 3D -object to add detail and realism. + a. Mesh: The mesh resource defines the geometry of the 3D object. It + contains a collection of vertices, triangles, and other geometric + information that describes the shape. - e. Colors: Colors represent color information used in the 3D model, -which can be applied to vertices or faces. + b. Components: Components allow you to define complex structures by + combining multiple meshes together. They are useful for hierarchical + assemblies and instances. - Build Items: Build items are the instances of resources used in the 3D -model. They specify the usage of resources within the model. For example, a -build item can refer to a specific mesh, material, and transformation to -represent an instance of an object. + c. Materials: Materials define the appearance properties of the + surfaces, such as color, texture, or surface finish. - Metadata: Metadata provides additional information about the model, such -as author, creation date, and custom properties. + d. Textures: Textures are images applied to the surfaces of the 3D + object to add detail and realism. - Attachments: Attachments can include additional files or data associated -with the 3MF object, such as texture images or other external resources. + e. Colors: Colors represent color information used in the 3D model, + which can be applied to vertices or faces. + + Build Items: Build items are the instances of resources used in the 3D + model. They specify the usage of resources within the model. For example, a + build item can refer to a specific mesh, material, and transformation to + represent an instance of an object. + + Metadata: Metadata provides additional information about the model, such + as author, creation date, and custom properties. + + Attachments: Attachments can include additional files or data associated + with the 3MF object, such as texture images or other external resources. + + When creating a 3MF object, you typically start with the wrapper and then + create or import the necessary resources, such as meshes, materials, and + textures, to define the 3D content. You then organize the model using build + items, specifying how the resources are used in the scene. Additionally, you + can add metadata and attachments as needed to complete the 3MF object. + +license: + + Copyright 2023 Gumyr + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -When creating a 3MF object, you typically start with the wrapper and then -create or import the necessary resources, such as meshes, materials, and -textures, to define the 3D content. You then organize the model using build -items, specifying how the resources are used in the scene. Additionally, you -can add metadata and attachments as needed to complete the 3MF object. """ @@ -96,8 +89,9 @@ import uuid import warnings from typing import Iterable, Union -from build123d import * -from build123d import Shape, downcast +from build123d.build_enums import MeshType, Unit +from build123d.geometry import Color, Vector +from build123d.topology import downcast, Compound, Shape, Shell, Solid from OCP.BRep import BRep_Tool from OCP.BRepBuilderAPI import ( BRepBuilderAPI_MakeFace, @@ -114,7 +108,16 @@ from py_lib3mf import Lib3MF from ocp_vscode import * -class Mesh3MF: +class Mesher: + """Mesher + + Tool for exporting and import meshed objects stored in 3MF or STL files. + + Args: + unit (Unit, optional): model units. Defaults to Unit.MM. + """ + + # Translate b3d Units to Lib3MF ModelUnits map_b3d_to_3mf_unit = { Unit.MC: Lib3MF.ModelUnit.MicroMeter, Unit.MM: Lib3MF.ModelUnit.MilliMeter, @@ -123,37 +126,51 @@ class Mesh3MF: Unit.FT: Lib3MF.ModelUnit.Foot, Unit.M: Lib3MF.ModelUnit.Meter, } + # Translate Lib3MF ModelUnits to b3d Units map_3mf_to_b3d_unit = {v: k for k, v in map_b3d_to_3mf_unit.items()} + # Translate b3d MeshTypes to 3MF ObjectType + map_b3d_mesh_type_3mf = { + MeshType.OTHER: Lib3MF.ObjectType.Other, + MeshType.MODEL: Lib3MF.ObjectType.Model, + MeshType.SUPPORT: Lib3MF.ObjectType.Support, + MeshType.SOLIDSUPPORT: Lib3MF.ObjectType.SolidSupport, + } + # Translate 3MF ObjectType to b3d MeshTypess + map_3mf_to_b3d_mesh_type = {v: k for k, v in map_b3d_mesh_type_3mf.items()} + def __init__(self, unit: Unit = Unit.MM): self.unit = unit - self.tessellations = None libpath = os.path.dirname(Lib3MF.__file__) self.wrapper = Lib3MF.Wrapper(os.path.join(libpath, "lib3mf")) self.model = self.wrapper.CreateModel() - self.model.SetUnit(Mesh3MF.map_b3d_to_3mf_unit[unit]) + self.model.SetUnit(Mesher.map_b3d_to_3mf_unit[unit]) self.meshes: list[Lib3MF.MeshObject] = [] - # self.mesh.MultiPropertyLayer @property def model_unit(self) -> Unit: + """Unit used in the model""" return self.unit @property def triangle_counts(self) -> list[int]: + """Number of triangles in each of the model's meshes""" return [m.GetTriangleCount() for m in self.meshes] @property def vertex_counts(self) -> list[int]: + """Number of vertices in each of the models's meshes""" return [m.GetVertexCount() for m in self.meshes] @property def mesh_count(self) -> int: + """Number of meshes in the model""" mesh_iterator: Lib3MF.MeshObjectIterator = self.model.GetMeshObjects() return mesh_iterator.Count() @property def library_version(self) -> str: + """3MF Consortium Lib#MF version""" major, minor, micro = self.wrapper.GetLibraryVersion() return f"{major}.{minor}.{micro}" @@ -165,6 +182,17 @@ class Mesh3MF: metadata_type: str, must_preserve: bool, ): + """add_meta_data + + Add meta data to the models + + Args: + name_space (str): categorizer of different metadata entries + name (str): metadata label + value (str): metadata content + metadata_type (str): metadata trype + must_preserve (bool): metadata must not be removed if unused + """ # Get an existing meta data group if there is one mdg = self.model.GetMetaDataGroup() if mdg is None: @@ -176,6 +204,9 @@ class Mesh3MF: mdg.AddMetaData(name_space, name, value, metadata_type, must_preserve) def add_code_to_metadata(self): + """Add the code calling this method to the 3MF metadata with the custom + name space `build123d`, name equal to the base file name and the type + as `python`""" caller_file = sys._getframe().f_back.f_code.co_filename code_file = open(caller_file, "r") # open code file in read mode source_code = code_file.read() # read whole file to a string @@ -190,6 +221,7 @@ class Mesh3MF: ) def get_meta_data(self) -> list[str]: + """Retrieve all of the metadata""" meta_data_group = self.model.GetMetaDataGroup() meta_data_contents = [] for i in range(meta_data_group.GetMetaDataCount()): @@ -201,6 +233,7 @@ class Mesh3MF: return meta_data_contents def get_meta_data_by_key(self, name_space: str, name: str) -> list[str]: + """Retrive the metadata value and type for the provided name space and name""" meta_data_group = self.model.GetMetaDataGroup() meta_data_contents = [] meta_data = meta_data_group.GetMetaDataByKey(name_space, name) @@ -208,25 +241,45 @@ class Mesh3MF: meta_data_contents.append(f"Value: {meta_data.GetValue()}") return meta_data_contents - def get_mesh_properties(self, mesh: Lib3MF.MeshObject) -> str: - newline = "\n" - properties = f"Name: {mesh.GetName()}{newline}" - properties += f"Part Number: {mesh.GetPartNumber()}{newline}" - properties += f"Type: {Lib3MF.ObjectType(mesh.GetType()).name}{newline}" - uuid_valid, uuid_value = mesh.GetUUID() - if uuid_valid: - properties += f"UUID: {uuid_value}s{newline}" + def get_mesh_properties(self) -> list[str]: + """Retrieve the properties from all the meshes""" + properties = [] + for mesh in self.meshes: + properties += f"Name: {mesh.GetName()}" + properties += f"Part Number: {mesh.GetPartNumber()}" + properties += f"Type: {Mesher.map_3mf_to_b3d_mesh_type[Lib3MF.ObjectType(mesh.GetType())].name}" + uuid_valid, uuid_value = mesh.GetUUID() + if uuid_valid: + properties += f"UUID: {uuid_value}" def add_shape( self, shape: Union[Shape, Iterable[Shape]], linear_deflection: float = 0.5, angular_deflection: float = 0.5, - object_type: Lib3MF.ObjectType = Lib3MF.ObjectType.Model, + mesh_type: MeshType = MeshType.MODEL, part_number: str = None, uuid: uuid = None, ): - def is_facet_forward( + """add_shape + + Add a shape to the 3MF/STL file. + + Args: + shape (Union[Shape, Iterable[Shape]]): build123d object + linear_deflection (float, optional): mesh control for edges. Defaults to 0.5. + angular_deflection (float, optional): mesh control for non-planar surfaces. Defaults to 0.5. + mesh_type (MeshType, optional): 3D printing use of mesh. Defaults to MeshType.MODEL. + part_number (str, optional): part #. Defaults to None. + uuid (uuid, optional): uuid from uuid package. Defaults to None. + + Rasises: + RuntimeError: 3mf mesh is invalid + Warning: Degenerate shape skipped + Warning: 3mf mesh is not manifold + """ + + def _is_facet_forward( points: tuple[gp_Pnt, gp_Pnt, gp_Pnt], shape_center: Vector ) -> bool: # Create the facet @@ -299,7 +352,7 @@ class Mesh3MF: mesh_3mf: Lib3MF.MeshObject = self.model.AddMeshObject() # Add the meta data - mesh_3mf.SetType(object_type) + mesh_3mf.SetType(Mesher.map_b3d_mesh_type_3mf[mesh_type]) if shape.label: mesh_3mf.SetName(shape.label) if part_number: @@ -329,7 +382,7 @@ class Mesh3MF: ) order = ( [0, 2, 1] - if not is_facet_forward(triangle_points, shape_center) + if not _is_facet_forward(triangle_points, shape_center) else [0, 1, 2] ) # order = [2, 1, 0] # Creates an invalid mesh @@ -377,7 +430,8 @@ class Mesh3MF: components = self.model.AddComponentsObject() components.AddComponent(mesh_3mf, self.wrapper.GetIdentityTransform()) - def get_shape(self, mesh_3mf: Lib3MF.MeshObject) -> Union[Shell, Solid]: + def _get_shape(self, mesh_3mf: Lib3MF.MeshObject) -> Shape: + """Build build123d object from lib3mf mesh""" # Extract all the vertices gp_pnts = [gp_Pnt(*p.Coordinates[0:3]) for p in mesh_3mf.GetVertices()] @@ -406,13 +460,24 @@ class Mesh3MF: return shape_obj - def read(self, file_name: str) -> list[Union[Shell, Solid]]: + def read(self, file_name: str) -> list[Shape]: + """read + + Args: + file_name (str): file path + + Raises: + ValueError: Unknown file format - must be 3mf or stl + + Returns: + list[Shape]: build123d shapes extracted from mesh file + """ input_file_format = file_name.split(".")[-1].lower() if input_file_format not in ["3mf", "stl"]: raise ValueError(f"Unknown file format {input_file_format}") reader = self.model.QueryReader(input_file_format) reader.ReadFromFile(file_name) - self.unit = Mesh3MF.map_3mf_to_b3d_unit[self.model.GetUnit()] + self.unit = Mesher.map_3mf_to_b3d_unit[self.model.GetUnit()] # Extract 3MF meshes and translate to OCP meshes mesh_iterator: Lib3MF.MeshObjectIterator = self.model.GetMeshObjects() @@ -422,7 +487,7 @@ class Mesh3MF: self.meshes.append(mesh_iterator.GetCurrentMeshObject()) shapes = [] for mesh in self.meshes: - shape = self.get_shape(mesh) + shape = self._get_shape(mesh) shape.label = mesh.GetName() triangle_properties = mesh.GetAllTriangleProperties() color_indices = [] @@ -445,14 +510,15 @@ class Mesh3MF: return shapes - def _get_meshes(self): - mesh_iterator: Lib3MF.MeshObjectIterator = self.model.GetMeshObjects() - self.meshes: list[Lib3MF.MeshObject] - for _i in range(mesh_iterator.Count()): - mesh_iterator.MoveNext() - self.meshes.append(mesh_iterator.GetCurrentMeshObject()) - def write(self, file_name: str): + """_summary_ + + Args: + file_name (str): file path + + Raises: + ValueError: Unknown file format - must be 3mf or stl + """ output_file_format = file_name.split(".")[-1].lower() if output_file_format not in ["3mf", "stl"]: raise ValueError(f"Unknown file format {output_file_format}")