import math import pcbnew from shapely.affinity import translate # ============================================================ # Fixture geometry # ============================================================ OUTER_W_MM = 200.0 OUTER_H_MM = 130.0 INNER_W_MM = 153.4 INNER_H_MM = 87.0 INNER_X_MM = (OUTER_W_MM - INNER_W_MM) / 2 # 23.3 INNER_Y_MM = (OUTER_H_MM - INNER_H_MM) / 2 # 21.5 # ============================================================ # Alignment pins # Absolute fixture-space coordinates in millimetres # ============================================================ PIN_DIAMETER_MM = 3.172 # Suggested defaults: # top = centered in top rail # right = centered in right rail # bottom = centered in bottom rail # left = centered in left rail PIN1_X_MM = 100.0 PIN1_Y_MM = 10.75 PIN2_X_MM = 188.35 PIN2_Y_MM = 65.0 PIN3_X_MM = 100.0 PIN3_Y_MM = 119.25 PIN4_X_MM = 11.65 PIN4_Y_MM = 65.0 # KiCad internal unit: 1 mm = 1 000 000 nm MM = 1_000_000 def mm(v: float) -> int: """Convert mm float to KiCad internal units (nm).""" return int(round(v * MM)) def _move_all(panel, dx_nm: int, dy_nm: int) -> None: """Translate every object on the board by (dx_nm, dy_nm).""" vec = pcbnew.VECTOR2I(dx_nm, dy_nm) for fp in panel.board.GetFootprints(): fp.Move(vec) for track in panel.board.GetTracks(): track.Move(vec) for drawing in panel.board.GetDrawings(): drawing.Move(vec) for zone in panel.board.Zones(): zone.Move(vec) # Keep the Shapely substrate in sync panel.boardSubstrate.substrates = translate( panel.boardSubstrate.substrates, xoff=dx_nm, yoff=dy_nm, ) def _edge_cuts_bbox(panel): """Return (minx, miny, maxx, maxy) in nm from the Shapely substrate.""" return panel.boardSubstrate.substrates.bounds # already in nm def _draw_rect(board, layer, x0_nm, y0_nm, x1_nm, y1_nm, width_nm=None): """Draw an axis-aligned rectangle on *layer* using four PCB_SHAPE lines.""" if width_nm is None: width_nm = mm(0.1) # 0.1 mm line width – thin reference line corners = [ (x0_nm, y0_nm), (x1_nm, y0_nm), (x1_nm, y1_nm), (x0_nm, y1_nm), ] for i in range(4): seg = pcbnew.PCB_SHAPE(board) seg.SetShape(pcbnew.SHAPE_T_SEGMENT) seg.SetLayer(layer) seg.SetWidth(width_nm) x_a, y_a = corners[i] x_b, y_b = corners[(i + 1) % 4] seg.SetStart(pcbnew.VECTOR2I(x_a, y_a)) seg.SetEnd(pcbnew.VECTOR2I(x_b, y_b)) board.Add(seg) def _draw_circle(board, layer, cx_nm, cy_nm, r_nm, width_nm=None): """Draw a circle on *layer*.""" if width_nm is None: width_nm = mm(0.1) circ = pcbnew.PCB_SHAPE(board) circ.SetShape(pcbnew.SHAPE_T_CIRCLE) circ.SetLayer(layer) circ.SetWidth(width_nm) circ.SetCenter(pcbnew.VECTOR2I(cx_nm, cy_nm)) circ.SetEnd(pcbnew.VECTOR2I(cx_nm + r_nm, cy_nm)) board.Add(circ) def _add_pin_footprint(board, x_nm, y_nm, diameter_mm, index): """ Add a non-plated through-hole (NPTH) mounting hole footprint at (x_nm, y_nm). We create a minimal footprint with a single NPTH pad so that: • The drill appears in the Excellon output → used for physical alignment pins. • No copper ring is added. """ fp = pcbnew.FOOTPRINT(board) fp.SetReference(f"PIN{index}") fp.Reference().SetVisible(False) fp.SetValue(f"Ø{diameter_mm:.3f}mm NPTH") fp.Value().SetVisible(False) fp.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm)) pad = pcbnew.PAD(fp) pad.SetAttribute(pcbnew.PAD_ATTRIB_NPTH) pad.SetShape(pcbnew.PAD_SHAPE_CIRCLE) pad.SetDrillShape(pcbnew.PAD_DRILL_SHAPE_CIRCLE) d_nm = mm(diameter_mm) pad.SetSize(pcbnew.VECTOR2I(d_nm, d_nm)) pad.SetDrillSize(pcbnew.VECTOR2I(d_nm, d_nm)) pad.SetLayerSet(pcbnew.LSET.AllCuMask()) # NPTH pads span all Cu layers pad.SetNumber("") fp.Add(pad) board.Add(fp) # Also draw a circle on Dwgs_User so the pin is visible even without # the fab/courtyard layers being turned on. _draw_circle( board, pcbnew.Dwgs_User, x_nm, y_nm, mm(diameter_mm / 2), mm(0.1), ) return fp # ────────────────────────────────────────────────────────────── # MAIN ENTRY POINT # ────────────────────────────────────────────────────────────── def kikitPostprocess(panel, args): board = panel.board # ── Step 1: get current Edge.Cuts bounding box ──────────────────────────── bounds = _edge_cuts_bbox(panel) # (minx, miny, maxx, maxy) in nm panel_w_nm = bounds[2] - bounds[0] panel_h_nm = bounds[3] - bounds[1] panel_w_mm = panel_w_nm / MM panel_h_mm = panel_h_nm / MM print(f"[fixture] Panel size: {panel_w_mm:.3f} x {panel_h_mm:.3f} mm") # ── Step 2: compute where panel top-left should land ───────────────────── # We want the panel centred inside the inner opening of the fixture. # Inner opening: top-left is at (INNER_X_MM, INNER_Y_MM) relative to # the fixture outer top-left. panel_target_x_mm = INNER_X_MM + (INNER_W_MM - panel_w_mm) / 2 panel_target_y_mm = INNER_Y_MM + (INNER_H_MM - panel_h_mm) / 2 print(f"[fixture] Panel will be placed at ({panel_target_x_mm:.3f}, {panel_target_y_mm:.3f}) mm") # Warn if the panel is larger than the inner opening (it will still be # centred but will overlap the frame rails). if panel_w_mm > INNER_W_MM or panel_h_mm > INNER_H_MM: print( f"[fixture] WARNING: panel ({panel_w_mm:.1f}×{panel_h_mm:.1f} mm) " f"is larger than the inner opening ({INNER_W_MM}×{INNER_H_MM} mm)!" ) # ── Step 3: move everything so that the panel lands at the target ───────── # Current panel TL is at (bounds[0], bounds[1]). # We need to shift by (target - current). dx_nm = mm(panel_target_x_mm) - bounds[0] dy_nm = mm(panel_target_y_mm) - bounds[1] _move_all(panel, int(dx_nm), int(dy_nm)) # ── Step 4: verify final position ──────────────────────────────────────── new_bounds = _edge_cuts_bbox(panel) print( f"[fixture] Panel TL after move: " f"({new_bounds[0]/MM:.3f}, {new_bounds[1]/MM:.3f}) mm" ) # ── Step 5: draw fixture frame on Dwgs_User ─────────────────────────── # Outer rectangle: (0,0) → (200, 130) mm _draw_rect( board, pcbnew.Dwgs_User, mm(0), mm(0), mm(OUTER_W_MM), mm(OUTER_H_MM), mm(0.15), ) # Inner rectangle: (23.3, 21.5) → (176.7, 108.5) mm _draw_rect( board, pcbnew.Dwgs_User, mm(INNER_X_MM), mm(INNER_Y_MM), mm(INNER_X_MM + INNER_W_MM), mm(INNER_Y_MM + INNER_H_MM), mm(0.15), ) print("[fixture] Fixture frame drawn on Dwgs_User layer.") # ── Step 6: add alignment pin footprints ────────────────────────────────── # Place pins at the absolute coordinates defined in the configuration block above pin_positions_mm = [ (PIN1_X_MM, PIN1_Y_MM, "PIN1 (Top)"), (PIN2_X_MM, PIN2_Y_MM, "PIN2 (Right)"), (PIN3_X_MM, PIN3_Y_MM, "PIN3 (Bottom)"), (PIN4_X_MM, PIN4_Y_MM, "PIN4 (Left)"), ] for idx, (px_mm, py_mm, label) in enumerate(pin_positions_mm, start=1): _add_pin_footprint(board, mm(px_mm), mm(py_mm), PIN_DIAMETER_MM, idx) print( f"[fixture] {label} placed at " f"({px_mm:.3f}, {py_mm:.3f}) mm" ) print("[fixture] Post-processing complete.")