Found while implementing A3, and it is the concrete instance that motivates it.
Filed so the case has a permanent home with its reproduction rather than existing only
inside a PR body.
Summary
The geometry engine does not model a stroke drawn with a macro or block aperture. That
limitation is documented in docs/geometry-diff.md under "Known limitations" — but the
output never said so. A board with such a trace added reported 0 changes at exit
0 under --fail-on-diff, and the JSON report was byte-identical to comparing a board
against a copy of itself.
The raster engine draws these as hairlines, so it did see the change. The two engines
answered the same question differently, and the geometry engine gave the dangerous
answer.
Reproduction
before/ has one pad. after/ has that pad plus a trace stroked with macro aperture
D11.
%FSLAX36Y36*%
%MOMM*%
%AMROUND*
1,1,$1,0,0*%
%ADD10C,1.0*%
%ADD11ROUND,1.0*%
D10*
X1000000Y1000000D03*
D11*
X3000000Y3000000D02*
X8000000Y8000000D01*
M02*
$ gerberdiff geomdiff before after --fail-on-diff
geomdiff: 0/1 layers changed, 0 changes (1 ms)
exit=0 # stderr: 0 bytes
$ gerberdiff diff before after --fail-on-diff
diff: 1/1 layers changed (271 ms)
exit=1
Verbose adds nothing: board-F_Cu: 0 changes, 1 unchanged, +0.000/-0.000 mm^2.
The JSON report compared equal to geomdiff before before, so no field distinguished
them — has_changes was false in both.
Mechanism
geometry/layer_geometry.py, _emit_stroke:
if ap is None or isinstance(ap, (BlockAperture, MacroAperture)):
# Strokes with macro/block apertures are not meaningful; the raster
# engine draws them with a hairline -- skip in the geometry engine.
return
A bare return. The same shape appears four more times in the emit path — an aperture
whose extents cannot be computed, a macro flash evaluating to empty, and block nesting
past the replay depth, which carried the comment "matches renderer: silently skip
over-deep nesting".
Why it is the same defect as #17
#17 was a flash dropped because its aperture was never defined. This is a stroke dropped
because its aperture is one the engine chooses not to model. Both leave a boolean
has_changes reading false, which cannot distinguish "nothing changed" from "I did not
look at that". #17 was fixable at the parse layer because the file was invalid; this one
is a real limitation of the engine, so the report has to be able to say so.
Fixed by
A3's implementation: the engine records what it could not model, the comparison reports
identical | different | indeterminate, and this case now exits 2 naming the reason.
Environment
gerberdiff 0.29.1, main, Python 3.13, Linux.
Found while implementing A3, and it is the concrete instance that motivates it.
Filed so the case has a permanent home with its reproduction rather than existing only
inside a PR body.
Summary
The geometry engine does not model a stroke drawn with a macro or block aperture. That
limitation is documented in
docs/geometry-diff.mdunder "Known limitations" — but theoutput never said so. A board with such a trace added reported
0 changesat exit0under--fail-on-diff, and the JSON report was byte-identical to comparing a boardagainst a copy of itself.
The raster engine draws these as hairlines, so it did see the change. The two engines
answered the same question differently, and the geometry engine gave the dangerous
answer.
Reproduction
before/has one pad.after/has that pad plus a trace stroked with macro apertureD11.Verbose adds nothing:
board-F_Cu: 0 changes, 1 unchanged, +0.000/-0.000 mm^2.The JSON report compared equal to
geomdiff before before, so no field distinguishedthem —
has_changeswasfalsein both.Mechanism
geometry/layer_geometry.py,_emit_stroke:A bare
return. The same shape appears four more times in the emit path — an aperturewhose extents cannot be computed, a macro flash evaluating to empty, and block nesting
past the replay depth, which carried the comment "matches renderer: silently skip
over-deep nesting".
Why it is the same defect as #17
#17 was a flash dropped because its aperture was never defined. This is a stroke dropped
because its aperture is one the engine chooses not to model. Both leave a boolean
has_changesreadingfalse, which cannot distinguish "nothing changed" from "I did notlook at that". #17 was fixable at the parse layer because the file was invalid; this one
is a real limitation of the engine, so the report has to be able to say so.
Fixed by
A3's implementation: the engine records what it could not model, the comparison reports
identical | different | indeterminate, and this case now exits2naming the reason.Environment
gerberdiff 0.29.1,
main, Python 3.13, Linux.