@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.js CHANGED
@@ -360,7 +360,7 @@
360
360
  }
361
361
  const cache = new Cache();
362
362
 
363
- var version = "7.0.1-beta6";
363
+ var version = "7.0.1-beta8";
364
364
 
365
365
  // use this syntax so babel plugin see this import here
366
366
  const VERSION = version;
@@ -17586,6 +17586,7 @@
17586
17586
  _defineProperty(this, "hitStrokeWidth", 'auto');
17587
17587
  _defineProperty(this, "_updatingEndpoints", false);
17588
17588
  _defineProperty(this, "_useEndpointCoords", true);
17589
+ _defineProperty(this, "_exportingSVG", false);
17589
17590
  this.setOptions(options);
17590
17591
  this.x1 = x1;
17591
17592
  this.x2 = x2;
@@ -17797,6 +17798,14 @@
17797
17798
  this.x2 = newX;
17798
17799
  this.y2 = newY;
17799
17800
  }
17801
+
17802
+ // Update gradient coordinates if stroke is a gradient (but not during SVG export)
17803
+ if (this.stroke instanceof Gradient && !this._exportingSVG) {
17804
+ this.stroke.coords.x1 = this.x1;
17805
+ this.stroke.coords.y1 = this.y1;
17806
+ this.stroke.coords.x2 = this.x2;
17807
+ this.stroke.coords.y2 = this.y2;
17808
+ }
17800
17809
  this.dirty = true;
17801
17810
  this.setCoords();
17802
17811
  (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
@@ -17867,6 +17876,14 @@
17867
17876
  if (coordProps.includes(key)) {
17868
17877
  this._setWidthHeight();
17869
17878
  this.dirty = true;
17879
+
17880
+ // Update gradient coordinates if stroke is a gradient (but not during SVG export)
17881
+ if (this.stroke instanceof Gradient && !this._exportingSVG) {
17882
+ this.stroke.coords.x1 = this.x1;
17883
+ this.stroke.coords.y1 = this.y1;
17884
+ this.stroke.coords.x2 = this.x2;
17885
+ this.stroke.coords.y2 = this.y2;
17886
+ }
17870
17887
  }
17871
17888
  if ((key === 'left' || key === 'top') && this.canvas && !this._updatingEndpoints) {
17872
17889
  const deltaX = this.left - oldLeft;
@@ -17877,6 +17894,14 @@
17877
17894
  this.y1 += deltaY;
17878
17895
  this.x2 += deltaX;
17879
17896
  this.y2 += deltaY;
17897
+
17898
+ // Update gradient coordinates if stroke is a gradient
17899
+ if (this.stroke instanceof Gradient) {
17900
+ this.stroke.coords.x1 = this.x1;
17901
+ this.stroke.coords.y1 = this.y1;
17902
+ this.stroke.coords.x2 = this.x2;
17903
+ this.stroke.coords.y2 = this.y2;
17904
+ }
17880
17905
  this._updatingEndpoints = false;
17881
17906
  }
17882
17907
  }
@@ -17890,17 +17915,23 @@
17890
17915
  super.render(ctx);
17891
17916
  }
17892
17917
  _renderDirectly(ctx) {
17893
- var _this$stroke;
17894
17918
  if (!this.visible) return;
17895
17919
  ctx.save();
17896
17920
  ctx.globalAlpha = this.opacity;
17897
- ctx.strokeStyle = ((_this$stroke = this.stroke) === null || _this$stroke === void 0 ? void 0 : _this$stroke.toString()) || '#000';
17898
17921
  ctx.lineWidth = this.strokeWidth;
17899
17922
  ctx.lineCap = this.strokeLineCap || 'butt';
17900
17923
  ctx.beginPath();
17901
17924
  ctx.moveTo(this.x1, this.y1);
17902
17925
  ctx.lineTo(this.x2, this.y2);
17926
+ const origStrokeStyle = ctx.strokeStyle;
17927
+ if (isFiller(this.stroke)) {
17928
+ ctx.strokeStyle = this.stroke.toLive(ctx);
17929
+ } else {
17930
+ var _this$stroke;
17931
+ ctx.strokeStyle = ((_this$stroke = this.stroke) === null || _this$stroke === void 0 ? void 0 : _this$stroke.toString()) || '#000';
17932
+ }
17903
17933
  ctx.stroke();
17934
+ ctx.strokeStyle = origStrokeStyle;
17904
17935
  ctx.restore();
17905
17936
  }
17906
17937
  _render(ctx) {
@@ -17975,7 +18006,15 @@
17975
18006
  _toSVG() {
17976
18007
  if (this._useEndpointCoords) {
17977
18008
  // Use absolute coordinates to bypass all Fabric.js transforms
17978
- 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`];
18009
+ // Handle gradients manually for proper SVG export
18010
+ let strokeAttr = '';
18011
+ if (this.stroke instanceof Gradient) {
18012
+ // Let Fabric.js handle gradient definition, but we'll use the reference
18013
+ strokeAttr = `stroke="url(#${this.stroke.id})"`;
18014
+ } else {
18015
+ strokeAttr = `stroke="${this.stroke || 'none'}"`;
18016
+ }
18017
+ return [`<line ${strokeAttr} stroke-width="${this.strokeWidth}" stroke-linecap="${this.strokeLineCap}" `, `stroke-dasharray="${this.strokeDashArray ? this.strokeDashArray.join(' ') : 'none'}" `, `stroke-dashoffset="${this.strokeDashOffset}" stroke-linejoin="${this.strokeLineJoin}" `, `stroke-miterlimit="${this.strokeMiterLimit}" fill="${this.fill || 'none'}" `, `fill-rule="${this.fillRule}" opacity="${this.opacity}" `, `x1="${this.x1}" y1="${this.y1}" x2="${this.x2}" y2="${this.y2}" />\n`];
17979
18018
  } else {
17980
18019
  // Use standard calcLinePoints for legacy mode
17981
18020
  const {
@@ -17989,9 +18028,26 @@
17989
18028
  }
17990
18029
  toSVG(reviver) {
17991
18030
  if (this._useEndpointCoords) {
17992
- // Override toSVG to prevent Fabric.js from adding transform wrapper
17993
- const markup = this._toSVG().join('');
17994
- return reviver ? reviver(markup) : markup;
18031
+ // For endpoint coords, we need to bypass transforms but still allow gradients
18032
+ // Let's temporarily disable transforms during SVG generation
18033
+ const originalLeft = this.left;
18034
+ const originalTop = this.top;
18035
+
18036
+ // Set position to center of line for gradient calculation
18037
+ this.left = (this.x1 + this.x2) / 2;
18038
+ this.top = (this.y1 + this.y2) / 2;
18039
+
18040
+ // Get the SVG with standard system (for gradient handling)
18041
+ const standardSVG = super.toSVG(reviver);
18042
+
18043
+ // Restore original position
18044
+ this.left = originalLeft;
18045
+ this.top = originalTop;
18046
+
18047
+ // Extract gradient definition and clean up the line element
18048
+ // Remove the transform wrapper and update coordinates
18049
+ const cleanSVG = standardSVG.replace(/<g transform="[^"]*"[^>]*>/g, '').replace(/<\/g>/g, '').replace(/x1="[^"]*"/g, `x1="${this.x1}"`).replace(/y1="[^"]*"/g, `y1="${this.y1}"`).replace(/x2="[^"]*"/g, `x2="${this.x2}"`).replace(/y2="[^"]*"/g, `y2="${this.y2}"`);
18050
+ return cleanSVG;
17995
18051
  }
17996
18052
  // Use default behavior for legacy mode
17997
18053
  return super.toSVG(reviver);
@@ -19223,6 +19279,97 @@
19223
19279
  };
19224
19280
  }
19225
19281
 
19282
+ /**
19283
+ * Get a representative character for font metrics measurement
19284
+ * Uses canvas to test which scripts the font actually supports
19285
+ */
19286
+ function getRepresentativeCharacter(fontFamily) {
19287
+ const context = getMeasurementContext();
19288
+
19289
+ // Wait for font to be ready if possible
19290
+ if (typeof document !== 'undefined' && 'fonts' in document) {
19291
+ try {
19292
+ // Check if font is ready, if not, use fallback immediately
19293
+ if (!document.fonts.check(`16px ${fontFamily}`)) {
19294
+ return 'M'; // Use safe fallback while font loads
19295
+ }
19296
+ } catch (e) {
19297
+ // Font check failed, use fallback
19298
+ return 'M';
19299
+ }
19300
+ }
19301
+
19302
+ // Test characters for different scripts
19303
+ const testChars = [{
19304
+ char: 'م',
19305
+ script: 'Arabic'
19306
+ },
19307
+ // Arabic
19308
+ {
19309
+ char: 'א',
19310
+ script: 'Hebrew'
19311
+ },
19312
+ // Hebrew
19313
+ {
19314
+ char: 'अ',
19315
+ script: 'Devanagari'
19316
+ },
19317
+ // Hindi/Sanskrit
19318
+ {
19319
+ char: 'ا',
19320
+ script: 'Urdu'
19321
+ },
19322
+ // Urdu
19323
+ {
19324
+ char: 'ک',
19325
+ script: 'Persian'
19326
+ },
19327
+ // Persian
19328
+ {
19329
+ char: 'த',
19330
+ script: 'Tamil'
19331
+ },
19332
+ // Tamil
19333
+ {
19334
+ char: 'ก',
19335
+ script: 'Thai'
19336
+ },
19337
+ // Thai
19338
+ {
19339
+ char: 'М',
19340
+ script: 'Cyrillic'
19341
+ },
19342
+ // Cyrillic
19343
+ {
19344
+ char: 'Ω',
19345
+ script: 'Greek'
19346
+ },
19347
+ // Greek
19348
+ {
19349
+ char: 'M',
19350
+ script: 'Latin'
19351
+ } // Latin (fallback)
19352
+ ];
19353
+
19354
+ // Set the font
19355
+ context.font = `16px ${fontFamily}`;
19356
+
19357
+ // Test each character to see which ones render properly
19358
+ // Use a more robust width check to avoid false positives
19359
+ const fallbackWidth = context.measureText('M').width;
19360
+ for (const test of testChars) {
19361
+ const metrics = context.measureText(test.char);
19362
+
19363
+ // Character is valid if it has width and isn't just a fallback glyph
19364
+ if (metrics.width > 0 && Math.abs(metrics.width - fallbackWidth) > 0.1) {
19365
+ return test.char;
19366
+ }
19367
+ }
19368
+
19369
+ // Fallback to Latin 'M'
19370
+ return 'M';
19371
+ }
19372
+
19226
19373
  /**
19227
19374
  * Get font metrics for layout calculations
19228
19375
  */
@@ -19236,8 +19383,9 @@
19236
19383
  const context = getMeasurementContext();
19237
19384
  applyFontStyle(context, options);
19238
19385
 
19239
- // Use 'M' as sample character for metrics
19240
- const metrics = context.measureText('M');
19386
+ // Use representative character based on font's primary script
19387
+ const sample = getRepresentativeCharacter(options.fontFamily);
19388
+ const metrics = context.measureText(sample);
19241
19389
  const fontSize = options.fontSize;
19242
19390
 
19243
19391
  // Calculate metrics with fallbacks
@@ -19289,7 +19437,11 @@
19289
19437
  } = options;
19290
19438
 
19291
19439
  // Normalize font family (add quotes if needed)
19292
- const normalizedFamily = fontFamily.includes(' ') && !fontFamily.includes('"') && !fontFamily.includes("'") ? `"${fontFamily}"` : fontFamily;
19440
+ let normalizedFamily = fontFamily.includes(' ') && !fontFamily.includes('"') && !fontFamily.includes("'") ? `"${fontFamily}"` : fontFamily;
19441
+
19442
+ // Note: Font fallbacks are handled in the rendering phase only
19443
+ // to avoid affecting measurement calculations for text wrapping
19444
+
19293
19445
  return `${fontStyle} ${fontWeight} ${fontSize}px ${normalizedFamily}`;
19294
19446
  }
19295
19447
 
@@ -19441,6 +19593,81 @@
19441
19593
  const kerningCache = new KerningCache();
19442
19594
  const fontMetricsCache = new FontMetricsCache();
19443
19595
 
19596
+ // Set up font loading listener to clear caches when fonts change
19597
+ if (typeof document !== 'undefined' && 'fonts' in document) {
19598
+ document.fonts.addEventListener('loadingdone', () => {
19599
+ // Clear all caches when fonts finish loading
19600
+ clearAllCaches();
19601
+ });
19602
+ }
19603
+
19604
+ /**
19605
+ * Clear all measurement caches
19606
+ */
19607
+ function clearAllCaches() {
19608
+ measurementCache.clear();
19609
+ kerningCache.clear();
19610
+ fontMetricsCache.clear();
19611
+ }
19612
+
19613
+ /**
19614
+ * Detect if a font lacks English glyph support
19615
+ * These fonts should use browser-native measurement instead of Fabric's character-by-character measurement
19616
+ */
19617
+ function fontLacksEnglishGlyphs(fontFamily) {
19618
+ if (typeof document === 'undefined') return false;
19619
+
19620
+ // Known fonts that lack English glyphs
19621
+ const knownNonEnglishFonts = ['stv', 'arabic', 'naskh', 'thuluth', 'kufi', 'diwani', 'nastaliq', 'kufic', 'hijazi', 'madinah', 'makkah'];
19622
+ const lowerFontFamily = fontFamily.toLowerCase();
19623
+
19624
+ // Check known list first
19625
+ if (knownNonEnglishFonts.some(font => lowerFontFamily.includes(font))) {
19626
+ return true;
19627
+ }
19628
+
19629
+ // Dynamic glyph support detection
19630
+ const context = getMeasurementContext();
19631
+ context.font = `16px ${fontFamily}`;
19632
+
19633
+ // Test English characters
19634
+ const englishChars = ['A', 'B', 'C', 'a', 'b', 'c', 'M', 'W'];
19635
+ const fallbackFont = 'Arial, sans-serif';
19636
+
19637
+ // Measure with target font
19638
+ const targetWidths = englishChars.map(char => context.measureText(char).width);
19639
+
19640
+ // Measure with fallback font
19641
+ context.font = `16px ${fallbackFont}`;
19642
+ const fallbackWidths = englishChars.map(char => context.measureText(char).width);
19643
+
19644
+ // If most measurements are identical, the font likely doesn't have English glyphs
19645
+ let identicalCount = 0;
19646
+ for (let i = 0; i < englishChars.length; i++) {
19647
+ if (Math.abs(targetWidths[i] - fallbackWidths[i]) < 0.5) {
19648
+ identicalCount++;
19649
+ }
19650
+ }
19651
+ const lacksSupportThreshold = englishChars.length * 0.7; // 70% identical = lacks support
19652
+ const lacksSupport = identicalCount >= lacksSupportThreshold;
19653
+ return lacksSupport;
19654
+ }
19655
+
19656
+ // Cache for font glyph detection results
19657
+ const fontGlyphCache = new Map();
19658
+
19659
+ /**
19660
+ * Cached version of font glyph detection
19661
+ */
19662
+ function fontLacksEnglishGlyphsCached(fontFamily) {
19663
+ if (fontGlyphCache.has(fontFamily)) {
19664
+ return fontGlyphCache.get(fontFamily);
19665
+ }
19666
+ const result = fontLacksEnglishGlyphs(fontFamily);
19667
+ fontGlyphCache.set(fontFamily, result);
19668
+ return result;
19669
+ }
19670
+
19444
19671
  /**
19445
19672
  * Unicode and Internationalization Support
19446
19673
  *
@@ -20624,6 +20851,15 @@
20624
20851
  * Does not return dimensions.
20625
20852
  */
20626
20853
  initDimensions() {
20854
+ // Check if font is ready for accurate measurements
20855
+ // Only block initialization if it's a critical font loading situation
20856
+ const fontReady = this._isFontReady();
20857
+ if (!fontReady && !this.initialized) {
20858
+ // Only schedule font loading on first initialization
20859
+ this._scheduleInitAfterFontLoad();
20860
+ // Continue with fallback measurements for now
20861
+ }
20862
+
20627
20863
  // Use advanced layout if enabled
20628
20864
  if (this.enableAdvancedLayout && !this.path) {
20629
20865
  return this.initDimensionsAdvanced();
@@ -20640,7 +20876,21 @@
20640
20876
  }
20641
20877
  if (this.textAlign.includes(JUSTIFY)) {
20642
20878
  // once text is measured we need to make space fatter to make justified text.
20643
- this.enlargeSpaces();
20879
+ // Ensure __charBounds exists before calling enlargeSpaces
20880
+ if (this.__charBounds && this.__charBounds.length > 0) {
20881
+ this.enlargeSpaces();
20882
+ } else {
20883
+ console.warn('⚠️ __charBounds not ready for justify alignment, deferring enlargeSpaces');
20884
+ // Defer the justify calculation until the next frame
20885
+ setTimeout(() => {
20886
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
20887
+ var _this$canvas;
20888
+ console.log('🔧 Applying deferred justify alignment');
20889
+ this.enlargeSpaces();
20890
+ (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
20891
+ }
20892
+ }, 0);
20893
+ }
20644
20894
  }
20645
20895
  }
20646
20896
 
@@ -20651,7 +20901,7 @@
20651
20901
  let diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces;
20652
20902
  const isRtl = this.direction === 'rtl';
20653
20903
  for (let i = 0, len = this._textLines.length; i < len; i++) {
20654
- if (this.textAlign !== JUSTIFY && (i === len - 1 || this.isEndOfWrapping(i))) {
20904
+ if (!this.textAlign.includes('justify') && (i === len - 1 || this.isEndOfWrapping(i))) {
20655
20905
  continue;
20656
20906
  }
20657
20907
  accumulatedSpace = 0;
@@ -20660,6 +20910,9 @@
20660
20910
  if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) {
20661
20911
  numberOfSpaces = spaces.length;
20662
20912
  diffSpace = (this.width - currentLineWidth) / numberOfSpaces;
20913
+ console.log(`🔧 EnlargeSpaces Line ${i}:`);
20914
+ console.log(` Current width: ${currentLineWidth}, Target: ${this.width}`);
20915
+ console.log(` Spaces: ${numberOfSpaces}, diffSpace: ${diffSpace.toFixed(2)}`);
20663
20916
  if (isRtl) {
20664
20917
  for (let j = 0; j < line.length; j++) {
20665
20918
  if (this._reSpaceAndTab.test(line[j])) ;
@@ -20775,6 +21028,18 @@
20775
21028
 
20776
21029
  // Convert layout to legacy format for compatibility
20777
21030
  this._convertLayoutToLegacyFormat(layout);
21031
+
21032
+ // Ensure justify alignment is properly applied for compatibility with legacy rendering
21033
+ if (this.textAlign.includes(JUSTIFY)) {
21034
+ // Force enlarge spaces after advanced layout calculation
21035
+ setTimeout(() => {
21036
+ if (this.enlargeSpaces) {
21037
+ var _this$canvas2;
21038
+ this.enlargeSpaces();
21039
+ (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.renderAll();
21040
+ }
21041
+ }, 0);
21042
+ }
20778
21043
  this.dirty = true;
20779
21044
  }
20780
21045
 
@@ -21786,7 +22051,19 @@
21786
22051
  fontSize = this.fontSize
21787
22052
  } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
21788
22053
  let forMeasuring = arguments.length > 1 ? arguments[1] : undefined;
21789
- const parsedFontFamily = fontFamily.includes("'") || fontFamily.includes('"') || fontFamily.includes(',') || FabricText.genericFonts.includes(fontFamily.toLowerCase()) ? fontFamily : `"${fontFamily}"`;
22054
+ let parsedFontFamily = fontFamily.includes("'") || fontFamily.includes('"') || fontFamily.includes(',') || FabricText.genericFonts.includes(fontFamily.toLowerCase()) ? fontFamily : `"${fontFamily}"`;
22055
+
22056
+ // For fonts like STV that don't support English/Latin characters,
22057
+ // add fallback fonts for consistent rendering of unsupported characters
22058
+ // Only add fallbacks during actual rendering, not for measurements
22059
+ if (!forMeasuring &&
22060
+ // Only during rendering, not measuring
22061
+ !fontFamily.includes(',') && (
22062
+ // Don't add fallbacks if already has them
22063
+ fontFamily.toLowerCase().includes('stv') || fontFamily.toLowerCase().includes('arabic') || fontFamily.toLowerCase().includes('naskh') || fontFamily.toLowerCase().includes('kufi'))) {
22064
+ // Add fallback fonts for unsupported characters (spaces, punctuation, etc.)
22065
+ parsedFontFamily = `${parsedFontFamily}, "Arial Unicode MS", Arial, sans-serif`;
22066
+ }
21790
22067
  return [fontStyle, fontWeight, `${forMeasuring ? this.CACHE_FONT_SIZE : fontSize}px`, parsedFontFamily].join(' ');
21791
22068
  }
21792
22069
 
@@ -21830,7 +22107,13 @@
21830
22107
  newLine = ['\n'];
21831
22108
  let newText = [];
21832
22109
  for (let i = 0; i < lines.length; i++) {
21833
- newLines[i] = this.graphemeSplit(lines[i]);
22110
+ // Use BiDi-aware grapheme splitting for RTL text
22111
+ if (this.direction === 'rtl' || this._containsArabicText(lines[i])) {
22112
+ newLines[i] = segmentGraphemes(lines[i]);
22113
+ console.log(`🔤 BiDi-aware split line ${i}: "${lines[i]}" -> [${newLines[i].join(', ')}]`);
22114
+ } else {
22115
+ newLines[i] = this.graphemeSplit(lines[i]);
22116
+ }
21834
22117
  newText = newText.concat(newLines[i], newLine);
21835
22118
  }
21836
22119
  newText.pop();
@@ -21842,6 +22125,14 @@
21842
22125
  };
21843
22126
  }
21844
22127
 
22128
+ /**
22129
+ * Check if text contains Arabic characters
22130
+ * @private
22131
+ */
22132
+ _containsArabicText(text) {
22133
+ return /[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
22134
+ }
22135
+
21845
22136
  /**
21846
22137
  * Returns object representation of an instance
21847
22138
  * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
@@ -21964,6 +22255,88 @@
21964
22255
 
21965
22256
  /* _FROM_SVG_END_ */
21966
22257
 
22258
+ /**
22259
+ * Check if the font is ready for accurate measurements
22260
+ * @private
22261
+ */
22262
+ _isFontReady() {
22263
+ if (typeof document === 'undefined' || !('fonts' in document)) {
22264
+ return true; // Assume ready in non-browser environments
22265
+ }
22266
+ try {
22267
+ return document.fonts.check(`${this.fontSize}px ${this.fontFamily}`);
22268
+ } catch (e) {
22269
+ return true; // Fallback to assuming ready if check fails
22270
+ }
22271
+ }
22272
+
22273
+ /**
22274
+ * Schedule re-initialization after font loads
22275
+ * @private
22276
+ */
22277
+ _scheduleInitAfterFontLoad() {
22278
+ if (typeof document === 'undefined' || !('fonts' in document)) {
22279
+ return;
22280
+ }
22281
+
22282
+ // Only schedule if not already waiting
22283
+ if (this._fontLoadScheduled) {
22284
+ return;
22285
+ }
22286
+ this._fontLoadScheduled = true;
22287
+ const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
22288
+ document.fonts.load(fontSpec).then(() => {
22289
+ this._fontLoadScheduled = false;
22290
+ // Re-initialize dimensions with proper font metrics
22291
+ this.initDimensions();
22292
+
22293
+ // Extra step for justify alignment after font loading
22294
+ if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
22295
+ setTimeout(() => {
22296
+ var _this$canvas3;
22297
+ if (this.enlargeSpaces) {
22298
+ this.enlargeSpaces();
22299
+ }
22300
+ (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
22301
+ }, 10);
22302
+ } else {
22303
+ var _this$canvas4;
22304
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.requestRenderAll();
22305
+ }
22306
+ }).catch(() => {
22307
+ this._fontLoadScheduled = false;
22308
+ });
22309
+ }
22310
+
22311
+ /**
22312
+ * Force complete text re-initialization (useful after JSON loading)
22313
+ */
22314
+ forceTextReinitialization() {
22315
+ console.log('🔄 Force reinitializing text object');
22316
+
22317
+ // Clear all caches
22318
+ this._clearCache();
22319
+ this.dirty = true;
22320
+
22321
+ // Force text splitting to rebuild internal structures
22322
+ this._splitText();
22323
+
22324
+ // Re-initialize dimensions
22325
+ this.initDimensions();
22326
+
22327
+ // Special handling for justify alignment
22328
+ if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
22329
+ // Ensure justify is applied after dimensions are set
22330
+ setTimeout(() => {
22331
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
22332
+ var _this$canvas5;
22333
+ this.enlargeSpaces();
22334
+ (_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
22335
+ }
22336
+ }, 10);
22337
+ }
22338
+ }
22339
+
21967
22340
  /**
21968
22341
  * Returns FabricText instance from an object representation
21969
22342
  * @param {Object} object plain js Object to create an instance from
@@ -21975,6 +22348,93 @@
21975
22348
  styles: stylesFromArray(object.styles || {}, object.text)
21976
22349
  }, {
21977
22350
  extraParam: 'text'
22351
+ }).then(textObject => {
22352
+ // Ensure text object is properly initialized after JSON deserialization
22353
+ // This is critical for justify alignment and other text layout features
22354
+ textObject.initialized = true;
22355
+
22356
+ // Force reinitialization to ensure proper layout
22357
+ if (textObject._clearCache) {
22358
+ textObject._clearCache();
22359
+ }
22360
+ textObject.dirty = true;
22361
+
22362
+ // Check if we need to wait for font loading (especially for custom fonts like STV)
22363
+ const fontSpec = `${textObject.fontSize}px ${textObject.fontFamily}`;
22364
+
22365
+ // For custom fonts, ensure they're loaded before initializing dimensions
22366
+ if (typeof document !== 'undefined' && 'fonts' in document && textObject.fontFamily !== 'Arial' && textObject.fontFamily !== 'Times New Roman') {
22367
+ return document.fonts.load(fontSpec).then(() => {
22368
+ var _textObject$fontFamil;
22369
+ console.log(`🔤 Font loaded for JSON object: ${fontSpec}`);
22370
+ // Ensure initialized flag is set again (in case constructor reset it)
22371
+ textObject.initialized = true;
22372
+
22373
+ // Special handling for STV fonts which have measurement issues
22374
+ const isStvFont = (_textObject$fontFamil = textObject.fontFamily) === null || _textObject$fontFamil === void 0 ? void 0 : _textObject$fontFamil.toLowerCase().includes('stv');
22375
+ if (isStvFont) {
22376
+ console.log(`🔤 STV font detected, using enhanced reinitialization`);
22377
+
22378
+ // Clear all cached state that might interfere with browser wrapping
22379
+ textObject._browserWrapCache = null;
22380
+ textObject._lastDimensionState = null;
22381
+ textObject._browserWrapInitialized = false;
22382
+ console.log(`🔤 STV font: Cleared all cached states for fresh initialization`);
22383
+
22384
+ // Force browser wrapping flag for STV fonts
22385
+ textObject._usingBrowserWrapping = true;
22386
+ console.log(`🔤 STV font: Forcing browser wrapping flag during JSON load`);
22387
+
22388
+ // Multiple initialization attempts for STV fonts
22389
+ const reinitWithDelay = attempt => {
22390
+ if (textObject.forceTextReinitialization) {
22391
+ textObject.forceTextReinitialization();
22392
+ } else {
22393
+ textObject.initDimensions();
22394
+ }
22395
+
22396
+ // Check if width is still problematic after initialization
22397
+ if (textObject.width < 50 && attempt < 3) {
22398
+ console.log(`🔤 STV font width still ${textObject.width}px, retrying in ${100 * attempt}ms (attempt ${attempt + 1}/3)`);
22399
+ setTimeout(() => reinitWithDelay(attempt + 1), 100 * attempt);
22400
+ }
22401
+ };
22402
+ reinitWithDelay(0);
22403
+ } else {
22404
+ // Use specialized reinitialization for Textbox objects
22405
+ if (textObject.forceTextReinitialization) {
22406
+ console.log(`🔤 Using Textbox specialized reinitialization`);
22407
+ textObject.forceTextReinitialization();
22408
+ } else {
22409
+ // Reinitialize dimensions with proper font metrics
22410
+ textObject.initDimensions();
22411
+ }
22412
+ }
22413
+ return textObject;
22414
+ }).catch(() => {
22415
+ console.warn(`⚠️ Font loading failed for ${fontSpec}, proceeding with fallback`);
22416
+ // Ensure initialized flag is set again
22417
+ textObject.initialized = true;
22418
+
22419
+ // Still initialize dimensions even if font loading fails
22420
+ if (textObject.forceTextReinitialization) {
22421
+ textObject.forceTextReinitialization();
22422
+ } else {
22423
+ textObject.initDimensions();
22424
+ }
22425
+ return textObject;
22426
+ });
22427
+ } else {
22428
+ // Standard fonts - ensure initialized and use appropriate method
22429
+ textObject.initialized = true;
22430
+ if (textObject.forceTextReinitialization) {
22431
+ console.log(`🔤 Using Textbox specialized reinitialization for standard font`);
22432
+ textObject.forceTextReinitialization();
22433
+ } else {
22434
+ textObject.initDimensions();
22435
+ }
22436
+ return textObject;
22437
+ }
21978
22438
  });
21979
22439
  }
21980
22440
  }
@@ -22618,6 +23078,13 @@
22618
23078
 
22619
23079
  // Apply all other font and text styles to match Fabric
22620
23080
  const letterSpacingPx = (target.charSpacing || 0) / 1000 * finalFontSize;
23081
+
23082
+ // Special handling for text objects loaded from JSON - ensure they're properly initialized
23083
+ if (target.dirty !== false && target.initDimensions) {
23084
+ console.log('🔧 Ensuring text object is properly initialized before overlay editing');
23085
+ // Force re-initialization if the text object seems to be in a dirty state
23086
+ target.initDimensions();
23087
+ }
22621
23088
  this.textarea.style.fontSize = `${finalFontSize}px`;
22622
23089
  this.textarea.style.lineHeight = String(fabricLineHeight);
22623
23090
  this.textarea.style.fontFamily = target.fontFamily || 'Arial';
@@ -26108,8 +26575,27 @@
26108
26575
  */
26109
26576
  initDimensions() {
26110
26577
  if (!this.initialized) {
26578
+ this.initialized = true;
26579
+ }
26580
+
26581
+ // Prevent rapid recalculations during moves
26582
+ if (this._usingBrowserWrapping) {
26583
+ const now = Date.now();
26584
+ const lastCall = this._lastInitDimensionsTime || 0;
26585
+ const isRapidCall = now - lastCall < 100;
26586
+ const isDuringLoading = this._jsonLoading || !this._browserWrapInitialized;
26587
+ if (isRapidCall && !isDuringLoading) {
26588
+ return;
26589
+ }
26590
+ this._lastInitDimensionsTime = now;
26591
+ }
26592
+
26593
+ // Skip if nothing changed
26594
+ const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
26595
+ if (this._lastDimensionState === currentState && this._textLines && this._textLines.length > 0) {
26111
26596
  return;
26112
26597
  }
26598
+ this._lastDimensionState = currentState;
26113
26599
 
26114
26600
  // Use advanced layout if enabled
26115
26601
  if (this.enableAdvancedLayout) {
@@ -26120,17 +26606,142 @@
26120
26606
  // clear dynamicMinWidth as it will be different after we re-wrap line
26121
26607
  this.dynamicMinWidth = 0;
26122
26608
  // wrap lines
26123
- this._styleMap = this._generateStyleMap(this._splitText());
26124
- // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap
26125
- if (this.dynamicMinWidth > this.width) {
26609
+ const splitTextResult = this._splitText();
26610
+ this._styleMap = this._generateStyleMap(splitTextResult);
26611
+
26612
+ // For browser wrapping, ensure _textLines is set from browser results
26613
+ if (this._usingBrowserWrapping && splitTextResult && splitTextResult.lines) {
26614
+ this._textLines = splitTextResult.lines.map(line => line.split(''));
26615
+
26616
+ // Store justify measurements and browser height
26617
+ const justifyMeasurements = splitTextResult.justifySpaceMeasurements;
26618
+ if (justifyMeasurements) {
26619
+ this._styleMap.justifySpaceMeasurements = justifyMeasurements;
26620
+ }
26621
+ const actualHeight = splitTextResult.actualBrowserHeight;
26622
+ if (actualHeight) {
26623
+ this._actualBrowserHeight = actualHeight;
26624
+ }
26625
+ }
26626
+ // Don't auto-resize width when using browser wrapping to prevent width increases during moves
26627
+ if (!this._usingBrowserWrapping && this.dynamicMinWidth > this.width) {
26126
26628
  this._set('width', this.dynamicMinWidth);
26127
26629
  }
26630
+
26631
+ // For browser wrapping fonts (like STV), ensure minimum width for new textboxes
26632
+ // since these fonts can't measure English characters properly
26633
+ if (this._usingBrowserWrapping && this.width < 50) {
26634
+ console.log(`🔤 BROWSER WRAP: Font ${this.fontFamily} has width ${this.width}px, setting to 300px for usability`);
26635
+ this.width = 300;
26636
+ }
26637
+
26638
+ // Mark browser wrapping as initialized when complete
26639
+ if (this._usingBrowserWrapping) {
26640
+ this._browserWrapInitialized = true;
26641
+ }
26128
26642
  if (this.textAlign.includes(JUSTIFY)) {
26643
+ // For browser wrapping fonts, apply browser-calculated justify spaces
26644
+ if (this._usingBrowserWrapping) {
26645
+ console.log('🔤 BROWSER WRAP: Applying browser-calculated justify spaces');
26646
+ this._applyBrowserJustifySpaces();
26647
+ return;
26648
+ }
26649
+
26650
+ // Don't apply justify alignment during drag operations to prevent snapping
26651
+ const now = Date.now();
26652
+ const lastDragTime = this._lastInitDimensionsTime || 0;
26653
+ const isDuringDrag = now - lastDragTime < 200; // 200ms window for drag detection
26654
+
26655
+ if (isDuringDrag) {
26656
+ console.log('🔤 Skipping justify during drag operation to prevent snapping');
26657
+ return;
26658
+ }
26659
+
26660
+ // For non-browser-wrapping fonts, use Fabric's justify system
26129
26661
  // once text is measured we need to make space fatter to make justified text.
26130
- this.enlargeSpaces();
26662
+ // Ensure __charBounds exists and fonts are ready before applying justify
26663
+ if (this.__charBounds && this.__charBounds.length > 0) {
26664
+ // Check if font is ready for accurate justify calculations
26665
+ const fontReady = this._isFontReady ? this._isFontReady() : true;
26666
+ if (fontReady) {
26667
+ this.enlargeSpaces();
26668
+ } else {
26669
+ console.warn('⚠️ Textbox: Font not ready for justify, deferring enlargeSpaces');
26670
+ // Defer justify calculation until font is ready
26671
+ this._scheduleJustifyAfterFontLoad();
26672
+ }
26673
+ } else {
26674
+ console.warn('⚠️ Textbox: __charBounds not ready for justify alignment, deferring enlargeSpaces');
26675
+ // Defer the justify calculation until the next frame
26676
+ setTimeout(() => {
26677
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
26678
+ var _this$canvas;
26679
+ console.log('🔧 Applying deferred Textbox justify alignment');
26680
+ this.enlargeSpaces();
26681
+ (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
26682
+ }
26683
+ }, 0);
26684
+ }
26685
+ }
26686
+ // Calculate height - use Fabric's calculation for proper text rendering space
26687
+ if (this._usingBrowserWrapping && this._textLines && this._textLines.length > 0) {
26688
+ const actualBrowserHeight = this._actualBrowserHeight;
26689
+ const oldHeight = this.height;
26690
+ // Use Fabric's height calculation since it knows how much space text rendering needs
26691
+ this.height = this.calcTextHeight();
26692
+
26693
+ // Force canvas refresh and control update if height changed significantly
26694
+ if (Math.abs(this.height - oldHeight) > 1) {
26695
+ var _this$canvas2, _this$_textLines;
26696
+ this.setCoords();
26697
+ (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
26698
+
26699
+ // DEBUG: Log exact positioning details
26700
+ console.log(`🎯 POSITIONING DEBUG:`);
26701
+ console.log(` Textbox height: ${this.height}px`);
26702
+ console.log(` Textbox top: ${this.top}px`);
26703
+ console.log(` Textbox left: ${this.left}px`);
26704
+ console.log(` Text lines: ${((_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length) || 0}`);
26705
+ console.log(` Font size: ${this.fontSize}px`);
26706
+ console.log(` Line height: ${this.lineHeight || 1.16}`);
26707
+ console.log(` Calculated line height: ${this.fontSize * (this.lineHeight || 1.16)}px`);
26708
+ console.log(` _getTopOffset(): ${this._getTopOffset()}px`);
26709
+ console.log(` calcTextHeight(): ${this.calcTextHeight()}px`);
26710
+ console.log(` Browser height: ${actualBrowserHeight}px`);
26711
+ console.log(` Height difference: ${this.height - this.calcTextHeight()}px`);
26712
+ }
26713
+ } else {
26714
+ this.height = this.calcTextHeight();
26715
+ }
26716
+ }
26717
+
26718
+ /**
26719
+ * Schedule justify calculation after font loads (Textbox-specific)
26720
+ * @private
26721
+ */
26722
+ _scheduleJustifyAfterFontLoad() {
26723
+ if (typeof document === 'undefined' || !('fonts' in document)) {
26724
+ return;
26131
26725
  }
26132
- // clear cache and re-calculate height
26133
- this.height = this.calcTextHeight();
26726
+
26727
+ // Only schedule if not already waiting
26728
+ if (this._fontJustifyScheduled) {
26729
+ return;
26730
+ }
26731
+ this._fontJustifyScheduled = true;
26732
+ const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
26733
+ document.fonts.load(fontSpec).then(() => {
26734
+ var _this$canvas3;
26735
+ this._fontJustifyScheduled = false;
26736
+ console.log('🔧 Textbox: Font loaded, applying justify alignment');
26737
+
26738
+ // Re-run initDimensions to ensure proper justify calculation
26739
+ this.initDimensions();
26740
+ (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
26741
+ }).catch(() => {
26742
+ this._fontJustifyScheduled = false;
26743
+ console.warn('⚠️ Textbox: Font loading failed, justify may be incorrect');
26744
+ });
26134
26745
  }
26135
26746
 
26136
26747
  /**
@@ -26497,19 +27108,33 @@
26497
27108
  width: wordWidth
26498
27109
  } = data[i];
26499
27110
  offset += word.length;
26500
- lineWidth += infixWidth + wordWidth - additionalSpace;
26501
- if (lineWidth > maxWidth && !lineJustStarted) {
27111
+
27112
+ // Predictive wrapping: check if adding this word would exceed the width
27113
+ const potentialLineWidth = lineWidth + infixWidth + wordWidth - additionalSpace;
27114
+ // Use exact width to match overlay editor behavior
27115
+ const conservativeMaxWidth = maxWidth; // No artificial buffer
27116
+
27117
+ // Debug logging for wrapping decisions
27118
+ const currentLineText = line.join('');
27119
+ console.log(`🔧 FABRIC WRAP CHECK: "${data[i].word}" -> potential: ${potentialLineWidth.toFixed(1)}px vs limit: ${conservativeMaxWidth.toFixed(1)}px`);
27120
+ if (potentialLineWidth > conservativeMaxWidth && !lineJustStarted) {
27121
+ // This word would exceed the width, wrap before adding it
27122
+ console.log(`🔧 FABRIC WRAP! Line: "${currentLineText}" (${lineWidth.toFixed(1)}px)`);
26502
27123
  graphemeLines.push(line);
26503
27124
  line = [];
26504
- lineWidth = wordWidth;
27125
+ lineWidth = wordWidth; // Start new line with just this word
26505
27126
  lineJustStarted = true;
26506
27127
  } else {
26507
- lineWidth += additionalSpace;
27128
+ // Word fits, add it to current line
27129
+ lineWidth = potentialLineWidth + additionalSpace;
26508
27130
  }
26509
27131
  if (!lineJustStarted && !splitByGrapheme) {
26510
27132
  line.push(infix);
26511
27133
  }
26512
27134
  line = line.concat(word);
27135
+
27136
+ // Debug: show current line after adding word
27137
+ console.log(`🔧 FABRIC AFTER ADD: Line now: "${line.join('')}" (${line.length} chars)`);
26513
27138
  infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset);
26514
27139
  offset++;
26515
27140
  lineJustStarted = false;
@@ -26519,9 +27144,19 @@
26519
27144
  // TODO: this code is probably not necessary anymore.
26520
27145
  // it can be moved out of this function since largestWordWidth is now
26521
27146
  // known in advance
26522
- if (largestWordWidth + reservedSpace > this.dynamicMinWidth) {
27147
+ // Don't modify dynamicMinWidth when using browser wrapping to prevent width increases
27148
+ if (!this._usingBrowserWrapping && largestWordWidth + reservedSpace > this.dynamicMinWidth) {
27149
+ console.log(`🔧 FABRIC updating dynamicMinWidth: ${this.dynamicMinWidth} -> ${largestWordWidth - additionalSpace + reservedSpace}`);
26523
27150
  this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace;
27151
+ } else if (this._usingBrowserWrapping) {
27152
+ console.log(`🔤 BROWSER WRAP: Skipping dynamicMinWidth update to prevent width increase`);
26524
27153
  }
27154
+
27155
+ // Debug: show final wrapped lines
27156
+ console.log(`🔧 FABRIC FINAL LINES: ${graphemeLines.length} lines`);
27157
+ graphemeLines.forEach((line, i) => {
27158
+ console.log(` Line ${i + 1}: "${line.join('')}" (${line.length} chars)`);
27159
+ });
26525
27160
  return graphemeLines;
26526
27161
  }
26527
27162
 
@@ -26565,6 +27200,260 @@
26565
27200
  * @override
26566
27201
  */
26567
27202
  _splitTextIntoLines(text) {
27203
+ // Check if we need browser wrapping using smart font detection
27204
+ const needsBrowserWrapping = this.fontFamily && fontLacksEnglishGlyphsCached(this.fontFamily);
27205
+ if (needsBrowserWrapping) {
27206
+ // Cache key based on text content, width, font properties, AND text alignment
27207
+ const textHash = text.length + text.slice(0, 50); // Include text content in cache key
27208
+ const cacheKey = `${textHash}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
27209
+
27210
+ // Check if we have a cached result and nothing has changed
27211
+ if (this._browserWrapCache && this._browserWrapCache.key === cacheKey) {
27212
+ const cachedResult = this._browserWrapCache.result;
27213
+
27214
+ // For justify alignment, ensure we have the measurements
27215
+ if (this.textAlign.includes('justify') && !cachedResult.justifySpaceMeasurements) ; else {
27216
+ return cachedResult;
27217
+ }
27218
+ }
27219
+ const result = this._splitTextIntoLinesWithBrowser(text);
27220
+
27221
+ // Cache the result
27222
+ this._browserWrapCache = {
27223
+ key: cacheKey,
27224
+ result
27225
+ };
27226
+
27227
+ // Mark that we used browser wrapping to prevent dynamicMinWidth modifications
27228
+ this._usingBrowserWrapping = true;
27229
+ return result;
27230
+ }
27231
+
27232
+ // Clear the browser wrapping flag when using regular wrapping
27233
+ this._usingBrowserWrapping = false;
27234
+
27235
+ // Default Fabric wrapping for other fonts
27236
+ const newText = super._splitTextIntoLines(text),
27237
+ graphemeLines = this._wrapText(newText.lines, this.width),
27238
+ lines = new Array(graphemeLines.length);
27239
+ for (let i = 0; i < graphemeLines.length; i++) {
27240
+ lines[i] = graphemeLines[i].join('');
27241
+ }
27242
+ newText.lines = lines;
27243
+ newText.graphemeLines = graphemeLines;
27244
+ return newText;
27245
+ }
27246
+
27247
+ /**
27248
+ * Use browser's native text wrapping for accurate handling of fonts without English glyphs
27249
+ * @private
27250
+ */
27251
+ _splitTextIntoLinesWithBrowser(text) {
27252
+ if (typeof document === 'undefined') {
27253
+ // Fallback to regular wrapping in Node.js
27254
+ return this._splitTextIntoLinesDefault(text);
27255
+ }
27256
+
27257
+ // Create a hidden element that mimics the overlay editor
27258
+ const testElement = document.createElement('div');
27259
+ testElement.style.position = 'absolute';
27260
+ testElement.style.left = '-9999px';
27261
+ testElement.style.visibility = 'hidden';
27262
+ testElement.style.fontSize = `${this.fontSize}px`;
27263
+ testElement.style.fontFamily = `"${this.fontFamily}"`;
27264
+ testElement.style.fontWeight = String(this.fontWeight || 'normal');
27265
+ testElement.style.fontStyle = String(this.fontStyle || 'normal');
27266
+ testElement.style.lineHeight = String(this.lineHeight || 1.16);
27267
+ testElement.style.width = `${this.width}px`;
27268
+ testElement.style.direction = this.direction || 'ltr';
27269
+ testElement.style.whiteSpace = 'pre-wrap';
27270
+ testElement.style.wordBreak = 'normal';
27271
+ testElement.style.overflowWrap = 'break-word';
27272
+
27273
+ // Set browser-native text alignment (including justify)
27274
+ if (this.textAlign.includes('justify')) {
27275
+ testElement.style.textAlign = 'justify';
27276
+ testElement.style.textAlignLast = 'auto'; // Let browser decide last line alignment
27277
+ } else {
27278
+ testElement.style.textAlign = this.textAlign;
27279
+ }
27280
+ testElement.textContent = text;
27281
+ document.body.appendChild(testElement);
27282
+
27283
+ // Get the browser's natural line breaks
27284
+ const range = document.createRange();
27285
+ const lines = [];
27286
+ const graphemeLines = [];
27287
+ try {
27288
+ // Simple approach: split by measuring character positions
27289
+ const textNode = testElement.firstChild;
27290
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
27291
+ let currentLineStart = 0;
27292
+ const textLength = text.length;
27293
+ let previousBottom = 0;
27294
+ for (let i = 0; i <= textLength; i++) {
27295
+ range.setStart(textNode, currentLineStart);
27296
+ range.setEnd(textNode, i);
27297
+ const rect = range.getBoundingClientRect();
27298
+ if (i > currentLineStart && (rect.bottom > previousBottom + 5 || i === textLength)) {
27299
+ // New line detected or end of text
27300
+ const lineEnd = i === textLength ? i : i - 1;
27301
+ const lineText = text.substring(currentLineStart, lineEnd).trim();
27302
+ if (lineText) {
27303
+ lines.push(lineText);
27304
+ // Convert to graphemes for compatibility
27305
+ const graphemeLine = lineText.split('');
27306
+ graphemeLines.push(graphemeLine);
27307
+ }
27308
+ currentLineStart = lineEnd;
27309
+ previousBottom = rect.bottom;
27310
+ }
27311
+ }
27312
+ }
27313
+ } catch (error) {
27314
+ console.warn('Browser wrapping failed, using fallback:', error);
27315
+ document.body.removeChild(testElement);
27316
+ return this._splitTextIntoLinesDefault(text);
27317
+ }
27318
+
27319
+ // Extract actual browser height BEFORE removing element
27320
+ const actualBrowserHeight = testElement.scrollHeight;
27321
+ const offsetHeight = testElement.offsetHeight;
27322
+ const clientHeight = testElement.clientHeight;
27323
+ const boundingRect = testElement.getBoundingClientRect();
27324
+ console.log(`🔤 Browser element measurements:`);
27325
+ console.log(` scrollHeight: ${actualBrowserHeight}px (content + padding + hidden overflow)`);
27326
+ console.log(` offsetHeight: ${offsetHeight}px (content + padding + border)`);
27327
+ console.log(` clientHeight: ${clientHeight}px (content + padding, no border/scrollbar)`);
27328
+ console.log(` boundingRect.height: ${boundingRect.height}px (actual rendered height)`);
27329
+ console.log(` Font size: ${this.fontSize}px, Line height: ${this.lineHeight || 1.16}, Lines: ${lines.length}`);
27330
+
27331
+ // For justify alignment, extract space measurements from browser BEFORE removing element
27332
+ let justifySpaceMeasurements = null;
27333
+ if (this.textAlign.includes('justify')) {
27334
+ justifySpaceMeasurements = this._extractJustifySpaceMeasurements(testElement, lines);
27335
+ }
27336
+ document.body.removeChild(testElement);
27337
+ console.log(`🔤 Browser wrapping result: ${lines.length} lines`);
27338
+
27339
+ // Try different height measurements to find the most accurate
27340
+ let bestHeight = actualBrowserHeight;
27341
+
27342
+ // If scrollHeight and offsetHeight differ significantly, investigate
27343
+ if (Math.abs(actualBrowserHeight - offsetHeight) > 2) {
27344
+ console.log(`🔤 Height discrepancy detected: scrollHeight=${actualBrowserHeight}px vs offsetHeight=${offsetHeight}px`);
27345
+ }
27346
+
27347
+ // Consider using boundingRect height if it's larger (sometimes more accurate for visible content)
27348
+ if (boundingRect.height > bestHeight) {
27349
+ console.log(`🔤 Using boundingRect height (${boundingRect.height}px) instead of scrollHeight (${bestHeight}px)`);
27350
+ bestHeight = boundingRect.height;
27351
+ }
27352
+
27353
+ // Font-specific height adjustments for accurate bounding box
27354
+ let adjustedHeight = bestHeight;
27355
+
27356
+ // Fonts without English glyphs need additional height buffer due to different font metrics
27357
+ const lacksEnglishGlyphs = fontLacksEnglishGlyphsCached(this.fontFamily);
27358
+ if (lacksEnglishGlyphs) {
27359
+ const glyphBuffer = this.fontSize * 0.25; // 25% of font size for non-English fonts
27360
+ adjustedHeight = bestHeight + glyphBuffer;
27361
+ console.log(`🔤 Non-English font detected (${this.fontFamily}): Adding ${glyphBuffer}px buffer (${bestHeight}px + ${glyphBuffer}px = ${adjustedHeight}px)`);
27362
+ } else {
27363
+ console.log(`🔤 Standard font (${this.fontFamily}): Using browser height directly (${bestHeight}px)`);
27364
+ }
27365
+ return {
27366
+ _unwrappedLines: [text.split('')],
27367
+ lines: lines,
27368
+ graphemeText: text.split(''),
27369
+ graphemeLines: graphemeLines,
27370
+ justifySpaceMeasurements: justifySpaceMeasurements,
27371
+ actualBrowserHeight: adjustedHeight
27372
+ };
27373
+ }
27374
+
27375
+ /**
27376
+ * Extract justify space measurements from browser
27377
+ * @private
27378
+ */
27379
+ _extractJustifySpaceMeasurements(element, lines) {
27380
+ console.log(`🔤 Extracting browser justify space measurements for ${lines.length} lines`);
27381
+
27382
+ // For now, we'll use a simplified approach:
27383
+ // Apply uniform space expansion to match the line width
27384
+ const spaceWidths = [];
27385
+ lines.forEach((line, lineIndex) => {
27386
+ const lineSpaces = [];
27387
+ const spaceCount = (line.match(/\s/g) || []).length;
27388
+ if (spaceCount > 0 && lineIndex < lines.length - 1) {
27389
+ // Don't justify last line
27390
+ // Calculate how much space expansion is needed
27391
+ const normalSpaceWidth = 6.4; // Default space width for STV font
27392
+ const lineWidth = this.width;
27393
+
27394
+ // Estimate natural line width
27395
+ const charCount = line.length - spaceCount;
27396
+ const avgCharWidth = 12; // Approximate for STV font
27397
+
27398
+ // Calculate expanded space width
27399
+ const remainingSpace = lineWidth - charCount * avgCharWidth;
27400
+ const expandedSpaceWidth = remainingSpace / spaceCount;
27401
+ console.log(`🔤 Line ${lineIndex}: ${spaceCount} spaces, natural: ${normalSpaceWidth}px -> justified: ${expandedSpaceWidth.toFixed(1)}px`);
27402
+
27403
+ // Fill array with expanded space widths for this line
27404
+ for (let i = 0; i < spaceCount; i++) {
27405
+ lineSpaces.push(expandedSpaceWidth);
27406
+ }
27407
+ }
27408
+ spaceWidths.push(lineSpaces);
27409
+ });
27410
+ return spaceWidths;
27411
+ }
27412
+
27413
+ /**
27414
+ * Apply browser-calculated justify space measurements
27415
+ * @private
27416
+ */
27417
+ _applyBrowserJustifySpaces() {
27418
+ if (!this._textLines || !this.__charBounds) {
27419
+ console.warn('🔤 BROWSER JUSTIFY: _textLines or __charBounds not ready');
27420
+ return;
27421
+ }
27422
+
27423
+ // Get space measurements from browser wrapping result
27424
+ const styleMap = this._styleMap;
27425
+ if (!styleMap || !styleMap.justifySpaceMeasurements) {
27426
+ console.warn('🔤 BROWSER JUSTIFY: No justify space measurements available');
27427
+ return;
27428
+ }
27429
+ const spaceWidths = styleMap.justifySpaceMeasurements;
27430
+ console.log('🔤 BROWSER JUSTIFY: Applying space measurements to __charBounds');
27431
+
27432
+ // Apply space widths to character bounds
27433
+ this._textLines.forEach((line, lineIndex) => {
27434
+ if (!this.__charBounds || !this.__charBounds[lineIndex] || !spaceWidths[lineIndex]) return;
27435
+ const lineBounds = this.__charBounds[lineIndex];
27436
+ const lineSpaceWidths = spaceWidths[lineIndex];
27437
+ let spaceIndex = 0;
27438
+ for (let charIndex = 0; charIndex < line.length; charIndex++) {
27439
+ if (/\s/.test(line[charIndex]) && spaceIndex < lineSpaceWidths.length) {
27440
+ const expandedWidth = lineSpaceWidths[spaceIndex];
27441
+ if (lineBounds[charIndex]) {
27442
+ const oldWidth = lineBounds[charIndex].width;
27443
+ lineBounds[charIndex].width = expandedWidth;
27444
+ console.log(`🔤 Line ${lineIndex} space ${spaceIndex}: ${oldWidth.toFixed(1)}px -> ${expandedWidth.toFixed(1)}px`);
27445
+ }
27446
+ spaceIndex++;
27447
+ }
27448
+ }
27449
+ });
27450
+ }
27451
+
27452
+ /**
27453
+ * Fallback to default Fabric wrapping
27454
+ * @private
27455
+ */
27456
+ _splitTextIntoLinesDefault(text) {
26568
27457
  const newText = super._splitTextIntoLines(text),
26569
27458
  graphemeLines = this._wrapText(newText.lines, this.width),
26570
27459
  lines = new Array(graphemeLines.length);
@@ -26599,7 +27488,7 @@
26599
27488
  * @private
26600
27489
  */
26601
27490
  initializeEventListeners() {
26602
- var _this$canvas;
27491
+ var _this$canvas4;
26603
27492
  // Track which side is being used for resize to handle position compensation
26604
27493
  let resizeOrigin = null;
26605
27494
 
@@ -26630,7 +27519,7 @@
26630
27519
  });
26631
27520
 
26632
27521
  // Also listen to canvas-level modified event as backup
26633
- (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.on('object:modified', e => {
27522
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.on('object:modified', e => {
26634
27523
  if (e.target === this) {
26635
27524
  const currentResizeOrigin = resizeOrigin; // Capture the value before reset
26636
27525
  setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
@@ -26674,7 +27563,7 @@
26674
27563
  const safetyThreshold = 2; // px - very subtle trigger
26675
27564
 
26676
27565
  if (maxRequiredWidth > this.width - safetyThreshold) {
26677
- var _this$canvas2;
27566
+ var _this$canvas5;
26678
27567
  // Set width to exactly what's needed + minimal safety margin
26679
27568
  const newWidth = maxRequiredWidth + 1; // Add just 1px safety margin
26680
27569
 
@@ -26707,7 +27596,66 @@
26707
27596
  this.__overlayEditor.refresh();
26708
27597
  }, 0);
26709
27598
  }
26710
- (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
27599
+ (_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
27600
+ }
27601
+ }
27602
+
27603
+ /**
27604
+ * Force complete textbox re-initialization (useful after JSON loading)
27605
+ * Overrides Text version with Textbox-specific logic
27606
+ */
27607
+ forceTextReinitialization() {
27608
+ console.log('🔄 Force reinitializing Textbox object');
27609
+
27610
+ // CRITICAL: Ensure textbox is marked as initialized
27611
+ this.initialized = true;
27612
+
27613
+ // Clear all caches and force dirty state
27614
+ this._clearCache();
27615
+ this.dirty = true;
27616
+ this.dynamicMinWidth = 0;
27617
+
27618
+ // Force isEditing false to ensure clean state
27619
+ this.isEditing = false;
27620
+ console.log(' → Set initialized=true, dirty=true, cleared caches');
27621
+
27622
+ // Re-initialize dimensions (this will handle justify properly)
27623
+ this.initDimensions();
27624
+
27625
+ // Double-check that justify was applied by checking space widths
27626
+ if (this.textAlign.includes('justify') && this.__charBounds) {
27627
+ setTimeout(() => {
27628
+ var _this$canvas6;
27629
+ // Verify justify was applied by checking if space widths vary
27630
+ let hasVariableSpaces = false;
27631
+ this.__charBounds.forEach((lineBounds, i) => {
27632
+ if (lineBounds && this._textLines && this._textLines[i]) {
27633
+ const spaces = lineBounds.filter((bound, j) => /\s/.test(this._textLines[i][j]));
27634
+ if (spaces.length > 1) {
27635
+ const firstSpaceWidth = spaces[0].width;
27636
+ hasVariableSpaces = spaces.some(space => Math.abs(space.width - firstSpaceWidth) > 0.1);
27637
+ }
27638
+ }
27639
+ });
27640
+ if (!hasVariableSpaces && this.__charBounds.length > 0) {
27641
+ console.warn(' ⚠️ Justify spaces still uniform - forcing enlargeSpaces again');
27642
+ if (this.enlargeSpaces) {
27643
+ this.enlargeSpaces();
27644
+ }
27645
+ } else {
27646
+ console.log(' ✅ Justify spaces properly expanded');
27647
+ }
27648
+
27649
+ // Ensure height is recalculated - use browser height if available
27650
+ if (this._usingBrowserWrapping && this._actualBrowserHeight) {
27651
+ this.height = this._actualBrowserHeight;
27652
+ console.log(`🔤 JUSTIFY: Preserved browser height: ${this.height}px`);
27653
+ } else {
27654
+ this.height = this.calcTextHeight();
27655
+ console.log(`🔧 JUSTIFY: Used calcTextHeight: ${this.height}px`);
27656
+ }
27657
+ (_this$canvas6 = this.canvas) === null || _this$canvas6 === void 0 || _this$canvas6.requestRenderAll();
27658
+ }, 10);
26711
27659
  }
26712
27660
  }
26713
27661