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