feat: implement reference image comparison tools and upload functionality

This commit is contained in:
Leivur R. Djurhuus 2026-03-14 12:58:57 -05:00
parent bac6d4c107
commit fb72ef6dc4

View file

@ -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 `<ImageCompareSlider>` 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)