From 85404f5ad60aa720198f2a3759012cbb871197ae Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Thu, 20 Jun 2024 19:26:33 +0200 Subject: [PATCH 1/4] Retrieve image content from polygon --- src/components/Board/Board.tsx | 13 +++++++++++++ src/components/Board/types.tsx | 1 + src/fabric/utils.ts | 25 +++++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 4365377..93ab117 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -87,20 +87,31 @@ const Board = React.forwardRef( scaleRatio, }), ); + + const content = originalFabricImage?.toDataURL({ + withoutTransform: true, + ...fabricUtils.getBoundingBox(updatedCoords), + }); + return { id: co.name!, category: "TODO_category", color: "TODO_color", value: "TODO_value", coords: updatedCoords, + content, }; }); } return []; }, })); + const { editor, onReady } = useFabricJSEditor(); + const [originalFabricImage, setOriginalFabricImage] = + useState(); + const [currentZoom, setCurrentZoom] = useState( initialStatus?.currentZoom || 100, ); @@ -154,6 +165,8 @@ const Board = React.forwardRef( fabric.Image.fromURL( image.src, (img) => { + setOriginalFabricImage(img); + const { canvas } = editor; const scaleRatio = Math.min( (canvas.width ?? 1) / (img.width ?? 1), diff --git a/src/components/Board/types.tsx b/src/components/Board/types.tsx index 37f9b99..ef6558e 100644 --- a/src/components/Board/types.tsx +++ b/src/components/Board/types.tsx @@ -5,4 +5,5 @@ export type CanvasObject = { value: string; coords: { x: number; y: number }[]; opacity?: number; + content?: string; }; diff --git a/src/fabric/utils.ts b/src/fabric/utils.ts index 0b8d4f6..e9070a4 100644 --- a/src/fabric/utils.ts +++ b/src/fabric/utils.ts @@ -319,3 +319,28 @@ export const isCoordInsideCoords = ( return isInside; }; + +export const getBoundingBox = (points: { x: number; y: number }[]) => { + if (points.length === 0) { + return { left: 0, top: 0, width: 0, height: 0 }; + } + + let minX = points[0].x; + let maxX = points[0].x; + let minY = points[0].y; + let maxY = points[0].y; + + points.forEach((point) => { + if (point.x < minX) minX = point.x; + if (point.x > maxX) maxX = point.x; + if (point.y < minY) minY = point.y; + if (point.y > maxY) maxY = point.y; + }); + + return { + left: minX, + top: minY, + width: maxX - minX, + height: maxY - minY, + }; +}; From fef3211a2d5639d857b1c52ca722b224b379ffa3 Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Thu, 20 Jun 2024 21:44:38 +0200 Subject: [PATCH 2/4] Removed unused code --- src/components/Board/Board.tsx | 36 ---------------------------------- 1 file changed, 36 deletions(-) diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 93ab117..ddc7d7c 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -459,42 +459,6 @@ const Board = React.forwardRef( } }, [editor?.canvas, imageSize.width, imageSize.height, items, scaleRatio]); - // const renderIcon = ( - // ctx: CanvasRenderingContext2D, - // left: number, - // top: number, - // styleOverride: unknown, - // fabricObject: fabric.Object, - // ) => { - // const deleteIcon = - // "data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg version='1.1' id='Ebene_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='595.275px' height='595.275px' viewBox='200 215 230 470' xml:space='preserve'%3E%3Ccircle style='fill:%23F44336;' cx='299.76' cy='439.067' r='218.516'/%3E%3Cg%3E%3Crect x='267.162' y='307.978' transform='matrix(0.7071 -0.7071 0.7071 0.7071 -222.6202 340.6915)' style='fill:white;' width='65.545' height='262.18'/%3E%3Crect x='266.988' y='308.153' transform='matrix(0.7071 0.7071 -0.7071 0.7071 398.3889 -83.3116)' style='fill:white;' width='65.544' height='262.179'/%3E%3C/g%3E%3C/svg%3E"; - - // const img = document.createElement("img"); - // img.src = deleteIcon; - // const size = 24; - // ctx.save(); - // ctx.translate(left, top); - // if (fabricObject.angle) - // ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle)); - // ctx.drawImage(img, -size / 2, -size / 2, size, size); - // ctx.restore(); - // }; - - // rect.controls = { - // onDelete: new fabric.Control({ - // x: 0.5, - // y: -0.5, - // offsetY: 16, - // cursorStyle: "pointer", - // mouseUpHandler: () => onDelete(), - // render: renderIcon, - // }), - // }; - - // editor?.canvas.add(rect); - // // setAction({ primitive: "rectangle", operation: "add" }); - // }; - return (
Date: Fri, 21 Jun 2024 16:44:08 +0200 Subject: [PATCH 3/4] Added helper function --- src/components/Board/Board.tsx | 203 +++++++++++++++++++--- src/components/Board/__docs__/Example.tsx | 1 + src/fabric/utils.ts | 12 ++ 3 files changed, 195 insertions(+), 21 deletions(-) diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index ddc7d7c..14d51fa 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { fabric } from "fabric"; import { v4 as uuidv4 } from "uuid"; import { FabricJSCanvas, useFabricJSEditor } from "fabricjs-react"; @@ -16,6 +16,7 @@ export type BoardProps = { currentZoom?: number; scaleRatio?: number; }; + helper: (object: CanvasObject) => React.ReactNode; onResetZoom?: () => void; onZoomChange?: (currentZoom: number) => void; onLoadedImage?: ({ @@ -37,7 +38,15 @@ export type BoardActions = { const Board = React.forwardRef( ( - { image, initialStatus, items, onResetZoom, onZoomChange, onLoadedImage }, + { + image, + initialStatus, + items, + onResetZoom, + onZoomChange, + onLoadedImage, + helper, + }, ref, ) => { // Set board actions @@ -54,7 +63,7 @@ const Board = React.forwardRef( drawObject(type?: "rectangle" | "polygon") { const isDrawing = !drawingObject?.isDrawing; if (isDrawing) { - const polygonId = uuidv4(); + const polygonId = fabricUtils.toPolygonId(uuidv4()); setDrawingObject({ id: polygonId, type: type ?? "polygon", @@ -109,6 +118,21 @@ const Board = React.forwardRef( const { editor, onReady } = useFabricJSEditor(); + // Object with all available items + const boardItems = React.useMemo( + () => + items.reduce( + (acc, obj) => { + const id = fabricUtils.toPolygonId(obj.id); + obj.id = id; + acc[id] = obj; + return acc; + }, + {} as { [key: string]: CanvasObject }, + ), + [items], + ); + const [originalFabricImage, setOriginalFabricImage] = useState(); @@ -125,11 +149,18 @@ const Board = React.forwardRef( height: 0, }); + const [objectHelper, setObjectHelper] = useState<{ + left: number; + top: number; + enabled: boolean; + itemId?: string; + }>({ left: 0, top: 0, enabled: false }); + const [drawingObject, setDrawingObject] = useState< NonNullable >({ isDrawing: false, type: "polygon", points: [] }); - const resetDrawingObject = () => { + const resetDrawingObject = useCallback(() => { const state: fabricTypes.CanvasAnnotationState["drawingObject"] = { isDrawing: false, type: "polygon", @@ -139,7 +170,7 @@ const Board = React.forwardRef( editor?.canvas as unknown as fabricTypes.CanvasAnnotationState ).drawingObject = state; setDrawingObject(state); - }; + }, [editor?.canvas]); useEffect(() => { const parentCanvasElement = document.getElementById( @@ -203,6 +234,7 @@ const Board = React.forwardRef( zoom, ); setCurrentZoom(zoom * 100); + editor.canvas.discardActiveObject(); opt.e.preventDefault(); opt.e.stopPropagation(); }, @@ -389,24 +421,68 @@ const Board = React.forwardRef( }, ); - // Selected Objects - editor.canvas.on( - "selection:created", + // Function to reset the annotator helper + const objectEventFunction = function ( + this: fabricTypes.CanvasAnnotationState, // eslint-disable-next-line @typescript-eslint/no-unused-vars - function (this: fabricTypes.CanvasAnnotationState, _opt) { - // console.log("SELECTED! ", opt.selected?.[0]); + _opt: fabric.IEvent, + ) { + // While object is being moved, remove the annotator helper + setObjectHelper({ ...objectHelper, enabled: false }); + }; + + // While object is being transformed + editor.canvas.on("object:moving", objectEventFunction); + editor.canvas.on("object:scaling", objectEventFunction); + editor.canvas.on("object:resizing", objectEventFunction); + editor.canvas.on("object:rotating", objectEventFunction); + + // Objects Modified - When object ends being modified (moved, scaled, resized..), call function + editor.canvas.on( + "object:modified", + function (this: fabricTypes.CanvasAnnotationState, opt) { + const obj = opt.target; + if (obj) { + const helper = fabricUtils.getObjectHelperCoords(obj); + setObjectHelper({ + left: helper.left, + top: helper.top, + enabled: true, + itemId: obj.name, + }); + } }, ); - // Objects Moving + const onSelectionEvent = function ( + this: fabricTypes.CanvasAnnotationState, + opt: fabric.IEvent, + ) { + setObjectHelper({ ...objectHelper, enabled: false }); + const selected = opt.selected?.[0]; + const isDrawing = this.drawingObject?.isDrawing ?? false; + if (selected && !isDrawing) { + const helper = fabricUtils.getObjectHelperCoords(selected); + setObjectHelper({ + left: helper.left, + top: helper.top, + enabled: true, + itemId: selected.name, + }); + } + }; + + // Some element was selected + editor.canvas.on("selection:created", onSelectionEvent); + editor.canvas.on("selection:updated", onSelectionEvent); + + // On object selection cleared editor.canvas.on( - "object:modified", + "selection:cleared", // eslint-disable-next-line @typescript-eslint/no-unused-vars function (this: fabricTypes.CanvasAnnotationState, _opt) { - const obj = _opt.target as fabricTypes.CustomObject | undefined; - if (obj) { - console.log(`Object ["${obj.name}"] modified event`); - } + // const selectedObject = opt.deselected?.[0]; + setObjectHelper({ enabled: false, top: 0, left: 0 }); }, ); @@ -417,7 +493,15 @@ const Board = React.forwardRef( return () => { editor.canvas.off(); }; - }, [editor, image, onLoadedImage, onZoomChange, drawingObject]); + }, [ + editor, + image, + onLoadedImage, + onZoomChange, + drawingObject, + resetDrawingObject, + objectHelper, + ]); // Update zoom parent value useEffect(() => { @@ -432,7 +516,7 @@ const Board = React.forwardRef( // Clear all objects from canvas fabricActions.deleteAll(editor?.canvas); - for (const item of items) { + for (const [, item] of Object.entries(boardItems)) { const scaledCoords = item.coords.map((p) => fabricUtils.toScaledCoord({ cInfo: { width: canvas.getWidth(), height: canvas.getHeight() }, @@ -449,23 +533,100 @@ const Board = React.forwardRef( fabric.Polygon, scaledCoords, { - name: `ID_${item.id}`, + name: item.id, stroke: item.color, fill: `rgba(${parse(item.color).values.join(",")},${item.opacity ?? 0.4})`, }, scaledCoords.length === 4, // Is a rectangle ); + + // const renderIcon = ( + // ctx: CanvasRenderingContext2D, + // left: number, + // top: number, + // styleOverride: unknown, + // fabricObject: fabric.Object, + // ) => { + // const deleteIcon = + // "data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg version='1.1' id='Ebene_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='595.275px' height='595.275px' viewBox='200 215 230 470' xml:space='preserve'%3E%3Ccircle style='fill:%23F44336;' cx='299.76' cy='439.067' r='218.516'/%3E%3Cg%3E%3Crect x='267.162' y='307.978' transform='matrix(0.7071 -0.7071 0.7071 0.7071 -222.6202 340.6915)' style='fill:white;' width='65.545' height='262.18'/%3E%3Crect x='266.988' y='308.153' transform='matrix(0.7071 0.7071 -0.7071 0.7071 398.3889 -83.3116)' style='fill:white;' width='65.544' height='262.179'/%3E%3C/g%3E%3C/svg%3E"; + + // const img = document.createElement("img"); + // img.src = deleteIcon; + // const size = 24; + // ctx.save(); + // ctx.translate(left, top); + // if (fabricObject.angle) + // ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle)); + // ctx.drawImage(img, -size / 2, -size / 2, size, size); + // ctx.restore(); + // }; + + // polygon.controls = { + // ...polygon.controls, + // onDelete: new fabric.Control({ + // x: 0.5, + // y: -0.5, + // offsetY: 16, + // cursorStyle: "pointer", + // mouseUpHandler: () => { + // console.log("deleted up!"); + // return true; + // }, + // render: renderIcon, + // }), + // }; canvas.add(polygon); } - }, [editor?.canvas, imageSize.width, imageSize.height, items, scaleRatio]); + }, [ + editor?.canvas, + imageSize.width, + imageSize.height, + boardItems, + scaleRatio, + ]); + + const renderObjectHelper = () => { + if ( + !helper || + objectHelper.itemId === undefined || + objectHelper.enabled === false + ) { + return <>; + } + const canvasObject = boardItems[objectHelper.itemId]; + const left = `${objectHelper.left}px`; + const top = `${objectHelper.top}px`; + + if (canvasObject === undefined) return <>; + return ( +
+
+ {helper(canvasObject)} +
+
+ ); + }; return (
+ {renderObjectHelper()}
); }, diff --git a/src/components/Board/__docs__/Example.tsx b/src/components/Board/__docs__/Example.tsx index 744e85b..9c6856c 100644 --- a/src/components/Board/__docs__/Example.tsx +++ b/src/components/Board/__docs__/Example.tsx @@ -65,6 +65,7 @@ const Example: FC = ({ items, image }) => { ref={ref} image={image} items={items} + helper={(obj) =>
Hello: {obj.id}
} onZoomChange={(v) => setCurrentZoom(v)} />
diff --git a/src/fabric/utils.ts b/src/fabric/utils.ts index e9070a4..2c416c6 100644 --- a/src/fabric/utils.ts +++ b/src/fabric/utils.ts @@ -344,3 +344,15 @@ export const getBoundingBox = (points: { x: number; y: number }[]) => { height: maxY - minY, }; }; + +export const getObjectHelperCoords = (obj: fabric.Object) => { + const boundingRect = obj.getBoundingRect(); + return { + left: boundingRect.left + boundingRect.width, + top: boundingRect.top + boundingRect.height, + }; +}; + +export const toPolygonId = (id?: string) => { + return `RCA_POLYGON_${id}`; +}; From 73683e97c04f5c21cb8b435659d9c005b4c5f0cb Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Fri, 21 Jun 2024 17:27:07 +0200 Subject: [PATCH 4/4] Removed duplicated code --- src/components/Board/Board.tsx | 78 +++++++++++++++-------- src/components/Board/__docs__/Example.tsx | 26 +++++++- 2 files changed, 75 insertions(+), 29 deletions(-) diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 14d51fa..aded477 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -16,7 +16,7 @@ export type BoardProps = { currentZoom?: number; scaleRatio?: number; }; - helper: (object: CanvasObject) => React.ReactNode; + helper: (object: CanvasObject, content?: string) => React.ReactNode; onResetZoom?: () => void; onZoomChange?: (currentZoom: number) => void; onLoadedImage?: ({ @@ -31,6 +31,8 @@ export type BoardProps = { export type BoardActions = { resetZoom: () => void; deleteSelectedObjects: () => void; + deleteObjectById: (id: string) => void; + deselectAll: () => void; downloadImage: () => void; drawObject: (type?: "rectangle" | "polygon") => void; retrieveObjects: () => CanvasObject[]; @@ -56,10 +58,20 @@ const Board = React.forwardRef( setCurrentZoom(100); onResetZoom?.(); }, + deselectAll() { + editor?.canvas.discardActiveObject(); + }, deleteSelectedObjects() { const canvas = editor?.canvas; if (canvas) fabricActions.deleteSelected(canvas); }, + deleteObjectById(id: string) { + const canvas = editor?.canvas; + if (canvas) { + canvas.discardActiveObject(); + fabricActions.deleteObjectByName(canvas, id); + } + }, drawObject(type?: "rectangle" | "polygon") { const isDrawing = !drawingObject?.isDrawing; if (isDrawing) { @@ -84,31 +96,15 @@ const Board = React.forwardRef( fabricUtils.retrieveObjects(canvas); if (!customObjects) return []; return customObjects.map((co) => { - const updatedCoordPoints = fabricUtils.pointsInCanvas(co); - const updatedCoords = updatedCoordPoints.map((p) => - fabricUtils.toOriginalCoord({ - cInfo: { - width: canvas.getWidth(), - height: canvas.getHeight(), - }, - iInfo: imageSize, - coord: p, - scaleRatio, - }), - ); - - const content = originalFabricImage?.toDataURL({ - withoutTransform: true, - ...fabricUtils.getBoundingBox(updatedCoords), - }); + const info = getObjectInfo(co); return { id: co.name!, category: "TODO_category", color: "TODO_color", value: "TODO_value", - coords: updatedCoords, - content, + coords: info.coords, + content: info.content, }; }); } @@ -153,7 +149,7 @@ const Board = React.forwardRef( left: number; top: number; enabled: boolean; - itemId?: string; + object?: fabricTypes.CustomObject; }>({ left: 0, top: 0, enabled: false }); const [drawingObject, setDrawingObject] = useState< @@ -172,6 +168,31 @@ const Board = React.forwardRef( setDrawingObject(state); }, [editor?.canvas]); + const getObjectInfo = (obj: fabricTypes.CustomObject) => { + const width = editor?.canvas.getWidth() ?? 0; + const height = editor?.canvas.getHeight() ?? 0; + const updatedCoordPoints = fabricUtils.pointsInCanvas(obj); + const updatedCoords = updatedCoordPoints.map((p) => + fabricUtils.toOriginalCoord({ + cInfo: { + width, + height, + }, + iInfo: imageSize, + coord: p, + scaleRatio, + }), + ); + + return { + coords: updatedCoords, + content: originalFabricImage?.toDataURL({ + withoutTransform: true, + ...fabricUtils.getBoundingBox(updatedCoords), + }), + }; + }; + useEffect(() => { const parentCanvasElement = document.getElementById( "react-annotator-canvas", @@ -448,7 +469,7 @@ const Board = React.forwardRef( left: helper.left, top: helper.top, enabled: true, - itemId: obj.name, + object: obj as fabricTypes.CustomObject, }); } }, @@ -467,7 +488,7 @@ const Board = React.forwardRef( left: helper.left, top: helper.top, enabled: true, - itemId: selected.name, + object: selected as fabricTypes.CustomObject, }); } }; @@ -588,16 +609,17 @@ const Board = React.forwardRef( const renderObjectHelper = () => { if ( !helper || - objectHelper.itemId === undefined || + objectHelper.object?.name === undefined || objectHelper.enabled === false ) { return <>; } - const canvasObject = boardItems[objectHelper.itemId]; + const canvasObject = boardItems[objectHelper.object.name]; + if (canvasObject === undefined) return <>; + const left = `${objectHelper.left}px`; const top = `${objectHelper.top}px`; - - if (canvasObject === undefined) return <>; + const info = getObjectInfo(objectHelper.object); return (
( margin: "5px", }} > - {helper(canvasObject)} + {helper(canvasObject, info.content)}
); diff --git a/src/components/Board/__docs__/Example.tsx b/src/components/Board/__docs__/Example.tsx index 9c6856c..a6a7ee3 100644 --- a/src/components/Board/__docs__/Example.tsx +++ b/src/components/Board/__docs__/Example.tsx @@ -65,7 +65,31 @@ const Example: FC = ({ items, image }) => { ref={ref} image={image} items={items} - helper={(obj) =>
Hello: {obj.id}
} + helper={(obj, content) => { + const processContent = (c?: string) => { + const startLength = 20; + const endLength = 20; + // Check if the string is long enough to be shortened + if (!c || c.length <= startLength + endLength) { + return content; + } + const start = c.substring(0, startLength); + const end = c.substring(c.length - endLength); + + // Concatenate with ellipsis + return `${start}...${end}`; + }; + return ( +
+

Hello {obj.id}

+

{processContent(content)}

+ + +
+ ); + }} onZoomChange={(v) => setCurrentZoom(v)} />