[PcbDraw] Applied the SVG patch like the upstream

- To make the code more convergent, is equivalent
This commit is contained in:
Salvador E. Tropea 2022-10-24 13:02:37 -03:00
parent 491c598afb
commit b8f9b2e924
4 changed files with 133 additions and 114 deletions

View File

@ -2632,7 +2632,7 @@ Notes:
The `kicad_all` method uses the whole size reported by KiCad. Usually includes extra space.
The `svg_paths` uses all visible drawings in the image. To use this method you
must install the `numpy` Python module (may not be available in docker images).
- `svg_precision`: [number=4] [0,6] Scale factor used to represent 1 mm in the SVG (KiCad 6).
- `svg_precision`: [number=4] [3,6] Scale factor used to represent 1 mm in the SVG (KiCad 6).
The value is how much zeros has the multiplier (1 mm = 10 power `svg_precision` units).
Note that for an A4 paper Firefox 91 and Chrome 105 can't handle more than 5.
- `variant`: [string=''] Board variant to apply.

View File

@ -1459,7 +1459,7 @@ outputs:
silk: '#d5dce4'
# [string='#bf2600'] Color for the V-CUTS
vcut: '#bf2600'
# [number=4] [0,6] Scale factor used to represent 1 mm in the SVG (KiCad 6).
# [number=4] [3,6] Scale factor used to represent 1 mm in the SVG (KiCad 6).
# The value is how much zeros has the multiplier (1 mm = 10 power `svg_precision` units).
# Note that for an A4 paper Firefox 91 and Chrome 105 can't handle more than 5
svg_precision: 4

View File

@ -271,10 +271,6 @@ def ki2mm(val: int) -> float:
def mm2ki(val: float) -> int:
return int(val * 1000000)
# KiCAD 5 and KiCAD 6 use different units of the SVG
ki2svg: Callable[[int], float] = (lambda x: int(x)) if isV6(KICAD_VERSION) else ki2dmil
svg2ki: Callable[[float], int] = (lambda x: int(x)) if isV6(KICAD_VERSION) else dmil2ki
def to_kicad_basic_units(val: str) -> int:
"""
Read string value and return it as KiCAD base units
@ -454,12 +450,6 @@ def get_board_polygon(svg_elements: etree.Element) -> etree.Element:
e = etree.Element("path", d=path, style="fill-rule: evenodd;")
return e
def component_to_board_scale_and_offset(svg: etree.Element) -> Tuple[float, float, float, float]:
width = ki2svg(to_kicad_basic_units(svg.attrib["width"]))
height = ki2svg(to_kicad_basic_units(svg.attrib["height"]))
x, y, vw, vh = [float(x) for x in svg.attrib["viewBox"].split()]
return width / vw, height / vh, x, y
def load_style(style_file: str) -> Dict[str, Any]:
try:
with open(style_file, "r") as f:
@ -505,57 +495,6 @@ def merge_bbox(left: Box, right: Box) -> Box:
def hack_is_valid_bbox(box: Any): # type: ignore
return all(-1e15 < c < 1e15 for c in box)
def shrink_svg(svg: etree.ElementTree, margin: tuple, compute_bbox: bool=False) -> None:
"""
Shrink the SVG canvas to the size of the drawing. Add margin in
KiCAD units.
"""
root = svg.getroot()
if compute_bbox:
# Compute the bbox using the SVG drawings, so things outside the PCB
# outline are counted.
# We have to overcome the limitation of different base types between
# PcbDraw and svgpathtools
from xml.etree.ElementTree import fromstring as xmlParse
from lxml.etree import tostring as serializeXml # type: ignore
from . import svgpathtools # type: ignore
paths = svgpathtools.document.flattened_paths(xmlParse(serializeXml(svg)))
if len(paths) == 0:
return
bbox = paths[0].bbox()
for x in paths:
b = x.bbox()
if hack_is_valid_bbox(b):
bbox = b
break
for x in paths:
box = x.bbox()
if not hack_is_valid_bbox(box):
# This is a hack due to instability in svpathtools
continue
bbox = merge_bbox(bbox, box)
bbox = list(bbox)
else:
# Get the current viewBox
# This is computed by KiCad using the PCB edge
x, y, vw, vh = [float(x) for x in root.attrib["viewBox"].split()]
bbox = [x, x+vw, y, y+vh]
# Apply the margin
bbox[0] -= ki2svg(margin[0])
bbox[1] += ki2svg(margin[1])
bbox[2] -= ki2svg(margin[2])
bbox[3] += ki2svg(margin[3])
root.attrib["viewBox"] = "{} {} {} {}".format(
bbox[0], bbox[2],
bbox[1] - bbox[0], bbox[3] - bbox[2]
)
root.attrib["width"] = str(ki2mm(svg2ki(bbox[1] - bbox[0]))) + "mm"
root.attrib["height"] = str(ki2mm(svg2ki(bbox[3] - bbox[2]))) + "mm"
def remove_empty_elems(tree: etree.Element) -> None:
"""
Given SVG tree, remove empty groups and defs
@ -585,7 +524,7 @@ class Hole:
orientation: int
drillsize: Tuple[int, int]
def get_svg_path_d(self) -> str:
def get_svg_path_d(self, ki2svg: Callable[[int], float]) -> str:
w, h = [ki2svg(x) for x in self.drillsize]
if w > h:
ew = w - h
@ -718,7 +657,7 @@ class PlotSubstrate(PlotInterface):
layer = etree.SubElement(self._container, "g", id="substrate-" + name,
style="fill:{0}; stroke:{0}; stroke-width: {1}".format(
self._plotter.get_style(name),
ki2svg(self.outline_width)))
self._plotter.ki2svg(self.outline_width)))
if name == "pads":
layer.attrib["mask"] = "url(#pads-mask)"
if name == "silk":
@ -730,12 +669,12 @@ class PlotSubstrate(PlotInterface):
forbidden_colors=["#ffffff"]):
layer.append(element)
for hole in collect_holes(self._plotter.board):
position = [ki2svg(coord) for coord in hole.position]
size = [ki2svg(coord) for coord in hole.drillsize]
position = [self._plotter.ki2svg(coord) for coord in hole.position]
size = [self._plotter.ki2svg(coord) for coord in hole.drillsize]
if size[0] == 0 or size[1] == 0:
continue
el = etree.SubElement(layer, "path")
el.attrib["d"] = hole.get_svg_path_d()
el.attrib["d"] = hole.get_svg_path_d(self._plotter.ki2svg)
el.attrib["transform"] = "translate({} {}) rotate({})".format(
position[0], position[1], -hole.orientation / 10)
@ -769,10 +708,10 @@ class PlotSubstrate(PlotInterface):
mask.append(element)
silkMask = self._plotter.get_def_slot(tag_name="mask", id=f"{name}-silkscreen")
bg = etree.SubElement(silkMask, "rect", attrib={
"x": str(ki2svg(self._boardsize.GetX())),
"y": str(ki2svg(self._boardsize.GetY())),
"width": str(ki2svg(self._boardsize.GetWidth())),
"height": str(ki2svg(self._boardsize.GetHeight())),
"x": str(self._plotter.ki2svg(self._boardsize.GetX())),
"y": str(self._plotter.ki2svg(self._boardsize.GetY())),
"width": str(self._plotter.ki2svg(self._boardsize.GetWidth())),
"height": str(self._plotter.ki2svg(self._boardsize.GetHeight())),
"fill": "white"
})
for element in extract_svg_content(read_svg_unique(source_filename, self._plotter.unique_prefix())):
@ -785,14 +724,14 @@ class PlotSubstrate(PlotInterface):
bb = self._plotter.boardsize
bg = etree.SubElement(container, "rect", x="0", y="0", fill="white")
bg.attrib["x"] = str(ki2svg(bb.GetX()))
bg.attrib["y"] = str(ki2svg(bb.GetY()))
bg.attrib["width"] = str(ki2svg(bb.GetWidth()))
bg.attrib["height"] = str(ki2svg(bb.GetHeight()))
bg.attrib["x"] = str(self._plotter.ki2svg(bb.GetX()))
bg.attrib["y"] = str(self._plotter.ki2svg(bb.GetY()))
bg.attrib["width"] = str(self._plotter.ki2svg(bb.GetWidth()))
bg.attrib["height"] = str(self._plotter.ki2svg(bb.GetHeight()))
for hole in collect_holes(self._plotter.board):
position = list(map(ki2svg, hole.position))
size = list(map(ki2svg, hole.drillsize))
position = list(map(self._plotter.ki2svg, hole.position))
size = list(map(self._plotter.ki2svg, hole.drillsize))
if size[0] > 0 and size[1] > 0:
if size[0] < size[1]:
stroke = size[0]
@ -871,7 +810,7 @@ class PlotComponents(PlotInterface):
group.append(component_element)
ci = component_info
group.attrib["transform"] = \
f"translate({ki2svg(position[0])} {ki2svg(position[1])}) " + \
f"translate({self._plotter.ki2svg(position[0])} {self._plotter.ki2svg(position[1])}) " + \
f"scale({ci.scale[0]}, {ci.scale[1]}) " + \
f"rotate({-math.degrees(position[2])}) " + \
f"translate({-ci.origin[0]} {-ci.origin[1]})"
@ -901,7 +840,7 @@ class PlotComponents(PlotInterface):
origin.getparent().remove(origin)
else:
self._plotter.yield_warning("origin", f"component: Component {lib}:{name} has not origin")
svg_scale_x, svg_scale_y, svg_offset_x, svg_offset_y = component_to_board_scale_and_offset(svg_tree)
svg_scale_x, svg_scale_y, svg_offset_x, svg_offset_y = self._component_to_board_scale_and_offset(svg_tree)
component_info = PlacedComponentInfo(
id=xml_id,
origin=(origin_x, origin_y),
@ -912,17 +851,24 @@ class PlotComponents(PlotInterface):
self._apply_resistor_code(component_element, id_prefix, ref, value)
return component_element, component_info
def _component_to_board_scale_and_offset(self, svg: etree.Element) \
-> Tuple[float, float, float, float]:
width = self._plotter.ki2svg(to_kicad_basic_units(svg.attrib["width"]))
height = self._plotter.ki2svg(to_kicad_basic_units(svg.attrib["height"]))
x, y, vw, vh = [float(x) for x in svg.attrib["viewBox"].split()]
return width / vw, height / vh, x, y
def _build_highlight(self, ref: str, info: PlacedComponentInfo,
position: Tuple[int, int, float]) -> None:
padding = mm2ki(self._plotter.get_style("highlight-padding"))
h = etree.Element("rect", id=f"h_{ref}",
x=str(ki2svg(-padding)),
y=str(ki2svg(-padding)),
width=str(ki2svg(int(info.size[0] + 2 * padding))),
height=str(ki2svg(int(info.size[1] + 2 * padding))),
x=str(self._plotter.ki2svg(-padding)),
y=str(self._plotter.ki2svg(-padding)),
width=str(self._plotter.ki2svg(int(info.size[0] + 2 * padding))),
height=str(self._plotter.ki2svg(int(info.size[1] + 2 * padding))),
style=self._plotter.get_style("highlight-style"))
h.attrib["transform"] = \
f"translate({ki2svg(position[0])} {ki2svg(position[1])}) " + \
f"translate({self._plotter.ki2svg(position[0])} {self._plotter.ki2svg(position[1])}) " + \
f"rotate({-math.degrees(position[2])}) " + \
f"translate({-(info.origin[0] - info.svg_offset[0]) * info.scale[0]}, {-(info.origin[1] - info.svg_offset[1]) * info.scale[1]})"
self._plotter.append_highlight_element(h)
@ -990,9 +936,9 @@ class PlotPlaceholders(PlotInterface):
def _append_placeholder(self, lib: str, name: str, ref: str, value: str,
position: Tuple[int, int, float]) -> None:
p = etree.Element("rect",
x=str(ki2svg(position[0] - mm2ki(0.5))),
y=str(ki2svg(position[1] - mm2ki(0.5))),
width=str(ki2svg(mm2ki(1))), height=str(ki2svg(mm2ki(1))), style="fill:red;")
x=str(self._plotter.ki2svg(position[0] - mm2ki(0.5))),
y=str(self._plotter.ki2svg(position[1] - mm2ki(0.5))),
width=str(self._plotter.ki2svg(mm2ki(1))), height=str(self._plotter.ki2svg(mm2ki(1))), style="fill:red;")
self._plotter.append_component_element(p)
@dataclass
@ -1062,6 +1008,9 @@ class PcbPlotter():
self.data_path: List[str] = [] # Base paths for libraries lookup
self.libs: List[str] = [] # Names of available libraries
self._libs_path: List[str] = []
self._svg_precision = 6 # The SVG precision for KiCAD 6 plotting
self._svg_divider = 1
self.style: Any = {} # Color scheme
self.margin: tuple = (0, 0, 0, 0) # Margin of the resulting document
self.compute_bbox: bool = False # Adjust the bbox using the SVG drawings
@ -1070,19 +1019,36 @@ class PcbPlotter():
self.yield_warning: Callable[[str, str], None] = lambda tag, msg: None # Handle warnings
self.ki2svg = self._ki2svg_v6 if isV6(KICAD_VERSION) else self._ki2svg_v5
self.svg2ki = self._svg2ki_v6 if isV6(KICAD_VERSION) else self._svg2ki_v5
@property
def svg_precision(self) -> int:
return self._svg_precision
@svg_precision.setter
def svg_precision(self, value: int) -> None:
# We need a setter as KiCAD silently clamps the value, so we also have
# to clamp.
if value < 3:
value = 3
if value > 6:
value = 6
self._svg_precision = value
self._svg_divider = 10 ** (6 - self.svg_precision)
def plot(self) -> etree.ElementTree:
"""
Plot the board based on the arguments stored in this class. Returns
SVG tree that you can either save or post-process as you wish.
"""
self._set_unit_conversion()
self._build_libs_path()
self._setup_document(self.render_back, self.mirror)
for plotter in self.plot_plan:
plotter.render(self)
remove_empty_elems(self._document.getroot())
remove_inkscape_annotation(self._document.getroot())
shrink_svg(self._document, self.margin, self.compute_bbox)
self._shrink_svg(self._document, self.margin, self.compute_bbox)
return self._document
@ -1221,8 +1187,6 @@ class PcbPlotter():
with tempfile.TemporaryDirectory() as tmp:
pctl = pcbnew.PLOT_CONTROLLER(self.board)
popt = pctl.GetPlotOptions()
if isV6(KICAD_VERSION):
popt.SetSvgPrecision(self.svg_precision, False)
popt.SetOutputDirectory(tmp)
popt.SetScale(1)
popt.SetMirror(False)
@ -1234,6 +1198,8 @@ class PcbPlotter():
# Method does not exist in older versions of KiCad
pass
popt.SetTextMode(pcbnew.PLOT_TEXT_MODE_STROKE)
if isV6(KICAD_VERSION):
popt.SetSvgPrecision(self.svg_precision, False)
for action in to_plot:
if len(action.layers) == 0:
continue
@ -1251,6 +1217,78 @@ class PcbPlotter():
if svg_file.endswith(f"-{action.name}.svg"):
action.action(action.name, os.path.join(tmp, svg_file))
def _ki2svg_v6(self, x: int) -> float:
"""
Convert dimensions from KiCAD to SVG. This method assumes the dimensions
use self.svg_precision.
"""
return x / self._svg_divider
def _svg2ki_v6(self, x: float) -> int:
"""
Convert dimensions from SVG to KiCAD. This method assumes the dimensions
use self.svg_precision.
"""
return int(x * self._svg_divider)
def _ki2svg_v5(self, x: int) -> float:
return ki2dmil(x)
def _svg2ki_v5(self, x: float) -> int:
return dmil2ki(x)
def _shrink_svg(self, svg: etree.ElementTree, margin: tuple, compute_bbox: bool=False) -> None:
"""
Shrink the SVG canvas to the size of the drawing. Add margin in
KiCAD units.
"""
root = svg.getroot()
if compute_bbox:
# Compute the bbox using the SVG drawings, so things outside the PCB
# outline are counted.
# We have to overcome the limitation of different base types between
# PcbDraw and svgpathtools
from xml.etree.ElementTree import fromstring as xmlParse
from lxml.etree import tostring as serializeXml # type: ignore
from . import svgpathtools # type: ignore
paths = svgpathtools.document.flattened_paths(xmlParse(serializeXml(svg)))
if len(paths) == 0:
return
bbox = paths[0].bbox()
for x in paths:
b = x.bbox()
if hack_is_valid_bbox(b):
bbox = b
break
for x in paths:
box = x.bbox()
if not hack_is_valid_bbox(box):
# This is a hack due to instability in svpathtools
continue
bbox = merge_bbox(bbox, box)
bbox = list(bbox)
else:
# Get the current viewBox
# This is computed by KiCad using the PCB edge
x, y, vw, vh = [float(x) for x in root.attrib["viewBox"].split()]
bbox = [x, x+vw, y, y+vh]
# Apply the margin
bbox[0] -= self.ki2svg(margin[0])
bbox[1] += self.ki2svg(margin[1])
bbox[2] -= self.ki2svg(margin[2])
bbox[3] += self.ki2svg(margin[3])
root.attrib["viewBox"] = "{} {} {} {}".format(
bbox[0], bbox[2],
bbox[1] - bbox[0], bbox[3] - bbox[2]
)
root.attrib["width"] = str(ki2mm(self.svg2ki(bbox[1] - bbox[0]))) + "mm"
root.attrib["height"] = str(ki2mm(self.svg2ki(bbox[3] - bbox[2]))) + "mm"
def _setup_document(self, render_back: bool, mirror: bool) -> None:
bb = self.board.ComputeBoundingBox(aBoardEdgesOnly=self.kicad_bb_only_edge)
self.boardsize = bb
@ -1265,12 +1303,12 @@ class PcbPlotter():
self._document = empty_svg(
width=f"{ki2mm(bb.GetWidth())}mm",
height=f"{ki2mm(bb.GetHeight())}mm",
viewBox=f"{ki2svg(-bb.GetWidth() - bb.GetX())} {ki2svg(bb.GetY())} {ki2svg(bb.GetWidth())} {ki2svg(bb.GetHeight())}")
viewBox=f"{self.ki2svg(-bb.GetWidth() - bb.GetX())} {self.ki2svg(bb.GetY())} {self.ki2svg(bb.GetWidth())} {self.ki2svg(bb.GetHeight())}")
else:
self._document = empty_svg(
width=f"{ki2mm(bb.GetWidth())}mm",
height=f"{ki2mm(bb.GetHeight())}mm",
viewBox=f"{ki2svg(bb.GetX())} {ki2svg(bb.GetY())} {ki2svg(bb.GetWidth())} {ki2svg(bb.GetHeight())}")
viewBox=f"{self.ki2svg(bb.GetX())} {self.ki2svg(bb.GetY())} {self.ki2svg(bb.GetWidth())} {self.ki2svg(bb.GetHeight())}")
self._defs = etree.SubElement(self._document.getroot(), "defs")
self._board_cont = etree.SubElement(self._document.getroot(), "g", transform=transform_string)
@ -1284,22 +1322,3 @@ class PcbPlotter():
self._board_cont.attrib["id"] = "boardContainer"
self._comp_cont.attrib["id"] = "componentContainer"
self._high_cont.attrib["id"] = "highlightContainer"
def _set_unit_conversion(self):
""" Setup the KiCad to SVG and SVG to KiCad conversions.
Only needed for KiCad 6 where we can select the `SVG precision` """
if not isV6(KICAD_VERSION):
# KiCad 5 has a fixed precision
return
global ki2svg
global svg2ki
if self.svg_precision == 6:
# This is the default for KiCad 6, lamentably this isn't supported by
# Chrome and Firefox (october 2022)
ki2svg = (lambda x: int(x))
svg2ki = (lambda x: int(x))
return
# Not the default precision, must be 0 to 5
divider = 10.0 ** (6 - self.svg_precision)
ki2svg = (lambda x: x / divider)
svg2ki = (lambda x: int(x * divider))

View File

@ -262,7 +262,7 @@ class PcbDrawOptions(VariantOptions):
The `svg_paths` uses all visible drawings in the image. To use this method you
must install the `numpy` Python module (may not be available in docker images) """
self.svg_precision = 4
""" [0,6] Scale factor used to represent 1 mm in the SVG (KiCad 6).
""" [3,6] Scale factor used to represent 1 mm in the SVG (KiCad 6).
The value is how much zeros has the multiplier (1 mm = 10 power `svg_precision` units).
Note that for an A4 paper Firefox 91 and Chrome 105 can't handle more than 5 """
super().__init__()