Skip to content

geomdiff reports an added trace as 0 changes when it is stroked with a macro aperture #25

Description

@CameronBrooks11

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.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    silence-defectSilence read as success. Add the case to https://heibench.com/silence.html

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions