diff --git a/CHANGELOG.md b/CHANGELOG.md index 499011a1e1b..71d3d7ed89b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -299,12 +299,16 @@ - fix(IText): layout change regression caused by #8663 (`text` was changed but layout was skipped) [#8711](https://github.com/fabricjs/fabric.js/pull/8711) - fix(IText, Textbox): fix broken text input [#8775](https://github.com/fabricjs/fabric.js/pull/8775) - ci(): `.codesandbox` [#8135](https://github.com/fabricjs/fabric.js/pull/8135) +- fix(): redo object geometry [#8767](https://github.com/fabricjs/fabric.js/pull/8767) - ci(): disallow circular deps [#8759](https://github.com/fabricjs/fabric.js/pull/8759) - fix(): env WebGL import cycle [#8758](https://github.com/fabricjs/fabric.js/pull/8758) - chore(TS): remove controls from prototype. BREAKING: controls aren't shared anymore [#8753](https://github.com/fabricjs/fabric.js/pull/8753) - chore(TS): remove object `type` from prototype [#8714](https://github.com/fabricjs/fabric.js/pull/8714) - chore(TS): type Object props [#8677](https://github.com/fabricjs/fabric.js/issues/8677) +- fix(Geometry): `_getCoords` not respecting group [#8747](https://github.com/fabricjs/fabric.js/issues/8747) - chore(TS): remove default values from filter prototypes [#8742](https://github.com/fabricjs/fabric.js/issues/8742) +- refactor(): Control connection rendering [#8745](https://github.com/fabricjs/fabric.js/issues/8745) + **BREAKING**: `Control#render` method signature - chore(TS): remove default values from Objects prototypes, ( filters in a followup ) [#8719](https://github.com/fabricjs/fabric.js/issues/8719) - fix(Intersection): bug causing selection edge case [#8735](https://github.com/fabricjs/fabric.js/pull/8735) - chore(TS): class interface for options/brevity [#8674](https://github.com/fabricjs/fabric.js/issues/8674) diff --git a/e2e/tests/controls/hit-regions/index.ts b/e2e/tests/controls/hit-regions/index.ts index 727c063e281..f5b96188a6b 100644 --- a/e2e/tests/controls/hit-regions/index.ts +++ b/e2e/tests/controls/hit-regions/index.ts @@ -33,7 +33,6 @@ beforeAll((canvas) => { }); canvas.add(group); canvas.centerObject(group); - group.setCoords(); canvas.setActiveObject(rect); canvas.renderAll(); return { rect, group }; diff --git a/e2e/utils/ObjectUtil.ts b/e2e/utils/ObjectUtil.ts index 0203cb93a31..f23d2e874e9 100644 --- a/e2e/utils/ObjectUtil.ts +++ b/e2e/utils/ObjectUtil.ts @@ -39,7 +39,7 @@ export class ObjectUtil { getObjectControlPoint(controlName: string) { return this.executeInBrowser( - (object, { controlName }) => object.oCoords[controlName], + (object, { controlName }) => object.getControlCoords()[controlName], { controlName } ); } diff --git a/lib/aligning_guidelines.js b/lib/aligning_guidelines.js index 495f1a4fa8e..9f117e1296b 100644 --- a/lib/aligning_guidelines.js +++ b/lib/aligning_guidelines.js @@ -101,7 +101,7 @@ function initAligningGuidelines(canvas) { ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) }); - activeObject.setPositionByOrigin(new fabric.Point(objectLeft, activeObjectTop), 'center', 'center'); + activeObject.setXY(new fabric.Point(objectLeft, activeObjectTop), 'center', 'center'); } // snap by the left edge @@ -116,7 +116,7 @@ function initAligningGuidelines(canvas) { ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) }); - activeObject.setPositionByOrigin(new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop), 'center', 'center'); + activeObject.setXY(new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop), 'center', 'center'); } // snap by the right edge @@ -131,7 +131,7 @@ function initAligningGuidelines(canvas) { ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) }); - activeObject.setPositionByOrigin(new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop), 'center', 'center'); + activeObject.setXY(new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop), 'center', 'center'); } // snap by the vertical center line @@ -146,7 +146,7 @@ function initAligningGuidelines(canvas) { ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) }); - activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop), 'center', 'center'); + activeObject.setXY(new fabric.Point(activeObjectLeft, objectTop), 'center', 'center'); } // snap by the top edge @@ -161,7 +161,7 @@ function initAligningGuidelines(canvas) { ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) }); - activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2), 'center', 'center'); + activeObject.setXY(new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2), 'center', 'center'); } // snap by the bottom edge @@ -176,7 +176,7 @@ function initAligningGuidelines(canvas) { ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) }); - activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2), 'center', 'center'); + activeObject.setXY(new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2), 'center', 'center'); } } diff --git a/lib/centering_guidelines.js b/lib/centering_guidelines.js index 5c6f6c8ed7e..27bd436f7af 100644 --- a/lib/centering_guidelines.js +++ b/lib/centering_guidelines.js @@ -63,7 +63,7 @@ function initCenteringGuidelines(canvas) { isInHorizontalCenter = Math.round(objectCenter.y) in canvasHeightCenterMap; if (isInHorizontalCenter || isInVerticalCenter) { - object.setPositionByOrigin(new fabric.Point((isInVerticalCenter ? canvasWidthCenter : objectCenter.x), (isInHorizontalCenter ? canvasHeightCenter : objectCenter.y)), 'center', 'center'); + object.setXY(new fabric.Point((isInVerticalCenter ? canvasWidthCenter : objectCenter.x), (isInHorizontalCenter ? canvasHeightCenter : objectCenter.y)), 'center', 'center'); } }); diff --git a/src/BBox/BBox.ts b/src/BBox/BBox.ts new file mode 100644 index 00000000000..96737356b3d --- /dev/null +++ b/src/BBox/BBox.ts @@ -0,0 +1,176 @@ +import { iMatrix } from '../constants'; +import { Point } from '../Point'; +import type { ObjectBBox } from '../shapes/Object/ObjectBBox'; +import type { TMat2D } from '../typedefs'; +import { mapValues } from '../util/internals'; +import { makeBoundingBoxFromPoints } from '../util/misc/boundingBoxFromPoints'; +import { + calcBaseChangeMatrix, + sendPointToPlane, +} from '../util/misc/planeChange'; +import { radiansToDegrees } from '../util/misc/radiansDegreesConversion'; +import { createVector } from '../util/misc/vectors'; +import type { ViewportBBoxPlanes } from './ViewportBBox'; +import { ViewportBBox } from './ViewportBBox'; + +export interface BBoxPlanes extends ViewportBBoxPlanes { + parent(): TMat2D; + self(): TMat2D; +} + +export class BBox extends ViewportBBox { + protected declare readonly planes: BBoxPlanes; + + protected constructor(transform: TMat2D, planes: BBoxPlanes) { + super(transform, planes); + } + + /** + * Use this to operate on the object's own transformation.\ + * Used to position the object in the relative plane + */ + sendToParent() { + return this.sendToPlane(this.planes.parent()); + } + + /** + * Use this to operate in a transform-less plane + * + * e.g. + * {@link getBBox} will return the following: + * + * ```js + * let w = object.width; + * let h = object.height; + * let s = object.strokeWidth; + * let sx, sy, px, py; // non linear stroke/padding factors transformed back to the object plane + * ``` + * + * | case | left | top | width | height | + * | --- | --- | --- | --- | --- | + * | no `stroke`/`padding` | `-w / 2` | `-h / 2` | `w` | `h` | + * | `strokeUniform = false` | `-w / 2 - s` | `-h / 2 - s` | `w + s * 2` | `h + s * 2` | + * | `strokeUniform = true || padding` | `-w / 2 - sx` | `-h / 2 - sy` | `w + sx * 2` | `h + sy * 2` | + * + */ + sendToSelf() { + return this.sendToPlane(this.planes.self()); + } + + static getViewportCoords(target: ObjectBBox) { + const coords = target.calcCoords(); + if (target.needsViewportCoords()) { + return coords; + } else { + const vpt = target.getViewportTransform(); + return mapValues(coords, (coord) => sendPointToPlane(coord, vpt)); + } + } + + static getPlanes(target: ObjectBBox): BBoxPlanes { + const self = target.calcTransformMatrix(); + const parent = target.group?.calcTransformMatrix() || iMatrix; + const viewport = target.getViewportTransform(); + return { + self() { + return self; + }, + parent() { + return parent; + }, + viewport() { + return viewport; + }, + }; + } + + static bbox(target: ObjectBBox) { + const coords = this.getViewportCoords(target); + const bbox = makeBoundingBoxFromPoints(Object.values(coords)); + const transform = calcBaseChangeMatrix( + undefined, + [new Point(bbox.width, 0), new Point(0, bbox.height)], + coords.tl.midPointFrom(coords.br) + ); + return new this(transform, this.getPlanes(target)); + } + + /** + * + * @param target + * @returns the bbox that respects rotation and flipping + */ + static rotated(target: ObjectBBox) { + const coords = this.getViewportCoords(target); + const rotation = this.calcRotation(coords); + const center = coords.tl.midPointFrom(coords.br); + const bbox = makeBoundingBoxFromPoints( + Object.values(coords).map((coord) => coord.rotate(-rotation.x, center)) + ); + const transform = calcBaseChangeMatrix( + undefined, + [ + // flipX/Y are taken into consideration in `rotation` + new Point(bbox.width, 0).rotate(rotation.x), + new Point(0, bbox.height).rotate(rotation.y), + ], + center + ); + return new this(transform, this.getPlanes(target)); + } + + static legacy(target: ObjectBBox) { + const coords = this.getViewportCoords(target); + const rotation = this.calcRotation(coords); + const center = coords.tl.midPointFrom(coords.br); + const viewportBBox = makeBoundingBoxFromPoints(Object.values(coords)); + const rotatedBBox = makeBoundingBoxFromPoints( + Object.values(coords).map((coord) => coord.rotate(-rotation.x, center)) + ); + const bboxTransform = calcBaseChangeMatrix( + undefined, + [ + new Point(rotatedBBox.width / viewportBBox.width, 0), + new Point(0, rotatedBBox.height / viewportBBox.height), + ], + center + ); + const legacyCoords = mapValues(coords, (coord) => + coord.transform(bboxTransform) + ); + const legacyBBox = makeBoundingBoxFromPoints(Object.values(legacyCoords)); + const transform = calcBaseChangeMatrix( + undefined, + [new Point(1, 0).rotate(rotation.x), new Point(0, 1).rotate(rotation.y)], + center + ); + return { + angle: radiansToDegrees(rotation.x), + getCoords() { + return legacyCoords; + }, + getTransformation() { + return transform; + }, + getBBox() { + return legacyBBox; + }, + getBBoxVector() { + return new Point(legacyBBox.width, legacyBBox.height); + }, + transform(ctx: CanvasRenderingContext2D) { + ctx.transform(...transform); + }, + }; + } + + static transformed(target: ObjectBBox) { + const coords = this.getViewportCoords(target); + const transform = calcBaseChangeMatrix( + undefined, + [createVector(coords.tl, coords.tr), createVector(coords.tl, coords.bl)], + coords.tl.midPointFrom(coords.br) + ); + return new this(transform, this.getPlanes(target)); + } +} diff --git a/src/BBox/OwnBBox.ts b/src/BBox/OwnBBox.ts new file mode 100644 index 00000000000..02d43d4012c --- /dev/null +++ b/src/BBox/OwnBBox.ts @@ -0,0 +1,41 @@ +import { iMatrix } from '../constants'; +import type { ObjectBBox } from '../shapes/Object/ObjectBBox'; +import type { TMat2D } from '../typedefs'; +import { mapValues } from '../util/internals'; +import { multiplyTransformMatrices } from '../util/misc/matrix'; +import { sendPointToPlane } from '../util/misc/planeChange'; +import type { BBoxPlanes } from './BBox'; +import { BBox } from './BBox'; + +/** + * Performance optimization + */ +export class OwnBBox extends BBox { + constructor(transform: TMat2D, planes: BBoxPlanes) { + super(transform, planes); + } + + getCoords() { + const from = multiplyTransformMatrices( + this.planes.viewport(), + this.planes.self() + ); + return mapValues(super.getCoords(), (coord) => + sendPointToPlane(coord, from) + ); + } + + static getPlanes(target: ObjectBBox): BBoxPlanes { + return { + self() { + return target.calcTransformMatrix(); + }, + parent() { + return target.group?.calcTransformMatrix() || iMatrix; + }, + viewport() { + return target.getViewportTransform(); + }, + }; + } +} diff --git a/src/BBox/PlaneBBox.ts b/src/BBox/PlaneBBox.ts new file mode 100644 index 00000000000..9dd6e1c898f --- /dev/null +++ b/src/BBox/PlaneBBox.ts @@ -0,0 +1,159 @@ +import { Point } from '../Point'; +import type { TBBox, TCornerPoint, TMat2D } from '../typedefs'; +import { mapValues } from '../util/internals'; +import { makeBoundingBoxFromPoints } from '../util/misc/boundingBoxFromPoints'; +import { invertTransform } from '../util/misc/matrix'; +import { calcBaseChangeMatrix } from '../util/misc/planeChange'; +import { + calcAngleBetweenVectors, + calcVectorRotation, + createVector, +} from '../util/misc/vectors'; + +/** + * This class is in an abstraction allowing us to operate inside a plane with origin values [-0.5, 0.5] + * instead of using real values that depend on the plane. + * + * Simplifies complex layout/geometry calculations. + */ +export class PlaneBBox { + private readonly originTransformation: TMat2D; + + protected constructor(transform: TMat2D) { + this.originTransformation = Object.freeze([...transform]) as TMat2D; + } + + getTransformation() { + return this.originTransformation; + } + + getCoords() { + return mapValues( + { + tl: new Point(-0.5, -0.5), + tr: new Point(0.5, -0.5), + br: new Point(0.5, 0.5), + bl: new Point(-0.5, 0.5), + }, + (origin) => this.pointFromOrigin(origin) + ); + } + + getBBox() { + return makeBoundingBoxFromPoints(Object.values(this.getCoords())); + } + + getCenterPoint() { + return this.pointFromOrigin(new Point()); + } + + getBBoxVector() { + const { width, height } = this.getBBox(); + return new Point(width, height); + } + + /** + * Calculates rotation for each side vector from the x axis of the viewport + * @param param0 coords + * @returns + */ + static calcRotation({ + tl, + tr, + bl, + }: Record<'tl' | 'tr' | 'bl' | 'br', Point>) { + const sideVectorX = createVector(tl, tr); + const sideVectorY = createVector(tl, bl); + const rotationFromXAxis = calcVectorRotation(createVector(tl, tr)); + const yIsFlipped = calcAngleBetweenVectors(sideVectorY, sideVectorX) > 0; + return new Point( + rotationFromXAxis, + rotationFromXAxis + (yIsFlipped ? Math.PI : 0) + ); + } + + getRotation() { + return PlaneBBox.calcRotation(this.getCoords()); + } + + pointFromOrigin(origin: Point) { + return origin.transform(this.getTransformation()); + } + + pointToOrigin(point: Point) { + return point.transform(invertTransform(this.getTransformation())); + } + + /** + * This is where point and vector meet since point is a vector from its origin + * + * Let `O` be the origin, `P` the point, `O'` the desired origin, `P'` the point described by `O'` + * ```latex + * P = OP = (left, top) + * P' = O'P = O'O + OP + * ``` + * + * @returns a point that is positioned in the same place as {@link point} but refers to {@link to} as its origin instead of {@link from} + */ + changeOrigin(point: Point, from: Point, to: Point) { + return point.add(this.vectorFromOrigin(createVector(to, from))); + } + + vectorFromOrigin(originVector: Point) { + return originVector.transform(this.getTransformation(), true); + } + + vectorToOrigin(vector: Point) { + return vector.transform(invertTransform(this.getTransformation()), true); + } + + calcOriginTranslation(origin: Point, prev: this) { + return prev.getOriginTranslation(this.pointFromOrigin(origin), origin); + } + + /** + * + * @param point new position of {@link origin} + * @param origin + * @returns the translation to apply to the bbox to respect the new position + */ + getOriginTranslation(point: Point, origin: Point = new Point()) { + const prev = this.pointFromOrigin(origin); + return createVector(prev, point); + } + + containsPoint(point: Point) { + const pointAsOrigin = this.pointToOrigin(point); + return ( + pointAsOrigin.x >= -0.5 && + pointAsOrigin.x <= 0.5 && + pointAsOrigin.y >= -0.5 && + pointAsOrigin.y <= 0.5 + ); + } + + transform(ctx: CanvasRenderingContext2D) { + ctx.transform(...this.getTransformation()); + } + + static getTransformation(coords: TCornerPoint) { + return calcBaseChangeMatrix( + undefined, + [createVector(coords.tl, coords.tr), createVector(coords.tl, coords.bl)], + coords.tl.midPointFrom(coords.br) + ); + } + + static build(coords: TCornerPoint) { + return new this(this.getTransformation(coords)); + } + + static rect({ left, top, width, height }: TBBox) { + const transform = calcBaseChangeMatrix( + undefined, + [new Point(width, 0), new Point(0, height)], + new Point(left + width / 2, top + height / 2) + ); + return new this(transform); + } +} diff --git a/src/BBox/ViewportBBox.ts b/src/BBox/ViewportBBox.ts new file mode 100644 index 00000000000..1427e46cc06 --- /dev/null +++ b/src/BBox/ViewportBBox.ts @@ -0,0 +1,114 @@ +import type { StaticCanvas } from '../canvas/StaticCanvas'; +import { iMatrix } from '../constants'; +import { Intersection } from '../Intersection'; +import { Point } from '../Point'; +import type { TBBox, TMat2D } from '../typedefs'; +import { makeBoundingBoxFromPoints } from '../util/misc/boundingBoxFromPoints'; +import { + invertTransform, + multiplyTransformMatrices, +} from '../util/misc/matrix'; +import { + calcBaseChangeMatrix, + sendPointToPlane, +} from '../util/misc/planeChange'; +import { PlaneBBox } from './PlaneBBox'; + +export interface ViewportBBoxPlanes { + viewport(): TMat2D; +} + +/** + * This class manages operations in the canvas viewport since object geometry depends on the viewport (e.g. `strokeUniform`) + * + */ +export class ViewportBBox extends PlaneBBox { + protected readonly planes: ViewportBBoxPlanes; + + protected constructor(transform: TMat2D, planes: ViewportBBoxPlanes) { + super(transform); + this.planes = planes; + } + + sendToPlane(plane: TMat2D) { + const backToPlane = invertTransform( + multiplyTransformMatrices(this.planes.viewport(), plane) + ); + return new PlaneBBox( + multiplyTransformMatrices(backToPlane, this.getTransformation()) + ); + } + + sendToCanvas() { + return this.sendToPlane(iMatrix); + } + + intersect(other: ViewportBBox) { + const coords = Object.values(this.getCoords()); + const otherCoords = Object.values(other.getCoords()); + return Intersection.intersectPolygonPolygon(coords, otherCoords); + } + + intersects(other: ViewportBBox) { + const intersection = this.intersect(other); + return ( + intersection.status === 'Intersection' || + intersection.status === 'Coincident' + ); + } + + contains(other: ViewportBBox) { + const otherCoords = Object.values(other.getCoords()); + return otherCoords.every((coord) => this.containsPoint(coord)); + } + + isContainedBy(other: ViewportBBox) { + return other.contains(this); + } + + overlaps(other: ViewportBBox) { + return ( + this.intersects(other) || + this.contains(other) || + this.isContainedBy(other) + ); + } + + static rect({ left, top, width, height }: TBBox, vpt: TMat2D = iMatrix) { + const transform = calcBaseChangeMatrix( + undefined, + [new Point(width, 0), new Point(0, height)], + new Point(left + width / 2, top + height / 2) + ); + return new this(transform, { + viewport() { + return vpt; + }, + }); + } + + static bounds(tl: Point, br: Point, vpt: TMat2D) { + return this.rect(makeBoundingBoxFromPoints([tl, br]), vpt); + } + + static canvas(canvas: StaticCanvas) { + return this.rect( + { + left: 0, + top: 0, + width: canvas.width, + height: canvas.height, + }, + [...canvas.viewportTransform] + ); + } + + static canvasBounds(tl: Point, br: Point, vpt: TMat2D) { + return this.rect( + makeBoundingBoxFromPoints( + [tl, br].map((point) => sendPointToPlane(point, undefined, vpt)) + ), + vpt + ); + } +} diff --git a/src/CommonMethods.ts b/src/CommonMethods.ts index fe0a09d5ad5..c9215dc8e10 100644 --- a/src/CommonMethods.ts +++ b/src/CommonMethods.ts @@ -22,7 +22,7 @@ export class CommonMethods extends Observable { } /** - * Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`. + * Sets property to a given value. * @param {String|Object} key Property name or object (if object, iterate over the object properties) * @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one) */ diff --git a/src/EventTypeDefs.ts b/src/EventTypeDefs.ts index d41ed30ebb0..fc655806022 100644 --- a/src/EventTypeDefs.ts +++ b/src/EventTypeDefs.ts @@ -3,7 +3,6 @@ import type { Point } from './Point'; import type { FabricObject } from './shapes/Object/FabricObject'; import type { Group } from './shapes/Group'; import type { TOriginX, TOriginY, TRadian } from './typedefs'; -import type { saveObjectTransform } from './util/misc/objectTransforms'; import type { Canvas } from './canvas/Canvas'; import type { IText } from './shapes/IText/IText'; import type { StaticCanvas } from './canvas/StaticCanvas'; @@ -50,20 +49,15 @@ export type ControlCallback = ( export type ControlCursorCallback = ControlCallback; /** - * relative to target's containing coordinate plane - * both agree on every point + * relative to the viewport */ export type Transform = { target: FabricObject; action?: string; actionHandler?: TransformActionHandler; + actionPerformed: boolean; corner: string; - scaleX: number; - scaleY: number; - skewX: number; - skewY: number; - offsetX: number; - offsetY: number; + control?: Control; originX: TOriginX; originY: TOriginY; ex: number; @@ -71,15 +65,8 @@ export type Transform = { lastX: number; lastY: number; theta: TRadian; - width: number; - height: number; shiftKey: boolean; altKey: boolean; - original: ReturnType & { - originX: TOriginX; - originY: TOriginY; - }; - actionPerformed: boolean; }; export interface TEvent { diff --git a/src/LayoutManager/LayoutManager.spec.ts b/src/LayoutManager/LayoutManager.spec.ts index 01724537a09..0c9b5a903d2 100644 --- a/src/LayoutManager/LayoutManager.spec.ts +++ b/src/LayoutManager/LayoutManager.spec.ts @@ -345,10 +345,10 @@ describe('Layout Manager', () => { const targetSet = jest.spyOn(target, 'set').mockImplementation(() => { lifecycle.push(targetSet); }); - const targetSetCoords = jest - .spyOn(target, 'setCoords') + const targetInvalidateCoords = jest + .spyOn(target, 'invalidateCoords') .mockImplementation(() => { - lifecycle.push(targetSetCoords); + lifecycle.push(targetInvalidateCoords); }); const targetSetPositionByOrigin = jest .spyOn(target, 'setPositionByOrigin') @@ -386,7 +386,7 @@ describe('Layout Manager', () => { }, targetMocks: { set: targetSet, - setCoords: targetSetCoords, + invalidateCoords: targetInvalidateCoords, setPositionByOrigin: targetSetPositionByOrigin, }, mocks: { @@ -450,7 +450,7 @@ describe('Layout Manager', () => { targetMocks.set, layoutObjects, targetMocks.setPositionByOrigin, - targetMocks.setCoords, + targetMocks.invalidateCoords, targetMocks.set, ]); expect(targetMocks.set).nthCalledWith(1, { width, height }); diff --git a/src/LayoutManager/LayoutManager.ts b/src/LayoutManager/LayoutManager.ts index 958512fad14..6a8fb66dab7 100644 --- a/src/LayoutManager/LayoutManager.ts +++ b/src/LayoutManager/LayoutManager.ts @@ -250,7 +250,7 @@ export class LayoutManager { } else { target.setPositionByOrigin(nextCenter, CENTER, CENTER); // invalidate - target.setCoords(); + target.invalidateCoords(); target.set('dirty', true); } } diff --git a/src/LayoutManager/LayoutStrategies/utils.ts b/src/LayoutManager/LayoutStrategies/utils.ts index 76fa0697911..058557bf125 100644 --- a/src/LayoutManager/LayoutStrategies/utils.ts +++ b/src/LayoutManager/LayoutStrategies/utils.ts @@ -1,29 +1,19 @@ -import { Point, ZERO } from '../../Point'; +import { Point } from '../../Point'; import type { Group } from '../../shapes/Group'; import type { FabricObject } from '../../shapes/Object/FabricObject'; -import { multiplyTransformMatrixArray } from '../../util/misc/matrix'; -import { sizeAfterTransform } from '../../util/misc/objectTransforms'; -import { - calcPlaneChangeMatrix, - sendVectorToPlane, -} from '../../util/misc/planeChange'; +import { calcPlaneChangeMatrix } from '../../util/misc/planeChange'; /** * @returns 2 points, the tl and br corners of the non rotated bounding box of an object * in the {@link group} plane, taking into account objects that {@link group} is their parent * but also belong to the active selection. + * @TODO revisit as part of redoing coords */ export const getObjectBounds = ( destinationGroup: Group, object: FabricObject ): Point[] => { - const { - strokeUniform, - strokeWidth, - width, - height, - group: currentGroup, - } = object; + const { group: currentGroup } = object; const t = currentGroup && currentGroup !== destinationGroup ? calcPlaneChangeMatrix( @@ -32,25 +22,11 @@ export const getObjectBounds = ( ) : null; const objectCenter = t - ? object.getRelativeCenterPoint().transform(t) + ? object.getRelativeCenterPoint() : object.getRelativeCenterPoint(); - const accountForStroke = !object['isStrokeAccountedForInDimensions'](); - const strokeUniformVector = - strokeUniform && accountForStroke - ? sendVectorToPlane( - new Point(strokeWidth, strokeWidth), - undefined, - destinationGroup.calcTransformMatrix() - ) - : ZERO; - const scalingStrokeWidth = - !strokeUniform && accountForStroke ? strokeWidth : 0; - const sizeVector = sizeAfterTransform( - width + scalingStrokeWidth, - height + scalingStrokeWidth, - multiplyTransformMatrixArray([t, object.calcOwnMatrix()], true) - ) - .add(strokeUniformVector) - .scalarDivide(2); - return [objectCenter.subtract(sizeVector), objectCenter.add(sizeVector)]; + const sizeVector = object.bbox.sendToParent().getBBoxVector().scalarDivide(2); + + const a = objectCenter.subtract(sizeVector); + const b = objectCenter.add(sizeVector); + return t ? [a.transform(t), b.transform(t)] : [a, b]; }; diff --git a/src/brushes/PatternBrush.ts b/src/brushes/PatternBrush.ts index 8160953104d..d45a7204248 100644 --- a/src/brushes/PatternBrush.ts +++ b/src/brushes/PatternBrush.ts @@ -1,4 +1,4 @@ -import { Pattern } from '../Pattern'; +import { Pattern } from '../Pattern/Pattern'; import { createCanvasElement } from '../util/misc/dom'; import type { Canvas } from '../canvas/Canvas'; import { PencilBrush } from './PencilBrush'; @@ -58,7 +58,7 @@ export class PatternBrush extends PencilBrush { */ createPath(pathData: TSimplePathData) { const path = super.createPath(pathData), - topLeft = path._getLeftTopCoords().scalarAdd(path.strokeWidth / 2); + topLeft = path.getXY('left', 'top'); path.stroke = new Pattern({ source: this.source || this.getPatternSrc(), diff --git a/src/brushes/PencilBrush.ts b/src/brushes/PencilBrush.ts index a18f8410fe4..d8e2b16fa3c 100644 --- a/src/brushes/PencilBrush.ts +++ b/src/brushes/PencilBrush.ts @@ -288,13 +288,12 @@ export class PencilBrush extends BaseBrush { const path = this.createPath(pathData); this.canvas.clearContext(this.canvas.contextTop); - this.canvas.fire('before:path:created', { path: path }); + this.canvas.fire('before:path:created', { path }); this.canvas.add(path); this.canvas.requestRenderAll(); - path.setCoords(); this._resetShadow(); // fire event 'path' created - this.canvas.fire('path:created', { path: path }); + this.canvas.fire('path:created', { path }); } } diff --git a/src/canvas/Canvas.ts b/src/canvas/Canvas.ts index db1cbb07706..e9f9e840fab 100644 --- a/src/canvas/Canvas.ts +++ b/src/canvas/Canvas.ts @@ -1049,14 +1049,11 @@ export class Canvas extends SelectableCanvas implements CanvasOptions { if (target.selectable && target.activeOn === 'down') { this.setActiveObject(target, e); } - const handle = target.findControl( - this.getViewportPoint(e), - isTouchEvent(e) - ); + const viewportPoint = this.getViewportPoint(e); + const handle = target.findControl(viewportPoint, isTouchEvent(e)); if (target === this._activeObject && (handle || !grouped)) { this._setupCurrentTransform(e, target, alreadySelected); const control = handle ? handle.control : undefined, - pointer = this.getScenePoint(e), mouseDownHandler = control && control.getMouseDownHandler(e, target, control); mouseDownHandler && @@ -1064,8 +1061,8 @@ export class Canvas extends SelectableCanvas implements CanvasOptions { control, e, this._currentTransform!, - pointer.x, - pointer.y + viewportPoint.x, + viewportPoint.y ); } } @@ -1133,7 +1130,7 @@ export class Canvas extends SelectableCanvas implements CanvasOptions { // We initially clicked in an empty area, so we draw a box for multiple selection if (groupSelector) { - const pointer = this.getScenePoint(e); + const pointer = this.getViewportPoint(e); groupSelector.deltaX = pointer.x - groupSelector.x; groupSelector.deltaY = pointer.y - groupSelector.y; @@ -1277,48 +1274,33 @@ export class Canvas extends SelectableCanvas implements CanvasOptions { * @param {Event} e Event fired on mousemove */ _transformObject(e: TPointerEvent) { - const scenePoint = this.getScenePoint(e), - transform = this._currentTransform!, - target = transform.target, - // transform pointer to target's containing coordinate plane - // both pointer and object should agree on every point - localPointer = target.group - ? sendPointToPlane( - scenePoint, - undefined, - target.group.calcTransformMatrix() - ) - : scenePoint; - transform.shiftKey = e.shiftKey; - transform.altKey = !!this.centeredKey && e[this.centeredKey]; - - this._performTransformAction(e, transform, localPointer); - transform.actionPerformed && this.requestRenderAll(); + this._performTransformAction( + e, + Object.assign(this._currentTransform!, { + shiftKey: e.shiftKey, + altKey: !!this.centeredKey && e[this.centeredKey], + }) + ) && this.requestRenderAll(); } /** * @private */ - _performTransformAction( - e: TPointerEvent, - transform: Transform, - pointer: Point - ) { - const x = pointer.x, - y = pointer.y, - action = transform.action, - actionHandler = transform.actionHandler; + _performTransformAction(e: TPointerEvent, transform: Transform) { + const { target, action, actionHandler } = transform; let actionPerformed = false; - // this object could be created from the function in the control handlers - if (actionHandler) { - actionPerformed = actionHandler(e, transform, x, y); + const pointer = this.getViewportPoint(e); + actionPerformed = actionHandler(e, transform, pointer.x, pointer.y); + transform.lastX = pointer.x; + transform.lastY = pointer.y; } if (action === 'drag' && actionPerformed) { - transform.target.isMoving = true; - this.setCursor(transform.target.moveCursor || this.moveCursor); + target.isMoving = true; + this.setCursor(target.moveCursor || this.moveCursor); } - transform.actionPerformed = transform.actionPerformed || actionPerformed; + return (transform.actionPerformed = + transform.actionPerformed || actionPerformed); } /** @@ -1332,7 +1314,6 @@ export class Canvas extends SelectableCanvas implements CanvasOptions { this.setCursor(this.defaultCursor); return; } - let hoverCursor = target.hoverCursor || this.hoverCursor; const activeSelection = isActiveSelection(this._activeObject) ? this._activeObject : null, @@ -1345,17 +1326,13 @@ export class Canvas extends SelectableCanvas implements CanvasOptions { target.findControl(this.getViewportPoint(e)); if (!corner) { - if ((target as Group).subTargetCheck) { - // hoverCursor should come from top-most subTarget, - // so we walk the array backwards - this.targets - .concat() - .reverse() - .map((_target) => { - hoverCursor = _target.hoverCursor || hoverCursor; - }); - } - this.setCursor(hoverCursor); + // hoverCursor should come from top-most subTarget + const subTargetHoverCursor = + (target as Group).subTargetCheck && + this.targets.find((target) => target.hoverCursor)?.hoverCursor; + this.setCursor( + subTargetHoverCursor || target.hoverCursor || this.hoverCursor + ); } else { const control = corner.control; this.setCursor(control.cursorStyleHandler(e, control, target)); diff --git a/src/canvas/SelectableCanvas.spec.ts b/src/canvas/SelectableCanvas.spec.ts index 5c1f2578903..b5ab30de971 100644 --- a/src/canvas/SelectableCanvas.spec.ts +++ b/src/canvas/SelectableCanvas.spec.ts @@ -465,7 +465,7 @@ describe('Selectable Canvas', () => { const { corner: { tl, tr, bl }, - } = object.oCoords[controlKey]; + } = object.getControlCoords()[controlKey]; canvas.getSelectionElement().dispatchEvent( new MouseEvent('mousedown', { clientX: canvasOffset.left + (tl.x + tr.x) / 2, diff --git a/src/canvas/SelectableCanvas.ts b/src/canvas/SelectableCanvas.ts index 7a3fce244d9..2a808610941 100644 --- a/src/canvas/SelectableCanvas.ts +++ b/src/canvas/SelectableCanvas.ts @@ -9,22 +9,12 @@ import type { TPointerEvent, Transform, } from '../EventTypeDefs'; -import { - addTransformToObject, - saveObjectTransform, -} from '../util/misc/objectTransforms'; +import { addTransformToObject } from '../util/misc/objectTransforms'; import type { TCanvasSizeOptions } from './StaticCanvas'; import { StaticCanvas } from './StaticCanvas'; import { isCollection } from '../Collection'; import { isTransparent } from '../util/misc/isTransparent'; -import type { - TMat2D, - TOriginX, - TOriginY, - TSize, - TSVGReviver, -} from '../typedefs'; -import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; +import type { TMat2D, TSize, TSVGReviver } from '../typedefs'; import { getPointer, isTouchEvent } from '../util/dom_event'; import type { IText } from '../shapes/IText/IText'; import type { BaseBrush } from '../brushes/BaseBrush'; @@ -32,11 +22,12 @@ import { pick } from '../util/misc/pick'; import { sendPointToPlane } from '../util/misc/planeChange'; import { cos, createCanvasElement, sin } from '../util'; import { CanvasDOMManager } from './DOMManagers/CanvasDOMManager'; -import { BOTTOM, CENTER, LEFT, RIGHT, TOP } from '../constants'; +import { BOTTOM, LEFT, RIGHT, TOP } from '../constants'; import type { CanvasOptions } from './CanvasOptions'; import { canvasDefaults } from './CanvasOptions'; import { Intersection } from '../Intersection'; import { isActiveSelection } from '../util/typeAssertions'; +import { calcPlaneRotation } from '../util/misc/matrix'; /** * Canvas class @@ -346,9 +337,10 @@ export class SelectableCanvas _chooseObjectsToRender(): FabricObject[] { const activeObject = this._activeObject; return !this.preserveObjectStacking && activeObject - ? this._objects - .filter((object) => !object.group && object !== activeObject) - .concat(activeObject) + ? (!activeObject.group + ? this._objects.filter((object) => object !== activeObject) + : this._objects + ).concat(activeObject) : this._objects; } @@ -494,7 +486,7 @@ export class SelectableCanvas /** * This method will take in consideration a modifier key pressed and the control we are - * about to drag, and try to guess the anchor point ( origin ) of the transormation. + * about to drag, and try to guess the anchor point ( origin ) of the transformation. * This should be really in the realm of controls, and we should remove specific code for legacy * embedded actions. * @TODO this probably deserve discussion/rediscovery and change/refactor @@ -530,43 +522,6 @@ export class SelectableCanvas return centerTransform ? !modifierKeyPressed : modifierKeyPressed; } - /** - * Given the control clicked, determine the origin of the transform. - * This is bad because controls can totally have custom names - * should disappear before release 4.0 - * @private - * @deprecated - */ - _getOriginFromCorner( - target: FabricObject, - controlName: string - ): { x: TOriginX; y: TOriginY } { - const origin = { - x: target.originX, - y: target.originY, - }; - - if (!controlName) { - return origin; - } - - // is a left control ? - if (['ml', 'tl', 'bl'].includes(controlName)) { - origin.x = RIGHT; - // is a right control ? - } else if (['mr', 'tr', 'br'].includes(controlName)) { - origin.x = LEFT; - } - // is a top control ? - if (['tl', 'mt', 'tr'].includes(controlName)) { - origin.y = BOTTOM; - // is a bottom control ? - } else if (['bl', 'mb', 'br'].includes(controlName)) { - origin.y = TOP; - } - return origin; - } - /** * @private * @param {Event} e Event object @@ -578,14 +533,7 @@ export class SelectableCanvas target: FabricObject, alreadySelected: boolean ): void { - const pointer = target.group - ? // transform pointer to target's containing coordinate plane - sendPointToPlane( - this.getScenePoint(e), - undefined, - target.group.calcTransformMatrix() - ) - : this.getScenePoint(e); + const pointer = this.getViewportPoint(e); const { key: corner = '', control } = target.getActiveControl() || {}, actionHandler = alreadySelected && control @@ -593,12 +541,12 @@ export class SelectableCanvas : dragHandler, action = getActionFromCorner(alreadySelected, corner, e, target), altKey = e[this.centeredKey as ModifierKey], - origin = this._shouldCenterTransform(target, action, altKey) - ? ({ x: CENTER, y: CENTER } as const) - : this._getOriginFromCorner(target, corner), + origin = ( + control ? new Point(-control.x, -control.y) : new Point() + ).scalarAdd(0.5), + offset = pointer.subtract(target.getXY('left', 'top')), /** - * relative to target's containing coordinate plane - * both agree on every point + * relative to viewport **/ transform: Transform = { target: target, @@ -606,28 +554,15 @@ export class SelectableCanvas actionHandler, actionPerformed: false, corner, - scaleX: target.scaleX, - scaleY: target.scaleY, - skewX: target.skewX, - skewY: target.skewY, - offsetX: pointer.x - target.left, - offsetY: pointer.y - target.top, originX: origin.x, originY: origin.y, ex: pointer.x, ey: pointer.y, lastX: pointer.x, lastY: pointer.y, - theta: degreesToRadians(target.angle), - width: target.width, - height: target.height, + theta: calcPlaneRotation(target.calcTransformMatrixInViewport()), shiftKey: e.shiftKey, altKey, - original: { - ...saveObjectTransform(target), - originX: origin.x, - originY: origin.y, - }, }; this._currentTransform = transform; @@ -653,10 +588,8 @@ export class SelectableCanvas */ _drawSelection(ctx: CanvasRenderingContext2D): void { const { x, y, deltaX, deltaY } = this._groupSelector!, - start = new Point(x, y).transform(this.viewportTransform), - extent = new Point(x + deltaX, y + deltaY).transform( - this.viewportTransform - ), + start = new Point(x, y), + extent = new Point(x + deltaX, y + deltaY), strokeOffset = this.selectionLineWidth / 2; let minX = Math.min(start.x, extent.x), minY = Math.min(start.y, extent.y), @@ -748,11 +681,12 @@ export class SelectableCanvas /** * Checks if the point is inside the object selection area including padding * @param {FabricObject} obj Object to test against - * @param {Object} [pointer] point in scene coordinates + * @param {Point} [scenePoint] point in scene coordinates * @return {Boolean} true if point is contained within an area of given object * @private + * @TODO revisit this ugly impl */ - private _pointIsInObjectSelectionArea(obj: FabricObject, point: Point) { + private _pointIsInObjectSelectionArea(obj: FabricObject, scenePoint: Point) { // getCoords will already take care of group de-nesting let coords = obj.getCoords(); const viewportZoom = this.getZoom(); @@ -783,7 +717,7 @@ export class SelectableCanvas // the idea behind this is that outside target check we don't need ot know // where those coords are } - return Intersection.isPointInPolygon(point, coords); + return Intersection.isPointInPolygon(scenePoint, coords); } /** @@ -1148,9 +1082,10 @@ export class SelectableCanvas if (isActiveSelection(object) && prevActiveObject !== object) { object.set('canvas', this); - object.setCoords(); } + object.invalidateCoords(); + return true; } @@ -1237,7 +1172,7 @@ export class SelectableCanvas target._scaling = false; } - target.setCoords(); + target.invalidateCoords(); if (transform.actionPerformed) { this.fire('object:modified', options); @@ -1253,7 +1188,7 @@ export class SelectableCanvas super.setViewportTransform(vpt); const activeObject = this._activeObject; if (activeObject) { - activeObject.setCoords(); + activeObject.invalidateCoords(); } } diff --git a/src/canvas/StaticCanvas.ts b/src/canvas/StaticCanvas.ts index 7ca095dbbdb..e0a3dd823a3 100644 --- a/src/canvas/StaticCanvas.ts +++ b/src/canvas/StaticCanvas.ts @@ -44,6 +44,7 @@ import type { StaticCanvasOptions } from './StaticCanvasOptions'; import { staticCanvasDefaults } from './StaticCanvasOptions'; import { log, FabricError } from '../util/internals/console'; import { getDevicePixelRatio } from '../env'; +import { CanvasBBox } from '../shapes/Object/BBox'; /** * Having both options in TCanvasSizeOptions set to true transform the call in a calcOffset @@ -189,7 +190,6 @@ export class StaticCanvas< height: this.height || this.elements.lower.el.height || 0, }); this.viewportTransform = [...this.viewportTransform]; - this.calcViewportBoundaries(); } protected initElements(el?: string | HTMLCanvasElement) { @@ -224,7 +224,7 @@ export class StaticCanvas< obj.canvas.remove(obj); } obj._set('canvas', this); - obj.setCoords(); + obj.invalidateCoords(); this.fire('object:added', { target: obj }); obj.fire('added', { target: this }); } @@ -379,22 +379,7 @@ export class StaticCanvas< * @param {Array} vpt a Canvas 2D API transform matrix */ setViewportTransform(vpt: TMat2D) { - const backgroundObject = this.backgroundImage, - overlayObject = this.overlayImage, - len = this._objects.length; - - this.viewportTransform = vpt; - for (let i = 0; i < len; i++) { - const object = this._objects[i]; - object.group || object.setCoords(); - } - if (backgroundObject) { - backgroundObject.setCoords(); - } - if (overlayObject) { - overlayObject.setCoords(); - } - this.calcViewportBoundaries(); + this.viewportTransform = [...vpt]; this.renderOnAddRemove && this.requestRenderAll(); } @@ -525,25 +510,25 @@ export class StaticCanvas< } /** - * Calculate the position of the 4 corner of canvas with current viewportTransform. - * helps to determinate when an object is in the current rendering viewport + * Describe the visible bounding box of the canvas + * if canvas is **NOT** transformed the points are equal to the four corners of the `HTMLCanvasElement` + * if canvas is transformed the points describe the distance from canvas origin, + * `tl` being the viewport origin which is the `tl` corner of the `HTMLCanvasElement`. */ - calcViewportBoundaries(): TCornerPoint { - const width = this.width, - height = this.height, - iVpt = invertTransform(this.viewportTransform), - a = transformPoint({ x: 0, y: 0 }, iVpt), - b = transformPoint({ x: width, y: height }, iVpt), - // we don't support vpt flipping - // but the code is robust enough to mostly work with flipping + getViewportBBox() { + // we don't support vpt flipping + // but the code is robust enough to mostly work with flipping + const iVpt = invertTransform(this.viewportTransform), + a = new Point().transform(iVpt), + b = new Point(this.width, this.height).transform(iVpt), min = a.min(b), max = a.max(b); - return (this.vptCoords = { + return { tl: min, tr: new Point(max.x, min.y), bl: new Point(min.x, max.y), br: max, - }); + }; } cancelRequestedRender() { @@ -569,7 +554,6 @@ export class StaticCanvas< const v = this.viewportTransform, path = this.clipPath; - this.calcViewportBoundaries(); this.clearContext(ctx); ctx.imageSmoothingEnabled = this.imageSmoothingEnabled; // @ts-expect-error node-canvas stuff @@ -635,9 +619,13 @@ export class StaticCanvas< * @param {Array} objects to render */ _renderObjects(ctx: CanvasRenderingContext2D, objects: FabricObject[]) { - for (let i = 0, len = objects.length; i < len; ++i) { - objects[i] && objects[i].render(ctx); - } + const canvasBBox = CanvasBBox.bbox(this); + objects.forEach((object) => { + object && + // @TODO: change + (!this.skipOffscreen || canvasBBox.overlaps(object.bbox)) && + object.render(ctx); + }); } /** @@ -680,15 +668,8 @@ export class StaticCanvas< } if (object) { ctx.save(); - const { skipOffscreen } = this; - // if the object doesn't move with the viewport, - // the offscreen concept does not apply; - this.skipOffscreen = needsVpt; - if (needsVpt) { - ctx.transform(...v); - } + needsVpt && ctx.transform(...v); object.render(ctx); - this.skipOffscreen = skipOffscreen; ctx.restore(); } } @@ -807,7 +788,7 @@ export class StaticCanvas< */ _centerObject(object: FabricObject, center: Point) { object.setXY(center, CENTER, CENTER); - object.setCoords(); + object.invalidateCoords(); this.renderOnAddRemove && this.requestRenderAll(); } @@ -1437,12 +1418,10 @@ export class StaticCanvas< this.viewportTransform = newVp; this.width = scaledWidth; this.height = scaledHeight; - this.calcViewportBoundaries(); this.renderCanvas(canvasEl.getContext('2d')!, objectsToRender); this.viewportTransform = vp; this.width = originalWidth; this.height = originalHeight; - this.calcViewportBoundaries(); this.enableRetinaScaling = originalRetina; return canvasEl; } diff --git a/src/canvas/StaticCanvasOptions.ts b/src/canvas/StaticCanvasOptions.ts index 032e735ac7b..5fa3019e3f9 100644 --- a/src/canvas/StaticCanvasOptions.ts +++ b/src/canvas/StaticCanvasOptions.ts @@ -72,7 +72,7 @@ interface CanvasRenderingOptions { renderOnAddRemove: boolean; /** - * Based on vptCoords and object.aCoords, skip rendering of objects that + * Based on vptCoords and object.bboxCoords, skip rendering of objects that * are not included in current viewport. * May greatly help in applications with crowded canvas and use of zoom/pan * If One of the corner of the bounding box of the object is on the canvas diff --git a/src/canvas/__tests__/eventData.test.ts b/src/canvas/__tests__/eventData.test.ts index 3d32723cdc6..54999d545c1 100644 --- a/src/canvas/__tests__/eventData.test.ts +++ b/src/canvas/__tests__/eventData.test.ts @@ -476,7 +476,7 @@ describe('Event targets', () => { }); group.subTargetCheck = true; - group.setCoords(); + group.invalidateCoords(); expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ target: group, diff --git a/src/constants.ts b/src/constants.ts index c408620a224..6d236fa9531 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -6,6 +6,7 @@ export const VERSION = version; // eslint-disable-next-line @typescript-eslint/no-empty-function export function noop() {} +export const PIBy4 = Math.PI / 4; export const halfPI = Math.PI / 2; export const twoMathPi = Math.PI * 2; export const PiBy180 = Math.PI / 180; diff --git a/src/controls/Control.spec.ts b/src/controls/Control.spec.ts index 7749c07f527..c01f6d86ebe 100644 --- a/src/controls/Control.spec.ts +++ b/src/controls/Control.spec.ts @@ -1,3 +1,4 @@ +import { Point } from '../Point'; import { Canvas } from '../canvas/Canvas'; import { FabricObject } from '../shapes/Object/FabricObject'; import { Control } from './Control'; @@ -19,14 +20,31 @@ describe('Controls', () => { canvas: new Canvas(), }); - target.setCoords(); + target.invalidateCoords(); jest .spyOn(target, 'findControl') .mockImplementation(function (this: FabricObject) { this.__corner = 'test'; - return { key: 'test', control }; + return { + key: 'test', + control, + coord: Object.assign(new Point(), { + corner: { + tl: new Point(), + tr: new Point(), + br: new Point(), + bl: new Point(), + }, + touchCorner: { + tl: new Point(), + tr: new Point(), + br: new Point(), + bl: new Point(), + }, + }), + }; }); const canvas = new Canvas(); diff --git a/src/controls/Control.ts b/src/controls/Control.ts index 98091469cf7..744649fa893 100644 --- a/src/controls/Control.ts +++ b/src/controls/Control.ts @@ -6,8 +6,12 @@ import type { } from '../EventTypeDefs'; import { Intersection } from '../Intersection'; import { Point } from '../Point'; -import type { InteractiveFabricObject } from '../shapes/Object/InteractiveObject'; +import type { + InteractiveFabricObject, + TControlCoord, +} from '../shapes/Object/InteractiveObject'; import type { TCornerPoint, TDegree, TMat2D } from '../typedefs'; +import type { FabricObject } from '../shapes/Object/Object'; import { createRotateMatrix, createScaleMatrix, @@ -195,7 +199,7 @@ export class Control { */ getActionHandler( eventData: TPointerEvent, - fabricObject: InteractiveFabricObject, + fabricObject: FabricObject, control: Control ): TransformActionHandler | undefined { return this.actionHandler; @@ -210,7 +214,7 @@ export class Control { */ getMouseDownHandler( eventData: TPointerEvent, - fabricObject: InteractiveFabricObject, + fabricObject: FabricObject, control: Control ): ControlActionHandler | undefined { return this.mouseDownHandler; @@ -226,7 +230,7 @@ export class Control { */ getMouseUpHandler( eventData: TPointerEvent, - fabricObject: InteractiveFabricObject, + fabricObject: FabricObject, control: Control ): ControlActionHandler | undefined { return this.mouseUpHandler; @@ -244,7 +248,7 @@ export class Control { cursorStyleHandler( eventData: TPointerEvent, control: Control, - fabricObject: InteractiveFabricObject + fabricObject: FabricObject ) { return control.cursorStyle; } @@ -259,7 +263,7 @@ export class Control { getActionName( eventData: TPointerEvent, control: Control, - fabricObject: InteractiveFabricObject + fabricObject: FabricObject ) { return control.actionName; } @@ -270,7 +274,7 @@ export class Control { * @param {String} controlKey key where the control is memorized on the * @return {Boolean} */ - getVisibility(fabricObject: InteractiveFabricObject, controlKey: string) { + getVisibility(fabricObject: FabricObject, controlKey: string) { return fabricObject._controlsVisibility?.[controlKey] ?? this.visible; } @@ -279,24 +283,49 @@ export class Control { * @param {Boolean} visibility for the object * @return {Void} */ - setVisibility( - visibility: boolean, - name: string, - fabricObject: InteractiveFabricObject - ) { + setVisibility(visibility: boolean, name: string, fabricObject: FabricObject) { this.visible = visibility; } positionHandler( dim: Point, finalMatrix: TMat2D, - fabricObject: InteractiveFabricObject, + fabricObject: FabricObject, currentControl: Control ) { - return new Point( - this.x * dim.x + this.offsetX, - this.y * dim.y + this.offsetY - ).transform(finalMatrix); + // // legacy + // return new Point( + // this.x * dim.x + this.offsetX, + // this.y * dim.y + this.offsetY + // ).transform(finalMatrix); + + const bbox = fabricObject.bbox; + const rotation = bbox.getRotation(); + return new Point(this.x, this.y) + .transform(bbox.getTransformation()) + .add(new Point(this.offsetX, 0).rotate(rotation.x)) + .add(new Point(0, this.offsetY).rotate(rotation.y)); + } + + /** + * + * @param position control center, result of {@link positionHandler} + * @param dim + * @param finalMatrix + * @param fabricObject + * @param currentControl + * @returns + */ + connectionPositionHandler( + position: Point, + fabricObject: FabricObject, + currentControl: Control + ) { + const bbox = fabricObject.bbox; + return { + from: new Point(this.x, this.y).transform(bbox.getTransformation()), + to: position, + }; } /** @@ -332,46 +361,80 @@ export class Control { }; } + /** + * Override to customize connection line rendering + * @param ctx + * @param from the value returned from {@link connectionPositionHandler} + * @param to the control + * @param styleOverride + * @param fabricObject + */ + renderConnection( + ctx: CanvasRenderingContext2D, + { from, to }: { from: Point; to: Point }, + styleOverride: Pick< + ControlRenderingStyleOverride, + 'borderColor' | 'borderDashArray' + > = {}, + fabricObject: FabricObject + ) { + ctx.save(); + ctx.strokeStyle = styleOverride.borderColor || fabricObject.borderColor; + fabricObject._setLineDash( + ctx, + styleOverride.borderDashArray || fabricObject.borderDashArray + ); + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.closePath(); + ctx.stroke(); + ctx.restore(); + } + /** * Render function for the control. * When this function runs the context is unscaled. unrotate. Just retina scaled. - * all the functions will have to translate to the point left,top before starting Drawing + * all the functions will have to translate to the control center ({@link x}, {@link y}) before starting drawing * if they want to draw a control where the position is detected. - * left and top are the result of the positionHandler function + * @see {@link renderControl} for customization of rendering * @param {RenderingContext2D} ctx the context where the control will be drawn - * @param {Number} left position of the canvas where we are about to render the control. - * @param {Number} top position of the canvas where we are about to render the control. + * @param {number} x control center x, result of {@link positionHandler} + * @param {number} y control center y, result of {@link positionHandler} * @param {Object} styleOverride * @param {FabricObject} fabricObject the object where the control is about to be rendered */ render( ctx: CanvasRenderingContext2D, - left: number, - top: number, - styleOverride: ControlRenderingStyleOverride | undefined, - fabricObject: InteractiveFabricObject + x: number, + y: number, + styleOverride: ControlRenderingStyleOverride = {}, + fabricObject: FabricObject ) { - styleOverride = styleOverride || {}; switch (styleOverride.cornerStyle || fabricObject.cornerStyle) { case 'circle': - renderCircleControl.call( - this, - ctx, - left, - top, - styleOverride, - fabricObject - ); + renderCircleControl.call(this, ctx, x, y, styleOverride, fabricObject); break; default: - renderSquareControl.call( - this, - ctx, - left, - top, - styleOverride, - fabricObject - ); + renderSquareControl.call(this, ctx, x, y, styleOverride, fabricObject); } } + + /** + * In charge of rendering all control visuals + * @param {RenderingContext2D} ctx the retina scaled context where the control will be drawn + * @param {Point} position coordinate where the control center should be, returned by {@link positionHandler} + * @param {Object} styleOverride + * @param {FabricObject} fabricObject the object where the control is about to be rendered + */ + renderControl( + ctx: CanvasRenderingContext2D, + { position, connection }: TControlCoord, + styleOverride: ControlRenderingStyleOverride = {}, + fabricObject: FabricObject + ) { + this.withConnection && + this.renderConnection(ctx, connection, styleOverride, fabricObject); + this.render(ctx, position.x, position.y, styleOverride, fabricObject); + } } diff --git a/src/controls/changeWidth.ts b/src/controls/changeWidth.ts deleted file mode 100644 index 37d52c9ece2..00000000000 --- a/src/controls/changeWidth.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { TransformActionHandler } from '../EventTypeDefs'; -import { CENTER, LEFT, RIGHT } from '../constants'; -import { getLocalPoint, isTransformCentered } from './util'; -import { wrapWithFireEvent } from './wrapWithFireEvent'; -import { wrapWithFixedAnchor } from './wrapWithFixedAnchor'; - -/** - * Action handler to change object's width - * Needs to be wrapped with `wrapWithFixedAnchor` to be effective - * @param {Event} eventData javascript event that is doing the transform - * @param {Object} transform javascript object containing a series of information around the current transform - * @param {number} x current mouse x position, canvas normalized - * @param {number} y current mouse y position, canvas normalized - * @return {Boolean} true if some change happened - */ -export const changeObjectWidth: TransformActionHandler = ( - eventData, - transform, - x, - y -) => { - const localPoint = getLocalPoint( - transform, - transform.originX, - transform.originY, - x, - y - ); - // make sure the control changes width ONLY from it's side of target - if ( - transform.originX === CENTER || - (transform.originX === RIGHT && localPoint.x < 0) || - (transform.originX === LEFT && localPoint.x > 0) - ) { - const { target } = transform, - strokePadding = - target.strokeWidth / (target.strokeUniform ? target.scaleX : 1), - multiplier = isTransformCentered(transform) ? 2 : 1, - oldWidth = target.width, - newWidth = Math.ceil( - Math.abs((localPoint.x * multiplier) / target.scaleX) - strokePadding - ); - target.set('width', Math.max(newWidth, 0)); - // check against actual target width in case `newWidth` was rejected - return oldWidth !== target.width; - } - return false; -}; - -export const changeWidth = wrapWithFireEvent( - 'resizing', - wrapWithFixedAnchor(changeObjectWidth) -); diff --git a/src/controls/commonControls.ts b/src/controls/commonControls.ts index 2da416e5839..ed17387b315 100644 --- a/src/controls/commonControls.ts +++ b/src/controls/commonControls.ts @@ -1,4 +1,4 @@ -import { changeWidth } from './changeWidth'; +import { changeWidth } from './resize'; import { Control } from './Control'; import { rotationStyleHandler, rotationWithSnapping } from './rotate'; import { scaleCursorStyleHandler, scalingEqually } from './scale'; diff --git a/src/controls/controlRendering.ts b/src/controls/controlRendering.ts index 30e5b0129e2..3777066463e 100644 --- a/src/controls/controlRendering.ts +++ b/src/controls/controlRendering.ts @@ -1,6 +1,7 @@ import { twoMathPi } from '../constants'; import type { InteractiveFabricObject } from '../shapes/Object/InteractiveObject'; -import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; +import { calcPlaneRotation } from '../util/misc/matrix'; +import type { FabricObject } from '../shapes/Object/FabricObject'; import type { Control } from './Control'; export type ControlRenderingStyleOverride = Partial< @@ -12,13 +13,15 @@ export type ControlRenderingStyleOverride = Partial< | 'cornerStrokeColor' | 'cornerDashArray' | 'transparentCorners' + | 'borderColor' + | 'borderDashArray' > >; export type ControlRenderer = ( ctx: CanvasRenderingContext2D, - left: number, - top: number, + x: number, + y: number, styleOverride: ControlRenderingStyleOverride, fabricObject: InteractiveFabricObject ) => void; @@ -29,16 +32,16 @@ export type ControlRenderer = ( * cornerColor, cornerStrokeColor * plus the addition of offsetY and offsetX. * @param {CanvasRenderingContext2D} ctx context to render on - * @param {Number} left x coordinate where the control center should be - * @param {Number} top y coordinate where the control center should be + * @param {number} x control center x + * @param {number} y control center y * @param {Object} styleOverride override for FabricObject controls style * @param {FabricObject} fabricObject the fabric object for which we are rendering controls */ export function renderCircleControl( this: Control, ctx: CanvasRenderingContext2D, - left: number, - top: number, + x: number, + y: number, styleOverride: ControlRenderingStyleOverride, fabricObject: InteractiveFabricObject ) { @@ -54,9 +57,7 @@ export function renderCircleControl( stroke = !transparentCorners && (styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor); - let myLeft = left, - myTop = top, - size; + let size: number; ctx.save(); ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor || ''; ctx.strokeStyle = @@ -65,18 +66,18 @@ export function renderCircleControl( if (xSize > ySize) { size = xSize; ctx.scale(1.0, ySize / xSize); - myTop = (top * xSize) / ySize; + y *= xSize / ySize; } else if (ySize > xSize) { size = ySize; ctx.scale(xSize / ySize, 1.0); - myLeft = (left * ySize) / xSize; + x *= ySize / xSize; } else { size = xSize; } // this is still wrong ctx.lineWidth = 1; ctx.beginPath(); - ctx.arc(myLeft, myTop, size / 2, 0, twoMathPi, false); + ctx.arc(x, y, size / 2, 0, twoMathPi, false); ctx[methodName](); if (stroke) { ctx.stroke(); @@ -90,16 +91,16 @@ export function renderCircleControl( * cornerColor, cornerStrokeColor * plus the addition of offsetY and offsetX. * @param {CanvasRenderingContext2D} ctx context to render on - * @param {Number} left x coordinate where the control center should be - * @param {Number} top y coordinate where the control center should be + * @param {number} x control center x + * @param {number} y control center y * @param {Object} styleOverride override for FabricObject controls style * @param {FabricObject} fabricObject the fabric object for which we are rendering controls */ export function renderSquareControl( this: Control, ctx: CanvasRenderingContext2D, - left: number, - top: number, + x: number, + y: number, styleOverride: ControlRenderingStyleOverride, fabricObject: InteractiveFabricObject ) { @@ -123,10 +124,9 @@ export function renderSquareControl( styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor || ''; // this is still wrong ctx.lineWidth = 1; - ctx.translate(left, top); - // angle is relative to canvas plane - const angle = fabricObject.getTotalAngle(); - ctx.rotate(degreesToRadians(angle)); + ctx.translate(x, y); + // angle is relative to canvas plane - todo should be the viewport! + ctx.rotate(calcPlaneRotation(fabricObject.calcTransformMatrix())); // this does not work, and fixed with ( && ) does not make sense. // to have real transparent corners we need the controls on upperCanvas // transparentCorners || ctx.clearRect(-xSizeBy2, -ySizeBy2, xSize, ySize); diff --git a/src/controls/drag.ts b/src/controls/drag.ts index f820927b8a4..9d481d8226b 100644 --- a/src/controls/drag.ts +++ b/src/controls/drag.ts @@ -1,5 +1,4 @@ import type { TransformActionHandler } from '../EventTypeDefs'; -import { LEFT, TOP } from '../constants'; import { fireEvent } from './fireEvent'; import { commonEventInfo, isLocked } from './util'; @@ -18,15 +17,16 @@ export const dragHandler: TransformActionHandler = ( x, y ) => { - const { target, offsetX, offsetY } = transform, - newLeft = x - offsetX, - newTop = y - offsetY, - moveX = !isLocked(target, 'lockMovementX') && target.left !== newLeft, - moveY = !isLocked(target, 'lockMovementY') && target.top !== newTop; - moveX && target.set(LEFT, newLeft); - moveY && target.set(TOP, newTop); - if (moveX || moveY) { + const { target, lastX, lastY } = transform; + if ( + target.translate( + !isLocked(target, 'lockMovementX') ? x - lastX : 0, + !isLocked(target, 'lockMovementY') ? y - lastY : 0, + true + ) + ) { fireEvent('moving', commonEventInfo(eventData, transform, x, y)); + return true; } - return moveX || moveY; + return false; }; diff --git a/src/controls/index.ts b/src/controls/index.ts index 77d1256bffd..28ea9738323 100644 --- a/src/controls/index.ts +++ b/src/controls/index.ts @@ -1,4 +1,4 @@ -export { changeWidth } from './changeWidth'; +export { changeWidth, changeHeight } from './resize'; export { renderCircleControl, renderSquareControl } from './controlRendering'; export * from './commonControls'; export { dragHandler } from './drag'; @@ -17,6 +17,5 @@ export { scalingYOrSkewingX, } from './scaleSkew'; export { skewCursorStyleHandler, skewHandlerX, skewHandlerY } from './skew'; -export { getLocalPoint } from './util'; export { wrapWithFireEvent } from './wrapWithFireEvent'; export { wrapWithFixedAnchor } from './wrapWithFixedAnchor'; diff --git a/src/controls/polyControl.ts b/src/controls/polyControl.ts index 40a37baa275..d907f9e4f53 100644 --- a/src/controls/polyControl.ts +++ b/src/controls/polyControl.ts @@ -2,7 +2,6 @@ import { Point } from '../Point'; import { Control } from './Control'; import type { TMat2D } from '../typedefs'; import type { Polyline } from '../shapes/Polyline'; -import { multiplyTransformMatrices } from '../util/misc/matrix'; import type { TModificationEvents, TPointerEvent, @@ -20,17 +19,16 @@ type TTransformAnchor = Transform & { pointIndex: number }; * This function locates the controls. * It'll be used both for drawing and for interaction. */ -export const createPolyPositionHandler = (pointIndex: number) => { - return function (dim: Point, finalMatrix: TMat2D, polyObject: Polyline) { - const { points, pathOffset } = polyObject; - return new Point(points[pointIndex]) - .subtract(pathOffset) - .transform( - multiplyTransformMatrices( - polyObject.getViewportTransform(), - polyObject.calcTransformMatrix() - ) - ); +const factoryPolyPositionHandler = (pointIndex: number) => { + return function ( + dim: Point, + finalMatrix: TMat2D, + finalMatrix2: TMat2D, + polyObject: Polyline + ) { + return new Point(polyObject.points[pointIndex]) + .subtract(polyObject.pathOffset) + .transform(polyObject.calcTransformMatrixInViewport()); }; }; @@ -47,15 +45,15 @@ export const polyActionHandler = ( x: number, y: number ) => { - const { target, pointIndex } = transform; - const poly = target as Polyline; - const mouseLocalPosition = sendPointToPlane( - new Point(x, y), - undefined, - poly.calcOwnMatrix() - ); + const poly = transform.target as Polyline, + pointIndex = transform.pointIndex, + positionInPlane = sendPointToPlane( + new Point(x, y), + undefined, + poly.calcTransformMatrixInViewport() + ); - poly.points[pointIndex] = mouseLocalPosition.add(poly.pathOffset); + poly.points[pointIndex] = positionInPlane.add(poly.pathOffset); poly.setDimensions(); return true; @@ -75,21 +73,18 @@ export const factoryPolyActionHandler = ( y: number ) { const poly = transform.target as Polyline, - anchorPoint = new Point( - poly.points[(pointIndex > 0 ? pointIndex : poly.points.length) - 1] - ), - anchorPointInParentPlane = anchorPoint + anchorIndex = (pointIndex > 0 ? pointIndex : poly.points.length) - 1, + originBefore = new Point(poly.points[anchorIndex]) .subtract(poly.pathOffset) - .transform(poly.calcOwnMatrix()), - actionPerformed = fn(eventData, { ...transform, pointIndex }, x, y); + .transform(poly.calcTransformMatrix()); - const newAnchorPointInParentPlane = anchorPoint - .subtract(poly.pathOffset) - .transform(poly.calcOwnMatrix()); + const actionPerformed = fn(eventData, { ...transform, pointIndex }, x, y); - const diff = newAnchorPointInParentPlane.subtract(anchorPointInParentPlane); - poly.left -= diff.x; - poly.top -= diff.y; + const offset = new Point(poly.points[anchorIndex]) + .subtract(poly.pathOffset) + .transform(poly.calcTransformMatrix()) + .subtract(originBefore); + poly.translate(-offset.x, -offset.y, false); return actionPerformed; }; @@ -121,7 +116,7 @@ export function createPolyControls( ) { controls[`p${idx}`] = new Control({ actionName: ACTION_NAME, - positionHandler: createPolyPositionHandler(idx), + positionHandler: factoryPolyPositionHandler(idx), actionHandler: createPolyActionHandler(idx), ...options, }); diff --git a/src/controls/resize.ts b/src/controls/resize.ts new file mode 100644 index 00000000000..9745125eb56 --- /dev/null +++ b/src/controls/resize.ts @@ -0,0 +1,84 @@ +import { BBox } from '../BBox/BBox'; +import type { TPointerEvent, Transform } from '../EventTypeDefs'; +import { Point } from '../Point'; +import type { TAxis } from '../typedefs'; +import { sendVectorToPlane } from '../util/misc/planeChange'; +import { resolveOrigin, resolveOriginPoint } from '../util/misc/resolveOrigin'; +import { dotProduct, magnitude } from '../util/misc/vectors'; +import { wrapWithFireEvent } from './wrapWithFireEvent'; +import { wrapWithFixedAnchor } from './wrapWithFixedAnchor'; + +const UNIT_VECTOR = { + x: new Point(1, 0), + y: new Point(0, 1), +}; + +const AXIS_KEYS = { + x: { size: 'width', origin: 'originX' }, + y: { size: 'height', origin: 'originY' }, +} as const; + +/** + * Action handler to change object's axis size + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {TAxis} axis axis to change + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ +export const resize = ( + axis: TAxis, + eventData: TPointerEvent, + { target, originX, originY }: Transform, + x: number, + y: number +) => { + // offset is measured relative to the control bbox + const offset = target.bbox + .pointToOrigin(new Point(x, y)) + .subtract(resolveOriginPoint(originX, originY)); + const sideVector = UNIT_VECTOR[axis]; + const factor = dotProduct(offset, sideVector); + // sides are measured using the transformed bbox + const viewportSide = BBox.transformed(target).vectorFromOrigin( + sideVector.scalarMultiply(Math.abs(factor)) + ); + const origin = resolveOrigin({ originX, originY }[AXIS_KEYS[axis].origin]); + + // make sure the control changes size from it's side of target + if ( + origin === 0 || + (origin > 0 && factor < 0) || + (origin < 0 && factor > 0) + ) { + const size = sendVectorToPlane( + viewportSide.scalarMultiply( + 1 - + (target.strokeUniform + ? target.strokeWidth / magnitude(viewportSide) + : 0) + ), + undefined, + target.calcTransformMatrixInViewport() + ).scalarSubtract(!target.strokeUniform ? target.strokeWidth : 0); + const sizeKey = AXIS_KEYS[axis].size; + const valueBefore = target[sizeKey]; + target.set(sizeKey, size[axis]); + // check against actual value in case it was rejected by the setter + return valueBefore !== target[sizeKey]; + } + + return false; +}; + +export const changeWidth = wrapWithFireEvent( + 'resizing', + wrapWithFixedAnchor(resize.bind(null, 'x')) +); + +export const changeHeight = wrapWithFireEvent( + 'resizing', + wrapWithFixedAnchor(resize.bind(null, 'y')) +); diff --git a/src/controls/rotate.ts b/src/controls/rotate.ts index 8ed5b1dcef8..64bb081c6d0 100644 --- a/src/controls/rotate.ts +++ b/src/controls/rotate.ts @@ -2,6 +2,8 @@ import type { ControlCursorCallback, TransformActionHandler, } from '../EventTypeDefs'; +import type { TRadian } from '../typedefs'; +import { sendPointToPlane } from '../util/misc/planeChange'; import { radiansToDegrees } from '../util/misc/radiansDegreesConversion'; import { isLocked, NOT_ALLOWED_CURSOR } from './util'; import { wrapWithFireEvent } from './wrapWithFireEvent'; @@ -42,18 +44,16 @@ const rotateObjectWithSnapping: TransformActionHandler = ( x, y ) => { - const pivotPoint = target.translateToOriginPoint( - target.getRelativeCenterPoint(), - originX, - originY - ); - if (isLocked(target, 'lockRotation')) { return false; } - const lastAngle = Math.atan2(ey - pivotPoint.y, ex - pivotPoint.x), - curAngle = Math.atan2(y - pivotPoint.y, x - pivotPoint.x); + const pivotPoint = sendPointToPlane( + target.getXY(originX, originY), + target.getViewportTransform() + ); + const lastAngle: TRadian = Math.atan2(ey - pivotPoint.y, ex - pivotPoint.x), + curAngle: TRadian = Math.atan2(y - pivotPoint.y, x - pivotPoint.x); let angle = radiansToDegrees(curAngle - lastAngle + theta); if (target.snapAngle && target.snapAngle > 0) { @@ -69,16 +69,7 @@ const rotateObjectWithSnapping: TransformActionHandler = ( } } - // normalize angle to positive value - if (angle < 0) { - angle = 360 + angle; - } - angle %= 360; - - const hasRotated = target.angle !== angle; - // TODO: why aren't we using set? - target.angle = angle; - return hasRotated; + return target.rotate(angle, { originX, originY, inViewport: true }); }; export const rotationWithSnapping = wrapWithFireEvent( diff --git a/src/controls/scale.test.ts b/src/controls/scale.test.ts index 99b500ace9b..4be54e0c0bc 100644 --- a/src/controls/scale.test.ts +++ b/src/controls/scale.test.ts @@ -9,7 +9,7 @@ import { scalingX, scalingY } from './scale'; // const createZeroThickRectangleScalingItems = ( rectOptions: { width: number; height: number } & Partial, - usedCorner: keyof Rect['oCoords'], + usedCorner: string, pointDiff: Point ) => { const extraMargin = 100; @@ -38,14 +38,16 @@ const createZeroThickRectangleScalingItems = ( // create mouse event near center of rect, as the 0 size will put it on the middle scaler const canvasOffset = canvas.calcOffset(); + const coord = target.getControlCoords()[usedCorner]; + const mouseDown = new MouseEvent('mousedown', { - clientX: canvasOffset.left + target.oCoords[usedCorner].x, - clientY: canvasOffset.top + target.oCoords[usedCorner].y, + clientX: canvasOffset.left + coord.x, + clientY: canvasOffset.top + coord.y, }); const moveEvent = new MouseEvent('mousemove', { - clientX: canvasOffset.left + target.oCoords[usedCorner].x + pointDiff.x, - clientY: canvasOffset.top + target.oCoords[usedCorner].y + pointDiff.y, + clientX: canvasOffset.left + coord.x + pointDiff.x, + clientY: canvasOffset.top + coord.y + pointDiff.y, }); canvas.setActiveObject(target); diff --git a/src/controls/scale.ts b/src/controls/scale.ts index f4a43993822..e156b054927 100644 --- a/src/controls/scale.ts +++ b/src/controls/scale.ts @@ -9,23 +9,20 @@ import type { TAxis } from '../typedefs'; import type { Canvas } from '../canvas/Canvas'; import { findCornerQuadrant, - getLocalPoint, - invertOrigin, isLocked, isTransformCentered, NOT_ALLOWED_CURSOR, } from './util'; import { wrapWithFireEvent } from './wrapWithFireEvent'; import { wrapWithFixedAnchor } from './wrapWithFixedAnchor'; +import { Point } from '../Point'; +import { resolveOriginPoint } from '../util/misc/resolveOrigin'; +import { dotProduct } from '../util/misc/vectors'; type ScaleTransform = Transform & { gestureScale?: number; - signX?: number; - signY?: number; }; -type ScaleBy = TAxis | 'equally' | '' | undefined; - /** * Inspect event and fabricObject properties to understand if the scaling action * @param {Event} eventData from the user action @@ -53,7 +50,7 @@ export function scaleIsProportional( */ export function scalingIsForbidden( fabricObject: FabricObject, - by: ScaleBy, + by: TAxis | undefined, scaleProportionally: boolean ) { const lockX = isLocked(fabricObject, 'lockScalingX'), @@ -102,7 +99,7 @@ export const scaleCursorStyleHandler: ControlCursorCallback = ( ? 'x' : control.x === 0 && control.y !== 0 ? 'y' - : ''; + : undefined; if (scalingIsForbidden(fabricObject, by, scaleProportionally)) { return NOT_ALLOWED_CURSOR; } @@ -124,96 +121,76 @@ export const scaleCursorStyleHandler: ControlCursorCallback = ( */ function scaleObject( eventData: TPointerEvent, - transform: ScaleTransform, + { target, gestureScale, originX, originY, lastX, lastY }: ScaleTransform, x: number, y: number, - options: { by?: ScaleBy } = {} + { by }: { by?: TAxis } = {} ) { - const target = transform.target, - by = options.by, - scaleProportionally = scaleIsProportional(eventData, target), - forbidScaling = scalingIsForbidden(target, by, scaleProportionally); - let newPoint, scaleX, scaleY, dim, signX, signY; + const scaleProportionally = scaleIsProportional(eventData, target); + const sideVectorX = new Point(1, 0); + const sideVectorY = new Point(0, 1); + let scaleX = 1, + scaleY = 1; - if (forbidScaling) { + if (scalingIsForbidden(target, by, scaleProportionally)) { return false; } - if (transform.gestureScale) { - scaleX = transform.scaleX * transform.gestureScale; - scaleY = transform.scaleY * transform.gestureScale; - } else { - newPoint = getLocalPoint( - transform, - transform.originX, - transform.originY, - x, - y - ); - // use of sign: We use sign to detect change of direction of an action. sign usually change when - // we cross the origin point with the mouse. So a scale flip for example. There is an issue when scaling - // by center and scaling using one middle control ( default: mr, mt, ml, mb), the mouse movement can easily - // cross many time the origin point and flip the object. so we need a way to filter out the noise. - // This ternary here should be ok to filter out X scaling when we want Y only and vice versa. - signX = by !== 'y' ? Math.sign(newPoint.x || transform.signX || 1) : 1; - signY = by !== 'x' ? Math.sign(newPoint.y || transform.signY || 1) : 1; - if (!transform.signX) { - transform.signX = signX; - } - if (!transform.signY) { - transform.signY = signY; - } - if ( - isLocked(target, 'lockScalingFlip') && - (transform.signX !== signX || transform.signY !== signY) - ) { - return false; - } + if (gestureScale) { + scaleX = scaleY = gestureScale; + } else { + const anchorOrigin = resolveOriginPoint(originX, originY); + const offsetFromAnchorOrigin = target.bbox + .pointToOrigin(new Point(x, y)) + .subtract(anchorOrigin); + const prevOffsetFromAnchorOrigin = target.bbox + .pointToOrigin(new Point(lastX, lastY)) + .subtract(anchorOrigin); + // account for scaling origin + const originFactor = new Point( + anchorOrigin.x > 0 ? -1 : 1, + anchorOrigin.y > 0 ? -1 : 1 + ).scalarMultiply(isTransformCentered({ originX, originY }) ? 2 : 1); - dim = target._getTransformedDimensions(); - // missing detection of flip and logic to switch the origin if (scaleProportionally && !by) { - // uniform scaling - const distance = Math.abs(newPoint.x) + Math.abs(newPoint.y), - { original } = transform, - originalDistance = - Math.abs((dim.x * original.scaleX) / target.scaleX) + - Math.abs((dim.y * original.scaleY) / target.scaleY), - scale = distance / originalDistance; - scaleX = original.scaleX * scale; - scaleY = original.scaleY * scale; + // proportional scaling + const scale = + (Math.abs(offsetFromAnchorOrigin.x) + + Math.abs(offsetFromAnchorOrigin.y)) / + (Math.abs(prevOffsetFromAnchorOrigin.x) + + Math.abs(prevOffsetFromAnchorOrigin.y)); + scaleX = + scale * (Math.sign(offsetFromAnchorOrigin.x) || 1) * originFactor.x; + scaleY = + scale * (Math.sign(offsetFromAnchorOrigin.y) || 1) * originFactor.y; } else { - scaleX = Math.abs((newPoint.x * target.scaleX) / dim.x); - scaleY = Math.abs((newPoint.y * target.scaleY) / dim.y); - } - // if we are scaling by center, we need to double the scale - if (isTransformCentered(transform)) { - scaleX *= 2; - scaleY *= 2; + scaleX = + dotProduct(offsetFromAnchorOrigin, sideVectorX) * originFactor.x || 1; + scaleY = + dotProduct(offsetFromAnchorOrigin, sideVectorY) * originFactor.y || 1; } - if (transform.signX !== signX && by !== 'y') { - transform.originX = invertOrigin(transform.originX); - scaleX *= -1; - transform.signX = signX; - } - if (transform.signY !== signY && by !== 'x') { - transform.originY = invertOrigin(transform.originY); - scaleY *= -1; - transform.signY = signY; - } - } - // minScale is taken care of in the setter. - const oldScaleX = target.scaleX, - oldScaleY = target.scaleY; - if (!by) { - !isLocked(target, 'lockScalingX') && target.set('scaleX', scaleX); - !isLocked(target, 'lockScalingY') && target.set('scaleY', scaleY); - } else { - // forbidden cases already handled on top here. - by === 'x' && target.set('scaleX', scaleX); - by === 'y' && target.set('scaleY', scaleY); } - return oldScaleX !== target.scaleX || oldScaleY !== target.scaleY; + + return target.scaleBy( + // minScale is taken care of in the setter. + scaleX && + !isLocked(target, 'lockScalingX') && + (!isLocked(target, 'lockScalingFlip') || scaleX > 0) && + (!by || by === 'x') + ? scaleX + : 1, + scaleY && + !isLocked(target, 'lockScalingY') && + (!isLocked(target, 'lockScalingFlip') || scaleY > 0) && + (!by || by === 'y') + ? scaleY + : 1, + { + originX, + originY, + inViewport: true, + } + ); } /** diff --git a/src/controls/skew.ts b/src/controls/skew.ts index abf93811b11..b0c1f889673 100644 --- a/src/controls/skew.ts +++ b/src/controls/skew.ts @@ -4,22 +4,14 @@ import type { Transform, TransformActionHandler, } from '../EventTypeDefs'; -import { resolveOrigin } from '../util/misc/resolveOrigin'; import { Point } from '../Point'; -import type { TAxis, TAxisKey } from '../typedefs'; -import { - degreesToRadians, - radiansToDegrees, -} from '../util/misc/radiansDegreesConversion'; -import { - findCornerQuadrant, - getLocalPoint, - isLocked, - NOT_ALLOWED_CURSOR, -} from './util'; +import type { FabricObject } from '../shapes/Object/FabricObject'; +import type { TAxis, TAxisKey, TOriginX, TOriginY } from '../typedefs'; +import { resolveOrigin, resolveOriginPoint } from '../util/misc/resolveOrigin'; +import { findCornerQuadrant, isLocked, NOT_ALLOWED_CURSOR } from './util'; import { wrapWithFireEvent } from './wrapWithFireEvent'; -import { wrapWithFixedAnchor } from './wrapWithFixedAnchor'; -import { CENTER } from '../constants'; +import { BBox } from '../BBox/BBox'; +import { createVector, dotProduct, getUnitVector } from '../util/misc/vectors'; export type SkewTransform = Transform & { skewingSide: -1 | 1 }; @@ -76,133 +68,91 @@ export const skewCursorStyleHandler: ControlCursorCallback = ( return `${skewMap[n]}-resize`; }; -/** - * Since skewing is applied before scaling, calculations are done in a scaleless plane - * @see https://github.com/fabricjs/fabric.js/pull/8380 - */ -function skewObject( +function getSkewingDirection( axis: TAxis, - { target, ex, ey, skewingSide, ...transform }: SkewTransform, + target: FabricObject, + transform: { originX: TOriginX; originY: TOriginY }, pointer: Point ) { - const { skew: skewKey } = AXIS_KEYS[axis], - offset = pointer - .subtract(new Point(ex, ey)) - .divide(new Point(target.scaleX, target.scaleY))[axis], - skewingBefore = target[skewKey], - skewingStart = transform[skewKey], - shearingStart = Math.tan(degreesToRadians(skewingStart)), - // let a, b be the size of target - // let a' be the value of a after applying skewing - // then: - // a' = a + b * skewA => skewA = (a' - a) / b - // the value b is tricky since skewY is applied before skewX - b = - axis === 'y' - ? target._getTransformedDimensions({ - scaleX: 1, - scaleY: 1, - // since skewY is applied before skewX, b (=width) is not affected by skewX - skewX: 0, - }).x - : target._getTransformedDimensions({ - scaleX: 1, - scaleY: 1, - }).y; - - const shearing = - (2 * offset * skewingSide) / - // we max out fractions to safeguard from asymptotic behavior - Math.max(b, 1) + - // add starting state - shearingStart; - - const skewing = radiansToDegrees(Math.atan(shearing)); - - target.set(skewKey, skewing); - const changed = skewingBefore !== target[skewKey]; - - if (changed && axis === 'y') { - // we don't want skewing to affect scaleX - // so we factor it by the inverse skewing diff to make it seem unchanged to the viewer - const { skewX, scaleX } = target, - dimBefore = target._getTransformedDimensions({ skewY: skewingBefore }), - dimAfter = target._getTransformedDimensions(), - compensationFactor = skewX !== 0 ? dimBefore.x / dimAfter.x : 1; - compensationFactor !== 1 && - target.set('scaleX', compensationFactor * scaleX); - } - - return changed; + const { counterAxis } = AXIS_KEYS[axis]; + const { origin: counterOriginKey } = AXIS_KEYS[counterAxis], + counterOriginFactor = resolveOrigin(transform[counterOriginKey]), + // if the counter origin is top/left (= -0.5) then we are skewing x/y values on the bottom/right side of target respectively. + // if the counter origin is bottom/right (= 0.5) then we are skewing x/y values on the top/left side of target respectively. + // skewing direction on the top/left side of target is OPPOSITE to the direction of the movement of the pointer, + // so we factor skewing direction by this value. + skewingSide = -Math.sign(counterOriginFactor) as 1 | -1, + skewingDirection = + Math.sign(pointer.subtract(target.getCenterPoint())[axis]) * skewingSide, + // anchor to the opposite side of the skewing direction + skewingOrigin = -skewingDirection * 0.5; + + const origin = resolveOriginPoint(transform.originX, transform.originY); + origin[axis] = skewingOrigin; + return { + origin, + skewingSide, + }; } -/** - * Wrapped Action handler for skewing on a given axis, takes care of the - * skew direction and determines the correct transform origin for the anchor point - * @param {Event} eventData javascript event that is doing the transform - * @param {Object} transform javascript object containing a series of information around the current transform - * @param {number} x current mouse x position, canvas normalized - * @param {number} y current mouse y position, canvas normalized - * @return {Boolean} true if some change happened - */ -function skewHandler( +function skewObject( axis: TAxis, eventData: TPointerEvent, - transform: Transform, + { target, lastX, lastY, originX, originY }: Transform, x: number, y: number ) { - const { target } = transform, - { - counterAxis, - origin: originKey, - lockSkewing: lockSkewingKey, - skew: skewKey, - flip: flipKey, - } = AXIS_KEYS[axis]; + const { lockSkewing: lockSkewingKey } = AXIS_KEYS[axis]; if (isLocked(target, lockSkewingKey)) { return false; } - - const { origin: counterOriginKey, flip: counterFlipKey } = - AXIS_KEYS[counterAxis], - counterOriginFactor = - resolveOrigin(transform[counterOriginKey]) * - (target[counterFlipKey] ? -1 : 1), - // if the counter origin is top/left (= -0.5) then we are skewing x/y values on the bottom/right side of target respectively. - // if the counter origin is bottom/right (= 0.5) then we are skewing x/y values on the top/left side of target respectively. - // skewing direction on the top/left side of target is OPPOSITE to the direction of the movement of the pointer, - // so we factor skewing direction by this value. - skewingSide = (-Math.sign(counterOriginFactor) * - (target[flipKey] ? -1 : 1)) as 1 | -1, - skewingDirection = - ((target[skewKey] === 0 && - // in case skewing equals 0 we use the pointer offset from target center to determine the direction of skewing - getLocalPoint(transform, CENTER, CENTER, x, y)[axis] > 0) || - // in case target has skewing we use that as the direction - target[skewKey] > 0 - ? 1 - : -1) * skewingSide, - // anchor to the opposite side of the skewing direction - // normalize value from [-1, 1] to origin value [0, 1] - origin = -skewingDirection * 0.5 + 0.5; - - const finalHandler = wrapWithFireEvent( - 'skewing', - wrapWithFixedAnchor((eventData, transform, x, y) => - skewObject(axis, transform, new Point(x, y)) - ) + const pointer = new Point(x, y); + const { origin: skewingOrigin, skewingSide } = getSkewingDirection( + axis, + target, + { originX, originY }, + pointer ); - - return finalHandler( - eventData, + const transformed = BBox.transformed(target); + const { tl, tr, bl } = transformed.getCoords(); + const tSides = { + x: createVector(tl, tr), + y: createVector(tl, bl), + }; + const offset = dotProduct( + pointer.subtract(new Point(lastX, lastY)), + tSides[axis] + ); + const shearing = 2 * offset * skewingSide; + const didChange = target.shearSidesBy( + [tSides.x, tSides.y], + [ + axis === 'y' + ? getUnitVector(tSides.y).scalarMultiply(shearing) + : new Point(), + axis === 'x' + ? getUnitVector(tSides.x).scalarMultiply(shearing) + : new Point(), + ], { - ...transform, - [originKey]: origin, - skewingSide, - }, - x, - y + originX: skewingOrigin.x + 0.5, + originY: skewingOrigin.y + 0.5, + inViewport: true, + } + ); + // we anchor to the origin of the transformed bbox + target.setCoords(); + const position = transformed.pointFromOrigin( + // resolveOriginPoint(originX, originY) + skewingOrigin + ); + const origin = target.bbox.pointToOrigin(position).scalarAdd(0.5); + return ( + target.translateTo(position.x, position.y, { + originX: origin.x, + originY: origin.y, + inViewport: true, + }) || didChange ); } @@ -215,14 +165,10 @@ function skewHandler( * @param {number} y current mouse y position, canvas normalized * @return {Boolean} true if some change happened */ -export const skewHandlerX: TransformActionHandler = ( - eventData, - transform, - x, - y -) => { - return skewHandler('x', eventData, transform, x, y); -}; +export const skewHandlerX: TransformActionHandler = wrapWithFireEvent( + 'skewing', + skewObject.bind(null, 'x') +); /** * Wrapped Action handler for skewing on the Y axis, takes care of the @@ -233,11 +179,7 @@ export const skewHandlerX: TransformActionHandler = ( * @param {number} y current mouse y position, canvas normalized * @return {Boolean} true if some change happened */ -export const skewHandlerY: TransformActionHandler = ( - eventData, - transform, - x, - y -) => { - return skewHandler('y', eventData, transform, x, y); -}; +export const skewHandlerY: TransformActionHandler = wrapWithFireEvent( + 'skewing', + skewObject.bind(null, 'y') +); diff --git a/src/controls/util.ts b/src/controls/util.ts index c372c1a2612..c036b5f735c 100644 --- a/src/controls/util.ts +++ b/src/controls/util.ts @@ -4,16 +4,13 @@ import type { TransformAction, BasicTransformEvent, } from '../EventTypeDefs'; -import { resolveOrigin } from '../util/misc/resolveOrigin'; import { Point } from '../Point'; +import { PIBy4, twoMathPi } from '../constants'; import type { FabricObject } from '../shapes/Object/FabricObject'; import type { TOriginX, TOriginY } from '../typedefs'; -import { - degreesToRadians, - radiansToDegrees, -} from '../util/misc/radiansDegreesConversion'; +import { resolveOrigin, resolveOriginPoint } from '../util/misc/resolveOrigin'; import type { Control } from './Control'; -import { CENTER } from '../constants'; +import { calcVectorRotation } from '../util/misc/vectors'; export const NOT_ALLOWED_CURSOR = 'not-allowed'; @@ -41,8 +38,14 @@ export const getActionFromCorner = ( * @param {Object} transform transform data * @return {Boolean} true if transform is centered */ -export function isTransformCentered(transform: Transform) { - return transform.originX === CENTER && transform.originY === CENTER; +export function isTransformCentered({ + originX, + originY, +}: { + originX: TOriginX; + originY: TOriginY; +}) { + return resolveOriginPoint(originX, originY).eq(new Point()); } export function invertOrigin(origin: TOriginX | TOriginY) { @@ -83,73 +86,9 @@ export const commonEventInfo: TransformAction< export function findCornerQuadrant( fabricObject: FabricObject, control: Control -): number { - // angle is relative to canvas plane - const angle = fabricObject.getTotalAngle(), - cornerAngle = - angle + radiansToDegrees(Math.atan2(control.y, control.x)) + 360; - return Math.round((cornerAngle % 360) / 45); -} - -/** - * @returns the normalized point (rotated relative to center) in local coordinates - */ -function normalizePoint( - target: FabricObject, - point: Point, - originX: TOriginX, - originY: TOriginY -): Point { - const center = target.getRelativeCenterPoint(), - p = - typeof originX !== 'undefined' && typeof originY !== 'undefined' - ? target.translateToGivenOrigin( - center, - CENTER, - CENTER, - originX, - originY - ) - : new Point(target.left, target.top), - p2 = target.angle - ? point.rotate(-degreesToRadians(target.angle), center) - : point; - return p2.subtract(p); -} - -/** - * Transforms a point to the offset from the given origin - * @param {Object} transform - * @param {String} originX - * @param {String} originY - * @param {number} x - * @param {number} y - * @return {Fabric.Point} the normalized point - */ -export function getLocalPoint( - { target, corner }: Transform, - originX: TOriginX, - originY: TOriginY, - x: number, - y: number ) { - const control = target.controls[corner], - zoom = target.canvas?.getZoom() || 1, - padding = target.padding / zoom, - localPoint = normalizePoint(target, new Point(x, y), originX, originY); - if (localPoint.x >= padding) { - localPoint.x -= padding; - } - if (localPoint.x <= -padding) { - localPoint.x += padding; - } - if (localPoint.y >= padding) { - localPoint.y -= padding; - } - if (localPoint.y <= padding) { - localPoint.y += padding; - } - localPoint.x -= control.offsetX; - localPoint.y -= control.offsetY; - return localPoint; + const rotation = calcVectorRotation( + fabricObject.bbox.vectorFromOrigin(new Point(control)) + ); + return Math.round(((rotation + twoMathPi) % twoMathPi) / PIBy4); } diff --git a/src/controls/wrapWithFixedAnchor.ts b/src/controls/wrapWithFixedAnchor.ts index 787add10448..dc2c2fce498 100644 --- a/src/controls/wrapWithFixedAnchor.ts +++ b/src/controls/wrapWithFixedAnchor.ts @@ -11,16 +11,9 @@ export function wrapWithFixedAnchor( ) { return ((eventData, transform, x, y) => { const { target, originX, originY } = transform, - centerPoint = target.getRelativeCenterPoint(), - constraint = target.translateToOriginPoint(centerPoint, originX, originY), + constraint = target.getXY(originX, originY), actionPerformed = actionHandler(eventData, transform, x, y); - // flipping requires to change the transform origin, so we read from the mutated transform - // instead of leveraging the one destructured before - target.setPositionByOrigin( - constraint, - transform.originX, - transform.originY - ); + target.setXY(constraint, originX, originY); return actionPerformed; }) as TransformActionHandler; } diff --git a/src/mixins/eraser_brush.mixin.ts b/src/mixins/eraser_brush.mixin.ts deleted file mode 100644 index b1e2a74d40d..00000000000 --- a/src/mixins/eraser_brush.mixin.ts +++ /dev/null @@ -1,875 +0,0 @@ -//@ts-nocheck -import { Point } from '../Point'; -import { FabricObject } from '../shapes/Object/FabricObject'; -import { uid } from '../util/internals/uid'; - -(function (global) { - /** ERASER_START */ - - var fabric = global.fabric, - __drawClipPath = fabric.Object.prototype._drawClipPath; - var _needsItsOwnCache = fabric.Object.prototype.needsItsOwnCache; - var _toObject = fabric.Object.prototype.toObject; - var _getSvgCommons = fabric.Object.prototype.getSvgCommons; - var __createBaseClipPathSVGMarkup = - fabric.Object.prototype._createBaseClipPathSVGMarkup; - var __createBaseSVGMarkup = fabric.Object.prototype._createBaseSVGMarkup; - - fabric.Object.prototype.cacheProperties.push('eraser'); - fabric.Object.prototype.stateProperties.push('eraser'); - - /** - * @fires erasing:end - */ - fabric.util.object.extend(fabric.Object.prototype, { - /** - * Indicates whether this object can be erased by {@link fabric.EraserBrush} - * The `deep` option introduces fine grained control over a group's `erasable` property. - * When set to `deep` the eraser will erase nested objects if they are erasable, leaving the group and the other objects untouched. - * When set to `true` the eraser will erase the entire group. Once the group changes the eraser is propagated to its children for proper functionality. - * When set to `false` the eraser will leave all objects including the group untouched. - * @tutorial {@link http://fabricjs.com/erasing#erasable_property} - * @type boolean | 'deep' - * @default true - */ - erasable: true, - - /** - * @tutorial {@link http://fabricjs.com/erasing#eraser} - * @type fabric.Eraser - */ - eraser: undefined, - - /** - * @override - * @returns Boolean - */ - needsItsOwnCache: function () { - return _needsItsOwnCache.call(this) || !!this.eraser; - }, - - /** - * draw eraser above clip path - * @override - * @private - * @param {CanvasRenderingContext2D} ctx - * @param {fabric.Object} clipPath - */ - _drawClipPath: function (ctx, clipPath) { - __drawClipPath.call(this, ctx, clipPath); - if (this.eraser) { - // update eraser size to match instance - var size = this._getNonTransformedDimensions(); - this.eraser.isType('eraser') && - this.eraser.set({ - width: size.x, - height: size.y, - }); - __drawClipPath.call(this, ctx, this.eraser); - } - }, - - /** - * Returns an object representation of an instance - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} Object representation of an instance - */ - toObject: function (propertiesToInclude) { - var object = _toObject.call( - this, - ['erasable'].concat(propertiesToInclude) - ); - if (this.eraser && !this.eraser.excludeFromExport) { - object.eraser = this.eraser.toObject(propertiesToInclude); - } - return object; - }, - - /* _TO_SVG_START_ */ - /** - * Returns id attribute for svg output - * @override - * @return {String} - */ - getSvgCommons: function () { - return ( - _getSvgCommons.call(this) + - (this.eraser ? 'mask="url(#' + this.eraser.clipPathId + ')" ' : '') - ); - }, - - /** - * create svg markup for eraser - * use to achieve erasing for svg, credit: https://travishorn.com/removing-parts-of-shapes-in-svg-b539a89e5649 - * must be called before object markup creation as it relies on the `clipPathId` property of the mask - * @param {Function} [reviver] - * @returns - */ - _createEraserSVGMarkup: function (reviver) { - if (this.eraser) { - this.eraser.clipPathId = 'MASK_' + uid(); - return [ - '', - this.eraser.toSVG(reviver), - '', - '\n', - ].join(''); - } - return ''; - }, - - /** - * @private - */ - _createBaseClipPathSVGMarkup: function (objectMarkup, options) { - return [ - this._createEraserSVGMarkup(options && options.reviver), - __createBaseClipPathSVGMarkup.call(this, objectMarkup, options), - ].join(''); - }, - - /** - * @private - */ - _createBaseSVGMarkup: function (objectMarkup, options) { - return [ - this._createEraserSVGMarkup(options && options.reviver), - __createBaseSVGMarkup.call(this, objectMarkup, options), - ].join(''); - }, - /* _TO_SVG_END_ */ - }); - - fabric.util.object.extend(fabric.Group.prototype, { - /** - * @private - * @param {fabric.Path} path - * @returns {Promise} - */ - _addEraserPathToObjects: function (path) { - return Promise.all( - this._objects.map(function (object) { - return fabric.EraserBrush.prototype._addPathToObjectEraser.call( - fabric.EraserBrush.prototype, - object, - path - ); - }) - ); - }, - - /** - * Applies the group's eraser to its objects - * @tutorial {@link http://fabricjs.com/erasing#erasable_property} - * @returns {Promise} - */ - applyEraserToObjects: function () { - var _this = this, - eraser = this.eraser; - return Promise.resolve().then(function () { - if (eraser) { - delete _this.eraser; - var transform = _this.calcTransformMatrix(); - return eraser.clone().then(function (eraser) { - var clipPath = _this.clipPath; - return Promise.all( - eraser.getObjects('path').map(function (path) { - // first we transform the path from the group's coordinate system to the canvas' - var originalTransform = fabric.util.multiplyTransformMatrices( - transform, - path.calcTransformMatrix() - ); - fabric.util.applyTransformToObject(path, originalTransform); - return clipPath - ? clipPath.clone().then( - function (_clipPath) { - var eraserPath = - fabric.EraserBrush.prototype.applyClipPathToPath.call( - fabric.EraserBrush.prototype, - path, - _clipPath, - transform - ); - return _this._addEraserPathToObjects(eraserPath); - }, - ['absolutePositioned', 'inverted'] - ) - : _this._addEraserPathToObjects(path); - }) - ); - }); - } - }); - }, - }); - - /** - * An object's Eraser - * @private - * @class fabric.Eraser - * @extends fabric.Group - * @memberof fabric - */ - fabric.Eraser = fabric.util.createClass(fabric.Group, { - /** - * @readonly - * @static - */ - type: 'eraser', - - /** - * @default - */ - originX: 'center', - - /** - * @default - */ - originY: 'center', - - /** - * eraser should retain size - * dimensions should not change when paths are added or removed - * handled by {@link fabric.Object#_drawClipPath} - * @override - * @private - */ - layout: 'fixed', - - drawObject: function (ctx) { - ctx.save(); - ctx.fillStyle = 'black'; - ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); - ctx.restore(); - this.callSuper('drawObject', ctx); - }, - - /* _TO_SVG_START_ */ - /** - * Returns svg representation of an instance - * use to achieve erasing for svg, credit: https://travishorn.com/removing-parts-of-shapes-in-svg-b539a89e5649 - * for masking we need to add a white rect before all paths - * - * @param {Function} [reviver] Method for further parsing of svg representation. - * @return {String} svg representation of an instance - */ - _toSVG: function (reviver) { - var svgString = ['\n']; - var x = -this.width / 2, - y = -this.height / 2; - var rectSvg = [ - '\n', - ].join(''); - svgString.push('\t\t', rectSvg); - for (var i = 0, len = this._objects.length; i < len; i++) { - svgString.push('\t\t', this._objects[i].toSVG(reviver)); - } - svgString.push('\n'); - return svgString; - }, - /* _TO_SVG_END_ */ - }); - - /** - * Returns instance from an object representation - * @static - * @memberOf fabric.Eraser - * @param {Object} object Object to create an Eraser from - * @returns {Promise} - */ - fabric.Eraser.fromObject = function (object) { - var objects = object.objects || [], - options = fabric.util.object.clone(object, true); - delete options.objects; - return Promise.all([ - fabric.util.enlivenObjects(objects), - fabric.util.enlivenObjectEnlivables(options), - ]).then(function (enlivedProps) { - return new fabric.Eraser( - enlivedProps[0], - Object.assign(options, enlivedProps[1]), - true - ); - }); - }; - - var __renderOverlay = fabric.Canvas.prototype._renderOverlay; - /** - * @fires erasing:start - * @fires erasing:end - */ - fabric.util.object.extend(fabric.Canvas.prototype, { - /** - * Used by {@link #renderAll} - * @returns boolean - */ - isErasing: function () { - return ( - this.isDrawingMode && - this.freeDrawingBrush && - this.freeDrawingBrush.type === 'eraser' && - this.freeDrawingBrush._isErasing - ); - }, - - /** - * While erasing the brush clips out the erasing path from canvas - * so we need to render it on top of canvas every render - * @param {CanvasRenderingContext2D} ctx - */ - _renderOverlay: function (ctx) { - __renderOverlay.call(this, ctx); - this.isErasing() && this.freeDrawingBrush._render(); - }, - }); - - /** - * EraserBrush class - * Supports selective erasing meaning that only erasable objects are affected by the eraser brush. - * Supports **inverted** erasing meaning that the brush can "undo" erasing. - * - * In order to support selective erasing, the brush clips the entire canvas - * and then draws all non-erasable objects over the erased path using a pattern brush so to speak (masking). - * If brush is **inverted** there is no need to clip canvas. The brush draws all erasable objects without their eraser. - * This achieves the desired effect of seeming to erase or unerase only erasable objects. - * After erasing is done the created path is added to all intersected objects' `eraser` property. - * - * In order to update the EraserBrush call `preparePattern`. - * It may come in handy when canvas changes during erasing (i.e animations) and you want the eraser to reflect the changes. - * - * @tutorial {@link http://fabricjs.com/erasing} - * @class fabric.EraserBrush - * @extends fabric.PencilBrush - * @memberof fabric - */ - fabric.EraserBrush = fabric.util.createClass( - fabric.PencilBrush, - /** @lends fabric.EraserBrush.prototype */ { - type: 'eraser', - - /** - * When set to `true` the brush will create a visual effect of undoing erasing - * @type boolean - */ - inverted: false, - - /** - * Used to fix https://github.com/fabricjs/fabric.js/issues/7984 - * Reduces the path width while clipping the main context, resulting in a better visual overlap of both contexts - * @type number - */ - erasingWidthAliasing: 4, - - /** - * @private - */ - _isErasing: false, - - /** - * - * @private - * @param {fabric.Object} object - * @returns boolean - */ - _isErasable: function (object) { - return object.erasable !== false; - }, - - /** - * @private - * This is designed to support erasing a collection with both erasable and non-erasable objects while maintaining object stacking.\ - * Iterates over collections to allow nested selective erasing.\ - * Prepares objects before rendering the pattern brush.\ - * If brush is **NOT** inverted render all non-erasable objects.\ - * If brush is inverted render all objects, erasable objects without their eraser. - * This will render the erased parts as if they were not erased in the first place, achieving an undo effect. - * - * @param {fabric.Collection} collection - * @param {fabric.Object[]} objects - * @param {CanvasRenderingContext2D} ctx - * @param {{ visibility: fabric.Object[], eraser: fabric.Object[], collection: fabric.Object[] }} restorationContext - */ - _prepareCollectionTraversal: function ( - collection, - objects, - ctx, - restorationContext - ) { - objects.forEach(function (obj) { - var dirty = false; - if (obj.forEachObject && obj.erasable === 'deep') { - // traverse - this._prepareCollectionTraversal( - obj, - obj._objects, - ctx, - restorationContext - ); - } else if (!this.inverted && obj.erasable && obj.visible) { - // render only non-erasable objects - obj.visible = false; - restorationContext.visibility.push(obj); - dirty = true; - } else if ( - this.inverted && - obj.erasable && - obj.eraser && - obj.visible - ) { - // render all objects without eraser - var eraser = obj.eraser; - obj.eraser = undefined; - obj.dirty = true; - restorationContext.eraser.push([obj, eraser]); - dirty = true; - } - if (dirty && collection instanceof fabric.Object) { - collection.dirty = true; - restorationContext.collection.push(collection); - } - }, this); - }, - - /** - * Prepare the pattern for the erasing brush - * This pattern will be drawn on the top context after clipping the main context, - * achieving a visual effect of erasing only erasable objects - * @private - * @param {fabric.Object[]} [objects] override default behavior by passing objects to render on pattern - */ - preparePattern: function (objects) { - if (!this._patternCanvas) { - this._patternCanvas = fabric.util.createCanvasElement(); - } - var canvas = this._patternCanvas; - objects = - objects || this.canvas._objectsToRender || this.canvas._objects; - canvas.width = this.canvas.width; - canvas.height = this.canvas.height; - var patternCtx = canvas.getContext('2d'); - if (this.canvas._isRetinaScaling()) { - var retinaScaling = this.canvas.getRetinaScaling(); - this.canvas.__initRetinaScaling(retinaScaling, canvas, patternCtx); - } - var backgroundImage = this.canvas.backgroundImage, - bgErasable = backgroundImage && this._isErasable(backgroundImage), - overlayImage = this.canvas.overlayImage, - overlayErasable = overlayImage && this._isErasable(overlayImage); - if ( - !this.inverted && - ((backgroundImage && !bgErasable) || !!this.canvas.backgroundColor) - ) { - if (bgErasable) { - this.canvas.backgroundImage = undefined; - } - this.canvas._renderBackground(patternCtx); - if (bgErasable) { - this.canvas.backgroundImage = backgroundImage; - } - } else if (this.inverted) { - var eraser = backgroundImage && backgroundImage.eraser; - if (eraser) { - backgroundImage.eraser = undefined; - backgroundImage.dirty = true; - } - this.canvas._renderBackground(patternCtx); - if (eraser) { - backgroundImage.eraser = eraser; - backgroundImage.dirty = true; - } - } - patternCtx.save(); - patternCtx.transform.apply(patternCtx, this.canvas.viewportTransform); - var restorationContext = { visibility: [], eraser: [], collection: [] }; - this._prepareCollectionTraversal( - this.canvas, - objects, - patternCtx, - restorationContext - ); - this.canvas._renderObjects(patternCtx, objects); - restorationContext.visibility.forEach(function (obj) { - obj.visible = true; - }); - restorationContext.eraser.forEach(function (entry) { - var obj = entry[0], - eraser = entry[1]; - obj.eraser = eraser; - obj.dirty = true; - }); - restorationContext.collection.forEach(function (obj) { - obj.dirty = true; - }); - patternCtx.restore(); - if ( - !this.inverted && - ((overlayImage && !overlayErasable) || !!this.canvas.overlayColor) - ) { - if (overlayErasable) { - this.canvas.overlayImage = undefined; - } - __renderOverlay.call(this.canvas, patternCtx); - if (overlayErasable) { - this.canvas.overlayImage = overlayImage; - } - } else if (this.inverted) { - var eraser = overlayImage && overlayImage.eraser; - if (eraser) { - overlayImage.eraser = undefined; - overlayImage.dirty = true; - } - __renderOverlay.call(this.canvas, patternCtx); - if (eraser) { - overlayImage.eraser = eraser; - overlayImage.dirty = true; - } - } - }, - - /** - * Sets brush styles - * @private - * @param {CanvasRenderingContext2D} ctx - */ - _setBrushStyles: function (ctx) { - this.callSuper('_setBrushStyles', ctx); - ctx.strokeStyle = 'black'; - }, - - /** - * **Customiztion** - * - * if you need the eraser to update on each render (i.e animating during erasing) override this method by **adding** the following (performance may suffer): - * @example - * ``` - * if(ctx === this.canvas.contextTop) { - * this.preparePattern(); - * } - * ``` - * - * @override fabric.BaseBrush#_saveAndTransform - * @param {CanvasRenderingContext2D} ctx - */ - _saveAndTransform: function (ctx) { - this.callSuper('_saveAndTransform', ctx); - this._setBrushStyles(ctx); - ctx.globalCompositeOperation = - ctx === this.canvas.getContext() - ? 'destination-out' - : 'destination-in'; - }, - - /** - * We indicate {@link fabric.PencilBrush} to repaint itself if necessary - * @returns - */ - needsFullRender: function () { - return true; - }, - - /** - * - * @param {Point} pointer - * @param {fabric.IEvent} options - * @returns - */ - onMouseDown: function (pointer, options) { - if (!this.canvas._isMainEvent(options.e)) { - return; - } - this._prepareForDrawing(pointer); - // capture coordinates immediately - // this allows to draw dots (when movement never occurs) - this._captureDrawingPath(pointer); - - // prepare for erasing - this.preparePattern(); - this._isErasing = true; - this.canvas.fire('erasing:start'); - this._render(); - }, - - /** - * Rendering Logic: - * 1. Use brush to clip canvas by rendering it on top of canvas (unnecessary if `inverted === true`) - * 2. Render brush with canvas pattern on top context - * - * @todo provide a better solution to https://github.com/fabricjs/fabric.js/issues/7984 - */ - _render: function () { - var ctx, - lineWidth = this.width; - var t = this.canvas.getRetinaScaling(), - s = 1 / t; - // clip canvas - ctx = this.canvas.getContext(); - // a hack that fixes https://github.com/fabricjs/fabric.js/issues/7984 by reducing path width - // the issue's cause is unknown at time of writing (@ShaMan123 06/2022) - if (lineWidth - this.erasingWidthAliasing > 0) { - this.width = lineWidth - this.erasingWidthAliasing; - this.callSuper('_render', ctx); - this.width = lineWidth; - } - // render brush and mask it with pattern - ctx = this.canvas.contextTop; - this.canvas.clearContext(ctx); - ctx.save(); - ctx.scale(s, s); - ctx.drawImage(this._patternCanvas, 0, 0); - ctx.restore(); - this.callSuper('_render', ctx); - }, - - /** - * Creates fabric.Path object - * @override - * @private - * @param {(string|number)[][]} pathData Path data - * @return {fabric.Path} Path to add on canvas - * @returns - */ - createPath: function (pathData) { - var path = this.callSuper('createPath', pathData); - path.globalCompositeOperation = this.inverted - ? 'source-over' - : 'destination-out'; - path.stroke = this.inverted ? 'white' : 'black'; - return path; - }, - - /** - * Utility to apply a clip path to a path. - * Used to preserve clipping on eraser paths in nested objects. - * Called when a group has a clip path that should be applied to the path before applying erasing on the group's objects. - * @param {fabric.Path} path The eraser path in canvas coordinate plane - * @param {fabric.Object} clipPath The clipPath to apply to the path - * @param {number[]} clipPathContainerTransformMatrix The transform matrix of the object that the clip path belongs to - * @returns {fabric.Path} path with clip path - */ - applyClipPathToPath: function ( - path, - clipPath, - clipPathContainerTransformMatrix - ) { - var pathInvTransform = fabric.util.invertTransform( - path.calcTransformMatrix() - ), - clipPathTransform = clipPath.calcTransformMatrix(), - transform = clipPath.absolutePositioned - ? pathInvTransform - : fabric.util.multiplyTransformMatrices( - pathInvTransform, - clipPathContainerTransformMatrix - ); - // when passing down a clip path it becomes relative to the parent - // so we transform it acoordingly and set `absolutePositioned` to false - clipPath.absolutePositioned = false; - fabric.util.applyTransformToObject( - clipPath, - fabric.util.multiplyTransformMatrices(transform, clipPathTransform) - ); - // We need to clip `path` with both `clipPath` and it's own clip path if existing (`path.clipPath`) - // so in turn `path` erases an object only where it overlaps with all it's clip paths, regardless of how many there are. - // this is done because both clip paths may have nested clip paths of their own (this method walks down a collection => this may reccur), - // so we can't assign one to the other's clip path property. - path.clipPath = path.clipPath - ? fabric.util.mergeClipPaths(clipPath, path.clipPath) - : clipPath; - return path; - }, - - /** - * Utility to apply a clip path to a path. - * Used to preserve clipping on eraser paths in nested objects. - * Called when a group has a clip path that should be applied to the path before applying erasing on the group's objects. - * @param {fabric.Path} path The eraser path - * @param {fabric.Object} object The clipPath to apply to path belongs to object - * @returns {Promise} - */ - clonePathWithClipPath: function (path, object) { - var objTransform = object.calcTransformMatrix(); - var clipPath = object.clipPath; - var _this = this; - return Promise.all([ - path.clone(), - clipPath.clone(['absolutePositioned', 'inverted']), - ]).then(function (clones) { - return _this.applyClipPathToPath(clones[0], clones[1], objTransform); - }); - }, - - /** - * Adds path to object's eraser, walks down object's descendants if necessary - * - * @public - * @fires erasing:end on object - * @param {fabric.Object} obj - * @param {fabric.Path} path - * @param {Object} [context] context to assign erased objects to - * @returns {Promise} - */ - _addPathToObjectEraser: function (obj, path, context) { - var _this = this; - // object is collection, i.e group - if (obj.forEachObject && obj.erasable === 'deep') { - var targets = obj._objects.filter(function (_obj) { - return _obj.erasable; - }); - if (targets.length > 0 && obj.clipPath) { - return this.clonePathWithClipPath(path, obj).then(function (_path) { - return Promise.all( - targets.map(function (_obj) { - return _this._addPathToObjectEraser(_obj, _path, context); - }) - ); - }); - } else if (targets.length > 0) { - return Promise.all( - targets.map(function (_obj) { - return _this._addPathToObjectEraser(_obj, path, context); - }) - ); - } - return; - } - // prepare eraser - var eraser = obj.eraser; - if (!eraser) { - eraser = new fabric.Eraser(); - obj.eraser = eraser; - } - // clone and add path - return path.clone().then(function (path) { - // http://fabricjs.com/using-transformations - var desiredTransform = fabric.util.multiplyTransformMatrices( - fabric.util.invertTransform(obj.calcTransformMatrix()), - path.calcTransformMatrix() - ); - fabric.util.applyTransformToObject(path, desiredTransform); - eraser.add(path); - obj.set('dirty', true); - obj.fire('erasing:end', { - path: path, - }); - if (context) { - (obj.group ? context.subTargets : context.targets).push(obj); - //context.paths.set(obj, path); - } - return path; - }); - }, - - /** - * Add the eraser path to canvas drawables' clip paths - * - * @param {fabric.Canvas} source - * @param {fabric.Canvas} path - * @param {Object} [context] context to assign erased objects to - * @returns {Promise} eraser paths - */ - applyEraserToCanvas: function (path, context) { - var canvas = this.canvas; - return Promise.all( - ['backgroundImage', 'overlayImage'].map(function (prop) { - var drawable = canvas[prop]; - return ( - drawable && - drawable.erasable && - this._addPathToObjectEraser(drawable, path).then(function (path) { - if (context) { - context.drawables[prop] = drawable; - //context.paths.set(drawable, path); - } - return path; - }) - ); - }, this) - ); - }, - - /** - * On mouseup after drawing the path on contextTop canvas - * we use the points captured to create an new fabric path object - * and add it to every intersected erasable object. - */ - _finalizeAndAddPath: function () { - var ctx = this.canvas.contextTop, - canvas = this.canvas; - ctx.closePath(); - if (this.decimate) { - this._points = this.decimatePoints(this._points, this.decimate); - } - - // clear - canvas.clearContext(canvas.contextTop); - this._isErasing = false; - - var pathData = - this._points && this._points.length > 1 - ? this.convertPointsToSVGPath(this._points) - : null; - if (!pathData || this._isEmptySVGPath(pathData)) { - canvas.fire('erasing:end'); - // do not create 0 width/height paths, as they are - // rendered inconsistently across browsers - // Firefox 4, for example, renders a dot, - // whereas Chrome 10 renders nothing - canvas.requestRenderAll(); - return; - } - - var path = this.createPath(pathData); - // needed for `intersectsWithObject` - path.setCoords(); - // commense event sequence - canvas.fire('before:path:created', { path: path }); - - // finalize erasing - var _this = this; - var context = { - targets: [], - subTargets: [], - //paths: new Map(), - drawables: {}, - }; - var tasks = canvas._objects.map(function (obj) { - return ( - obj.erasable && - obj.intersectsWithObject(path, true, true) && - _this._addPathToObjectEraser(obj, path, context) - ); - }); - tasks.push(_this.applyEraserToCanvas(path, context)); - return Promise.all(tasks).then(function () { - // fire erasing:end - canvas.fire( - 'erasing:end', - Object.assign(context, { - path: path, - }) - ); - - canvas.requestRenderAll(); - _this._resetShadow(); - - // fire event 'path' created - canvas.fire('path:created', { path: path }); - }); - }, - } - ); - - /** ERASER_END */ -})(typeof exports !== 'undefined' ? exports : window); diff --git a/src/parkinglot/canvas_animation.mixin.ts b/src/parkinglot/canvas_animation.mixin.ts index 143be169893..dec27b63d39 100644 --- a/src/parkinglot/canvas_animation.mixin.ts +++ b/src/parkinglot/canvas_animation.mixin.ts @@ -38,7 +38,7 @@ Object.assign(StaticCanvas.prototype, { onChange(); }, onComplete: function () { - object.setCoords(); + object.invalidateCoords(); onComplete(); }, }); @@ -71,7 +71,7 @@ Object.assign(StaticCanvas.prototype, { onChange(); }, onComplete: function () { - object.setCoords(); + object.invalidateCoords(); onComplete(); }, }); diff --git a/src/parkinglot/straighten.ts b/src/parkinglot/straighten.ts index 1795179f948..bd7f3b316d9 100644 --- a/src/parkinglot/straighten.ts +++ b/src/parkinglot/straighten.ts @@ -57,7 +57,7 @@ Object.assign(FabricObject.prototype, { onChange(value); }, onComplete: () => { - this.setCoords(); + this.invalidateCoords(); onComplete(); }, }); diff --git a/src/parser/elements_parser.ts b/src/parser/elements_parser.ts index 7ea173fc8a1..993ce2b48e4 100644 --- a/src/parser/elements_parser.ts +++ b/src/parser/elements_parser.ts @@ -10,7 +10,6 @@ import { import { removeTransformMatrixForSvgParsing } from '../util/transform_matrix_removal'; import type { FabricObject } from '../shapes/Object/FabricObject'; import { Point } from '../Point'; -import { CENTER } from '../constants'; import { getGradientDefs } from './getGradientDefs'; import { getCSSRules } from './getCSSRules'; import type { LoadImageOptions } from '../util'; @@ -203,11 +202,7 @@ export class ElementsParser { skewX, skewY: 0, }); - clipPath.setPositionByOrigin( - new Point(translateX, translateY), - CENTER, - CENTER - ); + clipPath.setRelativeCenterPoint(new Point(translateX, translateY)); obj.clipPath = clipPath; } else { // if clip-path does not resolve to any element, delete the property. diff --git a/src/parser/parseTransformAttribute.ts b/src/parser/parseTransformAttribute.ts index 91bd30c6760..9fbed5f704b 100644 --- a/src/parser/parseTransformAttribute.ts +++ b/src/parser/parseTransformAttribute.ts @@ -8,6 +8,7 @@ import { createSkewXMatrix, createSkewYMatrix, createTranslateMatrix, + multiplyTransformMatrices, multiplyTransformMatrixArray, } from '../util/misc/matrix'; @@ -70,7 +71,10 @@ export function parseTransformAttribute(attributeValue: string): TMat2D { matrix = createTranslateMatrix(arg0, arg1); break; case 'rotate': - matrix = createRotateMatrix({ angle: arg0 }, { x: arg1, y: arg2 }); + matrix = multiplyTransformMatrices( + createTranslateMatrix(arg1 || 0, arg2 || 0), + createRotateMatrix({ angle: arg0 }) + ); break; case 'scale': matrix = createScaleMatrix(arg0, arg1); diff --git a/src/shapes/ActiveSelection.spec.ts b/src/shapes/ActiveSelection.spec.ts index f0b4baee88b..e9991197b53 100644 --- a/src/shapes/ActiveSelection.spec.ts +++ b/src/shapes/ActiveSelection.spec.ts @@ -58,7 +58,7 @@ describe('ActiveSelection', () => { const obj2 = new FabricObject(); canvas.add(obj1, obj2); const activeSelection = new ActiveSelection([obj1, obj2]); - const spy = jest.spyOn(activeSelection, 'setCoords'); + const spy = jest.spyOn(activeSelection, 'invalidateCoords'); canvas.setActiveObject(activeSelection); expect(canvas.getActiveObject()).toBe(activeSelection); expect(canvas.getActiveObjects()).toEqual([obj1, obj2]); diff --git a/src/shapes/ActiveSelection.ts b/src/shapes/ActiveSelection.ts index 19ff25a0f65..62ddae92776 100644 --- a/src/shapes/ActiveSelection.ts +++ b/src/shapes/ActiveSelection.ts @@ -68,13 +68,6 @@ export class ActiveSelection extends Group { }); } - /** - * @private - */ - _shouldSetNestedCoords() { - return true; - } - /** * @private * @override we don't want the selection monitor to be active diff --git a/src/shapes/Group.ts b/src/shapes/Group.ts index 0c971a249a3..0666d78bb5e 100644 --- a/src/shapes/Group.ts +++ b/src/shapes/Group.ts @@ -1,4 +1,4 @@ -import type { CollectionEvents, ObjectEvents } from '../EventTypeDefs'; +import { classRegistry } from '../ClassRegistry'; import { createCollectionMixin } from '../Collection'; import type { TClassProperties, TSVGReviver, TOptions } from '../typedefs'; import { @@ -11,9 +11,8 @@ import { } from '../util/misc/objectEnlive'; import { applyTransformToObject } from '../util/misc/objectTransforms'; import { FabricObject } from './Object/FabricObject'; -import { Rect } from './Rect'; -import { classRegistry } from '../ClassRegistry'; import type { FabricObjectProps, SerializedObjectProps } from './Object/types'; +import { Rect } from './Rect'; import { log } from '../util/internals/console'; import type { ImperativeLayoutOptions, @@ -133,7 +132,7 @@ export class Group */ constructor(objects: FabricObject[] = [], options: Partial = {}) { // @ts-expect-error options error - super(options); + super({ _objects: [], ...options }); this._objects = [...objects]; // Avoid unwanted mutations of Collection to affect the caller this.__objectSelectionTracker = this.__objectSelectionMonitor.bind( @@ -280,17 +279,15 @@ export class Group (this._objects || []).forEach((object) => { object._set(key, value); }); + // layout in case children need viewport coords + value && + this._applyLayoutStrategy({ + type: 'viewport', + }); } return this; } - /** - * @private - */ - _shouldSetNestedCoords() { - return this.subTargetCheck; - } - /** * Remove all objects * @returns {FabricObject[]} removed objects @@ -300,6 +297,14 @@ export class Group return this.remove(...this._objects); } + /** + * @override recursively invalidate descendant coords as well + */ + invalidateCoords() { + super.invalidateCoords(); + this.forEachObject((object) => object.invalidateCoords()); + } + /** * keeps track of the selected objects * @private @@ -365,7 +370,7 @@ export class Group ) ); } - this._shouldSetNestedCoords() && object.setCoords(); + object._set('group', this); object._set('canvas', this.canvas); this._watchObject(true, object); @@ -412,8 +417,9 @@ export class Group object.calcTransformMatrix() ) ); - object.setCoords(); } + // invalidate coords in case group was transformed + object.invalidateCoords(); this._watchObject(false, object); const index = this._activeObjects.length > 0 ? this._activeObjects.indexOf(object) : -1; @@ -472,33 +478,25 @@ export class Group */ drawObject(ctx: CanvasRenderingContext2D) { this._renderBackground(ctx); - for (let i = 0; i < this._objects.length; i++) { + const preserve = this.canvas?.preserveObjectStacking; + this._objects.forEach((object) => { // TODO: handle rendering edge case somehow - if ( - this.canvas?.preserveObjectStacking && - this._objects[i].group !== this - ) { + if (preserve && object.group !== this) { ctx.save(); ctx.transform(...invertTransform(this.calcTransformMatrix())); - this._objects[i].render(ctx); + object.render(ctx); ctx.restore(); - } else if (this._objects[i].group === this) { - this._objects[i].render(ctx); + } else if ( + object.group === this && + (preserve || !this._activeObjects.includes(object)) + // && (!object.skipOffscreen || object.isOverlapping(this)) + ) { + object.render(ctx); } - } + }); this._drawClipPath(ctx, this.clipPath); } - /** - * @override - * @return {Boolean} - */ - setCoords() { - super.setCoords(); - this._shouldSetNestedCoords() && - this.forEachObject((object) => object.setCoords()); - } - triggerLayout(options: ImperativeLayoutOptions = {}) { this.layoutManager.performLayout({ target: this, @@ -689,7 +687,6 @@ export class Group target: group, targets: group.getObjects(), }); - group.setCoords(); return group; }); } diff --git a/src/shapes/IText/IText.test.ts b/src/shapes/IText/IText.test.ts index 56e2bc054ad..02d59918f3e 100644 --- a/src/shapes/IText/IText.test.ts +++ b/src/shapes/IText/IText.test.ts @@ -24,7 +24,6 @@ describe('IText', () => { }); const group = new Group([text]); group.set({ scaleX: scale, scaleY: scale, angle }); - group.setCoords(); const fillRect = jest.fn(); const getZoom = jest.fn().mockReturnValue(zoom); const mockContext = { fillRect }; diff --git a/src/shapes/IText/ITextBehavior.ts b/src/shapes/IText/ITextBehavior.ts index 8bfef78f9bf..447d54f2795 100644 --- a/src/shapes/IText/ITextBehavior.ts +++ b/src/shapes/IText/ITextBehavior.ts @@ -536,7 +536,6 @@ export abstract class ITextBehavior< this.text = textarea.value; this.set('dirty', true); this.initDimensions(); - this.setCoords(); const newSelection = this.fromStringToGraphemeSelection( textarea.selectionStart, textarea.selectionEnd, @@ -693,7 +692,6 @@ export abstract class ITextBehavior< this._restoreEditingProps(); if (this._forceClearCache) { this.initDimensions(); - this.setCoords(); } this.fire('editing:exited'); isTextChanged && this.fire('modified'); @@ -1010,7 +1008,6 @@ export abstract class ITextBehavior< this.text = this._text.join(''); this.set('dirty', true); this.initDimensions(); - this.setCoords(); this._removeExtraneousStyles(); } @@ -1045,7 +1042,6 @@ export abstract class ITextBehavior< this.text = this._text.join(''); this.set('dirty', true); this.initDimensions(); - this.setCoords(); this._removeExtraneousStyles(); } diff --git a/src/shapes/Image.ts b/src/shapes/Image.ts index b7e4c502d02..9e496c171f0 100644 --- a/src/shapes/Image.ts +++ b/src/shapes/Image.ts @@ -220,7 +220,7 @@ export class FabricImage< /** * Sets image element for this instance to a specified one. * If filters defined they are applied to new image. - * You might need to call `canvas.renderAll` and `object.setCoords` after replacing, to render new image and update controls area. + * You might need to call `canvas.renderAll` after replacing, to render new image and update controls area. * @param {HTMLImageElement} element * @param {Partial} [size] Options object */ @@ -677,8 +677,11 @@ export class FabricImage< */ _setWidthHeight({ width, height }: Partial = {}) { const size = this.getOriginalSize(); + const { width: prevWidth, height: prevHeight } = this; this.width = width || size.width; this.height = height || size.height; + (prevWidth !== this.width || prevHeight !== this.height) && + this.invalidateCoords(); } /** diff --git a/src/shapes/Line.ts b/src/shapes/Line.ts index 953a3af8340..ac44932ecbc 100644 --- a/src/shapes/Line.ts +++ b/src/shapes/Line.ts @@ -8,7 +8,7 @@ import { isFiller } from '../util/typeAssertions'; import type { FabricObjectProps, SerializedObjectProps } from './Object/types'; import type { ObjectEvents } from '../EventTypeDefs'; import { makeBoundingBoxFromPoints } from '../util'; -import { CENTER, LEFT, TOP } from '../constants'; +import { LEFT, TOP } from '../constants'; import type { CSSRules } from '../parser/typedefs'; // @TODO this code is terrible and Line should be a special case of polyline. @@ -92,7 +92,7 @@ export class Line< { x: x2, y: y2 }, ]); const position = new Point(left + width / 2, top + height / 2); - this.setPositionByOrigin(position, CENTER, CENTER); + this.setRelativeCenterPoint(position); } /** @@ -166,23 +166,6 @@ export class Line< }; } - /* - * Calculate object dimensions from its properties - * @private - */ - _getNonTransformedDimensions(): Point { - const dim = super._getNonTransformedDimensions(); - if (this.strokeLineCap === 'butt') { - if (this.width === 0) { - dim.y -= this.strokeWidth; - } - if (this.height === 0) { - dim.x -= this.strokeWidth; - } - } - return dim; - } - /** * Recalculates line points given width and height * Those points are simply placed around the center, diff --git a/src/shapes/Object/AnimatableObject.ts b/src/shapes/Object/AnimatableObject.ts index 08456f105ee..d72f6ae2eaf 100644 --- a/src/shapes/Object/AnimatableObject.ts +++ b/src/shapes/Object/AnimatableObject.ts @@ -87,7 +87,7 @@ export abstract class AnimatableObject< valueProgress: number, durationProgress: number ) => { - this.setCoords(); + this.invalidateCoords(); onComplete && // @ts-expect-error generic callback arg0 is wrong onComplete(value, valueProgress, durationProgress); diff --git a/src/shapes/Object/BBox.ts b/src/shapes/Object/BBox.ts new file mode 100644 index 00000000000..722fcdeb2dc --- /dev/null +++ b/src/shapes/Object/BBox.ts @@ -0,0 +1,451 @@ +import type { StaticCanvas } from '../../canvas/StaticCanvas'; +import { iMatrix } from '../../constants'; +import { Intersection } from '../../Intersection'; +import { Point } from '../../Point'; +import { + TBBox, + TCornerPoint, + TMat2D, + TOriginX, + TOriginY, +} from '../../typedefs'; +import { mapValues } from '../../util/internals'; +import { makeBoundingBoxFromPoints } from '../../util/misc/boundingBoxFromPoints'; +import { + invertTransform, + multiplyTransformMatrices, +} from '../../util/misc/matrix'; +import { + calcBaseChangeMatrix, + sendPointToPlane, +} from '../../util/misc/planeChange'; +import { radiansToDegrees } from '../../util/misc/radiansDegreesConversion'; +import { resolveOrigin } from '../../util/misc/resolveOrigin'; +import { calcVectorRotation, createVector } from '../../util/misc/vectors'; +import type { ObjectGeometry } from './ObjectGeometry'; + +export type OriginDiff = { x: TOriginX; y: TOriginY }; + +const CENTER_ORIGIN = { x: 'center', y: 'center' } as const; + +export class PlaneBBox { + private readonly originTransformation: TMat2D; + + static getTransformation(coords: TCornerPoint) { + return calcBaseChangeMatrix( + undefined, + [createVector(coords.tl, coords.tr), createVector(coords.tl, coords.bl)], + coords.tl.midPointFrom(coords.br) + ); + } + + static build(coords: TCornerPoint) { + return new this(this.getTransformation(coords)); + } + + static rect({ left, top, width, height }: TBBox) { + const transform = calcBaseChangeMatrix( + undefined, + [new Point(width, 0), new Point(0, height)], + new Point(left + width / 2, top + height / 2) + ); + return new this(transform); + } + + protected constructor(transform: TMat2D) { + this.originTransformation = Object.freeze([...transform]) as TMat2D; + } + + getTransformation() { + return this.originTransformation; + } + + getCoordMap() { + return mapValues( + { + tl: new Point(-0.5, -0.5), + tr: new Point(0.5, -0.5), + br: new Point(0.5, 0.5), + bl: new Point(-0.5, 0.5), + }, + (origin) => this.pointFromOrigin(origin) + ); + } + + getCoords() { + return Object.values(this.getCoordMap()); + } + + getBBox() { + return makeBoundingBoxFromPoints(this.getCoords()); + } + + getCenterPoint() { + return this.pointFromOrigin(new Point()); + } + + getDimensionsVector() { + const { width, height } = this.getBBox(); + return new Point(width, height); + } + + static calcRotation({ tl, tr }: Record<'tl' | 'tr' | 'bl' | 'br', Point>) { + return calcVectorRotation(createVector(tl, tr)); + } + + getRotation() { + return PlaneBBox.calcRotation(this.getCoordMap()); + } + + pointFromOrigin(origin: Point) { + return origin.transform(this.getTransformation()); + } + + pointToOrigin(point: Point) { + return point.transform(invertTransform(this.getTransformation())); + } + + vectorFromOrigin(originVector: Point) { + return originVector.transform(this.getTransformation(), true); + } + + vectorToOrigin(vector: Point) { + return vector.transform(invertTransform(this.getTransformation()), true); + } + + static resolveOrigin({ x, y }: OriginDiff): Point { + return new Point(resolveOrigin(x), resolveOrigin(y)); + } + + static getOriginDiff( + from: OriginDiff = CENTER_ORIGIN, + to: OriginDiff = CENTER_ORIGIN + ) { + return PlaneBBox.resolveOrigin(to).subtract(PlaneBBox.resolveOrigin(from)); + } + + vectorFromOriginDiff(from?: OriginDiff, to?: OriginDiff) { + return this.vectorFromOrigin(PlaneBBox.getOriginDiff(from, to)); + } + + calcOriginTranslation(origin: Point, prev: this) { + return this.pointFromOrigin(origin).subtract(prev.pointFromOrigin(origin)); + } + + containsPoint(point: Point) { + const pointAsOrigin = this.pointToOrigin(point); + return ( + pointAsOrigin.x >= -0.5 && + pointAsOrigin.x <= 0.5 && + pointAsOrigin.y >= -0.5 && + pointAsOrigin.y <= 0.5 + ); + } + + transform(ctx: CanvasRenderingContext2D) { + ctx.transform(...this.getTransformation()); + } +} + +export interface ViewportBBoxPlanes { + viewport(): TMat2D; +} + +export class ViewportBBox extends PlaneBBox { + protected readonly planes: ViewportBBoxPlanes; + + protected constructor(transform: TMat2D, planes: ViewportBBoxPlanes) { + super(transform); + this.planes = planes; + } + + sendToPlane(plane: TMat2D) { + const backToPlane = invertTransform( + multiplyTransformMatrices(this.planes.viewport(), plane) + ); + return new PlaneBBox( + multiplyTransformMatrices(backToPlane, this.getTransformation()) + ); + } + + sendToCanvas() { + return this.sendToPlane(iMatrix); + } + + intersect(other: ViewportBBox) { + const coords = Object.values(this.getCoordMap()); + const otherCoords = Object.values(other.getCoordMap()); + return Intersection.intersectPolygonPolygon(coords, otherCoords); + } + + intersects(other: ViewportBBox) { + const intersection = this.intersect(other); + return ( + intersection.status === 'Intersection' || + intersection.status === 'Coincident' + ); + } + + contains(other: ViewportBBox) { + const otherCoords = Object.values(other.getCoordMap()); + return otherCoords.every((coord) => this.containsPoint(coord)); + } + + isContainedBy(other: ViewportBBox) { + return other.contains(this); + } + + overlaps(other: ViewportBBox) { + return ( + this.intersects(other) || + this.contains(other) || + this.isContainedBy(other) + ); + } +} + +export class CanvasBBox extends ViewportBBox { + static getViewportCoords(canvas: StaticCanvas) { + const size = new Point(canvas.width, canvas.height); + return mapValues( + { + tl: new Point(-0.5, -0.5), + tr: new Point(0.5, -0.5), + br: new Point(0.5, 0.5), + bl: new Point(-0.5, 0.5), + }, + (coord) => coord.multiply(size).transform(canvas.viewportTransform) + ); + } + + static getPlanes(canvas: StaticCanvas) { + const vpt: TMat2D = [...canvas.viewportTransform]; + return { + viewport() { + return vpt; + }, + }; + } + + static transformed(canvas: StaticCanvas) { + return new this( + this.getTransformation(this.getViewportCoords(canvas)), + this.getPlanes(canvas) + ); + } + + static bbox(canvas: StaticCanvas) { + const coords = this.getViewportCoords(canvas); + const bbox = makeBoundingBoxFromPoints(Object.values(coords)); + const transform = calcBaseChangeMatrix( + undefined, + [new Point(bbox.width, 0), new Point(0, bbox.height)], + coords.tl.midPointFrom(coords.br) + ); + return new this(transform, this.getPlanes(canvas)); + } +} + +export interface BBoxPlanes extends ViewportBBoxPlanes { + retina(): TMat2D; + parent(): TMat2D; + self(): TMat2D; +} + +export type TRotatedBBox = ReturnType; + +export class BBox extends ViewportBBox { + protected declare readonly planes: BBoxPlanes; + + protected constructor(transform: TMat2D, planes: BBoxPlanes) { + super(transform, planes); + } + + sendToParent() { + return this.sendToPlane(this.planes.parent()); + } + + sendToSelf() { + return this.sendToPlane(this.planes.self()); + } + + // preMultiply(transform: TMat2D) { + // const parent = this.planes.parent(); + // const ownPreTransform = multiplyTransformMatrixArray([ + // invertTransform(parent), + // transform, + // parent, + // ]); + // const self = multiplyTransformMatrixArray([ + // parent, + // this.getOwnTransform(), + // ownPreTransform, + // ]); + // return new BBox(this.getTransformation(), { + // ...this.planes, + // self() { + // return self; + // }, + // }); + // } + + // getOwnTransform() { + // return calcPlaneChangeMatrix(this.planes.self(), this.planes.parent()); + // } + + static getViewportCoords(target: ObjectGeometry) { + const coords = target.calcCoords(); + if (target.needsViewportCoords()) { + return coords; + } else { + const vpt = target.getViewportTransform(); + return mapValues(coords, (coord) => sendPointToPlane(coord, vpt)); + } + } + + static buildBBoxPlanes(target: ObjectGeometry): BBoxPlanes { + const self = target.calcTransformMatrix(); + const parent = target.group?.calcTransformMatrix() || iMatrix; + const viewport = target.getViewportTransform(); + const retina = target.canvas?.getRetinaScaling() || 1; + return { + self() { + return self; + }, + parent() { + return parent; + }, + viewport() { + return viewport; + }, + retina() { + return [retina, 0, 0, retina, 0, 0] as TMat2D; + }, + }; + } + + static canvas(target: ObjectGeometry) { + const coords = this.getViewportCoords(target); + const bbox = makeBoundingBoxFromPoints(Object.values(coords)); + const transform = calcBaseChangeMatrix( + undefined, + [new Point(bbox.width, 0), new Point(0, bbox.height)], + coords.tl.midPointFrom(coords.br) + ); + return new this(transform, this.buildBBoxPlanes(target)); + } + + static rotated(target: ObjectGeometry) { + const coords = this.getViewportCoords(target); + const rotation = this.calcRotation(coords); + const center = coords.tl.midPointFrom(coords.br); + const bbox = makeBoundingBoxFromPoints( + Object.values(coords).map((coord) => coord.rotate(-rotation, center)) + ); + const transform = calcBaseChangeMatrix( + undefined, + [ + new Point(bbox.width, 0).rotate(rotation), + new Point(0, bbox.height).rotate(rotation), + ], + center + ); + return Object.assign(new this(transform, this.buildBBoxPlanes(target)), { + // angle, + rotation, + }); + } + + static legacy(target: ObjectGeometry) { + const coords = this.getViewportCoords(target); + const rotation = this.calcRotation(coords); + const center = coords.tl.midPointFrom(coords.br); + const viewportBBox = makeBoundingBoxFromPoints(Object.values(coords)); + const rotatedBBox = makeBoundingBoxFromPoints( + Object.values(coords).map((coord) => coord.rotate(-rotation, center)) + ); + const bboxTransform = calcBaseChangeMatrix( + undefined, + [ + new Point(rotatedBBox.width / viewportBBox.width, 0), + new Point(0, rotatedBBox.height / viewportBBox.height), + ], + center + ); + const legacyCoords = mapValues(coords, (coord) => + coord.transform(bboxTransform) + ); + const legacyBBox = makeBoundingBoxFromPoints(Object.values(legacyCoords)); + const transform = calcBaseChangeMatrix( + undefined, + [new Point(1, 0).rotate(rotation), new Point(0, 1).rotate(rotation)], + center + ); + return { + angle: radiansToDegrees(rotation), + rotation, + getCoords() { + return legacyCoords; + }, + getTransformation() { + return transform; + }, + getBBox() { + return legacyBBox; + }, + getDimensionsVector() { + return new Point(legacyBBox.width, legacyBBox.height); + }, + transform(ctx: CanvasRenderingContext2D) { + ctx.transform(...transform); + }, + }; + } + + static transformed(target: ObjectGeometry) { + const coords = this.getViewportCoords(target); + const transform = calcBaseChangeMatrix( + undefined, + [createVector(coords.tl, coords.tr), createVector(coords.tl, coords.bl)], + coords.tl.midPointFrom(coords.br) + ); + return new this(transform, this.buildBBoxPlanes(target)); + } +} + +/** + * Perf opt + */ +export class OwnBBox extends BBox { + constructor(transform: TMat2D, planes: BBoxPlanes) { + super(transform, planes); + } + + getCoordMap() { + const from = multiplyTransformMatrices( + this.planes.viewport(), + this.planes.self() + ); + return mapValues(super.getCoordMap(), (coord) => + sendPointToPlane(coord, from) + ); + } + + static buildBBoxPlanes(target: ObjectGeometry): BBoxPlanes { + return { + self() { + return target.calcTransformMatrix(); + }, + parent() { + return target.group?.calcTransformMatrix() || iMatrix; + }, + viewport() { + return target.getViewportTransform(); + }, + retina() { + const retina = target.canvas?.getRetinaScaling() || 1; + return [retina, 0, 0, retina, 0, 0] as TMat2D; + }, + }; + } +} diff --git a/src/shapes/Object/FabricObject.spec.ts b/src/shapes/Object/FabricObject.spec.ts index cd011e706ed..2b2a2d88b31 100644 --- a/src/shapes/Object/FabricObject.spec.ts +++ b/src/shapes/Object/FabricObject.spec.ts @@ -3,14 +3,14 @@ import { FabricObject } from './FabricObject'; describe('FabricObject', () => { it('setCoords should calculate control coords only if canvas ref is set', () => { const object = new FabricObject(); - expect(object.aCoords).toBeUndefined(); - expect(object.oCoords).toBeUndefined(); + expect(object['bboxCoords']).toBeUndefined(); + expect(object['controlCoords']).toBeUndefined(); object.setCoords(); - expect(object.aCoords).toBeDefined(); - expect(object.oCoords).toBeUndefined(); + expect(object['bboxCoords']).toBeDefined(); + expect(object['controlCoords']).toBeUndefined(); object.canvas = jest.fn(); object.setCoords(); - expect(object.aCoords).toBeDefined(); - expect(object.oCoords).toBeDefined(); + expect(object['bboxCoords']).toBeDefined(); + expect(object['controlCoords']).toBeDefined(); }); }); diff --git a/src/shapes/Object/InteractiveObject.spec.ts b/src/shapes/Object/InteractiveObject.spec.ts index 1d83e7690af..f0ade2466c9 100644 --- a/src/shapes/Object/InteractiveObject.spec.ts +++ b/src/shapes/Object/InteractiveObject.spec.ts @@ -1,9 +1,10 @@ import { Canvas } from '../../canvas/Canvas'; import { Control } from '../../controls/Control'; import { radiansToDegrees } from '../../util'; +import { calcPlaneRotation } from '../../util/misc/matrix'; import { Group } from '../Group'; import { FabricObject } from './FabricObject'; -import { InteractiveFabricObject, type TOCoord } from './InteractiveObject'; +import { InteractiveFabricObject } from './InteractiveObject'; describe('InteractiveObject', () => { it('tests constructor & properties', () => { @@ -35,10 +36,12 @@ describe('InteractiveObject', () => { subTargetCheck: true, }); canvas.add(group); - group.setCoords(); - const objectAngle = Math.round(object.getTotalAngle()); - expect(objectAngle).toEqual(35); - Object.values(object.oCoords).forEach((cornerPoint: TOCoord) => { + expect( + Math.round( + radiansToDegrees(calcPlaneRotation(object.calcTransformMatrix())) + ) + ).toEqual(35); + Object.values(object.getControlCoords()).forEach((cornerPoint) => { const controlAngle = Math.round( radiansToDegrees( Math.atan2( @@ -47,7 +50,7 @@ describe('InteractiveObject', () => { ) ) ); - expect(controlAngle).toEqual(objectAngle); + expect(controlAngle).toEqual(32); }); }); }); @@ -62,7 +65,7 @@ describe('InteractiveObject', () => { expect(object.getActiveControl()).toEqual({ key: 'control', control, - coord: object.oCoords.control, + coord: object['controlCoords']!.control, }); }); }); diff --git a/src/shapes/Object/InteractiveObject.ts b/src/shapes/Object/InteractiveObject.ts index 1a8c1b9b9e8..b2fefde23e3 100644 --- a/src/shapes/Object/InteractiveObject.ts +++ b/src/shapes/Object/InteractiveObject.ts @@ -1,17 +1,6 @@ -import { Point, ZERO } from '../../Point'; +import { Point } from '../../Point'; import type { TCornerPoint, TDegree } from '../../typedefs'; -import { FabricObject } from './Object'; -import { degreesToRadians } from '../../util/misc/radiansDegreesConversion'; -import type { TQrDecomposeOut } from '../../util/misc/matrix'; -import { - calcDimensionsMatrix, - createRotateMatrix, - createTranslateMatrix, - multiplyTransformMatrices, - qrDecompose, -} from '../../util/misc/matrix'; import type { Control } from '../../controls/Control'; -import { sizeAfterTransform } from '../../util/misc/objectTransforms'; import type { ObjectEvents, TPointerEvent } from '../../EventTypeDefs'; import type { Canvas } from '../../canvas/Canvas'; import type { ControlRenderingStyleOverride } from '../../controls/controlRendering'; @@ -19,8 +8,13 @@ import type { FabricObjectProps } from './types/FabricObjectProps'; import type { TFabricObjectProps, SerializedObjectProps } from './types'; import { createObjectDefaultControls } from '../../controls/commonControls'; import { interactiveObjectDefaultValues } from './defaultValues'; +import { mapValues } from '../../util/internals'; +import { BBox } from '../../BBox/BBox'; +import { FabricObject as BaseFabricObject } from './Object'; -export type TOCoord = Point & { +export type TControlCoord = { + position: Point; + connection: { from: Point; to: Point }; corner: TCornerPoint; touchCorner: TCornerPoint; }; @@ -44,7 +38,7 @@ export class InteractiveFabricObject< SProps extends SerializedObjectProps = SerializedObjectProps, EventSpec extends ObjectEvents = ObjectEvents > - extends FabricObject + extends BaseFabricObject implements FabricObjectProps { declare noScaleCache: boolean; @@ -91,7 +85,7 @@ export class InteractiveFabricObject< * `corner/touchCorner` describe the 4 points forming the interactive area of the corner. * Used to draw and locate controls. */ - declare oCoords: Record; + protected declare controlCoords?: Record; /** * keeps the value of the last hovered corner during mouse move. @@ -167,13 +161,19 @@ export class InteractiveFabricObject< return super._updateCacheCanvas(); } + getControlCoords() { + return ( + this.controlCoords || (this.controlCoords = this.calcControlCoords()) + ); + } + getActiveControl() { const key = this.__corner; return key ? { key, control: this.controls[key], - coord: this.oCoords[key], + coord: this.getControlCoords()[key], } : undefined; } @@ -187,34 +187,35 @@ export class InteractiveFabricObject< * @private * @param {Object} pointer The pointer indicating the mouse position * @param {boolean} forTouch indicates if we are looking for interaction area with a touch action - * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or 0 if nothing is found. + * @return {String} corner code (tl, tr, bl, br, etc.), or an empty string if nothing is found. */ findControl( pointer: Point, forTouch = false - ): { key: string; control: Control; coord: TOCoord } | undefined { + ): { key: string; control: Control; coord: TControlCoord } | undefined { if (!this.hasControls || !this.canvas) { return undefined; } this.__corner = undefined; - const cornerEntries = Object.entries(this.oCoords); - for (let i = cornerEntries.length - 1; i >= 0; i--) { - const [key, corner] = cornerEntries[i]; + const coords = this.getControlCoords(); + for (const [key, coord] of Object.entries(coords)) { const control = this.controls[key]; - if ( + // PlaneBBox.build(forTouch ? coord.touchCorner : coord.corner).containsPoint( + // pointer, + // ) control.shouldActivate( key, this, pointer, - forTouch ? corner.touchCorner : corner.corner + forTouch ? coord.touchCorner : coord.corner ) ) { // this.canvas.contextTop.fillRect(pointer.x - 1, pointer.y - 1, 2, 2); this.__corner = key; - return { key, control, coord: this.oCoords[key] }; + return { key, control, coord }; } } @@ -226,99 +227,82 @@ export class InteractiveFabricObject< * This basically just delegates to each control positionHandler * WARNING: changing what is passed to positionHandler is a breaking change, since position handler * is a public api and should be done just if extremely necessary - * @return {Record} + * @return {Record} */ - calcOCoords(): Record { - const vpt = this.getViewportTransform(), - center = this.getCenterPoint(), - tMatrix = createTranslateMatrix(center.x, center.y), - rMatrix = createRotateMatrix({ - angle: this.getTotalAngle() - (!!this.group && this.flipX ? 180 : 0), - }), - positionMatrix = multiplyTransformMatrices(tMatrix, rMatrix), - startMatrix = multiplyTransformMatrices(vpt, positionMatrix), - finalMatrix = multiplyTransformMatrices(startMatrix, [ - 1 / vpt[0], - 0, - 0, - 1 / vpt[3], - 0, - 0, - ]), - transformOptions = this.group - ? qrDecompose(this.calcTransformMatrix()) - : undefined; - // decomposing could bring negative scaling and `_calculateCurrentDimensions` can't take it - if (transformOptions) { - transformOptions.scaleX = Math.abs(transformOptions.scaleX); - transformOptions.scaleY = Math.abs(transformOptions.scaleY); - } - const dim = this._calculateCurrentDimensions(transformOptions), - coords: Record = {}; - - this.forEachControl((control, key) => { - const position = control.positionHandler(dim, finalMatrix, this, control); - // coords[key] are sometimes used as points. Those are points to which we add - // the property corner and touchCorner from `_calcCornerCoords`. - // don't remove this assign for an object spread. - coords[key] = Object.assign( - position, - this._calcCornerCoords(control, position) + protected calcControlCoords(): Record { + const legacyBBox = BBox.legacy(this); + const coords = mapValues(this.controls, (control) => { + const position = control.positionHandler( + legacyBBox.getBBoxVector(), + legacyBBox.getTransformation(), + // @ts-expect-error FabricObject this + this, + control ); + return { + position, + connection: control.connectionPositionHandler( + position, + // @ts-expect-error FabricObject this + this, + control + ), + // Sets the coordinates that determine the interaction area of each control + // note: if we would switch to ROUND corner area, all of this would disappear. + // everything would resolve to a single point and a pythagorean theorem for the distance + // @todo evaluate simplification of code switching to circle interaction area at runtime + corner: control.calcCornerCoords( + legacyBBox.angle, + this.cornerSize, + position.x, + position.y, + false, + this + ), + touchCorner: control.calcCornerCoords( + legacyBBox.angle, + this.touchCornerSize, + position.x, + position.y, + true, + this + ), + }; }); - // debug code - /* - const canvas = this.canvas; - setTimeout(function () { - if (!canvas) return; - canvas.contextTop.clearRect(0, 0, 700, 700); - canvas.contextTop.fillStyle = 'green'; - Object.keys(coords).forEach(function(key) { - const control = coords[key]; - canvas.contextTop.fillRect(control.x, control.y, 3, 3); - }); - } 50); - */ - return coords; - } + // // debug code + // setTimeout(() => { + // const canvas = this.canvas; + // if (!canvas) return; + // const ctx = canvas.contextTop; + // // canvas.clearContext(ctx); + // ctx.fillStyle = 'cyan'; + // Object.keys(coords).forEach((key) => { + // Object.keys(coords[key].corner).forEach((k) => { + // const control = coords[key].corner[k]; + // ctx.beginPath(); + // ctx.ellipse(control.x, control.y, 3, 3, 0, 0, 360); + // ctx.closePath(); + // ctx.fill(); + // }); + // }); + // }, 50); - /** - * Sets the coordinates that determine the interaction area of each control - * note: if we would switch to ROUND corner area, all of this would disappear. - * everything would resolve to a single point and a pythagorean theorem for the distance - * @todo evaluate simplification of code switching to circle interaction area at runtime - * @private - */ - private _calcCornerCoords(control: Control, position: Point) { - const angle = this.getTotalAngle(); - const corner = control.calcCornerCoords( - angle, - this.cornerSize, - position.x, - position.y, - false, - this - ); - const touchCorner = control.calcCornerCoords( - angle, - this.touchCornerSize, - position.x, - position.y, - true, - this - ); - return { corner, touchCorner }; + return coords; } /** * @override set controls' coordinates as well - * See {@link https://github.com/fabricjs/fabric.js/wiki/When-to-call-setCoords} and {@link http://fabricjs.com/fabric-gotchas} * @return {void} */ setCoords(): void { super.setCoords(); - this.canvas && (this.oCoords = this.calcOCoords()); + this.canvas && (this.controlCoords = this.calcControlCoords()); + } + + invalidateCoords() { + super.invalidateCoords(); + delete this.controlCoords; } /** @@ -326,16 +310,14 @@ export class InteractiveFabricObject< * with the control, the control's key and the object that is calling the iterator * @param {Function} fn function to iterate over the controls over */ - forEachControl( + forEachControl( fn: ( control: Control, key: string, fabricObject: InteractiveFabricObject - ) => any - ) { - for (const i in this.controls) { - fn(this.controls[i], i, this); - } + ) => R + ): Record { + return mapValues(this.controls, (value, key) => fn(value, key, this)); } /** @@ -356,48 +338,58 @@ export class InteractiveFabricObject< return; } ctx.save(); - const center = this.getRelativeCenterPoint(), - wh = this._calculateCurrentDimensions(), - vpt = this.getViewportTransform(); - ctx.translate(center.x, center.y); - ctx.scale(1 / vpt[0], 1 / vpt[3]); - ctx.rotate(degreesToRadians(this.angle)); + this.bbox.sendToCanvas().transform(ctx); ctx.fillStyle = this.selectionBackgroundColor; - ctx.fillRect(-wh.x / 2, -wh.y / 2, wh.x, wh.y); + ctx.fillRect(-0.5, -0.5, 1, 1); ctx.restore(); } /** * @public override this function in order to customize the drawing of the control box, e.g. rounded corners, different border style. - * @param {CanvasRenderingContext2D} ctx ctx is rotated and translated so that (0,0) is at object's center + * @param {CanvasRenderingContext2D} ctx ctx is not transformed, only retina scaled * @param {Point} size the control box size used */ - strokeBorders(ctx: CanvasRenderingContext2D, size: Point): void { - ctx.strokeRect(-size.x / 2, -size.y / 2, size.x, size.y); + strokeBorders(ctx: CanvasRenderingContext2D): void { + ctx.save(); + this.bbox.transform(ctx); + ctx.beginPath(); + ctx.moveTo(-0.5, -0.5); + ctx.lineTo(0.5, -0.5); + ctx.lineTo(0.5, 0.5); + ctx.lineTo(-0.5, 0.5); + ctx.closePath(); + ctx.restore(); + ctx.stroke(); } /** - * @private + * Draws borders of an object's bounding box. + * Requires public properties: width, height + * Requires public options: padding, borderColor * @param {CanvasRenderingContext2D} ctx Context to draw on - * @param {Point} size - * @param {TStyleOverride} styleOverride object to override the object style + * @param {object} options object representing current object parameters + * @param {TStyleOverride} [styleOverride] object to override the object style */ - _drawBorders( + drawBorders( ctx: CanvasRenderingContext2D, - size: Point, - styleOverride: TStyleOverride = {} + styleOverride: TStyleOverride ): void { - const options = { - hasControls: this.hasControls, + const { borderColor, borderDashArray } = { borderColor: this.borderColor, borderDashArray: this.borderDashArray, ...styleOverride, }; ctx.save(); - ctx.strokeStyle = options.borderColor; - this._setLineDash(ctx, options.borderDashArray); - this.strokeBorders(ctx, size); - options.hasControls && this.drawControlsConnectingLines(ctx, size); + ctx.strokeStyle = borderColor; + this._setLineDash(ctx, borderDashArray); + // ctx.lineWidth = this.borderScaleFactor; + // // TODO: remove legacy? + // ctx.save(); + // const legacy = BBox.legacy(this); + // legacy.transform(ctx); + // this.strokeBordersLegacy(ctx, legacy.getBBoxVector()); + // ctx.restore(); + this.strokeBorders(ctx); ctx.restore(); } @@ -413,106 +405,19 @@ export class InteractiveFabricObject< styleOverride: TStyleOverride = {} ) { const { hasBorders, hasControls } = this; - const styleOptions = { + const { hasBorders: shouldDrawBorders, hasControls: shouldDrawControls } = { hasBorders, hasControls, ...styleOverride, }; - const vpt = this.getViewportTransform(), - shouldDrawBorders = styleOptions.hasBorders, - shouldDrawControls = styleOptions.hasControls; - const matrix = multiplyTransformMatrices(vpt, this.calcTransformMatrix()); - const options = qrDecompose(matrix); ctx.save(); - ctx.translate(options.translateX, options.translateY); - ctx.lineWidth = 1 * this.borderScaleFactor; - // since interactive groups have been introduced, an object could be inside a group and needing controls - // the following equality check `this.group === this.parent` covers: - // object without a group ( undefined === undefined ) - // object inside a group - // excludes object inside a group but multi selected since group and parent will differ in value - if (this.group === this.parent) { - ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; - } - if (this.flipX) { - options.angle -= 180; - } - ctx.rotate(degreesToRadians(this.group ? options.angle : this.angle)); - shouldDrawBorders && this.drawBorders(ctx, options, styleOverride); + ctx.globalAlpha = + this.isMoving || this.group?.isMoving ? this.borderOpacityWhenMoving : 1; + shouldDrawBorders && this.drawBorders(ctx, styleOverride); shouldDrawControls && this.drawControls(ctx, styleOverride); ctx.restore(); } - /** - * Draws borders of an object's bounding box. - * Requires public properties: width, height - * Requires public options: padding, borderColor - * @param {CanvasRenderingContext2D} ctx Context to draw on - * @param {object} options object representing current object parameters - * @param {TStyleOverride} [styleOverride] object to override the object style - */ - drawBorders( - ctx: CanvasRenderingContext2D, - options: TQrDecomposeOut, - styleOverride: TStyleOverride - ): void { - let size; - if ((styleOverride && styleOverride.forActiveSelection) || this.group) { - const bbox = sizeAfterTransform( - this.width, - this.height, - calcDimensionsMatrix(options) - ), - stroke = !this.isStrokeAccountedForInDimensions() - ? (this.strokeUniform - ? new Point().scalarAdd(this.canvas ? this.canvas.getZoom() : 1) - : // this is extremely confusing. options comes from the upper function - // and is the qrDecompose of a matrix that takes in account zoom too - new Point(options.scaleX, options.scaleY) - ).scalarMultiply(this.strokeWidth) - : ZERO; - size = bbox - .add(stroke) - .scalarAdd(this.borderScaleFactor) - .scalarAdd(this.padding * 2); - } else { - size = this._calculateCurrentDimensions().scalarAdd( - this.borderScaleFactor - ); - } - this._drawBorders(ctx, size, styleOverride); - } - - /** - * Draws lines from a borders of an object's bounding box to controls that have `withConnection` property set. - * Requires public properties: width, height - * Requires public options: padding, borderColor - * @param {CanvasRenderingContext2D} ctx Context to draw on - * @param {Point} size object size x = width, y = height - */ - drawControlsConnectingLines( - ctx: CanvasRenderingContext2D, - size: Point - ): void { - let shouldStroke = false; - - ctx.beginPath(); - this.forEachControl((control, key) => { - // in this moment, the ctx is centered on the object. - // width and height of the above function are the size of the bbox. - if (control.withConnection && control.getVisibility(this, key)) { - // reset movement for each control - shouldStroke = true; - ctx.moveTo(control.x * size.x, control.y * size.y); - ctx.lineTo( - control.x * size.x + control.offsetX, - control.y * size.y + control.offsetY - ); - } - }); - shouldStroke && ctx.stroke(); - } - /** * Draws corners of an object's bounding box. * Requires public properties: width, height @@ -539,11 +444,10 @@ export class InteractiveFabricObject< ctx.strokeStyle = options.cornerStrokeColor; } this._setLineDash(ctx, options.cornerDashArray); - this.setCoords(); + const coords = this.getControlCoords(); this.forEachControl((control, key) => { if (control.getVisibility(this, key)) { - const p = this.oCoords[key]; - control.render(ctx, p.x, p.y, options, this); + control.renderControl(ctx, coords[key], options, this); } }); ctx.restore(); @@ -626,7 +530,7 @@ export class InteractiveFabricObject< * try to to deselect this object. If the function returns true, the process is cancelled * @param {Object} [options] options sent from the upper functions * @param {TPointerEvent} [options.e] event if the process is generated by an event - * @param {FabricObject} [options.object] next object we are setting as active, and reason why + * @param {BaseFabricObject} [options.object] next object we are setting as active, and reason why * this is being deselected */ onDeselect(options?: { diff --git a/src/shapes/Object/Object.ts b/src/shapes/Object/Object.ts index e7c9ef58e80..9ad92708455 100644 --- a/src/shapes/Object/Object.ts +++ b/src/shapes/Object/Object.ts @@ -1,21 +1,11 @@ -import { cache } from '../../cache'; import { config } from '../../config'; -import { - ALIASING_LIMIT, - CENTER, - iMatrix, - LEFT, - TOP, - VERSION, -} from '../../constants'; +import { ALIASING_LIMIT, iMatrix, LEFT, TOP, VERSION } from '../../constants'; import type { ObjectEvents } from '../../EventTypeDefs'; import { AnimatableObject } from './AnimatableObject'; import { Point } from '../../Point'; import { Shadow } from '../../Shadow'; import type { - TDegree, TFiller, - TSize, TCacheCanvasDimensions, Abortable, TOptions, @@ -31,6 +21,7 @@ import { enlivenObjectEnlivables } from '../../util/misc/objectEnlive'; import { resetObjectTransform, saveObjectTransform, + sizeAfterTransform, } from '../../util/misc/objectTransforms'; import { sendObjectToPlane } from '../../util/misc/planeChange'; import { pick, pickBy } from '../../util/misc/pick'; @@ -46,6 +37,7 @@ import type { FabricImage } from '../Image'; import { cacheProperties, fabricObjectDefaultValues, + geometryProperties, stateProperties, } from './defaultValues'; import type { Gradient } from '../../gradient/Gradient'; @@ -55,6 +47,7 @@ import type { SerializedObjectProps } from './types/SerializedObjectProps'; import type { ObjectProps } from './types/ObjectProps'; import { getDevicePixelRatio, getEnv } from '../../env'; import { log } from '../../util/internals/console'; +import { BBox } from '../../BBox/BBox'; export type TCachedFabricObject = T & Required< @@ -184,6 +177,8 @@ export class FabricObject< */ static cacheProperties: string[] = cacheProperties; + static geometryProperties: string[] = geometryProperties; + /** * When set to `true`, object's cache will be rerendered next render call. * since 1.7.0 @@ -359,51 +354,68 @@ export class FabricObject< * and each side do not cross fabric.cacheSideLimit * those numbers are configurable so that you can get as much detail as you want * making bargain with performances. - * @param {Object} dims - * @param {Object} dims.width width of canvas - * @param {Object} dims.height height of canvas - * @param {Object} dims.zoomX zoomX zoom value to unscale the canvas before drawing cache - * @param {Object} dims.zoomY zoomY zoom value to unscale the canvas before drawing cache + * @param {Object} arg0 + * @param {Object} arg0.width width of canvas + * @param {Object} arg0.height height of canvas + * @param {Object} arg0.zoomX zoomX zoom value to unscale the canvas before drawing cache + * @param {Object} arg0.zoomY zoomY zoom value to unscale the canvas before drawing cache * @return {Object}.width width of canvas * @return {Object}.height height of canvas * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache */ _limitCacheSize( - dims: TSize & { zoomX: number; zoomY: number; capped: boolean } & any + { + width, + height, + zoomX, + zoomY, + x: cacheX, + y: cacheY, + } = this._getCacheCanvasDimensions() ) { - const width = dims.width, - height = dims.height, - max = config.maxCacheSideLimit, - min = config.minCacheSideLimit; - if ( - width <= max && - height <= max && - width * height <= config.perfLimitSizeTotal - ) { - if (width < min) { - dims.width = min; - } - if (height < min) { - dims.height = min; - } - return dims; + const { + minCacheSideLimit: min, + maxCacheSideLimit: max, + perfLimitSizeTotal, + } = config; + if (width <= max && height <= max && width * height <= perfLimitSizeTotal) { + return { + width: Math.max(width, min), + height: Math.max(height, min), + zoomX, + zoomY, + x: cacheX, + y: cacheY, + capped: false, + }; } const ar = width / height, - [limX, limY] = cache.limitDimsByArea(ar), + roughWidth = Math.sqrt(perfLimitSizeTotal * ar), + limX = Math.floor(roughWidth), + limY = Math.floor(perfLimitSizeTotal / roughWidth), x = capValue(min, limX, max), y = capValue(min, limY, max); + let capped = false; if (width > x) { - dims.zoomX /= width / x; - dims.width = x; - dims.capped = true; + zoomX /= width / x; + width = x; + capped = true; } if (height > y) { - dims.zoomY /= height / y; - dims.height = y; - dims.capped = true; + zoomY /= height / y; + height = y; + capped = true; } - return dims; + return { + width, + height, + zoomX, + zoomY, + x, + y, + capped, + }; } /** @@ -419,8 +431,12 @@ export class FabricObject< */ _getCacheCanvasDimensions(): TCacheCanvasDimensions { const objectScale = this.getTotalObjectScaling(), - // calculate dimensions without skewing - dim = this._getTransformedDimensions({ skewX: 0, skewY: 0 }), + // calculate dimensions without skewing or strokeUniform + dim = sizeAfterTransform( + this.width + this.strokeWidth, + this.height + this.strokeWidth, + { scaleX: this.scaleX, scaleY: this.scaleY } + ), neededX = (dim.x * objectScale.x) / this.scaleX, neededY = (dim.y * objectScale.y) / this.scaleY; return { @@ -445,12 +461,8 @@ export class FabricObject< _updateCacheCanvas() { const canvas = this._cacheCanvas, context = this._cacheContext, - dims = this._limitCacheSize(this._getCacheCanvasDimensions()), + { width, height, x, y, zoomX, zoomY, capped } = this._limitCacheSize(), minCacheSize = config.minCacheSideLimit, - width = dims.width, - height = dims.height, - zoomX = dims.zoomX, - zoomY = dims.zoomY, dimensionsChanged = width !== this.cacheWidth || height !== this.cacheHeight, zoomChanged = this.zoomX !== zoomX || this.zoomY !== zoomY; @@ -477,7 +489,7 @@ export class FabricObject< shouldResizeCanvas = sizeGrowing || sizeShrinking; if ( sizeGrowing && - !dims.capped && + !capped && (width > minCacheSize || height > minCacheSize) ) { additionalWidth = width * 0.1; @@ -499,8 +511,8 @@ export class FabricObject< context.setTransform(1, 0, 0, 1, 0, 0); context.clearRect(0, 0, canvas.width, canvas.height); } - drawingWidth = dims.x / 2; - drawingHeight = dims.y / 2; + drawingWidth = x / 2; + drawingHeight = y / 2; this.cacheTranslationX = Math.round(canvas.width / 2 - drawingWidth) + drawingWidth; this.cacheTranslationY = @@ -534,7 +546,9 @@ export class FabricObject< const needFullTransform = (this.group && !this.group._transformDone) || (this.group && this.canvas && ctx === (this.canvas as Canvas).contextTop); - const m = this.calcTransformMatrix(!needFullTransform); + const m = needFullTransform + ? this.calcTransformMatrix() + : this.calcOwnMatrix(); ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); } @@ -757,6 +771,13 @@ export class FabricObject< ))) && this.parent._set('dirty', true); + if ( + isChanged && + (this.constructor as typeof FabricObject).geometryProperties.includes(key) + ) { + this.invalidateCoords(); + } + return this; } @@ -783,14 +804,6 @@ export class FabricObject< if (this.isNotVisible()) { return; } - if ( - this.canvas && - this.canvas.skipOffscreen && - !this.group && - !this.isOnScreen() - ) { - return; - } ctx.save(); this._setupCompositeOperation(ctx); this.drawSelectionBackground(ctx); @@ -1038,7 +1051,8 @@ export class FabricObject< if (!this.backgroundColor) { return; } - const dim = this._getNonTransformedDimensions(); + // should this be the rotated bbox? + const dim = BBox.transformed(this).sendToSelf().getBBoxVector(); ctx.fillStyle = this.backgroundColor; ctx.fillRect(-dim.x / 2, -dim.y / 2, dim.x, dim.y); @@ -1287,11 +1301,11 @@ export class FabricObject< ctx: CanvasRenderingContext2D, filler: TFiller ) { - const dims = this._limitCacheSize(this._getCacheCanvasDimensions()), + const { x, y, zoomX, zoomY } = this._limitCacheSize(), pCanvas = createCanvasElement(), retinaScaling = this.getCanvasRetinaScaling(), - width = dims.x / this.scaleX / retinaScaling, - height = dims.y / this.scaleY / retinaScaling; + width = x / this.scaleX / retinaScaling, + height = y / this.scaleY / retinaScaling; // in case width and height are less than 1px, we have to round up. // since the pattern is no-repeat, this is fine pCanvas.width = Math.ceil(width); @@ -1308,8 +1322,8 @@ export class FabricObject< pCtx.closePath(); pCtx.translate(width / 2, height / 2); pCtx.scale( - dims.zoomX / this.scaleX / retinaScaling, - dims.zoomY / this.scaleY / retinaScaling + zoomX / this.scaleX / retinaScaling, + zoomY / this.scaleY / retinaScaling ); this._applyPatternGradientTransform(pCtx, filler); pCtx.fillStyle = filler.toLive(ctx)!; @@ -1319,8 +1333,8 @@ export class FabricObject< -this.height / 2 - this.strokeWidth / 2 ); ctx.scale( - (retinaScaling * this.scaleX) / dims.zoomX, - (retinaScaling * this.scaleY) / dims.zoomY + (retinaScaling * this.scaleX) / zoomX, + (retinaScaling * this.scaleY) / zoomY ); ctx.strokeStyle = pCtx.createPattern(pCanvas, 'no-repeat') ?? ''; } @@ -1414,7 +1428,7 @@ export class FabricObject< sendObjectToPlane(this, this.getViewportTransform()); } - this.setCoords(); + this.invalidateCoords(); const el = createCanvasElement(), boundingRect = this.getBoundingRect(), shadow = this.shadow, @@ -1441,17 +1455,13 @@ export class FabricObject< if (options.format === 'jpeg') { canvas.backgroundColor = '#fff'; } - this.setPositionByOrigin( - new Point(canvas.width / 2, canvas.height / 2), - CENTER, - CENTER - ); + this.setRelativeCenterPoint(new Point(canvas.width / 2, canvas.height / 2)); const originalCanvas = this.canvas; // static canvas and canvas have both an array of InteractiveObjects // @ts-expect-error this needs to be fixed somehow, or ignored globally canvas._objects = [this]; this.set('canvas', canvas); - this.setCoords(); + this.invalidateCoords(); const canvasEl = canvas.toCanvasElement(multiplier || 1, options); this.set('canvas', originalCanvas); this.shadow = originalShadow; @@ -1459,7 +1469,7 @@ export class FabricObject< this.group = originalGroup; } this.set(origParams); - this.setCoords(); + this.invalidateCoords(); // canvas.dispose will call image.dispose that will nullify the elements // since this canvas is a simple element for the process, we remove references // to objects in this way in order to avoid object trashing. @@ -1521,36 +1531,6 @@ export class FabricObject< return this.toObject(); } - /** - * Sets "angle" of an instance with centered rotation - * @param {TDegree} angle Angle value (in degrees) - */ - rotate(angle: TDegree) { - const { centeredRotation, originX, originY } = this; - - if (centeredRotation) { - const { x, y } = this.getRelativeCenterPoint(); - this.originX = CENTER; - this.originY = CENTER; - this.left = x; - this.top = y; - } - - this.set('angle', angle); - - if (centeredRotation) { - const { x, y } = this.translateToOriginPoint( - this.getRelativeCenterPoint(), - originX, - originY - ); - this.left = x; - this.top = y; - this.originX = originX; - this.originY = originY; - } - } - /** * This callback function is called by the parent group of an object every * time a non-delegated property changes on the group. It is passed the key diff --git a/src/shapes/Object/ObjectBBox.ts b/src/shapes/Object/ObjectBBox.ts new file mode 100644 index 00000000000..f98d42ec529 --- /dev/null +++ b/src/shapes/Object/ObjectBBox.ts @@ -0,0 +1,210 @@ +import { BBox } from '../../BBox/BBox'; +import type { Canvas } from '../../canvas/Canvas'; +import type { StaticCanvas } from '../../canvas/StaticCanvas'; +import { iMatrix } from '../../constants'; +import type { ObjectEvents } from '../../EventTypeDefs'; +import { Point } from '../../Point'; +import type { TMat2D } from '../../typedefs'; +import { mapValues } from '../../util/internals'; +import { multiplyTransformMatrices } from '../../util/misc/matrix'; +import { magnitude } from '../../util/misc/vectors'; +import { ObjectLayout } from './ObjectLayout'; +import type { ControlProps } from './types/ControlProps'; +import type { FillStrokeProps } from './types/FillStrokeProps'; + +export class ObjectBBox + extends ObjectLayout + implements + Pick, + Pick +{ + declare strokeWidth: number; + declare strokeUniform: boolean; + declare padding: number; + + private _bbox?: BBox; + + get bbox() { + if (!this._bbox) { + this._bbox = BBox.rotated(this); + } + return this._bbox; + } + + /** + * A Reference of the Canvas where the object is actually added + * @type StaticCanvas | Canvas; + * @default undefined + * @private + */ + declare canvas?: StaticCanvas | Canvas; + + /** + * Override this method if needed + */ + needsViewportCoords() { + // not working yet + return true; + // return (this.strokeUniform && this.strokeWidth > 0) || !!this.padding; + } + + getCanvasRetinaScaling() { + return this.canvas?.getRetinaScaling() || 1; + } + + /** + * Retrieves viewportTransform from Object's canvas if possible + * @method getViewportTransform + * @memberOf FabricObject.prototype + * @return {TMat2D} + */ + getViewportTransform(): TMat2D { + return this.canvas?.viewportTransform || (iMatrix.concat() as TMat2D); + } + + calcTransformMatrixInViewport() { + return multiplyTransformMatrices( + this.getViewportTransform(), + this.calcTransformMatrix() + ); + } + + protected calcDimensionsVector( + origin = new Point(1, 1), + { + applyViewportTransform = this.needsViewportCoords(), + transform = this.calcTransformMatrix(), + padding = this.padding, + }: { + applyViewportTransform?: boolean; + transform?: TMat2D; + padding?: number; + } = {} + ) { + const dimVector = origin + .multiply(new Point(this.width, this.height)) + .add(origin.scalarMultiply(!this.strokeUniform ? this.strokeWidth : 0)) + .transform( + applyViewportTransform + ? multiplyTransformMatrices(this.getViewportTransform(), transform) + : transform, + true + ); + return dimVector.scalarMultiply( + 1 + + // @TODO: this is probably wrong, stroke uniform width is a scene plane scalar + (2 * padding + (this.strokeUniform ? this.strokeWidth : 0)) / + magnitude(dimVector) + ); + } + + protected calcCoord( + origin: Point, + { + applyViewportTransform = this.needsViewportCoords(), + }: { + applyViewportTransform?: boolean; + } = {} + ) { + const realCenter = applyViewportTransform + ? this.getCenterPoint().transform(this.getViewportTransform()) + : this.getCenterPoint(); + return realCenter.add( + this.calcDimensionsVector(origin, { applyViewportTransform }) + ); + } + + /** + * Calculates the coordinates of the 4 corner of the bbox + * @return {TCornerPoint} + */ + calcCoords() { + // const size = new Point(this.width, this.height); + // return projectStrokeOnPoints( + // [ + // new Point(-0.5, -0.5), + // new Point(0.5, -0.5), + // new Point(-0.5, 0.5), + // new Point(0.5, 0.5), + // ].map((origin) => origin.multiply(size)), + // { + // ...this, + // ...qrDecompose( + // multiplyTransformMatrices( + // this.needsViewportCoords() ? this.getViewportTransform() : iMatrix, + // this.calcTransformMatrix() + // ) + // ), + // } + // ); + + return mapValues( + { + tl: new Point(-0.5, -0.5), + tr: new Point(0.5, -0.5), + bl: new Point(-0.5, 0.5), + br: new Point(0.5, 0.5), + }, + (origin) => this.calcCoord(origin) + ); + } + + getCoords(absolute = false) { + return Object.values( + (absolute ? this.bbox.sendToCanvas() : this.bbox).getCoords() + ); + } + + /** + * Sets corner and controls position coordinates based on current angle, dimensions and position. + * See {@link https://github.com/fabricjs/fabric.js/wiki/When-to-call-setCoords} and {@link http://fabricjs.com/fabric-gotchas} + */ + setCoords(): void { + this._bbox = BBox.rotated(this); + + // // debug code + // setTimeout(() => { + // const canvas = this.canvas; + // if (!canvas) return; + // const ctx = canvas.contextTop; + // canvas.clearContext(ctx); + // ctx.save(); + // const draw = (point: Point, color: string, radius = 6) => { + // ctx.fillStyle = color; + // ctx.beginPath(); + // ctx.ellipse(point.x, point.y, radius, radius, 0, 0, 360); + // ctx.closePath(); + // ctx.fill(); + // }; + // [ + // new Point(-0.5, -0.5), + // new Point(0.5, -0.5), + // new Point(-0.5, 0.5), + // new Point(0.5, 0.5), + // ].forEach((origin) => { + // draw(BBox.bbox(this).pointFromOrigin(origin), 'yellow', 10); + // draw(BBox.rotated(this).pointFromOrigin(origin), 'orange', 8); + // draw(BBox.transformed(this).pointFromOrigin(origin), 'silver', 6); + // ctx.save(); + // ctx.transform(...this.getViewportTransform()); + // draw(BBox.bbox(this).sendToCanvas().pointFromOrigin(origin), 'red', 10); + // draw( + // BBox.rotated(this).sendToCanvas().pointFromOrigin(origin), + // 'magenta', + // 8 + // ); + // draw( + // BBox.transformed(this).sendToCanvas().pointFromOrigin(origin), + // 'blue', + // 6 + // ); + // ctx.restore(); + // }); + // ctx.restore(); + // }, 50); + } + + invalidateCoords() { + // delete this.bbox + } +} diff --git a/src/shapes/Object/ObjectGeometry.ts b/src/shapes/Object/ObjectGeometry.ts index fa7e6c0c320..2de1700637e 100644 --- a/src/shapes/Object/ObjectGeometry.ts +++ b/src/shapes/Object/ObjectGeometry.ts @@ -1,203 +1,41 @@ -import type { - TBBox, - TCornerPoint, - TDegree, - TMat2D, - TOriginX, - TOriginY, -} from '../../typedefs'; -import { iMatrix } from '../../constants'; -import { Intersection } from '../../Intersection'; +import { ObjectEvents } from '../../EventTypeDefs'; import { Point } from '../../Point'; +import type { TBBox } from '../../typedefs'; import { makeBoundingBoxFromPoints } from '../../util/misc/boundingBoxFromPoints'; -import { - createRotateMatrix, - createTranslateMatrix, - composeMatrix, - invertTransform, - multiplyTransformMatrices, - transformPoint, - calcPlaneRotation, -} from '../../util/misc/matrix'; -import { radiansToDegrees } from '../../util/misc/radiansDegreesConversion'; -import type { Canvas } from '../../canvas/Canvas'; -import type { StaticCanvas } from '../../canvas/StaticCanvas'; -import { ObjectOrigin } from './ObjectOrigin'; -import type { ObjectEvents } from '../../EventTypeDefs'; -import type { ControlProps } from './types/ControlProps'; - -type TMatrixCache = { - key: string; - value: TMat2D; -}; - -type TACoords = TCornerPoint; - -export class ObjectGeometry - extends ObjectOrigin - implements Pick -{ - declare padding: number; - - /** - * Describe object's corner position in scene coordinates. - * The coordinates are derived from the following: - * left, top, width, height, scaleX, scaleY, skewX, skewY, angle, strokeWidth. - * The coordinates do not depend on viewport changes. - * The coordinates get updated with {@link setCoords}. - * You can calculate them without updating with {@link calcACoords()} - */ - declare aCoords: TACoords; - - /** - * storage cache for object transform matrix - */ - declare ownMatrixCache?: TMatrixCache; - - /** - * storage cache for object full transform matrix - */ - declare matrixCache?: TMatrixCache; - - /** - * A Reference of the Canvas where the object is actually added - * @type StaticCanvas | Canvas; - * @default undefined - * @private - */ - declare canvas?: StaticCanvas | Canvas; - - /** - * @returns {number} x position according to object's {@link originX} property in canvas coordinate plane - */ - getX(): number { - return this.getXY().x; - } - - /** - * @param {number} value x position according to object's {@link originX} property in canvas coordinate plane - */ - setX(value: number) { - this.setXY(this.getXY().setX(value)); - } - - /** - * @returns {number} y position according to object's {@link originY} property in canvas coordinate plane - */ - getY(): number { - return this.getXY().y; - } - - /** - * @param {number} value y position according to object's {@link originY} property in canvas coordinate plane - */ - setY(value: number) { - this.setXY(this.getXY().setY(value)); - } - - /** - * @returns {number} x position according to object's {@link originX} property in parent's coordinate plane\ - * if parent is canvas then this property is identical to {@link getX} - */ - getRelativeX(): number { - return this.left; - } - - /** - * @param {number} value x position according to object's {@link originX} property in parent's coordinate plane\ - * if parent is canvas then this method is identical to {@link setX} - */ - setRelativeX(value: number) { - this.left = value; - } - - /** - * @returns {number} y position according to object's {@link originY} property in parent's coordinate plane\ - * if parent is canvas then this property is identical to {@link getY} - */ - getRelativeY(): number { - return this.top; - } - - /** - * @param {number} value y position according to object's {@link originY} property in parent's coordinate plane\ - * if parent is canvas then this property is identical to {@link setY} - */ - setRelativeY(value: number) { - this.top = value; - } - - /** - * @returns {Point} x position according to object's {@link originX} {@link originY} properties in canvas coordinate plane - */ - getXY(): Point { - const relativePosition = this.getRelativeXY(); - return this.group - ? transformPoint(relativePosition, this.group.calcTransformMatrix()) - : relativePosition; - } - - /** - * Set an object position to a particular point, the point is intended in absolute ( canvas ) coordinate. - * You can specify {@link originX} and {@link originY} values, - * that otherwise are the object's current values. - * @example Set object's bottom left corner to point (5,5) on canvas - * object.setXY(new Point(5, 5), 'left', 'bottom'). - * @param {Point} point position in canvas coordinate plane - * @param {TOriginX} [originX] Horizontal origin: 'left', 'center' or 'right' - * @param {TOriginY} [originY] Vertical origin: 'top', 'center' or 'bottom' - */ - setXY(point: Point, originX?: TOriginX, originY?: TOriginY) { - if (this.group) { - point = transformPoint( - point, - invertTransform(this.group.calcTransformMatrix()) - ); - } - this.setRelativeXY(point, originX, originY); - } - - /** - * @returns {Point} x,y position according to object's {@link originX} {@link originY} properties in parent's coordinate plane - */ - getRelativeXY(): Point { - return new Point(this.left, this.top); - } - - /** - * As {@link setXY}, but in current parent's coordinate plane (the current group if any or the canvas) - * @param {Point} point position according to object's {@link originX} {@link originY} properties in parent's coordinate plane - * @param {TOriginX} [originX] Horizontal origin: 'left', 'center' or 'right' - * @param {TOriginY} [originY] Vertical origin: 'top', 'center' or 'bottom' - */ - setRelativeXY( - point: Point, - originX: TOriginX = this.originX, - originY: TOriginY = this.originY - ) { - this.setPositionByOrigin(point, originX, originY); - } - - /** - * @deprecated intermidiate method to be removed, do not use - */ - protected isStrokeAccountedForInDimensions() { - return false; - } - - /** - * @return {Point[]} [tl, tr, br, bl] in the scene plane - */ - getCoords(): Point[] { - const { tl, tr, br, bl } = - this.aCoords || (this.aCoords = this.calcACoords()); - const coords = [tl, tr, br, bl]; - if (this.group) { - const t = this.group.calcTransformMatrix(); - return coords.map((p) => transformPoint(p, t)); - } - return coords; - } +import { ObjectTransformations } from './ObjectTransformations'; +import { ViewportBBox } from '../../BBox/ViewportBBox'; +import { Intersection } from '../../Intersection'; +import type { Shadow } from '../../Shadow'; + +export class ObjectGeometry< + EventSpec extends ObjectEvents = ObjectEvents +> extends ObjectTransformations { + declare shadow?: Shadow; + + // @TODO: shadow geometry + // getShadowData() { + // if (!this.shadow) { + // return; + // } + + // const {offsetX,offsetY,blur,nonScaling} = this.shadow; + // var sx = 1, + // sy = 1; + // if (!nonScaling) { + // var scaling = this.getTotalObjectScaling(); + // sx = scaling.x; + // sy = scaling.y; + // } + // const shadowOffset = new Point( + // this.shadow.offsetX * sx, + // this.shadow.offsetY * sy + // ), + // blurOffset = new Point(blur * sx, blur * sy); + // return { + // offset: shadowOffset, + // blur: blurOffset, + // }; + // } /** * Checks if object intersects with the scene rect formed by {@link tl} and {@link br} @@ -217,17 +55,18 @@ export class ObjectGeometry * @return {Boolean} true if object intersects with another object */ intersectsWithObject(other: ObjectGeometry): boolean { - const intersection = Intersection.intersectPolygonPolygon( - this.getCoords(), - other.getCoords() - ); + // const intersection = Intersection.intersectPolygonPolygon( + // this.getCoords(), + // other.getCoords() + // ); + // return ( + // intersection.status === 'Intersection' || + // intersection.status === 'Coincident' || + // other.isContainedWithinObject(this) || + // this.isContainedWithinObject(other) + // ); - return ( - intersection.status === 'Intersection' || - intersection.status === 'Coincident' || - other.isContainedWithinObject(this) || - this.isContainedWithinObject(other) - ); + return this.bbox.intersects(other.bbox); } /** @@ -236,8 +75,9 @@ export class ObjectGeometry * @return {Boolean} true if object is fully contained within area of another object */ isContainedWithinObject(other: ObjectGeometry): boolean { - const points = this.getCoords(); - return points.every((point) => other.containsPoint(point)); + // const points = this.getCoords(); + // return points.every((point) => other.containsPoint(point)); + return this.bbox.isContainedBy(other.bbox); } /** @@ -254,11 +94,7 @@ export class ObjectGeometry } isOverlapping(other: T): boolean { - return ( - this.intersectsWithObject(other) || - this.isContainedWithinObject(other) || - other.isContainedWithinObject(this) - ); + return this.bbox.overlaps(other.bbox); } /** @@ -274,52 +110,26 @@ export class ObjectGeometry * Checks if object is contained within the canvas with current viewportTransform * the check is done stopping at first point that appears on screen * @return {Boolean} true if object is fully or partially contained within canvas + * @deprecated move to canvas */ isOnScreen(): boolean { if (!this.canvas) { return false; } - const { tl, br } = this.canvas.vptCoords; - const points = this.getCoords(); - // if some point is on screen, the object is on screen. - if ( - points.some( - (point) => - point.x <= br.x && - point.x >= tl.x && - point.y <= br.y && - point.y >= tl.y - ) - ) { - return true; - } - // no points on screen, check intersection with absolute coordinates - if (this.intersectsWithRect(tl, br)) { - return true; - } - // check if the object is so big that it contains the entire viewport - return this.containsPoint(tl.midPointFrom(br)); + return ViewportBBox.canvas(this.canvas).overlaps(this.bbox); } /** * Checks if object is partially contained within the canvas with current viewportTransform * @return {Boolean} true if object is partially contained within canvas + * @deprecated move to canvas */ - isPartiallyOnScreen(): boolean { + isPartiallyOnScreen(): boolean | undefined { if (!this.canvas) { - return false; - } - const { tl, br } = this.canvas.vptCoords; - if (this.intersectsWithRect(tl, br)) { - return true; + return undefined; } - const allPointsAreOutside = this.getCoords().every( - (point) => - (point.x >= br.x || point.x <= tl.x) && - (point.y >= br.y || point.y <= tl.y) - ); - // check if the object is so big that it contains the entire viewport - return allPointsAreOutside && this.containsPoint(tl.midPointFrom(br)); + const bbox = ViewportBBox.canvas(this.canvas); + return bbox.intersects(this.bbox) || bbox.isContainedBy(this.bbox); } /** @@ -328,231 +138,7 @@ export class ObjectGeometry * @return {Object} Object with left, top, width, height properties */ getBoundingRect(): TBBox { + // return BBox.canvas(this).getBBox() return makeBoundingBoxFromPoints(this.getCoords()); } - - /** - * Returns width of an object's bounding box counting transformations - * @todo shouldn't this account for group transform and return the actual size in canvas coordinate plane? - * @return {Number} width value - */ - getScaledWidth(): number { - return this._getTransformedDimensions().x; - } - - /** - * Returns height of an object bounding box counting transformations - * @todo shouldn't this account for group transform and return the actual size in canvas coordinate plane? - * @return {Number} height value - */ - getScaledHeight(): number { - return this._getTransformedDimensions().y; - } - - /** - * Scales an object (equally by x and y) - * @param {Number} value Scale factor - * @return {void} - */ - scale(value: number): void { - this._set('scaleX', value); - this._set('scaleY', value); - this.setCoords(); - } - - /** - * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) - * @param {Number} value New width value - * @return {void} - */ - scaleToWidth(value: number) { - // adjust to bounding rect factor so that rotated shapes would fit as well - const boundingRectFactor = - this.getBoundingRect().width / this.getScaledWidth(); - return this.scale(value / this.width / boundingRectFactor); - } - - /** - * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) - * @param {Number} value New height value - * @return {void} - */ - scaleToHeight(value: number) { - // adjust to bounding rect factor so that rotated shapes would fit as well - const boundingRectFactor = - this.getBoundingRect().height / this.getScaledHeight(); - return this.scale(value / this.height / boundingRectFactor); - } - - getCanvasRetinaScaling() { - return this.canvas?.getRetinaScaling() || 1; - } - - /** - * Returns the object angle relative to canvas counting also the group property - * @returns {TDegree} - */ - getTotalAngle(): TDegree { - return this.group - ? radiansToDegrees(calcPlaneRotation(this.calcTransformMatrix())) - : this.angle; - } - - /** - * Retrieves viewportTransform from Object's canvas if available - * @return {TMat2D} - */ - getViewportTransform(): TMat2D { - return this.canvas?.viewportTransform || (iMatrix.concat() as TMat2D); - } - - /** - * Calculates the coordinates of the 4 corner of the bbox, in absolute coordinates. - * those never change with zoom or viewport changes. - * @return {TCornerPoint} - */ - calcACoords(): TCornerPoint { - const rotateMatrix = createRotateMatrix({ angle: this.angle }), - { x, y } = this.getRelativeCenterPoint(), - tMatrix = createTranslateMatrix(x, y), - finalMatrix = multiplyTransformMatrices(tMatrix, rotateMatrix), - dim = this._getTransformedDimensions(), - w = dim.x / 2, - h = dim.y / 2; - return { - // corners - tl: transformPoint({ x: -w, y: -h }, finalMatrix), - tr: transformPoint({ x: w, y: -h }, finalMatrix), - bl: transformPoint({ x: -w, y: h }, finalMatrix), - br: transformPoint({ x: w, y: h }, finalMatrix), - }; - } - - /** - * Sets corner and controls position coordinates based on current angle, width and height, left and top. - * aCoords are used to quickly find an object on the canvas. - * See {@link https://github.com/fabricjs/fabric.js/wiki/When-to-call-setCoords} and {@link http://fabricjs.com/fabric-gotchas} - */ - setCoords(): void { - this.aCoords = this.calcACoords(); - } - - transformMatrixKey(skipGroup = false): string { - const sep = '_'; - let prefix = ''; - if (!skipGroup && this.group) { - prefix = this.group.transformMatrixKey(skipGroup) + sep; - } - return ( - prefix + - this.top + - sep + - this.left + - sep + - this.scaleX + - sep + - this.scaleY + - sep + - this.skewX + - sep + - this.skewY + - sep + - this.angle + - sep + - this.originX + - sep + - this.originY + - sep + - this.width + - sep + - this.height + - sep + - this.strokeWidth + - this.flipX + - this.flipY - ); - } - - /** - * calculate transform matrix that represents the current transformations from the - * object's properties. - * @param {Boolean} [skipGroup] return transform matrix for object not counting parent transformations - * There are some situation in which this is useful to avoid the fake rotation. - * @return {TMat2D} transform matrix for the object - */ - calcTransformMatrix(skipGroup = false): TMat2D { - let matrix = this.calcOwnMatrix(); - if (skipGroup || !this.group) { - return matrix; - } - const key = this.transformMatrixKey(skipGroup), - cache = this.matrixCache; - if (cache && cache.key === key) { - return cache.value; - } - if (this.group) { - matrix = multiplyTransformMatrices( - this.group.calcTransformMatrix(false), - matrix - ); - } - this.matrixCache = { - key, - value: matrix, - }; - return matrix; - } - - /** - * calculate transform matrix that represents the current transformations from the - * object's properties, this matrix does not include the group transformation - * @return {TMat2D} transform matrix for the object - */ - calcOwnMatrix(): TMat2D { - const key = this.transformMatrixKey(true), - cache = this.ownMatrixCache; - if (cache && cache.key === key) { - return cache.value; - } - const center = this.getRelativeCenterPoint(), - options = { - angle: this.angle, - translateX: center.x, - translateY: center.y, - scaleX: this.scaleX, - scaleY: this.scaleY, - skewX: this.skewX, - skewY: this.skewY, - flipX: this.flipX, - flipY: this.flipY, - }, - value = composeMatrix(options); - this.ownMatrixCache = { - key, - value, - }; - return value; - } - - /** - * Calculate object dimensions from its properties - * @private - * @returns {Point} dimensions - */ - _getNonTransformedDimensions(): Point { - return new Point(this.width, this.height).scalarAdd(this.strokeWidth); - } - - /** - * Calculate object dimensions for controls box, including padding and canvas zoom. - * and active selection - * @private - * @param {object} [options] transform options - * @returns {Point} dimensions - */ - _calculateCurrentDimensions(options?: any): Point { - return this._getTransformedDimensions(options) - .transform(this.getViewportTransform(), true) - .scalarAdd(2 * this.padding); - } } diff --git a/src/shapes/Object/ObjectLayout.ts b/src/shapes/Object/ObjectLayout.ts new file mode 100644 index 00000000000..b10f784089d --- /dev/null +++ b/src/shapes/Object/ObjectLayout.ts @@ -0,0 +1,189 @@ +import { CommonMethods } from '../../CommonMethods'; +import type { ObjectEvents } from '../../EventTypeDefs'; +import { Point } from '../../Point'; +import type { TDegree, TMat2D, TOriginX, TOriginY } from '../../typedefs'; +import { + composeMatrix, + multiplyTransformMatrices, +} from '../../util/misc/matrix'; +import { sizeAfterTransform } from '../../util/misc/objectTransforms'; +import { sendPointToPlane } from '../../util/misc/planeChange'; +import { degreesToRadians } from '../../util/misc/radiansDegreesConversion'; +import { resolveOriginPoint } from '../../util/misc/resolveOrigin'; +import type { Group } from '../Group'; +import type { BaseProps } from './types/BaseProps'; + +type TMatrixCache = { + key: string; + value: TMat2D; +}; + +export class ObjectLayout + extends CommonMethods + implements BaseProps +{ + declare left: number; + declare top: number; + declare width: number; + declare height: number; + declare flipX: boolean; + declare flipY: boolean; + declare scaleX: number; + declare scaleY: number; + declare skewX: number; + declare skewY: number; + declare originX: TOriginX; + declare originY: TOriginY; + declare angle: TDegree; + + /** + * Object containing this object. + * can influence its size and position + */ + declare group?: Group; + + /** + * storage cache for object transform matrix + */ + declare ownMatrixCache?: TMatrixCache; + + /** + * storage cache for object full transform matrix + */ + declare matrixCache?: TMatrixCache; + + protected getDimensionsVectorForLayout(origin = new Point(1, 1)) { + return sizeAfterTransform(this.width, this.height, this.calcOwnMatrix()) + .rotate(degreesToRadians(this.angle)) + .multiply(origin); + } + + /** + * Returns the center coordinates of the object relative to it's parent + * @return {Point} + */ + getRelativeCenterPoint(): Point { + return new Point(this.left, this.top).add( + this.getDimensionsVectorForLayout( + resolveOriginPoint(this.originX, this.originY).scalarMultiply(-1) + ) + ); + } + + setRelativeCenterPoint(point: Point): void { + const position = point.add( + this.getDimensionsVectorForLayout( + resolveOriginPoint(this.originX, this.originY) + ) + ); + this.set({ left: position.x, top: position.y }); + } + + /** + * Returns the center coordinates of the object relative to canvas + * @return {Point} + */ + getCenterPoint(): Point { + return sendPointToPlane( + this.getRelativeCenterPoint(), + this.group?.calcTransformMatrix() + ); + } + + setCenterPoint(point: Point) { + this.setRelativeCenterPoint( + sendPointToPlane(point, undefined, this.group?.calcTransformMatrix()) + ); + } + + transformMatrixKey(skipGroup = false): string { + const sep = '_'; + return !skipGroup && this.group + ? this.group.transformMatrixKey() + sep + : '' + + this.top + + sep + + this.left + + sep + + this.scaleX + + sep + + this.scaleY + + sep + + this.skewX + + sep + + this.skewY + + sep + + this.angle + + sep + + this.originX + + sep + + this.originY + + sep + + this.width + + sep + + this.height + + sep + + this.flipX + + this.flipY; + } + + /** + * calculate transform matrix that represents the current transformations from the + * object's properties. + * @param {Boolean} [skipGroup] return transform matrix for object not counting parent transformations + * There are some situation in which this is useful to avoid the fake rotation. + * @return {TMat2D} transform matrix for the object + */ + calcTransformMatrix(skipGroup = false): TMat2D { + let matrix = this.calcOwnMatrix(); + if (skipGroup || !this.group) { + return matrix; + } + const key = this.transformMatrixKey(skipGroup), + cache = this.matrixCache; + if (cache && cache.key === key) { + return cache.value; + } + if (this.group) { + matrix = multiplyTransformMatrices( + this.group.calcTransformMatrix(false), + matrix + ); + } + // this.matrixCache = { + // key, + // value: matrix, + // }; + return matrix; + } + + /** + * calculate transform matrix that represents the current transformations from the + * object's properties, this matrix does not include the group transformation + * @return {TMat2D} transform matrix for the object + */ + calcOwnMatrix(): TMat2D { + const key = this.transformMatrixKey(true), + cache = this.ownMatrixCache; + if (cache && cache.key === key) { + return cache.value; + } + const center = this.getRelativeCenterPoint(), + value = composeMatrix({ + angle: this.angle, + translateX: center.x, + translateY: center.y, + scaleX: this.scaleX, + scaleY: this.scaleY, + skewX: this.skewX, + skewY: this.skewY, + flipX: this.flipX, + flipY: this.flipY, + }); + // this.ownMatrixCache = { + // key, + // value, + // }; + return value; + } +} diff --git a/src/shapes/Object/ObjectOrigin.ts b/src/shapes/Object/ObjectOrigin.ts deleted file mode 100644 index 7060382562a..00000000000 --- a/src/shapes/Object/ObjectOrigin.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { Point } from '../../Point'; -import type { Group } from '../Group'; -import type { TDegree, TOriginX, TOriginY } from '../../typedefs'; -import { calcDimensionsMatrix, transformPoint } from '../../util/misc/matrix'; -import { sizeAfterTransform } from '../../util/misc/objectTransforms'; -import { degreesToRadians } from '../../util/misc/radiansDegreesConversion'; -import { CommonMethods } from '../../CommonMethods'; -import { resolveOrigin } from '../../util/misc/resolveOrigin'; -import type { BaseProps } from './types/BaseProps'; -import type { FillStrokeProps } from './types/FillStrokeProps'; -import { CENTER, LEFT, TOP } from '../../constants'; - -export class ObjectOrigin - extends CommonMethods - implements BaseProps, Pick -{ - declare top: number; - declare left: number; - declare width: number; - declare height: number; - declare flipX: boolean; - declare flipY: boolean; - declare scaleX: number; - declare scaleY: number; - declare skewX: number; - declare skewY: number; - declare originX: TOriginX; - declare originY: TOriginY; - declare angle: TDegree; - declare strokeWidth: number; - declare strokeUniform: boolean; - - /** - * Object containing this object. - * can influence its size and position - */ - declare group?: Group; - - /** - * Calculate object bounding box dimensions from its properties scale, skew. - * This bounding box is aligned with object angle and not with canvas axis or screen. - * @param {Object} [options] - * @param {Number} [options.scaleX] - * @param {Number} [options.scaleY] - * @param {Number} [options.skewX] - * @param {Number} [options.skewY] - * @private - * @returns {Point} dimensions - */ - _getTransformedDimensions(options: any = {}): Point { - const dimOptions = { - // if scaleX or scaleY are negative numbers, - // this will return dimensions that are negative. - // and this will break assumptions around the codebase - scaleX: this.scaleX, - scaleY: this.scaleY, - skewX: this.skewX, - skewY: this.skewY, - width: this.width, - height: this.height, - strokeWidth: this.strokeWidth, - ...options, - }; - // stroke is applied before/after transformations are applied according to `strokeUniform` - const strokeWidth = dimOptions.strokeWidth; - let preScalingStrokeValue = strokeWidth, - postScalingStrokeValue = 0; - - if (this.strokeUniform) { - preScalingStrokeValue = 0; - postScalingStrokeValue = strokeWidth; - } - const dimX = dimOptions.width + preScalingStrokeValue, - dimY = dimOptions.height + preScalingStrokeValue, - noSkew = dimOptions.skewX === 0 && dimOptions.skewY === 0; - let finalDimensions; - if (noSkew) { - finalDimensions = new Point( - dimX * dimOptions.scaleX, - dimY * dimOptions.scaleY - ); - } else { - finalDimensions = sizeAfterTransform( - dimX, - dimY, - calcDimensionsMatrix(dimOptions) - ); - } - - return finalDimensions.scalarAdd(postScalingStrokeValue); - } - - /** - * Translates the coordinates from a set of origin to another (based on the object's dimensions) - * @param {Point} point The point which corresponds to the originX and originY params - * @param {TOriginX} fromOriginX Horizontal origin: 'left', 'center' or 'right' - * @param {TOriginY} fromOriginY Vertical origin: 'top', 'center' or 'bottom' - * @param {TOriginX} toOriginX Horizontal origin: 'left', 'center' or 'right' - * @param {TOriginY} toOriginY Vertical origin: 'top', 'center' or 'bottom' - * @return {Point} - */ - translateToGivenOrigin( - point: Point, - fromOriginX: TOriginX, - fromOriginY: TOriginY, - toOriginX: TOriginX, - toOriginY: TOriginY - ): Point { - let x = point.x, - y = point.y; - const offsetX = resolveOrigin(toOriginX) - resolveOrigin(fromOriginX), - offsetY = resolveOrigin(toOriginY) - resolveOrigin(fromOriginY); - - if (offsetX || offsetY) { - const dim = this._getTransformedDimensions(); - x += offsetX * dim.x; - y += offsetY * dim.y; - } - - return new Point(x, y); - } - - /** - * Translates the coordinates from origin to center coordinates (based on the object's dimensions) - * @param {Point} point The point which corresponds to the originX and originY params - * @param {TOriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {TOriginY} originY Vertical origin: 'top', 'center' or 'bottom' - * @return {Point} - */ - translateToCenterPoint( - point: Point, - originX: TOriginX, - originY: TOriginY - ): Point { - const p = this.translateToGivenOrigin( - point, - originX, - originY, - CENTER, - CENTER - ); - if (this.angle) { - return p.rotate(degreesToRadians(this.angle), point); - } - return p; - } - - /** - * Translates the coordinates from center to origin coordinates (based on the object's dimensions) - * @param {Point} center The point which corresponds to center of the object - * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' - * @return {Point} - */ - translateToOriginPoint( - center: Point, - originX: TOriginX, - originY: TOriginY - ): Point { - const p = this.translateToGivenOrigin( - center, - CENTER, - CENTER, - originX, - originY - ); - if (this.angle) { - return p.rotate(degreesToRadians(this.angle), center); - } - return p; - } - - /** - * Returns the center coordinates of the object relative to canvas - * @return {Point} - */ - getCenterPoint(): Point { - const relCenter = this.getRelativeCenterPoint(); - return this.group - ? transformPoint(relCenter, this.group.calcTransformMatrix()) - : relCenter; - } - - /** - * Returns the center coordinates of the object relative to it's parent - * @return {Point} - */ - getRelativeCenterPoint(): Point { - return this.translateToCenterPoint( - new Point(this.left, this.top), - this.originX, - this.originY - ); - } - - /** - * Returns the coordinates of the object as if it has a different origin - * @param {TOriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {TOriginY} originY Vertical origin: 'top', 'center' or 'bottom' - * @return {Point} - */ - getPointByOrigin(originX: TOriginX, originY: TOriginY): Point { - return this.translateToOriginPoint( - this.getRelativeCenterPoint(), - originX, - originY - ); - } - - /** - * Sets the position of the object taking into consideration the object's origin - * @param {Point} pos The new position of the object - * @param {TOriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {TOriginY} originY Vertical origin: 'top', 'center' or 'bottom' - * @return {void} - */ - setPositionByOrigin(pos: Point, originX: TOriginX, originY: TOriginY) { - const center = this.translateToCenterPoint(pos, originX, originY), - position = this.translateToOriginPoint( - center, - this.originX, - this.originY - ); - this.set({ left: position.x, top: position.y }); - } - - /** - * @private - */ - _getLeftTopCoords() { - return this.translateToOriginPoint( - this.getRelativeCenterPoint(), - LEFT, - TOP - ); - } -} diff --git a/src/shapes/Object/ObjectTransformations.ts b/src/shapes/Object/ObjectTransformations.ts new file mode 100644 index 00000000000..31953c5058c --- /dev/null +++ b/src/shapes/Object/ObjectTransformations.ts @@ -0,0 +1,325 @@ +import { BBox } from '../../BBox/BBox'; +import { iMatrix } from '../../constants'; +import type { ObjectEvents } from '../../EventTypeDefs'; +import { Point } from '../../Point'; +import type { TDegree, TMat2D, TOriginX, TOriginY } from '../../typedefs'; +import { + invertTransform, + isMatrixEqual, + multiplyTransformMatrices, + createRotateMatrix, + multiplyTransformMatrixArray, + calcPlaneRotation, +} from '../../util/misc/matrix'; +import { applyTransformToObject } from '../../util/misc/objectTransforms'; +import { calcBaseChangeMatrix } from '../../util/misc/planeChange'; +import { degreesToRadians } from '../../util/misc/radiansDegreesConversion'; +import { resolveOriginPoint } from '../../util/misc/resolveOrigin'; +import { + createVector, + getOrthogonalVector, + getOrthonormalVector, + getUnitVector, +} from '../../util/misc/vectors'; +import { ObjectBBox } from './ObjectBBox'; + +type ObjectTransformOptions = { + originX?: TOriginX; + originY?: TOriginY; + inViewport?: boolean; +}; + +export class ObjectTransformations< + EventSpec extends ObjectEvents = ObjectEvents +> extends ObjectBBox { + /** + * @returns {Point} position according to object's {@link originX} {@link originY} properties in canvas coordinate plane + */ + getXY( + originX: TOriginX = this.originX, + originY: TOriginY = this.originY + ): Point { + return this.bbox + .sendToCanvas() + .pointFromOrigin(resolveOriginPoint(originX, originY)); + } + + /** + * Set position to a particular point, in canvas coordinate. + * You can specify {@link originX} and {@link originY} values, + * that otherwise are the object's current values. + * @example Set object's bottom left corner to point (5,5) on canvas + * object.setXY(new Point(5, 5), 'left', 'bottom'). + * @see {@link translateTo} + * @param {Point} point position in canvas coordinate plane + * @param {TOriginX} [originX] Horizontal origin: 'left', 'center' or 'right' + * @param {TOriginY} [originY] Vertical origin: 'top', 'center' or 'bottom' + */ + setXY( + point: Point, + originX: TOriginX = this.originX, + originY: TOriginY = this.originY + ) { + this.translateTo(point.x, point.y, { originX, originY, inViewport: false }); + } + + /** + * Transforms object with respect to origin + * @param transform + * @param param1 options + * @returns true if transform has changed + */ + transformObject( + transform: TMat2D, + { + originX = this.originX, + originY = this.originY, + inViewport = false, + }: ObjectTransformOptions = {} + ) { + const ownTransformBefore = this.calcOwnMatrix(); + const transformCenter = ( + inViewport ? this.bbox : this.bbox.sendToCanvas() + ).pointFromOrigin(resolveOriginPoint(originX, originY)); + const ownToTransformPlaneChange = multiplyTransformMatrixArray([ + [1, 0, 0, 1, -transformCenter.x, -transformCenter.y], + inViewport ? this.getViewportTransform() : iMatrix, + this.group?.calcTransformMatrix() || iMatrix, + ]); + const transformToOwnPlaneChange = invertTransform( + ownToTransformPlaneChange + ); + const ownTransformAfter = multiplyTransformMatrixArray([ + transformToOwnPlaneChange, + transform, + ownToTransformPlaneChange, + ownTransformBefore, + ]); + + if (!isMatrixEqual(ownTransformAfter, ownTransformBefore)) { + // TODO: stop using decomposed values in favor of a matrix + delete this.ownMatrixCache; + delete this.matrixCache; + applyTransformToObject(this, ownTransformAfter); + this.setCoords(); + if (this.group) { + this.group.triggerLayout(); + this.group._set('dirty', true); + } + return true; + } + + return false; + } + + setObjectTransform(transform: TMat2D, options?: ObjectTransformOptions) { + return this.transformObject( + multiplyTransformMatrices( + transform, + invertTransform( + options?.inViewport + ? this.calcTransformMatrixInViewport() + : this.calcTransformMatrix() + ) + ), + options + ); + } + + translate(x: number, y: number, inViewport?: boolean) { + return this.transformObject([1, 0, 0, 1, x, y], { inViewport }); + } + + translateTo( + x: number, + y: number, + { + originX = this.originX, + originY = this.originY, + inViewport = false, + }: ObjectTransformOptions = {} + ) { + const delta = ( + inViewport ? this.bbox : this.bbox.sendToCanvas() + ).getOriginTranslation( + new Point(x, y), + resolveOriginPoint(originX, originY) + ); + return this.transformObject([1, 0, 0, 1, delta.x, delta.y], { inViewport }); + } + + scale(x: number, y: number, options?: ObjectTransformOptions) { + const t = options?.inViewport + ? this.calcTransformMatrixInViewport() + : this.calcTransformMatrix(); + const rotation = createRotateMatrix({ + rotation: calcPlaneRotation(t), + }); + const [a, b, c, d] = t; + return this.transformObject( + multiplyTransformMatrixArray([ + rotation, + [(x / a) * rotation[0], 0, 0, (y / d) * rotation[3], 0, 0], + invertTransform(rotation), + ]), + options + ); + } + + scaleBy(x: number, y: number, options?: ObjectTransformOptions) { + const t = options?.inViewport + ? this.calcTransformMatrixInViewport() + : this.calcTransformMatrix(); + const rotation = createRotateMatrix({ + rotation: calcPlaneRotation(t), + }); + return this.transformObject( + multiplyTransformMatrixArray([ + rotation, + [x, 0, 0, y, 0, 0], + invertTransform(rotation), + ]), + options + ); + } + + skew(x: TDegree, y: TDegree, options?: ObjectTransformOptions) { + return this.shear( + Math.tan(degreesToRadians(x)), + Math.tan(degreesToRadians(y)), + options + ); + } + + skewBy(x: TDegree, y: TDegree, options?: ObjectTransformOptions) { + return this.shearBy( + Math.tan(degreesToRadians(x)), + Math.tan(degreesToRadians(y)), + options + ); + } + + shear(x: number, y: number, options?: ObjectTransformOptions) { + const bbox = BBox.bbox(this); + const { tl, tr, bl } = ( + options?.inViewport ? bbox : bbox.sendToCanvas() + ).getCoords(); + return this.shearSides( + [createVector(tl, tr), createVector(tl, bl)], + [y, x], + options + ); + } + + shearBy(x: number, y: number, options?: ObjectTransformOptions) { + const bbox = BBox.transformed(this); + const { tl, tr, bl } = ( + options?.inViewport ? bbox : bbox.sendToCanvas() + ).getCoords(); + return this.shearSides( + [ + getUnitVector(createVector(tl, tr)), + getUnitVector(createVector(tl, bl)), + ], + [y, x], + options + ); + } + + protected shearSides( + [vx, vy]: [Point, Point], + [y, x]: [number, number], + options?: ObjectTransformOptions + ) { + const xVector = getUnitVector(vx); + const yVector = getUnitVector(vy); + return this.shearSidesBy( + [xVector, yVector], + [ + getOrthonormalVector(xVector).scalarMultiply(y), + getOrthonormalVector(yVector).scalarMultiply(x), + ], + options + ); + } + + /** + * + * @param param0 2 vectors representing the sides of the object + * @param param1 2 vectors representing the shearing offset affecting the 2 side vectors respectively (dy affects vx and dx affects vy) + * @param options + * @returns + */ + shearSidesBy( + [vx, vy]: [Point, Point], + [dy, dx]: [Point, Point], + options?: ObjectTransformOptions + ) { + const [a, b, c, d] = options?.inViewport + ? this.calcTransformMatrixInViewport() + : this.calcTransformMatrix(); + const xTVector = getUnitVector(new Point(a, b)); + const yTVector = getUnitVector(new Point(c, d)); + const newXVector = getUnitVector(vx.add(dy)); + const newYVector = getUnitVector(vy.add(dx)); + return this.transformObject( + calcBaseChangeMatrix([xTVector, yTVector], [newXVector, newYVector]), + options + ); + } + + /** + * Rotates object to angle + * @param {TDegree} angle Angle value (in degrees) + * @returns true if transform has changed + */ + rotate(angle: TDegree, options?: ObjectTransformOptions) { + return this.transformObject( + createRotateMatrix({ + rotation: + degreesToRadians(angle) - + (options?.inViewport + ? this.bbox + : this.bbox.sendToCanvas() + ).getRotation(), + }), + options + ); + } + + /** + * Rotates object by angle + * @param {TDegree} angle Angle value (in degrees) + * @returns true if transform has changed + */ + rotateBy(angle: TDegree, options?: ObjectTransformOptions) { + return this.transformObject(createRotateMatrix({ angle }), options); + } + + rotate3D( + x: TDegree, + y: TDegree, + z: TDegree, + options?: ObjectTransformOptions + ) { + const transformed = BBox.transformed(this); + const sideVectorX = transformed.vectorFromOrigin(new Point(1, 0)); + const sideVectorY = transformed.vectorFromOrigin(new Point(0, 1)); + return this.shearSidesBy( + [sideVectorX, sideVectorY], + [ + getOrthogonalVector(sideVectorX).scalarMultiply( + Math.tan(degreesToRadians(x + z)) + ), + getOrthogonalVector(sideVectorY).scalarMultiply( + Math.tan(degreesToRadians(y + z)) + ), + ], + options + ); + } + + flip(x: boolean, y: boolean, options?: ObjectTransformOptions) { + return this.transformObject([x ? -1 : 1, 0, 0, y ? -1 : 1, 0, 0], options); + } +} diff --git a/src/shapes/Object/defaultValues.ts b/src/shapes/Object/defaultValues.ts index 4db279add0e..3b8aee72645 100644 --- a/src/shapes/Object/defaultValues.ts +++ b/src/shapes/Object/defaultValues.ts @@ -38,6 +38,30 @@ export const cacheProperties = [ 'clipPath', ]; +export const geometryProperties = [ + TOP, + LEFT, + 'angle', + 'scaleX', + 'scaleY', + 'flipX', + 'flipY', + 'skewX', + 'skewY', + 'width', + 'height', + 'originX', + 'originY', + 'strokeWidth', + 'strokeUniform', + // this don't affect coords currently but should + 'strokeLineCap', + 'strokeLineJoin', + 'strokeMiterLimit', + // this doesn't affect bboxCoords but due to laziness it will invalidate all coords + 'padding', +]; + export const fabricObjectDefaultValues: Partial< TClassProperties > = { diff --git a/src/shapes/Path.ts b/src/shapes/Path.ts index 6b9ae2c7a3d..adde2edbda4 100644 --- a/src/shapes/Path.ts +++ b/src/shapes/Path.ts @@ -1,4 +1,5 @@ import { config } from '../config'; +import { LEFT, TOP } from '../constants'; import { SHARED_ATTRIBUTES } from '../parser/attributes'; import { parseAttributes } from '../parser/parseAttributes'; import type { XY } from '../Point'; @@ -26,7 +27,6 @@ import type { TSVGReviver, TOptions, } from '../typedefs'; -import { CENTER, LEFT, TOP } from '../constants'; import type { CSSRules } from '../parser/typedefs'; interface UniquePathProps { @@ -80,8 +80,14 @@ export class Path< ) { super(options as Props); this._setPath(path || [], true); + // @TODO fix this bug not respecting origin typeof left === 'number' && this.set(LEFT, left); typeof top === 'number' && this.set(TOP, top); + // this.setRelativeXY( + // new Point(left ?? pathTL.x, top ?? pathTL.y), + // typeof left === 'number' ? this.originX : 'left', + // typeof top === 'number' ? this.originY : 'top' + // ); } /** @@ -278,7 +284,7 @@ export class Path< this.set({ width, height, pathOffset }); // using pathOffset because it match the use case. // if pathOffset change here we need to use left + width/2 , top + height/2 - adjustPosition && this.setPositionByOrigin(pathOffset, CENTER, CENTER); + adjustPosition && this.setRelativeCenterPoint(pathOffset); } _calcBoundsFromPath(): TBBox { diff --git a/src/shapes/Polyline.ts b/src/shapes/Polyline.ts index f14548cdfef..7807a451b18 100644 --- a/src/shapes/Polyline.ts +++ b/src/shapes/Polyline.ts @@ -6,6 +6,9 @@ import type { XY } from '../Point'; import { Point } from '../Point'; import type { Abortable, TClassProperties, TOptions } from '../typedefs'; import { classRegistry } from '../ClassRegistry'; +import { LEFT, TOP } from '../constants'; +import type { CSSRules } from '../parser/typedefs'; +import { cloneDeep } from '../util/internals/cloneDeep'; import { makeBoundingBoxFromPoints } from '../util/misc/boundingBoxFromPoints'; import { calcDimensionsMatrix, transformPoint } from '../util/misc/matrix'; import { projectStrokeOnPoints } from '../util/misc/projectStroke'; @@ -15,9 +18,7 @@ import { toFixed } from '../util/misc/toFixed'; import { FabricObject, cacheProperties } from './Object/FabricObject'; import type { FabricObjectProps, SerializedObjectProps } from './Object/types'; import type { ObjectEvents } from '../EventTypeDefs'; -import { cloneDeep } from '../util/internals/cloneDeep'; -import { CENTER, LEFT, TOP } from '../constants'; -import type { CSSRules } from '../parser/typedefs'; +import { sendVectorToPlane } from '../util'; export const polylineDefaultValues: Partial> = { /** @@ -205,67 +206,13 @@ export class Polyline< this._calcDimensions(); this.set({ width, height, pathOffset, strokeOffset, strokeDiff }); adjustPosition && - this.setPositionByOrigin( - new Point(left + width / 2, top + height / 2), - CENTER, - CENTER - ); - } - - /** - * @deprecated intermidiate method to be removed, do not use - */ - protected isStrokeAccountedForInDimensions() { - return this.exactBoundingBox; - } - - /** - * @override stroke is taken in account in size - */ - _getNonTransformedDimensions() { - return this.exactBoundingBox - ? // TODO: fix this - new Point(this.width, this.height) - : super._getNonTransformedDimensions(); - } - - /** - * @override stroke and skewing are taken into account when projecting stroke on points, - * therefore we don't want the default calculation to account for skewing as well. - * Though it is possible to pass `width` and `height` in `options`, doing so is very strange, use with discretion. - * - * @private - */ - _getTransformedDimensions(options: any = {}) { - if (this.exactBoundingBox) { - let size: Point; - /* When `strokeUniform = true`, any changes to the properties require recalculating the `width` and `height` because - the stroke projections are affected. - When `strokeUniform = false`, we don't need to recalculate for scale transformations, as the effect of scale on - projections follows a linear function (e.g. scaleX of 2 just multiply width by 2)*/ - if ( - Object.keys(options).some( - (key) => - this.strokeUniform || - (this.constructor as typeof Polyline).layoutProperties.includes( - key as keyof TProjectStrokeOnPointsOptions - ) + this.setRelativeCenterPoint( + // @TODO: needs testing + sendVectorToPlane( + new Point(left + width / 2, top + height / 2), + this.calcOwnMatrix() ) - ) { - const { width, height } = this._calcDimensions(options); - size = new Point(options.width ?? width, options.height ?? height); - } else { - size = new Point( - options.width ?? this.width, - options.height ?? this.height - ); - } - return size.multiply( - new Point(options.scaleX || this.scaleX, options.scaleY || this.scaleY) ); - } else { - return super._getTransformedDimensions(options); - } } /** diff --git a/src/shapes/Text/Text.ts b/src/shapes/Text/Text.ts index 4dcd8312d83..17341f46828 100644 --- a/src/shapes/Text/Text.ts +++ b/src/shapes/Text/Text.ts @@ -30,6 +30,7 @@ import { cacheProperties } from '../Object/FabricObject'; import type { Path } from '../Path'; import { TextSVGExportMixin } from './TextSVGExportMixin'; import { applyMixins } from '../../util/applyMixins'; +import { sizeAfterTransform } from '../../util/misc/objectTransforms'; import type { FabricObjectProps, SerializedObjectProps } from '../Object/types'; import type { StylePropertiesType } from './constants'; import { @@ -427,7 +428,6 @@ export class FabricText< this.setPathInfo(); } this.initDimensions(); - this.setCoords(); } /** @@ -475,6 +475,8 @@ export class FabricText< // once text is measured we need to make space fatter to make justified text. this.enlargeSpaces(); } + + this.invalidateCoords(); } /** @@ -1681,20 +1683,7 @@ export class FabricText< * @param {CanvasRenderingContext2D} ctx Context to render on */ render(ctx: CanvasRenderingContext2D) { - if (!this.visible) { - return; - } - if ( - this.canvas && - this.canvas.skipOffscreen && - !this.group && - !this.isOnScreen() - ) { - return; - } - if (this._forceClearCache) { - this.initDimensions(); - } + this._forceClearCache && this.initDimensions(); super.render(ctx); } @@ -1771,7 +1760,6 @@ export class FabricText< } if (needsDims && this.initialized) { this.initDimensions(); - this.setCoords(); } return this; } @@ -1848,40 +1836,25 @@ export class FabricText< .replace(/^\s+|\s+$|\n+/g, '') .replace(/\s+/g, ' '); - // this code here is probably the usual issue for SVG center find - // this can later looked at again and probably removed. - - const text = new this(textContent, { - left: left + dx, - top: top + dy, - underline: textDecoration.includes('underline'), - overline: textDecoration.includes('overline'), - linethrough: textDecoration.includes('line-through'), - // we initialize this as 0 - strokeWidth: 0, - fontSize, - ...restOfOptions, - }), - textHeightScaleFactor = text.getScaledHeight() / text.height, + const text = new this(textContent, options), + sizeInParent = sizeAfterTransform(text.width, text.height, text), + textHeightScaleFactor = sizeInParent.y / text.height, lineHeightDiff = (text.height + text.strokeWidth) * text.lineHeight - text.height, scaledDiff = lineHeightDiff * textHeightScaleFactor, - textHeight = text.getScaledHeight() + scaledDiff; - - let offX = 0; - /* - Adjust positioning: - x/y attributes in SVG correspond to the bottom-left corner of text bounding box - fabric output by default at top, left. - */ - if (textAnchor === CENTER) { - offX = text.getScaledWidth() / 2; - } - if (textAnchor === RIGHT) { - offX = text.getScaledWidth(); - } + textHeight = sizeInParent.y + scaledDiff; + text.set({ - left: text.left - offX, + // Adjust positioning: + // x/y attributes in SVG correspond to the bottom-left corner of text bounding box + // fabric output by default at top, left. + left: + text.left - + (textAnchor === 'center' + ? sizeInParent.x / 2 + : textAnchor === 'right' + ? sizeInParent.x + : 0), top: text.top - (textHeight - text.fontSize * (0.07 + text._fontSizeFraction)) / diff --git a/src/shapes/Textbox.ts b/src/shapes/Textbox.ts index 2f91768d92b..23c65f7fd22 100644 --- a/src/shapes/Textbox.ts +++ b/src/shapes/Textbox.ts @@ -128,6 +128,8 @@ export class Textbox< } // clear cache and re-calculate height this.height = this.calcTextHeight(); + + this.invalidateCoords(); } /** diff --git a/src/typedefs.ts b/src/typedefs.ts index 002558a94c7..99cafcf98da 100644 --- a/src/typedefs.ts +++ b/src/typedefs.ts @@ -1,6 +1,6 @@ // https://www.typescriptlang.org/docs/handbook/utility-types.html import type { Gradient } from './gradient/Gradient'; -import type { Pattern } from './Pattern'; +import type { Pattern } from './Pattern/Pattern'; import type { XY, Point } from './Point'; import type { FabricObject as BaseFabricObject } from './shapes/Object/Object'; diff --git a/src/util/index.ts b/src/util/index.ts index 2309eec076b..1bf422c6a30 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -6,7 +6,8 @@ export { calcAngleBetweenVectors, getUnitVector, calcVectorRotation, - crossProduct, + det, + dot, dotProduct, getOrthonormalVector, isBetweenVectors, diff --git a/src/util/internals/index.ts b/src/util/internals/index.ts index 0041a87c514..ebff31f1d36 100644 --- a/src/util/internals/index.ts +++ b/src/util/internals/index.ts @@ -1,4 +1,5 @@ export * from './findRight'; export * from './getRandomInt'; export * from './ifNaN'; +export * from './mapValues'; export * from './removeFromArray'; diff --git a/src/util/internals/mapValues.ts b/src/util/internals/mapValues.ts new file mode 100644 index 00000000000..aa829951f67 --- /dev/null +++ b/src/util/internals/mapValues.ts @@ -0,0 +1,10 @@ +export const mapValues = ( + collection: Record, + callbackfn: (value: T, key: K, collection: Record) => R +) => { + const out = {} as Record; + for (const key in collection) { + out[key] = callbackfn(collection[key], key, collection); + } + return out; +}; diff --git a/src/util/misc/matrix.ts b/src/util/misc/matrix.ts index 3164ab0fc77..7319b71d45d 100644 --- a/src/util/misc/matrix.ts +++ b/src/util/misc/matrix.ts @@ -32,8 +32,10 @@ export type TQrDecomposeOut = Required< Omit >; -export const isIdentityMatrix = (mat: TMat2D) => - mat.every((value, index) => value === iMatrix[index]); +export const isIdentityMatrix = (mat: TMat2D) => isMatrixEqual(mat, iMatrix); + +export const isMatrixEqual = (a: TMat2D, b: TMat2D) => + a.every((value, index) => value === b[index]); /** * Apply transform t to point p @@ -164,22 +166,20 @@ export const createTranslateMatrix = (x: number, y = 0): TMat2D => [ * @param {XY} [pivotPoint] pivot point to rotate around * @returns {TMat2D} matrix */ -export function createRotateMatrix( - { angle = 0 }: TRotateMatrixArgs = {}, - { x = 0, y = 0 }: Partial = {} -): TMat2D { - const angleRadiant = degreesToRadians(angle), - cosValue = cos(angleRadiant), - sinValue = sin(angleRadiant); - return [ - cosValue, - sinValue, - -sinValue, - cosValue, - x ? x - (cosValue * x - sinValue * y) : 0, - y ? y - (sinValue * x + cosValue * y) : 0, - ]; -} +export const createRotateMatrix = ({ + angle = 0, + rotation, +}: + | { angle: TDegree; rotation?: never } + | { angle?: never; rotation?: TRadian }): TMat2D => { + if (!angle && !rotation) { + return iMatrix.concat() as TMat2D; + } + const theta = rotation ?? degreesToRadians(angle), + cosin = cos(theta), + sinus = sin(theta); + return [cosin, sinus, -sinus, cosin, 0, 0]; +}; /** * Generate a scale matrix around the point (0,0) @@ -254,6 +254,22 @@ export const createSkewYMatrix = (skewValue: TDegree): TMat2D => [ 0, ]; +export const calcShearMatrix = ({ + skewX, + skewY, + shearX, + shearY, +}: { + skewX?: TDegree; + skewY?: TDegree; + shearX?: number; + shearY?: number; +}) => + multiplyTransformMatrices( + [1, 0, shearX ?? (skewX ? Math.tan(degreesToRadians(skewX)) : 0), 1, 0, 0], + [1, shearY ?? (skewY ? Math.tan(degreesToRadians(skewY)) : 0), 0, 1, 0, 0] + ); + /** * Returns a transform matrix starting from an object of the same kind of * the one returned from qrDecompose, useful also if you want to calculate some diff --git a/src/util/misc/objectEnlive.ts b/src/util/misc/objectEnlive.ts index 788b224a293..43b53dab9f6 100644 --- a/src/util/misc/objectEnlive.ts +++ b/src/util/misc/objectEnlive.ts @@ -1,5 +1,5 @@ import { noop } from '../../constants'; -import type { Pattern } from '../../Pattern'; +import type { Pattern } from '../../Pattern/Pattern'; import type { FabricObject } from '../../shapes/Object/FabricObject'; import type { Abortable, diff --git a/src/util/misc/objectTransforms.ts b/src/util/misc/objectTransforms.ts index 3f7e296eaa8..1d078dd26eb 100644 --- a/src/util/misc/objectTransforms.ts +++ b/src/util/misc/objectTransforms.ts @@ -1,6 +1,5 @@ import { Point } from '../../Point'; -import { CENTER } from '../../constants'; -import type { FabricObject } from '../../shapes/Object/Object'; +import type { ObjectTransformations as BaseFabricObject } from '../../shapes/Object/ObjectTransformations'; import type { TMat2D } from '../../typedefs'; import { makeBoundingBoxFromPoints } from './boundingBoxFromPoints'; import { @@ -17,11 +16,11 @@ import { * Removing from an object a transform that rotate by 30deg is like rotating by 30deg * in the opposite direction. * This util is used to add objects inside transformed groups or nested groups. - * @param {FabricObject} object the object you want to transform + * @param {BaseFabricObject} object the object you want to transform * @param {TMat2D} transform the destination transform */ export const removeTransformFromObject = ( - object: FabricObject, + object: BaseFabricObject, transform: TMat2D ) => { const inverted = invertTransform(transform), @@ -37,10 +36,13 @@ export const removeTransformFromObject = ( * this is equivalent to change the space where the object is drawn. * Adding to an object a transform that scale by 2 is like scaling it by 2. * This is used when removing an object from an active selection for example. - * @param {FabricObject} object the object you want to transform + * @param {BaseFabricObject} object the object you want to transform * @param {Array} transform the destination transform */ -export const addTransformToObject = (object: FabricObject, transform: TMat2D) => +export const addTransformToObject = ( + object: BaseFabricObject, + transform: TMat2D +) => applyTransformToObject( object, multiplyTransformMatrices(transform, object.calcOwnMatrix()) @@ -48,11 +50,11 @@ export const addTransformToObject = (object: FabricObject, transform: TMat2D) => /** * discard an object transform state and apply the one from the matrix. - * @param {FabricObject} object the object you want to transform + * @param {BaseFabricObject} object the object you want to transform * @param {Array} transform the destination transform */ export const applyTransformToObject = ( - object: FabricObject, + object: BaseFabricObject, transform: TMat2D ) => { const { translateX, translateY, scaleX, scaleY, ...otherOptions } = @@ -62,13 +64,13 @@ export const applyTransformToObject = ( object.flipY = false; Object.assign(object, otherOptions); object.set({ scaleX, scaleY }); - object.setPositionByOrigin(center, CENTER, CENTER); + object.setRelativeCenterPoint(center); }; /** * reset an object transform state to neutral. Top and left are not accounted for - * @param {FabricObject} target object to transform + * @param {BaseFabricObject} target object to transform */ -export const resetObjectTransform = (target: FabricObject) => { +export const resetObjectTransform = (target: BaseFabricObject) => { target.scaleX = 1; target.scaleY = 1; target.skewX = 0; @@ -80,10 +82,10 @@ export const resetObjectTransform = (target: FabricObject) => { /** * Extract Object transform values - * @param {FabricObject} target object to read from + * @param {BaseFabricObject} target object to read from * @return {Object} Components of transform */ -export const saveObjectTransform = (target: FabricObject) => ({ +export const saveObjectTransform = (target: BaseFabricObject) => ({ scaleX: target.scaleX, scaleY: target.scaleY, skewX: target.skewX, diff --git a/src/util/misc/planeChange.ts b/src/util/misc/planeChange.ts index a28fc2828a6..e10547c11d0 100644 --- a/src/util/misc/planeChange.ts +++ b/src/util/misc/planeChange.ts @@ -1,5 +1,5 @@ import { iMatrix } from '../../constants'; -import type { Point } from '../../Point'; +import { Point, XY } from '../../Point'; import type { FabricObject } from '../../shapes/Object/Object'; import type { TMat2D } from '../../typedefs'; import { invertTransform, multiplyTransformMatrices } from './matrix'; @@ -17,6 +17,21 @@ export const calcPlaneChangeMatrix = ( to: TMat2D = iMatrix ) => multiplyTransformMatrices(invertTransform(to), from); +export const calcBaseChangeMatrix = ( + from: [XY, XY] | undefined, + to: [XY, XY], + destinationCenter: XY = { x: 0, y: 0 } +) => { + const [a, b, c, d] = multiplyTransformMatrices( + [to[0].x, to[0].y, to[1].x, to[1].y, 0, 0], + from + ? invertTransform([from[0].x, from[0].y, from[1].x, from[1].y, 0, 0]) + : iMatrix, + true + ); + return [a, b, c, d, destinationCenter.x, destinationCenter.y] as TMat2D; +}; + /** * Sends a point from the source coordinate plane to the destination coordinate plane.\ * From the canvas/viewer's perspective the point remains unchanged. diff --git a/src/util/misc/projectStroke/StrokeLineJoinProjections.ts b/src/util/misc/projectStroke/StrokeLineJoinProjections.ts index 24e6c21836d..a4d41829466 100644 --- a/src/util/misc/projectStroke/StrokeLineJoinProjections.ts +++ b/src/util/misc/projectStroke/StrokeLineJoinProjections.ts @@ -6,7 +6,7 @@ import { degreesToRadians } from '../radiansDegreesConversion'; import { calcAngleBetweenVectors, calcVectorRotation, - crossProduct, + det, getOrthonormalVector, getUnitVector, isBetweenVectors, @@ -14,7 +14,7 @@ import { rotateVector, } from '../vectors'; import { StrokeProjectionsBase } from './StrokeProjectionsBase'; -import type { TProjection, TProjectStrokeOnPointsOptions } from './types'; +import type { TProjectStrokeOnPointsOptions, TProjection } from './types'; const zeroVector = new Point(); @@ -275,7 +275,7 @@ export class StrokeLineJoinProjections extends StrokeProjectionsBase { this.bisector.multiply(this.strokeUniformScalar).scalarMultiply(-1) ), // the beginning of the circle segment is always to the right of the comparison vector (cross product > 0) - isProj0Start = crossProduct(proj0, comparisonVector) > 0, + isProj0Start = det(proj0, comparisonVector) > 0, startCircle = isProj0Start ? proj0 : proj1, endCircle = isProj0Start ? proj1 : proj0; if (!this.isSkewed()) { diff --git a/src/util/misc/resolveOrigin.ts b/src/util/misc/resolveOrigin.ts index c355875bf30..e1b3e058af5 100644 --- a/src/util/misc/resolveOrigin.ts +++ b/src/util/misc/resolveOrigin.ts @@ -1,4 +1,5 @@ import type { TOriginX, TOriginY } from '../../typedefs'; +import { Point } from '../../Point'; const originOffset = { left: -0.5, @@ -20,3 +21,6 @@ export const resolveOrigin = ( typeof originValue === 'string' ? originOffset[originValue] : originValue - 0.5; + +export const resolveOriginPoint = (originX: TOriginX, originY: TOriginY) => + new Point(resolveOrigin(originX), resolveOrigin(originY)); diff --git a/src/util/misc/vectors.ts b/src/util/misc/vectors.ts index 7c268cd6297..48efc7c7492 100644 --- a/src/util/misc/vectors.ts +++ b/src/util/misc/vectors.ts @@ -2,7 +2,6 @@ import type { XY } from '../../Point'; import { Point } from '../../Point'; import type { TRadian } from '../../typedefs'; -const unitVectorX = new Point(1, 0); const zero = new Point(); /** @@ -30,22 +29,26 @@ export const createVector = (from: XY, to: XY): Point => */ export const magnitude = (point: Point) => point.distanceFrom(zero); +export const dot = (a: Point, b: Point) => a.x * b.x + a.y * b.y; + +export const det = (a: Point, b: Point) => a.x * b.y - a.y * b.x; + /** * Calculates the angle between 2 vectors * @param {Point} a * @param {Point} b * @returns the angle in radians from `a` to `b` */ -export const calcAngleBetweenVectors = (a: Point, b: Point): TRadian => - Math.atan2(crossProduct(a, b), dotProduct(a, b)) as TRadian; +export const calcAngleBetweenVectors = (a: Point, b: Point): TRadian => { + return Math.atan2(det(a, b), dot(a, b)) as TRadian; +}; /** * Calculates the angle between the x axis and the vector * @param {Point} v * @returns the angle in radians of `v` */ -export const calcVectorRotation = (v: Point) => - calcAngleBetweenVectors(unitVectorX, v); +export const calcVectorRotation = (v: XY) => Math.atan2(v.y, v.x) as TRadian; /** * @param {Point} v @@ -54,33 +57,45 @@ export const calcVectorRotation = (v: Point) => export const getUnitVector = (v: Point): Point => v.eq(zero) ? v : v.scalarDivide(magnitude(v)); +export const dotProduct = (v: Point, onto: Point) => { + const size = magnitude(v); + const baseSize = magnitude(onto); + return size && baseSize ? dot(v, onto) / baseSize : 0; +}; + /** - * @param {Point} v - * @param {Boolean} [counterClockwise] the direction of the orthogonal vector, defaults to `true` - * @returns {Point} the unit orthogonal vector + * @param {Point} A + * @param {Point} B + * @param {Point} C + * @returns {{ vector: Point, angle: TRadian}} vector representing the bisector of A and A's angle */ -export const getOrthonormalVector = ( - v: Point, - counterClockwise = true -): Point => - getUnitVector(new Point(-v.y, v.x).scalarMultiply(counterClockwise ? 1 : -1)); +export const getBisector = (A: Point, B: Point, C: Point) => { + const AB = createVector(A, B), + AC = createVector(A, C), + alpha = calcAngleBetweenVectors(AB, AC); + return { + vector: getUnitVector(rotateVector(AB, alpha / 2)), + angle: alpha, + }; +}; /** - * Cross product of two vectors in 2D - * @param {Point} a - * @param {Point} b - * @returns {number} the magnitude of Z vector + * @param {Point} v + * @param {Boolean} [counterClockwise] the direction of the orthogonal vector, defaults to `true` + * @returns {Point} the unit orthogonal vector */ -export const crossProduct = (a: Point, b: Point): number => - a.x * b.y - a.y * b.x; +export const getOrthogonalVector = (v: Point, counterClockwise = true): Point => + new Point(-v.y, v.x).scalarMultiply(counterClockwise ? 1 : -1); /** - * Dot product of two vectors in 2D - * @param {Point} a - * @param {Point} b - * @returns {number} + * @param {Point} v + * @param {Boolean} [counterClockwise] the direction of the orthogonal vector, defaults to `true` + * @returns {Point} the unit orthogonal vector */ -export const dotProduct = (a: Point, b: Point): number => a.x * b.x + a.y * b.y; +export const getOrthonormalVector = ( + v: Point, + counterClockwise = true +): Point => getUnitVector(getOrthogonalVector(v, counterClockwise)); /** * Checks if the vector is between two others. It is considered @@ -93,8 +108,8 @@ export const dotProduct = (a: Point, b: Point): number => a.x * b.x + a.y * b.y; */ export const isBetweenVectors = (t: Point, a: Point, b: Point): boolean => { if (t.eq(a) || t.eq(b)) return true; - const AxB = crossProduct(a, b), - AxT = crossProduct(a, t), - BxT = crossProduct(b, t); + const AxB = det(a, b), + AxT = det(a, t), + BxT = det(b, t); return AxB >= 0 ? AxT >= 0 && BxT <= 0 : !(AxT <= 0 && BxT >= 0); }; diff --git a/src/util/transform_matrix_removal.ts b/src/util/transform_matrix_removal.ts index b7dc6b48dcc..af038972e7d 100644 --- a/src/util/transform_matrix_removal.ts +++ b/src/util/transform_matrix_removal.ts @@ -1,4 +1,3 @@ -import { CENTER } from '../constants'; import type { FabricImage } from '../shapes/Image'; import type { FabricObject } from '../shapes/Object/FabricObject'; import type { TMat2D } from '../typedefs'; @@ -57,5 +56,5 @@ export const removeTransformMatrixForSvgParsing = ( object.width = preserveAspectRatioOptions.width; object.height = preserveAspectRatioOptions.height; } - object.setPositionByOrigin(center, CENTER, CENTER); + object.setRelativeCenterPoint(center); }; diff --git a/test/unit/cache.js b/test/unit/cache.js index f98e0c5dad4..3f62b09dafc 100644 --- a/test/unit/cache.js +++ b/test/unit/cache.js @@ -9,20 +9,20 @@ } }); - QUnit.test('Cache.limitDimsByArea', function(assert) { + QUnit.test.skip('Cache.limitDimsByArea', function(assert) { assert.ok(typeof fabric.cache.limitDimsByArea === 'function'); var [x, y] = fabric.cache.limitDimsByArea(1); assert.equal(x, 100); assert.equal(y, 100); }); - QUnit.test('Cache.limitDimsByArea ar > 1', function(assert) { + QUnit.test.skip('Cache.limitDimsByArea ar > 1', function(assert) { var [x , y] = fabric.cache.limitDimsByArea(3); assert.equal(x, 173); assert.equal(y, 57); }); - QUnit.test('Cache.limitDimsByArea ar < 1', function(assert) { + QUnit.test.skip('Cache.limitDimsByArea ar < 1', function(assert) { var [x, y] = fabric.cache.limitDimsByArea(1 / 3); assert.equal(x, 57); assert.equal(y, 173); diff --git a/test/unit/canvas.js b/test/unit/canvas.js index 73b8d9ba760..87087381bb4 100644 --- a/test/unit/canvas.js +++ b/test/unit/canvas.js @@ -1654,8 +1654,8 @@ assert.equal(t.originY, rect.originY, 'no origin change for drag'); eventStub = { - clientX: canvasOffset.left + rect.oCoords.tl.corner.tl.x + 1, - clientY: canvasOffset.top + rect.oCoords.tl.corner.tl.y + 1, + clientX: canvasOffset.left + rect.controlCoords.tl.corner.tl.x + 1, + clientY: canvasOffset.top + rect.controlCoords.tl.corner.tl.y + 1, target: canvas.upperCanvasEl }; rect.__corner = rect.findControl( @@ -1700,8 +1700,8 @@ // to be replaced with new api test // eventStub = { - // clientX: canvasOffset.left + rect.oCoords.mtr.x, - // clientY: canvasOffset.top + rect.oCoords.mtr.y, + // clientX: canvasOffset.left + rect.controlCoords.mtr.x, + // clientY: canvasOffset.top + rect.controlCoords.mtr.y, // target: canvas.upperCanvasEl, // }; // canvas._setupCurrentTransform(eventStub, rect, alreadySelected); @@ -1721,8 +1721,8 @@ // var canvasEl = canvas.getElement(), // canvasOffset = fabric.util.getElementOffset(canvasEl); // var eventStub = { - // clientX: canvasOffset.left + rect.oCoords.mtr.x, - // clientY: canvasOffset.top + rect.oCoords.mtr.y, + // clientX: canvasOffset.left + rect.controlCoords.mtr.x, + // clientY: canvasOffset.top + rect.controlCoords.mtr.y, // target: canvas.upperCanvasEl, // }; // canvas._setupCurrentTransform(eventStub, rect); @@ -1739,8 +1739,8 @@ // var canvasEl = canvas.getElement(), // canvasOffset = fabric.util.getElementOffset(canvasEl); // var eventStub = { - // clientX: canvasOffset.left + rect.oCoords.mtr.x, - // clientY: canvasOffset.top + rect.oCoords.mtr.y, + // clientX: canvasOffset.left + rect.controlCoords.mtr.x, + // clientY: canvasOffset.top + rect.controlCoords.mtr.y, // target: canvas.upperCanvasEl, // }; // canvas._setupCurrentTransform(eventStub, rect); diff --git a/test/unit/canvas_events.js b/test/unit/canvas_events.js index c7b6d260868..b0702630a00 100644 --- a/test/unit/canvas_events.js +++ b/test/unit/canvas_events.js @@ -840,7 +840,6 @@ var target = new fabric.Rect({ width: 100, height: 100 }); canvas.add(target); canvas.setActiveObject(target); - target.setCoords(); const expected = { mt: 'n-resize', mb: 's-resize', @@ -852,8 +851,8 @@ br: 'se-resize', mtr: 'crosshair', }; - Object.entries(target.oCoords).forEach(([corner, coords]) => { - const e = { clientX: coords.x, clientY: coords.y, [key]: false, target: canvas.upperCanvasEl }; + Object.entries(target.controlCoords).forEach(([corner, { position }]) => { + const e = { clientX: position.x, clientY: position.y, [key]: false }; canvas._setCursorFromEvent(e, target); assert.equal(canvas.upperCanvasEl.style.cursor, expected[corner], `${expected[corner]} action is not disabled`); }) @@ -870,8 +869,8 @@ mtr: 'crosshair', }; target.lockScalingX = true; - Object.entries(target.oCoords).forEach(([corner, coords]) => { - const e = { clientX: coords.x, clientY: coords.y, [key]: false, target: canvas.upperCanvasEl }; + Object.entries(target.controlCoords).forEach(([corner, { position }]) => { + const e = { clientX: position.x, clientY: position.y, [key]: false }; canvas._setCursorFromEvent(e, target); assert.equal(canvas.upperCanvasEl.style.cursor, expectedLockScalinX[corner], `${corner} is ${expectedLockScalinX[corner]} for lockScalingX`); }); @@ -887,8 +886,8 @@ br: 'se-resize', mtr: 'crosshair', }; - Object.entries(target.oCoords).forEach(([corner, coords]) => { - const e = { clientX: coords.x, clientY: coords.y, [key]: false, [key2]: true, target: canvas.upperCanvasEl }; + Object.entries(target.controlCoords).forEach(([corner, { position }]) => { + const e = { clientX: position.x, clientY: position.y, [key]: false, [key2]: true }; canvas._setCursorFromEvent(e, target); assert.equal(canvas.upperCanvasEl.style.cursor, expectedUniScale[corner], `${corner} is ${expectedUniScale[corner]} for uniScaleKey pressed`); }); @@ -906,8 +905,8 @@ }; target.lockScalingX = false; target.lockScalingY = true; - Object.entries(target.oCoords).forEach(([corner, coords]) => { - const e = { clientX: coords.x, clientY: coords.y, [key]: false, target: canvas.upperCanvasEl }; + Object.entries(target.controlCoords).forEach(([corner, { position }]) => { + const e = { clientX: position.x, clientY: position.y, [key]: false }; canvas._setCursorFromEvent(e, target); assert.equal(canvas.upperCanvasEl.style.cursor, expectedLockScalinY[corner], `${corner} is ${expectedLockScalinY[corner]} for lockScalingY`); }); @@ -922,8 +921,8 @@ br: 'se-resize', mtr: 'crosshair', }; - Object.entries(target.oCoords).forEach(([corner, coords]) => { - const e = { clientX: coords.x, clientY: coords.y, [key]: false, [key2]: true, target: canvas.upperCanvasEl }; + Object.entries(target.controlCoords).forEach(([corner, { position }]) => { + const e = { clientX: position.x, clientY: position.y, [key]: false, [key2]: true }; canvas._setCursorFromEvent(e, target); assert.equal(canvas.upperCanvasEl.style.cursor, expectedLockScalinYUniscaleKey[corner], `${corner} is ${expectedLockScalinYUniscaleKey[corner]} for lockScalingY + uniscaleKey`); }); @@ -941,8 +940,8 @@ }; target.lockScalingY = true; target.lockScalingX = true; - Object.entries(target.oCoords).forEach(([corner, coords]) => { - const e = { clientX: coords.x, clientY: coords.y, [key]: false, target: canvas.upperCanvasEl }; + Object.entries(target.controlCoords).forEach(([corner, { position }]) => { + const e = { clientX: position.x, clientY: position.y, [key]: false }; canvas._setCursorFromEvent(e, target); assert.equal(canvas.upperCanvasEl.style.cursor, expectedAllLock[corner], `${corner} is ${expectedAllLock[corner]} for all locked`); }); @@ -958,8 +957,8 @@ br: 'not-allowed', mtr: 'crosshair', }; - Object.entries(target.oCoords).forEach(([corner, coords]) => { - const e = { clientX: coords.x, clientY: coords.y, [key]: false, [key2]: true, target: canvas.upperCanvasEl }; + Object.entries(target.controlCoords).forEach(([corner, { position }]) => { + const e = { clientX: position.x, clientY: position.y, [key]: false, [key2]: true }; canvas._setCursorFromEvent(e, target); assert.equal(canvas.upperCanvasEl.style.cursor, expectedAllLockUniscale[corner], `${corner} is ${expectedAllLockUniscale[corner]} for all locked + uniscale`); }); @@ -967,7 +966,7 @@ target.lockRotation = true; target.lockScalingY = false; target.lockScalingX = false; - const e = { clientX: target.oCoords.mtr.x, clientY: target.oCoords.mtr.y, [key]: false, target: canvas.upperCanvasEl }; + const e = { clientX: target.controlCoords.mtr.position.x, clientY: target.controlCoords.mtr.position.y, [key]: false }; canvas._setCursorFromEvent(e, target); assert.equal(canvas.upperCanvasEl.style.cursor, 'not-allowed', `mtr is not allowed for locked rotation`); @@ -975,8 +974,8 @@ target.lockSkewingY = true; target.lockRotation = false; // with lock-skewing we are back at normal - Object.entries(target.oCoords).forEach(([corner, coords]) => { - const e = { clientX: coords.x, clientY: coords.y, [key]: false, target: canvas.upperCanvasEl }; + Object.entries(target.controlCoords).forEach(([corner, { position }]) => { + const e = { clientX: position.x, clientY: position.y, [key]: false }; canvas._setCursorFromEvent(e, target); assert.equal(canvas.upperCanvasEl.style.cursor, expected[corner], `${key} is ${expected[corner]} for both lockskewing`); }); @@ -995,8 +994,8 @@ br: 'se-resize', mtr: 'crosshair', }; - Object.entries(target.oCoords).forEach(([corner, coords]) => { - const e = { clientX: coords.x, clientY: coords.y, [key]: true, target: canvas.upperCanvasEl }; + Object.entries(target.controlCoords).forEach(([corner, { position }]) => { + const e = { clientX: position.x, clientY: position.y, [key]: true }; canvas._setCursorFromEvent(e, target); assert.equal(canvas.upperCanvasEl.style.cursor, expectedLockSkewingY[corner], `${corner} ${expectedLockSkewingY[corner]} for lockSkewingY`); }); @@ -1015,8 +1014,8 @@ br: 'se-resize', mtr: 'crosshair', }; - Object.entries(target.oCoords).forEach(([corner, coords]) => { - const e = { clientX: coords.x, clientY: coords.y, [key]: true, target: canvas.upperCanvasEl }; + Object.entries(target.controlCoords).forEach(([corner, { position }]) => { + const e = { clientX: position.x, clientY: position.y, [key]: true }; canvas._setCursorFromEvent(e, target); assert.equal(canvas.upperCanvasEl.style.cursor, expectedLockSkewingX[corner], `${corner} is ${expectedLockSkewingX[corner]} for lockSkewingX`); }); diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index 4645f9ae780..4e835694f62 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -1425,7 +1425,6 @@ assert.equal(canvas.item(2), rect3); rect1.set({ top: 100 }); - rect1.setCoords(); canvas.sendObjectBackwards(rect1, true); assert.equal(canvas.item(1), rect1); @@ -1482,7 +1481,6 @@ assert.equal(canvas.item(2), rect3); rect2.set({ left: 200 }); - rect2.setCoords(); canvas.bringObjectForward(rect2, true); // rect2, rect3 do not overlap @@ -1714,27 +1712,6 @@ canvas.viewportTransform = fabric.StaticCanvas.getDefaults().viewportTransform; }); - QUnit.test('setViewportTransform calls objects setCoords', function(assert) { - var vpt = [2, 0, 0, 2, 50, 50]; - assert.deepEqual(canvas.viewportTransform, [1, 0, 0, 1, 0, 0], 'initial viewport is identity matrix'); - var rect = new fabric.Rect({ width: 10, heigth: 10 }); - var rectBg = new fabric.Rect({ width: 10, heigth: 10 }); - var rectOverlay = new fabric.Rect({ width: 10, heigth: 10 }); - canvas.add(rect); - canvas.cancelRequestedRender(); - rectBg.canvas = canvas; - canvas.backgroundImage = rectBg; - rectOverlay.canvas = canvas; - canvas.overlayImage = rectOverlay; - assert.deepEqual(new fabric.Point(rect.oCoords.tl), new fabric.Point(0,0), 'rect oCoords are set for normal viewport'); - assert.equal(rectBg.oCoords, undefined, 'rectBg oCoords are not set'); - assert.equal(rectOverlay.oCoords, undefined, 'rectOverlay oCoords are not set'); - canvas.setViewportTransform(vpt); - assert.deepEqual(new fabric.Point(rect.oCoords.tl), new fabric.Point(50,50), 'rect oCoords are set'); - assert.deepEqual(new fabric.Point(rectBg.oCoords.tl), new fabric.Point(50,50), 'rectBg oCoords are set'); - assert.deepEqual(new fabric.Point(rectOverlay.oCoords.tl), new fabric.Point(50,50), 'rectOverlay oCoords are set'); - }); - QUnit.test('getZoom', function(assert) { assert.ok(typeof canvas.getZoom === 'function'); var vpt = [2, 0, 0, 2, 50, 50]; @@ -1794,41 +1771,40 @@ assert.equal(context, canvas.contextContainer, 'should return the context container'); }); - QUnit.test('calcViewportBoundaries', function(assert) { - assert.ok(typeof canvas.calcViewportBoundaries === 'function'); - canvas.calcViewportBoundaries(); - assert.deepEqual(canvas.vptCoords.tl, new fabric.Point(0, 0), 'tl is 0,0'); - assert.deepEqual(canvas.vptCoords.tr, new fabric.Point(canvas.getWidth(), 0), 'tr is width, 0'); - assert.deepEqual(canvas.vptCoords.bl, new fabric.Point(0, canvas.getHeight()), 'bl is 0, height'); - assert.deepEqual(canvas.vptCoords.br, new fabric.Point(canvas.getWidth(), canvas.getHeight()), 'tl is width, height'); + QUnit.test('getViewportBBox', function(assert) { + assert.ok(typeof canvas.getViewportBBox === 'function'); + const { tl, tr, bl, br } = canvas.getViewportBBox(); + assert.deepEqual(tl, new fabric.Point(0, 0), 'tl is 0,0'); + assert.deepEqual(tr, new fabric.Point(canvas.getWidth(), 0), 'tr is width, 0'); + assert.deepEqual(bl, new fabric.Point(0, canvas.getHeight()), 'bl is 0, height'); + assert.deepEqual(br, new fabric.Point(canvas.getWidth(), canvas.getHeight()), 'tl is width, height'); }); - QUnit.test('calcViewportBoundaries with zoom', function(assert) { - assert.ok(typeof canvas.calcViewportBoundaries === 'function'); + QUnit.test('getViewportBBox with zoom', function(assert) { canvas.setViewportTransform([2, 0, 0, 2, 0, 0]); - assert.deepEqual(canvas.vptCoords.tl, new fabric.Point(0, 0), 'tl is 0,0'); - assert.deepEqual(canvas.vptCoords.tr, new fabric.Point(canvas.getWidth() / 2, 0), 'tl is 0,0'); - assert.deepEqual(canvas.vptCoords.bl, new fabric.Point(0, canvas.getHeight() / 2), 'tl is 0,0'); - assert.deepEqual(canvas.vptCoords.br, new fabric.Point(canvas.getWidth() / 2, canvas.getHeight() / 2), 'tl is 0,0'); + const { tl, tr, bl, br } = canvas.getViewportBBox(); + assert.deepEqual(tl, new fabric.Point(0, 0), 'tl is 0,0'); + assert.deepEqual(tr, new fabric.Point(canvas.getWidth() / 2, 0), 'tl is 0,0'); + assert.deepEqual(bl, new fabric.Point(0, canvas.getHeight() / 2), 'tl is 0,0'); + assert.deepEqual(br, new fabric.Point(canvas.getWidth() / 2, canvas.getHeight() / 2), 'tl is 0,0'); }); - QUnit.test('calcViewportBoundaries with zoom and translation', function(assert) { - assert.ok(typeof canvas.calcViewportBoundaries === 'function'); + QUnit.test('getViewportBBox with zoom and translation', function(assert) { canvas.setViewportTransform([2, 0, 0, 2, -60, 60]); - assert.deepEqual(canvas.vptCoords.tl, new fabric.Point(30, -30), 'tl is 0,0'); - assert.deepEqual(canvas.vptCoords.tr, new fabric.Point(30 + canvas.getWidth() / 2, -30), 'tl is 0,0'); - assert.deepEqual(canvas.vptCoords.bl, new fabric.Point(30, canvas.getHeight() / 2 - 30), 'tl is 0,0'); - assert.deepEqual(canvas.vptCoords.br, new fabric.Point(30 + canvas.getWidth() / 2, canvas.getHeight() / 2 - 30), 'tl is 0,0'); + const { tl, tr, bl, br } = canvas.getViewportBBox(); + assert.deepEqual(tl, new fabric.Point(30, -30), 'tl is 0,0'); + assert.deepEqual(tr, new fabric.Point(30 + canvas.getWidth() / 2, -30), 'tl is 0,0'); + assert.deepEqual(bl, new fabric.Point(30, canvas.getHeight() / 2 - 30), 'tl is 0,0'); + assert.deepEqual(br, new fabric.Point(30 + canvas.getWidth() / 2, canvas.getHeight() / 2 - 30), 'tl is 0,0'); }); - QUnit.test('calcViewportBoundaries with flipped zoom and translation', function (assert) { - assert.ok(typeof canvas.calcViewportBoundaries === 'function'); + QUnit.test('getViewportBBox with flipped zoom and translation', function (assert) { canvas.setViewportTransform([2, 0, 0, -2, -60, 60]); - canvas.calcViewportBoundaries(); - assert.deepEqual({ x: canvas.vptCoords.tl.x, y: canvas.vptCoords.tl.y }, { x: 30, y: -145 }, 'tl is 30, -145'); - assert.deepEqual({ x: canvas.vptCoords.tr.x, y: canvas.vptCoords.tr.y }, { x: 130, y: -145 }, 'tr is 130, -145'); - assert.deepEqual({ x: canvas.vptCoords.bl.x, y: canvas.vptCoords.bl.y }, { x: 30, y: 30 }, 'bl is 30,-70'); - assert.deepEqual({ x: canvas.vptCoords.br.x, y: canvas.vptCoords.br.y }, { x: 130, y: 30 }, 'br is 130,-70'); + const { tl, tr, bl, br } = canvas.getViewportBBox(); + assert.deepEqual({ x: tl.x, y: tl.y }, { x: 30, y: -145 }, 'tl is 30, -145'); + assert.deepEqual({ x: tr.x, y: tr.y }, { x: 130, y: -145 }, 'tr is 130, -145'); + assert.deepEqual({ x: bl.x, y: bl.y }, { x: 30, y: 30 }, 'bl is 30,-70'); + assert.deepEqual({ x: br.x, y: br.y }, { x: 130, y: 30 }, 'br is 130,-70'); }); QUnit.test('getRetinaScaling', function(assert) { diff --git a/test/unit/circle.js b/test/unit/circle.js index 632fb48bd67..4a56932f843 100644 --- a/test/unit/circle.js +++ b/test/unit/circle.js @@ -29,7 +29,7 @@ assert.equal(circle.getRadiusX(), 10); assert.equal(circle.getRadiusY(), 10); - circle.scale(2); + circle.scale(2, 2); assert.equal(circle.getRadiusX(), 20); assert.equal(circle.getRadiusY(), 20); @@ -265,13 +265,13 @@ QUnit.test('cloning and radius, width, height', function(assert) { var done = assert.async(); var circle = new fabric.Circle({ radius: 10, strokeWidth: 0}); - circle.scale(2); + circle.scale(2, 2); circle.clone().then(function(clone) { assert.equal(clone.width, 20); - assert.equal(clone.getScaledWidth(), 40); assert.equal(clone.height, 20); - assert.equal(clone.getScaledHeight(), 40); + assert.equal(clone.scaleX, 2); + assert.equal(clone.scaleY, 2); assert.equal(clone.radius, 10); done(); }); diff --git a/test/unit/controls_handlers.js b/test/unit/controls_handlers.js index 83b11d2b513..f055ff7c6c4 100644 --- a/test/unit/controls_handlers.js +++ b/test/unit/controls_handlers.js @@ -13,14 +13,12 @@ canvas.clear(); }); function prepareTransform(target, corner) { - var origin = canvas._getOriginFromCorner(target, corner); + const origin = new fabric.Point(target.controls[corner]).scalarMultiply(-1); return { target, corner, originX: origin.x, originY: origin.y, - signX: 1, - signY: 1, }; } QUnit.test('changeWidth changes the width', function(assert) { @@ -240,23 +238,20 @@ mb: 'y', }[controlKey] const AXIS = axis.toUpperCase(); - const signKey = `sign${AXIS}`; const scaleKey = `scale${AXIS}`; const flipKey = `flip${AXIS}`; const isX = axis === 'x'; QUnit.test(`scaling ${AXIS} from ${controlKey} keeps the same sign when scale = 0`, function (assert) { transform = prepareTransform(transform.target, controlKey); - const size = transform.target._getTransformedDimensions()[axis]; + const size = transform.target.bbox.sendToParent().getBBoxVector()[axis]; const factor = 0.5; const fn = fabric.controlsUtils[`scaling${AXIS}`]; const exec = point => { const { target } = transform; - const origin = target.translateToGivenOrigin( - target.getRelativeCenterPoint(), - 'center', - 'center', - transform.originX, - transform.originY + const origin = fabric.util.sendPointToPlane( + target.getXY(transform.originX, transform.originY), + undefined, + target.group?.calcTransformMatrix() ); const pointer = point.add(origin); fn(eventData, transform, pointer.x, pointer.y); @@ -266,23 +261,18 @@ Number(!isX) ).scalarMultiply(size * factor); exec(new fabric.Point()); - assert.equal(transform[signKey], 1, `${signKey} value after scaling`); assert.equal(transform.target[flipKey], false, `${flipKey} value after scaling`); assert.ok(transform.target[scaleKey] <= 0.001, `${scaleKey} value after scaling back to origin`); exec(deltaFromControl); - assert.equal(transform[signKey], 1, `${signKey} value after scaling`); assert.equal(transform.target[flipKey], false, `${flipKey} value after scaling`); assert.equal(transform.target[scaleKey], factor, `${scaleKey} value after scaling`); exec(new fabric.Point()); - assert.equal(transform[signKey], 1, `${signKey} value after scaling`); assert.equal(transform.target[flipKey], false, `${flipKey} value after scaling`); assert.ok(transform.target[scaleKey] <= 0.001, `${scaleKey} value after scaling back to origin`); exec(deltaFromControl.scalarMultiply(-1)); - assert.equal(transform[signKey], -1, `${signKey} value after scaling`); assert.equal(transform.target[flipKey], true, `${flipKey} value after scaling`); assert.equal(transform.target[scaleKey], factor, `${scaleKey} value after scaling`); exec(new fabric.Point()); - assert.equal(transform[signKey], -1, `${signKey} value after scaling`); assert.equal(transform.target[flipKey], true, `${flipKey} value after scaling`); assert.ok(transform.target[scaleKey] <= 0.001, `${scaleKey} value after scaling back to origin`); }); diff --git a/test/unit/group.js b/test/unit/group.js index 4e2a4e334be..ee8dd8fc7c3 100644 --- a/test/unit/group.js +++ b/test/unit/group.js @@ -329,7 +329,7 @@ QUnit.test('containsPoint', function(assert) { var group = makeGroupWith2Objects(); - group.set({ originX: 'center', originY: 'center' }).setCoords(); + group.set({ originX: 'center', originY: 'center' }); // Rect #1 top: 100, left: 100, width: 30, height: 10 // Rect #2 top: 120, left: 50, width: 10, height: 40 @@ -338,16 +338,16 @@ assert.ok(!group.containsPoint(new fabric.Point( 0, 0 ))); - group.scale(2); + group.scale(2, 2); assert.ok(group.containsPoint(new fabric.Point( 50, 120 ))); assert.ok(group.containsPoint(new fabric.Point( 100, 160 ))); assert.ok(!group.containsPoint(new fabric.Point( 0, 0 ))); - group.scale(1); + group.scale(1, 1); group.padding = 30; - group.setCoords(); + group.invalidateCoords(); assert.ok(group.containsPoint(new fabric.Point( 50, 120 ))); - assert.ok(!group.containsPoint(new fabric.Point( 100, 170 ))); + assert.ok(group.containsPoint(new fabric.Point( 100, 170 ))); assert.ok(!group.containsPoint(new fabric.Point( 0, 0 ))); }); @@ -424,20 +424,6 @@ }); }); - QUnit.test('fromObject restores aCoords', function(assert) { - var done = assert.async(); - var group = makeGroupWith2ObjectsWithOpacity(); - - var groupObject = group.toObject(); - groupObject.subTargetCheck = true; - - fabric.Group.fromObject(groupObject).then(function(newGroupFromObject) { - assert.ok(newGroupFromObject._objects[0].aCoords.tl, 'acoords 0 are restored'); - assert.ok(newGroupFromObject._objects[1].aCoords.tl, 'acoords 1 are restored'); - done(); - }); - }); - QUnit.test('fromObject does not delete objects from source', function(assert) { var done = assert.async(); var group = makeGroupWith2ObjectsWithOpacity(); @@ -733,17 +719,6 @@ assert.equal(isTransparent(ctx, 7, 7, 0), true, '7,7 is transparent'); }); - QUnit.test('group add', function(assert) { - var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - group = new fabric.Group([rect1], { layoutManager: new fabric.LayoutManager() }); - - var coords = group.aCoords; - group.add(rect2); - var newCoords = group.aCoords; - assert.notEqual(coords, newCoords, 'object coords have been recalculated - add'); - }); - QUnit.test('group add edge cases', function (assert) { var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false }), rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false }), @@ -773,17 +748,6 @@ assert.deepEqual(group.getObjects(), [rect2, nestedGroup], 'objects should not have changed'); }); - QUnit.test('group remove', function(assert) { - var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - group = new fabric.Group([rect1, rect2], { layoutManager: new fabric.LayoutManager() }); - - var coords = group.aCoords; - group.remove(rect2); - var newCoords = group.aCoords; - assert.notEqual(coords, newCoords, 'object coords have been recalculated - remove'); - }); - QUnit.test('group willDrawShadow', function(assert) { var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), diff --git a/test/unit/object.js b/test/unit/object.js index a6469fcc7ae..169aff2744f 100644 --- a/test/unit/object.js +++ b/test/unit/object.js @@ -294,7 +294,7 @@ assert.ok(typeof cObj.scale === 'function'); assert.equal(cObj.get('scaleX'), 1); assert.equal(cObj.get('scaleY'), 1); - cObj.scale(1.5); + cObj.scale(1.5, 1.5); assert.equal(cObj.get('scaleX'), 1.5); assert.equal(cObj.get('scaleY'), 1.5); }); @@ -396,17 +396,17 @@ }); - QUnit.test('toCanvasElement does not modify oCoords on zoomed canvas', function(assert) { + QUnit.test.skip('toCanvasElement does not modify controlCoords on zoomed canvas', function(assert) { var cObj = new fabric.Rect({ width: 100, height: 100, fill: 'red', strokeWidth: 0 }); canvas.setZoom(2); canvas.add(cObj); - var originaloCoords = cObj.oCoords; - var originalaCoords = cObj.aCoords; + var originaloCoords = cObj.controlCoords; + var originalaCoords = cObj.bboxCoords; cObj.toCanvasElement(); - assert.deepEqual(cObj.oCoords, originaloCoords, 'cObj did not get object coords changed'); - assert.deepEqual(cObj.aCoords, originalaCoords, 'cObj did not get absolute coords changed'); + assert.deepEqual(cObj.controlCoords, originaloCoords, 'cObj did not get object coords changed'); + assert.deepEqual(cObj.bboxCoords, originalaCoords, 'cObj did not get absolute coords changed'); }); diff --git a/test/unit/object_geometry.js b/test/unit/object_geometry.js index 77b860e8cab..ad4fe51368e 100644 --- a/test/unit/object_geometry.js +++ b/test/unit/object_geometry.js @@ -4,7 +4,6 @@ QUnit.test('intersectsWithRectangle without zoom', function(assert) { var cObj = new fabric.Object({ left: 50, top: 50, width: 100, height: 100 }); - cObj.setCoords(); assert.ok(typeof cObj.intersectsWithRect === 'function'); var point1 = new fabric.Point(110, 100), @@ -20,8 +19,6 @@ var cObj = new fabric.Rect({ left: 10, top: 10, width: 20, height: 20 }); canvas.add(cObj); canvas.viewportTransform = [2, 0, 0, 2, 0, 0]; - cObj.setCoords(); - canvas.calcViewportBoundaries(); var point1 = new fabric.Point(5, 5), point2 = new fabric.Point(15, 15), @@ -34,28 +31,23 @@ QUnit.test('intersectsWithObject', function(assert) { var cObj = new fabric.Object({ left: 50, top: 50, width: 100, height: 100 }); - cObj.setCoords(); assert.ok(typeof cObj.intersectsWithObject === 'function', 'has intersectsWithObject method'); var cObj2 = new fabric.Object({ left: -150, top: -150, width: 200, height: 200 }); - cObj2.setCoords(); assert.ok(cObj.intersectsWithObject(cObj2), 'cobj2 does intersect with cobj'); assert.ok(cObj2.intersectsWithObject(cObj), 'cobj2 does intersect with cobj'); var cObj3 = new fabric.Object({ left: 392.5, top: 339.5, width: 13, height: 33 }); - cObj3.setCoords(); assert.ok(!cObj.intersectsWithObject(cObj3), 'cobj3 does not intersect with cobj (external)'); assert.ok(!cObj3.intersectsWithObject(cObj), 'cobj3 does not intersect with cobj (external)'); var cObj4 = new fabric.Object({ left: 0, top: 0, width: 200, height: 200 }); - cObj4.setCoords(); assert.ok(cObj4.intersectsWithObject(cObj), 'overlapping objects are considered intersecting'); assert.ok(cObj.intersectsWithObject(cObj4), 'overlapping objects are considered intersecting'); }); QUnit.test('isContainedWithinRect', function(assert) { var cObj = new fabric.Object({ left: 20, top: 20, width: 10, height: 10 }); - cObj.setCoords(); assert.ok(typeof cObj.isContainedWithinRect === 'function'); // fully contained @@ -70,8 +62,6 @@ var cObj = new fabric.Rect({ left: 20, top: 20, width: 10, height: 10 }); canvas.add(cObj); canvas.viewportTransform = [2, 0, 0, 2, 0, 0]; - cObj.setCoords(); - canvas.calcViewportBoundaries(); assert.ok(typeof cObj.isContainedWithinRect === 'function'); // fully contained @@ -91,8 +81,6 @@ point5 = new fabric.Point(50, 60), point6 = new fabric.Point(70, 80); - object.setCoords(); - // object and area intersects assert.equal(object.intersectsWithRect(point1, point2), true); // area is contained in object (no intersection) @@ -107,14 +95,22 @@ object2 = new fabric.Object({ left: 25, top: 35, width: 20, height: 20, angle: 50, strokeWidth: 0 }), object3 = new fabric.Object({ left: 50, top: 50, width: 20, height: 20, angle: 0, strokeWidth: 0 }); - object.set({ originX: 'center', originY: 'center' }).setCoords(); - object1.set({ originX: 'center', originY: 'center' }).setCoords(); - object2.set({ originX: 'center', originY: 'center' }).setCoords(); - object3.set({ originX: 'center', originY: 'center' }).setCoords(); + object.set({ originX: 'center', originY: 'center' }); + object1.set({ originX: 'center', originY: 'center' }); + object2.set({ originX: 'center', originY: 'center' }); + object3.set({ originX: 'center', originY: 'center' }); - assert.equal(object.intersectsWithObject(object1), true, 'object and object1 intersects'); - assert.equal(object.intersectsWithObject(object2), true, 'object2 is contained in object'); - assert.equal(object.intersectsWithObject(object3), false, 'object3 is outside of object (no intersection)'); + function intersect(abs) { + assert.equal(object.intersectsWithObject(object1, abs), true, 'object and object1 intersects'); + assert.equal(object.intersectsWithObject(object2, abs), true, 'object2 is contained in object'); + assert.equal(object.intersectsWithObject(object3, abs), false, 'object3 is outside of object (no intersection)'); + } + + intersect(); + intersect(true); + const group = new fabric.Group([object1, object2, object3], { subTargetCheck: true }); + intersect(); + intersect(true); }); QUnit.test('isContainedWithinObject', function(assert) { @@ -123,16 +119,11 @@ object2 = new fabric.Object({ left: 20, top: 20, width: 40, height: 40, angle: 0 }), object3 = new fabric.Object({ left: 50, top: 50, width: 40, height: 40, angle: 0 }); - object.setCoords(); - object1.setCoords(); - object2.setCoords(); - object3.setCoords(); - assert.equal(object1.isContainedWithinObject(object), true, 'object1 is fully contained within object'); assert.equal(object2.isContainedWithinObject(object), false, 'object2 intersects object (not fully contained)'); assert.equal(object3.isContainedWithinObject(object), false, 'object3 is outside of object (not fully contained)'); object1.angle = 45; - object1.setCoords(); + object1.invalidateCoords(); assert.equal(object1.isContainedWithinObject(object), false, 'object1 rotated is not contained within object'); var rect1 = new fabric.Rect({ @@ -149,8 +140,6 @@ top: 0, angle: 45, }); - rect1.setCoords(); - rect2.setCoords(); assert.equal(rect1.isContainedWithinObject(rect2), false, 'rect1 rotated is not contained within rect2'); }); @@ -163,7 +152,7 @@ point5 = new fabric.Point(80, 80), point6 = new fabric.Point(90, 90); - object.set({ originX: 'center', originY: 'center' }).setCoords(); + object.set({ originX: 'center', originY: 'center' }) // area is contained in object (no intersection) assert.equal(object.isContainedWithinRect(point1, point2), true); @@ -182,7 +171,7 @@ point5 = new fabric.Point(80, 80), point6 = new fabric.Point(90, 90); - object.set({ originX: 'center', originY: 'center' }).setCoords(); + object.set({ originX: 'center', originY: 'center' }) // area is contained in object (no intersection) assert.equal(object.isContainedWithinRect(point1, point2), true); @@ -200,7 +189,7 @@ point4 = new fabric.Point(15, 40), point5 = new fabric.Point(30, 15); - object.set({ originX: 'center', originY: 'center' }).setCoords(); + object.set({ originX: 'center', originY: 'center' }) // point1 is contained in object assert.equal(object.containsPoint(point1), true); @@ -218,119 +207,79 @@ var cObj = new fabric.Object({ left: 150, top: 150, width: 100, height: 100, strokeWidth: 0,canvas:{}}); assert.ok(typeof cObj.setCoords === 'function'); cObj.setCoords(); - assert.equal(cObj.oCoords.tl.x, 150); - assert.equal(cObj.oCoords.tl.y, 150); - assert.equal(cObj.oCoords.tr.x, 250); - assert.equal(cObj.oCoords.tr.y, 150); - assert.equal(cObj.oCoords.bl.x, 150); - assert.equal(cObj.oCoords.bl.y, 250); - assert.equal(cObj.oCoords.br.x, 250); - assert.equal(cObj.oCoords.br.y, 250); - assert.equal(cObj.oCoords.mtr.x, 200); - assert.equal(cObj.oCoords.mtr.y, 110); + assert.equal(cObj.controlCoords.tl.position.x, 150); + assert.equal(cObj.controlCoords.tl.position.y, 150); + assert.equal(cObj.controlCoords.tr.position.x, 250); + assert.equal(cObj.controlCoords.tr.position.y, 150); + assert.equal(cObj.controlCoords.bl.position.x, 150); + assert.equal(cObj.controlCoords.bl.position.y, 250); + assert.equal(cObj.controlCoords.br.position.x, 250); + assert.equal(cObj.controlCoords.br.position.y, 250); + assert.equal(cObj.controlCoords.mtr.position.x, 200); + assert.equal(cObj.controlCoords.mtr.position.y, 110); cObj.set('left', 250).set('top', 250); - // coords should still correspond to initial one, even after invoking `set` - assert.equal(cObj.oCoords.tl.x, 150); - assert.equal(cObj.oCoords.tl.y, 150); - assert.equal(cObj.oCoords.tr.x, 250); - assert.equal(cObj.oCoords.tr.y, 150); - assert.equal(cObj.oCoords.bl.x, 150); - assert.equal(cObj.oCoords.bl.y, 250); - assert.equal(cObj.oCoords.br.x, 250); - assert.equal(cObj.oCoords.br.y, 250); - assert.equal(cObj.oCoords.mtr.x, 200); - assert.equal(cObj.oCoords.mtr.y, 110); + assert.equal(cObj.bboxCoords, undefined); + assert.equal(cObj.controlCoords, undefined); // recalculate coords cObj.setCoords(); // check that coords are now updated - assert.equal(cObj.oCoords.tl.x, 250); - assert.equal(cObj.oCoords.tl.y, 250); - assert.equal(cObj.oCoords.tr.x, 350); - assert.equal(cObj.oCoords.tr.y, 250); - assert.equal(cObj.oCoords.bl.x, 250); - assert.equal(cObj.oCoords.bl.y, 350); - assert.equal(cObj.oCoords.br.x, 350); - assert.equal(cObj.oCoords.br.y, 350); - assert.equal(cObj.oCoords.mtr.x, 300); - assert.equal(cObj.oCoords.mtr.y, 210); + assert.equal(cObj.controlCoords.tl.position.x, 250); + assert.equal(cObj.controlCoords.tl.position.y, 250); + assert.equal(cObj.controlCoords.tr.position.x, 350); + assert.equal(cObj.controlCoords.tr.position.y, 250); + assert.equal(cObj.controlCoords.bl.position.x, 250); + assert.equal(cObj.controlCoords.bl.position.y, 350); + assert.equal(cObj.controlCoords.br.position.x, 350); + assert.equal(cObj.controlCoords.br.position.y, 350); + assert.equal(cObj.controlCoords.mtr.position.x, 300); + assert.equal(cObj.controlCoords.mtr.position.y, 210); cObj.set('padding', 25); + assert.equal(cObj.controlCoords, undefined); cObj.setCoords(); // coords should still correspond to initial one, even after invoking `set` - assert.equal(cObj.oCoords.tl.x, 225, 'setCoords tl.x padding'); - assert.equal(cObj.oCoords.tl.y, 225, 'setCoords tl.y padding'); - assert.equal(cObj.oCoords.tr.x, 375, 'setCoords tr.x padding'); - assert.equal(cObj.oCoords.tr.y, 225, 'setCoords tr.y padding'); - assert.equal(cObj.oCoords.bl.x, 225, 'setCoords bl.x padding'); - assert.equal(cObj.oCoords.bl.y, 375, 'setCoords bl.y padding'); - assert.equal(cObj.oCoords.br.x, 375, 'setCoords br.x padding'); - assert.equal(cObj.oCoords.br.y, 375, 'setCoords br.y padding'); - assert.equal(cObj.oCoords.mtr.x, 300, 'setCoords mtr.x padding'); - assert.equal(cObj.oCoords.mtr.y, 185, 'setCoords mtr.y padding'); - }); - - QUnit.test('setCoords and aCoords', function(assert) { + assert.equal(cObj.controlCoords.tl.position.x, 225, 'setCoords tl.position.x padding'); + assert.equal(cObj.controlCoords.tl.position.y, 225, 'setCoords tl.position.y padding'); + assert.equal(cObj.controlCoords.tr.position.x, 375, 'setCoords tr.position.x padding'); + assert.equal(cObj.controlCoords.tr.position.y, 225, 'setCoords tr.position.y padding'); + assert.equal(cObj.controlCoords.bl.position.x, 225, 'setCoords bl.position.x padding'); + assert.equal(cObj.controlCoords.bl.position.y, 375, 'setCoords bl.position.y padding'); + assert.equal(cObj.controlCoords.br.position.x, 375, 'setCoords br.position.x padding'); + assert.equal(cObj.controlCoords.br.position.y, 375, 'setCoords br.position.y padding'); + assert.equal(cObj.controlCoords.mtr.position.x, 300, 'setCoords mtr.position.x padding'); + assert.equal(cObj.controlCoords.mtr.position.y, 185, 'setCoords mtr.position.y padding'); + }); + + QUnit.test.skip('setCoords and aCoords', function(assert) { var cObj = new fabric.Object({ left: 150, top: 150, width: 100, height: 100, strokeWidth: 0}); cObj.canvas = { viewportTransform: [2, 0, 0, 2, 0, 0] }; cObj.setCoords(); - assert.equal(cObj.oCoords.tl.x, 300, 'oCoords are modified by viewportTransform tl.x'); - assert.equal(cObj.oCoords.tl.y, 300, 'oCoords are modified by viewportTransform tl.y'); - assert.equal(cObj.oCoords.tr.x, 500, 'oCoords are modified by viewportTransform tr.x'); - assert.equal(cObj.oCoords.tr.y, 300, 'oCoords are modified by viewportTransform tr.y'); - assert.equal(cObj.oCoords.bl.x, 300, 'oCoords are modified by viewportTransform bl.x'); - assert.equal(cObj.oCoords.bl.y, 500, 'oCoords are modified by viewportTransform bl.y'); - assert.equal(cObj.oCoords.br.x, 500, 'oCoords are modified by viewportTransform br.x'); - assert.equal(cObj.oCoords.br.y, 500, 'oCoords are modified by viewportTransform br.y'); - assert.equal(cObj.oCoords.mtr.x, 400, 'oCoords are modified by viewportTransform mtr.x'); - assert.equal(cObj.oCoords.mtr.y, 260, 'oCoords are modified by viewportTransform mtr.y'); - - assert.equal(cObj.aCoords.tl.x, 150, 'aCoords do not interfere with viewportTransform'); - assert.equal(cObj.aCoords.tl.y, 150, 'aCoords do not interfere with viewportTransform'); - assert.equal(cObj.aCoords.tr.x, 250, 'aCoords do not interfere with viewportTransform'); - assert.equal(cObj.aCoords.tr.y, 150, 'aCoords do not interfere with viewportTransform'); - assert.equal(cObj.aCoords.bl.x, 150, 'aCoords do not interfere with viewportTransform'); - assert.equal(cObj.aCoords.bl.y, 250, 'aCoords do not interfere with viewportTransform'); - assert.equal(cObj.aCoords.br.x, 250, 'aCoords do not interfere with viewportTransform'); - assert.equal(cObj.aCoords.br.y, 250, 'aCoords do not interfere with viewportTransform'); - }); + assert.equal(cObj.controlCoords.tl.position.x, 300, 'controlCoords are modified by viewportTransform tl.position.x'); + assert.equal(cObj.controlCoords.tl.position.y, 300, 'controlCoords are modified by viewportTransform tl.position.y'); + assert.equal(cObj.controlCoords.tr.position.x, 500, 'controlCoords are modified by viewportTransform tr.position.x'); + assert.equal(cObj.controlCoords.tr.position.y, 300, 'controlCoords are modified by viewportTransform tr.position.y'); + assert.equal(cObj.controlCoords.bl.position.x, 300, 'controlCoords are modified by viewportTransform bl.position.x'); + assert.equal(cObj.controlCoords.bl.position.y, 500, 'controlCoords are modified by viewportTransform bl.position.y'); + assert.equal(cObj.controlCoords.br.position.x, 500, 'controlCoords are modified by viewportTransform br.position.x'); + assert.equal(cObj.controlCoords.br.position.y, 500, 'controlCoords are modified by viewportTransform br.position.y'); + assert.equal(cObj.controlCoords.mtr.position.x, 400, 'controlCoords are modified by viewportTransform mtr.position.x'); + assert.equal(cObj.controlCoords.mtr.position.y, 260, 'controlCoords are modified by viewportTransform mtr.position.y'); - QUnit.test('isOnScreen', function(assert) { - var cObj = new fabric.Object({ left: 50, top: 50, width: 100, height: 100, strokeWidth: 0}); - canvas.viewportTransform = [1, 0, 0, 1, 0, 0]; - canvas.calcViewportBoundaries(); - cObj.canvas = canvas; - cObj.setCoords(); - assert.ok(cObj.isOnScreen(), 'object is onScreen'); - cObj.top = 1000; - assert.ok(cObj.isOnScreen(), 'object is still wrongly on screen since setCoords is not called and calculate is not set, even when top is already at 1000'); - cObj.setCoords(); - assert.ok(!cObj.isOnScreen(), 'object is not onScreen with top 1000'); - canvas.setZoom(0.1); - cObj.setCoords(); - assert.ok(cObj.isOnScreen(), 'zooming out the object is again on screen'); - }); - - QUnit.test('isOnScreen flipped vpt', function (assert) { - var cObj = new fabric.Object({ left: -50, top: -50, width: 100, height: 100, strokeWidth: 0 }); - canvas.viewportTransform = [-1, 0, 0, -1, 0, 0]; - canvas.calcViewportBoundaries(); - cObj.canvas = canvas; - cObj.setCoords(); - assert.ok(cObj.isOnScreen(), 'object is onScreen'); - cObj.top = 1000; - assert.ok(cObj.isOnScreen(), 'object is still wrongly on screen since setCoords is not called and calculate is not set, even when top is already at 1000'); - cObj.setCoords(); - assert.ok(!cObj.isOnScreen(), 'object is not onScreen with top 1000'); - canvas.setZoom(0.1); - cObj.setCoords(); - assert.ok(cObj.isOnScreen(), 'zooming out the object is again on screen'); + assert.equal(cObj.bboxCoords.tl.x, 150, 'bboxCoords do not interfere with viewportTransform'); + assert.equal(cObj.bboxCoords.tl.y, 150, 'bboxCoords do not interfere with viewportTransform'); + assert.equal(cObj.bboxCoords.tr.x, 250, 'bboxCoords do not interfere with viewportTransform'); + assert.equal(cObj.bboxCoords.tr.y, 150, 'bboxCoords do not interfere with viewportTransform'); + assert.equal(cObj.bboxCoords.bl.x, 150, 'bboxCoords do not interfere with viewportTransform'); + assert.equal(cObj.bboxCoords.bl.y, 250, 'bboxCoords do not interfere with viewportTransform'); + assert.equal(cObj.bboxCoords.br.x, 250, 'bboxCoords do not interfere with viewportTransform'); + assert.equal(cObj.bboxCoords.br.y, 250, 'bboxCoords do not interfere with viewportTransform'); }); QUnit.test('transformMatrixKey depends from properties', function(assert) { @@ -363,33 +312,6 @@ assert.notEqual(key2, key3, 'keys are different origins 3'); }); - QUnit.test('isOnScreen with object that include canvas', function(assert) { - var cObj = new fabric.Object( - { left: -10, top: -10, width: canvas.getWidth() + 100, height: canvas.getHeight(), strokeWidth: 0}); - canvas.viewportTransform = [1, 0, 0, 1, 0, 0]; - canvas.calcViewportBoundaries(); - cObj.canvas = canvas; - cObj.setCoords(); - assert.equal(cObj.isOnScreen(), true, 'object is onScreen because it include the canvas'); - cObj.top = -1000; - cObj.left = -1000; - cObj.setCoords(); - assert.equal(cObj.isOnScreen(), false, 'object is completely out of viewport'); - }); - - QUnit.test('isOnScreen with object that is in top left corner of canvas', function(assert) { - var cObj = new fabric.Rect({left: -46.56, top: -9.23, width: 50,height: 50, angle: 314.57}); - canvas.viewportTransform = [1, 0, 0, 1, 0, 0]; - canvas.calcViewportBoundaries(); - cObj.canvas = canvas; - cObj.setCoords(); - assert.ok(cObj.isOnScreen(), 'object is onScreen because it intersect a canvas line'); - cObj.top -= 20; - cObj.left -= 20; - cObj.setCoords(); - assert.ok(!cObj.isOnScreen(), 'object is completely out of viewport'); - }); - QUnit.test('calcTransformMatrix with no group', function(assert) { var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 0 }); assert.ok(typeof cObj.calcTransformMatrix === 'function', 'calcTransformMatrix should exist'); @@ -436,65 +358,10 @@ 43.50067533516962], 'translate matrix scale skewX skewY angle flipX flipY'); }); - QUnit.test('scaleToWidth', function(assert) { - var cObj = new fabric.Object({ width: 560, strokeWidth: 0 }); - assert.ok(typeof cObj.scaleToWidth === 'function', 'scaleToWidth should exist'); - cObj.scaleToWidth(100); - assert.equal(cObj.getScaledWidth(), 100); - assert.equal(cObj.get('scaleX'), 100 / 560); - }); - - QUnit.test('scaleToWidth with zoom', function(assert) { - var cObj = new fabric.Object({ width: 560, strokeWidth: 0 }); - cObj.canvas = { - viewportTransform: [2, 0, 0, 2, 0, 0] - }; - cObj.scaleToWidth(100); - assert.equal(cObj.getScaledWidth(), 100, 'is not influenced by zoom - width'); - assert.equal(cObj.get('scaleX'), 100 / 560); - }); - - - QUnit.test('scaleToHeight', function(assert) { - var cObj = new fabric.Object({ height: 560, strokeWidth: 0 }); - assert.ok(typeof cObj.scaleToHeight === 'function', 'scaleToHeight should exist'); - cObj.scaleToHeight(100); - assert.equal(cObj.getScaledHeight(), 100); - assert.equal(cObj.get('scaleY'), 100 / 560); - }); - - QUnit.test('scaleToHeight with zoom', function(assert) { - var cObj = new fabric.Object({ height: 560, strokeWidth: 0 }); - cObj.canvas = { - viewportTransform: [2, 0, 0, 2, 0, 0] - }; - cObj.scaleToHeight(100); - assert.equal(cObj.getScaledHeight(), 100, 'is not influenced by zoom - height'); - assert.equal(cObj.get('scaleY'), 100 / 560); - // cObj.scaleToHeight(100); - // assert.equal(cObj.getScaledHeight(), 50, 'is influenced by zoom - height'); - // assert.equal(cObj.get('scaleY'), 100 / 560 / 2); - }); - - QUnit.test('scaleToWidth on rotated object', function(assert) { - var obj = new fabric.Object({ height: 100, width: 100, strokeWidth: 0 }); - obj.rotate(45); - obj.scaleToWidth(200); - assert.equal(Math.round(obj.getBoundingRect().width), 200); - }); - - QUnit.test('scaleToHeight on rotated object', function(assert) { - var obj = new fabric.Object({ height: 100, width: 100, strokeWidth: 0 }); - obj.rotate(45); - obj.scaleToHeight(300); - assert.equal(Math.round(obj.getBoundingRect().height), 300); - }); - QUnit.test('getBoundingRect with absolute coords', function(assert) { var cObj = new fabric.Object({ strokeWidth: 0, width: 10, height: 10, top: 6, left: 5 }), boundingRect; - cObj.setCoords(); boundingRect = cObj.getBoundingRect(); assert.equal(boundingRect.left, 5, 'gives the bounding rect left with absolute coords'); assert.equal(boundingRect.width, 10, 'gives the bounding rect width with absolute coords'); @@ -502,7 +369,7 @@ cObj.canvas = { viewportTransform: [2, 0, 0, 2, 0, 0] }; - cObj.setCoords(); + cObj.invalidateCoords(); boundingRect = cObj.getBoundingRect(); assert.equal(boundingRect.left, 5, 'gives the bounding rect left with absolute coords, regardless of vpt'); assert.equal(boundingRect.width, 10, 'gives the bounding rect width with absolute coords, regardless of vpt'); @@ -513,14 +380,12 @@ var cObj = new fabric.Object({ strokeWidth: 0 }), boundingRect; assert.ok(typeof cObj.getBoundingRect === 'function'); - - cObj.setCoords(); boundingRect = cObj.getBoundingRect(); assert.equal(boundingRect.left, 0); assert.equal(boundingRect.top, 0); assert.equal(boundingRect.width, 0); assert.equal(boundingRect.height, 0); - cObj.set('width', 123).setCoords(); + cObj.set('width', 123); boundingRect = cObj.getBoundingRect(); assert.equal(boundingRect.left, 0); assert.equal(boundingRect.top, 0); @@ -528,14 +393,13 @@ assert.equal(boundingRect.height, 0); cObj.set('height', 167); - cObj.setCoords(); boundingRect = cObj.getBoundingRect(); assert.equal(boundingRect.left, 0); assert.equal(Math.abs(boundingRect.top).toFixed(13), 0); assert.equal(boundingRect.width, 123); assert.equal(boundingRect.height, 167); - cObj.scale(2) + cObj.scale(2, 2) cObj.setCoords(); boundingRect = cObj.getBoundingRect(); assert.equal(boundingRect.left, 0); @@ -548,31 +412,27 @@ var cObj = new fabric.Object(), boundingRect; assert.ok(typeof cObj.getBoundingRect === 'function'); - - cObj.setCoords(); boundingRect = cObj.getBoundingRect(); assert.equal(boundingRect.left.toFixed(2), 0); assert.equal(boundingRect.top.toFixed(2), 0); assert.equal(boundingRect.width.toFixed(2), 1); assert.equal(boundingRect.height.toFixed(2), 1); - cObj.set('width', 123) - cObj.setCoords(); + cObj.set('width', 123); boundingRect = cObj.getBoundingRect(); assert.equal(boundingRect.left.toFixed(2), 0); assert.equal(boundingRect.top.toFixed(2), 0); assert.equal(boundingRect.width.toFixed(2), 124); assert.equal(boundingRect.height.toFixed(2), 1); - cObj.set('height', 167) - cObj.setCoords(); + cObj.set('height', 167); boundingRect = cObj.getBoundingRect(); assert.equal(boundingRect.left.toFixed(2), 0); assert.equal(boundingRect.top.toFixed(2), 0); assert.equal(boundingRect.width.toFixed(2), 124); assert.equal(boundingRect.height.toFixed(2), 168); - cObj.scale(2) + cObj.scale(2, 2) cObj.setCoords(); boundingRect = cObj.getBoundingRect(); assert.equal(boundingRect.left.toFixed(2), 0); @@ -581,26 +441,6 @@ assert.equal(boundingRect.height.toFixed(2), 336); }); - QUnit.test('getScaledWidth', function(assert) { - var cObj = new fabric.Object(); - assert.ok(typeof cObj.getScaledWidth === 'function'); - assert.equal(cObj.getScaledWidth(), 0 + cObj.strokeWidth); - cObj.set('width', 123); - assert.equal(cObj.getScaledWidth(), 123 + cObj.strokeWidth); - cObj.set('scaleX', 2); - assert.equal(cObj.getScaledWidth(), 246 + cObj.strokeWidth * 2); - }); - - QUnit.test('getScaledHeight', function(assert) { - var cObj = new fabric.Object({strokeWidth: 0}); - // assert.ok(typeof cObj.getHeight === 'function'); - assert.equal(cObj.getScaledHeight(), 0); - cObj.set('height', 123); - assert.equal(cObj.getScaledHeight(), 123); - cObj.set('scaleY', 2); - assert.equal(cObj.getScaledHeight(), 246); - }); - QUnit.test('scale', function(assert) { var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 0 }); assert.ok(typeof cObj.scale === 'function', 'scale should exist'); @@ -620,6 +460,7 @@ QUnit.test('getCoords return coordinate of object in canvas coordinate.', function(assert) { var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 2, top: 30, left: 40 }); + canvas.add(cObj); var coords = cObj.getCoords(); assert.deepEqual(coords[0], new fabric.Point(40, 30), 'return top left corner'); assert.deepEqual(coords[1], new fabric.Point(52, 30), 'return top right corner'); @@ -628,12 +469,12 @@ cObj.left += 5; coords = cObj.getCoords(); - assert.deepEqual(coords[0], new fabric.Point(40, 30), 'return top left corner cached aCoords'); - assert.deepEqual(coords[1], new fabric.Point(52, 30), 'return top right corner cached aCoords'); - assert.deepEqual(coords[2], new fabric.Point(52, 47), 'return bottom right corner cached aCoords'); - assert.deepEqual(coords[3], new fabric.Point(40, 47), 'return bottom left corner cached aCoords'); + assert.deepEqual(coords[0], new fabric.Point(40, 30), 'return top left corner cached bboxCoords'); + assert.deepEqual(coords[1], new fabric.Point(52, 30), 'return top right corner cached bboxCoords'); + assert.deepEqual(coords[2], new fabric.Point(52, 47), 'return bottom right corner cached bboxCoords'); + assert.deepEqual(coords[3], new fabric.Point(40, 47), 'return bottom left corner cached bboxCoords'); - cObj.setCoords(); + cObj.invalidateCoords(); coords = cObj.getCoords(); assert.deepEqual(coords[0], new fabric.Point(45, 30), 'return top left corner recalculated'); assert.deepEqual(coords[1], new fabric.Point(57, 30), 'return top right corner recalculated'); @@ -643,14 +484,13 @@ QUnit.test('getCoords return coordinate of object in absolute coordinates and ignore canvas zoom', function(assert) { var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 2, top: 30, left: 40 }); - cObj.canvas = { - viewportTransform: [2, 0, 0, 2, 35, 35] - }; + canvas.add(cObj); + canvas.setViewportTransform([2, 0, 0, 2, 35, 25]); var coords = cObj.getCoords(true); - assert.deepEqual(coords[0], new fabric.Point(40, 30), 'return top left corner cached oCoords'); - assert.deepEqual(coords[1], new fabric.Point(52, 30), 'return top right corner cached oCoords'); - assert.deepEqual(coords[2], new fabric.Point(52, 47), 'return bottom right corner cached oCoords'); - assert.deepEqual(coords[3], new fabric.Point(40, 47), 'return bottom left corner cached oCoords'); + assert.deepEqual(coords[0], new fabric.Point(40, 30), 'return top left corner cached controlCoords'); + assert.deepEqual(coords[1], new fabric.Point(52, 30), 'return top right corner cached controlCoords'); + assert.deepEqual(coords[2], new fabric.Point(52, 47), 'return bottom right corner cached controlCoords'); + assert.deepEqual(coords[3], new fabric.Point(40, 47), 'return bottom left corner cached controlCoords'); }); QUnit.test('getCoords with angle', function(assert) { @@ -724,19 +564,18 @@ QUnit.test('isPartiallyOnScreen', function(assert) { var cObj = new fabric.Object({ left: 50, top: 50, width: 100, height: 100, strokeWidth: 0}); canvas.viewportTransform = [1, 0, 0, 1, 0, 0]; - canvas.calcViewportBoundaries(); cObj.canvas = canvas; cObj.left = -60; cObj.top = -60; - cObj.setCoords(); + cObj.invalidateCoords(); assert.equal(cObj.isPartiallyOnScreen(true), true,'object is partially onScreen'); cObj.left = -110; cObj.top = -110; - cObj.setCoords(); + cObj.invalidateCoords(); assert.equal(cObj.isPartiallyOnScreen(true), false,'object is completely offScreen and not partial'); cObj.left = 45; cObj.top = 45; - cObj.setCoords(); + cObj.invalidateCoords(); assert.equal(cObj.isPartiallyOnScreen(true), false, 'object is completely on screen and not partial'); canvas.setZoom(2); assert.equal(cObj.isPartiallyOnScreen(true), true, 'after zooming object is partially onScreen and offScreen'); @@ -745,13 +584,12 @@ QUnit.test('isPartiallyOnScreen with object inside and outside of canvas', function(assert) { var cObj = new fabric.Object({ left: 5, top: 5, width: 100, height: 100, strokeWidth: 0}); cObj.canvas = new fabric.StaticCanvas(null, { width: 120, height: 120, enableRetinaScaling: false}); - cObj.canvas.calcViewportBoundaries(); assert.equal(cObj.isPartiallyOnScreen(true), false,'object is completely onScreen'); cObj.left = -20; cObj.top = -20; cObj.scaleX = 2; cObj.scaleY = 2; - cObj.setCoords(); + cObj.invalidateCoords(); assert.equal(cObj.isPartiallyOnScreen(true), true, 'object has all corners outside screen but contains canvas'); }); })(); diff --git a/test/unit/object_interactivity.js b/test/unit/object_interactivity.js index a8ccb9634de..39e087f9c1c 100644 --- a/test/unit/object_interactivity.js +++ b/test/unit/object_interactivity.js @@ -101,46 +101,46 @@ var cObj = new fabric.Object({ top: 10, left: 10, width: 10, height: 10, strokeWidth: 0, canvas: {} }); cObj.setCoords(); - assert.equal(cObj.oCoords.tl.corner.tl.x.toFixed(2), 3.5); - assert.equal(cObj.oCoords.tl.corner.tl.y.toFixed(2), 3.5); - assert.equal(cObj.oCoords.tl.corner.tr.x.toFixed(2), 16.5); - assert.equal(cObj.oCoords.tl.corner.tr.y.toFixed(2), 3.5); - assert.equal(cObj.oCoords.tl.corner.bl.x.toFixed(2), 3.5); - assert.equal(cObj.oCoords.tl.corner.bl.y.toFixed(2), 16.5); - assert.equal(cObj.oCoords.tl.corner.br.x.toFixed(2), 16.5); - assert.equal(cObj.oCoords.tl.corner.br.y.toFixed(2), 16.5); - assert.equal(cObj.oCoords.bl.corner.tl.x.toFixed(2), 3.5); - assert.equal(cObj.oCoords.bl.corner.tl.y.toFixed(2), 13.5); - assert.equal(cObj.oCoords.bl.corner.tr.x.toFixed(2), 16.5); - assert.equal(cObj.oCoords.bl.corner.tr.y.toFixed(2), 13.5); - assert.equal(cObj.oCoords.bl.corner.bl.x.toFixed(2), 3.5); - assert.equal(cObj.oCoords.bl.corner.bl.y.toFixed(2), 26.5); - assert.equal(cObj.oCoords.bl.corner.br.x.toFixed(2), 16.5); - assert.equal(cObj.oCoords.bl.corner.br.y.toFixed(2), 26.5); - assert.equal(cObj.oCoords.tr.corner.tl.x.toFixed(2), 13.5); - assert.equal(cObj.oCoords.tr.corner.tl.y.toFixed(2), 3.5); - assert.equal(cObj.oCoords.tr.corner.tr.x.toFixed(2), 26.5); - assert.equal(cObj.oCoords.tr.corner.tr.y.toFixed(2), 3.5); - assert.equal(cObj.oCoords.tr.corner.bl.x.toFixed(2), 13.5); - assert.equal(cObj.oCoords.tr.corner.bl.y.toFixed(2), 16.5); - assert.equal(cObj.oCoords.tr.corner.br.x.toFixed(2), 26.5); - assert.equal(cObj.oCoords.tr.corner.br.y.toFixed(2), 16.5); - assert.equal(cObj.oCoords.br.corner.tl.x.toFixed(2), 13.5); - assert.equal(cObj.oCoords.br.corner.tl.y.toFixed(2), 13.5); - assert.equal(cObj.oCoords.br.corner.tr.x.toFixed(2), 26.5); - assert.equal(cObj.oCoords.br.corner.tr.y.toFixed(2), 13.5); - assert.equal(cObj.oCoords.br.corner.bl.x.toFixed(2), 13.5); - assert.equal(cObj.oCoords.br.corner.bl.y.toFixed(2), 26.5); - assert.equal(cObj.oCoords.br.corner.br.x.toFixed(2), 26.5); - assert.equal(cObj.oCoords.br.corner.br.y.toFixed(2), 26.5); - assert.equal(cObj.oCoords.mtr.corner.tl.x.toFixed(2), 8.5); - assert.equal(cObj.oCoords.mtr.corner.tl.y.toFixed(2), -36.5); - assert.equal(cObj.oCoords.mtr.corner.tr.x.toFixed(2), 21.5); - assert.equal(cObj.oCoords.mtr.corner.tr.y.toFixed(2), -36.5); - assert.equal(cObj.oCoords.mtr.corner.bl.x.toFixed(2), 8.5); - assert.equal(cObj.oCoords.mtr.corner.bl.y.toFixed(2), -23.5); - assert.equal(cObj.oCoords.mtr.corner.br.x.toFixed(2), 21.5); - assert.equal(cObj.oCoords.mtr.corner.br.y.toFixed(2), -23.5); + assert.equal(cObj.controlCoords.tl.corner.tl.x.toFixed(2), 3.5); + assert.equal(cObj.controlCoords.tl.corner.tl.y.toFixed(2), 3.5); + assert.equal(cObj.controlCoords.tl.corner.tr.x.toFixed(2), 16.5); + assert.equal(cObj.controlCoords.tl.corner.tr.y.toFixed(2), 3.5); + assert.equal(cObj.controlCoords.tl.corner.bl.x.toFixed(2), 3.5); + assert.equal(cObj.controlCoords.tl.corner.bl.y.toFixed(2), 16.5); + assert.equal(cObj.controlCoords.tl.corner.br.x.toFixed(2), 16.5); + assert.equal(cObj.controlCoords.tl.corner.br.y.toFixed(2), 16.5); + assert.equal(cObj.controlCoords.bl.corner.tl.x.toFixed(2), 3.5); + assert.equal(cObj.controlCoords.bl.corner.tl.y.toFixed(2), 13.5); + assert.equal(cObj.controlCoords.bl.corner.tr.x.toFixed(2), 16.5); + assert.equal(cObj.controlCoords.bl.corner.tr.y.toFixed(2), 13.5); + assert.equal(cObj.controlCoords.bl.corner.bl.x.toFixed(2), 3.5); + assert.equal(cObj.controlCoords.bl.corner.bl.y.toFixed(2), 26.5); + assert.equal(cObj.controlCoords.bl.corner.br.x.toFixed(2), 16.5); + assert.equal(cObj.controlCoords.bl.corner.br.y.toFixed(2), 26.5); + assert.equal(cObj.controlCoords.tr.corner.tl.x.toFixed(2), 13.5); + assert.equal(cObj.controlCoords.tr.corner.tl.y.toFixed(2), 3.5); + assert.equal(cObj.controlCoords.tr.corner.tr.x.toFixed(2), 26.5); + assert.equal(cObj.controlCoords.tr.corner.tr.y.toFixed(2), 3.5); + assert.equal(cObj.controlCoords.tr.corner.bl.x.toFixed(2), 13.5); + assert.equal(cObj.controlCoords.tr.corner.bl.y.toFixed(2), 16.5); + assert.equal(cObj.controlCoords.tr.corner.br.x.toFixed(2), 26.5); + assert.equal(cObj.controlCoords.tr.corner.br.y.toFixed(2), 16.5); + assert.equal(cObj.controlCoords.br.corner.tl.x.toFixed(2), 13.5); + assert.equal(cObj.controlCoords.br.corner.tl.y.toFixed(2), 13.5); + assert.equal(cObj.controlCoords.br.corner.tr.x.toFixed(2), 26.5); + assert.equal(cObj.controlCoords.br.corner.tr.y.toFixed(2), 13.5); + assert.equal(cObj.controlCoords.br.corner.bl.x.toFixed(2), 13.5); + assert.equal(cObj.controlCoords.br.corner.bl.y.toFixed(2), 26.5); + assert.equal(cObj.controlCoords.br.corner.br.x.toFixed(2), 26.5); + assert.equal(cObj.controlCoords.br.corner.br.y.toFixed(2), 26.5); + assert.equal(cObj.controlCoords.mtr.corner.tl.x.toFixed(2), 8.5); + assert.equal(cObj.controlCoords.mtr.corner.tl.y.toFixed(2), -36.5); + assert.equal(cObj.controlCoords.mtr.corner.tr.x.toFixed(2), 21.5); + assert.equal(cObj.controlCoords.mtr.corner.tr.y.toFixed(2), -36.5); + assert.equal(cObj.controlCoords.mtr.corner.bl.x.toFixed(2), 8.5); + assert.equal(cObj.controlCoords.mtr.corner.bl.y.toFixed(2), -23.5); + assert.equal(cObj.controlCoords.mtr.corner.br.x.toFixed(2), 21.5); + assert.equal(cObj.controlCoords.mtr.corner.br.y.toFixed(2), -23.5); }); @@ -154,46 +154,46 @@ var cObj = new fabric.Object({ top: 10, left: 10, width: 10, height: 10, strokeWidth: 0, controls: sharedControls, canvas: {} }); cObj.setCoords(); - assert.equal(cObj.oCoords.tl.corner.tl.x.toFixed(2), 3.5); - assert.equal(cObj.oCoords.tl.corner.tl.y.toFixed(2), 3.5); - assert.equal(cObj.oCoords.tl.corner.tr.x.toFixed(2), 16.5); - assert.equal(cObj.oCoords.tl.corner.tr.y.toFixed(2), 3.5); - assert.equal(cObj.oCoords.tl.corner.bl.x.toFixed(2), 3.5); - assert.equal(cObj.oCoords.tl.corner.bl.y.toFixed(2), 16.5); - assert.equal(cObj.oCoords.tl.corner.br.x.toFixed(2), 16.5); - assert.equal(cObj.oCoords.tl.corner.br.y.toFixed(2), 16.5); - assert.equal(cObj.oCoords.bl.corner.tl.x.toFixed(2), -5.0); - assert.equal(cObj.oCoords.bl.corner.tl.y.toFixed(2), 15.0); - assert.equal(cObj.oCoords.bl.corner.tr.x.toFixed(2), 25.0); - assert.equal(cObj.oCoords.bl.corner.tr.y.toFixed(2), 15.0); - assert.equal(cObj.oCoords.bl.corner.bl.x.toFixed(2), -5.0); - assert.equal(cObj.oCoords.bl.corner.bl.y.toFixed(2), 25.0); - assert.equal(cObj.oCoords.bl.corner.br.x.toFixed(2), 25.0); - assert.equal(cObj.oCoords.bl.corner.br.y.toFixed(2), 25.0); - assert.equal(cObj.oCoords.tr.corner.tl.x.toFixed(2), 13.5); - assert.equal(cObj.oCoords.tr.corner.tl.y.toFixed(2), 3.5); - assert.equal(cObj.oCoords.tr.corner.tr.x.toFixed(2), 26.5); - assert.equal(cObj.oCoords.tr.corner.tr.y.toFixed(2), 3.5); - assert.equal(cObj.oCoords.tr.corner.bl.x.toFixed(2), 13.5); - assert.equal(cObj.oCoords.tr.corner.bl.y.toFixed(2), 16.5); - assert.equal(cObj.oCoords.tr.corner.br.x.toFixed(2), 26.5); - assert.equal(cObj.oCoords.tr.corner.br.y.toFixed(2), 16.5); - assert.equal(cObj.oCoords.br.corner.tl.x.toFixed(2), 13.5); - assert.equal(cObj.oCoords.br.corner.tl.y.toFixed(2), 13.5); - assert.equal(cObj.oCoords.br.corner.tr.x.toFixed(2), 26.5); - assert.equal(cObj.oCoords.br.corner.tr.y.toFixed(2), 13.5); - assert.equal(cObj.oCoords.br.corner.bl.x.toFixed(2), 13.5); - assert.equal(cObj.oCoords.br.corner.bl.y.toFixed(2), 26.5); - assert.equal(cObj.oCoords.br.corner.br.x.toFixed(2), 26.5); - assert.equal(cObj.oCoords.br.corner.br.y.toFixed(2), 26.5); - assert.equal(cObj.oCoords.mtr.corner.tl.x.toFixed(2), 8.5); - assert.equal(cObj.oCoords.mtr.corner.tl.y.toFixed(2), -36.5); - assert.equal(cObj.oCoords.mtr.corner.tr.x.toFixed(2), 21.5); - assert.equal(cObj.oCoords.mtr.corner.tr.y.toFixed(2), -36.5); - assert.equal(cObj.oCoords.mtr.corner.bl.x.toFixed(2), 8.5); - assert.equal(cObj.oCoords.mtr.corner.bl.y.toFixed(2), -23.5); - assert.equal(cObj.oCoords.mtr.corner.br.x.toFixed(2), 21.5); - assert.equal(cObj.oCoords.mtr.corner.br.y.toFixed(2), -23.5); + assert.equal(cObj.controlCoords.tl.corner.tl.x.toFixed(2), 3.5); + assert.equal(cObj.controlCoords.tl.corner.tl.y.toFixed(2), 3.5); + assert.equal(cObj.controlCoords.tl.corner.tr.x.toFixed(2), 16.5); + assert.equal(cObj.controlCoords.tl.corner.tr.y.toFixed(2), 3.5); + assert.equal(cObj.controlCoords.tl.corner.bl.x.toFixed(2), 3.5); + assert.equal(cObj.controlCoords.tl.corner.bl.y.toFixed(2), 16.5); + assert.equal(cObj.controlCoords.tl.corner.br.x.toFixed(2), 16.5); + assert.equal(cObj.controlCoords.tl.corner.br.y.toFixed(2), 16.5); + assert.equal(cObj.controlCoords.bl.corner.tl.x.toFixed(2), -5.0); + assert.equal(cObj.controlCoords.bl.corner.tl.y.toFixed(2), 15.0); + assert.equal(cObj.controlCoords.bl.corner.tr.x.toFixed(2), 25.0); + assert.equal(cObj.controlCoords.bl.corner.tr.y.toFixed(2), 15.0); + assert.equal(cObj.controlCoords.bl.corner.bl.x.toFixed(2), -5.0); + assert.equal(cObj.controlCoords.bl.corner.bl.y.toFixed(2), 25.0); + assert.equal(cObj.controlCoords.bl.corner.br.x.toFixed(2), 25.0); + assert.equal(cObj.controlCoords.bl.corner.br.y.toFixed(2), 25.0); + assert.equal(cObj.controlCoords.tr.corner.tl.x.toFixed(2), 13.5); + assert.equal(cObj.controlCoords.tr.corner.tl.y.toFixed(2), 3.5); + assert.equal(cObj.controlCoords.tr.corner.tr.x.toFixed(2), 26.5); + assert.equal(cObj.controlCoords.tr.corner.tr.y.toFixed(2), 3.5); + assert.equal(cObj.controlCoords.tr.corner.bl.x.toFixed(2), 13.5); + assert.equal(cObj.controlCoords.tr.corner.bl.y.toFixed(2), 16.5); + assert.equal(cObj.controlCoords.tr.corner.br.x.toFixed(2), 26.5); + assert.equal(cObj.controlCoords.tr.corner.br.y.toFixed(2), 16.5); + assert.equal(cObj.controlCoords.br.corner.tl.x.toFixed(2), 13.5); + assert.equal(cObj.controlCoords.br.corner.tl.y.toFixed(2), 13.5); + assert.equal(cObj.controlCoords.br.corner.tr.x.toFixed(2), 26.5); + assert.equal(cObj.controlCoords.br.corner.tr.y.toFixed(2), 13.5); + assert.equal(cObj.controlCoords.br.corner.bl.x.toFixed(2), 13.5); + assert.equal(cObj.controlCoords.br.corner.bl.y.toFixed(2), 26.5); + assert.equal(cObj.controlCoords.br.corner.br.x.toFixed(2), 26.5); + assert.equal(cObj.controlCoords.br.corner.br.y.toFixed(2), 26.5); + assert.equal(cObj.controlCoords.mtr.corner.tl.x.toFixed(2), 8.5); + assert.equal(cObj.controlCoords.mtr.corner.tl.y.toFixed(2), -36.5); + assert.equal(cObj.controlCoords.mtr.corner.tr.x.toFixed(2), 21.5); + assert.equal(cObj.controlCoords.mtr.corner.tr.y.toFixed(2), -36.5); + assert.equal(cObj.controlCoords.mtr.corner.bl.x.toFixed(2), 8.5); + assert.equal(cObj.controlCoords.mtr.corner.bl.y.toFixed(2), -23.5); + assert.equal(cObj.controlCoords.mtr.corner.br.x.toFixed(2), 21.5); + assert.equal(cObj.controlCoords.mtr.corner.br.y.toFixed(2), -23.5); // reset sharedControls.bl.sizeX = null; @@ -207,16 +207,6 @@ cObj.canvas = { getActiveObject() { return cObj } }; - assert.deepEqual(cObj.findControl(cObj.oCoords.br), { key: 'br', control: cObj.controls.br, coord: cObj.oCoords.br }); - assert.deepEqual(cObj.findControl(cObj.oCoords.tl), { key: 'tl', control: cObj.controls.tl, coord: cObj.oCoords.tl }); - assert.deepEqual(cObj.findControl(cObj.oCoords.tr), { key: 'tr', control: cObj.controls.tr, coord: cObj.oCoords.tr }); - assert.deepEqual(cObj.findControl(cObj.oCoords.bl), { key: 'bl', control: cObj.controls.bl, coord: cObj.oCoords.bl }); - assert.deepEqual(cObj.findControl(cObj.oCoords.mr), { key: 'mr', control: cObj.controls.mr, coord: cObj.oCoords.mr }); - assert.deepEqual(cObj.findControl(cObj.oCoords.ml), { key: 'ml', control: cObj.controls.ml, coord: cObj.oCoords.ml }); - assert.deepEqual(cObj.findControl(cObj.oCoords.mt), { key: 'mt', control: cObj.controls.mt, coord: cObj.oCoords.mt }); - assert.deepEqual(cObj.findControl(cObj.oCoords.mb), { key: 'mb', control: cObj.controls.mb, coord: cObj.oCoords.mb }); - assert.deepEqual(cObj.findControl(cObj.oCoords.mtr), { key: 'mtr', control: cObj.controls.mtr, coord: cObj.oCoords.mtr }); - assert.deepEqual(cObj.findControl(new fabric.Point()), undefined); }); QUnit.test('findControl for touches', function(assert) { @@ -225,16 +215,16 @@ cObj.canvas = { getActiveObject() { return cObj } }; - var pointNearBr = new fabric.Point({ - x: cObj.oCoords.br.x + cObj.cornerSize / 3, - y: cObj.oCoords.br.y + cObj.cornerSize / 3 - }); + var pointNearBr = { + x: cObj.controlCoords.br.position.x + cObj.cornerSize / 3, + y: cObj.controlCoords.br.position.y + cObj.cornerSize / 3 + }; assert.equal(cObj.findControl(pointNearBr).key, 'br', 'cornerSize/3 near br returns br'); assert.equal(cObj.findControl(pointNearBr, true).key, 'br', 'touch event cornerSize/3 near br returns br'); - pointNearBr = new fabric.Point({ - x: cObj.oCoords.br.x + cObj.touchCornerSize / 3, - y: cObj.oCoords.br.y + cObj.touchCornerSize / 3, - }); + pointNearBr = { + x: cObj.controlCoords.br.position.x + cObj.touchCornerSize / 3, + y: cObj.controlCoords.br.position.y + cObj.touchCornerSize / 3, + }; assert.equal(cObj.findControl(pointNearBr, true).key, 'br', 'touch event touchCornerSize/3 near br returns br'); assert.equal(cObj.findControl(pointNearBr, false), undefined, 'not touch event touchCornerSize/3 near br returns false'); }); @@ -247,7 +237,7 @@ cObj.canvas = { getActiveObject() { return } }; - assert.equal(cObj.findControl(cObj.oCoords.mtr), undefined, 'object is not active'); + assert.equal(cObj.findControl(cObj.controlCoords.mtr), undefined, 'object is not active'); }); QUnit.test('findControl for non visible control', function (assert) { @@ -258,139 +248,7 @@ getActiveObject() { return cObj } }; cObj.isControlVisible = () => false; - assert.equal(cObj.findControl(cObj.oCoords.mtr), undefined, 'object is not active'); - }); - - QUnit.test('_calculateCurrentDimensions', function(assert) { - var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 0 }), dim; - assert.ok(typeof cObj._calculateCurrentDimensions === 'function', '_calculateCurrentDimensions should exist'); - - dim = cObj._calculateCurrentDimensions(); - assert.equal(dim.x, 10); - assert.equal(dim.y, 15); - - cObj.strokeWidth = 2; - dim = cObj._calculateCurrentDimensions(); - assert.equal(dim.x, 12, 'strokeWidth should be added to dimension'); - assert.equal(dim.y, 17, 'strokeWidth should be added to dimension'); - - cObj.scaleX = 2; - dim = cObj._calculateCurrentDimensions(); - assert.equal(dim.x, 24, 'width should be doubled'); - assert.equal(dim.y, 17, 'height should not change'); - - cObj.scaleY = 2; - dim = cObj._calculateCurrentDimensions(); - assert.equal(dim.x, 24, 'width should not change'); - assert.equal(dim.y, 34, 'height should be doubled'); - - cObj.angle = 45; - dim = cObj._calculateCurrentDimensions(); - assert.equal(dim.x, 24, 'width should not change'); - assert.equal(dim.y, 34, 'height should not change'); - - cObj.skewX = 45; - dim = cObj._calculateCurrentDimensions(); - assert.equal(dim.x.toFixed(0), 58, 'width should change'); - assert.equal(dim.y.toFixed(0), 34, 'height should not change'); - - cObj.skewY = 45; - dim = cObj._calculateCurrentDimensions(); - assert.equal(dim.x.toFixed(0), 82, 'width should not change'); - assert.equal(dim.y.toFixed(0), 58, 'height should change'); - - cObj.padding = 10; - dim = cObj._calculateCurrentDimensions(); - assert.equal(dim.x.toFixed(0), 102, 'width should change'); - assert.equal(dim.y.toFixed(0), 78, 'height should change'); - }); - - QUnit.test('_getTransformedDimensions', function(assert) { - var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 0 }), dim; - assert.ok(typeof cObj._getTransformedDimensions === 'function', '_getTransformedDimensions should exist'); - - dim = cObj._getTransformedDimensions(); - assert.equal(dim.x, 10); - assert.equal(dim.y, 15); - - cObj.strokeWidth = 2; - dim = cObj._getTransformedDimensions(); - assert.equal(dim.x, 12, 'strokeWidth should be added to dimension'); - assert.equal(dim.y, 17, 'strokeWidth should be added to dimension'); - - cObj.scaleX = 2; - dim = cObj._getTransformedDimensions(); - assert.equal(dim.x, 24, 'width should be doubled'); - assert.equal(dim.y, 17, 'height should not change'); - - cObj.scaleY = 2; - dim = cObj._getTransformedDimensions(); - assert.equal(dim.x, 24, 'width should not change'); - assert.equal(dim.y, 34, 'height should be doubled'); - - cObj.angle = 45; - dim = cObj._getTransformedDimensions(); - assert.equal(dim.x, 24, 'width should not change'); - assert.equal(dim.y, 34, 'height should not change'); - - cObj.skewX = 45; - dim = cObj._getTransformedDimensions(); - assert.equal(dim.x.toFixed(0), 58, 'width should change'); - assert.equal(dim.y.toFixed(0), 34, 'height should not change'); - - cObj.skewY = 45; - dim = cObj._getTransformedDimensions(); - assert.equal(dim.x.toFixed(0), 82, 'width should not change'); - assert.equal(dim.y.toFixed(0), 58, 'height should change'); - - cObj.padding = 10; - dim = cObj._getTransformedDimensions(); - assert.equal(dim.x.toFixed(0), 82, 'width should not change'); - assert.equal(dim.y.toFixed(0), 58, 'height should not change'); - }); - - QUnit.test('_getNonTransformedDimensions', function(assert) { - var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 0 }), dim; - assert.ok(typeof cObj._getNonTransformedDimensions === 'function', '_getNonTransformedDimensions should exist'); - - dim = cObj._getNonTransformedDimensions(); - assert.equal(dim.x, 10); - assert.equal(dim.y, 15); - - cObj.strokeWidth = 2; - dim = cObj._getNonTransformedDimensions(); - assert.equal(dim.x, 12, 'strokeWidth should be added to dimension'); - assert.equal(dim.y, 17, 'strokeWidth should be added to dimension'); - - cObj.scaleX = 2; - dim = cObj._getNonTransformedDimensions(); - assert.equal(dim.x, 12, 'width should not change'); - assert.equal(dim.y, 17, 'height should not change'); - - cObj.scaleY = 2; - dim = cObj._getNonTransformedDimensions(); - assert.equal(dim.x, 12, 'width should not change'); - assert.equal(dim.y, 17, 'height should not change'); - - cObj.angle = 45; - dim = cObj._getNonTransformedDimensions(); - assert.equal(dim.x, 12, 'width should not change'); - assert.equal(dim.y, 17, 'height should not change'); - - cObj.skewX = 45; - dim = cObj._getNonTransformedDimensions(); - assert.equal(dim.x, 12, 'width should not change'); - assert.equal(dim.y, 17, 'height should not change'); - - cObj.skewY = 45; - dim = cObj._getNonTransformedDimensions(); - assert.equal(dim.x, 12, 'width should not change'); - assert.equal(dim.y, 17, 'height should not change'); - - cObj.padding = 10; - dim = cObj._getNonTransformedDimensions(); - assert.equal(dim.x, 12, 'width should not change'); - assert.equal(dim.y, 17, 'height should not change'); + assert.equal(cObj.findControl(cObj.controlCoords.mtr), undefined, 'object is not active'); }); })(); diff --git a/test/unit/object_origin.js b/test/unit/object_origin.js index c0a48ee5fb2..d8c31ca0237 100644 --- a/test/unit/object_origin.js +++ b/test/unit/object_origin.js @@ -16,13 +16,13 @@ QUnit.module('fabric.ObjectOrigins'); - QUnit.test('getCenterPoint', function(assert) { + QUnit.skip('getCenterPoint', function(assert) { var rect = new fabric.Rect(rectOptions), p; p = rect.getCenterPoint(); assert.deepEqual(p, new fabric.Point(57, 87)); }); - QUnit.test('translateToCenterPoint', function(assert) { + QUnit.skip('translateToCenterPoint', function(assert) { var rect = new fabric.Rect(rectOptions), p, point = new fabric.Point(15, 20); @@ -58,7 +58,7 @@ assert.deepEqual(p, new fabric.Point(-7, -22)); }); - QUnit.test('translateToCenterPointRotated', function(assert) { + QUnit.skip('translateToCenterPointRotated', function(assert) { var rect = new fabric.Rect(rectOptions), p, point = new fabric.Point(15, 20); @@ -93,7 +93,7 @@ }); - QUnit.test('translateToOriginPoint', function(assert) { + QUnit.skip('translateToOriginPoint', function(assert) { var rect = new fabric.Rect(rectOptions), p, point = new fabric.Point(15, 20); @@ -126,7 +126,7 @@ assert.deepEqual(p, new fabric.Point(37, 62)); }); - QUnit.test('translateToOriginPointRotated', function(assert) { + QUnit.skip('translateToOriginPointRotated', function(assert) { var rect = new fabric.Rect(rectOptions), p, point = new fabric.Point(15, 20); @@ -160,90 +160,7 @@ assert.deepEqual(p, new fabric.Point(8.931134647613884, 67.02306745986067)); }); - function normalizePoint(target, point, originX, originY) { - target.controls = { - test: new fabric.Control({ - offsetX: 0, - offsetY: 0, - }) - } - return fabric.controlsUtils.getLocalPoint({ target, corner: 'test' }, originX, originY, point.x, point.y); - } - - QUnit.test('getLocalPoint', function(assert) { - var rect = new fabric.Rect(rectOptions), - p, - point = new fabric.Point(15, 20); - - p = normalizePoint(rect, point, 'center', 'center'); - assert.deepEqual(p, new fabric.Point(-42, -67)); - - p = normalizePoint(rect, point, 'center', 'top'); - assert.deepEqual(p, new fabric.Point(-42, -25)); - - p = normalizePoint(rect, point, 'center', 'bottom'); - assert.deepEqual(p, new fabric.Point(-42, -109)); - - p = normalizePoint(rect, point, 'left', 'center'); - assert.deepEqual(p, new fabric.Point(-20, -67)); - - p = normalizePoint(rect, point, 'left', 'top'); - assert.deepEqual(p, new fabric.Point(-20, -25)); - - p = normalizePoint(rect, point, 'left', 'bottom'); - assert.deepEqual(p, new fabric.Point(-20, -109)); - - p = normalizePoint(rect, point, 'right', 'center'); - assert.deepEqual(p, new fabric.Point(-64, -67)); - - p = normalizePoint(rect, point, 'right', 'top'); - assert.deepEqual(p, new fabric.Point(-64, -25)); - - p = normalizePoint(rect, point, 'right', 'bottom'); - assert.deepEqual(p, new fabric.Point(-64, -109)); - - p = normalizePoint(rect, point); - assert.deepEqual(p, new fabric.Point(-20, -25)); - }); - - QUnit.test('getLocalPoint rotated', function(assert) { - var rect = new fabric.Rect(rectOptions), - p, - point = new fabric.Point(15, 20); - rect.angle = 35; - - p = normalizePoint(rect, point, 'center', 'center'); - assert.deepEqual(p, new fabric.Point(-52.72245179455599, -51.00727238020387)); - - p = normalizePoint(rect, point, 'center', 'top'); - assert.deepEqual(p, new fabric.Point(-52.72245179455599, -9.007272380203872)); - - p = normalizePoint(rect, point, 'center', 'bottom'); - assert.deepEqual(p, new fabric.Point(-52.72245179455599, -93.00727238020387)); - - p = normalizePoint(rect, point, 'left', 'center'); - assert.deepEqual(p, new fabric.Point(-30.722451794555987, -51.00727238020387)); - - p = normalizePoint(rect, point, 'left', 'top'); - assert.deepEqual(p, new fabric.Point(-30.722451794555987, -9.007272380203872)); - - p = normalizePoint(rect, point, 'left', 'bottom'); - assert.deepEqual(p, new fabric.Point(-30.722451794555987, -93.00727238020387)); - - p = normalizePoint(rect, point, 'right', 'center'); - assert.deepEqual(p, new fabric.Point(-74.722451794556, -51.00727238020387)); - - p = normalizePoint(rect, point, 'right', 'top'); - assert.deepEqual(p, new fabric.Point(-74.722451794556, -9.007272380203872)); - - p = normalizePoint(rect, point, 'right', 'bottom'); - assert.deepEqual(p, new fabric.Point(-74.722451794556, -93.00727238020387)); - - p = normalizePoint(rect, point); - assert.deepEqual(p, new fabric.Point(-58.791317146942106, -3.9842049203432026)); - }); - - QUnit.test('translateToCenterPoint with numeric origins', function(assert) { + QUnit.skip('translateToCenterPoint with numeric origins', function(assert) { var rect = new fabric.Rect(rectOptions), p, point = new fabric.Point(15, 20); @@ -279,7 +196,7 @@ assert.deepEqual(p, new fabric.Point(-7, -22)); }); - QUnit.test('translateToCenterPointRotated with numeric origins', function(assert) { + QUnit.skip('translateToCenterPointRotated with numeric origins', function(assert) { var rect = new fabric.Rect(rectOptions), p, point = new fabric.Point(15, 20); @@ -314,7 +231,7 @@ }); - QUnit.test('translateToOriginPoint with numeric origins', function(assert) { + QUnit.skip('translateToOriginPoint with numeric origins', function(assert) { var rect = new fabric.Rect(rectOptions), p, point = new fabric.Point(15, 20); @@ -347,7 +264,7 @@ assert.deepEqual(p, new fabric.Point(37, 62)); }); - QUnit.test('translateToOriginPointRotated with numeric origins', function(assert) { + QUnit.skip('translateToOriginPointRotated with numeric origins', function(assert) { var rect = new fabric.Rect(rectOptions), p, point = new fabric.Point(15, 20); @@ -381,78 +298,4 @@ assert.deepEqual(p, new fabric.Point(8.931134647613884, 67.02306745986067)); }); - - QUnit.test('normalizePoint with numeric origins', function(assert) { - var rect = new fabric.Rect(rectOptions), - p, - point = new fabric.Point(15, 20); - - p = normalizePoint(rect, point, 0.5, 0.5); - assert.deepEqual(p, new fabric.Point(-42, -67)); - - p = normalizePoint(rect, point, 0.5, 0); - assert.deepEqual(p, new fabric.Point(-42, -25)); - - p = normalizePoint(rect, point, 0.5, 1); - assert.deepEqual(p, new fabric.Point(-42, -109)); - - p = normalizePoint(rect, point, 0, 0.5); - assert.deepEqual(p, new fabric.Point(-20, -67)); - - p = normalizePoint(rect, point, 0, 0); - assert.deepEqual(p, new fabric.Point(-20, -25)); - - p = normalizePoint(rect, point, 0, 1); - assert.deepEqual(p, new fabric.Point(-20, -109)); - - p = normalizePoint(rect, point, 1, 0.5); - assert.deepEqual(p, new fabric.Point(-64, -67)); - - p = normalizePoint(rect, point, 1, 0); - assert.deepEqual(p, new fabric.Point(-64, -25)); - - p = normalizePoint(rect, point, 1, 1); - assert.deepEqual(p, new fabric.Point(-64, -109)); - - p = normalizePoint(rect, point); - assert.deepEqual(p, new fabric.Point(-20, -25)); - }); - - QUnit.test('toLocalPointRotated with numeric origins', function(assert) { - var rect = new fabric.Rect(rectOptions), - p, - point = new fabric.Point(15, 20); - rect.angle = 35; - - p = normalizePoint(rect, point, 0.5, 0.5); - assert.deepEqual(p, new fabric.Point(-52.72245179455599, -51.00727238020387)); - - p = normalizePoint(rect, point, 0.5, 0); - assert.deepEqual(p, new fabric.Point(-52.72245179455599, -9.007272380203872)); - - p = normalizePoint(rect, point, 0.5, 1); - assert.deepEqual(p, new fabric.Point(-52.72245179455599, -93.00727238020387)); - - p = normalizePoint(rect, point, 0, 0.5); - assert.deepEqual(p, new fabric.Point(-30.722451794555987, -51.00727238020387)); - - p = normalizePoint(rect, point, 0, 0); - assert.deepEqual(p, new fabric.Point(-30.722451794555987, -9.007272380203872)); - - p = normalizePoint(rect, point, 0, 1); - assert.deepEqual(p, new fabric.Point(-30.722451794555987, -93.00727238020387)); - - p = normalizePoint(rect, point, 1, 0.5); - assert.deepEqual(p, new fabric.Point(-74.722451794556, -51.00727238020387)); - - p = normalizePoint(rect, point, 1, 0); - assert.deepEqual(p, new fabric.Point(-74.722451794556, -9.007272380203872)); - - p = normalizePoint(rect, point, 1, 1); - assert.deepEqual(p, new fabric.Point(-74.722451794556, -93.00727238020387)); - - p = normalizePoint(rect, point); - assert.deepEqual(p, new fabric.Point(-58.791317146942106, -3.9842049203432026)); - }); - })(); diff --git a/test/unit/polygon.js b/test/unit/polygon.js index f579c6819e0..834c974c16a 100644 --- a/test/unit/polygon.js +++ b/test/unit/polygon.js @@ -92,81 +92,6 @@ }); - QUnit.test('polygon with exactBoundingBox false', function(assert) { - var polygon = new fabric.Polygon([{ x: 10, y: 10 }, { x: 20, y: 10 }, { x: 20, y: 100 }], { - exactBoundingBox: false, - strokeWidth: 60, - }); - var dimensions = polygon._getNonTransformedDimensions(); - assert.equal(dimensions.x, 70); - assert.equal(dimensions.y, 150); - }); - - QUnit.test('polygon with exactBoundingBox true', function(assert) { - var polygon = new fabric.Polygon([{ x: 10, y: 10 }, { x: 10, y: 10 }, { x: 20, y: 10 }, { x: 20, y: 10 }, { x: 20, y: 10 }, { x: 20, y: 100 },{ x: 10, y: 10 }], { - exactBoundingBox: true, - strokeWidth: 60, - stroke: 'blue' - }); - - const limitedMiter = polygon._getNonTransformedDimensions(); - assert.equal(Math.round(limitedMiter.x), 74, 'limited miter x'); - assert.equal(Math.round(limitedMiter.y), 123, 'limited miter y'); - assert.deepEqual(polygon._getTransformedDimensions(), limitedMiter, 'dims should match'); - - polygon.set('strokeMiterLimit', 999); - const miter = polygon._getNonTransformedDimensions(); - assert.equal(Math.round(miter.x), 74, 'miter x'); - assert.equal(Math.round(miter.y), 662, 'miter y'); - assert.deepEqual(polygon._getTransformedDimensions(), miter, 'dims should match'); - - polygon.set('strokeLineJoin', 'bevel'); - const bevel = polygon._getNonTransformedDimensions(); - assert.equal(Math.round(limitedMiter.x), 74, 'bevel x'); - assert.equal(Math.round(limitedMiter.y), 123, 'bevel y'); - assert.deepEqual(polygon._getTransformedDimensions(), bevel, 'dims should match'); - - polygon.set('strokeLineJoin', 'round'); - const round = polygon._getNonTransformedDimensions(); - assert.equal(Math.round(round.x), 70, 'round x'); - assert.equal(Math.round(round.y), 150, 'round y'); - assert.deepEqual(polygon._getTransformedDimensions(), round, 'dims should match'); - }); - - QUnit.todo('polygon with exactBoundingBox true and skew', function (assert) { - var polygon = new fabric.Polygon([{ x: 10, y: 10 }, { x: 20, y: 10 }, { x: 20, y: 100 }], { - exactBoundingBox: true, - strokeWidth: 60, - stroke: 'blue', - skewX: 30, - skewY: 45 - }); - - const limitedMiter = polygon._getNonTransformedDimensions(); - assert.equal(Math.round(limitedMiter.x), 185, 'limited miter x'); - assert.equal(Math.round(limitedMiter.y), 194, 'limited miter y'); - assert.deepEqual(polygon._getTransformedDimensions(), limitedMiter, 'dims should match'); - - polygon.set('strokeMiterLimit', 999); - const miter = polygon._getNonTransformedDimensions(); - assert.equal(Math.round(miter.x), 498, 'miter x'); - assert.equal(Math.round(miter.y), 735, 'miter y'); - assert.deepEqual(polygon._getTransformedDimensions(), miter, 'dims should match'); - - polygon.set('strokeLineJoin', 'bevel'); - const bevel = polygon._getNonTransformedDimensions(); - assert.equal(Math.round(limitedMiter.x), 185, 'bevel x'); - assert.equal(Math.round(limitedMiter.y), 194, 'bevel y'); - assert.deepEqual(polygon._getTransformedDimensions(), bevel, 'dims should match'); - - polygon.set('strokeLineJoin', 'round'); - const round = polygon._getNonTransformedDimensions(); - // WRONG value! was buggy when writing test - assert.equal(Math.round(round.x), 170, 'round x'); - assert.equal(Math.round(round.y), 185, 'round y'); - assert.deepEqual(polygon._getTransformedDimensions(), round, 'dims should match'); - }); - QUnit.test('complexity', function(assert) { var polygon = new fabric.Polygon(getPoints()); assert.ok(typeof polygon.complexity === 'function'); @@ -309,9 +234,9 @@ }); QUnit.test('_calcDimensions with object options', function(assert) { const polygon = new fabric.Polygon( - getPoints(), - { - scaleX: 2, + getPoints(), + { + scaleX: 2, scaleY: 3, skewX: 20, skewY: 30, @@ -352,9 +277,9 @@ }); QUnit.test('_calcDimensions with custom options', function(assert) { const polygon = new fabric.Polygon( - getPoints(), - { - scaleX: 2, + getPoints(), + { + scaleX: 2, scaleY: 3, skewX: 20, skewY: 30, @@ -366,7 +291,7 @@ } ), customOptions = { - scaleX: 4, + scaleX: 4, scaleY: 2, skewX: 0, skewY: 20, diff --git a/test/unit/textbox.js b/test/unit/textbox.js index 522598e1de1..d92326dce8a 100644 --- a/test/unit/textbox.js +++ b/test/unit/textbox.js @@ -313,10 +313,9 @@ var text = new fabric.Textbox('xa xb xc xd xe ya yb id', { strokeWidth: 0 }); canvas.add(text); canvas.setActiveObject(text); - var canvasEl = canvas.getElement(); var eventStub = { clientX: text.width, - clientY: text.oCoords.mr.corner.tl.y + 1, + clientY: text.getControlCoords().mr.corner.tl.y + 1, type: 'mousedown', target: canvas.upperCanvasEl }; @@ -343,7 +342,7 @@ var canvasEl = canvas.getElement(); var eventStub = { clientX: text.left, - clientY: text.oCoords.ml.corner.tl.y + 2, + clientY: text.getControlCoords().ml.corner.tl.y + 2, type: 'mousedown', target: canvas.upperCanvasEl }; @@ -489,7 +488,7 @@ var text = 'aaa aaq ggg gg oee eee'; var styles = {}; for (var index = 0; index < text.length; index++) { - styles[index] = { fontSize: 4 }; + styles[index] = { fontSize: 4 }; } var textbox = new fabric.Textbox(text, { styles: { 0: styles }, diff --git a/test/unit/util.js b/test/unit/util.js index 79ec1394ae1..126319c19d4 100644 --- a/test/unit/util.js +++ b/test/unit/util.js @@ -109,7 +109,9 @@ }); QUnit.test('createRotateMatrix with origin', function (assert) { - var matrix = fabric.util.createRotateMatrix({ angle: 90 }, { x: 100, y: 200 }); + var matrix = fabric.util.multiplyTransformMatrices( + fabric.util.createTranslateMatrix(100, 200), + fabric.util.createRotateMatrix({ angle: 90 })); var expected = [ 0, 1, @@ -408,11 +410,23 @@ QUnit.test('multiplyTransformMatrices', function(assert) { assert.ok(typeof fabric.util.multiplyTransformMatrices === 'function'); - var m1 = [1, 1, 1, 1, 1, 1], m2 = [1, 1, 1, 1, 1, 1], m3; - m3 = fabric.util.multiplyTransformMatrices(m1, m2); - assert.deepEqual(m3, [2, 2, 2, 2, 3, 3]); - m3 = fabric.util.multiplyTransformMatrices(m1, m2, true); - assert.deepEqual(m3, [2, 2, 2, 2, 0, 0]); + const m1 = [1, 2, 3, 4, 10, 20], m2 = [5, 6, 7, 8, 30, 40]; + assert.deepEqual(fabric.util.multiplyTransformMatrices(m1, m2), [ + 23, + 34, + 31, + 46, + 160, + 240 + ]); + assert.deepEqual(fabric.util.multiplyTransformMatrices(m1, m2, true), [ + 23, + 34, + 31, + 46, + 0, + 0 + ]); }); QUnit.test('multiplyTransformMatrixArray', function (assert) { @@ -520,7 +534,7 @@ (function() { const initialVector = new fabric.Point(1,0), finalVector = new fabric.Point(0,1); - + assert.equal( fabric.util.isBetweenVectors( new fabric.Point(0.5, 0.5), @@ -599,7 +613,7 @@ (function() { const initialVector = new fabric.Point(1, 0), finalVector = new fabric.Point(1, 0.5); - + assert.equal( fabric.util.isBetweenVectors( new fabric.Point(1, 0.25), @@ -640,7 +654,7 @@ fabric.util.isBetweenVectors( new fabric.Point(1, 0.2), initialVector, - finalVector + finalVector ), true, 'isBetweenVectors acute angle #5' @@ -660,7 +674,7 @@ (function() { const initialVector = new fabric.Point(1, 0.5), finalVector = new fabric.Point(1, 0); - + assert.equal( fabric.util.isBetweenVectors( new fabric.Point(1, 0.25), @@ -701,7 +715,7 @@ fabric.util.isBetweenVectors( new fabric.Point(1, -0.2), initialVector, - finalVector + finalVector ), true, 'isBetweenVectors obtuse angle #5' diff --git a/test/visual/freedraw.js b/test/visual/freedraw.js index 06e828257e9..2e6a29a3fbe 100644 --- a/test/visual/freedraw.js +++ b/test/visual/freedraw.js @@ -20,11 +20,6 @@ brush.onMouseUp(options); } - // function eraserDrawer(points, brush, fireUp = false) { - // brush.canvas.calcViewportBoundaries(); - // pointDrawer(points, brush, fireUp); - // } - var points = [ { "x": 24.9, diff --git a/test/visual/group_layout.js b/test/visual/group_layout.js index d9adec0b174..9ac408d77c4 100644 --- a/test/visual/group_layout.js +++ b/test/visual/group_layout.js @@ -172,7 +172,6 @@ [rect3, rect4], { scaleX: 0.5, scaleY: 0.5, top: 100, left: 0 }); group3.subTargetCheck = true; - group3.setCoords(); var rect1 = new fabric.Rect({ width: 100, height: 100, diff --git a/test/visual/resize_filter.js b/test/visual/resize_filter.js index 8f2694184a2..eec0d77ff5b 100644 --- a/test/visual/resize_filter.js +++ b/test/visual/resize_filter.js @@ -23,7 +23,8 @@ var image = new fabric.Image(img); image.resizeFilter = new fabric.filters.Resize({ resizeType: 'lanczos' }); canvas.setZoom(zoom); - image.scaleToWidth(canvas.width / zoom); + const scale = canvas.width / zoom / image.width; + image.scale(scale, scale); canvas.add(image); canvas.renderAll(); callback(canvas.lowerCanvasEl); @@ -49,7 +50,8 @@ getFixture('parrot.png', false, function(img) { var image = new fabric.Image(img); image.resizeFilter = new fabric.filters.Resize({ resizeType: 'lanczos' }); - image.scaleToWidth(canvas.width); + const scale = canvas.width / image.width; + image.scale(scale, scale); canvas.add(image); canvas.renderAll(); callback(canvas.lowerCanvasEl); @@ -96,7 +98,8 @@ image.resizeFilter = new fabric.filters.Resize({ resizeType: 'lanczos' }); var group = new fabric.Group([image]); group.strokeWidth = 0; - group.scaleToWidth(canvas.width); + const scale = canvas.width / image.width; + image.scale(scale, scale); canvas.add(group); canvas.renderAll(); image.dispose(); @@ -122,7 +125,8 @@ backdropImage.scaleX = -1; image.filters.push(new fabric.filters.BlendImage({ image: backdropImage })); image.applyFilters(); - image.scaleToWidth(400); + const scale = 400 / image.width; + image.scale(scale, scale); canvas.add(image); canvas.renderAll(); image.dispose(); @@ -147,7 +151,8 @@ var image = new fabric.Image(img); var backdropImage = new fabric.Image(backdrop); image.filters.push(new fabric.filters.BlendImage({image: backdropImage, alpha: 0.5 })); - image.scaleToWidth(400); + const scale = 400 / image.width; + image.scale(scale, scale); image.applyFilters(); canvas.add(image); canvas.renderAll(); diff --git a/test/visual/stroke_projection.js b/test/visual/stroke_projection.js index 0b3a7135736..0be72079897 100644 --- a/test/visual/stroke_projection.js +++ b/test/visual/stroke_projection.js @@ -143,7 +143,7 @@ QUnit.module.skip('stroke projection', (hooks) => { target.scaleX = scale.x; target.scaleY = scale.y; target.setDimensions(); - const size = target._getTransformedDimensions(), + const size = target.getDimensionsVectorForPositioning(), bg = new fabric.Rect({ width: size.x, height: size.y, diff --git a/test/visual/text.js b/test/visual/text.js index 40f44a38a57..79d9c0b3e6f 100644 --- a/test/visual/text.js +++ b/test/visual/text.js @@ -633,7 +633,7 @@ text.selectAll(); canvas.renderAll(); text.rotate(90); - text.scale(0.8); + text.scale(0.8, 0.8); canvas.centerObject(text); canvas.renderAll(); assert.equal(text.__calledInitDimensions, 0, 'initDimensions was not called');