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

This commit is contained in:
Jack Ha 2018-05-22 17:13:35 +02:00
parent 310aee07ac
commit f5bed242ed
8 changed files with 287 additions and 73 deletions

View file

@ -18,17 +18,20 @@ LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points
# good locations for objects that you try to put on a build place. # 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 # Different priority schemes can be defined so it alters the behavior while using
# the same logic. # the same logic.
#
# Note: Make sure the scale is the same between ShapeArray objects and the Arrange instance.
class Arrange: class Arrange:
build_volume = None build_volume = None
def __init__(self, x, y, offset_x, offset_y, scale= 1.0): def __init__(self, x, y, offset_x, offset_y, scale= 0.5):
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._scale = scale # convert input coordinates to arrange coordinates
self._offset_x = offset_x world_x, world_y = int(x * self._scale), int(y * self._scale)
self._offset_y = offset_y 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._last_priority = 0
self._is_empty = True self._is_empty = True
@ -39,7 +42,7 @@ class Arrange:
# \param scene_root Root for finding all scene nodes # \param scene_root Root for finding all scene nodes
# \param fixed_nodes Scene nodes to be placed # \param fixed_nodes Scene nodes to be placed
@classmethod @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 = Arrange(x, y, x // 2, y // 2, scale = scale)
arranger.centerFirst() arranger.centerFirst()
@ -61,13 +64,17 @@ class Arrange:
# If a build volume was set, add the disallowed areas # If a build volume was set, add the disallowed areas
if Arrange.build_volume: if Arrange.build_volume:
disallowed_areas = Arrange.build_volume.getDisallowedAreas() disallowed_areas = Arrange.build_volume.getDisallowedAreasNoBrim()
for area in disallowed_areas: for area in disallowed_areas:
points = copy.deepcopy(area._points) points = copy.deepcopy(area._points)
shape_arr = ShapeArray.fromPolygon(points, scale = scale) shape_arr = ShapeArray.fromPolygon(points, scale = scale)
arranger.place(0, 0, shape_arr, update_empty = False) arranger.place(0, 0, shape_arr, update_empty = False)
return arranger 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) ## Find placement for a node (using offset shape) and place it (using hull shape)
# return the nodes that should be placed # return the nodes that should be placed
# \param node # \param node
@ -104,7 +111,7 @@ class Arrange:
def centerFirst(self): def centerFirst(self):
# Square distance: creates a more round shape # Square distance: creates a more round shape
self._priority = numpy.fromfunction( 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 = numpy.unique(self._priority)
self._priority_unique_values.sort() self._priority_unique_values.sort()
@ -112,7 +119,7 @@ class Arrange:
# This is a strategy for the arranger. # This is a strategy for the arranger.
def backFirst(self): def backFirst(self):
self._priority = numpy.fromfunction( 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 = numpy.unique(self._priority)
self._priority_unique_values.sort() self._priority_unique_values.sort()
@ -126,9 +133,15 @@ class Arrange:
y = int(self._scale * y) y = int(self._scale * y)
offset_x = x + self._offset_x + shape_arr.offset_x offset_x = x + self._offset_x + shape_arr.offset_x
offset_y = y + self._offset_y + shape_arr.offset_y 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[ occupied_slice = self._occupied[
offset_y:offset_y + shape_arr.arr.shape[0], offset_y:occupied_y_max,
offset_x:offset_x + shape_arr.arr.shape[1]] offset_x:occupied_x_max]
try: try:
if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]): if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]):
return None return None
@ -140,7 +153,7 @@ class Arrange:
return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)]) return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
## Find "best" spot for ShapeArray ## 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 shape_arr ShapeArray
# \param start_prio Start with this priority value (and skip the ones before) # \param start_prio Start with this priority value (and skip the ones before)
# \param step Slicing value, higher = more skips = faster but less accurate # \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]: for priority in self._priority_unique_values[start_idx::step]:
tryout_idx = numpy.where(self._priority == priority) tryout_idx = numpy.where(self._priority == priority)
for idx in range(len(tryout_idx[0])): for idx in range(len(tryout_idx[0])):
x = tryout_idx[0][idx] x = tryout_idx[1][idx]
y = tryout_idx[1][idx] y = tryout_idx[0][idx]
projected_x = x - self._offset_x projected_x = int((x - self._offset_x) / self._scale)
projected_y = y - self._offset_y projected_y = int((y - self._offset_y) / self._scale)
# array to "world" coordinates
penalty_points = self.checkShape(projected_x, projected_y, shape_arr) penalty_points = self.checkShape(projected_x, projected_y, shape_arr)
if penalty_points is not None: if penalty_points is not None:
return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = priority) 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. # 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 = self._priority[min_y:max_y, min_x:max_x]
prio_slice[numpy.where(shape_arr.arr[ prio_slice[new_occupied] = 999
min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 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 @property
def isEmpty(self): def isEmpty(self):

View file

@ -1,6 +1,7 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Job import Job from UM.Job import Job
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
@ -17,6 +18,7 @@ from cura.Arranging.ShapeArray import ShapeArray
from typing import List from typing import List
## Do an arrangements on a bunch of build plates
class ArrangeArray: class ArrangeArray:
def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]): def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]):
self._x = x self._x = x
@ -79,7 +81,11 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
nodes_arr.sort(key=lambda item: item[0]) nodes_arr.sort(key=lambda item: item[0])
nodes_arr.reverse() 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 = ArrangeArray(x = x, y = y, fixed_nodes = [])
arrange_array.add() 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 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, # 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). # 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 try_placement = True
current_build_plate_number = 0 # always start with the first one 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: while try_placement:
# make sure that current_build_plate_number is not going crazy or you'll have a lot of arrange objects # 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(): while current_build_plate_number >= arrange_array.count():
arrange_array.add() arrange_array.add()
arranger = arrange_array.get(current_build_plate_number) 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 x, y = best_spot.x, best_spot.y
node.removeDecorator(ZOffsetDecorator) node.removeDecorator(ZOffsetDecorator)
if node.getBoundingBox(): if node.getBoundingBox():

View file

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

View file

@ -74,7 +74,7 @@ class ShapeArray:
# \param vertices # \param vertices
@classmethod @classmethod
def arrayFromPolygon(cls, shape, vertices): 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 fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill

View file

@ -25,6 +25,7 @@ catalog = i18nCatalog("cura")
import numpy import numpy
import math import math
import copy
from typing import List, Optional from typing import List, Optional
@ -61,6 +62,7 @@ class BuildVolume(SceneNode):
self._grid_shader = None self._grid_shader = None
self._disallowed_areas = [] self._disallowed_areas = []
self._disallowed_areas_no_brim = []
self._disallowed_area_mesh = None self._disallowed_area_mesh = None
self._error_areas = [] self._error_areas = []
@ -171,6 +173,9 @@ class BuildVolume(SceneNode):
def getDisallowedAreas(self) -> List[Polygon]: def getDisallowedAreas(self) -> List[Polygon]:
return self._disallowed_areas return self._disallowed_areas
def getDisallowedAreasNoBrim(self) -> List[Polygon]:
return self._disallowed_areas_no_brim
def setDisallowedAreas(self, areas: List[Polygon]): def setDisallowedAreas(self, areas: List[Polygon]):
self._disallowed_areas = areas 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. 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_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. #Check if prime positions intersect with disallowed areas.
for extruder in used_extruders: for extruder in used_extruders:
@ -687,12 +693,15 @@ class BuildVolume(SceneNode):
break break
result_areas[extruder_id].extend(prime_areas[extruder_id]) 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") nozzle_disallowed_areas = extruder.getProperty("nozzle_disallowed_areas", "value")
for area in nozzle_disallowed_areas: for area in nozzle_disallowed_areas:
polygon = Polygon(numpy.array(area, numpy.float32)) polygon = Polygon(numpy.array(area, numpy.float32))
polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size)) polygon_disallowed_border = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
result_areas[extruder_id].append(polygon) #Don't perform the offset on these. 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. # Add prime tower location as disallowed area.
if len(used_extruders) > 1: #No prime tower in single-extrusion. if len(used_extruders) > 1: #No prime tower in single-extrusion.
@ -708,6 +717,7 @@ class BuildVolume(SceneNode):
break break
if not prime_tower_collision: if not prime_tower_collision:
result_areas[extruder_id].extend(prime_tower_areas[extruder_id]) result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
else: else:
self._error_areas.extend(prime_tower_areas[extruder_id]) self._error_areas.extend(prime_tower_areas[extruder_id])
@ -716,6 +726,9 @@ class BuildVolume(SceneNode):
self._disallowed_areas = [] self._disallowed_areas = []
for extruder_id in result_areas: for extruder_id in result_areas:
self._disallowed_areas.extend(result_areas[extruder_id]) 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 ## Computes the disallowed areas for objects that are printed with print
# features. # features.

View file

@ -1260,29 +1260,6 @@ class CuraApplication(QtApplication):
nodes.append(node) nodes.append(node)
self.arrange(nodes, fixed_nodes = []) 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 ## Arrange a set of nodes given a set of fixed nodes
# \param nodes nodes that we have to place # \param nodes nodes that we have to place
# \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes # \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes

View file

@ -30,11 +30,18 @@ class MultiplyObjectsJob(Job):
total_progress = len(self._objects) * self._count total_progress = len(self._objects) * self._count
current_progress = 0 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() 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 = [] processed_nodes = []
nodes = [] nodes = []
not_fit_count = 0
for node in self._objects: for node in self._objects:
# If object is part of a group, multiply group # If object is part of a group, multiply group
current_node = node current_node = node
@ -46,12 +53,13 @@ class MultiplyObjectsJob(Job):
processed_nodes.append(current_node) processed_nodes.append(current_node)
node_too_big = False node_too_big = False
if node.getBoundingBox().width < 300 or node.getBoundingBox().depth < 300: 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) offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset = self._min_offset, scale = scale)
else: else:
node_too_big = True node_too_big = True
found_solution_for_all = True found_solution_for_all = True
arranger.resetLastPriority()
for i in range(self._count): for i in range(self._count):
# We do place the nodes one by one, as we want to yield in between. # We do place the nodes one by one, as we want to yield in between.
if not node_too_big: if not node_too_big:
@ -59,8 +67,9 @@ class MultiplyObjectsJob(Job):
if node_too_big or not solution_found: if node_too_big or not solution_found:
found_solution_for_all = False found_solution_for_all = False
new_location = new_node.getPosition() 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) new_node.setPosition(new_location)
not_fit_count += 1
# Same build plate # Same build plate
build_plate_number = current_node.callDecoration("getBuildPlateNumber") build_plate_number = current_node.callDecoration("getBuildPlateNumber")

View file

@ -4,9 +4,17 @@ from cura.Arranging.Arrange import Arrange
from cura.Arranging.ShapeArray import ShapeArray from cura.Arranging.ShapeArray import ShapeArray
def gimmeShapeArray(): ## Triangle of area 12
vertices = numpy.array([[-3, 1], [3, 1], [0, -3]]) def gimmeShapeArray(scale = 1.0):
shape_arr = ShapeArray.fromPolygon(vertices) 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 return shape_arr
@ -20,6 +28,45 @@ def test_smoke_ShapeArray():
shape_arr = gimmeShapeArray() 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 ## Test centerFirst
def test_centerFirst(): def test_centerFirst():
ar = Arrange(300, 300, 150, 150) ar = Arrange(300, 300, 150, 150)
@ -32,13 +79,33 @@ def test_centerFirst():
assert ar._priority[150][150] < ar._priority[130][130] 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 ## Test backFirst
def test_backFirst(): def test_backFirst():
ar = Arrange(300, 300, 150, 150) ar = Arrange(300, 300, 150, 150)
ar.backFirst() 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[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] assert ar._priority[150][150] > ar._priority[130][130]
@ -55,6 +122,113 @@ def test_smoke_bestSpot():
assert hasattr(best_spot, "priority") 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 ## Try to place an object and see if something explodes
def test_smoke_place(): def test_smoke_place():
ar = Arrange(30, 30, 15, 15) ar = Arrange(30, 30, 15, 15)
@ -80,6 +254,20 @@ def test_checkShape():
assert points3 > points 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. ## Check that placing an object on occupied place returns None.
def test_checkShape_place(): def test_checkShape_place():
ar = Arrange(30, 30, 15, 15) 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) 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 ## Polygon -> array
def test_arrayFromPolygon(): def test_arrayFromPolygon():
vertices = numpy.array([[-3, 1], [3, 1], [0, -3]]) vertices = numpy.array([[-3, 1], [3, 1], [0, -3]])
@ -145,3 +340,5 @@ def test_check2():
assert numpy.any(check_array) assert numpy.any(check_array)
assert not check_array[3][0] assert not check_array[3][0]
assert check_array[3][4] assert check_array[3][4]