From 9c8b3cc5f55e94fff6cb9e800ea8827da7caccfe Mon Sep 17 00:00:00 2001 From: Hiren Kangad Date: Sat, 14 Mar 2026 11:31:45 +0530 Subject: [PATCH] feat: wire PdfPickerModal into Editor with hires texture upgrade via socket --- frontend/src/hooks/useCanvasSetup.ts | 45 ++++++++++- frontend/src/pages/Editor.tsx | 110 ++++++++++++++++++++++++++- 2 files changed, 148 insertions(+), 7 deletions(-) diff --git a/frontend/src/hooks/useCanvasSetup.ts b/frontend/src/hooks/useCanvasSetup.ts index 9d9deb0..2101bfd 100644 --- a/frontend/src/hooks/useCanvasSetup.ts +++ b/frontend/src/hooks/useCanvasSetup.ts @@ -9,6 +9,8 @@ import { InboxZone } from '../canvas/InboxZone'; import { LaserPointer } from '../canvas/LaserPointer'; import { VideoSprite } from '../canvas/sprites/VideoSprite'; import { ImageSprite } from '../canvas/sprites/ImageSprite'; +import { PdfPageSprite } from '../canvas/sprites/PdfPageSprite'; +import type { PdfUploadedData } from '../canvas/image-drop'; import { UploadManager } from '../stores/uploadManager'; import { AnnotationStore } from '../stores/annotationStore'; import { PinOverlay } from '../canvas/PinOverlay'; @@ -61,7 +63,9 @@ interface CanvasSetupDeps { pasteOpts?: { onTextPaste?: (data: { text: string; html: string; hasImage: boolean }) => void; onShortTextPaste?: (text: string) => void; + onPdfUploaded?: (data: PdfUploadedData) => void; }; + onPdfUploaded?: (data: PdfUploadedData) => void; onCropModeChange?: (active: boolean) => void; } @@ -74,7 +78,7 @@ export function useCanvasSetup(deps: CanvasSetupDeps) { boardData, resolvedBoardId, user, isPublicView, canvasRef, selectionRef, undoRef, syncRef, inboxZoneRef, canvasContainerRef, uploadManager, onCanvasChange, showToast, setOnlineUsers, setSelectedLayerIds, - pasteOpts, + pasteOpts, onPdfUploaded, onCropModeChange, } = deps; @@ -324,6 +328,38 @@ export function useCanvasSetup(deps: CanvasSetupDeps) { // Update upload manager — transitions video jobs from "processing" to "done" uploadManager.processingComplete(imageId); + // PDF thumbnail arrived — update placed pdf-page items + if (data.type === 'pdf-thumbnail') { + for (const item of scene.items.values()) { + if (item.data.type !== 'pdf-page') continue; + const d = item.data as any; + if (d.pdfImageId === imageId && d.pageNumber === data.pageNumber) { + d.thumb = data.thumbAssetKey; + if (item.displayObject instanceof PdfPageSprite) { + item.displayObject.setThumb(data.thumbAssetKey); + } + break; + } + } + return; + } + + // PDF hires arrived — upgrade texture on placed pdf-page items + if (data.type === 'pdf-hires') { + for (const item of scene.items.values()) { + if (item.data.type !== 'pdf-page') continue; + const d = item.data as any; + if (d.pdfImageId === imageId && d.pageNumber === data.pageNumber) { + d.asset = data.hiresAssetKey; + if (item.displayObject instanceof PdfPageSprite) { + item.displayObject.upgradeTexture(data.hiresAssetKey); + } + break; + } + } + return; + } + // Find the video item by matching its DB image ID stored in scene data for (const item of scene.items.values()) { if (item.data.type !== 'video') continue; @@ -513,9 +549,10 @@ export function useCanvasSetup(deps: CanvasSetupDeps) { dropTarget = canvasEl?.parentElement ?? null; } if (dropTarget) { - dropCleanupRef.current = setupDragDrop(dropTarget, viewport, scene, resolvedBoardId, onCanvasChange, selection, uploadManager); + dropCleanupRef.current = setupDragDrop(dropTarget, viewport, scene, resolvedBoardId, onCanvasChange, selection, uploadManager, onPdfUploaded); } - pasteCleanupRef.current = setupPaste(viewport, scene, resolvedBoardId, onCanvasChange, selection, uploadManager, pasteOpts); + const mergedPasteOpts = pasteOpts ? { ...pasteOpts, onPdfUploaded } : { onPdfUploaded }; + pasteCleanupRef.current = setupPaste(viewport, scene, resolvedBoardId, onCanvasChange, selection, uploadManager, mergedPasteOpts); } }, 50); @@ -552,7 +589,7 @@ export function useCanvasSetup(deps: CanvasSetupDeps) { sharpnessCleanupRef.current = null; disconnectSocket(); }; - }, [boardData, resolvedBoardId, user, isPublicView, onCanvasChange, showToast, canvasRef, selectionRef, undoRef, syncRef, inboxZoneRef, uploadManager, setOnlineUsers, setSelectedLayerIds, pasteOpts, onCropModeChange]); + }, [boardData, resolvedBoardId, user, isPublicView, onCanvasChange, showToast, canvasRef, selectionRef, undoRef, syncRef, inboxZoneRef, uploadManager, setOnlineUsers, setSelectedLayerIds, pasteOpts, onPdfUploaded, onCropModeChange]); return { annotationStore: annotationStoreRef.current, pinOverlay: pinOverlayRef.current, textEditor: textEditorRef.current, cropOverlayRef, mdOverlay: mdOverlayRef.current }; } diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index f341cba..3b79cfb 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -46,7 +46,8 @@ import ReactDOM from 'react-dom'; import MarkdownReadView from '../components/MarkdownReadView'; import PasteChoicePopup from '../components/PasteChoicePopup'; import MarkdownFormatToolbar from '../components/MarkdownFormatToolbar'; -import { saveCanvas } from '../api'; +import PdfPickerModal from '../components/PdfPickerModal'; +import api, { saveCanvas } from '../api'; const LazyMarkdownEditView = React.lazy(() => import('../components/MarkdownEditView')); // Hooks @@ -147,6 +148,10 @@ export default function Editor({ isPublicView }: EditorProps) { const [minimapData, setMinimapData] = useState<{ items: any[]; viewportBounds: any; contentBounds: any }>({ items: [], viewportBounds: { x: 0, y: 0, w: 1, h: 1 }, contentBounds: { x: 0, y: 0, w: 1, h: 1 }, }); + const [pdfPicker, setPdfPicker] = useState<{ + imageId: string; fileName: string; pageCount: number; + dimensions: Array<{ w: number; h: number }>; + } | null>(null); // Derived const { boardData, loading, error } = useBoardLoader(boardId); @@ -247,18 +252,104 @@ export default function Editor({ isPublicView }: EditorProps) { onCanvasChange([textData.id]); }, [canvasRef, onCanvasChange]); + // PDF upload callback — opens picker for multi-page PDFs + const onPdfUploaded = useCallback((data: { imageId: string; fileName: string; pageCount: number; dimensions: Array<{ w: number; h: number }>; singlePage: boolean }) => { + if (data.singlePage) return; // single-page PDFs are placed directly + setPdfPicker({ + imageId: data.imageId, + fileName: data.fileName, + pageCount: data.pageCount, + dimensions: data.dimensions, + }); + }, []); + + // Handle placing selected PDF pages from the picker + const handlePdfPlace = useCallback(async (selectedPages: number[]) => { + if (!pdfPicker || !resolvedBoardId) return; + const scene = canvasRef.current?.getScene(); + const viewport = canvasRef.current?.getViewport(); + if (!scene || !viewport) return; + + try { + // Request hires rendering for selected pages + await api.post(`/api/boards/${resolvedBoardId}/pdf-pages`, { + imageId: pdfPicker.imageId, + pages: selectedPages, + }); + } catch (err) { + console.error('[Editor] pdf-pages request failed:', err); + } + + // Place pages in a grid at viewport center + const cx = viewport.center.x; + const cy = viewport.center.y; + const GAP = 20; + const cols = Math.ceil(Math.sqrt(selectedPages.length)) || 1; + let col = 0; + let cursorY = cy; + let rowMaxH = 0; + const colWidths: number[] = new Array(cols).fill(0); + const newItemIds: string[] = []; + + for (const page of selectedPages) { + const dim = pdfPicker.dimensions[page - 1] || { w: 612, h: 792 }; + // Cap each page to 600px max dimension + const maxDim = 600; + let w = dim.w; + let h = dim.h; + if (w > maxDim || h > maxDim) { + const scale = maxDim / Math.max(w, h); + w = Math.round(w * scale); + h = Math.round(h * scale); + } + + let cursorX = cx; + for (let c = 0; c < col; c++) cursorX += (colWidths[c] || 220) + GAP; + + const item = scene.addPdfPageFromUpload( + null, null, w, h, + cursorX, cursorY, + pdfPicker.imageId, pdfPicker.fileName, + page, pdfPicker.pageCount, + ); + newItemIds.push(item.id); + if (w > (colWidths[col] || 0)) colWidths[col] = w; + if (h > rowMaxH) rowMaxH = h; + + col++; + if (col >= cols) { + col = 0; + cursorY += rowMaxH + GAP; + rowMaxH = 0; + } + } + + // Select all placed items + const selection = selectionRef.current; + if (selection && newItemIds.length > 0) { + selection.selectOnly(newItemIds[0]); + for (let i = 1; i < newItemIds.length; i++) { + selection.toggle(newItemIds[i]); + } + } + + onCanvasChange(); + setPdfPicker(null); + }, [pdfPicker, resolvedBoardId, canvasRef, selectionRef, onCanvasChange]); + // Memoize pasteOpts so useCanvasSetup's effect doesn't re-run every render const pasteOpts = useMemo(() => ({ onTextPaste: handleTextPaste, onShortTextPaste: handleShortTextPaste, - }), [handleTextPaste, handleShortTextPaste]); + onPdfUploaded, + }), [handleTextPaste, handleShortTextPaste, onPdfUploaded]); // Canvas setup (selection, undo, sync, socket, drag/drop, paste, inbox, annotations) const { annotationStore, pinOverlay, textEditor, cropOverlayRef, mdOverlay } = useCanvasSetup({ boardData, resolvedBoardId, user, isPublicView, canvasRef, selectionRef, undoRef, syncRef, inboxZoneRef, canvasContainerRef, uploadManager, onCanvasChange, showToast, setOnlineUsers, setSelectedLayerIds, - pasteOpts, + pasteOpts, onPdfUploaded, onCropModeChange: setCropModeActive, }); @@ -1659,6 +1750,19 @@ export default function Editor({ isPublicView }: EditorProps) { ); })} + {/* PDF page picker modal */} + {pdfPicker && resolvedBoardId && ( + setPdfPicker(null)} + /> + )} + {/* Export dialog */} {showExport && (