KiCad PCB EDA Suite
Loading...
Searching...
No Matches
gerber_diff.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2025 KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software: you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20#include "gerber_diff.h"
21#include "gerber_to_png.h"
23#include <base_units.h>
24#include <cmath>
25#include <json_common.h>
26
27
28VECTOR2I CalculateAlignment( const SHAPE_POLY_SET& aReference, const SHAPE_POLY_SET& aComparison )
29{
30 if( aReference.OutlineCount() == 0 || aComparison.OutlineCount() == 0 )
31 return VECTOR2I( 0, 0 );
32
33 BOX2I refBBox = aReference.BBox();
34 BOX2I cmpBBox = aComparison.BBox();
35
36 // Align by bounding box origins
37 return refBBox.GetOrigin() - cmpBBox.GetOrigin();
38}
39
40
42{
44
45 constexpr double IU2_TO_MM2 = 1e-10;
46
47 // overlap = reference ∩ comparison
48 result.overlap = aReference;
49 result.overlap.BooleanIntersection( aComparison );
50
51 // additions = comparison - reference
52 result.additions = aComparison;
53 result.additions.BooleanSubtract( aReference );
54
55 // removals = reference - comparison
56 result.removals = aReference;
57 result.removals.BooleanSubtract( aComparison );
58
59 result.overlapArea = result.overlap.Area() * IU2_TO_MM2;
60 result.additionsArea = result.additions.Area() * IU2_TO_MM2;
61 result.removalsArea = result.removals.Area() * IU2_TO_MM2;
62
63 // reference = overlap ∪ removals, comparison = overlap ∪ additions (disjoint)
64 result.referenceArea = result.overlapArea + result.removalsArea;
65 result.comparisonArea = result.overlapArea + result.additionsArea;
66
67 if( result.referenceArea > 0.0 )
68 {
69 result.additionsPercent = 100.0 * result.additionsArea / result.referenceArea;
70 result.removalsPercent = 100.0 * result.removalsArea / result.referenceArea;
71 result.netChangePercent = result.additionsPercent - result.removalsPercent;
72 }
73
74 // Fracture so that holes become bridge-connected outlines suitable for rendering
75 // with PlotPoly (which only fills outlines, ignoring SHAPE_POLY_SET holes).
76 result.overlap.Fracture();
77 result.additions.Fracture();
78 result.removals.Fracture();
79
80 return result;
81}
82
83
84namespace
85{
86
87// Helper to convert mm^2 to other units
88double ConvertArea( double aMm2, DIFF_UNITS aUnits )
89{
90 switch( aUnits )
91 {
92 case DIFF_UNITS::MM: return aMm2;
93
94 case DIFF_UNITS::INCH: return aMm2 / ( 25.4 * 25.4 ); // mm^2 to in^2
95
96 case DIFF_UNITS::MILS: return aMm2 / ( 0.0254 * 0.0254 ); // mm^2 to mils^2
97 }
98
99 return aMm2;
100}
101
102
103wxString GetUnitSuffix( DIFF_UNITS aUnits )
104{
105 switch( aUnits )
106 {
107 case DIFF_UNITS::MM: return wxS( "mm" );
108 case DIFF_UNITS::INCH: return wxS( "in" );
109 case DIFF_UNITS::MILS: return wxS( "mils" );
110 }
111
112 return wxS( "mm" );
113}
114
115} // anonymous namespace
116
117
118wxString FormatDiffResultText( const GERBER_DIFF_RESULT& aResult, const wxString& aFile1, const wxString& aFile2,
119 DIFF_UNITS aUnits )
120{
121 wxString suffix = GetUnitSuffix( aUnits );
122 wxString out;
123
124 out += wxString::Format( wxS( "Comparing: %s vs %s\n\n" ), aFile1, aFile2 );
125
126 out += wxString::Format( wxS( "Reference area: %.2f %s^2\n" ), ConvertArea( aResult.referenceArea, aUnits ),
127 suffix );
128 out += wxString::Format( wxS( "Comparison area: %.2f %s^2\n" ), ConvertArea( aResult.comparisonArea, aUnits ),
129 suffix );
130 out += wxString::Format( wxS( "Overlap area: %.2f %s^2\n\n" ), ConvertArea( aResult.overlapArea, aUnits ),
131 suffix );
132
133 wxString addSign = aResult.additionsPercent >= 0 ? wxS( "+" ) : wxS( "" );
134 wxString remSign = aResult.removalsPercent > 0 ? wxS( "-" ) : wxS( "" );
135 wxString netSign = aResult.netChangePercent >= 0 ? wxS( "+" ) : wxS( "" );
136
137 out += wxString::Format( wxS( "Additions: %.2f %s^2 (%s%.2f%% of reference)\n" ),
138 ConvertArea( aResult.additionsArea, aUnits ), suffix, addSign, aResult.additionsPercent );
139 out += wxString::Format( wxS( "Removals: %.2f %s^2 (%s%.2f%% of reference)\n" ),
140 ConvertArea( aResult.removalsArea, aUnits ), suffix, remSign, aResult.removalsPercent );
141 out += wxString::Format( wxS( "Net change: %s%.2f %s^2 (%s%.2f%%)\n" ), netSign,
142 ConvertArea( aResult.additionsArea - aResult.removalsArea, aUnits ), suffix, netSign,
143 aResult.netChangePercent );
144
145 return out;
146}
147
148
149nlohmann::json FormatDiffResultJson( const GERBER_DIFF_RESULT& aResult, const wxString& aFile1, const wxString& aFile2,
150 DIFF_UNITS aUnits, double aMaxDiffPercent )
151{
152 wxString unitStr = GetUnitSuffix( aUnits );
153
154 nlohmann::json j;
155
156 j["reference_file"] = aFile1.ToStdString();
157 j["comparison_file"] = aFile2.ToStdString();
158 j["units"] = unitStr.ToStdString();
159
160 j["reference"] = { { "area", ConvertArea( aResult.referenceArea, aUnits ) } };
161
162 j["comparison"] = { { "area", ConvertArea( aResult.comparisonArea, aUnits ) } };
163
164 j["overlap"] = { { "area", ConvertArea( aResult.overlapArea, aUnits ) } };
165
166 j["additions"] = { { "area", ConvertArea( aResult.additionsArea, aUnits ) },
167 { "percent", aResult.additionsPercent } };
168
169 j["removals"] = { { "area", ConvertArea( aResult.removalsArea, aUnits ) }, { "percent", aResult.removalsPercent } };
170
171 j["net_change"] = { { "area", ConvertArea( aResult.additionsArea - aResult.removalsArea, aUnits ) },
172 { "percent", aResult.netChangePercent } };
173
174 // Determine if within threshold
175 double totalDiffPercent = aResult.additionsPercent + aResult.removalsPercent;
176 bool withinThreshold =
177 ( aMaxDiffPercent <= 0.0 ) ? ( totalDiffPercent == 0.0 ) : ( totalDiffPercent <= aMaxDiffPercent );
178
179 j["within_threshold"] = withinThreshold;
180 j["max_diff_percent"] = aMaxDiffPercent;
181 j["total_diff_percent"] = totalDiffPercent;
182
183 return j;
184}
185
186
187namespace
188{
189
193void RenderPolySet( PNG_PLOTTER& aPlotter, const SHAPE_POLY_SET& aPolySet, const KIGFX::COLOR4D& aColor )
194{
195 aPlotter.SetColor( aColor );
196
197 for( int i = 0; i < aPolySet.OutlineCount(); i++ )
198 {
199 const std::vector<VECTOR2I>& pts = aPolySet.COutline( i ).CPoints();
200
201 if( pts.size() >= 3 )
202 aPlotter.PlotPoly( pts, FILL_T::FILLED_SHAPE, 0 );
203 }
204}
205
206} // anonymous namespace
207
208
209bool RenderDiffToPng( const GERBER_DIFF_RESULT& aResult, const wxString& aOutputPath,
210 const DIFF_RENDER_OPTIONS& aOptions )
211{
212 // Calculate combined bounding box
213 BOX2I bbox;
214 bool first = true;
215
216 auto mergeBBox = [&bbox, &first]( const SHAPE_POLY_SET& poly )
217 {
218 if( poly.OutlineCount() > 0 )
219 {
220 BOX2I polyBox = poly.BBox();
221
222 if( first )
223 {
224 bbox = polyBox;
225 first = false;
226 }
227 else
228 {
229 bbox.Merge( polyBox );
230 }
231 }
232 };
233
234 mergeBBox( aResult.overlap );
235 mergeBBox( aResult.additions );
236 mergeBBox( aResult.removals );
237
238 if( first || bbox.GetWidth() == 0 || bbox.GetHeight() == 0 )
239 return false;
240
241 GERBER_PLOTTER_VIEWPORT vp = CalculatePlotterViewport( bbox, aOptions.dpi, aOptions.width, aOptions.height );
242
243 PNG_PLOTTER plotter;
244 plotter.SetPixelSize( vp.width, vp.height );
245 plotter.SetResolution( aOptions.dpi );
246 plotter.SetAntialias( aOptions.antialias );
247 plotter.SetBackgroundColor( aOptions.colorBackground );
248 plotter.SetViewport( vp.offset, vp.iuPerDecimil, vp.plotScale, false );
249
250 if( !plotter.StartPlot( wxEmptyString ) )
251 return false;
252
253 // Render in order: overlap first, then additions, then removals
254 // This ensures removals (red) are most visible on top
255 RenderPolySet( plotter, aResult.overlap, aOptions.colorOverlap );
256 RenderPolySet( plotter, aResult.additions, aOptions.colorAdditions );
257 RenderPolySet( plotter, aResult.removals, aOptions.colorRemovals );
258
259 plotter.EndPlot();
260
261 return plotter.SaveFile( aOutputPath );
262}
BOX2< VECTOR2I > BOX2I
Definition box2.h:922
constexpr size_type GetWidth() const
Definition box2.h:214
constexpr BOX2< Vec > & Merge(const BOX2< Vec > &aRect)
Modify the position and size of the rectangle in order to contain aRect.
Definition box2.h:658
constexpr size_type GetHeight() const
Definition box2.h:215
constexpr const Vec & GetOrigin() const
Definition box2.h:210
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:105
PNG rasterization plotter using Cairo graphics library.
Definition plotter_png.h:34
void SetPixelSize(int aWidth, int aHeight)
Set the output image dimensions in pixels.
Definition plotter_png.h:59
bool SaveFile(const wxString &aPath)
Save the rendered image to a PNG file.
void SetBackgroundColor(const COLOR4D &aColor)
Set the background color for the image.
Definition plotter_png.h:72
void SetResolution(int aDPI)
Set the output resolution in dots per inch.
Definition plotter_png.h:51
virtual bool EndPlot() override
void SetAntialias(bool aEnable)
Enable or disable anti-aliasing.
Definition plotter_png.h:79
virtual void SetColor(const COLOR4D &aColor) override
virtual void PlotPoly(const std::vector< VECTOR2I > &aCornerList, FILL_T aFill, int aWidth, void *aData=nullptr) override
Draw a polygon ( filled or not ).
virtual void SetViewport(const VECTOR2I &aOffset, double aIusPerDecimil, double aScale, bool aMirror) override
Set the plot offset and scaling for the current plot.
virtual bool StartPlot(const wxString &aPageNumber) override
const std::vector< VECTOR2I > & CPoints() const
Represent a set of closed polygons.
int OutlineCount() const
Return the number of outlines in the set.
const SHAPE_LINE_CHAIN & COutline(int aIndex) const
const BOX2I BBox(int aClearance=0) const override
Compute a bounding box of the shape, with a margin of aClearance a collision.
@ FILLED_SHAPE
Fill with object color.
Definition eda_shape.h:60
wxString FormatDiffResultText(const GERBER_DIFF_RESULT &aResult, const wxString &aFile1, const wxString &aFile2, DIFF_UNITS aUnits)
Format a diff result as human-readable text.
GERBER_DIFF_RESULT CalculateGerberDiff(const SHAPE_POLY_SET &aReference, const SHAPE_POLY_SET &aComparison)
Calculate the geometric differences between two poly sets.
VECTOR2I CalculateAlignment(const SHAPE_POLY_SET &aReference, const SHAPE_POLY_SET &aComparison)
Calculate the alignment offset to make a comparison poly set align with a reference.
nlohmann::json FormatDiffResultJson(const GERBER_DIFF_RESULT &aResult, const wxString &aFile1, const wxString &aFile2, DIFF_UNITS aUnits, double aMaxDiffPercent)
Format a diff result as JSON.
bool RenderDiffToPng(const GERBER_DIFF_RESULT &aResult, const wxString &aOutputPath, const DIFF_RENDER_OPTIONS &aOptions)
Render a diff result to PNG with colored regions.
DIFF_UNITS
Units for diff result formatting.
Definition gerber_diff.h:93
GERBER_PLOTTER_VIEWPORT CalculatePlotterViewport(const BOX2I &aBBox, int aDpi, int aWidth, int aHeight)
Compute pixel dimensions and plotter scale from a bounding box and DPI/size settings.
Options for diff PNG rendering.
KIGFX::COLOR4D colorRemovals
Red.
KIGFX::COLOR4D colorAdditions
Green.
int height
0 = calculate from DPI
KIGFX::COLOR4D colorOverlap
Gray.
KIGFX::COLOR4D colorBackground
White.
int width
0 = calculate from DPI
Result of a Gerber diff calculation containing the geometric differences and area statistics.
Definition gerber_diff.h:35
SHAPE_POLY_SET removals
Areas in file1 but not file2 (file1 AND NOT file2)
Definition gerber_diff.h:38
double removalsPercent
Removals as percent of reference area.
Definition gerber_diff.h:47
double additionsArea
Area of additions in mm^2.
Definition gerber_diff.h:43
SHAPE_POLY_SET overlap
Areas present in both files (file1 AND file2)
Definition gerber_diff.h:36
double referenceArea
Total area of file1 in mm^2.
Definition gerber_diff.h:40
double overlapArea
Area of overlap in mm^2.
Definition gerber_diff.h:42
double comparisonArea
Total area of file2 in mm^2.
Definition gerber_diff.h:41
double netChangePercent
Net change as percent of reference area.
Definition gerber_diff.h:48
double removalsArea
Area of removals in mm^2.
Definition gerber_diff.h:44
double additionsPercent
Additions as percent of reference area.
Definition gerber_diff.h:46
SHAPE_POLY_SET additions
Areas in file2 but not file1 (file2 AND NOT file1)
Definition gerber_diff.h:37
Computed plotter viewport parameters from a bounding box and render settings.
wxString result
Test unit parsing edge cases and error handling.
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:687