1package graphics
2
3import "fmt"
4
5const (
6 maxInt64 = int64(^uint64(0) >> 1)
7 minInt64 = -maxInt64 - 1
8)
9
10// PixelRect is a half-open rectangle in pixel coordinates. Width and Height
11// must be non-negative, and the exclusive right and bottom edges must fit in
12// int64. Coordinates may be negative so that scenes can be clipped without
13// first translating them into a viewport.
14type PixelRect struct {
15 X, Y int64
16 Width, Height int64
17}
18
19// CellRect is a half-open rectangle in cell coordinates. It has the same
20// overflow rules as PixelRect but is kept as a distinct type so that pixel
21// and cell coordinates cannot be accidentally mixed.
22type CellRect struct {
23 X, Y int64
24 Width, Height int64
25}
26
27type rect struct {
28 x, y int64
29 width, height int64
30}
31
32func pixelRect(r PixelRect) rect { return rect{x: r.X, y: r.Y, width: r.Width, height: r.Height} }
33func cellRect(r CellRect) rect { return rect{x: r.X, y: r.Y, width: r.Width, height: r.Height} }
34
35// NewPixelRect constructs a checked pixel rectangle.
36func NewPixelRect(x, y, width, height int64) (PixelRect, error) {
37 r := PixelRect{X: x, Y: y, Width: width, Height: height}
38 if !r.Valid() {
39 return PixelRect{}, fmt.Errorf("pixel rectangle: %w", ErrInvalidRect)
40 }
41 return r, nil
42}
43
44// NewCellRect constructs a checked cell rectangle.
45func NewCellRect(x, y, width, height int64) (CellRect, error) {
46 r := CellRect{X: x, Y: y, Width: width, Height: height}
47 if !r.Valid() {
48 return CellRect{}, fmt.Errorf("cell rectangle: %w", ErrInvalidRect)
49 }
50 return r, nil
51}
52
53func (r rect) valid() bool {
54 return r.width >= 0 && r.height >= 0 && edge(r.x, r.width) && edge(r.y, r.height)
55}
56
57func (r rect) empty() bool { return r.width == 0 || r.height == 0 }
58
59func (r rect) right() (int64, bool) { return checkedEdge(r.x, r.width) }
60
61func (r rect) bottom() (int64, bool) { return checkedEdge(r.y, r.height) }
62
63func (r rect) contains(x, y int64) bool {
64 right, okRight := r.right()
65 bottom, okBottom := r.bottom()
66 return okRight && okBottom && x >= r.x && x < right && y >= r.y && y < bottom
67}
68
69func (r rect) intersect(other rect) (rect, bool) {
70 if !r.valid() || !other.valid() || r.empty() || other.empty() {
71 return rect{}, false
72 }
73 right, _ := r.right()
74 otherRight, _ := other.right()
75 bottom, _ := r.bottom()
76 otherBottom, _ := other.bottom()
77 left := max(r.x, other.x)
78 top := max(r.y, other.y)
79 right = min(right, otherRight)
80 bottom = min(bottom, otherBottom)
81 if right <= left || bottom <= top {
82 return rect{}, false
83 }
84 return rect{x: left, y: top, width: right - left, height: bottom - top}, true
85}
86
87// Valid reports whether the rectangle's dimensions and exclusive edges are
88// representable without signed overflow.
89func (r PixelRect) Valid() bool { return pixelRect(r).valid() }
90
91// Valid reports whether the rectangle's dimensions and exclusive edges are
92// representable without signed overflow.
93func (r CellRect) Valid() bool { return cellRect(r).valid() }
94
95// Empty reports whether the rectangle has no area.
96func (r PixelRect) Empty() bool { return pixelRect(r).empty() }
97
98// Empty reports whether the rectangle has no area.
99func (r CellRect) Empty() bool { return cellRect(r).empty() }
100
101// Right returns the exclusive right edge. The boolean is false for an
102// invalid rectangle; it is never produced by arithmetic that wraps.
103func (r PixelRect) Right() (int64, bool) { return pixelRect(r).right() }
104
105// Bottom returns the exclusive bottom edge. The boolean is false for an
106// invalid rectangle; it is never produced by arithmetic that wraps.
107func (r PixelRect) Bottom() (int64, bool) { return pixelRect(r).bottom() }
108
109// Right returns the exclusive right edge. The boolean is false for an
110// invalid rectangle; it is never produced by arithmetic that wraps.
111func (r CellRect) Right() (int64, bool) { return cellRect(r).right() }
112
113// Bottom returns the exclusive bottom edge. The boolean is false for an
114// invalid rectangle; it is never produced by arithmetic that wraps.
115func (r CellRect) Bottom() (int64, bool) { return cellRect(r).bottom() }
116
117// Contains reports whether a point is inside the half-open rectangle.
118func (r PixelRect) Contains(x, y int64) bool { return pixelRect(r).contains(x, y) }
119
120// Contains reports whether a point is inside the half-open rectangle.
121func (r CellRect) Contains(x, y int64) bool { return cellRect(r).contains(x, y) }
122
123// Intersect returns the non-empty intersection of two rectangles. Invalid or
124// empty rectangles do not intersect. The result is safe even when the input
125// edges are close to the int64 limits.
126func (r PixelRect) Intersect(other PixelRect) (PixelRect, bool) {
127 intersect, ok := pixelRect(r).intersect(pixelRect(other))
128 return PixelRect{X: intersect.x, Y: intersect.y, Width: intersect.width, Height: intersect.height}, ok
129}
130
131// Intersect returns the non-empty intersection of two rectangles.
132func (r CellRect) Intersect(other CellRect) (CellRect, bool) {
133 intersect, ok := cellRect(r).intersect(cellRect(other))
134 return CellRect{X: intersect.x, Y: intersect.y, Width: intersect.width, Height: intersect.height}, ok
135}
136
137func edge(start, length int64) bool {
138 _, ok := checkedEdge(start, length)
139 return ok
140}
141
142func checkedEdge(start, length int64) (int64, bool) {
143 if length < 0 {
144 return 0, false
145 }
146 // For a non-negative length, a negative start cannot overflow the
147 // positive edge: even MinInt64 + MaxInt64 is -1. Avoid computing
148 // maxInt64-start in that case because the subtraction itself could wrap.
149 if start >= 0 && length > maxInt64-start {
150 return 0, false
151 }
152 return start + length, true
153}