Phase 3 implementation. Coordinate system implemented. Initial text box tool implemented.
This commit is contained in:
parent
c159ad4f37
commit
4cb038ec78
34 changed files with 6676 additions and 130 deletions
141
frontend/src/features/editor/tools/TextTool.ts
Normal file
141
frontend/src/features/editor/tools/TextTool.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
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);
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue