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