From 0a1b94aaadf42c7e2df1ccd228669a169a9a70d4 Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Wed, 3 Apr 2024 16:55:46 +0100 Subject: [PATCH 01/10] Drawing polygon --- src/components/Board/Board.tsx | 115 +++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 43 deletions(-) diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 5214d5c..11a8ebe 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -30,6 +30,7 @@ export type BoardActions = { toggleDragging: (value?: boolean) => void; resetZoom: () => void; deleteSelectedObjects: () => void; + drawPolygon: () => void; }; type CanvasAnnotationState = { @@ -37,6 +38,9 @@ type CanvasAnnotationState = { lastPosX: number; lastPosY: number; isDragging?: boolean; + drawingPolygon?: boolean; + lastClickCoords?: { x: number; y: number }; + polygonPoints?: { x: number; y: number }[]; }; const StyledCanvas = styled.div` @@ -80,9 +84,14 @@ const Board = React.forwardRef( editor?.canvas.discardActiveObject(); } }, + drawPolygon() { + console.log("drawing"); + }, })); const { editor, onReady } = useFabricJSEditor(); + const [drawingPolygon, setDrawingPolygon] = useState(false); + const [currentZoom, setCurrentZoom] = useState( initialStatus?.currentZoom || 100, ); @@ -90,12 +99,12 @@ const Board = React.forwardRef( const [scaleRatio, setScaleRation] = useState( initialStatus?.scaleRatio || 100, ); - // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [imageSize, setImageSize] = useState({ width: 0, height: 0, }); - // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [draggingEnabled, setDraggingEnabled] = useState( initialStatus?.draggingEnabled || false, ); @@ -148,6 +157,7 @@ const Board = React.forwardRef( editor.canvas.on( "mouse:wheel", function (this: CanvasAnnotationState, opt) { + if (this.drawingPolygon) return; const delta = opt.e.deltaY; let zoom = editor.canvas.getZoom(); zoom *= 0.999 ** delta; @@ -171,7 +181,38 @@ const Board = React.forwardRef( this.selection = false; this.lastPosX = evt.clientX; this.lastPosY = evt.clientY; + this.drawingPolygon = drawingPolygon; + + // Extract coords for polygon drawing + const pointer = editor?.canvas.getPointer(opt.e, true); + const lastClickCoords = { x: pointer.x, y: pointer.y }; + this.lastClickCoords = lastClickCoords; + + // Add all polygon points to be later drawn + if (drawingPolygon) { + if (this.polygonPoints) { + this.polygonPoints?.push(this.lastClickCoords); + } else { + this.polygonPoints = [this.lastClickCoords]; + } + // Draw the polygon with the existing coords + // if (this.polygonPoints?.length ?? 0 >= 2) { + // const polygonId = "polygonId"; + // const previousPolygon = editor.canvas + // .getObjects() + // .find((o) => o.name === polygonId); + + // if (previousPolygon) editor.canvas.remove(previousPolygon); + // const newPolygon = new fabric.Polygon(this.polygonPoints ?? [], { + // name: polygonId, + // fill: undefined, + // stroke: "red", + // strokeWidth: 2, + // }); + // editor.canvas.add(newPolygon); + // } + } opt.e.preventDefault(); opt.e.stopPropagation(); }, @@ -190,6 +231,33 @@ const Board = React.forwardRef( this.lastPosX = e.clientX; this.lastPosY = e.clientY; } + } else if (this.drawingPolygon) { + const lineId = "lineId"; + const pointer = editor?.canvas.getPointer(opt.e, true); + + const { initialX, initialY } = { + initialX: this.lastClickCoords?.x ?? 0, + initialY: this.lastClickCoords?.y ?? 0, + }; + const previousLine = editor.canvas + .getObjects() + .find((o) => o.name === lineId); + + if (previousLine) editor.canvas.remove(previousLine); + + const line = new fabric.Line( + [initialX, initialY, pointer.x, pointer.y], + { + name: lineId, + stroke: "red", + strokeWidth: 2, + cornerColor: "blue", + cornerStyle: "circle", + selectable: false, + }, + ); + + editor.canvas.add(line); } opt.e.preventDefault(); @@ -240,6 +308,7 @@ const Board = React.forwardRef( imageSrc, onLoadedImage, onZoomChange, + drawingPolygon, ]); // Update zoom parent value @@ -264,6 +333,7 @@ const Board = React.forwardRef( for (const item of items) { const polygon = new fabric.Polygon(item.coords.map(toScaledCoord), { + name: `ID_${item.id}`, fill: undefined, stroke: "red", strokeWidth: 0.3, @@ -272,21 +342,6 @@ const Board = React.forwardRef( } }, [editor?.canvas, imageSize.height, imageSize.width, items, scaleRatio]); - // const onAddRectangle = () => { - // // editor?.addRectangle(); - // const rect = new fabric.Rect({ - // name: "MERDAS", - // left: 0, - // top: 0, - // originX: "left", - // originY: "top", - // width: 100, - // height: 100, - // fill: "rgba(255,127,39,1)", - // selectable: true, - // visible: true, - // }); - // const renderIcon = ( // ctx: CanvasRenderingContext2D, // left: number, @@ -308,11 +363,6 @@ const Board = React.forwardRef( // ctx.restore(); // }; - // const onDelete = () => { - // editor?.deleteSelected(); - // return true; - // }; - // rect.controls = { // onDelete: new fabric.Control({ // x: 0.5, @@ -336,27 +386,6 @@ const Board = React.forwardRef( - - //
- // - // - // - // - // - // - //
- // - //
Zoom: {Math.round(currentZoom)}%
- //
- //
); }, ); From 1022a2058f60a312869515856ec0bd201a70fdbe Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Thu, 4 Apr 2024 00:08:48 +0100 Subject: [PATCH 02/10] WIP --- src/components/Board/Board.tsx | 17 ++++++++++++++++- src/components/Board/__docs__/Example.tsx | 7 +++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 11a8ebe..22c46b0 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -31,6 +31,8 @@ export type BoardActions = { resetZoom: () => void; deleteSelectedObjects: () => void; drawPolygon: () => void; + randomAction1: () => void; + randomAction2: () => void; }; type CanvasAnnotationState = { @@ -85,7 +87,20 @@ const Board = React.forwardRef( } }, drawPolygon() { - console.log("drawing"); + setDrawingPolygon(!drawingPolygon); + }, + randomAction1() { + const line = new fabric.Line([0, 0, 40, 40], { + stroke: "reg", + strokeWidth: 2, + selectable: true, + hasBorders: true, + }); + + editor?.canvas.add(line); + }, + randomAction2() { + console.log("randomAction2"); }, })); const { editor, onReady } = useFabricJSEditor(); diff --git a/src/components/Board/__docs__/Example.tsx b/src/components/Board/__docs__/Example.tsx index deb5059..bed5102 100644 --- a/src/components/Board/__docs__/Example.tsx +++ b/src/components/Board/__docs__/Example.tsx @@ -29,6 +29,13 @@ const Example: FC = ({ primary = true, items, imageSrc }) => { + + + Current zoom: {currentZoom} From f9d80f0016b3f0de20c7cab7eea5b83d9b170192 Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Thu, 4 Apr 2024 12:12:03 +0100 Subject: [PATCH 03/10] Working with polyline on polygon drawing --- src/components/Board/Board.tsx | 47 +++++++++++++++------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 22c46b0..2d75bfc 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -199,7 +199,7 @@ const Board = React.forwardRef( this.drawingPolygon = drawingPolygon; // Extract coords for polygon drawing - const pointer = editor?.canvas.getPointer(opt.e, true); + const pointer = editor?.canvas.getPointer(opt.e); const lastClickCoords = { x: pointer.x, y: pointer.y }; this.lastClickCoords = lastClickCoords; @@ -247,32 +247,27 @@ const Board = React.forwardRef( this.lastPosY = e.clientY; } } else if (this.drawingPolygon) { - const lineId = "lineId"; - const pointer = editor?.canvas.getPointer(opt.e, true); - - const { initialX, initialY } = { - initialX: this.lastClickCoords?.x ?? 0, - initialY: this.lastClickCoords?.y ?? 0, - }; - const previousLine = editor.canvas + const pointer = editor?.canvas.getPointer(opt.e); + + const polygonId = "polygonId"; + const previousPolygon = editor.canvas .getObjects() - .find((o) => o.name === lineId); - - if (previousLine) editor.canvas.remove(previousLine); - - const line = new fabric.Line( - [initialX, initialY, pointer.x, pointer.y], - { - name: lineId, - stroke: "red", - strokeWidth: 2, - cornerColor: "blue", - cornerStyle: "circle", - selectable: false, - }, - ); - - editor.canvas.add(line); + .find((o) => o.name === polygonId); + + if (previousPolygon) editor.canvas.remove(previousPolygon); + + const polygonPoints = + this.polygonPoints?.concat({ x: pointer.x, y: pointer.y }) ?? []; + + const newPolygon = new fabric.Polyline(polygonPoints, { + name: polygonId, + fill: "rgba(255,0,0,0.4)", + stroke: "red", + strokeWidth: 2, + hasBorders: false, + hasControls: false, + }); + editor.canvas.add(newPolygon); } opt.e.preventDefault(); From e3ba9c1c359ecadf0f6940bbe0313af0ed14c524 Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Fri, 5 Apr 2024 10:41:02 +0100 Subject: [PATCH 04/10] lint settings.json file --- .vscode/settings.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 7b4fd02..7a79f2c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,12 @@ "source.fixAll.eslint": "explicit" }, "editor.defaultFormatter": "esbenp.prettier-vscode", - "eslint.validate": ["javascript", "javascriptreact", "typescript"] + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript" + ], + "[sql]": { + "editor.defaultFormatter": "adpyke.vscode-sql-formatter" + } } \ No newline at end of file From 4dd44f963888e462bf0d28f7a6eee3ad7db417b2 Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Fri, 5 Apr 2024 11:55:49 +0100 Subject: [PATCH 05/10] WIP --- src/components/Board/Board.tsx | 10 +++++----- src/fabricUtils.ts | 12 ++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 src/fabricUtils.ts diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 3d43829..e13de2d 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -78,7 +78,6 @@ const Board = React.forwardRef( onResetZoom?.(); }, deleteSelectedObjects() { - editor?.deleteSelected(); const activeObjects = editor?.canvas.getActiveObjects(); if (activeObjects) { activeObjects.forEach((activeObject) => { @@ -279,6 +278,11 @@ const Board = React.forwardRef( this.polygonPoints = [this.lastClickCoords]; } + if (this.polygonPoints?.length === 4) { + setDrawingPolygon(false); + return; + } + // Draw the polygon with the existing coords // if (this.polygonPoints?.length ?? 0 >= 2) { // const polygonId = "polygonId"; @@ -461,10 +465,6 @@ const Board = React.forwardRef( // // setAction({ primitive: "rectangle", operation: "add" }); // }; - // const getVisible = () => { - // console.log(editor?.canvas.getObjects()?.[0].visible); - // }; - return ( diff --git a/src/fabricUtils.ts b/src/fabricUtils.ts new file mode 100644 index 0000000..00d86a6 --- /dev/null +++ b/src/fabricUtils.ts @@ -0,0 +1,12 @@ +import { fabric } from "fabric"; + +export const toPolygon = (object: fabric.Polyline) => { + return new fabric.Polygon(object.points!, { + name: object.name, + fill: object.fill, + stroke: object.stroke, + strokeWidth: object.strokeWidth, + hasBorders: object.hasBorders, + hasControls: object.hasControls, + }); +}; From fd19e650c940ff8c8bec7aed0826cfa7cd2e553f Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Fri, 5 Apr 2024 12:37:13 +0100 Subject: [PATCH 06/10] Small refactor to fabricUtils file --- src/components/Board/Board.tsx | 85 +++------------------------ src/fabricUtils.ts | 102 +++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 76 deletions(-) diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index e02549b..7695d55 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react"; import { fabric } from "fabric"; import { FabricJSCanvas, useFabricJSEditor } from "fabricjs-react"; import { CanvasObject } from "./types"; +import * as fabricUtils from "../../fabricUtils"; export type BoardProps = { primary?: boolean; @@ -71,13 +72,8 @@ const Board = React.forwardRef( onResetZoom?.(); }, deleteSelectedObjects() { - const activeObjects = editor?.canvas.getActiveObjects(); - if (activeObjects) { - activeObjects.forEach((activeObject) => { - editor?.canvas.remove(activeObject); - }); - editor?.canvas.discardActiveObject(); - } + const canvas = editor?.canvas; + if (canvas) fabricUtils.deleteSelected(canvas); }, drawPolygon() { setDrawingPolygon(!drawingPolygon); @@ -96,71 +92,7 @@ const Board = React.forwardRef( console.log("randomAction2"); }, downloadImage() { - // Create a temporary canvas to compose original image and annotations - const tempCanvas = document.createElement("canvas"); - const tempCtx = tempCanvas.getContext("2d")!; - - // Get the original image data from the canvas - const originalImageSrc = image.src; // Provide the path to your original image - const originalImage = new Image(); - originalImage.src = originalImageSrc; - - // Wait for the original image to load before composing - originalImage.onload = function () { - // Set the size of the temporary canvas to match the original image - tempCanvas.width = originalImage.width; - tempCanvas.height = originalImage.height; - - // Draw the original image onto the temporary canvas - tempCtx.drawImage(originalImage, 0, 0); - - // Get the Fabric.js canvas instance - // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - // const canvas = editor?.canvas!; - // const fabricCanvas = canvas.getObjects(); - // console.log(fabricCanvas[0]); - - // items.forEach((item) => { - // const polygon = new fabric.Polygon(item.coords, { - // name: `ID_${item.id}`, - // fill: undefined, - // stroke: "red", - // strokeWidth: 1, - // }); - // // tempCtx.save(); - // polygon.render(tempCtx); - // // tempCtx.restore(); - // }); - - // Loop through all objects on the Fabric.js canvas and draw them onto the temporary canvas - // fabricCanvas.forEach((obj) => { - // const scaleFactorX = tempCanvas.width / canvas.width!; - // const scaleFactorY = tempCanvas.height / canvas.height!; - - // console.log({ scaleFactorX, scaleFactorY }); - - // // Adjust top and left positions based on the scale - // const left = obj.left! * scaleFactorX; - // const top = obj.top! * scaleFactorY; - - // tempCtx.save(); - // tempCtx.translate(0, 0); - // tempCtx.scale(scaleFactorX, scaleFactorY); - // obj.render(tempCtx); - // tempCtx.restore(); - // }); - - // Convert the composed image on the temporary canvas to a data URL - const composedDataURL = tempCanvas.toDataURL("image/png"); - - // Create a temporary anchor element - const link = document.createElement("a"); - link.href = composedDataURL; - link.download = image.name; // Set the desired filename - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }; + fabricUtils.canvasImageDownload(image); }, })); const { editor, onReady } = useFabricJSEditor(); @@ -404,10 +336,11 @@ const Board = React.forwardRef( return { x, y }; }; + const canvas = editor?.canvas; + if (!canvas) return; + // Clear all objects from canvas - editor?.canvas?.getObjects().forEach((o) => editor?.canvas?.remove(o)); - editor?.canvas?.discardActiveObject(); - editor?.canvas?.renderAll(); + fabricUtils.deleteAll(editor?.canvas); for (const item of items) { const polygon = new fabric.Polygon(item.coords.map(toScaledCoord), { @@ -416,7 +349,7 @@ const Board = React.forwardRef( stroke: "red", strokeWidth: 0.3, }); - editor?.canvas.add(polygon); + canvas.add(polygon); } }, [editor?.canvas, imageSize.height, imageSize.width, items, scaleRatio]); diff --git a/src/fabricUtils.ts b/src/fabricUtils.ts index 00d86a6..3d48333 100644 --- a/src/fabricUtils.ts +++ b/src/fabricUtils.ts @@ -10,3 +10,105 @@ export const toPolygon = (object: fabric.Polyline) => { hasControls: object.hasControls, }); }; + +/** + * + * Deletes all objects from the canvas + * + * @param canvas fabricjs HTML canvas + */ +export const deleteAll = (canvas: fabric.Canvas) => { + canvas?.getObjects().forEach((o) => canvas?.remove(o)); + canvas?.discardActiveObject(); + canvas?.renderAll(); +}; + +/** + * + * Deletes all selected objects from the canvas + * + * @param canvas fabricjs HTML canvas + */ +export const deleteSelected = (canvas: fabric.Canvas) => { + const activeObjects = canvas.getActiveObjects(); + if (activeObjects) { + activeObjects.forEach((activeObject) => { + canvas.remove(activeObject); + }); + canvas.discardActiveObject(); + } +}; + +/** + * + * Applies an input image to a canvas and downloads the image from it. (TODO: later on it will be downloaded with the annotations if specified) + * + * @param image properties and resources + */ +export const canvasImageDownload = (image: { name: string; src: string }) => { + // Create a temporary canvas to compose original image and annotations + const tempCanvas = document.createElement("canvas"); + const tempCtx = tempCanvas.getContext("2d")!; + + // Get the original image data from the canvas + const originalImageSrc = image.src; // Provide the path to your original image + const originalImage = new Image(); + originalImage.src = originalImageSrc; + + // Wait for the original image to load before composing + originalImage.onload = function () { + // Set the size of the temporary canvas to match the original image + tempCanvas.width = originalImage.width; + tempCanvas.height = originalImage.height; + + // Draw the original image onto the temporary canvas + tempCtx.drawImage(originalImage, 0, 0); + + // Get the Fabric.js canvas instance + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + // const canvas = editor?.canvas!; + // const fabricCanvas = canvas.getObjects(); + // console.log(fabricCanvas[0]); + + // items.forEach((item) => { + // const polygon = new fabric.Polygon(item.coords, { + // name: `ID_${item.id}`, + // fill: undefined, + // stroke: "red", + // strokeWidth: 1, + // }); + // // tempCtx.save(); + // polygon.render(tempCtx); + // // tempCtx.restore(); + // }); + + // Loop through all objects on the Fabric.js canvas and draw them onto the temporary canvas + // fabricCanvas.forEach((obj) => { + // const scaleFactorX = tempCanvas.width / canvas.width!; + // const scaleFactorY = tempCanvas.height / canvas.height!; + + // console.log({ scaleFactorX, scaleFactorY }); + + // // Adjust top and left positions based on the scale + // const left = obj.left! * scaleFactorX; + // const top = obj.top! * scaleFactorY; + + // tempCtx.save(); + // tempCtx.translate(0, 0); + // tempCtx.scale(scaleFactorX, scaleFactorY); + // obj.render(tempCtx); + // tempCtx.restore(); + // }); + + // Convert the composed image on the temporary canvas to a data URL + const composedDataURL = tempCanvas.toDataURL("image/png"); + + // Create a temporary anchor element + const link = document.createElement("a"); + link.href = composedDataURL; + link.download = image.name; // Set the desired filename + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; +}; From eac8eccab3087e5345592f426836dffb2d02c402 Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Fri, 5 Apr 2024 16:51:24 +0100 Subject: [PATCH 07/10] Small refactor: moved fabric logic to specific file --- example/src/App.tsx | 2 +- src/components/Board/Board.tsx | 20 ++++----- src/fabricUtils.ts | 78 ++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 11 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index a16ef86..5707507 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -5,7 +5,7 @@ import "./App.css"; function App() { return ( <> - + ); } diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 7695d55..700f373 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -245,22 +245,22 @@ const Board = React.forwardRef( const pointer = editor?.canvas.getPointer(opt.e); const polygonId = "polygonId"; - const previousPolygon = editor.canvas - .getObjects() - .find((o) => o.name === polygonId); + const previousPolygon = fabricUtils.findObjectByName( + editor.canvas, + polygonId, + ); - if (previousPolygon) editor.canvas.remove(previousPolygon); + if (previousPolygon) + fabricUtils.deleteObject(editor.canvas, previousPolygon); + // Polygon "clicked" points with the cursor current pointer const polygonPoints = this.polygonPoints?.concat({ x: pointer.x, y: pointer.y }) ?? []; - const newPolygon = new fabric.Polyline(polygonPoints, { + const newPolygon = fabricUtils.createPolygon({ name: polygonId, - fill: "rgba(255,0,0,0.4)", - stroke: "red", - strokeWidth: 2, - hasBorders: false, - hasControls: false, + points: polygonPoints, + isPolyline: true, }); editor.canvas.add(newPolygon); } diff --git a/src/fabricUtils.ts b/src/fabricUtils.ts index 3d48333..0588248 100644 --- a/src/fabricUtils.ts +++ b/src/fabricUtils.ts @@ -112,3 +112,81 @@ export const canvasImageDownload = (image: { name: string; src: string }) => { document.body.removeChild(link); }; }; + +/** + * + * Finds an object in a canvas according to its name + * + * @param canvas html canvas to look for the object + * @param name object identifier + * @returns + */ +export const findObjectByName = (canvas: fabric.Canvas, name: string) => { + return canvas.getObjects().find((o) => o.name === name); +}; + +/** + * + * Deletes an object by its name + * + * @param canvas html canvas to look for the object + * @param name object identifier + */ +export const deleteObjectByName = (canvas: fabric.Canvas, name: string) => { + const obj = findObjectByName(canvas, name); + if (obj) canvas.remove(obj); +}; + +/** + * + * Deletes an object + * + * @param canvas html canvas to look for the object + * @param obj object to delete + */ +export const deleteObject = (canvas: fabric.Canvas, obj: fabric.Object) => { + if (obj) canvas.remove(obj); +}; + +/** + * + * @param name name of the object + * @param points array of points to create the polygon + * @param options options of the polygon (fill, stroke, controls..) + * @param isPolyline if it should create a the polygon as a polyline + * @returns + */ +export const createPolygon = ({ + name, + points, + options = { + fill: "rgba(255,0,0,0.4)", + stroke: "red", + strokeWidth: 2, + hasBorders: false, + hasControls: false, + }, + isPolyline = false, +}: { + name: string; + points: { x: number; y: number }[]; + options?: { + fill?: string; + stroke?: string; + strokeWidth?: number; + hasBorders?: boolean; + hasControls?: boolean; + }; + isPolyline?: boolean; +}) => { + if (isPolyline) { + return new fabric.Polyline(points, { + name, + ...options, + }); + } + return new fabric.Polygon(points, { + name, + ...options, + }); +}; From 252b7356728073337d831d9916062ff7b0a10735 Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Tue, 9 Apr 2024 01:03:29 +0100 Subject: [PATCH 08/10] Working create line polygon (still without much interaction --- src/components/Board/Board.tsx | 42 +++++++++-- src/fabricTypes.ts | 25 +++++++ src/fabricUtils.ts | 131 +++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 7 deletions(-) create mode 100644 src/fabricTypes.ts diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 700f373..55f40bb 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -79,13 +79,41 @@ const Board = React.forwardRef( setDrawingPolygon(!drawingPolygon); }, randomAction1() { - const line = new fabric.Line([0, 0, 40, 40], { - stroke: "reg", - strokeWidth: 2, - selectable: true, - hasBorders: true, - }); - + const line = new fabric.Polygon( + [ + { x: 40, y: 40 }, + { x: 120, y: 120 }, + ], + { + stroke: "red", + strokeWidth: 2, + selectable: true, + hasBorders: false, + hasControls: true, + cornerStyle: "rect", + cornerColor: "rgba(113, 113, 117, 0.5)", + objectCaching: false, + }, + ); + const controls = line.points?.reduce<{ + [key: string]: fabric.Control; + }>((acc, point, index) => { + acc["p" + index] = new fabricUtils.CustomControl( + { + positionHandler: fabricUtils.polygonPositionHandler, + actionHandler: fabricUtils.anchorWrapper( + index > 0 ? index - 1 : line.points!.length - 1, + fabricUtils.actionHandler, + ), + actionName: "modifyPolygon", + }, + index, + ); + return acc; + }, {}); + if (controls) { + line.controls = controls; + } editor?.canvas.add(line); }, randomAction2() { diff --git a/src/fabricTypes.ts b/src/fabricTypes.ts new file mode 100644 index 0000000..9d5cd76 --- /dev/null +++ b/src/fabricTypes.ts @@ -0,0 +1,25 @@ +// Types from FabricJS + +import { Transform } from "fabric/fabric-impl"; + +export type TMat2D = [ + a: number, + b: number, + c: number, + d: number, + e: number, + f: number, +]; + +export type CustomTransform = Transform & { + target: fabric.Object & { + __corner?: string; + pathOffset?: { x: number; y: number }; + points?: fabric.Point[]; + setPositionByOrigin?: ( + pos: fabric.Point, + originX: number, + originY: number, + ) => void; + }; +}; diff --git a/src/fabricUtils.ts b/src/fabricUtils.ts index 0588248..9ba0658 100644 --- a/src/fabricUtils.ts +++ b/src/fabricUtils.ts @@ -1,4 +1,5 @@ import { fabric } from "fabric"; +import * as fabricTypes from "./fabricTypes"; export const toPolygon = (object: fabric.Polyline) => { return new fabric.Polygon(object.points!, { @@ -190,3 +191,133 @@ export const createPolygon = ({ ...options, }); }; + +/** + * Custom FabricJS Control class with extra pointIndex + */ +export class CustomControl extends fabric.Control { + // Add an extra field pointIndex + pointIndex: number; + + // Override the constructor to include the new field + constructor(options: Partial, pointIndex: number) { + // Call the constructor of the base class + super(options); + + // Initialize the new field + this.pointIndex = pointIndex; + } +} + +/** + * Define a function that can locate the controls. + * This function will be used both for drawing and for interaction. + * More info: http://fabricjs.com/custom-controls-polygon + */ +export const polygonPositionHandler = function ( + this: CustomControl, + dim: { x: number; y: number }, + finalMatrix: fabricTypes.TMat2D, + fabricObject: fabric.Polyline, +) { + const x = + fabricObject!.points![this!.pointIndex!].x - fabricObject.pathOffset.x; + const y = + fabricObject!.points![this!.pointIndex!].y - fabricObject.pathOffset.y; + return fabric.util.transformPoint( + new fabric.Point(x, y), + fabric.util.multiplyTransformMatrices( + fabricObject!.canvas!.viewportTransform!, + fabricObject.calcTransformMatrix(), + ), + ); +}; + +/** + * More info: http://fabricjs.com/custom-controls-polygon + */ +export const getObjectSizeWithStroke = (object: fabric.Object) => { + const stroke = new fabric.Point( + object.strokeUniform ? 1 / object.scaleX! : 1, + object.strokeUniform ? 1 / object.scaleY! : 1, + ).multiply(object.strokeWidth!); + return new fabric.Point(object.width! + stroke.x, object.height! + stroke.y); +}; + +/** + * Define a function that will define what the control does + * This function will be called on every mouse move after a control has been clicked and is being dragged. + * The function receive as argument the mouse event, the current trasnform object and the current position in canvas coordinate + * transform.target is a reference to the current object being transformed, + * + * More info: http://fabricjs.com/custom-controls-polygon + */ +export const actionHandler = ( + eventData: MouseEvent, + transform: fabricTypes.CustomTransform, + x: number, + y: number, +) => { + const polygon = transform.target; + const currentControl = polygon.controls[polygon.__corner!] as CustomControl; + const mouseLocalPosition = polygon.toLocalPoint( + new fabric.Point(x, y), + "center", + "center", + ); + const polygonBaseSize = getObjectSizeWithStroke(polygon); + const size = polygon._getTransformedDimensions(0, 0); + const finalPointPosition = { + x: + (mouseLocalPosition.x * polygonBaseSize.x) / size.x + + polygon.pathOffset!.x, + y: + (mouseLocalPosition.y * polygonBaseSize.y) / size.y + + polygon.pathOffset!.y, + }; + polygon.points![currentControl.pointIndex] = new fabric.Point( + finalPointPosition.x, + finalPointPosition.y, + ); + return true; +}; + +/** + * Define a function that can keep the polygon in the same position when we change its width/height/top/left. + * + * More info: http://fabricjs.com/custom-controls-polygon + */ +export const anchorWrapper = ( + anchorIndex: number, + fn: ( + eventData: MouseEvent, + transform: fabricTypes.CustomTransform, + x: number, + y: number, + ) => boolean, +) => { + return function ( + eventData: MouseEvent, + transform: fabricTypes.CustomTransform, + x: number, + y: number, + ) { + const fabricObject = transform.target; + const points = fabricObject.points!; + const pathOffset = fabricObject.pathOffset!; + const point = new fabric.Point( + points[anchorIndex].x - pathOffset.x, + points[anchorIndex].y - pathOffset.y, + ); + const absolutePoint = fabric.util.transformPoint( + point, + fabricObject.calcTransformMatrix(), + ); + const actionPerformed = fn(eventData, transform, x, y); + const polygonBaseSize = getObjectSizeWithStroke(fabricObject); + const newX = (points[anchorIndex].x - pathOffset.x) / polygonBaseSize.x; + const newY = (points[anchorIndex].y - pathOffset.y) / polygonBaseSize.y; + fabricObject.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5); + return actionPerformed; + }; +}; From e75c8fa0639babc83b4a75fcfd05dbcc1b4d2300 Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Tue, 9 Apr 2024 10:22:17 +0100 Subject: [PATCH 09/10] Prefixed unused variables with underscore --- src/components/Board/Board.tsx | 2 +- src/fabricUtils.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 55f40bb..319e54c 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -97,7 +97,7 @@ const Board = React.forwardRef( ); const controls = line.points?.reduce<{ [key: string]: fabric.Control; - }>((acc, point, index) => { + }>((acc, _point, index) => { acc["p" + index] = new fabricUtils.CustomControl( { positionHandler: fabricUtils.polygonPositionHandler, diff --git a/src/fabricUtils.ts b/src/fabricUtils.ts index 9ba0658..c372aeb 100644 --- a/src/fabricUtils.ts +++ b/src/fabricUtils.ts @@ -216,8 +216,8 @@ export class CustomControl extends fabric.Control { */ export const polygonPositionHandler = function ( this: CustomControl, - dim: { x: number; y: number }, - finalMatrix: fabricTypes.TMat2D, + _dim: { x: number; y: number }, + _finalMatrix: fabricTypes.TMat2D, fabricObject: fabric.Polyline, ) { const x = @@ -253,7 +253,7 @@ export const getObjectSizeWithStroke = (object: fabric.Object) => { * More info: http://fabricjs.com/custom-controls-polygon */ export const actionHandler = ( - eventData: MouseEvent, + _eventData: MouseEvent, transform: fabricTypes.CustomTransform, x: number, y: number, From 84e6daf62c4f29ee7d153ccbbef0c2abbfd1a276 Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Tue, 9 Apr 2024 11:38:54 +0100 Subject: [PATCH 10/10] Fixed bug with action handler bounding box dimensions --- src/components/Board/Board.tsx | 7 ++++--- src/fabricTypes.ts | 1 + src/fabricUtils.ts | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 319e54c..2336477 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -86,9 +86,10 @@ const Board = React.forwardRef( ], { stroke: "red", - strokeWidth: 2, + fill: undefined, + strokeWidth: 1, selectable: true, - hasBorders: false, + hasBorders: true, hasControls: true, cornerStyle: "rect", cornerColor: "rgba(113, 113, 117, 0.5)", @@ -375,7 +376,7 @@ const Board = React.forwardRef( name: `ID_${item.id}`, fill: undefined, stroke: "red", - strokeWidth: 0.3, + strokeWidth: 1, // TODO: Change here! }); canvas.add(polygon); } diff --git a/src/fabricTypes.ts b/src/fabricTypes.ts index 9d5cd76..f931e8e 100644 --- a/src/fabricTypes.ts +++ b/src/fabricTypes.ts @@ -21,5 +21,6 @@ export type CustomTransform = Transform & { originX: number, originY: number, ) => void; + _setPositionDimensions?: (o: unknown) => unknown; // TODO: Understand why this needs to be here. Migrate to 'setDimensions'. }; }; diff --git a/src/fabricUtils.ts b/src/fabricUtils.ts index c372aeb..9c93685 100644 --- a/src/fabricUtils.ts +++ b/src/fabricUtils.ts @@ -314,6 +314,7 @@ export const anchorWrapper = ( fabricObject.calcTransformMatrix(), ); const actionPerformed = fn(eventData, transform, x, y); + fabricObject._setPositionDimensions?.({}); // TODO: Understand why this needs to be here. Migrate to 'setDimensions'. const polygonBaseSize = getObjectSizeWithStroke(fabricObject); const newX = (points[anchorIndex].x - pathOffset.x) / polygonBaseSize.x; const newY = (points[anchorIndex].y - pathOffset.y) / polygonBaseSize.y;