From 5c4e666d6a87c1c2593eff3a78c1370d8bac2dff Mon Sep 17 00:00:00 2001 From: 8npyvz5bd8-lang <8npyvz5bd8@privaterelay.appleid.com> Date: Thu, 14 May 2026 22:42:20 +0800 Subject: [PATCH 1/3] Fix global objectLimit rendering cap Apply objectLimit after filtering across all rendered object groups and keep the warning count based on the pre-limit filtered total. --- .../InteractiveGraphics.tsx | 111 ++++++++++++------ 1 file changed, 74 insertions(+), 37 deletions(-) diff --git a/site/components/InteractiveGraphics/InteractiveGraphics.tsx b/site/components/InteractiveGraphics/InteractiveGraphics.tsx index de014e1..7e81861 100644 --- a/site/components/InteractiveGraphics/InteractiveGraphics.tsx +++ b/site/components/InteractiveGraphics/InteractiveGraphics.tsx @@ -27,6 +27,7 @@ import { Polygon } from "./Polygon" import { Rect } from "./Rect" import { Text } from "./Text" import { Tooltip } from "./Tooltip" +import { applyObjectLimit } from "./applyObjectLimit" import { useDoesLineIntersectViewport, useFilterArrows, @@ -421,65 +422,101 @@ export const InteractiveGraphics = ({ filterLayerAndStep, }) - const filterAndLimit = ( + const filterObjectsWithOriginalIndex = ( objects: T[] | undefined, 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( + const filteredLinesBeforeLimit = useMemo( () => - filterAndLimit(graphics.lines, filterLines).sort( + filterObjectsWithOriginalIndex(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], + const filteredInfiniteLinesBeforeLimit = useMemo( + () => + filterObjectsWithOriginalIndex( + graphics.infiniteLines, + filterLayerAndStep, + ), + [graphics.infiniteLines, filterLayerAndStep], ) - const filteredRects = useMemo( - () => sortRectsByArea(filterAndLimit(graphics.rects, filterRects)), - [graphics.rects, filterRects, objectLimit], + const filteredRectsBeforeLimit = useMemo( + () => + sortRectsByArea( + filterObjectsWithOriginalIndex(graphics.rects, filterRects), + ), + [graphics.rects, filterRects], ) - const filteredPolygons = useMemo( - () => filterAndLimit(graphics.polygons, filterPolygons), - [graphics.polygons, filterPolygons, objectLimit], + const filteredPolygonsBeforeLimit = useMemo( + () => filterObjectsWithOriginalIndex(graphics.polygons, filterPolygons), + [graphics.polygons, filterPolygons], ) - const filteredPoints = useMemo( - () => filterAndLimit(graphics.points, filterPoints), - [graphics.points, filterPoints, objectLimit], + const filteredPointsBeforeLimit = useMemo( + () => filterObjectsWithOriginalIndex(graphics.points, filterPoints), + [graphics.points, filterPoints], ) - const filteredCircles = useMemo( - () => filterAndLimit(graphics.circles, filterCircles), - [graphics.circles, filterCircles, objectLimit], + const filteredCirclesBeforeLimit = useMemo( + () => filterObjectsWithOriginalIndex(graphics.circles, filterCircles), + [graphics.circles, filterCircles], ) - const filteredTexts = useMemo( - () => filterAndLimit(graphics.texts, filterTexts), - [graphics.texts, filterTexts, objectLimit], + const filteredTextsBeforeLimit = useMemo( + () => filterObjectsWithOriginalIndex(graphics.texts, filterTexts), + [graphics.texts, filterTexts], ) - const filteredArrows = useMemo( - () => filterAndLimit(graphics.arrows, filterArrows), - [graphics.arrows, filterArrows, objectLimit], + const filteredArrowsBeforeLimit = useMemo( + () => filterObjectsWithOriginalIndex(graphics.arrows, filterArrows), + [graphics.arrows, filterArrows], ) - const totalFilteredObjects = - filteredInfiniteLines.length + - filteredLines.length + - filteredRects.length + - filteredPolygons.length + - filteredPoints.length + - filteredCircles.length + - filteredTexts.length + - filteredArrows.length - const isLimitReached = objectLimit && totalFilteredObjects > objectLimit + const { + groups: [ + filteredArrows, + filteredInfiniteLines, + filteredLines, + filteredRects, + filteredPolygons, + filteredCircles, + filteredTexts, + filteredPoints, + ], + totalObjectCount: totalFilteredObjects, + isLimitReached, + } = useMemo( + () => + applyObjectLimit( + [ + filteredArrowsBeforeLimit, + filteredInfiniteLinesBeforeLimit, + filteredLinesBeforeLimit, + filteredRectsBeforeLimit, + filteredPolygonsBeforeLimit, + filteredCirclesBeforeLimit, + filteredTextsBeforeLimit, + filteredPointsBeforeLimit, + ] as const, + objectLimit, + ), + [ + filteredArrowsBeforeLimit, + filteredInfiniteLinesBeforeLimit, + filteredLinesBeforeLimit, + filteredRectsBeforeLimit, + filteredPolygonsBeforeLimit, + filteredCirclesBeforeLimit, + filteredTextsBeforeLimit, + filteredPointsBeforeLimit, + objectLimit, + ], + ) return (
From f10cd0661c0eb1aaecbdf82412c1ab460add7062 Mon Sep 17 00:00:00 2001 From: 8npyvz5bd8-lang <8npyvz5bd8@privaterelay.appleid.com> Date: Thu, 14 May 2026 22:42:47 +0800 Subject: [PATCH 2/3] Add objectLimit helper Add a typed helper that applies one global object cap and reports the pre-limit filtered count. --- .../InteractiveGraphics/applyObjectLimit.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 site/components/InteractiveGraphics/applyObjectLimit.ts diff --git a/site/components/InteractiveGraphics/applyObjectLimit.ts b/site/components/InteractiveGraphics/applyObjectLimit.ts new file mode 100644 index 0000000..d33a7ed --- /dev/null +++ b/site/components/InteractiveGraphics/applyObjectLimit.ts @@ -0,0 +1,62 @@ +type LimitedGroups[]> = { + [Index in keyof Groups]: Groups[Index] extends ReadonlyArray + ? Item[] + : never +} + +export const applyObjectLimit = < + Groups extends readonly ReadonlyArray[], +>( + groups: Groups, + objectLimit?: number, +): { + groups: LimitedGroups + totalObjectCount: number + isLimitReached: boolean +} => { + const totalObjectCount = groups.reduce( + (total, group) => total + group.length, + 0, + ) + + if (objectLimit === undefined || !Number.isFinite(objectLimit)) { + return { + groups: groups.map((group) => [...group]) as LimitedGroups, + totalObjectCount, + isLimitReached: false, + } + } + + const normalizedLimit = Math.max(0, Math.floor(objectLimit)) + + if (totalObjectCount <= normalizedLimit) { + return { + groups: groups.map((group) => [...group]) as LimitedGroups, + totalObjectCount, + isLimitReached: false, + } + } + + const limitedGroups = groups.map(() => []) as LimitedGroups + const writableGroups = limitedGroups as unknown as unknown[][] + let remainingObjects = normalizedLimit + + for ( + let groupIndex = groups.length - 1; + groupIndex >= 0 && remainingObjects > 0; + groupIndex-- + ) { + const group = groups[groupIndex] + const takeCount = Math.min(group.length, remainingObjects) + if (takeCount === 0) continue + + writableGroups[groupIndex] = group.slice(group.length - takeCount) + remainingObjects -= takeCount + } + + return { + groups: limitedGroups, + totalObjectCount, + isLimitReached: true, + } +} From 220a4660f5a9e3601a63a7f801971c443fe4fbce Mon Sep 17 00:00:00 2001 From: 8npyvz5bd8-lang <8npyvz5bd8@privaterelay.appleid.com> Date: Thu, 14 May 2026 22:43:14 +0800 Subject: [PATCH 3/3] Add objectLimit regression tests Cover global capping, no-limit truncation, and zero-limit behavior for InteractiveGraphics objectLimit. --- tests/applyObjectLimit.test.ts | 42 ++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/applyObjectLimit.test.ts diff --git a/tests/applyObjectLimit.test.ts b/tests/applyObjectLimit.test.ts new file mode 100644 index 0000000..9d090f9 --- /dev/null +++ b/tests/applyObjectLimit.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, test } from "bun:test" +import { applyObjectLimit } from "site/components/InteractiveGraphics/applyObjectLimit" + +describe("applyObjectLimit", () => { + test("caps objects globally across groups", () => { + const result = applyObjectLimit( + [ + [{ id: "line-1" }, { id: "line-2" }, { id: "line-3" }], + [{ id: "rect-1" }, { id: "rect-2" }], + [{ id: "point-1" }, { id: "point-2" }], + ] as const, + 3, + ) + + expect(result.totalObjectCount).toBe(7) + expect(result.isLimitReached).toBe(true) + expect(result.groups).toEqual([ + [], + [{ id: "rect-2" }], + [{ id: "point-1" }, { id: "point-2" }], + ]) + }) + + test("keeps all objects when the filtered count is within the limit", () => { + const result = applyObjectLimit( + [[{ id: "line-1" }], [{ id: "point-1" }]] as const, + 2, + ) + + expect(result.totalObjectCount).toBe(2) + expect(result.isLimitReached).toBe(false) + expect(result.groups).toEqual([[{ id: "line-1" }], [{ id: "point-1" }]]) + }) + + test("supports a zero limit", () => { + const result = applyObjectLimit([[{ id: "line-1" }]] as const, 0) + + expect(result.totalObjectCount).toBe(1) + expect(result.isLimitReached).toBe(true) + expect(result.groups).toEqual([[]]) + }) +})