diff --git a/cura/ConvexHullDecorator.py b/cura/ConvexHullDecorator.py index b53737cc80..1d03c250e5 100644 --- a/cura/ConvexHullDecorator.py +++ b/cura/ConvexHullDecorator.py @@ -1,28 +1,20 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from UM.Application import Application +from UM.Math.Polygon import Polygon +from UM.Logger import Logger +from . import ConvexHullNode + +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): + def __init__(self,): super().__init__() - self._convex_hull = None - - # 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. - self._convex_hull_boundary = None - - # 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 - self._convex_hull_head = None - # 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 full head - self._convex_hull_head_full = None - self._convex_hull_node = None - self._convex_hull_job = None + self._init2DConvexHullCache() self._profile = None Application.getInstance().getMachineManager().activeProfileChanged.connect(self._onActiveProfileChanged) @@ -31,59 +23,56 @@ class ConvexHullDecorator(SceneNodeDecorator): ## Force that a new (empty) object is created upon copy. def __deepcopy__(self, memo): - copy = ConvexHullDecorator() - return copy + return ConvexHullDecorator() - ## Get the unmodified convex hull of the node + ## Get the unmodified 2D projected convex hull of the node def getConvexHull(self): - return self._convex_hull + hull = self._compute2DConvexHull() + profile = Application.getInstance().getMachineManager().getWorkingProfile() + if profile: + if profile.getSettingValue("print_sequence") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"): + hull = hull.getMinkowskiHull(Polygon(numpy.array(profile.getSettingValue("machine_head_polygon"), numpy.float32))) + return hull ## Get the convex hull of the node with the full head size def getConvexHullHeadFull(self): - if not self._convex_hull_head_full: - return self.getConvexHull() - return self._convex_hull_head_full + 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 not self._convex_hull_head: - return self.getConvexHull() - return self._convex_hull_head + profile = Application.getInstance().getMachineManager().getWorkingProfile() + if profile: + if profile.getSettingValue("print_sequence") == "one_at_a_time" and not self._node.getParent().callDecoration( + "isGroup"): + return self._compute2DConvexHeadMin() + 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 not self._convex_hull_boundary: - return self.getConvexHull() - return self._convex_hull_boundary - - def setConvexHullBoundary(self, hull): - self._convex_hull_boundary = hull + profile = Application.getInstance().getMachineManager().getWorkingProfile() + if profile: + if profile.getSettingValue("print_sequence") == "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 setConvexHullHeadFull(self, hull): - self._convex_hull_head_full = hull + def recomputeConvexHull(self): + convex_hull = self.getConvexHull() + if self._convex_hull_node: + if self._convex_hull_node.getHull() == convex_hull: + Logger.log('d', 'ConvexHullDecorator not creating a new ConvexHullNode') + return + self._convex_hull_node.setParent(None) + Logger.log('d', 'ConvexHullDecorator creating ConvexHullNode') + hull_node = ConvexHullNode.ConvexHullNode(self._node, convex_hull, + Application.getInstance().getController().getScene().getRoot()) + self._convex_hull_node = hull_node - def setConvexHullHead(self, hull): - self._convex_hull_head = hull - - def setConvexHull(self, hull): - self._convex_hull = hull - - def getConvexHullJob(self): - return self._convex_hull_job - - def setConvexHullJob(self, job): - self._convex_hull_job = job - - def getConvexHullNode(self): - return self._convex_hull_node - - def setConvexHullNode(self, node): - self._convex_hull_node = node - def _onActiveProfileChanged(self): if self._profile: self._profile.settingValueChanged.disconnect(self._onSettingValueChanged) @@ -94,18 +83,118 @@ class ConvexHullDecorator(SceneNodeDecorator): self._profile.settingValueChanged.connect(self._onSettingValueChanged) def _onActiveMachineInstanceChanged(self): - if self._convex_hull_job: - self._convex_hull_job.cancel() - self.setConvexHull(None) if self._convex_hull_node: self._convex_hull_node.setParent(None) self._convex_hull_node = None def _onSettingValueChanged(self, setting): if setting == "print_sequence": - if self._convex_hull_job: - self._convex_hull_job.cancel() - self.setConvexHull(None) - if self._convex_hull_node: - self._convex_hull_node.setParent(None) - self._convex_hull_node = None + self.recomputeConvexHull() + + 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: + # Logger.log('d', 'Cache hit in _compute2DConvexHull group path') + return self._2d_convex_hull_group_result + + # First, calculate the normal convex hull around the points + convex_hull = child_polygon.getConvexHull() + + # Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull. + # This is done because of rounding errors. + rounded_hull = self._roundHull(convex_hull) + + # Store the result in the cache + self._2d_convex_hull_group_child_polygon = child_polygon + self._2d_convex_hull_group_result = rounded_hull + + return rounded_hull + + else: + if not self._node.getMeshData(): + return None + 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: + # Logger.log('d', 'Cache hit in _compute2DConvexHull mesh path') + 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. + vertex_data = vertex_data[vertex_data[:,1] >= 0] + + # 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) + + # First, calculate the normal convex hull around the points + convex_hull = hull.getConvexHull() + + # Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull. + # This is done because of rounding errors. + rounded_hull = convex_hull.getMinkowskiHull(Polygon(numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32))) + + # 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 = rounded_hull + + return rounded_hull + + def _getHeadAndFans(self): + profile = Application.getInstance().getMachineManager().getWorkingProfile() + return Polygon(numpy.array(profile.getSettingValue("machine_head_with_fans_polygon"), 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 + + def _roundHull(self, convex_hull): + return convex_hull.getMinkowskiHull(Polygon(numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32))) diff --git a/cura/ConvexHullJob.py b/cura/ConvexHullJob.py index 9fb18981d3..09d23b7104 100644 --- a/cura/ConvexHullJob.py +++ b/cura/ConvexHullJob.py @@ -4,6 +4,7 @@ from UM.Job import Job from UM.Application import Application from UM.Math.Polygon import Polygon +from UM.Logger import Logger import numpy import copy @@ -19,6 +20,11 @@ class ConvexHullJob(Job): def run(self): if not self._node: return + + ################################################################# + # Node Convex Hull + ################################################################# + ## If the scene node is a group, use the hull of the children to calculate its hull. if self._node.callDecoration("isGroup"): hull = Polygon(numpy.zeros((0, 2), dtype=numpy.int32)) @@ -47,10 +53,20 @@ class ConvexHullJob(Job): # 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) - duplicates = (vertex_data[:,0] == vertex_data[:,1]) | (vertex_data[:,1] == vertex_data[:,2]) | (vertex_data[:,0] == vertex_data[:,2]) - vertex_data = numpy.delete(vertex_data, numpy.where(duplicates), axis = 0) - hull = Polygon(vertex_data[:, [0, 2]]) + 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) # First, calculate the normal convex hull around the points hull = hull.getConvexHull() @@ -59,6 +75,16 @@ class ConvexHullJob(Job): # This is done because of rounding errors. hull = hull.getMinkowskiHull(Polygon(numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32))) + ################################################################# + # Print Head Exclusion Zone + ################################################################# + + + # + # TODO + # ConvexHullDecorator should use a memoization strategy in its getters. + # Make MeshData immutable + profile = Application.getInstance().getMachineManager().getWorkingProfile() if profile: if profile.getSettingValue("print_sequence") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"): @@ -99,3 +125,33 @@ class ConvexHullJob(Job): hull_node = self._node.getParent().callDecoration("getConvexHullNode") if hull_node: hull_node.setParent(None) + + try: + Logger.log('d', 'ConvexHullJob getConvexHull:' + dumpPoly(self._node.callDecoration("getConvexHull"))) + Logger.log('d', 'ConvexHullJob new getConvexHull:' + dumpPoly(self._node.callDecoration("newGetConvexHull"))) + except Exception: + pass + + try: + Logger.log('d', 'ConvexHullJob getConvexHullHeadFull:' + dumpPoly(self._node.callDecoration("getConvexHullHeadFull"))) + Logger.log('d', 'ConvexHullJob new getConvexHullHeadFull:' + dumpPoly(self._node.callDecoration("newGetConvexHullHeadFull"))) + except Exception: + pass + + try: + Logger.log('d', 'ConvexHullJob getConvexHullHead:' + dumpPoly(self._node.callDecoration("getConvexHullHead"))) + Logger.log('d', 'ConvexHullJob new getConvexHullHead:' + dumpPoly(self._node.callDecoration("newGetConvexHullHead"))) + except Exception: + pass + + try: + Logger.log('d', 'ConvexHullJob getConvexHullBoundary:' + dumpPoly(self._node.callDecoration("getConvexHullBoundary"))) + Logger.log('d', 'ConvexHullJob new getConvexHullBoundary:' + dumpPoly(self._node.callDecoration("newGetConvexHullBoundary"))) + except Exception: + pass + +def dumpPoly(poly): + if poly is None: + return "None" + else: + return repr(poly.getPoints()) diff --git a/cura/ConvexHullNode.py b/cura/ConvexHullNode.py index 905aeb16d4..b5a2df518d 100644 --- a/cura/ConvexHullNode.py +++ b/cura/ConvexHullNode.py @@ -8,7 +8,7 @@ from UM.Math.Vector import Vector from UM.Mesh.MeshBuilder import MeshBuilder # To create a mesh to display the convex hull with. from UM.View.GL.OpenGL import OpenGL - +from UM.Logger import spy class ConvexHullNode(SceneNode): ## Convex hull node is a special type of scene node that is used to display a 2D area, to indicate the @@ -46,6 +46,9 @@ class ConvexHullNode(SceneNode): if convex_hull_head: self._convex_hull_head_mesh = self.createHullMesh(convex_hull_head.getPoints()) + def getHull(self): + return self._hull + ## Actually create the mesh from the hullpoints # /param hull_points list of xy values # /return meshData @@ -62,7 +65,7 @@ class ConvexHullNode(SceneNode): mesh_builder.addFace(point_first, point_previous, point_new, color = self._color) point_previous = point_new # Prepare point_previous for the next triangle. - return mesh_builder.getData() + return mesh_builder.build() def getWatchedNode(self): return self._node @@ -80,9 +83,7 @@ class ConvexHullNode(SceneNode): return True def _onNodePositionChanged(self, node): - if node.callDecoration("getConvexHull"): - node.callDecoration("setConvexHull", None) - node.callDecoration("setConvexHullNode", None) + if node.callDecoration("getConvexHull"): self.setParent(None) # Garbage collection should delete this node after a while. def _onNodeParentChanged(self, node): diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 72ff8aec8c..4977cc799b 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -278,9 +278,11 @@ class CuraApplication(QtApplication): count += 1 if not scene_boundingbox: - scene_boundingbox = copy.deepcopy(node.getBoundingBox()) + scene_boundingbox = node.getBoundingBox() else: - scene_boundingbox += node.getBoundingBox() + other_bb = node.getBoundingBox() + if other_bb is not None: + scene_boundingbox = scene_boundingbox + node.getBoundingBox() if not scene_boundingbox: scene_boundingbox = AxisAlignedBox.Null diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 57d8f4e0ba..b0863ccfd7 100644 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -45,7 +45,7 @@ class PlatformPhysics: root = self._controller.getScene().getRoot() for node in BreadthFirstIterator(root): - if node is root or type(node) is not SceneNode: + if node is root or type(node) is not SceneNode or node.getBoundingBox() is None: continue bbox = node.getBoundingBox() @@ -73,14 +73,9 @@ class PlatformPhysics: # If there is no convex hull for the node, start calculating it and continue. if not node.getDecorator(ConvexHullDecorator): node.addDecorator(ConvexHullDecorator()) - - if not node.callDecoration("getConvexHull"): - if not node.callDecoration("getConvexHullJob"): - job = ConvexHullJob.ConvexHullJob(node) - job.start() - node.callDecoration("setConvexHullJob", job) - - elif Preferences.getInstance().getValue("physics/automatic_push_free"): + node.callDecoration("recomputeConvexHull") + + if Preferences.getInstance().getValue("physics/automatic_push_free"): # Check for collisions between convex hulls for other_node in BreadthFirstIterator(root): # Ignore root, ourselves and anything that is not a normal SceneNode.