@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
@@ -410,7 +410,7 @@ class Cache {
410
410
  }
411
411
  const cache = new Cache();
412
412
 
413
- var version = "7.0.1-beta6";
413
+ var version = "7.0.1-beta8";
414
414
 
415
415
  // use this syntax so babel plugin see this import here
416
416
  const VERSION = version;
@@ -17636,6 +17636,7 @@ class Line extends FabricObject {
17636
17636
  _defineProperty(this, "hitStrokeWidth", 'auto');
17637
17637
  _defineProperty(this, "_updatingEndpoints", false);
17638
17638
  _defineProperty(this, "_useEndpointCoords", true);
17639
+ _defineProperty(this, "_exportingSVG", false);
17639
17640
  this.setOptions(options);
17640
17641
  this.x1 = x1;
17641
17642
  this.x2 = x2;
@@ -17847,6 +17848,14 @@ class Line extends FabricObject {
17847
17848
  this.x2 = newX;
17848
17849
  this.y2 = newY;
17849
17850
  }
17851
+
17852
+ // Update gradient coordinates if stroke is a gradient (but not during SVG export)
17853
+ if (this.stroke instanceof Gradient && !this._exportingSVG) {
17854
+ this.stroke.coords.x1 = this.x1;
17855
+ this.stroke.coords.y1 = this.y1;
17856
+ this.stroke.coords.x2 = this.x2;
17857
+ this.stroke.coords.y2 = this.y2;
17858
+ }
17850
17859
  this.dirty = true;
17851
17860
  this.setCoords();
17852
17861
  (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
@@ -17917,6 +17926,14 @@ class Line extends FabricObject {
17917
17926
  if (coordProps.includes(key)) {
17918
17927
  this._setWidthHeight();
17919
17928
  this.dirty = true;
17929
+
17930
+ // Update gradient coordinates if stroke is a gradient (but not during SVG export)
17931
+ if (this.stroke instanceof Gradient && !this._exportingSVG) {
17932
+ this.stroke.coords.x1 = this.x1;
17933
+ this.stroke.coords.y1 = this.y1;
17934
+ this.stroke.coords.x2 = this.x2;
17935
+ this.stroke.coords.y2 = this.y2;
17936
+ }
17920
17937
  }
17921
17938
  if ((key === 'left' || key === 'top') && this.canvas && !this._updatingEndpoints) {
17922
17939
  const deltaX = this.left - oldLeft;
@@ -17927,6 +17944,14 @@ class Line extends FabricObject {
17927
17944
  this.y1 += deltaY;
17928
17945
  this.x2 += deltaX;
17929
17946
  this.y2 += deltaY;
17947
+
17948
+ // Update gradient coordinates if stroke is a gradient
17949
+ if (this.stroke instanceof Gradient) {
17950
+ this.stroke.coords.x1 = this.x1;
17951
+ this.stroke.coords.y1 = this.y1;
17952
+ this.stroke.coords.x2 = this.x2;
17953
+ this.stroke.coords.y2 = this.y2;
17954
+ }
17930
17955
  this._updatingEndpoints = false;
17931
17956
  }
17932
17957
  }
@@ -17940,17 +17965,23 @@ class Line extends FabricObject {
17940
17965
  super.render(ctx);
17941
17966
  }
17942
17967
  _renderDirectly(ctx) {
17943
- var _this$stroke;
17944
17968
  if (!this.visible) return;
17945
17969
  ctx.save();
17946
17970
  ctx.globalAlpha = this.opacity;
17947
- ctx.strokeStyle = ((_this$stroke = this.stroke) === null || _this$stroke === void 0 ? void 0 : _this$stroke.toString()) || '#000';
17948
17971
  ctx.lineWidth = this.strokeWidth;
17949
17972
  ctx.lineCap = this.strokeLineCap || 'butt';
17950
17973
  ctx.beginPath();
17951
17974
  ctx.moveTo(this.x1, this.y1);
17952
17975
  ctx.lineTo(this.x2, this.y2);
17976
+ const origStrokeStyle = ctx.strokeStyle;
17977
+ if (isFiller(this.stroke)) {
17978
+ ctx.strokeStyle = this.stroke.toLive(ctx);
17979
+ } else {
17980
+ var _this$stroke;
17981
+ ctx.strokeStyle = ((_this$stroke = this.stroke) === null || _this$stroke === void 0 ? void 0 : _this$stroke.toString()) || '#000';
17982
+ }
17953
17983
  ctx.stroke();
17984
+ ctx.strokeStyle = origStrokeStyle;
17954
17985
  ctx.restore();
17955
17986
  }
17956
17987
  _render(ctx) {
@@ -18025,7 +18056,15 @@ class Line extends FabricObject {
18025
18056
  _toSVG() {
18026
18057
  if (this._useEndpointCoords) {
18027
18058
  // Use absolute coordinates to bypass all Fabric.js transforms
18028
- 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`];
18059
+ // Handle gradients manually for proper SVG export
18060
+ let strokeAttr = '';
18061
+ if (this.stroke instanceof Gradient) {
18062
+ // Let Fabric.js handle gradient definition, but we'll use the reference
18063
+ strokeAttr = `stroke="url(#${this.stroke.id})"`;
18064
+ } else {
18065
+ strokeAttr = `stroke="${this.stroke || 'none'}"`;
18066
+ }
18067
+ return [`<line ${strokeAttr} stroke-width="${this.strokeWidth}" stroke-linecap="${this.strokeLineCap}" `, `stroke-dasharray="${this.strokeDashArray ? this.strokeDashArray.join(' ') : 'none'}" `, `stroke-dashoffset="${this.strokeDashOffset}" stroke-linejoin="${this.strokeLineJoin}" `, `stroke-miterlimit="${this.strokeMiterLimit}" fill="${this.fill || 'none'}" `, `fill-rule="${this.fillRule}" opacity="${this.opacity}" `, `x1="${this.x1}" y1="${this.y1}" x2="${this.x2}" y2="${this.y2}" />\n`];
18029
18068
  } else {
18030
18069
  // Use standard calcLinePoints for legacy mode
18031
18070
  const {
@@ -18039,9 +18078,26 @@ class Line extends FabricObject {
18039
18078
  }
18040
18079
  toSVG(reviver) {
18041
18080
  if (this._useEndpointCoords) {
18042
- // Override toSVG to prevent Fabric.js from adding transform wrapper
18043
- const markup = this._toSVG().join('');
18044
- return reviver ? reviver(markup) : markup;
18081
+ // For endpoint coords, we need to bypass transforms but still allow gradients
18082
+ // Let's temporarily disable transforms during SVG generation
18083
+ const originalLeft = this.left;
18084
+ const originalTop = this.top;
18085
+
18086
+ // Set position to center of line for gradient calculation
18087
+ this.left = (this.x1 + this.x2) / 2;
18088
+ this.top = (this.y1 + this.y2) / 2;
18089
+
18090
+ // Get the SVG with standard system (for gradient handling)
18091
+ const standardSVG = super.toSVG(reviver);
18092
+
18093
+ // Restore original position
18094
+ this.left = originalLeft;
18095
+ this.top = originalTop;
18096
+
18097
+ // Extract gradient definition and clean up the line element
18098
+ // Remove the transform wrapper and update coordinates
18099
+ const cleanSVG = standardSVG.replace(/<g transform="[^"]*"[^>]*>/g, '').replace(/<\/g>/g, '').replace(/x1="[^"]*"/g, `x1="${this.x1}"`).replace(/y1="[^"]*"/g, `y1="${this.y1}"`).replace(/x2="[^"]*"/g, `x2="${this.x2}"`).replace(/y2="[^"]*"/g, `y2="${this.y2}"`);
18100
+ return cleanSVG;
18045
18101
  }
18046
18102
  // Use default behavior for legacy mode
18047
18103
  return super.toSVG(reviver);
@@ -19273,6 +19329,97 @@ function measureGraphemeWithKerning(grapheme, previousGrapheme, options, ctx) {
19273
19329
  };
19274
19330
  }
19275
19331
 
19332
+ /**
19333
+ * Get a representative character for font metrics measurement
19334
+ * Uses canvas to test which scripts the font actually supports
19335
+ */
19336
+ function getRepresentativeCharacter(fontFamily) {
19337
+ const context = getMeasurementContext();
19338
+
19339
+ // Wait for font to be ready if possible
19340
+ if (typeof document !== 'undefined' && 'fonts' in document) {
19341
+ try {
19342
+ // Check if font is ready, if not, use fallback immediately
19343
+ if (!document.fonts.check(`16px ${fontFamily}`)) {
19344
+ return 'M'; // Use safe fallback while font loads
19345
+ }
19346
+ } catch (e) {
19347
+ // Font check failed, use fallback
19348
+ return 'M';
19349
+ }
19350
+ }
19351
+
19352
+ // Test characters for different scripts
19353
+ const testChars = [{
19354
+ char: 'م',
19355
+ script: 'Arabic'
19356
+ },
19357
+ // Arabic
19358
+ {
19359
+ char: 'א',
19360
+ script: 'Hebrew'
19361
+ },
19362
+ // Hebrew
19363
+ {
19364
+ char: 'अ',
19365
+ script: 'Devanagari'
19366
+ },
19367
+ // Hindi/Sanskrit
19368
+ {
19369
+ char: 'ا',
19370
+ script: 'Urdu'
19371
+ },
19372
+ // Urdu
19373
+ {
19374
+ char: 'ک',
19375
+ script: 'Persian'
19376
+ },
19377
+ // Persian
19378
+ {
19379
+ char: 'த',
19380
+ script: 'Tamil'
19381
+ },
19382
+ // Tamil
19383
+ {
19384
+ char: 'ก',
19385
+ script: 'Thai'
19386
+ },
19387
+ // Thai
19388
+ {
19389
+ char: 'М',
19390
+ script: 'Cyrillic'
19391
+ },
19392
+ // Cyrillic
19393
+ {
19394
+ char: 'Ω',
19395
+ script: 'Greek'
19396
+ },
19397
+ // Greek
19398
+ {
19399
+ char: 'M',
19400
+ script: 'Latin'
19401
+ } // Latin (fallback)
19402
+ ];
19403
+
19404
+ // Set the font
19405
+ context.font = `16px ${fontFamily}`;
19406
+
19407
+ // Test each character to see which ones render properly
19408
+ // Use a more robust width check to avoid false positives
19409
+ const fallbackWidth = context.measureText('M').width;
19410
+ for (const test of testChars) {
19411
+ const metrics = context.measureText(test.char);
19412
+
19413
+ // Character is valid if it has width and isn't just a fallback glyph
19414
+ if (metrics.width > 0 && Math.abs(metrics.width - fallbackWidth) > 0.1) {
19415
+ return test.char;
19416
+ }
19417
+ }
19418
+
19419
+ // Fallback to Latin 'M'
19420
+ return 'M';
19421
+ }
19422
+
19276
19423
  /**
19277
19424
  * Get font metrics for layout calculations
19278
19425
  */
@@ -19286,8 +19433,9 @@ function getFontMetrics(options) {
19286
19433
  const context = getMeasurementContext();
19287
19434
  applyFontStyle(context, options);
19288
19435
 
19289
- // Use 'M' as sample character for metrics
19290
- const metrics = context.measureText('M');
19436
+ // Use representative character based on font's primary script
19437
+ const sample = getRepresentativeCharacter(options.fontFamily);
19438
+ const metrics = context.measureText(sample);
19291
19439
  const fontSize = options.fontSize;
19292
19440
 
19293
19441
  // Calculate metrics with fallbacks
@@ -19339,7 +19487,11 @@ function getFontDeclaration(options) {
19339
19487
  } = options;
19340
19488
 
19341
19489
  // Normalize font family (add quotes if needed)
19342
- const normalizedFamily = fontFamily.includes(' ') && !fontFamily.includes('"') && !fontFamily.includes("'") ? `"${fontFamily}"` : fontFamily;
19490
+ let normalizedFamily = fontFamily.includes(' ') && !fontFamily.includes('"') && !fontFamily.includes("'") ? `"${fontFamily}"` : fontFamily;
19491
+
19492
+ // Note: Font fallbacks are handled in the rendering phase only
19493
+ // to avoid affecting measurement calculations for text wrapping
19494
+
19343
19495
  return `${fontStyle} ${fontWeight} ${fontSize}px ${normalizedFamily}`;
19344
19496
  }
19345
19497
 
@@ -19491,6 +19643,81 @@ const measurementCache = new MeasurementCache();
19491
19643
  const kerningCache = new KerningCache();
19492
19644
  const fontMetricsCache = new FontMetricsCache();
19493
19645
 
19646
+ // Set up font loading listener to clear caches when fonts change
19647
+ if (typeof document !== 'undefined' && 'fonts' in document) {
19648
+ document.fonts.addEventListener('loadingdone', () => {
19649
+ // Clear all caches when fonts finish loading
19650
+ clearAllCaches();
19651
+ });
19652
+ }
19653
+
19654
+ /**
19655
+ * Clear all measurement caches
19656
+ */
19657
+ function clearAllCaches() {
19658
+ measurementCache.clear();
19659
+ kerningCache.clear();
19660
+ fontMetricsCache.clear();
19661
+ }
19662
+
19663
+ /**
19664
+ * Detect if a font lacks English glyph support
19665
+ * These fonts should use browser-native measurement instead of Fabric's character-by-character measurement
19666
+ */
19667
+ function fontLacksEnglishGlyphs(fontFamily) {
19668
+ if (typeof document === 'undefined') return false;
19669
+
19670
+ // Known fonts that lack English glyphs
19671
+ const knownNonEnglishFonts = ['stv', 'arabic', 'naskh', 'thuluth', 'kufi', 'diwani', 'nastaliq', 'kufic', 'hijazi', 'madinah', 'makkah'];
19672
+ const lowerFontFamily = fontFamily.toLowerCase();
19673
+
19674
+ // Check known list first
19675
+ if (knownNonEnglishFonts.some(font => lowerFontFamily.includes(font))) {
19676
+ return true;
19677
+ }
19678
+
19679
+ // Dynamic glyph support detection
19680
+ const context = getMeasurementContext();
19681
+ context.font = `16px ${fontFamily}`;
19682
+
19683
+ // Test English characters
19684
+ const englishChars = ['A', 'B', 'C', 'a', 'b', 'c', 'M', 'W'];
19685
+ const fallbackFont = 'Arial, sans-serif';
19686
+
19687
+ // Measure with target font
19688
+ const targetWidths = englishChars.map(char => context.measureText(char).width);
19689
+
19690
+ // Measure with fallback font
19691
+ context.font = `16px ${fallbackFont}`;
19692
+ const fallbackWidths = englishChars.map(char => context.measureText(char).width);
19693
+
19694
+ // If most measurements are identical, the font likely doesn't have English glyphs
19695
+ let identicalCount = 0;
19696
+ for (let i = 0; i < englishChars.length; i++) {
19697
+ if (Math.abs(targetWidths[i] - fallbackWidths[i]) < 0.5) {
19698
+ identicalCount++;
19699
+ }
19700
+ }
19701
+ const lacksSupportThreshold = englishChars.length * 0.7; // 70% identical = lacks support
19702
+ const lacksSupport = identicalCount >= lacksSupportThreshold;
19703
+ return lacksSupport;
19704
+ }
19705
+
19706
+ // Cache for font glyph detection results
19707
+ const fontGlyphCache = new Map();
19708
+
19709
+ /**
19710
+ * Cached version of font glyph detection
19711
+ */
19712
+ function fontLacksEnglishGlyphsCached(fontFamily) {
19713
+ if (fontGlyphCache.has(fontFamily)) {
19714
+ return fontGlyphCache.get(fontFamily);
19715
+ }
19716
+ const result = fontLacksEnglishGlyphs(fontFamily);
19717
+ fontGlyphCache.set(fontFamily, result);
19718
+ return result;
19719
+ }
19720
+
19494
19721
  /**
19495
19722
  * Unicode and Internationalization Support
19496
19723
  *
@@ -20674,6 +20901,15 @@ class FabricText extends StyledText {
20674
20901
  * Does not return dimensions.
20675
20902
  */
20676
20903
  initDimensions() {
20904
+ // Check if font is ready for accurate measurements
20905
+ // Only block initialization if it's a critical font loading situation
20906
+ const fontReady = this._isFontReady();
20907
+ if (!fontReady && !this.initialized) {
20908
+ // Only schedule font loading on first initialization
20909
+ this._scheduleInitAfterFontLoad();
20910
+ // Continue with fallback measurements for now
20911
+ }
20912
+
20677
20913
  // Use advanced layout if enabled
20678
20914
  if (this.enableAdvancedLayout && !this.path) {
20679
20915
  return this.initDimensionsAdvanced();
@@ -20690,7 +20926,21 @@ class FabricText extends StyledText {
20690
20926
  }
20691
20927
  if (this.textAlign.includes(JUSTIFY)) {
20692
20928
  // once text is measured we need to make space fatter to make justified text.
20693
- this.enlargeSpaces();
20929
+ // Ensure __charBounds exists before calling enlargeSpaces
20930
+ if (this.__charBounds && this.__charBounds.length > 0) {
20931
+ this.enlargeSpaces();
20932
+ } else {
20933
+ console.warn('⚠️ __charBounds not ready for justify alignment, deferring enlargeSpaces');
20934
+ // Defer the justify calculation until the next frame
20935
+ setTimeout(() => {
20936
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
20937
+ var _this$canvas;
20938
+ console.log('🔧 Applying deferred justify alignment');
20939
+ this.enlargeSpaces();
20940
+ (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
20941
+ }
20942
+ }, 0);
20943
+ }
20694
20944
  }
20695
20945
  }
20696
20946
 
@@ -20701,7 +20951,7 @@ class FabricText extends StyledText {
20701
20951
  let diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces;
20702
20952
  const isRtl = this.direction === 'rtl';
20703
20953
  for (let i = 0, len = this._textLines.length; i < len; i++) {
20704
- if (this.textAlign !== JUSTIFY && (i === len - 1 || this.isEndOfWrapping(i))) {
20954
+ if (!this.textAlign.includes('justify') && (i === len - 1 || this.isEndOfWrapping(i))) {
20705
20955
  continue;
20706
20956
  }
20707
20957
  accumulatedSpace = 0;
@@ -20710,6 +20960,9 @@ class FabricText extends StyledText {
20710
20960
  if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) {
20711
20961
  numberOfSpaces = spaces.length;
20712
20962
  diffSpace = (this.width - currentLineWidth) / numberOfSpaces;
20963
+ console.log(`🔧 EnlargeSpaces Line ${i}:`);
20964
+ console.log(` Current width: ${currentLineWidth}, Target: ${this.width}`);
20965
+ console.log(` Spaces: ${numberOfSpaces}, diffSpace: ${diffSpace.toFixed(2)}`);
20713
20966
  if (isRtl) {
20714
20967
  for (let j = 0; j < line.length; j++) {
20715
20968
  if (this._reSpaceAndTab.test(line[j])) ;
@@ -20825,6 +21078,18 @@ class FabricText extends StyledText {
20825
21078
 
20826
21079
  // Convert layout to legacy format for compatibility
20827
21080
  this._convertLayoutToLegacyFormat(layout);
21081
+
21082
+ // Ensure justify alignment is properly applied for compatibility with legacy rendering
21083
+ if (this.textAlign.includes(JUSTIFY)) {
21084
+ // Force enlarge spaces after advanced layout calculation
21085
+ setTimeout(() => {
21086
+ if (this.enlargeSpaces) {
21087
+ var _this$canvas2;
21088
+ this.enlargeSpaces();
21089
+ (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.renderAll();
21090
+ }
21091
+ }, 0);
21092
+ }
20828
21093
  this.dirty = true;
20829
21094
  }
20830
21095
 
@@ -21836,7 +22101,19 @@ class FabricText extends StyledText {
21836
22101
  fontSize = this.fontSize
21837
22102
  } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
21838
22103
  let forMeasuring = arguments.length > 1 ? arguments[1] : undefined;
21839
- const parsedFontFamily = fontFamily.includes("'") || fontFamily.includes('"') || fontFamily.includes(',') || FabricText.genericFonts.includes(fontFamily.toLowerCase()) ? fontFamily : `"${fontFamily}"`;
22104
+ let parsedFontFamily = fontFamily.includes("'") || fontFamily.includes('"') || fontFamily.includes(',') || FabricText.genericFonts.includes(fontFamily.toLowerCase()) ? fontFamily : `"${fontFamily}"`;
22105
+
22106
+ // For fonts like STV that don't support English/Latin characters,
22107
+ // add fallback fonts for consistent rendering of unsupported characters
22108
+ // Only add fallbacks during actual rendering, not for measurements
22109
+ if (!forMeasuring &&
22110
+ // Only during rendering, not measuring
22111
+ !fontFamily.includes(',') && (
22112
+ // Don't add fallbacks if already has them
22113
+ fontFamily.toLowerCase().includes('stv') || fontFamily.toLowerCase().includes('arabic') || fontFamily.toLowerCase().includes('naskh') || fontFamily.toLowerCase().includes('kufi'))) {
22114
+ // Add fallback fonts for unsupported characters (spaces, punctuation, etc.)
22115
+ parsedFontFamily = `${parsedFontFamily}, "Arial Unicode MS", Arial, sans-serif`;
22116
+ }
21840
22117
  return [fontStyle, fontWeight, `${forMeasuring ? this.CACHE_FONT_SIZE : fontSize}px`, parsedFontFamily].join(' ');
21841
22118
  }
21842
22119
 
@@ -21880,7 +22157,13 @@ class FabricText extends StyledText {
21880
22157
  newLine = ['\n'];
21881
22158
  let newText = [];
21882
22159
  for (let i = 0; i < lines.length; i++) {
21883
- newLines[i] = this.graphemeSplit(lines[i]);
22160
+ // Use BiDi-aware grapheme splitting for RTL text
22161
+ if (this.direction === 'rtl' || this._containsArabicText(lines[i])) {
22162
+ newLines[i] = segmentGraphemes(lines[i]);
22163
+ console.log(`🔤 BiDi-aware split line ${i}: "${lines[i]}" -> [${newLines[i].join(', ')}]`);
22164
+ } else {
22165
+ newLines[i] = this.graphemeSplit(lines[i]);
22166
+ }
21884
22167
  newText = newText.concat(newLines[i], newLine);
21885
22168
  }
21886
22169
  newText.pop();
@@ -21892,6 +22175,14 @@ class FabricText extends StyledText {
21892
22175
  };
21893
22176
  }
21894
22177
 
22178
+ /**
22179
+ * Check if text contains Arabic characters
22180
+ * @private
22181
+ */
22182
+ _containsArabicText(text) {
22183
+ return /[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
22184
+ }
22185
+
21895
22186
  /**
21896
22187
  * Returns object representation of an instance
21897
22188
  * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
@@ -22014,6 +22305,88 @@ class FabricText extends StyledText {
22014
22305
 
22015
22306
  /* _FROM_SVG_END_ */
22016
22307
 
22308
+ /**
22309
+ * Check if the font is ready for accurate measurements
22310
+ * @private
22311
+ */
22312
+ _isFontReady() {
22313
+ if (typeof document === 'undefined' || !('fonts' in document)) {
22314
+ return true; // Assume ready in non-browser environments
22315
+ }
22316
+ try {
22317
+ return document.fonts.check(`${this.fontSize}px ${this.fontFamily}`);
22318
+ } catch (e) {
22319
+ return true; // Fallback to assuming ready if check fails
22320
+ }
22321
+ }
22322
+
22323
+ /**
22324
+ * Schedule re-initialization after font loads
22325
+ * @private
22326
+ */
22327
+ _scheduleInitAfterFontLoad() {
22328
+ if (typeof document === 'undefined' || !('fonts' in document)) {
22329
+ return;
22330
+ }
22331
+
22332
+ // Only schedule if not already waiting
22333
+ if (this._fontLoadScheduled) {
22334
+ return;
22335
+ }
22336
+ this._fontLoadScheduled = true;
22337
+ const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
22338
+ document.fonts.load(fontSpec).then(() => {
22339
+ this._fontLoadScheduled = false;
22340
+ // Re-initialize dimensions with proper font metrics
22341
+ this.initDimensions();
22342
+
22343
+ // Extra step for justify alignment after font loading
22344
+ if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
22345
+ setTimeout(() => {
22346
+ var _this$canvas3;
22347
+ if (this.enlargeSpaces) {
22348
+ this.enlargeSpaces();
22349
+ }
22350
+ (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
22351
+ }, 10);
22352
+ } else {
22353
+ var _this$canvas4;
22354
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.requestRenderAll();
22355
+ }
22356
+ }).catch(() => {
22357
+ this._fontLoadScheduled = false;
22358
+ });
22359
+ }
22360
+
22361
+ /**
22362
+ * Force complete text re-initialization (useful after JSON loading)
22363
+ */
22364
+ forceTextReinitialization() {
22365
+ console.log('🔄 Force reinitializing text object');
22366
+
22367
+ // Clear all caches
22368
+ this._clearCache();
22369
+ this.dirty = true;
22370
+
22371
+ // Force text splitting to rebuild internal structures
22372
+ this._splitText();
22373
+
22374
+ // Re-initialize dimensions
22375
+ this.initDimensions();
22376
+
22377
+ // Special handling for justify alignment
22378
+ if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
22379
+ // Ensure justify is applied after dimensions are set
22380
+ setTimeout(() => {
22381
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
22382
+ var _this$canvas5;
22383
+ this.enlargeSpaces();
22384
+ (_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
22385
+ }
22386
+ }, 10);
22387
+ }
22388
+ }
22389
+
22017
22390
  /**
22018
22391
  * Returns FabricText instance from an object representation
22019
22392
  * @param {Object} object plain js Object to create an instance from
@@ -22025,6 +22398,93 @@ class FabricText extends StyledText {
22025
22398
  styles: stylesFromArray(object.styles || {}, object.text)
22026
22399
  }, {
22027
22400
  extraParam: 'text'
22401
+ }).then(textObject => {
22402
+ // Ensure text object is properly initialized after JSON deserialization
22403
+ // This is critical for justify alignment and other text layout features
22404
+ textObject.initialized = true;
22405
+
22406
+ // Force reinitialization to ensure proper layout
22407
+ if (textObject._clearCache) {
22408
+ textObject._clearCache();
22409
+ }
22410
+ textObject.dirty = true;
22411
+
22412
+ // Check if we need to wait for font loading (especially for custom fonts like STV)
22413
+ const fontSpec = `${textObject.fontSize}px ${textObject.fontFamily}`;
22414
+
22415
+ // For custom fonts, ensure they're loaded before initializing dimensions
22416
+ if (typeof document !== 'undefined' && 'fonts' in document && textObject.fontFamily !== 'Arial' && textObject.fontFamily !== 'Times New Roman') {
22417
+ return document.fonts.load(fontSpec).then(() => {
22418
+ var _textObject$fontFamil;
22419
+ console.log(`🔤 Font loaded for JSON object: ${fontSpec}`);
22420
+ // Ensure initialized flag is set again (in case constructor reset it)
22421
+ textObject.initialized = true;
22422
+
22423
+ // Special handling for STV fonts which have measurement issues
22424
+ const isStvFont = (_textObject$fontFamil = textObject.fontFamily) === null || _textObject$fontFamil === void 0 ? void 0 : _textObject$fontFamil.toLowerCase().includes('stv');
22425
+ if (isStvFont) {
22426
+ console.log(`🔤 STV font detected, using enhanced reinitialization`);
22427
+
22428
+ // Clear all cached state that might interfere with browser wrapping
22429
+ textObject._browserWrapCache = null;
22430
+ textObject._lastDimensionState = null;
22431
+ textObject._browserWrapInitialized = false;
22432
+ console.log(`🔤 STV font: Cleared all cached states for fresh initialization`);
22433
+
22434
+ // Force browser wrapping flag for STV fonts
22435
+ textObject._usingBrowserWrapping = true;
22436
+ console.log(`🔤 STV font: Forcing browser wrapping flag during JSON load`);
22437
+
22438
+ // Multiple initialization attempts for STV fonts
22439
+ const reinitWithDelay = attempt => {
22440
+ if (textObject.forceTextReinitialization) {
22441
+ textObject.forceTextReinitialization();
22442
+ } else {
22443
+ textObject.initDimensions();
22444
+ }
22445
+
22446
+ // Check if width is still problematic after initialization
22447
+ if (textObject.width < 50 && attempt < 3) {
22448
+ console.log(`🔤 STV font width still ${textObject.width}px, retrying in ${100 * attempt}ms (attempt ${attempt + 1}/3)`);
22449
+ setTimeout(() => reinitWithDelay(attempt + 1), 100 * attempt);
22450
+ }
22451
+ };
22452
+ reinitWithDelay(0);
22453
+ } else {
22454
+ // Use specialized reinitialization for Textbox objects
22455
+ if (textObject.forceTextReinitialization) {
22456
+ console.log(`🔤 Using Textbox specialized reinitialization`);
22457
+ textObject.forceTextReinitialization();
22458
+ } else {
22459
+ // Reinitialize dimensions with proper font metrics
22460
+ textObject.initDimensions();
22461
+ }
22462
+ }
22463
+ return textObject;
22464
+ }).catch(() => {
22465
+ console.warn(`⚠️ Font loading failed for ${fontSpec}, proceeding with fallback`);
22466
+ // Ensure initialized flag is set again
22467
+ textObject.initialized = true;
22468
+
22469
+ // Still initialize dimensions even if font loading fails
22470
+ if (textObject.forceTextReinitialization) {
22471
+ textObject.forceTextReinitialization();
22472
+ } else {
22473
+ textObject.initDimensions();
22474
+ }
22475
+ return textObject;
22476
+ });
22477
+ } else {
22478
+ // Standard fonts - ensure initialized and use appropriate method
22479
+ textObject.initialized = true;
22480
+ if (textObject.forceTextReinitialization) {
22481
+ console.log(`🔤 Using Textbox specialized reinitialization for standard font`);
22482
+ textObject.forceTextReinitialization();
22483
+ } else {
22484
+ textObject.initDimensions();
22485
+ }
22486
+ return textObject;
22487
+ }
22028
22488
  });
22029
22489
  }
22030
22490
  }
@@ -22668,6 +23128,13 @@ class OverlayEditor {
22668
23128
 
22669
23129
  // Apply all other font and text styles to match Fabric
22670
23130
  const letterSpacingPx = (target.charSpacing || 0) / 1000 * finalFontSize;
23131
+
23132
+ // Special handling for text objects loaded from JSON - ensure they're properly initialized
23133
+ if (target.dirty !== false && target.initDimensions) {
23134
+ console.log('🔧 Ensuring text object is properly initialized before overlay editing');
23135
+ // Force re-initialization if the text object seems to be in a dirty state
23136
+ target.initDimensions();
23137
+ }
22671
23138
  this.textarea.style.fontSize = `${finalFontSize}px`;
22672
23139
  this.textarea.style.lineHeight = String(fabricLineHeight);
22673
23140
  this.textarea.style.fontFamily = target.fontFamily || 'Arial';
@@ -26158,8 +26625,27 @@ class Textbox extends IText {
26158
26625
  */
26159
26626
  initDimensions() {
26160
26627
  if (!this.initialized) {
26628
+ this.initialized = true;
26629
+ }
26630
+
26631
+ // Prevent rapid recalculations during moves
26632
+ if (this._usingBrowserWrapping) {
26633
+ const now = Date.now();
26634
+ const lastCall = this._lastInitDimensionsTime || 0;
26635
+ const isRapidCall = now - lastCall < 100;
26636
+ const isDuringLoading = this._jsonLoading || !this._browserWrapInitialized;
26637
+ if (isRapidCall && !isDuringLoading) {
26638
+ return;
26639
+ }
26640
+ this._lastInitDimensionsTime = now;
26641
+ }
26642
+
26643
+ // Skip if nothing changed
26644
+ const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
26645
+ if (this._lastDimensionState === currentState && this._textLines && this._textLines.length > 0) {
26161
26646
  return;
26162
26647
  }
26648
+ this._lastDimensionState = currentState;
26163
26649
 
26164
26650
  // Use advanced layout if enabled
26165
26651
  if (this.enableAdvancedLayout) {
@@ -26170,17 +26656,142 @@ class Textbox extends IText {
26170
26656
  // clear dynamicMinWidth as it will be different after we re-wrap line
26171
26657
  this.dynamicMinWidth = 0;
26172
26658
  // wrap lines
26173
- this._styleMap = this._generateStyleMap(this._splitText());
26174
- // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap
26175
- if (this.dynamicMinWidth > this.width) {
26659
+ const splitTextResult = this._splitText();
26660
+ this._styleMap = this._generateStyleMap(splitTextResult);
26661
+
26662
+ // For browser wrapping, ensure _textLines is set from browser results
26663
+ if (this._usingBrowserWrapping && splitTextResult && splitTextResult.lines) {
26664
+ this._textLines = splitTextResult.lines.map(line => line.split(''));
26665
+
26666
+ // Store justify measurements and browser height
26667
+ const justifyMeasurements = splitTextResult.justifySpaceMeasurements;
26668
+ if (justifyMeasurements) {
26669
+ this._styleMap.justifySpaceMeasurements = justifyMeasurements;
26670
+ }
26671
+ const actualHeight = splitTextResult.actualBrowserHeight;
26672
+ if (actualHeight) {
26673
+ this._actualBrowserHeight = actualHeight;
26674
+ }
26675
+ }
26676
+ // Don't auto-resize width when using browser wrapping to prevent width increases during moves
26677
+ if (!this._usingBrowserWrapping && this.dynamicMinWidth > this.width) {
26176
26678
  this._set('width', this.dynamicMinWidth);
26177
26679
  }
26680
+
26681
+ // For browser wrapping fonts (like STV), ensure minimum width for new textboxes
26682
+ // since these fonts can't measure English characters properly
26683
+ if (this._usingBrowserWrapping && this.width < 50) {
26684
+ console.log(`🔤 BROWSER WRAP: Font ${this.fontFamily} has width ${this.width}px, setting to 300px for usability`);
26685
+ this.width = 300;
26686
+ }
26687
+
26688
+ // Mark browser wrapping as initialized when complete
26689
+ if (this._usingBrowserWrapping) {
26690
+ this._browserWrapInitialized = true;
26691
+ }
26178
26692
  if (this.textAlign.includes(JUSTIFY)) {
26693
+ // For browser wrapping fonts, apply browser-calculated justify spaces
26694
+ if (this._usingBrowserWrapping) {
26695
+ console.log('🔤 BROWSER WRAP: Applying browser-calculated justify spaces');
26696
+ this._applyBrowserJustifySpaces();
26697
+ return;
26698
+ }
26699
+
26700
+ // Don't apply justify alignment during drag operations to prevent snapping
26701
+ const now = Date.now();
26702
+ const lastDragTime = this._lastInitDimensionsTime || 0;
26703
+ const isDuringDrag = now - lastDragTime < 200; // 200ms window for drag detection
26704
+
26705
+ if (isDuringDrag) {
26706
+ console.log('🔤 Skipping justify during drag operation to prevent snapping');
26707
+ return;
26708
+ }
26709
+
26710
+ // For non-browser-wrapping fonts, use Fabric's justify system
26179
26711
  // once text is measured we need to make space fatter to make justified text.
26180
- this.enlargeSpaces();
26712
+ // Ensure __charBounds exists and fonts are ready before applying justify
26713
+ if (this.__charBounds && this.__charBounds.length > 0) {
26714
+ // Check if font is ready for accurate justify calculations
26715
+ const fontReady = this._isFontReady ? this._isFontReady() : true;
26716
+ if (fontReady) {
26717
+ this.enlargeSpaces();
26718
+ } else {
26719
+ console.warn('⚠️ Textbox: Font not ready for justify, deferring enlargeSpaces');
26720
+ // Defer justify calculation until font is ready
26721
+ this._scheduleJustifyAfterFontLoad();
26722
+ }
26723
+ } else {
26724
+ console.warn('⚠️ Textbox: __charBounds not ready for justify alignment, deferring enlargeSpaces');
26725
+ // Defer the justify calculation until the next frame
26726
+ setTimeout(() => {
26727
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
26728
+ var _this$canvas;
26729
+ console.log('🔧 Applying deferred Textbox justify alignment');
26730
+ this.enlargeSpaces();
26731
+ (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
26732
+ }
26733
+ }, 0);
26734
+ }
26735
+ }
26736
+ // Calculate height - use Fabric's calculation for proper text rendering space
26737
+ if (this._usingBrowserWrapping && this._textLines && this._textLines.length > 0) {
26738
+ const actualBrowserHeight = this._actualBrowserHeight;
26739
+ const oldHeight = this.height;
26740
+ // Use Fabric's height calculation since it knows how much space text rendering needs
26741
+ this.height = this.calcTextHeight();
26742
+
26743
+ // Force canvas refresh and control update if height changed significantly
26744
+ if (Math.abs(this.height - oldHeight) > 1) {
26745
+ var _this$canvas2, _this$_textLines;
26746
+ this.setCoords();
26747
+ (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
26748
+
26749
+ // DEBUG: Log exact positioning details
26750
+ console.log(`🎯 POSITIONING DEBUG:`);
26751
+ console.log(` Textbox height: ${this.height}px`);
26752
+ console.log(` Textbox top: ${this.top}px`);
26753
+ console.log(` Textbox left: ${this.left}px`);
26754
+ console.log(` Text lines: ${((_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length) || 0}`);
26755
+ console.log(` Font size: ${this.fontSize}px`);
26756
+ console.log(` Line height: ${this.lineHeight || 1.16}`);
26757
+ console.log(` Calculated line height: ${this.fontSize * (this.lineHeight || 1.16)}px`);
26758
+ console.log(` _getTopOffset(): ${this._getTopOffset()}px`);
26759
+ console.log(` calcTextHeight(): ${this.calcTextHeight()}px`);
26760
+ console.log(` Browser height: ${actualBrowserHeight}px`);
26761
+ console.log(` Height difference: ${this.height - this.calcTextHeight()}px`);
26762
+ }
26763
+ } else {
26764
+ this.height = this.calcTextHeight();
26765
+ }
26766
+ }
26767
+
26768
+ /**
26769
+ * Schedule justify calculation after font loads (Textbox-specific)
26770
+ * @private
26771
+ */
26772
+ _scheduleJustifyAfterFontLoad() {
26773
+ if (typeof document === 'undefined' || !('fonts' in document)) {
26774
+ return;
26181
26775
  }
26182
- // clear cache and re-calculate height
26183
- this.height = this.calcTextHeight();
26776
+
26777
+ // Only schedule if not already waiting
26778
+ if (this._fontJustifyScheduled) {
26779
+ return;
26780
+ }
26781
+ this._fontJustifyScheduled = true;
26782
+ const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
26783
+ document.fonts.load(fontSpec).then(() => {
26784
+ var _this$canvas3;
26785
+ this._fontJustifyScheduled = false;
26786
+ console.log('🔧 Textbox: Font loaded, applying justify alignment');
26787
+
26788
+ // Re-run initDimensions to ensure proper justify calculation
26789
+ this.initDimensions();
26790
+ (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
26791
+ }).catch(() => {
26792
+ this._fontJustifyScheduled = false;
26793
+ console.warn('⚠️ Textbox: Font loading failed, justify may be incorrect');
26794
+ });
26184
26795
  }
26185
26796
 
26186
26797
  /**
@@ -26547,19 +27158,33 @@ class Textbox extends IText {
26547
27158
  width: wordWidth
26548
27159
  } = data[i];
26549
27160
  offset += word.length;
26550
- lineWidth += infixWidth + wordWidth - additionalSpace;
26551
- if (lineWidth > maxWidth && !lineJustStarted) {
27161
+
27162
+ // Predictive wrapping: check if adding this word would exceed the width
27163
+ const potentialLineWidth = lineWidth + infixWidth + wordWidth - additionalSpace;
27164
+ // Use exact width to match overlay editor behavior
27165
+ const conservativeMaxWidth = maxWidth; // No artificial buffer
27166
+
27167
+ // Debug logging for wrapping decisions
27168
+ const currentLineText = line.join('');
27169
+ console.log(`🔧 FABRIC WRAP CHECK: "${data[i].word}" -> potential: ${potentialLineWidth.toFixed(1)}px vs limit: ${conservativeMaxWidth.toFixed(1)}px`);
27170
+ if (potentialLineWidth > conservativeMaxWidth && !lineJustStarted) {
27171
+ // This word would exceed the width, wrap before adding it
27172
+ console.log(`🔧 FABRIC WRAP! Line: "${currentLineText}" (${lineWidth.toFixed(1)}px)`);
26552
27173
  graphemeLines.push(line);
26553
27174
  line = [];
26554
- lineWidth = wordWidth;
27175
+ lineWidth = wordWidth; // Start new line with just this word
26555
27176
  lineJustStarted = true;
26556
27177
  } else {
26557
- lineWidth += additionalSpace;
27178
+ // Word fits, add it to current line
27179
+ lineWidth = potentialLineWidth + additionalSpace;
26558
27180
  }
26559
27181
  if (!lineJustStarted && !splitByGrapheme) {
26560
27182
  line.push(infix);
26561
27183
  }
26562
27184
  line = line.concat(word);
27185
+
27186
+ // Debug: show current line after adding word
27187
+ console.log(`🔧 FABRIC AFTER ADD: Line now: "${line.join('')}" (${line.length} chars)`);
26563
27188
  infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset);
26564
27189
  offset++;
26565
27190
  lineJustStarted = false;
@@ -26569,9 +27194,19 @@ class Textbox extends IText {
26569
27194
  // TODO: this code is probably not necessary anymore.
26570
27195
  // it can be moved out of this function since largestWordWidth is now
26571
27196
  // known in advance
26572
- if (largestWordWidth + reservedSpace > this.dynamicMinWidth) {
27197
+ // Don't modify dynamicMinWidth when using browser wrapping to prevent width increases
27198
+ if (!this._usingBrowserWrapping && largestWordWidth + reservedSpace > this.dynamicMinWidth) {
27199
+ console.log(`🔧 FABRIC updating dynamicMinWidth: ${this.dynamicMinWidth} -> ${largestWordWidth - additionalSpace + reservedSpace}`);
26573
27200
  this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace;
27201
+ } else if (this._usingBrowserWrapping) {
27202
+ console.log(`🔤 BROWSER WRAP: Skipping dynamicMinWidth update to prevent width increase`);
26574
27203
  }
27204
+
27205
+ // Debug: show final wrapped lines
27206
+ console.log(`🔧 FABRIC FINAL LINES: ${graphemeLines.length} lines`);
27207
+ graphemeLines.forEach((line, i) => {
27208
+ console.log(` Line ${i + 1}: "${line.join('')}" (${line.length} chars)`);
27209
+ });
26575
27210
  return graphemeLines;
26576
27211
  }
26577
27212
 
@@ -26615,6 +27250,260 @@ class Textbox extends IText {
26615
27250
  * @override
26616
27251
  */
26617
27252
  _splitTextIntoLines(text) {
27253
+ // Check if we need browser wrapping using smart font detection
27254
+ const needsBrowserWrapping = this.fontFamily && fontLacksEnglishGlyphsCached(this.fontFamily);
27255
+ if (needsBrowserWrapping) {
27256
+ // Cache key based on text content, width, font properties, AND text alignment
27257
+ const textHash = text.length + text.slice(0, 50); // Include text content in cache key
27258
+ const cacheKey = `${textHash}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
27259
+
27260
+ // Check if we have a cached result and nothing has changed
27261
+ if (this._browserWrapCache && this._browserWrapCache.key === cacheKey) {
27262
+ const cachedResult = this._browserWrapCache.result;
27263
+
27264
+ // For justify alignment, ensure we have the measurements
27265
+ if (this.textAlign.includes('justify') && !cachedResult.justifySpaceMeasurements) ; else {
27266
+ return cachedResult;
27267
+ }
27268
+ }
27269
+ const result = this._splitTextIntoLinesWithBrowser(text);
27270
+
27271
+ // Cache the result
27272
+ this._browserWrapCache = {
27273
+ key: cacheKey,
27274
+ result
27275
+ };
27276
+
27277
+ // Mark that we used browser wrapping to prevent dynamicMinWidth modifications
27278
+ this._usingBrowserWrapping = true;
27279
+ return result;
27280
+ }
27281
+
27282
+ // Clear the browser wrapping flag when using regular wrapping
27283
+ this._usingBrowserWrapping = false;
27284
+
27285
+ // Default Fabric wrapping for other fonts
27286
+ const newText = super._splitTextIntoLines(text),
27287
+ graphemeLines = this._wrapText(newText.lines, this.width),
27288
+ lines = new Array(graphemeLines.length);
27289
+ for (let i = 0; i < graphemeLines.length; i++) {
27290
+ lines[i] = graphemeLines[i].join('');
27291
+ }
27292
+ newText.lines = lines;
27293
+ newText.graphemeLines = graphemeLines;
27294
+ return newText;
27295
+ }
27296
+
27297
+ /**
27298
+ * Use browser's native text wrapping for accurate handling of fonts without English glyphs
27299
+ * @private
27300
+ */
27301
+ _splitTextIntoLinesWithBrowser(text) {
27302
+ if (typeof document === 'undefined') {
27303
+ // Fallback to regular wrapping in Node.js
27304
+ return this._splitTextIntoLinesDefault(text);
27305
+ }
27306
+
27307
+ // Create a hidden element that mimics the overlay editor
27308
+ const testElement = document.createElement('div');
27309
+ testElement.style.position = 'absolute';
27310
+ testElement.style.left = '-9999px';
27311
+ testElement.style.visibility = 'hidden';
27312
+ testElement.style.fontSize = `${this.fontSize}px`;
27313
+ testElement.style.fontFamily = `"${this.fontFamily}"`;
27314
+ testElement.style.fontWeight = String(this.fontWeight || 'normal');
27315
+ testElement.style.fontStyle = String(this.fontStyle || 'normal');
27316
+ testElement.style.lineHeight = String(this.lineHeight || 1.16);
27317
+ testElement.style.width = `${this.width}px`;
27318
+ testElement.style.direction = this.direction || 'ltr';
27319
+ testElement.style.whiteSpace = 'pre-wrap';
27320
+ testElement.style.wordBreak = 'normal';
27321
+ testElement.style.overflowWrap = 'break-word';
27322
+
27323
+ // Set browser-native text alignment (including justify)
27324
+ if (this.textAlign.includes('justify')) {
27325
+ testElement.style.textAlign = 'justify';
27326
+ testElement.style.textAlignLast = 'auto'; // Let browser decide last line alignment
27327
+ } else {
27328
+ testElement.style.textAlign = this.textAlign;
27329
+ }
27330
+ testElement.textContent = text;
27331
+ document.body.appendChild(testElement);
27332
+
27333
+ // Get the browser's natural line breaks
27334
+ const range = document.createRange();
27335
+ const lines = [];
27336
+ const graphemeLines = [];
27337
+ try {
27338
+ // Simple approach: split by measuring character positions
27339
+ const textNode = testElement.firstChild;
27340
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
27341
+ let currentLineStart = 0;
27342
+ const textLength = text.length;
27343
+ let previousBottom = 0;
27344
+ for (let i = 0; i <= textLength; i++) {
27345
+ range.setStart(textNode, currentLineStart);
27346
+ range.setEnd(textNode, i);
27347
+ const rect = range.getBoundingClientRect();
27348
+ if (i > currentLineStart && (rect.bottom > previousBottom + 5 || i === textLength)) {
27349
+ // New line detected or end of text
27350
+ const lineEnd = i === textLength ? i : i - 1;
27351
+ const lineText = text.substring(currentLineStart, lineEnd).trim();
27352
+ if (lineText) {
27353
+ lines.push(lineText);
27354
+ // Convert to graphemes for compatibility
27355
+ const graphemeLine = lineText.split('');
27356
+ graphemeLines.push(graphemeLine);
27357
+ }
27358
+ currentLineStart = lineEnd;
27359
+ previousBottom = rect.bottom;
27360
+ }
27361
+ }
27362
+ }
27363
+ } catch (error) {
27364
+ console.warn('Browser wrapping failed, using fallback:', error);
27365
+ document.body.removeChild(testElement);
27366
+ return this._splitTextIntoLinesDefault(text);
27367
+ }
27368
+
27369
+ // Extract actual browser height BEFORE removing element
27370
+ const actualBrowserHeight = testElement.scrollHeight;
27371
+ const offsetHeight = testElement.offsetHeight;
27372
+ const clientHeight = testElement.clientHeight;
27373
+ const boundingRect = testElement.getBoundingClientRect();
27374
+ console.log(`🔤 Browser element measurements:`);
27375
+ console.log(` scrollHeight: ${actualBrowserHeight}px (content + padding + hidden overflow)`);
27376
+ console.log(` offsetHeight: ${offsetHeight}px (content + padding + border)`);
27377
+ console.log(` clientHeight: ${clientHeight}px (content + padding, no border/scrollbar)`);
27378
+ console.log(` boundingRect.height: ${boundingRect.height}px (actual rendered height)`);
27379
+ console.log(` Font size: ${this.fontSize}px, Line height: ${this.lineHeight || 1.16}, Lines: ${lines.length}`);
27380
+
27381
+ // For justify alignment, extract space measurements from browser BEFORE removing element
27382
+ let justifySpaceMeasurements = null;
27383
+ if (this.textAlign.includes('justify')) {
27384
+ justifySpaceMeasurements = this._extractJustifySpaceMeasurements(testElement, lines);
27385
+ }
27386
+ document.body.removeChild(testElement);
27387
+ console.log(`🔤 Browser wrapping result: ${lines.length} lines`);
27388
+
27389
+ // Try different height measurements to find the most accurate
27390
+ let bestHeight = actualBrowserHeight;
27391
+
27392
+ // If scrollHeight and offsetHeight differ significantly, investigate
27393
+ if (Math.abs(actualBrowserHeight - offsetHeight) > 2) {
27394
+ console.log(`🔤 Height discrepancy detected: scrollHeight=${actualBrowserHeight}px vs offsetHeight=${offsetHeight}px`);
27395
+ }
27396
+
27397
+ // Consider using boundingRect height if it's larger (sometimes more accurate for visible content)
27398
+ if (boundingRect.height > bestHeight) {
27399
+ console.log(`🔤 Using boundingRect height (${boundingRect.height}px) instead of scrollHeight (${bestHeight}px)`);
27400
+ bestHeight = boundingRect.height;
27401
+ }
27402
+
27403
+ // Font-specific height adjustments for accurate bounding box
27404
+ let adjustedHeight = bestHeight;
27405
+
27406
+ // Fonts without English glyphs need additional height buffer due to different font metrics
27407
+ const lacksEnglishGlyphs = fontLacksEnglishGlyphsCached(this.fontFamily);
27408
+ if (lacksEnglishGlyphs) {
27409
+ const glyphBuffer = this.fontSize * 0.25; // 25% of font size for non-English fonts
27410
+ adjustedHeight = bestHeight + glyphBuffer;
27411
+ console.log(`🔤 Non-English font detected (${this.fontFamily}): Adding ${glyphBuffer}px buffer (${bestHeight}px + ${glyphBuffer}px = ${adjustedHeight}px)`);
27412
+ } else {
27413
+ console.log(`🔤 Standard font (${this.fontFamily}): Using browser height directly (${bestHeight}px)`);
27414
+ }
27415
+ return {
27416
+ _unwrappedLines: [text.split('')],
27417
+ lines: lines,
27418
+ graphemeText: text.split(''),
27419
+ graphemeLines: graphemeLines,
27420
+ justifySpaceMeasurements: justifySpaceMeasurements,
27421
+ actualBrowserHeight: adjustedHeight
27422
+ };
27423
+ }
27424
+
27425
+ /**
27426
+ * Extract justify space measurements from browser
27427
+ * @private
27428
+ */
27429
+ _extractJustifySpaceMeasurements(element, lines) {
27430
+ console.log(`🔤 Extracting browser justify space measurements for ${lines.length} lines`);
27431
+
27432
+ // For now, we'll use a simplified approach:
27433
+ // Apply uniform space expansion to match the line width
27434
+ const spaceWidths = [];
27435
+ lines.forEach((line, lineIndex) => {
27436
+ const lineSpaces = [];
27437
+ const spaceCount = (line.match(/\s/g) || []).length;
27438
+ if (spaceCount > 0 && lineIndex < lines.length - 1) {
27439
+ // Don't justify last line
27440
+ // Calculate how much space expansion is needed
27441
+ const normalSpaceWidth = 6.4; // Default space width for STV font
27442
+ const lineWidth = this.width;
27443
+
27444
+ // Estimate natural line width
27445
+ const charCount = line.length - spaceCount;
27446
+ const avgCharWidth = 12; // Approximate for STV font
27447
+
27448
+ // Calculate expanded space width
27449
+ const remainingSpace = lineWidth - charCount * avgCharWidth;
27450
+ const expandedSpaceWidth = remainingSpace / spaceCount;
27451
+ console.log(`🔤 Line ${lineIndex}: ${spaceCount} spaces, natural: ${normalSpaceWidth}px -> justified: ${expandedSpaceWidth.toFixed(1)}px`);
27452
+
27453
+ // Fill array with expanded space widths for this line
27454
+ for (let i = 0; i < spaceCount; i++) {
27455
+ lineSpaces.push(expandedSpaceWidth);
27456
+ }
27457
+ }
27458
+ spaceWidths.push(lineSpaces);
27459
+ });
27460
+ return spaceWidths;
27461
+ }
27462
+
27463
+ /**
27464
+ * Apply browser-calculated justify space measurements
27465
+ * @private
27466
+ */
27467
+ _applyBrowserJustifySpaces() {
27468
+ if (!this._textLines || !this.__charBounds) {
27469
+ console.warn('🔤 BROWSER JUSTIFY: _textLines or __charBounds not ready');
27470
+ return;
27471
+ }
27472
+
27473
+ // Get space measurements from browser wrapping result
27474
+ const styleMap = this._styleMap;
27475
+ if (!styleMap || !styleMap.justifySpaceMeasurements) {
27476
+ console.warn('🔤 BROWSER JUSTIFY: No justify space measurements available');
27477
+ return;
27478
+ }
27479
+ const spaceWidths = styleMap.justifySpaceMeasurements;
27480
+ console.log('🔤 BROWSER JUSTIFY: Applying space measurements to __charBounds');
27481
+
27482
+ // Apply space widths to character bounds
27483
+ this._textLines.forEach((line, lineIndex) => {
27484
+ if (!this.__charBounds || !this.__charBounds[lineIndex] || !spaceWidths[lineIndex]) return;
27485
+ const lineBounds = this.__charBounds[lineIndex];
27486
+ const lineSpaceWidths = spaceWidths[lineIndex];
27487
+ let spaceIndex = 0;
27488
+ for (let charIndex = 0; charIndex < line.length; charIndex++) {
27489
+ if (/\s/.test(line[charIndex]) && spaceIndex < lineSpaceWidths.length) {
27490
+ const expandedWidth = lineSpaceWidths[spaceIndex];
27491
+ if (lineBounds[charIndex]) {
27492
+ const oldWidth = lineBounds[charIndex].width;
27493
+ lineBounds[charIndex].width = expandedWidth;
27494
+ console.log(`🔤 Line ${lineIndex} space ${spaceIndex}: ${oldWidth.toFixed(1)}px -> ${expandedWidth.toFixed(1)}px`);
27495
+ }
27496
+ spaceIndex++;
27497
+ }
27498
+ }
27499
+ });
27500
+ }
27501
+
27502
+ /**
27503
+ * Fallback to default Fabric wrapping
27504
+ * @private
27505
+ */
27506
+ _splitTextIntoLinesDefault(text) {
26618
27507
  const newText = super._splitTextIntoLines(text),
26619
27508
  graphemeLines = this._wrapText(newText.lines, this.width),
26620
27509
  lines = new Array(graphemeLines.length);
@@ -26649,7 +27538,7 @@ class Textbox extends IText {
26649
27538
  * @private
26650
27539
  */
26651
27540
  initializeEventListeners() {
26652
- var _this$canvas;
27541
+ var _this$canvas4;
26653
27542
  // Track which side is being used for resize to handle position compensation
26654
27543
  let resizeOrigin = null;
26655
27544
 
@@ -26680,7 +27569,7 @@ class Textbox extends IText {
26680
27569
  });
26681
27570
 
26682
27571
  // Also listen to canvas-level modified event as backup
26683
- (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.on('object:modified', e => {
27572
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.on('object:modified', e => {
26684
27573
  if (e.target === this) {
26685
27574
  const currentResizeOrigin = resizeOrigin; // Capture the value before reset
26686
27575
  setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
@@ -26724,7 +27613,7 @@ class Textbox extends IText {
26724
27613
  const safetyThreshold = 2; // px - very subtle trigger
26725
27614
 
26726
27615
  if (maxRequiredWidth > this.width - safetyThreshold) {
26727
- var _this$canvas2;
27616
+ var _this$canvas5;
26728
27617
  // Set width to exactly what's needed + minimal safety margin
26729
27618
  const newWidth = maxRequiredWidth + 1; // Add just 1px safety margin
26730
27619
 
@@ -26757,7 +27646,66 @@ class Textbox extends IText {
26757
27646
  this.__overlayEditor.refresh();
26758
27647
  }, 0);
26759
27648
  }
26760
- (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
27649
+ (_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
27650
+ }
27651
+ }
27652
+
27653
+ /**
27654
+ * Force complete textbox re-initialization (useful after JSON loading)
27655
+ * Overrides Text version with Textbox-specific logic
27656
+ */
27657
+ forceTextReinitialization() {
27658
+ console.log('🔄 Force reinitializing Textbox object');
27659
+
27660
+ // CRITICAL: Ensure textbox is marked as initialized
27661
+ this.initialized = true;
27662
+
27663
+ // Clear all caches and force dirty state
27664
+ this._clearCache();
27665
+ this.dirty = true;
27666
+ this.dynamicMinWidth = 0;
27667
+
27668
+ // Force isEditing false to ensure clean state
27669
+ this.isEditing = false;
27670
+ console.log(' → Set initialized=true, dirty=true, cleared caches');
27671
+
27672
+ // Re-initialize dimensions (this will handle justify properly)
27673
+ this.initDimensions();
27674
+
27675
+ // Double-check that justify was applied by checking space widths
27676
+ if (this.textAlign.includes('justify') && this.__charBounds) {
27677
+ setTimeout(() => {
27678
+ var _this$canvas6;
27679
+ // Verify justify was applied by checking if space widths vary
27680
+ let hasVariableSpaces = false;
27681
+ this.__charBounds.forEach((lineBounds, i) => {
27682
+ if (lineBounds && this._textLines && this._textLines[i]) {
27683
+ const spaces = lineBounds.filter((bound, j) => /\s/.test(this._textLines[i][j]));
27684
+ if (spaces.length > 1) {
27685
+ const firstSpaceWidth = spaces[0].width;
27686
+ hasVariableSpaces = spaces.some(space => Math.abs(space.width - firstSpaceWidth) > 0.1);
27687
+ }
27688
+ }
27689
+ });
27690
+ if (!hasVariableSpaces && this.__charBounds.length > 0) {
27691
+ console.warn(' ⚠️ Justify spaces still uniform - forcing enlargeSpaces again');
27692
+ if (this.enlargeSpaces) {
27693
+ this.enlargeSpaces();
27694
+ }
27695
+ } else {
27696
+ console.log(' ✅ Justify spaces properly expanded');
27697
+ }
27698
+
27699
+ // Ensure height is recalculated - use browser height if available
27700
+ if (this._usingBrowserWrapping && this._actualBrowserHeight) {
27701
+ this.height = this._actualBrowserHeight;
27702
+ console.log(`🔤 JUSTIFY: Preserved browser height: ${this.height}px`);
27703
+ } else {
27704
+ this.height = this.calcTextHeight();
27705
+ console.log(`🔧 JUSTIFY: Used calcTextHeight: ${this.height}px`);
27706
+ }
27707
+ (_this$canvas6 = this.canvas) === null || _this$canvas6 === void 0 || _this$canvas6.requestRenderAll();
27708
+ }, 10);
26761
27709
  }
26762
27710
  }
26763
27711