gerberdiff 0.29.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gerberdiff/__init__.py +86 -0
- gerberdiff/cli.py +527 -0
- gerberdiff/diff/__init__.py +0 -0
- gerberdiff/diff/diff_engine.py +409 -0
- gerberdiff/diff/layer_matcher.py +202 -0
- gerberdiff/export/__init__.py +0 -0
- gerberdiff/export/json_report.py +184 -0
- gerberdiff/export/png_export.py +92 -0
- gerberdiff/export/svg_export.py +187 -0
- gerberdiff/geometry/__init__.py +37 -0
- gerberdiff/geometry/attribute.py +203 -0
- gerberdiff/geometry/driver.py +227 -0
- gerberdiff/geometry/expand.py +232 -0
- gerberdiff/geometry/geom_diff.py +153 -0
- gerberdiff/geometry/layer_geometry.py +665 -0
- gerberdiff/geometry/macro_geom.py +215 -0
- gerberdiff/geometry/primitives.py +108 -0
- gerberdiff/geometry/types.py +85 -0
- gerberdiff/parse/__init__.py +0 -0
- gerberdiff/parse/arc_math.py +162 -0
- gerberdiff/parse/excellon_parser.py +338 -0
- gerberdiff/parse/gerber_parser.py +244 -0
- gerberdiff/parse/gerber_state.py +780 -0
- gerberdiff/parse/macro_parser.py +604 -0
- gerberdiff/parse/tokenizer.py +153 -0
- gerberdiff/py.typed +0 -0
- gerberdiff/render/__init__.py +0 -0
- gerberdiff/render/compiled_render.py +240 -0
- gerberdiff/render/draw_ops.py +205 -0
- gerberdiff/render/macro_renderer.py +343 -0
- gerberdiff/render/renderer.py +283 -0
- gerberdiff/render/viewport.py +85 -0
- gerberdiff/types.py +360 -0
- gerberdiff-0.29.0.dist-info/METADATA +105 -0
- gerberdiff-0.29.0.dist-info/RECORD +38 -0
- gerberdiff-0.29.0.dist-info/WHEEL +4 -0
- gerberdiff-0.29.0.dist-info/entry_points.txt +2 -0
- gerberdiff-0.29.0.dist-info/licenses/LICENSE +201 -0
gerberdiff/types.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, Literal, TypeAlias
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
# MacroDef lives in parse/macro_parser.py (Phase 4). Imported only for
|
|
11
|
+
# type-checking to avoid a runtime circular dependency.
|
|
12
|
+
from gerberdiff.parse.macro_parser import MacroDef
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Enums
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ApertureType(StrEnum):
|
|
21
|
+
Circle = "circle"
|
|
22
|
+
Rectangle = "rectangle"
|
|
23
|
+
Obround = "obround"
|
|
24
|
+
Polygon = "polygon"
|
|
25
|
+
Macro = "macro"
|
|
26
|
+
Block = "block"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ApertureState(StrEnum):
|
|
30
|
+
Off = "off"
|
|
31
|
+
On = "on"
|
|
32
|
+
Flash = "flash"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class InterpolationMode(StrEnum):
|
|
36
|
+
Linear = "linear"
|
|
37
|
+
ClockwiseCircular = "cw"
|
|
38
|
+
CounterClockwiseCircular = "ccw"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Polarity(StrEnum):
|
|
42
|
+
Dark = "dark"
|
|
43
|
+
Clear = "clear"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MirrorState(StrEnum):
|
|
47
|
+
# None_ avoids collision with the Python builtin None
|
|
48
|
+
None_ = "none"
|
|
49
|
+
FlipA = "flipA"
|
|
50
|
+
FlipB = "flipB"
|
|
51
|
+
FlipAB = "flipAB"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class UnitType(StrEnum):
|
|
55
|
+
Inch = "inch"
|
|
56
|
+
Millimeter = "mm"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ZeroOmission(StrEnum):
|
|
60
|
+
Leading = "leading" # leading zeros omitted (most common, RS-274X default)
|
|
61
|
+
Trailing = "trailing" # trailing zeros omitted
|
|
62
|
+
Explicit = "explicit" # all digits present (rare)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CoordinateMode(StrEnum):
|
|
66
|
+
Absolute = "absolute"
|
|
67
|
+
Incremental = "incremental"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DiagnosticSeverity(StrEnum):
|
|
71
|
+
Error = "error" # abort: parse result is unusable
|
|
72
|
+
Warning = "warning" # proceed: result may be degraded
|
|
73
|
+
Info = "info" # informational, suppressed unless -v
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class LayerStatus(StrEnum):
|
|
77
|
+
Matched = "matched"
|
|
78
|
+
Added = "added"
|
|
79
|
+
Removed = "removed"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class LayerType(StrEnum):
|
|
83
|
+
FCu = "FCu"
|
|
84
|
+
BCu = "BCu"
|
|
85
|
+
InCu = "InCu"
|
|
86
|
+
FMask = "FMask"
|
|
87
|
+
BMask = "BMask"
|
|
88
|
+
FPaste = "FPaste"
|
|
89
|
+
BPaste = "BPaste"
|
|
90
|
+
FSilk = "FSilk"
|
|
91
|
+
BSilk = "BSilk"
|
|
92
|
+
EdgeCuts = "EdgeCuts"
|
|
93
|
+
NPTH = "NPTH"
|
|
94
|
+
PTH = "PTH"
|
|
95
|
+
Drill = "Drill"
|
|
96
|
+
Unknown = "Unknown"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# Geometric primitives
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class ArcSegment:
|
|
106
|
+
"""Fully resolved arc geometry. Angles in degrees."""
|
|
107
|
+
|
|
108
|
+
center_x: float
|
|
109
|
+
center_y: float
|
|
110
|
+
radius: float
|
|
111
|
+
start_angle_deg: float
|
|
112
|
+
end_angle_deg: float
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class BoundingBox:
|
|
117
|
+
"""Axis-aligned bounding box. All values in inches.
|
|
118
|
+
|
|
119
|
+
Initialises to the sentinel state {+inf, +inf, -inf, -inf} so that the
|
|
120
|
+
first call to expand() sets the box correctly. Check is_valid before use.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
# math.inf is an immutable float constant -- field(default=...) is correct.
|
|
124
|
+
min_x: float = field(default=math.inf)
|
|
125
|
+
min_y: float = field(default=math.inf)
|
|
126
|
+
max_x: float = field(default=-math.inf)
|
|
127
|
+
max_y: float = field(default=-math.inf)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def is_valid(self) -> bool:
|
|
131
|
+
"""True if at least one point has been added."""
|
|
132
|
+
return math.isfinite(self.min_x)
|
|
133
|
+
|
|
134
|
+
def expand(self, x: float, y: float, radius: float = 0.0) -> None:
|
|
135
|
+
"""Expand to include the point (x+/-radius, y+/-radius)."""
|
|
136
|
+
self.min_x = min(self.min_x, x - radius)
|
|
137
|
+
self.min_y = min(self.min_y, y - radius)
|
|
138
|
+
self.max_x = max(self.max_x, x + radius)
|
|
139
|
+
self.max_y = max(self.max_y, y + radius)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Step-and-repeat / layer / net state
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class StepAndRepeat:
|
|
149
|
+
x: int = 1 # repeat count X (>=1)
|
|
150
|
+
y: int = 1 # repeat count Y (>=1)
|
|
151
|
+
dist_x: float = 0.0 # step distance X in inches
|
|
152
|
+
dist_y: float = 0.0 # step distance Y in inches
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass
|
|
156
|
+
class LayerState:
|
|
157
|
+
polarity: Polarity = Polarity.Dark
|
|
158
|
+
rotation: float = 0.0
|
|
159
|
+
mirror: MirrorState = MirrorState.None_
|
|
160
|
+
scale: float = 1.0
|
|
161
|
+
step_and_repeat: StepAndRepeat = field(default_factory=StepAndRepeat)
|
|
162
|
+
name: str | None = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class CoordState:
|
|
167
|
+
"""Snapshot of global image-level state at the time a net was emitted.
|
|
168
|
+
|
|
169
|
+
``unit`` is stored for diagnostic/display purposes only. All coordinates
|
|
170
|
+
in DrawOp are already normalised to inches by convert_coordinate at parse
|
|
171
|
+
time. The deprecated RS-274X image/axis commands (%MI%, %AS%, %OF%,
|
|
172
|
+
%SF%) are silently ignored by the parser; their fields have been removed
|
|
173
|
+
from this type.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
unit: UnitType = UnitType.Inch
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class DrawOp:
|
|
181
|
+
"""A single drawing operation. All coordinates in inches."""
|
|
182
|
+
|
|
183
|
+
start_x: float
|
|
184
|
+
start_y: float
|
|
185
|
+
stop_x: float
|
|
186
|
+
stop_y: float
|
|
187
|
+
aperture_index: int
|
|
188
|
+
aperture_state: ApertureState
|
|
189
|
+
interpolation: InterpolationMode
|
|
190
|
+
layer_index: int
|
|
191
|
+
net_state_index: int
|
|
192
|
+
arc_segment: ArcSegment | None = None
|
|
193
|
+
attributes: dict[str, str] | None = None # %TO.* object attributes
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass
|
|
197
|
+
class RegionFill:
|
|
198
|
+
"""A filled region produced by a G36..G37 block.
|
|
199
|
+
|
|
200
|
+
``segments`` are the ``DrawOp`` objects from inside the region (G36/G37
|
|
201
|
+
are not included). All coordinates in inches.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
layer_index: int
|
|
205
|
+
net_state_index: int
|
|
206
|
+
segments: list[DrawOp]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@dataclass
|
|
210
|
+
class Diagnostic:
|
|
211
|
+
severity: DiagnosticSeverity
|
|
212
|
+
message: str
|
|
213
|
+
line: int | None = None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# Aperture definitions
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@dataclass
|
|
222
|
+
class CircleAperture:
|
|
223
|
+
aperture_type: Literal[ApertureType.Circle] = ApertureType.Circle
|
|
224
|
+
diameter: float = 0.0 # inches
|
|
225
|
+
hole_diameter: float | None = None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@dataclass
|
|
229
|
+
class RectangleAperture:
|
|
230
|
+
aperture_type: Literal[ApertureType.Rectangle] = ApertureType.Rectangle
|
|
231
|
+
width: float = 0.0
|
|
232
|
+
height: float = 0.0
|
|
233
|
+
hole_diameter: float | None = None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass
|
|
237
|
+
class ObroundAperture:
|
|
238
|
+
aperture_type: Literal[ApertureType.Obround] = ApertureType.Obround
|
|
239
|
+
width: float = 0.0
|
|
240
|
+
height: float = 0.0
|
|
241
|
+
hole_diameter: float | None = None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@dataclass
|
|
245
|
+
class PolygonAperture:
|
|
246
|
+
aperture_type: Literal[ApertureType.Polygon] = ApertureType.Polygon
|
|
247
|
+
outer_diameter: float = 0.0
|
|
248
|
+
num_vertices: int = 4
|
|
249
|
+
rotation: float = 0.0
|
|
250
|
+
hole_diameter: float | None = None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@dataclass
|
|
254
|
+
class MacroAperture:
|
|
255
|
+
aperture_type: Literal[ApertureType.Macro] = ApertureType.Macro
|
|
256
|
+
macro_def: MacroDef | None = None # defined in parse/macro_parser.py (Phase 4)
|
|
257
|
+
params: list[float] = field(default_factory=list)
|
|
258
|
+
unit_scale: float = 1.0 # 1.0 for inch files; 1/25.4 for mm files
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@dataclass
|
|
262
|
+
class BlockAperture:
|
|
263
|
+
aperture_type: Literal[ApertureType.Block] = ApertureType.Block
|
|
264
|
+
draw_ops: list[DrawOp | RegionFill] = field(default_factory=list)
|
|
265
|
+
apertures: dict[int, Aperture] = field(default_factory=dict)
|
|
266
|
+
layers: list[LayerState] = field(default_factory=list)
|
|
267
|
+
bounding_box: BoundingBox = field(default_factory=BoundingBox)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# Union type alias -- used for aperture dict values and type-narrowing dispatch.
|
|
271
|
+
Aperture: TypeAlias = (
|
|
272
|
+
CircleAperture
|
|
273
|
+
| RectangleAperture
|
|
274
|
+
| ObroundAperture
|
|
275
|
+
| PolygonAperture
|
|
276
|
+
| MacroAperture
|
|
277
|
+
| BlockAperture
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
# Top-level IR output
|
|
283
|
+
# ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@dataclass
|
|
287
|
+
class ParsedImage:
|
|
288
|
+
"""The complete output of parsing one Gerber or Excellon file."""
|
|
289
|
+
|
|
290
|
+
draw_ops: list[DrawOp | RegionFill]
|
|
291
|
+
apertures: dict[int, Aperture] # D-code -> aperture
|
|
292
|
+
layers: list[LayerState]
|
|
293
|
+
coord_states: list[CoordState]
|
|
294
|
+
bounding_box: BoundingBox
|
|
295
|
+
diagnostics: list[Diagnostic]
|
|
296
|
+
source_path: Path | None = None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
# Diff result types
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@dataclass
|
|
305
|
+
class Region:
|
|
306
|
+
"""A contiguous changed area. Coordinates in inches."""
|
|
307
|
+
|
|
308
|
+
id: int
|
|
309
|
+
centroid_x: float
|
|
310
|
+
centroid_y: float
|
|
311
|
+
bounding_box: BoundingBox # consistent naming with ParsedImage, BlockAperture
|
|
312
|
+
pixel_count: int
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@dataclass
|
|
316
|
+
class LayerDiffResult:
|
|
317
|
+
name: str
|
|
318
|
+
status: LayerStatus # matched | added | removed
|
|
319
|
+
layer_type: LayerType
|
|
320
|
+
changed_pixel_count: int
|
|
321
|
+
total_pixel_count: int
|
|
322
|
+
regions: list[Region]
|
|
323
|
+
|
|
324
|
+
@property
|
|
325
|
+
def changed_fraction(self) -> float:
|
|
326
|
+
if self.total_pixel_count == 0:
|
|
327
|
+
return 0.0
|
|
328
|
+
return self.changed_pixel_count / self.total_pixel_count
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@dataclass
|
|
332
|
+
class DiffResult:
|
|
333
|
+
layers: list[LayerDiffResult]
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def has_changes(self) -> bool:
|
|
337
|
+
"""True when any layer was added, removed, or has changed pixels."""
|
|
338
|
+
return any(
|
|
339
|
+
lr.changed_pixel_count > 0 or lr.status != LayerStatus.Matched for lr in self.layers
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# ---------------------------------------------------------------------------
|
|
344
|
+
# Exceptions
|
|
345
|
+
# ---------------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class GerberParseError(Exception):
|
|
349
|
+
"""Raised by ``compute_full_diff`` when a fatal parse error is encountered.
|
|
350
|
+
|
|
351
|
+
Attributes
|
|
352
|
+
----------
|
|
353
|
+
path : Path
|
|
354
|
+
The file that triggered the error.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
def __init__(self, path: Path, message: str, line: int | None = None) -> None:
|
|
358
|
+
self.path = path
|
|
359
|
+
loc = f" (line {line})" if line is not None else ""
|
|
360
|
+
super().__init__(f"{path.name}: {message}{loc}")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gerberdiff
|
|
3
|
+
Version: 0.29.0
|
|
4
|
+
Summary: Visual raster diff tool for Gerber/Excellon PCB design files
|
|
5
|
+
Project-URL: Homepage, https://github.com/CameronBrooks11/gerberdiff
|
|
6
|
+
Project-URL: Repository, https://github.com/CameronBrooks11/gerberdiff
|
|
7
|
+
Project-URL: Changelog, https://github.com/CameronBrooks11/gerberdiff/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Issues, https://github.com/CameronBrooks11/gerberdiff/issues
|
|
9
|
+
Author-email: Cameron Brooks <cameronbrooks11@gmail.com>
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: diff,eda,excellon,gerber,grbl,kicad,pcb
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Manufacturing
|
|
16
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: cairocffi>=1.6
|
|
26
|
+
Requires-Dist: click>=8
|
|
27
|
+
Requires-Dist: numpy>=1.24
|
|
28
|
+
Requires-Dist: scipy>=1.10
|
|
29
|
+
Requires-Dist: shapely>=2.0
|
|
30
|
+
Provides-Extra: rich
|
|
31
|
+
Requires-Dist: rich>=13; extra == 'rich'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# gerberdiff
|
|
35
|
+
|
|
36
|
+
[](https://github.com/CameronBrooks11/gerberdiff/actions/workflows/ci.yml)
|
|
37
|
+
[](https://pypi.org/project/gerberdiff/)
|
|
38
|
+
[](https://pypi.org/project/gerberdiff/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
|
|
41
|
+
Diff tool for Gerber/Excellon PCB design files, with two complementary
|
|
42
|
+
engines:
|
|
43
|
+
|
|
44
|
+
- **Raster diff** (`diff`) -- visual overlay PNGs of changed pixels.
|
|
45
|
+
- **Geometry diff** (`geomdiff`) -- resolution-independent, attributed
|
|
46
|
+
changes (`moved` / `resized` / `added` / `removed`) computed on the
|
|
47
|
+
parsed vector geometry, down to micrometre displacements.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
pip install gerberdiff
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Requires Python >= 3.11. The raster engine needs the system Cairo library
|
|
56
|
+
(`libcairo2` on Debian/Ubuntu, `cairo` via Homebrew); the geometry engine
|
|
57
|
+
is Cairo-free.
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
# Geometry diff: what moved, resized, was added or removed -- and by how much
|
|
63
|
+
gerberdiff geomdiff before/ after/ --out-json report.json --out-svg overlays/
|
|
64
|
+
|
|
65
|
+
# Raster diff: visual overlay PNGs
|
|
66
|
+
gerberdiff diff before/ after/ --out-json report.json --out-png diffs/
|
|
67
|
+
|
|
68
|
+
# Exit 1 if any changes detected (useful in CI)
|
|
69
|
+
gerberdiff geomdiff before/ after/ --fail-on-diff
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import gerberdiff
|
|
74
|
+
from pathlib import Path
|
|
75
|
+
|
|
76
|
+
result = gerberdiff.compute_geometry_diff(Path("before/"), Path("after/"))
|
|
77
|
+
for layer in result.layers:
|
|
78
|
+
for change in layer.changes:
|
|
79
|
+
print(f"{layer.name}: {change.kind} {change.op_kind} "
|
|
80
|
+
f"dx={change.dx_mm} dy={change.dy_mm}")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Docs
|
|
84
|
+
|
|
85
|
+
| Topic | File |
|
|
86
|
+
| -------------------- | -------------------------------------------- |
|
|
87
|
+
| CLI reference | [docs/cli.md](docs/cli.md) |
|
|
88
|
+
| Python API | [docs/api.md](docs/api.md) |
|
|
89
|
+
| JSON report schemas | [docs/schema.md](docs/schema.md) |
|
|
90
|
+
| Architecture | [docs/architecture.md](docs/architecture.md) |
|
|
91
|
+
| Geometry diff engine | [docs/geometry-diff.md](docs/geometry-diff.md) |
|
|
92
|
+
|
|
93
|
+
## Known limitations
|
|
94
|
+
|
|
95
|
+
- **Excellon rout mode:** only drill hits are processed; routing paths produce a
|
|
96
|
+
`Warning` diagnostic but no geometry.
|
|
97
|
+
- **Deprecated RS-274X commands (`%MI%`, `%OF%`, `%SF%`, `%AS%`):** ignored with an
|
|
98
|
+
`Info` diagnostic.
|
|
99
|
+
- **Rectangle/obround aperture strokes:** the raster engine strokes with
|
|
100
|
+
`max(width, height)`; the geometry engine computes the exact Minkowski sum
|
|
101
|
+
for linear strokes (see [docs/geometry-diff.md](docs/geometry-diff.md)).
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
gerberdiff/__init__.py,sha256=k4SXSMEbSXgIhrUEzQmCfqiJ28rjNbJ-VjVtVeXpQDc,2126
|
|
2
|
+
gerberdiff/cli.py,sha256=i7aYlcYie7jnfJMPXIboeXWU-cQsVOXV0j0Nmc53bm4,18796
|
|
3
|
+
gerberdiff/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
gerberdiff/types.py,sha256=oH782FolWQepErTjjUR1yIN5Owum9ASkWAzkC0nhz5I,9710
|
|
5
|
+
gerberdiff/diff/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
gerberdiff/diff/diff_engine.py,sha256=lr0Ppw4N9Bf5gqpBIr3lu218ZPQzbPMRuhOhDCxBq7Q,14219
|
|
7
|
+
gerberdiff/diff/layer_matcher.py,sha256=ygj1nYC6-8KiVfinNSR4OY_3POtAnzCInMn762UbImo,6732
|
|
8
|
+
gerberdiff/export/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
gerberdiff/export/json_report.py,sha256=RjVE2cGzgkLKYdPcdVZ2gwh_2G-EADMQEUDDlQuZHAM,6075
|
|
10
|
+
gerberdiff/export/png_export.py,sha256=2WNb_Jfpj8G57kKsx6hMJzkNbgJrh5JMJp93SiM_2lw,3110
|
|
11
|
+
gerberdiff/export/svg_export.py,sha256=npkFHTH9--ZXwKGLsQJ-s3wuUbawWadRtISr0xtC8w4,6302
|
|
12
|
+
gerberdiff/geometry/__init__.py,sha256=K3uHr08lxQgEbTrRfvyTKAILVLDloV1lvj-IbizFNfo,929
|
|
13
|
+
gerberdiff/geometry/attribute.py,sha256=M2yjYATNywovknHQmNh7Vw0cIDXWafcV-hN_DOsjS3g,7470
|
|
14
|
+
gerberdiff/geometry/driver.py,sha256=SR3MM9JQOYrmnXyu8W0hvKYoUeEoQ7zlIg6vR6e4BFk,8042
|
|
15
|
+
gerberdiff/geometry/expand.py,sha256=gLpXGjMJAXzepuuu09-sKn5fjwBtdNFjiLAkkrUNOgc,7818
|
|
16
|
+
gerberdiff/geometry/geom_diff.py,sha256=5jIY5ybKskQYpq3H1St0gghPjxIvJhi9C6sZqidp-oc,5596
|
|
17
|
+
gerberdiff/geometry/layer_geometry.py,sha256=RyAJnwW2yKVrQ99z0cN86TSgEEuP1aEOs9IHNpeGKmk,22165
|
|
18
|
+
gerberdiff/geometry/macro_geom.py,sha256=TKiu-fH4SSMg2XwPzfMv_PKooZeTlO6sXZxsoAGyizI,7304
|
|
19
|
+
gerberdiff/geometry/primitives.py,sha256=By8gdNQLqwb3pwoimjvM2gXrmaWGMcjmIAAWwt3rdQw,3675
|
|
20
|
+
gerberdiff/geometry/types.py,sha256=DtTCZ2RTVokgos2DyfMzkAfrDnnysNA-JUnFMCLI25w,2667
|
|
21
|
+
gerberdiff/parse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
gerberdiff/parse/arc_math.py,sha256=pwe6Hva6lwrZzX8VMu2Pd0XrUL1cPsc-p3571EWCpSQ,5379
|
|
23
|
+
gerberdiff/parse/excellon_parser.py,sha256=pdRu7rAY3MzE9mIFLrkV7wGx86Ntff1ZmCzpEDRy5Io,10900
|
|
24
|
+
gerberdiff/parse/gerber_parser.py,sha256=3HVX2Oz-Ng7C5KvKahkgYMvYD1sZGxsZhbThnsjMryw,7594
|
|
25
|
+
gerberdiff/parse/gerber_state.py,sha256=e3XfM6hy3K8aQrkNdQ8wdjON_Ycv9RT5sXX9ir69ajo,28747
|
|
26
|
+
gerberdiff/parse/macro_parser.py,sha256=WIEXtEzYeb5RwUoEhFMGUQGpcACleh3hIlzXOlOEWQU,19841
|
|
27
|
+
gerberdiff/parse/tokenizer.py,sha256=6mhCv_VngFMKqVjMJ4KelHyWesgVbSFnn5EPXk2HzSs,4649
|
|
28
|
+
gerberdiff/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
+
gerberdiff/render/compiled_render.py,sha256=yyKeeYZsgFl4o4aNS5_G6Z0Y7b3JOtTX5b-g-pY0w28,7865
|
|
30
|
+
gerberdiff/render/draw_ops.py,sha256=CeINqVED4btp-_ZorjmCj1PvGn842eMVZz0hEyr4laA,7033
|
|
31
|
+
gerberdiff/render/macro_renderer.py,sha256=mCbwONgG2v2QSSzwZM4R8xyU18oEPtZPqst2002gIgY,10776
|
|
32
|
+
gerberdiff/render/renderer.py,sha256=sNoG0xZFnwb-Y2WAw02ZdyXaVRqiL4JtXdf5Bblt_jQ,9657
|
|
33
|
+
gerberdiff/render/viewport.py,sha256=Ym2iCFT4GkFEOp5Mtb6IGfy_LGI051VSM4TReWmzQxU,2520
|
|
34
|
+
gerberdiff-0.29.0.dist-info/METADATA,sha256=NoejnAu3AVP2g7D_Gox9B_2zekpn43DasibWLAOsLW8,4180
|
|
35
|
+
gerberdiff-0.29.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
36
|
+
gerberdiff-0.29.0.dist-info/entry_points.txt,sha256=0p8xVyTFlgL4mFCJwsKRrOPAIIF-X9HBut7jG16SbBE,50
|
|
37
|
+
gerberdiff-0.29.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
38
|
+
gerberdiff-0.29.0.dist-info/RECORD,,
|