@nasser-sw/fabric 7.0.0-beta1 → 7.0.1-beta10

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 (183) hide show
  1. package/0 +0 -0
  2. package/debug/{konva → konva-master}/CHANGELOG.md +2 -1
  3. package/debug/{konva → konva-master}/README.md +7 -3
  4. package/debug/{konva → konva-master}/package.json +1 -1
  5. package/debug/{konva → konva-master}/release.sh +1 -4
  6. package/debug/{konva → konva-master}/src/Canvas.ts +37 -0
  7. package/debug/{konva → konva-master}/src/shapes/Text.ts +2 -2
  8. package/dist/index.js +2198 -272
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.min.js +1 -1
  11. package/dist/index.min.js.map +1 -1
  12. package/dist/index.min.mjs +1 -1
  13. package/dist/index.min.mjs.map +1 -1
  14. package/dist/index.mjs +2198 -272
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/index.node.cjs +2198 -272
  17. package/dist/index.node.cjs.map +1 -1
  18. package/dist/index.node.mjs +2198 -272
  19. package/dist/index.node.mjs.map +1 -1
  20. package/dist/package.json.min.mjs +1 -1
  21. package/dist/package.json.mjs +1 -1
  22. package/dist/src/shapes/Line.d.ts +33 -86
  23. package/dist/src/shapes/Line.d.ts.map +1 -1
  24. package/dist/src/shapes/Line.min.mjs +1 -1
  25. package/dist/src/shapes/Line.min.mjs.map +1 -1
  26. package/dist/src/shapes/Line.mjs +405 -159
  27. package/dist/src/shapes/Line.mjs.map +1 -1
  28. package/dist/src/shapes/Polyline.d.ts +7 -0
  29. package/dist/src/shapes/Polyline.d.ts.map +1 -1
  30. package/dist/src/shapes/Polyline.min.mjs +1 -1
  31. package/dist/src/shapes/Polyline.min.mjs.map +1 -1
  32. package/dist/src/shapes/Polyline.mjs +48 -16
  33. package/dist/src/shapes/Polyline.mjs.map +1 -1
  34. package/dist/src/shapes/Text/Text.d.ts +19 -0
  35. package/dist/src/shapes/Text/Text.d.ts.map +1 -1
  36. package/dist/src/shapes/Text/Text.min.mjs +1 -1
  37. package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
  38. package/dist/src/shapes/Text/Text.mjs +302 -16
  39. package/dist/src/shapes/Text/Text.mjs.map +1 -1
  40. package/dist/src/shapes/Textbox.d.ts +56 -1
  41. package/dist/src/shapes/Textbox.d.ts.map +1 -1
  42. package/dist/src/shapes/Textbox.min.mjs +1 -1
  43. package/dist/src/shapes/Textbox.min.mjs.map +1 -1
  44. package/dist/src/shapes/Textbox.mjs +633 -11
  45. package/dist/src/shapes/Textbox.mjs.map +1 -1
  46. package/dist/src/shapes/Triangle.d.ts +27 -2
  47. package/dist/src/shapes/Triangle.d.ts.map +1 -1
  48. package/dist/src/shapes/Triangle.min.mjs +1 -1
  49. package/dist/src/shapes/Triangle.min.mjs.map +1 -1
  50. package/dist/src/shapes/Triangle.mjs +72 -12
  51. package/dist/src/shapes/Triangle.mjs.map +1 -1
  52. package/dist/src/text/examples/arabicTextExample.d.ts +60 -0
  53. package/dist/src/text/examples/arabicTextExample.d.ts.map +1 -0
  54. package/dist/src/text/measure.d.ts +9 -0
  55. package/dist/src/text/measure.d.ts.map +1 -1
  56. package/dist/src/text/measure.min.mjs +1 -1
  57. package/dist/src/text/measure.min.mjs.map +1 -1
  58. package/dist/src/text/measure.mjs +175 -4
  59. package/dist/src/text/measure.mjs.map +1 -1
  60. package/dist/src/text/overlayEditor.d.ts +8 -0
  61. package/dist/src/text/overlayEditor.d.ts.map +1 -1
  62. package/dist/src/text/overlayEditor.min.mjs +1 -1
  63. package/dist/src/text/overlayEditor.min.mjs.map +1 -1
  64. package/dist/src/text/overlayEditor.mjs +395 -56
  65. package/dist/src/text/overlayEditor.mjs.map +1 -1
  66. package/dist/src/text/scriptUtils.d.ts +142 -0
  67. package/dist/src/text/scriptUtils.d.ts.map +1 -0
  68. package/dist/src/text/scriptUtils.min.mjs +2 -0
  69. package/dist/src/text/scriptUtils.min.mjs.map +1 -0
  70. package/dist/src/text/scriptUtils.mjs +212 -0
  71. package/dist/src/text/scriptUtils.mjs.map +1 -0
  72. package/dist/src/util/misc/cornerRadius.d.ts +70 -0
  73. package/dist/src/util/misc/cornerRadius.d.ts.map +1 -0
  74. package/dist/src/util/misc/cornerRadius.min.mjs +2 -0
  75. package/dist/src/util/misc/cornerRadius.min.mjs.map +1 -0
  76. package/dist/src/util/misc/cornerRadius.mjs +181 -0
  77. package/dist/src/util/misc/cornerRadius.mjs.map +1 -0
  78. package/dist-extensions/src/shapes/CustomLine.d.ts +10 -0
  79. package/dist-extensions/src/shapes/CustomLine.d.ts.map +1 -0
  80. package/dist-extensions/src/shapes/Line.d.ts +33 -86
  81. package/dist-extensions/src/shapes/Line.d.ts.map +1 -1
  82. package/dist-extensions/src/shapes/Polyline.d.ts +7 -0
  83. package/dist-extensions/src/shapes/Polyline.d.ts.map +1 -1
  84. package/dist-extensions/src/shapes/Text/Text.d.ts +19 -0
  85. package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
  86. package/dist-extensions/src/shapes/Textbox.d.ts +56 -1
  87. package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
  88. package/dist-extensions/src/shapes/Triangle.d.ts +27 -2
  89. package/dist-extensions/src/shapes/Triangle.d.ts.map +1 -1
  90. package/dist-extensions/src/text/measure.d.ts +9 -0
  91. package/dist-extensions/src/text/measure.d.ts.map +1 -1
  92. package/dist-extensions/src/text/overlayEditor.d.ts +8 -0
  93. package/dist-extensions/src/text/overlayEditor.d.ts.map +1 -1
  94. package/dist-extensions/src/text/scriptUtils.d.ts +142 -0
  95. package/dist-extensions/src/text/scriptUtils.d.ts.map +1 -0
  96. package/dist-extensions/src/util/misc/cornerRadius.d.ts +70 -0
  97. package/dist-extensions/src/util/misc/cornerRadius.d.ts.map +1 -0
  98. package/fabric-test-editor.html +3552 -0
  99. package/fabric-test2.html +647 -0
  100. package/fabric.ts +182 -182
  101. package/fonts/STV Bold.ttf +0 -0
  102. package/fonts/STV Light.ttf +0 -0
  103. package/fonts/STV Regular.ttf +0 -0
  104. package/package.json +164 -164
  105. package/src/shapes/Line.ts +484 -157
  106. package/src/shapes/Polyline.ts +70 -29
  107. package/src/shapes/Text/Text.ts +317 -19
  108. package/src/shapes/Textbox.ts +663 -12
  109. package/src/shapes/Triangle.spec.ts +76 -0
  110. package/src/shapes/Triangle.ts +85 -15
  111. package/src/text/measure.ts +200 -50
  112. package/src/text/overlayEditor.ts +504 -94
  113. package/src/util/misc/cornerRadius.spec.ts +141 -0
  114. package/src/util/misc/cornerRadius.ts +269 -0
  115. /package/debug/{konva → konva-master}/LICENSE +0 -0
  116. /package/debug/{konva → konva-master}/gulpfile.mjs +0 -0
  117. /package/debug/{konva → konva-master}/resources/doc-includes/ContainerParams.txt +0 -0
  118. /package/debug/{konva → konva-master}/resources/doc-includes/NodeParams.txt +0 -0
  119. /package/debug/{konva → konva-master}/resources/doc-includes/ShapeParams.txt +0 -0
  120. /package/debug/{konva → konva-master}/resources/jsdoc.conf.json +0 -0
  121. /package/debug/{konva → konva-master}/rollup.config.mjs +0 -0
  122. /package/debug/{konva → konva-master}/src/Animation.ts +0 -0
  123. /package/debug/{konva → konva-master}/src/BezierFunctions.ts +0 -0
  124. /package/debug/{konva → konva-master}/src/Container.ts +0 -0
  125. /package/debug/{konva → konva-master}/src/Context.ts +0 -0
  126. /package/debug/{konva → konva-master}/src/Core.ts +0 -0
  127. /package/debug/{konva → konva-master}/src/DragAndDrop.ts +0 -0
  128. /package/debug/{konva → konva-master}/src/Factory.ts +0 -0
  129. /package/debug/{konva → konva-master}/src/FastLayer.ts +0 -0
  130. /package/debug/{konva → konva-master}/src/Global.ts +0 -0
  131. /package/debug/{konva → konva-master}/src/Group.ts +0 -0
  132. /package/debug/{konva → konva-master}/src/Layer.ts +0 -0
  133. /package/debug/{konva → konva-master}/src/Node.ts +0 -0
  134. /package/debug/{konva → konva-master}/src/PointerEvents.ts +0 -0
  135. /package/debug/{konva → konva-master}/src/Shape.ts +0 -0
  136. /package/debug/{konva → konva-master}/src/Stage.ts +0 -0
  137. /package/debug/{konva → konva-master}/src/Tween.ts +0 -0
  138. /package/debug/{konva → konva-master}/src/Util.ts +0 -0
  139. /package/debug/{konva → konva-master}/src/Validators.ts +0 -0
  140. /package/debug/{konva → konva-master}/src/_CoreInternals.ts +0 -0
  141. /package/debug/{konva → konva-master}/src/_FullInternals.ts +0 -0
  142. /package/debug/{konva → konva-master}/src/canvas-backend.ts +0 -0
  143. /package/debug/{konva → konva-master}/src/filters/Blur.ts +0 -0
  144. /package/debug/{konva → konva-master}/src/filters/Brighten.ts +0 -0
  145. /package/debug/{konva → konva-master}/src/filters/Brightness.ts +0 -0
  146. /package/debug/{konva → konva-master}/src/filters/Contrast.ts +0 -0
  147. /package/debug/{konva → konva-master}/src/filters/Emboss.ts +0 -0
  148. /package/debug/{konva → konva-master}/src/filters/Enhance.ts +0 -0
  149. /package/debug/{konva → konva-master}/src/filters/Grayscale.ts +0 -0
  150. /package/debug/{konva → konva-master}/src/filters/HSL.ts +0 -0
  151. /package/debug/{konva → konva-master}/src/filters/HSV.ts +0 -0
  152. /package/debug/{konva → konva-master}/src/filters/Invert.ts +0 -0
  153. /package/debug/{konva → konva-master}/src/filters/Kaleidoscope.ts +0 -0
  154. /package/debug/{konva → konva-master}/src/filters/Mask.ts +0 -0
  155. /package/debug/{konva → konva-master}/src/filters/Noise.ts +0 -0
  156. /package/debug/{konva → konva-master}/src/filters/Pixelate.ts +0 -0
  157. /package/debug/{konva → konva-master}/src/filters/Posterize.ts +0 -0
  158. /package/debug/{konva → konva-master}/src/filters/RGB.ts +0 -0
  159. /package/debug/{konva → konva-master}/src/filters/RGBA.ts +0 -0
  160. /package/debug/{konva → konva-master}/src/filters/Sepia.ts +0 -0
  161. /package/debug/{konva → konva-master}/src/filters/Solarize.ts +0 -0
  162. /package/debug/{konva → konva-master}/src/filters/Threshold.ts +0 -0
  163. /package/debug/{konva → konva-master}/src/index.ts +0 -0
  164. /package/debug/{konva → konva-master}/src/shapes/Arc.ts +0 -0
  165. /package/debug/{konva → konva-master}/src/shapes/Arrow.ts +0 -0
  166. /package/debug/{konva → konva-master}/src/shapes/Circle.ts +0 -0
  167. /package/debug/{konva → konva-master}/src/shapes/Ellipse.ts +0 -0
  168. /package/debug/{konva → konva-master}/src/shapes/Image.ts +0 -0
  169. /package/debug/{konva → konva-master}/src/shapes/Label.ts +0 -0
  170. /package/debug/{konva → konva-master}/src/shapes/Line.ts +0 -0
  171. /package/debug/{konva → konva-master}/src/shapes/Path.ts +0 -0
  172. /package/debug/{konva → konva-master}/src/shapes/Rect.ts +0 -0
  173. /package/debug/{konva → konva-master}/src/shapes/RegularPolygon.ts +0 -0
  174. /package/debug/{konva → konva-master}/src/shapes/Ring.ts +0 -0
  175. /package/debug/{konva → konva-master}/src/shapes/Sprite.ts +0 -0
  176. /package/debug/{konva → konva-master}/src/shapes/Star.ts +0 -0
  177. /package/debug/{konva → konva-master}/src/shapes/TextPath.ts +0 -0
  178. /package/debug/{konva → konva-master}/src/shapes/Transformer.ts +0 -0
  179. /package/debug/{konva → konva-master}/src/shapes/Wedge.ts +0 -0
  180. /package/debug/{konva → konva-master}/src/skia-backend.ts +0 -0
  181. /package/debug/{konva → konva-master}/src/types.ts +0 -0
  182. /package/debug/{konva → konva-master}/tsconfig.json +0 -0
  183. /package/debug/{konva → konva-master}/tsconfig.test.json +0 -0
@@ -412,7 +412,7 @@ class Cache {
412
412
  }
413
413
  const cache = new Cache();
414
414
 
415
- var version = "7.0.0-beta1";
415
+ var version = "7.0.1-beta9";
416
416
 
417
417
  // use this syntax so babel plugin see this import here
418
418
  const VERSION = version;
@@ -17629,33 +17629,30 @@ class PatternBrush extends PencilBrush {
17629
17629
  }
17630
17630
  }
17631
17631
 
17632
- // @TODO this code is terrible and Line should be a special case of polyline.
17633
-
17634
17632
  const coordProps = ['x1', 'x2', 'y1', 'y2'];
17635
- /**
17636
- * A Class to draw a line
17637
- * A bunch of methods will be added to Polyline to handle the line case
17638
- * The line class is very strange to work with, is all special, it hardly aligns
17639
- * to what a developer want everytime there is an angle
17640
- * @deprecated
17641
- */
17642
17633
  class Line extends FabricObject {
17643
- /**
17644
- * Constructor
17645
- * @param {Array} [points] Array of points
17646
- * @param {Object} [options] Options object
17647
- * @return {Line} thisArg
17648
- */
17649
17634
  constructor() {
17650
- let [x1, y1, x2, y2] = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0, 0, 0];
17635
+ let [x1, y1, x2, y2] = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0, 100, 0];
17651
17636
  let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
17652
17637
  super();
17653
- Object.assign(this, Line.ownDefaults);
17638
+ _defineProperty(this, "hitStrokeWidth", 'auto');
17639
+ _defineProperty(this, "_updatingEndpoints", false);
17640
+ _defineProperty(this, "_useEndpointCoords", true);
17641
+ _defineProperty(this, "_exportingSVG", false);
17654
17642
  this.setOptions(options);
17655
17643
  this.x1 = x1;
17656
17644
  this.x2 = x2;
17657
17645
  this.y1 = y1;
17658
17646
  this.y2 = y2;
17647
+ if (options.hitStrokeWidth !== undefined) {
17648
+ this.hitStrokeWidth = options.hitStrokeWidth;
17649
+ }
17650
+ this.hasBorders = false;
17651
+ this.hasControls = true;
17652
+ this.selectable = true;
17653
+ this.hoverCursor = 'move';
17654
+ this.perPixelTargetFind = false;
17655
+ this.strokeLineCap = 'butt';
17659
17656
  this._setWidthHeight();
17660
17657
  const {
17661
17658
  left,
@@ -17663,129 +17660,384 @@ class Line extends FabricObject {
17663
17660
  } = options;
17664
17661
  typeof left === 'number' && this.set(LEFT, left);
17665
17662
  typeof top === 'number' && this.set(TOP, top);
17663
+ this._setupLineControls();
17664
+ }
17665
+ _setupLineControls() {
17666
+ this.controls = {
17667
+ p1: new Control({
17668
+ x: 0,
17669
+ y: 0,
17670
+ cursorStyle: 'move',
17671
+ actionHandler: this._endpointActionHandler.bind(this),
17672
+ positionHandler: this._p1PositionHandler.bind(this),
17673
+ render: this._renderEndpointControl.bind(this),
17674
+ sizeX: 12,
17675
+ sizeY: 12
17676
+ }),
17677
+ p2: new Control({
17678
+ x: 0,
17679
+ y: 0,
17680
+ cursorStyle: 'move',
17681
+ actionHandler: this._endpointActionHandler.bind(this),
17682
+ positionHandler: this._p2PositionHandler.bind(this),
17683
+ render: this._renderEndpointControl.bind(this),
17684
+ sizeX: 12,
17685
+ sizeY: 12
17686
+ })
17687
+ };
17688
+ }
17689
+ _p1PositionHandler() {
17690
+ return new Point(this.x1, this.y1).transform(this.getViewportTransform());
17666
17691
  }
17692
+ _p2PositionHandler() {
17693
+ return new Point(this.x2, this.y2).transform(this.getViewportTransform());
17694
+ }
17695
+ _renderEndpointControl(ctx, left, top) {
17696
+ const size = 12;
17697
+ ctx.save();
17698
+ ctx.fillStyle = '#007bff';
17699
+ ctx.strokeStyle = '#ffffff';
17700
+ ctx.lineWidth = 2;
17701
+ ctx.beginPath();
17702
+ ctx.arc(left, top, size / 2, 0, 2 * Math.PI);
17703
+ ctx.fill();
17704
+ ctx.stroke();
17705
+ ctx.restore();
17706
+ }
17707
+ drawBorders(ctx) {
17708
+ let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
17709
+ if (this._useEndpointCoords) {
17710
+ this._drawLineBorders(ctx, styleOverride);
17711
+ return this;
17712
+ }
17713
+ return super.drawBorders(ctx, styleOverride, {});
17714
+ }
17715
+ _drawLineBorders(ctx) {
17716
+ var _this$canvas;
17717
+ let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
17718
+ const vpt = ((_this$canvas = this.canvas) === null || _this$canvas === void 0 ? void 0 : _this$canvas.viewportTransform) || [1, 0, 0, 1, 0, 0];
17719
+ ctx.save();
17720
+ ctx.setTransform(vpt[0], vpt[1], vpt[2], vpt[3], vpt[4], vpt[5]);
17721
+ ctx.strokeStyle = styleOverride.borderColor || this.borderColor || 'rgba(100, 200, 200, 0.5)';
17722
+ ctx.lineWidth = (this.strokeWidth || 1) + 5;
17723
+ ctx.lineCap = this.strokeLineCap || 'butt';
17724
+ ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
17725
+ ctx.beginPath();
17726
+ ctx.moveTo(this.x1, this.y1);
17727
+ ctx.lineTo(this.x2, this.y2);
17728
+ ctx.stroke();
17729
+ ctx.restore();
17730
+ }
17731
+ _renderControls(ctx) {
17732
+ let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
17733
+ ctx.save();
17734
+ ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
17735
+ this.drawControls(ctx, styleOverride);
17736
+ ctx.restore();
17737
+ }
17738
+ getBoundingRect() {
17739
+ if (this._useEndpointCoords) {
17740
+ const {
17741
+ x1,
17742
+ y1,
17743
+ x2,
17744
+ y2
17745
+ } = this;
17746
+ const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
17747
+ const padding = Math.max(effectiveStrokeWidth / 2 + 5, 10);
17748
+ return {
17749
+ left: Math.min(x1, x2) - padding,
17750
+ top: Math.min(y1, y2) - padding,
17751
+ width: Math.abs(x2 - x1) + padding * 2 || padding * 2,
17752
+ height: Math.abs(y2 - y1) + padding * 2 || padding * 2
17753
+ };
17754
+ }
17755
+ return super.getBoundingRect();
17756
+ }
17757
+ setCoords() {
17758
+ if (this._useEndpointCoords) {
17759
+ // Set width and height for hit detection and bounding box
17760
+ const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
17761
+ const hitPadding = Math.max(effectiveStrokeWidth / 2 + 5, 10);
17762
+ this.width = Math.abs(this.x2 - this.x1) + hitPadding * 2;
17763
+ this.height = Math.abs(this.y2 - this.y1) + hitPadding * 2;
17667
17764
 
17668
- /**
17669
- * @private
17670
- * @param {Object} [options] Options
17671
- */
17672
- _setWidthHeight() {
17673
- const {
17674
- x1,
17675
- y1,
17676
- x2,
17677
- y2
17678
- } = this;
17679
- this.width = Math.abs(x2 - x1);
17680
- this.height = Math.abs(y2 - y1);
17681
- const {
17682
- left,
17683
- top,
17684
- width,
17685
- height
17686
- } = makeBoundingBoxFromPoints([{
17687
- x: x1,
17688
- y: y1
17689
- }, {
17690
- x: x2,
17691
- y: y2
17692
- }]);
17693
- const position = new Point(left + width / 2, top + height / 2);
17694
- this.setPositionByOrigin(position, CENTER, CENTER);
17765
+ // Only update left/top if they haven't been explicitly set (e.g., during loading)
17766
+ if (this.left === 0 && this.top === 0) {
17767
+ const center = this._findCenterFromElement();
17768
+ this.left = center.x;
17769
+ this.top = center.y;
17770
+ }
17771
+ }
17772
+ super.setCoords();
17695
17773
  }
17774
+ getCoords() {
17775
+ if (this._useEndpointCoords) {
17776
+ const deltaX = this.x2 - this.x1;
17777
+ const deltaY = this.y2 - this.y1;
17778
+ const length = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
17779
+ if (length === 0) {
17780
+ return super.getCoords();
17781
+ }
17782
+ const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
17783
+ const halfWidth = Math.max(effectiveStrokeWidth / 2 + 2, 5);
17696
17784
 
17697
- /**
17698
- * @private
17699
- * @param {String} key
17700
- * @param {*} value
17701
- */
17785
+ // Unit vector perpendicular to line
17786
+ const perpX = -deltaY / length;
17787
+ const perpY = deltaX / length;
17788
+
17789
+ // Four corners of oriented rectangle
17790
+ return [new Point(this.x1 + perpX * halfWidth, this.y1 + perpY * halfWidth), new Point(this.x2 + perpX * halfWidth, this.y2 + perpY * halfWidth), new Point(this.x2 - perpX * halfWidth, this.y2 - perpY * halfWidth), new Point(this.x1 - perpX * halfWidth, this.y1 - perpY * halfWidth)];
17791
+ }
17792
+ return super.getCoords();
17793
+ }
17794
+ containsPoint(point) {
17795
+ if (this._useEndpointCoords) {
17796
+ var _this$canvas2;
17797
+ if (((_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 ? void 0 : _this$canvas2.getActiveObject()) === this) {
17798
+ return super.containsPoint(point);
17799
+ }
17800
+ const distance = this._distanceToLineSegment(point.x, point.y);
17801
+ const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth || 1;
17802
+ const tolerance = Math.max(effectiveStrokeWidth / 2 + 2, 5);
17803
+ return distance <= tolerance;
17804
+ }
17805
+ return super.containsPoint(point);
17806
+ }
17807
+ _distanceToLineSegment(px, py) {
17808
+ const x1 = this.x1,
17809
+ y1 = this.y1,
17810
+ x2 = this.x2,
17811
+ y2 = this.y2;
17812
+ const pd2 = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
17813
+ if (pd2 === 0) {
17814
+ return Math.sqrt((px - x1) * (px - x1) + (py - y1) * (py - y1));
17815
+ }
17816
+ const u = ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / pd2;
17817
+ let closestX, closestY;
17818
+ if (u < 0) {
17819
+ closestX = x1;
17820
+ closestY = y1;
17821
+ } else if (u > 1) {
17822
+ closestX = x2;
17823
+ closestY = y2;
17824
+ } else {
17825
+ closestX = x1 + u * (x2 - x1);
17826
+ closestY = y1 + u * (y2 - y1);
17827
+ }
17828
+ return Math.sqrt((px - closestX) * (px - closestX) + (py - closestY) * (py - closestY));
17829
+ }
17830
+ _endpointActionHandler(eventData, transformData, x, y) {
17831
+ var _this$canvas4;
17832
+ const controlKey = transformData.corner;
17833
+ const pointer = new Point(x, y);
17834
+ let newX = pointer.x;
17835
+ let newY = pointer.y;
17836
+ if (eventData.shiftKey) {
17837
+ const otherControl = controlKey === 'p1' ? 'p2' : 'p1';
17838
+ const otherX = this[otherControl === 'p1' ? 'x1' : 'x2'];
17839
+ const otherY = this[otherControl === 'p1' ? 'y1' : 'y2'];
17840
+ const snapped = this._snapToAngle(otherX, otherY, newX, newY);
17841
+ newX = snapped.x;
17842
+ newY = snapped.y;
17843
+ }
17844
+ if (this._useEndpointCoords) {
17845
+ var _this$canvas3;
17846
+ if (controlKey === 'p1') {
17847
+ this.x1 = newX;
17848
+ this.y1 = newY;
17849
+ } else if (controlKey === 'p2') {
17850
+ this.x2 = newX;
17851
+ this.y2 = newY;
17852
+ }
17853
+
17854
+ // Update gradient coordinates if stroke is a gradient (but not during SVG export)
17855
+ if (this.stroke instanceof Gradient && !this._exportingSVG) {
17856
+ this.stroke.coords.x1 = this.x1;
17857
+ this.stroke.coords.y1 = this.y1;
17858
+ this.stroke.coords.x2 = this.x2;
17859
+ this.stroke.coords.y2 = this.y2;
17860
+ }
17861
+ this.dirty = true;
17862
+ this.setCoords();
17863
+ (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
17864
+ return true;
17865
+ }
17866
+
17867
+ // Fallback for old system
17868
+ this._updatingEndpoints = true;
17869
+ if (controlKey === 'p1') {
17870
+ this.x1 = newX;
17871
+ this.y1 = newY;
17872
+ } else if (controlKey === 'p2') {
17873
+ this.x2 = newX;
17874
+ this.y2 = newY;
17875
+ }
17876
+ this._setWidthHeight();
17877
+ this.dirty = true;
17878
+ this._updatingEndpoints = false;
17879
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.requestRenderAll();
17880
+ this.fire('modified', {
17881
+ transform: transformData,
17882
+ target: this,
17883
+ e: eventData
17884
+ });
17885
+ return true;
17886
+ }
17887
+ _snapToAngle(fromX, fromY, toX, toY) {
17888
+ const deltaX = toX - fromX;
17889
+ const deltaY = toY - fromY;
17890
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
17891
+ if (distance === 0) return {
17892
+ x: toX,
17893
+ y: toY
17894
+ };
17895
+ let angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
17896
+ const snapIncrement = 15;
17897
+ const snappedAngle = Math.round(angle / snapIncrement) * snapIncrement;
17898
+ const snappedRadians = snappedAngle * (Math.PI / 180);
17899
+ return {
17900
+ x: fromX + Math.cos(snappedRadians) * distance,
17901
+ y: fromY + Math.sin(snappedRadians) * distance
17902
+ };
17903
+ }
17904
+ _setWidthHeight() {
17905
+ let skipReposition = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
17906
+ this.width = Math.abs(this.x2 - this.x1) || 1;
17907
+ this.height = Math.abs(this.y2 - this.y1) || 1;
17908
+ if (!skipReposition && !this._updatingEndpoints) {
17909
+ const {
17910
+ left,
17911
+ top,
17912
+ width,
17913
+ height
17914
+ } = makeBoundingBoxFromPoints([{
17915
+ x: this.x1,
17916
+ y: this.y1
17917
+ }, {
17918
+ x: this.x2,
17919
+ y: this.y2
17920
+ }]);
17921
+ this.setPositionByOrigin(new Point(left + width / 2, top + height / 2), CENTER, CENTER);
17922
+ }
17923
+ }
17702
17924
  _set(key, value) {
17925
+ const oldLeft = this.left;
17926
+ const oldTop = this.top;
17703
17927
  super._set(key, value);
17704
17928
  if (coordProps.includes(key)) {
17705
- // this doesn't make sense very much, since setting x1 when top or left
17706
- // are already set, is just going to show a strange result since the
17707
- // line will move way more than the developer expect.
17708
- // in fabric5 it worked only when the line didn't have extra transformations,
17709
- // in fabric6 too. With extra transform they behave bad in different ways.
17710
- // This needs probably a good rework or a tutorial if you have to create a dynamic line
17711
17929
  this._setWidthHeight();
17930
+ this.dirty = true;
17931
+
17932
+ // Update gradient coordinates if stroke is a gradient (but not during SVG export)
17933
+ if (this.stroke instanceof Gradient && !this._exportingSVG) {
17934
+ this.stroke.coords.x1 = this.x1;
17935
+ this.stroke.coords.y1 = this.y1;
17936
+ this.stroke.coords.x2 = this.x2;
17937
+ this.stroke.coords.y2 = this.y2;
17938
+ }
17939
+ }
17940
+ if ((key === 'left' || key === 'top') && this.canvas && !this._updatingEndpoints) {
17941
+ const deltaX = this.left - oldLeft;
17942
+ const deltaY = this.top - oldTop;
17943
+ if (deltaX !== 0 || deltaY !== 0) {
17944
+ this._updatingEndpoints = true;
17945
+ this.x1 += deltaX;
17946
+ this.y1 += deltaY;
17947
+ this.x2 += deltaX;
17948
+ this.y2 += deltaY;
17949
+
17950
+ // Update gradient coordinates if stroke is a gradient
17951
+ if (this.stroke instanceof Gradient) {
17952
+ this.stroke.coords.x1 = this.x1;
17953
+ this.stroke.coords.y1 = this.y1;
17954
+ this.stroke.coords.x2 = this.x2;
17955
+ this.stroke.coords.y2 = this.y2;
17956
+ }
17957
+ this._updatingEndpoints = false;
17958
+ }
17712
17959
  }
17713
17960
  return this;
17714
17961
  }
17715
-
17716
- /**
17717
- * @private
17718
- * @param {CanvasRenderingContext2D} ctx Context to render on
17719
- */
17962
+ render(ctx) {
17963
+ if (this._useEndpointCoords) {
17964
+ this._renderDirectly(ctx);
17965
+ return;
17966
+ }
17967
+ super.render(ctx);
17968
+ }
17969
+ _renderDirectly(ctx) {
17970
+ if (!this.visible) return;
17971
+ ctx.save();
17972
+ ctx.globalAlpha = this.opacity;
17973
+ ctx.lineWidth = this.strokeWidth;
17974
+ ctx.lineCap = this.strokeLineCap || 'butt';
17975
+ ctx.beginPath();
17976
+ ctx.moveTo(this.x1, this.y1);
17977
+ ctx.lineTo(this.x2, this.y2);
17978
+ const origStrokeStyle = ctx.strokeStyle;
17979
+ if (isFiller(this.stroke)) {
17980
+ ctx.strokeStyle = this.stroke.toLive(ctx);
17981
+ } else {
17982
+ var _this$stroke;
17983
+ ctx.strokeStyle = ((_this$stroke = this.stroke) === null || _this$stroke === void 0 ? void 0 : _this$stroke.toString()) || '#000';
17984
+ }
17985
+ ctx.stroke();
17986
+ ctx.strokeStyle = origStrokeStyle;
17987
+ ctx.restore();
17988
+ }
17720
17989
  _render(ctx) {
17990
+ if (this._useEndpointCoords) return;
17721
17991
  ctx.beginPath();
17722
17992
  const p = this.calcLinePoints();
17723
17993
  ctx.moveTo(p.x1, p.y1);
17724
17994
  ctx.lineTo(p.x2, p.y2);
17725
17995
  ctx.lineWidth = this.strokeWidth;
17726
-
17727
- // TODO: test this
17728
- // make sure setting "fill" changes color of a line
17729
- // (by copying fillStyle to strokeStyle, since line is stroked, not filled)
17730
17996
  const origStrokeStyle = ctx.strokeStyle;
17731
17997
  if (isFiller(this.stroke)) {
17732
17998
  ctx.strokeStyle = this.stroke.toLive(ctx);
17733
- } else {
17734
- var _this$stroke;
17735
- ctx.strokeStyle = (_this$stroke = this.stroke) !== null && _this$stroke !== void 0 ? _this$stroke : ctx.fillStyle;
17736
17999
  }
17737
18000
  this.stroke && this._renderStroke(ctx);
17738
18001
  ctx.strokeStyle = origStrokeStyle;
17739
18002
  }
17740
-
17741
- /**
17742
- * This function is an helper for svg import. it returns the center of the object in the svg
17743
- * untransformed coordinates
17744
- * @private
17745
- * @return {Point} center point from element coordinates
17746
- */
17747
18003
  _findCenterFromElement() {
17748
18004
  return new Point((this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2);
17749
18005
  }
17750
-
17751
- /**
17752
- * Returns object representation of an instance
17753
- * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
17754
- * @return {Object} object representation of an instance
17755
- */
17756
18006
  toObject() {
17757
18007
  let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
18008
+ if (this._useEndpointCoords) {
18009
+ return {
18010
+ ...super.toObject(propertiesToInclude),
18011
+ x1: this.x1,
18012
+ y1: this.y1,
18013
+ x2: this.x2,
18014
+ y2: this.y2
18015
+ };
18016
+ }
17758
18017
  return {
17759
18018
  ...super.toObject(propertiesToInclude),
17760
18019
  ...this.calcLinePoints()
17761
18020
  };
17762
18021
  }
17763
-
17764
- /*
17765
- * Calculate object dimensions from its properties
17766
- * @private
17767
- */
17768
18022
  _getNonTransformedDimensions() {
17769
18023
  const dim = super._getNonTransformedDimensions();
17770
- if (this.strokeLineCap === 'butt') {
17771
- if (this.width === 0) {
17772
- dim.y -= this.strokeWidth;
17773
- }
17774
- if (this.height === 0) {
17775
- dim.x -= this.strokeWidth;
17776
- }
18024
+ if (this.strokeLineCap === 'round') {
18025
+ dim.x += this.strokeWidth;
18026
+ dim.y += this.strokeWidth;
17777
18027
  }
17778
18028
  return dim;
17779
18029
  }
17780
-
17781
- /**
17782
- * Recalculates line points given width and height
17783
- * Those points are simply placed around the center,
17784
- * This is not useful outside internal render functions and svg output
17785
- * Is not meant to be for the developer.
17786
- * @private
17787
- */
17788
18030
  calcLinePoints() {
18031
+ if (this._updatingEndpoints) {
18032
+ const centerX = (this.x1 + this.x2) / 2;
18033
+ const centerY = (this.y1 + this.y2) / 2;
18034
+ return {
18035
+ x1: this.x1 - centerX,
18036
+ y1: this.y1 - centerY,
18037
+ x2: this.x2 - centerX,
18038
+ y2: this.y2 - centerY
18039
+ };
18040
+ }
17789
18041
  const {
17790
18042
  x1: _x1,
17791
18043
  x2: _x2,
@@ -17794,48 +18046,64 @@ class Line extends FabricObject {
17794
18046
  width,
17795
18047
  height
17796
18048
  } = this;
17797
- const xMult = _x1 <= _x2 ? -1 : 1,
17798
- yMult = _y1 <= _y2 ? -1 : 1,
17799
- x1 = xMult * width / 2,
17800
- y1 = yMult * height / 2,
17801
- x2 = xMult * -width / 2,
17802
- y2 = yMult * -height / 2;
18049
+ const xMult = _x1 <= _x2 ? -1 : 1;
18050
+ const yMult = _y1 <= _y2 ? -1 : 1;
17803
18051
  return {
17804
- x1,
17805
- x2,
17806
- y1,
17807
- y2
18052
+ x1: xMult * width / 2,
18053
+ y1: yMult * height / 2,
18054
+ x2: xMult * -width / 2,
18055
+ y2: yMult * -height / 2
17808
18056
  };
17809
18057
  }
17810
-
17811
- /* _FROM_SVG_START_ */
17812
-
17813
- /**
17814
- * Returns svg representation of an instance
17815
- * @return {Array} an array of strings with the specific svg representation
17816
- * of the instance
17817
- */
17818
18058
  _toSVG() {
17819
- const {
17820
- x1,
17821
- x2,
17822
- y1,
17823
- y2
17824
- } = this.calcLinePoints();
17825
- return ['<line ', 'COMMON_PARTS', `x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" />\n`];
18059
+ if (this._useEndpointCoords) {
18060
+ // Use absolute coordinates to bypass all Fabric.js transforms
18061
+ // Handle gradients manually for proper SVG export
18062
+ let strokeAttr = '';
18063
+ if (this.stroke instanceof Gradient) {
18064
+ // Let Fabric.js handle gradient definition, but we'll use the reference
18065
+ strokeAttr = `stroke="url(#${this.stroke.id})"`;
18066
+ } else {
18067
+ strokeAttr = `stroke="${this.stroke || 'none'}"`;
18068
+ }
18069
+ return [`<line ${strokeAttr} stroke-width="${this.strokeWidth}" stroke-linecap="${this.strokeLineCap}" `, `stroke-dasharray="${this.strokeDashArray ? this.strokeDashArray.join(' ') : 'none'}" `, `stroke-dashoffset="${this.strokeDashOffset}" stroke-linejoin="${this.strokeLineJoin}" `, `stroke-miterlimit="${this.strokeMiterLimit}" fill="${this.fill || 'none'}" `, `fill-rule="${this.fillRule}" opacity="${this.opacity}" `, `x1="${this.x1}" y1="${this.y1}" x2="${this.x2}" y2="${this.y2}" />\n`];
18070
+ } else {
18071
+ // Use standard calcLinePoints for legacy mode
18072
+ const {
18073
+ x1,
18074
+ x2,
18075
+ y1,
18076
+ y2
18077
+ } = this.calcLinePoints();
18078
+ return ['<line ', 'COMMON_PARTS', `x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" />\n`];
18079
+ }
17826
18080
  }
18081
+ toSVG(reviver) {
18082
+ if (this._useEndpointCoords) {
18083
+ // For endpoint coords, we need to bypass transforms but still allow gradients
18084
+ // Let's temporarily disable transforms during SVG generation
18085
+ const originalLeft = this.left;
18086
+ const originalTop = this.top;
17827
18087
 
17828
- /**
17829
- * List of attribute names to account for when parsing SVG element (used by {@link Line.fromElement})
17830
- * @see http://www.w3.org/TR/SVG/shapes.html#LineElement
17831
- */
18088
+ // Set position to center of line for gradient calculation
18089
+ this.left = (this.x1 + this.x2) / 2;
18090
+ this.top = (this.y1 + this.y2) / 2;
17832
18091
 
17833
- /**
17834
- * Returns Line instance from an SVG element
17835
- * @param {HTMLElement} element Element to parse
17836
- * @param {Object} [options] Options object
17837
- * @param {Function} [callback] callback function invoked after parsing
17838
- */
18092
+ // Get the SVG with standard system (for gradient handling)
18093
+ const standardSVG = super.toSVG(reviver);
18094
+
18095
+ // Restore original position
18096
+ this.left = originalLeft;
18097
+ this.top = originalTop;
18098
+
18099
+ // Extract gradient definition and clean up the line element
18100
+ // Remove the transform wrapper and update coordinates
18101
+ const cleanSVG = standardSVG.replace(/<g transform="[^"]*"[^>]*>/g, '').replace(/<\/g>/g, '').replace(/x1="[^"]*"/g, `x1="${this.x1}"`).replace(/y1="[^"]*"/g, `y1="${this.y1}"`).replace(/x2="[^"]*"/g, `x2="${this.x2}"`).replace(/y2="[^"]*"/g, `y2="${this.y2}"`);
18102
+ return cleanSVG;
18103
+ }
18104
+ // Use default behavior for legacy mode
18105
+ return super.toSVG(reviver);
18106
+ }
17839
18107
  static async fromElement(element, options, cssRules) {
17840
18108
  const {
17841
18109
  x1 = 0,
@@ -17846,14 +18114,6 @@ class Line extends FabricObject {
17846
18114
  } = parseAttributes(element, this.ATTRIBUTE_NAMES, cssRules);
17847
18115
  return new this([x1, y1, x2, y2], parsedAttributes);
17848
18116
  }
17849
-
17850
- /* _FROM_SVG_END_ */
17851
-
17852
- /**
17853
- * Returns Line instance from an object representation
17854
- * @param {Object} object Object to create an instance from
17855
- * @returns {Promise<Line>}
17856
- */
17857
18117
  static fromObject(_ref) {
17858
18118
  let {
17859
18119
  x1,
@@ -17870,32 +18130,195 @@ class Line extends FabricObject {
17870
18130
  });
17871
18131
  }
17872
18132
  }
18133
+ _defineProperty(Line, "type", 'Line');
18134
+ _defineProperty(Line, "cacheProperties", [...cacheProperties, ...coordProps]);
18135
+ _defineProperty(Line, "ATTRIBUTE_NAMES", SHARED_ATTRIBUTES.concat(coordProps));
18136
+ classRegistry.setClass(Line);
18137
+ classRegistry.setSVGClass(Line);
18138
+
17873
18139
  /**
17874
- * x value or first line edge
17875
- * @type number
18140
+ * Calculate the distance between two points
17876
18141
  */
18142
+ function pointDistance(p1, p2) {
18143
+ return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
18144
+ }
18145
+
17877
18146
  /**
17878
- * y value or first line edge
17879
- * @type number
18147
+ * Normalize a vector
17880
18148
  */
18149
+ function normalizeVector(vector) {
18150
+ const length = Math.sqrt(vector.x * vector.x + vector.y * vector.y);
18151
+ if (length === 0) return {
18152
+ x: 0,
18153
+ y: 0
18154
+ };
18155
+ return {
18156
+ x: vector.x / length,
18157
+ y: vector.y / length
18158
+ };
18159
+ }
18160
+
17881
18161
  /**
17882
- * x value or second line edge
17883
- * @type number
18162
+ * Get the maximum allowed radius for a corner based on adjacent edge lengths
17884
18163
  */
18164
+ function getMaxRadius(prevPoint, currentPoint, nextPoint) {
18165
+ const dist1 = pointDistance(prevPoint, currentPoint);
18166
+ const dist2 = pointDistance(currentPoint, nextPoint);
18167
+ return Math.min(dist1, dist2) / 2;
18168
+ }
18169
+
17885
18170
  /**
17886
- * y value or second line edge
17887
- * @type number
18171
+ * Calculate rounded corner data for a single corner
17888
18172
  */
17889
- _defineProperty(Line, "type", 'Line');
17890
- _defineProperty(Line, "cacheProperties", [...cacheProperties, ...coordProps]);
17891
- _defineProperty(Line, "ATTRIBUTE_NAMES", SHARED_ATTRIBUTES.concat(coordProps));
17892
- classRegistry.setClass(Line);
17893
- classRegistry.setSVGClass(Line);
18173
+ function calculateRoundedCorner(prevPoint, currentPoint, nextPoint, radius) {
18174
+ // Calculate edge vectors
18175
+ const edge1 = {
18176
+ x: currentPoint.x - prevPoint.x,
18177
+ y: currentPoint.y - prevPoint.y
18178
+ };
18179
+ const edge2 = {
18180
+ x: nextPoint.x - currentPoint.x,
18181
+ y: nextPoint.y - currentPoint.y
18182
+ };
18183
+
18184
+ // Normalize edge vectors
18185
+ const norm1 = normalizeVector(edge1);
18186
+ const norm2 = normalizeVector(edge2);
18187
+
18188
+ // Calculate the maximum allowed radius
18189
+ const maxRadius = getMaxRadius(prevPoint, currentPoint, nextPoint);
18190
+ const actualRadius = Math.min(radius, maxRadius);
18191
+
18192
+ // Calculate start and end points of the rounded corner
18193
+ const startPoint = {
18194
+ x: currentPoint.x - norm1.x * actualRadius,
18195
+ y: currentPoint.y - norm1.y * actualRadius
18196
+ };
18197
+ const endPoint = {
18198
+ x: currentPoint.x + norm2.x * actualRadius,
18199
+ y: currentPoint.y + norm2.y * actualRadius
18200
+ };
18201
+
18202
+ // Calculate control points for bezier curve
18203
+ // Using the magic number kRect for optimal circular approximation
18204
+ const controlOffset = actualRadius * kRect;
18205
+ const cp1 = {
18206
+ x: startPoint.x + norm1.x * controlOffset,
18207
+ y: startPoint.y + norm1.y * controlOffset
18208
+ };
18209
+ const cp2 = {
18210
+ x: endPoint.x - norm2.x * controlOffset,
18211
+ y: endPoint.y - norm2.y * controlOffset
18212
+ };
18213
+ return {
18214
+ corner: currentPoint,
18215
+ start: startPoint,
18216
+ end: endPoint,
18217
+ cp1,
18218
+ cp2,
18219
+ actualRadius
18220
+ };
18221
+ }
18222
+
18223
+ /**
18224
+ * Apply corner radius to a polygon defined by points
18225
+ */
18226
+ function applyCornerRadiusToPolygon(points, radius) {
18227
+ let radiusAsPercentage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
18228
+ if (points.length < 3) {
18229
+ throw new Error('Polygon must have at least 3 points');
18230
+ }
18231
+
18232
+ // Calculate bounding box if radius is percentage-based
18233
+ let actualRadius = radius;
18234
+ if (radiusAsPercentage) {
18235
+ const minX = Math.min(...points.map(p => p.x));
18236
+ const maxX = Math.max(...points.map(p => p.x));
18237
+ const minY = Math.min(...points.map(p => p.y));
18238
+ const maxY = Math.max(...points.map(p => p.y));
18239
+ const width = maxX - minX;
18240
+ const height = maxY - minY;
18241
+ const minDimension = Math.min(width, height);
18242
+ actualRadius = radius / 100 * minDimension;
18243
+ }
18244
+ const roundedCorners = [];
18245
+ for (let i = 0; i < points.length; i++) {
18246
+ const prevIndex = (i - 1 + points.length) % points.length;
18247
+ const nextIndex = (i + 1) % points.length;
18248
+ const prevPoint = points[prevIndex];
18249
+ const currentPoint = points[i];
18250
+ const nextPoint = points[nextIndex];
18251
+ const roundedCorner = calculateRoundedCorner(prevPoint, currentPoint, nextPoint, actualRadius);
18252
+ roundedCorners.push(roundedCorner);
18253
+ }
18254
+ return roundedCorners;
18255
+ }
18256
+
18257
+ /**
18258
+ * Render a rounded polygon to a canvas context
18259
+ */
18260
+ function renderRoundedPolygon(ctx, roundedCorners) {
18261
+ let closed = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
18262
+ if (roundedCorners.length === 0) return;
18263
+ ctx.beginPath();
18264
+
18265
+ // Start at the first corner's start point
18266
+ const firstCorner = roundedCorners[0];
18267
+ ctx.moveTo(firstCorner.start.x, firstCorner.start.y);
18268
+ for (let i = 0; i < roundedCorners.length; i++) {
18269
+ const corner = roundedCorners[i];
18270
+ const nextIndex = (i + 1) % roundedCorners.length;
18271
+ const nextCorner = roundedCorners[nextIndex];
18272
+
18273
+ // Draw the rounded corner using bezier curve
18274
+ ctx.bezierCurveTo(corner.cp1.x, corner.cp1.y, corner.cp2.x, corner.cp2.y, corner.end.x, corner.end.y);
18275
+
18276
+ // Draw line to next corner's start point (if not the last segment in open path)
18277
+ if (i < roundedCorners.length - 1 || closed) {
18278
+ ctx.lineTo(nextCorner.start.x, nextCorner.start.y);
18279
+ }
18280
+ }
18281
+ if (closed) {
18282
+ ctx.closePath();
18283
+ }
18284
+ }
18285
+
18286
+ /**
18287
+ * Generate SVG path data for a rounded polygon
18288
+ */
18289
+ function generateRoundedPolygonPath(roundedCorners) {
18290
+ let closed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
18291
+ if (roundedCorners.length === 0) return '';
18292
+ const pathData = [];
18293
+ const firstCorner = roundedCorners[0];
18294
+
18295
+ // Move to first corner's start point
18296
+ pathData.push(`M ${firstCorner.start.x} ${firstCorner.start.y}`);
18297
+ for (let i = 0; i < roundedCorners.length; i++) {
18298
+ const corner = roundedCorners[i];
18299
+ const nextIndex = (i + 1) % roundedCorners.length;
18300
+ const nextCorner = roundedCorners[nextIndex];
18301
+
18302
+ // Add bezier curve for the rounded corner
18303
+ pathData.push(`C ${corner.cp1.x} ${corner.cp1.y} ${corner.cp2.x} ${corner.cp2.y} ${corner.end.x} ${corner.end.y}`);
18304
+
18305
+ // Add line to next corner's start point (if not the last segment in open path)
18306
+ if (i < roundedCorners.length - 1 || closed) {
18307
+ pathData.push(`L ${nextCorner.start.x} ${nextCorner.start.y}`);
18308
+ }
18309
+ }
18310
+ if (closed) {
18311
+ pathData.push('Z');
18312
+ }
18313
+ return pathData.join(' ');
18314
+ }
17894
18315
 
17895
18316
  const triangleDefaultValues = {
17896
18317
  width: 100,
17897
- height: 100
18318
+ height: 100,
18319
+ cornerRadius: 0
17898
18320
  };
18321
+ const TRIANGLE_PROPS = ['cornerRadius'];
17899
18322
  class Triangle extends FabricObject {
17900
18323
  static getDefaults() {
17901
18324
  return {
@@ -17914,34 +18337,90 @@ class Triangle extends FabricObject {
17914
18337
  this.setOptions(options);
17915
18338
  }
17916
18339
 
18340
+ /**
18341
+ * Get triangle points as an array of XY coordinates
18342
+ * @private
18343
+ */
18344
+ _getTrianglePoints() {
18345
+ const widthBy2 = this.width / 2;
18346
+ const heightBy2 = this.height / 2;
18347
+ return [{
18348
+ x: -widthBy2,
18349
+ y: heightBy2
18350
+ },
18351
+ // bottom left
18352
+ {
18353
+ x: 0,
18354
+ y: -heightBy2
18355
+ },
18356
+ // top center
18357
+ {
18358
+ x: widthBy2,
18359
+ y: heightBy2
18360
+ } // bottom right
18361
+ ];
18362
+ }
18363
+
17917
18364
  /**
17918
18365
  * @private
17919
18366
  * @param {CanvasRenderingContext2D} ctx Context to render on
17920
18367
  */
17921
18368
  _render(ctx) {
17922
- const widthBy2 = this.width / 2,
17923
- heightBy2 = this.height / 2;
17924
- ctx.beginPath();
17925
- ctx.moveTo(-widthBy2, heightBy2);
17926
- ctx.lineTo(0, -heightBy2);
17927
- ctx.lineTo(widthBy2, heightBy2);
17928
- ctx.closePath();
18369
+ if (this.cornerRadius > 0) {
18370
+ // Render rounded triangle
18371
+ const points = this._getTrianglePoints();
18372
+ const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
18373
+ renderRoundedPolygon(ctx, roundedCorners, true);
18374
+ } else {
18375
+ // Render sharp triangle (original implementation)
18376
+ const widthBy2 = this.width / 2;
18377
+ const heightBy2 = this.height / 2;
18378
+ ctx.beginPath();
18379
+ ctx.moveTo(-widthBy2, heightBy2);
18380
+ ctx.lineTo(0, -heightBy2);
18381
+ ctx.lineTo(widthBy2, heightBy2);
18382
+ ctx.closePath();
18383
+ }
17929
18384
  this._renderPaintInOrder(ctx);
17930
18385
  }
17931
18386
 
18387
+ /**
18388
+ * Returns object representation of an instance
18389
+ * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
18390
+ * @return {Object} object representation of an instance
18391
+ */
18392
+ toObject() {
18393
+ let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
18394
+ return super.toObject([...TRIANGLE_PROPS, ...propertiesToInclude]);
18395
+ }
18396
+
17932
18397
  /**
17933
18398
  * Returns svg representation of an instance
17934
18399
  * @return {Array} an array of strings with the specific svg representation
17935
18400
  * of the instance
17936
18401
  */
17937
18402
  _toSVG() {
17938
- const widthBy2 = this.width / 2,
17939
- heightBy2 = this.height / 2,
17940
- points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
17941
- return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
18403
+ if (this.cornerRadius > 0) {
18404
+ // Generate rounded triangle as path
18405
+ const points = this._getTrianglePoints();
18406
+ const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
18407
+ const pathData = generateRoundedPolygonPath(roundedCorners, true);
18408
+ return ['<path ', 'COMMON_PARTS', `d="${pathData}" />`];
18409
+ } else {
18410
+ // Original sharp triangle implementation
18411
+ const widthBy2 = this.width / 2;
18412
+ const heightBy2 = this.height / 2;
18413
+ const points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
18414
+ return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
18415
+ }
17942
18416
  }
17943
18417
  }
18418
+ /**
18419
+ * Corner radius for rounded triangle corners
18420
+ * @type Number
18421
+ */
17944
18422
  _defineProperty(Triangle, "type", 'Triangle');
18423
+ _defineProperty(Triangle, "cacheProperties", [...cacheProperties, ...TRIANGLE_PROPS]);
17945
18424
  _defineProperty(Triangle, "ownDefaults", triangleDefaultValues);
17946
18425
  classRegistry.setClass(Triangle);
17947
18426
  classRegistry.setSVGClass(Triangle);
@@ -18106,7 +18585,8 @@ const polylineDefaultValues = {
18106
18585
  /**
18107
18586
  * @deprecated transient option soon to be removed in favor of a different design
18108
18587
  */
18109
- exactBoundingBox: false
18588
+ exactBoundingBox: false,
18589
+ cornerRadius: 0
18110
18590
  };
18111
18591
  class Polyline extends FabricObject {
18112
18592
  static getDefaults() {
@@ -18320,7 +18800,7 @@ class Polyline extends FabricObject {
18320
18800
  toObject() {
18321
18801
  let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
18322
18802
  return {
18323
- ...super.toObject(propertiesToInclude),
18803
+ ...super.toObject(['cornerRadius', ...propertiesToInclude]),
18324
18804
  points: this.points.map(_ref => {
18325
18805
  let {
18326
18806
  x,
@@ -18340,14 +18820,28 @@ class Polyline extends FabricObject {
18340
18820
  * of the instance
18341
18821
  */
18342
18822
  _toSVG() {
18343
- const points = [],
18344
- diffX = this.pathOffset.x,
18345
- diffY = this.pathOffset.y,
18346
- NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS;
18347
- for (let i = 0, len = this.points.length; i < len; i++) {
18348
- points.push(toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ');
18823
+ if (this.cornerRadius > 0 && this.points.length >= 3) {
18824
+ // Generate rounded polygon/polyline as path
18825
+ const diffX = this.pathOffset.x;
18826
+ const diffY = this.pathOffset.y;
18827
+ const adjustedPoints = this.points.map(point => ({
18828
+ x: point.x - diffX,
18829
+ y: point.y - diffY
18830
+ }));
18831
+ const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
18832
+ const pathData = generateRoundedPolygonPath(roundedCorners, !this.isOpen());
18833
+ return ['<path ', 'COMMON_PARTS', `d="${pathData}" />\n`];
18834
+ } else {
18835
+ // Original sharp corners implementation
18836
+ const points = [];
18837
+ const diffX = this.pathOffset.x;
18838
+ const diffY = this.pathOffset.y;
18839
+ const NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS;
18840
+ for (let i = 0, len = this.points.length; i < len; i++) {
18841
+ points.push(toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ');
18842
+ }
18843
+ return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
18349
18844
  }
18350
- return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
18351
18845
  }
18352
18846
 
18353
18847
  /**
@@ -18363,13 +18857,24 @@ class Polyline extends FabricObject {
18363
18857
  // NaN comes from parseFloat of a empty string in parser
18364
18858
  return;
18365
18859
  }
18366
- ctx.beginPath();
18367
- ctx.moveTo(this.points[0].x - x, this.points[0].y - y);
18368
- for (let i = 0; i < len; i++) {
18369
- const point = this.points[i];
18370
- ctx.lineTo(point.x - x, point.y - y);
18860
+ if (this.cornerRadius > 0 && len >= 3) {
18861
+ // Render with rounded corners
18862
+ const adjustedPoints = this.points.map(point => ({
18863
+ x: point.x - x,
18864
+ y: point.y - y
18865
+ }));
18866
+ const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
18867
+ renderRoundedPolygon(ctx, roundedCorners, !this.isOpen());
18868
+ } else {
18869
+ // Original sharp corners implementation
18870
+ ctx.beginPath();
18871
+ ctx.moveTo(this.points[0].x - x, this.points[0].y - y);
18872
+ for (let i = 0; i < len; i++) {
18873
+ const point = this.points[i];
18874
+ ctx.lineTo(point.x - x, point.y - y);
18875
+ }
18876
+ !this.isOpen() && ctx.closePath();
18371
18877
  }
18372
- !this.isOpen() && ctx.closePath();
18373
18878
  this._renderPaintInOrder(ctx);
18374
18879
  }
18375
18880
 
@@ -18434,10 +18939,15 @@ class Polyline extends FabricObject {
18434
18939
  * @type Boolean
18435
18940
  * @default false
18436
18941
  */
18942
+ /**
18943
+ * Corner radius for rounded corners
18944
+ * @type Number
18945
+ * @default 0
18946
+ */
18437
18947
  _defineProperty(Polyline, "ownDefaults", polylineDefaultValues);
18438
18948
  _defineProperty(Polyline, "type", 'Polyline');
18439
18949
  _defineProperty(Polyline, "layoutProperties", [SKEW_X, SKEW_Y, 'strokeLineCap', 'strokeLineJoin', 'strokeMiterLimit', 'strokeWidth', 'strokeUniform', 'points']);
18440
- _defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points']);
18950
+ _defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points', 'cornerRadius']);
18441
18951
  _defineProperty(Polyline, "ATTRIBUTE_NAMES", [...SHARED_ATTRIBUTES]);
18442
18952
  classRegistry.setClass(Polyline);
18443
18953
  classRegistry.setSVGClass(Polyline);
@@ -18821,6 +19331,97 @@ function measureGraphemeWithKerning(grapheme, previousGrapheme, options, ctx) {
18821
19331
  };
18822
19332
  }
18823
19333
 
19334
+ /**
19335
+ * Get a representative character for font metrics measurement
19336
+ * Uses canvas to test which scripts the font actually supports
19337
+ */
19338
+ function getRepresentativeCharacter(fontFamily) {
19339
+ const context = getMeasurementContext();
19340
+
19341
+ // Wait for font to be ready if possible
19342
+ if (typeof document !== 'undefined' && 'fonts' in document) {
19343
+ try {
19344
+ // Check if font is ready, if not, use fallback immediately
19345
+ if (!document.fonts.check(`16px ${fontFamily}`)) {
19346
+ return 'M'; // Use safe fallback while font loads
19347
+ }
19348
+ } catch (e) {
19349
+ // Font check failed, use fallback
19350
+ return 'M';
19351
+ }
19352
+ }
19353
+
19354
+ // Test characters for different scripts
19355
+ const testChars = [{
19356
+ char: 'م',
19357
+ script: 'Arabic'
19358
+ },
19359
+ // Arabic
19360
+ {
19361
+ char: 'א',
19362
+ script: 'Hebrew'
19363
+ },
19364
+ // Hebrew
19365
+ {
19366
+ char: 'अ',
19367
+ script: 'Devanagari'
19368
+ },
19369
+ // Hindi/Sanskrit
19370
+ {
19371
+ char: 'ا',
19372
+ script: 'Urdu'
19373
+ },
19374
+ // Urdu
19375
+ {
19376
+ char: 'ک',
19377
+ script: 'Persian'
19378
+ },
19379
+ // Persian
19380
+ {
19381
+ char: 'த',
19382
+ script: 'Tamil'
19383
+ },
19384
+ // Tamil
19385
+ {
19386
+ char: 'ก',
19387
+ script: 'Thai'
19388
+ },
19389
+ // Thai
19390
+ {
19391
+ char: 'М',
19392
+ script: 'Cyrillic'
19393
+ },
19394
+ // Cyrillic
19395
+ {
19396
+ char: 'Ω',
19397
+ script: 'Greek'
19398
+ },
19399
+ // Greek
19400
+ {
19401
+ char: 'M',
19402
+ script: 'Latin'
19403
+ } // Latin (fallback)
19404
+ ];
19405
+
19406
+ // Set the font
19407
+ context.font = `16px ${fontFamily}`;
19408
+
19409
+ // Test each character to see which ones render properly
19410
+ // Use a more robust width check to avoid false positives
19411
+ const fallbackWidth = context.measureText('M').width;
19412
+ for (const test of testChars) {
19413
+ const metrics = context.measureText(test.char);
19414
+
19415
+ // Character is valid if it has width and isn't just a fallback glyph
19416
+ if (metrics.width > 0 && Math.abs(metrics.width - fallbackWidth) > 0.1) {
19417
+ return test.char;
19418
+ }
19419
+ }
19420
+
19421
+ // Fallback to Latin 'M'
19422
+ return 'M';
19423
+ }
19424
+
18824
19425
  /**
18825
19426
  * Get font metrics for layout calculations
18826
19427
  */
@@ -18834,8 +19435,9 @@ function getFontMetrics(options) {
18834
19435
  const context = getMeasurementContext();
18835
19436
  applyFontStyle(context, options);
18836
19437
 
18837
- // Use 'M' as sample character for metrics
18838
- const metrics = context.measureText('M');
19438
+ // Use representative character based on font's primary script
19439
+ const sample = getRepresentativeCharacter(options.fontFamily);
19440
+ const metrics = context.measureText(sample);
18839
19441
  const fontSize = options.fontSize;
18840
19442
 
18841
19443
  // Calculate metrics with fallbacks
@@ -18887,7 +19489,11 @@ function getFontDeclaration(options) {
18887
19489
  } = options;
18888
19490
 
18889
19491
  // Normalize font family (add quotes if needed)
18890
- const normalizedFamily = fontFamily.includes(' ') && !fontFamily.includes('"') && !fontFamily.includes("'") ? `"${fontFamily}"` : fontFamily;
19492
+ let normalizedFamily = fontFamily.includes(' ') && !fontFamily.includes('"') && !fontFamily.includes("'") ? `"${fontFamily}"` : fontFamily;
19493
+
19494
+ // Note: Font fallbacks are handled in the rendering phase only
19495
+ // to avoid affecting measurement calculations for text wrapping
19496
+
18891
19497
  return `${fontStyle} ${fontWeight} ${fontSize}px ${normalizedFamily}`;
18892
19498
  }
18893
19499
 
@@ -19039,6 +19645,81 @@ const measurementCache = new MeasurementCache();
19039
19645
  const kerningCache = new KerningCache();
19040
19646
  const fontMetricsCache = new FontMetricsCache();
19041
19647
 
19648
+ // Set up font loading listener to clear caches when fonts change
19649
+ if (typeof document !== 'undefined' && 'fonts' in document) {
19650
+ document.fonts.addEventListener('loadingdone', () => {
19651
+ // Clear all caches when fonts finish loading
19652
+ clearAllCaches();
19653
+ });
19654
+ }
19655
+
19656
+ /**
19657
+ * Clear all measurement caches
19658
+ */
19659
+ function clearAllCaches() {
19660
+ measurementCache.clear();
19661
+ kerningCache.clear();
19662
+ fontMetricsCache.clear();
19663
+ }
19664
+
19665
+ /**
19666
+ * Detect if a font lacks English glyph support
19667
+ * These fonts should use browser-native measurement instead of Fabric's character-by-character measurement
19668
+ */
19669
+ function fontLacksEnglishGlyphs(fontFamily) {
19670
+ if (typeof document === 'undefined') return false;
19671
+
19672
+ // Known fonts that lack English glyphs
19673
+ const knownNonEnglishFonts = ['stv', 'arabic', 'naskh', 'thuluth', 'kufi', 'diwani', 'nastaliq', 'kufic', 'hijazi', 'madinah', 'makkah'];
19674
+ const lowerFontFamily = fontFamily.toLowerCase();
19675
+
19676
+ // Check known list first
19677
+ if (knownNonEnglishFonts.some(font => lowerFontFamily.includes(font))) {
19678
+ return true;
19679
+ }
19680
+
19681
+ // Dynamic glyph support detection
19682
+ const context = getMeasurementContext();
19683
+ context.font = `16px ${fontFamily}`;
19684
+
19685
+ // Test English characters
19686
+ const englishChars = ['A', 'B', 'C', 'a', 'b', 'c', 'M', 'W'];
19687
+ const fallbackFont = 'Arial, sans-serif';
19688
+
19689
+ // Measure with target font
19690
+ const targetWidths = englishChars.map(char => context.measureText(char).width);
19691
+
19692
+ // Measure with fallback font
19693
+ context.font = `16px ${fallbackFont}`;
19694
+ const fallbackWidths = englishChars.map(char => context.measureText(char).width);
19695
+
19696
+ // If most measurements are identical, the font likely doesn't have English glyphs
19697
+ let identicalCount = 0;
19698
+ for (let i = 0; i < englishChars.length; i++) {
19699
+ if (Math.abs(targetWidths[i] - fallbackWidths[i]) < 0.5) {
19700
+ identicalCount++;
19701
+ }
19702
+ }
19703
+ const lacksSupportThreshold = englishChars.length * 0.7; // 70% identical = lacks support
19704
+ const lacksSupport = identicalCount >= lacksSupportThreshold;
19705
+ return lacksSupport;
19706
+ }
19707
+
19708
+ // Cache for font glyph detection results
19709
+ const fontGlyphCache = new Map();
19710
+
19711
+ /**
19712
+ * Cached version of font glyph detection
19713
+ */
19714
+ function fontLacksEnglishGlyphsCached(fontFamily) {
19715
+ if (fontGlyphCache.has(fontFamily)) {
19716
+ return fontGlyphCache.get(fontFamily);
19717
+ }
19718
+ const result = fontLacksEnglishGlyphs(fontFamily);
19719
+ fontGlyphCache.set(fontFamily, result);
19720
+ return result;
19721
+ }
19722
+
19042
19723
  /**
19043
19724
  * Unicode and Internationalization Support
19044
19725
  *
@@ -20222,6 +20903,15 @@ class FabricText extends StyledText {
20222
20903
  * Does not return dimensions.
20223
20904
  */
20224
20905
  initDimensions() {
20906
+ // Check if font is ready for accurate measurements
20907
+ // Only block initialization if it's a critical font loading situation
20908
+ const fontReady = this._isFontReady();
20909
+ if (!fontReady && !this.initialized) {
20910
+ // Only schedule font loading on first initialization
20911
+ this._scheduleInitAfterFontLoad();
20912
+ // Continue with fallback measurements for now
20913
+ }
20914
+
20225
20915
  // Use advanced layout if enabled
20226
20916
  if (this.enableAdvancedLayout && !this.path) {
20227
20917
  return this.initDimensionsAdvanced();
@@ -20238,7 +20928,21 @@ class FabricText extends StyledText {
20238
20928
  }
20239
20929
  if (this.textAlign.includes(JUSTIFY)) {
20240
20930
  // once text is measured we need to make space fatter to make justified text.
20241
- this.enlargeSpaces();
20931
+ // Ensure __charBounds exists before calling enlargeSpaces
20932
+ if (this.__charBounds && this.__charBounds.length > 0) {
20933
+ this.enlargeSpaces();
20934
+ } else {
20935
+ console.warn('⚠️ __charBounds not ready for justify alignment, deferring enlargeSpaces');
20936
+ // Defer the justify calculation until the next frame
20937
+ setTimeout(() => {
20938
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
20939
+ var _this$canvas;
20940
+ console.log('🔧 Applying deferred justify alignment');
20941
+ this.enlargeSpaces();
20942
+ (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
20943
+ }
20944
+ }, 0);
20945
+ }
20242
20946
  }
20243
20947
  }
20244
20948
 
@@ -20247,8 +20951,9 @@ class FabricText extends StyledText {
20247
20951
  */
20248
20952
  enlargeSpaces() {
20249
20953
  let diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces;
20954
+ const isRtl = this.direction === 'rtl';
20250
20955
  for (let i = 0, len = this._textLines.length; i < len; i++) {
20251
- if (this.textAlign !== JUSTIFY && (i === len - 1 || this.isEndOfWrapping(i))) {
20956
+ if (!this.textAlign.includes('justify') && (i === len - 1 || this.isEndOfWrapping(i))) {
20252
20957
  continue;
20253
20958
  }
20254
20959
  accumulatedSpace = 0;
@@ -20257,15 +20962,47 @@ class FabricText extends StyledText {
20257
20962
  if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) {
20258
20963
  numberOfSpaces = spaces.length;
20259
20964
  diffSpace = (this.width - currentLineWidth) / numberOfSpaces;
20260
- for (let j = 0; j <= line.length; j++) {
20261
- charBound = this.__charBounds[i][j];
20262
- if (this._reSpaceAndTab.test(line[j])) {
20263
- charBound.width += diffSpace;
20264
- charBound.kernedWidth += diffSpace;
20265
- charBound.left += accumulatedSpace;
20266
- accumulatedSpace += diffSpace;
20267
- } else {
20268
- charBound.left += accumulatedSpace;
20965
+ console.log(`🔧 EnlargeSpaces Line ${i}:`);
20966
+ console.log(` Current width: ${currentLineWidth}, Target: ${this.width}`);
20967
+ console.log(` Spaces: ${numberOfSpaces}, diffSpace: ${diffSpace.toFixed(2)}`);
20968
+ if (isRtl) {
20969
+ for (let j = 0; j < line.length; j++) {
20970
+ if (this._reSpaceAndTab.test(line[j])) ;
20971
+ }
20972
+
20973
+ // For RTL, we need to work backwards through the visual positions
20974
+ // but still update logical positions correctly
20975
+ let spaceCount = 0;
20976
+ for (let j = 0; j <= line.length; j++) {
20977
+ charBound = this.__charBounds[i][j];
20978
+ if (charBound) {
20979
+ if (this._reSpaceAndTab.test(line[j])) {
20980
+ charBound.width += diffSpace;
20981
+ charBound.kernedWidth += diffSpace;
20982
+ spaceCount++;
20983
+ }
20984
+
20985
+ // For RTL, shift all characters to the right by the total expansion
20986
+ // minus the expansion that comes after this character
20987
+ const remainingSpaces = numberOfSpaces - spaceCount;
20988
+ const shiftAmount = remainingSpaces * diffSpace;
20989
+ charBound.left += shiftAmount;
20990
+ }
20991
+ }
20992
+ } else {
20993
+ // LTR processing (original logic)
20994
+ for (let j = 0; j <= line.length; j++) {
20995
+ charBound = this.__charBounds[i][j];
20996
+ if (charBound) {
20997
+ if (this._reSpaceAndTab.test(line[j])) {
20998
+ charBound.width += diffSpace;
20999
+ charBound.kernedWidth += diffSpace;
21000
+ charBound.left += accumulatedSpace;
21001
+ accumulatedSpace += diffSpace;
21002
+ } else {
21003
+ charBound.left += accumulatedSpace;
21004
+ }
21005
+ }
20269
21006
  }
20270
21007
  }
20271
21008
  }
@@ -20343,6 +21080,18 @@ class FabricText extends StyledText {
20343
21080
 
20344
21081
  // Convert layout to legacy format for compatibility
20345
21082
  this._convertLayoutToLegacyFormat(layout);
21083
+
21084
+ // Ensure justify alignment is properly applied for compatibility with legacy rendering
21085
+ if (this.textAlign.includes(JUSTIFY)) {
21086
+ // Force enlarge spaces after advanced layout calculation
21087
+ setTimeout(() => {
21088
+ if (this.enlargeSpaces) {
21089
+ var _this$canvas2;
21090
+ this.enlargeSpaces();
21091
+ (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.renderAll();
21092
+ }
21093
+ }, 0);
21094
+ }
20346
21095
  this.dirty = true;
20347
21096
  }
20348
21097
 
@@ -20923,7 +21672,15 @@ class FabricText extends StyledText {
20923
21672
  if (currentDirection !== this.direction) {
20924
21673
  ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl');
20925
21674
  ctx.direction = isLtr ? 'ltr' : 'rtl';
20926
- ctx.textAlign = isLtr ? LEFT : RIGHT;
21675
+
21676
+ // For justify alignments, we need to set the correct canvas text alignment
21677
+ // This is crucial for RTL text to render in the correct order
21678
+ if (isJustify) {
21679
+ // Justify uses LEFT alignment as a base, letting the character positioning handle justification
21680
+ ctx.textAlign = LEFT;
21681
+ } else {
21682
+ ctx.textAlign = isLtr ? LEFT : RIGHT;
21683
+ }
20927
21684
  }
20928
21685
  top -= lineHeight * this._fontSizeFraction / this.lineHeight;
20929
21686
  if (shortCut) {
@@ -21159,9 +21916,21 @@ class FabricText extends StyledText {
21159
21916
  direction = this.direction,
21160
21917
  isEndOfWrapping = this.isEndOfWrapping(lineIndex);
21161
21918
  let leftOffset = 0;
21162
- if (textAlign === JUSTIFY || textAlign === JUSTIFY_CENTER && !isEndOfWrapping || textAlign === JUSTIFY_RIGHT && !isEndOfWrapping || textAlign === JUSTIFY_LEFT && !isEndOfWrapping) {
21163
- return 0;
21919
+
21920
+ // Handle justify alignments (excluding last lines and wrapped line ends)
21921
+ const isJustifyLine = textAlign === JUSTIFY || textAlign === JUSTIFY_CENTER && !isEndOfWrapping || textAlign === JUSTIFY_RIGHT && !isEndOfWrapping || textAlign === JUSTIFY_LEFT && !isEndOfWrapping;
21922
+ if (isJustifyLine) {
21923
+ // Justify lines should start at the left edge for LTR and right edge for RTL
21924
+ // The space distribution is handled by enlargeSpaces()
21925
+ if (direction === 'rtl') {
21926
+ // For RTL justify, we need to account for the line being right-aligned
21927
+ return 0;
21928
+ } else {
21929
+ return 0;
21930
+ }
21164
21931
  }
21932
+
21933
+ // Handle non-justify alignments
21165
21934
  if (textAlign === CENTER) {
21166
21935
  leftOffset = lineDiff / 2;
21167
21936
  }
@@ -21174,6 +21943,8 @@ class FabricText extends StyledText {
21174
21943
  if (textAlign === JUSTIFY_RIGHT) {
21175
21944
  leftOffset = lineDiff;
21176
21945
  }
21946
+
21947
+ // Apply RTL adjustments for non-justify alignments
21177
21948
  if (direction === 'rtl') {
21178
21949
  if (textAlign === RIGHT || textAlign === JUSTIFY || textAlign === JUSTIFY_RIGHT) {
21179
21950
  leftOffset = 0;
@@ -21332,7 +22103,19 @@ class FabricText extends StyledText {
21332
22103
  fontSize = this.fontSize
21333
22104
  } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
21334
22105
  let forMeasuring = arguments.length > 1 ? arguments[1] : undefined;
21335
- const parsedFontFamily = fontFamily.includes("'") || fontFamily.includes('"') || fontFamily.includes(',') || FabricText.genericFonts.includes(fontFamily.toLowerCase()) ? fontFamily : `"${fontFamily}"`;
22106
+ let parsedFontFamily = fontFamily.includes("'") || fontFamily.includes('"') || fontFamily.includes(',') || FabricText.genericFonts.includes(fontFamily.toLowerCase()) ? fontFamily : `"${fontFamily}"`;
22107
+
22108
+ // For fonts like STV that don't support English/Latin characters,
22109
+ // add fallback fonts for consistent rendering of unsupported characters
22110
+ // Only add fallbacks during actual rendering, not for measurements
22111
+ if (!forMeasuring &&
22112
+ // Only during rendering, not measuring
22113
+ !fontFamily.includes(',') && (
22114
+ // Don't add fallbacks if already has them
22115
+ fontFamily.toLowerCase().includes('stv') || fontFamily.toLowerCase().includes('arabic') || fontFamily.toLowerCase().includes('naskh') || fontFamily.toLowerCase().includes('kufi'))) {
22116
+ // Add fallback fonts for unsupported characters (spaces, punctuation, etc.)
22117
+ parsedFontFamily = `${parsedFontFamily}, "Arial Unicode MS", Arial, sans-serif`;
22118
+ }
21336
22119
  return [fontStyle, fontWeight, `${forMeasuring ? this.CACHE_FONT_SIZE : fontSize}px`, parsedFontFamily].join(' ');
21337
22120
  }
21338
22121
 
@@ -21376,7 +22159,13 @@ class FabricText extends StyledText {
21376
22159
  newLine = ['\n'];
21377
22160
  let newText = [];
21378
22161
  for (let i = 0; i < lines.length; i++) {
21379
- newLines[i] = this.graphemeSplit(lines[i]);
22162
+ // Use BiDi-aware grapheme splitting for RTL text
22163
+ if (this.direction === 'rtl' || this._containsArabicText(lines[i])) {
22164
+ newLines[i] = segmentGraphemes(lines[i]);
22165
+ console.log(`🔤 BiDi-aware split line ${i}: "${lines[i]}" -> [${newLines[i].join(', ')}]`);
22166
+ } else {
22167
+ newLines[i] = this.graphemeSplit(lines[i]);
22168
+ }
21380
22169
  newText = newText.concat(newLines[i], newLine);
21381
22170
  }
21382
22171
  newText.pop();
@@ -21388,6 +22177,14 @@ class FabricText extends StyledText {
21388
22177
  };
21389
22178
  }
21390
22179
 
22180
+ /**
22181
+ * Check if text contains Arabic characters
22182
+ * @private
22183
+ */
22184
+ _containsArabicText(text) {
22185
+ return /[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
22186
+ }
22187
+
21391
22188
  /**
21392
22189
  * Returns object representation of an instance
21393
22190
  * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
@@ -21510,6 +22307,88 @@ class FabricText extends StyledText {
21510
22307
 
21511
22308
  /* _FROM_SVG_END_ */
21512
22309
 
22310
+ /**
22311
+ * Check if the font is ready for accurate measurements
22312
+ * @private
22313
+ */
22314
+ _isFontReady() {
22315
+ if (typeof document === 'undefined' || !('fonts' in document)) {
22316
+ return true; // Assume ready in non-browser environments
22317
+ }
22318
+ try {
22319
+ return document.fonts.check(`${this.fontSize}px ${this.fontFamily}`);
22320
+ } catch (e) {
22321
+ return true; // Fallback to assuming ready if check fails
22322
+ }
22323
+ }
22324
+
22325
+ /**
22326
+ * Schedule re-initialization after font loads
22327
+ * @private
22328
+ */
22329
+ _scheduleInitAfterFontLoad() {
22330
+ if (typeof document === 'undefined' || !('fonts' in document)) {
22331
+ return;
22332
+ }
22333
+
22334
+ // Only schedule if not already waiting
22335
+ if (this._fontLoadScheduled) {
22336
+ return;
22337
+ }
22338
+ this._fontLoadScheduled = true;
22339
+ const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
22340
+ document.fonts.load(fontSpec).then(() => {
22341
+ this._fontLoadScheduled = false;
22342
+ // Re-initialize dimensions with proper font metrics
22343
+ this.initDimensions();
22344
+
22345
+ // Extra step for justify alignment after font loading
22346
+ if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
22347
+ setTimeout(() => {
22348
+ var _this$canvas3;
22349
+ if (this.enlargeSpaces) {
22350
+ this.enlargeSpaces();
22351
+ }
22352
+ (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
22353
+ }, 10);
22354
+ } else {
22355
+ var _this$canvas4;
22356
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.requestRenderAll();
22357
+ }
22358
+ }).catch(() => {
22359
+ this._fontLoadScheduled = false;
22360
+ });
22361
+ }
22362
+
22363
+ /**
22364
+ * Force complete text re-initialization (useful after JSON loading)
22365
+ */
22366
+ forceTextReinitialization() {
22367
+ console.log('🔄 Force reinitializing text object');
22368
+
22369
+ // Clear all caches
22370
+ this._clearCache();
22371
+ this.dirty = true;
22372
+
22373
+ // Force text splitting to rebuild internal structures
22374
+ this._splitText();
22375
+
22376
+ // Re-initialize dimensions
22377
+ this.initDimensions();
22378
+
22379
+ // Special handling for justify alignment
22380
+ if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
22381
+ // Ensure justify is applied after dimensions are set
22382
+ setTimeout(() => {
22383
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
22384
+ var _this$canvas5;
22385
+ this.enlargeSpaces();
22386
+ (_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
22387
+ }
22388
+ }, 10);
22389
+ }
22390
+ }
22391
+
21513
22392
  /**
21514
22393
  * Returns FabricText instance from an object representation
21515
22394
  * @param {Object} object plain js Object to create an instance from
@@ -21521,6 +22400,93 @@ class FabricText extends StyledText {
21521
22400
  styles: stylesFromArray(object.styles || {}, object.text)
21522
22401
  }, {
21523
22402
  extraParam: 'text'
22403
+ }).then(textObject => {
22404
+ // Ensure text object is properly initialized after JSON deserialization
22405
+ // This is critical for justify alignment and other text layout features
22406
+ textObject.initialized = true;
22407
+
22408
+ // Force reinitialization to ensure proper layout
22409
+ if (textObject._clearCache) {
22410
+ textObject._clearCache();
22411
+ }
22412
+ textObject.dirty = true;
22413
+
22414
+ // Check if we need to wait for font loading (especially for custom fonts like STV)
22415
+ const fontSpec = `${textObject.fontSize}px ${textObject.fontFamily}`;
22416
+
22417
+ // For custom fonts, ensure they're loaded before initializing dimensions
22418
+ if (typeof document !== 'undefined' && 'fonts' in document && textObject.fontFamily !== 'Arial' && textObject.fontFamily !== 'Times New Roman') {
22419
+ return document.fonts.load(fontSpec).then(() => {
22420
+ var _textObject$fontFamil;
22421
+ console.log(`🔤 Font loaded for JSON object: ${fontSpec}`);
22422
+ // Ensure initialized flag is set again (in case constructor reset it)
22423
+ textObject.initialized = true;
22424
+
22425
+ // Special handling for STV fonts which have measurement issues
22426
+ const isStvFont = (_textObject$fontFamil = textObject.fontFamily) === null || _textObject$fontFamil === void 0 ? void 0 : _textObject$fontFamil.toLowerCase().includes('stv');
22427
+ if (isStvFont) {
22428
+ console.log(`🔤 STV font detected, using enhanced reinitialization`);
22429
+
22430
+ // Clear all cached state that might interfere with browser wrapping
22431
+ textObject._browserWrapCache = null;
22432
+ textObject._lastDimensionState = null;
22433
+ textObject._browserWrapInitialized = false;
22434
+ console.log(`🔤 STV font: Cleared all cached states for fresh initialization`);
22435
+
22436
+ // Force browser wrapping flag for STV fonts
22437
+ textObject._usingBrowserWrapping = true;
22438
+ console.log(`🔤 STV font: Forcing browser wrapping flag during JSON load`);
22439
+
22440
+ // Multiple initialization attempts for STV fonts
22441
+ const reinitWithDelay = attempt => {
22442
+ if (textObject.forceTextReinitialization) {
22443
+ textObject.forceTextReinitialization();
22444
+ } else {
22445
+ textObject.initDimensions();
22446
+ }
22447
+
22448
+ // Check if width is still problematic after initialization
22449
+ if (textObject.width < 50 && attempt < 3) {
22450
+ console.log(`🔤 STV font width still ${textObject.width}px, retrying in ${100 * attempt}ms (attempt ${attempt + 1}/3)`);
22451
+ setTimeout(() => reinitWithDelay(attempt + 1), 100 * attempt);
22452
+ }
22453
+ };
22454
+ reinitWithDelay(0);
22455
+ } else {
22456
+ // Use specialized reinitialization for Textbox objects
22457
+ if (textObject.forceTextReinitialization) {
22458
+ console.log(`🔤 Using Textbox specialized reinitialization`);
22459
+ textObject.forceTextReinitialization();
22460
+ } else {
22461
+ // Reinitialize dimensions with proper font metrics
22462
+ textObject.initDimensions();
22463
+ }
22464
+ }
22465
+ return textObject;
22466
+ }).catch(() => {
22467
+ console.warn(`⚠️ Font loading failed for ${fontSpec}, proceeding with fallback`);
22468
+ // Ensure initialized flag is set again
22469
+ textObject.initialized = true;
22470
+
22471
+ // Still initialize dimensions even if font loading fails
22472
+ if (textObject.forceTextReinitialization) {
22473
+ textObject.forceTextReinitialization();
22474
+ } else {
22475
+ textObject.initDimensions();
22476
+ }
22477
+ return textObject;
22478
+ });
22479
+ } else {
22480
+ // Standard fonts - ensure initialized and use appropriate method
22481
+ textObject.initialized = true;
22482
+ if (textObject.forceTextReinitialization) {
22483
+ console.log(`🔤 Using Textbox specialized reinitialization for standard font`);
22484
+ textObject.forceTextReinitialization();
22485
+ } else {
22486
+ textObject.initDimensions();
22487
+ }
22488
+ return textObject;
22489
+ }
21524
22490
  });
21525
22491
  }
21526
22492
  }
@@ -21960,8 +22926,12 @@ class OverlayEditor {
21960
22926
  this.textarea.style.pointerEvents = 'auto';
21961
22927
  // Set appropriate unicodeBidi based on content and direction
21962
22928
  const hasArabicText = /[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(this.target.text || '');
22929
+ const hasLatinText = /[a-zA-Z]/.test(this.target.text || '');
21963
22930
  const isLTRDirection = this.target.direction === 'ltr';
21964
- if (hasArabicText && isLTRDirection) {
22931
+ if (hasArabicText && hasLatinText && isLTRDirection) {
22932
+ // For mixed Arabic/Latin text in LTR mode, use embed for consistent line wrapping
22933
+ this.textarea.style.unicodeBidi = 'embed';
22934
+ } else if (hasArabicText && isLTRDirection) {
21965
22935
  // For Arabic text in LTR mode, use embed to preserve shaping while respecting direction
21966
22936
  this.textarea.style.unicodeBidi = 'embed';
21967
22937
  } else {
@@ -22050,14 +23020,26 @@ class OverlayEditor {
22050
23020
  parseFloat(this.hostDiv.style.width) / zoom;
22051
23021
  const currentHeight = parseFloat(this.hostDiv.style.height) / zoom;
22052
23022
 
22053
- // Only update if there's a meaningful change (avoid float precision issues)
23023
+ // Always update height for responsive controls (especially important for line deletion)
22054
23024
  const heightDiff = Math.abs(currentHeight - target.height);
22055
- const threshold = 1; // 1px threshold to avoid micro-changes
23025
+ const threshold = 0.5; // Lower threshold for better responsiveness to line changes
22056
23026
 
22057
23027
  if (heightDiff > threshold) {
23028
+ target.height;
22058
23029
  target.height = currentHeight;
22059
23030
  target.setCoords(); // Update control positions
23031
+
23032
+ // Force dirty to ensure proper re-rendering
23033
+ target.dirty = true;
22060
23034
  this.canvas.requestRenderAll(); // Re-render to show updated selection
23035
+
23036
+ // IMPORTANT: Reposition overlay after height change
23037
+ requestAnimationFrame(() => {
23038
+ if (!this.isDestroyed) {
23039
+ this.applyOverlayStyle();
23040
+ console.log('📐 Height changed - rechecking alignment after repositioning:');
23041
+ }
23042
+ });
22061
23043
  }
22062
23044
  }
22063
23045
 
@@ -22085,14 +23067,6 @@ class OverlayEditor {
22085
23067
  target.setCoords();
22086
23068
  const aCoords = target.aCoords;
22087
23069
 
22088
- // DEBUG: Log dimensions before edit
22089
- console.log('BEFORE EDIT:');
22090
- console.log(' target.width =', target.width);
22091
- console.log(' target.height =', target.height);
22092
- console.log(' target.getScaledWidth() =', target.getScaledWidth());
22093
- console.log(' target.getScaledHeight() =', target.getScaledHeight());
22094
- console.log(' target.padding =', target.padding);
22095
-
22096
23070
  // 2. Get canvas position and scroll offsets (like rtl-test.html)
22097
23071
  const canvasEl = canvas.upperCanvasEl;
22098
23072
  const canvasRect = canvasEl.getBoundingClientRect();
@@ -22115,14 +23089,12 @@ class OverlayEditor {
22115
23089
  const left = canvasRect.left + scrollX + screenPoint.x;
22116
23090
  const top = canvasRect.top + scrollY + screenPoint.y;
22117
23091
 
22118
- // 4. Get dimensions with zoom scaling - use target.width for text wrapping, scaled height for container
22119
- const width = target.width * (target.scaleX || 1) * zoom; // Account for object scale and viewport zoom
22120
- const height = target.height * (target.scaleY || 1) * zoom;
22121
- console.log('WIDTH CALCULATION:');
22122
- console.log(' target.width =', target.width);
22123
- console.log(' scaledWidth =', target.getScaledWidth());
22124
- console.log(' zoom =', zoom);
22125
- console.log(' final width =', width);
23092
+ // 4. Calculate the precise width and height for the container
23093
+ // **THE FIX:** Use getBoundingRect() for BOTH width and height.
23094
+ // This is the most reliable measure of the object's final rendered dimensions.
23095
+ const objectBounds = target.getBoundingRect();
23096
+ const width = Math.round(objectBounds.width * zoom);
23097
+ const height = Math.round(objectBounds.height * zoom);
22126
23098
 
22127
23099
  // 5. Apply styles to host DIV - absolute positioning like rtl-test.html
22128
23100
  this.hostDiv.style.position = 'absolute';
@@ -22146,50 +23118,333 @@ class OverlayEditor {
22146
23118
  const scaleX = target.scaleX || 1;
22147
23119
  const finalFontSize = baseFontSize * scaleX * zoom;
22148
23120
  const fabricLineHeight = target.lineHeight || 1.16;
22149
- // Apply padding and dimensions to textarea
22150
- const textareaWidth = paddingX > 0 ? `calc(100% - ${2 * paddingX}px)` : '100%';
22151
- const textareaHeight = paddingY > 0 ? `calc(100% - ${2 * paddingY}px)` : '100%';
22152
- this.textarea.style.width = textareaWidth;
22153
- this.textarea.style.height = textareaHeight;
23121
+ // **THE FIX:** Use 'border-box' so the width property includes padding.
23122
+ // This makes alignment much easier and more reliable.
23123
+ this.textarea.style.boxSizing = 'border-box';
23124
+
23125
+ // **THE FIX:** Set the textarea width to be IDENTICAL to the host div's width.
23126
+ // The padding will now be correctly contained *inside* this width.
23127
+ this.textarea.style.width = `${width}px`;
23128
+ this.textarea.style.height = '100%'; // Let hostDiv control height
22154
23129
  this.textarea.style.padding = `${paddingY}px ${paddingX}px`;
23130
+
23131
+ // Apply all other font and text styles to match Fabric
23132
+ const letterSpacingPx = (target.charSpacing || 0) / 1000 * finalFontSize;
23133
+
23134
+ // Special handling for text objects loaded from JSON - ensure they're properly initialized
23135
+ if (target.dirty !== false && target.initDimensions) {
23136
+ console.log('🔧 Ensuring text object is properly initialized before overlay editing');
23137
+ // Force re-initialization if the text object seems to be in a dirty state
23138
+ target.initDimensions();
23139
+ }
22155
23140
  this.textarea.style.fontSize = `${finalFontSize}px`;
22156
- this.textarea.style.lineHeight = String(fabricLineHeight); // Use unit-less multiplier
23141
+ this.textarea.style.lineHeight = String(fabricLineHeight);
22157
23142
  this.textarea.style.fontFamily = target.fontFamily || 'Arial';
22158
23143
  this.textarea.style.fontWeight = String(target.fontWeight || 'normal');
22159
23144
  this.textarea.style.fontStyle = target.fontStyle || 'normal';
22160
- this.textarea.style.textAlign = target.textAlign || 'left';
23145
+ // Handle text alignment and justification
23146
+ const textAlign = target.textAlign || 'left';
23147
+ let cssTextAlign = textAlign;
23148
+
23149
+ // Detect text direction from content for proper justify handling
23150
+ const autoDetectedDirection = this.firstStrongDir(this.textarea.value || '');
23151
+
23152
+ // DEBUG: Log alignment details
23153
+ console.log('🔍 ALIGNMENT DEBUG:');
23154
+ console.log(' Fabric textAlign:', textAlign);
23155
+ console.log(' Fabric direction:', target.direction);
23156
+ console.log(' Text content:', JSON.stringify(target.text));
23157
+ console.log(' Detected direction:', autoDetectedDirection);
23158
+
23159
+ // Map fabric.js justify to CSS
23160
+ if (textAlign.includes('justify')) {
23161
+ // Try to match fabric.js justify behavior more precisely
23162
+ try {
23163
+ // For justify, we need to replicate fabric.js space expansion
23164
+ // Use CSS justify but with specific settings to match fabric.js better
23165
+ cssTextAlign = 'justify';
23166
+
23167
+ // Set text-align-last based on justify type and detected direction
23168
+ // Smart justify: respect detected direction even when fabric alignment doesn't match
23169
+ if (textAlign === 'justify') {
23170
+ this.textarea.style.textAlignLast = autoDetectedDirection === 'rtl' ? 'right' : 'left';
23171
+ } else if (textAlign === 'justify-left') {
23172
+ // If text is RTL but fabric says justify-left, override to justify-right for better UX
23173
+ if (autoDetectedDirection === 'rtl') {
23174
+ this.textarea.style.textAlignLast = 'right';
23175
+ console.log(' → Overrode justify-left to justify-right for RTL text');
23176
+ } else {
23177
+ this.textarea.style.textAlignLast = 'left';
23178
+ }
23179
+ } else if (textAlign === 'justify-right') {
23180
+ // If text is LTR but fabric says justify-right, override to justify-left for better UX
23181
+ if (autoDetectedDirection === 'ltr') {
23182
+ this.textarea.style.textAlignLast = 'left';
23183
+ console.log(' → Overrode justify-right to justify-left for LTR text');
23184
+ } else {
23185
+ this.textarea.style.textAlignLast = 'right';
23186
+ }
23187
+ } else if (textAlign === 'justify-center') {
23188
+ this.textarea.style.textAlignLast = 'center';
23189
+ }
23190
+
23191
+ // Enhanced justify settings for better fabric.js matching
23192
+ this.textarea.style.textJustify = 'inter-word';
23193
+ this.textarea.style.wordSpacing = 'normal';
23194
+
23195
+ // Additional CSS properties for better justify matching
23196
+ this.textarea.style.textAlign = 'justify';
23197
+ this.textarea.style.textAlignLast = this.textarea.style.textAlignLast;
23198
+
23199
+ // Try to force better justify behavior
23200
+ this.textarea.style.textJustifyTrim = 'none';
23201
+ this.textarea.style.textAutospace = 'none';
23202
+ console.log(' → Applied justify alignment:', textAlign, 'with last-line:', this.textarea.style.textAlignLast);
23203
+ } catch (error) {
23204
+ console.warn(' → Justify setup failed, falling back to standard alignment:', error);
23205
+ cssTextAlign = textAlign.replace('justify-', '').replace('justify', 'left');
23206
+ }
23207
+ } else {
23208
+ this.textarea.style.textAlignLast = 'auto';
23209
+ this.textarea.style.textJustify = 'auto';
23210
+ this.textarea.style.wordSpacing = 'normal';
23211
+ console.log(' → Applied standard alignment:', cssTextAlign);
23212
+ }
23213
+ this.textarea.style.textAlign = cssTextAlign;
22161
23214
  this.textarea.style.color = ((_target$fill = target.fill) === null || _target$fill === void 0 ? void 0 : _target$fill.toString()) || '#000';
22162
- this.textarea.style.letterSpacing = `${(target.charSpacing || 0) / 1000}em`;
22163
- this.textarea.style.direction = target.direction || this.firstStrongDir(this.textarea.value || '');
23215
+ this.textarea.style.letterSpacing = `${letterSpacingPx}px`;
22164
23216
 
22165
- // Ensure consistent font rendering between Fabric and CSS
23217
+ // Use the already detected direction from above
23218
+ const fabricDirection = target.direction;
23219
+
23220
+ // Use auto-detected direction for better BiDi support, but respect fabric direction if it makes sense
23221
+ this.textarea.style.direction = autoDetectedDirection || fabricDirection || 'ltr';
22166
23222
  this.textarea.style.fontVariant = 'normal';
22167
23223
  this.textarea.style.fontStretch = 'normal';
22168
- this.textarea.style.textRendering = 'auto';
22169
- this.textarea.style.fontKerning = 'auto';
22170
- this.textarea.style.boxSizing = 'content-box'; // Padding is added outside width/height
23224
+ this.textarea.style.textRendering = 'auto'; // Changed from 'optimizeLegibility' to match canvas
23225
+ this.textarea.style.fontKerning = 'normal';
23226
+ this.textarea.style.fontFeatureSettings = 'normal';
23227
+ this.textarea.style.fontVariationSettings = 'normal';
22171
23228
  this.textarea.style.margin = '0';
22172
23229
  this.textarea.style.border = 'none';
22173
23230
  this.textarea.style.outline = 'none';
22174
23231
  this.textarea.style.background = 'transparent';
22175
- this.textarea.style.wordWrap = 'break-word';
23232
+ this.textarea.style.overflowWrap = 'break-word';
22176
23233
  this.textarea.style.whiteSpace = 'pre-wrap';
22177
-
22178
- // DEBUG: Log final textarea dimensions
22179
- console.log('TEXTAREA AFTER SETUP:');
22180
- console.log(' textarea width =', this.textarea.style.width);
22181
- console.log(' textarea height =', this.textarea.style.height);
22182
- console.log(' textarea padding =', this.textarea.style.padding);
22183
- console.log(' paddingX =', paddingX, 'paddingY =', paddingY);
22184
- console.log(' baseFontSize =', baseFontSize);
22185
- console.log(' scaleX =', scaleX);
22186
- console.log(' zoom =', zoom);
22187
- console.log(' finalFontSize =', finalFontSize);
22188
- console.log(' fabricLineHeight =', fabricLineHeight);
23234
+ this.textarea.style.hyphens = 'none';
23235
+
23236
+ // DEBUG: Log final CSS properties
23237
+ console.log('🎨 FINAL TEXTAREA CSS:');
23238
+ console.log(' textAlign:', this.textarea.style.textAlign);
23239
+ console.log(' textAlignLast:', this.textarea.style.textAlignLast);
23240
+ console.log(' direction:', this.textarea.style.direction);
23241
+ console.log(' unicodeBidi:', this.textarea.style.unicodeBidi);
23242
+ console.log(' width:', this.textarea.style.width);
23243
+ console.log(' textJustify:', this.textarea.style.textJustify);
23244
+ console.log(' wordSpacing:', this.textarea.style.wordSpacing);
23245
+ console.log(' whiteSpace:', this.textarea.style.whiteSpace);
23246
+
23247
+ // If justify, log Fabric object dimensions for comparison
23248
+ if (textAlign.includes('justify')) {
23249
+ var _calcTextWidth, _ref;
23250
+ console.log('🔧 FABRIC OBJECT JUSTIFY INFO:');
23251
+ console.log(' Fabric width:', target.width);
23252
+ console.log(' Fabric calcTextWidth:', (_calcTextWidth = (_ref = target).calcTextWidth) === null || _calcTextWidth === void 0 ? void 0 : _calcTextWidth.call(_ref));
23253
+ console.log(' Fabric textAlign:', target.textAlign);
23254
+ console.log(' Text lines:', target.textLines);
23255
+ }
23256
+
23257
+ // Debug font properties matching
23258
+ console.log('🔤 FONT PROPERTIES COMPARISON:');
23259
+ console.log(' Fabric fontFamily:', target.fontFamily);
23260
+ console.log(' Fabric fontWeight:', target.fontWeight);
23261
+ console.log(' Fabric fontStyle:', target.fontStyle);
23262
+ console.log(' Fabric fontSize:', target.fontSize);
23263
+ console.log(' → Textarea fontFamily:', this.textarea.style.fontFamily);
23264
+ console.log(' → Textarea fontWeight:', this.textarea.style.fontWeight);
23265
+ console.log(' → Textarea fontStyle:', this.textarea.style.fontStyle);
23266
+ console.log(' → Textarea fontSize:', this.textarea.style.fontSize);
23267
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
23268
+
23269
+ // Enhanced font rendering to better match fabric.js canvas rendering
23270
+ // Default to auto for more natural rendering
23271
+ this.textarea.style.webkitFontSmoothing = 'auto';
23272
+ this.textarea.style.mozOsxFontSmoothing = 'auto';
23273
+ this.textarea.style.fontSmooth = 'auto';
23274
+ this.textarea.style.textSizeAdjust = 'none';
23275
+
23276
+ // For bold fonts, use subpixel rendering to match canvas thickness better
23277
+ const fontWeight = String(target.fontWeight || 'normal');
23278
+ const isBold = fontWeight === 'bold' || fontWeight === '700' || parseInt(fontWeight) >= 600;
23279
+ if (isBold) {
23280
+ this.textarea.style.webkitFontSmoothing = 'subpixel-antialiased';
23281
+ this.textarea.style.mozOsxFontSmoothing = 'unset';
23282
+ console.log('🔤 Applied enhanced bold rendering for better thickness matching');
23283
+ }
23284
+ console.log('🎨 FONT SMOOTHING APPLIED:');
23285
+ console.log(' webkitFontSmoothing:', this.textarea.style.webkitFontSmoothing);
23286
+ console.log(' mozOsxFontSmoothing:', this.textarea.style.mozOsxFontSmoothing);
22189
23287
 
22190
23288
  // Initial bounds are set correctly by Fabric.js - don't force update here
22191
23289
  }
22192
23290
 
23291
+ /**
23292
+ * Debug method to compare textarea and canvas object bounding boxes
23293
+ */
23294
+ debugBoundingBoxComparison() {
23295
+ const target = this.target;
23296
+ const canvas = this.canvas;
23297
+ const zoom = canvas.getZoom();
23298
+
23299
+ // Get textarea bounding box (in screen coordinates)
23300
+ const textareaRect = this.textarea.getBoundingClientRect();
23301
+ const hostRect = this.hostDiv.getBoundingClientRect();
23302
+
23303
+ // Get canvas object bounding box (in screen coordinates)
23304
+ const canvasBounds = target.getBoundingRect();
23305
+ const canvasRect = canvas.upperCanvasEl.getBoundingClientRect();
23306
+
23307
+ // Convert canvas object bounds to screen coordinates
23308
+ const vpt = canvas.viewportTransform;
23309
+ const screenObjectBounds = {
23310
+ left: canvasRect.left + canvasBounds.left * zoom + vpt[4],
23311
+ top: canvasRect.top + canvasBounds.top * zoom + vpt[5],
23312
+ width: canvasBounds.width * zoom,
23313
+ height: canvasBounds.height * zoom
23314
+ };
23315
+ console.log('🔍 BOUNDING BOX COMPARISON:');
23316
+ console.log('📦 Textarea Rect:', {
23317
+ left: Math.round(textareaRect.left * 100) / 100,
23318
+ top: Math.round(textareaRect.top * 100) / 100,
23319
+ width: Math.round(textareaRect.width * 100) / 100,
23320
+ height: Math.round(textareaRect.height * 100) / 100
23321
+ });
23322
+ console.log('📦 Host Div Rect:', {
23323
+ left: Math.round(hostRect.left * 100) / 100,
23324
+ top: Math.round(hostRect.top * 100) / 100,
23325
+ width: Math.round(hostRect.width * 100) / 100,
23326
+ height: Math.round(hostRect.height * 100) / 100
23327
+ });
23328
+ console.log('📦 Canvas Object Bounds (screen):', {
23329
+ left: Math.round(screenObjectBounds.left * 100) / 100,
23330
+ top: Math.round(screenObjectBounds.top * 100) / 100,
23331
+ width: Math.round(screenObjectBounds.width * 100) / 100,
23332
+ height: Math.round(screenObjectBounds.height * 100) / 100
23333
+ });
23334
+ console.log('📦 Canvas Object Bounds (canvas):', canvasBounds);
23335
+
23336
+ // Calculate differences
23337
+ const hostVsObject = {
23338
+ leftDiff: Math.round((hostRect.left - screenObjectBounds.left) * 100) / 100,
23339
+ topDiff: Math.round((hostRect.top - screenObjectBounds.top) * 100) / 100,
23340
+ widthDiff: Math.round((hostRect.width - screenObjectBounds.width) * 100) / 100,
23341
+ heightDiff: Math.round((hostRect.height - screenObjectBounds.height) * 100) / 100
23342
+ };
23343
+ const textareaVsObject = {
23344
+ leftDiff: Math.round((textareaRect.left - screenObjectBounds.left) * 100) / 100,
23345
+ topDiff: Math.round((textareaRect.top - screenObjectBounds.top) * 100) / 100,
23346
+ widthDiff: Math.round((textareaRect.width - screenObjectBounds.width) * 100) / 100,
23347
+ heightDiff: Math.round((textareaRect.height - screenObjectBounds.height) * 100) / 100
23348
+ };
23349
+ console.log('📏 Host Div vs Canvas Object Diff:', hostVsObject);
23350
+ console.log('📏 Textarea vs Canvas Object Diff:', textareaVsObject);
23351
+
23352
+ // Check if they're aligned (within 2px tolerance)
23353
+ const tolerance = 2;
23354
+ const hostAligned = Math.abs(hostVsObject.leftDiff) < tolerance && Math.abs(hostVsObject.topDiff) < tolerance && Math.abs(hostVsObject.widthDiff) < tolerance && Math.abs(hostVsObject.heightDiff) < tolerance;
23355
+ const textareaAligned = Math.abs(textareaVsObject.leftDiff) < tolerance && Math.abs(textareaVsObject.topDiff) < tolerance && Math.abs(textareaVsObject.widthDiff) < tolerance && Math.abs(textareaVsObject.heightDiff) < tolerance;
23356
+ console.log(hostAligned ? '✅ Host Div ALIGNED with canvas object' : '❌ Host Div MISALIGNED with canvas object');
23357
+ console.log(textareaAligned ? '✅ Textarea ALIGNED with canvas object' : '❌ Textarea MISALIGNED with canvas object');
23358
+ console.log('🔍 Zoom:', zoom, 'Viewport Transform:', vpt);
23359
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
23360
+ }
23361
+
23362
+ /**
23363
+ * Debug method to compare text wrapping between textarea and Fabric text object
23364
+ */
23365
+ debugTextWrapping() {
23366
+ const target = this.target;
23367
+ const text = this.textarea.value;
23368
+ console.log('📝 TEXT WRAPPING COMPARISON:');
23369
+ console.log('📄 Text Content:', `"${text}"`);
23370
+ console.log('📄 Text Length:', text.length);
23371
+
23372
+ // Analyze line breaks
23373
+ const explicitLines = text.split('\n');
23374
+ console.log('📄 Explicit Lines (\\n):', explicitLines.length);
23375
+ explicitLines.forEach((line, i) => {
23376
+ console.log(` Line ${i + 1}: "${line}" (${line.length} chars)`);
23377
+ });
23378
+
23379
+ // Get textarea computed styles for wrapping analysis
23380
+ const textareaStyles = window.getComputedStyle(this.textarea);
23381
+ console.log('📐 Textarea Wrapping Styles:');
23382
+ console.log(' width:', textareaStyles.width);
23383
+ console.log(' fontSize:', textareaStyles.fontSize);
23384
+ console.log(' fontFamily:', textareaStyles.fontFamily);
23385
+ console.log(' fontWeight:', textareaStyles.fontWeight);
23386
+ console.log(' letterSpacing:', textareaStyles.letterSpacing);
23387
+ console.log(' lineHeight:', textareaStyles.lineHeight);
23388
+ console.log(' whiteSpace:', textareaStyles.whiteSpace);
23389
+ console.log(' wordWrap:', textareaStyles.wordWrap);
23390
+ console.log(' overflowWrap:', textareaStyles.overflowWrap);
23391
+ console.log(' direction:', textareaStyles.direction);
23392
+ console.log(' textAlign:', textareaStyles.textAlign);
23393
+
23394
+ // Get Fabric text object properties for comparison
23395
+ console.log('📐 Fabric Text Object Properties:');
23396
+ console.log(' width:', target.width);
23397
+ console.log(' fontSize:', target.fontSize);
23398
+ console.log(' fontFamily:', target.fontFamily);
23399
+ console.log(' fontWeight:', target.fontWeight);
23400
+ console.log(' charSpacing:', target.charSpacing);
23401
+ console.log(' lineHeight:', target.lineHeight);
23402
+ console.log(' direction:', target.direction);
23403
+ console.log(' textAlign:', target.textAlign);
23404
+ console.log(' scaleX:', target.scaleX);
23405
+ console.log(' scaleY:', target.scaleY);
23406
+
23407
+ // Calculate effective dimensions for comparison - use actual rendered width
23408
+ // **THE FIX:** Use getBoundingRect to get the *actual rendered width* of the Fabric object.
23409
+ const fabricEffectiveWidth = this.target.getBoundingRect().width;
23410
+ // Use the exact width set on textarea for comparison
23411
+ const textareaComputedWidth = parseFloat(window.getComputedStyle(this.textarea).width);
23412
+ const textareaEffectiveWidth = textareaComputedWidth / this.canvas.getZoom();
23413
+ const widthDiff = Math.abs(textareaEffectiveWidth - fabricEffectiveWidth);
23414
+ console.log('📏 Effective Width Comparison:');
23415
+ console.log(' Textarea Effective Width:', textareaEffectiveWidth);
23416
+ console.log(' Fabric Effective Width:', fabricEffectiveWidth);
23417
+ console.log(' Width Difference:', widthDiff.toFixed(2) + 'px');
23418
+ console.log(widthDiff < 1 ? '✅ Widths MATCH for wrapping' : '❌ Width MISMATCH may cause different wrapping');
23419
+
23420
+ // Check text direction and bidi handling
23421
+ const hasRTLText = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
23422
+ const hasBidiText = /[\u0590-\u06FF]/.test(text) && /[a-zA-Z]/.test(text);
23423
+ console.log('🌍 Text Direction Analysis:');
23424
+ console.log(' Has RTL characters:', hasRTLText);
23425
+ console.log(' Has mixed Bidi text:', hasBidiText);
23426
+ console.log(' Textarea direction:', textareaStyles.direction);
23427
+ console.log(' Fabric direction:', target.direction || 'auto');
23428
+ console.log(' Textarea unicodeBidi:', textareaStyles.unicodeBidi);
23429
+
23430
+ // Measure actual rendered line count
23431
+ const textareaScrollHeight = this.textarea.scrollHeight;
23432
+ const textareaLineHeight = parseFloat(textareaStyles.lineHeight) || parseFloat(textareaStyles.fontSize) * 1.2;
23433
+ const estimatedTextareaLines = Math.round(textareaScrollHeight / textareaLineHeight);
23434
+ console.log('📊 Line Count Analysis:');
23435
+ console.log(' Textarea scrollHeight:', textareaScrollHeight);
23436
+ console.log(' Textarea lineHeight:', textareaLineHeight);
23437
+ console.log(' Estimated rendered lines:', estimatedTextareaLines);
23438
+ console.log(' Explicit line breaks:', explicitLines.length);
23439
+ if (estimatedTextareaLines > explicitLines.length) {
23440
+ console.log('🔄 Text wrapping detected in textarea');
23441
+ console.log(' Wrapped lines:', estimatedTextareaLines - explicitLines.length);
23442
+ } else {
23443
+ console.log('📏 No text wrapping in textarea');
23444
+ }
23445
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
23446
+ }
23447
+
22193
23448
  /**
22194
23449
  * Focus the textarea and position cursor at end
22195
23450
  */
@@ -22218,6 +23473,11 @@ class OverlayEditor {
22218
23473
  this.canvas.requestRenderAll();
22219
23474
  this.target.setCoords();
22220
23475
  this.applyOverlayStyle();
23476
+
23477
+ // Fix character mapping issues after JSON loading for browser-wrapped fonts
23478
+ if (this.target._fixCharacterMappingAfterJsonLoad) {
23479
+ this.target._fixCharacterMappingAfterJsonLoad();
23480
+ }
22221
23481
  this.textarea.focus();
22222
23482
  this.textarea.setSelectionRange(this.textarea.value.length, this.textarea.value.length);
22223
23483
 
@@ -22263,6 +23523,23 @@ class OverlayEditor {
22263
23523
  // Handle commit/cancel after restoring visibility
22264
23524
  if (commit && !this.isComposing) {
22265
23525
  const finalText = this.textarea.value;
23526
+
23527
+ // Auto-detect text direction and update fabric object if needed
23528
+ const detectedDirection = this.firstStrongDir(finalText);
23529
+ const currentDirection = this.target.direction || 'ltr';
23530
+ if (detectedDirection && detectedDirection !== currentDirection) {
23531
+ console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
23532
+ console.log(` Text content: "${finalText.substring(0, 50)}..."`);
23533
+
23534
+ // Update the fabric object's direction
23535
+ this.target.set('direction', detectedDirection);
23536
+
23537
+ // Force a re-render to apply the direction change
23538
+ this.canvas.requestRenderAll();
23539
+ console.log(`✅ Fabric object direction updated to: ${detectedDirection}`);
23540
+ } else {
23541
+ console.log(`📝 Overlay Exit: Direction unchanged (${currentDirection}), text: "${finalText.substring(0, 30)}..."`);
23542
+ }
22266
23543
  if (this.onCommit) {
22267
23544
  this.onCommit(finalText);
22268
23545
  }
@@ -22302,25 +23579,40 @@ class OverlayEditor {
22302
23579
  }
22303
23580
  }
22304
23581
  autoResizeTextarea() {
22305
- // Allow both vertical growth and shrinking; host width stays fixed
22306
- const oldHeight = parseFloat(window.getComputedStyle(this.textarea).height);
22307
-
22308
- // Reset height to measure actual needed height
22309
- this.textarea.style.height = 'auto';
23582
+ // Store the scroll position and the container's old height for comparison.
23583
+ const scrollTop = this.textarea.scrollTop;
23584
+ const oldHeight = parseFloat(this.hostDiv.style.height || '0');
23585
+
23586
+ // 1. **Force a reliable reflow.**
23587
+ // First, reset the textarea's height to a minimal value. This is the crucial step
23588
+ // that forces the browser to recalculate the content's height from scratch,
23589
+ // ignoring the hostDiv's larger, stale height.
23590
+ this.textarea.style.height = '1px';
23591
+
23592
+ // 2. Read the now-accurate scrollHeight. This value reflects the minimum
23593
+ // height required for the content, whether it's single or multi-line.
22310
23594
  const scrollHeight = this.textarea.scrollHeight;
22311
23595
 
22312
- // Add extra padding to prevent text clipping (especially for line height)
22313
- const lineHeightBuffer = 8; // Extra space to prevent clipping
22314
- const newHeight = Math.max(scrollHeight + lineHeightBuffer, 25); // Minimum height with buffer
22315
- const heightChanged = Math.abs(newHeight - oldHeight) > 2; // Only if meaningful change
23596
+ // A small buffer for rendering consistency across browsers.
23597
+ const buffer = 2;
23598
+ const newHeight = scrollHeight + buffer;
22316
23599
 
22317
- this.textarea.style.height = `${newHeight}px`;
22318
- this.hostDiv.style.height = `${newHeight}px`; // Match exactly
23600
+ // Check if the height has changed significantly.
23601
+ const heightChanged = Math.abs(newHeight - oldHeight) > 1;
22319
23602
 
22320
- // Only update object bounds if height actually changed
23603
+ // 4. Only update heights and object bounds if there was a change.
22321
23604
  if (heightChanged) {
23605
+ this.textarea.style.height = `${newHeight}px`;
23606
+ this.hostDiv.style.height = `${newHeight}px`;
22322
23607
  this.updateObjectBounds();
23608
+ } else {
23609
+ // If no significant change, ensure the textarea's height matches the container
23610
+ // to prevent any minor visual misalignment.
23611
+ this.textarea.style.height = this.hostDiv.style.height;
22323
23612
  }
23613
+
23614
+ // 5. Restore the original scroll position.
23615
+ this.textarea.scrollTop = scrollTop;
22324
23616
  }
22325
23617
  handleKeyDown(e) {
22326
23618
  if (e.key === 'Escape') {
@@ -22329,6 +23621,19 @@ class OverlayEditor {
22329
23621
  } else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
22330
23622
  e.preventDefault();
22331
23623
  this.destroy(true); // Commit
23624
+ } else if (e.key === 'Enter' || e.key === 'Backspace' || e.key === 'Delete') {
23625
+ // For keys that might change the height, schedule a resize check
23626
+ // Use both immediate and delayed checks to catch all scenarios
23627
+ requestAnimationFrame(() => {
23628
+ if (!this.isDestroyed) {
23629
+ this.autoResizeTextarea();
23630
+ }
23631
+ });
23632
+ setTimeout(() => {
23633
+ if (!this.isDestroyed) {
23634
+ this.autoResizeTextarea();
23635
+ }
23636
+ }, 10); // Small delay to ensure DOM is updated
22332
23637
  }
22333
23638
  }
22334
23639
  handleFocus() {
@@ -25305,6 +26610,7 @@ class Textbox extends IText {
25305
26610
  ...Textbox.ownDefaults,
25306
26611
  ...options
25307
26612
  });
26613
+ this.initializeEventListeners();
25308
26614
  }
25309
26615
 
25310
26616
  /**
@@ -25326,8 +26632,27 @@ class Textbox extends IText {
25326
26632
  */
25327
26633
  initDimensions() {
25328
26634
  if (!this.initialized) {
26635
+ this.initialized = true;
26636
+ }
26637
+
26638
+ // Prevent rapid recalculations during moves
26639
+ if (this._usingBrowserWrapping) {
26640
+ const now = Date.now();
26641
+ const lastCall = this._lastInitDimensionsTime || 0;
26642
+ const isRapidCall = now - lastCall < 100;
26643
+ const isDuringLoading = this._jsonLoading || !this._browserWrapInitialized;
26644
+ if (isRapidCall && !isDuringLoading) {
26645
+ return;
26646
+ }
26647
+ this._lastInitDimensionsTime = now;
26648
+ }
26649
+
26650
+ // Skip if nothing changed
26651
+ const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
26652
+ if (this._lastDimensionState === currentState && this._textLines && this._textLines.length > 0) {
25329
26653
  return;
25330
26654
  }
26655
+ this._lastDimensionState = currentState;
25331
26656
 
25332
26657
  // Use advanced layout if enabled
25333
26658
  if (this.enableAdvancedLayout) {
@@ -25338,17 +26663,142 @@ class Textbox extends IText {
25338
26663
  // clear dynamicMinWidth as it will be different after we re-wrap line
25339
26664
  this.dynamicMinWidth = 0;
25340
26665
  // wrap lines
25341
- this._styleMap = this._generateStyleMap(this._splitText());
25342
- // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap
25343
- if (this.dynamicMinWidth > this.width) {
26666
+ const splitTextResult = this._splitText();
26667
+ this._styleMap = this._generateStyleMap(splitTextResult);
26668
+
26669
+ // For browser wrapping, ensure _textLines is set from browser results
26670
+ if (this._usingBrowserWrapping && splitTextResult && splitTextResult.lines) {
26671
+ this._textLines = splitTextResult.lines.map(line => line.split(''));
26672
+
26673
+ // Store justify measurements and browser height
26674
+ const justifyMeasurements = splitTextResult.justifySpaceMeasurements;
26675
+ if (justifyMeasurements) {
26676
+ this._styleMap.justifySpaceMeasurements = justifyMeasurements;
26677
+ }
26678
+ const actualHeight = splitTextResult.actualBrowserHeight;
26679
+ if (actualHeight) {
26680
+ this._actualBrowserHeight = actualHeight;
26681
+ }
26682
+ }
26683
+ // Don't auto-resize width when using browser wrapping to prevent width increases during moves
26684
+ if (!this._usingBrowserWrapping && this.dynamicMinWidth > this.width) {
25344
26685
  this._set('width', this.dynamicMinWidth);
25345
26686
  }
26687
+
26688
+ // For browser wrapping fonts (like STV), ensure minimum width for new textboxes
26689
+ // since these fonts can't measure English characters properly
26690
+ if (this._usingBrowserWrapping && this.width < 50) {
26691
+ console.log(`🔤 BROWSER WRAP: Font ${this.fontFamily} has width ${this.width}px, setting to 300px for usability`);
26692
+ this.width = 300;
26693
+ }
26694
+
26695
+ // Mark browser wrapping as initialized when complete
26696
+ if (this._usingBrowserWrapping) {
26697
+ this._browserWrapInitialized = true;
26698
+ }
25346
26699
  if (this.textAlign.includes(JUSTIFY)) {
26700
+ // For browser wrapping fonts, apply browser-calculated justify spaces
26701
+ if (this._usingBrowserWrapping) {
26702
+ console.log('🔤 BROWSER WRAP: Applying browser-calculated justify spaces');
26703
+ this._applyBrowserJustifySpaces();
26704
+ return;
26705
+ }
26706
+
26707
+ // Don't apply justify alignment during drag operations to prevent snapping
26708
+ const now = Date.now();
26709
+ const lastDragTime = this._lastInitDimensionsTime || 0;
26710
+ const isDuringDrag = now - lastDragTime < 200; // 200ms window for drag detection
26711
+
26712
+ if (isDuringDrag) {
26713
+ console.log('🔤 Skipping justify during drag operation to prevent snapping');
26714
+ return;
26715
+ }
26716
+
26717
+ // For non-browser-wrapping fonts, use Fabric's justify system
25347
26718
  // once text is measured we need to make space fatter to make justified text.
25348
- this.enlargeSpaces();
26719
+ // Ensure __charBounds exists and fonts are ready before applying justify
26720
+ if (this.__charBounds && this.__charBounds.length > 0) {
26721
+ // Check if font is ready for accurate justify calculations
26722
+ const fontReady = this._isFontReady ? this._isFontReady() : true;
26723
+ if (fontReady) {
26724
+ this.enlargeSpaces();
26725
+ } else {
26726
+ console.warn('⚠️ Textbox: Font not ready for justify, deferring enlargeSpaces');
26727
+ // Defer justify calculation until font is ready
26728
+ this._scheduleJustifyAfterFontLoad();
26729
+ }
26730
+ } else {
26731
+ console.warn('⚠️ Textbox: __charBounds not ready for justify alignment, deferring enlargeSpaces');
26732
+ // Defer the justify calculation until the next frame
26733
+ setTimeout(() => {
26734
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
26735
+ var _this$canvas;
26736
+ console.log('🔧 Applying deferred Textbox justify alignment');
26737
+ this.enlargeSpaces();
26738
+ (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
26739
+ }
26740
+ }, 0);
26741
+ }
26742
+ }
26743
+ // Calculate height - use Fabric's calculation for proper text rendering space
26744
+ if (this._usingBrowserWrapping && this._textLines && this._textLines.length > 0) {
26745
+ const actualBrowserHeight = this._actualBrowserHeight;
26746
+ const oldHeight = this.height;
26747
+ // Use Fabric's height calculation since it knows how much space text rendering needs
26748
+ this.height = this.calcTextHeight();
26749
+
26750
+ // Force canvas refresh and control update if height changed significantly
26751
+ if (Math.abs(this.height - oldHeight) > 1) {
26752
+ var _this$canvas2, _this$_textLines;
26753
+ this.setCoords();
26754
+ (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
26755
+
26756
+ // DEBUG: Log exact positioning details
26757
+ console.log(`🎯 POSITIONING DEBUG:`);
26758
+ console.log(` Textbox height: ${this.height}px`);
26759
+ console.log(` Textbox top: ${this.top}px`);
26760
+ console.log(` Textbox left: ${this.left}px`);
26761
+ console.log(` Text lines: ${((_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length) || 0}`);
26762
+ console.log(` Font size: ${this.fontSize}px`);
26763
+ console.log(` Line height: ${this.lineHeight || 1.16}`);
26764
+ console.log(` Calculated line height: ${this.fontSize * (this.lineHeight || 1.16)}px`);
26765
+ console.log(` _getTopOffset(): ${this._getTopOffset()}px`);
26766
+ console.log(` calcTextHeight(): ${this.calcTextHeight()}px`);
26767
+ console.log(` Browser height: ${actualBrowserHeight}px`);
26768
+ console.log(` Height difference: ${this.height - this.calcTextHeight()}px`);
26769
+ }
26770
+ } else {
26771
+ this.height = this.calcTextHeight();
26772
+ }
26773
+ }
26774
+
26775
+ /**
26776
+ * Schedule justify calculation after font loads (Textbox-specific)
26777
+ * @private
26778
+ */
26779
+ _scheduleJustifyAfterFontLoad() {
26780
+ if (typeof document === 'undefined' || !('fonts' in document)) {
26781
+ return;
25349
26782
  }
25350
- // clear cache and re-calculate height
25351
- this.height = this.calcTextHeight();
26783
+
26784
+ // Only schedule if not already waiting
26785
+ if (this._fontJustifyScheduled) {
26786
+ return;
26787
+ }
26788
+ this._fontJustifyScheduled = true;
26789
+ const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
26790
+ document.fonts.load(fontSpec).then(() => {
26791
+ var _this$canvas3;
26792
+ this._fontJustifyScheduled = false;
26793
+ console.log('🔧 Textbox: Font loaded, applying justify alignment');
26794
+
26795
+ // Re-run initDimensions to ensure proper justify calculation
26796
+ this.initDimensions();
26797
+ (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
26798
+ }).catch(() => {
26799
+ this._fontJustifyScheduled = false;
26800
+ console.warn('⚠️ Textbox: Font loading failed, justify may be incorrect');
26801
+ });
25352
26802
  }
25353
26803
 
25354
26804
  /**
@@ -25715,19 +27165,33 @@ class Textbox extends IText {
25715
27165
  width: wordWidth
25716
27166
  } = data[i];
25717
27167
  offset += word.length;
25718
- lineWidth += infixWidth + wordWidth - additionalSpace;
25719
- if (lineWidth > maxWidth && !lineJustStarted) {
27168
+
27169
+ // Predictive wrapping: check if adding this word would exceed the width
27170
+ const potentialLineWidth = lineWidth + infixWidth + wordWidth - additionalSpace;
27171
+ // Use exact width to match overlay editor behavior
27172
+ const conservativeMaxWidth = maxWidth; // No artificial buffer
27173
+
27174
+ // Debug logging for wrapping decisions
27175
+ const currentLineText = line.join('');
27176
+ console.log(`🔧 FABRIC WRAP CHECK: "${data[i].word}" -> potential: ${potentialLineWidth.toFixed(1)}px vs limit: ${conservativeMaxWidth.toFixed(1)}px`);
27177
+ if (potentialLineWidth > conservativeMaxWidth && !lineJustStarted) {
27178
+ // This word would exceed the width, wrap before adding it
27179
+ console.log(`🔧 FABRIC WRAP! Line: "${currentLineText}" (${lineWidth.toFixed(1)}px)`);
25720
27180
  graphemeLines.push(line);
25721
27181
  line = [];
25722
- lineWidth = wordWidth;
27182
+ lineWidth = wordWidth; // Start new line with just this word
25723
27183
  lineJustStarted = true;
25724
27184
  } else {
25725
- lineWidth += additionalSpace;
27185
+ // Word fits, add it to current line
27186
+ lineWidth = potentialLineWidth + additionalSpace;
25726
27187
  }
25727
27188
  if (!lineJustStarted && !splitByGrapheme) {
25728
27189
  line.push(infix);
25729
27190
  }
25730
27191
  line = line.concat(word);
27192
+
27193
+ // Debug: show current line after adding word
27194
+ console.log(`🔧 FABRIC AFTER ADD: Line now: "${line.join('')}" (${line.length} chars)`);
25731
27195
  infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset);
25732
27196
  offset++;
25733
27197
  lineJustStarted = false;
@@ -25737,9 +27201,19 @@ class Textbox extends IText {
25737
27201
  // TODO: this code is probably not necessary anymore.
25738
27202
  // it can be moved out of this function since largestWordWidth is now
25739
27203
  // known in advance
25740
- if (largestWordWidth + reservedSpace > this.dynamicMinWidth) {
27204
+ // Don't modify dynamicMinWidth when using browser wrapping to prevent width increases
27205
+ if (!this._usingBrowserWrapping && largestWordWidth + reservedSpace > this.dynamicMinWidth) {
27206
+ console.log(`🔧 FABRIC updating dynamicMinWidth: ${this.dynamicMinWidth} -> ${largestWordWidth - additionalSpace + reservedSpace}`);
25741
27207
  this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace;
27208
+ } else if (this._usingBrowserWrapping) {
27209
+ console.log(`🔤 BROWSER WRAP: Skipping dynamicMinWidth update to prevent width increase`);
25742
27210
  }
27211
+
27212
+ // Debug: show final wrapped lines
27213
+ console.log(`🔧 FABRIC FINAL LINES: ${graphemeLines.length} lines`);
27214
+ graphemeLines.forEach((line, i) => {
27215
+ console.log(` Line ${i + 1}: "${line.join('')}" (${line.length} chars)`);
27216
+ });
25743
27217
  return graphemeLines;
25744
27218
  }
25745
27219
 
@@ -25783,6 +27257,260 @@ class Textbox extends IText {
25783
27257
  * @override
25784
27258
  */
25785
27259
  _splitTextIntoLines(text) {
27260
+ // Check if we need browser wrapping using smart font detection
27261
+ const needsBrowserWrapping = this.fontFamily && fontLacksEnglishGlyphsCached(this.fontFamily);
27262
+ if (needsBrowserWrapping) {
27263
+ // Cache key based on text content, width, font properties, AND text alignment
27264
+ const textHash = text.length + text.slice(0, 50); // Include text content in cache key
27265
+ const cacheKey = `${textHash}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
27266
+
27267
+ // Check if we have a cached result and nothing has changed
27268
+ if (this._browserWrapCache && this._browserWrapCache.key === cacheKey) {
27269
+ const cachedResult = this._browserWrapCache.result;
27270
+
27271
+ // For justify alignment, ensure we have the measurements
27272
+ if (this.textAlign.includes('justify') && !cachedResult.justifySpaceMeasurements) ; else {
27273
+ return cachedResult;
27274
+ }
27275
+ }
27276
+ const result = this._splitTextIntoLinesWithBrowser(text);
27277
+
27278
+ // Cache the result
27279
+ this._browserWrapCache = {
27280
+ key: cacheKey,
27281
+ result
27282
+ };
27283
+
27284
+ // Mark that we used browser wrapping to prevent dynamicMinWidth modifications
27285
+ this._usingBrowserWrapping = true;
27286
+ return result;
27287
+ }
27288
+
27289
+ // Clear the browser wrapping flag when using regular wrapping
27290
+ this._usingBrowserWrapping = false;
27291
+
27292
+ // Default Fabric wrapping for other fonts
27293
+ const newText = super._splitTextIntoLines(text),
27294
+ graphemeLines = this._wrapText(newText.lines, this.width),
27295
+ lines = new Array(graphemeLines.length);
27296
+ for (let i = 0; i < graphemeLines.length; i++) {
27297
+ lines[i] = graphemeLines[i].join('');
27298
+ }
27299
+ newText.lines = lines;
27300
+ newText.graphemeLines = graphemeLines;
27301
+ return newText;
27302
+ }
27303
+
27304
+ /**
27305
+ * Use browser's native text wrapping for accurate handling of fonts without English glyphs
27306
+ * @private
27307
+ */
27308
+ _splitTextIntoLinesWithBrowser(text) {
27309
+ if (typeof document === 'undefined') {
27310
+ // Fallback to regular wrapping in Node.js
27311
+ return this._splitTextIntoLinesDefault(text);
27312
+ }
27313
+
27314
+ // Create a hidden element that mimics the overlay editor
27315
+ const testElement = document.createElement('div');
27316
+ testElement.style.position = 'absolute';
27317
+ testElement.style.left = '-9999px';
27318
+ testElement.style.visibility = 'hidden';
27319
+ testElement.style.fontSize = `${this.fontSize}px`;
27320
+ testElement.style.fontFamily = `"${this.fontFamily}"`;
27321
+ testElement.style.fontWeight = String(this.fontWeight || 'normal');
27322
+ testElement.style.fontStyle = String(this.fontStyle || 'normal');
27323
+ testElement.style.lineHeight = String(this.lineHeight || 1.16);
27324
+ testElement.style.width = `${this.width}px`;
27325
+ testElement.style.direction = this.direction || 'ltr';
27326
+ testElement.style.whiteSpace = 'pre-wrap';
27327
+ testElement.style.wordBreak = 'normal';
27328
+ testElement.style.overflowWrap = 'break-word';
27329
+
27330
+ // Set browser-native text alignment (including justify)
27331
+ if (this.textAlign.includes('justify')) {
27332
+ testElement.style.textAlign = 'justify';
27333
+ testElement.style.textAlignLast = 'auto'; // Let browser decide last line alignment
27334
+ } else {
27335
+ testElement.style.textAlign = this.textAlign;
27336
+ }
27337
+ testElement.textContent = text;
27338
+ document.body.appendChild(testElement);
27339
+
27340
+ // Get the browser's natural line breaks
27341
+ const range = document.createRange();
27342
+ const lines = [];
27343
+ const graphemeLines = [];
27344
+ try {
27345
+ // Simple approach: split by measuring character positions
27346
+ const textNode = testElement.firstChild;
27347
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
27348
+ let currentLineStart = 0;
27349
+ const textLength = text.length;
27350
+ let previousBottom = 0;
27351
+ for (let i = 0; i <= textLength; i++) {
27352
+ range.setStart(textNode, currentLineStart);
27353
+ range.setEnd(textNode, i);
27354
+ const rect = range.getBoundingClientRect();
27355
+ if (i > currentLineStart && (rect.bottom > previousBottom + 5 || i === textLength)) {
27356
+ // New line detected or end of text
27357
+ const lineEnd = i === textLength ? i : i - 1;
27358
+ const lineText = text.substring(currentLineStart, lineEnd).trim();
27359
+ if (lineText) {
27360
+ lines.push(lineText);
27361
+ // Convert to graphemes for compatibility
27362
+ const graphemeLine = lineText.split('');
27363
+ graphemeLines.push(graphemeLine);
27364
+ }
27365
+ currentLineStart = lineEnd;
27366
+ previousBottom = rect.bottom;
27367
+ }
27368
+ }
27369
+ }
27370
+ } catch (error) {
27371
+ console.warn('Browser wrapping failed, using fallback:', error);
27372
+ document.body.removeChild(testElement);
27373
+ return this._splitTextIntoLinesDefault(text);
27374
+ }
27375
+
27376
+ // Extract actual browser height BEFORE removing element
27377
+ const actualBrowserHeight = testElement.scrollHeight;
27378
+ const offsetHeight = testElement.offsetHeight;
27379
+ const clientHeight = testElement.clientHeight;
27380
+ const boundingRect = testElement.getBoundingClientRect();
27381
+ console.log(`🔤 Browser element measurements:`);
27382
+ console.log(` scrollHeight: ${actualBrowserHeight}px (content + padding + hidden overflow)`);
27383
+ console.log(` offsetHeight: ${offsetHeight}px (content + padding + border)`);
27384
+ console.log(` clientHeight: ${clientHeight}px (content + padding, no border/scrollbar)`);
27385
+ console.log(` boundingRect.height: ${boundingRect.height}px (actual rendered height)`);
27386
+ console.log(` Font size: ${this.fontSize}px, Line height: ${this.lineHeight || 1.16}, Lines: ${lines.length}`);
27387
+
27388
+ // For justify alignment, extract space measurements from browser BEFORE removing element
27389
+ let justifySpaceMeasurements = null;
27390
+ if (this.textAlign.includes('justify')) {
27391
+ justifySpaceMeasurements = this._extractJustifySpaceMeasurements(testElement, lines);
27392
+ }
27393
+ document.body.removeChild(testElement);
27394
+ console.log(`🔤 Browser wrapping result: ${lines.length} lines`);
27395
+
27396
+ // Try different height measurements to find the most accurate
27397
+ let bestHeight = actualBrowserHeight;
27398
+
27399
+ // If scrollHeight and offsetHeight differ significantly, investigate
27400
+ if (Math.abs(actualBrowserHeight - offsetHeight) > 2) {
27401
+ console.log(`🔤 Height discrepancy detected: scrollHeight=${actualBrowserHeight}px vs offsetHeight=${offsetHeight}px`);
27402
+ }
27403
+
27404
+ // Consider using boundingRect height if it's larger (sometimes more accurate for visible content)
27405
+ if (boundingRect.height > bestHeight) {
27406
+ console.log(`🔤 Using boundingRect height (${boundingRect.height}px) instead of scrollHeight (${bestHeight}px)`);
27407
+ bestHeight = boundingRect.height;
27408
+ }
27409
+
27410
+ // Font-specific height adjustments for accurate bounding box
27411
+ let adjustedHeight = bestHeight;
27412
+
27413
+ // Fonts without English glyphs need additional height buffer due to different font metrics
27414
+ const lacksEnglishGlyphs = fontLacksEnglishGlyphsCached(this.fontFamily);
27415
+ if (lacksEnglishGlyphs) {
27416
+ const glyphBuffer = this.fontSize * 0.25; // 25% of font size for non-English fonts
27417
+ adjustedHeight = bestHeight + glyphBuffer;
27418
+ console.log(`🔤 Non-English font detected (${this.fontFamily}): Adding ${glyphBuffer}px buffer (${bestHeight}px + ${glyphBuffer}px = ${adjustedHeight}px)`);
27419
+ } else {
27420
+ console.log(`🔤 Standard font (${this.fontFamily}): Using browser height directly (${bestHeight}px)`);
27421
+ }
27422
+ return {
27423
+ _unwrappedLines: [text.split('')],
27424
+ lines: lines,
27425
+ graphemeText: text.split(''),
27426
+ graphemeLines: graphemeLines,
27427
+ justifySpaceMeasurements: justifySpaceMeasurements,
27428
+ actualBrowserHeight: adjustedHeight
27429
+ };
27430
+ }
27431
+
27432
+ /**
27433
+ * Extract justify space measurements from browser
27434
+ * @private
27435
+ */
27436
+ _extractJustifySpaceMeasurements(element, lines) {
27437
+ console.log(`🔤 Extracting browser justify space measurements for ${lines.length} lines`);
27438
+
27439
+ // For now, we'll use a simplified approach:
27440
+ // Apply uniform space expansion to match the line width
27441
+ const spaceWidths = [];
27442
+ lines.forEach((line, lineIndex) => {
27443
+ const lineSpaces = [];
27444
+ const spaceCount = (line.match(/\s/g) || []).length;
27445
+ if (spaceCount > 0 && lineIndex < lines.length - 1) {
27446
+ // Don't justify last line
27447
+ // Calculate how much space expansion is needed
27448
+ const normalSpaceWidth = 6.4; // Default space width for STV font
27449
+ const lineWidth = this.width;
27450
+
27451
+ // Estimate natural line width
27452
+ const charCount = line.length - spaceCount;
27453
+ const avgCharWidth = 12; // Approximate for STV font
27454
+
27455
+ // Calculate expanded space width
27456
+ const remainingSpace = lineWidth - charCount * avgCharWidth;
27457
+ const expandedSpaceWidth = remainingSpace / spaceCount;
27458
+ console.log(`🔤 Line ${lineIndex}: ${spaceCount} spaces, natural: ${normalSpaceWidth}px -> justified: ${expandedSpaceWidth.toFixed(1)}px`);
27459
+
27460
+ // Fill array with expanded space widths for this line
27461
+ for (let i = 0; i < spaceCount; i++) {
27462
+ lineSpaces.push(expandedSpaceWidth);
27463
+ }
27464
+ }
27465
+ spaceWidths.push(lineSpaces);
27466
+ });
27467
+ return spaceWidths;
27468
+ }
27469
+
27470
+ /**
27471
+ * Apply browser-calculated justify space measurements
27472
+ * @private
27473
+ */
27474
+ _applyBrowserJustifySpaces() {
27475
+ if (!this._textLines || !this.__charBounds) {
27476
+ console.warn('🔤 BROWSER JUSTIFY: _textLines or __charBounds not ready');
27477
+ return;
27478
+ }
27479
+
27480
+ // Get space measurements from browser wrapping result
27481
+ const styleMap = this._styleMap;
27482
+ if (!styleMap || !styleMap.justifySpaceMeasurements) {
27483
+ console.warn('🔤 BROWSER JUSTIFY: No justify space measurements available');
27484
+ return;
27485
+ }
27486
+ const spaceWidths = styleMap.justifySpaceMeasurements;
27487
+ console.log('🔤 BROWSER JUSTIFY: Applying space measurements to __charBounds');
27488
+
27489
+ // Apply space widths to character bounds
27490
+ this._textLines.forEach((line, lineIndex) => {
27491
+ if (!this.__charBounds || !this.__charBounds[lineIndex] || !spaceWidths[lineIndex]) return;
27492
+ const lineBounds = this.__charBounds[lineIndex];
27493
+ const lineSpaceWidths = spaceWidths[lineIndex];
27494
+ let spaceIndex = 0;
27495
+ for (let charIndex = 0; charIndex < line.length; charIndex++) {
27496
+ if (/\s/.test(line[charIndex]) && spaceIndex < lineSpaceWidths.length) {
27497
+ const expandedWidth = lineSpaceWidths[spaceIndex];
27498
+ if (lineBounds[charIndex]) {
27499
+ const oldWidth = lineBounds[charIndex].width;
27500
+ lineBounds[charIndex].width = expandedWidth;
27501
+ console.log(`🔤 Line ${lineIndex} space ${spaceIndex}: ${oldWidth.toFixed(1)}px -> ${expandedWidth.toFixed(1)}px`);
27502
+ }
27503
+ spaceIndex++;
27504
+ }
27505
+ }
27506
+ });
27507
+ }
27508
+
27509
+ /**
27510
+ * Fallback to default Fabric wrapping
27511
+ * @private
27512
+ */
27513
+ _splitTextIntoLinesDefault(text) {
25786
27514
  const newText = super._splitTextIntoLines(text),
25787
27515
  graphemeLines = this._wrapText(newText.lines, this.width),
25788
27516
  lines = new Array(graphemeLines.length);
@@ -25812,6 +27540,204 @@ class Textbox extends IText {
25812
27540
  }
25813
27541
  }
25814
27542
 
27543
+ /**
27544
+ * Initialize event listeners for safety snap functionality
27545
+ * @private
27546
+ */
27547
+ initializeEventListeners() {
27548
+ var _this$canvas4;
27549
+ // Track which side is being used for resize to handle position compensation
27550
+ let resizeOrigin = null;
27551
+
27552
+ // Detect resize origin during resizing
27553
+ this.on('resizing', e => {
27554
+ // Check transform origin to determine which side is being resized
27555
+ if (e.transform) {
27556
+ const {
27557
+ originX
27558
+ } = e.transform;
27559
+ // originX tells us which side is the anchor - opposite side is being dragged
27560
+ resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
27561
+ } else if (e.originX) {
27562
+ const {
27563
+ originX
27564
+ } = e;
27565
+ resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
27566
+ }
27567
+ });
27568
+
27569
+ // Only trigger safety snap after resize is complete (not during)
27570
+ // Use 'modified' event which fires after user releases the mouse
27571
+ this.on('modified', () => {
27572
+ const currentResizeOrigin = resizeOrigin; // Capture the value before reset
27573
+ // Small delay to ensure text layout is updated
27574
+ setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
27575
+ resizeOrigin = null; // Reset after capturing
27576
+ });
27577
+
27578
+ // Also listen to canvas-level modified event as backup
27579
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.on('object:modified', e => {
27580
+ if (e.target === this) {
27581
+ const currentResizeOrigin = resizeOrigin; // Capture the value before reset
27582
+ setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
27583
+ resizeOrigin = null; // Reset after capturing
27584
+ }
27585
+ });
27586
+ }
27587
+
27588
+ /**
27589
+ * Safety snap to prevent glyph clipping after manual resize.
27590
+ * Similar to Polotno - checks if any glyphs are too close to edges
27591
+ * and automatically expands width if needed.
27592
+ * @private
27593
+ * @param resizeOrigin - Which side was used for resizing ('left' or 'right')
27594
+ */
27595
+ safetySnapWidth(resizeOrigin) {
27596
+ // For Textbox objects, we always want to check for clipping regardless of isWrapping flag
27597
+ if (!this._textLines || this.type.toLowerCase() !== 'textbox' || this._textLines.length === 0) {
27598
+ return;
27599
+ }
27600
+ const lineCount = this._textLines.length;
27601
+ if (lineCount === 0) return;
27602
+ let maxRequiredWidth = 0; // Width including RTL buffer
27603
+
27604
+ for (let i = 0; i < lineCount; i++) {
27605
+ const lineText = this._textLines[i].join(''); // Convert grapheme array to string
27606
+ const lineWidth = this.getLineWidth(i);
27607
+
27608
+ // RTL detection - regex for Arabic, Hebrew, and other RTL characters
27609
+ const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/;
27610
+ if (rtlRegex.test(lineText)) {
27611
+ // Add minimal RTL compensation buffer - just enough to prevent clipping
27612
+ const rtlBuffer = (this.fontSize || 16) * 0.15; // 15% of font size (much smaller)
27613
+ maxRequiredWidth = Math.max(maxRequiredWidth, lineWidth + rtlBuffer);
27614
+ } else {
27615
+ maxRequiredWidth = Math.max(maxRequiredWidth, lineWidth);
27616
+ }
27617
+ }
27618
+
27619
+ // Safety margin - how close glyphs can get before we snap
27620
+ const safetyThreshold = 2; // px - very subtle trigger
27621
+
27622
+ if (maxRequiredWidth > this.width - safetyThreshold) {
27623
+ var _this$canvas5;
27624
+ // Set width to exactly what's needed + minimal safety margin
27625
+ const newWidth = maxRequiredWidth + 1; // Add just 1px safety margin
27626
+
27627
+ // Store original position before width change
27628
+ const originalLeft = this.left;
27629
+ const originalTop = this.top;
27630
+ const widthIncrease = newWidth - this.width;
27631
+
27632
+ // Change width
27633
+ this.set('width', newWidth);
27634
+
27635
+ // Force text layout recalculation
27636
+ this.initDimensions();
27637
+
27638
+ // Only compensate position when resizing from left handle
27639
+ // Right handle resize doesn't shift the text position
27640
+ if (resizeOrigin === 'left') {
27641
+ // When resizing from left, the expansion pushes text right
27642
+ // Compensate by moving the textbox left by the width increase
27643
+ this.set({
27644
+ 'left': originalLeft - widthIncrease,
27645
+ 'top': originalTop
27646
+ });
27647
+ }
27648
+ this.setCoords();
27649
+
27650
+ // Also refresh the overlay editor if it exists
27651
+ if (this.__overlayEditor) {
27652
+ setTimeout(() => {
27653
+ this.__overlayEditor.refresh();
27654
+ }, 0);
27655
+ }
27656
+ (_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
27657
+ }
27658
+ }
27659
+
27660
+ /**
27661
+ * Fix character selection mismatch after JSON loading for browser-wrapped fonts
27662
+ * @private
27663
+ */
27664
+ _fixCharacterMappingAfterJsonLoad() {
27665
+ if (this._usingBrowserWrapping) {
27666
+ // Clear all cached states to force fresh text layout calculation
27667
+ this._browserWrapCache = null;
27668
+ this._lastDimensionState = null;
27669
+
27670
+ // Force complete re-initialization
27671
+ this.initDimensions();
27672
+ this._forceClearCache = true;
27673
+
27674
+ // Ensure canvas refresh
27675
+ this.setCoords();
27676
+ if (this.canvas) {
27677
+ this.canvas.requestRenderAll();
27678
+ }
27679
+ }
27680
+ }
27681
+
27682
+ /**
27683
+ * Force complete textbox re-initialization (useful after JSON loading)
27684
+ * Overrides Text version with Textbox-specific logic
27685
+ */
27686
+ forceTextReinitialization() {
27687
+ console.log('🔄 Force reinitializing Textbox object');
27688
+
27689
+ // CRITICAL: Ensure textbox is marked as initialized
27690
+ this.initialized = true;
27691
+
27692
+ // Clear all caches and force dirty state
27693
+ this._clearCache();
27694
+ this.dirty = true;
27695
+ this.dynamicMinWidth = 0;
27696
+
27697
+ // Force isEditing false to ensure clean state
27698
+ this.isEditing = false;
27699
+ console.log(' → Set initialized=true, dirty=true, cleared caches');
27700
+
27701
+ // Re-initialize dimensions (this will handle justify properly)
27702
+ this.initDimensions();
27703
+
27704
+ // Double-check that justify was applied by checking space widths
27705
+ if (this.textAlign.includes('justify') && this.__charBounds) {
27706
+ setTimeout(() => {
27707
+ var _this$canvas6;
27708
+ // Verify justify was applied by checking if space widths vary
27709
+ let hasVariableSpaces = false;
27710
+ this.__charBounds.forEach((lineBounds, i) => {
27711
+ if (lineBounds && this._textLines && this._textLines[i]) {
27712
+ const spaces = lineBounds.filter((bound, j) => /\s/.test(this._textLines[i][j]));
27713
+ if (spaces.length > 1) {
27714
+ const firstSpaceWidth = spaces[0].width;
27715
+ hasVariableSpaces = spaces.some(space => Math.abs(space.width - firstSpaceWidth) > 0.1);
27716
+ }
27717
+ }
27718
+ });
27719
+ if (!hasVariableSpaces && this.__charBounds.length > 0) {
27720
+ console.warn(' ⚠️ Justify spaces still uniform - forcing enlargeSpaces again');
27721
+ if (this.enlargeSpaces) {
27722
+ this.enlargeSpaces();
27723
+ }
27724
+ } else {
27725
+ console.log(' ✅ Justify spaces properly expanded');
27726
+ }
27727
+
27728
+ // Ensure height is recalculated - use browser height if available
27729
+ if (this._usingBrowserWrapping && this._actualBrowserHeight) {
27730
+ this.height = this._actualBrowserHeight;
27731
+ console.log(`🔤 JUSTIFY: Preserved browser height: ${this.height}px`);
27732
+ } else {
27733
+ this.height = this.calcTextHeight();
27734
+ console.log(`🔧 JUSTIFY: Used calcTextHeight: ${this.height}px`);
27735
+ }
27736
+ (_this$canvas6 = this.canvas) === null || _this$canvas6 === void 0 || _this$canvas6.requestRenderAll();
27737
+ }, 10);
27738
+ }
27739
+ }
27740
+
25815
27741
  /**
25816
27742
  * Returns object representation of an instance
25817
27743
  * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output