Browse Source

CURA-5370 Small refactor for Arranger: make x and y consistent (numpy arrays start with y first in general), faster, cleanup, more unit tests, take actual build plate size in Arranger instances

Jack Ha 6 years ago
parent
commit
f5bed242ed

+ 37 - 21
cura/Arranging/Arrange.py

@@ -18,17 +18,20 @@ LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points
 #   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.
+#
+#   Note: Make sure the scale is the same between ShapeArray objects and the Arrange instance.
 class Arrange:
     build_volume = None
 
-    def __init__(self, x, y, offset_x, offset_y, scale= 1.0):
-        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)
+    def __init__(self, x, y, offset_x, offset_y, scale= 0.5):
         self._scale = scale  # convert input coordinates to arrange coordinates
-        self._offset_x = offset_x
-        self._offset_y = offset_y
+        world_x, world_y = int(x * self._scale), int(y * self._scale)
+        self._shape = (world_y, world_x)
+        self._priority = numpy.zeros((world_y, world_x), dtype=numpy.int32)  # beware: these are indexed (y, x)
+        self._priority_unique_values = []
+        self._occupied = numpy.zeros((world_y, world_x), dtype=numpy.int32)  # beware: these are indexed (y, x)
+        self._offset_x = int(offset_x * self._scale)
+        self._offset_y = int(offset_y * self._scale)
         self._last_priority = 0
         self._is_empty = True
 
@@ -39,7 +42,7 @@ class Arrange:
     #   \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, x = 220, y = 220):
+    def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250):
         arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
         arranger.centerFirst()
 
@@ -61,13 +64,17 @@ class Arrange:
 
         # If a build volume was set, add the disallowed areas
         if Arrange.build_volume:
-            disallowed_areas = Arrange.build_volume.getDisallowedAreas()
+            disallowed_areas = Arrange.build_volume.getDisallowedAreasNoBrim()
             for area in disallowed_areas:
                 points = copy.deepcopy(area._points)
                 shape_arr = ShapeArray.fromPolygon(points, scale = scale)
                 arranger.place(0, 0, shape_arr, update_empty = False)
         return arranger
 
+    ##  This resets the optimization for finding location based on size
+    def resetLastPriority(self):
+        self._last_priority = 0
+
     ##  Find placement for a node (using offset shape) and place it (using hull shape)
     #   return the nodes that should be placed
     #   \param node
@@ -104,7 +111,7 @@ class Arrange:
     def centerFirst(self):
         # 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)
+            lambda j, i: (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()
 
@@ -112,7 +119,7 @@ class Arrange:
     #   This is a strategy for the arranger.
     def backFirst(self):
         self._priority = numpy.fromfunction(
-            lambda i, j: 10 * j + abs(self._offset_x - i), self.shape, dtype=numpy.int32)
+            lambda j, i: 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()
 
@@ -126,9 +133,15 @@ class Arrange:
         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
+        if offset_x < 0 or offset_y < 0:
+            return None  # out of bounds in self._occupied
+        occupied_x_max = offset_x + shape_arr.arr.shape[1]
+        occupied_y_max = offset_y + shape_arr.arr.shape[0]
+        if occupied_x_max > self._occupied.shape[1] + 1 or occupied_y_max > self._occupied.shape[0] + 1:
+            return None  # out of bounds in self._occupied
         occupied_slice = self._occupied[
-            offset_y:offset_y + shape_arr.arr.shape[0],
-            offset_x:offset_x + shape_arr.arr.shape[1]]
+            offset_y:occupied_y_max,
+            offset_x:occupied_x_max]
         try:
             if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]):
                 return None
@@ -140,7 +153,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
+    #   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
@@ -153,12 +166,11 @@ class Arrange:
         for priority in self._priority_unique_values[start_idx::step]:
             tryout_idx = numpy.where(self._priority == priority)
             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
+                x = tryout_idx[1][idx]
+                y = tryout_idx[0][idx]
+                projected_x = int((x - self._offset_x) / self._scale)
+                projected_y = int((y - self._offset_y) / self._scale)
 
-                # array to "world" coordinates
                 penalty_points = self.checkShape(projected_x, projected_y, shape_arr)
                 if penalty_points is not None:
                     return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = priority)
@@ -191,8 +203,12 @@ class Arrange:
 
         # 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
+        prio_slice[new_occupied] = 999
+
+        # If you want to see how the rasterized arranger build plate looks like, uncomment this code
+        # numpy.set_printoptions(linewidth=500, edgeitems=200)
+        # print(self._occupied.shape)
+        # print(self._occupied)
 
     @property
     def isEmpty(self):

+ 8 - 11
cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py

@@ -1,6 +1,7 @@
 # Copyright (c) 2017 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
+from UM.Application import Application
 from UM.Job import Job
 from UM.Scene.SceneNode import SceneNode
 from UM.Math.Vector import Vector
@@ -17,6 +18,7 @@ from cura.Arranging.ShapeArray import ShapeArray
 from typing import List
 
 
+##  Do an arrangements on a bunch of build plates
 class ArrangeArray:
     def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]):
         self._x = x
@@ -79,7 +81,11 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
         nodes_arr.sort(key=lambda item: item[0])
         nodes_arr.reverse()
 
-        x, y = 200, 200
+        global_container_stack = Application.getInstance().getGlobalContainerStack()
+        machine_width = global_container_stack.getProperty("machine_width", "value")
+        machine_depth = global_container_stack.getProperty("machine_depth", "value")
+
+        x, y = machine_width, machine_depth
 
         arrange_array = ArrangeArray(x = x, y = y, fixed_nodes = [])
         arrange_array.add()
@@ -93,27 +99,18 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
         for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(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)
 
             try_placement = True
 
             current_build_plate_number = 0  # always start with the first one
 
-            # # Only for first build plate
-            # if last_size == size and last_build_plate_number == current_build_plate_number:
-            #     # This optimization works if many of the objects have the same size
-            #     # Continue with same build plate number
-            #     start_priority = last_priority
-            # else:
-            #     start_priority = 0
-
             while try_placement:
                 # make sure that current_build_plate_number is not going crazy or you'll have a lot of arrange objects
                 while current_build_plate_number >= arrange_array.count():
                     arrange_array.add()
                 arranger = arrange_array.get(current_build_plate_number)
 
-                best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10)
+                best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority)
                 x, y = best_spot.x, best_spot.y
                 node.removeDecorator(ZOffsetDecorator)
                 if node.getBoundingBox():

+ 10 - 5
cura/Arranging/ArrangeObjectsJob.py

@@ -1,6 +1,7 @@
 # Copyright (c) 2017 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
+from UM.Application import Application
 from UM.Job import Job
 from UM.Scene.SceneNode import SceneNode
 from UM.Math.Vector import Vector
@@ -32,7 +33,11 @@ class ArrangeObjectsJob(Job):
                                  progress = 0,
                                  title = i18n_catalog.i18nc("@info:title", "Finding Location"))
         status_message.show()
-        arranger = Arrange.create(fixed_nodes = self._fixed_nodes)
+        global_container_stack = Application.getInstance().getGlobalContainerStack()
+        machine_width = global_container_stack.getProperty("machine_width", "value")
+        machine_depth = global_container_stack.getProperty("machine_depth", "value")
+
+        arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes)
 
         # Collect nodes to be placed
         nodes_arr = []  # fill with (size, node, offset_shape_arr, hull_shape_arr)
@@ -50,15 +55,15 @@ class ArrangeObjectsJob(Job):
         last_size = None
         grouped_operation = GroupedOperation()
         found_solution_for_all = True
+        not_fit_count = 0
         for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(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)
             if last_size == size:  # This optimization works if many of the objects have the same size
                 start_priority = last_priority
             else:
                 start_priority = 0
-            best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10)
+            best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority)
             x, y = best_spot.x, best_spot.y
             node.removeDecorator(ZOffsetDecorator)
             if node.getBoundingBox():
@@ -70,12 +75,12 @@ class ArrangeObjectsJob(Job):
                 last_priority = best_spot.priority
 
                 arranger.place(x, y, hull_shape_arr)  # take place before the next one
-
                 grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
             else:
                 Logger.log("d", "Arrange all: could not find spot!")
                 found_solution_for_all = False
-                grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, - idx * 20), set_position = True))
+                grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, -not_fit_count * 20), set_position = True))
+                not_fit_count += 1
 
             status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
             Job.yieldThread()

+ 1 - 1
cura/Arranging/ShapeArray.py

@@ -74,7 +74,7 @@ class ShapeArray:
     #   \param vertices
     @classmethod
     def arrayFromPolygon(cls, shape, vertices):
-        base_array = numpy.zeros(shape, dtype=float)  # Initialize your array of zeros
+        base_array = numpy.zeros(shape, dtype = numpy.int32)  # Initialize your array of zeros
 
         fill = numpy.ones(base_array.shape) * True  # Initialize boolean array defining shape fill
 

+ 16 - 3
cura/BuildVolume.py

@@ -25,6 +25,7 @@ catalog = i18nCatalog("cura")
 
 import numpy
 import math
+import copy
 
 from typing import List, Optional
 
@@ -61,6 +62,7 @@ class BuildVolume(SceneNode):
         self._grid_shader = None
 
         self._disallowed_areas = []
+        self._disallowed_areas_no_brim = []
         self._disallowed_area_mesh = None
 
         self._error_areas = []
@@ -171,6 +173,9 @@ class BuildVolume(SceneNode):
     def getDisallowedAreas(self) -> List[Polygon]:
         return self._disallowed_areas
 
+    def getDisallowedAreasNoBrim(self) -> List[Polygon]:
+        return self._disallowed_areas_no_brim
+
     def setDisallowedAreas(self, areas: List[Polygon]):
         self._disallowed_areas = areas
 
@@ -658,7 +663,8 @@ class BuildVolume(SceneNode):
 
         result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added.
         prime_areas = self._computeDisallowedAreasPrimeBlob(disallowed_border_size, used_extruders)
-        prime_disallowed_areas = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking.
+        result_areas_no_brim = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking.
+        prime_disallowed_areas = copy.deepcopy(result_areas_no_brim)
 
         #Check if prime positions intersect with disallowed areas.
         for extruder in used_extruders:
@@ -687,12 +693,15 @@ class BuildVolume(SceneNode):
                     break
 
             result_areas[extruder_id].extend(prime_areas[extruder_id])
+            result_areas_no_brim[extruder_id].extend(prime_areas[extruder_id])
 
             nozzle_disallowed_areas = extruder.getProperty("nozzle_disallowed_areas", "value")
             for area in nozzle_disallowed_areas:
                 polygon = Polygon(numpy.array(area, numpy.float32))
-                polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
-                result_areas[extruder_id].append(polygon) #Don't perform the offset on these.
+                polygon_disallowed_border = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
+                result_areas[extruder_id].append(polygon_disallowed_border) #Don't perform the offset on these.
+                #polygon_minimal_border = polygon.getMinkowskiHull(5)
+                result_areas_no_brim[extruder_id].append(polygon)  # no brim
 
         # Add prime tower location as disallowed area.
         if len(used_extruders) > 1: #No prime tower in single-extrusion.
@@ -708,6 +717,7 @@ class BuildVolume(SceneNode):
                         break
                 if not prime_tower_collision:
                     result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
+                    result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
                 else:
                     self._error_areas.extend(prime_tower_areas[extruder_id])
 
@@ -716,6 +726,9 @@ class BuildVolume(SceneNode):
         self._disallowed_areas = []
         for extruder_id in result_areas:
             self._disallowed_areas.extend(result_areas[extruder_id])
+        self._disallowed_areas_no_brim = []
+        for extruder_id in result_areas_no_brim:
+            self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id])
 
     ##  Computes the disallowed areas for objects that are printed with print
     #   features.

+ 0 - 23
cura/CuraApplication.py

@@ -1260,29 +1260,6 @@ class CuraApplication(QtApplication):
                     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 not isinstance(node, 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 not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
-                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 a set of nodes given a set of 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

+ 13 - 4
cura/MultiplyObjectsJob.py

@@ -30,11 +30,18 @@ class MultiplyObjectsJob(Job):
         total_progress = len(self._objects) * self._count
         current_progress = 0
 
+        global_container_stack = Application.getInstance().getGlobalContainerStack()
+        machine_width = global_container_stack.getProperty("machine_width", "value")
+        machine_depth = global_container_stack.getProperty("machine_depth", "value")
+
         root = scene.getRoot()
-        arranger = Arrange.create(scene_root=root)
+        scale = 0.5
+        arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale)
         processed_nodes = []
         nodes = []
 
+        not_fit_count = 0
+
         for node in self._objects:
             # If object is part of a group, multiply group
             current_node = node
@@ -46,12 +53,13 @@ class MultiplyObjectsJob(Job):
             processed_nodes.append(current_node)
 
             node_too_big = False
-            if node.getBoundingBox().width < 300 or node.getBoundingBox().depth < 300:
-                offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset=self._min_offset)
+            if node.getBoundingBox().width < machine_width or node.getBoundingBox().depth < machine_depth:
+                offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset = self._min_offset, scale = scale)
             else:
                 node_too_big = True
 
             found_solution_for_all = True
+            arranger.resetLastPriority()
             for i in range(self._count):
                 # We do place the nodes one by one, as we want to yield in between.
                 if not node_too_big:
@@ -59,8 +67,9 @@ class MultiplyObjectsJob(Job):
                 if node_too_big or not solution_found:
                     found_solution_for_all = False
                     new_location = new_node.getPosition()
-                    new_location = new_location.set(z = 100 - i * 20)
+                    new_location = new_location.set(z = - not_fit_count * 20)
                     new_node.setPosition(new_location)
+                    not_fit_count += 1
 
                 # Same build plate
                 build_plate_number = current_node.callDecoration("getBuildPlateNumber")

+ 202 - 5
tests/TestArrange.py

@@ -4,9 +4,17 @@ from cura.Arranging.Arrange import Arrange
 from cura.Arranging.ShapeArray import ShapeArray
 
 
-def gimmeShapeArray():
-    vertices = numpy.array([[-3, 1], [3, 1], [0, -3]])
-    shape_arr = ShapeArray.fromPolygon(vertices)
+##  Triangle of area 12
+def gimmeShapeArray(scale = 1.0):
+    vertices = numpy.array([[-3, 1], [3, 1], [0, -3]], dtype=numpy.int32)
+    shape_arr = ShapeArray.fromPolygon(vertices, scale = scale)
+    return shape_arr
+
+
+##  Boring square
+def gimmeShapeArraySquare(scale = 1.0):
+    vertices = numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32)
+    shape_arr = ShapeArray.fromPolygon(vertices, scale = scale)
     return shape_arr
 
 
@@ -20,6 +28,45 @@ def test_smoke_ShapeArray():
     shape_arr = gimmeShapeArray()
 
 
+##  Test ShapeArray
+def test_ShapeArray():
+    scale = 1
+    ar = Arrange(16, 16, 8, 8, scale = scale)
+    ar.centerFirst()
+
+    shape_arr = gimmeShapeArray(scale)
+    print(shape_arr.arr)
+    count = len(numpy.where(shape_arr.arr == 1)[0])
+    print(count)
+    assert count >= 10  # should approach 12
+
+
+##  Test ShapeArray with scaling
+def test_ShapeArray_scaling():
+    scale = 2
+    ar = Arrange(16, 16, 8, 8, scale = scale)
+    ar.centerFirst()
+
+    shape_arr = gimmeShapeArray(scale)
+    print(shape_arr.arr)
+    count = len(numpy.where(shape_arr.arr == 1)[0])
+    print(count)
+    assert count >= 40  # should approach 2*2*12 = 48
+
+
+##  Test ShapeArray with scaling
+def test_ShapeArray_scaling2():
+    scale = 0.5
+    ar = Arrange(16, 16, 8, 8, scale = scale)
+    ar.centerFirst()
+
+    shape_arr = gimmeShapeArray(scale)
+    print(shape_arr.arr)
+    count = len(numpy.where(shape_arr.arr == 1)[0])
+    print(count)
+    assert count >= 1  # should approach 3, but it can be inaccurate due to pixel rounding
+
+
 ##  Test centerFirst
 def test_centerFirst():
     ar = Arrange(300, 300, 150, 150)
@@ -32,13 +79,33 @@ def test_centerFirst():
     assert ar._priority[150][150] < ar._priority[130][130]
 
 
+##  Test centerFirst
+def test_centerFirst_rectangular():
+    ar = Arrange(400, 300, 200, 150)
+    ar.centerFirst()
+    assert ar._priority[150][200] < ar._priority[150][220]
+    assert ar._priority[150][200] < ar._priority[170][200]
+    assert ar._priority[150][200] < ar._priority[170][220]
+    assert ar._priority[150][200] < ar._priority[180][150]
+    assert ar._priority[150][200] < ar._priority[130][200]
+    assert ar._priority[150][200] < ar._priority[130][180]
+
+
+##  Test centerFirst
+def test_centerFirst_rectangular():
+    ar = Arrange(10, 20, 5, 10)
+    ar.centerFirst()
+    print(ar._priority)
+    assert ar._priority[10][5] < ar._priority[10][7]
+
+
 ##  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][150]
     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][150]
     assert ar._priority[150][150] > ar._priority[130][130]
 
 
@@ -55,6 +122,113 @@ def test_smoke_bestSpot():
     assert hasattr(best_spot, "priority")
 
 
+##  Real life test
+def test_bestSpot():
+    ar = Arrange(16, 16, 8, 8)
+    ar.centerFirst()
+
+    shape_arr = gimmeShapeArray()
+    best_spot = ar.bestSpot(shape_arr)
+    assert best_spot.x == 0
+    assert best_spot.y == 0
+    ar.place(best_spot.x, best_spot.y, shape_arr)
+
+    # Place object a second time
+    best_spot = ar.bestSpot(shape_arr)
+    assert best_spot.x is not None  # we found a location
+    assert best_spot.x != 0 or best_spot.y != 0  # it can't be on the same location
+    ar.place(best_spot.x, best_spot.y, shape_arr)
+
+    print(ar._occupied)  # For debugging
+
+
+##  Real life test rectangular build plate
+def test_bestSpot_rectangular_build_plate():
+    ar = Arrange(16, 40, 8, 20)
+    ar.centerFirst()
+
+    shape_arr = gimmeShapeArray()
+    best_spot = ar.bestSpot(shape_arr)
+    ar.place(best_spot.x, best_spot.y, shape_arr)
+    assert best_spot.x == 0
+    assert best_spot.y == 0
+
+    # Place object a second time
+    best_spot2 = ar.bestSpot(shape_arr)
+    assert best_spot2.x is not None  # we found a location
+    assert best_spot2.x != 0 or best_spot2.y != 0  # it can't be on the same location
+    ar.place(best_spot2.x, best_spot2.y, shape_arr)
+
+    # Place object a 3rd time
+    best_spot3 = ar.bestSpot(shape_arr)
+    assert best_spot3.x is not None  # we found a location
+    assert best_spot3.x != best_spot.x or best_spot3.y != best_spot.y  # it can't be on the same location
+    assert best_spot3.x != best_spot2.x or best_spot3.y != best_spot2.y  # it can't be on the same location
+    ar.place(best_spot3.x, best_spot3.y, shape_arr)
+
+    best_spot_x = ar.bestSpot(shape_arr)
+    ar.place(best_spot_x.x, best_spot_x.y, shape_arr)
+
+    best_spot_x = ar.bestSpot(shape_arr)
+    ar.place(best_spot_x.x, best_spot_x.y, shape_arr)
+
+    best_spot_x = ar.bestSpot(shape_arr)
+    ar.place(best_spot_x.x, best_spot_x.y, shape_arr)
+
+    print(ar._occupied)  # For debugging
+
+
+##  Real life test
+def test_bestSpot_scale():
+    scale = 0.5
+    ar = Arrange(16, 16, 8, 8, scale = scale)
+    ar.centerFirst()
+
+    shape_arr = gimmeShapeArray(scale)
+    best_spot = ar.bestSpot(shape_arr)
+    assert best_spot.x == 0
+    assert best_spot.y == 0
+    ar.place(best_spot.x, best_spot.y, shape_arr)
+
+    print(ar._occupied)
+
+    # Place object a second time
+    best_spot = ar.bestSpot(shape_arr)
+    assert best_spot.x is not None  # we found a location
+    assert best_spot.x != 0 or best_spot.y != 0  # it can't be on the same location
+    ar.place(best_spot.x, best_spot.y, shape_arr)
+
+    print(ar._occupied)  # For debugging
+
+
+##  Real life test
+def test_bestSpot_scale_rectangular():
+    scale = 0.5
+    ar = Arrange(16, 40, 8, 20, scale = scale)
+    ar.centerFirst()
+
+    shape_arr = gimmeShapeArray(scale)
+
+    shape_arr_square = gimmeShapeArraySquare(scale)
+    best_spot = ar.bestSpot(shape_arr_square)
+    assert best_spot.x == 0
+    assert best_spot.y == 0
+    ar.place(best_spot.x, best_spot.y, shape_arr_square)
+
+    print(ar._occupied)
+
+    # Place object a second time
+    best_spot = ar.bestSpot(shape_arr)
+    assert best_spot.x is not None  # we found a location
+    assert best_spot.x != 0 or best_spot.y != 0  # it can't be on the same location
+    ar.place(best_spot.x, best_spot.y, shape_arr)
+
+    best_spot = ar.bestSpot(shape_arr_square)
+    ar.place(best_spot.x, best_spot.y, shape_arr_square)
+
+    print(ar._occupied)  # For debugging
+
+
 ##  Try to place an object and see if something explodes
 def test_smoke_place():
     ar = Arrange(30, 30, 15, 15)
@@ -80,6 +254,20 @@ def test_checkShape():
     assert points3 > points
 
 
+##  See of our center has less penalty points than out of the center
+def test_checkShape_rectangular():
+    ar = Arrange(20, 30, 10, 15)
+    ar.centerFirst()
+    print(ar._priority)
+
+    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
+
+
 ## Check that placing an object on occupied place returns None.
 def test_checkShape_place():
     ar = Arrange(30, 30, 15, 15)
@@ -104,6 +292,13 @@ def test_smoke_place_objects():
         ar.place(best_spot_x, best_spot_y, shape_arr)
 
 
+# Test some internals
+def test_compare_occupied_and_priority_tables():
+    ar = Arrange(10, 15, 5, 7)
+    ar.centerFirst()
+    assert ar._priority.shape == ar._occupied.shape
+
+
 ##  Polygon -> array
 def test_arrayFromPolygon():
     vertices = numpy.array([[-3, 1], [3, 1], [0, -3]])
@@ -145,3 +340,5 @@ def test_check2():
     assert numpy.any(check_array)
     assert not check_array[3][0]
     assert check_array[3][4]
+
+