Fix handling of zero-length line segments during pretty SVG export

Fixess gitlab issue #8
This commit is contained in:
jaseg 2026-03-09 17:10:15 +01:00
parent bcc4aeefa7
commit 5ccfd7a259
4 changed files with 49 additions and 2 deletions

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)