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

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 (66) hide show
  1. package/dist/index.js +504 -102
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.min.js +1 -1
  4. package/dist/index.min.js.map +1 -1
  5. package/dist/index.min.mjs +1 -1
  6. package/dist/index.min.mjs.map +1 -1
  7. package/dist/index.mjs +504 -102
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/index.node.cjs +504 -102
  10. package/dist/index.node.cjs.map +1 -1
  11. package/dist/index.node.mjs +504 -102
  12. package/dist/index.node.mjs.map +1 -1
  13. package/dist/package.json.min.mjs +1 -1
  14. package/dist/package.json.mjs +1 -1
  15. package/dist/src/shapes/Polyline.d.ts +7 -0
  16. package/dist/src/shapes/Polyline.d.ts.map +1 -1
  17. package/dist/src/shapes/Polyline.min.mjs +1 -1
  18. package/dist/src/shapes/Polyline.min.mjs.map +1 -1
  19. package/dist/src/shapes/Polyline.mjs +48 -16
  20. package/dist/src/shapes/Polyline.mjs.map +1 -1
  21. package/dist/src/shapes/Text/Text.d.ts.map +1 -1
  22. package/dist/src/shapes/Text/Text.min.mjs +1 -1
  23. package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
  24. package/dist/src/shapes/Text/Text.mjs +64 -12
  25. package/dist/src/shapes/Text/Text.mjs.map +1 -1
  26. package/dist/src/shapes/Textbox.d.ts.map +1 -1
  27. package/dist/src/shapes/Textbox.min.mjs +1 -1
  28. package/dist/src/shapes/Textbox.min.mjs.map +1 -1
  29. package/dist/src/shapes/Textbox.mjs +2 -52
  30. package/dist/src/shapes/Textbox.mjs.map +1 -1
  31. package/dist/src/shapes/Triangle.d.ts +27 -2
  32. package/dist/src/shapes/Triangle.d.ts.map +1 -1
  33. package/dist/src/shapes/Triangle.min.mjs +1 -1
  34. package/dist/src/shapes/Triangle.min.mjs.map +1 -1
  35. package/dist/src/shapes/Triangle.mjs +72 -12
  36. package/dist/src/shapes/Triangle.mjs.map +1 -1
  37. package/dist/src/text/overlayEditor.d.ts.map +1 -1
  38. package/dist/src/text/overlayEditor.min.mjs +1 -1
  39. package/dist/src/text/overlayEditor.min.mjs.map +1 -1
  40. package/dist/src/text/overlayEditor.mjs +143 -9
  41. package/dist/src/text/overlayEditor.mjs.map +1 -1
  42. package/dist/src/util/misc/cornerRadius.d.ts +70 -0
  43. package/dist/src/util/misc/cornerRadius.d.ts.map +1 -0
  44. package/dist/src/util/misc/cornerRadius.min.mjs +2 -0
  45. package/dist/src/util/misc/cornerRadius.min.mjs.map +1 -0
  46. package/dist/src/util/misc/cornerRadius.mjs +181 -0
  47. package/dist/src/util/misc/cornerRadius.mjs.map +1 -0
  48. package/dist-extensions/src/shapes/Polyline.d.ts +7 -0
  49. package/dist-extensions/src/shapes/Polyline.d.ts.map +1 -1
  50. package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
  51. package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
  52. package/dist-extensions/src/shapes/Triangle.d.ts +27 -2
  53. package/dist-extensions/src/shapes/Triangle.d.ts.map +1 -1
  54. package/dist-extensions/src/text/overlayEditor.d.ts.map +1 -1
  55. package/dist-extensions/src/util/misc/cornerRadius.d.ts +70 -0
  56. package/dist-extensions/src/util/misc/cornerRadius.d.ts.map +1 -0
  57. package/fabric-test-editor.html +1048 -0
  58. package/package.json +164 -164
  59. package/src/shapes/Polyline.ts +70 -29
  60. package/src/shapes/Text/Text.ts +79 -14
  61. package/src/shapes/Textbox.ts +1 -1
  62. package/src/shapes/Triangle.spec.ts +76 -0
  63. package/src/shapes/Triangle.ts +85 -15
  64. package/src/text/overlayEditor.ts +152 -12
  65. package/src/util/misc/cornerRadius.spec.ts +141 -0
  66. package/src/util/misc/cornerRadius.ts +269 -0
@@ -0,0 +1,76 @@
1
+ import { Triangle } from './Triangle';
2
+
3
+ describe('Triangle with Corner Radius', () => {
4
+ describe('constructor', () => {
5
+ it('should create triangle with default cornerRadius of 0', () => {
6
+ const triangle = new Triangle();
7
+ expect(triangle.cornerRadius).toBe(0);
8
+ });
9
+
10
+ it('should create triangle with specified cornerRadius', () => {
11
+ const triangle = new Triangle({ cornerRadius: 10 });
12
+ expect(triangle.cornerRadius).toBe(10);
13
+ });
14
+ });
15
+
16
+ describe('rendering', () => {
17
+ let canvas: HTMLCanvasElement;
18
+ let ctx: CanvasRenderingContext2D;
19
+
20
+ beforeEach(() => {
21
+ canvas = document.createElement('canvas');
22
+ canvas.width = 200;
23
+ canvas.height = 200;
24
+ ctx = canvas.getContext('2d')!;
25
+ });
26
+
27
+ it('should render sharp triangle when cornerRadius is 0', () => {
28
+ const triangle = new Triangle({ width: 100, height: 100, cornerRadius: 0 });
29
+ const spy = jest.spyOn(ctx, 'moveTo');
30
+ const lineSpy = jest.spyOn(ctx, 'lineTo');
31
+ const bezierSpy = jest.spyOn(ctx, 'bezierCurveTo');
32
+
33
+ triangle._render(ctx);
34
+
35
+ expect(spy).toHaveBeenCalled();
36
+ expect(lineSpy).toHaveBeenCalled();
37
+ expect(bezierSpy).not.toHaveBeenCalled(); // No curves for sharp corners
38
+ });
39
+
40
+ it('should render rounded triangle when cornerRadius > 0', () => {
41
+ const triangle = new Triangle({ width: 100, height: 100, cornerRadius: 10 });
42
+ const bezierSpy = jest.spyOn(ctx, 'bezierCurveTo');
43
+
44
+ triangle._render(ctx);
45
+
46
+ expect(bezierSpy).toHaveBeenCalled(); // Should use curves for rounded corners
47
+ });
48
+ });
49
+
50
+ describe('toObject', () => {
51
+ it('should include cornerRadius in serialized object', () => {
52
+ const triangle = new Triangle({ cornerRadius: 15 });
53
+ const obj = triangle.toObject();
54
+
55
+ expect(obj.cornerRadius).toBe(15);
56
+ });
57
+ });
58
+
59
+ describe('_toSVG', () => {
60
+ it('should generate polygon SVG for sharp triangle', () => {
61
+ const triangle = new Triangle({ width: 100, height: 100, cornerRadius: 0 });
62
+ const svg = triangle._toSVG();
63
+
64
+ expect(svg.join('')).toContain('<polygon');
65
+ expect(svg.join('')).toContain('points=');
66
+ });
67
+
68
+ it('should generate path SVG for rounded triangle', () => {
69
+ const triangle = new Triangle({ width: 100, height: 100, cornerRadius: 10 });
70
+ const svg = triangle._toSVG();
71
+
72
+ expect(svg.join('')).toContain('<path');
73
+ expect(svg.join('')).toContain('d=');
74
+ });
75
+ });
76
+ });
@@ -1,24 +1,50 @@
1
1
  import { classRegistry } from '../ClassRegistry';
2
- import { FabricObject } from './Object/FabricObject';
2
+ import { FabricObject, cacheProperties } from './Object/FabricObject';
3
3
  import type { FabricObjectProps, SerializedObjectProps } from './Object/types';
4
4
  import type { TClassProperties, TOptions } from '../typedefs';
5
5
  import type { ObjectEvents } from '../EventTypeDefs';
6
+ import {
7
+ applyCornerRadiusToPolygon,
8
+ renderRoundedPolygon,
9
+ generateRoundedPolygonPath,
10
+ } from '../util/misc/cornerRadius';
6
11
 
7
12
  export const triangleDefaultValues: Partial<TClassProperties<Triangle>> = {
8
13
  width: 100,
9
14
  height: 100,
15
+ cornerRadius: 0,
10
16
  };
11
17
 
18
+ interface UniqueTriangleProps {
19
+ cornerRadius: number;
20
+ }
21
+
22
+ export interface SerializedTriangleProps
23
+ extends SerializedObjectProps,
24
+ UniqueTriangleProps {}
25
+
26
+ export interface TriangleProps extends FabricObjectProps, UniqueTriangleProps {}
27
+
28
+ const TRIANGLE_PROPS = ['cornerRadius'] as const;
29
+
12
30
  export class Triangle<
13
- Props extends TOptions<FabricObjectProps> = Partial<FabricObjectProps>,
14
- SProps extends SerializedObjectProps = SerializedObjectProps,
31
+ Props extends TOptions<TriangleProps> = Partial<TriangleProps>,
32
+ SProps extends SerializedTriangleProps = SerializedTriangleProps,
15
33
  EventSpec extends ObjectEvents = ObjectEvents,
16
34
  >
17
35
  extends FabricObject<Props, SProps, EventSpec>
18
- implements FabricObjectProps
36
+ implements TriangleProps
19
37
  {
38
+ /**
39
+ * Corner radius for rounded triangle corners
40
+ * @type Number
41
+ */
42
+ declare cornerRadius: number;
43
+
20
44
  static type = 'Triangle';
21
45
 
46
+ static cacheProperties = [...cacheProperties, ...TRIANGLE_PROPS];
47
+
22
48
  static ownDefaults = triangleDefaultValues;
23
49
 
24
50
  static getDefaults(): Record<string, any> {
@@ -35,33 +61,77 @@ export class Triangle<
35
61
  this.setOptions(options);
36
62
  }
37
63
 
64
+ /**
65
+ * Get triangle points as an array of XY coordinates
66
+ * @private
67
+ */
68
+ private _getTrianglePoints() {
69
+ const widthBy2 = this.width / 2;
70
+ const heightBy2 = this.height / 2;
71
+
72
+ return [
73
+ { x: -widthBy2, y: heightBy2 }, // bottom left
74
+ { x: 0, y: -heightBy2 }, // top center
75
+ { x: widthBy2, y: heightBy2 }, // bottom right
76
+ ];
77
+ }
78
+
38
79
  /**
39
80
  * @private
40
81
  * @param {CanvasRenderingContext2D} ctx Context to render on
41
82
  */
42
83
  _render(ctx: CanvasRenderingContext2D) {
43
- const widthBy2 = this.width / 2,
44
- heightBy2 = this.height / 2;
84
+ if (this.cornerRadius > 0) {
85
+ // Render rounded triangle
86
+ const points = this._getTrianglePoints();
87
+ const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
88
+ renderRoundedPolygon(ctx, roundedCorners, true);
89
+ } else {
90
+ // Render sharp triangle (original implementation)
91
+ const widthBy2 = this.width / 2;
92
+ const heightBy2 = this.height / 2;
45
93
 
46
- ctx.beginPath();
47
- ctx.moveTo(-widthBy2, heightBy2);
48
- ctx.lineTo(0, -heightBy2);
49
- ctx.lineTo(widthBy2, heightBy2);
50
- ctx.closePath();
94
+ ctx.beginPath();
95
+ ctx.moveTo(-widthBy2, heightBy2);
96
+ ctx.lineTo(0, -heightBy2);
97
+ ctx.lineTo(widthBy2, heightBy2);
98
+ ctx.closePath();
99
+ }
51
100
 
52
101
  this._renderPaintInOrder(ctx);
53
102
  }
54
103
 
104
+ /**
105
+ * Returns object representation of an instance
106
+ * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
107
+ * @return {Object} object representation of an instance
108
+ */
109
+ toObject<
110
+ T extends Omit<Props & TClassProperties<this>, keyof SProps>,
111
+ K extends keyof T = never,
112
+ >(propertiesToInclude: K[] = []): Pick<T, K> & SProps {
113
+ return super.toObject([...TRIANGLE_PROPS, ...propertiesToInclude]);
114
+ }
115
+
55
116
  /**
56
117
  * Returns svg representation of an instance
57
118
  * @return {Array} an array of strings with the specific svg representation
58
119
  * of the instance
59
120
  */
60
121
  _toSVG() {
61
- const widthBy2 = this.width / 2,
62
- heightBy2 = this.height / 2,
63
- points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
64
- return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
122
+ if (this.cornerRadius > 0) {
123
+ // Generate rounded triangle as path
124
+ const points = this._getTrianglePoints();
125
+ const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
126
+ const pathData = generateRoundedPolygonPath(roundedCorners, true);
127
+ return ['<path ', 'COMMON_PARTS', `d="${pathData}" />`];
128
+ } else {
129
+ // Original sharp triangle implementation
130
+ const widthBy2 = this.width / 2;
131
+ const heightBy2 = this.height / 2;
132
+ const points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
133
+ return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
134
+ }
65
135
  }
66
136
  }
67
137
 
@@ -343,15 +343,88 @@ export class OverlayEditor {
343
343
  this.textarea.style.fontFamily = target.fontFamily || 'Arial';
344
344
  this.textarea.style.fontWeight = String(target.fontWeight || 'normal');
345
345
  this.textarea.style.fontStyle = target.fontStyle || 'normal';
346
- this.textarea.style.textAlign = (target as any).textAlign || 'left';
346
+ // Handle text alignment and justification
347
+ const textAlign = (target as any).textAlign || 'left';
348
+ let cssTextAlign = textAlign;
349
+
350
+ // Detect text direction from content for proper justify handling
351
+ const autoDetectedDirection = this.firstStrongDir(this.textarea.value || '');
352
+
353
+ // DEBUG: Log alignment details
354
+ console.log('🔍 ALIGNMENT DEBUG:');
355
+ console.log(' Fabric textAlign:', textAlign);
356
+ console.log(' Fabric direction:', (target as any).direction);
357
+ console.log(' Text content:', JSON.stringify(target.text));
358
+ console.log(' Detected direction:', autoDetectedDirection);
359
+
360
+ // Map fabric.js justify to CSS
361
+ if (textAlign.includes('justify')) {
362
+ // Try to match fabric.js justify behavior more precisely
363
+ try {
364
+ // For justify, we need to replicate fabric.js space expansion
365
+ // Use CSS justify but with specific settings to match fabric.js better
366
+ cssTextAlign = 'justify';
367
+
368
+ // Set text-align-last based on justify type and detected direction
369
+ // Smart justify: respect detected direction even when fabric alignment doesn't match
370
+ if (textAlign === 'justify') {
371
+ this.textarea.style.textAlignLast = autoDetectedDirection === 'rtl' ? 'right' : 'left';
372
+ } else if (textAlign === 'justify-left') {
373
+ // If text is RTL but fabric says justify-left, override to justify-right for better UX
374
+ if (autoDetectedDirection === 'rtl') {
375
+ this.textarea.style.textAlignLast = 'right';
376
+ console.log(' → Overrode justify-left to justify-right for RTL text');
377
+ } else {
378
+ this.textarea.style.textAlignLast = 'left';
379
+ }
380
+ } else if (textAlign === 'justify-right') {
381
+ // If text is LTR but fabric says justify-right, override to justify-left for better UX
382
+ if (autoDetectedDirection === 'ltr') {
383
+ this.textarea.style.textAlignLast = 'left';
384
+ console.log(' → Overrode justify-right to justify-left for LTR text');
385
+ } else {
386
+ this.textarea.style.textAlignLast = 'right';
387
+ }
388
+ } else if (textAlign === 'justify-center') {
389
+ this.textarea.style.textAlignLast = 'center';
390
+ }
391
+
392
+ // Enhanced justify settings for better fabric.js matching
393
+ (this.textarea.style as any).textJustify = 'inter-word';
394
+ (this.textarea.style as any).wordSpacing = 'normal';
395
+
396
+ // Additional CSS properties for better justify matching
397
+ this.textarea.style.textAlign = 'justify';
398
+ this.textarea.style.textAlignLast = this.textarea.style.textAlignLast;
399
+
400
+ // Try to force better justify behavior
401
+ (this.textarea.style as any).textJustifyTrim = 'none';
402
+ (this.textarea.style as any).textAutospace = 'none';
403
+
404
+ console.log(' → Applied justify alignment:', textAlign, 'with last-line:', this.textarea.style.textAlignLast);
405
+ } catch (error) {
406
+ console.warn(' → Justify setup failed, falling back to standard alignment:', error);
407
+ cssTextAlign = textAlign.replace('justify-', '').replace('justify', 'left');
408
+ }
409
+ } else {
410
+ this.textarea.style.textAlignLast = 'auto';
411
+ (this.textarea.style as any).textJustify = 'auto';
412
+ (this.textarea.style as any).wordSpacing = 'normal';
413
+ console.log(' → Applied standard alignment:', cssTextAlign);
414
+ }
415
+
416
+ this.textarea.style.textAlign = cssTextAlign;
347
417
  this.textarea.style.color = target.fill?.toString() || '#000';
348
418
  this.textarea.style.letterSpacing = `${letterSpacingPx}px`;
349
- this.textarea.style.direction =
350
- (target as any).direction ||
351
- this.firstStrongDir(this.textarea.value || '');
419
+
420
+ // Use the already detected direction from above
421
+ const fabricDirection = (target as any).direction;
422
+
423
+ // Use auto-detected direction for better BiDi support, but respect fabric direction if it makes sense
424
+ this.textarea.style.direction = autoDetectedDirection || fabricDirection || 'ltr';
352
425
  this.textarea.style.fontVariant = 'normal';
353
426
  this.textarea.style.fontStretch = 'normal';
354
- this.textarea.style.textRendering = 'optimizeLegibility';
427
+ this.textarea.style.textRendering = 'auto'; // Changed from 'optimizeLegibility' to match canvas
355
428
  this.textarea.style.fontKerning = 'normal';
356
429
  this.textarea.style.fontFeatureSettings = 'normal';
357
430
  this.textarea.style.fontVariationSettings = 'normal';
@@ -363,14 +436,61 @@ export class OverlayEditor {
363
436
  this.textarea.style.whiteSpace = 'pre-wrap';
364
437
  this.textarea.style.hyphens = 'none';
365
438
 
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();
439
+ // DEBUG: Log final CSS properties
440
+ console.log('🎨 FINAL TEXTAREA CSS:');
441
+ console.log(' textAlign:', this.textarea.style.textAlign);
442
+ console.log(' textAlignLast:', this.textarea.style.textAlignLast);
443
+ console.log(' direction:', this.textarea.style.direction);
444
+ console.log(' unicodeBidi:', this.textarea.style.unicodeBidi);
445
+ console.log(' width:', this.textarea.style.width);
446
+ console.log(' textJustify:', (this.textarea.style as any).textJustify);
447
+ console.log(' wordSpacing:', (this.textarea.style as any).wordSpacing);
448
+ console.log(' whiteSpace:', this.textarea.style.whiteSpace);
449
+
450
+ // If justify, log Fabric object dimensions for comparison
451
+ if (textAlign.includes('justify')) {
452
+ console.log('🔧 FABRIC OBJECT JUSTIFY INFO:');
453
+ console.log(' Fabric width:', (target as any).width);
454
+ console.log(' Fabric calcTextWidth:', (target as any).calcTextWidth?.());
455
+ console.log(' Fabric textAlign:', (target as any).textAlign);
456
+ console.log(' Text lines:', (target as any).textLines);
457
+ }
458
+
459
+ // Debug font properties matching
460
+ console.log('🔤 FONT PROPERTIES COMPARISON:');
461
+ console.log(' Fabric fontFamily:', target.fontFamily);
462
+ console.log(' Fabric fontWeight:', target.fontWeight);
463
+ console.log(' Fabric fontStyle:', target.fontStyle);
464
+ console.log(' Fabric fontSize:', target.fontSize);
465
+ console.log(' → Textarea fontFamily:', this.textarea.style.fontFamily);
466
+ console.log(' → Textarea fontWeight:', this.textarea.style.fontWeight);
467
+ console.log(' → Textarea fontStyle:', this.textarea.style.fontStyle);
468
+ console.log(' → Textarea fontSize:', this.textarea.style.fontSize);
469
+
470
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
471
+
472
+ // Enhanced font rendering to better match fabric.js canvas rendering
473
+ // Default to auto for more natural rendering
474
+ (this.textarea.style as any).webkitFontSmoothing = 'auto';
475
+ (this.textarea.style as any).mozOsxFontSmoothing = 'auto';
476
+ (this.textarea.style as any).fontSmooth = 'auto';
477
+ (this.textarea.style as any).textSizeAdjust = 'none';
478
+
479
+ // For bold fonts, use subpixel rendering to match canvas thickness better
480
+ const fontWeight = String(target.fontWeight || 'normal');
481
+ const isBold = fontWeight === 'bold' || fontWeight === '700' ||
482
+ (parseInt(fontWeight) >= 600);
483
+
484
+ if (isBold) {
485
+ (this.textarea.style as any).webkitFontSmoothing = 'subpixel-antialiased';
486
+ (this.textarea.style as any).mozOsxFontSmoothing = 'unset';
487
+ console.log('🔤 Applied enhanced bold rendering for better thickness matching');
488
+ }
489
+
490
+ console.log('🎨 FONT SMOOTHING APPLIED:');
491
+ console.log(' webkitFontSmoothing:', (this.textarea.style as any).webkitFontSmoothing);
492
+ console.log(' mozOsxFontSmoothing:', (this.textarea.style as any).mozOsxFontSmoothing);
371
493
 
372
- // Debug: Compare text wrapping behavior
373
- this.debugTextWrapping();
374
494
 
375
495
  // Initial bounds are set correctly by Fabric.js - don't force update here
376
496
  }
@@ -664,6 +784,26 @@ export class OverlayEditor {
664
784
  // Handle commit/cancel after restoring visibility
665
785
  if (commit && !this.isComposing) {
666
786
  const finalText = this.textarea.value;
787
+
788
+ // Auto-detect text direction and update fabric object if needed
789
+ const detectedDirection = this.firstStrongDir(finalText);
790
+ const currentDirection = (this.target as any).direction || 'ltr';
791
+
792
+ if (detectedDirection && detectedDirection !== currentDirection) {
793
+ console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
794
+ console.log(` Text content: "${finalText.substring(0, 50)}..."`);
795
+
796
+ // Update the fabric object's direction
797
+ (this.target as any).set('direction', detectedDirection);
798
+
799
+ // Force a re-render to apply the direction change
800
+ this.canvas.requestRenderAll();
801
+
802
+ console.log(`✅ Fabric object direction updated to: ${detectedDirection}`);
803
+ } else {
804
+ console.log(`📝 Overlay Exit: Direction unchanged (${currentDirection}), text: "${finalText.substring(0, 30)}..."`);
805
+ }
806
+
667
807
  if (this.onCommit) {
668
808
  this.onCommit(finalText);
669
809
  }
@@ -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
+ });