Help debugging plugin I'm creating

Working with KiCAD 9.0.2 on Ubuntu 24.04

Partly as a hobby, but mostly because the via-fence generation plugins out there are buggy, I’m trying to create one based on the following principles: gather the selected trace — a sequence of “chunks” (chunk can be either a segment or an arc); split it into teeny tiny straight line segments of 0.01mm length (this is at the abstract mathematical level, not changing the PCB layout). For each of those, I generate two points, P_L and P_R (left and right of the trace), perpendicular to the tiny segment, 0.9mm away from it — thus, 0.9mm from the trace (for now, I’m hardcoding everything; I will worry about user-provided parameters once I manage to make the algorithm work).

The idea is that with these teeny tiny segments, the sequence of points P_L and P_R won’t do weird things as long as the trace does not have discontinuities in its tangent direction (which is normally the case for RF-style traces where I need the via fences).

I then create segments (again, at the abstract mathematical level) from P_L[k] to P_L[k+1] and same for P_R — so, I create two lines/curves that travel next to the original trace, 0.9mm away on each side. The segments of these two curves P_L and P_R are not 0.01mm in length (because of the curves); so, I now simply advance through the segments, adding their lengths until the accumulated sum reaches 0.75mm (again, hardcoding everything for now); then, I place a via there — I do this independently for P_L and P_R, because they don’t advance covering distance at the same rate. [[ actually, I determine the total length and do an integer division to see the exact number of vias that fit at approximately 0.75mm distance — so, it ends up being in general slightly above 0.75mm)

As a debugging assist / sanity-check, I am actually drawing the segments that define the trace as well as the P_L’s and P_R’s in sequence, on the User.Drawings layer.

See the script below — written with the help of chatGPT (please forgive me: I am pathologically Python-phobic and therefore Python impaired :‒\ ) … seems to work with a sequence of segments and arcs as shown in the screenshot below (with the expected glitches at the inner sides of the 45 degree corners between segments):

If I select those segments and do fillet tracks (with 2mm radius) to convert everything into a “smooth” curve, this is the horrific mess I get:

It looks like the endpoints in the arcs are inconsistent, or in any case something like the various chunks not being in sequence (end of one chunk coinciding with the start of the next chunk), and the script is unsuccessfully trying to put them in order (which worked for the other example, but not sure what’s going on here).

See script below. Can you confirm whether the above hypothesis has any merit? Any suggested debugging steps to help me figure out what’s going on?

import pcbnew
import wx
import math

class ViaFenceGeneratorProper(pcbnew.ActionPlugin):
    def defaults(self):
        self.name = "Via Fence Generator Proper"
        self.category = "Via Tools"
        self.description = "Draws $T$, $T_L$, $T_R$ and places evenly spaced vias along offsets"
        self.show_toolbar_button = True
        self.icon_file_name = ""

    def Run(self):
        board = pcbnew.GetBoard()
        items = [t for t in board.GetTracks()
                 if t.IsSelected() and isinstance(t, (pcbnew.PCB_TRACK, pcbnew.PCB_ARC))]
        if not items:
            wx.MessageBox("No selected tracks or arcs found.")
            return

        # Determine GND-like net
        netcodes = board.GetNetsByName()
        preferred = ["GND", "GNDA", "GNDD"]
        net = None
        for name in preferred:
            if name in netcodes:
                net = netcodes[name]
                break
        if not net:
            wx.MessageBox("No suitable net found (GND, GNDA, GNDD).")
            return

        # Build endpoint map for chaining
        endpoint_map = {}
        for item in items:
            for pt in (item.GetStart(), item.GetEnd()):
                key = (pt.x, pt.y)
                endpoint_map.setdefault(key, []).append(item)

        def find_chain(start_item):
            visited_ids = set([id(start_item)])
            chain = [start_item]

            def extend(pt, prepend=False):
                key = (pt.x, pt.y)
                while True:
                    options = [s for s in endpoint_map.get(key, []) if id(s) not in visited_ids]
                    if not options:
                        break
                    nxt = options[0]
                    visited_ids.add(id(nxt))
                    next_pt = nxt.GetEnd() if (nxt.GetStart().x, nxt.GetStart().y) == key else nxt.GetStart()
                    if prepend:
                        chain.insert(0, nxt)
                    else:
                        chain.append(nxt)
                    key = (next_pt.x, next_pt.y)

            extend(start_item.GetEnd(), prepend=False)
            extend(start_item.GetStart(), prepend=True)
            return chain

        chain = find_chain(items[0])

        seg_len   = 0.01 * 1e6
        offset_nm = 0.9  * 1e6
        thickness = int(0.05 * 1e6)
        layer     = 17
        drill     = int(0.4 * 1e6)
        diameter  = int(0.6 * 1e6)

        # Sample centerline
        centerline = []
        for t in chain:
            if isinstance(t, pcbnew.PCB_ARC):
                c = t.GetCenter()
                r = t.GetRadius()
                a0 = t.GetArcAngleStart().AsRadians()
                da = t.GetAngle().AsRadians()
                length = abs(da * r)
                steps = max(1, int(length / seg_len))
                for i in range(steps + 1):
                    theta = a0 + da * i / steps
                    centerline.append((c.x + r * math.cos(theta), c.y + r * math.sin(theta)))
            else:
                s = t.GetStart(); e = t.GetEnd()
                dx = e.x - s.x; dy = e.y - s.y
                length = math.hypot(dx, dy)
                steps = max(1, int(length / seg_len))
                for i in range(steps + 1):
                    f = i / steps
                    centerline.append((s.x + dx * f, s.y + dy * f))

        # Deduplicate
        dedup = []
        for pt in centerline:
            if not dedup or pt != dedup[-1]:
                dedup.append(pt)
        centerline = dedup

        # Draw centerline T
        for i in range(1, len(centerline)):
            x0, y0 = centerline[i-1]; x1, y1 = centerline[i]
            seg = pcbnew.PCB_SHAPE(board)
            seg.SetShape(pcbnew.SHAPE_T_SEGMENT)
            seg.SetStart(pcbnew.VECTOR2I(int(x0), int(y0)))
            seg.SetEnd(  pcbnew.VECTOR2I(int(x1), int(y1)))
            seg.SetLayer(layer)
            seg.SetWidth(thickness)
            board.Add(seg)

        # Offset curves
        TL = []; TR = []
        for i in range(len(centerline)):
            if i == 0:
                x1, y1 = centerline[0]; x2, y2 = centerline[1]
            elif i == len(centerline)-1:
                x1, y1 = centerline[-2]; x2, y2 = centerline[-1]
            else:
                x1, y1 = centerline[i-1]; x2, y2 = centerline[i+1]
            dx = x2 - x1; dy = y2 - y1
            d = math.hypot(dx, dy)
            if d == 0:
                TL.append(centerline[i]); TR.append(centerline[i])
            else:
                nx = -dy/d; ny = dx/d
                cx, cy = centerline[i]
                TL.append((cx + nx*offset_nm, cy + ny*offset_nm))
                TR.append((cx - nx*offset_nm, cy - ny*offset_nm))

        # Compute spacing
        def compute_spacing(path):
            total = 0.0
            for i in range(1, len(path)):
                x0, y0 = path[i-1]; x1, y1 = path[i]
                total += math.hypot(x1 - x0, y1 - y0)
            count = max(1, int(total // (0.75 * 1e6)))
            return total / count if count > 0 else total

        spacing_L = compute_spacing(TL)
        spacing_R = compute_spacing(TR)

        # Draw path and place vias: start, evenly spaced, end if needed
        def draw_curve_with_vias(path, spacing_nm):
            cumulative = 0.0
            next_via_at = spacing_nm

            # Place first via
            x0, y0 = path[0]
            via = pcbnew.PCB_VIA(board)
            via.SetPosition(pcbnew.VECTOR2I(int(x0), int(y0)))
            via.SetDrill(drill)
            via.SetWidth(diameter)
            via.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
            via.SetNet(net)
            board.Add(via)

            for i in range(1, len(path)):
                x0, y0 = path[i-1]; x1, y1 = path[i]
                dx = x1 - x0; dy = y1 - y0
                d = math.hypot(dx, dy)

                # Draw segment
                seg = pcbnew.PCB_SHAPE(board)
                seg.SetShape(pcbnew.SHAPE_T_SEGMENT)
                seg.SetStart(pcbnew.VECTOR2I(int(x0), int(y0)))
                seg.SetEnd(  pcbnew.VECTOR2I(int(x1), int(y1)))
                seg.SetLayer(layer)
                seg.SetWidth(thickness)
                board.Add(seg)

                cumulative += d
                while cumulative >= next_via_at:
                    ratio = (next_via_at - (cumulative - d)) / d
                    xv = x0 + dx * ratio
                    yv = y0 + dy * ratio
                    via = pcbnew.PCB_VIA(board)
                    via.SetPosition(pcbnew.VECTOR2I(int(xv), int(yv)))
                    via.SetDrill(drill)
                    via.SetWidth(diameter)
                    via.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
                    via.SetNet(net)
                    board.Add(via)
                    next_via_at += spacing_nm

            # Final via if remainder is significant
            if cumulative - (next_via_at - spacing_nm) > 0.7 * 1e6:
                xf, yf = path[-1]
                via = pcbnew.PCB_VIA(board)
                via.SetPosition(pcbnew.VECTOR2I(int(xf), int(yf)))
                via.SetDrill(drill)
                via.SetWidth(diameter)
                via.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
                via.SetNet(net)
                board.Add(via)

        draw_curve_with_vias(TL, spacing_L)
        draw_curve_with_vias(TR, spacing_R)

        wx.MessageBox("Drew $T$, $T_L$, $T_R$ and placed evenly spaced vias (including start/end).")
        pcbnew.Refresh()

ViaFenceGeneratorProper().register()

1 Like

It seems to me that in your second image you have ran the algorithm twice, there are vias around initial tracks and another set of vias around pieces of track your plugin created which creates (at least part of) the mess.

Make a clean example with simpler initial shape where it is easier to see what is going on.

Also there are issues in your algo even in the first image, notice around the inside of the track corner joins where vias overlap.

I don’t have a lot of experience in this sort of thing but my understanding is that common approach here is to not treat each segment separately but combine the shape into one complex polygon and then inflate the polygon. The outline of the inflated polygon will then be where the vias should be placed.

1 Like

Actually, that was expected (I mean, a known limitation), as I mentioned in the parenthesis, “with the expected glitches at the innser sides of the 45 degree corners

And no, that was not the result of running the plugin twice!! That was actually the horrific mess that the script was creating for that particular trace, after filleting the tracks!!

Anyway, turns out it was indeed the fact that the “chunks” of the trace were grossly out of order; I suspect that because I selected the straight segments and did “Fillet tracks”, perhaps those resulting newly-created arcs are all added at the end of whatever internal list of objects KiCAD keeps …

I had suspected that before, and had asked chatGPT to fix it, but it never worked — looks like chatGPT was just failing to fix the issue correctly; once I started adding dialog boxes to print the values, and then with baby steps asking it to make modifications, things ended up working.

There are additional modifications to the algorithm — the way it worked was: I was coming up with the algorithm and the mathematical/geometric operations needed, explained them to chatGPT, and it gave me Python code in response (complete updated scripts that I just copy-n-pasted into my text editor) … Actually, for one detail, it did not really follow my instructions … turns out, it actually saw through the real intent of what I was trying, and did an interpolation that I had not asked it to do! kudos, you damn chatGPT AI evil monster!!! :sweat_smile: !!

Anyway, in case someone could benefit from it, here is the latest version that seems to be working without glitches (it does have some limitations, plus it has all parameters hardcoded — no guarantees or warranty whatsoever, code provided for your entertainment only, if you choose to use it, use it entirely at your own risk, etc. etc.)

import pcbnew
import wx
import math
from decimal import Decimal, ROUND_HALF_UP

class ViaFenceGeneratorProper(pcbnew.ActionPlugin):
    def defaults(self):
        self.name = "Via Fence Generator Proper"
        self.category = "Via Tools"
        self.description = "Generates a proper via fence with adaptive segment length"
        self.show_toolbar_button = True
        self.icon_file_name = ""

    def Run(self):
        board = pcbnew.GetBoard()
        items = [t for t in board.GetTracks()
                 if t.IsSelected() and isinstance(t, (pcbnew.PCB_TRACK, pcbnew.PCB_ARC))]
        if not items:
            wx.MessageBox("No selected tracks or arcs found.")
            return

        def dist2(a, b):
            dx = a[0] - b[0]
            dy = a[1] - b[1]
            return dx * dx + dy * dy

        def segments_intersect(a1, a2, b1, b2):
            def ccw(p1, p2, p3):
                return (p3[1] - p1[1]) * (p2[0] - p1[0]) > (p2[1] - p1[1]) * (p3[0] - p1[0])
            return (ccw(a1, b1, b2) != ccw(a2, b1, b2)) and (ccw(a1, a2, b1) != ccw(a1, a2, b2))

        def determine_seg_len(via_spacing_val):
            rounded = Decimal(str(via_spacing_val)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
            digits = str(rounded).split(".")
            second_decimal = int(digits[1][1]) if len(digits[1]) > 1 else 0
            return 0.1 if second_decimal == 0 else 0.05

        # --- Parameters
        via_spacing = 0.75  # mm
        seg_len = determine_seg_len(via_spacing) * 1e6
        offset_nm = 0.9 * 1e6
        prune_dist_mm = 0.9
        thickness = int(0.05 * 1e6)  # not really used; this is for the User.Drawings lines
        layer = 17  # not really used; code to draw on User.Drawings is commented out
        via_drill_mm = 0.4
        via_diameter_mm = 0.6
        drill = int(via_drill_mm * 1e6)
        diameter = int(via_diameter_mm * 1e6)
        final_via_threshold = max(0.9 * via_spacing, via_diameter_mm)

        # --- Trace chaining
        tol_sq = 1_000_000
        endpoints = [(t.GetStart().x, t.GetStart().y) for t in items]
        endpoints += [(t.GetEnd().x, t.GetEnd().y) for t in items]

        def group_points(points):
            groups = []
            for pt in points:
                for g in groups:
                    if dist2(pt, g[0]) <= tol_sq:
                        g.append(pt)
                        break
                else:
                    groups.append([pt])
            return groups

        groups = group_points(endpoints)
        unique = [g[0] for g in groups if len(g) == 1]
        if len(unique) != 2:
            wx.MessageBox("❌ Could not identify exactly 2 trace endpoints.", "Error")
            return

        start_pt = unique[0]
        used_ids = set()
        ordered_chunks = []
        current_pt = None

        for t in items:
            a = (t.GetStart().x, t.GetStart().y)
            b = (t.GetEnd().x, t.GetEnd().y)
            if dist2(a, start_pt) <= tol_sq or dist2(b, start_pt) <= tol_sq:
                if dist2(b, start_pt) <= tol_sq:
                    ordered_chunks.append((t, b, a))
                    current_pt = a
                else:
                    ordered_chunks.append((t, a, b))
                    current_pt = b
                used_ids.add(id(t))
                break

        while True:
            found = False
            for t in items:
                if id(t) in used_ids:
                    continue
                a = (t.GetStart().x, t.GetStart().y)
                b = (t.GetEnd().x, t.GetEnd().y)
                if dist2(a, current_pt) <= tol_sq:
                    ordered_chunks.append((t, a, b))
                    current_pt = b
                    used_ids.add(id(t))
                    found = True
                    break
                elif dist2(b, current_pt) <= tol_sq:
                    ordered_chunks.append((t, b, a))
                    current_pt = a
                    used_ids.add(id(t))
                    found = True
                    break
            if not found:
                break

        # --- Net
        netcodes = board.GetNetsByName()
        net = None
        for name in ["GND", "GNDA", "GNDD"]:
            if name in netcodes:
                net = netcodes[name]
                break
        if not net:
            wx.MessageBox("No GND-like net found.")
            return

        # --- Sample centerline
        centerline = []
        for t, p0, p1 in ordered_chunks:
            if isinstance(t, pcbnew.PCB_ARC):
                c = t.GetCenter()
                cx, cy = c.x, c.y
                v1 = (p0[0] - cx, p0[1] - cy)
                v2 = (p1[0] - cx, p1[1] - cy)
                cross = v1[0] * v2[1] - v1[1] * v2[0]
                clockwise = cross < 0
                r = math.hypot(*v1)
                a0 = math.atan2(v1[1], v1[0])
                a1 = math.atan2(v2[1], v2[0])
                if clockwise:
                    if a0 < a1:
                        a0 += 2 * math.pi
                    sweep = a0 - a1
                else:
                    if a1 < a0:
                        a1 += 2 * math.pi
                    sweep = a1 - a0
                steps = max(1, int(abs(sweep * r) / seg_len))
                for i in range(steps + 1):
                    angle = a0 + (i / steps) * (-sweep if clockwise else sweep)
                    centerline.append((cx + r * math.cos(angle), cy + r * math.sin(angle)))
            else:
                dx = p1[0] - p0[0]
                dy = p1[1] - p0[1]
                length = math.hypot(dx, dy)
                steps = max(1, int(length / seg_len))
                for i in range(steps + 1):
                    f = i / steps
                    centerline.append((p0[0] + dx * f, p0[1] + dy * f))

        centerline = [pt for i, pt in enumerate(centerline) if i == 0 or pt != centerline[i - 1]]

        def build_offset_with_pruned_spoke_check(path, sign):
            result = []
            spokes = []
            arclens = []
            removals = set()
            acc_len = 0.0
            for i in range(len(path)):
                if i == 0:
                    x1, y1 = path[0]; x2, y2 = path[1]
                elif i == len(path) - 1:
                    x1, y1 = path[-2]; x2, y2 = path[-1]
                else:
                    x1, y1 = path[i - 1]; x2, y2 = path[i + 1]
                dx = x2 - x1
                dy = y2 - y1
                d = math.hypot(dx, dy)
                if d == 0:
                    continue
                nx, ny = -dy / d, dx / d
                cx, cy = path[i]
                px = cx + sign * nx * offset_nm
                py = cy + sign * ny * offset_nm
                new_spoke = ((cx, cy), (px, py))

                intersecting_indices = []
                for j in range(len(spokes)):
                    if acc_len - arclens[j] > prune_dist_mm * 1e6:
                        continue
                    if segments_intersect(spokes[j][0], spokes[j][1], new_spoke[0], new_spoke[1]):
                        intersecting_indices.append(j)

                if intersecting_indices:
                    removals.update(intersecting_indices)
                    continue

                if removals:
                    for j in sorted(removals, reverse=True):
                        del result[j]
                        del spokes[j]
                        del arclens[j]
                    removals = set()

                result.append((px, py))
                spokes.append(new_spoke)
                arclens.append(acc_len)

                if i > 0:
                    acc_len += math.hypot(path[i][0] - path[i - 1][0], path[i][1] - path[i - 1][1])

            return result

        TL = build_offset_with_pruned_spoke_check(centerline, +1)
        TR = build_offset_with_pruned_spoke_check(centerline, -1)

        def compute_spacing(path):
            total = sum(math.hypot(path[i][0] - path[i-1][0], path[i][1] - path[i-1][1])
                        for i in range(1, len(path)))
            count = max(1, int(total // (via_spacing * 1e6)))
            return total / count if count > 0 else total

        spacing_L = compute_spacing(TL)
        spacing_R = compute_spacing(TR)

        vias_to_group = []

        def draw_curve_with_vias(path, spacing_nm):
            cumulative = 0.0
            next_via_at = spacing_nm
            x0, y0 = path[0]
            via = pcbnew.PCB_VIA(board)
            via.SetPosition(pcbnew.VECTOR2I(int(x0), int(y0)))
            via.SetDrill(drill)
            via.SetWidth(diameter)
            via.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
            via.SetNet(net)
            board.Add(via)
            vias_to_group.append(via)

            for i in range(1, len(path)):
                x0, y0 = path[i-1]; x1, y1 = path[i]
                dx = x1 - x0; dy = y1 - y0
                d = math.hypot(dx, dy)

                cumulative += d
                while cumulative >= next_via_at:
                    t = (next_via_at - (cumulative - d)) / d
                    xv = x0 + dx * t
                    yv = y0 + dy * t
                    via = pcbnew.PCB_VIA(board)
                    via.SetPosition(pcbnew.VECTOR2I(int(xv), int(yv)))
                    via.SetDrill(drill)
                    via.SetWidth(diameter)
                    via.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
                    via.SetNet(net)
                    board.Add(via)
                    vias_to_group.append(via)
                    next_via_at += spacing_nm

            if cumulative - (next_via_at - spacing_nm) > final_via_threshold * 1e6:
                xf, yf = path[-1]
                via = pcbnew.PCB_VIA(board)
                via.SetPosition(pcbnew.VECTOR2I(int(xf), int(yf)))
                via.SetDrill(drill)
                via.SetWidth(diameter)
                via.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
                via.SetNet(net)
                board.Add(via)
                vias_to_group.append(via)

        draw_curve_with_vias(TL, spacing_L)
        draw_curve_with_vias(TR, spacing_R)

        if vias_to_group:
            group = pcbnew.PCB_GROUP(board)
            for obj in vias_to_group:
                group.AddItem(obj)
            board.Add(group)

        wx.MessageBox("Via fence with adaptive segmenting and optimized pruning done.")
        pcbnew.Refresh()

ViaFenceGeneratorProper().register()

Glad you got it working but trusting HallucinateGPT without checking it’s code is setting yourself up for … adventures :slight_smile:

1 Like

I read in the past few days that even the AI folks are starting to get nervous about ‘real’ content available for training as AI gets more of the pie.

One forum I’m on has expressly prohibited the posting of the output on the forum.

Hahaha, agreed … well, sort of …

I don’t have the (Python) skills to check its code per se … Actually, having a lot of experience in programming (but mostly C and C++), sure, there are the occasional fragments of Python where I can check and spot a mistake/bug …

But perhaps the key, in my case at least, being Python-impaired and thus “unable” to check its code, is that I know I have to test the programs, exercise all corner cases I can think of … (not like I have done that yet :upside_down_face: ).

I also went in baby-steps, and at most critical steps I asked it to generate drawings on the User.Drawings layer for sanity-check and verification of the math/geometry that I was instructing it to implement. That increases a bit my level of confidence in the correctness of its output…