From ae01451b6dbc5fafd7fe3c908790284e1b0ca3dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Mr=C3=A1zek?= Date: Wed, 28 Dec 2022 20:10:47 +0100 Subject: [PATCH] Implement percentage for panel position --- doc/panelizeCli.md | 5 ++-- kikit/defs.py | 18 ++++++++++++ kikit/kicadUtil.py | 25 ++++++++++++++++ kikit/panelize.py | 30 ++++++++++++++++++-- kikit/panelize_ui.py | 4 +-- kikit/panelize_ui_impl.py | 12 ++++++-- kikit/panelize_ui_sections.py | 20 +++++++++---- kikit/resources/panelizePresets/default.json | 6 ++-- kikit/units.py | 24 +++++++++++++++- 9 files changed, 126 insertions(+), 18 deletions(-) create mode 100644 kikit/kicadUtil.py diff --git a/doc/panelizeCli.md b/doc/panelizeCli.md index 43b82e54..edb0c83d 100644 --- a/doc/panelizeCli.md +++ b/doc/panelizeCli.md @@ -499,8 +499,9 @@ size from the source board. This feature is not supported on KiCAD 5 - `anchor` - Point of the panel to be placed at given position. Can be one of `tl`, `tr`, `bl`, `br` (corners), `mt`, `mb`, `ml`, `mr` (middle of sides), - `c` (center). The anchors refer to the panel outline. Default `tl` -- `posx`, `posy` - the position of the panel on the page. Default `15mm` + `c` (center). The anchors refer to the panel outline. Default `mt` +- `posx`, `posy` - the position of the panel on the page. Default `50%` for + `posx` and `20mm` for `posy`. ### Custom diff --git a/kikit/defs.py b/kikit/defs.py index fa8fdf40..01fef371 100644 --- a/kikit/defs.py +++ b/kikit/defs.py @@ -1,4 +1,5 @@ from enum import Enum, IntEnum +from .units import mm, inch # These classes miss in the exported interface @@ -88,3 +89,20 @@ class MODULE_ATTR_T(IntEnum): PAPER_SIZES = [f"A{size}" for size in range(6)] + ["A", "B", "C", "D", "E"] + \ ["USLetter", "USLegal", "USLedger"] PAPER_SIZES = PAPER_SIZES + [f"{paper}-portrait" for paper in PAPER_SIZES] + +PAPER_DIMENSIONS = { + "A5": (210 * mm, 148 * mm), + "A4": (297 * mm, 210 * mm), + "A3": (420 * mm, 297 * mm), + "A2": (594 * mm, 420 * mm), + "A1": (841 * mm, 594 * mm), + "A0": (1198 * mm, 841 * mm), + "A": (11 * inch, 8.5 * inch), + "B": (17 * inch, 11 * inch), + "C": (22 * inch, 17 * inch), + "D": (34 * inch, 22 * inch), + "E": (44 * inch, 34 * inch), + "USLetter": (11 * inch, 8.5 * inch), + "USLegal": (14 * inch, 8.5 * inch), + "USLedger": (17 * inch, 11 * inch) +} diff --git a/kikit/kicadUtil.py b/kikit/kicadUtil.py new file mode 100644 index 00000000..8b29fefe --- /dev/null +++ b/kikit/kicadUtil.py @@ -0,0 +1,25 @@ +from typing import Tuple +from .common import KiLength +from .units import mm +from .sexpr import SExpr, findNode +from .defs import PAPER_DIMENSIONS + +def getPageDimensionsFromAst(ast: SExpr) -> Tuple[KiLength, KiLength]: + paperNode = findNode(ast, "paper") + if paperNode is None: + # KiCAD 5 board use "page" instead of "paper" + paperNode = findNode(ast, "page") + if paperNode is None: + raise RuntimeError("Source document doesn't contain paper size information") + value = paperNode.items[1].value + if value == "User": + size = (paperNode[2].value, paperNode[3].value) + return tuple(int(float(x) * mm) for x in size) + try: + size = PAPER_DIMENSIONS[value] + if len(paperNode.items) >= 3 and paperNode.items[2] == "portrait": + size = (size[1], size[0]) + return tuple(int(x) for x in size) + except KeyError: + raise RuntimeError(f"Uknown paper size {value}") from None + diff --git a/kikit/panelize.py b/kikit/panelize.py index fb557fb5..bc63a4c3 100644 --- a/kikit/panelize.py +++ b/kikit/panelize.py @@ -22,10 +22,11 @@ from kikit import substrate from kikit import units +from kikit.kicadUtil import getPageDimensionsFromAst from kikit.substrate import Substrate, linestringToKicad, extractRings -from kikit.defs import STROKE_T, Layer, EDA_TEXT_HJUSTIFY_T, EDA_TEXT_VJUSTIFY_T, PAPER_SIZES +from kikit.defs import PAPER_DIMENSIONS, STROKE_T, Layer, EDA_TEXT_HJUSTIFY_T, EDA_TEXT_VJUSTIFY_T, PAPER_SIZES from kikit.common import * -from kikit.sexpr import parseSexprF, SExpr, Atom +from kikit.sexpr import parseSexprF, SExpr, Atom, findNode from kikit.annotations import AnnotationReader, TabAnnotation from kikit.drc import DrcExclusion, readBoardDrcExclusions, serializeExclusion @@ -734,6 +735,14 @@ def inheritPageSize(self, board: Union[pcbnew.BOARD, str]) -> None: if not isinstance(board, pcbnew.BOARD): board = pcbnew.LoadBoard(board) self.board.SetPageSettings(board.GetPageSettings()) + self.pageSize = None + + # What follows is a hack as KiCAD has no API for page access. Therefore, + # we have to read out the page size from the source board and save it so + # we can recover it. + with open(board.GetFileName(), "r") as f: + tree = parseSexprF(f, limit=10) # Introduce limit to speed up parsing + self._inheritedPageDimensions = getPageDimensionsFromAst(tree) def setPageSize(self, size: Union[str, Tuple[int, int]] ) -> None: """ @@ -744,6 +753,23 @@ def setPageSize(self, size: Union[str, Tuple[int, int]] ) -> None: raise RuntimeError(f"Unknown paper size: {size}") self.pageSize = size + def getPageDimensions(self) -> Tuple[KiLength, KiLength]: + """ + Get page size in KiCAD units for the current panel + """ + if self.pageSize is None: + return self._inheritedPageDimensions + if isinstance(self.pageSize, tuple): + return self.pageSize + if isinstance(self.pageSize, str): + if self.pageSize.endswith("-portrait"): + # Portrait + pageSize = PAPER_DIMENSIONS[self.pageSize.split("-")[0]] + return pageSize[1], pageSize[0] + else: + return PAPER_DIMENSIONS[self.pageSize] + raise RuntimeError("Unknown page dimension - this is probably a bug and you should report it.") + def setProperties(self, properties): """ Set text properties cached in the board diff --git a/kikit/panelize_ui.py b/kikit/panelize_ui.py index 4c9b92c4..132f9adc 100644 --- a/kikit/panelize_ui.py +++ b/kikit/panelize_ui.py @@ -289,8 +289,8 @@ def doPanelization(input, output, preset, plugins=[]): ki.buildCopperfill(preset["copperfill"], panel) ki.setStackup(preset["source"], panel) - ki.positionPanel(preset["page"], panel) ki.setPageSize(preset["page"], panel, board) + ki.positionPanel(preset["page"], panel) ki.runUserScript(preset["post"], panel) useHookPlugins(lambda x: x.finish(panel)) @@ -348,8 +348,8 @@ def separate(input, output, source, page, debug, keepannotations, preservearcs): panel.appendBoard(input, destination, sourceArea, interpretAnnotations=(not keepannotations)) ki.setStackup(preset["source"], panel) - ki.positionPanel(preset["page"], panel) ki.setPageSize(preset["page"], panel, board) + ki.positionPanel(preset["page"], panel) panel.save(reconstructArcs=preservearcs) except Exception as e: diff --git a/kikit/panelize_ui_impl.py b/kikit/panelize_ui_impl.py index 28a97ab7..05811101 100644 --- a/kikit/panelize_ui_impl.py +++ b/kikit/panelize_ui_impl.py @@ -5,7 +5,7 @@ from shapely.geometry import box from kikit.plugin import HookPlugin from kikit.text import kikitTextVars -from kikit.units import BaseValue +from kikit.units import BaseValue, PercentageValue from kikit.panelize_ui_sections import * from kikit.substrate import SubstrateNeighbors from kikit.common import resolveAnchor @@ -33,6 +33,8 @@ def encodePreset(value): return "none" if isinstance(value, BaseValue): return str(value) + if isinstance(value, PercentageValue): + return str(value) if isinstance(value, EDA_TEXT_HJUSTIFY_T) or isinstance(value, EDA_TEXT_VJUSTIFY_T): return writeJustify(value) if isinstance(value, Enum): @@ -644,8 +646,12 @@ def positionPanel(preset, panel): Position the panel on the paper """ try: - origin = resolveAnchor(preset["anchor"])(panel.boardSubstrate.boundingBox()) - translateVec = (-origin[0] + preset["posx"], -origin[1] + preset["posy"]) + bBox = panel.boardSubstrate.boundingBox() + pageSize = panel.getPageDimensions() + posx = preset["posx"] * pageSize[0] if isinstance(preset["posx"], PercentageValue) else preset["posx"] + posy = preset["posy"] * pageSize[1] if isinstance(preset["posy"], PercentageValue) else preset["posy"] + origin = resolveAnchor(preset["anchor"])(bBox) + translateVec = (-origin[0] + posx, -origin[1] + posy) panel.translate(translateVec) except KeyError as e: raise PresetError(f"Missing parameter '{e}' in section 'page'") diff --git a/kikit/panelize_ui_sections.py b/kikit/panelize_ui_sections.py index 443f3fb5..da1ae120 100644 --- a/kikit/panelize_ui_sections.py +++ b/kikit/panelize_ui_sections.py @@ -2,7 +2,7 @@ import os from typing import Any, List from kikit import plugin -from kikit.units import readLength, readAngle +from kikit.units import readLength, readAngle, readPercents from kikit.defs import Layer, EDA_TEXT_HJUSTIFY_T, EDA_TEXT_VJUSTIFY_T, PAPER_SIZES class PresetError(RuntimeError): @@ -31,6 +31,16 @@ def __init__(self, *args, **kwargs): def validate(self, x): return readLength(x) +class SLengthOrPercent(SectionBase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def validate(self, x): + x = x.strip() + if x.endswith("%"): + return readPercents(x) + return readLength(x) + class SAngle(SectionBase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -648,12 +658,12 @@ def ppPost(section): ANCHORS, always(), "Anchor for positioning the panel on the page"), - "posx": SLength( + "posx": SLengthOrPercent( always(), - "X position of the panel"), - "posy": SLength( + "X position of the panel. Length or percents of page width."), + "posy": SLengthOrPercent( always(), - "Y position of the panel"), + "Y position of the panel. Length or percents of page height."), "width": SLength( typeIn(["user"]), "Width of custom paper"), diff --git a/kikit/resources/panelizePresets/default.json b/kikit/resources/panelizePresets/default.json index debeb792..107a5ad7 100644 --- a/kikit/resources/panelizePresets/default.json +++ b/kikit/resources/panelizePresets/default.json @@ -172,9 +172,9 @@ }, "page": { "type": "inherit", - "anchor": "tl", - "posx": "15mm", - "posy": "15mm", + "anchor": "mt", + "posx": "50%", + "posy": "20mm", "width": "1000mm", "height": "1000mm" }, diff --git a/kikit/units.py b/kikit/units.py index bce74253..9d8033e5 100644 --- a/kikit/units.py +++ b/kikit/units.py @@ -15,7 +15,7 @@ class UnitError(RuntimeError): deg = 10 rad = 180 / math.pi * deg -UNIT_SPLIT = re.compile(r"\s*(-?\s*\d+(\.\d*)?)\s*(\w+)$") +UNIT_SPLIT = re.compile(r"\s*(-?\s*\d+(\.\d*)?)\s*(\w+|\%)$") class BaseValue(int): """ @@ -33,6 +33,24 @@ def __repr__(self): return f"" +class PercentageValue(float): + """ + Value in percents that remembers its original string representation. + + Value is stored as floating point number where 1 corresponds to 100 %. + """ + def __new__(cls, value, strRepr): + x = super().__new__(cls, value) + x.str = strRepr + return x + + def __str__(self): + return self.str + + def __repr__(self): + return f"" + + def readUnit(unitDir, unitStr): match = UNIT_SPLIT.match(unitStr) if not match: @@ -70,3 +88,7 @@ def readAngle(unitStr): if not isinstance(unitStr, str): raise RuntimeError(f"Got '{unitStr}', an angle with units was expected") return BaseValue(readUnit(unitDir, unitStr), unitStr) + +def readPercents(unitStr): + unitDir = { "%": 0.01 } + return PercentageValue(readUnit(unitDir, unitStr), unitStr)