feat: wire PdfPickerModal into Editor with hires texture upgrade via socket
This commit is contained in:
@@ -9,6 +9,8 @@ import { InboxZone } from '../canvas/InboxZone';
|
|||||||
import { LaserPointer } from '../canvas/LaserPointer';
|
import { LaserPointer } from '../canvas/LaserPointer';
|
||||||
import { VideoSprite } from '../canvas/sprites/VideoSprite';
|
import { VideoSprite } from '../canvas/sprites/VideoSprite';
|
||||||
import { ImageSprite } from '../canvas/sprites/ImageSprite';
|
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 { UploadManager } from '../stores/uploadManager';
|
||||||
import { AnnotationStore } from '../stores/annotationStore';
|
import { AnnotationStore } from '../stores/annotationStore';
|
||||||
import { PinOverlay } from '../canvas/PinOverlay';
|
import { PinOverlay } from '../canvas/PinOverlay';
|
||||||
@@ -61,7 +63,9 @@ interface CanvasSetupDeps {
|
|||||||
pasteOpts?: {
|
pasteOpts?: {
|
||||||
onTextPaste?: (data: { text: string; html: string; hasImage: boolean }) => void;
|
onTextPaste?: (data: { text: string; html: string; hasImage: boolean }) => void;
|
||||||
onShortTextPaste?: (text: string) => void;
|
onShortTextPaste?: (text: string) => void;
|
||||||
|
onPdfUploaded?: (data: PdfUploadedData) => void;
|
||||||
};
|
};
|
||||||
|
onPdfUploaded?: (data: PdfUploadedData) => void;
|
||||||
onCropModeChange?: (active: boolean) => void;
|
onCropModeChange?: (active: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +78,7 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
|
|||||||
boardData, resolvedBoardId, user, isPublicView,
|
boardData, resolvedBoardId, user, isPublicView,
|
||||||
canvasRef, selectionRef, undoRef, syncRef, inboxZoneRef, canvasContainerRef,
|
canvasRef, selectionRef, undoRef, syncRef, inboxZoneRef, canvasContainerRef,
|
||||||
uploadManager, onCanvasChange, showToast, setOnlineUsers, setSelectedLayerIds,
|
uploadManager, onCanvasChange, showToast, setOnlineUsers, setSelectedLayerIds,
|
||||||
pasteOpts,
|
pasteOpts, onPdfUploaded,
|
||||||
onCropModeChange,
|
onCropModeChange,
|
||||||
} = deps;
|
} = deps;
|
||||||
|
|
||||||
@@ -324,6 +328,38 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
|
|||||||
// Update upload manager — transitions video jobs from "processing" to "done"
|
// Update upload manager — transitions video jobs from "processing" to "done"
|
||||||
uploadManager.processingComplete(imageId);
|
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
|
// Find the video item by matching its DB image ID stored in scene data
|
||||||
for (const item of scene.items.values()) {
|
for (const item of scene.items.values()) {
|
||||||
if (item.data.type !== 'video') continue;
|
if (item.data.type !== 'video') continue;
|
||||||
@@ -513,9 +549,10 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
|
|||||||
dropTarget = canvasEl?.parentElement ?? null;
|
dropTarget = canvasEl?.parentElement ?? null;
|
||||||
}
|
}
|
||||||
if (dropTarget) {
|
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);
|
}, 50);
|
||||||
|
|
||||||
@@ -552,7 +589,7 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
|
|||||||
sharpnessCleanupRef.current = null;
|
sharpnessCleanupRef.current = null;
|
||||||
disconnectSocket();
|
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 };
|
return { annotationStore: annotationStoreRef.current, pinOverlay: pinOverlayRef.current, textEditor: textEditorRef.current, cropOverlayRef, mdOverlay: mdOverlayRef.current };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ import ReactDOM from 'react-dom';
|
|||||||
import MarkdownReadView from '../components/MarkdownReadView';
|
import MarkdownReadView from '../components/MarkdownReadView';
|
||||||
import PasteChoicePopup from '../components/PasteChoicePopup';
|
import PasteChoicePopup from '../components/PasteChoicePopup';
|
||||||
import MarkdownFormatToolbar from '../components/MarkdownFormatToolbar';
|
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'));
|
const LazyMarkdownEditView = React.lazy(() => import('../components/MarkdownEditView'));
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
@@ -147,6 +148,10 @@ export default function Editor({ isPublicView }: EditorProps) {
|
|||||||
const [minimapData, setMinimapData] = useState<{ items: any[]; viewportBounds: any; contentBounds: any }>({
|
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 },
|
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
|
// Derived
|
||||||
const { boardData, loading, error } = useBoardLoader(boardId);
|
const { boardData, loading, error } = useBoardLoader(boardId);
|
||||||
@@ -247,18 +252,104 @@ export default function Editor({ isPublicView }: EditorProps) {
|
|||||||
onCanvasChange([textData.id]);
|
onCanvasChange([textData.id]);
|
||||||
}, [canvasRef, onCanvasChange]);
|
}, [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
|
// Memoize pasteOpts so useCanvasSetup's effect doesn't re-run every render
|
||||||
const pasteOpts = useMemo(() => ({
|
const pasteOpts = useMemo(() => ({
|
||||||
onTextPaste: handleTextPaste,
|
onTextPaste: handleTextPaste,
|
||||||
onShortTextPaste: handleShortTextPaste,
|
onShortTextPaste: handleShortTextPaste,
|
||||||
}), [handleTextPaste, handleShortTextPaste]);
|
onPdfUploaded,
|
||||||
|
}), [handleTextPaste, handleShortTextPaste, onPdfUploaded]);
|
||||||
|
|
||||||
// Canvas setup (selection, undo, sync, socket, drag/drop, paste, inbox, annotations)
|
// Canvas setup (selection, undo, sync, socket, drag/drop, paste, inbox, annotations)
|
||||||
const { annotationStore, pinOverlay, textEditor, cropOverlayRef, mdOverlay } = useCanvasSetup({
|
const { annotationStore, pinOverlay, textEditor, cropOverlayRef, mdOverlay } = useCanvasSetup({
|
||||||
boardData, resolvedBoardId, user, isPublicView,
|
boardData, resolvedBoardId, user, isPublicView,
|
||||||
canvasRef, selectionRef, undoRef, syncRef, inboxZoneRef, canvasContainerRef,
|
canvasRef, selectionRef, undoRef, syncRef, inboxZoneRef, canvasContainerRef,
|
||||||
uploadManager, onCanvasChange, showToast, setOnlineUsers, setSelectedLayerIds,
|
uploadManager, onCanvasChange, showToast, setOnlineUsers, setSelectedLayerIds,
|
||||||
pasteOpts,
|
pasteOpts, onPdfUploaded,
|
||||||
onCropModeChange: setCropModeActive,
|
onCropModeChange: setCropModeActive,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1659,6 +1750,19 @@ export default function Editor({ isPublicView }: EditorProps) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{/* PDF page picker modal */}
|
||||||
|
{pdfPicker && resolvedBoardId && (
|
||||||
|
<PdfPickerModal
|
||||||
|
imageId={pdfPicker.imageId}
|
||||||
|
fileName={pdfPicker.fileName}
|
||||||
|
pageCount={pdfPicker.pageCount}
|
||||||
|
dimensions={pdfPicker.dimensions}
|
||||||
|
boardId={resolvedBoardId}
|
||||||
|
onPlace={handlePdfPlace}
|
||||||
|
onCancel={() => setPdfPicker(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Export dialog */}
|
{/* Export dialog */}
|
||||||
{showExport && (
|
{showExport && (
|
||||||
<ExportDialog
|
<ExportDialog
|
||||||
|
|||||||
Reference in New Issue
Block a user