@nasser-sw/fabric 7.0.1-beta8 → 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 (138) 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 +915 -23
  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 +915 -23
  83. package/dist/index.mjs.map +1 -1
  84. package/dist/index.node.cjs +915 -23
  85. package/dist/index.node.cjs.map +1 -1
  86. package/dist/index.node.mjs +915 -23
  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/Text/Text.d.ts +19 -0
  91. package/dist/src/shapes/Text/Text.d.ts.map +1 -1
  92. package/dist/src/shapes/Text/Text.min.mjs +1 -1
  93. package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
  94. package/dist/src/shapes/Text/Text.mjs +238 -4
  95. package/dist/src/shapes/Text/Text.mjs.map +1 -1
  96. package/dist/src/shapes/Textbox.d.ts +38 -1
  97. package/dist/src/shapes/Textbox.d.ts.map +1 -1
  98. package/dist/src/shapes/Textbox.min.mjs +1 -1
  99. package/dist/src/shapes/Textbox.min.mjs.map +1 -1
  100. package/dist/src/shapes/Textbox.mjs +497 -15
  101. package/dist/src/shapes/Textbox.mjs.map +1 -1
  102. package/dist/src/text/examples/arabicTextExample.d.ts +60 -0
  103. package/dist/src/text/examples/arabicTextExample.d.ts.map +1 -0
  104. package/dist/src/text/measure.d.ts +9 -0
  105. package/dist/src/text/measure.d.ts.map +1 -1
  106. package/dist/src/text/measure.min.mjs +1 -1
  107. package/dist/src/text/measure.min.mjs.map +1 -1
  108. package/dist/src/text/measure.mjs +175 -4
  109. package/dist/src/text/measure.mjs.map +1 -1
  110. package/dist/src/text/overlayEditor.d.ts.map +1 -1
  111. package/dist/src/text/overlayEditor.min.mjs +1 -1
  112. package/dist/src/text/overlayEditor.min.mjs.map +1 -1
  113. package/dist/src/text/overlayEditor.mjs +7 -0
  114. package/dist/src/text/overlayEditor.mjs.map +1 -1
  115. package/dist/src/text/scriptUtils.d.ts +142 -0
  116. package/dist/src/text/scriptUtils.d.ts.map +1 -0
  117. package/dist/src/text/scriptUtils.min.mjs +2 -0
  118. package/dist/src/text/scriptUtils.min.mjs.map +1 -0
  119. package/dist/src/text/scriptUtils.mjs +212 -0
  120. package/dist/src/text/scriptUtils.mjs.map +1 -0
  121. package/dist-extensions/src/shapes/Text/Text.d.ts +19 -0
  122. package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
  123. package/dist-extensions/src/shapes/Textbox.d.ts +38 -1
  124. package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
  125. package/dist-extensions/src/text/measure.d.ts +9 -0
  126. package/dist-extensions/src/text/measure.d.ts.map +1 -1
  127. package/dist-extensions/src/text/overlayEditor.d.ts.map +1 -1
  128. package/dist-extensions/src/text/scriptUtils.d.ts +142 -0
  129. package/dist-extensions/src/text/scriptUtils.d.ts.map +1 -0
  130. package/fabric-test-editor.html +2401 -46
  131. package/fonts/STV Bold.ttf +0 -0
  132. package/fonts/STV Light.ttf +0 -0
  133. package/fonts/STV Regular.ttf +0 -0
  134. package/package.json +1 -1
  135. package/src/shapes/Text/Text.ts +238 -5
  136. package/src/shapes/Textbox.ts +521 -11
  137. package/src/text/measure.ts +200 -50
  138. package/src/text/overlayEditor.ts +7 -0
@@ -6,6 +6,27 @@
6
6
  <title>Fabric.js 7 Test Editor</title>
7
7
  <script src="./dist/index.js"></script>
8
8
  <style>
9
+ /* Custom Fonts */
10
+ @font-face {
11
+ font-family: 'STV Bold';
12
+ src: url('./fonts/STV Bold.ttf') format('truetype');
13
+ font-weight: bold;
14
+ font-style: normal;
15
+ }
16
+
17
+ @font-face {
18
+ font-family: 'STV Light';
19
+ src: url('./fonts/STV Light.ttf') format('truetype');
20
+ font-weight: 300;
21
+ font-style: normal;
22
+ }
23
+
24
+ @font-face {
25
+ font-family: 'STV Regular';
26
+ src: url('./fonts/STV Regular.ttf') format('truetype');
27
+ font-weight: normal;
28
+ font-style: normal;
29
+ }
9
30
  * {
10
31
  margin: 0;
11
32
  padding: 0;
@@ -252,6 +273,9 @@
252
273
  <option value="Courier New">Courier New</option>
253
274
  <option value="Helvetica">Helvetica</option>
254
275
  <option value="Verdana">Verdana</option>
276
+ <option value="STV Bold">STV Bold</option>
277
+ <option value="STV Light">STV Light</option>
278
+ <option value="STV Regular">STV Regular</option>
255
279
  </select>
256
280
  </div>
257
281
 
@@ -304,15 +328,21 @@
304
328
  <div class="slider-value" id="cornerRadiusValue">0px</div>
305
329
  </div>
306
330
  <button onclick="testCornerRadius()">Test All Corner Radius</button>
331
+ <button onclick="testCustomFontBounds()">
332
+ Test Custom Font Bounds
333
+ </button>
307
334
  </div>
308
335
  <div class="control-group">
309
336
  <h3>Line Properties</h3>
310
- <div style="margin-bottom: 10px;">
311
- <label for="roundedEndpoints" style="display: flex; align-items: center; cursor: pointer;">
337
+ <div style="margin-bottom: 10px">
338
+ <label
339
+ for="roundedEndpoints"
340
+ style="display: flex; align-items: center; cursor: pointer"
341
+ >
312
342
  <input
313
343
  type="checkbox"
314
344
  id="roundedEndpoints"
315
- style="margin-right: 8px;"
345
+ style="margin-right: 8px"
316
346
  />
317
347
  Rounded Endpoints (Line Caps)
318
348
  </label>
@@ -321,8 +351,12 @@
321
351
 
322
352
  <div class="control-group">
323
353
  <h3>Drawing Mode</h3>
324
- <button id="enableDrawing" onclick="enableDrawing()">Enable Drawing</button>
325
- <button id="disableDrawing" onclick="disableDrawing()">Disable Drawing</button>
354
+ <button id="enableDrawing" onclick="enableDrawing()">
355
+ Enable Drawing
356
+ </button>
357
+ <button id="disableDrawing" onclick="disableDrawing()">
358
+ Disable Drawing
359
+ </button>
326
360
  <div class="slider-container">
327
361
  <label for="brushWidth">Brush Width</label>
328
362
  <input
@@ -355,6 +389,17 @@
355
389
  <button onclick="changeFontFamily()">Change Font</button>
356
390
  </div>
357
391
 
392
+ <div class="control-group">
393
+ <h3>Object Alignment</h3>
394
+ <button onclick="centerObject()">Center</button>
395
+ <button onclick="centerObjectH()">Center Horizontally</button>
396
+ <button onclick="centerObjectV()">Center Vertically</button>
397
+ <button onclick="alignLeft()">Align Left</button>
398
+ <button onclick="alignRight()">Align Right</button>
399
+ <button onclick="alignTop()">Align Top</button>
400
+ <button onclick="alignBottom()">Align Bottom</button>
401
+ </div>
402
+
358
403
  <div class="control-group">
359
404
  <h3>Actions</h3>
360
405
  <button onclick="deleteSelected()" class="secondary">
@@ -362,6 +407,7 @@
362
407
  </button>
363
408
  <button onclick="clearCanvas()" class="secondary">Clear Canvas</button>
364
409
  <button onclick="exportJSON()" class="secondary">Export JSON</button>
410
+ <button onclick="importJSON()" class="secondary">Import JSON</button>
365
411
  </div>
366
412
 
367
413
  <div class="control-group">
@@ -382,6 +428,74 @@
382
428
  <button onclick="fitToWindow()">Fit to Window</button>
383
429
  </div>
384
430
 
431
+ <div class="control-group">
432
+ <h3>Debug Visualization</h3>
433
+ <button onclick="toggleDebugMode()">Toggle Debug Mode</button>
434
+ <button onclick="debugSelectedText()">Debug Selected Text</button>
435
+ <button onclick="testArabicFonts()">Test Arabic Fonts</button>
436
+ <button onclick="forceRefreshText()">Force Refresh Text</button>
437
+ <button onclick="testWidthConstraints()">Test Width Constraints</button>
438
+ <button onclick="testForcedNarrowText()">
439
+ Test Forced Narrow Text
440
+ </button>
441
+ <button onclick="testUnlimitedHeight()">Test Unlimited Height</button>
442
+ <button onclick="testWrappingConsistency()">
443
+ Test Wrapping Consistency
444
+ </button>
445
+ <div style="margin: 10px 0">
446
+ <label
447
+ for="debugShowBounds"
448
+ style="display: flex; align-items: center; cursor: pointer"
449
+ >
450
+ <input
451
+ type="checkbox"
452
+ id="debugShowBounds"
453
+ style="margin-right: 8px"
454
+ />
455
+ Show Bounding Boxes
456
+ </label>
457
+ </div>
458
+ <div style="margin: 10px 0">
459
+ <label
460
+ for="debugShowBaseline"
461
+ style="display: flex; align-items: center; cursor: pointer"
462
+ >
463
+ <input
464
+ type="checkbox"
465
+ id="debugShowBaseline"
466
+ style="margin-right: 8px"
467
+ />
468
+ Show Baseline
469
+ </label>
470
+ </div>
471
+ <div style="margin: 10px 0">
472
+ <label
473
+ for="debugShowMetrics"
474
+ style="display: flex; align-items: center; cursor: pointer"
475
+ >
476
+ <input
477
+ type="checkbox"
478
+ id="debugShowMetrics"
479
+ style="margin-right: 8px"
480
+ />
481
+ Show Metrics Info
482
+ </label>
483
+ </div>
484
+ <div style="margin: 10px 0">
485
+ <label
486
+ for="debugShowWrap"
487
+ style="display: flex; align-items: center; cursor: pointer"
488
+ >
489
+ <input
490
+ type="checkbox"
491
+ id="debugShowWrap"
492
+ style="margin-right: 8px"
493
+ />
494
+ Show Text Wrapping
495
+ </label>
496
+ </div>
497
+ </div>
498
+
385
499
  <div class="control-group">
386
500
  <h3>Canvas Info</h3>
387
501
  <div id="canvasInfo" style="font-size: 12px; color: #6b7280">
@@ -389,6 +503,17 @@
389
503
  Selected: None<br />
390
504
  Zoom: 100%
391
505
  </div>
506
+ <div
507
+ id="debugInfo"
508
+ style="
509
+ font-size: 11px;
510
+ color: #ef4444;
511
+ margin-top: 10px;
512
+ display: none;
513
+ "
514
+ >
515
+ <!-- Debug info will appear here -->
516
+ </div>
392
517
  </div>
393
518
  </div>
394
519
 
@@ -397,11 +522,32 @@
397
522
  </div>
398
523
 
399
524
  <script>
525
+ // Test that the new fabric.js measurement system works correctly
526
+ function testBoundingBoxAccuracy(text, fontFamily, fontSize) {
527
+ console.log('🧪 Testing bounding box accuracy:', {
528
+ text,
529
+ fontFamily,
530
+ fontSize,
531
+ });
532
+
533
+ // Check if font is available
534
+ const fontReady = document.fonts
535
+ ? document.fonts.check(`${fontSize}px ${fontFamily}`)
536
+ : true;
537
+ console.log('📝 Font ready:', fontReady);
538
+
539
+ return {
540
+ fontReady,
541
+ shouldWaitForFont: !fontReady,
542
+ };
543
+ }
544
+
400
545
  // Initialize Fabric.js canvas
401
546
  const canvas = new fabric.Canvas('canvas', {
402
547
  backgroundColor: 'white',
403
548
  selection: true,
404
549
  preserveObjectStacking: true,
550
+ skipOffscreen: false, // Disable offscreen culling to show tall textboxes fully
405
551
  });
406
552
 
407
553
  // Create workspace (like your editor) - removed 'clip' name to prevent clipping issues with line dragging
@@ -508,7 +654,100 @@
508
654
  return 'ltr'; // default
509
655
  }
510
656
 
511
- // Add text function (matching your editor)
657
+ // Helper function to create textbox with forced width override
658
+ function createTextboxWithForcedWidth(text, options, targetWidth) {
659
+ const textbox = new fabric.Textbox(text, options);
660
+
661
+ // Override the dynamic width constraints
662
+ textbox.minWidth = Math.min(targetWidth, 10); // Very small minimum
663
+ textbox.dynamicMinWidth = 0; // Reset dynamic constraint
664
+ textbox.width = targetWidth; // Set desired width
665
+
666
+ // Only force character-based wrapping for extremely narrow boxes (< 60px)
667
+ if (targetWidth < 60) {
668
+ textbox.splitByGrapheme = true;
669
+ }
670
+
671
+ // Recalculate with new constraints
672
+ textbox.initDimensions();
673
+
674
+ console.log(
675
+ `📏 Created textbox with forced width: ${targetWidth}px (dynamicMinWidth: ${textbox.dynamicMinWidth}px)`,
676
+ );
677
+
678
+ return textbox;
679
+ }
680
+
681
+ // Helper function to create textbox with unlimited height growth
682
+ function createTextboxWithUnlimitedHeight(text, options, targetWidth) {
683
+ const textbox = new fabric.Textbox(text, options);
684
+
685
+ // Override width constraints
686
+ textbox.minWidth = Math.min(targetWidth, 10);
687
+ textbox.dynamicMinWidth = 0;
688
+ textbox.width = targetWidth;
689
+
690
+ // Only force character-based wrapping for extremely narrow boxes (< 60px)
691
+ if (targetWidth < 60) {
692
+ textbox.splitByGrapheme = true;
693
+ }
694
+
695
+ // Override the _getAdvancedLayoutOptions to remove height constraint
696
+ const originalGetOptions = textbox._getAdvancedLayoutOptions;
697
+ textbox._getAdvancedLayoutOptions = function () {
698
+ const options = originalGetOptions.call(this);
699
+ // Remove height constraint to allow unlimited growth
700
+ delete options.height;
701
+
702
+ // Force consistent wrapping behavior
703
+ options.wrap = 'word'; // Ensure word wrapping
704
+ return options;
705
+ };
706
+
707
+ // Override text splitting to use consistent logic with debug
708
+ const originalSplitText = textbox._splitTextIntoLines;
709
+ textbox._splitTextIntoLines = function (text) {
710
+ console.log('🔧 TEXTBOX SPLIT: Using consistent wrapping logic');
711
+
712
+ // Force use of advanced layout if available
713
+ if (this.enableAdvancedLayout && this._getAdvancedLayoutOptions) {
714
+ const layoutOptions = this._getAdvancedLayoutOptions();
715
+
716
+ // Debug the layout options being used
717
+ console.log('🔧 Layout options:', {
718
+ width: layoutOptions.width,
719
+ wrap: layoutOptions.wrap,
720
+ direction: layoutOptions.direction,
721
+ fontFamily: layoutOptions.fontFamily,
722
+ });
723
+
724
+ // Let the advanced layout handle the wrapping
725
+ this._updateDimensionsWithAdvancedLayout();
726
+
727
+ // Return existing results if advanced layout worked
728
+ if (this._textLines && this._textLines.length > 0) {
729
+ return {
730
+ lines: this._textLines,
731
+ graphemeLines: this._textLines.map((line) => line.split('')),
732
+ };
733
+ }
734
+ }
735
+
736
+ // Fallback to original method
737
+ return originalSplitText.call(this, text);
738
+ };
739
+
740
+ // Recalculate with unlimited height
741
+ textbox.initDimensions();
742
+
743
+ console.log(
744
+ `📏 Created unlimited height textbox: width=${targetWidth}px, height=${textbox.height}px, lines=${textbox._textLines?.length || 0}`,
745
+ );
746
+
747
+ return textbox;
748
+ }
749
+
750
+ // Add text function with smart measurement system
512
751
  function addText() {
513
752
  const value =
514
753
  document.getElementById('textValue').value || 'Sample Text';
@@ -535,6 +774,21 @@
535
774
  console.log('🔄 Auto-detected RTL text, switched direction to RTL');
536
775
  }
537
776
 
777
+ // Test the improved bounding box system
778
+ const boundingBoxTest = testBoundingBoxAccuracy(
779
+ value,
780
+ fontFamily,
781
+ fontSize,
782
+ );
783
+
784
+ console.log('📐 Creating text with improved measurement system:', {
785
+ text: value,
786
+ font: fontFamily,
787
+ fontSize: fontSize,
788
+ direction: direction,
789
+ fontReady: boundingBoxTest.fontReady,
790
+ });
791
+
538
792
  const textbox = new fabric.Textbox(value, {
539
793
  left: 200,
540
794
  top: 200,
@@ -543,7 +797,6 @@
543
797
  fontFamily: fontFamily,
544
798
  fontWeight: fontWeight,
545
799
  fontStyle: fontStyle,
546
- lineHeight: 1.2,
547
800
 
548
801
  // RTL/LTR configuration (now with auto-detection)
549
802
  direction: direction,
@@ -552,15 +805,32 @@
552
805
  // ✅ Enable overlay editing (this handles most text editing)
553
806
  useOverlayEditing: true,
554
807
 
808
+ // ✅ Enable advanced layout with improved measurements
809
+ enableAdvancedLayout: false,
810
+
555
811
  // ✅ Keep these
556
812
  lockMovementX: false,
557
813
  lockMovementY: false,
814
+
815
+ // Better spacing for complex scripts
816
+ charSpacing: 0,
558
817
  });
559
818
 
560
819
  canvas.add(textbox);
561
820
  canvas.setActiveObject(textbox);
562
821
  canvas.renderAll();
563
822
  updateCanvasInfo();
823
+
824
+ // Log the dynamic width constraint info
825
+ console.log(`📊 Textbox width constraints:`, {
826
+ width: textbox.width,
827
+ minWidth: textbox.minWidth,
828
+ dynamicMinWidth: textbox.dynamicMinWidth,
829
+ canWrap:
830
+ textbox.dynamicMinWidth <= textbox.width
831
+ ? 'YES'
832
+ : 'NO (width will auto-expand)',
833
+ });
564
834
  }
565
835
 
566
836
  // Add shapes functions
@@ -675,7 +945,8 @@
675
945
  const strokeColor = document.getElementById('strokeColor').value;
676
946
  const strokeWidth =
677
947
  parseInt(document.getElementById('strokeWidth').value) || 2;
678
- const roundedEndpoints = document.getElementById('roundedEndpoints').checked;
948
+ const roundedEndpoints =
949
+ document.getElementById('roundedEndpoints').checked;
679
950
 
680
951
  // Create line with absolute coordinates (start and end points)
681
952
  const line = new fabric.Line([100, 100, 300, 200], {
@@ -692,7 +963,9 @@
692
963
  canvas.renderAll();
693
964
  updateCanvasInfo();
694
965
 
695
- console.log('✏️ Line added with draggable endpoints! Drag the blue circles to adjust.');
966
+ console.log(
967
+ '✏️ Line added with draggable endpoints! Drag the blue circles to adjust.',
968
+ );
696
969
  }
697
970
 
698
971
  // Text alignment function
@@ -804,6 +1077,335 @@
804
1077
  URL.revokeObjectURL(url);
805
1078
  }
806
1079
 
1080
+ function importJSON() {
1081
+ // Create file input if it doesn't exist
1082
+ let fileInput = document.getElementById('jsonFileInput');
1083
+ if (!fileInput) {
1084
+ fileInput = document.createElement('input');
1085
+ fileInput.type = 'file';
1086
+ fileInput.id = 'jsonFileInput';
1087
+ fileInput.accept = '.json';
1088
+ fileInput.style.display = 'none';
1089
+ document.body.appendChild(fileInput);
1090
+ }
1091
+
1092
+ // Set up file handler
1093
+ fileInput.onchange = function(event) {
1094
+ const file = event.target.files[0];
1095
+ if (!file) return;
1096
+
1097
+ const reader = new FileReader();
1098
+ reader.onload = function(e) {
1099
+ try {
1100
+ const jsonData = JSON.parse(e.target.result);
1101
+ console.log('Loading JSON:', jsonData);
1102
+
1103
+ // Clear canvas first
1104
+ canvas.clear();
1105
+
1106
+ // Load the JSON data
1107
+ canvas.loadFromJSON(jsonData).then(() => {
1108
+ console.log('✅ JSON loaded successfully');
1109
+ canvas.renderAll();
1110
+ updateCanvasInfo();
1111
+
1112
+ // Check for Arabic text with justify alignment and log info
1113
+ const textObjects = canvas.getObjects().filter(obj => obj.type === 'textbox' || obj.type === 'text');
1114
+ console.log(`🔍 Found ${textObjects.length} text objects after JSON load`);
1115
+
1116
+ textObjects.forEach((obj, index) => {
1117
+ console.log(`📝 ${obj.type} ${index + 1}: "${obj.text.substring(0, 50)}..." - Alignment: ${obj.textAlign}, Font: ${obj.fontFamily}, Advanced: ${obj.enableAdvancedLayout}`);
1118
+
1119
+ // Check if this is using STV font
1120
+ if (obj.fontFamily && obj.fontFamily.toLowerCase().includes('stv')) {
1121
+ console.log(` → 🔤 STV FONT DETECTED: Will ensure proper loading`);
1122
+
1123
+ // Check if STV font is available
1124
+ if (typeof document !== 'undefined' && 'fonts' in document) {
1125
+ const fontSpec = `${obj.fontSize}px ${obj.fontFamily}`;
1126
+ const isReady = document.fonts.check(fontSpec);
1127
+ console.log(` → STV Font status: ${isReady ? '✅ Ready' : '⏳ Loading...'}`);
1128
+
1129
+ if (!isReady) {
1130
+ console.log(` → Loading STV font: ${fontSpec}`);
1131
+ document.fonts.load(fontSpec).then(() => {
1132
+ console.log(` → ✅ STV font loaded successfully`);
1133
+ // Force rerender after font loads
1134
+ setTimeout(() => canvas.renderAll(), 100);
1135
+ }).catch(err => {
1136
+ console.warn(` → ⚠️ STV font loading failed:`, err);
1137
+ });
1138
+ }
1139
+ }
1140
+ }
1141
+
1142
+ // Show debug for ALL text objects, not just justify
1143
+ if (obj.textAlign && obj.textAlign.includes('justify')) {
1144
+ console.log(` → This has JUSTIFY alignment, will debug`);
1145
+ } else {
1146
+ console.log(` → This has NON-JUSTIFY alignment (${obj.textAlign}), will still debug for comparison`);
1147
+ }
1148
+
1149
+ // DEBUG ALL TEXT OBJECTS TO SEE WHAT'S HAPPENING
1150
+
1151
+ // Debug function to compare overlay vs fabric text
1152
+ const debugTextComparison = (obj, attempt) => {
1153
+ console.log(`\n🔍 DEBUG COMPARISON - Attempt ${attempt} for ${obj.type} ${index + 1}`);
1154
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
1155
+
1156
+ // Fabric object state
1157
+ console.log(`📊 FABRIC OBJECT STATE:`);
1158
+ console.log(` Text: "${obj.text}"`);
1159
+ console.log(` TextAlign: ${obj.textAlign}`);
1160
+ console.log(` FontFamily: ${obj.fontFamily}`);
1161
+ console.log(` FontSize: ${obj.fontSize}`);
1162
+ console.log(` Width: ${obj.width}`);
1163
+ console.log(` Height: ${obj.height}`);
1164
+ console.log(` Direction: ${obj.direction}`);
1165
+ console.log(` EnableAdvancedLayout: ${obj.enableAdvancedLayout}`);
1166
+ console.log(` Dirty: ${obj.dirty}`);
1167
+ console.log(` Initialized: ${obj.initialized}`);
1168
+
1169
+ // Text lines and bounds with detailed character analysis
1170
+ if (obj._textLines) {
1171
+ console.log(` _textLines count: ${obj._textLines.length}`);
1172
+ obj._textLines.forEach((line, i) => {
1173
+ const lineText = line.join('');
1174
+ console.log(` Line ${i}: [${line.join(', ')}] (${line.length} chars)`);
1175
+ console.log(` Line ${i} as string: "${lineText}"`);
1176
+
1177
+ // Show Unicode code points for analysis
1178
+ const codePoints = Array.from(lineText).map(char =>
1179
+ `${char}(U+${char.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')})`
1180
+ ).join(' ');
1181
+ console.log(` Line ${i} Unicode: ${codePoints}`);
1182
+ });
1183
+ } else {
1184
+ console.log(` _textLines: NOT SET`);
1185
+ }
1186
+
1187
+ // Compare with original text
1188
+ console.log(`\n📝 ORIGINAL TEXT ANALYSIS:`);
1189
+ console.log(` Original: "${obj.text}"`);
1190
+ const originalCodePoints = Array.from(obj.text).map(char =>
1191
+ `${char}(U+${char.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')})`
1192
+ ).join(' ');
1193
+ console.log(` Original Unicode: ${originalCodePoints.substring(0, 200)}...`);
1194
+
1195
+ if (obj.__charBounds) {
1196
+ console.log(` __charBounds count: ${obj.__charBounds.length} lines`);
1197
+ obj.__charBounds.forEach((lineBounds, i) => {
1198
+ if (lineBounds && lineBounds.length > 0) {
1199
+ const lineWidth = lineBounds[lineBounds.length - 1].left + lineBounds[lineBounds.length - 1].width;
1200
+ console.log(` Line ${i}: ${lineBounds.length} chars, total width: ${lineWidth.toFixed(2)}`);
1201
+
1202
+ // Show space character details
1203
+ const spaces = lineBounds.filter((bound, j) => obj._textLines[i] && obj._textLines[i][j] && /\s/.test(obj._textLines[i][j]));
1204
+ if (spaces.length > 0) {
1205
+ console.log(` Spaces: ${spaces.length} found, widths: [${spaces.map(s => s.width.toFixed(2)).join(', ')}]`);
1206
+ }
1207
+ } else {
1208
+ console.log(` Line ${i}: NO BOUNDS`);
1209
+ }
1210
+ });
1211
+ } else {
1212
+ console.log(` __charBounds: NOT SET`);
1213
+ }
1214
+
1215
+ // Font loading status
1216
+ const fontReady = obj._isFontReady ? obj._isFontReady() : 'unknown';
1217
+ console.log(` Font Ready: ${fontReady}`);
1218
+
1219
+ // Text measurements
1220
+ if (obj.calcTextWidth) {
1221
+ const calcWidth = obj.calcTextWidth();
1222
+ console.log(` Calculated Width: ${calcWidth}`);
1223
+ }
1224
+
1225
+ if (obj.calcTextHeight) {
1226
+ const calcHeight = obj.calcTextHeight();
1227
+ console.log(` Calculated Height: ${calcHeight}`);
1228
+ }
1229
+
1230
+ // Textbox-specific
1231
+ if (obj.type === 'textbox') {
1232
+ console.log(` DynamicMinWidth: ${obj.dynamicMinWidth || 'not set'}`);
1233
+ console.log(` MinWidth: ${obj.minWidth}`);
1234
+ }
1235
+
1236
+ console.log(`\n🎭 OVERLAY EDITOR COMPARISON:`);
1237
+
1238
+ // Create a temporary overlay to see what it would look like
1239
+ try {
1240
+ // Create temporary textarea to simulate overlay editor
1241
+ const tempTextarea = document.createElement('textarea');
1242
+ tempTextarea.value = obj.text;
1243
+ tempTextarea.style.position = 'absolute';
1244
+ tempTextarea.style.left = '-9999px';
1245
+ tempTextarea.style.fontSize = `${obj.fontSize}px`;
1246
+ tempTextarea.style.fontFamily = obj.fontFamily;
1247
+ tempTextarea.style.fontWeight = obj.fontWeight || 'normal';
1248
+ tempTextarea.style.fontStyle = obj.fontStyle || 'normal';
1249
+ tempTextarea.style.lineHeight = String(obj.lineHeight || 1.16);
1250
+ tempTextarea.style.width = `${obj.width}px`;
1251
+ tempTextarea.style.direction = obj.direction || 'ltr';
1252
+ tempTextarea.style.textAlign = obj.textAlign.includes('justify') ? 'justify' : obj.textAlign;
1253
+ tempTextarea.style.whiteSpace = 'pre-wrap';
1254
+ tempTextarea.style.wordBreak = 'normal';
1255
+ tempTextarea.style.overflowWrap = 'break-word';
1256
+
1257
+ // Add to DOM temporarily
1258
+ document.body.appendChild(tempTextarea);
1259
+
1260
+ // Get computed styles
1261
+ const computed = window.getComputedStyle(tempTextarea);
1262
+ console.log(` Overlay fontSize: ${computed.fontSize}`);
1263
+ console.log(` Overlay fontFamily: ${computed.fontFamily}`);
1264
+ console.log(` Overlay width: ${computed.width}`);
1265
+ console.log(` Overlay textAlign: ${computed.textAlign}`);
1266
+ console.log(` Overlay direction: ${computed.direction}`);
1267
+ console.log(` Overlay lineHeight: ${computed.lineHeight}`);
1268
+ console.log(` Overlay whiteSpace: ${computed.whiteSpace}`);
1269
+
1270
+ // CRITICAL: Check how overlay editor handles the text
1271
+ console.log(`\n🎭 OVERLAY TEXT ORDERING ANALYSIS:`);
1272
+ console.log(` Overlay value: "${tempTextarea.value}"`);
1273
+
1274
+ // Get first 50 characters for detailed comparison
1275
+ const first50Fabric = obj.text.substring(0, 50);
1276
+ const first50Overlay = tempTextarea.value.substring(0, 50);
1277
+
1278
+ console.log(`\n📊 CHARACTER-BY-CHARACTER COMPARISON (First 50):`);
1279
+ console.log(` Fabric text: "${first50Fabric}"`);
1280
+ console.log(` Overlay text: "${first50Overlay}"`);
1281
+ console.log(` Match: ${first50Fabric === first50Overlay ? '✅ IDENTICAL' : '❌ DIFFERENT'}`);
1282
+
1283
+ if (first50Fabric !== first50Overlay) {
1284
+ console.log(`\n🔍 DETAILED CHAR DIFFERENCE ANALYSIS:`);
1285
+ const maxLen = Math.max(first50Fabric.length, first50Overlay.length);
1286
+ for (let i = 0; i < Math.min(20, maxLen); i++) {
1287
+ const fChar = first50Fabric[i] || '(missing)';
1288
+ const oChar = first50Overlay[i] || '(missing)';
1289
+ const fCode = fChar !== '(missing)' ? `U+${fChar.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')}` : '';
1290
+ const oCode = oChar !== '(missing)' ? `U+${oChar.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')}` : '';
1291
+ const match = fChar === oChar ? '✅' : '❌';
1292
+ console.log(` [${i.toString().padStart(2)}] ${match} F:"${fChar}"${fCode} vs O:"${oChar}"${oCode}`);
1293
+ }
1294
+ }
1295
+
1296
+ // Compare with Fabric's internal _textLines
1297
+ if (obj._textLines && obj._textLines.length > 0) {
1298
+ const fabricLine0 = obj._textLines[0].join('');
1299
+ const first50FabricLine0 = fabricLine0.substring(0, 50);
1300
+
1301
+ console.log(`\n🔤 FABRIC _textLines[0] COMPARISON:`);
1302
+ console.log(` Original : "${first50Fabric}"`);
1303
+ console.log(` _textLines[0]: "${first50FabricLine0}"`);
1304
+ console.log(` Lines Match: ${first50Fabric === first50FabricLine0 ? '✅ IDENTICAL' : '❌ DIFFERENT'}`);
1305
+
1306
+ if (first50Fabric !== first50FabricLine0) {
1307
+ console.log(` ⚠️ FABRIC INTERNAL MISMATCH: _textLines[0] != original text`);
1308
+ }
1309
+ }
1310
+
1311
+ // Test browser's BiDi handling
1312
+ console.log(`\n🧭 BROWSER BiDi DIRECTION TEST:`);
1313
+ tempTextarea.focus();
1314
+ tempTextarea.setSelectionRange(0, 10);
1315
+ const selectedText = tempTextarea.value.substring(0, 10);
1316
+ console.log(` Selected first 10: "${selectedText}"`);
1317
+
1318
+ // Test with different selection ranges to see ordering
1319
+ const selections = [
1320
+ { start: 0, end: 5, name: 'chars 0-5' },
1321
+ { start: 5, end: 10, name: 'chars 5-10' },
1322
+ { start: 0, end: 15, name: 'chars 0-15' }
1323
+ ];
1324
+
1325
+ selections.forEach(sel => {
1326
+ if (tempTextarea.value.length >= sel.end) {
1327
+ tempTextarea.setSelectionRange(sel.start, sel.end);
1328
+ const selText = tempTextarea.value.substring(sel.start, sel.end);
1329
+ console.log(` ${sel.name}: "${selText}"`);
1330
+ }
1331
+ });
1332
+
1333
+ // Measure overlay dimensions
1334
+ tempTextarea.style.height = '1px';
1335
+ const overlayScrollHeight = tempTextarea.scrollHeight;
1336
+ const overlayScrollWidth = tempTextarea.scrollWidth;
1337
+ console.log(` Overlay scrollHeight: ${overlayScrollHeight}`);
1338
+ console.log(` Overlay scrollWidth: ${overlayScrollWidth}`);
1339
+
1340
+ // Remove from DOM
1341
+ document.body.removeChild(tempTextarea);
1342
+
1343
+ console.log(`\n⚖️ COMPARISON RESULTS:`);
1344
+ console.log(` Width difference: Fabric(${obj.width}) vs Overlay(${overlayScrollWidth}) = ${(obj.width - overlayScrollWidth).toFixed(2)}px`);
1345
+ console.log(` Height difference: Fabric(${obj.height}) vs Overlay(${overlayScrollHeight}) = ${(obj.height - overlayScrollHeight).toFixed(2)}px`);
1346
+
1347
+ const widthDiffPercent = Math.abs((obj.width - overlayScrollWidth) / obj.width * 100);
1348
+ const heightDiffPercent = Math.abs((obj.height - overlayScrollHeight) / obj.height * 100);
1349
+
1350
+ if (widthDiffPercent > 5) {
1351
+ console.warn(` ⚠️ SIGNIFICANT WIDTH DIFFERENCE: ${widthDiffPercent.toFixed(1)}%`);
1352
+ }
1353
+ if (heightDiffPercent > 5) {
1354
+ console.warn(` ⚠️ SIGNIFICANT HEIGHT DIFFERENCE: ${heightDiffPercent.toFixed(1)}%`);
1355
+ }
1356
+
1357
+ } catch (error) {
1358
+ console.error(` ❌ Overlay comparison failed:`, error);
1359
+ }
1360
+
1361
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
1362
+ };
1363
+
1364
+ // Debug comparison and optional additional reinitialization if needed
1365
+ const debugAndCheck = (attempt) => {
1366
+ console.log(`🔍 Attempt ${attempt}: Checking ${obj.type} ${index + 1}`);
1367
+
1368
+ // Run debug comparison
1369
+ debugTextComparison(obj, attempt);
1370
+
1371
+ // Additional check for justify alignment - ensure enlargeSpaces runs
1372
+ if (obj.textAlign && obj.textAlign.includes('justify')) {
1373
+ console.log(` → Checking justify alignment for ${obj.type}`);
1374
+
1375
+ setTimeout(() => {
1376
+ if (obj.enlargeSpaces && obj.__charBounds && obj.__charBounds.length > 0) {
1377
+ console.log(` → Running enlargeSpaces() for justify alignment`);
1378
+ obj.enlargeSpaces();
1379
+ canvas.renderAll();
1380
+ } else {
1381
+ console.log(` → enlargeSpaces not available or __charBounds not ready`);
1382
+ }
1383
+ }, 50);
1384
+ }
1385
+ };
1386
+
1387
+ // Run debug check - the core fix is now in fromObject method
1388
+ setTimeout(() => debugAndCheck(1), 100);
1389
+ });
1390
+ }).catch(error => {
1391
+ console.error('❌ Failed to load JSON:', error);
1392
+ alert('Failed to load JSON file: ' + error.message);
1393
+ });
1394
+ } catch (error) {
1395
+ console.error('❌ Invalid JSON file:', error);
1396
+ alert('Invalid JSON file: ' + error.message);
1397
+ }
1398
+ };
1399
+
1400
+ reader.readAsText(file);
1401
+ // Reset file input for next use
1402
+ event.target.value = '';
1403
+ };
1404
+
1405
+ // Trigger file selection
1406
+ fileInput.click();
1407
+ }
1408
+
807
1409
  // Update canvas info
808
1410
  function updateCanvasInfo() {
809
1411
  const objects = canvas
@@ -851,7 +1453,7 @@
851
1453
  const canvasZoomSlider = document.getElementById('canvasZoom');
852
1454
  const canvasZoomValue = document.getElementById('canvasZoomValue');
853
1455
 
854
- canvasZoomSlider.addEventListener('input', function() {
1456
+ canvasZoomSlider.addEventListener('input', function () {
855
1457
  const zoomLevel = parseFloat(this.value);
856
1458
  canvas.setZoom(zoomLevel);
857
1459
  canvasZoomValue.textContent = Math.round(zoomLevel * 100) + '%';
@@ -872,38 +1474,41 @@
872
1474
  const container = document.querySelector('.canvas-container');
873
1475
  const containerWidth = container.clientWidth - 40; // padding
874
1476
  const containerHeight = container.clientHeight - 40; // padding
875
-
1477
+
876
1478
  const canvasWidth = canvas.getWidth();
877
1479
  const canvasHeight = canvas.getHeight();
878
-
1480
+
879
1481
  const scaleX = containerWidth / canvasWidth;
880
1482
  const scaleY = containerHeight / canvasHeight;
881
1483
  const scale = Math.min(scaleX, scaleY, 3); // max zoom 300%
882
-
1484
+
883
1485
  canvas.setZoom(scale);
884
1486
  canvasZoomSlider.value = scale;
885
1487
  canvasZoomValue.textContent = Math.round(scale * 100) + '%';
886
1488
  canvas.renderAll();
887
1489
  updateCanvasInfo();
888
- console.log('🔍 Canvas zoom fit to window:', Math.round(scale * 100) + '%');
1490
+ console.log(
1491
+ '🔍 Canvas zoom fit to window:',
1492
+ Math.round(scale * 100) + '%',
1493
+ );
889
1494
  }
890
1495
 
891
1496
  // Mouse wheel zoom support
892
- canvas.on('mouse:wheel', function(opt) {
1497
+ canvas.on('mouse:wheel', function (opt) {
893
1498
  const delta = opt.e.deltaY;
894
1499
  let zoom = canvas.getZoom();
895
1500
  zoom *= 0.999 ** delta;
896
-
1501
+
897
1502
  // Clamp zoom between 10% and 300%
898
1503
  zoom = Math.max(0.1, Math.min(3, zoom));
899
-
1504
+
900
1505
  canvas.setZoom(zoom);
901
1506
  canvasZoomSlider.value = zoom;
902
1507
  canvasZoomValue.textContent = Math.round(zoom * 100) + '%';
903
-
1508
+
904
1509
  opt.e.preventDefault();
905
1510
  opt.e.stopPropagation();
906
-
1511
+
907
1512
  updateCanvasInfo();
908
1513
  console.log('🔍 Mouse wheel zoom:', Math.round(zoom * 100) + '%');
909
1514
  });
@@ -942,19 +1547,23 @@
942
1547
  // Drawing mode functions
943
1548
  function enableDrawing() {
944
1549
  const strokeColor = document.getElementById('strokeColor').value;
945
- const brushWidth = parseInt(document.getElementById('brushWidth').value) || 5;
946
-
947
- console.log('🎨 Enabling drawing mode with:', { strokeColor, brushWidth });
948
-
1550
+ const brushWidth =
1551
+ parseInt(document.getElementById('brushWidth').value) || 5;
1552
+
1553
+ console.log('🎨 Enabling drawing mode with:', {
1554
+ strokeColor,
1555
+ brushWidth,
1556
+ });
1557
+
949
1558
  // Ensure brush is properly initialized
950
1559
  if (!canvas.freeDrawingBrush) {
951
1560
  canvas.freeDrawingBrush = new fabric.PencilBrush(canvas);
952
1561
  }
953
-
1562
+
954
1563
  canvas.freeDrawingBrush.width = brushWidth;
955
1564
  canvas.freeDrawingBrush.color = strokeColor;
956
1565
  canvas.isDrawingMode = true;
957
-
1566
+
958
1567
  console.log('🎨 Drawing mode enabled');
959
1568
  updateCanvasInfo();
960
1569
  }
@@ -968,20 +1577,28 @@
968
1577
  // Brush width slider functionality
969
1578
  const brushWidthSlider = document.getElementById('brushWidth');
970
1579
  const brushWidthValue = document.getElementById('brushWidthValue');
971
-
1580
+
972
1581
  brushWidthSlider.addEventListener('input', function () {
973
1582
  const value = parseInt(this.value);
974
1583
  brushWidthValue.textContent = value + 'px';
975
-
976
- console.log('🎨 Brush width changed to:', value, 'Canvas objects before:', canvas.getObjects().length);
977
-
1584
+
1585
+ console.log(
1586
+ '🎨 Brush width changed to:',
1587
+ value,
1588
+ 'Canvas objects before:',
1589
+ canvas.getObjects().length,
1590
+ );
1591
+
978
1592
  // Update brush width in real-time
979
1593
  if (canvas.freeDrawingBrush) {
980
1594
  canvas.freeDrawingBrush.width = value;
981
1595
  console.log('🎨 Set brush width to:', value);
982
1596
  }
983
-
984
- console.log('🎨 Canvas objects after setting brush width:', canvas.getObjects().length);
1597
+
1598
+ console.log(
1599
+ '🎨 Canvas objects after setting brush width:',
1600
+ canvas.getObjects().length,
1601
+ );
985
1602
  });
986
1603
 
987
1604
  // Corner radius slider functionality
@@ -996,7 +1613,11 @@
996
1613
  const activeObjects = canvas.getActiveObjects();
997
1614
  if (activeObjects.length > 0) {
998
1615
  activeObjects.forEach((obj) => {
999
- if (obj.type === 'triangle' || obj.type === 'polygon' || obj.type === 'polyline') {
1616
+ if (
1617
+ obj.type === 'triangle' ||
1618
+ obj.type === 'polygon' ||
1619
+ obj.type === 'polyline'
1620
+ ) {
1000
1621
  obj.set('cornerRadius', value);
1001
1622
  } else if (obj.type === 'rect') {
1002
1623
  // For rectangles, use rx and ry properties
@@ -1141,48 +1762,1782 @@
1141
1762
  canvas.add(rect, triangle, hexagon, pentagon, star, arrow, line);
1142
1763
 
1143
1764
  // Add text label
1144
- const label = new fabric.Textbox('Corner Radius Demo - All shapes support rounded corners like Canva!', {
1145
- left: 150,
1146
- top: 400,
1147
- fontSize: 16,
1148
- fontFamily: 'Arial',
1149
- fill: '#374151',
1150
- textAlign: 'center',
1151
- width: 400,
1152
- });
1765
+ const label = new fabric.Textbox(
1766
+ 'Corner Radius Demo - All shapes support rounded corners like Canva!',
1767
+ {
1768
+ left: 150,
1769
+ top: 400,
1770
+ fontSize: 16,
1771
+ fontFamily: 'Arial',
1772
+ fill: '#374151',
1773
+ textAlign: 'center',
1774
+ width: 400,
1775
+ },
1776
+ );
1153
1777
 
1154
1778
  canvas.add(label);
1155
1779
  canvas.renderAll();
1156
1780
  updateCanvasInfo();
1157
1781
 
1158
- console.log('🎨 Corner radius demo created! All shapes now support corner radius like Canva.');
1782
+ console.log(
1783
+ '🎨 Corner radius demo created! All shapes now support corner radius like Canva.',
1784
+ );
1785
+ }
1786
+
1787
+ // Test custom font bounding box improvements
1788
+ function testCustomFontBounds() {
1789
+ clearCanvas();
1790
+
1791
+ console.log('🧪 Testing custom font bounding box improvements...');
1792
+
1793
+ // Test problematic characters that often exceed bounds
1794
+ const testTexts = [
1795
+ { text: 'ÄÇĞÜİŞ', desc: 'Turkish with diacritics', size: 48 },
1796
+ { text: 'jgypqÄÇ', desc: 'Mixed ascenders/descenders', size: 32 },
1797
+ { text: 'Typography', desc: 'Standard text', size: 24 },
1798
+ { text: 'قال الله تعالى', desc: 'Arabic text', size: 28 },
1799
+ { text: '🎨✨💫', desc: 'Emoji test', size: 32 },
1800
+ ];
1801
+
1802
+ const fonts = ['Arial', 'STV Bold', 'STV Light', 'STV Regular'];
1803
+ let yPos = 120;
1804
+
1805
+ fonts.forEach((font, fontIndex) => {
1806
+ console.log(`🔤 Testing font: ${font}`);
1807
+
1808
+ testTexts.forEach((test, testIndex) => {
1809
+ const xPos = 150 + testIndex * 140;
1810
+ const currentYPos = yPos + fontIndex * 100;
1811
+
1812
+ // Test font readiness
1813
+ const fontTest = testBoundingBoxAccuracy(
1814
+ test.text,
1815
+ font,
1816
+ test.size,
1817
+ );
1818
+
1819
+ // Create text with improved measurement system
1820
+ const textbox = new fabric.Textbox(test.text, {
1821
+ left: xPos,
1822
+ top: currentYPos,
1823
+ fontSize: test.size,
1824
+ fontFamily: font,
1825
+ fill: fontIndex % 2 === 0 ? '#000000' : '#0066cc',
1826
+ // enableAdvancedLayout: true, // Disabled to keep editing working
1827
+ // Add slight background to visualize bounds
1828
+ backgroundColor: 'rgba(255, 255, 0, 0.1)',
1829
+ borderColor: fontTest.fontReady ? 'green' : 'red',
1830
+ borderOpacityWhenMoving: 0.8,
1831
+ width: 120,
1832
+ editable: true,
1833
+ selectable: true,
1834
+ });
1835
+
1836
+ canvas.add(textbox);
1837
+
1838
+ // Add label with more detailed font info
1839
+ const label = new fabric.Text(
1840
+ `${font}\n${test.desc}\nReady: ${fontTest.fontReady ? '✅' : '❌'}`,
1841
+ {
1842
+ left: xPos,
1843
+ top: currentYPos + test.size + 10,
1844
+ fontSize: 10,
1845
+ fontFamily: 'Arial',
1846
+ fill: fontTest.fontReady ? '#22c55e' : '#ef4444',
1847
+ textAlign: 'center',
1848
+ originX: 'center',
1849
+ },
1850
+ );
1851
+
1852
+ canvas.add(label);
1853
+
1854
+ console.log(
1855
+ `✅ Added test: ${test.desc} with ${font} (ready: ${fontTest.fontReady})`,
1856
+ );
1857
+ });
1858
+ });
1859
+
1860
+ // Add main title
1861
+ const title = new fabric.Text(
1862
+ 'Custom Font Bounding Box Test\n(Yellow background shows calculated bounds)',
1863
+ {
1864
+ left: 400,
1865
+ top: 50,
1866
+ fontSize: 16,
1867
+ fontFamily: 'Arial',
1868
+ fill: '#333',
1869
+ textAlign: 'center',
1870
+ originX: 'center',
1871
+ },
1872
+ );
1873
+
1874
+ canvas.add(title);
1875
+ canvas.renderAll();
1876
+
1877
+ console.log(
1878
+ '🧪 Custom font bounds test complete! Check if characters stay within yellow bounds.',
1879
+ );
1880
+ updateCanvasInfo();
1881
+ }
1882
+
1883
+ // Font family dropdown real-time update with smart measurement
1884
+ const fontFamilyDropdown = document.getElementById('fontFamily');
1885
+ fontFamilyDropdown.addEventListener('change', function () {
1886
+ const selectedFamily = this.value;
1887
+ const activeObjects = canvas.getActiveObjects();
1888
+
1889
+ if (activeObjects.length > 0) {
1890
+ activeObjects.forEach((obj) => {
1891
+ if (
1892
+ obj.type === 'textbox' ||
1893
+ obj.type === 'text' ||
1894
+ obj.type === 'i-text'
1895
+ ) {
1896
+ // Get current text content and properties
1897
+ const currentText = obj.text || '';
1898
+ const currentFontSize = obj.fontSize || 16;
1899
+ const currentDirection = obj.direction || 'ltr';
1900
+
1901
+ // Test font readiness for better bounding box calculation
1902
+ const fontTest = testBoundingBoxAccuracy(
1903
+ currentText,
1904
+ selectedFamily,
1905
+ currentFontSize,
1906
+ );
1907
+
1908
+ console.log('🔤 Font change with improved measurement:', {
1909
+ font: selectedFamily,
1910
+ text: currentText.substring(0, 20) + '...',
1911
+ fontReady: fontTest.fontReady,
1912
+ });
1913
+
1914
+ // Let fabric.js handle the measurement improvements automatically
1915
+ obj.set({
1916
+ fontFamily: selectedFamily,
1917
+ // enableAdvancedLayout: true // Disabled to keep editing working
1918
+ });
1919
+ }
1920
+ });
1921
+ canvas.renderAll();
1922
+ console.log('🔤 Font family changed to:', selectedFamily);
1923
+ updateCanvasInfo();
1924
+ }
1925
+ });
1926
+
1927
+ // Font loading listener to re-render when fonts become available
1928
+ if (document.fonts) {
1929
+ document.fonts.addEventListener('loadingdone', function (event) {
1930
+ console.log('🔤 Font loading completed, re-rendering canvas');
1931
+
1932
+ // Re-render all text objects to use newly loaded fonts
1933
+ const objects = canvas.getObjects();
1934
+ let textObjectsFound = 0;
1935
+
1936
+ objects.forEach((obj) => {
1937
+ if (
1938
+ obj.type === 'textbox' ||
1939
+ obj.type === 'text' ||
1940
+ obj.type === 'i-text'
1941
+ ) {
1942
+ textObjectsFound++;
1943
+
1944
+ try {
1945
+ // Force recalculation of text dimensions and measurements
1946
+ obj._clearCache();
1947
+ obj.dirty = true;
1948
+
1949
+ // Safer approach - just force a re-render with new measurements
1950
+ if (obj.type === 'textbox') {
1951
+ // Force the textbox to recalculate its bounds
1952
+ obj.initialized = false;
1953
+ obj.initDimensions();
1954
+ } else {
1955
+ // For regular text, force dimension recalculation
1956
+ obj._dimensionsAffectingProps =
1957
+ obj._dimensionsAffectingProps || {};
1958
+ obj._fontSizeFraction = undefined; // Force recalc
1959
+ }
1960
+
1961
+ // Force height and width recalculation if methods exist
1962
+ if (typeof obj.calcTextHeight === 'function') {
1963
+ obj.calcTextHeight();
1964
+ }
1965
+ if (typeof obj.calcTextWidth === 'function') {
1966
+ obj.calcTextWidth();
1967
+ }
1968
+
1969
+ console.log(
1970
+ `🔤 Updated ${obj.type} with font ${obj.fontFamily}`,
1971
+ );
1972
+ } catch (error) {
1973
+ console.error('🚨 Error updating text object:', error);
1974
+ console.log('🔤 Object details:', {
1975
+ type: obj.type,
1976
+ fontFamily: obj.fontFamily,
1977
+ text: (obj.text || '').substring(0, 20),
1978
+ });
1979
+ }
1980
+ }
1981
+ });
1982
+
1983
+ if (textObjectsFound > 0) {
1984
+ canvas.renderAll();
1985
+ console.log(
1986
+ `🔤 Re-rendered ${textObjectsFound} text objects after font loading`,
1987
+ );
1988
+ }
1989
+ });
1159
1990
  }
1160
1991
 
1161
1992
  // Rounded endpoints checkbox functionality
1162
- const roundedEndpointsCheckbox = document.getElementById('roundedEndpoints');
1993
+ const roundedEndpointsCheckbox =
1994
+ document.getElementById('roundedEndpoints');
1163
1995
  roundedEndpointsCheckbox.addEventListener('change', function () {
1164
1996
  const isChecked = this.checked;
1165
1997
  console.log('🔧 Rounded endpoints checkbox changed:', isChecked);
1166
1998
  // Update strokeLineCap of selected line object(s) in real-time using Fabric.js built-in property
1167
1999
  const activeObjects = canvas.getActiveObjects();
1168
- console.log('🔧 Active objects:', activeObjects.length, activeObjects.map(o => o.type));
2000
+ console.log(
2001
+ '🔧 Active objects:',
2002
+ activeObjects.length,
2003
+ activeObjects.map((o) => o.type),
2004
+ );
1169
2005
  if (activeObjects.length > 0) {
1170
2006
  activeObjects.forEach((obj) => {
1171
2007
  if (obj.type === 'line') {
1172
2008
  const lineCap = isChecked ? 'round' : 'butt';
1173
2009
  console.log('🔧 Setting strokeLineCap on line:', lineCap);
1174
2010
  obj.set('strokeLineCap', lineCap);
1175
- console.log('🔧 Line strokeLineCap after set:', obj.strokeLineCap);
2011
+ console.log(
2012
+ '🔧 Line strokeLineCap after set:',
2013
+ obj.strokeLineCap,
2014
+ );
1176
2015
  }
1177
2016
  });
1178
2017
  canvas.renderAll();
1179
2018
  }
1180
2019
  });
1181
2020
 
2021
+ // Object alignment functions
2022
+ function centerObject() {
2023
+ const activeObject = canvas.getActiveObject();
2024
+ if (activeObject && activeObject.name !== 'workspace') {
2025
+ canvas.centerObject(activeObject);
2026
+ activeObject.setCoords();
2027
+ canvas.renderAll();
2028
+ console.log('📍 Object centered');
2029
+ updateCanvasInfo();
2030
+ }
2031
+ }
2032
+
2033
+ function centerObjectH() {
2034
+ const activeObject = canvas.getActiveObject();
2035
+ if (activeObject && activeObject.name !== 'workspace') {
2036
+ canvas.centerObjectH(activeObject);
2037
+ activeObject.setCoords();
2038
+ canvas.renderAll();
2039
+ console.log('📍 Object centered horizontally');
2040
+ updateCanvasInfo();
2041
+ }
2042
+ }
2043
+
2044
+ function centerObjectV() {
2045
+ const activeObject = canvas.getActiveObject();
2046
+ if (activeObject && activeObject.name !== 'workspace') {
2047
+ canvas.centerObjectV(activeObject);
2048
+ activeObject.setCoords();
2049
+ canvas.renderAll();
2050
+ console.log('📍 Object centered vertically');
2051
+ updateCanvasInfo();
2052
+ }
2053
+ }
2054
+
2055
+ function alignLeft() {
2056
+ const activeObject = canvas.getActiveObject();
2057
+ if (activeObject && activeObject.name !== 'workspace') {
2058
+ activeObject.set({
2059
+ left: 0,
2060
+ originX: 'left',
2061
+ });
2062
+ activeObject.setCoords();
2063
+ canvas.renderAll();
2064
+ console.log('📍 Object aligned left');
2065
+ updateCanvasInfo();
2066
+ }
2067
+ }
2068
+
2069
+ function alignRight() {
2070
+ const activeObject = canvas.getActiveObject();
2071
+ if (activeObject && activeObject.name !== 'workspace') {
2072
+ activeObject.set({
2073
+ left: canvas.width,
2074
+ originX: 'right',
2075
+ });
2076
+ activeObject.setCoords();
2077
+ canvas.renderAll();
2078
+ console.log('📍 Object aligned right');
2079
+ updateCanvasInfo();
2080
+ }
2081
+ }
2082
+
2083
+ function alignTop() {
2084
+ const activeObject = canvas.getActiveObject();
2085
+ if (activeObject && activeObject.name !== 'workspace') {
2086
+ activeObject.set({
2087
+ top: 0,
2088
+ originY: 'top',
2089
+ });
2090
+ activeObject.setCoords();
2091
+ canvas.renderAll();
2092
+ console.log('📍 Object aligned top');
2093
+ updateCanvasInfo();
2094
+ }
2095
+ }
2096
+
2097
+ function alignBottom() {
2098
+ const activeObject = canvas.getActiveObject();
2099
+ if (activeObject && activeObject.name !== 'workspace') {
2100
+ activeObject.set({
2101
+ top: canvas.height,
2102
+ originY: 'bottom',
2103
+ });
2104
+ activeObject.setCoords();
2105
+ canvas.renderAll();
2106
+ console.log('📍 Object aligned bottom');
2107
+ updateCanvasInfo();
2108
+ }
2109
+ }
2110
+
2111
+ // Debug visualization variables
2112
+ let debugMode = false;
2113
+ let debugOverlay = null;
2114
+
2115
+ // Debug helper functions (based on our measurement improvements)
2116
+ function getBestMeasurementCharacter(ctx, fontFamily) {
2117
+ const latinChar = 'M';
2118
+ const arabicChar = 'م';
2119
+
2120
+ if (
2121
+ /arabic|amiri|stv|noto.*arabic|cairo|scheherazade/i.test(fontFamily)
2122
+ ) {
2123
+ return arabicChar;
2124
+ }
2125
+
2126
+ const originalFont = ctx.font;
2127
+ ctx.font = ctx.font.replace(/[^,]+$/, 'Arial, sans-serif');
2128
+ const fallbackMetrics = ctx.measureText(latinChar);
2129
+
2130
+ ctx.font = originalFont;
2131
+ const actualMetrics = ctx.measureText(latinChar);
2132
+
2133
+ const widthDiff = Math.abs(actualMetrics.width - fallbackMetrics.width);
2134
+ const hasLatinSupport =
2135
+ widthDiff > Math.max(1.0, fallbackMetrics.width * 0.05);
2136
+
2137
+ return hasLatinSupport ? latinChar : arabicChar;
2138
+ }
2139
+
2140
+ function getDebugTextMetrics(textObj) {
2141
+ // Create a temporary canvas context for measurements
2142
+ const tempCanvas = document.createElement('canvas');
2143
+ const ctx = tempCanvas.getContext('2d');
2144
+
2145
+ // Set font properties
2146
+ const fontSize = textObj.fontSize || 16;
2147
+ const fontFamily = textObj.fontFamily || 'Arial';
2148
+ const fontWeight = textObj.fontWeight || 'normal';
2149
+ const fontStyle = textObj.fontStyle || 'normal';
2150
+
2151
+ ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
2152
+ ctx.textBaseline = 'alphabetic';
2153
+
2154
+ const text = textObj.text || '';
2155
+ const textMetrics = ctx.measureText(text);
2156
+ const measurementChar = getBestMeasurementCharacter(ctx, fontFamily);
2157
+ const charMetrics = ctx.measureText(measurementChar);
2158
+
2159
+ return {
2160
+ textMetrics,
2161
+ charMetrics,
2162
+ measurementChar,
2163
+ fontProps: { fontSize, fontFamily, fontWeight, fontStyle },
2164
+ };
2165
+ }
2166
+
2167
+ // Debug visualization functions
2168
+ function toggleDebugMode() {
2169
+ debugMode = !debugMode;
2170
+ const debugInfo = document.getElementById('debugInfo');
2171
+
2172
+ if (debugMode) {
2173
+ debugInfo.style.display = 'block';
2174
+ debugInfo.innerHTML =
2175
+ '<strong>🔍 Debug Mode: ON</strong><br>Select a text object to see debug info';
2176
+ console.log('🔍 Debug mode enabled');
2177
+ } else {
2178
+ debugInfo.style.display = 'none';
2179
+ clearDebugOverlay();
2180
+ console.log('🔍 Debug mode disabled');
2181
+ }
2182
+
2183
+ // Update all text objects
2184
+ canvas.renderAll();
2185
+ }
2186
+
2187
+ function debugSelectedText() {
2188
+ const activeObject = canvas.getActiveObject();
2189
+ if (
2190
+ !activeObject ||
2191
+ (activeObject.type !== 'textbox' &&
2192
+ activeObject.type !== 'text' &&
2193
+ activeObject.type !== 'i-text')
2194
+ ) {
2195
+ alert('Please select a text object first');
2196
+ return;
2197
+ }
2198
+
2199
+ const debug = getDebugTextMetrics(activeObject);
2200
+ const debugInfo = document.getElementById('debugInfo');
2201
+
2202
+ debugInfo.style.display = 'block';
2203
+ debugInfo.innerHTML = `
2204
+ <strong>🔍 Debug Info for "${(activeObject.text || '').substring(0, 20)}..."</strong><br>
2205
+ <strong>Font:</strong> ${debug.fontProps.fontFamily} ${debug.fontProps.fontSize}px<br>
2206
+ <strong>Measurement char:</strong> '${debug.measurementChar}'<br>
2207
+ <strong>Text width:</strong> ${debug.textMetrics.width.toFixed(1)}px<br>
2208
+ <strong>Font ascent:</strong> ${debug.charMetrics.fontBoundingBoxAscent?.toFixed(1) || 'N/A'}px<br>
2209
+ <strong>Font descent:</strong> ${debug.charMetrics.fontBoundingBoxDescent?.toFixed(1) || 'N/A'}px<br>
2210
+ <strong>Actual ascent:</strong> ${debug.textMetrics.actualBoundingBoxAscent?.toFixed(1) || 'N/A'}px<br>
2211
+ <strong>Actual descent:</strong> ${debug.textMetrics.actualBoundingBoxDescent?.toFixed(1) || 'N/A'}px<br>
2212
+ <strong>Object bounds:</strong> ${activeObject.width?.toFixed(1) || 'auto'} x ${activeObject.height?.toFixed(1) || 'auto'}px
2213
+ `;
2214
+
2215
+ // Draw debug overlay
2216
+ drawDebugOverlay(activeObject, debug);
2217
+
2218
+ // COMPARISON LOGGING - Fabric.js vs Debug System
2219
+ console.log(
2220
+ '🔍================== MEASUREMENT COMPARISON ==================',
2221
+ );
2222
+ console.log('📊 FABRIC.JS ACTUAL STATE:');
2223
+ console.log(' Text:', activeObject.text);
2224
+ console.log(
2225
+ ' Font:',
2226
+ activeObject.fontFamily,
2227
+ activeObject.fontSize + 'px',
2228
+ );
2229
+ console.log(' Width limit:', activeObject.width + 'px');
2230
+ console.log(
2231
+ ' Actual height:',
2232
+ activeObject.height?.toFixed(1) + 'px',
2233
+ );
2234
+ console.log(
2235
+ ' Lines count:',
2236
+ activeObject._textLines?.length || 'unknown',
2237
+ );
2238
+ console.log(
2239
+ ' 🔧 enableAdvancedLayout:',
2240
+ activeObject.enableAdvancedLayout || false,
2241
+ );
2242
+ if (activeObject._textLines) {
2243
+ activeObject._textLines.forEach((line, i) => {
2244
+ console.log(` Line ${i + 1}:`, JSON.stringify(line));
2245
+ });
2246
+ }
2247
+
2248
+ console.log('🎯 OUR DEBUG SYSTEM CALCULATION:');
2249
+ console.log(' Measurement char:', debug.measurementChar);
2250
+ console.log(
2251
+ ' Full text width:',
2252
+ debug.textMetrics.width.toFixed(1) + 'px',
2253
+ );
2254
+ console.log(' Should wrap at:', activeObject.width + 'px');
2255
+ console.log(
2256
+ ' Should wrap?',
2257
+ debug.textMetrics.width > activeObject.width ? 'YES' : 'NO',
2258
+ );
2259
+
2260
+ // Simulate word-by-word measurement using the SAME method as layout engine
2261
+ const words = activeObject.text.split(' ');
2262
+
2263
+ // Create measurement options like layout engine does
2264
+ const measurementOptions = {
2265
+ fontFamily: activeObject.fontFamily,
2266
+ fontSize: activeObject.fontSize,
2267
+ fontStyle: activeObject.fontStyle,
2268
+ fontWeight: activeObject.fontWeight,
2269
+ letterSpacing: activeObject.letterSpacing,
2270
+ direction:
2271
+ activeObject.direction === 'inherit'
2272
+ ? 'ltr'
2273
+ : activeObject.direction || 'ltr',
2274
+ };
2275
+
2276
+ // Use the same grapheme-based measurement as layout engine
2277
+ function measureTextLikeLayoutEngine(text) {
2278
+ const tempCanvas = document.createElement('canvas');
2279
+ const ctx = tempCanvas.getContext('2d');
2280
+ ctx.font = `${measurementOptions.fontStyle || 'normal'} ${measurementOptions.fontWeight || 'normal'} ${measurementOptions.fontSize}px ${measurementOptions.fontFamily}`;
2281
+
2282
+ // Set text direction for proper measurement
2283
+ ctx.direction = measurementOptions.direction || 'ltr';
2284
+
2285
+ // Simple approximation - use the browser's measureText but apply our font-specific adjustments
2286
+ const basicMeasurement = ctx.measureText(text);
2287
+
2288
+ // Apply STV font correction factor based on what we observed
2289
+ // Layout engine: 206.1px, Browser: 236.9px → factor = 206.1/236.9 = 0.87
2290
+ const isSTV = /stv/i.test(measurementOptions.fontFamily);
2291
+ let correctionFactor = isSTV ? 0.87 : 1.0;
2292
+
2293
+ // Additional correction for RTL text if needed
2294
+ if (measurementOptions.direction === 'rtl' && isSTV) {
2295
+ // RTL Arabic text may need slightly different measurement
2296
+ correctionFactor *= 0.95;
2297
+ }
2298
+
2299
+ return basicMeasurement.width * correctionFactor;
2300
+ }
2301
+
2302
+ let currentLine = '';
2303
+ let lineCount = 1;
2304
+ console.log('🔤 WORD-BY-WORD SIMULATION (Layout Engine Method):');
2305
+ for (let i = 0; i < words.length; i++) {
2306
+ const word = words[i];
2307
+ const testLine = currentLine + (currentLine ? ' ' : '') + word;
2308
+ const width = measureTextLikeLayoutEngine(testLine);
2309
+
2310
+ console.log(
2311
+ ` Adding "${word}": "${testLine}" = ${width.toFixed(1)}px`,
2312
+ );
2313
+
2314
+ // Use exact width to match overlay editor behavior
2315
+ const browserAdjustedWidth = activeObject.width; // No artificial buffer
2316
+ if (width > browserAdjustedWidth && currentLine !== '') {
2317
+ const currentLineWidth = measureTextLikeLayoutEngine(currentLine);
2318
+ console.log(
2319
+ ` ✂️ WRAP! Line ${lineCount}: "${currentLine}" (${currentLineWidth.toFixed(1)}px)`,
2320
+ );
2321
+ currentLine = word;
2322
+ lineCount++;
2323
+ } else {
2324
+ currentLine = testLine;
2325
+ }
2326
+ }
2327
+
2328
+ if (currentLine) {
2329
+ const finalWidth = measureTextLikeLayoutEngine(currentLine);
2330
+ console.log(
2331
+ ` 📝 Final line ${lineCount}: "${currentLine}" (${finalWidth.toFixed(1)}px)`,
2332
+ );
2333
+ }
2334
+
2335
+ console.log('📊 COMPARISON SUMMARY:');
2336
+ console.log(' Expected lines:', lineCount);
2337
+ console.log(
2338
+ ' Fabric lines:',
2339
+ activeObject._textLines?.length || 'unknown',
2340
+ );
2341
+ console.log(
2342
+ ' Match?',
2343
+ lineCount === (activeObject._textLines?.length || 0)
2344
+ ? '✅ YES'
2345
+ : '❌ NO',
2346
+ );
2347
+ console.log('🔍================= END COMPARISON ===================');
2348
+
2349
+ console.log('🔍 Debug info for text object:', debug);
2350
+ }
2351
+
2352
+ function drawDebugOverlay(textObj, debug) {
2353
+ clearDebugOverlay();
2354
+
2355
+ const showBounds = document.getElementById('debugShowBounds').checked;
2356
+ const showBaseline =
2357
+ document.getElementById('debugShowBaseline').checked;
2358
+ const showMetrics = document.getElementById('debugShowMetrics').checked;
2359
+ const showWrap = document.getElementById('debugShowWrap').checked;
2360
+
2361
+ if (!showBounds && !showBaseline && !showMetrics && !showWrap) return;
2362
+
2363
+ // Get object's actual bounding rect - this accounts for Fabric.js transforms
2364
+ const boundingRect = textObj.getBoundingRect();
2365
+
2366
+ // Get text object position accounting for origin and transforms
2367
+ const matrix = textObj.calcTransformMatrix();
2368
+ const objectCenter = fabric.util.transformPoint(
2369
+ { x: textObj.width / 2, y: textObj.height / 2 },
2370
+ matrix,
2371
+ );
2372
+
2373
+ // Calculate the actual text baseline position within the object
2374
+ const fontSize = textObj.fontSize || 16;
2375
+ const fontFamily = textObj.fontFamily || 'Arial';
2376
+ const direction = textObj.direction || 'ltr';
2377
+
2378
+ // Use the same improved fallback logic as our measurement system
2379
+ const isSTVFont = /stv/i.test(fontFamily);
2380
+ const isArabicFont =
2381
+ /arabic|amiri|stv|noto.*arabic|cairo|scheherazade|droid.*arabic|tahoma/i.test(
2382
+ fontFamily,
2383
+ );
2384
+
2385
+ let ascent, descent;
2386
+
2387
+ if (
2388
+ debug.charMetrics.fontBoundingBoxAscent !== undefined &&
2389
+ debug.charMetrics.fontBoundingBoxDescent !== undefined
2390
+ ) {
2391
+ // Use browser-provided metrics if available
2392
+ ascent = debug.charMetrics.fontBoundingBoxAscent;
2393
+ descent = debug.charMetrics.fontBoundingBoxDescent;
2394
+ } else {
2395
+ // Apply the same improved fallbacks with better RTL handling
2396
+ if (isSTVFont) {
2397
+ // STV fonts need special handling for Arabic diacritics
2398
+ ascent = fontSize * 0.75; // Increased for Arabic diacritics
2399
+ descent = fontSize * 0.25; // Increased for Arabic descenders
2400
+ } else if (isArabicFont) {
2401
+ ascent = fontSize * 0.75;
2402
+ descent = fontSize * 0.18;
2403
+ } else {
2404
+ ascent = fontSize * 0.8;
2405
+ descent = fontSize * 0.2;
2406
+ }
2407
+ }
2408
+
2409
+ // Improved baseline calculation that accounts for RTL text positioning
2410
+ // For RTL text in Fabric.js, the baseline needs adjustment based on text alignment
2411
+ let baselineY;
2412
+ if (direction === 'rtl' && textObj.textAlign === 'right') {
2413
+ // For RTL right-aligned text, baseline is positioned differently
2414
+ baselineY = boundingRect.top + ascent + fontSize * 0.1; // Slight adjustment for RTL
2415
+ } else {
2416
+ // Standard LTR or center-aligned text
2417
+ baselineY = boundingRect.top + boundingRect.height - descent;
2418
+ }
2419
+
2420
+ const debugShapes = [];
2421
+
2422
+ // Show bounding boxes
2423
+ if (showBounds) {
2424
+ // Font bounding box (red) - based on font metrics from measurement character
2425
+ // For RTL text, position needs to account for text alignment
2426
+ let fontBoundsLeft = boundingRect.left;
2427
+ let fontBoundsWidth = boundingRect.width;
2428
+
2429
+ if (direction === 'rtl' && textObj.textAlign === 'right') {
2430
+ // For RTL right-aligned text, align the debug box to the right
2431
+ const actualTextWidth = debug.textMetrics.width;
2432
+ fontBoundsLeft =
2433
+ boundingRect.left + boundingRect.width - actualTextWidth;
2434
+ fontBoundsWidth = actualTextWidth;
2435
+ }
2436
+
2437
+ const fontBounds = new fabric.Rect({
2438
+ left: fontBoundsLeft,
2439
+ top: baselineY - ascent,
2440
+ width: fontBoundsWidth,
2441
+ height: ascent + descent,
2442
+ fill: 'rgba(255, 0, 0, 0.1)', // Semi-transparent red fill for better visibility
2443
+ stroke: 'red',
2444
+ strokeWidth: 2,
2445
+ strokeDashArray: [5, 5],
2446
+ selectable: false,
2447
+ evented: false,
2448
+ name: 'debug-font-bounds',
2449
+ originX: 'left',
2450
+ originY: 'top',
2451
+ });
2452
+ debugShapes.push(fontBounds);
2453
+
2454
+ // Fabric.js calculated bounds (purple) - what Fabric thinks the bounds are
2455
+ const fabricBounds = new fabric.Rect({
2456
+ left: boundingRect.left,
2457
+ top: boundingRect.top,
2458
+ width: boundingRect.width,
2459
+ height: boundingRect.height,
2460
+ fill: 'rgba(128, 0, 128, 0.05)', // Very light purple fill
2461
+ stroke: 'purple',
2462
+ strokeWidth: 1,
2463
+ strokeDashArray: [2, 2],
2464
+ selectable: false,
2465
+ evented: false,
2466
+ name: 'debug-fabric-bounds',
2467
+ originX: 'left',
2468
+ originY: 'top',
2469
+ });
2470
+ debugShapes.push(fabricBounds);
2471
+
2472
+ // Actual text bounding box (blue) if available
2473
+ if (debug.textMetrics.actualBoundingBoxAscent !== undefined) {
2474
+ let actualLeft =
2475
+ boundingRect.left +
2476
+ (debug.textMetrics.actualBoundingBoxLeft || 0);
2477
+ let actualWidth =
2478
+ (debug.textMetrics.actualBoundingBoxRight ||
2479
+ debug.textMetrics.width) -
2480
+ (debug.textMetrics.actualBoundingBoxLeft || 0);
2481
+
2482
+ // Adjust for RTL text positioning
2483
+ if (direction === 'rtl' && textObj.textAlign === 'right') {
2484
+ actualLeft =
2485
+ boundingRect.left +
2486
+ boundingRect.width -
2487
+ actualWidth -
2488
+ (debug.textMetrics.actualBoundingBoxLeft || 0);
2489
+ }
2490
+
2491
+ const actualBounds = new fabric.Rect({
2492
+ left: actualLeft,
2493
+ top: baselineY - debug.textMetrics.actualBoundingBoxAscent,
2494
+ width: actualWidth,
2495
+ height:
2496
+ debug.textMetrics.actualBoundingBoxAscent +
2497
+ debug.textMetrics.actualBoundingBoxDescent,
2498
+ fill: 'rgba(0, 0, 255, 0.1)', // Semi-transparent blue fill
2499
+ stroke: 'blue',
2500
+ strokeWidth: 2,
2501
+ selectable: false,
2502
+ evented: false,
2503
+ name: 'debug-actual-bounds',
2504
+ originX: 'left',
2505
+ originY: 'top',
2506
+ });
2507
+ debugShapes.push(actualBounds);
2508
+ }
2509
+ }
2510
+
2511
+ // Show baseline
2512
+ if (showBaseline) {
2513
+ // For RTL text, show baseline across the actual text width, not the full container
2514
+ let baselineLeft = boundingRect.left - 10;
2515
+ let baselineRight = boundingRect.left + boundingRect.width + 10;
2516
+
2517
+ if (direction === 'rtl' && textObj.textAlign === 'right') {
2518
+ const actualTextWidth = debug.textMetrics.width;
2519
+ baselineLeft =
2520
+ boundingRect.left + boundingRect.width - actualTextWidth - 10;
2521
+ baselineRight = boundingRect.left + boundingRect.width + 10;
2522
+ }
2523
+
2524
+ const baseline = new fabric.Line(
2525
+ [baselineLeft, baselineY, baselineRight, baselineY],
2526
+ {
2527
+ stroke: 'green',
2528
+ strokeWidth: 2,
2529
+ strokeDashArray: [3, 3],
2530
+ selectable: false,
2531
+ evented: false,
2532
+ name: 'debug-baseline',
2533
+ },
2534
+ );
2535
+ debugShapes.push(baseline);
2536
+ }
2537
+
2538
+ // Show metrics info
2539
+ if (showMetrics) {
2540
+ // Position metrics info differently for RTL text
2541
+ let metricsLeft = boundingRect.left + boundingRect.width + 15;
2542
+ if (direction === 'rtl') {
2543
+ // For RTL, show metrics on the left side to avoid overlap
2544
+ metricsLeft = Math.max(10, boundingRect.left - 200);
2545
+ }
2546
+
2547
+ const metricsText = new fabric.Text(
2548
+ `Direction: ${direction.toUpperCase()}\n` +
2549
+ `Align: ${textObj.textAlign || 'left'}\n` +
2550
+ `Char: ${debug.measurementChar} (${debug.charMetrics.width?.toFixed(1) || 'N/A'}px)\n` +
2551
+ `Font A: ${ascent.toFixed(1)}px\n` +
2552
+ `Font D: ${descent.toFixed(1)}px\n` +
2553
+ `Fabric: ${boundingRect.width.toFixed(1)}x${boundingRect.height.toFixed(1)}px\n` +
2554
+ `Text W: ${debug.textMetrics.width.toFixed(1)}px\n` +
2555
+ `Actual A: ${debug.textMetrics.actualBoundingBoxAscent?.toFixed(1) || 'N/A'}px`,
2556
+ {
2557
+ left: metricsLeft,
2558
+ top: baselineY - 60,
2559
+ fontSize: 11,
2560
+ fontFamily: 'monospace',
2561
+ fill: '#444',
2562
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
2563
+ selectable: false,
2564
+ evented: false,
2565
+ name: 'debug-metrics-info',
2566
+ },
2567
+ );
2568
+ debugShapes.push(metricsText);
2569
+ }
2570
+
2571
+ // Show text wrapping visualization
2572
+ if (showWrap && textObj.type === 'textbox') {
2573
+ const wrappingShapes = drawTextWrappingDebug(
2574
+ textObj,
2575
+ boundingRect,
2576
+ baselineY,
2577
+ ascent,
2578
+ descent,
2579
+ );
2580
+ debugShapes.push(...wrappingShapes);
2581
+ }
2582
+
2583
+ // Add all debug shapes to canvas
2584
+ debugShapes.forEach((shape) => canvas.add(shape));
2585
+ canvas.renderAll();
2586
+
2587
+ // Store reference for cleanup
2588
+ debugOverlay = debugShapes;
2589
+ }
2590
+
2591
+ function drawTextWrappingDebug(
2592
+ textObj,
2593
+ boundingRect,
2594
+ baselineY,
2595
+ ascent,
2596
+ descent,
2597
+ ) {
2598
+ const wrappingShapes = [];
2599
+ const text = textObj.text || '';
2600
+ const fontSize = textObj.fontSize || 16;
2601
+ const fontFamily = textObj.fontFamily || 'Arial';
2602
+ const direction = textObj.direction || 'ltr';
2603
+
2604
+ if (!text) return wrappingShapes;
2605
+
2606
+ // Create a temporary canvas to measure text wrapping
2607
+ const tempCanvas = document.createElement('canvas');
2608
+ const ctx = tempCanvas.getContext('2d');
2609
+ ctx.font = `${textObj.fontStyle || 'normal'} ${textObj.fontWeight || 'normal'} ${fontSize}px ${fontFamily}`;
2610
+ ctx.direction = direction;
2611
+ ctx.textBaseline = 'alphabetic';
2612
+
2613
+ // Simulate text wrapping logic similar to Fabric.js
2614
+ const words = text.split(' ');
2615
+ const maxWidth = textObj.width || boundingRect.width;
2616
+
2617
+ console.log('🎯 DEBUG WRAP VISUALIZATION:');
2618
+ console.log(' textObj.width:', textObj.width);
2619
+ console.log(' boundingRect.width:', boundingRect.width);
2620
+ console.log(' Using maxWidth:', maxWidth);
2621
+ const lineHeight = fontSize * 1.16; // Fabric.js default line height
2622
+
2623
+ let currentLine = '';
2624
+ let lines = [];
2625
+ let currentY = baselineY;
2626
+
2627
+ // Simple word-wrapping algorithm
2628
+ for (let i = 0; i < words.length; i++) {
2629
+ const word = words[i];
2630
+ const testLine = currentLine + (currentLine ? ' ' : '') + word;
2631
+ const metrics = ctx.measureText(testLine);
2632
+
2633
+ if (metrics.width > maxWidth && currentLine !== '') {
2634
+ // Line is too long, wrap it
2635
+ lines.push({
2636
+ text: currentLine,
2637
+ y: currentY,
2638
+ width: ctx.measureText(currentLine).width,
2639
+ });
2640
+ currentLine = word;
2641
+ currentY += lineHeight;
2642
+ } else {
2643
+ currentLine = testLine;
2644
+ }
2645
+ }
2646
+
2647
+ // Add the last line
2648
+ if (currentLine) {
2649
+ lines.push({
2650
+ text: currentLine,
2651
+ y: currentY,
2652
+ width: ctx.measureText(currentLine).width,
2653
+ });
2654
+ }
2655
+
2656
+ // Draw line visualizations
2657
+ lines.forEach((line, index) => {
2658
+ // Line boundary boxes (orange)
2659
+ const lineBox = new fabric.Rect({
2660
+ left: boundingRect.left,
2661
+ top: line.y - ascent,
2662
+ width: maxWidth,
2663
+ height: lineHeight,
2664
+ fill: 'rgba(255, 165, 0, 0.1)',
2665
+ stroke: 'orange',
2666
+ strokeWidth: 1,
2667
+ strokeDashArray: [3, 3],
2668
+ selectable: false,
2669
+ evented: false,
2670
+ name: 'debug-line-box',
2671
+ originX: 'left',
2672
+ originY: 'top',
2673
+ });
2674
+ wrappingShapes.push(lineBox);
2675
+
2676
+ // Text width indicator (yellow) - properly positioned for RTL
2677
+ let textWidthLeft;
2678
+ if (direction === 'rtl') {
2679
+ // For RTL, the text should be aligned to the right side of the container
2680
+ textWidthLeft = boundingRect.left + maxWidth - line.width;
2681
+ } else {
2682
+ // For LTR, align to the left
2683
+ textWidthLeft = boundingRect.left;
2684
+ }
2685
+
2686
+ const textWidthBox = new fabric.Rect({
2687
+ left: textWidthLeft,
2688
+ top: line.y - ascent + 2,
2689
+ width: line.width,
2690
+ height: lineHeight - 4,
2691
+ fill: 'rgba(255, 255, 0, 0.3)', // Slightly more opaque for better visibility
2692
+ stroke: 'gold',
2693
+ strokeWidth: 1,
2694
+ selectable: false,
2695
+ evented: false,
2696
+ name: 'debug-text-width',
2697
+ originX: 'left',
2698
+ originY: 'top',
2699
+ });
2700
+ wrappingShapes.push(textWidthBox);
2701
+
2702
+ // Line number labels
2703
+ const lineLabel = new fabric.Text(`L${index + 1}`, {
2704
+ left: boundingRect.left - 25,
2705
+ top: line.y - 5,
2706
+ fontSize: 10,
2707
+ fontFamily: 'monospace',
2708
+ fill: 'orange',
2709
+ selectable: false,
2710
+ evented: false,
2711
+ name: 'debug-line-label',
2712
+ });
2713
+ wrappingShapes.push(lineLabel);
2714
+ });
2715
+
2716
+ // Add wrap width indicator (vertical line)
2717
+ const wrapIndicator = new fabric.Line(
2718
+ [
2719
+ boundingRect.left + maxWidth,
2720
+ boundingRect.top - 10,
2721
+ boundingRect.left + maxWidth,
2722
+ boundingRect.top + boundingRect.height + 10,
2723
+ ],
2724
+ {
2725
+ stroke: 'red',
2726
+ strokeWidth: 2,
2727
+ strokeDashArray: [8, 4],
2728
+ selectable: false,
2729
+ evented: false,
2730
+ name: 'debug-wrap-indicator',
2731
+ },
2732
+ );
2733
+ wrappingShapes.push(wrapIndicator);
2734
+
2735
+ // Wrap width label
2736
+ const wrapLabel = new fabric.Text(`Wrap: ${maxWidth.toFixed(0)}px`, {
2737
+ left: boundingRect.left + maxWidth + 5,
2738
+ top: boundingRect.top - 15,
2739
+ fontSize: 10,
2740
+ fontFamily: 'monospace',
2741
+ fill: 'red',
2742
+ backgroundColor: 'rgba(255, 255, 255, 0.8)',
2743
+ selectable: false,
2744
+ evented: false,
2745
+ name: 'debug-wrap-label',
2746
+ });
2747
+ wrappingShapes.push(wrapLabel);
2748
+
2749
+ return wrappingShapes;
2750
+ }
2751
+
2752
+ function clearDebugOverlay() {
2753
+ if (debugOverlay) {
2754
+ debugOverlay.forEach((shape) => canvas.remove(shape));
2755
+ debugOverlay = null;
2756
+ canvas.renderAll();
2757
+ }
2758
+ }
2759
+
2760
+ function forceRefreshText() {
2761
+ const activeObject = canvas.getActiveObject();
2762
+ if (
2763
+ !activeObject ||
2764
+ (activeObject.type !== 'textbox' &&
2765
+ activeObject.type !== 'text' &&
2766
+ activeObject.type !== 'i-text')
2767
+ ) {
2768
+ alert('Please select a text object first');
2769
+ return;
2770
+ }
2771
+
2772
+ console.log('🔄 Force refreshing text object...');
2773
+
2774
+ // Clear all caches
2775
+ if (typeof activeObject._clearCache === 'function') {
2776
+ activeObject._clearCache();
2777
+ }
2778
+
2779
+ // Force re-initialization
2780
+ activeObject.dirty = true;
2781
+ activeObject.initialized = false;
2782
+
2783
+ // For textboxes, force recalculation of dimensions
2784
+ if (activeObject.type === 'textbox') {
2785
+ // Store current properties
2786
+ const currentText = activeObject.text;
2787
+ const currentWidth = activeObject.width;
2788
+
2789
+ // Temporarily change text to force re-layout
2790
+ activeObject.set('text', currentText + ' ');
2791
+ canvas.renderAll();
2792
+
2793
+ // Reset to original text
2794
+ activeObject.set('text', currentText);
2795
+ activeObject.set('width', currentWidth);
2796
+
2797
+ // Force layout recalculation
2798
+ if (typeof activeObject.initDimensions === 'function') {
2799
+ activeObject.initDimensions();
2800
+ }
2801
+ }
2802
+
2803
+ // Force complete re-render
2804
+ canvas.renderAll();
2805
+
2806
+ console.log('🔄 Text object refreshed');
2807
+
2808
+ // Automatically debug the refreshed text
2809
+ if (debugMode) {
2810
+ setTimeout(debugSelectedText, 100);
2811
+ }
2812
+ }
2813
+
2814
+ function testWidthConstraints() {
2815
+ clearCanvas();
2816
+
2817
+ const arabicText = 'مرحبا بالعالم العربي';
2818
+ console.log(
2819
+ '🧪 Testing width constraints and dynamicMinWidth behavior...',
2820
+ );
2821
+
2822
+ // Test different width scenarios
2823
+ const testScenarios = [
2824
+ { width: 300, desc: 'Wide (300px) - Should wrap normally' },
2825
+ { width: 160, desc: 'Medium (160px) - At dynamic limit' },
2826
+ { width: 120, desc: 'Narrow (120px) - Below dynamic limit' },
2827
+ { width: 80, desc: 'Very narrow (80px) - Force character wrap' },
2828
+ ];
2829
+
2830
+ let yPos = 120;
2831
+
2832
+ testScenarios.forEach((scenario, index) => {
2833
+ const xPos = 150;
2834
+ const currentYPos = yPos + index * 120;
2835
+
2836
+ // Create standard textbox
2837
+ const standardTextbox = new fabric.Textbox(arabicText, {
2838
+ left: xPos,
2839
+ top: currentYPos,
2840
+ fontSize: 20,
2841
+ fontFamily: 'STV Regular',
2842
+ fill: '#333',
2843
+ width: scenario.width,
2844
+ direction: 'rtl',
2845
+ textAlign: 'right',
2846
+ backgroundColor: 'rgba(255, 200, 200, 0.3)', // Light red background
2847
+ });
2848
+
2849
+ canvas.add(standardTextbox);
2850
+
2851
+ // Create forced-width textbox using our helper function
2852
+ const forcedTextbox = createTextboxWithForcedWidth(
2853
+ arabicText,
2854
+ {
2855
+ left: xPos + 350,
2856
+ top: currentYPos,
2857
+ fontSize: 20,
2858
+ fontFamily: 'STV Regular',
2859
+ fill: '#333',
2860
+ direction: 'rtl',
2861
+ textAlign: 'right',
2862
+ backgroundColor: 'rgba(200, 255, 200, 0.3)', // Light green background
2863
+ },
2864
+ scenario.width,
2865
+ );
2866
+
2867
+ canvas.add(forcedTextbox);
2868
+
2869
+ // Add labels
2870
+ const standardLabel = new fabric.Text(
2871
+ `Standard\n${scenario.desc}\nAuto-expansion: ${standardTextbox.dynamicMinWidth > scenario.width ? 'YES' : 'NO'}`,
2872
+ {
2873
+ left: xPos,
2874
+ top: currentYPos - 50,
2875
+ fontSize: 10,
2876
+ fontFamily: 'Arial',
2877
+ fill: '#666',
2878
+ textAlign: 'center',
2879
+ originX: 'center',
2880
+ },
2881
+ );
2882
+
2883
+ const forcedLabel = new fabric.Text(
2884
+ `Forced Width\n${scenario.desc}\nOverride: YES`,
2885
+ {
2886
+ left: xPos + 350,
2887
+ top: currentYPos - 50,
2888
+ fontSize: 10,
2889
+ fontFamily: 'Arial',
2890
+ fill: '#666',
2891
+ textAlign: 'center',
2892
+ originX: 'center',
2893
+ },
2894
+ );
2895
+
2896
+ canvas.add(standardLabel);
2897
+ canvas.add(forcedLabel);
2898
+
2899
+ console.log(`📊 ${scenario.desc}:`, {
2900
+ targetWidth: scenario.width,
2901
+ standardActualWidth: standardTextbox.width,
2902
+ standardDynamicMin: standardTextbox.dynamicMinWidth,
2903
+ forcedActualWidth: forcedTextbox.width,
2904
+ forcedDynamicMin: forcedTextbox.dynamicMinWidth,
2905
+ });
2906
+ });
2907
+
2908
+ // Add main title
2909
+ const title = new fabric.Text(
2910
+ 'Width Constraint Test\nRed = Standard (auto-expansion) | Green = Forced Width Override',
2911
+ {
2912
+ left: 400,
2913
+ top: 50,
2914
+ fontSize: 14,
2915
+ fontFamily: 'Arial',
2916
+ fill: '#333',
2917
+ textAlign: 'center',
2918
+ originX: 'center',
2919
+ },
2920
+ );
2921
+
2922
+ canvas.add(title);
2923
+ canvas.renderAll();
2924
+ updateCanvasInfo();
2925
+
2926
+ console.log('🧪 Width constraints test complete!');
2927
+ console.log(
2928
+ '📋 Compare the red (standard) vs green (forced) textboxes',
2929
+ );
2930
+ console.log(
2931
+ '💡 Notice how standard textboxes auto-expand when text is wider than container',
2932
+ );
2933
+ }
2934
+
2935
+ function testForcedNarrowText() {
2936
+ clearCanvas();
2937
+
2938
+ const texts = [
2939
+ 'مرحبا بالعالم العربي',
2940
+ 'This is a long English text that should wrap',
2941
+ 'Mixed: Hello مرحبا World عالم!',
2942
+ ];
2943
+
2944
+ console.log('🧪 Testing forced narrow text wrapping...');
2945
+
2946
+ texts.forEach((text, index) => {
2947
+ const yPos = 150 + index * 150;
2948
+
2949
+ // Create very narrow textboxes (80px) with different methods
2950
+ const methods = [
2951
+ { name: 'Standard', bg: 'rgba(255, 200, 200, 0.3)' },
2952
+ { name: 'Forced + Character Wrap', bg: 'rgba(200, 255, 200, 0.3)' },
2953
+ { name: 'Forced + Word Wrap', bg: 'rgba(200, 200, 255, 0.3)' },
2954
+ ];
2955
+
2956
+ methods.forEach((method, methodIndex) => {
2957
+ const xPos = 150 + methodIndex * 200;
2958
+ let textbox;
2959
+
2960
+ if (methodIndex === 0) {
2961
+ // Standard textbox
2962
+ textbox = new fabric.Textbox(text, {
2963
+ left: xPos,
2964
+ top: yPos,
2965
+ fontSize: 16,
2966
+ fontFamily: 'Arial',
2967
+ fill: '#333',
2968
+ width: 80,
2969
+ direction: text.includes('مرحبا') ? 'rtl' : 'ltr',
2970
+ textAlign: text.includes('مرحبا') ? 'right' : 'left',
2971
+ backgroundColor: method.bg,
2972
+ });
2973
+ } else if (methodIndex === 1) {
2974
+ // Forced width with character wrapping
2975
+ textbox = createTextboxWithForcedWidth(
2976
+ text,
2977
+ {
2978
+ left: xPos,
2979
+ top: yPos,
2980
+ fontSize: 16,
2981
+ fontFamily: 'Arial',
2982
+ fill: '#333',
2983
+ direction: text.includes('مرحبا') ? 'rtl' : 'ltr',
2984
+ textAlign: text.includes('مرحبا') ? 'right' : 'left',
2985
+ backgroundColor: method.bg,
2986
+ splitByGrapheme: true, // Character-based wrapping
2987
+ },
2988
+ 80,
2989
+ );
2990
+ } else {
2991
+ // Forced width with word wrapping
2992
+ textbox = createTextboxWithForcedWidth(
2993
+ text,
2994
+ {
2995
+ left: xPos,
2996
+ top: yPos,
2997
+ fontSize: 16,
2998
+ fontFamily: 'Arial',
2999
+ fill: '#333',
3000
+ direction: text.includes('مرحبا') ? 'rtl' : 'ltr',
3001
+ textAlign: text.includes('مرحبا') ? 'right' : 'left',
3002
+ backgroundColor: method.bg,
3003
+ splitByGrapheme: false, // Word-based wrapping
3004
+ },
3005
+ 80,
3006
+ );
3007
+ }
3008
+
3009
+ canvas.add(textbox);
3010
+
3011
+ // Add method label
3012
+ const label = new fabric.Text(
3013
+ `${method.name}\nWidth: ${textbox.width}px\nDynMin: ${textbox.dynamicMinWidth}px`,
3014
+ {
3015
+ left: xPos,
3016
+ top: yPos - 40,
3017
+ fontSize: 9,
3018
+ fontFamily: 'Arial',
3019
+ fill: '#666',
3020
+ textAlign: 'center',
3021
+ originX: 'center',
3022
+ },
3023
+ );
3024
+
3025
+ canvas.add(label);
3026
+ });
3027
+
3028
+ // Add text sample label
3029
+ const sampleLabel = new fabric.Text(
3030
+ `Text ${index + 1}: "${text.substring(0, 20)}..."`,
3031
+ {
3032
+ left: 50,
3033
+ top: yPos + 20,
3034
+ fontSize: 10,
3035
+ fontFamily: 'Arial',
3036
+ fill: '#999',
3037
+ textAlign: 'left',
3038
+ },
3039
+ );
3040
+
3041
+ canvas.add(sampleLabel);
3042
+ });
3043
+
3044
+ // Add main title
3045
+ const title = new fabric.Text(
3046
+ 'Forced Narrow Text Test (80px width)\nRed = Standard | Green = Forced + Char Wrap | Blue = Forced + Word Wrap',
3047
+ {
3048
+ left: 400,
3049
+ top: 50,
3050
+ fontSize: 14,
3051
+ fontFamily: 'Arial',
3052
+ fill: '#333',
3053
+ textAlign: 'center',
3054
+ originX: 'center',
3055
+ },
3056
+ );
3057
+
3058
+ canvas.add(title);
3059
+ canvas.renderAll();
3060
+ updateCanvasInfo();
3061
+
3062
+ console.log('🧪 Forced narrow text test complete!');
3063
+ console.log(
3064
+ '📋 Compare how different methods handle very narrow containers',
3065
+ );
3066
+ }
3067
+
3068
+ function testUnlimitedHeight() {
3069
+ clearCanvas();
3070
+
3071
+ // Create a very long Arabic text that should create many lines
3072
+ const veryLongArabicText =
3073
+ 'مرحبا بالعالم العربي هذا نص عربي طويل جداً يجب أن يتم تقسيمه على عدة أسطر كثيرة جداً لاختبار نظام التفاف النص والارتفاع غير المحدود في النصوص العربية من اليمين إلى اليسار ونريد أن نرى كم سطر يمكن أن نحصل عليه مع النص الطويل جداً هذا وهل سيتوقف عند حد معين أم سيستمر في النمو حسب النص المتاح وهذا مهم جداً لفهم طريقة عمل النظام';
3074
+
3075
+ console.log(
3076
+ '🧪 Testing unlimited height growth vs height-constrained textboxes with STV Regular font...',
3077
+ );
3078
+
3079
+ const testWidth = 120; // Very narrow to force many wrapping lines
3080
+
3081
+ // Test with standard textbox (height constrained)
3082
+ const standardTextbox = new fabric.Textbox(veryLongArabicText, {
3083
+ left: 150,
3084
+ top: 100,
3085
+ fontSize: 18,
3086
+ fontFamily: 'STV Regular',
3087
+ fill: '#333',
3088
+ width: testWidth,
3089
+ direction: 'rtl',
3090
+ textAlign: 'right',
3091
+ backgroundColor: 'rgba(255, 200, 200, 0.3)', // Light red
3092
+ enableAdvancedLayout: false, // Use consistent layout system
3093
+ wrap: 'word', // Explicit word wrapping
3094
+ });
3095
+
3096
+ // Override width constraints but keep height constraint
3097
+ standardTextbox.minWidth = 10;
3098
+ standardTextbox.dynamicMinWidth = 0;
3099
+ standardTextbox.initDimensions();
3100
+
3101
+ canvas.add(standardTextbox);
3102
+
3103
+ // Test with unlimited height textbox
3104
+ const unlimitedTextbox = createTextboxWithUnlimitedHeight(
3105
+ veryLongArabicText,
3106
+ {
3107
+ left: 300,
3108
+ top: 100,
3109
+ fontSize: 18,
3110
+ fontFamily: 'STV Regular',
3111
+ fill: '#333',
3112
+ direction: 'rtl',
3113
+ textAlign: 'right',
3114
+ backgroundColor: 'rgba(200, 255, 200, 0.3)', // Light green
3115
+ enableAdvancedLayout: false, // Use consistent layout system
3116
+ wrap: 'word', // Explicit word wrapping
3117
+ },
3118
+ testWidth,
3119
+ );
3120
+
3121
+ canvas.add(unlimitedTextbox);
3122
+
3123
+ // Add labels
3124
+ const standardLabel = new fabric.Text(
3125
+ `Standard Textbox\nHeight Constrained\nWidth: ${standardTextbox.width}px\nHeight: ${standardTextbox.height.toFixed(1)}px\nLines: ${standardTextbox._textLines?.length || 0}`,
3126
+ {
3127
+ left: 150,
3128
+ top: 50,
3129
+ fontSize: 10,
3130
+ fontFamily: 'Arial',
3131
+ fill: '#666',
3132
+ textAlign: 'center',
3133
+ originX: 'center',
3134
+ },
3135
+ );
3136
+
3137
+ const unlimitedLabel = new fabric.Text(
3138
+ `Unlimited Height Textbox\nNo Height Constraint\nWidth: ${unlimitedTextbox.width}px\nHeight: ${unlimitedTextbox.height.toFixed(1)}px\nLines: ${unlimitedTextbox._textLines?.length || 0}`,
3139
+ {
3140
+ left: 300,
3141
+ top: 50,
3142
+ fontSize: 10,
3143
+ fontFamily: 'Arial',
3144
+ fill: '#666',
3145
+ textAlign: 'center',
3146
+ originX: 'center',
3147
+ },
3148
+ );
3149
+
3150
+ canvas.add(standardLabel);
3151
+ canvas.add(unlimitedLabel);
3152
+
3153
+ // Add main title
3154
+ const title = new fabric.Text(
3155
+ 'Unlimited Height Test with STV Regular Font\nRed = Standard (height constrained) | Green = Unlimited Height Growth',
3156
+ {
3157
+ left: 400,
3158
+ top: 20,
3159
+ fontSize: 14,
3160
+ fontFamily: 'Arial',
3161
+ fill: '#333',
3162
+ textAlign: 'center',
3163
+ originX: 'center',
3164
+ },
3165
+ );
3166
+
3167
+ canvas.add(title);
3168
+ canvas.renderAll();
3169
+ updateCanvasInfo();
3170
+
3171
+ console.log('📊 Height constraint comparison:', {
3172
+ standardHeight: standardTextbox.height.toFixed(1) + 'px',
3173
+ standardLines: standardTextbox._textLines?.length || 0,
3174
+ unlimitedHeight: unlimitedTextbox.height.toFixed(1) + 'px',
3175
+ unlimitedLines: unlimitedTextbox._textLines?.length || 0,
3176
+ difference: `${(((unlimitedTextbox.height - standardTextbox.height) / standardTextbox.height) * 100).toFixed(1)}% taller`,
3177
+ });
3178
+
3179
+ // Enable debug mode to see wrapping visualization
3180
+ if (!debugMode) {
3181
+ toggleDebugMode();
3182
+ document.getElementById('debugShowWrap').checked = true;
3183
+ }
3184
+
3185
+ // Auto-select the unlimited textbox for debug comparison
3186
+ setTimeout(() => {
3187
+ canvas.setActiveObject(unlimitedTextbox);
3188
+ debugSelectedText();
3189
+ }, 500);
3190
+
3191
+ console.log('🧪 Unlimited height test complete!');
3192
+ console.log(
3193
+ '💡 Compare red (height constrained) vs green (unlimited height) textboxes',
3194
+ );
3195
+ console.log(
3196
+ '📏 The green box should show many more lines than the red box',
3197
+ );
3198
+ console.log(
3199
+ '🔍 Debug visualization enabled - compare wrapping visualization with actual text',
3200
+ );
3201
+ }
3202
+
3203
+ function testWrappingConsistency() {
3204
+ clearCanvas();
3205
+
3206
+ const testText = 'مرحبا بالعالم العربي هذا نص طويل';
3207
+ console.log(
3208
+ '🧪 Testing wrapping consistency between debug visualization and actual text...',
3209
+ );
3210
+
3211
+ // Create textbox with STV Regular font
3212
+ const textbox = createTextboxWithUnlimitedHeight(
3213
+ testText,
3214
+ {
3215
+ left: 300,
3216
+ top: 150,
3217
+ fontSize: 20,
3218
+ fontFamily: 'STV Regular',
3219
+ fill: '#333',
3220
+ direction: 'rtl',
3221
+ textAlign: 'right',
3222
+ backgroundColor: 'rgba(255, 255, 0, 0.1)', // Light yellow background
3223
+ enableAdvancedLayout: false,
3224
+ wrap: 'word',
3225
+ },
3226
+ 120,
3227
+ );
3228
+
3229
+ canvas.add(textbox);
3230
+
3231
+ // Add title
3232
+ const title = new fabric.Text(
3233
+ 'Wrapping Consistency Test\nCompare actual text vs debug wrapping visualization',
3234
+ {
3235
+ left: 400,
3236
+ top: 50,
3237
+ fontSize: 14,
3238
+ fontFamily: 'Arial',
3239
+ fill: '#333',
3240
+ textAlign: 'center',
3241
+ originX: 'center',
3242
+ },
3243
+ );
3244
+
3245
+ canvas.add(title);
3246
+
3247
+ // Enable debug mode automatically
3248
+ if (!debugMode) {
3249
+ toggleDebugMode();
3250
+ }
3251
+
3252
+ // Enable all debug options
3253
+ document.getElementById('debugShowBounds').checked = true;
3254
+ document.getElementById('debugShowBaseline').checked = true;
3255
+ document.getElementById('debugShowMetrics').checked = true;
3256
+ document.getElementById('debugShowWrap').checked = true;
3257
+
3258
+ // Select textbox and run debug
3259
+ setTimeout(() => {
3260
+ canvas.setActiveObject(textbox);
3261
+ debugSelectedText();
3262
+
3263
+ // Log detailed comparison
3264
+ console.log('🔍 WRAPPING ANALYSIS:');
3265
+ console.log('📊 Textbox details:', {
3266
+ text: textbox.text,
3267
+ width: textbox.width + 'px',
3268
+ height: textbox.height + 'px',
3269
+ lines: textbox._textLines?.length || 0,
3270
+ actualLines: textbox._textLines || [],
3271
+ });
3272
+
3273
+ // Compare with what debug system predicts
3274
+ const debug = getDebugTextMetrics(textbox);
3275
+ console.log('🎯 Debug prediction vs Reality:');
3276
+ console.log(
3277
+ ' Fabric lines:',
3278
+ textbox._textLines?.map((line, i) => `${i + 1}: "${line}"`),
3279
+ );
3280
+ }, 1000);
3281
+
3282
+ canvas.renderAll();
3283
+ updateCanvasInfo();
3284
+
3285
+ console.log('🧪 Wrapping consistency test setup complete!');
3286
+ console.log('📋 Look at the yellow textbox and compare:');
3287
+ console.log(' • Orange boxes: Debug predicted line boundaries');
3288
+ console.log(' • Yellow boxes: Debug predicted text width per line');
3289
+ console.log(
3290
+ ' • Actual text: How Fabric.js actually wrapped the text',
3291
+ );
3292
+ }
3293
+
3294
+ function testArabicFonts() {
3295
+ clearCanvas();
3296
+
3297
+ const arabicText = 'مرحبا بالعالم العربي';
3298
+ const mixedText = 'Hello مرحبا World';
3299
+ const longArabicText =
3300
+ 'هذا نص عربي طويل جداً يجب أن يتم تقسيمه على عدة أسطر لاختبار نظام التفاف النص في النصوص من اليمين إلى اليسار';
3301
+
3302
+ const testCases = [
3303
+ { text: arabicText, desc: 'Short Arabic' },
3304
+ { text: mixedText, desc: 'Mixed LTR/RTL' },
3305
+ { text: longArabicText, desc: 'Long wrapping Arabic' },
3306
+ ];
3307
+
3308
+ const fonts = ['Arial', 'STV Regular', 'STV Bold', 'STV Light'];
3309
+ let yPos = 120;
3310
+
3311
+ console.log(
3312
+ '🧪 Testing Arabic fonts with improved debug visualization...',
3313
+ );
3314
+
3315
+ fonts.forEach((font, fontIndex) => {
3316
+ testCases.forEach((testCase, caseIndex) => {
3317
+ const xPos = 150 + caseIndex * 250;
3318
+ const currentYPos = yPos + fontIndex * 140;
3319
+
3320
+ const textObj = new fabric.Textbox(testCase.text, {
3321
+ left: xPos,
3322
+ top: currentYPos,
3323
+ fontSize: 24,
3324
+ fontFamily: font,
3325
+ fill: '#333',
3326
+ width: 200,
3327
+ direction: 'rtl',
3328
+ textAlign: 'right',
3329
+ enableAdvancedLayout: false, // Enable new measurement system!
3330
+ // Add slight background to make debug overlay more visible
3331
+ backgroundColor: 'rgba(255, 255, 255, 0.8)',
3332
+ });
3333
+
3334
+ canvas.add(textObj);
3335
+
3336
+ // Add descriptive labels
3337
+ const label = new fabric.Text(`${font}\n${testCase.desc}`, {
3338
+ left: xPos,
3339
+ top: currentYPos - 30,
3340
+ fontSize: 10,
3341
+ fontFamily: 'Arial',
3342
+ fill: '#666',
3343
+ textAlign: 'center',
3344
+ originX: 'center',
3345
+ });
3346
+
3347
+ canvas.add(label);
3348
+
3349
+ console.log(`🔤 Added test case: ${font} - ${testCase.desc}`);
3350
+ });
3351
+ });
3352
+
3353
+ canvas.renderAll();
3354
+ updateCanvasInfo();
3355
+
3356
+ // Automatically enable debug mode and show all debug options
3357
+ if (!debugMode) {
3358
+ toggleDebugMode();
3359
+ }
3360
+
3361
+ // Enable all debug visualizations by default
3362
+ document.getElementById('debugShowBounds').checked = true;
3363
+ document.getElementById('debugShowBaseline').checked = true;
3364
+ document.getElementById('debugShowMetrics').checked = true;
3365
+ document.getElementById('debugShowWrap').checked = true;
3366
+
3367
+ console.log(
3368
+ '🧪 Arabic font test complete with improved RTL debug visualization!',
3369
+ );
3370
+ console.log('📋 Debug features enabled:');
3371
+ console.log(' • Red boxes: Font metrics bounds');
3372
+ console.log(' • Blue boxes: Actual text bounds');
3373
+ console.log(' • Purple boxes: Fabric.js calculated bounds');
3374
+ console.log(' • Green lines: Text baseline');
3375
+ console.log(' • Yellow boxes: Text wrapping visualization');
3376
+ console.log(' • Metrics info: Positioned appropriately for RTL text');
3377
+ console.log(
3378
+ '🔍 Click on any text object to see detailed debug information',
3379
+ );
3380
+ }
3381
+
3382
+ // Event listeners for debug checkboxes
3383
+ document
3384
+ .getElementById('debugShowBounds')
3385
+ .addEventListener('change', function () {
3386
+ if (debugMode) {
3387
+ const activeObject = canvas.getActiveObject();
3388
+ if (
3389
+ activeObject &&
3390
+ (activeObject.type === 'textbox' ||
3391
+ activeObject.type === 'text' ||
3392
+ activeObject.type === 'i-text')
3393
+ ) {
3394
+ debugSelectedText();
3395
+ }
3396
+ }
3397
+ });
3398
+
3399
+ document
3400
+ .getElementById('debugShowBaseline')
3401
+ .addEventListener('change', function () {
3402
+ if (debugMode) {
3403
+ const activeObject = canvas.getActiveObject();
3404
+ if (
3405
+ activeObject &&
3406
+ (activeObject.type === 'textbox' ||
3407
+ activeObject.type === 'text' ||
3408
+ activeObject.type === 'i-text')
3409
+ ) {
3410
+ debugSelectedText();
3411
+ }
3412
+ }
3413
+ });
3414
+
3415
+ document
3416
+ .getElementById('debugShowMetrics')
3417
+ .addEventListener('change', function () {
3418
+ if (debugMode) {
3419
+ const activeObject = canvas.getActiveObject();
3420
+ if (
3421
+ activeObject &&
3422
+ (activeObject.type === 'textbox' ||
3423
+ activeObject.type === 'text' ||
3424
+ activeObject.type === 'i-text')
3425
+ ) {
3426
+ debugSelectedText();
3427
+ }
3428
+ }
3429
+ });
3430
+
3431
+ document
3432
+ .getElementById('debugShowWrap')
3433
+ .addEventListener('change', function () {
3434
+ if (debugMode) {
3435
+ const activeObject = canvas.getActiveObject();
3436
+ if (
3437
+ activeObject &&
3438
+ (activeObject.type === 'textbox' ||
3439
+ activeObject.type === 'text' ||
3440
+ activeObject.type === 'i-text')
3441
+ ) {
3442
+ debugSelectedText();
3443
+ }
3444
+ }
3445
+ });
3446
+
3447
+ // Auto-debug on selection change
3448
+ canvas.on('selection:created', function () {
3449
+ if (debugMode) {
3450
+ setTimeout(debugSelectedText, 100); // Small delay to ensure selection is ready
3451
+ }
3452
+ });
3453
+
3454
+ canvas.on('selection:updated', function () {
3455
+ if (debugMode) {
3456
+ setTimeout(debugSelectedText, 100);
3457
+ }
3458
+ });
3459
+
3460
+ canvas.on('selection:cleared', function () {
3461
+ clearDebugOverlay();
3462
+ if (debugMode) {
3463
+ const debugInfo = document.getElementById('debugInfo');
3464
+ debugInfo.innerHTML =
3465
+ '<strong>🔍 Debug Mode: ON</strong><br>Select a text object to see debug info';
3466
+ }
3467
+ });
3468
+
3469
+ // Live debug updates when resizing/moving textbox
3470
+ canvas.on('object:scaling', function (e) {
3471
+ if (
3472
+ debugMode &&
3473
+ e.target &&
3474
+ (e.target.type === 'textbox' ||
3475
+ e.target.type === 'text' ||
3476
+ e.target.type === 'i-text')
3477
+ ) {
3478
+ setTimeout(() => {
3479
+ console.log('📏 LIVE RESIZE UPDATE:');
3480
+ debugSelectedText();
3481
+ }, 50);
3482
+ }
3483
+ });
3484
+
3485
+ canvas.on('object:resizing', function (e) {
3486
+ if (
3487
+ debugMode &&
3488
+ e.target &&
3489
+ (e.target.type === 'textbox' ||
3490
+ e.target.type === 'text' ||
3491
+ e.target.type === 'i-text')
3492
+ ) {
3493
+ setTimeout(() => {
3494
+ console.log('📏 LIVE RESIZE UPDATE:');
3495
+ debugSelectedText();
3496
+ }, 50);
3497
+ }
3498
+ });
3499
+
3500
+ canvas.on('object:modified', function (e) {
3501
+ if (
3502
+ debugMode &&
3503
+ e.target &&
3504
+ (e.target.type === 'textbox' ||
3505
+ e.target.type === 'text' ||
3506
+ e.target.type === 'i-text')
3507
+ ) {
3508
+ setTimeout(() => {
3509
+ console.log('📏 TEXTBOX MODIFIED:');
3510
+ debugSelectedText();
3511
+ }, 100);
3512
+ }
3513
+ });
3514
+
3515
+ // Also update during dragging for immediate feedback
3516
+ canvas.on('object:moving', function (e) {
3517
+ if (
3518
+ debugMode &&
3519
+ e.target &&
3520
+ (e.target.type === 'textbox' ||
3521
+ e.target.type === 'text' ||
3522
+ e.target.type === 'i-text')
3523
+ ) {
3524
+ // Only update debug overlay, not full console logs (too noisy during drag)
3525
+ setTimeout(() => {
3526
+ const debug = getDebugTextMetrics(e.target);
3527
+ drawDebugOverlay(e.target, debug);
3528
+ }, 10);
3529
+ }
3530
+ });
3531
+
1182
3532
  // Log Fabric.js version
1183
3533
  console.log('Fabric.js version:', fabric.version);
1184
3534
  console.log('Canvas initialized with useOverlayEditing support');
1185
- console.log('🎨 Corner radius support added for all shapes (Triangle, Polygon, etc.)');
3535
+ console.log(
3536
+ '🔍 Debug visualization enabled - use Debug controls to analyze text metrics',
3537
+ );
3538
+ console.log(
3539
+ '🎨 Corner radius support added for all shapes (Triangle, Polygon, etc.)',
3540
+ );
1186
3541
 
1187
3542
  // Test overlay editing on double-click
1188
3543
  canvas.on('mouse:dblclick', function (options) {