refactor(graphics): share rectangle arithmetic
- commit
- 251cbdf88c66a7952b5405a45e150782ca802bf6
- author
- brice · · 2026-08-23 16:17 UTC
- parent
- 78f404d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +56 -52
M graphics/rect.go +56 -52
| @@ -24,6 +24,14 @@ type CellRect struct { | |||
| 24 | 24 | Width, Height int64 | |
| 25 | 25 | } | |
| 26 | 26 | ||
| 27 | + | type rect struct { | |
| 28 | + | x, y int64 | |
| 29 | + | width, height int64 | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | func pixelRect(r PixelRect) rect { return rect{x: r.X, y: r.Y, width: r.Width, height: r.Height} } | |
| 33 | + | func cellRect(r CellRect) rect { return rect{x: r.X, y: r.Y, width: r.Width, height: r.Height} } | |
| 34 | + | ||
| 27 | 35 | // NewPixelRect constructs a checked pixel rectangle. | |
| 28 | 36 | func NewPixelRect(x, y, width, height int64) (PixelRect, error) { | |
| 29 | 37 | r := PixelRect{X: x, Y: y, Width: width, Height: height} | |
| @@ -42,92 +50,88 @@ func NewCellRect(x, y, width, height int64) (CellRect, error) { | |||
| 42 | 50 | return r, nil | |
| 43 | 51 | } | |
| 44 | 52 | ||
| 53 | + | func (r rect) valid() bool { | |
| 54 | + | return r.width >= 0 && r.height >= 0 && edge(r.x, r.width) && edge(r.y, r.height) | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | func (r rect) empty() bool { return r.width == 0 || r.height == 0 } | |
| 58 | + | ||
| 59 | + | func (r rect) right() (int64, bool) { return checkedEdge(r.x, r.width) } | |
| 60 | + | ||
| 61 | + | func (r rect) bottom() (int64, bool) { return checkedEdge(r.y, r.height) } | |
| 62 | + | ||
| 63 | + | func (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 | + | ||
| 69 | + | func (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 | + | ||
| 45 | 87 | // Valid reports whether the rectangle's dimensions and exclusive edges are | |
| 46 | 88 | // representable without signed overflow. | |
| 47 | - | func (r PixelRect) Valid() bool { | |
| 48 | - | return r.Width >= 0 && r.Height >= 0 && edge(r.X, r.Width) && edge(r.Y, r.Height) | |
| 49 | - | } | |
| 89 | + | func (r PixelRect) Valid() bool { return pixelRect(r).valid() } | |
| 50 | 90 | ||
| 51 | 91 | // Valid reports whether the rectangle's dimensions and exclusive edges are | |
| 52 | 92 | // representable without signed overflow. | |
| 53 | - | func (r CellRect) Valid() bool { | |
| 54 | - | return r.Width >= 0 && r.Height >= 0 && edge(r.X, r.Width) && edge(r.Y, r.Height) | |
| 55 | - | } | |
| 93 | + | func (r CellRect) Valid() bool { return cellRect(r).valid() } | |
| 56 | 94 | ||
| 57 | 95 | // Empty reports whether the rectangle has no area. | |
| 58 | - | func (r PixelRect) Empty() bool { return r.Width == 0 || r.Height == 0 } | |
| 96 | + | func (r PixelRect) Empty() bool { return pixelRect(r).empty() } | |
| 59 | 97 | ||
| 60 | 98 | // Empty reports whether the rectangle has no area. | |
| 61 | - | func (r CellRect) Empty() bool { return r.Width == 0 || r.Height == 0 } | |
| 99 | + | func (r CellRect) Empty() bool { return cellRect(r).empty() } | |
| 62 | 100 | ||
| 63 | 101 | // Right returns the exclusive right edge. The boolean is false for an | |
| 64 | 102 | // invalid rectangle; it is never produced by arithmetic that wraps. | |
| 65 | - | func (r PixelRect) Right() (int64, bool) { return checkedEdge(r.X, r.Width) } | |
| 103 | + | func (r PixelRect) Right() (int64, bool) { return pixelRect(r).right() } | |
| 66 | 104 | ||
| 67 | 105 | // Bottom returns the exclusive bottom edge. The boolean is false for an | |
| 68 | 106 | // invalid rectangle; it is never produced by arithmetic that wraps. | |
| 69 | - | func (r PixelRect) Bottom() (int64, bool) { return checkedEdge(r.Y, r.Height) } | |
| 107 | + | func (r PixelRect) Bottom() (int64, bool) { return pixelRect(r).bottom() } | |
| 70 | 108 | ||
| 71 | 109 | // Right returns the exclusive right edge. The boolean is false for an | |
| 72 | 110 | // invalid rectangle; it is never produced by arithmetic that wraps. | |
| 73 | - | func (r CellRect) Right() (int64, bool) { return checkedEdge(r.X, r.Width) } | |
| 111 | + | func (r CellRect) Right() (int64, bool) { return cellRect(r).right() } | |
| 74 | 112 | ||
| 75 | 113 | // Bottom returns the exclusive bottom edge. The boolean is false for an | |
| 76 | 114 | // invalid rectangle; it is never produced by arithmetic that wraps. | |
| 77 | - | func (r CellRect) Bottom() (int64, bool) { return checkedEdge(r.Y, r.Height) } | |
| 115 | + | func (r CellRect) Bottom() (int64, bool) { return cellRect(r).bottom() } | |
| 78 | 116 | ||
| 79 | 117 | // Contains reports whether a point is inside the half-open rectangle. | |
| 80 | - | func (r PixelRect) Contains(x, y int64) bool { | |
| 81 | - | right, okRight := r.Right() | |
| 82 | - | bottom, okBottom := r.Bottom() | |
| 83 | - | return okRight && okBottom && x >= r.X && x < right && y >= r.Y && y < bottom | |
| 84 | - | } | |
| 118 | + | func (r PixelRect) Contains(x, y int64) bool { return pixelRect(r).contains(x, y) } | |
| 85 | 119 | ||
| 86 | 120 | // Contains reports whether a point is inside the half-open rectangle. | |
| 87 | - | func (r CellRect) Contains(x, y int64) bool { | |
| 88 | - | right, okRight := r.Right() | |
| 89 | - | bottom, okBottom := r.Bottom() | |
| 90 | - | return okRight && okBottom && x >= r.X && x < right && y >= r.Y && y < bottom | |
| 91 | - | } | |
| 121 | + | func (r CellRect) Contains(x, y int64) bool { return cellRect(r).contains(x, y) } | |
| 92 | 122 | ||
| 93 | 123 | // Intersect returns the non-empty intersection of two rectangles. Invalid or | |
| 94 | 124 | // empty rectangles do not intersect. The result is safe even when the input | |
| 95 | 125 | // edges are close to the int64 limits. | |
| 96 | 126 | func (r PixelRect) Intersect(other PixelRect) (PixelRect, bool) { | |
| 97 | - | if !r.Valid() || !other.Valid() || r.Empty() || other.Empty() { | |
| 98 | - | return PixelRect{}, false | |
| 99 | - | } | |
| 100 | - | right, _ := r.Right() | |
| 101 | - | otherRight, _ := other.Right() | |
| 102 | - | bottom, _ := r.Bottom() | |
| 103 | - | otherBottom, _ := other.Bottom() | |
| 104 | - | left := max(r.X, other.X) | |
| 105 | - | top := max(r.Y, other.Y) | |
| 106 | - | right = min(right, otherRight) | |
| 107 | - | bottom = min(bottom, otherBottom) | |
| 108 | - | if right <= left || bottom <= top { | |
| 109 | - | return PixelRect{}, false | |
| 110 | - | } | |
| 111 | - | return PixelRect{X: left, Y: top, Width: right - left, Height: bottom - top}, true | |
| 127 | + | intersect, ok := pixelRect(r).intersect(pixelRect(other)) | |
| 128 | + | return PixelRect{X: intersect.x, Y: intersect.y, Width: intersect.width, Height: intersect.height}, ok | |
| 112 | 129 | } | |
| 113 | 130 | ||
| 114 | 131 | // Intersect returns the non-empty intersection of two rectangles. | |
| 115 | 132 | func (r CellRect) Intersect(other CellRect) (CellRect, bool) { | |
| 116 | - | if !r.Valid() || !other.Valid() || r.Empty() || other.Empty() { | |
| 117 | - | return CellRect{}, false | |
| 118 | - | } | |
| 119 | - | right, _ := r.Right() | |
| 120 | - | otherRight, _ := other.Right() | |
| 121 | - | bottom, _ := r.Bottom() | |
| 122 | - | otherBottom, _ := other.Bottom() | |
| 123 | - | left := max(r.X, other.X) | |
| 124 | - | top := max(r.Y, other.Y) | |
| 125 | - | right = min(right, otherRight) | |
| 126 | - | bottom = min(bottom, otherBottom) | |
| 127 | - | if right <= left || bottom <= top { | |
| 128 | - | return CellRect{}, false | |
| 129 | - | } | |
| 130 | - | return CellRect{X: left, Y: top, Width: right - left, Height: bottom - top}, true | |
| 133 | + | intersect, ok := cellRect(r).intersect(cellRect(other)) | |
| 134 | + | return CellRect{X: intersect.x, Y: intersect.y, Width: intersect.width, Height: intersect.height}, ok | |
| 131 | 135 | } | |
| 132 | 136 | ||
| 133 | 137 | func edge(start, length int64) bool { | |
| @@ -24,6 +24,14 @@ type CellRect struct { | |||||
| 24 | Width, Height int64 | 24 | Width, Height int64 | ||
| 25 | } | 25 | } | ||
| 26 | 26 | ||||
| 27 | + | type rect struct { | |||
| 28 | + | x, y int64 | |||
| 29 | + | width, height int64 | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | func pixelRect(r PixelRect) rect { return rect{x: r.X, y: r.Y, width: r.Width, height: r.Height} } | |||
| 33 | + | func cellRect(r CellRect) rect { return rect{x: r.X, y: r.Y, width: r.Width, height: r.Height} } | |||
| 34 | + | ||||
| 27 | // NewPixelRect constructs a checked pixel rectangle. | 35 | // NewPixelRect constructs a checked pixel rectangle. | ||
| 28 | func NewPixelRect(x, y, width, height int64) (PixelRect, error) { | 36 | func NewPixelRect(x, y, width, height int64) (PixelRect, error) { | ||
| 29 | r := PixelRect{X: x, Y: y, Width: width, Height: height} | 37 | r := PixelRect{X: x, Y: y, Width: width, Height: height} | ||
| @@ -42,92 +50,88 @@ func NewCellRect(x, y, width, height int64) (CellRect, error) { | |||||
| 42 | return r, nil | 50 | return r, nil | ||
| 43 | } | 51 | } | ||
| 44 | 52 | ||||
| 53 | + | func (r rect) valid() bool { | |||
| 54 | + | return r.width >= 0 && r.height >= 0 && edge(r.x, r.width) && edge(r.y, r.height) | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | func (r rect) empty() bool { return r.width == 0 || r.height == 0 } | |||
| 58 | + | ||||
| 59 | + | func (r rect) right() (int64, bool) { return checkedEdge(r.x, r.width) } | |||
| 60 | + | ||||
| 61 | + | func (r rect) bottom() (int64, bool) { return checkedEdge(r.y, r.height) } | |||
| 62 | + | ||||
| 63 | + | func (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 | + | ||||
| 69 | + | func (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 | + | ||||
| 45 | // Valid reports whether the rectangle's dimensions and exclusive edges are | 87 | // Valid reports whether the rectangle's dimensions and exclusive edges are | ||
| 46 | // representable without signed overflow. | 88 | // representable without signed overflow. | ||
| 47 | - | func (r PixelRect) Valid() bool { | 89 | + | func (r PixelRect) Valid() bool { return pixelRect(r).valid() } |
| 48 | - | return r.Width >= 0 && r.Height >= 0 && edge(r.X, r.Width) && edge(r.Y, r.Height) | |||
| 49 | - | } | |||
| 50 | 90 | ||||
| 51 | // Valid reports whether the rectangle's dimensions and exclusive edges are | 91 | // Valid reports whether the rectangle's dimensions and exclusive edges are | ||
| 52 | // representable without signed overflow. | 92 | // representable without signed overflow. | ||
| 53 | - | func (r CellRect) Valid() bool { | 93 | + | func (r CellRect) Valid() bool { return cellRect(r).valid() } |
| 54 | - | return r.Width >= 0 && r.Height >= 0 && edge(r.X, r.Width) && edge(r.Y, r.Height) | |||
| 55 | - | } | |||
| 56 | 94 | ||||
| 57 | // Empty reports whether the rectangle has no area. | 95 | // Empty reports whether the rectangle has no area. | ||
| 58 | - | func (r PixelRect) Empty() bool { return r.Width == 0 || r.Height == 0 } | 96 | + | func (r PixelRect) Empty() bool { return pixelRect(r).empty() } |
| 59 | 97 | ||||
| 60 | // Empty reports whether the rectangle has no area. | 98 | // Empty reports whether the rectangle has no area. | ||
| 61 | - | func (r CellRect) Empty() bool { return r.Width == 0 || r.Height == 0 } | 99 | + | func (r CellRect) Empty() bool { return cellRect(r).empty() } |
| 62 | 100 | ||||
| 63 | // Right returns the exclusive right edge. The boolean is false for an | 101 | // Right returns the exclusive right edge. The boolean is false for an | ||
| 64 | // invalid rectangle; it is never produced by arithmetic that wraps. | 102 | // invalid rectangle; it is never produced by arithmetic that wraps. | ||
| 65 | - | func (r PixelRect) Right() (int64, bool) { return checkedEdge(r.X, r.Width) } | 103 | + | func (r PixelRect) Right() (int64, bool) { return pixelRect(r).right() } |
| 66 | 104 | ||||
| 67 | // Bottom returns the exclusive bottom edge. The boolean is false for an | 105 | // Bottom returns the exclusive bottom edge. The boolean is false for an | ||
| 68 | // invalid rectangle; it is never produced by arithmetic that wraps. | 106 | // invalid rectangle; it is never produced by arithmetic that wraps. | ||
| 69 | - | func (r PixelRect) Bottom() (int64, bool) { return checkedEdge(r.Y, r.Height) } | 107 | + | func (r PixelRect) Bottom() (int64, bool) { return pixelRect(r).bottom() } |
| 70 | 108 | ||||
| 71 | // Right returns the exclusive right edge. The boolean is false for an | 109 | // Right returns the exclusive right edge. The boolean is false for an | ||
| 72 | // invalid rectangle; it is never produced by arithmetic that wraps. | 110 | // invalid rectangle; it is never produced by arithmetic that wraps. | ||
| 73 | - | func (r CellRect) Right() (int64, bool) { return checkedEdge(r.X, r.Width) } | 111 | + | func (r CellRect) Right() (int64, bool) { return cellRect(r).right() } |
| 74 | 112 | ||||
| 75 | // Bottom returns the exclusive bottom edge. The boolean is false for an | 113 | // Bottom returns the exclusive bottom edge. The boolean is false for an | ||
| 76 | // invalid rectangle; it is never produced by arithmetic that wraps. | 114 | // invalid rectangle; it is never produced by arithmetic that wraps. | ||
| 77 | - | func (r CellRect) Bottom() (int64, bool) { return checkedEdge(r.Y, r.Height) } | 115 | + | func (r CellRect) Bottom() (int64, bool) { return cellRect(r).bottom() } |
| 78 | 116 | ||||
| 79 | // Contains reports whether a point is inside the half-open rectangle. | 117 | // Contains reports whether a point is inside the half-open rectangle. | ||
| 80 | - | func (r PixelRect) Contains(x, y int64) bool { | 118 | + | func (r PixelRect) Contains(x, y int64) bool { return pixelRect(r).contains(x, y) } |
| 81 | - | right, okRight := r.Right() | |||
| 82 | - | bottom, okBottom := r.Bottom() | |||
| 83 | - | return okRight && okBottom && x >= r.X && x < right && y >= r.Y && y < bottom | |||
| 84 | - | } | |||
| 85 | 119 | ||||
| 86 | // Contains reports whether a point is inside the half-open rectangle. | 120 | // Contains reports whether a point is inside the half-open rectangle. | ||
| 87 | - | func (r CellRect) Contains(x, y int64) bool { | 121 | + | func (r CellRect) Contains(x, y int64) bool { return cellRect(r).contains(x, y) } |
| 88 | - | right, okRight := r.Right() | |||
| 89 | - | bottom, okBottom := r.Bottom() | |||
| 90 | - | return okRight && okBottom && x >= r.X && x < right && y >= r.Y && y < bottom | |||
| 91 | - | } | |||
| 92 | 122 | ||||
| 93 | // Intersect returns the non-empty intersection of two rectangles. Invalid or | 123 | // Intersect returns the non-empty intersection of two rectangles. Invalid or | ||
| 94 | // empty rectangles do not intersect. The result is safe even when the input | 124 | // empty rectangles do not intersect. The result is safe even when the input | ||
| 95 | // edges are close to the int64 limits. | 125 | // edges are close to the int64 limits. | ||
| 96 | func (r PixelRect) Intersect(other PixelRect) (PixelRect, bool) { | 126 | func (r PixelRect) Intersect(other PixelRect) (PixelRect, bool) { | ||
| 97 | - | if !r.Valid() || !other.Valid() || r.Empty() || other.Empty() { | 127 | + | intersect, ok := pixelRect(r).intersect(pixelRect(other)) |
| 98 | - | return PixelRect{}, false | 128 | + | return PixelRect{X: intersect.x, Y: intersect.y, Width: intersect.width, Height: intersect.height}, ok |
| 99 | - | } | |||
| 100 | - | right, _ := r.Right() | |||
| 101 | - | otherRight, _ := other.Right() | |||
| 102 | - | bottom, _ := r.Bottom() | |||
| 103 | - | otherBottom, _ := other.Bottom() | |||
| 104 | - | left := max(r.X, other.X) | |||
| 105 | - | top := max(r.Y, other.Y) | |||
| 106 | - | right = min(right, otherRight) | |||
| 107 | - | bottom = min(bottom, otherBottom) | |||
| 108 | - | if right <= left || bottom <= top { | |||
| 109 | - | return PixelRect{}, false | |||
| 110 | - | } | |||
| 111 | - | return PixelRect{X: left, Y: top, Width: right - left, Height: bottom - top}, true | |||
| 112 | } | 129 | } | ||
| 113 | 130 | ||||
| 114 | // Intersect returns the non-empty intersection of two rectangles. | 131 | // Intersect returns the non-empty intersection of two rectangles. | ||
| 115 | func (r CellRect) Intersect(other CellRect) (CellRect, bool) { | 132 | func (r CellRect) Intersect(other CellRect) (CellRect, bool) { | ||
| 116 | - | if !r.Valid() || !other.Valid() || r.Empty() || other.Empty() { | 133 | + | intersect, ok := cellRect(r).intersect(cellRect(other)) |
| 117 | - | return CellRect{}, false | 134 | + | return CellRect{X: intersect.x, Y: intersect.y, Width: intersect.width, Height: intersect.height}, ok |
| 118 | - | } | |||
| 119 | - | right, _ := r.Right() | |||
| 120 | - | otherRight, _ := other.Right() | |||
| 121 | - | bottom, _ := r.Bottom() | |||
| 122 | - | otherBottom, _ := other.Bottom() | |||
| 123 | - | left := max(r.X, other.X) | |||
| 124 | - | top := max(r.Y, other.Y) | |||
| 125 | - | right = min(right, otherRight) | |||
| 126 | - | bottom = min(bottom, otherBottom) | |||
| 127 | - | if right <= left || bottom <= top { | |||
| 128 | - | return CellRect{}, false | |||
| 129 | - | } | |||
| 130 | - | return CellRect{X: left, Y: top, Width: right - left, Height: bottom - top}, true | |||
| 131 | } | 135 | } | ||
| 132 | 136 | ||||
| 133 | func edge(start, length int64) bool { | 137 | func edge(start, length int64) bool { | ||