Refactor/reacrhitecture to fsd+ #49
+42
@@ -42,4 +42,46 @@ describe('computeLineRenderModel', () => {
|
||||
expect(model.windowChars).toEqual([]);
|
||||
expect(model.rightText).toBe('');
|
||||
});
|
||||
|
||||
it('places entire line in rightText when slider is at 0', () => {
|
||||
const line = makeLine([
|
||||
{ char: 'A', widthA: 10, widthB: 10 },
|
||||
{ char: 'B', widthA: 10, widthB: 10 },
|
||||
{ char: 'C', widthA: 10, widthB: 10 },
|
||||
]);
|
||||
const model = computeLineRenderModel(line, 0, 500, 0);
|
||||
expect(model.leftText).toBe('');
|
||||
expect(model.windowChars).toEqual([]);
|
||||
expect(model.rightText).toBe('ABC');
|
||||
});
|
||||
|
||||
it('places entire line in leftText when slider is at 100', () => {
|
||||
const line = makeLine([
|
||||
{ char: 'A', widthA: 10, widthB: 10 },
|
||||
{ char: 'B', widthA: 10, widthB: 10 },
|
||||
{ char: 'C', widthA: 10, widthB: 10 },
|
||||
]);
|
||||
const model = computeLineRenderModel(line, 100, 500, 0);
|
||||
expect(model.leftText).toBe('ABC');
|
||||
expect(model.windowChars).toEqual([]);
|
||||
expect(model.rightText).toBe('');
|
||||
});
|
||||
|
||||
it('splits line correctly with slider mid-line (window=0)', () => {
|
||||
// Equal widths → line is centered. Container=300, total=30 → xOffset=135.
|
||||
// Char thresholds (per the threshold formula in the design):
|
||||
// threshold[i] = xOffset + prefA[i] + widthA[i]/2
|
||||
// i=0: 135 + 0 + 5 = 140 → 140/300 = 46.67%
|
||||
// i=1: 135 + 10 + 5 = 150 → 150/300 = 50.00%
|
||||
// i=2: 135 + 20 + 5 = 160 → 160/300 = 53.33%
|
||||
const line = makeLine([
|
||||
{ char: 'A', widthA: 10, widthB: 10 },
|
||||
{ char: 'B', widthA: 10, widthB: 10 },
|
||||
{ char: 'C', widthA: 10, widthB: 10 },
|
||||
]);
|
||||
// Slider just past B's threshold (50%) but not C's (53.33%).
|
||||
const model = computeLineRenderModel(line, 51, 300, 0);
|
||||
expect(model.leftText).toBe('AB');
|
||||
expect(model.rightText).toBe('C');
|
||||
});
|
||||
});
|
||||
|
||||
+66
-2
@@ -41,8 +41,72 @@ export function computeLineRenderModel(
|
||||
containerWidth: number,
|
||||
windowSize: number,
|
||||
): LineRenderModel {
|
||||
if (line.chars.length === 0) {
|
||||
const chars = line.chars;
|
||||
const n = chars.length;
|
||||
if (n === 0) {
|
||||
return { leftText: '', windowChars: [], rightText: '' };
|
||||
}
|
||||
throw new Error('not implemented');
|
||||
|
||||
const split = findSplitIndex(chars, sliderPos, containerWidth);
|
||||
|
||||
const halfWindow = Math.floor(Math.max(0, windowSize) / 2);
|
||||
let windowStart = clamp(split - halfWindow, 0, n);
|
||||
let windowEnd = clamp(windowStart + Math.max(0, windowSize), 0, n);
|
||||
windowStart = Math.max(0, windowEnd - Math.max(0, windowSize));
|
||||
|
||||
const leftText = chars.slice(0, windowStart).map(c => c.char).join('');
|
||||
const rightText = chars.slice(windowEnd).map(c => c.char).join('');
|
||||
const windowChars = chars.slice(windowStart, windowEnd).map((c, idx) => ({
|
||||
key: `${windowStart + idx}-${c.char}`,
|
||||
char: c.char,
|
||||
isPast: (windowStart + idx) < split,
|
||||
}));
|
||||
|
||||
return { leftText, windowChars, rightText };
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks chars left-to-right computing the per-char threshold using prefix sums
|
||||
* of widthA (past chars, fontA) and suffix sums of widthB (future chars, fontB).
|
||||
* Returns the count of chars whose threshold the slider has passed.
|
||||
*/
|
||||
function findSplitIndex(
|
||||
chars: ComparisonChar[],
|
||||
sliderPos: number,
|
||||
containerWidth: number,
|
||||
): number {
|
||||
const n = chars.length;
|
||||
const sliderX = (sliderPos / 100) * containerWidth;
|
||||
|
||||
const prefA = new Float64Array(n + 1);
|
||||
for (let i = 0; i < n; i++) {
|
||||
prefA[i + 1] = prefA[i] + chars[i].widthA;
|
||||
}
|
||||
const sufB = new Float64Array(n + 1);
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
sufB[i] = sufB[i + 1] + chars[i].widthB;
|
||||
}
|
||||
|
||||
let split = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const totalWidth = prefA[i] + chars[i].widthA + sufB[i + 1];
|
||||
const xOffset = (containerWidth - totalWidth) / 2;
|
||||
const threshold = xOffset + prefA[i] + chars[i].widthA / 2;
|
||||
if (sliderX > threshold) {
|
||||
split = i + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return split;
|
||||
}
|
||||
|
||||
function clamp(value: number, lo: number, hi: number): number {
|
||||
if (value < lo) {
|
||||
return lo;
|
||||
}
|
||||
if (value > hi) {
|
||||
return hi;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user