Feat: add bounding box and courtyard info to component queries (#163)
Components now include boundingBox (min/max X/Y, width, height) in get_component_list and get_component_properties responses. The SWIG get_component_properties also includes courtyard dimensions when the footprint defines a courtyard layer. SWIG backend uses GetBoundingBox() and GetCourtyard(). IPC backend tries get_item_bounding_box(), then pad-extent fallback, then falls back to SWIG backend data when available. This ensures bounding box data is present regardless of which backend handles the query. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
119
tests/test_component_bbox_courtyard.py
Normal file
119
tests/test_component_bbox_courtyard.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""Tests for bounding-box / courtyard data in component queries (SWIG path).
|
||||
|
||||
``get_component_properties`` and ``get_component_list`` now include a
|
||||
``boundingBox`` field built from ``module.GetBoundingBox()`` and (for the
|
||||
single-component query) a ``courtyard`` field built from
|
||||
``module.GetCourtyard()`` against ``F_CrtYd``/``B_CrtYd``.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "python"))
|
||||
|
||||
|
||||
def _bbox(left_nm, top_nm, right_nm, bottom_nm):
|
||||
bb = MagicMock()
|
||||
bb.GetLeft.return_value = left_nm
|
||||
bb.GetTop.return_value = top_nm
|
||||
bb.GetRight.return_value = right_nm
|
||||
bb.GetBottom.return_value = bottom_nm
|
||||
return bb
|
||||
|
||||
|
||||
def _make_module(*, ref="U1", value="LM317", with_courtyard=False):
|
||||
pos = MagicMock()
|
||||
pos.x = 10_000_000 # 10 mm
|
||||
pos.y = 20_000_000 # 20 mm
|
||||
|
||||
module = MagicMock()
|
||||
module.GetPosition.return_value = pos
|
||||
module.GetReference.return_value = ref
|
||||
module.GetValue.return_value = value
|
||||
module.GetFPIDAsString.return_value = "Lib:Footprint"
|
||||
orientation = MagicMock()
|
||||
orientation.AsDegrees.return_value = 0
|
||||
module.GetOrientation.return_value = orientation
|
||||
module.GetLayer.return_value = 0
|
||||
module.GetAttributes.return_value = 0
|
||||
module.GetBoundingBox.return_value = _bbox(0, 0, 5_000_000, 3_000_000) # 5 x 3 mm
|
||||
|
||||
courtyard_outline = MagicMock()
|
||||
if with_courtyard:
|
||||
courtyard_outline.OutlineCount.return_value = 1
|
||||
courtyard_outline.BBox.return_value = _bbox(-500_000, -500_000, 5_500_000, 3_500_000)
|
||||
else:
|
||||
courtyard_outline.OutlineCount.return_value = 0
|
||||
module.GetCourtyard.return_value = courtyard_outline
|
||||
|
||||
return module
|
||||
|
||||
|
||||
def _make_board(modules):
|
||||
board = MagicMock()
|
||||
board.GetLayerName.return_value = "F.Cu"
|
||||
board.GetFootprints.return_value = list(modules)
|
||||
if modules:
|
||||
board.FindFootprintByReference.return_value = modules[0]
|
||||
else:
|
||||
board.FindFootprintByReference.return_value = None
|
||||
return board
|
||||
|
||||
|
||||
def test_get_component_properties_returns_bounding_box():
|
||||
from commands.component import ComponentCommands
|
||||
|
||||
module = _make_module()
|
||||
cmd = ComponentCommands(board=_make_board([module]))
|
||||
result = cmd.get_component_properties({"reference": "U1"})
|
||||
|
||||
assert result["success"] is True
|
||||
bb = result["component"]["boundingBox"]
|
||||
assert bb["min_x"] == 0.0
|
||||
assert bb["min_y"] == 0.0
|
||||
assert bb["max_x"] == 5.0
|
||||
assert bb["max_y"] == 3.0
|
||||
assert bb["width"] == 5.0
|
||||
assert bb["height"] == 3.0
|
||||
assert bb["unit"] == "mm"
|
||||
|
||||
|
||||
def test_get_component_properties_returns_courtyard_when_present():
|
||||
from commands.component import ComponentCommands
|
||||
|
||||
module = _make_module(with_courtyard=True)
|
||||
cmd = ComponentCommands(board=_make_board([module]))
|
||||
result = cmd.get_component_properties({"reference": "U1"})
|
||||
|
||||
courtyard = result["component"]["courtyard"]
|
||||
assert courtyard is not None
|
||||
assert courtyard["min_x"] == -0.5
|
||||
assert courtyard["max_x"] == 5.5
|
||||
assert courtyard["unit"] == "mm"
|
||||
|
||||
|
||||
def test_get_component_properties_courtyard_none_when_absent():
|
||||
from commands.component import ComponentCommands
|
||||
|
||||
module = _make_module(with_courtyard=False)
|
||||
cmd = ComponentCommands(board=_make_board([module]))
|
||||
result = cmd.get_component_properties({"reference": "U1"})
|
||||
|
||||
assert result["component"]["courtyard"] is None
|
||||
|
||||
|
||||
def test_get_component_list_includes_bounding_box():
|
||||
from commands.component import ComponentCommands
|
||||
|
||||
modules = [_make_module(ref="R1"), _make_module(ref="R2")]
|
||||
cmd = ComponentCommands(board=_make_board(modules))
|
||||
result = cmd.get_component_list({})
|
||||
|
||||
assert result["success"] is True
|
||||
assert len(result["components"]) == 2
|
||||
for comp in result["components"]:
|
||||
assert "boundingBox" in comp
|
||||
assert comp["boundingBox"]["unit"] == "mm"
|
||||
Reference in New Issue
Block a user