mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-06 22:47:29 -06:00
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:
parent
310aee07ac
commit
f5bed242ed
8 changed files with 287 additions and 73 deletions
|
@ -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):
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue