diff --git a/CHANGELOG.md b/CHANGELOG.md index 228c17d831c..0223f243489 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- chore(TS): migrate Polyline/Polygon [#8417](https://github.com/fabricjs/fabric.js/pull/8417) - chore(TS): migrate Rect [#8411](https://github.com/fabricjs/fabric.js/pull/8411) - chore(TS): migrate Ellipse [#8408](https://github.com/fabricjs/fabric.js/pull/8408) - chore(TS): migrate Triangle to TS [#8410](https://github.com/fabricjs/fabric.js/pull/8410) diff --git a/src/point.class.ts b/src/point.class.ts index 530df68c9a0..5c201e80900 100644 --- a/src/point.class.ts +++ b/src/point.class.ts @@ -16,8 +16,6 @@ export class Point { y: number; - type = 'point'; - constructor(); constructor(x: number, y: number); constructor(point: IPoint); diff --git a/src/shapes/object.class.ts b/src/shapes/object.class.ts index 2be5a720649..1a3ad04d2c6 100644 --- a/src/shapes/object.class.ts +++ b/src/shapes/object.class.ts @@ -235,14 +235,14 @@ export class FabricObject extends ObjectGeometry { * @type String * @default butt */ - strokeLineCap: string; + strokeLineCap: CanvasLineCap; /** * Corner style of an object's stroke (one of "bevel", "round", "miter") * @type String * @default */ - strokeLineJoin: string; + strokeLineJoin: CanvasLineJoin; /** * Maximum miter length (used for strokeLineJoin = "miter") of an object's stroke diff --git a/src/shapes/polygon.class.ts b/src/shapes/polygon.class.ts index d388ad5d68c..e6762b59b9c 100644 --- a/src/shapes/polygon.class.ts +++ b/src/shapes/polygon.class.ts @@ -1,77 +1,51 @@ -//@ts-nocheck +import { fabric } from '../../HEADER'; +import { TClassProperties } from '../typedefs'; +import { FabricObject } from './object.class'; +import { polyFromElement, Polyline } from './polyline.class'; -import { projectStrokeOnPoints } from '../util/misc/projectStroke'; - -(function (global) { - var fabric = global.fabric || (global.fabric = {}); - - /** - * Polygon class - * @class fabric.Polygon - * @extends fabric.Polyline - * @see {@link fabric.Polygon#initialize} for constructor definition - */ - fabric.Polygon = fabric.util.createClass( - fabric.Polyline, - /** @lends fabric.Polygon.prototype */ { - /** - * Type of an object - * @type String - * @default - */ - type: 'polygon', - - /** - * @private - */ - _projectStrokeOnPoints: function () { - return projectStrokeOnPoints(this.points, this); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function (ctx) { - if (!this.commonRender(ctx)) { - return; - } - ctx.closePath(); - this._renderPaintInOrder(ctx); - }, - } - ); +export class Polygon extends Polyline { + protected isOpen() { + return false; + } /* _FROM_SVG_START_ */ - /** - * List of attribute names to account for when parsing SVG element (used by `fabric.Polygon.fromElement`) - * @static - * @memberOf fabric.Polygon - * @see: http://www.w3.org/TR/SVG/shapes.html#PolygonElement - */ - fabric.Polygon.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(); /** - * Returns {@link fabric.Polygon} instance from an SVG element + * Returns {@link Polygon} instance from an SVG element * @static - * @memberOf fabric.Polygon + * @memberOf Polygon * @param {SVGElement} element Element to parse * @param {Function} callback callback function invoked after parsing * @param {Object} [options] Options object */ - fabric.Polygon.fromElement = fabric.Polyline.fromElementGenerator('Polygon'); + static fromElement( + element: SVGElement, + callback: (poly: Polygon | null) => any, + options?: any + ) { + return polyFromElement(Polygon, element, callback, options); + } + /* _FROM_SVG_END_ */ /** - * Returns fabric.Polygon instance from an object representation + * Returns Polygon instance from an object representation * @static - * @memberOf fabric.Polygon + * @memberOf Polygon * @param {Object} object Object to create an instance from - * @returns {Promise} + * @returns {Promise} */ - fabric.Polygon.fromObject = function (object) { - return fabric.Object._fromObject(fabric.Polygon, object, { + static fromObject(object: any) { + return FabricObject._fromObject(Polygon, object, { extraParam: 'points', }); - }; -})(typeof exports !== 'undefined' ? exports : window); + } +} + +export const polygonDefaultValues: Partial> = { + type: 'polygon', +}; + +Object.assign(Polygon.prototype, polygonDefaultValues); +/** @todo TODO_JS_MIGRATION remove next line after refactoring build */ +fabric.Polygon = Polygon; diff --git a/src/shapes/polyline.class.ts b/src/shapes/polyline.class.ts index 6584e0348f0..860e652d456 100644 --- a/src/shapes/polyline.class.ts +++ b/src/shapes/polyline.class.ts @@ -1,353 +1,347 @@ -//@ts-nocheck - +import { fabric } from '../../HEADER'; import { config } from '../config'; +import { SHARED_ATTRIBUTES } from '../parser/attributes'; import { parseAttributes } from '../parser/parseAttributes'; import { parsePointsAttribute } from '../parser/parsePointsAttribute'; -import { Point } from '../point.class'; +import { IPoint, Point } from '../point.class'; +import { TClassProperties } from '../typedefs'; import { makeBoundingBoxFromPoints } from '../util/misc/boundingBoxFromPoints'; import { projectStrokeOnPoints } from '../util/misc/projectStroke'; import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; +import { toFixed } from '../util/misc/toFixed'; +import { FabricObject } from './fabricObject.class'; +import { fabricObjectDefaultValues } from './object.class'; -(function (global) { - var fabric = global.fabric || (global.fabric = {}), - extend = fabric.util.object.extend, - toFixed = fabric.util.toFixed; +export function polyFromElement< + T extends { + new (points: IPoint[], options: any): any; + ATTRIBUTE_NAMES: string[]; + } +>( + klass: T, + element: SVGElement, + callback: (poly: InstanceType | null) => any, + options = {} +) { + if (!element) { + return callback(null); + } + const points = parsePointsAttribute(element.getAttribute('points')), + // we omit left and top to instruct the constructor to position the object using the bbox + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { left, top, ...parsedAttributes } = parseAttributes( + element, + klass.ATTRIBUTE_NAMES + ); + callback( + new klass(points || [], { + ...parsedAttributes, + ...options, + fromSVG: true, + }) + ); +} +export class Polyline extends FabricObject { /** - * Polyline class - * @class fabric.Polyline - * @extends fabric.Object - * @see {@link fabric.Polyline#initialize} for constructor definition + * Points array + * @type Array + * @default */ - fabric.Polyline = fabric.util.createClass( - fabric.Object, - /** @lends fabric.Polyline.prototype */ { - /** - * Type of an object - * @type String - * @default - */ - type: 'polyline', + points: IPoint[]; - /** - * Points array - * @type Array - * @default - */ - points: null, + /** + * WARNING: Feature in progress + * Calculate the exact bounding box taking in account strokeWidth on acute angles + * this will be turned to true by default on fabric 6.0 + * maybe will be left in as an optimization since calculations may be slow + * @deprecated + * @type Boolean + * @default false + * @todo set default to true and remove flag and related logic + */ + exactBoundingBox: boolean; - /** - * WARNING: Feature in progress - * Calculate the exact bounding box taking in account strokeWidth on acute angles - * this will be turned to true by default on fabric 6.0 - * maybe will be left in as an optimization since calculations may be slow - * @deprecated - * @type Boolean - * @default false - * @todo set default to true and remove flag and related logic - */ - exactBoundingBox: false, + private initialized: true | undefined; - initialized: false, + /** + * A list of properties that if changed trigger a recalculation of dimensions + * @todo check if you really need to recalculate for all cases + */ + strokeBBoxAffectingProperties: (keyof this)[]; - cacheProperties: fabric.Object.prototype.cacheProperties.concat('points'), + fromSVG: boolean; - /** - * A list of properties that if changed trigger a recalculation of dimensions - * @todo check if you really need to recalculate for all cases - */ - strokeBBoxAffectingProperties: [ - 'skewX', - 'skewY', - 'strokeLineCap', - 'strokeLineJoin', - 'strokeMiterLimit', - 'strokeWidth', - 'strokeUniform', - 'points', - ], + pathOffset: Point; - /** - * Constructor - * @param {Array} points Array of points (where each point is an object with x and y) - * @param {Object} [options] Options object - * @return {fabric.Polyline} thisArg - * @example - * var poly = new fabric.Polyline([ - * { x: 10, y: 10 }, - * { x: 50, y: 30 }, - * { x: 40, y: 70 }, - * { x: 60, y: 50 }, - * { x: 100, y: 150 }, - * { x: 40, y: 100 } - * ], { - * stroke: 'red', - * left: 100, - * top: 100 - * }); - */ - initialize: function (points, options = {}) { - this.points = points || []; - this.callSuper('initialize', options); - this.initialized = true; - const bboxTL = this.setDimensions(); - const origin = this.translateToGivenOrigin( - new Point(options.left ?? bboxTL.x, options.top ?? bboxTL.y), - typeof options.left === 'number' ? this.originX : 'left', - typeof options.top === 'number' ? this.originY : 'top', - this.originX, - this.originY - ); - this.setPositionByOrigin(origin, this.originX, this.originY); - }, + strokeOffset: Point; - /** - * @private - */ - _projectStrokeOnPoints: function () { - return projectStrokeOnPoints(this.points, this, true); - }, + /** + * Constructor + * @param {Array} points Array of points (where each point is an object with x and y) + * @param {Object} [options] Options object + * @return {Polyline} thisArg + * @example + * var poly = new Polyline([ + * { x: 10, y: 10 }, + * { x: 50, y: 30 }, + * { x: 40, y: 70 }, + * { x: 60, y: 50 }, + * { x: 100, y: 150 }, + * { x: 40, y: 100 } + * ], { + * stroke: 'red', + * left: 100, + * top: 100 + * }); + */ + constructor(points: IPoint[] = [], { left, top, ...options }: any = {}) { + super({ points, ...options }); + this.initialized = true; + this.setBoundingBox(true); + typeof left === 'number' && this.set('left', left); + typeof top === 'number' && this.set('top', top); + } - /** - * Calculate the polygon bounding box - * @private - */ - _calcDimensions: function () { - const points = this.exactBoundingBox - ? this._projectStrokeOnPoints().map( - (projection) => projection.projectedPoint - ) - : this.points; - if (points.length === 0) { - return { - left: 0, - top: 0, - width: 0, - height: 0, - pathOffset: new Point(), - }; - } - const bbox = makeBoundingBoxFromPoints(points); - const bboxNoStroke = makeBoundingBoxFromPoints(this.points); - const offsetX = bbox.left + bbox.width / 2, - offsetY = bbox.top + bbox.height / 2; - const pathOffsetX = - offsetX - offsetY * Math.tan(degreesToRadians(this.skewX)); - const pathOffsetY = - offsetY - pathOffsetX * Math.tan(degreesToRadians(this.skewY)); - // TODO: remove next line - const legacyCorrection = - !this.fromSVG && !this.exactBoundingBox ? this.strokeWidth / 2 : 0; - return { - ...bbox, - left: bbox.left - legacyCorrection, - top: bbox.top - legacyCorrection, - pathOffset: new Point(pathOffsetX, pathOffsetY), - strokeOffset: new Point(bboxNoStroke.left, bboxNoStroke.top).subtract( - bbox.left, - bbox.top - ), - }; - }, + protected isOpen() { + return true; + } - /** - * @returns {Point} top left position of the bounding box, useful for complementary positioning - */ - setDimensions: function () { - const { left, top, width, height, pathOffset, strokeOffset } = - this._calcDimensions(); - this.set({ width, height, pathOffset, strokeOffset }); - return new Point(left, top); - }, + private _projectStrokeOnPoints() { + return projectStrokeOnPoints(this.points, this, this.isOpen()); + } + + /** + * Calculate the polygon bounding box + * @private + */ + _calcDimensions() { + const points = this.exactBoundingBox + ? this._projectStrokeOnPoints().map( + (projection) => projection.projectedPoint + ) + : this.points; + if (points.length === 0) { + return { + left: 0, + top: 0, + width: 0, + height: 0, + pathOffset: new Point(), + strokeOffset: new Point(), + }; + } + const bbox = makeBoundingBoxFromPoints(points); + const bboxNoStroke = makeBoundingBoxFromPoints(this.points); + const offsetX = bbox.left + bbox.width / 2, + offsetY = bbox.top + bbox.height / 2; + const pathOffsetX = + offsetX - offsetY * Math.tan(degreesToRadians(this.skewX)); + const pathOffsetY = + offsetY - pathOffsetX * Math.tan(degreesToRadians(this.skewY)); + // TODO: remove next line + const legacyCorrection = + !this.fromSVG && !this.exactBoundingBox ? this.strokeWidth / 2 : 0; + return { + ...bbox, + left: bbox.left - legacyCorrection, + top: bbox.top - legacyCorrection, + pathOffset: new Point(pathOffsetX, pathOffsetY), + strokeOffset: new Point(bboxNoStroke.left, bboxNoStroke.top).subtract( + new Point(bbox.left, bbox.top) + ), + }; + } - /** - * @override stroke is taken in account in size - */ - _getNonTransformedDimensions: function () { - return this.exactBoundingBox - ? new Point(this.width, this.height) - : this.callSuper('_getNonTransformedDimensions'); - }, + setDimensions() { + this.setBoundingBox(); + } - /** - * @override stroke and skewing are taken into account when projecting stroke on points, - * therefore we don't want the default calculation to account for skewing as well - * - * @private - */ - _getTransformedDimensions: function (options) { - return this.exactBoundingBox - ? this.callSuper('_getTransformedDimensions', { - ...(options || {}), - // disable stroke bbox calculations - strokeWidth: 0, - // disable skewing bbox calculations - skewX: 0, - skewY: 0, - }) - : this.callSuper('_getTransformedDimensions', options); - }, + setBoundingBox(adjustPosition?: boolean) { + const { left, top, width, height, pathOffset, strokeOffset } = + this._calcDimensions(); + this.set({ width, height, pathOffset, strokeOffset }); + adjustPosition && + this.setPositionByOrigin(new Point(left, top), 'left', 'top'); + } - /** - * Recalculates dimensions when changing skew and scale - * @private - */ - _set: function (key, value) { - const changed = this.initialized && this[key] !== value; - const output = this.callSuper('_set', key, value); - if ( - changed && - (((key === 'scaleX' || key === 'scaleY') && - this.strokeUniform && - this.strokeBBoxAffectingProperties.includes('strokeUniform') && - this.strokeLineJoin !== 'round') || - this.strokeBBoxAffectingProperties.includes(key)) - ) { - this.setDimensions(); - } - return output; - }, + /** + * @override stroke is taken in account in size + */ + _getNonTransformedDimensions() { + return this.exactBoundingBox + ? new Point(this.width, this.height) + : super._getNonTransformedDimensions(); + } - /** - * Returns object representation of an instance - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} Object representation of an instance - */ - toObject: function (propertiesToInclude) { - return extend(this.callSuper('toObject', propertiesToInclude), { - points: this.points.concat(), - }); - }, + /** + * @override stroke and skewing are taken into account when projecting stroke on points, + * therefore we don't want the default calculation to account for skewing as well + * + * @private + */ + _getTransformedDimensions(options: any) { + return this.exactBoundingBox + ? super._getTransformedDimensions({ + ...(options || {}), + // disable stroke bbox calculations + strokeWidth: 0, + // disable skewing bbox calculations + skewX: 0, + skewY: 0, + }) + : super._getTransformedDimensions(options); + } - /* _TO_SVG_START_ */ - /** - * Returns svg representation of an instance - * @return {Array} an array of strings with the specific svg representation - * of the instance - */ - _toSVG: function () { - var points = [], - diffX = this.pathOffset.x, - diffY = this.pathOffset.y, - NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS; + /** + * Recalculates dimensions when changing skew and scale + * @private + */ + _set(key: string, value: any) { + const changed = this.initialized && this[key as keyof this] !== value; + const output = super._set(key, value); + if ( + changed && + (((key === 'scaleX' || key === 'scaleY') && + this.strokeUniform && + this.strokeBBoxAffectingProperties.includes('strokeUniform') && + this.strokeLineJoin !== 'round') || + this.strokeBBoxAffectingProperties.includes(key as keyof this)) + ) { + this.setDimensions(); + } + return output; + } - for (var i = 0, len = this.points.length; i < len; i++) { - points.push( - toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), - ',', - toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), - ' ' - ); - } - return [ - '<' + this.type + ' ', - 'COMMON_PARTS', - 'points="', - points.join(''), - '" />\n', - ]; - }, - /* _TO_SVG_END_ */ + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject(propertiesToInclude: (keyof this)[]): object { + return { + ...super.toObject(propertiesToInclude), + points: this.points.concat(), + }; + } - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - commonRender: function (ctx) { - var point, - len = this.points.length, - x = this.pathOffset.x, - y = this.pathOffset.y; + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG() { + const points = [], + diffX = this.pathOffset.x, + diffY = this.pathOffset.y, + NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS; - if (!len || isNaN(this.points[len - 1].y)) { - // do not draw if no points or odd points - // NaN comes from parseFloat of a empty string in parser - return false; - } - ctx.beginPath(); - ctx.moveTo(this.points[0].x - x, this.points[0].y - y); - for (var i = 0; i < len; i++) { - point = this.points[i]; - ctx.lineTo(point.x - x, point.y - y); - } - return true; - }, + for (let i = 0, len = this.points.length; i < len; i++) { + points.push( + toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), + ',', + toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), + ' ' + ); + } + return [ + `<${this.type} `, + 'COMMON_PARTS', + `points="${points.join('')}" />\n`, + ]; + } - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function (ctx) { - if (!this.commonRender(ctx)) { - return; - } - this._renderPaintInOrder(ctx); - }, + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render(ctx: CanvasRenderingContext2D) { + const len = this.points.length, + x = this.pathOffset.x, + y = this.pathOffset.y; - /** - * Returns complexity of an instance - * @return {Number} complexity of this instance - */ - complexity: function () { - return this.get('points').length; - }, + if (!len || isNaN(this.points[len - 1].y)) { + // do not draw if no points or odd points + // NaN comes from parseFloat of a empty string in parser + return; } - ); + ctx.beginPath(); + ctx.moveTo(this.points[0].x - x, this.points[0].y - y); + for (let i = 0; i < len; i++) { + const point = this.points[i]; + ctx.lineTo(point.x - x, point.y - y); + } + !this.isOpen() && ctx.closePath(); + this._renderPaintInOrder(ctx); + } + + /** + * Returns complexity of an instance + * @return {Number} complexity of this instance + */ + complexity(): number { + return this.points.length; + } /* _FROM_SVG_START_ */ + /** - * List of attribute names to account for when parsing SVG element (used by {@link fabric.Polyline.fromElement}) + * List of attribute names to account for when parsing SVG element (used by {@link Polyline.fromElement}) * @static - * @memberOf fabric.Polyline + * @memberOf Polyline * @see: http://www.w3.org/TR/SVG/shapes.html#PolylineElement */ - fabric.Polyline.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(); + static ATTRIBUTE_NAMES = [...SHARED_ATTRIBUTES]; /** - * Returns fabric.Polyline instance from an SVG element + * Returns Polyline instance from an SVG element * @static - * @memberOf fabric.Polyline + * @memberOf Polyline * @param {SVGElement} element Element to parser * @param {Function} callback callback function invoked after parsing * @param {Object} [options] Options object */ - fabric.Polyline.fromElementGenerator = function (_class) { - return function (element, callback, options = {}) { - if (!element) { - return callback(null); - } - const points = parsePointsAttribute(element.getAttribute('points')), - // we omit left and top to instruct the constructor to position the object using the bbox - // eslint-disable-next-line @typescript-eslint/no-unused-vars - { left, top, ...parsedAttributes } = parseAttributes( - element, - fabric[_class].ATTRIBUTE_NAMES - ); - callback( - new fabric[_class](points, { - ...parsedAttributes, - ...options, - fromSVG: true, - }) - ); - }; - }; - - fabric.Polyline.fromElement = - fabric.Polyline.fromElementGenerator('Polyline'); + static fromElement( + element: SVGElement, + callback: (poly: Polyline | null) => any, + options?: any + ) { + return polyFromElement(Polyline, element, callback, options); + } /* _FROM_SVG_END_ */ /** - * Returns fabric.Polyline instance from an object representation + * Returns Polyline instance from an object representation * @static - * @memberOf fabric.Polyline + * @memberOf Polyline * @param {Object} object Object to create an instance from - * @returns {Promise} + * @returns {Promise} */ - fabric.Polyline.fromObject = function (object) { - return fabric.Object._fromObject(fabric.Polyline, object, { + static fromObject(object: object): Promise { + return FabricObject._fromObject(Polyline, object, { extraParam: 'points', }); - }; -})(typeof exports !== 'undefined' ? exports : window); + } +} + +export const polylineDefaultValues: Partial> = { + type: 'polyline', + exactBoundingBox: false, + cacheProperties: fabricObjectDefaultValues.cacheProperties.concat('points'), + strokeBBoxAffectingProperties: [ + 'skewX', + 'skewY', + 'strokeLineCap', + 'strokeLineJoin', + 'strokeMiterLimit', + 'strokeWidth', + 'strokeUniform', + 'points', + ], +}; + +Object.assign(Polyline.prototype, polylineDefaultValues); + +/** @todo TODO_JS_MIGRATION remove next line after refactoring build */ +fabric.Polyline = Polyline; diff --git a/test/unit/point.js b/test/unit/point.js index 0096de3d8b3..2384877d5ce 100644 --- a/test/unit/point.js +++ b/test/unit/point.js @@ -11,7 +11,6 @@ assert.ok(point instanceof fabric.Point); assert.ok(point.constructor === fabric.Point); assert.ok(typeof point.constructor === 'function'); - assert.equal(point.type, 'point'); assert.strictEqual(point.x, 0, 'constructor assign x value'); assert.strictEqual(point.y, 0, 'constructor assign y value'); diff --git a/test/unit/polygon.js b/test/unit/polygon.js index c91493341c9..bda87ead65a 100644 --- a/test/unit/polygon.js +++ b/test/unit/polygon.js @@ -58,6 +58,7 @@ var polygon = new fabric.Polygon(getPoints()); assert.ok(polygon instanceof fabric.Polygon); + assert.ok(polygon instanceof fabric.Polyline); assert.ok(polygon instanceof fabric.Object); assert.equal(polygon.type, 'polygon');