Vertical zone split: detect divider lines and create independent sub-zones
Pages with two side-by-side vocabulary columns separated by a vertical black line are now split into independent sub-zones before row/column detection. Each sub-zone gets its own rows, preventing misalignment from different heading rhythms. - _detect_vertical_dividers(): finds pipe word_boxes at consistent x positions spanning >50% of zone height - _split_zone_at_vertical_dividers(): creates left/right PageZone objects with layout_hint and vsplit_group metadata - Column union skips vsplit zones (independent column sets) - Frontend renders vsplit zones side by side via flex layout - PageZone gets layout_hint + vsplit_group fields Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -186,25 +186,66 @@ export function GridEditor({ sessionId, onNext }: GridEditorProps) {
|
||||
<GridImageOverlay sessionId={sessionId} grid={grid} />
|
||||
)}
|
||||
|
||||
{/* Zone tables */}
|
||||
{/* Zone tables — group vsplit zones side by side */}
|
||||
<div className="space-y-4">
|
||||
{grid.zones.map((zone) => (
|
||||
<div
|
||||
key={zone.zone_index}
|
||||
className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"
|
||||
>
|
||||
<GridTable
|
||||
zone={zone}
|
||||
layoutMetrics={grid.layout_metrics}
|
||||
selectedCell={selectedCell}
|
||||
onSelectCell={setSelectedCell}
|
||||
onCellTextChange={updateCellText}
|
||||
onToggleColumnBold={toggleColumnBold}
|
||||
onToggleRowHeader={toggleRowHeader}
|
||||
onNavigate={handleNavigate}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{(() => {
|
||||
// Group consecutive zones with same vsplit_group
|
||||
const groups: typeof grid.zones[][] = []
|
||||
for (const zone of grid.zones) {
|
||||
const prev = groups[groups.length - 1]
|
||||
if (
|
||||
prev &&
|
||||
zone.vsplit_group != null &&
|
||||
prev[0].vsplit_group === zone.vsplit_group
|
||||
) {
|
||||
prev.push(zone)
|
||||
} else {
|
||||
groups.push([zone])
|
||||
}
|
||||
}
|
||||
return groups.map((group) =>
|
||||
group.length === 1 ? (
|
||||
<div
|
||||
key={group[0].zone_index}
|
||||
className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"
|
||||
>
|
||||
<GridTable
|
||||
zone={group[0]}
|
||||
layoutMetrics={grid.layout_metrics}
|
||||
selectedCell={selectedCell}
|
||||
onSelectCell={setSelectedCell}
|
||||
onCellTextChange={updateCellText}
|
||||
onToggleColumnBold={toggleColumnBold}
|
||||
onToggleRowHeader={toggleRowHeader}
|
||||
onNavigate={handleNavigate}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
key={`vsplit-${group[0].vsplit_group}`}
|
||||
className="flex gap-2"
|
||||
>
|
||||
{group.map((zone) => (
|
||||
<div
|
||||
key={zone.zone_index}
|
||||
className="flex-1 min-w-0 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"
|
||||
>
|
||||
<GridTable
|
||||
zone={zone}
|
||||
layoutMetrics={grid.layout_metrics}
|
||||
selectedCell={selectedCell}
|
||||
onSelectCell={setSelectedCell}
|
||||
onCellTextChange={updateCellText}
|
||||
onToggleColumnBold={toggleColumnBold}
|
||||
onToggleRowHeader={toggleRowHeader}
|
||||
onNavigate={handleNavigate}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Tip */}
|
||||
|
||||
@@ -52,6 +52,8 @@ export interface GridZone {
|
||||
rows: GridRow[]
|
||||
cells: GridEditorCell[]
|
||||
header_rows: number[]
|
||||
layout_hint?: 'left_of_vsplit' | 'right_of_vsplit' | 'middle_of_vsplit'
|
||||
vsplit_group?: number
|
||||
}
|
||||
|
||||
export interface BBox {
|
||||
|
||||
Reference in New Issue
Block a user