@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
@@ -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
@@ -65,8 +66,27 @@ class Textbox extends IText {
65
66
  */
66
67
  initDimensions() {
67
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) {
68
87
  return;
69
88
  }
89
+ this._lastDimensionState = currentState;
70
90
 
71
91
  // Use advanced layout if enabled
72
92
  if (this.enableAdvancedLayout) {
@@ -77,17 +97,142 @@ class Textbox extends IText {
77
97
  // clear dynamicMinWidth as it will be different after we re-wrap line
78
98
  this.dynamicMinWidth = 0;
79
99
  // wrap lines
80
- this._styleMap = this._generateStyleMap(this._splitText());
81
- // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap
82
- 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) {
83
119
  this._set('width', this.dynamicMinWidth);
84
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
+ }
85
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
86
152
  // once text is measured we need to make space fatter to make justified text.
87
- 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();
206
+ }
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;
88
216
  }
89
- // clear cache and re-calculate height
90
- this.height = this.calcTextHeight();
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
+ });
91
236
  }
92
237
 
93
238
  /**
@@ -454,19 +599,33 @@ class Textbox extends IText {
454
599
  width: wordWidth
455
600
  } = data[i];
456
601
  offset += word.length;
457
- lineWidth += infixWidth + wordWidth - additionalSpace;
458
- 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)`);
459
614
  graphemeLines.push(line);
460
615
  line = [];
461
- lineWidth = wordWidth;
616
+ lineWidth = wordWidth; // Start new line with just this word
462
617
  lineJustStarted = true;
463
618
  } else {
464
- lineWidth += additionalSpace;
619
+ // Word fits, add it to current line
620
+ lineWidth = potentialLineWidth + additionalSpace;
465
621
  }
466
622
  if (!lineJustStarted && !splitByGrapheme) {
467
623
  line.push(infix);
468
624
  }
469
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)`);
470
629
  infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset);
471
630
  offset++;
472
631
  lineJustStarted = false;
@@ -476,9 +635,19 @@ class Textbox extends IText {
476
635
  // TODO: this code is probably not necessary anymore.
477
636
  // it can be moved out of this function since largestWordWidth is now
478
637
  // known in advance
479
- 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}`);
480
641
  this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace;
642
+ } else if (this._usingBrowserWrapping) {
643
+ console.log(`🔤 BROWSER WRAP: Skipping dynamicMinWidth update to prevent width increase`);
481
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
+ });
482
651
  return graphemeLines;
483
652
  }
484
653
 
@@ -522,6 +691,260 @@ class Textbox extends IText {
522
691
  * @override
523
692
  */
524
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) {
525
948
  const newText = super._splitTextIntoLines(text),
526
949
  graphemeLines = this._wrapText(newText.lines, this.width),
527
950
  lines = new Array(graphemeLines.length);
@@ -556,7 +979,7 @@ class Textbox extends IText {
556
979
  * @private
557
980
  */
558
981
  initializeEventListeners() {
559
- var _this$canvas;
982
+ var _this$canvas4;
560
983
  // Track which side is being used for resize to handle position compensation
561
984
  let resizeOrigin = null;
562
985
 
@@ -587,7 +1010,7 @@ class Textbox extends IText {
587
1010
  });
588
1011
 
589
1012
  // Also listen to canvas-level modified event as backup
590
- (_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.on('object:modified', e => {
1013
+ (_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.on('object:modified', e => {
591
1014
  if (e.target === this) {
592
1015
  const currentResizeOrigin = resizeOrigin; // Capture the value before reset
593
1016
  setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
@@ -631,7 +1054,7 @@ class Textbox extends IText {
631
1054
  const safetyThreshold = 2; // px - very subtle trigger
632
1055
 
633
1056
  if (maxRequiredWidth > this.width - safetyThreshold) {
634
- var _this$canvas2;
1057
+ var _this$canvas5;
635
1058
  // Set width to exactly what's needed + minimal safety margin
636
1059
  const newWidth = maxRequiredWidth + 1; // Add just 1px safety margin
637
1060
 
@@ -664,7 +1087,66 @@ class Textbox extends IText {
664
1087
  this.__overlayEditor.refresh();
665
1088
  }, 0);
666
1089
  }
667
- (_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
1090
+ (_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
1091
+ }
1092
+ }
1093
+
1094
+ /**
1095
+ * Force complete textbox re-initialization (useful after JSON loading)
1096
+ * Overrides Text version with Textbox-specific logic
1097
+ */
1098
+ forceTextReinitialization() {
1099
+ console.log('🔄 Force reinitializing Textbox object');
1100
+
1101
+ // CRITICAL: Ensure textbox is marked as initialized
1102
+ this.initialized = true;
1103
+
1104
+ // Clear all caches and force dirty state
1105
+ this._clearCache();
1106
+ this.dirty = true;
1107
+ this.dynamicMinWidth = 0;
1108
+
1109
+ // Force isEditing false to ensure clean state
1110
+ this.isEditing = false;
1111
+ console.log(' → Set initialized=true, dirty=true, cleared caches');
1112
+
1113
+ // Re-initialize dimensions (this will handle justify properly)
1114
+ this.initDimensions();
1115
+
1116
+ // Double-check that justify was applied by checking space widths
1117
+ if (this.textAlign.includes('justify') && this.__charBounds) {
1118
+ setTimeout(() => {
1119
+ var _this$canvas6;
1120
+ // Verify justify was applied by checking if space widths vary
1121
+ let hasVariableSpaces = false;
1122
+ this.__charBounds.forEach((lineBounds, i) => {
1123
+ if (lineBounds && this._textLines && this._textLines[i]) {
1124
+ const spaces = lineBounds.filter((bound, j) => /\s/.test(this._textLines[i][j]));
1125
+ if (spaces.length > 1) {
1126
+ const firstSpaceWidth = spaces[0].width;
1127
+ hasVariableSpaces = spaces.some(space => Math.abs(space.width - firstSpaceWidth) > 0.1);
1128
+ }
1129
+ }
1130
+ });
1131
+ if (!hasVariableSpaces && this.__charBounds.length > 0) {
1132
+ console.warn(' ⚠️ Justify spaces still uniform - forcing enlargeSpaces again');
1133
+ if (this.enlargeSpaces) {
1134
+ this.enlargeSpaces();
1135
+ }
1136
+ } else {
1137
+ console.log(' ✅ Justify spaces properly expanded');
1138
+ }
1139
+
1140
+ // Ensure height is recalculated - use browser height if available
1141
+ if (this._usingBrowserWrapping && this._actualBrowserHeight) {
1142
+ this.height = this._actualBrowserHeight;
1143
+ console.log(`🔤 JUSTIFY: Preserved browser height: ${this.height}px`);
1144
+ } else {
1145
+ this.height = this.calcTextHeight();
1146
+ console.log(`🔧 JUSTIFY: Used calcTextHeight: ${this.height}px`);
1147
+ }
1148
+ (_this$canvas6 = this.canvas) === null || _this$canvas6 === void 0 || _this$canvas6.requestRenderAll();
1149
+ }, 10);
668
1150
  }
669
1151
  }
670
1152