Implement factory for Arrange

CURA-7951
This commit is contained in:
c.lamboo 2023-08-23 17:58:00 +02:00
parent 118f49a052
commit 668038c59f
7 changed files with 152 additions and 150 deletions

View file

@ -9,7 +9,7 @@ from UM.Message import Message
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from cura.Arranging.GridArrange import GridArrange from cura.Arranging.GridArrange import GridArrange
from cura.Arranging.Nest2DArrange import arrange from cura.Arranging.Nest2DArrange import Nest2DArrange
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -33,7 +33,13 @@ class ArrangeObjectsJob(Job):
status_message.show() status_message.show()
try: try:
found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes, grid_arrange= self._grid_arrange) if self._grid_arrange:
arranger = GridArrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes)
else:
arranger = Nest2DArrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes,
factor=1000)
found_solution_for_all = arranger.arrange()
except: # If the thread crashes, the message should still close except: # If the thread crashes, the message should still close
Logger.logException("e", "Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.") Logger.logException("e", "Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.")

View file

@ -0,0 +1,27 @@
from typing import List, TYPE_CHECKING, Optional, Tuple, Set
if TYPE_CHECKING:
from UM.Operations.GroupedOperation import GroupedOperation
class Arranger:
def createGroupOperationForArrange(self, add_new_nodes_in_scene: bool = True) -> Tuple["GroupedOperation", int]:
"""
Find placement for a set of scene nodes, but don't actually move them just yet.
:param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations
:return: tuple (found_solution_for_all, node_items)
WHERE
found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects
node_items: A list of the nodes return by libnest2d, which contain the new positions on the buildplate
"""
raise NotImplementedError
def arrange(self, add_new_nodes_in_scene: bool = True) -> bool:
"""
Find placement for a set of scene nodes, and move them by using a single grouped operation.
:param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations
:return: found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects
"""
grouped_operation, not_fit_count = self.createGroupOperationForArrange(add_new_nodes_in_scene)
grouped_operation.push()
return not_fit_count == 0

View file

@ -1,8 +1,6 @@
import math import math
from typing import List, TYPE_CHECKING, Optional, Tuple, Set from typing import List, TYPE_CHECKING, Optional, Tuple, Set
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
@ -12,9 +10,10 @@ from UM.Math.Vector import Vector
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.TranslateOperation import TranslateOperation from UM.Operations.TranslateOperation import TranslateOperation
from cura.Arranging.Arranger import Arranger
class GridArrange: class GridArrange(Arranger):
def __init__(self, nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: List["SceneNode"] = None): def __init__(self, nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: List["SceneNode"] = None):
if fixed_nodes is None: if fixed_nodes is None:
fixed_nodes = [] fixed_nodes = []
@ -38,7 +37,7 @@ class GridArrange:
self._initial_leftover_grid_x = math.floor(self._initial_leftover_grid_x) self._initial_leftover_grid_x = math.floor(self._initial_leftover_grid_x)
self._initial_leftover_grid_y = math.floor(self._initial_leftover_grid_y) self._initial_leftover_grid_y = math.floor(self._initial_leftover_grid_y)
def createGroupOperationForArrange(self) -> Tuple[GroupedOperation, int]: def createGroupOperationForArrange(self, add_new_nodes_in_scene: bool = True) -> Tuple[GroupedOperation, int]:
# Find grid indexes that intersect with fixed objects # Find grid indexes that intersect with fixed objects
fixed_nodes_grid_ids = set() fixed_nodes_grid_ids = set()
for node in self._fixed_nodes: for node in self._fixed_nodes:
@ -77,7 +76,8 @@ class GridArrange:
left_over_grid_y = self._initial_leftover_grid_y left_over_grid_y = self._initial_leftover_grid_y
for node in leftover_nodes: for node in leftover_nodes:
grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root)) if add_new_nodes_in_scene:
grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
# find the first next grid position that isn't occupied by a fixed node # find the first next grid position that isn't occupied by a fixed node
while (self._initial_leftover_grid_x, left_over_grid_y) in fixed_nodes_grid_ids: while (self._initial_leftover_grid_x, left_over_grid_y) in fixed_nodes_grid_ids:
left_over_grid_y = left_over_grid_y - 1 left_over_grid_y = left_over_grid_y - 1
@ -156,7 +156,7 @@ class GridArrange:
# ┌───────┬───────┐ < coord_build_plate_back = -1 # ┌───────┬───────┐ < coord_build_plate_back = -1
# │ │ │ # │ │ │
# │ │(0,0) │ # │ │(0,0) │
# (-1,0)───────o───────┤(1,0) # (-1,0)───────o───────┤(1,0)
# │ │ │ # │ │ │
# │ │ │ # │ │ │
# └───────┴───────┘ < coord_build_plate_front = +1 # └───────┴───────┘ < coord_build_plate_front = +1

View file

@ -15,131 +15,122 @@ from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.RotateOperation import RotateOperation from UM.Operations.RotateOperation import RotateOperation
from UM.Operations.TranslateOperation import TranslateOperation from UM.Operations.TranslateOperation import TranslateOperation
from cura.Arranging.GridArrange import GridArrange from cura.Arranging.Arranger import Arranger
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from cura.BuildVolume import BuildVolume from cura.BuildVolume import BuildVolume
def findNodePlacement( class Nest2DArrange(Arranger):
nodes_to_arrange: List["SceneNode"], def __init__(self,
build_volume: "BuildVolume", nodes_to_arrange: List["SceneNode"],
fixed_nodes: Optional[List["SceneNode"]] = None, build_volume: "BuildVolume",
factor: int = 10000, fixed_nodes: Optional[List["SceneNode"]] = None,
*, *,
lock_rotation: bool = False factor: int = 10000,
) -> Tuple[bool, List[Item]]: lock_rotation: bool = False):
""" """
Find placement for a set of scene nodes, but don't actually move them just yet. :param nodes_to_arrange: The list of nodes that need to be moved.
:param nodes_to_arrange: The list of nodes that need to be moved. :param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this.
:param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this. :param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes
:param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes are placed.
are placed. :param factor: The library that we use is int based. This factor defines how accuracte we want it to be.
:param factor: The library that we use is int based. This factor defines how accurate we want it to be. :param lock_rotation: If set to true the orientation of the object will remain the same
:param lock_rotation: If set to true the orientation of the object will remain the same """
super().__init__()
self._nodes_to_arrange = nodes_to_arrange
self._build_volume = build_volume
self._fixed_nodes = fixed_nodes
self._factor = factor
self._lock_rotation = lock_rotation
:return: tuple (found_solution_for_all, node_items) def findNodePlacement(self) -> Tuple[bool, List[Item]]:
WHERE spacing = int(1.5 * self._factor) # 1.5mm spacing.
found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects
node_items: A list of the nodes return by libnest2d, which contain the new positions on the buildplate
"""
spacing = int(1.5 * factor) # 1.5mm spacing.
machine_width = build_volume.getWidth() machine_width = self._build_volume.getWidth()
machine_depth = build_volume.getDepth() machine_depth = self._build_volume.getDepth()
build_plate_bounding_box = Box(int(machine_width * factor), int(machine_depth * factor)) build_plate_bounding_box = Box(int(machine_width * self._factor), int(machine_depth * self._factor))
if fixed_nodes is None: if self._fixed_nodes is None:
fixed_nodes = [] self._fixed_nodes = []
# Add all the items we want to arrange # Add all the items we want to arrange
node_items = [] node_items = []
for node in nodes_to_arrange: for node in self._nodes_to_arrange:
hull_polygon = node.callDecoration("getConvexHull") hull_polygon = node.callDecoration("getConvexHull")
if not hull_polygon or hull_polygon.getPoints is None: if not hull_polygon or hull_polygon.getPoints is None:
Logger.log("w", "Object {} cannot be arranged because it has no convex hull.".format(node.getName())) Logger.log("w", "Object {} cannot be arranged because it has no convex hull.".format(node.getName()))
continue continue
converted_points = [] converted_points = []
for point in hull_polygon.getPoints():
converted_points.append(Point(int(point[0] * factor), int(point[1] * factor)))
item = Item(converted_points)
node_items.append(item)
# Use a tiny margin for the build_plate_polygon (the nesting doesn't like overlapping disallowed areas)
half_machine_width = 0.5 * machine_width - 1
half_machine_depth = 0.5 * machine_depth - 1
build_plate_polygon = Polygon(numpy.array([
[half_machine_width, -half_machine_depth],
[-half_machine_width, -half_machine_depth],
[-half_machine_width, half_machine_depth],
[half_machine_width, half_machine_depth]
], numpy.float32))
disallowed_areas = build_volume.getDisallowedAreas()
num_disallowed_areas_added = 0
for area in disallowed_areas:
converted_points = []
# Clip the disallowed areas so that they don't overlap the bounding box (The arranger chokes otherwise)
clipped_area = area.intersectionConvexHulls(build_plate_polygon)
if clipped_area.getPoints() is not None and len(clipped_area.getPoints()) > 2: # numpy array has to be explicitly checked against None
for point in clipped_area.getPoints():
converted_points.append(Point(int(point[0] * factor), int(point[1] * factor)))
disallowed_area = Item(converted_points)
disallowed_area.markAsDisallowedAreaInBin(0)
node_items.append(disallowed_area)
num_disallowed_areas_added += 1
for node in fixed_nodes:
converted_points = []
hull_polygon = node.callDecoration("getConvexHull")
if hull_polygon is not None and hull_polygon.getPoints() is not None and len(hull_polygon.getPoints()) > 2: # numpy array has to be explicitly checked against None
for point in hull_polygon.getPoints(): for point in hull_polygon.getPoints():
converted_points.append(Point(int(point[0] * factor), int(point[1] * factor))) converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor)))
item = Item(converted_points) item = Item(converted_points)
item.markAsFixedInBin(0)
node_items.append(item) node_items.append(item)
num_disallowed_areas_added += 1
config = NfpConfig() # Use a tiny margin for the build_plate_polygon (the nesting doesn't like overlapping disallowed areas)
config.accuracy = 1.0 half_machine_width = 0.5 * machine_width - 1
config.alignment = NfpConfig.Alignment.DONT_ALIGN half_machine_depth = 0.5 * machine_depth - 1
if lock_rotation: build_plate_polygon = Polygon(numpy.array([
config.rotations = [0.0] [half_machine_width, -half_machine_depth],
[-half_machine_width, -half_machine_depth],
[-half_machine_width, half_machine_depth],
[half_machine_width, half_machine_depth]
], numpy.float32))
num_bins = nest(node_items, build_plate_bounding_box, spacing, config) disallowed_areas = self._build_volume.getDisallowedAreas()
num_disallowed_areas_added = 0
for area in disallowed_areas:
converted_points = []
# Strip the fixed items (previously placed) and the disallowed areas from the results again. # Clip the disallowed areas so that they don't overlap the bounding box (The arranger chokes otherwise)
node_items = list(filter(lambda item: not item.isFixed(), node_items)) clipped_area = area.intersectionConvexHulls(build_plate_polygon)
found_solution_for_all = num_bins == 1 if clipped_area.getPoints() is not None and len(
clipped_area.getPoints()) > 2: # numpy array has to be explicitly checked against None
for point in clipped_area.getPoints():
converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor)))
return found_solution_for_all, node_items disallowed_area = Item(converted_points)
disallowed_area.markAsDisallowedAreaInBin(0)
node_items.append(disallowed_area)
num_disallowed_areas_added += 1
for node in self._fixed_nodes:
converted_points = []
hull_polygon = node.callDecoration("getConvexHull")
def createGroupOperationForArrange(nodes_to_arrange: List["SceneNode"], if hull_polygon is not None and hull_polygon.getPoints() is not None and len(
build_volume: "BuildVolume", hull_polygon.getPoints()) > 2: # numpy array has to be explicitly checked against None
fixed_nodes: Optional[List["SceneNode"]] = None, for point in hull_polygon.getPoints():
factor: int = 10000, converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor)))
*, item = Item(converted_points)
add_new_nodes_in_scene: bool = False, item.markAsFixedInBin(0)
lock_rotation: bool = False, node_items.append(item)
grid_arrange: bool = False) -> Tuple[GroupedOperation, int]: num_disallowed_areas_added += 1
if grid_arrange:
grid = GridArrange(nodes_to_arrange, build_volume, fixed_nodes) config = NfpConfig()
return grid.createGroupOperationForArrange() config.accuracy = 1.0
else: config.alignment = NfpConfig.Alignment.DONT_ALIGN
if self._lock_rotation:
config.rotations = [0.0]
num_bins = nest(node_items, build_plate_bounding_box, spacing, config)
# Strip the fixed items (previously placed) and the disallowed areas from the results again.
node_items = list(filter(lambda item: not item.isFixed(), node_items))
found_solution_for_all = num_bins == 1
return found_solution_for_all, node_items
def createGroupOperationForArrange(self, add_new_nodes_in_scene: bool = True) -> Tuple[GroupedOperation, int]:
scene_root = Application.getInstance().getController().getScene().getRoot() scene_root = Application.getInstance().getController().getScene().getRoot()
found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor, found_solution_for_all, node_items = self.findNodePlacement()
lock_rotation = lock_rotation)
not_fit_count = 0 not_fit_count = 0
grouped_operation = GroupedOperation() grouped_operation = GroupedOperation()
for node, node_item in zip(nodes_to_arrange, node_items): for node, node_item in zip(self._nodes_to_arrange, node_items):
if add_new_nodes_in_scene: if add_new_nodes_in_scene:
grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root)) grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
@ -148,40 +139,13 @@ def createGroupOperationForArrange(nodes_to_arrange: List["SceneNode"],
rotation_matrix = Matrix() rotation_matrix = Matrix()
rotation_matrix.setByRotationAxis(node_item.rotation(), Vector(0, -1, 0)) rotation_matrix.setByRotationAxis(node_item.rotation(), Vector(0, -1, 0))
grouped_operation.addOperation(RotateOperation(node, Quaternion.fromMatrix(rotation_matrix))) grouped_operation.addOperation(RotateOperation(node, Quaternion.fromMatrix(rotation_matrix)))
grouped_operation.addOperation(TranslateOperation(node, Vector(node_item.translation().x() / factor, 0, grouped_operation.addOperation(
node_item.translation().y() / factor))) TranslateOperation(node, Vector(node_item.translation().x() / self._factor, 0,
node_item.translation().y() / self._factor)))
else: else:
# We didn't find a spot # We didn't find a spot
grouped_operation.addOperation( grouped_operation.addOperation(
TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True)) TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True))
not_fit_count += 1 not_fit_count += 1
return grouped_operation, not_fit_count return grouped_operation, not_fit_count
def arrange(
nodes_to_arrange: List["SceneNode"],
build_volume: "BuildVolume",
fixed_nodes: Optional[List["SceneNode"]] = None,
factor=10000,
add_new_nodes_in_scene: bool = False,
lock_rotation: bool = False,
grid_arrange: bool = False
) -> bool:
"""
Find placement for a set of scene nodes, and move them by using a single grouped operation.
:param nodes_to_arrange: The list of nodes that need to be moved.
:param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this.
:param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes
are placed.
:param factor: The library that we use is int based. This factor defines how accuracte we want it to be.
:param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations
:param lock_rotation: If set to true the orientation of the object will remain the same
:return: found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects
"""
grouped_operation, not_fit_count = createGroupOperationForArrange(nodes_to_arrange, build_volume, fixed_nodes,
factor, add_new_nodes_in_scene = add_new_nodes_in_scene, lock_rotation = lock_rotation, grid_arrange = grid_arrange)
grouped_operation.push()
return not_fit_count == 0

View file

@ -22,7 +22,10 @@ from cura.Operations.SetParentOperation import SetParentOperation
from cura.MultiplyObjectsJob import MultiplyObjectsJob from cura.MultiplyObjectsJob import MultiplyObjectsJob
from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from cura.Arranging.Nest2DArrange import createGroupOperationForArrange
from cura.Arranging.GridArrange import GridArrange
from cura.Arranging.Nest2DArrange import Nest2DArrange
from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation
@ -238,7 +241,9 @@ class CuraActions(QObject):
if node.callDecoration("isSliceable"): if node.callDecoration("isSliceable"):
fixed_nodes.append(node) fixed_nodes.append(node)
# Add the new nodes to the scene, and arrange them # Add the new nodes to the scene, and arrange them
group_operation, not_fit_count = createGroupOperationForArrange(nodes, application.getBuildVolume(), fixed_nodes, grid_arrange = True)
arranger = GridArrange(nodes, application.getBuildVolume(), fixed_nodes)
group_operation, not_fit_count = arranger.createGroupOperationForArrange()
group_operation.push() group_operation.push()
# deselect currently selected nodes, and select the new nodes # deselect currently selected nodes, and select the new nodes

View file

@ -54,7 +54,6 @@ from cura import ApplicationMetadata
from cura.API import CuraAPI from cura.API import CuraAPI
from cura.API.Account import Account from cura.API.Account import Account
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
from cura.Arranging.Nest2DArrange import arrange
from cura.Machines.MachineErrorChecker import MachineErrorChecker from cura.Machines.MachineErrorChecker import MachineErrorChecker
from cura.Machines.Models.BuildPlateModel import BuildPlateModel from cura.Machines.Models.BuildPlateModel import BuildPlateModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel

View file

@ -14,7 +14,8 @@ from UM.Operations.TranslateOperation import TranslateOperation
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from cura.Arranging.Nest2DArrange import createGroupOperationForArrange from cura.Arranging.GridArrange import GridArrange
from cura.Arranging.Nest2DArrange import Nest2DArrange
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -77,12 +78,12 @@ class MultiplyObjectsJob(Job):
found_solution_for_all = True found_solution_for_all = True
group_operation = GroupedOperation() group_operation = GroupedOperation()
if nodes: if nodes:
group_operation, not_fit_count = createGroupOperationForArrange(nodes, if self._grid_arrange:
Application.getInstance().getBuildVolume(), arranger = GridArrange(nodes, Application.getInstance().getBuildVolume(), fixed_nodes)
fixed_nodes, else:
factor=10000, arranger = Nest2DArrange(nodes, Application.getInstance().getBuildVolume(), fixed_nodes, factor=1000)
add_new_nodes_in_scene=True,
grid_arrange=self._grid_arrange) group_operation, not_fit_count = arranger.createGroupOperationForArrange(add_new_nodes_in_scene=True)
if nodes_to_add_without_arrange: if nodes_to_add_without_arrange:
for nested_node in nodes_to_add_without_arrange: for nested_node in nodes_to_add_without_arrange: