AnasSarkiz/muse-voice-remote

Muse Voice Remote — bare ESP32-S3R8, 155 TOP JLCPCB parts with real supplier CAD, native 3D assembly and GLB exports. Revised 17 mm case allocation; 0.30 mm via drill /0.45 mm pad. Checked prototype PCB; mechanical, RF and MUSE qualification pending.

Version
1.3.2
License
unset
Stars
0

scripts/route-precise-branch.py

#!/usr/bin/env python3
"""Route an existing net through exact free-space geometry, then check KiCad.

Use a constrained triangulation when a finite routing grid misses narrow but
connected passages. This adds only board copper; supplier footprints and the
minimum width/clearance rules remain unchanged.
"""
import argparse
import heapq
import math
import os
from pathlib import Path

helper = Path(__file__).with_name("finish-unrouted.py")
api = {"__file__": str(helper)}
exec(helper.read_text().split("\n# A FreeRouting pattern")[0], api)
from shapely import constrained_delaunay_triangles
from shapely.geometry import Point, LineString, box

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--net", required=True)
parser.add_argument("--layer", required=True, choices=["top", "inner1", "inner2", "bottom"])
parser.add_argument("--start", type=float, nargs=2)
parser.add_argument("--end", type=float, nargs=2)
parser.add_argument("--start-pad", nargs=2, metavar=("REFERENCE","PIN"))
parser.add_argument("--end-pad", nargs=2, metavar=("REFERENCE","PIN"))
parser.add_argument("--width", type=float, default=.15)
parser.add_argument("--reinforce", action="store_true", help="Allow a checked parallel supply connection without increasing unconnected pairs")
args = parser.parse_args()
pcbnew = api["pcbnew"]
path = api["path"]
before = path.read_bytes()
old = api["check"]()
if any(v["type"] not in {"track_dangling", "via_dangling"} for v in old["violations"]):
    raise RuntimeError("Repair actual geometry violations before precision routing")
board = pcbnew.LoadBoard(str(path))
if args.width < .15: raise ValueError("Width cannot be below the fabrication minimum")
api["width"] = args.width
def pad_position(spec):
    footprint=next(f for f in board.GetFootprints() if f.GetReference()==spec[0])
    pad=next(p for p in footprint.Pads() if p.GetNumber()==spec[1])
    if pad.GetNetname()!=args.net: raise ValueError("Selected pad has a different net")
    return api["xy"](pad.GetPosition())
args.start = pad_position(args.start_pad) if args.start_pad else args.start
args.end = pad_position(args.end_pad) if args.end_pad else args.end
if args.start is None or args.end is None: parser.error("Specify both endpoints as coordinates or pads")
layer_index = ["top", "inner1", "inner2", "bottom"].index(args.layer)
layer = api["layers"][layer_index]
net_code = next(n.GetNetCode() for n in board.GetNetsByNetcode().values() if n.GetNetname() == args.net)
occupied, _ = api["obstacles"](board, args.net)
free = box(-32.5,-23.5,32.5,23.5).difference(occupied[layer_index].context)
polygons = list(free.geoms) if free.geom_type=="MultiPolygon" else [free]
region = next((p for p in polygons if p.covers(Point(args.start)) and p.covers(Point(args.end))), None)
if region is None:
    raise RuntimeError("Endpoints do not share a connected free-space region")
triangles = list(constrained_delaunay_triangles(region).geoms)
print(f"Triangulated {len(triangles)} free-space cells", flush=True)
starts = [i for i,t in enumerate(triangles) if t.covers(Point(args.start))]
goals = {i for i,t in enumerate(triangles) if t.covers(Point(args.end))}
if not starts or not goals:
    raise RuntimeError("Triangulation does not contain an endpoint")
centres = [(t.centroid.x,t.centroid.y) for t in triangles]
edges = {}
neighbors = [[] for _ in triangles]
for i,t in enumerate(triangles):
    vertices = list(t.exterior.coords)[:-1]
    for a,b in zip(vertices,vertices[1:]+vertices[:1]):
        key = tuple(sorted((a,b)))
        if key in edges:
            j = edges[key]
            midpoint = ((a[0]+b[0])/2,(a[1]+b[1])/2)
            distance = math.dist(centres[i],midpoint)+math.dist(midpoint,centres[j])
            neighbors[i].append((j,midpoint,distance))
            neighbors[j].append((i,midpoint,distance))
        else:
            edges[key] = i
cost = {i:math.dist(args.start,centres[i]) for i in starts}
queue = [(cost[i],i) for i in starts]
heapq.heapify(queue)
previous = {}
target = None
while queue:
    score,i = heapq.heappop(queue)
    if score>cost[i]:continue
    if i in goals:
        target=i;break
    for j,midpoint,length in neighbors[i]:
        candidate = score+length
        if candidate<cost.get(j,math.inf):
            cost[j]=candidate;previous[j]=(i,midpoint)
            heapq.heappush(queue,(candidate,j))
if target is None:
    raise RuntimeError("No path through the constrained triangulation")
chain=[target]
while chain[-1] in previous:chain.append(previous[chain[-1]][0])
chain.reverse()
points=[tuple(args.start),centres[chain[0]]]
for i,j in zip(chain,chain[1:]):
    points.extend([previous[j][1],centres[j]])
points.append(tuple(args.end))
# Retain only necessary turns using visibility within the exact free region.
simple=[points[0]]
position=0
while position<len(points)-1:
    candidate=len(points)-1
    while candidate>position+1 and not region.covers(LineString([points[position],points[candidate]])):
        candidate-=1
    simple.append(points[candidate]);position=candidate
for a,b in zip(simple,simple[1:]):
    if math.dist(a,b)<.000001:continue
    track=pcbnew.PCB_TRACK(board)
    track.SetStart(api["vector"](a));track.SetEnd(api["vector"](b))
    track.SetLayer(layer);track.SetWidth(pcbnew.FromMM(args.width));track.SetNetCode(net_code)
    board.Add(track)
pcbnew.SaveBoard(str(path),board)
after=api["check"]()
if (len(after["unconnected_items"])>len(old["unconnected_items"]) if args.reinforce else len(after["unconnected_items"])>=len(old["unconnected_items"])) or any(v["type"] not in {"track_dangling", "via_dangling"} for v in after["violations"]):
    path.write_bytes(before)
    raise RuntimeError("Independent KiCad check rejected the precision path; restored CAD")
print(f"Accepted {len(simple)-1} exact-geometry segments; {len(after['unconnected_items'])} unconnected pairs remain",flush=True)
os._exit(0)