From a7696d7c805eccff10e201fccabdb3224284ffda Mon Sep 17 00:00:00 2001 From: luizzappa <65685842+luizzappa@users.noreply.github.com> Date: Sun, 8 Jan 2023 23:27:14 -0300 Subject: [PATCH] feat(Controls) Add handlers to modify polygon points (#8556) --- CHANGELOG.md | 1 + index.js | 2 + src/controls/polyControl.ts | 135 +++++++++++++++++++++++++++++++++++ src/shapes/polyline.class.ts | 2 +- 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 src/controls/polyControl.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 89da59d9f17..c8be8308a19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- feat(PolyControl): modify the shape of a poly with control points [#8556](https://github.com/fabricjs/fabric.js/pull/8556) - BREAKING: remove Object.stateful and Object.statefulCache [#8573](https://github.com/fabricjs/fabric.js/pull/8573) - fix(IText): refactor clearing context top logic of itext to align with brush pattern, using the canvas rendering cycle in order to guard from edge cases #8560 - fix(Canvas): `_initRetinaScaling` initializaing the scaling regardless of settings in Canvas. [#8565](https://github.com/fabricjs/fabric.js/pull/8565) diff --git a/index.js b/index.js index f496a21aacf..a05af7cf7aa 100644 --- a/index.js +++ b/index.js @@ -264,6 +264,7 @@ import { renderSquareControl, } from './src/controls/controls.render'; import { dragHandler } from './src/controls/drag'; +import { createPolyControls } from './src/controls/polyControl'; import { rotationStyleHandler, rotationWithSnapping, @@ -306,6 +307,7 @@ const controlsUtils = { skewHandlerX, skewHandlerY, dragHandler, + createPolyControls, scaleOrSkewActionName, rotationStyleHandler, wrapWithFixedAnchor, diff --git a/src/controls/polyControl.ts b/src/controls/polyControl.ts new file mode 100644 index 00000000000..c2d4df2b605 --- /dev/null +++ b/src/controls/polyControl.ts @@ -0,0 +1,135 @@ +import { Point } from '../point.class'; +import { Control } from './control.class'; +import { TMat2D } from '../typedefs'; +import { iMatrix } from '../constants'; +import type { Polyline } from '../shapes/polyline.class'; +import { multiplyTransformMatrices } from '../util/misc/matrix'; +import { + TPointerEvent, + Transform, + TransformActionHandler, +} from '../EventTypeDefs'; +import { getLocalPoint } from './util'; + +type TTransformAnchor = Transform & { pointIndex: number }; + +const getSize = (poly: Polyline) => { + return new Point(poly.width, poly.height); +}; + +/** + * This function locates the controls. + * It'll be used both for drawing and for interaction. + */ +const factoryPolyPositionHandler = (pointIndex: number) => { + return function (dim: Point, finalMatrix: TMat2D, polyObject: Polyline) { + const x = polyObject.points[pointIndex].x - polyObject.pathOffset.x, + y = polyObject.points[pointIndex].y - polyObject.pathOffset.y; + return new Point(x, y).transform( + multiplyTransformMatrices( + polyObject.canvas?.viewportTransform ?? iMatrix, + polyObject.calcTransformMatrix() + ) + ); + }; +}; + +/** + * This function defines what the control does. + * It'll be called on every mouse move after a control has been clicked and is being dragged. + * The function receives as argument the mouse event, the current transform object + * and the current position in canvas coordinate `transform.target` is a reference to the + * current object being transformed. + */ +const polyActionHandler = ( + eventData: TPointerEvent, + transform: TTransformAnchor, + x: number, + y: number +) => { + const poly = transform.target as Polyline, + pointIndex = transform.pointIndex, + mouseLocalPosition = getLocalPoint(transform, 'center', 'center', x, y), + polygonBaseSize = getSize(poly), + size = poly._getTransformedDimensions(), + sizeFactor = polygonBaseSize.divide(size), + adjustFlip = new Point(poly.flipX ? -1 : 1, poly.flipY ? -1 : 1); + + const finalPointPosition = mouseLocalPosition + .multiply(adjustFlip) + .multiply(sizeFactor) + .add(poly.pathOffset); + + poly.points[pointIndex] = finalPointPosition; + poly.setDimensions(); + + return true; +}; + +/** + * Keep the polygon in the same position when we change its `width`/`height`/`top`/`left`. + */ +const anchorWrapper = ( + pointIndex: number, + fn: TransformActionHandler +) => { + return function ( + eventData: TPointerEvent, + transform: Transform, + x: number, + y: number + ) { + const poly = transform.target as Polyline, + anchorIndex = (pointIndex > 0 ? pointIndex : poly.points.length) - 1, + pointInParentPlane = new Point( + poly.points[anchorIndex].x - poly.pathOffset.x, + poly.points[anchorIndex].y - poly.pathOffset.y + ).transform(poly.calcOwnMatrix()), + actionPerformed = fn(eventData, { ...transform, pointIndex }, x, y), + polygonBaseSize = getSize(poly), + adjustFlip = new Point(poly.flipX ? -1 : 1, poly.flipY ? -1 : 1); + + const newPosition = new Point( + poly.points[anchorIndex].x, + poly.points[anchorIndex].y + ) + .subtract(poly.pathOffset) + .divide(polygonBaseSize) + .multiply(adjustFlip); + + poly.setPositionByOrigin( + pointInParentPlane, + newPosition.x + 0.5, + newPosition.y + 0.5 + ); + return actionPerformed; + }; +}; + +export function createPolyControls( + poly: Polyline, + options?: Partial +): Record; +export function createPolyControls( + numOfControls: number, + options?: Partial +): Record; +export function createPolyControls( + arg0: number | Polyline, + options: Partial = {} +) { + const controls = {} as Record; + for ( + let idx = 0; + idx < (typeof arg0 === 'number' ? arg0 : arg0.points.length); + idx++ + ) { + controls[`p${idx}`] = new Control({ + actionName: 'modifyPoly', + positionHandler: factoryPolyPositionHandler(idx), + actionHandler: anchorWrapper(idx, polyActionHandler), + ...options, + }); + } + return controls; +} diff --git a/src/shapes/polyline.class.ts b/src/shapes/polyline.class.ts index 01bcc48f59d..c4a223e2dd2 100644 --- a/src/shapes/polyline.class.ts +++ b/src/shapes/polyline.class.ts @@ -149,7 +149,7 @@ export class Polyline extends FabricObject { * * @private */ - _getTransformedDimensions(options: any) { + _getTransformedDimensions(options?: any) { return this.exactBoundingBox ? super._getTransformedDimensions({ ...(options || {}),