I fixed the inset in one caller, and the other one printed the bug
A part came off the printer with black V-shaped wedges pushed into the notches of its outline, in a place where a coloured border was meant to run a uniform 1.40 mm all the way round. I had found that exact defect two weeks earlier, understood it, written a replacement for the function that caused it, and published the whole story. It was still printing, because I had fixed it in one caller and left the other one where it was.
Then I spent half a day fixing the wrong fault.
What the earlier fix was
Insetting a polygon by moving each vertex inward along the bisector of its
two edges is the obvious implementation and it has a known failure: the
vertex has to travel d / cos(θ/2), so as a corner sharpens the
cosine goes to zero and the vertex shoots off into space. At a re-entrant
corner that spike points back into the shape you were insetting.
ELI5: what's a re-entrant corner?
A corner that points inward rather than outward — the notch in a jigsaw piece, or the inside corner of an L-shaped room. Most shapes people draw are mostly outward corners, which is why code that handles those fine can sit for months before anything notices.
The fix was to stop moving vertices and compute the inset as an erosion
instead: the set of centres of discs of radius d that fit
entirely inside the shape. A disc cannot fit inside a spike, so the failure
cannot happen by construction. That is all in the earlier post and I won't
repeat it here.
ELI5: what's an erosion?
Shrinking a shape by rolling a circle around the inside of its outline and keeping only what the circle's centre can reach. It answers "what still fits" rather than "where should this corner go", which is why it has no opinion about corners at all.
The half day
The wedges were black because the border that was supposed to cover that seam had pulled back from it, exposing the dark part underneath. So my first read was the obvious one: the border is too narrow. I widened it from 1.2 mm to 1.4, then to 1.6.
ELI5: why would a gap print black?
The printer can change filament partway through a part, so a single object comes out in more than one colour. Here a dark base carries a lighter border printed on top of it. Anywhere the border fails to reach, you are looking straight at the base — so a gap doesn't show up as a hole, it shows up as the wrong colour.
Rendered from the models at 1.2, 1.4 and 1.6, the wedges are identical. Not smaller. Not improved-but-not-enough. The same wedges, in the same places, at the same size, across a change that moved the thing I believed was causing them by a third.
That is the part worth the post. A fix that partly works tells you that you are on the right fault and need more of it. A fix that changes nothing at all is telling you something much more specific, and much easier to act on: whatever you just changed is not in the path that produces this symptom. I had that measurement early and read it as "not enough yet" three times.
There were two faults, and only one of them was mine to widen
The border genuinely was marginally too narrow. Against the tolerance stack it needed to be 1.6 mm rather than 1.2, and widening it fixed a real problem — a thin, even line of dark showing anywhere the insert sat toward one side of its pocket. That fault is uniform. It is the same everywhere on the outline, and a uniform width is exactly the right instrument for it.
The wedges were a different fault with the same colour. The inset that defines where the border stops was still being computed by the old vertex-bisector code, in a second caller I had never migrated. Nominally that inset is a uniform 1.40 mm. Measured on the built parts, it ran as deep as 6.96 mm inside the edge, deepest exactly where the outline is most sharply notched.
So the limit was ragged, and a uniform lip cannot chase a ragged limit. There is no width that covers a 6.96 mm excursion without swallowing the whole design everywhere else.
inset dark showing
(nominal past the
1.40 mm) border
vertex bisector - what printed 3.90-6.96 4.8-12.9 mm2 per part
erosion - now 1.47-1.56 0.00 mm2, every part
The tell was inside the fix I had already written
The replacement function's docstring describes this defect. I wrote it, two weeks earlier, as the justification for why the function needed to exist: a 7.00 mm tongue reaching into an opening where the inset is 1.40, on 19 of 22 outlines, printed and visible.
That was not a general note about miter limits. It was a description of the exact fault that was still printing, on the exact part it was still printing on, sitting in the file I was editing. I read past it repeatedly, because I had filed it as the bug I fixed rather than the bug this function is for, and those are not the same thing when the function has more than one caller.
# the lip — migrated when I wrote the fix
lip = erode_poly(outline, clearance + lip_width)
# the decoration limit — same defect, same file, still on the old path
limit = offset_poly(outline, clearance + lip_width - border)
Two lines apart. A fix is not finished when the function exists, and it is not finished when the caller you were looking at is converted.
What actually settled it
Three separate offsets in this argument were worked out on paper, and I got the sign backwards on two of them. What ended it was drawing the thing: one composite render of the border laid over the real pattern, from the models I already had. It reproduced the photographed wedges exactly, in the same notches, and showed the border width moving nothing.
That took a few minutes and replaced a day of arithmetic. If you have models of every part involved, render them together before reasoning about an offset — an offset is a claim about where two pieces of geometry sit relative to each other, and that is a question a picture answers directly and a sign convention answers only if you get it right.
What I'd tell you to check first
A fix in the function is not a fix in the build. When you
replace a shared routine because it was wrong, the job is not done until
you have listed its callers and decided about each one. One
grep for the old function's name would have found the second
caller in this file, at any point across two weeks. I never ran it, because
the bug felt closed.
A change that moves the symptom by exactly nothing is a result, not a failed attempt. Partial improvement means keep going; zero change means you are not in the path. Three increments produced pixel identical output and I treated it as three failures to go far enough.
One symptom does not mean one cause, and the causes need not be related. Two independent faults here produced dark showing in the same place for different reasons, and the one I could measure and fix was not the one anyone was pointing at. Fixing it was still correct. It just wasn't the answer.
Draw it before you reason about it. Especially for offsets, and especially when you have every model in hand and are still doing the geometry in your head.