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