Reference Mark Algorithm
This page details how LayerForge selects alignment marks for each pair of adjacent layers, and carries them from one pair to the next (TR-9). A mark is chosen inside two pieces' shared, shrunk overlap, reused across boundaries while it still fits, and retired -- never revived -- the moment it does not.
A single-slice model has no adjacent layer to pair with at all, so it gets no marks: there is nothing to align it to.
Choosing a Mark for a Pair
For one pair of adjacent pieces, ReferenceMarkCalculator.choose_mark_for_pair
first tries every candidate carried from the boundary before it, in the order
given, and reuses the first one that still fits the new shrunk overlap and stays
clear of every mark already placed nearby. Only when none of them fit does it
choose a shape and sample fresh points inside the shared region -- the centroid
first, then a fixed-seed random sequence -- returning the first point that fits.
This is reuse-then-first-fit, not a maximised stability score, so a mark often
lands at or near the centroid, since that is usually the first point tried.
Carrying Marks Between Pairs
plan_marks walks the boundaries between adjacent slices in order, one boundary
of lookback at a time: a mark carried from the pairing right before it, on the
same piece, is reused when it still fits; otherwise a fresh mark is chosen for
this pairing alone. A mark is retired -- simply not carried further -- the
instant it stops fitting; nothing revives one from farther back, and reuse never
survives past the one boundary where it stopped fitting.
flowchart TD
A[Candidate carried from the boundary before] --> B{Still fits, and clear of nearby marks?}
B -- yes --> C[Reuse it]
B -- no --> D[Sample a fresh point]
A boundary that places no mark at all -- because its shrunk region is empty, or nothing sampled clears tolerance -- still owes the next boundary its spacing: the position(s) that were in play keep propagating, avoid-only, through consecutive no-mark boundaries until a piece either gets a mark of its own (which then takes over what the boundary after it must avoid) or the run ends. Within one such run of consecutive no-mark boundaries, this keeps a fresh mark from landing within tolerance of the position that would otherwise be forgotten (TR-10), without ever making that missing mark itself reusable.
Adjusting Marks
After placement, marks may still be too close to a contour or to one another, or
their hole may not fit the piece. ReferenceMarkAdjuster filters marks that violate
the configured minimum separation, and marks whose whole hole (the outline of the
shape, at its size and angle) does not lie inside a contour with the web to spare.
The web is marks.min_web_ratio times the layer height. The sketch below shows the
centre rules only:
class ReferenceMarkAdjuster:
@staticmethod
def adjust_marks(marks, contours, config=None):
adjusted = []
for mark in marks:
pt = Point(mark.x, mark.y)
if any(poly.boundary.distance(pt) < config.min_distance for poly in contours):
continue
if any(pt.distance(Point(m.x, m.y)) < config.min_distance for m in adjusted):
continue
adjusted.append(mark)
return adjusted
The final mark set thus respects minimum distances while keeping a reused mark's shape and angle whenever possible.
Parameter Effects
The parameters controlling mark placement can be tuned to suit different model sizes. The following diagrams illustrate how each option influences the final reference marks.
tolerance
A candidate position within the tolerance radius of a stored mark is that mark. It takes the stored coordinates and look, and the nearest stored mark wins. Sampled candidates in that range are skipped, so no two stored marks lie within the tolerance of each other.
flowchart LR
A((Stored mark)) -- within tolerance --> B[Reuse]
A -- beyond tolerance --> C[New mark]
min_distance
Marks must stay at least this far from contours and other marks. Holes
count as contour edges. If a contour is too small for any mark to keep this
distance, it gets no mark and a warning names the slice. Use a smaller
min_distance in that case.
flowchart LR
C[Contour]
M1((Mark1)) -- min_distance --> C
M1 ---|min_distance| M2((Mark2))
available_shapes
A new mark takes the shape with the least symmetry that the list allows: a triangle or an arrow (they have a direction), then a square, then a circle. Of two shapes with the same symmetry the one with the larger outline wins, so the default list gives the triangle. The order of the list does not matter. Every mark of a run has the same angle, so a mark with a direction fixes the rotation of its piece and a circle or a square does not.
flowchart LR
N[New mark] --> D{Shape with a direction listed?}
D -- yes --> L[The larger of them]
D -- no --> S[The square before the circle]
angle
Controls the orientation of newly generated marks. The command line option
--mark-angle takes degrees. Inside the package, angles are radians.
flowchart LR
A[Default orientation] -- angle --> B[Rotated]
Tips for Different Model Scales
The defaults follow the sheet, not the model. The mark size is the larger of the layer height and
1.5 times the kerf, the minimum distance is the mark size, and the tolerance is a tenth of it. With
a 3 mm sheet and a 0.3 mm kerf that is a size of 3, a distance of 3 and a tolerance of 0.3, whatever
the size of the model. So set --layer-height and --kerf for your material and machine, and leave
the rest.
- Small pieces – a mark needs room for its hole and for the web on each side. At the defaults
(a 3 mm sheet) a stack needs a little over 6 mm width for a mark: measured on a
6 mm-square piece 9 mm tall (three 3 mm layers, so two boundaries), a 6.000 mm
width gets no mark on any of its three slices, while 6.003 mm and 6.004 mm each get one
on every slice (
tests/test_disc_matches_chosen_shape.py). A 10 mm cube at the same defaults gets marks throughout. If a contour gets no mark, the warning names the slice: use a smaller--mark-size, or a thinner sheet -- unless the warning instead says the model has only one layer, in which case no size or distance change helps. - Thick sheet – the mark grows with the sheet (a 5 mm sheet gives a size of 5, and its web is
2.5), so a 10 mm cube gets no marks at that sheet. Set a smaller
--mark-size; a size below the least hole size for the sheet is a warning, not an error. - Large models – the defaults need no change. Raise
--mark-toleranceonly if a mark drifts between layers.
Still open
This page describes the algorithm as it now runs: shapes chosen to fix rotation, marks chosen per pair of adjacent layers, and sizes that follow the sheet thickness are all built. Still open: a check that adjacent layers can be aligned in exactly one way (TR-2, #92). See Alignment requirements.