mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
119 lines
3.9 KiB
Python
119 lines
3.9 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from cadref.analysis import AXIS_INDEX, bbox_center, bbox_size, major_planar_face_groups
|
|
from cadref.lookup import build_selector_index
|
|
|
|
|
|
def axis_index(axis: str) -> int:
|
|
normalized = str(axis or "").strip().lower()
|
|
if normalized not in AXIS_INDEX:
|
|
raise ValueError(f"Axis must be one of x, y, z; got {axis!r}")
|
|
return int(AXIS_INDEX[normalized])
|
|
|
|
|
|
def _coerce_bbox(bbox: object) -> dict[str, object]:
|
|
if not isinstance(bbox, dict):
|
|
raise ValueError("Expected bbox dict with min/max coordinates.")
|
|
if not isinstance(bbox.get("min"), list) or not isinstance(bbox.get("max"), list):
|
|
raise ValueError("BBox dict must include min/max coordinate lists.")
|
|
return bbox
|
|
|
|
|
|
def bbox_coordinate(bbox: object, axis: str, side: str) -> float:
|
|
normalized_bbox = _coerce_bbox(bbox)
|
|
axis_idx = axis_index(axis)
|
|
normalized_side = str(side or "").strip().lower()
|
|
if normalized_side not in {"min", "max"}:
|
|
raise ValueError(f"Side must be 'min' or 'max'; got {side!r}")
|
|
return float(normalized_bbox[normalized_side][axis_idx])
|
|
|
|
|
|
def bbox_span(bbox: object, axis: str) -> float:
|
|
size = bbox_size(_coerce_bbox(bbox))
|
|
if size is None:
|
|
raise ValueError("Failed to compute bbox span.")
|
|
return float(size[axis_index(axis)])
|
|
|
|
|
|
def assert_close(actual: float, expected: float, *, tol: float = 1e-6, label: str = "value") -> float:
|
|
actual_value = float(actual)
|
|
expected_value = float(expected)
|
|
if abs(actual_value - expected_value) > float(tol):
|
|
raise AssertionError(
|
|
f"{label} mismatch: expected {expected_value:.6f}, got {actual_value:.6f} (tol={float(tol):.6f})"
|
|
)
|
|
return actual_value
|
|
|
|
|
|
def assert_bbox_coordinate(
|
|
bbox: object,
|
|
axis: str,
|
|
side: str,
|
|
expected: float,
|
|
*,
|
|
tol: float = 1e-6,
|
|
label: str | None = None,
|
|
) -> float:
|
|
actual = bbox_coordinate(bbox, axis, side)
|
|
return assert_close(actual, expected, tol=tol, label=label or f"bbox {side} {axis}")
|
|
|
|
|
|
def assert_bbox_span(
|
|
bbox: object,
|
|
axis: str,
|
|
expected: float,
|
|
*,
|
|
tol: float = 1e-6,
|
|
label: str | None = None,
|
|
) -> float:
|
|
actual = bbox_span(bbox, axis)
|
|
return assert_close(actual, expected, tol=tol, label=label or f"bbox span {axis}")
|
|
|
|
|
|
def selector_count(manifest: dict[str, Any], selector_type: str) -> int:
|
|
stats = manifest.get("stats")
|
|
if not isinstance(stats, dict):
|
|
return 0
|
|
if selector_type == "shape":
|
|
return int(stats.get("shapeCount") or 0)
|
|
if selector_type == "face":
|
|
return int(stats.get("faceCount") or 0)
|
|
if selector_type == "edge":
|
|
return int(stats.get("edgeCount") or 0)
|
|
if selector_type == "occurrence":
|
|
return int(stats.get("occurrenceCount") or 0)
|
|
raise ValueError(f"Unsupported selector_type {selector_type!r}")
|
|
|
|
|
|
def assert_selector_count(
|
|
manifest: dict[str, Any],
|
|
selector_type: str,
|
|
expected: int,
|
|
*,
|
|
label: str | None = None,
|
|
) -> int:
|
|
actual = selector_count(manifest, selector_type)
|
|
if actual != int(expected):
|
|
raise AssertionError(f"{label or selector_type} count mismatch: expected {int(expected)}, got {actual}")
|
|
return actual
|
|
|
|
|
|
def geometry_summary_from_manifest(manifest: dict[str, Any]) -> dict[str, object]:
|
|
bbox = _coerce_bbox(manifest.get("bbox"))
|
|
summary: dict[str, object] = {
|
|
"bbox": bbox,
|
|
"center": bbox_center(bbox),
|
|
"size": bbox_size(bbox),
|
|
"occurrenceCount": selector_count(manifest, "occurrence"),
|
|
"shapeCount": selector_count(manifest, "shape"),
|
|
"faceCount": selector_count(manifest, "face"),
|
|
"edgeCount": selector_count(manifest, "edge"),
|
|
}
|
|
try:
|
|
index = build_selector_index(manifest)
|
|
except Exception:
|
|
return summary
|
|
summary["majorPlanes"] = major_planar_face_groups(index)
|
|
return summary
|