Skip to content

Add gap-tree multi-column reading order sorting - #2

Open
luw2007 wants to merge 3 commits into
batu3384:mainfrom
luw2007:feature/gap-tree-multicolumn-sorting
Open

luw2007 wants to merge 3 commits into
batu3384:mainfrom
luw2007:feature/gap-tree-multicolumn-sorting

Conversation

@luw2007

@luw2007 luw2007 commented Sep 10, 2026

Copy link
Copy Markdown

Summary

Builds on #1 (Monospace Layout toggle). Adds a Umi-OCR-style gap tree algorithm that detects column boundaries from continuous vertical gaps and sorts OCR blocks in human reading order — the left column is read fully top-to-bottom before moving to the right column.

This solves the multi-column document problem where naive y-sorting interleaves rows from different columns (e.g., Left1 Right1\nLeft2 Right2 instead of Left1\nLeft2\nRight1\nRight2).

Note: This PR is stacked on #1. It includes the monospace layout changes from #1 plus the gap tree additions. Once #1 is merged, this PR's diff will shrink to only the gap-tree-specific changes.

Algorithm

  1. detectVerticalCuts: Divide the y-axis into 20 bands. For each band, find x-ranges with no blocks. An x-position is a column boundary if it's a gap in ≥40% of bands. Nearby cuts are merged.

  2. detectHorizontalCuts: Cluster blocks into rows, then find row gaps exceeding 2× the median gap — these mark paragraph/section boundaries.

  3. buildLayoutTree: Recursively split the block set — vertical cuts first (columns), then horizontal cuts (row groups). Leaf nodes hold unsplit blocks.

  4. traverseLayoutTree: Pre-order traversal — left subtree before right (for vertical splits), top before bottom (for horizontal splits).

The algorithm is OCR-engine-agnostic; it only requires (x0, y0, x1, y1) per block.

Changes (gap-tree-specific)

File Change
Models/OCRResult.swift readingOrderSortedBlocks() + LayoutNode enum + detectVerticalCuts, detectHorizontalCuts, buildLayoutTree, traverseLayoutTree; monospaceAlignedText gains multicolumnSorting parameter
Models/AppModels.swift MonospaceLayoutSettings.multicolumnSortingEnabled flag
AppState.swift setMonospaceMulticolumnSorting(_:) setter
Services/CaptureCoordinator.swift Pass multicolumnSorting through in both capture and watch modes
Views/Settings/SettingsTabViews.swift Sub-toggle under Monospace Layout section, with description
Views/SettingsView.swift Binding for multicolumn sorting
ScreenTextGrabTests/OCRResultTests.swift 4 new gap-tree tests

Design decisions

  • Composable with monospace alignment: The gap tree sorts blocks before row clustering in monospaceAlignedText. When multicolumnSorting: false (default), behavior is unchanged.
  • Optional sub-toggle: Lives under the Monospace Layout section in Settings, only visible when monospace is enabled. Defaults to off to avoid surprising users with reordered output.
  • Vertical cuts prioritized over horizontal: Column detection takes precedence because multi-column reading order is the primary use case. Horizontal cuts handle paragraph grouping within columns.

Testing

  • Two-column layout: left column (L1, L2, L3) fully before right column (R1, R2, R3)
  • Single-column layout: returns original order unchanged
  • Monospace with sorting: left-column rows appear before right-column rows
  • Monospace without sorting: same-y blocks interleave on a single line (baseline behavior)

swiftc -typecheck passes with zero errors.

Add a new .monospace CaptureOutputPreset that uses OCR block
boundingBox positions to produce monospace-aligned text:
- y-clustering groups blocks into lines
- x-sorting orders blocks within each line
- x-differences convert to spaces to preserve horizontal position
- y-differences convert to blank lines using median line height

Interface changes:
- CaptureOutputFormatter.format() / clipboardPayload() accept optional
  OCRResult parameter
- CaptureOutputWriter.copyCapturedText accepts optional OCRResult
- CaptureCoordinator protocol and implementations pass OCRResult through
- Main capture flow and watch mode pass the actual OCRResult
- History repaste / menu bar / table review pass nil (no block data)

UI:
- Monospace appears automatically in output preset menus (allCases)
- Menu bar icon: textformat
- Localized titles in Turkish and English

Tests:
- OCRResultTests: 8 new tests covering horizontal alignment, multi-line,
  blank line insertion, empty/single block, x-sorting, explicit columns
- CaptureOutputFormatterTests: 2 new tests for monospace preset with and
  without OCRResult
Replace the standalone .monospace output preset with a toggle-based
feature. When enabled, the capture pipeline uses OCR block boundingBox
positions to produce monospace-aligned text that preserves horizontal
and vertical layout.

Algorithm (ported from ocr-layout.swift):
- Fixed-column horizontal alignment: col = Int(x * columns), pad to col
- y-clustering into rows (threshold 0.02)
- Median row height for blank line estimation
- Vertical gaps convert to blank lines (capped at 6)

Changes:
- AppModels: MonospaceLayoutSettings struct + UserDefaults store
  (isEnabled Bool, columns Int default 120, clamped 20-300)
- AppState: @published monospaceLayoutSettings, setters, persist
- OCRResult: monospaceAlignedText(columns:) using fixed-column algorithm
- CaptureCoordinator: resolvedRawText(for:) applies monospace when toggle ON;
  works for both normal capture and watch mode
- Settings UI: Toggle + column slider (40-200) in General tab,
  under Output Format section
- Tests: 9 new OCRResultTests covering horizontal position, multi-line,
  blank lines, empty/single block, x-sorting, explicit columns, clamping

No changes to CaptureOutputFormatter/CaptureOutputWriter interfaces —
the toggle is applied at the coordinator level before text reaches
the formatter, so it composes with any existing output preset.
Adds a Umi-OCR-style gap tree algorithm that detects column
boundaries from continuous vertical gaps and sorts blocks in
human reading order (left column fully, then right column).

Algorithm:
- detectVerticalCuts: divide y-axis into 20 bands, find x gaps
  present in >=40% of bands, merge nearby cuts
- detectHorizontalCuts: cluster into rows, find gaps >2x median
  as paragraph boundaries
- buildLayoutTree: recursive split by vertical cuts first (columns),
  then horizontal cuts (row groups), leaf nodes contain blocks
- traverseLayoutTree: pre-order traversal gives reading order

Integration:
- monospaceAlignedText gains multicolumnSorting parameter
- MonospaceLayoutSettings gains multicolumnSortingEnabled flag
- Settings UI: sub-toggle under Monospace Layout section
- Works for both normal capture and watch mode

Tests:
- Two-column layout reads left column first
- Single-column layout unchanged
- monospace with sorting: left rows before right rows
- monospace without sorting: same-y blocks interleave on one line
@luw2007
luw2007 force-pushed the feature/gap-tree-multicolumn-sorting branch from 7bd3532 to 380e491 Compare September 11, 2026 07:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant