Repository: yaqwsx/KiKit (language: python)

Issue:
Feature request: Corner joints

Would be great if there was an option in addition to Vcuts and mouse bites, to have the diagonal tabs in the corners.




Discussion / hints:
Could you provide me with a simple drawing of such panel? How should be the tabs oriented? What cuts should be performed?
I think this is what it means 

![obrazek](https://user-images.githubusercontent.com/3955056/92231830-b912ee80-eead-11ea-8116-89bd61ded801.png)


Fix patch:
diff --git a/.gitignore b/.gitignore
index af1f9bc9..bec422b3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,4 +22,7 @@ KiKit.egg-info
 # Ignore venv
 venv
 
+# Ignore png tab drawings
+doc/resources/tabdrawing*.png
+
 
diff --git a/Makefile b/Makefile
index 640f3331..eb0078a2 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
 
 
-.PHONY: doc clean package release test
+.PHONY: doc clean package release test test-system test-unit
 
 all: doc package test
 
@@ -9,8 +9,11 @@ doc: doc/panelization.md doc/examples.md
 doc/panelization.md: kikit/panelize.py scripts/panelizeDoc.py
 	PYTHONPATH=`pwd` python3 scripts/panelizeDoc.py > $@
 
-doc/examples.md: scripts/exampleDoc.py
-	pcbdraw --silent doc/resources/conn.kicad_pcb doc/resources/conn.png
+doc/resources/conn.png: doc/resources/conn.kicad_pcb
+	pcbdraw --silent $< $@
+	convert $@ -define png:include-chunk=none $@
+
+doc/examples.md: scripts/exampleDoc.py doc/resources/conn.png
 	PYTHONPATH=`pwd` python3 scripts/exampleDoc.py > $@
 
 package:
@@ -23,9 +26,14 @@ install: package
 release: package
 	twine upload dist/*
 
-test: build/test $(shell find kikit -type f)
+test: test-system test-unit
+
+test-system: build/test $(shell find kikit -type f)
 	cd build/test && bats ../../test/system
 
+test-unit:
+	cd test/units && pytest
+
 build/test:
 	mkdir -p $@
 
diff --git a/README.md b/README.md
index dea99c9c..57bcf8fa 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,12 @@
-# KiKit - Automation for KiCAD
+# KiKit – Automation for KiCAD
 
-KiKit is a Python library and CLI tool to automate several tasks in a standard
-KiCAD workflow like:
+KiKit is a Python library, KiCAD plugin and  a CLI tool to automate several
+tasks in a standard KiCAD workflow like:
 
-- panelization of the boards (see [examples](doc/examples.md))
-- exporting manufacturing data
+- panelization of both, regular and oddly shaped, boards (see
+  [examples](doc/examples.md))
+- automated exporting manufacturing data based on manufacturer presets
+- multi-board project in KiCAD
 - building board presentation pages (see [an example presentation page generated
   by KiKit](https://roboticsbrno.github.io/RB0002-BatteryPack))
 
@@ -14,6 +16,10 @@ KiCAD workflow like:
 
 [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/E1E2181LU)
 
+# KiKit is currently undergoing large changes. If you use stable version read!
+
+The documentation for latest stable (v0.7) is available at [https://github.com/yaqwsx/KiKit/tree/v0.7](https://github.com/yaqwsx/KiKit/tree/v0.7)
+
 ## Installation
 
 KiKit is available as a [PyPi package](https://pypi.org/project/KiKit/),
@@ -33,7 +39,7 @@ pip3 install git+https://github.com/yaqwsx/KiKit@master
 Note that if you have a stable version installed, you need to add `--force` to
 upgrade it to upstream.
 
-Note that to use [KiCAD GUI plugins](doc/plugins.md), you have to enable them -
+Note that to use [KiCAD GUI plugins](doc/plugins.md), you have to enable them –
 see the [installation guide](doc/installation.md) for more details.
 
 If you would like to use KiKit on Windows or MacOS, see the [installation
@@ -69,6 +75,7 @@ KiKit](https://roboticsbrno.github.io/RB0002-BatteryPack).
 - compared to hand-creation of panels, your panels will pass DRC (as tracks from
   different instances of the same board have distinct nets when using KiKit)
 - if you have multiple boards in a single file, you can split them
+- simplifying [multi-board project in KiCAD](doc/multiboard.md)
 - [automated export of gerbers and assembly data](doc/fabrication.md)
 - [3D printed self-registering solder paste stencils](doc/stencil.md)
 - [steel stencils with alignment jig](doc/stencil.md)
diff --git a/doc/examples.md b/doc/examples.md
index 522dcf2c..db53b017 100644
--- a/doc/examples.md
+++ b/doc/examples.md
@@ -1,41 +1,54 @@
 
 # Examples
 
-This document will show you several examples of KiKit CLI for panelization. We
-will show everything on a single board located in
-`doc/resources/conn.kicad_pcb`. The board looks like this:
+This document will show you several examples of KiKit CLI for panelization. Note
+that this is **not an exhaustive description** of everything that KiKit can do,
+nor proper documentation. For further details, please refer to:
 
-![conn](resources/conn.png)
-
-# Extract Board
+- [installation guide](installation.md)
+- [description of all panelization options](panelizeCli.md)
+- [more detail about KiKit's algorithm for tab creation](understandingTabs.md)
+- [reference for the Python interface](panelization.md)
 
-The simples command is `extractboard`. By calling:
+We will show everything on a single board located in
+`doc/resources/conn.kicad_pcb`. The board looks like this when rendered via
+PcbDraw:
 
-```
-kikit panelize extractboard --sourcearea 100 50 100 100 doc/resources/conn.kicad_pcb output.kicad_pcb
-```
-
-We extract our board into a new file. This command is usefull when you designed
-multiple boards in a single file (e.g., to have shared schematic for board
-sandwiches). The `sourcearea` is given as a rectangle. You should specify X, Y
-coordinates of upper left corner width and height in millimeters. Also note,
-that only board items which fully fit inside this rectangle are extracted.
+![conn](resources/conn.png)
 
 
-# Panelize
+# Basic panels & layout
 
 Let's start with our first panel.
 
 ```
-kikit panelize grid --gridsize 2 2 --vcuts doc/resources/conn.kicad_pcb panel.kicad_pcb
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2;' \
+    --tabs full \
+    --cuts vcuts \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
 ```
-![panel1](resources/panel1.png)
 
-We specified that we want 2x2 panel, no space between board and separate them by
-V-cuts. Note, that due to the rounded corners, this panel cannot be
-manufactured. We will fix it later.
+![examplePanel1](resources/examplePanel1.png)
 
-One side note - if you try it with your own board some components might be gone.
+We specified that we want 2x2 panel, no space between board and separate them by
+V-cuts. We also specified that we want to build full tabs (although no tabs are
+visible in this example). This is ,however, essential – if we omitted tabs, no
+cuts between the boards would be performed. Note, that due to the rounded
+corners, this panel cannot be manufactured. We will fix it later.
+
+Note that the `\` in the input is there to make shell happy, so we can break our
+command into multiple lines. Also note that there are single quotes around the
+key-value pair – again, to make shell happy and to interpret a string with
+spaces as a single option.
+
+Also note that KiKit accepts all options in categories (e.g., `layout`, `tabs`,
+`cuts`, ...). You can specify the parameters as a semicolon-separated key-value
+list. To learn about the precise syntax of the CLI and about all options, please
+refer to – [documentation](panelizeCli.md).
+
+One side note – if you try it with your own board some components might be gone.
 KiKit respects the KiCAD component selection criteria. When you specify an input
 rectangle, only the components that **fully fit** inside the input rectangle are
 selected. This however take in account **both name and value labels** (even when
@@ -52,131 +65,446 @@ copies of the notes around the board).
 
 How to include the missing components?
 - specify the source area explicitly to include all your components
-- specify `--tolerance 10` to enlarge the board outline bounding box by e.g. 10
-  mm. The default value is 5 mm.
+- specify `--source 'tolerance: 10mm'` to enlarge the board outline bounding box
+  by e.g. 10 mm. The default value is 5 mm.
+
+I told you that the panel above is not suitable for manufacturing. Let's see why:
+
+```
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2;' \
+    --tabs full \
+    --cuts vcuts \
+    --post 'millradius: 1mm' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
+```
+
+![examplePanel2](resources/examplePanel2.png)
+
+We specified a milling simulation post-processing. This simulates the milling
+operation in the fab house. As you can see, the sharp internal corners cannot be
+manufactured. I recommend you to use milling postprocessing always – you can
+easily see if your cuts are off or you have too narrow slots in your design.
+
+Usually, one would use full tabs only for rectangular boards. Usually, when you
+have rounded corners, you will use short tabs instead and add some space between
+the boards. So let's fix it:
+
+```
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+    --tabs 'fixed; hwidth: 10mm; vwidth: 15mm' \
+    --cuts vcuts \
+    --post 'millradius: 1mm' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
+```
+
+![examplePanel3](resources/examplePanel3.png)
+
+In that way, the rounded corners can be machined. Lets' see the same example
+with mousebites instead:
+
+```
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+    --tabs 'fixed; width: 5mm' \
+    --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
+```
+
+![examplePanel4](resources/examplePanel4.png)
+
+We changed cut styles to mousebites and we specified that they should be
+performed by 0.5mm holes with a spacing of 1 mm. You could also use inches if
+you want – just specify `<number>in. Since we use mousebites, we used narrower
+tabs. We also specified that the cuts should be inset 0.25 mm into the board
+outline. This is suitable when your board should fit into a cover – when you
+break away the tabs, all burs will be inside the intended board outline.
+
+
+What happens, when we simulate the milling operation?
+
+```
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+    --tabs 'fixed; width: 5mm' \
+    --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm' \
+    --post 'millradius: 1mm' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
+```
+
+![examplePanel5](resources/examplePanel5.png)
+
+See? The cuts are somewhat short. This is due to the internal corners that
+cannot be milled. KiKit can fix that for you – just specify you want to prolong
+your cuts tangentially by a small amount:
+
+```
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+    --tabs 'fixed; width: 3mm' \
+    --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+    --post 'millradius: 1mm' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
+```
+
+![examplePanel6](resources/examplePanel6.png)
+
+If you want, you can also specify a number of tabs to generate. KiKit will place
+them evenly:
+
+```
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+    --tabs 'fixed; width: 3mm; vcount: 2' \
+    --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+    --post 'millradius: 1mm' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
+```
+
+![examplePanel7](resources/examplePanel7.png)
+
+You can also append frame or rails to the panel. Frames and rail are useful in
+the following situations:
+
+- you want to assemble your board, so you need tooling holes, fiducial.
+- you want to append a text to board (e.g., to identify a manufacturing batch)
+- your boards are not rectangluar and you want to use V-Cuts (most manufactures
+  require the outer edge of the panel to be a rectangle in order to manufacture
+  V-Cuts)
+
+Let's start with rails:
+
+```
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+    --tabs 'fixed; width: 3mm; vcount: 2' \
+    --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+    --framing 'railstb; width: 5mm; space: 3mm;' \
+    --post 'millradius: 1mm' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
+```
+
+![examplePanel8](resources/examplePanel8.png)
 
-Now back to our example. Let's see how the same panel will look like with mouse
-bites instead:
+Similarly, you can add left and right rail via the `railslr` style. If you want
+a full frame, use the style `frame`. When you place a full frame, it might make
+sense to include cuts in the corner of the frame, so you can break it apart
+easily. Let's see an example:
 
 ```
-kikit panelize grid --gridsize 2 2 --mousebites 0.5 1 0 doc/resources/conn.kicad_pcb panel.kicad_pcb
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+    --tabs 'fixed; width: 3mm; vcount: 2' \
+    --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+    --framing 'frame; width: 5mm; space: 3mm; cuts: true' \
+    --post 'millradius: 1mm' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
 ```
-![panel2](resources/panel2.png)
 
-You specify mouse bites by three numbers - hole diameter, hole spacing and
-offset. All in millimeters. We use offset 0, because we have no tabs. Otherwise
-the recommended value is 0.25 mm.
+![examplePanel9](resources/examplePanel9.png)
 
-The approach shown above is good for boards without rounded corners. If send
-panel above for fabrication we would obtain something like this:
+When you use V-cuts it might make sense to not remove all material, but only
+mill a slot around the board of the board. This yields a stronger panel – and
+some manufacturers require such style for assembly with V-Cuts. This is achieved
+via framing style `tightframe`. Note that it does not make much sense with
+mousebites.
 
 ```
-kikit panelize grid --gridsize 2 2 --mousebites 0.5 1 0 --radius 1 doc/resources/conn.kicad_pcb panel.kicad_pcb
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2; space: 6mm' \
+    --tabs 'fixed; width: 3mm; vcount: 2' \
+    --cuts vcuts \
+    --framing 'tightframe; width: 5mm; space: 3mm; ' \
+    --post 'millradius: 1mm' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
 ```
-![panel3](resources/panel3.png)
 
-The `--radius` argument simulates the milling operation. You specify mill radius
-(usuall the smallest diameter is 2 mm). We recommend to use the radius argument.
-See the distorted corners in picture above? Let's fix it by adding tabs.
+![examplePanel10](resources/examplePanel10.png)
+
+Once we have a frame, we can append a tooling holes, fiducials and some text to
+it:
 
 ```
-kikit panelize grid --space 3 --gridsize 2 2 --tabwidth 18 --tabheight 10 --vcuts --radius 1 doc/resources/conn.kicad_pcb panel.kicad_pcb
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+    --tabs 'fixed; width: 3mm; vcount: 2' \
+    --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+    --framing 'railstb; width: 5mm; space: 3mm;' \
+    --tooling '3hole; hoffset: 2.5mm; voffset: 2.5mm; size: 1.5mm' \
+    --fiducials '3fid; hoffset: 5mm; voffset: 2.5mm; coppersize: 2mm; opening: 1mm;' \
+    --text 'simple; text: yaqwsx's panel; anchor: mt; voffset: 2.5mm; hjustify: center; vjustify: center;' \
+    --post 'millradius: 1mm' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
 ```
-![panel4](resources/panel4.png)
 
-We introduced tabs - extra space between the board. We also specified the tab
-width and height, so there is clearance for milling the corners.
+![examplePanel11](resources/examplePanel11.png)
+
+There are many options for text and fiducials. Be sure to read the [full
+documentation](panelizeCli.md).
+
+# Advanced features & layouts
+
+It is possible that you have some critical features you want to avoid with tabs.
+KiKit has several features that can help you. Let's start with the simple ones.
+
+First, you can rotate the boards in your layout. This might make not much sense
+for rectanglar boards, but it might save you when you have circular or oddly
+shaped boards:
 
-When doing similar panel with mousebites, you usually want shorter tabs and
-possibly more of them. We can do it by specifing `--htabs` and `--vtabs`:
 
 ```
-kikit panelize grid --space 3 --gridsize 2 2 --tabwidth 3 --tabheight 3 --htabs 1 --vtabs 2 --mousebites 0.5 1 0.25 --radius 1 doc/resources/conn.kicad_pcb panel.kicad_pcb
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2; space: 0mm; rotation: 45deg;' \
+    --tabs 'fixed; width: 3mm;' \
+    --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.75mm' \
+    --framing 'frame; width: 5mm; space: 3mm; cuts: true' \
+    --post 'millradius: 1mm' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
+```
+
+![examplePanel12](resources/examplePanel12.png)
+
+When your board has a connector sticking one one side of the board, it makes
+sense to rotate the boards every other column, row or combination of both. KiKit
+supports this via layout option `alternation`. You should be careful about
+component references when rotating boards – KiCAD's references have a property
+"Stay upright" which makes them always face up (even when placed on a panel). So
+be sure to turn it off before panelizing. Here's an example:
+
+```
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2; space: 3mm; alternation: cols;' \
+    --tabs 'fixed; width: 3mm; vcount: 2' \
+    --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+    --framing 'frame; width: 5mm; space: 3mm; cuts: true' \
+    --post 'millradius: 1mm' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
+```
+
+![examplePanel13](resources/examplePanel13.png)
+
+Another solution might be to not put tabs on, e.g., vertical edges of the PCB.
+However, in that case your panel might be weak for further assembly. You can
+make it more stiff by including backbones – a full piece of substrate between
+the panels. Note that adding a backbone does not extend space between boards -
+that's up to you. You can add either vertical, horizontal or both backbones.
+Also, similarly with frames, you can put cuts on your backbone to make
+depanelization of your boards easier. Enough theory, let's see an example
+
+```
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2; hspace: 2mm; vspace: 9mm; hbackbone: 5mm; hbonecut: true' \
+    --tabs 'fixed; width: 3mm; vcount: 2; hcount: 0' \
+    --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+    --framing 'railstb; width: 5mm; space: 3mm;' \
+    --post 'millradius: 1mm' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
+```
+
+![examplePanel14](resources/examplePanel14.png)
+
+The most powerful feature of KiKit regarding tab placement are tabs via
+annotation. Remember our test board? When you open it in Pcbnew, you can see
+that there are some special footprints – KiKit's annotations:
+
+![conn-pcbnew](resources/conn-pcbnew.png)
+
+They specify where to place tabs. You can even specify individual tab width via
+text property of the symbol. How to use it? Just specify tab style to
+`annotation`. We also have to increase the source area tolerance, so it can
+capture the annotations.
+
 ```
-![panel5](resources/panel5.png)
 
-If you want, you can also add rails to the panel. Rails are suitable for placing
-tooling holes and fiducials, which are required by some assembly houses.
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2; space: 5mm;' \
+    --tabs annotation \
+    --source 'tolerance: 15mm' \
+    --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+    --framing 'railstb; width: 5mm; space: 3mm;' \
+    --post 'millradius: 1mm' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
+```
 
-The rails can be added by either `--railsTb <width>` (for top and bottom rails)
-or by `--railsLr <widht>` (for left and right rails). The rails work with both;
-mousebites and V-cuts.
+![examplePanel15](resources/examplePanel15.png)
 
-Then you can add fiducials to the top-left, bottom-left and top-right corner via
-`--fiducials <horizontalOffset> <verticalOffset> <copperDiameter>
-<openingDiameter>`. Similarly, you can add tooling holes via `--tooling
-<horizontalOffset> <verticalOffset> <diameter>`.
+Well, the panel looks strange – right? That's because KiKit always constructs a
+half-bridges. When you specify the tabs location, you have to either ensure they
+match or put a piece of substrate they can reach – e.g., a backbone or a
+tightframe. If you are interested in the details, read more about tabs in
+section [Understanding tabs](understandingTabs.md). Let's fix it:
 
 ```
-kikit panelize grid --space 3 --gridsize 2 2 --tabwidth 18 --tabheight 10 --vcuts --radius 1 --railsTb 5 --fiducials 10 2.5 1 2 --tooling 5 2.5 1.5 doc/resources/conn.kicad_pcb panel.kicad_pcb
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2; space: 8mm; hbackbone: 3mm; vbackbone: 3mm' \
+    --tabs annotation \
+    --source 'tolerance: 15mm' \
+    --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+    --framing 'railstb; width: 5mm; space: 3mm;' \
+    --post 'millradius: 1mm' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
 ```
-![panel6](resources/panel6.png)
 
-If you want to get a full frame around a board, don't use combination of top and
-bottom rails. It will generate a correct shape, however, it won't generate cuts
-to easily break the frame. Instead, use the "--panelsize <width> <height>"
-option in combination with `--framecutH` or `--framecutV` to add the cuts in the
-corners of the frame. Note that in the following example we use mousebites to
-see the frame cuts
+![examplePanel16](resources/examplePanel16.png)
+
+Note that the annotation can have an arbitrary orientation. The arrow just must
+be outside board edge and points towards it. KiKit will also place only those
+tabs, that have a neighboring substrate. For precise algorithm, see section
+[understanding tabs](understandingTabs.md).
+
+When you make flex PCBs or you want to save etchant, it make sense to pour
+copper on all non-functional parts of the panel. It will make the PCB rigid. You
+can do so via `copperfill` post-processing operation:
 
 ```
-kikit panelize grid --space 3 --gridsize 2 2 --tabwidth 5 --tabheight 5 --mousebites 0.5 1 0 --radius 1 --panelsize 75 58 --framecutH --fiducials 10 2.5 1 2 --tooling 5 2.5 1.5 doc/resources/conn.kicad_pcb panel.kicad_pcb
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+    --tabs 'fixed; width: 3mm;' \
+    --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+    --framing 'railstb; width: 5mm; space: 3mm;' \
+    --post 'millradius: 1mm; copperfill: true' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
 ```
-![panel7](resources/panel7.png)
 
-This was the `grid` command. There is also command `tightgrid` which works
-similarly, but instead of adding tabs and frames around the board, in places a
-full frame around the boards and mills a slot around the contours. Why this
-might be useful? For example when you make panel out of circular boards which
-you want to separate by V-cuts (by cutting a little bit to their interior). In
-that case don't forget to specify `--vcutcurves` to approximate curvature cuts
-via a straight V-cut. Back to `tightgrid`:
+![examplePanel17](resources/examplePanel17.png)
+
+When you use V-cuts with `copperfill` you (or your fab house) might want to
+include a clearance around the V-cuts:
 
 ```
-kikit panelize tightgrid --slotwidth 2.5 --space 8 --gridsize 2 2 --tabwidth 15 --tabheight 8 --mousebites 0.5 1 0.25 --radius 1 --panelsize 80 60 doc/resources/conn.kicad_pcb panel.kicad_pcb
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+    --tabs 'fixed; hwidth: 10mm; vwidth: 15mm' \
+    --cuts 'vcuts; clearance: 1.5mm' \
+    --framing 'railstb; width: 5mm; space: 3mm;' \
+    --post 'millradius: 1mm; copperfill: true' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
 ```
-![panel8](resources/panel8.png)
 
-You can also rotate the input board. Might not be usefull for
-rectangular boards, but if you have a circular or oddly shaped board...
+![examplePanel18](resources/examplePanel18.png)
+
+Note one last facts about V-cuts. V-cuts can only be straight and
+horizontal/vertical. But you can use them with circular boards if you want by
+cutting a little inside them. The option `cutcurves`, that will approximate the
+cut by staring and ending point.
+
+# I would like... but KiKit does not support it!
+
+If you need something special; e.g., custom placement of tooling holes, multiple
+texts, etc. KiKit has you covered.
+
+The CLI interface allows you to run a custom script over the final panel. The
+script can use KiKit Python interface to modify it. For the sake of simplicity,
+let's add a hole in the middle of the frame. Therefore, we write the following
+script:
+
+```.py
+from kikit.units import mm
+from pcbnew import wxPoint
 
+def kikitPostprocess(panel, arg):
+    minx, miny, maxx, maxy = panel.panelBBox()
+    position = wxPoint((minx + maxx) / 2, miny + 2 * mm)
+    panel.addNPTHole(position, 3 * mm)
 ```
-kikit panelize grid --space 2 --gridsize 2 2 --tabwidth 3 --tabheight 3 --mousebites 0.5 1 0.25 --radius 1 --panelsize 80 80 --rotation 45 doc/resources/conn.kicad_pcb panel.kicad_pcb
+
+Then run KiKit:
+
+```
+
+kikit panelize \
+    --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+    --tabs 'fixed; width: 3mm; vcount: 2' \
+    --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+    --framing 'railstb; width: 5mm; space: 3mm;' \
+    --post 'millradius: 1mm; script: doc/resources/examplePost.py' \
+    doc/resources/conn.kicad_pcb panel.kicad_pcb
 ```
-![panel9](resources/panel9.png)
 
-Especially for irregular shaped board it might make sense to rotate boards by
-180° based on their position in the grid. It could allow you to pack the boards
-more closely. Also, if you create a panel out of boards that have e.g., a
-connector on one side, it makes sense to rotate the second column by 180° so the
-connectors are facing out of the panel. You can do so by specifying the
-`--alternation` option. This options allows you to rotate board in every second
-column (`cols`), every second row (`rows`) or combination of both:
+![examplePanel19](resources/examplePanel19.png)
+
+You can learn more about available functions from the comment in the source code
+or in [documentation](panelization.md).
+
+If you implement a feature that your fab house requires (e.g., new tooling hole
+style), consider submitting a pull request for KiKit instead. I believe the
+others will benefit from it.
+
+# Managing presets
 
+The last section of this document is dedicated to management of presets. You can
+read the specification in the [documentation for CLI](panelizeCli.md). Here I
+would like to focus on practical examples.
+
+As you should know from the documentation, the panelization preset is divided
+into sections; e. g., `layout`, `tabs`, etc. The key-value parameters in these
+sections can be specified via JSON files. In KiKit, you can specify these files
+via `-p` option:
+
+```
+kikit panelize -p myPreset.json -p :<builtInPreset> <other parameters>
 ```
-kikit panelize grid --space 2 --gridsize 2 2 --tabwidth 3 --tabheight 3 --mousebites 0.5 1 0.25 --radius 1 --panelsize 80 80 --alternation cols doc/resources/conn.kicad_pcb panel.kicad_pcb
+
+The parameters in the later specified presets override the parameters in the
+previously specified presets. This allows you to define a named piece-wise
+presets. Therefore, you can prepare various presets for mousebites – e.g.,
+`fineMousebites.json` and `coarseMousebites.json`:
+
+```.js
+// fineMousebites.json
+{
+    "cuts": {
+        "style": "mousebites",
+        "drill": "0.5mm",
+        "spacing": "0.9mm",
+        "offset": "0.25mm"
+    }
+}
+
+// coarseMousebites.json
+{
+    "cuts": {
+        "style": "mousebites",
+        "drill": "0.3mm",
+        "spacing": "0.2mm",
+        "offset": "0.15mm"
+    }
+}
 ```
-![panel10](resources/panel10.png)
 
-Sometimes you might find yourself in a need for precise tab placement. This can be
-easily done. Just draw a line in one of KiCAD layers (be careful, the
-orientation matters - the line has to target the board substrate) and specify
-option `--tabsfrom`. Don't forget to disable automatically generated tabs by
-specifying `--vtabs 0` and `--htabs 0`. See for yourself, there are some lines
-already prepared for you in `conn.kicad_pcb`
+Then you can specify your panelization commands easily via:
 
 ```
-kikit panelize tightgrid --slotwidth 2.5 --space 8 --gridsize 2 2 --htabs 0 --vtabs 0 --tabsfrom Eco2.User 3 --tabsfrom Eco1.User 5 --mousebites 0.5 1 0.25 --radius 1 --panelsize 80 80 doc/resources/conn.kicad_pcb panel.kicad_pcb
+kikit panelize -p fineMousebites.json <otheroptions>
 ```
-![panel11](resources/panel11.png)
 
-Especially when you work with flex PCBs it makes sense to leave copper on
-non-functional parts of the panel to make it stiffer. It might also make sense
-to leave on traditional PCBs as it reduces the amount of etching. To do so,
-simply specify `--copperfill`. Here is one of the examples above with a copper
-fill:
+Therefore, you can build a custom library of commonly used-options; e.g., per
+fabrication house. KiKit offers some built-in styles – see
+[`panelizePresets`](../kikit/resources/panelizePresets). Note that the built-in
+preset `default.json` is always used as a base and it specifies conservative
+default values so you can only override the options relevant for you.
+
+To give you an example – with KiKit, you will no longer have to remember what
+diameter of tooling holes JLC PCB requires, just use:
 
 ```
-kikit panelize grid --space 3 --gridsize 2 2 --tabwidth 18 --tabheight 10 --vcuts --radius 1 --panelsize 70 55 --copperfill doc/resources/conn.kicad_pcb panel.kicad_pcb
+kikit panelize -p :jlcTooling <otheroptions>
 ```
-![panel12](resources/panel12.png)
+
diff --git a/doc/installation.md b/doc/installation.md
index 1d47f267..1ef09222 100644
--- a/doc/installation.md
+++ b/doc/installation.md
@@ -8,8 +8,9 @@ pip install KiKit # Use pip or pip3 based on your distribution
 ```
 
 Then you are ready to use it. Note that if you would like to use GUI plugins in
-KiCAD, you have enable them. See section "Enabling plugins". Also, there are two
-optional dependencies:
+KiCAD, you have enable them. Similarly, you can also register the KiKit
+footprint library. See section "Enabling plugins" and "Enabling Kikit annotation
+footprint library". Also, there are two optional dependencies:
 
 - PcbDraw - to be able to export presentation pages (install it via `pip install
   PcbDraw`)
@@ -33,6 +34,17 @@ install it directly from GitHub:
 pip3 install git+https://github.com/yaqwsx/KiKit@master
 ```
 
+## Enabling KiKit annotation footprint library
+
+KiKit distributes a footprint library called `kikit`. This library contains
+footprints that can be used for annotation of the PCB (e.g., mark tab
+locations). To use it, you have to add it into KiCAD.
+
+You can:
+- register the library automatically via invoking `kikit-plugin registerlib` of
+- add the library manually in KiCAD. You get the library location via
+  `kikit-info lib`. Note that the library has to be named `kikit`
+
 
 ## Enabling plugins
 
diff --git a/doc/multiboard.md b/doc/multiboard.md
new file mode 100644
index 00000000..bc4c8c6c
--- /dev/null
+++ b/doc/multiboard.md
@@ -0,0 +1,74 @@
+## Multi-board workflow with KiKit
+
+KiCAD does not support multiple board per project, nor boards with shared
+schematics. However, with the following workflow, you can easily draw multiple
+boards with shared schematics and, e.g., easily ensure that tha board connectors
+match.
+
+The workflow is the following:
+
+First, draw a separate schematics sheet for each of your boards and propagate
+the pins on the inter-board connectors. E.g., in one of my project it looks like
+this:
+
+![Multi board schematics](resources/multiboardsch.jpg)
+
+I also place the corresponding connectors next to each other to illustrate that
+they connect. Then draw your board schematics as usual - with one exception.
+**Do not use global power symbols nor global labels**, use only local labels.
+This ensures that the power lines are separate for each board and DRC won't
+complain, that you have not connected the power between two board.
+
+Then, draw all your boards into a single board file side-by-side. See file
+[resources/multiboard.kicad_pcb](resources/multiboard.kicad_pcb) for a dead
+simple illustration. You can also draw lines that will help you align your
+boards' connectors.
+
+Before manufacturing, use KiKit to extract the boards into a separate board
+files via the `extract` command.
+
+This can be done in two ways:
+
+- via bounding box selection
+- via annotation
+
+We will show how to use it on
+[resources/multiboard.kicad_pcb](resources/multiboard.kicad_pcb) which looks
+like this:
+
+![Multi board PCB](resources/multiboardpcb.jpg)
+
+
+## Bounding box selection
+
+Simply specify the top left and bottom right corner. Everything that fits fully
+inside it, will be included in the board. The command for separation of board A
+into a separate board file is:
+
+```
+kikit separate --source 'rectangle; tlx: 89mm; tly: 89mm; brx: 111mm; bry: 111mm' \
+        multiboard.kicad_pcb board_a.kicad_pcb
+```
+
+After that, `board_a.kicad_pcb` will contain only a board A. Note that the `\`
+is there for shell as we split our command into two lines.
+
+## Annotation
+
+As you can see, the source file contains an annotation in the form of virtual
+footprints `kikit:Board`. You can place them into your document so that the
+arrow points to the board edge. Than you can use the reference of the annotation
+symbol for separation of the board. To separate board A simply invoke:
+
+```
+kikit separate --source 'annotation; ref: B1' \
+    multiboard.kicad_pcb board_a.kicad_pcb
+```
+
+After that, `board_a.kicad_pcb` will contain only a board A. Note that the `\`
+is there for shell as we split our command into two lines.
+
+## About panlization
+
+Note that if panelize your boards, you don't have to separate your boards first;
+just use the `--source` with the panelization command.
diff --git a/doc/panelization.md b/doc/panelization.md
index 829d0149..a2e51e3c 100644
--- a/doc/panelization.md
+++ b/doc/panelization.md
@@ -4,18 +4,32 @@
 When you want to panelize a board, you are expected to load the `kikit.panelize`
 module and create an instance of the `Panel` class.
 
-All units are in the internal KiCAD units (1 nm). You can use functions `fromMm(mm)` and
-`toMm(kiUnits)` to convert to/from them (synopsis below). You are also encouraged to use
-the functions and objects the native KiCAD Python API offers, e.g.: `wxPoint(args)`, `wxPointMM(mmx, mmy)`, `wxRect(args)`, `wxRectMM(x, y, wx, wy)`.
+All units are in the internal KiCAD units (1 nm). You can use predefined
+constants to convert from/to them:
 
+```.py
+from kikit.units import *
+
+l = 1 * mm    # 1 mm
+l = 42 * inch # 42 inches
+l = 15 * cm   # 15 cm
+a = 90 * deg  # 90°
+a = 1 * rad   # 1 radian
+```
+
+You can also use functions `fromMm(mm)` and
+`toMm(kiUnits)` 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.: wxPoint(args)`, `wxPointMM(mmx, mmy)`, `wxRect(args)`, `wxRectMM(x, y, wx, wy).
 
 
 ## Basic Concepts
 
-The `panelize.Panel` class holds a panel under construction. Basically it is
-`pcbnew.BOARD` without outlines. The outlines are held separately as
+The `kikit.panelize.Panel` class holds a panel under construction. Basically it
+is `pcbnew.BOARD` without outlines. The outlines are held separately as
 `shapely.MultiPolygon` so we can easily merge pieces of a substrate, add cuts
-and export it back to `pcbnew.BOARD`.
+and export it back to `pcbnew.BOARD`. This is all handled by the class
+`kikit.substrate.Substrate`.
 
 ## Tabs
 
@@ -35,6 +49,9 @@ add the piece of a substrate via `panelize.Panel.appendSubstrate`. This design
 choice was made as batch adding of substrates is more efficient. Therefore, you
 are advised to first generate all the tabs and then append them to the board.
 
+You read more about the algorithms for generating tabs in a separate document
+[understanding tabs](understandingTabs.md).
+
 ## Cuts
 
 All methods constructing panels do not create cuts directly, instead, they
@@ -58,11 +75,105 @@ include components sticking out of the board outline, you can specify tolerance
 -- a distance by which the source area is expanded when copying components.
 
 
+#### `appendBoard`
+```
+appendBoard(self, filename, destination, sourceArea=None, origin=Origin.Center, 
+            rotationAngle=0, shrink=False, tolerance=0, bufferOutline=1000, 
+            netRenamer=None, refRenamer=None)
+```
+
 ## Panel class
 
-Basic interface for panel building. Instance of this class represents a
-single panel. You can append boards, add substrate pieces, make cuts or add
-holes to the panel. Once you finish, you have to save the panel to a file.
+This class has the following relevant members:
+- `board` - `pcbnew.BOARD` of the panel. Does not contain any edges.
+- `substrates` - `kikit.substrate.Substrate` - individual substrates appended
+  via `None`. You can use them to get the
+  original outline (and e.g., generate tabs accroding to it).
+- `boardSubstrate` - `kikit.substrate.Substrate` of the whole panel.
+- `backboneLines` - a list of lines representing backbone candidates. Read more
+  about it in [understanding tabs](understandingTabs.md).
+
+
+#### `addCornerFiducials`
+```
+addCornerFiducials(self, fidCount, horizontalOffset, verticalOffset, 
+                   copperDiameter, openingDiameter)
+```
+Add up to 4 fiducials to the top-left, top-right, bottom-left and
+bottom-right corner of the board (in this order). This function expects
+there is enough space on the board/frame/rail to place the feature.
+
+The offsets are measured from the outer edges of the substrate.
+
+#### `addCornerTooling`
+```
+addCornerTooling(self, holeCount, horizontalOffset, verticalOffset, diameter, 
+                 paste=False)
+```
+Add up to 4 tooling holes to the top-left, top-right, bottom-left and
+bottom-right corner of the board (in this order). This function expects
+there is enough space on the board/frame/rail to place the feature.
+
+The offsets are measured from the outer edges of the substrate.
+
+#### `addFiducial`
+```
+addFiducial(self, position, copperDiameter, openingDiameter, bottom=False)
+```
+Add fiducial, i.e round copper pad with solder mask opening to the position (`wxPoint`),
+with given copperDiameter and openingDiameter. By setting bottom to True, the fiducial
+is placed on bottom side.
+
+#### `addKeepout`
+```
+addKeepout(self, area, noTracks=True, noVias=True, noCopper=True)
+```
+Add a keepout area from top and bottom layers. Area is a shapely
+polygon. Return the keepout area.
+
+#### `addLine`
+```
+addLine(self, start, end, thickness, layer)
+```
+Add a line to the panel based on starting and ending point
+
+#### `addMillFillets`
+```
+addMillFillets(self, millRadius)
+```
+Add fillets to inner conernes which will be produced a by mill with
+given radius.
+
+#### `addNPTHole`
+```
+addNPTHole(self, position, diameter, paste=False)
+```
+Add a drilled non-plated hole to the position (`wxPoint`) with given
+diameter. The paste option allows to place the hole on the paste layers.
+
+#### `addText`
+```
+addText(self, text, position, orientation=0, width=1500000, height=1500000, 
+        thickness=300000, hJustify=EDA_TEXT_HJUSTIFY_T.GR_TEXT_HJUSTIFY_CENTER, 
+        vJustify=EDA_TEXT_VJUSTIFY_T.GR_TEXT_VJUSTIFY_CENTER, 
+        layer=Layer.F_SilkS)
+```
+Add text at given position to the panel. If appending to the bottom
+side, text is automatically mirrored.
+
+#### `addVCutH`
+```
+addVCutH(self, pos)
+```
+Adds a horizontal V-CUT at pos (integer in KiCAD units).
+
+#### `addVCutV`
+```
+addVCutV(self, pos)
+```
+Adds a horizontal V-CUT at pos (integer in KiCAD units).
+
+#### `appendBoard`
 ```
 appendBoard(self, filename, destination, sourceArea=None, origin=Origin.Center, 
             rotationAngle=0, shrink=False, tolerance=0, bufferOutline=1000, 
@@ -87,91 +198,341 @@ The renamers are given board seq number and original name
 
 Returns bounding box (wxRect) of the extracted area placed at the
 destination and the extracted substrate of the board.
-```
-save(self, filename)
-```
-Saves the panel to a file.
+
+#### `appendSubstrate`
 ```
 appendSubstrate(self, substrate)
 ```
 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
 rounded by specifying non-zero filletRadius.
+
+#### `boardsBBox`
+```
+boardsBBox(self)
+```
+Return common bounding box for all boards in the design (ignores the
+individual pieces of substrate) as a shapely box.
+
+#### `buildFullTabs`
+```
+buildFullTabs(self)
+```
+Make full tabs. This strategy basically cuts the bounding boxes of the
+PCBs. Not suitable for mousebites. Expects there is a valid partition
+line.
+
+Return a list of cuts.
+
+#### `buildPartitionLineFromBB`
+```
+buildPartitionLineFromBB(self, boundarySubstrates=[], safeMargin=0)
+```
+Builds partition & backbone line from bounding boxes of the substrates.
+You can optionally pass extra substrates (e.g., for frame).
+
+#### `buildTabAnnotationsCorners`
+```
+buildTabAnnotationsCorners(self, width)
+```
+Add tab annotations to the corners of the individual substrates.
+
+#### `buildTabAnnotationsFixed`
+```
+buildTabAnnotationsFixed(self, hcount, vcount, hwidth, vwidth, minDistance, 
+                         ghostSubstrates)
+```
+Add tab annotations for the individual substrates based on number of
+tabs in horizontal and vertical direction. You can specify individual
+width in each direction.
+
+If the edge is short for the specified number of tabs with given minimal
+spacing, the count is reduced.
+
+You can also specify ghost substrates (for the future framing).
+
+#### `buildTabAnnotationsSpacing`
+```
+buildTabAnnotationsSpacing(self, spacing, hwidth, vwidth, ghostSubstrates)
+```
+Add tab annotations for the individual substrates based on their spacing.
+
+You can also specify ghost substrates (for the future framing).
+
+#### `buildTabsFromAnnotations`
+```
+buildTabsFromAnnotations(self)
+```
+Given annotations for the individual substrates, create tabs for them.
+Tabs are appended to the panel, cuts are returned.
+
+Expects that a valid partition line is assigned to the the panel.
+
+#### `clearTabsAnnotations`
+```
+clearTabsAnnotations(self)
+```
+Remove all existing tab annotations from the panel.
+
+#### `copperFillNonBoardAreas`
+```
+copperFillNonBoardAreas(self)
+```
+Fill top and bottom layers with copper on unused areas of the panel
+(frame, rails and tabs)
+
+#### `debugRenderBackboneLines`
+```
+debugRenderBackboneLines(self)
+```
+Render partition line to the panel to be easily able to inspect them via
+Pcbnew.
+
+#### `debugRenderBoundingBoxes`
+```
+debugRenderBoundingBoxes(self)
+```
+None
+
+#### `debugRenderPartitionLines`
 ```
-makeGrid(self, boardfile, rows, cols, destination, sourceArea=None, tolerance=0, 
-         verSpace=0, horSpace=0, verTabCount=1, horTabCount=1, verTabWidth=0, 
-         horTabWidth=0, outerVerTabThickness=0, outerHorTabThickness=0, 
-         rotation=0, forceOuterCutsH=False, forceOuterCutsV=False, 
+debugRenderPartitionLines(self)
+```
+Render partition line to the panel to be easily able to inspect them via
+Pcbnew.
+
+#### `inheritCopperLayers`
+```
+inheritCopperLayers(self, board)
+```
+Update the panel's layer count to match the design being panelized.
+Raise an error if this is attempted twice with inconsistent layer count
+boards.
+
+#### `inheritDesignSettings`
+```
+inheritDesignSettings(self, boardFilename)
+```
+Inherit design settings from the given board specified by a filename
+
+#### `inheritProperties`
+```
+inheritProperties(self, boardFilename)
+```
+Inherit text properties from a board specified by a properties
+
+#### `makeFrame`
+```
+makeFrame(self, width)
+```
+Build a frame around the board. Return frame cuts
+
+#### `makeFrameCutsH`
+```
+makeFrameCutsH(self, innerArea, frameInnerArea, outerArea)
+```
+Generate horizontal cuts for the frame corners and return them
+
+#### `makeFrameCutsV`
+```
+makeFrameCutsV(self, innerArea, frameInnerArea, outerArea)
+```
+Generate vertical cuts for the frame corners and return them
+
+#### `makeGrid`
+```
+makeGrid(self, boardfile, sourceArea, rows, cols, destination, verSpace, 
+         horSpace, rotation, 
          placementClass=<class 'kikit.panelize.BasicGridPosition'>, 
          netRenamePattern=Board_{n}-{orig}, refRenamePattern=Board_{n}-{orig})
 ```
-Creates a grid of boards (row x col) as a panel at given destination
-separated by V-CUTS. The source can be either extracted automatically or
-from given sourceArea. There can be a spacing between the individual
-board (verSpacing, horSpacing) and the tab width can be adjusted
-(verTabWidth, horTabWidth). Also, the user can control whether to append
-the outer tabs (e.g. to connect it to a frame) by setting
-outerVerTabsWidth and outerHorTabsWidth.
+Place the given board in a regular grid pattern with given spacing
+(verSpace, horSpace). The board position can be fine-tuned via
+placementClass. The nets and references are renamed according to the
+patterns.
+
+Returns a list of the placed substrates. You can use these to generate
+tabs, frames, backbones, etc.
+
+#### `makeMouseBites`
+```
+makeMouseBites(self, cuts, diameter, spacing, offset=250000, prolongation=500000)
+```
+Take a list of cuts and perform mouse bites. The cuts can be prolonged
+to
+
+#### `makeRailsLr`
+```
+makeRailsLr(self, thickness)
+```
+Adds a rail to left and right.
 
-Returns a tuple - wxRect with the panel bounding box (excluding
-outerTabs) and a list of cuts (list of lines) to make. You can use the
-list to either create a V-CUTS via makeVCuts or mouse bites via
-makeMouseBites.
+#### `makeRailsTb`
 ```
-makeTightGrid(self, boardfile, rows, cols, destination, verSpace, horSpace, 
-              slotWidth, width, height, sourceArea=None, tolerance=0, 
-              verTabWidth=0, horTabWidth=0, verTabCount=1, horTabCount=1, 
-              rotation=0, 
-              placementClass=<class 'kikit.panelize.BasicGridPosition'>, 
-              netRenamePattern=Board_{n}-{orig}, 
-              refRenamePattern=Board_{n}-{orig})
+makeRailsTb(self, thickness)
 ```
-Creates a grid of boards just like `makeGrid`, however, it creates a
-milled slot around perimeter of each board and 4 tabs.
+Adds a rail to top and bottom.
+
+#### `makeTightFrame`
 ```
-makeFrame(self, innerArea, width, height, offset)
+makeTightFrame(self, width, slotwidth)
 ```
-Adds a frame around given `innerArea` (`wxRect`), which can be obtained,
-e.g., by `makeGrid`, with given `width` and `height`. Space with width
-`offset` is added around the `innerArea`.
+Build a full frame with board perimeter milled out
+
+#### `makeVCuts`
 ```
 makeVCuts(self, cuts, boundCurves=False)
 ```
 Take a list of lines to cut and performs V-CUTS. When boundCurves is
 set, approximate curved cuts by a line from the first and last point.
 Otherwise, raise an exception.
+
+#### `panelBBox`
 ```
-makeMouseBites(self, cuts, diameter, spacing, offset=250000)
+panelBBox(self)
 ```
-Take a list of cuts and perform mouse bites.
+Return bounding box of the panel as a shapely box.
+
+#### `panelCorners`
 ```
-addNPTHole(self, position, diameter)
+panelCorners(self, horizontalOffset=0, verticalOffset=0)
 ```
-Add a drilled non-plated hole to the position (`wxPoint`) with given
-diameter.
+Return the list of top-left, top-right, bottom-left and bottom-right
+corners of the panel. You can specify offsets.
+
+#### `renderBackbone`
 ```
-addFiducial(self, position, copperDiameter, openingDiameter, bottom=False)
+renderBackbone(self, vthickness, hthickness, vcut, hcut)
 ```
-Add fiducial, i.e round copper pad with solder mask opening to the position (`wxPoint`),
-with given copperDiameter and openingDiameter. By setting bottom to True, the fiducial
-is placed on bottom side.
+Render horizontal and vertical backbone lines. If zero thickness is
+specified, no backbone is rendered.
+
+vcut, hcut specifies if vertical or horizontal backbones should be cut.
+
+Return a list of cuts
+
+#### `save`
+```
+save(self, filename)
+```
+Saves the panel to a file.
+
+#### `setAuxiliaryOrigin`
+```
+setAuxiliaryOrigin(self, point)
+```
+Set the auxiliary origin used e.g., for drill files
+
+#### `setDesignSettings`
+```
+setDesignSettings(self, designSettings)
+```
+Set design settings
+
+#### `setGridOrigin`
+```
+setGridOrigin(self, point)
+```
+Set grid origin
+
+#### `setProperties`
+```
+setProperties(self, properties)
+```
+Set text properties cached in the board
+
+#### `setVCutClearance`
+```
+setVCutClearance(self, clearance)
+```
+Set V-cut clearance
+
+#### `setVCutLayer`
+```
+setVCutLayer(self, layer)
+```
+Set layer on which the V-Cuts will be rendered
+
+## Substrate class
+
+This class represents a pice of substrate (with no components). Basically it is
+just a relatively thin wrapper around shapely polygons. On top of that, it keeps
+a partition line for the substrate. ead more about partition lines in
+[understanding tabs](understandingTabs.md).
+
+
+
+#### `boundary`
+```
+boundary(self)
+```
+Return shapely geometry representing the outer ring
+
+#### `boundingBox`
+```
+boundingBox(self)
+```
+Return bounding box as wxRect
 
-## Examples
+#### `bounds`
+```
+bounds(self)
+```
+Return shapely bounds of substrates
 
-### Simple grid
+#### `exterior`
+```
+exterior(self)
+```
+Return a geometry representing the substrate with no holes
 
-The following example creates a 3 x 3 grid of boards in a frame separated by V-CUTS.
+#### `isSinglePiece`
+```
+isSinglePiece(self)
+```
+Decide whether the substrate consists of a single piece
 
+#### `millFillets`
 ```
-panel = Panel()
-size, cuts = panel.makeGrid("test.kicad_pcb", 4, 3, wxPointMM(100, 40),
-            tolerance=fromMm(5), verSpace=fromMm(5), horSpace=fromMm(5),
-            outerHorTabThickness=fromMm(3), outerVerTabThickness=fromMm(3),
-            verTabWidth=fromMm(15), horTabWidth=fromMm(8))
-panel.makeVCuts(cuts)
-# alternative: panel.makeMouseBites(cuts, diameter=fromMm(0.5), spacing=fromMm(1))
-panel.makeFrame(size, fromMm(100), fromMm(100), fromMm(3), radius=fromMm(1))
-panel.addMillFillets(fromMm(1))
-panel.save("out.kicad_pcb")
+millFillets(self, millRadius)
 ```
+Add fillets to inner conernes which will be produced a by mill with
+given radius.
 
+#### `removeIslands`
+```
+removeIslands(self)
+```
+Removes all islads - pieces of substrate fully contained within outline
+of another board
+
+#### `serialize`
+```
+serialize(self)
+```
+Produces a list of PCB_SHAPE on the Edge.Cuts layer
+
+#### `tab`
+```
+tab(self, origin, direction, width, partitionLine=None, maxHeight=50000000)
+```
+Create a tab for the substrate. The tab starts at the specified origin
+(2D point) and tries to penetrate existing substrate in direction (a 2D
+vector). The tab is constructed with given width. If the substrate is
+not penetrated within maxHeight, exception is raised.
+
+When partitionLine is specified, tha tab is extended to the opposite
+side - limited by the partition line. Note that if tab cannot span
+towards the partition line, then the the tab is not created - it returns
+a tuple (None, None).
+
+Returns a pair tab and cut outline. Add the tab it via union - batch
+adding of geometry is more efficient.
+
+#### `union`
+```
+union(self, other)
+```
+Appends a substrate, polygon or list of polygons. If there is a common
+intersection, with existing substrate, it will be merged into a single
+substrate.
diff --git a/doc/panelizeCli.md b/doc/panelizeCli.md
new file mode 100644
index 00000000..7f50bf3f
--- /dev/null
+++ b/doc/panelizeCli.md
@@ -0,0 +1,395 @@
+# Panelization CLI
+
+The whole panelization process of KiKit's CLI is driven by a configuration
+structure. The configuration contains several categories (e.g., `layout`,
+`tabs`, `framing`). Each of the categories have number of named parameters
+(e.g., `tabsCount`). All categories and their parameters are described further
+below.
+
+# Configurations
+
+The configuration can be supplied to KiKit via a JSON file with comments and
+from the command line. The example of a configuration in a JSON file is the
+following
+
+```.js
+{
+    // There can be C-like comments
+    "layout": {
+        "type": "grid",
+        "rows": 1,
+        "cols": 1,
+        "hspace": "0mm",
+        "vspace": "0mm",
+        "rotation": "0deg",
+        "alternation": "none",
+        "renamenet": "Board_{n}-{orig}",
+        "renameref": "{orig}"
+    },
+    "source": {
+        "type": "auto",
+        "tolerance": "1mm"
+    },
+    "tabs": {
+        "type": "normal",
+        "source": "none"
+    },
+    "cuts": {
+        "type": "none"
+    },
+    "framing": {
+        "type": "none",
+        "thickness": "0mm",
+        "millRadius": "0mm",
+        "copperFill": false
+    }
+}
+```
+
+KiKit accepts `-p <configurationFile>` option to specify one or more
+configurations. When multiple configurations are specified, they are composed
+into a single configuration. The later specified configurations overwrite
+parameters of the former specified configurations. This allows us to start with
+a basic configuration and have a number of small configurations specifying
+details.
+
+To give and example, consider the two following configurations:
+
+```.js
+// A
+{
+    "tabs": {
+        "type": "normal",
+        "width": "3mm"
+    },
+    "framing": {
+        "type": "frame",
+        "frameWidth": "100mm",
+        "frameHeight": "100mm"
+    }
+}
+
+// B
+{
+    "framing": {
+        "type": "rails"
+        "width": "5mm"
+    }
+}
+```
+
+When we merge `B` into `A`, we get:
+
+```.js
+{
+    "tabs": {
+        "type": "normal",
+        "width": "3mm"
+    },
+    "framing": {
+        "type": "rails"
+        "width": "5mm",
+        "frameWidth": "100mm",
+        "frameHeight": "100mm"
+    }
+}
+```
+
+You can also override every parameter from CLI. There is an option for each
+category, which accepts a semicolon-separated list of key-value pairs; e.g.:
+
+```
+--layout 'rows: 3; cols: 4'
+```
+
+The options from CLI have the highest priority - they override values from
+specified from the files. If you need to specify the character `;`, you can
+escape it via `\`.
+
+Therefore, a full invocation of KiKit for panelization can look like this:
+```
+kikit panelize -p myDefault.json -p useVcuts.json -p myFrame.json
+    --layout 'rows: 3; cols: 4'
+    board.kicad_pcb panel.kicad_pcb.
+```
+
+Note the single quotes -- without them your shell will eat the spaces and the
+command will be interpreted badly. The command will use our default
+configuration, then it will override some options to use V-cuts and then it adds
+a frame specified by `myFrame.json`. Last we specify the panel size from CLI.
+
+Note that KiKit always start with a default configuration (specified in the file
+[default.json](../../kikit/resources/panelizePresets/default.json)). There are
+also some configuration files shipped with KiKit. You can find them in the
+directory `kikit/resources/panelizePresets`. When you want to use them via
+option `-p`, just prefix their name with `:` and drop the suffix. E.g., for
+`vcuts.json` use `-p :vcuts`.
+
+If you would like to inspect which configuration was used by KiKit, you can dump
+it into a file with the `-d <filename>` option.
+
+# Units
+
+You can specify units in the configuration files and CLI. Always specify them as
+string, e.g., "2mm" or "0.5 inch" (do not forget the quotes in the JSON files).
+
+Supported length units: mm, cm, dm, m, mil, inch, in.
+
+Supported angle units: deg, °, rad.
+
+# Configuration categories
+
+There are the following categories: layout, source, tabs, cuts, framing, and
+tooling.
+
+Each category has a mandatory parameter `type` which dictates the style of that
+feature. Note that you can specify the type parameter in a simplified manner in
+the CLI by specifying it first and omitting the `type` word; e.g., `--cuts
+'mousebites, someParameter: 10cm'`.
+
+## Layout
+
+**Types**: grid
+
+**Common options**:
+
+- `rows`, `cols`: Specify the number of boards in the grid pattern
+- `hspace`, `vspace`, `space`: Specify space the gap between the boards. You can
+  specify separately vertical and horizontal spacing or you can specify `space`
+  to make them the same (it has higher priority).
+- `rotation`: Rotate the boards before placing them in the panel
+- `alternation`: Specify some alternations of board rotation. **All labels on
+  the board in KiCAD have by-default active the "stay upright" flag. Disable
+  this flag, otherwise your labels will be upside down on some boards**.
+    - `none`: Do not alternate
+    - `rows`: Rotate boards by 180° on every next row
+    - `cols`: Rotate boards by 180° on every next column
+    - `rowsCols`: Rotate boards by 180° based on a chessboard pattern
+- `renamenet`, `renameref`: A pattern by which to rename the nets and
+  references. You can use `{n}` and `{orig}` to get the board number and
+  original name. Default values are `Board_{n}-{orig}` for nets and `{orig}` for
+  references.
+- `vbackbone`, `hbackbone`: The width of vertical and horizontal backbone (0
+  means no backbone). The backbone does not increase the spacing of the boards.
+- `vbonecut`, `hbonecut`: true/false. If there are both backbones specified,
+  specifies if there should be a vertical or horizontal cut (or both) where the
+  backbones cross.
+
+#### Grid
+
+The boars are placed in a grid pattern connected by tabs. There are no special
+options.
+
+## Source
+
+This option allows you to specify the source area, e.g., when multiple boards
+are present. You can read more about multi-board project [here](multiboard.md).
+
+**Types**: auto, rectangle, annotation
+
+#### Auto
+
+Find all board edges and use them to construct source rectangle. Suitable for
+most cases when there is only a single board in the design. Note that might want
+to increase `tolerance` or specify the source area explicitly via `rectangle` if
+you have components sticking out of your design.
+
+- `tolerance`: KiKit extracts only board items that fit fully into the source
+  area (including all drawings on all layers). Tolerance enlarges the source
+  area by given amount, to e.g., not omit KiKit annotations for tabs or
+  connectors sticking out of the board.
+
+#### Rectangle
+
+Specify the source rectangle explicitly.
+
+- `tlx, tly, brx, bry` - specify the coordinates (via length units) of the
+  rectangle via top-left and bottom-right corner.
+
+#### Annotation
+
+KiKit offers you to place an annotation footprint `kikit:Board` into your design
+file to name the board. The area is determined by a bounding box of the lines in
+the `Edge.Cuts` layer that the arrows point to. Note that the tip of the arrow
+must lie inside the
+
+- `ref` - specify the annotation symbol reference
+
+## Tabs
+
+**Types**: fixed, spacing, full, annotation
+
+Place tabs. To make some of the options clear, please see the [explanation of
+tab placement process](understandingTabs.md).
+
+#### Fixed
+
+Place given number of tabs on the PCB edge. The tabs are spaced uniformly. If
+you need a custom tab placement (e.g., to avoid critical feature), see type
+*annotation*.
+
+- `vwidth`, `hwidth`, `width`: The width of tabs in the vertical and horizontal
+  direction. `width` overrides both.
+- `vcount`, `hcount`: Number of tabs in a given direction.
+- `mindistance`: Minimal spacing between the tabs. If there are too many tabs,
+  their count is reduced.
+
+#### Spacing
+
+Place tabs on the PCB edges based on spacing.
+
+- `vwidth`, `hwidth`, `width`: The width of tabs in the vertical and horizontal
+  direction. `width` overrides both.
+- `spacing`: The maximum spacing of the tabs.
+
+#### Full
+
+Create tabs that are full width of the PCB. Suitable for PCBs separated by
+V-Cuts. This mode does not make much sense for mousebites in practice. Note that
+in this mode the cuts do not faithfully copy the PCB outline and, instead, they
+cut the bounding box of the PCB. There are no other options.
+
+#### Corner
+
+Create tabs in the corners of the PCB.
+
+- `width`: The width of tabs
+
+#### Annotation
+
+Add tabs based on PCB annotations. Place a footprint `kikit:Tab` at the edges of
+your PCB. You can edit the text field prefixed with `KIKIT: ` to adjust the tab
+parameters. There are no extra parameters. However, the individual tabs can have
+the following properties specified in the text field of the component:
+
+- `width`: width of the tab.
+
+## Cuts
+
+Specify how to perform the cuts on the tabs separating the board.
+
+**Types**: none, mousebites, vcuts
+
+#### None
+
+Do not perform any cuts
+
+#### Mousebites
+
+Use mousebites to
+
+- `drill` - specify drill size for the bites
+- `spacing` - specify the spacing of the holes
+- `offset` - specify the offset, positive offset puts the cuts into the board,
+  negative puts the cuts into the tabs
+- `prolong` - distance for tangential prolongation of the cuts (to cut through
+  the internal corner fillets caused by milling)
+
+#### V-Cuts
+
+- `clearance` - specify clearance for copper in V-cuts
+- `cutcurves` - true/false - specify if curves should be approximated by
+  straight cuts (e.g., for cutting tabs on circular boards)
+- `layer` - specify the layer to render V-cuts on.
+
+
+## Framing
+
+KiKit allows you to frame the panel with a full frame, or bottom/top or
+left/right rails.
+
+**Types**: none, railstb, railsrl, frame, tightframe
+**Common options**:
+
+- `hspace`, `vspace`, `space` - specify the space between PCB and the
+  frame/rail. `space` overrides `hspace and vspace`.
+- `width` - specify with of the rails or frame
+
+#### Railstb/Railsrl
+
+Add rail (either on top and bottom or on left and right) to the panel.
+
+#### Frame
+
+Add a frame around the board.
+
+- `cuts` - true/false - specify whether to add cuts to the corners of the frame
+  for easy removal.
+
+#### Tighframe
+
+Add a frame around the board which fills the whole area of the panel - the
+boards have just a milled slot around their perimeter.
+
+- `slotwidth` - width of the milled slot.
+
+## Tooling
+
+Add tooling holes to the (rail/frame of) the panel. The holes are positioned
+by
+
+**Types**: none, 3hole, 4hole
+**Common options**:
+
+- `hoffset`, `voffset` - specify the offset from from panel edges
+- `size` - diameter of the holes
+- `paste` - if true, the holes are included in the paste layer (therefore they
+  appear on the stencil).
+
+## Fiducials
+
+Add fiducial to the (rail/frame of) the panel.
+
+**Types**: none, 3fid, 4fid
+**Common options**:
+
+- `hoffset`, `voffset` - specify the offset from from panel edges
+- `coppersize`, `opening` - diameter of the copper spot and solder mask opening
+
+## Text
+
+Add text to the panel. Allows you to put a single block of text on panel. If you
+need more text or more sophisticated placing options, see `script` option from
+`postprocess`.
+
+**Types**: none, simple
+**Common options**:
+
+- `text` - The text to be displayed. Note that you can escape `;` via `\`
+- `anchor` - Origin of the text. Can be one of `tl`, `tr`, `bl`, `br` (corners),
+  `mt`, `mb`, `ml`, `mr` (middle of sides), `c` (center).
+- `hoffset`, `voffset` - specify the offset from anchor
+- `orientation` - specify the orientation (angle)
+- `width`, `height` - width and height of the text
+- `hjustify` - justification of the text. One of `left`, `right`, `center`
+- `vjustify` - justification of the text. One of `top`, `bottom`, `center`
+- `thickness` - stroke thickness
+- `layer` - specify text layer
+
+## Postprocess
+
+Finishing touches to the panel.
+
+**Types**: auto
+**Common options**:
+
+- `copperfill` - fill tabs and frame with copper (e.g., to save etchant or to
+  increase rigidity of flex-PCB panels)
+- `millradius` - simulate the milling operation (add fillets to the internal
+  corners). Specify mill radius (usually 1 mm). 0 radius disables the
+  functionality.
+- `script` - a path to custom Python file. The file should contain a function
+  `kikitPostprocess(panel, args)` that receives the prepared panel as the
+  `kikit.panelize.Panel` object and the user-supplied arguments as a string -
+  see `scriptarg`. The function can make arbitrary changes to the panel - you
+  can append text, footprints, alter labels, etc. The function is invoked after
+  the whole panel is constructed (including all other postprocessing). **If you
+  try to add a functionality for a common fabrication houses via scripting,
+  consider submitting PR for KiKit**.
+- `scriptarg`: An arbitrary string passed to the user post-processing script
+  specified in `script`
+- `origin` - specify if the auxilary origin an grid origin should be placed. Can
+  be one of `tl`, `tr`, `bl`, `br` (corners), `mt`, `mb`, `ml`, `mr` (middle of
+  sides), `c` (center). Empty string does not changes the origin.
+
+
diff --git a/doc/resources/conn-pcbnew.png b/doc/resources/conn-pcbnew.png
new file mode 100644
index 00000000..808b41e6
Binary files /dev/null and b/doc/resources/conn-pcbnew.png differ
diff --git a/doc/resources/conn.kicad_pcb b/doc/resources/conn.kicad_pcb
index 2118df6b..98920548 100644
--- a/doc/resources/conn.kicad_pcb
+++ b/doc/resources/conn.kicad_pcb
@@ -1,11 +1,11 @@
-(kicad_pcb (version 20171130) (host pcbnew 5.1.5+dfsg1-2build2)
+(kicad_pcb (version 20171130) (host pcbnew 5.1.9-73d0e3b20d~88~ubuntu20.04.1)
 
   (general
     (thickness 1.6)
-    (drawings 21)
+    (drawings 13)
     (tracks 156)
     (zones 0)
-    (modules 6)
+    (modules 10)
     (nets 11)
   )
 
@@ -127,6 +127,82 @@
     (add_net "Net-(J4-Pad6)")
   )
 
+  (module kikit:Tab (layer F.Cu) (tedit 60708B1A) (tstamp 60708B76)
+    (at 110.5 58.75 270)
+    (descr "A symbol representing annotation for tab placement")
+    (attr virtual)
+    (fp_text reference REF** (at 0 -2 90) (layer F.SilkS) hide
+      (effects (font (size 1 1) (thickness 0.15)))
+    )
+    (fp_text value Tab (at -3 -1 90) (layer Dwgs.User)
+      (effects (font (size 1.2 1.2) (thickness 0.2)))
+    )
+    (fp_line (start 0.25 1) (end 0.25 -1) (layer Dwgs.User) (width 0.3))
+    (fp_line (start 0.25 0) (end -2.75 0) (layer Dwgs.User) (width 0.3))
+    (fp_line (start 0.25 0) (end -0.75 -1) (layer Dwgs.User) (width 0.3))
+    (fp_line (start 0.25 0) (end -0.75 1) (layer Dwgs.User) (width 0.3))
+    (fp_text user "KIKIT: width: 3mm" (at -5.5 0) (layer Dwgs.User)
+      (effects (font (size 1 1) (thickness 0.15)))
+    )
+  )
+
+  (module kikit:Tab (layer F.Cu) (tedit 60708B1A) (tstamp 6053DFEC)
+    (at 105.25 73)
+    (descr "A symbol representing annotation for tab placement")
+    (attr virtual)
+    (fp_text reference REF** (at 0 -2) (layer F.SilkS) hide
+      (effects (font (size 1 1) (thickness 0.15)))
+    )
+    (fp_text value Tab (at -3 -1) (layer Dwgs.User)
+      (effects (font (size 1.2 1.2) (thickness 0.2)))
+    )
+    (fp_line (start 0.25 1) (end 0.25 -1) (layer Dwgs.User) (width 0.3))
+    (fp_line (start 0.25 0) (end -2.75 0) (layer Dwgs.User) (width 0.3))
+    (fp_line (start 0.25 0) (end -0.75 -1) (layer Dwgs.User) (width 0.3))
+    (fp_line (start 0.25 0) (end -0.75 1) (layer Dwgs.User) (width 0.3))
+    (fp_text user "KIKIT: width: 3mm" (at -5.5 0 90) (layer Dwgs.User)
+      (effects (font (size 1 1) (thickness 0.15)))
+    )
+  )
+
+  (module kikit:Tab (layer F.Cu) (tedit 60708B1A) (tstamp 6053DFDC)
+    (at 118.5 77.25 90)
+    (descr "A symbol representing annotation for tab placement")
+    (attr virtual)
+    (fp_text reference REF** (at 0 -2 90) (layer F.SilkS) hide
+      (effects (font (size 1 1) (thickness 0.15)))
+    )
+    (fp_text value Tab (at -3 -1 90) (layer Dwgs.User)
+      (effects (font (size 1.2 1.2) (thickness 0.2)))
+    )
+    (fp_text user "KIKIT: width: 3mm" (at -5.5 0) (layer Dwgs.User)
+      (effects (font (size 1 1) (thickness 0.15)))
+    )
+    (fp_line (start 0.25 0) (end -0.75 1) (layer Dwgs.User) (width 0.3))
+    (fp_line (start 0.25 0) (end -0.75 -1) (layer Dwgs.User) (width 0.3))
+    (fp_line (start 0.25 0) (end -2.75 0) (layer Dwgs.User) (width 0.3))
+    (fp_line (start 0.25 1) (end 0.25 -1) (layer Dwgs.User) (width 0.3))
+  )
+
+  (module kikit:Tab (layer F.Cu) (tedit 60708B1A) (tstamp 60708AE0)
+    (at 131.75 63 180)
+    (descr "A symbol representing annotation for tab placement")
+    (attr virtual)
+    (fp_text reference REF** (at 0 -2) (layer F.SilkS) hide
+      (effects (font (size 1 1) (thickness 0.15)))
+    )
+    (fp_text value Tab (at -3 -1) (layer Dwgs.User)
+      (effects (font (size 1.2 1.2) (thickness 0.2)))
+    )
+    (fp_text user "KIKIT: width: 3mm" (at -5.5 0 90) (layer Dwgs.User)
+      (effects (font (size 1 1) (thickness 0.15)))
+    )
+    (fp_line (start 0.25 0) (end -0.75 1) (layer Dwgs.User) (width 0.3))
+    (fp_line (start 0.25 0) (end -0.75 -1) (layer Dwgs.User) (width 0.3))
+    (fp_line (start 0.25 0) (end -2.75 0) (layer Dwgs.User) (width 0.3))
+    (fp_line (start 0.25 1) (end 0.25 -1) (layer Dwgs.User) (width 0.3))
+  )
+
   (module rofi:molex-505004-0812 (layer F.Cu) (tedit 5D6FB713) (tstamp 5D6FC0A4)
     (at 118.5 65.5 180)
     (path /5D6FDCDA)
@@ -176,9 +252,6 @@
     (fp_text value OUT (at 0 18.5 90) (layer F.Fab)
       (effects (font (size 1 1) (thickness 0.15)))
     )
-    (fp_text user %R (at 0 8 180) (layer F.Fab)
-      (effects (font (size 1 1) (thickness 0.15)))
-    )
     (fp_line (start -1.5 17.5) (end -1.5 -1.5) (layer F.CrtYd) (width 0.05))
     (fp_line (start 1.5 17.5) (end -1.5 17.5) (layer F.CrtYd) (width 0.05))
     (fp_line (start 1.5 -1.5) (end 1.5 17.5) (layer F.CrtYd) (width 0.05))
@@ -194,6 +267,9 @@
     (fp_line (start 1 -0.5) (end 1 17) (layer F.Fab) (width 0.1))
     (fp_line (start 0.5 -1) (end 1 -0.5) (layer F.Fab) (width 0.1))
     (fp_line (start -1 -1) (end 0.5 -1) (layer F.Fab) (width 0.1))
+    (fp_text user %R (at 0 8 180) (layer F.Fab)
+      (effects (font (size 1 1) (thickness 0.15)))
+    )
     (pad 9 thru_hole oval (at 0 16 90) (size 1.35 1.35) (drill 0.8) (layers *.Cu *.Mask)
       (net 9 9))
     (pad 8 thru_hole oval (at 0 14 90) (size 1.35 1.35) (drill 0.8) (layers *.Cu *.Mask)
@@ -305,11 +381,11 @@
     (fp_text value MountingHole (at 0 3.1) (layer F.Fab)
       (effects (font (size 1 1) (thickness 0.15)))
     )
+    (fp_circle (center 0 0) (end 2 0) (layer Cmts.User) (width 0.15))
+    (fp_circle (center 0 0) (end 2.25 0) (layer F.CrtYd) (width 0.05))
     (fp_text user %R (at 0.3 0) (layer F.Fab)
       (effects (font (size 1 1) (thickness 0.15)))
     )
-    (fp_circle (center 0 0) (end 2 0) (layer Cmts.User) (width 0.15))
-    (fp_circle (center 0 0) (end 2.25 0) (layer F.CrtYd) (width 0.05))
     (pad "" np_thru_hole circle (at 0 0) (size 2 2) (drill 2) (layers *.Cu *.Mask))
   )
 
@@ -338,22 +414,6 @@
       (net 7 7))
   )
 
-  (gr_text TabPosition (at 141 71) (layer Eco1.User) (tstamp 5EB19712)
-    (effects (font (size 1 1) (thickness 0.15)))
-  )
-  (gr_text TabPosition (at 96 64) (layer Eco1.User) (tstamp 5EB1970B)
-    (effects (font (size 1 1) (thickness 0.15)))
-  )
-  (gr_line (start 103 64) (end 107 64) (layer Eco1.User) (width 0.15) (tstamp 5EB196FD))
-  (gr_line (start 134 71) (end 130 71) (layer Eco1.User) (width 0.15))
-  (gr_text TabPosition (at 125 82) (layer Eco2.User) (tstamp 5EB196DD)
-    (effects (font (size 1 1) (thickness 0.15)))
-  )
-  (gr_text TabPosition (at 112 53) (layer Eco2.User)
-    (effects (font (size 1 1) (thickness 0.15)))
-  )
-  (gr_line (start 125 79) (end 125 76) (layer Eco2.User) (width 0.15) (tstamp 5EB196BA))
-  (gr_line (start 112 57) (end 112 60) (layer Eco2.User) (width 0.15))
   (gr_text "PogoPads\nv0.2 9/2019\nro.fi.muni.cz" (at 118.5 70.5) (layer B.SilkS)
     (effects (font (size 1.5 1.5) (thickness 0.3)) (justify mirror))
   )
diff --git a/doc/resources/conn.png b/doc/resources/conn.png
index fde2322d..6c3eb6ee 100644
Binary files a/doc/resources/conn.png and b/doc/resources/conn.png differ
diff --git a/doc/resources/examplePanel1.png b/doc/resources/examplePanel1.png
new file mode 100644
index 00000000..95eddc62
Binary files /dev/null and b/doc/resources/examplePanel1.png differ
diff --git a/doc/resources/examplePanel10.png b/doc/resources/examplePanel10.png
new file mode 100644
index 00000000..350ee935
Binary files /dev/null and b/doc/resources/examplePanel10.png differ
diff --git a/doc/resources/examplePanel11.png b/doc/resources/examplePanel11.png
new file mode 100644
index 00000000..d3266967
Binary files /dev/null and b/doc/resources/examplePanel11.png differ
diff --git a/doc/resources/examplePanel12.png b/doc/resources/examplePanel12.png
new file mode 100644
index 00000000..82950e22
Binary files /dev/null and b/doc/resources/examplePanel12.png differ
diff --git a/doc/resources/examplePanel13.png b/doc/resources/examplePanel13.png
new file mode 100644
index 00000000..588399a3
Binary files /dev/null and b/doc/resources/examplePanel13.png differ
diff --git a/doc/resources/examplePanel14.png b/doc/resources/examplePanel14.png
new file mode 100644
index 00000000..07413ff9
Binary files /dev/null and b/doc/resources/examplePanel14.png differ
diff --git a/doc/resources/examplePanel15.png b/doc/resources/examplePanel15.png
new file mode 100644
index 00000000..28a9be49
Binary files /dev/null and b/doc/resources/examplePanel15.png differ
diff --git a/doc/resources/examplePanel16.png b/doc/resources/examplePanel16.png
new file mode 100644
index 00000000..03af2bad
Binary files /dev/null and b/doc/resources/examplePanel16.png differ
diff --git a/doc/resources/examplePanel17.png b/doc/resources/examplePanel17.png
new file mode 100644
index 00000000..18c162f6
Binary files /dev/null and b/doc/resources/examplePanel17.png differ
diff --git a/doc/resources/examplePanel18.png b/doc/resources/examplePanel18.png
new file mode 100644
index 00000000..650c9e8b
Binary files /dev/null and b/doc/resources/examplePanel18.png differ
diff --git a/doc/resources/examplePanel19.png b/doc/resources/examplePanel19.png
new file mode 100644
index 00000000..be381607
Binary files /dev/null and b/doc/resources/examplePanel19.png differ
diff --git a/doc/resources/examplePanel2.png b/doc/resources/examplePanel2.png
new file mode 100644
index 00000000..c371c673
Binary files /dev/null and b/doc/resources/examplePanel2.png differ
diff --git a/doc/resources/examplePanel3.png b/doc/resources/examplePanel3.png
new file mode 100644
index 00000000..d557e834
Binary files /dev/null and b/doc/resources/examplePanel3.png differ
diff --git a/doc/resources/examplePanel4.png b/doc/resources/examplePanel4.png
new file mode 100644
index 00000000..4a691024
Binary files /dev/null and b/doc/resources/examplePanel4.png differ
diff --git a/doc/resources/examplePanel5.png b/doc/resources/examplePanel5.png
new file mode 100644
index 00000000..07ef9ff5
Binary files /dev/null and b/doc/resources/examplePanel5.png differ
diff --git a/doc/resources/examplePanel6.png b/doc/resources/examplePanel6.png
new file mode 100644
index 00000000..e3198eb9
Binary files /dev/null and b/doc/resources/examplePanel6.png differ
diff --git a/doc/resources/examplePanel7.png b/doc/resources/examplePanel7.png
new file mode 100644
index 00000000..7e2eabd7
Binary files /dev/null and b/doc/resources/examplePanel7.png differ
diff --git a/doc/resources/examplePanel8.png b/doc/resources/examplePanel8.png
new file mode 100644
index 00000000..972ca7d9
Binary files /dev/null and b/doc/resources/examplePanel8.png differ
diff --git a/doc/resources/examplePanel9.png b/doc/resources/examplePanel9.png
new file mode 100644
index 00000000..2ab567a9
Binary files /dev/null and b/doc/resources/examplePanel9.png differ
diff --git a/doc/resources/examplePost.py b/doc/resources/examplePost.py
new file mode 100644
index 00000000..1287a0dc
--- /dev/null
+++ b/doc/resources/examplePost.py
@@ -0,0 +1,7 @@
+from kikit.units import mm
+from pcbnew import wxPoint
+
+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
diff --git a/doc/resources/multiboard.kicad_pcb b/doc/resources/multiboard.kicad_pcb
new file mode 100644
index 00000000..a26be08a
--- /dev/null
+++ b/doc/resources/multiboard.kicad_pcb
@@ -0,0 +1,173 @@
+(kicad_pcb (version 20171130) (host pcbnew 5.1.9-73d0e3b20d~88~ubuntu20.04.1)
+
+  (general
+    (thickness 1.6)
+    (drawings 18)
+    (tracks 0)
+    (zones 0)
+    (modules 3)
+    (nets 1)
+  )
+
+  (page A4)
+  (layers
+    (0 F.Cu signal)
+    (31 B.Cu signal)
+    (32 B.Adhes user)
+    (33 F.Adhes user)
+    (34 B.Paste user)
+    (35 F.Paste user)
+    (36 B.SilkS user)
+    (37 F.SilkS user)
+    (38 B.Mask user)
+    (39 F.Mask user)
+    (40 Dwgs.User user)
+    (41 Cmts.User user)
+    (42 Eco1.User user)
+    (43 Eco2.User user)
+    (44 Edge.Cuts user)
+    (45 Margin user)
+    (46 B.CrtYd user)
+    (47 F.CrtYd user)
+    (48 B.Fab user)
+    (49 F.Fab user)
+  )
+
+  (setup
+    (last_trace_width 0.25)
+    (trace_clearance 0.2)
+    (zone_clearance 0.508)
+    (zone_45_only no)
+    (trace_min 0.2)
+    (via_size 0.8)
+    (via_drill 0.4)
+    (via_min_size 0.4)
+    (via_min_drill 0.3)
+    (uvia_size 0.3)
+    (uvia_drill 0.1)
+    (uvias_allowed no)
+    (uvia_min_size 0.2)
+    (uvia_min_drill 0.1)
+    (edge_width 0.2)
+    (segment_width 0.2)
+    (pcb_text_width 0.3)
+    (pcb_text_size 1.5 1.5)
+    (mod_edge_width 0.12)
+    (mod_text_size 1 1)
+    (mod_text_width 0.15)
+    (pad_size 1.524 1.524)
+    (pad_drill 0.762)
+    (pad_to_mask_clearance 0)
+    (aux_axis_origin 0 0)
+    (visible_elements FFFFFF7F)
+    (pcbplotparams
+      (layerselection 0x010fc_ffffffff)
+      (usegerberextensions false)
+      (usegerberattributes true)
+      (usegerberadvancedattributes true)
+      (creategerberjobfile true)
+      (excludeedgelayer true)
+      (linewidth 0.150000)
+      (plotframeref false)
+      (viasonmask false)
+      (mode 1)
+      (useauxorigin false)
+      (hpglpennumber 1)
+      (hpglpenspeed 20)
+      (hpglpendiameter 15.000000)
+      (psnegative false)
+      (psa4output false)
+      (plotreference true)
+      (plotvalue true)
+      (plotinvisibletext false)
+      (padsonsilk false)
+      (subtractmaskfromsilk false)
+      (outputformat 1)
+      (mirror false)
+      (drillshape 1)
+      (scaleselection 1)
+      (outputdirectory ""))
+  )
+
+  (net 0 "")
+
+  (net_class Default "This is the default net class."
+    (clearance 0.2)
+    (trace_width 0.25)
+    (via_dia 0.8)
+    (via_drill 0.4)
+    (uvia_dia 0.3)
+    (uvia_drill 0.1)
+  )
+
+  (module kikit:Board (layer F.Cu) (tedit 605A1488) (tstamp 605A707C)
+    (at 185 90)
+    (descr "Mark board for extraction")
+    (fp_text reference B3 (at -4.5 -5) (layer Dwgs.User)
+      (effects (font (size 1 1) (thickness 0.15)) (justify left))
+    )
+    (fp_text value Board (at -7.25 -5) (layer Dwgs.User)
+      (effects (font (size 1 1) (thickness 0.15)))
+    )
+    (fp_line (start 0 0) (end -1.25 -4) (layer Dwgs.User) (width 0.2))
+    (fp_line (start -1.25 -4) (end -9.25 -4) (layer Dwgs.User) (width 0.2))
+    (fp_line (start 0 0) (end 0.5 -1) (layer Dwgs.User) (width 0.2))
+    (fp_line (start 0 0) (end -1 -0.5) (layer Dwgs.User) (width 0.2))
+  )
+
+  (module kikit:Board (layer F.Cu) (tedit 605A1488) (tstamp 605A706E)
+    (at 135 90)
+    (descr "Mark board for extraction")
+    (fp_text reference B2 (at -4.5 -5) (layer Dwgs.User)
+      (effects (font (size 1 1) (thickness 0.15)) (justify left))
+    )
+    (fp_text value Board (at -7.25 -5) (layer Dwgs.User)
+      (effects (font (size 1 1) (thickness 0.15)))
+    )
+    (fp_line (start 0 0) (end -1 -0.5) (layer Dwgs.User) (width 0.2))
+    (fp_line (start 0 0) (end 0.5 -1) (layer Dwgs.User) (width 0.2))
+    (fp_line (start -1.25 -4) (end -9.25 -4) (layer Dwgs.User) (width 0.2))
+    (fp_line (start 0 0) (end -1.25 -4) (layer Dwgs.User) (width 0.2))
+  )
+
+  (module kikit:Board (layer F.Cu) (tedit 605A1488) (tstamp 605A7057)
+    (at 100 90)
+    (descr "Mark board for extraction")
+    (fp_text reference B1 (at -4.5 -5) (layer Dwgs.User)
+      (effects (font (size 1 1) (thickness 0.15)) (justify left))
+    )
+    (fp_text value Board (at -7.25 -5) (layer Dwgs.User)
+      (effects (font (size 1 1) (thickness 0.15)))
+    )
+    (fp_line (start 0 0) (end -1.25 -4) (layer Dwgs.User) (width 0.2))
+    (fp_line (start -1.25 -4) (end -9.25 -4) (layer Dwgs.User) (width 0.2))
+    (fp_line (start 0 0) (end 0.5 -1) (layer Dwgs.User) (width 0.2))
+    (fp_line (start 0 0) (end -1 -0.5) (layer Dwgs.User) (width 0.2))
+  )
+
+  (gr_text C (at 185 100) (layer F.SilkS) (tstamp 605A709F)
+    (effects (font (size 5 5) (thickness 1)))
+  )
+  (gr_text B (at 145 100) (layer F.SilkS) (tstamp 605A709D)
+    (effects (font (size 5 5) (thickness 1)))
+  )
+  (gr_text A (at 100 100) (layer F.SilkS)
+    (effects (font (size 5 5) (thickness 1)))
+  )
+  (gr_line (start 200 100) (end 190 100) (layer Edge.Cuts) (width 0.2) (tstamp 605A142A))
+  (gr_line (start 200 105) (end 200 100) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 190 105) (end 200 105) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 190 100) (end 190 105) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 210 90) (end 180 90) (layer Edge.Cuts) (width 0.2) (tstamp 605A1425))
+  (gr_line (start 210 110) (end 210 90) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 180 110) (end 210 110) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 180 90) (end 180 110) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 150 110) (end 135 110) (layer Edge.Cuts) (width 0.2) (tstamp 605A1415))
+  (gr_line (start 150 100) (end 150 110) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 160 100) (end 150 100) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 160 90) (end 160 100) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 135 90) (end 160 90) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 135 110) (end 135 90) (layer Edge.Cuts) (width 0.2))
+  (gr_circle (center 100 100) (end 110 100) (layer Edge.Cuts) (width 0.2))
+
+)
diff --git a/doc/resources/multiboardpcb.jpg b/doc/resources/multiboardpcb.jpg
new file mode 100644
index 00000000..1d20b4c8
Binary files /dev/null and b/doc/resources/multiboardpcb.jpg differ
diff --git a/doc/resources/multiboardsch.jpg b/doc/resources/multiboardsch.jpg
new file mode 100644
index 00000000..a78d2f5b
Binary files /dev/null and b/doc/resources/multiboardsch.jpg differ
diff --git a/doc/resources/panel1.png b/doc/resources/panel1.png
deleted file mode 100644
index 0beea71a..00000000
Binary files a/doc/resources/panel1.png and /dev/null differ
diff --git a/doc/resources/panel10.png b/doc/resources/panel10.png
deleted file mode 100644
index d34c688d..00000000
Binary files a/doc/resources/panel10.png and /dev/null differ
diff --git a/doc/resources/panel11.png b/doc/resources/panel11.png
deleted file mode 100644
index ded025b3..00000000
Binary files a/doc/resources/panel11.png and /dev/null differ
diff --git a/doc/resources/panel12.png b/doc/resources/panel12.png
deleted file mode 100644
index 6dd36335..00000000
Binary files a/doc/resources/panel12.png and /dev/null differ
diff --git a/doc/resources/panel2.png b/doc/resources/panel2.png
deleted file mode 100644
index 36f0fc3a..00000000
Binary files a/doc/resources/panel2.png and /dev/null differ
diff --git a/doc/resources/panel3.png b/doc/resources/panel3.png
deleted file mode 100644
index bdb8a49f..00000000
Binary files a/doc/resources/panel3.png and /dev/null differ
diff --git a/doc/resources/panel4.png b/doc/resources/panel4.png
deleted file mode 100644
index 0c8ab14e..00000000
Binary files a/doc/resources/panel4.png and /dev/null differ
diff --git a/doc/resources/panel5.png b/doc/resources/panel5.png
deleted file mode 100644
index 6f29b400..00000000
Binary files a/doc/resources/panel5.png and /dev/null differ
diff --git a/doc/resources/panel6.png b/doc/resources/panel6.png
deleted file mode 100644
index 79a6ab8f..00000000
Binary files a/doc/resources/panel6.png and /dev/null differ
diff --git a/doc/resources/panel7.png b/doc/resources/panel7.png
deleted file mode 100644
index 8d5c0577..00000000
Binary files a/doc/resources/panel7.png and /dev/null differ
diff --git a/doc/resources/panel8.png b/doc/resources/panel8.png
deleted file mode 100644
index 4fbd479b..00000000
Binary files a/doc/resources/panel8.png and /dev/null differ
diff --git a/doc/resources/panel9.png b/doc/resources/panel9.png
deleted file mode 100644
index ebbbf8f4..00000000
Binary files a/doc/resources/panel9.png and /dev/null differ
diff --git a/doc/resources/partition1.svg b/doc/resources/partition1.svg
new file mode 100644
index 00000000..6c30a1e1
--- /dev/null
+++ b/doc/resources/partition1.svg
@@ -0,0 +1,259 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="137.82373mm"
+   height="58.673637mm"
+   viewBox="0 0 137.82373 58.673637"
+   version="1.1"
+   id="svg8"
+   sodipodi:docname="partition1.svg"
+   inkscape:version="1.0.2 (1.0.2+r75+1)"
+   inkscape:export-filename="/home/xmrazek7/projects/kikit/doc/resources/tabdrawing3.png"
+   inkscape:export-xdpi="300"
+   inkscape:export-ydpi="300">
+  <defs
+     id="defs2">
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path857"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Mend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Mend"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path863"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.4,0,0,-0.4,-4,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-6"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-1"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-6-0"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-1-2"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-3"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-7"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-5"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-9"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1.373429"
+     inkscape:cx="305.44307"
+     inkscape:cy="211.3196"
+     inkscape:document-units="mm"
+     inkscape:current-layer="g2855"
+     showgrid="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1042"
+     inkscape:window-x="0"
+     inkscape:window-y="18"
+     inkscape:window-maximized="0"
+     fit-margin-top="5"
+     fit-margin-left="30"
+     fit-margin-right="30"
+     fit-margin-bottom="5"
+     inkscape:document-rotation="0">
+    <inkscape:grid
+       type="xygrid"
+       id="grid815"
+       originx="-20.341823"
+       originy="-555.66589" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(9.8860587,-135.36166)">
+    <g
+       id="g2855"
+       transform="translate(14.552083)">
+      <text
+         id="text823-6-9"
+         y="165.69102"
+         x="89.637901"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05556px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
+         xml:space="preserve"><tspan
+           style="font-size:3.52778px;stroke-width:0.264583"
+           y="165.69102"
+           x="89.637901"
+           sodipodi:role="line"
+           id="tspan7591">Partition Line</tspan></text>
+      <rect
+         style="fill:#8ecade;fill-rule:evenodd;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+         id="rect7526"
+         width="29.104166"
+         height="18.520834"
+         x="-10.384131"
+         y="143.25827" />
+      <rect
+         style="fill:#8ecade;fill-rule:evenodd;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+         id="rect7526-1"
+         width="29.104166"
+         height="18.520834"
+         x="24.011703"
+         y="143.25827" />
+      <rect
+         style="fill:#8ecade;fill-rule:evenodd;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+         id="rect7526-2"
+         width="29.104166"
+         height="18.520834"
+         x="58.407539"
+         y="143.25827" />
+      <rect
+         style="fill:#8ecade;fill-rule:evenodd;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+         id="rect7526-7"
+         width="29.104166"
+         height="18.520834"
+         x="-10.384128"
+         y="167.07077" />
+      <rect
+         style="fill:#8ecade;fill-rule:evenodd;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+         id="rect7526-1-0"
+         width="29.104166"
+         height="18.520834"
+         x="24.011705"
+         y="167.07077" />
+      <rect
+         style="fill:#8ecade;fill-rule:evenodd;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+         id="rect7526-2-9"
+         width="29.104166"
+         height="18.520834"
+         x="58.407543"
+         y="167.07077" />
+      <path
+         style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="M -10.384131,164.42493 H 87.511701"
+         id="path7585" />
+      <path
+         style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="M 21.365869,143.25826 V 185.5916"
+         id="path7587" />
+      <path
+         style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="M 55.761701,143.25826 V 185.5916"
+         id="path7589" />
+    </g>
+  </g>
+</svg>
diff --git a/doc/resources/partition2.svg b/doc/resources/partition2.svg
new file mode 100644
index 00000000..090946e7
--- /dev/null
+++ b/doc/resources/partition2.svg
@@ -0,0 +1,256 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="137.82373mm"
+   height="58.673637mm"
+   viewBox="0 0 137.82373 58.673637"
+   version="1.1"
+   id="svg8"
+   sodipodi:docname="partition2.svg"
+   inkscape:version="1.0.2 (1.0.2+r75+1)"
+   inkscape:export-filename="/home/xmrazek7/projects/kikit/doc/resources/tabdrawing3.png"
+   inkscape:export-xdpi="300"
+   inkscape:export-ydpi="300">
+  <defs
+     id="defs2">
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path857"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Mend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Mend"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path863"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.4,0,0,-0.4,-4,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-6"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-1"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-6-0"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-1-2"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-3"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-7"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-5"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-9"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1.373429"
+     inkscape:cx="305.44307"
+     inkscape:cy="211.3196"
+     inkscape:document-units="mm"
+     inkscape:current-layer="g2855"
+     showgrid="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1042"
+     inkscape:window-x="0"
+     inkscape:window-y="18"
+     inkscape:window-maximized="0"
+     fit-margin-top="5"
+     fit-margin-left="30"
+     fit-margin-right="30"
+     fit-margin-bottom="5"
+     inkscape:document-rotation="0">
+    <inkscape:grid
+       type="xygrid"
+       id="grid815"
+       originx="-20.341823"
+       originy="-555.66589" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(9.8860587,-135.36166)">
+    <g
+       id="g2855"
+       transform="translate(14.552083)">
+      <text
+         id="text823-6-9"
+         y="165.69102"
+         x="89.637901"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05556px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
+         xml:space="preserve"><tspan
+           style="font-size:3.52778px;stroke-width:0.264583"
+           y="165.69102"
+           x="89.637901"
+           sodipodi:role="line"
+           id="tspan7591">Partition Line</tspan></text>
+      <rect
+         style="fill:#8ecade;fill-rule:evenodd;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+         id="rect7526"
+         width="29.104166"
+         height="18.520834"
+         x="-10.384131"
+         y="143.25827" />
+      <rect
+         style="fill:#8ecade;fill-rule:evenodd;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+         id="rect7526-1"
+         width="29.104166"
+         height="42.333347"
+         x="24.011703"
+         y="143.25827" />
+      <rect
+         style="fill:#8ecade;fill-rule:evenodd;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+         id="rect7526-2"
+         width="29.104166"
+         height="18.520834"
+         x="58.407539"
+         y="143.25827" />
+      <rect
+         style="fill:#8ecade;fill-rule:evenodd;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+         id="rect7526-7"
+         width="29.104166"
+         height="18.520834"
+         x="-10.384128"
+         y="167.07077" />
+      <rect
+         style="fill:#8ecade;fill-rule:evenodd;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+         id="rect7526-2-9"
+         width="29.104166"
+         height="18.520834"
+         x="58.407543"
+         y="167.07077" />
+      <path
+         style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="M 21.365869,143.25826 V 185.5916"
+         id="path7587" />
+      <path
+         style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="M 55.761701,143.25826 V 185.5916"
+         id="path7589" />
+      <path
+         style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+         d="m 21.365868,164.42494 h -31.75"
+         id="path7613" />
+      <path
+         style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+         d="M 55.761701,164.42494 H 87.5117"
+         id="path7615" />
+    </g>
+  </g>
+</svg>
diff --git a/doc/resources/tabdrawing.svg b/doc/resources/tabdrawing.svg
new file mode 100644
index 00000000..d946d728
--- /dev/null
+++ b/doc/resources/tabdrawing.svg
@@ -0,0 +1,418 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="210mm"
+   height="297mm"
+   viewBox="0 0 210 297"
+   version="1.1"
+   id="svg8"
+   sodipodi:docname="tabdrawing.svg"
+   inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
+  <defs
+     id="defs2">
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0.0"
+       refX="0.0"
+       id="Arrow1Lend"
+       style="overflow:visible;"
+       inkscape:isstock="true">
+      <path
+         id="path857"
+         d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
+         style="fill-rule:evenodd;stroke:#da98cf;stroke-width:1pt;stroke-opacity:1;fill:#da98cf;fill-opacity:1"
+         transform="scale(0.8) rotate(180) translate(12.5,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Mend"
+       orient="auto"
+       refY="0.0"
+       refX="0.0"
+       id="Arrow1Mend"
+       style="overflow:visible;"
+       inkscape:isstock="true">
+      <path
+         id="path863"
+         d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1"
+         transform="scale(0.4) rotate(180) translate(10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-6"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-1"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-6-0"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-1-2"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-3"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-7"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-5"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-9"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.7"
+     inkscape:cx="564.52937"
+     inkscape:cy="595.6568"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1042"
+     inkscape:window-x="0"
+     inkscape:window-y="18"
+     inkscape:window-maximized="0">
+    <inkscape:grid
+       type="xygrid"
+       id="grid815" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <g
+       id="g2825">
+      <path
+         inkscape:connector-curvature="0"
+         id="path819"
+         d="m 27.78125,17.864583 18.520833,-7.9374998 14.552083,7.9374998 5.291666,9.260418 31.75,-10.583335"
+         style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+      <text
+         id="text823"
+         y="21.455357"
+         x="19.938242"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="21.455357"
+           x="19.938242"
+           id="tspan821"
+           sodipodi:role="line">Complicated PCB edge</tspan></text>
+    </g>
+    <g
+       id="g2855"
+       transform="translate(14.552083)">
+      <path
+         inkscape:connector-curvature="0"
+         id="path819-3-2"
+         d="m 13.229167,176.61459 18.520832,-7.9375 14.552083,7.9375 5.291667,9.26042 31.75,-10.58334"
+         style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+      <text
+         id="text823-6-9"
+         y="180.20537"
+         x="5.3861585"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="180.20537"
+           x="5.3861585"
+           id="tspan821-7-1"
+           sodipodi:role="line">Complicated PCB edge</tspan></text>
+      <circle
+         r="1.3229166"
+         cy="146.1875"
+         cx="44.979168"
+         id="path846-2"
+         style="fill:#f2ca79;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <text
+         id="text850-7"
+         y="142.88692"
+         x="45.860886"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="142.88692"
+           x="45.860886"
+           id="tspan848-0"
+           sodipodi:role="line">Tab origin</tspan></text>
+      <path
+         inkscape:connector-curvature="0"
+         id="path852-9"
+         d="m 44.979165,146.1875 v 42.33334"
+         style="fill:none;stroke:#da98cf;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-5)" />
+      <text
+         id="text850-5-3"
+         y="156.77083"
+         x="61.37595"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="156.77083"
+           x="61.37595"
+           id="tspan848-3-6"
+           sodipodi:role="line">Lef ray</tspan></text>
+      <path
+         inkscape:connector-curvature="0"
+         id="path852-9-2"
+         d="m 30.427083,146.1875 v 42.33334"
+         style="fill:none;stroke:#da98cf;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-5-0)" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path852-9-2-8"
+         d="m 59.53125,146.1875 v 42.33334"
+         style="fill:none;stroke:#da98cf;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-5-0-6)" />
+      <text
+         id="text850-5-3-7"
+         y="156.89104"
+         x="15.888432"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="156.89104"
+           x="15.888432"
+           id="tspan848-3-6-9"
+           sodipodi:role="line">Righ ray</tspan></text>
+      <circle
+         r="2.6458333"
+         cy="169.21164"
+         cx="30.440514"
+         id="path2113"
+         style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ea6e6f;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <circle
+         r="2.6458333"
+         cy="183.23456"
+         cx="59.544682"
+         id="path2113-2"
+         style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ea6e6f;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    </g>
+    <g
+       id="g2869"
+       transform="translate(21.166665)">
+      <path
+         inkscape:connector-curvature="0"
+         id="path2723"
+         d="m 23.8125,224.23958 h 29.104166 v 37.04167 l -7.9375,2.64583 -5.291666,-9.26042 -14.552084,-7.9375 -1.322916,0.52917 z"
+         style="fill:#bfda70;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path819-3-2-2"
+         d="m 6.6145848,254.66667 18.5208322,-7.9375 14.552083,7.9375 5.291667,9.26042 31.75,-10.58334"
+         style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+      <text
+         id="text823-6-9-2"
+         y="258.25748"
+         x="-1.2284237"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="258.25748"
+           x="-1.2284237"
+           id="tspan821-7-1-8"
+           sodipodi:role="line">Complicated PCB edge</tspan></text>
+      <circle
+         r="1.3229166"
+         cy="224.23958"
+         cx="38.364586"
+         id="path846-2-9"
+         style="fill:#f2ca79;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <text
+         id="text850-7-7"
+         y="220.939"
+         x="39.246304"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="220.939"
+           x="39.246304"
+           id="tspan848-0-3"
+           sodipodi:role="line">Tab origin</tspan></text>
+      <path
+         sodipodi:nodetypes="ccccc"
+         inkscape:connector-curvature="0"
+         id="path2721"
+         d="m 23.8125,247.25833 1.322917,-0.52916 14.552083,7.93749 5.291666,9.26042 7.9375,-2.64583"
+         style="fill:none;stroke:#6887cd;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <text
+         id="text2763"
+         y="238.21718"
+         x="28.155291"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="238.21718"
+           x="28.155291"
+           id="tspan2761"
+           sodipodi:role="line">Tab substrate</tspan></text>
+      <text
+         id="text823-6-9-2-8"
+         y="268.27386"
+         x="27.59226"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="268.27386"
+           x="27.59226"
+           id="tspan821-7-1-8-4"
+           sodipodi:role="line">Cut produced by the tab</tspan></text>
+    </g>
+    <g
+       id="g2838"
+       transform="translate(18.520833)">
+      <path
+         inkscape:connector-curvature="0"
+         id="path819-3"
+         d="m 9.2604167,94.593758 18.5208323,-7.9375 14.552083,7.9375 5.291667,9.260422 31.75,-10.583339"
+         style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+      <text
+         id="text823-6"
+         y="98.184532"
+         x="1.4174085"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="98.184532"
+           x="1.4174085"
+           id="tspan821-7"
+           sodipodi:role="line">Complicated PCB edge</tspan></text>
+      <circle
+         r="1.3229166"
+         cy="64.166664"
+         cx="41.010418"
+         id="path846"
+         style="fill:#f2ca79;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <text
+         id="text850"
+         y="60.866089"
+         x="41.892136"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="60.866089"
+           x="41.892136"
+           id="tspan848"
+           sodipodi:role="line">Tab origin</tspan></text>
+      <path
+         inkscape:connector-curvature="0"
+         id="path852"
+         d="M 41.010416,64.166667 V 102.53125"
+         style="fill:none;stroke:#da98cf;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend)" />
+      <text
+         id="text850-5"
+         y="78.224503"
+         x="43.910088"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="78.224503"
+           x="43.910088"
+           id="tspan848-3"
+           sodipodi:role="line">Tab direction</tspan></text>
+      <text
+         id="text850-5-5"
+         y="82.760216"
+         x="44.099075"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="82.760216"
+           x="44.099075"
+           id="tspan848-3-0"
+           sodipodi:role="line">(the line you specify</tspan></text>
+    </g>
+  </g>
+</svg>
diff --git a/doc/resources/tabdrawing1.svg b/doc/resources/tabdrawing1.svg
new file mode 100644
index 00000000..0905af86
--- /dev/null
+++ b/doc/resources/tabdrawing1.svg
@@ -0,0 +1,210 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="137.82373mm"
+   height="27.504622mm"
+   viewBox="0 0 137.82373 27.504622"
+   version="1.1"
+   id="svg8"
+   sodipodi:docname="tabdrawing1.svg"
+   inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
+   inkscape:export-filename="/home/xmrazek7/projects/kikit/doc/resources/tabdrawing1.png"
+   inkscape:export-xdpi="300"
+   inkscape:export-ydpi="300">
+  <defs
+     id="defs2">
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path857"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Mend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Mend"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path863"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.4,0,0,-0.4,-4,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-6"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-1"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-6-0"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-1-2"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-3"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-7"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-5"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-9"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.7"
+     inkscape:cx="152.60829"
+     inkscape:cy="-404.8416"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1042"
+     inkscape:window-x="0"
+     inkscape:window-y="18"
+     inkscape:window-maximized="0"
+     fit-margin-top="5"
+     fit-margin-left="30"
+     fit-margin-right="30"
+     fit-margin-bottom="5">
+    <inkscape:grid
+       type="xygrid"
+       id="grid815"
+       originx="9.8860595"
+       originy="-264.7152" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(9.8860582,-4.7801785)">
+    <g
+       id="g2825">
+      <path
+         inkscape:connector-curvature="0"
+         id="path819"
+         d="m 27.78125,17.864583 18.520833,-7.9374998 14.552083,7.9374998 5.291666,9.260418 31.75,-10.583335"
+         style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+      <text
+         id="text823"
+         y="21.455357"
+         x="19.938242"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="21.455357"
+           x="19.938242"
+           id="tspan821"
+           sodipodi:role="line">Complicated PCB edge</tspan></text>
+    </g>
+  </g>
+</svg>
diff --git a/doc/resources/tabdrawing2.svg b/doc/resources/tabdrawing2.svg
new file mode 100644
index 00000000..7eba2849
--- /dev/null
+++ b/doc/resources/tabdrawing2.svg
@@ -0,0 +1,255 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="137.82373mm"
+   height="55.673145mm"
+   viewBox="0 0 137.82373 55.673145"
+   version="1.1"
+   id="svg8"
+   sodipodi:docname="tabdrawing2.svg"
+   inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
+   inkscape:export-filename="/home/xmrazek7/projects/kikit/doc/resources/tabdrawing2.png"
+   inkscape:export-xdpi="300"
+   inkscape:export-ydpi="300">
+  <defs
+     id="defs2">
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path857"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Mend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Mend"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path863"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.4,0,0,-0.4,-4,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-6"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-1"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-6-0"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-1-2"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-3"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-7"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-5"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-9"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.7"
+     inkscape:cx="95.484725"
+     inkscape:cy="-143.91137"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1042"
+     inkscape:window-x="0"
+     inkscape:window-y="18"
+     inkscape:window-maximized="0"
+     fit-margin-top="5"
+     fit-margin-left="30"
+     fit-margin-right="30"
+     fit-margin-bottom="5">
+    <inkscape:grid
+       type="xygrid"
+       id="grid815"
+       originx="-5.2278825"
+       originy="-452.70124" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(9.8860587,-53.340834)">
+    <g
+       id="g2838"
+       transform="translate(18.520833)">
+      <path
+         inkscape:connector-curvature="0"
+         id="path819-3"
+         d="m 9.2604167,94.593758 18.5208323,-7.9375 14.552083,7.9375 5.291667,9.260422 31.75,-10.583339"
+         style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+      <text
+         id="text823-6"
+         y="98.184532"
+         x="1.4174085"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="98.184532"
+           x="1.4174085"
+           id="tspan821-7"
+           sodipodi:role="line">Complicated PCB edge</tspan></text>
+      <circle
+         r="1.3229166"
+         cy="64.166664"
+         cx="41.010418"
+         id="path846"
+         style="fill:#f2ca79;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <text
+         id="text850"
+         y="60.866089"
+         x="41.892136"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="60.866089"
+           x="41.892136"
+           id="tspan848"
+           sodipodi:role="line">Tab origin</tspan></text>
+      <path
+         inkscape:connector-curvature="0"
+         id="path852"
+         d="M 41.010416,64.166667 V 102.53125"
+         style="fill:none;stroke:#da98cf;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend)" />
+      <text
+         id="text850-5"
+         y="78.224503"
+         x="43.910088"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="78.224503"
+           x="43.910088"
+           id="tspan848-3"
+           sodipodi:role="line">Tab direction</tspan></text>
+      <text
+         id="text850-5-5"
+         y="82.760216"
+         x="44.099075"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="82.760216"
+           x="44.099075"
+           id="tspan848-3-0"
+           sodipodi:role="line">(the line you specify</tspan></text>
+    </g>
+  </g>
+</svg>
diff --git a/doc/resources/tabdrawing3.svg b/doc/resources/tabdrawing3.svg
new file mode 100644
index 00000000..7f969207
--- /dev/null
+++ b/doc/resources/tabdrawing3.svg
@@ -0,0 +1,277 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="137.82373mm"
+   height="58.673637mm"
+   viewBox="0 0 137.82373 58.673637"
+   version="1.1"
+   id="svg8"
+   sodipodi:docname="tabdrawing3.svg"
+   inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
+   inkscape:export-filename="/home/xmrazek7/projects/kikit/doc/resources/tabdrawing3.png"
+   inkscape:export-xdpi="300"
+   inkscape:export-ydpi="300">
+  <defs
+     id="defs2">
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path857"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Mend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Mend"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path863"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.4,0,0,-0.4,-4,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-6"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-1"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-6-0"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-1-2"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-3"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-7"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-5"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-9"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.7"
+     inkscape:cx="38.361165"
+     inkscape:cy="209.78787"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1042"
+     inkscape:window-x="0"
+     inkscape:window-y="18"
+     inkscape:window-maximized="0"
+     fit-margin-top="5"
+     fit-margin-left="30"
+     fit-margin-right="30"
+     fit-margin-bottom="5">
+    <inkscape:grid
+       type="xygrid"
+       id="grid815"
+       originx="-20.341823"
+       originy="-555.66589" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(9.8860587,-135.36166)">
+    <g
+       id="g2855"
+       transform="translate(14.552083)">
+      <path
+         inkscape:connector-curvature="0"
+         id="path819-3-2"
+         d="m 13.229167,176.61459 18.520832,-7.9375 14.552083,7.9375 5.291667,9.26042 31.75,-10.58334"
+         style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+      <text
+         id="text823-6-9"
+         y="180.20537"
+         x="5.3861585"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="180.20537"
+           x="5.3861585"
+           id="tspan821-7-1"
+           sodipodi:role="line">Complicated PCB edge</tspan></text>
+      <circle
+         r="1.3229166"
+         cy="146.1875"
+         cx="44.979168"
+         id="path846-2"
+         style="fill:#f2ca79;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <text
+         id="text850-7"
+         y="142.88692"
+         x="45.860886"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="142.88692"
+           x="45.860886"
+           id="tspan848-0"
+           sodipodi:role="line">Tab origin</tspan></text>
+      <path
+         inkscape:connector-curvature="0"
+         id="path852-9"
+         d="m 44.979165,146.1875 v 42.33334"
+         style="fill:none;stroke:#da98cf;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-5)" />
+      <text
+         id="text850-5-3"
+         y="156.77083"
+         x="61.37595"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="156.77083"
+           x="61.37595"
+           id="tspan848-3-6"
+           sodipodi:role="line">Lef ray</tspan></text>
+      <path
+         inkscape:connector-curvature="0"
+         id="path852-9-2"
+         d="m 30.427083,146.1875 v 42.33334"
+         style="fill:none;stroke:#da98cf;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-5-0)" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path852-9-2-8"
+         d="m 59.53125,146.1875 v 42.33334"
+         style="fill:none;stroke:#da98cf;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-5-0-6)" />
+      <text
+         id="text850-5-3-7"
+         y="156.89104"
+         x="15.888432"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="156.89104"
+           x="15.888432"
+           id="tspan848-3-6-9"
+           sodipodi:role="line">Righ ray</tspan></text>
+      <circle
+         r="2.6458333"
+         cy="169.21164"
+         cx="30.440514"
+         id="path2113"
+         style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ea6e6f;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <circle
+         r="2.6458333"
+         cy="183.23456"
+         cx="59.544682"
+         id="path2113-2"
+         style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ea6e6f;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    </g>
+  </g>
+</svg>
diff --git a/doc/resources/tabdrawing4.svg b/doc/resources/tabdrawing4.svg
new file mode 100644
index 00000000..685f4855
--- /dev/null
+++ b/doc/resources/tabdrawing4.svg
@@ -0,0 +1,261 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="137.82373mm"
+   height="60.902542mm"
+   viewBox="0 0 137.82373 60.902542"
+   version="1.1"
+   id="svg8"
+   sodipodi:docname="tabdrawing4.svg"
+   inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
+   inkscape:export-filename="/home/xmrazek7/bitmap.png"
+   inkscape:export-xdpi="300"
+   inkscape:export-ydpi="300">
+  <defs
+     id="defs2">
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path857"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Mend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Mend"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path863"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.4,0,0,-0.4,-4,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-6"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-1"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-6-0"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-1-2"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-3"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-7"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-5"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-9"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1.00000003pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.7"
+     inkscape:cx="429.09475"
+     inkscape:cy="124.05412"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1042"
+     inkscape:window-x="0"
+     inkscape:window-y="18"
+     inkscape:window-maximized="0"
+     fit-margin-top="5"
+     fit-margin-left="30"
+     fit-margin-right="30"
+     fit-margin-bottom="5.3">
+    <inkscape:grid
+       type="xygrid"
+       id="grid815"
+       originx="-35.455763"
+       originy="-578.34964" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(9.8860589,-213.41374)">
+    <g
+       id="g2869"
+       transform="translate(21.166665)">
+      <path
+         inkscape:connector-curvature="0"
+         id="path2723"
+         d="m 23.8125,224.23958 h 29.104166 v 37.04167 l -7.9375,2.64583 -5.291666,-9.26042 -14.552084,-7.9375 -1.322916,0.52917 z"
+         style="fill:#bfda70;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path819-3-2-2"
+         d="m 6.6145848,254.66667 18.5208322,-7.9375 14.552083,7.9375 5.291667,9.26042 31.75,-10.58334"
+         style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+      <text
+         id="text823-6-9-2"
+         y="258.25748"
+         x="-1.2284237"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="258.25748"
+           x="-1.2284237"
+           id="tspan821-7-1-8"
+           sodipodi:role="line">Complicated PCB edge</tspan></text>
+      <circle
+         r="1.3229166"
+         cy="224.23958"
+         cx="38.364586"
+         id="path846-2-9"
+         style="fill:#f2ca79;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <text
+         id="text850-7-7"
+         y="220.939"
+         x="39.246304"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="220.939"
+           x="39.246304"
+           id="tspan848-0-3"
+           sodipodi:role="line">Tab origin</tspan></text>
+      <path
+         sodipodi:nodetypes="ccccc"
+         inkscape:connector-curvature="0"
+         id="path2721"
+         d="m 23.8125,247.25833 1.322917,-0.52916 14.552083,7.93749 5.291666,9.26042 7.9375,-2.64583"
+         style="fill:none;stroke:#6887cd;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <text
+         id="text2763"
+         y="238.21718"
+         x="28.155291"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="238.21718"
+           x="28.155291"
+           id="tspan2761"
+           sodipodi:role="line">Tab substrate</tspan></text>
+      <text
+         id="text823-6-9-2-8"
+         y="268.27386"
+         x="27.59226"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555534px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
+         xml:space="preserve"><tspan
+           style="font-size:3.52777767px;stroke-width:0.26458332"
+           y="268.27386"
+           x="27.59226"
+           id="tspan821-7-1-8-4"
+           sodipodi:role="line">Cut produced by the tab</tspan></text>
+    </g>
+  </g>
+</svg>
diff --git a/doc/resources/tabdrawing5.svg b/doc/resources/tabdrawing5.svg
new file mode 100644
index 00000000..d3dc548f
--- /dev/null
+++ b/doc/resources/tabdrawing5.svg
@@ -0,0 +1,318 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="137.82373mm"
+   height="70mm"
+   viewBox="0 0 137.82373 70"
+   version="1.1"
+   id="svg8"
+   sodipodi:docname="tabdrawing5.svg"
+   inkscape:version="1.0.2 (1.0.2+r75+1)"
+   inkscape:export-filename="/home/xmrazek7/bitmap.png"
+   inkscape:export-xdpi="300"
+   inkscape:export-ydpi="300">
+  <defs
+     id="defs2">
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path857"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Mend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Mend"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         id="path863"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1"
+         transform="matrix(-0.4,0,0,-0.4,-4,0)"
+         inkscape:connector-curvature="0" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-6"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-1"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-6-0"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-1-2"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-3"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-7"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-5"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-9"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-36"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-75"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0"
+       refX="0"
+       id="Arrow1Lend-5-0-36-3"
+       style="overflow:visible"
+       inkscape:isstock="true">
+      <path
+         inkscape:connector-curvature="0"
+         id="path857-6-6-75-5"
+         d="M 0,0 5,-5 -12.5,0 5,5 Z"
+         style="fill:#da98cf;fill-opacity:1;fill-rule:evenodd;stroke:#da98cf;stroke-width:1pt;stroke-opacity:1"
+         transform="matrix(-0.8,0,0,-0.8,-10,0)" />
+    </marker>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1.3611027"
+     inkscape:cx="163.46609"
+     inkscape:cy="153.18311"
+     inkscape:document-units="mm"
+     inkscape:current-layer="g2869"
+     showgrid="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1042"
+     inkscape:window-x="0"
+     inkscape:window-y="18"
+     inkscape:window-maximized="0"
+     fit-margin-top="5"
+     fit-margin-left="30"
+     fit-margin-right="30"
+     fit-margin-bottom="5.3"
+     inkscape:document-rotation="0"
+     showguides="false">
+    <inkscape:grid
+       type="xygrid"
+       id="grid815"
+       originx="-35.455763"
+       originy="-578.34964" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(9.8860589,-213.41374)">
+    <g
+       id="g2869"
+       transform="translate(21.166665)">
+      <path
+         inkscape:connector-curvature="0"
+         id="path2723"
+         d="m 23.449846,225.085 29.104167,17.19792 V 272.71 l -7.574847,1.80048 -5.291666,-9.26042 -14.552084,-7.9375 -1.68557,0.84536 z"
+         style="fill:#bfda70;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         sodipodi:nodetypes="cccccccc" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path819-3-2-2"
+         d="m 6.6145848,265.25007 18.5208322,-7.9375 14.552083,7.9375 5.291667,9.26042 31.75,-10.58334"
+         style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+      <text
+         id="text823-6-9-2"
+         y="268.84097"
+         x="-1.2284237"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05556px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
+         xml:space="preserve"><tspan
+           style="font-size:3.52778px;stroke-width:0.264583"
+           y="268.84097"
+           x="-1.2284237"
+           id="tspan821-7-1-8"
+           sodipodi:role="line">Complicated PCB edge</tspan></text>
+      <circle
+         r="1.3229166"
+         cy="234.82277"
+         cx="38.364586"
+         id="path846-2-9"
+         style="fill:#f2ca79;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <text
+         id="text850-7-7"
+         y="230.45964"
+         x="35.242928"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05556px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
+         xml:space="preserve"><tspan
+           style="font-size:3.52778px;stroke-width:0.264583"
+           y="230.45964"
+           x="35.242928"
+           id="tspan848-0-3"
+           sodipodi:role="line">Tab origin</tspan></text>
+      <path
+         sodipodi:nodetypes="ccccc"
+         inkscape:connector-curvature="0"
+         id="path2721"
+         d="m 23.8125,257.84173 1.322917,-0.52916 14.552083,7.93749 5.291666,9.26042 7.9375,-2.64583"
+         style="fill:none;stroke:#6887cd;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <text
+         id="text2763"
+         y="248.80037"
+         x="28.155291"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05556px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
+         xml:space="preserve"><tspan
+           style="font-size:3.52778px;stroke-width:0.264583"
+           y="248.80037"
+           x="28.155291"
+           id="tspan2761"
+           sodipodi:role="line">Tab substrate</tspan></text>
+      <text
+         id="text823-6-9-2-8"
+         y="278.85736"
+         x="27.59226"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05556px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
+         xml:space="preserve"><tspan
+           style="font-size:3.52778px;stroke-width:0.264583"
+           y="278.85736"
+           x="27.59226"
+           id="tspan821-7-1-8-4"
+           sodipodi:role="line">Cut produced by the tab</tspan></text>
+      <text
+         id="text850-7-7-2"
+         y="226.58212"
+         x="0.83421552"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05556px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
+         xml:space="preserve"><tspan
+           style="font-size:3.52778px;stroke-width:0.264583"
+           y="226.58212"
+           x="0.83421552"
+           id="tspan848-0-3-9"
+           sodipodi:role="line">Partition line</tspan></text>
+    </g>
+    <path
+       inkscape:connector-curvature="0"
+       id="path852-9-2"
+       d="M 44.616511,258.15792 V 215.82458"
+       style="fill:none;stroke:#da98cf;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-5-0-36)" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path852-9-2-6"
+       d="M 73.720678,272.71001 V 230.37667"
+       style="fill:none;stroke:#da98cf;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-5-0-36-3)" />
+    <path
+       style="fill:none;stroke:#ea6e6f;stroke-width:0.75;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 34.033178,218.47042 46.302083,27.78125"
+       id="path7452"
+       sodipodi:nodetypes="cc" />
+  </g>
+</svg>
diff --git a/doc/understandingTabs.md b/doc/understandingTabs.md
new file mode 100644
index 00000000..33b7fdcc
--- /dev/null
+++ b/doc/understandingTabs.md
@@ -0,0 +1,63 @@
+# How are tabs in KiKit created?
+
+When you place multiple PCB into the panel, KiKit expects you to generate a
+so-called partition line for each substrate. Partition line is an oriented
+(poly)line that partitions the free space between the PCBs. It gives you the
+information "this part of the free space belongs to this substrate and this
+substrate is responsible for placing tabs in that space". So for a regular input the partition line can look like this:
+
+![partition1](resources/partition1.svg)
+
+For more complicated input, it can look like this:
+
+![partition2](resources/partition2.svg)
+
+Note several facts:
+- partition line is used for backbone generator
+- partition line is not generated automatically, it is up to the user to
+  generate it. KiKit offers `Panel.buildPartitionLineFromBB` that builds the
+  partition line based on bounding boxes. If you need possibly a more
+  complicated lines, you have to implement them by yourself.
+- partition line is used for deciding if an annotation yields a tab or not - if
+  the tab does not hit the partition line, it is not created.
+- when we create partition line from bounding boxes, we include "ghost
+  substrates" representing the framing, that will be added in the future.
+
+When KiKit generates a tab, it generates it based on tab origin, direction and
+optionally the partition line. When a tab is successfully generated, it consists
+out of two components - a piece of a substrate (which will be later appended to
+the panel) and a cut-line.
+
+So assume we have the following PCB outline (the PCB is below the line, there is
+a free space above the line):
+
+![tabdrawing1](resources/tabdrawing1.svg)
+
+Then you specify your tab origin and its direction:
+
+![tabdrawing2](resources/tabdrawing2.svg)
+
+This is your input (e.g., via an annotation). Now KiKit does its job; it shoots
+two rays `tabWidth` apart and looks for the intersection with existing
+substrates. Note that if the ray starts within the PCB, no intersection will be
+found.
+
+![tabdrawing3](resources/tabdrawing3.svg)
+
+Once we have the intersections, we can easily generate the tab substrate and the
+cut:
+
+![tabdrawing4](resources/tabdrawing4.svg)
+
+Note that if we specify a partition line, than we shoot new rays in the opposite
+direction and try to hit the line. If we manage to do so, we get a tab.
+Otherwise, no tab is generated.
+
+![tabdrawing5](resources/tabdrawing5.svg)
+
+This is the basic algorithm for generating tabs. Well, we might also call them
+"half tabs". KiKit usually generates the half tabs around the board bounding box
+and then expects that two half tabs in the middle of the panel will merge into a
+single one. Also, KiKit first generates all the tabs and then merges them in one
+step to the board substrate. The cut is just a polyline which is in later steps
+either rendered as a V-cut or via mousebites.
\ No newline at end of file
diff --git a/kikit/common.py b/kikit/common.py
index 15b8c183..ed6535b8 100644
--- a/kikit/common.py
+++ b/kikit/common.py
@@ -1,11 +1,15 @@
 from kikit.pcbnew_compatibility import pcbnew
+from kikit.intervals import Interval, AxialLine
 from pcbnew import wxPoint, wxRect
 import os
-from itertools import product, chain
+from itertools import product, chain, islice
 import numpy as np
+from shapely.geometry import LinearRing
 
 PKG_BASE = os.path.dirname(__file__)
 KIKIT_LIB = os.path.join(PKG_BASE, "resources/kikit.pretty")
+SHP_EPSILON = pcbnew.FromMM(0.01) # Common factor of enlarging substrates to
+                                  # cover up numerical imprecisions of Shapely
 
 def fromDegrees(angle):
     return angle * 10
@@ -56,7 +60,6 @@ def collectFootprints(boardCollection, sourceArea):
     """ Returns a list of board footprints fully contained in the source area ignoring reference a value label"""
     return list([x for x in boardCollection if fitsIn(x.GetFootprintRect(), sourceArea)])
 
-
 def getBBoxWithoutContours(edge):
     width = edge.GetWidth()
     edge.SetWidth(0)
@@ -64,6 +67,16 @@ def getBBoxWithoutContours(edge):
     edge.SetWidth(width)
     return bBox
 
+def listGeometries(shapelyObject):
+    """
+    Given a shapely object, return an iterable of all geometries. I.e., for
+    single items, return an iterable containing only the original item. For
+    collections, return iterable of all the geometries in it.
+    """
+    if hasattr(shapelyObject, 'geoms'):
+        return shapelyObject.geoms
+    return [shapelyObject]
+
 def findBoundingBox(edges):
     """
     Return a bounding box of all drawings in edges
@@ -106,6 +119,13 @@ def makePerpendicular(vector):
     """
     return np.array([vector[1], -vector[0]])
 
+def linestringToSegments(linestring):
+    """
+    Given a Shapely linestring, return a list of tuples with start and endpoint
+    of the segment
+    """
+    return [x for x in zip(linestring.coords, islice(linestring.coords, 1, None))]
+
 def tl(rect):
     """ Return top left corner of rect """
     return wxPoint(rect.GetX(), rect.GetY())
@@ -135,4 +155,141 @@ def parseReferences(dStr):
     """
     Parse comma separated list of component references to a list
     """
-    return [x.strip() for x in dStr.split(",") if len(x.strip()) > 0]
\ No newline at end of file
+    return [x.strip() for x in dStr.split(",") if len(x.strip()) > 0]
+
+
+def shpBBoxLeft(bbox):
+    """
+    Given a shapely bounding box, return left edge as (pos, interval)
+    """
+    return AxialLine(bbox[0], bbox[1], bbox[3])
+
+def shpBBoxRight(bbox):
+    """
+    Given a shapely bounding box, return right edge as (pos, interval)
+    """
+    return AxialLine(bbox[2], bbox[1], bbox[3])
+
+def shpBBoxTop(bbox):
+    """
+    Given a shapely bounding box, return top edge as (pos, interval)
+    """
+    return AxialLine(bbox[1], bbox[0], bbox[2])
+
+def shpBBoxBottom(bbox):
+    """
+    Given a shapely bounding box, return bottom edge as (pos, interval)
+    """
+    return AxialLine(bbox[3], bbox[0], bbox[2])
+
+def shpBBoxMerge(a, b):
+    """
+    Given two shapely bounding boxes, return smallest bounding box where both
+    can fit.
+    """
+    return (
+        min(a[0], b[0]),
+        min(a[1], b[1]),
+        max(a[2], b[2]),
+        max(a[3], b[3])
+    )
+
+def shpBBoxExpand(box, x, y=None):
+    """
+    Given a shapely bounding box, return new one expanded by given amount. If y
+    is not supplied, it the same as x.
+    """
+    if y is None:
+        y = x
+    return (box[0] - x, box[1] - y, box[2] + x, box[3] + y)
+
+def isLinestringCyclic(line):
+    c = line.coords
+    return c[0] == c[-1] or isinstance(line, LinearRing)
+
+def fromOpt(object, default):
+    """
+    Given an object, return it if not None. Otherwise return default
+    """
+    return object if object is not None else default
+
+def isBottomLayer(layer):
+    """
+    Decide if layer is a bottom layer
+    """
+    return str(layer).startswith("Layer.B_")
+
+def commonPoints(lines):
+    """
+    Given a list of lines, return dictionary - vertice -> count. Where count
+    specifies how many lines share the vertex.
+    """
+    count = {}
+    for l in lines:
+        for c in l.coords:
+            count[c] = count.get(c, 0) + 1
+    return count
+
+def isHorizontal(start, end):
+    """
+    Given a line decide if it is horizontal
+    """
+    return start[1] == end[1]
+
+def isVertical(start, end):
+    """
+    Given a line decide if it is vertical
+    """
+    return start[0] == end[0]
+
+def resolveAnchor(anchor):
+    """
+    Given a string anchor name, return a function that transforms wxRect into
+    a wxPoint
+    """
+    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)
+    }
+    return choices[anchor]
+
+def indexOf(list, predicate):
+    """
+    Return the index of the first element that satisfies predicate. If no
+    element is found, return -1
+    """
+    for i, x in enumerate(list):
+        if predicate(x):
+            return i
+    return -1
+
+def readParameterList(inputStr):
+    """
+    Given a string, read semicolon separated parameter list in the form of
+    `key: value; key: value`. You can escape via `\\`
+    """
+    from kikit.panelize_ui import splitStr
+
+    if len(inputStr.strip()) == 0:
+        return {}
+    try:
+        values = {}
+        for i, pair in enumerate(splitStr(";", "\\", inputStr)):
+            if len(pair.strip()) == 0:
+                continue
+            s = pair.split(":")
+            if i == 0 and len(s) == 1:
+                values["type"] = s[0].strip()
+                continue
+            key, value = s[0].strip(), s[1].strip()
+            values[key] = value
+        return values
+    except (TypeError, IndexError):
+        raise RuntimeError(f"'{pair}' is not a valid key: value pair")
diff --git a/kikit/doc.py b/kikit/doc.py
index 559fb1db..299536ee 100644
--- a/kikit/doc.py
+++ b/kikit/doc.py
@@ -4,6 +4,8 @@
 import shutil
 import os
 import sys
+import threading
+from itertools import chain
 
 def header(func):
     signature = inspect.signature(func)
@@ -25,29 +27,72 @@ def header(func):
     return("\n".join(lines))
 
 def printHeader(func):
-    print("```\n" + header(func) + "\n```")
+    print(f"\n#### `{func.__name__}`")
+    print(f"```\n{header(func)}\n```")
 
 def printHelp(x):
     print(inspect.getdoc(x))
 
+def quote(args):
+    """
+    Given a list of command line arguments, quote them so they can be can be
+    printed
+    """
+    def q(x):
+        if " " in x:
+            return "'" + x + "'"
+        else:
+            return x
+    return [q(x) for x in args]
+
+
+runBoardExampleThreads = []
+
 def runBoardExample(name, args):
     """
     Run kikit CLI command with args - omitting the output file name. Prints a
-    markdown with the command and includes a generated board image
+    markdown with the command and includes a generated board image.
+
+    Generating images runs in parallel, so do not forget to invoke
+    runBoardExampleJoin() at the end of your script.
     """
     dirname = tempfile.mkdtemp()
     output = os.path.join(dirname, "x.kicad_pcb")
-    realArgs = ["python3", "-m", "kikit.ui"] + args + [output]
-    fakeArgs = ["kikit"] + args + ["panel.kicad_pcb"]
-    try:
-        subprocess.run(realArgs, check=True, capture_output=True)
-        subprocess.run(["pcbdraw", "--vcuts", "--silent", output,
-                "doc/resources/{}.png".format(name)], check=True, capture_output=True)
-    except subprocess.CalledProcessError as e:
-        print("Command: " + " ".join(e.cmd), file=sys.stderr)
-        print("Stdout: " + e.stdout.decode("utf8"), file=sys.stderr)
-        sys.exit(1)
-    print("```\n{}\n```".format(" ".join(fakeArgs)))
+    realArgs = ["python3", "-m", "kikit.ui"] + list(chain(*args)) + [output]
+
+    # We print first, so in a case of failure we have the command in a nice
+    # copy-paste-ready form
+    args[0] = ["kikit"] + args[0]
+    args[-1] = args[-1] + ["panel.kicad_pcb"]
+    print("```\n")
+    for i, c in enumerate(args):
+        if i != 0:
+            print("    ", end="")
+        end = "\n" if i + 1 == len(args) else " \\\n"
+        print(" ".join(quote(c)), end=end)
+    print("```\n")
     print("![{0}](resources/{0}.png)".format(name))
-    shutil.rmtree(dirname)
+
+    def run():
+        try:
+            outimage = f"doc/resources/{name}.png"
+            subprocess.run(realArgs, check=True, capture_output=True)
+            subprocess.run(["pcbdraw", "--vcuts", "--silent", output,
+                    outimage], check=True, capture_output=True)
+            subprocess.run(["convert", outimage, "-define",
+                "png:include-chunk=none", outimage], check=True, capture_output=True)
+        except subprocess.CalledProcessError as e:
+            print("Command: " + " ".join(e.cmd), file=sys.stderr)
+            print("Stdout: " + e.stdout.decode("utf8"), file=sys.stderr)
+            print("Stderr: " + e.stderr.decode("utf8"), file=sys.stderr)
+            sys.exit(1)
+        shutil.rmtree(dirname)
+    t = threading.Thread(target=run)
+    t.start()
+    global runBoardExampleThreads
+    runBoardExampleThreads.append(t)
+
+def runBoardExampleJoin():
+    for t in runBoardExampleThreads:
+        t.join()
 
diff --git a/kikit/info.py b/kikit/info.py
index ca47d9c5..ad627fce 100644
--- a/kikit/info.py
+++ b/kikit/info.py
@@ -1,4 +1,5 @@
 from kikit import pcbnew_compatibility
+from kikit.common import KIKIT_LIB
 import sys
 import click
 
@@ -22,6 +23,13 @@ def drcapi():
     else:
         print("0")
 
+@click.command()
+def lib():
+    """
+    Return KiKit library location
+    """
+    print(KIKIT_LIB)
+
 
 @click.group()
 def cli():
@@ -31,4 +39,5 @@ def cli():
     pass
 
 cli.add_command(kicadversion)
-cli.add_command(drcapi)
\ No newline at end of file
+cli.add_command(drcapi)
+cli.add_command(lib)
\ No newline at end of file
diff --git a/kikit/intervals.py b/kikit/intervals.py
new file mode 100644
index 00000000..3b542c29
--- /dev/null
+++ b/kikit/intervals.py
@@ -0,0 +1,529 @@
+from itertools import islice, chain
+from math import isclose
+from copy import copy
+
+class Interval:
+    """
+    Basic interval representation
+    """
+    def __init__(self, a, b):
+        self.min = min(a, b)
+        self.max = max(a, b)
+
+    def __contains__(self, item):
+        return self.min <= item and item <= self.max
+
+    def intersect(self, other):
+        """
+        Return a new interval representing the overlap, otherwise None
+        """
+        if self.min > other.max or other.min > self.max:
+            return None
+        return Interval(max(self.min, other.min), min(self.max, other.max))
+
+    def nontrivialIntersect(self, other):
+        """
+        Return a new interval representing the overlap larger than a single
+        point, otherwise None
+        """
+        i = self.intersect(other)
+        if i is None or i.trivial():
+            return None
+        return i
+
+    def trivial(self):
+        return self.min == self.max
+
+    @property
+    def length(self):
+        return self.max - self.min
+
+    def __eq__(self, other):
+        return isclose(self.min, other.min) and isclose(self.max, other.max)
+
+    def __repr__(self):
+        return f"I({self.min}, {self.max})"
+
+    def __str__(self):
+        return f"<{self.min}, {self.max}>"
+
+class IntervalList:
+    def __init__(self, intervals):
+        self.intervals = self._normalize(self._toList(intervals))
+
+    def __eq__(self, other):
+        return self.intervals == self._toList(other)
+
+    def __repr__(self):
+        return f"IL[ {', '.join([x.__repr__() for x in self.intervals])} ]"
+
+    def __str__(self):
+        return f"IL[ {', '.join([x.__str__() for x in self.intervals])} ]"
+
+    def trivial(self):
+        return len(self.intervals) == 0
+
+    @staticmethod
+    def _toList(object):
+        """
+        Convert the object into a list of intervals
+        """
+        if isinstance(object, Interval):
+            return [object]
+        if isinstance(object, list):
+            return object
+        if isinstance(object, IntervalList):
+            return object.intervals
+        raise RuntimeError("Uknown object")
+
+    @staticmethod
+    def _normalize(intervals):
+        intervals.sort(key=lambda x: x.min)
+        newIntervals = []
+        for b in intervals:
+            if b.trivial():
+                continue
+            if len(newIntervals) == 0:
+                newIntervals.append(copy(b))
+                continue
+            a = newIntervals[-1]
+            if b.min <= a.max:
+                a.max = max(a.max, b.max)
+            else:
+                newIntervals.append(copy(b))
+        return newIntervals
+
+    @staticmethod
+    def _eventList(intervals0, intervals1):
+        """
+        Build event list; event is a tuple (1/0, x, event). Event +1 means open
+        interval, -1 means close interval. The events are sorted by x
+        """
+        base1 = [[(0, x.min, 1), (0, x.max, -1)] for x in intervals0]
+        base2 = [[(1, x.min, 1), (1, x.max, -1)] for x in intervals1]
+        l = list(chain(*base1, *base2))
+        l.sort(key=lambda x: x[1])
+        return l
+
+    def union(self, other):
+        """
+        Union this interval with other and return new instance
+        """
+        return IntervalList(self.intervals + self._toList(other))
+
+    def intersect(self, other):
+        """
+        Perform self / other and return new instance
+        """
+        intervals = []
+        visibleIntervals = 0
+        for _, p, e in self._eventList(self.intervals, self._toList(other)):
+            if visibleIntervals == 1 and e == 1:
+                start = p
+            if visibleIntervals == 2 and e == -1:
+                intervals.append(Interval(start, p))
+            visibleIntervals += e
+            assert visibleIntervals >= 0 and visibleIntervals <= 2
+        return IntervalList(intervals)
+
+    def difference(self, other):
+        """
+        Perform self / other and return new instance
+        """
+        intervals = []
+        aOpen = 0
+        bOpen = 0
+        for ident, p, e in self._eventList(self.intervals, self._toList(other)):
+            if (ident == 0 and e == 1) and bOpen == 0: # Rising A, no B
+                start = p
+            if (ident == 1 and e == -1) and aOpen == 1: # Falling B, A is on
+                start = p
+            if (ident == 0 and e == -1) and bOpen == 0: # Falling A, no B
+                intervals.append(Interval(start, p))
+            if (ident == 1 and e == 1) and aOpen == 1: # Raising B, A is on
+                intervals.append(Interval(start, p))
+            if ident == 0:
+                aOpen += e
+            else:
+                bOpen += e
+            assert aOpen >= 0 and aOpen <= 1
+            assert bOpen >= 0 and bOpen <= 1
+        return IntervalList(intervals)
+
+
+class BoxNeighbors:
+    """
+    Given a set of axially arranged non-overlapping boxes answers the query for
+    the closest top, bottom, left and right neighbor.
+
+    Neighbor is defined as the closest box in a given direction that has
+    non-empty intersection of their projections in the given direction.
+    """
+    def __init__(self, boxes):
+        """
+        Given a dictionary id -> box initializes the structure.
+
+        Boxes are represented by a tuple (minx, miny, maxx, maxy)
+        """
+        xProj = lambda b: Interval(b[0], b[2])
+        yProj = lambda b: Interval(b[1], b[3])
+
+        leftList = self._prepareProjection(yProj, lambda b: -b[2], boxes)
+        self._leftQ = self._computeQuery(leftList)
+        rightList = self._prepareProjection(yProj, lambda b: b[0], boxes)
+        self._rightQ = self._computeQuery(rightList)
+        topList = self._prepareProjection(xProj, lambda b: -b[3], boxes)
+        self._topQ = self._computeQuery(topList)
+        bottomList = self._prepareProjection(xProj, lambda b: b[1], boxes)
+        self._bottomQ = self._computeQuery(bottomList)
+
+    @staticmethod
+    def _prepareProjection(getInterval, getDistance, boxes):
+        x = [(ident, getInterval(b), getDistance(b)) for ident, b in boxes.items()]
+        x.sort(key=lambda t: t[2])
+        return x
+
+    @staticmethod
+    def _computeQuery(list):
+        neighbors = {}
+        for i, (ident, interval, pos) in enumerate(list):
+            n = []
+            rest = IntervalList(interval)
+            for j in range(i + 1, len(list)):
+                nIdent, nInterval, nPos = list[j]
+                shadow = rest.intersect(nInterval)
+                if shadow.trivial():
+                    continue
+                n.append((nIdent, shadow))
+                rest = rest.difference(nInterval)
+                if rest.trivial():
+                    break
+            neighbors[ident] = n
+        return neighbors
+
+    @staticmethod
+    def _simplify(result):
+        return [ident for ident, _ in result]
+
+    def left(self, ident):
+        return self._simplify(self._leftQ[ident])
+
+    def leftC(self, ident):
+        return self._leftQ[ident]
+
+    def right(self, ident):
+        return self._simplify(self._rightQ[ident])
+
+    def rightC(self, ident):
+        return self._rightQ[ident]
+
+    def top(self, ident):
+        return self._simplify(self._topQ[ident])
+
+    def topC(self, ident):
+        return self._topQ[ident]
+
+    def bottom(self, ident):
+        return self._simplify(self._bottomQ[ident])
+
+    def bottomC(self, ident):
+        return self._bottomQ[ident]
+
+
+class AxialLine(Interval):
+    """
+    Representation of a horizontal or vertical line
+    """
+    def __init__(self, x, y1, y2, tag=None):
+        super().__init__(y1, y2)
+        self.x = x
+        self.tag = tag
+
+    def cut(self, y):
+        """
+        Cut the line at y. Return a list of newly created AxialLines
+        """
+        if y not in self or y == self.min or y == self.max:
+            return [self]
+        return [
+            AxialLine(self.x, self.min, y, self.tag),
+            AxialLine(self.x, y, self.max, self.tag)
+        ]
+
+    def __eq__(self, other):
+        return isclose(self.x, other.x) and super().__eq__(other)
+
+    def __repr__(self):
+        return f"Line[{self.tag}]({self.x}, {self.min}, {self.max})"
+
+    def __hash__(self):
+        return hash((self.x, self.min, self.max, self.tag))
+
+class ShadowLine:
+    """
+    Represents a horizontal or vertical line with a shadow (possible
+    prolongation)
+    """
+    def __init__(self, line, shadow):
+        assert isinstance(line, AxialLine)
+        assert isinstance(shadow, Interval)
+        self.line = line
+        self.shadow = shadow
+
+    @property
+    def shadowLine(self):
+        return AxialLine(self.line.x, self.shadow.min, self.shadow.max,
+            self.line.tag)
+
+    def __eq__(self, other):
+        return self.line == other.line and self.shadow == other.shadow
+
+    def __repr__(self):
+        return f"Shadow({self.line.__repr__()}, {self.shadow.__repr__()})"
+
+def collectBoxEdges(box):
+    """
+    Given a box, return a tuple (horiz edges, vert edges) as lists of AxialLine
+    """
+    return (
+        [AxialLine(box[1], box[0], box[2]), AxialLine(box[3], box[0], box[2])],
+        [AxialLine(box[0], box[1], box[3]), AxialLine(box[2], box[1], box[3])]
+    )
+
+def collectHardStops(boxes):
+    """
+    Given an iterable of boxes, return all partition lines hard stops - i.e.,
+    union of all edges as a tuple (horiz edges, vert edges) as lists of
+    AxialLine
+    """
+    from kikit.common import shpBBoxMerge
+
+    hedges, vedges = set(), set()
+    commonBox = None
+    for b in boxes:
+        if commonBox is None:
+            commonBox = b
+        else:
+            commonBox = shpBBoxMerge(commonBox, b)
+        h, v = collectBoxEdges(b)
+        hedges.update(h)
+        vedges.update(v)
+    h, v = collectBoxEdges(commonBox)
+    hedges.update(h)
+    vedges.update(v)
+    return list(hedges), list(vedges)
+
+def defaultSeedFilter(boxIdA, boxIdB, vertical, seedline):
+    return True
+
+def collectSeedLines(boxes, seedFilter):
+    """
+    Given a dictionary ident -> box return a list of all midlines between
+    neighboring boxes.
+
+    The function seedFilter of type(boxAId, boxBId, vertical, seedLine) -> bool,
+    serves as a predicate that can filter unwanted seed lines - e.g., too far
+    apart or comming from ghost boxes.
+
+    Returns (horlines, verlines), where the lines are tagged with ident
+    """
+    neighbors = BoxNeighbors(boxes)
+    horlines, verlines = [], []
+    for identA, boxA in boxes.items():
+        for identB, shadow in neighbors.leftC(identA):
+            mid = (boxA[0] + boxes[identB][2]) / 2
+            candidates = [AxialLine(mid, e.min, e.max, identA)
+                for e in shadow.intervals]
+            verlines.extend([x for x in candidates
+                if seedFilter(identA, identB, True, x)])
+        for identB, shadow in neighbors.rightC(identA):
+            mid = (boxA[2] + boxes[identB][0]) / 2
+            candidates = [AxialLine(mid, e.min, e.max, identA)
+                for e in shadow.intervals]
+            verlines.extend([x for x in candidates
+                if seedFilter(identA, identB, True, x)])
+        for identB, shadow in neighbors.topC(identA):
+            mid = (boxA[1] + boxes[identB][3]) / 2
+            candidates = [AxialLine(mid, e.min, e.max, identA)
+                for e in shadow.intervals]
+            horlines.extend([x for x in candidates
+                if seedFilter(identA, identB, False, x)])
+        for identB, shadow in neighbors.bottomC(identA):
+            mid = (boxA[3] + boxes[identB][1]) / 2
+            candidates = [AxialLine(mid, e.min, e.max, identA)
+                for e in shadow.intervals]
+            horlines.extend([x for x in candidates
+                if seedFilter(identA, identB, False, x)])
+    return horlines, verlines
+
+def upperBound(sortedCollection, item, key=lambda x: x):
+    """
+    Given a sorted collection, perform binary search to find element x for which
+    the following holds: item < key(x) and the value key(x) is the smallest.
+    Returns index of such an element.
+    """
+    lo = 0
+    hi = len(sortedCollection)
+    while lo < hi:
+        mid = (lo + hi) // 2
+        if item < key(sortedCollection[mid]):
+            hi = mid
+        else:
+            lo = mid + 1
+    return lo
+
+def lowerBound(sortedCollection, item, key=lambda x: x):
+    """
+    Given a sorted collection, perform binary search to find element x for which
+    the following holds: item > key(x) and the value key(x) is the largest.
+    Returns index of such an element.
+    """
+    lo = 0
+    hi = len(sortedCollection)
+    while lo < hi:
+        mid = (lo + hi) // 2
+        if item > key(sortedCollection[mid]):
+            lo = mid + 1
+        else:
+            hi = mid
+    return lo - 1
+
+def buildShadows(lines, boundaries):
+    """
+    Given an iterable of AxialLines, build their prolonged shadows. Shadows
+    stop at the boundaries. Lines and boundaries are expected to be
+    perpendicular to each other. This function assumes there is a boundary for
+    every line.
+    """
+    boundaries = list(boundaries)
+    boundaries.sort(key=lambda line: line.x)
+
+    shadowLines = []
+    for l in lines:
+        # Extend to right
+        righStart = lowerBound(boundaries, l.max, key=lambda line: line.x)
+        for i in range(righStart, len(boundaries)):
+            b = boundaries[i]
+            if l.x in b and b.x > l.min:
+                rightExtend = b.x
+                break
+        # Extend to left
+        leftStart = upperBound(boundaries, l.min, key=lambda line: line.x)
+        for i in range(leftStart, 0, -1):
+            b  = boundaries[i]
+            if l.x in b and b.x < l.max:
+                leftExtend = b.x
+                break
+        assert rightExtend is not None
+        assert leftExtend is not None
+        shadowLines.append(ShadowLine(l, Interval(leftExtend, rightExtend)))
+    return shadowLines
+
+def trimShadows(shadows, boundaries):
+    """
+    Given an iterable of ShadowLines and Axial lines as boudaries, trim the
+    shadows so they do not cross any boundary. Return new shadows.
+    """
+    boundaries = list(boundaries)
+    boundaries.sort(key=lambda line: line.x)
+    newShadows = []
+    for l in shadows:
+        # Trim right
+        rightStart = upperBound(boundaries, l.line.min, key=lambda line: line.x)
+        rightTrim = l.shadow.max
+        for i in range(rightStart, len(boundaries)):
+            b = boundaries[i]
+            if l.line.x in b and l.shadow.min < b.x <= l.shadow.max:
+                rightTrim = b.x
+                break
+        # Trim left
+        leftStart = upperBound(boundaries, l.line.max, key=lambda line: line.x) - 1
+        leftTrim = l.shadow.min
+        for i in range(leftStart, 0, -1):
+            b = boundaries[i]
+            if l.line.x in b and l.shadow.min <= b.x < l.shadow.max:
+                leftTrim = b.x
+                break
+        newShadows.append(ShadowLine(l.line, Interval(leftTrim, rightTrim)))
+    return newShadows
+
+class BoxPartitionLines:
+    """
+    Given a set of axially arranged non-overlapping boxes answers the query for
+    the partition lines of each box.
+
+    Partition line is a horizontal or a vertical line. The union of all
+    partition lines splits the free space between the boxes such that the gaps
+    between two neighboring boxes are split evenly. See the following
+    illustration:
+
+    +---+ | +----+ | +--------+
+    |   | | |    | | |        |
+    +---+ | |    | | |        |
+    ------| |    | | +--------+
+    +---+ | |    | |-----------
+    |   | | |    |   |   +----+
+    +---+ | +----+   |   +----+
+    """
+
+    def __init__(self, boxes, seedFilter=defaultSeedFilter,
+                 safeHorizontalMargin=0, safeVerticalMargin=0):
+        """
+        Given a dictionary id -> box initializes the structure.
+
+        Boxes are represented by a tuple (minx, miny, maxx, maxy)
+
+        The margin guarantees there will be no partition line too close to edge
+        (necessary to handle some pathological cases)
+        """
+        from kikit.common import shpBBoxExpand
+
+        hstops, vstops = collectHardStops(boxes.values())
+        hSafeStops, vSafeStops = collectHardStops([
+            shpBBoxExpand(x, safeVerticalMargin) for x in boxes.values()])
+        hseeds, vseeds = collectSeedLines(boxes, seedFilter)
+        hshadows = buildShadows(hseeds, chain(vstops, vSafeStops))
+        vshadows = buildShadows(vseeds, chain(hstops, hSafeStops))
+
+        hPartition = trimShadows(hshadows, chain(
+            [x.shadowLine for x in vshadows], vSafeStops))
+        vPartition = trimShadows(vshadows, chain(
+            [x.shadowLine for x in hshadows], hSafeStops))
+
+        self.query = { ident: ([], []) for ident in boxes.keys() }
+        for l in hPartition:
+            self.query[l.line.tag][0].append(
+                AxialLine(l.line.x, l.shadow.min, l.shadow.max))
+        for l in vPartition:
+            self.query[l.line.tag][1].append(
+                AxialLine(l.line.x, l.shadow.min, l.shadow.max))
+
+    def partitionLines(self, ident):
+        """
+        Return a tuple (horiz. lines, vert. lines) represented as AxialLine
+        """
+        return self.query[ident]
+
+    def _visualize(self, vars):
+        """
+        When debugging, you can invoke self._visualize(locals()) in __init__ to
+        see what is happening.
+        """
+        import matplotlib.pyplot as plt
+
+        for h in vars["hstops"]:
+            plt.hlines(h.x, h.min, h.max, ["g"])
+        for v in vars["vstops"]:
+            plt.vlines(v.x, v.min, v.max, ["g"])
+
+        plt.axis('equal')
+        for h in vars["hshadows"]:
+            plt.hlines(h.line.x, h.shadow.min, h.shadow.max, ["r"])
+        for v in vars["vshadows"]:
+            plt.vlines(v.line.x, v.shadow.min, v.shadow.max, ["r"])
+
+        for h in vars["hseeds"]:
+            plt.hlines(h.x, h.min, h.max)
+        for v in vars["vseeds"]:
+            plt.vlines(v.x, v.min, v.max)
+        plt.show()
diff --git a/kikit/panelize.py b/kikit/panelize.py
index 9ce73306..166af6c7 100644
--- a/kikit/panelize.py
+++ b/kikit/panelize.py
@@ -1,9 +1,11 @@
 from kikit import pcbnew_compatibility
 from kikit.pcbnew_compatibility import pcbnew
+from kikit.common import normalize
 from pcbnew import (GetBoard, LoadBoard,
     FromMM, ToMM, wxPoint, wxRect, wxRectMM, wxPointMM)
 from enum import Enum, IntEnum
-from shapely.geometry import Polygon, MultiPolygon, Point, LineString, box
+from shapely.geometry import (Polygon, MultiPolygon, Point, LineString, box,
+    GeometryCollection, MultiLineString)
 from shapely.prepared import prep
 import shapely
 from itertools import product, chain
@@ -11,8 +13,9 @@
 import os
 
 from kikit import substrate
-from kikit.substrate import Substrate, linestringToKicad
-from kikit.defs import STROKE_T, Layer, EDA_TEXT_HJUSTIFY_T
+from kikit import units
+from kikit.substrate import Substrate, linestringToKicad, extractRings
+from kikit.defs import STROKE_T, Layer, EDA_TEXT_HJUSTIFY_T, EDA_TEXT_VJUSTIFY_T
 
 from kikit.common import *
 
@@ -148,37 +151,11 @@ def expandRect(rect, offsetX, offsetY=None):
     """
     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)
 
-def translateRect(rect, translation):
-    """
-    Given a wxRect return a new rect translated by transaltion (tuple-like object)
-    """
-    return wxRect(rect.GetX() + translation[0], rect.GetY() + translation[1],
-        rect.GetWidth(), rect.GetHeight())
-
-def normalizeRect(rect):
-    """ If rectangle is specified via negative width/height, corrects it """
-    if rect.GetHeight() < 0:
-        rect.SetY(rect.GetY() + rect.GetHeight())
-        rect.SetHeight(-rect.GetHeight())
-    if rect.GetWidth() < 0:
-        rect.SetX(rect.GetX() + rect.GetWidth())
-        rect.SetWidth(-rect.GetWidth())
-    return rect
-
-def flipRect(rect):
-    return wxRect(rect.GetY(), rect.GetX(), rect.GetHeight(), rect.GetWidth())
-
-def mirrorRectX(rect, axis):
-    return wxRect(2 * axis - rect.GetX() - rect.GetWidth(), rect.GetY(),
-                  rect.GetWidth(), rect.GetHeight())
-
-def mirrorRectY(rect, axis):
-    return wxRect(rect.GetX(), 2 * axis - rect.GetY() - rect.GetHeight(),
-                  rect.GetWidth(), rect.GetHeight())
-
 def rectToRing(rect):
     return [
         (rect.GetX(), rect.GetY()),
@@ -266,6 +243,131 @@ def increaseZonePriorities(board, amount=1):
     for zone in board.Zones():
         zone.SetPriority(zone.GetPriority() + amount)
 
+def tabSpacing(width, count):
+    """
+    Given a width of board edge and tab count, return an iterable with tab
+    offsets.
+    """
+    return [width * i / (count + 1) for i in range(1, count + 1)]
+
+def prolongCut(cut, prolongation):
+    """
+    Given a cut (Shapely LineString) it tangentially prolongs it by prolongation
+    """
+    c = list([np.array(x) for x in cut.coords])
+    c[0] += normalize(c[0] - c[1]) * prolongation
+    c[-1] += normalize(c[-1] - c[-2]) * prolongation
+    return LineString(c)
+
+def polygonToZone(polygon, board):
+    """
+    Given a polygon and target board, creates a KiCAD zone. The zone has to be
+    added to the board.
+    """
+    zone = pcbnew.ZONE(board)
+    boundary = polygon.exterior
+    zone.Outline().AddOutline(linestringToKicad(boundary))
+    for hole in polygon.interiors:
+        boundary = hole.exterior
+        zone.Outline().AddHole(linestringToKicad(boundary))
+    return zone
+
+def isAnnotation(footprint):
+    """
+    Given a footprint, decide if it is KiKit annotation
+    """
+    info = footprint.GetFPID()
+    if info.GetLibNickname() != "kikit":
+        return False
+    return info.GetLibItemName() in ["Tab", "Board"]
+
+def readKiKitProps(footprint):
+    """
+    Given a footprint, returns a string containing KiKit annotations.
+    Annotations are in FP_TEXT starting with prefix `KIKIT:`.
+
+    Returns a dictionary of key-value pairs.
+    """
+    for x in footprint.GraphicalItemsList():
+        if not isinstance(x, pcbnew.FP_TEXT):
+            continue
+        text = x.GetText()
+        if text.startswith("KIKIT:"):
+            return readParameterList(text[len("KIKIT:"):])
+    return {}
+
+class TabAnnotation:
+    def __init__(self, ref, origin, direction, width, maxLength=fromMm(100)):
+        self.ref = ref
+        self.origin = origin
+        self.direction = direction
+        self.width = width
+        self.maxLength = maxLength
+
+    @staticmethod
+    def fromFootprint(footprint):
+        origin = footprint.GetPosition()
+        radOrientaion = footprint.GetOrientationRadians()
+        direction = (np.cos(radOrientaion), -np.sin(radOrientaion))
+        props = readKiKitProps(footprint)
+        width = units.readLength(props["width"])
+        return TabAnnotation(footprint.GetReference(), origin, direction, width)
+
+def convertToAnnotation(footprint):
+    """
+    Given a footprint, convert it into an annotation. One footprint might
+    represent a zero or multiple annotations, so this function returns a list.
+    """
+    name = footprint.GetFPID().GetLibItemName()
+    if name == "Tab":
+        return [TabAnnotation.fromFootprint(footprint)]
+    # We ignore Board annotation
+    return []
+
+def buildTabs(substrate, partitionLines, tabAnnotations):
+    """
+    Given substrate, partitionLines of the substrate and an iterable of tab
+    annotations, build tabs. Note that if the tab does not hit the partition
+    line, it is not included in the design.
+
+    Return a pair of lists: tabs and cuts.
+    """
+    tabs, cuts = [], []
+    for annotation in tabAnnotations:
+        t, c = substrate.tab(annotation.origin, annotation.direction,
+            annotation.width, partitionLines, annotation.maxLength)
+        if t is not None:
+            tabs.append(t)
+            cuts.append(c)
+    return tabs, cuts
+
+def normalizePartitionLineOrientation(line):
+    """
+    Given a LineString or MultiLineString, normalize orientation of the
+    partition line. For open linestrings, the orientation does not matter. For
+    closed linerings, it has to be counter-clock-wise.
+    """
+    if isinstance(line, MultiLineString):
+        return MultiLineString([normalizePartitionLineOrientation(x) for x in line.geoms])
+    if isinstance(line, GeometryCollection):
+        return GeometryCollection([normalizePartitionLineOrientation(l) for l in line.geoms])
+    if not isLinestringCyclic(line):
+        return line
+    r = LinearRing(line.coords)
+    if not r.is_ccw:
+        return line
+    return LineString(list(r.coords)[::-1])
+
+def maxTabCount(edgeLen, width, minDistance):
+    """
+    Given a length of edge, tab width and their minimal distance, return maximal
+    number of tabs.
+    """
+    if edgeLen < width:
+        return 0
+    c = 1 + (edgeLen - minDistance) // (minDistance + width)
+    return max(0, int(c))
+
 class Panel:
     """
     Basic interface for panel building. Instance of this class represents a
@@ -278,11 +380,13 @@ def __init__(self):
         """
         self.board = pcbnew.BOARD()
         self.substrates = [] # Substrates of the individual boards; e.g. for masking
-        self.boardCounter = 0
         self.boardSubstrate = Substrate([]) # Keep substrate in internal representation,
                                             # Draw it just before saving
+        self.backboneLines = []
         self.hVCuts = set() # Keep V-cuts as numbers and append them just before saving
         self.vVCuts = set() # to make them truly span the whole panel
+        self.vCutLayer = Layer.Cmts_User
+        self.vCutClearance = 0
         self.copperLayerCount = None
         self.zonesToRefill = pcbnew.ZONES()
 
@@ -293,18 +397,26 @@ def save(self, filename):
         for edge in self.boardSubstrate.serialize():
             self.board.Add(edge)
         vcuts = self._renderVCutH() + self._renderVCutV()
-        for cut in vcuts:
+        keepouts = []
+        for cut, clearanceArea in vcuts:
             self.board.Add(cut)
+            if clearanceArea is not None:
+                keepouts.append(self.addKeepout(clearanceArea))
         fillerTool = pcbnew.ZONE_FILLER(self.board)
         fillerTool.Fill(self.zonesToRefill)
         self.board.Save(filename)
+        # Remove cuts
+        for cut, _ in vcuts:
+            self.board.Remove(cut)
+        # Remove V-cuts keepouts
+        for keepout in keepouts:
+            self.board.Remove(keepout)
+        # Remove edges
         for edge in collectEdges(self.board, "Edge.Cuts"):
             self.board.Remove(edge)
-        for cut in vcuts:
-            self.board.Remove(cut)
 
     def _uniquePrefix(self):
-        return "Board_{}-".format(self.boardCounter)
+        return "Board_{}-".format(len(self.substrates))
 
     def inheritDesignSettings(self, boardFilename):
         """
@@ -359,7 +471,7 @@ def appendBoard(self, filename, destination, sourceArea=None,
         """
         board = LoadBoard(filename)
         thickness = board.GetDesignSettings().GetBoardThickness()
-        if self.boardCounter == 0:
+        if len(self.substrates) == 0:
             self.board.GetDesignSettings().SetBoardThickness(thickness)
         else:
             panelThickness = self.board.GetDesignSettings().GetBoardThickness()
@@ -367,7 +479,6 @@ def appendBoard(self, filename, destination, sourceArea=None,
                 raise PanelError(f"Cannot append board {filename} as its " \
                                  f"thickness ({toMm(thickness)} mm) differs from " \
                                  f"thickness of the panel ({toMm(panelThickness)}) mm")
-        self.boardCounter += 1
         self.inheritCopperLayers(board)
 
         if not sourceArea:
@@ -381,9 +492,9 @@ def appendBoard(self, filename, destination, sourceArea=None,
 
         if netRenamer is None:
             netRenamer = lambda x, y: self._uniquePrefix() + y
-        renameNets(board, lambda x: netRenamer(self.boardCounter, x))
+        renameNets(board, lambda x: netRenamer(len(self.substrates), x))
         if refRenamer is not None:
-            renameRefs(board, lambda x: refRenamer(self.boardCounter, x))
+            renameRefs(board, lambda x: refRenamer(len(self.substrates), x))
 
         drawings = collectItems(board.GetDrawings(), enlargedSourceArea)
         footprints = collectFootprints(board.GetFootprints(), enlargedSourceArea)
@@ -391,11 +502,15 @@ def appendBoard(self, filename, destination, sourceArea=None,
         zones = collectItems(board.Zones(), enlargedSourceArea)
 
         edges = []
+        annotations = []
         for footprint in footprints:
             footprint.Rotate(originPoint, rotationAngle)
             footprint.Move(translation)
             edges += removeCutsFromFootprint(footprint)
-            appendItem(self.board, footprint)
+            if isAnnotation(footprint):
+                annotations.extend(convertToAnnotation(footprint))
+            else:
+                appendItem(self.board, footprint)
         for track in tracks:
             track.Rotate(originPoint, rotationAngle)
             track.Move(translation)
@@ -414,9 +529,11 @@ def appendBoard(self, filename, destination, sourceArea=None,
         edges += [edge for edge in drawings if isBoardEdge(edge)]
         otherDrawings = [edge for edge in drawings if not isBoardEdge(edge)]
         try:
+            o = Substrate(edges, -bufferOutline)
             s = Substrate(edges, bufferOutline)
             self.boardSubstrate.union(s)
-            self.substrates.append(s)
+            self.substrates.append(o)
+            self.substrates[-1].annotations = annotations
         except substrate.PositionError as e:
             point = undoTransformation(e.point, rotationAngle, originPoint, translation)
             raise substrate.PositionError(filename + ": " + e.origMessage, point)
@@ -433,6 +550,24 @@ def appendSubstrate(self, substrate):
         polygon = toPolygon(substrate)
         self.boardSubstrate.union(polygon)
 
+    def boardsBBox(self):
+        """
+        Return common bounding box for all boards in the design (ignores the
+        individual pieces of substrate) as a shapely box.
+        """
+        if len(self.substrates) == 0:
+            raise RuntimeError("There are no substrates, cannot compute bounding box")
+        bbox = self.substrates[0].bounds()
+        for p in islice(self.substrates, 1, None):
+            bbox = shpBBoxMerge(bbox, p.bounds())
+        return bbox
+
+    def panelBBox(self):
+        """
+        Return bounding box of the panel as a shapely box.
+        """
+        return self.boardSubstrate.bounds()
+
     def addVCutH(self, pos):
         """
         Adds a horizontal V-CUT at pos (integer in KiCAD units).
@@ -445,6 +580,18 @@ def addVCutV(self, pos):
         """
         self.vVCuts.add(pos)
 
+    def setVCutLayer(self, layer):
+        """
+        Set layer on which the V-Cuts will be rendered
+        """
+        self.vCutLayer = layer
+
+    def setVCutClearance(self, clearance):
+        """
+        Set V-cut clearance
+        """
+        self.vCutClearance = clearance
+
     def _setVCutSegmentStyle(self, segment, layer):
         segment.SetShape(STROKE_T.S_SEGMENT)
         segment.SetLayer(layer)
@@ -457,41 +604,58 @@ def _setVCutLabelStyle(self, label, layer):
         label.SetTextSize(pcbnew.wxSizeMM(2, 2))
         label.SetHorizJustify(EDA_TEXT_HJUSTIFY_T.GR_TEXT_HJUSTIFY_LEFT)
 
-    def _renderVCutV(self, layer=Layer.Cmts_User):
+    def _renderVCutV(self):
         """ return list of PCB_SHAPE V-Cuts """
         bBox = self.boardSubstrate.boundingBox()
         minY, maxY = bBox.GetY() - fromMm(3), bBox.GetY() + bBox.GetHeight() + fromMm(3)
         segments = []
         for cut in self.vVCuts:
             segment = pcbnew.PCB_SHAPE()
-            self._setVCutSegmentStyle(segment, layer)
+            self._setVCutSegmentStyle(segment, self.vCutLayer)
             segment.SetStart(pcbnew.wxPoint(cut, minY))
             segment.SetEnd(pcbnew.wxPoint(cut, maxY))
-            segments.append(segment)
+
+            keepout = None
+            if self.vCutClearance != 0:
+                keepout = shapely.geometry.box(
+                    cut - self.vCutClearance / 2,
+                    bBox.GetY(),
+                    cut + self.vCutClearance / 2,
+                    bBox.GetY() + bBox.GetHeight())
+            segments.append((segment, keepout))
 
             label = pcbnew.PCB_TEXT(segment)
-            self._setVCutLabelStyle(label, layer)
+            self._setVCutLabelStyle(label, self.vCutLayer)
             label.SetPosition(wxPoint(cut, minY - fromMm(3)))
             label.SetTextAngle(900)
-            segments.append(label)
+            segments.append((label, None))
         return segments
 
-    def _renderVCutH(self, layer=Layer.Cmts_User):
+    def _renderVCutH(self):
         """ return list of PCB_SHAPE V-Cuts """
         bBox = self.boardSubstrate.boundingBox()
         minX, maxX = bBox.GetX() - fromMm(3), bBox.GetX() + bBox.GetWidth() + fromMm(3)
         segments = []
         for cut in self.hVCuts:
             segment = pcbnew.PCB_SHAPE()
-            self._setVCutSegmentStyle(segment, layer)
+            self._setVCutSegmentStyle(segment, self.vCutLayer)
             segment.SetStart(pcbnew.wxPoint(minX, cut))
             segment.SetEnd(pcbnew.wxPoint(maxX, cut))
-            segments.append(segment)
+
+            keepout = None
+            if self.vCutClearance != 0:
+                keepout = shapely.geometry.box(
+                    bBox.GetX(),
+                    cut - self.vCutClearance / 2,
+                    bBox.GetX() + bBox.GetWidth(),
+                    cut + self.vCutClearance / 2)
+            segments.append((segment, keepout))
+
 
             label = pcbnew.PCB_TEXT(segment)
-            self._setVCutLabelStyle(label, layer)
+            self._setVCutLabelStyle(label, self.vCutLayer)
             label.SetPosition(wxPoint(maxX + fromMm(3), cut))
-            segments.append(label)
+            segments.append((label, None))
         return segments
 
     def _placeBoardsInGrid(self, boardfile, rows, cols, destination, sourceArea, tolerance,
@@ -517,301 +681,66 @@ def _placeBoardsInGrid(self, boardfile, rows, cols, destination, sourceArea, tol
                 topLeftSize = boardSize
         return topLeftSize
 
-    def _makeFullHorizontalTabs(self, destination, rows, cols, boardSize,
-                                verSpace, horSpace, outerVerSpace, outerHorSpace,
-                                forceOuterCuts):
-        """
-        Crate full tabs for given grid.
-
-        Return tab body, list of cut edges and list of fillet candidates as a
-        tuple.
-        """
-        width = cols * boardSize.GetWidth() + (cols - 1) * horSpace
-        height = rows * boardSize.GetHeight() + (rows - 1) * verSpace
-        polygons = []
-        cuts = []
-        for i in range(cols - 1):
-            pos = (i + 1) * boardSize.GetWidth() + i * horSpace
-            tl = destination + wxPoint(pos, -outerVerSpace)
-            tr = destination + wxPoint(pos + horSpace, -outerVerSpace)
-            br = destination + wxPoint(pos + horSpace, height + outerVerSpace)
-            bl = destination + wxPoint(pos, height + outerVerSpace)
-            if horSpace > 0:
-                polygon = Polygon([tl, tr, br, bl])
-                polygons.append(polygon)
-                cuts.append(LineString([tl, bl]))
-            cuts.append(LineString([br, tr]))
-
-        if outerHorSpace > 0:
-            # Outer tabs
-            polygons.append(Polygon([
-                destination + wxPoint(-outerHorSpace, -outerVerSpace),
-                destination + wxPoint(0, -outerVerSpace),
-                destination + wxPoint(0, height + outerVerSpace),
-                destination + wxPoint(-outerHorSpace, height + outerVerSpace)]))
-            polygons.append(Polygon([
-                destination + wxPoint(width + outerHorSpace, -outerVerSpace),
-                destination + wxPoint(width, -outerVerSpace),
-                destination + wxPoint(width, height + outerVerSpace),
-                destination + wxPoint(width + outerHorSpace, height + outerVerSpace)]))
-        if forceOuterCuts or outerHorSpace > 0:
-            cuts.append(LineString([destination + wxPoint(0, height + outerVerSpace),
-                destination + wxPoint(0, -outerVerSpace)]))
-            cuts.append(LineString([destination + wxPoint(width, -outerVerSpace),
-                destination + wxPoint(width, height + outerVerSpace)]))
-        return polygons, cuts
-
-    def _makeFullVerticalTabs(self, destination, rows, cols, boardSize,
-                              verSpace, horSpace, outerVerSpace, outerHorSpace,
-                              forceOuterCuts):
-        """
-        Crate full tabs for given grid.
-
-        Return tab body, list of cut edges and list of fillet candidates as a
-        tuple.
-        """
-        width = cols * boardSize.GetWidth() + (cols - 1) * horSpace
-        height = rows * boardSize.GetHeight() + (rows - 1) * verSpace
-        polygons = []
-        cuts = []
-        for i in range(rows - 1):
-            pos = (i + 1) * boardSize.GetHeight() + i * verSpace
-            tl = destination + wxPoint(-outerHorSpace, pos)
-            tr = destination + wxPoint(width + outerHorSpace, pos)
-            br = destination + wxPoint(width + outerHorSpace, pos + verSpace)
-            bl = destination + wxPoint(-outerHorSpace, pos + verSpace)
-            if verSpace > 0:
-                polygon = Polygon([tl, tr, br, bl])
-                polygons.append(polygon)
-                cuts.append(LineString([tr, tl]))
-            cuts.append(LineString([bl, br]))
-        if outerVerSpace > 0:
-            # Outer tabs
-            polygons.append(Polygon([
-                destination + wxPoint(-outerHorSpace, 0),
-                destination + wxPoint(-outerHorSpace, -outerVerSpace),
-                destination + wxPoint(outerHorSpace + width, - outerVerSpace),
-                destination + wxPoint(outerHorSpace + width, 0)]))
-            polygons.append(Polygon([
-                destination + wxPoint(-outerHorSpace, height),
-                destination + wxPoint(-outerHorSpace, height + outerVerSpace),
-                destination + wxPoint(outerHorSpace + width, height + outerVerSpace),
-                destination + wxPoint(outerHorSpace + width, height)]))
-        if forceOuterCuts or outerVerSpace > 0:
-            cuts.append(LineString([destination + wxPoint(-outerHorSpace, 0),
-                destination + wxPoint(width + outerHorSpace, 0)]))
-            cuts.append(LineString([destination + wxPoint(width + outerHorSpace, height),
-                destination + wxPoint(-outerHorSpace, height)]))
-        return polygons, cuts
-
-    def _tabSpacing(self, width, count):
-        """
-        Given a width of board edge and tab count, return an iterable with tab
-        offsets.
-        """
-        return [width * i / (count + 1) for i in range(1, count + 1)]
-
-    def _makeVerGridTabs(self, destination, rows, cols, boardSize, verSpace,
-                      horSpace, verTabWidth, horTabWidth, verTabCount,
-                      horTabCount, outerVerTabThickness, outerHorTabThickness,
-                      placementClass):
-        placement = placementClass(destination, boardSize, horSpace, verSpace)
-        polygons = []
-        cuts = []
-        for i, j in product(range(rows), range(cols)):
-            dest = placement.position(i, j)
-            if (i != 0 and verSpace > 0) or outerVerTabThickness > 0: # Add tabs to the top side
-                tabThickness = outerVerTabThickness if i == 0 else verSpace / 2
-                for tabPos in self._tabSpacing(boardSize.GetWidth(), verTabCount):
-                    t, f = self.boardSubstrate.tab(
-                        dest + wxPoint(tabPos, -tabThickness), [0, 1], verTabWidth)
-                    polygons.append(t)
-                    cuts.append(f)
-            if (i != rows - 1 and verSpace > 0) or outerVerTabThickness > 0: # Add tabs to the bottom side
-                tabThickness = outerVerTabThickness if i == rows - 1 else verSpace / 2
-                for tabPos in self._tabSpacing(boardSize.GetWidth(), verTabCount):
-                    origin = dest + wxPoint(tabPos, boardSize.GetHeight() + tabThickness)
-                    t, f = self.boardSubstrate.tab(origin, [0, -1], verTabWidth)
-                    polygons.append(t)
-                    cuts.append(f)
-        return polygons, cuts
-
-    def _makeHorGridTabs(self, destination, rows, cols, boardSize, verSpace,
-                      horSpace, verTabWidth, horTabWidth, verTabCount,
-                      horTabCount, outerVerTabThickness, outerHorTabThickness,
-                      placementClass):
-        placement = placementClass(destination, boardSize, horSpace, verSpace)
-        polygons = []
-        cuts = []
-        for i, j in product(range(rows), range(cols)):
-            dest = placement.position(i, j)
-            if (j != 0 and horSpace > 0) or outerHorTabThickness > 0: # Add tabs to the left side
-                tabThickness = outerHorTabThickness if j == 0 else horSpace / 2
-                for tabPos in self._tabSpacing(boardSize.GetHeight(), horTabCount):
-                    t, f = self.boardSubstrate.tab(
-                        dest + wxPoint(-tabThickness, tabPos), [1, 0], horTabWidth)
-                    polygons.append(t)
-                    cuts.append(f)
-            if (j != cols - 1 and horSpace > 0) or outerHorTabThickness > 0: # Add tabs to the right side
-                tabThickness = outerHorTabThickness if j == cols - 1 else horSpace / 2
-                for tabPos in self._tabSpacing(boardSize.GetHeight(), horTabCount):
-                    origin = dest + wxPoint(boardSize.GetWidth() + tabThickness, tabPos)
-                    t, f = self.boardSubstrate.tab(
-                        origin, [-1, 0], horTabWidth)
-                    polygons.append(t)
-                    cuts.append(f)
-        return polygons, cuts
-
-    def makeGrid(self, boardfile, rows, cols, destination, sourceArea=None,
-                 tolerance=0, verSpace=0, horSpace=0, verTabCount=1,
-                 horTabCount=1, verTabWidth=0, horTabWidth=0,
-                 outerVerTabThickness=0, outerHorTabThickness=0, rotation=0,
-                 forceOuterCutsH=False, forceOuterCutsV=False,
-                 placementClass=BasicGridPosition,
-                 netRenamePattern="Board_{n}-{orig}",
-                 refRenamePattern="Board_{n}-{orig}"):
-        """
-        Creates a grid of boards (row x col) as a panel at given destination
-        separated by V-CUTS. The source can be either extracted automatically or
-        from given sourceArea. There can be a spacing between the individual
-        board (verSpacing, horSpacing) and the tab width can be adjusted
-        (verTabWidth, horTabWidth). Also, the user can control whether to append
-        the outer tabs (e.g. to connect it to a frame) by setting
-        outerVerTabsWidth and outerHorTabsWidth.
-
-        Returns a tuple - wxRect with the panel bounding box (excluding
-        outerTabs) and a list of cuts (list of lines) to make. You can use the
-        list to either create a V-CUTS via makeVCuts or mouse bites via
-        makeMouseBites.
+    def makeGrid(self, boardfile, sourceArea, rows, cols, destination,
+                    verSpace, horSpace, rotation,
+                    placementClass=BasicGridPosition,
+                    netRenamePattern="Board_{n}-{orig}",
+                    refRenamePattern="Board_{n}-{orig}"):
         """
-        netRenamer = lambda x, y: netRenamePattern.format(n=x, orig=y)
-        refRenamer = lambda x, y: refRenamePattern.format(n=x, orig=y)
-        boardSize = self._placeBoardsInGrid(boardfile, rows, cols, destination,
-                                    sourceArea, tolerance, verSpace, horSpace,
-                                    rotation, netRenamer, refRenamer, placementClass)
-        gridDest = wxPoint(boardSize.GetX(), boardSize.GetY())
-        tabs, cuts = [], []
-
-        if verTabCount != 0:
-            if verTabWidth == 0:
-                t, c = self._makeFullVerticalTabs(gridDest, rows, cols,
-                    boardSize, verSpace, horSpace, outerVerTabThickness,outerHorTabThickness,
-                    forceOuterCutsV)
-            else:
-                t, c = self._makeVerGridTabs(gridDest, rows, cols, boardSize,
-                    verSpace, horSpace, verTabWidth, horTabWidth, verTabCount,
-                    horTabCount, outerVerTabThickness, outerHorTabThickness,
-                    placementClass)
-            tabs += t
-            cuts += c
-
-        if horTabCount != 0:
-            if horTabWidth == 0:
-                t, c = self._makeFullHorizontalTabs(gridDest, rows, cols,
-                    boardSize, verSpace, horSpace, outerVerTabThickness, outerHorTabThickness,
-                    forceOuterCutsH)
-            else:
-                t, c = self._makeHorGridTabs(gridDest, rows, cols, boardSize,
-                    verSpace, horSpace, verTabWidth, horTabWidth, verTabCount,
-                    horTabCount, outerVerTabThickness, outerHorTabThickness,
-                    placementClass)
-            tabs += t
-            cuts += c
+        Place the given board in a regular grid pattern with given spacing
+        (verSpace, horSpace). The board position can be fine-tuned via
+        placementClass. The nets and references are renamed according to the
+        patterns.
 
-        tabs = list([t.buffer(fromMm(0.01), join_style=2) for t in tabs])
-        self.appendSubstrate(tabs)
-
-        return (wxRect(gridDest[0], gridDest[1],
-                       cols * boardSize.GetWidth() + (cols - 1) * horSpace,
-                       rows * boardSize.GetHeight() + (rows - 1) * verSpace),
-                cuts)
-
-
-    def makeTightGrid(self, boardfile, rows, cols, destination, verSpace,
-                      horSpace, slotWidth, width, height, sourceArea=None,
-                      tolerance=0, verTabWidth=0, horTabWidth=0,
-                      verTabCount=1, horTabCount=1, rotation=0,
-                      placementClass=BasicGridPosition,
-                      netRenamePattern="Board_{n}-{orig}",
-                      refRenamePattern="Board_{n}-{orig}"):
-        """
-        Creates a grid of boards just like `makeGrid`, however, it creates a
-        milled slot around perimeter of each board and 4 tabs.
+        Returns a list of the placed substrates. You can use these to generate
+        tabs, frames, backbones, etc.
         """
+        substrateCount = len(self.substrates)
         netRenamer = lambda x, y: netRenamePattern.format(n=x, orig=y)
         refRenamer = lambda x, y: refRenamePattern.format(n=x, orig=y)
-        boardSize = self._placeBoardsInGrid(boardfile, rows, cols, destination,
-                                    sourceArea, tolerance, verSpace, horSpace,
-                                    rotation, netRenamer, refRenamer, placementClass)
-        gridDest = wxPoint(boardSize.GetX(), boardSize.GetY())
-        panelSize = wxRect(gridDest[0], gridDest[1],
-                       cols * boardSize.GetWidth() + (cols - 1) * horSpace,
-                       rows * boardSize.GetHeight() + (rows - 1) * verSpace)
+        self._placeBoardsInGrid(boardfile, rows, cols, destination,
+                                sourceArea, 0, verSpace, horSpace,
+                                rotation, netRenamer, refRenamer, placementClass)
+        return self.substrates[substrateCount:]
 
-        tabs, cuts = [], []
-        if verTabCount != 0:
-            t, c = self._makeVerGridTabs(gridDest, rows, cols, boardSize,
-                    verSpace, horSpace, verTabWidth, horTabWidth, verTabCount,
-                    horTabCount, slotWidth, slotWidth, placementClass)
-            tabs += t
-            cuts += c
-        if horTabCount != 0:
-            t, c = self._makeHorGridTabs(gridDest, rows, cols, boardSize,
-                    verSpace, horSpace, verTabWidth, horTabWidth, verTabCount,
-                    horTabCount, slotWidth, slotWidth, placementClass)
-            tabs += t
-            cuts += c
-
-        xDiff = (width - panelSize.GetWidth()) // 2
-        if xDiff < 0:
-            raise RuntimeError("The frame is to small")
-        yDiff = (height - panelSize.GetHeight()) // 2
-        if yDiff < 0:
-            raise RuntimeError("The frame is to small")
-        outerRect = expandRect(panelSize, xDiff, yDiff)
-        outerRing = rectToRing(outerRect)
-        frame = Polygon(outerRing)
-        frame = frame.difference(self.boardSubstrate.exterior().buffer(slotWidth))
-        self.appendSubstrate(frame)
-
-        tabs = list([t.buffer(fromMm(0.01), join_style=2) for t in tabs])
-        self.appendSubstrate(tabs)
-
-        if verTabCount != 0 or horTabCount != 0:
-            self.boardSubstrate.removeIslands()
-
-        return (outerRect, cuts)
-
-
-    def makeFrame(self, innerArea, width, height, offset):
-        """
-        Adds a frame around given `innerArea` (`wxRect`), which can be obtained,
-        e.g., by `makeGrid`, with given `width` and `height`. Space with width
-        `offset` is added around the `innerArea`.
-        """
-        innerAreaExpanded = expandRect(innerArea, offset-fromMm(0.01))
-        xDiff = (width - innerAreaExpanded.GetWidth()) // 2
-        if xDiff < 0:
-            raise RuntimeError("The frame is to small")
-        yDiff = (height - innerAreaExpanded.GetHeight()) // 2
-        if yDiff < 0:
-            raise RuntimeError("The frame is to small")
-        innerRing = rectToRing(innerAreaExpanded)
-        outerRect = expandRect(innerAreaExpanded, xDiff, yDiff)
-        outerRing = rectToRing(outerRect)
+    def makeFrame(self, width):
+        """
+        Build a frame around the board. Return frame cuts
+        """
+        frameInnerRect = expandRect(self.boardSubstrate.boundingBox(), -SHP_EPSILON)
+        frameOuterRect = expandRect(frameInnerRect, width)
+        outerRing = rectToRing(frameOuterRect)
+        innerRing = rectToRing(frameInnerRect)
         polygon = Polygon(outerRing, [innerRing])
-        frame_cuts_v = self.makeFrameCutsV( innerArea, innerAreaExpanded, outerRect )
-        frame_cuts_h = self.makeFrameCutsH( innerArea, innerAreaExpanded, outerRect )
         self.appendSubstrate(polygon)
-        return (outerRect, frame_cuts_v, frame_cuts_h)
+
+        innerArea = self.substrates[0].boundingBox()
+        for s in self.substrates:
+            innerArea = combineBoundingBoxes(innerArea, s.boundingBox())
+        frameCutsV = self.makeFrameCutsV(innerArea, frameInnerRect, frameOuterRect)
+        frameCutsH = self.makeFrameCutsH(innerArea, frameInnerRect, frameOuterRect)
+        return chain(frameCutsV, frameCutsH)
+
+    def makeTightFrame(self, width, slotwidth):
+        """
+        Build a full frame with board perimeter milled out
+        """
+        self.makeFrame(width)
+        boardSlot = GeometryCollection()
+        for s in self.substrates:
+            boardSlot = boardSlot.union(s.exterior())
+        boardSlot = boardSlot.buffer(slotwidth)
+        frameBody = box(*self.boardSubstrate.bounds()).difference(boardSlot)
+        self.appendSubstrate(frameBody)
 
     def makeRailsTb(self, thickness):
         """
         Adds a rail to top and bottom.
         """
-        minx, miny, maxx, maxy = self.boardSubstrate.bounds()
-        topRail = box(minx, maxy - fromMm(0.001), maxx, maxy + thickness)
-        bottomRail = box(minx, miny + fromMm(0.001), maxx, miny - thickness)
+        minx, miny, maxx, maxy = self.panelBBox()
+        thickness -= SHP_EPSILON
+        topRail = box(minx, maxy - SHP_EPSILON, maxx, maxy + thickness)
+        bottomRail = box(minx, miny + SHP_EPSILON, maxx, miny - thickness)
         self.appendSubstrate(topRail)
         self.appendSubstrate(bottomRail)
 
@@ -819,28 +748,35 @@ def makeRailsLr(self, thickness):
         """
         Adds a rail to left and right.
         """
-        minx, miny, maxx, maxy = self.boardSubstrate.bounds()
-        leftRail = box(minx - thickness + fromMm(0.001), miny, minx, maxy)
-        rightRail = box(maxx - fromMm(0.001), miny, maxx + thickness, maxy)
+        minx, miny, maxx, maxy = self.panelBBox()
+        thickness -= SHP_EPSILON
+        leftRail = box(minx - thickness + SHP_EPSILON, miny, minx, maxy)
+        rightRail = box(maxx - SHP_EPSILON, miny, maxx + thickness, maxy)
         self.appendSubstrate(leftRail)
         self.appendSubstrate(rightRail)
 
-    def makeFrameCutsV(self, innerArea, innerAreaExpanded, outerArea ):
+    def makeFrameCutsV(self, innerArea, frameInnerArea, outerArea):
+        """
+        Generate vertical cuts for the frame corners and return them
+        """
         x_coords = [ innerArea.GetX(),
                      innerArea.GetX() + innerArea.GetWidth() ]
-        y_coords = sorted([ innerAreaExpanded.GetY(),
-                            innerAreaExpanded.GetY() + innerAreaExpanded.GetHeight(),
+        y_coords = sorted([ frameInnerArea.GetY(),
+                            frameInnerArea.GetY() + frameInnerArea.GetHeight(),
                             outerArea.GetY(),
                             outerArea.GetY() + outerArea.GetHeight() ])
         cuts =  [ [(x_coord, y_coords[0]), (x_coord, y_coords[1])] for x_coord in x_coords ]
         cuts += [ [(x_coord, y_coords[2]), (x_coord, y_coords[3])] for x_coord in x_coords ]
         return map(LineString, cuts)
 
-    def makeFrameCutsH(self, innerArea, innerAreaExpanded, outerArea ):
+    def makeFrameCutsH(self, innerArea, frameInnerArea, outerArea):
+        """
+        Generate horizontal cuts for the frame corners and return them
+        """
         y_coords = [ innerArea.GetY(),
                      innerArea.GetY() + innerArea.GetHeight() ]
-        x_coords = sorted([ innerAreaExpanded.GetX(),
-                            innerAreaExpanded.GetX() + innerAreaExpanded.GetWidth(),
+        x_coords = sorted([ frameInnerArea.GetX(),
+                            frameInnerArea.GetX() + frameInnerArea.GetWidth(),
                             outerArea.GetX(),
                             outerArea.GetX() + outerArea.GetWidth() ])
         cuts =  [ [(x_coords[0], y_coord), (x_coords[1], y_coord)] for y_coord in y_coords ]
@@ -854,7 +790,7 @@ def makeVCuts(self, cuts, boundCurves=False):
         Otherwise, raise an exception.
         """
         for cut in cuts:
-            if len(cut.simplify(fromMm(0.01)).coords) > 2 and not boundCurves:
+            if len(cut.simplify(SHP_EPSILON).coords) > 2 and not boundCurves:
                 raise RuntimeError("Cannot V-Cut a curve")
             start = roundPoint(cut.coords[0])
             end = roundPoint(cut.coords[-1])
@@ -865,13 +801,16 @@ def makeVCuts(self, cuts, boundCurves=False):
             else:
                 raise RuntimeError("Cannot perform V-Cut which is not horizontal or vertical")
 
-    def makeMouseBites(self, cuts, diameter, spacing, offset=fromMm(0.25)):
+    def makeMouseBites(self, cuts, diameter, spacing, offset=fromMm(0.25),
+        prolongation=fromMm(0.5)):
         """
-        Take a list of cuts and perform mouse bites.
+        Take a list of cuts and perform mouse bites. The cuts can be prolonged
+        to
         """
-        bloatedSubstrate = prep(self.boardSubstrate.substrates.buffer(fromMm(0.01)))
+        bloatedSubstrate = prep(self.boardSubstrate.substrates.buffer(SHP_EPSILON))
         for cut in cuts:
-            cut = cut.simplify(fromMm(0.001)) # Remove self-intersecting geometry
+            cut = cut.simplify(SHP_EPSILON) # Remove self-intersecting geometry
+            cut = prolongCut(cut, prolongation)
             offsetCut = cut.parallel_offset(offset, "left")
             length = offsetCut.length
             count = int(length / spacing) + 1
@@ -880,19 +819,24 @@ def makeMouseBites(self, cuts, diameter, spacing, offset=fromMm(0.25)):
                     hole = offsetCut.interpolate(0.5, normalized=True)
                 else:
                     hole = offsetCut.interpolate( i * length / (count - 1) )
-                if bloatedSubstrate.intersects(hole.buffer(0.8 * diameter / 2)):
+                if bloatedSubstrate.intersects(hole):
                     self.addNPTHole(wxPoint(hole.x, hole.y), diameter)
 
-    def addNPTHole(self, position, diameter):
+    def addNPTHole(self, position, diameter, paste=False):
         """
         Add a drilled non-plated hole to the position (`wxPoint`) with given
-        diameter.
+        diameter. The paste option allows to place the hole on the paste layers.
         """
         module = pcbnew.PCB_IO().FootprintLoad(KIKIT_LIB, "NPTH")
         module.SetPosition(position)
         for pad in module.Pads():
             pad.SetDrillSize(pcbnew.wxSize(diameter, diameter))
             pad.SetSize(pcbnew.wxSize(diameter, diameter))
+            if paste:
+                layerSet = pad.GetLayerSet()
+                layerSet.AddLayer(Layer.F_Paste)
+                layerSet.AddLayer(Layer.B_Paste)
+                pad.SetLayerSet(layerSet)
         self.board.Add(module)
 
     def addFiducial(self, position, copperDiameter, openingDiameter, bottom=False):
@@ -914,36 +858,42 @@ def addFiducial(self, position, copperDiameter, openingDiameter, bottom=False):
             pad.SetLocalClearance(int((openingDiameter - copperDiameter) / 2))
         self.board.Add(module)
 
-    def addFiducials(self, horizontalOffset, verticalOffset, copperDiameter, openingDiameter):
+    def panelCorners(self, horizontalOffset=0, verticalOffset=0):
         """
-        Add 3 fiducials to the top-left, top-right and bottom-left corner of the
-        board. This function expects there is enough space on the
-        board/frame/rail to place the feature.
-
-        The offsets are measured from the outer edges of the substrate.
+        Return the list of top-left, top-right, bottom-left and bottom-right
+        corners of the panel. You can specify offsets.
         """
-        minx, miny, maxx, maxy = self.boardSubstrate.bounds()
-        topLeft= wxPoint(minx + horizontalOffset, miny + verticalOffset)
+        minx, miny, maxx, maxy = self.panelBBox()
+        topLeft = wxPoint(minx + horizontalOffset, miny + verticalOffset)
         topRight = wxPoint(maxx - horizontalOffset, miny + verticalOffset)
         bottomLeft = wxPoint(minx + horizontalOffset, maxy - verticalOffset)
-        for pos in [topLeft, topRight, bottomLeft]:
+        bottomRight = wxPoint(maxx - horizontalOffset, maxy - verticalOffset)
+        return [topLeft, topRight, bottomLeft, bottomRight]
+
+    def addCornerFiducials(self, fidCount, horizontalOffset, verticalOffset,
+            copperDiameter, openingDiameter):
+        """
+        Add up to 4 fiducials to the top-left, top-right, bottom-left and
+        bottom-right corner of the board (in this order). This function expects
+        there is enough space on the board/frame/rail to place the feature.
+
+        The offsets are measured from the outer edges of the substrate.
+        """
+        for pos in self.panelCorners(horizontalOffset, verticalOffset)[:fidCount]:
             self.addFiducial(pos, copperDiameter, openingDiameter, False)
             self.addFiducial(pos, copperDiameter, openingDiameter, True)
 
-    def addTooling(self, horizontalOffset, verticalOffset, diameter):
+    def addCornerTooling(self, holeCount, horizontalOffset, verticalOffset,
+                         diameter, paste=False):
         """
-        Add 3 tooling holes to the top-left, top-right and bottom-left corner of
-        the board. This function expects there is enough space on the
-        board/frame/rail to place the feature.
+        Add up to 4 tooling holes to the top-left, top-right, bottom-left and
+        bottom-right corner of the board (in this order). This function expects
+        there is enough space on the board/frame/rail to place the feature.
 
         The offsets are measured from the outer edges of the substrate.
         """
-        minx, miny, maxx, maxy = self.boardSubstrate.bounds()
-        topLeft= wxPoint(minx + horizontalOffset, miny + verticalOffset)
-        topRight = wxPoint(maxx - horizontalOffset, miny + verticalOffset)
-        bottomLeft = wxPoint(minx + horizontalOffset, maxy - verticalOffset)
-        for pos in [topLeft, topRight, bottomLeft]:
-            self.addNPTHole(pos, diameter)
+        for pos in self.panelCorners(horizontalOffset, verticalOffset)[:holeCount]:
+            self.addNPTHole(pos, diameter, paste)
 
     def addMillFillets(self, millRadius):
         """
@@ -952,34 +902,141 @@ def addMillFillets(self, millRadius):
         """
         self.boardSubstrate.millFillets(millRadius)
 
-    def layerToTabs(self, layerName, tabWidth):
+    def clearTabsAnnotations(self):
         """
-        Take all line pcbshapes from the given layer and convert them to
-        tabs.
-
-        The tabs are created by placing a tab origin into the line starting
-        point and spanning the tab in the line direction up to the line length.
-        Therefore, it is necessary that the line is long enough to penetarete
-        the boardoutline.
+        Remove all existing tab annotations from the panel.
+        """
+        for s in self.substrates:
+            s.annotations = list(
+                filter(lambda x: not isinstance(x, TabAnnotation), s.annotations))
 
-        The lines are deleted from panel.
+    def buildTabsFromAnnotations(self):
+        """
+        Given annotations for the individual substrates, create tabs for them.
+        Tabs are appended to the panel, cuts are returned.
 
-        Returns list of tabs substrates and a list of cuts to perform.
+        Expects that a valid partition line is assigned to the the panel.
         """
-        lines = [element for element in self.board.GetDrawings()
-                    if isinstance(element, pcbnew.PCB_SHAPE) and
-                       element.GetShape() == STROKE_T.S_SEGMENT and
-                       element.GetLayerName() == layerName]
         tabs, cuts = [], []
-        for line in lines:
-            origin = line.GetStart()
-            direction = line.GetEnd() - line.GetStart()
-            tab, cut = self.boardSubstrate.tab(origin, direction, tabWidth,
-                line.GetLength())
-            tabs.append(tab)
-            cuts.append(cut)
-            self.board.Remove(line)
-        return tabs, cuts
+        for s in self.substrates:
+            t, c = buildTabs(s, s.partitionLine, s.annotations)
+            tabs.extend(t)
+            cuts.extend(c)
+        self.boardSubstrate.union(tabs)
+        return cuts
+
+    def _buildTabAnnotationForEdge(self, edge, dir, count, width):
+        """
+        Given an edge as AxialLine, dir and count, return a list of
+        annotations.
+        """
+        pos = lambda offset: (
+            abs(dir[0]) * edge.x + abs(dir[1]) * (edge.min + offset),
+            abs(dir[1]) * edge.x + abs(dir[0]) * (edge.min + offset))
+        return [TabAnnotation(None, pos(offset), dir, width)
+            for offset in tabSpacing(edge.length, count)]
+
+    def _buildTabAnnotations(self, countFn, widthFn, ghostSubstrates):
+        """
+        Add tab annotations for the individual substrates based on their
+        bounding boxes. Assign tabs annotations to the edges of the bounding
+        box. You provides a function countFn, widthFn that take edge length and
+        direction that return number of tabs per edge or tab width
+        respectively.
+
+        You can also specify ghost substrates (for the future framing).
+        """
+        neighbors = substrate.SubstrateNeighbors(self.substrates + ghostSubstrates)
+        S = substrate.SubstrateNeighbors
+        sides = [
+            (S.leftC, shpBBoxLeft, [1, 0]),
+            (S.rightC, shpBBoxRight,[-1, 0]),
+            (S.topC, shpBBoxTop, [0, 1]),
+            (S.bottomC, shpBBoxBottom,[0, -1])
+        ]
+        for i, s in enumerate(self.substrates):
+            for query, side, dir in sides:
+                for n, shadow in query(neighbors, s):
+                    edge = side(s.bounds())
+                    for section in shadow.intervals:
+                        edge.min, edge.max = section.min, section.max
+                        tWidth = widthFn(edge.length, dir)
+                        tCount = countFn(edge.length, dir)
+                        a = self._buildTabAnnotationForEdge(edge, dir, tCount, tWidth)
+                        self.substrates[i].annotations.extend(a)
+
+    def buildTabAnnotationsFixed(self, hcount, vcount, hwidth, vwidth,
+            minDistance, ghostSubstrates):
+        """
+        Add tab annotations for the individual substrates based on number of
+        tabs in horizontal and vertical direction. You can specify individual
+        width in each direction.
+
+        If the edge is short for the specified number of tabs with given minimal
+        spacing, the count is reduced.
+
+        You can also specify ghost substrates (for the future framing).
+        """
+        def widthFn(edgeLength, dir):
+            return abs(dir[0]) * hwidth + abs(dir[1]) * vwidth
+        def countFn(edgeLength, dir):
+            countLimit = abs(dir[0]) * hcount + abs(dir[1]) * vcount
+            width = widthFn(edgeLength, dir)
+            return min(countLimit, maxTabCount(edgeLength, width, minDistance))
+        return self._buildTabAnnotations(countFn, widthFn, ghostSubstrates)
+
+
+    def buildTabAnnotationsSpacing(self, spacing, hwidth, vwidth, ghostSubstrates):
+        """
+        Add tab annotations for the individual substrates based on their spacing.
+
+        You can also specify ghost substrates (for the future framing).
+        """
+        def widthFn(edgeLength, dir):
+            return abs(dir[0]) * hwidth + abs(dir[1]) * vwidth
+        def countFn(edgeLength, dir):
+            return maxTabCount(edgeLength, widthFn(edgeLength, dir), spacing)
+        return self._buildTabAnnotations(countFn, widthFn, ghostSubstrates)
+
+    def buildTabAnnotationsCorners(self, width):
+        """
+        Add tab annotations to the corners of the individual substrates.
+        """
+        for i, s in enumerate(self.substrates):
+            minx, miny, maxx, maxy = s.bounds()
+            midx = (minx + maxx) / 2
+            midy = (miny + maxy) / 2
+
+            for x, y in product([minx, maxx], [miny, maxy]):
+                dir = normalize((midx - x, midy - y))
+                a = TabAnnotation(None, (x, y), dir, width)
+                self.substrates[i].annotations.append(a)
+
+
+    def buildFullTabs(self):
+        """
+        Make full tabs. This strategy basically cuts the bounding boxes of the
+        PCBs. Not suitable for mousebites. Expects there is a valid partition
+        line.
+
+        Return a list of cuts.
+        """
+        # Compute the bounding box gap polygon. Note that we cannot just merge a
+        # rectangle as that would remove internal holes
+        bBoxes = box(*self.substrates[0].bounds())
+        for s in islice(self.substrates, 1, None):
+            bBoxes = bBoxes.union(box(*s.bounds()))
+        outerBounds = self.substrates[0].partitionLine.bounds
+        for s in islice(self.substrates, 1, None):
+            outerBounds = shpBBoxMerge(outerBounds, s.partitionLine.bounds)
+        fill = box(*outerBounds).difference(bBoxes.buffer(SHP_EPSILON))
+        self.appendSubstrate(fill.buffer(SHP_EPSILON))
+
+        # Make the cuts
+        substrateBoundaries = [linestringToSegments(s.partitionLine)
+            for s in self.substrates]
+        substrateCuts = [LineString(x) for x in chain(*substrateBoundaries)]
+        return substrateCuts
 
     def inheritCopperLayers(self, board):
         """
@@ -1019,3 +1076,233 @@ def copperFillNonBoardAreas(self):
         zoneContainer.SetLayer(Layer.B_Cu)
         self.board.Add(zoneContainer)
         self.zonesToRefill.append(zoneContainer)
+
+    def addKeepout(self, area, noTracks=True, noVias=True, noCopper=True):
+        """
+        Add a keepout area from top and bottom layers. Area is a shapely
+        polygon. Return the keepout area.
+        """
+        zone = polygonToZone(area, self.board)
+        zone.SetIsKeepout(True)
+        zone.SetDoNotAllowTracks(noTracks)
+        zone.SetDoNotAllowVias(noVias)
+        zone.SetDoNotAllowCopperPour(noCopper)
+
+        zone.SetLayer(Layer.F_Cu)
+        layerSet = zone.GetLayerSet()
+        layerSet.AddLayer(Layer.B_Cu)
+        zone.SetLayerSet(layerSet)
+
+        self.board.Add(zone)
+        return zone
+
+    def addText(self, text, position, orientation=0,
+                width=fromMm(1.5), height=fromMm(1.5), thickness=fromMm(0.3),
+                hJustify=EDA_TEXT_HJUSTIFY_T.GR_TEXT_HJUSTIFY_CENTER,
+                vJustify=EDA_TEXT_VJUSTIFY_T.GR_TEXT_VJUSTIFY_CENTER,
+                layer=Layer.F_SilkS):
+        """
+        Add text at given position to the panel. If appending to the bottom
+        side, text is automatically mirrored.
+        """
+        textObject = pcbnew.PCB_TEXT(self.board)
+        textObject.SetText(text)
+        textObject.SetTextX(position[0])
+        textObject.SetTextY(position[1])
+        textObject.SetThickness(thickness)
+        textObject.SetTextSize(pcbnew.wxSize(width, height))
+        textObject.SetHorizJustify(hJustify)
+        textObject.SetVertJustify(vJustify)
+        textObject.SetTextAngle(orientation)
+        textObject.SetLayer(layer)
+        textObject.SetMirrored(isBottomLayer(layer))
+        self.board.Add(textObject)
+
+    def setAuxiliaryOrigin(self, point):
+        """
+        Set the auxiliary origin used e.g., for drill files
+        """
+        self.board.SetAuxOrigin(point)
+
+    def setGridOrigin(self, point):
+        """
+        Set grid origin
+        """
+        self.board.SetGridOrigin(point)
+
+    def _buildPartitionLineFromBB(self, partition):
+        for s in self.substrates:
+            hLines, vLines = partition.partitionSubstrate(s)
+            hSLines = [((l.min, l.x), (l.max, l.x)) for l in hLines]
+            vSLines = [((l.x, l.min), (l.x, l.max)) for l in vLines]
+            lines = hSLines + vSLines
+            multiline = shapely.ops.linemerge(lines)
+            multiline = normalizePartitionLineOrientation(multiline)
+
+            s.partitionLine = multiline
+
+    def _buildBackboneLineFromBB(self, partition, boundarySubstrates):
+        hBoneLines, vBoneLines = set(), set()
+        for s in self.substrates:
+            hLines, vLines = partition.partitionSubstrate(s)
+            hBoneLines.update(hLines)
+            vBoneLines.update(vLines)
+        for s in boundarySubstrates:
+            hLines, vLines = partition.partitionSubstrate(s)
+            for l in hLines:
+                hBoneLines.remove(l)
+            for l in vLines:
+                vBoneLines.remove(l)
+        minx, miny, maxx, maxy = self.boardSubstrate.bounds()
+        # Cut backbone on substrates boundaries:
+        cut = lambda xs, y: chain(*[x.cut(y) for x in xs])
+        hBoneLines = cut(cut(hBoneLines, minx), maxx)
+        vBoneLines = cut(cut(vBoneLines, miny), maxy)
+        hBLines = [LineString([(l.min, l.x), (l.max, l.x)]) for l in hBoneLines]
+        vBLines = [LineString([(l.x, l.min), (l.x, l.max)]) for l in vBoneLines]
+        self.backboneLines = list(chain(hBLines, vBLines))
+
+    def buildPartitionLineFromBB(self, boundarySubstrates=[], safeMargin=0):
+        """
+        Builds partition & backbone line from bounding boxes of the substrates.
+        You can optionally pass extra substrates (e.g., for frame).
+        """
+        partition = substrate.SubstratePartitionLines(
+            self.substrates, boundarySubstrates,
+            safeMargin, safeMargin)
+        self._buildPartitionLineFromBB(partition)
+        self._buildBackboneLineFromBB(partition, boundarySubstrates)
+
+    def addLine(self, start, end, thickness, layer):
+        """
+        Add a line to the panel based on starting and ending point
+        """
+        segment = pcbnew.PCB_SHAPE()
+        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]))
+        self.board.Add(segment)
+        return segment
+
+    def _renderLines(self, lines, layer, thickness=fromMm(0.5)):
+        for geom in lines:
+            for linestring in listGeometries(geom):
+                for start, end in linestringToSegments(linestring):
+                    self.addLine(start, end, thickness, layer)
+
+    def debugRenderPartitionLines(self):
+        """
+        Render partition line to the panel to be easily able to inspect them via
+        Pcbnew.
+        """
+        lines = [s.paritionLine for s in self.substrates]
+        self._renderLines(lines, Layer.Eco1_User, fromMm(0.5))
+
+    def debugRenderBackboneLines(self):
+        """
+        Render partition line to the panel to be easily able to inspect them via
+        Pcbnew.
+        """
+        self._renderLines(self.backboneLines, Layer.Eco2_User, fromMm(0.5))
+
+    def debugRenderBoundingBoxes(self):
+        lines = [box(*s.bounds()).exterior for s in self.substrates]
+        self._renderLines(lines, Layer.Cmts_User, fromMm(0.5))
+
+    def renderBackbone(self, vthickness, hthickness, vcut, hcut):
+        """
+        Render horizontal and vertical backbone lines. If zero thickness is
+        specified, no backbone is rendered.
+
+        vcut, hcut specifies if vertical or horizontal backbones should be cut.
+
+        Return a list of cuts
+        """
+        cutpoints = commonPoints(self.backboneLines)
+        pieces, cuts = [], []
+        for l in self.backboneLines:
+            start = l.coords[0]
+            end = l.coords[1]
+            if isHorizontal(start, end) and hthickness > 0:
+                minX = min(start[0], end[0])
+                maxX = max(start[0], end[0])
+                bb = box(minX, start[1] - hthickness // 2,
+                         maxX, start[1] + hthickness // 2)
+                pieces.append(bb)
+                if not hcut:
+                    continue
+
+                candidates = []
+
+                if cutpoints[start] > 2:
+                    candidates.append(((start[0] + vthickness // 2, start[1]), -1))
+
+                if cutpoints[end] == 2:
+                    candidates.append((end, 1))
+                elif cutpoints[end] > 2:
+                    candidates.append(((end[0] - vthickness // 2, end[1]), 1))
+
+                for x, c in candidates:
+                    cut = LineString([
+                        (x[0], x[1] - c * hthickness // 2),
+                        (x[0], x[1] + c * hthickness // 2)])
+                    cuts.append(cut)
+            if isVertical(start, end) and vthickness > 0:
+                minY = min(start[1], end[1])
+                maxY = max(start[1], end[1])
+                bb = box(start[0] - vthickness // 2, minY,
+                         start[0] + vthickness // 2, maxY)
+                pieces.append(bb)
+                if not vcut:
+                    continue
+
+                candidates = []
+
+                if cutpoints[start] > 2:
+                    candidates.append(((start[0], start[1] + hthickness // 2), 1))
+
+                if cutpoints[end] == 2:
+                    candidates.append((end, -1))
+                elif cutpoints[end] > 2:
+                    candidates.append(((end[0], end[1] - hthickness // 2), -1))
+
+                for x, c in candidates:
+                    cut = LineString([
+                        (x[0] - c * vthickness // 2, x[1]),
+                        (x[0] + c * vthickness // 2, x[1])])
+                    cuts.append(cut)
+        backbones = list([b.buffer(SHP_EPSILON, join_style=2) for b in pieces])
+        self.appendSubstrate(backbones)
+        return cuts
+
+def getFootprintByReference(board, reference):
+    """
+    Return a footprint by with given reference
+    """
+    for f in board.GetFootprints():
+        if f.GetReference() == reference:
+            return f
+    raise RuntimeError(f"Footprint with reference '{reference}' not found")
+
+def extractSourceAreaByAnnotation(board, reference):
+    """
+    Given a board and a reference to annotation in the form of symbol
+    `kikit:Board`, extract the source area. The source area is a bounding box of
+    continuous lines in the Edge.Cuts on which the arrow in reference point.
+    """
+    annotation = getFootprintByReference(board, reference)
+    tip = annotation.GetPosition()
+    edges = collectEdges(board, "Edge.Cuts")
+    # KiCAD 6 will need an adjustment - method Collide was introduced with
+    # different parameters. But v6 API is not available yet, so we leave this
+    # to future ourselves.
+    pointedAt = indexOf(edges, lambda x: x.HitTest(tip))
+    rings = extractRings(edges)
+    ringPointedAt = indexOf(rings, lambda x: pointedAt in x)
+    if ringPointedAt == -1:
+        raise RuntimeError("Annotation symbol '{reference}' does not point to a board edge")
+    return findBoundingBox([edges[i] for i in rings[ringPointedAt]])
+
+
diff --git a/kikit/panelize_ui.py b/kikit/panelize_ui.py
index f4f7482d..e023d32c 100644
--- a/kikit/panelize_ui.py
+++ b/kikit/panelize_ui.py
@@ -1,60 +1,177 @@
 import click
+import os
+import csv
+import io
+import traceback
+from kikit.units import readLength, readAngle
 
-def validateSpaceRadius(space, radius):
-    if space <= 0:
-        return
-    if space < 2 * radius:
-        raise RuntimeError(f"Fillet radius ({radius} mm) should to be less than " \
-                           f"half space between boards ({space} mm).")
+def splitStr(delimiter, escapeChar, s):
+    """
+    Splits s based on delimiter that can be escaped via escapeChar
+    """
+    # Let's use csv reader to implement this
+    reader = csv.reader(io.StringIO(s), delimiter=delimiter, escapechar=escapeChar)
+    # Unpack first line
+    for x in reader:
+        return x
 
-def getPlacementClass(name):
-    from kikit.panelize import (BasicGridPosition, OddEvenColumnPosition,
-        OddEvenRowsPosition, OddEvenRowsColumnsPosition)
-    mapping = {
-        "none": BasicGridPosition,
-        "rows": OddEvenRowsPosition,
-        "cols": OddEvenColumnPosition,
-        "rowsCols": OddEvenRowsColumnsPosition
-    }
-    try:
-        return mapping[name]
-    except KeyError:
-        raise RuntimeError(f"Invalid alternation option '{name}' passed. " +
-            "Valid options are: " + ", ".join(mapping.keys()))
 
-@click.group()
-def panelize():
+class Section(click.ParamType):
     """
-    Create a simple predefined panel patterns
+    A CLI argument type for overriding section parameters. Basically a semicolon
+    separated list of `key: value` pairs. The first word might omit the key; in
+    that case "type" key is used.
     """
+    name = "parameter_list"
+
+    def convert(self, value, param, ctx):
+        if len(value.strip()) == 0:
+            self.fail(f"{value} is not a valid argument specification",
+                param, ct)
+        try:
+            values = {}
+            for i, pair in enumerate(splitStr(";", "\\", value)):
+                if len(pair.strip()) == 0:
+                    continue
+                s = pair.split(":")
+                if i == 0 and len(s) == 1:
+                    values["type"] = s[0].strip()
+                    continue
+                key, value = s[0].strip(), s[1].strip()
+                values[key] = value
+            return values
+        except (TypeError, IndexError):
+            self.fail(f"'{pair}' is not a valid key: value pair",
+                param,
+                ctx)
+
+class PresetError(RuntimeError):
     pass
 
 @click.command()
 @click.argument("input", type=click.Path(dir_okay=False))
 @click.argument("output", type=click.Path(dir_okay=False))
-@click.option("--sourcearea", "-s", type=(float, float, float, float), help="x y w h in millimeters")
-def extractBoard(input, output, sourcearea):
+@click.option("--preset", "-p", multiple=True,
+    help="A panelization preset file; use prefix ':' for built-in styles.")
+@click.option("--layout", "-l", type=Section(),
+    help="Override layout settings.")
+@click.option("--source", "-s", type=Section(),
+    help="Override source settings.")
+@click.option("--tabs", "-t", type=Section(),
+    help="Override tab settings.")
+@click.option("--cuts", "-c", type=Section(),
+    help="Override cut settings.")
+@click.option("--framing", "-r", type=Section(),
+    help="Override framing settings.")
+@click.option("--tooling", "-o", type=Section(),
+    help="Override tooling settings.")
+@click.option("--fiducials", "-f", type=Section(),
+    help="Override fiducials settings.")
+@click.option("--text", "-t", type=Section(),
+    help="Override text settings.")
+@click.option("--post", "-z", type=Section(),
+    help="Override post processing settings settings.")
+@click.option("--debug", type=Section(),
+    help="Include debug traces or drawings in the panel.")
+@click.option("--dump", "-d", type=click.Path(file_okay=True, dir_okay=False),
+    help="Dump constructured preset into a JSON file.")
+def panelize(input, output, preset, layout, source, tabs, cuts, framing,
+                tooling, fiducials, text, post, debug, dump):
+    try:
+        # Hide the import in the function to make KiKit start faster
+        from kikit import panelize_ui_impl as ki
+        from kikit.panelize import Panel
+        from pcbnew import LoadBoard, wxPointMM
+        import commentjson
+        import sys
+        from itertools import chain
+
+
+        preset = ki.obtainPreset(preset,
+            layout=layout, source=source, tabs=tabs, cuts=cuts, framing=framing,
+            tooling=tooling, fiducials=fiducials, text=text, post=post, debug=debug)
+
+        board = LoadBoard(input)
+
+        panel = Panel()
+        panel.inheritDesignSettings(input)
+        panel.inheritProperties(input)
+
+        sourceArea = ki.readSourceArea(preset["source"], board)
+        substrates = ki.buildLayout(preset["layout"], panel, input, sourceArea)
+        framingSubstrates = ki.dummyFramingSubstrate(substrates,
+            ki.frameOffset(preset["framing"]))
+        panel.buildPartitionLineFromBB(framingSubstrates)
+
+        tabCuts = ki.buildTabs(preset["tabs"], panel, substrates,
+            framingSubstrates)
+        backboneCuts = ki.buildBackBone(preset["layout"], panel, substrates,
+            ki.frameOffset(preset["framing"]))
+        frameCuts = ki.buildFraming(preset["framing"], panel)
+
+        ki.buildTooling(preset["tooling"], panel)
+        ki.buildFiducials(preset["fiducials"], panel)
+        ki.buildText(preset["text"], panel)
+        ki.buildPostprocessing(preset["post"], panel)
+
+        ki.makeTabCuts(preset["cuts"], panel, tabCuts)
+        ki.makeOtherCuts(preset["cuts"], panel, chain(backboneCuts, frameCuts))
+
+        ki.runUserScript(preset["post"], panel)
+
+        ki.buildDebugAnnotation(preset["debug"], panel)
+
+        panel.save(output)
+
+        if (dump):
+            with open(dump, "w") as f:
+                f.write(commentjson.dumps(preset, indent=4))
+    except Exception as e:
+        sys.stderr.write("An error occurred: " + str(e) + "\n")
+        sys.stderr.write("No output files produced\n")
+        if isinstance(preset, dict) and preset["debug"]["trace"]:
+            traceback.print_exc(file=sys.stderr)
+        sys.exit(1)
+
+@click.command()
+@click.argument("input", type=click.Path(dir_okay=False))
+@click.argument("output", type=click.Path(dir_okay=False))
+@click.option("--source", "-s", type=Section(),
+    help="Specify source settings.")
+@click.option("--debug", type=Section(),
+    help="Include debug traces or drawings in the panel.")
+def separate(input, output, source, debug):
     """
-    Extract a single board out of a file
+    Separate a single board out of a multi-board design. The separated board is
+    placed in the middle of the sheet.
 
-    The extracted board is placed in the middle of the sheet
+    You can specify the board via bounding box or annotation. See documentation
+    for further details on usage.
     """
-    # Hide the import in the function to make KiKit start faster
-    from kikit.panelize import Panel, fromMm, wxPointMM, wxRectMM, fromDegrees
-    import sys
     try:
+        from kikit import panelize_ui_impl as ki
+        from kikit.panelize import Panel
+        from pcbnew import LoadBoard, wxPointMM
+
+        preset = ki.obtainPreset([], validate=False, source=source, debug=debug)
+
+        board = LoadBoard(input)
+        sourceArea = ki.readSourceArea(preset["source"], board)
+
         panel = Panel()
-        destination = wxPointMM(150, 100)
-        area = wxRectMM(*sourcearea)
         panel.inheritDesignSettings(input)
         panel.inheritProperties(input)
-        panel.appendBoard(input, destination, area, tolerance=fromMm(2))
+        destination = wxPointMM(150, 100)
+        panel.appendBoard(input, destination, sourceArea)
         panel.save(output)
     except Exception as e:
         sys.stderr.write("An error occurred: " + str(e) + "\n")
         sys.stderr.write("No output files produced\n")
+        if isinstance(preset, dict) and preset["debug"]["trace"]:
+            traceback.print_exc(file=sys.stderr)
         sys.exit(1)
 
+
 @click.command()
 @click.argument("input", type=click.Path(dir_okay=False))
 @click.argument("output", type=click.Path(dir_okay=False))
@@ -116,6 +233,7 @@ def grid(input, output, space, hspace, vspace, gridsize, panelsize, tabwidth,
     """
     # Hide the import in the function to make KiKit start faster
     from kikit.panelize import Panel, fromMm, wxPointMM, wxRectMM, fromDegrees
+    from kikit import panelize_ui_impl as ki
     import sys
     try:
         panel = Panel()
@@ -147,7 +265,7 @@ def grid(input, output, space, hspace, vspace, gridsize, panelsize, tabwidth,
             frame = False
             railslr = fromMm(railslr)
             oht = fromMm(hspace)
-        placementClass = getPlacementClass(alternation)
+        placementClass = ki.getPlacementClass(alternation)
 
         validateSpaceRadius(vspace, radius)
         validateSpaceRadius(hspace, radius)
@@ -308,7 +426,3 @@ def tightgrid(input, output, space, hspace, vspace, gridsize, panelsize,
         sys.stderr.write("An error occurred: " + str(e) + "\n")
         sys.stderr.write("No output files produced\n")
         sys.exit(1)
-
-panelize.add_command(extractBoard)
-panelize.add_command(grid)
-panelize.add_command(tightgrid)
\ No newline at end of file
diff --git a/kikit/panelize_ui_impl.py b/kikit/panelize_ui_impl.py
new file mode 100644
index 00000000..33b2f561
--- /dev/null
+++ b/kikit/panelize_ui_impl.py
@@ -0,0 +1,548 @@
+from kikit.panelize_ui import Section, PresetError
+from kikit.panelize import *
+from kikit.defs import Layer
+from shapely.geometry import box
+from kikit.units import readLength, readAngle
+from kikit.substrate import SubstrateNeighbors
+from kikit.common import resolveAnchor
+import commentjson
+import csv
+import io
+
+# This package exists as it is not needed for showing help and running other
+# commands, however, it has heavy dependencies (pcbnew) that take a second
+# to load. Splitting it speeds up KiKit's startup.
+
+PKG_BASE = os.path.dirname(__file__)
+PRESET_LIB = os.path.join(PKG_BASE, "resources/panelizePresets")
+
+def validatePresetLayout(preset):
+    if not isinstance(preset, dict):
+        raise PresetError("Preset is not a dictionary")
+    for name, section in preset.items():
+        if not isinstance(section, dict):
+            raise PresetError(f"Section '{name}' is not a dictionary")
+
+def validateChoice(sectionName, section, key, choices):
+    if section[key] not in choices:
+        c = ", ".join(choices)
+        raise PresetError(f"'{section[key]}' is not allowed for {sectionName}.{key}. Use one of {c}.")
+
+def readParameters(section, method, what):
+    for x in what:
+        if x in section:
+            section[x] = method(section[x])
+
+def readLayer(s):
+    return Layer[s.replace(".", "_")]
+
+def readBool(s):
+    sl = str(s).lower()
+    if sl in ["1", "true", "yes"]:
+        return True
+    if sl in ["0", "false", "no"]:
+        return False
+    raise PresetError(f"Uknown boolean value '{s}'")
+
+def readHJustify(s):
+    choices = {
+        "left": EDA_TEXT_HJUSTIFY_T.GR_TEXT_HJUSTIFY_LEFT,
+        "right": EDA_TEXT_HJUSTIFY_T.GR_TEXT_HJUSTIFY_RIGHT,
+        "center": EDA_TEXT_HJUSTIFY_T.GR_TEXT_HJUSTIFY_CENTER
+    }
+    if s in choices:
+        return choices[s]
+    raise PresetError(f"'{s}' is not valid justification value")
+
+def readVJustify(s):
+    choices = {
+        "top": EDA_TEXT_VJUSTIFY_T.GR_TEXT_VJUSTIFY_TOP,
+        "center": EDA_TEXT_VJUSTIFY_T.GR_TEXT_VJUSTIFY_CENTER,
+        "bottom": EDA_TEXT_VJUSTIFY_T.GR_TEXT_VJUSTIFY_BOTTOM
+    }
+    if s in choices:
+        return choices[s]
+    raise PresetError(f"'{s}' is not valid justification value")
+
+ANCHORS = ["tl", "tr", "bl", "br", "mt", "mb", "ml", "mr", "c"]
+
+def ppLayout(section):
+    validateChoice("layout", section, "type", ["grid"])
+    validateChoice("layout", section, "alternation",
+        ["none", "rows", "cols", "rowsCols"])
+    readParameters(section, readLength,
+        ["hspace", "vspace", "space", "hbackbone", "vbackbone"])
+    readParameters(section, readAngle, ["rotation"])
+    readParameters(section, int, ["rows", "cols"])
+    readParameters(section, readBool, ["vbonecut, hbonecut"])
+    # The space parameter overrides hspace and vspace
+    if "space" in section:
+        section["hspace"] = section["vspace"] = section["space"]
+
+def ppSource(section):
+    validateChoice("source", section, "type", ["auto", "rectangle", "annotation"])
+    readParameters(section, readLength,
+        ["tolerance", "tlx", "tly", "brx", "bry"])
+    readParameters(section, str, "ref")
+
+def ppTabs(section):
+    validateChoice("tabs", section, "type", ["none", "fixed", "spacing", "full",
+        "corner", "annotation"])
+    readParameters(section, readLength, ["vwidth", "hwidth", "width",
+        "mindistance", "spacing"])
+    readParameters(section, int, ["vcount", "hcount"])
+    if "width" in section:
+        section["vwidth"] = section["hwidth"] = section["width"]
+
+def ppCuts(section):
+    validateChoice("cuts", section, "type", ["none", "mousebites", "vcuts"])
+    readParameters(section, readLength, ["drill", "spacing", "offset",
+        "prolong", "clearance", "threshold"])
+    readParameters(section, readBool, ["cutcurves"])
+    readParameters(section, readLayer, ["layer"])
+
+def ppFraming(section):
+    validateChoice("framing", section, "type",
+        ["none", "railstb", "railslr", "frame", "tightframe"])
+    readParameters(section, readLength,
+        ["hspace", "vspace", "space", "width", "slotwidth"])
+    readParameters(section, readBool, ["cuts"])
+    # The space parameter overrides hspace and vspace
+    if "space" in section:
+        section["hspace"] = section["vspace"] = section["space"]
+
+def ppTooling(section):
+    validateChoice("tooling", section, "type", ["none", "3hole", "4hole"])
+    readParameters(section, readLength, ["hoffset", "voffset", "size"])
+    readParameters(section, readBool, ["paste"])
+
+def ppFiducials(section):
+    validateChoice("fiducials", section, "type", ["none", "3fid", "4fid"])
+    readParameters(section, readLength,
+        ["hoffset", "voffset", "coppersize", "opening"])
+
+def ppText(section):
+    validateChoice("text", section, "type", ["none", "simple"])
+    readParameters(section, readLength,
+        ["hoffset", "voffset", "width", "height", "thickness"])
+    readParameters(section, readHJustify, ["hjustify"])
+    readParameters(section, readVJustify, ["vjustify"])
+    readParameters(section, readLayer, ["layer"])
+    readParameters(section, readAngle, ["orientation"])
+    validateChoice("text", section, "anchor", ANCHORS)
+
+def ppPost(section):
+    validateChoice("type", section, "type", ["auto"])
+    readParameters(section, readBool, ["copperfill"])
+    readParameters(section, readLength, ["millradius"])
+    readParameters(section, str, ["script", "scriptarg"])
+    validateChoice("text", section, "origin", ANCHORS + [""])
+
+def ppDebug(section):
+    readParameters(section, readBool, ["drawPartitionLines",
+        "drawBackboneLines", "drawboxes", "trace"])
+
+def postProcessPreset(preset):
+    process = {
+        "layout": ppLayout,
+        "source": ppSource,
+        "tabs": ppTabs,
+        "cuts": ppCuts,
+        "framing": ppFraming,
+        "tooling": ppTooling,
+        "fiducials": ppFiducials,
+        "text": ppText,
+        "post": ppPost,
+        "debug": ppDebug
+    }
+    for name, section in preset.items():
+        process[name](section)
+
+def loadPreset(path):
+    """
+    Load a preset from path and perform simple validation on its structure.
+    Automatically resolves built-in styles (prefixed with :, omitting suffix).
+    """
+    if path.startswith(":"):
+        presetName = path
+        path = os.path.join(PRESET_LIB, path[1:] + ".json")
+        if not os.path.exists(path):
+            raise RuntimeError(f"Uknown built-in preset '{presetName}'")
+    try:
+        with open(path, "r") as f:
+            preset = commentjson.load(f)
+            validatePresetLayout(preset)
+            return preset
+    except OSError as e:
+        raise RuntimeError(f"Cannot open preset '{path}'")
+    except PresetError as e:
+        raise PresetError(f"{path}: {e}")
+
+def mergePresets(a, b):
+    """
+    Merge b into a. Values from b overwrite values from a.
+    """
+    for category in b:
+        if category not in a:
+            a[category] = {}
+        for key, value in b[category].items():
+            a[category][key] = value
+
+def loadPresetChain(chain):
+    """
+    Given a list of preset names (or paths), load the whole chain.
+    """
+    assert len(chain) > 0
+
+    preset = loadPreset(chain[0])
+    for p in chain[1:]:
+        newPreset = loadPreset(p)
+        mergePresets(preset, newPreset)
+    return preset
+
+def validateSections(preset):
+    """
+    Perform a logic validation of the given preset - e.g., if a style is applied,
+    validate all required keys are present. Ignores excessive keys.
+    """
+    VALID_SECTIONS = ["layout", "source", "tabs", "cuts", "framing", "tooling",
+        "fiducials", "text", "post", "debug"]
+    extraSections = set(preset.keys()).difference(VALID_SECTIONS)
+    if len(extraSections) != 0:
+        raise PresetError(f"Extra sections {', '.join(extraSections)} in preset")
+    missingSections = set(VALID_SECTIONS).difference(preset.keys())
+    if len(missingSections) != 0:
+        raise PresetError(f"Missing sections {', '.join(missingSections)} in preset")
+
+def getPlacementClass(name):
+    from kikit.panelize import (BasicGridPosition, OddEvenColumnPosition,
+        OddEvenRowsPosition, OddEvenRowsColumnsPosition)
+    mapping = {
+        "none": BasicGridPosition,
+        "rows": OddEvenRowsPosition,
+        "cols": OddEvenColumnPosition,
+        "rowsCols": OddEvenRowsColumnsPosition
+    }
+    try:
+        return mapping[name]
+    except KeyError:
+        raise RuntimeError(f"Invalid alternation option '{name}' passed. " +
+            "Valid options are: " + ", ".join(mapping.keys()))
+
+def readSourceArea(specification, board):
+    """
+    Extract source area based on preset and a board.
+    """
+    try:
+        type = specification["type"]
+        tolerance = specification["tolerance"]
+        if type == "auto":
+            return expandRect(findBoardBoundingBox(board), tolerance)
+        if type == "annotation":
+            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)
+        raise PresetError(f"Unknown type '{type}' of source specification.")
+    except KeyError as e:
+        raise PresetError(f"Missing parameter '{e}' in section 'source'")
+
+def obtainPreset(presetPaths, validate=True, **kwargs):
+    """
+    Given a preset paths from the user and the overrides in the form of named
+    arguments, construt the preset.
+
+    Ensures a valid preset is always found
+    """
+    presetChain = [":default"] + list(presetPaths)
+    preset = loadPresetChain(presetChain)
+    for name, section in kwargs.items():
+        if section is not None:
+            mergePresets(preset, {name: section})
+    if validate:
+        validateSections(preset)
+    postProcessPreset(preset)
+    return preset
+
+def buildLayout(layout, panel, sourceBoard, sourceArea):
+    """
+    Build layout for the boards - e.g., make a grid out of them.
+
+    Return the list of created substrates.
+    """
+    try:
+        type = layout["type"]
+        if type in ["grid"]:
+            placementClass = getPlacementClass(layout["alternation"])
+            return panel.makeGrid(
+                boardfile=sourceBoard, sourceArea=sourceArea,
+                rows=layout["rows"], cols=layout["cols"], destination=wxPointMM(50, 50),
+                rotation=layout["rotation"],
+                verSpace=layout["vspace"], horSpace=layout["hspace"],
+                placementClass=placementClass,
+                netRenamePattern=layout["renamenet"], refRenamePattern=layout["renameref"])
+        raise PresetError(f"Unknown type '{type}' of layout specification.")
+    except KeyError as e:
+        raise PresetError(f"Missing parameter '{e}' in section 'layout'")
+
+def buildTabs(properties, panel, substrates, boundarySubstrates):
+    """
+    Build tabs for the substrates in between the boards. Return a list of cuts.
+    """
+    try:
+        type = properties["type"]
+        if type == "none":
+            return []
+        if type == "fixed":
+            panel.clearTabsAnnotations()
+            panel.buildTabAnnotationsFixed(properties["hcount"],
+                properties["vcount"], properties["hwidth"], properties["vwidth"],
+                properties["mindistance"], boundarySubstrates)
+            return panel.buildTabsFromAnnotations()
+        if type == "spacing":
+            panel.clearTabsAnnotations()
+            panel.buildTabAnnotationsSpacing(properties["spacing"],
+                properties["hwidth"], properties["vwidth"], boundarySubstrates)
+            return panel.buildTabsFromAnnotations()
+        if type == "corner":
+            panel.clearTabsAnnotations()
+            panel.buildTabAnnotationsCorners(properties["width"])
+            return panel.buildTabsFromAnnotations()
+        if type == "full":
+            return panel.buildFullTabs()
+        if type == "annotation":
+            return panel.buildTabsFromAnnotations()
+        raise PresetError(f"Unknown type '{type}' of tabs specification.")
+    except KeyError as e:
+        raise PresetError(f"Missing parameter '{e}' in section 'tabs'")
+
+def buildBackBone(layout, panel, substrates, frameSpace):
+    """
+    Append backbones to the panel. Return backbone cuts.
+    """
+    try:
+        return panel.renderBackbone(layout["vbackbone"], layout["hbackbone"],
+                                    layout["vbonecut"], layout["hbonecut"])
+    except KeyError as e:
+        raise PresetError(f"Missing parameter '{e}' in section 'layout'")
+
+def frameOffset(framing):
+    try:
+        type = framing["type"]
+        if type == "none":
+            return None, None
+        if type == "railstb":
+            return framing["vspace"], None
+        if type == "railslr":
+            return None, framing["hspace"]
+        return framing["vspace"], framing["hspace"]
+    except KeyError as e:
+        raise PresetError(f"Missing parameter '{e}' in section 'framing'")
+
+def makeTabCuts(properties, panel, cuts):
+    """
+    Perform cuts on tab (does not ignore offset)
+    """
+    makeCuts(properties, panel, cuts, False)
+
+def makeOtherCuts(properties, panel, cuts):
+    """
+    Perform non-tab cuts (ignore offset)
+    """
+    makeCuts(properties, panel, cuts, True)
+
+def makeCuts(properties, panel, cuts, ignoreOffset):
+    """
+    Perform cuts
+    """
+    try:
+        type = properties["type"]
+        if type == "none":
+            return
+        if type == "vcuts":
+            panel.makeVCuts(cuts, properties["cutcurves"])
+            panel.setVCutLayer(properties["layer"])
+            panel.setVCutClearance(properties["clearance"])
+        elif type == "mousebites":
+            offset = 0 if ignoreOffset else properties["offset"]
+            panel.makeMouseBites(cuts, properties["drill"],
+                properties["spacing"], offset, properties["prolong"])
+        else:
+            raise PresetError(f"Unknown type '{type}' of cuts specification.")
+    except KeyError as e:
+        raise PresetError(f"Missing parameter '{e}' in section 'cuts'")
+
+
+def polygonToSubstrate(polygon):
+    s = Substrate([])
+    s.union(polygon)
+    return s
+
+def dummyFramingSubstrate(substrates, frameOffset):
+    """
+    Generate dummy substrates that pretend to be the frame (to appear)
+    """
+    vSpace, hSpace = frameOffset
+    dummy = []
+    minx, miny, maxx, maxy = substrates[0].bounds()
+    for s in substrates:
+        minx2, miny2, maxx2, maxy2 = s.bounds()
+        minx = min(minx, minx2)
+        miny = min(miny, miny2)
+        maxx = max(maxx, maxx2)
+        maxy = max(maxy, maxy2)
+    width = fromMm(0)
+    if vSpace is not None:
+        top = box(minx, miny - 2 * vSpace - width, maxx, miny - 2 * vSpace)
+        bottom = box(minx, maxy + 2 * vSpace, maxx, maxy + 2 * vSpace + width)
+        dummy.append(polygonToSubstrate(top))
+        dummy.append(polygonToSubstrate(bottom))
+    if hSpace is not None:
+        left = box(minx - 2 * hSpace - width, miny, minx - 2 * hSpace, maxy)
+        right = box(maxx + 2 * hSpace, miny, maxx + 2 * hSpace + width, maxy)
+        dummy.append(polygonToSubstrate(left))
+        dummy.append(polygonToSubstrate(right))
+    return dummy
+
+def buildFraming(preset, panel):
+    """
+    Build frame according to the preset and return cuts
+    """
+    try:
+        type = preset["type"]
+        if type == "none":
+            return []
+        if type == "railstb":
+            panel.makeRailsTb(preset["width"])
+            return []
+        if type == "railslr":
+            panel.makeRailsLr(preset["width"])
+            return []
+        if type == "frame":
+            cuts = panel.makeFrame(preset["width"])
+            return cuts if preset["cuts"] else []
+        if type == "tightframe":
+            panel.makeTightFrame(preset["width"], preset["slotwidth"])
+            panel.boardSubstrate.removeIslands()
+            return []
+        raise PresetError(f"Unknown type '{type}' of frame specification.")
+    except KeyError as e:
+        raise PresetError(f"Missing parameter '{e}' in section 'framing'")
+
+def buildTooling(preset, panel):
+    """
+    Build tooling holes according to the preset
+    """
+    try:
+        type = preset["type"]
+        if type == "none":
+            return
+        hoffset, voffset = preset["hoffset"], preset["voffset"]
+        diameter = preset["size"]
+        paste = preset["paste"]
+        if type == "3hole":
+            panel.addCornerTooling(3, hoffset, voffset, diameter, paste)
+            return
+        if type == "4hole":
+            panel.addCornerTooling(4, hoffset, voffset, diameter, paste)
+            return
+        raise PresetError(f"Unknown type '{type}' of tooling specification.")
+    except KeyError as e:
+        raise PresetError(f"Missing parameter '{e}' in section 'tooling'")
+
+def buildFiducials(preset, panel):
+    """
+    Build tooling holes according to the preset
+    """
+    try:
+        type = preset["type"]
+        if type == "none":
+            return
+        hoffset, voffset = preset["hoffset"], preset["voffset"]
+        coppersize, opening = preset["coppersize"], preset["opening"]
+        if type == "3fid":
+            panel.addCornerFiducials(3, hoffset, voffset, coppersize, opening)
+            return
+        if type == "4fid":
+            panel.addCornerFiducials(4, hoffset, voffset, coppersize, opening)
+            return
+        raise PresetError(f"Unknown type '{type}' of fiducial specification.")
+    except KeyError as e:
+        raise PresetError(f"Missing parameter '{e}' in section 'fiducials'")
+
+def buildText(preset, panel):
+    """
+    Build text according to the preset
+    """
+    try:
+        type = preset["type"]
+        if type == "none":
+            return
+        if type == "simple":
+            origin = resolveAnchor(preset["anchor"])(panel.boardSubstrate.boundingBox())
+            origin += wxPoint(preset["hoffset"], preset["voffset"])
+
+            panel.addText(
+                text=preset["text"],
+                position=origin,
+                orientation=preset["orientation"],
+                width=preset["width"],
+                height=preset["height"],
+                thickness=preset["thickness"],
+                hJustify=preset["hjustify"],
+                vJustify=preset["vjustify"],
+                layer=preset["layer"])
+            return
+        raise PresetError(f"Unknown type '{type}' of text specification.")
+    except KeyError as e:
+        raise PresetError(f"Missing parameter '{e}' in section 'text'")
+
+def buildPostprocessing(preset, panel):
+    """
+    Perform postprocessing operations
+    """
+    try:
+        type = preset["type"]
+        if type != "auto":
+            raise PresetError(f"Unknown type '{type}' of postprocessing specification.")
+        if preset["millradius"] > 0:
+            panel.addMillFillets(preset["millradius"])
+        if preset["copperfill"]:
+            panel.copperFillNonBoardAreas()
+        if preset["origin"]:
+            origin = resolveAnchor(preset["origin"])(panel.boardSubstrate.boundingBox())
+            panel.setAuxiliaryOrigin(origin)
+            panel.setGridOrigin(origin)
+    except KeyError as e:
+        raise PresetError(f"Missing parameter '{e}' in section 'postprocessing'")
+
+def runUserScript(preset, panel):
+    """
+    Run post processing script
+    """
+    if preset["script"] == "":
+        return
+    import importlib.util
+
+    spec = importlib.util.spec_from_file_location("kikit.user_script",
+        preset["script"])
+    userScriptModule = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(userScriptModule)
+    userScriptModule.kikitPostprocess(panel, preset["scriptarg"])
+
+
+def buildDebugAnnotation(preset, panel):
+    """
+    Add debug annotation to the panel
+    """
+    try:
+        if preset["drawPartitionLines"]:
+            panel.debugRenderPartitionLines()
+        if preset["drawBackboneLines"]:
+            panel.debugRenderBackboneLines()
+        if preset["drawboxes"]:
+            panel.debugRenderBoundingBoxes()
+    except KeyError as e:
+        raise PresetError(f"Missing parameter '{e}' in section 'debug'")
\ No newline at end of file
diff --git a/kikit/plugin/__init__.py b/kikit/plugin/__init__.py
index 221e7c2b..bda9ca10 100644
--- a/kikit/plugin/__init__.py
+++ b/kikit/plugin/__init__.py
@@ -1,8 +1,15 @@
 import textwrap
 import os
 import sys
+import platform
 from pathlib import Path
 import click
+from itertools import islice
+from copy import deepcopy
+from datetime import datetime
+import shutil
+from kikit.sexpr import Atom, SExpr, parseSexprF
+from kikit.common import KIKIT_LIB
 
 # Tuple: package, name, description
 availablePlugins = [
@@ -71,6 +78,100 @@ def enable(all, plugin):
     with open(location, "w") as f:
         f.write(registrationRoutine(plugins))
 
+def getFpLibTablePath():
+    """
+    Return path to the FP Lib table
+    """
+    # Currently, we only support Linux and stable KiKit
+    if platform.system() != "Linux":
+        raise RuntimeError(f"Usupported platform '{platform.system()}'")
+    return str(Path.home() / ".config" / "kicad" / "fp-lib-table")
+
+def findLib(fpLibTable, lib):
+    """
+    Given footprint library table, find a library entry
+    """
+    if len(fpLibTable) == 0 or fpLibTable[0] != "fp_lib_table":
+        raise RuntimeError("Invalid table")
+    for x in islice(fpLibTable, 1, None):
+        if x[0] != "lib":
+            continue
+        for y in islice(x, 1, None):
+            if not isinstance(y, SExpr) or len(y) != 2:
+                continue
+            if y[0] == "name" and y[1] == lib:
+                return x
+    return None
+
+def pushNewLib(fpLibTable):
+    """
+    Add new KiCAD library into the table. Try to respect the formatting. Return
+    the Sexpression for the newly inserted item.
+    """
+    if len(fpLibTable) == 0 or fpLibTable[0] != "fp_lib_table":
+        raise RuntimeError("Invalid table")
+    if len(fpLibTable) == 1:
+        # There are no libraries, we can choose formatting
+        s = SExpr([
+            Atom("lib"),
+            SExpr([Atom("name"), Atom("", " ")], " "),
+            SExpr([Atom("type"), Atom("", " ")]),
+            SExpr([Atom("uri"), Atom("", " ")]),
+            SExpr([Atom("options"), Atom("", " ")]),
+            SExpr([Atom("descr"), Atom("", " ")])
+        ], "\n    ")
+        fpLibTable.trailingWhitespace = "\n"
+        fpLibTable.items.append(s)
+        return s
+    # There are already libraries, copy last item and erase it:
+    s = deepcopy(fpLibTable[-1])
+    fpLibTable.items.append(s)
+    for x in islice(s, 1, None):
+        x[1].value = ""
+    return s
+
+@click.command()
+@click.option("--path", "-p",
+    type=click.Path(dir_okay=False, file_okay=True, exists=True),
+    default=None,
+    help="You can optionally specify custom path for the fp_lib_table file")
+def registerlib(path):
+    """
+    Add KiKit's footprint library into the global footprint library table. If
+    the library has already been registered, update the path.
+    """
+    if path is None:
+        path = getFpLibTablePath()
+    with open(path, "r") as f:
+        fpLibTable = parseSexprF(f)
+        rest = f.read()
+    kikitLib = findLib(fpLibTable, "kikit")
+    if kikitLib is None:
+        kikitLib = pushNewLib(fpLibTable)
+
+    rewriteTable = {
+        "name": "kikit",
+        "type": "KiCAD",
+        "uri": KIKIT_LIB,
+        "options": "",
+        "descr": "KiKit Footprint library"
+    }
+    for x in islice(kikitLib, 1, None):
+        x[1].value = rewriteTable[x[0].value]
+
+    ident = datetime.now().strftime("%Y-%m-%d--%H-%M:%S")
+    backupName = f"{path}.bak.{ident}"
+    shutil.copy(path, backupName)
+    print(f"A copy of the original {path} was made into {backupName}. ", end="")
+    print("You can restore it if something goes wrong.", end="\n\n")
+
+    with open(path, "w") as f:
+        f.write(str(fpLibTable))
+        f.write(rest)
+
+    print(f"KiKit footprint library successfully added to the global footprint table '{path}'. Please restart KiCAD.")
+
+
 @click.group()
 def cli():
     """
@@ -81,6 +182,7 @@ def cli():
 
 cli.add_command(enable)
 cli.add_command(list)
+cli.add_command(registerlib)
 
 if __name__ == "__main__":
     cli()
\ No newline at end of file
diff --git a/kikit/resources/kikit.pretty/Board.kicad_mod b/kikit/resources/kikit.pretty/Board.kicad_mod
new file mode 100644
index 00000000..12c4f5a6
--- /dev/null
+++ b/kikit/resources/kikit.pretty/Board.kicad_mod
@@ -0,0 +1,14 @@
+(module Board (layer F.Cu) (tedit 605A21C1)
+  (descr "Mark board for extraction")
+  (attr virtual)
+  (fp_text reference REF** (at -4.5 -5) (layer Dwgs.User)
+    (effects (font (size 1 1) (thickness 0.15)) (justify left))
+  )
+  (fp_text value Board (at -7.25 -5) (layer Dwgs.User)
+    (effects (font (size 1 1) (thickness 0.15)))
+  )
+  (fp_line (start 0 0) (end -1.25 -4) (layer Dwgs.User) (width 0.2))
+  (fp_line (start -1.25 -4) (end -9.25 -4) (layer Dwgs.User) (width 0.2))
+  (fp_line (start 0 0) (end 0.5 -1) (layer Dwgs.User) (width 0.2))
+  (fp_line (start 0 0) (end -1 -0.5) (layer Dwgs.User) (width 0.2))
+)
diff --git a/kikit/resources/kikit.pretty/Tab.kicad_mod b/kikit/resources/kikit.pretty/Tab.kicad_mod
new file mode 100644
index 00000000..c89a44e0
--- /dev/null
+++ b/kikit/resources/kikit.pretty/Tab.kicad_mod
@@ -0,0 +1,17 @@
+(module Tab (layer F.Cu) (tedit 60708B1A)
+  (descr "A symbol representing annotation for tab placement")
+  (attr virtual)
+  (fp_text reference REF** (at 0 -2) (layer F.SilkS) hide
+    (effects (font (size 1 1) (thickness 0.15)))
+  )
+  (fp_text value Tab (at -2.75 -1) (layer Dwgs.User)
+    (effects (font (size 1.2 1.2) (thickness 0.2)))
+  )
+  (fp_line (start 0.25 0) (end -0.75 1) (layer Dwgs.User) (width 0.3))
+  (fp_line (start 0.25 0) (end -0.75 -1) (layer Dwgs.User) (width 0.3))
+  (fp_line (start 0.25 0) (end -2.75 0) (layer Dwgs.User) (width 0.3))
+  (fp_line (start 0.25 1) (end 0.25 -1) (layer Dwgs.User) (width 0.3))
+  (fp_text user "KIKIT: width: 3mm" (at -5.5 0 90) (layer Dwgs.User)
+    (effects (font (size 1 1) (thickness 0.15)))
+  )
+)
diff --git a/kikit/resources/panelizePresets/default.json b/kikit/resources/panelizePresets/default.json
new file mode 100644
index 00000000..21a0a595
--- /dev/null
+++ b/kikit/resources/panelizePresets/default.json
@@ -0,0 +1,92 @@
+{
+    "layout": {
+        "type": "grid",
+        "rows": 1,
+        "cols": 1,
+        "hspace": "0mm",
+        "vspace": "0mm",
+        "rotation": "0deg",
+        "alternation": "none",
+        "renamenet": "Board_{n}-{orig}",
+        "renameref": "{orig}",
+        "hbackbone": "0mm",
+        "vbackbone": "0mm",
+        "vbonecut": true,
+        "hbonecut": true
+    },
+    "source": {
+        "type": "auto",
+        "tolerance": "1mm",
+        "tlx": "0mm",
+        "tly": "0mm",
+        "brx": "0mm",
+        "brx": "0mm",
+        "ref": ""
+    },
+    "tabs": {
+        "type": "spacing",
+        "hcount": 1,
+        "vcount": 1,
+        "hwidth": "3mm",
+        "vwidth": "3mm",
+        "mindistance": "0mm",
+        "spacing": "10mm"
+    },
+    "cuts": {
+        "type": "none",
+        "clearance": "0mm",
+        "cutcurves": false,
+        "layer": "Cmts.User",
+        "offset": "0mm",
+        "prolong": "0mm"
+    },
+    "framing": {
+        "type": "none",
+        "thickness": "0mm",
+        "millRadius": "0mm",
+        "cuts": true,
+        "copperFill": false,
+        "slotwidth": "2mm"
+    },
+    "tooling": {
+        "type": "none",
+        "layout": "3hole",
+        "paste": false
+    },
+    "fiducials": {
+        "type": "none",
+        "hoffset": "0mm",
+        "voffset": "0mm",
+        "coppersize": "1mm",
+        "opening": "1mm"
+    },
+    "text": {
+        "type": "none",
+        "text": "",
+        "anchor": "mt",
+        "hoffset": "0mm",
+        "voffset": "0mm",
+        "orientation": "0deg",
+        "width": "1.5mm",
+        "height": "1.5mm",
+        "hjustify": "center",
+        "vjustify": "center",
+        "thickness": "0.3mm",
+        "layer": "F_SilkS"
+    },
+    "post": {
+        "type": "auto",
+        "copperfill": false,
+        "millradius": "0mm",
+        "script": "",
+        "scriptarg": "",
+        "origin": ""
+    },
+    "debug": {
+        "type": "none",
+        "drawPartitionLines": false,
+        "drawBackboneLines": false,
+        "drawboxes": false,
+        "trace": false
+    }
+}
\ No newline at end of file
diff --git a/kikit/resources/panelizePresets/jlcTooling.json b/kikit/resources/panelizePresets/jlcTooling.json
new file mode 100644
index 00000000..86596e78
--- /dev/null
+++ b/kikit/resources/panelizePresets/jlcTooling.json
@@ -0,0 +1,7 @@
+{
+    "tooling": {
+        "style": "3hole",
+        "size": "1.152mm",
+        "paste": false
+    }
+}
\ No newline at end of file
diff --git a/kikit/sexpr.py b/kikit/sexpr.py
new file mode 100644
index 00000000..9edee09d
--- /dev/null
+++ b/kikit/sexpr.py
@@ -0,0 +1,155 @@
+import itertools
+from io import StringIO
+
+# Simple white-space aware S-Expression parser (parsing and dumping yields the
+# same result). Might not support all features of S-expression, but should be
+# enough to properly parser KiCAD's S-Expressions
+
+def ParseError(RuntimeError):
+    pass
+
+class Atom:
+    def __init__(self, value, leadingWhitespace=""):
+        self.value = value
+        self.leadingWhitespace = leadingWhitespace
+
+    def __str__(self):
+        if any([x.isspace() for x in self.value]) or len(self.value) == 0:
+            return self.leadingWhitespace + '"' + self.value + '"'
+        return self.leadingWhitespace + self.value
+
+    def __repr__(self):
+        return f"Atom({self.value}, '{self.leadingWhitespace}')"
+
+    def __eq__(self, o):
+        if isinstance(o, str):
+            return self.value == o
+        return self.value == o.value and self.leadingWhitespace == o.leadingWhitespace
+
+class SExpr:
+    def __init__(self, items=None, leadingWhitespace="", trailingWhitespace=""):
+        if items is None:
+            self.items = []
+        else:
+            self.items = items
+        self.leadingWhitespace = leadingWhitespace
+        self.trailingWhitespace = trailingWhitespace
+
+    def __str__(self):
+        # TBA: we should validate that two atoms do not get squished together
+        # as they have wrongly specified whitespace
+        return (self.leadingWhitespace + "("
+            + "".join([str(x) for x in self.items])
+            + self.trailingWhitespace + ")")
+
+    def __repr__(self):
+        val = [x.__repr__() for x in self.items]
+        return f"Expr([{', '.join(val)}], '{self.leadingWhitespace}', '{self.trailingWhitespace}')"
+
+    def __eq__(self, o):
+        return (self.items == o.items and
+                self.leadingWhitespace == o.leadingWhitespace and
+                self.trailingWhitespace == o.trailingWhitespace)
+
+    def __iter__(self):
+        return self.items.__iter__()
+
+    def __getitem__(self, key):
+        return self.items.__getitem__(key)
+
+    def __len__(self):
+        return self.items.__len__()
+
+# Python 3 does not support peeking nor relative seeking on text files so we
+# implement goBack, shift and peek via seeking. It does not performs the best,
+# but it yields simple code.
+def goBack(file, n=1):
+    file.seek(file.tell() - n)
+
+def shift(stream, what=None):
+    c = stream.read(1)
+    if len(c) == 0:
+        raise ParseError("Unexpected file end")
+    if what is not None and c != what:
+        raise ParseError(f"Expected '{what}', got '{c}'")
+    return c
+
+def peek(stream):
+    c = shift(stream)
+    goBack(stream)
+    return c
+
+def atomEnd(c):
+    return c.isspace() or c in set("()")
+
+def readQuotedString(stream):
+    shift(stream, '"')
+    s = []
+    escaped = False
+    c = peek(stream)
+    while c != '"' or escaped:
+        if c == "\\":
+            escaped = True
+        else:
+            escaped = False
+            s.append(shift(stream))
+        c = peek(stream)
+    shift(stream, '"')
+    return "".join(s)
+
+def readString(stream):
+    s = []
+    c = peek(stream)
+    while not atomEnd(c):
+        s.append(shift(stream))
+        c = peek(stream)
+    return "".join(s)
+
+def readAtom(stream):
+    c = peek(stream)
+    if c == '"':
+        value = readQuotedString(stream)
+    else:
+        value = readString(stream)
+    return Atom(value)
+
+def readWhitespace(stream):
+    w = []
+    c = peek(stream)
+    while c.isspace():
+        w.append(shift(stream))
+        c = peek(stream)
+    return "".join(w)
+
+def readSexpr(stream):
+    shift(stream, "(")
+
+    expr = SExpr()
+    c = peek(stream)
+    whitespace = ""
+    while c != ")":
+        if c.isspace():
+            whitespace = readWhitespace(stream)
+        elif c == "(":
+            s = readSexpr(stream)
+            s.leadingWhitespace = whitespace
+            expr.items.append(s)
+            whitespace = ""
+        else:
+            a = readAtom(stream)
+            a.leadingWhitespace = whitespace
+            expr.items.append(a)
+            whitespace = ""
+        c = peek(stream)
+    shift(stream, ")")
+    expr.trailingWhitespace = whitespace
+    return expr
+
+def parseSexprF(stream):
+    lw = readWhitespace(stream)
+    s = readSexpr(stream)
+    s.leadingWhitespace = lw
+    return s
+
+def parseSexprS(s):
+    return parseSexprF(StringIO(s))
diff --git a/kikit/stencil.py b/kikit/stencil.py
index 45de4581..3198bae9 100644
--- a/kikit/stencil.py
+++ b/kikit/stencil.py
@@ -183,7 +183,7 @@ def createOuterPolygon(board, jigFrameSize, outerBorder):
     outerSubstrate.substrates = outerSubstrate.substrates.buffer(outerBorder)
     tabs = []
     for hole in holes:
-        tab, _ = outerSubstrate.tab(hole, centerpoint - hole, INNER_BORDER, fromMm(1000))
+        tab, _ = outerSubstrate.tab(hole, centerpoint - hole, INNER_BORDER, maxHeight=fromMm(1000))
         tabs.append(tab)
     outerSubstrate.union(tabs)
     outerSubstrate.union([Point(x).buffer(INNER_BORDER / 2) for x in holes])
diff --git a/kikit/substrate.py b/kikit/substrate.py
index 16cde938..bbde55ba 100644
--- a/kikit/substrate.py
+++ b/kikit/substrate.py
@@ -1,7 +1,9 @@
-from shapely.geometry import Polygon, MultiPolygon, LineString, MultiLineString, Point
-from shapely.ops import unary_union, split
+from shapely.geometry import (Polygon, MultiPolygon, LineString,
+    MultiLineString, LinearRing, Point)
+from shapely.ops import unary_union, split, nearest_points
 import shapely
 import numpy as np
+from kikit.intervals import Interval, BoxNeighbors, BoxPartitionLines
 from kikit.pcbnew_compatibility import pcbnew
 from enum import IntEnum
 from itertools import product
@@ -16,10 +18,13 @@ def __init__(self, message, point):
         self.point = point
         self.origMessage = message
 
+class NoIntersectionError(RuntimeError):
+    pass
+
 def toTuple(item):
     if isinstance(item, pcbnew.wxPoint):
         return item[0], item[1]
-    raise NotImplementedError("toTuple for {} not implemented".format(type(item)))
+    raise NotImplementedError(f"toTuple for {type(item)} not implemented")
 
 def roundPoint(point, precision=-2):
     return pcbnew.wxPoint(round(point[0], precision), round(point[1], precision))
@@ -95,7 +100,7 @@ def extractRings(geometryList):
             raise PositionError("Discontinuous outline at [{}, {}]", point)
         if l == 2:
             continue
-        raise PositionError("Multiple outlines ({}) at [{}, {}]".format(l), point)
+        raise PositionError("Multiple outlines ({}) at [{{}}, {{}}]".format(l), point)
 
     rings = []
     unused = [True] * len(geometryList)
@@ -253,71 +258,55 @@ def commonCircle(a, b, c):
     r = np.sqrt(x*x + y*y + np.linalg.det(m14) / m11d)
     return (x, y, r)
 
-def splitLine(linestring, point, tolerance=pcbnew.FromMM(0.01)):
-    splitted = split(linestring, point.buffer(tolerance, resolution=1))
-    if len(splitted) != 3:
-        print(splitted)
-        raise RuntimeError("Expected 3 segments in line spitting")
-    p1 = LineString(list(splitted[0].coords) + [point])
-    p2 = LineString([point] + list(splitted[2].coords))
-    return shapely.geometry.collection.GeometryCollection([p1, p2])
-
-def cutOutline(point, linestring, segmentLimit=None, tolerance=pcbnew.FromMM(0.001)):
+def liesOnSegment(start, end, point, tolerance=fromMm(0.01)):
     """
-    Given a point finds an entity in (multi)linestring which goes through the
-    point.
-
-    It is possible to restrict the number of segments used by specifying
-    segmentLimit.
-
-    When segment limit is passed, return a tuple of the string starting and
-    ending in that point. When no limit is passed returns a single string.
+    Decide if a point lies on a given segment within tolerance
     """
-    if not isinstance(point, Point):
-        point = Point(point[0], point[1])
-    if isinstance(linestring, LineString):
-        geom = [linestring]
-    elif isinstance(linestring, MultiLineString):
-        geom = linestring.geoms
-    else:
-        raise RuntimeError("Unknown geometry '{}' passed".format(type(linestring)))
-    for string in geom:
-        bufferedPoint = point.buffer(tolerance, resolution=1)
-        splitted = split(string, bufferedPoint)
-        if len(splitted) == 1 and not Point(splitted[0].coords[0]).intersects(bufferedPoint):
-            continue
-        if len(splitted) == 3:
-            string = LineString([point] + list(splitted[2].coords) + splitted[0].coords[1:])
-        elif len(splitted) == 2:
-            string = LineString(list(splitted[1].coords) + splitted[0].coords[1:])
-        else:
-            string = splitted[0]
-        if segmentLimit is None:
-            return string
-        limit1 = max(1, len(string.coords) - segmentLimit)
-        limit2 = min(segmentLimit, len(string.coords) - 1)
-        return LineString(string.coords[limit1:]), LineString(string.coords[:limit2])
-    return None, None
+    segment = LineString([start, end])
+    point = Point(point)
+    candidatePoint, _ = nearest_points(segment, point)
+    return candidatePoint.distance(point) < tolerance
 
-def extractPoint(collection):
+def biteBoundary(boundary, pointA, pointB, tolerance=fromMm(0.01)):
     """
-    Given a geometry collection, return first point if it. None if no point in
-    the collection
+    Given an oriented and possibly cyclic boundary in a form of shapely
+    linestring, return a part of the boundary between pointA and pointB. The
+    orientation matters - the segment is oriented from A to B.
+
+    If no intersection is found, None is returned.
     """
-    if isinstance(collection, LineString):
-        return None
-    if isinstance(collection, Point):
-        return collection
-    for x in collection:
-        if isinstance(x, Point):
-            return x
+    if isLinestringCyclic(boundary):
+        c = boundary.coords
+        if c[0] == c[-1]:
+            boundaryCoords = chain(c, islice(c, 1, None))
+        else:
+            boundaryCoords = chain(c, c)
+    else:
+        boundaryCoords = boundary.coords
+    boundaryCoords = list(boundaryCoords)
+    faceCoords = []
+    targetPoint = pointA
+    inCut = False
+    for a, b in zip(boundaryCoords, islice(boundaryCoords, 1, None)):
+        if liesOnSegment(a, b, targetPoint, tolerance):
+            faceCoords.append(targetPoint)
+            if inCut:
+                return LineString(faceCoords)
+            inCut = True
+            targetPoint = pointB
+            # The pointB might lie on the same segment
+            if liesOnSegment(a, b, targetPoint, tolerance):
+                return LineString([pointA, pointB])
+        if inCut:
+            faceCoords.append(b)
     return None
 
+
 def closestIntersectionPoint(origin, direction, outline, maxDistance):
     testLine = LineString([origin, origin + direction * maxDistance])
     inter = testLine.intersection(outline)
     if inter.is_empty:
-        raise RuntimeError("No intersection found within given distance")
+        raise NoIntersectionError("No intersection found within given distance")
     origin = Point(origin[0], origin[1])
     if isinstance(inter, Point):
         geoms = [inter]
@@ -346,8 +335,13 @@ def __init__(self, geometryList, bufferDistance=0):
         self.substrates = self.substrates.buffer(bufferDistance)
         if not self.substrates.is_empty:
             self.substrates = shapely.ops.orient(self.substrates)
+        self.partitionLine = shapely.geometry.GeometryCollection()
+        self.annotations = []
 
     def bounds(self):
+        """
+        Return shapely bounds of substrates
+        """
         return self.substrates.bounds
 
     def union(self, other):
@@ -397,6 +391,9 @@ def _serializeRing(self, ring):
         return segments
 
     def boundingBox(self):
+        """
+        Return bounding box as wxRect
+        """
         minx, miny, maxx, maxy = self.substrates.bounds
         return pcbnew.wxRect(int(minx), int(miny), int(maxx - minx), int(maxy - miny))
 
@@ -414,45 +411,26 @@ def exterior(self):
         return unary_union(polygons)
 
     def boundary(self):
-        return self.substrates.boundary
-
-    def fillet(self, point, radius):
         """
-        Add a fillet to the substrate at given point. If the point does not lie
-        on an inner corner or the surrounding geometry does not allow for fillet,
-        does nothing. Return true if the fillet was created, else otherwise
+        Return shapely geometry representing the outer ring
         """
-        if radius == 0:
-            return
-        LEN_LIMIT = 40 # Should be roughly equal to half the circular segments,
-                    # may prevent from finding a solution
-        cut1, cut2 = cutOutline(point, self.substrates.boundary, LEN_LIMIT,
-            tolerance=pcbnew.FromMM(0.02))
-        if not cut1 or not cut2:
-            # The point does not intersect any outline
-            return False
-        offset1 = cut1.parallel_offset(radius, 'right', join_style=2)
-        offset2 = cut2.parallel_offset(radius, 'right', join_style=2)
-        filletCenter = extractPoint(offset1.intersection(offset2))
-        if not filletCenter:
-            return False
-        a, _ = shapely.ops.nearest_points(cut1, filletCenter)
-        b, _ = shapely.ops.nearest_points(cut2, filletCenter)
-        if not a or not b:
-            return False
-        patch = Polygon([a, b, point]).buffer(pcbnew.FromMM(0.001)).difference(filletCenter.buffer(radius))
-        self.union(patch)
-        return True
-
-    def tab(self, origin, direction, width, maxHeight=pcbnew.FromMM(50)):
+        return self.substrates.boundary
+
+    def tab(self, origin, direction, width, partitionLine=None,
+               maxHeight=pcbnew.FromMM(50)):
         """
         Create a tab for the substrate. The tab starts at the specified origin
         (2D point) and tries to penetrate existing substrate in direction (a 2D
         vector). The tab is constructed with given width. If the substrate is
         not penetrated within maxHeight, exception is raised.
 
-        Returns a pair tab and cut outline. Add the tab it via
-        uion - batch adding of geometry is more efficient.
+        When partitionLine is specified, tha tab is extended to the opposite
+        side - limited by the partition line. Note that if tab cannot span
+        towards the partition line, then the the tab is not created - it returns
+        a tuple (None, None).
+
+        Returns a pair tab and cut outline. Add the tab it via union - batch
+        adding of geometry is more efficient.
         """
         origin = np.array(origin)
         direction = normalize(direction)
@@ -463,20 +441,47 @@ def tab(self, origin, direction, width, maxHeight=pcbnew.FromMM(50)):
             boundary, maxHeight)
         splitPointB = closestIntersectionPoint(sideOriginB, direction,
             boundary, maxHeight)
-        shiftedOutline = cutOutline(splitPointB, boundary)
-        tabFace = splitLine(shiftedOutline, splitPointA)[0]
-        tab = Polygon(list(tabFace.coords) + [sideOriginA, sideOriginB])
-        return tab, tabFace
+        tabFace = biteBoundary(boundary, splitPointB, splitPointA)
+        if partitionLine is None:
+            # There is nothing else to do, return the tab
+            tab = Polygon(list(tabFace.coords) + [sideOriginA, sideOriginB])
+            return tab, tabFace
+        # Span the tab towwards the partition line
+        # There might be multiple geometries in the partition line, so try them
+        # individually.
+        direction = -direction
+        for p in listGeometries(partitionLine):
+            try:
+                partitionSplitPointA = closestIntersectionPoint(splitPointA.coords[0],
+                        direction, p, maxHeight)
+                partitionSplitPointB = closestIntersectionPoint(splitPointB.coords[0],
+                    direction, p, maxHeight)
+            except NoIntersectionError: # We cannot span towards the partition line
+                continue
+            if isLinestringCyclic(p):
+                candidates = [(partitionSplitPointA, partitionSplitPointB)]
+            else:
+                candidates = [(partitionSplitPointA, partitionSplitPointB),
+                    (partitionSplitPointB, partitionSplitPointA)]
+            for i, (spa, spb) in enumerate(candidates):
+                partitionFace = biteBoundary(p, spa, spb)
+                if partitionFace is None:
+                    continue
+                partitionFaceCoord = list(partitionFace.coords)
+                if i == 1:
+                    partitionFaceCoord = partitionFaceCoord[::-1]
+                tab = Polygon(list(tabFace.coords) + partitionFaceCoord)
+                return tab, tabFace
+        return None, None
 
     def millFillets(self, millRadius):
         """
         Add fillets to inner conernes which will be produced a by mill with
         given radius.
         """
-        epsilon = fromMm(0.01)
-        if millRadius < epsilon:
+        if millRadius < SHP_EPSILON:
             return
-        self.substrates = self.substrates.buffer(millRadius - epsilon).buffer(-millRadius).buffer(epsilon)
+        self.substrates = self.substrates.buffer(millRadius - SHP_EPSILON).buffer(-millRadius).buffer(SHP_EPSILON)
 
     def removeIslands(self):
         """
@@ -526,3 +531,68 @@ def showPolygons(polygons):
             x2, y2 = inter.xy
             plt.fill(x2, y2, color="w")
     plt.show()
+
+class SubstrateNeighbors:
+    """
+    Thin wrapper around BoxNeighbors for finding substrate pieces' neighbors.
+    """
+    def __init__(self, substrates):
+        self._revMap = { id(s): s for s in substrates }
+        self._neighbors = BoxNeighbors( { id(s): s.bounds() for s in substrates })
+
+    def _reverse(self, queryRes):
+        return [self._revMap[ x ] for x in queryRes]
+
+    def _reverseC(self, queryRes):
+        return [(self._revMap[ x ], shadow) for x, shadow in queryRes]
+
+    def left(self, s):
+        return self._reverse(self._neighbors.left(id(s)))
+
+    def leftC(self, s):
+        return self._reverseC(self._neighbors.leftC(id(s)))
+
+    def right(self, s):
+        return self._reverse(self._neighbors.right(id(s)))
+
+    def rightC(self, s):
+        return self._reverseC(self._neighbors.rightC(id(s)))
+
+    def bottom(self, s):
+        return self._reverse(self._neighbors.bottom(id(s)))
+
+    def bottomC(self, s):
+        return self._reverseC(self._neighbors.bottomC(id(s)))
+
+    def top(self, s):
+        return self._reverse(self._neighbors.top(id(s)))
+
+    def topC(self, s):
+        return self._reverseC(self._neighbors.topC(id(s)))
+
+class SubstratePartitionLines:
+    """
+    Thin wrapper around BoxPartitionLines for finding substrate pieces'
+    partition lines. It allows you to specify ghost substrates. No partition
+    line is formed between two ghost substrates.
+    """
+    def __init__(self, substrates, ghostSubstrates=[],
+                 safeHorizontalMargin=0, safeVerticalMargin=0):
+        boxes = {id(s): s.bounds() for s in chain(substrates, ghostSubstrates)}
+        ghosts = set([id(s) for s in ghostSubstrates])
+        def seedFilter(idA, idB, v, l):
+            if l.length < SHP_EPSILON:
+                return False
+            return idA not in ghosts or idB not in ghosts
+        self._partition = BoxPartitionLines(
+            boxes,
+            seedFilter,
+            safeHorizontalMargin, safeVerticalMargin)
+
+    @property
+    def query(self):
+        return self._partition.query
+
+    def partitionSubstrate(self, substrate):
+        return self._partition.partitionLines(id(substrate))
+
diff --git a/kikit/ui.py b/kikit/ui.py
index 97f07b84..0d8b62e1 100644
--- a/kikit/ui.py
+++ b/kikit/ui.py
@@ -11,6 +11,7 @@ def cli():
 
 cli.add_command(export_ui.export)
 cli.add_command(panelize_ui.panelize)
+cli.add_command(panelize_ui.separate)
 cli.add_command(present_ui.present)
 cli.add_command(modify_ui.modify)
 cli.add_command(stencil_ui.stencil)
diff --git a/kikit/units.py b/kikit/units.py
new file mode 100644
index 00000000..5a964c9a
--- /dev/null
+++ b/kikit/units.py
@@ -0,0 +1,48 @@
+import re
+import math
+
+class UnitError(RuntimeError):
+    pass
+
+# Define unit conversion constants
+mm = 1000000
+cm = 10 * mm
+dm = 100 * mm
+m  = 1000 * mm
+mil = 25400
+inch = 1000 * mil
+
+deg = 10
+rad = 180 / math.pi * deg
+
+UNIT_SPLIT = re.compile(r"\s*(-?\s*\d+(\.\d*)?)\s*(\w+)$")
+
+def readUnit(unitDir, unitStr):
+    match = UNIT_SPLIT.match(unitStr)
+    if not match:
+        raise UnitError(f"Cannot read quantity '{unitStr}'")
+    try:
+        amount = float(match.group(1))
+        return amount * unitDir[match.group(3)]
+    except KeyError:
+        raise UnitError(f"Unknown unit in '{unitStr}'")
+
+def readLength(unitStr):
+    unitDir = {
+        "mm": mm,
+        "cm": cm,
+        "dm": dm,
+        "m": m,
+        "mil": mil,
+        "inch": inch,
+        "in": inch
+    }
+    return int(readUnit(unitDir, unitStr))
+
+def readAngle(unitStr):
+    unitDir = {
+        "deg": deg,
+        "°": deg,
+        "rad": rad
+    }
+    return readUnit(unitDir, unitStr)
diff --git a/scripts/exampleDoc.py b/scripts/exampleDoc.py
index 25c0a89a..4a3259a3 100644
--- a/scripts/exampleDoc.py
+++ b/scripts/exampleDoc.py
@@ -4,52 +4,72 @@
 Generate documentation in markdown format for examples
 """
 
-from kikit.doc import runBoardExample
-
+import sys
+from kikit.doc import runBoardExample, runBoardExampleJoin
 from kikit.pcbnew_compatibility import pcbnew
 
+counter = 0
+
+def autoName():
+    global counter
+    counter += 1
+    return f"examplePanel{counter}"
+
+SRC = "doc/resources/conn.kicad_pcb"
+
 print(
 """
 # Examples
 
-This document will show you several examples of KiKit CLI for panelization. We
-will show everything on a single board located in
-`doc/resources/conn.kicad_pcb`. The board looks like this:
-
-![conn](resources/conn.png)
+This document will show you several examples of KiKit CLI for panelization. Note
+that this is **not an exhaustive description** of everything that KiKit can do,
+nor proper documentation. For further details, please refer to:
 
-# Extract Board
+- [installation guide](installation.md)
+- [description of all panelization options](panelizeCli.md)
+- [more detail about KiKit's algorithm for tab creation](understandingTabs.md)
+- [reference for the Python interface](panelization.md)
 
-The simples command is `extractboard`. By calling:
+We will show everything on a single board located in
+`doc/resources/conn.kicad_pcb`. The board looks like this when rendered via
+PcbDraw:
 
-```
-kikit panelize extractboard --sourcearea 100 50 100 100 doc/resources/conn.kicad_pcb output.kicad_pcb
-```
-
-We extract our board into a new file. This command is usefull when you designed
-multiple boards in a single file (e.g., to have shared schematic for board
-sandwiches). The `sourcearea` is given as a rectangle. You should specify X, Y
-coordinates of upper left corner width and height in millimeters. Also note,
-that only board items which fully fit inside this rectangle are extracted.
+![conn](resources/conn.png)
 """)
 
 print(
 """
-# Panelize
+# Basic panels & layout
 
 Let's start with our first panel.
 """)
 
-runBoardExample("panel1",
-    ["panelize", "grid", "--gridsize", "2", "2", "--vcuts", "doc/resources/conn.kicad_pcb"])
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2;"],
+        ["--tabs", "full"],
+        ["--cuts", "vcuts"],
+        [SRC]])
 
 print(
 """
 We specified that we want 2x2 panel, no space between board and separate them by
-V-cuts. Note, that due to the rounded corners, this panel cannot be
-manufactured. We will fix it later.
-
-One side note - if you try it with your own board some components might be gone.
+V-cuts. We also specified that we want to build full tabs (although no tabs are
+visible in this example). This is ,however, essential – if we omitted tabs, no
+cuts between the boards would be performed. Note, that due to the rounded
+corners, this panel cannot be manufactured. We will fix it later.
+
+Note that the `\` in the input is there to make shell happy, so we can break our
+command into multiple lines. Also note that there are single quotes around the
+key-value pair – again, to make shell happy and to interpret a string with
+spaces as a single option.
+
+Also note that KiKit accepts all options in categories (e.g., `layout`, `tabs`,
+`cuts`, ...). You can specify the parameters as a semicolon-separated key-value
+list. To learn about the precise syntax of the CLI and about all options, please
+refer to – [documentation](panelizeCli.md).
+
+One side note – if you try it with your own board some components might be gone.
 KiKit respects the KiCAD component selection criteria. When you specify an input
 rectangle, only the components that **fully fit** inside the input rectangle are
 selected. This however take in account **both name and value labels** (even when
@@ -66,140 +86,416 @@
 
 How to include the missing components?
 - specify the source area explicitly to include all your components
-- specify `--tolerance 10` to enlarge the board outline bounding box by e.g. 10
-  mm. The default value is 5 mm.
+- specify `--source 'tolerance: 10mm'` to enlarge the board outline bounding box
+  by e.g. 10 mm. The default value is 5 mm.
+
+I told you that the panel above is not suitable for manufacturing. Let's see why:
+""")
+
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2;"],
+        ["--tabs", "full"],
+        ["--cuts", "vcuts"],
+        ["--post", "millradius: 1mm"],
+        [SRC]])
+
+print("""
+We specified a milling simulation post-processing. This simulates the milling
+operation in the fab house. As you can see, the sharp internal corners cannot be
+manufactured. I recommend you to use milling postprocessing always – you can
+easily see if your cuts are off or you have too narrow slots in your design.
+
+Usually, one would use full tabs only for rectangular boards. Usually, when you
+have rounded corners, you will use short tabs instead and add some space between
+the boards. So let's fix it:
+""")
 
-Now back to our example. Let's see how the same panel will look like with mouse
-bites instead:
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2; space: 2mm"],
+        ["--tabs", "fixed; hwidth: 10mm; vwidth: 15mm"],
+        ["--cuts", "vcuts"],
+        ["--post", "millradius: 1mm"],
+        [SRC]])
+
+print("""
+In that way, the rounded corners can be machined. Lets' see the same example
+with mousebites instead:
 """)
 
-runBoardExample("panel2",
-    ["panelize", "grid", "--gridsize", "2", "2", "--mousebites", "0.5", "1", "0",  "doc/resources/conn.kicad_pcb"])
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2; space: 2mm"],
+        ["--tabs", "fixed; width: 5mm"],
+        ["--cuts", "mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm"],
+        [SRC]])
 
 print(
 """
-You specify mouse bites by three numbers - hole diameter, hole spacing and
-offset. All in millimeters. We use offset 0, because we have no tabs. Otherwise
-the recommended value is 0.25 mm.
+We changed cut styles to mousebites and we specified that they should be
+performed by 0.5mm holes with a spacing of 1 mm. You could also use inches if
+you want – just specify `<number>in. Since we use mousebites, we used narrower
+tabs. We also specified that the cuts should be inset 0.25 mm into the board
+outline. This is suitable when your board should fit into a cover – when you
+break away the tabs, all burs will be inside the intended board outline.
 
-The approach shown above is good for boards without rounded corners. If send
-panel above for fabrication we would obtain something like this:
+
+What happens, when we simulate the milling operation?
 """)
 
-runBoardExample("panel3",
-    ["panelize", "grid", "--gridsize", "2", "2", "--mousebites", "0.5", "1", "0", "--radius", "1", "doc/resources/conn.kicad_pcb"])
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2; space: 2mm"],
+        ["--tabs", "fixed; width: 5mm"],
+        ["--cuts", "mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm"],
+        ["--post", "millradius: 1mm"],
+        [SRC]])
 
 print(
 """
-The `--radius` argument simulates the milling operation. You specify mill radius
-(usuall the smallest diameter is 2 mm). We recommend to use the radius argument.
-See the distorted corners in picture above? Let's fix it by adding tabs.
+See? The cuts are somewhat short. This is due to the internal corners that
+cannot be milled. KiKit can fix that for you – just specify you want to prolong
+your cuts tangentially by a small amount:
 """)
 
-runBoardExample("panel4",
-    ["panelize", "grid", "--space", "3", "--gridsize", "2", "2", "--tabwidth", "18", "--tabheight", "10", "--vcuts", "--radius", "1", "doc/resources/conn.kicad_pcb"])
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2; space: 2mm"],
+        ["--tabs", "fixed; width: 3mm"],
+        ["--cuts", "mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm"],
+        ["--post", "millradius: 1mm"],
+        [SRC]])
+
+print("""
+If you want, you can also specify a number of tabs to generate. KiKit will place
+them evenly:
+""")
 
-print(
-"""
-We introduced tabs - extra space between the board. We also specified the tab
-width and height, so there is clearance for milling the corners.
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2; space: 2mm"],
+        ["--tabs", "fixed; width: 3mm; vcount: 2"],
+        ["--cuts", "mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm"],
+        ["--post", "millradius: 1mm"],
+        [SRC]])
+
+print("""
+You can also append frame or rails to the panel. Frames and rail are useful in
+the following situations:
+
+- you want to assemble your board, so you need tooling holes, fiducial.
+- you want to append a text to board (e.g., to identify a manufacturing batch)
+- your boards are not rectangluar and you want to use V-Cuts (most manufactures
+  require the outer edge of the panel to be a rectangle in order to manufacture
+  V-Cuts)
+
+Let's start with rails:
+""")
 
-When doing similar panel with mousebites, you usually want shorter tabs and
-possibly more of them. We can do it by specifing `--htabs` and `--vtabs`:
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2; space: 2mm"],
+        ["--tabs", "fixed; width: 3mm; vcount: 2"],
+        ["--cuts", "mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm"],
+        ["--framing", "railstb; width: 5mm; space: 3mm;"],
+        ["--post", "millradius: 1mm"],
+        [SRC]])
+
+print("""
+Similarly, you can add left and right rail via the `railslr` style. If you want
+a full frame, use the style `frame`. When you place a full frame, it might make
+sense to include cuts in the corner of the frame, so you can break it apart
+easily. Let's see an example:
 """)
 
-runBoardExample("panel5",
-    ["panelize", "grid", "--space", "3", "--gridsize", "2", "2", "--tabwidth", "3", "--tabheight", "3", "--htabs", "1", "--vtabs", "2", "--mousebites", "0.5", "1", "0.25", "--radius", "1", "doc/resources/conn.kicad_pcb"])
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2; space: 2mm"],
+        ["--tabs", "fixed; width: 3mm; vcount: 2"],
+        ["--cuts", "mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm"],
+        ["--framing", "frame; width: 5mm; space: 3mm; cuts: true"],
+        ["--post", "millradius: 1mm"],
+        [SRC]])
+
+print("""
+When you use V-cuts it might make sense to not remove all material, but only
+mill a slot around the board of the board. This yields a stronger panel – and
+some manufacturers require such style for assembly with V-Cuts. This is achieved
+via framing style `tightframe`. Note that it does not make much sense with
+mousebites.
+""")
 
-print(
-"""
-If you want, you can also add rails to the panel. Rails are suitable for placing
-tooling holes and fiducials, which are required by some assembly houses.
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2; space: 6mm"],
+        ["--tabs", "fixed; width: 3mm; vcount: 2"],
+        ["--cuts", "vcuts"],
+        ["--framing", "tightframe; width: 5mm; space: 3mm; "],
+        ["--post", "millradius: 1mm"],
+        [SRC]])
+
+print("""
+Once we have a frame, we can append a tooling holes, fiducials and some text to
+it:
+""")
+
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2; space: 2mm"],
+        ["--tabs", "fixed; width: 3mm; vcount: 2"],
+        ["--cuts", "mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm"],
+        ["--framing", "railstb; width: 5mm; space: 3mm;"],
+        ["--tooling", "3hole; hoffset: 2.5mm; voffset: 2.5mm; size: 1.5mm"],
+        ["--fiducials", "3fid; hoffset: 5mm; voffset: 2.5mm; coppersize: 2mm; opening: 1mm;"],
+        ["--text", "simple; text: yaqwsx's panel; anchor: mt; voffset: 2.5mm; hjustify: center; vjustify: center;"],
+        ["--post", "millradius: 1mm"],
+        [SRC]])
+
+print("""
+There are many options for text and fiducials. Be sure to read the [full
+documentation](panelizeCli.md).
+
+# Advanced features & layouts
+
+It is possible that you have some critical features you want to avoid with tabs.
+KiKit has several features that can help you. Let's start with the simple ones.
 
-The rails can be added by either `--railsTb <width>` (for top and bottom rails)
-or by `--railsLr <widht>` (for left and right rails). The rails work with both;
-mousebites and V-cuts.
+First, you can rotate the boards in your layout. This might make not much sense
+for rectanglar boards, but it might save you when you have circular or oddly
+shaped boards:
 
-Then you can add fiducials to the top-left, bottom-left and top-right corner via
-`--fiducials <horizontalOffset> <verticalOffset> <copperDiameter>
-<openingDiameter>`. Similarly, you can add tooling holes via `--tooling
-<horizontalOffset> <verticalOffset> <diameter>`.
 """)
 
-runBoardExample("panel6",
-    ["panelize", "grid", "--space", "3", "--gridsize", "2", "2", "--tabwidth", "18", "--tabheight", "10", "--vcuts", "--radius", "1", "--railsTb", "5", "--fiducials", "10", "2.5", "1", "2", "--tooling", "5", "2.5", "1.5", "doc/resources/conn.kicad_pcb"])
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2; space: 0mm; rotation: 45deg;"],
+        ["--tabs", "fixed; width: 3mm;"],
+        ["--cuts", "mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.75mm"],
+        ["--framing", "frame; width: 5mm; space: 3mm; cuts: true"],
+        ["--post", "millradius: 1mm"],
+        [SRC]])
+
+print("""
+When your board has a connector sticking one one side of the board, it makes
+sense to rotate the boards every other column, row or combination of both. KiKit
+supports this via layout option `alternation`. You should be careful about
+component references when rotating boards – KiCAD's references have a property
+"Stay upright" which makes them always face up (even when placed on a panel). So
+be sure to turn it off before panelizing. Here's an example:
+""")
 
-print(
-"""
-If you want to get a full frame around a board, don't use combination of top and
-bottom rails. It will generate a correct shape, however, it won't generate cuts
-to easily break the frame. Instead, use the "--panelsize <width> <height>"
-option in combination with `--framecutH` or `--framecutV` to add the cuts in the
-corners of the frame. Note that in the following example we use mousebites to
-see the frame cuts
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2; space: 3mm; alternation: cols;"],
+        ["--tabs", "fixed; width: 3mm; vcount: 2"],
+        ["--cuts", "mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm"],
+        ["--framing", "frame; width: 5mm; space: 3mm; cuts: true"],
+        ["--post", "millradius: 1mm"],
+        [SRC]])
+
+print("""
+Another solution might be to not put tabs on, e.g., vertical edges of the PCB.
+However, in that case your panel might be weak for further assembly. You can
+make it more stiff by including backbones – a full piece of substrate between
+the panels. Note that adding a backbone does not extend space between boards -
+that's up to you. You can add either vertical, horizontal or both backbones.
+Also, similarly with frames, you can put cuts on your backbone to make
+depanelization of your boards easier. Enough theory, let's see an example
 """)
 
-runBoardExample("panel7",
-    ["panelize", "grid", "--space", "3", "--gridsize", "2", "2", "--tabwidth", "5", "--tabheight", "5", "--mousebites", "0.5", "1", "0", "--radius", "1", "--panelsize", "75", "58", "--framecutH", "--fiducials", "10", "2.5", "1", "2", "--tooling", "5", "2.5", "1.5", "doc/resources/conn.kicad_pcb"])
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2; hspace: 2mm; vspace: 9mm; hbackbone: 5mm; hbonecut: true"],
+        ["--tabs", "fixed; width: 3mm; vcount: 2; hcount: 0"],
+        ["--cuts", "mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm"],
+        ["--framing", "railstb; width: 5mm; space: 3mm;"],
+        ["--post", "millradius: 1mm"],
+        [SRC]])
+
+print("""
+The most powerful feature of KiKit regarding tab placement are tabs via
+annotation. Remember our test board? When you open it in Pcbnew, you can see
+that there are some special footprints – KiKit's annotations:
+
+![conn-pcbnew](resources/conn-pcbnew.png)
+
+They specify where to place tabs. You can even specify individual tab width via
+text property of the symbol. How to use it? Just specify tab style to
+`annotation`. We also have to increase the source area tolerance, so it can
+capture the annotations.
+""")
 
-print(
-"""
-This was the `grid` command. There is also command `tightgrid` which works
-similarly, but instead of adding tabs and frames around the board, in places a
-full frame around the boards and mills a slot around the contours. Why this
-might be useful? For example when you make panel out of circular boards which
-you want to separate by V-cuts (by cutting a little bit to their interior). In
-that case don't forget to specify `--vcutcurves` to approximate curvature cuts
-via a straight V-cut. Back to `tightgrid`:
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2; space: 5mm;"],
+        ["--tabs", "annotation"],
+        ["--source", "tolerance: 15mm"],
+        ["--cuts", "mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm"],
+        ["--framing", "railstb; width: 5mm; space: 3mm;"],
+        ["--post", "millradius: 1mm"],
+        [SRC]])
+
+print("""
+Well, the panel looks strange – right? That's because KiKit always constructs a
+half-bridges. When you specify the tabs location, you have to either ensure they
+match or put a piece of substrate they can reach – e.g., a backbone or a
+tightframe. If you are interested in the details, read more about tabs in
+section [Understanding tabs](understandingTabs.md). Let's fix it:
 """)
-runBoardExample("panel8",
-    ["panelize", "tightgrid", "--slotwidth", "2.5", "--space", "8", "--gridsize", "2", "2", "--tabwidth", "15", "--tabheight", "8", "--mousebites", "0.5", "1", "0.25", "--radius", "1", "--panelsize", "80", "60", "doc/resources/conn.kicad_pcb"])
 
-print(
-"""
-You can also rotate the input board. Might not be usefull for
-rectangular boards, but if you have a circular or oddly shaped board...
-"""
-)
-runBoardExample("panel9",
-    ["panelize", "grid", "--space", "2", "--gridsize", "2", "2", "--tabwidth", "3", "--tabheight", "3", "--mousebites", "0.5", "1", "0.25", "--radius", "1", "--panelsize", "80", "80", "--rotation", "45", "doc/resources/conn.kicad_pcb"])
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2; space: 8mm; hbackbone: 3mm; vbackbone: 3mm"],
+        ["--tabs", "annotation"],
+        ["--source", "tolerance: 15mm"],
+        ["--cuts", "mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm"],
+        ["--framing", "railstb; width: 5mm; space: 3mm;"],
+        ["--post", "millradius: 1mm"],
+        [SRC]])
+
+print("""
+Note that the annotation can have an arbitrary orientation. The arrow just must
+be outside board edge and points towards it. KiKit will also place only those
+tabs, that have a neighboring substrate. For precise algorithm, see section
+[understanding tabs](understandingTabs.md).
+
+When you make flex PCBs or you want to save etchant, it make sense to pour
+copper on all non-functional parts of the panel. It will make the PCB rigid. You
+can do so via `copperfill` post-processing operation:
+""")
 
-print(
-"""
-Especially for irregular shaped board it might make sense to rotate boards by
-180° based on their position in the grid. It could allow you to pack the boards
-more closely. Also, if you create a panel out of boards that have e.g., a
-connector on one side, it makes sense to rotate the second column by 180° so the
-connectors are facing out of the panel. You can do so by specifying the
-`--alternation` option. This options allows you to rotate board in every second
-column (`cols`), every second row (`rows`) or combination of both:
-"""
-)
-runBoardExample("panel10",
-    ["panelize", "grid", "--space", "2", "--gridsize", "2", "2", "--tabwidth", "3", "--tabheight", "3", "--mousebites", "0.5", "1", "0.25", "--radius", "1", "--panelsize", "80", "80", "--alternation", "cols", "doc/resources/conn.kicad_pcb"])
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2; space: 2mm"],
+        ["--tabs", "fixed; width: 3mm;"],
+        ["--cuts", "mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm"],
+        ["--framing", "railstb; width: 5mm; space: 3mm;"],
+        ["--post", "millradius: 1mm; copperfill: true"],
+        [SRC]])
+
+print("""
+When you use V-cuts with `copperfill` you (or your fab house) might want to
+include a clearance around the V-cuts:
+""")
 
-print(
-"""
-Sometimes you might find yourself in a need for precise tab placement. This can be
-easily done. Just draw a line in one of KiCAD layers (be careful, the
-orientation matters - the line has to target the board substrate) and specify
-option `--tabsfrom`. Don't forget to disable automatically generated tabs by
-specifying `--vtabs 0` and `--htabs 0`. See for yourself, there are some lines
-already prepared for you in `conn.kicad_pcb`
-"""
-)
-runBoardExample("panel11",
-    ["panelize", "tightgrid", "--slotwidth", "2.5", "--space", "8", "--gridsize", "2", "2", "--htabs", "0", "--vtabs", "0", "--tabsfrom", "Eco2.User", "3", "--tabsfrom", "Eco1.User", "5", "--mousebites", "0.5", "1", "0.25", "--radius", "1", "--panelsize", "80", "80", "doc/resources/conn.kicad_pcb"])
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2; space: 2mm"],
+        ["--tabs", "fixed; hwidth: 10mm; vwidth: 15mm"],
+        ["--cuts", "vcuts; clearance: 1.5mm"],
+        ["--framing", "railstb; width: 5mm; space: 3mm;"],
+        ["--post", "millradius: 1mm; copperfill: true"],
+        [SRC]])
+
+print("""
+Note one last facts about V-cuts. V-cuts can only be straight and
+horizontal/vertical. But you can use them with circular boards if you want by
+cutting a little inside them. The option `cutcurves`, that will approximate the
+cut by staring and ending point.
+
+# I would like... but KiKit does not support it!
+
+If you need something special; e.g., custom placement of tooling holes, multiple
+texts, etc. KiKit has you covered.
+
+The CLI interface allows you to run a custom script over the final panel. The
+script can use KiKit Python interface to modify it. For the sake of simplicity,
+let's add a hole in the middle of the frame. Therefore, we write the following
+script:
+
+```.py
+from kikit.units import mm
+from pcbnew import wxPoint
+
+def kikitPostprocess(panel, arg):
+    minx, miny, maxx, maxy = panel.panelBBox()
+    position = wxPoint((minx + maxx) / 2, miny + 2 * mm)
+    panel.addNPTHole(position, 3 * mm)
+```
+
+Then run KiKit:
+""")
+
+runBoardExample(autoName(),
+    [["panelize"],
+        ["--layout", "grid; rows: 2; cols: 2; space: 2mm"],
+        ["--tabs", "fixed; width: 3mm; vcount: 2"],
+        ["--cuts", "mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm"],
+        ["--framing", "railstb; width: 5mm; space: 3mm;"],
+        ["--post", "millradius: 1mm; script: doc/resources/examplePost.py"],
+        [SRC]])
+
+
+print("""
+You can learn more about available functions from the comment in the source code
+or in [documentation](panelization.md).
+
+If you implement a feature that your fab house requires (e.g., new tooling hole
+style), consider submitting a pull request for KiKit instead. I believe the
+others will benefit from it.
+
+# Managing presets
+
+The last section of this document is dedicated to management of presets. You can
+read the specification in the [documentation for CLI](panelizeCli.md). Here I
+would like to focus on practical examples.
+
+As you should know from the documentation, the panelization preset is divided
+into sections; e. g., `layout`, `tabs`, etc. The key-value parameters in these
+sections can be specified via JSON files. In KiKit, you can specify these files
+via `-p` option:
+
+```
+kikit panelize -p myPreset.json -p :<builtInPreset> <other parameters>
+```
+
+The parameters in the later specified presets override the parameters in the
+previously specified presets. This allows you to define a named piece-wise
+presets. Therefore, you can prepare various presets for mousebites – e.g.,
+`fineMousebites.json` and `coarseMousebites.json`:
+
+```.js
+// fineMousebites.json
+{
+    "cuts": {
+        "style": "mousebites",
+        "drill": "0.5mm",
+        "spacing": "0.9mm",
+        "offset": "0.25mm"
+    }
+}
+
+// coarseMousebites.json
+{
+    "cuts": {
+        "style": "mousebites",
+        "drill": "0.3mm",
+        "spacing": "0.2mm",
+        "offset": "0.15mm"
+    }
+}
+```
+
+Then you can specify your panelization commands easily via:
+
+```
+kikit panelize -p fineMousebites.json <otheroptions>
+```
+
+Therefore, you can build a custom library of commonly used-options; e.g., per
+fabrication house. KiKit offers some built-in styles – see
+[`panelizePresets`](../kikit/resources/panelizePresets). Note that the built-in
+preset `default.json` is always used as a base and it specifies conservative
+default values so you can only override the options relevant for you.
+
+To give you an example – with KiKit, you will no longer have to remember what
+diameter of tooling holes JLC PCB requires, just use:
+
+```
+kikit panelize -p :jlcTooling <otheroptions>
+```
+""")
 
-print(
-"""
-Especially when you work with flex PCBs it makes sense to leave copper on
-non-functional parts of the panel to make it stiffer. It might also make sense
-to leave on traditional PCBs as it reduces the amount of etching. To do so,
-simply specify `--copperfill`. Here is one of the examples above with a copper
-fill:
-"""
-)
 
-runBoardExample("panel12",
-    ["panelize", "grid", "--space", "3", "--gridsize", "2", "2", "--tabwidth", "18", "--tabheight", "10", "--vcuts", "--radius", "1", "--panelsize", "70", "55", "--copperfill", "doc/resources/conn.kicad_pcb"])
\ No newline at end of file
+runBoardExampleJoin()
\ No newline at end of file
diff --git a/scripts/panelizeDoc.py b/scripts/panelizeDoc.py
index 7597612b..db800d6f 100644
--- a/scripts/panelizeDoc.py
+++ b/scripts/panelizeDoc.py
@@ -5,35 +5,57 @@
 """
 
 from kikit import panelize
+from kikit import substrate
 from kikit.doc import header, printHelp, printHeader
-
 from kikit.pcbnew_compatibility import pcbnew
+import inspect
+
+def synopsis(object):
+    printHeader(object)
+    printHelp(object)
+
+def classMethods(c):
+    for name, o in inspect.getmembers(c, predicate=inspect.isfunction):
+        if name.startswith("_"):
+            continue
+        synopsis(o)
 
 print(
-"""
+f"""
 # Panelization
 
 When you want to panelize a board, you are expected to load the `kikit.panelize`
 module and create an instance of the `Panel` class.
 
-All units are in the internal KiCAD units (1 nm). You can use functions `{}` and
-`{}` to convert to/from them (synopsis below). You are also encouraged to use
-the functions and objects the native KiCAD Python API offers, e.g.: {}.
+All units are in the internal KiCAD units (1 nm). You can use predefined
+constants to convert from/to them:
 
-""".format(
-    header(panelize.fromMm),
-    header(panelize.toMm),
-    "`" + "`, `".join([header(pcbnew.wxPoint), header(pcbnew.wxPointMM),
-              header(pcbnew.wxRect), header(pcbnew.wxRectMM)]) + "`"))
+```.py
+from kikit.units import *
+
+l = 1 * mm    # 1 mm
+l = 42 * inch # 42 inches
+l = 15 * cm   # 15 cm
+a = 90 * deg  # 90°
+a = 1 * rad   # 1 radian
+```
+
+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)])}.
+""")
 
 print(
 """
 ## Basic Concepts
 
-The `panelize.Panel` class holds a panel under construction. Basically it is
-`pcbnew.BOARD` without outlines. The outlines are held separately as
+The `kikit.panelize.Panel` class holds a panel under construction. Basically it
+is `pcbnew.BOARD` without outlines. The outlines are held separately as
 `shapely.MultiPolygon` so we can easily merge pieces of a substrate, add cuts
-and export it back to `pcbnew.BOARD`.
+and export it back to `pcbnew.BOARD`. This is all handled by the class
+`kikit.substrate.Substrate`.
 
 ## Tabs
 
@@ -53,6 +75,9 @@
 choice was made as batch adding of substrates is more efficient. Therefore, you
 are advised to first generate all the tabs and then append them to the board.
 
+You read more about the algorithms for generating tabs in a separate document
+[understanding tabs](understandingTabs.md).
+
 ## Cuts
 
 All methods constructing panels do not create cuts directly, instead, they
@@ -77,61 +102,30 @@
 """
 )
 
-print("""
+print(f"""
 ## Panel class
-""")
-
-printHelp(panelize.Panel)
-
-printHeader(panelize.Panel.appendBoard)
-printHelp(panelize.Panel.appendBoard)
-
-printHeader(panelize.Panel.save)
-printHelp(panelize.Panel.save)
-
-printHeader(panelize.Panel.appendSubstrate)
-printHelp(panelize.Panel.appendSubstrate)
-
-printHeader(panelize.Panel.makeGrid)
-printHelp(panelize.Panel.makeGrid)
-
-printHeader(panelize.Panel.makeTightGrid)
-printHelp(panelize.Panel.makeTightGrid)
-
-printHeader(panelize.Panel.makeFrame)
-printHelp(panelize.Panel.makeFrame)
-
-printHeader(panelize.Panel.makeVCuts)
-printHelp(panelize.Panel.makeVCuts)
-
-printHeader(panelize.Panel.makeMouseBites)
-printHelp(panelize.Panel.makeMouseBites)
-
-printHeader(panelize.Panel.addNPTHole)
-printHelp(panelize.Panel.addNPTHole)
-
-printHeader(panelize.Panel.addFiducial)
-printHelp(panelize.Panel.addFiducial)
 
+This class has the following relevant members:
+- `board` - `pcbnew.BOARD` of the panel. Does not contain any edges.
+- `substrates` - `kikit.substrate.Substrate` - individual substrates appended
+  via `{printHeader(panelize.Panel.appendBoard)}`. You can use them to get the
+  original outline (and e.g., generate tabs accroding to it).
+- `boardSubstrate` - `kikit.substrate.Substrate` of the whole panel.
+- `backboneLines` - a list of lines representing backbone candidates. Read more
+  about it in [understanding tabs](understandingTabs.md).
+""")
 
-print("""
-## Examples
+classMethods(panelize.Panel)
 
-### Simple grid
+print(f"""
+## Substrate class
 
-The following example creates a 3 x 3 grid of boards in a frame separated by V-CUTS.
+This class represents a pice of substrate (with no components). Basically it is
+just a relatively thin wrapper around shapely polygons. On top of that, it keeps
+a partition line for the substrate. ead more about partition lines in
+[understanding tabs](understandingTabs.md).
 
-```
-panel = Panel()
-size, cuts = panel.makeGrid("test.kicad_pcb", 4, 3, wxPointMM(100, 40),
-            tolerance=fromMm(5), verSpace=fromMm(5), horSpace=fromMm(5),
-            outerHorTabThickness=fromMm(3), outerVerTabThickness=fromMm(3),
-            verTabWidth=fromMm(15), horTabWidth=fromMm(8))
-panel.makeVCuts(cuts)
-# alternative: panel.makeMouseBites(cuts, diameter=fromMm(0.5), spacing=fromMm(1))
-panel.makeFrame(size, fromMm(100), fromMm(100), fromMm(3), radius=fromMm(1))
-panel.addMillFillets(fromMm(1))
-panel.save("out.kicad_pcb")
-```
 """)
 
+classMethods(substrate.Substrate)
+
diff --git a/setup.py b/setup.py
index 8a42d7d2..23f81ede 100644
--- a/setup.py
+++ b/setup.py
@@ -31,11 +31,15 @@
         "click",
         "markdown2",
         "pybars3",
-        "solidpython"
+        "solidpython",
+        "commentjson"
     ],
     setup_requires=[
         "versioneer"
     ],
+    extras_require={
+        "dev": ["pytest"],
+    },
     zip_safe=False,
     include_package_data=True,
     entry_points = {


Test patch:
diff --git a/.github/workflows/test-kikit.yml b/.github/workflows/test-kikit.yml
index 663fe6eb..aa16b51a 100644
--- a/.github/workflows/test-kikit.yml
+++ b/.github/workflows/test-kikit.yml
@@ -17,7 +17,7 @@ jobs:
             libgraphicsmagick1-dev libmagickcore-dev \
             openscad bats
           sudo python -m pip install PcbDraw
-          sudo python -m pip install -e .
+          sudo python -m pip install -e .\[dev\]
       - run: make test
       - run: make doc
       - run: sudo make package
diff --git a/test/resources/multiboard.kicad_pcb b/test/resources/multiboard.kicad_pcb
new file mode 100644
index 00000000..a26be08a
--- /dev/null
+++ b/test/resources/multiboard.kicad_pcb
@@ -0,0 +1,173 @@
+(kicad_pcb (version 20171130) (host pcbnew 5.1.9-73d0e3b20d~88~ubuntu20.04.1)
+
+  (general
+    (thickness 1.6)
+    (drawings 18)
+    (tracks 0)
+    (zones 0)
+    (modules 3)
+    (nets 1)
+  )
+
+  (page A4)
+  (layers
+    (0 F.Cu signal)
+    (31 B.Cu signal)
+    (32 B.Adhes user)
+    (33 F.Adhes user)
+    (34 B.Paste user)
+    (35 F.Paste user)
+    (36 B.SilkS user)
+    (37 F.SilkS user)
+    (38 B.Mask user)
+    (39 F.Mask user)
+    (40 Dwgs.User user)
+    (41 Cmts.User user)
+    (42 Eco1.User user)
+    (43 Eco2.User user)
+    (44 Edge.Cuts user)
+    (45 Margin user)
+    (46 B.CrtYd user)
+    (47 F.CrtYd user)
+    (48 B.Fab user)
+    (49 F.Fab user)
+  )
+
+  (setup
+    (last_trace_width 0.25)
+    (trace_clearance 0.2)
+    (zone_clearance 0.508)
+    (zone_45_only no)
+    (trace_min 0.2)
+    (via_size 0.8)
+    (via_drill 0.4)
+    (via_min_size 0.4)
+    (via_min_drill 0.3)
+    (uvia_size 0.3)
+    (uvia_drill 0.1)
+    (uvias_allowed no)
+    (uvia_min_size 0.2)
+    (uvia_min_drill 0.1)
+    (edge_width 0.2)
+    (segment_width 0.2)
+    (pcb_text_width 0.3)
+    (pcb_text_size 1.5 1.5)
+    (mod_edge_width 0.12)
+    (mod_text_size 1 1)
+    (mod_text_width 0.15)
+    (pad_size 1.524 1.524)
+    (pad_drill 0.762)
+    (pad_to_mask_clearance 0)
+    (aux_axis_origin 0 0)
+    (visible_elements FFFFFF7F)
+    (pcbplotparams
+      (layerselection 0x010fc_ffffffff)
+      (usegerberextensions false)
+      (usegerberattributes true)
+      (usegerberadvancedattributes true)
+      (creategerberjobfile true)
+      (excludeedgelayer true)
+      (linewidth 0.150000)
+      (plotframeref false)
+      (viasonmask false)
+      (mode 1)
+      (useauxorigin false)
+      (hpglpennumber 1)
+      (hpglpenspeed 20)
+      (hpglpendiameter 15.000000)
+      (psnegative false)
+      (psa4output false)
+      (plotreference true)
+      (plotvalue true)
+      (plotinvisibletext false)
+      (padsonsilk false)
+      (subtractmaskfromsilk false)
+      (outputformat 1)
+      (mirror false)
+      (drillshape 1)
+      (scaleselection 1)
+      (outputdirectory ""))
+  )
+
+  (net 0 "")
+
+  (net_class Default "This is the default net class."
+    (clearance 0.2)
+    (trace_width 0.25)
+    (via_dia 0.8)
+    (via_drill 0.4)
+    (uvia_dia 0.3)
+    (uvia_drill 0.1)
+  )
+
+  (module kikit:Board (layer F.Cu) (tedit 605A1488) (tstamp 605A707C)
+    (at 185 90)
+    (descr "Mark board for extraction")
+    (fp_text reference B3 (at -4.5 -5) (layer Dwgs.User)
+      (effects (font (size 1 1) (thickness 0.15)) (justify left))
+    )
+    (fp_text value Board (at -7.25 -5) (layer Dwgs.User)
+      (effects (font (size 1 1) (thickness 0.15)))
+    )
+    (fp_line (start 0 0) (end -1.25 -4) (layer Dwgs.User) (width 0.2))
+    (fp_line (start -1.25 -4) (end -9.25 -4) (layer Dwgs.User) (width 0.2))
+    (fp_line (start 0 0) (end 0.5 -1) (layer Dwgs.User) (width 0.2))
+    (fp_line (start 0 0) (end -1 -0.5) (layer Dwgs.User) (width 0.2))
+  )
+
+  (module kikit:Board (layer F.Cu) (tedit 605A1488) (tstamp 605A706E)
+    (at 135 90)
+    (descr "Mark board for extraction")
+    (fp_text reference B2 (at -4.5 -5) (layer Dwgs.User)
+      (effects (font (size 1 1) (thickness 0.15)) (justify left))
+    )
+    (fp_text value Board (at -7.25 -5) (layer Dwgs.User)
+      (effects (font (size 1 1) (thickness 0.15)))
+    )
+    (fp_line (start 0 0) (end -1 -0.5) (layer Dwgs.User) (width 0.2))
+    (fp_line (start 0 0) (end 0.5 -1) (layer Dwgs.User) (width 0.2))
+    (fp_line (start -1.25 -4) (end -9.25 -4) (layer Dwgs.User) (width 0.2))
+    (fp_line (start 0 0) (end -1.25 -4) (layer Dwgs.User) (width 0.2))
+  )
+
+  (module kikit:Board (layer F.Cu) (tedit 605A1488) (tstamp 605A7057)
+    (at 100 90)
+    (descr "Mark board for extraction")
+    (fp_text reference B1 (at -4.5 -5) (layer Dwgs.User)
+      (effects (font (size 1 1) (thickness 0.15)) (justify left))
+    )
+    (fp_text value Board (at -7.25 -5) (layer Dwgs.User)
+      (effects (font (size 1 1) (thickness 0.15)))
+    )
+    (fp_line (start 0 0) (end -1.25 -4) (layer Dwgs.User) (width 0.2))
+    (fp_line (start -1.25 -4) (end -9.25 -4) (layer Dwgs.User) (width 0.2))
+    (fp_line (start 0 0) (end 0.5 -1) (layer Dwgs.User) (width 0.2))
+    (fp_line (start 0 0) (end -1 -0.5) (layer Dwgs.User) (width 0.2))
+  )
+
+  (gr_text C (at 185 100) (layer F.SilkS) (tstamp 605A709F)
+    (effects (font (size 5 5) (thickness 1)))
+  )
+  (gr_text B (at 145 100) (layer F.SilkS) (tstamp 605A709D)
+    (effects (font (size 5 5) (thickness 1)))
+  )
+  (gr_text A (at 100 100) (layer F.SilkS)
+    (effects (font (size 5 5) (thickness 1)))
+  )
+  (gr_line (start 200 100) (end 190 100) (layer Edge.Cuts) (width 0.2) (tstamp 605A142A))
+  (gr_line (start 200 105) (end 200 100) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 190 105) (end 200 105) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 190 100) (end 190 105) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 210 90) (end 180 90) (layer Edge.Cuts) (width 0.2) (tstamp 605A1425))
+  (gr_line (start 210 110) (end 210 90) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 180 110) (end 210 110) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 180 90) (end 180 110) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 150 110) (end 135 110) (layer Edge.Cuts) (width 0.2) (tstamp 605A1415))
+  (gr_line (start 150 100) (end 150 110) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 160 100) (end 150 100) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 160 90) (end 160 100) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 135 90) (end 160 90) (layer Edge.Cuts) (width 0.2))
+  (gr_line (start 135 110) (end 135 90) (layer Edge.Cuts) (width 0.2))
+  (gr_circle (center 100 100) (end 110 100) (layer Edge.Cuts) (width 0.2))
+
+)
diff --git a/test/system/panelize.bats b/test/system/panelize.bats
index 38595441..2415fbc7 100644
--- a/test/system/panelize.bats
+++ b/test/system/panelize.bats
@@ -3,123 +3,145 @@
 load common
 
 @test "Extract board" {
-    kikit panelize extractboard \
-        --sourcearea 100 50 100 100 \
-        $RES/conn.kicad_pcb extract_res.kicad_pcb
+    kikit separate --source 'rectangle; tlx: 89mm; tly: 89mm; brx: 111mm; bry: 111mm' \
+        $RES/multiboard.kicad_pcb board_a.kicad_pcb
+
+    kikit separate --source 'annotation; ref: B1' \
+        $RES/multiboard.kicad_pcb board_a.kicad_pcb
 }
 
 @test "Simple grid, no space, vcuts" {
-    kikit panelize grid \
-        --gridsize 2 2 \
-        --vcuts \
-        $RES/conn.kicad_pcb panel1.kicad_pcb
+    kikit panelize \
+        --layout 'grid; rows: 2; cols: 2;' \
+        --tabs full \
+        --cuts vcuts \
+        $RES/conn.kicad_pcb panel.kicad_pcb
 }
 
-@test "Simple grid, no space, mousebites" {
-    kikit panelize grid \
-        --gridsize 2 2 \
-        --mousebites 0.5 1 0 \
-        $RES/conn.kicad_pcb panel2.kicad_pcb
+@test "Simple grid, spacing, vcuts" {
+    kikit panelize \
+        --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+        --tabs 'fixed; hwidth: 10mm; vwidth: 15mm' \
+        --cuts vcuts \
+        --post 'millradius: 1mm' \
+        $RES/conn.kicad_pcb panel.kicad_pcb
 }
 
-@test "Simple grid, no space, mousebites with radius" {
-    kikit panelize grid \
-        --gridsize 2 2 \
-        --mousebites 0.5 1 0 \
-        --radius 1 \
-        $RES/conn.kicad_pcb panel3.kicad_pcb
+@test "Simple grid, spacing, mousebites" {
+    kikit panelize \
+        --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+        --tabs 'fixed; width: 5mm' \
+        --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm' \
+        $RES/conn.kicad_pcb panel.kicad_pcb
 }
 
-@test "Simple grid, with space, vcusts and radius" {
-    kikit panelize grid \
-        --space 3 --gridsize 2 2 \
-        --tabwidth 18 --tabheight 10 \
-        --vcuts \
-        --radius 1 \
-        $RES/conn.kicad_pcb panel4.kicad_pcb
+
+@test "Simple grid, spacing, mousebites prolonged" {
+    kikit panelize \
+        --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+        --tabs 'fixed; width: 3mm' \
+        --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+        --post 'millradius: 1mm' \
+        $RES/conn.kicad_pcb panel.kicad_pcb
 }
 
-@test "Simple grid, with space, mousebites, multiple tabs and radius" {
-    kikit panelize grid \
-        --space 3 --gridsize 2 2 \
-        --tabwidth 3 --tabheight 3 --htabs 1 --vtabs 2 \
-        --mousebites 0.5 1 0.25 \
-        --radius 1 \
-        $RES/conn.kicad_pcb panel5.kicad_pcb
+@test "Simple grid, change number of tabs" {
+    kikit panelize \
+        --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+        --tabs 'fixed; width: 3mm; vcount: 2' \
+        --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+        --post 'millradius: 1mm' \
+        $RES/conn.kicad_pcb panel.kicad_pcb
 }
 
-@test "Simple grid, with space, vcuts, rails, tooling and fiducials" {
-    kikit panelize grid \
-        --space 3 --gridsize 2 2 \
-        --tabwidth 18 --tabheight 10 \
-        --vcuts --radius 1 \
-        --railsTb 5 --fiducials 10 2.5 1 2 --tooling 5 2.5 1.5 \
-        $RES/conn.kicad_pcb panel6.kicad_pcb
+@test "Simple grid, rails" {
+    kikit panelize \
+        --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+        --tabs 'fixed; width: 3mm; vcount: 2' \
+        --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+        --framing 'railstb; width: 5mm; space: 3mm;' \
+        --post 'millradius: 1mm' \
+        $RES/conn.kicad_pcb panel.kicad_pcb
+
+    kikit panelize \
+        --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+        --tabs 'fixed; width: 3mm; vcount: 2' \
+        --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+        --framing 'railslr; width: 5mm; space: 3mm;' \
+        --post 'millradius: 1mm' \
+        $RES/conn.kicad_pcb panel.kicad_pcb
 }
 
-@test "Simple grid with frame, tooling and fiducials" {
-    kikit panelize grid \
-        --space 3 --gridsize 2 2 \
-        --tabwidth 5 --tabheight 5 \
-        --mousebites 0.5 1 0 \
-        --radius 1 \
-        --panelsize 75 58 --framecutH \
-        --fiducials 10 2.5 1 2 --tooling 5 2.5 1.5 \
-        $RES/conn.kicad_pcb panel7.kicad_pcb
+@test "Simple grid, frame" {
+    kikit panelize \
+        --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+        --tabs 'fixed; width: 3mm; vcount: 2' \
+        --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+        --framing 'frame; width: 5mm; space: 3mm; cuts: true' \
+        --post 'millradius: 1mm' \
+        $RES/conn.kicad_pcb panel.kicad_pcb
 }
 
-@test "Tightgrid" {
-    kikit panelize tightgrid \
-        --slotwidth 2.5 --space 8 --gridsize 2 2 \
-        --tabwidth 15 --tabheight 8 \
-        --mousebites 0.5 1 0.25 \
-        --radius 1 --panelsize 80 60 \
-        $RES/conn.kicad_pcb panel8.kicad_pcb
+@test "Simple grid, tightgrid" {
+    kikit panelize \
+        --layout 'grid; rows: 2; cols: 2; space: 6mm' \
+        --tabs 'fixed; width: 3mm; vcount: 2' \
+        --cuts vcuts \
+        --framing 'tightframe; width: 5mm; space: 3mm; ' \
+        --post 'millradius: 1mm' \
+        $RES/conn.kicad_pcb panel.kicad_pcb
 }
 
-@test "Grid with board rotation" {
-    kikit panelize grid \
-        --space 2 --gridsize 2 2 \
-        --tabwidth 3 --tabheight 3 \
-        --mousebites 0.5 1 0.25 \
-        --radius 1 --panelsize 80 80 --rotation 45 \
-        $RES/conn.kicad_pcb panel9.kicad_pcb
+@test "Simple grid, framing features" {
+    kikit panelize \
+        --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+        --tabs 'fixed; width: 3mm; vcount: 2' \
+        --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+        --framing 'railstb; width: 5mm; space: 3mm;' \
+        --tooling '3hole; hoffset: 2.5mm; voffset: 2.5mm; size: 1.5mm' \
+        --fiducials '3fid; hoffset: 5mm; voffset: 2.5mm; coppersize: 2mm; opening: 1mm;' \
+        --text 'simple; text: yaqwsx panel; anchor: mt; voffset: 2.5mm; hjustify: center; vjustify: center;' \
+        --post 'millradius: 1mm' \
+        $RES/conn.kicad_pcb panel.kicad_pcb
 }
 
 @test "Grid with alternation" {
-    kikit panelize grid \
-        --space 2 --gridsize 2 2 \
-        --tabwidth 3 --tabheight 3 \
-        --mousebites 0.5 1 0.25 \
-        --radius 1 --panelsize 80 80 --alternation rows \
-        $RES/conn.kicad_pcb panel-alternationRows.kicad_pcb
-    kikit panelize grid \
-        --space 2 --gridsize 2 2 \
-        --tabwidth 3 --tabheight 3 \
-        --mousebites 0.5 1 0.25 \
-        --radius 1 --panelsize 80 80 --alternation cols \
-        $RES/conn.kicad_pcb panel-alternationCols.kicad_pcb
-    kikit panelize grid \
-        --space 2 --gridsize 2 2 \
-        --tabwidth 3 --tabheight 3 \
-        --mousebites 0.5 1 0.25 \
-        --radius 1 --panelsize 80 80 --alternation rowsCols \
-        $RES/conn.kicad_pcb panel-alternationRowsCols.kicad_pcb
+    kikit panelize \
+        --layout 'grid; rows: 2; cols: 2; space: 3mm; alternation: cols;' \
+        --tabs 'fixed; width: 3mm; vcount: 2' \
+        --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+        --framing 'frame; width: 5mm; space: 3mm; cuts: true' \
+        --post 'millradius: 1mm' \
+        $RES/conn.kicad_pcb panel.kicad_pcb
+}
+
+@test "Grid with backbone" {
+    kikit panelize \
+        --layout 'grid; rows: 2; cols: 2; hspace: 2mm; vspace: 9mm; hbackbone: 5mm; hbonecut: true' \
+        --tabs 'fixed; width: 3mm; vcount: 2; hcount: 0' \
+        --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+        --framing 'railstb; width: 5mm; space: 3mm;' \
+        --post 'millradius: 1mm' \
+        $RES/conn.kicad_pcb panel.kicad_pcb
 }
 
-@test "Tightgrid with custom tab positions" {
-    kikit panelize tightgrid \
-        --slotwidth 2.5 --space 8 --gridsize 2 2 \
-        --htabs 0 --vtabs 0 --tabsfrom Eco2.User 3 --tabsfrom Eco1.User 5 \
-        --mousebites 0.5 1 0.25 \
-        --radius 1 --panelsize 80 80 \
-        $RES/conn.kicad_pcb panel10.kicad_pcb
+@test "Tabs from annotation" {
+    kikit panelize \
+        --layout 'grid; rows: 2; cols: 2; space: 8mm; hbackbone: 3mm; vbackbone: 3mm' \
+        --tabs annotation \
+        --source 'tolerance: 15mm' \
+        --cuts 'mousebites; drill: 0.5mm; spacing: 1mm; offset: 0.2mm; prolong: 0.5mm' \
+        --framing 'railstb; width: 5mm; space: 3mm;' \
+        --post 'millradius: 1mm' \
+        $RES/conn.kicad_pcb panel.kicad_pcb
 }
 
-@test "Copper fill" {
-    kikit panelize grid \
-        --space 3 --gridsize 2 2 \
-        --tabwidth 18 --tabheight 10 \
-        --vcuts --radius 1 --panelsize 70 55 --copperfill \
-        $RES/conn.kicad_pcb panel11.kicad_pcb
+@test "Copperfill" {
+    kikit panelize \
+        --layout 'grid; rows: 2; cols: 2; space: 2mm' \
+        --tabs 'fixed; hwidth: 10mm; vwidth: 15mm' \
+        --cuts 'vcuts; clearance: 1.5mm' \
+        --framing 'railstb; width: 5mm; space: 3mm;' \
+        --post 'millradius: 1mm; copperfill: true' \
+        $RES/conn.kicad_pcb panel.kicad_pcb
 }
\ No newline at end of file
diff --git a/test/units/test_intervals.py b/test/units/test_intervals.py
new file mode 100644
index 00000000..4ed587b0
--- /dev/null
+++ b/test/units/test_intervals.py
@@ -0,0 +1,195 @@
+import pytest
+from kikit.intervals import *
+
+
+def test_intersection():
+    a = Interval(0, 2)
+    b = Interval(1, 2)
+    c = Interval(1, 3)
+    d = Interval(2, 3)
+    e = Interval(3, 4)
+
+    assert a.intersect(b) == Interval(1, 2)
+    assert a.intersect(c) == Interval(1, 2)
+    assert a.intersect(d) == Interval(2, 2)
+    assert a.intersect(e) is None
+
+def test_IntervalLists():
+    I = Interval
+    IL = IntervalList
+    a = IL([ I(0, 1), I(2, 3), I(5, 10) ])
+    b = IL([ I(0, 2), I(4, 8) ])
+
+
+    assert a.union(b) == IL([I(0, 3), I(4, 10)])
+    assert a.difference(b) == IL([I(2, 3), I(8, 10)])
+    assert a.intersect(b) == IL([I(0, 1), I(5, 8)])
+
+def test_boxNeighbors():
+    """
+    The test case is as follows:
+
+    +---+ +---+ +---+
+    | 1 | | 2 | | 3 |
+    +---+ +---+ +---+
+    +---+ +---+ +---+
+    | 4 | | 5 | | 6 |
+    +---+ +---+ +---+
+    +---------+ +---+
+    |    7    | | 8 |
+    +---------+ +---+
+
+    """
+
+    # minx, miny, maxx, maxy
+    b1 = (1, 1, 2, 2)
+    b2 = (3, 1, 4, 2)
+    b3 = (5, 1, 6, 2)
+    b4 = (1, 3, 2, 4)
+    b5 = (3, 3, 4, 4)
+    b6 = (5, 3, 6, 4)
+    b7 = (1, 5, 4, 6)
+    b8 = (5, 5, 6, 6)
+    boxes = { i: b for i, b in enumerate([b1, b2, b3, b4, b5, b6, b7, b8], 1) }
+
+    n = BoxNeighbors(boxes)
+
+    assert n.left(1) == []
+    assert n.right(1) == [2]
+    assert n.top(1) == []
+    assert n.bottom(1) == [4]
+
+    assert set(n.top(7)) == set([4, 5])
+
+def test_boxNeighborsOverlap():
+    """
+    The test case is as follows:
+
+    +---+---+---+
+    | 1 | 2 | 3 |
+    +---+---+---+
+    | 4 | 5 | 6 |
+    +---+---+---+
+    """
+    # minx, miny, maxx, maxy
+    b1 = (1, 1, 2, 2)
+    b2 = (2, 1, 3, 2)
+    b3 = (3, 1, 4, 2)
+    b4 = (1, 2, 2, 3)
+    b5 = (2, 2, 3, 3)
+    b6 = (3, 2, 4, 3)
+
+    boxes = { i: b for i, b in enumerate([b1, b2, b3, b4, b5, b6], 1) }
+
+    n = BoxNeighbors(boxes)
+
+    assert n.right(1) == [2]
+    assert n.left(2) == [1]
+    assert n.bottom(2) == [5]
+
+def test_boxNeighborsNotSameLevel():
+    """
+    The test case is as follows:
+
+    +---+ +---+
+    | 1 | | 2 |
+    +---+ +---+
+    +---+
+    | 3 |
+    +---+
+    +---------+
+    |    4    |
+    +---------+
+    """
+    # minx, miny, maxx, maxy
+    # minx, miny, maxx, maxy
+    b1 = (1, 1, 2, 2)
+    b2 = (3, 1, 4, 2)
+    b3 = (1, 3, 2, 4)
+    b4 = (1, 5, 4, 6)
+
+    boxes = { i: b for i, b in enumerate([b1, b2, b3, b4], 1) }
+
+    n = BoxNeighbors(boxes)
+
+    assert set(n.top(4)) == set([2, 3])
+    assert n.bottom(3) == [4]
+    assert n.bottom(2) == [4]
+    assert n.bottom(1) == [3]
+
+def test_bounds():
+    a = [1, 2, 3, 4, 5, 6, 7, 8]
+    b = [2, 4, 6, 8, 10, 12, 14]
+    c = [1, 2, 2, 2, 2, 3, 4]
+
+    assert a[upperBound(a, 5)] == 6
+    assert b[upperBound(b, 5)] == 6
+    assert b[upperBound(b, 1)] == 2
+    assert upperBound(b, 14) == 7
+    assert upperBound(b, 15) == 7
+    assert upperBound(c, 2) == 5
+    assert upperBound(c, 1) == 1
+
+    assert lowerBound(a, 1) == -1
+    assert a[lowerBound(a, 3)] == 2
+    assert a[lowerBound(a, 5)] == 4
+    assert b[lowerBound(b, 5)] == 4
+    assert a[lowerBound(a, 14)] == 8
+    assert b[lowerBound(b, 15)] == 14
+    assert lowerBound(c, 2) == 0
+    assert lowerBound(c, 1) == -1
+
+
+def test_buildShadows():
+    AL = AxialLine
+    lines = [AL(1, 2, 3, "L1"), AL(2, 1, 2, "L2")]
+    bounds = [
+        AL(0, 0, 3),
+        AL(1, 1, 2),
+        AL(3, 2, 3),
+        AL(4, 0, 2),
+        AL(5, 0, 3)
+    ]
+
+    shadows = buildShadows(lines, bounds)
+    assert shadows == [ShadowLine(lines[0], Interval(1, 4)),
+                       ShadowLine(lines[1], Interval(1, 3))]
+
+def test_trimShadows():
+    AL = AxialLine
+    SL = ShadowLine
+    I = Interval
+
+    lines = [SL(AL(1, 2, 3, "L1"), I(1, 5)), SL(AL(2, 1, 2, "L2"), I(1, 3))]
+    bounds = [
+        AL(0, 0, 3),
+        AL(1, 1, 2),
+        AL(3, 2, 3),
+        AL(4, 0, 2),
+        AL(5, 0, 3)
+    ]
+    shadows = trimShadows(lines, bounds)
+    assert shadows == [
+        SL(lines[0].line, I(1, 4)), SL(lines[1].line, I(1, 3))
+    ]
+
+def test_BoxPartitionLines():
+    pass
+    boxes = {
+        1: (0, 5, 1, 6),
+        2: (2, 5, 3, 6),
+        3: (4, 5, 5, 6),
+        4: (0, 3, 1, 4),
+        5: (2, 3, 3, 4),
+        6: (4, 3, 5, 4),
+        7: (0, 1, 1, 2),
+        8: (2, 1, 3, 2),
+        9: (4, 1, 5, 2),
+        10: (0, -1, 5, 0),
+        11: (0, 7, 5, 8)
+    }
+
+    filter = lambda idA, idB, v, l: idA < 10 or idB < 10
+    lines = BoxPartitionLines(boxes, filter)
+
+    hlines, vlines = lines.partitionLines(5)
\ No newline at end of file
diff --git a/test/units/test_panelize.py b/test/units/test_panelize.py
new file mode 100644
index 00000000..6dac8715
--- /dev/null
+++ b/test/units/test_panelize.py
@@ -0,0 +1,12 @@
+import pytest
+from kikit.panelize import prolongCut
+from shapely.geometry import LineString
+from math import sqrt
+
+
+def test_prolongCut():
+    line = LineString([(0, 0), (1, 1)])
+    prolonged = prolongCut(line, 0.5)
+
+    assert prolonged.coords[0] == pytest.approx((sqrt(2)/2 * -0.5, sqrt(2)/2 * -0.5))
+    assert prolonged.coords[1] == pytest.approx((1 + sqrt(2)/2 * 0.5, 1 + sqrt(2)/2 * 0.5))
diff --git a/test/units/test_preset.py b/test/units/test_preset.py
new file mode 100644
index 00000000..063fdafc
--- /dev/null
+++ b/test/units/test_preset.py
@@ -0,0 +1,33 @@
+import pytest
+from kikit.panelize_ui_impl import *
+
+def test_presetLayout():
+    with pytest.raises(PresetError):
+        validatePresetLayout([])
+    validatePresetLayout({})
+    with pytest.raises(PresetError):
+        validatePresetLayout({"a": []})
+    validatePresetLayout({"a": {"b": 43}})
+
+def test_merge():
+    # Merge into empty
+    a = {}
+    mergePresets(a, {"a": {}})
+    assert a == {"a": {}}
+
+    mergePresets(a, {"a": {
+        "value": 42,
+        "otherValue": 70
+    }})
+    assert a == {"a": {
+        "value": 42,
+        "otherValue": 70
+    }}
+
+    mergePresets(a, {"a": {
+        "value": 43
+    }})
+    assert a == {"a": {
+        "value": 43,
+        "otherValue": 70
+    }}
\ No newline at end of file
diff --git a/test/units/test_sexpr.py b/test/units/test_sexpr.py
new file mode 100644
index 00000000..b9d3d394
--- /dev/null
+++ b/test/units/test_sexpr.py
@@ -0,0 +1,20 @@
+import pytest
+from kikit.sexpr import *
+
+def eval(s, truth):
+    parsed = parseSexprS(s)
+    assert parsed == truth
+    assert str(parsed) == s
+
+def test_sexpr():
+    eval("(a b)", SExpr([Atom("a"), Atom("b", " ")]))
+    eval("(a b )", SExpr([Atom("a"), Atom("b", " ")], "", " "))
+    eval("((a)b(c))", SExpr([
+        SExpr([Atom("a")]), Atom("b"), SExpr([Atom("c")])
+    ]))
+
+    source = """(sym_lib_table
+        (lib (name 4xxx)(type Legacy)(uri ${KICAD_SYMBOL_DIR}/4xxx.lib)(options "")(descr "4xxx series symbols"))
+        (lib (name 74xGxx)(type Legacy)(uri ${KICAD_SYMBOL_DIR}/74xGxx.lib)(options "")(descr "74xGxx symbols"))
+    )"""
+    assert str(parseSexprS(source)) == source
diff --git a/test/units/test_substrate.py b/test/units/test_substrate.py
new file mode 100644
index 00000000..d0f2e111
--- /dev/null
+++ b/test/units/test_substrate.py
@@ -0,0 +1,22 @@
+import pytest
+from kikit.substrate import *
+
+def test_biteBoundary():
+    l1 = LineString([ (0, 0), (1, 0), (1, 1), (0, 1)])
+    l2 = LineString([ (0, 0), (1, 0), (1, 1), (0, 1), (0, 0)])
+    l3 = LinearRing([ (0, 0), (1, 0), (1, 1), (0, 1)])
+
+    t1 = biteBoundary(l1, (1, 0.5), (0.5, 1), 0.01)
+    assert t1 == LineString([(1, 0.5), (1, 1), (0.5, 1)])
+
+    t2 = biteBoundary(l1, (0, 0.5), (0.5, 0), 0.1)
+    assert t2 is None
+
+    t3 = biteBoundary(l2, (0, 0.5), (0.5, 0), 0.1)
+    assert t3 == LineString([(0, 0.5), (0, 0), (0.5, 0)])
+
+    t4 = biteBoundary(l3, (0, 0.5), (0.5, 0), 0.1)
+    assert t4 == LineString([(0, 0.5), (0, 0), (0.5, 0)])
+
+    t5 = biteBoundary(l1, (1, 0.25), (1, 0.75), 0.1)
+    assert t5 == LineString([(1, 0.25), (1, 0.75)])
\ No newline at end of file
diff --git a/test/units/test_units.py b/test/units/test_units.py
new file mode 100644
index 00000000..08649982
--- /dev/null
+++ b/test/units/test_units.py
@@ -0,0 +1,15 @@
+import pytest
+from pcbnew import FromMM, FromMils
+from kikit.units import readLength, readAngle, UnitError
+
+def test_readLength():
+    assert readLength("4.24mm") == FromMM(4.24)
+    assert readLength("4.24 mm") == FromMM(4.24)
+    assert readLength("4mm") == FromMM(4)
+
+    assert readLength("1m") == FromMM(1000)
+    assert readLength("1cm") == FromMM(10)
+
+    assert readLength("1mil") == FromMils(1)
+    assert readLength("1inch") == FromMils(1000)
+