@nasser-sw/fabric 7.0.1-beta16 → 7.0.1-beta17

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 (95) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/dist/index.js +1982 -649
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.min.js +1 -1
  5. package/dist/index.min.js.map +1 -1
  6. package/dist/index.min.mjs +1 -1
  7. package/dist/index.min.mjs.map +1 -1
  8. package/dist/index.mjs +1982 -649
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/index.node.cjs +1982 -649
  11. package/dist/index.node.cjs.map +1 -1
  12. package/dist/index.node.mjs +1982 -649
  13. package/dist/index.node.mjs.map +1 -1
  14. package/dist/package.json.min.mjs +1 -1
  15. package/dist/package.json.mjs +1 -1
  16. package/dist/src/shapes/IText/IText.d.ts +31 -6
  17. package/dist/src/shapes/IText/IText.d.ts.map +1 -1
  18. package/dist/src/shapes/IText/IText.min.mjs +1 -1
  19. package/dist/src/shapes/IText/IText.min.mjs.map +1 -1
  20. package/dist/src/shapes/IText/IText.mjs +495 -126
  21. package/dist/src/shapes/IText/IText.mjs.map +1 -1
  22. package/dist/src/shapes/IText/ITextBehavior.d.ts +12 -0
  23. package/dist/src/shapes/IText/ITextBehavior.d.ts.map +1 -1
  24. package/dist/src/shapes/IText/ITextBehavior.min.mjs +1 -1
  25. package/dist/src/shapes/IText/ITextBehavior.min.mjs.map +1 -1
  26. package/dist/src/shapes/IText/ITextBehavior.mjs +127 -36
  27. package/dist/src/shapes/IText/ITextBehavior.mjs.map +1 -1
  28. package/dist/src/shapes/IText/ITextClickBehavior.d.ts.map +1 -1
  29. package/dist/src/shapes/IText/ITextClickBehavior.min.mjs +1 -1
  30. package/dist/src/shapes/IText/ITextClickBehavior.min.mjs.map +1 -1
  31. package/dist/src/shapes/IText/ITextClickBehavior.mjs +21 -4
  32. package/dist/src/shapes/IText/ITextClickBehavior.mjs.map +1 -1
  33. package/dist/src/shapes/IText/ITextKeyBehavior.min.mjs +1 -1
  34. package/dist/src/shapes/IText/ITextKeyBehavior.min.mjs.map +1 -1
  35. package/dist/src/shapes/IText/ITextKeyBehavior.mjs +17 -21
  36. package/dist/src/shapes/IText/ITextKeyBehavior.mjs.map +1 -1
  37. package/dist/src/shapes/Text/Text.d.ts +69 -1
  38. package/dist/src/shapes/Text/Text.d.ts.map +1 -1
  39. package/dist/src/shapes/Text/Text.min.mjs +1 -1
  40. package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
  41. package/dist/src/shapes/Text/Text.mjs +374 -60
  42. package/dist/src/shapes/Text/Text.mjs.map +1 -1
  43. package/dist/src/shapes/Text/constants.d.ts.map +1 -1
  44. package/dist/src/shapes/Text/constants.min.mjs +1 -1
  45. package/dist/src/shapes/Text/constants.min.mjs.map +1 -1
  46. package/dist/src/shapes/Text/constants.mjs +2 -1
  47. package/dist/src/shapes/Text/constants.mjs.map +1 -1
  48. package/dist/src/shapes/Textbox.d.ts +8 -1
  49. package/dist/src/shapes/Textbox.d.ts.map +1 -1
  50. package/dist/src/shapes/Textbox.min.mjs +1 -1
  51. package/dist/src/shapes/Textbox.min.mjs.map +1 -1
  52. package/dist/src/shapes/Textbox.mjs +406 -63
  53. package/dist/src/shapes/Textbox.mjs.map +1 -1
  54. package/dist/src/text/hitTest.min.mjs +1 -1
  55. package/dist/src/text/hitTest.min.mjs.map +1 -1
  56. package/dist/src/text/hitTest.mjs +1 -198
  57. package/dist/src/text/hitTest.mjs.map +1 -1
  58. package/dist/src/text/layout.min.mjs +1 -1
  59. package/dist/src/text/layout.min.mjs.map +1 -1
  60. package/dist/src/text/layout.mjs +122 -5
  61. package/dist/src/text/layout.mjs.map +1 -1
  62. package/dist/src/text/overlayEditor.min.mjs +1 -1
  63. package/dist/src/text/overlayEditor.min.mjs.map +1 -1
  64. package/dist/src/text/overlayEditor.mjs +132 -142
  65. package/dist/src/text/overlayEditor.mjs.map +1 -1
  66. package/dist/src/text/unicode.d.ts +28 -0
  67. package/dist/src/text/unicode.d.ts.map +1 -1
  68. package/dist/src/text/unicode.min.mjs +1 -1
  69. package/dist/src/text/unicode.min.mjs.map +1 -1
  70. package/dist/src/text/unicode.mjs +294 -1
  71. package/dist/src/text/unicode.mjs.map +1 -1
  72. package/dist-extensions/src/shapes/IText/IText.d.ts +31 -6
  73. package/dist-extensions/src/shapes/IText/IText.d.ts.map +1 -1
  74. package/dist-extensions/src/shapes/IText/ITextBehavior.d.ts +12 -0
  75. package/dist-extensions/src/shapes/IText/ITextBehavior.d.ts.map +1 -1
  76. package/dist-extensions/src/shapes/IText/ITextClickBehavior.d.ts.map +1 -1
  77. package/dist-extensions/src/shapes/Text/Text.d.ts +69 -1
  78. package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
  79. package/dist-extensions/src/shapes/Text/constants.d.ts.map +1 -1
  80. package/dist-extensions/src/shapes/Textbox.d.ts +8 -1
  81. package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
  82. package/dist-extensions/src/text/unicode.d.ts +28 -0
  83. package/dist-extensions/src/text/unicode.d.ts.map +1 -1
  84. package/package.json +164 -164
  85. package/rtl-debug.html +358 -200
  86. package/src/shapes/IText/IText.ts +524 -110
  87. package/src/shapes/IText/ITextBehavior.ts +174 -80
  88. package/src/shapes/IText/ITextClickBehavior.ts +20 -6
  89. package/src/shapes/IText/ITextKeyBehavior.ts +15 -15
  90. package/src/shapes/Text/Text.ts +488 -107
  91. package/src/shapes/Text/constants.ts +4 -2
  92. package/src/shapes/Textbox.ts +414 -65
  93. package/src/text/layout.ts +150 -23
  94. package/src/text/overlayEditor.ts +148 -148
  95. package/src/text/unicode.ts +177 -2
@@ -240,9 +240,9 @@ export class OverlayEditor {
240
240
  requestAnimationFrame(() => {
241
241
  if (!this.isDestroyed) {
242
242
  this.applyOverlayStyle();
243
- console.log(
244
- '📐 Height changed - rechecking alignment after repositioning:',
245
- );
243
+ // console.log(
244
+ // '📐 Height changed - rechecking alignment after repositioning:',
245
+ // );
246
246
  }
247
247
  });
248
248
  }
@@ -340,7 +340,7 @@ export class OverlayEditor {
340
340
 
341
341
  // Special handling for text objects loaded from JSON - ensure they're properly initialized
342
342
  if (target.dirty !== false && target.initDimensions) {
343
- console.log('🔧 Ensuring text object is properly initialized before overlay editing');
343
+ // console.log('🔧 Ensuring text object is properly initialized before overlay editing');
344
344
  // Force re-initialization if the text object seems to be in a dirty state
345
345
  target.initDimensions();
346
346
  }
@@ -358,11 +358,11 @@ export class OverlayEditor {
358
358
  const autoDetectedDirection = this.firstStrongDir(this.textarea.value || '');
359
359
 
360
360
  // DEBUG: Log alignment details
361
- console.log('🔍 ALIGNMENT DEBUG:');
362
- console.log(' Fabric textAlign:', textAlign);
363
- console.log(' Fabric direction:', (target as any).direction);
364
- console.log(' Text content:', JSON.stringify(target.text));
365
- console.log(' Detected direction:', autoDetectedDirection);
361
+ // console.log('🔍 ALIGNMENT DEBUG:');
362
+ // console.log(' Fabric textAlign:', textAlign);
363
+ // console.log(' Fabric direction:', (target as any).direction);
364
+ // console.log(' Text content:', JSON.stringify(target.text));
365
+ // console.log(' Detected direction:', autoDetectedDirection);
366
366
 
367
367
  // Map fabric.js justify to CSS
368
368
  if (textAlign.includes('justify')) {
@@ -380,7 +380,7 @@ export class OverlayEditor {
380
380
  // If text is RTL but fabric says justify-left, override to justify-right for better UX
381
381
  if (autoDetectedDirection === 'rtl') {
382
382
  this.textarea.style.textAlignLast = 'right';
383
- console.log(' → Overrode justify-left to justify-right for RTL text');
383
+ // console.log(' → Overrode justify-left to justify-right for RTL text');
384
384
  } else {
385
385
  this.textarea.style.textAlignLast = 'left';
386
386
  }
@@ -388,7 +388,7 @@ export class OverlayEditor {
388
388
  // If text is LTR but fabric says justify-right, override to justify-left for better UX
389
389
  if (autoDetectedDirection === 'ltr') {
390
390
  this.textarea.style.textAlignLast = 'left';
391
- console.log(' → Overrode justify-right to justify-left for LTR text');
391
+ // console.log(' → Overrode justify-right to justify-left for LTR text');
392
392
  } else {
393
393
  this.textarea.style.textAlignLast = 'right';
394
394
  }
@@ -408,16 +408,16 @@ export class OverlayEditor {
408
408
  (this.textarea.style as any).textJustifyTrim = 'none';
409
409
  (this.textarea.style as any).textAutospace = 'none';
410
410
 
411
- console.log(' → Applied justify alignment:', textAlign, 'with last-line:', this.textarea.style.textAlignLast);
411
+ // console.log(' → Applied justify alignment:', textAlign, 'with last-line:', this.textarea.style.textAlignLast);
412
412
  } catch (error) {
413
- console.warn(' → Justify setup failed, falling back to standard alignment:', error);
413
+ // console.warn(' → Justify setup failed, falling back to standard alignment:', error);
414
414
  cssTextAlign = textAlign.replace('justify-', '').replace('justify', 'left');
415
415
  }
416
416
  } else {
417
417
  this.textarea.style.textAlignLast = 'auto';
418
418
  (this.textarea.style as any).textJustify = 'auto';
419
419
  (this.textarea.style as any).wordSpacing = 'normal';
420
- console.log(' → Applied standard alignment:', cssTextAlign);
420
+ // console.log(' → Applied standard alignment:', cssTextAlign);
421
421
  }
422
422
 
423
423
  this.textarea.style.textAlign = cssTextAlign;
@@ -444,37 +444,37 @@ export class OverlayEditor {
444
444
  this.textarea.style.hyphens = 'none';
445
445
 
446
446
  // DEBUG: Log final CSS properties
447
- console.log('🎨 FINAL TEXTAREA CSS:');
448
- console.log(' textAlign:', this.textarea.style.textAlign);
449
- console.log(' textAlignLast:', this.textarea.style.textAlignLast);
450
- console.log(' direction:', this.textarea.style.direction);
451
- console.log(' unicodeBidi:', this.textarea.style.unicodeBidi);
452
- console.log(' width:', this.textarea.style.width);
453
- console.log(' textJustify:', (this.textarea.style as any).textJustify);
454
- console.log(' wordSpacing:', (this.textarea.style as any).wordSpacing);
455
- console.log(' whiteSpace:', this.textarea.style.whiteSpace);
447
+ // console.log('🎨 FINAL TEXTAREA CSS:');
448
+ // console.log(' textAlign:', this.textarea.style.textAlign);
449
+ // console.log(' textAlignLast:', this.textarea.style.textAlignLast);
450
+ // console.log(' direction:', this.textarea.style.direction);
451
+ // console.log(' unicodeBidi:', this.textarea.style.unicodeBidi);
452
+ // console.log(' width:', this.textarea.style.width);
453
+ // console.log(' textJustify:', (this.textarea.style as any).textJustify);
454
+ // console.log(' wordSpacing:', (this.textarea.style as any).wordSpacing);
455
+ // console.log(' whiteSpace:', this.textarea.style.whiteSpace);
456
456
 
457
457
  // If justify, log Fabric object dimensions for comparison
458
458
  if (textAlign.includes('justify')) {
459
- console.log('🔧 FABRIC OBJECT JUSTIFY INFO:');
460
- console.log(' Fabric width:', (target as any).width);
461
- console.log(' Fabric calcTextWidth:', (target as any).calcTextWidth?.());
462
- console.log(' Fabric textAlign:', (target as any).textAlign);
463
- console.log(' Text lines:', (target as any).textLines);
459
+ // console.log('🔧 FABRIC OBJECT JUSTIFY INFO:');
460
+ // console.log(' Fabric width:', (target as any).width);
461
+ // console.log(' Fabric calcTextWidth:', (target as any).calcTextWidth?.());
462
+ // console.log(' Fabric textAlign:', (target as any).textAlign);
463
+ // console.log(' Text lines:', (target as any).textLines);
464
464
  }
465
465
 
466
466
  // Debug font properties matching
467
- console.log('🔤 FONT PROPERTIES COMPARISON:');
468
- console.log(' Fabric fontFamily:', target.fontFamily);
469
- console.log(' Fabric fontWeight:', target.fontWeight);
470
- console.log(' Fabric fontStyle:', target.fontStyle);
471
- console.log(' Fabric fontSize:', target.fontSize);
472
- console.log(' → Textarea fontFamily:', this.textarea.style.fontFamily);
473
- console.log(' → Textarea fontWeight:', this.textarea.style.fontWeight);
474
- console.log(' → Textarea fontStyle:', this.textarea.style.fontStyle);
475
- console.log(' → Textarea fontSize:', this.textarea.style.fontSize);
467
+ // console.log('🔤 FONT PROPERTIES COMPARISON:');
468
+ // console.log(' Fabric fontFamily:', target.fontFamily);
469
+ // console.log(' Fabric fontWeight:', target.fontWeight);
470
+ // console.log(' Fabric fontStyle:', target.fontStyle);
471
+ // console.log(' Fabric fontSize:', target.fontSize);
472
+ // console.log(' → Textarea fontFamily:', this.textarea.style.fontFamily);
473
+ // console.log(' → Textarea fontWeight:', this.textarea.style.fontWeight);
474
+ // console.log(' → Textarea fontStyle:', this.textarea.style.fontStyle);
475
+ // console.log(' → Textarea fontSize:', this.textarea.style.fontSize);
476
476
 
477
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
477
+ // console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
478
478
 
479
479
  // Enhanced font rendering to better match fabric.js canvas rendering
480
480
  // Default to auto for more natural rendering
@@ -491,12 +491,12 @@ export class OverlayEditor {
491
491
  if (isBold) {
492
492
  (this.textarea.style as any).webkitFontSmoothing = 'subpixel-antialiased';
493
493
  (this.textarea.style as any).mozOsxFontSmoothing = 'unset';
494
- console.log('🔤 Applied enhanced bold rendering for better thickness matching');
494
+ // console.log('🔤 Applied enhanced bold rendering for better thickness matching');
495
495
  }
496
496
 
497
- console.log('🎨 FONT SMOOTHING APPLIED:');
498
- console.log(' webkitFontSmoothing:', (this.textarea.style as any).webkitFontSmoothing);
499
- console.log(' mozOsxFontSmoothing:', (this.textarea.style as any).mozOsxFontSmoothing);
497
+ // console.log('🎨 FONT SMOOTHING APPLIED:');
498
+ // console.log(' webkitFontSmoothing:', (this.textarea.style as any).webkitFontSmoothing);
499
+ // console.log(' mozOsxFontSmoothing:', (this.textarea.style as any).mozOsxFontSmoothing);
500
500
 
501
501
 
502
502
  // Initial bounds are set correctly by Fabric.js - don't force update here
@@ -527,26 +527,26 @@ export class OverlayEditor {
527
527
  height: canvasBounds.height * zoom,
528
528
  };
529
529
 
530
- console.log('🔍 BOUNDING BOX COMPARISON:');
531
- console.log('📦 Textarea Rect:', {
532
- left: Math.round(textareaRect.left * 100) / 100,
533
- top: Math.round(textareaRect.top * 100) / 100,
534
- width: Math.round(textareaRect.width * 100) / 100,
535
- height: Math.round(textareaRect.height * 100) / 100,
536
- });
537
- console.log('📦 Host Div Rect:', {
538
- left: Math.round(hostRect.left * 100) / 100,
539
- top: Math.round(hostRect.top * 100) / 100,
540
- width: Math.round(hostRect.width * 100) / 100,
541
- height: Math.round(hostRect.height * 100) / 100,
542
- });
543
- console.log('📦 Canvas Object Bounds (screen):', {
544
- left: Math.round(screenObjectBounds.left * 100) / 100,
545
- top: Math.round(screenObjectBounds.top * 100) / 100,
546
- width: Math.round(screenObjectBounds.width * 100) / 100,
547
- height: Math.round(screenObjectBounds.height * 100) / 100,
548
- });
549
- console.log('📦 Canvas Object Bounds (canvas):', canvasBounds);
530
+ // console.log('🔍 BOUNDING BOX COMPARISON:');
531
+ // console.log('📦 Textarea Rect:', {
532
+ // left: Math.round(textareaRect.left * 100) / 100,
533
+ // top: Math.round(textareaRect.top * 100) / 100,
534
+ // width: Math.round(textareaRect.width * 100) / 100,
535
+ // height: Math.round(textareaRect.height * 100) / 100,
536
+ // });
537
+ // console.log('📦 Host Div Rect:', {
538
+ // left: Math.round(hostRect.left * 100) / 100,
539
+ // top: Math.round(hostRect.top * 100) / 100,
540
+ // width: Math.round(hostRect.width * 100) / 100,
541
+ // height: Math.round(hostRect.height * 100) / 100,
542
+ // });
543
+ // console.log('📦 Canvas Object Bounds (screen):', {
544
+ // left: Math.round(screenObjectBounds.left * 100) / 100,
545
+ // top: Math.round(screenObjectBounds.top * 100) / 100,
546
+ // width: Math.round(screenObjectBounds.width * 100) / 100,
547
+ // height: Math.round(screenObjectBounds.height * 100) / 100,
548
+ // });
549
+ // console.log('📦 Canvas Object Bounds (canvas):', canvasBounds);
550
550
 
551
551
  // Calculate differences
552
552
  const hostVsObject = {
@@ -571,8 +571,8 @@ export class OverlayEditor {
571
571
  100,
572
572
  };
573
573
 
574
- console.log('📏 Host Div vs Canvas Object Diff:', hostVsObject);
575
- console.log('📏 Textarea vs Canvas Object Diff:', textareaVsObject);
574
+ // console.log('📏 Host Div vs Canvas Object Diff:', hostVsObject);
575
+ // console.log('📏 Textarea vs Canvas Object Diff:', textareaVsObject);
576
576
 
577
577
  // Check if they're aligned (within 2px tolerance)
578
578
  const tolerance = 2;
@@ -588,18 +588,18 @@ export class OverlayEditor {
588
588
  Math.abs(textareaVsObject.widthDiff) < tolerance &&
589
589
  Math.abs(textareaVsObject.heightDiff) < tolerance;
590
590
 
591
- console.log(
592
- hostAligned
593
- ? '✅ Host Div ALIGNED with canvas object'
594
- : '❌ Host Div MISALIGNED with canvas object',
595
- );
596
- console.log(
597
- textareaAligned
598
- ? '✅ Textarea ALIGNED with canvas object'
599
- : '❌ Textarea MISALIGNED with canvas object',
600
- );
601
- console.log('🔍 Zoom:', zoom, 'Viewport Transform:', vpt);
602
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
591
+ // console.log(
592
+ // hostAligned
593
+ // ? '✅ Host Div ALIGNED with canvas object'
594
+ // : '❌ Host Div MISALIGNED with canvas object',
595
+ // );
596
+ // console.log(
597
+ // textareaAligned
598
+ // ? '✅ Textarea ALIGNED with canvas object'
599
+ // : '❌ Textarea MISALIGNED with canvas object',
600
+ // );
601
+ // console.log('🔍 Zoom:', zoom, 'Viewport Transform:', vpt);
602
+ // console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
603
603
  }
604
604
 
605
605
  /**
@@ -609,44 +609,44 @@ export class OverlayEditor {
609
609
  const target = this.target;
610
610
  const text = this.textarea.value;
611
611
 
612
- console.log('📝 TEXT WRAPPING COMPARISON:');
613
- console.log('📄 Text Content:', `"${text}"`);
614
- console.log('📄 Text Length:', text.length);
612
+ // console.log('📝 TEXT WRAPPING COMPARISON:');
613
+ // console.log('📄 Text Content:', `"${text}"`);
614
+ // console.log('📄 Text Length:', text.length);
615
615
 
616
616
  // Analyze line breaks
617
617
  const explicitLines = text.split('\n');
618
- console.log('📄 Explicit Lines (\\n):', explicitLines.length);
618
+ // console.log('📄 Explicit Lines (\\n):', explicitLines.length);
619
619
  explicitLines.forEach((line, i) => {
620
- console.log(` Line ${i + 1}: "${line}" (${line.length} chars)`);
620
+ // console.log(` Line ${i + 1}: "${line}" (${line.length} chars)`);
621
621
  });
622
622
 
623
623
  // Get textarea computed styles for wrapping analysis
624
624
  const textareaStyles = window.getComputedStyle(this.textarea);
625
- console.log('📐 Textarea Wrapping Styles:');
626
- console.log(' width:', textareaStyles.width);
627
- console.log(' fontSize:', textareaStyles.fontSize);
628
- console.log(' fontFamily:', textareaStyles.fontFamily);
629
- console.log(' fontWeight:', textareaStyles.fontWeight);
630
- console.log(' letterSpacing:', textareaStyles.letterSpacing);
631
- console.log(' lineHeight:', textareaStyles.lineHeight);
632
- console.log(' whiteSpace:', textareaStyles.whiteSpace);
633
- console.log(' wordWrap:', textareaStyles.wordWrap);
634
- console.log(' overflowWrap:', textareaStyles.overflowWrap);
635
- console.log(' direction:', textareaStyles.direction);
636
- console.log(' textAlign:', textareaStyles.textAlign);
625
+ // console.log('📐 Textarea Wrapping Styles:');
626
+ // console.log(' width:', textareaStyles.width);
627
+ // console.log(' fontSize:', textareaStyles.fontSize);
628
+ // console.log(' fontFamily:', textareaStyles.fontFamily);
629
+ // console.log(' fontWeight:', textareaStyles.fontWeight);
630
+ // console.log(' letterSpacing:', textareaStyles.letterSpacing);
631
+ // console.log(' lineHeight:', textareaStyles.lineHeight);
632
+ // console.log(' whiteSpace:', textareaStyles.whiteSpace);
633
+ // console.log(' wordWrap:', textareaStyles.wordWrap);
634
+ // console.log(' overflowWrap:', textareaStyles.overflowWrap);
635
+ // console.log(' direction:', textareaStyles.direction);
636
+ // console.log(' textAlign:', textareaStyles.textAlign);
637
637
 
638
638
  // Get Fabric text object properties for comparison
639
- console.log('📐 Fabric Text Object Properties:');
640
- console.log(' width:', (target as any).width);
641
- console.log(' fontSize:', target.fontSize);
642
- console.log(' fontFamily:', target.fontFamily);
643
- console.log(' fontWeight:', target.fontWeight);
644
- console.log(' charSpacing:', target.charSpacing);
645
- console.log(' lineHeight:', target.lineHeight);
646
- console.log(' direction:', (target as any).direction);
647
- console.log(' textAlign:', (target as any).textAlign);
648
- console.log(' scaleX:', target.scaleX);
649
- console.log(' scaleY:', target.scaleY);
639
+ // console.log('📐 Fabric Text Object Properties:');
640
+ // console.log(' width:', (target as any).width);
641
+ // console.log(' fontSize:', target.fontSize);
642
+ // console.log(' fontFamily:', target.fontFamily);
643
+ // console.log(' fontWeight:', target.fontWeight);
644
+ // console.log(' charSpacing:', target.charSpacing);
645
+ // console.log(' lineHeight:', target.lineHeight);
646
+ // console.log(' direction:', (target as any).direction);
647
+ // console.log(' textAlign:', (target as any).textAlign);
648
+ // console.log(' scaleX:', target.scaleX);
649
+ // console.log(' scaleY:', target.scaleY);
650
650
 
651
651
  // Calculate effective dimensions for comparison - use actual rendered width
652
652
  // **THE FIX:** Use getBoundingRect to get the *actual rendered width* of the Fabric object.
@@ -659,15 +659,15 @@ export class OverlayEditor {
659
659
  textareaComputedWidth / this.canvas.getZoom();
660
660
  const widthDiff = Math.abs(textareaEffectiveWidth - fabricEffectiveWidth);
661
661
 
662
- console.log('📏 Effective Width Comparison:');
663
- console.log(' Textarea Effective Width:', textareaEffectiveWidth);
664
- console.log(' Fabric Effective Width:', fabricEffectiveWidth);
665
- console.log(' Width Difference:', widthDiff.toFixed(2) + 'px');
666
- console.log(
667
- widthDiff < 1
668
- ? '✅ Widths MATCH for wrapping'
669
- : '❌ Width MISMATCH may cause different wrapping',
670
- );
662
+ // console.log('📏 Effective Width Comparison:');
663
+ // console.log(' Textarea Effective Width:', textareaEffectiveWidth);
664
+ // console.log(' Fabric Effective Width:', fabricEffectiveWidth);
665
+ // console.log(' Width Difference:', widthDiff.toFixed(2) + 'px');
666
+ // console.log(
667
+ // widthDiff < 1
668
+ // ? '✅ Widths MATCH for wrapping'
669
+ // : '❌ Width MISMATCH may cause different wrapping',
670
+ // );
671
671
 
672
672
  // Check text direction and bidi handling
673
673
  const hasRTLText =
@@ -676,12 +676,12 @@ export class OverlayEditor {
676
676
  );
677
677
  const hasBidiText = /[\u0590-\u06FF]/.test(text) && /[a-zA-Z]/.test(text);
678
678
 
679
- console.log('🌍 Text Direction Analysis:');
680
- console.log(' Has RTL characters:', hasRTLText);
681
- console.log(' Has mixed Bidi text:', hasBidiText);
682
- console.log(' Textarea direction:', textareaStyles.direction);
683
- console.log(' Fabric direction:', (target as any).direction || 'auto');
684
- console.log(' Textarea unicodeBidi:', textareaStyles.unicodeBidi);
679
+ // console.log('🌍 Text Direction Analysis:');
680
+ // console.log(' Has RTL characters:', hasRTLText);
681
+ // console.log(' Has mixed Bidi text:', hasBidiText);
682
+ // console.log(' Textarea direction:', textareaStyles.direction);
683
+ // console.log(' Fabric direction:', (target as any).direction || 'auto');
684
+ // console.log(' Textarea unicodeBidi:', textareaStyles.unicodeBidi);
685
685
 
686
686
  // Measure actual rendered line count
687
687
  const textareaScrollHeight = this.textarea.scrollHeight;
@@ -692,23 +692,23 @@ export class OverlayEditor {
692
692
  textareaScrollHeight / textareaLineHeight,
693
693
  );
694
694
 
695
- console.log('📊 Line Count Analysis:');
696
- console.log(' Textarea scrollHeight:', textareaScrollHeight);
697
- console.log(' Textarea lineHeight:', textareaLineHeight);
698
- console.log(' Estimated rendered lines:', estimatedTextareaLines);
699
- console.log(' Explicit line breaks:', explicitLines.length);
695
+ // console.log('📊 Line Count Analysis:');
696
+ // console.log(' Textarea scrollHeight:', textareaScrollHeight);
697
+ // console.log(' Textarea lineHeight:', textareaLineHeight);
698
+ // console.log(' Estimated rendered lines:', estimatedTextareaLines);
699
+ // console.log(' Explicit line breaks:', explicitLines.length);
700
700
 
701
701
  if (estimatedTextareaLines > explicitLines.length) {
702
- console.log('🔄 Text wrapping detected in textarea');
703
- console.log(
704
- ' Wrapped lines:',
705
- estimatedTextareaLines - explicitLines.length,
706
- );
702
+ // console.log('🔄 Text wrapping detected in textarea');
703
+ // console.log(
704
+ // ' Wrapped lines:',
705
+ // estimatedTextareaLines - explicitLines.length,
706
+ // );
707
707
  } else {
708
- console.log('📏 No text wrapping in textarea');
708
+ // console.log('📏 No text wrapping in textarea');
709
709
  }
710
710
 
711
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
711
+ // console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
712
712
  }
713
713
 
714
714
 
@@ -795,28 +795,28 @@ export class OverlayEditor {
795
795
 
796
796
  // Handle commit/cancel after restoring visibility
797
797
  if (commit && !this.isComposing) {
798
- const finalText = this.textarea.value;
799
-
800
- // Auto-detect text direction and update fabric object if needed
801
- const detectedDirection = this.firstStrongDir(finalText);
802
- const currentDirection = (this.target as any).direction || 'ltr';
803
- const hasExplicitDirection =
804
- currentDirection && currentDirection !== 'inherit';
805
-
806
- // Only update direction when not explicitly set on the object
807
- if (!hasExplicitDirection && detectedDirection && detectedDirection !== currentDirection) {
808
- console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
809
- console.log(` Text content: "${finalText.substring(0, 50)}..."`);
810
-
811
- // Update the fabric object's direction
812
- (this.target as any).set('direction', detectedDirection);
798
+ const finalText = this.textarea.value;
799
+
800
+ // Auto-detect text direction and update fabric object if needed
801
+ const detectedDirection = this.firstStrongDir(finalText);
802
+ const currentDirection = (this.target as any).direction || 'ltr';
803
+ const hasExplicitDirection =
804
+ currentDirection && currentDirection !== 'inherit';
805
+
806
+ // Only update direction when not explicitly set on the object
807
+ if (!hasExplicitDirection && detectedDirection && detectedDirection !== currentDirection) {
808
+ // console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
809
+ // console.log(` Text content: "${finalText.substring(0, 50)}..."`);
810
+
811
+ // Update the fabric object's direction
812
+ (this.target as any).set('direction', detectedDirection);
813
813
 
814
814
  // Force a re-render to apply the direction change
815
815
  this.canvas.requestRenderAll();
816
816
 
817
- console.log(`✅ Fabric object direction updated to: ${detectedDirection}`);
817
+ // console.log(`✅ Fabric object direction updated to: ${detectedDirection}`);
818
818
  } else {
819
- console.log(`📝 Overlay Exit: Direction unchanged (${currentDirection}), text: "${finalText.substring(0, 30)}..."`);
819
+ // console.log(`📝 Overlay Exit: Direction unchanged (${currentDirection}), text: "${finalText.substring(0, 30)}..."`);
820
820
  }
821
821
 
822
822
  if (this.onCommit) {
@@ -445,11 +445,186 @@ export function getLineBreakClass(char: string): string {
445
445
  export function isLineBreakAllowed(before: string, after: string): boolean {
446
446
  const beforeClass = getLineBreakClass(before);
447
447
  const afterClass = getLineBreakClass(after);
448
-
448
+
449
449
  // Simplified line breaking rules
450
450
  if (beforeClass === 'OP') return false; // Don't break after opening
451
451
  if (afterClass === 'CL') return false; // Don't break before closing
452
452
  if (beforeClass === 'SP') return true; // Always allow break after space
453
-
453
+
454
454
  return true; // Default allow
455
+ }
456
+
457
+ // ============================================================================
458
+ // Arabic Kashida (Tatweel) Support
459
+ // ============================================================================
460
+
461
+ /**
462
+ * Arabic Tatweel (kashida) character used for justification
463
+ */
464
+ export const ARABIC_TATWEEL = '\u0640';
465
+
466
+ /**
467
+ * Arabic letters that do NOT connect to the following letter (non-connecting on left).
468
+ * These letters cannot have kashida inserted after them.
469
+ * ا (Alef), د (Dal), ذ (Thal), ر (Ra), ز (Zay), و (Waw), ة (Teh Marbuta), ء (Hamza)
470
+ */
471
+ const ARABIC_NON_CONNECTING = new Set([
472
+ '\u0627', // Alef
473
+ '\u062F', // Dal
474
+ '\u0630', // Thal
475
+ '\u0631', // Ra
476
+ '\u0632', // Zay
477
+ '\u0648', // Waw
478
+ '\u0629', // Teh Marbuta
479
+ '\u0621', // Hamza
480
+ '\u0622', // Alef with Madda
481
+ '\u0623', // Alef with Hamza Above
482
+ '\u0625', // Alef with Hamza Below
483
+ '\u0672', // Alef with Wavy Hamza Above
484
+ '\u0673', // Alef with Wavy Hamza Below
485
+ '\u0675', // High Hamza Alef
486
+ '\u0688', // Dal with Small Tah
487
+ '\u0689', // Dal with Ring
488
+ '\u068A', // Dal with Dot Below
489
+ '\u068B', // Dal with Dot Below and Small Tah
490
+ '\u068C', // Dahal
491
+ '\u068D', // Ddahal
492
+ '\u068E', // Dul
493
+ '\u068F', // Dal with Three Dots Above Downwards
494
+ '\u0690', // Dal with Four Dots Above
495
+ '\u0691', // Rreh
496
+ '\u0692', // Reh with Small V
497
+ '\u0693', // Reh with Ring
498
+ '\u0694', // Reh with Dot Below
499
+ '\u0695', // Reh with Small V Below
500
+ '\u0696', // Reh with Dot Below and Dot Above
501
+ '\u0697', // Reh with Two Dots Above
502
+ '\u0698', // Jeh
503
+ '\u0699', // Reh with Four Dots Above
504
+ '\u06C4', // Waw with Ring
505
+ '\u06C5', // Kirghiz Oe
506
+ '\u06C6', // Oe
507
+ '\u06C7', // U
508
+ '\u06C8', // Yu
509
+ '\u06C9', // Kirghiz Yu
510
+ '\u06CA', // Waw with Two Dots Above
511
+ '\u06CB', // Ve
512
+ '\u06CD', // Yeh with Tail
513
+ '\u06CF', // Waw with Dot Above
514
+ ]);
515
+
516
+ /**
517
+ * Check if a character is an Arabic letter (main Arabic block + extended)
518
+ */
519
+ export function isArabicLetter(char: string): boolean {
520
+ if (!char) return false;
521
+ const code = char.charCodeAt(0);
522
+ // Arabic: U+0600-U+06FF (main block)
523
+ // Arabic Supplement: U+0750-U+077F
524
+ // Arabic Extended-A: U+08A0-U+08FF
525
+ return (
526
+ (code >= 0x0620 && code <= 0x064A) || // Main letters
527
+ (code >= 0x066E && code <= 0x06D3) || // Extended letters
528
+ (code >= 0x0750 && code <= 0x077F) || // Arabic Supplement
529
+ (code >= 0x08A0 && code <= 0x08FF) // Arabic Extended-A
530
+ );
531
+ }
532
+
533
+ /**
534
+ * Check if kashida can be inserted between two characters.
535
+ * Kashida can only be inserted:
536
+ * - Between two Arabic letters
537
+ * - After a letter that connects to the next (not in ARABIC_NON_CONNECTING)
538
+ * - Not at word boundaries (no whitespace before/after)
539
+ */
540
+ // Alef variants that form ligatures with lam
541
+ const ARABIC_ALEF_VARIANTS = new Set([
542
+ '\u0627', // ا ALEF
543
+ '\u0623', // أ ALEF WITH HAMZA ABOVE
544
+ '\u0625', // إ ALEF WITH HAMZA BELOW
545
+ '\u0622', // آ ALEF WITH MADDA ABOVE
546
+ '\u0671', // ٱ ALEF WASLA
547
+ ]);
548
+
549
+ // Lam character
550
+ const ARABIC_LAM = '\u0644'; // ل
551
+
552
+ export function canInsertKashida(prevChar: string, nextChar: string): boolean {
553
+ if (!prevChar || !nextChar) return false;
554
+
555
+ // Can't insert at whitespace boundaries
556
+ if (/\s/.test(prevChar) || /\s/.test(nextChar)) return false;
557
+
558
+ // Both must be Arabic letters
559
+ if (!isArabicLetter(prevChar) || !isArabicLetter(nextChar)) return false;
560
+
561
+ // Previous char must connect to the next (not be non-connecting)
562
+ if (ARABIC_NON_CONNECTING.has(prevChar)) return false;
563
+
564
+ // NEVER insert kashida between lam and alef - they form a ligature (لا)
565
+ if (prevChar === ARABIC_LAM && ARABIC_ALEF_VARIANTS.has(nextChar)) return false;
566
+
567
+ return true;
568
+ }
569
+
570
+ /**
571
+ * Represents a valid kashida insertion point
572
+ */
573
+ export interface KashidaPoint {
574
+ /** Index in the grapheme array where kashida can be inserted after */
575
+ charIndex: number;
576
+ /** Priority for kashida insertion (higher = insert here first) */
577
+ priority: number;
578
+ }
579
+
580
+ /**
581
+ * Find all valid kashida insertion points in a line of text.
582
+ * Returns points sorted by priority (highest first).
583
+ *
584
+ * Priority rules (similar to Adobe Illustrator):
585
+ * 1. Between connected letters (ب + ب = highest)
586
+ * 2. Prefer middle of words over edges
587
+ * 3. Avoid inserting right before/after spaces
588
+ */
589
+ export function findKashidaPoints(graphemes: string[]): KashidaPoint[] {
590
+ const points: KashidaPoint[] = [];
591
+
592
+ for (let i = 0; i < graphemes.length - 1; i++) {
593
+ const prev = graphemes[i];
594
+ const next = graphemes[i + 1];
595
+
596
+ if (canInsertKashida(prev, next)) {
597
+ // Calculate priority based on position in word
598
+ let priority = 1;
599
+
600
+ // Find word boundaries
601
+ let wordStart = i;
602
+ let wordEnd = i + 1;
603
+
604
+ while (wordStart > 0 && !isWhitespace(graphemes[wordStart - 1])) {
605
+ wordStart--;
606
+ }
607
+ while (wordEnd < graphemes.length && !isWhitespace(graphemes[wordEnd])) {
608
+ wordEnd++;
609
+ }
610
+
611
+ const wordLength = wordEnd - wordStart;
612
+ const posInWord = i - wordStart;
613
+
614
+ // Higher priority for middle positions
615
+ const distFromEdge = Math.min(posInWord, wordLength - 1 - posInWord);
616
+ priority = distFromEdge + 1;
617
+
618
+ // Boost priority for longer words
619
+ if (wordLength > 4) priority += 1;
620
+ if (wordLength > 6) priority += 1;
621
+
622
+ points.push({ charIndex: i, priority });
623
+ }
624
+ }
625
+
626
+ // Sort by priority descending
627
+ points.sort((a, b) => b.priority - a.priority);
628
+
629
+ return points;
455
630
  }