commit 8fddcbe292b9edbfa59bd29987f51066f804de8e Author: cpu Date: Thu Jun 11 18:31:24 2026 +0200 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1605a40 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +panel/ + diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..992490e --- /dev/null +++ b/Readme.md @@ -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. + +![Panel](../images/Flow_Controller_Panel.png) + +## 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/). + diff --git a/__pycache__/cleanup.cpython-312.pyc b/__pycache__/cleanup.cpython-312.pyc new file mode 100644 index 0000000..1eed653 Binary files /dev/null and b/__pycache__/cleanup.cpython-312.pyc differ diff --git a/cleanup.py b/cleanup.py new file mode 100644 index 0000000..b492d8f --- /dev/null +++ b/cleanup.py @@ -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) \ No newline at end of file diff --git a/export_panel_outlines_gerber.py b/export_panel_outlines_gerber.py new file mode 100755 index 0000000..9c3f38e --- /dev/null +++ b/export_panel_outlines_gerber.py @@ -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() \ No newline at end of file diff --git a/myPreset.json b/myPreset.json new file mode 100644 index 0000000..79fe701 --- /dev/null +++ b/myPreset.json @@ -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 + } +} diff --git a/tooling_plugin.py b/tooling_plugin.py new file mode 100644 index 0000000..b9cd6eb --- /dev/null +++ b/tooling_plugin.py @@ -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) +