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 <noreply@anthropic.com>
This commit is contained in:
Tim Hadwen
2026-02-01 22:48:44 +10:00
parent 5ac89f2df3
commit 5e3e00c8c1

View File

@@ -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)