@nasser-sw/fabric 7.0.1-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 (181) 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 +1853 -288
  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 +1853 -288
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/index.node.cjs +1853 -288
  17. package/dist/index.node.cjs.map +1 -1
  18. package/dist/index.node.mjs +1853 -288
  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 +43 -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 +521 -67
  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.map +1 -1
  61. package/dist/src/text/overlayEditor.min.mjs +1 -1
  62. package/dist/src/text/overlayEditor.min.mjs.map +1 -1
  63. package/dist/src/text/overlayEditor.mjs +155 -9
  64. package/dist/src/text/overlayEditor.mjs.map +1 -1
  65. package/dist/src/text/scriptUtils.d.ts +142 -0
  66. package/dist/src/text/scriptUtils.d.ts.map +1 -0
  67. package/dist/src/text/scriptUtils.min.mjs +2 -0
  68. package/dist/src/text/scriptUtils.min.mjs.map +1 -0
  69. package/dist/src/text/scriptUtils.mjs +212 -0
  70. package/dist/src/text/scriptUtils.mjs.map +1 -0
  71. package/dist/src/util/misc/cornerRadius.d.ts +70 -0
  72. package/dist/src/util/misc/cornerRadius.d.ts.map +1 -0
  73. package/dist/src/util/misc/cornerRadius.min.mjs +2 -0
  74. package/dist/src/util/misc/cornerRadius.min.mjs.map +1 -0
  75. package/dist/src/util/misc/cornerRadius.mjs +181 -0
  76. package/dist/src/util/misc/cornerRadius.mjs.map +1 -0
  77. package/dist-extensions/src/shapes/CustomLine.d.ts +10 -0
  78. package/dist-extensions/src/shapes/CustomLine.d.ts.map +1 -0
  79. package/dist-extensions/src/shapes/Line.d.ts +33 -86
  80. package/dist-extensions/src/shapes/Line.d.ts.map +1 -1
  81. package/dist-extensions/src/shapes/Polyline.d.ts +7 -0
  82. package/dist-extensions/src/shapes/Polyline.d.ts.map +1 -1
  83. package/dist-extensions/src/shapes/Text/Text.d.ts +19 -0
  84. package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
  85. package/dist-extensions/src/shapes/Textbox.d.ts +43 -1
  86. package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
  87. package/dist-extensions/src/shapes/Triangle.d.ts +27 -2
  88. package/dist-extensions/src/shapes/Triangle.d.ts.map +1 -1
  89. package/dist-extensions/src/text/measure.d.ts +9 -0
  90. package/dist-extensions/src/text/measure.d.ts.map +1 -1
  91. package/dist-extensions/src/text/overlayEditor.d.ts.map +1 -1
  92. package/dist-extensions/src/text/scriptUtils.d.ts +142 -0
  93. package/dist-extensions/src/text/scriptUtils.d.ts.map +1 -0
  94. package/dist-extensions/src/util/misc/cornerRadius.d.ts +70 -0
  95. package/dist-extensions/src/util/misc/cornerRadius.d.ts.map +1 -0
  96. package/fabric-test-editor.html +3552 -0
  97. package/fabric-test2.html +647 -0
  98. package/fabric.ts +182 -182
  99. package/fonts/STV Bold.ttf +0 -0
  100. package/fonts/STV Light.ttf +0 -0
  101. package/fonts/STV Regular.ttf +0 -0
  102. package/package.json +164 -164
  103. package/src/shapes/Line.ts +484 -157
  104. package/src/shapes/Polyline.ts +70 -29
  105. package/src/shapes/Text/Text.ts +317 -19
  106. package/src/shapes/Textbox.ts +544 -12
  107. package/src/shapes/Triangle.spec.ts +76 -0
  108. package/src/shapes/Triangle.ts +85 -15
  109. package/src/text/measure.ts +200 -50
  110. package/src/text/overlayEditor.ts +164 -12
  111. package/src/util/misc/cornerRadius.spec.ts +141 -0
  112. package/src/util/misc/cornerRadius.ts +269 -0
  113. /package/debug/{konva → konva-master}/LICENSE +0 -0
  114. /package/debug/{konva → konva-master}/gulpfile.mjs +0 -0
  115. /package/debug/{konva → konva-master}/resources/doc-includes/ContainerParams.txt +0 -0
  116. /package/debug/{konva → konva-master}/resources/doc-includes/NodeParams.txt +0 -0
  117. /package/debug/{konva → konva-master}/resources/doc-includes/ShapeParams.txt +0 -0
  118. /package/debug/{konva → konva-master}/resources/jsdoc.conf.json +0 -0
  119. /package/debug/{konva → konva-master}/rollup.config.mjs +0 -0
  120. /package/debug/{konva → konva-master}/src/Animation.ts +0 -0
  121. /package/debug/{konva → konva-master}/src/BezierFunctions.ts +0 -0
  122. /package/debug/{konva → konva-master}/src/Container.ts +0 -0
  123. /package/debug/{konva → konva-master}/src/Context.ts +0 -0
  124. /package/debug/{konva → konva-master}/src/Core.ts +0 -0
  125. /package/debug/{konva → konva-master}/src/DragAndDrop.ts +0 -0
  126. /package/debug/{konva → konva-master}/src/Factory.ts +0 -0
  127. /package/debug/{konva → konva-master}/src/FastLayer.ts +0 -0
  128. /package/debug/{konva → konva-master}/src/Global.ts +0 -0
  129. /package/debug/{konva → konva-master}/src/Group.ts +0 -0
  130. /package/debug/{konva → konva-master}/src/Layer.ts +0 -0
  131. /package/debug/{konva → konva-master}/src/Node.ts +0 -0
  132. /package/debug/{konva → konva-master}/src/PointerEvents.ts +0 -0
  133. /package/debug/{konva → konva-master}/src/Shape.ts +0 -0
  134. /package/debug/{konva → konva-master}/src/Stage.ts +0 -0
  135. /package/debug/{konva → konva-master}/src/Tween.ts +0 -0
  136. /package/debug/{konva → konva-master}/src/Util.ts +0 -0
  137. /package/debug/{konva → konva-master}/src/Validators.ts +0 -0
  138. /package/debug/{konva → konva-master}/src/_CoreInternals.ts +0 -0
  139. /package/debug/{konva → konva-master}/src/_FullInternals.ts +0 -0
  140. /package/debug/{konva → konva-master}/src/canvas-backend.ts +0 -0
  141. /package/debug/{konva → konva-master}/src/filters/Blur.ts +0 -0
  142. /package/debug/{konva → konva-master}/src/filters/Brighten.ts +0 -0
  143. /package/debug/{konva → konva-master}/src/filters/Brightness.ts +0 -0
  144. /package/debug/{konva → konva-master}/src/filters/Contrast.ts +0 -0
  145. /package/debug/{konva → konva-master}/src/filters/Emboss.ts +0 -0
  146. /package/debug/{konva → konva-master}/src/filters/Enhance.ts +0 -0
  147. /package/debug/{konva → konva-master}/src/filters/Grayscale.ts +0 -0
  148. /package/debug/{konva → konva-master}/src/filters/HSL.ts +0 -0
  149. /package/debug/{konva → konva-master}/src/filters/HSV.ts +0 -0
  150. /package/debug/{konva → konva-master}/src/filters/Invert.ts +0 -0
  151. /package/debug/{konva → konva-master}/src/filters/Kaleidoscope.ts +0 -0
  152. /package/debug/{konva → konva-master}/src/filters/Mask.ts +0 -0
  153. /package/debug/{konva → konva-master}/src/filters/Noise.ts +0 -0
  154. /package/debug/{konva → konva-master}/src/filters/Pixelate.ts +0 -0
  155. /package/debug/{konva → konva-master}/src/filters/Posterize.ts +0 -0
  156. /package/debug/{konva → konva-master}/src/filters/RGB.ts +0 -0
  157. /package/debug/{konva → konva-master}/src/filters/RGBA.ts +0 -0
  158. /package/debug/{konva → konva-master}/src/filters/Sepia.ts +0 -0
  159. /package/debug/{konva → konva-master}/src/filters/Solarize.ts +0 -0
  160. /package/debug/{konva → konva-master}/src/filters/Threshold.ts +0 -0
  161. /package/debug/{konva → konva-master}/src/index.ts +0 -0
  162. /package/debug/{konva → konva-master}/src/shapes/Arc.ts +0 -0
  163. /package/debug/{konva → konva-master}/src/shapes/Arrow.ts +0 -0
  164. /package/debug/{konva → konva-master}/src/shapes/Circle.ts +0 -0
  165. /package/debug/{konva → konva-master}/src/shapes/Ellipse.ts +0 -0
  166. /package/debug/{konva → konva-master}/src/shapes/Image.ts +0 -0
  167. /package/debug/{konva → konva-master}/src/shapes/Label.ts +0 -0
  168. /package/debug/{konva → konva-master}/src/shapes/Line.ts +0 -0
  169. /package/debug/{konva → konva-master}/src/shapes/Path.ts +0 -0
  170. /package/debug/{konva → konva-master}/src/shapes/Rect.ts +0 -0
  171. /package/debug/{konva → konva-master}/src/shapes/RegularPolygon.ts +0 -0
  172. /package/debug/{konva → konva-master}/src/shapes/Ring.ts +0 -0
  173. /package/debug/{konva → konva-master}/src/shapes/Sprite.ts +0 -0
  174. /package/debug/{konva → konva-master}/src/shapes/Star.ts +0 -0
  175. /package/debug/{konva → konva-master}/src/shapes/TextPath.ts +0 -0
  176. /package/debug/{konva → konva-master}/src/shapes/Transformer.ts +0 -0
  177. /package/debug/{konva → konva-master}/src/shapes/Wedge.ts +0 -0
  178. /package/debug/{konva → konva-master}/src/skia-backend.ts +0 -0
  179. /package/debug/{konva → konva-master}/src/types.ts +0 -0
  180. /package/debug/{konva → konva-master}/tsconfig.json +0 -0
  181. /package/debug/{konva → konva-master}/tsconfig.test.json +0 -0
@@ -410,7 +410,7 @@ class Cache {
410
410
  }
411
411
  const cache = new Cache();
412
412
 
413
- var version = "7.0.0-beta1";
413
+ var version = "7.0.1-beta9";
414
414
 
415
415
  // use this syntax so babel plugin see this import here
416
416
  const VERSION = version;
@@ -17627,33 +17627,30 @@ class PatternBrush extends PencilBrush {
17627
17627
  }
17628
17628
  }
17629
17629
 
17630
- // @TODO this code is terrible and Line should be a special case of polyline.
17631
-
17632
17630
  const coordProps = ['x1', 'x2', 'y1', 'y2'];
17633
- /**
17634
- * A Class to draw a line
17635
- * A bunch of methods will be added to Polyline to handle the line case
17636
- * The line class is very strange to work with, is all special, it hardly aligns
17637
- * to what a developer want everytime there is an angle
17638
- * @deprecated
17639
- */
17640
17631
  class Line extends FabricObject {
17641
- /**
17642
- * Constructor
17643
- * @param {Array} [points] Array of points
17644
- * @param {Object} [options] Options object
17645
- * @return {Line} thisArg
17646
- */
17647
17632
  constructor() {
17648
- let [x1, y1, x2, y2] = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0, 0, 0];
17633
+ let [x1, y1, x2, y2] = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0, 100, 0];
17649
17634
  let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
17650
17635
  super();
17651
- Object.assign(this, Line.ownDefaults);
17636
+ _defineProperty(this, "hitStrokeWidth", 'auto');
17637
+ _defineProperty(this, "_updatingEndpoints", false);
17638
+ _defineProperty(this, "_useEndpointCoords", true);
17639
+ _defineProperty(this, "_exportingSVG", false);
17652
17640
  this.setOptions(options);
17653
17641
  this.x1 = x1;
17654
17642
  this.x2 = x2;
17655
17643
  this.y1 = y1;
17656
17644
  this.y2 = y2;
17645
+ if (options.hitStrokeWidth !== undefined) {
17646
+ this.hitStrokeWidth = options.hitStrokeWidth;
17647
+ }
17648
+ this.hasBorders = false;
17649
+ this.hasControls = true;
17650
+ this.selectable = true;
17651
+ this.hoverCursor = 'move';
17652
+ this.perPixelTargetFind = false;
17653
+ this.strokeLineCap = 'butt';
17657
17654
  this._setWidthHeight();
17658
17655
  const {
17659
17656
  left,
@@ -17661,129 +17658,384 @@ class Line extends FabricObject {
17661
17658
  } = options;
17662
17659
  typeof left === 'number' && this.set(LEFT, left);
17663
17660
  typeof top === 'number' && this.set(TOP, top);
17661
+ this._setupLineControls();
17662
+ }
17663
+ _setupLineControls() {
17664
+ this.controls = {
17665
+ p1: new Control({
17666
+ x: 0,
17667
+ y: 0,
17668
+ cursorStyle: 'move',
17669
+ actionHandler: this._endpointActionHandler.bind(this),
17670
+ positionHandler: this._p1PositionHandler.bind(this),
17671
+ render: this._renderEndpointControl.bind(this),
17672
+ sizeX: 12,
17673
+ sizeY: 12
17674
+ }),
17675
+ p2: new Control({
17676
+ x: 0,
17677
+ y: 0,
17678
+ cursorStyle: 'move',
17679
+ actionHandler: this._endpointActionHandler.bind(this),
17680
+ positionHandler: this._p2PositionHandler.bind(this),
17681
+ render: this._renderEndpointControl.bind(this),
17682
+ sizeX: 12,
17683
+ sizeY: 12
17684
+ })
17685
+ };
17686
+ }
17687
+ _p1PositionHandler() {
17688
+ return new Point(this.x1, this.y1).transform(this.getViewportTransform());
17689
+ }
17690
+ _p2PositionHandler() {
17691
+ return new Point(this.x2, this.y2).transform(this.getViewportTransform());
17692
+ }
17693
+ _renderEndpointControl(ctx, left, top) {
17694
+ const size = 12;
17695
+ ctx.save();
17696
+ ctx.fillStyle = '#007bff';
17697
+ ctx.strokeStyle = '#ffffff';
17698
+ ctx.lineWidth = 2;
17699
+ ctx.beginPath();
17700
+ ctx.arc(left, top, size / 2, 0, 2 * Math.PI);
17701
+ ctx.fill();
17702
+ ctx.stroke();
17703
+ ctx.restore();
17704
+ }
17705
+ drawBorders(ctx) {
17706
+ let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
17707
+ if (this._useEndpointCoords) {
17708
+ this._drawLineBorders(ctx, styleOverride);
17709
+ return this;
17710
+ }
17711
+ return super.drawBorders(ctx, styleOverride, {});
17712
+ }
17713
+ _drawLineBorders(ctx) {
17714
+ var _this$canvas;
17715
+ let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
17716
+ const vpt = ((_this$canvas = this.canvas) === null || _this$canvas === void 0 ? void 0 : _this$canvas.viewportTransform) || [1, 0, 0, 1, 0, 0];
17717
+ ctx.save();
17718
+ ctx.setTransform(vpt[0], vpt[1], vpt[2], vpt[3], vpt[4], vpt[5]);
17719
+ ctx.strokeStyle = styleOverride.borderColor || this.borderColor || 'rgba(100, 200, 200, 0.5)';
17720
+ ctx.lineWidth = (this.strokeWidth || 1) + 5;
17721
+ ctx.lineCap = this.strokeLineCap || 'butt';
17722
+ ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
17723
+ ctx.beginPath();
17724
+ ctx.moveTo(this.x1, this.y1);
17725
+ ctx.lineTo(this.x2, this.y2);
17726
+ ctx.stroke();
17727
+ ctx.restore();
17728
+ }
17729
+ _renderControls(ctx) {
17730
+ let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
17731
+ ctx.save();
17732
+ ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
17733
+ this.drawControls(ctx, styleOverride);
17734
+ ctx.restore();
17735
+ }
17736
+ getBoundingRect() {
17737
+ if (this._useEndpointCoords) {
17738
+ const {
17739
+ x1,
17740
+ y1,
17741
+ x2,
17742
+ y2
17743
+ } = this;
17744
+ const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
17745
+ const padding = Math.max(effectiveStrokeWidth / 2 + 5, 10);
17746
+ return {
17747
+ left: Math.min(x1, x2) - padding,
17748
+ top: Math.min(y1, y2) - padding,
17749
+ width: Math.abs(x2 - x1) + padding * 2 || padding * 2,
17750
+ height: Math.abs(y2 - y1) + padding * 2 || padding * 2
17751
+ };
17752
+ }
17753
+ return super.getBoundingRect();
17664
17754
  }
17755
+ setCoords() {
17756
+ if (this._useEndpointCoords) {
17757
+ // Set width and height for hit detection and bounding box
17758
+ const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
17759
+ const hitPadding = Math.max(effectiveStrokeWidth / 2 + 5, 10);
17760
+ this.width = Math.abs(this.x2 - this.x1) + hitPadding * 2;
17761
+ this.height = Math.abs(this.y2 - this.y1) + hitPadding * 2;
17665
17762
 
17666
- /**
17667
- * @private
17668
- * @param {Object} [options] Options
17669
- */
17670
- _setWidthHeight() {
17671
- const {
17672
- x1,
17673
- y1,
17674
- x2,
17675
- y2
17676
- } = this;
17677
- this.width = Math.abs(x2 - x1);
17678
- this.height = Math.abs(y2 - y1);
17679
- const {
17680
- left,
17681
- top,
17682
- width,
17683
- height
17684
- } = makeBoundingBoxFromPoints([{
17685
- x: x1,
17686
- y: y1
17687
- }, {
17688
- x: x2,
17689
- y: y2
17690
- }]);
17691
- const position = new Point(left + width / 2, top + height / 2);
17692
- this.setPositionByOrigin(position, CENTER, CENTER);
17763
+ // Only update left/top if they haven't been explicitly set (e.g., during loading)
17764
+ if (this.left === 0 && this.top === 0) {
17765
+ const center = this._findCenterFromElement();
17766
+ this.left = center.x;
17767
+ this.top = center.y;
17768
+ }
17769
+ }
17770
+ super.setCoords();
17693
17771
  }
17772
+ getCoords() {
17773
+ if (this._useEndpointCoords) {
17774
+ const deltaX = this.x2 - this.x1;
17775
+ const deltaY = this.y2 - this.y1;
17776
+ const length = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
17777
+ if (length === 0) {
17778
+ return super.getCoords();
17779
+ }
17780
+ const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
17781
+ const halfWidth = Math.max(effectiveStrokeWidth / 2 + 2, 5);
17694
17782
 
17695
- /**
17696
- * @private
17697
- * @param {String} key
17698
- * @param {*} value
17699
- */
17783
+ // Unit vector perpendicular to line
17784
+ const perpX = -deltaY / length;
17785
+ const perpY = deltaX / length;
17786
+
17787
+ // Four corners of oriented rectangle
17788
+ 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)];
17789
+ }
17790
+ return super.getCoords();
17791
+ }
17792
+ containsPoint(point) {
17793
+ if (this._useEndpointCoords) {
17794
+ var _this$canvas2;
17795
+ if (((_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 ? void 0 : _this$canvas2.getActiveObject()) === this) {
17796
+ return super.containsPoint(point);
17797
+ }
17798
+ const distance = this._distanceToLineSegment(point.x, point.y);
17799
+ const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth || 1;
17800
+ const tolerance = Math.max(effectiveStrokeWidth / 2 + 2, 5);
17801
+ return distance <= tolerance;
17802
+ }
17803
+ return super.containsPoint(point);
17804
+ }
17805
+ _distanceToLineSegment(px, py) {
17806
+ const x1 = this.x1,
17807
+ y1 = this.y1,
17808
+ x2 = this.x2,
17809
+ y2 = this.y2;
17810
+ const pd2 = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
17811
+ if (pd2 === 0) {
17812
+ return Math.sqrt((px - x1) * (px - x1) + (py - y1) * (py - y1));
17813
+ }
17814
+ const u = ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / pd2;
17815
+ let closestX, closestY;
17816
+ if (u < 0) {
17817
+ closestX = x1;
17818
+ closestY = y1;
17819
+ } else if (u > 1) {
17820
+ closestX = x2;
17821
+ closestY = y2;
17822
+ } else {
17823
+ closestX = x1 + u * (x2 - x1);
17824
+ closestY = y1 + u * (y2 - y1);
17825
+ }
17826
+ return Math.sqrt((px - closestX) * (px - closestX) + (py - closestY) * (py - closestY));
17827
+ }
17828
+ _endpointActionHandler(eventData, transformData, x, y) {
17829
+ var _this$canvas4;
17830
+ const controlKey = transformData.corner;
17831
+ const pointer = new Point(x, y);
17832
+ let newX = pointer.x;
17833
+ let newY = pointer.y;
17834
+ if (eventData.shiftKey) {
17835
+ const otherControl = controlKey === 'p1' ? 'p2' : 'p1';
17836
+ const otherX = this[otherControl === 'p1' ? 'x1' : 'x2'];
17837
+ const otherY = this[otherControl === 'p1' ? 'y1' : 'y2'];
17838
+ const snapped = this._snapToAngle(otherX, otherY, newX, newY);
17839
+ newX = snapped.x;
17840
+ newY = snapped.y;
17841
+ }
17842
+ if (this._useEndpointCoords) {
17843
+ var _this$canvas3;
17844
+ if (controlKey === 'p1') {
17845
+ this.x1 = newX;
17846
+ this.y1 = newY;
17847
+ } else if (controlKey === 'p2') {
17848
+ this.x2 = newX;
17849
+ this.y2 = newY;
17850
+ }
17851
+
17852
+ // Update gradient coordinates if stroke is a gradient (but not during SVG export)
17853
+ if (this.stroke instanceof Gradient && !this._exportingSVG) {
17854
+ this.stroke.coords.x1 = this.x1;
17855
+ this.stroke.coords.y1 = this.y1;
17856
+ this.stroke.coords.x2 = this.x2;
17857
+ this.stroke.coords.y2 = this.y2;
17858
+ }
17859
+ this.dirty = true;
17860
+ this.setCoords();
17861
+ (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
17862
+ return true;
17863
+ }
17864
+
17865
+ // Fallback for old system
17866
+ this._updatingEndpoints = true;
17867
+ if (controlKey === 'p1') {
17868
+ this.x1 = newX;
17869
+ this.y1 = newY;
17870
+ } else if (controlKey === 'p2') {
17871
+ this.x2 = newX;
17872
+ this.y2 = newY;
17873
+ }
17874
+ this._setWidthHeight();
17875
+ this.dirty = true;
17876
+ this._updatingEndpoints = false;
17877
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.requestRenderAll();
17878
+ this.fire('modified', {
17879
+ transform: transformData,
17880
+ target: this,
17881
+ e: eventData
17882
+ });
17883
+ return true;
17884
+ }
17885
+ _snapToAngle(fromX, fromY, toX, toY) {
17886
+ const deltaX = toX - fromX;
17887
+ const deltaY = toY - fromY;
17888
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
17889
+ if (distance === 0) return {
17890
+ x: toX,
17891
+ y: toY
17892
+ };
17893
+ let angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
17894
+ const snapIncrement = 15;
17895
+ const snappedAngle = Math.round(angle / snapIncrement) * snapIncrement;
17896
+ const snappedRadians = snappedAngle * (Math.PI / 180);
17897
+ return {
17898
+ x: fromX + Math.cos(snappedRadians) * distance,
17899
+ y: fromY + Math.sin(snappedRadians) * distance
17900
+ };
17901
+ }
17902
+ _setWidthHeight() {
17903
+ let skipReposition = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
17904
+ this.width = Math.abs(this.x2 - this.x1) || 1;
17905
+ this.height = Math.abs(this.y2 - this.y1) || 1;
17906
+ if (!skipReposition && !this._updatingEndpoints) {
17907
+ const {
17908
+ left,
17909
+ top,
17910
+ width,
17911
+ height
17912
+ } = makeBoundingBoxFromPoints([{
17913
+ x: this.x1,
17914
+ y: this.y1
17915
+ }, {
17916
+ x: this.x2,
17917
+ y: this.y2
17918
+ }]);
17919
+ this.setPositionByOrigin(new Point(left + width / 2, top + height / 2), CENTER, CENTER);
17920
+ }
17921
+ }
17700
17922
  _set(key, value) {
17923
+ const oldLeft = this.left;
17924
+ const oldTop = this.top;
17701
17925
  super._set(key, value);
17702
17926
  if (coordProps.includes(key)) {
17703
- // this doesn't make sense very much, since setting x1 when top or left
17704
- // are already set, is just going to show a strange result since the
17705
- // line will move way more than the developer expect.
17706
- // in fabric5 it worked only when the line didn't have extra transformations,
17707
- // in fabric6 too. With extra transform they behave bad in different ways.
17708
- // This needs probably a good rework or a tutorial if you have to create a dynamic line
17709
17927
  this._setWidthHeight();
17928
+ this.dirty = true;
17929
+
17930
+ // Update gradient coordinates if stroke is a gradient (but not during SVG export)
17931
+ if (this.stroke instanceof Gradient && !this._exportingSVG) {
17932
+ this.stroke.coords.x1 = this.x1;
17933
+ this.stroke.coords.y1 = this.y1;
17934
+ this.stroke.coords.x2 = this.x2;
17935
+ this.stroke.coords.y2 = this.y2;
17936
+ }
17937
+ }
17938
+ if ((key === 'left' || key === 'top') && this.canvas && !this._updatingEndpoints) {
17939
+ const deltaX = this.left - oldLeft;
17940
+ const deltaY = this.top - oldTop;
17941
+ if (deltaX !== 0 || deltaY !== 0) {
17942
+ this._updatingEndpoints = true;
17943
+ this.x1 += deltaX;
17944
+ this.y1 += deltaY;
17945
+ this.x2 += deltaX;
17946
+ this.y2 += deltaY;
17947
+
17948
+ // Update gradient coordinates if stroke is a gradient
17949
+ if (this.stroke instanceof Gradient) {
17950
+ this.stroke.coords.x1 = this.x1;
17951
+ this.stroke.coords.y1 = this.y1;
17952
+ this.stroke.coords.x2 = this.x2;
17953
+ this.stroke.coords.y2 = this.y2;
17954
+ }
17955
+ this._updatingEndpoints = false;
17956
+ }
17710
17957
  }
17711
17958
  return this;
17712
17959
  }
17713
-
17714
- /**
17715
- * @private
17716
- * @param {CanvasRenderingContext2D} ctx Context to render on
17717
- */
17960
+ render(ctx) {
17961
+ if (this._useEndpointCoords) {
17962
+ this._renderDirectly(ctx);
17963
+ return;
17964
+ }
17965
+ super.render(ctx);
17966
+ }
17967
+ _renderDirectly(ctx) {
17968
+ if (!this.visible) return;
17969
+ ctx.save();
17970
+ ctx.globalAlpha = this.opacity;
17971
+ ctx.lineWidth = this.strokeWidth;
17972
+ ctx.lineCap = this.strokeLineCap || 'butt';
17973
+ ctx.beginPath();
17974
+ ctx.moveTo(this.x1, this.y1);
17975
+ ctx.lineTo(this.x2, this.y2);
17976
+ const origStrokeStyle = ctx.strokeStyle;
17977
+ if (isFiller(this.stroke)) {
17978
+ ctx.strokeStyle = this.stroke.toLive(ctx);
17979
+ } else {
17980
+ var _this$stroke;
17981
+ ctx.strokeStyle = ((_this$stroke = this.stroke) === null || _this$stroke === void 0 ? void 0 : _this$stroke.toString()) || '#000';
17982
+ }
17983
+ ctx.stroke();
17984
+ ctx.strokeStyle = origStrokeStyle;
17985
+ ctx.restore();
17986
+ }
17718
17987
  _render(ctx) {
17988
+ if (this._useEndpointCoords) return;
17719
17989
  ctx.beginPath();
17720
17990
  const p = this.calcLinePoints();
17721
17991
  ctx.moveTo(p.x1, p.y1);
17722
17992
  ctx.lineTo(p.x2, p.y2);
17723
17993
  ctx.lineWidth = this.strokeWidth;
17724
-
17725
- // TODO: test this
17726
- // make sure setting "fill" changes color of a line
17727
- // (by copying fillStyle to strokeStyle, since line is stroked, not filled)
17728
17994
  const origStrokeStyle = ctx.strokeStyle;
17729
17995
  if (isFiller(this.stroke)) {
17730
17996
  ctx.strokeStyle = this.stroke.toLive(ctx);
17731
- } else {
17732
- var _this$stroke;
17733
- ctx.strokeStyle = (_this$stroke = this.stroke) !== null && _this$stroke !== void 0 ? _this$stroke : ctx.fillStyle;
17734
17997
  }
17735
17998
  this.stroke && this._renderStroke(ctx);
17736
17999
  ctx.strokeStyle = origStrokeStyle;
17737
18000
  }
17738
-
17739
- /**
17740
- * This function is an helper for svg import. it returns the center of the object in the svg
17741
- * untransformed coordinates
17742
- * @private
17743
- * @return {Point} center point from element coordinates
17744
- */
17745
18001
  _findCenterFromElement() {
17746
18002
  return new Point((this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2);
17747
18003
  }
17748
-
17749
- /**
17750
- * Returns object representation of an instance
17751
- * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
17752
- * @return {Object} object representation of an instance
17753
- */
17754
18004
  toObject() {
17755
18005
  let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
18006
+ if (this._useEndpointCoords) {
18007
+ return {
18008
+ ...super.toObject(propertiesToInclude),
18009
+ x1: this.x1,
18010
+ y1: this.y1,
18011
+ x2: this.x2,
18012
+ y2: this.y2
18013
+ };
18014
+ }
17756
18015
  return {
17757
18016
  ...super.toObject(propertiesToInclude),
17758
18017
  ...this.calcLinePoints()
17759
18018
  };
17760
18019
  }
17761
-
17762
- /*
17763
- * Calculate object dimensions from its properties
17764
- * @private
17765
- */
17766
18020
  _getNonTransformedDimensions() {
17767
18021
  const dim = super._getNonTransformedDimensions();
17768
- if (this.strokeLineCap === 'butt') {
17769
- if (this.width === 0) {
17770
- dim.y -= this.strokeWidth;
17771
- }
17772
- if (this.height === 0) {
17773
- dim.x -= this.strokeWidth;
17774
- }
18022
+ if (this.strokeLineCap === 'round') {
18023
+ dim.x += this.strokeWidth;
18024
+ dim.y += this.strokeWidth;
17775
18025
  }
17776
18026
  return dim;
17777
18027
  }
17778
-
17779
- /**
17780
- * Recalculates line points given width and height
17781
- * Those points are simply placed around the center,
17782
- * This is not useful outside internal render functions and svg output
17783
- * Is not meant to be for the developer.
17784
- * @private
17785
- */
17786
18028
  calcLinePoints() {
18029
+ if (this._updatingEndpoints) {
18030
+ const centerX = (this.x1 + this.x2) / 2;
18031
+ const centerY = (this.y1 + this.y2) / 2;
18032
+ return {
18033
+ x1: this.x1 - centerX,
18034
+ y1: this.y1 - centerY,
18035
+ x2: this.x2 - centerX,
18036
+ y2: this.y2 - centerY
18037
+ };
18038
+ }
17787
18039
  const {
17788
18040
  x1: _x1,
17789
18041
  x2: _x2,
@@ -17792,48 +18044,64 @@ class Line extends FabricObject {
17792
18044
  width,
17793
18045
  height
17794
18046
  } = this;
17795
- const xMult = _x1 <= _x2 ? -1 : 1,
17796
- yMult = _y1 <= _y2 ? -1 : 1,
17797
- x1 = xMult * width / 2,
17798
- y1 = yMult * height / 2,
17799
- x2 = xMult * -width / 2,
17800
- y2 = yMult * -height / 2;
18047
+ const xMult = _x1 <= _x2 ? -1 : 1;
18048
+ const yMult = _y1 <= _y2 ? -1 : 1;
17801
18049
  return {
17802
- x1,
17803
- x2,
17804
- y1,
17805
- y2
18050
+ x1: xMult * width / 2,
18051
+ y1: yMult * height / 2,
18052
+ x2: xMult * -width / 2,
18053
+ y2: yMult * -height / 2
17806
18054
  };
17807
18055
  }
17808
-
17809
- /* _FROM_SVG_START_ */
17810
-
17811
- /**
17812
- * Returns svg representation of an instance
17813
- * @return {Array} an array of strings with the specific svg representation
17814
- * of the instance
17815
- */
17816
18056
  _toSVG() {
17817
- const {
17818
- x1,
17819
- x2,
17820
- y1,
17821
- y2
17822
- } = this.calcLinePoints();
17823
- return ['<line ', 'COMMON_PARTS', `x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" />\n`];
18057
+ if (this._useEndpointCoords) {
18058
+ // Use absolute coordinates to bypass all Fabric.js transforms
18059
+ // Handle gradients manually for proper SVG export
18060
+ let strokeAttr = '';
18061
+ if (this.stroke instanceof Gradient) {
18062
+ // Let Fabric.js handle gradient definition, but we'll use the reference
18063
+ strokeAttr = `stroke="url(#${this.stroke.id})"`;
18064
+ } else {
18065
+ strokeAttr = `stroke="${this.stroke || 'none'}"`;
18066
+ }
18067
+ 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`];
18068
+ } else {
18069
+ // Use standard calcLinePoints for legacy mode
18070
+ const {
18071
+ x1,
18072
+ x2,
18073
+ y1,
18074
+ y2
18075
+ } = this.calcLinePoints();
18076
+ return ['<line ', 'COMMON_PARTS', `x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" />\n`];
18077
+ }
17824
18078
  }
18079
+ toSVG(reviver) {
18080
+ if (this._useEndpointCoords) {
18081
+ // For endpoint coords, we need to bypass transforms but still allow gradients
18082
+ // Let's temporarily disable transforms during SVG generation
18083
+ const originalLeft = this.left;
18084
+ const originalTop = this.top;
17825
18085
 
17826
- /**
17827
- * List of attribute names to account for when parsing SVG element (used by {@link Line.fromElement})
17828
- * @see http://www.w3.org/TR/SVG/shapes.html#LineElement
17829
- */
18086
+ // Set position to center of line for gradient calculation
18087
+ this.left = (this.x1 + this.x2) / 2;
18088
+ this.top = (this.y1 + this.y2) / 2;
17830
18089
 
17831
- /**
17832
- * Returns Line instance from an SVG element
17833
- * @param {HTMLElement} element Element to parse
17834
- * @param {Object} [options] Options object
17835
- * @param {Function} [callback] callback function invoked after parsing
17836
- */
18090
+ // Get the SVG with standard system (for gradient handling)
18091
+ const standardSVG = super.toSVG(reviver);
18092
+
18093
+ // Restore original position
18094
+ this.left = originalLeft;
18095
+ this.top = originalTop;
18096
+
18097
+ // Extract gradient definition and clean up the line element
18098
+ // Remove the transform wrapper and update coordinates
18099
+ 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}"`);
18100
+ return cleanSVG;
18101
+ }
18102
+ // Use default behavior for legacy mode
18103
+ return super.toSVG(reviver);
18104
+ }
17837
18105
  static async fromElement(element, options, cssRules) {
17838
18106
  const {
17839
18107
  x1 = 0,
@@ -17844,14 +18112,6 @@ class Line extends FabricObject {
17844
18112
  } = parseAttributes(element, this.ATTRIBUTE_NAMES, cssRules);
17845
18113
  return new this([x1, y1, x2, y2], parsedAttributes);
17846
18114
  }
17847
-
17848
- /* _FROM_SVG_END_ */
17849
-
17850
- /**
17851
- * Returns Line instance from an object representation
17852
- * @param {Object} object Object to create an instance from
17853
- * @returns {Promise<Line>}
17854
- */
17855
18115
  static fromObject(_ref) {
17856
18116
  let {
17857
18117
  x1,
@@ -17868,32 +18128,195 @@ class Line extends FabricObject {
17868
18128
  });
17869
18129
  }
17870
18130
  }
18131
+ _defineProperty(Line, "type", 'Line');
18132
+ _defineProperty(Line, "cacheProperties", [...cacheProperties, ...coordProps]);
18133
+ _defineProperty(Line, "ATTRIBUTE_NAMES", SHARED_ATTRIBUTES.concat(coordProps));
18134
+ classRegistry.setClass(Line);
18135
+ classRegistry.setSVGClass(Line);
18136
+
18137
+ /**
18138
+ * Calculate the distance between two points
18139
+ */
18140
+ function pointDistance(p1, p2) {
18141
+ return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
18142
+ }
18143
+
18144
+ /**
18145
+ * Normalize a vector
18146
+ */
18147
+ function normalizeVector(vector) {
18148
+ const length = Math.sqrt(vector.x * vector.x + vector.y * vector.y);
18149
+ if (length === 0) return {
18150
+ x: 0,
18151
+ y: 0
18152
+ };
18153
+ return {
18154
+ x: vector.x / length,
18155
+ y: vector.y / length
18156
+ };
18157
+ }
18158
+
17871
18159
  /**
17872
- * x value or first line edge
17873
- * @type number
18160
+ * Get the maximum allowed radius for a corner based on adjacent edge lengths
17874
18161
  */
18162
+ function getMaxRadius(prevPoint, currentPoint, nextPoint) {
18163
+ const dist1 = pointDistance(prevPoint, currentPoint);
18164
+ const dist2 = pointDistance(currentPoint, nextPoint);
18165
+ return Math.min(dist1, dist2) / 2;
18166
+ }
18167
+
17875
18168
  /**
17876
- * y value or first line edge
17877
- * @type number
18169
+ * Calculate rounded corner data for a single corner
17878
18170
  */
18171
+ function calculateRoundedCorner(prevPoint, currentPoint, nextPoint, radius) {
18172
+ // Calculate edge vectors
18173
+ const edge1 = {
18174
+ x: currentPoint.x - prevPoint.x,
18175
+ y: currentPoint.y - prevPoint.y
18176
+ };
18177
+ const edge2 = {
18178
+ x: nextPoint.x - currentPoint.x,
18179
+ y: nextPoint.y - currentPoint.y
18180
+ };
18181
+
18182
+ // Normalize edge vectors
18183
+ const norm1 = normalizeVector(edge1);
18184
+ const norm2 = normalizeVector(edge2);
18185
+
18186
+ // Calculate the maximum allowed radius
18187
+ const maxRadius = getMaxRadius(prevPoint, currentPoint, nextPoint);
18188
+ const actualRadius = Math.min(radius, maxRadius);
18189
+
18190
+ // Calculate start and end points of the rounded corner
18191
+ const startPoint = {
18192
+ x: currentPoint.x - norm1.x * actualRadius,
18193
+ y: currentPoint.y - norm1.y * actualRadius
18194
+ };
18195
+ const endPoint = {
18196
+ x: currentPoint.x + norm2.x * actualRadius,
18197
+ y: currentPoint.y + norm2.y * actualRadius
18198
+ };
18199
+
18200
+ // Calculate control points for bezier curve
18201
+ // Using the magic number kRect for optimal circular approximation
18202
+ const controlOffset = actualRadius * kRect;
18203
+ const cp1 = {
18204
+ x: startPoint.x + norm1.x * controlOffset,
18205
+ y: startPoint.y + norm1.y * controlOffset
18206
+ };
18207
+ const cp2 = {
18208
+ x: endPoint.x - norm2.x * controlOffset,
18209
+ y: endPoint.y - norm2.y * controlOffset
18210
+ };
18211
+ return {
18212
+ corner: currentPoint,
18213
+ start: startPoint,
18214
+ end: endPoint,
18215
+ cp1,
18216
+ cp2,
18217
+ actualRadius
18218
+ };
18219
+ }
18220
+
17879
18221
  /**
17880
- * x value or second line edge
17881
- * @type number
18222
+ * Apply corner radius to a polygon defined by points
18223
+ */
18224
+ function applyCornerRadiusToPolygon(points, radius) {
18225
+ let radiusAsPercentage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
18226
+ if (points.length < 3) {
18227
+ throw new Error('Polygon must have at least 3 points');
18228
+ }
18229
+
18230
+ // Calculate bounding box if radius is percentage-based
18231
+ let actualRadius = radius;
18232
+ if (radiusAsPercentage) {
18233
+ const minX = Math.min(...points.map(p => p.x));
18234
+ const maxX = Math.max(...points.map(p => p.x));
18235
+ const minY = Math.min(...points.map(p => p.y));
18236
+ const maxY = Math.max(...points.map(p => p.y));
18237
+ const width = maxX - minX;
18238
+ const height = maxY - minY;
18239
+ const minDimension = Math.min(width, height);
18240
+ actualRadius = radius / 100 * minDimension;
18241
+ }
18242
+ const roundedCorners = [];
18243
+ for (let i = 0; i < points.length; i++) {
18244
+ const prevIndex = (i - 1 + points.length) % points.length;
18245
+ const nextIndex = (i + 1) % points.length;
18246
+ const prevPoint = points[prevIndex];
18247
+ const currentPoint = points[i];
18248
+ const nextPoint = points[nextIndex];
18249
+ const roundedCorner = calculateRoundedCorner(prevPoint, currentPoint, nextPoint, actualRadius);
18250
+ roundedCorners.push(roundedCorner);
18251
+ }
18252
+ return roundedCorners;
18253
+ }
18254
+
18255
+ /**
18256
+ * Render a rounded polygon to a canvas context
17882
18257
  */
18258
+ function renderRoundedPolygon(ctx, roundedCorners) {
18259
+ let closed = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
18260
+ if (roundedCorners.length === 0) return;
18261
+ ctx.beginPath();
18262
+
18263
+ // Start at the first corner's start point
18264
+ const firstCorner = roundedCorners[0];
18265
+ ctx.moveTo(firstCorner.start.x, firstCorner.start.y);
18266
+ for (let i = 0; i < roundedCorners.length; i++) {
18267
+ const corner = roundedCorners[i];
18268
+ const nextIndex = (i + 1) % roundedCorners.length;
18269
+ const nextCorner = roundedCorners[nextIndex];
18270
+
18271
+ // Draw the rounded corner using bezier curve
18272
+ ctx.bezierCurveTo(corner.cp1.x, corner.cp1.y, corner.cp2.x, corner.cp2.y, corner.end.x, corner.end.y);
18273
+
18274
+ // Draw line to next corner's start point (if not the last segment in open path)
18275
+ if (i < roundedCorners.length - 1 || closed) {
18276
+ ctx.lineTo(nextCorner.start.x, nextCorner.start.y);
18277
+ }
18278
+ }
18279
+ if (closed) {
18280
+ ctx.closePath();
18281
+ }
18282
+ }
18283
+
17883
18284
  /**
17884
- * y value or second line edge
17885
- * @type number
18285
+ * Generate SVG path data for a rounded polygon
17886
18286
  */
17887
- _defineProperty(Line, "type", 'Line');
17888
- _defineProperty(Line, "cacheProperties", [...cacheProperties, ...coordProps]);
17889
- _defineProperty(Line, "ATTRIBUTE_NAMES", SHARED_ATTRIBUTES.concat(coordProps));
17890
- classRegistry.setClass(Line);
17891
- classRegistry.setSVGClass(Line);
18287
+ function generateRoundedPolygonPath(roundedCorners) {
18288
+ let closed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
18289
+ if (roundedCorners.length === 0) return '';
18290
+ const pathData = [];
18291
+ const firstCorner = roundedCorners[0];
18292
+
18293
+ // Move to first corner's start point
18294
+ pathData.push(`M ${firstCorner.start.x} ${firstCorner.start.y}`);
18295
+ for (let i = 0; i < roundedCorners.length; i++) {
18296
+ const corner = roundedCorners[i];
18297
+ const nextIndex = (i + 1) % roundedCorners.length;
18298
+ const nextCorner = roundedCorners[nextIndex];
18299
+
18300
+ // Add bezier curve for the rounded corner
18301
+ pathData.push(`C ${corner.cp1.x} ${corner.cp1.y} ${corner.cp2.x} ${corner.cp2.y} ${corner.end.x} ${corner.end.y}`);
18302
+
18303
+ // Add line to next corner's start point (if not the last segment in open path)
18304
+ if (i < roundedCorners.length - 1 || closed) {
18305
+ pathData.push(`L ${nextCorner.start.x} ${nextCorner.start.y}`);
18306
+ }
18307
+ }
18308
+ if (closed) {
18309
+ pathData.push('Z');
18310
+ }
18311
+ return pathData.join(' ');
18312
+ }
17892
18313
 
17893
18314
  const triangleDefaultValues = {
17894
18315
  width: 100,
17895
- height: 100
18316
+ height: 100,
18317
+ cornerRadius: 0
17896
18318
  };
18319
+ const TRIANGLE_PROPS = ['cornerRadius'];
17897
18320
  class Triangle extends FabricObject {
17898
18321
  static getDefaults() {
17899
18322
  return {
@@ -17912,34 +18335,90 @@ class Triangle extends FabricObject {
17912
18335
  this.setOptions(options);
17913
18336
  }
17914
18337
 
18338
+ /**
18339
+ * Get triangle points as an array of XY coordinates
18340
+ * @private
18341
+ */
18342
+ _getTrianglePoints() {
18343
+ const widthBy2 = this.width / 2;
18344
+ const heightBy2 = this.height / 2;
18345
+ return [{
18346
+ x: -widthBy2,
18347
+ y: heightBy2
18348
+ },
18349
+ // bottom left
18350
+ {
18351
+ x: 0,
18352
+ y: -heightBy2
18353
+ },
18354
+ // top center
18355
+ {
18356
+ x: widthBy2,
18357
+ y: heightBy2
18358
+ } // bottom right
18359
+ ];
18360
+ }
18361
+
17915
18362
  /**
17916
18363
  * @private
17917
18364
  * @param {CanvasRenderingContext2D} ctx Context to render on
17918
18365
  */
17919
18366
  _render(ctx) {
17920
- const widthBy2 = this.width / 2,
17921
- heightBy2 = this.height / 2;
17922
- ctx.beginPath();
17923
- ctx.moveTo(-widthBy2, heightBy2);
17924
- ctx.lineTo(0, -heightBy2);
17925
- ctx.lineTo(widthBy2, heightBy2);
17926
- ctx.closePath();
18367
+ if (this.cornerRadius > 0) {
18368
+ // Render rounded triangle
18369
+ const points = this._getTrianglePoints();
18370
+ const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
18371
+ renderRoundedPolygon(ctx, roundedCorners, true);
18372
+ } else {
18373
+ // Render sharp triangle (original implementation)
18374
+ const widthBy2 = this.width / 2;
18375
+ const heightBy2 = this.height / 2;
18376
+ ctx.beginPath();
18377
+ ctx.moveTo(-widthBy2, heightBy2);
18378
+ ctx.lineTo(0, -heightBy2);
18379
+ ctx.lineTo(widthBy2, heightBy2);
18380
+ ctx.closePath();
18381
+ }
17927
18382
  this._renderPaintInOrder(ctx);
17928
18383
  }
17929
18384
 
18385
+ /**
18386
+ * Returns object representation of an instance
18387
+ * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
18388
+ * @return {Object} object representation of an instance
18389
+ */
18390
+ toObject() {
18391
+ let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
18392
+ return super.toObject([...TRIANGLE_PROPS, ...propertiesToInclude]);
18393
+ }
18394
+
17930
18395
  /**
17931
18396
  * Returns svg representation of an instance
17932
18397
  * @return {Array} an array of strings with the specific svg representation
17933
18398
  * of the instance
17934
18399
  */
17935
18400
  _toSVG() {
17936
- const widthBy2 = this.width / 2,
17937
- heightBy2 = this.height / 2,
17938
- points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
17939
- return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
18401
+ if (this.cornerRadius > 0) {
18402
+ // Generate rounded triangle as path
18403
+ const points = this._getTrianglePoints();
18404
+ const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
18405
+ const pathData = generateRoundedPolygonPath(roundedCorners, true);
18406
+ return ['<path ', 'COMMON_PARTS', `d="${pathData}" />`];
18407
+ } else {
18408
+ // Original sharp triangle implementation
18409
+ const widthBy2 = this.width / 2;
18410
+ const heightBy2 = this.height / 2;
18411
+ const points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
18412
+ return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
18413
+ }
17940
18414
  }
17941
18415
  }
18416
+ /**
18417
+ * Corner radius for rounded triangle corners
18418
+ * @type Number
18419
+ */
17942
18420
  _defineProperty(Triangle, "type", 'Triangle');
18421
+ _defineProperty(Triangle, "cacheProperties", [...cacheProperties, ...TRIANGLE_PROPS]);
17943
18422
  _defineProperty(Triangle, "ownDefaults", triangleDefaultValues);
17944
18423
  classRegistry.setClass(Triangle);
17945
18424
  classRegistry.setSVGClass(Triangle);
@@ -18104,7 +18583,8 @@ const polylineDefaultValues = {
18104
18583
  /**
18105
18584
  * @deprecated transient option soon to be removed in favor of a different design
18106
18585
  */
18107
- exactBoundingBox: false
18586
+ exactBoundingBox: false,
18587
+ cornerRadius: 0
18108
18588
  };
18109
18589
  class Polyline extends FabricObject {
18110
18590
  static getDefaults() {
@@ -18318,7 +18798,7 @@ class Polyline extends FabricObject {
18318
18798
  toObject() {
18319
18799
  let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
18320
18800
  return {
18321
- ...super.toObject(propertiesToInclude),
18801
+ ...super.toObject(['cornerRadius', ...propertiesToInclude]),
18322
18802
  points: this.points.map(_ref => {
18323
18803
  let {
18324
18804
  x,
@@ -18338,14 +18818,28 @@ class Polyline extends FabricObject {
18338
18818
  * of the instance
18339
18819
  */
18340
18820
  _toSVG() {
18341
- const points = [],
18342
- diffX = this.pathOffset.x,
18343
- diffY = this.pathOffset.y,
18344
- NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS;
18345
- for (let i = 0, len = this.points.length; i < len; i++) {
18346
- points.push(toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ');
18821
+ if (this.cornerRadius > 0 && this.points.length >= 3) {
18822
+ // Generate rounded polygon/polyline as path
18823
+ const diffX = this.pathOffset.x;
18824
+ const diffY = this.pathOffset.y;
18825
+ const adjustedPoints = this.points.map(point => ({
18826
+ x: point.x - diffX,
18827
+ y: point.y - diffY
18828
+ }));
18829
+ const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
18830
+ const pathData = generateRoundedPolygonPath(roundedCorners, !this.isOpen());
18831
+ return ['<path ', 'COMMON_PARTS', `d="${pathData}" />\n`];
18832
+ } else {
18833
+ // Original sharp corners implementation
18834
+ const points = [];
18835
+ const diffX = this.pathOffset.x;
18836
+ const diffY = this.pathOffset.y;
18837
+ const NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS;
18838
+ for (let i = 0, len = this.points.length; i < len; i++) {
18839
+ points.push(toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ');
18840
+ }
18841
+ return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
18347
18842
  }
18348
- return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
18349
18843
  }
18350
18844
 
18351
18845
  /**
@@ -18361,13 +18855,24 @@ class Polyline extends FabricObject {
18361
18855
  // NaN comes from parseFloat of a empty string in parser
18362
18856
  return;
18363
18857
  }
18364
- ctx.beginPath();
18365
- ctx.moveTo(this.points[0].x - x, this.points[0].y - y);
18366
- for (let i = 0; i < len; i++) {
18367
- const point = this.points[i];
18368
- ctx.lineTo(point.x - x, point.y - y);
18858
+ if (this.cornerRadius > 0 && len >= 3) {
18859
+ // Render with rounded corners
18860
+ const adjustedPoints = this.points.map(point => ({
18861
+ x: point.x - x,
18862
+ y: point.y - y
18863
+ }));
18864
+ const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
18865
+ renderRoundedPolygon(ctx, roundedCorners, !this.isOpen());
18866
+ } else {
18867
+ // Original sharp corners implementation
18868
+ ctx.beginPath();
18869
+ ctx.moveTo(this.points[0].x - x, this.points[0].y - y);
18870
+ for (let i = 0; i < len; i++) {
18871
+ const point = this.points[i];
18872
+ ctx.lineTo(point.x - x, point.y - y);
18873
+ }
18874
+ !this.isOpen() && ctx.closePath();
18369
18875
  }
18370
- !this.isOpen() && ctx.closePath();
18371
18876
  this._renderPaintInOrder(ctx);
18372
18877
  }
18373
18878
 
@@ -18432,10 +18937,15 @@ class Polyline extends FabricObject {
18432
18937
  * @type Boolean
18433
18938
  * @default false
18434
18939
  */
18940
+ /**
18941
+ * Corner radius for rounded corners
18942
+ * @type Number
18943
+ * @default 0
18944
+ */
18435
18945
  _defineProperty(Polyline, "ownDefaults", polylineDefaultValues);
18436
18946
  _defineProperty(Polyline, "type", 'Polyline');
18437
18947
  _defineProperty(Polyline, "layoutProperties", [SKEW_X, SKEW_Y, 'strokeLineCap', 'strokeLineJoin', 'strokeMiterLimit', 'strokeWidth', 'strokeUniform', 'points']);
18438
- _defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points']);
18948
+ _defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points', 'cornerRadius']);
18439
18949
  _defineProperty(Polyline, "ATTRIBUTE_NAMES", [...SHARED_ATTRIBUTES]);
18440
18950
  classRegistry.setClass(Polyline);
18441
18951
  classRegistry.setSVGClass(Polyline);
@@ -18819,6 +19329,97 @@ function measureGraphemeWithKerning(grapheme, previousGrapheme, options, ctx) {
18819
19329
  };
18820
19330
  }
18821
19331
 
19332
+ /**
19333
+ * Get a representative character for font metrics measurement
19334
+ * Uses canvas to test which scripts the font actually supports
19335
+ */
19336
+ function getRepresentativeCharacter(fontFamily) {
19337
+ const context = getMeasurementContext();
19338
+
19339
+ // Wait for font to be ready if possible
19340
+ if (typeof document !== 'undefined' && 'fonts' in document) {
19341
+ try {
19342
+ // Check if font is ready, if not, use fallback immediately
19343
+ if (!document.fonts.check(`16px ${fontFamily}`)) {
19344
+ return 'M'; // Use safe fallback while font loads
19345
+ }
19346
+ } catch (e) {
19347
+ // Font check failed, use fallback
19348
+ return 'M';
19349
+ }
19350
+ }
19351
+
19352
+ // Test characters for different scripts
19353
+ const testChars = [{
19354
+ char: 'م',
19355
+ script: 'Arabic'
19356
+ },
19357
+ // Arabic
19358
+ {
19359
+ char: 'א',
19360
+ script: 'Hebrew'
19361
+ },
19362
+ // Hebrew
19363
+ {
19364
+ char: 'अ',
19365
+ script: 'Devanagari'
19366
+ },
19367
+ // Hindi/Sanskrit
19368
+ {
19369
+ char: 'ا',
19370
+ script: 'Urdu'
19371
+ },
19372
+ // Urdu
19373
+ {
19374
+ char: 'ک',
19375
+ script: 'Persian'
19376
+ },
19377
+ // Persian
19378
+ {
19379
+ char: 'த',
19380
+ script: 'Tamil'
19381
+ },
19382
+ // Tamil
19383
+ {
19384
+ char: 'ก',
19385
+ script: 'Thai'
19386
+ },
19387
+ // Thai
19388
+ {
19389
+ char: 'М',
19390
+ script: 'Cyrillic'
19391
+ },
19392
+ // Cyrillic
19393
+ {
19394
+ char: 'Ω',
19395
+ script: 'Greek'
19396
+ },
19397
+ // Greek
19398
+ {
19399
+ char: 'M',
19400
+ script: 'Latin'
19401
+ } // Latin (fallback)
19402
+ ];
19403
+
19404
+ // Set the font
19405
+ context.font = `16px ${fontFamily}`;
19406
+
19407
+ // Test each character to see which ones render properly
19408
+ // Use a more robust width check to avoid false positives
19409
+ const fallbackWidth = context.measureText('M').width;
19410
+ for (const test of testChars) {
19411
+ const metrics = context.measureText(test.char);
19412
+
19413
+ // Character is valid if it has width and isn't just a fallback glyph
19414
+ if (metrics.width > 0 && Math.abs(metrics.width - fallbackWidth) > 0.1) {
19415
+ return test.char;
19416
+ }
19417
+ }
19418
+
19419
+ // Fallback to Latin 'M'
19420
+ return 'M';
19421
+ }
19422
+
18822
19423
  /**
18823
19424
  * Get font metrics for layout calculations
18824
19425
  */
@@ -18832,8 +19433,9 @@ function getFontMetrics(options) {
18832
19433
  const context = getMeasurementContext();
18833
19434
  applyFontStyle(context, options);
18834
19435
 
18835
- // Use 'M' as sample character for metrics
18836
- const metrics = context.measureText('M');
19436
+ // Use representative character based on font's primary script
19437
+ const sample = getRepresentativeCharacter(options.fontFamily);
19438
+ const metrics = context.measureText(sample);
18837
19439
  const fontSize = options.fontSize;
18838
19440
 
18839
19441
  // Calculate metrics with fallbacks
@@ -18885,7 +19487,11 @@ function getFontDeclaration(options) {
18885
19487
  } = options;
18886
19488
 
18887
19489
  // Normalize font family (add quotes if needed)
18888
- const normalizedFamily = fontFamily.includes(' ') && !fontFamily.includes('"') && !fontFamily.includes("'") ? `"${fontFamily}"` : fontFamily;
19490
+ let normalizedFamily = fontFamily.includes(' ') && !fontFamily.includes('"') && !fontFamily.includes("'") ? `"${fontFamily}"` : fontFamily;
19491
+
19492
+ // Note: Font fallbacks are handled in the rendering phase only
19493
+ // to avoid affecting measurement calculations for text wrapping
19494
+
18889
19495
  return `${fontStyle} ${fontWeight} ${fontSize}px ${normalizedFamily}`;
18890
19496
  }
18891
19497
 
@@ -19037,6 +19643,81 @@ const measurementCache = new MeasurementCache();
19037
19643
  const kerningCache = new KerningCache();
19038
19644
  const fontMetricsCache = new FontMetricsCache();
19039
19645
 
19646
+ // Set up font loading listener to clear caches when fonts change
19647
+ if (typeof document !== 'undefined' && 'fonts' in document) {
19648
+ document.fonts.addEventListener('loadingdone', () => {
19649
+ // Clear all caches when fonts finish loading
19650
+ clearAllCaches();
19651
+ });
19652
+ }
19653
+
19654
+ /**
19655
+ * Clear all measurement caches
19656
+ */
19657
+ function clearAllCaches() {
19658
+ measurementCache.clear();
19659
+ kerningCache.clear();
19660
+ fontMetricsCache.clear();
19661
+ }
19662
+
19663
+ /**
19664
+ * Detect if a font lacks English glyph support
19665
+ * These fonts should use browser-native measurement instead of Fabric's character-by-character measurement
19666
+ */
19667
+ function fontLacksEnglishGlyphs(fontFamily) {
19668
+ if (typeof document === 'undefined') return false;
19669
+
19670
+ // Known fonts that lack English glyphs
19671
+ const knownNonEnglishFonts = ['stv', 'arabic', 'naskh', 'thuluth', 'kufi', 'diwani', 'nastaliq', 'kufic', 'hijazi', 'madinah', 'makkah'];
19672
+ const lowerFontFamily = fontFamily.toLowerCase();
19673
+
19674
+ // Check known list first
19675
+ if (knownNonEnglishFonts.some(font => lowerFontFamily.includes(font))) {
19676
+ return true;
19677
+ }
19678
+
19679
+ // Dynamic glyph support detection
19680
+ const context = getMeasurementContext();
19681
+ context.font = `16px ${fontFamily}`;
19682
+
19683
+ // Test English characters
19684
+ const englishChars = ['A', 'B', 'C', 'a', 'b', 'c', 'M', 'W'];
19685
+ const fallbackFont = 'Arial, sans-serif';
19686
+
19687
+ // Measure with target font
19688
+ const targetWidths = englishChars.map(char => context.measureText(char).width);
19689
+
19690
+ // Measure with fallback font
19691
+ context.font = `16px ${fallbackFont}`;
19692
+ const fallbackWidths = englishChars.map(char => context.measureText(char).width);
19693
+
19694
+ // If most measurements are identical, the font likely doesn't have English glyphs
19695
+ let identicalCount = 0;
19696
+ for (let i = 0; i < englishChars.length; i++) {
19697
+ if (Math.abs(targetWidths[i] - fallbackWidths[i]) < 0.5) {
19698
+ identicalCount++;
19699
+ }
19700
+ }
19701
+ const lacksSupportThreshold = englishChars.length * 0.7; // 70% identical = lacks support
19702
+ const lacksSupport = identicalCount >= lacksSupportThreshold;
19703
+ return lacksSupport;
19704
+ }
19705
+
19706
+ // Cache for font glyph detection results
19707
+ const fontGlyphCache = new Map();
19708
+
19709
+ /**
19710
+ * Cached version of font glyph detection
19711
+ */
19712
+ function fontLacksEnglishGlyphsCached(fontFamily) {
19713
+ if (fontGlyphCache.has(fontFamily)) {
19714
+ return fontGlyphCache.get(fontFamily);
19715
+ }
19716
+ const result = fontLacksEnglishGlyphs(fontFamily);
19717
+ fontGlyphCache.set(fontFamily, result);
19718
+ return result;
19719
+ }
19720
+
19040
19721
  /**
19041
19722
  * Unicode and Internationalization Support
19042
19723
  *
@@ -20220,6 +20901,15 @@ class FabricText extends StyledText {
20220
20901
  * Does not return dimensions.
20221
20902
  */
20222
20903
  initDimensions() {
20904
+ // Check if font is ready for accurate measurements
20905
+ // Only block initialization if it's a critical font loading situation
20906
+ const fontReady = this._isFontReady();
20907
+ if (!fontReady && !this.initialized) {
20908
+ // Only schedule font loading on first initialization
20909
+ this._scheduleInitAfterFontLoad();
20910
+ // Continue with fallback measurements for now
20911
+ }
20912
+
20223
20913
  // Use advanced layout if enabled
20224
20914
  if (this.enableAdvancedLayout && !this.path) {
20225
20915
  return this.initDimensionsAdvanced();
@@ -20236,7 +20926,21 @@ class FabricText extends StyledText {
20236
20926
  }
20237
20927
  if (this.textAlign.includes(JUSTIFY)) {
20238
20928
  // once text is measured we need to make space fatter to make justified text.
20239
- this.enlargeSpaces();
20929
+ // Ensure __charBounds exists before calling enlargeSpaces
20930
+ if (this.__charBounds && this.__charBounds.length > 0) {
20931
+ this.enlargeSpaces();
20932
+ } else {
20933
+ console.warn('⚠️ __charBounds not ready for justify alignment, deferring enlargeSpaces');
20934
+ // Defer the justify calculation until the next frame
20935
+ setTimeout(() => {
20936
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
20937
+ var _this$canvas;
20938
+ console.log('🔧 Applying deferred justify alignment');
20939
+ this.enlargeSpaces();
20940
+ (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
20941
+ }
20942
+ }, 0);
20943
+ }
20240
20944
  }
20241
20945
  }
20242
20946
 
@@ -20245,8 +20949,9 @@ class FabricText extends StyledText {
20245
20949
  */
20246
20950
  enlargeSpaces() {
20247
20951
  let diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces;
20952
+ const isRtl = this.direction === 'rtl';
20248
20953
  for (let i = 0, len = this._textLines.length; i < len; i++) {
20249
- if (this.textAlign !== JUSTIFY && (i === len - 1 || this.isEndOfWrapping(i))) {
20954
+ if (!this.textAlign.includes('justify') && (i === len - 1 || this.isEndOfWrapping(i))) {
20250
20955
  continue;
20251
20956
  }
20252
20957
  accumulatedSpace = 0;
@@ -20255,15 +20960,47 @@ class FabricText extends StyledText {
20255
20960
  if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) {
20256
20961
  numberOfSpaces = spaces.length;
20257
20962
  diffSpace = (this.width - currentLineWidth) / numberOfSpaces;
20258
- for (let j = 0; j <= line.length; j++) {
20259
- charBound = this.__charBounds[i][j];
20260
- if (this._reSpaceAndTab.test(line[j])) {
20261
- charBound.width += diffSpace;
20262
- charBound.kernedWidth += diffSpace;
20263
- charBound.left += accumulatedSpace;
20264
- accumulatedSpace += diffSpace;
20265
- } else {
20266
- charBound.left += accumulatedSpace;
20963
+ console.log(`🔧 EnlargeSpaces Line ${i}:`);
20964
+ console.log(` Current width: ${currentLineWidth}, Target: ${this.width}`);
20965
+ console.log(` Spaces: ${numberOfSpaces}, diffSpace: ${diffSpace.toFixed(2)}`);
20966
+ if (isRtl) {
20967
+ for (let j = 0; j < line.length; j++) {
20968
+ if (this._reSpaceAndTab.test(line[j])) ;
20969
+ }
20970
+
20971
+ // For RTL, we need to work backwards through the visual positions
20972
+ // but still update logical positions correctly
20973
+ let spaceCount = 0;
20974
+ for (let j = 0; j <= line.length; j++) {
20975
+ charBound = this.__charBounds[i][j];
20976
+ if (charBound) {
20977
+ if (this._reSpaceAndTab.test(line[j])) {
20978
+ charBound.width += diffSpace;
20979
+ charBound.kernedWidth += diffSpace;
20980
+ spaceCount++;
20981
+ }
20982
+
20983
+ // For RTL, shift all characters to the right by the total expansion
20984
+ // minus the expansion that comes after this character
20985
+ const remainingSpaces = numberOfSpaces - spaceCount;
20986
+ const shiftAmount = remainingSpaces * diffSpace;
20987
+ charBound.left += shiftAmount;
20988
+ }
20989
+ }
20990
+ } else {
20991
+ // LTR processing (original logic)
20992
+ for (let j = 0; j <= line.length; j++) {
20993
+ charBound = this.__charBounds[i][j];
20994
+ if (charBound) {
20995
+ if (this._reSpaceAndTab.test(line[j])) {
20996
+ charBound.width += diffSpace;
20997
+ charBound.kernedWidth += diffSpace;
20998
+ charBound.left += accumulatedSpace;
20999
+ accumulatedSpace += diffSpace;
21000
+ } else {
21001
+ charBound.left += accumulatedSpace;
21002
+ }
21003
+ }
20267
21004
  }
20268
21005
  }
20269
21006
  }
@@ -20341,6 +21078,18 @@ class FabricText extends StyledText {
20341
21078
 
20342
21079
  // Convert layout to legacy format for compatibility
20343
21080
  this._convertLayoutToLegacyFormat(layout);
21081
+
21082
+ // Ensure justify alignment is properly applied for compatibility with legacy rendering
21083
+ if (this.textAlign.includes(JUSTIFY)) {
21084
+ // Force enlarge spaces after advanced layout calculation
21085
+ setTimeout(() => {
21086
+ if (this.enlargeSpaces) {
21087
+ var _this$canvas2;
21088
+ this.enlargeSpaces();
21089
+ (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.renderAll();
21090
+ }
21091
+ }, 0);
21092
+ }
20344
21093
  this.dirty = true;
20345
21094
  }
20346
21095
 
@@ -20921,7 +21670,15 @@ class FabricText extends StyledText {
20921
21670
  if (currentDirection !== this.direction) {
20922
21671
  ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl');
20923
21672
  ctx.direction = isLtr ? 'ltr' : 'rtl';
20924
- ctx.textAlign = isLtr ? LEFT : RIGHT;
21673
+
21674
+ // For justify alignments, we need to set the correct canvas text alignment
21675
+ // This is crucial for RTL text to render in the correct order
21676
+ if (isJustify) {
21677
+ // Justify uses LEFT alignment as a base, letting the character positioning handle justification
21678
+ ctx.textAlign = LEFT;
21679
+ } else {
21680
+ ctx.textAlign = isLtr ? LEFT : RIGHT;
21681
+ }
20925
21682
  }
20926
21683
  top -= lineHeight * this._fontSizeFraction / this.lineHeight;
20927
21684
  if (shortCut) {
@@ -21157,9 +21914,21 @@ class FabricText extends StyledText {
21157
21914
  direction = this.direction,
21158
21915
  isEndOfWrapping = this.isEndOfWrapping(lineIndex);
21159
21916
  let leftOffset = 0;
21160
- if (textAlign === JUSTIFY || textAlign === JUSTIFY_CENTER && !isEndOfWrapping || textAlign === JUSTIFY_RIGHT && !isEndOfWrapping || textAlign === JUSTIFY_LEFT && !isEndOfWrapping) {
21161
- return 0;
21917
+
21918
+ // Handle justify alignments (excluding last lines and wrapped line ends)
21919
+ const isJustifyLine = textAlign === JUSTIFY || textAlign === JUSTIFY_CENTER && !isEndOfWrapping || textAlign === JUSTIFY_RIGHT && !isEndOfWrapping || textAlign === JUSTIFY_LEFT && !isEndOfWrapping;
21920
+ if (isJustifyLine) {
21921
+ // Justify lines should start at the left edge for LTR and right edge for RTL
21922
+ // The space distribution is handled by enlargeSpaces()
21923
+ if (direction === 'rtl') {
21924
+ // For RTL justify, we need to account for the line being right-aligned
21925
+ return 0;
21926
+ } else {
21927
+ return 0;
21928
+ }
21162
21929
  }
21930
+
21931
+ // Handle non-justify alignments
21163
21932
  if (textAlign === CENTER) {
21164
21933
  leftOffset = lineDiff / 2;
21165
21934
  }
@@ -21172,6 +21941,8 @@ class FabricText extends StyledText {
21172
21941
  if (textAlign === JUSTIFY_RIGHT) {
21173
21942
  leftOffset = lineDiff;
21174
21943
  }
21944
+
21945
+ // Apply RTL adjustments for non-justify alignments
21175
21946
  if (direction === 'rtl') {
21176
21947
  if (textAlign === RIGHT || textAlign === JUSTIFY || textAlign === JUSTIFY_RIGHT) {
21177
21948
  leftOffset = 0;
@@ -21330,7 +22101,19 @@ class FabricText extends StyledText {
21330
22101
  fontSize = this.fontSize
21331
22102
  } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
21332
22103
  let forMeasuring = arguments.length > 1 ? arguments[1] : undefined;
21333
- const parsedFontFamily = fontFamily.includes("'") || fontFamily.includes('"') || fontFamily.includes(',') || FabricText.genericFonts.includes(fontFamily.toLowerCase()) ? fontFamily : `"${fontFamily}"`;
22104
+ let parsedFontFamily = fontFamily.includes("'") || fontFamily.includes('"') || fontFamily.includes(',') || FabricText.genericFonts.includes(fontFamily.toLowerCase()) ? fontFamily : `"${fontFamily}"`;
22105
+
22106
+ // For fonts like STV that don't support English/Latin characters,
22107
+ // add fallback fonts for consistent rendering of unsupported characters
22108
+ // Only add fallbacks during actual rendering, not for measurements
22109
+ if (!forMeasuring &&
22110
+ // Only during rendering, not measuring
22111
+ !fontFamily.includes(',') && (
22112
+ // Don't add fallbacks if already has them
22113
+ fontFamily.toLowerCase().includes('stv') || fontFamily.toLowerCase().includes('arabic') || fontFamily.toLowerCase().includes('naskh') || fontFamily.toLowerCase().includes('kufi'))) {
22114
+ // Add fallback fonts for unsupported characters (spaces, punctuation, etc.)
22115
+ parsedFontFamily = `${parsedFontFamily}, "Arial Unicode MS", Arial, sans-serif`;
22116
+ }
21334
22117
  return [fontStyle, fontWeight, `${forMeasuring ? this.CACHE_FONT_SIZE : fontSize}px`, parsedFontFamily].join(' ');
21335
22118
  }
21336
22119
 
@@ -21374,7 +22157,13 @@ class FabricText extends StyledText {
21374
22157
  newLine = ['\n'];
21375
22158
  let newText = [];
21376
22159
  for (let i = 0; i < lines.length; i++) {
21377
- newLines[i] = this.graphemeSplit(lines[i]);
22160
+ // Use BiDi-aware grapheme splitting for RTL text
22161
+ if (this.direction === 'rtl' || this._containsArabicText(lines[i])) {
22162
+ newLines[i] = segmentGraphemes(lines[i]);
22163
+ console.log(`🔤 BiDi-aware split line ${i}: "${lines[i]}" -> [${newLines[i].join(', ')}]`);
22164
+ } else {
22165
+ newLines[i] = this.graphemeSplit(lines[i]);
22166
+ }
21378
22167
  newText = newText.concat(newLines[i], newLine);
21379
22168
  }
21380
22169
  newText.pop();
@@ -21386,6 +22175,14 @@ class FabricText extends StyledText {
21386
22175
  };
21387
22176
  }
21388
22177
 
22178
+ /**
22179
+ * Check if text contains Arabic characters
22180
+ * @private
22181
+ */
22182
+ _containsArabicText(text) {
22183
+ return /[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
22184
+ }
22185
+
21389
22186
  /**
21390
22187
  * Returns object representation of an instance
21391
22188
  * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
@@ -21495,18 +22292,100 @@ class FabricText extends StyledText {
21495
22292
  if (textAnchor === CENTER) {
21496
22293
  offX = text.getScaledWidth() / 2;
21497
22294
  }
21498
- if (textAnchor === RIGHT) {
21499
- offX = text.getScaledWidth();
22295
+ if (textAnchor === RIGHT) {
22296
+ offX = text.getScaledWidth();
22297
+ }
22298
+ text.set({
22299
+ left: text.left - offX,
22300
+ top: text.top - (textHeight - text.fontSize * (0.07 + text._fontSizeFraction)) / text.lineHeight,
22301
+ strokeWidth
22302
+ });
22303
+ return text;
22304
+ }
22305
+
22306
+ /* _FROM_SVG_END_ */
22307
+
22308
+ /**
22309
+ * Check if the font is ready for accurate measurements
22310
+ * @private
22311
+ */
22312
+ _isFontReady() {
22313
+ if (typeof document === 'undefined' || !('fonts' in document)) {
22314
+ return true; // Assume ready in non-browser environments
22315
+ }
22316
+ try {
22317
+ return document.fonts.check(`${this.fontSize}px ${this.fontFamily}`);
22318
+ } catch (e) {
22319
+ return true; // Fallback to assuming ready if check fails
22320
+ }
22321
+ }
22322
+
22323
+ /**
22324
+ * Schedule re-initialization after font loads
22325
+ * @private
22326
+ */
22327
+ _scheduleInitAfterFontLoad() {
22328
+ if (typeof document === 'undefined' || !('fonts' in document)) {
22329
+ return;
22330
+ }
22331
+
22332
+ // Only schedule if not already waiting
22333
+ if (this._fontLoadScheduled) {
22334
+ return;
21500
22335
  }
21501
- text.set({
21502
- left: text.left - offX,
21503
- top: text.top - (textHeight - text.fontSize * (0.07 + text._fontSizeFraction)) / text.lineHeight,
21504
- strokeWidth
22336
+ this._fontLoadScheduled = true;
22337
+ const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
22338
+ document.fonts.load(fontSpec).then(() => {
22339
+ this._fontLoadScheduled = false;
22340
+ // Re-initialize dimensions with proper font metrics
22341
+ this.initDimensions();
22342
+
22343
+ // Extra step for justify alignment after font loading
22344
+ if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
22345
+ setTimeout(() => {
22346
+ var _this$canvas3;
22347
+ if (this.enlargeSpaces) {
22348
+ this.enlargeSpaces();
22349
+ }
22350
+ (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
22351
+ }, 10);
22352
+ } else {
22353
+ var _this$canvas4;
22354
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.requestRenderAll();
22355
+ }
22356
+ }).catch(() => {
22357
+ this._fontLoadScheduled = false;
21505
22358
  });
21506
- return text;
21507
22359
  }
21508
22360
 
21509
- /* _FROM_SVG_END_ */
22361
+ /**
22362
+ * Force complete text re-initialization (useful after JSON loading)
22363
+ */
22364
+ forceTextReinitialization() {
22365
+ console.log('🔄 Force reinitializing text object');
22366
+
22367
+ // Clear all caches
22368
+ this._clearCache();
22369
+ this.dirty = true;
22370
+
22371
+ // Force text splitting to rebuild internal structures
22372
+ this._splitText();
22373
+
22374
+ // Re-initialize dimensions
22375
+ this.initDimensions();
22376
+
22377
+ // Special handling for justify alignment
22378
+ if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
22379
+ // Ensure justify is applied after dimensions are set
22380
+ setTimeout(() => {
22381
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
22382
+ var _this$canvas5;
22383
+ this.enlargeSpaces();
22384
+ (_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
22385
+ }
22386
+ }, 10);
22387
+ }
22388
+ }
21510
22389
 
21511
22390
  /**
21512
22391
  * Returns FabricText instance from an object representation
@@ -21519,6 +22398,93 @@ class FabricText extends StyledText {
21519
22398
  styles: stylesFromArray(object.styles || {}, object.text)
21520
22399
  }, {
21521
22400
  extraParam: 'text'
22401
+ }).then(textObject => {
22402
+ // Ensure text object is properly initialized after JSON deserialization
22403
+ // This is critical for justify alignment and other text layout features
22404
+ textObject.initialized = true;
22405
+
22406
+ // Force reinitialization to ensure proper layout
22407
+ if (textObject._clearCache) {
22408
+ textObject._clearCache();
22409
+ }
22410
+ textObject.dirty = true;
22411
+
22412
+ // Check if we need to wait for font loading (especially for custom fonts like STV)
22413
+ const fontSpec = `${textObject.fontSize}px ${textObject.fontFamily}`;
22414
+
22415
+ // For custom fonts, ensure they're loaded before initializing dimensions
22416
+ if (typeof document !== 'undefined' && 'fonts' in document && textObject.fontFamily !== 'Arial' && textObject.fontFamily !== 'Times New Roman') {
22417
+ return document.fonts.load(fontSpec).then(() => {
22418
+ var _textObject$fontFamil;
22419
+ console.log(`🔤 Font loaded for JSON object: ${fontSpec}`);
22420
+ // Ensure initialized flag is set again (in case constructor reset it)
22421
+ textObject.initialized = true;
22422
+
22423
+ // Special handling for STV fonts which have measurement issues
22424
+ const isStvFont = (_textObject$fontFamil = textObject.fontFamily) === null || _textObject$fontFamil === void 0 ? void 0 : _textObject$fontFamil.toLowerCase().includes('stv');
22425
+ if (isStvFont) {
22426
+ console.log(`🔤 STV font detected, using enhanced reinitialization`);
22427
+
22428
+ // Clear all cached state that might interfere with browser wrapping
22429
+ textObject._browserWrapCache = null;
22430
+ textObject._lastDimensionState = null;
22431
+ textObject._browserWrapInitialized = false;
22432
+ console.log(`🔤 STV font: Cleared all cached states for fresh initialization`);
22433
+
22434
+ // Force browser wrapping flag for STV fonts
22435
+ textObject._usingBrowserWrapping = true;
22436
+ console.log(`🔤 STV font: Forcing browser wrapping flag during JSON load`);
22437
+
22438
+ // Multiple initialization attempts for STV fonts
22439
+ const reinitWithDelay = attempt => {
22440
+ if (textObject.forceTextReinitialization) {
22441
+ textObject.forceTextReinitialization();
22442
+ } else {
22443
+ textObject.initDimensions();
22444
+ }
22445
+
22446
+ // Check if width is still problematic after initialization
22447
+ if (textObject.width < 50 && attempt < 3) {
22448
+ console.log(`🔤 STV font width still ${textObject.width}px, retrying in ${100 * attempt}ms (attempt ${attempt + 1}/3)`);
22449
+ setTimeout(() => reinitWithDelay(attempt + 1), 100 * attempt);
22450
+ }
22451
+ };
22452
+ reinitWithDelay(0);
22453
+ } else {
22454
+ // Use specialized reinitialization for Textbox objects
22455
+ if (textObject.forceTextReinitialization) {
22456
+ console.log(`🔤 Using Textbox specialized reinitialization`);
22457
+ textObject.forceTextReinitialization();
22458
+ } else {
22459
+ // Reinitialize dimensions with proper font metrics
22460
+ textObject.initDimensions();
22461
+ }
22462
+ }
22463
+ return textObject;
22464
+ }).catch(() => {
22465
+ console.warn(`⚠️ Font loading failed for ${fontSpec}, proceeding with fallback`);
22466
+ // Ensure initialized flag is set again
22467
+ textObject.initialized = true;
22468
+
22469
+ // Still initialize dimensions even if font loading fails
22470
+ if (textObject.forceTextReinitialization) {
22471
+ textObject.forceTextReinitialization();
22472
+ } else {
22473
+ textObject.initDimensions();
22474
+ }
22475
+ return textObject;
22476
+ });
22477
+ } else {
22478
+ // Standard fonts - ensure initialized and use appropriate method
22479
+ textObject.initialized = true;
22480
+ if (textObject.forceTextReinitialization) {
22481
+ console.log(`🔤 Using Textbox specialized reinitialization for standard font`);
22482
+ textObject.forceTextReinitialization();
22483
+ } else {
22484
+ textObject.initDimensions();
22485
+ }
22486
+ return textObject;
22487
+ }
21522
22488
  });
21523
22489
  }
21524
22490
  }
@@ -22162,18 +23128,98 @@ class OverlayEditor {
22162
23128
 
22163
23129
  // Apply all other font and text styles to match Fabric
22164
23130
  const letterSpacingPx = (target.charSpacing || 0) / 1000 * finalFontSize;
23131
+
23132
+ // Special handling for text objects loaded from JSON - ensure they're properly initialized
23133
+ if (target.dirty !== false && target.initDimensions) {
23134
+ console.log('🔧 Ensuring text object is properly initialized before overlay editing');
23135
+ // Force re-initialization if the text object seems to be in a dirty state
23136
+ target.initDimensions();
23137
+ }
22165
23138
  this.textarea.style.fontSize = `${finalFontSize}px`;
22166
23139
  this.textarea.style.lineHeight = String(fabricLineHeight);
22167
23140
  this.textarea.style.fontFamily = target.fontFamily || 'Arial';
22168
23141
  this.textarea.style.fontWeight = String(target.fontWeight || 'normal');
22169
23142
  this.textarea.style.fontStyle = target.fontStyle || 'normal';
22170
- this.textarea.style.textAlign = target.textAlign || 'left';
23143
+ // Handle text alignment and justification
23144
+ const textAlign = target.textAlign || 'left';
23145
+ let cssTextAlign = textAlign;
23146
+
23147
+ // Detect text direction from content for proper justify handling
23148
+ const autoDetectedDirection = this.firstStrongDir(this.textarea.value || '');
23149
+
23150
+ // DEBUG: Log alignment details
23151
+ console.log('🔍 ALIGNMENT DEBUG:');
23152
+ console.log(' Fabric textAlign:', textAlign);
23153
+ console.log(' Fabric direction:', target.direction);
23154
+ console.log(' Text content:', JSON.stringify(target.text));
23155
+ console.log(' Detected direction:', autoDetectedDirection);
23156
+
23157
+ // Map fabric.js justify to CSS
23158
+ if (textAlign.includes('justify')) {
23159
+ // Try to match fabric.js justify behavior more precisely
23160
+ try {
23161
+ // For justify, we need to replicate fabric.js space expansion
23162
+ // Use CSS justify but with specific settings to match fabric.js better
23163
+ cssTextAlign = 'justify';
23164
+
23165
+ // Set text-align-last based on justify type and detected direction
23166
+ // Smart justify: respect detected direction even when fabric alignment doesn't match
23167
+ if (textAlign === 'justify') {
23168
+ this.textarea.style.textAlignLast = autoDetectedDirection === 'rtl' ? 'right' : 'left';
23169
+ } else if (textAlign === 'justify-left') {
23170
+ // If text is RTL but fabric says justify-left, override to justify-right for better UX
23171
+ if (autoDetectedDirection === 'rtl') {
23172
+ this.textarea.style.textAlignLast = 'right';
23173
+ console.log(' → Overrode justify-left to justify-right for RTL text');
23174
+ } else {
23175
+ this.textarea.style.textAlignLast = 'left';
23176
+ }
23177
+ } else if (textAlign === 'justify-right') {
23178
+ // If text is LTR but fabric says justify-right, override to justify-left for better UX
23179
+ if (autoDetectedDirection === 'ltr') {
23180
+ this.textarea.style.textAlignLast = 'left';
23181
+ console.log(' → Overrode justify-right to justify-left for LTR text');
23182
+ } else {
23183
+ this.textarea.style.textAlignLast = 'right';
23184
+ }
23185
+ } else if (textAlign === 'justify-center') {
23186
+ this.textarea.style.textAlignLast = 'center';
23187
+ }
23188
+
23189
+ // Enhanced justify settings for better fabric.js matching
23190
+ this.textarea.style.textJustify = 'inter-word';
23191
+ this.textarea.style.wordSpacing = 'normal';
23192
+
23193
+ // Additional CSS properties for better justify matching
23194
+ this.textarea.style.textAlign = 'justify';
23195
+ this.textarea.style.textAlignLast = this.textarea.style.textAlignLast;
23196
+
23197
+ // Try to force better justify behavior
23198
+ this.textarea.style.textJustifyTrim = 'none';
23199
+ this.textarea.style.textAutospace = 'none';
23200
+ console.log(' → Applied justify alignment:', textAlign, 'with last-line:', this.textarea.style.textAlignLast);
23201
+ } catch (error) {
23202
+ console.warn(' → Justify setup failed, falling back to standard alignment:', error);
23203
+ cssTextAlign = textAlign.replace('justify-', '').replace('justify', 'left');
23204
+ }
23205
+ } else {
23206
+ this.textarea.style.textAlignLast = 'auto';
23207
+ this.textarea.style.textJustify = 'auto';
23208
+ this.textarea.style.wordSpacing = 'normal';
23209
+ console.log(' → Applied standard alignment:', cssTextAlign);
23210
+ }
23211
+ this.textarea.style.textAlign = cssTextAlign;
22171
23212
  this.textarea.style.color = ((_target$fill = target.fill) === null || _target$fill === void 0 ? void 0 : _target$fill.toString()) || '#000';
22172
23213
  this.textarea.style.letterSpacing = `${letterSpacingPx}px`;
22173
- this.textarea.style.direction = target.direction || this.firstStrongDir(this.textarea.value || '');
23214
+
23215
+ // Use the already detected direction from above
23216
+ const fabricDirection = target.direction;
23217
+
23218
+ // Use auto-detected direction for better BiDi support, but respect fabric direction if it makes sense
23219
+ this.textarea.style.direction = autoDetectedDirection || fabricDirection || 'ltr';
22174
23220
  this.textarea.style.fontVariant = 'normal';
22175
23221
  this.textarea.style.fontStretch = 'normal';
22176
- this.textarea.style.textRendering = 'optimizeLegibility';
23222
+ this.textarea.style.textRendering = 'auto'; // Changed from 'optimizeLegibility' to match canvas
22177
23223
  this.textarea.style.fontKerning = 'normal';
22178
23224
  this.textarea.style.fontFeatureSettings = 'normal';
22179
23225
  this.textarea.style.fontVariationSettings = 'normal';
@@ -22184,14 +23230,58 @@ class OverlayEditor {
22184
23230
  this.textarea.style.overflowWrap = 'break-word';
22185
23231
  this.textarea.style.whiteSpace = 'pre-wrap';
22186
23232
  this.textarea.style.hyphens = 'none';
22187
- this.textarea.style.webkitFontSmoothing = 'antialiased';
22188
- this.textarea.style.mozOsxFontSmoothing = 'grayscale';
22189
23233
 
22190
- // Debug: Compare textarea and canvas object bounding boxes
22191
- this.debugBoundingBoxComparison();
23234
+ // DEBUG: Log final CSS properties
23235
+ console.log('🎨 FINAL TEXTAREA CSS:');
23236
+ console.log(' textAlign:', this.textarea.style.textAlign);
23237
+ console.log(' textAlignLast:', this.textarea.style.textAlignLast);
23238
+ console.log(' direction:', this.textarea.style.direction);
23239
+ console.log(' unicodeBidi:', this.textarea.style.unicodeBidi);
23240
+ console.log(' width:', this.textarea.style.width);
23241
+ console.log(' textJustify:', this.textarea.style.textJustify);
23242
+ console.log(' wordSpacing:', this.textarea.style.wordSpacing);
23243
+ console.log(' whiteSpace:', this.textarea.style.whiteSpace);
23244
+
23245
+ // If justify, log Fabric object dimensions for comparison
23246
+ if (textAlign.includes('justify')) {
23247
+ var _calcTextWidth, _ref;
23248
+ console.log('🔧 FABRIC OBJECT JUSTIFY INFO:');
23249
+ console.log(' Fabric width:', target.width);
23250
+ console.log(' Fabric calcTextWidth:', (_calcTextWidth = (_ref = target).calcTextWidth) === null || _calcTextWidth === void 0 ? void 0 : _calcTextWidth.call(_ref));
23251
+ console.log(' Fabric textAlign:', target.textAlign);
23252
+ console.log(' Text lines:', target.textLines);
23253
+ }
23254
+
23255
+ // Debug font properties matching
23256
+ console.log('🔤 FONT PROPERTIES COMPARISON:');
23257
+ console.log(' Fabric fontFamily:', target.fontFamily);
23258
+ console.log(' Fabric fontWeight:', target.fontWeight);
23259
+ console.log(' Fabric fontStyle:', target.fontStyle);
23260
+ console.log(' Fabric fontSize:', target.fontSize);
23261
+ console.log(' → Textarea fontFamily:', this.textarea.style.fontFamily);
23262
+ console.log(' → Textarea fontWeight:', this.textarea.style.fontWeight);
23263
+ console.log(' → Textarea fontStyle:', this.textarea.style.fontStyle);
23264
+ console.log(' → Textarea fontSize:', this.textarea.style.fontSize);
23265
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
22192
23266
 
22193
- // Debug: Compare text wrapping behavior
22194
- this.debugTextWrapping();
23267
+ // Enhanced font rendering to better match fabric.js canvas rendering
23268
+ // Default to auto for more natural rendering
23269
+ this.textarea.style.webkitFontSmoothing = 'auto';
23270
+ this.textarea.style.mozOsxFontSmoothing = 'auto';
23271
+ this.textarea.style.fontSmooth = 'auto';
23272
+ this.textarea.style.textSizeAdjust = 'none';
23273
+
23274
+ // For bold fonts, use subpixel rendering to match canvas thickness better
23275
+ const fontWeight = String(target.fontWeight || 'normal');
23276
+ const isBold = fontWeight === 'bold' || fontWeight === '700' || parseInt(fontWeight) >= 600;
23277
+ if (isBold) {
23278
+ this.textarea.style.webkitFontSmoothing = 'subpixel-antialiased';
23279
+ this.textarea.style.mozOsxFontSmoothing = 'unset';
23280
+ console.log('🔤 Applied enhanced bold rendering for better thickness matching');
23281
+ }
23282
+ console.log('🎨 FONT SMOOTHING APPLIED:');
23283
+ console.log(' webkitFontSmoothing:', this.textarea.style.webkitFontSmoothing);
23284
+ console.log(' mozOsxFontSmoothing:', this.textarea.style.mozOsxFontSmoothing);
22195
23285
 
22196
23286
  // Initial bounds are set correctly by Fabric.js - don't force update here
22197
23287
  }
@@ -22381,6 +23471,11 @@ class OverlayEditor {
22381
23471
  this.canvas.requestRenderAll();
22382
23472
  this.target.setCoords();
22383
23473
  this.applyOverlayStyle();
23474
+
23475
+ // Fix character mapping issues after JSON loading for browser-wrapped fonts
23476
+ if (this.target._fixCharacterMappingAfterJsonLoad) {
23477
+ this.target._fixCharacterMappingAfterJsonLoad();
23478
+ }
22384
23479
  this.textarea.focus();
22385
23480
  this.textarea.setSelectionRange(this.textarea.value.length, this.textarea.value.length);
22386
23481
 
@@ -22426,6 +23521,23 @@ class OverlayEditor {
22426
23521
  // Handle commit/cancel after restoring visibility
22427
23522
  if (commit && !this.isComposing) {
22428
23523
  const finalText = this.textarea.value;
23524
+
23525
+ // Auto-detect text direction and update fabric object if needed
23526
+ const detectedDirection = this.firstStrongDir(finalText);
23527
+ const currentDirection = this.target.direction || 'ltr';
23528
+ if (detectedDirection && detectedDirection !== currentDirection) {
23529
+ console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
23530
+ console.log(` Text content: "${finalText.substring(0, 50)}..."`);
23531
+
23532
+ // Update the fabric object's direction
23533
+ this.target.set('direction', detectedDirection);
23534
+
23535
+ // Force a re-render to apply the direction change
23536
+ this.canvas.requestRenderAll();
23537
+ console.log(`✅ Fabric object direction updated to: ${detectedDirection}`);
23538
+ } else {
23539
+ console.log(`📝 Overlay Exit: Direction unchanged (${currentDirection}), text: "${finalText.substring(0, 30)}..."`);
23540
+ }
22429
23541
  if (this.onCommit) {
22430
23542
  this.onCommit(finalText);
22431
23543
  }
@@ -25518,8 +26630,27 @@ class Textbox extends IText {
25518
26630
  */
25519
26631
  initDimensions() {
25520
26632
  if (!this.initialized) {
26633
+ this.initialized = true;
26634
+ }
26635
+
26636
+ // Prevent rapid recalculations during moves
26637
+ if (this._usingBrowserWrapping) {
26638
+ const now = Date.now();
26639
+ const lastCall = this._lastInitDimensionsTime || 0;
26640
+ const isRapidCall = now - lastCall < 100;
26641
+ const isDuringLoading = this._jsonLoading || !this._browserWrapInitialized;
26642
+ if (isRapidCall && !isDuringLoading) {
26643
+ return;
26644
+ }
26645
+ this._lastInitDimensionsTime = now;
26646
+ }
26647
+
26648
+ // Skip if nothing changed
26649
+ const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
26650
+ if (this._lastDimensionState === currentState && this._textLines && this._textLines.length > 0) {
25521
26651
  return;
25522
26652
  }
26653
+ this._lastDimensionState = currentState;
25523
26654
 
25524
26655
  // Use advanced layout if enabled
25525
26656
  if (this.enableAdvancedLayout) {
@@ -25530,17 +26661,142 @@ class Textbox extends IText {
25530
26661
  // clear dynamicMinWidth as it will be different after we re-wrap line
25531
26662
  this.dynamicMinWidth = 0;
25532
26663
  // wrap lines
25533
- this._styleMap = this._generateStyleMap(this._splitText());
25534
- // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap
25535
- if (this.dynamicMinWidth > this.width) {
26664
+ const splitTextResult = this._splitText();
26665
+ this._styleMap = this._generateStyleMap(splitTextResult);
26666
+
26667
+ // For browser wrapping, ensure _textLines is set from browser results
26668
+ if (this._usingBrowserWrapping && splitTextResult && splitTextResult.lines) {
26669
+ this._textLines = splitTextResult.lines.map(line => line.split(''));
26670
+
26671
+ // Store justify measurements and browser height
26672
+ const justifyMeasurements = splitTextResult.justifySpaceMeasurements;
26673
+ if (justifyMeasurements) {
26674
+ this._styleMap.justifySpaceMeasurements = justifyMeasurements;
26675
+ }
26676
+ const actualHeight = splitTextResult.actualBrowserHeight;
26677
+ if (actualHeight) {
26678
+ this._actualBrowserHeight = actualHeight;
26679
+ }
26680
+ }
26681
+ // Don't auto-resize width when using browser wrapping to prevent width increases during moves
26682
+ if (!this._usingBrowserWrapping && this.dynamicMinWidth > this.width) {
25536
26683
  this._set('width', this.dynamicMinWidth);
25537
26684
  }
26685
+
26686
+ // For browser wrapping fonts (like STV), ensure minimum width for new textboxes
26687
+ // since these fonts can't measure English characters properly
26688
+ if (this._usingBrowserWrapping && this.width < 50) {
26689
+ console.log(`🔤 BROWSER WRAP: Font ${this.fontFamily} has width ${this.width}px, setting to 300px for usability`);
26690
+ this.width = 300;
26691
+ }
26692
+
26693
+ // Mark browser wrapping as initialized when complete
26694
+ if (this._usingBrowserWrapping) {
26695
+ this._browserWrapInitialized = true;
26696
+ }
25538
26697
  if (this.textAlign.includes(JUSTIFY)) {
26698
+ // For browser wrapping fonts, apply browser-calculated justify spaces
26699
+ if (this._usingBrowserWrapping) {
26700
+ console.log('🔤 BROWSER WRAP: Applying browser-calculated justify spaces');
26701
+ this._applyBrowserJustifySpaces();
26702
+ return;
26703
+ }
26704
+
26705
+ // Don't apply justify alignment during drag operations to prevent snapping
26706
+ const now = Date.now();
26707
+ const lastDragTime = this._lastInitDimensionsTime || 0;
26708
+ const isDuringDrag = now - lastDragTime < 200; // 200ms window for drag detection
26709
+
26710
+ if (isDuringDrag) {
26711
+ console.log('🔤 Skipping justify during drag operation to prevent snapping');
26712
+ return;
26713
+ }
26714
+
26715
+ // For non-browser-wrapping fonts, use Fabric's justify system
25539
26716
  // once text is measured we need to make space fatter to make justified text.
25540
- this.enlargeSpaces();
26717
+ // Ensure __charBounds exists and fonts are ready before applying justify
26718
+ if (this.__charBounds && this.__charBounds.length > 0) {
26719
+ // Check if font is ready for accurate justify calculations
26720
+ const fontReady = this._isFontReady ? this._isFontReady() : true;
26721
+ if (fontReady) {
26722
+ this.enlargeSpaces();
26723
+ } else {
26724
+ console.warn('⚠️ Textbox: Font not ready for justify, deferring enlargeSpaces');
26725
+ // Defer justify calculation until font is ready
26726
+ this._scheduleJustifyAfterFontLoad();
26727
+ }
26728
+ } else {
26729
+ console.warn('⚠️ Textbox: __charBounds not ready for justify alignment, deferring enlargeSpaces');
26730
+ // Defer the justify calculation until the next frame
26731
+ setTimeout(() => {
26732
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
26733
+ var _this$canvas;
26734
+ console.log('🔧 Applying deferred Textbox justify alignment');
26735
+ this.enlargeSpaces();
26736
+ (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
26737
+ }
26738
+ }, 0);
26739
+ }
26740
+ }
26741
+ // Calculate height - use Fabric's calculation for proper text rendering space
26742
+ if (this._usingBrowserWrapping && this._textLines && this._textLines.length > 0) {
26743
+ const actualBrowserHeight = this._actualBrowserHeight;
26744
+ const oldHeight = this.height;
26745
+ // Use Fabric's height calculation since it knows how much space text rendering needs
26746
+ this.height = this.calcTextHeight();
26747
+
26748
+ // Force canvas refresh and control update if height changed significantly
26749
+ if (Math.abs(this.height - oldHeight) > 1) {
26750
+ var _this$canvas2, _this$_textLines;
26751
+ this.setCoords();
26752
+ (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
26753
+
26754
+ // DEBUG: Log exact positioning details
26755
+ console.log(`🎯 POSITIONING DEBUG:`);
26756
+ console.log(` Textbox height: ${this.height}px`);
26757
+ console.log(` Textbox top: ${this.top}px`);
26758
+ console.log(` Textbox left: ${this.left}px`);
26759
+ console.log(` Text lines: ${((_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length) || 0}`);
26760
+ console.log(` Font size: ${this.fontSize}px`);
26761
+ console.log(` Line height: ${this.lineHeight || 1.16}`);
26762
+ console.log(` Calculated line height: ${this.fontSize * (this.lineHeight || 1.16)}px`);
26763
+ console.log(` _getTopOffset(): ${this._getTopOffset()}px`);
26764
+ console.log(` calcTextHeight(): ${this.calcTextHeight()}px`);
26765
+ console.log(` Browser height: ${actualBrowserHeight}px`);
26766
+ console.log(` Height difference: ${this.height - this.calcTextHeight()}px`);
26767
+ }
26768
+ } else {
26769
+ this.height = this.calcTextHeight();
26770
+ }
26771
+ }
26772
+
26773
+ /**
26774
+ * Schedule justify calculation after font loads (Textbox-specific)
26775
+ * @private
26776
+ */
26777
+ _scheduleJustifyAfterFontLoad() {
26778
+ if (typeof document === 'undefined' || !('fonts' in document)) {
26779
+ return;
26780
+ }
26781
+
26782
+ // Only schedule if not already waiting
26783
+ if (this._fontJustifyScheduled) {
26784
+ return;
25541
26785
  }
25542
- // clear cache and re-calculate height
25543
- this.height = this.calcTextHeight();
26786
+ this._fontJustifyScheduled = true;
26787
+ const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
26788
+ document.fonts.load(fontSpec).then(() => {
26789
+ var _this$canvas3;
26790
+ this._fontJustifyScheduled = false;
26791
+ console.log('🔧 Textbox: Font loaded, applying justify alignment');
26792
+
26793
+ // Re-run initDimensions to ensure proper justify calculation
26794
+ this.initDimensions();
26795
+ (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
26796
+ }).catch(() => {
26797
+ this._fontJustifyScheduled = false;
26798
+ console.warn('⚠️ Textbox: Font loading failed, justify may be incorrect');
26799
+ });
25544
26800
  }
25545
26801
 
25546
26802
  /**
@@ -25907,19 +27163,33 @@ class Textbox extends IText {
25907
27163
  width: wordWidth
25908
27164
  } = data[i];
25909
27165
  offset += word.length;
25910
- lineWidth += infixWidth + wordWidth - additionalSpace;
25911
- if (lineWidth > maxWidth && !lineJustStarted) {
27166
+
27167
+ // Predictive wrapping: check if adding this word would exceed the width
27168
+ const potentialLineWidth = lineWidth + infixWidth + wordWidth - additionalSpace;
27169
+ // Use exact width to match overlay editor behavior
27170
+ const conservativeMaxWidth = maxWidth; // No artificial buffer
27171
+
27172
+ // Debug logging for wrapping decisions
27173
+ const currentLineText = line.join('');
27174
+ console.log(`🔧 FABRIC WRAP CHECK: "${data[i].word}" -> potential: ${potentialLineWidth.toFixed(1)}px vs limit: ${conservativeMaxWidth.toFixed(1)}px`);
27175
+ if (potentialLineWidth > conservativeMaxWidth && !lineJustStarted) {
27176
+ // This word would exceed the width, wrap before adding it
27177
+ console.log(`🔧 FABRIC WRAP! Line: "${currentLineText}" (${lineWidth.toFixed(1)}px)`);
25912
27178
  graphemeLines.push(line);
25913
27179
  line = [];
25914
- lineWidth = wordWidth;
27180
+ lineWidth = wordWidth; // Start new line with just this word
25915
27181
  lineJustStarted = true;
25916
27182
  } else {
25917
- lineWidth += additionalSpace;
27183
+ // Word fits, add it to current line
27184
+ lineWidth = potentialLineWidth + additionalSpace;
25918
27185
  }
25919
27186
  if (!lineJustStarted && !splitByGrapheme) {
25920
27187
  line.push(infix);
25921
27188
  }
25922
27189
  line = line.concat(word);
27190
+
27191
+ // Debug: show current line after adding word
27192
+ console.log(`🔧 FABRIC AFTER ADD: Line now: "${line.join('')}" (${line.length} chars)`);
25923
27193
  infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset);
25924
27194
  offset++;
25925
27195
  lineJustStarted = false;
@@ -25929,9 +27199,19 @@ class Textbox extends IText {
25929
27199
  // TODO: this code is probably not necessary anymore.
25930
27200
  // it can be moved out of this function since largestWordWidth is now
25931
27201
  // known in advance
25932
- if (largestWordWidth + reservedSpace > this.dynamicMinWidth) {
27202
+ // Don't modify dynamicMinWidth when using browser wrapping to prevent width increases
27203
+ if (!this._usingBrowserWrapping && largestWordWidth + reservedSpace > this.dynamicMinWidth) {
27204
+ console.log(`🔧 FABRIC updating dynamicMinWidth: ${this.dynamicMinWidth} -> ${largestWordWidth - additionalSpace + reservedSpace}`);
25933
27205
  this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace;
27206
+ } else if (this._usingBrowserWrapping) {
27207
+ console.log(`🔤 BROWSER WRAP: Skipping dynamicMinWidth update to prevent width increase`);
25934
27208
  }
27209
+
27210
+ // Debug: show final wrapped lines
27211
+ console.log(`🔧 FABRIC FINAL LINES: ${graphemeLines.length} lines`);
27212
+ graphemeLines.forEach((line, i) => {
27213
+ console.log(` Line ${i + 1}: "${line.join('')}" (${line.length} chars)`);
27214
+ });
25935
27215
  return graphemeLines;
25936
27216
  }
25937
27217
 
@@ -25975,6 +27255,260 @@ class Textbox extends IText {
25975
27255
  * @override
25976
27256
  */
25977
27257
  _splitTextIntoLines(text) {
27258
+ // Check if we need browser wrapping using smart font detection
27259
+ const needsBrowserWrapping = this.fontFamily && fontLacksEnglishGlyphsCached(this.fontFamily);
27260
+ if (needsBrowserWrapping) {
27261
+ // Cache key based on text content, width, font properties, AND text alignment
27262
+ const textHash = text.length + text.slice(0, 50); // Include text content in cache key
27263
+ const cacheKey = `${textHash}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
27264
+
27265
+ // Check if we have a cached result and nothing has changed
27266
+ if (this._browserWrapCache && this._browserWrapCache.key === cacheKey) {
27267
+ const cachedResult = this._browserWrapCache.result;
27268
+
27269
+ // For justify alignment, ensure we have the measurements
27270
+ if (this.textAlign.includes('justify') && !cachedResult.justifySpaceMeasurements) ; else {
27271
+ return cachedResult;
27272
+ }
27273
+ }
27274
+ const result = this._splitTextIntoLinesWithBrowser(text);
27275
+
27276
+ // Cache the result
27277
+ this._browserWrapCache = {
27278
+ key: cacheKey,
27279
+ result
27280
+ };
27281
+
27282
+ // Mark that we used browser wrapping to prevent dynamicMinWidth modifications
27283
+ this._usingBrowserWrapping = true;
27284
+ return result;
27285
+ }
27286
+
27287
+ // Clear the browser wrapping flag when using regular wrapping
27288
+ this._usingBrowserWrapping = false;
27289
+
27290
+ // Default Fabric wrapping for other fonts
27291
+ const newText = super._splitTextIntoLines(text),
27292
+ graphemeLines = this._wrapText(newText.lines, this.width),
27293
+ lines = new Array(graphemeLines.length);
27294
+ for (let i = 0; i < graphemeLines.length; i++) {
27295
+ lines[i] = graphemeLines[i].join('');
27296
+ }
27297
+ newText.lines = lines;
27298
+ newText.graphemeLines = graphemeLines;
27299
+ return newText;
27300
+ }
27301
+
27302
+ /**
27303
+ * Use browser's native text wrapping for accurate handling of fonts without English glyphs
27304
+ * @private
27305
+ */
27306
+ _splitTextIntoLinesWithBrowser(text) {
27307
+ if (typeof document === 'undefined') {
27308
+ // Fallback to regular wrapping in Node.js
27309
+ return this._splitTextIntoLinesDefault(text);
27310
+ }
27311
+
27312
+ // Create a hidden element that mimics the overlay editor
27313
+ const testElement = document.createElement('div');
27314
+ testElement.style.position = 'absolute';
27315
+ testElement.style.left = '-9999px';
27316
+ testElement.style.visibility = 'hidden';
27317
+ testElement.style.fontSize = `${this.fontSize}px`;
27318
+ testElement.style.fontFamily = `"${this.fontFamily}"`;
27319
+ testElement.style.fontWeight = String(this.fontWeight || 'normal');
27320
+ testElement.style.fontStyle = String(this.fontStyle || 'normal');
27321
+ testElement.style.lineHeight = String(this.lineHeight || 1.16);
27322
+ testElement.style.width = `${this.width}px`;
27323
+ testElement.style.direction = this.direction || 'ltr';
27324
+ testElement.style.whiteSpace = 'pre-wrap';
27325
+ testElement.style.wordBreak = 'normal';
27326
+ testElement.style.overflowWrap = 'break-word';
27327
+
27328
+ // Set browser-native text alignment (including justify)
27329
+ if (this.textAlign.includes('justify')) {
27330
+ testElement.style.textAlign = 'justify';
27331
+ testElement.style.textAlignLast = 'auto'; // Let browser decide last line alignment
27332
+ } else {
27333
+ testElement.style.textAlign = this.textAlign;
27334
+ }
27335
+ testElement.textContent = text;
27336
+ document.body.appendChild(testElement);
27337
+
27338
+ // Get the browser's natural line breaks
27339
+ const range = document.createRange();
27340
+ const lines = [];
27341
+ const graphemeLines = [];
27342
+ try {
27343
+ // Simple approach: split by measuring character positions
27344
+ const textNode = testElement.firstChild;
27345
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
27346
+ let currentLineStart = 0;
27347
+ const textLength = text.length;
27348
+ let previousBottom = 0;
27349
+ for (let i = 0; i <= textLength; i++) {
27350
+ range.setStart(textNode, currentLineStart);
27351
+ range.setEnd(textNode, i);
27352
+ const rect = range.getBoundingClientRect();
27353
+ if (i > currentLineStart && (rect.bottom > previousBottom + 5 || i === textLength)) {
27354
+ // New line detected or end of text
27355
+ const lineEnd = i === textLength ? i : i - 1;
27356
+ const lineText = text.substring(currentLineStart, lineEnd).trim();
27357
+ if (lineText) {
27358
+ lines.push(lineText);
27359
+ // Convert to graphemes for compatibility
27360
+ const graphemeLine = lineText.split('');
27361
+ graphemeLines.push(graphemeLine);
27362
+ }
27363
+ currentLineStart = lineEnd;
27364
+ previousBottom = rect.bottom;
27365
+ }
27366
+ }
27367
+ }
27368
+ } catch (error) {
27369
+ console.warn('Browser wrapping failed, using fallback:', error);
27370
+ document.body.removeChild(testElement);
27371
+ return this._splitTextIntoLinesDefault(text);
27372
+ }
27373
+
27374
+ // Extract actual browser height BEFORE removing element
27375
+ const actualBrowserHeight = testElement.scrollHeight;
27376
+ const offsetHeight = testElement.offsetHeight;
27377
+ const clientHeight = testElement.clientHeight;
27378
+ const boundingRect = testElement.getBoundingClientRect();
27379
+ console.log(`🔤 Browser element measurements:`);
27380
+ console.log(` scrollHeight: ${actualBrowserHeight}px (content + padding + hidden overflow)`);
27381
+ console.log(` offsetHeight: ${offsetHeight}px (content + padding + border)`);
27382
+ console.log(` clientHeight: ${clientHeight}px (content + padding, no border/scrollbar)`);
27383
+ console.log(` boundingRect.height: ${boundingRect.height}px (actual rendered height)`);
27384
+ console.log(` Font size: ${this.fontSize}px, Line height: ${this.lineHeight || 1.16}, Lines: ${lines.length}`);
27385
+
27386
+ // For justify alignment, extract space measurements from browser BEFORE removing element
27387
+ let justifySpaceMeasurements = null;
27388
+ if (this.textAlign.includes('justify')) {
27389
+ justifySpaceMeasurements = this._extractJustifySpaceMeasurements(testElement, lines);
27390
+ }
27391
+ document.body.removeChild(testElement);
27392
+ console.log(`🔤 Browser wrapping result: ${lines.length} lines`);
27393
+
27394
+ // Try different height measurements to find the most accurate
27395
+ let bestHeight = actualBrowserHeight;
27396
+
27397
+ // If scrollHeight and offsetHeight differ significantly, investigate
27398
+ if (Math.abs(actualBrowserHeight - offsetHeight) > 2) {
27399
+ console.log(`🔤 Height discrepancy detected: scrollHeight=${actualBrowserHeight}px vs offsetHeight=${offsetHeight}px`);
27400
+ }
27401
+
27402
+ // Consider using boundingRect height if it's larger (sometimes more accurate for visible content)
27403
+ if (boundingRect.height > bestHeight) {
27404
+ console.log(`🔤 Using boundingRect height (${boundingRect.height}px) instead of scrollHeight (${bestHeight}px)`);
27405
+ bestHeight = boundingRect.height;
27406
+ }
27407
+
27408
+ // Font-specific height adjustments for accurate bounding box
27409
+ let adjustedHeight = bestHeight;
27410
+
27411
+ // Fonts without English glyphs need additional height buffer due to different font metrics
27412
+ const lacksEnglishGlyphs = fontLacksEnglishGlyphsCached(this.fontFamily);
27413
+ if (lacksEnglishGlyphs) {
27414
+ const glyphBuffer = this.fontSize * 0.25; // 25% of font size for non-English fonts
27415
+ adjustedHeight = bestHeight + glyphBuffer;
27416
+ console.log(`🔤 Non-English font detected (${this.fontFamily}): Adding ${glyphBuffer}px buffer (${bestHeight}px + ${glyphBuffer}px = ${adjustedHeight}px)`);
27417
+ } else {
27418
+ console.log(`🔤 Standard font (${this.fontFamily}): Using browser height directly (${bestHeight}px)`);
27419
+ }
27420
+ return {
27421
+ _unwrappedLines: [text.split('')],
27422
+ lines: lines,
27423
+ graphemeText: text.split(''),
27424
+ graphemeLines: graphemeLines,
27425
+ justifySpaceMeasurements: justifySpaceMeasurements,
27426
+ actualBrowserHeight: adjustedHeight
27427
+ };
27428
+ }
27429
+
27430
+ /**
27431
+ * Extract justify space measurements from browser
27432
+ * @private
27433
+ */
27434
+ _extractJustifySpaceMeasurements(element, lines) {
27435
+ console.log(`🔤 Extracting browser justify space measurements for ${lines.length} lines`);
27436
+
27437
+ // For now, we'll use a simplified approach:
27438
+ // Apply uniform space expansion to match the line width
27439
+ const spaceWidths = [];
27440
+ lines.forEach((line, lineIndex) => {
27441
+ const lineSpaces = [];
27442
+ const spaceCount = (line.match(/\s/g) || []).length;
27443
+ if (spaceCount > 0 && lineIndex < lines.length - 1) {
27444
+ // Don't justify last line
27445
+ // Calculate how much space expansion is needed
27446
+ const normalSpaceWidth = 6.4; // Default space width for STV font
27447
+ const lineWidth = this.width;
27448
+
27449
+ // Estimate natural line width
27450
+ const charCount = line.length - spaceCount;
27451
+ const avgCharWidth = 12; // Approximate for STV font
27452
+
27453
+ // Calculate expanded space width
27454
+ const remainingSpace = lineWidth - charCount * avgCharWidth;
27455
+ const expandedSpaceWidth = remainingSpace / spaceCount;
27456
+ console.log(`🔤 Line ${lineIndex}: ${spaceCount} spaces, natural: ${normalSpaceWidth}px -> justified: ${expandedSpaceWidth.toFixed(1)}px`);
27457
+
27458
+ // Fill array with expanded space widths for this line
27459
+ for (let i = 0; i < spaceCount; i++) {
27460
+ lineSpaces.push(expandedSpaceWidth);
27461
+ }
27462
+ }
27463
+ spaceWidths.push(lineSpaces);
27464
+ });
27465
+ return spaceWidths;
27466
+ }
27467
+
27468
+ /**
27469
+ * Apply browser-calculated justify space measurements
27470
+ * @private
27471
+ */
27472
+ _applyBrowserJustifySpaces() {
27473
+ if (!this._textLines || !this.__charBounds) {
27474
+ console.warn('🔤 BROWSER JUSTIFY: _textLines or __charBounds not ready');
27475
+ return;
27476
+ }
27477
+
27478
+ // Get space measurements from browser wrapping result
27479
+ const styleMap = this._styleMap;
27480
+ if (!styleMap || !styleMap.justifySpaceMeasurements) {
27481
+ console.warn('🔤 BROWSER JUSTIFY: No justify space measurements available');
27482
+ return;
27483
+ }
27484
+ const spaceWidths = styleMap.justifySpaceMeasurements;
27485
+ console.log('🔤 BROWSER JUSTIFY: Applying space measurements to __charBounds');
27486
+
27487
+ // Apply space widths to character bounds
27488
+ this._textLines.forEach((line, lineIndex) => {
27489
+ if (!this.__charBounds || !this.__charBounds[lineIndex] || !spaceWidths[lineIndex]) return;
27490
+ const lineBounds = this.__charBounds[lineIndex];
27491
+ const lineSpaceWidths = spaceWidths[lineIndex];
27492
+ let spaceIndex = 0;
27493
+ for (let charIndex = 0; charIndex < line.length; charIndex++) {
27494
+ if (/\s/.test(line[charIndex]) && spaceIndex < lineSpaceWidths.length) {
27495
+ const expandedWidth = lineSpaceWidths[spaceIndex];
27496
+ if (lineBounds[charIndex]) {
27497
+ const oldWidth = lineBounds[charIndex].width;
27498
+ lineBounds[charIndex].width = expandedWidth;
27499
+ console.log(`🔤 Line ${lineIndex} space ${spaceIndex}: ${oldWidth.toFixed(1)}px -> ${expandedWidth.toFixed(1)}px`);
27500
+ }
27501
+ spaceIndex++;
27502
+ }
27503
+ }
27504
+ });
27505
+ }
27506
+
27507
+ /**
27508
+ * Fallback to default Fabric wrapping
27509
+ * @private
27510
+ */
27511
+ _splitTextIntoLinesDefault(text) {
25978
27512
  const newText = super._splitTextIntoLines(text),
25979
27513
  graphemeLines = this._wrapText(newText.lines, this.width),
25980
27514
  lines = new Array(graphemeLines.length);
@@ -26009,37 +27543,24 @@ class Textbox extends IText {
26009
27543
  * @private
26010
27544
  */
26011
27545
  initializeEventListeners() {
26012
- var _this$canvas;
27546
+ var _this$canvas4;
26013
27547
  // Track which side is being used for resize to handle position compensation
26014
27548
  let resizeOrigin = null;
26015
27549
 
26016
27550
  // Detect resize origin during resizing
26017
27551
  this.on('resizing', e => {
26018
27552
  // Check transform origin to determine which side is being resized
26019
- console.log('🔍 Resize event data:', e);
26020
27553
  if (e.transform) {
26021
27554
  const {
26022
- originX,
26023
- originY
27555
+ originX
26024
27556
  } = e.transform;
26025
- console.log('🔍 Transform origins:', {
26026
- originX,
26027
- originY
26028
- });
26029
27557
  // originX tells us which side is the anchor - opposite side is being dragged
26030
27558
  resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
26031
- console.log('🎯 Setting resizeOrigin to:', resizeOrigin);
26032
27559
  } else if (e.originX) {
26033
27560
  const {
26034
- originX,
26035
- originY
27561
+ originX
26036
27562
  } = e;
26037
- console.log('🔍 Event origins:', {
26038
- originX,
26039
- originY
26040
- });
26041
27563
  resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
26042
- console.log('🎯 Setting resizeOrigin to:', resizeOrigin);
26043
27564
  }
26044
27565
  });
26045
27566
 
@@ -26047,19 +27568,15 @@ class Textbox extends IText {
26047
27568
  // Use 'modified' event which fires after user releases the mouse
26048
27569
  this.on('modified', () => {
26049
27570
  const currentResizeOrigin = resizeOrigin; // Capture the value before reset
26050
- console.log('✅ Modified event fired - resize complete, triggering safety snap', {
26051
- resizeOrigin: currentResizeOrigin
26052
- });
26053
27571
  // Small delay to ensure text layout is updated
26054
27572
  setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
26055
27573
  resizeOrigin = null; // Reset after capturing
26056
27574
  });
26057
27575
 
26058
27576
  // Also listen to canvas-level modified event as backup
26059
- (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.on('object:modified', e => {
27577
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.on('object:modified', e => {
26060
27578
  if (e.target === this) {
26061
27579
  const currentResizeOrigin = resizeOrigin; // Capture the value before reset
26062
- console.log('✅ Canvas object:modified fired for this textbox');
26063
27580
  setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
26064
27581
  resizeOrigin = null; // Reset after capturing
26065
27582
  }
@@ -26074,38 +27591,17 @@ class Textbox extends IText {
26074
27591
  * @param resizeOrigin - Which side was used for resizing ('left' or 'right')
26075
27592
  */
26076
27593
  safetySnapWidth(resizeOrigin) {
26077
- var _this$_textLines;
26078
- console.log('🔍 safetySnapWidth called', {
26079
- isWrapping: this.isWrapping,
26080
- hasTextLines: !!this._textLines,
26081
- lineCount: ((_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length) || 0,
26082
- currentWidth: this.width,
26083
- type: this.type,
26084
- text: this.text
26085
- });
26086
-
26087
27594
  // For Textbox objects, we always want to check for clipping regardless of isWrapping flag
26088
27595
  if (!this._textLines || this.type.toLowerCase() !== 'textbox' || this._textLines.length === 0) {
26089
- var _this$_textLines2;
26090
- console.log('❌ Early return - missing requirements', {
26091
- hasTextLines: !!this._textLines,
26092
- typeMatch: this.type.toLowerCase() === 'textbox',
26093
- actualType: this.type,
26094
- hasLines: ((_this$_textLines2 = this._textLines) === null || _this$_textLines2 === void 0 ? void 0 : _this$_textLines2.length) > 0
26095
- });
26096
27596
  return;
26097
27597
  }
26098
27598
  const lineCount = this._textLines.length;
26099
27599
  if (lineCount === 0) return;
26100
-
26101
- // Check all lines, not just the last one
26102
- let maxActualLineWidth = 0; // Actual measured width without buffers
26103
27600
  let maxRequiredWidth = 0; // Width including RTL buffer
26104
27601
 
26105
27602
  for (let i = 0; i < lineCount; i++) {
26106
27603
  const lineText = this._textLines[i].join(''); // Convert grapheme array to string
26107
27604
  const lineWidth = this.getLineWidth(i);
26108
- maxActualLineWidth = Math.max(maxActualLineWidth, lineWidth);
26109
27605
 
26110
27606
  // RTL detection - regex for Arabic, Hebrew, and other RTL characters
26111
27607
  const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/;
@@ -26122,14 +27618,9 @@ class Textbox extends IText {
26122
27618
  const safetyThreshold = 2; // px - very subtle trigger
26123
27619
 
26124
27620
  if (maxRequiredWidth > this.width - safetyThreshold) {
26125
- var _this$canvas2;
27621
+ var _this$canvas5;
26126
27622
  // Set width to exactly what's needed + minimal safety margin
26127
27623
  const newWidth = maxRequiredWidth + 1; // Add just 1px safety margin
26128
- console.log(`Safety snap: ${this.width.toFixed(0)}px -> ${newWidth.toFixed(0)}px`, {
26129
- maxActualLineWidth: maxActualLineWidth.toFixed(1),
26130
- maxRequiredWidth: maxRequiredWidth.toFixed(1),
26131
- difference: (newWidth - this.width).toFixed(1)
26132
- });
26133
27624
 
26134
27625
  // Store original position before width change
26135
27626
  const originalLeft = this.left;
@@ -26145,19 +27636,12 @@ class Textbox extends IText {
26145
27636
  // Only compensate position when resizing from left handle
26146
27637
  // Right handle resize doesn't shift the text position
26147
27638
  if (resizeOrigin === 'left') {
26148
- console.log('🔧 Compensating for left-side resize', {
26149
- originalLeft,
26150
- widthIncrease,
26151
- newLeft: originalLeft - widthIncrease
26152
- });
26153
27639
  // When resizing from left, the expansion pushes text right
26154
27640
  // Compensate by moving the textbox left by the width increase
26155
27641
  this.set({
26156
27642
  'left': originalLeft - widthIncrease,
26157
27643
  'top': originalTop
26158
27644
  });
26159
- } else {
26160
- console.log('✅ Right-side resize, no compensation needed');
26161
27645
  }
26162
27646
  this.setCoords();
26163
27647
 
@@ -26167,7 +27651,88 @@ class Textbox extends IText {
26167
27651
  this.__overlayEditor.refresh();
26168
27652
  }, 0);
26169
27653
  }
26170
- (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
27654
+ (_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
27655
+ }
27656
+ }
27657
+
27658
+ /**
27659
+ * Fix character selection mismatch after JSON loading for browser-wrapped fonts
27660
+ * @private
27661
+ */
27662
+ _fixCharacterMappingAfterJsonLoad() {
27663
+ if (this._usingBrowserWrapping) {
27664
+ // Clear all cached states to force fresh text layout calculation
27665
+ this._browserWrapCache = null;
27666
+ this._lastDimensionState = null;
27667
+
27668
+ // Force complete re-initialization
27669
+ this.initDimensions();
27670
+ this._forceClearCache = true;
27671
+
27672
+ // Ensure canvas refresh
27673
+ this.setCoords();
27674
+ if (this.canvas) {
27675
+ this.canvas.requestRenderAll();
27676
+ }
27677
+ }
27678
+ }
27679
+
27680
+ /**
27681
+ * Force complete textbox re-initialization (useful after JSON loading)
27682
+ * Overrides Text version with Textbox-specific logic
27683
+ */
27684
+ forceTextReinitialization() {
27685
+ console.log('🔄 Force reinitializing Textbox object');
27686
+
27687
+ // CRITICAL: Ensure textbox is marked as initialized
27688
+ this.initialized = true;
27689
+
27690
+ // Clear all caches and force dirty state
27691
+ this._clearCache();
27692
+ this.dirty = true;
27693
+ this.dynamicMinWidth = 0;
27694
+
27695
+ // Force isEditing false to ensure clean state
27696
+ this.isEditing = false;
27697
+ console.log(' → Set initialized=true, dirty=true, cleared caches');
27698
+
27699
+ // Re-initialize dimensions (this will handle justify properly)
27700
+ this.initDimensions();
27701
+
27702
+ // Double-check that justify was applied by checking space widths
27703
+ if (this.textAlign.includes('justify') && this.__charBounds) {
27704
+ setTimeout(() => {
27705
+ var _this$canvas6;
27706
+ // Verify justify was applied by checking if space widths vary
27707
+ let hasVariableSpaces = false;
27708
+ this.__charBounds.forEach((lineBounds, i) => {
27709
+ if (lineBounds && this._textLines && this._textLines[i]) {
27710
+ const spaces = lineBounds.filter((bound, j) => /\s/.test(this._textLines[i][j]));
27711
+ if (spaces.length > 1) {
27712
+ const firstSpaceWidth = spaces[0].width;
27713
+ hasVariableSpaces = spaces.some(space => Math.abs(space.width - firstSpaceWidth) > 0.1);
27714
+ }
27715
+ }
27716
+ });
27717
+ if (!hasVariableSpaces && this.__charBounds.length > 0) {
27718
+ console.warn(' ⚠️ Justify spaces still uniform - forcing enlargeSpaces again');
27719
+ if (this.enlargeSpaces) {
27720
+ this.enlargeSpaces();
27721
+ }
27722
+ } else {
27723
+ console.log(' ✅ Justify spaces properly expanded');
27724
+ }
27725
+
27726
+ // Ensure height is recalculated - use browser height if available
27727
+ if (this._usingBrowserWrapping && this._actualBrowserHeight) {
27728
+ this.height = this._actualBrowserHeight;
27729
+ console.log(`🔤 JUSTIFY: Preserved browser height: ${this.height}px`);
27730
+ } else {
27731
+ this.height = this.calcTextHeight();
27732
+ console.log(`🔧 JUSTIFY: Used calcTextHeight: ${this.height}px`);
27733
+ }
27734
+ (_this$canvas6 = this.canvas) === null || _this$canvas6 === void 0 || _this$canvas6.requestRenderAll();
27735
+ }, 10);
26171
27736
  }
26172
27737
  }
26173
27738