@nasser-sw/fabric 7.0.1-beta1 → 7.0.1-beta3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/index.js +504 -102
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.min.js +1 -1
  4. package/dist/index.min.js.map +1 -1
  5. package/dist/index.min.mjs +1 -1
  6. package/dist/index.min.mjs.map +1 -1
  7. package/dist/index.mjs +504 -102
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/index.node.cjs +504 -102
  10. package/dist/index.node.cjs.map +1 -1
  11. package/dist/index.node.mjs +504 -102
  12. package/dist/index.node.mjs.map +1 -1
  13. package/dist/package.json.min.mjs +1 -1
  14. package/dist/package.json.mjs +1 -1
  15. package/dist/src/shapes/Polyline.d.ts +7 -0
  16. package/dist/src/shapes/Polyline.d.ts.map +1 -1
  17. package/dist/src/shapes/Polyline.min.mjs +1 -1
  18. package/dist/src/shapes/Polyline.min.mjs.map +1 -1
  19. package/dist/src/shapes/Polyline.mjs +48 -16
  20. package/dist/src/shapes/Polyline.mjs.map +1 -1
  21. package/dist/src/shapes/Text/Text.d.ts.map +1 -1
  22. package/dist/src/shapes/Text/Text.min.mjs +1 -1
  23. package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
  24. package/dist/src/shapes/Text/Text.mjs +64 -12
  25. package/dist/src/shapes/Text/Text.mjs.map +1 -1
  26. package/dist/src/shapes/Textbox.d.ts.map +1 -1
  27. package/dist/src/shapes/Textbox.min.mjs +1 -1
  28. package/dist/src/shapes/Textbox.min.mjs.map +1 -1
  29. package/dist/src/shapes/Textbox.mjs +2 -52
  30. package/dist/src/shapes/Textbox.mjs.map +1 -1
  31. package/dist/src/shapes/Triangle.d.ts +27 -2
  32. package/dist/src/shapes/Triangle.d.ts.map +1 -1
  33. package/dist/src/shapes/Triangle.min.mjs +1 -1
  34. package/dist/src/shapes/Triangle.min.mjs.map +1 -1
  35. package/dist/src/shapes/Triangle.mjs +72 -12
  36. package/dist/src/shapes/Triangle.mjs.map +1 -1
  37. package/dist/src/text/overlayEditor.d.ts.map +1 -1
  38. package/dist/src/text/overlayEditor.min.mjs +1 -1
  39. package/dist/src/text/overlayEditor.min.mjs.map +1 -1
  40. package/dist/src/text/overlayEditor.mjs +143 -9
  41. package/dist/src/text/overlayEditor.mjs.map +1 -1
  42. package/dist/src/util/misc/cornerRadius.d.ts +70 -0
  43. package/dist/src/util/misc/cornerRadius.d.ts.map +1 -0
  44. package/dist/src/util/misc/cornerRadius.min.mjs +2 -0
  45. package/dist/src/util/misc/cornerRadius.min.mjs.map +1 -0
  46. package/dist/src/util/misc/cornerRadius.mjs +181 -0
  47. package/dist/src/util/misc/cornerRadius.mjs.map +1 -0
  48. package/dist-extensions/src/shapes/Polyline.d.ts +7 -0
  49. package/dist-extensions/src/shapes/Polyline.d.ts.map +1 -1
  50. package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
  51. package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
  52. package/dist-extensions/src/shapes/Triangle.d.ts +27 -2
  53. package/dist-extensions/src/shapes/Triangle.d.ts.map +1 -1
  54. package/dist-extensions/src/text/overlayEditor.d.ts.map +1 -1
  55. package/dist-extensions/src/util/misc/cornerRadius.d.ts +70 -0
  56. package/dist-extensions/src/util/misc/cornerRadius.d.ts.map +1 -0
  57. package/fabric-test-editor.html +1048 -0
  58. package/package.json +164 -164
  59. package/src/shapes/Polyline.ts +70 -29
  60. package/src/shapes/Text/Text.ts +79 -14
  61. package/src/shapes/Textbox.ts +1 -1
  62. package/src/shapes/Triangle.spec.ts +76 -0
  63. package/src/shapes/Triangle.ts +85 -15
  64. package/src/text/overlayEditor.ts +152 -12
  65. package/src/util/misc/cornerRadius.spec.ts +141 -0
  66. package/src/util/misc/cornerRadius.ts +269 -0
@@ -412,7 +412,7 @@ class Cache {
412
412
  }
413
413
  const cache = new Cache();
414
414
 
415
- var version = "7.0.0-beta1";
415
+ var version = "7.0.1-beta2";
416
416
 
417
417
  // use this syntax so babel plugin see this import here
418
418
  const VERSION = version;
@@ -17892,10 +17892,189 @@ _defineProperty(Line, "ATTRIBUTE_NAMES", SHARED_ATTRIBUTES.concat(coordProps));
17892
17892
  classRegistry.setClass(Line);
17893
17893
  classRegistry.setSVGClass(Line);
17894
17894
 
17895
+ /**
17896
+ * Calculate the distance between two points
17897
+ */
17898
+ function pointDistance(p1, p2) {
17899
+ return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
17900
+ }
17901
+
17902
+ /**
17903
+ * Normalize a vector
17904
+ */
17905
+ function normalizeVector(vector) {
17906
+ const length = Math.sqrt(vector.x * vector.x + vector.y * vector.y);
17907
+ if (length === 0) return {
17908
+ x: 0,
17909
+ y: 0
17910
+ };
17911
+ return {
17912
+ x: vector.x / length,
17913
+ y: vector.y / length
17914
+ };
17915
+ }
17916
+
17917
+ /**
17918
+ * Get the maximum allowed radius for a corner based on adjacent edge lengths
17919
+ */
17920
+ function getMaxRadius(prevPoint, currentPoint, nextPoint) {
17921
+ const dist1 = pointDistance(prevPoint, currentPoint);
17922
+ const dist2 = pointDistance(currentPoint, nextPoint);
17923
+ return Math.min(dist1, dist2) / 2;
17924
+ }
17925
+
17926
+ /**
17927
+ * Calculate rounded corner data for a single corner
17928
+ */
17929
+ function calculateRoundedCorner(prevPoint, currentPoint, nextPoint, radius) {
17930
+ // Calculate edge vectors
17931
+ const edge1 = {
17932
+ x: currentPoint.x - prevPoint.x,
17933
+ y: currentPoint.y - prevPoint.y
17934
+ };
17935
+ const edge2 = {
17936
+ x: nextPoint.x - currentPoint.x,
17937
+ y: nextPoint.y - currentPoint.y
17938
+ };
17939
+
17940
+ // Normalize edge vectors
17941
+ const norm1 = normalizeVector(edge1);
17942
+ const norm2 = normalizeVector(edge2);
17943
+
17944
+ // Calculate the maximum allowed radius
17945
+ const maxRadius = getMaxRadius(prevPoint, currentPoint, nextPoint);
17946
+ const actualRadius = Math.min(radius, maxRadius);
17947
+
17948
+ // Calculate start and end points of the rounded corner
17949
+ const startPoint = {
17950
+ x: currentPoint.x - norm1.x * actualRadius,
17951
+ y: currentPoint.y - norm1.y * actualRadius
17952
+ };
17953
+ const endPoint = {
17954
+ x: currentPoint.x + norm2.x * actualRadius,
17955
+ y: currentPoint.y + norm2.y * actualRadius
17956
+ };
17957
+
17958
+ // Calculate control points for bezier curve
17959
+ // Using the magic number kRect for optimal circular approximation
17960
+ const controlOffset = actualRadius * kRect;
17961
+ const cp1 = {
17962
+ x: startPoint.x + norm1.x * controlOffset,
17963
+ y: startPoint.y + norm1.y * controlOffset
17964
+ };
17965
+ const cp2 = {
17966
+ x: endPoint.x - norm2.x * controlOffset,
17967
+ y: endPoint.y - norm2.y * controlOffset
17968
+ };
17969
+ return {
17970
+ corner: currentPoint,
17971
+ start: startPoint,
17972
+ end: endPoint,
17973
+ cp1,
17974
+ cp2,
17975
+ actualRadius
17976
+ };
17977
+ }
17978
+
17979
+ /**
17980
+ * Apply corner radius to a polygon defined by points
17981
+ */
17982
+ function applyCornerRadiusToPolygon(points, radius) {
17983
+ let radiusAsPercentage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
17984
+ if (points.length < 3) {
17985
+ throw new Error('Polygon must have at least 3 points');
17986
+ }
17987
+
17988
+ // Calculate bounding box if radius is percentage-based
17989
+ let actualRadius = radius;
17990
+ if (radiusAsPercentage) {
17991
+ const minX = Math.min(...points.map(p => p.x));
17992
+ const maxX = Math.max(...points.map(p => p.x));
17993
+ const minY = Math.min(...points.map(p => p.y));
17994
+ const maxY = Math.max(...points.map(p => p.y));
17995
+ const width = maxX - minX;
17996
+ const height = maxY - minY;
17997
+ const minDimension = Math.min(width, height);
17998
+ actualRadius = radius / 100 * minDimension;
17999
+ }
18000
+ const roundedCorners = [];
18001
+ for (let i = 0; i < points.length; i++) {
18002
+ const prevIndex = (i - 1 + points.length) % points.length;
18003
+ const nextIndex = (i + 1) % points.length;
18004
+ const prevPoint = points[prevIndex];
18005
+ const currentPoint = points[i];
18006
+ const nextPoint = points[nextIndex];
18007
+ const roundedCorner = calculateRoundedCorner(prevPoint, currentPoint, nextPoint, actualRadius);
18008
+ roundedCorners.push(roundedCorner);
18009
+ }
18010
+ return roundedCorners;
18011
+ }
18012
+
18013
+ /**
18014
+ * Render a rounded polygon to a canvas context
18015
+ */
18016
+ function renderRoundedPolygon(ctx, roundedCorners) {
18017
+ let closed = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
18018
+ if (roundedCorners.length === 0) return;
18019
+ ctx.beginPath();
18020
+
18021
+ // Start at the first corner's start point
18022
+ const firstCorner = roundedCorners[0];
18023
+ ctx.moveTo(firstCorner.start.x, firstCorner.start.y);
18024
+ for (let i = 0; i < roundedCorners.length; i++) {
18025
+ const corner = roundedCorners[i];
18026
+ const nextIndex = (i + 1) % roundedCorners.length;
18027
+ const nextCorner = roundedCorners[nextIndex];
18028
+
18029
+ // Draw the rounded corner using bezier curve
18030
+ ctx.bezierCurveTo(corner.cp1.x, corner.cp1.y, corner.cp2.x, corner.cp2.y, corner.end.x, corner.end.y);
18031
+
18032
+ // Draw line to next corner's start point (if not the last segment in open path)
18033
+ if (i < roundedCorners.length - 1 || closed) {
18034
+ ctx.lineTo(nextCorner.start.x, nextCorner.start.y);
18035
+ }
18036
+ }
18037
+ if (closed) {
18038
+ ctx.closePath();
18039
+ }
18040
+ }
18041
+
18042
+ /**
18043
+ * Generate SVG path data for a rounded polygon
18044
+ */
18045
+ function generateRoundedPolygonPath(roundedCorners) {
18046
+ let closed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
18047
+ if (roundedCorners.length === 0) return '';
18048
+ const pathData = [];
18049
+ const firstCorner = roundedCorners[0];
18050
+
18051
+ // Move to first corner's start point
18052
+ pathData.push(`M ${firstCorner.start.x} ${firstCorner.start.y}`);
18053
+ for (let i = 0; i < roundedCorners.length; i++) {
18054
+ const corner = roundedCorners[i];
18055
+ const nextIndex = (i + 1) % roundedCorners.length;
18056
+ const nextCorner = roundedCorners[nextIndex];
18057
+
18058
+ // Add bezier curve for the rounded corner
18059
+ pathData.push(`C ${corner.cp1.x} ${corner.cp1.y} ${corner.cp2.x} ${corner.cp2.y} ${corner.end.x} ${corner.end.y}`);
18060
+
18061
+ // Add line to next corner's start point (if not the last segment in open path)
18062
+ if (i < roundedCorners.length - 1 || closed) {
18063
+ pathData.push(`L ${nextCorner.start.x} ${nextCorner.start.y}`);
18064
+ }
18065
+ }
18066
+ if (closed) {
18067
+ pathData.push('Z');
18068
+ }
18069
+ return pathData.join(' ');
18070
+ }
18071
+
17895
18072
  const triangleDefaultValues = {
17896
18073
  width: 100,
17897
- height: 100
18074
+ height: 100,
18075
+ cornerRadius: 0
17898
18076
  };
18077
+ const TRIANGLE_PROPS = ['cornerRadius'];
17899
18078
  class Triangle extends FabricObject {
17900
18079
  static getDefaults() {
17901
18080
  return {
@@ -17914,34 +18093,90 @@ class Triangle extends FabricObject {
17914
18093
  this.setOptions(options);
17915
18094
  }
17916
18095
 
18096
+ /**
18097
+ * Get triangle points as an array of XY coordinates
18098
+ * @private
18099
+ */
18100
+ _getTrianglePoints() {
18101
+ const widthBy2 = this.width / 2;
18102
+ const heightBy2 = this.height / 2;
18103
+ return [{
18104
+ x: -widthBy2,
18105
+ y: heightBy2
18106
+ },
18107
+ // bottom left
18108
+ {
18109
+ x: 0,
18110
+ y: -heightBy2
18111
+ },
18112
+ // top center
18113
+ {
18114
+ x: widthBy2,
18115
+ y: heightBy2
18116
+ } // bottom right
18117
+ ];
18118
+ }
18119
+
17917
18120
  /**
17918
18121
  * @private
17919
18122
  * @param {CanvasRenderingContext2D} ctx Context to render on
17920
18123
  */
17921
18124
  _render(ctx) {
17922
- const widthBy2 = this.width / 2,
17923
- heightBy2 = this.height / 2;
17924
- ctx.beginPath();
17925
- ctx.moveTo(-widthBy2, heightBy2);
17926
- ctx.lineTo(0, -heightBy2);
17927
- ctx.lineTo(widthBy2, heightBy2);
17928
- ctx.closePath();
18125
+ if (this.cornerRadius > 0) {
18126
+ // Render rounded triangle
18127
+ const points = this._getTrianglePoints();
18128
+ const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
18129
+ renderRoundedPolygon(ctx, roundedCorners, true);
18130
+ } else {
18131
+ // Render sharp triangle (original implementation)
18132
+ const widthBy2 = this.width / 2;
18133
+ const heightBy2 = this.height / 2;
18134
+ ctx.beginPath();
18135
+ ctx.moveTo(-widthBy2, heightBy2);
18136
+ ctx.lineTo(0, -heightBy2);
18137
+ ctx.lineTo(widthBy2, heightBy2);
18138
+ ctx.closePath();
18139
+ }
17929
18140
  this._renderPaintInOrder(ctx);
17930
18141
  }
17931
18142
 
18143
+ /**
18144
+ * Returns object representation of an instance
18145
+ * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
18146
+ * @return {Object} object representation of an instance
18147
+ */
18148
+ toObject() {
18149
+ let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
18150
+ return super.toObject([...TRIANGLE_PROPS, ...propertiesToInclude]);
18151
+ }
18152
+
17932
18153
  /**
17933
18154
  * Returns svg representation of an instance
17934
18155
  * @return {Array} an array of strings with the specific svg representation
17935
18156
  * of the instance
17936
18157
  */
17937
18158
  _toSVG() {
17938
- const widthBy2 = this.width / 2,
17939
- heightBy2 = this.height / 2,
17940
- points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
17941
- return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
18159
+ if (this.cornerRadius > 0) {
18160
+ // Generate rounded triangle as path
18161
+ const points = this._getTrianglePoints();
18162
+ const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
18163
+ const pathData = generateRoundedPolygonPath(roundedCorners, true);
18164
+ return ['<path ', 'COMMON_PARTS', `d="${pathData}" />`];
18165
+ } else {
18166
+ // Original sharp triangle implementation
18167
+ const widthBy2 = this.width / 2;
18168
+ const heightBy2 = this.height / 2;
18169
+ const points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
18170
+ return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
18171
+ }
17942
18172
  }
17943
18173
  }
18174
+ /**
18175
+ * Corner radius for rounded triangle corners
18176
+ * @type Number
18177
+ */
17944
18178
  _defineProperty(Triangle, "type", 'Triangle');
18179
+ _defineProperty(Triangle, "cacheProperties", [...cacheProperties, ...TRIANGLE_PROPS]);
17945
18180
  _defineProperty(Triangle, "ownDefaults", triangleDefaultValues);
17946
18181
  classRegistry.setClass(Triangle);
17947
18182
  classRegistry.setSVGClass(Triangle);
@@ -18106,7 +18341,8 @@ const polylineDefaultValues = {
18106
18341
  /**
18107
18342
  * @deprecated transient option soon to be removed in favor of a different design
18108
18343
  */
18109
- exactBoundingBox: false
18344
+ exactBoundingBox: false,
18345
+ cornerRadius: 0
18110
18346
  };
18111
18347
  class Polyline extends FabricObject {
18112
18348
  static getDefaults() {
@@ -18320,7 +18556,7 @@ class Polyline extends FabricObject {
18320
18556
  toObject() {
18321
18557
  let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
18322
18558
  return {
18323
- ...super.toObject(propertiesToInclude),
18559
+ ...super.toObject(['cornerRadius', ...propertiesToInclude]),
18324
18560
  points: this.points.map(_ref => {
18325
18561
  let {
18326
18562
  x,
@@ -18340,14 +18576,28 @@ class Polyline extends FabricObject {
18340
18576
  * of the instance
18341
18577
  */
18342
18578
  _toSVG() {
18343
- const points = [],
18344
- diffX = this.pathOffset.x,
18345
- diffY = this.pathOffset.y,
18346
- NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS;
18347
- for (let i = 0, len = this.points.length; i < len; i++) {
18348
- points.push(toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ');
18579
+ if (this.cornerRadius > 0 && this.points.length >= 3) {
18580
+ // Generate rounded polygon/polyline as path
18581
+ const diffX = this.pathOffset.x;
18582
+ const diffY = this.pathOffset.y;
18583
+ const adjustedPoints = this.points.map(point => ({
18584
+ x: point.x - diffX,
18585
+ y: point.y - diffY
18586
+ }));
18587
+ const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
18588
+ const pathData = generateRoundedPolygonPath(roundedCorners, !this.isOpen());
18589
+ return ['<path ', 'COMMON_PARTS', `d="${pathData}" />\n`];
18590
+ } else {
18591
+ // Original sharp corners implementation
18592
+ const points = [];
18593
+ const diffX = this.pathOffset.x;
18594
+ const diffY = this.pathOffset.y;
18595
+ const NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS;
18596
+ for (let i = 0, len = this.points.length; i < len; i++) {
18597
+ points.push(toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ');
18598
+ }
18599
+ return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
18349
18600
  }
18350
- return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
18351
18601
  }
18352
18602
 
18353
18603
  /**
@@ -18363,13 +18613,24 @@ class Polyline extends FabricObject {
18363
18613
  // NaN comes from parseFloat of a empty string in parser
18364
18614
  return;
18365
18615
  }
18366
- ctx.beginPath();
18367
- ctx.moveTo(this.points[0].x - x, this.points[0].y - y);
18368
- for (let i = 0; i < len; i++) {
18369
- const point = this.points[i];
18370
- ctx.lineTo(point.x - x, point.y - y);
18616
+ if (this.cornerRadius > 0 && len >= 3) {
18617
+ // Render with rounded corners
18618
+ const adjustedPoints = this.points.map(point => ({
18619
+ x: point.x - x,
18620
+ y: point.y - y
18621
+ }));
18622
+ const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
18623
+ renderRoundedPolygon(ctx, roundedCorners, !this.isOpen());
18624
+ } else {
18625
+ // Original sharp corners implementation
18626
+ ctx.beginPath();
18627
+ ctx.moveTo(this.points[0].x - x, this.points[0].y - y);
18628
+ for (let i = 0; i < len; i++) {
18629
+ const point = this.points[i];
18630
+ ctx.lineTo(point.x - x, point.y - y);
18631
+ }
18632
+ !this.isOpen() && ctx.closePath();
18371
18633
  }
18372
- !this.isOpen() && ctx.closePath();
18373
18634
  this._renderPaintInOrder(ctx);
18374
18635
  }
18375
18636
 
@@ -18434,10 +18695,15 @@ class Polyline extends FabricObject {
18434
18695
  * @type Boolean
18435
18696
  * @default false
18436
18697
  */
18698
+ /**
18699
+ * Corner radius for rounded corners
18700
+ * @type Number
18701
+ * @default 0
18702
+ */
18437
18703
  _defineProperty(Polyline, "ownDefaults", polylineDefaultValues);
18438
18704
  _defineProperty(Polyline, "type", 'Polyline');
18439
18705
  _defineProperty(Polyline, "layoutProperties", [SKEW_X, SKEW_Y, 'strokeLineCap', 'strokeLineJoin', 'strokeMiterLimit', 'strokeWidth', 'strokeUniform', 'points']);
18440
- _defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points']);
18706
+ _defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points', 'cornerRadius']);
18441
18707
  _defineProperty(Polyline, "ATTRIBUTE_NAMES", [...SHARED_ATTRIBUTES]);
18442
18708
  classRegistry.setClass(Polyline);
18443
18709
  classRegistry.setSVGClass(Polyline);
@@ -20247,6 +20513,7 @@ class FabricText extends StyledText {
20247
20513
  */
20248
20514
  enlargeSpaces() {
20249
20515
  let diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces;
20516
+ const isRtl = this.direction === 'rtl';
20250
20517
  for (let i = 0, len = this._textLines.length; i < len; i++) {
20251
20518
  if (this.textAlign !== JUSTIFY && (i === len - 1 || this.isEndOfWrapping(i))) {
20252
20519
  continue;
@@ -20257,15 +20524,44 @@ class FabricText extends StyledText {
20257
20524
  if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) {
20258
20525
  numberOfSpaces = spaces.length;
20259
20526
  diffSpace = (this.width - currentLineWidth) / numberOfSpaces;
20260
- for (let j = 0; j <= line.length; j++) {
20261
- charBound = this.__charBounds[i][j];
20262
- if (this._reSpaceAndTab.test(line[j])) {
20263
- charBound.width += diffSpace;
20264
- charBound.kernedWidth += diffSpace;
20265
- charBound.left += accumulatedSpace;
20266
- accumulatedSpace += diffSpace;
20267
- } else {
20268
- charBound.left += accumulatedSpace;
20527
+ if (isRtl) {
20528
+ for (let j = 0; j < line.length; j++) {
20529
+ if (this._reSpaceAndTab.test(line[j])) ;
20530
+ }
20531
+
20532
+ // For RTL, we need to work backwards through the visual positions
20533
+ // but still update logical positions correctly
20534
+ let spaceCount = 0;
20535
+ for (let j = 0; j <= line.length; j++) {
20536
+ charBound = this.__charBounds[i][j];
20537
+ if (charBound) {
20538
+ if (this._reSpaceAndTab.test(line[j])) {
20539
+ charBound.width += diffSpace;
20540
+ charBound.kernedWidth += diffSpace;
20541
+ spaceCount++;
20542
+ }
20543
+
20544
+ // For RTL, shift all characters to the right by the total expansion
20545
+ // minus the expansion that comes after this character
20546
+ const remainingSpaces = numberOfSpaces - spaceCount;
20547
+ const shiftAmount = remainingSpaces * diffSpace;
20548
+ charBound.left += shiftAmount;
20549
+ }
20550
+ }
20551
+ } else {
20552
+ // LTR processing (original logic)
20553
+ for (let j = 0; j <= line.length; j++) {
20554
+ charBound = this.__charBounds[i][j];
20555
+ if (charBound) {
20556
+ if (this._reSpaceAndTab.test(line[j])) {
20557
+ charBound.width += diffSpace;
20558
+ charBound.kernedWidth += diffSpace;
20559
+ charBound.left += accumulatedSpace;
20560
+ accumulatedSpace += diffSpace;
20561
+ } else {
20562
+ charBound.left += accumulatedSpace;
20563
+ }
20564
+ }
20269
20565
  }
20270
20566
  }
20271
20567
  }
@@ -20923,7 +21219,15 @@ class FabricText extends StyledText {
20923
21219
  if (currentDirection !== this.direction) {
20924
21220
  ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl');
20925
21221
  ctx.direction = isLtr ? 'ltr' : 'rtl';
20926
- ctx.textAlign = isLtr ? LEFT : RIGHT;
21222
+
21223
+ // For justify alignments, we need to set the correct canvas text alignment
21224
+ // This is crucial for RTL text to render in the correct order
21225
+ if (isJustify) {
21226
+ // Justify uses LEFT alignment as a base, letting the character positioning handle justification
21227
+ ctx.textAlign = LEFT;
21228
+ } else {
21229
+ ctx.textAlign = isLtr ? LEFT : RIGHT;
21230
+ }
20927
21231
  }
20928
21232
  top -= lineHeight * this._fontSizeFraction / this.lineHeight;
20929
21233
  if (shortCut) {
@@ -21159,9 +21463,21 @@ class FabricText extends StyledText {
21159
21463
  direction = this.direction,
21160
21464
  isEndOfWrapping = this.isEndOfWrapping(lineIndex);
21161
21465
  let leftOffset = 0;
21162
- if (textAlign === JUSTIFY || textAlign === JUSTIFY_CENTER && !isEndOfWrapping || textAlign === JUSTIFY_RIGHT && !isEndOfWrapping || textAlign === JUSTIFY_LEFT && !isEndOfWrapping) {
21163
- return 0;
21466
+
21467
+ // Handle justify alignments (excluding last lines and wrapped line ends)
21468
+ const isJustifyLine = textAlign === JUSTIFY || textAlign === JUSTIFY_CENTER && !isEndOfWrapping || textAlign === JUSTIFY_RIGHT && !isEndOfWrapping || textAlign === JUSTIFY_LEFT && !isEndOfWrapping;
21469
+ if (isJustifyLine) {
21470
+ // Justify lines should start at the left edge for LTR and right edge for RTL
21471
+ // The space distribution is handled by enlargeSpaces()
21472
+ if (direction === 'rtl') {
21473
+ // For RTL justify, we need to account for the line being right-aligned
21474
+ return 0;
21475
+ } else {
21476
+ return 0;
21477
+ }
21164
21478
  }
21479
+
21480
+ // Handle non-justify alignments
21165
21481
  if (textAlign === CENTER) {
21166
21482
  leftOffset = lineDiff / 2;
21167
21483
  }
@@ -21174,6 +21490,8 @@ class FabricText extends StyledText {
21174
21490
  if (textAlign === JUSTIFY_RIGHT) {
21175
21491
  leftOffset = lineDiff;
21176
21492
  }
21493
+
21494
+ // Apply RTL adjustments for non-justify alignments
21177
21495
  if (direction === 'rtl') {
21178
21496
  if (textAlign === RIGHT || textAlign === JUSTIFY || textAlign === JUSTIFY_RIGHT) {
21179
21497
  leftOffset = 0;
@@ -22169,13 +22487,86 @@ class OverlayEditor {
22169
22487
  this.textarea.style.fontFamily = target.fontFamily || 'Arial';
22170
22488
  this.textarea.style.fontWeight = String(target.fontWeight || 'normal');
22171
22489
  this.textarea.style.fontStyle = target.fontStyle || 'normal';
22172
- this.textarea.style.textAlign = target.textAlign || 'left';
22490
+ // Handle text alignment and justification
22491
+ const textAlign = target.textAlign || 'left';
22492
+ let cssTextAlign = textAlign;
22493
+
22494
+ // Detect text direction from content for proper justify handling
22495
+ const autoDetectedDirection = this.firstStrongDir(this.textarea.value || '');
22496
+
22497
+ // DEBUG: Log alignment details
22498
+ console.log('🔍 ALIGNMENT DEBUG:');
22499
+ console.log(' Fabric textAlign:', textAlign);
22500
+ console.log(' Fabric direction:', target.direction);
22501
+ console.log(' Text content:', JSON.stringify(target.text));
22502
+ console.log(' Detected direction:', autoDetectedDirection);
22503
+
22504
+ // Map fabric.js justify to CSS
22505
+ if (textAlign.includes('justify')) {
22506
+ // Try to match fabric.js justify behavior more precisely
22507
+ try {
22508
+ // For justify, we need to replicate fabric.js space expansion
22509
+ // Use CSS justify but with specific settings to match fabric.js better
22510
+ cssTextAlign = 'justify';
22511
+
22512
+ // Set text-align-last based on justify type and detected direction
22513
+ // Smart justify: respect detected direction even when fabric alignment doesn't match
22514
+ if (textAlign === 'justify') {
22515
+ this.textarea.style.textAlignLast = autoDetectedDirection === 'rtl' ? 'right' : 'left';
22516
+ } else if (textAlign === 'justify-left') {
22517
+ // If text is RTL but fabric says justify-left, override to justify-right for better UX
22518
+ if (autoDetectedDirection === 'rtl') {
22519
+ this.textarea.style.textAlignLast = 'right';
22520
+ console.log(' → Overrode justify-left to justify-right for RTL text');
22521
+ } else {
22522
+ this.textarea.style.textAlignLast = 'left';
22523
+ }
22524
+ } else if (textAlign === 'justify-right') {
22525
+ // If text is LTR but fabric says justify-right, override to justify-left for better UX
22526
+ if (autoDetectedDirection === 'ltr') {
22527
+ this.textarea.style.textAlignLast = 'left';
22528
+ console.log(' → Overrode justify-right to justify-left for LTR text');
22529
+ } else {
22530
+ this.textarea.style.textAlignLast = 'right';
22531
+ }
22532
+ } else if (textAlign === 'justify-center') {
22533
+ this.textarea.style.textAlignLast = 'center';
22534
+ }
22535
+
22536
+ // Enhanced justify settings for better fabric.js matching
22537
+ this.textarea.style.textJustify = 'inter-word';
22538
+ this.textarea.style.wordSpacing = 'normal';
22539
+
22540
+ // Additional CSS properties for better justify matching
22541
+ this.textarea.style.textAlign = 'justify';
22542
+ this.textarea.style.textAlignLast = this.textarea.style.textAlignLast;
22543
+
22544
+ // Try to force better justify behavior
22545
+ this.textarea.style.textJustifyTrim = 'none';
22546
+ this.textarea.style.textAutospace = 'none';
22547
+ console.log(' → Applied justify alignment:', textAlign, 'with last-line:', this.textarea.style.textAlignLast);
22548
+ } catch (error) {
22549
+ console.warn(' → Justify setup failed, falling back to standard alignment:', error);
22550
+ cssTextAlign = textAlign.replace('justify-', '').replace('justify', 'left');
22551
+ }
22552
+ } else {
22553
+ this.textarea.style.textAlignLast = 'auto';
22554
+ this.textarea.style.textJustify = 'auto';
22555
+ this.textarea.style.wordSpacing = 'normal';
22556
+ console.log(' → Applied standard alignment:', cssTextAlign);
22557
+ }
22558
+ this.textarea.style.textAlign = cssTextAlign;
22173
22559
  this.textarea.style.color = ((_target$fill = target.fill) === null || _target$fill === void 0 ? void 0 : _target$fill.toString()) || '#000';
22174
22560
  this.textarea.style.letterSpacing = `${letterSpacingPx}px`;
22175
- this.textarea.style.direction = target.direction || this.firstStrongDir(this.textarea.value || '');
22561
+
22562
+ // Use the already detected direction from above
22563
+ const fabricDirection = target.direction;
22564
+
22565
+ // Use auto-detected direction for better BiDi support, but respect fabric direction if it makes sense
22566
+ this.textarea.style.direction = autoDetectedDirection || fabricDirection || 'ltr';
22176
22567
  this.textarea.style.fontVariant = 'normal';
22177
22568
  this.textarea.style.fontStretch = 'normal';
22178
- this.textarea.style.textRendering = 'optimizeLegibility';
22569
+ this.textarea.style.textRendering = 'auto'; // Changed from 'optimizeLegibility' to match canvas
22179
22570
  this.textarea.style.fontKerning = 'normal';
22180
22571
  this.textarea.style.fontFeatureSettings = 'normal';
22181
22572
  this.textarea.style.fontVariationSettings = 'normal';
@@ -22186,14 +22577,58 @@ class OverlayEditor {
22186
22577
  this.textarea.style.overflowWrap = 'break-word';
22187
22578
  this.textarea.style.whiteSpace = 'pre-wrap';
22188
22579
  this.textarea.style.hyphens = 'none';
22189
- this.textarea.style.webkitFontSmoothing = 'antialiased';
22190
- this.textarea.style.mozOsxFontSmoothing = 'grayscale';
22191
22580
 
22192
- // Debug: Compare textarea and canvas object bounding boxes
22193
- this.debugBoundingBoxComparison();
22581
+ // DEBUG: Log final CSS properties
22582
+ console.log('🎨 FINAL TEXTAREA CSS:');
22583
+ console.log(' textAlign:', this.textarea.style.textAlign);
22584
+ console.log(' textAlignLast:', this.textarea.style.textAlignLast);
22585
+ console.log(' direction:', this.textarea.style.direction);
22586
+ console.log(' unicodeBidi:', this.textarea.style.unicodeBidi);
22587
+ console.log(' width:', this.textarea.style.width);
22588
+ console.log(' textJustify:', this.textarea.style.textJustify);
22589
+ console.log(' wordSpacing:', this.textarea.style.wordSpacing);
22590
+ console.log(' whiteSpace:', this.textarea.style.whiteSpace);
22591
+
22592
+ // If justify, log Fabric object dimensions for comparison
22593
+ if (textAlign.includes('justify')) {
22594
+ var _calcTextWidth, _ref;
22595
+ console.log('🔧 FABRIC OBJECT JUSTIFY INFO:');
22596
+ console.log(' Fabric width:', target.width);
22597
+ console.log(' Fabric calcTextWidth:', (_calcTextWidth = (_ref = target).calcTextWidth) === null || _calcTextWidth === void 0 ? void 0 : _calcTextWidth.call(_ref));
22598
+ console.log(' Fabric textAlign:', target.textAlign);
22599
+ console.log(' Text lines:', target.textLines);
22600
+ }
22601
+
22602
+ // Debug font properties matching
22603
+ console.log('🔤 FONT PROPERTIES COMPARISON:');
22604
+ console.log(' Fabric fontFamily:', target.fontFamily);
22605
+ console.log(' Fabric fontWeight:', target.fontWeight);
22606
+ console.log(' Fabric fontStyle:', target.fontStyle);
22607
+ console.log(' Fabric fontSize:', target.fontSize);
22608
+ console.log(' → Textarea fontFamily:', this.textarea.style.fontFamily);
22609
+ console.log(' → Textarea fontWeight:', this.textarea.style.fontWeight);
22610
+ console.log(' → Textarea fontStyle:', this.textarea.style.fontStyle);
22611
+ console.log(' → Textarea fontSize:', this.textarea.style.fontSize);
22612
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
22194
22613
 
22195
- // Debug: Compare text wrapping behavior
22196
- this.debugTextWrapping();
22614
+ // Enhanced font rendering to better match fabric.js canvas rendering
22615
+ // Default to auto for more natural rendering
22616
+ this.textarea.style.webkitFontSmoothing = 'auto';
22617
+ this.textarea.style.mozOsxFontSmoothing = 'auto';
22618
+ this.textarea.style.fontSmooth = 'auto';
22619
+ this.textarea.style.textSizeAdjust = 'none';
22620
+
22621
+ // For bold fonts, use subpixel rendering to match canvas thickness better
22622
+ const fontWeight = String(target.fontWeight || 'normal');
22623
+ const isBold = fontWeight === 'bold' || fontWeight === '700' || parseInt(fontWeight) >= 600;
22624
+ if (isBold) {
22625
+ this.textarea.style.webkitFontSmoothing = 'subpixel-antialiased';
22626
+ this.textarea.style.mozOsxFontSmoothing = 'unset';
22627
+ console.log('🔤 Applied enhanced bold rendering for better thickness matching');
22628
+ }
22629
+ console.log('🎨 FONT SMOOTHING APPLIED:');
22630
+ console.log(' webkitFontSmoothing:', this.textarea.style.webkitFontSmoothing);
22631
+ console.log(' mozOsxFontSmoothing:', this.textarea.style.mozOsxFontSmoothing);
22197
22632
 
22198
22633
  // Initial bounds are set correctly by Fabric.js - don't force update here
22199
22634
  }
@@ -22428,6 +22863,23 @@ class OverlayEditor {
22428
22863
  // Handle commit/cancel after restoring visibility
22429
22864
  if (commit && !this.isComposing) {
22430
22865
  const finalText = this.textarea.value;
22866
+
22867
+ // Auto-detect text direction and update fabric object if needed
22868
+ const detectedDirection = this.firstStrongDir(finalText);
22869
+ const currentDirection = this.target.direction || 'ltr';
22870
+ if (detectedDirection && detectedDirection !== currentDirection) {
22871
+ console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
22872
+ console.log(` Text content: "${finalText.substring(0, 50)}..."`);
22873
+
22874
+ // Update the fabric object's direction
22875
+ this.target.set('direction', detectedDirection);
22876
+
22877
+ // Force a re-render to apply the direction change
22878
+ this.canvas.requestRenderAll();
22879
+ console.log(`✅ Fabric object direction updated to: ${detectedDirection}`);
22880
+ } else {
22881
+ console.log(`📝 Overlay Exit: Direction unchanged (${currentDirection}), text: "${finalText.substring(0, 30)}..."`);
22882
+ }
22431
22883
  if (this.onCommit) {
22432
22884
  this.onCommit(finalText);
22433
22885
  }
@@ -26018,30 +26470,17 @@ class Textbox extends IText {
26018
26470
  // Detect resize origin during resizing
26019
26471
  this.on('resizing', e => {
26020
26472
  // Check transform origin to determine which side is being resized
26021
- console.log('🔍 Resize event data:', e);
26022
26473
  if (e.transform) {
26023
26474
  const {
26024
- originX,
26025
- originY
26475
+ originX
26026
26476
  } = e.transform;
26027
- console.log('🔍 Transform origins:', {
26028
- originX,
26029
- originY
26030
- });
26031
26477
  // originX tells us which side is the anchor - opposite side is being dragged
26032
26478
  resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
26033
- console.log('🎯 Setting resizeOrigin to:', resizeOrigin);
26034
26479
  } else if (e.originX) {
26035
26480
  const {
26036
- originX,
26037
- originY
26481
+ originX
26038
26482
  } = e;
26039
- console.log('🔍 Event origins:', {
26040
- originX,
26041
- originY
26042
- });
26043
26483
  resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
26044
- console.log('🎯 Setting resizeOrigin to:', resizeOrigin);
26045
26484
  }
26046
26485
  });
26047
26486
 
@@ -26049,9 +26488,6 @@ class Textbox extends IText {
26049
26488
  // Use 'modified' event which fires after user releases the mouse
26050
26489
  this.on('modified', () => {
26051
26490
  const currentResizeOrigin = resizeOrigin; // Capture the value before reset
26052
- console.log('✅ Modified event fired - resize complete, triggering safety snap', {
26053
- resizeOrigin: currentResizeOrigin
26054
- });
26055
26491
  // Small delay to ensure text layout is updated
26056
26492
  setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
26057
26493
  resizeOrigin = null; // Reset after capturing
@@ -26061,7 +26497,6 @@ class Textbox extends IText {
26061
26497
  (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.on('object:modified', e => {
26062
26498
  if (e.target === this) {
26063
26499
  const currentResizeOrigin = resizeOrigin; // Capture the value before reset
26064
- console.log('✅ Canvas object:modified fired for this textbox');
26065
26500
  setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
26066
26501
  resizeOrigin = null; // Reset after capturing
26067
26502
  }
@@ -26076,38 +26511,17 @@ class Textbox extends IText {
26076
26511
  * @param resizeOrigin - Which side was used for resizing ('left' or 'right')
26077
26512
  */
26078
26513
  safetySnapWidth(resizeOrigin) {
26079
- var _this$_textLines;
26080
- console.log('🔍 safetySnapWidth called', {
26081
- isWrapping: this.isWrapping,
26082
- hasTextLines: !!this._textLines,
26083
- lineCount: ((_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length) || 0,
26084
- currentWidth: this.width,
26085
- type: this.type,
26086
- text: this.text
26087
- });
26088
-
26089
26514
  // For Textbox objects, we always want to check for clipping regardless of isWrapping flag
26090
26515
  if (!this._textLines || this.type.toLowerCase() !== 'textbox' || this._textLines.length === 0) {
26091
- var _this$_textLines2;
26092
- console.log('❌ Early return - missing requirements', {
26093
- hasTextLines: !!this._textLines,
26094
- typeMatch: this.type.toLowerCase() === 'textbox',
26095
- actualType: this.type,
26096
- hasLines: ((_this$_textLines2 = this._textLines) === null || _this$_textLines2 === void 0 ? void 0 : _this$_textLines2.length) > 0
26097
- });
26098
26516
  return;
26099
26517
  }
26100
26518
  const lineCount = this._textLines.length;
26101
26519
  if (lineCount === 0) return;
26102
-
26103
- // Check all lines, not just the last one
26104
- let maxActualLineWidth = 0; // Actual measured width without buffers
26105
26520
  let maxRequiredWidth = 0; // Width including RTL buffer
26106
26521
 
26107
26522
  for (let i = 0; i < lineCount; i++) {
26108
26523
  const lineText = this._textLines[i].join(''); // Convert grapheme array to string
26109
26524
  const lineWidth = this.getLineWidth(i);
26110
- maxActualLineWidth = Math.max(maxActualLineWidth, lineWidth);
26111
26525
 
26112
26526
  // RTL detection - regex for Arabic, Hebrew, and other RTL characters
26113
26527
  const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/;
@@ -26127,11 +26541,6 @@ class Textbox extends IText {
26127
26541
  var _this$canvas2;
26128
26542
  // Set width to exactly what's needed + minimal safety margin
26129
26543
  const newWidth = maxRequiredWidth + 1; // Add just 1px safety margin
26130
- console.log(`Safety snap: ${this.width.toFixed(0)}px -> ${newWidth.toFixed(0)}px`, {
26131
- maxActualLineWidth: maxActualLineWidth.toFixed(1),
26132
- maxRequiredWidth: maxRequiredWidth.toFixed(1),
26133
- difference: (newWidth - this.width).toFixed(1)
26134
- });
26135
26544
 
26136
26545
  // Store original position before width change
26137
26546
  const originalLeft = this.left;
@@ -26147,19 +26556,12 @@ class Textbox extends IText {
26147
26556
  // Only compensate position when resizing from left handle
26148
26557
  // Right handle resize doesn't shift the text position
26149
26558
  if (resizeOrigin === 'left') {
26150
- console.log('🔧 Compensating for left-side resize', {
26151
- originalLeft,
26152
- widthIncrease,
26153
- newLeft: originalLeft - widthIncrease
26154
- });
26155
26559
  // When resizing from left, the expansion pushes text right
26156
26560
  // Compensate by moving the textbox left by the width increase
26157
26561
  this.set({
26158
26562
  'left': originalLeft - widthIncrease,
26159
26563
  'top': originalTop
26160
26564
  });
26161
- } else {
26162
- console.log('✅ Right-side resize, no compensation needed');
26163
26565
  }
26164
26566
  this.setCoords();
26165
26567