Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 75 additions & 29 deletions site/components/InteractiveGraphics/InteractiveGraphics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SuperGrid } from "react-supergrid"
import { getGraphicsBounds } from "site/utils/getGraphicsBounds"
import { getMaxStep } from "site/utils/getMaxStep"
import { sortRectsByArea } from "site/utils/sortRectsByArea"
import { applyObjectLimit } from "site/utils/applyObjectLimit"
import {
applyToPoint,
compose,
Expand Down Expand Up @@ -421,56 +422,56 @@ export const InteractiveGraphics = ({
filterLayerAndStep,
})

const filterAndLimit = <T,>(
const filterOnly = <T,>(
objects: T[] | undefined,
filterFn: (obj: T) => boolean,
): (T & { originalIndex: number })[] => {
if (!objects) return []
const filtered = objects
.map((obj, index) => ({ ...obj, originalIndex: index }))
.filter(filterFn)
return objectLimit ? filtered.slice(-objectLimit) : filtered
return filtered
}

const filteredLines = useMemo(
() =>
filterAndLimit(graphics.lines, filterLines).sort(
filterOnly(graphics.lines, filterLines).sort(
(a, b) =>
(a.zIndex ?? 0) - (b.zIndex ?? 0) ||
a.originalIndex - b.originalIndex,
),
[graphics.lines, filterLines, objectLimit],
[graphics.lines, filterLines],
)
const filteredInfiniteLines = useMemo(
() => filterAndLimit(graphics.infiniteLines, filterLayerAndStep),
[graphics.infiniteLines, filterLayerAndStep, objectLimit],
() => filterOnly(graphics.infiniteLines, filterLayerAndStep),
[graphics.infiniteLines, filterLayerAndStep],
)
const filteredRects = useMemo(
() => sortRectsByArea(filterAndLimit(graphics.rects, filterRects)),
[graphics.rects, filterRects, objectLimit],
() => sortRectsByArea(filterOnly(graphics.rects, filterRects)),
[graphics.rects, filterRects],
)
const filteredPolygons = useMemo(
() => filterAndLimit(graphics.polygons, filterPolygons),
[graphics.polygons, filterPolygons, objectLimit],
() => filterOnly(graphics.polygons, filterPolygons),
[graphics.polygons, filterPolygons],
)
const filteredPoints = useMemo(
() => filterAndLimit(graphics.points, filterPoints),
[graphics.points, filterPoints, objectLimit],
() => filterOnly(graphics.points, filterPoints),
[graphics.points, filterPoints],
)
const filteredCircles = useMemo(
() => filterAndLimit(graphics.circles, filterCircles),
[graphics.circles, filterCircles, objectLimit],
() => filterOnly(graphics.circles, filterCircles),
[graphics.circles, filterCircles],
)
const filteredTexts = useMemo(
() => filterAndLimit(graphics.texts, filterTexts),
[graphics.texts, filterTexts, objectLimit],
() => filterOnly(graphics.texts, filterTexts),
[graphics.texts, filterTexts],
)
const filteredArrows = useMemo(
() => filterAndLimit(graphics.arrows, filterArrows),
[graphics.arrows, filterArrows, objectLimit],
() => filterOnly(graphics.arrows, filterArrows),
[graphics.arrows, filterArrows],
)

const totalFilteredObjects =
const totalPreLimitObjects =
filteredInfiniteLines.length +
filteredLines.length +
filteredRects.length +
Expand All @@ -479,7 +480,52 @@ export const InteractiveGraphics = ({
filteredCircles.length +
filteredTexts.length +
filteredArrows.length
const isLimitReached = objectLimit && totalFilteredObjects > objectLimit

const [
limitedLines,
limitedInfiniteLines,
limitedRects,
limitedPolygons,
limitedPoints,
limitedCircles,
limitedTexts,
limitedArrows,
] = useMemo(() => {
const buckets = [
filteredLines,
filteredInfiniteLines,
filteredRects,
filteredPolygons,
filteredPoints,
filteredCircles,
filteredTexts,
filteredArrows,
]
const typedBuckets = buckets as [
typeof filteredLines,
typeof filteredInfiniteLines,
typeof filteredRects,
typeof filteredPolygons,
typeof filteredPoints,
typeof filteredCircles,
typeof filteredTexts,
typeof filteredArrows,
]
if (!objectLimit || totalPreLimitObjects <= objectLimit) return typedBuckets
return applyObjectLimit(buckets, objectLimit) as typeof typedBuckets
}, [
filteredLines,
filteredInfiniteLines,
filteredRects,
filteredPolygons,
filteredPoints,
filteredCircles,
filteredTexts,
filteredArrows,
objectLimit,
totalPreLimitObjects,
])
const isLimitReached = objectLimit && totalPreLimitObjects > objectLimit

return (
<div>
Expand Down Expand Up @@ -555,7 +601,7 @@ export const InteractiveGraphics = ({
{isLimitReached && (
<span style={{ color: "red", fontSize: "12px" }}>
Display limited to {objectLimit} objects. Received:{" "}
{totalFilteredObjects}.
{totalPreLimitObjects}.
</span>
)}
</div>
Expand Down Expand Up @@ -598,15 +644,15 @@ export const InteractiveGraphics = ({
onContextMenu={handleContextMenu}
>
<DimensionOverlay transform={realToScreen}>
{filteredArrows.map((arrow) => (
{limitedArrows.map((arrow) => (
<Arrow
key={arrow.originalIndex}
arrow={arrow}
index={arrow.originalIndex}
interactiveState={interactiveState}
/>
))}
{filteredInfiniteLines.map((infiniteLine) => (
{limitedInfiniteLines.map((infiniteLine) => (
<InfiniteLine
key={infiniteLine.originalIndex}
infiniteLine={infiniteLine}
Expand All @@ -615,7 +661,7 @@ export const InteractiveGraphics = ({
size={size}
/>
))}
{filteredLines.map((line) => (
{limitedLines.map((line) => (
<Line
key={line.originalIndex}
line={line}
Expand All @@ -625,39 +671,39 @@ export const InteractiveGraphics = ({
mousePosition={mousePosition}
/>
))}
{filteredRects.map((rect) => (
{limitedRects.map((rect) => (
<Rect
key={rect.originalIndex}
rect={rect}
index={rect.originalIndex}
interactiveState={interactiveState}
/>
))}
{filteredPolygons.map((polygon) => (
{limitedPolygons.map((polygon) => (
<Polygon
key={polygon.originalIndex}
polygon={polygon}
index={polygon.originalIndex}
interactiveState={interactiveState}
/>
))}
{filteredCircles.map((circle) => (
{limitedCircles.map((circle) => (
<Circle
key={circle.originalIndex}
circle={circle}
index={circle.originalIndex}
interactiveState={interactiveState}
/>
))}
{filteredTexts.map((txt) => (
{limitedTexts.map((txt) => (
<Text
key={txt.originalIndex}
textObj={txt}
index={txt.originalIndex}
interactiveState={interactiveState}
/>
))}
{filteredPoints.map((point) => (
{limitedPoints.map((point) => (
<Point
key={point.originalIndex}
point={point}
Expand Down
36 changes: 36 additions & 0 deletions site/utils/applyObjectLimit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Applies a global object limit across multiple buckets.
* Takes the last `limit` objects distributed across buckets from the end,
* filling from the last bucket backwards.
*
* Example: buckets = [[L1,L2], [R1,R2,R3]], limit = 3
* -> [[L1], [R1,R2,R3]] (last 3 across all buckets)
*/
export function applyObjectLimit<T extends unknown[]>(
buckets: T[],
limit: number,
): T[] {
if (!limit) {
return buckets.map(() => [] as unknown as T)
}

const total = buckets.reduce((sum, b) => sum + b.length, 0)
if (total <= limit) return buckets

let remaining = limit
const result: T[] = new Array(buckets.length)

for (let i = buckets.length - 1; i >= 0; i--) {
const take = Math.min(buckets[i].length, remaining)
result[i] = take > 0 ? (buckets[i].slice(-take) as T) : ([] as unknown as T)
remaining -= take
if (remaining <= 0) {
for (let j = i - 1; j >= 0; j--) {
result[j] = [] as unknown as T
}
return result
}
}

return result
}
68 changes: 68 additions & 0 deletions tests/apply-object-limit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { expect, test, describe } from "bun:test"
import { applyObjectLimit } from "site/utils/applyObjectLimit"

describe("applyObjectLimit", () => {
test("returns buckets unchanged when total <= limit", () => {
const buckets = [[1, 2], [3, 4], [5]]
const result = applyObjectLimit(buckets, 10)
expect(result).toBe(buckets)
})

test("applies global limit across buckets from the end", () => {
const buckets = [[1, 2, 3], [4, 5, 6], [7]]
const result = applyObjectLimit(buckets, 3)
// limit=3: take 1 from [7], then 2 from [4,5,6] -> [[],[5,6],[7]]
expect(result[0]).toEqual([])
expect(result[1]).toEqual([5, 6])
expect(result[2]).toEqual([7])
const total = result.reduce((sum, b) => sum + b.length, 0)
expect(total).toBe(3)
})

test("single bucket gets capped to limit", () => {
const buckets = [[1, 2, 3, 4, 5]]
const result = applyObjectLimit(buckets, 3)
expect(result[0]).toEqual([3, 4, 5])
})

test("zero limit empties all buckets", () => {
const buckets = [
[1, 2],
[3, 4],
]
const result = applyObjectLimit(buckets, 0)
expect(result[0]).toEqual([])
expect(result[1]).toEqual([])
})

test("preserves order within buckets", () => {
const buckets = [
[10, 20, 30],
[40, 50],
]
const result = applyObjectLimit(buckets, 3)
// last 3: take all [40,50] + last 1 from [10,20,30]=[30] -> [[30],[40,50]]
expect(result[0]).toEqual([30])
expect(result[1]).toEqual([40, 50])
})

test("limit exactly equals total returns original buckets", () => {
const buckets = [[1, 2], [3]]
const result = applyObjectLimit(buckets, 3)
expect(result).toBe(buckets)
})

test("first buckets become empty when limit fits only last buckets", () => {
const buckets = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]
const result = applyObjectLimit(buckets, 2)
expect(result[0]).toEqual([])
expect(result[1]).toEqual([])
expect(result[2]).toEqual([8, 9])
const total = result.reduce((sum, b) => sum + b.length, 0)
expect(total).toBe(2)
})
})
Loading