@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
package/dist/index.mjs CHANGED
@@ -354,7 +354,7 @@ class Cache {
354
354
  }
355
355
  const cache = new Cache();
356
356
 
357
- var version = "7.0.0-beta1";
357
+ var version = "7.0.1-beta2";
358
358
 
359
359
  // use this syntax so babel plugin see this import here
360
360
  const VERSION = version;
@@ -17834,10 +17834,189 @@ _defineProperty(Line, "ATTRIBUTE_NAMES", SHARED_ATTRIBUTES.concat(coordProps));
17834
17834
  classRegistry.setClass(Line);
17835
17835
  classRegistry.setSVGClass(Line);
17836
17836
 
17837
+ /**
17838
+ * Calculate the distance between two points
17839
+ */
17840
+ function pointDistance(p1, p2) {
17841
+ return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
17842
+ }
17843
+
17844
+ /**
17845
+ * Normalize a vector
17846
+ */
17847
+ function normalizeVector(vector) {
17848
+ const length = Math.sqrt(vector.x * vector.x + vector.y * vector.y);
17849
+ if (length === 0) return {
17850
+ x: 0,
17851
+ y: 0
17852
+ };
17853
+ return {
17854
+ x: vector.x / length,
17855
+ y: vector.y / length
17856
+ };
17857
+ }
17858
+
17859
+ /**
17860
+ * Get the maximum allowed radius for a corner based on adjacent edge lengths
17861
+ */
17862
+ function getMaxRadius(prevPoint, currentPoint, nextPoint) {
17863
+ const dist1 = pointDistance(prevPoint, currentPoint);
17864
+ const dist2 = pointDistance(currentPoint, nextPoint);
17865
+ return Math.min(dist1, dist2) / 2;
17866
+ }
17867
+
17868
+ /**
17869
+ * Calculate rounded corner data for a single corner
17870
+ */
17871
+ function calculateRoundedCorner(prevPoint, currentPoint, nextPoint, radius) {
17872
+ // Calculate edge vectors
17873
+ const edge1 = {
17874
+ x: currentPoint.x - prevPoint.x,
17875
+ y: currentPoint.y - prevPoint.y
17876
+ };
17877
+ const edge2 = {
17878
+ x: nextPoint.x - currentPoint.x,
17879
+ y: nextPoint.y - currentPoint.y
17880
+ };
17881
+
17882
+ // Normalize edge vectors
17883
+ const norm1 = normalizeVector(edge1);
17884
+ const norm2 = normalizeVector(edge2);
17885
+
17886
+ // Calculate the maximum allowed radius
17887
+ const maxRadius = getMaxRadius(prevPoint, currentPoint, nextPoint);
17888
+ const actualRadius = Math.min(radius, maxRadius);
17889
+
17890
+ // Calculate start and end points of the rounded corner
17891
+ const startPoint = {
17892
+ x: currentPoint.x - norm1.x * actualRadius,
17893
+ y: currentPoint.y - norm1.y * actualRadius
17894
+ };
17895
+ const endPoint = {
17896
+ x: currentPoint.x + norm2.x * actualRadius,
17897
+ y: currentPoint.y + norm2.y * actualRadius
17898
+ };
17899
+
17900
+ // Calculate control points for bezier curve
17901
+ // Using the magic number kRect for optimal circular approximation
17902
+ const controlOffset = actualRadius * kRect;
17903
+ const cp1 = {
17904
+ x: startPoint.x + norm1.x * controlOffset,
17905
+ y: startPoint.y + norm1.y * controlOffset
17906
+ };
17907
+ const cp2 = {
17908
+ x: endPoint.x - norm2.x * controlOffset,
17909
+ y: endPoint.y - norm2.y * controlOffset
17910
+ };
17911
+ return {
17912
+ corner: currentPoint,
17913
+ start: startPoint,
17914
+ end: endPoint,
17915
+ cp1,
17916
+ cp2,
17917
+ actualRadius
17918
+ };
17919
+ }
17920
+
17921
+ /**
17922
+ * Apply corner radius to a polygon defined by points
17923
+ */
17924
+ function applyCornerRadiusToPolygon(points, radius) {
17925
+ let radiusAsPercentage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
17926
+ if (points.length < 3) {
17927
+ throw new Error('Polygon must have at least 3 points');
17928
+ }
17929
+
17930
+ // Calculate bounding box if radius is percentage-based
17931
+ let actualRadius = radius;
17932
+ if (radiusAsPercentage) {
17933
+ const minX = Math.min(...points.map(p => p.x));
17934
+ const maxX = Math.max(...points.map(p => p.x));
17935
+ const minY = Math.min(...points.map(p => p.y));
17936
+ const maxY = Math.max(...points.map(p => p.y));
17937
+ const width = maxX - minX;
17938
+ const height = maxY - minY;
17939
+ const minDimension = Math.min(width, height);
17940
+ actualRadius = radius / 100 * minDimension;
17941
+ }
17942
+ const roundedCorners = [];
17943
+ for (let i = 0; i < points.length; i++) {
17944
+ const prevIndex = (i - 1 + points.length) % points.length;
17945
+ const nextIndex = (i + 1) % points.length;
17946
+ const prevPoint = points[prevIndex];
17947
+ const currentPoint = points[i];
17948
+ const nextPoint = points[nextIndex];
17949
+ const roundedCorner = calculateRoundedCorner(prevPoint, currentPoint, nextPoint, actualRadius);
17950
+ roundedCorners.push(roundedCorner);
17951
+ }
17952
+ return roundedCorners;
17953
+ }
17954
+
17955
+ /**
17956
+ * Render a rounded polygon to a canvas context
17957
+ */
17958
+ function renderRoundedPolygon(ctx, roundedCorners) {
17959
+ let closed = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
17960
+ if (roundedCorners.length === 0) return;
17961
+ ctx.beginPath();
17962
+
17963
+ // Start at the first corner's start point
17964
+ const firstCorner = roundedCorners[0];
17965
+ ctx.moveTo(firstCorner.start.x, firstCorner.start.y);
17966
+ for (let i = 0; i < roundedCorners.length; i++) {
17967
+ const corner = roundedCorners[i];
17968
+ const nextIndex = (i + 1) % roundedCorners.length;
17969
+ const nextCorner = roundedCorners[nextIndex];
17970
+
17971
+ // Draw the rounded corner using bezier curve
17972
+ ctx.bezierCurveTo(corner.cp1.x, corner.cp1.y, corner.cp2.x, corner.cp2.y, corner.end.x, corner.end.y);
17973
+
17974
+ // Draw line to next corner's start point (if not the last segment in open path)
17975
+ if (i < roundedCorners.length - 1 || closed) {
17976
+ ctx.lineTo(nextCorner.start.x, nextCorner.start.y);
17977
+ }
17978
+ }
17979
+ if (closed) {
17980
+ ctx.closePath();
17981
+ }
17982
+ }
17983
+
17984
+ /**
17985
+ * Generate SVG path data for a rounded polygon
17986
+ */
17987
+ function generateRoundedPolygonPath(roundedCorners) {
17988
+ let closed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
17989
+ if (roundedCorners.length === 0) return '';
17990
+ const pathData = [];
17991
+ const firstCorner = roundedCorners[0];
17992
+
17993
+ // Move to first corner's start point
17994
+ pathData.push(`M ${firstCorner.start.x} ${firstCorner.start.y}`);
17995
+ for (let i = 0; i < roundedCorners.length; i++) {
17996
+ const corner = roundedCorners[i];
17997
+ const nextIndex = (i + 1) % roundedCorners.length;
17998
+ const nextCorner = roundedCorners[nextIndex];
17999
+
18000
+ // Add bezier curve for the rounded corner
18001
+ pathData.push(`C ${corner.cp1.x} ${corner.cp1.y} ${corner.cp2.x} ${corner.cp2.y} ${corner.end.x} ${corner.end.y}`);
18002
+
18003
+ // Add line to next corner's start point (if not the last segment in open path)
18004
+ if (i < roundedCorners.length - 1 || closed) {
18005
+ pathData.push(`L ${nextCorner.start.x} ${nextCorner.start.y}`);
18006
+ }
18007
+ }
18008
+ if (closed) {
18009
+ pathData.push('Z');
18010
+ }
18011
+ return pathData.join(' ');
18012
+ }
18013
+
17837
18014
  const triangleDefaultValues = {
17838
18015
  width: 100,
17839
- height: 100
18016
+ height: 100,
18017
+ cornerRadius: 0
17840
18018
  };
18019
+ const TRIANGLE_PROPS = ['cornerRadius'];
17841
18020
  class Triangle extends FabricObject {
17842
18021
  static getDefaults() {
17843
18022
  return {
@@ -17856,34 +18035,90 @@ class Triangle extends FabricObject {
17856
18035
  this.setOptions(options);
17857
18036
  }
17858
18037
 
18038
+ /**
18039
+ * Get triangle points as an array of XY coordinates
18040
+ * @private
18041
+ */
18042
+ _getTrianglePoints() {
18043
+ const widthBy2 = this.width / 2;
18044
+ const heightBy2 = this.height / 2;
18045
+ return [{
18046
+ x: -widthBy2,
18047
+ y: heightBy2
18048
+ },
18049
+ // bottom left
18050
+ {
18051
+ x: 0,
18052
+ y: -heightBy2
18053
+ },
18054
+ // top center
18055
+ {
18056
+ x: widthBy2,
18057
+ y: heightBy2
18058
+ } // bottom right
18059
+ ];
18060
+ }
18061
+
17859
18062
  /**
17860
18063
  * @private
17861
18064
  * @param {CanvasRenderingContext2D} ctx Context to render on
17862
18065
  */
17863
18066
  _render(ctx) {
17864
- const widthBy2 = this.width / 2,
17865
- heightBy2 = this.height / 2;
17866
- ctx.beginPath();
17867
- ctx.moveTo(-widthBy2, heightBy2);
17868
- ctx.lineTo(0, -heightBy2);
17869
- ctx.lineTo(widthBy2, heightBy2);
17870
- ctx.closePath();
18067
+ if (this.cornerRadius > 0) {
18068
+ // Render rounded triangle
18069
+ const points = this._getTrianglePoints();
18070
+ const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
18071
+ renderRoundedPolygon(ctx, roundedCorners, true);
18072
+ } else {
18073
+ // Render sharp triangle (original implementation)
18074
+ const widthBy2 = this.width / 2;
18075
+ const heightBy2 = this.height / 2;
18076
+ ctx.beginPath();
18077
+ ctx.moveTo(-widthBy2, heightBy2);
18078
+ ctx.lineTo(0, -heightBy2);
18079
+ ctx.lineTo(widthBy2, heightBy2);
18080
+ ctx.closePath();
18081
+ }
17871
18082
  this._renderPaintInOrder(ctx);
17872
18083
  }
17873
18084
 
18085
+ /**
18086
+ * Returns object representation of an instance
18087
+ * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
18088
+ * @return {Object} object representation of an instance
18089
+ */
18090
+ toObject() {
18091
+ let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
18092
+ return super.toObject([...TRIANGLE_PROPS, ...propertiesToInclude]);
18093
+ }
18094
+
17874
18095
  /**
17875
18096
  * Returns svg representation of an instance
17876
18097
  * @return {Array} an array of strings with the specific svg representation
17877
18098
  * of the instance
17878
18099
  */
17879
18100
  _toSVG() {
17880
- const widthBy2 = this.width / 2,
17881
- heightBy2 = this.height / 2,
17882
- points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
17883
- return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
18101
+ if (this.cornerRadius > 0) {
18102
+ // Generate rounded triangle as path
18103
+ const points = this._getTrianglePoints();
18104
+ const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
18105
+ const pathData = generateRoundedPolygonPath(roundedCorners, true);
18106
+ return ['<path ', 'COMMON_PARTS', `d="${pathData}" />`];
18107
+ } else {
18108
+ // Original sharp triangle implementation
18109
+ const widthBy2 = this.width / 2;
18110
+ const heightBy2 = this.height / 2;
18111
+ const points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
18112
+ return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
18113
+ }
17884
18114
  }
17885
18115
  }
18116
+ /**
18117
+ * Corner radius for rounded triangle corners
18118
+ * @type Number
18119
+ */
17886
18120
  _defineProperty(Triangle, "type", 'Triangle');
18121
+ _defineProperty(Triangle, "cacheProperties", [...cacheProperties, ...TRIANGLE_PROPS]);
17887
18122
  _defineProperty(Triangle, "ownDefaults", triangleDefaultValues);
17888
18123
  classRegistry.setClass(Triangle);
17889
18124
  classRegistry.setSVGClass(Triangle);
@@ -18048,7 +18283,8 @@ const polylineDefaultValues = {
18048
18283
  /**
18049
18284
  * @deprecated transient option soon to be removed in favor of a different design
18050
18285
  */
18051
- exactBoundingBox: false
18286
+ exactBoundingBox: false,
18287
+ cornerRadius: 0
18052
18288
  };
18053
18289
  class Polyline extends FabricObject {
18054
18290
  static getDefaults() {
@@ -18262,7 +18498,7 @@ class Polyline extends FabricObject {
18262
18498
  toObject() {
18263
18499
  let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
18264
18500
  return {
18265
- ...super.toObject(propertiesToInclude),
18501
+ ...super.toObject(['cornerRadius', ...propertiesToInclude]),
18266
18502
  points: this.points.map(_ref => {
18267
18503
  let {
18268
18504
  x,
@@ -18282,14 +18518,28 @@ class Polyline extends FabricObject {
18282
18518
  * of the instance
18283
18519
  */
18284
18520
  _toSVG() {
18285
- const points = [],
18286
- diffX = this.pathOffset.x,
18287
- diffY = this.pathOffset.y,
18288
- NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS;
18289
- for (let i = 0, len = this.points.length; i < len; i++) {
18290
- points.push(toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ');
18521
+ if (this.cornerRadius > 0 && this.points.length >= 3) {
18522
+ // Generate rounded polygon/polyline as path
18523
+ const diffX = this.pathOffset.x;
18524
+ const diffY = this.pathOffset.y;
18525
+ const adjustedPoints = this.points.map(point => ({
18526
+ x: point.x - diffX,
18527
+ y: point.y - diffY
18528
+ }));
18529
+ const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
18530
+ const pathData = generateRoundedPolygonPath(roundedCorners, !this.isOpen());
18531
+ return ['<path ', 'COMMON_PARTS', `d="${pathData}" />\n`];
18532
+ } else {
18533
+ // Original sharp corners implementation
18534
+ const points = [];
18535
+ const diffX = this.pathOffset.x;
18536
+ const diffY = this.pathOffset.y;
18537
+ const NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS;
18538
+ for (let i = 0, len = this.points.length; i < len; i++) {
18539
+ points.push(toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ');
18540
+ }
18541
+ return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
18291
18542
  }
18292
- return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
18293
18543
  }
18294
18544
 
18295
18545
  /**
@@ -18305,13 +18555,24 @@ class Polyline extends FabricObject {
18305
18555
  // NaN comes from parseFloat of a empty string in parser
18306
18556
  return;
18307
18557
  }
18308
- ctx.beginPath();
18309
- ctx.moveTo(this.points[0].x - x, this.points[0].y - y);
18310
- for (let i = 0; i < len; i++) {
18311
- const point = this.points[i];
18312
- ctx.lineTo(point.x - x, point.y - y);
18558
+ if (this.cornerRadius > 0 && len >= 3) {
18559
+ // Render with rounded corners
18560
+ const adjustedPoints = this.points.map(point => ({
18561
+ x: point.x - x,
18562
+ y: point.y - y
18563
+ }));
18564
+ const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
18565
+ renderRoundedPolygon(ctx, roundedCorners, !this.isOpen());
18566
+ } else {
18567
+ // Original sharp corners implementation
18568
+ ctx.beginPath();
18569
+ ctx.moveTo(this.points[0].x - x, this.points[0].y - y);
18570
+ for (let i = 0; i < len; i++) {
18571
+ const point = this.points[i];
18572
+ ctx.lineTo(point.x - x, point.y - y);
18573
+ }
18574
+ !this.isOpen() && ctx.closePath();
18313
18575
  }
18314
- !this.isOpen() && ctx.closePath();
18315
18576
  this._renderPaintInOrder(ctx);
18316
18577
  }
18317
18578
 
@@ -18376,10 +18637,15 @@ class Polyline extends FabricObject {
18376
18637
  * @type Boolean
18377
18638
  * @default false
18378
18639
  */
18640
+ /**
18641
+ * Corner radius for rounded corners
18642
+ * @type Number
18643
+ * @default 0
18644
+ */
18379
18645
  _defineProperty(Polyline, "ownDefaults", polylineDefaultValues);
18380
18646
  _defineProperty(Polyline, "type", 'Polyline');
18381
18647
  _defineProperty(Polyline, "layoutProperties", [SKEW_X, SKEW_Y, 'strokeLineCap', 'strokeLineJoin', 'strokeMiterLimit', 'strokeWidth', 'strokeUniform', 'points']);
18382
- _defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points']);
18648
+ _defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points', 'cornerRadius']);
18383
18649
  _defineProperty(Polyline, "ATTRIBUTE_NAMES", [...SHARED_ATTRIBUTES]);
18384
18650
  classRegistry.setClass(Polyline);
18385
18651
  classRegistry.setSVGClass(Polyline);
@@ -20189,6 +20455,7 @@ class FabricText extends StyledText {
20189
20455
  */
20190
20456
  enlargeSpaces() {
20191
20457
  let diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces;
20458
+ const isRtl = this.direction === 'rtl';
20192
20459
  for (let i = 0, len = this._textLines.length; i < len; i++) {
20193
20460
  if (this.textAlign !== JUSTIFY && (i === len - 1 || this.isEndOfWrapping(i))) {
20194
20461
  continue;
@@ -20199,15 +20466,44 @@ class FabricText extends StyledText {
20199
20466
  if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) {
20200
20467
  numberOfSpaces = spaces.length;
20201
20468
  diffSpace = (this.width - currentLineWidth) / numberOfSpaces;
20202
- for (let j = 0; j <= line.length; j++) {
20203
- charBound = this.__charBounds[i][j];
20204
- if (this._reSpaceAndTab.test(line[j])) {
20205
- charBound.width += diffSpace;
20206
- charBound.kernedWidth += diffSpace;
20207
- charBound.left += accumulatedSpace;
20208
- accumulatedSpace += diffSpace;
20209
- } else {
20210
- charBound.left += accumulatedSpace;
20469
+ if (isRtl) {
20470
+ for (let j = 0; j < line.length; j++) {
20471
+ if (this._reSpaceAndTab.test(line[j])) ;
20472
+ }
20473
+
20474
+ // For RTL, we need to work backwards through the visual positions
20475
+ // but still update logical positions correctly
20476
+ let spaceCount = 0;
20477
+ for (let j = 0; j <= line.length; j++) {
20478
+ charBound = this.__charBounds[i][j];
20479
+ if (charBound) {
20480
+ if (this._reSpaceAndTab.test(line[j])) {
20481
+ charBound.width += diffSpace;
20482
+ charBound.kernedWidth += diffSpace;
20483
+ spaceCount++;
20484
+ }
20485
+
20486
+ // For RTL, shift all characters to the right by the total expansion
20487
+ // minus the expansion that comes after this character
20488
+ const remainingSpaces = numberOfSpaces - spaceCount;
20489
+ const shiftAmount = remainingSpaces * diffSpace;
20490
+ charBound.left += shiftAmount;
20491
+ }
20492
+ }
20493
+ } else {
20494
+ // LTR processing (original logic)
20495
+ for (let j = 0; j <= line.length; j++) {
20496
+ charBound = this.__charBounds[i][j];
20497
+ if (charBound) {
20498
+ if (this._reSpaceAndTab.test(line[j])) {
20499
+ charBound.width += diffSpace;
20500
+ charBound.kernedWidth += diffSpace;
20501
+ charBound.left += accumulatedSpace;
20502
+ accumulatedSpace += diffSpace;
20503
+ } else {
20504
+ charBound.left += accumulatedSpace;
20505
+ }
20506
+ }
20211
20507
  }
20212
20508
  }
20213
20509
  }
@@ -20865,7 +21161,15 @@ class FabricText extends StyledText {
20865
21161
  if (currentDirection !== this.direction) {
20866
21162
  ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl');
20867
21163
  ctx.direction = isLtr ? 'ltr' : 'rtl';
20868
- ctx.textAlign = isLtr ? LEFT : RIGHT;
21164
+
21165
+ // For justify alignments, we need to set the correct canvas text alignment
21166
+ // This is crucial for RTL text to render in the correct order
21167
+ if (isJustify) {
21168
+ // Justify uses LEFT alignment as a base, letting the character positioning handle justification
21169
+ ctx.textAlign = LEFT;
21170
+ } else {
21171
+ ctx.textAlign = isLtr ? LEFT : RIGHT;
21172
+ }
20869
21173
  }
20870
21174
  top -= lineHeight * this._fontSizeFraction / this.lineHeight;
20871
21175
  if (shortCut) {
@@ -21101,9 +21405,21 @@ class FabricText extends StyledText {
21101
21405
  direction = this.direction,
21102
21406
  isEndOfWrapping = this.isEndOfWrapping(lineIndex);
21103
21407
  let leftOffset = 0;
21104
- if (textAlign === JUSTIFY || textAlign === JUSTIFY_CENTER && !isEndOfWrapping || textAlign === JUSTIFY_RIGHT && !isEndOfWrapping || textAlign === JUSTIFY_LEFT && !isEndOfWrapping) {
21105
- return 0;
21408
+
21409
+ // Handle justify alignments (excluding last lines and wrapped line ends)
21410
+ const isJustifyLine = textAlign === JUSTIFY || textAlign === JUSTIFY_CENTER && !isEndOfWrapping || textAlign === JUSTIFY_RIGHT && !isEndOfWrapping || textAlign === JUSTIFY_LEFT && !isEndOfWrapping;
21411
+ if (isJustifyLine) {
21412
+ // Justify lines should start at the left edge for LTR and right edge for RTL
21413
+ // The space distribution is handled by enlargeSpaces()
21414
+ if (direction === 'rtl') {
21415
+ // For RTL justify, we need to account for the line being right-aligned
21416
+ return 0;
21417
+ } else {
21418
+ return 0;
21419
+ }
21106
21420
  }
21421
+
21422
+ // Handle non-justify alignments
21107
21423
  if (textAlign === CENTER) {
21108
21424
  leftOffset = lineDiff / 2;
21109
21425
  }
@@ -21116,6 +21432,8 @@ class FabricText extends StyledText {
21116
21432
  if (textAlign === JUSTIFY_RIGHT) {
21117
21433
  leftOffset = lineDiff;
21118
21434
  }
21435
+
21436
+ // Apply RTL adjustments for non-justify alignments
21119
21437
  if (direction === 'rtl') {
21120
21438
  if (textAlign === RIGHT || textAlign === JUSTIFY || textAlign === JUSTIFY_RIGHT) {
21121
21439
  leftOffset = 0;
@@ -22111,13 +22429,86 @@ class OverlayEditor {
22111
22429
  this.textarea.style.fontFamily = target.fontFamily || 'Arial';
22112
22430
  this.textarea.style.fontWeight = String(target.fontWeight || 'normal');
22113
22431
  this.textarea.style.fontStyle = target.fontStyle || 'normal';
22114
- this.textarea.style.textAlign = target.textAlign || 'left';
22432
+ // Handle text alignment and justification
22433
+ const textAlign = target.textAlign || 'left';
22434
+ let cssTextAlign = textAlign;
22435
+
22436
+ // Detect text direction from content for proper justify handling
22437
+ const autoDetectedDirection = this.firstStrongDir(this.textarea.value || '');
22438
+
22439
+ // DEBUG: Log alignment details
22440
+ console.log('🔍 ALIGNMENT DEBUG:');
22441
+ console.log(' Fabric textAlign:', textAlign);
22442
+ console.log(' Fabric direction:', target.direction);
22443
+ console.log(' Text content:', JSON.stringify(target.text));
22444
+ console.log(' Detected direction:', autoDetectedDirection);
22445
+
22446
+ // Map fabric.js justify to CSS
22447
+ if (textAlign.includes('justify')) {
22448
+ // Try to match fabric.js justify behavior more precisely
22449
+ try {
22450
+ // For justify, we need to replicate fabric.js space expansion
22451
+ // Use CSS justify but with specific settings to match fabric.js better
22452
+ cssTextAlign = 'justify';
22453
+
22454
+ // Set text-align-last based on justify type and detected direction
22455
+ // Smart justify: respect detected direction even when fabric alignment doesn't match
22456
+ if (textAlign === 'justify') {
22457
+ this.textarea.style.textAlignLast = autoDetectedDirection === 'rtl' ? 'right' : 'left';
22458
+ } else if (textAlign === 'justify-left') {
22459
+ // If text is RTL but fabric says justify-left, override to justify-right for better UX
22460
+ if (autoDetectedDirection === 'rtl') {
22461
+ this.textarea.style.textAlignLast = 'right';
22462
+ console.log(' → Overrode justify-left to justify-right for RTL text');
22463
+ } else {
22464
+ this.textarea.style.textAlignLast = 'left';
22465
+ }
22466
+ } else if (textAlign === 'justify-right') {
22467
+ // If text is LTR but fabric says justify-right, override to justify-left for better UX
22468
+ if (autoDetectedDirection === 'ltr') {
22469
+ this.textarea.style.textAlignLast = 'left';
22470
+ console.log(' → Overrode justify-right to justify-left for LTR text');
22471
+ } else {
22472
+ this.textarea.style.textAlignLast = 'right';
22473
+ }
22474
+ } else if (textAlign === 'justify-center') {
22475
+ this.textarea.style.textAlignLast = 'center';
22476
+ }
22477
+
22478
+ // Enhanced justify settings for better fabric.js matching
22479
+ this.textarea.style.textJustify = 'inter-word';
22480
+ this.textarea.style.wordSpacing = 'normal';
22481
+
22482
+ // Additional CSS properties for better justify matching
22483
+ this.textarea.style.textAlign = 'justify';
22484
+ this.textarea.style.textAlignLast = this.textarea.style.textAlignLast;
22485
+
22486
+ // Try to force better justify behavior
22487
+ this.textarea.style.textJustifyTrim = 'none';
22488
+ this.textarea.style.textAutospace = 'none';
22489
+ console.log(' → Applied justify alignment:', textAlign, 'with last-line:', this.textarea.style.textAlignLast);
22490
+ } catch (error) {
22491
+ console.warn(' → Justify setup failed, falling back to standard alignment:', error);
22492
+ cssTextAlign = textAlign.replace('justify-', '').replace('justify', 'left');
22493
+ }
22494
+ } else {
22495
+ this.textarea.style.textAlignLast = 'auto';
22496
+ this.textarea.style.textJustify = 'auto';
22497
+ this.textarea.style.wordSpacing = 'normal';
22498
+ console.log(' → Applied standard alignment:', cssTextAlign);
22499
+ }
22500
+ this.textarea.style.textAlign = cssTextAlign;
22115
22501
  this.textarea.style.color = ((_target$fill = target.fill) === null || _target$fill === void 0 ? void 0 : _target$fill.toString()) || '#000';
22116
22502
  this.textarea.style.letterSpacing = `${letterSpacingPx}px`;
22117
- this.textarea.style.direction = target.direction || this.firstStrongDir(this.textarea.value || '');
22503
+
22504
+ // Use the already detected direction from above
22505
+ const fabricDirection = target.direction;
22506
+
22507
+ // Use auto-detected direction for better BiDi support, but respect fabric direction if it makes sense
22508
+ this.textarea.style.direction = autoDetectedDirection || fabricDirection || 'ltr';
22118
22509
  this.textarea.style.fontVariant = 'normal';
22119
22510
  this.textarea.style.fontStretch = 'normal';
22120
- this.textarea.style.textRendering = 'optimizeLegibility';
22511
+ this.textarea.style.textRendering = 'auto'; // Changed from 'optimizeLegibility' to match canvas
22121
22512
  this.textarea.style.fontKerning = 'normal';
22122
22513
  this.textarea.style.fontFeatureSettings = 'normal';
22123
22514
  this.textarea.style.fontVariationSettings = 'normal';
@@ -22128,14 +22519,58 @@ class OverlayEditor {
22128
22519
  this.textarea.style.overflowWrap = 'break-word';
22129
22520
  this.textarea.style.whiteSpace = 'pre-wrap';
22130
22521
  this.textarea.style.hyphens = 'none';
22131
- this.textarea.style.webkitFontSmoothing = 'antialiased';
22132
- this.textarea.style.mozOsxFontSmoothing = 'grayscale';
22133
22522
 
22134
- // Debug: Compare textarea and canvas object bounding boxes
22135
- this.debugBoundingBoxComparison();
22523
+ // DEBUG: Log final CSS properties
22524
+ console.log('🎨 FINAL TEXTAREA CSS:');
22525
+ console.log(' textAlign:', this.textarea.style.textAlign);
22526
+ console.log(' textAlignLast:', this.textarea.style.textAlignLast);
22527
+ console.log(' direction:', this.textarea.style.direction);
22528
+ console.log(' unicodeBidi:', this.textarea.style.unicodeBidi);
22529
+ console.log(' width:', this.textarea.style.width);
22530
+ console.log(' textJustify:', this.textarea.style.textJustify);
22531
+ console.log(' wordSpacing:', this.textarea.style.wordSpacing);
22532
+ console.log(' whiteSpace:', this.textarea.style.whiteSpace);
22533
+
22534
+ // If justify, log Fabric object dimensions for comparison
22535
+ if (textAlign.includes('justify')) {
22536
+ var _calcTextWidth, _ref;
22537
+ console.log('🔧 FABRIC OBJECT JUSTIFY INFO:');
22538
+ console.log(' Fabric width:', target.width);
22539
+ console.log(' Fabric calcTextWidth:', (_calcTextWidth = (_ref = target).calcTextWidth) === null || _calcTextWidth === void 0 ? void 0 : _calcTextWidth.call(_ref));
22540
+ console.log(' Fabric textAlign:', target.textAlign);
22541
+ console.log(' Text lines:', target.textLines);
22542
+ }
22543
+
22544
+ // Debug font properties matching
22545
+ console.log('🔤 FONT PROPERTIES COMPARISON:');
22546
+ console.log(' Fabric fontFamily:', target.fontFamily);
22547
+ console.log(' Fabric fontWeight:', target.fontWeight);
22548
+ console.log(' Fabric fontStyle:', target.fontStyle);
22549
+ console.log(' Fabric fontSize:', target.fontSize);
22550
+ console.log(' → Textarea fontFamily:', this.textarea.style.fontFamily);
22551
+ console.log(' → Textarea fontWeight:', this.textarea.style.fontWeight);
22552
+ console.log(' → Textarea fontStyle:', this.textarea.style.fontStyle);
22553
+ console.log(' → Textarea fontSize:', this.textarea.style.fontSize);
22554
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
22136
22555
 
22137
- // Debug: Compare text wrapping behavior
22138
- this.debugTextWrapping();
22556
+ // Enhanced font rendering to better match fabric.js canvas rendering
22557
+ // Default to auto for more natural rendering
22558
+ this.textarea.style.webkitFontSmoothing = 'auto';
22559
+ this.textarea.style.mozOsxFontSmoothing = 'auto';
22560
+ this.textarea.style.fontSmooth = 'auto';
22561
+ this.textarea.style.textSizeAdjust = 'none';
22562
+
22563
+ // For bold fonts, use subpixel rendering to match canvas thickness better
22564
+ const fontWeight = String(target.fontWeight || 'normal');
22565
+ const isBold = fontWeight === 'bold' || fontWeight === '700' || parseInt(fontWeight) >= 600;
22566
+ if (isBold) {
22567
+ this.textarea.style.webkitFontSmoothing = 'subpixel-antialiased';
22568
+ this.textarea.style.mozOsxFontSmoothing = 'unset';
22569
+ console.log('🔤 Applied enhanced bold rendering for better thickness matching');
22570
+ }
22571
+ console.log('🎨 FONT SMOOTHING APPLIED:');
22572
+ console.log(' webkitFontSmoothing:', this.textarea.style.webkitFontSmoothing);
22573
+ console.log(' mozOsxFontSmoothing:', this.textarea.style.mozOsxFontSmoothing);
22139
22574
 
22140
22575
  // Initial bounds are set correctly by Fabric.js - don't force update here
22141
22576
  }
@@ -22370,6 +22805,23 @@ class OverlayEditor {
22370
22805
  // Handle commit/cancel after restoring visibility
22371
22806
  if (commit && !this.isComposing) {
22372
22807
  const finalText = this.textarea.value;
22808
+
22809
+ // Auto-detect text direction and update fabric object if needed
22810
+ const detectedDirection = this.firstStrongDir(finalText);
22811
+ const currentDirection = this.target.direction || 'ltr';
22812
+ if (detectedDirection && detectedDirection !== currentDirection) {
22813
+ console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
22814
+ console.log(` Text content: "${finalText.substring(0, 50)}..."`);
22815
+
22816
+ // Update the fabric object's direction
22817
+ this.target.set('direction', detectedDirection);
22818
+
22819
+ // Force a re-render to apply the direction change
22820
+ this.canvas.requestRenderAll();
22821
+ console.log(`✅ Fabric object direction updated to: ${detectedDirection}`);
22822
+ } else {
22823
+ console.log(`📝 Overlay Exit: Direction unchanged (${currentDirection}), text: "${finalText.substring(0, 30)}..."`);
22824
+ }
22373
22825
  if (this.onCommit) {
22374
22826
  this.onCommit(finalText);
22375
22827
  }
@@ -25960,30 +26412,17 @@ class Textbox extends IText {
25960
26412
  // Detect resize origin during resizing
25961
26413
  this.on('resizing', e => {
25962
26414
  // Check transform origin to determine which side is being resized
25963
- console.log('🔍 Resize event data:', e);
25964
26415
  if (e.transform) {
25965
26416
  const {
25966
- originX,
25967
- originY
26417
+ originX
25968
26418
  } = e.transform;
25969
- console.log('🔍 Transform origins:', {
25970
- originX,
25971
- originY
25972
- });
25973
26419
  // originX tells us which side is the anchor - opposite side is being dragged
25974
26420
  resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
25975
- console.log('🎯 Setting resizeOrigin to:', resizeOrigin);
25976
26421
  } else if (e.originX) {
25977
26422
  const {
25978
- originX,
25979
- originY
26423
+ originX
25980
26424
  } = e;
25981
- console.log('🔍 Event origins:', {
25982
- originX,
25983
- originY
25984
- });
25985
26425
  resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
25986
- console.log('🎯 Setting resizeOrigin to:', resizeOrigin);
25987
26426
  }
25988
26427
  });
25989
26428
 
@@ -25991,9 +26430,6 @@ class Textbox extends IText {
25991
26430
  // Use 'modified' event which fires after user releases the mouse
25992
26431
  this.on('modified', () => {
25993
26432
  const currentResizeOrigin = resizeOrigin; // Capture the value before reset
25994
- console.log('✅ Modified event fired - resize complete, triggering safety snap', {
25995
- resizeOrigin: currentResizeOrigin
25996
- });
25997
26433
  // Small delay to ensure text layout is updated
25998
26434
  setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
25999
26435
  resizeOrigin = null; // Reset after capturing
@@ -26003,7 +26439,6 @@ class Textbox extends IText {
26003
26439
  (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.on('object:modified', e => {
26004
26440
  if (e.target === this) {
26005
26441
  const currentResizeOrigin = resizeOrigin; // Capture the value before reset
26006
- console.log('✅ Canvas object:modified fired for this textbox');
26007
26442
  setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
26008
26443
  resizeOrigin = null; // Reset after capturing
26009
26444
  }
@@ -26018,38 +26453,17 @@ class Textbox extends IText {
26018
26453
  * @param resizeOrigin - Which side was used for resizing ('left' or 'right')
26019
26454
  */
26020
26455
  safetySnapWidth(resizeOrigin) {
26021
- var _this$_textLines;
26022
- console.log('🔍 safetySnapWidth called', {
26023
- isWrapping: this.isWrapping,
26024
- hasTextLines: !!this._textLines,
26025
- lineCount: ((_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length) || 0,
26026
- currentWidth: this.width,
26027
- type: this.type,
26028
- text: this.text
26029
- });
26030
-
26031
26456
  // For Textbox objects, we always want to check for clipping regardless of isWrapping flag
26032
26457
  if (!this._textLines || this.type.toLowerCase() !== 'textbox' || this._textLines.length === 0) {
26033
- var _this$_textLines2;
26034
- console.log('❌ Early return - missing requirements', {
26035
- hasTextLines: !!this._textLines,
26036
- typeMatch: this.type.toLowerCase() === 'textbox',
26037
- actualType: this.type,
26038
- hasLines: ((_this$_textLines2 = this._textLines) === null || _this$_textLines2 === void 0 ? void 0 : _this$_textLines2.length) > 0
26039
- });
26040
26458
  return;
26041
26459
  }
26042
26460
  const lineCount = this._textLines.length;
26043
26461
  if (lineCount === 0) return;
26044
-
26045
- // Check all lines, not just the last one
26046
- let maxActualLineWidth = 0; // Actual measured width without buffers
26047
26462
  let maxRequiredWidth = 0; // Width including RTL buffer
26048
26463
 
26049
26464
  for (let i = 0; i < lineCount; i++) {
26050
26465
  const lineText = this._textLines[i].join(''); // Convert grapheme array to string
26051
26466
  const lineWidth = this.getLineWidth(i);
26052
- maxActualLineWidth = Math.max(maxActualLineWidth, lineWidth);
26053
26467
 
26054
26468
  // RTL detection - regex for Arabic, Hebrew, and other RTL characters
26055
26469
  const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/;
@@ -26069,11 +26483,6 @@ class Textbox extends IText {
26069
26483
  var _this$canvas2;
26070
26484
  // Set width to exactly what's needed + minimal safety margin
26071
26485
  const newWidth = maxRequiredWidth + 1; // Add just 1px safety margin
26072
- console.log(`Safety snap: ${this.width.toFixed(0)}px -> ${newWidth.toFixed(0)}px`, {
26073
- maxActualLineWidth: maxActualLineWidth.toFixed(1),
26074
- maxRequiredWidth: maxRequiredWidth.toFixed(1),
26075
- difference: (newWidth - this.width).toFixed(1)
26076
- });
26077
26486
 
26078
26487
  // Store original position before width change
26079
26488
  const originalLeft = this.left;
@@ -26089,19 +26498,12 @@ class Textbox extends IText {
26089
26498
  // Only compensate position when resizing from left handle
26090
26499
  // Right handle resize doesn't shift the text position
26091
26500
  if (resizeOrigin === 'left') {
26092
- console.log('🔧 Compensating for left-side resize', {
26093
- originalLeft,
26094
- widthIncrease,
26095
- newLeft: originalLeft - widthIncrease
26096
- });
26097
26501
  // When resizing from left, the expansion pushes text right
26098
26502
  // Compensate by moving the textbox left by the width increase
26099
26503
  this.set({
26100
26504
  'left': originalLeft - widthIncrease,
26101
26505
  'top': originalTop
26102
26506
  });
26103
- } else {
26104
- console.log('✅ Right-side resize, no compensation needed');
26105
26507
  }
26106
26508
  this.setCoords();
26107
26509