Find optimal offset for grid arrange

CURA-7951
This commit is contained in:
c.lamboo 2023-08-24 08:33:59 +02:00
parent 668038c59f
commit f67a6970dd

View file

@ -1,11 +1,11 @@
import math import math
from typing import List, TYPE_CHECKING, Optional, Tuple, Set from typing import List, TYPE_CHECKING, Tuple, Set
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from cura.BuildVolume import BuildVolume
from UM.Application import Application from UM.Application import Application
from UM.Math import AxisAlignedBox
from UM.Math.Vector import Vector 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
@ -22,14 +22,25 @@ class GridArrange(Arranger):
self._build_volume_bounding_box = build_volume.getBoundingBox() self._build_volume_bounding_box = build_volume.getBoundingBox()
self._fixed_nodes = fixed_nodes self._fixed_nodes = fixed_nodes
self._offset_x: float = 10 self._margin_x: float = 1
self._offset_y: float = 10 self._margin_y: float = 1
self._grid_width = 0 self._grid_width = 0
self._grid_height = 0 self._grid_height = 0
for node in self._nodes_to_arrange: for node in self._nodes_to_arrange:
bounding_box = node.getBoundingBox() bounding_box = node.getBoundingBox()
self._grid_width = max(self._grid_width, bounding_box.width) self._grid_width = max(self._grid_width, bounding_box.width)
self._grid_height = max(self._grid_height, bounding_box.depth) self._grid_height = max(self._grid_height, bounding_box.depth)
self._grid_width += self._margin_x
self._grid_height += self._margin_y
# Round up the grid size to the nearest cm
self._grid_width = math.ceil(self._grid_width / 10) * 10
self._grid_height = math.ceil(self._grid_height / 10) * 10
self._offset_x = 0
self._offset_y = 0
self._findOptimalGridOffset()
coord_initial_leftover_x = self._build_volume_bounding_box.right + 2 * self._grid_width 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 coord_initial_leftover_y = (self._build_volume_bounding_box.back + self._build_volume_bounding_box.front) * 0.5
@ -37,39 +48,38 @@ class GridArrange(Arranger):
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, 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() self._fixed_nodes_grid_ids = set()
for node in self._fixed_nodes: for node in self._fixed_nodes:
fixed_nodes_grid_ids = fixed_nodes_grid_ids.union(self.intersectingGridIdxInclusive(node.getBoundingBox())) self._fixed_nodes_grid_ids = self._fixed_nodes_grid_ids.union(
self.intersectingGridIdxInclusive(node.getBoundingBox()))
build_plate_grid_ids = self.intersectingGridIdxExclusive(self._build_volume_bounding_box) self._build_plate_grid_ids = self.intersectingGridIdxExclusive(self._build_volume_bounding_box)
# Filter out the corner grid squares if the build plate shape is elliptic # Filter out the corner grid squares if the build plate shape is elliptic
if self._build_volume.getShape() == "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)) self._build_plate_grid_ids = set(
filter(lambda grid_id: self.checkGridUnderDiscSpace(grid_id[0], grid_id[1]),
self._build_plate_grid_ids))
allowed_grid_idx = build_plate_grid_ids.difference(fixed_nodes_grid_ids) self._allowed_grid_idx = self._build_plate_grid_ids.difference(self._fixed_nodes_grid_ids)
def createGroupOperationForArrange(self, add_new_nodes_in_scene: bool = True) -> Tuple[GroupedOperation, int]:
# Find the sequence in which items are placed # 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_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 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) 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: sequence: List[Tuple[int, int]] = list(self._allowed_grid_idx)
grid_x, grid_y = grid_id sequence.sort(key=lambda grid_id: (grid_build_plate_center_x - grid_id[0]) ** 2 + (
distance_squared = (grid_build_plate_center_x - grid_x) ** 2 + (grid_build_plate_center_y - grid_y) ** 2 grid_build_plate_center_y - grid_id[1]) ** 2)
return distance_squared
sequence: List[Tuple[int, int]] = list(allowed_grid_idx)
sequence.sort(key=distToCenter)
scene_root = Application.getInstance().getController().getScene().getRoot() scene_root = Application.getInstance().getController().getScene().getRoot()
grouped_operation = GroupedOperation() grouped_operation = GroupedOperation()
for grid_id, node in zip(sequence, self._nodes_to_arrange): for grid_id, node in zip(sequence, self._nodes_to_arrange):
grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root)) grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
grid_x, grid_y = grid_id grid_x, grid_y = grid_id
operation = self.moveNodeOnGrid(node, grid_x, grid_y) operation = self._moveNodeOnGrid(node, grid_x, grid_y)
grouped_operation.addOperation(operation) grouped_operation.addOperation(operation)
leftover_nodes = self._nodes_to_arrange[len(sequence):] leftover_nodes = self._nodes_to_arrange[len(sequence):]
@ -79,18 +89,103 @@ class GridArrange(Arranger):
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))
# 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 self._fixed_nodes_grid_ids:
left_over_grid_y = left_over_grid_y - 1 left_over_grid_y = left_over_grid_y - 1
operation = self.moveNodeOnGrid(node, self._initial_leftover_grid_x, left_over_grid_y) operation = self._moveNodeOnGrid(node, self._initial_leftover_grid_x, left_over_grid_y)
grouped_operation.addOperation(operation) grouped_operation.addOperation(operation)
left_over_grid_y = left_over_grid_y - 1 left_over_grid_y = left_over_grid_y - 1
return grouped_operation, len(leftover_nodes) return grouped_operation, len(leftover_nodes)
def moveNodeOnGrid(self, node: "SceneNode", grid_x: int, grid_y: int) -> "Operation.Operation": def _findOptimalGridOffset(self):
coord_grid_x, coord_grid_y = self.gridSpaceToCoordSpace(grid_x, grid_y) if len(self._fixed_nodes) == 0:
center_grid_x = coord_grid_x + (0.5 * (self._grid_width + self._offset_x)) self._offset_x = 0
center_grid_y = coord_grid_y + (0.5 * (self._grid_height + self._offset_y)) self._offset_y = 0
return
if len(self._fixed_nodes) == 1:
center_grid_x = 0.5 * self._grid_width + self._build_volume_bounding_box.left
center_grid_y = 0.5 * self._grid_height + self._build_volume_bounding_box.back
bounding_box = self._fixed_nodes[0].getBoundingBox()
center_node_x = (bounding_box.left + bounding_box.right) * 0.5
center_node_y = (bounding_box.back + bounding_box.front) * 0.5
self._offset_x = center_node_x - center_grid_x
self._offset_y = center_node_y - center_grid_y
return
class Event:
def __init__(self, coord: float, change: float):
self.coord = coord
self.change = change
events_horizontal: List[Event] = []
events_vertical: List[Event] = []
for node in self._fixed_nodes:
bounding_box = node.getBoundingBox()
left = bounding_box.left - self._build_volume_bounding_box.left
right = bounding_box.right - self._build_volume_bounding_box.left
back = bounding_box.back - self._build_volume_bounding_box.back
front = bounding_box.front - self._build_volume_bounding_box.back
value_left = math.ceil(left / self._grid_width) * self._grid_width - left
value_right = math.ceil(right / self._grid_width) * self._grid_width - right
value_back = math.ceil(back / self._grid_height) * self._grid_height - back
value_front = math.ceil(front / self._grid_height) * self._grid_height - front
# give nodes a weight according to their size. This
# weight is heuristically chosen to be proportional to
# the number of grid squares the node-boundary occupies
weight = bounding_box.width + bounding_box.depth
events_horizontal.append(Event(value_left, weight))
events_horizontal.append(Event(value_right, -weight))
events_vertical.append(Event(value_back, weight))
events_vertical.append(Event(value_front, -weight))
events_horizontal.sort(key=lambda event: event.coord)
events_vertical.sort(key=lambda event: event.coord)
def findOptimalOffsetAxis(events: List[Event], interval: float) -> float:
prev_coord = events[-1].coord - interval
current_offset = 0
best_offset = float('inf')
best_coord_length = float('-inf')
best_coord = 0.0
for event in events:
coord_length = event.coord - prev_coord
if current_offset < best_offset or (current_offset == best_offset and coord_length > best_coord_length):
best_offset = current_offset
best_coord_length = coord_length
best_coord = event.coord
current_offset += event.change
prev_coord = event.coord
return best_coord - best_coord_length * 0.5
center_grid_x = 0.5 * self._grid_width
center_grid_y = 0.5 * self._grid_height
optimal_center_x = self._grid_width - findOptimalOffsetAxis(events_horizontal, self._grid_width)
optimal_center_y = self._grid_height - findOptimalOffsetAxis(events_vertical, self._grid_height)
self._offset_x = optimal_center_x - center_grid_x
self._offset_y = optimal_center_y - center_grid_y
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)
center_grid_y = coord_grid_y + (0.5 * self._grid_height)
bounding_box = node.getBoundingBox() bounding_box = node.getBoundingBox()
center_node_x = (bounding_box.left + bounding_box.right) * 0.5 center_node_x = (bounding_box.left + bounding_box.right) * 0.5
@ -101,7 +196,7 @@ class GridArrange(Arranger):
return TranslateOperation(node, Vector(delta_x, 0, delta_y)) return TranslateOperation(node, Vector(delta_x, 0, delta_y))
def getGridCornerPoints(self, bounding_box: "BoundingVolume") -> Tuple[float, float, float, float]: def _getGridCornerPoints(self, bounding_box: "BoundingVolume") -> Tuple[float, float, float, float]:
coord_x1 = bounding_box.left coord_x1 = bounding_box.left
coord_x2 = bounding_box.right coord_x2 = bounding_box.right
coord_y1 = bounding_box.back coord_y1 = bounding_box.back
@ -111,7 +206,7 @@ class GridArrange(Arranger):
return grid_x1, grid_y1, grid_x2, grid_y2 return grid_x1, grid_y1, grid_x2, grid_y2
def intersectingGridIdxInclusive(self, bounding_box: "BoundingVolume") -> Set[Tuple[int, int]]: def intersectingGridIdxInclusive(self, bounding_box: "BoundingVolume") -> Set[Tuple[int, int]]:
grid_x1, grid_y1, grid_x2, grid_y2 = self.getGridCornerPoints(bounding_box) grid_x1, grid_y1, grid_x2, grid_y2 = self._getGridCornerPoints(bounding_box)
grid_idx = set() grid_idx = set()
for grid_x in range(math.floor(grid_x1), math.ceil(grid_x2)): 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)): for grid_y in range(math.floor(grid_y1), math.ceil(grid_y2)):
@ -119,26 +214,26 @@ class GridArrange(Arranger):
return grid_idx return grid_idx
def intersectingGridIdxExclusive(self, bounding_box: "BoundingVolume") -> Set[Tuple[int, int]]: def intersectingGridIdxExclusive(self, bounding_box: "BoundingVolume") -> Set[Tuple[int, int]]:
grid_x1, grid_y1, grid_x2, grid_y2 = self.getGridCornerPoints(bounding_box) grid_x1, grid_y1, grid_x2, grid_y2 = self._getGridCornerPoints(bounding_box)
grid_idx = set() grid_idx = set()
for grid_x in range(math.ceil(grid_x1), math.floor(grid_x2)): 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)): for grid_y in range(math.ceil(grid_y1), math.floor(grid_y2)):
grid_idx.add((grid_x, grid_y)) grid_idx.add((grid_x, grid_y))
return grid_idx return grid_idx
def gridSpaceToCoordSpace(self, x: float, y: float) -> Tuple[float, float]: 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_x = x * self._grid_width + self._build_volume_bounding_box.left + self._offset_x
grid_y = y * (self._grid_height + self._offset_y) + self._build_volume_bounding_box.back grid_y = y * self._grid_height + self._build_volume_bounding_box.back + self._offset_y
return grid_x, grid_y return grid_x, grid_y
def coordSpaceToGridSpace(self, grid_x: float, grid_y: float) -> Tuple[float, float]: 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_x = (grid_x - self._build_volume_bounding_box.left - self._offset_x) / self._grid_width
coord_y = (grid_y - self._build_volume_bounding_box.back) / (self._grid_height + self._offset_y) coord_y = (grid_y - self._build_volume_bounding_box.back - self._offset_y) / self._grid_height
return coord_x, coord_y return coord_x, coord_y
def checkGridUnderDiscSpace(self, grid_x: int, grid_y: int) -> bool: def checkGridUnderDiscSpace(self, grid_x: int, grid_y: int) -> bool:
left, back = self.gridSpaceToCoordSpace(grid_x, grid_y) left, back = self._gridSpaceToCoordSpace(grid_x, grid_y)
right, front = self.gridSpaceToCoordSpace(grid_x + 1, grid_y + 1) right, front = self._gridSpaceToCoordSpace(grid_x + 1, grid_y + 1)
corners = [(left, back), (right, back), (right, front), (left, front)] corners = [(left, back), (right, back), (right, front), (left, front)]
return all([self.checkPointUnderDiscSpace(x, y) for x, y in corners]) return all([self.checkPointUnderDiscSpace(x, y) for x, y in corners])
@ -165,15 +260,14 @@ class GridArrange(Arranger):
disc_y = ((y - self._build_volume_bounding_box.back) / self._build_volume_bounding_box.depth) * 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 return disc_x, disc_y
def drawDebugSvg(self): def _drawDebugSvg(self):
with open("Builvolume_test.svg", "w") as f: with open("Builvolume_test.svg", "w") as f:
build_volume_bounding_box = self._build_volume_bounding_box build_volume_bounding_box = self._build_volume_bounding_box
f.write( f.write(
f"<svg xmlns='http://www.w3.org/2000/svg' viewBox='{build_volume_bounding_box.left - 100} {build_volume_bounding_box.back - 100} {build_volume_bounding_box.width + 200} {build_volume_bounding_box.depth + 200}'>\n") f"<svg xmlns='http://www.w3.org/2000/svg' viewBox='{build_volume_bounding_box.left - 100} {build_volume_bounding_box.back - 100} {build_volume_bounding_box.width + 200} {build_volume_bounding_box.depth + 200}'>\n")
ellipse = True if self._build_volume.getShape() == "elliptic":
if ellipse:
f.write( f.write(
f""" f"""
<ellipse <ellipse
@ -181,7 +275,7 @@ class GridArrange(Arranger):
cy='{(build_volume_bounding_box.back + build_volume_bounding_box.front) * 0.5}' cy='{(build_volume_bounding_box.back + build_volume_bounding_box.front) * 0.5}'
rx='{build_volume_bounding_box.width * 0.5}' rx='{build_volume_bounding_box.width * 0.5}'
ry='{build_volume_bounding_box.depth * 0.5}' ry='{build_volume_bounding_box.depth * 0.5}'
fill=\"blue\" fill=\"lightgrey\"
/> />
""") """)
else: else:
@ -196,30 +290,32 @@ class GridArrange(Arranger):
/> />
""") """)
for grid_x in range(0, 100): for grid_x in range(-10, 10):
for grid_y in range(0, 100): for grid_y in range(-10, 10):
# if (grid_x, grid_y) in intersecting_grid_idx: if (grid_x, grid_y) in self._allowed_grid_idx:
# fill_color = "red" fill_color = "rgba(0, 255, 0, 0.5)"
# elif (grid_x, grid_y) in build_plate_grid_idx: elif (grid_x, grid_y) in self._build_plate_grid_ids:
# fill_color = "green" fill_color = "rgba(255, 165, 0, 0.5)"
# else: else:
# fill_color = "orange" fill_color = "rgba(255, 0, 0, 0.5)"
coord_grid_x, coord_grid_y = self.gridSpaceToCoordSpace(grid_x, grid_y) coord_grid_x, coord_grid_y = self._gridSpaceToCoordSpace(grid_x, grid_y)
f.write( f.write(
f""" f"""
<rect <rect
x="{coord_grid_x}" x="{coord_grid_x + self._margin_x * 0.5}"
y="{coord_grid_y}" y="{coord_grid_y + self._margin_y * 0.5}"
width="{self._grid_width}" width="{self._grid_width - self._margin_x}"
height="{self._grid_height}" height="{self._grid_height - self._margin_y}"
fill="#ff00ff88" fill="{fill_color}"
stroke="black" stroke="black"
/> />
""") """)
f.write(f""" f.write(f"""
<text <text
font-size="8" font-size="4"
text-anchor="middle"
alignment-baseline="middle"
x="{coord_grid_x + self._grid_width * 0.5}" x="{coord_grid_x + self._grid_width * 0.5}"
y="{coord_grid_y + self._grid_height * 0.5}" y="{coord_grid_y + self._grid_height * 0.5}"
> >
@ -237,24 +333,25 @@ class GridArrange(Arranger):
fill="red" fill="red"
/> />
""") """)
for node in self._nodes_to_arrange:
bounding_box = node.getBoundingBox()
f.write(f"""
<rect
x="{bounding_box.left}"
y="{bounding_box.back}"
width="{bounding_box.width}"
height="{bounding_box.depth}"
fill="rgba(0,0,0,0.1)"
stroke="blue"
stroke-width="3"
/>
""")
for x in range(math.floor(self._build_volume_bounding_box.left), math.floor(self._build_volume_bounding_box.right), 50): f.write(f"""
for y in range(math.floor(self._build_volume_bounding_box.back), math.floor(self._build_volume_bounding_box.front), 50): <circle
color = "green" if self.checkPointUnderDiscSpace(x, y) else "red" cx="{self._offset_x}"
f.write(f""" cy="{self._offset_y}"
<circle cx="{x}" cy="{y}" r="10" fill="{color}" /> r="2"
""") stroke="red"
f.write(f"</svg>") fill="none"
/>""")
# 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
# f.write(f"""
# <circle
# cx="{coord_build_plate_center_x}"
# cy="{coord_build_plate_center_y}"
# r="2"
# stroke="blue"
# fill="none"
# />""")
f.write(f"</svg>")