@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
@@ -249,66 +249,71 @@ function layoutSingleLine(text: string, options: TextLayoutOptions, textOffset:
249
249
  const graphemes = segmentGraphemes(text);
250
250
  const bounds: GraphemeBounds[] = [];
251
251
  const measurementOptions = createMeasurementOptions(options);
252
-
252
+
253
253
  let x = 0;
254
254
  let lineWidth = 0;
255
255
  let lineHeight = 0;
256
256
  let charIndex = textOffset; // Track character position in original text
257
-
258
- // Measure each grapheme
257
+
258
+ // Measure each grapheme in logical order
259
259
  for (let i = 0; i < graphemes.length; i++) {
260
260
  const grapheme = graphemes[i];
261
261
  const prevGrapheme = i > 0 ? graphemes[i - 1] : undefined;
262
-
262
+
263
263
  // Measure with kerning
264
264
  const measurement = measureGraphemeWithKerning(
265
265
  grapheme,
266
266
  prevGrapheme,
267
267
  measurementOptions
268
268
  );
269
-
269
+
270
270
  // Apply letter spacing (Konva style - applied to ALL characters including last)
271
271
  const letterSpacing = options.letterSpacing || 0;
272
- const charSpacing = options.charSpacing ?
272
+ const charSpacing = options.charSpacing ?
273
273
  (options.fontSize * options.charSpacing) / 1000 : 0;
274
-
274
+
275
275
  const totalSpacing = letterSpacing + charSpacing;
276
276
  const effectiveWidth = measurement.kernedWidth + totalSpacing;
277
-
277
+
278
278
  bounds.push({
279
279
  grapheme,
280
- x,
280
+ x, // Will be updated by BiDi reordering
281
281
  y: 0, // Will be adjusted later
282
282
  width: measurement.width,
283
283
  height: measurement.height,
284
284
  kernedWidth: measurement.kernedWidth,
285
- left: x,
285
+ left: x, // Logical position (cumulative)
286
286
  baseline: measurement.baseline,
287
287
  charIndex: charIndex, // Character position in original text
288
288
  graphemeIndex: textOffset + i, // Grapheme index in original text
289
289
  });
290
-
290
+
291
291
  // Update character index for next iteration
292
292
  charIndex += grapheme.length;
293
-
293
+
294
294
  x += effectiveWidth;
295
295
  lineWidth += effectiveWidth;
296
296
  lineHeight = Math.max(lineHeight, measurement.height);
297
297
  }
298
298
 
299
+ // Note: BiDi visual reordering is handled by the browser's canvas fillText
300
+ // The layout stores positions in logical order; hit testing handles the visual mapping
301
+
299
302
  // Remove trailing spacing from total width (but keep in bounds for rendering)
300
303
  if (bounds.length > 0) {
301
304
  const letterSpacing = options.letterSpacing || 0;
302
- const charSpacing = options.charSpacing ?
305
+ const charSpacing = options.charSpacing ?
303
306
  (options.fontSize * options.charSpacing) / 1000 : 0;
304
307
  const totalSpacing = letterSpacing + charSpacing;
305
-
308
+
306
309
  // Konva applies letterSpacing to all chars, so we don't remove it
307
310
  // lineWidth -= totalSpacing;
308
311
  }
309
312
 
310
313
  // Apply line height
311
- const finalHeight = lineHeight * options.lineHeight;
314
+ // Note: Fabric.js uses _fontSizeMult = 1.13 for line height calculation
315
+ const fontSizeMult = 1.13;
316
+ const finalHeight = lineHeight * options.lineHeight * fontSizeMult;
312
317
 
313
318
  return {
314
319
  text,
@@ -390,18 +395,138 @@ function wrapByCharacters(text: string, maxWidth: number, options: TextLayoutOpt
390
395
  return lines.length > 0 ? lines : [''];
391
396
  }
392
397
 
398
+ /**
399
+ * Apply BiDi visual reordering to calculate correct visual X positions
400
+ * This implements the Unicode Bidirectional Algorithm for character placement
401
+ */
402
+ function applyBiDiVisualReordering(
403
+ line: LayoutLine,
404
+ options: TextLayoutOptions
405
+ ): LayoutLine {
406
+ const baseDirection = options.direction === 'inherit' ? 'ltr' : options.direction;
407
+
408
+ // Quick check: if all characters are same direction as base, no reordering needed
409
+ const runs = analyzeBiDi(line.text, baseDirection);
410
+ const hasMixedBiDi = runs.length > 1 || (runs.length === 1 && runs[0].direction !== baseDirection);
411
+
412
+ if (!hasMixedBiDi) {
413
+ // For pure LTR or pure RTL, just set visual x = logical left
414
+ // For RTL base direction, we need to flip positions
415
+ if (baseDirection === 'rtl') {
416
+ // RTL: rightmost character should be at x=0, leftmost at x=lineWidth
417
+ line.bounds.forEach(bound => {
418
+ bound.x = line.width - bound.left - bound.kernedWidth;
419
+ });
420
+ }
421
+ // For LTR, x is already correct (same as left)
422
+ return line;
423
+ }
424
+
425
+ // Mixed BiDi text - need to reorder runs visually
426
+ // 1. Build mapping from grapheme index to run
427
+ const graphemeToRun: number[] = [];
428
+ let runGraphemeStart = 0;
429
+
430
+ for (let runIdx = 0; runIdx < runs.length; runIdx++) {
431
+ const run = runs[runIdx];
432
+ const runGraphemes = segmentGraphemes(run.text);
433
+ for (let i = 0; i < runGraphemes.length; i++) {
434
+ graphemeToRun.push(runIdx);
435
+ }
436
+ runGraphemeStart += runGraphemes.length;
437
+ }
438
+
439
+ // 2. Calculate run widths and positions
440
+ const runWidths: number[] = [];
441
+ const runStartIndices: number[] = [];
442
+ let currentIdx = 0;
443
+
444
+ for (const run of runs) {
445
+ runStartIndices.push(currentIdx);
446
+ const runGraphemes = segmentGraphemes(run.text);
447
+ let runWidth = 0;
448
+ for (let i = 0; i < runGraphemes.length; i++) {
449
+ if (currentIdx + i < line.bounds.length) {
450
+ const letterSpacing = options.letterSpacing || 0;
451
+ const charSpacing = options.charSpacing ?
452
+ (options.fontSize * options.charSpacing) / 1000 : 0;
453
+ runWidth += line.bounds[currentIdx + i].kernedWidth + letterSpacing + charSpacing;
454
+ }
455
+ }
456
+ runWidths.push(runWidth);
457
+ currentIdx += runGraphemes.length;
458
+ }
459
+
460
+ // 3. Determine visual order of runs based on base direction
461
+ // RTL base: runs display right-to-left (first run on right)
462
+ // LTR base: runs display left-to-right (first run on left)
463
+ const visualRunOrder = runs.map((_, i) => i);
464
+ if (baseDirection === 'rtl') {
465
+ visualRunOrder.reverse();
466
+ }
467
+
468
+ // 4. Calculate visual X position for each run
469
+ const runVisualX: number[] = new Array(runs.length);
470
+ let currentX = 0;
471
+
472
+ for (const runIdx of visualRunOrder) {
473
+ runVisualX[runIdx] = currentX;
474
+ currentX += runWidths[runIdx];
475
+ }
476
+
477
+ // 5. Assign visual X positions to each grapheme
478
+ for (let i = 0; i < line.bounds.length; i++) {
479
+ const runIdx = graphemeToRun[i];
480
+ if (runIdx === undefined) continue;
481
+
482
+ const run = runs[runIdx];
483
+ const runStart = runStartIndices[runIdx];
484
+
485
+ // Calculate spacing once
486
+ const letterSpacing = options.letterSpacing || 0;
487
+ const charSpacing = options.charSpacing ?
488
+ (options.fontSize * options.charSpacing) / 1000 : 0;
489
+ const totalSpacing = letterSpacing + charSpacing;
490
+
491
+ // Calculate offset within run (sum of widths of chars before this one)
492
+ let offsetInRun = 0;
493
+ for (let j = runStart; j < i; j++) {
494
+ offsetInRun += line.bounds[j].kernedWidth + totalSpacing;
495
+ }
496
+
497
+ // Character width including spacing
498
+ const charWidth = line.bounds[i].kernedWidth + totalSpacing;
499
+
500
+ // For RTL runs, characters within the run are reversed visually
501
+ // First logical char appears on the right, last on the left
502
+ if (run.direction === 'rtl') {
503
+ // Visual X = run right edge - cumulative width including this char
504
+ // This places first char at right side of run, last char at left side
505
+ line.bounds[i].x = runVisualX[runIdx] + runWidths[runIdx] - offsetInRun - charWidth;
506
+ } else {
507
+ // LTR run: visual position is run start + offset within run
508
+ line.bounds[i].x = runVisualX[runIdx] + offsetInRun;
509
+ }
510
+ }
511
+
512
+ return line;
513
+ }
514
+
393
515
  /**
394
516
  * Apply text alignment to lines
395
517
  */
396
518
  function applyAlignment(
397
- lines: LayoutLine[],
398
- align: string,
519
+ lines: LayoutLine[],
520
+ align: string,
399
521
  containerWidth: number,
400
522
  options: TextLayoutOptions
401
523
  ): LayoutLine[] {
402
524
  return lines.map(line => {
525
+ // First apply BiDi reordering to get correct visual X positions
526
+ applyBiDiVisualReordering(line, options);
527
+
403
528
  let offsetX = 0;
404
-
529
+
405
530
  switch (align) {
406
531
  case 'center':
407
532
  offsetX = (containerWidth - line.width) / 2;
@@ -419,15 +544,15 @@ function applyAlignment(
419
544
  offsetX = 0;
420
545
  break;
421
546
  }
422
-
423
- // Apply offset to all bounds
547
+
548
+ // Apply offset to all bounds (both visual x and logical left for alignment)
424
549
  if (offsetX !== 0) {
425
550
  line.bounds.forEach(bound => {
426
551
  bound.x += offsetX;
427
552
  bound.left += offsetX;
428
553
  });
429
554
  }
430
-
555
+
431
556
  return line;
432
557
  });
433
558
  }
@@ -530,8 +655,10 @@ function handleHeightOverflow(
530
655
  * Create empty line for empty paragraphs
531
656
  */
532
657
  function createEmptyLine(options: TextLayoutOptions): LayoutLine {
533
- const height = options.fontSize * options.lineHeight;
534
-
658
+ // Fabric.js uses _fontSizeMult = 1.13 for line height calculation
659
+ const fontSizeMult = 1.13;
660
+ const height = options.fontSize * options.lineHeight * fontSizeMult;
661
+
535
662
  return {
536
663
  text: '',
537
664
  graphemes: [],