Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Control): calcCornerCoords angle + calculation #9377

Merged
merged 12 commits into from
Sep 26, 2023
Merged
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