diff --git a/cura/Arranging/Arrange.py b/cura/Arranging/Arrange.py index a70ccb9f0c..e83bf8e372 100644 --- a/cura/Arranging/Arrange.py +++ b/cura/Arranging/Arrange.py @@ -16,17 +16,20 @@ from collections import namedtuple import numpy import copy -## Return object for bestSpot LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points", "priority"]) +"""Return object for bestSpot""" class Arrange: """ - The Arrange classed is used together with ShapeArray. Use it to find good locations for objects that you try to put + The Arrange classed is used together with :py:class:`cura.Arranging.ShapeArray.ShapeArray`. Use it to find 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. + .. note:: + + Make sure the scale is the same between :py:class:`cura.Arranging.ShapeArray.ShapeArray` objects and the :py:class:`cura.Arranging.Arrange.Arrange` instance. """ + build_volume = None # type: Optional[BuildVolume] def __init__(self, x, y, offset_x, offset_y, scale = 0.5): @@ -42,20 +45,20 @@ class Arrange: self._is_empty = True @classmethod - def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8): - """ - Helper to create an Arranger instance + def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8) -> "Arrange": + """Helper to create an :py:class:`cura.Arranging.Arrange.Arrange` instance Either fill in scene_root and create will find all sliceable nodes by itself, or use fixed_nodes to provide the nodes yourself. - :param scene_root: Root for finding all scene nodes - :param fixed_nodes: Scene nodes to be placed - :param scale: - :param x: - :param y: - :param min_offset: - :return: + + :param scene_root: Root for finding all scene nodes default = None + :param fixed_nodes: Scene nodes to be placed default = None + :param scale: default = 0.5 + :param x: default = 350 + :param y: default = 250 + :param min_offset: default = 8 """ + arranger = Arrange(x, y, x // 2, y // 2, scale = scale) arranger.centerFirst() @@ -90,19 +93,21 @@ class Arrange: arranger.place(0, 0, shape_arr, update_empty = False) return arranger - ## This resets the optimization for finding location based on size def resetLastPriority(self): + """This resets the optimization for finding location based on size""" + self._last_priority = 0 - def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1): - """ - Find placement for a node (using offset shape) and place it (using hull shape) - :param node: - :param offset_shape_arr: hapeArray with offset, for placing the shape - :param hull_shape_arr: ShapeArray without offset, used to find location - :param step: + def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1) -> bool: + """ Find placement for a node (using offset shape) and place it (using hull shape) + + :param node: The node to be placed + :param offset_shape_arr: shape array with offset, for placing the shape + :param hull_shape_arr: shape array without offset, used to find location + :param step: default = 1 :return: the nodes that should be placed """ + best_spot = self.bestSpot( hull_shape_arr, start_prio = self._last_priority, step = step) x, y = best_spot.x, best_spot.y @@ -129,10 +134,8 @@ class Arrange: return found_spot def centerFirst(self): - """ - Fill priority, center is best. Lower value is better. - :return: - """ + """ Fill priority, center is best. Lower value is better. """ + # Square distance: creates a more round shape self._priority = numpy.fromfunction( lambda j, i: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self._shape, dtype=numpy.int32) @@ -140,23 +143,22 @@ class Arrange: self._priority_unique_values.sort() def backFirst(self): - """ - Fill priority, back is best. Lower value is better - :return: - """ + """ Fill priority, back is best. Lower value is better """ + self._priority = numpy.fromfunction( 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() - def checkShape(self, x, y, shape_arr): - """ - Return the amount of "penalty points" for polygon, which is the sum of priority + def checkShape(self, x, y, shape_arr) -> Optional[numpy.ndarray]: + """ Return the amount of "penalty points" for polygon, which is the sum of priority + :param x: x-coordinate to check shape - :param y: - :param shape_arr: the ShapeArray object to place + :param y: y-coordinate to check shape + :param shape_arr: the shape array object to place :return: None if occupied """ + x = int(self._scale * x) y = int(self._scale * y) offset_x = x + self._offset_x + shape_arr.offset_x @@ -180,14 +182,15 @@ class Arrange: offset_x:offset_x + shape_arr.arr.shape[1]] return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)]) - def bestSpot(self, shape_arr, start_prio = 0, step = 1): - """ - Find "best" spot for ShapeArray - :param shape_arr: + def bestSpot(self, shape_arr, start_prio = 0, step = 1) -> LocationSuggestion: + """ Find "best" spot for ShapeArray + + :param shape_arr: shape array :param start_prio: Start with this priority value (and skip the ones before) :param step: Slicing value, higher = more skips = faster but less accurate :return: namedtuple with properties x, y, penalty_points, priority. """ + start_idx_list = numpy.where(self._priority_unique_values == start_prio) if start_idx_list: try: @@ -210,15 +213,16 @@ class Arrange: return LocationSuggestion(x = None, y = None, penalty_points = None, priority = priority) # No suitable location found :-( def place(self, x, y, shape_arr, update_empty = True): - """ - Place the object. + """ Place the object. + Marks the locations in self._occupied and self._priority + :param x: :param y: :param shape_arr: :param update_empty: updates the _is_empty, used when adding disallowed areas - :return: """ + x = int(self._scale * x) y = int(self._scale * y) offset_x = x + self._offset_x + shape_arr.offset_x diff --git a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py index 7736efbeeb..0f337a229b 100644 --- a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py +++ b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py @@ -18,8 +18,9 @@ from cura.Arranging.ShapeArray import ShapeArray from typing import List -## Do arrangements on multiple build plates (aka builtiplexer) class ArrangeArray: + """Do arrangements on multiple build plates (aka builtiplexer)""" + def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]) -> None: self._x = x self._y = y diff --git a/cura/Arranging/ShapeArray.py b/cura/Arranging/ShapeArray.py index 403db5e706..c704ae7ca2 100644 --- a/cura/Arranging/ShapeArray.py +++ b/cura/Arranging/ShapeArray.py @@ -11,19 +11,24 @@ if TYPE_CHECKING: from UM.Scene.SceneNode import SceneNode -## Polygon representation as an array for use with Arrange class ShapeArray: + """Polygon representation as an array for use with :py:class:`cura.Arranging.Arrange.Arrange`""" + def __init__(self, arr: numpy.array, offset_x: float, offset_y: float, scale: float = 1) -> None: self.arr = arr self.offset_x = offset_x self.offset_y = offset_y self.scale = scale - ## Instantiate from a bunch of vertices - # \param vertices - # \param scale scale the coordinates @classmethod def fromPolygon(cls, vertices: numpy.array, scale: float = 1) -> "ShapeArray": + """Instantiate from a bunch of vertices + + :param vertices: + :param scale: scale the coordinates + :return: a shape array instantiated from a bunch of vertices + """ + # scale vertices = vertices * scale # flip y, x -> x, y @@ -44,12 +49,16 @@ class ShapeArray: arr[0][0] = 1 return cls(arr, offset_x, offset_y) - ## Instantiate an offset and hull ShapeArray from a scene node. - # \param node source node where the convex hull must be present - # \param min_offset offset for the offset ShapeArray - # \param scale scale the coordinates @classmethod def fromNode(cls, node: "SceneNode", min_offset: float, scale: float = 0.5, include_children: bool = False) -> Tuple[Optional["ShapeArray"], Optional["ShapeArray"]]: + """Instantiate an offset and hull ShapeArray from a scene node. + + :param node: source node where the convex hull must be present + :param min_offset: offset for the offset ShapeArray + :param scale: scale the coordinates + :return: A tuple containing an offset and hull shape array + """ + transform = node._transformation transform_x = transform._data[0][3] transform_y = transform._data[2][3] @@ -88,14 +97,19 @@ class ShapeArray: return offset_shape_arr, hull_shape_arr - ## Create np.array with dimensions defined by shape - # Fills polygon defined by vertices with ones, all other values zero - # Only works correctly for convex hull vertices - # Originally from: http://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array - # \param shape numpy format shape, [x-size, y-size] - # \param vertices @classmethod def arrayFromPolygon(cls, shape: Tuple[int, int], vertices: numpy.array) -> numpy.array: + """Create :py:class:`numpy.ndarray` with dimensions defined by shape + + Fills polygon defined by vertices with ones, all other values zero + Only works correctly for convex hull vertices + Originally from: `Stackoverflow - generating a filled polygon inside a numpy array `_ + + :param shape: numpy format shape, [x-size, y-size] + :param vertices: + :return: numpy array with dimensions defined by shape + """ + 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 @@ -111,16 +125,21 @@ class ShapeArray: return base_array - ## Return indices that mark one side of the line, used by arrayFromPolygon - # Uses the line defined by p1 and p2 to check array of - # input indices against interpolated value - # Returns boolean array, with True inside and False outside of shape - # Originally from: http://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array - # \param p1 2-tuple with x, y for point 1 - # \param p2 2-tuple with x, y for point 2 - # \param base_array boolean array to project the line on @classmethod def _check(cls, p1: numpy.array, p2: numpy.array, base_array: numpy.array) -> Optional[numpy.array]: + """Return indices that mark one side of the line, used by arrayFromPolygon + + Uses the line defined by p1 and p2 to check array of + input indices against interpolated value + Returns boolean array, with True inside and False outside of shape + Originally from: `Stackoverflow - generating a filled polygon inside a numpy array `_ + + :param p1: 2-tuple with x, y for point 1 + :param p2: 2-tuple with x, y for point 2 + :param base_array: boolean array to project the line on + :return: A numpy array with indices that mark one side of the line + """ + if p1[0] == p2[0] and p1[1] == p2[1]: return None idxs = numpy.indices(base_array.shape) # Create 3D array of indices