From 0ea726503b9539d17036b2505e0cf84f6820b332 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Mr=C3=A1zek?= <email@honzamrazek.cz>
Date: Fri, 16 Dec 2022 11:50:54 +0100
Subject: [PATCH] Start v7 compatibility

---
 doc/examples.md              |   4 +-
 doc/resources/examplePost.py |   6 +-
 kikit/annotations.py         |   4 +-
 kikit/common.py              |  92 +++++++++--------
 kikit/drc.py                 |  21 +++-
 kikit/export.py              |  21 +++-
 kikit/fab/common.py          |   6 +-
 kikit/panelize.py            | 192 +++++++++++++++++++----------------
 kikit/panelize_ui.py         |   5 +-
 kikit/panelize_ui_impl.py    |  17 ++--
 kikit/plugin.py              |   2 +-
 kikit/stencil.py             |  80 +++++++--------
 kikit/substrate.py           |  33 +++---
 kikit/units.py               |  25 ++++-
 scripts/exampleDoc.py        |   4 +-
 scripts/panelizeDoc.py       |   5 +-
 test/system/panelize.bats    |   1 +
 17 files changed, 292 insertions(+), 226 deletions(-)

diff --git a/doc/examples.md b/doc/examples.md
index 98a7339..b57fda0 100644
--- a/doc/examples.md
+++ b/doc/examples.md
@@ -782,11 +782,11 @@ script:
 
 ```.py
 from kikit.units import mm
-from pcbnew import wxPoint
+from pcbnew import VECTOR2I
 
 def kikitPostprocess(panel, arg):
     minx, miny, maxx, maxy = panel.panelBBox()
-    position = wxPoint((minx + maxx) / 2, miny + 2 * mm)
+    position = VECTOR2I((minx + maxx) / 2, miny + 2 * mm)
     panel.addNPTHole(position, 3 * mm)
 ```
 
diff --git a/doc/resources/examplePost.py b/doc/resources/examplePost.py
index 1287a0d..c76bb82 100644
--- a/doc/resources/examplePost.py
+++ b/doc/resources/examplePost.py
@@ -1,7 +1,7 @@
 from kikit.units import mm
-from pcbnew import wxPoint
+from pcbnew import VECTOR2I
 
 def kikitPostprocess(panel, arg):
     minx, miny, maxx, maxy = panel.panelBBox()
-    position = wxPoint((minx + maxx) / 2, miny + 2 * mm)
-    panel.addNPTHole(position, 3 * mm)
\ No newline at end of file
+    position = VECTOR2I((minx + maxx) / 2, miny + 2 * mm)
+    panel.addNPTHole(position, 3 * mm)
diff --git a/kikit/annotations.py b/kikit/annotations.py
index fa197cd..d7ddce4 100644
--- a/kikit/annotations.py
+++ b/kikit/annotations.py
@@ -37,8 +37,8 @@ def __init__(self, ref, origin, direction, width, maxLength=fromMm(100)):
     @staticmethod
     def fromFootprint(footprint):
         origin = footprint.GetPosition()
-        radOrientaion = footprint.GetOrientationRadians()
-        direction = (np.cos(radOrientaion), -np.sin(radOrientaion))
+        radOrientation = footprint.GetOrientationRadians()
+        direction = (np.cos(radOrientation), -np.sin(radOrientation))
         props = readKiKitProps(footprint)
         width = units.readLength(props["width"])
         return TabAnnotation(footprint.GetReference(), origin, direction, width)
diff --git a/kikit/common.py b/kikit/common.py
index 8a1e45d..c06a19f 100644
--- a/kikit/common.py
+++ b/kikit/common.py
@@ -2,9 +2,9 @@
 from typing import List, Optional, Tuple, Union
 from kikit.defs import Layer
 from kikit.typing import Box
-from pcbnewTransition import pcbnew, isV6
-from kikit.intervals import Interval, AxialLine
-from pcbnew import wxPoint, wxRect, EDA_RECT
+from pcbnewTransition import pcbnew, isV6, isV7
+from kikit.intervals import AxialLine
+from pcbnew import BOX2I, VECTOR2I, EDA_ANGLE
 import os
 from itertools import product, chain, islice
 import numpy as np
@@ -17,30 +17,36 @@
                                    # cover up numerical imprecisions of Shapely
 
 KiLength = int
-KiAngle = int
-KiKitPoint = Union[pcbnew.wxPoint, Tuple[int, int]]
+KiAngle = EDA_ANGLE
+KiPoint = VECTOR2I
 
-def fromDegrees(angle):
-    return angle * 10
+def fromDegrees(angle: Union[float,int]) -> KiAngle:
+    """Convert angle in degrees to Kicad angle representation"""
+    return EDA_ANGLE(angle, pcbnew.DEGREES_T)
 
-def fromKicadAngle(angle):
-    return angle / 10
-
-def fromMm(mm):
+def fromMm(mm: float) -> KiLength:
     """Convert millimeters to KiCAD internal units"""
     return pcbnew.FromMM(mm)
 
-def toMm(kiUnits):
+def toMm(kiUnits: KiLength) -> float:
     """Convert KiCAD internal units to millimeters"""
     return pcbnew.ToMM(int(kiUnits))
 
-def fitsIn(what: Union[wxRect, wxPoint, EDA_RECT], where: wxRect) -> bool:
+def toKiCADPoint(p) -> KiPoint:
+    """Convert tuple or array like objects to KiCAD point (VECTOR2I)"""
+    assert len(p) == 2
+    return VECTOR2I(*[int(x) for x in p])
+
+def fitsIn(what: Union[BOX2I, VECTOR2I], where: BOX2I) -> bool:
     """
-    Return true iff 'what' (wxRect or wxPoint) is fully contained in 'where'
-    (wxRect)
+    Return true iff 'what' (BOX2I or VECTOR2I) is fully contained in 'where'
+    (BOX2I)
     """
-    assert isinstance(what, (wxRect, EDA_RECT, wxPoint))
-    if isinstance(what, wxPoint):
+    if isV7():
+        assert isinstance(what, (BOX2I, VECTOR2I, pcbnew.wxPoint))
+    else:
+        assert isinstance(what, (BOX2I, VECTOR2I, pcbnew.wxPoint, pcbnew.EDA_RECT))
+    if isinstance(what, VECTOR2I) or isinstance(what, (VECTOR2I, pcbnew.wxPoint)):
         return (what[0] >= where.GetX() and
                 what[0] <= where.GetX() + where.GetWidth() and
                 what[1] >= where.GetY() and
@@ -52,15 +58,12 @@ def fitsIn(what: Union[wxRect, wxPoint, EDA_RECT], where: wxRect) -> bool:
                 what.GetY() + what.GetHeight() <= where.GetY() + where.GetHeight())
 
 def combineBoundingBoxes(a, b):
-    """ Retrun wxRect as a combination of source bounding boxes """
+    """ Retrun BOX2I as a combination of source bounding boxes """
     x1 = min(a.GetX(), b.GetX())
     y1 = min(a.GetY(), b.GetY())
     x2 = max(a.GetX() + a.GetWidth(), b.GetX() + b.GetWidth())
     y2 = max(a.GetY() + a.GetHeight(), b.GetY() + b.GetHeight())
-    # Beware that we cannot use the following code! It will add 1 to width and
-    # height. See https://github.com/wxWidgets/wxWidgets/blob/e43895e5317a1e82e295788264553d9839190337/src/common/gdicmn.cpp#L94-L114
-    # return wxRect(topLeft, bottomRight)
-    return wxRect(x1, y1, x2 - x1, y2 - y1)
+    return BOX2I(VECTOR2I(x1, y1), VECTOR2I(x2 - x1, y2 - y1))
 
 def collectEdges(board, layerId, sourceArea=None):
     """ Collect edges in sourceArea on given layer including footprints """
@@ -114,23 +117,25 @@ def findBoundingBox(edges):
 
 def findBoardBoundingBox(board, sourceArea=None):
     """
-    Returns a bounding box (wxRect) of all Edge.Cuts items either in
-    specified source area (wxRect) or in the whole board
+    Returns a bounding box (BOX2I) of all Edge.Cuts items either in
+    specified source area (BOX2I) or in the whole board
     """
     edges = collectEdges(board, Layer.Edge_Cuts, sourceArea)
     return findBoundingBox(edges)
 
 def rectCenter(rect):
     """
-    Given a wxRect return its center
+    Given a BOX2I return its center
     """
-    return wxPoint(rect.GetX() + rect.GetWidth() // 2, rect.GetY() + rect.GetHeight() // 2)
+    return VECTOR2I(rect.GetX() + rect.GetWidth() // 2, rect.GetY() + rect.GetHeight() // 2)
 
 def rectByCenter(center, width, height):
     """
-    Given a center point and size, return wxRect
+    Given a center point and size, return BOX2I
     """
-    return wxRect(center[0] - width // 2, center[1] - height // 2, width, height)
+    return BOX2I(
+        VECTOR2I(center[0] - width // 2, center[1] - height // 2),
+        VECTOR2I(width, height))
 
 def normalize(vector):
     """ Return a vector with unit length """
@@ -152,19 +157,19 @@ def linestringToSegments(linestring):
 
 def tl(rect):
     """ Return top left corner of rect """
-    return wxPoint(rect.GetX(), rect.GetY())
+    return VECTOR2I(rect.GetX(), rect.GetY())
 
 def tr(rect):
     """ Return top right corner of rect """
-    return wxPoint(rect.GetX() + rect.GetWidth(), rect.GetY())
+    return VECTOR2I(rect.GetX() + rect.GetWidth(), rect.GetY())
 
 def br(rect):
     """ Return bottom right corner of rect """
-    return wxPoint(rect.GetX() + rect.GetWidth(), rect.GetY() + rect.GetHeight())
+    return VECTOR2I(rect.GetX() + rect.GetWidth(), rect.GetY() + rect.GetHeight())
 
 def bl(rect):
     """ Return bottom left corner of rect """
-    return wxPoint(rect.GetX(), rect.GetY() + rect.GetHeight())
+    return VECTOR2I(rect.GetX(), rect.GetY() + rect.GetHeight())
 
 def removeComponents(board, references):
     """
@@ -229,7 +234,8 @@ def shpBBoxExpand(box: Box, x: float, y: Optional[float]=None) -> Box:
 
 def shpBoxToRect(box):
     box = list([int(x) for x in box])
-    return wxRect(box[0], box[1], box[2] - box[0], box[3] - box[1])
+    return BOX2I(toKiCADPoint((box[0], box[1])),
+                 toKiCADPoint((box[2] - box[0], box[3] - box[1])))
 
 def rectToShpBox(rect):
     return shapely.geometry.box(rect.GetX(), rect.GetY(),
@@ -276,19 +282,19 @@ def isVertical(start, end):
 
 def resolveAnchor(anchor):
     """
-    Given a string anchor name, return a function that transforms wxRect into
-    a wxPoint
+    Given a string anchor name, return a function that transforms BOX2I into
+    a VECTOR2I
     """
     choices = {
         "tl": lambda x: x.GetPosition(),
-        "tr": lambda x: x.GetPosition() + wxPoint(x.GetWidth(), 0),
-        "bl": lambda x: x.GetPosition() + wxPoint(0, x.GetHeight()),
-        "br": lambda x: x.GetPosition() + wxPoint(x.GetWidth(), x.GetHeight()),
-        "mt": lambda x: x.GetPosition() + wxPoint(x.GetWidth() / 2, 0),
-        "mb": lambda x: x.GetPosition() + wxPoint(x.GetWidth() / 2, x.GetHeight()),
-        "ml": lambda x: x.GetPosition() + wxPoint(0, x.GetHeight() / 2),
-        "mr": lambda x: x.GetPosition() + wxPoint(x.GetWidth(), x.GetHeight() / 2),
-        "c":  lambda x: x.GetPosition() + wxPoint(x.GetWidth() / 2, x.GetHeight() / 2)
+        "tr": lambda x: x.GetPosition() + toKiCADPoint((x.GetWidth(), 0)),
+        "bl": lambda x: x.GetPosition() + toKiCADPoint((0, x.GetHeight())),
+        "br": lambda x: x.GetPosition() + toKiCADPoint((x.GetWidth(), x.GetHeight())),
+        "mt": lambda x: x.GetPosition() + toKiCADPoint((x.GetWidth() / 2, 0)),
+        "mb": lambda x: x.GetPosition() + toKiCADPoint((x.GetWidth() / 2, x.GetHeight())),
+        "ml": lambda x: x.GetPosition() + toKiCADPoint((0, x.GetHeight() / 2)),
+        "mr": lambda x: x.GetPosition() + toKiCADPoint((x.GetWidth(), x.GetHeight() / 2)),
+        "c":  lambda x: x.GetPosition() + toKiCADPoint((x.GetWidth() / 2, x.GetHeight() / 2))
     }
     return choices[anchor]
 
diff --git a/kikit/drc.py b/kikit/drc.py
index d794d84..ca613bc 100644
--- a/kikit/drc.py
+++ b/kikit/drc.py
@@ -8,7 +8,7 @@
 from typing import Any, Dict, Iterable, List, TextIO, Tuple, Union
 from pathlib import Path
 
-from pcbnewTransition import isV6, pcbnew
+from pcbnewTransition import isV7, pcbnew
 
 from kikit.common import fromMm, toMm
 from kikit.drc_ui import ReportLevel
@@ -20,10 +20,17 @@ def roundCoord(x: int) -> int:
     # emulate that
     return round(x - 50, -4)
 
+def getItemDescription(item: pcbnew.BOARD_ITEM, units: pcbnew.EDA_UNITS = pcbnew.EDA_UNITS_MILLIMETRES):
+    if isV7():
+        uProvider = pcbnew.UNITS_PROVIDER(pcbnew.pcbIUScale, units)
+        return item.GetSelectMenuText(uProvider)
+    else:
+        return item.GetSelectMenuText(units)
+
 def getItemFingerprint(item: pcbnew.BOARD_ITEM):
     return (roundCoord(item.GetPosition()[0]), # Round down, since the output does the same
             roundCoord(item.GetPosition()[1]), # Round down, since the output does the same
-            item.GetSelectMenuText(pcbnew.EDA_UNITS_MILLIMETRES))
+            getItemDescription(item))
 
 def collectFingerprints(board: pcbnew.BOARD) -> Dict[ItemFingerprint, pcbnew.BOARD_ITEM]:
     """
@@ -50,7 +57,7 @@ def collect(items: Iterable[pcbnew.BOARD_ITEM]) -> None:
 @dataclass
 class DrcExclusion:
     type: str
-    position: pcbnew.wxPoint
+    position: pcbnew.VECTOR2I
     objects: List[pcbnew.BOARD_ITEM] = field(default_factory=list)
 
     def eqRepr(self) -> Tuple[str, Union[Tuple[str, str], str]]:
@@ -80,7 +87,7 @@ def _formatObject(self, obj: pcbnew.BOARD_ITEM, units: Any) -> str:
             pos = f"{toMm(p[0]):.4f} mm, {toMm(p[1]):.4f} mm"
         if units == pcbnew.EDA_UNITS_INCHES:
             pos = f"{pcbnew.ToMils(p[0]):.1f} mil, {pcbnew.ToMils(p[1]):.1f} mil"
-        return f"@({pos}): {obj.GetSelectMenuText(units)}"
+        return f"@({pos}): {getItemDescription(obj, units)}"
 
     def eqRepr(self) -> Tuple[str, Union[Tuple[str, str], str]]:
         if len(self.objects) == 1:
@@ -200,7 +207,7 @@ def deserializeExclusion(exclusionText: str, board: pcbnew.BOARD) -> DrcExclusio
     objects = [board.GetItem(pcbnew.KIID(x)) for x in items[3:]]
     objects = [x for x in objects if x is not None]
     return DrcExclusion(items[0],
-                        pcbnew.wxPoint(int(items[1]), int(items[2])),
+                        pcbnew.VECTOR2I(int(items[1]), int(items[2])),
                         objects)
 
 def serializeExclusion(exclusion: DrcExclusion) -> str:
@@ -227,6 +234,10 @@ def readBoardDrcExclusions(board: pcbnew.BOARD) -> List[DrcExclusion]:
     return [deserializeExclusion(e, board) for e in exclusions]
 
 def runImpl(board, useMm, ignoreExcluded, strict, level, yieldViolation):
+    import faulthandler
+    import sys
+    faulthandler.enable(sys.stderr)
+
     units = pcbnew.EDA_UNITS_MILLIMETRES if useMm else EDA_UNITS_INCHES
     report = runBoardDrc(board, strict)
     if ignoreExcluded:
diff --git a/kikit/export.py b/kikit/export.py
index 77742ec..93059ee 100644
--- a/kikit/export.py
+++ b/kikit/export.py
@@ -4,6 +4,8 @@
 from pcbnewTransition import pcbnew
 from pcbnew import *
 
+from kikit.defs import Layer
+
 fullGerberPlotPlan = [
     # name, id, comment
     ("CuTop", F_Cu, "Top layer"),
@@ -56,6 +58,15 @@ def hasCopper(plotPlan):
             return True
     return False
 
+def setExcludeEdgeLayer(plotOptions, excludeEdge):
+    try:
+        plotOptions.SetExcludeEdgeLayer(excludeEdge)
+    except AttributeError:
+        if excludeEdge:
+            plotOptions.SetLayerSelection(LSET())
+        else:
+            plotOptions.SetLayerSelection(LSET(Layer.Edge_Cuts))
+
 def gerberImpl(boardfile, outputdir, plot_plan=fullGerberPlotPlan, drilling=True, settings=exportSettingsJlcpcb):
     """
     Export board to gerbers.
@@ -85,15 +96,14 @@ def gerberImpl(boardfile, outputdir, plot_plan=fullGerberPlotPlan, drilling=True
     popt.SetIncludeGerberNetlistInfo(True)
     popt.SetCreateGerberJobFile(True)
     popt.SetUseGerberProtelExtensions(settings["UseGerberProtelExtensions"])
-    popt.SetExcludeEdgeLayer(settings["ExcludeEdgeLayer"])
+    setExcludeEdgeLayer(popt, settings["ExcludeEdgeLayer"])
     popt.SetScale(1)
     popt.SetUseAuxOrigin(settings["UseAuxOrigin"])
     popt.SetUseGerberX2format(False)
-    popt.SetDrillMarksType(0) # NO_DRILL_SHAPE
 
     # This by gerbers only
     popt.SetSubtractMaskFromSilk(False)
-    popt.SetDrillMarksType(PCB_PLOT_PARAMS.NO_DRILL_SHAPE)
+    popt.SetDrillMarksType(pcbnew.DRILL_MARKS_NO_DRILL_SHAPE)
     popt.SetSkipPlotNPTH_Pads(False)
 
     # prepare the gerber job file
@@ -139,11 +149,12 @@ def gerberImpl(boardfile, outputdir, plot_plan=fullGerberPlotPlan, drilling=True
         if settings["UseAuxOrigin"]:
             offset = board.GetDesignSettings().GetAuxOrigin()
         else:
-            offset = wxPoint(0,0)
+            offset = VECTOR2I(0, 0)
 
         # False to generate 2 separate drill files (one for plated holes, one for non plated holes)
         # True to generate only one drill file
         mergeNPTH = settings["MergeNPTH"]
+        print(f"Type {offset}: {type(offset)}")
         drlwriter.SetOptions(mirror, minimalHeader, offset, mergeNPTH)
         drlwriter.SetRouteModeForOvalHoles(False)
 
@@ -170,7 +181,7 @@ def pasteDxfExport(board, plotDir):
     popt.SetAutoScale(False)
     popt.SetScale(1)
     popt.SetMirror(False)
-    popt.SetExcludeEdgeLayer(True)
+    setExcludeEdgeLayer(popt, True)
     popt.SetScale(1)
     popt.SetDXFPlotUnits(DXF_UNITS_MILLIMETERS)
     popt.SetDXFPlotPolygonMode(False)
diff --git a/kikit/fab/common.py b/kikit/fab/common.py
index 96cfa21..21e5ef7 100644
--- a/kikit/fab/common.py
+++ b/kikit/fab/common.py
@@ -80,14 +80,14 @@ def layerToSide(layer):
 
 def footprintPosition(footprint, placeOffset, compensation):
     pos = footprint.GetPosition() - placeOffset
-    angle = -radians(footprint.GetOrientation() / 10.0)
+    angle = -footprint.GetOrientation().AsRadians()
     x = compensation[0] * cos(angle) - compensation[1] * sin(angle)
     y = compensation[0] * sin(angle) + compensation[1] * cos(angle)
-    pos += wxPoint(fromMm(x), fromMm(y))
+    pos += VECTOR2I(fromMm(x), fromMm(y))
     return pos
 
 def footprintOrientation(footprint, compensation):
-    return (footprint.GetOrientation() / 10 + compensation[2]) % 360
+    return (footprint.GetOrientation().AsDegrees() + compensation[2]) % 360
 
 def parseCompensation(compensation):
     comps = [float(x) for x in compensation.split(";")]
diff --git a/kikit/panelize.py b/kikit/panelize.py
index 35a1f71..b8e9ea1 100644
--- a/kikit/panelize.py
+++ b/kikit/panelize.py
@@ -4,10 +4,11 @@
 from kikit import sexpr
 from kikit.common import normalize
 
+from pathlib import Path
+
 from typing import Any, Callable, Dict, Iterable, List, Set, Tuple, Union
 
-from pcbnew import (GetBoard, LoadBoard,
-                    FromMM, ToMM, wxPoint, wxRect, wxRectMM, wxPointMM)
+from pcbnew import (LoadBoard, ToMM, VECTOR2I, BOX2I, EDA_ANGLE)
 from enum import Enum
 from shapely.geometry import (Polygon, MultiPolygon, Point, LineString, box,
                               GeometryCollection, MultiLineString)
@@ -29,6 +30,7 @@
 from kikit.sexpr import parseSexprF, SExpr, Atom, findNode
 from kikit.annotations import AnnotationReader, TabAnnotation
 from kikit.drc import DrcExclusion, readBoardDrcExclusions, serializeExclusion
+from kikit.units import mm, deg
 
 class PanelError(RuntimeError):
     pass
@@ -40,7 +42,7 @@ def identity(x):
     return x
 
 class GridPlacerBase:
-    def position(self, i: int, j: int, boardSize: Optional[wxRect]) -> wxPoint:
+    def position(self, i: int, j: int, boardSize: Optional[BOX2I]) -> VECTOR2I:
         """
         Given row and col coords of a board, return physical physical position
         of the board. All function calls (except for 0, 0) also receive board
@@ -71,48 +73,48 @@ def __init__(self, horSpace: int, verSpace: int,
         self.hboneskip = hboneskip
         self.vboneskip = vboneskip
 
-    def position(self, i: int, j: int, boardSize: Optional[wxRect]) -> wxPoint:
+    def position(self, i: int, j: int, boardSize: Optional[BOX2I]) -> VECTOR2I:
         if boardSize is None:
             assert i == 0 and j == 0
-            return wxPoint(0, 0)
+            return VECTOR2I(0, 0)
         hbonecount = 0 if self.hbonewidth == 0 \
                        else i // (self.hboneskip + 1)
         vbonecount = 0 if self.vbonewidth == 0 \
                        else j // (self.vboneskip + 1)
-        return wxPoint(j * (boardSize.GetWidth() + self.horSpace) + \
+        return VECTOR2I(j * (boardSize.GetWidth() + self.horSpace) + \
                             vbonecount * (self.vbonewidth + self.horSpace),
                        i * (boardSize.GetHeight() + self.verSpace) + \
                             hbonecount * (self.hbonewidth + self.verSpace))
 
-    def rotation(self, i: int, j: int) -> int:
-        return 0
+    def rotation(self, i: int, j: int) -> KiAngle:
+        return EDA_ANGLE(0, pcbnew.DEGREES_T)
 
 class OddEvenRowsPosition(BasicGridPosition):
     """
     Rotate boards by 180° for every row
     """
-    def rotation(self, i: int, j: int) -> int:
+    def rotation(self, i: int, j: int) -> KiAngle:
         if i % 2 == 0:
-            return 0
-        return 1800
+            return EDA_ANGLE(0, pcbnew.DEGREES_T)
+        return EDA_ANGLE(180, pcbnew.DEGREES_T)
 
 class OddEvenColumnPosition(BasicGridPosition):
     """
     Rotate boards by 180° for every column
     """
-    def rotation(self, i: int, j: int) -> int:
+    def rotation(self, i: int, j: int) -> KiAngle:
         if j % 2 == 0:
-            return 0
-        return 1800
+            return EDA_ANGLE(0, pcbnew.DEGREES_T)
+        return EDA_ANGLE(180, pcbnew.DEGREES_T)
 
 class OddEvenRowsColumnsPosition(BasicGridPosition):
     """
     Rotate boards by 180 for every row and column
     """
-    def rotation(self, i: int, j: int) -> int:
+    def rotation(self, i: int, j: int) -> KiAngle:
         if (i % 2) == (j % 2):
             return 0
-        return 1800
+        return EDA_ANGLE(180, pcbnew.DEGREES_T)
 
 
 class Origin(Enum):
@@ -149,18 +151,18 @@ def serialize(self) -> Any:
         return data
 
 def getOriginCoord(origin, bBox):
-    """Returns real coordinates (wxPoint) of the origin for given bounding box"""
+    """Returns real coordinates (VECTOR2I) of the origin for given bounding box"""
     if origin == Origin.Center:
-        return wxPoint(bBox.GetX() + bBox.GetWidth() // 2,
-                       bBox.GetY() + bBox.GetHeight() // 2)
+        return VECTOR2I(bBox.GetX() + bBox.GetWidth() // 2,
+                        bBox.GetY() + bBox.GetHeight() // 2)
     if origin == Origin.TopLeft:
-        return wxPoint(bBox.GetX(), bBox.GetY())
+        return VECTOR2I(bBox.GetX(), bBox.GetY())
     if origin == Origin.TopRight:
-        return wxPoint(bBox.GetX() + bBox.GetWidth(), bBox.GetY())
+        return VECTOR2I(bBox.GetX() + bBox.GetWidth(), bBox.GetY())
     if origin == Origin.BottomLeft:
-        return wxPoint(bBox.GetX(), bBox.GetY() + bBox.GetHeight())
+        return VECTOR2I(bBox.GetX(), bBox.GetY() + bBox.GetHeight())
     if origin == Origin.BottomRight:
-        return wxPoint(bBox.GetX() + bBox.GetWidth(), bBox.GetY() + bBox.GetHeight())
+        return VECTOR2I(bBox.GetX() + bBox.GetWidth(), bBox.GetY() + bBox.GetHeight())
 
 def appendItem(board: pcbnew.BOARD, item: pcbnew.BOARD_ITEM,
                yieldMapping: Optional[Callable[[str, str], None]]=None) -> None:
@@ -197,13 +199,13 @@ def remapNets(collection, mapping):
     for item in collection:
         item.SetNetCode(mapping[item.GetNetname()].GetNetCode())
 
-ToPolygonGeometry = Union[Polygon, wxRect, Substrate]
+ToPolygonGeometry = Union[Polygon, BOX2I, Substrate]
 def toPolygon(entity: Union[List[ToPolygonGeometry], ToPolygonGeometry]) -> Polygon:
     if isinstance(entity, list):
         return list([toPolygon(e) for e in entity])
     if isinstance(entity, Polygon) or isinstance(entity, MultiPolygon):
         return entity
-    if isinstance(entity, wxRect):
+    if isinstance(entity, BOX2I):
         return Polygon([
             (entity.GetX(), entity.GetY()),
             (entity.GetX() + entity.GetWidth(), entity.GetY()),
@@ -218,17 +220,18 @@ def rectString(rect):
                 ToMM(rect.GetX()), ToMM(rect.GetY()),
                 ToMM(rect.GetWidth()), ToMM(rect.GetHeight()))
 
-def expandRect(rect: wxRect, offsetX: KiLength, offsetY: Optional[KiLength]=None):
+def expandRect(rect: BOX2I, offsetX: KiLength, offsetY: Optional[KiLength]=None):
     """
-    Given a wxRect returns a new rectangle, which is larger in all directions
+    Given a BOX2I returns a new rectangle, which is larger in all directions
     by offset. If only offsetX is passed, it used for both X and Y offset
     """
     if offsetY is None:
         offsetY = offsetX
     offsetX = int(offsetX)
     offsetY = int(offsetY)
-    return wxRect(rect.GetX() - offsetX, rect.GetY() - offsetY,
-        rect.GetWidth() + 2 * offsetX, rect.GetHeight() + 2 * offsetY)
+    return BOX2I(
+            VECTOR2I(rect.GetX() - offsetX, rect.GetY() - offsetY),
+            VECTOR2I(rect.GetWidth() + 2 * offsetX, rect.GetHeight() + 2 * offsetY))
 
 def rectToRing(rect):
     return [
@@ -243,18 +246,18 @@ def roundPoint(point, precision=-4):
         return Point(round(point.x, precision), round(point.y, precision))
     return Point(round(point[0], precision), round(point[1], precision))
 
-def doTransformation(point: KiKitPoint, rotation: KiAngle, origin: KiKitPoint, translation: KiKitPoint) -> wxPoint:
+def doTransformation(point: KiPoint, rotation: KiAngle, origin: KiPoint, translation: KiPoint) -> VECTOR2I:
     """
     Abuses KiCAD to perform a tranformation of a point
     """
     segment = pcbnew.PCB_SHAPE()
     segment.SetShape(STROKE_T.S_SEGMENT)
-    segment.SetStart(wxPoint(int(point[0]), int(point[1])))
-    segment.SetEnd(wxPoint(0, 0))
-    segment.Rotate(wxPoint(int(origin[0]), int(origin[1])), -rotation)
-    segment.Move(wxPoint(translation[0], translation[1]))
-    # We build a fresh wxPoint - otherwise there is a shared reference
-    return wxPoint(segment.GetStartX(), segment.GetStartY())
+    segment.SetStart(toKiCADPoint(point))
+    segment.SetEnd(VECTOR2I(0, 0))
+    segment.Rotate(toKiCADPoint(origin), -rotation)
+    segment.Move(toKiCADPoint(translation))
+    # We build a fresh VECTOR2I - otherwise there is a shared reference
+    return VECTOR2I(segment.GetStartX(), segment.GetStartY())
 
 def undoTransformation(point, rotation, origin, translation):
     """
@@ -265,12 +268,12 @@ def undoTransformation(point, rotation, origin, translation):
     # Abuse PcbNew to do so
     segment = pcbnew.PCB_SHAPE()
     segment.SetShape(STROKE_T.S_SEGMENT)
-    segment.SetStart(wxPoint(int(point[0]), int(point[1])))
-    segment.SetEnd(wxPoint(0, 0))
-    segment.Move(wxPoint(-translation[0], -translation[1]))
+    segment.SetStart(VECTOR2I(int(point[0]), int(point[1])))
+    segment.SetEnd(VECTOR2I(0, 0))
+    segment.Move(VECTOR2I(-translation[0], -translation[1]))
     segment.Rotate(origin, -rotation)
-    # We build a fresh wxPoint - otherwise there is a shared reference
-    return wxPoint(segment.GetStartX(), segment.GetStartY())
+    # We build a fresh VECTOR2I - otherwise there is a shared reference
+    return VECTOR2I(segment.GetStartX(), segment.GetStartY())
 
 def removeCutsFromFootprint(footprint):
     """
@@ -330,7 +333,7 @@ def isBoardEdge(edge):
 
 def increaseZonePriorities(board, amount=1):
     for zone in board.Zones():
-        zone.SetPriority(zone.GetPriority() + amount)
+        zone.SetAssignedPriority(zone.GetAssignedPriority() + amount)
 
 def tabSpacing(width, count):
     """
@@ -796,16 +799,21 @@ def setTitleBlock(self, titleBlock):
         """
         self.board.SetTitleBlock(titleBlock)
 
-    def appendBoard(self, filename, destination, sourceArea=None,
-                    origin=Origin.Center, rotationAngle=0, shrink=False,
-                    tolerance=0, bufferOutline=fromMm(0.001), netRenamer=None,
-                    refRenamer=None, inheritDrc=True, interpretAnnotations=True,
-                    bakeText=False):
+    def appendBoard(self, filename: Union[str, Path], destination: VECTOR2I,
+                    sourceArea: Optional[BOX2I] = None,
+                    origin: Origin = Origin.Center,
+                    rotationAngle: KiAngle = fromDegrees(0),
+                    shrink: bool = False, tolerance: KiLength = 0,
+                    bufferOutline: KiLength = fromMm(0.001),
+                    netRenamer: Optional[Callable[[int, str], str]] = None,
+                    refRenamer: Optional[Callable[[int, str], str]] = None,
+                    inheritDrc: bool = True, interpretAnnotations: bool=True,
+                    bakeText: bool = False):
         """
         Appends a board to the panel.
 
-        The sourceArea (wxRect) of the board specified by filename is extracted
-        and placed at destination (wxPoint). The source area (wxRect) can be
+        The sourceArea (BOX2I) of the board specified by filename is extracted
+        and placed at destination (VECTOR2I). The source area (BOX2I) can be
         auto detected if it is not provided. Only board items which fit entirely
         into the source area are selected. You can also specify rotation. Both
         translation and rotation origin are specified by origin. Origin
@@ -825,10 +833,17 @@ def appendBoard(self, filename, destination, sourceArea=None,
 
         Similarly, you can substitute variables in the text via bakeText.
 
-        Returns bounding box (wxRect) of the extracted area placed at the
+        Returns bounding box (BOX2I) of the extracted area placed at the
         destination and the extracted substrate of the board.
         """
-        board = LoadBoard(filename)
+        # Since we want to follow KiCAD's new API, we require angles to be given
+        # as EDA_ANGLE. However, there might be old scripts that will pass a
+        # number.
+        if not isinstance(rotationAngle, EDA_ANGLE):
+            raise RuntimeError("Board rotation has to be passed as EDA_ANGLE, not a number")
+
+
+        board = LoadBoard(str(filename))
         if inheritDrc:
             self.sourcePaths.add(filename)
         if bakeText:
@@ -851,7 +866,7 @@ def appendBoard(self, filename, destination, sourceArea=None,
             sourceArea = findBoardBoundingBox(board, sourceArea)
         enlargedSourceArea = expandRect(sourceArea, tolerance)
         originPoint = getOriginCoord(origin, sourceArea)
-        translation = wxPoint(destination[0] - originPoint[0],
+        translation = VECTOR2I(destination[0] - originPoint[0],
                               destination[1] - originPoint[1])
 
         if netRenamer is None:
@@ -966,7 +981,7 @@ def _readProjectVariables(self, board: pcbnew.BOARD) -> Dict[str, str]:
     def appendSubstrate(self, substrate: ToPolygonGeometry) -> None:
         """
         Append a piece of substrate or a list of pieces to the panel. Substrate
-        can be either wxRect or Shapely polygon. Newly appended corners can be
+        can be either BOX2I or Shapely polygon. Newly appended corners can be
         rounded by specifying non-zero filletRadius.
         """
         polygon = toPolygon(substrate)
@@ -1017,13 +1032,13 @@ def setVCutClearance(self, clearance):
     def _setVCutSegmentStyle(self, segment, layer):
         segment.SetShape(STROKE_T.S_SEGMENT)
         segment.SetLayer(layer)
-        segment.SetWidth(fromMm(0.4))
+        segment.SetWidth(int(0.4 * mm))
 
     def _setVCutLabelStyle(self, label, layer):
         label.SetText("V-CUT")
         label.SetLayer(layer)
-        label.SetTextThickness(fromMm(0.4))
-        label.SetTextSize(pcbnew.wxSizeMM(2, 2))
+        label.SetTextThickness(int(0.4 * mm))
+        label.SetTextSize(toKiCADPoint((2 * mm, 2 * mm)))
         label.SetHorizJustify(EDA_TEXT_HJUSTIFY_T.GR_TEXT_HJUSTIFY_LEFT)
 
     def _renderVCutV(self):
@@ -1034,8 +1049,8 @@ def _renderVCutV(self):
         for cut in self.vVCuts:
             segment = pcbnew.PCB_SHAPE()
             self._setVCutSegmentStyle(segment, self.vCutLayer)
-            segment.SetStart(pcbnew.wxPoint(cut, minY))
-            segment.SetEnd(pcbnew.wxPoint(cut, maxY))
+            segment.SetStart(toKiCADPoint((cut, minY)))
+            segment.SetEnd(toKiCADPoint((cut, maxY)))
 
             keepout = None
             if self.vCutClearance != 0:
@@ -1048,8 +1063,8 @@ def _renderVCutV(self):
 
             label = pcbnew.PCB_TEXT(segment)
             self._setVCutLabelStyle(label, self.vCutLayer)
-            label.SetPosition(wxPoint(cut, minY - fromMm(3)))
-            label.SetTextAngle(900)
+            label.SetPosition(toKiCADPoint((cut, minY - fromMm(3))))
+            label.SetTextAngle(fromDegrees(90))
             segments.append((label, None))
         return segments
 
@@ -1061,8 +1076,8 @@ def _renderVCutH(self):
         for cut in self.hVCuts:
             segment = pcbnew.PCB_SHAPE()
             self._setVCutSegmentStyle(segment, self.vCutLayer)
-            segment.SetStart(pcbnew.wxPoint(minX, cut))
-            segment.SetEnd(pcbnew.wxPoint(maxX, cut))
+            segment.SetStart(toKiCADPoint((minX, cut)))
+            segment.SetEnd(toKiCADPoint((maxX, cut)))
 
             keepout = None
             if self.vCutClearance != 0:
@@ -1076,13 +1091,13 @@ def _renderVCutH(self):
 
             label = pcbnew.PCB_TEXT(segment)
             self._setVCutLabelStyle(label, self.vCutLayer)
-            label.SetPosition(wxPoint(maxX + fromMm(3), cut))
+            label.SetPosition(toKiCADPoint((maxX + fromMm(3), cut)))
             segments.append((label, None))
         return segments
 
-    def makeGrid(self, boardfile: str, sourceArea: wxRect, rows: int, cols: int,
-                 destination: wxPoint, placer: GridPlacerBase,
-                 rotation: KiAngle=0, netRenamePattern: str="Board_{n}-{orig}",
+    def makeGrid(self, boardfile: str, sourceArea: BOX2I, rows: int, cols: int,
+                 destination: VECTOR2I, placer: GridPlacerBase,
+                 rotation: KiAngle=fromDegrees(0), netRenamePattern: str="Board_{n}-{orig}",
                  refRenamePattern: str="Board_{n}-{orig}", tolerance: KiLength=0,
                  bakeText: bool=False) \
                      -> List[Substrate]:
@@ -1105,7 +1120,7 @@ def makeGrid(self, boardfile: str, sourceArea: wxRect, rows: int, cols: int,
         cols - the number of boards to place in the horizontal direction
 
         destination - the center coordinates of the first board in the grid (for
-        example, wxPointMM(100,50))
+        example, VECTOR2I(100 * mm,50 * mm))
 
         rotation - the rotation angle to be applied to the source board before
         placing it
@@ -1142,6 +1157,8 @@ def makeGrid(self, boardfile: str, sourceArea: wxRect, rows: int, cols: int,
         Returns a list of the placed substrates. You can use these to generate
         tabs, frames, backbones, etc.
         """
+        if not isinstance(rotation, EDA_ANGLE):
+            raise RuntimeError(f"Rotation values have to be passed as `EDA_ANGLE` not {type(rotation)}")
         substrateCount = len(self.substrates)
         netRenamer = lambda x, y: netRenamePattern.format(n=x, orig=y)
         refRenamer = lambda x, y: refRenamePattern.format(n=x, orig=y)
@@ -1354,7 +1371,7 @@ def makeMouseBites(self, cuts, diameter, spacing, offset=fromMm(0.25),
                 else:
                     hole = offsetCut.interpolate( i * length / (count - 1) )
                 if bloatedSubstrate.intersects(hole):
-                    self.addNPTHole(wxPoint(hole.x, hole.y), diameter)
+                    self.addNPTHole(toKiCADPoint((hole.x, hole.y)), diameter)
 
     def makeCutsToLayer(self, cuts, layer=Layer.Cmts_User, prolongation=fromMm(0)):
         """
@@ -1371,21 +1388,21 @@ def makeCutsToLayer(self, cuts, layer=Layer.Cmts_User, prolongation=fromMm(0)):
                 segment = pcbnew.PCB_SHAPE()
                 segment.SetShape(STROKE_T.S_SEGMENT)
                 segment.SetLayer(layer)
-                segment.SetStart(wxPoint(*a))
-                segment.SetEnd(wxPoint(*b))
+                segment.SetStart(toKiCADPoint(a))
+                segment.SetEnd(toKiCADPoint(b))
                 segment.SetWidth(fromMm(0.3))
                 self.board.Add(segment)
 
-    def addNPTHole(self, position, diameter, paste=False):
+    def addNPTHole(self, position: VECTOR2I, diameter: KiLength, paste: bool=False) -> None:
         """
-        Add a drilled non-plated hole to the position (`wxPoint`) with given
+        Add a drilled non-plated hole to the position (`VECTOR2I`) with given
         diameter. The paste option allows to place the hole on the paste layers.
         """
         footprint = pcbnew.FootprintLoad(KIKIT_LIB, "NPTH")
         footprint.SetPosition(position)
         for pad in footprint.Pads():
-            pad.SetDrillSize(pcbnew.wxSize(diameter, diameter))
-            pad.SetSize(pcbnew.wxSize(diameter, diameter))
+            pad.SetDrillSize(toKiCADPoint((diameter, diameter)))
+            pad.SetSize(toKiCADPoint((diameter, diameter)))
             if paste:
                 layerSet = pad.GetLayerSet()
                 layerSet.AddLayer(Layer.F_Paste)
@@ -1393,9 +1410,10 @@ def addNPTHole(self, position, diameter, paste=False):
                 pad.SetLayerSet(layerSet)
         self.board.Add(footprint)
 
-    def addFiducial(self, position, copperDiameter, openingDiameter, bottom=False):
+    def addFiducial(self, position: VECTOR2I, copperDiameter: KiLength,
+                    openingDiameter: KiLength, bottom: bool=False):
         """
-        Add fiducial, i.e round copper pad with solder mask opening to the position (`wxPoint`),
+        Add fiducial, i.e round copper pad with solder mask opening to the position (`VECTOR2I`),
         with given copperDiameter and openingDiameter. By setting bottom to True, the fiducial
         is placed on bottom side.
         """
@@ -1411,7 +1429,7 @@ def addFiducial(self, position, copperDiameter, openingDiameter, bottom=False):
             else:
                 footprint.Flip(position)
         for pad in footprint.Pads():
-            pad.SetSize(pcbnew.wxSize(copperDiameter, copperDiameter))
+            pad.SetSize(toKiCADPoint((copperDiameter, copperDiameter)))
             pad.SetLocalSolderMaskMargin(int((openingDiameter - copperDiameter) / 2))
             pad.SetLocalClearance(int((openingDiameter - copperDiameter) / 2))
 
@@ -1422,10 +1440,10 @@ def panelCorners(self, horizontalOffset=0, verticalOffset=0):
         corners of the panel. You can specify offsets.
         """
         minx, miny, maxx, maxy = self.panelBBox()
-        topLeft = wxPoint(minx + horizontalOffset, miny + verticalOffset)
-        topRight = wxPoint(maxx - horizontalOffset, miny + verticalOffset)
-        bottomLeft = wxPoint(minx + horizontalOffset, maxy - verticalOffset)
-        bottomRight = wxPoint(maxx - horizontalOffset, maxy - verticalOffset)
+        topLeft = toKiCADPoint((minx + horizontalOffset, miny + verticalOffset))
+        topRight = toKiCADPoint((maxx - horizontalOffset, miny + verticalOffset))
+        bottomLeft = toKiCADPoint((minx + horizontalOffset, maxy - verticalOffset))
+        bottomRight = toKiCADPoint((maxx - horizontalOffset, maxy - verticalOffset))
         return [topLeft, topRight, bottomLeft, bottomRight]
 
     def addCornerFiducials(self, fidCount, horizontalOffset, verticalOffset,
@@ -1583,7 +1601,7 @@ def buildTabAnnotationsCorners(self, width):
                 a = TabAnnotation(None, (x, y), dir, width)
                 self.substrates[i].annotations.append(a)
 
-    def _buildSingleFullTab(self, s: Substrate, a: KiKitPoint, b: KiKitPoint,
+    def _buildSingleFullTab(self, s: Substrate, a: KiPoint, b: KiPoint,
                             cutoutDepth: KiLength) \
             -> Tuple[List[LineString], List[Polygon]]:
         partitionFace = LineString([a, b])
@@ -1717,7 +1735,7 @@ def copperFillNonBoardAreas(self, clearance: KiLength=fromMm(1),
             zoneContainer.Outline().AddOutline(linestringToKicad(g.exterior))
             for hole in g.interiors:
                 zoneContainer.Outline().AddHole(linestringToKicad(hole))
-            zoneContainer.SetPriority(0)
+            zoneContainer.SetAssignedPriority(0)
 
             for l in layers:
                 if not self.board.GetEnabledLayers().Contains(l):
@@ -1773,7 +1791,7 @@ def addText(self, text, position, orientation=0,
         textObject.SetTextX(position[0])
         textObject.SetTextY(position[1])
         textObject.SetTextThickness(thickness)
-        textObject.SetTextSize(pcbnew.wxSize(width, height))
+        textObject.SetTextSize(toKiCADPoint((width, height)))
         textObject.SetHorizJustify(hJustify)
         textObject.SetVertJustify(vJustify)
         textObject.SetTextAngle(orientation)
@@ -1869,8 +1887,8 @@ def addLine(self, start, end, thickness, layer):
         segment.SetShape(STROKE_T.S_SEGMENT)
         segment.SetLayer(layer)
         segment.SetWidth(thickness)
-        segment.SetStart(pcbnew.wxPoint(start[0], start[1]))
-        segment.SetEnd(pcbnew.wxPoint(end[0], end[1]))
+        segment.SetStart(toKiCADPoint((start[0], start[1])))
+        segment.SetEnd(toKiCADPoint((end[0], end[1])))
         self.board.Add(segment)
         return segment
 
@@ -2024,7 +2042,7 @@ def translate(self, vec):
         specify the panel placement in the sheet. When we translate panel as the
         last operation, none of the operations have to be placement-aware.
         """
-        vec = wxPoint(vec[0], vec[1])
+        vec = toKiCADPoint(vec)
         for drawing in self.board.GetDrawings():
             drawing.Move(vec)
         for footprint in self.board.GetFootprints():
diff --git a/kikit/panelize_ui.py b/kikit/panelize_ui.py
index 132f9ad..ee87b4c 100644
--- a/kikit/panelize_ui.py
+++ b/kikit/panelize_ui.py
@@ -326,8 +326,9 @@ def separate(input, output, source, page, debug, keepannotations, preservearcs):
     try:
         from kikit import panelize_ui_impl as ki
         from kikit.panelize import Panel
+        from kikit.units import mm
         from pcbnewTransition.transition import isV6, pcbnew
-        from pcbnew import LoadBoard, wxPointMM
+        from pcbnew import LoadBoard, VECTOR2I
         from kikit.common import fakeKiCADGui
         app = fakeKiCADGui()
 
@@ -344,7 +345,7 @@ def separate(input, output, source, page, debug, keepannotations, preservearcs):
         panel.inheritProperties(board)
         panel.inheritTitleBlock(board)
 
-        destination = wxPointMM(150, 100)
+        destination = VECTOR2I(150 * mm, 100 * mm)
         panel.appendBoard(input, destination, sourceArea,
             interpretAnnotations=(not keepannotations))
         ki.setStackup(preset["source"], panel)
diff --git a/kikit/panelize_ui_impl.py b/kikit/panelize_ui_impl.py
index b0e0d10..1d8d187 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, PercentageValue
+from kikit.units import BaseValue, BaseAngle, PercentageValue
 from kikit.panelize_ui_sections import *
 from kikit.substrate import SubstrateNeighbors
 from kikit.common import resolveAnchor
@@ -31,7 +31,7 @@ def encodePreset(value):
         return getattr(value, "__kikit_preset_repr")
     if value is None:
         return "none"
-    if isinstance(value, BaseValue):
+    if isinstance(value, BaseValue) or isinstance(value, BaseAngle):
         return str(value)
     if isinstance(value, PercentageValue):
         return str(value)
@@ -188,9 +188,9 @@ def readSourceArea(specification, board):
             ref = specification["ref"]
             return expandRect(extractSourceAreaByAnnotation(board, ref), tolerance)
         if type == "rectangle":
-            tl = wxPoint(specification["tlx"], specification["tly"])
-            br = wxPoint(specification["brx"], specification["bry"])
-            return expandRect(wxRect(tl, br), tolerance)
+            tl = VECTOR2I(specification["tlx"], specification["tly"])
+            br = VECTOR2I(specification["brx"], specification["bry"])
+            return expandRect(BOX2I(tl, (br - tl)), tolerance)
         raise PresetError(f"Unknown type '{type}' of source specification.")
     except KeyError as e:
         raise PresetError(f"Missing parameter '{e}' in section 'source'")
@@ -251,7 +251,7 @@ def buildLayout(preset, panel, sourceBoard, sourceArea):
                 vboneskip=layout["vboneskip"])
             substrates = panel.makeGrid(
                 boardfile=sourceBoard, sourceArea=sourceArea,
-                rows=layout["rows"], cols=layout["cols"], destination=wxPointMM(0, 0),
+                rows=layout["rows"], cols=layout["cols"], destination=VECTOR2I(0, 0),
                 rotation=layout["rotation"], placer=placer,
                 netRenamePattern=layout["renamenet"], refRenamePattern=layout["renameref"],
                 bakeText=layout["baketext"])
@@ -549,7 +549,7 @@ def buildText(preset, panel):
             raise RuntimeError(f"Unknown variable {e} in text:\n{preset['text']}") from None
         if type == "simple":
             origin = resolveAnchor(preset["anchor"])(panel.boardSubstrate.boundingBox())
-            origin += wxPoint(preset["hoffset"], preset["voffset"])
+            origin += VECTOR2I(preset["hoffset"], preset["voffset"])
 
             panel.addText(
                 text=text,
@@ -609,7 +609,8 @@ def buildPostprocessing(preset, panel):
         if preset["copperfill"]:
             panel.copperFillNonBoardAreas()
         if preset["origin"]:
-            origin = resolveAnchor(preset["origin"])(panel.boardSubstrate.boundingBox())
+            bBox = panel.boardSubstrate.boundingBox()
+            origin = resolveAnchor(preset["origin"])(bBox)
             panel.setAuxiliaryOrigin(origin)
             panel.setGridOrigin(origin)
         if preset["dimensions"]:
diff --git a/kikit/plugin.py b/kikit/plugin.py
index 760338b..d53fb43 100644
--- a/kikit/plugin.py
+++ b/kikit/plugin.py
@@ -102,7 +102,7 @@ def __init__(self, preset: Preset, userArg: str, netPattern: str,
         self.rotation = rotation
 
     def buildLayout(self, panel: Panel, inputFile: str,
-                    sourceArea: pcbnew.wxRect) -> Iterable[Substrate]:
+                    sourceArea: pcbnew.BOX2I) -> Iterable[Substrate]:
         """
         This function is supposed to build the layout (append the boards to the
         panel) and return an iterable of substrates of these boards.
diff --git a/kikit/stencil.py b/kikit/stencil.py
index 3b769ee..259318f 100644
--- a/kikit/stencil.py
+++ b/kikit/stencil.py
@@ -1,5 +1,5 @@
 from pcbnewTransition import pcbnew
-from pcbnew import wxPoint
+from pcbnew import VECTOR2I
 import numpy as np
 import json
 from collections import OrderedDict
@@ -32,11 +32,11 @@ def addBottomCounterpart(board, item):
 def addRoundedCorner(board, center, start, end, thickness):
     corner = pcbnew.PCB_SHAPE()
     corner.SetShape(STROKE_T.S_ARC)
-    corner.SetCenter(wxPoint(center[0], center[1]))
+    corner.SetCenter(VECTOR2I(center[0], center[1]))
     if isV6():
-        corner.SetStart(wxPoint(start[0], start[1]))
+        corner.SetStart(VECTOR2I(start[0], start[1]))
     else:
-        corner.SetArcStart(wxPoint(start[0], start[1]))
+        corner.SetArcStart(VECTOR2I(start[0], start[1]))
 
     if np.cross(start - center, end - center) > 0:
         if isV6():
@@ -56,8 +56,8 @@ def addRoundedCorner(board, center, start, end, thickness):
 def addLine(board, start, end, thickness):
     line = pcbnew.PCB_SHAPE()
     line.SetShape(STROKE_T.S_SEGMENT)
-    line.SetStart(wxPoint(start[0], start[1]))
-    line.SetEnd(wxPoint(end[0], end[1]))
+    line.SetStart(VECTOR2I(start[0], start[1]))
+    line.SetEnd(VECTOR2I(end[0], end[1]))
     line.SetWidth(thickness)
     line.SetLayer(Layer.F_Paste)
     board.Add(line)
@@ -70,9 +70,9 @@ def addBite(board, origin, direction, normal, thickness):
     """
     direction = normalize(direction) * thickness
     normal = normalize(normal) * thickness
-    center = wxPoint(origin[0], origin[1]) + wxPoint(normal[0], normal[1])
+    center = VECTOR2I(origin[0], origin[1]) + VECTOR2I(normal[0], normal[1])
     start = origin
-    end = center + wxPoint(direction[0], direction[1])
+    end = center + VECTOR2I(direction[0], direction[1])
     # addLine(board, end, end + normal / 2, thickness)
     addRoundedCorner(board, center, start, end, thickness)
 
@@ -91,10 +91,10 @@ def addFrame(board, rect, bridgeWidth, bridgeSpacing, clearance):
     R=fromMm(1)
 
     corners = [
-        (tl(rect), wxPoint(R, 0), wxPoint(0, R)), # TL
-        (tr(rect), wxPoint(0, R), wxPoint(-R, 0)), # TR
-        (br(rect), wxPoint(-R, 0), wxPoint(0, -R)), # BR
-        (bl(rect), wxPoint(0, -R), wxPoint(R, 0)) # BL
+        (tl(rect), VECTOR2I(R, 0), VECTOR2I(0, R)), # TL
+        (tr(rect), VECTOR2I(0, R), VECTOR2I(-R, 0)), # TR
+        (br(rect), VECTOR2I(-R, 0), VECTOR2I(0, -R)), # BR
+        (bl(rect), VECTOR2I(0, -R), VECTOR2I(R, 0)) # BL
     ]
     for c, sOffset, eOffset in corners:
         addRoundedCorner(board, c + sOffset + eOffset, c + sOffset, c + eOffset, clearance)
@@ -105,16 +105,16 @@ def addFrame(board, rect, bridgeWidth, bridgeSpacing, clearance):
         end = start + cutLength
 
         y1, y2 = rect.GetY(), rect.GetY() + rect.GetHeight()
-        addLine(board, wxPoint(start, y1), wxPoint(end, y1), clearance)
+        addLine(board, VECTOR2I(start, y1), VECTOR2I(end, y1), clearance)
         if i != 0:
-            addBite(board, wxPoint(start, y1), wxPoint(-1, 0), wxPoint(0, 1), clearance)
+            addBite(board, VECTOR2I(start, y1), VECTOR2I(-1, 0), VECTOR2I(0, 1), clearance)
         if i != count - 1:
-            addBite(board, wxPoint(end, y1), wxPoint(1, 0), wxPoint(0, 1), clearance)
-        addLine(board, wxPoint(start, y2), wxPoint(end, y2), clearance)
+            addBite(board, VECTOR2I(end, y1), VECTOR2I(1, 0), VECTOR2I(0, 1), clearance)
+        addLine(board, VECTOR2I(start, y2), VECTOR2I(end, y2), clearance)
         if i != 0:
-            addBite(board, wxPoint(start, y2), wxPoint(-1, 0), wxPoint(0, -1), clearance)
+            addBite(board, VECTOR2I(start, y2), VECTOR2I(-1, 0), VECTOR2I(0, -1), clearance)
         if i != count - 1:
-            addBite(board, wxPoint(end, y2), wxPoint(1, 0), wxPoint(0, -1), clearance)
+            addBite(board, VECTOR2I(end, y2), VECTOR2I(1, 0), VECTOR2I(0, -1), clearance)
 
     count, cutLength = numberOfCuts(rect.GetHeight() - 2 * R, bridgeWidth, bridgeSpacing)
     for i in range(count):
@@ -122,26 +122,26 @@ def addFrame(board, rect, bridgeWidth, bridgeSpacing, clearance):
         end = start + cutLength
 
         x1, x2 = rect.GetX(), rect.GetX() + rect.GetWidth()
-        addLine(board, wxPoint(x1, start), wxPoint(x1, end), clearance)
+        addLine(board, VECTOR2I(x1, start), VECTOR2I(x1, end), clearance)
         if i != 0:
-            addBite(board, wxPoint(x1, start), wxPoint(0, -1), wxPoint(1, 0), clearance)
+            addBite(board, VECTOR2I(x1, start), VECTOR2I(0, -1), VECTOR2I(1, 0), clearance)
         if i != count - 1:
-            addBite(board, wxPoint(x1, end), wxPoint(0, 1), wxPoint(1, 0), clearance)
-        addLine(board, wxPoint(x2, start), wxPoint(x2, end), clearance)
+            addBite(board, VECTOR2I(x1, end), VECTOR2I(0, 1), VECTOR2I(1, 0), clearance)
+        addLine(board, VECTOR2I(x2, start), VECTOR2I(x2, end), clearance)
         if i != 0:
-            addBite(board, wxPoint(x2, start), wxPoint(0, -1), wxPoint(-1, 0), clearance)
+            addBite(board, VECTOR2I(x2, start), VECTOR2I(0, -1), VECTOR2I(-1, 0), clearance)
         if i != count - 1:
-            addBite(board, wxPoint(x2, end), wxPoint(0, 1), wxPoint(-1, 0), clearance)
+            addBite(board, VECTOR2I(x2, end), VECTOR2I(0, 1), VECTOR2I(-1, 0), clearance)
 
 def addHole(board, position, radius):
     circle = pcbnew.PCB_SHAPE()
     circle.SetShape(STROKE_T.S_CIRCLE)
-    circle.SetCenter(wxPoint(position[0], position[1]))
+    circle.SetCenter(VECTOR2I(position[0], position[1]))
     if isV6():
         # Set 3'oclock point of the circle to set radius
-        circle.SetEnd(wxPoint(position[0], position[1]) + wxPoint(radius/2, 0))
+        circle.SetEnd(VECTOR2I(position[0], position[1]) + VECTOR2I(radius/2, 0))
     else:
-        circle.SetArcStart(wxPoint(position[0], position[1]) + wxPoint(radius/2, 0))
+        circle.SetArcStart(VECTOR2I(position[0], position[1]) + VECTOR2I(radius/2, 0))
     circle.SetWidth(radius)
     circle.SetLayer(Layer.F_Paste)
     board.Add(circle)
@@ -168,27 +168,27 @@ def addJigFrame(board, jigFrameSize, bridgeWidth=fromMm(2),
 
     for i in range(MOUNTING_HOLES_COUNT):
         x = frameSize.GetX() + OUTER_BORDER / 2 + (i + 1) * (frameSize.GetWidth() - OUTER_BORDER) / (MOUNTING_HOLES_COUNT + 1)
-        addHole(board, wxPoint(x, OUTER_BORDER / 2 + frameSize.GetY()), MOUNTING_HOLE_R)
-        addHole(board, wxPoint(x, - OUTER_BORDER / 2 +frameSize.GetY() + frameSize.GetHeight()), MOUNTING_HOLE_R)
+        addHole(board, VECTOR2I(x, OUTER_BORDER / 2 + frameSize.GetY()), MOUNTING_HOLE_R)
+        addHole(board, VECTOR2I(x, - OUTER_BORDER / 2 +frameSize.GetY() + frameSize.GetHeight()), MOUNTING_HOLE_R)
     for i in range(MOUNTING_HOLES_COUNT):
         y = frameSize.GetY() + OUTER_BORDER / 2 + (i + 1) * (frameSize.GetHeight() - OUTER_BORDER) / (MOUNTING_HOLES_COUNT + 1)
-        addHole(board, wxPoint(OUTER_BORDER / 2 + frameSize.GetX(), y), MOUNTING_HOLE_R)
-        addHole(board, wxPoint(- OUTER_BORDER / 2 +frameSize.GetX() + frameSize.GetWidth(), y), MOUNTING_HOLE_R)
+        addHole(board, VECTOR2I(OUTER_BORDER / 2 + frameSize.GetX(), y), MOUNTING_HOLE_R)
+        addHole(board, VECTOR2I(- OUTER_BORDER / 2 +frameSize.GetX() + frameSize.GetWidth(), y), MOUNTING_HOLE_R)
 
     PIN_TOLERANCE = fromMm(0.05)
-    addHole(board, tl(frameSize) + wxPoint(OUTER_BORDER / 2, OUTER_BORDER / 2), MOUNTING_HOLE_R + PIN_TOLERANCE)
-    addHole(board, tr(frameSize) + wxPoint(-OUTER_BORDER / 2, OUTER_BORDER / 2), MOUNTING_HOLE_R + PIN_TOLERANCE)
-    addHole(board, br(frameSize) + wxPoint(-OUTER_BORDER / 2, -OUTER_BORDER / 2), MOUNTING_HOLE_R + PIN_TOLERANCE)
-    addHole(board, bl(frameSize) + wxPoint(OUTER_BORDER / 2, -OUTER_BORDER / 2), MOUNTING_HOLE_R + PIN_TOLERANCE)
+    addHole(board, tl(frameSize) + VECTOR2I(OUTER_BORDER / 2, OUTER_BORDER / 2), MOUNTING_HOLE_R + PIN_TOLERANCE)
+    addHole(board, tr(frameSize) + VECTOR2I(-OUTER_BORDER / 2, OUTER_BORDER / 2), MOUNTING_HOLE_R + PIN_TOLERANCE)
+    addHole(board, br(frameSize) + VECTOR2I(-OUTER_BORDER / 2, -OUTER_BORDER / 2), MOUNTING_HOLE_R + PIN_TOLERANCE)
+    addHole(board, bl(frameSize) + VECTOR2I(OUTER_BORDER / 2, -OUTER_BORDER / 2), MOUNTING_HOLE_R + PIN_TOLERANCE)
 
-def jigMountingHoles(jigFrameSize, origin=wxPoint(0, 0)):
+def jigMountingHoles(jigFrameSize, origin=VECTOR2I(0, 0)):
     """ Get list of all mounting holes in a jig of given size """
     w, h = jigFrameSize
     holes = [
-        wxPoint(0, (w + INNER_BORDER) / 2),
-        wxPoint(0, -(w + INNER_BORDER) / 2),
-        wxPoint((h + INNER_BORDER) / 2, 0),
-        wxPoint(-(h + INNER_BORDER) / 2, 0),
+        VECTOR2I(0, (w + INNER_BORDER) / 2),
+        VECTOR2I(0, -(w + INNER_BORDER) / 2),
+        VECTOR2I((h + INNER_BORDER) / 2, 0),
+        VECTOR2I(-(h + INNER_BORDER) / 2, 0),
     ]
     return [x + origin for x in holes]
 
diff --git a/kikit/substrate.py b/kikit/substrate.py
index 6e5bfac..a7f5bc9 100644
--- a/kikit/substrate.py
+++ b/kikit/substrate.py
@@ -14,7 +14,7 @@
 from typing import List, Tuple, Union
 
 from kikit.common import *
-
+from kikit.units import deg
 from kikit.defs import STROKE_T, Layer
 
 TABFAIL_VISUAL = False
@@ -35,13 +35,12 @@ class TabFilletError(RuntimeError):
 
 def roundPoint(point, precision=-4):
     return (round(point[0], precision), round(point[1], precision))
-    return pcbnew.wxPoint(round(point[0], precision), round(point[1], precision))
 
 def getStartPoint(geom):
     if isV6():
         if geom.GetShape() == STROKE_T.S_CIRCLE:
             # Circle start is circle center /o\
-            point = geom.GetStart() + pcbnew.wxPoint(geom.GetRadius(), 0)
+            point = geom.GetStart() + pcbnew.VECTOR2I(geom.GetRadius(), 0)
         elif geom.GetShape() == STROKE_T.S_RECT:
             point = geom.GetStart()
         else:
@@ -56,7 +55,7 @@ def getEndPoint(geom):
     if isV6():
         if geom.GetShape() == STROKE_T.S_CIRCLE:
             # Circle start is circle center /o\
-            point = geom.GetStart() + pcbnew.wxPoint(geom.GetRadius(), 0)
+            point = geom.GetStart() + pcbnew.VECTOR2I(geom.GetRadius(), 0)
         elif geom.GetShape() == STROKE_T.S_RECT:
             # Rectangle is closed, so it starts at the same point as it ends
             point = geom.GetStart()
@@ -159,16 +158,18 @@ def approximateArc(arc, endWith):
     Take DRAWINGITEM and approximate it using lines
     """
     SEGMENTS_PER_FULL= 4 * 32 # To Be consistent with default shapely settings
-    startAngle = arc.GetArcAngleStart() / 10
+
+    startAngle = EDA_ANGLE(0, pcbnew.DEGREES_T)
+    endAngle = EDA_ANGLE(0, pcbnew.DEGREES_T)
+    arc.CalcArcAngles(startAngle, endAngle)
     if arc.GetShape() == STROKE_T.S_CIRCLE:
-        endAngle = startAngle + 360
+        endAngle = startAngle + 360 * deg
         segments = SEGMENTS_PER_FULL
     else:
-        endAngle = startAngle + arc.GetArcAngle() / 10
-        segments = abs(int((endAngle - startAngle) * SEGMENTS_PER_FULL // 360))
+        segments = abs(int((endAngle.AsDegrees() - startAngle.AsDegrees()) * SEGMENTS_PER_FULL // 360))
     # Ensure a minimal number of segments for small angle section of arcs
     segments = max(segments, 12)
-    theta = np.radians(np.linspace(startAngle, endAngle, segments))
+    theta = np.linspace(startAngle.AsRadians(), endAngle.AsRadians(), segments)
     x = arc.GetCenter()[0] + arc.GetRadius() * np.cos(theta)
     y = arc.GetCenter()[1] + arc.GetRadius() * np.sin(theta)
     outline = list(np.column_stack([x, y]))
@@ -361,7 +362,7 @@ def commonCircleKiCAD(a, b, c):
     """
     arc = pcbnew.PCB_SHAPE()
     arc.SetShape(STROKE_T.S_ARC)
-    arc.SetArcGeometry(wxPoint(*a), wxPoint(*b), wxPoint(*c))
+    arc.SetArcGeometry(toKiCADPoint(a), toKiCADPoint(b), toKiCADPoint(c))
     center = [int(x) for x in arc.GetCenter()]
     mid = [(a[i] + b[i]) // 2 for i in range(2)]
     if center == mid:
@@ -607,8 +608,8 @@ def _constructEdgeSegment(self, a, b):
         segment = pcbnew.PCB_SHAPE()
         segment.SetShape(STROKE_T.S_SEGMENT)
         segment.SetLayer(Layer.Edge_Cuts)
-        segment.SetStart(wxPoint(*a))
-        segment.SetEnd(wxPoint(*b))
+        segment.SetStart(toKiCADPoint(a))
+        segment.SetEnd(toKiCADPoint(b))
         return segment
 
     def _constructArc(self, a, b, c):
@@ -618,15 +619,17 @@ def _constructArc(self, a, b, c):
         arc = pcbnew.PCB_SHAPE()
         arc.SetShape(STROKE_T.S_ARC)
         arc.SetLayer(Layer.Edge_Cuts)
-        arc.SetArcGeometry(wxPoint(*a), wxPoint(*b), wxPoint(*c))
+        arc.SetArcGeometry(toKiCADPoint(a), toKiCADPoint(b), toKiCADPoint(c))
         return arc
 
     def boundingBox(self):
         """
-        Return bounding box as wxRect
+        Return bounding box as BOX2I
         """
         minx, miny, maxx, maxy = self.substrates.bounds
-        return pcbnew.wxRect(int(minx), int(miny), int(maxx - minx), int(maxy - miny))
+        return pcbnew.BOX2I(
+            pcbnew.VECTOR2I(int(minx), int(miny)),
+            pcbnew.VECTOR2I(int(maxx - minx), int(maxy - miny)))
 
     def exterior(self):
         """
diff --git a/kikit/units.py b/kikit/units.py
index 9d8033e..2f85928 100644
--- a/kikit/units.py
+++ b/kikit/units.py
@@ -1,5 +1,6 @@
 import re
 import math
+from pcbnewTransition import pcbnew
 
 class UnitError(RuntimeError):
     pass
@@ -12,8 +13,8 @@ class UnitError(RuntimeError):
 mil = 25400
 inch = 1000 * mil
 
-deg = 10
-rad = 180 / math.pi * deg
+deg = pcbnew.EDA_ANGLE(1, pcbnew.DEGREES_T)
+rad = pcbnew.EDA_ANGLE(1, pcbnew.RADIANS_T)
 
 UNIT_SPLIT = re.compile(r"\s*(-?\s*\d+(\.\d*)?)\s*(\w+|\%)$")
 
@@ -32,6 +33,20 @@ def __str__(self):
     def __repr__(self):
         return f"<BaseValue: {int(self)}, {self.str} >"
 
+class BaseAngle(pcbnew.EDA_ANGLE):
+    """
+    Angle value that remembers its original string representation.
+    """
+    def __init__(self, value: pcbnew.EDA_ANGLE, strRepr: str) -> None:
+        super().__init__(value.AsDegrees(), pcbnew.DEGREES_T)
+        self.str = strRepr
+
+    def __str__(self):
+        return self.str
+
+    def __repr__(self):
+        return f"<BaseAngle: {int(self)}, {self.str} >"
+
 
 class PercentageValue(float):
     """
@@ -77,17 +92,17 @@ def readLength(unitStr):
         raise RuntimeError(f"Got '{unitStr}', a length with units was expected")
     return BaseValue(readUnit(unitDir, unitStr), unitStr)
 
-def readAngle(unitStr):
+def readAngle(unitStr: str) -> BaseAngle:
     unitDir = {
         "deg": deg,
         "°": deg,
         "rad": rad
     }
     if isinstance(unitStr, int):
-        return BaseValue(unitStr, f"{unitStr / 10} deg")
+        return BaseAngle(pcbnew.EDA_ANGLE(unitStr, pcbnew.TENTHS_OF_A_DEGREE_T), f"{unitStr / 10} deg")
     if not isinstance(unitStr, str):
         raise RuntimeError(f"Got '{unitStr}', an angle with units was expected")
-    return BaseValue(readUnit(unitDir, unitStr), unitStr)
+    return BaseAngle(readUnit(unitDir, unitStr), unitStr)
 
 def readPercents(unitStr):
     unitDir = { "%": 0.01 }
diff --git a/scripts/exampleDoc.py b/scripts/exampleDoc.py
index 707acbf..dceed0e 100644
--- a/scripts/exampleDoc.py
+++ b/scripts/exampleDoc.py
@@ -525,11 +525,11 @@ def autoName():
 
 ```.py
 from kikit.units import mm
-from pcbnew import wxPoint
+from pcbnew import VECTOR2I
 
 def kikitPostprocess(panel, arg):
     minx, miny, maxx, maxy = panel.panelBBox()
-    position = wxPoint((minx + maxx) / 2, miny + 2 * mm)
+    position = VECTOR2I((minx + maxx) / 2, miny + 2 * mm)
     panel.addNPTHole(position, 3 * mm)
 ```
 
diff --git a/scripts/panelizeDoc.py b/scripts/panelizeDoc.py
index 312501a..cef4bf9 100644
--- a/scripts/panelizeDoc.py
+++ b/scripts/panelizeDoc.py
@@ -43,8 +43,7 @@ def classMethods(c):
 You can also use functions `{header(panelize.fromMm)}` and
 `{header(panelize.toMm)}` to convert to/from them if you like them more. You are
 also encouraged to use the functions and objects the native KiCAD Python API
-offers, e.g.: {'`, `'.join([header(pcbnew.wxPoint), header(pcbnew.wxPointMM),
-              header(pcbnew.wxRect), header(pcbnew.wxRectMM)])}.
+offers, e.g.: {'`, `'.join([header(pcbnew.VECTOR2I),header(pcbnew.BOX2I)])}.
 """)
 
 print(
@@ -64,7 +63,7 @@ def classMethods(c):
 
 To generate a piece of a substrate, create a shapely.Polygon. Then add the piece
 of substrate via `panelize.Panel.appendSubstrate`. This method also accepts a
-`wxRect` for convenience.
+`BOX2I` for convenience.
 
 The tab generator is available via `panelize.Panel.boardSubstrate.tab`. This
 method takes an origin point, direction, and tab width. It tries to build a tab
diff --git a/test/system/panelize.bats b/test/system/panelize.bats
index 1e665b5..2d750a5 100644
--- a/test/system/panelize.bats
+++ b/test/system/panelize.bats
@@ -4,6 +4,7 @@ load common
 
 @test "Extract board" {
     kikit separate --source 'rectangle; tlx: 89mm; tly: 89mm; brx: 111mm; bry: 111mm' \
+        --debug 'trace: true' \
         $RES/multiboard.kicad_pcb board_a.kicad_pcb
 
     kikit separate --source 'annotation; ref: B1' \
