From 2c3d88c81f7b6797ce2eb6563a83a319ed6af274 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Wed, 24 Jun 2026 13:49:23 +0300 Subject: [PATCH] feat(CompareBoard): add measureRoleHeight via Pretext line count --- .../lib/measure/measureFrameHeight.test.ts | 32 +++++++++++++ .../lib/measure/measureFrameHeight.ts | 46 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/features/CompareBoard/lib/measure/measureFrameHeight.test.ts create mode 100644 src/features/CompareBoard/lib/measure/measureFrameHeight.ts diff --git a/src/features/CompareBoard/lib/measure/measureFrameHeight.test.ts b/src/features/CompareBoard/lib/measure/measureFrameHeight.test.ts new file mode 100644 index 0000000..4cd65c4 --- /dev/null +++ b/src/features/CompareBoard/lib/measure/measureFrameHeight.test.ts @@ -0,0 +1,32 @@ +import { + describe, + expect, + it, + vi, +} from 'vitest'; +import { measureRoleHeight } from './measureFrameHeight'; + +describe('measureRoleHeight', () => { + it('multiplies pretext line count by sizePx*lineHeight', () => { + const layout = vi.fn().mockReturnValue({ lineCount: 3, height: 0 }); + const prepared = {} as never; + // 3 lines * 20px * 1.5 = 90 + expect(measureRoleHeight({ prepared, maxWidth: 600, sizePx: 20, lineHeight: 1.5 }, layout)).toBe(90); + }); + it('passes width and pixel line-height into pretext layout', () => { + const layout = vi.fn().mockReturnValue({ lineCount: 1, height: 0 }); + measureRoleHeight({ prepared: {} as never, maxWidth: 600, sizePx: 16, lineHeight: 1.25 }, layout); + expect(layout).toHaveBeenCalledWith(expect.anything(), 600, 16 * 1.25); + }); + it('returns 0 when the text lays out to zero lines (empty specimen)', () => { + const layout = vi.fn().mockReturnValue({ lineCount: 0, height: 0 }); + expect(measureRoleHeight({ prepared: {} as never, maxWidth: 600, sizePx: 16, lineHeight: 1.5 }, layout)) + .toBe(0); + }); + it('handles fractional sizes and line-heights without rounding', () => { + const layout = vi.fn().mockReturnValue({ lineCount: 2, height: 0 }); + // 2 * 15.5 * 1.4 = 43.4 + expect(measureRoleHeight({ prepared: {} as never, maxWidth: 320, sizePx: 15.5, lineHeight: 1.4 }, layout)) + .toBeCloseTo(43.4); + }); +}); diff --git a/src/features/CompareBoard/lib/measure/measureFrameHeight.ts b/src/features/CompareBoard/lib/measure/measureFrameHeight.ts new file mode 100644 index 0000000..908680d --- /dev/null +++ b/src/features/CompareBoard/lib/measure/measureFrameHeight.ts @@ -0,0 +1,46 @@ +import { + type PreparedText, + layout as pretextLayout, +} from '@chenglou/pretext'; + +/** + * Inputs for measuring one role block's rendered height. + */ +export interface RoleHeightInput { + /** + * Pretext-prepared specimen text for this role+font. + */ + prepared: PreparedText; + /** + * Available width in px (the focal frame's content width). + */ + maxWidth: number; + /** + * Resolved font-size in px. + */ + sizePx: number; + /** + * Unitless line-height multiplier. + */ + lineHeight: number; +} + +/** + * Height in px of a role's text block at the given width, from Pretext's + * pure-arithmetic line count. + * + * Height is `lineCount * sizePx * lineHeight` rather than Pretext's own + * `height` so it tracks the CSS box model exactly (line-height as a multiple of + * font-size), keeping measurement and render in lockstep — the zero-shift + * invariant. + * + * @param input - Prepared text plus width and resolved type metrics. + * @param layout - Pretext layout fn; injectable for unit tests, defaults to + * `@chenglou/pretext`'s `layout`. + * @returns The block height in px. + */ +export function measureRoleHeight(input: RoleHeightInput, layout = pretextLayout): number { + const { prepared, maxWidth, sizePx, lineHeight } = input; + const { lineCount } = layout(prepared, maxWidth, sizePx * lineHeight); + return lineCount * sizePx * lineHeight; +}