style: apply Black formatting to changed files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,7 +123,8 @@ 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({
|
||||
symbols.append(
|
||||
{
|
||||
"reference": reference,
|
||||
"lib_id": lib_id,
|
||||
"x": x,
|
||||
@@ -131,11 +133,11 @@ def _parse_symbols(sexp_data: list) -> List[Dict[str, Any]]:
|
||||
"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([
|
||||
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"]}},
|
||||
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({
|
||||
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({
|
||||
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({
|
||||
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
|
||||
|
||||
@@ -2597,19 +2597,34 @@ class KiCADInterface:
|
||||
svg_output = None
|
||||
|
||||
try:
|
||||
cmd = [kicad_cli, "sch", "export", "svg", "--output", tmp_dir, schematic_path]
|
||||
cmd = [
|
||||
kicad_cli,
|
||||
"sch",
|
||||
"export",
|
||||
"svg",
|
||||
"--output",
|
||||
tmp_dir,
|
||||
schematic_path,
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
||||
|
||||
if result.returncode != 0:
|
||||
return {"success": False, "message": f"SVG export failed: {result.stderr}"}
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"SVG export failed: {result.stderr}",
|
||||
}
|
||||
|
||||
# kicad-cli names the file after the schematic
|
||||
svg_files = [f for f in os.listdir(tmp_dir) if f.endswith(".svg")]
|
||||
if not svg_files:
|
||||
return {"success": False, "message": "kicad-cli produced no SVG output"}
|
||||
return {
|
||||
"success": False,
|
||||
"message": "kicad-cli produced no SVG output",
|
||||
}
|
||||
svg_output = os.path.join(tmp_dir, svg_files[0])
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
tree = ET.parse(svg_output)
|
||||
root = tree.getroot()
|
||||
|
||||
@@ -2642,8 +2657,13 @@ class KiCADInterface:
|
||||
try:
|
||||
from cairosvg import svg2png
|
||||
except ImportError:
|
||||
return {"success": False, "message": "PNG export requires the 'cairosvg' package. Install it with: pip install cairosvg"}
|
||||
png_data = svg2png(url=cropped_svg_path, output_width=width, output_height=height)
|
||||
return {
|
||||
"success": False,
|
||||
"message": "PNG export requires the 'cairosvg' package. Install it with: pip install cairosvg",
|
||||
}
|
||||
png_data = svg2png(
|
||||
url=cropped_svg_path, output_width=width, output_height=height
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"imageData": base64.b64encode(png_data).decode("utf-8"),
|
||||
@@ -2651,15 +2671,16 @@ class KiCADInterface:
|
||||
}
|
||||
finally:
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_schematic_view_region: {e}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
|
||||
def _handle_find_overlapping_elements(self, params):
|
||||
"""Detect spatially overlapping symbols, wires, and labels"""
|
||||
logger.info("Finding overlapping elements in schematic")
|
||||
@@ -2681,6 +2702,7 @@ class KiCADInterface:
|
||||
except Exception as e:
|
||||
logger.error(f"Error finding overlapping elements: {e}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
@@ -2709,6 +2731,7 @@ class KiCADInterface:
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting elements in region: {e}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
@@ -2733,6 +2756,7 @@ class KiCADInterface:
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking wire collisions: {e}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,7 @@ except ImportError:
|
||||
|
||||
class _FakeSchematic:
|
||||
"""Minimal stand-in for skip.Schematic used in PinLocator cache."""
|
||||
|
||||
def __init__(self, path: str):
|
||||
self.path = path
|
||||
self.symbol = []
|
||||
|
||||
@@ -39,7 +39,6 @@ from commands.schematic_analysis import (
|
||||
find_wires_crossing_symbols,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -127,6 +126,7 @@ def _make_led_sexp(ref: str, x: float, y: float, rotation: float = 0) -> str:
|
||||
# Unit tests — geometry helpers
|
||||
# ===================================================================
|
||||
|
||||
|
||||
class TestGeometryHelpers:
|
||||
"""Test low-level geometry utilities."""
|
||||
|
||||
@@ -174,6 +174,7 @@ class TestGeometryHelpers:
|
||||
# Unit tests — S-expression parsers
|
||||
# ===================================================================
|
||||
|
||||
|
||||
class TestSexpParsers:
|
||||
"""Test S-expression parsing functions with synthetic data."""
|
||||
|
||||
@@ -223,6 +224,7 @@ class TestSexpParsers:
|
||||
# Unit tests — analysis functions with mocked PinLocator
|
||||
# ===================================================================
|
||||
|
||||
|
||||
class TestAABBOverlap:
|
||||
"""Test AABB overlap helper."""
|
||||
|
||||
@@ -254,14 +256,18 @@ class TestFindOverlappingElements:
|
||||
|
||||
def test_overlapping_symbols_detected(self):
|
||||
# Two resistors at nearly the same position — bboxes fully overlap
|
||||
extra = _make_resistor_sexp("R1", 100, 100) + _make_resistor_sexp("R2", 100.1, 100)
|
||||
extra = _make_resistor_sexp("R1", 100, 100) + _make_resistor_sexp(
|
||||
"R2", 100.1, 100
|
||||
)
|
||||
tmp = _make_temp_schematic(extra)
|
||||
result = find_overlapping_elements(tmp, tolerance=0.5)
|
||||
assert result["totalOverlaps"] >= 1
|
||||
assert len(result["overlappingSymbols"]) >= 1
|
||||
|
||||
def test_well_separated_symbols_not_flagged(self):
|
||||
extra = _make_resistor_sexp("R1", 100, 100) + _make_resistor_sexp("R2", 200, 200)
|
||||
extra = _make_resistor_sexp("R1", 100, 100) + _make_resistor_sexp(
|
||||
"R2", 200, 200
|
||||
)
|
||||
tmp = _make_temp_schematic(extra)
|
||||
result = find_overlapping_elements(tmp, tolerance=0.5)
|
||||
assert result["totalOverlaps"] == 0
|
||||
@@ -288,7 +294,9 @@ class TestFindOverlappingElements:
|
||||
"""
|
||||
# R1 at y=100, R2 at y=105 — pin spans [96.19, 103.81] and [101.19, 108.81]
|
||||
# These overlap in Y from 101.19 to 103.81
|
||||
extra = _make_resistor_sexp("R1", 100, 100) + _make_resistor_sexp("R2", 100, 105)
|
||||
extra = _make_resistor_sexp("R1", 100, 100) + _make_resistor_sexp(
|
||||
"R2", 100, 105
|
||||
)
|
||||
tmp = _make_temp_schematic(extra)
|
||||
result = find_overlapping_elements(tmp, tolerance=0.5)
|
||||
assert result["totalOverlaps"] >= 1, (
|
||||
@@ -302,7 +310,9 @@ class TestFindOverlappingElements:
|
||||
|
||||
R pins at y ±3.81, but different X positions far enough apart.
|
||||
"""
|
||||
extra = _make_resistor_sexp("R1", 100, 100) + _make_resistor_sexp("R2", 110, 100)
|
||||
extra = _make_resistor_sexp("R1", 100, 100) + _make_resistor_sexp(
|
||||
"R2", 110, 100
|
||||
)
|
||||
tmp = _make_temp_schematic(extra)
|
||||
result = find_overlapping_elements(tmp, tolerance=0.5)
|
||||
assert result["totalOverlaps"] == 0
|
||||
@@ -355,6 +365,7 @@ class TestComputeSymbolBbox:
|
||||
def test_returns_none_for_unknown_symbol(self):
|
||||
tmp = _make_temp_schematic()
|
||||
from commands.pin_locator import PinLocator
|
||||
|
||||
locator = PinLocator()
|
||||
result = compute_symbol_bbox(tmp, "NONEXISTENT", locator)
|
||||
assert result is None
|
||||
@@ -409,8 +420,7 @@ class TestIntegrationFindWiresCrossingSymbols:
|
||||
result = find_wires_crossing_symbols(tmp)
|
||||
# The wire must not be reported against the far-away R? at (200, 100)
|
||||
collisions_at_200 = [
|
||||
c for c in result
|
||||
if abs(c["component"]["position"]["x"] - 200) < 0.5
|
||||
c for c in result if abs(c["component"]["position"]["x"] - 200) < 0.5
|
||||
]
|
||||
assert len(collisions_at_200) == 0, (
|
||||
"Wire at x≈100 must not be flagged against the R? at x=200; "
|
||||
@@ -431,9 +441,9 @@ class TestIntegrationFindWiresCrossingSymbols:
|
||||
tmp = _make_temp_schematic(extra)
|
||||
result = find_wires_crossing_symbols(tmp)
|
||||
d1_crossings = [c for c in result if c["component"]["reference"] == "D1"]
|
||||
assert len(d1_crossings) >= 1, (
|
||||
"Wire starting at pin but passing through body must be detected"
|
||||
)
|
||||
assert (
|
||||
len(d1_crossings) >= 1
|
||||
), "Wire starting at pin but passing through body must be detected"
|
||||
|
||||
def test_wire_terminating_at_pin_from_outside(self):
|
||||
"""A wire that arrives at a pin from outside the component body
|
||||
@@ -448,17 +458,17 @@ class TestIntegrationFindWiresCrossingSymbols:
|
||||
tmp = _make_temp_schematic(extra)
|
||||
result = find_wires_crossing_symbols(tmp)
|
||||
d1_crossings = [c for c in result if c["component"]["reference"] == "D1"]
|
||||
assert len(d1_crossings) == 0, (
|
||||
"Wire terminating at pin from outside should not be flagged"
|
||||
)
|
||||
assert (
|
||||
len(d1_crossings) == 0
|
||||
), "Wire terminating at pin from outside should not be flagged"
|
||||
|
||||
def test_wire_shorts_component_pins_detected_as_collision(self):
|
||||
"""Regression: a wire connecting pin1→pin2 of the same component
|
||||
must be reported even though both endpoints land on pins."""
|
||||
r_sexp = _make_resistor_sexp("R_short", 100.0, 100.0)
|
||||
wire_sexp = (
|
||||
'(wire (pts (xy 100 103.81) (xy 100 96.19))\n'
|
||||
' (stroke (width 0) (type default))\n'
|
||||
"(wire (pts (xy 100 103.81) (xy 100 96.19))\n"
|
||||
" (stroke (width 0) (type default))\n"
|
||||
' (uuid "aaaaaaaa-0000-0000-0000-000000000001"))'
|
||||
)
|
||||
sch = _make_temp_schematic(r_sexp + "\n" + wire_sexp)
|
||||
@@ -511,6 +521,7 @@ class TestIntegrationGetElementsInRegion:
|
||||
# Unit tests — _check_wire_overlap
|
||||
# ===================================================================
|
||||
|
||||
|
||||
class TestCheckWireOverlap:
|
||||
"""Test wire overlap detection for horizontal, vertical, and diagonal cases."""
|
||||
|
||||
@@ -613,6 +624,7 @@ class TestIntegrationDiagonalWireOverlap:
|
||||
# Unit tests — _extract_lib_symbols
|
||||
# ===================================================================
|
||||
|
||||
|
||||
class TestExtractLibSymbols:
|
||||
"""Test _extract_lib_symbols helper."""
|
||||
|
||||
@@ -684,6 +696,7 @@ class TestExtractLibSymbols:
|
||||
# Unit tests — _parse_lib_symbol_graphics
|
||||
# ===================================================================
|
||||
|
||||
|
||||
class TestParseLibSymbolGraphics:
|
||||
"""Test graphics extraction from lib_symbol definitions."""
|
||||
|
||||
@@ -745,6 +758,7 @@ class TestParseLibSymbolGraphics:
|
||||
# Unit tests — _transform_local_point
|
||||
# ===================================================================
|
||||
|
||||
|
||||
class TestTransformLocalPoint:
|
||||
"""Test local→absolute coordinate transform."""
|
||||
|
||||
@@ -776,6 +790,7 @@ class TestTransformLocalPoint:
|
||||
# Unit tests — _compute_symbol_bbox_direct with graphics
|
||||
# ===================================================================
|
||||
|
||||
|
||||
class TestComputeSymbolBboxWithGraphics:
|
||||
"""Test that bounding box computation uses graphics points when available."""
|
||||
|
||||
@@ -783,14 +798,36 @@ class TestComputeSymbolBboxWithGraphics:
|
||||
"""Device:R rectangle is (-1.016, -2.54) to (1.016, 2.54) in local coords.
|
||||
Pins at (0, ±3.81). Placed at (100, 100) with no rotation.
|
||||
Bbox should span from pin-to-pin in Y and use rectangle width in X."""
|
||||
sym = {"x": 100.0, "y": 100.0, "rotation": 0, "mirror_x": False, "mirror_y": False}
|
||||
sym = {
|
||||
"x": 100.0,
|
||||
"y": 100.0,
|
||||
"rotation": 0,
|
||||
"mirror_x": False,
|
||||
"mirror_y": False,
|
||||
}
|
||||
pin_defs = {
|
||||
"1": {"x": 0, "y": 3.81, "angle": 270, "length": 1.27, "name": "~", "type": "passive"},
|
||||
"2": {"x": 0, "y": -3.81, "angle": 90, "length": 1.27, "name": "~", "type": "passive"},
|
||||
"1": {
|
||||
"x": 0,
|
||||
"y": 3.81,
|
||||
"angle": 270,
|
||||
"length": 1.27,
|
||||
"name": "~",
|
||||
"type": "passive",
|
||||
},
|
||||
"2": {
|
||||
"x": 0,
|
||||
"y": -3.81,
|
||||
"angle": 90,
|
||||
"length": 1.27,
|
||||
"name": "~",
|
||||
"type": "passive",
|
||||
},
|
||||
}
|
||||
graphics_points = [(-1.016, -2.54), (1.016, 2.54)]
|
||||
|
||||
bbox = _compute_symbol_bbox_direct(sym, pin_defs, graphics_points=graphics_points)
|
||||
bbox = _compute_symbol_bbox_direct(
|
||||
sym, pin_defs, graphics_points=graphics_points
|
||||
)
|
||||
assert bbox is not None
|
||||
min_x, min_y, max_x, max_y = bbox
|
||||
# X should come from rectangle: 100 ± 1.016
|
||||
@@ -802,10 +839,30 @@ class TestComputeSymbolBboxWithGraphics:
|
||||
|
||||
def test_fallback_without_graphics(self):
|
||||
"""Without graphics_points, should use the old degenerate expansion."""
|
||||
sym = {"x": 100.0, "y": 100.0, "rotation": 0, "mirror_x": False, "mirror_y": False}
|
||||
sym = {
|
||||
"x": 100.0,
|
||||
"y": 100.0,
|
||||
"rotation": 0,
|
||||
"mirror_x": False,
|
||||
"mirror_y": False,
|
||||
}
|
||||
pin_defs = {
|
||||
"1": {"x": 0, "y": 3.81, "angle": 270, "length": 1.27, "name": "~", "type": "passive"},
|
||||
"2": {"x": 0, "y": -3.81, "angle": 90, "length": 1.27, "name": "~", "type": "passive"},
|
||||
"1": {
|
||||
"x": 0,
|
||||
"y": 3.81,
|
||||
"angle": 270,
|
||||
"length": 1.27,
|
||||
"name": "~",
|
||||
"type": "passive",
|
||||
},
|
||||
"2": {
|
||||
"x": 0,
|
||||
"y": -3.81,
|
||||
"angle": 90,
|
||||
"length": 1.27,
|
||||
"name": "~",
|
||||
"type": "passive",
|
||||
},
|
||||
}
|
||||
|
||||
bbox = _compute_symbol_bbox_direct(sym, pin_defs)
|
||||
@@ -817,15 +874,37 @@ class TestComputeSymbolBboxWithGraphics:
|
||||
|
||||
def test_rotated_symbol_graphics(self):
|
||||
"""Graphics points should be rotated along with the symbol."""
|
||||
sym = {"x": 100.0, "y": 100.0, "rotation": 90, "mirror_x": False, "mirror_y": False}
|
||||
sym = {
|
||||
"x": 100.0,
|
||||
"y": 100.0,
|
||||
"rotation": 90,
|
||||
"mirror_x": False,
|
||||
"mirror_y": False,
|
||||
}
|
||||
pin_defs = {
|
||||
"1": {"x": 0, "y": 3.81, "angle": 270, "length": 1.27, "name": "~", "type": "passive"},
|
||||
"2": {"x": 0, "y": -3.81, "angle": 90, "length": 1.27, "name": "~", "type": "passive"},
|
||||
"1": {
|
||||
"x": 0,
|
||||
"y": 3.81,
|
||||
"angle": 270,
|
||||
"length": 1.27,
|
||||
"name": "~",
|
||||
"type": "passive",
|
||||
},
|
||||
"2": {
|
||||
"x": 0,
|
||||
"y": -3.81,
|
||||
"angle": 90,
|
||||
"length": 1.27,
|
||||
"name": "~",
|
||||
"type": "passive",
|
||||
},
|
||||
}
|
||||
# Rectangle corners in local coords
|
||||
graphics_points = [(-1.016, -2.54), (1.016, 2.54)]
|
||||
|
||||
bbox = _compute_symbol_bbox_direct(sym, pin_defs, graphics_points=graphics_points)
|
||||
bbox = _compute_symbol_bbox_direct(
|
||||
sym, pin_defs, graphics_points=graphics_points
|
||||
)
|
||||
assert bbox is not None
|
||||
min_x, min_y, max_x, max_y = bbox
|
||||
# After 90° rotation, X and Y swap roles
|
||||
@@ -856,7 +935,9 @@ class TestIntegrationGraphicsBbox:
|
||||
|
||||
assert len(graphics_points) >= 2, "Should have extracted rectangle points"
|
||||
|
||||
bbox = _compute_symbol_bbox_direct(r1, pin_defs, graphics_points=graphics_points)
|
||||
bbox = _compute_symbol_bbox_direct(
|
||||
r1, pin_defs, graphics_points=graphics_points
|
||||
)
|
||||
assert bbox is not None
|
||||
min_x, min_y, max_x, max_y = bbox
|
||||
# Rectangle is ±1.016 in X, NOT ±1.5 from degenerate expansion
|
||||
|
||||
Reference in New Issue
Block a user