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, SignatureAnnotation, SignatureDrawProps, SignatureTypeProps } from '../../../lib/annotations/types'; import { screenRectToPdf, pdfRectToScreen } from '../../../lib/coords'; import type { ViewportParams } from '../../../lib/coords'; let previewObj: fabric.FabricObject | null = null; let currentPreviewCanvas: fabric.Canvas | null = null; let isCreatingPreview = false; const createPreview = async (canvas: fabric.Canvas, vp: ViewportParams, props: any) => { isCreatingPreview = true; const storeState = useEditorStore.getState(); if (props.mode === 'draw') { const url = `/api/v1/documents/${storeState.documentId}/assets/${props.ref}`; try { const img = await fabric.Image.fromURL(url); const maxWidth = 200 * vp.scale; if (img.width! > maxWidth) { img.scaleToWidth(maxWidth); } img.set({ originX: 'center', originY: 'center', opacity: 0.5, evented: false }); previewObj = img; } catch (e) {} } else { previewObj = new fabric.Text(props.text, { fontFamily: props.fontFamily, fontSize: 48 * vp.scale * (vp.dpr || 1), fill: props.color, originX: 'center', originY: 'center', opacity: 0.5, evented: false }); } isCreatingPreview = false; if (previewObj && currentPreviewCanvas === canvas) { canvas.add(previewObj); canvas.requestRenderAll(); } }; export const SignatureTool: ToolHandler = { name: 'signature', onActivate: (canvas: fabric.Canvas) => { canvas.defaultCursor = 'none'; canvas.selection = false; }, onDeactivate: (canvas: fabric.Canvas) => { canvas.defaultCursor = 'default'; if (previewObj && currentPreviewCanvas === canvas) { currentPreviewCanvas.remove(previewObj); currentPreviewCanvas.requestRenderAll(); } previewObj = null; currentPreviewCanvas = null; isCreatingPreview = false; }, onPointerMove: (e: any, canvas: fabric.Canvas, vp: ViewportParams, _pageNumber: number) => { const props = useEditorStore.getState().pendingSignatureProps; if (!props) return; // Switch canvas if we moved to a new page if (currentPreviewCanvas !== canvas) { if (previewObj && currentPreviewCanvas) { currentPreviewCanvas.remove(previewObj); currentPreviewCanvas.requestRenderAll(); } currentPreviewCanvas = canvas; previewObj = null; } if (!previewObj && !isCreatingPreview) { createPreview(canvas, vp, props); } if (previewObj) { const pointer = e.scenePoint || canvas.getScenePoint(e.e); previewObj.set({ left: pointer.x, top: pointer.y }); canvas.requestRenderAll(); } }, onPointerDown: async (e: any, canvas: fabric.Canvas, vp: ViewportParams, pageNumber: number) => { if (e.target && e.target !== previewObj) return; const storeState = useEditorStore.getState(); const props = storeState.pendingSignatureProps; if (!props) return; if (previewObj && currentPreviewCanvas === canvas) { currentPreviewCanvas.remove(previewObj); previewObj = null; currentPreviewCanvas = null; } const pointer = e.scenePoint || canvas.getScenePoint(e.e); let fabricObj: fabric.FabricObject; if (props.mode === 'draw') { const drawProps = props as SignatureDrawProps; const url = `/api/v1/documents/${storeState.documentId}/assets/${drawProps.ref}`; try { const img = await fabric.Image.fromURL(url); // By default, scale it down to a reasonable size if it's too big const maxWidth = 200 * vp.scale; if (img.width! > maxWidth) { img.scaleToWidth(maxWidth); } img.set({ left: pointer.x, top: pointer.y, originX: 'center', originY: 'center', }); // We set id on the object so it can be identified (img as any).id = uuidv4(); fabricObj = img; } catch (err) { console.error("Failed to load signature image", err); return; } } else { const typeProps = props as SignatureTypeProps; const textObj = new fabric.Text(typeProps.text, { left: pointer.x, top: pointer.y, fontFamily: typeProps.fontFamily, fontSize: 48 * vp.scale * (vp.dpr || 1), fill: typeProps.color, originX: 'center', originY: 'center', }); (textObj as any).id = uuidv4(); fabricObj = textObj; } fabricObj.set({ transparentCorners: false, cornerColor: '#3b82f6', cornerStrokeColor: '#3b82f6', borderColor: '#3b82f6', cornerSize: 8, padding: 5, }); (fabricObj as any).annotationType = 'signature'; (fabricObj as any).annotationProps = props; canvas.add(fabricObj); canvas.setActiveObject(fabricObj); canvas.requestRenderAll(); // Convert to canonical space const bounds = fabricObj.getBoundingRect(); const pdfRect = screenRectToPdf({ x: bounds.left, y: bounds.top, width: bounds.width, height: bounds.height }, vp); const annotation: SignatureAnnotation = { id: (fabricObj as any).id, page: pageNumber, type: 'signature', rect: pdfRect, rotation: 0, z: 0, props: props, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; useEditorStore.getState().addAnnotation(annotation); // Switch back to select tool and clear pending useEditorStore.getState().setPendingSignatureProps(null); useEditorStore.getState().setActiveTool('select'); }, renderToFabric: async (annotation: Annotation, canvas: fabric.Canvas, vp: ViewportParams) => { if (annotation.type !== 'signature') return null; const sigAnn = annotation as SignatureAnnotation; const screenRect = pdfRectToScreen(sigAnn.rect, vp); let fabricObj: fabric.FabricObject; if (sigAnn.props.mode === 'draw') { const url = `/api/v1/documents/${useEditorStore.getState().documentId}/assets/${sigAnn.props.ref}`; try { const img = await fabric.Image.fromURL(url); img.set({ left: screenRect.x, top: screenRect.y, scaleX: screenRect.width / img.width!, scaleY: screenRect.height / img.height!, }); fabricObj = img; } catch (e) { console.error("Failed to load signature asset", e); return null; } } else { const typeProps = sigAnn.props as SignatureTypeProps; fabricObj = new fabric.Text(typeProps.text, { left: screenRect.x, top: screenRect.y, fontFamily: typeProps.fontFamily, fill: typeProps.color, }); // Set scale manually to match the saved rect fabricObj.set({ scaleX: screenRect.width / fabricObj.width!, scaleY: screenRect.height / fabricObj.height!, }); } fabricObj.set({ id: sigAnn.id, annotationType: 'signature', annotationProps: sigAnn.props, transparentCorners: false, cornerColor: '#3b82f6', cornerStrokeColor: '#3b82f6', borderColor: '#3b82f6', cornerSize: 8, padding: 5, } as any); canvas.add(fabricObj); } };