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
74 changes: 56 additions & 18 deletions site/components/InteractiveGraphics/InteractiveGraphics.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import useResizeObserver from "@react-hook/resize-observer"
import { useCallback, useEffect, useMemo, useState } from "react"
import { SuperGrid } from "react-supergrid"
import { applyObjectLimit } from "site/utils/applyObjectLimit"
import { getGraphicsBounds } from "site/utils/getGraphicsBounds"
import { getMaxStep } from "site/utils/getMaxStep"
import { sortRectsByArea } from "site/utils/sortRectsByArea"
Expand Down Expand Up @@ -426,10 +427,9 @@ export const InteractiveGraphics = ({
filterFn: (obj: T) => boolean,
): (T & { originalIndex: number })[] => {
if (!objects) return []
const filtered = objects
return objects
.map((obj, index) => ({ ...obj, originalIndex: index }))
.filter(filterFn)
return objectLimit ? filtered.slice(-objectLimit) : filtered
}

const filteredLines = useMemo(
Expand All @@ -439,35 +439,73 @@ export const InteractiveGraphics = ({
(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],
[graphics.infiniteLines, filterLayerAndStep],
)
const filteredRects = useMemo(
() => sortRectsByArea(filterAndLimit(graphics.rects, filterRects)),
[graphics.rects, filterRects, objectLimit],
[graphics.rects, filterRects],
)
const filteredPolygons = useMemo(
() => filterAndLimit(graphics.polygons, filterPolygons),
[graphics.polygons, filterPolygons, objectLimit],
[graphics.polygons, filterPolygons],
)
const filteredPoints = useMemo(
() => filterAndLimit(graphics.points, filterPoints),
[graphics.points, filterPoints, objectLimit],
[graphics.points, filterPoints],
)
const filteredCircles = useMemo(
() => filterAndLimit(graphics.circles, filterCircles),
[graphics.circles, filterCircles, objectLimit],
[graphics.circles, filterCircles],
)
const filteredTexts = useMemo(
() => filterAndLimit(graphics.texts, filterTexts),
[graphics.texts, filterTexts, objectLimit],
[graphics.texts, filterTexts],
)
const filteredArrows = useMemo(
() => filterAndLimit(graphics.arrows, filterArrows),
[graphics.arrows, filterArrows, objectLimit],
[graphics.arrows, filterArrows],
)

const limitedBuckets = useMemo(
() =>
applyObjectLimit(
{
arrows: filteredArrows,
infiniteLines: filteredInfiniteLines,
lines: filteredLines,
rects: filteredRects,
polygons: filteredPolygons,
circles: filteredCircles,
texts: filteredTexts,
points: filteredPoints,
},
[
"arrows",
"infiniteLines",
"lines",
"rects",
"polygons",
"circles",
"texts",
"points",
],
objectLimit,
),
[
filteredArrows,
filteredInfiniteLines,
filteredLines,
filteredRects,
filteredPolygons,
filteredCircles,
filteredTexts,
filteredPoints,
objectLimit,
],
)

const totalFilteredObjects =
Expand Down Expand Up @@ -598,15 +636,15 @@ export const InteractiveGraphics = ({
onContextMenu={handleContextMenu}
>
<DimensionOverlay transform={realToScreen}>
{filteredArrows.map((arrow) => (
{limitedBuckets.arrows.map((arrow) => (
<Arrow
key={arrow.originalIndex}
arrow={arrow}
index={arrow.originalIndex}
interactiveState={interactiveState}
/>
))}
{filteredInfiniteLines.map((infiniteLine) => (
{limitedBuckets.infiniteLines.map((infiniteLine) => (
<InfiniteLine
key={infiniteLine.originalIndex}
infiniteLine={infiniteLine}
Expand All @@ -615,7 +653,7 @@ export const InteractiveGraphics = ({
size={size}
/>
))}
{filteredLines.map((line) => (
{limitedBuckets.lines.map((line) => (
<Line
key={line.originalIndex}
line={line}
Expand All @@ -625,39 +663,39 @@ export const InteractiveGraphics = ({
mousePosition={mousePosition}
/>
))}
{filteredRects.map((rect) => (
{limitedBuckets.rects.map((rect) => (
<Rect
key={rect.originalIndex}
rect={rect}
index={rect.originalIndex}
interactiveState={interactiveState}
/>
))}
{filteredPolygons.map((polygon) => (
{limitedBuckets.polygons.map((polygon) => (
<Polygon
key={polygon.originalIndex}
polygon={polygon}
index={polygon.originalIndex}
interactiveState={interactiveState}
/>
))}
{filteredCircles.map((circle) => (
{limitedBuckets.circles.map((circle) => (
<Circle
key={circle.originalIndex}
circle={circle}
index={circle.originalIndex}
interactiveState={interactiveState}
/>
))}
{filteredTexts.map((txt) => (
{limitedBuckets.texts.map((txt) => (
<Text
key={txt.originalIndex}
textObj={txt}
index={txt.originalIndex}
interactiveState={interactiveState}
/>
))}
{filteredPoints.map((point) => (
{limitedBuckets.points.map((point) => (
<Point
key={point.originalIndex}
point={point}
Expand Down
29 changes: 29 additions & 0 deletions site/utils/applyObjectLimit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export const applyObjectLimit = <T extends Record<string, any[]>>(
buckets: T,
renderOrder: ReadonlyArray<keyof T & string>,
limit: number | undefined,
): T => {
if (!limit || limit <= 0) return buckets

let total = 0
for (const key of renderOrder) total += buckets[key]?.length ?? 0
if (total <= limit) return buckets

const result = { ...buckets }
for (const key of renderOrder) (result as any)[key] = []

let budget = limit
for (let i = renderOrder.length - 1; i >= 0; i--) {
if (budget <= 0) break
const key = renderOrder[i]
const arr = buckets[key] ?? []
if (arr.length <= budget) {
;(result as any)[key] = arr
budget -= arr.length
} else {
;(result as any)[key] = arr.slice(-budget)
budget = 0
}
}
return result
}
113 changes: 113 additions & 0 deletions tests/applyObjectLimit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { describe, expect, test } from "bun:test"
import { applyObjectLimit } from "site/utils/applyObjectLimit"

const order = [
"arrows",
"infiniteLines",
"lines",
"rects",
"polygons",
"circles",
"texts",
"points",
] as const

const emptyBuckets = () => ({
arrows: [] as number[],
infiniteLines: [] as number[],
lines: [] as number[],
rects: [] as number[],
polygons: [] as number[],
circles: [] as number[],
texts: [] as number[],
points: [] as number[],
})

describe("applyObjectLimit", () => {
test("returns buckets unchanged when limit is undefined", () => {
const buckets = { ...emptyBuckets(), lines: [1, 2, 3], points: [4, 5] }
const result = applyObjectLimit(buckets, order, undefined)
expect(result).toBe(buckets)
})

test("returns buckets unchanged when limit is zero", () => {
const buckets = { ...emptyBuckets(), lines: [1, 2, 3] }
const result = applyObjectLimit(buckets, order, 0)
expect(result).toBe(buckets)
})

test("returns buckets unchanged when total does not exceed limit", () => {
const buckets = { ...emptyBuckets(), lines: [1, 2], points: [3] }
const result = applyObjectLimit(buckets, order, 5)
expect(result).toBe(buckets)
})

test("caps total objects at limit when only one bucket exceeds it", () => {
const buckets = { ...emptyBuckets(), points: [1, 2, 3, 4, 5] }
const result = applyObjectLimit(buckets, order, 3)
expect(result.points).toEqual([3, 4, 5])
})

test("fills budget from the last bucket backwards across types", () => {
const buckets = {
...emptyBuckets(),
arrows: [10, 11, 12],
lines: [20, 21],
points: [30],
}
const result = applyObjectLimit(buckets, order, 4)
expect(result.points).toEqual([30])
expect(result.lines).toEqual([20, 21])
expect(result.arrows).toEqual([12])
expect(result.infiniteLines).toEqual([])
expect(result.rects).toEqual([])
expect(result.polygons).toEqual([])
expect(result.circles).toEqual([])
expect(result.texts).toEqual([])
})

test("earlier buckets are dropped entirely when later buckets exhaust budget", () => {
const buckets = {
...emptyBuckets(),
arrows: [1, 2],
lines: [3, 4],
points: [5, 6, 7],
}
const result = applyObjectLimit(buckets, order, 3)
expect(result.points).toEqual([5, 6, 7])
expect(result.lines).toEqual([])
expect(result.arrows).toEqual([])
})

test("does not mutate the input buckets", () => {
const buckets = {
...emptyBuckets(),
arrows: [1, 2, 3],
points: [4, 5],
}
const snapshot = {
...buckets,
arrows: [...buckets.arrows],
points: [...buckets.points],
}
applyObjectLimit(buckets, order, 2)
expect(buckets.arrows).toEqual(snapshot.arrows)
expect(buckets.points).toEqual(snapshot.points)
})

test("limit equal to total returns all objects", () => {
const buckets = { ...emptyBuckets(), lines: [1, 2], points: [3, 4] }
const result = applyObjectLimit(buckets, order, 4)
expect(result.lines).toEqual([1, 2])
expect(result.points).toEqual([3, 4])
})

test("preserves order within each kept bucket", () => {
const buckets = {
...emptyBuckets(),
lines: [100, 200, 300, 400],
}
const result = applyObjectLimit(buckets, order, 2)
expect(result.lines).toEqual([300, 400])
})
})
Loading