Added docs for 3mf/stl mesher

This commit is contained in:
gumyr 2023-08-09 13:28:31 -04:00
parent dcf71b6fef
commit a058b45ca2
3 changed files with 182 additions and 103 deletions

View file

@ -34,6 +34,7 @@ __all__ = [
"Keep", "Keep",
"Kind", "Kind",
"LengthMode", "LengthMode",
"MeshType",
"Mode", "Mode",
"PositionMode", "PositionMode",
"Select", "Select",

View file

@ -163,6 +163,18 @@ class LengthMode(Enum):
return f"<{self.__class__.__name__}.{self.name}>" 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): class PositionMode(Enum):
"""Position along curve mode""" """Position along curve mode"""

View file

@ -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
""" """
""" build123d exporter/import for 3MF and STL
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:
Wrapper: The wrapper is the highest-level component representing the name: mesher.py
entire 3MF model. It serves as a container for all other resources and by: Gumyr
provides access to the complete 3D model. The wrapper is the starting point date: Aug 9th 2023
for creating and managing the 3MF model.
Model: The model is a core component that contains the geometric and desc:
non-geometric resources of the 3D object. It represents the actual 3D This module provides the Mesher class that implements exporting and importing
content, including geometry, materials, colors, textures, and other model both 3MF and STL mesh files. It uses the 3MF Consortium's Lib3MF library
information. (see https://github.com/3MFConsortium/lib3mf).
Resources: Within the model, various resources are used to define Creating a 3MF object involves constructing a valid 3D model conforming to
different aspects of the 3D object. Some essential resources are: 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 Wrapper: The wrapper is the highest-level component representing the
contains a collection of vertices, triangles, and other geometric entire 3MF model. It serves as a container for all other resources and
information that describes the shape. 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 Model: The model is a core component that contains the geometric and
combining multiple meshes together. They are useful for hierarchical non-geometric resources of the 3D object. It represents the actual 3D
assemblies and instances. content, including geometry, materials, colors, textures, and other model
information.
c. Materials: Materials define the appearance properties of the Resources: Within the model, various resources are used to define
surfaces, such as color, texture, or surface finish. different aspects of the 3D object. Some essential resources are:
d. Textures: Textures are images applied to the surfaces of the 3D a. Mesh: The mesh resource defines the geometry of the 3D object. It
object to add detail and realism. 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, b. Components: Components allow you to define complex structures by
which can be applied to vertices or faces. 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 c. Materials: Materials define the appearance properties of the
model. They specify the usage of resources within the model. For example, a surfaces, such as color, texture, or surface finish.
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 d. Textures: Textures are images applied to the surfaces of the 3D
as author, creation date, and custom properties. object to add detail and realism.
Attachments: Attachments can include additional files or data associated e. Colors: Colors represent color information used in the 3D model,
with the 3MF object, such as texture images or other external resources. 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 import warnings
from typing import Iterable, Union from typing import Iterable, Union
from build123d import * from build123d.build_enums import MeshType, Unit
from build123d import Shape, downcast from build123d.geometry import Color, Vector
from build123d.topology import downcast, Compound, Shape, Shell, Solid
from OCP.BRep import BRep_Tool from OCP.BRep import BRep_Tool
from OCP.BRepBuilderAPI import ( from OCP.BRepBuilderAPI import (
BRepBuilderAPI_MakeFace, BRepBuilderAPI_MakeFace,
@ -114,7 +108,16 @@ from py_lib3mf import Lib3MF
from ocp_vscode import * 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 = { map_b3d_to_3mf_unit = {
Unit.MC: Lib3MF.ModelUnit.MicroMeter, Unit.MC: Lib3MF.ModelUnit.MicroMeter,
Unit.MM: Lib3MF.ModelUnit.MilliMeter, Unit.MM: Lib3MF.ModelUnit.MilliMeter,
@ -123,37 +126,51 @@ class Mesh3MF:
Unit.FT: Lib3MF.ModelUnit.Foot, Unit.FT: Lib3MF.ModelUnit.Foot,
Unit.M: Lib3MF.ModelUnit.Meter, 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()} 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): def __init__(self, unit: Unit = Unit.MM):
self.unit = unit self.unit = unit
self.tessellations = None
libpath = os.path.dirname(Lib3MF.__file__) libpath = os.path.dirname(Lib3MF.__file__)
self.wrapper = Lib3MF.Wrapper(os.path.join(libpath, "lib3mf")) self.wrapper = Lib3MF.Wrapper(os.path.join(libpath, "lib3mf"))
self.model = self.wrapper.CreateModel() 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.meshes: list[Lib3MF.MeshObject] = []
# self.mesh.MultiPropertyLayer
@property @property
def model_unit(self) -> Unit: def model_unit(self) -> Unit:
"""Unit used in the model"""
return self.unit return self.unit
@property @property
def triangle_counts(self) -> list[int]: def triangle_counts(self) -> list[int]:
"""Number of triangles in each of the model's meshes"""
return [m.GetTriangleCount() for m in self.meshes] return [m.GetTriangleCount() for m in self.meshes]
@property @property
def vertex_counts(self) -> list[int]: def vertex_counts(self) -> list[int]:
"""Number of vertices in each of the models's meshes"""
return [m.GetVertexCount() for m in self.meshes] return [m.GetVertexCount() for m in self.meshes]
@property @property
def mesh_count(self) -> int: def mesh_count(self) -> int:
"""Number of meshes in the model"""
mesh_iterator: Lib3MF.MeshObjectIterator = self.model.GetMeshObjects() mesh_iterator: Lib3MF.MeshObjectIterator = self.model.GetMeshObjects()
return mesh_iterator.Count() return mesh_iterator.Count()
@property @property
def library_version(self) -> str: def library_version(self) -> str:
"""3MF Consortium Lib#MF version"""
major, minor, micro = self.wrapper.GetLibraryVersion() major, minor, micro = self.wrapper.GetLibraryVersion()
return f"{major}.{minor}.{micro}" return f"{major}.{minor}.{micro}"
@ -165,6 +182,17 @@ class Mesh3MF:
metadata_type: str, metadata_type: str,
must_preserve: bool, 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 # Get an existing meta data group if there is one
mdg = self.model.GetMetaDataGroup() mdg = self.model.GetMetaDataGroup()
if mdg is None: if mdg is None:
@ -176,6 +204,9 @@ class Mesh3MF:
mdg.AddMetaData(name_space, name, value, metadata_type, must_preserve) mdg.AddMetaData(name_space, name, value, metadata_type, must_preserve)
def add_code_to_metadata(self): 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 caller_file = sys._getframe().f_back.f_code.co_filename
code_file = open(caller_file, "r") # open code file in read mode code_file = open(caller_file, "r") # open code file in read mode
source_code = code_file.read() # read whole file to a string source_code = code_file.read() # read whole file to a string
@ -190,6 +221,7 @@ class Mesh3MF:
) )
def get_meta_data(self) -> list[str]: def get_meta_data(self) -> list[str]:
"""Retrieve all of the metadata"""
meta_data_group = self.model.GetMetaDataGroup() meta_data_group = self.model.GetMetaDataGroup()
meta_data_contents = [] meta_data_contents = []
for i in range(meta_data_group.GetMetaDataCount()): for i in range(meta_data_group.GetMetaDataCount()):
@ -201,6 +233,7 @@ class Mesh3MF:
return meta_data_contents return meta_data_contents
def get_meta_data_by_key(self, name_space: str, name: str) -> list[str]: 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_group = self.model.GetMetaDataGroup()
meta_data_contents = [] meta_data_contents = []
meta_data = meta_data_group.GetMetaDataByKey(name_space, name) meta_data = meta_data_group.GetMetaDataByKey(name_space, name)
@ -208,25 +241,45 @@ class Mesh3MF:
meta_data_contents.append(f"Value: {meta_data.GetValue()}") meta_data_contents.append(f"Value: {meta_data.GetValue()}")
return meta_data_contents return meta_data_contents
def get_mesh_properties(self, mesh: Lib3MF.MeshObject) -> str: def get_mesh_properties(self) -> list[str]:
newline = "\n" """Retrieve the properties from all the meshes"""
properties = f"Name: {mesh.GetName()}{newline}" properties = []
properties += f"Part Number: {mesh.GetPartNumber()}{newline}" for mesh in self.meshes:
properties += f"Type: {Lib3MF.ObjectType(mesh.GetType()).name}{newline}" properties += f"Name: {mesh.GetName()}"
uuid_valid, uuid_value = mesh.GetUUID() properties += f"Part Number: {mesh.GetPartNumber()}"
if uuid_valid: properties += f"Type: {Mesher.map_3mf_to_b3d_mesh_type[Lib3MF.ObjectType(mesh.GetType())].name}"
properties += f"UUID: {uuid_value}s{newline}" uuid_valid, uuid_value = mesh.GetUUID()
if uuid_valid:
properties += f"UUID: {uuid_value}"
def add_shape( def add_shape(
self, self,
shape: Union[Shape, Iterable[Shape]], shape: Union[Shape, Iterable[Shape]],
linear_deflection: float = 0.5, linear_deflection: float = 0.5,
angular_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, part_number: str = None,
uuid: uuid = 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 points: tuple[gp_Pnt, gp_Pnt, gp_Pnt], shape_center: Vector
) -> bool: ) -> bool:
# Create the facet # Create the facet
@ -299,7 +352,7 @@ class Mesh3MF:
mesh_3mf: Lib3MF.MeshObject = self.model.AddMeshObject() mesh_3mf: Lib3MF.MeshObject = self.model.AddMeshObject()
# Add the meta data # Add the meta data
mesh_3mf.SetType(object_type) mesh_3mf.SetType(Mesher.map_b3d_mesh_type_3mf[mesh_type])
if shape.label: if shape.label:
mesh_3mf.SetName(shape.label) mesh_3mf.SetName(shape.label)
if part_number: if part_number:
@ -329,7 +382,7 @@ class Mesh3MF:
) )
order = ( order = (
[0, 2, 1] [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] else [0, 1, 2]
) )
# order = [2, 1, 0] # Creates an invalid mesh # order = [2, 1, 0] # Creates an invalid mesh
@ -377,7 +430,8 @@ class Mesh3MF:
components = self.model.AddComponentsObject() components = self.model.AddComponentsObject()
components.AddComponent(mesh_3mf, self.wrapper.GetIdentityTransform()) 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 # Extract all the vertices
gp_pnts = [gp_Pnt(*p.Coordinates[0:3]) for p in mesh_3mf.GetVertices()] gp_pnts = [gp_Pnt(*p.Coordinates[0:3]) for p in mesh_3mf.GetVertices()]
@ -406,13 +460,24 @@ class Mesh3MF:
return shape_obj 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() input_file_format = file_name.split(".")[-1].lower()
if input_file_format not in ["3mf", "stl"]: if input_file_format not in ["3mf", "stl"]:
raise ValueError(f"Unknown file format {input_file_format}") raise ValueError(f"Unknown file format {input_file_format}")
reader = self.model.QueryReader(input_file_format) reader = self.model.QueryReader(input_file_format)
reader.ReadFromFile(file_name) 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 # Extract 3MF meshes and translate to OCP meshes
mesh_iterator: Lib3MF.MeshObjectIterator = self.model.GetMeshObjects() mesh_iterator: Lib3MF.MeshObjectIterator = self.model.GetMeshObjects()
@ -422,7 +487,7 @@ class Mesh3MF:
self.meshes.append(mesh_iterator.GetCurrentMeshObject()) self.meshes.append(mesh_iterator.GetCurrentMeshObject())
shapes = [] shapes = []
for mesh in self.meshes: for mesh in self.meshes:
shape = self.get_shape(mesh) shape = self._get_shape(mesh)
shape.label = mesh.GetName() shape.label = mesh.GetName()
triangle_properties = mesh.GetAllTriangleProperties() triangle_properties = mesh.GetAllTriangleProperties()
color_indices = [] color_indices = []
@ -445,14 +510,15 @@ class Mesh3MF:
return shapes 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): 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() output_file_format = file_name.split(".")[-1].lower()
if output_file_format not in ["3mf", "stl"]: if output_file_format not in ["3mf", "stl"]:
raise ValueError(f"Unknown file format {output_file_format}") raise ValueError(f"Unknown file format {output_file_format}")