diff --git a/src/pages/exam/setup/ExamTemplateSetupPage.tsx b/src/pages/exam/setup/ExamTemplateSetupPage.tsx index 5709f53..48a5633 100644 --- a/src/pages/exam/setup/ExamTemplateSetupPage.tsx +++ b/src/pages/exam/setup/ExamTemplateSetupPage.tsx @@ -29,7 +29,7 @@ const TOOLS = [ { id: SHAPE_TYPES.furniture, label: 'Furniture', icon: canvasShapePalette.furniture.icon, tip: 'Mark page numbers, margins, blank space, or decoration to exclude from extraction.', color: 'inherit' as const }, ] -const PAGE_START_X = 260 +const PAGE_START_X = 0 const PDF_PAGE_IDS_PREFIX = 'pdf-page-' function pageGeometryFromImages(pages: PdfPageImage[]): CanvasPageGeometry[] { @@ -41,6 +41,22 @@ function pageGeometryFromImages(pages: PdfPageImage[]): CanvasPageGeometry[] { }) } +function applyDocViewConstraints(editor: Editor, pages: PdfPageImage[]) { + const maxW = pages.length ? Math.max(...pages.map((p) => p.width)) : PAGE_WIDTH + const totalH = pages.reduce((sum, p) => sum + p.height, 0) || PAGE_HEIGHT + editor.setCameraOptions({ + constraints: { + bounds: { x: -64, y: -64, w: maxW + 128, h: totalH + 128 }, + padding: { x: 64, y: 64 }, + origin: { x: 0.5, y: 0 }, + initialZoom: 'fit-x-100', + baseZoom: 'default', + behavior: 'contain', + }, + isLocked: false, + }) +} + function apiMessage(err: unknown): { message: string; conflict: boolean } { if (axios.isAxiosError(err)) { const detail = (err.response?.data as { detail?: string } | undefined)?.detail @@ -176,6 +192,7 @@ const ExamTemplateSetupInner: React.FC = () => { ed.createShapes([{ id: shapeId, type: PDF_PAGE_SHAPE_TYPE, x: geometry.x, y: geometry.y, isLocked: true, props: { w: geometry.w, h: geometry.h, src: newPage.src, pageNumber: newPage.pageNumber } } as any]) bringDomainShapesToFront(ed) } + applyDocViewConstraints(ed, partialPages) } setPdfStatus('ready') }) @@ -193,6 +210,8 @@ const ExamTemplateSetupInner: React.FC = () => { syncPdfPages(editor, pages) loadShapes(editor, shapesFromTemplate(detail, geometries)) bringDomainShapesToFront(editor) + applyDocViewConstraints(editor, pages) + editor.resetZoom() } setDirty(false) } catch (e) { @@ -248,25 +267,10 @@ const ExamTemplateSetupInner: React.FC = () => { )), [activeTool]) return ( - t.zIndex.drawer + 20, bgcolor: 'background.default' }}> - - { - editorRef.current = editor - editor.user.updateUserPreferences({ colorScheme: theme.palette.mode === 'dark' ? 'dark' : 'light' }) - editor.store.listen(() => setDirty(true), { scope: 'document' }) - if (template) loadShapes(editor, shapesFromTemplate(template, pageGeometriesRef.current)); else seedGuide(editor) - bringDomainShapesToFront(editor) - }} - /> - + t.zIndex.drawer + 20, bgcolor: 'background.default', display: 'flex', flexDirection: 'column' }}> - + {/* Top bar */} + @@ -277,32 +281,62 @@ const ExamTemplateSetupInner: React.FC = () => { - - {toolButtons} - + {/* Body row */} + - - Setup guide - - 1) Boundary start/end lines define each main question. 2) Draw amber Part boxes for markable sub-questions. 3) Draw coloured Response, Context, Q Number, Mark Area, Reference, and Furniture regions; Save derives parent links by containment. - - - {(['boundary', 'part', 'response', 'context', 'question_number', 'mark_area', 'reference', 'furniture'] as const).map((kind) => { - const p = canvasShapePalette[kind] - return - })} - - - Multi-page boundary pairing - Draw “Q start” on page N, then “Q end” on a later page; save pairs boundaries by reading order into one question span. - Open design choices resolved for v1: labels use “Q start/end”; persistent Attached pills confirm containment; rectangles stay simple for dense multi-column papers; Back button remains explicit. - - PDF backdrop: {pdfStatus === 'ready' ? 'loaded and locked behind regions' : pdfStatus === 'loading' ? 'loading…' : pdfStatus === 'missing' ? 'no source PDF for this template' : pdfError ?? 'failed to load'} - - + {/* Left tool sidebar */} + + {toolButtons} + - {loading && } - {conflict && setConflict(null)}>{conflict}} + {/* Canvas area */} + + + { + editorRef.current = editor + editor.user.updateUserPreferences({ colorScheme: theme.palette.mode === 'dark' ? 'dark' : 'light' }) + editor.store.listen(() => setDirty(true), { scope: 'document' }) + applyDocViewConstraints(editor, []) + editor.resetZoom() + if (template) loadShapes(editor, shapesFromTemplate(template, pageGeometriesRef.current)); else seedGuide(editor) + bringDomainShapesToFront(editor) + }} + /> + + + {/* Guide panel */} + + Setup guide + + 1) Boundary start/end lines define each main question. 2) Draw amber Part boxes for markable sub-questions. 3) Draw coloured Response, Context, Q Number, Mark Area, Reference, and Furniture regions; Save derives parent links by containment. + + + {(['boundary', 'part', 'response', 'context', 'question_number', 'mark_area', 'reference', 'furniture'] as const).map((kind) => { + const p = canvasShapePalette[kind] + return + })} + + + Multi-page boundary pairing + Draw "Q start" on page N, then "Q end" on a later page; save pairs boundaries by reading order into one question span. + Open design choices resolved for v1: labels use "Q start/end"; persistent Attached pills confirm containment; rectangles stay simple for dense multi-column papers; Back button remains explicit. + + PDF backdrop: {pdfStatus === 'ready' ? 'loaded and locked behind regions' : pdfStatus === 'loading' ? 'loading…' : pdfStatus === 'missing' ? 'no source PDF for this template' : pdfError ?? 'failed to load'} + + + + {/* Conflict alert */} + {conflict && setConflict(null)}>{conflict}} + + + + {loading && } setError(null)}> setError(null)}>{error} )