Skip to content

Commit

Permalink
fix(Control): calcCornerCoords angle + calculation (#9377)
Browse files Browse the repository at this point in the history
  • Loading branch information
ShaMan123 authored Sep 26, 2023
1 parent 6f24521 commit fc21b49
Show file tree
Hide file tree
Showing 10 changed files with 255 additions and 43 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]

- fix(Control): `calcCornerCoords` angle + calculation [#9377](https://github.com/fabricjs/fabric.js/pull/9377)
- patch(): dep findCrossPoints in favor of `isPointInPolygon` [#9374](https://github.com/fabricjs/fabric.js/pull/9374)
- docs() enable typedocs to run again [#9356](https://github.com/fabricjs/fabric.js/pull/9356)
- chore(): cleanup logs and error messages [#9369](https://github.com/fabricjs/fabric.js/pull/9369)
Expand Down
27 changes: 27 additions & 0 deletions e2e/tests/controls/hit-regions/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { expect, test } from '@playwright/test';
import setup from '../../../setup';
import { CanvasUtil } from '../../../utils/CanvasUtil';

setup();

test('Control hit regions', async ({ page }) => {
const canvasUtil = new CanvasUtil(page);
await canvasUtil.executeInBrowser((canvas) => {
const rect = canvas.getActiveObject();
const render = ({ x, y }: fabric.Point, fill: string) => {
const ctx = canvas.getTopContext();
ctx.fillStyle = fill;
ctx.beginPath();
ctx.arc(x, y, 1, 0, Math.PI * 2);
ctx.fill();
};
for (let y = 0; y <= canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
const point = new fabric.Point(x, y);
rect._findTargetCorner(point, true) && render(point, 'indigo');
rect._findTargetCorner(point) && render(point, 'magenta');
}
}
});
expect(await new CanvasUtil(page).screenshot()).toMatchSnapshot();
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions e2e/tests/controls/hit-regions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Runs in the **BROWSER**
* Imports are defined in 'e2e/imports.ts'
*/

import * as fabric from 'fabric';
import { beforeAll } from '../../test';

beforeAll((canvas) => {
canvas.setDimensions({ width: 300, height: 325 });
const controls = fabric.controlsUtils.createObjectDefaultControls();
Object.values(controls).forEach((control) => {
control.sizeX = 20;
control.sizeY = 25;
control.touchSizeX = 30;
control.touchSizeY = 35;
});
const rect = new fabric.Rect({
left: 25,
top: 60,
width: 75,
height: 100,
controls,
scaleY: 2,
fill: 'blue',
padding: 10,
});
const group = new fabric.Group([rect], {
angle: 30,
scaleX: 2,
interactive: true,
subTargetCheck: true,
});
canvas.add(group);
canvas.centerObject(group);
group.setCoords();
canvas.setActiveObject(rect);
canvas.renderAll();
return { rect, group };
});
2 changes: 1 addition & 1 deletion e2e/utils/CanvasUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class CanvasUtil {

async executeInBrowser<C, R>(
runInBrowser: (canvas: Canvas, context: C) => R,
context: C
context?: C
): Promise<R> {
return (
await this.page.evaluateHandle<Canvas>(
Expand Down
117 changes: 117 additions & 0 deletions src/benchmarks/calcCornerCoords.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import {
Object as FabricObject,
Point,
util,
Control,
} from '../../dist/index.mjs';

// Swapping of calcCornerCoords in #9377

// OLD CODE FOR REFERENCE AND IMPLEMENTATION TEST

const halfPI = Math.PI / 2;

class OldControl extends Control {
calcCornerCoords(
objectAngle,
angle,
objectCornerSize,
centerX,
centerY,
isTouch
) {
let cosHalfOffset, sinHalfOffset, cosHalfOffsetComp, sinHalfOffsetComp;
const xSize = isTouch ? this.touchSizeX : this.sizeX,
ySize = isTouch ? this.touchSizeY : this.sizeY;
if (xSize && ySize && xSize !== ySize) {
// handle rectangular corners
const controlTriangleAngle = Math.atan2(ySize, xSize);
const cornerHypotenuse = Math.sqrt(xSize * xSize + ySize * ySize) / 2;
const newTheta =
controlTriangleAngle - util.degreesToRadians(objectAngle);
const newThetaComp =
halfPI - controlTriangleAngle - util.degreesToRadians(objectAngle);
cosHalfOffset = cornerHypotenuse * util.cos(newTheta);
sinHalfOffset = cornerHypotenuse * util.sin(newTheta);
// use complementary angle for two corners
cosHalfOffsetComp = cornerHypotenuse * util.cos(newThetaComp);
sinHalfOffsetComp = cornerHypotenuse * util.sin(newThetaComp);
} else {
// handle square corners
// use default object corner size unless size is defined
const cornerSize = xSize && ySize ? xSize : objectCornerSize;
const cornerHypotenuse = cornerSize * Math.SQRT1_2;
// complementary angles are equal since they're both 45 degrees
const newTheta = util.degreesToRadians(45 - objectAngle);
cosHalfOffset = cosHalfOffsetComp = cornerHypotenuse * util.cos(newTheta);
sinHalfOffset = sinHalfOffsetComp = cornerHypotenuse * util.sin(newTheta);
}

return {
tl: new Point(centerX - sinHalfOffsetComp, centerY - cosHalfOffsetComp),
tr: new Point(centerX + cosHalfOffset, centerY - sinHalfOffset),
bl: new Point(centerX - cosHalfOffset, centerY + sinHalfOffset),
br: new Point(centerX + sinHalfOffsetComp, centerY + cosHalfOffsetComp),
};
}
}

class OldObject extends FabricObject {
_calcCornerCoords(control, position) {
const corner = control.calcCornerCoords(
this.angle,
this.cornerSize,
position.x,
position.y,
false
);
const touchCorner = control.calcCornerCoords(
this.angle,
this.touchCornerSize,
position.x,
position.y,
true
);
return { corner, touchCorner };
}
}

// END OF OLD CODE

const newObject = new FabricObject({ width: 100, height: 100 });

const oldObject = new OldObject({ width: 100, height: 100 });

newObject.controls = {
tl: new Control({
x: -0.5,
y: -0.5,
}),
};

oldObject.controls = {
tl: new OldControl({
x: -0.5,
y: -0.5,
}),
};

const benchmark = (callback) => {
const start = Date.now();
callback();
return Date.now() - start;
};

const controlNew = benchmark(() => {
for (let i = 0; i < 1_000_000; i++) {
newObject._calcCornerCoords(newObject.controls.tl, new Point(4.5, 4.5));
}
});

const controlOld = benchmark(() => {
for (let i = 0; i < 1_000_000; i++) {
oldObject._calcCornerCoords(oldObject.controls.tl, new Point(4.5, 4.5));
}
});

console.log({ controlOld, controlNew });
2 changes: 2 additions & 0 deletions src/benchmarks/raycasting.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Object as FabricObject, Point } from '../../dist/index.mjs';

// SWAPPING OF RAY CASTING LOGIC IN #9381

// OLD CODE FOR REFERENCE AND IMPLEMENTATION TEST

// type TLineDescriptor = {
Expand Down
62 changes: 23 additions & 39 deletions src/controls/Control.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { halfPI } from '../constants';
import type {
ControlActionHandler,
TPointerEvent,
Expand All @@ -9,9 +8,12 @@ import { Intersection } from '../Intersection';
import { Point } from '../Point';
import type { InteractiveFabricObject } from '../shapes/Object/InteractiveObject';
import type { TCornerPoint, TDegree, TMat2D } from '../typedefs';
import { cos } from '../util/misc/cos';
import { degreesToRadians } from '../util/misc/radiansDegreesConversion';
import { sin } from '../util/misc/sin';
import {
createRotateMatrix,
createScaleMatrix,
createTranslateMatrix,
multiplyTransformMatrixArray,
} from '../util/misc/matrix';
import type { ControlRenderingStyleOverride } from './controlRendering';
import { renderCircleControl, renderSquareControl } from './controlRendering';

Expand Down Expand Up @@ -94,31 +96,31 @@ export class Control {
* @type {?Number}
* @default null
*/
sizeX: number | null = null;
sizeX = 0;

/**
* Sets the height of the control. If null, defaults to object's cornerSize.
* Expects both sizeX and sizeY to be set when set.
* @type {?Number}
* @default null
*/
sizeY: number | null = null;
sizeY = 0;

/**
* Sets the length of the touch area of the control. If null, defaults to object's touchCornerSize.
* Expects both touchSizeX and touchSizeY to be set when set.
* @type {?Number}
* @default null
*/
touchSizeX: number | null = null;
touchSizeX = 0;

/**
* Sets the height of the touch area of the control. If null, defaults to object's touchCornerSize.
* Expects both touchSizeX and touchSizeY to be set when set.
* @type {?Number}
* @default null
*/
touchSizeY: number | null = null;
touchSizeY = 0;

/**
* Css cursor style to display when the control is hovered.
Expand Down Expand Up @@ -307,43 +309,25 @@ export class Control {
* @param {boolean} isTouch true if touch corner, false if normal corner
*/
calcCornerCoords(
objectAngle: TDegree,
angle: TDegree,
objectCornerSize: number,
centerX: number,
centerY: number,
isTouch: boolean
) {
let cosHalfOffset, sinHalfOffset, cosHalfOffsetComp, sinHalfOffsetComp;
const xSize = isTouch ? this.touchSizeX : this.sizeX,
ySize = isTouch ? this.touchSizeY : this.sizeY;
if (xSize && ySize && xSize !== ySize) {
// handle rectangular corners
const controlTriangleAngle = Math.atan2(ySize, xSize);
const cornerHypotenuse = Math.sqrt(xSize * xSize + ySize * ySize) / 2;
const newTheta = controlTriangleAngle - degreesToRadians(objectAngle);
const newThetaComp =
halfPI - controlTriangleAngle - degreesToRadians(objectAngle);
cosHalfOffset = cornerHypotenuse * cos(newTheta);
sinHalfOffset = cornerHypotenuse * sin(newTheta);
// use complementary angle for two corners
cosHalfOffsetComp = cornerHypotenuse * cos(newThetaComp);
sinHalfOffsetComp = cornerHypotenuse * sin(newThetaComp);
} else {
// handle square corners
// use default object corner size unless size is defined
const cornerSize = xSize && ySize ? xSize : objectCornerSize;
const cornerHypotenuse = cornerSize * Math.SQRT1_2;
// complementary angles are equal since they're both 45 degrees
const newTheta = degreesToRadians(45 - objectAngle);
cosHalfOffset = cosHalfOffsetComp = cornerHypotenuse * cos(newTheta);
sinHalfOffset = sinHalfOffsetComp = cornerHypotenuse * sin(newTheta);
}

const t = multiplyTransformMatrixArray([
createTranslateMatrix(centerX, centerY),
createRotateMatrix({ angle }),
createScaleMatrix(
(isTouch ? this.touchSizeX : this.sizeX) || objectCornerSize,
(isTouch ? this.touchSizeY : this.sizeY) || objectCornerSize
),
]);
return {
tl: new Point(centerX - sinHalfOffsetComp, centerY - cosHalfOffsetComp),
tr: new Point(centerX + cosHalfOffset, centerY - sinHalfOffset),
bl: new Point(centerX - cosHalfOffset, centerY + sinHalfOffset),
br: new Point(centerX + sinHalfOffsetComp, centerY + cosHalfOffsetComp),
tl: new Point(-0.5, -0.5).transform(t),
tr: new Point(0.5, -0.5).transform(t),
bl: new Point(-0.5, 0.5).transform(t),
br: new Point(0.5, 0.5).transform(t),
};
}

Expand Down
40 changes: 40 additions & 0 deletions src/shapes/Object/InteractiveObject.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { radiansToDegrees } from '../../util';
import { Group } from '../Group';
import { FabricObject } from './FabricObject';
import type { TOCoord } from './InteractiveObject';

describe('Object', () => {
describe('setCoords for objects inside group with rotation', () => {
it('all corners are rotated as much as the object total angle', () => {
const object = new FabricObject({
left: 25,
top: 60,
width: 75,
height: 100,
angle: 10,
scaleY: 2,
fill: 'blue',
});
const group = new Group([object], {
angle: 30,
scaleX: 2,
interactive: true,
subTargetCheck: true,
});
group.setCoords();
const objectAngle = Math.round(object.getTotalAngle());
expect(objectAngle).toEqual(35);
Object.values(object.oCoords).forEach((cornerPoint: TOCoord) => {
const controlAngle = Math.round(
radiansToDegrees(
Math.atan2(
cornerPoint.corner.tr.y - cornerPoint.corner.tl.y,
cornerPoint.corner.tr.x - cornerPoint.corner.tl.x
)
)
);
expect(controlAngle).toEqual(objectAngle);
});
});
});
});
7 changes: 4 additions & 3 deletions src/shapes/Object/InteractiveObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { FabricObjectProps } from './types/FabricObjectProps';
import type { TFabricObjectProps, SerializedObjectProps } from './types';
import { createObjectDefaultControls } from '../../controls/commonControls';

type TOCoord = Point & {
export type TOCoord = Point & {
corner: TCornerPoint;
touchCorner: TCornerPoint;
};
Expand Down Expand Up @@ -278,15 +278,16 @@ export class InteractiveFabricObject<
* @private
*/
private _calcCornerCoords(control: Control, position: Point) {
const angle = this.getTotalAngle();
const corner = control.calcCornerCoords(
this.angle,
angle,
this.cornerSize,
position.x,
position.y,
false
);
const touchCorner = control.calcCornerCoords(
this.angle,
angle,
this.touchCornerSize,
position.x,
position.y,
Expand Down

0 comments on commit fc21b49

Please sign in to comment.