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
26 changes: 25 additions & 1 deletion src/layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ type LayoutModule = typeof import('./layout.ts')
type LineBreakModule = typeof import('./line-break.ts')
type RichInlineModule = typeof import('./rich-inline.ts')
type AnalysisModule = typeof import('./analysis.ts')
type MeasurementModule = typeof import('./measurement.ts')
type SegmentMetrics = ReturnType<MeasurementModule['getSegmentMetrics']>

let prepare: LayoutModule['prepare']
let prepareWithSegments: LayoutModule['prepareWithSegments']
Expand All @@ -32,6 +34,7 @@ let materializeRichInlineLineRange: RichInlineModule['materializeRichInlineLineR
let measureRichInlineStats: RichInlineModule['measureRichInlineStats']
let walkRichInlineLineRanges: RichInlineModule['walkRichInlineLineRanges']
let isCJK: AnalysisModule['isCJK']
let getSegmentBreakableFitAdvances: MeasurementModule['getSegmentBreakableFitAdvances']

const emojiPresentationRe = /\p{Emoji_Presentation}/u
const punctuationRe = /[.,!?;:%)\]}'"”’»›…—-]/u
Expand Down Expand Up @@ -262,11 +265,12 @@ class TestOffscreenCanvas {

beforeAll(async () => {
Reflect.set(globalThis, 'OffscreenCanvas', TestOffscreenCanvas)
const [analysisMod, mod, lineBreakMod, richInlineMod] = await Promise.all([
const [analysisMod, mod, lineBreakMod, richInlineMod, measurementMod] = await Promise.all([
import('./analysis.ts'),
import('./layout.ts'),
import('./line-break.ts'),
import('./rich-inline.ts'),
import('./measurement.ts'),
])
;({ isCJK } = analysisMod)
;({
Expand All @@ -283,13 +287,33 @@ beforeAll(async () => {
} = mod)
;({ countPreparedLines, measurePreparedLineGeometry, stepPreparedLineGeometry, walkPreparedLines } = lineBreakMod)
;({ prepareRichInline, materializeRichInlineLineRange, measureRichInlineStats, walkRichInlineLineRanges } = richInlineMod)
;({ getSegmentBreakableFitAdvances } = measurementMod)
})

beforeEach(() => {
setLocale(undefined)
clearCache()
})

describe('measurement invariants', () => {
test('breakable fit advance cache is keyed by fit mode', () => {
const metrics: SegmentMetrics = { width: 80, containsCJK: false }
const cache: Map<string, SegmentMetrics> = new Map([
['a', { width: 10, containsCJK: false }],
['b', { width: 20, containsCJK: false }],
['c', { width: 30, containsCJK: false }],
['ab', { width: 35, containsCJK: false }],
['bc', { width: 60, containsCJK: false }],
['abc', metrics],
])

expect(getSegmentBreakableFitAdvances('abc', metrics, cache, 0, 'sum-graphemes')).toEqual([10, 20, 30])
expect(getSegmentBreakableFitAdvances('abc', metrics, cache, 0, 'pair-context')).toEqual([10, 25, 40])
expect(getSegmentBreakableFitAdvances('abc', metrics, cache, 0, 'segment-prefixes')).toEqual([10, 25, 45])
expect(getSegmentBreakableFitAdvances('abc', metrics, cache, 0, 'sum-graphemes')).toEqual([10, 20, 30])
})
})

describe('prepare invariants', () => {
test('whitespace-only input stays empty', () => {
const prepared = prepare(' \t\n ', FONT)
Expand Down
40 changes: 23 additions & 17 deletions src/measurement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ export type SegmentMetrics = {
width: number
containsCJK: boolean
emojiCount?: number
breakableFitMode?: BreakableFitMode
breakableFitAdvances?: number[] | null
breakableFitAdvancesByMode?: Partial<Record<BreakableFitMode, number[] | null>>
}

export type EngineProfile = {
Expand Down Expand Up @@ -204,32 +203,41 @@ export function getSegmentBreakableFitAdvances(
emojiCorrection: number,
mode: BreakableFitMode,
): number[] | null {
if (metrics.breakableFitAdvances !== undefined && metrics.breakableFitMode === mode) {
return metrics.breakableFitAdvances
}
metrics.breakableFitMode = mode

const graphemeSegmenter = getSharedGraphemeSegmenter()
const graphemes: string[] = []
for (const gs of graphemeSegmenter.segment(seg)) {
graphemes.push(gs.segment)
}

const cacheMode =
mode === 'segment-prefixes' && graphemes.length > MAX_PREFIX_FIT_GRAPHEMES
? 'pair-context'
: mode
const cached = metrics.breakableFitAdvancesByMode?.[cacheMode]
if (cached !== undefined) {
return cached
}

function cacheAdvances(advances: number[] | null): number[] | null {
const byMode = metrics.breakableFitAdvancesByMode ??= {}
byMode[cacheMode] = advances
return advances
}

if (graphemes.length <= 1) {
metrics.breakableFitAdvances = null
return metrics.breakableFitAdvances
return cacheAdvances(null)
}

if (mode === 'sum-graphemes') {
if (cacheMode === 'sum-graphemes') {
const advances: number[] = []
for (const grapheme of graphemes) {
const graphemeMetrics = getSegmentMetrics(grapheme, cache)
advances.push(getCorrectedSegmentWidth(grapheme, graphemeMetrics, emojiCorrection))
}
metrics.breakableFitAdvances = advances
return metrics.breakableFitAdvances
return cacheAdvances(advances)
}

if (mode === 'pair-context' || graphemes.length > MAX_PREFIX_FIT_GRAPHEMES) {
if (cacheMode === 'pair-context') {
const advances: number[] = []
let previousGrapheme: string | null = null
let previousWidth = 0
Expand All @@ -250,8 +258,7 @@ export function getSegmentBreakableFitAdvances(
previousWidth = currentWidth
}

metrics.breakableFitAdvances = advances
return metrics.breakableFitAdvances
return cacheAdvances(advances)
}

const advances: number[] = []
Expand All @@ -266,8 +273,7 @@ export function getSegmentBreakableFitAdvances(
prefixWidth = nextPrefixWidth
}

metrics.breakableFitAdvances = advances
return metrics.breakableFitAdvances
return cacheAdvances(advances)
}

export function getFontMeasurementState(font: string, needsEmojiCorrection: boolean): {
Expand Down