修复 EPUB 文本分页显示错位并补充分页调试能力

本次提交围绕 DTCoreText 文本页的分页一致性、交互索引和高亮命中进行了集中修复。

主要改动:

1. 文本页显示改为基于整章 attributed string 的上下文布局,只对当前页 range 进行渲染,避免页面子串重新换行导致的页末断行偏差。

2. 页面布局快照与交互控制器统一改为使用 chapter-absolute 索引,修正点击、选区、菜单锚点与高亮矩形在整章上下文下的定位。

3. 修复跨章节高亮串页问题,并调整文本页高亮命中逻辑:保留 CoreText 层绘制,点击时按高亮真实 rect 精确命中,避免重复绘制和整行误判。

4. 收紧 reader 级页面缓存策略,避免预加载同时持有多份整章显示副本带来的内存放大。

5. 新增分页边界校验器、垂直对齐器和 settings-flip 自动化调试入口,用于复现与诊断页范围/显示度量不一致问题。

6. 放宽 inline attachment 的 avoid-break 处理,并补充相关分页问题调查文档与索引。
This commit is contained in:
shenlei
2026-07-08 14:27:36 +09:00
parent d0efe3f2cc
commit 5a41066b66
12 changed files with 2167 additions and 1606 deletions
@@ -44,10 +44,12 @@ final class RDEPUBPageInteractionController {
x: point.x - line.baselineOrigin.x,
y: point.y - line.baselineOrigin.y
)
// The layout frame is built in chapter context, so DTCoreText string
// indices are chapter-absolute already.
let idx = dtLine.stringIndex(forPosition: relativePoint)
guard idx != NSNotFound, idx >= 0 else { return nil }
return normalizedIndex(
idx + pageOffset(for: snapshot),
idx,
lineRange: line.stringRange,
pageRange: snapshot.pageContentRange
)
@@ -74,10 +76,9 @@ final class RDEPUBPageInteractionController {
#if canImport(DTCoreText)
guard let dtLine = dtLineContaining(range: line.stringRange) else { continue }
let pageOffset = pageOffset(for: snapshot)
let startX = dtLine.offset(forStringIndex: overlap.location - pageOffset)
let startX = dtLine.offset(forStringIndex: overlap.location)
let endIdx = overlap.location + overlap.length
let endX = dtLine.offset(forStringIndex: endIdx - pageOffset)
let endX = dtLine.offset(forStringIndex: endIdx)
#else
let startX: CGFloat = 0
let endX: CGFloat = line.frame.width
@@ -133,7 +134,7 @@ final class RDEPUBPageInteractionController {
#if canImport(DTCoreText)
guard let dtLine = dtLineContaining(range: line.stringRange) else { return nil }
let offsetX = dtLine.offset(forStringIndex: index - pageOffset(for: snapshot))
let offsetX = dtLine.offset(forStringIndex: index)
#else
let offsetX: CGFloat = 0
#endif
@@ -183,16 +184,11 @@ final class RDEPUBPageInteractionController {
#if canImport(DTCoreText)
private func dtLineContaining(range: NSRange) -> DTCoreTextLayoutLine? {
guard let dtLayoutFrame, let snapshot else { return nil }
let localLocation = max(range.location - pageOffset(for: snapshot), 0)
return dtLayoutFrame.lineContaining(UInt(localLocation))
guard let dtLayoutFrame else { return nil }
return dtLayoutFrame.lineContaining(UInt(max(range.location, 0)))
}
#endif
private func pageOffset(for snapshot: RDEPUBPageLayoutSnapshot) -> Int {
snapshot.page.pageStartOffset
}
private func mergeAdjacentRects(_ rects: [CGRect]) -> [CGRect] {
guard rects.count > 1 else { return rects }