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
|
# S-expression parsing helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _load_sexp(schematic_path: Path) -> list:
|
def _load_sexp(schematic_path: Path) -> list:
|
||||||
"""Load schematic file and return parsed S-expression data."""
|
"""Load schematic file and return parsed S-expression data."""
|
||||||
with open(schematic_path, "r", encoding="utf-8") as f:
|
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('"')
|
reference = str(sub[2]).strip('"')
|
||||||
|
|
||||||
is_power = reference.startswith("#PWR") or reference.startswith("#FLG")
|
is_power = reference.startswith("#PWR") or reference.startswith("#FLG")
|
||||||
symbols.append({
|
symbols.append(
|
||||||
"reference": reference,
|
{
|
||||||
"lib_id": lib_id,
|
"reference": reference,
|
||||||
"x": x,
|
"lib_id": lib_id,
|
||||||
"y": y,
|
"x": x,
|
||||||
"rotation": rotation,
|
"y": y,
|
||||||
"mirror_x": mirror_x,
|
"rotation": rotation,
|
||||||
"mirror_y": mirror_y,
|
"mirror_x": mirror_x,
|
||||||
"is_power": is_power,
|
"mirror_y": mirror_y,
|
||||||
})
|
"is_power": is_power,
|
||||||
|
}
|
||||||
|
)
|
||||||
return symbols
|
return symbols
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_lib_symbol_graphics(symbol_def: list) -> List[Tuple[float, float]]:
|
def _parse_lib_symbol_graphics(symbol_def: list) -> List[Tuple[float, float]]:
|
||||||
"""
|
"""
|
||||||
Parse graphical body elements from a lib_symbol definition and return
|
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:]:
|
for sub in sexp[1:]:
|
||||||
if isinstance(sub, list) and len(sub) > 0 and sub[0] == Symbol("pts"):
|
if isinstance(sub, list) and len(sub) > 0 and sub[0] == Symbol("pts"):
|
||||||
for pt in sub[1:]:
|
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])))
|
points.append((float(pt[1]), float(pt[2])))
|
||||||
|
|
||||||
elif tag == Symbol("circle"):
|
elif tag == Symbol("circle"):
|
||||||
# (circle (center x y) (radius r) ...)
|
# (circle (center x y) (radius r) ...)
|
||||||
cx, cy, r = 0.0, 0.0, 0.0
|
cx, cy, r = 0.0, 0.0, 0.0
|
||||||
for sub in sexp[1:]:
|
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])
|
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])
|
r = float(sub[1])
|
||||||
if r > 0:
|
if r > 0:
|
||||||
points.extend([
|
points.extend(
|
||||||
(cx - r, cy - r),
|
[
|
||||||
(cx + r, cy + r),
|
(cx - r, cy - r),
|
||||||
])
|
(cx + r, cy + r),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
elif tag == Symbol("arc"):
|
elif tag == Symbol("arc"):
|
||||||
# (arc (start x y) (mid x y) (end x y) ...)
|
# (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:]:
|
for sub in sexp[1:]:
|
||||||
if isinstance(sub, list) and len(sub) > 0 and sub[0] == Symbol("pts"):
|
if isinstance(sub, list) and len(sub) > 0 and sub[0] == Symbol("pts"):
|
||||||
for pt in sub[1:]:
|
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])))
|
points.append((float(pt[1]), float(pt[2])))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -223,8 +243,11 @@ def _extract_lib_symbols(sexp_data: list) -> Dict[str, Dict]:
|
|||||||
"""
|
"""
|
||||||
lib_symbols_section = None
|
lib_symbols_section = None
|
||||||
for item in sexp_data:
|
for item in sexp_data:
|
||||||
if (isinstance(item, list) and len(item) > 0
|
if (
|
||||||
and item[0] == Symbol("lib_symbols")):
|
isinstance(item, list)
|
||||||
|
and len(item) > 0
|
||||||
|
and item[0] == Symbol("lib_symbols")
|
||||||
|
):
|
||||||
lib_symbols_section = item
|
lib_symbols_section = item
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -233,8 +256,7 @@ def _extract_lib_symbols(sexp_data: list) -> Dict[str, Dict]:
|
|||||||
|
|
||||||
result: Dict[str, Dict] = {}
|
result: Dict[str, Dict] = {}
|
||||||
for item in lib_symbols_section[1:]:
|
for item in lib_symbols_section[1:]:
|
||||||
if (isinstance(item, list) and len(item) > 1
|
if isinstance(item, list) and len(item) > 1 and item[0] == Symbol("symbol"):
|
||||||
and item[0] == Symbol("symbol")):
|
|
||||||
symbol_name = str(item[1]).strip('"')
|
symbol_name = str(item[1]).strip('"')
|
||||||
result[symbol_name] = {
|
result[symbol_name] = {
|
||||||
"pins": PinLocator.parse_symbol_definition(item),
|
"pins": PinLocator.parse_symbol_definition(item),
|
||||||
@@ -247,6 +269,7 @@ def _extract_lib_symbols(sexp_data: list) -> Dict[str, Dict]:
|
|||||||
# Geometry helpers
|
# Geometry helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def compute_symbol_bbox(
|
def compute_symbol_bbox(
|
||||||
schematic_path: Path,
|
schematic_path: Path,
|
||||||
reference: str,
|
reference: str,
|
||||||
@@ -266,8 +289,14 @@ def compute_symbol_bbox(
|
|||||||
|
|
||||||
|
|
||||||
def _line_segment_intersects_aabb(
|
def _line_segment_intersects_aabb(
|
||||||
x1: float, y1: float, x2: float, y2: float,
|
x1: float,
|
||||||
box_min_x: float, box_min_y: float, box_max_x: float, box_max_y: float,
|
y1: float,
|
||||||
|
x2: float,
|
||||||
|
y2: float,
|
||||||
|
box_min_x: float,
|
||||||
|
box_min_y: float,
|
||||||
|
box_max_x: float,
|
||||||
|
box_max_y: float,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Test whether line segment (x1,y1)→(x2,y2) intersects an axis-aligned bounding box.
|
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(
|
def _point_in_rect(
|
||||||
px: float, py: float,
|
px: float,
|
||||||
min_x: float, min_y: float, max_x: float, max_y: float,
|
py: float,
|
||||||
|
min_x: float,
|
||||||
|
min_y: float,
|
||||||
|
max_x: float,
|
||||||
|
max_y: float,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Check if a point is within a rectangle."""
|
"""Check if a point is within a rectangle."""
|
||||||
return min_x <= px <= max_x and min_y <= py <= max_y
|
return min_x <= px <= max_x and min_y <= py <= max_y
|
||||||
@@ -325,10 +358,13 @@ def _aabb_overlap(
|
|||||||
|
|
||||||
|
|
||||||
def _transform_local_point(
|
def _transform_local_point(
|
||||||
lx: float, ly: float,
|
lx: float,
|
||||||
sym_x: float, sym_y: float,
|
ly: float,
|
||||||
|
sym_x: float,
|
||||||
|
sym_y: float,
|
||||||
rotation: float,
|
rotation: float,
|
||||||
mirror_x: bool, mirror_y: bool,
|
mirror_x: bool,
|
||||||
|
mirror_y: bool,
|
||||||
) -> Tuple[float, float]:
|
) -> Tuple[float, float]:
|
||||||
"""
|
"""
|
||||||
Transform a point from local symbol coordinates to absolute schematic
|
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)
|
return (min_x, min_y, max_x, max_y)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Tool 3: find_overlapping_elements
|
# Tool 3: find_overlapping_elements
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def find_overlapping_elements(
|
def find_overlapping_elements(
|
||||||
schematic_path: Path, tolerance: float = 0.5
|
schematic_path: Path, tolerance: float = 0.5
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
@@ -453,7 +489,11 @@ def find_overlapping_elements(
|
|||||||
lib_defs = _extract_lib_symbols(sexp_data)
|
lib_defs = _extract_lib_symbols(sexp_data)
|
||||||
|
|
||||||
# --- Symbol-symbol overlap using bounding-box intersection (O(n²)) ---
|
# --- 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
|
# Pre-compute bounding boxes for all non-template symbols
|
||||||
symbol_bboxes = []
|
symbol_bboxes = []
|
||||||
@@ -463,7 +503,9 @@ def find_overlapping_elements(
|
|||||||
graphics_points = lib_data.get("graphics_points", [])
|
graphics_points = lib_data.get("graphics_points", [])
|
||||||
bbox = None
|
bbox = None
|
||||||
if pin_defs:
|
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))
|
symbol_bboxes.append((sym, bbox))
|
||||||
|
|
||||||
for i in range(len(symbol_bboxes)):
|
for i in range(len(symbol_bboxes)):
|
||||||
@@ -482,10 +524,16 @@ def find_overlapping_elements(
|
|||||||
|
|
||||||
if overlap_detected:
|
if overlap_detected:
|
||||||
entry = {
|
entry = {
|
||||||
"element1": {"reference": s1["reference"], "libId": s1["lib_id"],
|
"element1": {
|
||||||
"position": {"x": s1["x"], "y": s1["y"]}},
|
"reference": s1["reference"],
|
||||||
"element2": {"reference": s2["reference"], "libId": s2["lib_id"],
|
"libId": s1["lib_id"],
|
||||||
"position": {"x": s2["x"], "y": s2["y"]}},
|
"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),
|
"distance": round(dist, 4),
|
||||||
}
|
}
|
||||||
# Flag power symbol pairs specifically
|
# Flag power symbol pairs specifically
|
||||||
@@ -502,13 +550,21 @@ def find_overlapping_elements(
|
|||||||
l2 = labels[j]
|
l2 = labels[j]
|
||||||
dist = _distance((l1["x"], l1["y"]), (l2["x"], l2["y"]))
|
dist = _distance((l1["x"], l1["y"]), (l2["x"], l2["y"]))
|
||||||
if dist < tolerance:
|
if dist < tolerance:
|
||||||
overlapping_labels.append({
|
overlapping_labels.append(
|
||||||
"element1": {"name": l1["name"], "type": l1["type"],
|
{
|
||||||
"position": {"x": l1["x"], "y": l1["y"]}},
|
"element1": {
|
||||||
"element2": {"name": l2["name"], "type": l2["type"],
|
"name": l1["name"],
|
||||||
"position": {"x": l2["x"], "y": l2["y"]}},
|
"type": l1["type"],
|
||||||
"distance": round(dist, 4),
|
"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 ---
|
# --- Wire-wire collinear overlap ---
|
||||||
for i in range(len(wires)):
|
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)
|
min2, max2 = min(proj_s2, proj_e2), max(proj_s2, proj_e2)
|
||||||
if min1 < max2 and min2 < max1:
|
if min1 < max2 and min2 < max1:
|
||||||
return {
|
return {
|
||||||
"wire1": {"start": {"x": s1[0], "y": s1[1]}, "end": {"x": e1[0], "y": e1[1]}},
|
"wire1": {
|
||||||
"wire2": {"start": {"x": s2[0], "y": s2[1]}, "end": {"x": e2[0], "y": e2[1]}},
|
"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",
|
"type": "collinear_overlap",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,9 +648,13 @@ def _check_wire_overlap(
|
|||||||
# Tool 4: get_elements_in_region
|
# Tool 4: get_elements_in_region
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def get_elements_in_region(
|
def get_elements_in_region(
|
||||||
schematic_path: Path,
|
schematic_path: Path,
|
||||||
x1: float, y1: float, x2: float, y2: float,
|
x1: float,
|
||||||
|
y1: float,
|
||||||
|
x2: float,
|
||||||
|
y2: float,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
List all wires, labels, and symbols within a rectangular region.
|
List all wires, labels, and symbols within a rectangular region.
|
||||||
@@ -637,23 +703,31 @@ def get_elements_in_region(
|
|||||||
region_wires = []
|
region_wires = []
|
||||||
for w in wires:
|
for w in wires:
|
||||||
s, e = w["start"], w["end"]
|
s, e = w["start"], w["end"]
|
||||||
if (_point_in_rect(s[0], s[1], min_x, min_y, max_x, max_y) or
|
if (
|
||||||
_point_in_rect(e[0], e[1], min_x, min_y, max_x, max_y) or
|
_point_in_rect(s[0], s[1], min_x, min_y, max_x, max_y)
|
||||||
_line_segment_intersects_aabb(s[0], s[1], e[0], e[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)
|
||||||
region_wires.append({
|
or _line_segment_intersects_aabb(
|
||||||
"start": {"x": s[0], "y": s[1]},
|
s[0], s[1], e[0], e[1], min_x, min_y, max_x, max_y
|
||||||
"end": {"x": e[0], "y": e[1]},
|
)
|
||||||
})
|
):
|
||||||
|
region_wires.append(
|
||||||
|
{
|
||||||
|
"start": {"x": s[0], "y": s[1]},
|
||||||
|
"end": {"x": e[0], "y": e[1]},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Labels: include if position is within bounds
|
# Labels: include if position is within bounds
|
||||||
region_labels = []
|
region_labels = []
|
||||||
for lbl in labels:
|
for lbl in labels:
|
||||||
if _point_in_rect(lbl["x"], lbl["y"], min_x, min_y, max_x, max_y):
|
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"],
|
"name": lbl["name"],
|
||||||
"position": {"x": lbl["x"], "y": lbl["y"]},
|
"type": lbl["type"],
|
||||||
})
|
"position": {"x": lbl["x"], "y": lbl["y"]},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"symbols": region_symbols,
|
"symbols": region_symbols,
|
||||||
@@ -671,6 +745,7 @@ def get_elements_in_region(
|
|||||||
# Tool 5: check_wire_collisions
|
# Tool 5: check_wire_collisions
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _compute_pin_positions_direct(
|
def _compute_pin_positions_direct(
|
||||||
sym: Dict[str, Any], pin_defs: Dict[str, Dict]
|
sym: Dict[str, Any], pin_defs: Dict[str, Dict]
|
||||||
) -> Dict[str, List[float]]:
|
) -> Dict[str, List[float]]:
|
||||||
@@ -748,7 +823,9 @@ def find_wires_crossing_symbols(schematic_path: Path) -> List[Dict[str, Any]]:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
graphics_points = lib_data.get("graphics_points", [])
|
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:
|
if bbox is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -757,11 +834,13 @@ def find_wires_crossing_symbols(schematic_path: Path) -> List[Dict[str, Any]]:
|
|||||||
for pos in pin_positions.values():
|
for pos in pin_positions.values():
|
||||||
pin_set.add((pos[0], pos[1]))
|
pin_set.add((pos[0], pos[1]))
|
||||||
|
|
||||||
symbol_data.append({
|
symbol_data.append(
|
||||||
"sym": sym,
|
{
|
||||||
"bbox": bbox,
|
"sym": sym,
|
||||||
"pin_set": pin_set,
|
"bbox": bbox,
|
||||||
})
|
"pin_set": pin_set,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Test each wire against each symbol bbox
|
# Test each wire against each symbol bbox
|
||||||
for w in wires:
|
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
|
continue # Wire terminates at pin from outside
|
||||||
|
|
||||||
sym = sd["sym"]
|
sym = sd["sym"]
|
||||||
collisions.append({
|
collisions.append(
|
||||||
|
{
|
||||||
"wire": {
|
"wire": {
|
||||||
"start": {"x": sx, "y": sy},
|
"start": {"x": sx, "y": sy},
|
||||||
"end": {"x": ex, "y": ey},
|
"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"]},
|
"position": {"x": sym["x"], "y": sym["y"]},
|
||||||
},
|
},
|
||||||
"intersectionType": "passes_through",
|
"intersectionType": "passes_through",
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return collisions
|
return collisions
|
||||||
|
|||||||
@@ -2597,19 +2597,34 @@ class KiCADInterface:
|
|||||||
svg_output = None
|
svg_output = None
|
||||||
|
|
||||||
try:
|
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)
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
||||||
|
|
||||||
if result.returncode != 0:
|
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
|
# kicad-cli names the file after the schematic
|
||||||
svg_files = [f for f in os.listdir(tmp_dir) if f.endswith(".svg")]
|
svg_files = [f for f in os.listdir(tmp_dir) if f.endswith(".svg")]
|
||||||
if not svg_files:
|
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])
|
svg_output = os.path.join(tmp_dir, svg_files[0])
|
||||||
|
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
tree = ET.parse(svg_output)
|
tree = ET.parse(svg_output)
|
||||||
root = tree.getroot()
|
root = tree.getroot()
|
||||||
|
|
||||||
@@ -2642,8 +2657,13 @@ class KiCADInterface:
|
|||||||
try:
|
try:
|
||||||
from cairosvg import svg2png
|
from cairosvg import svg2png
|
||||||
except ImportError:
|
except ImportError:
|
||||||
return {"success": False, "message": "PNG export requires the 'cairosvg' package. Install it with: pip install cairosvg"}
|
return {
|
||||||
png_data = svg2png(url=cropped_svg_path, output_width=width, output_height=height)
|
"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 {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"imageData": base64.b64encode(png_data).decode("utf-8"),
|
"imageData": base64.b64encode(png_data).decode("utf-8"),
|
||||||
@@ -2651,15 +2671,16 @@ class KiCADInterface:
|
|||||||
}
|
}
|
||||||
finally:
|
finally:
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in get_schematic_view_region: {e}")
|
logger.error(f"Error in get_schematic_view_region: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
def _handle_find_overlapping_elements(self, params):
|
def _handle_find_overlapping_elements(self, params):
|
||||||
"""Detect spatially overlapping symbols, wires, and labels"""
|
"""Detect spatially overlapping symbols, wires, and labels"""
|
||||||
logger.info("Finding overlapping elements in schematic")
|
logger.info("Finding overlapping elements in schematic")
|
||||||
@@ -2681,6 +2702,7 @@ class KiCADInterface:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error finding overlapping elements: {e}")
|
logger.error(f"Error finding overlapping elements: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
@@ -2709,6 +2731,7 @@ class KiCADInterface:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting elements in region: {e}")
|
logger.error(f"Error getting elements in region: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
@@ -2733,6 +2756,7 @@ class KiCADInterface:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error checking wire collisions: {e}")
|
logger.error(f"Error checking wire collisions: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,7 @@ except ImportError:
|
|||||||
|
|
||||||
class _FakeSchematic:
|
class _FakeSchematic:
|
||||||
"""Minimal stand-in for skip.Schematic used in PinLocator cache."""
|
"""Minimal stand-in for skip.Schematic used in PinLocator cache."""
|
||||||
|
|
||||||
def __init__(self, path: str):
|
def __init__(self, path: str):
|
||||||
self.path = path
|
self.path = path
|
||||||
self.symbol = []
|
self.symbol = []
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ from commands.schematic_analysis import (
|
|||||||
find_wires_crossing_symbols,
|
find_wires_crossing_symbols,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Helpers
|
# Helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -127,6 +126,7 @@ def _make_led_sexp(ref: str, x: float, y: float, rotation: float = 0) -> str:
|
|||||||
# Unit tests — geometry helpers
|
# Unit tests — geometry helpers
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestGeometryHelpers:
|
class TestGeometryHelpers:
|
||||||
"""Test low-level geometry utilities."""
|
"""Test low-level geometry utilities."""
|
||||||
|
|
||||||
@@ -174,6 +174,7 @@ class TestGeometryHelpers:
|
|||||||
# Unit tests — S-expression parsers
|
# Unit tests — S-expression parsers
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestSexpParsers:
|
class TestSexpParsers:
|
||||||
"""Test S-expression parsing functions with synthetic data."""
|
"""Test S-expression parsing functions with synthetic data."""
|
||||||
|
|
||||||
@@ -223,6 +224,7 @@ class TestSexpParsers:
|
|||||||
# Unit tests — analysis functions with mocked PinLocator
|
# Unit tests — analysis functions with mocked PinLocator
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestAABBOverlap:
|
class TestAABBOverlap:
|
||||||
"""Test AABB overlap helper."""
|
"""Test AABB overlap helper."""
|
||||||
|
|
||||||
@@ -254,14 +256,18 @@ class TestFindOverlappingElements:
|
|||||||
|
|
||||||
def test_overlapping_symbols_detected(self):
|
def test_overlapping_symbols_detected(self):
|
||||||
# Two resistors at nearly the same position — bboxes fully overlap
|
# 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)
|
tmp = _make_temp_schematic(extra)
|
||||||
result = find_overlapping_elements(tmp, tolerance=0.5)
|
result = find_overlapping_elements(tmp, tolerance=0.5)
|
||||||
assert result["totalOverlaps"] >= 1
|
assert result["totalOverlaps"] >= 1
|
||||||
assert len(result["overlappingSymbols"]) >= 1
|
assert len(result["overlappingSymbols"]) >= 1
|
||||||
|
|
||||||
def test_well_separated_symbols_not_flagged(self):
|
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)
|
tmp = _make_temp_schematic(extra)
|
||||||
result = find_overlapping_elements(tmp, tolerance=0.5)
|
result = find_overlapping_elements(tmp, tolerance=0.5)
|
||||||
assert result["totalOverlaps"] == 0
|
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]
|
# 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
|
# 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)
|
tmp = _make_temp_schematic(extra)
|
||||||
result = find_overlapping_elements(tmp, tolerance=0.5)
|
result = find_overlapping_elements(tmp, tolerance=0.5)
|
||||||
assert result["totalOverlaps"] >= 1, (
|
assert result["totalOverlaps"] >= 1, (
|
||||||
@@ -302,7 +310,9 @@ class TestFindOverlappingElements:
|
|||||||
|
|
||||||
R pins at y ±3.81, but different X positions far enough apart.
|
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)
|
tmp = _make_temp_schematic(extra)
|
||||||
result = find_overlapping_elements(tmp, tolerance=0.5)
|
result = find_overlapping_elements(tmp, tolerance=0.5)
|
||||||
assert result["totalOverlaps"] == 0
|
assert result["totalOverlaps"] == 0
|
||||||
@@ -355,6 +365,7 @@ class TestComputeSymbolBbox:
|
|||||||
def test_returns_none_for_unknown_symbol(self):
|
def test_returns_none_for_unknown_symbol(self):
|
||||||
tmp = _make_temp_schematic()
|
tmp = _make_temp_schematic()
|
||||||
from commands.pin_locator import PinLocator
|
from commands.pin_locator import PinLocator
|
||||||
|
|
||||||
locator = PinLocator()
|
locator = PinLocator()
|
||||||
result = compute_symbol_bbox(tmp, "NONEXISTENT", locator)
|
result = compute_symbol_bbox(tmp, "NONEXISTENT", locator)
|
||||||
assert result is None
|
assert result is None
|
||||||
@@ -409,8 +420,7 @@ class TestIntegrationFindWiresCrossingSymbols:
|
|||||||
result = find_wires_crossing_symbols(tmp)
|
result = find_wires_crossing_symbols(tmp)
|
||||||
# The wire must not be reported against the far-away R? at (200, 100)
|
# The wire must not be reported against the far-away R? at (200, 100)
|
||||||
collisions_at_200 = [
|
collisions_at_200 = [
|
||||||
c for c in result
|
c for c in result if abs(c["component"]["position"]["x"] - 200) < 0.5
|
||||||
if abs(c["component"]["position"]["x"] - 200) < 0.5
|
|
||||||
]
|
]
|
||||||
assert len(collisions_at_200) == 0, (
|
assert len(collisions_at_200) == 0, (
|
||||||
"Wire at x≈100 must not be flagged against the R? at x=200; "
|
"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)
|
tmp = _make_temp_schematic(extra)
|
||||||
result = find_wires_crossing_symbols(tmp)
|
result = find_wires_crossing_symbols(tmp)
|
||||||
d1_crossings = [c for c in result if c["component"]["reference"] == "D1"]
|
d1_crossings = [c for c in result if c["component"]["reference"] == "D1"]
|
||||||
assert len(d1_crossings) >= 1, (
|
assert (
|
||||||
"Wire starting at pin but passing through body must be detected"
|
len(d1_crossings) >= 1
|
||||||
)
|
), "Wire starting at pin but passing through body must be detected"
|
||||||
|
|
||||||
def test_wire_terminating_at_pin_from_outside(self):
|
def test_wire_terminating_at_pin_from_outside(self):
|
||||||
"""A wire that arrives at a pin from outside the component body
|
"""A wire that arrives at a pin from outside the component body
|
||||||
@@ -448,17 +458,17 @@ class TestIntegrationFindWiresCrossingSymbols:
|
|||||||
tmp = _make_temp_schematic(extra)
|
tmp = _make_temp_schematic(extra)
|
||||||
result = find_wires_crossing_symbols(tmp)
|
result = find_wires_crossing_symbols(tmp)
|
||||||
d1_crossings = [c for c in result if c["component"]["reference"] == "D1"]
|
d1_crossings = [c for c in result if c["component"]["reference"] == "D1"]
|
||||||
assert len(d1_crossings) == 0, (
|
assert (
|
||||||
"Wire terminating at pin from outside should not be flagged"
|
len(d1_crossings) == 0
|
||||||
)
|
), "Wire terminating at pin from outside should not be flagged"
|
||||||
|
|
||||||
def test_wire_shorts_component_pins_detected_as_collision(self):
|
def test_wire_shorts_component_pins_detected_as_collision(self):
|
||||||
"""Regression: a wire connecting pin1→pin2 of the same component
|
"""Regression: a wire connecting pin1→pin2 of the same component
|
||||||
must be reported even though both endpoints land on pins."""
|
must be reported even though both endpoints land on pins."""
|
||||||
r_sexp = _make_resistor_sexp("R_short", 100.0, 100.0)
|
r_sexp = _make_resistor_sexp("R_short", 100.0, 100.0)
|
||||||
wire_sexp = (
|
wire_sexp = (
|
||||||
'(wire (pts (xy 100 103.81) (xy 100 96.19))\n'
|
"(wire (pts (xy 100 103.81) (xy 100 96.19))\n"
|
||||||
' (stroke (width 0) (type default))\n'
|
" (stroke (width 0) (type default))\n"
|
||||||
' (uuid "aaaaaaaa-0000-0000-0000-000000000001"))'
|
' (uuid "aaaaaaaa-0000-0000-0000-000000000001"))'
|
||||||
)
|
)
|
||||||
sch = _make_temp_schematic(r_sexp + "\n" + wire_sexp)
|
sch = _make_temp_schematic(r_sexp + "\n" + wire_sexp)
|
||||||
@@ -511,6 +521,7 @@ class TestIntegrationGetElementsInRegion:
|
|||||||
# Unit tests — _check_wire_overlap
|
# Unit tests — _check_wire_overlap
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestCheckWireOverlap:
|
class TestCheckWireOverlap:
|
||||||
"""Test wire overlap detection for horizontal, vertical, and diagonal cases."""
|
"""Test wire overlap detection for horizontal, vertical, and diagonal cases."""
|
||||||
|
|
||||||
@@ -613,6 +624,7 @@ class TestIntegrationDiagonalWireOverlap:
|
|||||||
# Unit tests — _extract_lib_symbols
|
# Unit tests — _extract_lib_symbols
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestExtractLibSymbols:
|
class TestExtractLibSymbols:
|
||||||
"""Test _extract_lib_symbols helper."""
|
"""Test _extract_lib_symbols helper."""
|
||||||
|
|
||||||
@@ -684,6 +696,7 @@ class TestExtractLibSymbols:
|
|||||||
# Unit tests — _parse_lib_symbol_graphics
|
# Unit tests — _parse_lib_symbol_graphics
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestParseLibSymbolGraphics:
|
class TestParseLibSymbolGraphics:
|
||||||
"""Test graphics extraction from lib_symbol definitions."""
|
"""Test graphics extraction from lib_symbol definitions."""
|
||||||
|
|
||||||
@@ -745,6 +758,7 @@ class TestParseLibSymbolGraphics:
|
|||||||
# Unit tests — _transform_local_point
|
# Unit tests — _transform_local_point
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestTransformLocalPoint:
|
class TestTransformLocalPoint:
|
||||||
"""Test local→absolute coordinate transform."""
|
"""Test local→absolute coordinate transform."""
|
||||||
|
|
||||||
@@ -776,6 +790,7 @@ class TestTransformLocalPoint:
|
|||||||
# Unit tests — _compute_symbol_bbox_direct with graphics
|
# Unit tests — _compute_symbol_bbox_direct with graphics
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestComputeSymbolBboxWithGraphics:
|
class TestComputeSymbolBboxWithGraphics:
|
||||||
"""Test that bounding box computation uses graphics points when available."""
|
"""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.
|
"""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.
|
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."""
|
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 = {
|
pin_defs = {
|
||||||
"1": {"x": 0, "y": 3.81, "angle": 270, "length": 1.27, "name": "~", "type": "passive"},
|
"1": {
|
||||||
"2": {"x": 0, "y": -3.81, "angle": 90, "length": 1.27, "name": "~", "type": "passive"},
|
"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)]
|
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
|
assert bbox is not None
|
||||||
min_x, min_y, max_x, max_y = bbox
|
min_x, min_y, max_x, max_y = bbox
|
||||||
# X should come from rectangle: 100 ± 1.016
|
# X should come from rectangle: 100 ± 1.016
|
||||||
@@ -802,10 +839,30 @@ class TestComputeSymbolBboxWithGraphics:
|
|||||||
|
|
||||||
def test_fallback_without_graphics(self):
|
def test_fallback_without_graphics(self):
|
||||||
"""Without graphics_points, should use the old degenerate expansion."""
|
"""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 = {
|
pin_defs = {
|
||||||
"1": {"x": 0, "y": 3.81, "angle": 270, "length": 1.27, "name": "~", "type": "passive"},
|
"1": {
|
||||||
"2": {"x": 0, "y": -3.81, "angle": 90, "length": 1.27, "name": "~", "type": "passive"},
|
"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)
|
bbox = _compute_symbol_bbox_direct(sym, pin_defs)
|
||||||
@@ -817,15 +874,37 @@ class TestComputeSymbolBboxWithGraphics:
|
|||||||
|
|
||||||
def test_rotated_symbol_graphics(self):
|
def test_rotated_symbol_graphics(self):
|
||||||
"""Graphics points should be rotated along with the symbol."""
|
"""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 = {
|
pin_defs = {
|
||||||
"1": {"x": 0, "y": 3.81, "angle": 270, "length": 1.27, "name": "~", "type": "passive"},
|
"1": {
|
||||||
"2": {"x": 0, "y": -3.81, "angle": 90, "length": 1.27, "name": "~", "type": "passive"},
|
"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
|
# Rectangle corners in local coords
|
||||||
graphics_points = [(-1.016, -2.54), (1.016, 2.54)]
|
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
|
assert bbox is not None
|
||||||
min_x, min_y, max_x, max_y = bbox
|
min_x, min_y, max_x, max_y = bbox
|
||||||
# After 90° rotation, X and Y swap roles
|
# After 90° rotation, X and Y swap roles
|
||||||
@@ -856,7 +935,9 @@ class TestIntegrationGraphicsBbox:
|
|||||||
|
|
||||||
assert len(graphics_points) >= 2, "Should have extracted rectangle points"
|
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
|
assert bbox is not None
|
||||||
min_x, min_y, max_x, max_y = bbox
|
min_x, min_y, max_x, max_y = bbox
|
||||||
# Rectangle is ±1.016 in X, NOT ±1.5 from degenerate expansion
|
# Rectangle is ±1.016 in X, NOT ±1.5 from degenerate expansion
|
||||||
|
|||||||
Reference in New Issue
Block a user