blob: bfa0c76adc871b3e76b4451a45078a8fb8983adf [file] [edit]
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package layout
import (
"fmt"
"sort"
)
// LinearMap manages a sequence of contiguous elements stacked along one dimension.
// It provides efficient O(log N) lookup for coordinates and intersections.
//
// Internal Representation:
// It uses an N+1 boundary array where boundaries[i] is the starting offset
// of element i, and boundaries[N] is the total extent. This guarantees
// contiguity by design.
type LinearMap struct {
// boundaries stores the starting coordinates of each element, plus the end
// coordinate of the last element. len(boundaries) == N + 1.
//
// Invariant: The array is monotonically non-decreasing (boundaries[i] <= boundaries[i+1]).
// This is guaranteed because element sizes are required to be non-negative.
boundaries []int
}
// NewLinearMap constructs a map from a slice of sizes, calculating
// the N+1 boundaries automatically starting from the local origin (0).
//
// It panics if any size is negative, as this represents a logic error in
// the calling layout code.
func NewLinearMap(sizes []int) *LinearMap {
count := len(sizes)
b := make([]int, count+1)
current := 0
for i, s := range sizes {
if s < 0 {
panic(fmt.Sprintf("layout: element %d has negative size %d", i, s))
}
b[i] = current
current += s
}
b[count] = current
return &LinearMap{
boundaries: b,
}
}
// Count returns the number of elements in the map.
func (m *LinearMap) Count() int {
if len(m.boundaries) == 0 {
return 0
}
return len(m.boundaries) - 1
}
// Total returns the full horizontal or vertical extent of the map.
func (m *LinearMap) Total() int {
if len(m.boundaries) == 0 {
return 0
}
return m.boundaries[len(m.boundaries)-1]
}
// Span returns the spatial footprint of the i-th element.
func (m *LinearMap) Span(i int) Span {
if i < 0 || i >= m.Count() {
return Span{}
}
return Span{
Offset: m.boundaries[i],
Size: m.boundaries[i+1] - m.boundaries[i],
}
}
// At returns the index of the element containing the given coordinate.
//
// Example:
//
// Sizes: [10, 20, 30] -> Boundaries: [0, 10, 30, 60]
// At(5) -> 0 (Inside [0, 10))
// At(15) -> 1 (Inside [10, 30))
// At(40) -> 2 (Inside [30, 60))
//
// Returns -1 if the coordinate is out of bounds.
func (m *LinearMap) At(coord int) int {
if coord < 0 || coord >= m.Total() {
return -1
}
// Search for the largest boundary <= coord.
// We look for the first boundary that is strictly greater than coord.
idx := sort.Search(len(m.boundaries), func(i int) bool {
return m.boundaries[i] > coord
})
return idx - 1
}
// Intersects returns the range of indices that overlap with the given span.
//
// Example:
//
// Sizes: [10, 10, 10] -> Boundaries: [0, 10, 20, 30]
// Intersects({5, 10}) -> {0, 2} (Window [5, 15) overlaps items [0, 10) and [10, 20))
// Intersects({25, 5}) -> {2, 3} (Window [25, 30) overlaps item [20, 30))
// Intersects({35, 10}) -> {3, 3} (Window [35, 45) past end, empty range)
func (m *LinearMap) Intersects(window Span) Interval {
if window.IsEmpty() || m.Count() == 0 {
return Interval{0, 0}
}
// 1. Find the first element that ends after the window starts.
startIdx := sort.Search(m.Count(), func(i int) bool {
// Element i ends at boundaries[i+1].
return m.boundaries[i+1] > window.Offset
})
if startIdx == m.Count() {
return Interval{startIdx, startIdx}
}
// 2. Find the first element that starts at or after the window ends.
// All elements before this one intersect the window.
if window.IsUnbounded() {
return Interval{startIdx, m.Count()}
}
endIdx := sort.Search(m.Count(), func(i int) bool {
return m.boundaries[i] >= window.End()
})
return Interval{startIdx, endIdx}
}