From d8c20b9d6cb50987e3ade50638e16118536babef Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 28 Mar 2017 11:33:07 +0200 Subject: [PATCH] First version of multiply object seems to work quite well. CURA-3239 --- cura/Arrange.py | 121 ++++++++++++++++++++++------------------ cura/CuraApplication.py | 68 ++++++++++++++-------- 2 files changed, 112 insertions(+), 77 deletions(-) diff --git a/cura/Arrange.py b/cura/Arrange.py index 986f9110c1..d1f166ef87 100755 --- a/cura/Arrange.py +++ b/cura/Arrange.py @@ -12,47 +12,24 @@ class ShapeArray: def from_polygon(cls, vertices, scale = 1): # scale vertices = vertices * scale + # flip x, y + flip_vertices = np.zeros((vertices.shape)) + flip_vertices[:, 0] = vertices[:, 1] + flip_vertices[:, 1] = vertices[:, 0] + flip_vertices = flip_vertices[::-1] # offset - offset_y = int(np.amin(vertices[:, 0])) - offset_x = int(np.amin(vertices[:, 1])) - # normalize to 0 - vertices[:, 0] = np.add(vertices[:, 0], -offset_y) - vertices[:, 1] = np.add(vertices[:, 1], -offset_x) - shape = [int(np.amax(vertices[:, 0])), int(np.amax(vertices[:, 1]))] - arr = cls.array_from_polygon(shape, vertices) + offset_y = int(np.amin(flip_vertices[:, 0])) + offset_x = int(np.amin(flip_vertices[:, 1])) + # offset to 0 + flip_vertices[:, 0] = np.add(flip_vertices[:, 0], -offset_y) + flip_vertices[:, 1] = np.add(flip_vertices[:, 1], -offset_x) + shape = [int(np.amax(flip_vertices[:, 0])), int(np.amax(flip_vertices[:, 1]))] + #from UM.Logger import Logger + #Logger.log("d", " Vertices: %s" % str(flip_vertices)) + arr = cls.array_from_polygon(shape, flip_vertices) return cls(arr, offset_x, offset_y) - ## Return indices that mark one side of the line, used by array_from_polygon - # Uses the line defined by p1 and p2 to check array of - # input indices against interpolated value - - # Returns boolean array, with True inside and False outside of shape # Originally from: http://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array - @classmethod - def _check(cls, p1, p2, base_array): - """ - """ - if p1[0] == p2[0] and p1[1] == p2[1]: - return - idxs = np.indices(base_array.shape) # Create 3D array of indices - - p1 = p1.astype(float) - p2 = p2.astype(float) - - if p2[0] == p1[0]: - sign = np.sign(p2[1] - p1[1]) - return idxs[1] * sign - - if p2[1] == p1[1]: - sign = np.sign(p2[0] - p1[0]) - return idxs[1] * sign - - # Calculate max column idx for each row idx based on interpolated line between two points - - max_col_idx = (idxs[0] - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]) + p1[1] - sign = np.sign(p2[0] - p1[0]) - return idxs[1] * sign <= max_col_idx * sign - @classmethod def array_from_polygon(cls, shape, vertices): """ @@ -74,6 +51,35 @@ class ShapeArray: return base_array + ## Return indices that mark one side of the line, used by array_from_polygon + # Uses the line defined by p1 and p2 to check array of + # input indices against interpolated value + + # Returns boolean array, with True inside and False outside of shape + # Originally from: http://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array + @classmethod + def _check(cls, p1, p2, base_array): + if p1[0] == p2[0] and p1[1] == p2[1]: + return + idxs = np.indices(base_array.shape) # Create 3D array of indices + + p1 = p1.astype(float) + p2 = p2.astype(float) + + if p2[0] == p1[0]: + sign = np.sign(p2[1] - p1[1]) + return idxs[1] * sign + + if p2[1] == p1[1]: + sign = np.sign(p2[0] - p1[0]) + return idxs[1] * sign + + # Calculate max column idx for each row idx based on interpolated line between two points + + max_col_idx = (idxs[0] - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]) + p1[1] + sign = np.sign(p2[0] - p1[0]) + return idxs[1] * sign <= max_col_idx * sign + class Arrange: def __init__(self, x, y, offset_x, offset_y, scale=1): @@ -99,7 +105,10 @@ class Arrange: occupied_slice = self._occupied[ offset_y:offset_y + shape_arr.arr.shape[0], offset_x:offset_x + shape_arr.arr.shape[1]] - if np.any(occupied_slice[np.where(shape_arr.arr == 1)]): + try: + if np.any(occupied_slice[np.where(shape_arr.arr == 1)]): + return 999999 + except IndexError: # out of bounds if you try to place an object outside return 999999 prio_slice = self._priority[ offset_y:offset_y + shape_arr.arr.shape[0], @@ -122,33 +131,39 @@ class Arrange: return best_x, best_y, best_points ## Faster - def bestSpot(self, shape_arr): - min_y = max(-shape_arr.offset_y, 0) - self._offset_y - max_y = self.shape[0] - shape_arr.arr.shape[0] - self._offset_y - min_x = max(-shape_arr.offset_x, 0) - self._offset_x - max_x = self.shape[1] - shape_arr.arr.shape[1] - self._offset_x - - for prio in range(200): + def bestSpot(self, shape_arr, start_prio = 0): + for prio in range(start_prio, 300): tryout_idx = np.where(self._priority == prio) for idx in range(len(tryout_idx[0])): x = tryout_idx[0][idx] y = tryout_idx[1][idx] projected_x = x - self._offset_x projected_y = y - self._offset_y - if projected_x < min_x or projected_x > max_x or projected_y < min_y or projected_y > max_y: - continue + # array to "world" coordinates penalty_points = self.check_shape(projected_x, projected_y, shape_arr) if penalty_points != 999999: - return projected_x, projected_y, penalty_points - return None, None, None # No suitable location found :-( + return projected_x, projected_y, penalty_points, prio + return None, None, None, prio # No suitable location found :-( + ## Place the object def place(self, x, y, shape_arr): x = int(self._scale * x) y = int(self._scale * y) offset_x = x + self._offset_x + shape_arr.offset_x offset_y = y + self._offset_y + shape_arr.offset_y - occupied_slice = self._occupied[ - offset_y:offset_y + shape_arr.arr.shape[0], - offset_x:offset_x + shape_arr.arr.shape[1]] - occupied_slice[np.where(shape_arr.arr == 1)] = 1 + shape_y, shape_x = self._occupied.shape + + min_x = min(max(offset_x, 0), shape_x - 1) + min_y = min(max(offset_y, 0), shape_y - 1) + max_x = min(max(offset_x + shape_arr.arr.shape[1], 0), shape_x - 1) + max_y = min(max(offset_y + shape_arr.arr.shape[0], 0), shape_y - 1) + occupied_slice = self._occupied[min_y:max_y, min_x:max_x] + # we use a slice of shape because it can be out of bounds + occupied_slice[np.where(shape_arr.arr[ + min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 1 + + # Set priority to low (= high number), so it won't get picked at trying out. + prio_slice = self._priority[min_y:max_y, min_x:max_x] + prio_slice[np.where(shape_arr.arr[ + min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 999 diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 00fc38a5e1..2fdf8954f6 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -14,6 +14,7 @@ from UM.Math.Matrix import Matrix from UM.Resources import Resources from UM.Scene.ToolHandle import ToolHandle from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator +from UM.Math.Polygon import Polygon from UM.Mesh.ReadMeshJob import ReadMeshJob from UM.Logger import Logger from UM.Preferences import Preferences @@ -32,6 +33,7 @@ from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.SetTransformOperation import SetTransformOperation +from cura.Arrange import Arrange, ShapeArray from cura.SetParentOperation import SetParentOperation from cura.SliceableObjectDecorator import SliceableObjectDecorator from cura.BlockSlicingDecorator import BlockSlicingDecorator @@ -844,16 +846,16 @@ class CuraApplication(QtApplication): op.push() ## Create a number of copies of existing object. + # object_id + # count: number of copies + # min_offset: minimum offset to other objects. @pyqtSlot("quint64", int) - def multiplyObject(self, object_id, count): + def multiplyObject(self, object_id, count, min_offset = 5): node = self.getController().getScene().findObject(object_id) if not node and object_id != 0: # Workaround for tool handles overlapping the selected object node = Selection.getSelectedObject(0) - ### testing - - from cura.Arrange import Arrange, ShapeArray arranger = Arrange(215, 215, 107, 107) arranger.centerFirst() @@ -863,35 +865,56 @@ class CuraApplication(QtApplication): # Only count sliceable objects if node_.callDecoration("isSliceable"): Logger.log("d", " # Placing [%s]" % str(node_)) + vertices = node_.callDecoration("getConvexHull") points = copy.deepcopy(vertices._points) - #points[:,1] = -points[:,1] - #points = points[::-1] # reverse shape_arr = ShapeArray.from_polygon(points) - transform = node_._transformation - x = transform._data[0][3] - y = transform._data[2][3] - arranger.place(x, y, shape_arr) + arranger.place(0, 0, shape_arr) + Logger.log("d", "Current buildplate: \n%s" % str(arranger._occupied[::10, ::10])) + Logger.log("d", "Current scrores: \n%s" % str(arranger._priority[::20, ::20])) nodes = [] - for _ in range(count): + + # hacky way to undo transformation + transform = node._transformation + transform_x = transform._data[0][3] + transform_y = transform._data[2][3] + hull_verts = node.callDecoration("getConvexHull") + + offset_verts = hull_verts.getMinkowskiHull(Polygon.approximatedCircle(min_offset)) + offset_points = copy.deepcopy(offset_verts._points) # x, y + offset_points[:, 0] = numpy.add(offset_points[:, 0], -transform_x) + offset_points[:, 1] = numpy.add(offset_points[:, 1], -transform_y) + offset_shape_arr = ShapeArray.from_polygon(offset_points) + + hull_points = copy.deepcopy(hull_verts._points) + hull_points[:, 0] = numpy.add(hull_points[:, 0], -transform_x) + hull_points[:, 1] = numpy.add(hull_points[:, 1], -transform_y) + hull_shape_arr = ShapeArray.from_polygon(hull_points) # x, y + + start_prio = 0 + + for i in range(count): new_node = copy.deepcopy(node) - vertices = new_node.callDecoration("getConvexHull") - points = copy.deepcopy(vertices._points) - #points[:, 1] = -points[:, 1] - #points = points[::-1] # reverse - shape_arr = ShapeArray.from_polygon(points) - transformation = new_node._transformation + Logger.log("d", " # Finding spot for %s" % new_node) - x, y, penalty_points = arranger.bestSpot(shape_arr) + x, y, penalty_points, start_prio = arranger.bestSpot( + offset_shape_arr, start_prio = start_prio) + transformation = new_node._transformation if x is not None: # We could find a place transformation._data[0][3] = x transformation._data[2][3] = y - arranger.place(x, y, shape_arr) # take place before the next one + Logger.log("d", "Best place is: %s %s (points = %s)" % (x, y, penalty_points)) + arranger.place(x, y, hull_shape_arr) # take place before the next one + Logger.log("d", "New buildplate: \n%s" % str(arranger._occupied[::10, ::10])) + else: + Logger.log("d", "Could not find spot!") + transformation._data[0][3] = 200 + transformation._data[2][3] = -100 + i * 20 + # TODO: where to place it? + # new_node.setTransformation(transformation) nodes.append(new_node) - ### testing - if node: current_node = node @@ -902,9 +925,6 @@ class CuraApplication(QtApplication): op = GroupedOperation() for new_node in nodes: op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent())) - # for _ in range(count): - # new_node = copy.deepcopy(current_node) - # op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent())) op.push() ## Center object on platform.