Direct Manipulation on a Spatial Canvas: How to Build a Correction Layer Over an Automatic Pipeline
TLDR
Tuning global threshold sliders to fix region classification errors on specific document pages introduces side effects on other pages. A more effective solution is providing a direct spatial canvas editing layer where users can select, resize, reclassify, or suppress specific bounding boxes on screen. Normalizing canvas interaction coordinates into worker space guarantees that user corrections persist accurately across display resizes and re-extractions.
| Correction Strategy | Modification Scope | Side Effect Risk | Precision |
|---|---|---|---|
| Global Threshold Sliders | Entire document / All pages | High (Fixing page 3 breaks page 7) | Low (Imprecise parameter tuning) |
| Canvas Region Editor | Single target bounding box | Zero (Local override isolation) | High (Pixel-exact direct manipulation) |
Problem statement: the fallacy of global threshold tuning
When an automated PDF Processor misclassifies a table on page 3, adjusting global parameters (e.g., column gap distance, Y-band tolerances) alters extraction logic for all pages in the document.
A parameter value that fixes page 3 often corrupts previously clean layouts on page 7.
Local structural errors require direct spatial correction tools rather than global parameter adjustments.
Technical architecture & implementation
1. Dual-Mode canvas interaction model
Maintain two distinct interaction modes via toolbar state:
- Select / Edit Mode: Select regions, drag eight resize handles, translate boxes, reclassify via hotkeys (
Tfor Table,Pfor Paragraph), or suppress viaDelete. - Column Split Mode: Draw vertical guideline overlays that bypass automatic column detection.
2. Coordinate space normalization
Convert UI canvas display pixels to normalized pipeline worker space at interaction time to ensure corrections survive canvas resizes:
// Convert display canvas pixels to worker viewport space
export function canvasPxToWorkerPx(canvasPx, canvasWidthPx, workerWidthPx) {
return (canvasPx / canvasWidthPx) * workerWidthPx;
}
// Convert worker viewport space back to display canvas pixels for rendering export function workerPxToCanvasPx(workerPx, workerWidthPx, canvasWidthPx) { return (workerPx / workerWidthPx) * canvasWidthPx; }
3. Interactive eight-handle bounding box resizing
Render corner and midpoint handles for selected regions, mapping cursor styles to handle directions:
const HANDLE_CURSORS = {
nw: "nw-resize",
n: "n-resize",
ne: "ne-resize",
w: "w-resize",
e: "e-resize",
sw: "sw-resize",
s: "s-resize",
se: "se-resize",
};
export function resizeRegionBoundingBox( region, activeHandle, deltaX, deltaY, minSizePx = 4, ) { const updated = { ...region.bbox };
if (activeHandle.includes("w")) { const newWidth = updated.w - deltaX; if (newWidth >= minSizePx) { updated.x += deltaX; updated.w = newWidth; } } if (activeHandle.includes("e")) { updated.w = Math.max(minSizePx, updated.w + deltaX); } if (activeHandle.includes("n")) { const newHeight = updated.h - deltaY; if (newHeight >= minSizePx) { updated.y += deltaY; updated.h = newHeight; } } if (activeHandle.includes("s")) { updated.h = Math.max(minSizePx, updated.h + deltaY); }
return updated; }
4. Manual column split bypass
When users place manual vertical split lines, skip automatic column scanning entirely:
export function resolvePageColumnSplits(
manualSplits,
textMeta,
viewport,
scale,
) {
// Manual Split Bypass: If user drew split lines, use them exclusively
if (manualSplits && manualSplits.length > 0) {
const validSplits = manualSplits
.filter((s) => s.x > viewport.width 0.05 && s.x < viewport.width 0.95)
.sort((a, b) => a.x - b.x);
return buildColumnZonesFromManualSplits(validSplits, viewport.width); }
// Fallback to automatic bipartite column detector return detectPageColumns(textMeta, viewport, scale); }
Rule of thumb: Store spatial UI canvas corrections in worker coordinate space, and allow manual split overlays to bypass automated layout detectors.