@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
@@ -8,7 +8,7 @@ import { classRegistry } from '../../ClassRegistry.mjs';
8
8
  import { graphemeSplit } from '../../util/lang_string.mjs';
9
9
  import { createCanvasElementFor } from '../../util/misc/dom.mjs';
10
10
  import { layoutText } from '../../text/layout.mjs';
11
- import { segmentGraphemes } from '../../text/unicode.mjs';
11
+ import { findKashidaPoints, ARABIC_TATWEEL, segmentGraphemes } from '../../text/unicode.mjs';
12
12
  import { hasStyleChanged, stylesToArray, stylesFromArray } from '../../util/misc/textStyles.mjs';
13
13
  import { getPathSegmentsInfo, getPointOnPath } from '../../util/path/index.mjs';
14
14
  import '../Object/FabricObject.mjs';
@@ -64,6 +64,12 @@ class FabricText extends StyledText {
64
64
  * @protected
65
65
  */
66
66
  _defineProperty(this, "__charBounds", []);
67
+ /**
68
+ * contains kashida extension info for each line.
69
+ * Each entry contains { charIndex, width } for characters that have kashida extensions.
70
+ * @protected
71
+ */
72
+ _defineProperty(this, "__kashidaInfo", []);
67
73
  Object.assign(this, FabricText.ownDefaults);
68
74
  this.setOptions(options);
69
75
  if (!this.styles) {
@@ -182,11 +188,31 @@ class FabricText extends StyledText {
182
188
  }
183
189
 
184
190
  /**
185
- * Enlarge space boxes and shift the others for justify alignment
191
+ * Enlarge space boxes and shift the others for justify alignment.
192
+ * Supports Arabic kashida (tatweel) justification when kashida property is set.
193
+ * When kashida is enabled, actual tatweel characters are inserted into the text.
186
194
  */
187
195
  enlargeSpaces() {
188
- let diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces;
196
+ // console.log('=== enlargeSpaces START ===');
197
+ // console.log('this.kashida:', this.kashida);
198
+
199
+ // Kashida ratios: proportion of extra space distributed via kashida vs space expansion
200
+ const kashidaRatios = {
201
+ none: 0,
202
+ short: 0.25,
203
+ medium: 0.5,
204
+ long: 0.75,
205
+ stylistic: 1.0
206
+ };
207
+ const kashidaRatio = kashidaRatios[this.kashida] || 0;
208
+ // console.log('kashidaRatio:', kashidaRatio);
209
+
210
+ // Reset kashida info
211
+ this.__kashidaInfo = [];
189
212
  for (let i = 0, len = this._textLines.length; i < len; i++) {
213
+ // Initialize kashida info for this line
214
+ this.__kashidaInfo[i] = [];
215
+
190
216
  // Check if this line should be justified
191
217
  const hasTextAfter = this._textLines.slice(i + 1).some(line => {
192
218
  const lineText = Array.isArray(line) ? line.join('') : line;
@@ -196,33 +222,121 @@ class FabricText extends StyledText {
196
222
  const isLastLine = i === len - 1 || this.isEndOfWrapping(i) || isVisualLastLine;
197
223
  const shouldJustifyLine = this.textAlign.includes('justify') && !isLastLine;
198
224
  if (!shouldJustifyLine) {
225
+ // console.log(` Line ${i}: skipped (not justified)`);
199
226
  continue;
200
227
  }
201
- accumulatedSpace = 0;
202
- line = this._textLines[i];
203
- currentLineWidth = this.getLineWidth(i);
204
- if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) {
205
- numberOfSpaces = spaces.length;
206
- diffSpace = (this.width - currentLineWidth) / numberOfSpaces;
207
-
208
- // Same logic for both LTR and RTL:
209
- // Expand space widths and shift subsequent characters
210
- // The rendering handles direction via ctx.direction
211
- for (let j = 0; j <= line.length; j++) {
212
- charBound = this.__charBounds[i][j];
213
- if (charBound) {
214
- if (this._reSpaceAndTab.test(line[j])) {
215
- charBound.width += diffSpace;
216
- charBound.kernedWidth += diffSpace;
217
- charBound.left += accumulatedSpace;
218
- accumulatedSpace += diffSpace;
219
- } else {
220
- charBound.left += accumulatedSpace;
228
+ const line = this._textLines[i];
229
+ const currentLineWidth = this.getLineWidth(i);
230
+ const totalExtraSpace = this.width - currentLineWidth;
231
+ // console.log(` Line ${i}: width=${this.width}, lineWidth=${currentLineWidth}, extraSpace=${totalExtraSpace}`);
232
+
233
+ if (totalExtraSpace <= 0) {
234
+ // console.log(` Line ${i}: skipped (no extra space)`);
235
+ continue;
236
+ }
237
+
238
+ // Find spaces for space expansion
239
+ const spaces = this.textLines[i].match(this._reSpacesAndTabs);
240
+ const numberOfSpaces = spaces ? spaces.length : 0;
241
+
242
+ // Find kashida points if enabled
243
+ const kashidaPoints = kashidaRatio > 0 ? findKashidaPoints(line) : [];
244
+ const hasKashidaPoints = kashidaPoints.length > 0;
245
+
246
+ // Calculate space distribution
247
+ let kashidaSpace = 0;
248
+ if (hasKashidaPoints && kashidaRatio > 0) {
249
+ // Distribute between kashida and spaces
250
+ kashidaSpace = totalExtraSpace * kashidaRatio;
251
+ }
252
+
253
+ // Calculate per-kashida and per-space widths
254
+ const perKashidaWidth = hasKashidaPoints ? kashidaSpace / kashidaPoints.length : 0;
255
+
256
+ // If kashida is enabled, insert tatweel characters into the text
257
+ if (hasKashidaPoints && perKashidaWidth > 0) {
258
+ // console.log(`=== Inserting kashida for line ${i} ===`);
259
+ // console.log(` kashidaPoints: ${kashidaPoints.length}, perKashidaWidth: ${perKashidaWidth}`);
260
+
261
+ // Sort by charIndex descending to insert from end (so indices stay valid)
262
+ const sortedPoints = [...kashidaPoints].sort((a, b) => b.charIndex - a.charIndex);
263
+
264
+ // Calculate how many tatweels to insert per point
265
+ // Measure tatweel width to determine count
266
+ const ctx = getMeasuringContext();
267
+ // console.log(` getMeasuringContext: ${ctx ? 'OK' : 'NULL'}`);
268
+
269
+ if (ctx) {
270
+ ctx.font = this._getFontDeclaration();
271
+ const tatweelWidth = ctx.measureText(ARABIC_TATWEEL).width;
272
+ // console.log(` tatweelWidth: ${tatweelWidth}`);
273
+
274
+ if (tatweelWidth > 0) {
275
+ const newLine = [...line];
276
+ for (const point of sortedPoints) {
277
+ const tatweelCount = Math.max(1, Math.round(perKashidaWidth / tatweelWidth));
278
+ // console.log(` Point ${point.charIndex}: inserting ${tatweelCount} tatweels`);
279
+
280
+ // Insert tatweels after the character
281
+ for (let t = 0; t < tatweelCount; t++) {
282
+ newLine.splice(point.charIndex + 1, 0, ARABIC_TATWEEL);
283
+ }
284
+
285
+ // Store kashida info with updated indices and tatweel count
286
+ this.__kashidaInfo[i].push({
287
+ charIndex: point.charIndex,
288
+ width: perKashidaWidth,
289
+ tatweelCount: tatweelCount
290
+ });
291
+ }
292
+
293
+ // console.log(` Total inserted: ${insertedCount} tatweels`);
294
+ // console.log(` Original line length: ${line.length}, new line length: ${newLine.length}`);
295
+ // console.log(` New line: ${newLine.join('')}`);
296
+
297
+ // Update _textLines with the new line containing tatweels
298
+ this._textLines[i] = newLine;
299
+
300
+ // Update textLines string version
301
+ if (this.textLines && this.textLines[i] !== undefined) {
302
+ this.textLines[i] = newLine.join('');
221
303
  }
304
+
305
+ // Recalculate charBounds for this line since text changed
306
+ this.__charBounds[i] = [];
307
+ this.__lineWidths[i] = undefined;
308
+ this._measureLine(i);
309
+
310
+ // console.log(` After remeasure, lineWidth: ${this.__lineWidths[i]}`);
311
+ }
312
+ }
313
+ }
314
+
315
+ // Now apply space expansion to remaining extra space
316
+ const newLineWidth = this.getLineWidth(i);
317
+ const remainingSpace = this.width - newLineWidth;
318
+ if (remainingSpace > 0 && numberOfSpaces > 0) {
319
+ const extraPerSpace = remainingSpace / numberOfSpaces;
320
+ let accumulatedOffset = 0;
321
+ for (let j = 0; j < this._textLines[i].length; j++) {
322
+ const charBound = this.__charBounds[i][j];
323
+ if (!charBound) continue;
324
+ charBound.left += accumulatedOffset;
325
+ if (this._reSpaceAndTab.test(this._textLines[i][j])) {
326
+ charBound.width += extraPerSpace;
327
+ charBound.kernedWidth += extraPerSpace;
328
+ accumulatedOffset += extraPerSpace;
222
329
  }
223
330
  }
224
331
  }
225
332
  }
333
+
334
+ // Final debug log showing kashida state
335
+ // console.log('=== enlargeSpaces END ===');
336
+ // console.log('Final __kashidaInfo:', JSON.stringify(this.__kashidaInfo.map((lineInfo, i) => ({
337
+ // line: i,
338
+ // entries: lineInfo.map(k => ({ charIndex: k.charIndex, tatweelCount: k.tatweelCount }))
339
+ // }))));
226
340
  }
227
341
 
228
342
  /**
@@ -242,7 +356,9 @@ class FabricText extends StyledText {
242
356
  return {
243
357
  text: this.text,
244
358
  width: this.width,
245
- height: this.height,
359
+ // Don't pass height constraint to allow vertical auto-expansion
360
+ // Only pass height if ellipsis is enabled (need to truncate)
361
+ height: this.ellipsis ? this.height : undefined,
246
362
  wrap: this.wrap || 'word',
247
363
  align: this._mapTextAlignToAlign(this.textAlign),
248
364
  ellipsis: this.ellipsis || false,
@@ -297,9 +413,13 @@ class FabricText extends StyledText {
297
413
  // Convert layout to legacy format for compatibility
298
414
  this._convertLayoutToLegacyFormat(layout);
299
415
 
300
- // Ensure justify alignment is properly applied for compatibility with legacy rendering
301
- // Skip legacy enlargeSpaces when using advanced layout; Konva layout already distributes spaces.
302
-
416
+ // Apply kashida if enabled for justify alignment
417
+ // This must be called after _convertLayoutToLegacyFormat to ensure __charBounds exists
418
+ if (this.textAlign.includes(JUSTIFY) && this.kashida && this.kashida !== 'none') {
419
+ if (this.__charBounds && this.__charBounds.length > 0) {
420
+ this.enlargeSpaces();
421
+ }
422
+ }
303
423
  this.dirty = true;
304
424
  }
305
425
 
@@ -311,16 +431,27 @@ class FabricText extends StyledText {
311
431
  this._textLines = layout.lines.map(line => line.graphemes);
312
432
  this.textLines = layout.lines.map(line => line.text);
313
433
 
434
+ // Set _text as flat array of all graphemes (required for editing)
435
+ this._text = layout.lines.flatMap(line => line.graphemes);
436
+
314
437
  // Convert bounds to legacy format
438
+ // IMPORTANT: Preserve both logical (left) and visual (renderLeft) positions
439
+ // - left: cumulative logical offset (for text editing operations)
440
+ // - renderLeft: actual visual X position after BiDi reordering and alignment
441
+ // The renderLeft is critical for correct cursor/selection hit testing in mixed RTL/LTR text
315
442
  this.__charBounds = layout.lines.map(line => line.bounds.map(bound => ({
316
443
  left: bound.left,
317
444
  top: bound.y,
318
445
  width: bound.width,
319
446
  height: bound.height,
320
447
  kernedWidth: bound.kernedWidth,
321
- deltaY: bound.deltaY || 0
448
+ deltaY: bound.deltaY || 0,
449
+ renderLeft: bound.x // Visual X position for hit testing
322
450
  })));
323
451
 
452
+ // Populate line widths cache to prevent getLineWidth from triggering legacy measurement
453
+ this.__lineWidths = layout.lines.map(line => line.width);
454
+
324
455
  // Update grapheme info for compatibility
325
456
  if (layout.lines.length > 0) {
326
457
  this._unwrappedTextLines = layout.lines.map(line => line.graphemes);
@@ -488,6 +619,48 @@ class FabricText extends StyledText {
488
619
  this._renderChars(method, ctx, line, left, top, lineIndex);
489
620
  }
490
621
 
622
+ /**
623
+ * Build display text lines with kashida characters inserted.
624
+ * This creates a version of _textLines with tatweel characters added at kashida points.
625
+ * @private
626
+ */
627
+ _buildKashidaDisplayLines() {
628
+ if (this.kashida === 'none' || !this.__kashidaInfo) {
629
+ return this._textLines;
630
+ }
631
+ const displayLines = [];
632
+ for (let lineIndex = 0; lineIndex < this._textLines.length; lineIndex++) {
633
+ const line = this._textLines[lineIndex];
634
+ const kashidaInfo = this.__kashidaInfo[lineIndex];
635
+ if (!kashidaInfo || kashidaInfo.length === 0) {
636
+ displayLines.push([...line]);
637
+ continue;
638
+ }
639
+
640
+ // Sort kashida points by charIndex descending so we can insert from the end
641
+ const sortedKashida = [...kashidaInfo].sort((a, b) => b.charIndex - a.charIndex);
642
+
643
+ // Calculate how many tatweels to insert based on width
644
+ const newLine = [...line];
645
+ for (const {
646
+ charIndex,
647
+ width
648
+ } of sortedKashida) {
649
+ if (width <= 0 || charIndex >= newLine.length) continue;
650
+
651
+ // Calculate number of tatweel characters based on width
652
+ // Each tatweel is approximately 5px at font size 24
653
+ const tatweelCount = Math.max(1, Math.round(width / 3));
654
+ const tatweels = ARABIC_TATWEEL.repeat(tatweelCount);
655
+
656
+ // Insert tatweels after the character at charIndex
657
+ newLine.splice(charIndex + 1, 0, tatweels);
658
+ }
659
+ displayLines.push(newLine);
660
+ }
661
+ return displayLines;
662
+ }
663
+
491
664
  /**
492
665
  * Renders the text background for lines, taking care of style
493
666
  * @private
@@ -568,10 +741,13 @@ class FabricText extends StyledText {
568
741
  const fontCache = cache.getFontCache(charStyle),
569
742
  fontDeclaration = this._getFontDeclaration(charStyle),
570
743
  couple = previousChar + _char,
571
- stylesAreEqual = previousChar && fontDeclaration === this._getFontDeclaration(prevCharStyle),
744
+ // Skip kerning for tatweel (kashida) characters - they extend connections
745
+ // and kerning would make the following character appear too narrow
746
+ isTatweel = previousChar === '\u0640',
747
+ stylesAreEqual = previousChar && !isTatweel && fontDeclaration === this._getFontDeclaration(prevCharStyle),
572
748
  fontMultiplier = charStyle.fontSize / this.CACHE_FONT_SIZE;
573
749
  let width, coupleWidth, previousWidth, kernedWidth;
574
- if (previousChar && fontCache[previousChar] !== undefined) {
750
+ if (previousChar && !isTatweel && fontCache[previousChar] !== undefined) {
575
751
  previousWidth = fontCache[previousChar];
576
752
  }
577
753
  if (fontCache[_char] !== undefined) {
@@ -589,11 +765,11 @@ class FabricText extends StyledText {
589
765
  kernedWidth = width = ctx.measureText(_char).width;
590
766
  fontCache[_char] = width;
591
767
  }
592
- if (previousWidth === undefined && stylesAreEqual && previousChar) {
768
+ if (previousWidth === undefined && stylesAreEqual && previousChar && !isTatweel) {
593
769
  previousWidth = ctx.measureText(previousChar).width;
594
770
  fontCache[previousChar] = previousWidth;
595
771
  }
596
- if (stylesAreEqual && coupleWidth === undefined) {
772
+ if (stylesAreEqual && coupleWidth === undefined && !isTatweel) {
597
773
  // we can measure the kerning couple and subtract the width of the previous character
598
774
  coupleWidth = ctx.measureText(couple).width;
599
775
  fontCache[couple] = coupleWidth;
@@ -640,10 +816,7 @@ class FabricText extends StyledText {
640
816
  */
641
817
  _measureLine(lineIndex) {
642
818
  // Debug: detect if measureLine is called after justify was applied
643
- if (this._justifyApplied) {
644
- console.warn(`WARNING: _measureLine called for line ${lineIndex} AFTER justify was applied! This will overwrite justified charBounds.`);
645
- console.trace('Stack trace:');
646
- }
819
+ if (this._justifyApplied) ;
647
820
  let width = 0,
648
821
  prevGrapheme,
649
822
  graphemeInfo;
@@ -818,13 +991,7 @@ class FabricText extends StyledText {
818
991
  top = this._getTopOffset();
819
992
 
820
993
  // Debug: log once per render
821
- if (method === 'fillText' && (_this$textAlign = this.textAlign) !== null && _this$textAlign !== void 0 && _this$textAlign.includes('justify')) {
822
- console.log('=== RENDER DEBUG ===');
823
- console.log('direction:', this.direction);
824
- console.log('textAlign:', this.textAlign);
825
- console.log('width:', this.width);
826
- console.log('_getLeftOffset:', left);
827
- }
994
+ if (method === 'fillText' && (_this$textAlign = this.textAlign) !== null && _this$textAlign !== void 0 && _this$textAlign.includes('justify')) ;
828
995
  for (let i = 0, len = this._textLines.length; i < len; i++) {
829
996
  var _this$textAlign2;
830
997
  const heightOfLine = this.getHeightOfLine(i),
@@ -833,8 +1000,8 @@ class FabricText extends StyledText {
833
1000
 
834
1001
  // Debug: log line offsets for justify
835
1002
  if (method === 'fillText' && (_this$textAlign2 = this.textAlign) !== null && _this$textAlign2 !== void 0 && _this$textAlign2.includes('justify')) {
836
- const lineWidth = this.getLineWidth(i);
837
- console.log(`Line ${i}: leftOffset=${leftOffset.toFixed(2)}, lineWidth=${lineWidth.toFixed(2)}, renderAt=${(left + leftOffset).toFixed(2)}`);
1003
+ this.getLineWidth(i);
1004
+ // console.log(`Line ${i}: leftOffset=${leftOffset.toFixed(2)}, lineWidth=${lineWidth.toFixed(2)}, renderAt=${(left + leftOffset).toFixed(2)}`);
838
1005
  }
839
1006
  this._renderTextLine(method, ctx, this._textLines[i], left + leftOffset, top + lineHeights + maxHeight, i);
840
1007
  lineHeights += heightOfLine;
@@ -885,12 +1052,18 @@ class FabricText extends StyledText {
885
1052
  const lineHeight = this.getHeightOfLine(lineIndex),
886
1053
  isJustify = this.textAlign.includes(JUSTIFY),
887
1054
  path = this.path,
888
- shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path,
889
1055
  isLtr = this.direction === 'ltr',
890
1056
  sign = this.direction === 'ltr' ? 1 : -1,
891
1057
  // this was changed in the PR #7674
892
1058
  // currentDirection = ctx.canvas.getAttribute('dir');
893
1059
  currentDirection = ctx.direction;
1060
+
1061
+ // Check if we should use BiDi-aware rendering with pre-calculated positions
1062
+ // This is needed for advanced layout with RTL or mixed BiDi text
1063
+ const chars = this.__charBounds[lineIndex];
1064
+ this.enableAdvancedLayout && (chars === null || chars === void 0 ? void 0 : chars.length) > 0 && chars[0].renderLeft !== undefined;
1065
+
1066
+ const shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path;
894
1067
  let actualStyle,
895
1068
  nextStyle,
896
1069
  charsToRender = '',
@@ -899,6 +1072,9 @@ class FabricText extends StyledText {
899
1072
  timeToRender,
900
1073
  drawingLeft;
901
1074
  ctx.save();
1075
+
1076
+ // For BiDi rendering with pre-calculated positions, disable browser BiDi
1077
+ // and render each character at its calculated visual position
902
1078
  if (currentDirection !== this.direction) {
903
1079
  ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl');
904
1080
  ctx.direction = isLtr ? 'ltr' : 'rtl';
@@ -918,12 +1094,12 @@ class FabricText extends StyledText {
918
1094
  }
919
1095
  // Debug: Log charBounds being used for first line only during justify
920
1096
  if (isJustify && lineIndex === 0 && method === 'fillText') {
921
- console.log(`\n=== RENDER _renderChars line ${lineIndex} ===`);
922
- console.log('Initial left:', left.toFixed(2), 'sign:', sign);
923
- console.log('_justifyApplied flag:', this._justifyApplied);
1097
+ // console.log(`\n=== RENDER _renderChars line ${lineIndex} ===`);
1098
+ // console.log('Initial left:', left.toFixed(2), 'sign:', sign);
1099
+ // console.log('_justifyApplied flag:', (this as any)._justifyApplied);
924
1100
  const lineBounds = this.__charBounds[lineIndex];
925
- const totalKW = (lineBounds === null || lineBounds === void 0 ? void 0 : lineBounds.reduce((s, b) => s + ((b === null || b === void 0 ? void 0 : b.kernedWidth) || 0), 0)) || 0;
926
- console.log('Total kernedWidth in charBounds:', totalKW.toFixed(2), '(should be ~300 if justify was applied)');
1101
+ (lineBounds === null || lineBounds === void 0 ? void 0 : lineBounds.reduce((s, b) => s + ((b === null || b === void 0 ? void 0 : b.kernedWidth) || 0), 0)) || 0;
1102
+ // console.log('Total kernedWidth in charBounds:', totalKW.toFixed(2), '(should be ~300 if justify was applied)');
927
1103
  // Log first few space widths to verify expansion
928
1104
  const spaceIndices = [3, 9, 15, 23, 31];
929
1105
  spaceIndices.forEach(idx => {
@@ -972,10 +1148,6 @@ class FabricText extends StyledText {
972
1148
  // For RTL with textAlign='right': x is the right edge, so drawingLeft = left
973
1149
  // Both cases: drawingLeft = left (the text alignment handles the edge correctly)
974
1150
  drawingLeft = left;
975
- // Debug: log first chunk positioning for justify
976
- if (isJustify && lineIndex === 0 && method === 'fillText' && i < 5) {
977
- console.log(` Chunk ending at char ${i}: left=${left.toFixed(2)}, boxWidth=${boxWidth.toFixed(2)}, drawingLeft=${drawingLeft.toFixed(2)}, textAlign=${isLtr ? 'left' : 'right'}`);
978
- }
979
1151
  this._renderChar(method, ctx, lineIndex, i, charsToRender, drawingLeft, top);
980
1152
  }
981
1153
  charsToRender = '';
@@ -984,11 +1156,6 @@ class FabricText extends StyledText {
984
1156
  boxWidth = 0;
985
1157
  }
986
1158
  }
987
- // Debug: log final position for justify
988
- if (isJustify && lineIndex === 0 && method === 'fillText') {
989
- console.log('Final left position after rendering:', left.toFixed(2));
990
- console.log('Expected final position:', (sign > 0 ? this.width / 2 : -this.width / 2).toFixed(2));
991
- }
992
1159
  ctx.restore();
993
1160
  }
994
1161
 
@@ -1220,12 +1387,159 @@ class FabricText extends StyledText {
1220
1387
  * @private
1221
1388
  */
1222
1389
  _clearCache() {
1390
+ // console.log('🗑️ _clearCache called');
1391
+ // console.trace('🗑️ _clearCache stack trace');
1223
1392
  this._forceClearCache = false;
1224
1393
  this.__lineWidths = [];
1225
1394
  this.__lineHeights = [];
1226
1395
  this.__charBounds = [];
1396
+ this.__kashidaInfo = [];
1227
1397
  // Reset justify applied flag
1228
1398
  this._justifyApplied = false;
1399
+ // Reset dimension state to force recalculation
1400
+ this._lastDimensionState = null;
1401
+ }
1402
+
1403
+ /**
1404
+ * Convert a display character index (in _textLines with tatweels) to original text index.
1405
+ * When kashida is applied, _textLines contains extra tatweel characters that don't exist
1406
+ * in the original text. This method maps back to the original index.
1407
+ * @param lineIndex - The line index
1408
+ * @param displayCharIndex - Character index in the display text (with tatweels)
1409
+ * @returns Original character index (without tatweels)
1410
+ */
1411
+ _displayToOriginalIndex(lineIndex, displayCharIndex) {
1412
+ var _this$__kashidaInfo;
1413
+ // console.log(`🔄 _displayToOriginalIndex called: line=${lineIndex}, displayIdx=${displayCharIndex}`);
1414
+ // console.log(`🔄 __kashidaInfo exists: ${!!this.__kashidaInfo}, length: ${this.__kashidaInfo?.length}`);
1415
+ // console.log(`🔄 __kashidaInfo raw:`, JSON.stringify(this.__kashidaInfo));
1416
+
1417
+ const kashidaInfo = (_this$__kashidaInfo = this.__kashidaInfo) === null || _this$__kashidaInfo === void 0 ? void 0 : _this$__kashidaInfo[lineIndex];
1418
+ if (!kashidaInfo || kashidaInfo.length === 0) {
1419
+ // No kashida on this line, indices are the same
1420
+ // console.log(`🔄 No kashida info for line ${lineIndex}, returning same index`);
1421
+ return displayCharIndex;
1422
+ }
1423
+
1424
+ // Sort kashida info by charIndex ascending for proper traversal
1425
+ const sortedKashida = [...kashidaInfo].sort((a, b) => a.charIndex - b.charIndex);
1426
+
1427
+ // console.log(`🔄 _displayToOriginalIndex: line=${lineIndex}, displayIdx=${displayCharIndex}`);
1428
+ // console.log(`🔄 kashidaInfo:`, sortedKashida.map(k => `{charIdx:${k.charIndex}, cnt:${k.tatweelCount}}`).join(', '));
1429
+
1430
+ let tatweelsBeforeIndex = 0;
1431
+ for (const k of sortedKashida) {
1432
+ const tatweelCount = k.tatweelCount || 0;
1433
+ // Position where tatweels start (after the original character)
1434
+ const tatweelStartPos = k.charIndex + 1 + tatweelsBeforeIndex;
1435
+ const tatweelEndPos = tatweelStartPos + tatweelCount;
1436
+
1437
+ // console.log(`🔄 k.charIndex=${k.charIndex}, tatweelStartPos=${tatweelStartPos}, tatweelEndPos=${tatweelEndPos}, tatweelsBeforeIndex=${tatweelsBeforeIndex}`);
1438
+
1439
+ if (displayCharIndex < tatweelStartPos) {
1440
+ // Before this kashida point
1441
+ // console.log(`🔄 displayIdx < tatweelStartPos, break`);
1442
+ break;
1443
+ } else if (displayCharIndex < tatweelEndPos) {
1444
+ // Within tatweel characters - map to the character before tatweels
1445
+ // console.log(`🔄 Within tatweel, return ${k.charIndex + 1}`);
1446
+ return k.charIndex + 1;
1447
+ } else {
1448
+ // After this kashida point
1449
+ tatweelsBeforeIndex += tatweelCount;
1450
+ // console.log(`🔄 After this kashida, tatweelsBeforeIndex now=${tatweelsBeforeIndex}`);
1451
+ }
1452
+ }
1453
+
1454
+ // Subtract all tatweels that come before this position
1455
+ const result = displayCharIndex - tatweelsBeforeIndex;
1456
+ // console.log(`🔄 Final result: ${displayCharIndex} - ${tatweelsBeforeIndex} = ${result}`);
1457
+ return result;
1458
+ }
1459
+
1460
+ /**
1461
+ * Convert an original text character index to display index (in _textLines with tatweels).
1462
+ * @param lineIndex - The line index
1463
+ * @param originalCharIndex - Character index in the original text (without tatweels)
1464
+ * @returns Display character index (with tatweels)
1465
+ */
1466
+ _originalToDisplayIndex(lineIndex, originalCharIndex) {
1467
+ var _this$__kashidaInfo2;
1468
+ const kashidaInfo = (_this$__kashidaInfo2 = this.__kashidaInfo) === null || _this$__kashidaInfo2 === void 0 ? void 0 : _this$__kashidaInfo2[lineIndex];
1469
+ if (!kashidaInfo || kashidaInfo.length === 0) {
1470
+ // No kashida on this line, indices are the same
1471
+ return originalCharIndex;
1472
+ }
1473
+
1474
+ // Sort kashida info by charIndex ascending
1475
+ const sortedKashida = [...kashidaInfo].sort((a, b) => a.charIndex - b.charIndex);
1476
+ let tatweelsBeforeIndex = 0;
1477
+ for (const k of sortedKashida) {
1478
+ const tatweelCount = k.tatweelCount || 0;
1479
+ // If the original char index is after this kashida insertion point,
1480
+ // add the tatweels to the offset
1481
+ if (originalCharIndex > k.charIndex) {
1482
+ tatweelsBeforeIndex += tatweelCount;
1483
+ } else {
1484
+ break;
1485
+ }
1486
+ }
1487
+ return originalCharIndex + tatweelsBeforeIndex;
1488
+ }
1489
+
1490
+ /**
1491
+ * Check if a display character index points to a tatweel character.
1492
+ * @param lineIndex - The line index
1493
+ * @param displayCharIndex - Character index in the display text
1494
+ * @returns True if the character at this index is a tatweel
1495
+ */
1496
+ _isTatweelAtDisplayIndex(lineIndex, displayCharIndex) {
1497
+ var _this$__kashidaInfo3;
1498
+ const kashidaInfo = (_this$__kashidaInfo3 = this.__kashidaInfo) === null || _this$__kashidaInfo3 === void 0 ? void 0 : _this$__kashidaInfo3[lineIndex];
1499
+ if (!kashidaInfo || kashidaInfo.length === 0) {
1500
+ return false;
1501
+ }
1502
+
1503
+ // Sort kashida info by charIndex ascending
1504
+ const sortedKashida = [...kashidaInfo].sort((a, b) => a.charIndex - b.charIndex);
1505
+ let tatweelsBeforeIndex = 0;
1506
+ for (const k of sortedKashida) {
1507
+ const tatweelCount = k.tatweelCount || 0;
1508
+ const tatweelStartPos = k.charIndex + 1 + tatweelsBeforeIndex;
1509
+ const tatweelEndPos = tatweelStartPos + tatweelCount;
1510
+ if (displayCharIndex >= tatweelStartPos && displayCharIndex < tatweelEndPos) {
1511
+ return true;
1512
+ }
1513
+ tatweelsBeforeIndex += tatweelCount;
1514
+ }
1515
+ return false;
1516
+ }
1517
+
1518
+ /**
1519
+ * Get the total number of tatweel characters inserted in a line.
1520
+ * @param lineIndex - The line index
1521
+ * @returns Total number of tatweels in this line
1522
+ */
1523
+ _getTatweelCountForLine(lineIndex) {
1524
+ var _this$__kashidaInfo4;
1525
+ const kashidaInfo = (_this$__kashidaInfo4 = this.__kashidaInfo) === null || _this$__kashidaInfo4 === void 0 ? void 0 : _this$__kashidaInfo4[lineIndex];
1526
+ if (!kashidaInfo || kashidaInfo.length === 0) {
1527
+ return 0;
1528
+ }
1529
+ return kashidaInfo.reduce((sum, k) => sum + (k.tatweelCount || 0), 0);
1530
+ }
1531
+
1532
+ /**
1533
+ * Get the original line length (without tatweels).
1534
+ * When kashida is applied, _textLines contains extra tatweel characters.
1535
+ * This returns the length as it would be in the original text.
1536
+ * @param lineIndex - The line index
1537
+ * @returns Original line length without tatweels
1538
+ */
1539
+ _getOriginalLineLength(lineIndex) {
1540
+ var _this$_textLines$line;
1541
+ const displayLength = ((_this$_textLines$line = this._textLines[lineIndex]) === null || _this$_textLines$line === void 0 ? void 0 : _this$_textLines$line.length) || 0;
1542
+ return displayLength - this._getTatweelCountForLine(lineIndex);
1229
1543
  }
1230
1544
 
1231
1545
  /**