KiBot/kibot/PcbDraw
Salvador E. Tropea b7add3644d [Populate] Included mistune
- The dependency is too narrow mistune>=2.0.2, <=2.0.5
- Debian Sid uses 3.x
- The API changes too often and the author doesn't provide backwards
  compatibility
2024-03-20 06:46:52 -03:00
..
mistune [Populate] Included mistune 2024-03-20 06:46:52 -03:00
pybars Dropped Python 2.x support in included libs 2023-08-08 12:27:06 -03:00
svgpathtools
README.md [PcbDraw][Fixed] Reverted upstream patch 2024-02-08 10:16:24 -03:00
__init__.py
eda_angle.py [PCBDraw] Added missing pcbnewTransition file 2023-02-14 07:35:32 -03:00
mdrenderer.py [Populate] Included mistune 2024-03-20 06:46:52 -03:00
np.py
pcbnew_transition.py [PcbDraw][Fixed] Reverted upstream patch 2024-02-08 10:16:24 -03:00
plot.py [PcbDraw][Fixed] Reverted upstream patch 2024-02-08 10:16:24 -03:00
populate.py [Populate] Included mistune 2024-03-20 06:46:52 -03:00
present.py [KiKit Present] Now using the detected git binary 2022-12-06 13:36:35 -03:00
unit.py Support for extra data in the Value field 2023-03-30 12:39:36 -03:00

README.md

PcbDraw code

Why?

  • Important dependency: PcbDraw is currently a core functionality of KiBot because its used for the pcb_print output
  • Increased number of dependencies: The upstream code pulls too much dependencies, some of them optional, others that we don't need. This is a constant problem.
  • Incompatible interface and behavior: This should be fixed now that 1.0.0 is out, but I don't agree with the idea of doing small changes just because they look more elegant.
  • Now integrable: This is one of the changes in 1.0.0, now the code is easier to call as module.
  • Repeated functionality: The render stuff is already implemented by KiAuto.

Details

Currently only the plot module is included.

unit.py

  • Replaced unit code.
    • So we have only one units conversion
    • I think the only difference is that KiBot code currently supports the locales decimal point

plot.py

  • Made the pcbdraw import relative
  • Disabled shrink_svg by default
    • Pulls a problematic dependency: numpy
    • We keep the margin addition
    • The svgpathtool computation can be optionally enabled (plotter.compute_bbox)
  • Changed calls to ComputeBoundingBox() to optionally use aBoardEdgesOnly=True
    • To get the same behavior as 0.9.0-5
    • This changes the size of the SVG to the size of the board
    • Controlled by plotter.kicad_bb_only_edge
    • We now also store the bbox in the plotter.boardsize member, avoiding repeated calls to KiCad API
  • Added no_warn_back option to disable warnings on the opposite side
@@ -813,6 +813,7 @@
     highlight: Callable[[str], bool] = lambda x: False # References to highlight
     remapping: Callable[[str, str, str], Tuple[str, str]] = lambda ref, lib, name: (lib, name)
     resistor_values: Dict[str, ResistorValue] = field(default_factory=dict)
+    no_warn_back: bool = False

     def render(self, plotter: PcbPlotter) -> None:
         self._plotter = plotter
@@ -848,7 +849,8 @@
         else:
             ret = self._create_component(lib, name, ref, value)
             if ret is None:
-                self._plotter.yield_warning("component", f"Component {lib}:{name} has not footprint.")
+                if name[-5:] != '.back' or not self.no_warn_back:
+                    self._plotter.yield_warning("component", f"Component {lib}:{name} has not footprint.")
                 return
             component_element, component_info = ret
             self._used_components[unique_name] = component_info
  • Added option to plot only the solder mask. The PCB_Print output uses it and plotting all the stuff looks stupid. The patch added too much nested conditionals so I moved the information to data structures. The patch looks big, but is just a mechanism to skip the unneeded layers.
diff --git a/kibot/PcbDraw/plot.py b/kibot/PcbDraw/plot.py
index af473cdb..f8990722 100644
--- a/kibot/PcbDraw/plot.py
+++ b/kibot/PcbDraw/plot.py
@@ -648,35 +648,44 @@ class PlotInterface:
         raise NotImplementedError("Plot interface wasn't implemented")
 
 
+SUBSTRATE_ELEMENTS = {
+    "board": (pcbnew.Edge_Cuts, pcbnew.Edge_Cuts),
+    "clad": (pcbnew.F_Mask, pcbnew.B_Mask),
+    "copper": (pcbnew.F_Cu, pcbnew.B_Cu),
+    "pads": (pcbnew.F_Cu, pcbnew.B_Cu),
+    "pads-mask": (pcbnew.F_Mask, pcbnew.B_Mask),
+    "silk": (pcbnew.F_SilkS, pcbnew.B_SilkS),
+    "outline": (pcbnew.Edge_Cuts, pcbnew.Edge_Cuts)
+}
+ELEMENTS_USED = (
+    # Normal plot, all the elements
+    ("board", "clad", "copper", "pads", "pads-mask", "silk", "outline"),
+    # Solder mask plot
+    ("board", "pads-mask")
+)
+
+
 @dataclass
 class PlotSubstrate(PlotInterface):
     drill_holes: bool = True
     outline_width: int = mm2ki(0.1)
+    only_mask: bool = False
 
     def render(self, plotter: PcbPlotter) -> None:
         self._plotter = plotter # ...so we don't have to pass it explicitly
+        SUBSTRATE_PROCESS = {
+            "board": self._process_baselayer,
+            "clad": self._process_layer,
+            "copper": self._process_layer,
+            "pads": self._process_layer,
+            "pads-mask": self._process_mask,
+            "silk": self._process_layer,
+            "outline": self._process_outline
+        }
 
         to_plot: List[PlotAction] = []
-        if plotter.render_back:
-            to_plot = [
-                PlotAction("board", [pcbnew.Edge_Cuts], self._process_baselayer),
-                PlotAction("clad", [pcbnew.B_Mask], self._process_layer),
-                PlotAction("copper", [pcbnew.B_Cu], self._process_layer),
-                PlotAction("pads", [pcbnew.B_Cu], self._process_layer),
-                PlotAction("pads-mask", [pcbnew.B_Mask], self._process_mask),
-                PlotAction("silk", [pcbnew.B_SilkS], self._process_layer),
-                PlotAction("outline", [pcbnew.Edge_Cuts], self._process_outline)
-            ]
-        else:
-            to_plot = [
-                PlotAction("board", [pcbnew.Edge_Cuts], self._process_baselayer),
-                PlotAction("clad", [pcbnew.F_Mask], self._process_layer),
-                PlotAction("copper", [pcbnew.F_Cu], self._process_layer),
-                PlotAction("pads", [pcbnew.F_Cu], self._process_layer),
-                PlotAction("pads-mask", [pcbnew.F_Mask], self._process_mask),
-                PlotAction("silk", [pcbnew.F_SilkS], self._process_layer),
-                PlotAction("outline", [pcbnew.Edge_Cuts], self._process_outline)
-            ]
+        for e in ELEMENTS_USED[self.only_mask]:
+            to_plot.append(PlotAction(e, [SUBSTRATE_ELEMENTS[e][plotter.render_back]], SUBSTRATE_PROCESS[e]))
 
         self._container = etree.Element("g", id="substrate")
         self._container.attrib["clip-path"] = "url(#cut-off)"
  • Fixed the collect_holes function to support KiCad 5
    • pad.GetDrillSizeX() and pad.GetDrillSizeY() are KiCad 6 specific, you must use pad.GetDrillSize()
    • KiCad 5 vias were skipped
    • Vias detection crashed on KiCad 5
diff --git a/kibot/PcbDraw/plot.py b/kibot/PcbDraw/plot.py
index f8990722..17f90185 100644
--- a/kibot/PcbDraw/plot.py
+++ b/kibot/PcbDraw/plot.py
@@ -626,13 +626,15 @@ def collect_holes(board: pcbnew.BOARD) -> List[Hole]:
             continue
         for pad in module.Pads():
             pos = pad.GetPosition()
+            drs = pad.GetDrillSize()
             holes.append(Hole(
                 position=(pos[0], pos[1]),
                 orientation=pad.GetOrientation(),
-                drillsize=(pad.GetDrillSizeX(), pad.GetDrillSizeY())
+                drillsize=(drs.x, drs.y)
             ))
+    via_type = 'VIA' if not isV6(KICAD_VERSION) else 'PCB_VIA'
     for track in board.GetTracks():
-        if not isinstance(track, pcbnew.PCB_VIA) or not isV6(KICAD_VERSION):
+        if track.GetClass() != via_type:
             continue
         pos = track.GetPosition()
         holes.append(Hole(
  • Changed pcbnewTransition to be locally included.

    • Just 2.8 kiB no worth the effort of pulling a dependency
  • Replaced numpy by a very simple code

    • Currently svgpathtool is disabled, it really needs numpy
    • numpy is used to:
      • Multiply matrices (1 line code)
      • Find the index of the smaller element (1 line code)
      • I added a replacemt for the array function, it just makes all matrix elements float
  • Allow constructing PcbPlotter() using an already loaded board

    • So we don't load it again
  • Changed the margin to be a tuple, so the user can control the margins individually

  • Added KiCad 6 SVG precision.

    • Fixes issues with Browsers
    • A plotter member controls the precision
    • We adjust the ki2svg and svg2ki converters before plotting
    • This idea was also adopted by upstream and the code adapted to the way the upstream implemented it

mdrenderer.py

No current changes

populate.py

  • Removed the command line interface, just because it pulls click
  • Added create_renderer. Just creates the correct MD/HTML mistune renderer
  • Made mdrenderer import relative. So we get the mdrenderer from the same dir, not the system
  • Replicated find_data_file (from plot.py) to avoid cross dependencies

present.py

This file comes from KiKit, but it has too much in common with populate.py.

  • Removed click import, unused
  • Removed the try in boardpage, too broad, doesn't help
  • Removed kikit import, _renderBoards must be implemented in a different way
  • Imported local pybars
  • Added source_front, source_back and source_gerbers to the boards. So now the images and gerbers come from outside.
  • Adapted Template._renderBoards to just copy the files (keeping the extensions)
  • Added listResources, with some changes to copyRelativeTo and _copyResources
  • The command used for git is now configurable

2023-02-14 Update

  • Changed to transition 0.3.2 (is a tag, detached from main?!)
  • Applied v7 compatibility patches from 9c676a7494995c5aeab086e041bc0ca3967f472d to 6e9c0b7077b5cfed58866f13ad745130e8920185 (2023-01-12)

2023-03-01 Update

  • Bumped lib footprints (afbab947d981c4583fc6e168c66fc63c31ba6d69)

2023-03-20 Various fixes and changes in resistor colors

diff --git a/kibot/PcbDraw/plot.py b/kibot/PcbDraw/plot.py
index 8ca660e6..9dc45ba9 100644
--- a/kibot/PcbDraw/plot.py
+++ b/kibot/PcbDraw/plot.py
@@ -18,6 +18,7 @@ from . import np
 from .unit import read_resistance
 from lxml import etree, objectify # type: ignore
 from .pcbnew_transition import KICAD_VERSION, isV6, isV7, pcbnew # type: ignore
+from ..gs import GS
 
 T = TypeVar("T")
 Numeric = Union[int, float]
@@ -56,6 +57,8 @@ default_style = {
         7: '#cc00cc',
         8: '#666666',
         9: '#cccccc',
+        -1: '#ffc800',
+        -2: '#d9d9d9',
         '1%': '#805500',
         '2%': '#ff0000',
         '0.5%': '#00cc11',
@@ -64,6 +67,7 @@ default_style = {
         '0.05%': '#666666',
         '5%': '#ffc800',
         '10%': '#d9d9d9',
+        '20%': '#ffe598',
     }
 }
 
@@ -884,10 +888,15 @@ class PlotComponents(PlotInterface):
         try:
             res, tolerance = self._get_resistance_from_value(value)
             power = math.floor(res.log10()) - 1
-            res = Decimal(int(res / 10 ** power))
+            res = str(Decimal(int(res / Decimal(10) ** power)))
+            if power == -3:
+                power += 1
+                res = '0'+res
+            elif power < -3:
+                raise UserWarning(f"Resistor value must be 0.01 or bigger")
             resistor_colors = [
-                self._plotter.get_style("tht-resistor-band-colors", int(str(res)[0])),
-                self._plotter.get_style("tht-resistor-band-colors", int(str(res)[1])),
+                self._plotter.get_style("tht-resistor-band-colors", int(res[0])),
+                self._plotter.get_style("tht-resistor-band-colors", int(res[1])),
                 self._plotter.get_style("tht-resistor-band-colors", int(power)),
                 self._plotter.get_style("tht-resistor-band-colors", tolerance)
             ]
@@ -914,7 +923,7 @@ class PlotComponents(PlotInterface):
             return
 
     def _get_resistance_from_value(self, value: str) -> Tuple[Decimal, str]:
-        res, tolerance = None, "5%"
+        res, tolerance = None, str(GS.global_default_resistor_tolerance)+"%"
         value_l = value.split(" ", maxsplit=1)
         try:
             res = read_resistance(value_l[0])
@@ -1084,6 +1093,12 @@ class PcbPlotter():
             lib = str(footprint.GetFPID().GetLibNickname()).strip()
             name = str(footprint.GetFPID().GetLibItemName()).strip()
             value = footprint.GetValue().strip()
+            if not LEGACY_KICAD:
+                # Look for a tolerance in the properties
+                prop = footprint.GetProperties()
+                tol = next(filter(lambda x: x, map(prop.get, GS.global_field_tolerance)), None)
+                if tol:
+                    value = value+' '+tol
             ref = footprint.GetReference().strip()
             center = footprint.GetPosition()
             orient = math.radians(footprint.GetOrientation().AsDegrees())

2023-03-27 Fix for KiCad 7.0.1 polygons

diff --git a/kibot/PcbDraw/plot.py b/kibot/PcbDraw/plot.py
index 9dc45ba9..8df84469 100644
--- a/kibot/PcbDraw/plot.py
+++ b/kibot/PcbDraw/plot.py
@@ -408,7 +408,22 @@ def get_board_polygon(svg_elements: etree.Element) -> etree.Element:
     for group in svg_elements:
         for svg_element in group:
             if svg_element.tag == "path":
-                elements.append(SvgPathItem(svg_element.attrib["d"]))
+                path = svg_element.attrib["d"]
+                # Check if this is a closed polygon (KiCad 7.0.1+)
+                polygon = re.fullmatch(r"M ((\d+\.\d+),(\d+\.\d+) )+Z", path)
+                if polygon:
+                    # Yes, decompose it in lines
+                    polygon = re.findall(r"(\d+\.\d+),(\d+\.\d+) ", path)
+                    start = polygon[0]
+                    # Close it
+                    polygon.append(polygon[0])
+                    # Add the lines
+                    for end in polygon[1:]:
+                        path = 'M'+start[0]+' '+start[1]+' L'+end[0]+' '+end[1]
+                        elements.append(SvgPathItem(path))
+                        start = end
+                else:
+                    elements.append(SvgPathItem(path))
             elif svg_element.tag == "circle":
                 # Convert circle to path
                 att = svg_element.attrib

2023-03-30 Removed the tolerance look-up, now using electro_grammar

So now unit.py is in charge of returning the tolerance. Note that we still use a field, but in a very ridiculous way because we add it to the value, to then separate it.

diff --git a/kibot/PcbDraw/plot.py b/kibot/PcbDraw/plot.py
index 23b7d31f..65fbea66 100644
--- a/kibot/PcbDraw/plot.py
+++ b/kibot/PcbDraw/plot.py
@@ -938,21 +938,20 @@ class PlotComponents(PlotInterface):
             return
 
     def _get_resistance_from_value(self, value: str) -> Tuple[Decimal, str]:
-        res, tolerance = None, str(GS.global_default_resistor_tolerance)+"%"
-        value_l = value.split(" ", maxsplit=1)
+        res, tolerance = None, None
         try:
-            res = read_resistance(value_l[0])
+            res, tolerance = read_resistance(value)
         except ValueError:
-            raise UserWarning(f"Invalid resistor value {value_l[0]}")
-        if len(value_l) > 1:
-            t_string = value_l[1].strip().replace(" ", "")
-            if "%" in t_string:
-                s = self._plotter.get_style("tht-resistor-band-colors")
-                if not isinstance(s, dict):
-                    raise RuntimeError(f"Invalid style specified, tht-resistor-band-colors should be dictionary, got {type(s)}")
-                if t_string.strip() not in s:
-                    raise UserWarning(f"Invalid resistor tolerance {value_l[1]}")
-                tolerance = t_string
+            raise UserWarning(f"Invalid resistor value {value}")
+        if tolerance is None:
+            tolerance = GS.global_default_resistor_tolerance
+        tolerance = str(tolerance)+"%"
+        s = self._plotter.get_style("tht-resistor-band-colors")
+        if not isinstance(s, dict):
+            raise RuntimeError(f"Invalid style specified, tht-resistor-band-colors should be dictionary, got {type(s)}")
+        if tolerance not in s:
+            raise UserWarning(f"Invalid resistor tolerance {tolerance}")
+            tolerance = "5%"
         return res, tolerance
 
 
@@ -1113,7 +1112,7 @@ class PcbPlotter():
                 prop = footprint.GetProperties()
                 tol = next(filter(lambda x: x, map(prop.get, GS.global_field_tolerance)), None)
                 if tol:
-                    value = value+' '+tol
+                    value = value+' '+tol.strip()
             ref = footprint.GetReference().strip()
             center = footprint.GetPosition()
             orient = math.radians(footprint.GetOrientation().AsDegrees())
diff --git a/kibot/PcbDraw/unit.py b/kibot/PcbDraw/unit.py
index 2fad683c..0c5dfcab 100644
--- a/kibot/PcbDraw/unit.py
+++ b/kibot/PcbDraw/unit.py
@@ -1,10 +1,9 @@
 # Author: Salvador E. Tropea
 # License: MIT
-from decimal import Decimal
 from ..bom.units import comp_match
 
 
-def read_resistance(value: str) -> Decimal:
+def read_resistance(value: str):
     """
     Given a string, try to parse resistance and return it as Ohms (Decimal)
 
@@ -13,5 +12,4 @@ def read_resistance(value: str) -> Decimal:
     res = comp_match(value, 'R')
     if res is None:
         raise ValueError(f"Cannot parse '{value}' to resistance")
-    v, mul, uni = res
-    return Decimal(str(v))*Decimal(str(mul[0]))
+    return res.get_decimal(), res.get_extra('tolerance')

2024-02-08 Adapt to v8

  • Include v8 as a v7
  • Now KiCad optimizes the SVGs, so we can't just set the fill/stroke at top-level and remove it from every group. This is because now KiCad exploits the fill: none and stroke: none cases. So now, instead of removing the fill/stroke, I'm forcing them to the desired color, unless KiCad used none
  • Added SvgPathItem.str to make the debug easier (the code was failing to assemble the edge)
  • Applied the upstream patch "As we cannot interpret mask cropping, ..." (last chunk)
diff --git a/kibot/PcbDraw/plot.py b/kibot/PcbDraw/plot.py
index c9653ac5..a72a944c 100644
--- a/kibot/PcbDraw/plot.py
+++ b/kibot/PcbDraw/plot.py
@@ -17,7 +17,7 @@ from typing import Callable, Dict, List, Optional, Tuple, TypeVar, Union, Any
 from . import np
 from .unit import read_resistance
 from lxml import etree, objectify # type: ignore
-from .pcbnew_transition import KICAD_VERSION, isV6, isV7, pcbnew # type: ignore
+from .pcbnew_transition import isV6, isV7, isV8, pcbnew # type: ignore
 from ..gs import GS
 
 T = TypeVar("T")
@@ -31,7 +31,7 @@ PKG_BASE = os.path.dirname(__file__)
 
 etree.register_namespace("xlink", "http://www.w3.org/1999/xlink")
 
-LEGACY_KICAD = not isV6() and not isV7()
+LEGACY_KICAD = not isV6() and not isV7() and not isV8()
 
 default_style = {
     "copper": "#417e5a",
@@ -103,7 +103,7 @@ class SvgPathItem:
         dx = p1[0] - p2[0]
         dy = p1[1] - p2[1]
         pseudo_distance = dx*dx + dy*dy
-        if isV7():
+        if isV7() or isV8():
             return pseudo_distance < 0.01 ** 2
         return pseudo_distance < 100 ** 2
 
@@ -123,6 +123,9 @@ class SvgPathItem:
             assert(self.args is not None)
             self.args[4] = 1 if self.args[4] < 0.5 else 0
 
+    def __str__(self) -> str:
+        return f"{self.start} - {self.end} {self.type}"
+
 def matrix(data: List[List[Numeric]]) -> Matrix:
     return np.array(data, dtype=np.float32)
 
@@ -358,7 +361,9 @@ def extract_svg_content(root: etree.Element) -> List[etree.Element]:
             el.tag = el.tag.split('}', 1)[1]
     return [ x for x in root if x.tag and x.tag not in ["title", "desc"]]
 
-def strip_style_svg(root: etree.Element, keys: List[str], forbidden_colors: List[str]) -> bool:
+def strip_style_svg(root: etree.Element, keys: List[str], forbidden_colors: List[str], new_val: str) -> bool:
+    """ Remove elements with fill and/or stoke using any *forbidden_colors*
+        Change attributes listed in keys """
     elements_to_remove = []
     for el in root.getiterator():
         if "style" in el.attrib:
@@ -375,8 +380,14 @@ def strip_style_svg(root: etree.Element, keys: List[str], forbidden_colors: List
             stroke = styles.get("stroke", "").lower()
             if fill in forbidden_colors or stroke in forbidden_colors:
                 elements_to_remove.append(el)
+            new_styles = {}
+            for key, val in styles.items():
+                if key not in keys or val == 'none':
+                    new_styles[key] = val
+                else:
+                    new_styles[key] = new_val
             el.attrib["style"] = ";" \
-                .join([f"{key}: {val}" for key, val in styles.items() if key not in keys]) \
+                .join([f"{key}: {val}" for key, val in new_styles.items()]) \
                 .replace("  ", " ") \
                 .strip()
     for el in elements_to_remove:
@@ -662,8 +673,9 @@ class PlotSubstrate(PlotInterface):
         self._plotter.append_board_element(self._container)
 
     def _process_layer(self,name: str, source_filename: str) -> None:
+        style = self._plotter.get_style(name)
         layer = etree.SubElement(self._container, "g", id="substrate-" + name,
-            style="fill:{0}; stroke:{0};".format(self._plotter.get_style(name)))
+            style="fill:{0}; stroke:{0};".format(style))
         if name == "pads":
             layer.attrib["mask"] = "url(#pads-mask)"
         if name == "silk":
@@ -672,15 +684,16 @@ class PlotSubstrate(PlotInterface):
             # Forbidden colors = workaround - KiCAD plots vias white
             # See https://gitlab.com/kicad/code/kicad/-/issues/10491
             if not strip_style_svg(element, keys=["fill", "stroke"],
-                                   forbidden_colors=["#ffffff"]):
+                                   forbidden_colors=["#ffffff"], new_val=style):
                 layer.append(element)
 
     def _process_outline(self, name: str, source_filename: str) -> None:
         if self.outline_width == 0:
             return
+        style = self._plotter.get_style(name)
         layer = etree.SubElement(self._container, "g", id="substrate-" + name,
             style="fill:{0}; stroke:{0}; stroke-width: {1}".format(
-                self._plotter.get_style(name),
+                style,
                 self._plotter.ki2svg(self.outline_width)))
         if name == "pads":
             layer.attrib["mask"] = "url(#pads-mask)"
@@ -690,7 +703,7 @@ class PlotSubstrate(PlotInterface):
             # Forbidden colors = workaround - KiCAD plots vias white
             # See https://gitlab.com/kicad/code/kicad/-/issues/10491
             if not strip_style_svg(element, keys=["fill", "stroke", "stroke-width"],
-                                   forbidden_colors=["#ffffff"]):
+                                   forbidden_colors=["#ffffff"], new_val=style):
                 layer.append(element)
         for hole in collect_holes(self._plotter.board):
             position = [self._plotter.ki2svg(coord) for coord in hole.position]
@@ -708,9 +721,9 @@ class PlotSubstrate(PlotInterface):
             get_board_polygon(
                 extract_svg_content(
                     read_svg_unique(source_filename, self._plotter.unique_prefix()))))
-
+        style = self._plotter.get_style(name)
         layer = etree.SubElement(self._container, "g", id="substrate-"+name,
-            style="fill:{0}; stroke:{0};".format(self._plotter.get_style(name)))
+            style="fill:{0}; stroke:{0};".format(style))
         layer.append(
             get_board_polygon(
                 extract_svg_content(
@@ -719,7 +732,7 @@ class PlotSubstrate(PlotInterface):
             # Forbidden colors = workaround - KiCAD plots vias white
             # See https://gitlab.com/kicad/code/kicad/-/issues/10491
             if not strip_style_svg(element, keys=["fill", "stroke"],
-                                  forbidden_colors=["#ffffff"]):
+                                  forbidden_colors=["#ffffff"], new_val=style):
                 layer.append(element)
 
     def _process_mask(self, name: str, source_filename: str) -> None:
@@ -980,13 +993,14 @@ class PlotVCuts(PlotInterface):
         ])
 
     def _process_vcuts(self, name: str, source_filename: str) -> None:
+        style = self._plotter.get_style("vcut")
         layer = etree.Element("g", id="substrate-vcuts",
-            style="fill:{0}; stroke:{0};".format(self._plotter.get_style("vcut")))
+            style="fill:{0}; stroke:{0};".format(style))
         for element in extract_svg_content(read_svg_unique(source_filename, self._plotter.unique_prefix())):
             # Forbidden colors = workaround - KiCAD plots vias white
             # See https://gitlab.com/kicad/code/kicad/-/issues/10491
             if not strip_style_svg(element, keys=["fill", "stroke"],
-                                   forbidden_colors=["#ffffff"]):
+                                   forbidden_colors=["#ffffff"], new_val=style):
                 layer.append(element)
         self._plotter.append_board_element(layer)
 
@@ -1002,11 +1016,12 @@ class PlotPaste(PlotInterface):
         self._plotter.execute_plot_plan(plan)
 
     def _process_paste(self, name: str, source_filename: str) -> None:
+        style = self._plotter.get_style("paste")
         layer = etree.Element("g", id="substrate-paste",
-            style="fill:{0}; stroke:{0};".format(self._plotter.get_style("paste")))
+            style="fill:{0}; stroke:{0};".format(style))
         for element in extract_svg_content(read_svg_unique(source_filename, self._plotter.unique_prefix())):
             if not strip_style_svg(element, keys=["fill", "stroke"],
-                                   forbidden_colors=["#ffffff"]):
+                                   forbidden_colors=["#ffffff"], new_val=style):
                 layer.append(element)
         self._plotter.append_board_element(layer)
 
@@ -1047,7 +1062,7 @@ class PcbPlotter():
 
         self.yield_warning: Callable[[str, str], None] = lambda tag, msg: None # Handle warnings
 
-        if isV7():
+        if isV7() or isV8():
             self.ki2svg = self._ki2svg_v7
             self.svg2ki = self._svg2ki_v7
         elif isV6():
@@ -1243,7 +1258,7 @@ class PcbPlotter():
             popt.SetTextMode(pcbnew.PLOT_TEXT_MODE_STROKE)
             if isV6():
                 popt.SetSvgPrecision(self.svg_precision, False)
-            elif isV7():
+            elif isV7() or isV8():
                 popt.SetSvgPrecision(self.svg_precision)
             for action in to_plot:
                 if len(action.layers) == 0:
@@ -1304,7 +1319,19 @@ class PcbPlotter():
 
             from lxml.etree import tostring as serializeXml # type: ignore
             from . import svgpathtools # type: ignore
-            paths = svgpathtools.document.flattened_paths(xmlParse(serializeXml(svg)))
+            tree = xmlParse(serializeXml(svg))
+
+            # As we cannot interpret mask cropping, we cannot simply take all paths
+            # from source document (as e.g., silkscreen outside PCB) would enlarge
+            # the canvas. Instead, we take bounding box of the substrate and
+            # components separately
+            paths = []
+            components = tree.find(".//*[@id='componentContainer']")
+            if components is not None:
+                paths += svgpathtools.document.flattened_paths(components)
+            substrate = tree.find(".//*[@id='cut-off']")
+            if substrate is not None:
+                paths += svgpathtools.document.flattened_paths(substrate)
 
             if len(paths) == 0:
                 return

2024-02-08 Revert changes, because impact on v5

  • The isV8() is not strictly needed, there to allow doing diffs between old and new generated SVGs
  • The upstream approach fails to compute the mirror for KiCad 5/6, but I'm not sure if also for other cases
diff --git a/kibot/PcbDraw/plot.py b/kibot/PcbDraw/plot.py
index a72a944c..dd86b03d 100644
--- a/kibot/PcbDraw/plot.py
+++ b/kibot/PcbDraw/plot.py
@@ -384,7 +384,7 @@ def strip_style_svg(root: etree.Element, keys: List[str], forbidden_colors: List
             for key, val in styles.items():
                 if key not in keys or val == 'none':
                     new_styles[key] = val
-                else:
+                elif isV8():
                     new_styles[key] = new_val
             el.attrib["style"] = ";" \
                 .join([f"{key}: {val}" for key, val in new_styles.items()]) \
@@ -1319,19 +1319,7 @@ class PcbPlotter():
 
             from lxml.etree import tostring as serializeXml # type: ignore
             from . import svgpathtools # type: ignore
-            tree = xmlParse(serializeXml(svg))
-
-            # As we cannot interpret mask cropping, we cannot simply take all paths
-            # from source document (as e.g., silkscreen outside PCB) would enlarge
-            # the canvas. Instead, we take bounding box of the substrate and
-            # components separately
-            paths = []
-            components = tree.find(".//*[@id='componentContainer']")
-            if components is not None:
-                paths += svgpathtools.document.flattened_paths(components)
-            substrate = tree.find(".//*[@id='cut-off']")
-            if substrate is not None:
-                paths += svgpathtools.document.flattened_paths(substrate)
+            paths = svgpathtools.document.flattened_paths(xmlParse(serializeXml(svg)))
 
             if len(paths) == 0:
                 return