Fix remaining svg rendering/gerber compositing bugs
This commit is contained in:
parent
4d77937f01
commit
2ce0ff81ae
4 changed files with 25 additions and 31 deletions
|
|
@ -177,14 +177,22 @@ def point_line_distance(l1, l2, p):
|
|||
return abs((x2-x1)*(y1-y0) - (x1-x0)*(y2-y1)) / length
|
||||
|
||||
def svg_arc(old, new, center, clockwise):
|
||||
print(f'{old=} {new=} {center=}')
|
||||
r = point_distance(old, new)
|
||||
r = point_distance(old, center)
|
||||
d = point_line_distance(old, new, center)
|
||||
# invert sweep flag since the svg y axis is mirrored
|
||||
sweep_flag = int(not clockwise)
|
||||
large_arc = int((d > 0) == clockwise) # FIXME check signs
|
||||
print(f'{r=:.3} {d=:.3} {sweep_flag=} {large_arc=} {clockwise=}')
|
||||
return f'A {r:.6} {r:.6} 0 {large_arc} {sweep_flag} {new[0]:.6} {new[1]:.6}'
|
||||
# In the degenerate case where old == new, we always take the long way around. To represent this "full-circle arc"
|
||||
# in SVG, we have to split it into two.
|
||||
if math.isclose(point_distance(old, new), 0):
|
||||
intermediate = center[0] + (center[0] - old[0]), center[1] + (center[1] - old[1])
|
||||
# Note that we have to preserve the sweep flag to avoid causing self-intersections by flipping the direction of
|
||||
# a circular cutin
|
||||
return f'A {r:.6} {r:.6} 0 1 {sweep_flag} {intermediate[0]:.6} {intermediate[1]:.6} ' +\
|
||||
f'A {r:.6} {r:.6} 0 1 {sweep_flag} {new[0]:.6} {new[1]:.6}'
|
||||
|
||||
else: # normal case
|
||||
large_arc = int((d > 0) == clockwise)
|
||||
return f'A {r:.6} {r:.6} 0 {large_arc} {sweep_flag} {new[0]:.6} {new[1]:.6}'
|
||||
|
||||
@dataclass
|
||||
class ArcPoly(GraphicPrimitive):
|
||||
|
|
|
|||
|
|
@ -91,7 +91,6 @@ class GerberFile(CamFile):
|
|||
|
||||
if force_bounds is None:
|
||||
(min_x, min_y), (max_x, max_y) = self.bounding_box(svg_unit, default=((0, 0), (0, 0)))
|
||||
print('bounding box:', (min_x, min_y), (max_x, max_y))
|
||||
else:
|
||||
(min_x, min_y), (max_x, max_y) = force_bounds
|
||||
min_x = convert(min_x, arg_unit, svg_unit)
|
||||
|
|
@ -129,8 +128,9 @@ class GerberFile(CamFile):
|
|||
# dedup apertures
|
||||
new_apertures = {}
|
||||
replace_apertures = {}
|
||||
mock_settings = self.import_settings
|
||||
for ap in self.apertures + other.apertures:
|
||||
gbr = ap.to_gerber()
|
||||
gbr = ap.to_gerber(mock_settings)
|
||||
if gbr not in new_apertures:
|
||||
new_apertures[gbr] = ap
|
||||
else:
|
||||
|
|
@ -281,13 +281,7 @@ class GerberFile(CamFile):
|
|||
def offset(self, dx=0, dy=0, unit='mm'):
|
||||
# TODO round offset to file resolution
|
||||
|
||||
#print(f'offset {dx},{dy} file unit')
|
||||
#for obj in self.objects:
|
||||
# print(' ', obj)
|
||||
self.objects = [ obj.with_offset(dx, dy, unit) for obj in self.objects ]
|
||||
#print('after:')
|
||||
#for obj in self.objects:
|
||||
# print(' ', obj)
|
||||
|
||||
def rotate(self, angle:'radian', center=(0,0), unit='mm'):
|
||||
""" Rotate file contents around given point.
|
||||
|
|
@ -307,17 +301,9 @@ class GerberFile(CamFile):
|
|||
for ap in self.apertures:
|
||||
ap.rotation += angle
|
||||
|
||||
#print(f'rotate {angle} @ {center}')
|
||||
#for obj in self.objects:
|
||||
# print(' ', obj)
|
||||
|
||||
for obj in self.objects:
|
||||
obj.rotate(angle, *center, unit)
|
||||
|
||||
#print('after')
|
||||
#for obj in self.objects:
|
||||
# print(' ', obj)
|
||||
|
||||
def invert_polarity(self):
|
||||
for obj in self.objects:
|
||||
obj.polarity_dark = not p.polarity_dark
|
||||
|
|
@ -459,10 +445,6 @@ class GraphicsState:
|
|||
|
||||
def _create_arc(self, old_point, new_point, control_point, aperture=True):
|
||||
clockwise = self.interpolation_mode == CircularCWModeStmt
|
||||
print('creating arc')
|
||||
print(' old point', old_point)
|
||||
print(' new point', new_point)
|
||||
print(' control point', self.map_coord(*control_point, relative=True))
|
||||
return go.Arc(*old_point, *new_point, *self.map_coord(*control_point, relative=True),
|
||||
clockwise=clockwise, aperture=(self.aperture if aperture else None),
|
||||
polarity_dark=self.polarity_dark, unit=self.file_settings.unit)
|
||||
|
|
|
|||
|
|
@ -63,13 +63,14 @@ def run_cargo_cmd(cmd, args, **kwargs):
|
|||
def svg_to_png(in_svg, out_png, dpi=100, bg='black'):
|
||||
run_cargo_cmd('resvg', ['--background', bg, '--dpi', str(dpi), in_svg, out_png], check=True, stdout=subprocess.DEVNULL)
|
||||
|
||||
def gerbv_export(in_gbr, out_svg, format='svg', origin=(0, 0), size=(6, 6), fg='#ffffff'):
|
||||
def gerbv_export(in_gbr, out_svg, format='svg', origin=(0, 0), size=(6, 6), fg='#ffffff', bg='#000000'):
|
||||
x, y = origin
|
||||
w, h = size
|
||||
cmd = ['gerbv', '-x', format,
|
||||
'--border=0',
|
||||
f'--origin={x:.6f}x{y:.6f}', f'--window_inch={w:.6f}x{h:.6f}',
|
||||
f'--foreground={fg}',
|
||||
f'--background={bg}',
|
||||
'-o', str(out_svg), str(in_gbr)]
|
||||
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ def print_on_error(request):
|
|||
|
||||
if request.node.rep_call.failed:
|
||||
for msg in messages:
|
||||
print(msg)
|
||||
print(msg, end='')
|
||||
|
||||
@pytest.fixture
|
||||
def tmpfile(request):
|
||||
|
|
@ -322,8 +322,14 @@ def test_svg_export(reference, tmpfile):
|
|||
with open(out_svg, 'w') as f:
|
||||
f.write(str(grb.to_svg(force_bounds=bounds, arg_unit='inch', color='white')))
|
||||
|
||||
# NOTE: Instead of having gerbv directly export a PNG, we ask gerbv to output SVG which we then rasterize using
|
||||
# resvg. We have to do this since gerbv's built-in cairo-based PNG export has severe aliasing issues. In contrast,
|
||||
# using resvg for both allows an apples-to-apples comparison of both results.
|
||||
ref_svg = tmpfile('Reference export', '.svg')
|
||||
ref_png = tmpfile('Reference render', '.png')
|
||||
gerbv_export(reference, ref_png, origin=bounds[0], size=bounds[1], format='png', fg='#000000')
|
||||
gerbv_export(reference, ref_svg, origin=bounds[0], size=bounds[1])
|
||||
svg_to_png(ref_svg, ref_png, dpi=72) # make dpi match Cairo's default
|
||||
|
||||
out_png = tmpfile('Output render', '.png')
|
||||
svg_to_png(out_svg, out_png, dpi=72) # make dpi match Cairo's default
|
||||
|
||||
|
|
@ -363,11 +369,8 @@ def test_bounding_box(reference, tmpfile):
|
|||
assert (img > 0).any() # there must be some content, none of the test gerbers are completely empty.
|
||||
cols = img.sum(axis=1)
|
||||
rows = img.sum(axis=0)
|
||||
print('shape:', img.shape)
|
||||
col_prefix, col_suffix = np.argmax(cols > 0), np.argmax(cols[::-1] > 0)
|
||||
row_prefix, row_suffix = np.argmax(rows > 0), np.argmax(rows[::-1] > 0)
|
||||
print('cols', 'prefix:', row_prefix, 'suffix:', row_suffix)
|
||||
print('rows', 'prefix:', row_prefix, 'suffix:', row_suffix)
|
||||
|
||||
# Check that all margins are completely black and that the content touches the margins. Allow for some tolerance to
|
||||
# allow for antialiasing artifacts.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue