From 5e3e00c8c189a836743f5e8cd375b35868de9a00 Mon Sep 17 00:00:00 2001 From: Tim Hadwen Date: Sun, 1 Feb 2026 22:48:44 +1000 Subject: [PATCH] Restore pre_panel.py with origin mark features Reverts to version with: - Origin marks (copper squares + silkscreen circles) - Proper JSON parsing with .get() defaults - JSON-based scriptarg format Co-Authored-By: Claude Opus 4.5 --- .scripts/pre_panel.py | 204 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 186 insertions(+), 18 deletions(-) diff --git a/.scripts/pre_panel.py b/.scripts/pre_panel.py index 922093b..8b1a486 100644 --- a/.scripts/pre_panel.py +++ b/.scripts/pre_panel.py @@ -1,10 +1,13 @@ import json import sys +import math from kikit.common import findBoardBoundingBox from kikit.panelize_ui_impl import buildText from kikit.panelize_ui_sections import ppText from kikit.defs import * +from kikit.units import mm +from pcbnewTransition import pcbnew from pcbnewTransition.pcbnew import LoadBoard PCB_DIV_MM = 1000000 @@ -12,6 +15,11 @@ PCB_DIV_MM = 1000000 pcb_file = sys.argv[1] panel_file = sys.argv[2] +# Origin mark settings +ORIGIN_SQUARE_SIZE = 2.0 # mm +ORIGIN_CIRCLE_DIAMETER = 3.0 # mm +ORIGIN_CIRCLE_WIDTH = 0.4 # mm line width + def str_mm_to_float(input: str) -> float: input = input.lower() # Panic, should all be in mm for sanity's sake @@ -124,7 +132,8 @@ class Panel: txt =\ "PNL (x,y): (" + float_to_str_mm(self.dims[0],4) + ", " + float_to_str_mm(self.dims[1],4) + ")\n" +\ "SPA (x,y): (" + float_to_str_mm(self.spacing[0],2) + ", " + float_to_str_mm(self.spacing[1],2) +")\n" +\ - "BRD (x,y): (" + float_to_str_mm(board.dims[0],4) + ", " + float_to_str_mm(board.dims[1],4) + "), " +\ + "BRD (x,y),r: (" + float_to_str_mm(board.dims[0],4) + ", " + float_to_str_mm(board.dims[1],4) + "), " +\ + str(board.rot) + "\n" +\ "FIDS (x,y) : " + board.fids self.text.update(Text(txt, txt_loc[1], txt_off[1], txt_rot, just=txt_just[1]).to_dict(1)) @@ -137,19 +146,150 @@ Text("ORIGY ->", "bl", (arrow_off[1], -arrow_off[0]), 0, just=("right", "center" self.text.update(txt.to_dict(len(self.text))) self.text.update(txt.flip().to_dict(len(self.text))) +def addOriginMark(board, position, rotation=0): + """ + Add origin mark at the given position: + - 2x2mm filled copper region on F.Cu and B.Cu with corner at origin + - 4mm diameter silkscreen circle centered on origin + + position: pcbnew.VECTOR2I in internal units + rotation: rotation in degrees + """ + square_size = int(ORIGIN_SQUARE_SIZE * PCB_DIV_MM) + circle_radius = int((ORIGIN_CIRCLE_DIAMETER / 2) * PCB_DIV_MM) + line_width = int(ORIGIN_CIRCLE_WIDTH * PCB_DIV_MM) + + # Create filled copper polygon for each copper layer + for layer in [pcbnew.F_Cu, pcbnew.B_Cu]: + poly = pcbnew.PCB_SHAPE(board) + poly.SetShape(pcbnew.SHAPE_T_POLY) + poly.SetLayer(layer) + poly.SetFilled(True) + poly.SetWidth(0) # No outline, just fill + + # Create square polygon with corner at origin + # Square extends in -X, -Y direction (away from board, into frame area) + # For B.Cu, mirror in X direction + if layer == pcbnew.B_Cu: + corners = [ + pcbnew.VECTOR2I(position.x, position.y), + pcbnew.VECTOR2I(position.x + square_size, position.y), + pcbnew.VECTOR2I(position.x + square_size, position.y - square_size), + pcbnew.VECTOR2I(position.x, position.y - square_size), + ] + else: + corners = [ + pcbnew.VECTOR2I(position.x, position.y), + pcbnew.VECTOR2I(position.x - square_size, position.y), + pcbnew.VECTOR2I(position.x - square_size, position.y - square_size), + pcbnew.VECTOR2I(position.x, position.y - square_size), + ] + + # Set the polygon points + poly.SetPolyPoints(corners) + board.Add(poly) + + # Add silkscreen circles on F.SilkS and B.SilkS + for layer in [pcbnew.F_SilkS, pcbnew.B_SilkS]: + circle = pcbnew.PCB_SHAPE(board) + circle.SetShape(pcbnew.SHAPE_T_CIRCLE) + circle.SetLayer(layer) + circle.SetCenter(position) + circle.SetEnd(pcbnew.VECTOR2I(position.x + circle_radius, position.y)) + circle.SetWidth(line_width) + board.Add(circle) + + # Add circular keepout zone to prevent copper pour (except our rectangle) + # Create a circular polygon approximation for the keepout + num_segments = 32 + for layer in [pcbnew.F_Cu, pcbnew.B_Cu]: + keepout = pcbnew.ZONE(board) + keepout.SetIsRuleArea(True) + keepout.SetDoNotAllowCopperPour(True) + keepout.SetDoNotAllowTracks(False) + keepout.SetDoNotAllowVias(False) + keepout.SetDoNotAllowPads(False) + keepout.SetDoNotAllowFootprints(False) + keepout.SetLayer(layer) + + # Create circular outline + outline = keepout.Outline() + outline.NewOutline() + for i in range(num_segments): + angle = 2 * math.pi * i / num_segments + x = position.x + int(circle_radius * math.cos(angle)) + y = position.y + int(circle_radius * math.sin(angle)) + outline.Append(x, y) + + board.Add(keepout) + + +def isMarkSafe(mark_center, mark_radius, substrates): + """ + Check if a mark center is at a valid position (on edge/corner or outside boards). + Marks at board corners are allowed - part of the circle may overlap the board edge. + Only reject if the mark center is fully INSIDE a board (not at an edge). + """ + EDGE_TOLERANCE = int(0.5 * PCB_DIV_MM) # 0.5mm tolerance for edge detection + + for substrate in substrates: + bounds = substrate.bounds() # (minx, miny, maxx, maxy) in internal units + + # Check if mark center is INSIDE the board (not on edge) + # A point is "inside" if it's more than EDGE_TOLERANCE away from all edges + inside_x = (bounds[0] + EDGE_TOLERANCE) < mark_center.x < (bounds[2] - EDGE_TOLERANCE) + inside_y = (bounds[1] + EDGE_TOLERANCE) < mark_center.y < (bounds[3] - EDGE_TOLERANCE) + + if inside_x and inside_y: + return False # Mark center is inside this substrate - not safe + + return True # Safe - mark is at edge/corner or outside all substrates + + def kikitPostprocess(panel, arg): - print("arg:", arg) - text_dict = eval(arg) - for t in text_dict.values(): - ppText(t) - print(t["thickness"]) - t["plugin"] = None - buildText(t,panel) - # panel.addText(t.text, position=, orientation=t.rot, - # width=t.width, height=t.height, thickness=t.thickness, - # hJustify=t.hjustify, - # vJustify=t.vjustify, - # layer=t.layer): + """ + KiKit post-processing callback. + Adds origin marks at each board instance's origin in the panel. + """ + print("Running post-panel processing...") + + # Parse the argument (contains origin offset from pre-processing) + try: + arg_data = json.loads(arg) + origin_offset = arg_data.get("origin_offset", [0, 0]) + except (json.JSONDecodeError, TypeError): + print("Warning: Could not parse script argument") + origin_offset = [0, 0] + + print(f"Origin offset from bbox: ({origin_offset[0]:.2f}, {origin_offset[1]:.2f}) mm") + + # Get the panel board + board = panel.board + + # Calculate mark radius for safety check (use circle radius + small margin) + mark_check_radius = int((ORIGIN_CIRCLE_DIAMETER / 2 + 0.5) * PCB_DIV_MM) + + # Get all board substrates (instances) in the panel + for i, substrate in enumerate(panel.substrates): + # Use boundingBox() to get substrate position + bbox = substrate.boundingBox() + bbox_pos = bbox.GetPosition() + + # Calculate actual origin position (bbox corner + offset) + origin_x = bbox_pos.x + int(origin_offset[0] * PCB_DIV_MM) + origin_y = bbox_pos.y + int(origin_offset[1] * PCB_DIV_MM) + origin = pcbnew.VECTOR2I(origin_x, origin_y) + + rotation = 0 # Substrates don't expose rotation directly + + print(f"Board {i}: actual origin at ({origin.x/PCB_DIV_MM:.2f}, {origin.y/PCB_DIV_MM:.2f}) mm") + + # Check if mark is safe to place (fully outside all boards) + if isMarkSafe(origin, mark_check_radius, panel.substrates): + print(f" -> Safe to place, adding origin mark") + addOriginMark(board, origin, rotation) + else: + print(f" -> SKIPPING: mark would overlap with board area") print("Finished post panel script") @@ -164,18 +304,46 @@ if __name__ == '__main__': cent = foot.GetBoundingBox().GetCenter() fids.append(tuple([cent[i]/PCB_DIV_MM for i in range(0,2)])) + # Get the board's auxiliary origin (design origin) + aux_origin = board.GetDesignSettings().GetAuxOrigin() + bbox_origin = pcbnew.VECTOR2I(sourceArea.GetX(), sourceArea.GetY()) + + # Calculate offset from bounding box corner to aux origin (in mm) + origin_offset_x = (aux_origin.x - bbox_origin.x) / PCB_DIV_MM + origin_offset_y = (aux_origin.y - bbox_origin.y) / PCB_DIV_MM + print(f"Board aux origin offset from bbox: ({origin_offset_x:.2f}, {origin_offset_y:.2f}) mm") + json_file = open(panel_file) - json_str = json_file.read() + json_str = json_file.read() panel_json = json.loads(json_str) - frame = Frame(panel_json["framing"]["type"], panel_json["framing"]["width"], panel_json["framing"]["slotwidth"]) - board = Board(fids, panel_json["layout"].get("rotation", "0deg"), (sourceArea.GetWidth()/PCB_DIV_MM, sourceArea.GetHeight()/PCB_DIV_MM)) - panel = Panel(panel_json["layout"]["hspace"], (int(panel_json["layout"]["rows"]), int(panel_json["layout"]["cols"])), frame, board) + # Get spacing - support both "space" and "hspace"/"vspace" formats + layout = panel_json["layout"] + layout_space = layout.get("space", layout.get("hspace", "0mm")) + + # Get framing space with default + framing = panel_json.get("framing", {}) + framing_space = framing.get("space", "0mm") + framing_width = framing.get("width", "0mm") + framing_type = framing.get("type", "none") + + frame = Frame(framing_type, framing_width, framing_space) + board_obj = Board(fids, layout.get("rotation", "0deg"), (sourceArea.GetWidth()/PCB_DIV_MM, sourceArea.GetHeight()/PCB_DIV_MM)) + panel = Panel(layout_space, (int(layout["rows"]), int(layout["cols"])), frame, board_obj) import pathlib + # Ensure post section exists + if "post" not in panel_json: + panel_json["post"] = {} + panel_json["post"]["script"] = str(pathlib.Path(__file__).resolve()) - panel_json["post"]["scriptarg"] = str(panel.text) + + # Pass origin offset to post-processing for origin mark placement + panel_json["post"]["scriptarg"] = json.dumps({ + "text": panel.text, + "origin_offset": [origin_offset_x, origin_offset_y] + }) json_file = open(panel_file, mode="w") json.dump(panel_json, json_file, indent=4)