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