mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-08-07 05:53:59 -06:00
Merge pull request #3846 from Ultimaker/improve-arranger
CURA-5370 Small refactor for Arranger:
This commit is contained in:
commit
fb307fbbf5
9 changed files with 338 additions and 85 deletions
|
@ -1,3 +1,6 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Logger import Logger
|
||||
from UM.Math.Vector import Vector
|
||||
|
@ -18,17 +21,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 +45,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 +67,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 +114,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 +122,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 +136,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 +156,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 +169,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 +206,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):
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Copyright (c) 2018 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 arrangements on multiple build plates (aka builtiplexer)
|
||||
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():
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -455,7 +460,7 @@ class BuildVolume(SceneNode):
|
|||
minimum = Vector(min_w, min_h - 1.0, min_d),
|
||||
maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d))
|
||||
|
||||
bed_adhesion_size = self._getEdgeDisallowedSize()
|
||||
bed_adhesion_size = self.getEdgeDisallowedSize()
|
||||
|
||||
# As this works better for UM machines, we only add the disallowed_area_size for the z direction.
|
||||
# This is probably wrong in all other cases. TODO!
|
||||
|
@ -647,7 +652,7 @@ class BuildVolume(SceneNode):
|
|||
|
||||
extruder_manager = ExtruderManager.getInstance()
|
||||
used_extruders = extruder_manager.getUsedExtruderStacks()
|
||||
disallowed_border_size = self._getEdgeDisallowedSize()
|
||||
disallowed_border_size = self.getEdgeDisallowedSize()
|
||||
|
||||
if not used_extruders:
|
||||
# If no extruder is used, assume that the active extruder is used (else nothing is drawn)
|
||||
|
@ -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.
|
||||
|
@ -949,12 +962,12 @@ class BuildVolume(SceneNode):
|
|||
all_values[i] = 0
|
||||
return all_values
|
||||
|
||||
## Convenience function to calculate the disallowed radius around the edge.
|
||||
## Calculate the disallowed radius around the edge.
|
||||
#
|
||||
# This disallowed radius is to allow for space around the models that is
|
||||
# not part of the collision radius, such as bed adhesion (skirt/brim/raft)
|
||||
# and travel avoid distance.
|
||||
def _getEdgeDisallowedSize(self):
|
||||
def getEdgeDisallowedSize(self):
|
||||
if not self._global_container_stack or not self._global_container_stack.extruders:
|
||||
return 0
|
||||
|
||||
|
|
|
@ -73,7 +73,8 @@ class CuraActions(QObject):
|
|||
# \param count The number of times to multiply the selection.
|
||||
@pyqtSlot(int)
|
||||
def multiplySelection(self, count: int) -> None:
|
||||
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = 8)
|
||||
min_offset = Application.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
|
||||
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8))
|
||||
job.start()
|
||||
|
||||
## Delete all selected objects.
|
||||
|
|
|
@ -1276,34 +1276,12 @@ 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
|
||||
def arrange(self, nodes, fixed_nodes):
|
||||
job = ArrangeObjectsJob(nodes, fixed_nodes)
|
||||
min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
|
||||
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
|
||||
job.start()
|
||||
|
||||
## Reload all mesh data on the screen from file.
|
||||
|
@ -1657,7 +1635,6 @@ class CuraApplication(QtApplication):
|
|||
if(original_node.getScale() != Vector(1.0, 1.0, 1.0)):
|
||||
node.scale(original_node.getScale())
|
||||
|
||||
|
||||
node.setSelectable(True)
|
||||
node.setName(os.path.basename(filename))
|
||||
self.getBuildVolume().checkBoundsAndUpdate(node)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -4,9 +4,27 @@ 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 gimmeTriangle():
|
||||
return numpy.array([[-3, 1], [3, 1], [0, -3]], dtype=numpy.int32)
|
||||
|
||||
|
||||
## Boring square
|
||||
def gimmeSquare():
|
||||
return numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32)
|
||||
|
||||
|
||||
## Triangle of area 12
|
||||
def gimmeShapeArray(scale = 1.0):
|
||||
vertices = gimmeTriangle()
|
||||
shape_arr = ShapeArray.fromPolygon(vertices, scale = scale)
|
||||
return shape_arr
|
||||
|
||||
|
||||
## Boring square
|
||||
def gimmeShapeArraySquare(scale = 1.0):
|
||||
vertices = gimmeSquare()
|
||||
shape_arr = ShapeArray.fromPolygon(vertices, scale = scale)
|
||||
return shape_arr
|
||||
|
||||
|
||||
|
@ -20,9 +38,48 @@ 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)
|
||||
ar = Arrange(300, 300, 150, 150, scale = 1)
|
||||
ar.centerFirst()
|
||||
assert ar._priority[150][150] < ar._priority[170][150]
|
||||
assert ar._priority[150][150] < ar._priority[150][170]
|
||||
|
@ -32,19 +89,39 @@ def test_centerFirst():
|
|||
assert ar._priority[150][150] < ar._priority[130][130]
|
||||
|
||||
|
||||
## Test centerFirst
|
||||
def test_centerFirst_rectangular():
|
||||
ar = Arrange(400, 300, 200, 150, scale = 1)
|
||||
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, scale = 1)
|
||||
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 = Arrange(300, 300, 150, 150, scale = 1)
|
||||
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]
|
||||
|
||||
|
||||
## See if the result of bestSpot has the correct form
|
||||
def test_smoke_bestSpot():
|
||||
ar = Arrange(30, 30, 15, 15)
|
||||
ar = Arrange(30, 30, 15, 15, scale = 1)
|
||||
ar.centerFirst()
|
||||
|
||||
shape_arr = gimmeShapeArray()
|
||||
|
@ -55,6 +132,113 @@ def test_smoke_bestSpot():
|
|||
assert hasattr(best_spot, "priority")
|
||||
|
||||
|
||||
## Real life test
|
||||
def test_bestSpot():
|
||||
ar = Arrange(16, 16, 8, 8, scale = 1)
|
||||
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, scale = 1)
|
||||
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 +264,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)
|
||||
|
@ -95,7 +293,7 @@ def test_checkShape_place():
|
|||
|
||||
## Test the whole sequence
|
||||
def test_smoke_place_objects():
|
||||
ar = Arrange(20, 20, 10, 10)
|
||||
ar = Arrange(20, 20, 10, 10, scale = 1)
|
||||
ar.centerFirst()
|
||||
shape_arr = gimmeShapeArray()
|
||||
|
||||
|
@ -104,6 +302,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 +350,30 @@ def test_check2():
|
|||
assert numpy.any(check_array)
|
||||
assert not check_array[3][0]
|
||||
assert check_array[3][4]
|
||||
|
||||
|
||||
## Just adding some stuff to ensure fromNode works as expected. Some parts should actually be in UM
|
||||
def test_parts_of_fromNode():
|
||||
from UM.Math.Polygon import Polygon
|
||||
p = Polygon(numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32))
|
||||
offset = 1
|
||||
print(p._points)
|
||||
p_offset = p.getMinkowskiHull(Polygon.approximatedCircle(offset))
|
||||
print("--------------")
|
||||
print(p_offset._points)
|
||||
assert len(numpy.where(p_offset._points[:, 0] >= 2.9)) > 0
|
||||
assert len(numpy.where(p_offset._points[:, 0] <= -2.9)) > 0
|
||||
assert len(numpy.where(p_offset._points[:, 1] >= 2.9)) > 0
|
||||
assert len(numpy.where(p_offset._points[:, 1] <= -2.9)) > 0
|
||||
|
||||
|
||||
def test_parts_of_fromNode2():
|
||||
from UM.Math.Polygon import Polygon
|
||||
p = Polygon(numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32) * 2) # 4x4
|
||||
offset = 13.3
|
||||
scale = 0.5
|
||||
p_offset = p.getMinkowskiHull(Polygon.approximatedCircle(offset))
|
||||
shape_arr1 = ShapeArray.fromPolygon(p._points, scale = scale)
|
||||
shape_arr2 = ShapeArray.fromPolygon(p_offset._points, scale = scale)
|
||||
assert shape_arr1.arr.shape[0] >= (4 * scale) - 1 # -1 is to account for rounding errors
|
||||
assert shape_arr2.arr.shape[0] >= (2 * offset + 4) * scale - 1
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue