@nasser-sw/fabric 7.0.1-beta1 → 7.0.1-beta10

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 (181) hide show
  1. package/0 +0 -0
  2. package/debug/{konva → konva-master}/CHANGELOG.md +2 -1
  3. package/debug/{konva → konva-master}/README.md +7 -3
  4. package/debug/{konva → konva-master}/package.json +1 -1
  5. package/debug/{konva → konva-master}/release.sh +1 -4
  6. package/debug/{konva → konva-master}/src/Canvas.ts +37 -0
  7. package/debug/{konva → konva-master}/src/shapes/Text.ts +2 -2
  8. package/dist/index.js +1853 -288
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.min.js +1 -1
  11. package/dist/index.min.js.map +1 -1
  12. package/dist/index.min.mjs +1 -1
  13. package/dist/index.min.mjs.map +1 -1
  14. package/dist/index.mjs +1853 -288
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/index.node.cjs +1853 -288
  17. package/dist/index.node.cjs.map +1 -1
  18. package/dist/index.node.mjs +1853 -288
  19. package/dist/index.node.mjs.map +1 -1
  20. package/dist/package.json.min.mjs +1 -1
  21. package/dist/package.json.mjs +1 -1
  22. package/dist/src/shapes/Line.d.ts +33 -86
  23. package/dist/src/shapes/Line.d.ts.map +1 -1
  24. package/dist/src/shapes/Line.min.mjs +1 -1
  25. package/dist/src/shapes/Line.min.mjs.map +1 -1
  26. package/dist/src/shapes/Line.mjs +405 -159
  27. package/dist/src/shapes/Line.mjs.map +1 -1
  28. package/dist/src/shapes/Polyline.d.ts +7 -0
  29. package/dist/src/shapes/Polyline.d.ts.map +1 -1
  30. package/dist/src/shapes/Polyline.min.mjs +1 -1
  31. package/dist/src/shapes/Polyline.min.mjs.map +1 -1
  32. package/dist/src/shapes/Polyline.mjs +48 -16
  33. package/dist/src/shapes/Polyline.mjs.map +1 -1
  34. package/dist/src/shapes/Text/Text.d.ts +19 -0
  35. package/dist/src/shapes/Text/Text.d.ts.map +1 -1
  36. package/dist/src/shapes/Text/Text.min.mjs +1 -1
  37. package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
  38. package/dist/src/shapes/Text/Text.mjs +302 -16
  39. package/dist/src/shapes/Text/Text.mjs.map +1 -1
  40. package/dist/src/shapes/Textbox.d.ts +43 -1
  41. package/dist/src/shapes/Textbox.d.ts.map +1 -1
  42. package/dist/src/shapes/Textbox.min.mjs +1 -1
  43. package/dist/src/shapes/Textbox.min.mjs.map +1 -1
  44. package/dist/src/shapes/Textbox.mjs +521 -67
  45. package/dist/src/shapes/Textbox.mjs.map +1 -1
  46. package/dist/src/shapes/Triangle.d.ts +27 -2
  47. package/dist/src/shapes/Triangle.d.ts.map +1 -1
  48. package/dist/src/shapes/Triangle.min.mjs +1 -1
  49. package/dist/src/shapes/Triangle.min.mjs.map +1 -1
  50. package/dist/src/shapes/Triangle.mjs +72 -12
  51. package/dist/src/shapes/Triangle.mjs.map +1 -1
  52. package/dist/src/text/examples/arabicTextExample.d.ts +60 -0
  53. package/dist/src/text/examples/arabicTextExample.d.ts.map +1 -0
  54. package/dist/src/text/measure.d.ts +9 -0
  55. package/dist/src/text/measure.d.ts.map +1 -1
  56. package/dist/src/text/measure.min.mjs +1 -1
  57. package/dist/src/text/measure.min.mjs.map +1 -1
  58. package/dist/src/text/measure.mjs +175 -4
  59. package/dist/src/text/measure.mjs.map +1 -1
  60. package/dist/src/text/overlayEditor.d.ts.map +1 -1
  61. package/dist/src/text/overlayEditor.min.mjs +1 -1
  62. package/dist/src/text/overlayEditor.min.mjs.map +1 -1
  63. package/dist/src/text/overlayEditor.mjs +155 -9
  64. package/dist/src/text/overlayEditor.mjs.map +1 -1
  65. package/dist/src/text/scriptUtils.d.ts +142 -0
  66. package/dist/src/text/scriptUtils.d.ts.map +1 -0
  67. package/dist/src/text/scriptUtils.min.mjs +2 -0
  68. package/dist/src/text/scriptUtils.min.mjs.map +1 -0
  69. package/dist/src/text/scriptUtils.mjs +212 -0
  70. package/dist/src/text/scriptUtils.mjs.map +1 -0
  71. package/dist/src/util/misc/cornerRadius.d.ts +70 -0
  72. package/dist/src/util/misc/cornerRadius.d.ts.map +1 -0
  73. package/dist/src/util/misc/cornerRadius.min.mjs +2 -0
  74. package/dist/src/util/misc/cornerRadius.min.mjs.map +1 -0
  75. package/dist/src/util/misc/cornerRadius.mjs +181 -0
  76. package/dist/src/util/misc/cornerRadius.mjs.map +1 -0
  77. package/dist-extensions/src/shapes/CustomLine.d.ts +10 -0
  78. package/dist-extensions/src/shapes/CustomLine.d.ts.map +1 -0
  79. package/dist-extensions/src/shapes/Line.d.ts +33 -86
  80. package/dist-extensions/src/shapes/Line.d.ts.map +1 -1
  81. package/dist-extensions/src/shapes/Polyline.d.ts +7 -0
  82. package/dist-extensions/src/shapes/Polyline.d.ts.map +1 -1
  83. package/dist-extensions/src/shapes/Text/Text.d.ts +19 -0
  84. package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
  85. package/dist-extensions/src/shapes/Textbox.d.ts +43 -1
  86. package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
  87. package/dist-extensions/src/shapes/Triangle.d.ts +27 -2
  88. package/dist-extensions/src/shapes/Triangle.d.ts.map +1 -1
  89. package/dist-extensions/src/text/measure.d.ts +9 -0
  90. package/dist-extensions/src/text/measure.d.ts.map +1 -1
  91. package/dist-extensions/src/text/overlayEditor.d.ts.map +1 -1
  92. package/dist-extensions/src/text/scriptUtils.d.ts +142 -0
  93. package/dist-extensions/src/text/scriptUtils.d.ts.map +1 -0
  94. package/dist-extensions/src/util/misc/cornerRadius.d.ts +70 -0
  95. package/dist-extensions/src/util/misc/cornerRadius.d.ts.map +1 -0
  96. package/fabric-test-editor.html +3552 -0
  97. package/fabric-test2.html +647 -0
  98. package/fabric.ts +182 -182
  99. package/fonts/STV Bold.ttf +0 -0
  100. package/fonts/STV Light.ttf +0 -0
  101. package/fonts/STV Regular.ttf +0 -0
  102. package/package.json +164 -164
  103. package/src/shapes/Line.ts +484 -157
  104. package/src/shapes/Polyline.ts +70 -29
  105. package/src/shapes/Text/Text.ts +317 -19
  106. package/src/shapes/Textbox.ts +544 -12
  107. package/src/shapes/Triangle.spec.ts +76 -0
  108. package/src/shapes/Triangle.ts +85 -15
  109. package/src/text/measure.ts +200 -50
  110. package/src/text/overlayEditor.ts +164 -12
  111. package/src/util/misc/cornerRadius.spec.ts +141 -0
  112. package/src/util/misc/cornerRadius.ts +269 -0
  113. /package/debug/{konva → konva-master}/LICENSE +0 -0
  114. /package/debug/{konva → konva-master}/gulpfile.mjs +0 -0
  115. /package/debug/{konva → konva-master}/resources/doc-includes/ContainerParams.txt +0 -0
  116. /package/debug/{konva → konva-master}/resources/doc-includes/NodeParams.txt +0 -0
  117. /package/debug/{konva → konva-master}/resources/doc-includes/ShapeParams.txt +0 -0
  118. /package/debug/{konva → konva-master}/resources/jsdoc.conf.json +0 -0
  119. /package/debug/{konva → konva-master}/rollup.config.mjs +0 -0
  120. /package/debug/{konva → konva-master}/src/Animation.ts +0 -0
  121. /package/debug/{konva → konva-master}/src/BezierFunctions.ts +0 -0
  122. /package/debug/{konva → konva-master}/src/Container.ts +0 -0
  123. /package/debug/{konva → konva-master}/src/Context.ts +0 -0
  124. /package/debug/{konva → konva-master}/src/Core.ts +0 -0
  125. /package/debug/{konva → konva-master}/src/DragAndDrop.ts +0 -0
  126. /package/debug/{konva → konva-master}/src/Factory.ts +0 -0
  127. /package/debug/{konva → konva-master}/src/FastLayer.ts +0 -0
  128. /package/debug/{konva → konva-master}/src/Global.ts +0 -0
  129. /package/debug/{konva → konva-master}/src/Group.ts +0 -0
  130. /package/debug/{konva → konva-master}/src/Layer.ts +0 -0
  131. /package/debug/{konva → konva-master}/src/Node.ts +0 -0
  132. /package/debug/{konva → konva-master}/src/PointerEvents.ts +0 -0
  133. /package/debug/{konva → konva-master}/src/Shape.ts +0 -0
  134. /package/debug/{konva → konva-master}/src/Stage.ts +0 -0
  135. /package/debug/{konva → konva-master}/src/Tween.ts +0 -0
  136. /package/debug/{konva → konva-master}/src/Util.ts +0 -0
  137. /package/debug/{konva → konva-master}/src/Validators.ts +0 -0
  138. /package/debug/{konva → konva-master}/src/_CoreInternals.ts +0 -0
  139. /package/debug/{konva → konva-master}/src/_FullInternals.ts +0 -0
  140. /package/debug/{konva → konva-master}/src/canvas-backend.ts +0 -0
  141. /package/debug/{konva → konva-master}/src/filters/Blur.ts +0 -0
  142. /package/debug/{konva → konva-master}/src/filters/Brighten.ts +0 -0
  143. /package/debug/{konva → konva-master}/src/filters/Brightness.ts +0 -0
  144. /package/debug/{konva → konva-master}/src/filters/Contrast.ts +0 -0
  145. /package/debug/{konva → konva-master}/src/filters/Emboss.ts +0 -0
  146. /package/debug/{konva → konva-master}/src/filters/Enhance.ts +0 -0
  147. /package/debug/{konva → konva-master}/src/filters/Grayscale.ts +0 -0
  148. /package/debug/{konva → konva-master}/src/filters/HSL.ts +0 -0
  149. /package/debug/{konva → konva-master}/src/filters/HSV.ts +0 -0
  150. /package/debug/{konva → konva-master}/src/filters/Invert.ts +0 -0
  151. /package/debug/{konva → konva-master}/src/filters/Kaleidoscope.ts +0 -0
  152. /package/debug/{konva → konva-master}/src/filters/Mask.ts +0 -0
  153. /package/debug/{konva → konva-master}/src/filters/Noise.ts +0 -0
  154. /package/debug/{konva → konva-master}/src/filters/Pixelate.ts +0 -0
  155. /package/debug/{konva → konva-master}/src/filters/Posterize.ts +0 -0
  156. /package/debug/{konva → konva-master}/src/filters/RGB.ts +0 -0
  157. /package/debug/{konva → konva-master}/src/filters/RGBA.ts +0 -0
  158. /package/debug/{konva → konva-master}/src/filters/Sepia.ts +0 -0
  159. /package/debug/{konva → konva-master}/src/filters/Solarize.ts +0 -0
  160. /package/debug/{konva → konva-master}/src/filters/Threshold.ts +0 -0
  161. /package/debug/{konva → konva-master}/src/index.ts +0 -0
  162. /package/debug/{konva → konva-master}/src/shapes/Arc.ts +0 -0
  163. /package/debug/{konva → konva-master}/src/shapes/Arrow.ts +0 -0
  164. /package/debug/{konva → konva-master}/src/shapes/Circle.ts +0 -0
  165. /package/debug/{konva → konva-master}/src/shapes/Ellipse.ts +0 -0
  166. /package/debug/{konva → konva-master}/src/shapes/Image.ts +0 -0
  167. /package/debug/{konva → konva-master}/src/shapes/Label.ts +0 -0
  168. /package/debug/{konva → konva-master}/src/shapes/Line.ts +0 -0
  169. /package/debug/{konva → konva-master}/src/shapes/Path.ts +0 -0
  170. /package/debug/{konva → konva-master}/src/shapes/Rect.ts +0 -0
  171. /package/debug/{konva → konva-master}/src/shapes/RegularPolygon.ts +0 -0
  172. /package/debug/{konva → konva-master}/src/shapes/Ring.ts +0 -0
  173. /package/debug/{konva → konva-master}/src/shapes/Sprite.ts +0 -0
  174. /package/debug/{konva → konva-master}/src/shapes/Star.ts +0 -0
  175. /package/debug/{konva → konva-master}/src/shapes/TextPath.ts +0 -0
  176. /package/debug/{konva → konva-master}/src/shapes/Transformer.ts +0 -0
  177. /package/debug/{konva → konva-master}/src/shapes/Wedge.ts +0 -0
  178. /package/debug/{konva → konva-master}/src/skia-backend.ts +0 -0
  179. /package/debug/{konva → konva-master}/src/types.ts +0 -0
  180. /package/debug/{konva → konva-master}/tsconfig.json +0 -0
  181. /package/debug/{konva → konva-master}/tsconfig.test.json +0 -0
@@ -337,21 +337,101 @@ export class OverlayEditor {
337
337
 
338
338
  // Apply all other font and text styles to match Fabric
339
339
  const letterSpacingPx = ((target.charSpacing || 0) / 1000) * finalFontSize;
340
+
341
+ // Special handling for text objects loaded from JSON - ensure they're properly initialized
342
+ if (target.dirty !== false && target.initDimensions) {
343
+ console.log('🔧 Ensuring text object is properly initialized before overlay editing');
344
+ // Force re-initialization if the text object seems to be in a dirty state
345
+ target.initDimensions();
346
+ }
340
347
 
341
348
  this.textarea.style.fontSize = `${finalFontSize}px`;
342
349
  this.textarea.style.lineHeight = String(fabricLineHeight);
343
350
  this.textarea.style.fontFamily = target.fontFamily || 'Arial';
344
351
  this.textarea.style.fontWeight = String(target.fontWeight || 'normal');
345
352
  this.textarea.style.fontStyle = target.fontStyle || 'normal';
346
- this.textarea.style.textAlign = (target as any).textAlign || 'left';
353
+ // Handle text alignment and justification
354
+ const textAlign = (target as any).textAlign || 'left';
355
+ let cssTextAlign = textAlign;
356
+
357
+ // Detect text direction from content for proper justify handling
358
+ const autoDetectedDirection = this.firstStrongDir(this.textarea.value || '');
359
+
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);
366
+
367
+ // Map fabric.js justify to CSS
368
+ if (textAlign.includes('justify')) {
369
+ // Try to match fabric.js justify behavior more precisely
370
+ try {
371
+ // For justify, we need to replicate fabric.js space expansion
372
+ // Use CSS justify but with specific settings to match fabric.js better
373
+ cssTextAlign = 'justify';
374
+
375
+ // Set text-align-last based on justify type and detected direction
376
+ // Smart justify: respect detected direction even when fabric alignment doesn't match
377
+ if (textAlign === 'justify') {
378
+ this.textarea.style.textAlignLast = autoDetectedDirection === 'rtl' ? 'right' : 'left';
379
+ } else if (textAlign === 'justify-left') {
380
+ // If text is RTL but fabric says justify-left, override to justify-right for better UX
381
+ if (autoDetectedDirection === 'rtl') {
382
+ this.textarea.style.textAlignLast = 'right';
383
+ console.log(' → Overrode justify-left to justify-right for RTL text');
384
+ } else {
385
+ this.textarea.style.textAlignLast = 'left';
386
+ }
387
+ } else if (textAlign === 'justify-right') {
388
+ // If text is LTR but fabric says justify-right, override to justify-left for better UX
389
+ if (autoDetectedDirection === 'ltr') {
390
+ this.textarea.style.textAlignLast = 'left';
391
+ console.log(' → Overrode justify-right to justify-left for LTR text');
392
+ } else {
393
+ this.textarea.style.textAlignLast = 'right';
394
+ }
395
+ } else if (textAlign === 'justify-center') {
396
+ this.textarea.style.textAlignLast = 'center';
397
+ }
398
+
399
+ // Enhanced justify settings for better fabric.js matching
400
+ (this.textarea.style as any).textJustify = 'inter-word';
401
+ (this.textarea.style as any).wordSpacing = 'normal';
402
+
403
+ // Additional CSS properties for better justify matching
404
+ this.textarea.style.textAlign = 'justify';
405
+ this.textarea.style.textAlignLast = this.textarea.style.textAlignLast;
406
+
407
+ // Try to force better justify behavior
408
+ (this.textarea.style as any).textJustifyTrim = 'none';
409
+ (this.textarea.style as any).textAutospace = 'none';
410
+
411
+ console.log(' → Applied justify alignment:', textAlign, 'with last-line:', this.textarea.style.textAlignLast);
412
+ } catch (error) {
413
+ console.warn(' → Justify setup failed, falling back to standard alignment:', error);
414
+ cssTextAlign = textAlign.replace('justify-', '').replace('justify', 'left');
415
+ }
416
+ } else {
417
+ this.textarea.style.textAlignLast = 'auto';
418
+ (this.textarea.style as any).textJustify = 'auto';
419
+ (this.textarea.style as any).wordSpacing = 'normal';
420
+ console.log(' → Applied standard alignment:', cssTextAlign);
421
+ }
422
+
423
+ this.textarea.style.textAlign = cssTextAlign;
347
424
  this.textarea.style.color = target.fill?.toString() || '#000';
348
425
  this.textarea.style.letterSpacing = `${letterSpacingPx}px`;
349
- this.textarea.style.direction =
350
- (target as any).direction ||
351
- this.firstStrongDir(this.textarea.value || '');
426
+
427
+ // Use the already detected direction from above
428
+ const fabricDirection = (target as any).direction;
429
+
430
+ // Use auto-detected direction for better BiDi support, but respect fabric direction if it makes sense
431
+ this.textarea.style.direction = autoDetectedDirection || fabricDirection || 'ltr';
352
432
  this.textarea.style.fontVariant = 'normal';
353
433
  this.textarea.style.fontStretch = 'normal';
354
- this.textarea.style.textRendering = 'optimizeLegibility';
434
+ this.textarea.style.textRendering = 'auto'; // Changed from 'optimizeLegibility' to match canvas
355
435
  this.textarea.style.fontKerning = 'normal';
356
436
  this.textarea.style.fontFeatureSettings = 'normal';
357
437
  this.textarea.style.fontVariationSettings = 'normal';
@@ -363,14 +443,61 @@ export class OverlayEditor {
363
443
  this.textarea.style.whiteSpace = 'pre-wrap';
364
444
  this.textarea.style.hyphens = 'none';
365
445
 
366
- (this.textarea.style as any).webkitFontSmoothing = 'antialiased';
367
- (this.textarea.style as any).mozOsxFontSmoothing = 'grayscale';
368
-
369
- // Debug: Compare textarea and canvas object bounding boxes
370
- this.debugBoundingBoxComparison();
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);
456
+
457
+ // If justify, log Fabric object dimensions for comparison
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);
464
+ }
465
+
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);
476
+
477
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
478
+
479
+ // Enhanced font rendering to better match fabric.js canvas rendering
480
+ // Default to auto for more natural rendering
481
+ (this.textarea.style as any).webkitFontSmoothing = 'auto';
482
+ (this.textarea.style as any).mozOsxFontSmoothing = 'auto';
483
+ (this.textarea.style as any).fontSmooth = 'auto';
484
+ (this.textarea.style as any).textSizeAdjust = 'none';
485
+
486
+ // For bold fonts, use subpixel rendering to match canvas thickness better
487
+ const fontWeight = String(target.fontWeight || 'normal');
488
+ const isBold = fontWeight === 'bold' || fontWeight === '700' ||
489
+ (parseInt(fontWeight) >= 600);
490
+
491
+ if (isBold) {
492
+ (this.textarea.style as any).webkitFontSmoothing = 'subpixel-antialiased';
493
+ (this.textarea.style as any).mozOsxFontSmoothing = 'unset';
494
+ console.log('🔤 Applied enhanced bold rendering for better thickness matching');
495
+ }
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);
371
500
 
372
- // Debug: Compare text wrapping behavior
373
- this.debugTextWrapping();
374
501
 
375
502
  // Initial bounds are set correctly by Fabric.js - don't force update here
376
503
  }
@@ -615,6 +742,11 @@ export class OverlayEditor {
615
742
  this.target.setCoords();
616
743
  this.applyOverlayStyle();
617
744
 
745
+ // Fix character mapping issues after JSON loading for browser-wrapped fonts
746
+ if ((this.target as any)._fixCharacterMappingAfterJsonLoad) {
747
+ (this.target as any)._fixCharacterMappingAfterJsonLoad();
748
+ }
749
+
618
750
  this.textarea.focus();
619
751
 
620
752
  this.textarea.setSelectionRange(
@@ -664,6 +796,26 @@ export class OverlayEditor {
664
796
  // Handle commit/cancel after restoring visibility
665
797
  if (commit && !this.isComposing) {
666
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
+
804
+ if (detectedDirection && detectedDirection !== currentDirection) {
805
+ console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
806
+ console.log(` Text content: "${finalText.substring(0, 50)}..."`);
807
+
808
+ // Update the fabric object's direction
809
+ (this.target as any).set('direction', detectedDirection);
810
+
811
+ // Force a re-render to apply the direction change
812
+ this.canvas.requestRenderAll();
813
+
814
+ console.log(`✅ Fabric object direction updated to: ${detectedDirection}`);
815
+ } else {
816
+ console.log(`📝 Overlay Exit: Direction unchanged (${currentDirection}), text: "${finalText.substring(0, 30)}..."`);
817
+ }
818
+
667
819
  if (this.onCommit) {
668
820
  this.onCommit(finalText);
669
821
  }
@@ -0,0 +1,141 @@
1
+ import {
2
+ pointDistance,
3
+ normalizeVector,
4
+ angleBetweenVectors,
5
+ getMaxRadius,
6
+ calculateRoundedCorner,
7
+ applyCornerRadiusToPolygon,
8
+ generateRoundedPolygonPath,
9
+ } from './cornerRadius';
10
+
11
+ describe('cornerRadius utilities', () => {
12
+ describe('pointDistance', () => {
13
+ it('should calculate distance between two points correctly', () => {
14
+ expect(pointDistance({ x: 0, y: 0 }, { x: 3, y: 4 })).toBe(5);
15
+ expect(pointDistance({ x: 1, y: 1 }, { x: 1, y: 1 })).toBe(0);
16
+ expect(pointDistance({ x: 0, y: 0 }, { x: 1, y: 0 })).toBe(1);
17
+ });
18
+ });
19
+
20
+ describe('normalizeVector', () => {
21
+ it('should normalize vectors correctly', () => {
22
+ const result = normalizeVector({ x: 3, y: 4 });
23
+ expect(result.x).toBeCloseTo(0.6);
24
+ expect(result.y).toBeCloseTo(0.8);
25
+ });
26
+
27
+ it('should handle zero vector', () => {
28
+ const result = normalizeVector({ x: 0, y: 0 });
29
+ expect(result).toEqual({ x: 0, y: 0 });
30
+ });
31
+ });
32
+
33
+ describe('getMaxRadius', () => {
34
+ it('should return half of the shortest adjacent edge', () => {
35
+ const prevPoint = { x: 0, y: 0 };
36
+ const currentPoint = { x: 10, y: 0 };
37
+ const nextPoint = { x: 10, y: 5 };
38
+
39
+ expect(getMaxRadius(prevPoint, currentPoint, nextPoint)).toBe(2.5);
40
+ });
41
+ });
42
+
43
+ describe('calculateRoundedCorner', () => {
44
+ it('should calculate rounded corner data correctly', () => {
45
+ const prevPoint = { x: 0, y: 0 };
46
+ const currentPoint = { x: 10, y: 0 };
47
+ const nextPoint = { x: 10, y: 10 };
48
+ const radius = 2;
49
+
50
+ const result = calculateRoundedCorner(prevPoint, currentPoint, nextPoint, radius);
51
+
52
+ expect(result.corner).toEqual(currentPoint);
53
+ expect(result.actualRadius).toBe(radius);
54
+ expect(result.start.x).toBeCloseTo(8);
55
+ expect(result.start.y).toBeCloseTo(0);
56
+ expect(result.end.x).toBeCloseTo(10);
57
+ expect(result.end.y).toBeCloseTo(2);
58
+ });
59
+
60
+ it('should constrain radius to maximum allowed', () => {
61
+ const prevPoint = { x: 0, y: 0 };
62
+ const currentPoint = { x: 2, y: 0 };
63
+ const nextPoint = { x: 2, y: 2 };
64
+ const radius = 5; // Request larger radius than possible
65
+
66
+ const result = calculateRoundedCorner(prevPoint, currentPoint, nextPoint, radius);
67
+
68
+ expect(result.actualRadius).toBe(1); // Should be constrained to 1
69
+ });
70
+ });
71
+
72
+ describe('applyCornerRadiusToPolygon', () => {
73
+ it('should apply corner radius to a simple triangle', () => {
74
+ const points = [
75
+ { x: 0, y: 0 },
76
+ { x: 10, y: 0 },
77
+ { x: 5, y: 10 }
78
+ ];
79
+ const radius = 2;
80
+
81
+ const result = applyCornerRadiusToPolygon(points, radius);
82
+
83
+ expect(result).toHaveLength(3);
84
+ expect(result[0].actualRadius).toBeCloseTo(2);
85
+ expect(result[1].actualRadius).toBeCloseTo(2);
86
+ expect(result[2].actualRadius).toBeCloseTo(2);
87
+ });
88
+
89
+ it('should handle percentage-based radius', () => {
90
+ const points = [
91
+ { x: 0, y: 0 },
92
+ { x: 100, y: 0 },
93
+ { x: 100, y: 100 },
94
+ { x: 0, y: 100 }
95
+ ];
96
+ const radius = 10; // 10%
97
+
98
+ const result = applyCornerRadiusToPolygon(points, radius, true);
99
+
100
+ expect(result).toHaveLength(4);
101
+ expect(result[0].actualRadius).toBeCloseTo(10); // 10% of 100px = 10px
102
+ });
103
+
104
+ it('should throw error for polygons with less than 3 points', () => {
105
+ expect(() => {
106
+ applyCornerRadiusToPolygon([{ x: 0, y: 0 }, { x: 1, y: 1 }], 5);
107
+ }).toThrow('Polygon must have at least 3 points');
108
+ });
109
+ });
110
+
111
+ describe('generateRoundedPolygonPath', () => {
112
+ it('should generate valid SVG path data', () => {
113
+ const points = [
114
+ { x: 0, y: 0 },
115
+ { x: 10, y: 0 },
116
+ { x: 5, y: 10 }
117
+ ];
118
+ const roundedCorners = applyCornerRadiusToPolygon(points, 2);
119
+
120
+ const pathData = generateRoundedPolygonPath(roundedCorners, true);
121
+
122
+ expect(pathData).toContain('M '); // Should start with move command
123
+ expect(pathData).toContain('C '); // Should contain bezier curve commands
124
+ expect(pathData).toContain('L '); // Should contain line commands
125
+ expect(pathData).toContain('Z'); // Should end with close command for closed path
126
+ });
127
+
128
+ it('should generate open path when closed=false', () => {
129
+ const points = [
130
+ { x: 0, y: 0 },
131
+ { x: 10, y: 0 },
132
+ { x: 5, y: 10 }
133
+ ];
134
+ const roundedCorners = applyCornerRadiusToPolygon(points, 2);
135
+
136
+ const pathData = generateRoundedPolygonPath(roundedCorners, false);
137
+
138
+ expect(pathData).not.toContain('Z'); // Should not end with close command
139
+ });
140
+ });
141
+ });
@@ -0,0 +1,269 @@
1
+ import type { XY } from '../../Point';
2
+ import { Point } from '../../Point';
3
+ import { kRect } from '../../constants';
4
+
5
+ export interface CornerRadiusOptions {
6
+ /**
7
+ * Corner radius value
8
+ */
9
+ radius: number;
10
+ /**
11
+ * Whether to apply radius as percentage of the smallest dimension
12
+ */
13
+ radiusAsPercentage?: boolean;
14
+ }
15
+
16
+ export interface RoundedCornerPoint {
17
+ /**
18
+ * Original corner point
19
+ */
20
+ corner: XY;
21
+ /**
22
+ * Start point of the rounded corner arc
23
+ */
24
+ start: XY;
25
+ /**
26
+ * End point of the rounded corner arc
27
+ */
28
+ end: XY;
29
+ /**
30
+ * First control point for bezier curve
31
+ */
32
+ cp1: XY;
33
+ /**
34
+ * Second control point for bezier curve
35
+ */
36
+ cp2: XY;
37
+ /**
38
+ * Actual radius used (may be different from requested if constrained)
39
+ */
40
+ actualRadius: number;
41
+ }
42
+
43
+ /**
44
+ * Calculate the distance between two points
45
+ */
46
+ export function pointDistance(p1: XY, p2: XY): number {
47
+ return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
48
+ }
49
+
50
+ /**
51
+ * Normalize a vector
52
+ */
53
+ export function normalizeVector(vector: XY): XY {
54
+ const length = Math.sqrt(vector.x * vector.x + vector.y * vector.y);
55
+ if (length === 0) return { x: 0, y: 0 };
56
+ return { x: vector.x / length, y: vector.y / length };
57
+ }
58
+
59
+ /**
60
+ * Calculate the angle between two vectors
61
+ */
62
+ export function angleBetweenVectors(v1: XY, v2: XY): number {
63
+ const dot = v1.x * v2.x + v1.y * v2.y;
64
+ const det = v1.x * v2.y - v1.y * v2.x;
65
+ return Math.atan2(det, dot);
66
+ }
67
+
68
+ /**
69
+ * Get the maximum allowed radius for a corner based on adjacent edge lengths
70
+ */
71
+ export function getMaxRadius(
72
+ prevPoint: XY,
73
+ currentPoint: XY,
74
+ nextPoint: XY,
75
+ ): number {
76
+ const dist1 = pointDistance(prevPoint, currentPoint);
77
+ const dist2 = pointDistance(currentPoint, nextPoint);
78
+ return Math.min(dist1, dist2) / 2;
79
+ }
80
+
81
+ /**
82
+ * Calculate rounded corner data for a single corner
83
+ */
84
+ export function calculateRoundedCorner(
85
+ prevPoint: XY,
86
+ currentPoint: XY,
87
+ nextPoint: XY,
88
+ radius: number,
89
+ ): RoundedCornerPoint {
90
+ // Calculate edge vectors
91
+ const edge1 = {
92
+ x: currentPoint.x - prevPoint.x,
93
+ y: currentPoint.y - prevPoint.y,
94
+ };
95
+ const edge2 = {
96
+ x: nextPoint.x - currentPoint.x,
97
+ y: nextPoint.y - currentPoint.y,
98
+ };
99
+
100
+ // Normalize edge vectors
101
+ const norm1 = normalizeVector(edge1);
102
+ const norm2 = normalizeVector(edge2);
103
+
104
+ // Calculate the maximum allowed radius
105
+ const maxRadius = getMaxRadius(prevPoint, currentPoint, nextPoint);
106
+ const actualRadius = Math.min(radius, maxRadius);
107
+
108
+ // Calculate start and end points of the rounded corner
109
+ const startPoint = {
110
+ x: currentPoint.x - norm1.x * actualRadius,
111
+ y: currentPoint.y - norm1.y * actualRadius,
112
+ };
113
+
114
+ const endPoint = {
115
+ x: currentPoint.x + norm2.x * actualRadius,
116
+ y: currentPoint.y + norm2.y * actualRadius,
117
+ };
118
+
119
+ // Calculate control points for bezier curve
120
+ // Using the magic number kRect for optimal circular approximation
121
+ const controlOffset = actualRadius * kRect;
122
+
123
+ const cp1 = {
124
+ x: startPoint.x + norm1.x * controlOffset,
125
+ y: startPoint.y + norm1.y * controlOffset,
126
+ };
127
+
128
+ const cp2 = {
129
+ x: endPoint.x - norm2.x * controlOffset,
130
+ y: endPoint.y - norm2.y * controlOffset,
131
+ };
132
+
133
+ return {
134
+ corner: currentPoint,
135
+ start: startPoint,
136
+ end: endPoint,
137
+ cp1,
138
+ cp2,
139
+ actualRadius,
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Apply corner radius to a polygon defined by points
145
+ */
146
+ export function applyCornerRadiusToPolygon(
147
+ points: XY[],
148
+ radius: number,
149
+ radiusAsPercentage = false,
150
+ ): RoundedCornerPoint[] {
151
+ if (points.length < 3) {
152
+ throw new Error('Polygon must have at least 3 points');
153
+ }
154
+
155
+ // Calculate bounding box if radius is percentage-based
156
+ let actualRadius = radius;
157
+ if (radiusAsPercentage) {
158
+ const minX = Math.min(...points.map((p) => p.x));
159
+ const maxX = Math.max(...points.map((p) => p.x));
160
+ const minY = Math.min(...points.map((p) => p.y));
161
+ const maxY = Math.max(...points.map((p) => p.y));
162
+ const width = maxX - minX;
163
+ const height = maxY - minY;
164
+ const minDimension = Math.min(width, height);
165
+ actualRadius = (radius / 100) * minDimension;
166
+ }
167
+
168
+ const roundedCorners: RoundedCornerPoint[] = [];
169
+
170
+ for (let i = 0; i < points.length; i++) {
171
+ const prevIndex = (i - 1 + points.length) % points.length;
172
+ const nextIndex = (i + 1) % points.length;
173
+
174
+ const prevPoint = points[prevIndex];
175
+ const currentPoint = points[i];
176
+ const nextPoint = points[nextIndex];
177
+
178
+ const roundedCorner = calculateRoundedCorner(
179
+ prevPoint,
180
+ currentPoint,
181
+ nextPoint,
182
+ actualRadius,
183
+ );
184
+
185
+ roundedCorners.push(roundedCorner);
186
+ }
187
+
188
+ return roundedCorners;
189
+ }
190
+
191
+ /**
192
+ * Render a rounded polygon to a canvas context
193
+ */
194
+ export function renderRoundedPolygon(
195
+ ctx: CanvasRenderingContext2D,
196
+ roundedCorners: RoundedCornerPoint[],
197
+ closed = true,
198
+ ) {
199
+ if (roundedCorners.length === 0) return;
200
+
201
+ ctx.beginPath();
202
+
203
+ // Start at the first corner's start point
204
+ const firstCorner = roundedCorners[0];
205
+ ctx.moveTo(firstCorner.start.x, firstCorner.start.y);
206
+
207
+ for (let i = 0; i < roundedCorners.length; i++) {
208
+ const corner = roundedCorners[i];
209
+ const nextIndex = (i + 1) % roundedCorners.length;
210
+ const nextCorner = roundedCorners[nextIndex];
211
+
212
+ // Draw the rounded corner using bezier curve
213
+ ctx.bezierCurveTo(
214
+ corner.cp1.x,
215
+ corner.cp1.y,
216
+ corner.cp2.x,
217
+ corner.cp2.y,
218
+ corner.end.x,
219
+ corner.end.y,
220
+ );
221
+
222
+ // Draw line to next corner's start point (if not the last segment in open path)
223
+ if (i < roundedCorners.length - 1 || closed) {
224
+ ctx.lineTo(nextCorner.start.x, nextCorner.start.y);
225
+ }
226
+ }
227
+
228
+ if (closed) {
229
+ ctx.closePath();
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Generate SVG path data for a rounded polygon
235
+ */
236
+ export function generateRoundedPolygonPath(
237
+ roundedCorners: RoundedCornerPoint[],
238
+ closed = true,
239
+ ): string {
240
+ if (roundedCorners.length === 0) return '';
241
+
242
+ const pathData: string[] = [];
243
+ const firstCorner = roundedCorners[0];
244
+
245
+ // Move to first corner's start point
246
+ pathData.push(`M ${firstCorner.start.x} ${firstCorner.start.y}`);
247
+
248
+ for (let i = 0; i < roundedCorners.length; i++) {
249
+ const corner = roundedCorners[i];
250
+ const nextIndex = (i + 1) % roundedCorners.length;
251
+ const nextCorner = roundedCorners[nextIndex];
252
+
253
+ // Add bezier curve for the rounded corner
254
+ pathData.push(
255
+ `C ${corner.cp1.x} ${corner.cp1.y} ${corner.cp2.x} ${corner.cp2.y} ${corner.end.x} ${corner.end.y}`,
256
+ );
257
+
258
+ // Add line to next corner's start point (if not the last segment in open path)
259
+ if (i < roundedCorners.length - 1 || closed) {
260
+ pathData.push(`L ${nextCorner.start.x} ${nextCorner.start.y}`);
261
+ }
262
+ }
263
+
264
+ if (closed) {
265
+ pathData.push('Z');
266
+ }
267
+
268
+ return pathData.join(' ');
269
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes