@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
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Advanced Text Measurement System
3
- *
3
+ *
4
4
  * Provides precise text measurement with caching, font metrics,
5
5
  * and DPI awareness for optimal performance and accuracy.
6
6
  */
@@ -62,7 +62,7 @@ function getMeasurementContext(): CanvasRenderingContext2D {
62
62
  export function measureGrapheme(
63
63
  grapheme: string,
64
64
  options: MeasurementOptions,
65
- ctx?: CanvasRenderingContext2D
65
+ ctx?: CanvasRenderingContext2D,
66
66
  ): GraphemeMeasurement {
67
67
  // Check cache first
68
68
  const cached = measurementCache.get(grapheme, options);
@@ -72,14 +72,14 @@ export function measureGrapheme(
72
72
 
73
73
  // Use provided context or get global one
74
74
  const context = ctx || getMeasurementContext();
75
-
75
+
76
76
  // Set font properties
77
77
  applyFontStyle(context, options);
78
-
78
+
79
79
  // Measure the grapheme
80
80
  const metrics = context.measureText(grapheme);
81
81
  const fontMetrics = getFontMetrics(options);
82
-
82
+
83
83
  // Calculate comprehensive measurements
84
84
  const measurement: GraphemeMeasurement = {
85
85
  width: metrics.width,
@@ -88,10 +88,10 @@ export function measureGrapheme(
88
88
  descent: fontMetrics.descent,
89
89
  baseline: fontMetrics.ascent,
90
90
  };
91
-
91
+
92
92
  // Cache the result
93
93
  measurementCache.set(grapheme, options, measurement);
94
-
94
+
95
95
  return measurement;
96
96
  }
97
97
 
@@ -102,11 +102,11 @@ export function measureGraphemeWithKerning(
102
102
  grapheme: string,
103
103
  previousGrapheme: string | undefined,
104
104
  options: MeasurementOptions,
105
- ctx?: CanvasRenderingContext2D
105
+ ctx?: CanvasRenderingContext2D,
106
106
  ): KerningMeasurement {
107
107
  // Get individual measurement
108
108
  const individual = measureGrapheme(grapheme, options, ctx);
109
-
109
+
110
110
  // If no previous character, kerning width equals regular width
111
111
  if (!previousGrapheme) {
112
112
  return {
@@ -114,7 +114,7 @@ export function measureGraphemeWithKerning(
114
114
  kernedWidth: individual.width,
115
115
  };
116
116
  }
117
-
117
+
118
118
  // Check kerning cache
119
119
  const kerningPair = `${previousGrapheme}${grapheme}`;
120
120
  const cachedKerning = kerningCache.get(kerningPair, options);
@@ -124,25 +124,83 @@ export function measureGraphemeWithKerning(
124
124
  kernedWidth: cachedKerning,
125
125
  };
126
126
  }
127
-
127
+
128
128
  // Use provided context or get global one
129
129
  const context = ctx || getMeasurementContext();
130
130
  applyFontStyle(context, options);
131
-
131
+
132
132
  // Measure the pair
133
133
  const pairWidth = context.measureText(previousGrapheme + grapheme).width;
134
- const previousWidth = measureGrapheme(previousGrapheme, options, context).width;
134
+ const previousWidth = measureGrapheme(
135
+ previousGrapheme,
136
+ options,
137
+ context,
138
+ ).width;
135
139
  const kernedWidth = pairWidth - previousWidth;
136
-
140
+
137
141
  // Cache kerning result
138
142
  kerningCache.set(kerningPair, options, kernedWidth);
139
-
143
+
140
144
  return {
141
145
  ...individual,
142
146
  kernedWidth,
143
147
  };
144
148
  }
145
149
 
150
+ /**
151
+ * Get a representative character for font metrics measurement
152
+ * Uses canvas to test which scripts the font actually supports
153
+ */
154
+ function getRepresentativeCharacter(fontFamily: string): string {
155
+ const context = getMeasurementContext();
156
+
157
+ // Wait for font to be ready if possible
158
+ if (typeof document !== 'undefined' && 'fonts' in document) {
159
+ try {
160
+ // Check if font is ready, if not, use fallback immediately
161
+ if (!document.fonts.check(`16px ${fontFamily}`)) {
162
+ return 'M'; // Use safe fallback while font loads
163
+ }
164
+ } catch (e) {
165
+ // Font check failed, use fallback
166
+ return 'M';
167
+ }
168
+ }
169
+
170
+ // Test characters for different scripts
171
+ const testChars = [
172
+ { char: 'م', script: 'Arabic' }, // Arabic
173
+ { char: 'א', script: 'Hebrew' }, // Hebrew
174
+ { char: 'अ', script: 'Devanagari' }, // Hindi/Sanskrit
175
+ { char: 'ا', script: 'Urdu' }, // Urdu
176
+ { char: 'ک', script: 'Persian' }, // Persian
177
+ { char: 'த', script: 'Tamil' }, // Tamil
178
+ { char: 'ก', script: 'Thai' }, // Thai
179
+ { char: 'М', script: 'Cyrillic' }, // Cyrillic
180
+ { char: 'Ω', script: 'Greek' }, // Greek
181
+ { char: 'M', script: 'Latin' } // Latin (fallback)
182
+ ];
183
+
184
+ // Set the font
185
+ context.font = `16px ${fontFamily}`;
186
+
187
+ // Test each character to see which ones render properly
188
+ // Use a more robust width check to avoid false positives
189
+ const fallbackWidth = context.measureText('M').width;
190
+
191
+ for (const test of testChars) {
192
+ const metrics = context.measureText(test.char);
193
+
194
+ // Character is valid if it has width and isn't just a fallback glyph
195
+ if (metrics.width > 0 && Math.abs(metrics.width - fallbackWidth) > 0.1) {
196
+ return test.char;
197
+ }
198
+ }
199
+
200
+ // Fallback to Latin 'M'
201
+ return 'M';
202
+ }
203
+
146
204
  /**
147
205
  * Get font metrics for layout calculations
148
206
  */
@@ -152,20 +210,24 @@ export function getFontMetrics(options: MeasurementOptions): FontMetrics {
152
210
  if (cached) {
153
211
  return cached;
154
212
  }
155
-
213
+
156
214
  const context = getMeasurementContext();
157
215
  applyFontStyle(context, options);
158
-
159
- // Use 'M' as sample character for metrics
160
- const metrics = context.measureText('M');
216
+
217
+ // Use representative character based on font's primary script
218
+ const sample = getRepresentativeCharacter(options.fontFamily);
219
+ const metrics = context.measureText(sample);
161
220
  const fontSize = options.fontSize;
162
-
221
+
163
222
  // Calculate metrics with fallbacks
164
- const fontBoundingBoxAscent = metrics.fontBoundingBoxAscent ?? fontSize * 0.91;
165
- const fontBoundingBoxDescent = metrics.fontBoundingBoxDescent ?? fontSize * 0.21;
166
- const actualBoundingBoxAscent = metrics.actualBoundingBoxAscent ?? fontSize * 0.716;
223
+ const fontBoundingBoxAscent =
224
+ metrics.fontBoundingBoxAscent ?? fontSize * 0.91;
225
+ const fontBoundingBoxDescent =
226
+ metrics.fontBoundingBoxDescent ?? fontSize * 0.21;
227
+ const actualBoundingBoxAscent =
228
+ metrics.actualBoundingBoxAscent ?? fontSize * 0.716;
167
229
  const actualBoundingBoxDescent = metrics.actualBoundingBoxDescent ?? 0;
168
-
230
+
169
231
  const result: FontMetrics = {
170
232
  ascent: fontBoundingBoxAscent,
171
233
  descent: fontBoundingBoxDescent,
@@ -176,7 +238,7 @@ export function getFontMetrics(options: MeasurementOptions): FontMetrics {
176
238
  actualBoundingBoxAscent,
177
239
  actualBoundingBoxDescent,
178
240
  };
179
-
241
+
180
242
  fontMetricsCache.set(cacheKey, result);
181
243
  return result;
182
244
  }
@@ -184,21 +246,24 @@ export function getFontMetrics(options: MeasurementOptions): FontMetrics {
184
246
  /**
185
247
  * Apply font styling to canvas context
186
248
  */
187
- function applyFontStyle(ctx: CanvasRenderingContext2D, options: MeasurementOptions): void {
249
+ function applyFontStyle(
250
+ ctx: CanvasRenderingContext2D,
251
+ options: MeasurementOptions,
252
+ ): void {
188
253
  const fontDeclaration = getFontDeclaration(options);
189
254
  ctx.font = fontDeclaration;
190
-
255
+
191
256
  if (options.letterSpacing) {
192
257
  // Modern browsers support letterSpacing
193
258
  if ('letterSpacing' in ctx) {
194
259
  (ctx as any).letterSpacing = `${options.letterSpacing}px`;
195
260
  }
196
261
  }
197
-
262
+
198
263
  if (options.direction) {
199
264
  ctx.direction = options.direction;
200
265
  }
201
-
266
+
202
267
  ctx.textBaseline = 'alphabetic';
203
268
  }
204
269
 
@@ -207,14 +272,18 @@ function applyFontStyle(ctx: CanvasRenderingContext2D, options: MeasurementOptio
207
272
  */
208
273
  function getFontDeclaration(options: MeasurementOptions): string {
209
274
  const { fontStyle, fontWeight, fontSize, fontFamily } = options;
210
-
275
+
211
276
  // Normalize font family (add quotes if needed)
212
- const normalizedFamily = fontFamily.includes(' ') &&
213
- !fontFamily.includes('"') &&
277
+ let normalizedFamily =
278
+ fontFamily.includes(' ') &&
279
+ !fontFamily.includes('"') &&
214
280
  !fontFamily.includes("'")
215
- ? `"${fontFamily}"`
216
- : fontFamily;
217
-
281
+ ? `"${fontFamily}"`
282
+ : fontFamily;
283
+
284
+ // Note: Font fallbacks are handled in the rendering phase only
285
+ // to avoid affecting measurement calculations for text wrapping
286
+
218
287
  return `${fontStyle} ${fontWeight} ${fontSize}px ${normalizedFamily}`;
219
288
  }
220
289
 
@@ -301,12 +370,19 @@ export class MeasurementCache {
301
370
  return `${fontDecl}|${grapheme}|${letterSpacing}`;
302
371
  }
303
372
 
304
- get(grapheme: string, options: MeasurementOptions): GraphemeMeasurement | undefined {
373
+ get(
374
+ grapheme: string,
375
+ options: MeasurementOptions,
376
+ ): GraphemeMeasurement | undefined {
305
377
  const key = this.getCacheKey(grapheme, options);
306
378
  return this.cache.get(key);
307
379
  }
308
380
 
309
- set(grapheme: string, options: MeasurementOptions, measurement: GraphemeMeasurement): void {
381
+ set(
382
+ grapheme: string,
383
+ options: MeasurementOptions,
384
+ measurement: GraphemeMeasurement,
385
+ ): void {
310
386
  const key = this.getCacheKey(grapheme, options);
311
387
  this.cache.set(key, measurement);
312
388
  }
@@ -380,6 +456,14 @@ export const measurementCache = new MeasurementCache();
380
456
  export const kerningCache = new KerningCache();
381
457
  export const fontMetricsCache = new FontMetricsCache();
382
458
 
459
+ // Set up font loading listener to clear caches when fonts change
460
+ if (typeof document !== 'undefined' && 'fonts' in document) {
461
+ document.fonts.addEventListener('loadingdone', () => {
462
+ // Clear all caches when fonts finish loading
463
+ clearAllCaches();
464
+ });
465
+ }
466
+
383
467
  /**
384
468
  * Clear all measurement caches
385
469
  */
@@ -406,15 +490,15 @@ export function getCacheStats() {
406
490
  export function batchMeasureGraphemes(
407
491
  graphemes: string[],
408
492
  options: MeasurementOptions,
409
- ctx?: CanvasRenderingContext2D
493
+ ctx?: CanvasRenderingContext2D,
410
494
  ): GraphemeMeasurement[] {
411
495
  const context = ctx || getMeasurementContext();
412
496
  applyFontStyle(context, options);
413
-
497
+
414
498
  // Separate cached and uncached measurements
415
499
  const results: GraphemeMeasurement[] = new Array(graphemes.length);
416
500
  const uncachedIndices: number[] = [];
417
-
501
+
418
502
  // Check cache for all graphemes
419
503
  graphemes.forEach((grapheme, index) => {
420
504
  const cached = measurementCache.get(grapheme, options);
@@ -424,13 +508,13 @@ export function batchMeasureGraphemes(
424
508
  uncachedIndices.push(index);
425
509
  }
426
510
  });
427
-
511
+
428
512
  // Measure uncached graphemes
429
513
  const fontMetrics = getFontMetrics(options);
430
- uncachedIndices.forEach(index => {
514
+ uncachedIndices.forEach((index) => {
431
515
  const grapheme = graphemes[index];
432
516
  const metrics = context.measureText(grapheme);
433
-
517
+
434
518
  const measurement: GraphemeMeasurement = {
435
519
  width: metrics.width,
436
520
  height: fontMetrics.lineHeight,
@@ -438,11 +522,11 @@ export function batchMeasureGraphemes(
438
522
  descent: fontMetrics.descent,
439
523
  baseline: fontMetrics.ascent,
440
524
  };
441
-
525
+
442
526
  measurementCache.set(grapheme, options, measurement);
443
527
  results[index] = measurement;
444
528
  });
445
-
529
+
446
530
  return results;
447
531
  }
448
532
 
@@ -451,13 +535,13 @@ export function batchMeasureGraphemes(
451
535
  */
452
536
  export function estimateTextWidth(
453
537
  text: string,
454
- options: MeasurementOptions
538
+ options: MeasurementOptions,
455
539
  ): number {
456
540
  // Use average character width for estimation
457
541
  const avgChar = 'n'; // Representative character
458
542
  const avgMeasurement = measureGrapheme(avgChar, options);
459
543
  const letterSpacing = options.letterSpacing || 0;
460
-
544
+
461
545
  return text.length * (avgMeasurement.width + letterSpacing);
462
546
  }
463
547
 
@@ -466,11 +550,77 @@ export function estimateTextWidth(
466
550
  */
467
551
  export function isFontReady(fontFamily: string): boolean {
468
552
  if (typeof document === 'undefined') return true;
469
-
553
+
470
554
  if ('fonts' in document) {
471
555
  return document.fonts.check(`16px ${fontFamily}`);
472
556
  }
473
-
557
+
474
558
  // Fallback - assume font is ready
475
559
  return true;
476
- }
560
+ }
561
+
562
+ /**
563
+ * Detect if a font lacks English glyph support
564
+ * These fonts should use browser-native measurement instead of Fabric's character-by-character measurement
565
+ */
566
+ export function fontLacksEnglishGlyphs(fontFamily: string): boolean {
567
+ if (typeof document === 'undefined') return false;
568
+
569
+ // Known fonts that lack English glyphs
570
+ const knownNonEnglishFonts = [
571
+ 'stv', 'arabic', 'naskh', 'thuluth', 'kufi', 'diwani',
572
+ 'nastaliq', 'kufic', 'hijazi', 'madinah', 'makkah'
573
+ ];
574
+
575
+ const lowerFontFamily = fontFamily.toLowerCase();
576
+
577
+ // Check known list first
578
+ if (knownNonEnglishFonts.some(font => lowerFontFamily.includes(font))) {
579
+ return true;
580
+ }
581
+
582
+ // Dynamic glyph support detection
583
+ const context = getMeasurementContext();
584
+ context.font = `16px ${fontFamily}`;
585
+
586
+ // Test English characters
587
+ const englishChars = ['A', 'B', 'C', 'a', 'b', 'c', 'M', 'W'];
588
+ const fallbackFont = 'Arial, sans-serif';
589
+
590
+ // Measure with target font
591
+ const targetWidths = englishChars.map(char => context.measureText(char).width);
592
+
593
+ // Measure with fallback font
594
+ context.font = `16px ${fallbackFont}`;
595
+ const fallbackWidths = englishChars.map(char => context.measureText(char).width);
596
+
597
+ // If most measurements are identical, the font likely doesn't have English glyphs
598
+ let identicalCount = 0;
599
+ for (let i = 0; i < englishChars.length; i++) {
600
+ if (Math.abs(targetWidths[i] - fallbackWidths[i]) < 0.5) {
601
+ identicalCount++;
602
+ }
603
+ }
604
+
605
+ const lacksSupportThreshold = englishChars.length * 0.7; // 70% identical = lacks support
606
+ const lacksSupport = identicalCount >= lacksSupportThreshold;
607
+
608
+
609
+ return lacksSupport;
610
+ }
611
+
612
+ // Cache for font glyph detection results
613
+ const fontGlyphCache = new Map<string, boolean>();
614
+
615
+ /**
616
+ * Cached version of font glyph detection
617
+ */
618
+ export function fontLacksEnglishGlyphsCached(fontFamily: string): boolean {
619
+ if (fontGlyphCache.has(fontFamily)) {
620
+ return fontGlyphCache.get(fontFamily)!;
621
+ }
622
+
623
+ const result = fontLacksEnglishGlyphs(fontFamily);
624
+ fontGlyphCache.set(fontFamily, result);
625
+ return result;
626
+ }