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[0]
# 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[0] / 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 clippedLines
.
# 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[0] / 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[0] / 2
clippedCoords = np.vstack(clippedCoords)
unclippedCoords = np.vstack(unclippedCoords).reshape(-1,2,3)
HexIslandHatcher
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.
Conclusions
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.
Dear Luke Parry,
thank you very much for this beautiful and innovative approach and the pleasant visualisation. Has the exposure strategy with hexagonal islands already been tested by you?
Kind regards
Alex
Hi Alex,
Thank you for nice comments. The exposure strategy specifically for hexagonal island isn’t tested although being honest I don’t see any specific benefit for using the shape.
The aim of this is to show how we can implement our own island shapes but also vary or clip the scan-infills inside the islands. This I believe has greater value especially if we were able to couple with simulation to predict the thermal history.
Always more exciting things to come too,
All the best,
Luke