ShapeArray.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. # Copyright (c) 2019 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import numpy
  4. import copy
  5. from typing import Optional, Tuple, TYPE_CHECKING
  6. from UM.Math.Polygon import Polygon
  7. if TYPE_CHECKING:
  8. from UM.Scene.SceneNode import SceneNode
  9. class ShapeArray:
  10. """Polygon representation as an array for use with :py:class:`cura.Arranging.Arrange.Arrange`"""
  11. def __init__(self, arr: numpy.array, offset_x: float, offset_y: float, scale: float = 1) -> None:
  12. self.arr = arr
  13. self.offset_x = offset_x
  14. self.offset_y = offset_y
  15. self.scale = scale
  16. @classmethod
  17. def fromPolygon(cls, vertices: numpy.array, scale: float = 1) -> "ShapeArray":
  18. """Instantiate from a bunch of vertices
  19. :param vertices:
  20. :param scale: scale the coordinates
  21. :return: a shape array instantiated from a bunch of vertices
  22. """
  23. # scale
  24. vertices = vertices * scale
  25. # flip y, x -> x, y
  26. flip_vertices = numpy.zeros((vertices.shape))
  27. flip_vertices[:, 0] = vertices[:, 1]
  28. flip_vertices[:, 1] = vertices[:, 0]
  29. flip_vertices = flip_vertices[::-1]
  30. # offset, we want that all coordinates have positive values
  31. offset_y = int(numpy.amin(flip_vertices[:, 0]))
  32. offset_x = int(numpy.amin(flip_vertices[:, 1]))
  33. flip_vertices[:, 0] = numpy.add(flip_vertices[:, 0], -offset_y)
  34. flip_vertices[:, 1] = numpy.add(flip_vertices[:, 1], -offset_x)
  35. shape = numpy.array([int(numpy.amax(flip_vertices[:, 0])), int(numpy.amax(flip_vertices[:, 1]))])
  36. shape[numpy.where(shape == 0)] = 1
  37. arr = cls.arrayFromPolygon(shape, flip_vertices)
  38. if not numpy.ndarray.any(arr):
  39. # set at least 1 pixel
  40. arr[0][0] = 1
  41. return cls(arr, offset_x, offset_y)
  42. @classmethod
  43. def fromNode(cls, node: "SceneNode", min_offset: float, scale: float = 0.5, include_children: bool = False) -> Tuple[Optional["ShapeArray"], Optional["ShapeArray"]]:
  44. """Instantiate an offset and hull ShapeArray from a scene node.
  45. :param node: source node where the convex hull must be present
  46. :param min_offset: offset for the offset ShapeArray
  47. :param scale: scale the coordinates
  48. :return: A tuple containing an offset and hull shape array
  49. """
  50. transform = node._transformation
  51. transform_x = transform._data[0][3]
  52. transform_y = transform._data[2][3]
  53. hull_verts = node.callDecoration("getConvexHull")
  54. # If a model is too small then it will not contain any points
  55. if hull_verts is None or not hull_verts.getPoints().any():
  56. return None, None
  57. # For one_at_a_time printing you need the convex hull head.
  58. hull_head_verts = node.callDecoration("getConvexHullHead") or hull_verts
  59. if hull_head_verts is None:
  60. hull_head_verts = Polygon()
  61. # If the child-nodes are included, adjust convex hulls as well:
  62. if include_children:
  63. children = node.getAllChildren()
  64. if not children is None:
  65. for child in children:
  66. # 'Inefficient' combination of convex hulls through known code rather than mess it up:
  67. child_hull = child.callDecoration("getConvexHull")
  68. if not child_hull is None:
  69. hull_verts = hull_verts.unionConvexHulls(child_hull)
  70. child_hull_head = child.callDecoration("getConvexHullHead") or child_hull
  71. if not child_hull_head is None:
  72. hull_head_verts = hull_head_verts.unionConvexHulls(child_hull_head)
  73. offset_verts = hull_head_verts.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
  74. offset_points = copy.deepcopy(offset_verts._points) # x, y
  75. offset_points[:, 0] = numpy.add(offset_points[:, 0], -transform_x)
  76. offset_points[:, 1] = numpy.add(offset_points[:, 1], -transform_y)
  77. offset_shape_arr = ShapeArray.fromPolygon(offset_points, scale = scale)
  78. hull_points = copy.deepcopy(hull_verts._points)
  79. hull_points[:, 0] = numpy.add(hull_points[:, 0], -transform_x)
  80. hull_points[:, 1] = numpy.add(hull_points[:, 1], -transform_y)
  81. hull_shape_arr = ShapeArray.fromPolygon(hull_points, scale = scale) # x, y
  82. return offset_shape_arr, hull_shape_arr
  83. @classmethod
  84. def arrayFromPolygon(cls, shape: Tuple[int, int], vertices: numpy.array) -> numpy.array:
  85. """Create :py:class:`numpy.ndarray` with dimensions defined by shape
  86. Fills polygon defined by vertices with ones, all other values zero
  87. Only works correctly for convex hull vertices
  88. Originally from: `Stackoverflow - generating a filled polygon inside a numpy array <https://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array>`_
  89. :param shape: numpy format shape, [x-size, y-size]
  90. :param vertices:
  91. :return: numpy array with dimensions defined by shape
  92. """
  93. base_array = numpy.zeros(shape, dtype = numpy.int32) # Initialize your array of zeros
  94. fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill
  95. # Create check array for each edge segment, combine into fill array
  96. for k in range(vertices.shape[0]):
  97. check_array = cls._check(vertices[k - 1], vertices[k], base_array)
  98. if check_array is not None:
  99. fill = numpy.all([fill, check_array], axis=0)
  100. # Set all values inside polygon to one
  101. base_array[fill] = 1
  102. return base_array
  103. @classmethod
  104. def _check(cls, p1: numpy.array, p2: numpy.array, base_array: numpy.array) -> Optional[numpy.array]:
  105. """Return indices that mark one side of the line, used by arrayFromPolygon
  106. Uses the line defined by p1 and p2 to check array of
  107. input indices against interpolated value
  108. Returns boolean array, with True inside and False outside of shape
  109. Originally from: `Stackoverflow - generating a filled polygon inside a numpy array <https://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array>`_
  110. :param p1: 2-tuple with x, y for point 1
  111. :param p2: 2-tuple with x, y for point 2
  112. :param base_array: boolean array to project the line on
  113. :return: A numpy array with indices that mark one side of the line
  114. """
  115. if p1[0] == p2[0] and p1[1] == p2[1]:
  116. return None
  117. idxs = numpy.indices(base_array.shape) # Create 3D array of indices
  118. p1 = p1.astype(float)
  119. p2 = p2.astype(float)
  120. if p2[0] == p1[0]:
  121. sign = numpy.sign(p2[1] - p1[1])
  122. return idxs[1] * sign
  123. if p2[1] == p1[1]:
  124. sign = numpy.sign(p2[0] - p1[0])
  125. return idxs[1] * sign
  126. # Calculate max column idx for each row idx based on interpolated line between two points
  127. max_col_idx = (idxs[0] - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]) + p1[1]
  128. sign = numpy.sign(p2[0] - p1[0])
  129. return idxs[1] * sign <= max_col_idx * sign