File size: 3,949 Bytes
9f084b2
 
 
 
 
 
 
 
bbbfba8
9f084b2
bbbfba8
 
 
9f084b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
"""Coordinate transformations — convert between provider space and canonical space.

All transformations are explicit and documented.  No implicit heavy conversion
is allowed in serializers (see AGENTS.md rule §5).
"""

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from src.app.geometry.bbox import BBoxTuple
    from src.app.geometry.polygon import PolygonPoints


def rescale_bbox(bbox: BBoxTuple, factor: float) -> BBoxTuple:
    """Scale a bbox by a multiplicative factor.

    Used when the provider received a resized image.
    factor > 1 = upscale, factor < 1 = downscale.
    """
    if factor <= 0:
        raise ValueError(f"Scale factor must be > 0, got {factor}")
    return (
        bbox[0] * factor,
        bbox[1] * factor,
        bbox[2] * factor,
        bbox[3] * factor,
    )


def rescale_polygon(polygon: PolygonPoints, factor: float) -> PolygonPoints:
    """Scale all polygon points by a multiplicative factor."""
    if factor <= 0:
        raise ValueError(f"Scale factor must be > 0, got {factor}")
    return [(x * factor, y * factor) for x, y in polygon]


def rescale_point(point: tuple[float, float], factor: float) -> tuple[float, float]:
    """Scale a single point by a factor."""
    if factor <= 0:
        raise ValueError(f"Scale factor must be > 0, got {factor}")
    return (point[0] * factor, point[1] * factor)


def clip_bbox_to_page(
    bbox: BBoxTuple, page_width: float, page_height: float
) -> BBoxTuple:
    """Clip a bbox so it fits within page boundaries.

    Clamps x, y to [0, page_width/height] and adjusts width/height.
    Returns a bbox with width/height >= 1 (avoids degenerate boxes).
    """
    x, y, w, h = bbox
    # Clamp top-left
    cx = max(0.0, min(x, page_width - 1))
    cy = max(0.0, min(y, page_height - 1))
    # Clamp bottom-right
    cx2 = max(cx + 1, min(x + w, page_width))
    cy2 = max(cy + 1, min(y + h, page_height))
    return (cx, cy, cx2 - cx, cy2 - cy)


def clip_polygon_to_page(
    polygon: PolygonPoints, page_width: float, page_height: float
) -> PolygonPoints:
    """Clamp all polygon points to page boundaries."""
    return [
        (max(0.0, min(x, page_width)), max(0.0, min(y, page_height)))
        for x, y in polygon
    ]


def rotate_bbox_90(
    bbox: BBoxTuple, page_width: float, page_height: float, times: int = 1
) -> BBoxTuple:
    """Rotate a bbox by 90° clockwise around the page center.

    *times* = number of 90° rotations (1, 2, 3).
    page_width/height are the dimensions BEFORE rotation.
    """
    times = times % 4
    x, y, w, h = bbox
    for _ in range(times):
        # 90° clockwise: (x, y) -> (page_h - y - h, x)
        new_x = page_height - y - h
        new_y = x
        x, y, w, h = new_x, new_y, h, w
        # Swap page dimensions for next iteration
        page_width, page_height = page_height, page_width
    return (x, y, w, h)


def rotate_point_90(
    point: tuple[float, float], page_width: float, page_height: float, times: int = 1
) -> tuple[float, float]:
    """Rotate a point by 90° clockwise around the page origin."""
    times = times % 4
    x, y = point
    for _ in range(times):
        x, y = page_height - y, x
        page_width, page_height = page_height, page_width
    return (x, y)


def rotate_polygon_90(
    polygon: PolygonPoints, page_width: float, page_height: float, times: int = 1
) -> PolygonPoints:
    """Rotate all polygon points by 90° clockwise."""
    return [
        rotate_point_90(p, page_width, page_height, times) for p in polygon
    ]


def translate_bbox(bbox: BBoxTuple, dx: float, dy: float) -> BBoxTuple:
    """Translate a bbox by (dx, dy) pixels."""
    return (bbox[0] + dx, bbox[1] + dy, bbox[2], bbox[3])


def translate_polygon(polygon: PolygonPoints, dx: float, dy: float) -> PolygonPoints:
    """Translate all polygon points by (dx, dy)."""
    return [(x + dx, y + dy) for x, y in polygon]