@nasser-sw/fabric 7.0.1-beta8 → 7.0.1-beta9

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 (138) hide show
  1. package/debug/konva-master/CHANGELOG.md +1475 -0
  2. package/debug/konva-master/LICENSE +22 -0
  3. package/debug/konva-master/README.md +209 -0
  4. package/debug/konva-master/gulpfile.mjs +110 -0
  5. package/debug/konva-master/package.json +139 -0
  6. package/debug/konva-master/release.sh +62 -0
  7. package/debug/konva-master/resources/doc-includes/ContainerParams.txt +6 -0
  8. package/debug/konva-master/resources/doc-includes/NodeParams.txt +20 -0
  9. package/debug/konva-master/resources/doc-includes/ShapeParams.txt +53 -0
  10. package/debug/konva-master/resources/jsdoc.conf.json +28 -0
  11. package/debug/konva-master/rollup.config.mjs +32 -0
  12. package/debug/konva-master/src/Animation.ts +237 -0
  13. package/debug/konva-master/src/BezierFunctions.ts +826 -0
  14. package/debug/konva-master/src/Canvas.ts +230 -0
  15. package/debug/konva-master/src/Container.ts +649 -0
  16. package/debug/konva-master/src/Context.ts +1017 -0
  17. package/debug/konva-master/src/Core.ts +5 -0
  18. package/debug/konva-master/src/DragAndDrop.ts +173 -0
  19. package/debug/konva-master/src/Factory.ts +246 -0
  20. package/debug/konva-master/src/FastLayer.ts +29 -0
  21. package/debug/konva-master/src/Global.ts +210 -0
  22. package/debug/konva-master/src/Group.ts +31 -0
  23. package/debug/konva-master/src/Layer.ts +546 -0
  24. package/debug/konva-master/src/Node.ts +3477 -0
  25. package/debug/konva-master/src/PointerEvents.ts +67 -0
  26. package/debug/konva-master/src/Shape.ts +2081 -0
  27. package/debug/konva-master/src/Stage.ts +1000 -0
  28. package/debug/konva-master/src/Tween.ts +811 -0
  29. package/debug/konva-master/src/Util.ts +1123 -0
  30. package/debug/konva-master/src/Validators.ts +210 -0
  31. package/debug/konva-master/src/_CoreInternals.ts +85 -0
  32. package/debug/konva-master/src/_FullInternals.ts +171 -0
  33. package/debug/konva-master/src/canvas-backend.ts +36 -0
  34. package/debug/konva-master/src/filters/Blur.ts +388 -0
  35. package/debug/konva-master/src/filters/Brighten.ts +48 -0
  36. package/debug/konva-master/src/filters/Brightness.ts +30 -0
  37. package/debug/konva-master/src/filters/Contrast.ts +75 -0
  38. package/debug/konva-master/src/filters/Emboss.ts +207 -0
  39. package/debug/konva-master/src/filters/Enhance.ts +154 -0
  40. package/debug/konva-master/src/filters/Grayscale.ts +25 -0
  41. package/debug/konva-master/src/filters/HSL.ts +108 -0
  42. package/debug/konva-master/src/filters/HSV.ts +106 -0
  43. package/debug/konva-master/src/filters/Invert.ts +23 -0
  44. package/debug/konva-master/src/filters/Kaleidoscope.ts +274 -0
  45. package/debug/konva-master/src/filters/Mask.ts +220 -0
  46. package/debug/konva-master/src/filters/Noise.ts +44 -0
  47. package/debug/konva-master/src/filters/Pixelate.ts +107 -0
  48. package/debug/konva-master/src/filters/Posterize.ts +46 -0
  49. package/debug/konva-master/src/filters/RGB.ts +82 -0
  50. package/debug/konva-master/src/filters/RGBA.ts +103 -0
  51. package/debug/konva-master/src/filters/Sepia.ts +27 -0
  52. package/debug/konva-master/src/filters/Solarize.ts +29 -0
  53. package/debug/konva-master/src/filters/Threshold.ts +44 -0
  54. package/debug/konva-master/src/index.ts +3 -0
  55. package/debug/konva-master/src/shapes/Arc.ts +176 -0
  56. package/debug/konva-master/src/shapes/Arrow.ts +231 -0
  57. package/debug/konva-master/src/shapes/Circle.ts +76 -0
  58. package/debug/konva-master/src/shapes/Ellipse.ts +121 -0
  59. package/debug/konva-master/src/shapes/Image.ts +319 -0
  60. package/debug/konva-master/src/shapes/Label.ts +386 -0
  61. package/debug/konva-master/src/shapes/Line.ts +364 -0
  62. package/debug/konva-master/src/shapes/Path.ts +1013 -0
  63. package/debug/konva-master/src/shapes/Rect.ts +79 -0
  64. package/debug/konva-master/src/shapes/RegularPolygon.ts +167 -0
  65. package/debug/konva-master/src/shapes/Ring.ts +94 -0
  66. package/debug/konva-master/src/shapes/Sprite.ts +370 -0
  67. package/debug/konva-master/src/shapes/Star.ts +125 -0
  68. package/debug/konva-master/src/shapes/Text.ts +1065 -0
  69. package/debug/konva-master/src/shapes/TextPath.ts +583 -0
  70. package/debug/konva-master/src/shapes/Transformer.ts +1889 -0
  71. package/debug/konva-master/src/shapes/Wedge.ts +129 -0
  72. package/debug/konva-master/src/skia-backend.ts +35 -0
  73. package/debug/konva-master/src/types.ts +84 -0
  74. package/debug/konva-master/tsconfig.json +31 -0
  75. package/debug/konva-master/tsconfig.test.json +7 -0
  76. package/dist/index.js +915 -23
  77. package/dist/index.js.map +1 -1
  78. package/dist/index.min.js +1 -1
  79. package/dist/index.min.js.map +1 -1
  80. package/dist/index.min.mjs +1 -1
  81. package/dist/index.min.mjs.map +1 -1
  82. package/dist/index.mjs +915 -23
  83. package/dist/index.mjs.map +1 -1
  84. package/dist/index.node.cjs +915 -23
  85. package/dist/index.node.cjs.map +1 -1
  86. package/dist/index.node.mjs +915 -23
  87. package/dist/index.node.mjs.map +1 -1
  88. package/dist/package.json.min.mjs +1 -1
  89. package/dist/package.json.mjs +1 -1
  90. package/dist/src/shapes/Text/Text.d.ts +19 -0
  91. package/dist/src/shapes/Text/Text.d.ts.map +1 -1
  92. package/dist/src/shapes/Text/Text.min.mjs +1 -1
  93. package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
  94. package/dist/src/shapes/Text/Text.mjs +238 -4
  95. package/dist/src/shapes/Text/Text.mjs.map +1 -1
  96. package/dist/src/shapes/Textbox.d.ts +38 -1
  97. package/dist/src/shapes/Textbox.d.ts.map +1 -1
  98. package/dist/src/shapes/Textbox.min.mjs +1 -1
  99. package/dist/src/shapes/Textbox.min.mjs.map +1 -1
  100. package/dist/src/shapes/Textbox.mjs +497 -15
  101. package/dist/src/shapes/Textbox.mjs.map +1 -1
  102. package/dist/src/text/examples/arabicTextExample.d.ts +60 -0
  103. package/dist/src/text/examples/arabicTextExample.d.ts.map +1 -0
  104. package/dist/src/text/measure.d.ts +9 -0
  105. package/dist/src/text/measure.d.ts.map +1 -1
  106. package/dist/src/text/measure.min.mjs +1 -1
  107. package/dist/src/text/measure.min.mjs.map +1 -1
  108. package/dist/src/text/measure.mjs +175 -4
  109. package/dist/src/text/measure.mjs.map +1 -1
  110. package/dist/src/text/overlayEditor.d.ts.map +1 -1
  111. package/dist/src/text/overlayEditor.min.mjs +1 -1
  112. package/dist/src/text/overlayEditor.min.mjs.map +1 -1
  113. package/dist/src/text/overlayEditor.mjs +7 -0
  114. package/dist/src/text/overlayEditor.mjs.map +1 -1
  115. package/dist/src/text/scriptUtils.d.ts +142 -0
  116. package/dist/src/text/scriptUtils.d.ts.map +1 -0
  117. package/dist/src/text/scriptUtils.min.mjs +2 -0
  118. package/dist/src/text/scriptUtils.min.mjs.map +1 -0
  119. package/dist/src/text/scriptUtils.mjs +212 -0
  120. package/dist/src/text/scriptUtils.mjs.map +1 -0
  121. package/dist-extensions/src/shapes/Text/Text.d.ts +19 -0
  122. package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
  123. package/dist-extensions/src/shapes/Textbox.d.ts +38 -1
  124. package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
  125. package/dist-extensions/src/text/measure.d.ts +9 -0
  126. package/dist-extensions/src/text/measure.d.ts.map +1 -1
  127. package/dist-extensions/src/text/overlayEditor.d.ts.map +1 -1
  128. package/dist-extensions/src/text/scriptUtils.d.ts +142 -0
  129. package/dist-extensions/src/text/scriptUtils.d.ts.map +1 -0
  130. package/fabric-test-editor.html +2401 -46
  131. package/fonts/STV Bold.ttf +0 -0
  132. package/fonts/STV Light.ttf +0 -0
  133. package/fonts/STV Regular.ttf +0 -0
  134. package/package.json +1 -1
  135. package/src/shapes/Text/Text.ts +238 -5
  136. package/src/shapes/Textbox.ts +521 -11
  137. package/src/text/measure.ts +200 -50
  138. package/src/text/overlayEditor.ts +7 -0
@@ -8,6 +8,7 @@ import type { SerializedITextProps, ITextProps } from './IText/IText';
8
8
  import type { ITextEvents } from './IText/ITextBehavior';
9
9
  import type { TextLinesInfo } from './Text/Text';
10
10
  import type { Control } from '../controls/Control';
11
+ import { fontLacksEnglishGlyphsCached } from '../text/measure';
11
12
  import { layoutText } from '../text/layout';
12
13
 
13
14
  // @TODO: Many things here are configuration related and shouldn't be on the class nor prototype
@@ -128,8 +129,28 @@ export class Textbox<
128
129
  */
129
130
  initDimensions() {
130
131
  if (!this.initialized) {
132
+ this.initialized = true;
133
+ }
134
+
135
+ // Prevent rapid recalculations during moves
136
+ if ((this as any)._usingBrowserWrapping) {
137
+ const now = Date.now();
138
+ const lastCall = (this as any)._lastInitDimensionsTime || 0;
139
+ const isRapidCall = now - lastCall < 100;
140
+ const isDuringLoading = (this as any)._jsonLoading || !(this as any)._browserWrapInitialized;
141
+
142
+ if (isRapidCall && !isDuringLoading) {
143
+ return;
144
+ }
145
+ (this as any)._lastInitDimensionsTime = now;
146
+ }
147
+
148
+ // Skip if nothing changed
149
+ const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
150
+ if ((this as any)._lastDimensionState === currentState && this._textLines && this._textLines.length > 0) {
131
151
  return;
132
152
  }
153
+ (this as any)._lastDimensionState = currentState;
133
154
 
134
155
  // Use advanced layout if enabled
135
156
  if (this.enableAdvancedLayout) {
@@ -141,17 +162,142 @@ export class Textbox<
141
162
  // clear dynamicMinWidth as it will be different after we re-wrap line
142
163
  this.dynamicMinWidth = 0;
143
164
  // wrap lines
144
- this._styleMap = this._generateStyleMap(this._splitText());
145
- // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap
146
- if (this.dynamicMinWidth > this.width) {
165
+ const splitTextResult = this._splitText();
166
+ this._styleMap = this._generateStyleMap(splitTextResult);
167
+
168
+ // For browser wrapping, ensure _textLines is set from browser results
169
+ if ((this as any)._usingBrowserWrapping && splitTextResult && splitTextResult.lines) {
170
+ this._textLines = splitTextResult.lines.map(line => line.split(''));
171
+
172
+ // Store justify measurements and browser height
173
+ const justifyMeasurements = (splitTextResult as any).justifySpaceMeasurements;
174
+ if (justifyMeasurements) {
175
+ (this._styleMap as any).justifySpaceMeasurements = justifyMeasurements;
176
+ }
177
+
178
+ const actualHeight = (splitTextResult as any).actualBrowserHeight;
179
+ if (actualHeight) {
180
+ (this as any)._actualBrowserHeight = actualHeight;
181
+ }
182
+ }
183
+ // Don't auto-resize width when using browser wrapping to prevent width increases during moves
184
+ if (!((this as any)._usingBrowserWrapping) && this.dynamicMinWidth > this.width) {
147
185
  this._set('width', this.dynamicMinWidth);
148
186
  }
187
+
188
+ // For browser wrapping fonts (like STV), ensure minimum width for new textboxes
189
+ // since these fonts can't measure English characters properly
190
+ if ((this as any)._usingBrowserWrapping && this.width < 50) {
191
+ console.log(`🔤 BROWSER WRAP: Font ${this.fontFamily} has width ${this.width}px, setting to 300px for usability`);
192
+ this.width = 300;
193
+ }
194
+
195
+ // Mark browser wrapping as initialized when complete
196
+ if ((this as any)._usingBrowserWrapping) {
197
+ (this as any)._browserWrapInitialized = true;
198
+ }
199
+
149
200
  if (this.textAlign.includes(JUSTIFY)) {
201
+ // For browser wrapping fonts, apply browser-calculated justify spaces
202
+ if ((this as any)._usingBrowserWrapping) {
203
+ console.log('🔤 BROWSER WRAP: Applying browser-calculated justify spaces');
204
+ this._applyBrowserJustifySpaces();
205
+ return;
206
+ }
207
+
208
+ // Don't apply justify alignment during drag operations to prevent snapping
209
+ const now = Date.now();
210
+ const lastDragTime = (this as any)._lastInitDimensionsTime || 0;
211
+ const isDuringDrag = now - lastDragTime < 200; // 200ms window for drag detection
212
+
213
+ if (isDuringDrag) {
214
+ console.log('🔤 Skipping justify during drag operation to prevent snapping');
215
+ return;
216
+ }
217
+
218
+ // For non-browser-wrapping fonts, use Fabric's justify system
150
219
  // once text is measured we need to make space fatter to make justified text.
151
- this.enlargeSpaces();
220
+ // Ensure __charBounds exists and fonts are ready before applying justify
221
+ if (this.__charBounds && this.__charBounds.length > 0) {
222
+ // Check if font is ready for accurate justify calculations
223
+ const fontReady = this._isFontReady ? this._isFontReady() : true;
224
+ if (fontReady) {
225
+ this.enlargeSpaces();
226
+ } else {
227
+ console.warn('⚠️ Textbox: Font not ready for justify, deferring enlargeSpaces');
228
+ // Defer justify calculation until font is ready
229
+ this._scheduleJustifyAfterFontLoad();
230
+ }
231
+ } else {
232
+ console.warn('⚠️ Textbox: __charBounds not ready for justify alignment, deferring enlargeSpaces');
233
+ // Defer the justify calculation until the next frame
234
+ setTimeout(() => {
235
+ if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
236
+ console.log('🔧 Applying deferred Textbox justify alignment');
237
+ this.enlargeSpaces();
238
+ this.canvas?.requestRenderAll();
239
+ }
240
+ }, 0);
241
+ }
242
+ }
243
+ // Calculate height - use Fabric's calculation for proper text rendering space
244
+ if ((this as any)._usingBrowserWrapping && this._textLines && this._textLines.length > 0) {
245
+ const actualBrowserHeight = (this as any)._actualBrowserHeight;
246
+ const oldHeight = this.height;
247
+ // Use Fabric's height calculation since it knows how much space text rendering needs
248
+ this.height = this.calcTextHeight();
249
+
250
+ // Force canvas refresh and control update if height changed significantly
251
+ if (Math.abs(this.height - oldHeight) > 1) {
252
+ this.setCoords();
253
+ this.canvas?.requestRenderAll();
254
+
255
+ // DEBUG: Log exact positioning details
256
+ console.log(`🎯 POSITIONING DEBUG:`);
257
+ console.log(` Textbox height: ${this.height}px`);
258
+ console.log(` Textbox top: ${this.top}px`);
259
+ console.log(` Textbox left: ${this.left}px`);
260
+ console.log(` Text lines: ${this._textLines?.length || 0}`);
261
+ console.log(` Font size: ${this.fontSize}px`);
262
+ console.log(` Line height: ${this.lineHeight || 1.16}`);
263
+ console.log(` Calculated line height: ${this.fontSize * (this.lineHeight || 1.16)}px`);
264
+ console.log(` _getTopOffset(): ${this._getTopOffset()}px`);
265
+ console.log(` calcTextHeight(): ${this.calcTextHeight()}px`);
266
+ console.log(` Browser height: ${actualBrowserHeight}px`);
267
+ console.log(` Height difference: ${this.height - this.calcTextHeight()}px`);
268
+ }
269
+ } else {
270
+ this.height = this.calcTextHeight();
152
271
  }
153
- // clear cache and re-calculate height
154
- this.height = this.calcTextHeight();
272
+ }
273
+
274
+ /**
275
+ * Schedule justify calculation after font loads (Textbox-specific)
276
+ * @private
277
+ */
278
+ _scheduleJustifyAfterFontLoad(): void {
279
+ if (typeof document === 'undefined' || !('fonts' in document)) {
280
+ return;
281
+ }
282
+
283
+ // Only schedule if not already waiting
284
+ if ((this as any)._fontJustifyScheduled) {
285
+ return;
286
+ }
287
+ (this as any)._fontJustifyScheduled = true;
288
+
289
+ const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
290
+ document.fonts.load(fontSpec).then(() => {
291
+ (this as any)._fontJustifyScheduled = false;
292
+ console.log('🔧 Textbox: Font loaded, applying justify alignment');
293
+
294
+ // Re-run initDimensions to ensure proper justify calculation
295
+ this.initDimensions();
296
+ this.canvas?.requestRenderAll();
297
+ }).catch(() => {
298
+ (this as any)._fontJustifyScheduled = false;
299
+ console.warn('⚠️ Textbox: Font loading failed, justify may be incorrect');
300
+ });
155
301
  }
156
302
 
157
303
  /**
@@ -546,20 +692,35 @@ export class Textbox<
546
692
  const { word, width: wordWidth } = data[i];
547
693
  offset += word.length;
548
694
 
549
- lineWidth += infixWidth + wordWidth - additionalSpace;
550
- if (lineWidth > maxWidth && !lineJustStarted) {
695
+ // Predictive wrapping: check if adding this word would exceed the width
696
+ const potentialLineWidth = lineWidth + infixWidth + wordWidth - additionalSpace;
697
+ // Use exact width to match overlay editor behavior
698
+ const conservativeMaxWidth = maxWidth; // No artificial buffer
699
+
700
+ // Debug logging for wrapping decisions
701
+ const currentLineText = line.join('');
702
+ console.log(`🔧 FABRIC WRAP CHECK: "${data[i].word}" -> potential: ${potentialLineWidth.toFixed(1)}px vs limit: ${conservativeMaxWidth.toFixed(1)}px`);
703
+
704
+ if (potentialLineWidth > conservativeMaxWidth && !lineJustStarted) {
705
+ // This word would exceed the width, wrap before adding it
706
+ console.log(`🔧 FABRIC WRAP! Line: "${currentLineText}" (${lineWidth.toFixed(1)}px)`);
551
707
  graphemeLines.push(line);
552
708
  line = [];
553
- lineWidth = wordWidth;
709
+ lineWidth = wordWidth; // Start new line with just this word
554
710
  lineJustStarted = true;
555
711
  } else {
556
- lineWidth += additionalSpace;
712
+ // Word fits, add it to current line
713
+ lineWidth = potentialLineWidth + additionalSpace;
557
714
  }
558
715
 
559
716
  if (!lineJustStarted && !splitByGrapheme) {
560
717
  line.push(infix);
561
718
  }
562
719
  line = line.concat(word);
720
+
721
+ // Debug: show current line after adding word
722
+ console.log(`🔧 FABRIC AFTER ADD: Line now: "${line.join('')}" (${line.length} chars)`);
723
+
563
724
 
564
725
  infixWidth = splitByGrapheme
565
726
  ? 0
@@ -573,9 +734,20 @@ export class Textbox<
573
734
  // TODO: this code is probably not necessary anymore.
574
735
  // it can be moved out of this function since largestWordWidth is now
575
736
  // known in advance
576
- if (largestWordWidth + reservedSpace > this.dynamicMinWidth) {
737
+ // Don't modify dynamicMinWidth when using browser wrapping to prevent width increases
738
+ if (!((this as any)._usingBrowserWrapping) && largestWordWidth + reservedSpace > this.dynamicMinWidth) {
739
+ console.log(`🔧 FABRIC updating dynamicMinWidth: ${this.dynamicMinWidth} -> ${largestWordWidth - additionalSpace + reservedSpace}`);
577
740
  this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace;
741
+ } else if ((this as any)._usingBrowserWrapping) {
742
+ console.log(`🔤 BROWSER WRAP: Skipping dynamicMinWidth update to prevent width increase`);
578
743
  }
744
+
745
+ // Debug: show final wrapped lines
746
+ console.log(`🔧 FABRIC FINAL LINES: ${graphemeLines.length} lines`);
747
+ graphemeLines.forEach((line, i) => {
748
+ console.log(` Line ${i + 1}: "${line.join('')}" (${line.length} chars)`);
749
+ });
750
+
579
751
  return graphemeLines;
580
752
  }
581
753
 
@@ -619,6 +791,284 @@ export class Textbox<
619
791
  * @override
620
792
  */
621
793
  _splitTextIntoLines(text: string) {
794
+ // Check if we need browser wrapping using smart font detection
795
+ const needsBrowserWrapping = this.fontFamily && fontLacksEnglishGlyphsCached(this.fontFamily);
796
+
797
+ if (needsBrowserWrapping) {
798
+ // Cache key based on text content, width, font properties, AND text alignment
799
+ const textHash = text.length + text.slice(0, 50); // Include text content in cache key
800
+ const cacheKey = `${textHash}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
801
+
802
+ // Check if we have a cached result and nothing has changed
803
+ if ((this as any)._browserWrapCache && (this as any)._browserWrapCache.key === cacheKey) {
804
+ const cachedResult = (this as any)._browserWrapCache.result;
805
+
806
+ // For justify alignment, ensure we have the measurements
807
+ if (this.textAlign.includes('justify') && !(cachedResult as any).justifySpaceMeasurements) {
808
+ // Fall through to recalculate
809
+ } else {
810
+ return cachedResult;
811
+ }
812
+ }
813
+
814
+ const result = this._splitTextIntoLinesWithBrowser(text);
815
+
816
+ // Cache the result
817
+ (this as any)._browserWrapCache = { key: cacheKey, result };
818
+
819
+ // Mark that we used browser wrapping to prevent dynamicMinWidth modifications
820
+ (this as any)._usingBrowserWrapping = true;
821
+
822
+ return result;
823
+ }
824
+
825
+ // Clear the browser wrapping flag when using regular wrapping
826
+ (this as any)._usingBrowserWrapping = false;
827
+
828
+ // Default Fabric wrapping for other fonts
829
+ const newText = super._splitTextIntoLines(text),
830
+ graphemeLines = this._wrapText(newText.lines, this.width),
831
+ lines = new Array(graphemeLines.length);
832
+ for (let i = 0; i < graphemeLines.length; i++) {
833
+ lines[i] = graphemeLines[i].join('');
834
+ }
835
+ newText.lines = lines;
836
+ newText.graphemeLines = graphemeLines;
837
+ return newText;
838
+ }
839
+
840
+ /**
841
+ * Use browser's native text wrapping for accurate handling of fonts without English glyphs
842
+ * @private
843
+ */
844
+ _splitTextIntoLinesWithBrowser(text: string) {
845
+ if (typeof document === 'undefined') {
846
+ // Fallback to regular wrapping in Node.js
847
+ return this._splitTextIntoLinesDefault(text);
848
+ }
849
+
850
+ // Create a hidden element that mimics the overlay editor
851
+ const testElement = document.createElement('div');
852
+ testElement.style.position = 'absolute';
853
+ testElement.style.left = '-9999px';
854
+ testElement.style.visibility = 'hidden';
855
+ testElement.style.fontSize = `${this.fontSize}px`;
856
+ testElement.style.fontFamily = `"${this.fontFamily}"`;
857
+ testElement.style.fontWeight = String(this.fontWeight || 'normal');
858
+ testElement.style.fontStyle = String(this.fontStyle || 'normal');
859
+ testElement.style.lineHeight = String(this.lineHeight || 1.16);
860
+
861
+ testElement.style.width = `${this.width}px`;
862
+
863
+ testElement.style.direction = this.direction || 'ltr';
864
+ testElement.style.whiteSpace = 'pre-wrap';
865
+ testElement.style.wordBreak = 'normal';
866
+ testElement.style.overflowWrap = 'break-word';
867
+
868
+ // Set browser-native text alignment (including justify)
869
+ if (this.textAlign.includes('justify')) {
870
+ testElement.style.textAlign = 'justify';
871
+ testElement.style.textAlignLast = 'auto'; // Let browser decide last line alignment
872
+ } else {
873
+ testElement.style.textAlign = this.textAlign;
874
+ }
875
+
876
+ testElement.textContent = text;
877
+
878
+ document.body.appendChild(testElement);
879
+
880
+ // Get the browser's natural line breaks
881
+ const range = document.createRange();
882
+ const lines: string[] = [];
883
+ const graphemeLines: string[][] = [];
884
+
885
+ try {
886
+ // Simple approach: split by measuring character positions
887
+ const textNode = testElement.firstChild;
888
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
889
+ let currentLineStart = 0;
890
+ const textLength = text.length;
891
+ let previousBottom = 0;
892
+
893
+ for (let i = 0; i <= textLength; i++) {
894
+ range.setStart(textNode, currentLineStart);
895
+ range.setEnd(textNode, i);
896
+ const rect = range.getBoundingClientRect();
897
+
898
+ if (i > currentLineStart && (rect.bottom > previousBottom + 5 || i === textLength)) {
899
+ // New line detected or end of text
900
+ const lineEnd = i === textLength ? i : i - 1;
901
+ const lineText = text.substring(currentLineStart, lineEnd).trim();
902
+ if (lineText) {
903
+ lines.push(lineText);
904
+ // Convert to graphemes for compatibility
905
+ const graphemeLine = lineText.split('');
906
+ graphemeLines.push(graphemeLine);
907
+ }
908
+ currentLineStart = lineEnd;
909
+ previousBottom = rect.bottom;
910
+ }
911
+ }
912
+ }
913
+ } catch (error) {
914
+ console.warn('Browser wrapping failed, using fallback:', error);
915
+ document.body.removeChild(testElement);
916
+ return this._splitTextIntoLinesDefault(text);
917
+ }
918
+
919
+ // Extract actual browser height BEFORE removing element
920
+ const actualBrowserHeight = testElement.scrollHeight;
921
+ const offsetHeight = testElement.offsetHeight;
922
+ const clientHeight = testElement.clientHeight;
923
+ const boundingRect = testElement.getBoundingClientRect();
924
+
925
+ console.log(`🔤 Browser element measurements:`);
926
+ console.log(` scrollHeight: ${actualBrowserHeight}px (content + padding + hidden overflow)`);
927
+ console.log(` offsetHeight: ${offsetHeight}px (content + padding + border)`);
928
+ console.log(` clientHeight: ${clientHeight}px (content + padding, no border/scrollbar)`);
929
+ console.log(` boundingRect.height: ${boundingRect.height}px (actual rendered height)`);
930
+ console.log(` Font size: ${this.fontSize}px, Line height: ${this.lineHeight || 1.16}, Lines: ${lines.length}`);
931
+
932
+ // For justify alignment, extract space measurements from browser BEFORE removing element
933
+ let justifySpaceMeasurements = null;
934
+ if (this.textAlign.includes('justify')) {
935
+ justifySpaceMeasurements = this._extractJustifySpaceMeasurements(testElement, lines);
936
+ }
937
+
938
+ document.body.removeChild(testElement);
939
+
940
+ console.log(`🔤 Browser wrapping result: ${lines.length} lines`);
941
+
942
+ // Try different height measurements to find the most accurate
943
+ let bestHeight = actualBrowserHeight;
944
+
945
+ // If scrollHeight and offsetHeight differ significantly, investigate
946
+ if (Math.abs(actualBrowserHeight - offsetHeight) > 2) {
947
+ console.log(`🔤 Height discrepancy detected: scrollHeight=${actualBrowserHeight}px vs offsetHeight=${offsetHeight}px`);
948
+ }
949
+
950
+ // Consider using boundingRect height if it's larger (sometimes more accurate for visible content)
951
+ if (boundingRect.height > bestHeight) {
952
+ console.log(`🔤 Using boundingRect height (${boundingRect.height}px) instead of scrollHeight (${bestHeight}px)`);
953
+ bestHeight = boundingRect.height;
954
+ }
955
+
956
+ // Font-specific height adjustments for accurate bounding box
957
+ let adjustedHeight = bestHeight;
958
+
959
+ // Fonts without English glyphs need additional height buffer due to different font metrics
960
+ const lacksEnglishGlyphs = fontLacksEnglishGlyphsCached(this.fontFamily);
961
+ if (lacksEnglishGlyphs) {
962
+ const glyphBuffer = this.fontSize * 0.25; // 25% of font size for non-English fonts
963
+ adjustedHeight = bestHeight + glyphBuffer;
964
+ console.log(`🔤 Non-English font detected (${this.fontFamily}): Adding ${glyphBuffer}px buffer (${bestHeight}px + ${glyphBuffer}px = ${adjustedHeight}px)`);
965
+ } else {
966
+ console.log(`🔤 Standard font (${this.fontFamily}): Using browser height directly (${bestHeight}px)`);
967
+ }
968
+
969
+ return {
970
+ _unwrappedLines: [text.split('')],
971
+ lines: lines,
972
+ graphemeText: text.split(''),
973
+ graphemeLines: graphemeLines,
974
+ justifySpaceMeasurements: justifySpaceMeasurements,
975
+ actualBrowserHeight: adjustedHeight,
976
+ };
977
+ }
978
+
979
+
980
+
981
+
982
+ /**
983
+ * Extract justify space measurements from browser
984
+ * @private
985
+ */
986
+ _extractJustifySpaceMeasurements(element: HTMLElement, lines: string[]) {
987
+ console.log(`🔤 Extracting browser justify space measurements for ${lines.length} lines`);
988
+
989
+ // For now, we'll use a simplified approach:
990
+ // Apply uniform space expansion to match the line width
991
+ const spaceWidths: number[][] = [];
992
+
993
+ lines.forEach((line, lineIndex) => {
994
+ const lineSpaces: number[] = [];
995
+ const spaceCount = (line.match(/\s/g) || []).length;
996
+
997
+ if (spaceCount > 0 && lineIndex < lines.length - 1) { // Don't justify last line
998
+ // Calculate how much space expansion is needed
999
+ const normalSpaceWidth = 6.4; // Default space width for STV font
1000
+ const lineWidth = this.width;
1001
+
1002
+ // Estimate natural line width
1003
+ const charCount = line.length - spaceCount;
1004
+ const avgCharWidth = 12; // Approximate for STV font
1005
+ const naturalWidth = charCount * avgCharWidth + spaceCount * normalSpaceWidth;
1006
+
1007
+ // Calculate expanded space width
1008
+ const remainingSpace = lineWidth - (charCount * avgCharWidth);
1009
+ const expandedSpaceWidth = remainingSpace / spaceCount;
1010
+
1011
+ console.log(`🔤 Line ${lineIndex}: ${spaceCount} spaces, natural: ${normalSpaceWidth}px -> justified: ${expandedSpaceWidth.toFixed(1)}px`);
1012
+
1013
+ // Fill array with expanded space widths for this line
1014
+ for (let i = 0; i < spaceCount; i++) {
1015
+ lineSpaces.push(expandedSpaceWidth);
1016
+ }
1017
+ }
1018
+
1019
+ spaceWidths.push(lineSpaces);
1020
+ });
1021
+
1022
+ return spaceWidths;
1023
+ }
1024
+
1025
+ /**
1026
+ * Apply browser-calculated justify space measurements
1027
+ * @private
1028
+ */
1029
+ _applyBrowserJustifySpaces() {
1030
+ if (!this._textLines || !this.__charBounds) {
1031
+ console.warn('🔤 BROWSER JUSTIFY: _textLines or __charBounds not ready');
1032
+ return;
1033
+ }
1034
+
1035
+ // Get space measurements from browser wrapping result
1036
+ const styleMap = this._styleMap as any;
1037
+ if (!styleMap || !styleMap.justifySpaceMeasurements) {
1038
+ console.warn('🔤 BROWSER JUSTIFY: No justify space measurements available');
1039
+ return;
1040
+ }
1041
+
1042
+ const spaceWidths = styleMap.justifySpaceMeasurements as number[][];
1043
+ console.log('🔤 BROWSER JUSTIFY: Applying space measurements to __charBounds');
1044
+
1045
+ // Apply space widths to character bounds
1046
+ this._textLines.forEach((line, lineIndex) => {
1047
+ if (!this.__charBounds || !this.__charBounds[lineIndex] || !spaceWidths[lineIndex]) return;
1048
+
1049
+ const lineBounds = this.__charBounds[lineIndex];
1050
+ const lineSpaceWidths = spaceWidths[lineIndex];
1051
+ let spaceIndex = 0;
1052
+
1053
+ for (let charIndex = 0; charIndex < line.length; charIndex++) {
1054
+ if (/\s/.test(line[charIndex]) && spaceIndex < lineSpaceWidths.length) {
1055
+ const expandedWidth = lineSpaceWidths[spaceIndex];
1056
+ if (lineBounds[charIndex]) {
1057
+ const oldWidth = lineBounds[charIndex].width;
1058
+ lineBounds[charIndex].width = expandedWidth;
1059
+ console.log(`🔤 Line ${lineIndex} space ${spaceIndex}: ${oldWidth.toFixed(1)}px -> ${expandedWidth.toFixed(1)}px`);
1060
+ }
1061
+ spaceIndex++;
1062
+ }
1063
+ }
1064
+ });
1065
+ }
1066
+
1067
+ /**
1068
+ * Fallback to default Fabric wrapping
1069
+ * @private
1070
+ */
1071
+ _splitTextIntoLinesDefault(text: string) {
622
1072
  const newText = super._splitTextIntoLines(text),
623
1073
  graphemeLines = this._wrapText(newText.lines, this.width),
624
1074
  lines = new Array(graphemeLines.length);
@@ -768,6 +1218,66 @@ export class Textbox<
768
1218
  }
769
1219
  }
770
1220
 
1221
+ /**
1222
+ * Force complete textbox re-initialization (useful after JSON loading)
1223
+ * Overrides Text version with Textbox-specific logic
1224
+ */
1225
+ forceTextReinitialization(): void {
1226
+ console.log('🔄 Force reinitializing Textbox object');
1227
+
1228
+ // CRITICAL: Ensure textbox is marked as initialized
1229
+ this.initialized = true;
1230
+
1231
+ // Clear all caches and force dirty state
1232
+ this._clearCache();
1233
+ this.dirty = true;
1234
+ this.dynamicMinWidth = 0;
1235
+
1236
+ // Force isEditing false to ensure clean state
1237
+ this.isEditing = false;
1238
+
1239
+ console.log(' → Set initialized=true, dirty=true, cleared caches');
1240
+
1241
+ // Re-initialize dimensions (this will handle justify properly)
1242
+ this.initDimensions();
1243
+
1244
+ // Double-check that justify was applied by checking space widths
1245
+ if (this.textAlign.includes('justify') && this.__charBounds) {
1246
+ setTimeout(() => {
1247
+ // Verify justify was applied by checking if space widths vary
1248
+ let hasVariableSpaces = false;
1249
+ this.__charBounds.forEach((lineBounds, i) => {
1250
+ if (lineBounds && this._textLines && this._textLines[i]) {
1251
+ const spaces = lineBounds.filter((bound, j) => /\s/.test(this._textLines[i][j]));
1252
+ if (spaces.length > 1) {
1253
+ const firstSpaceWidth = spaces[0].width;
1254
+ hasVariableSpaces = spaces.some(space => Math.abs(space.width - firstSpaceWidth) > 0.1);
1255
+ }
1256
+ }
1257
+ });
1258
+
1259
+ if (!hasVariableSpaces && this.__charBounds.length > 0) {
1260
+ console.warn(' ⚠️ Justify spaces still uniform - forcing enlargeSpaces again');
1261
+ if (this.enlargeSpaces) {
1262
+ this.enlargeSpaces();
1263
+ }
1264
+ } else {
1265
+ console.log(' ✅ Justify spaces properly expanded');
1266
+ }
1267
+
1268
+ // Ensure height is recalculated - use browser height if available
1269
+ if ((this as any)._usingBrowserWrapping && (this as any)._actualBrowserHeight) {
1270
+ this.height = (this as any)._actualBrowserHeight;
1271
+ console.log(`🔤 JUSTIFY: Preserved browser height: ${this.height}px`);
1272
+ } else {
1273
+ this.height = this.calcTextHeight();
1274
+ console.log(`🔧 JUSTIFY: Used calcTextHeight: ${this.height}px`);
1275
+ }
1276
+ this.canvas?.requestRenderAll();
1277
+ }, 10);
1278
+ }
1279
+ }
1280
+
771
1281
  /**
772
1282
  * Returns object representation of an instance
773
1283
  * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output