@nasser-sw/fabric 7.0.0-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 (183) 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 +2198 -272
  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 +2198 -272
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/index.node.cjs +2198 -272
  17. package/dist/index.node.cjs.map +1 -1
  18. package/dist/index.node.mjs +2198 -272
  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 +56 -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 +633 -11
  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 +8 -0
  61. package/dist/src/text/overlayEditor.d.ts.map +1 -1
  62. package/dist/src/text/overlayEditor.min.mjs +1 -1
  63. package/dist/src/text/overlayEditor.min.mjs.map +1 -1
  64. package/dist/src/text/overlayEditor.mjs +395 -56
  65. package/dist/src/text/overlayEditor.mjs.map +1 -1
  66. package/dist/src/text/scriptUtils.d.ts +142 -0
  67. package/dist/src/text/scriptUtils.d.ts.map +1 -0
  68. package/dist/src/text/scriptUtils.min.mjs +2 -0
  69. package/dist/src/text/scriptUtils.min.mjs.map +1 -0
  70. package/dist/src/text/scriptUtils.mjs +212 -0
  71. package/dist/src/text/scriptUtils.mjs.map +1 -0
  72. package/dist/src/util/misc/cornerRadius.d.ts +70 -0
  73. package/dist/src/util/misc/cornerRadius.d.ts.map +1 -0
  74. package/dist/src/util/misc/cornerRadius.min.mjs +2 -0
  75. package/dist/src/util/misc/cornerRadius.min.mjs.map +1 -0
  76. package/dist/src/util/misc/cornerRadius.mjs +181 -0
  77. package/dist/src/util/misc/cornerRadius.mjs.map +1 -0
  78. package/dist-extensions/src/shapes/CustomLine.d.ts +10 -0
  79. package/dist-extensions/src/shapes/CustomLine.d.ts.map +1 -0
  80. package/dist-extensions/src/shapes/Line.d.ts +33 -86
  81. package/dist-extensions/src/shapes/Line.d.ts.map +1 -1
  82. package/dist-extensions/src/shapes/Polyline.d.ts +7 -0
  83. package/dist-extensions/src/shapes/Polyline.d.ts.map +1 -1
  84. package/dist-extensions/src/shapes/Text/Text.d.ts +19 -0
  85. package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
  86. package/dist-extensions/src/shapes/Textbox.d.ts +56 -1
  87. package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
  88. package/dist-extensions/src/shapes/Triangle.d.ts +27 -2
  89. package/dist-extensions/src/shapes/Triangle.d.ts.map +1 -1
  90. package/dist-extensions/src/text/measure.d.ts +9 -0
  91. package/dist-extensions/src/text/measure.d.ts.map +1 -1
  92. package/dist-extensions/src/text/overlayEditor.d.ts +8 -0
  93. package/dist-extensions/src/text/overlayEditor.d.ts.map +1 -1
  94. package/dist-extensions/src/text/scriptUtils.d.ts +142 -0
  95. package/dist-extensions/src/text/scriptUtils.d.ts.map +1 -0
  96. package/dist-extensions/src/util/misc/cornerRadius.d.ts +70 -0
  97. package/dist-extensions/src/util/misc/cornerRadius.d.ts.map +1 -0
  98. package/fabric-test-editor.html +3552 -0
  99. package/fabric-test2.html +647 -0
  100. package/fabric.ts +182 -182
  101. package/fonts/STV Bold.ttf +0 -0
  102. package/fonts/STV Light.ttf +0 -0
  103. package/fonts/STV Regular.ttf +0 -0
  104. package/package.json +164 -164
  105. package/src/shapes/Line.ts +484 -157
  106. package/src/shapes/Polyline.ts +70 -29
  107. package/src/shapes/Text/Text.ts +317 -19
  108. package/src/shapes/Textbox.ts +663 -12
  109. package/src/shapes/Triangle.spec.ts +76 -0
  110. package/src/shapes/Triangle.ts +85 -15
  111. package/src/text/measure.ts +200 -50
  112. package/src/text/overlayEditor.ts +504 -94
  113. package/src/util/misc/cornerRadius.spec.ts +141 -0
  114. package/src/util/misc/cornerRadius.ts +269 -0
  115. /package/debug/{konva → konva-master}/LICENSE +0 -0
  116. /package/debug/{konva → konva-master}/gulpfile.mjs +0 -0
  117. /package/debug/{konva → konva-master}/resources/doc-includes/ContainerParams.txt +0 -0
  118. /package/debug/{konva → konva-master}/resources/doc-includes/NodeParams.txt +0 -0
  119. /package/debug/{konva → konva-master}/resources/doc-includes/ShapeParams.txt +0 -0
  120. /package/debug/{konva → konva-master}/resources/jsdoc.conf.json +0 -0
  121. /package/debug/{konva → konva-master}/rollup.config.mjs +0 -0
  122. /package/debug/{konva → konva-master}/src/Animation.ts +0 -0
  123. /package/debug/{konva → konva-master}/src/BezierFunctions.ts +0 -0
  124. /package/debug/{konva → konva-master}/src/Container.ts +0 -0
  125. /package/debug/{konva → konva-master}/src/Context.ts +0 -0
  126. /package/debug/{konva → konva-master}/src/Core.ts +0 -0
  127. /package/debug/{konva → konva-master}/src/DragAndDrop.ts +0 -0
  128. /package/debug/{konva → konva-master}/src/Factory.ts +0 -0
  129. /package/debug/{konva → konva-master}/src/FastLayer.ts +0 -0
  130. /package/debug/{konva → konva-master}/src/Global.ts +0 -0
  131. /package/debug/{konva → konva-master}/src/Group.ts +0 -0
  132. /package/debug/{konva → konva-master}/src/Layer.ts +0 -0
  133. /package/debug/{konva → konva-master}/src/Node.ts +0 -0
  134. /package/debug/{konva → konva-master}/src/PointerEvents.ts +0 -0
  135. /package/debug/{konva → konva-master}/src/Shape.ts +0 -0
  136. /package/debug/{konva → konva-master}/src/Stage.ts +0 -0
  137. /package/debug/{konva → konva-master}/src/Tween.ts +0 -0
  138. /package/debug/{konva → konva-master}/src/Util.ts +0 -0
  139. /package/debug/{konva → konva-master}/src/Validators.ts +0 -0
  140. /package/debug/{konva → konva-master}/src/_CoreInternals.ts +0 -0
  141. /package/debug/{konva → konva-master}/src/_FullInternals.ts +0 -0
  142. /package/debug/{konva → konva-master}/src/canvas-backend.ts +0 -0
  143. /package/debug/{konva → konva-master}/src/filters/Blur.ts +0 -0
  144. /package/debug/{konva → konva-master}/src/filters/Brighten.ts +0 -0
  145. /package/debug/{konva → konva-master}/src/filters/Brightness.ts +0 -0
  146. /package/debug/{konva → konva-master}/src/filters/Contrast.ts +0 -0
  147. /package/debug/{konva → konva-master}/src/filters/Emboss.ts +0 -0
  148. /package/debug/{konva → konva-master}/src/filters/Enhance.ts +0 -0
  149. /package/debug/{konva → konva-master}/src/filters/Grayscale.ts +0 -0
  150. /package/debug/{konva → konva-master}/src/filters/HSL.ts +0 -0
  151. /package/debug/{konva → konva-master}/src/filters/HSV.ts +0 -0
  152. /package/debug/{konva → konva-master}/src/filters/Invert.ts +0 -0
  153. /package/debug/{konva → konva-master}/src/filters/Kaleidoscope.ts +0 -0
  154. /package/debug/{konva → konva-master}/src/filters/Mask.ts +0 -0
  155. /package/debug/{konva → konva-master}/src/filters/Noise.ts +0 -0
  156. /package/debug/{konva → konva-master}/src/filters/Pixelate.ts +0 -0
  157. /package/debug/{konva → konva-master}/src/filters/Posterize.ts +0 -0
  158. /package/debug/{konva → konva-master}/src/filters/RGB.ts +0 -0
  159. /package/debug/{konva → konva-master}/src/filters/RGBA.ts +0 -0
  160. /package/debug/{konva → konva-master}/src/filters/Sepia.ts +0 -0
  161. /package/debug/{konva → konva-master}/src/filters/Solarize.ts +0 -0
  162. /package/debug/{konva → konva-master}/src/filters/Threshold.ts +0 -0
  163. /package/debug/{konva → konva-master}/src/index.ts +0 -0
  164. /package/debug/{konva → konva-master}/src/shapes/Arc.ts +0 -0
  165. /package/debug/{konva → konva-master}/src/shapes/Arrow.ts +0 -0
  166. /package/debug/{konva → konva-master}/src/shapes/Circle.ts +0 -0
  167. /package/debug/{konva → konva-master}/src/shapes/Ellipse.ts +0 -0
  168. /package/debug/{konva → konva-master}/src/shapes/Image.ts +0 -0
  169. /package/debug/{konva → konva-master}/src/shapes/Label.ts +0 -0
  170. /package/debug/{konva → konva-master}/src/shapes/Line.ts +0 -0
  171. /package/debug/{konva → konva-master}/src/shapes/Path.ts +0 -0
  172. /package/debug/{konva → konva-master}/src/shapes/Rect.ts +0 -0
  173. /package/debug/{konva → konva-master}/src/shapes/RegularPolygon.ts +0 -0
  174. /package/debug/{konva → konva-master}/src/shapes/Ring.ts +0 -0
  175. /package/debug/{konva → konva-master}/src/shapes/Sprite.ts +0 -0
  176. /package/debug/{konva → konva-master}/src/shapes/Star.ts +0 -0
  177. /package/debug/{konva → konva-master}/src/shapes/TextPath.ts +0 -0
  178. /package/debug/{konva → konva-master}/src/shapes/Transformer.ts +0 -0
  179. /package/debug/{konva → konva-master}/src/shapes/Wedge.ts +0 -0
  180. /package/debug/{konva → konva-master}/src/skia-backend.ts +0 -0
  181. /package/debug/{konva → konva-master}/src/types.ts +0 -0
  182. /package/debug/{konva → konva-master}/tsconfig.json +0 -0
  183. /package/debug/{konva → konva-master}/tsconfig.test.json +0 -0
@@ -3,6 +3,7 @@ import { IText } from './IText/IText.mjs';
3
3
  import { classRegistry } from '../ClassRegistry.mjs';
4
4
  import { createTextboxDefaultControls } from '../controls/commonControls.mjs';
5
5
  import { JUSTIFY } from './Text/constants.mjs';
6
+ import { fontLacksEnglishGlyphsCached } from '../text/measure.mjs';
6
7
  import { layoutText } from '../text/layout.mjs';
7
8
 
8
9
  // @TODO: Many things here are configuration related and shouldn't be on the class nor prototype
@@ -43,6 +44,7 @@ class Textbox extends IText {
43
44
  ...Textbox.ownDefaults,
44
45
  ...options
45
46
  });
47
+ this.initializeEventListeners();
46
48
  }
47
49
 
48
50
  /**
@@ -64,8 +66,27 @@ class Textbox extends IText {
64
66
  */
65
67
  initDimensions() {
66
68
  if (!this.initialized) {
69
+ this.initialized = true;
70
+ }
71
+
72
+ // Prevent rapid recalculations during moves
73
+ if (this._usingBrowserWrapping) {
74
+ const now = Date.now();
75
+ const lastCall = this._lastInitDimensionsTime || 0;
76
+ const isRapidCall = now - lastCall < 100;
77
+ const isDuringLoading = this._jsonLoading || !this._browserWrapInitialized;
78
+ if (isRapidCall && !isDuringLoading) {
79
+ return;
80
+ }
81
+ this._lastInitDimensionsTime = now;
82
+ }
83
+
84
+ // Skip if nothing changed
85
+ const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
86
+ if (this._lastDimensionState === currentState && this._textLines && this._textLines.length > 0) {
67
87
  return;
68
88
  }
89
+ this._lastDimensionState = currentState;
69
90
 
70
91
  // Use advanced layout if enabled
71
92
  if (this.enableAdvancedLayout) {
@@ -76,17 +97,142 @@ class Textbox extends IText {
76
97
  // clear dynamicMinWidth as it will be different after we re-wrap line
77
98
  this.dynamicMinWidth = 0;
78
99
  // wrap lines
79
- this._styleMap = this._generateStyleMap(this._splitText());
80
- // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap
81
- if (this.dynamicMinWidth > this.width) {
100
+ const splitTextResult = this._splitText();
101
+ this._styleMap = this._generateStyleMap(splitTextResult);
102
+
103
+ // For browser wrapping, ensure _textLines is set from browser results
104
+ if (this._usingBrowserWrapping && splitTextResult && splitTextResult.lines) {
105
+ this._textLines = splitTextResult.lines.map(line => line.split(''));
106
+
107
+ // Store justify measurements and browser height
108
+ const justifyMeasurements = splitTextResult.justifySpaceMeasurements;
109
+ if (justifyMeasurements) {
110
+ this._styleMap.justifySpaceMeasurements = justifyMeasurements;
111
+ }
112
+ const actualHeight = splitTextResult.actualBrowserHeight;
113
+ if (actualHeight) {
114
+ this._actualBrowserHeight = actualHeight;
115
+ }
116
+ }
117
+ // Don't auto-resize width when using browser wrapping to prevent width increases during moves
118
+ if (!this._usingBrowserWrapping && this.dynamicMinWidth > this.width) {
82
119
  this._set('width', this.dynamicMinWidth);
83
120
  }
121
+
122
+ // For browser wrapping fonts (like STV), ensure minimum width for new textboxes
123
+ // since these fonts can't measure English characters properly
124
+ if (this._usingBrowserWrapping && this.width < 50) {
125
+ console.log(`🔤 BROWSER WRAP: Font ${this.fontFamily} has width ${this.width}px, setting to 300px for usability`);
126
+ this.width = 300;
127
+ }
128
+
129
+ // Mark browser wrapping as initialized when complete
130
+ if (this._usingBrowserWrapping) {
131
+ this._browserWrapInitialized = true;
132
+ }
84
133
  if (this.textAlign.includes(JUSTIFY)) {
134
+ // For browser wrapping fonts, apply browser-calculated justify spaces
135
+ if (this._usingBrowserWrapping) {
136
+ console.log('🔤 BROWSER WRAP: Applying browser-calculated justify spaces');
137
+ this._applyBrowserJustifySpaces();
138
+ return;
139
+ }
140
+
141
+ // Don't apply justify alignment during drag operations to prevent snapping
142
+ const now = Date.now();
143
+ const lastDragTime = this._lastInitDimensionsTime || 0;
144
+ const isDuringDrag = now - lastDragTime < 200; // 200ms window for drag detection
145
+
146
+ if (isDuringDrag) {
147
+ console.log('🔤 Skipping justify during drag operation to prevent snapping');
148
+ return;
149
+ }
150
+
151
+ // For non-browser-wrapping fonts, use Fabric's justify system
85
152
  // once text is measured we need to make space fatter to make justified text.
86
- this.enlargeSpaces();
153
+ // Ensure __charBounds exists and fonts are ready before applying justify
154
+ if (this.__charBounds && this.__charBounds.length > 0) {
155
+ // Check if font is ready for accurate justify calculations
156
+ const fontReady = this._isFontReady ? this._isFontReady() : true;
157
+ if (fontReady) {
158
+ this.enlargeSpaces();
159
+ } else {
160
+ console.warn('⚠️ Textbox: Font not ready for justify, deferring enlargeSpaces');
161
+ // Defer justify calculation until font is ready
162
+ this._scheduleJustifyAfterFontLoad();
163
+ }
164
+ } else {
165
+ console.warn('⚠️ Textbox: __charBounds not ready for justify alignment, deferring enlargeSpaces');
166
+ // Defer the justify calculation until the next frame
167
+ setTimeout(() => {
168
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
169
+ var _this$canvas;
170
+ console.log('🔧 Applying deferred Textbox justify alignment');
171
+ this.enlargeSpaces();
172
+ (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
173
+ }
174
+ }, 0);
175
+ }
176
+ }
177
+ // Calculate height - use Fabric's calculation for proper text rendering space
178
+ if (this._usingBrowserWrapping && this._textLines && this._textLines.length > 0) {
179
+ const actualBrowserHeight = this._actualBrowserHeight;
180
+ const oldHeight = this.height;
181
+ // Use Fabric's height calculation since it knows how much space text rendering needs
182
+ this.height = this.calcTextHeight();
183
+
184
+ // Force canvas refresh and control update if height changed significantly
185
+ if (Math.abs(this.height - oldHeight) > 1) {
186
+ var _this$canvas2, _this$_textLines;
187
+ this.setCoords();
188
+ (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
189
+
190
+ // DEBUG: Log exact positioning details
191
+ console.log(`🎯 POSITIONING DEBUG:`);
192
+ console.log(` Textbox height: ${this.height}px`);
193
+ console.log(` Textbox top: ${this.top}px`);
194
+ console.log(` Textbox left: ${this.left}px`);
195
+ console.log(` Text lines: ${((_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length) || 0}`);
196
+ console.log(` Font size: ${this.fontSize}px`);
197
+ console.log(` Line height: ${this.lineHeight || 1.16}`);
198
+ console.log(` Calculated line height: ${this.fontSize * (this.lineHeight || 1.16)}px`);
199
+ console.log(` _getTopOffset(): ${this._getTopOffset()}px`);
200
+ console.log(` calcTextHeight(): ${this.calcTextHeight()}px`);
201
+ console.log(` Browser height: ${actualBrowserHeight}px`);
202
+ console.log(` Height difference: ${this.height - this.calcTextHeight()}px`);
203
+ }
204
+ } else {
205
+ this.height = this.calcTextHeight();
87
206
  }
88
- // clear cache and re-calculate height
89
- this.height = this.calcTextHeight();
207
+ }
208
+
209
+ /**
210
+ * Schedule justify calculation after font loads (Textbox-specific)
211
+ * @private
212
+ */
213
+ _scheduleJustifyAfterFontLoad() {
214
+ if (typeof document === 'undefined' || !('fonts' in document)) {
215
+ return;
216
+ }
217
+
218
+ // Only schedule if not already waiting
219
+ if (this._fontJustifyScheduled) {
220
+ return;
221
+ }
222
+ this._fontJustifyScheduled = true;
223
+ const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
224
+ document.fonts.load(fontSpec).then(() => {
225
+ var _this$canvas3;
226
+ this._fontJustifyScheduled = false;
227
+ console.log('🔧 Textbox: Font loaded, applying justify alignment');
228
+
229
+ // Re-run initDimensions to ensure proper justify calculation
230
+ this.initDimensions();
231
+ (_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
232
+ }).catch(() => {
233
+ this._fontJustifyScheduled = false;
234
+ console.warn('⚠️ Textbox: Font loading failed, justify may be incorrect');
235
+ });
90
236
  }
91
237
 
92
238
  /**
@@ -453,19 +599,33 @@ class Textbox extends IText {
453
599
  width: wordWidth
454
600
  } = data[i];
455
601
  offset += word.length;
456
- lineWidth += infixWidth + wordWidth - additionalSpace;
457
- if (lineWidth > maxWidth && !lineJustStarted) {
602
+
603
+ // Predictive wrapping: check if adding this word would exceed the width
604
+ const potentialLineWidth = lineWidth + infixWidth + wordWidth - additionalSpace;
605
+ // Use exact width to match overlay editor behavior
606
+ const conservativeMaxWidth = maxWidth; // No artificial buffer
607
+
608
+ // Debug logging for wrapping decisions
609
+ const currentLineText = line.join('');
610
+ console.log(`🔧 FABRIC WRAP CHECK: "${data[i].word}" -> potential: ${potentialLineWidth.toFixed(1)}px vs limit: ${conservativeMaxWidth.toFixed(1)}px`);
611
+ if (potentialLineWidth > conservativeMaxWidth && !lineJustStarted) {
612
+ // This word would exceed the width, wrap before adding it
613
+ console.log(`🔧 FABRIC WRAP! Line: "${currentLineText}" (${lineWidth.toFixed(1)}px)`);
458
614
  graphemeLines.push(line);
459
615
  line = [];
460
- lineWidth = wordWidth;
616
+ lineWidth = wordWidth; // Start new line with just this word
461
617
  lineJustStarted = true;
462
618
  } else {
463
- lineWidth += additionalSpace;
619
+ // Word fits, add it to current line
620
+ lineWidth = potentialLineWidth + additionalSpace;
464
621
  }
465
622
  if (!lineJustStarted && !splitByGrapheme) {
466
623
  line.push(infix);
467
624
  }
468
625
  line = line.concat(word);
626
+
627
+ // Debug: show current line after adding word
628
+ console.log(`🔧 FABRIC AFTER ADD: Line now: "${line.join('')}" (${line.length} chars)`);
469
629
  infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset);
470
630
  offset++;
471
631
  lineJustStarted = false;
@@ -475,9 +635,19 @@ class Textbox extends IText {
475
635
  // TODO: this code is probably not necessary anymore.
476
636
  // it can be moved out of this function since largestWordWidth is now
477
637
  // known in advance
478
- if (largestWordWidth + reservedSpace > this.dynamicMinWidth) {
638
+ // Don't modify dynamicMinWidth when using browser wrapping to prevent width increases
639
+ if (!this._usingBrowserWrapping && largestWordWidth + reservedSpace > this.dynamicMinWidth) {
640
+ console.log(`🔧 FABRIC updating dynamicMinWidth: ${this.dynamicMinWidth} -> ${largestWordWidth - additionalSpace + reservedSpace}`);
479
641
  this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace;
642
+ } else if (this._usingBrowserWrapping) {
643
+ console.log(`🔤 BROWSER WRAP: Skipping dynamicMinWidth update to prevent width increase`);
480
644
  }
645
+
646
+ // Debug: show final wrapped lines
647
+ console.log(`🔧 FABRIC FINAL LINES: ${graphemeLines.length} lines`);
648
+ graphemeLines.forEach((line, i) => {
649
+ console.log(` Line ${i + 1}: "${line.join('')}" (${line.length} chars)`);
650
+ });
481
651
  return graphemeLines;
482
652
  }
483
653
 
@@ -521,6 +691,260 @@ class Textbox extends IText {
521
691
  * @override
522
692
  */
523
693
  _splitTextIntoLines(text) {
694
+ // Check if we need browser wrapping using smart font detection
695
+ const needsBrowserWrapping = this.fontFamily && fontLacksEnglishGlyphsCached(this.fontFamily);
696
+ if (needsBrowserWrapping) {
697
+ // Cache key based on text content, width, font properties, AND text alignment
698
+ const textHash = text.length + text.slice(0, 50); // Include text content in cache key
699
+ const cacheKey = `${textHash}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
700
+
701
+ // Check if we have a cached result and nothing has changed
702
+ if (this._browserWrapCache && this._browserWrapCache.key === cacheKey) {
703
+ const cachedResult = this._browserWrapCache.result;
704
+
705
+ // For justify alignment, ensure we have the measurements
706
+ if (this.textAlign.includes('justify') && !cachedResult.justifySpaceMeasurements) ; else {
707
+ return cachedResult;
708
+ }
709
+ }
710
+ const result = this._splitTextIntoLinesWithBrowser(text);
711
+
712
+ // Cache the result
713
+ this._browserWrapCache = {
714
+ key: cacheKey,
715
+ result
716
+ };
717
+
718
+ // Mark that we used browser wrapping to prevent dynamicMinWidth modifications
719
+ this._usingBrowserWrapping = true;
720
+ return result;
721
+ }
722
+
723
+ // Clear the browser wrapping flag when using regular wrapping
724
+ this._usingBrowserWrapping = false;
725
+
726
+ // Default Fabric wrapping for other fonts
727
+ const newText = super._splitTextIntoLines(text),
728
+ graphemeLines = this._wrapText(newText.lines, this.width),
729
+ lines = new Array(graphemeLines.length);
730
+ for (let i = 0; i < graphemeLines.length; i++) {
731
+ lines[i] = graphemeLines[i].join('');
732
+ }
733
+ newText.lines = lines;
734
+ newText.graphemeLines = graphemeLines;
735
+ return newText;
736
+ }
737
+
738
+ /**
739
+ * Use browser's native text wrapping for accurate handling of fonts without English glyphs
740
+ * @private
741
+ */
742
+ _splitTextIntoLinesWithBrowser(text) {
743
+ if (typeof document === 'undefined') {
744
+ // Fallback to regular wrapping in Node.js
745
+ return this._splitTextIntoLinesDefault(text);
746
+ }
747
+
748
+ // Create a hidden element that mimics the overlay editor
749
+ const testElement = document.createElement('div');
750
+ testElement.style.position = 'absolute';
751
+ testElement.style.left = '-9999px';
752
+ testElement.style.visibility = 'hidden';
753
+ testElement.style.fontSize = `${this.fontSize}px`;
754
+ testElement.style.fontFamily = `"${this.fontFamily}"`;
755
+ testElement.style.fontWeight = String(this.fontWeight || 'normal');
756
+ testElement.style.fontStyle = String(this.fontStyle || 'normal');
757
+ testElement.style.lineHeight = String(this.lineHeight || 1.16);
758
+ testElement.style.width = `${this.width}px`;
759
+ testElement.style.direction = this.direction || 'ltr';
760
+ testElement.style.whiteSpace = 'pre-wrap';
761
+ testElement.style.wordBreak = 'normal';
762
+ testElement.style.overflowWrap = 'break-word';
763
+
764
+ // Set browser-native text alignment (including justify)
765
+ if (this.textAlign.includes('justify')) {
766
+ testElement.style.textAlign = 'justify';
767
+ testElement.style.textAlignLast = 'auto'; // Let browser decide last line alignment
768
+ } else {
769
+ testElement.style.textAlign = this.textAlign;
770
+ }
771
+ testElement.textContent = text;
772
+ document.body.appendChild(testElement);
773
+
774
+ // Get the browser's natural line breaks
775
+ const range = document.createRange();
776
+ const lines = [];
777
+ const graphemeLines = [];
778
+ try {
779
+ // Simple approach: split by measuring character positions
780
+ const textNode = testElement.firstChild;
781
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
782
+ let currentLineStart = 0;
783
+ const textLength = text.length;
784
+ let previousBottom = 0;
785
+ for (let i = 0; i <= textLength; i++) {
786
+ range.setStart(textNode, currentLineStart);
787
+ range.setEnd(textNode, i);
788
+ const rect = range.getBoundingClientRect();
789
+ if (i > currentLineStart && (rect.bottom > previousBottom + 5 || i === textLength)) {
790
+ // New line detected or end of text
791
+ const lineEnd = i === textLength ? i : i - 1;
792
+ const lineText = text.substring(currentLineStart, lineEnd).trim();
793
+ if (lineText) {
794
+ lines.push(lineText);
795
+ // Convert to graphemes for compatibility
796
+ const graphemeLine = lineText.split('');
797
+ graphemeLines.push(graphemeLine);
798
+ }
799
+ currentLineStart = lineEnd;
800
+ previousBottom = rect.bottom;
801
+ }
802
+ }
803
+ }
804
+ } catch (error) {
805
+ console.warn('Browser wrapping failed, using fallback:', error);
806
+ document.body.removeChild(testElement);
807
+ return this._splitTextIntoLinesDefault(text);
808
+ }
809
+
810
+ // Extract actual browser height BEFORE removing element
811
+ const actualBrowserHeight = testElement.scrollHeight;
812
+ const offsetHeight = testElement.offsetHeight;
813
+ const clientHeight = testElement.clientHeight;
814
+ const boundingRect = testElement.getBoundingClientRect();
815
+ console.log(`🔤 Browser element measurements:`);
816
+ console.log(` scrollHeight: ${actualBrowserHeight}px (content + padding + hidden overflow)`);
817
+ console.log(` offsetHeight: ${offsetHeight}px (content + padding + border)`);
818
+ console.log(` clientHeight: ${clientHeight}px (content + padding, no border/scrollbar)`);
819
+ console.log(` boundingRect.height: ${boundingRect.height}px (actual rendered height)`);
820
+ console.log(` Font size: ${this.fontSize}px, Line height: ${this.lineHeight || 1.16}, Lines: ${lines.length}`);
821
+
822
+ // For justify alignment, extract space measurements from browser BEFORE removing element
823
+ let justifySpaceMeasurements = null;
824
+ if (this.textAlign.includes('justify')) {
825
+ justifySpaceMeasurements = this._extractJustifySpaceMeasurements(testElement, lines);
826
+ }
827
+ document.body.removeChild(testElement);
828
+ console.log(`🔤 Browser wrapping result: ${lines.length} lines`);
829
+
830
+ // Try different height measurements to find the most accurate
831
+ let bestHeight = actualBrowserHeight;
832
+
833
+ // If scrollHeight and offsetHeight differ significantly, investigate
834
+ if (Math.abs(actualBrowserHeight - offsetHeight) > 2) {
835
+ console.log(`🔤 Height discrepancy detected: scrollHeight=${actualBrowserHeight}px vs offsetHeight=${offsetHeight}px`);
836
+ }
837
+
838
+ // Consider using boundingRect height if it's larger (sometimes more accurate for visible content)
839
+ if (boundingRect.height > bestHeight) {
840
+ console.log(`🔤 Using boundingRect height (${boundingRect.height}px) instead of scrollHeight (${bestHeight}px)`);
841
+ bestHeight = boundingRect.height;
842
+ }
843
+
844
+ // Font-specific height adjustments for accurate bounding box
845
+ let adjustedHeight = bestHeight;
846
+
847
+ // Fonts without English glyphs need additional height buffer due to different font metrics
848
+ const lacksEnglishGlyphs = fontLacksEnglishGlyphsCached(this.fontFamily);
849
+ if (lacksEnglishGlyphs) {
850
+ const glyphBuffer = this.fontSize * 0.25; // 25% of font size for non-English fonts
851
+ adjustedHeight = bestHeight + glyphBuffer;
852
+ console.log(`🔤 Non-English font detected (${this.fontFamily}): Adding ${glyphBuffer}px buffer (${bestHeight}px + ${glyphBuffer}px = ${adjustedHeight}px)`);
853
+ } else {
854
+ console.log(`🔤 Standard font (${this.fontFamily}): Using browser height directly (${bestHeight}px)`);
855
+ }
856
+ return {
857
+ _unwrappedLines: [text.split('')],
858
+ lines: lines,
859
+ graphemeText: text.split(''),
860
+ graphemeLines: graphemeLines,
861
+ justifySpaceMeasurements: justifySpaceMeasurements,
862
+ actualBrowserHeight: adjustedHeight
863
+ };
864
+ }
865
+
866
+ /**
867
+ * Extract justify space measurements from browser
868
+ * @private
869
+ */
870
+ _extractJustifySpaceMeasurements(element, lines) {
871
+ console.log(`🔤 Extracting browser justify space measurements for ${lines.length} lines`);
872
+
873
+ // For now, we'll use a simplified approach:
874
+ // Apply uniform space expansion to match the line width
875
+ const spaceWidths = [];
876
+ lines.forEach((line, lineIndex) => {
877
+ const lineSpaces = [];
878
+ const spaceCount = (line.match(/\s/g) || []).length;
879
+ if (spaceCount > 0 && lineIndex < lines.length - 1) {
880
+ // Don't justify last line
881
+ // Calculate how much space expansion is needed
882
+ const normalSpaceWidth = 6.4; // Default space width for STV font
883
+ const lineWidth = this.width;
884
+
885
+ // Estimate natural line width
886
+ const charCount = line.length - spaceCount;
887
+ const avgCharWidth = 12; // Approximate for STV font
888
+
889
+ // Calculate expanded space width
890
+ const remainingSpace = lineWidth - charCount * avgCharWidth;
891
+ const expandedSpaceWidth = remainingSpace / spaceCount;
892
+ console.log(`🔤 Line ${lineIndex}: ${spaceCount} spaces, natural: ${normalSpaceWidth}px -> justified: ${expandedSpaceWidth.toFixed(1)}px`);
893
+
894
+ // Fill array with expanded space widths for this line
895
+ for (let i = 0; i < spaceCount; i++) {
896
+ lineSpaces.push(expandedSpaceWidth);
897
+ }
898
+ }
899
+ spaceWidths.push(lineSpaces);
900
+ });
901
+ return spaceWidths;
902
+ }
903
+
904
+ /**
905
+ * Apply browser-calculated justify space measurements
906
+ * @private
907
+ */
908
+ _applyBrowserJustifySpaces() {
909
+ if (!this._textLines || !this.__charBounds) {
910
+ console.warn('🔤 BROWSER JUSTIFY: _textLines or __charBounds not ready');
911
+ return;
912
+ }
913
+
914
+ // Get space measurements from browser wrapping result
915
+ const styleMap = this._styleMap;
916
+ if (!styleMap || !styleMap.justifySpaceMeasurements) {
917
+ console.warn('🔤 BROWSER JUSTIFY: No justify space measurements available');
918
+ return;
919
+ }
920
+ const spaceWidths = styleMap.justifySpaceMeasurements;
921
+ console.log('🔤 BROWSER JUSTIFY: Applying space measurements to __charBounds');
922
+
923
+ // Apply space widths to character bounds
924
+ this._textLines.forEach((line, lineIndex) => {
925
+ if (!this.__charBounds || !this.__charBounds[lineIndex] || !spaceWidths[lineIndex]) return;
926
+ const lineBounds = this.__charBounds[lineIndex];
927
+ const lineSpaceWidths = spaceWidths[lineIndex];
928
+ let spaceIndex = 0;
929
+ for (let charIndex = 0; charIndex < line.length; charIndex++) {
930
+ if (/\s/.test(line[charIndex]) && spaceIndex < lineSpaceWidths.length) {
931
+ const expandedWidth = lineSpaceWidths[spaceIndex];
932
+ if (lineBounds[charIndex]) {
933
+ const oldWidth = lineBounds[charIndex].width;
934
+ lineBounds[charIndex].width = expandedWidth;
935
+ console.log(`🔤 Line ${lineIndex} space ${spaceIndex}: ${oldWidth.toFixed(1)}px -> ${expandedWidth.toFixed(1)}px`);
936
+ }
937
+ spaceIndex++;
938
+ }
939
+ }
940
+ });
941
+ }
942
+
943
+ /**
944
+ * Fallback to default Fabric wrapping
945
+ * @private
946
+ */
947
+ _splitTextIntoLinesDefault(text) {
524
948
  const newText = super._splitTextIntoLines(text),
525
949
  graphemeLines = this._wrapText(newText.lines, this.width),
526
950
  lines = new Array(graphemeLines.length);
@@ -550,6 +974,204 @@ class Textbox extends IText {
550
974
  }
551
975
  }
552
976
 
977
+ /**
978
+ * Initialize event listeners for safety snap functionality
979
+ * @private
980
+ */
981
+ initializeEventListeners() {
982
+ var _this$canvas4;
983
+ // Track which side is being used for resize to handle position compensation
984
+ let resizeOrigin = null;
985
+
986
+ // Detect resize origin during resizing
987
+ this.on('resizing', e => {
988
+ // Check transform origin to determine which side is being resized
989
+ if (e.transform) {
990
+ const {
991
+ originX
992
+ } = e.transform;
993
+ // originX tells us which side is the anchor - opposite side is being dragged
994
+ resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
995
+ } else if (e.originX) {
996
+ const {
997
+ originX
998
+ } = e;
999
+ resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
1000
+ }
1001
+ });
1002
+
1003
+ // Only trigger safety snap after resize is complete (not during)
1004
+ // Use 'modified' event which fires after user releases the mouse
1005
+ this.on('modified', () => {
1006
+ const currentResizeOrigin = resizeOrigin; // Capture the value before reset
1007
+ // Small delay to ensure text layout is updated
1008
+ setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
1009
+ resizeOrigin = null; // Reset after capturing
1010
+ });
1011
+
1012
+ // Also listen to canvas-level modified event as backup
1013
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.on('object:modified', e => {
1014
+ if (e.target === this) {
1015
+ const currentResizeOrigin = resizeOrigin; // Capture the value before reset
1016
+ setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
1017
+ resizeOrigin = null; // Reset after capturing
1018
+ }
1019
+ });
1020
+ }
1021
+
1022
+ /**
1023
+ * Safety snap to prevent glyph clipping after manual resize.
1024
+ * Similar to Polotno - checks if any glyphs are too close to edges
1025
+ * and automatically expands width if needed.
1026
+ * @private
1027
+ * @param resizeOrigin - Which side was used for resizing ('left' or 'right')
1028
+ */
1029
+ safetySnapWidth(resizeOrigin) {
1030
+ // For Textbox objects, we always want to check for clipping regardless of isWrapping flag
1031
+ if (!this._textLines || this.type.toLowerCase() !== 'textbox' || this._textLines.length === 0) {
1032
+ return;
1033
+ }
1034
+ const lineCount = this._textLines.length;
1035
+ if (lineCount === 0) return;
1036
+ let maxRequiredWidth = 0; // Width including RTL buffer
1037
+
1038
+ for (let i = 0; i < lineCount; i++) {
1039
+ const lineText = this._textLines[i].join(''); // Convert grapheme array to string
1040
+ const lineWidth = this.getLineWidth(i);
1041
+
1042
+ // RTL detection - regex for Arabic, Hebrew, and other RTL characters
1043
+ const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/;
1044
+ if (rtlRegex.test(lineText)) {
1045
+ // Add minimal RTL compensation buffer - just enough to prevent clipping
1046
+ const rtlBuffer = (this.fontSize || 16) * 0.15; // 15% of font size (much smaller)
1047
+ maxRequiredWidth = Math.max(maxRequiredWidth, lineWidth + rtlBuffer);
1048
+ } else {
1049
+ maxRequiredWidth = Math.max(maxRequiredWidth, lineWidth);
1050
+ }
1051
+ }
1052
+
1053
+ // Safety margin - how close glyphs can get before we snap
1054
+ const safetyThreshold = 2; // px - very subtle trigger
1055
+
1056
+ if (maxRequiredWidth > this.width - safetyThreshold) {
1057
+ var _this$canvas5;
1058
+ // Set width to exactly what's needed + minimal safety margin
1059
+ const newWidth = maxRequiredWidth + 1; // Add just 1px safety margin
1060
+
1061
+ // Store original position before width change
1062
+ const originalLeft = this.left;
1063
+ const originalTop = this.top;
1064
+ const widthIncrease = newWidth - this.width;
1065
+
1066
+ // Change width
1067
+ this.set('width', newWidth);
1068
+
1069
+ // Force text layout recalculation
1070
+ this.initDimensions();
1071
+
1072
+ // Only compensate position when resizing from left handle
1073
+ // Right handle resize doesn't shift the text position
1074
+ if (resizeOrigin === 'left') {
1075
+ // When resizing from left, the expansion pushes text right
1076
+ // Compensate by moving the textbox left by the width increase
1077
+ this.set({
1078
+ 'left': originalLeft - widthIncrease,
1079
+ 'top': originalTop
1080
+ });
1081
+ }
1082
+ this.setCoords();
1083
+
1084
+ // Also refresh the overlay editor if it exists
1085
+ if (this.__overlayEditor) {
1086
+ setTimeout(() => {
1087
+ this.__overlayEditor.refresh();
1088
+ }, 0);
1089
+ }
1090
+ (_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
1091
+ }
1092
+ }
1093
+
1094
+ /**
1095
+ * Fix character selection mismatch after JSON loading for browser-wrapped fonts
1096
+ * @private
1097
+ */
1098
+ _fixCharacterMappingAfterJsonLoad() {
1099
+ if (this._usingBrowserWrapping) {
1100
+ // Clear all cached states to force fresh text layout calculation
1101
+ this._browserWrapCache = null;
1102
+ this._lastDimensionState = null;
1103
+
1104
+ // Force complete re-initialization
1105
+ this.initDimensions();
1106
+ this._forceClearCache = true;
1107
+
1108
+ // Ensure canvas refresh
1109
+ this.setCoords();
1110
+ if (this.canvas) {
1111
+ this.canvas.requestRenderAll();
1112
+ }
1113
+ }
1114
+ }
1115
+
1116
+ /**
1117
+ * Force complete textbox re-initialization (useful after JSON loading)
1118
+ * Overrides Text version with Textbox-specific logic
1119
+ */
1120
+ forceTextReinitialization() {
1121
+ console.log('🔄 Force reinitializing Textbox object');
1122
+
1123
+ // CRITICAL: Ensure textbox is marked as initialized
1124
+ this.initialized = true;
1125
+
1126
+ // Clear all caches and force dirty state
1127
+ this._clearCache();
1128
+ this.dirty = true;
1129
+ this.dynamicMinWidth = 0;
1130
+
1131
+ // Force isEditing false to ensure clean state
1132
+ this.isEditing = false;
1133
+ console.log(' → Set initialized=true, dirty=true, cleared caches');
1134
+
1135
+ // Re-initialize dimensions (this will handle justify properly)
1136
+ this.initDimensions();
1137
+
1138
+ // Double-check that justify was applied by checking space widths
1139
+ if (this.textAlign.includes('justify') && this.__charBounds) {
1140
+ setTimeout(() => {
1141
+ var _this$canvas6;
1142
+ // Verify justify was applied by checking if space widths vary
1143
+ let hasVariableSpaces = false;
1144
+ this.__charBounds.forEach((lineBounds, i) => {
1145
+ if (lineBounds && this._textLines && this._textLines[i]) {
1146
+ const spaces = lineBounds.filter((bound, j) => /\s/.test(this._textLines[i][j]));
1147
+ if (spaces.length > 1) {
1148
+ const firstSpaceWidth = spaces[0].width;
1149
+ hasVariableSpaces = spaces.some(space => Math.abs(space.width - firstSpaceWidth) > 0.1);
1150
+ }
1151
+ }
1152
+ });
1153
+ if (!hasVariableSpaces && this.__charBounds.length > 0) {
1154
+ console.warn(' ⚠️ Justify spaces still uniform - forcing enlargeSpaces again');
1155
+ if (this.enlargeSpaces) {
1156
+ this.enlargeSpaces();
1157
+ }
1158
+ } else {
1159
+ console.log(' ✅ Justify spaces properly expanded');
1160
+ }
1161
+
1162
+ // Ensure height is recalculated - use browser height if available
1163
+ if (this._usingBrowserWrapping && this._actualBrowserHeight) {
1164
+ this.height = this._actualBrowserHeight;
1165
+ console.log(`🔤 JUSTIFY: Preserved browser height: ${this.height}px`);
1166
+ } else {
1167
+ this.height = this.calcTextHeight();
1168
+ console.log(`🔧 JUSTIFY: Used calcTextHeight: ${this.height}px`);
1169
+ }
1170
+ (_this$canvas6 = this.canvas) === null || _this$canvas6 === void 0 || _this$canvas6.requestRenderAll();
1171
+ }, 10);
1172
+ }
1173
+ }
1174
+
553
1175
  /**
554
1176
  * Returns object representation of an instance
555
1177
  * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output