Cura/plugins/CuraEngineBackend/SupportMeshCreator.py
2025-06-18 09:07:24 +02:00

150 lines
6.6 KiB
Python

# Copyright (c) 2018 fieldOfView
# The Blackbelt plugin is released under the terms of the LGPLv3 or higher.
import numpy
import math
import trimesh
from UM.Extension import Extension
from UM.Application import Application
from UM.Logger import Logger
from UM.Mesh.MeshData import MeshData, calculateNormalsFromIndexedVertices
from UM.Math.Vector import Vector
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class SupportMeshCreator():
def __init__(self,
support_angle = None,
filter_upwards_facing_faces = True,
down_vector = numpy.array([0, -1, 0]),
bottom_cut_off = 0,
minimum_island_area = 0
):
self._support_angle = support_angle
if self._support_angle is None:
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
support_extruder_nr = global_container_stack.getExtruderPositionValueWithDefault("support_extruder_nr")
support_angle_stack = Application.getInstance().getExtruderManager().getExtruderStack(support_extruder_nr)
self._support_angle = support_angle_stack.getProperty("support_angle", "value")
else:
self._support_angle = 50
self._filter_upwards_facing_faces = filter_upwards_facing_faces
self._minimum_island_area = minimum_island_area
self._down_vector = down_vector
self._bottom_cut_off = bottom_cut_off
def createSupportMeshForNode(self, node):
# convert node meshdata to trimesh
node_name = node.getName()
mesh_data = node.getMeshData().getTransformed(node.getWorldTransformation())
node_vertices = mesh_data.getVertices()
node_indices = mesh_data.getIndices()
if node_indices is None:
# some file formats (eg 3mf) don't supply indices, but have unique vertices per face
node_indices = numpy.arange(len(node_vertices)).reshape(-1, 3)
support_mesh = self.createSupportMesh(node_name, node_vertices, node_indices)
if support_mesh is not None:
# convert resulting trimesh into meshdata
mesh_data = self._toMeshData(support_mesh)
return mesh_data
def createSupportMesh(self, node_name, node_vertices, node_indices):
tri_mesh = trimesh.base.Trimesh(vertices=node_vertices, faces=node_indices)
# make sure normals are sane
tri_mesh.fix_normals()
cos_support_angle = math.cos(math.radians(90 - self._support_angle))
# get indices of faces that face down more than support_angle
cos_angle_between_normal_down = numpy.dot(tri_mesh.face_normals, self._down_vector)
faces_needing_support = numpy.argwhere(cos_angle_between_normal_down >= cos_support_angle).flatten()
# filter out faces that point upwards
if len(faces_needing_support) == 0 and self._filter_upwards_facing_faces:
faces_facing_down = numpy.argwhere(tri_mesh.face_normals[:,1] < 0)
faces_needing_support = numpy.intersect1d(faces_facing_down, faces_needing_support)
if len(faces_needing_support) == 0:
Logger.log("d", "Node %s doesn't need support" % node_name)
return None
roof_indices = node_indices[faces_needing_support]
# filter out faces that are coplanar with the bottom
non_bottom_indices = numpy.where(numpy.any(node_vertices[roof_indices].take(1, axis=2) > self._bottom_cut_off, axis=1))[0].flatten()
roof_indices = roof_indices[non_bottom_indices]
if len(roof_indices) == 0:
Logger.log("d", "Node %s doesn't need support" % node_name)
return None
roof = trimesh.base.Trimesh(vertices=node_vertices, faces=roof_indices)
roof.remove_unreferenced_vertices()
roof.process()
if self._minimum_island_area > 0:
# filter out all islands that would result in small towers
scale_matrix = trimesh.transformations.scale_matrix(0, direction=[0,1,0])
roof_elements = roof.split(only_watertight=False)
roof = trimesh.base.Trimesh()
for roof_element in roof_elements:
xy_element = roof_element.copy()
xy_element.apply_transform(scale_matrix)
if xy_element.area >= self._minimum_island_area:
roof = roof + roof_element
num_roof_vertices = len(roof.vertices)
if num_roof_vertices == 0:
Logger.log("d", "All surfaces of node %s that need support are smaller than %d" % (node_name, self._minimum_island_area))
return None
connecting_faces = []
roof_outline = roof.outline()
for entity in roof_outline.entities:
entity_points = entity.points
outline = roof_outline.vertices[entity_points]
# numpy magic to find indices for each outline vertex
outline_indices = numpy.where((roof.vertices==outline[:,None]).all(-1))[1]
num_outline_vertices = len(outline)
for i in range(0, num_outline_vertices - 1):
connecting_faces.append([outline_indices[i], outline_indices[i + 1] + num_roof_vertices, outline_indices[i] + num_roof_vertices])
connecting_faces.append([outline_indices[i], outline_indices[i + 1], outline_indices[i + 1] + num_roof_vertices])
support_vertices = numpy.concatenate((roof.vertices, roof.vertices * [1,0,1]))
support_faces = numpy.concatenate((roof.faces, roof.faces + len(roof.vertices), connecting_faces))
support_mesh = trimesh.base.Trimesh(vertices=support_vertices, faces=support_faces)
support_mesh.fix_normals()
return support_mesh
def _toMeshData(self, tri_node: trimesh.base.Trimesh) -> MeshData:
tri_faces = tri_node.faces
tri_vertices = tri_node.vertices
indices = []
vertices = []
index_count = 0
face_count = 0
for tri_face in tri_faces:
face = []
for tri_index in tri_face:
vertices.append(tri_vertices[tri_index])
face.append(index_count)
index_count += 1
indices.append(face)
face_count += 1
vertices = numpy.asarray(vertices, dtype=numpy.float32)
indices = numpy.asarray(indices, dtype=numpy.int32)
normals = calculateNormalsFromIndexedVertices(vertices, indices, face_count)
mesh_data = MeshData(vertices=vertices, indices=indices, normals=normals)
return mesh_data