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
34 changes: 21 additions & 13 deletions lib/utils/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,20 +155,28 @@ export interface AlignLine {
* @param lines 一组对齐吸附线信息
*/
export const uniqAlignLines = (lines: AlignLine[]) => {
const uniqLines: AlignLine[] = [];
lines.forEach((line) => {
const index = uniqLines.findIndex((_line) => _line.value === line.value);
if (index === -1) uniqLines.push(line);
else {
const uniqLine = uniqLines[index];
const rangeMin = Math.min(uniqLine.range[0], line.range[0]);
const rangeMax = Math.max(uniqLine.range[1], line.range[1]);
const range: [number, number] = [rangeMin, rangeMax];
const _line = { value: line.value, range };
uniqLines[index] = _line;
// Dedupe by `value` in O(n) via a Map keyed on value, instead of an
// O(n²) `findIndex` over the accumulating result. This runs on every
// drag/scale mousemove with one snap line per nearby element edge, so the
// quadratic version janks on element-dense canvases. A Map preserves
// first-occurrence insertion order (re-`set`-ting an existing key keeps its
// position), so the output order and merge semantics are unchanged.
const byValue = new Map<number, AlignLine>();
for (const line of lines) {
const existing = byValue.get(line.value);
if (!existing) {
byValue.set(line.value, line);
} else {
byValue.set(line.value, {
value: line.value,
range: [
Math.min(existing.range[0], line.range[0]),
Math.max(existing.range[1], line.range[1]),
],
});
}
});
return uniqLines;
}
return Array.from(byValue.values());
};

/**
Expand Down
45 changes: 45 additions & 0 deletions tests/utils/uniq-align-lines.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, it } from 'vitest';

import { uniqAlignLines, type AlignLine } from '@/lib/utils/element';

describe('uniqAlignLines', () => {
it('dedupes by value and merges ranges to the outer bounds', () => {
const lines: AlignLine[] = [
{ value: 10, range: [0, 5] },
{ value: 20, range: [2, 8] },
{ value: 10, range: [3, 12] },
{ value: 10, range: [-1, 4] },
];
expect(uniqAlignLines(lines)).toEqual([
{ value: 10, range: [-1, 12] }, // min(0,3,-1) .. max(5,12,4)
{ value: 20, range: [2, 8] },
]);
});

it('preserves first-occurrence order', () => {
const lines: AlignLine[] = [
{ value: 30, range: [0, 1] },
{ value: 10, range: [0, 1] },
{ value: 20, range: [0, 1] },
{ value: 10, range: [0, 1] },
];
expect(uniqAlignLines(lines).map((l) => l.value)).toEqual([30, 10, 20]);
});

it('returns a single line unchanged', () => {
const lines: AlignLine[] = [{ value: 7, range: [1, 2] }];
expect(uniqAlignLines(lines)).toEqual([{ value: 7, range: [1, 2] }]);
});

it('returns [] for empty input', () => {
expect(uniqAlignLines([])).toEqual([]);
});

it('collapses many duplicates to the unique values (linear)', () => {
const lines: AlignLine[] = Array.from({ length: 5000 }, (_, i) => ({
value: i % 50,
range: [i, i + 1] as [number, number],
}));
expect(uniqAlignLines(lines)).toHaveLength(50);
});
});
Loading