Skip to content

Commit

Permalink
patch(Control): move hit detection to shouldActivate (#9374)
Browse files Browse the repository at this point in the history
  • Loading branch information
asturur authored Sep 24, 2023
1 parent ef27fcd commit 6df783b
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 118 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 10 additions & 3 deletions src/controls/Control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)
);
}

Expand Down
19 changes: 10 additions & 9 deletions src/shapes/Object/InteractiveObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
118 changes: 12 additions & 106 deletions src/shapes/Object/ObjectGeometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -306,7 +299,11 @@ export class ObjectGeometry<EventSpec extends ObjectEvents = ObjectEvents>
): 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;
Expand Down Expand Up @@ -349,7 +346,7 @@ export class ObjectGeometry<EventSpec extends ObjectEvents = ObjectEvents>
/**
* 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
Expand All @@ -361,8 +358,7 @@ export class ObjectGeometry<EventSpec extends ObjectEvents = ObjectEvents>
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;
}
Expand Down Expand Up @@ -440,96 +436,6 @@ export class ObjectGeometry<EventSpec extends ObjectEvents = ObjectEvents>
);
}

/**
* 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.
Expand Down
114 changes: 114 additions & 0 deletions src/util/intersection/findCrossPoint.ts
Original file line number Diff line number Diff line change
@@ -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;
};

0 comments on commit 6df783b

Please sign in to comment.