diff --git a/CHANGELOG.md b/CHANGELOG.md index f5fc760c15b..3e50e6c627f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- patch(Control): move hit detection to shouldActivate [#9374](https://github.com/fabricjs/fabric.js/pull/9374) - fix(StaticCanvas): disposing animations [#9361](https://github.com/fabricjs/fabric.js/pull/9361) - fix(IText): cursor width under group [#9341](https://github.com/fabricjs/fabric.js/pull/9341) - TS(Canvas): constructor optional el [#9348](https://github.com/fabricjs/fabric.js/pull/9348) diff --git a/src/controls/Control.ts b/src/controls/Control.ts index 702665d7c1f..e9e2d24024c 100644 --- a/src/controls/Control.ts +++ b/src/controls/Control.ts @@ -7,7 +7,8 @@ import type { } from '../EventTypeDefs'; import { Point } from '../Point'; import type { InteractiveFabricObject } from '../shapes/Object/InteractiveObject'; -import type { TDegree, TMat2D } from '../typedefs'; +import type { TCornerPoint, TDegree, TMat2D } from '../typedefs'; +import { cornerPointContainsPoint } from '../util/intersection/findCrossPoint'; import { cos } from '../util/misc/cos'; import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; import { sin } from '../util/misc/sin'; @@ -169,11 +170,17 @@ export class Control { */ declare mouseUpHandler?: ControlActionHandler; - shouldActivate(controlKey: string, fabricObject: InteractiveFabricObject) { + shouldActivate( + controlKey: string, + fabricObject: InteractiveFabricObject, + pointer: Point, + cornerPoint: TCornerPoint + ) { // TODO: locking logic can be handled here instead of in the control handler logic return ( fabricObject.canvas?.getActiveObject() === fabricObject && - fabricObject.isControlVisible(controlKey) + fabricObject.isControlVisible(controlKey) && + cornerPointContainsPoint(pointer, cornerPoint) ); } diff --git a/src/shapes/Object/InteractiveObject.ts b/src/shapes/Object/InteractiveObject.ts index ac05ef8c198..d027559cd9c 100644 --- a/src/shapes/Object/InteractiveObject.ts +++ b/src/shapes/Object/InteractiveObject.ts @@ -198,18 +198,19 @@ export class InteractiveFabricObject< const cornerEntries = Object.entries(this.oCoords); for (let i = cornerEntries.length - 1; i >= 0; i--) { const [key, corner] = cornerEntries[i]; - if (this.controls[key].shouldActivate(key, this)) { - const lines = this._getImageLines( + if ( + this.controls[key].shouldActivate( + key, + this, + pointer, forTouch ? corner.touchCorner : corner.corner - ); - const xPoints = this._findCrossPoints(pointer, lines); - if (xPoints !== 0 && xPoints % 2 === 1) { - this.__corner = key; - return key; - } + ) + ) { + // this.canvas.contextTop.fillRect(pointer.x - 1, pointer.y - 1, 2, 2); + return (this.__corner = key); } - // // debugging + // // debugging needs rework // // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); diff --git a/src/shapes/Object/ObjectGeometry.ts b/src/shapes/Object/ObjectGeometry.ts index d9e76e9e2bc..7ad7fcdb076 100644 --- a/src/shapes/Object/ObjectGeometry.ts +++ b/src/shapes/Object/ObjectGeometry.ts @@ -27,18 +27,11 @@ import type { StaticCanvas } from '../../canvas/StaticCanvas'; import { ObjectOrigin } from './ObjectOrigin'; import type { ObjectEvents } from '../../EventTypeDefs'; import type { ControlProps } from './types/ControlProps'; - -type TLineDescriptor = { - o: Point; - d: Point; -}; - -type TBBoxLines = { - topline: TLineDescriptor; - leftline: TLineDescriptor; - bottomline: TLineDescriptor; - rightline: TLineDescriptor; -}; +import { + type TBBoxLines, + findCrossPoints, + getImageLines, +} from '../../util/intersection/findCrossPoint'; type TMatrixCache = { key: string; @@ -306,7 +299,11 @@ export class ObjectGeometry ): boolean { const points = this.getCoords(absolute, calculate), otherCoords = absolute ? other.aCoords : other.lineCoords, - lines = other._getImageLines(otherCoords); + // this is maybe an excessive optimization that makes the code + // unnecessarly ugly. this is the only use case of passing lines + // to containsPoint. This optimization should go away but can go away + // in its own pr. + lines = getImageLines(otherCoords); for (let i = 0; i < 4; i++) { if (!other.containsPoint(points[i], lines)) { return false; @@ -349,7 +346,7 @@ export class ObjectGeometry /** * Checks if point is inside the object * @param {Point} point Point to check against - * @param {Object} [lines] object returned from @method _getImageLines + * @param {Object} [lines] object returned from util getImageLines * @param {Boolean} [absolute] use coordinates without viewportTransform * @param {Boolean} [calculate] use coordinates of current position instead of stored ones * @return {Boolean} true if point is inside the object @@ -361,8 +358,7 @@ export class ObjectGeometry calculate = false ): boolean { const coords = this._getCoords(absolute, calculate), - imageLines = lines || this._getImageLines(coords), - xPoints = this._findCrossPoints(point, imageLines); + xPoints = findCrossPoints(point, lines || getImageLines(coords)); // if xPoints is odd then point is inside the object return xPoints !== 0 && xPoints % 2 === 1; } @@ -440,96 +436,6 @@ export class ObjectGeometry ); } - /** - * Method that returns an object with the object edges in it, given the coordinates of the corners - * @private - * @param {Object} lineCoords or aCoords Coordinates of the object corners - */ - _getImageLines({ tl, tr, bl, br }: TCornerPoint): TBBoxLines { - const lines = { - topline: { - o: tl, - d: tr, - }, - rightline: { - o: tr, - d: br, - }, - bottomline: { - o: br, - d: bl, - }, - leftline: { - o: bl, - d: tl, - }, - }; - - // // debugging - // if (this.canvas.contextTop) { - // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); - // } - - return lines; - } - - /** - * Helper method to determine how many cross points are between the 4 object edges - * and the horizontal line determined by a point on canvas - * @private - * @param {Point} point Point to check - * @param {Object} lines Coordinates of the object being evaluated - * @return {number} number of crossPoint - */ - _findCrossPoints(point: Point, lines: TBBoxLines): number { - let xcount = 0; - - for (const lineKey in lines) { - let xi; - const iLine = lines[lineKey as keyof TBBoxLines]; - // optimization 1: line below point. no cross - if (iLine.o.y < point.y && iLine.d.y < point.y) { - continue; - } - // optimization 2: line above point. no cross - if (iLine.o.y >= point.y && iLine.d.y >= point.y) { - continue; - } - // optimization 3: vertical line case - if (iLine.o.x === iLine.d.x && iLine.o.x >= point.x) { - xi = iLine.o.x; - } - // calculate the intersection point - else { - const b1 = 0; - const b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x); - const a1 = point.y - b1 * point.x; - const a2 = iLine.o.y - b2 * iLine.o.x; - - xi = -(a1 - a2) / (b1 - b2); - } - // don't count xi < point.x cases - if (xi >= point.x) { - xcount += 1; - } - // optimization 4: specific for square images - if (xcount === 2) { - break; - } - } - return xcount; - } - /** * Returns coordinates of object's bounding rectangle (left, top, width, height) * the box is intended as aligned to axis of canvas. diff --git a/src/util/intersection/findCrossPoint.ts b/src/util/intersection/findCrossPoint.ts new file mode 100644 index 00000000000..d4942740f7d --- /dev/null +++ b/src/util/intersection/findCrossPoint.ts @@ -0,0 +1,114 @@ +import type { XY } from '../../Point'; +import type { TCornerPoint } from '../../typedefs'; + +type TLineDescriptor = { + o: XY; + d: XY; +}; + +export type TBBoxLines = { + topline: TLineDescriptor; + leftline: TLineDescriptor; + bottomline: TLineDescriptor; + rightline: TLineDescriptor; +}; + +/** + * Helper method to determine how many cross points are between the 4 object edges + * and the horizontal line determined by a point on canvas + * @private + * @param {Point} point Point to check + * @param {Object} lines Coordinates of the object being evaluated + * @return {number} number of crossPoint + */ +export const findCrossPoints = (point: XY, lines: TBBoxLines): number => { + let xcount = 0; + + for (const lineKey in lines) { + let xi; + const iLine = lines[lineKey as keyof TBBoxLines]; + // optimization 1: line below point. no cross + if (iLine.o.y < point.y && iLine.d.y < point.y) { + continue; + } + // optimization 2: line above point. no cross + if (iLine.o.y >= point.y && iLine.d.y >= point.y) { + continue; + } + // optimization 3: vertical line case + if (iLine.o.x === iLine.d.x && iLine.o.x >= point.x) { + xi = iLine.o.x; + } + // calculate the intersection point + else { + const b1 = 0; + const b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x); + const a1 = point.y - b1 * point.x; + const a2 = iLine.o.y - b2 * iLine.o.x; + + xi = -(a1 - a2) / (b1 - b2); + } + // don't count xi < point.x cases + if (xi >= point.x) { + xcount += 1; + } + // optimization 4: specific for square images (square or rects?) + // todo remove this optimazion for + if (xcount === 2) { + break; + } + } + return xcount; +}; + +/** + * Method that returns an object with the object edges in it, given the coordinates of the corners + * @private + * @param {Object} lineCoords or aCoords Coordinates of the object corners + */ +export const getImageLines = ({ tl, tr, bl, br }: TCornerPoint): TBBoxLines => { + const lines = { + topline: { + o: tl, + d: tr, + }, + rightline: { + o: tr, + d: br, + }, + bottomline: { + o: br, + d: bl, + }, + leftline: { + o: bl, + d: tl, + }, + }; + + // // debugging + // if (this.canvas.contextTop) { + // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); + // } + + return lines; +}; + +export const cornerPointContainsPoint = ( + point: XY, + cornerPoint: TCornerPoint +): boolean => { + const xPoints = findCrossPoints(point, getImageLines(cornerPoint)); + // if xPoints is odd then point is inside the object + return xPoints !== 0 && xPoints % 2 === 1; +};