initial
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
panel/
|
||||||
|
|
||||||
47
Readme.md
Normal file
47
Readme.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
TODO: Add table of content
|
||||||
|
# KiKit Processor
|
||||||
|
Processing script for KiKit panelizes PCBs and draws a fixture sketch and positions the whole panel for easy CNC and MSLA processing.
|
||||||
|
|
||||||
|
The script:
|
||||||
|
|
||||||
|
- Panelizes the board
|
||||||
|
- Moves the finished panel to the origin `(0, 0)`
|
||||||
|
- Adds alignment holes
|
||||||
|
- Adds silskcreen text
|
||||||
|
- Extends the copper layers
|
||||||
|
|
||||||
|
The resulting output is intended for repeatable CNC/MSLA processing.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Install kikit
|
||||||
|
Install `kikit`:
|
||||||
|
```bash
|
||||||
|
pipx install --system-site-packages kikit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Panelize the PCB using the preset defined in `myPreset.json`.
|
||||||
|
kikit panelize \
|
||||||
|
-p myPreset.json \
|
||||||
|
../Flow_Controller/Flow_Controller.kicad_pcb \
|
||||||
|
panel/Flow_Controller_Panel.kicad_pcb
|
||||||
|
```
|
||||||
|
|
||||||
|
The new `panel/Flow_Controller_Panel.kicad_pcb` file will contain the panelized PCB with the following feature specified in `myPreset.json`. E.g.: Grid of 1 x 2 with space 2.1 mm and fiducials.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"layout": {
|
||||||
|
"type": "grid",
|
||||||
|
"rows": 1,
|
||||||
|
"cols": 2,
|
||||||
|
"hspace": "2.1mm",
|
||||||
|
"vspace": "2.1mm"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
See all values in [default.json](https://raw.githubusercontent.com/yaqwsx/KiKit/refs/heads/master/kikit/resources/panelizePresets/default.json)
|
||||||
|
|
||||||
|
Check the kikit panelization [examples](https://yaqwsx.github.io/KiKit/latest/panelization/examples/).
|
||||||
|
|
||||||
BIN
__pycache__/cleanup.cpython-312.pyc
Normal file
BIN
__pycache__/cleanup.cpython-312.pyc
Normal file
Binary file not shown.
96
cleanup.py
Normal file
96
cleanup.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import pcbnew
|
||||||
|
from shapely.geometry import MultiPolygon, Polygon
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# KiKit postprocess hook – cleanup.py
|
||||||
|
#
|
||||||
|
# 1. Copy all Edge.Cuts content (substrate rings) to Eco1_User as segments,
|
||||||
|
# EXCEPT the outermost panel rectangle.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TOLERANCE_NM = 1_000 # 1 µm
|
||||||
|
|
||||||
|
|
||||||
|
def _mm(nm_val):
|
||||||
|
return f"{pcbnew.ToMM(int(nm_val)):.4f} mm"
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_rings(geom):
|
||||||
|
"""Return all rings as list of (shapely_ring, coord_list)."""
|
||||||
|
rings = []
|
||||||
|
if isinstance(geom, Polygon):
|
||||||
|
rings.append((geom.exterior, list(geom.exterior.coords)))
|
||||||
|
for interior in geom.interiors:
|
||||||
|
rings.append((interior, list(interior.coords)))
|
||||||
|
elif isinstance(geom, MultiPolygon):
|
||||||
|
for poly in geom.geoms:
|
||||||
|
rings.extend(_collect_rings(poly))
|
||||||
|
return rings
|
||||||
|
|
||||||
|
|
||||||
|
def _is_outer_frame(ring_geom, x0, y0, x1, y1, tol=TOLERANCE_NM):
|
||||||
|
"""True if the ring's bounding box equals the panel bounding box."""
|
||||||
|
b = ring_geom.bounds
|
||||||
|
return (abs(b[0] - x0) <= tol and abs(b[1] - y0) <= tol and
|
||||||
|
abs(b[2] - x1) <= tol and abs(b[3] - y1) <= tol)
|
||||||
|
|
||||||
|
|
||||||
|
def _shapely_to_segments(board, coords, layer, width_mm=0.05):
|
||||||
|
pts = list(coords)
|
||||||
|
if not pts:
|
||||||
|
return 0
|
||||||
|
if pts[0] != pts[-1]:
|
||||||
|
pts.append(pts[0])
|
||||||
|
count = 0
|
||||||
|
for i in range(len(pts) - 1):
|
||||||
|
seg = pcbnew.PCB_SHAPE(board)
|
||||||
|
seg.SetShape(pcbnew.SHAPE_T_SEGMENT)
|
||||||
|
seg.SetLayer(layer)
|
||||||
|
seg.SetWidth(pcbnew.FromMM(width_mm))
|
||||||
|
seg.SetStart(pcbnew.VECTOR2I(int(pts[i][0]), int(pts[i][1])))
|
||||||
|
seg.SetEnd (pcbnew.VECTOR2I(int(pts[i+1][0]), int(pts[i+1][1])))
|
||||||
|
board.Add(seg)
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def kikitPostprocess(panel, arg):
|
||||||
|
print("=" * 60)
|
||||||
|
print("[cleanup] START")
|
||||||
|
|
||||||
|
board = panel.board
|
||||||
|
substrate = panel.boardSubstrate
|
||||||
|
|
||||||
|
# outer panel bbox
|
||||||
|
b = substrate.bounds()
|
||||||
|
x0, y0, x1, y1 = int(b[0]), int(b[1]), int(b[2]), int(b[3])
|
||||||
|
print(f"[cleanup] Panel bbox: ({_mm(x0)},{_mm(y0)})–({_mm(x1)},{_mm(y1)})")
|
||||||
|
|
||||||
|
# get substrate geometry
|
||||||
|
geom = None
|
||||||
|
for attr in ("substrates", "substrate", "_substrate", "geometry"):
|
||||||
|
if hasattr(substrate, attr):
|
||||||
|
geom = getattr(substrate, attr)
|
||||||
|
break
|
||||||
|
if geom is None:
|
||||||
|
print("[cleanup] ERROR: cannot access substrate geometry")
|
||||||
|
return
|
||||||
|
|
||||||
|
rings = _collect_rings(geom)
|
||||||
|
print(f"[cleanup] Total rings in substrate: {len(rings)}")
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
for i, (ring_geom, coords) in enumerate(rings):
|
||||||
|
rb = ring_geom.bounds
|
||||||
|
if _is_outer_frame(ring_geom, x0, y0, x1, y1):
|
||||||
|
print(f"[cleanup] ring[{i}] pts={len(coords)-1} → OUTER FRAME, skipped")
|
||||||
|
continue
|
||||||
|
n = _shapely_to_segments(board, coords, pcbnew.Eco1_User)
|
||||||
|
total += n
|
||||||
|
print(f"[cleanup] ring[{i}] pts={len(coords)-1}"
|
||||||
|
f" bbox=({_mm(rb[0])},{_mm(rb[1])})–({_mm(rb[2])},{_mm(rb[3])})"
|
||||||
|
f" → {n} segments on Eco1_User")
|
||||||
|
|
||||||
|
print(f"[cleanup] Total Eco1_User segments added: {total}")
|
||||||
|
print("[cleanup] END")
|
||||||
|
print("=" * 60)
|
||||||
44
export_panel_outlines_gerber.py
Executable file
44
export_panel_outlines_gerber.py
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import pcbnew
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Export a single PCB layer to a Gerber file using pcbnew."
|
||||||
|
)
|
||||||
|
parser.add_argument("input", help="Input .kicad_pcb file")
|
||||||
|
parser.add_argument("--output", "-o", required=True, help="Output directory")
|
||||||
|
parser.add_argument("--layers", "-l", required=True,
|
||||||
|
help="Layer name to export (e.g. User.Eco1)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
board = pcbnew.LoadBoard(args.input)
|
||||||
|
|
||||||
|
# Resolve layer name to ID
|
||||||
|
layer_id = board.GetLayerID(args.layers)
|
||||||
|
if layer_id == -1:
|
||||||
|
parser.error(f"Unknown layer: {args.layers!r}. "
|
||||||
|
f"Run with a known layer name (e.g. User.Eco1, Edge.Cuts).")
|
||||||
|
|
||||||
|
# Resolve output directory to absolute path so pcbnew doesn't make it
|
||||||
|
# relative to the board file location
|
||||||
|
output_dir = os.path.abspath(args.output)
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
prefix = args.layers.replace(".", "-")
|
||||||
|
|
||||||
|
pc = pcbnew.PLOT_CONTROLLER(board)
|
||||||
|
po = pc.GetPlotOptions()
|
||||||
|
po.SetOutputDirectory(output_dir)
|
||||||
|
po.SetPlotFrameRef(False)
|
||||||
|
|
||||||
|
pc.SetLayer(layer_id)
|
||||||
|
pc.OpenPlotfile(prefix, pcbnew.PLOT_FORMAT_GERBER, args.layers)
|
||||||
|
print(f"Plotting layer {args.layers!r} (id={layer_id}) to {pc.GetPlotFileName()}")
|
||||||
|
pc.PlotLayer()
|
||||||
|
pc.ClosePlot()
|
||||||
|
print("Done.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
106
myPreset.json
Normal file
106
myPreset.json
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"layout": {
|
||||||
|
"type": "grid",
|
||||||
|
"rows": 1,
|
||||||
|
"cols": 2,
|
||||||
|
"hspace": "2.1mm",
|
||||||
|
"vspace": "2.1mm"
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"type": "fixed",
|
||||||
|
"hcount": 1,
|
||||||
|
"vcount": 1,
|
||||||
|
"hwidth": "3mm",
|
||||||
|
"vwidth": "3mm"
|
||||||
|
},
|
||||||
|
"cuts": {
|
||||||
|
"type": "mousebites",
|
||||||
|
"offset": "0.2mm",
|
||||||
|
"prolong": "0.7mm",
|
||||||
|
"drill": "0.5mm",
|
||||||
|
"spacing": "0.8mm"
|
||||||
|
},
|
||||||
|
"framing": {
|
||||||
|
"type": "tightframe",
|
||||||
|
"copperFill": true,
|
||||||
|
"slotwidth": "2.1mm",
|
||||||
|
"mintotalheight": "87.040mm",
|
||||||
|
"mintotalwidth": "153.408mm",
|
||||||
|
"maxtotalheight": "87.040mm",
|
||||||
|
"maxtotalwidth": "153.408mm"
|
||||||
|
},
|
||||||
|
"tooling": {
|
||||||
|
"type": "3hole",
|
||||||
|
"layout": "3hole",
|
||||||
|
"hoffset": "6mm",
|
||||||
|
"voffset": "6mm",
|
||||||
|
"size": "2mm",
|
||||||
|
"paste": true,
|
||||||
|
"soldermaskmargin": "0mm"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"type": "simple",
|
||||||
|
"text": "Front",
|
||||||
|
"anchor": "mt",
|
||||||
|
"hoffset": "0mm",
|
||||||
|
"voffset": "10mm",
|
||||||
|
"orientation": "0deg",
|
||||||
|
"width": "3.5mm",
|
||||||
|
"height": "3.5mm",
|
||||||
|
"hjustify": "center",
|
||||||
|
"vjustify": "center",
|
||||||
|
"thickness": "0.3mm",
|
||||||
|
"layer": "F.SilkS"
|
||||||
|
},
|
||||||
|
"text2": {
|
||||||
|
"type": "simple",
|
||||||
|
"text": "Back",
|
||||||
|
"anchor": "mt",
|
||||||
|
"hoffset": "0mm",
|
||||||
|
"voffset": "10mm",
|
||||||
|
"orientation": "0deg",
|
||||||
|
"width": "3.5mm",
|
||||||
|
"height": "3.5mm",
|
||||||
|
"hjustify": "center",
|
||||||
|
"vjustify": "center",
|
||||||
|
"thickness": "0.3mm",
|
||||||
|
"layer": "B.SilkS"
|
||||||
|
},
|
||||||
|
"copperfill": {
|
||||||
|
"type": "solid",
|
||||||
|
"clearance": "0.5mm",
|
||||||
|
"edgeclearance": "0.5mm",
|
||||||
|
"layers": "F.Cu,B.Cu"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"type": "auto",
|
||||||
|
"copperfill": false,
|
||||||
|
"reconstructarcs": false,
|
||||||
|
"millradius": "1mm",
|
||||||
|
"millradiusouter": "0mm",
|
||||||
|
"script": "cleanup.py",
|
||||||
|
"scriptarg": "",
|
||||||
|
"origin": "tl",
|
||||||
|
"refillzones": false,
|
||||||
|
"dimensions": true,
|
||||||
|
"edgewidth": "0.1mm"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"type": "inherit",
|
||||||
|
"anchor": "tl",
|
||||||
|
"posx": "0mm",
|
||||||
|
"posy": "0mm",
|
||||||
|
"width": "1000mm",
|
||||||
|
"height": "1000mm"
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"type": "none",
|
||||||
|
"drawPartitionLines": false,
|
||||||
|
"drawBackboneLines": false,
|
||||||
|
"drawboxes": false,
|
||||||
|
"trace": false,
|
||||||
|
"deterministic": false,
|
||||||
|
"drawtabfail": false,
|
||||||
|
"drawTabFillet": false
|
||||||
|
}
|
||||||
|
}
|
||||||
131
tooling_plugin.py
Normal file
131
tooling_plugin.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
from kikit.plugin import ToolingPlugin
|
||||||
|
import pcbnew
|
||||||
|
|
||||||
|
class CustomTooling(ToolingPlugin):
|
||||||
|
def buildTooling(self, panel):
|
||||||
|
board = panel.board
|
||||||
|
|
||||||
|
# panelBBox() -> (xmin, ymin, xmax, ymax)
|
||||||
|
xmin, ymin, xmax, ymax = panel.panelBBox()
|
||||||
|
|
||||||
|
min_x = pcbnew.ToMM(xmin)
|
||||||
|
min_y = pcbnew.ToMM(ymin)
|
||||||
|
max_x = pcbnew.ToMM(xmax)
|
||||||
|
max_y = pcbnew.ToMM(ymax)
|
||||||
|
|
||||||
|
margin_x_mm = 10
|
||||||
|
margin_y_mm = 2.5
|
||||||
|
|
||||||
|
center_x = (min_x + max_x) / 2
|
||||||
|
center_y = (min_y + max_y) / 2
|
||||||
|
|
||||||
|
holes = [
|
||||||
|
(min_x + margin_x_mm, min_y + margin_y_mm), # top left
|
||||||
|
(center_x, min_y + margin_y_mm), # top center
|
||||||
|
(max_x - margin_x_mm, min_y + margin_y_mm), # top right
|
||||||
|
|
||||||
|
(min_x + margin_x_mm, max_y - margin_y_mm), # bottom left
|
||||||
|
(center_x, max_y - margin_y_mm), # bottom center
|
||||||
|
(max_x - margin_x_mm, max_y - margin_y_mm), # bottom right
|
||||||
|
]
|
||||||
|
|
||||||
|
hole_d = pcbnew.FromMM(3.172)
|
||||||
|
|
||||||
|
for x_mm, y_mm in holes:
|
||||||
|
fp = pcbnew.FOOTPRINT(board)
|
||||||
|
fp.SetReference("")
|
||||||
|
|
||||||
|
pos = pcbnew.VECTOR2I(
|
||||||
|
pcbnew.FromMM(x_mm),
|
||||||
|
pcbnew.FromMM(y_mm)
|
||||||
|
)
|
||||||
|
|
||||||
|
fp.SetPosition(pos)
|
||||||
|
|
||||||
|
pad = pcbnew.PAD(fp)
|
||||||
|
pad.SetShape(pcbnew.PAD_SHAPE_CIRCLE)
|
||||||
|
pad.SetAttribute(pcbnew.PAD_ATTRIB_NPTH)
|
||||||
|
|
||||||
|
pad.SetSize(pcbnew.VECTOR2I(hole_d, hole_d))
|
||||||
|
pad.SetDrillSize(pcbnew.VECTOR2I(hole_d, hole_d))
|
||||||
|
pad.SetPosition(pos)
|
||||||
|
|
||||||
|
fp.Add(pad)
|
||||||
|
board.Add(fp)
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# SCREEN RECTANGLE
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
screen_w = 153.4
|
||||||
|
screen_h = 87.0
|
||||||
|
|
||||||
|
# Panel center
|
||||||
|
center_x = (min_x + max_x) / 2
|
||||||
|
center_y = (min_y + max_y) / 2
|
||||||
|
|
||||||
|
# Screen rectangle corners
|
||||||
|
screen_x0 = center_x - (screen_w / 2)
|
||||||
|
screen_y0 = center_y - (screen_h / 2)
|
||||||
|
|
||||||
|
screen_x1 = center_x + (screen_w / 2)
|
||||||
|
screen_y1 = center_y + (screen_h / 2)
|
||||||
|
|
||||||
|
screen = pcbnew.PCB_SHAPE(board)
|
||||||
|
screen.SetShape(pcbnew.SHAPE_T_RECT)
|
||||||
|
screen.SetLayer(pcbnew.Dwgs_User)
|
||||||
|
|
||||||
|
screen.SetStart(
|
||||||
|
pcbnew.VECTOR2I(
|
||||||
|
pcbnew.FromMM(screen_x0),
|
||||||
|
pcbnew.FromMM(screen_y0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
screen.SetEnd(
|
||||||
|
pcbnew.VECTOR2I(
|
||||||
|
pcbnew.FromMM(screen_x1),
|
||||||
|
pcbnew.FromMM(screen_y1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
screen.SetWidth(pcbnew.FromMM(0.2))
|
||||||
|
|
||||||
|
board.Add(screen)
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# FIXTURE RECTANGLE
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
fixture_w = 200.0
|
||||||
|
fixture_h = 130.0
|
||||||
|
|
||||||
|
# Fixture rectangle corners
|
||||||
|
fixture_x0 = center_x - (fixture_w / 2)
|
||||||
|
fixture_y0 = center_y - (fixture_h / 2)
|
||||||
|
|
||||||
|
fixture_x1 = center_x + (fixture_w / 2)
|
||||||
|
fixture_y1 = center_y + (fixture_h / 2)
|
||||||
|
|
||||||
|
fixture = pcbnew.PCB_SHAPE(board)
|
||||||
|
fixture.SetShape(pcbnew.SHAPE_T_RECT)
|
||||||
|
fixture.SetLayer(pcbnew.Dwgs_User)
|
||||||
|
|
||||||
|
fixture.SetStart(
|
||||||
|
pcbnew.VECTOR2I(
|
||||||
|
pcbnew.FromMM(fixture_x0),
|
||||||
|
pcbnew.FromMM(fixture_y0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fixture.SetEnd(
|
||||||
|
pcbnew.VECTOR2I(
|
||||||
|
pcbnew.FromMM(fixture_x1),
|
||||||
|
pcbnew.FromMM(fixture_y1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fixture.SetWidth(pcbnew.FromMM(0.2))
|
||||||
|
|
||||||
|
board.Add(fixture)
|
||||||
|
|
||||||
Reference in New Issue
Block a user