@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.
- package/dist/index.js +504 -102
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/index.min.mjs +1 -1
- package/dist/index.min.mjs.map +1 -1
- package/dist/index.mjs +504 -102
- package/dist/index.mjs.map +1 -1
- package/dist/index.node.cjs +504 -102
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.mjs +504 -102
- package/dist/index.node.mjs.map +1 -1
- package/dist/package.json.min.mjs +1 -1
- package/dist/package.json.mjs +1 -1
- package/dist/src/shapes/Polyline.d.ts +7 -0
- package/dist/src/shapes/Polyline.d.ts.map +1 -1
- package/dist/src/shapes/Polyline.min.mjs +1 -1
- package/dist/src/shapes/Polyline.min.mjs.map +1 -1
- package/dist/src/shapes/Polyline.mjs +48 -16
- package/dist/src/shapes/Polyline.mjs.map +1 -1
- package/dist/src/shapes/Text/Text.d.ts.map +1 -1
- package/dist/src/shapes/Text/Text.min.mjs +1 -1
- package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
- package/dist/src/shapes/Text/Text.mjs +64 -12
- package/dist/src/shapes/Text/Text.mjs.map +1 -1
- package/dist/src/shapes/Textbox.d.ts.map +1 -1
- package/dist/src/shapes/Textbox.min.mjs +1 -1
- package/dist/src/shapes/Textbox.min.mjs.map +1 -1
- package/dist/src/shapes/Textbox.mjs +2 -52
- package/dist/src/shapes/Textbox.mjs.map +1 -1
- package/dist/src/shapes/Triangle.d.ts +27 -2
- package/dist/src/shapes/Triangle.d.ts.map +1 -1
- package/dist/src/shapes/Triangle.min.mjs +1 -1
- package/dist/src/shapes/Triangle.min.mjs.map +1 -1
- package/dist/src/shapes/Triangle.mjs +72 -12
- package/dist/src/shapes/Triangle.mjs.map +1 -1
- package/dist/src/text/overlayEditor.d.ts.map +1 -1
- package/dist/src/text/overlayEditor.min.mjs +1 -1
- package/dist/src/text/overlayEditor.min.mjs.map +1 -1
- package/dist/src/text/overlayEditor.mjs +143 -9
- package/dist/src/text/overlayEditor.mjs.map +1 -1
- package/dist/src/util/misc/cornerRadius.d.ts +70 -0
- package/dist/src/util/misc/cornerRadius.d.ts.map +1 -0
- package/dist/src/util/misc/cornerRadius.min.mjs +2 -0
- package/dist/src/util/misc/cornerRadius.min.mjs.map +1 -0
- package/dist/src/util/misc/cornerRadius.mjs +181 -0
- package/dist/src/util/misc/cornerRadius.mjs.map +1 -0
- package/dist-extensions/src/shapes/Polyline.d.ts +7 -0
- package/dist-extensions/src/shapes/Polyline.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Triangle.d.ts +27 -2
- package/dist-extensions/src/shapes/Triangle.d.ts.map +1 -1
- package/dist-extensions/src/text/overlayEditor.d.ts.map +1 -1
- package/dist-extensions/src/util/misc/cornerRadius.d.ts +70 -0
- package/dist-extensions/src/util/misc/cornerRadius.d.ts.map +1 -0
- package/fabric-test-editor.html +1048 -0
- package/package.json +164 -164
- package/src/shapes/Polyline.ts +70 -29
- package/src/shapes/Text/Text.ts +79 -14
- package/src/shapes/Textbox.ts +1 -1
- package/src/shapes/Triangle.spec.ts +76 -0
- package/src/shapes/Triangle.ts +85 -15
- package/src/text/overlayEditor.ts +152 -12
- package/src/util/misc/cornerRadius.spec.ts +141 -0
- 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
|
+
});
|
package/src/shapes/Triangle.ts
CHANGED
|
@@ -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<
|
|
14
|
-
SProps extends
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
points =
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
367
|
-
(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
this.
|
|
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
|
+
});
|