ReadViewSDK/Sources/RDReaderView/EPUBUI/RDEPUBSettingsFlipAutomation.swift
shenlei 5a41066b66 修复 EPUB 文本分页显示错位并补充分页调试能力
本次提交围绕 DTCoreText 文本页的分页一致性、交互索引和高亮命中进行了集中修复。

主要改动:

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

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

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

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

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

6. 放宽 inline attachment 的 avoid-break 处理,并补充相关分页问题调查文档与索引。
2026-07-08 14:27:36 +09:00

85 lines
3.5 KiB
Swift

import UIKit
/// Debug-only automation (`--demo-settings-flip`) that replays the user
/// gesture sequence suspected of producing stale page tables: open the
/// settings panel, change the line height, close the panel, flip pages
/// with several timing variants aimed at the preview-repagination and
/// in-flight chapter build races. Combine with
/// `--demo-pagination-validate` to detect any resulting metric mismatch.
enum RDEPUBSettingsFlipAutomation {
static let isEnabled = ProcessInfo.processInfo.arguments.contains("--demo-settings-flip")
private(set) static var hasStarted = false
static func startIfNeeded(controller: RDEPUBReaderController) {
guard isEnabled, !hasStarted else { return }
hasStarted = true
print("[SETTINGS-FLIP] scheduled")
// Pass 1 starts while progressive pagination of the freshly opened
// book is still running, so prefetch builds are in flight.
run(after: 2.0) { [weak controller] in
guard let controller else { return }
pass(controller: controller, index: 1, lineHeight: 1.8, changeToCloseDelay: 1.2) {
pass(controller: controller, index: 2, lineHeight: 1.6, changeToCloseDelay: 0.05) {
pass(controller: controller, index: 3, lineHeight: 1.8, changeToCloseDelay: 0.3) {
print("[SETTINGS-FLIP] finished all passes")
}
}
}
}
}
/// One panel round-trip: present change line height close after
/// `changeToCloseDelay` flip pages forward and back.
private static func pass(
controller: RDEPUBReaderController,
index: Int,
lineHeight: CGFloat,
changeToCloseDelay: TimeInterval,
completion: @escaping () -> Void
) {
print("[SETTINGS-FLIP] pass \(index) present panel")
controller.presentSettings()
run(after: 0.7) { [weak controller] in
guard let controller else { return }
print("[SETTINGS-FLIP] pass \(index) set lineHeightMultiple=\(lineHeight)")
controller.updateConfiguration { $0.lineHeightMultiple = lineHeight }
run(after: changeToCloseDelay) { [weak controller] in
guard let controller else { return }
print("[SETTINGS-FLIP] pass \(index) close panel")
controller.dismiss(animated: true) { [weak controller] in
controller?.runtime.settingsPanelDidDisappear()
run(after: 1.0) { [weak controller] in
guard let controller else { return }
flipPages(controller: controller, index: index, completion: completion)
}
}
}
}
}
private static func flipPages(
controller: RDEPUBReaderController,
index: Int,
completion: @escaping () -> Void
) {
let startPage = controller.readerView.currentPage + 1
let offsets = [1, 2, 3, 2, 1, 0]
print("[SETTINGS-FLIP] pass \(index) flip pages from \(startPage)")
for (step, offset) in offsets.enumerated() {
run(after: 0.6 * Double(step + 1)) { [weak controller] in
_ = controller?.go(toPageNumber: startPage + offset, animated: false)
}
}
run(after: 0.6 * Double(offsets.count + 1) + 0.5, block: completion)
}
private static func run(after delay: TimeInterval, block: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: block)
}
}