@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
package/dist/index.mjs CHANGED
@@ -354,7 +354,7 @@ class Cache {
354
354
  }
355
355
  const cache = new Cache();
356
356
 
357
- var version = "7.0.0-beta1";
357
+ var version = "7.0.1-beta9";
358
358
 
359
359
  // use this syntax so babel plugin see this import here
360
360
  const VERSION = version;
@@ -17571,33 +17571,30 @@ class PatternBrush extends PencilBrush {
17571
17571
  }
17572
17572
  }
17573
17573
 
17574
- // @TODO this code is terrible and Line should be a special case of polyline.
17575
-
17576
17574
  const coordProps = ['x1', 'x2', 'y1', 'y2'];
17577
- /**
17578
- * A Class to draw a line
17579
- * A bunch of methods will be added to Polyline to handle the line case
17580
- * The line class is very strange to work with, is all special, it hardly aligns
17581
- * to what a developer want everytime there is an angle
17582
- * @deprecated
17583
- */
17584
17575
  class Line extends FabricObject {
17585
- /**
17586
- * Constructor
17587
- * @param {Array} [points] Array of points
17588
- * @param {Object} [options] Options object
17589
- * @return {Line} thisArg
17590
- */
17591
17576
  constructor() {
17592
- let [x1, y1, x2, y2] = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0, 0, 0];
17577
+ let [x1, y1, x2, y2] = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0, 100, 0];
17593
17578
  let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
17594
17579
  super();
17595
- Object.assign(this, Line.ownDefaults);
17580
+ _defineProperty(this, "hitStrokeWidth", 'auto');
17581
+ _defineProperty(this, "_updatingEndpoints", false);
17582
+ _defineProperty(this, "_useEndpointCoords", true);
17583
+ _defineProperty(this, "_exportingSVG", false);
17596
17584
  this.setOptions(options);
17597
17585
  this.x1 = x1;
17598
17586
  this.x2 = x2;
17599
17587
  this.y1 = y1;
17600
17588
  this.y2 = y2;
17589
+ if (options.hitStrokeWidth !== undefined) {
17590
+ this.hitStrokeWidth = options.hitStrokeWidth;
17591
+ }
17592
+ this.hasBorders = false;
17593
+ this.hasControls = true;
17594
+ this.selectable = true;
17595
+ this.hoverCursor = 'move';
17596
+ this.perPixelTargetFind = false;
17597
+ this.strokeLineCap = 'butt';
17601
17598
  this._setWidthHeight();
17602
17599
  const {
17603
17600
  left,
@@ -17605,129 +17602,384 @@ class Line extends FabricObject {
17605
17602
  } = options;
17606
17603
  typeof left === 'number' && this.set(LEFT, left);
17607
17604
  typeof top === 'number' && this.set(TOP, top);
17605
+ this._setupLineControls();
17606
+ }
17607
+ _setupLineControls() {
17608
+ this.controls = {
17609
+ p1: new Control({
17610
+ x: 0,
17611
+ y: 0,
17612
+ cursorStyle: 'move',
17613
+ actionHandler: this._endpointActionHandler.bind(this),
17614
+ positionHandler: this._p1PositionHandler.bind(this),
17615
+ render: this._renderEndpointControl.bind(this),
17616
+ sizeX: 12,
17617
+ sizeY: 12
17618
+ }),
17619
+ p2: new Control({
17620
+ x: 0,
17621
+ y: 0,
17622
+ cursorStyle: 'move',
17623
+ actionHandler: this._endpointActionHandler.bind(this),
17624
+ positionHandler: this._p2PositionHandler.bind(this),
17625
+ render: this._renderEndpointControl.bind(this),
17626
+ sizeX: 12,
17627
+ sizeY: 12
17628
+ })
17629
+ };
17630
+ }
17631
+ _p1PositionHandler() {
17632
+ return new Point(this.x1, this.y1).transform(this.getViewportTransform());
17608
17633
  }
17634
+ _p2PositionHandler() {
17635
+ return new Point(this.x2, this.y2).transform(this.getViewportTransform());
17636
+ }
17637
+ _renderEndpointControl(ctx, left, top) {
17638
+ const size = 12;
17639
+ ctx.save();
17640
+ ctx.fillStyle = '#007bff';
17641
+ ctx.strokeStyle = '#ffffff';
17642
+ ctx.lineWidth = 2;
17643
+ ctx.beginPath();
17644
+ ctx.arc(left, top, size / 2, 0, 2 * Math.PI);
17645
+ ctx.fill();
17646
+ ctx.stroke();
17647
+ ctx.restore();
17648
+ }
17649
+ drawBorders(ctx) {
17650
+ let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
17651
+ if (this._useEndpointCoords) {
17652
+ this._drawLineBorders(ctx, styleOverride);
17653
+ return this;
17654
+ }
17655
+ return super.drawBorders(ctx, styleOverride, {});
17656
+ }
17657
+ _drawLineBorders(ctx) {
17658
+ var _this$canvas;
17659
+ let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
17660
+ const vpt = ((_this$canvas = this.canvas) === null || _this$canvas === void 0 ? void 0 : _this$canvas.viewportTransform) || [1, 0, 0, 1, 0, 0];
17661
+ ctx.save();
17662
+ ctx.setTransform(vpt[0], vpt[1], vpt[2], vpt[3], vpt[4], vpt[5]);
17663
+ ctx.strokeStyle = styleOverride.borderColor || this.borderColor || 'rgba(100, 200, 200, 0.5)';
17664
+ ctx.lineWidth = (this.strokeWidth || 1) + 5;
17665
+ ctx.lineCap = this.strokeLineCap || 'butt';
17666
+ ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
17667
+ ctx.beginPath();
17668
+ ctx.moveTo(this.x1, this.y1);
17669
+ ctx.lineTo(this.x2, this.y2);
17670
+ ctx.stroke();
17671
+ ctx.restore();
17672
+ }
17673
+ _renderControls(ctx) {
17674
+ let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
17675
+ ctx.save();
17676
+ ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
17677
+ this.drawControls(ctx, styleOverride);
17678
+ ctx.restore();
17679
+ }
17680
+ getBoundingRect() {
17681
+ if (this._useEndpointCoords) {
17682
+ const {
17683
+ x1,
17684
+ y1,
17685
+ x2,
17686
+ y2
17687
+ } = this;
17688
+ const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
17689
+ const padding = Math.max(effectiveStrokeWidth / 2 + 5, 10);
17690
+ return {
17691
+ left: Math.min(x1, x2) - padding,
17692
+ top: Math.min(y1, y2) - padding,
17693
+ width: Math.abs(x2 - x1) + padding * 2 || padding * 2,
17694
+ height: Math.abs(y2 - y1) + padding * 2 || padding * 2
17695
+ };
17696
+ }
17697
+ return super.getBoundingRect();
17698
+ }
17699
+ setCoords() {
17700
+ if (this._useEndpointCoords) {
17701
+ // Set width and height for hit detection and bounding box
17702
+ const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
17703
+ const hitPadding = Math.max(effectiveStrokeWidth / 2 + 5, 10);
17704
+ this.width = Math.abs(this.x2 - this.x1) + hitPadding * 2;
17705
+ this.height = Math.abs(this.y2 - this.y1) + hitPadding * 2;
17609
17706
 
17610
- /**
17611
- * @private
17612
- * @param {Object} [options] Options
17613
- */
17614
- _setWidthHeight() {
17615
- const {
17616
- x1,
17617
- y1,
17618
- x2,
17619
- y2
17620
- } = this;
17621
- this.width = Math.abs(x2 - x1);
17622
- this.height = Math.abs(y2 - y1);
17623
- const {
17624
- left,
17625
- top,
17626
- width,
17627
- height
17628
- } = makeBoundingBoxFromPoints([{
17629
- x: x1,
17630
- y: y1
17631
- }, {
17632
- x: x2,
17633
- y: y2
17634
- }]);
17635
- const position = new Point(left + width / 2, top + height / 2);
17636
- this.setPositionByOrigin(position, CENTER, CENTER);
17707
+ // Only update left/top if they haven't been explicitly set (e.g., during loading)
17708
+ if (this.left === 0 && this.top === 0) {
17709
+ const center = this._findCenterFromElement();
17710
+ this.left = center.x;
17711
+ this.top = center.y;
17712
+ }
17713
+ }
17714
+ super.setCoords();
17637
17715
  }
17716
+ getCoords() {
17717
+ if (this._useEndpointCoords) {
17718
+ const deltaX = this.x2 - this.x1;
17719
+ const deltaY = this.y2 - this.y1;
17720
+ const length = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
17721
+ if (length === 0) {
17722
+ return super.getCoords();
17723
+ }
17724
+ const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
17725
+ const halfWidth = Math.max(effectiveStrokeWidth / 2 + 2, 5);
17638
17726
 
17639
- /**
17640
- * @private
17641
- * @param {String} key
17642
- * @param {*} value
17643
- */
17727
+ // Unit vector perpendicular to line
17728
+ const perpX = -deltaY / length;
17729
+ const perpY = deltaX / length;
17730
+
17731
+ // Four corners of oriented rectangle
17732
+ 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)];
17733
+ }
17734
+ return super.getCoords();
17735
+ }
17736
+ containsPoint(point) {
17737
+ if (this._useEndpointCoords) {
17738
+ var _this$canvas2;
17739
+ if (((_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 ? void 0 : _this$canvas2.getActiveObject()) === this) {
17740
+ return super.containsPoint(point);
17741
+ }
17742
+ const distance = this._distanceToLineSegment(point.x, point.y);
17743
+ const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth || 1;
17744
+ const tolerance = Math.max(effectiveStrokeWidth / 2 + 2, 5);
17745
+ return distance <= tolerance;
17746
+ }
17747
+ return super.containsPoint(point);
17748
+ }
17749
+ _distanceToLineSegment(px, py) {
17750
+ const x1 = this.x1,
17751
+ y1 = this.y1,
17752
+ x2 = this.x2,
17753
+ y2 = this.y2;
17754
+ const pd2 = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
17755
+ if (pd2 === 0) {
17756
+ return Math.sqrt((px - x1) * (px - x1) + (py - y1) * (py - y1));
17757
+ }
17758
+ const u = ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / pd2;
17759
+ let closestX, closestY;
17760
+ if (u < 0) {
17761
+ closestX = x1;
17762
+ closestY = y1;
17763
+ } else if (u > 1) {
17764
+ closestX = x2;
17765
+ closestY = y2;
17766
+ } else {
17767
+ closestX = x1 + u * (x2 - x1);
17768
+ closestY = y1 + u * (y2 - y1);
17769
+ }
17770
+ return Math.sqrt((px - closestX) * (px - closestX) + (py - closestY) * (py - closestY));
17771
+ }
17772
+ _endpointActionHandler(eventData, transformData, x, y) {
17773
+ var _this$canvas4;
17774
+ const controlKey = transformData.corner;
17775
+ const pointer = new Point(x, y);
17776
+ let newX = pointer.x;
17777
+ let newY = pointer.y;
17778
+ if (eventData.shiftKey) {
17779
+ const otherControl = controlKey === 'p1' ? 'p2' : 'p1';
17780
+ const otherX = this[otherControl === 'p1' ? 'x1' : 'x2'];
17781
+ const otherY = this[otherControl === 'p1' ? 'y1' : 'y2'];
17782
+ const snapped = this._snapToAngle(otherX, otherY, newX, newY);
17783
+ newX = snapped.x;
17784
+ newY = snapped.y;
17785
+ }
17786
+ if (this._useEndpointCoords) {
17787
+ var _this$canvas3;
17788
+ if (controlKey === 'p1') {
17789
+ this.x1 = newX;
17790
+ this.y1 = newY;
17791
+ } else if (controlKey === 'p2') {
17792
+ this.x2 = newX;
17793
+ this.y2 = newY;
17794
+ }
17795
+
17796
+ // Update gradient coordinates if stroke is a gradient (but not during SVG export)
17797
+ if (this.stroke instanceof Gradient && !this._exportingSVG) {
17798
+ this.stroke.coords.x1 = this.x1;
17799
+ this.stroke.coords.y1 = this.y1;
17800
+ this.stroke.coords.x2 = this.x2;
17801
+ this.stroke.coords.y2 = this.y2;
17802
+ }
17803
+ this.dirty = true;
17804
+ this.setCoords();
17805
+ (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
17806
+ return true;
17807
+ }
17808
+
17809
+ // Fallback for old system
17810
+ this._updatingEndpoints = true;
17811
+ if (controlKey === 'p1') {
17812
+ this.x1 = newX;
17813
+ this.y1 = newY;
17814
+ } else if (controlKey === 'p2') {
17815
+ this.x2 = newX;
17816
+ this.y2 = newY;
17817
+ }
17818
+ this._setWidthHeight();
17819
+ this.dirty = true;
17820
+ this._updatingEndpoints = false;
17821
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.requestRenderAll();
17822
+ this.fire('modified', {
17823
+ transform: transformData,
17824
+ target: this,
17825
+ e: eventData
17826
+ });
17827
+ return true;
17828
+ }
17829
+ _snapToAngle(fromX, fromY, toX, toY) {
17830
+ const deltaX = toX - fromX;
17831
+ const deltaY = toY - fromY;
17832
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
17833
+ if (distance === 0) return {
17834
+ x: toX,
17835
+ y: toY
17836
+ };
17837
+ let angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
17838
+ const snapIncrement = 15;
17839
+ const snappedAngle = Math.round(angle / snapIncrement) * snapIncrement;
17840
+ const snappedRadians = snappedAngle * (Math.PI / 180);
17841
+ return {
17842
+ x: fromX + Math.cos(snappedRadians) * distance,
17843
+ y: fromY + Math.sin(snappedRadians) * distance
17844
+ };
17845
+ }
17846
+ _setWidthHeight() {
17847
+ let skipReposition = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
17848
+ this.width = Math.abs(this.x2 - this.x1) || 1;
17849
+ this.height = Math.abs(this.y2 - this.y1) || 1;
17850
+ if (!skipReposition && !this._updatingEndpoints) {
17851
+ const {
17852
+ left,
17853
+ top,
17854
+ width,
17855
+ height
17856
+ } = makeBoundingBoxFromPoints([{
17857
+ x: this.x1,
17858
+ y: this.y1
17859
+ }, {
17860
+ x: this.x2,
17861
+ y: this.y2
17862
+ }]);
17863
+ this.setPositionByOrigin(new Point(left + width / 2, top + height / 2), CENTER, CENTER);
17864
+ }
17865
+ }
17644
17866
  _set(key, value) {
17867
+ const oldLeft = this.left;
17868
+ const oldTop = this.top;
17645
17869
  super._set(key, value);
17646
17870
  if (coordProps.includes(key)) {
17647
- // this doesn't make sense very much, since setting x1 when top or left
17648
- // are already set, is just going to show a strange result since the
17649
- // line will move way more than the developer expect.
17650
- // in fabric5 it worked only when the line didn't have extra transformations,
17651
- // in fabric6 too. With extra transform they behave bad in different ways.
17652
- // This needs probably a good rework or a tutorial if you have to create a dynamic line
17653
17871
  this._setWidthHeight();
17872
+ this.dirty = true;
17873
+
17874
+ // Update gradient coordinates if stroke is a gradient (but not during SVG export)
17875
+ if (this.stroke instanceof Gradient && !this._exportingSVG) {
17876
+ this.stroke.coords.x1 = this.x1;
17877
+ this.stroke.coords.y1 = this.y1;
17878
+ this.stroke.coords.x2 = this.x2;
17879
+ this.stroke.coords.y2 = this.y2;
17880
+ }
17881
+ }
17882
+ if ((key === 'left' || key === 'top') && this.canvas && !this._updatingEndpoints) {
17883
+ const deltaX = this.left - oldLeft;
17884
+ const deltaY = this.top - oldTop;
17885
+ if (deltaX !== 0 || deltaY !== 0) {
17886
+ this._updatingEndpoints = true;
17887
+ this.x1 += deltaX;
17888
+ this.y1 += deltaY;
17889
+ this.x2 += deltaX;
17890
+ this.y2 += deltaY;
17891
+
17892
+ // Update gradient coordinates if stroke is a gradient
17893
+ if (this.stroke instanceof Gradient) {
17894
+ this.stroke.coords.x1 = this.x1;
17895
+ this.stroke.coords.y1 = this.y1;
17896
+ this.stroke.coords.x2 = this.x2;
17897
+ this.stroke.coords.y2 = this.y2;
17898
+ }
17899
+ this._updatingEndpoints = false;
17900
+ }
17654
17901
  }
17655
17902
  return this;
17656
17903
  }
17657
-
17658
- /**
17659
- * @private
17660
- * @param {CanvasRenderingContext2D} ctx Context to render on
17661
- */
17904
+ render(ctx) {
17905
+ if (this._useEndpointCoords) {
17906
+ this._renderDirectly(ctx);
17907
+ return;
17908
+ }
17909
+ super.render(ctx);
17910
+ }
17911
+ _renderDirectly(ctx) {
17912
+ if (!this.visible) return;
17913
+ ctx.save();
17914
+ ctx.globalAlpha = this.opacity;
17915
+ ctx.lineWidth = this.strokeWidth;
17916
+ ctx.lineCap = this.strokeLineCap || 'butt';
17917
+ ctx.beginPath();
17918
+ ctx.moveTo(this.x1, this.y1);
17919
+ ctx.lineTo(this.x2, this.y2);
17920
+ const origStrokeStyle = ctx.strokeStyle;
17921
+ if (isFiller(this.stroke)) {
17922
+ ctx.strokeStyle = this.stroke.toLive(ctx);
17923
+ } else {
17924
+ var _this$stroke;
17925
+ ctx.strokeStyle = ((_this$stroke = this.stroke) === null || _this$stroke === void 0 ? void 0 : _this$stroke.toString()) || '#000';
17926
+ }
17927
+ ctx.stroke();
17928
+ ctx.strokeStyle = origStrokeStyle;
17929
+ ctx.restore();
17930
+ }
17662
17931
  _render(ctx) {
17932
+ if (this._useEndpointCoords) return;
17663
17933
  ctx.beginPath();
17664
17934
  const p = this.calcLinePoints();
17665
17935
  ctx.moveTo(p.x1, p.y1);
17666
17936
  ctx.lineTo(p.x2, p.y2);
17667
17937
  ctx.lineWidth = this.strokeWidth;
17668
-
17669
- // TODO: test this
17670
- // make sure setting "fill" changes color of a line
17671
- // (by copying fillStyle to strokeStyle, since line is stroked, not filled)
17672
17938
  const origStrokeStyle = ctx.strokeStyle;
17673
17939
  if (isFiller(this.stroke)) {
17674
17940
  ctx.strokeStyle = this.stroke.toLive(ctx);
17675
- } else {
17676
- var _this$stroke;
17677
- ctx.strokeStyle = (_this$stroke = this.stroke) !== null && _this$stroke !== void 0 ? _this$stroke : ctx.fillStyle;
17678
17941
  }
17679
17942
  this.stroke && this._renderStroke(ctx);
17680
17943
  ctx.strokeStyle = origStrokeStyle;
17681
17944
  }
17682
-
17683
- /**
17684
- * This function is an helper for svg import. it returns the center of the object in the svg
17685
- * untransformed coordinates
17686
- * @private
17687
- * @return {Point} center point from element coordinates
17688
- */
17689
17945
  _findCenterFromElement() {
17690
17946
  return new Point((this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2);
17691
17947
  }
17692
-
17693
- /**
17694
- * Returns object representation of an instance
17695
- * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
17696
- * @return {Object} object representation of an instance
17697
- */
17698
17948
  toObject() {
17699
17949
  let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
17950
+ if (this._useEndpointCoords) {
17951
+ return {
17952
+ ...super.toObject(propertiesToInclude),
17953
+ x1: this.x1,
17954
+ y1: this.y1,
17955
+ x2: this.x2,
17956
+ y2: this.y2
17957
+ };
17958
+ }
17700
17959
  return {
17701
17960
  ...super.toObject(propertiesToInclude),
17702
17961
  ...this.calcLinePoints()
17703
17962
  };
17704
17963
  }
17705
-
17706
- /*
17707
- * Calculate object dimensions from its properties
17708
- * @private
17709
- */
17710
17964
  _getNonTransformedDimensions() {
17711
17965
  const dim = super._getNonTransformedDimensions();
17712
- if (this.strokeLineCap === 'butt') {
17713
- if (this.width === 0) {
17714
- dim.y -= this.strokeWidth;
17715
- }
17716
- if (this.height === 0) {
17717
- dim.x -= this.strokeWidth;
17718
- }
17966
+ if (this.strokeLineCap === 'round') {
17967
+ dim.x += this.strokeWidth;
17968
+ dim.y += this.strokeWidth;
17719
17969
  }
17720
17970
  return dim;
17721
17971
  }
17722
-
17723
- /**
17724
- * Recalculates line points given width and height
17725
- * Those points are simply placed around the center,
17726
- * This is not useful outside internal render functions and svg output
17727
- * Is not meant to be for the developer.
17728
- * @private
17729
- */
17730
17972
  calcLinePoints() {
17973
+ if (this._updatingEndpoints) {
17974
+ const centerX = (this.x1 + this.x2) / 2;
17975
+ const centerY = (this.y1 + this.y2) / 2;
17976
+ return {
17977
+ x1: this.x1 - centerX,
17978
+ y1: this.y1 - centerY,
17979
+ x2: this.x2 - centerX,
17980
+ y2: this.y2 - centerY
17981
+ };
17982
+ }
17731
17983
  const {
17732
17984
  x1: _x1,
17733
17985
  x2: _x2,
@@ -17736,48 +17988,64 @@ class Line extends FabricObject {
17736
17988
  width,
17737
17989
  height
17738
17990
  } = this;
17739
- const xMult = _x1 <= _x2 ? -1 : 1,
17740
- yMult = _y1 <= _y2 ? -1 : 1,
17741
- x1 = xMult * width / 2,
17742
- y1 = yMult * height / 2,
17743
- x2 = xMult * -width / 2,
17744
- y2 = yMult * -height / 2;
17991
+ const xMult = _x1 <= _x2 ? -1 : 1;
17992
+ const yMult = _y1 <= _y2 ? -1 : 1;
17745
17993
  return {
17746
- x1,
17747
- x2,
17748
- y1,
17749
- y2
17994
+ x1: xMult * width / 2,
17995
+ y1: yMult * height / 2,
17996
+ x2: xMult * -width / 2,
17997
+ y2: yMult * -height / 2
17750
17998
  };
17751
17999
  }
17752
-
17753
- /* _FROM_SVG_START_ */
17754
-
17755
- /**
17756
- * Returns svg representation of an instance
17757
- * @return {Array} an array of strings with the specific svg representation
17758
- * of the instance
17759
- */
17760
18000
  _toSVG() {
17761
- const {
17762
- x1,
17763
- x2,
17764
- y1,
17765
- y2
17766
- } = this.calcLinePoints();
17767
- return ['<line ', 'COMMON_PARTS', `x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" />\n`];
18001
+ if (this._useEndpointCoords) {
18002
+ // Use absolute coordinates to bypass all Fabric.js transforms
18003
+ // Handle gradients manually for proper SVG export
18004
+ let strokeAttr = '';
18005
+ if (this.stroke instanceof Gradient) {
18006
+ // Let Fabric.js handle gradient definition, but we'll use the reference
18007
+ strokeAttr = `stroke="url(#${this.stroke.id})"`;
18008
+ } else {
18009
+ strokeAttr = `stroke="${this.stroke || 'none'}"`;
18010
+ }
18011
+ 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`];
18012
+ } else {
18013
+ // Use standard calcLinePoints for legacy mode
18014
+ const {
18015
+ x1,
18016
+ x2,
18017
+ y1,
18018
+ y2
18019
+ } = this.calcLinePoints();
18020
+ return ['<line ', 'COMMON_PARTS', `x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" />\n`];
18021
+ }
17768
18022
  }
18023
+ toSVG(reviver) {
18024
+ if (this._useEndpointCoords) {
18025
+ // For endpoint coords, we need to bypass transforms but still allow gradients
18026
+ // Let's temporarily disable transforms during SVG generation
18027
+ const originalLeft = this.left;
18028
+ const originalTop = this.top;
17769
18029
 
17770
- /**
17771
- * List of attribute names to account for when parsing SVG element (used by {@link Line.fromElement})
17772
- * @see http://www.w3.org/TR/SVG/shapes.html#LineElement
17773
- */
18030
+ // Set position to center of line for gradient calculation
18031
+ this.left = (this.x1 + this.x2) / 2;
18032
+ this.top = (this.y1 + this.y2) / 2;
17774
18033
 
17775
- /**
17776
- * Returns Line instance from an SVG element
17777
- * @param {HTMLElement} element Element to parse
17778
- * @param {Object} [options] Options object
17779
- * @param {Function} [callback] callback function invoked after parsing
17780
- */
18034
+ // Get the SVG with standard system (for gradient handling)
18035
+ const standardSVG = super.toSVG(reviver);
18036
+
18037
+ // Restore original position
18038
+ this.left = originalLeft;
18039
+ this.top = originalTop;
18040
+
18041
+ // Extract gradient definition and clean up the line element
18042
+ // Remove the transform wrapper and update coordinates
18043
+ 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}"`);
18044
+ return cleanSVG;
18045
+ }
18046
+ // Use default behavior for legacy mode
18047
+ return super.toSVG(reviver);
18048
+ }
17781
18049
  static async fromElement(element, options, cssRules) {
17782
18050
  const {
17783
18051
  x1 = 0,
@@ -17788,14 +18056,6 @@ class Line extends FabricObject {
17788
18056
  } = parseAttributes(element, this.ATTRIBUTE_NAMES, cssRules);
17789
18057
  return new this([x1, y1, x2, y2], parsedAttributes);
17790
18058
  }
17791
-
17792
- /* _FROM_SVG_END_ */
17793
-
17794
- /**
17795
- * Returns Line instance from an object representation
17796
- * @param {Object} object Object to create an instance from
17797
- * @returns {Promise<Line>}
17798
- */
17799
18059
  static fromObject(_ref) {
17800
18060
  let {
17801
18061
  x1,
@@ -17812,32 +18072,195 @@ class Line extends FabricObject {
17812
18072
  });
17813
18073
  }
17814
18074
  }
18075
+ _defineProperty(Line, "type", 'Line');
18076
+ _defineProperty(Line, "cacheProperties", [...cacheProperties, ...coordProps]);
18077
+ _defineProperty(Line, "ATTRIBUTE_NAMES", SHARED_ATTRIBUTES.concat(coordProps));
18078
+ classRegistry.setClass(Line);
18079
+ classRegistry.setSVGClass(Line);
18080
+
17815
18081
  /**
17816
- * x value or first line edge
17817
- * @type number
18082
+ * Calculate the distance between two points
17818
18083
  */
18084
+ function pointDistance(p1, p2) {
18085
+ return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
18086
+ }
18087
+
17819
18088
  /**
17820
- * y value or first line edge
17821
- * @type number
18089
+ * Normalize a vector
17822
18090
  */
18091
+ function normalizeVector(vector) {
18092
+ const length = Math.sqrt(vector.x * vector.x + vector.y * vector.y);
18093
+ if (length === 0) return {
18094
+ x: 0,
18095
+ y: 0
18096
+ };
18097
+ return {
18098
+ x: vector.x / length,
18099
+ y: vector.y / length
18100
+ };
18101
+ }
18102
+
17823
18103
  /**
17824
- * x value or second line edge
17825
- * @type number
18104
+ * Get the maximum allowed radius for a corner based on adjacent edge lengths
17826
18105
  */
18106
+ function getMaxRadius(prevPoint, currentPoint, nextPoint) {
18107
+ const dist1 = pointDistance(prevPoint, currentPoint);
18108
+ const dist2 = pointDistance(currentPoint, nextPoint);
18109
+ return Math.min(dist1, dist2) / 2;
18110
+ }
18111
+
17827
18112
  /**
17828
- * y value or second line edge
17829
- * @type number
18113
+ * Calculate rounded corner data for a single corner
17830
18114
  */
17831
- _defineProperty(Line, "type", 'Line');
17832
- _defineProperty(Line, "cacheProperties", [...cacheProperties, ...coordProps]);
17833
- _defineProperty(Line, "ATTRIBUTE_NAMES", SHARED_ATTRIBUTES.concat(coordProps));
17834
- classRegistry.setClass(Line);
17835
- classRegistry.setSVGClass(Line);
18115
+ function calculateRoundedCorner(prevPoint, currentPoint, nextPoint, radius) {
18116
+ // Calculate edge vectors
18117
+ const edge1 = {
18118
+ x: currentPoint.x - prevPoint.x,
18119
+ y: currentPoint.y - prevPoint.y
18120
+ };
18121
+ const edge2 = {
18122
+ x: nextPoint.x - currentPoint.x,
18123
+ y: nextPoint.y - currentPoint.y
18124
+ };
18125
+
18126
+ // Normalize edge vectors
18127
+ const norm1 = normalizeVector(edge1);
18128
+ const norm2 = normalizeVector(edge2);
18129
+
18130
+ // Calculate the maximum allowed radius
18131
+ const maxRadius = getMaxRadius(prevPoint, currentPoint, nextPoint);
18132
+ const actualRadius = Math.min(radius, maxRadius);
18133
+
18134
+ // Calculate start and end points of the rounded corner
18135
+ const startPoint = {
18136
+ x: currentPoint.x - norm1.x * actualRadius,
18137
+ y: currentPoint.y - norm1.y * actualRadius
18138
+ };
18139
+ const endPoint = {
18140
+ x: currentPoint.x + norm2.x * actualRadius,
18141
+ y: currentPoint.y + norm2.y * actualRadius
18142
+ };
18143
+
18144
+ // Calculate control points for bezier curve
18145
+ // Using the magic number kRect for optimal circular approximation
18146
+ const controlOffset = actualRadius * kRect;
18147
+ const cp1 = {
18148
+ x: startPoint.x + norm1.x * controlOffset,
18149
+ y: startPoint.y + norm1.y * controlOffset
18150
+ };
18151
+ const cp2 = {
18152
+ x: endPoint.x - norm2.x * controlOffset,
18153
+ y: endPoint.y - norm2.y * controlOffset
18154
+ };
18155
+ return {
18156
+ corner: currentPoint,
18157
+ start: startPoint,
18158
+ end: endPoint,
18159
+ cp1,
18160
+ cp2,
18161
+ actualRadius
18162
+ };
18163
+ }
18164
+
18165
+ /**
18166
+ * Apply corner radius to a polygon defined by points
18167
+ */
18168
+ function applyCornerRadiusToPolygon(points, radius) {
18169
+ let radiusAsPercentage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
18170
+ if (points.length < 3) {
18171
+ throw new Error('Polygon must have at least 3 points');
18172
+ }
18173
+
18174
+ // Calculate bounding box if radius is percentage-based
18175
+ let actualRadius = radius;
18176
+ if (radiusAsPercentage) {
18177
+ const minX = Math.min(...points.map(p => p.x));
18178
+ const maxX = Math.max(...points.map(p => p.x));
18179
+ const minY = Math.min(...points.map(p => p.y));
18180
+ const maxY = Math.max(...points.map(p => p.y));
18181
+ const width = maxX - minX;
18182
+ const height = maxY - minY;
18183
+ const minDimension = Math.min(width, height);
18184
+ actualRadius = radius / 100 * minDimension;
18185
+ }
18186
+ const roundedCorners = [];
18187
+ for (let i = 0; i < points.length; i++) {
18188
+ const prevIndex = (i - 1 + points.length) % points.length;
18189
+ const nextIndex = (i + 1) % points.length;
18190
+ const prevPoint = points[prevIndex];
18191
+ const currentPoint = points[i];
18192
+ const nextPoint = points[nextIndex];
18193
+ const roundedCorner = calculateRoundedCorner(prevPoint, currentPoint, nextPoint, actualRadius);
18194
+ roundedCorners.push(roundedCorner);
18195
+ }
18196
+ return roundedCorners;
18197
+ }
18198
+
18199
+ /**
18200
+ * Render a rounded polygon to a canvas context
18201
+ */
18202
+ function renderRoundedPolygon(ctx, roundedCorners) {
18203
+ let closed = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
18204
+ if (roundedCorners.length === 0) return;
18205
+ ctx.beginPath();
18206
+
18207
+ // Start at the first corner's start point
18208
+ const firstCorner = roundedCorners[0];
18209
+ ctx.moveTo(firstCorner.start.x, firstCorner.start.y);
18210
+ for (let i = 0; i < roundedCorners.length; i++) {
18211
+ const corner = roundedCorners[i];
18212
+ const nextIndex = (i + 1) % roundedCorners.length;
18213
+ const nextCorner = roundedCorners[nextIndex];
18214
+
18215
+ // Draw the rounded corner using bezier curve
18216
+ ctx.bezierCurveTo(corner.cp1.x, corner.cp1.y, corner.cp2.x, corner.cp2.y, corner.end.x, corner.end.y);
18217
+
18218
+ // Draw line to next corner's start point (if not the last segment in open path)
18219
+ if (i < roundedCorners.length - 1 || closed) {
18220
+ ctx.lineTo(nextCorner.start.x, nextCorner.start.y);
18221
+ }
18222
+ }
18223
+ if (closed) {
18224
+ ctx.closePath();
18225
+ }
18226
+ }
18227
+
18228
+ /**
18229
+ * Generate SVG path data for a rounded polygon
18230
+ */
18231
+ function generateRoundedPolygonPath(roundedCorners) {
18232
+ let closed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
18233
+ if (roundedCorners.length === 0) return '';
18234
+ const pathData = [];
18235
+ const firstCorner = roundedCorners[0];
18236
+
18237
+ // Move to first corner's start point
18238
+ pathData.push(`M ${firstCorner.start.x} ${firstCorner.start.y}`);
18239
+ for (let i = 0; i < roundedCorners.length; i++) {
18240
+ const corner = roundedCorners[i];
18241
+ const nextIndex = (i + 1) % roundedCorners.length;
18242
+ const nextCorner = roundedCorners[nextIndex];
18243
+
18244
+ // Add bezier curve for the rounded corner
18245
+ pathData.push(`C ${corner.cp1.x} ${corner.cp1.y} ${corner.cp2.x} ${corner.cp2.y} ${corner.end.x} ${corner.end.y}`);
18246
+
18247
+ // Add line to next corner's start point (if not the last segment in open path)
18248
+ if (i < roundedCorners.length - 1 || closed) {
18249
+ pathData.push(`L ${nextCorner.start.x} ${nextCorner.start.y}`);
18250
+ }
18251
+ }
18252
+ if (closed) {
18253
+ pathData.push('Z');
18254
+ }
18255
+ return pathData.join(' ');
18256
+ }
17836
18257
 
17837
18258
  const triangleDefaultValues = {
17838
18259
  width: 100,
17839
- height: 100
18260
+ height: 100,
18261
+ cornerRadius: 0
17840
18262
  };
18263
+ const TRIANGLE_PROPS = ['cornerRadius'];
17841
18264
  class Triangle extends FabricObject {
17842
18265
  static getDefaults() {
17843
18266
  return {
@@ -17856,34 +18279,90 @@ class Triangle extends FabricObject {
17856
18279
  this.setOptions(options);
17857
18280
  }
17858
18281
 
18282
+ /**
18283
+ * Get triangle points as an array of XY coordinates
18284
+ * @private
18285
+ */
18286
+ _getTrianglePoints() {
18287
+ const widthBy2 = this.width / 2;
18288
+ const heightBy2 = this.height / 2;
18289
+ return [{
18290
+ x: -widthBy2,
18291
+ y: heightBy2
18292
+ },
18293
+ // bottom left
18294
+ {
18295
+ x: 0,
18296
+ y: -heightBy2
18297
+ },
18298
+ // top center
18299
+ {
18300
+ x: widthBy2,
18301
+ y: heightBy2
18302
+ } // bottom right
18303
+ ];
18304
+ }
18305
+
17859
18306
  /**
17860
18307
  * @private
17861
18308
  * @param {CanvasRenderingContext2D} ctx Context to render on
17862
18309
  */
17863
18310
  _render(ctx) {
17864
- const widthBy2 = this.width / 2,
17865
- heightBy2 = this.height / 2;
17866
- ctx.beginPath();
17867
- ctx.moveTo(-widthBy2, heightBy2);
17868
- ctx.lineTo(0, -heightBy2);
17869
- ctx.lineTo(widthBy2, heightBy2);
17870
- ctx.closePath();
18311
+ if (this.cornerRadius > 0) {
18312
+ // Render rounded triangle
18313
+ const points = this._getTrianglePoints();
18314
+ const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
18315
+ renderRoundedPolygon(ctx, roundedCorners, true);
18316
+ } else {
18317
+ // Render sharp triangle (original implementation)
18318
+ const widthBy2 = this.width / 2;
18319
+ const heightBy2 = this.height / 2;
18320
+ ctx.beginPath();
18321
+ ctx.moveTo(-widthBy2, heightBy2);
18322
+ ctx.lineTo(0, -heightBy2);
18323
+ ctx.lineTo(widthBy2, heightBy2);
18324
+ ctx.closePath();
18325
+ }
17871
18326
  this._renderPaintInOrder(ctx);
17872
18327
  }
17873
18328
 
18329
+ /**
18330
+ * Returns object representation of an instance
18331
+ * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
18332
+ * @return {Object} object representation of an instance
18333
+ */
18334
+ toObject() {
18335
+ let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
18336
+ return super.toObject([...TRIANGLE_PROPS, ...propertiesToInclude]);
18337
+ }
18338
+
17874
18339
  /**
17875
18340
  * Returns svg representation of an instance
17876
18341
  * @return {Array} an array of strings with the specific svg representation
17877
18342
  * of the instance
17878
18343
  */
17879
18344
  _toSVG() {
17880
- const widthBy2 = this.width / 2,
17881
- heightBy2 = this.height / 2,
17882
- points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
17883
- return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
18345
+ if (this.cornerRadius > 0) {
18346
+ // Generate rounded triangle as path
18347
+ const points = this._getTrianglePoints();
18348
+ const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
18349
+ const pathData = generateRoundedPolygonPath(roundedCorners, true);
18350
+ return ['<path ', 'COMMON_PARTS', `d="${pathData}" />`];
18351
+ } else {
18352
+ // Original sharp triangle implementation
18353
+ const widthBy2 = this.width / 2;
18354
+ const heightBy2 = this.height / 2;
18355
+ const points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
18356
+ return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
18357
+ }
17884
18358
  }
17885
18359
  }
18360
+ /**
18361
+ * Corner radius for rounded triangle corners
18362
+ * @type Number
18363
+ */
17886
18364
  _defineProperty(Triangle, "type", 'Triangle');
18365
+ _defineProperty(Triangle, "cacheProperties", [...cacheProperties, ...TRIANGLE_PROPS]);
17887
18366
  _defineProperty(Triangle, "ownDefaults", triangleDefaultValues);
17888
18367
  classRegistry.setClass(Triangle);
17889
18368
  classRegistry.setSVGClass(Triangle);
@@ -18048,7 +18527,8 @@ const polylineDefaultValues = {
18048
18527
  /**
18049
18528
  * @deprecated transient option soon to be removed in favor of a different design
18050
18529
  */
18051
- exactBoundingBox: false
18530
+ exactBoundingBox: false,
18531
+ cornerRadius: 0
18052
18532
  };
18053
18533
  class Polyline extends FabricObject {
18054
18534
  static getDefaults() {
@@ -18262,7 +18742,7 @@ class Polyline extends FabricObject {
18262
18742
  toObject() {
18263
18743
  let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
18264
18744
  return {
18265
- ...super.toObject(propertiesToInclude),
18745
+ ...super.toObject(['cornerRadius', ...propertiesToInclude]),
18266
18746
  points: this.points.map(_ref => {
18267
18747
  let {
18268
18748
  x,
@@ -18282,14 +18762,28 @@ class Polyline extends FabricObject {
18282
18762
  * of the instance
18283
18763
  */
18284
18764
  _toSVG() {
18285
- const points = [],
18286
- diffX = this.pathOffset.x,
18287
- diffY = this.pathOffset.y,
18288
- NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS;
18289
- for (let i = 0, len = this.points.length; i < len; i++) {
18290
- points.push(toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ');
18765
+ if (this.cornerRadius > 0 && this.points.length >= 3) {
18766
+ // Generate rounded polygon/polyline as path
18767
+ const diffX = this.pathOffset.x;
18768
+ const diffY = this.pathOffset.y;
18769
+ const adjustedPoints = this.points.map(point => ({
18770
+ x: point.x - diffX,
18771
+ y: point.y - diffY
18772
+ }));
18773
+ const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
18774
+ const pathData = generateRoundedPolygonPath(roundedCorners, !this.isOpen());
18775
+ return ['<path ', 'COMMON_PARTS', `d="${pathData}" />\n`];
18776
+ } else {
18777
+ // Original sharp corners implementation
18778
+ const points = [];
18779
+ const diffX = this.pathOffset.x;
18780
+ const diffY = this.pathOffset.y;
18781
+ const NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS;
18782
+ for (let i = 0, len = this.points.length; i < len; i++) {
18783
+ points.push(toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ');
18784
+ }
18785
+ return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
18291
18786
  }
18292
- return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
18293
18787
  }
18294
18788
 
18295
18789
  /**
@@ -18305,13 +18799,24 @@ class Polyline extends FabricObject {
18305
18799
  // NaN comes from parseFloat of a empty string in parser
18306
18800
  return;
18307
18801
  }
18308
- ctx.beginPath();
18309
- ctx.moveTo(this.points[0].x - x, this.points[0].y - y);
18310
- for (let i = 0; i < len; i++) {
18311
- const point = this.points[i];
18312
- ctx.lineTo(point.x - x, point.y - y);
18802
+ if (this.cornerRadius > 0 && len >= 3) {
18803
+ // Render with rounded corners
18804
+ const adjustedPoints = this.points.map(point => ({
18805
+ x: point.x - x,
18806
+ y: point.y - y
18807
+ }));
18808
+ const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
18809
+ renderRoundedPolygon(ctx, roundedCorners, !this.isOpen());
18810
+ } else {
18811
+ // Original sharp corners implementation
18812
+ ctx.beginPath();
18813
+ ctx.moveTo(this.points[0].x - x, this.points[0].y - y);
18814
+ for (let i = 0; i < len; i++) {
18815
+ const point = this.points[i];
18816
+ ctx.lineTo(point.x - x, point.y - y);
18817
+ }
18818
+ !this.isOpen() && ctx.closePath();
18313
18819
  }
18314
- !this.isOpen() && ctx.closePath();
18315
18820
  this._renderPaintInOrder(ctx);
18316
18821
  }
18317
18822
 
@@ -18376,10 +18881,15 @@ class Polyline extends FabricObject {
18376
18881
  * @type Boolean
18377
18882
  * @default false
18378
18883
  */
18884
+ /**
18885
+ * Corner radius for rounded corners
18886
+ * @type Number
18887
+ * @default 0
18888
+ */
18379
18889
  _defineProperty(Polyline, "ownDefaults", polylineDefaultValues);
18380
18890
  _defineProperty(Polyline, "type", 'Polyline');
18381
18891
  _defineProperty(Polyline, "layoutProperties", [SKEW_X, SKEW_Y, 'strokeLineCap', 'strokeLineJoin', 'strokeMiterLimit', 'strokeWidth', 'strokeUniform', 'points']);
18382
- _defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points']);
18892
+ _defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points', 'cornerRadius']);
18383
18893
  _defineProperty(Polyline, "ATTRIBUTE_NAMES", [...SHARED_ATTRIBUTES]);
18384
18894
  classRegistry.setClass(Polyline);
18385
18895
  classRegistry.setSVGClass(Polyline);
@@ -18763,6 +19273,97 @@ function measureGraphemeWithKerning(grapheme, previousGrapheme, options, ctx) {
18763
19273
  };
18764
19274
  }
18765
19275
 
19276
+ /**
19277
+ * Get a representative character for font metrics measurement
19278
+ * Uses canvas to test which scripts the font actually supports
19279
+ */
19280
+ function getRepresentativeCharacter(fontFamily) {
19281
+ const context = getMeasurementContext();
19282
+
19283
+ // Wait for font to be ready if possible
19284
+ if (typeof document !== 'undefined' && 'fonts' in document) {
19285
+ try {
19286
+ // Check if font is ready, if not, use fallback immediately
19287
+ if (!document.fonts.check(`16px ${fontFamily}`)) {
19288
+ return 'M'; // Use safe fallback while font loads
19289
+ }
19290
+ } catch (e) {
19291
+ // Font check failed, use fallback
19292
+ return 'M';
19293
+ }
19294
+ }
19295
+
19296
+ // Test characters for different scripts
19297
+ const testChars = [{
19298
+ char: 'م',
19299
+ script: 'Arabic'
19300
+ },
19301
+ // Arabic
19302
+ {
19303
+ char: 'א',
19304
+ script: 'Hebrew'
19305
+ },
19306
+ // Hebrew
19307
+ {
19308
+ char: 'अ',
19309
+ script: 'Devanagari'
19310
+ },
19311
+ // Hindi/Sanskrit
19312
+ {
19313
+ char: 'ا',
19314
+ script: 'Urdu'
19315
+ },
19316
+ // Urdu
19317
+ {
19318
+ char: 'ک',
19319
+ script: 'Persian'
19320
+ },
19321
+ // Persian
19322
+ {
19323
+ char: 'த',
19324
+ script: 'Tamil'
19325
+ },
19326
+ // Tamil
19327
+ {
19328
+ char: 'ก',
19329
+ script: 'Thai'
19330
+ },
19331
+ // Thai
19332
+ {
19333
+ char: 'М',
19334
+ script: 'Cyrillic'
19335
+ },
19336
+ // Cyrillic
19337
+ {
19338
+ char: 'Ω',
19339
+ script: 'Greek'
19340
+ },
19341
+ // Greek
19342
+ {
19343
+ char: 'M',
19344
+ script: 'Latin'
19345
+ } // Latin (fallback)
19346
+ ];
19347
+
19348
+ // Set the font
19349
+ context.font = `16px ${fontFamily}`;
19350
+
19351
+ // Test each character to see which ones render properly
19352
+ // Use a more robust width check to avoid false positives
19353
+ const fallbackWidth = context.measureText('M').width;
19354
+ for (const test of testChars) {
19355
+ const metrics = context.measureText(test.char);
19356
+
19357
+ // Character is valid if it has width and isn't just a fallback glyph
19358
+ if (metrics.width > 0 && Math.abs(metrics.width - fallbackWidth) > 0.1) {
19359
+ return test.char;
19360
+ }
19361
+ }
19362
+
19363
+ // Fallback to Latin 'M'
19364
+ return 'M';
19365
+ }
19366
+
18766
19367
  /**
18767
19368
  * Get font metrics for layout calculations
18768
19369
  */
@@ -18776,8 +19377,9 @@ function getFontMetrics(options) {
18776
19377
  const context = getMeasurementContext();
18777
19378
  applyFontStyle(context, options);
18778
19379
 
18779
- // Use 'M' as sample character for metrics
18780
- const metrics = context.measureText('M');
19380
+ // Use representative character based on font's primary script
19381
+ const sample = getRepresentativeCharacter(options.fontFamily);
19382
+ const metrics = context.measureText(sample);
18781
19383
  const fontSize = options.fontSize;
18782
19384
 
18783
19385
  // Calculate metrics with fallbacks
@@ -18829,7 +19431,11 @@ function getFontDeclaration(options) {
18829
19431
  } = options;
18830
19432
 
18831
19433
  // Normalize font family (add quotes if needed)
18832
- const normalizedFamily = fontFamily.includes(' ') && !fontFamily.includes('"') && !fontFamily.includes("'") ? `"${fontFamily}"` : fontFamily;
19434
+ let normalizedFamily = fontFamily.includes(' ') && !fontFamily.includes('"') && !fontFamily.includes("'") ? `"${fontFamily}"` : fontFamily;
19435
+
19436
+ // Note: Font fallbacks are handled in the rendering phase only
19437
+ // to avoid affecting measurement calculations for text wrapping
19438
+
18833
19439
  return `${fontStyle} ${fontWeight} ${fontSize}px ${normalizedFamily}`;
18834
19440
  }
18835
19441
 
@@ -18981,6 +19587,81 @@ const measurementCache = new MeasurementCache();
18981
19587
  const kerningCache = new KerningCache();
18982
19588
  const fontMetricsCache = new FontMetricsCache();
18983
19589
 
19590
+ // Set up font loading listener to clear caches when fonts change
19591
+ if (typeof document !== 'undefined' && 'fonts' in document) {
19592
+ document.fonts.addEventListener('loadingdone', () => {
19593
+ // Clear all caches when fonts finish loading
19594
+ clearAllCaches();
19595
+ });
19596
+ }
19597
+
19598
+ /**
19599
+ * Clear all measurement caches
19600
+ */
19601
+ function clearAllCaches() {
19602
+ measurementCache.clear();
19603
+ kerningCache.clear();
19604
+ fontMetricsCache.clear();
19605
+ }
19606
+
19607
+ /**
19608
+ * Detect if a font lacks English glyph support
19609
+ * These fonts should use browser-native measurement instead of Fabric's character-by-character measurement
19610
+ */
19611
+ function fontLacksEnglishGlyphs(fontFamily) {
19612
+ if (typeof document === 'undefined') return false;
19613
+
19614
+ // Known fonts that lack English glyphs
19615
+ const knownNonEnglishFonts = ['stv', 'arabic', 'naskh', 'thuluth', 'kufi', 'diwani', 'nastaliq', 'kufic', 'hijazi', 'madinah', 'makkah'];
19616
+ const lowerFontFamily = fontFamily.toLowerCase();
19617
+
19618
+ // Check known list first
19619
+ if (knownNonEnglishFonts.some(font => lowerFontFamily.includes(font))) {
19620
+ return true;
19621
+ }
19622
+
19623
+ // Dynamic glyph support detection
19624
+ const context = getMeasurementContext();
19625
+ context.font = `16px ${fontFamily}`;
19626
+
19627
+ // Test English characters
19628
+ const englishChars = ['A', 'B', 'C', 'a', 'b', 'c', 'M', 'W'];
19629
+ const fallbackFont = 'Arial, sans-serif';
19630
+
19631
+ // Measure with target font
19632
+ const targetWidths = englishChars.map(char => context.measureText(char).width);
19633
+
19634
+ // Measure with fallback font
19635
+ context.font = `16px ${fallbackFont}`;
19636
+ const fallbackWidths = englishChars.map(char => context.measureText(char).width);
19637
+
19638
+ // If most measurements are identical, the font likely doesn't have English glyphs
19639
+ let identicalCount = 0;
19640
+ for (let i = 0; i < englishChars.length; i++) {
19641
+ if (Math.abs(targetWidths[i] - fallbackWidths[i]) < 0.5) {
19642
+ identicalCount++;
19643
+ }
19644
+ }
19645
+ const lacksSupportThreshold = englishChars.length * 0.7; // 70% identical = lacks support
19646
+ const lacksSupport = identicalCount >= lacksSupportThreshold;
19647
+ return lacksSupport;
19648
+ }
19649
+
19650
+ // Cache for font glyph detection results
19651
+ const fontGlyphCache = new Map();
19652
+
19653
+ /**
19654
+ * Cached version of font glyph detection
19655
+ */
19656
+ function fontLacksEnglishGlyphsCached(fontFamily) {
19657
+ if (fontGlyphCache.has(fontFamily)) {
19658
+ return fontGlyphCache.get(fontFamily);
19659
+ }
19660
+ const result = fontLacksEnglishGlyphs(fontFamily);
19661
+ fontGlyphCache.set(fontFamily, result);
19662
+ return result;
19663
+ }
19664
+
18984
19665
  /**
18985
19666
  * Unicode and Internationalization Support
18986
19667
  *
@@ -20164,6 +20845,15 @@ class FabricText extends StyledText {
20164
20845
  * Does not return dimensions.
20165
20846
  */
20166
20847
  initDimensions() {
20848
+ // Check if font is ready for accurate measurements
20849
+ // Only block initialization if it's a critical font loading situation
20850
+ const fontReady = this._isFontReady();
20851
+ if (!fontReady && !this.initialized) {
20852
+ // Only schedule font loading on first initialization
20853
+ this._scheduleInitAfterFontLoad();
20854
+ // Continue with fallback measurements for now
20855
+ }
20856
+
20167
20857
  // Use advanced layout if enabled
20168
20858
  if (this.enableAdvancedLayout && !this.path) {
20169
20859
  return this.initDimensionsAdvanced();
@@ -20180,7 +20870,21 @@ class FabricText extends StyledText {
20180
20870
  }
20181
20871
  if (this.textAlign.includes(JUSTIFY)) {
20182
20872
  // once text is measured we need to make space fatter to make justified text.
20183
- this.enlargeSpaces();
20873
+ // Ensure __charBounds exists before calling enlargeSpaces
20874
+ if (this.__charBounds && this.__charBounds.length > 0) {
20875
+ this.enlargeSpaces();
20876
+ } else {
20877
+ console.warn('⚠️ __charBounds not ready for justify alignment, deferring enlargeSpaces');
20878
+ // Defer the justify calculation until the next frame
20879
+ setTimeout(() => {
20880
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
20881
+ var _this$canvas;
20882
+ console.log('🔧 Applying deferred justify alignment');
20883
+ this.enlargeSpaces();
20884
+ (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
20885
+ }
20886
+ }, 0);
20887
+ }
20184
20888
  }
20185
20889
  }
20186
20890
 
@@ -20189,8 +20893,9 @@ class FabricText extends StyledText {
20189
20893
  */
20190
20894
  enlargeSpaces() {
20191
20895
  let diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces;
20896
+ const isRtl = this.direction === 'rtl';
20192
20897
  for (let i = 0, len = this._textLines.length; i < len; i++) {
20193
- if (this.textAlign !== JUSTIFY && (i === len - 1 || this.isEndOfWrapping(i))) {
20898
+ if (!this.textAlign.includes('justify') && (i === len - 1 || this.isEndOfWrapping(i))) {
20194
20899
  continue;
20195
20900
  }
20196
20901
  accumulatedSpace = 0;
@@ -20199,15 +20904,47 @@ class FabricText extends StyledText {
20199
20904
  if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) {
20200
20905
  numberOfSpaces = spaces.length;
20201
20906
  diffSpace = (this.width - currentLineWidth) / numberOfSpaces;
20202
- for (let j = 0; j <= line.length; j++) {
20203
- charBound = this.__charBounds[i][j];
20204
- if (this._reSpaceAndTab.test(line[j])) {
20205
- charBound.width += diffSpace;
20206
- charBound.kernedWidth += diffSpace;
20207
- charBound.left += accumulatedSpace;
20208
- accumulatedSpace += diffSpace;
20209
- } else {
20210
- charBound.left += accumulatedSpace;
20907
+ console.log(`🔧 EnlargeSpaces Line ${i}:`);
20908
+ console.log(` Current width: ${currentLineWidth}, Target: ${this.width}`);
20909
+ console.log(` Spaces: ${numberOfSpaces}, diffSpace: ${diffSpace.toFixed(2)}`);
20910
+ if (isRtl) {
20911
+ for (let j = 0; j < line.length; j++) {
20912
+ if (this._reSpaceAndTab.test(line[j])) ;
20913
+ }
20914
+
20915
+ // For RTL, we need to work backwards through the visual positions
20916
+ // but still update logical positions correctly
20917
+ let spaceCount = 0;
20918
+ for (let j = 0; j <= line.length; j++) {
20919
+ charBound = this.__charBounds[i][j];
20920
+ if (charBound) {
20921
+ if (this._reSpaceAndTab.test(line[j])) {
20922
+ charBound.width += diffSpace;
20923
+ charBound.kernedWidth += diffSpace;
20924
+ spaceCount++;
20925
+ }
20926
+
20927
+ // For RTL, shift all characters to the right by the total expansion
20928
+ // minus the expansion that comes after this character
20929
+ const remainingSpaces = numberOfSpaces - spaceCount;
20930
+ const shiftAmount = remainingSpaces * diffSpace;
20931
+ charBound.left += shiftAmount;
20932
+ }
20933
+ }
20934
+ } else {
20935
+ // LTR processing (original logic)
20936
+ for (let j = 0; j <= line.length; j++) {
20937
+ charBound = this.__charBounds[i][j];
20938
+ if (charBound) {
20939
+ if (this._reSpaceAndTab.test(line[j])) {
20940
+ charBound.width += diffSpace;
20941
+ charBound.kernedWidth += diffSpace;
20942
+ charBound.left += accumulatedSpace;
20943
+ accumulatedSpace += diffSpace;
20944
+ } else {
20945
+ charBound.left += accumulatedSpace;
20946
+ }
20947
+ }
20211
20948
  }
20212
20949
  }
20213
20950
  }
@@ -20285,6 +21022,18 @@ class FabricText extends StyledText {
20285
21022
 
20286
21023
  // Convert layout to legacy format for compatibility
20287
21024
  this._convertLayoutToLegacyFormat(layout);
21025
+
21026
+ // Ensure justify alignment is properly applied for compatibility with legacy rendering
21027
+ if (this.textAlign.includes(JUSTIFY)) {
21028
+ // Force enlarge spaces after advanced layout calculation
21029
+ setTimeout(() => {
21030
+ if (this.enlargeSpaces) {
21031
+ var _this$canvas2;
21032
+ this.enlargeSpaces();
21033
+ (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.renderAll();
21034
+ }
21035
+ }, 0);
21036
+ }
20288
21037
  this.dirty = true;
20289
21038
  }
20290
21039
 
@@ -20865,7 +21614,15 @@ class FabricText extends StyledText {
20865
21614
  if (currentDirection !== this.direction) {
20866
21615
  ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl');
20867
21616
  ctx.direction = isLtr ? 'ltr' : 'rtl';
20868
- ctx.textAlign = isLtr ? LEFT : RIGHT;
21617
+
21618
+ // For justify alignments, we need to set the correct canvas text alignment
21619
+ // This is crucial for RTL text to render in the correct order
21620
+ if (isJustify) {
21621
+ // Justify uses LEFT alignment as a base, letting the character positioning handle justification
21622
+ ctx.textAlign = LEFT;
21623
+ } else {
21624
+ ctx.textAlign = isLtr ? LEFT : RIGHT;
21625
+ }
20869
21626
  }
20870
21627
  top -= lineHeight * this._fontSizeFraction / this.lineHeight;
20871
21628
  if (shortCut) {
@@ -21101,9 +21858,21 @@ class FabricText extends StyledText {
21101
21858
  direction = this.direction,
21102
21859
  isEndOfWrapping = this.isEndOfWrapping(lineIndex);
21103
21860
  let leftOffset = 0;
21104
- if (textAlign === JUSTIFY || textAlign === JUSTIFY_CENTER && !isEndOfWrapping || textAlign === JUSTIFY_RIGHT && !isEndOfWrapping || textAlign === JUSTIFY_LEFT && !isEndOfWrapping) {
21105
- return 0;
21861
+
21862
+ // Handle justify alignments (excluding last lines and wrapped line ends)
21863
+ const isJustifyLine = textAlign === JUSTIFY || textAlign === JUSTIFY_CENTER && !isEndOfWrapping || textAlign === JUSTIFY_RIGHT && !isEndOfWrapping || textAlign === JUSTIFY_LEFT && !isEndOfWrapping;
21864
+ if (isJustifyLine) {
21865
+ // Justify lines should start at the left edge for LTR and right edge for RTL
21866
+ // The space distribution is handled by enlargeSpaces()
21867
+ if (direction === 'rtl') {
21868
+ // For RTL justify, we need to account for the line being right-aligned
21869
+ return 0;
21870
+ } else {
21871
+ return 0;
21872
+ }
21106
21873
  }
21874
+
21875
+ // Handle non-justify alignments
21107
21876
  if (textAlign === CENTER) {
21108
21877
  leftOffset = lineDiff / 2;
21109
21878
  }
@@ -21116,6 +21885,8 @@ class FabricText extends StyledText {
21116
21885
  if (textAlign === JUSTIFY_RIGHT) {
21117
21886
  leftOffset = lineDiff;
21118
21887
  }
21888
+
21889
+ // Apply RTL adjustments for non-justify alignments
21119
21890
  if (direction === 'rtl') {
21120
21891
  if (textAlign === RIGHT || textAlign === JUSTIFY || textAlign === JUSTIFY_RIGHT) {
21121
21892
  leftOffset = 0;
@@ -21274,7 +22045,19 @@ class FabricText extends StyledText {
21274
22045
  fontSize = this.fontSize
21275
22046
  } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
21276
22047
  let forMeasuring = arguments.length > 1 ? arguments[1] : undefined;
21277
- const parsedFontFamily = fontFamily.includes("'") || fontFamily.includes('"') || fontFamily.includes(',') || FabricText.genericFonts.includes(fontFamily.toLowerCase()) ? fontFamily : `"${fontFamily}"`;
22048
+ let parsedFontFamily = fontFamily.includes("'") || fontFamily.includes('"') || fontFamily.includes(',') || FabricText.genericFonts.includes(fontFamily.toLowerCase()) ? fontFamily : `"${fontFamily}"`;
22049
+
22050
+ // For fonts like STV that don't support English/Latin characters,
22051
+ // add fallback fonts for consistent rendering of unsupported characters
22052
+ // Only add fallbacks during actual rendering, not for measurements
22053
+ if (!forMeasuring &&
22054
+ // Only during rendering, not measuring
22055
+ !fontFamily.includes(',') && (
22056
+ // Don't add fallbacks if already has them
22057
+ fontFamily.toLowerCase().includes('stv') || fontFamily.toLowerCase().includes('arabic') || fontFamily.toLowerCase().includes('naskh') || fontFamily.toLowerCase().includes('kufi'))) {
22058
+ // Add fallback fonts for unsupported characters (spaces, punctuation, etc.)
22059
+ parsedFontFamily = `${parsedFontFamily}, "Arial Unicode MS", Arial, sans-serif`;
22060
+ }
21278
22061
  return [fontStyle, fontWeight, `${forMeasuring ? this.CACHE_FONT_SIZE : fontSize}px`, parsedFontFamily].join(' ');
21279
22062
  }
21280
22063
 
@@ -21318,7 +22101,13 @@ class FabricText extends StyledText {
21318
22101
  newLine = ['\n'];
21319
22102
  let newText = [];
21320
22103
  for (let i = 0; i < lines.length; i++) {
21321
- newLines[i] = this.graphemeSplit(lines[i]);
22104
+ // Use BiDi-aware grapheme splitting for RTL text
22105
+ if (this.direction === 'rtl' || this._containsArabicText(lines[i])) {
22106
+ newLines[i] = segmentGraphemes(lines[i]);
22107
+ console.log(`🔤 BiDi-aware split line ${i}: "${lines[i]}" -> [${newLines[i].join(', ')}]`);
22108
+ } else {
22109
+ newLines[i] = this.graphemeSplit(lines[i]);
22110
+ }
21322
22111
  newText = newText.concat(newLines[i], newLine);
21323
22112
  }
21324
22113
  newText.pop();
@@ -21330,6 +22119,14 @@ class FabricText extends StyledText {
21330
22119
  };
21331
22120
  }
21332
22121
 
22122
+ /**
22123
+ * Check if text contains Arabic characters
22124
+ * @private
22125
+ */
22126
+ _containsArabicText(text) {
22127
+ return /[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
22128
+ }
22129
+
21333
22130
  /**
21334
22131
  * Returns object representation of an instance
21335
22132
  * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
@@ -21452,6 +22249,88 @@ class FabricText extends StyledText {
21452
22249
 
21453
22250
  /* _FROM_SVG_END_ */
21454
22251
 
22252
+ /**
22253
+ * Check if the font is ready for accurate measurements
22254
+ * @private
22255
+ */
22256
+ _isFontReady() {
22257
+ if (typeof document === 'undefined' || !('fonts' in document)) {
22258
+ return true; // Assume ready in non-browser environments
22259
+ }
22260
+ try {
22261
+ return document.fonts.check(`${this.fontSize}px ${this.fontFamily}`);
22262
+ } catch (e) {
22263
+ return true; // Fallback to assuming ready if check fails
22264
+ }
22265
+ }
22266
+
22267
+ /**
22268
+ * Schedule re-initialization after font loads
22269
+ * @private
22270
+ */
22271
+ _scheduleInitAfterFontLoad() {
22272
+ if (typeof document === 'undefined' || !('fonts' in document)) {
22273
+ return;
22274
+ }
22275
+
22276
+ // Only schedule if not already waiting
22277
+ if (this._fontLoadScheduled) {
22278
+ return;
22279
+ }
22280
+ this._fontLoadScheduled = true;
22281
+ const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
22282
+ document.fonts.load(fontSpec).then(() => {
22283
+ this._fontLoadScheduled = false;
22284
+ // Re-initialize dimensions with proper font metrics
22285
+ this.initDimensions();
22286
+
22287
+ // Extra step for justify alignment after font loading
22288
+ if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
22289
+ setTimeout(() => {
22290
+ var _this$canvas3;
22291
+ if (this.enlargeSpaces) {
22292
+ this.enlargeSpaces();
22293
+ }
22294
+ (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
22295
+ }, 10);
22296
+ } else {
22297
+ var _this$canvas4;
22298
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.requestRenderAll();
22299
+ }
22300
+ }).catch(() => {
22301
+ this._fontLoadScheduled = false;
22302
+ });
22303
+ }
22304
+
22305
+ /**
22306
+ * Force complete text re-initialization (useful after JSON loading)
22307
+ */
22308
+ forceTextReinitialization() {
22309
+ console.log('🔄 Force reinitializing text object');
22310
+
22311
+ // Clear all caches
22312
+ this._clearCache();
22313
+ this.dirty = true;
22314
+
22315
+ // Force text splitting to rebuild internal structures
22316
+ this._splitText();
22317
+
22318
+ // Re-initialize dimensions
22319
+ this.initDimensions();
22320
+
22321
+ // Special handling for justify alignment
22322
+ if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
22323
+ // Ensure justify is applied after dimensions are set
22324
+ setTimeout(() => {
22325
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
22326
+ var _this$canvas5;
22327
+ this.enlargeSpaces();
22328
+ (_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
22329
+ }
22330
+ }, 10);
22331
+ }
22332
+ }
22333
+
21455
22334
  /**
21456
22335
  * Returns FabricText instance from an object representation
21457
22336
  * @param {Object} object plain js Object to create an instance from
@@ -21463,6 +22342,93 @@ class FabricText extends StyledText {
21463
22342
  styles: stylesFromArray(object.styles || {}, object.text)
21464
22343
  }, {
21465
22344
  extraParam: 'text'
22345
+ }).then(textObject => {
22346
+ // Ensure text object is properly initialized after JSON deserialization
22347
+ // This is critical for justify alignment and other text layout features
22348
+ textObject.initialized = true;
22349
+
22350
+ // Force reinitialization to ensure proper layout
22351
+ if (textObject._clearCache) {
22352
+ textObject._clearCache();
22353
+ }
22354
+ textObject.dirty = true;
22355
+
22356
+ // Check if we need to wait for font loading (especially for custom fonts like STV)
22357
+ const fontSpec = `${textObject.fontSize}px ${textObject.fontFamily}`;
22358
+
22359
+ // For custom fonts, ensure they're loaded before initializing dimensions
22360
+ if (typeof document !== 'undefined' && 'fonts' in document && textObject.fontFamily !== 'Arial' && textObject.fontFamily !== 'Times New Roman') {
22361
+ return document.fonts.load(fontSpec).then(() => {
22362
+ var _textObject$fontFamil;
22363
+ console.log(`🔤 Font loaded for JSON object: ${fontSpec}`);
22364
+ // Ensure initialized flag is set again (in case constructor reset it)
22365
+ textObject.initialized = true;
22366
+
22367
+ // Special handling for STV fonts which have measurement issues
22368
+ const isStvFont = (_textObject$fontFamil = textObject.fontFamily) === null || _textObject$fontFamil === void 0 ? void 0 : _textObject$fontFamil.toLowerCase().includes('stv');
22369
+ if (isStvFont) {
22370
+ console.log(`🔤 STV font detected, using enhanced reinitialization`);
22371
+
22372
+ // Clear all cached state that might interfere with browser wrapping
22373
+ textObject._browserWrapCache = null;
22374
+ textObject._lastDimensionState = null;
22375
+ textObject._browserWrapInitialized = false;
22376
+ console.log(`🔤 STV font: Cleared all cached states for fresh initialization`);
22377
+
22378
+ // Force browser wrapping flag for STV fonts
22379
+ textObject._usingBrowserWrapping = true;
22380
+ console.log(`🔤 STV font: Forcing browser wrapping flag during JSON load`);
22381
+
22382
+ // Multiple initialization attempts for STV fonts
22383
+ const reinitWithDelay = attempt => {
22384
+ if (textObject.forceTextReinitialization) {
22385
+ textObject.forceTextReinitialization();
22386
+ } else {
22387
+ textObject.initDimensions();
22388
+ }
22389
+
22390
+ // Check if width is still problematic after initialization
22391
+ if (textObject.width < 50 && attempt < 3) {
22392
+ console.log(`🔤 STV font width still ${textObject.width}px, retrying in ${100 * attempt}ms (attempt ${attempt + 1}/3)`);
22393
+ setTimeout(() => reinitWithDelay(attempt + 1), 100 * attempt);
22394
+ }
22395
+ };
22396
+ reinitWithDelay(0);
22397
+ } else {
22398
+ // Use specialized reinitialization for Textbox objects
22399
+ if (textObject.forceTextReinitialization) {
22400
+ console.log(`🔤 Using Textbox specialized reinitialization`);
22401
+ textObject.forceTextReinitialization();
22402
+ } else {
22403
+ // Reinitialize dimensions with proper font metrics
22404
+ textObject.initDimensions();
22405
+ }
22406
+ }
22407
+ return textObject;
22408
+ }).catch(() => {
22409
+ console.warn(`⚠️ Font loading failed for ${fontSpec}, proceeding with fallback`);
22410
+ // Ensure initialized flag is set again
22411
+ textObject.initialized = true;
22412
+
22413
+ // Still initialize dimensions even if font loading fails
22414
+ if (textObject.forceTextReinitialization) {
22415
+ textObject.forceTextReinitialization();
22416
+ } else {
22417
+ textObject.initDimensions();
22418
+ }
22419
+ return textObject;
22420
+ });
22421
+ } else {
22422
+ // Standard fonts - ensure initialized and use appropriate method
22423
+ textObject.initialized = true;
22424
+ if (textObject.forceTextReinitialization) {
22425
+ console.log(`🔤 Using Textbox specialized reinitialization for standard font`);
22426
+ textObject.forceTextReinitialization();
22427
+ } else {
22428
+ textObject.initDimensions();
22429
+ }
22430
+ return textObject;
22431
+ }
21466
22432
  });
21467
22433
  }
21468
22434
  }
@@ -21902,8 +22868,12 @@ class OverlayEditor {
21902
22868
  this.textarea.style.pointerEvents = 'auto';
21903
22869
  // Set appropriate unicodeBidi based on content and direction
21904
22870
  const hasArabicText = /[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(this.target.text || '');
22871
+ const hasLatinText = /[a-zA-Z]/.test(this.target.text || '');
21905
22872
  const isLTRDirection = this.target.direction === 'ltr';
21906
- if (hasArabicText && isLTRDirection) {
22873
+ if (hasArabicText && hasLatinText && isLTRDirection) {
22874
+ // For mixed Arabic/Latin text in LTR mode, use embed for consistent line wrapping
22875
+ this.textarea.style.unicodeBidi = 'embed';
22876
+ } else if (hasArabicText && isLTRDirection) {
21907
22877
  // For Arabic text in LTR mode, use embed to preserve shaping while respecting direction
21908
22878
  this.textarea.style.unicodeBidi = 'embed';
21909
22879
  } else {
@@ -21992,14 +22962,26 @@ class OverlayEditor {
21992
22962
  parseFloat(this.hostDiv.style.width) / zoom;
21993
22963
  const currentHeight = parseFloat(this.hostDiv.style.height) / zoom;
21994
22964
 
21995
- // Only update if there's a meaningful change (avoid float precision issues)
22965
+ // Always update height for responsive controls (especially important for line deletion)
21996
22966
  const heightDiff = Math.abs(currentHeight - target.height);
21997
- const threshold = 1; // 1px threshold to avoid micro-changes
22967
+ const threshold = 0.5; // Lower threshold for better responsiveness to line changes
21998
22968
 
21999
22969
  if (heightDiff > threshold) {
22970
+ target.height;
22000
22971
  target.height = currentHeight;
22001
22972
  target.setCoords(); // Update control positions
22973
+
22974
+ // Force dirty to ensure proper re-rendering
22975
+ target.dirty = true;
22002
22976
  this.canvas.requestRenderAll(); // Re-render to show updated selection
22977
+
22978
+ // IMPORTANT: Reposition overlay after height change
22979
+ requestAnimationFrame(() => {
22980
+ if (!this.isDestroyed) {
22981
+ this.applyOverlayStyle();
22982
+ console.log('📐 Height changed - rechecking alignment after repositioning:');
22983
+ }
22984
+ });
22003
22985
  }
22004
22986
  }
22005
22987
 
@@ -22027,14 +23009,6 @@ class OverlayEditor {
22027
23009
  target.setCoords();
22028
23010
  const aCoords = target.aCoords;
22029
23011
 
22030
- // DEBUG: Log dimensions before edit
22031
- console.log('BEFORE EDIT:');
22032
- console.log(' target.width =', target.width);
22033
- console.log(' target.height =', target.height);
22034
- console.log(' target.getScaledWidth() =', target.getScaledWidth());
22035
- console.log(' target.getScaledHeight() =', target.getScaledHeight());
22036
- console.log(' target.padding =', target.padding);
22037
-
22038
23012
  // 2. Get canvas position and scroll offsets (like rtl-test.html)
22039
23013
  const canvasEl = canvas.upperCanvasEl;
22040
23014
  const canvasRect = canvasEl.getBoundingClientRect();
@@ -22057,14 +23031,12 @@ class OverlayEditor {
22057
23031
  const left = canvasRect.left + scrollX + screenPoint.x;
22058
23032
  const top = canvasRect.top + scrollY + screenPoint.y;
22059
23033
 
22060
- // 4. Get dimensions with zoom scaling - use target.width for text wrapping, scaled height for container
22061
- const width = target.width * (target.scaleX || 1) * zoom; // Account for object scale and viewport zoom
22062
- const height = target.height * (target.scaleY || 1) * zoom;
22063
- console.log('WIDTH CALCULATION:');
22064
- console.log(' target.width =', target.width);
22065
- console.log(' scaledWidth =', target.getScaledWidth());
22066
- console.log(' zoom =', zoom);
22067
- console.log(' final width =', width);
23034
+ // 4. Calculate the precise width and height for the container
23035
+ // **THE FIX:** Use getBoundingRect() for BOTH width and height.
23036
+ // This is the most reliable measure of the object's final rendered dimensions.
23037
+ const objectBounds = target.getBoundingRect();
23038
+ const width = Math.round(objectBounds.width * zoom);
23039
+ const height = Math.round(objectBounds.height * zoom);
22068
23040
 
22069
23041
  // 5. Apply styles to host DIV - absolute positioning like rtl-test.html
22070
23042
  this.hostDiv.style.position = 'absolute';
@@ -22088,50 +23060,333 @@ class OverlayEditor {
22088
23060
  const scaleX = target.scaleX || 1;
22089
23061
  const finalFontSize = baseFontSize * scaleX * zoom;
22090
23062
  const fabricLineHeight = target.lineHeight || 1.16;
22091
- // Apply padding and dimensions to textarea
22092
- const textareaWidth = paddingX > 0 ? `calc(100% - ${2 * paddingX}px)` : '100%';
22093
- const textareaHeight = paddingY > 0 ? `calc(100% - ${2 * paddingY}px)` : '100%';
22094
- this.textarea.style.width = textareaWidth;
22095
- this.textarea.style.height = textareaHeight;
23063
+ // **THE FIX:** Use 'border-box' so the width property includes padding.
23064
+ // This makes alignment much easier and more reliable.
23065
+ this.textarea.style.boxSizing = 'border-box';
23066
+
23067
+ // **THE FIX:** Set the textarea width to be IDENTICAL to the host div's width.
23068
+ // The padding will now be correctly contained *inside* this width.
23069
+ this.textarea.style.width = `${width}px`;
23070
+ this.textarea.style.height = '100%'; // Let hostDiv control height
22096
23071
  this.textarea.style.padding = `${paddingY}px ${paddingX}px`;
23072
+
23073
+ // Apply all other font and text styles to match Fabric
23074
+ const letterSpacingPx = (target.charSpacing || 0) / 1000 * finalFontSize;
23075
+
23076
+ // Special handling for text objects loaded from JSON - ensure they're properly initialized
23077
+ if (target.dirty !== false && target.initDimensions) {
23078
+ console.log('🔧 Ensuring text object is properly initialized before overlay editing');
23079
+ // Force re-initialization if the text object seems to be in a dirty state
23080
+ target.initDimensions();
23081
+ }
22097
23082
  this.textarea.style.fontSize = `${finalFontSize}px`;
22098
- this.textarea.style.lineHeight = String(fabricLineHeight); // Use unit-less multiplier
23083
+ this.textarea.style.lineHeight = String(fabricLineHeight);
22099
23084
  this.textarea.style.fontFamily = target.fontFamily || 'Arial';
22100
23085
  this.textarea.style.fontWeight = String(target.fontWeight || 'normal');
22101
23086
  this.textarea.style.fontStyle = target.fontStyle || 'normal';
22102
- this.textarea.style.textAlign = target.textAlign || 'left';
23087
+ // Handle text alignment and justification
23088
+ const textAlign = target.textAlign || 'left';
23089
+ let cssTextAlign = textAlign;
23090
+
23091
+ // Detect text direction from content for proper justify handling
23092
+ const autoDetectedDirection = this.firstStrongDir(this.textarea.value || '');
23093
+
23094
+ // DEBUG: Log alignment details
23095
+ console.log('🔍 ALIGNMENT DEBUG:');
23096
+ console.log(' Fabric textAlign:', textAlign);
23097
+ console.log(' Fabric direction:', target.direction);
23098
+ console.log(' Text content:', JSON.stringify(target.text));
23099
+ console.log(' Detected direction:', autoDetectedDirection);
23100
+
23101
+ // Map fabric.js justify to CSS
23102
+ if (textAlign.includes('justify')) {
23103
+ // Try to match fabric.js justify behavior more precisely
23104
+ try {
23105
+ // For justify, we need to replicate fabric.js space expansion
23106
+ // Use CSS justify but with specific settings to match fabric.js better
23107
+ cssTextAlign = 'justify';
23108
+
23109
+ // Set text-align-last based on justify type and detected direction
23110
+ // Smart justify: respect detected direction even when fabric alignment doesn't match
23111
+ if (textAlign === 'justify') {
23112
+ this.textarea.style.textAlignLast = autoDetectedDirection === 'rtl' ? 'right' : 'left';
23113
+ } else if (textAlign === 'justify-left') {
23114
+ // If text is RTL but fabric says justify-left, override to justify-right for better UX
23115
+ if (autoDetectedDirection === 'rtl') {
23116
+ this.textarea.style.textAlignLast = 'right';
23117
+ console.log(' → Overrode justify-left to justify-right for RTL text');
23118
+ } else {
23119
+ this.textarea.style.textAlignLast = 'left';
23120
+ }
23121
+ } else if (textAlign === 'justify-right') {
23122
+ // If text is LTR but fabric says justify-right, override to justify-left for better UX
23123
+ if (autoDetectedDirection === 'ltr') {
23124
+ this.textarea.style.textAlignLast = 'left';
23125
+ console.log(' → Overrode justify-right to justify-left for LTR text');
23126
+ } else {
23127
+ this.textarea.style.textAlignLast = 'right';
23128
+ }
23129
+ } else if (textAlign === 'justify-center') {
23130
+ this.textarea.style.textAlignLast = 'center';
23131
+ }
23132
+
23133
+ // Enhanced justify settings for better fabric.js matching
23134
+ this.textarea.style.textJustify = 'inter-word';
23135
+ this.textarea.style.wordSpacing = 'normal';
23136
+
23137
+ // Additional CSS properties for better justify matching
23138
+ this.textarea.style.textAlign = 'justify';
23139
+ this.textarea.style.textAlignLast = this.textarea.style.textAlignLast;
23140
+
23141
+ // Try to force better justify behavior
23142
+ this.textarea.style.textJustifyTrim = 'none';
23143
+ this.textarea.style.textAutospace = 'none';
23144
+ console.log(' → Applied justify alignment:', textAlign, 'with last-line:', this.textarea.style.textAlignLast);
23145
+ } catch (error) {
23146
+ console.warn(' → Justify setup failed, falling back to standard alignment:', error);
23147
+ cssTextAlign = textAlign.replace('justify-', '').replace('justify', 'left');
23148
+ }
23149
+ } else {
23150
+ this.textarea.style.textAlignLast = 'auto';
23151
+ this.textarea.style.textJustify = 'auto';
23152
+ this.textarea.style.wordSpacing = 'normal';
23153
+ console.log(' → Applied standard alignment:', cssTextAlign);
23154
+ }
23155
+ this.textarea.style.textAlign = cssTextAlign;
22103
23156
  this.textarea.style.color = ((_target$fill = target.fill) === null || _target$fill === void 0 ? void 0 : _target$fill.toString()) || '#000';
22104
- this.textarea.style.letterSpacing = `${(target.charSpacing || 0) / 1000}em`;
22105
- this.textarea.style.direction = target.direction || this.firstStrongDir(this.textarea.value || '');
23157
+ this.textarea.style.letterSpacing = `${letterSpacingPx}px`;
22106
23158
 
22107
- // Ensure consistent font rendering between Fabric and CSS
23159
+ // Use the already detected direction from above
23160
+ const fabricDirection = target.direction;
23161
+
23162
+ // Use auto-detected direction for better BiDi support, but respect fabric direction if it makes sense
23163
+ this.textarea.style.direction = autoDetectedDirection || fabricDirection || 'ltr';
22108
23164
  this.textarea.style.fontVariant = 'normal';
22109
23165
  this.textarea.style.fontStretch = 'normal';
22110
- this.textarea.style.textRendering = 'auto';
22111
- this.textarea.style.fontKerning = 'auto';
22112
- this.textarea.style.boxSizing = 'content-box'; // Padding is added outside width/height
23166
+ this.textarea.style.textRendering = 'auto'; // Changed from 'optimizeLegibility' to match canvas
23167
+ this.textarea.style.fontKerning = 'normal';
23168
+ this.textarea.style.fontFeatureSettings = 'normal';
23169
+ this.textarea.style.fontVariationSettings = 'normal';
22113
23170
  this.textarea.style.margin = '0';
22114
23171
  this.textarea.style.border = 'none';
22115
23172
  this.textarea.style.outline = 'none';
22116
23173
  this.textarea.style.background = 'transparent';
22117
- this.textarea.style.wordWrap = 'break-word';
23174
+ this.textarea.style.overflowWrap = 'break-word';
22118
23175
  this.textarea.style.whiteSpace = 'pre-wrap';
22119
-
22120
- // DEBUG: Log final textarea dimensions
22121
- console.log('TEXTAREA AFTER SETUP:');
22122
- console.log(' textarea width =', this.textarea.style.width);
22123
- console.log(' textarea height =', this.textarea.style.height);
22124
- console.log(' textarea padding =', this.textarea.style.padding);
22125
- console.log(' paddingX =', paddingX, 'paddingY =', paddingY);
22126
- console.log(' baseFontSize =', baseFontSize);
22127
- console.log(' scaleX =', scaleX);
22128
- console.log(' zoom =', zoom);
22129
- console.log(' finalFontSize =', finalFontSize);
22130
- console.log(' fabricLineHeight =', fabricLineHeight);
23176
+ this.textarea.style.hyphens = 'none';
23177
+
23178
+ // DEBUG: Log final CSS properties
23179
+ console.log('🎨 FINAL TEXTAREA CSS:');
23180
+ console.log(' textAlign:', this.textarea.style.textAlign);
23181
+ console.log(' textAlignLast:', this.textarea.style.textAlignLast);
23182
+ console.log(' direction:', this.textarea.style.direction);
23183
+ console.log(' unicodeBidi:', this.textarea.style.unicodeBidi);
23184
+ console.log(' width:', this.textarea.style.width);
23185
+ console.log(' textJustify:', this.textarea.style.textJustify);
23186
+ console.log(' wordSpacing:', this.textarea.style.wordSpacing);
23187
+ console.log(' whiteSpace:', this.textarea.style.whiteSpace);
23188
+
23189
+ // If justify, log Fabric object dimensions for comparison
23190
+ if (textAlign.includes('justify')) {
23191
+ var _calcTextWidth, _ref;
23192
+ console.log('🔧 FABRIC OBJECT JUSTIFY INFO:');
23193
+ console.log(' Fabric width:', target.width);
23194
+ console.log(' Fabric calcTextWidth:', (_calcTextWidth = (_ref = target).calcTextWidth) === null || _calcTextWidth === void 0 ? void 0 : _calcTextWidth.call(_ref));
23195
+ console.log(' Fabric textAlign:', target.textAlign);
23196
+ console.log(' Text lines:', target.textLines);
23197
+ }
23198
+
23199
+ // Debug font properties matching
23200
+ console.log('🔤 FONT PROPERTIES COMPARISON:');
23201
+ console.log(' Fabric fontFamily:', target.fontFamily);
23202
+ console.log(' Fabric fontWeight:', target.fontWeight);
23203
+ console.log(' Fabric fontStyle:', target.fontStyle);
23204
+ console.log(' Fabric fontSize:', target.fontSize);
23205
+ console.log(' → Textarea fontFamily:', this.textarea.style.fontFamily);
23206
+ console.log(' → Textarea fontWeight:', this.textarea.style.fontWeight);
23207
+ console.log(' → Textarea fontStyle:', this.textarea.style.fontStyle);
23208
+ console.log(' → Textarea fontSize:', this.textarea.style.fontSize);
23209
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
23210
+
23211
+ // Enhanced font rendering to better match fabric.js canvas rendering
23212
+ // Default to auto for more natural rendering
23213
+ this.textarea.style.webkitFontSmoothing = 'auto';
23214
+ this.textarea.style.mozOsxFontSmoothing = 'auto';
23215
+ this.textarea.style.fontSmooth = 'auto';
23216
+ this.textarea.style.textSizeAdjust = 'none';
23217
+
23218
+ // For bold fonts, use subpixel rendering to match canvas thickness better
23219
+ const fontWeight = String(target.fontWeight || 'normal');
23220
+ const isBold = fontWeight === 'bold' || fontWeight === '700' || parseInt(fontWeight) >= 600;
23221
+ if (isBold) {
23222
+ this.textarea.style.webkitFontSmoothing = 'subpixel-antialiased';
23223
+ this.textarea.style.mozOsxFontSmoothing = 'unset';
23224
+ console.log('🔤 Applied enhanced bold rendering for better thickness matching');
23225
+ }
23226
+ console.log('🎨 FONT SMOOTHING APPLIED:');
23227
+ console.log(' webkitFontSmoothing:', this.textarea.style.webkitFontSmoothing);
23228
+ console.log(' mozOsxFontSmoothing:', this.textarea.style.mozOsxFontSmoothing);
22131
23229
 
22132
23230
  // Initial bounds are set correctly by Fabric.js - don't force update here
22133
23231
  }
22134
23232
 
23233
+ /**
23234
+ * Debug method to compare textarea and canvas object bounding boxes
23235
+ */
23236
+ debugBoundingBoxComparison() {
23237
+ const target = this.target;
23238
+ const canvas = this.canvas;
23239
+ const zoom = canvas.getZoom();
23240
+
23241
+ // Get textarea bounding box (in screen coordinates)
23242
+ const textareaRect = this.textarea.getBoundingClientRect();
23243
+ const hostRect = this.hostDiv.getBoundingClientRect();
23244
+
23245
+ // Get canvas object bounding box (in screen coordinates)
23246
+ const canvasBounds = target.getBoundingRect();
23247
+ const canvasRect = canvas.upperCanvasEl.getBoundingClientRect();
23248
+
23249
+ // Convert canvas object bounds to screen coordinates
23250
+ const vpt = canvas.viewportTransform;
23251
+ const screenObjectBounds = {
23252
+ left: canvasRect.left + canvasBounds.left * zoom + vpt[4],
23253
+ top: canvasRect.top + canvasBounds.top * zoom + vpt[5],
23254
+ width: canvasBounds.width * zoom,
23255
+ height: canvasBounds.height * zoom
23256
+ };
23257
+ console.log('🔍 BOUNDING BOX COMPARISON:');
23258
+ console.log('📦 Textarea Rect:', {
23259
+ left: Math.round(textareaRect.left * 100) / 100,
23260
+ top: Math.round(textareaRect.top * 100) / 100,
23261
+ width: Math.round(textareaRect.width * 100) / 100,
23262
+ height: Math.round(textareaRect.height * 100) / 100
23263
+ });
23264
+ console.log('📦 Host Div Rect:', {
23265
+ left: Math.round(hostRect.left * 100) / 100,
23266
+ top: Math.round(hostRect.top * 100) / 100,
23267
+ width: Math.round(hostRect.width * 100) / 100,
23268
+ height: Math.round(hostRect.height * 100) / 100
23269
+ });
23270
+ console.log('📦 Canvas Object Bounds (screen):', {
23271
+ left: Math.round(screenObjectBounds.left * 100) / 100,
23272
+ top: Math.round(screenObjectBounds.top * 100) / 100,
23273
+ width: Math.round(screenObjectBounds.width * 100) / 100,
23274
+ height: Math.round(screenObjectBounds.height * 100) / 100
23275
+ });
23276
+ console.log('📦 Canvas Object Bounds (canvas):', canvasBounds);
23277
+
23278
+ // Calculate differences
23279
+ const hostVsObject = {
23280
+ leftDiff: Math.round((hostRect.left - screenObjectBounds.left) * 100) / 100,
23281
+ topDiff: Math.round((hostRect.top - screenObjectBounds.top) * 100) / 100,
23282
+ widthDiff: Math.round((hostRect.width - screenObjectBounds.width) * 100) / 100,
23283
+ heightDiff: Math.round((hostRect.height - screenObjectBounds.height) * 100) / 100
23284
+ };
23285
+ const textareaVsObject = {
23286
+ leftDiff: Math.round((textareaRect.left - screenObjectBounds.left) * 100) / 100,
23287
+ topDiff: Math.round((textareaRect.top - screenObjectBounds.top) * 100) / 100,
23288
+ widthDiff: Math.round((textareaRect.width - screenObjectBounds.width) * 100) / 100,
23289
+ heightDiff: Math.round((textareaRect.height - screenObjectBounds.height) * 100) / 100
23290
+ };
23291
+ console.log('📏 Host Div vs Canvas Object Diff:', hostVsObject);
23292
+ console.log('📏 Textarea vs Canvas Object Diff:', textareaVsObject);
23293
+
23294
+ // Check if they're aligned (within 2px tolerance)
23295
+ const tolerance = 2;
23296
+ const hostAligned = Math.abs(hostVsObject.leftDiff) < tolerance && Math.abs(hostVsObject.topDiff) < tolerance && Math.abs(hostVsObject.widthDiff) < tolerance && Math.abs(hostVsObject.heightDiff) < tolerance;
23297
+ const textareaAligned = Math.abs(textareaVsObject.leftDiff) < tolerance && Math.abs(textareaVsObject.topDiff) < tolerance && Math.abs(textareaVsObject.widthDiff) < tolerance && Math.abs(textareaVsObject.heightDiff) < tolerance;
23298
+ console.log(hostAligned ? '✅ Host Div ALIGNED with canvas object' : '❌ Host Div MISALIGNED with canvas object');
23299
+ console.log(textareaAligned ? '✅ Textarea ALIGNED with canvas object' : '❌ Textarea MISALIGNED with canvas object');
23300
+ console.log('🔍 Zoom:', zoom, 'Viewport Transform:', vpt);
23301
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
23302
+ }
23303
+
23304
+ /**
23305
+ * Debug method to compare text wrapping between textarea and Fabric text object
23306
+ */
23307
+ debugTextWrapping() {
23308
+ const target = this.target;
23309
+ const text = this.textarea.value;
23310
+ console.log('📝 TEXT WRAPPING COMPARISON:');
23311
+ console.log('📄 Text Content:', `"${text}"`);
23312
+ console.log('📄 Text Length:', text.length);
23313
+
23314
+ // Analyze line breaks
23315
+ const explicitLines = text.split('\n');
23316
+ console.log('📄 Explicit Lines (\\n):', explicitLines.length);
23317
+ explicitLines.forEach((line, i) => {
23318
+ console.log(` Line ${i + 1}: "${line}" (${line.length} chars)`);
23319
+ });
23320
+
23321
+ // Get textarea computed styles for wrapping analysis
23322
+ const textareaStyles = window.getComputedStyle(this.textarea);
23323
+ console.log('📐 Textarea Wrapping Styles:');
23324
+ console.log(' width:', textareaStyles.width);
23325
+ console.log(' fontSize:', textareaStyles.fontSize);
23326
+ console.log(' fontFamily:', textareaStyles.fontFamily);
23327
+ console.log(' fontWeight:', textareaStyles.fontWeight);
23328
+ console.log(' letterSpacing:', textareaStyles.letterSpacing);
23329
+ console.log(' lineHeight:', textareaStyles.lineHeight);
23330
+ console.log(' whiteSpace:', textareaStyles.whiteSpace);
23331
+ console.log(' wordWrap:', textareaStyles.wordWrap);
23332
+ console.log(' overflowWrap:', textareaStyles.overflowWrap);
23333
+ console.log(' direction:', textareaStyles.direction);
23334
+ console.log(' textAlign:', textareaStyles.textAlign);
23335
+
23336
+ // Get Fabric text object properties for comparison
23337
+ console.log('📐 Fabric Text Object Properties:');
23338
+ console.log(' width:', target.width);
23339
+ console.log(' fontSize:', target.fontSize);
23340
+ console.log(' fontFamily:', target.fontFamily);
23341
+ console.log(' fontWeight:', target.fontWeight);
23342
+ console.log(' charSpacing:', target.charSpacing);
23343
+ console.log(' lineHeight:', target.lineHeight);
23344
+ console.log(' direction:', target.direction);
23345
+ console.log(' textAlign:', target.textAlign);
23346
+ console.log(' scaleX:', target.scaleX);
23347
+ console.log(' scaleY:', target.scaleY);
23348
+
23349
+ // Calculate effective dimensions for comparison - use actual rendered width
23350
+ // **THE FIX:** Use getBoundingRect to get the *actual rendered width* of the Fabric object.
23351
+ const fabricEffectiveWidth = this.target.getBoundingRect().width;
23352
+ // Use the exact width set on textarea for comparison
23353
+ const textareaComputedWidth = parseFloat(window.getComputedStyle(this.textarea).width);
23354
+ const textareaEffectiveWidth = textareaComputedWidth / this.canvas.getZoom();
23355
+ const widthDiff = Math.abs(textareaEffectiveWidth - fabricEffectiveWidth);
23356
+ console.log('📏 Effective Width Comparison:');
23357
+ console.log(' Textarea Effective Width:', textareaEffectiveWidth);
23358
+ console.log(' Fabric Effective Width:', fabricEffectiveWidth);
23359
+ console.log(' Width Difference:', widthDiff.toFixed(2) + 'px');
23360
+ console.log(widthDiff < 1 ? '✅ Widths MATCH for wrapping' : '❌ Width MISMATCH may cause different wrapping');
23361
+
23362
+ // Check text direction and bidi handling
23363
+ const hasRTLText = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
23364
+ const hasBidiText = /[\u0590-\u06FF]/.test(text) && /[a-zA-Z]/.test(text);
23365
+ console.log('🌍 Text Direction Analysis:');
23366
+ console.log(' Has RTL characters:', hasRTLText);
23367
+ console.log(' Has mixed Bidi text:', hasBidiText);
23368
+ console.log(' Textarea direction:', textareaStyles.direction);
23369
+ console.log(' Fabric direction:', target.direction || 'auto');
23370
+ console.log(' Textarea unicodeBidi:', textareaStyles.unicodeBidi);
23371
+
23372
+ // Measure actual rendered line count
23373
+ const textareaScrollHeight = this.textarea.scrollHeight;
23374
+ const textareaLineHeight = parseFloat(textareaStyles.lineHeight) || parseFloat(textareaStyles.fontSize) * 1.2;
23375
+ const estimatedTextareaLines = Math.round(textareaScrollHeight / textareaLineHeight);
23376
+ console.log('📊 Line Count Analysis:');
23377
+ console.log(' Textarea scrollHeight:', textareaScrollHeight);
23378
+ console.log(' Textarea lineHeight:', textareaLineHeight);
23379
+ console.log(' Estimated rendered lines:', estimatedTextareaLines);
23380
+ console.log(' Explicit line breaks:', explicitLines.length);
23381
+ if (estimatedTextareaLines > explicitLines.length) {
23382
+ console.log('🔄 Text wrapping detected in textarea');
23383
+ console.log(' Wrapped lines:', estimatedTextareaLines - explicitLines.length);
23384
+ } else {
23385
+ console.log('📏 No text wrapping in textarea');
23386
+ }
23387
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
23388
+ }
23389
+
22135
23390
  /**
22136
23391
  * Focus the textarea and position cursor at end
22137
23392
  */
@@ -22160,6 +23415,11 @@ class OverlayEditor {
22160
23415
  this.canvas.requestRenderAll();
22161
23416
  this.target.setCoords();
22162
23417
  this.applyOverlayStyle();
23418
+
23419
+ // Fix character mapping issues after JSON loading for browser-wrapped fonts
23420
+ if (this.target._fixCharacterMappingAfterJsonLoad) {
23421
+ this.target._fixCharacterMappingAfterJsonLoad();
23422
+ }
22163
23423
  this.textarea.focus();
22164
23424
  this.textarea.setSelectionRange(this.textarea.value.length, this.textarea.value.length);
22165
23425
 
@@ -22205,6 +23465,23 @@ class OverlayEditor {
22205
23465
  // Handle commit/cancel after restoring visibility
22206
23466
  if (commit && !this.isComposing) {
22207
23467
  const finalText = this.textarea.value;
23468
+
23469
+ // Auto-detect text direction and update fabric object if needed
23470
+ const detectedDirection = this.firstStrongDir(finalText);
23471
+ const currentDirection = this.target.direction || 'ltr';
23472
+ if (detectedDirection && detectedDirection !== currentDirection) {
23473
+ console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
23474
+ console.log(` Text content: "${finalText.substring(0, 50)}..."`);
23475
+
23476
+ // Update the fabric object's direction
23477
+ this.target.set('direction', detectedDirection);
23478
+
23479
+ // Force a re-render to apply the direction change
23480
+ this.canvas.requestRenderAll();
23481
+ console.log(`✅ Fabric object direction updated to: ${detectedDirection}`);
23482
+ } else {
23483
+ console.log(`📝 Overlay Exit: Direction unchanged (${currentDirection}), text: "${finalText.substring(0, 30)}..."`);
23484
+ }
22208
23485
  if (this.onCommit) {
22209
23486
  this.onCommit(finalText);
22210
23487
  }
@@ -22244,25 +23521,40 @@ class OverlayEditor {
22244
23521
  }
22245
23522
  }
22246
23523
  autoResizeTextarea() {
22247
- // Allow both vertical growth and shrinking; host width stays fixed
22248
- const oldHeight = parseFloat(window.getComputedStyle(this.textarea).height);
22249
-
22250
- // Reset height to measure actual needed height
22251
- this.textarea.style.height = 'auto';
23524
+ // Store the scroll position and the container's old height for comparison.
23525
+ const scrollTop = this.textarea.scrollTop;
23526
+ const oldHeight = parseFloat(this.hostDiv.style.height || '0');
23527
+
23528
+ // 1. **Force a reliable reflow.**
23529
+ // First, reset the textarea's height to a minimal value. This is the crucial step
23530
+ // that forces the browser to recalculate the content's height from scratch,
23531
+ // ignoring the hostDiv's larger, stale height.
23532
+ this.textarea.style.height = '1px';
23533
+
23534
+ // 2. Read the now-accurate scrollHeight. This value reflects the minimum
23535
+ // height required for the content, whether it's single or multi-line.
22252
23536
  const scrollHeight = this.textarea.scrollHeight;
22253
23537
 
22254
- // Add extra padding to prevent text clipping (especially for line height)
22255
- const lineHeightBuffer = 8; // Extra space to prevent clipping
22256
- const newHeight = Math.max(scrollHeight + lineHeightBuffer, 25); // Minimum height with buffer
22257
- const heightChanged = Math.abs(newHeight - oldHeight) > 2; // Only if meaningful change
23538
+ // A small buffer for rendering consistency across browsers.
23539
+ const buffer = 2;
23540
+ const newHeight = scrollHeight + buffer;
22258
23541
 
22259
- this.textarea.style.height = `${newHeight}px`;
22260
- this.hostDiv.style.height = `${newHeight}px`; // Match exactly
23542
+ // Check if the height has changed significantly.
23543
+ const heightChanged = Math.abs(newHeight - oldHeight) > 1;
22261
23544
 
22262
- // Only update object bounds if height actually changed
23545
+ // 4. Only update heights and object bounds if there was a change.
22263
23546
  if (heightChanged) {
23547
+ this.textarea.style.height = `${newHeight}px`;
23548
+ this.hostDiv.style.height = `${newHeight}px`;
22264
23549
  this.updateObjectBounds();
23550
+ } else {
23551
+ // If no significant change, ensure the textarea's height matches the container
23552
+ // to prevent any minor visual misalignment.
23553
+ this.textarea.style.height = this.hostDiv.style.height;
22265
23554
  }
23555
+
23556
+ // 5. Restore the original scroll position.
23557
+ this.textarea.scrollTop = scrollTop;
22266
23558
  }
22267
23559
  handleKeyDown(e) {
22268
23560
  if (e.key === 'Escape') {
@@ -22271,6 +23563,19 @@ class OverlayEditor {
22271
23563
  } else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
22272
23564
  e.preventDefault();
22273
23565
  this.destroy(true); // Commit
23566
+ } else if (e.key === 'Enter' || e.key === 'Backspace' || e.key === 'Delete') {
23567
+ // For keys that might change the height, schedule a resize check
23568
+ // Use both immediate and delayed checks to catch all scenarios
23569
+ requestAnimationFrame(() => {
23570
+ if (!this.isDestroyed) {
23571
+ this.autoResizeTextarea();
23572
+ }
23573
+ });
23574
+ setTimeout(() => {
23575
+ if (!this.isDestroyed) {
23576
+ this.autoResizeTextarea();
23577
+ }
23578
+ }, 10); // Small delay to ensure DOM is updated
22274
23579
  }
22275
23580
  }
22276
23581
  handleFocus() {
@@ -25247,6 +26552,7 @@ class Textbox extends IText {
25247
26552
  ...Textbox.ownDefaults,
25248
26553
  ...options
25249
26554
  });
26555
+ this.initializeEventListeners();
25250
26556
  }
25251
26557
 
25252
26558
  /**
@@ -25268,8 +26574,27 @@ class Textbox extends IText {
25268
26574
  */
25269
26575
  initDimensions() {
25270
26576
  if (!this.initialized) {
26577
+ this.initialized = true;
26578
+ }
26579
+
26580
+ // Prevent rapid recalculations during moves
26581
+ if (this._usingBrowserWrapping) {
26582
+ const now = Date.now();
26583
+ const lastCall = this._lastInitDimensionsTime || 0;
26584
+ const isRapidCall = now - lastCall < 100;
26585
+ const isDuringLoading = this._jsonLoading || !this._browserWrapInitialized;
26586
+ if (isRapidCall && !isDuringLoading) {
26587
+ return;
26588
+ }
26589
+ this._lastInitDimensionsTime = now;
26590
+ }
26591
+
26592
+ // Skip if nothing changed
26593
+ const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
26594
+ if (this._lastDimensionState === currentState && this._textLines && this._textLines.length > 0) {
25271
26595
  return;
25272
26596
  }
26597
+ this._lastDimensionState = currentState;
25273
26598
 
25274
26599
  // Use advanced layout if enabled
25275
26600
  if (this.enableAdvancedLayout) {
@@ -25280,17 +26605,142 @@ class Textbox extends IText {
25280
26605
  // clear dynamicMinWidth as it will be different after we re-wrap line
25281
26606
  this.dynamicMinWidth = 0;
25282
26607
  // wrap lines
25283
- this._styleMap = this._generateStyleMap(this._splitText());
25284
- // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap
25285
- if (this.dynamicMinWidth > this.width) {
26608
+ const splitTextResult = this._splitText();
26609
+ this._styleMap = this._generateStyleMap(splitTextResult);
26610
+
26611
+ // For browser wrapping, ensure _textLines is set from browser results
26612
+ if (this._usingBrowserWrapping && splitTextResult && splitTextResult.lines) {
26613
+ this._textLines = splitTextResult.lines.map(line => line.split(''));
26614
+
26615
+ // Store justify measurements and browser height
26616
+ const justifyMeasurements = splitTextResult.justifySpaceMeasurements;
26617
+ if (justifyMeasurements) {
26618
+ this._styleMap.justifySpaceMeasurements = justifyMeasurements;
26619
+ }
26620
+ const actualHeight = splitTextResult.actualBrowserHeight;
26621
+ if (actualHeight) {
26622
+ this._actualBrowserHeight = actualHeight;
26623
+ }
26624
+ }
26625
+ // Don't auto-resize width when using browser wrapping to prevent width increases during moves
26626
+ if (!this._usingBrowserWrapping && this.dynamicMinWidth > this.width) {
25286
26627
  this._set('width', this.dynamicMinWidth);
25287
26628
  }
26629
+
26630
+ // For browser wrapping fonts (like STV), ensure minimum width for new textboxes
26631
+ // since these fonts can't measure English characters properly
26632
+ if (this._usingBrowserWrapping && this.width < 50) {
26633
+ console.log(`🔤 BROWSER WRAP: Font ${this.fontFamily} has width ${this.width}px, setting to 300px for usability`);
26634
+ this.width = 300;
26635
+ }
26636
+
26637
+ // Mark browser wrapping as initialized when complete
26638
+ if (this._usingBrowserWrapping) {
26639
+ this._browserWrapInitialized = true;
26640
+ }
25288
26641
  if (this.textAlign.includes(JUSTIFY)) {
26642
+ // For browser wrapping fonts, apply browser-calculated justify spaces
26643
+ if (this._usingBrowserWrapping) {
26644
+ console.log('🔤 BROWSER WRAP: Applying browser-calculated justify spaces');
26645
+ this._applyBrowserJustifySpaces();
26646
+ return;
26647
+ }
26648
+
26649
+ // Don't apply justify alignment during drag operations to prevent snapping
26650
+ const now = Date.now();
26651
+ const lastDragTime = this._lastInitDimensionsTime || 0;
26652
+ const isDuringDrag = now - lastDragTime < 200; // 200ms window for drag detection
26653
+
26654
+ if (isDuringDrag) {
26655
+ console.log('🔤 Skipping justify during drag operation to prevent snapping');
26656
+ return;
26657
+ }
26658
+
26659
+ // For non-browser-wrapping fonts, use Fabric's justify system
25289
26660
  // once text is measured we need to make space fatter to make justified text.
25290
- this.enlargeSpaces();
26661
+ // Ensure __charBounds exists and fonts are ready before applying justify
26662
+ if (this.__charBounds && this.__charBounds.length > 0) {
26663
+ // Check if font is ready for accurate justify calculations
26664
+ const fontReady = this._isFontReady ? this._isFontReady() : true;
26665
+ if (fontReady) {
26666
+ this.enlargeSpaces();
26667
+ } else {
26668
+ console.warn('⚠️ Textbox: Font not ready for justify, deferring enlargeSpaces');
26669
+ // Defer justify calculation until font is ready
26670
+ this._scheduleJustifyAfterFontLoad();
26671
+ }
26672
+ } else {
26673
+ console.warn('⚠️ Textbox: __charBounds not ready for justify alignment, deferring enlargeSpaces');
26674
+ // Defer the justify calculation until the next frame
26675
+ setTimeout(() => {
26676
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
26677
+ var _this$canvas;
26678
+ console.log('🔧 Applying deferred Textbox justify alignment');
26679
+ this.enlargeSpaces();
26680
+ (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
26681
+ }
26682
+ }, 0);
26683
+ }
26684
+ }
26685
+ // Calculate height - use Fabric's calculation for proper text rendering space
26686
+ if (this._usingBrowserWrapping && this._textLines && this._textLines.length > 0) {
26687
+ const actualBrowserHeight = this._actualBrowserHeight;
26688
+ const oldHeight = this.height;
26689
+ // Use Fabric's height calculation since it knows how much space text rendering needs
26690
+ this.height = this.calcTextHeight();
26691
+
26692
+ // Force canvas refresh and control update if height changed significantly
26693
+ if (Math.abs(this.height - oldHeight) > 1) {
26694
+ var _this$canvas2, _this$_textLines;
26695
+ this.setCoords();
26696
+ (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
26697
+
26698
+ // DEBUG: Log exact positioning details
26699
+ console.log(`🎯 POSITIONING DEBUG:`);
26700
+ console.log(` Textbox height: ${this.height}px`);
26701
+ console.log(` Textbox top: ${this.top}px`);
26702
+ console.log(` Textbox left: ${this.left}px`);
26703
+ console.log(` Text lines: ${((_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length) || 0}`);
26704
+ console.log(` Font size: ${this.fontSize}px`);
26705
+ console.log(` Line height: ${this.lineHeight || 1.16}`);
26706
+ console.log(` Calculated line height: ${this.fontSize * (this.lineHeight || 1.16)}px`);
26707
+ console.log(` _getTopOffset(): ${this._getTopOffset()}px`);
26708
+ console.log(` calcTextHeight(): ${this.calcTextHeight()}px`);
26709
+ console.log(` Browser height: ${actualBrowserHeight}px`);
26710
+ console.log(` Height difference: ${this.height - this.calcTextHeight()}px`);
26711
+ }
26712
+ } else {
26713
+ this.height = this.calcTextHeight();
26714
+ }
26715
+ }
26716
+
26717
+ /**
26718
+ * Schedule justify calculation after font loads (Textbox-specific)
26719
+ * @private
26720
+ */
26721
+ _scheduleJustifyAfterFontLoad() {
26722
+ if (typeof document === 'undefined' || !('fonts' in document)) {
26723
+ return;
25291
26724
  }
25292
- // clear cache and re-calculate height
25293
- this.height = this.calcTextHeight();
26725
+
26726
+ // Only schedule if not already waiting
26727
+ if (this._fontJustifyScheduled) {
26728
+ return;
26729
+ }
26730
+ this._fontJustifyScheduled = true;
26731
+ const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
26732
+ document.fonts.load(fontSpec).then(() => {
26733
+ var _this$canvas3;
26734
+ this._fontJustifyScheduled = false;
26735
+ console.log('🔧 Textbox: Font loaded, applying justify alignment');
26736
+
26737
+ // Re-run initDimensions to ensure proper justify calculation
26738
+ this.initDimensions();
26739
+ (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
26740
+ }).catch(() => {
26741
+ this._fontJustifyScheduled = false;
26742
+ console.warn('⚠️ Textbox: Font loading failed, justify may be incorrect');
26743
+ });
25294
26744
  }
25295
26745
 
25296
26746
  /**
@@ -25657,19 +27107,33 @@ class Textbox extends IText {
25657
27107
  width: wordWidth
25658
27108
  } = data[i];
25659
27109
  offset += word.length;
25660
- lineWidth += infixWidth + wordWidth - additionalSpace;
25661
- if (lineWidth > maxWidth && !lineJustStarted) {
27110
+
27111
+ // Predictive wrapping: check if adding this word would exceed the width
27112
+ const potentialLineWidth = lineWidth + infixWidth + wordWidth - additionalSpace;
27113
+ // Use exact width to match overlay editor behavior
27114
+ const conservativeMaxWidth = maxWidth; // No artificial buffer
27115
+
27116
+ // Debug logging for wrapping decisions
27117
+ const currentLineText = line.join('');
27118
+ console.log(`🔧 FABRIC WRAP CHECK: "${data[i].word}" -> potential: ${potentialLineWidth.toFixed(1)}px vs limit: ${conservativeMaxWidth.toFixed(1)}px`);
27119
+ if (potentialLineWidth > conservativeMaxWidth && !lineJustStarted) {
27120
+ // This word would exceed the width, wrap before adding it
27121
+ console.log(`🔧 FABRIC WRAP! Line: "${currentLineText}" (${lineWidth.toFixed(1)}px)`);
25662
27122
  graphemeLines.push(line);
25663
27123
  line = [];
25664
- lineWidth = wordWidth;
27124
+ lineWidth = wordWidth; // Start new line with just this word
25665
27125
  lineJustStarted = true;
25666
27126
  } else {
25667
- lineWidth += additionalSpace;
27127
+ // Word fits, add it to current line
27128
+ lineWidth = potentialLineWidth + additionalSpace;
25668
27129
  }
25669
27130
  if (!lineJustStarted && !splitByGrapheme) {
25670
27131
  line.push(infix);
25671
27132
  }
25672
27133
  line = line.concat(word);
27134
+
27135
+ // Debug: show current line after adding word
27136
+ console.log(`🔧 FABRIC AFTER ADD: Line now: "${line.join('')}" (${line.length} chars)`);
25673
27137
  infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset);
25674
27138
  offset++;
25675
27139
  lineJustStarted = false;
@@ -25679,9 +27143,19 @@ class Textbox extends IText {
25679
27143
  // TODO: this code is probably not necessary anymore.
25680
27144
  // it can be moved out of this function since largestWordWidth is now
25681
27145
  // known in advance
25682
- if (largestWordWidth + reservedSpace > this.dynamicMinWidth) {
27146
+ // Don't modify dynamicMinWidth when using browser wrapping to prevent width increases
27147
+ if (!this._usingBrowserWrapping && largestWordWidth + reservedSpace > this.dynamicMinWidth) {
27148
+ console.log(`🔧 FABRIC updating dynamicMinWidth: ${this.dynamicMinWidth} -> ${largestWordWidth - additionalSpace + reservedSpace}`);
25683
27149
  this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace;
27150
+ } else if (this._usingBrowserWrapping) {
27151
+ console.log(`🔤 BROWSER WRAP: Skipping dynamicMinWidth update to prevent width increase`);
25684
27152
  }
27153
+
27154
+ // Debug: show final wrapped lines
27155
+ console.log(`🔧 FABRIC FINAL LINES: ${graphemeLines.length} lines`);
27156
+ graphemeLines.forEach((line, i) => {
27157
+ console.log(` Line ${i + 1}: "${line.join('')}" (${line.length} chars)`);
27158
+ });
25685
27159
  return graphemeLines;
25686
27160
  }
25687
27161
 
@@ -25725,6 +27199,260 @@ class Textbox extends IText {
25725
27199
  * @override
25726
27200
  */
25727
27201
  _splitTextIntoLines(text) {
27202
+ // Check if we need browser wrapping using smart font detection
27203
+ const needsBrowserWrapping = this.fontFamily && fontLacksEnglishGlyphsCached(this.fontFamily);
27204
+ if (needsBrowserWrapping) {
27205
+ // Cache key based on text content, width, font properties, AND text alignment
27206
+ const textHash = text.length + text.slice(0, 50); // Include text content in cache key
27207
+ const cacheKey = `${textHash}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
27208
+
27209
+ // Check if we have a cached result and nothing has changed
27210
+ if (this._browserWrapCache && this._browserWrapCache.key === cacheKey) {
27211
+ const cachedResult = this._browserWrapCache.result;
27212
+
27213
+ // For justify alignment, ensure we have the measurements
27214
+ if (this.textAlign.includes('justify') && !cachedResult.justifySpaceMeasurements) ; else {
27215
+ return cachedResult;
27216
+ }
27217
+ }
27218
+ const result = this._splitTextIntoLinesWithBrowser(text);
27219
+
27220
+ // Cache the result
27221
+ this._browserWrapCache = {
27222
+ key: cacheKey,
27223
+ result
27224
+ };
27225
+
27226
+ // Mark that we used browser wrapping to prevent dynamicMinWidth modifications
27227
+ this._usingBrowserWrapping = true;
27228
+ return result;
27229
+ }
27230
+
27231
+ // Clear the browser wrapping flag when using regular wrapping
27232
+ this._usingBrowserWrapping = false;
27233
+
27234
+ // Default Fabric wrapping for other fonts
27235
+ const newText = super._splitTextIntoLines(text),
27236
+ graphemeLines = this._wrapText(newText.lines, this.width),
27237
+ lines = new Array(graphemeLines.length);
27238
+ for (let i = 0; i < graphemeLines.length; i++) {
27239
+ lines[i] = graphemeLines[i].join('');
27240
+ }
27241
+ newText.lines = lines;
27242
+ newText.graphemeLines = graphemeLines;
27243
+ return newText;
27244
+ }
27245
+
27246
+ /**
27247
+ * Use browser's native text wrapping for accurate handling of fonts without English glyphs
27248
+ * @private
27249
+ */
27250
+ _splitTextIntoLinesWithBrowser(text) {
27251
+ if (typeof document === 'undefined') {
27252
+ // Fallback to regular wrapping in Node.js
27253
+ return this._splitTextIntoLinesDefault(text);
27254
+ }
27255
+
27256
+ // Create a hidden element that mimics the overlay editor
27257
+ const testElement = document.createElement('div');
27258
+ testElement.style.position = 'absolute';
27259
+ testElement.style.left = '-9999px';
27260
+ testElement.style.visibility = 'hidden';
27261
+ testElement.style.fontSize = `${this.fontSize}px`;
27262
+ testElement.style.fontFamily = `"${this.fontFamily}"`;
27263
+ testElement.style.fontWeight = String(this.fontWeight || 'normal');
27264
+ testElement.style.fontStyle = String(this.fontStyle || 'normal');
27265
+ testElement.style.lineHeight = String(this.lineHeight || 1.16);
27266
+ testElement.style.width = `${this.width}px`;
27267
+ testElement.style.direction = this.direction || 'ltr';
27268
+ testElement.style.whiteSpace = 'pre-wrap';
27269
+ testElement.style.wordBreak = 'normal';
27270
+ testElement.style.overflowWrap = 'break-word';
27271
+
27272
+ // Set browser-native text alignment (including justify)
27273
+ if (this.textAlign.includes('justify')) {
27274
+ testElement.style.textAlign = 'justify';
27275
+ testElement.style.textAlignLast = 'auto'; // Let browser decide last line alignment
27276
+ } else {
27277
+ testElement.style.textAlign = this.textAlign;
27278
+ }
27279
+ testElement.textContent = text;
27280
+ document.body.appendChild(testElement);
27281
+
27282
+ // Get the browser's natural line breaks
27283
+ const range = document.createRange();
27284
+ const lines = [];
27285
+ const graphemeLines = [];
27286
+ try {
27287
+ // Simple approach: split by measuring character positions
27288
+ const textNode = testElement.firstChild;
27289
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
27290
+ let currentLineStart = 0;
27291
+ const textLength = text.length;
27292
+ let previousBottom = 0;
27293
+ for (let i = 0; i <= textLength; i++) {
27294
+ range.setStart(textNode, currentLineStart);
27295
+ range.setEnd(textNode, i);
27296
+ const rect = range.getBoundingClientRect();
27297
+ if (i > currentLineStart && (rect.bottom > previousBottom + 5 || i === textLength)) {
27298
+ // New line detected or end of text
27299
+ const lineEnd = i === textLength ? i : i - 1;
27300
+ const lineText = text.substring(currentLineStart, lineEnd).trim();
27301
+ if (lineText) {
27302
+ lines.push(lineText);
27303
+ // Convert to graphemes for compatibility
27304
+ const graphemeLine = lineText.split('');
27305
+ graphemeLines.push(graphemeLine);
27306
+ }
27307
+ currentLineStart = lineEnd;
27308
+ previousBottom = rect.bottom;
27309
+ }
27310
+ }
27311
+ }
27312
+ } catch (error) {
27313
+ console.warn('Browser wrapping failed, using fallback:', error);
27314
+ document.body.removeChild(testElement);
27315
+ return this._splitTextIntoLinesDefault(text);
27316
+ }
27317
+
27318
+ // Extract actual browser height BEFORE removing element
27319
+ const actualBrowserHeight = testElement.scrollHeight;
27320
+ const offsetHeight = testElement.offsetHeight;
27321
+ const clientHeight = testElement.clientHeight;
27322
+ const boundingRect = testElement.getBoundingClientRect();
27323
+ console.log(`🔤 Browser element measurements:`);
27324
+ console.log(` scrollHeight: ${actualBrowserHeight}px (content + padding + hidden overflow)`);
27325
+ console.log(` offsetHeight: ${offsetHeight}px (content + padding + border)`);
27326
+ console.log(` clientHeight: ${clientHeight}px (content + padding, no border/scrollbar)`);
27327
+ console.log(` boundingRect.height: ${boundingRect.height}px (actual rendered height)`);
27328
+ console.log(` Font size: ${this.fontSize}px, Line height: ${this.lineHeight || 1.16}, Lines: ${lines.length}`);
27329
+
27330
+ // For justify alignment, extract space measurements from browser BEFORE removing element
27331
+ let justifySpaceMeasurements = null;
27332
+ if (this.textAlign.includes('justify')) {
27333
+ justifySpaceMeasurements = this._extractJustifySpaceMeasurements(testElement, lines);
27334
+ }
27335
+ document.body.removeChild(testElement);
27336
+ console.log(`🔤 Browser wrapping result: ${lines.length} lines`);
27337
+
27338
+ // Try different height measurements to find the most accurate
27339
+ let bestHeight = actualBrowserHeight;
27340
+
27341
+ // If scrollHeight and offsetHeight differ significantly, investigate
27342
+ if (Math.abs(actualBrowserHeight - offsetHeight) > 2) {
27343
+ console.log(`🔤 Height discrepancy detected: scrollHeight=${actualBrowserHeight}px vs offsetHeight=${offsetHeight}px`);
27344
+ }
27345
+
27346
+ // Consider using boundingRect height if it's larger (sometimes more accurate for visible content)
27347
+ if (boundingRect.height > bestHeight) {
27348
+ console.log(`🔤 Using boundingRect height (${boundingRect.height}px) instead of scrollHeight (${bestHeight}px)`);
27349
+ bestHeight = boundingRect.height;
27350
+ }
27351
+
27352
+ // Font-specific height adjustments for accurate bounding box
27353
+ let adjustedHeight = bestHeight;
27354
+
27355
+ // Fonts without English glyphs need additional height buffer due to different font metrics
27356
+ const lacksEnglishGlyphs = fontLacksEnglishGlyphsCached(this.fontFamily);
27357
+ if (lacksEnglishGlyphs) {
27358
+ const glyphBuffer = this.fontSize * 0.25; // 25% of font size for non-English fonts
27359
+ adjustedHeight = bestHeight + glyphBuffer;
27360
+ console.log(`🔤 Non-English font detected (${this.fontFamily}): Adding ${glyphBuffer}px buffer (${bestHeight}px + ${glyphBuffer}px = ${adjustedHeight}px)`);
27361
+ } else {
27362
+ console.log(`🔤 Standard font (${this.fontFamily}): Using browser height directly (${bestHeight}px)`);
27363
+ }
27364
+ return {
27365
+ _unwrappedLines: [text.split('')],
27366
+ lines: lines,
27367
+ graphemeText: text.split(''),
27368
+ graphemeLines: graphemeLines,
27369
+ justifySpaceMeasurements: justifySpaceMeasurements,
27370
+ actualBrowserHeight: adjustedHeight
27371
+ };
27372
+ }
27373
+
27374
+ /**
27375
+ * Extract justify space measurements from browser
27376
+ * @private
27377
+ */
27378
+ _extractJustifySpaceMeasurements(element, lines) {
27379
+ console.log(`🔤 Extracting browser justify space measurements for ${lines.length} lines`);
27380
+
27381
+ // For now, we'll use a simplified approach:
27382
+ // Apply uniform space expansion to match the line width
27383
+ const spaceWidths = [];
27384
+ lines.forEach((line, lineIndex) => {
27385
+ const lineSpaces = [];
27386
+ const spaceCount = (line.match(/\s/g) || []).length;
27387
+ if (spaceCount > 0 && lineIndex < lines.length - 1) {
27388
+ // Don't justify last line
27389
+ // Calculate how much space expansion is needed
27390
+ const normalSpaceWidth = 6.4; // Default space width for STV font
27391
+ const lineWidth = this.width;
27392
+
27393
+ // Estimate natural line width
27394
+ const charCount = line.length - spaceCount;
27395
+ const avgCharWidth = 12; // Approximate for STV font
27396
+
27397
+ // Calculate expanded space width
27398
+ const remainingSpace = lineWidth - charCount * avgCharWidth;
27399
+ const expandedSpaceWidth = remainingSpace / spaceCount;
27400
+ console.log(`🔤 Line ${lineIndex}: ${spaceCount} spaces, natural: ${normalSpaceWidth}px -> justified: ${expandedSpaceWidth.toFixed(1)}px`);
27401
+
27402
+ // Fill array with expanded space widths for this line
27403
+ for (let i = 0; i < spaceCount; i++) {
27404
+ lineSpaces.push(expandedSpaceWidth);
27405
+ }
27406
+ }
27407
+ spaceWidths.push(lineSpaces);
27408
+ });
27409
+ return spaceWidths;
27410
+ }
27411
+
27412
+ /**
27413
+ * Apply browser-calculated justify space measurements
27414
+ * @private
27415
+ */
27416
+ _applyBrowserJustifySpaces() {
27417
+ if (!this._textLines || !this.__charBounds) {
27418
+ console.warn('🔤 BROWSER JUSTIFY: _textLines or __charBounds not ready');
27419
+ return;
27420
+ }
27421
+
27422
+ // Get space measurements from browser wrapping result
27423
+ const styleMap = this._styleMap;
27424
+ if (!styleMap || !styleMap.justifySpaceMeasurements) {
27425
+ console.warn('🔤 BROWSER JUSTIFY: No justify space measurements available');
27426
+ return;
27427
+ }
27428
+ const spaceWidths = styleMap.justifySpaceMeasurements;
27429
+ console.log('🔤 BROWSER JUSTIFY: Applying space measurements to __charBounds');
27430
+
27431
+ // Apply space widths to character bounds
27432
+ this._textLines.forEach((line, lineIndex) => {
27433
+ if (!this.__charBounds || !this.__charBounds[lineIndex] || !spaceWidths[lineIndex]) return;
27434
+ const lineBounds = this.__charBounds[lineIndex];
27435
+ const lineSpaceWidths = spaceWidths[lineIndex];
27436
+ let spaceIndex = 0;
27437
+ for (let charIndex = 0; charIndex < line.length; charIndex++) {
27438
+ if (/\s/.test(line[charIndex]) && spaceIndex < lineSpaceWidths.length) {
27439
+ const expandedWidth = lineSpaceWidths[spaceIndex];
27440
+ if (lineBounds[charIndex]) {
27441
+ const oldWidth = lineBounds[charIndex].width;
27442
+ lineBounds[charIndex].width = expandedWidth;
27443
+ console.log(`🔤 Line ${lineIndex} space ${spaceIndex}: ${oldWidth.toFixed(1)}px -> ${expandedWidth.toFixed(1)}px`);
27444
+ }
27445
+ spaceIndex++;
27446
+ }
27447
+ }
27448
+ });
27449
+ }
27450
+
27451
+ /**
27452
+ * Fallback to default Fabric wrapping
27453
+ * @private
27454
+ */
27455
+ _splitTextIntoLinesDefault(text) {
25728
27456
  const newText = super._splitTextIntoLines(text),
25729
27457
  graphemeLines = this._wrapText(newText.lines, this.width),
25730
27458
  lines = new Array(graphemeLines.length);
@@ -25754,6 +27482,204 @@ class Textbox extends IText {
25754
27482
  }
25755
27483
  }
25756
27484
 
27485
+ /**
27486
+ * Initialize event listeners for safety snap functionality
27487
+ * @private
27488
+ */
27489
+ initializeEventListeners() {
27490
+ var _this$canvas4;
27491
+ // Track which side is being used for resize to handle position compensation
27492
+ let resizeOrigin = null;
27493
+
27494
+ // Detect resize origin during resizing
27495
+ this.on('resizing', e => {
27496
+ // Check transform origin to determine which side is being resized
27497
+ if (e.transform) {
27498
+ const {
27499
+ originX
27500
+ } = e.transform;
27501
+ // originX tells us which side is the anchor - opposite side is being dragged
27502
+ resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
27503
+ } else if (e.originX) {
27504
+ const {
27505
+ originX
27506
+ } = e;
27507
+ resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
27508
+ }
27509
+ });
27510
+
27511
+ // Only trigger safety snap after resize is complete (not during)
27512
+ // Use 'modified' event which fires after user releases the mouse
27513
+ this.on('modified', () => {
27514
+ const currentResizeOrigin = resizeOrigin; // Capture the value before reset
27515
+ // Small delay to ensure text layout is updated
27516
+ setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
27517
+ resizeOrigin = null; // Reset after capturing
27518
+ });
27519
+
27520
+ // Also listen to canvas-level modified event as backup
27521
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.on('object:modified', e => {
27522
+ if (e.target === this) {
27523
+ const currentResizeOrigin = resizeOrigin; // Capture the value before reset
27524
+ setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
27525
+ resizeOrigin = null; // Reset after capturing
27526
+ }
27527
+ });
27528
+ }
27529
+
27530
+ /**
27531
+ * Safety snap to prevent glyph clipping after manual resize.
27532
+ * Similar to Polotno - checks if any glyphs are too close to edges
27533
+ * and automatically expands width if needed.
27534
+ * @private
27535
+ * @param resizeOrigin - Which side was used for resizing ('left' or 'right')
27536
+ */
27537
+ safetySnapWidth(resizeOrigin) {
27538
+ // For Textbox objects, we always want to check for clipping regardless of isWrapping flag
27539
+ if (!this._textLines || this.type.toLowerCase() !== 'textbox' || this._textLines.length === 0) {
27540
+ return;
27541
+ }
27542
+ const lineCount = this._textLines.length;
27543
+ if (lineCount === 0) return;
27544
+ let maxRequiredWidth = 0; // Width including RTL buffer
27545
+
27546
+ for (let i = 0; i < lineCount; i++) {
27547
+ const lineText = this._textLines[i].join(''); // Convert grapheme array to string
27548
+ const lineWidth = this.getLineWidth(i);
27549
+
27550
+ // RTL detection - regex for Arabic, Hebrew, and other RTL characters
27551
+ const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/;
27552
+ if (rtlRegex.test(lineText)) {
27553
+ // Add minimal RTL compensation buffer - just enough to prevent clipping
27554
+ const rtlBuffer = (this.fontSize || 16) * 0.15; // 15% of font size (much smaller)
27555
+ maxRequiredWidth = Math.max(maxRequiredWidth, lineWidth + rtlBuffer);
27556
+ } else {
27557
+ maxRequiredWidth = Math.max(maxRequiredWidth, lineWidth);
27558
+ }
27559
+ }
27560
+
27561
+ // Safety margin - how close glyphs can get before we snap
27562
+ const safetyThreshold = 2; // px - very subtle trigger
27563
+
27564
+ if (maxRequiredWidth > this.width - safetyThreshold) {
27565
+ var _this$canvas5;
27566
+ // Set width to exactly what's needed + minimal safety margin
27567
+ const newWidth = maxRequiredWidth + 1; // Add just 1px safety margin
27568
+
27569
+ // Store original position before width change
27570
+ const originalLeft = this.left;
27571
+ const originalTop = this.top;
27572
+ const widthIncrease = newWidth - this.width;
27573
+
27574
+ // Change width
27575
+ this.set('width', newWidth);
27576
+
27577
+ // Force text layout recalculation
27578
+ this.initDimensions();
27579
+
27580
+ // Only compensate position when resizing from left handle
27581
+ // Right handle resize doesn't shift the text position
27582
+ if (resizeOrigin === 'left') {
27583
+ // When resizing from left, the expansion pushes text right
27584
+ // Compensate by moving the textbox left by the width increase
27585
+ this.set({
27586
+ 'left': originalLeft - widthIncrease,
27587
+ 'top': originalTop
27588
+ });
27589
+ }
27590
+ this.setCoords();
27591
+
27592
+ // Also refresh the overlay editor if it exists
27593
+ if (this.__overlayEditor) {
27594
+ setTimeout(() => {
27595
+ this.__overlayEditor.refresh();
27596
+ }, 0);
27597
+ }
27598
+ (_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
27599
+ }
27600
+ }
27601
+
27602
+ /**
27603
+ * Fix character selection mismatch after JSON loading for browser-wrapped fonts
27604
+ * @private
27605
+ */
27606
+ _fixCharacterMappingAfterJsonLoad() {
27607
+ if (this._usingBrowserWrapping) {
27608
+ // Clear all cached states to force fresh text layout calculation
27609
+ this._browserWrapCache = null;
27610
+ this._lastDimensionState = null;
27611
+
27612
+ // Force complete re-initialization
27613
+ this.initDimensions();
27614
+ this._forceClearCache = true;
27615
+
27616
+ // Ensure canvas refresh
27617
+ this.setCoords();
27618
+ if (this.canvas) {
27619
+ this.canvas.requestRenderAll();
27620
+ }
27621
+ }
27622
+ }
27623
+
27624
+ /**
27625
+ * Force complete textbox re-initialization (useful after JSON loading)
27626
+ * Overrides Text version with Textbox-specific logic
27627
+ */
27628
+ forceTextReinitialization() {
27629
+ console.log('🔄 Force reinitializing Textbox object');
27630
+
27631
+ // CRITICAL: Ensure textbox is marked as initialized
27632
+ this.initialized = true;
27633
+
27634
+ // Clear all caches and force dirty state
27635
+ this._clearCache();
27636
+ this.dirty = true;
27637
+ this.dynamicMinWidth = 0;
27638
+
27639
+ // Force isEditing false to ensure clean state
27640
+ this.isEditing = false;
27641
+ console.log(' → Set initialized=true, dirty=true, cleared caches');
27642
+
27643
+ // Re-initialize dimensions (this will handle justify properly)
27644
+ this.initDimensions();
27645
+
27646
+ // Double-check that justify was applied by checking space widths
27647
+ if (this.textAlign.includes('justify') && this.__charBounds) {
27648
+ setTimeout(() => {
27649
+ var _this$canvas6;
27650
+ // Verify justify was applied by checking if space widths vary
27651
+ let hasVariableSpaces = false;
27652
+ this.__charBounds.forEach((lineBounds, i) => {
27653
+ if (lineBounds && this._textLines && this._textLines[i]) {
27654
+ const spaces = lineBounds.filter((bound, j) => /\s/.test(this._textLines[i][j]));
27655
+ if (spaces.length > 1) {
27656
+ const firstSpaceWidth = spaces[0].width;
27657
+ hasVariableSpaces = spaces.some(space => Math.abs(space.width - firstSpaceWidth) > 0.1);
27658
+ }
27659
+ }
27660
+ });
27661
+ if (!hasVariableSpaces && this.__charBounds.length > 0) {
27662
+ console.warn(' ⚠️ Justify spaces still uniform - forcing enlargeSpaces again');
27663
+ if (this.enlargeSpaces) {
27664
+ this.enlargeSpaces();
27665
+ }
27666
+ } else {
27667
+ console.log(' ✅ Justify spaces properly expanded');
27668
+ }
27669
+
27670
+ // Ensure height is recalculated - use browser height if available
27671
+ if (this._usingBrowserWrapping && this._actualBrowserHeight) {
27672
+ this.height = this._actualBrowserHeight;
27673
+ console.log(`🔤 JUSTIFY: Preserved browser height: ${this.height}px`);
27674
+ } else {
27675
+ this.height = this.calcTextHeight();
27676
+ console.log(`🔧 JUSTIFY: Used calcTextHeight: ${this.height}px`);
27677
+ }
27678
+ (_this$canvas6 = this.canvas) === null || _this$canvas6 === void 0 || _this$canvas6.requestRenderAll();
27679
+ }, 10);
27680
+ }
27681
+ }
27682
+
25757
27683
  /**
25758
27684
  * Returns object representation of an instance
25759
27685
  * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output