Merge branch 'feature_arrange_cleanup' of github.com:Ultimaker/Cura

This commit is contained in:
Jaime van Kessel 2017-04-05 10:45:42 +02:00
commit ecf905f580
6 changed files with 562 additions and 10 deletions

172
cura/Arrange.py Executable file
View file

@ -0,0 +1,172 @@
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Logger import Logger
from cura.ShapeArray import ShapeArray
from collections import namedtuple
import numpy
import copy
## Return object for bestSpot
LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points", "priority"])
## The Arrange classed is used together with 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.
class Arrange:
def __init__(self, x, y, offset_x, offset_y, scale=1):
self.shape = (y, x)
self._priority = numpy.zeros((x, y), dtype=numpy.int32)
self._priority_unique_values = []
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 Root for finding all scene nodes
# \param fixed_nodes Scene nodes to be placed
@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:
vertices = fixed_node.callDecoration("getConvexHull")
points = copy.deepcopy(vertices._points)
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
arranger.place(0, 0, shape_arr)
return arranger
## Find placement for a node (using offset shape) and place it (using hull shape)
# return the nodes that should be placed
# \param node
# \param offset_shape_arr ShapeArray with offset, used to find location
# \param hull_shape_arr ShapeArray without offset, for placing the shape
# \param count Number of objects
def findNodePlacements(self, node, offset_shape_arr, hull_shape_arr, count = 1, step = 1):
nodes = []
start_prio = 0
for i in range(count):
new_node = copy.deepcopy(node)
best_spot = self.bestSpot(
offset_shape_arr, start_prio = start_prio, step = step)
x, y = best_spot.x, best_spot.y
start_prio = best_spot.priority
transformation = new_node._transformation
if x is not None: # We could find a place
transformation._data[0][3] = x
transformation._data[2][3] = y
self.place(x, y, hull_shape_arr) # take place before the next one
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, center is best. lower value is better
def centerFirst(self):
# Distance x + distance y: creates diamond shape
#self._priority = numpy.fromfunction(
# lambda i, j: abs(self._offset_x-i)+abs(self._offset_y-j), self.shape, dtype=numpy.int32)
# Square distance: creates a more round shape
self._priority = numpy.fromfunction(
lambda i, j: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self.shape, dtype=numpy.int32)
self._priority_unique_values = numpy.unique(self._priority)
self._priority_unique_values.sort()
## Fill priority, back is best. lower value is better
def backFirst(self):
self._priority = numpy.fromfunction(
lambda i, j: 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()
## Return the amount of "penalty points" for polygon, which is the sum of priority
# 999999 if occupied
# \param x x-coordinate to check shape
# \param y y-coordinate
# \param shape_arr the ShapeArray object to place
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
offset_y = y + self._offset_y + shape_arr.offset_y
occupied_slice = self._occupied[
offset_y:offset_y + shape_arr.arr.shape[0],
offset_x:offset_x + shape_arr.arr.shape[1]]
try:
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 numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
## Find "best" spot for ShapeArray
# Return namedtuple with properties x, y, penalty_points, priority
# \param shape_arr ShapeArray
# \param start_prio Start with this priority value (and skip the ones before)
# \param step Slicing value, higher = more skips = faster but less accurate
def bestSpot(self, shape_arr, start_prio = 0, step = 1):
start_idx_list = numpy.where(self._priority_unique_values == start_prio)
if start_idx_list:
start_idx = start_idx_list[0][0]
else:
start_idx = 0
for prio in self._priority_unique_values[start_idx::step]:
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]
projected_x = x - self._offset_x
projected_y = y - self._offset_y
# array to "world" coordinates
penalty_points = self.checkShape(projected_x, projected_y, shape_arr)
if penalty_points != 999999:
return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = prio)
return LocationSuggestion(x = None, y = None, penalty_points = None, priority = prio) # No suitable location found :-(
## Place the object.
# Marks the locations in self._occupied and self._priority
# \param x x-coordinate
# \param y y-coordinate
# \param shape_arr ShapeArray object
def place(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
offset_y = y + self._offset_y + shape_arr.offset_y
shape_y, shape_x = self._occupied.shape
min_x = min(max(offset_x, 0), shape_x - 1)
min_y = min(max(offset_y, 0), shape_y - 1)
max_x = min(max(offset_x + shape_arr.arr.shape[1], 0), shape_x - 1)
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[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[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

@ -31,6 +31,9 @@ from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.SetTransformOperation import SetTransformOperation
from cura.Arrange import Arrange
from cura.ShapeArray import ShapeArray
from cura.ConvexHullDecorator import ConvexHullDecorator
from cura.SetParentOperation import SetParentOperation
from cura.SliceableObjectDecorator import SliceableObjectDecorator
from cura.BlockSlicingDecorator import BlockSlicingDecorator
@ -838,22 +841,29 @@ class CuraApplication(QtApplication):
op.push()
## Create a number of copies of existing object.
# \param object_id
# \param count number of copies
# \param min_offset minimum offset to other objects.
@pyqtSlot("quint64", int)
def multiplyObject(self, object_id, count):
def multiplyObject(self, object_id, count, min_offset = 8):
node = self.getController().getScene().findObject(object_id)
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
node = Selection.getSelectedObject(0)
if node:
current_node = node
# Find the topmost group
while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
current_node = current_node.getParent()
# If object is part of a group, multiply group
current_node = node
while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
current_node = current_node.getParent()
root = self.getController().getScene().getRoot()
arranger = Arrange.create(scene_root = root)
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset = min_offset)
nodes = arranger.findNodePlacements(current_node, offset_shape_arr, hull_shape_arr, count = count)
if nodes:
op = GroupedOperation()
for _ in range(count):
new_node = copy.deepcopy(current_node)
for new_node in nodes:
op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent()))
op.push()
@ -973,6 +983,83 @@ class CuraApplication(QtApplication):
op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1)))
op.push()
## Arrange all objects.
@pyqtSlot()
def arrangeAll(self):
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 Selection
@pyqtSlot()
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
if node in nodes: # exclude selected node from fixed_nodes
continue
fixed_nodes.append(node)
self.arrange(nodes, fixed_nodes)
## Arrange the nodes, given fixed nodes
# \param nodes nodes that we have to place
# \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes
def arrange(self, nodes, fixed_nodes):
min_offset = 8
arranger = Arrange.create(fixed_nodes = fixed_nodes)
# Collect nodes to be placed
nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
for node in nodes:
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))
# Sort nodes biggest area first
nodes_arr.sort(key = lambda item: item[0])
nodes_arr.reverse()
# Place nodes one at a time
start_prio = 0
for size, node, offset_shape_arr, hull_shape_arr in nodes_arr:
# For performance reasons, we assume that when a location does not fit,
# it will also not fit for the next object (while what can be untrue).
# We also skip possibilities by slicing through the possibilities (step = 10)
best_spot = arranger.bestSpot(offset_shape_arr, start_prio = start_prio, step = 10)
x, y = best_spot.x, best_spot.y
start_prio = best_spot.priority
if x is not None: # We could find a place
arranger.place(x, y, hull_shape_arr) # take place before the next one
node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
if node.getBoundingBox():
center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
else:
center_y = 0
op = GroupedOperation()
op.addOperation(SetTransformOperation(node, Vector(x, center_y, y)))
op.push()
## Reload all mesh data on the screen from file.
@pyqtSlot()
def reloadAll(self):
@ -1209,6 +1296,10 @@ class CuraApplication(QtApplication):
filename = job.getFileName()
self._currently_loading_files.remove(filename)
root = self.getController().getScene().getRoot()
arranger = Arrange.create(scene_root = root)
min_offset = 8
for node in nodes:
node.setSelectable(True)
node.setName(os.path.basename(filename))
@ -1229,8 +1320,18 @@ class CuraApplication(QtApplication):
scene = self.getController().getScene()
op = AddSceneNodeOperation(node, scene.getRoot())
op.push()
# If there is no convex hull for the node, start calculating it and continue.
if not node.getDecorator(ConvexHullDecorator):
node.addDecorator(ConvexHullDecorator())
# find node location
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 = arranger.findNodePlacements(node, offset_shape_arr, hull_shape_arr, count = 1, step = 10)
for new_node in nodes:
op = AddSceneNodeOperation(new_node, scene.getRoot())
op.push()
scene.sceneChanged.emit(node)

111
cura/ShapeArray.py Executable file
View file

@ -0,0 +1,111 @@
import numpy
import copy
from UM.Math.Polygon import Polygon
## Polygon representation as an array for use with Arrange
class ShapeArray:
def __init__(self, arr, offset_x, offset_y, scale = 1):
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, scale = 1):
# scale
vertices = vertices * scale
# flip y, x -> x, y
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(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)
## 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, min_offset, scale = 0.5):
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
# \param shape numpy format shape, [x-size, y-size]
# \param vertices
@classmethod
def arrayFromPolygon(cls, shape, vertices):
base_array = numpy.zeros(shape, dtype=float) # Initialize your array of zeros
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 = 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
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, p2, base_array):
if p1[0] == p2[0] and p1[1] == p2[1]:
return
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 = numpy.sign(p2[1] - p1[1])
return idxs[1] * sign
if p2[1] == p1[1]:
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 = numpy.sign(p2[0] - p1[0])
return idxs[1] * sign <= max_col_idx * sign

17
resources/qml/Actions.qml Normal file → Executable file
View file

@ -31,6 +31,8 @@ Item
property alias selectAll: selectAllAction;
property alias deleteAll: deleteAllAction;
property alias reloadAll: reloadAllAction;
property alias arrangeAll: arrangeAllAction;
property alias arrangeSelection: arrangeSelectionAction;
property alias resetAllTranslation: resetAllTranslationAction;
property alias resetAll: resetAllAction;
@ -266,6 +268,21 @@ Item
onTriggered: CuraApplication.reloadAll();
}
Action
{
id: arrangeAllAction;
text: catalog.i18nc("@action:inmenu menubar:edit","Arrange All Models");
onTriggered: Printer.arrangeAll();
shortcut: "Ctrl+R";
}
Action
{
id: arrangeSelectionAction;
text: catalog.i18nc("@action:inmenu menubar:edit","Arrange Selection");
onTriggered: Printer.arrangeSelection();
}
Action
{
id: resetAllTranslationAction;

View file

@ -131,6 +131,7 @@ UM.MainWindow
MenuItem { action: Cura.Actions.redo; }
MenuSeparator { }
MenuItem { action: Cura.Actions.selectAll; }
MenuItem { action: Cura.Actions.arrangeAll; }
MenuItem { action: Cura.Actions.deleteSelection; }
MenuItem { action: Cura.Actions.deleteAll; }
MenuItem { action: Cura.Actions.resetAllTranslation; }
@ -603,6 +604,7 @@ UM.MainWindow
MenuItem { action: Cura.Actions.multiplyObject; }
MenuSeparator { }
MenuItem { action: Cura.Actions.selectAll; }
MenuItem { action: Cura.Actions.arrangeAll; }
MenuItem { action: Cura.Actions.deleteAll; }
MenuItem { action: Cura.Actions.reloadAll; }
MenuItem { action: Cura.Actions.resetAllTranslation; }
@ -663,6 +665,7 @@ UM.MainWindow
{
id: contextMenu;
MenuItem { action: Cura.Actions.selectAll; }
MenuItem { action: Cura.Actions.arrangeAll; }
MenuItem { action: Cura.Actions.deleteAll; }
MenuItem { action: Cura.Actions.reloadAll; }
MenuItem { action: Cura.Actions.resetAllTranslation; }

148
tests/TestArrange.py Executable file
View file

@ -0,0 +1,148 @@
import pytest
import numpy
import time
from cura.Arrange import Arrange
from cura.ShapeArray import ShapeArray
def gimmeShapeArray():
vertices = numpy.array([[-3, 1], [3, 1], [0, -3]])
shape_arr = ShapeArray.fromPolygon(vertices)
return shape_arr
## Smoke test for Arrange
def test_smoke_arrange():
ar = Arrange.create(fixed_nodes = [])
## Smoke test for ShapeArray
def test_smoke_ShapeArray():
shape_arr = gimmeShapeArray()
## Test centerFirst
def test_centerFirst():
ar = Arrange(300, 300, 150, 150)
ar.centerFirst()
assert ar._priority[150][150] < ar._priority[170][150]
assert ar._priority[150][150] < ar._priority[150][170]
assert ar._priority[150][150] < ar._priority[170][170]
assert ar._priority[150][150] < ar._priority[130][150]
assert ar._priority[150][150] < ar._priority[150][130]
assert ar._priority[150][150] < ar._priority[130][130]
## Test backFirst
def test_backFirst():
ar = Arrange(300, 300, 150, 150)
ar.backFirst()
assert ar._priority[150][150] < ar._priority[150][170]
assert ar._priority[150][150] < ar._priority[170][170]
assert ar._priority[150][150] > ar._priority[150][130]
assert ar._priority[150][150] > ar._priority[130][130]
## See if the result of bestSpot has the correct form
def test_smoke_bestSpot():
ar = Arrange(30, 30, 15, 15)
ar.centerFirst()
shape_arr = gimmeShapeArray()
best_spot = ar.bestSpot(shape_arr)
assert hasattr(best_spot, "x")
assert hasattr(best_spot, "y")
assert hasattr(best_spot, "penalty_points")
assert hasattr(best_spot, "priority")
## Try to place an object and see if something explodes
def test_smoke_place():
ar = Arrange(30, 30, 15, 15)
ar.centerFirst()
shape_arr = gimmeShapeArray()
assert not numpy.any(ar._occupied)
ar.place(0, 0, shape_arr)
assert numpy.any(ar._occupied)
## See of our center has less penalty points than out of the center
def test_checkShape():
ar = Arrange(30, 30, 15, 15)
ar.centerFirst()
shape_arr = gimmeShapeArray()
points = ar.checkShape(0, 0, shape_arr)
points2 = ar.checkShape(5, 0, shape_arr)
points3 = ar.checkShape(0, 5, shape_arr)
assert points2 > points
assert points3 > points
## After placing an object on a location that location should give more penalty points
def test_checkShape_place():
ar = Arrange(30, 30, 15, 15)
ar.centerFirst()
shape_arr = gimmeShapeArray()
points = ar.checkShape(3, 6, shape_arr)
ar.place(3, 6, shape_arr)
points2 = ar.checkShape(3, 6, shape_arr)
assert points2 > points
## Test the whole sequence
def test_smoke_place_objects():
ar = Arrange(20, 20, 10, 10)
ar.centerFirst()
shape_arr = gimmeShapeArray()
print(shape_arr)
now = time.time()
for i in range(5):
best_spot_x, best_spot_y, score, prio = ar.bestSpot(shape_arr)
print(best_spot_x, best_spot_y, score)
ar.place(best_spot_x, best_spot_y, shape_arr)
print(ar._occupied)
print(time.time() - now)
## Polygon -> array
def test_arrayFromPolygon():
vertices = numpy.array([[-3, 1], [3, 1], [0, -3]])
array = ShapeArray.arrayFromPolygon([5, 5], vertices)
assert numpy.any(array)
## Polygon -> array
def test_arrayFromPolygon2():
vertices = numpy.array([[-3, 1], [3, 1], [2, -3]])
array = ShapeArray.arrayFromPolygon([5, 5], vertices)
assert numpy.any(array)
## Line definition -> array with true/false
def test_check():
base_array = numpy.zeros([5, 5], dtype=float)
p1 = numpy.array([0, 0])
p2 = numpy.array([4, 4])
check_array = ShapeArray._check(p1, p2, base_array)
assert numpy.any(check_array)
assert check_array[3][0]
assert not check_array[0][3]
## Line definition -> array with true/false
def test_check2():
base_array = numpy.zeros([5, 5], dtype=float)
p1 = numpy.array([0, 3])
p2 = numpy.array([4, 3])
check_array = ShapeArray._check(p1, p2, base_array)
assert numpy.any(check_array)
assert not check_array[3][0]
assert check_array[3][4]