import * as fabric from 'fabric'; import { v4 as uuidv4 } from 'uuid'; import { useEditorStore } from '../store'; import type { ToolHandler } from '../../../lib/annotations/registry'; import type { Annotation, TextAnnotation } from '../../../lib/annotations/types'; import { screenToPdf, pdfRectToScreen } from '../../../lib/coords'; import type { ViewportParams } from '../../../lib/coords'; export const TextTool: ToolHandler = { name: 'text', onActivate: (canvas: fabric.Canvas) => { canvas.defaultCursor = 'text'; canvas.selection = false; }, onDeactivate: (canvas: fabric.Canvas) => { canvas.defaultCursor = 'default'; }, onPointerDown: (e: any, canvas: fabric.Canvas, vp: ViewportParams, pageNumber: number) => { // If we clicked on an existing object, don't create a new one if (e.target) return; // Create the Fabric object first so the user can immediately type const pointer = e.scenePoint || canvas.getScenePoint(e.e); const textbox = new fabric.Textbox('', { left: pointer.x, top: pointer.y, width: 150 * vp.scale, fontFamily: 'Liberation Sans', fontSize: 14 * vp.scale * (vp.dpr || 1), // scaled loosely for screen display fontWeight: 'normal', fontStyle: 'normal', fill: '#000000', backgroundColor: undefined, originX: 'left', originY: 'top', transparentCorners: false, cornerColor: '#3b82f6', cornerStrokeColor: '#3b82f6', borderColor: '#3b82f6', cornerSize: 8, padding: 5, }); // Hide all corners and vertical resizers, leaving only middle-left and middle-right for width textbox.setControlsVisibility({ tl: false, tr: false, bl: false, br: false, mt: false, mb: false, mtr: false // hide rotation handle for now }); canvas.add(textbox); canvas.setActiveObject(textbox); textbox.enterEditing(); textbox.on('editing:exited', () => { if (!textbox.text || textbox.text.trim() === '') { canvas.remove(textbox); return; } const pt = screenToPdf({ x: textbox.left!, y: textbox.top! }, vp); const newAnn: TextAnnotation = { id: uuidv4(), page: pageNumber, type: 'text', rect: { x: pt.x, y: pt.y, width: textbox.width! / (vp.scale * (vp.dpr || 1)), height: textbox.height! / (vp.scale * (vp.dpr || 1)) }, rotation: 0, z: 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), props: { text: textbox.text, fontFamily: 'Liberation Sans', fontSize: 14, color: '#000000', align: 'left', bold: false, italic: false, lineHeight: 1.2, highlightColor: null } }; // We attach the ID to the fabric object so we can link it (textbox as any).id = newAnn.id; useEditorStore.getState().addAnnotation(newAnn); }); }, renderToFabric: (annotation: Annotation, canvas: fabric.Canvas, vp: ViewportParams) => { const textAnn = annotation as TextAnnotation; const pt = pdfRectToScreen(textAnn.rect, vp); const textbox = new fabric.Textbox(textAnn.props.text, { left: pt.x, top: pt.y, width: textAnn.rect.width * vp.scale * (vp.dpr || 1), fontFamily: textAnn.props.fontFamily, fontSize: textAnn.props.fontSize * vp.scale * (vp.dpr || 1), fontWeight: textAnn.props.bold ? 'bold' : 'normal', fontStyle: textAnn.props.italic ? 'italic' : 'normal', fill: textAnn.props.color, backgroundColor: textAnn.props.highlightColor || undefined, originX: 'left', originY: 'top', transparentCorners: false, cornerColor: '#3b82f6', cornerStrokeColor: '#3b82f6', borderColor: '#3b82f6', cornerSize: 8, padding: 5, }); textbox.setControlsVisibility({ tl: false, tr: false, bl: false, br: false, mt: false, mb: false, mtr: false }); (textbox as any).id = annotation.id; canvas.add(textbox); } };