From fb72ef6dc443be0ba92904c44291234a5bcd38d0 Mon Sep 17 00:00:00 2001 From: "Leivur R. Djurhuus" Date: Sat, 14 Mar 2026 12:58:57 -0500 Subject: [PATCH] feat: implement reference image comparison tools and upload functionality --- IMPLEMENTATION_PLAN.md | 64 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 266cf77..47f3541 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -290,6 +290,70 @@ src/ - Excel export (multi-sheet styled workbook) - Deadline tracking (approaching/overdue detection + notifications) +### Phase 3b: Reference Image Comparison (Markup Review) +- **Reference image upload** — attach reference/approved images to any revision round. Stored via the existing `attachments` JSON field on `Revision`, with file upload to local storage (Phase 1) or S3/Cloud (production). Supports PNG, JPG, WebP up to 10MB. +- **Current render upload** — upload the current WIP render alongside the reference for direct comparison. Each revision round can have a "reference" image and a "current" image. +- **A-B comparison slider** — draggable vertical divider overlaying reference (left) and current (right) images. User drags to reveal/hide each side. Built as a `` component using pointer events + CSS clip-path. No external dependency needed. +- **Toggle mode** — click/tap to flip between reference and current image (full-frame swap with crossfade transition). Keyboard shortcut: `Space` to toggle. Useful on smaller screens where the slider is impractical. +- **Side-by-side mode** — display both images at equal size in a horizontal split. Synced zoom/pan so both images move together. Default on wide screens. +- **Zoom & pan** — scroll-to-zoom with pinch support. Pan by click-drag when zoomed. Zoom level synced across both images in all comparison modes. +- **Image gallery per revision** — revision rounds accumulate images over time. Gallery shows all uploaded images for the stage across rounds, with round number labels. Quick-select any past round's image as the reference for comparison. +- **Context in StageDetailSheet** — new "Compare" tab in the stage detail sheet (alongside existing Revisions and Comments tabs). Comparison tools live here. Selecting a revision round auto-loads its images. +- **Lightweight markup overlay** (optional enhancement) — basic annotation tools (arrow, rectangle, freehand, text pin) drawn on top of the current image. Annotations saved as JSON on the revision. Not a full drawing app — just enough to point at problems. + +#### Data Model Changes +``` +// Extend Revision.attachments JSON schema: +{ + referenceImage?: { url: string, filename: string, size: number, uploadedAt: string }, + currentImage?: { url: string, filename: string, size: number, uploadedAt: string }, + annotations?: Array<{ type: "arrow"|"rect"|"freehand"|"pin", points: number[], text?: string, color: string }> +} +``` + +#### New API Endpoints +``` +POST /api/stages/:stageId/revisions/:revisionId/upload — multipart form upload (reference or current image) +DELETE /api/stages/:stageId/revisions/:revisionId/upload — remove an uploaded image +``` + +#### New Components +``` +src/components/revisions/ +├── image-compare-slider.tsx — A-B slider with drag handle +├── image-toggle.tsx — Toggle/crossfade between two images +├── image-side-by-side.tsx — Synced side-by-side with zoom/pan +├── image-upload-zone.tsx — Drag-and-drop upload for reference/current +├── revision-image-gallery.tsx — Browse images across revision rounds +├── compare-tab.tsx — Orchestrator: mode selector + comparison view +└── markup-overlay.tsx — Lightweight annotation layer (optional) +``` + +#### PNG Transparency Handling +CG renders are typically PNGs with transparent backgrounds and semi-transparent drop shadows. When two transparent PNGs are layered in comparison modes (slider, toggle, overlay), the shadows multiply and produce incorrect visual results — making review unreliable. + +**Solution: server-side alpha compositing on upload.** +- On the upload endpoint, detect if the image is a PNG with an alpha channel (check IHDR color type via `sharp` or raw buffer inspection). +- If alpha is present, composite the image onto a solid **white (#FFFFFF) background** using `sharp.flatten({ background: '#FFFFFF' })` before storing. +- Store the flattened version as the comparison-ready image. Optionally keep the original transparent PNG as a separate `originalUrl` for download purposes. +- JPG and WebP uploads (no alpha) skip this step — no processing needed. +- This happens once at upload time, not at render time — avoids per-view processing cost and ensures all comparison modes work correctly without CSS hacks. + +``` +// Upload processing pipeline (pseudo): +const img = sharp(buffer); +const metadata = await img.metadata(); +if (metadata.format === 'png' && metadata.hasAlpha) { + buffer = await img.flatten({ background: '#FFFFFF' }).png().toBuffer(); +} +``` + +#### UX Notes +- Images are almost never reviewed in isolation — the reference comparison is the primary review workflow. The compare tab should be the default/first tab when images exist. +- Default to slider mode on desktop, toggle mode on mobile. +- Reference images persist across rounds (the approved reference doesn't change until a new one is set). Current images update each round. +- Empty state: prompt to upload a reference image with a clear CTA. Show how comparison works with a placeholder illustration. + ### Phase 4: Polish - Command palette (Cmd+K search) - Bulk operations (multi-select in table → bulk status/assignment/priority)