diff --git a/bbp_ng/UTIL_blender_mesh.py b/bbp_ng/UTIL_blender_mesh.py index e54ee69..9f2080e 100644 --- a/bbp_ng/UTIL_blender_mesh.py +++ b/bbp_ng/UTIL_blender_mesh.py @@ -16,20 +16,23 @@ class FaceVertexData(): mPosIdx: int mNmlIdx: int mUvIdx: int - + def __init__(self, pos: int = 0, nml: int = 0, uv: int = 0): self.mPosIdx = pos self.mNmlIdx = nml self.mUvIdx = uv class FaceData(): - mIndices: tuple[FaceVertexData, ...] + ## @remark List or tuple. List is convenient for adding and removing + mIndices: tuple[FaceVertexData] | list[FaceVertexData] + ## Face used material slot index + # @remark If material slot is empty, or this face do not use material, set this value to 0. mMtlIdx: int - - def __init__(self, indices: tuple[FaceVertexData, ...] = tuple(), mtlidx: int = 0): + + def __init__(self, indices: tuple[FaceVertexData] | list[FaceVertexData] = tuple(), mtlidx: int = 0): self.mIndices = indices self.mMtlIdx = mtlidx - + def is_indices_legal(self) -> bool: return len(self.mIndices) >= 3 @@ -62,14 +65,217 @@ def _nest_custom_split_normal(nml_idx: array.array, nml_array: array.array) -> t pos: int = idx * 3 yield (nml_array[pos], nml_array[pos + 1], nml_array[pos + 2]) +class TemporaryMesh(): + """ + + """ + + __mBindingObject: bpy.types.Object | None + __mTempMesh: bpy.types.Mesh | None + + def __init__(self, binding_obj: bpy.types.Object): + self.__mBindingObject = binding_obj + self.__mTempMesh = None + + if self.__mBindingObject.data is None: + raise UTIL_functions.BBPException('try getting mesh from an object without mesh.') + self.__mTempMesh = self.__mBindingObject.to_mesh() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.dispose() + + def is_valid(self) -> bool: + if self.__mBindingObject is None: return False + if self.__mTempMesh is None: return False + return True + + def dispose(self): + if self.is_valid(): + self.__mTempMesh = None + self.__mBindingObject.to_mesh_clear() + self.__mBindingObject = None + + def get_temp_mesh(self) -> bpy.types.Mesh: + if not self.is_valid(): + raise UTIL_functions.BBPException('try calling invalid TemporaryMesh.') + return self.__mTempMesh + + #endregion class MeshReader(): """ The passed mesh must be created by bpy.types.Object.to_mesh() and destroyed by bpy.types.Object.to_mesh_clear(). Because this class must trianglate mesh. To prevent change original mesh, this operations is essential. + A helper class TemporaryMesh can help you do this. """ - pass + + __mAssocMesh: bpy.types.Mesh | None ##< The binding mesh for this reader. None if this reader is invalid. + + + + def __init__(self, assoc_mesh: bpy.types.Mesh): + self.__mAssocMesh = assoc_mesh + + # triangulate temp mesh + if self.is_valid(): + self.__triangulate_mesh() + self.__mAssocMesh.calc_normals_split() + + def is_valid(self) -> bool: + return self.__mAssocMesh is not None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.dispose() + + def dispose(self): + if self.is_valid(): + # reset mesh + self.__mAssocMesh.free_normals_split() + self.__mAssocMesh = None + + def get_vertex_position_count(self) -> int: + if not self.is_valid(): + raise UTIL_functions.BBPException('try to call an invalid MeshReader.') + + return len(self.__mAssocMesh.vertices) + + def get_vertex_position(self) -> typing.Iterator[UTIL_virtools_types.VxVector3]: + if not self.is_valid(): + raise UTIL_functions.BBPException('try to call an invalid MeshReader.') + + cache: UTIL_virtools_types.VxVector3 = UTIL_virtools_types.VxVector3() + for vec in self.__mAssocMesh.vertices: + cache.x = vec.co.x + cache.y = vec.co.y + cache.z = vec.co.z + yield cache + + def get_vertex_normal_count(self) -> int: + if not self.is_valid(): + raise UTIL_functions.BBPException('try to call an invalid MeshReader.') + + # return loops count, equaling with face count * 3 in theory. + return len(self.__mAssocMesh.loops) + + def get_vertex_normal(self) -> typing.Iterator[UTIL_virtools_types.VxVector3]: + if not self.is_valid(): + raise UTIL_functions.BBPException('try to call an invalid MeshReader.') + + cache: UTIL_virtools_types.VxVector3 = UTIL_virtools_types.VxVector3() + for nml in self.__mAssocMesh.loops: + cache.x = nml.normal.x + cache.y = nml.normal.y + cache.z = nml.normal.z + yield cache + + def get_vertex_uv_count(self) -> int: + if not self.is_valid(): + raise UTIL_functions.BBPException('try to call an invalid MeshReader.') + + if self.__mAssocMesh.uv_layers.active is None: + # if no uv layer, we need make a fake one + # return the same value with normals. + # it also mean create uv for each face vertex + return len(self.__mAssocMesh.loops) + else: + # otherwise return its size, also equaling with face count * 3 in theory + return len(self.__mAssocMesh.uv_layers.active.uv) + + def get_vertex_uv(self) -> typing.Iterator[UTIL_virtools_types.VxVector2]: + if not self.is_valid(): + raise UTIL_functions.BBPException('try to call an invalid MeshReader.') + + cache: UTIL_virtools_types.VxVector2 = UTIL_virtools_types.VxVector2() + if self.__mAssocMesh.uv_layers.active is None: + # create a fake one + cache.x = 0.0 + cache.y = 0.0 + for _ in range(self.get_vertex_uv_count()): + yield cache + else: + for uv in self.__mAssocMesh.uv_layers.active.uv: + cache.x = uv.vector.x + cache.y = uv.vector.y + yield cache + + def get_material_slot_count(self) -> int: + if not self.is_valid(): + raise UTIL_functions.BBPException('try to call an invalid MeshReader.') + + return len(self.__mAssocMesh.materials) + + def get_material_slot(self) -> typing.Iterator[bpy.types.Material]: + """ + @remark This generator may return None if this slot do not link to may material. + """ + if not self.is_valid(): + raise UTIL_functions.BBPException('try to call an invalid MeshReader.') + + for mtl in self.__mAssocMesh.materials: + yield mtl + + def get_face_count(self) -> int: + if not self.is_valid(): + raise UTIL_functions.BBPException('try to call an invalid MeshReader.') + + return len(self.__mAssocMesh.polygons) + + def get_face(self) -> typing.Iterator[FaceData]: + if not self.is_valid(): + raise UTIL_functions.BBPException('try to call an invalid MeshReader.') + + # detect whether we have material + no_mtl: bool = self.get_material_slot_count() == 0 + + # use list as indices container for convenient adding and deleting. + cache: FaceData = FaceData([], 0) + for face in self.__mAssocMesh.polygons: + # confirm material use + # a face without mtl have 2 situations. first is the whole object do not have mtl + # another is this face use an empty mtl slot. + if no_mtl: + cache.mMtlIdx = 0 + else: + cache.mMtlIdx = face.material_index + + # resize indices + self.__resize_face_data_indices(cache.mIndices, face.loop_total) + # set indices + for i in range(face.loop_total): + cache.mIndices[i].mPosIdx = self.__mAssocMesh.loops[face.loop_start + i].vertex_index + cache.mIndices[i].mNmlIdx = face.loop_start + i + cache.mIndices[i].mUvIdx = face.loop_start + i + + # return value + yield cache + + def __resize_face_data_indices(self, ls: list[FaceVertexData], expected_size: int) -> None: + diff: int = expected_size - len(ls) + if diff > 0: + # add entry + for _ in range(diff): + ls.append(FaceVertexData()) + elif diff < 0: + # remove entry + for _ in range(diff): + ls.pop() + else: + # no count diff, pass + pass + + def __triangulate_mesh(self) -> None: + bm = bmesh.new() + bm.from_mesh(self.__mAssocMesh) + bmesh.ops.triangulate(bm, faces = bm.faces) + bm.to_mesh(self.__mAssocMesh) + bm.free() class MeshWriter(): """ @@ -116,7 +322,7 @@ class MeshWriter(): ## Same as __mFacePosIndices, but store face vertex uv index. # Array item is int32(L). Length is equal to __mFacePosIndices __mFaceUvIndices: array.array - ## Array item is int32(L). + ## Array item is int32(L). # Length is the face count. # It indicate how much vertex need to be consumed in __mFacePosIndices, __mFaceNmlIndices and __mFaceUvIndices for one face. __mFaceVertexCount: array.array @@ -126,7 +332,7 @@ class MeshWriter(): # Each item is unique make sure by __mMtlSlotMap __mMtlSlot: list[bpy.types.Material] ## The map to make sure every item in __mMtlSlot is unique. - # Key is bpy.types.Material + # Key is bpy.types.Material # Value is key's index in __mMtlSlot. __mMtlSlotMap: dict[bpy.types.Material, int] @@ -155,7 +361,16 @@ class MeshWriter(): def __exit__(self, exc_type, exc_value, traceback): self.dispose() + def dispose(self): + if self.is_valid(): + # write mesh + self.__write_mesh() + # reset mesh + self.__mAssocMesh = None + def add_part(self, data: MeshWriterPartData): + if not self.is_valid(): + raise UTIL_functions.BBPException('try to call an invalid MeshWriter.') if not data.is_valid(): raise UTIL_functions.BBPException('invalid mesh part data.') @@ -198,14 +413,7 @@ class MeshWriter(): self.__mFaceMtlIdx.append(0) else: self.__mFaceMtlIdx.append(mtl_remap[mtl_idx]) - - def dispose(self): - if self.is_valid(): - # write mesh - self.__write_mesh() - # reset mesh - self.__mAssocMesh = None - + def __write_mesh(self): # detect status if not self.is_valid(): @@ -216,7 +424,7 @@ class MeshWriter(): # push material data for mtl in self.__mMtlSlot: self.__mAssocMesh.materials.append(mtl) - + # add corresponding count for vertex position self.__mAssocMesh.vertices.add(len(self.__mVertexPos)) # add loops data, it is the sum count of indices @@ -228,7 +436,7 @@ class MeshWriter(): self.__mAssocMesh.uv_layers.new(do_init = False) # split normals, it is IMPORTANT self.__mAssocMesh.create_normals_split() - + # add vertex position data self.__mAssocMesh.vertices.foreach_set('co', self.__mVertexPos) # add face vertex pos index data @@ -241,7 +449,7 @@ class MeshWriter(): self.__mAssocMesh.uv_layers[0].uv.foreach_set('vector', list(_flat_face_uv_index(self.__mFaceUvIndices, self.__mVertexUV)) ) # NOTE: blender 3.5 changed. UV must be visited by .uv, not the .data - + # iterate face to set face data fVertexIdx: int = 0 for fi in range(len(self.__mFaceVertexCount)): @@ -250,31 +458,31 @@ class MeshWriter(): # the loop_total will be auto calculated by the next loop_start. # loop_total become read-only self.__mAssocMesh.polygons[fi].loop_start = fVertexIdx - + # set material index self.__mAssocMesh.polygons[fi].material_index = self.__mFaceMtlIdx[fi] - + # set auto smooth. it is IMPORTANT # because it related to whether custom split normal can work self.__mAssocMesh.polygons[fi].use_smooth = True - + # inc vertex idx fVertexIdx += self.__mFaceVertexCount[fi] - + # validate mesh. # it is IMPORTANT that do NOT delete custom data # because we need use these data to set custom split normal later self.__mAssocMesh.validate(clean_customdata = False) # update mesh without mesh calc self.__mAssocMesh.update(calc_edges = False, calc_edges_loose = False) - + # set custom split normal data self.__mAssocMesh.normals_split_custom_set( tuple(_nest_custom_split_normal(self.__mFaceNmlIndices, self.__mVertexNormal)) ) # enable auto smooth. it is IMPORTANT self.__mAssocMesh.use_auto_smooth = True - + def __clear_mesh(self): if not self.is_valid(): raise UTIL_functions.BBPException('try to call an invalid MeshWriter.') @@ -283,6 +491,6 @@ class MeshWriter(): self.__mAssocMesh.clear_geometry() # clear mtl slot because clear_geometry will not do this. self.__mAssocMesh.materials.clear() - + class MeshUVModifier(): pass