# Copyright (c) 2016 Ultimaker B.V. # Cura is released under the terms of the AGPLv3 or higher. from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from UM.Application import Application from cura.Settings.ExtruderManager import ExtruderManager from UM.Math.Polygon import Polygon from . import ConvexHullNode import UM.Settings.ContainerRegistry import numpy ## The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node. # If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed. class ConvexHullDecorator(SceneNodeDecorator): def __init__(self): super().__init__() self._convex_hull_node = None self._init2DConvexHullCache() self._global_stack = None self._raft_thickness = 0.0 # For raft thickness, DRY self._build_volume = Application.getInstance().getBuildVolume() self._build_volume.raftThicknessChanged.connect(self._onChanged) Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) Application.getInstance().getController().toolOperationStarted.connect(self._onChanged) Application.getInstance().getController().toolOperationStopped.connect(self._onChanged) self._onGlobalStackChanged() def setNode(self, node): previous_node = self._node # Disconnect from previous node signals if previous_node is not None and node is not previous_node: previous_node.transformationChanged.disconnect(self._onChanged) previous_node.parentChanged.disconnect(self._onChanged) super().setNode(node) self._node.transformationChanged.connect(self._onChanged) self._node.parentChanged.connect(self._onChanged) self._onChanged() ## Force that a new (empty) object is created upon copy. def __deepcopy__(self, memo): return ConvexHullDecorator() ## Get the unmodified 2D projected convex hull of the node def getConvexHull(self): if self._node is None: return None hull = self._compute2DConvexHull() if self._global_stack and self._node: if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"): hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon", "value"), numpy.float32))) hull = self._add2DAdhesionMargin(hull) return hull ## Get the convex hull of the node with the full head size def getConvexHullHeadFull(self): if self._node is None: return None return self._compute2DConvexHeadFull() ## Get convex hull of the object + head size # In case of printing all at once this is the same as the convex hull. # For one at the time this is area with intersection of mirrored head def getConvexHullHead(self): if self._node is None: return None if self._global_stack: if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"): head_with_fans = self._compute2DConvexHeadMin() head_with_fans_with_adhesion_margin = self._add2DAdhesionMargin(head_with_fans) return head_with_fans_with_adhesion_margin return None ## Get convex hull of the node # In case of printing all at once this is the same as the convex hull. # For one at the time this is the area without the head. def getConvexHullBoundary(self): if self._node is None: return None if self._global_stack: if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"): # Printing one at a time and it's not an object in a group return self._compute2DConvexHull() return None def recomputeConvexHull(self): controller = Application.getInstance().getController() root = controller.getScene().getRoot() if self._node is None or controller.isToolOperationActive() or not self.__isDescendant(root, self._node): if self._convex_hull_node: self._convex_hull_node.setParent(None) self._convex_hull_node = None return convex_hull = self.getConvexHull() if self._convex_hull_node: self._convex_hull_node.setParent(None) hull_node = ConvexHullNode.ConvexHullNode(self._node, convex_hull, self._raft_thickness, root) self._convex_hull_node = hull_node def _onSettingValueChanged(self, key, property_name): if property_name != "value": #Not the value that was changed. return if key in self._affected_settings: self._onChanged() if key in self._influencing_settings: self._init2DConvexHullCache() #Invalidate the cache. self._onChanged() def _init2DConvexHullCache(self): # Cache for the group code path in _compute2DConvexHull() self._2d_convex_hull_group_child_polygon = None self._2d_convex_hull_group_result = None # Cache for the mesh code path in _compute2DConvexHull() self._2d_convex_hull_mesh = None self._2d_convex_hull_mesh_world_transform = None self._2d_convex_hull_mesh_result = None def _compute2DConvexHull(self): if self._node.callDecoration("isGroup"): points = numpy.zeros((0, 2), dtype=numpy.int32) for child in self._node.getChildren(): child_hull = child.callDecoration("_compute2DConvexHull") if child_hull: points = numpy.append(points, child_hull.getPoints(), axis = 0) if points.size < 3: return None child_polygon = Polygon(points) # Check the cache if child_polygon == self._2d_convex_hull_group_child_polygon: return self._2d_convex_hull_group_result convex_hull = child_polygon.getConvexHull() #First calculate the normal convex hull around the points. offset_hull = self._offsetHull(convex_hull) #Then apply the offset from the settings. # Store the result in the cache self._2d_convex_hull_group_child_polygon = child_polygon self._2d_convex_hull_group_result = offset_hull return offset_hull else: offset_hull = None mesh = None world_transform = None if self._node.getMeshData(): mesh = self._node.getMeshData() world_transform = self._node.getWorldTransformation() # Check the cache if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform: return self._2d_convex_hull_mesh_result vertex_data = mesh.getConvexHullTransformedVertices(world_transform) # Don't use data below 0. # TODO; We need a better check for this as this gives poor results for meshes with long edges. # Do not throw away vertices: the convex hull may be too small and objects can collide. # vertex_data = vertex_data[vertex_data[:,1] >= -0.01] if len(vertex_data) >= 4: # Round the vertex data to 1/10th of a mm, then remove all duplicate vertices # This is done to greatly speed up further convex hull calculations as the convex hull # becomes much less complex when dealing with highly detailed models. vertex_data = numpy.round(vertex_data, 1) vertex_data = vertex_data[:, [0, 2]] # Drop the Y components to project to 2D. # Grab the set of unique points. # # This basically finds the unique rows in the array by treating them as opaque groups of bytes # which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch. # See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array vertex_byte_view = numpy.ascontiguousarray(vertex_data).view( numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1]))) _, idx = numpy.unique(vertex_byte_view, return_index=True) vertex_data = vertex_data[idx] # Select the unique rows by index. hull = Polygon(vertex_data) if len(vertex_data) >= 4: convex_hull = hull.getConvexHull() offset_hull = self._offsetHull(convex_hull) else: return Polygon([]) # Node has no mesh data, so just return an empty Polygon. # Store the result in the cache self._2d_convex_hull_mesh = mesh self._2d_convex_hull_mesh_world_transform = world_transform self._2d_convex_hull_mesh_result = offset_hull return offset_hull def _getHeadAndFans(self): return Polygon(numpy.array(self._global_stack.getProperty("machine_head_with_fans_polygon", "value"), numpy.float32)) def _compute2DConvexHeadFull(self): return self._compute2DConvexHull().getMinkowskiHull(self._getHeadAndFans()) def _compute2DConvexHeadMin(self): headAndFans = self._getHeadAndFans() mirrored = headAndFans.mirror([0, 0], [0, 1]).mirror([0, 0], [1, 0]) # Mirror horizontally & vertically. head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored) # Min head hull is used for the push free min_head_hull = self._compute2DConvexHull().getMinkowskiHull(head_and_fans) return min_head_hull ## Compensate given 2D polygon with adhesion margin # \return 2D polygon with added margin def _add2DAdhesionMargin(self, poly): # Compensate for raft/skirt/brim # Add extra margin depending on adhesion type adhesion_type = self._global_stack.getProperty("adhesion_type", "value") if adhesion_type == "raft": extra_margin = max(0, self._getSettingProperty("raft_margin", "value")) elif adhesion_type == "brim": extra_margin = max(0, self._getSettingProperty("brim_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value")) elif adhesion_type == "skirt": extra_margin = max( 0, self._getSettingProperty("skirt_gap", "value") + self._getSettingProperty("skirt_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value")) else: raise Exception("Unknown bed adhesion type. Did you forget to update the convex hull calculations for your new bed adhesion type?") # adjust head_and_fans with extra margin if extra_margin > 0: extra_margin_polygon = Polygon.approximatedCircle(extra_margin) poly = poly.getMinkowskiHull(extra_margin_polygon) return poly ## Offset the convex hull with settings that influence the collision area. # # This also applies a minimum offset of 0.5mm, because of edge cases due # to the rounding we apply. # # \param convex_hull Polygon of the original convex hull. # \return New Polygon instance that is offset with everything that # influences the collision area. def _offsetHull(self, convex_hull): horizontal_expansion = max(0.5, self._getSettingProperty("xy_offset", "value")) expansion_polygon = Polygon(numpy.array([ [-horizontal_expansion, -horizontal_expansion], [-horizontal_expansion, horizontal_expansion], [horizontal_expansion, horizontal_expansion], [horizontal_expansion, -horizontal_expansion] ], numpy.float32)) return convex_hull.getMinkowskiHull(expansion_polygon) def _onChanged(self, *args): self._raft_thickness = self._build_volume.getRaftThickness() self.recomputeConvexHull() def _onGlobalStackChanged(self): if self._global_stack: self._global_stack.propertyChanged.disconnect(self._onSettingValueChanged) self._global_stack.containersChanged.disconnect(self._onChanged) extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_stack.getId()) for extruder in extruders: extruder.propertyChanged.disconnect(self._onSettingValueChanged) self._global_stack = Application.getInstance().getGlobalContainerStack() if self._global_stack: self._global_stack.propertyChanged.connect(self._onSettingValueChanged) self._global_stack.containersChanged.connect(self._onChanged) extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_stack.getId()) for extruder in extruders: extruder.propertyChanged.connect(self._onSettingValueChanged) self._onChanged() ## Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property). def _getSettingProperty(self, setting_key, property="value"): per_mesh_stack = self._node.callDecoration("getStack") if per_mesh_stack: return per_mesh_stack.getProperty(setting_key, property) multi_extrusion = self._global_stack.getProperty("machine_extruder_count", "value") > 1 if not multi_extrusion: return self._global_stack.getProperty(setting_key, property) extruder_index = self._global_stack.getProperty(setting_key, "limit_to_extruder") if extruder_index == "-1": #No limit_to_extruder. extruder_stack_id = self._node.callDecoration("getActiveExtruder") if not extruder_stack_id: #Decoration doesn't exist. extruder_stack_id = ExtruderManager.getInstance().extruderIds["0"] extruder_stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0] return extruder_stack.getProperty(setting_key, property) else: #Limit_to_extruder is set. Use that one. extruder_stack_id = ExtruderManager.getInstance().extruderIds[str(extruder_index)] stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0] return stack.getProperty(setting_key, property) ## Returns true if node is a descendant or the same as the root node. def __isDescendant(self, root, node): if node is None: return False if root is node: return True return self.__isDescendant(root, node.getParent()) _affected_settings = [ "adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "raft_margin", "print_sequence", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance", "brim_line_count"] ## Settings that change the convex hull. # # If these settings change, the convex hull should be recalculated. _influencing_settings = {"xy_offset"}