@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.js CHANGED
@@ -360,7 +360,7 @@
360
360
  }
361
361
  const cache = new Cache();
362
362
 
363
- var version = "7.0.0-beta1";
363
+ var version = "7.0.1-beta2";
364
364
 
365
365
  // use this syntax so babel plugin see this import here
366
366
  const VERSION = version;
@@ -17840,10 +17840,189 @@
17840
17840
  classRegistry.setClass(Line);
17841
17841
  classRegistry.setSVGClass(Line);
17842
17842
 
17843
+ /**
17844
+ * Calculate the distance between two points
17845
+ */
17846
+ function pointDistance(p1, p2) {
17847
+ return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
17848
+ }
17849
+
17850
+ /**
17851
+ * Normalize a vector
17852
+ */
17853
+ function normalizeVector(vector) {
17854
+ const length = Math.sqrt(vector.x * vector.x + vector.y * vector.y);
17855
+ if (length === 0) return {
17856
+ x: 0,
17857
+ y: 0
17858
+ };
17859
+ return {
17860
+ x: vector.x / length,
17861
+ y: vector.y / length
17862
+ };
17863
+ }
17864
+
17865
+ /**
17866
+ * Get the maximum allowed radius for a corner based on adjacent edge lengths
17867
+ */
17868
+ function getMaxRadius(prevPoint, currentPoint, nextPoint) {
17869
+ const dist1 = pointDistance(prevPoint, currentPoint);
17870
+ const dist2 = pointDistance(currentPoint, nextPoint);
17871
+ return Math.min(dist1, dist2) / 2;
17872
+ }
17873
+
17874
+ /**
17875
+ * Calculate rounded corner data for a single corner
17876
+ */
17877
+ function calculateRoundedCorner(prevPoint, currentPoint, nextPoint, radius) {
17878
+ // Calculate edge vectors
17879
+ const edge1 = {
17880
+ x: currentPoint.x - prevPoint.x,
17881
+ y: currentPoint.y - prevPoint.y
17882
+ };
17883
+ const edge2 = {
17884
+ x: nextPoint.x - currentPoint.x,
17885
+ y: nextPoint.y - currentPoint.y
17886
+ };
17887
+
17888
+ // Normalize edge vectors
17889
+ const norm1 = normalizeVector(edge1);
17890
+ const norm2 = normalizeVector(edge2);
17891
+
17892
+ // Calculate the maximum allowed radius
17893
+ const maxRadius = getMaxRadius(prevPoint, currentPoint, nextPoint);
17894
+ const actualRadius = Math.min(radius, maxRadius);
17895
+
17896
+ // Calculate start and end points of the rounded corner
17897
+ const startPoint = {
17898
+ x: currentPoint.x - norm1.x * actualRadius,
17899
+ y: currentPoint.y - norm1.y * actualRadius
17900
+ };
17901
+ const endPoint = {
17902
+ x: currentPoint.x + norm2.x * actualRadius,
17903
+ y: currentPoint.y + norm2.y * actualRadius
17904
+ };
17905
+
17906
+ // Calculate control points for bezier curve
17907
+ // Using the magic number kRect for optimal circular approximation
17908
+ const controlOffset = actualRadius * kRect;
17909
+ const cp1 = {
17910
+ x: startPoint.x + norm1.x * controlOffset,
17911
+ y: startPoint.y + norm1.y * controlOffset
17912
+ };
17913
+ const cp2 = {
17914
+ x: endPoint.x - norm2.x * controlOffset,
17915
+ y: endPoint.y - norm2.y * controlOffset
17916
+ };
17917
+ return {
17918
+ corner: currentPoint,
17919
+ start: startPoint,
17920
+ end: endPoint,
17921
+ cp1,
17922
+ cp2,
17923
+ actualRadius
17924
+ };
17925
+ }
17926
+
17927
+ /**
17928
+ * Apply corner radius to a polygon defined by points
17929
+ */
17930
+ function applyCornerRadiusToPolygon(points, radius) {
17931
+ let radiusAsPercentage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
17932
+ if (points.length < 3) {
17933
+ throw new Error('Polygon must have at least 3 points');
17934
+ }
17935
+
17936
+ // Calculate bounding box if radius is percentage-based
17937
+ let actualRadius = radius;
17938
+ if (radiusAsPercentage) {
17939
+ const minX = Math.min(...points.map(p => p.x));
17940
+ const maxX = Math.max(...points.map(p => p.x));
17941
+ const minY = Math.min(...points.map(p => p.y));
17942
+ const maxY = Math.max(...points.map(p => p.y));
17943
+ const width = maxX - minX;
17944
+ const height = maxY - minY;
17945
+ const minDimension = Math.min(width, height);
17946
+ actualRadius = radius / 100 * minDimension;
17947
+ }
17948
+ const roundedCorners = [];
17949
+ for (let i = 0; i < points.length; i++) {
17950
+ const prevIndex = (i - 1 + points.length) % points.length;
17951
+ const nextIndex = (i + 1) % points.length;
17952
+ const prevPoint = points[prevIndex];
17953
+ const currentPoint = points[i];
17954
+ const nextPoint = points[nextIndex];
17955
+ const roundedCorner = calculateRoundedCorner(prevPoint, currentPoint, nextPoint, actualRadius);
17956
+ roundedCorners.push(roundedCorner);
17957
+ }
17958
+ return roundedCorners;
17959
+ }
17960
+
17961
+ /**
17962
+ * Render a rounded polygon to a canvas context
17963
+ */
17964
+ function renderRoundedPolygon(ctx, roundedCorners) {
17965
+ let closed = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
17966
+ if (roundedCorners.length === 0) return;
17967
+ ctx.beginPath();
17968
+
17969
+ // Start at the first corner's start point
17970
+ const firstCorner = roundedCorners[0];
17971
+ ctx.moveTo(firstCorner.start.x, firstCorner.start.y);
17972
+ for (let i = 0; i < roundedCorners.length; i++) {
17973
+ const corner = roundedCorners[i];
17974
+ const nextIndex = (i + 1) % roundedCorners.length;
17975
+ const nextCorner = roundedCorners[nextIndex];
17976
+
17977
+ // Draw the rounded corner using bezier curve
17978
+ ctx.bezierCurveTo(corner.cp1.x, corner.cp1.y, corner.cp2.x, corner.cp2.y, corner.end.x, corner.end.y);
17979
+
17980
+ // Draw line to next corner's start point (if not the last segment in open path)
17981
+ if (i < roundedCorners.length - 1 || closed) {
17982
+ ctx.lineTo(nextCorner.start.x, nextCorner.start.y);
17983
+ }
17984
+ }
17985
+ if (closed) {
17986
+ ctx.closePath();
17987
+ }
17988
+ }
17989
+
17990
+ /**
17991
+ * Generate SVG path data for a rounded polygon
17992
+ */
17993
+ function generateRoundedPolygonPath(roundedCorners) {
17994
+ let closed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
17995
+ if (roundedCorners.length === 0) return '';
17996
+ const pathData = [];
17997
+ const firstCorner = roundedCorners[0];
17998
+
17999
+ // Move to first corner's start point
18000
+ pathData.push(`M ${firstCorner.start.x} ${firstCorner.start.y}`);
18001
+ for (let i = 0; i < roundedCorners.length; i++) {
18002
+ const corner = roundedCorners[i];
18003
+ const nextIndex = (i + 1) % roundedCorners.length;
18004
+ const nextCorner = roundedCorners[nextIndex];
18005
+
18006
+ // Add bezier curve for the rounded corner
18007
+ pathData.push(`C ${corner.cp1.x} ${corner.cp1.y} ${corner.cp2.x} ${corner.cp2.y} ${corner.end.x} ${corner.end.y}`);
18008
+
18009
+ // Add line to next corner's start point (if not the last segment in open path)
18010
+ if (i < roundedCorners.length - 1 || closed) {
18011
+ pathData.push(`L ${nextCorner.start.x} ${nextCorner.start.y}`);
18012
+ }
18013
+ }
18014
+ if (closed) {
18015
+ pathData.push('Z');
18016
+ }
18017
+ return pathData.join(' ');
18018
+ }
18019
+
17843
18020
  const triangleDefaultValues = {
17844
18021
  width: 100,
17845
- height: 100
18022
+ height: 100,
18023
+ cornerRadius: 0
17846
18024
  };
18025
+ const TRIANGLE_PROPS = ['cornerRadius'];
17847
18026
  class Triangle extends FabricObject {
17848
18027
  static getDefaults() {
17849
18028
  return {
@@ -17862,34 +18041,90 @@
17862
18041
  this.setOptions(options);
17863
18042
  }
17864
18043
 
18044
+ /**
18045
+ * Get triangle points as an array of XY coordinates
18046
+ * @private
18047
+ */
18048
+ _getTrianglePoints() {
18049
+ const widthBy2 = this.width / 2;
18050
+ const heightBy2 = this.height / 2;
18051
+ return [{
18052
+ x: -widthBy2,
18053
+ y: heightBy2
18054
+ },
18055
+ // bottom left
18056
+ {
18057
+ x: 0,
18058
+ y: -heightBy2
18059
+ },
18060
+ // top center
18061
+ {
18062
+ x: widthBy2,
18063
+ y: heightBy2
18064
+ } // bottom right
18065
+ ];
18066
+ }
18067
+
17865
18068
  /**
17866
18069
  * @private
17867
18070
  * @param {CanvasRenderingContext2D} ctx Context to render on
17868
18071
  */
17869
18072
  _render(ctx) {
17870
- const widthBy2 = this.width / 2,
17871
- heightBy2 = this.height / 2;
17872
- ctx.beginPath();
17873
- ctx.moveTo(-widthBy2, heightBy2);
17874
- ctx.lineTo(0, -heightBy2);
17875
- ctx.lineTo(widthBy2, heightBy2);
17876
- ctx.closePath();
18073
+ if (this.cornerRadius > 0) {
18074
+ // Render rounded triangle
18075
+ const points = this._getTrianglePoints();
18076
+ const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
18077
+ renderRoundedPolygon(ctx, roundedCorners, true);
18078
+ } else {
18079
+ // Render sharp triangle (original implementation)
18080
+ const widthBy2 = this.width / 2;
18081
+ const heightBy2 = this.height / 2;
18082
+ ctx.beginPath();
18083
+ ctx.moveTo(-widthBy2, heightBy2);
18084
+ ctx.lineTo(0, -heightBy2);
18085
+ ctx.lineTo(widthBy2, heightBy2);
18086
+ ctx.closePath();
18087
+ }
17877
18088
  this._renderPaintInOrder(ctx);
17878
18089
  }
17879
18090
 
18091
+ /**
18092
+ * Returns object representation of an instance
18093
+ * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
18094
+ * @return {Object} object representation of an instance
18095
+ */
18096
+ toObject() {
18097
+ let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
18098
+ return super.toObject([...TRIANGLE_PROPS, ...propertiesToInclude]);
18099
+ }
18100
+
17880
18101
  /**
17881
18102
  * Returns svg representation of an instance
17882
18103
  * @return {Array} an array of strings with the specific svg representation
17883
18104
  * of the instance
17884
18105
  */
17885
18106
  _toSVG() {
17886
- const widthBy2 = this.width / 2,
17887
- heightBy2 = this.height / 2,
17888
- points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
17889
- return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
18107
+ if (this.cornerRadius > 0) {
18108
+ // Generate rounded triangle as path
18109
+ const points = this._getTrianglePoints();
18110
+ const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
18111
+ const pathData = generateRoundedPolygonPath(roundedCorners, true);
18112
+ return ['<path ', 'COMMON_PARTS', `d="${pathData}" />`];
18113
+ } else {
18114
+ // Original sharp triangle implementation
18115
+ const widthBy2 = this.width / 2;
18116
+ const heightBy2 = this.height / 2;
18117
+ const points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
18118
+ return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
18119
+ }
17890
18120
  }
17891
18121
  }
18122
+ /**
18123
+ * Corner radius for rounded triangle corners
18124
+ * @type Number
18125
+ */
17892
18126
  _defineProperty(Triangle, "type", 'Triangle');
18127
+ _defineProperty(Triangle, "cacheProperties", [...cacheProperties, ...TRIANGLE_PROPS]);
17893
18128
  _defineProperty(Triangle, "ownDefaults", triangleDefaultValues);
17894
18129
  classRegistry.setClass(Triangle);
17895
18130
  classRegistry.setSVGClass(Triangle);
@@ -18054,7 +18289,8 @@
18054
18289
  /**
18055
18290
  * @deprecated transient option soon to be removed in favor of a different design
18056
18291
  */
18057
- exactBoundingBox: false
18292
+ exactBoundingBox: false,
18293
+ cornerRadius: 0
18058
18294
  };
18059
18295
  class Polyline extends FabricObject {
18060
18296
  static getDefaults() {
@@ -18268,7 +18504,7 @@
18268
18504
  toObject() {
18269
18505
  let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
18270
18506
  return {
18271
- ...super.toObject(propertiesToInclude),
18507
+ ...super.toObject(['cornerRadius', ...propertiesToInclude]),
18272
18508
  points: this.points.map(_ref => {
18273
18509
  let {
18274
18510
  x,
@@ -18288,14 +18524,28 @@
18288
18524
  * of the instance
18289
18525
  */
18290
18526
  _toSVG() {
18291
- const points = [],
18292
- diffX = this.pathOffset.x,
18293
- diffY = this.pathOffset.y,
18294
- NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS;
18295
- for (let i = 0, len = this.points.length; i < len; i++) {
18296
- points.push(toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ');
18527
+ if (this.cornerRadius > 0 && this.points.length >= 3) {
18528
+ // Generate rounded polygon/polyline as path
18529
+ const diffX = this.pathOffset.x;
18530
+ const diffY = this.pathOffset.y;
18531
+ const adjustedPoints = this.points.map(point => ({
18532
+ x: point.x - diffX,
18533
+ y: point.y - diffY
18534
+ }));
18535
+ const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
18536
+ const pathData = generateRoundedPolygonPath(roundedCorners, !this.isOpen());
18537
+ return ['<path ', 'COMMON_PARTS', `d="${pathData}" />\n`];
18538
+ } else {
18539
+ // Original sharp corners implementation
18540
+ const points = [];
18541
+ const diffX = this.pathOffset.x;
18542
+ const diffY = this.pathOffset.y;
18543
+ const NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS;
18544
+ for (let i = 0, len = this.points.length; i < len; i++) {
18545
+ points.push(toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ');
18546
+ }
18547
+ return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
18297
18548
  }
18298
- return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
18299
18549
  }
18300
18550
 
18301
18551
  /**
@@ -18311,13 +18561,24 @@
18311
18561
  // NaN comes from parseFloat of a empty string in parser
18312
18562
  return;
18313
18563
  }
18314
- ctx.beginPath();
18315
- ctx.moveTo(this.points[0].x - x, this.points[0].y - y);
18316
- for (let i = 0; i < len; i++) {
18317
- const point = this.points[i];
18318
- ctx.lineTo(point.x - x, point.y - y);
18564
+ if (this.cornerRadius > 0 && len >= 3) {
18565
+ // Render with rounded corners
18566
+ const adjustedPoints = this.points.map(point => ({
18567
+ x: point.x - x,
18568
+ y: point.y - y
18569
+ }));
18570
+ const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
18571
+ renderRoundedPolygon(ctx, roundedCorners, !this.isOpen());
18572
+ } else {
18573
+ // Original sharp corners implementation
18574
+ ctx.beginPath();
18575
+ ctx.moveTo(this.points[0].x - x, this.points[0].y - y);
18576
+ for (let i = 0; i < len; i++) {
18577
+ const point = this.points[i];
18578
+ ctx.lineTo(point.x - x, point.y - y);
18579
+ }
18580
+ !this.isOpen() && ctx.closePath();
18319
18581
  }
18320
- !this.isOpen() && ctx.closePath();
18321
18582
  this._renderPaintInOrder(ctx);
18322
18583
  }
18323
18584
 
@@ -18382,10 +18643,15 @@
18382
18643
  * @type Boolean
18383
18644
  * @default false
18384
18645
  */
18646
+ /**
18647
+ * Corner radius for rounded corners
18648
+ * @type Number
18649
+ * @default 0
18650
+ */
18385
18651
  _defineProperty(Polyline, "ownDefaults", polylineDefaultValues);
18386
18652
  _defineProperty(Polyline, "type", 'Polyline');
18387
18653
  _defineProperty(Polyline, "layoutProperties", [SKEW_X, SKEW_Y, 'strokeLineCap', 'strokeLineJoin', 'strokeMiterLimit', 'strokeWidth', 'strokeUniform', 'points']);
18388
- _defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points']);
18654
+ _defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points', 'cornerRadius']);
18389
18655
  _defineProperty(Polyline, "ATTRIBUTE_NAMES", [...SHARED_ATTRIBUTES]);
18390
18656
  classRegistry.setClass(Polyline);
18391
18657
  classRegistry.setSVGClass(Polyline);
@@ -20195,6 +20461,7 @@
20195
20461
  */
20196
20462
  enlargeSpaces() {
20197
20463
  let diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces;
20464
+ const isRtl = this.direction === 'rtl';
20198
20465
  for (let i = 0, len = this._textLines.length; i < len; i++) {
20199
20466
  if (this.textAlign !== JUSTIFY && (i === len - 1 || this.isEndOfWrapping(i))) {
20200
20467
  continue;
@@ -20205,15 +20472,44 @@
20205
20472
  if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) {
20206
20473
  numberOfSpaces = spaces.length;
20207
20474
  diffSpace = (this.width - currentLineWidth) / numberOfSpaces;
20208
- for (let j = 0; j <= line.length; j++) {
20209
- charBound = this.__charBounds[i][j];
20210
- if (this._reSpaceAndTab.test(line[j])) {
20211
- charBound.width += diffSpace;
20212
- charBound.kernedWidth += diffSpace;
20213
- charBound.left += accumulatedSpace;
20214
- accumulatedSpace += diffSpace;
20215
- } else {
20216
- charBound.left += accumulatedSpace;
20475
+ if (isRtl) {
20476
+ for (let j = 0; j < line.length; j++) {
20477
+ if (this._reSpaceAndTab.test(line[j])) ;
20478
+ }
20479
+
20480
+ // For RTL, we need to work backwards through the visual positions
20481
+ // but still update logical positions correctly
20482
+ let spaceCount = 0;
20483
+ for (let j = 0; j <= line.length; j++) {
20484
+ charBound = this.__charBounds[i][j];
20485
+ if (charBound) {
20486
+ if (this._reSpaceAndTab.test(line[j])) {
20487
+ charBound.width += diffSpace;
20488
+ charBound.kernedWidth += diffSpace;
20489
+ spaceCount++;
20490
+ }
20491
+
20492
+ // For RTL, shift all characters to the right by the total expansion
20493
+ // minus the expansion that comes after this character
20494
+ const remainingSpaces = numberOfSpaces - spaceCount;
20495
+ const shiftAmount = remainingSpaces * diffSpace;
20496
+ charBound.left += shiftAmount;
20497
+ }
20498
+ }
20499
+ } else {
20500
+ // LTR processing (original logic)
20501
+ for (let j = 0; j <= line.length; j++) {
20502
+ charBound = this.__charBounds[i][j];
20503
+ if (charBound) {
20504
+ if (this._reSpaceAndTab.test(line[j])) {
20505
+ charBound.width += diffSpace;
20506
+ charBound.kernedWidth += diffSpace;
20507
+ charBound.left += accumulatedSpace;
20508
+ accumulatedSpace += diffSpace;
20509
+ } else {
20510
+ charBound.left += accumulatedSpace;
20511
+ }
20512
+ }
20217
20513
  }
20218
20514
  }
20219
20515
  }
@@ -20871,7 +21167,15 @@
20871
21167
  if (currentDirection !== this.direction) {
20872
21168
  ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl');
20873
21169
  ctx.direction = isLtr ? 'ltr' : 'rtl';
20874
- ctx.textAlign = isLtr ? LEFT : RIGHT;
21170
+
21171
+ // For justify alignments, we need to set the correct canvas text alignment
21172
+ // This is crucial for RTL text to render in the correct order
21173
+ if (isJustify) {
21174
+ // Justify uses LEFT alignment as a base, letting the character positioning handle justification
21175
+ ctx.textAlign = LEFT;
21176
+ } else {
21177
+ ctx.textAlign = isLtr ? LEFT : RIGHT;
21178
+ }
20875
21179
  }
20876
21180
  top -= lineHeight * this._fontSizeFraction / this.lineHeight;
20877
21181
  if (shortCut) {
@@ -21107,9 +21411,21 @@
21107
21411
  direction = this.direction,
21108
21412
  isEndOfWrapping = this.isEndOfWrapping(lineIndex);
21109
21413
  let leftOffset = 0;
21110
- if (textAlign === JUSTIFY || textAlign === JUSTIFY_CENTER && !isEndOfWrapping || textAlign === JUSTIFY_RIGHT && !isEndOfWrapping || textAlign === JUSTIFY_LEFT && !isEndOfWrapping) {
21111
- return 0;
21414
+
21415
+ // Handle justify alignments (excluding last lines and wrapped line ends)
21416
+ const isJustifyLine = textAlign === JUSTIFY || textAlign === JUSTIFY_CENTER && !isEndOfWrapping || textAlign === JUSTIFY_RIGHT && !isEndOfWrapping || textAlign === JUSTIFY_LEFT && !isEndOfWrapping;
21417
+ if (isJustifyLine) {
21418
+ // Justify lines should start at the left edge for LTR and right edge for RTL
21419
+ // The space distribution is handled by enlargeSpaces()
21420
+ if (direction === 'rtl') {
21421
+ // For RTL justify, we need to account for the line being right-aligned
21422
+ return 0;
21423
+ } else {
21424
+ return 0;
21425
+ }
21112
21426
  }
21427
+
21428
+ // Handle non-justify alignments
21113
21429
  if (textAlign === CENTER) {
21114
21430
  leftOffset = lineDiff / 2;
21115
21431
  }
@@ -21122,6 +21438,8 @@
21122
21438
  if (textAlign === JUSTIFY_RIGHT) {
21123
21439
  leftOffset = lineDiff;
21124
21440
  }
21441
+
21442
+ // Apply RTL adjustments for non-justify alignments
21125
21443
  if (direction === 'rtl') {
21126
21444
  if (textAlign === RIGHT || textAlign === JUSTIFY || textAlign === JUSTIFY_RIGHT) {
21127
21445
  leftOffset = 0;
@@ -22117,13 +22435,86 @@
22117
22435
  this.textarea.style.fontFamily = target.fontFamily || 'Arial';
22118
22436
  this.textarea.style.fontWeight = String(target.fontWeight || 'normal');
22119
22437
  this.textarea.style.fontStyle = target.fontStyle || 'normal';
22120
- this.textarea.style.textAlign = target.textAlign || 'left';
22438
+ // Handle text alignment and justification
22439
+ const textAlign = target.textAlign || 'left';
22440
+ let cssTextAlign = textAlign;
22441
+
22442
+ // Detect text direction from content for proper justify handling
22443
+ const autoDetectedDirection = this.firstStrongDir(this.textarea.value || '');
22444
+
22445
+ // DEBUG: Log alignment details
22446
+ console.log('🔍 ALIGNMENT DEBUG:');
22447
+ console.log(' Fabric textAlign:', textAlign);
22448
+ console.log(' Fabric direction:', target.direction);
22449
+ console.log(' Text content:', JSON.stringify(target.text));
22450
+ console.log(' Detected direction:', autoDetectedDirection);
22451
+
22452
+ // Map fabric.js justify to CSS
22453
+ if (textAlign.includes('justify')) {
22454
+ // Try to match fabric.js justify behavior more precisely
22455
+ try {
22456
+ // For justify, we need to replicate fabric.js space expansion
22457
+ // Use CSS justify but with specific settings to match fabric.js better
22458
+ cssTextAlign = 'justify';
22459
+
22460
+ // Set text-align-last based on justify type and detected direction
22461
+ // Smart justify: respect detected direction even when fabric alignment doesn't match
22462
+ if (textAlign === 'justify') {
22463
+ this.textarea.style.textAlignLast = autoDetectedDirection === 'rtl' ? 'right' : 'left';
22464
+ } else if (textAlign === 'justify-left') {
22465
+ // If text is RTL but fabric says justify-left, override to justify-right for better UX
22466
+ if (autoDetectedDirection === 'rtl') {
22467
+ this.textarea.style.textAlignLast = 'right';
22468
+ console.log(' → Overrode justify-left to justify-right for RTL text');
22469
+ } else {
22470
+ this.textarea.style.textAlignLast = 'left';
22471
+ }
22472
+ } else if (textAlign === 'justify-right') {
22473
+ // If text is LTR but fabric says justify-right, override to justify-left for better UX
22474
+ if (autoDetectedDirection === 'ltr') {
22475
+ this.textarea.style.textAlignLast = 'left';
22476
+ console.log(' → Overrode justify-right to justify-left for LTR text');
22477
+ } else {
22478
+ this.textarea.style.textAlignLast = 'right';
22479
+ }
22480
+ } else if (textAlign === 'justify-center') {
22481
+ this.textarea.style.textAlignLast = 'center';
22482
+ }
22483
+
22484
+ // Enhanced justify settings for better fabric.js matching
22485
+ this.textarea.style.textJustify = 'inter-word';
22486
+ this.textarea.style.wordSpacing = 'normal';
22487
+
22488
+ // Additional CSS properties for better justify matching
22489
+ this.textarea.style.textAlign = 'justify';
22490
+ this.textarea.style.textAlignLast = this.textarea.style.textAlignLast;
22491
+
22492
+ // Try to force better justify behavior
22493
+ this.textarea.style.textJustifyTrim = 'none';
22494
+ this.textarea.style.textAutospace = 'none';
22495
+ console.log(' → Applied justify alignment:', textAlign, 'with last-line:', this.textarea.style.textAlignLast);
22496
+ } catch (error) {
22497
+ console.warn(' → Justify setup failed, falling back to standard alignment:', error);
22498
+ cssTextAlign = textAlign.replace('justify-', '').replace('justify', 'left');
22499
+ }
22500
+ } else {
22501
+ this.textarea.style.textAlignLast = 'auto';
22502
+ this.textarea.style.textJustify = 'auto';
22503
+ this.textarea.style.wordSpacing = 'normal';
22504
+ console.log(' → Applied standard alignment:', cssTextAlign);
22505
+ }
22506
+ this.textarea.style.textAlign = cssTextAlign;
22121
22507
  this.textarea.style.color = ((_target$fill = target.fill) === null || _target$fill === void 0 ? void 0 : _target$fill.toString()) || '#000';
22122
22508
  this.textarea.style.letterSpacing = `${letterSpacingPx}px`;
22123
- this.textarea.style.direction = target.direction || this.firstStrongDir(this.textarea.value || '');
22509
+
22510
+ // Use the already detected direction from above
22511
+ const fabricDirection = target.direction;
22512
+
22513
+ // Use auto-detected direction for better BiDi support, but respect fabric direction if it makes sense
22514
+ this.textarea.style.direction = autoDetectedDirection || fabricDirection || 'ltr';
22124
22515
  this.textarea.style.fontVariant = 'normal';
22125
22516
  this.textarea.style.fontStretch = 'normal';
22126
- this.textarea.style.textRendering = 'optimizeLegibility';
22517
+ this.textarea.style.textRendering = 'auto'; // Changed from 'optimizeLegibility' to match canvas
22127
22518
  this.textarea.style.fontKerning = 'normal';
22128
22519
  this.textarea.style.fontFeatureSettings = 'normal';
22129
22520
  this.textarea.style.fontVariationSettings = 'normal';
@@ -22134,14 +22525,58 @@
22134
22525
  this.textarea.style.overflowWrap = 'break-word';
22135
22526
  this.textarea.style.whiteSpace = 'pre-wrap';
22136
22527
  this.textarea.style.hyphens = 'none';
22137
- this.textarea.style.webkitFontSmoothing = 'antialiased';
22138
- this.textarea.style.mozOsxFontSmoothing = 'grayscale';
22139
22528
 
22140
- // Debug: Compare textarea and canvas object bounding boxes
22141
- this.debugBoundingBoxComparison();
22529
+ // DEBUG: Log final CSS properties
22530
+ console.log('🎨 FINAL TEXTAREA CSS:');
22531
+ console.log(' textAlign:', this.textarea.style.textAlign);
22532
+ console.log(' textAlignLast:', this.textarea.style.textAlignLast);
22533
+ console.log(' direction:', this.textarea.style.direction);
22534
+ console.log(' unicodeBidi:', this.textarea.style.unicodeBidi);
22535
+ console.log(' width:', this.textarea.style.width);
22536
+ console.log(' textJustify:', this.textarea.style.textJustify);
22537
+ console.log(' wordSpacing:', this.textarea.style.wordSpacing);
22538
+ console.log(' whiteSpace:', this.textarea.style.whiteSpace);
22539
+
22540
+ // If justify, log Fabric object dimensions for comparison
22541
+ if (textAlign.includes('justify')) {
22542
+ var _calcTextWidth, _ref;
22543
+ console.log('🔧 FABRIC OBJECT JUSTIFY INFO:');
22544
+ console.log(' Fabric width:', target.width);
22545
+ console.log(' Fabric calcTextWidth:', (_calcTextWidth = (_ref = target).calcTextWidth) === null || _calcTextWidth === void 0 ? void 0 : _calcTextWidth.call(_ref));
22546
+ console.log(' Fabric textAlign:', target.textAlign);
22547
+ console.log(' Text lines:', target.textLines);
22548
+ }
22549
+
22550
+ // Debug font properties matching
22551
+ console.log('🔤 FONT PROPERTIES COMPARISON:');
22552
+ console.log(' Fabric fontFamily:', target.fontFamily);
22553
+ console.log(' Fabric fontWeight:', target.fontWeight);
22554
+ console.log(' Fabric fontStyle:', target.fontStyle);
22555
+ console.log(' Fabric fontSize:', target.fontSize);
22556
+ console.log(' → Textarea fontFamily:', this.textarea.style.fontFamily);
22557
+ console.log(' → Textarea fontWeight:', this.textarea.style.fontWeight);
22558
+ console.log(' → Textarea fontStyle:', this.textarea.style.fontStyle);
22559
+ console.log(' → Textarea fontSize:', this.textarea.style.fontSize);
22560
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
22142
22561
 
22143
- // Debug: Compare text wrapping behavior
22144
- this.debugTextWrapping();
22562
+ // Enhanced font rendering to better match fabric.js canvas rendering
22563
+ // Default to auto for more natural rendering
22564
+ this.textarea.style.webkitFontSmoothing = 'auto';
22565
+ this.textarea.style.mozOsxFontSmoothing = 'auto';
22566
+ this.textarea.style.fontSmooth = 'auto';
22567
+ this.textarea.style.textSizeAdjust = 'none';
22568
+
22569
+ // For bold fonts, use subpixel rendering to match canvas thickness better
22570
+ const fontWeight = String(target.fontWeight || 'normal');
22571
+ const isBold = fontWeight === 'bold' || fontWeight === '700' || parseInt(fontWeight) >= 600;
22572
+ if (isBold) {
22573
+ this.textarea.style.webkitFontSmoothing = 'subpixel-antialiased';
22574
+ this.textarea.style.mozOsxFontSmoothing = 'unset';
22575
+ console.log('🔤 Applied enhanced bold rendering for better thickness matching');
22576
+ }
22577
+ console.log('🎨 FONT SMOOTHING APPLIED:');
22578
+ console.log(' webkitFontSmoothing:', this.textarea.style.webkitFontSmoothing);
22579
+ console.log(' mozOsxFontSmoothing:', this.textarea.style.mozOsxFontSmoothing);
22145
22580
 
22146
22581
  // Initial bounds are set correctly by Fabric.js - don't force update here
22147
22582
  }
@@ -22376,6 +22811,23 @@
22376
22811
  // Handle commit/cancel after restoring visibility
22377
22812
  if (commit && !this.isComposing) {
22378
22813
  const finalText = this.textarea.value;
22814
+
22815
+ // Auto-detect text direction and update fabric object if needed
22816
+ const detectedDirection = this.firstStrongDir(finalText);
22817
+ const currentDirection = this.target.direction || 'ltr';
22818
+ if (detectedDirection && detectedDirection !== currentDirection) {
22819
+ console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
22820
+ console.log(` Text content: "${finalText.substring(0, 50)}..."`);
22821
+
22822
+ // Update the fabric object's direction
22823
+ this.target.set('direction', detectedDirection);
22824
+
22825
+ // Force a re-render to apply the direction change
22826
+ this.canvas.requestRenderAll();
22827
+ console.log(`✅ Fabric object direction updated to: ${detectedDirection}`);
22828
+ } else {
22829
+ console.log(`📝 Overlay Exit: Direction unchanged (${currentDirection}), text: "${finalText.substring(0, 30)}..."`);
22830
+ }
22379
22831
  if (this.onCommit) {
22380
22832
  this.onCommit(finalText);
22381
22833
  }
@@ -25966,30 +26418,17 @@
25966
26418
  // Detect resize origin during resizing
25967
26419
  this.on('resizing', e => {
25968
26420
  // Check transform origin to determine which side is being resized
25969
- console.log('🔍 Resize event data:', e);
25970
26421
  if (e.transform) {
25971
26422
  const {
25972
- originX,
25973
- originY
26423
+ originX
25974
26424
  } = e.transform;
25975
- console.log('🔍 Transform origins:', {
25976
- originX,
25977
- originY
25978
- });
25979
26425
  // originX tells us which side is the anchor - opposite side is being dragged
25980
26426
  resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
25981
- console.log('🎯 Setting resizeOrigin to:', resizeOrigin);
25982
26427
  } else if (e.originX) {
25983
26428
  const {
25984
- originX,
25985
- originY
26429
+ originX
25986
26430
  } = e;
25987
- console.log('🔍 Event origins:', {
25988
- originX,
25989
- originY
25990
- });
25991
26431
  resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
25992
- console.log('🎯 Setting resizeOrigin to:', resizeOrigin);
25993
26432
  }
25994
26433
  });
25995
26434
 
@@ -25997,9 +26436,6 @@
25997
26436
  // Use 'modified' event which fires after user releases the mouse
25998
26437
  this.on('modified', () => {
25999
26438
  const currentResizeOrigin = resizeOrigin; // Capture the value before reset
26000
- console.log('✅ Modified event fired - resize complete, triggering safety snap', {
26001
- resizeOrigin: currentResizeOrigin
26002
- });
26003
26439
  // Small delay to ensure text layout is updated
26004
26440
  setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
26005
26441
  resizeOrigin = null; // Reset after capturing
@@ -26009,7 +26445,6 @@
26009
26445
  (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.on('object:modified', e => {
26010
26446
  if (e.target === this) {
26011
26447
  const currentResizeOrigin = resizeOrigin; // Capture the value before reset
26012
- console.log('✅ Canvas object:modified fired for this textbox');
26013
26448
  setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
26014
26449
  resizeOrigin = null; // Reset after capturing
26015
26450
  }
@@ -26024,38 +26459,17 @@
26024
26459
  * @param resizeOrigin - Which side was used for resizing ('left' or 'right')
26025
26460
  */
26026
26461
  safetySnapWidth(resizeOrigin) {
26027
- var _this$_textLines;
26028
- console.log('🔍 safetySnapWidth called', {
26029
- isWrapping: this.isWrapping,
26030
- hasTextLines: !!this._textLines,
26031
- lineCount: ((_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length) || 0,
26032
- currentWidth: this.width,
26033
- type: this.type,
26034
- text: this.text
26035
- });
26036
-
26037
26462
  // For Textbox objects, we always want to check for clipping regardless of isWrapping flag
26038
26463
  if (!this._textLines || this.type.toLowerCase() !== 'textbox' || this._textLines.length === 0) {
26039
- var _this$_textLines2;
26040
- console.log('❌ Early return - missing requirements', {
26041
- hasTextLines: !!this._textLines,
26042
- typeMatch: this.type.toLowerCase() === 'textbox',
26043
- actualType: this.type,
26044
- hasLines: ((_this$_textLines2 = this._textLines) === null || _this$_textLines2 === void 0 ? void 0 : _this$_textLines2.length) > 0
26045
- });
26046
26464
  return;
26047
26465
  }
26048
26466
  const lineCount = this._textLines.length;
26049
26467
  if (lineCount === 0) return;
26050
-
26051
- // Check all lines, not just the last one
26052
- let maxActualLineWidth = 0; // Actual measured width without buffers
26053
26468
  let maxRequiredWidth = 0; // Width including RTL buffer
26054
26469
 
26055
26470
  for (let i = 0; i < lineCount; i++) {
26056
26471
  const lineText = this._textLines[i].join(''); // Convert grapheme array to string
26057
26472
  const lineWidth = this.getLineWidth(i);
26058
- maxActualLineWidth = Math.max(maxActualLineWidth, lineWidth);
26059
26473
 
26060
26474
  // RTL detection - regex for Arabic, Hebrew, and other RTL characters
26061
26475
  const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/;
@@ -26075,11 +26489,6 @@
26075
26489
  var _this$canvas2;
26076
26490
  // Set width to exactly what's needed + minimal safety margin
26077
26491
  const newWidth = maxRequiredWidth + 1; // Add just 1px safety margin
26078
- console.log(`Safety snap: ${this.width.toFixed(0)}px -> ${newWidth.toFixed(0)}px`, {
26079
- maxActualLineWidth: maxActualLineWidth.toFixed(1),
26080
- maxRequiredWidth: maxRequiredWidth.toFixed(1),
26081
- difference: (newWidth - this.width).toFixed(1)
26082
- });
26083
26492
 
26084
26493
  // Store original position before width change
26085
26494
  const originalLeft = this.left;
@@ -26095,19 +26504,12 @@
26095
26504
  // Only compensate position when resizing from left handle
26096
26505
  // Right handle resize doesn't shift the text position
26097
26506
  if (resizeOrigin === 'left') {
26098
- console.log('🔧 Compensating for left-side resize', {
26099
- originalLeft,
26100
- widthIncrease,
26101
- newLeft: originalLeft - widthIncrease
26102
- });
26103
26507
  // When resizing from left, the expansion pushes text right
26104
26508
  // Compensate by moving the textbox left by the width increase
26105
26509
  this.set({
26106
26510
  'left': originalLeft - widthIncrease,
26107
26511
  'top': originalTop
26108
26512
  });
26109
- } else {
26110
- console.log('✅ Right-side resize, no compensation needed');
26111
26513
  }
26112
26514
  this.setCoords();
26113
26515