diff --git a/README.md b/README.md index c851c5d9..55f015e4 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index a1c0adbc..6ca6bebc 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -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 diff --git a/kibot/PcbDraw/plot.py b/kibot/PcbDraw/plot.py index 9685d032..9a0151b3 100644 --- a/kibot/PcbDraw/plot.py +++ b/kibot/PcbDraw/plot.py @@ -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)) diff --git a/kibot/out_pcbdraw.py b/kibot/out_pcbdraw.py index 2514f986..76200998 100644 --- a/kibot/out_pcbdraw.py +++ b/kibot/out_pcbdraw.py @@ -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__()