Paperjet/frontend/src/features/editor/tools/TextTool.ts
Elijah 4cb038ec78
Some checks failed
Automated Container Build / build-and-push (push) Failing after 4s
CI / Backend (Python) (push) Failing after 17s
CI / Frontend (TypeScript) (push) Failing after 4m48s
Phase 3 implementation. Coordinate system implemented. Initial text box tool implemented.
2026-06-12 16:30:07 -07:00

141 lines
4.1 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, 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);
}
};