diff --git a/cura/Arrange.py b/cura/Arrange.py new file mode 100755 index 0000000000..78b89b9e6a --- /dev/null +++ b/cura/Arrange.py @@ -0,0 +1,172 @@ +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. 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. +class Arrange: + def __init__(self, x, y, offset_x, offset_y, scale=1): + self.shape = (y, x) + self._priority = numpy.zeros((x, y), dtype=numpy.int32) + self._priority_unique_values = [] + 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 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) + 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: + vertices = fixed_node.callDecoration("getConvexHull") + points = copy.deepcopy(vertices._points) + shape_arr = ShapeArray.fromPolygon(points, scale = scale) + arranger.place(0, 0, shape_arr) + return arranger + + ## 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 + for i in range(count): + new_node = copy.deepcopy(node) + + 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 + transformation._data[2][3] = y + self.place(x, y, hull_shape_arr) # take place before the next one + 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, center is best. lower value is better + def centerFirst(self): + # 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: (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() + + ## 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) + 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 + # \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) + 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]] + try: + 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 numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)]) + + ## 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: + start_idx = start_idx_list[0][0] + else: + start_idx = 0 + for prio in self._priority_unique_values[start_idx::step]: + 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] + projected_x = x - self._offset_x + projected_y = y - self._offset_y + + # array to "world" coordinates + penalty_points = self.checkShape(projected_x, projected_y, shape_arr) + if penalty_points != 999999: + 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. + # 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) + offset_x = x + self._offset_x + shape_arr.offset_x + offset_y = y + self._offset_y + shape_arr.offset_y + 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[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[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 0f917ab012..f711c86c8a 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -31,6 +31,9 @@ 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 +from cura.ShapeArray import ShapeArray +from cura.ConvexHullDecorator import ConvexHullDecorator from cura.SetParentOperation import SetParentOperation from cura.SliceableObjectDecorator import SliceableObjectDecorator from cura.BlockSlicingDecorator import BlockSlicingDecorator @@ -838,22 +841,29 @@ class CuraApplication(QtApplication): op.push() ## Create a number of copies of existing object. + # \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): + 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 node = Selection.getSelectedObject(0) - if node: - current_node = node - # Find the topmost group - while current_node.getParent() and current_node.getParent().callDecoration("isGroup"): - current_node = current_node.getParent() + # If object is part of a group, multiply group + current_node = node + 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(current_node, min_offset = min_offset) + nodes = arranger.findNodePlacements(current_node, offset_shape_arr, hull_shape_arr, count = count) + + if nodes: 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())) op.push() @@ -973,6 +983,83 @@ class CuraApplication(QtApplication): op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1))) op.push() + ## Arrange all objects. + @pyqtSlot() + def arrangeAll(self): + 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() + + # 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 + if node in nodes: # exclude selected node from fixed_nodes + continue + fixed_nodes.append(node) + 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 + + 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: + # 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 + 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): @@ -1209,6 +1296,10 @@ class CuraApplication(QtApplication): filename = job.getFileName() self._currently_loading_files.remove(filename) + root = self.getController().getScene().getRoot() + arranger = Arrange.create(scene_root = root) + min_offset = 8 + for node in nodes: node.setSelectable(True) node.setName(os.path.basename(filename)) @@ -1229,8 +1320,18 @@ 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 + 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 = arranger.findNodePlacements(node, offset_shape_arr, hull_shape_arr, count = 1, step = 10) + + for new_node in nodes: + op = AddSceneNodeOperation(new_node, scene.getRoot()) + op.push() scene.sceneChanged.emit(node) diff --git a/cura/ShapeArray.py b/cura/ShapeArray.py new file mode 100755 index 0000000000..534fa78e4d --- /dev/null +++ b/cura/ShapeArray.py @@ -0,0 +1,111 @@ +import numpy +import copy + +from UM.Math.Polygon import Polygon + + +## Polygon representation as an array for use with Arrange +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 + + ## Instantiate from a bunch of vertices + # \param vertices + # \param scale scale the coordinates + @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) + + ## 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 + 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 + # \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 + + 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 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 + # 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]: + 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 old mode 100644 new mode 100755 index 26103d1a60..b5f5823ece --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -31,6 +31,8 @@ Item property alias selectAll: selectAllAction; property alias deleteAll: deleteAllAction; property alias reloadAll: reloadAllAction; + property alias arrangeAll: arrangeAllAction; + property alias arrangeSelection: arrangeSelectionAction; property alias resetAllTranslation: resetAllTranslationAction; property alias resetAll: resetAllAction; @@ -266,6 +268,21 @@ Item onTriggered: CuraApplication.reloadAll(); } + Action + { + id: arrangeAllAction; + text: catalog.i18nc("@action:inmenu menubar:edit","Arrange All Models"); + onTriggered: Printer.arrangeAll(); + shortcut: "Ctrl+R"; + } + + Action + { + id: arrangeSelectionAction; + text: catalog.i18nc("@action:inmenu menubar:edit","Arrange Selection"); + onTriggered: Printer.arrangeSelection(); + } + Action { id: resetAllTranslationAction; diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 486959dab8..b425432a00 100755 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -131,6 +131,7 @@ 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.resetAllTranslation; } @@ -603,6 +604,7 @@ 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.resetAllTranslation; } @@ -663,6 +665,7 @@ 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.resetAllTranslation; } diff --git a/tests/TestArrange.py b/tests/TestArrange.py new file mode 100755 index 0000000000..764da3cb65 --- /dev/null +++ b/tests/TestArrange.py @@ -0,0 +1,148 @@ +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 + + +## 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() + 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] + + +## Test backFirst +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] + + +## See if the result of bestSpot has the correct form +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") + + +## Try to place an object and see if something explodes +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) + + +## 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() + 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) + + +## 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]