@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
@@ -5,6 +5,7 @@ import { createTextboxDefaultControls } from '../controls/commonControls.mjs';
5
5
  import { JUSTIFY } from './Text/constants.mjs';
6
6
  import { fontLacksEnglishGlyphsCached } from '../text/measure.mjs';
7
7
  import { layoutText } from '../text/layout.mjs';
8
+ import { ARABIC_TATWEEL, findKashidaPoints } from '../text/unicode.mjs';
8
9
 
9
10
  // @TODO: Many things here are configuration related and shouldn't be on the class nor prototype
10
11
  // regexes, list of properties that are not suppose to change by instances, magic consts.
@@ -70,7 +71,7 @@ class Textbox extends IText {
70
71
  }
71
72
 
72
73
  // Skip if nothing changed
73
- const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
74
+ const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}|${this.kashida}`;
74
75
  if (this._lastDimensionState === currentState && this._textLines && this._textLines.length > 0) {
75
76
  return;
76
77
  }
@@ -169,12 +170,18 @@ class Textbox extends IText {
169
170
  }
170
171
 
171
172
  // Use new layout engine
173
+ // When kashida is enabled, don't let layout engine apply justify - we'll handle it with kashida
174
+ const useKashidaJustify = this.kashida !== 'none' && this.textAlign.includes(JUSTIFY);
175
+ const effectiveAlign = useKashidaJustify ? this.direction === 'rtl' ? 'right' : 'left' // Natural alignment, kashida will justify
176
+ : this._mapTextAlignToAlign(this.textAlign);
172
177
  const layout = layoutText({
173
178
  text: this.text,
174
179
  width: this.width,
175
- height: this.height,
180
+ // Don't pass height constraint to allow vertical auto-expansion
181
+ // Only pass height if explicitly set to constrain (e.g., for ellipsis)
182
+ height: this.ellipsis ? this.height : undefined,
176
183
  wrap: this.wrap || 'word',
177
- align: this._mapTextAlignToAlign(this.textAlign),
184
+ align: effectiveAlign,
178
185
  ellipsis: this.ellipsis || false,
179
186
  fontSize: this.fontSize,
180
187
  lineHeight: this.lineHeight,
@@ -212,9 +219,264 @@ class Textbox extends IText {
212
219
 
213
220
  // Generate style map for compatibility
214
221
  this._styleMap = this._generateStyleMapFromLayout(layout);
222
+
223
+ // Apply kashida for justified text in advanced layout mode
224
+ if (this.textAlign.includes(JUSTIFY) && this.kashida !== 'none') {
225
+ this._applyKashidaToLayout();
226
+ }
215
227
  this.dirty = true;
216
228
  }
217
229
 
230
+ /**
231
+ * Apply kashida (tatweel) characters to layout for Arabic text justification.
232
+ * This method INSERTS actual tatweel characters into the text lines.
233
+ * @private
234
+ */
235
+ _applyKashidaToLayout() {
236
+ if (!this._textLines || !this.__charBounds) {
237
+ return;
238
+ }
239
+
240
+ // Clear visual positions cache - it becomes stale when kashida is applied
241
+ // Check if cache exists (it's initialized in IText constructor which runs after this during construction)
242
+ if (this._visualPositionsCache) {
243
+ this._clearVisualPositionsCache();
244
+ }
245
+ const kashidaRatios = {
246
+ none: 0,
247
+ short: 0.25,
248
+ medium: 0.5,
249
+ long: 0.75,
250
+ stylistic: 1.0
251
+ };
252
+ const kashidaRatio = kashidaRatios[this.kashida] || 0;
253
+ if (kashidaRatio === 0) {
254
+ return;
255
+ }
256
+
257
+ // Calculate tatweel width once
258
+ const canvas = document.createElement('canvas');
259
+ const ctx = canvas.getContext('2d');
260
+ if (!ctx) {
261
+ return;
262
+ }
263
+ ctx.font = this._getFontDeclaration();
264
+ const tatweelWidth = ctx.measureText(ARABIC_TATWEEL).width;
265
+ if (tatweelWidth <= 0) {
266
+ return;
267
+ }
268
+
269
+ // Reset kashida info
270
+ this.__kashidaInfo = [];
271
+ const totalLines = this._textLines.length;
272
+ for (let lineIndex = 0; lineIndex < totalLines; lineIndex++) {
273
+ this.__kashidaInfo[lineIndex] = [];
274
+ const line = this._textLines[lineIndex];
275
+ if (!this.__charBounds || !this.__charBounds[lineIndex]) {
276
+ continue;
277
+ }
278
+
279
+ // Don't apply kashida to the last line
280
+ const isLastLine = lineIndex === totalLines - 1;
281
+ if (isLastLine) {
282
+ continue;
283
+ }
284
+ const lineBounds = this.__charBounds[lineIndex];
285
+ const lastBound = lineBounds[lineBounds.length - 1];
286
+
287
+ // Calculate current line width
288
+ const currentLineWidth = lastBound ? lastBound.left + lastBound.kernedWidth : 0;
289
+ const totalExtraSpace = this.width - currentLineWidth;
290
+
291
+ // Only apply kashida if there's significant extra space to fill
292
+ if (totalExtraSpace <= 2) {
293
+ continue;
294
+ }
295
+
296
+ // Find kashida points
297
+ const kashidaPoints = findKashidaPoints(line);
298
+ if (kashidaPoints.length === 0) {
299
+ continue;
300
+ }
301
+
302
+ // Calculate kashida space
303
+ const kashidaSpace = totalExtraSpace * kashidaRatio;
304
+
305
+ // Calculate how many tatweels can fit
306
+ const totalTatweels = Math.floor(kashidaSpace / tatweelWidth);
307
+ if (totalTatweels === 0) {
308
+ continue;
309
+ }
310
+
311
+ // Limit kashida points
312
+ const maxKashidaPoints = Math.min(kashidaPoints.length, totalTatweels);
313
+ const usedKashidaPoints = kashidaPoints.slice(0, maxKashidaPoints);
314
+
315
+ // Distribute tatweels evenly
316
+ const tatweelsPerPoint = Math.floor(totalTatweels / maxKashidaPoints);
317
+ const extraTatweels = totalTatweels % maxKashidaPoints;
318
+
319
+ // console.log(`=== Inserting Kashida into line ${lineIndex} ===`);
320
+ // console.log(` totalTatweels: ${totalTatweels}, usedPoints: ${usedKashidaPoints.length}`);
321
+
322
+ // Sort by charIndex descending so we insert from the end (prevents index shifting issues)
323
+ const sortedPoints = [...usedKashidaPoints].sort((a, b) => b.charIndex - a.charIndex);
324
+
325
+ // Create new line with tatweels inserted
326
+ const newLine = [...line];
327
+ for (let i = 0; i < sortedPoints.length; i++) {
328
+ const point = sortedPoints[i];
329
+ const originalIndex = usedKashidaPoints.indexOf(point);
330
+ const count = tatweelsPerPoint + (originalIndex < extraTatweels ? 1 : 0);
331
+ if (count > 0) {
332
+ // Insert tatweels AFTER the character at charIndex
333
+ const tatweels = Array(count).fill(ARABIC_TATWEEL);
334
+ newLine.splice(point.charIndex + 1, 0, ...tatweels);
335
+ // console.log(` Inserted ${count} tatweels after char ${point.charIndex}`);
336
+
337
+ // Store kashida info for index conversion
338
+ this.__kashidaInfo[lineIndex].push({
339
+ charIndex: point.charIndex,
340
+ width: count * tatweelWidth,
341
+ tatweelCount: count
342
+ });
343
+ }
344
+ }
345
+
346
+ // Update _textLines with the new line containing tatweels
347
+ this._textLines[lineIndex] = newLine;
348
+
349
+ // Update textLines (string version)
350
+ if (this.textLines) {
351
+ this.textLines[lineIndex] = newLine.join('');
352
+ }
353
+
354
+ // Clear and recalculate charBounds for this line
355
+ this.__charBounds[lineIndex] = [];
356
+ this.__lineWidths[lineIndex] = undefined;
357
+ this._measureLine(lineIndex);
358
+
359
+ // Now expand spaces to fill any remaining gap
360
+ let newLineBounds = this.__charBounds[lineIndex];
361
+ if (newLineBounds && newLineBounds.length > 0) {
362
+ let newLastBound = newLineBounds[newLineBounds.length - 1];
363
+ let newLineWidth = newLastBound ? newLastBound.left + newLastBound.kernedWidth : 0;
364
+ let remainingGap = this.width - newLineWidth;
365
+ if (remainingGap > 0.5) {
366
+ // Count spaces in the new line
367
+ let spaceCount = 0;
368
+ for (let i = 0; i < newLine.length; i++) {
369
+ if (/\s/.test(newLine[i])) {
370
+ spaceCount++;
371
+ }
372
+ }
373
+ if (spaceCount > 0) {
374
+ const extraPerSpace = remainingGap / spaceCount;
375
+ let accumulatedExtra = 0;
376
+
377
+ // Expand space widths AND update left positions for subsequent chars
378
+ for (let i = 0; i < newLineBounds.length; i++) {
379
+ const bound = newLineBounds[i];
380
+ if (!bound) continue;
381
+
382
+ // Update left position to account for previous space expansions
383
+ bound.left += accumulatedExtra;
384
+
385
+ // If this is a space, expand it
386
+ if (/\s/.test(newLine[i])) {
387
+ bound.width += extraPerSpace;
388
+ bound.kernedWidth += extraPerSpace;
389
+ accumulatedExtra += extraPerSpace;
390
+ }
391
+ }
392
+ // Update the extra entry at the end (cursor position)
393
+ if (newLineBounds[newLine.length]) {
394
+ newLineBounds[newLine.length].left += accumulatedExtra;
395
+ }
396
+
397
+ // Recalculate remaining gap after space expansion
398
+ newLastBound = newLineBounds[newLineBounds.length - 1];
399
+ newLineWidth = newLastBound ? newLastBound.left + newLastBound.kernedWidth : 0;
400
+ remainingGap = this.width - newLineWidth;
401
+ }
402
+ }
403
+
404
+ // If there's still a gap after space expansion, distribute it across all kashida points
405
+ if (remainingGap > 0.5 && this.__kashidaInfo[lineIndex].length > 0) {
406
+ const kashidaPointCount = this.__kashidaInfo[lineIndex].length;
407
+ const extraPerKashida = remainingGap / kashidaPointCount;
408
+
409
+ // Find kashida positions in newLine and expand their widths
410
+ let kashidaIndex = 0;
411
+ let accumulatedExtra = 0;
412
+ for (let i = 0; i < newLineBounds.length; i++) {
413
+ const bound = newLineBounds[i];
414
+ if (!bound) continue;
415
+
416
+ // Update left position for accumulated expansion
417
+ bound.left += accumulatedExtra;
418
+
419
+ // Check if this is a tatweel character
420
+ if (newLine[i] === ARABIC_TATWEEL) {
421
+ var _this$__kashidaInfo$l;
422
+ // Distribute extra width among tatweels
423
+ const extraForThis = extraPerKashida / (((_this$__kashidaInfo$l = this.__kashidaInfo[lineIndex][kashidaIndex]) === null || _this$__kashidaInfo$l === void 0 ? void 0 : _this$__kashidaInfo$l.tatweelCount) || 1);
424
+ bound.width += extraForThis;
425
+ bound.kernedWidth += extraForThis;
426
+ accumulatedExtra += extraForThis;
427
+
428
+ // Move to next kashida info when we've passed this group
429
+ const currentKashidaInfo = this.__kashidaInfo[lineIndex][kashidaIndex];
430
+ if (currentKashidaInfo && i > 0) {
431
+ // Check if next char is not tatweel - means we're done with this group
432
+ if (i + 1 >= newLine.length || newLine[i + 1] !== ARABIC_TATWEEL) {
433
+ kashidaIndex++;
434
+ }
435
+ }
436
+ }
437
+ }
438
+
439
+ // Update the extra entry at the end
440
+ if (newLineBounds[newLine.length]) {
441
+ newLineBounds[newLine.length].left += accumulatedExtra;
442
+ }
443
+ }
444
+ }
445
+
446
+ // Set line width to textbox width (for justified lines)
447
+ this.__lineWidths[lineIndex] = this.width;
448
+
449
+ // console.log(` New line length: ${newLine.length}, text: ${newLine.join('')}`);
450
+ }
451
+
452
+ // For justified lines with kashida, line width should equal textbox width
453
+ // Only set undefined widths (non-justified lines without kashida)
454
+ for (let i = 0; i < this._textLines.length; i++) {
455
+ if (this.__lineWidths[i] === undefined && this.__charBounds[i]) {
456
+ const bounds = this.__charBounds[i];
457
+ const lastBound = bounds[bounds.length - 1];
458
+ if (lastBound) {
459
+ this.__lineWidths[i] = lastBound.left + lastBound.kernedWidth;
460
+ }
461
+ }
462
+ }
463
+
464
+ // Update _text to match the new _textLines (required for editing)
465
+ this._text = this._textLines.flat();
466
+
467
+ // DON'T update this.text - keep the original text intact
468
+ // The tatweels are in _textLines and _text for rendering purposes only
469
+
470
+ this._justifyApplied = true;
471
+
472
+ // Debug log final kashida state
473
+ // console.log('=== _applyKashidaToLayout END ===');
474
+ // console.log('Final __kashidaInfo:', JSON.stringify(this.__kashidaInfo.map((lineInfo, i) => ({
475
+ // line: i,
476
+ // entries: lineInfo.map(k => ({ charIndex: k.charIndex, tatweelCount: k.tatweelCount }))
477
+ // }))));
478
+ }
479
+
218
480
  /**
219
481
  * Generate style map from new layout format
220
482
  * @private
@@ -737,84 +999,100 @@ class Textbox extends IText {
737
999
  * @private
738
1000
  */
739
1001
  _extractJustifySpaceMeasurements(element, lines) {
740
- console.log('=== _extractJustifySpaceMeasurements START ===');
741
- console.log('Textbox width:', this.width);
742
- console.log('Lines count:', lines.length);
1002
+ // console.log('=== _extractJustifySpaceMeasurements START ===');
1003
+ // console.log('Textbox width:', this.width);
1004
+ // console.log('Lines count:', lines.length);
1005
+
743
1006
  const measureCtx = this._browserMeasureCtx || (this._browserMeasureCtx = document.createElement('canvas').getContext('2d'));
744
1007
  if (!measureCtx) {
745
- console.log('ERROR: No measure context');
1008
+ // console.log('ERROR: No measure context');
746
1009
  return [];
747
1010
  }
748
1011
  measureCtx.font = `${this.fontStyle || 'normal'} ${this.fontWeight || 'normal'} ${this.fontSize}px "${this.fontFamily}"`;
749
1012
  const normalSpaceWidth = measureCtx.measureText(' ').width || 6;
750
- console.log('Font:', measureCtx.font);
751
- console.log('Normal space width:', normalSpaceWidth);
1013
+ // console.log('Font:', measureCtx.font);
1014
+ // console.log('Normal space width:', normalSpaceWidth);
1015
+
752
1016
  const spaceWidths = [];
753
1017
  lines.forEach((line, lineIndex) => {
754
1018
  const lineSpaces = [];
755
1019
  const spaceCount = (line.match(/\s/g) || []).length;
756
1020
  const isLastLine = lineIndex === lines.length - 1;
757
- console.log(`\nLine ${lineIndex}: "${line.substring(0, 50)}..." spaces: ${spaceCount}, isLast: ${isLastLine}`);
1021
+
1022
+ // console.log(`\nLine ${lineIndex}: "${line.substring(0, 50)}..." spaces: ${spaceCount}, isLast: ${isLastLine}`);
1023
+
758
1024
  if (spaceCount > 0 && !isLastLine) {
759
1025
  // Don't justify last line
760
1026
  const naturalWidth = measureCtx.measureText(line).width;
761
1027
  const remainingSpace = this.width - naturalWidth;
762
1028
  const extraPerSpace = remainingSpace > 0 ? remainingSpace / spaceCount : 0;
763
1029
  const expandedSpaceWidth = normalSpaceWidth + extraPerSpace;
764
- console.log(` Natural width: ${naturalWidth.toFixed(2)}, Remaining: ${remainingSpace.toFixed(2)}`);
765
- console.log(` Extra per space: ${extraPerSpace.toFixed(2)}, Expanded space: ${expandedSpaceWidth.toFixed(2)}`);
1030
+
1031
+ // console.log(` Natural width: ${naturalWidth.toFixed(2)}, Remaining: ${remainingSpace.toFixed(2)}`);
1032
+ // console.log(` Extra per space: ${extraPerSpace.toFixed(2)}, Expanded space: ${expandedSpaceWidth.toFixed(2)}`);
1033
+
766
1034
  const safeWidth = Math.max(normalSpaceWidth, expandedSpaceWidth);
767
1035
  for (let i = 0; i < spaceCount; i++) {
768
1036
  lineSpaces.push(safeWidth);
769
1037
  }
770
1038
  } else if (spaceCount > 0) {
771
1039
  // Last line: keep natural space width
772
- console.log(` Last line - using normal space width: ${normalSpaceWidth}`);
1040
+ // console.log(` Last line - using normal space width: ${normalSpaceWidth}`);
773
1041
  for (let i = 0; i < spaceCount; i++) {
774
1042
  lineSpaces.push(normalSpaceWidth);
775
1043
  }
776
1044
  }
777
1045
  spaceWidths.push(lineSpaces);
778
1046
  });
779
- console.log('\nFinal spaceWidths:', spaceWidths);
780
- console.log('=== _extractJustifySpaceMeasurements END ===\n');
1047
+
1048
+ // console.log('\nFinal spaceWidths:', spaceWidths);
1049
+ // console.log('=== _extractJustifySpaceMeasurements END ===\n');
781
1050
  return spaceWidths;
782
1051
  }
783
1052
 
784
1053
  /**
785
- * Apply justify space expansion using actual charBounds measurements
1054
+ * Apply justify space expansion using actual charBounds measurements.
1055
+ * Supports Arabic kashida (tatweel) justification when kashida property is set.
786
1056
  * @private
787
1057
  */
788
1058
  _applyBrowserJustifySpaces() {
789
- var _this$_textLines, _this$__charBounds;
790
- console.log('=== _applyBrowserJustifySpaces START ===');
791
- console.log('_textLines:', (_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length, 'lines');
792
- console.log('__charBounds:', (_this$__charBounds = this.__charBounds) === null || _this$__charBounds === void 0 ? void 0 : _this$__charBounds.length, 'lines');
793
- console.log('textbox width:', this.width);
794
1059
  if (!this._textLines || !this.__charBounds) {
795
- console.log('EARLY RETURN: _textLines or __charBounds missing');
796
1060
  return;
797
1061
  }
1062
+
1063
+ // Kashida ratios: proportion of extra space distributed via kashida vs space expansion
1064
+ const kashidaRatios = {
1065
+ none: 0,
1066
+ short: 0.25,
1067
+ medium: 0.5,
1068
+ long: 0.75,
1069
+ stylistic: 1.0
1070
+ };
1071
+ const kashidaRatio = kashidaRatios[this.kashida] || 0;
1072
+
1073
+ // Reset kashida info
1074
+ this.__kashidaInfo = [];
798
1075
  const totalLines = this._textLines.length;
799
1076
  this._textLines.forEach((line, lineIndex) => {
800
- const lineText = line.join('');
801
- const isLastLine = lineIndex === totalLines - 1;
802
- console.log(`\n--- Line ${lineIndex}: "${lineText}" isLast: ${isLastLine} ---`);
1077
+ // Initialize kashida info for this line
1078
+ this.__kashidaInfo[lineIndex] = [];
803
1079
  if (!this.__charBounds || !this.__charBounds[lineIndex]) {
804
- console.log(' SKIP: No charBounds for this line');
805
1080
  return;
806
1081
  }
807
1082
 
808
1083
  // Don't justify the last line
1084
+ const isLastLine = lineIndex === totalLines - 1;
809
1085
  if (isLastLine) {
810
- console.log(' SKIP: Last line - no justify');
811
1086
  return;
812
1087
  }
813
1088
  const lineBounds = this.__charBounds[lineIndex];
814
1089
 
815
1090
  // Calculate current line width from charBounds
816
1091
  const currentLineWidth = lineBounds.reduce((sum, b) => sum + ((b === null || b === void 0 ? void 0 : b.kernedWidth) || 0), 0);
817
- console.log(' Current line width from charBounds:', currentLineWidth);
1092
+ const totalExtraSpace = this.width - currentLineWidth;
1093
+ if (totalExtraSpace <= 0) {
1094
+ return;
1095
+ }
818
1096
 
819
1097
  // Count spaces and find space indices
820
1098
  const spaceIndices = [];
@@ -824,53 +1102,118 @@ class Textbox extends IText {
824
1102
  }
825
1103
  }
826
1104
  const spaceCount = spaceIndices.length;
827
- console.log(' Space count:', spaceCount, 'at indices:', spaceIndices);
828
- if (spaceCount === 0) {
829
- console.log(' SKIP: No spaces to expand');
830
- return;
1105
+
1106
+ // Find kashida points if enabled
1107
+ const kashidaPoints = kashidaRatio > 0 ? findKashidaPoints(line) : [];
1108
+ const hasKashidaPoints = kashidaPoints.length > 0;
1109
+
1110
+ // Calculate space distribution
1111
+ let kashidaSpace = 0;
1112
+ if (hasKashidaPoints && kashidaRatio > 0) {
1113
+ // Distribute between kashida and spaces
1114
+ kashidaSpace = totalExtraSpace * kashidaRatio;
831
1115
  }
832
1116
 
833
- // Calculate how much extra space we need
834
- const remainingSpace = this.width - currentLineWidth;
835
- console.log(' Remaining space to fill:', remainingSpace);
836
- if (remainingSpace <= 0) {
837
- console.log(' SKIP: Line already fills or exceeds width');
838
- return;
1117
+ // Calculate per-kashida and per-space widths
1118
+ const perKashidaWidth = hasKashidaPoints ? kashidaSpace / kashidaPoints.length : 0;
1119
+
1120
+ // If kashida is enabled, insert actual tatweel characters
1121
+ if (hasKashidaPoints && perKashidaWidth > 0) {
1122
+ // console.log(`=== Inserting kashida in _applyBrowserJustifySpaces line ${lineIndex} ===`);
1123
+
1124
+ // Sort by charIndex descending to insert from end
1125
+ const sortedPoints = [...kashidaPoints].sort((a, b) => b.charIndex - a.charIndex);
1126
+
1127
+ // Calculate tatweel width
1128
+ const canvas = document.createElement('canvas');
1129
+ const ctx = canvas.getContext('2d');
1130
+ if (ctx) {
1131
+ ctx.font = this._getFontDeclaration();
1132
+ const tatweelWidth = ctx.measureText(ARABIC_TATWEEL).width;
1133
+ // console.log(` tatweelWidth: ${tatweelWidth}`);
1134
+
1135
+ if (tatweelWidth > 0) {
1136
+ const newLine = [...line];
1137
+ for (const point of sortedPoints) {
1138
+ const tatweelCount = Math.max(1, Math.round(perKashidaWidth / tatweelWidth));
1139
+ // console.log(` Point ${point.charIndex}: inserting ${tatweelCount} tatweels`);
1140
+
1141
+ // Insert tatweels after the character
1142
+ for (let t = 0; t < tatweelCount; t++) {
1143
+ newLine.splice(point.charIndex + 1, 0, ARABIC_TATWEEL);
1144
+ }
1145
+
1146
+ // Store kashida info with tatweelCount for index conversion
1147
+ this.__kashidaInfo[lineIndex].push({
1148
+ charIndex: point.charIndex,
1149
+ width: perKashidaWidth,
1150
+ tatweelCount: tatweelCount
1151
+ });
1152
+ }
1153
+
1154
+ // console.log(` New line: ${newLine.join('')}`);
1155
+
1156
+ // Update _textLines with kashida
1157
+ this._textLines[lineIndex] = newLine;
1158
+
1159
+ // Update textLines string version
1160
+ if (this.textLines && this.textLines[lineIndex] !== undefined) {
1161
+ this.textLines[lineIndex] = newLine.join('');
1162
+ }
1163
+
1164
+ // Recalculate charBounds
1165
+ this.__charBounds[lineIndex] = [];
1166
+ this.__lineWidths[lineIndex] = undefined;
1167
+ this._measureLine(lineIndex);
1168
+ }
1169
+ }
1170
+ } else {
1171
+ // No kashida - just store info for reference (tatweelCount is 0 since no tatweels inserted)
1172
+ for (const point of kashidaPoints) {
1173
+ this.__kashidaInfo[lineIndex].push({
1174
+ charIndex: point.charIndex,
1175
+ width: perKashidaWidth,
1176
+ tatweelCount: 0
1177
+ });
1178
+ }
839
1179
  }
840
- const extraPerSpace = remainingSpace / spaceCount;
841
- console.log(' Extra per space:', extraPerSpace);
842
-
843
- // Apply expansion
844
- let accumulated = 0;
845
- for (let charIndex = 0; charIndex < line.length; charIndex++) {
846
- const bound = lineBounds[charIndex];
847
- if (!bound) continue;
848
-
849
- // Shift this character by accumulated expansion
850
- bound.left += accumulated;
851
-
852
- // If this is a space, expand it
853
- if (spaceIndices.includes(charIndex)) {
854
- const oldWidth = bound.width;
855
- const newWidth = oldWidth + extraPerSpace;
856
- bound.width = newWidth;
857
- bound.kernedWidth = newWidth;
858
- accumulated += extraPerSpace;
859
- console.log(` Space at char ${charIndex}: ${oldWidth.toFixed(2)} -> ${newWidth.toFixed(2)} (accumulated: ${accumulated.toFixed(2)})`);
1180
+
1181
+ // Now apply space expansion to remaining extra space
1182
+ const newLineBounds = this.__charBounds[lineIndex];
1183
+ const newLineWidth = newLineBounds.reduce((sum, b) => sum + ((b === null || b === void 0 ? void 0 : b.kernedWidth) || 0), 0);
1184
+ const remainingSpace = this.width - newLineWidth;
1185
+ if (remainingSpace > 0 && spaceCount > 0) {
1186
+ const extraPerSpace = remainingSpace / spaceCount;
1187
+ let accumulated = 0;
1188
+ for (let charIndex = 0; charIndex < this._textLines[lineIndex].length; charIndex++) {
1189
+ const bound = newLineBounds[charIndex];
1190
+ if (!bound) continue;
1191
+ bound.left += accumulated;
1192
+
1193
+ // Check if this is a space (need to check against the updated line)
1194
+ if (/\s/.test(this._textLines[lineIndex][charIndex])) {
1195
+ bound.width += extraPerSpace;
1196
+ bound.kernedWidth += extraPerSpace;
1197
+ accumulated += extraPerSpace;
1198
+ }
860
1199
  }
861
1200
  }
862
1201
 
863
1202
  // Update cached line width
864
- const finalLineWidth = lineBounds.reduce((max, b) => Math.max(max, b.left + b.width), 0);
1203
+ const finalLineBounds = this.__charBounds[lineIndex];
1204
+ const finalLineWidth = finalLineBounds.reduce((max, b) => Math.max(max, ((b === null || b === void 0 ? void 0 : b.left) || 0) + ((b === null || b === void 0 ? void 0 : b.width) || 0)), 0);
865
1205
  this.__lineWidths[lineIndex] = finalLineWidth;
866
- console.log(' Final line width:', finalLineWidth.toFixed(2), 'target:', this.width);
867
1206
  });
868
- console.log('=== _applyBrowserJustifySpaces END ===\n');
869
1207
  this.dirty = true;
870
1208
  // Mark that justify has been applied - for debugging to detect if measureLine overwrites it
871
1209
  this._justifyApplied = true;
872
- // Don't call requestRenderAll here - it will be called by the caller
873
- // and calling it here might trigger another initDimensions that clears justify
1210
+
1211
+ // Debug log final kashida state
1212
+ // console.log('=== _applyBrowserJustifySpaces END ===');
1213
+ // console.log('Final __kashidaInfo:', JSON.stringify(this.__kashidaInfo.map((lineInfo, i) => ({
1214
+ // line: i,
1215
+ // entries: lineInfo.map(k => ({ charIndex: k.charIndex, tatweelCount: k.tatweelCount }))
1216
+ // }))));
874
1217
  }
875
1218
 
876
1219
  /**