feat(font): compute split index and 3-region slice
This commit is contained in:
+42
@@ -42,4 +42,46 @@ describe('computeLineRenderModel', () => {
|
|||||||
expect(model.windowChars).toEqual([]);
|
expect(model.windowChars).toEqual([]);
|
||||||
expect(model.rightText).toBe('');
|
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,
|
containerWidth: number,
|
||||||
windowSize: number,
|
windowSize: number,
|
||||||
): LineRenderModel {
|
): LineRenderModel {
|
||||||
if (line.chars.length === 0) {
|
const chars = line.chars;
|
||||||
|
const n = chars.length;
|
||||||
|
if (n === 0) {
|
||||||
return { leftText: '', windowChars: [], rightText: '' };
|
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