import math from typing import List, TYPE_CHECKING, Optional, Tuple, Set if TYPE_CHECKING: from UM.Scene.SceneNode import SceneNode from UM.Application import Application from UM.Math import AxisAlignedBox from UM.Math.Vector import Vector from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.TranslateOperation import TranslateOperation class GridArrange: def __init__(self, nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: List["SceneNode"] = None): if fixed_nodes is None: fixed_nodes = [] self._nodes_to_arrange = nodes_to_arrange self._build_volume = build_volume self._build_volume_bounding_box = build_volume.getBoundingBox() self._fixed_nodes = fixed_nodes self._offset_x: float = 10 self._offset_y: float = 10 self._grid_width = 0 self._grid_height = 0 for node in self._nodes_to_arrange: bounding_box = node.getBoundingBox() self._grid_width = max(self._grid_width, bounding_box.width) self._grid_height = max(self._grid_height, bounding_box.depth) coord_initial_leftover_x = self._build_volume_bounding_box.right + 2 * self._grid_width coord_initial_leftover_y = (self._build_volume_bounding_box.back + self._build_volume_bounding_box.front) * 0.5 self._initial_leftover_grid_x, self._initial_leftover_grid_y = self.coordSpaceToGridSpace(coord_initial_leftover_x, coord_initial_leftover_y) self._initial_leftover_grid_x = math.floor(self._initial_leftover_grid_x) self._initial_leftover_grid_y = math.floor(self._initial_leftover_grid_y) def createGroupOperationForArrange(self) -> Tuple[GroupedOperation, int]: # Find grid indexes that intersect with fixed objects fixed_nodes_grid_ids = set() for node in self._fixed_nodes: fixed_nodes_grid_ids = fixed_nodes_grid_ids.union(self.intersectingGridIdxInclusive(node.getBoundingBox())) build_plate_grid_ids = self.intersectingGridIdxExclusive(self._build_volume_bounding_box) # Filter out the corner grid squares if the build plate shape is elliptic if self._build_volume.getShape() == "elliptic": build_plate_grid_ids = set(filter(lambda grid_id: self.checkGridUnderDiscSpace(grid_id[0], grid_id[1]), build_plate_grid_ids)) allowed_grid_idx = build_plate_grid_ids.difference(fixed_nodes_grid_ids) # Find the sequence in which items are placed coord_build_plate_center_x = self._build_volume_bounding_box.width * 0.5 + self._build_volume_bounding_box.left coord_build_plate_center_y = self._build_volume_bounding_box.depth * 0.5 + self._build_volume_bounding_box.back grid_build_plate_center_x, grid_build_plate_center_y = self.coordSpaceToGridSpace(coord_build_plate_center_x, coord_build_plate_center_y) def distToCenter(grid_id: Tuple[int, int]) -> float: grid_x, grid_y = grid_id distance_squared = (grid_build_plate_center_x - grid_x) ** 2 + (grid_build_plate_center_y - grid_y) ** 2 return distance_squared sequence: List[Tuple[int, int]] = list(allowed_grid_idx) sequence.sort(key=distToCenter) scene_root = Application.getInstance().getController().getScene().getRoot() grouped_operation = GroupedOperation() for grid_id, node in zip(sequence, self._nodes_to_arrange): grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root)) grid_x, grid_y = grid_id operation = self.moveNodeOnGrid(node, grid_x, grid_y) grouped_operation.addOperation(operation) leftover_nodes = self._nodes_to_arrange[len(sequence):] left_over_grid_y = self._initial_leftover_grid_y for node in leftover_nodes: grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root)) # 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: left_over_grid_y = left_over_grid_y - 1 operation = self.moveNodeOnGrid(node, self._initial_leftover_grid_x, left_over_grid_y) grouped_operation.addOperation(operation) left_over_grid_y = left_over_grid_y - 1 return grouped_operation, len(leftover_nodes) def moveNodeOnGrid(self, node: "SceneNode", grid_x: int, grid_y: int) -> "Operation.Operation": coord_grid_x, coord_grid_y = self.gridSpaceToCoordSpace(grid_x, grid_y) center_grid_x = coord_grid_x + (0.5 * (self._grid_width + self._offset_x)) center_grid_y = coord_grid_y + (0.5 * (self._grid_height + self._offset_y)) bounding_box = node.getBoundingBox() center_node_x = (bounding_box.left + bounding_box.right) * 0.5 center_node_y = (bounding_box.back + bounding_box.front) * 0.5 delta_x = center_grid_x - center_node_x delta_y = center_grid_y - center_node_y return TranslateOperation(node, Vector(delta_x, 0, delta_y)) def getGridCornerPoints(self, bounding_box: "BoundingVolume") -> Tuple[float, float, float, float]: coord_x1 = bounding_box.left coord_x2 = bounding_box.right coord_y1 = bounding_box.back coord_y2 = bounding_box.front grid_x1, grid_y1 = self.coordSpaceToGridSpace(coord_x1, coord_y1) grid_x2, grid_y2 = self.coordSpaceToGridSpace(coord_x2, coord_y2) return grid_x1, grid_y1, grid_x2, grid_y2 def intersectingGridIdxInclusive(self, bounding_box: "BoundingVolume") -> Set[Tuple[int, int]]: grid_x1, grid_y1, grid_x2, grid_y2 = self.getGridCornerPoints(bounding_box) grid_idx = set() for grid_x in range(math.floor(grid_x1), math.ceil(grid_x2)): for grid_y in range(math.floor(grid_y1), math.ceil(grid_y2)): grid_idx.add((grid_x, grid_y)) return grid_idx def intersectingGridIdxExclusive(self, bounding_box: "BoundingVolume") -> Set[Tuple[int, int]]: grid_x1, grid_y1, grid_x2, grid_y2 = self.getGridCornerPoints(bounding_box) grid_idx = set() for grid_x in range(math.ceil(grid_x1), math.floor(grid_x2)): for grid_y in range(math.ceil(grid_y1), math.floor(grid_y2)): grid_idx.add((grid_x, grid_y)) return grid_idx def gridSpaceToCoordSpace(self, x: float, y: float) -> Tuple[float, float]: grid_x = x * (self._grid_width + self._offset_x) + self._build_volume_bounding_box.left grid_y = y * (self._grid_height + self._offset_y) + self._build_volume_bounding_box.back return grid_x, grid_y def coordSpaceToGridSpace(self, grid_x: float, grid_y: float) -> Tuple[float, float]: coord_x = (grid_x - self._build_volume_bounding_box.left) / (self._grid_width + self._offset_x) coord_y = (grid_y - self._build_volume_bounding_box.back) / (self._grid_height + self._offset_y) return coord_x, coord_y def checkGridUnderDiscSpace(self, grid_x: int, grid_y: int) -> bool: left, back = self.gridSpaceToCoordSpace(grid_x, grid_y) right, front = self.gridSpaceToCoordSpace(grid_x + 1, grid_y + 1) corners = [(left, back), (right, back), (right, front), (left, front)] return all([self.checkPointUnderDiscSpace(x, y) for x, y in corners]) def checkPointUnderDiscSpace(self, x: float, y: float) -> bool: disc_x, disc_y = self.coordSpaceToDiscSpace(x, y) distance_to_center_squared = disc_x ** 2 + disc_y ** 2 return distance_to_center_squared <= 1.0 def coordSpaceToDiscSpace(self, x: float, y: float) -> Tuple[float, float]: # Transform coordinate system to # # coord_build_plate_left = -1 # | coord_build_plate_right = 1 # v (0,1) v # ┌───────┬───────┐ < coord_build_plate_back = -1 # │ │ │ # │ │(0,0) │ # (-1,0)│───────o───────┤(1,0) # │ │ │ # │ │ │ # └───────┴───────┘ < coord_build_plate_front = +1 # (0,-1) disc_x = ((x - self._build_volume_bounding_box.left) / self._build_volume_bounding_box.width) * 2.0 - 1.0 disc_y = ((y - self._build_volume_bounding_box.back) / self._build_volume_bounding_box.depth) * 2.0 - 1.0 return disc_x, disc_y def drawDebugSvg(self): with open("Builvolume_test.svg", "w") as f: build_volume_bounding_box = self._build_volume_bounding_box f.write( f"\n") ellipse = True if ellipse: f.write( f""" """) else: f.write( f""" """) for grid_x in range(0, 100): for grid_y in range(0, 100): # if (grid_x, grid_y) in intersecting_grid_idx: # fill_color = "red" # elif (grid_x, grid_y) in build_plate_grid_idx: # fill_color = "green" # else: # fill_color = "orange" coord_grid_x, coord_grid_y = self.gridSpaceToCoordSpace(grid_x, grid_y) f.write( f""" """) f.write(f""" {grid_x},{grid_y} """) for node in self._fixed_nodes: bounding_box = node.getBoundingBox() f.write(f""" """) for node in self._nodes_to_arrange: bounding_box = node.getBoundingBox() f.write(f""" """) for x in range(math.floor(self._build_volume_bounding_box.left), math.floor(self._build_volume_bounding_box.right), 50): for y in range(math.floor(self._build_volume_bounding_box.back), math.floor(self._build_volume_bounding_box.front), 50): color = "green" if self.checkPointUnderDiscSpace(x, y) else "red" f.write(f""" """) f.write(f"")