From abb5d1e76eaa85c770336c9568fe6a258da7ad0c Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 3 Apr 2017 10:40:04 +0200 Subject: [PATCH] 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 { }