Pretty SVG WIP
This commit is contained in:
parent
b75404efce
commit
f558f66bc0
4 changed files with 176 additions and 156 deletions
|
|
@ -1,122 +1,25 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2013-2014 Paulo Henrique Silva <ph.silva@gmail.com>
|
||||
import click
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under
|
||||
# the License.
|
||||
|
||||
import os
|
||||
import argparse
|
||||
from .render import available_renderers
|
||||
from .render import theme
|
||||
from .pcb import PCB
|
||||
from . import load_layer
|
||||
from .layers import LayerStack
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Render gerber files to image',
|
||||
prog='gerber-render'
|
||||
)
|
||||
parser.add_argument(
|
||||
'filenames', metavar='FILENAME', type=str, nargs='+',
|
||||
help='Gerber files to render. If a directory is provided, it should '
|
||||
'be provided alone and should contain the gerber files for a '
|
||||
'single PCB.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--outfile', '-o', type=str, nargs='?', default='out',
|
||||
help="Output Filename (extension will be added automatically)"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--backend', '-b', choices=available_renderers.keys(), default='cairo',
|
||||
help='Choose the backend to use to generate the output.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--theme', '-t', choices=theme.THEMES.keys(), default='default',
|
||||
help='Select render theme.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--width', type=int, default=1920, help='Maximum width.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--height', type=int, default=1080, help='Maximum height.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--verbose', '-v', action='store_true', default=False,
|
||||
help='Increase verbosity of the output.'
|
||||
)
|
||||
# parser.add_argument(
|
||||
# '--quick', '-q', action='store_true', default=False,
|
||||
# help='Skip longer running rendering steps to produce lower quality'
|
||||
# ' output faster. This only has an effect for the freecad backend.'
|
||||
# )
|
||||
# parser.add_argument(
|
||||
# '--nox', action='store_true', default=False,
|
||||
# help='Run without using any GUI elements. This may produce suboptimal'
|
||||
# 'output. For the freecad backend, colors, transparancy, and '
|
||||
# 'visibility cannot be set without a GUI instance.'
|
||||
# )
|
||||
@click.command()
|
||||
@click.option('-t' ,'--top', help='Render board top side.', is_flag=True)
|
||||
@click.option('-b' ,'--bottom', help='Render board bottom side.', is_flag=True)
|
||||
@click.argument('gerber_dir_or_zip', type=click.Path(exists=True))
|
||||
@click.argument('output_svg', required=False, default='-', type=click.File('w'))
|
||||
def render(gerber_dir_or_zip, output_svg, top, bottom):
|
||||
if (bool(top) + bool(bottom)) != 1:
|
||||
raise click.UsageError('Excactly one of --top or --bottom must be given.')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
renderer = available_renderers[args.backend]()
|
||||
|
||||
if args.backend in ['cairo', ]:
|
||||
outext = 'png'
|
||||
else:
|
||||
outext = None
|
||||
|
||||
if os.path.exists(args.filenames[0]) and os.path.isdir(args.filenames[0]):
|
||||
directory = args.filenames[0]
|
||||
pcb = PCB.from_directory(directory)
|
||||
|
||||
if args.backend in ['cairo', ]:
|
||||
top = pcb.top_layers
|
||||
bottom = pcb.bottom_layers
|
||||
copper = pcb.copper_layers
|
||||
|
||||
outline = pcb.outline_layer
|
||||
if outline:
|
||||
top = [outline] + top
|
||||
bottom = [outline] + bottom
|
||||
copper = [outline] + copper + pcb.drill_layers
|
||||
|
||||
renderer.render_layers(
|
||||
layers=top, theme=theme.THEMES[args.theme],
|
||||
max_height=args.height, max_width=args.width,
|
||||
filename='{0}.top.{1}'.format(args.outfile, outext)
|
||||
)
|
||||
renderer.render_layers(
|
||||
layers=bottom, theme=theme.THEMES[args.theme],
|
||||
max_height=args.height, max_width=args.width,
|
||||
filename='{0}.bottom.{1}'.format(args.outfile, outext)
|
||||
)
|
||||
renderer.render_layers(
|
||||
layers=copper, theme=theme.THEMES['Transparent Multilayer'],
|
||||
max_height=args.height, max_width=args.width,
|
||||
filename='{0}.copper.{1}'.format(args.outfile, outext))
|
||||
else:
|
||||
pass
|
||||
else:
|
||||
filenames = args.filenames
|
||||
for filename in filenames:
|
||||
layer = load_layer(filename)
|
||||
settings = theme.THEMES[args.theme].get(layer.layer_class, None)
|
||||
renderer.render_layer(layer, settings=settings)
|
||||
renderer.dump(filename='{0}.{1}'.format(args.outfile, outext))
|
||||
stack = LayerStack.open(gerber_dir_or_zip, lazy=True)
|
||||
print(f'Loaded {stack}')
|
||||
|
||||
svg = stack.to_pretty_svg(side=('top' if top else 'bottom'))
|
||||
output_svg.write(str(svg))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
render()
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import math
|
||||
import copy
|
||||
from dataclasses import dataclass, astuple, replace, field, fields
|
||||
from dataclasses import dataclass, astuple, field, fields
|
||||
|
||||
from .utils import MM, InterpMode, to_unit, rotate_point
|
||||
from . import graphic_primitives as gp
|
||||
|
|
@ -258,56 +258,51 @@ class Region(GraphicObject):
|
|||
|
||||
There is one exception from the last two rules: To emulate a region with a hole in it, *cut-ins* are allowed. At a
|
||||
cut-in, the region is allowed to touch (but never overlap!) itself.
|
||||
|
||||
:attr poly: :py:class:`~.graphic_primitives.ArcPoly` describing the actual outline of this Region. The coordinates of
|
||||
this poly are in the unit of this instance's :py:attr:`unit` field.
|
||||
"""
|
||||
|
||||
def __init__(self, outline=None, arc_centers=None, *, unit, polarity_dark):
|
||||
self.unit = unit
|
||||
self.polarity_dark = polarity_dark
|
||||
outline = [] if outline is None else outline
|
||||
arc_centers = [] if arc_centers is None else arc_centers
|
||||
self.poly = gp.ArcPoly(outline, arc_centers)
|
||||
self.outline = [] if outline is None else outline
|
||||
self.arc_centers = [] if arc_centers is None else arc_centers
|
||||
|
||||
def __len__(self):
|
||||
return len(self.poly)
|
||||
return len(self.outline)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.poly)
|
||||
return bool(self.outline)
|
||||
|
||||
def _offset(self, dx, dy):
|
||||
self.poly.outline = [ (x+dx, y+dy) for x, y in self.poly.outline ]
|
||||
self.outline = [ (x+dx, y+dy) for x, y in self.outline ]
|
||||
|
||||
def _rotate(self, angle, cx=0, cy=0):
|
||||
self.poly.outline = [ gp.rotate_point(x, y, angle, cx, cy) for x, y in self.poly.outline ]
|
||||
self.poly.arc_centers = [
|
||||
self.outline = [ gp.rotate_point(x, y, angle, cx, cy) for x, y in self.outline ]
|
||||
self.arc_centers = [
|
||||
(arc[0], gp.rotate_point(*arc[1], angle, cx-p[0], cy-p[1])) if arc else None
|
||||
for p, arc in zip(self.poly.outline, self.poly.arc_centers) ]
|
||||
for p, arc in zip(self.outline, self.arc_centers) ]
|
||||
|
||||
def append(self, obj):
|
||||
if obj.unit != self.unit:
|
||||
obj = obj.converted(self.unit)
|
||||
|
||||
if not self.poly.outline:
|
||||
self.poly.outline.append(obj.p1)
|
||||
self.poly.outline.append(obj.p2)
|
||||
if not self.outline:
|
||||
self.outline.append(obj.p1)
|
||||
self.outline.append(obj.p2)
|
||||
|
||||
if isinstance(obj, Arc):
|
||||
self.poly.arc_centers.append((obj.clockwise, obj.center_relative))
|
||||
self.arc_centers.append((obj.clockwise, obj.center_relative))
|
||||
else:
|
||||
self.poly.arc_centers.append(None)
|
||||
self.arc_centers.append(None)
|
||||
|
||||
def to_primitives(self, unit=None):
|
||||
self.poly.polarity_dark = self.polarity_dark # FIXME: is this the right spot to do this?
|
||||
if unit == self.unit:
|
||||
yield self.poly
|
||||
yield gp.ArcPoly(outline=self.outline, arc_centers=self.arc_centers, polarity_dark=self.polarity_dark)
|
||||
|
||||
else:
|
||||
to = lambda value: self.unit.convert_to(unit, value)
|
||||
conv_outline = [ (to(x), to(y)) for x, y in self.poly.outline ]
|
||||
conv_outline = [ (to(x), to(y)) for x, y in self.outline ]
|
||||
convert_entry = lambda entry: (entry[0], (to(entry[1][0]), to(entry[1][1])))
|
||||
conv_arc = [ None if entry is None else convert_entry(entry) for entry in self.poly.arc_centers ]
|
||||
conv_arc = [ None if entry is None else convert_entry(entry) for entry in self.arc_centers ]
|
||||
|
||||
yield gp.ArcPoly(conv_outline, conv_arc, polarity_dark=self.polarity_dark)
|
||||
|
||||
|
|
@ -319,9 +314,9 @@ class Region(GraphicObject):
|
|||
# TODO report gerbv issue upstream
|
||||
yield gs.interpolation_mode_statement() + '*'
|
||||
|
||||
yield from gs.set_current_point(self.poly.outline[0], unit=self.unit)
|
||||
yield from gs.set_current_point(self.outline[0], unit=self.unit)
|
||||
|
||||
for point, arc_center in zip(self.poly.outline[1:], self.poly.arc_centers):
|
||||
for point, arc_center in zip(self.outline[1:], self.arc_centers):
|
||||
if arc_center is None:
|
||||
yield from gs.set_interpolation_mode(InterpMode.LINEAR)
|
||||
|
||||
|
|
@ -410,10 +405,13 @@ class Line(GraphicObject):
|
|||
"""
|
||||
return self.tool.plated
|
||||
|
||||
def to_primitives(self, unit=None):
|
||||
def as_primitive(self, unit=None):
|
||||
conv = self.converted(unit)
|
||||
w = self.aperture.equivalent_width(unit) if self.aperture else 0.1 # for debugging
|
||||
yield gp.Line(*conv.p1, *conv.p2, w, polarity_dark=self.polarity_dark)
|
||||
return gp.Line(*conv.p1, *conv.p2, w, polarity_dark=self.polarity_dark)
|
||||
|
||||
def to_primitives(self, unit=None):
|
||||
yield self.as_primitive(unit=unit)
|
||||
|
||||
def to_statements(self, gs):
|
||||
yield from gs.set_polarity(self.polarity_dark)
|
||||
|
|
@ -613,16 +611,19 @@ class Arc(GraphicObject):
|
|||
self.x2, self.y2 = gp.rotate_point(self.x2, self.y2, rotation, cx, cy)
|
||||
self.cx, self.cy = new_cx - self.x1, new_cy - self.y1
|
||||
|
||||
def to_primitives(self, unit=None):
|
||||
def as_primitive(self, unit=None):
|
||||
conv = self.converted(unit)
|
||||
w = self.aperture.equivalent_width(unit) if self.aperture else 0.1 # for debugging
|
||||
yield gp.Arc(x1=conv.x1, y1=conv.y1,
|
||||
return gp.Arc(x1=conv.x1, y1=conv.y1,
|
||||
x2=conv.x2, y2=conv.y2,
|
||||
cx=conv.cx, cy=conv.cy,
|
||||
clockwise=self.clockwise,
|
||||
width=w,
|
||||
polarity_dark=self.polarity_dark)
|
||||
|
||||
def to_primitives(self, unit=None):
|
||||
yield self.as_primitive(unit=unit)
|
||||
|
||||
def to_statements(self, gs):
|
||||
yield from gs.set_polarity(self.polarity_dark)
|
||||
yield from gs.set_aperture(self.aperture)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from .utils import *
|
|||
prec = lambda x: f'{float(x):.6}'
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class GraphicPrimitive:
|
||||
|
||||
# hackety hack: Work around python < 3.10 not having dataclasses.KW_ONLY.
|
||||
|
|
@ -63,7 +63,7 @@ class GraphicPrimitive:
|
|||
raise NotImplementedError()
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class Circle(GraphicPrimitive):
|
||||
#: Center X coordinate
|
||||
x : float
|
||||
|
|
@ -80,7 +80,7 @@ class Circle(GraphicPrimitive):
|
|||
return tag('circle', cx=prec(self.x), cy=prec(self.y), r=prec(self.r), style=f'fill: {color}')
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class ArcPoly(GraphicPrimitive):
|
||||
""" Polygon whose sides may be either straight lines or circular arcs. """
|
||||
|
||||
|
|
@ -132,7 +132,7 @@ class ArcPoly(GraphicPrimitive):
|
|||
""" Return ``True`` if this polygon has any outline points. """
|
||||
return bool(len(self))
|
||||
|
||||
def _path_d(self):
|
||||
def path_d(self):
|
||||
if len(self.outline) == 0:
|
||||
return
|
||||
|
||||
|
|
@ -147,10 +147,10 @@ class ArcPoly(GraphicPrimitive):
|
|||
|
||||
def to_svg(self, fg='black', bg='white', tag=Tag):
|
||||
color = fg if self.polarity_dark else bg
|
||||
return tag('path', d=' '.join(self._path_d()), style=f'fill: {color}')
|
||||
return tag('path', d=' '.join(self.path_d()), style=f'fill: {color}')
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class Line(GraphicPrimitive):
|
||||
""" Straight line with round end caps. """
|
||||
#: Start X coordinate. As usual in modern graphics APIs, this is at the center of the half-circle capping off this
|
||||
|
|
@ -165,6 +165,9 @@ class Line(GraphicPrimitive):
|
|||
#: Line width
|
||||
width : float
|
||||
|
||||
def flip(self):
|
||||
return replace(self, x1=self.x2, y1=self.y2, x2=self.x1, y2=self.y1)
|
||||
|
||||
@classmethod
|
||||
def from_obround(kls, x:float, y:float, w:float, h:float, rotation:float=0, polarity_dark:bool=True):
|
||||
""" Convert a gerber obround into a :py:class:`~.graphic_primitives.Line`. """
|
||||
|
|
@ -188,7 +191,7 @@ class Line(GraphicPrimitive):
|
|||
return tag('path', d=f'M {self.x1:.6} {self.y1:.6} L {self.x2:.6} {self.y2:.6}',
|
||||
style=f'fill: none; stroke: {color}; stroke-width: {width}; stroke-linecap: round')
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class Arc(GraphicPrimitive):
|
||||
""" Circular arc with line width ``width`` going from ``(x1, y1)`` to ``(x2, y2)`` around center at ``(cx, cy)``. """
|
||||
#: Start X coodinate
|
||||
|
|
@ -209,6 +212,10 @@ class Arc(GraphicPrimitive):
|
|||
#: Line width of this arc.
|
||||
width : float
|
||||
|
||||
def flip(self):
|
||||
return replace(self, x1=self.x2, y1=self.y2, x2=self.x1, y2=self.y1,
|
||||
cx=(self.x + self.cx) - self.x2, cy=(self.y + self.cy) - self.y2, clockwise=not self.clockwise)
|
||||
|
||||
def bounding_box(self):
|
||||
r = self.width/2
|
||||
endpoints = add_bounds(Circle(self.x1, self.y1, r).bounding_box(), Circle(self.x2, self.y2, r).bounding_box())
|
||||
|
|
@ -236,7 +243,7 @@ class Arc(GraphicPrimitive):
|
|||
return tag('path', d=f'M {self.x1:.6} {self.y1:.6} {arc}',
|
||||
style=f'fill: none; stroke: {color}; stroke-width: {width}; stroke-linecap: round; fill: none')
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class Rectangle(GraphicPrimitive):
|
||||
#: **Center** X coordinate
|
||||
x : float
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import sys
|
|||
import re
|
||||
import warnings
|
||||
import copy
|
||||
import bisect
|
||||
import textwrap
|
||||
import itertools
|
||||
from collections import namedtuple
|
||||
from pathlib import Path
|
||||
|
|
@ -35,6 +37,8 @@ from .ipc356 import Netlist
|
|||
from .cam import FileSettings, LazyCamFile
|
||||
from .layer_rules import MATCH_RULES
|
||||
from .utils import sum_bounds, setup_svg, MM, Tag
|
||||
from . import graphic_objects as go
|
||||
from . import graphic_primitives as gp
|
||||
|
||||
|
||||
STANDARD_LAYERS = [
|
||||
|
|
@ -49,6 +53,15 @@ STANDARD_LAYERS = [
|
|||
'bottom paste',
|
||||
]
|
||||
|
||||
DEFAULT_COLORS = {
|
||||
'copper': '#cccccc',
|
||||
'mask': '#004200bf',
|
||||
'paste': '#999999',
|
||||
'silk': '#e0e0e0',
|
||||
'drill': '#303030',
|
||||
'outline': '#F0C000',
|
||||
}
|
||||
|
||||
class NamingScheme:
|
||||
kicad = {
|
||||
'top copper': '{board_name}-F.Cu.gbr',
|
||||
|
|
@ -483,28 +496,64 @@ class LayerStack:
|
|||
|
||||
return setup_svg(tags, bounds, margin=margin, arg_unit=arg_unit, svg_unit=svg_unit, pagecolor=bg, tag=tag)
|
||||
|
||||
def to_pretty_svg(self, side='top', margin=0, arg_unit=MM, svg_unit=MM, force_bounds=None, tag=Tag, inkscape=False):
|
||||
def to_pretty_svg(self, side='top', margin=0, arg_unit=MM, svg_unit=MM, force_bounds=None, tag=Tag, inkscape=False, colors=None):
|
||||
if colors is None:
|
||||
colors = DEFAULT_COLORS
|
||||
|
||||
colors_alpha = {}
|
||||
for layer, color in colors.items():
|
||||
if isinstance(color, str):
|
||||
if re.match(r'#[0-9a-fA-F]{8}', color):
|
||||
colors_alpha[layer] = (color[:-2], int(color[-2:], 16)/255)
|
||||
else:
|
||||
colors_alpha[layer] = (color, 1)
|
||||
else:
|
||||
colors_alpha[layer] = color
|
||||
|
||||
if force_bounds:
|
||||
bounds = svg_unit.convert_bounds_from(arg_unit, force_bounds)
|
||||
else:
|
||||
bounds = self.board_bounds(unit=svg_unit, default=((0, 0), (0, 0)))
|
||||
|
||||
tags = []
|
||||
filter_defs = []
|
||||
|
||||
for layer, (color, alpha) in colors_alpha.items():
|
||||
filter_defs.append(textwrap.dedent(f'''
|
||||
<filter id="f-{layer}">
|
||||
<feFlood result="flood-black" flood-color="black" flood-opacity="1"/>
|
||||
<feFlood result="flood-green" flood-color="{color}"/>
|
||||
<feBlend in="SourceGraphic" in2="flood-black" result="overlay" mode="normal"/>
|
||||
<feBlend in="overlay" in2="flood-green" result="colored" mode="multiply"/>
|
||||
<feColorMatrix in="overlay" type="matrix" result="alphaOut" values="0 0 0 0 0
|
||||
0 0 0 0 0
|
||||
0 0 0 0 0
|
||||
{alpha} 0 0 0 0"/>
|
||||
<feComposite in="colored" in2="alphaOut" operator="in"/>
|
||||
</filter>'''.strip()))
|
||||
|
||||
tags = [tag('defs', filter_defs)]
|
||||
inkscape_attrs = lambda label: dict(inkscape__groupmode='layer', inkscape__label=label) if inkscape else {}
|
||||
|
||||
for use, color in {'copper': 'black', 'mask': 'blue', 'silk': 'red'}.items():
|
||||
for use in ['copper', 'mask', 'silk', 'paste']:
|
||||
if (side, use) not in self:
|
||||
warnings.warn(f'Layer "{side} {use}" not found. Found layers: {", ".join(side + " " + use for side, use in self.graphic_layers)}')
|
||||
continue
|
||||
|
||||
layer = self[(side, use)]
|
||||
tags.append(tag('g', list(layer.instance.svg_objects(svg_unit=svg_unit, fg=color, bg="white", tag=Tag)),
|
||||
id=f'l-{side}-{use}', **inkscape_attrs(f'{side} {use}')))
|
||||
fg, bg = ('white', 'black') if use != 'mask' else ('black', 'white')
|
||||
objects = list(layer.instance.svg_objects(svg_unit=svg_unit, fg=fg, bg=bg, tag=Tag))
|
||||
if use == 'mask':
|
||||
objects.insert(0, tag('path', id='outline-path', d=self.outline_svg_d(unit=svg_unit), style='fill:white'))
|
||||
tags.append(tag('g', objects, id=f'l-{side}-{use}', filter=f'url(#f-{use})', **inkscape_attrs(f'{side} {use}')))
|
||||
|
||||
for i, layer in enumerate(self.drill_layers):
|
||||
tags.append(tag('g', list(layer.instance.svg_objects(svg_unit=svg_unit, fg='magenta', bg="white", tag=Tag)),
|
||||
tags.append(tag('g', list(layer.instance.svg_objects(svg_unit=svg_unit, fg='white', bg='black', tag=Tag)),
|
||||
id=f'l-drill-{i}', **inkscape_attrs(f'drill-{i}')))
|
||||
|
||||
if self.outline:
|
||||
tags.append(tag('g', list(self.outline.instance.svg_objects(svg_unit=svg_unit, fg='white', bg='black', tag=Tag)),
|
||||
id=f'l-outline-{i}', **inkscape_attrs(f'outline-{i}')))
|
||||
|
||||
return setup_svg(tags, bounds, margin=margin, arg_unit=arg_unit, svg_unit=svg_unit, pagecolor="white", tag=tag, inkscape=inkscape)
|
||||
|
||||
def bounding_box(self, unit=MM, default=None):
|
||||
|
|
@ -635,7 +684,67 @@ class LayerStack:
|
|||
@property
|
||||
def outline(self):
|
||||
return self['mechanical outline']
|
||||
|
||||
|
||||
def outline_svg_d(self, tol=0.01, unit=MM):
|
||||
chains = self.outline_polygons(tol, unit)
|
||||
polys = []
|
||||
for chain in chains:
|
||||
outline = [ (chain[0].x1, chain[0].y1), *((elem.x2, elem.y2) for elem in chain) ]
|
||||
arcs = [ (elem.clockwise, (elem.cx, elem.cy)) if isinstance(elem, gp.Arc) else None for elem in chain ]
|
||||
poly = gp.ArcPoly(outline=outline, arc_centers=arcs)
|
||||
polys.append(' '.join(poly.path_d()) + ' Z')
|
||||
return ' '.join(polys)
|
||||
|
||||
def outline_polygons(self, tol=0.01, unit=MM):
|
||||
polygons = []
|
||||
lines = [ obj.as_primitive(unit) for obj in self.outline.instance.objects if isinstance(obj, (go.Line, go.Arc)) ]
|
||||
|
||||
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
|
||||
|
||||
joins = {}
|
||||
for cur in lines:
|
||||
for i, (x, y) in enumerate([(cur.x1, cur.y1), (cur.x2, cur.y2)]):
|
||||
x_left = bisect.bisect_left (by_x, x, key=lambda elem: elem[0] + tol)
|
||||
x_right = bisect.bisect_right(by_x, x, key=lambda elem: elem[0] - tol)
|
||||
selected = { elem for elem_x, elem in by_x[x_left:x_right] if elem != cur }
|
||||
|
||||
if not selected:
|
||||
continue # loose end
|
||||
|
||||
nearest = sorted(selected, key=lambda elem: min(dist_sq(elem.x1, elem.y1, x, y), dist_sq(elem.x2, elem.y2, x, y)))[0]
|
||||
|
||||
d1, d2 = dist_sq(nearest.x1, nearest.y1, x, y), dist_sq(nearest.x2, nearest.y2, x, y)
|
||||
j = 0 if d1 < d2 else 1
|
||||
|
||||
if (nearest, j) in joins and joins[(nearest, j)] != (cur, i):
|
||||
raise ValueError(f'Error: three-way intersection of {(nearest, j)}; {(cur, i)}; and {joins[(nearest, j)]}')
|
||||
|
||||
if (cur, i) in joins and joins[(cur, i)] != (nearest, j):
|
||||
raise ValueError(f'Error: three-way intersection of {(nearest, j)}; {(cur, i)}; and {joins[(nearest, j)]}')
|
||||
|
||||
joins[(cur, i)] = (nearest, j)
|
||||
joins[(nearest, j)] = (cur, i)
|
||||
|
||||
def flip_if(obj, i):
|
||||
if i:
|
||||
c = copy.copy(obj)
|
||||
c.flip()
|
||||
return c
|
||||
else:
|
||||
return obj
|
||||
|
||||
while joins:
|
||||
(first, i), (cur, j) = joins.popitem()
|
||||
del joins[(cur, j)]
|
||||
l = [ flip_if(first, not i), flip_if(cur, j) ]
|
||||
while cur != first and (cur, not j) in joins:
|
||||
cur, j = joins.pop((cur, not j))
|
||||
del joins[(cur, j)]
|
||||
l.append(flip_if(cur, j))
|
||||
yield l
|
||||
|
||||
|
||||
def _merge_layer(self, target, source):
|
||||
if source is None:
|
||||
return
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue