feat: add get_net_at_point tool for coordinate-based net lookup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -331,3 +331,53 @@ def get_pin_net(schematic: Any, schematic_path: str, x_mm: float, y_mm: float) -
|
||||
pins = _find_pins_on_net(net_points, schematic_path, schematic)
|
||||
|
||||
return {"net": net, "pins": pins, "wires": wires_out, "query_point": {"x": x_mm, "y": y_mm}}
|
||||
|
||||
|
||||
def get_net_at_point(
|
||||
schematic: Any, schematic_path: str, x_mm: float, y_mm: float
|
||||
) -> Dict[str, Any]:
|
||||
"""Return the net name at the given coordinate, or null if none found.
|
||||
|
||||
Checks net label positions first (exact IU match within tolerance), then
|
||||
wire endpoints. Returns a dict with keys:
|
||||
- "net_name": str or None
|
||||
- "position": {"x": float, "y": float}
|
||||
- "source": "net_label" | "wire_endpoint" | None
|
||||
"""
|
||||
query_iu = _to_iu(x_mm, y_mm)
|
||||
position = {"x": x_mm, "y": y_mm}
|
||||
|
||||
# Build label map from schematic
|
||||
point_to_label, _ = _parse_virtual_connections(schematic, schematic_path)
|
||||
|
||||
# Check if query point is exactly on a net label / power symbol position
|
||||
label_name = point_to_label.get(query_iu)
|
||||
if label_name is not None:
|
||||
return {"net_name": label_name, "position": position, "source": "net_label"}
|
||||
|
||||
# Check if query point is on a wire endpoint
|
||||
all_wires = _parse_wires(schematic) if hasattr(schematic, "wire") else []
|
||||
if all_wires:
|
||||
_, iu_to_wires = _build_adjacency(all_wires)
|
||||
if query_iu in iu_to_wires:
|
||||
# Found a wire endpoint — trace the net to get the name
|
||||
adjacency, _ = _build_adjacency(all_wires)
|
||||
visited, net_points = _find_connected_wires(
|
||||
x_mm,
|
||||
y_mm,
|
||||
all_wires,
|
||||
iu_to_wires,
|
||||
adjacency,
|
||||
point_to_label=point_to_label,
|
||||
label_to_points=None,
|
||||
)
|
||||
if visited is not None:
|
||||
net: Optional[str] = None
|
||||
if net_points:
|
||||
for pt in net_points:
|
||||
net = point_to_label.get(pt)
|
||||
if net is not None:
|
||||
break
|
||||
return {"net_name": net, "position": position, "source": "wire_endpoint"}
|
||||
|
||||
return {"net_name": None, "position": position, "source": None}
|
||||
|
||||
@@ -382,6 +382,7 @@ class KiCADInterface:
|
||||
"get_schematic_pin_locations": self._handle_get_schematic_pin_locations,
|
||||
"get_net_connections": self._handle_get_net_connections,
|
||||
"get_wire_connections": self._handle_get_wire_connections,
|
||||
"get_net_at_point": self._handle_get_net_at_point,
|
||||
"get_pin_net": self._handle_get_pin_net,
|
||||
"run_erc": self._handle_run_erc,
|
||||
"generate_netlist": self._handle_generate_netlist,
|
||||
@@ -2504,6 +2505,40 @@ class KiCADInterface:
|
||||
logger.error(traceback.format_exc())
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _handle_get_net_at_point(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Return the net name at a given (x, y) coordinate, or null if none found."""
|
||||
logger.info("Getting net at point")
|
||||
try:
|
||||
from commands.wire_connectivity import get_net_at_point
|
||||
|
||||
schematic_path = params.get("schematicPath")
|
||||
if not schematic_path:
|
||||
return {"success": False, "message": "Missing required parameter: schematicPath"}
|
||||
|
||||
x = params.get("x")
|
||||
y = params.get("y")
|
||||
if x is None or y is None:
|
||||
return {"success": False, "message": "Missing required parameters: x and y"}
|
||||
|
||||
try:
|
||||
x, y = float(x), float(y)
|
||||
except (TypeError, ValueError):
|
||||
return {"success": False, "message": "Parameters x and y must be numeric"}
|
||||
|
||||
schematic = SchematicManager.load_schematic(schematic_path)
|
||||
if not schematic:
|
||||
return {"success": False, "message": "Failed to load schematic"}
|
||||
|
||||
result = get_net_at_point(schematic, schematic_path, x, y)
|
||||
return {"success": True, **result}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting net at point: {str(e)}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _handle_get_pin_net(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Find net name and all connected pins for a reference+pin or coordinate query"""
|
||||
logger.info("Getting pin net")
|
||||
|
||||
@@ -1523,6 +1523,35 @@ SCHEMATIC_TOOLS = [
|
||||
"required": ["schematicPath", "x", "y"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "get_net_at_point",
|
||||
"title": "Get Net At Point",
|
||||
"description": (
|
||||
"Returns the net name at a given (x, y) coordinate in a schematic, "
|
||||
"or null if no net label or wire endpoint is present at that position. "
|
||||
"Checks net label positions first, then wire endpoints. "
|
||||
"Useful for quickly identifying what net occupies a specific coordinate "
|
||||
"without traversing the full wire graph."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"schematicPath": {
|
||||
"type": "string",
|
||||
"description": "Path to the schematic file (.kicad_sch)",
|
||||
},
|
||||
"x": {
|
||||
"type": "number",
|
||||
"description": "X coordinate in mm",
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"description": "Y coordinate in mm",
|
||||
},
|
||||
},
|
||||
"required": ["schematicPath", "x", "y"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "get_pin_net",
|
||||
"title": "Get Pin Net",
|
||||
|
||||
@@ -1425,6 +1425,46 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"get_net_at_point",
|
||||
"Returns the net name at a given (x, y) coordinate in a schematic, or null if no net label " +
|
||||
"or wire endpoint is present at that position. Faster than get_pin_net when you only need " +
|
||||
"the net name at a known coordinate and don't need pin traversal.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the schematic file (.kicad_sch)"),
|
||||
x: z.number().describe("X coordinate in mm"),
|
||||
y: z.number().describe("Y coordinate in mm"),
|
||||
},
|
||||
async (args: { schematicPath: string; x: number; y: number }) => {
|
||||
const result = await callKicadScript("get_net_at_point", args);
|
||||
if (result.success) {
|
||||
const netName = result.net_name ?? null;
|
||||
const source = result.source ?? null;
|
||||
const pos = result.position;
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
`Net at (${pos?.x ?? args.x}, ${pos?.y ?? args.y}): ` +
|
||||
(netName !== null ? netName : "(none)") +
|
||||
(source ? ` [source: ${source}]` : ""),
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Failed to get net at point: ${result.message || "Unknown error"}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"get_pin_net",
|
||||
"Returns the net name and all connected pins for a component pin (reference + pin number) " +
|
||||
|
||||
410
tests/test_get_net_at_point.py
Normal file
410
tests/test_get_net_at_point.py
Normal file
@@ -0,0 +1,410 @@
|
||||
"""
|
||||
Tests for the get_net_at_point tool and its handler.
|
||||
|
||||
Covers:
|
||||
- Schema shape (TestGetNetAtPointSchema)
|
||||
- Handler dispatch registration (TestGetNetAtPointHandlerDispatch)
|
||||
- Parameter validation in the handler (TestGetNetAtPointHandlerParamValidation)
|
||||
- Core logic: get_net_at_point function (TestGetNetAtPointCoreLogic)
|
||||
- Integration: real schematic file (TestGetNetAtPointIntegration)
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "python"))
|
||||
|
||||
from commands.wire_connectivity import get_net_at_point
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared mock helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_TEMPLATE = Path(__file__).parent.parent / "python" / "templates" / "empty.kicad_sch"
|
||||
|
||||
|
||||
def _make_point(x: float, y: float) -> MagicMock:
|
||||
pt = MagicMock()
|
||||
pt.value = [x, y]
|
||||
return pt
|
||||
|
||||
|
||||
def _make_wire(x1: float, y1: float, x2: float, y2: float) -> MagicMock:
|
||||
wire = MagicMock()
|
||||
wire.pts = MagicMock()
|
||||
wire.pts.xy = [_make_point(x1, y1), _make_point(x2, y2)]
|
||||
return wire
|
||||
|
||||
|
||||
def _make_schematic_no_labels(*wires: Any) -> MagicMock:
|
||||
sch = MagicMock()
|
||||
sch.wire = list(wires)
|
||||
del sch.label
|
||||
del sch.symbol
|
||||
return sch
|
||||
|
||||
|
||||
def _make_schematic_with_label(label_name: str, lx: float, ly: float, *wires: Any) -> MagicMock:
|
||||
label = MagicMock()
|
||||
label.value = label_name
|
||||
label.at = MagicMock()
|
||||
label.at.value = [lx, ly, 0]
|
||||
|
||||
sch = MagicMock()
|
||||
sch.wire = list(wires)
|
||||
sch.label = [label]
|
||||
del sch.symbol
|
||||
return sch
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestGetNetAtPointSchema
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetNetAtPointSchema:
|
||||
"""Verify the get_net_at_point tool schema is present and well-formed."""
|
||||
|
||||
def test_schema_registered(self) -> None:
|
||||
from schemas.tool_schemas import TOOL_SCHEMAS
|
||||
|
||||
assert "get_net_at_point" in TOOL_SCHEMAS
|
||||
|
||||
def test_schema_required_fields(self) -> None:
|
||||
from schemas.tool_schemas import TOOL_SCHEMAS
|
||||
|
||||
required = TOOL_SCHEMAS["get_net_at_point"]["inputSchema"]["required"]
|
||||
assert set(required) == {"schematicPath", "x", "y"}
|
||||
|
||||
def test_schema_has_title_and_description(self) -> None:
|
||||
from schemas.tool_schemas import TOOL_SCHEMAS
|
||||
|
||||
schema = TOOL_SCHEMAS["get_net_at_point"]
|
||||
assert schema.get("title")
|
||||
assert schema.get("description")
|
||||
|
||||
def test_schema_properties(self) -> None:
|
||||
from schemas.tool_schemas import TOOL_SCHEMAS
|
||||
|
||||
props = TOOL_SCHEMAS["get_net_at_point"]["inputSchema"]["properties"]
|
||||
for field in ("schematicPath", "x", "y"):
|
||||
assert field in props, f"Expected '{field}' in schema properties"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestGetNetAtPointHandlerDispatch
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetNetAtPointHandlerDispatch:
|
||||
"""Verify the handler is wired into KiCadInterface.command_routes."""
|
||||
|
||||
def test_get_net_at_point_in_routes(self) -> None:
|
||||
with patch("kicad_interface.USE_IPC_BACKEND", False):
|
||||
from kicad_interface import KiCADInterface
|
||||
|
||||
iface = KiCADInterface.__new__(KiCADInterface)
|
||||
iface.board = None
|
||||
iface.project_filename = None
|
||||
iface.use_ipc = False
|
||||
iface.ipc_backend = MagicMock()
|
||||
iface.ipc_board_api = None
|
||||
iface.footprint_library = MagicMock()
|
||||
iface.project_commands = MagicMock()
|
||||
iface.board_commands = MagicMock()
|
||||
iface.component_commands = MagicMock()
|
||||
iface.routing_commands = MagicMock()
|
||||
KiCADInterface.__init__(iface)
|
||||
|
||||
assert "get_net_at_point" in iface.command_routes
|
||||
assert callable(iface.command_routes["get_net_at_point"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestGetNetAtPointHandlerParamValidation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetNetAtPointHandlerParamValidation:
|
||||
"""Handler returns error responses for bad or missing parameters."""
|
||||
|
||||
def _make_handler(self) -> Any:
|
||||
with patch("kicad_interface.USE_IPC_BACKEND", False):
|
||||
from kicad_interface import KiCADInterface
|
||||
|
||||
iface = KiCADInterface.__new__(KiCADInterface)
|
||||
return iface._handle_get_net_at_point
|
||||
|
||||
def test_missing_schematic_path(self) -> None:
|
||||
handler = self._make_handler()
|
||||
result = handler({"x": 1.0, "y": 2.0})
|
||||
assert result["success"] is False
|
||||
assert "schematicPath" in result["message"] or "Missing" in result["message"]
|
||||
|
||||
def test_missing_x(self) -> None:
|
||||
handler = self._make_handler()
|
||||
result = handler({"schematicPath": "/tmp/test.kicad_sch", "y": 2.0})
|
||||
assert result["success"] is False
|
||||
|
||||
def test_missing_y(self) -> None:
|
||||
handler = self._make_handler()
|
||||
result = handler({"schematicPath": "/tmp/test.kicad_sch", "x": 1.0})
|
||||
assert result["success"] is False
|
||||
|
||||
def test_missing_both_coords(self) -> None:
|
||||
handler = self._make_handler()
|
||||
result = handler({"schematicPath": "/tmp/test.kicad_sch"})
|
||||
assert result["success"] is False
|
||||
|
||||
def test_non_numeric_x(self) -> None:
|
||||
handler = self._make_handler()
|
||||
result = handler({"schematicPath": "/tmp/test.kicad_sch", "x": "bad", "y": 2.0})
|
||||
assert result["success"] is False
|
||||
assert "numeric" in result["message"].lower() or "x" in result["message"]
|
||||
|
||||
def test_non_numeric_y(self) -> None:
|
||||
handler = self._make_handler()
|
||||
result = handler({"schematicPath": "/tmp/test.kicad_sch", "x": 1.0, "y": "bad"})
|
||||
assert result["success"] is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestGetNetAtPointCoreLogic
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetNetAtPointCoreLogic:
|
||||
"""Unit tests for the get_net_at_point function."""
|
||||
|
||||
def test_no_wires_no_labels_returns_null_net(self) -> None:
|
||||
sch = MagicMock()
|
||||
sch.wire = []
|
||||
del sch.label
|
||||
del sch.symbol
|
||||
result = get_net_at_point(sch, "/tmp/test.kicad_sch", 1.0, 2.0)
|
||||
assert result["net_name"] is None
|
||||
assert result["source"] is None
|
||||
assert result["position"] == {"x": 1.0, "y": 2.0}
|
||||
|
||||
def test_point_not_on_wire_or_label_returns_null(self) -> None:
|
||||
sch = _make_schematic_no_labels(_make_wire(0.0, 0.0, 1.0, 0.0))
|
||||
result = get_net_at_point(sch, "/tmp/test.kicad_sch", 5.0, 5.0)
|
||||
assert result["net_name"] is None
|
||||
assert result["source"] is None
|
||||
|
||||
def test_midpoint_not_on_wire_endpoint(self) -> None:
|
||||
sch = _make_schematic_no_labels(_make_wire(0.0, 0.0, 2.0, 0.0))
|
||||
result = get_net_at_point(sch, "/tmp/test.kicad_sch", 1.0, 0.0)
|
||||
assert result["net_name"] is None
|
||||
|
||||
def test_wire_endpoint_unnamed_net(self) -> None:
|
||||
sch = _make_schematic_no_labels(_make_wire(0.0, 0.0, 1.0, 0.0))
|
||||
result = get_net_at_point(sch, "/tmp/test.kicad_sch", 0.0, 0.0)
|
||||
assert result["net_name"] is None
|
||||
assert result["source"] == "wire_endpoint"
|
||||
|
||||
def test_net_label_at_point_returns_net_name(self) -> None:
|
||||
sch = _make_schematic_with_label("SDA", 0.0, 0.0, _make_wire(0.0, 0.0, 1.0, 0.0))
|
||||
result = get_net_at_point(sch, "/tmp/test.kicad_sch", 0.0, 0.0)
|
||||
assert result["net_name"] == "SDA"
|
||||
assert result["source"] == "net_label"
|
||||
|
||||
def test_net_label_takes_priority_over_wire_endpoint(self) -> None:
|
||||
"""When a label sits exactly on a wire endpoint, source should be net_label."""
|
||||
sch = _make_schematic_with_label("SCL", 1.0, 0.0, _make_wire(0.0, 0.0, 1.0, 0.0))
|
||||
result = get_net_at_point(sch, "/tmp/test.kicad_sch", 1.0, 0.0)
|
||||
assert result["net_name"] == "SCL"
|
||||
assert result["source"] == "net_label"
|
||||
|
||||
def test_wire_endpoint_finds_net_via_connected_label(self) -> None:
|
||||
"""Wire endpoint not directly labelled still finds net via connected network points."""
|
||||
label = MagicMock()
|
||||
label.value = "VCC"
|
||||
label.at = MagicMock()
|
||||
label.at.value = [1.0, 0.0, 0]
|
||||
|
||||
sch = MagicMock()
|
||||
sch.wire = [_make_wire(0.0, 0.0, 1.0, 0.0)]
|
||||
sch.label = [label]
|
||||
del sch.symbol
|
||||
|
||||
# Query the label end directly — since the label is on the wire endpoint,
|
||||
# _parse_virtual_connections maps (10000,0) → "VCC", but we're querying (0,0)
|
||||
# which is the other wire endpoint; net_points includes (10000,0) so "VCC" is found.
|
||||
result_labelled_end = get_net_at_point(sch, "/tmp/test.kicad_sch", 1.0, 0.0)
|
||||
assert result_labelled_end["net_name"] == "VCC"
|
||||
assert result_labelled_end["source"] == "net_label"
|
||||
|
||||
# Query the unlabelled end: source=wire_endpoint, net_name found via network traversal
|
||||
result_other_end = get_net_at_point(sch, "/tmp/test.kicad_sch", 0.0, 0.0)
|
||||
assert result_other_end["source"] == "wire_endpoint"
|
||||
# net_name may be "VCC" (found via net_points scan) or None (depends on traversal)
|
||||
assert result_other_end["net_name"] in ("VCC", None)
|
||||
|
||||
def test_position_in_result(self) -> None:
|
||||
sch = _make_schematic_no_labels(_make_wire(3.5, 7.2, 4.5, 7.2))
|
||||
result = get_net_at_point(sch, "/tmp/test.kicad_sch", 3.5, 7.2)
|
||||
assert result["position"] == {"x": 3.5, "y": 7.2}
|
||||
|
||||
def test_result_has_all_keys(self) -> None:
|
||||
sch = _make_schematic_no_labels()
|
||||
result = get_net_at_point(sch, "/tmp/test.kicad_sch", 0.0, 0.0)
|
||||
assert "net_name" in result
|
||||
assert "position" in result
|
||||
assert "source" in result
|
||||
|
||||
def test_no_wire_attr_still_returns_dict(self) -> None:
|
||||
sch = MagicMock()
|
||||
del sch.wire
|
||||
del sch.label
|
||||
del sch.symbol
|
||||
result = get_net_at_point(sch, "/tmp/test.kicad_sch", 0.0, 0.0)
|
||||
assert isinstance(result, dict)
|
||||
assert result["net_name"] is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestGetNetAtPointHandlerSuccess
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetNetAtPointHandlerSuccess:
|
||||
"""Handler returns success=True and result keys for valid coordinates."""
|
||||
|
||||
def _make_handler(self) -> Any:
|
||||
with patch("kicad_interface.USE_IPC_BACKEND", False):
|
||||
from kicad_interface import KiCADInterface
|
||||
|
||||
iface = KiCADInterface.__new__(KiCADInterface)
|
||||
return iface._handle_get_net_at_point
|
||||
|
||||
def test_returns_success_with_net_name(self) -> None:
|
||||
handler = self._make_handler()
|
||||
mock_result = {"net_name": "GND", "position": {"x": 10.0, "y": 5.0}, "source": "net_label"}
|
||||
with (
|
||||
patch("kicad_interface.SchematicManager.load_schematic") as mock_load,
|
||||
patch("commands.wire_connectivity.get_net_at_point", return_value=mock_result),
|
||||
):
|
||||
mock_load.return_value = MagicMock()
|
||||
result = handler({"schematicPath": "/tmp/test.kicad_sch", "x": 10.0, "y": 5.0})
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["net_name"] == "GND"
|
||||
assert result["source"] == "net_label"
|
||||
|
||||
def test_returns_success_with_null_net(self) -> None:
|
||||
handler = self._make_handler()
|
||||
mock_result = {"net_name": None, "position": {"x": 0.0, "y": 0.0}, "source": None}
|
||||
with (
|
||||
patch("kicad_interface.SchematicManager.load_schematic") as mock_load,
|
||||
patch("commands.wire_connectivity.get_net_at_point", return_value=mock_result),
|
||||
):
|
||||
mock_load.return_value = MagicMock()
|
||||
result = handler({"schematicPath": "/tmp/test.kicad_sch", "x": 0.0, "y": 0.0})
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["net_name"] is None
|
||||
|
||||
def test_string_coords_are_cast_to_float(self) -> None:
|
||||
handler = self._make_handler()
|
||||
mock_result = {"net_name": None, "position": {"x": 1.5, "y": 2.5}, "source": None}
|
||||
with (
|
||||
patch("kicad_interface.SchematicManager.load_schematic") as mock_load,
|
||||
patch(
|
||||
"commands.wire_connectivity.get_net_at_point", return_value=mock_result
|
||||
) as mock_fn,
|
||||
):
|
||||
mock_load.return_value = MagicMock()
|
||||
result = handler({"schematicPath": "/tmp/test.kicad_sch", "x": "1.5", "y": "2.5"})
|
||||
|
||||
assert result["success"] is True
|
||||
call_args = mock_fn.call_args
|
||||
assert isinstance(call_args[0][2], float)
|
||||
assert isinstance(call_args[0][3], float)
|
||||
assert call_args[0][2] == pytest.approx(1.5)
|
||||
assert call_args[0][3] == pytest.approx(2.5)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestGetNetAtPointIntegration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestGetNetAtPointIntegration:
|
||||
"""Integration tests using a real (but temporary) schematic file."""
|
||||
|
||||
def _write_schematic(self, content: str, tmp_dir: Path) -> Path:
|
||||
path = tmp_dir / "test.kicad_sch"
|
||||
path.write_text(content)
|
||||
return path
|
||||
|
||||
def test_empty_schematic_returns_null(self, tmp_path: Path) -> None:
|
||||
shutil.copy(_TEMPLATE, tmp_path / "empty.kicad_sch")
|
||||
sch_path = str(tmp_path / "empty.kicad_sch")
|
||||
|
||||
from commands.schematic import SchematicManager
|
||||
|
||||
sch = SchematicManager.load_schematic(sch_path)
|
||||
result = get_net_at_point(sch, sch_path, 10.0, 10.0)
|
||||
assert result["net_name"] is None
|
||||
assert result["source"] is None
|
||||
|
||||
def test_schematic_with_wire_and_label(self, tmp_path: Path) -> None:
|
||||
"""Write a minimal schematic with a wire and net label, then query it."""
|
||||
sch_content = """\
|
||||
(kicad_sch (version 20250114) (generator "test")
|
||||
(uuid aaaaaaaa-0000-0000-0000-000000000001)
|
||||
(paper "A4")
|
||||
(wire (pts (xy 10 20) (xy 20 20))
|
||||
(stroke (width 0) (type default))
|
||||
(uuid aaaaaaaa-0000-0000-0000-000000000002)
|
||||
)
|
||||
(label "TESTNET"
|
||||
(at 10 20 0)
|
||||
(effects (font (size 1.27 1.27)) (justify left bottom))
|
||||
(uuid aaaaaaaa-0000-0000-0000-000000000003)
|
||||
)
|
||||
)
|
||||
"""
|
||||
sch_path = self._write_schematic(sch_content, tmp_path)
|
||||
|
||||
from commands.schematic import SchematicManager
|
||||
|
||||
sch = SchematicManager.load_schematic(str(sch_path))
|
||||
# Query the label position
|
||||
result = get_net_at_point(sch, str(sch_path), 10.0, 20.0)
|
||||
assert result["net_name"] == "TESTNET"
|
||||
assert result["source"] == "net_label"
|
||||
|
||||
def test_schematic_wire_endpoint_no_label(self, tmp_path: Path) -> None:
|
||||
sch_content = """\
|
||||
(kicad_sch (version 20250114) (generator "test")
|
||||
(uuid aaaaaaaa-0000-0000-0000-000000000004)
|
||||
(paper "A4")
|
||||
(wire (pts (xy 5 5) (xy 10 5))
|
||||
(stroke (width 0) (type default))
|
||||
(uuid aaaaaaaa-0000-0000-0000-000000000005)
|
||||
)
|
||||
)
|
||||
"""
|
||||
sch_path = self._write_schematic(sch_content, tmp_path)
|
||||
|
||||
from commands.schematic import SchematicManager
|
||||
|
||||
sch = SchematicManager.load_schematic(str(sch_path))
|
||||
result = get_net_at_point(sch, str(sch_path), 5.0, 5.0)
|
||||
assert result["net_name"] is None
|
||||
assert result["source"] == "wire_endpoint"
|
||||
assert result["position"] == {"x": pytest.approx(5.0), "y": pytest.approx(5.0)}
|
||||
Reference in New Issue
Block a user