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