From 9d6dd1580b94cc27d2937cd0ee94c550ad607a0f Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 9 Mar 2017 10:21:25 +0100 Subject: [PATCH 01/17] WIP Added first arranger functions. CURA-3239 --- cura/Arrange.py | 154 ++++++++++++++++++++++++++++++++++++++++ cura/CuraApplication.py | 48 ++++++++++++- 2 files changed, 200 insertions(+), 2 deletions(-) create mode 100755 cura/Arrange.py diff --git a/cura/Arrange.py b/cura/Arrange.py new file mode 100755 index 0000000000..986f9110c1 --- /dev/null +++ b/cura/Arrange.py @@ -0,0 +1,154 @@ +import numpy as np + +## Some polygon converted to an array +class ShapeArray: + def __init__(self, arr, offset_x, offset_y, scale = 1): + self.arr = arr + self.offset_x = offset_x + self.offset_y = offset_y + self.scale = scale + + @classmethod + def from_polygon(cls, vertices, scale = 1): + # scale + vertices = vertices * scale + # 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) + 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): + """ + Creates np.array with dimensions defined by shape + Fills polygon defined by vertices with ones, all other values zero + + Only works correctly for convex hull vertices + """ + base_array = np.zeros(shape, dtype=float) # Initialize your array of zeros + + fill = np.ones(base_array.shape) * True # Initialize boolean array defining shape fill + + # Create check array for each edge segment, combine into fill array + for k in range(vertices.shape[0]): + fill = np.all([fill, cls._check(vertices[k - 1], vertices[k], base_array)], axis=0) + + # Set all values inside polygon to one + base_array[fill] = 1 + + return base_array + + +class Arrange: + def __init__(self, x, y, offset_x, offset_y, scale=1): + self.shape = (y, x) + self._priority = np.zeros((x, y), dtype=np.int32) + self._occupied = np.zeros((x, y), dtype=np.int32) + self._scale = scale # convert input coordinates to arrange coordinates + self._offset_x = offset_x + self._offset_y = offset_y + + ## Fill priority, take offset as center. lower is better + def centerFirst(self): + self._priority = np.fromfunction( + lambda i, j: abs(self._offset_x-i)+abs(self._offset_y-j), self.shape) + + ## Return the amount of "penalty points" for polygon, which is the sum of priority + # 999999 if occupied + def check_shape(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]] + if np.any(occupied_slice[np.where(shape_arr.arr == 1)]): + return 999999 + prio_slice = self._priority[ + offset_y:offset_y + shape_arr.arr.shape[0], + offset_x:offset_x + shape_arr.arr.shape[1]] + return np.sum(prio_slice[np.where(shape_arr.arr == 1)]) + + ## Slower but better (it tries all possible locations) + def bestSpot2(self, shape_arr): + best_x, best_y, best_points = None, None, None + 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 y in range(min_y, max_y): + for x in range(min_x, max_x): + penalty_points = self.check_shape(x, y, shape_arr) + if best_points is None or penalty_points < best_points: + best_points = penalty_points + best_x, best_y = x, y + 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): + 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 :-( + + 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 diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 301dff3d20..9a443d7251 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -827,6 +827,48 @@ class CuraApplication(QtApplication): 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() + + # place all objects that are already there + root = self.getController().getScene().getRoot() + for node_ in DepthFirstIterator(root): + # 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) + + nodes = [] + for _ 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) + 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 + # new_node.setTransformation(transformation) + nodes.append(new_node) + ### testing + + if node: current_node = node # Find the topmost group @@ -834,9 +876,11 @@ class CuraApplication(QtApplication): current_node = current_node.getParent() op = GroupedOperation() - for _ in range(count): - new_node = copy.deepcopy(current_node) + 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. From d8c20b9d6cb50987e3ade50638e16118536babef Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 28 Mar 2017 11:33:07 +0200 Subject: [PATCH 02/17] 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. From bf08d30e7dc9a9fdd8f83db80d35cb1461bbb6f5 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 28 Mar 2017 14:27:22 +0200 Subject: [PATCH 03/17] Added first arrange function and smart placement after loading. CURA-3239 --- cura/CuraApplication.py | 152 ++++++++++++++++++++++++++++++-------- resources/qml/Actions.qml | 8 ++ resources/qml/Cura.qml | 3 + 3 files changed, 133 insertions(+), 30 deletions(-) mode change 100644 => 100755 resources/qml/Actions.qml diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 2fdf8954f6..f8e518c99e 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -34,6 +34,7 @@ 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.ConvexHullDecorator import ConvexHullDecorator from cura.SetParentOperation import SetParentOperation from cura.SliceableObjectDecorator import SliceableObjectDecorator from cura.BlockSlicingDecorator import BlockSlicingDecorator @@ -845,36 +846,31 @@ 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, 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) - - arranger = Arrange(215, 215, 107, 107) + ## Testing, prepare arranger for use + def _prepareArranger(self, fixed_nodes = None): + arranger = Arrange(215, 215, 107, 107) # TODO: fill in dimensions arranger.centerFirst() - # place all objects that are already there - root = self.getController().getScene().getRoot() - for node_ in DepthFirstIterator(root): - # Only count sliceable objects - if node_.callDecoration("isSliceable"): - Logger.log("d", " # Placing [%s]" % str(node_)) + if fixed_nodes is None: + fixed_nodes = [] + root = self.getController().getScene().getRoot() + for node_ in DepthFirstIterator(root): + # Only count sliceable objects + if node_.callDecoration("isSliceable"): + fixed_nodes.append(node_) + # place all objects fixed nodes + for fixed_node in fixed_nodes: + Logger.log("d", " # Placing [%s]" % str(fixed_node)) - vertices = node_.callDecoration("getConvexHull") - points = copy.deepcopy(vertices._points) - shape_arr = ShapeArray.from_polygon(points) - arranger.place(0, 0, shape_arr) + vertices = fixed_node.callDecoration("getConvexHull") + points = copy.deepcopy(vertices._points) + shape_arr = ShapeArray.from_polygon(points) + 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 = [] + return arranger + @classmethod + def _nodeAsShapeArr(cls, node, min_offset): # hacky way to undo transformation transform = node._transformation transform_x = transform._data[0][3] @@ -892,8 +888,13 @@ class CuraApplication(QtApplication): hull_points[:, 1] = numpy.add(hull_points[:, 1], -transform_y) hull_shape_arr = ShapeArray.from_polygon(hull_points) # x, y - start_prio = 0 + return offset_shape_arr, hull_shape_arr + @classmethod + def _findNodePlacements(cls, arranger, node, offset_shape_arr, hull_shape_arr, count = 1): + # offset_shape_arr, hull_shape_arr, arranger -> nodes, arranger + nodes = [] + start_prio = 0 for i in range(count): new_node = copy.deepcopy(node) @@ -911,12 +912,27 @@ class CuraApplication(QtApplication): 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) + return nodes - if node: + ## 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, 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) + + arranger = self._prepareArranger() + offset_shape_arr, hull_shape_arr = self._nodeAsShapeArr(node, min_offset = min_offset) + nodes = self._findNodePlacements(arranger, node, offset_shape_arr, hull_shape_arr, count = count) + + if nodes: current_node = node # Find the topmost group while current_node.getParent() and current_node.getParent().callDecoration("isGroup"): @@ -927,6 +943,7 @@ class CuraApplication(QtApplication): op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent())) op.push() + ## Center object on platform. @pyqtSlot("quint64") def centerObject(self, object_id): @@ -1043,6 +1060,60 @@ class CuraApplication(QtApplication): op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1))) op.push() + ## Testing: arrange selected objects or all objects + @pyqtSlot() + def arrange(self): + min_offset = 5 + + if Selection.getAllSelectedObjects(): + nodes = Selection.getAllSelectedObjects() + + # What nodes are on the build plate and are not being moved + fixed_nodes = [] + for node in DepthFirstIterator(self.getController().getScene().getRoot()): + if type(node) is not SceneNode: + continue + if not node.getMeshData() and not node.callDecoration("isGroup"): + continue # Node that doesnt have a mesh and is not a group. + if node.getParent() and node.getParent().callDecoration("isGroup"): + continue # Grouped nodes don't need resetting as their parent (the group) is resetted) + if not node.isSelectable(): + continue # i.e. node with layer data + fixed_nodes.append(node) + else: + nodes = [] + fixed_nodes = [] + for node in DepthFirstIterator(self.getController().getScene().getRoot()): + if type(node) is not SceneNode: + continue + if not node.getMeshData() and not node.callDecoration("isGroup"): + continue # Node that doesnt have a mesh and is not a group. + if node.getParent() and node.getParent().callDecoration("isGroup"): + continue # Grouped nodes don't need resetting as their parent (the group) is resetted) + if not node.isSelectable(): + continue # i.e. node with layer data + nodes.append(node) + + arranger = self._prepareArranger(fixed_nodes = fixed_nodes) + for node in nodes: + offset_shape_arr, hull_shape_arr = self._nodeAsShapeArr(node, min_offset = min_offset) + + x, y, penalty_points, start_prio = arranger.bestSpot( + offset_shape_arr) + if x is not None: # We could find a place + 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 + + node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator) + if node.getBoundingBox(): + center_y = node.getWorldPosition().y - node.getBoundingBox().bottom + else: + center_y = 0 + + op = GroupedOperation() + op.addOperation(SetTransformOperation(node, Vector(x, center_y, y))) + op.push() + ## Reload all mesh data on the screen from file. @pyqtSlot() def reloadAll(self): @@ -1302,6 +1373,11 @@ class CuraApplication(QtApplication): filename = job.getFileName() self._currently_loading_files.remove(filename) + arranger = self._prepareArranger() + min_offset = 5 + total_time = 0 + import time + for node in nodes: node.setSelectable(True) node.setName(os.path.basename(filename)) @@ -1322,11 +1398,27 @@ class CuraApplication(QtApplication): scene = self.getController().getScene() - op = AddSceneNodeOperation(node, scene.getRoot()) - op.push() + # If there is no convex hull for the node, start calculating it and continue. + if not node.getDecorator(ConvexHullDecorator): + node.addDecorator(ConvexHullDecorator()) + + # find node location + if 1: + start_time = time.time() + offset_shape_arr, hull_shape_arr = self._nodeAsShapeArr(node, min_offset = min_offset) + nodes = self._findNodePlacements(arranger, node, offset_shape_arr, hull_shape_arr, count = 1) + total_time += (time.time() - start_time) + else: + nodes = [node] + + for new_node in nodes: + op = AddSceneNodeOperation(new_node, scene.getRoot()) + op.push() scene.sceneChanged.emit(node) + Logger.log("d", "Placement of %s objects took %.1f seconds" % (len(nodes), total_time)) + def addNonSliceableExtension(self, extension): self._non_sliceable_extensions.append(extension) diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml old mode 100644 new mode 100755 index 94140ea876..5ead304909 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -31,6 +31,7 @@ Item property alias selectAll: selectAllAction; property alias deleteAll: deleteAllAction; property alias reloadAll: reloadAllAction; + property alias arrange: arrangeAction; property alias resetAllTranslation: resetAllTranslationAction; property alias resetAll: resetAllAction; @@ -266,6 +267,13 @@ Item onTriggered: Printer.reloadAll(); } + Action + { + id: arrangeAction; + text: catalog.i18nc("@action:inmenu menubar:edit","Arrange"); + onTriggered: Printer.arrange(); + } + Action { id: resetAllTranslationAction; diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 3e5ecf03fe..f64fc21913 100755 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -133,6 +133,7 @@ UM.MainWindow MenuItem { action: Cura.Actions.selectAll; } MenuItem { action: Cura.Actions.deleteSelection; } MenuItem { action: Cura.Actions.deleteAll; } + MenuItem { action: Cura.Actions.arrange; } MenuItem { action: Cura.Actions.resetAllTranslation; } MenuItem { action: Cura.Actions.resetAll; } MenuSeparator { } @@ -638,6 +639,7 @@ UM.MainWindow MenuItem { action: Cura.Actions.selectAll; } MenuItem { action: Cura.Actions.deleteAll; } MenuItem { action: Cura.Actions.reloadAll; } + MenuItem { action: Cura.Actions.arrange; } MenuItem { action: Cura.Actions.resetAllTranslation; } MenuItem { action: Cura.Actions.resetAll; } MenuSeparator { } @@ -698,6 +700,7 @@ UM.MainWindow MenuItem { action: Cura.Actions.selectAll; } MenuItem { action: Cura.Actions.deleteAll; } MenuItem { action: Cura.Actions.reloadAll; } + MenuItem { action: Cura.Actions.arrange; } MenuItem { action: Cura.Actions.resetAllTranslation; } MenuItem { action: Cura.Actions.resetAll; } MenuSeparator { } From f357dea086682e8b249d03105e386ab93c63e31c Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 29 Mar 2017 09:32:29 +0200 Subject: [PATCH 04/17] Tuned arranger a bit, good enough for proof of concept. CURA-3239 --- cura/Arrange.py | 38 +++++++++++++++----------------------- cura/CuraApplication.py | 30 ++++++++++++------------------ 2 files changed, 27 insertions(+), 41 deletions(-) diff --git a/cura/Arrange.py b/cura/Arrange.py index d1f166ef87..db6a7c781e 100755 --- a/cura/Arrange.py +++ b/cura/Arrange.py @@ -12,20 +12,17 @@ class ShapeArray: def from_polygon(cls, vertices, scale = 1): # scale vertices = vertices * scale - # flip x, y + # flip y, x -> 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, we want that all coordinates have positive values 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) @@ -85,6 +82,7 @@ class Arrange: def __init__(self, x, y, offset_x, offset_y, scale=1): self.shape = (y, x) self._priority = np.zeros((x, y), dtype=np.int32) + self._priority_unique_values = [] self._occupied = np.zeros((x, y), dtype=np.int32) self._scale = scale # convert input coordinates to arrange coordinates self._offset_x = offset_x @@ -92,8 +90,12 @@ class Arrange: ## Fill priority, take offset as center. lower is better def centerFirst(self): + #self._priority = np.fromfunction( + # lambda i, j: abs(self._offset_x-i)+abs(self._offset_y-j), self.shape) self._priority = np.fromfunction( - lambda i, j: abs(self._offset_x-i)+abs(self._offset_y-j), self.shape) + lambda i, j: abs(self._offset_x-i)**2+abs(self._offset_y-j)**2, self.shape, dtype=np.int32) + self._priority_unique_values = np.unique(self._priority) + self._priority_unique_values.sort() ## Return the amount of "penalty points" for polygon, which is the sum of priority # 999999 if occupied @@ -115,24 +117,14 @@ class Arrange: offset_x:offset_x + shape_arr.arr.shape[1]] return np.sum(prio_slice[np.where(shape_arr.arr == 1)]) - ## Slower but better (it tries all possible locations) - def bestSpot2(self, shape_arr): - best_x, best_y, best_points = None, None, None - 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 y in range(min_y, max_y): - for x in range(min_x, max_x): - penalty_points = self.check_shape(x, y, shape_arr) - if best_points is None or penalty_points < best_points: - best_points = penalty_points - best_x, best_y = x, y - return best_x, best_y, best_points - - ## Faster + ## Find "best" spot def bestSpot(self, shape_arr, start_prio = 0): - for prio in range(start_prio, 300): + start_idx_list = np.where(self._priority_unique_values == start_prio) + if start_idx_list: + start_idx = start_idx_list[0] + else: + start_idx = 0 + for prio in self._priority_unique_values[start_idx:]: tryout_idx = np.where(self._priority == prio) for idx in range(len(tryout_idx[0])): x = tryout_idx[0][idx] diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index f8e518c99e..2678ea2fa0 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -848,7 +848,9 @@ class CuraApplication(QtApplication): ## Testing, prepare arranger for use def _prepareArranger(self, fixed_nodes = None): - arranger = Arrange(215, 215, 107, 107) # TODO: fill in dimensions + #arranger = Arrange(215, 215, 107, 107) # TODO: fill in dimensions + scale = 0.5 + arranger = Arrange(250, 250, 125, 125, scale = scale) # TODO: fill in dimensions arranger.centerFirst() if fixed_nodes is None: @@ -864,13 +866,14 @@ class CuraApplication(QtApplication): vertices = fixed_node.callDecoration("getConvexHull") points = copy.deepcopy(vertices._points) - shape_arr = ShapeArray.from_polygon(points) + shape_arr = ShapeArray.from_polygon(points, scale=scale) arranger.place(0, 0, shape_arr) Logger.log("d", "Current buildplate: \n%s" % str(arranger._occupied[::10, ::10])) return arranger @classmethod def _nodeAsShapeArr(cls, node, min_offset): + scale = 0.5 # hacky way to undo transformation transform = node._transformation transform_x = transform._data[0][3] @@ -881,12 +884,12 @@ class CuraApplication(QtApplication): 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) + offset_shape_arr = ShapeArray.from_polygon(offset_points, scale = scale) 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 + hull_shape_arr = ShapeArray.from_polygon(hull_points, scale = scale) # x, y return offset_shape_arr, hull_shape_arr @@ -913,7 +916,6 @@ class CuraApplication(QtApplication): transformation._data[0][3] = 200 transformation._data[2][3] = -100 + i * 20 - # new_node.setTransformation(transformation) nodes.append(new_node) return nodes @@ -922,7 +924,7 @@ class CuraApplication(QtApplication): # count: number of copies # min_offset: minimum offset to other objects. @pyqtSlot("quint64", int) - def multiplyObject(self, object_id, count, min_offset = 5): + def multiplyObject(self, object_id, count, min_offset = 8): node = self.getController().getScene().findObject(object_id) if not node and object_id != 0: # Workaround for tool handles overlapping the selected object @@ -1063,7 +1065,7 @@ class CuraApplication(QtApplication): ## Testing: arrange selected objects or all objects @pyqtSlot() def arrange(self): - min_offset = 5 + min_offset = 8 if Selection.getAllSelectedObjects(): nodes = Selection.getAllSelectedObjects() @@ -1374,9 +1376,8 @@ class CuraApplication(QtApplication): self._currently_loading_files.remove(filename) arranger = self._prepareArranger() - min_offset = 5 + min_offset = 8 total_time = 0 - import time for node in nodes: node.setSelectable(True) @@ -1403,13 +1404,8 @@ class CuraApplication(QtApplication): node.addDecorator(ConvexHullDecorator()) # find node location - if 1: - start_time = time.time() - offset_shape_arr, hull_shape_arr = self._nodeAsShapeArr(node, min_offset = min_offset) - nodes = self._findNodePlacements(arranger, node, offset_shape_arr, hull_shape_arr, count = 1) - total_time += (time.time() - start_time) - else: - nodes = [node] + offset_shape_arr, hull_shape_arr = self._nodeAsShapeArr(node, min_offset = min_offset) + nodes = self._findNodePlacements(arranger, node, offset_shape_arr, hull_shape_arr, count = 1) for new_node in nodes: op = AddSceneNodeOperation(new_node, scene.getRoot()) @@ -1417,8 +1413,6 @@ class CuraApplication(QtApplication): scene.sceneChanged.emit(node) - Logger.log("d", "Placement of %s objects took %.1f seconds" % (len(nodes), total_time)) - def addNonSliceableExtension(self, extension): self._non_sliceable_extensions.append(extension) From 099752125b5774054fdc7d1b91331fb676a811e4 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 29 Mar 2017 10:00:21 +0200 Subject: [PATCH 05/17] Arrange all now places biggest objects first. CURA-3239 --- cura/Arrange.py | 10 ++++++++-- cura/CuraApplication.py | 9 ++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/cura/Arrange.py b/cura/Arrange.py index db6a7c781e..dde162962e 100755 --- a/cura/Arrange.py +++ b/cura/Arrange.py @@ -90,10 +90,16 @@ class Arrange: ## Fill priority, take offset as center. lower is better def centerFirst(self): + # Distance x + distance y #self._priority = np.fromfunction( - # lambda i, j: abs(self._offset_x-i)+abs(self._offset_y-j), self.shape) + # lambda i, j: abs(self._offset_x-i)+abs(self._offset_y-j), self.shape, dtype=np.int32) + # Square distance + # self._priority = np.fromfunction( + # lambda i, j: abs(self._offset_x-i)**2+abs(self._offset_y-j)**2, self.shape, dtype=np.int32) self._priority = np.fromfunction( - lambda i, j: abs(self._offset_x-i)**2+abs(self._offset_y-j)**2, self.shape, dtype=np.int32) + lambda i, j: abs(self._offset_x-i)**3+abs(self._offset_y-j)**3, self.shape, dtype=np.int32) + # self._priority = np.fromfunction( + # lambda i, j: max(abs(self._offset_x-i), abs(self._offset_y-j)), self.shape, dtype=np.int32) self._priority_unique_values = np.unique(self._priority) self._priority_unique_values.sort() diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 2678ea2fa0..e5b6d48617 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -850,7 +850,7 @@ class CuraApplication(QtApplication): def _prepareArranger(self, fixed_nodes = None): #arranger = Arrange(215, 215, 107, 107) # TODO: fill in dimensions scale = 0.5 - arranger = Arrange(250, 250, 125, 125, scale = scale) # TODO: fill in dimensions + arranger = Arrange(220, 220, 110, 110, scale = scale) # TODO: fill in dimensions arranger.centerFirst() if fixed_nodes is None: @@ -1097,9 +1097,16 @@ class CuraApplication(QtApplication): nodes.append(node) arranger = self._prepareArranger(fixed_nodes = fixed_nodes) + nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr) for node in nodes: offset_shape_arr, hull_shape_arr = self._nodeAsShapeArr(node, min_offset = min_offset) + nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr)) + nodes_arr.sort(key = lambda item: item[0]) + nodes_arr.reverse() + + for size, node, offset_shape_arr, hull_shape_arr in nodes_arr: + Logger.log("d", "Placing object sized: %s" % size) x, y, penalty_points, start_prio = arranger.bestSpot( offset_shape_arr) if x is not None: # We could find a place From 9db816b0fc728ee28be417a16bb4319e2366a0a1 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 30 Mar 2017 17:26:56 +0200 Subject: [PATCH 06/17] Funed arranger for better performance. CURA-3239 --- cura/Arrange.py | 4 ++-- cura/CuraApplication.py | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/cura/Arrange.py b/cura/Arrange.py index dde162962e..93ac1eece1 100755 --- a/cura/Arrange.py +++ b/cura/Arrange.py @@ -124,13 +124,13 @@ class Arrange: return np.sum(prio_slice[np.where(shape_arr.arr == 1)]) ## Find "best" spot - def bestSpot(self, shape_arr, start_prio = 0): + def bestSpot(self, shape_arr, start_prio = 0, step = 1): start_idx_list = np.where(self._priority_unique_values == start_prio) if start_idx_list: start_idx = start_idx_list[0] else: start_idx = 0 - for prio in self._priority_unique_values[start_idx:]: + for prio in self._priority_unique_values[start_idx::step]: tryout_idx = np.where(self._priority == prio) for idx in range(len(tryout_idx[0])): x = tryout_idx[0][idx] diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index e5b6d48617..5f87d9fd98 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -894,7 +894,7 @@ class CuraApplication(QtApplication): return offset_shape_arr, hull_shape_arr @classmethod - def _findNodePlacements(cls, arranger, node, offset_shape_arr, hull_shape_arr, count = 1): + def _findNodePlacements(cls, arranger, node, offset_shape_arr, hull_shape_arr, count = 1, step = 1): # offset_shape_arr, hull_shape_arr, arranger -> nodes, arranger nodes = [] start_prio = 0 @@ -903,7 +903,7 @@ class CuraApplication(QtApplication): Logger.log("d", " # Finding spot for %s" % new_node) x, y, penalty_points, start_prio = arranger.bestSpot( - offset_shape_arr, start_prio = start_prio) + offset_shape_arr, start_prio = start_prio, step = step) transformation = new_node._transformation if x is not None: # We could find a place transformation._data[0][3] = x @@ -1105,12 +1105,13 @@ class CuraApplication(QtApplication): nodes_arr.sort(key = lambda item: item[0]) nodes_arr.reverse() + start_prio = 0 for size, node, offset_shape_arr, hull_shape_arr in nodes_arr: - Logger.log("d", "Placing object sized: %s" % size) + # we assume that when a location does not fit, it will also not fit for the next + # object (while what can be untrue). That saves a lot of time. x, y, penalty_points, start_prio = arranger.bestSpot( - offset_shape_arr) + offset_shape_arr, start_prio = start_prio) if x is not None: # We could find a place - 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 node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator) @@ -1384,7 +1385,6 @@ class CuraApplication(QtApplication): arranger = self._prepareArranger() min_offset = 8 - total_time = 0 for node in nodes: node.setSelectable(True) @@ -1412,7 +1412,8 @@ class CuraApplication(QtApplication): # find node location offset_shape_arr, hull_shape_arr = self._nodeAsShapeArr(node, min_offset = min_offset) - nodes = self._findNodePlacements(arranger, node, offset_shape_arr, hull_shape_arr, count = 1) + # step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher + nodes = self._findNodePlacements(arranger, node, offset_shape_arr, hull_shape_arr, count = 1, step = 10) for new_node in nodes: op = AddSceneNodeOperation(new_node, scene.getRoot()) From abb5d1e76eaa85c770336c9568fe6a258da7ad0c Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 3 Apr 2017 10:40:04 +0200 Subject: [PATCH 07/17] Arranger: moved functions, split Arrange into Arrange All and Arrange Selection. CURA-3239 --- cura/Arrange.py | 160 ++++++++++++++++++++++++++++---------- cura/CuraApplication.py | 158 +++++++++++-------------------------- resources/qml/Actions.qml | 16 +++- resources/qml/Cura.qml | 6 +- 4 files changed, 181 insertions(+), 159 deletions(-) diff --git a/cura/Arrange.py b/cura/Arrange.py index 93ac1eece1..303d0c6804 100755 --- a/cura/Arrange.py +++ b/cura/Arrange.py @@ -1,6 +1,9 @@ -import numpy as np +import numpy +from UM.Math.Polygon import Polygon -## Some polygon converted to an array + +## Polygon representation as an array +# class ShapeArray: def __init__(self, arr, offset_x, offset_y, scale = 1): self.arr = arr @@ -9,39 +12,59 @@ class ShapeArray: self.scale = scale @classmethod - def from_polygon(cls, vertices, scale = 1): + def fromPolygon(cls, vertices, scale = 1): # scale vertices = vertices * scale # flip y, x -> x, y - flip_vertices = np.zeros((vertices.shape)) + flip_vertices = numpy.zeros((vertices.shape)) flip_vertices[:, 0] = vertices[:, 1] flip_vertices[:, 1] = vertices[:, 0] flip_vertices = flip_vertices[::-1] # offset, we want that all coordinates have positive values - offset_y = int(np.amin(flip_vertices[:, 0])) - offset_x = int(np.amin(flip_vertices[:, 1])) - 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]))] - arr = cls.array_from_polygon(shape, flip_vertices) + offset_y = int(numpy.amin(flip_vertices[:, 0])) + offset_x = int(numpy.amin(flip_vertices[:, 1])) + flip_vertices[:, 0] = numpy.add(flip_vertices[:, 0], -offset_y) + flip_vertices[:, 1] = numpy.add(flip_vertices[:, 1], -offset_x) + shape = [int(numpy.amax(flip_vertices[:, 0])), int(numpy.amax(flip_vertices[:, 1]))] + arr = cls.arrayFromPolygon(shape, flip_vertices) return cls(arr, offset_x, offset_y) + ## Return an offset and hull ShapeArray from a scenenode. + @classmethod + def fromNode(cls, node, min_offset, scale = 0.5): + # 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.fromPolygon(offset_points, scale = scale) + + 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.fromPolygon(hull_points, scale = scale) # x, y + + return offset_shape_arr, hull_shape_arr + + + ## Create np.array with dimensions defined by shape + # Fills polygon defined by vertices with ones, all other values zero + # Only works correctly for convex hull vertices # Originally from: http://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array @classmethod - def array_from_polygon(cls, shape, vertices): - """ - Creates np.array with dimensions defined by shape - Fills polygon defined by vertices with ones, all other values zero + def arrayFromPolygon(cls, shape, vertices): + base_array = numpy.zeros(shape, dtype=float) # Initialize your array of zeros - Only works correctly for convex hull vertices - """ - base_array = np.zeros(shape, dtype=float) # Initialize your array of zeros - - fill = np.ones(base_array.shape) * True # Initialize boolean array defining shape fill + fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill # Create check array for each edge segment, combine into fill array for k in range(vertices.shape[0]): - fill = np.all([fill, cls._check(vertices[k - 1], vertices[k], base_array)], axis=0) + fill = numpy.all([fill, cls._check(vertices[k - 1], vertices[k], base_array)], axis=0) # Set all values inside polygon to one base_array[fill] = 1 @@ -51,43 +74,102 @@ class ShapeArray: ## 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 + idxs = numpy.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]) + sign = numpy.sign(p2[1] - p1[1]) return idxs[1] * sign if p2[1] == p1[1]: - sign = np.sign(p2[0] - p1[0]) + sign = numpy.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]) + sign = numpy.sign(p2[0] - p1[0]) return idxs[1] * sign <= max_col_idx * sign +from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator +from UM.Logger import Logger +import copy + + class Arrange: def __init__(self, x, y, offset_x, offset_y, scale=1): self.shape = (y, x) - self._priority = np.zeros((x, y), dtype=np.int32) + self._priority = numpy.zeros((x, y), dtype=numpy.int32) self._priority_unique_values = [] - self._occupied = np.zeros((x, y), dtype=np.int32) + self._occupied = numpy.zeros((x, y), dtype=numpy.int32) self._scale = scale # convert input coordinates to arrange coordinates self._offset_x = offset_x self._offset_y = offset_y + ## Helper to create an Arranger instance + # + # Either fill in scene_root and create will find all sliceable nodes by itself, + # or use fixed_nodes to provide the nodes yourself. + # \param scene_root + # \param fixed_nodes + @classmethod + def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5): + arranger = Arrange(220, 220, 110, 110, scale = scale) + arranger.centerFirst() + + if fixed_nodes is None: + fixed_nodes = [] + for node_ in DepthFirstIterator(scene_root): + # Only count sliceable objects + if node_.callDecoration("isSliceable"): + fixed_nodes.append(node_) + # place all objects fixed nodes + for fixed_node in fixed_nodes: + Logger.log("d", " # Placing [%s]" % str(fixed_node)) + + vertices = fixed_node.callDecoration("getConvexHull") + points = copy.deepcopy(vertices._points) + shape_arr = ShapeArray.fromPolygon(points, scale = scale) + arranger.place(0, 0, shape_arr) + Logger.log("d", "Current buildplate: \n%s" % str(arranger._occupied[::10, ::10])) + return arranger + + ## Find placement for a node and place it + # + def findNodePlacements(self, node, offset_shape_arr, hull_shape_arr, count = 1, step = 1): + # offset_shape_arr, hull_shape_arr, arranger -> nodes, arranger + nodes = [] + start_prio = 0 + for i in range(count): + new_node = copy.deepcopy(node) + + Logger.log("d", " # Finding spot for %s" % new_node) + x, y, penalty_points, start_prio = self.bestSpot( + offset_shape_arr, start_prio = start_prio, step = step) + transformation = new_node._transformation + if x is not None: # We could find a place + transformation._data[0][3] = x + transformation._data[2][3] = y + Logger.log("d", "Best place is: %s %s (points = %s)" % (x, y, penalty_points)) + self.place(x, y, hull_shape_arr) # take place before the next one + Logger.log("d", "New buildplate: \n%s" % str(self._occupied[::10, ::10])) + else: + Logger.log("d", "Could not find spot!") + transformation._data[0][3] = 200 + transformation._data[2][3] = -100 + i * 20 + + nodes.append(new_node) + return nodes + ## Fill priority, take offset as center. lower is better def centerFirst(self): # Distance x + distance y @@ -96,16 +178,16 @@ class Arrange: # Square distance # self._priority = np.fromfunction( # lambda i, j: abs(self._offset_x-i)**2+abs(self._offset_y-j)**2, self.shape, dtype=np.int32) - self._priority = np.fromfunction( - lambda i, j: abs(self._offset_x-i)**3+abs(self._offset_y-j)**3, self.shape, dtype=np.int32) + self._priority = numpy.fromfunction( + lambda i, j: abs(self._offset_x-i)**3+abs(self._offset_y-j)**3, self.shape, dtype=numpy.int32) # self._priority = np.fromfunction( # lambda i, j: max(abs(self._offset_x-i), abs(self._offset_y-j)), self.shape, dtype=np.int32) - self._priority_unique_values = np.unique(self._priority) + self._priority_unique_values = numpy.unique(self._priority) self._priority_unique_values.sort() ## Return the amount of "penalty points" for polygon, which is the sum of priority # 999999 if occupied - def check_shape(self, x, y, shape_arr): + def checkShape(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 @@ -114,24 +196,24 @@ class Arrange: offset_y:offset_y + shape_arr.arr.shape[0], offset_x:offset_x + shape_arr.arr.shape[1]] try: - if np.any(occupied_slice[np.where(shape_arr.arr == 1)]): + if numpy.any(occupied_slice[numpy.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], offset_x:offset_x + shape_arr.arr.shape[1]] - return np.sum(prio_slice[np.where(shape_arr.arr == 1)]) + return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)]) - ## Find "best" spot + ## Find "best" spot for ShapeArray def bestSpot(self, shape_arr, start_prio = 0, step = 1): - start_idx_list = np.where(self._priority_unique_values == start_prio) + start_idx_list = numpy.where(self._priority_unique_values == start_prio) if start_idx_list: start_idx = start_idx_list[0] else: start_idx = 0 for prio in self._priority_unique_values[start_idx::step]: - tryout_idx = np.where(self._priority == prio) + tryout_idx = numpy.where(self._priority == prio) for idx in range(len(tryout_idx[0])): x = tryout_idx[0][idx] y = tryout_idx[1][idx] @@ -139,7 +221,7 @@ class Arrange: projected_y = y - self._offset_y # array to "world" coordinates - penalty_points = self.check_shape(projected_x, projected_y, shape_arr) + penalty_points = self.checkShape(projected_x, projected_y, shape_arr) if penalty_points != 999999: return projected_x, projected_y, penalty_points, prio return None, None, None, prio # No suitable location found :-( @@ -158,10 +240,10 @@ class Arrange: 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[ + occupied_slice[numpy.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[ + prio_slice[numpy.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 5f87d9fd98..8cf20df0f1 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -14,7 +14,6 @@ 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 @@ -846,79 +845,6 @@ class CuraApplication(QtApplication): op.push() - ## Testing, prepare arranger for use - def _prepareArranger(self, fixed_nodes = None): - #arranger = Arrange(215, 215, 107, 107) # TODO: fill in dimensions - scale = 0.5 - arranger = Arrange(220, 220, 110, 110, scale = scale) # TODO: fill in dimensions - arranger.centerFirst() - - if fixed_nodes is None: - fixed_nodes = [] - root = self.getController().getScene().getRoot() - for node_ in DepthFirstIterator(root): - # Only count sliceable objects - if node_.callDecoration("isSliceable"): - fixed_nodes.append(node_) - # place all objects fixed nodes - for fixed_node in fixed_nodes: - Logger.log("d", " # Placing [%s]" % str(fixed_node)) - - vertices = fixed_node.callDecoration("getConvexHull") - points = copy.deepcopy(vertices._points) - shape_arr = ShapeArray.from_polygon(points, scale=scale) - arranger.place(0, 0, shape_arr) - Logger.log("d", "Current buildplate: \n%s" % str(arranger._occupied[::10, ::10])) - return arranger - - @classmethod - def _nodeAsShapeArr(cls, node, min_offset): - scale = 0.5 - # 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, scale = scale) - - 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, scale = scale) # x, y - - return offset_shape_arr, hull_shape_arr - - @classmethod - def _findNodePlacements(cls, arranger, node, offset_shape_arr, hull_shape_arr, count = 1, step = 1): - # offset_shape_arr, hull_shape_arr, arranger -> nodes, arranger - nodes = [] - start_prio = 0 - for i in range(count): - new_node = copy.deepcopy(node) - - Logger.log("d", " # Finding spot for %s" % new_node) - x, y, penalty_points, start_prio = arranger.bestSpot( - offset_shape_arr, start_prio = start_prio, step = step) - transformation = new_node._transformation - if x is not None: # We could find a place - transformation._data[0][3] = x - transformation._data[2][3] = y - 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 - - nodes.append(new_node) - return nodes - ## Create a number of copies of existing object. # object_id # count: number of copies @@ -930,9 +856,10 @@ class CuraApplication(QtApplication): if not node and object_id != 0: # Workaround for tool handles overlapping the selected object node = Selection.getSelectedObject(0) - arranger = self._prepareArranger() - offset_shape_arr, hull_shape_arr = self._nodeAsShapeArr(node, min_offset = min_offset) - nodes = self._findNodePlacements(arranger, node, offset_shape_arr, hull_shape_arr, count = count) + root = self.getController().getScene().getRoot() + arranger = Arrange.create(scene_root = root) + offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset) + nodes = arranger.findNodePlacements(node, offset_shape_arr, hull_shape_arr, count = count) if nodes: current_node = node @@ -945,7 +872,6 @@ class CuraApplication(QtApplication): op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent())) op.push() - ## Center object on platform. @pyqtSlot("quint64") def centerObject(self, object_id): @@ -1064,42 +990,47 @@ class CuraApplication(QtApplication): ## Testing: arrange selected objects or all objects @pyqtSlot() - def arrange(self): + def arrangeSelection(self): + nodes = Selection.getAllSelectedObjects() + + # What nodes are on the build plate and are not being moved + fixed_nodes = [] + for node in DepthFirstIterator(self.getController().getScene().getRoot()): + if type(node) is not SceneNode: + continue + if not node.getMeshData() and not node.callDecoration("isGroup"): + continue # Node that doesnt have a mesh and is not a group. + if node.getParent() and node.getParent().callDecoration("isGroup"): + continue # Grouped nodes don't need resetting as their parent (the group) is resetted) + if not node.isSelectable(): + continue # i.e. node with layer data + fixed_nodes.append(node) + self.arrange(nodes, fixed_nodes) + + @pyqtSlot() + def arrangeAll(self): + nodes = [] + fixed_nodes = [] + for node in DepthFirstIterator(self.getController().getScene().getRoot()): + if type(node) is not SceneNode: + continue + if not node.getMeshData() and not node.callDecoration("isGroup"): + continue # Node that doesnt have a mesh and is not a group. + if node.getParent() and node.getParent().callDecoration("isGroup"): + continue # Grouped nodes don't need resetting as their parent (the group) is resetted) + if not node.isSelectable(): + continue # i.e. node with layer data + nodes.append(node) + self.arrange(nodes, fixed_nodes) + + ## Arrange the nodes, given fixed nodes + def arrange(self, nodes, fixed_nodes): min_offset = 8 - if Selection.getAllSelectedObjects(): - nodes = Selection.getAllSelectedObjects() - - # What nodes are on the build plate and are not being moved - fixed_nodes = [] - for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode: - continue - if not node.getMeshData() and not node.callDecoration("isGroup"): - continue # Node that doesnt have a mesh and is not a group. - if node.getParent() and node.getParent().callDecoration("isGroup"): - continue # Grouped nodes don't need resetting as their parent (the group) is resetted) - if not node.isSelectable(): - continue # i.e. node with layer data - fixed_nodes.append(node) - else: - nodes = [] - fixed_nodes = [] - for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode: - continue - if not node.getMeshData() and not node.callDecoration("isGroup"): - continue # Node that doesnt have a mesh and is not a group. - if node.getParent() and node.getParent().callDecoration("isGroup"): - continue # Grouped nodes don't need resetting as their parent (the group) is resetted) - if not node.isSelectable(): - continue # i.e. node with layer data - nodes.append(node) - - arranger = self._prepareArranger(fixed_nodes = fixed_nodes) + arranger = Arrange.create(fixed_nodes = fixed_nodes) nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr) for node in nodes: - offset_shape_arr, hull_shape_arr = self._nodeAsShapeArr(node, min_offset = min_offset) + offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset) nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr)) nodes_arr.sort(key = lambda item: item[0]) @@ -1383,7 +1314,8 @@ class CuraApplication(QtApplication): filename = job.getFileName() self._currently_loading_files.remove(filename) - arranger = self._prepareArranger() + root = self.getController().getScene().getRoot() + arranger = Arrange.create(scene_root = root) min_offset = 8 for node in nodes: @@ -1411,9 +1343,9 @@ class CuraApplication(QtApplication): node.addDecorator(ConvexHullDecorator()) # find node location - offset_shape_arr, hull_shape_arr = self._nodeAsShapeArr(node, min_offset = min_offset) + offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset) # step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher - nodes = self._findNodePlacements(arranger, node, offset_shape_arr, hull_shape_arr, count = 1, step = 10) + nodes = arranger.findNodePlacements(node, offset_shape_arr, hull_shape_arr, count = 1, step = 10) for new_node in nodes: op = AddSceneNodeOperation(new_node, scene.getRoot()) diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index 5ead304909..b5e92f3dd4 100755 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -31,7 +31,8 @@ Item property alias selectAll: selectAllAction; property alias deleteAll: deleteAllAction; property alias reloadAll: reloadAllAction; - property alias arrange: arrangeAction; + property alias arrangeAll: arrangeAllAction; + property alias arrangeSelection: arrangeSelectionAction; property alias resetAllTranslation: resetAllTranslationAction; property alias resetAll: resetAllAction; @@ -269,9 +270,16 @@ Item Action { - id: arrangeAction; - text: catalog.i18nc("@action:inmenu menubar:edit","Arrange"); - onTriggered: Printer.arrange(); + id: arrangeAllAction; + text: catalog.i18nc("@action:inmenu menubar:edit","Arrange All"); + onTriggered: Printer.arrangeAll(); + } + + Action + { + id: arrangeSelectionAction; + text: catalog.i18nc("@action:inmenu menubar:edit","Arrange Selection"); + onTriggered: Printer.arrangeSelection(); } Action diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index f64fc21913..e74f0b9e2e 100755 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -133,7 +133,7 @@ UM.MainWindow MenuItem { action: Cura.Actions.selectAll; } MenuItem { action: Cura.Actions.deleteSelection; } MenuItem { action: Cura.Actions.deleteAll; } - MenuItem { action: Cura.Actions.arrange; } + MenuItem { action: Cura.Actions.arrangeAll; } MenuItem { action: Cura.Actions.resetAllTranslation; } MenuItem { action: Cura.Actions.resetAll; } MenuSeparator { } @@ -639,7 +639,7 @@ UM.MainWindow MenuItem { action: Cura.Actions.selectAll; } MenuItem { action: Cura.Actions.deleteAll; } MenuItem { action: Cura.Actions.reloadAll; } - MenuItem { action: Cura.Actions.arrange; } + MenuItem { action: Cura.Actions.arrangeSelection; } MenuItem { action: Cura.Actions.resetAllTranslation; } MenuItem { action: Cura.Actions.resetAll; } MenuSeparator { } @@ -700,7 +700,7 @@ UM.MainWindow MenuItem { action: Cura.Actions.selectAll; } MenuItem { action: Cura.Actions.deleteAll; } MenuItem { action: Cura.Actions.reloadAll; } - MenuItem { action: Cura.Actions.arrange; } + MenuItem { action: Cura.Actions.arrangeAll; } MenuItem { action: Cura.Actions.resetAllTranslation; } MenuItem { action: Cura.Actions.resetAll; } MenuSeparator { } From bd874a62aca5da089a4a8cd59629a0351157f7e2 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 3 Apr 2017 10:44:53 +0200 Subject: [PATCH 08/17] Arranger: moved functions, split Arrange into Arrange All and Arrange Selection. CURA-3239 --- cura/CuraApplication.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 8cf20df0f1..e34d02a704 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -988,7 +988,24 @@ class CuraApplication(QtApplication): op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1))) op.push() - ## Testing: arrange selected objects or all objects + ## Arrange all objects. + @pyqtSlot() + def arrangeAll(self): + nodes = [] + fixed_nodes = [] + for node in DepthFirstIterator(self.getController().getScene().getRoot()): + if type(node) is not SceneNode: + continue + if not node.getMeshData() and not node.callDecoration("isGroup"): + continue # Node that doesnt have a mesh and is not a group. + if node.getParent() and node.getParent().callDecoration("isGroup"): + continue # Grouped nodes don't need resetting as their parent (the group) is resetted) + if not node.isSelectable(): + continue # i.e. node with layer data + nodes.append(node) + self.arrange(nodes, fixed_nodes) + + ## Arrange Selection @pyqtSlot() def arrangeSelection(self): nodes = Selection.getAllSelectedObjects() @@ -1007,22 +1024,6 @@ class CuraApplication(QtApplication): fixed_nodes.append(node) self.arrange(nodes, fixed_nodes) - @pyqtSlot() - def arrangeAll(self): - nodes = [] - fixed_nodes = [] - for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode: - continue - if not node.getMeshData() and not node.callDecoration("isGroup"): - continue # Node that doesnt have a mesh and is not a group. - if node.getParent() and node.getParent().callDecoration("isGroup"): - continue # Grouped nodes don't need resetting as their parent (the group) is resetted) - if not node.isSelectable(): - continue # i.e. node with layer data - nodes.append(node) - self.arrange(nodes, fixed_nodes) - ## Arrange the nodes, given fixed nodes def arrange(self, nodes, fixed_nodes): min_offset = 8 From e866c03b50481af10bb89748002b435596eba392 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 3 Apr 2017 11:12:20 +0200 Subject: [PATCH 09/17] Fixed multiplyObject group. CURA-3239 --- cura/Arrange.py | 2 +- cura/CuraApplication.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cura/Arrange.py b/cura/Arrange.py index 303d0c6804..6437532b21 100755 --- a/cura/Arrange.py +++ b/cura/Arrange.py @@ -143,7 +143,7 @@ class Arrange: Logger.log("d", "Current buildplate: \n%s" % str(arranger._occupied[::10, ::10])) return arranger - ## Find placement for a node and place it + ## Find placement for a node (using offset shape) and place it (using hull shape) # def findNodePlacements(self, node, offset_shape_arr, hull_shape_arr, count = 1, step = 1): # offset_shape_arr, hull_shape_arr, arranger -> nodes, arranger diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index e34d02a704..7610ca5302 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -856,17 +856,17 @@ class CuraApplication(QtApplication): if not node and object_id != 0: # Workaround for tool handles overlapping the selected object node = Selection.getSelectedObject(0) + current_node = node + # Find the topmost group + while current_node.getParent() and current_node.getParent().callDecoration("isGroup"): + current_node = current_node.getParent() + root = self.getController().getScene().getRoot() arranger = Arrange.create(scene_root = root) - offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset) - nodes = arranger.findNodePlacements(node, offset_shape_arr, hull_shape_arr, count = count) + offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset = min_offset) + nodes = arranger.findNodePlacements(current_node, offset_shape_arr, hull_shape_arr, count = count) if nodes: - current_node = node - # Find the topmost group - while current_node.getParent() and current_node.getParent().callDecoration("isGroup"): - current_node = current_node.getParent() - op = GroupedOperation() for new_node in nodes: op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent())) From d6cd37626b76db0e6e4e2287de4e8fc7f7736de2 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 3 Apr 2017 11:47:04 +0200 Subject: [PATCH 10/17] Removed logging, improved spot for left-over object. CURA-3239 --- cura/Arrange.py | 8 +------- cura/CuraApplication.py | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/cura/Arrange.py b/cura/Arrange.py index 6437532b21..ed683b3610 100755 --- a/cura/Arrange.py +++ b/cura/Arrange.py @@ -134,13 +134,10 @@ class Arrange: fixed_nodes.append(node_) # place all objects fixed nodes for fixed_node in fixed_nodes: - Logger.log("d", " # Placing [%s]" % str(fixed_node)) - vertices = fixed_node.callDecoration("getConvexHull") points = copy.deepcopy(vertices._points) shape_arr = ShapeArray.fromPolygon(points, scale = scale) arranger.place(0, 0, shape_arr) - Logger.log("d", "Current buildplate: \n%s" % str(arranger._occupied[::10, ::10])) return arranger ## Find placement for a node (using offset shape) and place it (using hull shape) @@ -152,20 +149,17 @@ class Arrange: for i in range(count): new_node = copy.deepcopy(node) - Logger.log("d", " # Finding spot for %s" % new_node) x, y, penalty_points, start_prio = self.bestSpot( offset_shape_arr, start_prio = start_prio, step = step) transformation = new_node._transformation if x is not None: # We could find a place transformation._data[0][3] = x transformation._data[2][3] = y - Logger.log("d", "Best place is: %s %s (points = %s)" % (x, y, penalty_points)) self.place(x, y, hull_shape_arr) # take place before the next one - Logger.log("d", "New buildplate: \n%s" % str(self._occupied[::10, ::10])) else: Logger.log("d", "Could not find spot!") transformation._data[0][3] = 200 - transformation._data[2][3] = -100 + i * 20 + transformation._data[2][3] = 100 + i * 20 nodes.append(new_node) return nodes diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 7610ca5302..e737b488a7 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -856,8 +856,8 @@ class CuraApplication(QtApplication): if not node and object_id != 0: # Workaround for tool handles overlapping the selected object node = Selection.getSelectedObject(0) + # If object is part of a group, multiply group current_node = node - # Find the topmost group while current_node.getParent() and current_node.getParent().callDecoration("isGroup"): current_node = current_node.getParent() From a83b1dd638567fc6f14897c0eb76207af4b5dd20 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 3 Apr 2017 14:48:31 +0200 Subject: [PATCH 11/17] Split ShapeArray from Arranger. CURA-3239 --- cura/Arrange.py | 129 ++++++-------------------------------- cura/CuraApplication.py | 8 ++- cura/ShapeArray.py | 103 ++++++++++++++++++++++++++++++ resources/qml/Actions.qml | 3 +- resources/qml/Cura.qml | 6 +- 5 files changed, 131 insertions(+), 118 deletions(-) create mode 100755 cura/ShapeArray.py diff --git a/cura/Arrange.py b/cura/Arrange.py index ed683b3610..e0bd94b742 100755 --- a/cura/Arrange.py +++ b/cura/Arrange.py @@ -1,110 +1,15 @@ -import numpy -from UM.Math.Polygon import Polygon - - -## Polygon representation as an array -# -class ShapeArray: - def __init__(self, arr, offset_x, offset_y, scale = 1): - self.arr = arr - self.offset_x = offset_x - self.offset_y = offset_y - self.scale = scale - - @classmethod - def fromPolygon(cls, vertices, scale = 1): - # scale - vertices = vertices * scale - # flip y, x -> x, y - flip_vertices = numpy.zeros((vertices.shape)) - flip_vertices[:, 0] = vertices[:, 1] - flip_vertices[:, 1] = vertices[:, 0] - flip_vertices = flip_vertices[::-1] - # offset, we want that all coordinates have positive values - offset_y = int(numpy.amin(flip_vertices[:, 0])) - offset_x = int(numpy.amin(flip_vertices[:, 1])) - flip_vertices[:, 0] = numpy.add(flip_vertices[:, 0], -offset_y) - flip_vertices[:, 1] = numpy.add(flip_vertices[:, 1], -offset_x) - shape = [int(numpy.amax(flip_vertices[:, 0])), int(numpy.amax(flip_vertices[:, 1]))] - arr = cls.arrayFromPolygon(shape, flip_vertices) - return cls(arr, offset_x, offset_y) - - ## Return an offset and hull ShapeArray from a scenenode. - @classmethod - def fromNode(cls, node, min_offset, scale = 0.5): - # 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.fromPolygon(offset_points, scale = scale) - - 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.fromPolygon(hull_points, scale = scale) # x, y - - return offset_shape_arr, hull_shape_arr - - - ## Create np.array with dimensions defined by shape - # Fills polygon defined by vertices with ones, all other values zero - # Only works correctly for convex hull vertices - # Originally from: http://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array - @classmethod - def arrayFromPolygon(cls, shape, vertices): - base_array = numpy.zeros(shape, dtype=float) # Initialize your array of zeros - - fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill - - # Create check array for each edge segment, combine into fill array - for k in range(vertices.shape[0]): - fill = numpy.all([fill, cls._check(vertices[k - 1], vertices[k], base_array)], axis=0) - - # Set all values inside polygon to one - base_array[fill] = 1 - - 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 = numpy.indices(base_array.shape) # Create 3D array of indices - - p1 = p1.astype(float) - p2 = p2.astype(float) - - if p2[0] == p1[0]: - sign = numpy.sign(p2[1] - p1[1]) - return idxs[1] * sign - - if p2[1] == p1[1]: - sign = numpy.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 = numpy.sign(p2[0] - p1[0]) - return idxs[1] * sign <= max_col_idx * sign - - from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Logger import Logger +from cura.ShapeArray import ShapeArray + +import numpy import copy +## The Arrange classed is used together with ShapeArray. The class tries to find +# good locations for objects that you try to put on a build place. +# Different priority schemes can be defined so it alters the behavior while using +# the same logic. class Arrange: def __init__(self, x, y, offset_x, offset_y, scale=1): self.shape = (y, x) @@ -166,16 +71,18 @@ class Arrange: ## Fill priority, take offset as center. lower is better def centerFirst(self): - # Distance x + distance y - #self._priority = np.fromfunction( - # lambda i, j: abs(self._offset_x-i)+abs(self._offset_y-j), self.shape, dtype=np.int32) - # Square distance - # self._priority = np.fromfunction( - # lambda i, j: abs(self._offset_x-i)**2+abs(self._offset_y-j)**2, self.shape, dtype=np.int32) + # Distance x + distance y: creates diamond shape + #self._priority = numpy.fromfunction( + # lambda i, j: abs(self._offset_x-i)+abs(self._offset_y-j), self.shape, dtype=numpy.int32) + # Square distance: creates a more round shape self._priority = numpy.fromfunction( - lambda i, j: abs(self._offset_x-i)**3+abs(self._offset_y-j)**3, self.shape, dtype=numpy.int32) - # self._priority = np.fromfunction( - # lambda i, j: max(abs(self._offset_x-i), abs(self._offset_y-j)), self.shape, dtype=np.int32) + lambda i, j: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self.shape, dtype=numpy.int32) + self._priority_unique_values = numpy.unique(self._priority) + self._priority_unique_values.sort() + + def backFirst(self): + self._priority = numpy.fromfunction( + lambda i, j: 10 * j + abs(self._offset_x - i), self.shape, dtype=numpy.int32) self._priority_unique_values = numpy.unique(self._priority) self._priority_unique_values.sort() diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index e737b488a7..7c10a58f8e 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -32,7 +32,8 @@ 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.Arrange import Arrange +from cura.ShapeArray import ShapeArray from cura.ConvexHullDecorator import ConvexHullDecorator from cura.SetParentOperation import SetParentOperation from cura.SliceableObjectDecorator import SliceableObjectDecorator @@ -992,7 +993,6 @@ class CuraApplication(QtApplication): @pyqtSlot() def arrangeAll(self): nodes = [] - fixed_nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): if type(node) is not SceneNode: continue @@ -1003,7 +1003,7 @@ class CuraApplication(QtApplication): if not node.isSelectable(): continue # i.e. node with layer data nodes.append(node) - self.arrange(nodes, fixed_nodes) + self.arrange(nodes, fixed_nodes = []) ## Arrange Selection @pyqtSlot() @@ -1021,6 +1021,8 @@ class CuraApplication(QtApplication): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) if not node.isSelectable(): continue # i.e. node with layer data + if node in nodes: # exclude selected node from fixed_nodes + continue fixed_nodes.append(node) self.arrange(nodes, fixed_nodes) diff --git a/cura/ShapeArray.py b/cura/ShapeArray.py new file mode 100755 index 0000000000..6a1711bc2d --- /dev/null +++ b/cura/ShapeArray.py @@ -0,0 +1,103 @@ +import numpy +import copy + +from UM.Math.Polygon import Polygon + + +## Polygon representation as an array +# +class ShapeArray: + def __init__(self, arr, offset_x, offset_y, scale = 1): + self.arr = arr + self.offset_x = offset_x + self.offset_y = offset_y + self.scale = scale + + @classmethod + def fromPolygon(cls, vertices, scale = 1): + # scale + vertices = vertices * scale + # flip y, x -> x, y + flip_vertices = numpy.zeros((vertices.shape)) + flip_vertices[:, 0] = vertices[:, 1] + flip_vertices[:, 1] = vertices[:, 0] + flip_vertices = flip_vertices[::-1] + # offset, we want that all coordinates have positive values + offset_y = int(numpy.amin(flip_vertices[:, 0])) + offset_x = int(numpy.amin(flip_vertices[:, 1])) + flip_vertices[:, 0] = numpy.add(flip_vertices[:, 0], -offset_y) + flip_vertices[:, 1] = numpy.add(flip_vertices[:, 1], -offset_x) + shape = [int(numpy.amax(flip_vertices[:, 0])), int(numpy.amax(flip_vertices[:, 1]))] + arr = cls.arrayFromPolygon(shape, flip_vertices) + return cls(arr, offset_x, offset_y) + + ## Return an offset and hull ShapeArray from a scenenode. + @classmethod + def fromNode(cls, node, min_offset, scale = 0.5): + # 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.fromPolygon(offset_points, scale = scale) + + 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.fromPolygon(hull_points, scale = scale) # x, y + + return offset_shape_arr, hull_shape_arr + + + ## Create np.array with dimensions defined by shape + # Fills polygon defined by vertices with ones, all other values zero + # Only works correctly for convex hull vertices + # Originally from: http://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array + @classmethod + def arrayFromPolygon(cls, shape, vertices): + base_array = numpy.zeros(shape, dtype=float) # Initialize your array of zeros + + fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill + + # Create check array for each edge segment, combine into fill array + for k in range(vertices.shape[0]): + fill = numpy.all([fill, cls._check(vertices[k - 1], vertices[k], base_array)], axis=0) + + # Set all values inside polygon to one + base_array[fill] = 1 + + 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 = numpy.indices(base_array.shape) # Create 3D array of indices + + p1 = p1.astype(float) + p2 = p2.astype(float) + + if p2[0] == p1[0]: + sign = numpy.sign(p2[1] - p1[1]) + return idxs[1] * sign + + if p2[1] == p1[1]: + sign = numpy.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 = numpy.sign(p2[0] - p1[0]) + return idxs[1] * sign <= max_col_idx * sign + diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index b5e92f3dd4..66c0c1884e 100755 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -271,8 +271,9 @@ Item Action { id: arrangeAllAction; - text: catalog.i18nc("@action:inmenu menubar:edit","Arrange All"); + text: catalog.i18nc("@action:inmenu menubar:edit","Arrange All Models"); onTriggered: Printer.arrangeAll(); + shortcut: "Ctrl+R"; } Action diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index e74f0b9e2e..d56f28d0ec 100755 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -131,9 +131,9 @@ UM.MainWindow MenuItem { action: Cura.Actions.redo; } MenuSeparator { } MenuItem { action: Cura.Actions.selectAll; } + MenuItem { action: Cura.Actions.arrangeAll; } MenuItem { action: Cura.Actions.deleteSelection; } MenuItem { action: Cura.Actions.deleteAll; } - MenuItem { action: Cura.Actions.arrangeAll; } MenuItem { action: Cura.Actions.resetAllTranslation; } MenuItem { action: Cura.Actions.resetAll; } MenuSeparator { } @@ -637,9 +637,9 @@ UM.MainWindow MenuItem { action: Cura.Actions.multiplyObject; } MenuSeparator { } MenuItem { action: Cura.Actions.selectAll; } + MenuItem { action: Cura.Actions.arrangeAll; } MenuItem { action: Cura.Actions.deleteAll; } MenuItem { action: Cura.Actions.reloadAll; } - MenuItem { action: Cura.Actions.arrangeSelection; } MenuItem { action: Cura.Actions.resetAllTranslation; } MenuItem { action: Cura.Actions.resetAll; } MenuSeparator { } @@ -698,9 +698,9 @@ UM.MainWindow { id: contextMenu; MenuItem { action: Cura.Actions.selectAll; } + MenuItem { action: Cura.Actions.arrangeAll; } MenuItem { action: Cura.Actions.deleteAll; } MenuItem { action: Cura.Actions.reloadAll; } - MenuItem { action: Cura.Actions.arrangeAll; } MenuItem { action: Cura.Actions.resetAllTranslation; } MenuItem { action: Cura.Actions.resetAll; } MenuSeparator { } From d1b907865780c21a460340cd96300245497def3f Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 3 Apr 2017 16:36:48 +0200 Subject: [PATCH 12/17] Added first arranger tests, small refactors. CURA-3239 --- cura/Arrange.py | 13 +++++-- cura/CuraApplication.py | 4 ++- tests/TestArrange.py | 75 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 4 deletions(-) create mode 100755 tests/TestArrange.py diff --git a/cura/Arrange.py b/cura/Arrange.py index e0bd94b742..e69c5efef4 100755 --- a/cura/Arrange.py +++ b/cura/Arrange.py @@ -2,10 +2,15 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Logger import Logger from cura.ShapeArray import ShapeArray +from collections import namedtuple + import numpy import copy +## Return object for bestSpot +LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points", "priority"]) + ## The Arrange classed is used together with ShapeArray. The class tries to find # good locations for objects that you try to put on a build place. # Different priority schemes can be defined so it alters the behavior while using @@ -54,7 +59,7 @@ class Arrange: for i in range(count): new_node = copy.deepcopy(node) - x, y, penalty_points, start_prio = self.bestSpot( + x, y = self.bestSpot( offset_shape_arr, start_prio = start_prio, step = step) transformation = new_node._transformation if x is not None: # We could find a place @@ -80,6 +85,7 @@ class Arrange: self._priority_unique_values = numpy.unique(self._priority) self._priority_unique_values.sort() + ## def backFirst(self): self._priority = numpy.fromfunction( lambda i, j: 10 * j + abs(self._offset_x - i), self.shape, dtype=numpy.int32) @@ -107,6 +113,7 @@ class Arrange: return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)]) ## Find "best" spot for ShapeArray + # Return namedtuple with properties x, y, penalty_points, priority def bestSpot(self, shape_arr, start_prio = 0, step = 1): start_idx_list = numpy.where(self._priority_unique_values == start_prio) if start_idx_list: @@ -124,8 +131,8 @@ class Arrange: # array to "world" coordinates penalty_points = self.checkShape(projected_x, projected_y, shape_arr) if penalty_points != 999999: - return projected_x, projected_y, penalty_points, prio - return None, None, None, prio # No suitable location found :-( + return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = prio) + return LocationSuggestion(x = None, y = None, penalty_points = None, priority = prio) # No suitable location found :-( ## Place the object def place(self, x, y, shape_arr): diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 7c10a58f8e..e788b175e4 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1043,8 +1043,10 @@ class CuraApplication(QtApplication): for size, node, offset_shape_arr, hull_shape_arr in nodes_arr: # we assume that when a location does not fit, it will also not fit for the next # object (while what can be untrue). That saves a lot of time. - x, y, penalty_points, start_prio = arranger.bestSpot( + best_spot = arranger.bestSpot( offset_shape_arr, start_prio = start_prio) + x, y = best_spot.x, best_spot.y + start_prio = best_spot.priority if x is not None: # We could find a place arranger.place(x, y, hull_shape_arr) # take place before the next one diff --git a/tests/TestArrange.py b/tests/TestArrange.py new file mode 100755 index 0000000000..84e9ed446f --- /dev/null +++ b/tests/TestArrange.py @@ -0,0 +1,75 @@ +import pytest +import numpy +import time + +from cura.Arrange import Arrange +from cura.ShapeArray import ShapeArray + + +def gimmeShapeArray(): + vertices = numpy.array([[-3, 1], [3, 1], [0, -3]]) + shape_arr = ShapeArray.fromPolygon(vertices) + return shape_arr + + +def test_smoke_arrange(): + ar = Arrange.create(fixed_nodes = []) + + +def test_centerFirst(): + ar = Arrange(300, 300, 150, 150) + ar.centerFirst() + assert ar._priority[150][150] < ar._priority[170][150] + assert ar._priority[150][150] < ar._priority[150][170] + assert ar._priority[150][150] < ar._priority[170][170] + assert ar._priority[150][150] < ar._priority[130][150] + assert ar._priority[150][150] < ar._priority[150][130] + assert ar._priority[150][150] < ar._priority[130][130] + + +def test_backFirst(): + ar = Arrange(300, 300, 150, 150) + ar.backFirst() + assert ar._priority[150][150] < ar._priority[150][170] + assert ar._priority[150][150] < ar._priority[170][170] + assert ar._priority[150][150] > ar._priority[150][130] + assert ar._priority[150][150] > ar._priority[130][130] + + +def test_smoke_bestSpot(): + ar = Arrange(30, 30, 15, 15) + ar.centerFirst() + + shape_arr = gimmeShapeArray() + best_spot = ar.bestSpot(shape_arr) + assert hasattr(best_spot, "x") + assert hasattr(best_spot, "y") + assert hasattr(best_spot, "penalty_points") + assert hasattr(best_spot, "priority") + + +def test_smoke_place(): + ar = Arrange(30, 30, 15, 15) + ar.centerFirst() + + shape_arr = gimmeShapeArray() + + assert not numpy.any(ar._occupied) + ar.place(0, 0, shape_arr) + assert numpy.any(ar._occupied) + + +def test_place_objects(): + ar = Arrange(20, 20, 10, 10) + ar.centerFirst() + shape_arr = gimmeShapeArray() + print(shape_arr) + + now = time.time() + for i in range(5): + best_spot_x, best_spot_y, score, prio = ar.bestSpot(shape_arr) + print(best_spot_x, best_spot_y, score) + ar.place(best_spot_x, best_spot_y, shape_arr) + print(ar._occupied) + + print(time.time() - now) From 1ebf947ff2a7adc65c79cf1bcf4c814b4a0819a9 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 3 Apr 2017 17:03:30 +0200 Subject: [PATCH 13/17] Added tests for ShapeArray. CURA-3239 --- cura/Arrange.py | 3 +- cura/ShapeArray.py | 2 +- tests/TestArrange.py | 75 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/cura/Arrange.py b/cura/Arrange.py index e69c5efef4..1508a6618d 100755 --- a/cura/Arrange.py +++ b/cura/Arrange.py @@ -51,9 +51,8 @@ class Arrange: return arranger ## Find placement for a node (using offset shape) and place it (using hull shape) - # + # return the nodes that should be placed def findNodePlacements(self, node, offset_shape_arr, hull_shape_arr, count = 1, step = 1): - # offset_shape_arr, hull_shape_arr, arranger -> nodes, arranger nodes = [] start_prio = 0 for i in range(count): diff --git a/cura/ShapeArray.py b/cura/ShapeArray.py index 6a1711bc2d..e6ddff7a94 100755 --- a/cura/ShapeArray.py +++ b/cura/ShapeArray.py @@ -73,7 +73,7 @@ class ShapeArray: return base_array - ## Return indices that mark one side of the line, used by array_from_polygon + ## Return indices that mark one side of the line, used by arrayFromPolygon # 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 diff --git a/tests/TestArrange.py b/tests/TestArrange.py index 84e9ed446f..764da3cb65 100755 --- a/tests/TestArrange.py +++ b/tests/TestArrange.py @@ -12,10 +12,17 @@ def gimmeShapeArray(): return shape_arr +## Smoke test for Arrange def test_smoke_arrange(): ar = Arrange.create(fixed_nodes = []) +## Smoke test for ShapeArray +def test_smoke_ShapeArray(): + shape_arr = gimmeShapeArray() + + +## Test centerFirst def test_centerFirst(): ar = Arrange(300, 300, 150, 150) ar.centerFirst() @@ -27,6 +34,7 @@ def test_centerFirst(): assert ar._priority[150][150] < ar._priority[130][130] +## Test backFirst def test_backFirst(): ar = Arrange(300, 300, 150, 150) ar.backFirst() @@ -36,6 +44,7 @@ def test_backFirst(): assert ar._priority[150][150] > ar._priority[130][130] +## See if the result of bestSpot has the correct form def test_smoke_bestSpot(): ar = Arrange(30, 30, 15, 15) ar.centerFirst() @@ -48,6 +57,7 @@ def test_smoke_bestSpot(): assert hasattr(best_spot, "priority") +## Try to place an object and see if something explodes def test_smoke_place(): ar = Arrange(30, 30, 15, 15) ar.centerFirst() @@ -59,7 +69,34 @@ def test_smoke_place(): assert numpy.any(ar._occupied) -def test_place_objects(): +## See of our center has less penalty points than out of the center +def test_checkShape(): + ar = Arrange(30, 30, 15, 15) + ar.centerFirst() + + shape_arr = gimmeShapeArray() + points = ar.checkShape(0, 0, shape_arr) + points2 = ar.checkShape(5, 0, shape_arr) + points3 = ar.checkShape(0, 5, shape_arr) + assert points2 > points + assert points3 > points + + +## After placing an object on a location that location should give more penalty points +def test_checkShape_place(): + ar = Arrange(30, 30, 15, 15) + ar.centerFirst() + + shape_arr = gimmeShapeArray() + points = ar.checkShape(3, 6, shape_arr) + ar.place(3, 6, shape_arr) + points2 = ar.checkShape(3, 6, shape_arr) + + assert points2 > points + + +## Test the whole sequence +def test_smoke_place_objects(): ar = Arrange(20, 20, 10, 10) ar.centerFirst() shape_arr = gimmeShapeArray() @@ -73,3 +110,39 @@ def test_place_objects(): print(ar._occupied) print(time.time() - now) + + +## Polygon -> array +def test_arrayFromPolygon(): + vertices = numpy.array([[-3, 1], [3, 1], [0, -3]]) + array = ShapeArray.arrayFromPolygon([5, 5], vertices) + assert numpy.any(array) + + +## Polygon -> array +def test_arrayFromPolygon2(): + vertices = numpy.array([[-3, 1], [3, 1], [2, -3]]) + array = ShapeArray.arrayFromPolygon([5, 5], vertices) + assert numpy.any(array) + + +## Line definition -> array with true/false +def test_check(): + base_array = numpy.zeros([5, 5], dtype=float) + p1 = numpy.array([0, 0]) + p2 = numpy.array([4, 4]) + check_array = ShapeArray._check(p1, p2, base_array) + assert numpy.any(check_array) + assert check_array[3][0] + assert not check_array[0][3] + + +## Line definition -> array with true/false +def test_check2(): + base_array = numpy.zeros([5, 5], dtype=float) + p1 = numpy.array([0, 3]) + p2 = numpy.array([4, 3]) + check_array = ShapeArray._check(p1, p2, base_array) + assert numpy.any(check_array) + assert not check_array[3][0] + assert check_array[3][4] From 1df9066340a13a221004f9fc1a131e7cb8df5caf Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 4 Apr 2017 09:52:07 +0200 Subject: [PATCH 14/17] Fix error after refactor, added comments --- cura/Arrange.py | 4 +++- cura/CuraApplication.py | 12 ++++++++---- cura/ShapeArray.py | 3 +-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/cura/Arrange.py b/cura/Arrange.py index 1508a6618d..5ea1e1c12d 100755 --- a/cura/Arrange.py +++ b/cura/Arrange.py @@ -58,8 +58,10 @@ class Arrange: for i in range(count): new_node = copy.deepcopy(node) - x, y = self.bestSpot( + best_spot = self.bestSpot( offset_shape_arr, start_prio = start_prio, step = step) + x, y = best_spot.x, best_spot.y + start_prio = best_spot.priority transformation = new_node._transformation if x is not None: # We could find a place transformation._data[0][3] = x diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index e788b175e4..10cbb52629 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1031,20 +1031,24 @@ class CuraApplication(QtApplication): min_offset = 8 arranger = Arrange.create(fixed_nodes = fixed_nodes) + + # Collect nodes to be placed nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr) for node in nodes: offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset) nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr)) + # Sort nodes biggest area first nodes_arr.sort(key = lambda item: item[0]) nodes_arr.reverse() + # Place nodes one at a time start_prio = 0 for size, node, offset_shape_arr, hull_shape_arr in nodes_arr: - # we assume that when a location does not fit, it will also not fit for the next - # object (while what can be untrue). That saves a lot of time. - best_spot = arranger.bestSpot( - offset_shape_arr, start_prio = start_prio) + # For performance reasons, we assume that when a location does not fit, + # it will also not fit for the next object (while what can be untrue). + # We also skip possibilities by slicing through the possibilities (step = 10) + best_spot = arranger.bestSpot(offset_shape_arr, start_prio = start_prio, step = 10) x, y = best_spot.x, best_spot.y start_prio = best_spot.priority if x is not None: # We could find a place diff --git a/cura/ShapeArray.py b/cura/ShapeArray.py index e6ddff7a94..6d310b7be8 100755 --- a/cura/ShapeArray.py +++ b/cura/ShapeArray.py @@ -31,10 +31,9 @@ class ShapeArray: arr = cls.arrayFromPolygon(shape, flip_vertices) return cls(arr, offset_x, offset_y) - ## Return an offset and hull ShapeArray from a scenenode. + ## Return an offset and hull ShapeArray from a scene node. @classmethod def fromNode(cls, node, min_offset, scale = 0.5): - # hacky way to undo transformation transform = node._transformation transform_x = transform._data[0][3] transform_y = transform._data[2][3] From 3d16c4120e82583eb5cdc4288d642d7971372db5 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 4 Apr 2017 09:59:42 +0200 Subject: [PATCH 15/17] Added comments. CURA-3239 --- cura/Arrange.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/cura/Arrange.py b/cura/Arrange.py index 5ea1e1c12d..aa5f16e3a7 100755 --- a/cura/Arrange.py +++ b/cura/Arrange.py @@ -11,7 +11,7 @@ import copy ## Return object for bestSpot LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points", "priority"]) -## The Arrange classed is used together with ShapeArray. The class tries to find +## The Arrange classed is used together with ShapeArray. Use it to find # good locations for objects that you try to put on a build place. # Different priority schemes can be defined so it alters the behavior while using # the same logic. @@ -29,8 +29,8 @@ class Arrange: # # Either fill in scene_root and create will find all sliceable nodes by itself, # or use fixed_nodes to provide the nodes yourself. - # \param scene_root - # \param fixed_nodes + # \param scene_root Root for finding all scene nodes + # \param fixed_nodes Scene nodes to be placed @classmethod def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5): arranger = Arrange(220, 220, 110, 110, scale = scale) @@ -52,6 +52,10 @@ class Arrange: ## Find placement for a node (using offset shape) and place it (using hull shape) # return the nodes that should be placed + # \param node + # \param offset_shape_arr ShapeArray with offset, used to find location + # \param hull_shape_arr ShapeArray without offset, for placing the shape + # \param count Number of objects def findNodePlacements(self, node, offset_shape_arr, hull_shape_arr, count = 1, step = 1): nodes = [] start_prio = 0 @@ -75,7 +79,7 @@ class Arrange: nodes.append(new_node) return nodes - ## Fill priority, take offset as center. lower is better + ## Fill priority, center is best. lower value is better def centerFirst(self): # Distance x + distance y: creates diamond shape #self._priority = numpy.fromfunction( @@ -86,7 +90,7 @@ class Arrange: self._priority_unique_values = numpy.unique(self._priority) self._priority_unique_values.sort() - ## + ## Fill priority, back is best. lower value is better def backFirst(self): self._priority = numpy.fromfunction( lambda i, j: 10 * j + abs(self._offset_x - i), self.shape, dtype=numpy.int32) @@ -95,6 +99,9 @@ class Arrange: ## Return the amount of "penalty points" for polygon, which is the sum of priority # 999999 if occupied + # \param x x-coordinate to check shape + # \param y y-coordinate + # \param shape_arr the ShapeArray object to place def checkShape(self, x, y, shape_arr): x = int(self._scale * x) y = int(self._scale * y) @@ -115,6 +122,9 @@ class Arrange: ## Find "best" spot for ShapeArray # Return namedtuple with properties x, y, penalty_points, priority + # \param shape_arr ShapeArray + # \param start_prio Start with this priority value (and skip the ones before) + # \param step Slicing value, higher = more skips = faster but less accurate def bestSpot(self, shape_arr, start_prio = 0, step = 1): start_idx_list = numpy.where(self._priority_unique_values == start_prio) if start_idx_list: @@ -135,7 +145,11 @@ class Arrange: return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = prio) return LocationSuggestion(x = None, y = None, penalty_points = None, priority = prio) # No suitable location found :-( - ## Place the object + ## Place the object. + # Marks the locations in self._occupied and self._priority + # \param x x-coordinate + # \param y y-coordinate + # \param shape_arr ShapeArray object def place(self, x, y, shape_arr): x = int(self._scale * x) y = int(self._scale * y) From 535330ef515b152a3723070f9b4950a0bcf1f9a1 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 4 Apr 2017 10:29:10 +0200 Subject: [PATCH 16/17] Added comments. CURA-3239 --- cura/CuraApplication.py | 8 +++++--- cura/ShapeArray.py | 17 +++++++++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 10cbb52629..a2afe245e0 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -847,9 +847,9 @@ 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. + # \param object_id + # \param count number of copies + # \param min_offset minimum offset to other objects. @pyqtSlot("quint64", int) def multiplyObject(self, object_id, count, min_offset = 8): node = self.getController().getScene().findObject(object_id) @@ -1027,6 +1027,8 @@ class CuraApplication(QtApplication): self.arrange(nodes, fixed_nodes) ## Arrange the nodes, given fixed nodes + # \param nodes nodes that we have to place + # \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes def arrange(self, nodes, fixed_nodes): min_offset = 8 diff --git a/cura/ShapeArray.py b/cura/ShapeArray.py index 6d310b7be8..534fa78e4d 100755 --- a/cura/ShapeArray.py +++ b/cura/ShapeArray.py @@ -4,8 +4,7 @@ import copy from UM.Math.Polygon import Polygon -## Polygon representation as an array -# +## Polygon representation as an array for use with Arrange class ShapeArray: def __init__(self, arr, offset_x, offset_y, scale = 1): self.arr = arr @@ -13,6 +12,9 @@ class ShapeArray: self.offset_y = offset_y self.scale = scale + ## Instantiate from a bunch of vertices + # \param vertices + # \param scale scale the coordinates @classmethod def fromPolygon(cls, vertices, scale = 1): # scale @@ -31,7 +33,10 @@ class ShapeArray: arr = cls.arrayFromPolygon(shape, flip_vertices) return cls(arr, offset_x, offset_y) - ## Return an offset and hull ShapeArray from a scene node. + ## Instantiate an offset and hull ShapeArray from a scene node. + # \param node source node where the convex hull must be present + # \param min_offset offset for the offset ShapeArray + # \param scale scale the coordinates @classmethod def fromNode(cls, node, min_offset, scale = 0.5): transform = node._transformation @@ -52,11 +57,12 @@ class ShapeArray: return offset_shape_arr, hull_shape_arr - ## Create np.array with dimensions defined by shape # Fills polygon defined by vertices with ones, all other values zero # Only works correctly for convex hull vertices # Originally from: http://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array + # \param shape numpy format shape, [x-size, y-size] + # \param vertices @classmethod def arrayFromPolygon(cls, shape, vertices): base_array = numpy.zeros(shape, dtype=float) # Initialize your array of zeros @@ -77,6 +83,9 @@ class ShapeArray: # 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 + # \param p1 2-tuple with x, y for point 1 + # \param p2 2-tuple with x, y for point 2 + # \param base_array boolean array to project the line on @classmethod def _check(cls, p1, p2, base_array): if p1[0] == p2[0] and p1[1] == p2[1]: From 4e91d55bdfe430e066bdee16ed2b29a3c9948855 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 5 Apr 2017 10:41:54 +0200 Subject: [PATCH 17/17] Fix Arranger bestSpot. CURA-3239 --- cura/Arrange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Arrange.py b/cura/Arrange.py index aa5f16e3a7..78b89b9e6a 100755 --- a/cura/Arrange.py +++ b/cura/Arrange.py @@ -128,7 +128,7 @@ class Arrange: def bestSpot(self, shape_arr, start_prio = 0, step = 1): start_idx_list = numpy.where(self._priority_unique_values == start_prio) if start_idx_list: - start_idx = start_idx_list[0] + start_idx = start_idx_list[0][0] else: start_idx = 0 for prio in self._priority_unique_values[start_idx::step]: