From c29f41a9a6e8a47a7db32d7b1e1acfefdfdd0131 Mon Sep 17 00:00:00 2001 From: zergzorg Date: Wed, 20 May 2026 23:30:52 +0300 Subject: [PATCH 1/3] Apply objectLimit globally --- .../InteractiveGraphics.tsx | 112 ++++++++++++------ site/utils/applyObjectLimit.ts | 29 +++++ tests/apply-object-limit.test.ts | 45 +++++++ 3 files changed, 150 insertions(+), 36 deletions(-) create mode 100644 site/utils/applyObjectLimit.ts create mode 100644 tests/apply-object-limit.test.ts diff --git a/site/components/InteractiveGraphics/InteractiveGraphics.tsx b/site/components/InteractiveGraphics/InteractiveGraphics.tsx index de014e1..967346d 100644 --- a/site/components/InteractiveGraphics/InteractiveGraphics.tsx +++ b/site/components/InteractiveGraphics/InteractiveGraphics.tsx @@ -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" @@ -421,65 +422,104 @@ export const InteractiveGraphics = ({ filterLayerAndStep, }) - const filterAndLimit = ( + const filterObjects = ( 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 preLimitLines = useMemo( () => - filterAndLimit(graphics.lines, filterLines).sort( + filterObjects(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 preLimitInfiniteLines = useMemo( + () => filterObjects(graphics.infiniteLines, filterLayerAndStep), + [graphics.infiniteLines, filterLayerAndStep], ) - const filteredRects = useMemo( - () => sortRectsByArea(filterAndLimit(graphics.rects, filterRects)), - [graphics.rects, filterRects, objectLimit], + const preLimitRects = useMemo( + () => sortRectsByArea(filterObjects(graphics.rects, filterRects)), + [graphics.rects, filterRects], ) - const filteredPolygons = useMemo( - () => filterAndLimit(graphics.polygons, filterPolygons), - [graphics.polygons, filterPolygons, objectLimit], + const preLimitPolygons = useMemo( + () => filterObjects(graphics.polygons, filterPolygons), + [graphics.polygons, filterPolygons], ) - const filteredPoints = useMemo( - () => filterAndLimit(graphics.points, filterPoints), - [graphics.points, filterPoints, objectLimit], + const preLimitPoints = useMemo( + () => filterObjects(graphics.points, filterPoints), + [graphics.points, filterPoints], ) - const filteredCircles = useMemo( - () => filterAndLimit(graphics.circles, filterCircles), - [graphics.circles, filterCircles, objectLimit], + const preLimitCircles = useMemo( + () => filterObjects(graphics.circles, filterCircles), + [graphics.circles, filterCircles], ) - const filteredTexts = useMemo( - () => filterAndLimit(graphics.texts, filterTexts), - [graphics.texts, filterTexts, objectLimit], + const preLimitTexts = useMemo( + () => filterObjects(graphics.texts, filterTexts), + [graphics.texts, filterTexts], ) - const filteredArrows = useMemo( - () => filterAndLimit(graphics.arrows, filterArrows), - [graphics.arrows, filterArrows, objectLimit], + const preLimitArrows = useMemo( + () => filterObjects(graphics.arrows, filterArrows), + [graphics.arrows, filterArrows], + ) + + const limitedObjects = useMemo( + () => + applyObjectLimit( + { + arrows: preLimitArrows, + infiniteLines: preLimitInfiniteLines, + lines: preLimitLines, + rects: preLimitRects, + polygons: preLimitPolygons, + circles: preLimitCircles, + texts: preLimitTexts, + points: preLimitPoints, + }, + objectLimit, + ), + [ + preLimitArrows, + preLimitInfiniteLines, + preLimitLines, + preLimitRects, + preLimitPolygons, + preLimitCircles, + preLimitTexts, + preLimitPoints, + objectLimit, + ], ) const totalFilteredObjects = - filteredInfiniteLines.length + - filteredLines.length + - filteredRects.length + - filteredPolygons.length + - filteredPoints.length + - filteredCircles.length + - filteredTexts.length + - filteredArrows.length - const isLimitReached = objectLimit && totalFilteredObjects > objectLimit + preLimitInfiniteLines.length + + preLimitLines.length + + preLimitRects.length + + preLimitPolygons.length + + preLimitPoints.length + + preLimitCircles.length + + preLimitTexts.length + + preLimitArrows.length + const isLimitReached = + objectLimit !== undefined && totalFilteredObjects > objectLimit + + const { + arrows: filteredArrows, + infiniteLines: filteredInfiniteLines, + lines: filteredLines, + rects: filteredRects, + polygons: filteredPolygons, + circles: filteredCircles, + texts: filteredTexts, + points: filteredPoints, + } = limitedObjects return (
diff --git a/site/utils/applyObjectLimit.ts b/site/utils/applyObjectLimit.ts new file mode 100644 index 0000000..e559cfd --- /dev/null +++ b/site/utils/applyObjectLimit.ts @@ -0,0 +1,29 @@ +type ObjectGroups = Record + +export const applyObjectLimit = ( + groups: TGroups, + objectLimit?: number, +): TGroups => { + if (objectLimit === undefined) { + return groups + } + + const limit = Math.max(0, objectLimit) + let remaining = limit + const entries = Object.entries(groups) as [ + keyof TGroups, + TGroups[keyof TGroups], + ][] + const limitedGroups = {} as TGroups + + for (let index = entries.length - 1; index >= 0; index--) { + const [key, objects] = entries[index] + const keepCount = Math.min(objects.length, remaining) + limitedGroups[key] = objects.slice( + objects.length - keepCount, + ) as TGroups[typeof key] + remaining -= keepCount + } + + return limitedGroups +} diff --git a/tests/apply-object-limit.test.ts b/tests/apply-object-limit.test.ts new file mode 100644 index 0000000..13936d1 --- /dev/null +++ b/tests/apply-object-limit.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from "bun:test" +import { applyObjectLimit } from "site/utils/applyObjectLimit" + +describe("applyObjectLimit", () => { + test("applies one shared limit across object groups", () => { + const limited = applyObjectLimit( + { + lines: ["line-1", "line-2"], + rects: ["rect-1", "rect-2"], + points: ["point-1", "point-2"], + }, + 3, + ) + + expect(limited).toEqual({ + lines: [], + rects: ["rect-2"], + points: ["point-1", "point-2"], + }) + }) + + test("returns filtered groups unchanged when no limit is set", () => { + const groups = { + lines: ["line-1"], + points: ["point-1", "point-2"], + } + + expect(applyObjectLimit(groups)).toBe(groups) + }) + + test("supports an explicit zero-object limit", () => { + expect( + applyObjectLimit( + { + lines: ["line-1"], + points: ["point-1"], + }, + 0, + ), + ).toEqual({ + lines: [], + points: [], + }) + }) +}) From 4442197e90234bb5ad928028678f50668f67b68c Mon Sep 17 00:00:00 2001 From: zergzorg Date: Wed, 20 May 2026 23:34:50 +0300 Subject: [PATCH 2/3] Fix object limit helper types --- site/utils/applyObjectLimit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/utils/applyObjectLimit.ts b/site/utils/applyObjectLimit.ts index e559cfd..b1cd6fc 100644 --- a/site/utils/applyObjectLimit.ts +++ b/site/utils/applyObjectLimit.ts @@ -21,7 +21,7 @@ export const applyObjectLimit = ( const keepCount = Math.min(objects.length, remaining) limitedGroups[key] = objects.slice( objects.length - keepCount, - ) as TGroups[typeof key] + ) as unknown as TGroups[typeof key] remaining -= keepCount } From f363b2009fc9695da275e2e0b1e50becbe0ee9bd Mon Sep 17 00:00:00 2001 From: zergzorg Date: Thu, 21 May 2026 10:32:09 +0300 Subject: [PATCH 3/3] Expand object limit edge coverage Signed-off-by: zergzorg --- tests/apply-object-limit.test.ts | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/apply-object-limit.test.ts b/tests/apply-object-limit.test.ts index 13936d1..5ced412 100644 --- a/tests/apply-object-limit.test.ts +++ b/tests/apply-object-limit.test.ts @@ -42,4 +42,53 @@ describe("applyObjectLimit", () => { points: [], }) }) + + test("carries unused budget to earlier groups", () => { + const limited = applyObjectLimit( + { + lines: ["line-1", "line-2"], + rects: ["rect-1"], + points: [], + }, + 2, + ) + + expect(limited).toEqual({ + lines: ["line-2"], + rects: ["rect-1"], + points: [], + }) + }) + + test("preserves every group key after the budget is consumed", () => { + const limited = applyObjectLimit( + { + lines: ["line-1"], + rects: ["rect-1"], + points: ["point-1"], + }, + 1, + ) + + expect(limited).toEqual({ + lines: [], + rects: [], + points: ["point-1"], + }) + }) + + test("treats negative limits as zero", () => { + expect( + applyObjectLimit( + { + lines: ["line-1"], + points: ["point-1"], + }, + -1, + ), + ).toEqual({ + lines: [], + points: [], + }) + }) })