style: apply Black formatting to changed files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Eugene Mikhantyev
2026-03-15 23:17:25 +00:00
parent be11948a44
commit 59bd4c4acf
5 changed files with 731 additions and 610 deletions

View File

@@ -22,6 +22,7 @@ logger = logging.getLogger("kicad_interface")
# S-expression parsing helpers
# ---------------------------------------------------------------------------
def _load_sexp(schematic_path: Path) -> list:
"""Load schematic file and return parsed S-expression data."""
with open(schematic_path, "r", encoding="utf-8") as f:
@@ -122,20 +123,21 @@ def _parse_symbols(sexp_data: list) -> List[Dict[str, Any]]:
reference = str(sub[2]).strip('"')
is_power = reference.startswith("#PWR") or reference.startswith("#FLG")
symbols.append({
"reference": reference,
"lib_id": lib_id,
"x": x,
"y": y,
"rotation": rotation,
"mirror_x": mirror_x,
"mirror_y": mirror_y,
"is_power": is_power,
})
symbols.append(
{
"reference": reference,
"lib_id": lib_id,
"x": x,
"y": y,
"rotation": rotation,
"mirror_x": mirror_x,
"mirror_y": mirror_y,
"is_power": is_power,
}
)
return symbols
def _parse_lib_symbol_graphics(symbol_def: list) -> List[Tuple[float, float]]:
"""
Parse graphical body elements from a lib_symbol definition and return
@@ -167,22 +169,36 @@ def _parse_lib_symbol_graphics(symbol_def: list) -> List[Tuple[float, float]]:
for sub in sexp[1:]:
if isinstance(sub, list) and len(sub) > 0 and sub[0] == Symbol("pts"):
for pt in sub[1:]:
if isinstance(pt, list) and len(pt) >= 3 and pt[0] == Symbol("xy"):
if (
isinstance(pt, list)
and len(pt) >= 3
and pt[0] == Symbol("xy")
):
points.append((float(pt[1]), float(pt[2])))
elif tag == Symbol("circle"):
# (circle (center x y) (radius r) ...)
cx, cy, r = 0.0, 0.0, 0.0
for sub in sexp[1:]:
if isinstance(sub, list) and len(sub) >= 3 and sub[0] == Symbol("center"):
if (
isinstance(sub, list)
and len(sub) >= 3
and sub[0] == Symbol("center")
):
cx, cy = float(sub[1]), float(sub[2])
elif isinstance(sub, list) and len(sub) >= 2 and sub[0] == Symbol("radius"):
elif (
isinstance(sub, list)
and len(sub) >= 2
and sub[0] == Symbol("radius")
):
r = float(sub[1])
if r > 0:
points.extend([
(cx - r, cy - r),
(cx + r, cy + r),
])
points.extend(
[
(cx - r, cy - r),
(cx + r, cy + r),
]
)
elif tag == Symbol("arc"):
# (arc (start x y) (mid x y) (end x y) ...)
@@ -196,7 +212,11 @@ def _parse_lib_symbol_graphics(symbol_def: list) -> List[Tuple[float, float]]:
for sub in sexp[1:]:
if isinstance(sub, list) and len(sub) > 0 and sub[0] == Symbol("pts"):
for pt in sub[1:]:
if isinstance(pt, list) and len(pt) >= 3 and pt[0] == Symbol("xy"):
if (
isinstance(pt, list)
and len(pt) >= 3
and pt[0] == Symbol("xy")
):
points.append((float(pt[1]), float(pt[2])))
else:
@@ -223,8 +243,11 @@ def _extract_lib_symbols(sexp_data: list) -> Dict[str, Dict]:
"""
lib_symbols_section = None
for item in sexp_data:
if (isinstance(item, list) and len(item) > 0
and item[0] == Symbol("lib_symbols")):
if (
isinstance(item, list)
and len(item) > 0
and item[0] == Symbol("lib_symbols")
):
lib_symbols_section = item
break
@@ -233,8 +256,7 @@ def _extract_lib_symbols(sexp_data: list) -> Dict[str, Dict]:
result: Dict[str, Dict] = {}
for item in lib_symbols_section[1:]:
if (isinstance(item, list) and len(item) > 1
and item[0] == Symbol("symbol")):
if isinstance(item, list) and len(item) > 1 and item[0] == Symbol("symbol"):
symbol_name = str(item[1]).strip('"')
result[symbol_name] = {
"pins": PinLocator.parse_symbol_definition(item),
@@ -247,6 +269,7 @@ def _extract_lib_symbols(sexp_data: list) -> Dict[str, Dict]:
# Geometry helpers
# ---------------------------------------------------------------------------
def compute_symbol_bbox(
schematic_path: Path,
reference: str,
@@ -266,8 +289,14 @@ def compute_symbol_bbox(
def _line_segment_intersects_aabb(
x1: float, y1: float, x2: float, y2: float,
box_min_x: float, box_min_y: float, box_max_x: float, box_max_y: float,
x1: float,
y1: float,
x2: float,
y2: float,
box_min_x: float,
box_min_y: float,
box_max_x: float,
box_max_y: float,
) -> bool:
"""
Test whether line segment (x1,y1)→(x2,y2) intersects an axis-aligned bounding box.
@@ -301,8 +330,12 @@ def _line_segment_intersects_aabb(
def _point_in_rect(
px: float, py: float,
min_x: float, min_y: float, max_x: float, max_y: float,
px: float,
py: float,
min_x: float,
min_y: float,
max_x: float,
max_y: float,
) -> bool:
"""Check if a point is within a rectangle."""
return min_x <= px <= max_x and min_y <= py <= max_y
@@ -325,10 +358,13 @@ def _aabb_overlap(
def _transform_local_point(
lx: float, ly: float,
sym_x: float, sym_y: float,
lx: float,
ly: float,
sym_x: float,
sym_y: float,
rotation: float,
mirror_x: bool, mirror_y: bool,
mirror_x: bool,
mirror_y: bool,
) -> Tuple[float, float]:
"""
Transform a point from local symbol coordinates to absolute schematic
@@ -424,11 +460,11 @@ def _compute_symbol_bbox_direct(
return (min_x, min_y, max_x, max_y)
# ---------------------------------------------------------------------------
# Tool 3: find_overlapping_elements
# ---------------------------------------------------------------------------
def find_overlapping_elements(
schematic_path: Path, tolerance: float = 0.5
) -> Dict[str, Any]:
@@ -453,7 +489,11 @@ def find_overlapping_elements(
lib_defs = _extract_lib_symbols(sexp_data)
# --- Symbol-symbol overlap using bounding-box intersection (O(n²)) ---
non_template_symbols = [s for s in symbols if not s["reference"].startswith("_TEMPLATE") and s["reference"]]
non_template_symbols = [
s
for s in symbols
if not s["reference"].startswith("_TEMPLATE") and s["reference"]
]
# Pre-compute bounding boxes for all non-template symbols
symbol_bboxes = []
@@ -463,7 +503,9 @@ def find_overlapping_elements(
graphics_points = lib_data.get("graphics_points", [])
bbox = None
if pin_defs:
bbox = _compute_symbol_bbox_direct(sym, pin_defs, graphics_points=graphics_points)
bbox = _compute_symbol_bbox_direct(
sym, pin_defs, graphics_points=graphics_points
)
symbol_bboxes.append((sym, bbox))
for i in range(len(symbol_bboxes)):
@@ -482,10 +524,16 @@ def find_overlapping_elements(
if overlap_detected:
entry = {
"element1": {"reference": s1["reference"], "libId": s1["lib_id"],
"position": {"x": s1["x"], "y": s1["y"]}},
"element2": {"reference": s2["reference"], "libId": s2["lib_id"],
"position": {"x": s2["x"], "y": s2["y"]}},
"element1": {
"reference": s1["reference"],
"libId": s1["lib_id"],
"position": {"x": s1["x"], "y": s1["y"]},
},
"element2": {
"reference": s2["reference"],
"libId": s2["lib_id"],
"position": {"x": s2["x"], "y": s2["y"]},
},
"distance": round(dist, 4),
}
# Flag power symbol pairs specifically
@@ -502,13 +550,21 @@ def find_overlapping_elements(
l2 = labels[j]
dist = _distance((l1["x"], l1["y"]), (l2["x"], l2["y"]))
if dist < tolerance:
overlapping_labels.append({
"element1": {"name": l1["name"], "type": l1["type"],
"position": {"x": l1["x"], "y": l1["y"]}},
"element2": {"name": l2["name"], "type": l2["type"],
"position": {"x": l2["x"], "y": l2["y"]}},
"distance": round(dist, 4),
})
overlapping_labels.append(
{
"element1": {
"name": l1["name"],
"type": l1["type"],
"position": {"x": l1["x"], "y": l1["y"]},
},
"element2": {
"name": l2["name"],
"type": l2["type"],
"position": {"x": l2["x"], "y": l2["y"]},
},
"distance": round(dist, 4),
}
)
# --- Wire-wire collinear overlap ---
for i in range(len(wires)):
@@ -574,8 +630,14 @@ def _check_wire_overlap(
min2, max2 = min(proj_s2, proj_e2), max(proj_s2, proj_e2)
if min1 < max2 and min2 < max1:
return {
"wire1": {"start": {"x": s1[0], "y": s1[1]}, "end": {"x": e1[0], "y": e1[1]}},
"wire2": {"start": {"x": s2[0], "y": s2[1]}, "end": {"x": e2[0], "y": e2[1]}},
"wire1": {
"start": {"x": s1[0], "y": s1[1]},
"end": {"x": e1[0], "y": e1[1]},
},
"wire2": {
"start": {"x": s2[0], "y": s2[1]},
"end": {"x": e2[0], "y": e2[1]},
},
"type": "collinear_overlap",
}
@@ -586,9 +648,13 @@ def _check_wire_overlap(
# Tool 4: get_elements_in_region
# ---------------------------------------------------------------------------
def get_elements_in_region(
schematic_path: Path,
x1: float, y1: float, x2: float, y2: float,
x1: float,
y1: float,
x2: float,
y2: float,
) -> Dict[str, Any]:
"""
List all wires, labels, and symbols within a rectangular region.
@@ -637,23 +703,31 @@ def get_elements_in_region(
region_wires = []
for w in wires:
s, e = w["start"], w["end"]
if (_point_in_rect(s[0], s[1], min_x, min_y, max_x, max_y) or
_point_in_rect(e[0], e[1], min_x, min_y, max_x, max_y) or
_line_segment_intersects_aabb(s[0], s[1], e[0], e[1], min_x, min_y, max_x, max_y)):
region_wires.append({
"start": {"x": s[0], "y": s[1]},
"end": {"x": e[0], "y": e[1]},
})
if (
_point_in_rect(s[0], s[1], min_x, min_y, max_x, max_y)
or _point_in_rect(e[0], e[1], min_x, min_y, max_x, max_y)
or _line_segment_intersects_aabb(
s[0], s[1], e[0], e[1], min_x, min_y, max_x, max_y
)
):
region_wires.append(
{
"start": {"x": s[0], "y": s[1]},
"end": {"x": e[0], "y": e[1]},
}
)
# Labels: include if position is within bounds
region_labels = []
for lbl in labels:
if _point_in_rect(lbl["x"], lbl["y"], min_x, min_y, max_x, max_y):
region_labels.append({
"name": lbl["name"],
"type": lbl["type"],
"position": {"x": lbl["x"], "y": lbl["y"]},
})
region_labels.append(
{
"name": lbl["name"],
"type": lbl["type"],
"position": {"x": lbl["x"], "y": lbl["y"]},
}
)
return {
"symbols": region_symbols,
@@ -671,6 +745,7 @@ def get_elements_in_region(
# Tool 5: check_wire_collisions
# ---------------------------------------------------------------------------
def _compute_pin_positions_direct(
sym: Dict[str, Any], pin_defs: Dict[str, Dict]
) -> Dict[str, List[float]]:
@@ -748,7 +823,9 @@ def find_wires_crossing_symbols(schematic_path: Path) -> List[Dict[str, Any]]:
continue
graphics_points = lib_data.get("graphics_points", [])
bbox = _compute_symbol_bbox_direct(sym, pin_defs, margin=margin, graphics_points=graphics_points)
bbox = _compute_symbol_bbox_direct(
sym, pin_defs, margin=margin, graphics_points=graphics_points
)
if bbox is None:
continue
@@ -757,11 +834,13 @@ def find_wires_crossing_symbols(schematic_path: Path) -> List[Dict[str, Any]]:
for pos in pin_positions.values():
pin_set.add((pos[0], pos[1]))
symbol_data.append({
"sym": sym,
"bbox": bbox,
"pin_set": pin_set,
})
symbol_data.append(
{
"sym": sym,
"bbox": bbox,
"pin_set": pin_set,
}
)
# Test each wire against each symbol bbox
for w in wires:
@@ -810,7 +889,8 @@ def find_wires_crossing_symbols(schematic_path: Path) -> List[Dict[str, Any]]:
continue # Wire terminates at pin from outside
sym = sd["sym"]
collisions.append({
collisions.append(
{
"wire": {
"start": {"x": sx, "y": sy},
"end": {"x": ex, "y": ey},
@@ -821,6 +901,7 @@ def find_wires_crossing_symbols(schematic_path: Path) -> List[Dict[str, Any]]:
"position": {"x": sym["x"], "y": sym["y"]},
},
"intersectionType": "passes_through",
})
}
)
return collisions