239 lines
7.3 KiB
TypeScript
239 lines
7.3 KiB
TypeScript
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);
|
|
}
|
|
};
|