From 6df783b55a37836e3accf334514c9adb86735c78 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 24 Sep 2023 15:15:37 +0200 Subject: [PATCH 01/12] patch(Control): move hit detection to shouldActivate (#9374) --- CHANGELOG.md | 1 + src/controls/Control.ts | 13 ++- src/shapes/Object/InteractiveObject.ts | 19 ++-- src/shapes/Object/ObjectGeometry.ts | 118 +++--------------------- src/util/intersection/findCrossPoint.ts | 114 +++++++++++++++++++++++ 5 files changed, 147 insertions(+), 118 deletions(-) create mode 100644 src/util/intersection/findCrossPoint.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f5fc760c15b..3e50e6c627f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- patch(Control): move hit detection to shouldActivate [#9374](https://github.com/fabricjs/fabric.js/pull/9374) - fix(StaticCanvas): disposing animations [#9361](https://github.com/fabricjs/fabric.js/pull/9361) - fix(IText): cursor width under group [#9341](https://github.com/fabricjs/fabric.js/pull/9341) - TS(Canvas): constructor optional el [#9348](https://github.com/fabricjs/fabric.js/pull/9348) diff --git a/src/controls/Control.ts b/src/controls/Control.ts index 702665d7c1f..e9e2d24024c 100644 --- a/src/controls/Control.ts +++ b/src/controls/Control.ts @@ -7,7 +7,8 @@ import type { } from '../EventTypeDefs'; import { Point } from '../Point'; import type { InteractiveFabricObject } from '../shapes/Object/InteractiveObject'; -import type { TDegree, TMat2D } from '../typedefs'; +import type { TCornerPoint, TDegree, TMat2D } from '../typedefs'; +import { cornerPointContainsPoint } from '../util/intersection/findCrossPoint'; import { cos } from '../util/misc/cos'; import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; import { sin } from '../util/misc/sin'; @@ -169,11 +170,17 @@ export class Control { */ declare mouseUpHandler?: ControlActionHandler; - shouldActivate(controlKey: string, fabricObject: InteractiveFabricObject) { + shouldActivate( + controlKey: string, + fabricObject: InteractiveFabricObject, + pointer: Point, + cornerPoint: TCornerPoint + ) { // TODO: locking logic can be handled here instead of in the control handler logic return ( fabricObject.canvas?.getActiveObject() === fabricObject && - fabricObject.isControlVisible(controlKey) + fabricObject.isControlVisible(controlKey) && + cornerPointContainsPoint(pointer, cornerPoint) ); } diff --git a/src/shapes/Object/InteractiveObject.ts b/src/shapes/Object/InteractiveObject.ts index ac05ef8c198..d027559cd9c 100644 --- a/src/shapes/Object/InteractiveObject.ts +++ b/src/shapes/Object/InteractiveObject.ts @@ -198,18 +198,19 @@ export class InteractiveFabricObject< const cornerEntries = Object.entries(this.oCoords); for (let i = cornerEntries.length - 1; i >= 0; i--) { const [key, corner] = cornerEntries[i]; - if (this.controls[key].shouldActivate(key, this)) { - const lines = this._getImageLines( + if ( + this.controls[key].shouldActivate( + key, + this, + pointer, forTouch ? corner.touchCorner : corner.corner - ); - const xPoints = this._findCrossPoints(pointer, lines); - if (xPoints !== 0 && xPoints % 2 === 1) { - this.__corner = key; - return key; - } + ) + ) { + // this.canvas.contextTop.fillRect(pointer.x - 1, pointer.y - 1, 2, 2); + return (this.__corner = key); } - // // debugging + // // debugging needs rework // // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); diff --git a/src/shapes/Object/ObjectGeometry.ts b/src/shapes/Object/ObjectGeometry.ts index d9e76e9e2bc..7ad7fcdb076 100644 --- a/src/shapes/Object/ObjectGeometry.ts +++ b/src/shapes/Object/ObjectGeometry.ts @@ -27,18 +27,11 @@ import type { StaticCanvas } from '../../canvas/StaticCanvas'; import { ObjectOrigin } from './ObjectOrigin'; import type { ObjectEvents } from '../../EventTypeDefs'; import type { ControlProps } from './types/ControlProps'; - -type TLineDescriptor = { - o: Point; - d: Point; -}; - -type TBBoxLines = { - topline: TLineDescriptor; - leftline: TLineDescriptor; - bottomline: TLineDescriptor; - rightline: TLineDescriptor; -}; +import { + type TBBoxLines, + findCrossPoints, + getImageLines, +} from '../../util/intersection/findCrossPoint'; type TMatrixCache = { key: string; @@ -306,7 +299,11 @@ export class ObjectGeometry ): boolean { const points = this.getCoords(absolute, calculate), otherCoords = absolute ? other.aCoords : other.lineCoords, - lines = other._getImageLines(otherCoords); + // this is maybe an excessive optimization that makes the code + // unnecessarly ugly. this is the only use case of passing lines + // to containsPoint. This optimization should go away but can go away + // in its own pr. + lines = getImageLines(otherCoords); for (let i = 0; i < 4; i++) { if (!other.containsPoint(points[i], lines)) { return false; @@ -349,7 +346,7 @@ export class ObjectGeometry /** * Checks if point is inside the object * @param {Point} point Point to check against - * @param {Object} [lines] object returned from @method _getImageLines + * @param {Object} [lines] object returned from util getImageLines * @param {Boolean} [absolute] use coordinates without viewportTransform * @param {Boolean} [calculate] use coordinates of current position instead of stored ones * @return {Boolean} true if point is inside the object @@ -361,8 +358,7 @@ export class ObjectGeometry calculate = false ): boolean { const coords = this._getCoords(absolute, calculate), - imageLines = lines || this._getImageLines(coords), - xPoints = this._findCrossPoints(point, imageLines); + xPoints = findCrossPoints(point, lines || getImageLines(coords)); // if xPoints is odd then point is inside the object return xPoints !== 0 && xPoints % 2 === 1; } @@ -440,96 +436,6 @@ export class ObjectGeometry ); } - /** - * Method that returns an object with the object edges in it, given the coordinates of the corners - * @private - * @param {Object} lineCoords or aCoords Coordinates of the object corners - */ - _getImageLines({ tl, tr, bl, br }: TCornerPoint): TBBoxLines { - const lines = { - topline: { - o: tl, - d: tr, - }, - rightline: { - o: tr, - d: br, - }, - bottomline: { - o: br, - d: bl, - }, - leftline: { - o: bl, - d: tl, - }, - }; - - // // debugging - // if (this.canvas.contextTop) { - // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); - // } - - return lines; - } - - /** - * Helper method to determine how many cross points are between the 4 object edges - * and the horizontal line determined by a point on canvas - * @private - * @param {Point} point Point to check - * @param {Object} lines Coordinates of the object being evaluated - * @return {number} number of crossPoint - */ - _findCrossPoints(point: Point, lines: TBBoxLines): number { - let xcount = 0; - - for (const lineKey in lines) { - let xi; - const iLine = lines[lineKey as keyof TBBoxLines]; - // optimization 1: line below point. no cross - if (iLine.o.y < point.y && iLine.d.y < point.y) { - continue; - } - // optimization 2: line above point. no cross - if (iLine.o.y >= point.y && iLine.d.y >= point.y) { - continue; - } - // optimization 3: vertical line case - if (iLine.o.x === iLine.d.x && iLine.o.x >= point.x) { - xi = iLine.o.x; - } - // calculate the intersection point - else { - const b1 = 0; - const b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x); - const a1 = point.y - b1 * point.x; - const a2 = iLine.o.y - b2 * iLine.o.x; - - xi = -(a1 - a2) / (b1 - b2); - } - // don't count xi < point.x cases - if (xi >= point.x) { - xcount += 1; - } - // optimization 4: specific for square images - if (xcount === 2) { - break; - } - } - return xcount; - } - /** * Returns coordinates of object's bounding rectangle (left, top, width, height) * the box is intended as aligned to axis of canvas. diff --git a/src/util/intersection/findCrossPoint.ts b/src/util/intersection/findCrossPoint.ts new file mode 100644 index 00000000000..d4942740f7d --- /dev/null +++ b/src/util/intersection/findCrossPoint.ts @@ -0,0 +1,114 @@ +import type { XY } from '../../Point'; +import type { TCornerPoint } from '../../typedefs'; + +type TLineDescriptor = { + o: XY; + d: XY; +}; + +export type TBBoxLines = { + topline: TLineDescriptor; + leftline: TLineDescriptor; + bottomline: TLineDescriptor; + rightline: TLineDescriptor; +}; + +/** + * Helper method to determine how many cross points are between the 4 object edges + * and the horizontal line determined by a point on canvas + * @private + * @param {Point} point Point to check + * @param {Object} lines Coordinates of the object being evaluated + * @return {number} number of crossPoint + */ +export const findCrossPoints = (point: XY, lines: TBBoxLines): number => { + let xcount = 0; + + for (const lineKey in lines) { + let xi; + const iLine = lines[lineKey as keyof TBBoxLines]; + // optimization 1: line below point. no cross + if (iLine.o.y < point.y && iLine.d.y < point.y) { + continue; + } + // optimization 2: line above point. no cross + if (iLine.o.y >= point.y && iLine.d.y >= point.y) { + continue; + } + // optimization 3: vertical line case + if (iLine.o.x === iLine.d.x && iLine.o.x >= point.x) { + xi = iLine.o.x; + } + // calculate the intersection point + else { + const b1 = 0; + const b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x); + const a1 = point.y - b1 * point.x; + const a2 = iLine.o.y - b2 * iLine.o.x; + + xi = -(a1 - a2) / (b1 - b2); + } + // don't count xi < point.x cases + if (xi >= point.x) { + xcount += 1; + } + // optimization 4: specific for square images (square or rects?) + // todo remove this optimazion for + if (xcount === 2) { + break; + } + } + return xcount; +}; + +/** + * Method that returns an object with the object edges in it, given the coordinates of the corners + * @private + * @param {Object} lineCoords or aCoords Coordinates of the object corners + */ +export const getImageLines = ({ tl, tr, bl, br }: TCornerPoint): TBBoxLines => { + const lines = { + topline: { + o: tl, + d: tr, + }, + rightline: { + o: tr, + d: br, + }, + bottomline: { + o: br, + d: bl, + }, + leftline: { + o: bl, + d: tl, + }, + }; + + // // debugging + // if (this.canvas.contextTop) { + // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); + // } + + return lines; +}; + +export const cornerPointContainsPoint = ( + point: XY, + cornerPoint: TCornerPoint +): boolean => { + const xPoints = findCrossPoints(point, getImageLines(cornerPoint)); + // if xPoints is odd then point is inside the object + return xPoints !== 0 && xPoints % 2 === 1; +}; From 396308950bf9389ba512aeea4f08b5bbc9445c62 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 24 Sep 2023 15:37:39 +0200 Subject: [PATCH 02/12] BREAKING(Object) Remove lines parameter from object.containsPoint (#9375) --- CHANGELOG.md | 1 + src/Collection.ts | 5 ++-- src/shapes/Object/ObjectGeometry.ts | 38 ++++++++----------------- src/util/intersection/findCrossPoint.ts | 4 +-- 4 files changed, 17 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e50e6c627f..618ba972c72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- feature(Object) BREAKING: Remove lines parameter from object.containsPoint [#9375](https://github.com/fabricjs/fabric.js/pull/9375) - patch(Control): move hit detection to shouldActivate [#9374](https://github.com/fabricjs/fabric.js/pull/9374) - fix(StaticCanvas): disposing animations [#9361](https://github.com/fabricjs/fabric.js/pull/9361) - fix(IText): cursor width under group [#9341](https://github.com/fabricjs/fabric.js/pull/9341) diff --git a/src/Collection.ts b/src/Collection.ts index c46649cc270..9d697843964 100644 --- a/src/Collection.ts +++ b/src/Collection.ts @@ -329,9 +329,8 @@ export function createCollectionMixin(Base: TBase) { object.visible && ((includeIntersecting && object.intersectsWithRect(tl, br, true)) || object.isContainedWithinRect(tl, br, true) || - (includeIntersecting && - object.containsPoint(tl, undefined, true)) || - (includeIntersecting && object.containsPoint(br, undefined, true))) + (includeIntersecting && object.containsPoint(tl, true)) || + (includeIntersecting && object.containsPoint(br, true))) ) { objects.push(object); } diff --git a/src/shapes/Object/ObjectGeometry.ts b/src/shapes/Object/ObjectGeometry.ts index 7ad7fcdb076..efcf5cb83e3 100644 --- a/src/shapes/Object/ObjectGeometry.ts +++ b/src/shapes/Object/ObjectGeometry.ts @@ -27,11 +27,7 @@ import type { StaticCanvas } from '../../canvas/StaticCanvas'; import { ObjectOrigin } from './ObjectOrigin'; import type { ObjectEvents } from '../../EventTypeDefs'; import type { ControlProps } from './types/ControlProps'; -import { - type TBBoxLines, - findCrossPoints, - getImageLines, -} from '../../util/intersection/findCrossPoint'; +import { cornerPointContainsPoint } from '../../util/intersection/findCrossPoint'; type TMatrixCache = { key: string; @@ -289,7 +285,7 @@ export class ObjectGeometry * Checks if object is fully contained within area of another object * @param {Object} other Object to test * @param {Boolean} [absolute] use coordinates without viewportTransform - * @param {Boolean} [calculate] use coordinates of current position instead of store ones + * @param {Boolean} [calculate] use coordinates of current position instead of stored ones * @return {Boolean} true if object is fully contained within area of another object */ isContainedWithinObject( @@ -297,15 +293,11 @@ export class ObjectGeometry absolute = false, calculate = false ): boolean { - const points = this.getCoords(absolute, calculate), - otherCoords = absolute ? other.aCoords : other.lineCoords, - // this is maybe an excessive optimization that makes the code - // unnecessarly ugly. this is the only use case of passing lines - // to containsPoint. This optimization should go away but can go away - // in its own pr. - lines = getImageLines(otherCoords); + const points = this.getCoords(absolute, calculate); for (let i = 0; i < 4; i++) { - if (!other.containsPoint(points[i], lines)) { + // bug/confusing: this containsPoint should receive 'calculate' as well. + // will come later because it needs to come with tests + if (!other.containsPoint(points[i], absolute)) { return false; } } @@ -346,21 +338,15 @@ export class ObjectGeometry /** * Checks if point is inside the object * @param {Point} point Point to check against - * @param {Object} [lines] object returned from util getImageLines * @param {Boolean} [absolute] use coordinates without viewportTransform * @param {Boolean} [calculate] use coordinates of current position instead of stored ones * @return {Boolean} true if point is inside the object */ - containsPoint( - point: Point, - lines?: TBBoxLines, - absolute = false, - calculate = false - ): boolean { - const coords = this._getCoords(absolute, calculate), - xPoints = findCrossPoints(point, lines || getImageLines(coords)); - // if xPoints is odd then point is inside the object - return xPoints !== 0 && xPoints % 2 === 1; + containsPoint(point: Point, absolute = false, calculate = false): boolean { + return cornerPointContainsPoint( + point, + this._getCoords(absolute, calculate) + ); } /** @@ -410,7 +396,7 @@ export class ObjectGeometry ): boolean { // worst case scenario the object is so big that contains the screen const centerPoint = pointTL.midPointFrom(pointBR); - return this.containsPoint(centerPoint, undefined, true, calculate); + return this.containsPoint(centerPoint, true, calculate); } /** diff --git a/src/util/intersection/findCrossPoint.ts b/src/util/intersection/findCrossPoint.ts index d4942740f7d..e1aef57e412 100644 --- a/src/util/intersection/findCrossPoint.ts +++ b/src/util/intersection/findCrossPoint.ts @@ -21,7 +21,7 @@ export type TBBoxLines = { * @param {Object} lines Coordinates of the object being evaluated * @return {number} number of crossPoint */ -export const findCrossPoints = (point: XY, lines: TBBoxLines): number => { +const findCrossPoints = (point: XY, lines: TBBoxLines): number => { let xcount = 0; for (const lineKey in lines) { @@ -66,7 +66,7 @@ export const findCrossPoints = (point: XY, lines: TBBoxLines): number => { * @private * @param {Object} lineCoords or aCoords Coordinates of the object corners */ -export const getImageLines = ({ tl, tr, bl, br }: TCornerPoint): TBBoxLines => { +const getImageLines = ({ tl, tr, bl, br }: TCornerPoint): TBBoxLines => { const lines = { topline: { o: tl, From fc7a0302d1f24f33160fd2d861a630302f3e3911 Mon Sep 17 00:00:00 2001 From: Shachar <34343793+ShaMan123@users.noreply.github.com> Date: Mon, 25 Sep 2023 02:14:41 +0530 Subject: [PATCH 03/12] chore(): cleanup logs and error messages (#9369) --- .eslintrc.js | 9 +++++++++ CHANGELOG.md | 1 + src/ClassRegistry.ts | 4 +++- src/Pattern/Pattern.ts | 3 ++- .../DOMManagers/StaticCanvasDOMManager.ts | 7 +++---- src/canvas/StaticCanvas.ts | 10 +++++----- src/filters/BaseFilter.ts | 14 ++++++++------ src/filters/GLProbes/WebGLProbe.ts | 3 ++- src/parser/parseSVGDocument.ts | 3 ++- src/shapes/Group.ts | 15 +++++++-------- src/shapes/Image.ts | 3 ++- src/shapes/Object/Object.ts | 3 ++- src/util/internals/console.ts | 18 ++++++++++++++++++ src/util/internals/dom_request.ts | 3 ++- src/util/misc/dom.ts | 3 ++- src/util/misc/objectEnlive.ts | 5 +++-- src/util/misc/planeChange.ts | 5 +++-- test/unit/class_registry.js | 4 ++-- 18 files changed, 76 insertions(+), 37 deletions(-) create mode 100644 src/util/internals/console.ts diff --git a/.eslintrc.js b/.eslintrc.js index c48b030f98f..69893815f67 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -36,6 +36,7 @@ module.exports = { ], 'no-restricted-syntax': [ 'error', + // explore how to define the selector: https://astexplorer.net/ { selector: '[callee.object.name="Math"][callee.property.name="hypot"]', message: @@ -46,6 +47,14 @@ module.exports = { message: 'Aliasing or destructing `Math` is not allowed due to restrictions on `Math.hypot` usage.', }, + { + selector: '[callee.object.name="console"]', + message: 'Use the `log` util', + }, + { + selector: 'NewExpression[callee.name="Error"]', + message: 'Use `FabricError`', + }, ], }, }; diff --git a/CHANGELOG.md b/CHANGELOG.md index 618ba972c72..98b2a5a9054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- chore(): cleanup logs and error messages [#9369](https://github.com/fabricjs/fabric.js/pull/9369) - feature(Object) BREAKING: Remove lines parameter from object.containsPoint [#9375](https://github.com/fabricjs/fabric.js/pull/9375) - patch(Control): move hit detection to shouldActivate [#9374](https://github.com/fabricjs/fabric.js/pull/9374) - fix(StaticCanvas): disposing animations [#9361](https://github.com/fabricjs/fabric.js/pull/9361) diff --git a/src/ClassRegistry.ts b/src/ClassRegistry.ts index 568502d9726..67835cd76d2 100644 --- a/src/ClassRegistry.ts +++ b/src/ClassRegistry.ts @@ -1,3 +1,5 @@ +import { FabricError } from './util/internals/console'; + /* * This Map connects the objects type value with their * class implementation. It used from any object to understand which are @@ -25,7 +27,7 @@ export class ClassRegistry { getClass(classType: string): any { const constructor = this[JSON].get(classType); if (!constructor) { - throw new Error(`No class registered for ${classType}`); + throw new FabricError(`No class registered for ${classType}`); } return constructor; } diff --git a/src/Pattern/Pattern.ts b/src/Pattern/Pattern.ts index bbfeefb2e00..88540b8796c 100644 --- a/src/Pattern/Pattern.ts +++ b/src/Pattern/Pattern.ts @@ -11,6 +11,7 @@ import type { PatternOptions, SerializedPatternOptions, } from './types'; +import { log } from '../util/internals/console'; /** * @see {@link http://fabricjs.com/patterns demo} @@ -32,7 +33,7 @@ export class Pattern { } set type(value) { - console.warn('Setting type has no effect', value); + log('warn', 'Setting type has no effect', value); } /** diff --git a/src/canvas/DOMManagers/StaticCanvasDOMManager.ts b/src/canvas/DOMManagers/StaticCanvasDOMManager.ts index b85c7289b77..8c30481c757 100644 --- a/src/canvas/DOMManagers/StaticCanvasDOMManager.ts +++ b/src/canvas/DOMManagers/StaticCanvasDOMManager.ts @@ -4,6 +4,7 @@ import type { CSSDimensions } from './util'; import { setCSSDimensions, getElementOffset } from './util'; import { createCanvasElement, isHTMLCanvas } from '../../util/misc/dom'; import { setCanvasDimensions } from './util'; +import { FabricError } from '../../util/internals/console'; export type CanvasItem = { el: HTMLCanvasElement; @@ -33,11 +34,9 @@ export class StaticCanvasDOMManager { (getFabricDocument().getElementById(arg0) as HTMLCanvasElement)) || createCanvasElement(); if (el.hasAttribute('data-fabric')) { - /* _DEV_MODE_START_ */ - throw new Error( - 'fabric.js: trying to initialize a canvas that has already been initialized' + throw new FabricError( + 'Trying to initialize a canvas that has already been initialized. Did you forget to dispose the canvas?' ); - /* _DEV_MODE_END_ */ } this._originalCanvasStyle = el.style.cssText; el.setAttribute('data-fabric', 'main'); diff --git a/src/canvas/StaticCanvas.ts b/src/canvas/StaticCanvas.ts index 7d642a740cb..6c28da9f1a4 100644 --- a/src/canvas/StaticCanvas.ts +++ b/src/canvas/StaticCanvas.ts @@ -47,6 +47,7 @@ import type { CSSDimensions } from './DOMManagers/util'; import type { FabricObject } from '../shapes/Object/FabricObject'; import type { StaticCanvasOptions } from './StaticCanvasOptions'; import { staticCanvasDefaults } from './StaticCanvasOptions'; +import { log, FabricError } from '../util/internals/console'; export type TCanvasSizeOptions = { backstoreOnly?: boolean; @@ -214,12 +215,11 @@ export class StaticCanvas< _onObjectAdded(obj: FabricObject) { if (obj.canvas && (obj.canvas as StaticCanvas) !== this) { - /* _DEV_MODE_START_ */ - console.warn( - 'fabric.Canvas: trying to add an object that belongs to a different canvas.\n' + + log( + 'warn', + 'Canvas is trying to add an object that belongs to a different canvas.\n' + 'Resulting to default behavior: removing object from previous canvas and adding to new canvas' ); - /* _DEV_MODE_END_ */ obj.canvas.remove(obj); } obj._set('canvas', this); @@ -1267,7 +1267,7 @@ export class StaticCanvas< { signal }: Abortable = {} ): Promise { if (!json) { - return Promise.reject(new Error('fabric.js: `json` is undefined')); + return Promise.reject(new FabricError('`json` is undefined')); } // parse json if it wasn't already diff --git a/src/filters/BaseFilter.ts b/src/filters/BaseFilter.ts index 7f06174d28b..68ecb462c88 100644 --- a/src/filters/BaseFilter.ts +++ b/src/filters/BaseFilter.ts @@ -14,6 +14,7 @@ import { vertexSource, } from './shaders/baseFilter'; import type { Abortable } from '../typedefs'; +import { FabricError } from '../util/internals/console'; export class BaseFilter { /** @@ -92,12 +93,14 @@ export class BaseFilter { const program = gl.createProgram(); if (!vertexShader || !fragmentShader || !program) { - throw new Error('Vertex, fragment shader or program creation error'); + throw new FabricError( + 'Vertex, fragment shader or program creation error' + ); } gl.shaderSource(vertexShader, vertexSource); gl.compileShader(vertexShader); if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { - throw new Error( + throw new FabricError( `Vertex shader compile error for ${this.type}: ${gl.getShaderInfoLog( vertexShader )}` @@ -107,7 +110,7 @@ export class BaseFilter { gl.shaderSource(fragmentShader, fragmentSource); gl.compileShader(fragmentShader); if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { - throw new Error( + throw new FabricError( `Fragment shader compile error for ${this.type}: ${gl.getShaderInfoLog( fragmentShader )}` @@ -118,9 +121,8 @@ export class BaseFilter { gl.attachShader(program, fragmentShader); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { - throw new Error( - // eslint-disable-next-line prefer-template - 'Shader link error for "${this.type}" ' + gl.getProgramInfoLog(program) + throw new FabricError( + `Shader link error for "${this.type}" ${gl.getProgramInfoLog(program)}` ); } diff --git a/src/filters/GLProbes/WebGLProbe.ts b/src/filters/GLProbes/WebGLProbe.ts index c2526c10edd..c02cac2dec8 100644 --- a/src/filters/GLProbes/WebGLProbe.ts +++ b/src/filters/GLProbes/WebGLProbe.ts @@ -1,3 +1,4 @@ +import { log } from '../../util/internals/console'; import { GLProbe } from './GLProbe'; import type { GLPrecision } from './GLProbe'; @@ -37,7 +38,7 @@ export class WebGLProbe extends GLProbe { this.GLPrecision = (['highp', 'mediump', 'lowp'] as const).find( (precision) => this.testPrecision(gl, precision) ); - console.log(`fabric: max texture size ${this.maxTextureSize}`); + log('log', `WebGL: max texture size ${this.maxTextureSize}`); } } diff --git a/src/parser/parseSVGDocument.ts b/src/parser/parseSVGDocument.ts index 71d917f1a01..c28748383d4 100644 --- a/src/parser/parseSVGDocument.ts +++ b/src/parser/parseSVGDocument.ts @@ -5,6 +5,7 @@ import { parseUseDirectives } from './parseUseDirectives'; import type { SVGParsingOutput, TSvgReviverCallback } from './typedefs'; import type { LoadImageOptions } from '../util/misc/objectEnlive'; import { ElementsParser } from './elements_parser'; +import { log, SignalAbortedError } from '../util/internals/console'; const isValidSvgTag = (el: Element) => svgValidTagNamesRegEx.test(el.nodeName.replace('svg:', '')); @@ -39,7 +40,7 @@ export async function parseSVGDocument( { crossOrigin, signal }: LoadImageOptions = {} ): Promise { if (signal && signal.aborted) { - console.log('`options.signal` is in `aborted` state'); + log('log', new SignalAbortedError('parseSVGDocument')); // this is an unhappy path, we dont care about speed return createEmptyResponse(); } diff --git a/src/shapes/Group.ts b/src/shapes/Group.ts index 6bfac5f03c9..15ae6d704d1 100644 --- a/src/shapes/Group.ts +++ b/src/shapes/Group.ts @@ -22,6 +22,7 @@ import { Rect } from './Rect'; import { classRegistry } from '../ClassRegistry'; import type { FabricObjectProps, SerializedObjectProps } from './Object/types'; import { CENTER } from '../constants'; +import { log } from '../util/internals/console'; export type LayoutContextType = | 'initialization' @@ -204,19 +205,17 @@ export class Group extends createCollectionMixin( canEnterGroup(object: FabricObject) { if (object === this || this.isDescendantOf(object)) { // prevent circular object tree - /* _DEV_MODE_START_ */ - console.error( - 'fabric.Group: circular object trees are not supported, this call has no effect' + log( + 'error', + 'Group: circular object trees are not supported, this call has no effect' ); - /* _DEV_MODE_END_ */ return false; } else if (this._objects.indexOf(object) !== -1) { // is already in the objects array - /* _DEV_MODE_START_ */ - console.error( - 'fabric.Group: duplicate objects are not supported inside group, this call has no effect' + log( + 'error', + 'Group: duplicate objects are not supported inside group, this call has no effect' ); - /* _DEV_MODE_END_ */ return false; } return true; diff --git a/src/shapes/Image.ts b/src/shapes/Image.ts index 9457b016456..7af94291f9a 100644 --- a/src/shapes/Image.ts +++ b/src/shapes/Image.ts @@ -30,6 +30,7 @@ import { getDocumentFromElement } from '../util/dom_misc'; import type { CSSRules } from '../parser/typedefs'; import type { Resize } from '../filters/Resize'; import type { TCachedFabricObject } from './Object/Object'; +import { log } from '../util/internals/console'; // @todo Would be nice to have filtering code not imported directly. @@ -850,7 +851,7 @@ export class Image< options, parsedAttributes ).catch((err) => { - console.log(err); + log('log', 'Unable to parse Image', err); return null; }); } diff --git a/src/shapes/Object/Object.ts b/src/shapes/Object/Object.ts index 8571463e00e..760874f0e04 100644 --- a/src/shapes/Object/Object.ts +++ b/src/shapes/Object/Object.ts @@ -53,6 +53,7 @@ import type { Canvas } from '../../canvas/Canvas'; import type { SerializedObjectProps } from './types/SerializedObjectProps'; import type { ObjectProps } from './types/ObjectProps'; import { getEnv } from '../../env'; +import { log } from '../../util/internals/console'; export type TCachedFabricObject = T & Required< @@ -295,7 +296,7 @@ export class FabricObject< } set type(value) { - console.warn('Setting type has no effect', value); + log('warn', 'Setting type has no effect', value); } /** diff --git a/src/util/internals/console.ts b/src/util/internals/console.ts new file mode 100644 index 00000000000..fa56a99b6b8 --- /dev/null +++ b/src/util/internals/console.ts @@ -0,0 +1,18 @@ +export const log = ( + severity: 'log' | 'warn' | 'error', + ...optionalParams: any[] +) => + // eslint-disable-next-line no-restricted-syntax + console[severity]('fabric', ...optionalParams); + +export class FabricError extends Error { + constructor(message?: string, options?: ErrorOptions) { + super(`fabric: ${message}`, options); + } +} + +export class SignalAbortedError extends FabricError { + constructor(context: string) { + super(`${context} 'options.signal' is in 'aborted' state`); + } +} diff --git a/src/util/internals/dom_request.ts b/src/util/internals/dom_request.ts index f1cc9ed586f..5b6b65c9b37 100644 --- a/src/util/internals/dom_request.ts +++ b/src/util/internals/dom_request.ts @@ -1,6 +1,7 @@ import { getFabricWindow } from '../../env'; import { noop } from '../../constants'; import type { Abortable } from '../../typedefs'; +import { SignalAbortedError } from './console'; type requestOptions = Abortable & { onComplete?: (xhr: XMLHttpRequest) => void; @@ -29,7 +30,7 @@ export function request(url: string, options: requestOptions = {}) { }; if (signal && signal.aborted) { - throw new Error('`options.signal` is in `aborted` state'); + throw new SignalAbortedError('request'); } else if (signal) { signal.addEventListener('abort', abort, { once: true }); } diff --git a/src/util/misc/dom.ts b/src/util/misc/dom.ts index a1f00247dce..02eecfc6df3 100644 --- a/src/util/misc/dom.ts +++ b/src/util/misc/dom.ts @@ -1,5 +1,6 @@ import { getFabricDocument } from '../../env'; import type { ImageFormat } from '../../typedefs'; +import { FabricError } from '../internals/console'; /** * Creates canvas element * @return {CanvasElement} initialized canvas element @@ -7,7 +8,7 @@ import type { ImageFormat } from '../../typedefs'; export const createCanvasElement = (): HTMLCanvasElement => { const element = getFabricDocument().createElement('canvas'); if (!element || typeof element.getContext === 'undefined') { - throw new Error('Failed to create `canvas` element'); + throw new FabricError('Failed to create `canvas` element'); } return element; }; diff --git a/src/util/misc/objectEnlive.ts b/src/util/misc/objectEnlive.ts index 2118a224c83..16b90498933 100644 --- a/src/util/misc/objectEnlive.ts +++ b/src/util/misc/objectEnlive.ts @@ -6,6 +6,7 @@ import { createImage } from './dom'; import { classRegistry } from '../../ClassRegistry'; import type { BaseFilter } from '../../filters/BaseFilter'; import type { FabricObject as BaseFabricObject } from '../../shapes/Object/Object'; +import { FabricError, SignalAbortedError } from '../internals/console'; export type LoadImageOptions = Abortable & { /** @@ -26,7 +27,7 @@ export const loadImage = ( ) => new Promise(function (resolve, reject) { if (signal && signal.aborted) { - return reject(new Error('`options.signal` is in `aborted` state')); + return reject(new SignalAbortedError('loadImage')); } const img = createImage(); let abort: EventListenerOrEventListenerObject; @@ -49,7 +50,7 @@ export const loadImage = ( img.onload = done; img.onerror = function () { abort && signal?.removeEventListener('abort', abort); - reject(new Error('Error loading ' + img.src)); + reject(new FabricError(`Error loading ${img.src}`)); }; crossOrigin && (img.crossOrigin = crossOrigin); img.src = url; diff --git a/src/util/misc/planeChange.ts b/src/util/misc/planeChange.ts index 946727b99ce..358092179ee 100644 --- a/src/util/misc/planeChange.ts +++ b/src/util/misc/planeChange.ts @@ -5,6 +5,7 @@ import type { TMat2D } from '../../typedefs'; import type { StaticCanvas } from '../../canvas/StaticCanvas'; import { invertTransform, multiplyTransformMatrices } from './matrix'; import { applyTransformToObject } from './objectTransforms'; +import { FabricError } from '../internals/console'; export type ObjectRelation = 'sibling' | 'child'; @@ -71,10 +72,10 @@ export const transformPointRelativeToCanvas = ( ): Point => { // is this still needed with TS? if (relationBefore !== 'child' && relationBefore !== 'sibling') { - throw new Error(`fabric.js: received bad argument ${relationBefore}`); + throw new FabricError(`received bad argument ${relationBefore}`); } if (relationAfter !== 'child' && relationAfter !== 'sibling') { - throw new Error(`fabric.js: received bad argument ${relationAfter}`); + throw new FabricError(`received bad argument ${relationAfter}`); } if (relationBefore === relationAfter) { return point; diff --git a/test/unit/class_registry.js b/test/unit/class_registry.js index a0aeb224aca..df8060b9f8d 100644 --- a/test/unit/class_registry.js +++ b/test/unit/class_registry.js @@ -3,7 +3,7 @@ QUnit.module('classRegistry'); QUnit.test('getClass throw when no class is registered', function (assert) { assert.ok(fabric.classRegistry, 'classRegistry is available'); - assert.throws(() => classRegistry.getClass('rect'), new Error(`No class registered for rect`), 'initially Rect is undefined'); + assert.throws(() => classRegistry.getClass('rect'), new Error(`fabric: No class registered for rect`), 'initially Rect is undefined'); }); QUnit.test('getClass will return specific class matched by name', function (assert) { class TestClass { @@ -12,7 +12,7 @@ classRegistry.setClass(TestClass); assert.equal(classRegistry.getClass('NonTestClass'), TestClass, 'resolves class correctly'); assert.equal(classRegistry.getClass('nontestclass'), TestClass, 'resolves class correctly to lower case'); - assert.throws(() => classRegistry.getClass('TestClass'), new Error(`No class registered for TestClass`), 'Does not resolve by class constructor name'); + assert.throws(() => classRegistry.getClass('TestClass'), new Error(`fabric: No class registered for TestClass`), 'Does not resolve by class constructor name'); }); QUnit.test('getClass will return specific class from custom type', function (assert) { class TestClass2 { From c38ed9b17f568b976b863963a6799d8b612b6c7d Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 24 Sep 2023 22:54:08 +0200 Subject: [PATCH 04/12] docs(): enable typedocs to run again, fixed config (#9356) --- CHANGELOG.md | 1 + jest.extend.ts | 4 ++- package-lock.json | 40 +++++++++++++------------- package.json | 4 +-- src/canvas/__tests__/eventData.test.ts | 2 +- src/shapes/ActiveSelection.spec.ts | 2 +- src/shapes/IText/IText.test.ts | 3 +- src/shapes/Object/Object.ts | 2 +- typedoc.config.json | 6 ++++ typedoc.json | 5 ++-- 10 files changed, 40 insertions(+), 29 deletions(-) create mode 100644 typedoc.config.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 98b2a5a9054..4c5266bdf26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- docs() enable typedocs to run again [#9356](https://github.com/fabricjs/fabric.js/pull/9356) - chore(): cleanup logs and error messages [#9369](https://github.com/fabricjs/fabric.js/pull/9369) - feature(Object) BREAKING: Remove lines parameter from object.containsPoint [#9375](https://github.com/fabricjs/fabric.js/pull/9375) - patch(Control): move hit detection to shouldActivate [#9374](https://github.com/fabricjs/fabric.js/pull/9374) diff --git a/jest.extend.ts b/jest.extend.ts index 9d63eec1fba..da4c9d4a575 100644 --- a/jest.extend.ts +++ b/jest.extend.ts @@ -14,7 +14,7 @@ type ObjectOptions = ExtendedOptions & { }; export const roundSnapshotOptions = { - cloneDeepWith: (value) => { + cloneDeepWith: (value: any) => { if (typeof value === 'number') { return Math.round(value); } @@ -23,6 +23,7 @@ export const roundSnapshotOptions = { expect.extend({ toMatchSnapshot( + this: any, received: any, propertiesOrHint?: ExtendedOptions, hint?: string @@ -43,6 +44,7 @@ expect.extend({ ); }, toMatchObjectSnapshot( + this: any, received: FabricObject | Record, { cloneDeepWith: customizer, diff --git a/package-lock.json b/package-lock.json index 0696690c3eb..4dce6f87d97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fabric", - "version": "6.0.0-beta11", + "version": "6.0.0-beta13", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "fabric", - "version": "6.0.0-beta11", + "version": "6.0.0-beta13", "license": "MIT", "devDependencies": { "@babel/cli": "^7.22.9", @@ -57,8 +57,8 @@ "source-map-support": "^0.5.21", "testem": "^3.8.0", "tslib": "^2.4.1", - "typedoc": "^0.24.8", - "typedoc-plugin-markdown": "^3.15.4", + "typedoc": "^0.25.1", + "typedoc-plugin-markdown": "3.16", "typescript": "^4.9.4", "v8-to-istanbul": "^9.1.0" }, @@ -11781,30 +11781,30 @@ } }, "node_modules/typedoc": { - "version": "0.24.8", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.24.8.tgz", - "integrity": "sha512-ahJ6Cpcvxwaxfu4KtjA8qZNqS43wYt6JL27wYiIgl1vd38WW/KWX11YuAeZhuz9v+ttrutSsgK+XO1CjL1kA3w==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.1.tgz", + "integrity": "sha512-c2ye3YUtGIadxN2O6YwPEXgrZcvhlZ6HlhWZ8jQRNzwLPn2ylhdGqdR8HbyDRyALP8J6lmSANILCkkIdNPFxqA==", "dev": true, "dependencies": { "lunr": "^2.3.9", "marked": "^4.3.0", - "minimatch": "^9.0.0", + "minimatch": "^9.0.3", "shiki": "^0.14.1" }, "bin": { "typedoc": "bin/typedoc" }, "engines": { - "node": ">= 14.14" + "node": ">= 16" }, "peerDependencies": { - "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x" + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x" } }, "node_modules/typedoc-plugin-markdown": { - "version": "3.15.4", - "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.15.4.tgz", - "integrity": "sha512-KpjFL/NDrQAbY147oIoOgob2vAdEchsMcTVd6+e6H2lC1l5xhi48bhP/fMJI7qYQ8th5nubervgqw51z7gY66A==", + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.16.0.tgz", + "integrity": "sha512-eeiC78fDNGFwemPIHiwRC+mEC7W5jwt3fceUev2gJ2nFnXpVHo8eRrpC9BLWZDee6ehnz/sPmNjizbXwpfaTBw==", "dev": true, "dependencies": { "handlebars": "^4.7.7" @@ -20360,14 +20360,14 @@ } }, "typedoc": { - "version": "0.24.8", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.24.8.tgz", - "integrity": "sha512-ahJ6Cpcvxwaxfu4KtjA8qZNqS43wYt6JL27wYiIgl1vd38WW/KWX11YuAeZhuz9v+ttrutSsgK+XO1CjL1kA3w==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.1.tgz", + "integrity": "sha512-c2ye3YUtGIadxN2O6YwPEXgrZcvhlZ6HlhWZ8jQRNzwLPn2ylhdGqdR8HbyDRyALP8J6lmSANILCkkIdNPFxqA==", "dev": true, "requires": { "lunr": "^2.3.9", "marked": "^4.3.0", - "minimatch": "^9.0.0", + "minimatch": "^9.0.3", "shiki": "^0.14.1" }, "dependencies": { @@ -20392,9 +20392,9 @@ } }, "typedoc-plugin-markdown": { - "version": "3.15.4", - "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.15.4.tgz", - "integrity": "sha512-KpjFL/NDrQAbY147oIoOgob2vAdEchsMcTVd6+e6H2lC1l5xhi48bhP/fMJI7qYQ8th5nubervgqw51z7gY66A==", + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.16.0.tgz", + "integrity": "sha512-eeiC78fDNGFwemPIHiwRC+mEC7W5jwt3fceUev2gJ2nFnXpVHo8eRrpC9BLWZDee6ehnz/sPmNjizbXwpfaTBw==", "dev": true, "requires": { "handlebars": "^4.7.7" diff --git a/package.json b/package.json index 3b36792a553..02a96ccdfec 100644 --- a/package.json +++ b/package.json @@ -123,8 +123,8 @@ "source-map-support": "^0.5.21", "testem": "^3.8.0", "tslib": "^2.4.1", - "typedoc": "^0.24.8", - "typedoc-plugin-markdown": "^3.15.4", + "typedoc": "^0.25.1", + "typedoc-plugin-markdown": "3.16", "typescript": "^4.9.4", "v8-to-istanbul": "^9.1.0" }, diff --git a/src/canvas/__tests__/eventData.test.ts b/src/canvas/__tests__/eventData.test.ts index e3976a2cde7..4b1ba7a1e8d 100644 --- a/src/canvas/__tests__/eventData.test.ts +++ b/src/canvas/__tests__/eventData.test.ts @@ -20,7 +20,7 @@ describe('Canvas event data', () => { }; beforeEach(() => { - canvas = new Canvas(null); + canvas = new Canvas(); spy = jest.spyOn(canvas, 'fire'); }); diff --git a/src/shapes/ActiveSelection.spec.ts b/src/shapes/ActiveSelection.spec.ts index c7f6e393404..bba71f69220 100644 --- a/src/shapes/ActiveSelection.spec.ts +++ b/src/shapes/ActiveSelection.spec.ts @@ -72,7 +72,7 @@ describe('ActiveSelection', () => { }); it('sets coords after attaching to canvas', () => { - const canvas = new Canvas(null, { + const canvas = new Canvas(undefined, { activeSelection: new ActiveSelection([ new FabricObject({ left: 100, diff --git a/src/shapes/IText/IText.test.ts b/src/shapes/IText/IText.test.ts index 6317dfa0bb5..56e2bc054ad 100644 --- a/src/shapes/IText/IText.test.ts +++ b/src/shapes/IText/IText.test.ts @@ -1,3 +1,4 @@ +import type { Canvas } from '../../canvas/Canvas'; import '../../../jest.extend'; import { Group } from '../Group'; import { IText } from './IText'; @@ -28,7 +29,7 @@ describe('IText', () => { const getZoom = jest.fn().mockReturnValue(zoom); const mockContext = { fillRect }; const mockCanvas = { contextTop: mockContext, getZoom }; - jest.replaceProperty(text, 'canvas', mockCanvas); + jest.replaceProperty(text, 'canvas', mockCanvas as unknown as Canvas); text.renderCursorAt(1); const call = fillRect.mock.calls[0]; diff --git a/src/shapes/Object/Object.ts b/src/shapes/Object/Object.ts index 760874f0e04..63ca977d222 100644 --- a/src/shapes/Object/Object.ts +++ b/src/shapes/Object/Object.ts @@ -513,7 +513,7 @@ export class FabricObject< * @param {string[]} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} Object representation of an instance */ - protected toObject(propertiesToInclude: any[] = []): any { + toObject(propertiesToInclude: any[] = []): any { const NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS, clipPathData = this.clipPath && !this.clipPath.excludeFromExport diff --git a/typedoc.config.json b/typedoc.config.json new file mode 100644 index 00000000000..773ff54f742 --- /dev/null +++ b/typedoc.config.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "./tsconfig.json", + "include": ["fabric.ts"], + "exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"] +} diff --git a/typedoc.json b/typedoc.json index 13d3b3a3ce8..e6420b1d40f 100644 --- a/typedoc.json +++ b/typedoc.json @@ -3,8 +3,9 @@ "$schema": "https://typedoc.org/schema.json", "entryPoints": ["fabric.ts"], "out": "docs", - "exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"], "excludeExternals": true, - "tsconfig": "tsconfig.json", + "tsconfig": "typedoc.config.json", + "hideBreadcrumbs": "true", + "publicPath": "/apidocs/", "plugin": ["typedoc-plugin-markdown"] } From 6f24521f49a5df1b7db5f5a285d592d4bf8b290b Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Tue, 26 Sep 2023 01:30:27 +0200 Subject: [PATCH 05/12] feat(Intersection) add implementation of pointInPolygon (#9381) --- CHANGELOG.md | 1 + package.json | 2 +- src/Intersection.spec.ts | 74 ++++++++++++ src/Intersection.ts | 30 +++++ src/benchmarks/README.md | 7 ++ .../raycasting.mjs} | 106 ++++++++++++------ src/controls/Control.ts | 6 +- src/shapes/Object/InteractiveObject.ts | 16 +-- src/shapes/Object/ObjectGeometry.ts | 7 +- test/unit/draggable_text.js | 12 +- test/unit/group.js | 14 +-- 11 files changed, 204 insertions(+), 71 deletions(-) create mode 100644 src/Intersection.spec.ts create mode 100644 src/benchmarks/README.md rename src/{util/intersection/findCrossPoint.ts => benchmarks/raycasting.mjs} (53%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c5266bdf26..b77c2b0484c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- patch(): dep findCrossPoints in favor of `isPointInPolygon` [#9374](https://github.com/fabricjs/fabric.js/pull/9374) - docs() enable typedocs to run again [#9356](https://github.com/fabricjs/fabric.js/pull/9356) - chore(): cleanup logs and error messages [#9369](https://github.com/fabricjs/fabric.js/pull/9369) - feature(Object) BREAKING: Remove lines parameter from object.containsPoint [#9375](https://github.com/fabricjs/fabric.js/pull/9375) diff --git a/package.json b/package.json index 02a96ccdfec..dbd886ecd07 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "test:jest": "jest", "test": "npm run cli -- test", "sandbox": "npm run sandboxscript -- sandbox", - "test:unit-browser": "npm run test -- -s unit -p 8080 -l -c chrome firefox", + "test:unit-browser": "npm run cli -- test --suite unit --launch --context chrome", "test:visual-browser": "npm run test -- -s visual -p 8081 -l -c chrome firefox", "test:coverage": "nyc --silent qunit test/node_test_setup.js test/lib test/unit", "test:visual:coverage": "nyc --silent --no-clean qunit test/node_test_setup.js test/lib test/visual", diff --git a/src/Intersection.spec.ts b/src/Intersection.spec.ts new file mode 100644 index 00000000000..73a894eb7da --- /dev/null +++ b/src/Intersection.spec.ts @@ -0,0 +1,74 @@ +import { Intersection } from './Intersection'; +import { Point } from './Point'; + +const polygonPoints = [ + new Point(4, 1), + new Point(6, 2), + new Point(4, 5), + new Point(6, 6), + new Point(10, 3), + new Point(11, 4), + new Point(7, 9), + new Point(1, 5), +]; + +describe('Intersection', () => { + describe('isPointInPolygon normal cases', () => { + describe('testing non coincident points', () => { + /** + * To visualize this test for easy understanding paste this svg in an online editor + + + + + + + + + + + + + + + + + + + + * sample: https://editsvgcode.com/ + */ + test.each([ + [new Point(0.5, 0.5), false], + [new Point(4.5, 0.5), false], + [new Point(9.5, 0.5), false], + [new Point(0.5, 2.5), false], + [new Point(4.5, 2.5), true], + [new Point(9.5, 2.5), false], + [new Point(0.5, 5.5), false], + [new Point(4.5, 5.5), true], + [new Point(8.5, 5.5), true], + [new Point(9.5, 5.5), true], + [new Point(10.5, 5.5), false], + [new Point(0.5, 8.5), false], + [new Point(4.5, 8.5), false], + [new Point(6.5, 8.5), true], + [new Point(9.5, 8.5), false], + ])('%p is in polygon %p, case index %#', (point, result) => { + expect(Intersection.isPointInPolygon(point, polygonPoints)).toBe( + result + ); + }); + }); + describe('testing coincident points', () => { + test.each([ + [new Point(4, 1), true], + [new Point(6, 2), true], + ])('%p is in polygon %p, case index %#', (point, result) => { + expect(Intersection.isPointInPolygon(point, polygonPoints)).toBe( + result + ); + }); + }); + }); +}); diff --git a/src/Intersection.ts b/src/Intersection.ts index 66d181ce2e3..143d444554d 100644 --- a/src/Intersection.ts +++ b/src/Intersection.ts @@ -81,6 +81,36 @@ export class Intersection { } } + /** + * Use the ray casting algorithm to determine if {@link point} is in the polygon defined by {@link points} + * @see https://en.wikipedia.org/wiki/Point_in_polygon + * @param point + * @param points polygon points + * @returns + */ + static isPointInPolygon(point: Point, points: Point[]) { + const other = new Point(point).setX( + Math.min(point.x - 1, ...points.map((p) => p.x)) + ); + let hits = 0; + for (let index = 0; index < points.length; index++) { + const inter = this.intersectSegmentSegment( + // polygon side + points[index], + points[(index + 1) % points.length], + // ray + point, + other + ); + if (inter.includes(point)) { + // point is on the polygon side + return true; + } + hits += Number(inter.status === 'Intersection'); + } + return hits % 2 === 1; + } + /** * Checks if a line intersects another * @see {@link https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection line intersection} diff --git a/src/benchmarks/README.md b/src/benchmarks/README.md new file mode 100644 index 00000000000..41b258f5f27 --- /dev/null +++ b/src/benchmarks/README.md @@ -0,0 +1,7 @@ +# Benchmarks + +This folder contains files with code we used to evaluate code changes and as a quick repository to test our changes again if necessary. + +This folder is in JS and import code from the build of Fabric so we can test an implementation that is more similar to the end result. + +NOTE: as of now the files for now are not supposed to run with a command, this folder is just a warehouse of test functions, possibly runnable witn `node filepath.mjs`` diff --git a/src/util/intersection/findCrossPoint.ts b/src/benchmarks/raycasting.mjs similarity index 53% rename from src/util/intersection/findCrossPoint.ts rename to src/benchmarks/raycasting.mjs index e1aef57e412..8d708d0abb9 100644 --- a/src/util/intersection/findCrossPoint.ts +++ b/src/benchmarks/raycasting.mjs @@ -1,17 +1,18 @@ -import type { XY } from '../../Point'; -import type { TCornerPoint } from '../../typedefs'; +import { Object as FabricObject, Point } from '../../dist/index.mjs'; -type TLineDescriptor = { - o: XY; - d: XY; -}; +// OLD CODE FOR REFERENCE AND IMPLEMENTATION TEST -export type TBBoxLines = { - topline: TLineDescriptor; - leftline: TLineDescriptor; - bottomline: TLineDescriptor; - rightline: TLineDescriptor; -}; +// type TLineDescriptor = { +// o: XY; +// d: XY; +// }; + +// type TBBoxLines = { +// topline: TLineDescriptor; +// leftline: TLineDescriptor; +// bottomline: TLineDescriptor; +// rightline: TLineDescriptor; +// }; /** * Helper method to determine how many cross points are between the 4 object edges @@ -21,12 +22,12 @@ export type TBBoxLines = { * @param {Object} lines Coordinates of the object being evaluated * @return {number} number of crossPoint */ -const findCrossPoints = (point: XY, lines: TBBoxLines): number => { +const findCrossPoints = (point, lines) => { let xcount = 0; for (const lineKey in lines) { let xi; - const iLine = lines[lineKey as keyof TBBoxLines]; + const iLine = lines[lineKey]; // optimization 1: line below point. no cross if (iLine.o.y < point.y && iLine.d.y < point.y) { continue; @@ -66,7 +67,7 @@ const findCrossPoints = (point: XY, lines: TBBoxLines): number => { * @private * @param {Object} lineCoords or aCoords Coordinates of the object corners */ -const getImageLines = ({ tl, tr, bl, br }: TCornerPoint): TBBoxLines => { +const getImageLines = ({ tl, tr, bl, br }) => { const lines = { topline: { o: tl, @@ -86,29 +87,66 @@ const getImageLines = ({ tl, tr, bl, br }: TCornerPoint): TBBoxLines => { }, }; - // // debugging - // if (this.canvas.contextTop) { - // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); - // } - return lines; }; -export const cornerPointContainsPoint = ( - point: XY, - cornerPoint: TCornerPoint -): boolean => { +export const cornerPointContainsPoint = (point, cornerPoint) => { const xPoints = findCrossPoints(point, getImageLines(cornerPoint)); // if xPoints is odd then point is inside the object return xPoints !== 0 && xPoints % 2 === 1; }; + +// END OF OLD CODE + +class Test1 extends FabricObject { + containsPoint(point, absolute, calculate) { + return cornerPointContainsPoint( + point, + this._getCoords(absolute, calculate) + ); + } +} + +const rect1 = new Test1({ + left: 10, + top: 10, + width: 10, + height: 10, + angle: 15.5, +}); + +const rect2 = new FabricObject({ + left: 10, + top: 10, + width: 10, + height: 10, + angle: 15.5, +}); + +const points = Array(1_000_000) + .fill(null) + .map((_) => new Point(Math.random() * 40, Math.random() * 40)); + +const benchmark = (callback) => { + const start = Date.now(); + callback(); + return Date.now() - start; +}; + +const benchmark1 = benchmark(() => { + const newPoints = points.map((point) => ({ x: point.x, y: point.y })); + newPoints.forEach((point) => rect1.containsPoint(point)); +}); + +const benchmark2 = benchmark(() => { + const newPoints = points.map((point) => new Point(point.x, point.y)); + newPoints.forEach((point) => rect2.containsPoint(point)); +}); + +// eslint-disable-next-line no-restricted-syntax +console.log({ + benchmark1, + benchmark2, + bench1_run: benchmark1 / points.length, + bench2_run: benchmark2 / points.length, +}); diff --git a/src/controls/Control.ts b/src/controls/Control.ts index e9e2d24024c..21c010b8881 100644 --- a/src/controls/Control.ts +++ b/src/controls/Control.ts @@ -5,10 +5,10 @@ import type { TPointerEvent, TransformActionHandler, } from '../EventTypeDefs'; +import { Intersection } from '../Intersection'; import { Point } from '../Point'; import type { InteractiveFabricObject } from '../shapes/Object/InteractiveObject'; import type { TCornerPoint, TDegree, TMat2D } from '../typedefs'; -import { cornerPointContainsPoint } from '../util/intersection/findCrossPoint'; import { cos } from '../util/misc/cos'; import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; import { sin } from '../util/misc/sin'; @@ -174,13 +174,13 @@ export class Control { controlKey: string, fabricObject: InteractiveFabricObject, pointer: Point, - cornerPoint: TCornerPoint + { tl, tr, br, bl }: TCornerPoint ) { // TODO: locking logic can be handled here instead of in the control handler logic return ( fabricObject.canvas?.getActiveObject() === fabricObject && fabricObject.isControlVisible(controlKey) && - cornerPointContainsPoint(pointer, cornerPoint) + Intersection.isPointInPolygon(pointer, [tl, tr, br, bl]) ); } diff --git a/src/shapes/Object/InteractiveObject.ts b/src/shapes/Object/InteractiveObject.ts index d027559cd9c..39f90506e68 100644 --- a/src/shapes/Object/InteractiveObject.ts +++ b/src/shapes/Object/InteractiveObject.ts @@ -194,7 +194,6 @@ export class InteractiveFabricObject< } this.__corner = undefined; - // had to keep the reverse loop because was breaking tests const cornerEntries = Object.entries(this.oCoords); for (let i = cornerEntries.length - 1; i >= 0; i--) { const [key, corner] = cornerEntries[i]; @@ -209,21 +208,8 @@ export class InteractiveFabricObject< // this.canvas.contextTop.fillRect(pointer.x - 1, pointer.y - 1, 2, 2); return (this.__corner = key); } - - // // debugging needs rework - // - // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); } + return ''; } diff --git a/src/shapes/Object/ObjectGeometry.ts b/src/shapes/Object/ObjectGeometry.ts index efcf5cb83e3..b9b39fb866d 100644 --- a/src/shapes/Object/ObjectGeometry.ts +++ b/src/shapes/Object/ObjectGeometry.ts @@ -27,7 +27,6 @@ import type { StaticCanvas } from '../../canvas/StaticCanvas'; import { ObjectOrigin } from './ObjectOrigin'; import type { ObjectEvents } from '../../EventTypeDefs'; import type { ControlProps } from './types/ControlProps'; -import { cornerPointContainsPoint } from '../../util/intersection/findCrossPoint'; type TMatrixCache = { key: string; @@ -343,10 +342,8 @@ export class ObjectGeometry * @return {Boolean} true if point is inside the object */ containsPoint(point: Point, absolute = false, calculate = false): boolean { - return cornerPointContainsPoint( - point, - this._getCoords(absolute, calculate) - ); + const { tl, tr, br, bl } = this._getCoords(absolute, calculate); + return Intersection.isPointInPolygon(point, [tl, tr, br, bl]); } /** diff --git a/test/unit/draggable_text.js b/test/unit/draggable_text.js index 26d68e20103..bf96fc0d41a 100644 --- a/test/unit/draggable_text.js +++ b/test/unit/draggable_text.js @@ -363,7 +363,7 @@ function assertDragEventStream(name, a, b) { isClick: false, previousTarget: undefined }, - ...dragEvents.slice(0, 5).map(e => ({ + ...dragEvents.slice(0, 6).map(e => ({ e, target: iText2, type: 'dragover', @@ -373,22 +373,22 @@ function assertDragEventStream(name, a, b) { canDrop: true })), { - e: dragEvents[5], + e: dragEvents[6], target: iText2, type: 'dragleave', subTargets: [], dragSource: iText, dropTarget: undefined, canDrop: false, - pointer: new fabric.Point(220, 0), - absolutePointer: new fabric.Point(220, 0), + pointer: new fabric.Point(220, -5), + absolutePointer: new fabric.Point(220, -5), isClick: false, nextTarget: undefined }, ]); assert.deepEqual(renderEffects, [ - ...dragEvents.slice(0, 5).map(e => ({ e, source: iText, target: iText2 })), - ...dragEvents.slice(5).map(e => ({ e, source: iText, target: undefined })), + ...dragEvents.slice(0, 6).map(e => ({ e, source: iText, target: iText2 })), + ...dragEvents.slice(6).map(e => ({ e, source: iText, target: undefined })), ], 'render effects'); assert.equal(fabric.getFabricDocument().activeElement, iText.hiddenTextarea, 'should have focused hiddenTextarea'); }); diff --git a/test/unit/group.js b/test/unit/group.js index 4ccdb5eb0a1..2ca68bd8bda 100644 --- a/test/unit/group.js +++ b/test/unit/group.js @@ -330,19 +330,19 @@ assert.ok(typeof group.containsPoint === 'function'); - assert.ok(!group.containsPoint({ x: 0, y: 0 })); + assert.ok(!group.containsPoint(new fabric.Point( 0, 0 ))); group.scale(2); - assert.ok(group.containsPoint({ x: 50, y: 120 })); - assert.ok(group.containsPoint({ x: 100, y: 160 })); - assert.ok(!group.containsPoint({ x: 0, y: 0 })); + 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.padding = 30; group.setCoords(); - assert.ok(group.containsPoint({ x: 50, y: 120 })); - assert.ok(!group.containsPoint({ x: 100, y: 170 })); - assert.ok(!group.containsPoint({ x: 0, y: 0 })); + 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( 0, 0 ))); }); QUnit.test('forEachObject', function(assert) { From fc21b49f8ec2e194563a8807ae1159580d2dd9c7 Mon Sep 17 00:00:00 2001 From: Shachar <34343793+ShaMan123@users.noreply.github.com> Date: Tue, 26 Sep 2023 14:21:24 +0530 Subject: [PATCH 06/12] fix(Control): `calcCornerCoords` angle + calculation (#9377) --- CHANGELOG.md | 1 + e2e/tests/controls/hit-regions/index.spec.ts | 27 ++++ .../Control-hit-regions-1.png | Bin 0 -> 15066 bytes e2e/tests/controls/hit-regions/index.ts | 40 ++++++ e2e/utils/CanvasUtil.ts | 2 +- src/benchmarks/calcCornerCoords.mjs | 117 ++++++++++++++++++ src/benchmarks/raycasting.mjs | 2 + src/controls/Control.ts | 62 ++++------ src/shapes/Object/InteractiveObject.spec.ts | 40 ++++++ src/shapes/Object/InteractiveObject.ts | 7 +- 10 files changed, 255 insertions(+), 43 deletions(-) create mode 100644 e2e/tests/controls/hit-regions/index.spec.ts create mode 100644 e2e/tests/controls/hit-regions/index.spec.ts-snapshots/Control-hit-regions-1.png create mode 100644 e2e/tests/controls/hit-regions/index.ts create mode 100644 src/benchmarks/calcCornerCoords.mjs create mode 100644 src/shapes/Object/InteractiveObject.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b77c2b0484c..bd604fc6780 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- fix(Control): `calcCornerCoords` angle + calculation [#9377](https://github.com/fabricjs/fabric.js/pull/9377) - patch(): dep findCrossPoints in favor of `isPointInPolygon` [#9374](https://github.com/fabricjs/fabric.js/pull/9374) - docs() enable typedocs to run again [#9356](https://github.com/fabricjs/fabric.js/pull/9356) - chore(): cleanup logs and error messages [#9369](https://github.com/fabricjs/fabric.js/pull/9369) diff --git a/e2e/tests/controls/hit-regions/index.spec.ts b/e2e/tests/controls/hit-regions/index.spec.ts new file mode 100644 index 00000000000..fefa34e8874 --- /dev/null +++ b/e2e/tests/controls/hit-regions/index.spec.ts @@ -0,0 +1,27 @@ +import { expect, test } from '@playwright/test'; +import setup from '../../../setup'; +import { CanvasUtil } from '../../../utils/CanvasUtil'; + +setup(); + +test('Control hit regions', async ({ page }) => { + const canvasUtil = new CanvasUtil(page); + await canvasUtil.executeInBrowser((canvas) => { + const rect = canvas.getActiveObject(); + const render = ({ x, y }: fabric.Point, fill: string) => { + const ctx = canvas.getTopContext(); + ctx.fillStyle = fill; + ctx.beginPath(); + ctx.arc(x, y, 1, 0, Math.PI * 2); + ctx.fill(); + }; + for (let y = 0; y <= canvas.height; y++) { + for (let x = 0; x < canvas.width; x++) { + const point = new fabric.Point(x, y); + rect._findTargetCorner(point, true) && render(point, 'indigo'); + rect._findTargetCorner(point) && render(point, 'magenta'); + } + } + }); + expect(await new CanvasUtil(page).screenshot()).toMatchSnapshot(); +}); diff --git a/e2e/tests/controls/hit-regions/index.spec.ts-snapshots/Control-hit-regions-1.png b/e2e/tests/controls/hit-regions/index.spec.ts-snapshots/Control-hit-regions-1.png new file mode 100644 index 0000000000000000000000000000000000000000..da0f8d43f09dfc8ea4ab8813b156df0bf4d461fe GIT binary patch literal 15066 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 literal 0 HcmV?d00001 diff --git a/e2e/tests/controls/hit-regions/index.ts b/e2e/tests/controls/hit-regions/index.ts new file mode 100644 index 00000000000..727c063e281 --- /dev/null +++ b/e2e/tests/controls/hit-regions/index.ts @@ -0,0 +1,40 @@ +/** + * Runs in the **BROWSER** + * Imports are defined in 'e2e/imports.ts' + */ + +import * as fabric from 'fabric'; +import { beforeAll } from '../../test'; + +beforeAll((canvas) => { + canvas.setDimensions({ width: 300, height: 325 }); + const controls = fabric.controlsUtils.createObjectDefaultControls(); + Object.values(controls).forEach((control) => { + control.sizeX = 20; + control.sizeY = 25; + control.touchSizeX = 30; + control.touchSizeY = 35; + }); + const rect = new fabric.Rect({ + left: 25, + top: 60, + width: 75, + height: 100, + controls, + scaleY: 2, + fill: 'blue', + padding: 10, + }); + const group = new fabric.Group([rect], { + angle: 30, + scaleX: 2, + interactive: true, + subTargetCheck: true, + }); + canvas.add(group); + canvas.centerObject(group); + group.setCoords(); + canvas.setActiveObject(rect); + canvas.renderAll(); + return { rect, group }; +}); diff --git a/e2e/utils/CanvasUtil.ts b/e2e/utils/CanvasUtil.ts index d5a67672d05..3d2057682bc 100644 --- a/e2e/utils/CanvasUtil.ts +++ b/e2e/utils/CanvasUtil.ts @@ -33,7 +33,7 @@ export class CanvasUtil { async executeInBrowser( runInBrowser: (canvas: Canvas, context: C) => R, - context: C + context?: C ): Promise { return ( await this.page.evaluateHandle( diff --git a/src/benchmarks/calcCornerCoords.mjs b/src/benchmarks/calcCornerCoords.mjs new file mode 100644 index 00000000000..53533fdb588 --- /dev/null +++ b/src/benchmarks/calcCornerCoords.mjs @@ -0,0 +1,117 @@ +import { + Object as FabricObject, + Point, + util, + Control, +} from '../../dist/index.mjs'; + +// Swapping of calcCornerCoords in #9377 + +// OLD CODE FOR REFERENCE AND IMPLEMENTATION TEST + +const halfPI = Math.PI / 2; + +class OldControl extends Control { + calcCornerCoords( + objectAngle, + angle, + objectCornerSize, + centerX, + centerY, + isTouch + ) { + let cosHalfOffset, sinHalfOffset, cosHalfOffsetComp, sinHalfOffsetComp; + const xSize = isTouch ? this.touchSizeX : this.sizeX, + ySize = isTouch ? this.touchSizeY : this.sizeY; + if (xSize && ySize && xSize !== ySize) { + // handle rectangular corners + const controlTriangleAngle = Math.atan2(ySize, xSize); + const cornerHypotenuse = Math.sqrt(xSize * xSize + ySize * ySize) / 2; + const newTheta = + controlTriangleAngle - util.degreesToRadians(objectAngle); + const newThetaComp = + halfPI - controlTriangleAngle - util.degreesToRadians(objectAngle); + cosHalfOffset = cornerHypotenuse * util.cos(newTheta); + sinHalfOffset = cornerHypotenuse * util.sin(newTheta); + // use complementary angle for two corners + cosHalfOffsetComp = cornerHypotenuse * util.cos(newThetaComp); + sinHalfOffsetComp = cornerHypotenuse * util.sin(newThetaComp); + } else { + // handle square corners + // use default object corner size unless size is defined + const cornerSize = xSize && ySize ? xSize : objectCornerSize; + const cornerHypotenuse = cornerSize * Math.SQRT1_2; + // complementary angles are equal since they're both 45 degrees + const newTheta = util.degreesToRadians(45 - objectAngle); + cosHalfOffset = cosHalfOffsetComp = cornerHypotenuse * util.cos(newTheta); + sinHalfOffset = sinHalfOffsetComp = cornerHypotenuse * util.sin(newTheta); + } + + return { + tl: new Point(centerX - sinHalfOffsetComp, centerY - cosHalfOffsetComp), + tr: new Point(centerX + cosHalfOffset, centerY - sinHalfOffset), + bl: new Point(centerX - cosHalfOffset, centerY + sinHalfOffset), + br: new Point(centerX + sinHalfOffsetComp, centerY + cosHalfOffsetComp), + }; + } +} + +class OldObject extends FabricObject { + _calcCornerCoords(control, position) { + const corner = control.calcCornerCoords( + this.angle, + this.cornerSize, + position.x, + position.y, + false + ); + const touchCorner = control.calcCornerCoords( + this.angle, + this.touchCornerSize, + position.x, + position.y, + true + ); + return { corner, touchCorner }; + } +} + +// END OF OLD CODE + +const newObject = new FabricObject({ width: 100, height: 100 }); + +const oldObject = new OldObject({ width: 100, height: 100 }); + +newObject.controls = { + tl: new Control({ + x: -0.5, + y: -0.5, + }), +}; + +oldObject.controls = { + tl: new OldControl({ + x: -0.5, + y: -0.5, + }), +}; + +const benchmark = (callback) => { + const start = Date.now(); + callback(); + return Date.now() - start; +}; + +const controlNew = benchmark(() => { + for (let i = 0; i < 1_000_000; i++) { + newObject._calcCornerCoords(newObject.controls.tl, new Point(4.5, 4.5)); + } +}); + +const controlOld = benchmark(() => { + for (let i = 0; i < 1_000_000; i++) { + oldObject._calcCornerCoords(oldObject.controls.tl, new Point(4.5, 4.5)); + } +}); + +console.log({ controlOld, controlNew }); diff --git a/src/benchmarks/raycasting.mjs b/src/benchmarks/raycasting.mjs index 8d708d0abb9..2565e64acd2 100644 --- a/src/benchmarks/raycasting.mjs +++ b/src/benchmarks/raycasting.mjs @@ -1,5 +1,7 @@ import { Object as FabricObject, Point } from '../../dist/index.mjs'; +// SWAPPING OF RAY CASTING LOGIC IN #9381 + // OLD CODE FOR REFERENCE AND IMPLEMENTATION TEST // type TLineDescriptor = { diff --git a/src/controls/Control.ts b/src/controls/Control.ts index 21c010b8881..acc3b13c33d 100644 --- a/src/controls/Control.ts +++ b/src/controls/Control.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { halfPI } from '../constants'; import type { ControlActionHandler, TPointerEvent, @@ -9,9 +8,12 @@ import { Intersection } from '../Intersection'; import { Point } from '../Point'; import type { InteractiveFabricObject } from '../shapes/Object/InteractiveObject'; import type { TCornerPoint, TDegree, TMat2D } from '../typedefs'; -import { cos } from '../util/misc/cos'; -import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; -import { sin } from '../util/misc/sin'; +import { + createRotateMatrix, + createScaleMatrix, + createTranslateMatrix, + multiplyTransformMatrixArray, +} from '../util/misc/matrix'; import type { ControlRenderingStyleOverride } from './controlRendering'; import { renderCircleControl, renderSquareControl } from './controlRendering'; @@ -94,7 +96,7 @@ export class Control { * @type {?Number} * @default null */ - sizeX: number | null = null; + sizeX = 0; /** * Sets the height of the control. If null, defaults to object's cornerSize. @@ -102,7 +104,7 @@ export class Control { * @type {?Number} * @default null */ - sizeY: number | null = null; + sizeY = 0; /** * Sets the length of the touch area of the control. If null, defaults to object's touchCornerSize. @@ -110,7 +112,7 @@ export class Control { * @type {?Number} * @default null */ - touchSizeX: number | null = null; + touchSizeX = 0; /** * Sets the height of the touch area of the control. If null, defaults to object's touchCornerSize. @@ -118,7 +120,7 @@ export class Control { * @type {?Number} * @default null */ - touchSizeY: number | null = null; + touchSizeY = 0; /** * Css cursor style to display when the control is hovered. @@ -307,43 +309,25 @@ export class Control { * @param {boolean} isTouch true if touch corner, false if normal corner */ calcCornerCoords( - objectAngle: TDegree, + angle: TDegree, objectCornerSize: number, centerX: number, centerY: number, isTouch: boolean ) { - let cosHalfOffset, sinHalfOffset, cosHalfOffsetComp, sinHalfOffsetComp; - const xSize = isTouch ? this.touchSizeX : this.sizeX, - ySize = isTouch ? this.touchSizeY : this.sizeY; - if (xSize && ySize && xSize !== ySize) { - // handle rectangular corners - const controlTriangleAngle = Math.atan2(ySize, xSize); - const cornerHypotenuse = Math.sqrt(xSize * xSize + ySize * ySize) / 2; - const newTheta = controlTriangleAngle - degreesToRadians(objectAngle); - const newThetaComp = - halfPI - controlTriangleAngle - degreesToRadians(objectAngle); - cosHalfOffset = cornerHypotenuse * cos(newTheta); - sinHalfOffset = cornerHypotenuse * sin(newTheta); - // use complementary angle for two corners - cosHalfOffsetComp = cornerHypotenuse * cos(newThetaComp); - sinHalfOffsetComp = cornerHypotenuse * sin(newThetaComp); - } else { - // handle square corners - // use default object corner size unless size is defined - const cornerSize = xSize && ySize ? xSize : objectCornerSize; - const cornerHypotenuse = cornerSize * Math.SQRT1_2; - // complementary angles are equal since they're both 45 degrees - const newTheta = degreesToRadians(45 - objectAngle); - cosHalfOffset = cosHalfOffsetComp = cornerHypotenuse * cos(newTheta); - sinHalfOffset = sinHalfOffsetComp = cornerHypotenuse * sin(newTheta); - } - + const t = multiplyTransformMatrixArray([ + createTranslateMatrix(centerX, centerY), + createRotateMatrix({ angle }), + createScaleMatrix( + (isTouch ? this.touchSizeX : this.sizeX) || objectCornerSize, + (isTouch ? this.touchSizeY : this.sizeY) || objectCornerSize + ), + ]); return { - tl: new Point(centerX - sinHalfOffsetComp, centerY - cosHalfOffsetComp), - tr: new Point(centerX + cosHalfOffset, centerY - sinHalfOffset), - bl: new Point(centerX - cosHalfOffset, centerY + sinHalfOffset), - br: new Point(centerX + sinHalfOffsetComp, centerY + cosHalfOffsetComp), + tl: new Point(-0.5, -0.5).transform(t), + tr: new Point(0.5, -0.5).transform(t), + bl: new Point(-0.5, 0.5).transform(t), + br: new Point(0.5, 0.5).transform(t), }; } diff --git a/src/shapes/Object/InteractiveObject.spec.ts b/src/shapes/Object/InteractiveObject.spec.ts new file mode 100644 index 00000000000..65b8ae849b5 --- /dev/null +++ b/src/shapes/Object/InteractiveObject.spec.ts @@ -0,0 +1,40 @@ +import { radiansToDegrees } from '../../util'; +import { Group } from '../Group'; +import { FabricObject } from './FabricObject'; +import type { TOCoord } from './InteractiveObject'; + +describe('Object', () => { + describe('setCoords for objects inside group with rotation', () => { + it('all corners are rotated as much as the object total angle', () => { + const object = new FabricObject({ + left: 25, + top: 60, + width: 75, + height: 100, + angle: 10, + scaleY: 2, + fill: 'blue', + }); + const group = new Group([object], { + angle: 30, + scaleX: 2, + interactive: true, + subTargetCheck: true, + }); + group.setCoords(); + const objectAngle = Math.round(object.getTotalAngle()); + expect(objectAngle).toEqual(35); + Object.values(object.oCoords).forEach((cornerPoint: TOCoord) => { + const controlAngle = Math.round( + radiansToDegrees( + Math.atan2( + cornerPoint.corner.tr.y - cornerPoint.corner.tl.y, + cornerPoint.corner.tr.x - cornerPoint.corner.tl.x + ) + ) + ); + expect(controlAngle).toEqual(objectAngle); + }); + }); + }); +}); diff --git a/src/shapes/Object/InteractiveObject.ts b/src/shapes/Object/InteractiveObject.ts index 39f90506e68..9c3d160ad06 100644 --- a/src/shapes/Object/InteractiveObject.ts +++ b/src/shapes/Object/InteractiveObject.ts @@ -18,7 +18,7 @@ import type { FabricObjectProps } from './types/FabricObjectProps'; import type { TFabricObjectProps, SerializedObjectProps } from './types'; import { createObjectDefaultControls } from '../../controls/commonControls'; -type TOCoord = Point & { +export type TOCoord = Point & { corner: TCornerPoint; touchCorner: TCornerPoint; }; @@ -278,15 +278,16 @@ export class InteractiveFabricObject< * @private */ private _calcCornerCoords(control: Control, position: Point) { + const angle = this.getTotalAngle(); const corner = control.calcCornerCoords( - this.angle, + angle, this.cornerSize, position.x, position.y, false ); const touchCorner = control.calcCornerCoords( - this.angle, + angle, this.touchCornerSize, position.x, position.y, From 678b79c72ef4b2ffdffc82335d842d1e994302fb Mon Sep 17 00:00:00 2001 From: Jiayi Hu Date: Tue, 26 Sep 2023 11:00:18 +0200 Subject: [PATCH 07/12] fix(Canvas): Avoid firing twice when working with objects inside groups (#9329) --- CHANGELOG.md | 1 + src/canvas/Canvas.ts | 4 ++-- src/canvas/__tests__/eventData.test.ts | 22 ++++++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd604fc6780..5c5edb5295a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- fix(Canvas): avoid firing event twice when working with nested objects [#9329](https://github.com/fabricjs/fabric.js/pull/9329) - fix(Control): `calcCornerCoords` angle + calculation [#9377](https://github.com/fabricjs/fabric.js/pull/9377) - patch(): dep findCrossPoints in favor of `isPointInPolygon` [#9374](https://github.com/fabricjs/fabric.js/pull/9374) - docs() enable typedocs to run again [#9356](https://github.com/fabricjs/fabric.js/pull/9356) diff --git a/src/canvas/Canvas.ts b/src/canvas/Canvas.ts index 19693880b0f..27df5efd13f 100644 --- a/src/canvas/Canvas.ts +++ b/src/canvas/Canvas.ts @@ -903,7 +903,7 @@ export class Canvas extends SelectableCanvas implements CanvasOptions { this.fire(eventType, options); target && target.fire(eventType, options); for (let i = 0; i < subTargets.length; i++) { - subTargets[i].fire(eventType, options); + subTargets[i] !== target && subTargets[i].fire(eventType, options); } return options; } @@ -943,7 +943,7 @@ export class Canvas extends SelectableCanvas implements CanvasOptions { // this may be a little be more complicated of what we want to handle target && target.fire(`mouse${eventType}`, options); for (let i = 0; i < targets.length; i++) { - targets[i].fire(`mouse${eventType}`, options); + targets[i] !== target && targets[i].fire(`mouse${eventType}`, options); } } diff --git a/src/canvas/__tests__/eventData.test.ts b/src/canvas/__tests__/eventData.test.ts index 4b1ba7a1e8d..58c5d04a402 100644 --- a/src/canvas/__tests__/eventData.test.ts +++ b/src/canvas/__tests__/eventData.test.ts @@ -1,7 +1,10 @@ /* eslint-disable no-restricted-globals */ import '../../../jest.extend'; +import type { TPointerEvent } from '../../EventTypeDefs'; import { Point } from '../../Point'; +import { Group } from '../../shapes/Group'; import { IText } from '../../shapes/IText/IText'; +import { FabricObject } from '../../shapes/Object/FabricObject'; import type { TMat2D } from '../../typedefs'; import { Canvas } from '../Canvas'; @@ -100,3 +103,22 @@ describe('Canvas event data', () => { } ); }); + +it('A selected subtarget should not fire an event twice', () => { + const target = new FabricObject(); + const group = new Group([target], { + subTargetCheck: true, + interactive: true, + }); + const canvas = new Canvas(null); + canvas.add(group); + const targetSpy = jest.fn(); + target.on('mousedown', targetSpy); + jest.spyOn(canvas, '_checkTarget').mockReturnValue(true); + canvas.__onMouseDown({ + target: canvas.getSelectionElement(), + clientX: 0, + clientY: 0, + } as unknown as TPointerEvent); + expect(targetSpy).toHaveBeenCalledTimes(1); +}); From f7ff31d698479f29d6a4537dd4e959cac28cf946 Mon Sep 17 00:00:00 2001 From: Shachar <34343793+ShaMan123@users.noreply.github.com> Date: Tue, 26 Sep 2023 17:31:54 +0530 Subject: [PATCH 08/12] fix(ObjectGeometry): `containsPoint` - BREAKING changes to `Canvas#_checkTarget` signature (#9372) --- CHANGELOG.md | 3 ++ src/canvas/SelectableCanvas.ts | 48 ++++++++--------------------- src/shapes/Object/ObjectGeometry.ts | 18 +++++------ test/unit/canvas.js | 24 --------------- test/unit/object_interactivity.js | 10 +++--- 5 files changed, 27 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c5edb5295a..07117a26535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [next] +- fix(Geometry): `containsPoint` [#9372](https://github.com/fabricjs/fabric.js/pull/9372) + **BREAKING**: + - `Canvas#_checkTarget(point, object, pointFromViewport)` => `Canvas#_checkTarget(object, pointFromViewport)` - fix(Canvas): avoid firing event twice when working with nested objects [#9329](https://github.com/fabricjs/fabric.js/pull/9329) - fix(Control): `calcCornerCoords` angle + calculation [#9377](https://github.com/fabricjs/fabric.js/pull/9377) - patch(): dep findCrossPoints in favor of `isPointInPolygon` [#9374](https://github.com/fabricjs/fabric.js/pull/9374) diff --git a/src/canvas/SelectableCanvas.ts b/src/canvas/SelectableCanvas.ts index 5835ba41dd2..b68c8a1f537 100644 --- a/src/canvas/SelectableCanvas.ts +++ b/src/canvas/SelectableCanvas.ts @@ -17,7 +17,7 @@ import { import type { TCanvasSizeOptions } from './StaticCanvas'; import { StaticCanvas } from './StaticCanvas'; import { isCollection } from '../util/typeAssertions'; -import { invertTransform, transformPoint } from '../util/misc/matrix'; +import { invertTransform } from '../util/misc/matrix'; import { isTransparent } from '../util/misc/isTransparent'; import type { TMat2D, @@ -409,18 +409,6 @@ export class SelectableCanvas this.fire('after:render', { ctx }); } - /** - * Given a pointer on the canvas with a viewport applied, - * find out the pointer in object coordinates - * @private - */ - _normalizePointer(object: FabricObject, pointer: Point): Point { - return transformPoint( - this.restorePointerVpt(pointer), - invertTransform(object.calcTransformMatrix()) - ); - } - /** * Set the canvas tolerance value for pixel taret find. * Use only integer numbers. @@ -441,8 +429,8 @@ export class SelectableCanvas * @TODO this seems dumb that we treat controls with transparency. we can find controls * programmatically without painting them, the cache canvas optimization is always valid * @param {FabricObject} target Object to check - * @param {Number} x Left coordinate - * @param {Number} y Top coordinate + * @param {Number} x Left coordinate in viewport space + * @param {Number} y Top coordinate in viewport space * @return {Boolean} */ isTargetTransparent(target: FabricObject, x: number, y: number): boolean { @@ -762,30 +750,23 @@ export class SelectableCanvas /** * Checks point is inside the object. - * @param {Object} [pointer] x,y object of point coordinates we want to check. * @param {FabricObject} obj Object to test against - * @param {Object} [globalPointer] x,y object of point coordinates relative to canvas used to search per pixel target. + * @param {Object} [pointer] point from viewport. * @return {Boolean} true if point is contained within an area of given object * @private */ - _checkTarget( - pointer: Point, - obj: FabricObject, - globalPointer: Point - ): boolean { + _checkTarget(obj: FabricObject, pointer: Point): boolean { if ( obj && obj.visible && obj.evented && - // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html - // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html - obj.containsPoint(pointer) + obj.containsPoint(this.restorePointerVpt(pointer), true) ) { if ( (this.perPixelTargetFind || obj.perPixelTargetFind) && !(obj as unknown as IText).isEditing ) { - if (!this.isTargetTransparent(obj, globalPointer.x, globalPointer.y)) { + if (!this.isTargetTransparent(obj, pointer.x, pointer.y)) { return true; } } else { @@ -807,17 +788,12 @@ export class SelectableCanvas pointer: Point ): FabricObject | undefined { // Cache all targets where their bounding box contains point. - let target, - i = objects.length; + let i = objects.length; // Do not check for currently grouped objects, since we check the parent group itself. // until we call this function specifically to search inside the activeGroup while (i--) { - const objToCheck = objects[i]; - const pointerToUse = objToCheck.group - ? this._normalizePointer(objToCheck.group, pointer) - : pointer; - if (this._checkTarget(pointerToUse, objToCheck, pointer)) { - target = objects[i]; + const target = objects[i]; + if (this._checkTarget(target, pointer)) { if (isCollection(target) && target.subTargetCheck) { const subTarget = this._searchPossibleTargets( target._objects as FabricObject[], @@ -825,10 +801,9 @@ export class SelectableCanvas ); subTarget && this.targets.push(subTarget); } - break; + return target; } } - return target; } /** @@ -856,6 +831,7 @@ export class SelectableCanvas /** * Returns pointer coordinates without the effect of the viewport + * Takes a point in html canvas space and gives you back a point of the scene. * @param {Object} pointer with "x" and "y" number values in canvas HTML coordinates * @return {Object} object with "x" and "y" number values in fabricCanvas coordinates */ diff --git a/src/shapes/Object/ObjectGeometry.ts b/src/shapes/Object/ObjectGeometry.ts index b9b39fb866d..be1e68636a7 100644 --- a/src/shapes/Object/ObjectGeometry.ts +++ b/src/shapes/Object/ObjectGeometry.ts @@ -200,7 +200,7 @@ export class ObjectGeometry * that are attached to the object instance * @return {Object} {tl, tr, br, bl} points */ - _getCoords(absolute = false, calculate = false): TCornerPoint { + private _getCoords(absolute = false, calculate = false): TCornerPoint { if (calculate) { return absolute ? this.calcACoords() : this.calcLineCoords(); } @@ -293,14 +293,8 @@ export class ObjectGeometry calculate = false ): boolean { const points = this.getCoords(absolute, calculate); - for (let i = 0; i < 4; i++) { - // bug/confusing: this containsPoint should receive 'calculate' as well. - // will come later because it needs to come with tests - if (!other.containsPoint(points[i], absolute)) { - return false; - } - } - return true; + calculate && other.getCoords(absolute, true); + return points.every((point) => other.containsPoint(point)); } /** @@ -342,8 +336,10 @@ export class ObjectGeometry * @return {Boolean} true if point is inside the object */ containsPoint(point: Point, absolute = false, calculate = false): boolean { - const { tl, tr, br, bl } = this._getCoords(absolute, calculate); - return Intersection.isPointInPolygon(point, [tl, tr, br, bl]); + return Intersection.isPointInPolygon( + point, + this.getCoords(absolute, calculate) + ); } /** diff --git a/test/unit/canvas.js b/test/unit/canvas.js index 7a5d6008d87..14a268f1859 100644 --- a/test/unit/canvas.js +++ b/test/unit/canvas.js @@ -1588,30 +1588,6 @@ }); }); - - QUnit.test('normalize pointer', function(assert) { - assert.ok(typeof canvas._normalizePointer === 'function'); - var pointer = new fabric.Point({ x: 10, y: 20 }), - object = makeRect({ top: 10, left: 10, width: 50, height: 50, strokeWidth: 0}), - normalizedPointer = canvas._normalizePointer(object, pointer); - assert.equal(normalizedPointer.x, -25, 'should be in top left corner of rect'); - assert.equal(normalizedPointer.y, -15, 'should be in top left corner of rect'); - object.angle = 90; - normalizedPointer = canvas._normalizePointer(object, pointer); - assert.equal(normalizedPointer.x, -15, 'should consider angle'); - assert.equal(normalizedPointer.y, -25, 'should consider angle'); - object.angle = 0; - object.scaleX = 2; - object.scaleY = 2; - normalizedPointer = canvas._normalizePointer(object, pointer); - assert.equal(normalizedPointer.x, -25, 'should consider scale'); - assert.equal(normalizedPointer.y, -20, 'should consider scale'); - object.skewX = 60; - normalizedPointer = canvas._normalizePointer(object, pointer); - assert.equal(normalizedPointer.x.toFixed(2), -33.66, 'should consider skewX'); - assert.equal(normalizedPointer.y, -20, 'should not change'); - }); - QUnit.test('restorePointerVpt', function(assert) { assert.ok(typeof canvas.restorePointerVpt === 'function'); var pointer = new fabric.Point({ x: 10, y: 20 }), diff --git a/test/unit/object_interactivity.js b/test/unit/object_interactivity.js index a90632c3f99..c0426fcc9cb 100644 --- a/test/unit/object_interactivity.js +++ b/test/unit/object_interactivity.js @@ -216,7 +216,7 @@ assert.equal(cObj._findTargetCorner(cObj.oCoords.mt), 'mt'); assert.equal(cObj._findTargetCorner(cObj.oCoords.mb), 'mb'); assert.equal(cObj._findTargetCorner(cObj.oCoords.mtr), 'mtr'); - assert.equal(cObj._findTargetCorner({ x: 0, y: 0 }), false); + assert.equal(cObj._findTargetCorner(new fabric.Point()), false); }); QUnit.test('_findTargetCorner for touches', function(assert) { @@ -225,16 +225,16 @@ cObj.canvas = { getActiveObject() { return cObj } }; - var pointNearBr = { + var pointNearBr = new fabric.Point({ x: cObj.oCoords.br.x + cObj.cornerSize / 3, y: cObj.oCoords.br.y + cObj.cornerSize / 3 - }; + }); assert.equal(cObj._findTargetCorner(pointNearBr), 'br', 'cornerSize/3 near br returns br'); assert.equal(cObj._findTargetCorner(pointNearBr, true), 'br', 'touch event cornerSize/3 near br returns br'); - pointNearBr = { + pointNearBr = new fabric.Point({ x: cObj.oCoords.br.x + cObj.touchCornerSize / 3, y: cObj.oCoords.br.y + cObj.touchCornerSize / 3, - }; + }); assert.equal(cObj._findTargetCorner(pointNearBr, true), 'br', 'touch event touchCornerSize/3 near br returns br'); assert.equal(cObj._findTargetCorner(pointNearBr, false), false, 'not touch event touchCornerSize/3 near br returns false'); }); From 23430b187d2bc17540eafce17936c80958c274a5 Mon Sep 17 00:00:00 2001 From: zhe he Date: Tue, 26 Sep 2023 20:23:19 +0800 Subject: [PATCH 09/12] =?UTF-8?q?Force=20users=20to=20use=20FabricObject?= =?UTF-8?q?=20instead=20of=20Object=E3=80=81FabricImage=20instead=20of=20I?= =?UTF-8?q?mage=E3=80=81FabricText=20instead=20of=20Text=20(#9172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + fabric.ts | 10 +++-- src/filters/BlendImage.ts | 8 ++-- src/parser/elements_parser.ts | 4 +- src/shapes/IText/ITextBehavior.ts | 4 +- src/shapes/Image.ts | 34 +++++++++------- src/shapes/Object/FabricObject.ts | 8 ++++ src/shapes/Object/Object.ts | 6 +-- src/shapes/Text/StyledText.ts | 7 +++- src/shapes/Text/Text.spec.ts | 18 ++++----- src/shapes/Text/Text.ts | 39 ++++++++++++------- src/shapes/Text/TextSVGExportMixin.ts | 29 +++++++------- .../Text/__snapshots__/Text.spec.ts.snap | 12 +++--- src/shapes/Text/constants.ts | 4 +- src/util/transform_matrix_removal.ts | 6 +-- src/util/typeAssertions.ts | 4 +- test/unit/canvas_static.js | 4 +- test/unit/circle.js | 2 +- test/unit/class_registry.js | 2 +- test/unit/ellipse.js | 2 +- test/unit/image.js | 22 +++++------ test/unit/itext.js | 2 +- test/unit/line.js | 2 +- test/unit/object.js | 6 +-- test/unit/object_clipPath.js | 4 +- test/unit/path.js | 2 +- test/unit/polygon.js | 2 +- test/unit/polyline.js | 2 +- test/unit/rect.js | 2 +- test/unit/text.js | 8 ++-- test/unit/textbox.js | 2 +- 31 files changed, 147 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07117a26535..e2fe32a05a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- chore(): Rename exports that conflicts with JS/WEB api ( Object, Text, Image ). Kept backward compatibility with deprecation notice [#9172](https://github.com/fabricjs/fabric.js/pull/9172) - fix(Geometry): `containsPoint` [#9372](https://github.com/fabricjs/fabric.js/pull/9372) **BREAKING**: - `Canvas#_checkTarget(point, object, pointFromViewport)` => `Canvas#_checkTarget(object, pointFromViewport)` diff --git a/fabric.ts b/fabric.ts index 30298048c9f..71cbc74d1cb 100644 --- a/fabric.ts +++ b/fabric.ts @@ -41,7 +41,11 @@ export { CircleBrush } from './src/brushes/CircleBrush'; export { SprayBrush } from './src/brushes/SprayBrush'; export { PatternBrush } from './src/brushes/PatternBrush'; -export { FabricObject as Object } from './src/shapes/Object/FabricObject'; +export { + FabricObject, + _Object as Object, +} from './src/shapes/Object/FabricObject'; + export type { TFabricObjectProps, FabricObjectProps, @@ -71,7 +75,7 @@ export type { TPathSide, TextProps, } from './src/shapes/Text/Text'; -export { Text } from './src/shapes/Text/Text'; +export { Text, FabricText } from './src/shapes/Text/Text'; export type { ITextProps, SerializedITextProps, @@ -104,7 +108,7 @@ export type { MultiSelectionStacking, } from './src/shapes/ActiveSelection'; export { ActiveSelection } from './src/shapes/ActiveSelection'; -export { Image } from './src/shapes/Image'; +export { Image, FabricImage } from './src/shapes/Image'; export type { ImageSource, SerializedImageProps, diff --git a/src/filters/BlendImage.ts b/src/filters/BlendImage.ts index e5b5534d620..45384955212 100644 --- a/src/filters/BlendImage.ts +++ b/src/filters/BlendImage.ts @@ -1,4 +1,4 @@ -import { Image } from '../shapes/Image'; +import { FabricImage } from '../shapes/Image'; import type { TClassProperties } from '../typedefs'; import { createCanvasElement } from '../util/misc/dom'; import { BaseFilter } from './BaseFilter'; @@ -51,7 +51,7 @@ export class BlendImage extends BaseFilter { * Color to make the blend operation with. default to a reddish color since black or white * gives always strong result. **/ - declare image: Image; + declare image: FabricImage; declare mode: TBlendImageMode; @@ -81,7 +81,7 @@ export class BlendImage extends BaseFilter { this.unbindAdditionalTexture(gl, gl.TEXTURE1); } - createTexture(backend: WebGLFilterBackend, image: Image) { + createTexture(backend: WebGLFilterBackend, image: FabricImage) { return backend.getCachedTexture(image.cacheKey, image.getElement()); } @@ -220,7 +220,7 @@ export class BlendImage extends BaseFilter { { type, image, ...filterOptions }: Record, options: { signal: AbortSignal } ) { - return Image.fromObject(image, options).then( + return FabricImage.fromObject(image, options).then( (enlivedImage) => new this({ ...filterOptions, image: enlivedImage }) as BaseFilter ); diff --git a/src/parser/elements_parser.ts b/src/parser/elements_parser.ts index 0bb1fe266a6..9bc7080c3ab 100644 --- a/src/parser/elements_parser.ts +++ b/src/parser/elements_parser.ts @@ -1,6 +1,6 @@ import { Gradient } from '../gradient/Gradient'; import { Group } from '../shapes/Group'; -import { Image } from '../shapes/Image'; +import { FabricImage } from '../shapes/Image'; import { classRegistry } from '../ClassRegistry'; import { invertTransform, @@ -77,7 +77,7 @@ export class ElementsParser { ); this.resolveGradient(obj, el, 'fill'); this.resolveGradient(obj, el, 'stroke'); - if (obj instanceof Image && obj._originalElement) { + if (obj instanceof FabricImage && obj._originalElement) { removeTransformMatrixForSvgParsing( obj, obj.parsePreserveAspectRatioAttribute() diff --git a/src/shapes/IText/ITextBehavior.ts b/src/shapes/IText/ITextBehavior.ts index db1a37f4626..ef8901948db 100644 --- a/src/shapes/IText/ITextBehavior.ts +++ b/src/shapes/IText/ITextBehavior.ts @@ -5,7 +5,7 @@ import type { } from '../../EventTypeDefs'; import { Point } from '../../Point'; import type { FabricObject } from '../Object/FabricObject'; -import { Text } from '../Text/Text'; +import { FabricText } from '../Text/Text'; import { animate } from '../../util/animation/animate'; import type { TOnAnimationChangeCallback } from '../../util/animation/types'; import type { ValueAnimation } from '../../util/animation/ValueAnimation'; @@ -43,7 +43,7 @@ export abstract class ITextBehavior< Props extends TOptions = Partial, SProps extends SerializedTextProps = SerializedTextProps, EventSpec extends ITextEvents = ITextEvents -> extends Text { +> extends FabricText { declare abstract isEditing: boolean; declare abstract cursorDelay: number; declare abstract selectionStart: number; diff --git a/src/shapes/Image.ts b/src/shapes/Image.ts index 7af94291f9a..b059a6a4bb9 100644 --- a/src/shapes/Image.ts +++ b/src/shapes/Image.ts @@ -75,7 +75,7 @@ const IMAGE_PROPS = ['cropX', 'cropY'] as const; /** * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#images} */ -export class Image< +export class FabricImage< Props extends TOptions = Partial, SProps extends SerializedImageProps = SerializedImageProps, EventSpec extends ObjectEvents = ObjectEvents @@ -84,7 +84,7 @@ export class Image< implements ImageProps { /** - * When calling {@link Image.getSrc}, return value from element src with `element.getAttribute('src')`. + * When calling {@link FabricImage.getSrc}, return value from element src with `element.getAttribute('src')`. * This allows for relative urls as image src. * @since 2.7.0 * @type Boolean @@ -183,7 +183,7 @@ export class Image< static getDefaults() { return { ...super.getDefaults(), - ...Image.ownDefaults, + ...FabricImage.ownDefaults, }; } /** @@ -231,7 +231,7 @@ export class Image< this._element = element; this._originalElement = element; this._setWidthHeight(size); - element.classList.add(Image.CSS_CANVAS); + element.classList.add(FabricImage.CSS_CANVAS); if (this.filters.length !== 0) { this.applyFilters(); } @@ -599,7 +599,7 @@ export class Image< * @param {CanvasRenderingContext2D} ctx Context to render on */ drawCacheOnCanvas( - this: TCachedFabricObject, + this: TCachedFabricObject, ctx: CanvasRenderingContext2D ) { ctx.imageSmoothingEnabled = this.imageSmoothing; @@ -765,7 +765,7 @@ export class Image< static CSS_CANVAS = 'canvas-img'; /** - * List of attribute names to account for when parsing SVG element (used by {@link Image.fromElement}) + * List of attribute names to account for when parsing SVG element (used by {@link FabricImage.fromElement}) * @static * @see {@link http://www.w3.org/TR/SVG/struct.html#ImageElement} */ @@ -782,12 +782,12 @@ export class Image< ]; /** - * Creates an instance of Image from its object representation + * Creates an instance of FabricImage from its object representation * @static * @param {Object} object Object to create an instance from * @param {object} [options] Options object * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal - * @returns {Promise} + * @returns {Promise} */ static fromObject>( { filters: f, resizeFilter: rf, src, crossOrigin, ...object }: T, @@ -816,20 +816,20 @@ export class Image< * @static * @param {String} url URL to create an image from * @param {LoadImageOptions} [options] Options object - * @returns {Promise} + * @returns {Promise} */ static fromURL>( url: string, { crossOrigin = null, signal }: LoadImageOptions = {}, imageOptions: T - ): Promise { + ): Promise { return loadImage(url, { crossOrigin, signal }).then( (img) => new this(img, imageOptions) ); } /** - * Returns {@link Image} instance from an SVG element + * Returns {@link FabricImage} instance from an SVG element * @static * @param {HTMLElement} element Element to parse * @param {Object} [options] Options object @@ -857,5 +857,13 @@ export class Image< } } -classRegistry.setClass(Image); -classRegistry.setSVGClass(Image); +classRegistry.setClass(FabricImage); +classRegistry.setSVGClass(FabricImage); + +/** + * @deprecated The old fabric.Image class can't be imported as Image because of conflict with Web API. + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/Image + * For this reason it has been renamed to FabricImage. + * Please use `import { FabricImage }` in place of `import { Image as FabricImage }` + */ +export class Image extends FabricImage {} diff --git a/src/shapes/Object/FabricObject.ts b/src/shapes/Object/FabricObject.ts index 75ac06478a1..648652edda7 100644 --- a/src/shapes/Object/FabricObject.ts +++ b/src/shapes/Object/FabricObject.ts @@ -27,3 +27,11 @@ classRegistry.setClass(FabricObject); classRegistry.setClass(FabricObject, 'object'); export { cacheProperties } from './defaultValues'; + +/** + * @deprecated The old fabric.Object class can't be imported as Object because of conflict with the JS api + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object + * For this reason it has been renamed to FabricObject. + * Please use `import { FabricObject }` in place of `import { Object as FabricObject }` + */ +export class _Object extends FabricObject {} diff --git a/src/shapes/Object/Object.ts b/src/shapes/Object/Object.ts index 63ca977d222..d7d39027fc6 100644 --- a/src/shapes/Object/Object.ts +++ b/src/shapes/Object/Object.ts @@ -41,7 +41,7 @@ import { isSerializableFiller, isTextObject, } from '../../util/typeAssertions'; -import type { Image } from '../Image'; +import type { FabricImage } from '../Image'; import { cacheProperties, fabricObjectDefaultValues, @@ -1333,9 +1333,9 @@ export class FabricObject< * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 - * @return {Image} Object cloned as image. + * @return {FabricImage} Object cloned as image. */ - cloneAsImage(options: any): Image { + cloneAsImage(options: any): FabricImage { const canvasEl = this.toCanvasElement(options); // TODO: how to import Image w/o an import cycle? const ImageClass = classRegistry.getClass('image'); diff --git a/src/shapes/Text/StyledText.ts b/src/shapes/Text/StyledText.ts index 5a73472a801..04fa73b06ab 100644 --- a/src/shapes/Text/StyledText.ts +++ b/src/shapes/Text/StyledText.ts @@ -4,10 +4,13 @@ import type { TOptions } from '../../typedefs'; import { FabricObject } from '../Object/FabricObject'; import { styleProperties } from './constants'; import type { StylePropertiesType } from './constants'; -import type { Text } from './Text'; +import type { FabricText } from './Text'; import { pick } from '../../util'; -export type CompleteTextStyleDeclaration = Pick; +export type CompleteTextStyleDeclaration = Pick< + FabricText, + StylePropertiesType +>; export type TextStyleDeclaration = Partial; diff --git a/src/shapes/Text/Text.spec.ts b/src/shapes/Text/Text.spec.ts index b44293f8409..fe3e1f264a9 100644 --- a/src/shapes/Text/Text.spec.ts +++ b/src/shapes/Text/Text.spec.ts @@ -1,20 +1,20 @@ import { roundSnapshotOptions } from '../../../jest.extend'; import { cache } from '../../cache'; import { config } from '../../config'; -import { Text } from './Text'; +import { FabricText } from './Text'; afterEach(() => { config.restoreDefaults(); }); -describe('Text', () => { +describe('FabricText', () => { it('toObject', async () => { - expect(new Text('text')).toMatchObjectSnapshot(); + expect(new FabricText('text').toObject()).toMatchObjectSnapshot(); }); it('fromObject', async () => { - expect((await Text.fromObject({ text: 'text' })).toObject()).toEqual( - new Text('text').toObject() + expect((await FabricText.fromObject({ text: 'text' })).toObject()).toEqual( + new FabricText('text').toObject() ); }); @@ -22,7 +22,7 @@ describe('Text', () => { it('measuring', () => { cache.clearFontCache(); const zwc = '\u200b'; - const text = new Text(''); + const text = new FabricText(''); const style = text.getCompleteStyleDeclaration(0, 0); const measurement = text._measureChar('a', style, zwc, style); expect(measurement).toMatchSnapshot(roundSnapshotOptions); @@ -30,13 +30,13 @@ describe('Text', () => { }); it('splits into lines', () => { - const text = new Text('test foo bar-baz\nqux'); + const text = new FabricText('test foo bar-baz\nqux'); expect(text._splitTextIntoLines(text.text)).toMatchSnapshot(); }); }); it('toSVG with NUM_FRACTION_DIGITS', async () => { - const text = await Text.fromObject({ + const text = await FabricText.fromObject({ text: 'xxxxxx', styles: [ { fill: 'red' }, @@ -53,7 +53,7 @@ describe('Text', () => { }); it('subscript/superscript', async () => { - const text = await Text.fromObject({ + const text = await FabricText.fromObject({ text: 'xxxxxx', styles: [ { stroke: 'black', fill: 'blue' }, diff --git a/src/shapes/Text/Text.ts b/src/shapes/Text/Text.ts index 0bd28cbefb7..7016d2f4dfd 100644 --- a/src/shapes/Text/Text.ts +++ b/src/shapes/Text/Text.ts @@ -119,7 +119,7 @@ export interface TextProps extends FabricObjectProps, UniqueTextProps { * Text class * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#text} */ -export class Text< +export class FabricText< Props extends TOptions = Partial, SProps extends SerializedTextProps = SerializedTextProps, EventSpec extends ObjectEvents = ObjectEvents @@ -415,7 +415,7 @@ export class Text< static type = 'Text'; static getDefaults() { - return { ...super.getDefaults(), ...Text.ownDefaults }; + return { ...super.getDefaults(), ...FabricText.ownDefaults }; } constructor(text: string, options: Props = {} as Props) { @@ -1663,7 +1663,7 @@ export class Text< fontFamily.includes("'") || fontFamily.includes('"') || fontFamily.includes(',') || - Text.genericFonts.includes(fontFamily.toLowerCase()) + FabricText.genericFonts.includes(fontFamily.toLowerCase()) ? fontFamily : `"${fontFamily}"`; return [ @@ -1748,7 +1748,7 @@ export class Text< } set(key: string | any, value?: any) { - const { textLayoutProperties } = this.constructor as typeof Text; + const { textLayoutProperties } = this.constructor as typeof FabricText; super.set(key, value); let needsDims = false; let isAddingPath = false; @@ -1793,7 +1793,7 @@ export class Text< /* _FROM_SVG_START_ */ /** - * List of attribute names to account for when parsing SVG element (used by {@link Text.fromElement}) + * List of attribute names to account for when parsing SVG element (used by {@link FabricText.fromElement}) * @static * @memberOf Text * @see: http://www.w3.org/TR/SVG/text.html#TextElement @@ -1813,7 +1813,7 @@ export class Text< ); /** - * Returns Text instance from an SVG element (not yet implemented) + * Returns FabricText instance from an SVG element (not yet implemented) * @static * @memberOf Text * @param {HTMLElement} element Element to parse @@ -1826,7 +1826,7 @@ export class Text< ) { const parsedAttributes = parseAttributes( element, - Text.ATTRIBUTE_NAMES, + FabricText.ATTRIBUTE_NAMES, cssRules ); @@ -1892,13 +1892,14 @@ export class Text< /* _FROM_SVG_END_ */ /** - * Returns Text instance from an object representation + * Returns FabricText instance from an object representation * @param {Object} object plain js Object to create an instance from - * @returns {Promise} + * @returns {Promise} */ - static fromObject, S extends Text>( - object: T - ) { + static fromObject< + T extends TOptions, + S extends FabricText + >(object: T) { return this._fromObject( { ...object, @@ -1911,6 +1912,14 @@ export class Text< } } -applyMixins(Text, [TextSVGExportMixin]); -classRegistry.setClass(Text); -classRegistry.setSVGClass(Text); +applyMixins(FabricText, [TextSVGExportMixin]); +classRegistry.setClass(FabricText); +classRegistry.setSVGClass(FabricText); + +/** + * @deprecated The old fabric.Text class can't be imported as Text because of conflict with Web API. + * https://developer.mozilla.org/en-US/docs/Web/API/Text/Text + * For this reason it has been renamed to FabricText. + * Please use `import { FabricText }` in place of `import { Text as FabricText }` + */ +export class Text extends FabricText {} diff --git a/src/shapes/Text/TextSVGExportMixin.ts b/src/shapes/Text/TextSVGExportMixin.ts index 0a951e06e2c..825c412cff9 100644 --- a/src/shapes/Text/TextSVGExportMixin.ts +++ b/src/shapes/Text/TextSVGExportMixin.ts @@ -7,7 +7,7 @@ import { toFixed } from '../../util/misc/toFixed'; import { FabricObjectSVGExportMixin } from '../Object/FabricObjectSVGExportMixin'; import type { TextStyleDeclaration } from './StyledText'; import { JUSTIFY } from '../Text/constants'; -import type { Text } from './Text'; +import type { FabricText } from './Text'; const multipleSpacesRegex = / +/g; const dblQuoteRegex = /"/g; @@ -23,13 +23,13 @@ function createSVGInlineRect( } export class TextSVGExportMixin extends FabricObjectSVGExportMixin { - _toSVG(this: TextSVGExportMixin & Text): string[] { + _toSVG(this: TextSVGExportMixin & FabricText): string[] { const offsets = this._getSVGLeftTopOffsets(), textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft); return this._wrapSVGTextAndBg(textAndBg); } - toSVG(this: TextSVGExportMixin & Text, reviver: TSVGReviver): string { + toSVG(this: TextSVGExportMixin & FabricText, reviver: TSVGReviver): string { return this._createBaseSVGMarkup(this._toSVG(), { reviver, noStyle: true, @@ -37,7 +37,7 @@ export class TextSVGExportMixin extends FabricObjectSVGExportMixin { }); } - private _getSVGLeftTopOffsets(this: TextSVGExportMixin & Text) { + private _getSVGLeftTopOffsets(this: TextSVGExportMixin & FabricText) { return { textLeft: -this.width / 2, textTop: -this.height / 2, @@ -46,7 +46,7 @@ export class TextSVGExportMixin extends FabricObjectSVGExportMixin { } private _wrapSVGTextAndBg( - this: TextSVGExportMixin & Text, + this: TextSVGExportMixin & FabricText, { textBgRects, textSpans, @@ -85,7 +85,7 @@ export class TextSVGExportMixin extends FabricObjectSVGExportMixin { * @return {Object} */ private _getSVGTextAndBg( - this: TextSVGExportMixin & Text, + this: TextSVGExportMixin & FabricText, textTopOffset: number, textLeftOffset: number ) { @@ -136,7 +136,7 @@ export class TextSVGExportMixin extends FabricObjectSVGExportMixin { } private _createTextCharSpan( - this: TextSVGExportMixin & Text, + this: TextSVGExportMixin & FabricText, char: string, styleDecl: TextStyleDeclaration, left: number, @@ -160,7 +160,7 @@ export class TextSVGExportMixin extends FabricObjectSVGExportMixin { } private _setSVGTextLineText( - this: TextSVGExportMixin & Text, + this: TextSVGExportMixin & FabricText, textSpans: string[], lineIndex: number, textLeftOffset: number, @@ -224,7 +224,7 @@ export class TextSVGExportMixin extends FabricObjectSVGExportMixin { } private _setSVGTextLineBg( - this: TextSVGExportMixin & Text, + this: TextSVGExportMixin & FabricText, textBgRects: (string | number)[], i: number, leftOffset: number, @@ -272,7 +272,10 @@ export class TextSVGExportMixin extends FabricObjectSVGExportMixin { /** * @deprecated unused */ - _getSVGLineTopOffset(this: TextSVGExportMixin & Text, lineIndex: number) { + _getSVGLineTopOffset( + this: TextSVGExportMixin & FabricText, + lineIndex: number + ) { let lineTopOffset = 0, j; for (j = 0; j < lineIndex; j++) { @@ -292,7 +295,7 @@ export class TextSVGExportMixin extends FabricObjectSVGExportMixin { * @param {Boolean} skipShadow a boolean to skip shadow filter output * @return {String} */ - getSvgStyles(this: TextSVGExportMixin & Text, skipShadow?: boolean) { + getSvgStyles(this: TextSVGExportMixin & FabricText, skipShadow?: boolean) { // @ts-expect-error TS doesn't respect this type casting return `${super.getSvgStyles(skipShadow)} white-space: pre;`; } @@ -304,7 +307,7 @@ export class TextSVGExportMixin extends FabricObjectSVGExportMixin { * @return {String} */ getSvgSpanStyles( - this: TextSVGExportMixin & Text, + this: TextSVGExportMixin & FabricText, style: TextStyleDeclaration, useWhiteSpace?: boolean ) { @@ -347,7 +350,7 @@ export class TextSVGExportMixin extends FabricObjectSVGExportMixin { * @return {String} */ getSvgTextDecoration( - this: TextSVGExportMixin & Text, + this: TextSVGExportMixin & FabricText, style: TextStyleDeclaration ) { return (['overline', 'underline', 'line-through'] as const) diff --git a/src/shapes/Text/__snapshots__/Text.spec.ts.snap b/src/shapes/Text/__snapshots__/Text.spec.ts.snap index 1e88f3deda2..4cbea8a4c25 100644 --- a/src/shapes/Text/__snapshots__/Text.spec.ts.snap +++ b/src/shapes/Text/__snapshots__/Text.spec.ts.snap @@ -1,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Text measuring, splitting measuring 1`] = ` +exports[`FabricText measuring, splitting measuring 1`] = ` { "kernedWidth": 18, "width": 18, } `; -exports[`Text measuring, splitting splits into lines 1`] = ` +exports[`FabricText measuring, splitting splits into lines 1`] = ` { "_unwrappedLines": [ [ @@ -89,7 +89,7 @@ exports[`Text measuring, splitting splits into lines 1`] = ` } `; -exports[`Text subscript/superscript 1`] = ` +exports[`FabricText subscript/superscript 1`] = ` [ { "end": 1, @@ -146,7 +146,7 @@ exports[`Text subscript/superscript 1`] = ` ] `; -exports[`Text toObject 1`] = ` +exports[`FabricText toObject 1`] = ` { "angle": 0, "backgroundColor": "", @@ -199,14 +199,14 @@ exports[`Text toObject 1`] = ` } `; -exports[`Text toSVG with NUM_FRACTION_DIGITS 1`] = ` +exports[`FabricText toSVG with NUM_FRACTION_DIGITS 1`] = ` " xxxxxx " `; -exports[`Text toSVG with NUM_FRACTION_DIGITS 2`] = ` +exports[`FabricText toSVG with NUM_FRACTION_DIGITS 2`] = ` " xxxxxx diff --git a/src/shapes/Text/constants.ts b/src/shapes/Text/constants.ts index 88c5d8c1ff2..f7b86b1f0e3 100644 --- a/src/shapes/Text/constants.ts +++ b/src/shapes/Text/constants.ts @@ -1,6 +1,6 @@ import { LEFT, reNewline } from '../../constants'; import type { TClassProperties } from '../../typedefs'; -import type { Text } from './Text'; +import type { FabricText } from './Text'; const fontProperties = [ 'fontSize', @@ -62,7 +62,7 @@ export const styleProperties: Readonly = [ // @TODO: Many things here are configuration related and shouldn't be on the class nor prototype // regexes, list of properties that are not suppose to change by instances, magic consts. // this will be a separated effort -export const textDefaultValues: Partial> = { +export const textDefaultValues: Partial> = { _reNewline: reNewline, _reSpacesAndTabs: /[ \t\r]/g, _reSpaceAndTab: /[ \t\r]/, diff --git a/src/util/transform_matrix_removal.ts b/src/util/transform_matrix_removal.ts index 29b56012b92..b7dc6b48dcc 100644 --- a/src/util/transform_matrix_removal.ts +++ b/src/util/transform_matrix_removal.ts @@ -1,5 +1,5 @@ import { CENTER } from '../constants'; -import type { Image } from '../shapes/Image'; +import type { FabricImage } from '../shapes/Image'; import type { FabricObject } from '../shapes/Object/FabricObject'; import type { TMat2D } from '../typedefs'; import { qrDecompose } from './misc/matrix'; @@ -50,8 +50,8 @@ export const removeTransformMatrixForSvgParsing = ( if (preserveAspectRatioOptions) { object.scaleX *= preserveAspectRatioOptions.scaleX; object.scaleY *= preserveAspectRatioOptions.scaleY; - (object as Image).cropX = preserveAspectRatioOptions.cropX; - (object as Image).cropY = preserveAspectRatioOptions.cropY; + (object as FabricImage).cropX = preserveAspectRatioOptions.cropX; + (object as FabricImage).cropY = preserveAspectRatioOptions.cropY; center.x += preserveAspectRatioOptions.offsetLeft; center.y += preserveAspectRatioOptions.offsetTop; object.width = preserveAspectRatioOptions.width; diff --git a/src/util/typeAssertions.ts b/src/util/typeAssertions.ts index b50a465111b..8c42e77e896 100644 --- a/src/util/typeAssertions.ts +++ b/src/util/typeAssertions.ts @@ -6,7 +6,7 @@ import type { } from '../shapes/Object/Object'; import type { FabricObjectWithDragSupport } from '../shapes/Object/InteractiveObject'; import type { TFiller } from '../typedefs'; -import type { Text } from '../shapes/Text/Text'; +import type { FabricText } from '../shapes/Text/Text'; import type { Pattern } from '../Pattern'; import type { IText } from '../shapes/IText/IText'; import type { Textbox } from '../shapes/Textbox'; @@ -46,7 +46,7 @@ export const isActiveSelection = ( export const isTextObject = ( fabricObject?: FabricObject -): fabricObject is Text => { +): fabricObject is FabricText => { // we could use instanceof but that would mean pulling in Text code for a simple check // @todo discuss what to do and how to do return !!fabricObject && fabricObject.isType('Text', 'IText', 'Textbox'); diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index b027b3b4dd9..745ea44c2d1 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -1262,7 +1262,7 @@ canvas.loadFromJSON(serialized).then(function() { assert.ok(!canvas.isEmpty(), 'canvas is not empty'); assert.equal(canvas.backgroundColor, 'green'); - assert.ok(canvas.backgroundImage instanceof fabric.Image); + assert.ok(canvas.backgroundImage instanceof fabric.FabricImage); done(); }); }); @@ -1327,7 +1327,7 @@ var canvas3 = new fabric.StaticCanvas(); var json = '{"clipPath": {"type":"Text","left":150,"top":200,"width":128,"height":64.32,"fill":"#000000","stroke":"","strokeWidth":"","scaleX":0.8,"scaleY":0.8,"angle":0,"flipX":false,"flipY":false,"opacity":1,"text":"NAME HERE","fontSize":24,"fontWeight":"normal","fontFamily":"Delicious_500","fontStyle":"normal","lineHeight":"","textDecoration":"","textAlign":"center","path":"","strokeStyle":"","backgroundColor":""}}'; canvas3.loadFromJSON(json).then(function() { - assert.ok(canvas3.clipPath instanceof fabric.Text); + assert.ok(canvas3.clipPath instanceof fabric.FabricText); assert.equal(canvas3.clipPath.constructor.type, 'Text'); done(); }); diff --git a/test/unit/circle.js b/test/unit/circle.js index bb1003b8c71..a68bf9543bc 100644 --- a/test/unit/circle.js +++ b/test/unit/circle.js @@ -8,7 +8,7 @@ var circle = new fabric.Circle(); assert.ok(circle instanceof fabric.Circle, 'should inherit from fabric.Circle'); - assert.ok(circle instanceof fabric.Object, 'should inherit from fabric.Object'); + assert.ok(circle instanceof fabric.FabricObject, 'should inherit from fabric.Object'); assert.deepEqual(circle.constructor.type, 'Circle'); }); diff --git a/test/unit/class_registry.js b/test/unit/class_registry.js index df8060b9f8d..0bc74ee992a 100644 --- a/test/unit/class_registry.js +++ b/test/unit/class_registry.js @@ -39,6 +39,6 @@ assert.equal(fabric.classRegistry.getClass('rect'), fabric.Rect, 'resolves class correctly'); assert.equal(fabric.classRegistry.getClass('i-text'), fabric.IText, 'resolves class correctly'); assert.equal(fabric.classRegistry.getClass('activeSelection'), fabric.ActiveSelection, 'resolves class correctly'); - assert.equal(fabric.classRegistry.getClass('object'), fabric.Object, 'resolves class correctly'); + assert.equal(fabric.classRegistry.getClass('object'), fabric.FabricObject, 'resolves class correctly'); }); })() diff --git a/test/unit/ellipse.js b/test/unit/ellipse.js index 7a79fd2d1bd..43302703b4d 100644 --- a/test/unit/ellipse.js +++ b/test/unit/ellipse.js @@ -10,7 +10,7 @@ var ellipse = new fabric.Ellipse(); assert.ok(ellipse instanceof fabric.Ellipse, 'should inherit from fabric.Ellipse'); - assert.ok(ellipse instanceof fabric.Object, 'should inherit from fabric.Object'); + assert.ok(ellipse instanceof fabric.FabricObject, 'should inherit from fabric.Object'); assert.equal(ellipse.constructor.type, 'Ellipse'); }); diff --git a/test/unit/image.js b/test/unit/image.js index 2984aa0c161..2b3d7afd8ec 100644 --- a/test/unit/image.js +++ b/test/unit/image.js @@ -115,8 +115,8 @@ assert.ok(fabric.Image); createImageObject(function(image) { - assert.ok(image instanceof fabric.Image); - assert.ok(image instanceof fabric.Object); + assert.ok(image instanceof fabric.FabricImage); + assert.ok(image instanceof fabric.FabricObject); assert.equal(image.constructor.type, 'Image'); @@ -443,7 +443,7 @@ createImageObject(function(image) { assert.ok(typeof image.clone === 'function'); image.clone().then(function(clone) { - assert.ok(clone instanceof fabric.Image); + assert.ok(clone instanceof fabric.FabricImage); assert.deepEqual(clone.toObject(), image.toObject(), 'clone and original image are equal'); done(); }); @@ -467,7 +467,7 @@ var done = assert.async(); assert.ok(typeof fabric.Image.fromObject === 'function'); fabric.Image.fromObject({ ...REFERENCE_IMG_OBJECT, src: IMG_SRC }).then(function (instance) { - assert.ok(instance instanceof fabric.Image); + assert.ok(instance instanceof fabric.FabricImage); done(); }); }); @@ -488,7 +488,7 @@ } }; fabric.Image.fromObject(obj).then(function(instance){ - assert.ok(instance instanceof fabric.Image); + assert.ok(instance instanceof fabric.FabricImage); assert.ok(instance.clipPath instanceof fabric.Rect); assert.ok(Array.isArray(instance.filters), 'should enliven filters'); assert.equal(instance.filters.length, 1, 'should enliven filters'); @@ -536,7 +536,7 @@ var done = assert.async(); assert.ok(typeof fabric.Image.fromURL === 'function'); fabric.Image.fromURL(IMG_SRC).then(function(instance) { - assert.ok(instance instanceof fabric.Image); + assert.ok(instance instanceof fabric.FabricImage); assert.sameImageObject(REFERENCE_IMG_OBJECT, instance.toObject()); done(); }); @@ -548,7 +548,7 @@ fabric.Image.fromURL(IMG_SRC, { crossOrigin: 'use-credentials', }).then(function(instance) { - assert.ok(instance instanceof fabric.Image); + assert.ok(instance instanceof fabric.FabricImage); assert.sameImageObject({ ...REFERENCE_IMG_OBJECT, crossOrigin: 'use-credentials' }, instance.toObject()); done(); }); @@ -558,7 +558,7 @@ var done = assert.async(); assert.ok(typeof fabric.Image.fromURL === 'function'); fabric.Image.fromURL(IMG_URL_NON_EXISTING, function(instance) { - assert.ok(instance instanceof fabric.Image); + assert.ok(instance instanceof fabric.FabricImage); }).catch(function(e) { assert.ok(e instanceof Error); done(); @@ -579,7 +579,7 @@ }); fabric.Image.fromElement(imageEl).then((imgObject)=> { - assert.ok(imgObject instanceof fabric.Image); + assert.ok(imgObject instanceof fabric.FabricImage); assert.deepEqual(imgObject.get('width'), 14, 'width of an object'); assert.deepEqual(imgObject.get('height'), 17, 'height of an object'); assert.deepEqual(imgObject.getSrc(), IMAGE_DATA_URL, 'src of an object'); @@ -602,7 +602,7 @@ }); fabric.Image.fromElement(imageEl).then((imgObject)=> { - assert.ok(imgObject instanceof fabric.Image); + assert.ok(imgObject instanceof fabric.FabricImage); assert.deepEqual(imgObject.get('imageSmoothing'), false, 'imageSmoothing set to false'); done(); }); @@ -623,7 +623,7 @@ fabric.Image.fromElement(imageEl).then((imgObject)=> { fabric.util.removeTransformMatrixForSvgParsing(imgObject, imgObject.parsePreserveAspectRatioAttribute()); - assert.ok(imgObject instanceof fabric.Image); + assert.ok(imgObject instanceof fabric.FabricImage); assert.deepEqual(imgObject.get('width'), 14, 'width of an object'); assert.deepEqual(imgObject.get('height'), 17, 'height of an object'); assert.deepEqual(imgObject.get('scaleX'), 10, 'scaleX compensate the width'); diff --git a/test/unit/itext.js b/test/unit/itext.js index 2de2df44ae2..67708de9100 100644 --- a/test/unit/itext.js +++ b/test/unit/itext.js @@ -65,7 +65,7 @@ var iText = new fabric.IText('test'); assert.ok(iText instanceof fabric.IText); - assert.ok(iText instanceof fabric.Text); + assert.ok(iText instanceof fabric.FabricText); }); QUnit.test('initial properties', function(assert) { diff --git a/test/unit/line.js b/test/unit/line.js index b05e16c2502..1f22882699c 100644 --- a/test/unit/line.js +++ b/test/unit/line.js @@ -45,7 +45,7 @@ var line = new fabric.Line([10, 11, 20, 21]); assert.ok(line instanceof fabric.Line); - assert.ok(line instanceof fabric.Object); + assert.ok(line instanceof fabric.FabricObject); assert.equal(line.constructor.type, 'Line'); diff --git a/test/unit/object.js b/test/unit/object.js index de8e3b5a458..6c6ede2fcc0 100644 --- a/test/unit/object.js +++ b/test/unit/object.js @@ -24,7 +24,7 @@ var cObj = new fabric.Object(); assert.ok(cObj); - assert.ok(cObj instanceof fabric.Object); + assert.ok(cObj instanceof fabric.FabricObject); assert.ok(cObj.constructor === fabric.Object); assert.equal(cObj.constructor.type, 'FabricObject'); @@ -361,7 +361,7 @@ assert.ok(typeof cObj.cloneAsImage === 'function'); var image = cObj.cloneAsImage(); assert.ok(image); - assert.ok(image instanceof fabric.Image); + assert.ok(image instanceof fabric.FabricImage); assert.equal(image.width, 100, 'the image has same dimension of object'); }); @@ -370,7 +370,7 @@ fabric.config.configure({ devicePixelRatio: 2 }); var image = cObj.cloneAsImage({ enableRetinaScaling: true }); assert.ok(image); - assert.ok(image instanceof fabric.Image); + assert.ok(image instanceof fabric.FabricImage); assert.equal(image.width, 200, 'the image has been scaled by retina'); }); diff --git a/test/unit/object_clipPath.js b/test/unit/object_clipPath.js index 412f4eda0bf..a46ac2f6a87 100644 --- a/test/unit/object_clipPath.js +++ b/test/unit/object_clipPath.js @@ -102,7 +102,7 @@ fabric.Rect.fromObject(toObject).then(function(rect) { assert.ok(rect.clipPath instanceof fabric.Circle, 'clipPath is enlived'); assert.equal(rect.clipPath.radius, 50, 'radius is restored correctly'); - assert.ok(rect.clipPath.clipPath instanceof fabric.Text, 'nested clipPath is enlived'); + assert.ok(rect.clipPath.clipPath instanceof fabric.FabricText, 'nested clipPath is enlived'); assert.equal(rect.clipPath.clipPath.text, 'clipPath', 'instance is restored correctly'); done(); }); @@ -117,7 +117,7 @@ fabric.Rect.fromObject(toObject).then(function(rect) { assert.ok(rect.clipPath instanceof fabric.Circle, 'clipPath is enlived'); assert.equal(rect.clipPath.radius, 50, 'radius is restored correctly'); - assert.ok(rect.clipPath.clipPath instanceof fabric.Text, 'neted clipPath is enlived'); + assert.ok(rect.clipPath.clipPath instanceof fabric.FabricText, 'neted clipPath is enlived'); assert.equal(rect.clipPath.clipPath.text, 'clipPath', 'instance is restored correctly'); assert.equal(rect.clipPath.clipPath.inverted, true, 'instance inverted is restored correctly'); assert.equal(rect.clipPath.clipPath.absolutePositioned, true, 'instance absolutePositioned is restored correctly'); diff --git a/test/unit/path.js b/test/unit/path.js index 427ba06c53b..9973c5a6a41 100644 --- a/test/unit/path.js +++ b/test/unit/path.js @@ -78,7 +78,7 @@ makePathObject(function(path) { assert.ok(path instanceof fabric.Path); - assert.ok(path instanceof fabric.Object); + assert.ok(path instanceof fabric.FabricObject); assert.equal(path.constructor.type, 'Path'); diff --git a/test/unit/polygon.js b/test/unit/polygon.js index 92bdc73ab67..f579c6819e0 100644 --- a/test/unit/polygon.js +++ b/test/unit/polygon.js @@ -59,7 +59,7 @@ assert.ok(polygon instanceof fabric.Polygon); assert.ok(polygon instanceof fabric.Polyline); - assert.ok(polygon instanceof fabric.Object); + assert.ok(polygon instanceof fabric.FabricObject); assert.equal(polygon.constructor.type, 'Polygon'); assert.deepEqual(polygon.get('points'), [{ x: 10, y: 12 }, { x: 20, y: 22 }]); diff --git a/test/unit/polyline.js b/test/unit/polyline.js index 347e3fa40cb..7694f7ef313 100644 --- a/test/unit/polyline.js +++ b/test/unit/polyline.js @@ -58,7 +58,7 @@ var polyline = new fabric.Polyline(getPoints()); assert.ok(polyline instanceof fabric.Polyline); - assert.ok(polyline instanceof fabric.Object); + assert.ok(polyline instanceof fabric.FabricObject); assert.equal(polyline.constructor.type, 'Polyline'); assert.deepEqual(polyline.get('points'), [{ x: 10, y: 12 }, { x: 20, y: 22 }]); diff --git a/test/unit/rect.js b/test/unit/rect.js index 16ea91f5b5b..65128709f08 100644 --- a/test/unit/rect.js +++ b/test/unit/rect.js @@ -44,7 +44,7 @@ var rect = new fabric.Rect(); assert.ok(rect instanceof fabric.Rect); - assert.ok(rect instanceof fabric.Object); + assert.ok(rect instanceof fabric.FabricObject); assert.deepEqual(rect.constructor.type, 'Rect'); }); diff --git a/test/unit/text.js b/test/unit/text.js index 0c1070cb14b..71e877bf52f 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -72,8 +72,8 @@ var text = createTextObject(); assert.ok(text); - assert.ok(text instanceof fabric.Text); - assert.ok(text instanceof fabric.Object); + assert.ok(text instanceof fabric.FabricText); + assert.ok(text instanceof fabric.FabricObject); assert.equal(text.constructor.type, 'Text'); assert.equal(text.get('text'), 'x'); @@ -194,7 +194,7 @@ elText.textContent = 'x'; fabric.Text.fromElement(elText, function(text) { - assert.ok(text instanceof fabric.Text); + assert.ok(text instanceof fabric.FabricText); var expectedObject = { ...REFERENCE_TEXT_OBJECT, left: 0, @@ -236,7 +236,7 @@ // temp workaround for text objects not obtaining width under node textWithAttrs.width = CHAR_WIDTH; - assert.ok(textWithAttrs instanceof fabric.Text); + assert.ok(textWithAttrs instanceof fabric.FabricText); var expectedObject = { ...REFERENCE_TEXT_OBJECT, diff --git a/test/unit/textbox.js b/test/unit/textbox.js index a4e1741117a..522598e1de1 100644 --- a/test/unit/textbox.js +++ b/test/unit/textbox.js @@ -16,7 +16,7 @@ var textbox = new fabric.Textbox('test'); assert.ok(textbox instanceof fabric.Textbox); assert.ok(textbox instanceof fabric.IText); - assert.ok(textbox instanceof fabric.Text); + assert.ok(textbox instanceof fabric.FabricText); }); QUnit.test('constructor with width', function(assert) { From f070b5ea8ce01277cc013bf6db540a094303a7e0 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Tue, 26 Sep 2023 16:52:09 +0200 Subject: [PATCH 10/12] ci(): **Breaking** Remove node 14 support - we are not testing node14 compatibility anymore (#9383) --- .github/workflows/tests.yml | 2 +- CHANGELOG.md | 1 + package-lock.json | 33 +-------------------------------- package.json | 3 +-- test/node_test_setup.js | 6 ------ 5 files changed, 4 insertions(+), 41 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7b5e4cd3f2e..73ffbee1622 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -79,7 +79,7 @@ jobs: # For more information see: # https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions # supported Node.js release schedule: https://nodejs.org/en/about/releases/ - node-version: [14.x, 16.x, 20.x] + node-version: [16.x, 20.x] suite: [unit, visual] steps: - uses: actions/checkout@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index e2fe32a05a2..f6b49bbc65b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- Breaking: Remove node 14 [#9383](https://github.com/fabricjs/fabric.js/pull/9383) - chore(): Rename exports that conflicts with JS/WEB api ( Object, Text, Image ). Kept backward compatibility with deprecation notice [#9172](https://github.com/fabricjs/fabric.js/pull/9172) - fix(Geometry): `containsPoint` [#9372](https://github.com/fabricjs/fabric.js/pull/9372) **BREAKING**: diff --git a/package-lock.json b/package-lock.json index 4dce6f87d97..95d2dc7e80c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,6 @@ "@types/node": "^17.0.21", "@typescript-eslint/eslint-plugin": "^5.59.5", "@typescript-eslint/parser": "^5.59.5", - "abort-controller": "^3.0.0", "auto-changelog": "^2.3.0", "axios": "^0.27.2", "babel-plugin-import-json-value": "^0.1.2", @@ -63,7 +62,7 @@ "v8-to-istanbul": "^9.1.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" }, "optionalDependencies": { "canvas": "^2.11.2", @@ -3542,17 +3541,6 @@ "license": "ISC", "optional": true }, - "node_modules/abort-controller": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/accepts": { "version": "1.3.8", "dev": true, @@ -5551,14 +5539,6 @@ "node": ">= 0.6" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/eventemitter3": { "version": "4.0.7", "dev": true, @@ -14749,13 +14729,6 @@ "version": "1.1.1", "optional": true }, - "abort-controller": { - "version": "3.0.0", - "dev": true, - "requires": { - "event-target-shim": "^5.0.0" - } - }, "accepts": { "version": "1.3.8", "dev": true, @@ -16088,10 +16061,6 @@ "version": "1.8.1", "dev": true }, - "event-target-shim": { - "version": "5.0.1", - "dev": true - }, "eventemitter3": { "version": "4.0.7", "dev": true diff --git a/package.json b/package.json index dbd886ecd07..dcd22f2b04f 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,6 @@ "@types/node": "^17.0.21", "@typescript-eslint/eslint-plugin": "^5.59.5", "@typescript-eslint/parser": "^5.59.5", - "abort-controller": "^3.0.0", "auto-changelog": "^2.3.0", "axios": "^0.27.2", "babel-plugin-import-json-value": "^0.1.2", @@ -129,7 +128,7 @@ "v8-to-istanbul": "^9.1.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.20.0" }, "module": "./dist/index.mjs", "main": "./dist/index.node.cjs", diff --git a/test/node_test_setup.js b/test/node_test_setup.js index 59c7f006cff..0f599a93d66 100644 --- a/test/node_test_setup.js +++ b/test/node_test_setup.js @@ -4,12 +4,6 @@ require('./lib/assert'); var chalk = require('chalk'); var commander = require('commander'); -// TODO remove block and dependency when node 14 fades out -// node 14 AbortController polyfill for tests -if (!global.AbortController) { - require("abort-controller/polyfill"); -} - commander.program .option('-d, --debug', 'debug visual tests by overriding refs (golden images) in case of visual changes', false) From a98b1408d231f39ae3806295199f65b1a3387949 Mon Sep 17 00:00:00 2001 From: Shachar <34343793+ShaMan123@users.noreply.github.com> Date: Wed, 27 Sep 2023 15:33:17 +0530 Subject: [PATCH 11/12] ci(e2e): fix babel compiling error (#9388) * fix babel watch * fix watching fs.watch on wondows throws on file removal https://github.com/nodejs/help/issues/4252 * update CHANGELOG.md * Update CONTRIBUTING.md * Update CONTRIBUTING.md Update CONTRIBUTING.md --------- Co-authored-by: github-actions[bot] --- CHANGELOG.md | 1 + CONTRIBUTING.md | 18 ++++----- playwright.setup.ts | 96 ++++++++++++++++++++++++++++++++++++++------- 3 files changed, 92 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6b49bbc65b..0462451867b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- ci(e2e): fix babel compiling error [#9388](https://github.com/fabricjs/fabric.js/pull/9388) - Breaking: Remove node 14 [#9383](https://github.com/fabricjs/fabric.js/pull/9383) - chore(): Rename exports that conflicts with JS/WEB api ( Object, Text, Image ). Kept backward compatibility with deprecation notice [#9172](https://github.com/fabricjs/fabric.js/pull/9172) - fix(Geometry): `containsPoint` [#9372](https://github.com/fabricjs/fabric.js/pull/9372) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9d4866f22a7..125aea13a56 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -129,15 +129,15 @@ It is more than likely you will be requested to change stuff and refine your wor [![🧪](../../actions/workflows/tests.yml/badge.svg)](../../actions/workflows/tests.yml) [![CodeQL](../../actions/workflows/codeql-analysis.yml/badge.svg)](../../actions/workflows/codeql-analysis.yml) -| Suite | unit (node) | e2e (browser) | -| ---------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Framework | [`jest`][jest] | [`playwright`][playwright] | -| Setup | |
npm run build -- -f -w
| -| Running Tests




\ -- [filter] [watch]
|
npm run test:jest -- [filters] [-w]



It is advised to use filters to save time.
|
npm run test:e2e -- [filters] [--ui]

In some machines babel is flaky and doesn't build the test files. In that is the case, try running the command using `npx` directly, see [`playwright.setup.ts`](./playwright.setup.ts). | -| Writing Tests | Add/update `src/*.(spec\|test).ts` files | - Update tests in `e2e/tests`
- Create a new test based on `e2e/template` | -| Test Gen | |
npm start vanilla
npx playwright codegen http://localhost:1234
| -| Test Spec | | - `index.ts`: built and loaded into the web app
- `index.spec.ts`: test spec
| -| Outputs | Snapshots next to the test file | - Snapshots next to the test file
- `e2e/test-report`
- `e2e/test-results` | +| Suite | unit (node) | e2e (browser) | +| ------------------------------------------------------------------------------------------------------------- | :--------------------------------------------- | :----------------------------------------------------------------------------------- | +| Framework | [`jest`][jest] | [`playwright`][playwright] | +| Setup | |
npm run build -- -f -w
| +| Running Tests

\ -- [filter] [watch]

It is advised to use filters to save time |
npm run test:jest -- [filters] [-w]
|
npm run test:e2e -- [filters] [--ui]
| +| Writing Tests | Add/update `src/*.(spec\|test).ts` files | - Update tests in `e2e/tests`
- Create a new test based on `e2e/template` | +| Test Gen | |
npm start vanilla
npx playwright codegen http://localhost:1234
| +| Test Spec | | - `index.ts`: built and loaded into the web app
- `index.spec.ts`: test spec
| +| Outputs | Snapshots next to the test file | - Snapshots next to the test file
- `e2e/test-report`
- `e2e/test-results` | ### Legacy Test Suite diff --git a/playwright.setup.ts b/playwright.setup.ts index 66c651d1982..98d6cf1676d 100644 --- a/playwright.setup.ts +++ b/playwright.setup.ts @@ -1,16 +1,84 @@ -import { PlaywrightTestConfig } from '@playwright/test'; -import { spawn } from 'child_process'; - -export default (config: PlaywrightTestConfig) => { - const watch = process.argv.includes('--ui'); - return new Promise((resolve) => { - const p = spawn( - `babel --no-babelrc e2e/tests --extensions '.ts' --ignore '**/*.spec.ts' --out-dir e2e/dist --config-file ./e2e/.babelrc.mjs ${ - watch ? '-w' : '' - }`, - { shell: true, detached: false } - ); - p.stdout.pipe(process.stdout); - p.stdout.on('data', resolve); +import { transformFileAsync } from '@babel/core'; +import type { PlaywrightTestConfig } from '@playwright/test'; +import { readdirSync, rmSync, statSync, watch, writeFileSync } from 'node:fs'; +import { ensureFileSync } from 'fs-extra'; +import match from 'micromatch'; +import path from 'path'; + +const include = ['**/*.ts']; +const exclude = ['**/*.spec.ts']; + +const src = path.resolve(process.cwd(), 'e2e', 'tests'); +const dist = path.resolve(process.cwd(), 'e2e', 'dist'); + +const includeRe = include.map((glob) => match.makeRe(glob)); +const excludeRe = exclude.map((glob) => match.makeRe(glob)); + +const walkSync = (dir: string, callback: (file: string) => any) => { + const files = readdirSync(dir); + files.forEach((file) => { + var filepath = path.resolve(dir, file); + const stats = statSync(filepath); + if (stats.isDirectory()) { + walkSync(filepath, callback); + } else if (stats.isFile()) { + callback(filepath); + } + }); +}; + +const shouldBuild = (file: string) => + includeRe.some((re) => re.test(file)) && + excludeRe.every((re) => !re.test(file)); + +const getDistFileName = (file: string) => { + const { dir, name } = path.parse( + path.resolve(dist, path.relative(src, file)) + ); + return path.format({ + dir, + name, + ext: '.js', }); }; + +const buildFile = async (file: string) => { + const result = await transformFileAsync(file, { + configFile: './e2e/.babelrc.mjs', + babelrc: undefined, + }); + if (result?.code) { + const distFile = getDistFileName(file); + ensureFileSync(distFile); + writeFileSync(distFile, result.code); + } +}; + +export default async (config: PlaywrightTestConfig) => { + const files: string[] = []; + walkSync(src, (file) => files.push(file)); + + rmSync(dist, { recursive: true, force: true }); + const tasks = await Promise.all( + files.filter((file) => shouldBuild(file)).map((file) => buildFile(file)) + ); + console.log( + `Successfully compiled ${tasks.length} files from ${path.relative( + process.cwd(), + src + )} to ${path.relative(process.cwd(), dist)}` + ); + + // watch + if (process.argv.includes('--ui')) { + const watcher = watch( + src, + { recursive: true, persistent: true }, + (type, filename) => { + const file = path.join(src, filename); + shouldBuild(file) && buildFile(file); + } + ); + process.once('exit', () => watcher.close()); + } +}; From 228d552356053cc0d250fab601b7c38cc168875e Mon Sep 17 00:00:00 2001 From: Shachar <34343793+ShaMan123@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:49:37 +0530 Subject: [PATCH 12/12] fix(Canvas): invalidate `_objectsToRender` on stack change (#9387) --- CHANGELOG.md | 1 + src/canvas/SelectableCanvas.ts | 9 ++- src/canvas/__tests__/SelectableCanvas.spec.ts | 60 +++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/canvas/__tests__/SelectableCanvas.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0462451867b..52d43f42d4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- fix(Canvas): invalidate `_objectsToRender` on stack change [#9387](https://github.com/fabricjs/fabric.js/pull/9387) - ci(e2e): fix babel compiling error [#9388](https://github.com/fabricjs/fabric.js/pull/9388) - Breaking: Remove node 14 [#9383](https://github.com/fabricjs/fabric.js/pull/9383) - chore(): Rename exports that conflicts with JS/WEB api ( Object, Text, Image ). Kept backward compatibility with deprecation notice [#9172](https://github.com/fabricjs/fabric.js/pull/9172) diff --git a/src/canvas/SelectableCanvas.ts b/src/canvas/SelectableCanvas.ts index b68c8a1f537..2eb88f853f4 100644 --- a/src/canvas/SelectableCanvas.ts +++ b/src/canvas/SelectableCanvas.ts @@ -213,10 +213,10 @@ export class SelectableCanvas * @type FabricObject[] * @private */ - _objectsToRender?: FabricObject[] = []; + _objectsToRender?: FabricObject[]; /** - * hold a referenfce to a data structure that contains information + * hold a reference to a data structure that contains information * on the current on going transform * @type * @private @@ -344,6 +344,11 @@ export class SelectableCanvas super._onObjectRemoved(obj); } + _onStackOrderChanged() { + this._objectsToRender = undefined; + super._onStackOrderChanged(); + } + /** * Divides objects in two groups, one to render immediately * and one to render as activeGroup. diff --git a/src/canvas/__tests__/SelectableCanvas.spec.ts b/src/canvas/__tests__/SelectableCanvas.spec.ts new file mode 100644 index 00000000000..9a17580e792 --- /dev/null +++ b/src/canvas/__tests__/SelectableCanvas.spec.ts @@ -0,0 +1,60 @@ +import { FabricObject } from '../../shapes/Object/FabricObject'; +import { Canvas } from '../Canvas'; + +describe('Canvas', () => { + describe('invalidating `_objectsToRender`', () => { + test('initial state', () => { + const canvas = new Canvas(); + expect(canvas._objectsToRender).toBeUndefined(); + }); + + test('mousedown', () => { + const canvas = new Canvas(); + canvas.add(new FabricObject({ width: 10, height: 10 })); + canvas._objectsToRender = []; + canvas + .getSelectionElement() + .dispatchEvent(new MouseEvent('mousedown', { clientX: 5, clientY: 5 })); + expect(canvas._objectsToRender).toBeUndefined(); + }); + + test('object added/removed', () => { + const canvas = new Canvas(); + canvas._objectsToRender = []; + const object = new FabricObject({ width: 10, height: 10 }); + canvas.add(object); + expect(canvas._objectsToRender).toBeUndefined(); + canvas._objectsToRender = []; + canvas.remove(object); + expect(canvas._objectsToRender).toBeUndefined(); + }); + + test('stack change', () => { + const canvas = new Canvas(); + const object = new FabricObject({ width: 10, height: 10 }); + const object2 = new FabricObject({ + left: 5, + top: 5, + width: 10, + height: 10, + }); + canvas.add(object, object2); + + canvas._objectsToRender = []; + canvas.sendObjectBackwards(object2); + expect(canvas._objectsToRender).toBeUndefined(); + + canvas._objectsToRender = []; + canvas.bringObjectForward(object2); + expect(canvas._objectsToRender).toBeUndefined(); + + canvas._objectsToRender = []; + canvas.bringObjectToFront(object); + expect(canvas._objectsToRender).toBeUndefined(); + + canvas._objectsToRender = []; + canvas.sendObjectToBack(object); + expect(canvas._objectsToRender).toBeUndefined(); + }); + }); +});