Finally, use the new convex hull code to compute the object 'shadow' and exclusion zones.

Contributes to CURA-1504
This commit is contained in:
Simon Edwards 2016-05-23 13:47:21 +02:00
parent 0b858f3878
commit d7127b800c
5 changed files with 222 additions and 79 deletions

View File

@ -1,28 +1,20 @@
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
from UM.Application import Application 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. ## 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. # 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): class ConvexHullDecorator(SceneNodeDecorator):
def __init__(self): def __init__(self,):
super().__init__() 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_node = None
self._convex_hull_job = None self._init2DConvexHullCache()
self._profile = None self._profile = None
Application.getInstance().getMachineManager().activeProfileChanged.connect(self._onActiveProfileChanged) Application.getInstance().getMachineManager().activeProfileChanged.connect(self._onActiveProfileChanged)
@ -31,58 +23,55 @@ class ConvexHullDecorator(SceneNodeDecorator):
## Force that a new (empty) object is created upon copy. ## Force that a new (empty) object is created upon copy.
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
copy = ConvexHullDecorator() return ConvexHullDecorator()
return copy
## Get the unmodified convex hull of the node ## Get the unmodified 2D projected convex hull of the node
def getConvexHull(self): 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 ## Get the convex hull of the node with the full head size
def getConvexHullHeadFull(self): def getConvexHullHeadFull(self):
if not self._convex_hull_head_full: return self._compute2DConvexHeadFull()
return self.getConvexHull()
return self._convex_hull_head_full
## Get convex hull of the object + head size ## Get convex hull of the object + head size
# In case of printing all at once this is the same as the convex hull. # 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 # For one at the time this is area with intersection of mirrored head
def getConvexHullHead(self): def getConvexHullHead(self):
if not self._convex_hull_head: profile = Application.getInstance().getMachineManager().getWorkingProfile()
return self.getConvexHull() if profile:
return self._convex_hull_head 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 ## Get convex hull of the node
# In case of printing all at once this is the same as the convex hull. # 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. # For one at the time this is the area without the head.
def getConvexHullBoundary(self): def getConvexHullBoundary(self):
if not self._convex_hull_boundary: profile = Application.getInstance().getMachineManager().getWorkingProfile()
return self.getConvexHull() if profile:
return self._convex_hull_boundary 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 setConvexHullBoundary(self, hull): def recomputeConvexHull(self):
self._convex_hull_boundary = hull convex_hull = self.getConvexHull()
if self._convex_hull_node:
def setConvexHullHeadFull(self, hull): if self._convex_hull_node.getHull() == convex_hull:
self._convex_hull_head_full = hull Logger.log('d', 'ConvexHullDecorator not creating a new ConvexHullNode')
return
def setConvexHullHead(self, hull): self._convex_hull_node.setParent(None)
self._convex_hull_head = hull Logger.log('d', 'ConvexHullDecorator creating ConvexHullNode')
hull_node = ConvexHullNode.ConvexHullNode(self._node, convex_hull,
def setConvexHull(self, hull): Application.getInstance().getController().getScene().getRoot())
self._convex_hull = hull self._convex_hull_node = hull_node
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): def _onActiveProfileChanged(self):
if self._profile: if self._profile:
@ -94,18 +83,118 @@ class ConvexHullDecorator(SceneNodeDecorator):
self._profile.settingValueChanged.connect(self._onSettingValueChanged) self._profile.settingValueChanged.connect(self._onSettingValueChanged)
def _onActiveMachineInstanceChanged(self): def _onActiveMachineInstanceChanged(self):
if self._convex_hull_job:
self._convex_hull_job.cancel()
self.setConvexHull(None)
if self._convex_hull_node: if self._convex_hull_node:
self._convex_hull_node.setParent(None) self._convex_hull_node.setParent(None)
self._convex_hull_node = None self._convex_hull_node = None
def _onSettingValueChanged(self, setting): def _onSettingValueChanged(self, setting):
if setting == "print_sequence": if setting == "print_sequence":
if self._convex_hull_job: self.recomputeConvexHull()
self._convex_hull_job.cancel()
self.setConvexHull(None) def _init2DConvexHullCache(self):
if self._convex_hull_node: # Cache for the group code path in _compute2DConvexHull()
self._convex_hull_node.setParent(None) self._2d_convex_hull_group_child_polygon = None
self._convex_hull_node = 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)))

View File

@ -4,6 +4,7 @@
from UM.Job import Job from UM.Job import Job
from UM.Application import Application from UM.Application import Application
from UM.Math.Polygon import Polygon from UM.Math.Polygon import Polygon
from UM.Logger import Logger
import numpy import numpy
import copy import copy
@ -19,6 +20,11 @@ class ConvexHullJob(Job):
def run(self): def run(self):
if not self._node: if not self._node:
return return
#################################################################
# Node Convex Hull
#################################################################
## If the scene node is a group, use the hull of the children to calculate its hull. ## If the scene node is a group, use the hull of the children to calculate its hull.
if self._node.callDecoration("isGroup"): if self._node.callDecoration("isGroup"):
hull = Polygon(numpy.zeros((0, 2), dtype=numpy.int32)) 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 # 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. # becomes much less complex when dealing with highly detailed models.
vertex_data = numpy.round(vertex_data, 1) 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 # First, calculate the normal convex hull around the points
hull = hull.getConvexHull() hull = hull.getConvexHull()
@ -59,6 +75,16 @@ class ConvexHullJob(Job):
# This is done because of rounding errors. # 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))) 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() profile = Application.getInstance().getMachineManager().getWorkingProfile()
if profile: if profile:
if profile.getSettingValue("print_sequence") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"): 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") hull_node = self._node.getParent().callDecoration("getConvexHullNode")
if hull_node: if hull_node:
hull_node.setParent(None) 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())

View File

@ -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.Mesh.MeshBuilder import MeshBuilder # To create a mesh to display the convex hull with.
from UM.View.GL.OpenGL import OpenGL from UM.View.GL.OpenGL import OpenGL
from UM.Logger import spy
class ConvexHullNode(SceneNode): class ConvexHullNode(SceneNode):
## Convex hull node is a special type of scene node that is used to display a 2D area, to indicate the ## 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: if convex_hull_head:
self._convex_hull_head_mesh = self.createHullMesh(convex_hull_head.getPoints()) self._convex_hull_head_mesh = self.createHullMesh(convex_hull_head.getPoints())
def getHull(self):
return self._hull
## Actually create the mesh from the hullpoints ## Actually create the mesh from the hullpoints
# /param hull_points list of xy values # /param hull_points list of xy values
# /return meshData # /return meshData
@ -62,7 +65,7 @@ class ConvexHullNode(SceneNode):
mesh_builder.addFace(point_first, point_previous, point_new, color = self._color) mesh_builder.addFace(point_first, point_previous, point_new, color = self._color)
point_previous = point_new # Prepare point_previous for the next triangle. point_previous = point_new # Prepare point_previous for the next triangle.
return mesh_builder.getData() return mesh_builder.build()
def getWatchedNode(self): def getWatchedNode(self):
return self._node return self._node
@ -81,8 +84,6 @@ class ConvexHullNode(SceneNode):
def _onNodePositionChanged(self, node): def _onNodePositionChanged(self, node):
if node.callDecoration("getConvexHull"): if node.callDecoration("getConvexHull"):
node.callDecoration("setConvexHull", None)
node.callDecoration("setConvexHullNode", None)
self.setParent(None) # Garbage collection should delete this node after a while. self.setParent(None) # Garbage collection should delete this node after a while.
def _onNodeParentChanged(self, node): def _onNodeParentChanged(self, node):

View File

@ -278,9 +278,11 @@ class CuraApplication(QtApplication):
count += 1 count += 1
if not scene_boundingbox: if not scene_boundingbox:
scene_boundingbox = copy.deepcopy(node.getBoundingBox()) scene_boundingbox = node.getBoundingBox()
else: 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: if not scene_boundingbox:
scene_boundingbox = AxisAlignedBox.Null scene_boundingbox = AxisAlignedBox.Null

View File

@ -45,7 +45,7 @@ class PlatformPhysics:
root = self._controller.getScene().getRoot() root = self._controller.getScene().getRoot()
for node in BreadthFirstIterator(root): 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 continue
bbox = node.getBoundingBox() bbox = node.getBoundingBox()
@ -73,14 +73,9 @@ class PlatformPhysics:
# If there is no convex hull for the node, start calculating it and continue. # If there is no convex hull for the node, start calculating it and continue.
if not node.getDecorator(ConvexHullDecorator): if not node.getDecorator(ConvexHullDecorator):
node.addDecorator(ConvexHullDecorator()) node.addDecorator(ConvexHullDecorator())
node.callDecoration("recomputeConvexHull")
if not node.callDecoration("getConvexHull"): if Preferences.getInstance().getValue("physics/automatic_push_free"):
if not node.callDecoration("getConvexHullJob"):
job = ConvexHullJob.ConvexHullJob(node)
job.start()
node.callDecoration("setConvexHullJob", job)
elif Preferences.getInstance().getValue("physics/automatic_push_free"):
# Check for collisions between convex hulls # Check for collisions between convex hulls
for other_node in BreadthFirstIterator(root): for other_node in BreadthFirstIterator(root):
# Ignore root, ourselves and anything that is not a normal SceneNode. # Ignore root, ourselves and anything that is not a normal SceneNode.