"""Perspective scaling: map real-world cm dimensions to a pixel footprint. Uses the row-based ``Perspective`` model so an item is the correct apparent size for where it sits in the room (spec section 5, stage 4). """ from __future__ import annotations from .floor_plane import Perspective def footprint_quad( persp: Perspective, anchor_xy: tuple[float, float], width_cm: float, depth_cm: float ) -> list[list[float]]: """Floor corners (px) of a ``width_cm`` x ``depth_cm`` rectangle whose FRONT edge is centered at ``anchor_xy``. Returns 4 points ordered front-left, front-right, back-right, back-left. The back edge is both higher up the image and narrower, giving a perspective- correct trapezoid rather than a flat sprite. """ x_a, y_a = anchor_xy ppc_front = persp.ppc(y_a) w_front = width_cm * ppc_front dy = depth_cm * ppc_front * persp.foreshorten y_back = max(persp.horizon_y + 1.0, y_a - dy) w_back = width_cm * persp.ppc(y_back) return [ [x_a - w_front / 2, y_a], [x_a + w_front / 2, y_a], [x_a + w_back / 2, y_back], [x_a - w_back / 2, y_back], ] def object_height_px(persp: Perspective, anchor_y: float, height_cm: float) -> float: """Apparent vertical extent (px) of an item of ``height_cm`` at row ``anchor_y``.""" return height_cm * persp.ppc(anchor_y) def describe_scale( persp: Perspective, anchor_xy: tuple[float, float], width_cm: float, depth_cm: float, height_cm: float, ) -> str: x_a, y_a = anchor_xy ppc = persp.ppc(y_a) return ( f"floor row y={int(y_a)} -> {ppc:.2f}px/cm; " f"{width_cm}x{depth_cm}x{height_cm}cm item renders ~" f"{int(width_cm * ppc)}x{int(height_cm * ppc)}px (W*H)" )