The fact that most island scan strategies employed in SLM are nearly always square raised the question whether we could do more. I recently came across this ability to define ‘hexagon’ island regions advertised in the 2020 release of Autodesk Netfabb. Unfortunately this is a commercial tool and not always available. The practical reasons for implementing a hexagon island scanning strategy are largely unclear, but this prompted to create an example to illustrate how one would create custom island regions using PySLM. This in future could open some interesting ideas of tuning the scan strategy spatially across a layer.
The user needs to customise the behaviour they desire by deriving subclasses from:
These classes serve the purpose for defining a ‘regular’ tessellated sub-region containing hatches. Regular regions that share the same shape characteristics for using the infill optimises the overall clipping performance outlined in the previous post.
Theoretically, we could build 2D unstructured cells e.g. Voronoi patterns, however, internally hatches for each region will require individual clipping and penalised with a significant performance hit during the hatching process.
The Island subclass region is the most important part to re-define the behavior. If we want to change the island regions to become regular tessellated polygons, the
localBoundary method should be re-defined. In this example, it will generate a hexagon region, but the implementation below should be generic to cover other N-gon primitives:
def localBoundary(self) -> np.ndarray: # Redefine the local boundary to be the hexagon shape if HexIsland._boundary is None: # Simple approach is to use a radius to define the overall island size #radius = np.sqrt(2*(self._islandWidth*0.5 + self._islandOverlap)**2) numPoints = 6 radius = self._islandWidth / np.cos(np.pi/numPoints) / 2 + self._islandOverlap print('island', radius, self._islandWidth) # Generate polygon island coords = np.zeros((numPoints+1, 2)) for i in np.arange(0,numPoints): # Subtracting -0.5 orientates the polygon along its face angle = (i-0.5)/numPoints*2*np.pi coords[i] = [np.cos(angle), np.sin(angle)] # Close the polygon coords[-1] = coords # Scale the polygon coords *= radius # Assign to the static class attribute HexIsland._boundary = coords return HexIsland._boundary
The polygon shape is defined by
numPoints, so this can be changed to another polygon if desired. The polygon boundary is defined using a radius for the island region and from this a regular polygon is constructed on the outside. The polygon points are rotated by adjusting the start angle so there is a vertical edge on the RHS.
This is generated once as a static class attribute, stored in
_boundary to remove the overhead when generating the boundary.
The next step is to generate the internal hatch, which in this occasion needs to be clipped with the local boundary. First, the hatch vectors are generated covering the exterior region using the same radius as the polygon. This ensures that for any rotation transformation of the hatch vectors within the island are fully covered. This is relatively familiar to other code which generates these.
def generateInternalHatch(self, isOdd = True) -> np.ndarray: """ Generates a set of hatches orthogonal to the island's coordinate system :math:`(x\\prime, y\\prime)`. :param isOdd: The chosen orientation of the hatching :return: (nx3) Set of sorted hatch coordinates """ numPoints = 6 radius = self._islandWidth / np.cos(np.pi / numPoints) / 2 + self._islandOverlap startX = -radius startY = -radius endX = radius endY = radius # Generate the basic hatch lines to fill the island region x = np.tile(np.arange(startX, endX, self._hatchDistance).reshape(-1, 1), 2).flatten() y = np.array([startY, endY]) y = np.resize(y, x.shape) z = np.arange(0, y.shape / 2, 0.5).astype(np.int64) coords = np.hstack([x.reshape(-1, 1), y.reshape(-1, 1), z.reshape(-1,1)]) # Toggle the hatch angle theta_h = np.deg2rad(90.0) if isOdd else np.deg2rad(0.0) # Create the 2D rotation matrix with an additional row, column to preserve the hatch order c, s = np.cos(theta_h), np.sin(theta_h) R = np.array([(c, -s, 0), (s, c, 0), (0, 0, 1.0)]) # Apply the rotation matrix and translate to bounding box centre coords = np.matmul(R, coords.T).T
The next stage is to clip the hatch vectors with the local boundary. This is achieved using the static class method
hatching.BaseHatcher.clipLines. The clipped hatches need to be sorted using the ‘z’ index or 2nd column of the
# Clip the hatch fill to the boundary boundary = [[self.localBoundary()]] clippedLines = np.array(hatching.BaseHatcher.clipLines(boundary, coords)) # Sort the hatches clippedLines = clippedLines[:, :, :3] id = np.argsort(clippedLines[:, 0, 2]) clippedLines = clippedLines[id, :, :] # Convert to a flat 2D array of hatches and resort the indices coordsUp = clippedLines.reshape(-1,3) coordsUp[:,2] = np.arange(0, coordsUp.shape / 2, 0.5).astype(np.int64) return coordsUp
After sorting, the ‘z’ indexes need to the be condensed or flattened by re-building the ‘z’ index into sequential order. This is done to ensure when the hatches for islands are merged, we simply increment the index of the island using the length of the hatch array rather than performing
np.max each time. This is later seen in the method hatching.IslandHatcher.hatch
# Generate the hatches for all the islands idx = 0 for island in sortedIslands: # Generate the hatches for each island subregion coords = island.hatch() # Note for sorting later the order of the hatch vector is updated based on the sortedIsland coords[:, 2] += idx ... ... # idx += coords.shape / 2 clippedCoords = np.vstack(clippedCoords) unclippedCoords = np.vstack(unclippedCoords).reshape(-1,2,3)
The final stage, is to re-implement
hatching.IslandHatcher as a subclass. In this class, at a minimum, the
generateIsland method needs to be redefined to correctly positioned the islands so that they tessellate correctly.
def generateIslands(self, paths, hatchAngle: float = 90.0): """ Generate a series of tessellating Hex Islands to fill the region. For now this requires re-implementing because the boundaries of the island may be different shapes and require a specific placement in order to correctly tessellate within a region. """ # Hatch angle theta_h = np.radians(hatchAngle) # 'rad' # Get the bounding box of the boundary bbox = self.boundaryBoundingBox(paths) print('bounding box bbox', bbox) # Expand the bounding box bboxCentre = np.mean(bbox.reshape(2, 2), axis=0) # Calculates the diagonal length for which is the longest diagonal = bbox[2:] - bboxCentre bboxRadius = np.sqrt(diagonal.dot(diagonal)) # Number of sides of the polygon island numPoints = 6 # Construct a square which wraps the radius numIslandsX = int(2 * bboxRadius / self._islandWidth) + 1 numIslandsY = int(2 * bboxRadius / ((self._islandWidth + self._islandOverlap) * np.sin(2*np.pi/numPoints)) )+ 1
The key difference here is defining the number of islands in the y-direction to account for the tessellation of the polygons. This is a simple geometry problem. The y-offset for the islands is simply the vertical component of the 2 x island radius at the angular increment to form the polygon.
The HexIsland are generated with the offsets and appended to the list. These are then treat internally by the parent class IslandHatcher.
... ... for i in np.arange(0, numIslandsX): for j in np.arange(0, numIslandsY): # gGenerate the island position startX = -bboxRadius + i * self._islandWidth + np.mod(j, 2) * self._islandWidth / 2 startY = -bboxRadius + j * (self._islandWidth) * np.sin(2*np.pi/numPoints) pos = np.array([(startX, startY)]) # Apply the rotation matrix and translate to bounding box centre pos = np.matmul(R, pos.T) pos = pos.T + bboxCentre # Generate a HexIsland and append to the island island = HexIsland(origin=pos, orientation=theta_h, islandWidth=self._islandWidth, islandOverlap=self._islandOverlap, hatchDistance=self._hatchDistance) island.posId = (i, j) island.id = id islands.append(island) id += 1 return islands
The island tessellation generated is shown below, with the an offset between islands applied by modifying the radius.
The fully clipped scan strategy is shown below with the scanning ordered in the Y-direction.
This post illustrates how one can effectively decompose a layer region into a series of repeatable ‘island’ units which can be processed in an efficient manner, by only clipping hatches at boundary regions. This potentially has the ability to define spatially aware island regions; for example this could be redefining island sizes or parameters towards the boundary of a part. It could be used to alter the scan strategies within the region too, with the effect of changing the thermal behavior.
The full excerpt of the example can be found on github at examples/example_custom_island_hatcher.py.