Arranger: moved functions, split Arrange into Arrange All and Arrange Selection. CURA-3239

This commit is contained in:
Jack Ha 2017-04-03 10:40:04 +02:00
parent 9db816b0fc
commit abb5d1e76e
4 changed files with 181 additions and 159 deletions

View File

@ -1,6 +1,9 @@
import numpy as np
import numpy
from UM.Math.Polygon import Polygon
## Some polygon converted to an array
## Polygon representation as an array
#
class ShapeArray:
def __init__(self, arr, offset_x, offset_y, scale = 1):
self.arr = arr
@ -9,39 +12,59 @@ class ShapeArray:
self.scale = scale
@classmethod
def from_polygon(cls, vertices, scale = 1):
def fromPolygon(cls, vertices, scale = 1):
# scale
vertices = vertices * scale
# flip y, x -> x, y
flip_vertices = np.zeros((vertices.shape))
flip_vertices = numpy.zeros((vertices.shape))
flip_vertices[:, 0] = vertices[:, 1]
flip_vertices[:, 1] = vertices[:, 0]
flip_vertices = flip_vertices[::-1]
# offset, we want that all coordinates have positive values
offset_y = int(np.amin(flip_vertices[:, 0]))
offset_x = int(np.amin(flip_vertices[:, 1]))
flip_vertices[:, 0] = np.add(flip_vertices[:, 0], -offset_y)
flip_vertices[:, 1] = np.add(flip_vertices[:, 1], -offset_x)
shape = [int(np.amax(flip_vertices[:, 0])), int(np.amax(flip_vertices[:, 1]))]
arr = cls.array_from_polygon(shape, flip_vertices)
offset_y = int(numpy.amin(flip_vertices[:, 0]))
offset_x = int(numpy.amin(flip_vertices[:, 1]))
flip_vertices[:, 0] = numpy.add(flip_vertices[:, 0], -offset_y)
flip_vertices[:, 1] = numpy.add(flip_vertices[:, 1], -offset_x)
shape = [int(numpy.amax(flip_vertices[:, 0])), int(numpy.amax(flip_vertices[:, 1]))]
arr = cls.arrayFromPolygon(shape, flip_vertices)
return cls(arr, offset_x, offset_y)
## Return an offset and hull ShapeArray from a scenenode.
@classmethod
def fromNode(cls, node, min_offset, scale = 0.5):
# hacky way to undo transformation
transform = node._transformation
transform_x = transform._data[0][3]
transform_y = transform._data[2][3]
hull_verts = node.callDecoration("getConvexHull")
offset_verts = hull_verts.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
offset_points = copy.deepcopy(offset_verts._points) # x, y
offset_points[:, 0] = numpy.add(offset_points[:, 0], -transform_x)
offset_points[:, 1] = numpy.add(offset_points[:, 1], -transform_y)
offset_shape_arr = ShapeArray.fromPolygon(offset_points, scale = scale)
hull_points = copy.deepcopy(hull_verts._points)
hull_points[:, 0] = numpy.add(hull_points[:, 0], -transform_x)
hull_points[:, 1] = numpy.add(hull_points[:, 1], -transform_y)
hull_shape_arr = ShapeArray.fromPolygon(hull_points, scale = scale) # x, y
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
@classmethod
def array_from_polygon(cls, shape, vertices):
"""
Creates np.array with dimensions defined by shape
Fills polygon defined by vertices with ones, all other values zero
def arrayFromPolygon(cls, shape, vertices):
base_array = numpy.zeros(shape, dtype=float) # Initialize your array of zeros
Only works correctly for convex hull vertices
"""
base_array = np.zeros(shape, dtype=float) # Initialize your array of zeros
fill = np.ones(base_array.shape) * True # Initialize boolean array defining shape fill
fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill
# Create check array for each edge segment, combine into fill array
for k in range(vertices.shape[0]):
fill = np.all([fill, cls._check(vertices[k - 1], vertices[k], base_array)], axis=0)
fill = numpy.all([fill, cls._check(vertices[k - 1], vertices[k], base_array)], axis=0)
# Set all values inside polygon to one
base_array[fill] = 1
@ -51,43 +74,102 @@ class ShapeArray:
## Return indices that mark one side of the line, used by array_from_polygon
# 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
@classmethod
def _check(cls, p1, p2, base_array):
if p1[0] == p2[0] and p1[1] == p2[1]:
return
idxs = np.indices(base_array.shape) # Create 3D array of indices
idxs = numpy.indices(base_array.shape) # Create 3D array of indices
p1 = p1.astype(float)
p2 = p2.astype(float)
if p2[0] == p1[0]:
sign = np.sign(p2[1] - p1[1])
sign = numpy.sign(p2[1] - p1[1])
return idxs[1] * sign
if p2[1] == p1[1]:
sign = np.sign(p2[0] - p1[0])
sign = numpy.sign(p2[0] - p1[0])
return idxs[1] * sign
# Calculate max column idx for each row idx based on interpolated line between two points
max_col_idx = (idxs[0] - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]) + p1[1]
sign = np.sign(p2[0] - p1[0])
sign = numpy.sign(p2[0] - p1[0])
return idxs[1] * sign <= max_col_idx * sign
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Logger import Logger
import copy
class Arrange:
def __init__(self, x, y, offset_x, offset_y, scale=1):
self.shape = (y, x)
self._priority = np.zeros((x, y), dtype=np.int32)
self._priority = numpy.zeros((x, y), dtype=numpy.int32)
self._priority_unique_values = []
self._occupied = np.zeros((x, y), dtype=np.int32)
self._occupied = numpy.zeros((x, y), dtype=numpy.int32)
self._scale = scale # convert input coordinates to arrange coordinates
self._offset_x = offset_x
self._offset_y = offset_y
## Helper to create an Arranger 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
# \param fixed_nodes
@classmethod
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5):
arranger = Arrange(220, 220, 110, 110, scale = scale)
arranger.centerFirst()
if fixed_nodes is None:
fixed_nodes = []
for node_ in DepthFirstIterator(scene_root):
# Only count sliceable objects
if node_.callDecoration("isSliceable"):
fixed_nodes.append(node_)
# place all objects fixed nodes
for fixed_node in fixed_nodes:
Logger.log("d", " # Placing [%s]" % str(fixed_node))
vertices = fixed_node.callDecoration("getConvexHull")
points = copy.deepcopy(vertices._points)
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
arranger.place(0, 0, shape_arr)
Logger.log("d", "Current buildplate: \n%s" % str(arranger._occupied[::10, ::10]))
return arranger
## Find placement for a node and place it
#
def findNodePlacements(self, node, offset_shape_arr, hull_shape_arr, count = 1, step = 1):
# offset_shape_arr, hull_shape_arr, arranger -> nodes, arranger
nodes = []
start_prio = 0
for i in range(count):
new_node = copy.deepcopy(node)
Logger.log("d", " # Finding spot for %s" % new_node)
x, y, penalty_points, start_prio = self.bestSpot(
offset_shape_arr, start_prio = start_prio, step = step)
transformation = new_node._transformation
if x is not None: # We could find a place
transformation._data[0][3] = x
transformation._data[2][3] = y
Logger.log("d", "Best place is: %s %s (points = %s)" % (x, y, penalty_points))
self.place(x, y, hull_shape_arr) # take place before the next one
Logger.log("d", "New buildplate: \n%s" % str(self._occupied[::10, ::10]))
else:
Logger.log("d", "Could not find spot!")
transformation._data[0][3] = 200
transformation._data[2][3] = -100 + i * 20
nodes.append(new_node)
return nodes
## Fill priority, take offset as center. lower is better
def centerFirst(self):
# Distance x + distance y
@ -96,16 +178,16 @@ class Arrange:
# Square distance
# self._priority = np.fromfunction(
# lambda i, j: abs(self._offset_x-i)**2+abs(self._offset_y-j)**2, self.shape, dtype=np.int32)
self._priority = np.fromfunction(
lambda i, j: abs(self._offset_x-i)**3+abs(self._offset_y-j)**3, self.shape, dtype=np.int32)
self._priority = numpy.fromfunction(
lambda i, j: abs(self._offset_x-i)**3+abs(self._offset_y-j)**3, self.shape, dtype=numpy.int32)
# self._priority = np.fromfunction(
# lambda i, j: max(abs(self._offset_x-i), abs(self._offset_y-j)), self.shape, dtype=np.int32)
self._priority_unique_values = np.unique(self._priority)
self._priority_unique_values = numpy.unique(self._priority)
self._priority_unique_values.sort()
## Return the amount of "penalty points" for polygon, which is the sum of priority
# 999999 if occupied
def check_shape(self, x, y, shape_arr):
def checkShape(self, x, y, shape_arr):
x = int(self._scale * x)
y = int(self._scale * y)
offset_x = x + self._offset_x + shape_arr.offset_x
@ -114,24 +196,24 @@ class Arrange:
offset_y:offset_y + shape_arr.arr.shape[0],
offset_x:offset_x + shape_arr.arr.shape[1]]
try:
if np.any(occupied_slice[np.where(shape_arr.arr == 1)]):
if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]):
return 999999
except IndexError: # out of bounds if you try to place an object outside
return 999999
prio_slice = self._priority[
offset_y:offset_y + shape_arr.arr.shape[0],
offset_x:offset_x + shape_arr.arr.shape[1]]
return np.sum(prio_slice[np.where(shape_arr.arr == 1)])
return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
## Find "best" spot
## Find "best" spot for ShapeArray
def bestSpot(self, shape_arr, start_prio = 0, step = 1):
start_idx_list = np.where(self._priority_unique_values == start_prio)
start_idx_list = numpy.where(self._priority_unique_values == start_prio)
if start_idx_list:
start_idx = start_idx_list[0]
else:
start_idx = 0
for prio in self._priority_unique_values[start_idx::step]:
tryout_idx = np.where(self._priority == prio)
tryout_idx = numpy.where(self._priority == prio)
for idx in range(len(tryout_idx[0])):
x = tryout_idx[0][idx]
y = tryout_idx[1][idx]
@ -139,7 +221,7 @@ class Arrange:
projected_y = y - self._offset_y
# array to "world" coordinates
penalty_points = self.check_shape(projected_x, projected_y, shape_arr)
penalty_points = self.checkShape(projected_x, projected_y, shape_arr)
if penalty_points != 999999:
return projected_x, projected_y, penalty_points, prio
return None, None, None, prio # No suitable location found :-(
@ -158,10 +240,10 @@ class Arrange:
max_y = min(max(offset_y + shape_arr.arr.shape[0], 0), shape_y - 1)
occupied_slice = self._occupied[min_y:max_y, min_x:max_x]
# we use a slice of shape because it can be out of bounds
occupied_slice[np.where(shape_arr.arr[
occupied_slice[numpy.where(shape_arr.arr[
min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 1
# 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[np.where(shape_arr.arr[
prio_slice[numpy.where(shape_arr.arr[
min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 999

View File

@ -14,7 +14,6 @@ from UM.Math.Matrix import Matrix
from UM.Resources import Resources
from UM.Scene.ToolHandle import ToolHandle
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Math.Polygon import Polygon
from UM.Mesh.ReadMeshJob import ReadMeshJob
from UM.Logger import Logger
from UM.Preferences import Preferences
@ -846,79 +845,6 @@ class CuraApplication(QtApplication):
op.push()
## Testing, prepare arranger for use
def _prepareArranger(self, fixed_nodes = None):
#arranger = Arrange(215, 215, 107, 107) # TODO: fill in dimensions
scale = 0.5
arranger = Arrange(220, 220, 110, 110, scale = scale) # TODO: fill in dimensions
arranger.centerFirst()
if fixed_nodes is None:
fixed_nodes = []
root = self.getController().getScene().getRoot()
for node_ in DepthFirstIterator(root):
# Only count sliceable objects
if node_.callDecoration("isSliceable"):
fixed_nodes.append(node_)
# place all objects fixed nodes
for fixed_node in fixed_nodes:
Logger.log("d", " # Placing [%s]" % str(fixed_node))
vertices = fixed_node.callDecoration("getConvexHull")
points = copy.deepcopy(vertices._points)
shape_arr = ShapeArray.from_polygon(points, scale=scale)
arranger.place(0, 0, shape_arr)
Logger.log("d", "Current buildplate: \n%s" % str(arranger._occupied[::10, ::10]))
return arranger
@classmethod
def _nodeAsShapeArr(cls, node, min_offset):
scale = 0.5
# hacky way to undo transformation
transform = node._transformation
transform_x = transform._data[0][3]
transform_y = transform._data[2][3]
hull_verts = node.callDecoration("getConvexHull")
offset_verts = hull_verts.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
offset_points = copy.deepcopy(offset_verts._points) # x, y
offset_points[:, 0] = numpy.add(offset_points[:, 0], -transform_x)
offset_points[:, 1] = numpy.add(offset_points[:, 1], -transform_y)
offset_shape_arr = ShapeArray.from_polygon(offset_points, scale = scale)
hull_points = copy.deepcopy(hull_verts._points)
hull_points[:, 0] = numpy.add(hull_points[:, 0], -transform_x)
hull_points[:, 1] = numpy.add(hull_points[:, 1], -transform_y)
hull_shape_arr = ShapeArray.from_polygon(hull_points, scale = scale) # x, y
return offset_shape_arr, hull_shape_arr
@classmethod
def _findNodePlacements(cls, arranger, node, offset_shape_arr, hull_shape_arr, count = 1, step = 1):
# offset_shape_arr, hull_shape_arr, arranger -> nodes, arranger
nodes = []
start_prio = 0
for i in range(count):
new_node = copy.deepcopy(node)
Logger.log("d", " # Finding spot for %s" % new_node)
x, y, penalty_points, start_prio = arranger.bestSpot(
offset_shape_arr, start_prio = start_prio, step = step)
transformation = new_node._transformation
if x is not None: # We could find a place
transformation._data[0][3] = x
transformation._data[2][3] = y
Logger.log("d", "Best place is: %s %s (points = %s)" % (x, y, penalty_points))
arranger.place(x, y, hull_shape_arr) # take place before the next one
Logger.log("d", "New buildplate: \n%s" % str(arranger._occupied[::10, ::10]))
else:
Logger.log("d", "Could not find spot!")
transformation._data[0][3] = 200
transformation._data[2][3] = -100 + i * 20
nodes.append(new_node)
return nodes
## Create a number of copies of existing object.
# object_id
# count: number of copies
@ -930,9 +856,10 @@ class CuraApplication(QtApplication):
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
node = Selection.getSelectedObject(0)
arranger = self._prepareArranger()
offset_shape_arr, hull_shape_arr = self._nodeAsShapeArr(node, min_offset = min_offset)
nodes = self._findNodePlacements(arranger, node, offset_shape_arr, hull_shape_arr, count = count)
root = self.getController().getScene().getRoot()
arranger = Arrange.create(scene_root = root)
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset)
nodes = arranger.findNodePlacements(node, offset_shape_arr, hull_shape_arr, count = count)
if nodes:
current_node = node
@ -945,7 +872,6 @@ class CuraApplication(QtApplication):
op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent()))
op.push()
## Center object on platform.
@pyqtSlot("quint64")
def centerObject(self, object_id):
@ -1064,42 +990,47 @@ class CuraApplication(QtApplication):
## Testing: arrange selected objects or all objects
@pyqtSlot()
def arrange(self):
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 type(node) is not 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
fixed_nodes.append(node)
self.arrange(nodes, fixed_nodes)
@pyqtSlot()
def arrangeAll(self):
nodes = []
fixed_nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if type(node) is not 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
nodes.append(node)
self.arrange(nodes, fixed_nodes)
## Arrange the nodes, given fixed nodes
def arrange(self, nodes, fixed_nodes):
min_offset = 8
if Selection.getAllSelectedObjects():
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 type(node) is not 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
fixed_nodes.append(node)
else:
nodes = []
fixed_nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if type(node) is not 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
nodes.append(node)
arranger = self._prepareArranger(fixed_nodes = fixed_nodes)
arranger = Arrange.create(fixed_nodes = fixed_nodes)
nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
for node in nodes:
offset_shape_arr, hull_shape_arr = self._nodeAsShapeArr(node, min_offset = min_offset)
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset)
nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr))
nodes_arr.sort(key = lambda item: item[0])
@ -1383,7 +1314,8 @@ class CuraApplication(QtApplication):
filename = job.getFileName()
self._currently_loading_files.remove(filename)
arranger = self._prepareArranger()
root = self.getController().getScene().getRoot()
arranger = Arrange.create(scene_root = root)
min_offset = 8
for node in nodes:
@ -1411,9 +1343,9 @@ class CuraApplication(QtApplication):
node.addDecorator(ConvexHullDecorator())
# find node location
offset_shape_arr, hull_shape_arr = self._nodeAsShapeArr(node, min_offset = min_offset)
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset)
# step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher
nodes = self._findNodePlacements(arranger, node, offset_shape_arr, hull_shape_arr, count = 1, step = 10)
nodes = arranger.findNodePlacements(node, offset_shape_arr, hull_shape_arr, count = 1, step = 10)
for new_node in nodes:
op = AddSceneNodeOperation(new_node, scene.getRoot())

View File

@ -31,7 +31,8 @@ Item
property alias selectAll: selectAllAction;
property alias deleteAll: deleteAllAction;
property alias reloadAll: reloadAllAction;
property alias arrange: arrangeAction;
property alias arrangeAll: arrangeAllAction;
property alias arrangeSelection: arrangeSelectionAction;
property alias resetAllTranslation: resetAllTranslationAction;
property alias resetAll: resetAllAction;
@ -269,9 +270,16 @@ Item
Action
{
id: arrangeAction;
text: catalog.i18nc("@action:inmenu menubar:edit","Arrange");
onTriggered: Printer.arrange();
id: arrangeAllAction;
text: catalog.i18nc("@action:inmenu menubar:edit","Arrange All");
onTriggered: Printer.arrangeAll();
}
Action
{
id: arrangeSelectionAction;
text: catalog.i18nc("@action:inmenu menubar:edit","Arrange Selection");
onTriggered: Printer.arrangeSelection();
}
Action

View File

@ -133,7 +133,7 @@ UM.MainWindow
MenuItem { action: Cura.Actions.selectAll; }
MenuItem { action: Cura.Actions.deleteSelection; }
MenuItem { action: Cura.Actions.deleteAll; }
MenuItem { action: Cura.Actions.arrange; }
MenuItem { action: Cura.Actions.arrangeAll; }
MenuItem { action: Cura.Actions.resetAllTranslation; }
MenuItem { action: Cura.Actions.resetAll; }
MenuSeparator { }
@ -639,7 +639,7 @@ UM.MainWindow
MenuItem { action: Cura.Actions.selectAll; }
MenuItem { action: Cura.Actions.deleteAll; }
MenuItem { action: Cura.Actions.reloadAll; }
MenuItem { action: Cura.Actions.arrange; }
MenuItem { action: Cura.Actions.arrangeSelection; }
MenuItem { action: Cura.Actions.resetAllTranslation; }
MenuItem { action: Cura.Actions.resetAll; }
MenuSeparator { }
@ -700,7 +700,7 @@ UM.MainWindow
MenuItem { action: Cura.Actions.selectAll; }
MenuItem { action: Cura.Actions.deleteAll; }
MenuItem { action: Cura.Actions.reloadAll; }
MenuItem { action: Cura.Actions.arrange; }
MenuItem { action: Cura.Actions.arrangeAll; }
MenuItem { action: Cura.Actions.resetAllTranslation; }
MenuItem { action: Cura.Actions.resetAll; }
MenuSeparator { }