diff --git a/src/gerbonara/graphic_primitives.py b/src/gerbonara/graphic_primitives.py index 20f7634..e644c0e 100644 --- a/src/gerbonara/graphic_primitives.py +++ b/src/gerbonara/graphic_primitives.py @@ -62,6 +62,12 @@ class GraphicPrimitive: raise NotImplementedError() + def is_zero_size(self): + """ Return whether this primitive is zero size + + :rtype: bool + """ + @dataclass(frozen=True) class Circle(GraphicPrimitive): @@ -84,6 +90,9 @@ class Circle(GraphicPrimitive): [(True, (self.x, self.y)), (True, (self.x, self.y))], polarity_dark=self.polarity_dark) + def is_zero_size(self): + return math.isclose(self.r, 0) + @dataclass(frozen=True) class ArcPoly(GraphicPrimitive): @@ -180,6 +189,20 @@ class ArcPoly(GraphicPrimitive): return self + def is_zero_size(self): + for (x1, y1), (x2, y2), (clockwise, (cx, cy)) in self.segments: + if clockwise is not None: # arc + if math.isclose(cx, x1) and math.isclose(cy, y1): + continue + + if math.isclose(x1, x2) and math.isclose(y1, y2): + return False + + if math.isclose(polygon_area(self.outline), 0): + return True + return False + + @dataclass(frozen=True) class Line(GraphicPrimitive): """ Straight line with round end caps. """ @@ -244,6 +267,9 @@ class Line(GraphicPrimitive): None, ], polarity_dark=self.polarity_dark) + def is_zero_size(self): + return math.isclose(self.x1, self.x2) and math.isclose(self.y1, self.y2) + @dataclass(frozen=True) class Arc(GraphicPrimitive): @@ -310,6 +336,9 @@ class Arc(GraphicPrimitive): (not self.clockwise, (self.cx, self.cy)), ], polarity_dark=self.polarity_dark) + def is_zero_size(self): + return False # an arc with identical start and end points is defined as a circle + @dataclass(frozen=True) class Rectangle(GraphicPrimitive): @@ -345,3 +374,6 @@ class Rectangle(GraphicPrimitive): return tag('rect', x=prec(x), y=prec(y), width=prec(self.w), height=prec(self.h), **svg_rotation(self.rotation, self.x, self.y), fill=color) + def is_zero_size(self): + return math.isclose(self.w, 0) or math.isclose(self.h, 0) + diff --git a/src/gerbonara/layers.py b/src/gerbonara/layers.py index e810375..e02ffb4 100644 --- a/src/gerbonara/layers.py +++ b/src/gerbonara/layers.py @@ -1012,7 +1012,7 @@ class LayerStack: if use == 'mask': objects.insert(0, tag('path', id='outline-path', d=self.outline_svg_d(unit=svg_unit), fill='white')) layers.append(tag('g', objects, id=f'l-{side}-{use}', filter=f'url(#f-{use})', - fill=default_fill, stroke=default_stroke, **stroke_attrs, + fill=default_fill, stroke=default_stroke, **stroke_attrs, fill_rule='evenodd', **inkscape_attrs(f'{side} {use}'), transform=layer_transform)) for i, layer in enumerate(self.drill_layers): @@ -1284,6 +1284,7 @@ class LayerStack: maybe_allegro_hint = '' if self.generator != 'allegro' else ' This file looks like it was generated by Allegro/OrCAD. These tools produce quite mal-formed gerbers, and often export text on the outline layer. If you generated this file yourself, maybe try twiddling with the export settings.' polygons = [] lines = [ obj.as_primitive(unit) for obj in self.outline.instance.objects if isinstance(obj, (go.Line, go.Arc)) ] + lines = [ prim for prim in lines if not prim.is_zero_size() ] by_x = sorted([ (obj.x1, obj) for obj in lines ] + [ (obj.x2, obj) for obj in lines ], key=lambda x: x[0]) dist_sq = lambda x1, y1, x2, y2: (x2-x1)**2 + (y2-y1)**2 diff --git a/src/gerbonara/utils.py b/src/gerbonara/utils.py index fa23f52..3e58aeb 100644 --- a/src/gerbonara/utils.py +++ b/src/gerbonara/utils.py @@ -605,6 +605,18 @@ def point_in_polygon(point, poly): return res +def polygon_area(poly): + # https://en.wikipedia.org/wiki/Shoelace_formula + + if not poly or len(poly) < 3: + return 0 + + acc = 0 + for (x1, y1), (x2, y2) in zip(poly, poly[-1:] + poly): + acc += (y1 + y2) * (x1 - x2) + return acc/2 + + def bbox_intersect(a, b): if a is None or b is None: return False diff --git a/tests/test_layers.py b/tests/test_layers.py index a5393b4..2d398ed 100644 --- a/tests/test_layers.py +++ b/tests/test_layers.py @@ -330,10 +330,12 @@ REFERENCE_DIRS = { 'wc2-F_SilkS.gbr': 'top silk', 'wc2.kicad_pcb': None, }, + 'zero-length-lines': { + } } @filter_syntax_warnings -@pytest.mark.parametrize('ref_dir,file_map', list(REFERENCE_DIRS.items())) +@pytest.mark.parametrize('ref_dir,file_map', [(k, v) for k, v in REFERENCE_DIRS.items() if v]) def test_layer_classifier(ref_dir, file_map): path = reference_path(ref_dir) print('Reference path is', path)