@nasser-sw/fabric 7.0.1-beta7 → 7.0.1-beta9

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 (148) hide show
  1. package/debug/konva-master/CHANGELOG.md +1475 -0
  2. package/debug/konva-master/LICENSE +22 -0
  3. package/debug/konva-master/README.md +209 -0
  4. package/debug/konva-master/gulpfile.mjs +110 -0
  5. package/debug/konva-master/package.json +139 -0
  6. package/debug/konva-master/release.sh +62 -0
  7. package/debug/konva-master/resources/doc-includes/ContainerParams.txt +6 -0
  8. package/debug/konva-master/resources/doc-includes/NodeParams.txt +20 -0
  9. package/debug/konva-master/resources/doc-includes/ShapeParams.txt +53 -0
  10. package/debug/konva-master/resources/jsdoc.conf.json +28 -0
  11. package/debug/konva-master/rollup.config.mjs +32 -0
  12. package/debug/konva-master/src/Animation.ts +237 -0
  13. package/debug/konva-master/src/BezierFunctions.ts +826 -0
  14. package/debug/konva-master/src/Canvas.ts +230 -0
  15. package/debug/konva-master/src/Container.ts +649 -0
  16. package/debug/konva-master/src/Context.ts +1017 -0
  17. package/debug/konva-master/src/Core.ts +5 -0
  18. package/debug/konva-master/src/DragAndDrop.ts +173 -0
  19. package/debug/konva-master/src/Factory.ts +246 -0
  20. package/debug/konva-master/src/FastLayer.ts +29 -0
  21. package/debug/konva-master/src/Global.ts +210 -0
  22. package/debug/konva-master/src/Group.ts +31 -0
  23. package/debug/konva-master/src/Layer.ts +546 -0
  24. package/debug/konva-master/src/Node.ts +3477 -0
  25. package/debug/konva-master/src/PointerEvents.ts +67 -0
  26. package/debug/konva-master/src/Shape.ts +2081 -0
  27. package/debug/konva-master/src/Stage.ts +1000 -0
  28. package/debug/konva-master/src/Tween.ts +811 -0
  29. package/debug/konva-master/src/Util.ts +1123 -0
  30. package/debug/konva-master/src/Validators.ts +210 -0
  31. package/debug/konva-master/src/_CoreInternals.ts +85 -0
  32. package/debug/konva-master/src/_FullInternals.ts +171 -0
  33. package/debug/konva-master/src/canvas-backend.ts +36 -0
  34. package/debug/konva-master/src/filters/Blur.ts +388 -0
  35. package/debug/konva-master/src/filters/Brighten.ts +48 -0
  36. package/debug/konva-master/src/filters/Brightness.ts +30 -0
  37. package/debug/konva-master/src/filters/Contrast.ts +75 -0
  38. package/debug/konva-master/src/filters/Emboss.ts +207 -0
  39. package/debug/konva-master/src/filters/Enhance.ts +154 -0
  40. package/debug/konva-master/src/filters/Grayscale.ts +25 -0
  41. package/debug/konva-master/src/filters/HSL.ts +108 -0
  42. package/debug/konva-master/src/filters/HSV.ts +106 -0
  43. package/debug/konva-master/src/filters/Invert.ts +23 -0
  44. package/debug/konva-master/src/filters/Kaleidoscope.ts +274 -0
  45. package/debug/konva-master/src/filters/Mask.ts +220 -0
  46. package/debug/konva-master/src/filters/Noise.ts +44 -0
  47. package/debug/konva-master/src/filters/Pixelate.ts +107 -0
  48. package/debug/konva-master/src/filters/Posterize.ts +46 -0
  49. package/debug/konva-master/src/filters/RGB.ts +82 -0
  50. package/debug/konva-master/src/filters/RGBA.ts +103 -0
  51. package/debug/konva-master/src/filters/Sepia.ts +27 -0
  52. package/debug/konva-master/src/filters/Solarize.ts +29 -0
  53. package/debug/konva-master/src/filters/Threshold.ts +44 -0
  54. package/debug/konva-master/src/index.ts +3 -0
  55. package/debug/konva-master/src/shapes/Arc.ts +176 -0
  56. package/debug/konva-master/src/shapes/Arrow.ts +231 -0
  57. package/debug/konva-master/src/shapes/Circle.ts +76 -0
  58. package/debug/konva-master/src/shapes/Ellipse.ts +121 -0
  59. package/debug/konva-master/src/shapes/Image.ts +319 -0
  60. package/debug/konva-master/src/shapes/Label.ts +386 -0
  61. package/debug/konva-master/src/shapes/Line.ts +364 -0
  62. package/debug/konva-master/src/shapes/Path.ts +1013 -0
  63. package/debug/konva-master/src/shapes/Rect.ts +79 -0
  64. package/debug/konva-master/src/shapes/RegularPolygon.ts +167 -0
  65. package/debug/konva-master/src/shapes/Ring.ts +94 -0
  66. package/debug/konva-master/src/shapes/Sprite.ts +370 -0
  67. package/debug/konva-master/src/shapes/Star.ts +125 -0
  68. package/debug/konva-master/src/shapes/Text.ts +1065 -0
  69. package/debug/konva-master/src/shapes/TextPath.ts +583 -0
  70. package/debug/konva-master/src/shapes/Transformer.ts +1889 -0
  71. package/debug/konva-master/src/shapes/Wedge.ts +129 -0
  72. package/debug/konva-master/src/skia-backend.ts +35 -0
  73. package/debug/konva-master/src/types.ts +84 -0
  74. package/debug/konva-master/tsconfig.json +31 -0
  75. package/debug/konva-master/tsconfig.test.json +7 -0
  76. package/dist/index.js +977 -29
  77. package/dist/index.js.map +1 -1
  78. package/dist/index.min.js +1 -1
  79. package/dist/index.min.js.map +1 -1
  80. package/dist/index.min.mjs +1 -1
  81. package/dist/index.min.mjs.map +1 -1
  82. package/dist/index.mjs +977 -29
  83. package/dist/index.mjs.map +1 -1
  84. package/dist/index.node.cjs +977 -29
  85. package/dist/index.node.cjs.map +1 -1
  86. package/dist/index.node.mjs +977 -29
  87. package/dist/index.node.mjs.map +1 -1
  88. package/dist/package.json.min.mjs +1 -1
  89. package/dist/package.json.mjs +1 -1
  90. package/dist/src/shapes/Line.d.ts +1 -0
  91. package/dist/src/shapes/Line.d.ts.map +1 -1
  92. package/dist/src/shapes/Line.min.mjs +1 -1
  93. package/dist/src/shapes/Line.min.mjs.map +1 -1
  94. package/dist/src/shapes/Line.mjs +63 -6
  95. package/dist/src/shapes/Line.mjs.map +1 -1
  96. package/dist/src/shapes/Text/Text.d.ts +19 -0
  97. package/dist/src/shapes/Text/Text.d.ts.map +1 -1
  98. package/dist/src/shapes/Text/Text.min.mjs +1 -1
  99. package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
  100. package/dist/src/shapes/Text/Text.mjs +238 -4
  101. package/dist/src/shapes/Text/Text.mjs.map +1 -1
  102. package/dist/src/shapes/Textbox.d.ts +38 -1
  103. package/dist/src/shapes/Textbox.d.ts.map +1 -1
  104. package/dist/src/shapes/Textbox.min.mjs +1 -1
  105. package/dist/src/shapes/Textbox.min.mjs.map +1 -1
  106. package/dist/src/shapes/Textbox.mjs +497 -15
  107. package/dist/src/shapes/Textbox.mjs.map +1 -1
  108. package/dist/src/text/examples/arabicTextExample.d.ts +60 -0
  109. package/dist/src/text/examples/arabicTextExample.d.ts.map +1 -0
  110. package/dist/src/text/measure.d.ts +9 -0
  111. package/dist/src/text/measure.d.ts.map +1 -1
  112. package/dist/src/text/measure.min.mjs +1 -1
  113. package/dist/src/text/measure.min.mjs.map +1 -1
  114. package/dist/src/text/measure.mjs +175 -4
  115. package/dist/src/text/measure.mjs.map +1 -1
  116. package/dist/src/text/overlayEditor.d.ts.map +1 -1
  117. package/dist/src/text/overlayEditor.min.mjs +1 -1
  118. package/dist/src/text/overlayEditor.min.mjs.map +1 -1
  119. package/dist/src/text/overlayEditor.mjs +7 -0
  120. package/dist/src/text/overlayEditor.mjs.map +1 -1
  121. package/dist/src/text/scriptUtils.d.ts +142 -0
  122. package/dist/src/text/scriptUtils.d.ts.map +1 -0
  123. package/dist/src/text/scriptUtils.min.mjs +2 -0
  124. package/dist/src/text/scriptUtils.min.mjs.map +1 -0
  125. package/dist/src/text/scriptUtils.mjs +212 -0
  126. package/dist/src/text/scriptUtils.mjs.map +1 -0
  127. package/dist-extensions/src/shapes/Line.d.ts +1 -0
  128. package/dist-extensions/src/shapes/Line.d.ts.map +1 -1
  129. package/dist-extensions/src/shapes/Text/Text.d.ts +19 -0
  130. package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
  131. package/dist-extensions/src/shapes/Textbox.d.ts +38 -1
  132. package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
  133. package/dist-extensions/src/text/measure.d.ts +9 -0
  134. package/dist-extensions/src/text/measure.d.ts.map +1 -1
  135. package/dist-extensions/src/text/overlayEditor.d.ts.map +1 -1
  136. package/dist-extensions/src/text/scriptUtils.d.ts +142 -0
  137. package/dist-extensions/src/text/scriptUtils.d.ts.map +1 -0
  138. package/fabric-test-editor.html +2401 -46
  139. package/fabric-test2.html +43 -0
  140. package/fonts/STV Bold.ttf +0 -0
  141. package/fonts/STV Light.ttf +0 -0
  142. package/fonts/STV Regular.ttf +0 -0
  143. package/package.json +1 -1
  144. package/src/shapes/Line.ts +132 -46
  145. package/src/shapes/Text/Text.ts +238 -5
  146. package/src/shapes/Textbox.ts +521 -11
  147. package/src/text/measure.ts +200 -50
  148. package/src/text/overlayEditor.ts +7 -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.1-beta6";
357
+ var version = "7.0.1-beta8";
358
358
 
359
359
  // use this syntax so babel plugin see this import here
360
360
  const VERSION = version;
@@ -17580,6 +17580,7 @@ class Line extends FabricObject {
17580
17580
  _defineProperty(this, "hitStrokeWidth", 'auto');
17581
17581
  _defineProperty(this, "_updatingEndpoints", false);
17582
17582
  _defineProperty(this, "_useEndpointCoords", true);
17583
+ _defineProperty(this, "_exportingSVG", false);
17583
17584
  this.setOptions(options);
17584
17585
  this.x1 = x1;
17585
17586
  this.x2 = x2;
@@ -17791,6 +17792,14 @@ class Line extends FabricObject {
17791
17792
  this.x2 = newX;
17792
17793
  this.y2 = newY;
17793
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
+ }
17794
17803
  this.dirty = true;
17795
17804
  this.setCoords();
17796
17805
  (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
@@ -17861,6 +17870,14 @@ class Line extends FabricObject {
17861
17870
  if (coordProps.includes(key)) {
17862
17871
  this._setWidthHeight();
17863
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
+ }
17864
17881
  }
17865
17882
  if ((key === 'left' || key === 'top') && this.canvas && !this._updatingEndpoints) {
17866
17883
  const deltaX = this.left - oldLeft;
@@ -17871,6 +17888,14 @@ class Line extends FabricObject {
17871
17888
  this.y1 += deltaY;
17872
17889
  this.x2 += deltaX;
17873
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
+ }
17874
17899
  this._updatingEndpoints = false;
17875
17900
  }
17876
17901
  }
@@ -17884,17 +17909,23 @@ class Line extends FabricObject {
17884
17909
  super.render(ctx);
17885
17910
  }
17886
17911
  _renderDirectly(ctx) {
17887
- var _this$stroke;
17888
17912
  if (!this.visible) return;
17889
17913
  ctx.save();
17890
17914
  ctx.globalAlpha = this.opacity;
17891
- ctx.strokeStyle = ((_this$stroke = this.stroke) === null || _this$stroke === void 0 ? void 0 : _this$stroke.toString()) || '#000';
17892
17915
  ctx.lineWidth = this.strokeWidth;
17893
17916
  ctx.lineCap = this.strokeLineCap || 'butt';
17894
17917
  ctx.beginPath();
17895
17918
  ctx.moveTo(this.x1, this.y1);
17896
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
+ }
17897
17927
  ctx.stroke();
17928
+ ctx.strokeStyle = origStrokeStyle;
17898
17929
  ctx.restore();
17899
17930
  }
17900
17931
  _render(ctx) {
@@ -17969,7 +18000,15 @@ class Line extends FabricObject {
17969
18000
  _toSVG() {
17970
18001
  if (this._useEndpointCoords) {
17971
18002
  // Use absolute coordinates to bypass all Fabric.js transforms
17972
- return [`<line stroke="${this.stroke}" stroke-width="${this.strokeWidth}" stroke-linecap="${this.strokeLineCap}" `, `x1="${this.x1}" y1="${this.y1}" x2="${this.x2}" y2="${this.y2}" />\n`];
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`];
17973
18012
  } else {
17974
18013
  // Use standard calcLinePoints for legacy mode
17975
18014
  const {
@@ -17983,9 +18022,26 @@ class Line extends FabricObject {
17983
18022
  }
17984
18023
  toSVG(reviver) {
17985
18024
  if (this._useEndpointCoords) {
17986
- // Override toSVG to prevent Fabric.js from adding transform wrapper
17987
- const markup = this._toSVG().join('');
17988
- return reviver ? reviver(markup) : markup;
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;
18029
+
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;
18033
+
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;
17989
18045
  }
17990
18046
  // Use default behavior for legacy mode
17991
18047
  return super.toSVG(reviver);
@@ -19217,6 +19273,97 @@ function measureGraphemeWithKerning(grapheme, previousGrapheme, options, ctx) {
19217
19273
  };
19218
19274
  }
19219
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
+
19220
19367
  /**
19221
19368
  * Get font metrics for layout calculations
19222
19369
  */
@@ -19230,8 +19377,9 @@ function getFontMetrics(options) {
19230
19377
  const context = getMeasurementContext();
19231
19378
  applyFontStyle(context, options);
19232
19379
 
19233
- // Use 'M' as sample character for metrics
19234
- 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);
19235
19383
  const fontSize = options.fontSize;
19236
19384
 
19237
19385
  // Calculate metrics with fallbacks
@@ -19283,7 +19431,11 @@ function getFontDeclaration(options) {
19283
19431
  } = options;
19284
19432
 
19285
19433
  // Normalize font family (add quotes if needed)
19286
- 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
+
19287
19439
  return `${fontStyle} ${fontWeight} ${fontSize}px ${normalizedFamily}`;
19288
19440
  }
19289
19441
 
@@ -19435,6 +19587,81 @@ const measurementCache = new MeasurementCache();
19435
19587
  const kerningCache = new KerningCache();
19436
19588
  const fontMetricsCache = new FontMetricsCache();
19437
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
+
19438
19665
  /**
19439
19666
  * Unicode and Internationalization Support
19440
19667
  *
@@ -20618,6 +20845,15 @@ class FabricText extends StyledText {
20618
20845
  * Does not return dimensions.
20619
20846
  */
20620
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
+
20621
20857
  // Use advanced layout if enabled
20622
20858
  if (this.enableAdvancedLayout && !this.path) {
20623
20859
  return this.initDimensionsAdvanced();
@@ -20634,7 +20870,21 @@ class FabricText extends StyledText {
20634
20870
  }
20635
20871
  if (this.textAlign.includes(JUSTIFY)) {
20636
20872
  // once text is measured we need to make space fatter to make justified text.
20637
- 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
+ }
20638
20888
  }
20639
20889
  }
20640
20890
 
@@ -20645,7 +20895,7 @@ class FabricText extends StyledText {
20645
20895
  let diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces;
20646
20896
  const isRtl = this.direction === 'rtl';
20647
20897
  for (let i = 0, len = this._textLines.length; i < len; i++) {
20648
- if (this.textAlign !== JUSTIFY && (i === len - 1 || this.isEndOfWrapping(i))) {
20898
+ if (!this.textAlign.includes('justify') && (i === len - 1 || this.isEndOfWrapping(i))) {
20649
20899
  continue;
20650
20900
  }
20651
20901
  accumulatedSpace = 0;
@@ -20654,6 +20904,9 @@ class FabricText extends StyledText {
20654
20904
  if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) {
20655
20905
  numberOfSpaces = spaces.length;
20656
20906
  diffSpace = (this.width - currentLineWidth) / numberOfSpaces;
20907
+ console.log(`🔧 EnlargeSpaces Line ${i}:`);
20908
+ console.log(` Current width: ${currentLineWidth}, Target: ${this.width}`);
20909
+ console.log(` Spaces: ${numberOfSpaces}, diffSpace: ${diffSpace.toFixed(2)}`);
20657
20910
  if (isRtl) {
20658
20911
  for (let j = 0; j < line.length; j++) {
20659
20912
  if (this._reSpaceAndTab.test(line[j])) ;
@@ -20769,6 +21022,18 @@ class FabricText extends StyledText {
20769
21022
 
20770
21023
  // Convert layout to legacy format for compatibility
20771
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
+ }
20772
21037
  this.dirty = true;
20773
21038
  }
20774
21039
 
@@ -21780,7 +22045,19 @@ class FabricText extends StyledText {
21780
22045
  fontSize = this.fontSize
21781
22046
  } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
21782
22047
  let forMeasuring = arguments.length > 1 ? arguments[1] : undefined;
21783
- 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
+ }
21784
22061
  return [fontStyle, fontWeight, `${forMeasuring ? this.CACHE_FONT_SIZE : fontSize}px`, parsedFontFamily].join(' ');
21785
22062
  }
21786
22063
 
@@ -21824,7 +22101,13 @@ class FabricText extends StyledText {
21824
22101
  newLine = ['\n'];
21825
22102
  let newText = [];
21826
22103
  for (let i = 0; i < lines.length; i++) {
21827
- 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
+ }
21828
22111
  newText = newText.concat(newLines[i], newLine);
21829
22112
  }
21830
22113
  newText.pop();
@@ -21836,6 +22119,14 @@ class FabricText extends StyledText {
21836
22119
  };
21837
22120
  }
21838
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
+
21839
22130
  /**
21840
22131
  * Returns object representation of an instance
21841
22132
  * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
@@ -21958,6 +22249,88 @@ class FabricText extends StyledText {
21958
22249
 
21959
22250
  /* _FROM_SVG_END_ */
21960
22251
 
22252
+ /**
22253
+ * Check if the font is ready for accurate measurements
22254
+ * @private
22255
+ */
22256
+ _isFontReady() {
22257
+ if (typeof document === 'undefined' || !('fonts' in document)) {
22258
+ return true; // Assume ready in non-browser environments
22259
+ }
22260
+ try {
22261
+ return document.fonts.check(`${this.fontSize}px ${this.fontFamily}`);
22262
+ } catch (e) {
22263
+ return true; // Fallback to assuming ready if check fails
22264
+ }
22265
+ }
22266
+
22267
+ /**
22268
+ * Schedule re-initialization after font loads
22269
+ * @private
22270
+ */
22271
+ _scheduleInitAfterFontLoad() {
22272
+ if (typeof document === 'undefined' || !('fonts' in document)) {
22273
+ return;
22274
+ }
22275
+
22276
+ // Only schedule if not already waiting
22277
+ if (this._fontLoadScheduled) {
22278
+ return;
22279
+ }
22280
+ this._fontLoadScheduled = true;
22281
+ const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
22282
+ document.fonts.load(fontSpec).then(() => {
22283
+ this._fontLoadScheduled = false;
22284
+ // Re-initialize dimensions with proper font metrics
22285
+ this.initDimensions();
22286
+
22287
+ // Extra step for justify alignment after font loading
22288
+ if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
22289
+ setTimeout(() => {
22290
+ var _this$canvas3;
22291
+ if (this.enlargeSpaces) {
22292
+ this.enlargeSpaces();
22293
+ }
22294
+ (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
22295
+ }, 10);
22296
+ } else {
22297
+ var _this$canvas4;
22298
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.requestRenderAll();
22299
+ }
22300
+ }).catch(() => {
22301
+ this._fontLoadScheduled = false;
22302
+ });
22303
+ }
22304
+
22305
+ /**
22306
+ * Force complete text re-initialization (useful after JSON loading)
22307
+ */
22308
+ forceTextReinitialization() {
22309
+ console.log('🔄 Force reinitializing text object');
22310
+
22311
+ // Clear all caches
22312
+ this._clearCache();
22313
+ this.dirty = true;
22314
+
22315
+ // Force text splitting to rebuild internal structures
22316
+ this._splitText();
22317
+
22318
+ // Re-initialize dimensions
22319
+ this.initDimensions();
22320
+
22321
+ // Special handling for justify alignment
22322
+ if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
22323
+ // Ensure justify is applied after dimensions are set
22324
+ setTimeout(() => {
22325
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
22326
+ var _this$canvas5;
22327
+ this.enlargeSpaces();
22328
+ (_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
22329
+ }
22330
+ }, 10);
22331
+ }
22332
+ }
22333
+
21961
22334
  /**
21962
22335
  * Returns FabricText instance from an object representation
21963
22336
  * @param {Object} object plain js Object to create an instance from
@@ -21969,6 +22342,93 @@ class FabricText extends StyledText {
21969
22342
  styles: stylesFromArray(object.styles || {}, object.text)
21970
22343
  }, {
21971
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
+ }
21972
22432
  });
21973
22433
  }
21974
22434
  }
@@ -22612,6 +23072,13 @@ class OverlayEditor {
22612
23072
 
22613
23073
  // Apply all other font and text styles to match Fabric
22614
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
+ }
22615
23082
  this.textarea.style.fontSize = `${finalFontSize}px`;
22616
23083
  this.textarea.style.lineHeight = String(fabricLineHeight);
22617
23084
  this.textarea.style.fontFamily = target.fontFamily || 'Arial';
@@ -26102,8 +26569,27 @@ class Textbox extends IText {
26102
26569
  */
26103
26570
  initDimensions() {
26104
26571
  if (!this.initialized) {
26572
+ this.initialized = true;
26573
+ }
26574
+
26575
+ // Prevent rapid recalculations during moves
26576
+ if (this._usingBrowserWrapping) {
26577
+ const now = Date.now();
26578
+ const lastCall = this._lastInitDimensionsTime || 0;
26579
+ const isRapidCall = now - lastCall < 100;
26580
+ const isDuringLoading = this._jsonLoading || !this._browserWrapInitialized;
26581
+ if (isRapidCall && !isDuringLoading) {
26582
+ return;
26583
+ }
26584
+ this._lastInitDimensionsTime = now;
26585
+ }
26586
+
26587
+ // Skip if nothing changed
26588
+ const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
26589
+ if (this._lastDimensionState === currentState && this._textLines && this._textLines.length > 0) {
26105
26590
  return;
26106
26591
  }
26592
+ this._lastDimensionState = currentState;
26107
26593
 
26108
26594
  // Use advanced layout if enabled
26109
26595
  if (this.enableAdvancedLayout) {
@@ -26114,17 +26600,142 @@ class Textbox extends IText {
26114
26600
  // clear dynamicMinWidth as it will be different after we re-wrap line
26115
26601
  this.dynamicMinWidth = 0;
26116
26602
  // wrap lines
26117
- this._styleMap = this._generateStyleMap(this._splitText());
26118
- // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap
26119
- if (this.dynamicMinWidth > this.width) {
26603
+ const splitTextResult = this._splitText();
26604
+ this._styleMap = this._generateStyleMap(splitTextResult);
26605
+
26606
+ // For browser wrapping, ensure _textLines is set from browser results
26607
+ if (this._usingBrowserWrapping && splitTextResult && splitTextResult.lines) {
26608
+ this._textLines = splitTextResult.lines.map(line => line.split(''));
26609
+
26610
+ // Store justify measurements and browser height
26611
+ const justifyMeasurements = splitTextResult.justifySpaceMeasurements;
26612
+ if (justifyMeasurements) {
26613
+ this._styleMap.justifySpaceMeasurements = justifyMeasurements;
26614
+ }
26615
+ const actualHeight = splitTextResult.actualBrowserHeight;
26616
+ if (actualHeight) {
26617
+ this._actualBrowserHeight = actualHeight;
26618
+ }
26619
+ }
26620
+ // Don't auto-resize width when using browser wrapping to prevent width increases during moves
26621
+ if (!this._usingBrowserWrapping && this.dynamicMinWidth > this.width) {
26120
26622
  this._set('width', this.dynamicMinWidth);
26121
26623
  }
26624
+
26625
+ // For browser wrapping fonts (like STV), ensure minimum width for new textboxes
26626
+ // since these fonts can't measure English characters properly
26627
+ if (this._usingBrowserWrapping && this.width < 50) {
26628
+ console.log(`🔤 BROWSER WRAP: Font ${this.fontFamily} has width ${this.width}px, setting to 300px for usability`);
26629
+ this.width = 300;
26630
+ }
26631
+
26632
+ // Mark browser wrapping as initialized when complete
26633
+ if (this._usingBrowserWrapping) {
26634
+ this._browserWrapInitialized = true;
26635
+ }
26122
26636
  if (this.textAlign.includes(JUSTIFY)) {
26637
+ // For browser wrapping fonts, apply browser-calculated justify spaces
26638
+ if (this._usingBrowserWrapping) {
26639
+ console.log('🔤 BROWSER WRAP: Applying browser-calculated justify spaces');
26640
+ this._applyBrowserJustifySpaces();
26641
+ return;
26642
+ }
26643
+
26644
+ // Don't apply justify alignment during drag operations to prevent snapping
26645
+ const now = Date.now();
26646
+ const lastDragTime = this._lastInitDimensionsTime || 0;
26647
+ const isDuringDrag = now - lastDragTime < 200; // 200ms window for drag detection
26648
+
26649
+ if (isDuringDrag) {
26650
+ console.log('🔤 Skipping justify during drag operation to prevent snapping');
26651
+ return;
26652
+ }
26653
+
26654
+ // For non-browser-wrapping fonts, use Fabric's justify system
26123
26655
  // once text is measured we need to make space fatter to make justified text.
26124
- this.enlargeSpaces();
26656
+ // Ensure __charBounds exists and fonts are ready before applying justify
26657
+ if (this.__charBounds && this.__charBounds.length > 0) {
26658
+ // Check if font is ready for accurate justify calculations
26659
+ const fontReady = this._isFontReady ? this._isFontReady() : true;
26660
+ if (fontReady) {
26661
+ this.enlargeSpaces();
26662
+ } else {
26663
+ console.warn('⚠️ Textbox: Font not ready for justify, deferring enlargeSpaces');
26664
+ // Defer justify calculation until font is ready
26665
+ this._scheduleJustifyAfterFontLoad();
26666
+ }
26667
+ } else {
26668
+ console.warn('⚠️ Textbox: __charBounds not ready for justify alignment, deferring enlargeSpaces');
26669
+ // Defer the justify calculation until the next frame
26670
+ setTimeout(() => {
26671
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
26672
+ var _this$canvas;
26673
+ console.log('🔧 Applying deferred Textbox justify alignment');
26674
+ this.enlargeSpaces();
26675
+ (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
26676
+ }
26677
+ }, 0);
26678
+ }
26679
+ }
26680
+ // Calculate height - use Fabric's calculation for proper text rendering space
26681
+ if (this._usingBrowserWrapping && this._textLines && this._textLines.length > 0) {
26682
+ const actualBrowserHeight = this._actualBrowserHeight;
26683
+ const oldHeight = this.height;
26684
+ // Use Fabric's height calculation since it knows how much space text rendering needs
26685
+ this.height = this.calcTextHeight();
26686
+
26687
+ // Force canvas refresh and control update if height changed significantly
26688
+ if (Math.abs(this.height - oldHeight) > 1) {
26689
+ var _this$canvas2, _this$_textLines;
26690
+ this.setCoords();
26691
+ (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
26692
+
26693
+ // DEBUG: Log exact positioning details
26694
+ console.log(`🎯 POSITIONING DEBUG:`);
26695
+ console.log(` Textbox height: ${this.height}px`);
26696
+ console.log(` Textbox top: ${this.top}px`);
26697
+ console.log(` Textbox left: ${this.left}px`);
26698
+ console.log(` Text lines: ${((_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length) || 0}`);
26699
+ console.log(` Font size: ${this.fontSize}px`);
26700
+ console.log(` Line height: ${this.lineHeight || 1.16}`);
26701
+ console.log(` Calculated line height: ${this.fontSize * (this.lineHeight || 1.16)}px`);
26702
+ console.log(` _getTopOffset(): ${this._getTopOffset()}px`);
26703
+ console.log(` calcTextHeight(): ${this.calcTextHeight()}px`);
26704
+ console.log(` Browser height: ${actualBrowserHeight}px`);
26705
+ console.log(` Height difference: ${this.height - this.calcTextHeight()}px`);
26706
+ }
26707
+ } else {
26708
+ this.height = this.calcTextHeight();
26709
+ }
26710
+ }
26711
+
26712
+ /**
26713
+ * Schedule justify calculation after font loads (Textbox-specific)
26714
+ * @private
26715
+ */
26716
+ _scheduleJustifyAfterFontLoad() {
26717
+ if (typeof document === 'undefined' || !('fonts' in document)) {
26718
+ return;
26125
26719
  }
26126
- // clear cache and re-calculate height
26127
- this.height = this.calcTextHeight();
26720
+
26721
+ // Only schedule if not already waiting
26722
+ if (this._fontJustifyScheduled) {
26723
+ return;
26724
+ }
26725
+ this._fontJustifyScheduled = true;
26726
+ const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
26727
+ document.fonts.load(fontSpec).then(() => {
26728
+ var _this$canvas3;
26729
+ this._fontJustifyScheduled = false;
26730
+ console.log('🔧 Textbox: Font loaded, applying justify alignment');
26731
+
26732
+ // Re-run initDimensions to ensure proper justify calculation
26733
+ this.initDimensions();
26734
+ (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
26735
+ }).catch(() => {
26736
+ this._fontJustifyScheduled = false;
26737
+ console.warn('⚠️ Textbox: Font loading failed, justify may be incorrect');
26738
+ });
26128
26739
  }
26129
26740
 
26130
26741
  /**
@@ -26491,19 +27102,33 @@ class Textbox extends IText {
26491
27102
  width: wordWidth
26492
27103
  } = data[i];
26493
27104
  offset += word.length;
26494
- lineWidth += infixWidth + wordWidth - additionalSpace;
26495
- if (lineWidth > maxWidth && !lineJustStarted) {
27105
+
27106
+ // Predictive wrapping: check if adding this word would exceed the width
27107
+ const potentialLineWidth = lineWidth + infixWidth + wordWidth - additionalSpace;
27108
+ // Use exact width to match overlay editor behavior
27109
+ const conservativeMaxWidth = maxWidth; // No artificial buffer
27110
+
27111
+ // Debug logging for wrapping decisions
27112
+ const currentLineText = line.join('');
27113
+ console.log(`🔧 FABRIC WRAP CHECK: "${data[i].word}" -> potential: ${potentialLineWidth.toFixed(1)}px vs limit: ${conservativeMaxWidth.toFixed(1)}px`);
27114
+ if (potentialLineWidth > conservativeMaxWidth && !lineJustStarted) {
27115
+ // This word would exceed the width, wrap before adding it
27116
+ console.log(`🔧 FABRIC WRAP! Line: "${currentLineText}" (${lineWidth.toFixed(1)}px)`);
26496
27117
  graphemeLines.push(line);
26497
27118
  line = [];
26498
- lineWidth = wordWidth;
27119
+ lineWidth = wordWidth; // Start new line with just this word
26499
27120
  lineJustStarted = true;
26500
27121
  } else {
26501
- lineWidth += additionalSpace;
27122
+ // Word fits, add it to current line
27123
+ lineWidth = potentialLineWidth + additionalSpace;
26502
27124
  }
26503
27125
  if (!lineJustStarted && !splitByGrapheme) {
26504
27126
  line.push(infix);
26505
27127
  }
26506
27128
  line = line.concat(word);
27129
+
27130
+ // Debug: show current line after adding word
27131
+ console.log(`🔧 FABRIC AFTER ADD: Line now: "${line.join('')}" (${line.length} chars)`);
26507
27132
  infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset);
26508
27133
  offset++;
26509
27134
  lineJustStarted = false;
@@ -26513,9 +27138,19 @@ class Textbox extends IText {
26513
27138
  // TODO: this code is probably not necessary anymore.
26514
27139
  // it can be moved out of this function since largestWordWidth is now
26515
27140
  // known in advance
26516
- if (largestWordWidth + reservedSpace > this.dynamicMinWidth) {
27141
+ // Don't modify dynamicMinWidth when using browser wrapping to prevent width increases
27142
+ if (!this._usingBrowserWrapping && largestWordWidth + reservedSpace > this.dynamicMinWidth) {
27143
+ console.log(`🔧 FABRIC updating dynamicMinWidth: ${this.dynamicMinWidth} -> ${largestWordWidth - additionalSpace + reservedSpace}`);
26517
27144
  this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace;
27145
+ } else if (this._usingBrowserWrapping) {
27146
+ console.log(`🔤 BROWSER WRAP: Skipping dynamicMinWidth update to prevent width increase`);
26518
27147
  }
27148
+
27149
+ // Debug: show final wrapped lines
27150
+ console.log(`🔧 FABRIC FINAL LINES: ${graphemeLines.length} lines`);
27151
+ graphemeLines.forEach((line, i) => {
27152
+ console.log(` Line ${i + 1}: "${line.join('')}" (${line.length} chars)`);
27153
+ });
26519
27154
  return graphemeLines;
26520
27155
  }
26521
27156
 
@@ -26559,6 +27194,260 @@ class Textbox extends IText {
26559
27194
  * @override
26560
27195
  */
26561
27196
  _splitTextIntoLines(text) {
27197
+ // Check if we need browser wrapping using smart font detection
27198
+ const needsBrowserWrapping = this.fontFamily && fontLacksEnglishGlyphsCached(this.fontFamily);
27199
+ if (needsBrowserWrapping) {
27200
+ // Cache key based on text content, width, font properties, AND text alignment
27201
+ const textHash = text.length + text.slice(0, 50); // Include text content in cache key
27202
+ const cacheKey = `${textHash}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
27203
+
27204
+ // Check if we have a cached result and nothing has changed
27205
+ if (this._browserWrapCache && this._browserWrapCache.key === cacheKey) {
27206
+ const cachedResult = this._browserWrapCache.result;
27207
+
27208
+ // For justify alignment, ensure we have the measurements
27209
+ if (this.textAlign.includes('justify') && !cachedResult.justifySpaceMeasurements) ; else {
27210
+ return cachedResult;
27211
+ }
27212
+ }
27213
+ const result = this._splitTextIntoLinesWithBrowser(text);
27214
+
27215
+ // Cache the result
27216
+ this._browserWrapCache = {
27217
+ key: cacheKey,
27218
+ result
27219
+ };
27220
+
27221
+ // Mark that we used browser wrapping to prevent dynamicMinWidth modifications
27222
+ this._usingBrowserWrapping = true;
27223
+ return result;
27224
+ }
27225
+
27226
+ // Clear the browser wrapping flag when using regular wrapping
27227
+ this._usingBrowserWrapping = false;
27228
+
27229
+ // Default Fabric wrapping for other fonts
27230
+ const newText = super._splitTextIntoLines(text),
27231
+ graphemeLines = this._wrapText(newText.lines, this.width),
27232
+ lines = new Array(graphemeLines.length);
27233
+ for (let i = 0; i < graphemeLines.length; i++) {
27234
+ lines[i] = graphemeLines[i].join('');
27235
+ }
27236
+ newText.lines = lines;
27237
+ newText.graphemeLines = graphemeLines;
27238
+ return newText;
27239
+ }
27240
+
27241
+ /**
27242
+ * Use browser's native text wrapping for accurate handling of fonts without English glyphs
27243
+ * @private
27244
+ */
27245
+ _splitTextIntoLinesWithBrowser(text) {
27246
+ if (typeof document === 'undefined') {
27247
+ // Fallback to regular wrapping in Node.js
27248
+ return this._splitTextIntoLinesDefault(text);
27249
+ }
27250
+
27251
+ // Create a hidden element that mimics the overlay editor
27252
+ const testElement = document.createElement('div');
27253
+ testElement.style.position = 'absolute';
27254
+ testElement.style.left = '-9999px';
27255
+ testElement.style.visibility = 'hidden';
27256
+ testElement.style.fontSize = `${this.fontSize}px`;
27257
+ testElement.style.fontFamily = `"${this.fontFamily}"`;
27258
+ testElement.style.fontWeight = String(this.fontWeight || 'normal');
27259
+ testElement.style.fontStyle = String(this.fontStyle || 'normal');
27260
+ testElement.style.lineHeight = String(this.lineHeight || 1.16);
27261
+ testElement.style.width = `${this.width}px`;
27262
+ testElement.style.direction = this.direction || 'ltr';
27263
+ testElement.style.whiteSpace = 'pre-wrap';
27264
+ testElement.style.wordBreak = 'normal';
27265
+ testElement.style.overflowWrap = 'break-word';
27266
+
27267
+ // Set browser-native text alignment (including justify)
27268
+ if (this.textAlign.includes('justify')) {
27269
+ testElement.style.textAlign = 'justify';
27270
+ testElement.style.textAlignLast = 'auto'; // Let browser decide last line alignment
27271
+ } else {
27272
+ testElement.style.textAlign = this.textAlign;
27273
+ }
27274
+ testElement.textContent = text;
27275
+ document.body.appendChild(testElement);
27276
+
27277
+ // Get the browser's natural line breaks
27278
+ const range = document.createRange();
27279
+ const lines = [];
27280
+ const graphemeLines = [];
27281
+ try {
27282
+ // Simple approach: split by measuring character positions
27283
+ const textNode = testElement.firstChild;
27284
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
27285
+ let currentLineStart = 0;
27286
+ const textLength = text.length;
27287
+ let previousBottom = 0;
27288
+ for (let i = 0; i <= textLength; i++) {
27289
+ range.setStart(textNode, currentLineStart);
27290
+ range.setEnd(textNode, i);
27291
+ const rect = range.getBoundingClientRect();
27292
+ if (i > currentLineStart && (rect.bottom > previousBottom + 5 || i === textLength)) {
27293
+ // New line detected or end of text
27294
+ const lineEnd = i === textLength ? i : i - 1;
27295
+ const lineText = text.substring(currentLineStart, lineEnd).trim();
27296
+ if (lineText) {
27297
+ lines.push(lineText);
27298
+ // Convert to graphemes for compatibility
27299
+ const graphemeLine = lineText.split('');
27300
+ graphemeLines.push(graphemeLine);
27301
+ }
27302
+ currentLineStart = lineEnd;
27303
+ previousBottom = rect.bottom;
27304
+ }
27305
+ }
27306
+ }
27307
+ } catch (error) {
27308
+ console.warn('Browser wrapping failed, using fallback:', error);
27309
+ document.body.removeChild(testElement);
27310
+ return this._splitTextIntoLinesDefault(text);
27311
+ }
27312
+
27313
+ // Extract actual browser height BEFORE removing element
27314
+ const actualBrowserHeight = testElement.scrollHeight;
27315
+ const offsetHeight = testElement.offsetHeight;
27316
+ const clientHeight = testElement.clientHeight;
27317
+ const boundingRect = testElement.getBoundingClientRect();
27318
+ console.log(`🔤 Browser element measurements:`);
27319
+ console.log(` scrollHeight: ${actualBrowserHeight}px (content + padding + hidden overflow)`);
27320
+ console.log(` offsetHeight: ${offsetHeight}px (content + padding + border)`);
27321
+ console.log(` clientHeight: ${clientHeight}px (content + padding, no border/scrollbar)`);
27322
+ console.log(` boundingRect.height: ${boundingRect.height}px (actual rendered height)`);
27323
+ console.log(` Font size: ${this.fontSize}px, Line height: ${this.lineHeight || 1.16}, Lines: ${lines.length}`);
27324
+
27325
+ // For justify alignment, extract space measurements from browser BEFORE removing element
27326
+ let justifySpaceMeasurements = null;
27327
+ if (this.textAlign.includes('justify')) {
27328
+ justifySpaceMeasurements = this._extractJustifySpaceMeasurements(testElement, lines);
27329
+ }
27330
+ document.body.removeChild(testElement);
27331
+ console.log(`🔤 Browser wrapping result: ${lines.length} lines`);
27332
+
27333
+ // Try different height measurements to find the most accurate
27334
+ let bestHeight = actualBrowserHeight;
27335
+
27336
+ // If scrollHeight and offsetHeight differ significantly, investigate
27337
+ if (Math.abs(actualBrowserHeight - offsetHeight) > 2) {
27338
+ console.log(`🔤 Height discrepancy detected: scrollHeight=${actualBrowserHeight}px vs offsetHeight=${offsetHeight}px`);
27339
+ }
27340
+
27341
+ // Consider using boundingRect height if it's larger (sometimes more accurate for visible content)
27342
+ if (boundingRect.height > bestHeight) {
27343
+ console.log(`🔤 Using boundingRect height (${boundingRect.height}px) instead of scrollHeight (${bestHeight}px)`);
27344
+ bestHeight = boundingRect.height;
27345
+ }
27346
+
27347
+ // Font-specific height adjustments for accurate bounding box
27348
+ let adjustedHeight = bestHeight;
27349
+
27350
+ // Fonts without English glyphs need additional height buffer due to different font metrics
27351
+ const lacksEnglishGlyphs = fontLacksEnglishGlyphsCached(this.fontFamily);
27352
+ if (lacksEnglishGlyphs) {
27353
+ const glyphBuffer = this.fontSize * 0.25; // 25% of font size for non-English fonts
27354
+ adjustedHeight = bestHeight + glyphBuffer;
27355
+ console.log(`🔤 Non-English font detected (${this.fontFamily}): Adding ${glyphBuffer}px buffer (${bestHeight}px + ${glyphBuffer}px = ${adjustedHeight}px)`);
27356
+ } else {
27357
+ console.log(`🔤 Standard font (${this.fontFamily}): Using browser height directly (${bestHeight}px)`);
27358
+ }
27359
+ return {
27360
+ _unwrappedLines: [text.split('')],
27361
+ lines: lines,
27362
+ graphemeText: text.split(''),
27363
+ graphemeLines: graphemeLines,
27364
+ justifySpaceMeasurements: justifySpaceMeasurements,
27365
+ actualBrowserHeight: adjustedHeight
27366
+ };
27367
+ }
27368
+
27369
+ /**
27370
+ * Extract justify space measurements from browser
27371
+ * @private
27372
+ */
27373
+ _extractJustifySpaceMeasurements(element, lines) {
27374
+ console.log(`🔤 Extracting browser justify space measurements for ${lines.length} lines`);
27375
+
27376
+ // For now, we'll use a simplified approach:
27377
+ // Apply uniform space expansion to match the line width
27378
+ const spaceWidths = [];
27379
+ lines.forEach((line, lineIndex) => {
27380
+ const lineSpaces = [];
27381
+ const spaceCount = (line.match(/\s/g) || []).length;
27382
+ if (spaceCount > 0 && lineIndex < lines.length - 1) {
27383
+ // Don't justify last line
27384
+ // Calculate how much space expansion is needed
27385
+ const normalSpaceWidth = 6.4; // Default space width for STV font
27386
+ const lineWidth = this.width;
27387
+
27388
+ // Estimate natural line width
27389
+ const charCount = line.length - spaceCount;
27390
+ const avgCharWidth = 12; // Approximate for STV font
27391
+
27392
+ // Calculate expanded space width
27393
+ const remainingSpace = lineWidth - charCount * avgCharWidth;
27394
+ const expandedSpaceWidth = remainingSpace / spaceCount;
27395
+ console.log(`🔤 Line ${lineIndex}: ${spaceCount} spaces, natural: ${normalSpaceWidth}px -> justified: ${expandedSpaceWidth.toFixed(1)}px`);
27396
+
27397
+ // Fill array with expanded space widths for this line
27398
+ for (let i = 0; i < spaceCount; i++) {
27399
+ lineSpaces.push(expandedSpaceWidth);
27400
+ }
27401
+ }
27402
+ spaceWidths.push(lineSpaces);
27403
+ });
27404
+ return spaceWidths;
27405
+ }
27406
+
27407
+ /**
27408
+ * Apply browser-calculated justify space measurements
27409
+ * @private
27410
+ */
27411
+ _applyBrowserJustifySpaces() {
27412
+ if (!this._textLines || !this.__charBounds) {
27413
+ console.warn('🔤 BROWSER JUSTIFY: _textLines or __charBounds not ready');
27414
+ return;
27415
+ }
27416
+
27417
+ // Get space measurements from browser wrapping result
27418
+ const styleMap = this._styleMap;
27419
+ if (!styleMap || !styleMap.justifySpaceMeasurements) {
27420
+ console.warn('🔤 BROWSER JUSTIFY: No justify space measurements available');
27421
+ return;
27422
+ }
27423
+ const spaceWidths = styleMap.justifySpaceMeasurements;
27424
+ console.log('🔤 BROWSER JUSTIFY: Applying space measurements to __charBounds');
27425
+
27426
+ // Apply space widths to character bounds
27427
+ this._textLines.forEach((line, lineIndex) => {
27428
+ if (!this.__charBounds || !this.__charBounds[lineIndex] || !spaceWidths[lineIndex]) return;
27429
+ const lineBounds = this.__charBounds[lineIndex];
27430
+ const lineSpaceWidths = spaceWidths[lineIndex];
27431
+ let spaceIndex = 0;
27432
+ for (let charIndex = 0; charIndex < line.length; charIndex++) {
27433
+ if (/\s/.test(line[charIndex]) && spaceIndex < lineSpaceWidths.length) {
27434
+ const expandedWidth = lineSpaceWidths[spaceIndex];
27435
+ if (lineBounds[charIndex]) {
27436
+ const oldWidth = lineBounds[charIndex].width;
27437
+ lineBounds[charIndex].width = expandedWidth;
27438
+ console.log(`🔤 Line ${lineIndex} space ${spaceIndex}: ${oldWidth.toFixed(1)}px -> ${expandedWidth.toFixed(1)}px`);
27439
+ }
27440
+ spaceIndex++;
27441
+ }
27442
+ }
27443
+ });
27444
+ }
27445
+
27446
+ /**
27447
+ * Fallback to default Fabric wrapping
27448
+ * @private
27449
+ */
27450
+ _splitTextIntoLinesDefault(text) {
26562
27451
  const newText = super._splitTextIntoLines(text),
26563
27452
  graphemeLines = this._wrapText(newText.lines, this.width),
26564
27453
  lines = new Array(graphemeLines.length);
@@ -26593,7 +27482,7 @@ class Textbox extends IText {
26593
27482
  * @private
26594
27483
  */
26595
27484
  initializeEventListeners() {
26596
- var _this$canvas;
27485
+ var _this$canvas4;
26597
27486
  // Track which side is being used for resize to handle position compensation
26598
27487
  let resizeOrigin = null;
26599
27488
 
@@ -26624,7 +27513,7 @@ class Textbox extends IText {
26624
27513
  });
26625
27514
 
26626
27515
  // Also listen to canvas-level modified event as backup
26627
- (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.on('object:modified', e => {
27516
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.on('object:modified', e => {
26628
27517
  if (e.target === this) {
26629
27518
  const currentResizeOrigin = resizeOrigin; // Capture the value before reset
26630
27519
  setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
@@ -26668,7 +27557,7 @@ class Textbox extends IText {
26668
27557
  const safetyThreshold = 2; // px - very subtle trigger
26669
27558
 
26670
27559
  if (maxRequiredWidth > this.width - safetyThreshold) {
26671
- var _this$canvas2;
27560
+ var _this$canvas5;
26672
27561
  // Set width to exactly what's needed + minimal safety margin
26673
27562
  const newWidth = maxRequiredWidth + 1; // Add just 1px safety margin
26674
27563
 
@@ -26701,7 +27590,66 @@ class Textbox extends IText {
26701
27590
  this.__overlayEditor.refresh();
26702
27591
  }, 0);
26703
27592
  }
26704
- (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
27593
+ (_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
27594
+ }
27595
+ }
27596
+
27597
+ /**
27598
+ * Force complete textbox re-initialization (useful after JSON loading)
27599
+ * Overrides Text version with Textbox-specific logic
27600
+ */
27601
+ forceTextReinitialization() {
27602
+ console.log('🔄 Force reinitializing Textbox object');
27603
+
27604
+ // CRITICAL: Ensure textbox is marked as initialized
27605
+ this.initialized = true;
27606
+
27607
+ // Clear all caches and force dirty state
27608
+ this._clearCache();
27609
+ this.dirty = true;
27610
+ this.dynamicMinWidth = 0;
27611
+
27612
+ // Force isEditing false to ensure clean state
27613
+ this.isEditing = false;
27614
+ console.log(' → Set initialized=true, dirty=true, cleared caches');
27615
+
27616
+ // Re-initialize dimensions (this will handle justify properly)
27617
+ this.initDimensions();
27618
+
27619
+ // Double-check that justify was applied by checking space widths
27620
+ if (this.textAlign.includes('justify') && this.__charBounds) {
27621
+ setTimeout(() => {
27622
+ var _this$canvas6;
27623
+ // Verify justify was applied by checking if space widths vary
27624
+ let hasVariableSpaces = false;
27625
+ this.__charBounds.forEach((lineBounds, i) => {
27626
+ if (lineBounds && this._textLines && this._textLines[i]) {
27627
+ const spaces = lineBounds.filter((bound, j) => /\s/.test(this._textLines[i][j]));
27628
+ if (spaces.length > 1) {
27629
+ const firstSpaceWidth = spaces[0].width;
27630
+ hasVariableSpaces = spaces.some(space => Math.abs(space.width - firstSpaceWidth) > 0.1);
27631
+ }
27632
+ }
27633
+ });
27634
+ if (!hasVariableSpaces && this.__charBounds.length > 0) {
27635
+ console.warn(' ⚠️ Justify spaces still uniform - forcing enlargeSpaces again');
27636
+ if (this.enlargeSpaces) {
27637
+ this.enlargeSpaces();
27638
+ }
27639
+ } else {
27640
+ console.log(' ✅ Justify spaces properly expanded');
27641
+ }
27642
+
27643
+ // Ensure height is recalculated - use browser height if available
27644
+ if (this._usingBrowserWrapping && this._actualBrowserHeight) {
27645
+ this.height = this._actualBrowserHeight;
27646
+ console.log(`🔤 JUSTIFY: Preserved browser height: ${this.height}px`);
27647
+ } else {
27648
+ this.height = this.calcTextHeight();
27649
+ console.log(`🔧 JUSTIFY: Used calcTextHeight: ${this.height}px`);
27650
+ }
27651
+ (_this$canvas6 = this.canvas) === null || _this$canvas6 === void 0 || _this$canvas6.requestRenderAll();
27652
+ }, 10);
26705
27653
  }
26706
27654
  }
26707
27655