@nasser-sw/fabric 7.0.1-beta16 → 7.0.1-beta17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +7 -0
- package/dist/index.js +1982 -649
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/index.min.mjs +1 -1
- package/dist/index.min.mjs.map +1 -1
- package/dist/index.mjs +1982 -649
- package/dist/index.mjs.map +1 -1
- package/dist/index.node.cjs +1982 -649
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.mjs +1982 -649
- package/dist/index.node.mjs.map +1 -1
- package/dist/package.json.min.mjs +1 -1
- package/dist/package.json.mjs +1 -1
- package/dist/src/shapes/IText/IText.d.ts +31 -6
- package/dist/src/shapes/IText/IText.d.ts.map +1 -1
- package/dist/src/shapes/IText/IText.min.mjs +1 -1
- package/dist/src/shapes/IText/IText.min.mjs.map +1 -1
- package/dist/src/shapes/IText/IText.mjs +495 -126
- package/dist/src/shapes/IText/IText.mjs.map +1 -1
- package/dist/src/shapes/IText/ITextBehavior.d.ts +12 -0
- package/dist/src/shapes/IText/ITextBehavior.d.ts.map +1 -1
- package/dist/src/shapes/IText/ITextBehavior.min.mjs +1 -1
- package/dist/src/shapes/IText/ITextBehavior.min.mjs.map +1 -1
- package/dist/src/shapes/IText/ITextBehavior.mjs +127 -36
- package/dist/src/shapes/IText/ITextBehavior.mjs.map +1 -1
- package/dist/src/shapes/IText/ITextClickBehavior.d.ts.map +1 -1
- package/dist/src/shapes/IText/ITextClickBehavior.min.mjs +1 -1
- package/dist/src/shapes/IText/ITextClickBehavior.min.mjs.map +1 -1
- package/dist/src/shapes/IText/ITextClickBehavior.mjs +21 -4
- package/dist/src/shapes/IText/ITextClickBehavior.mjs.map +1 -1
- package/dist/src/shapes/IText/ITextKeyBehavior.min.mjs +1 -1
- package/dist/src/shapes/IText/ITextKeyBehavior.min.mjs.map +1 -1
- package/dist/src/shapes/IText/ITextKeyBehavior.mjs +17 -21
- package/dist/src/shapes/IText/ITextKeyBehavior.mjs.map +1 -1
- package/dist/src/shapes/Text/Text.d.ts +69 -1
- package/dist/src/shapes/Text/Text.d.ts.map +1 -1
- package/dist/src/shapes/Text/Text.min.mjs +1 -1
- package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
- package/dist/src/shapes/Text/Text.mjs +374 -60
- package/dist/src/shapes/Text/Text.mjs.map +1 -1
- package/dist/src/shapes/Text/constants.d.ts.map +1 -1
- package/dist/src/shapes/Text/constants.min.mjs +1 -1
- package/dist/src/shapes/Text/constants.min.mjs.map +1 -1
- package/dist/src/shapes/Text/constants.mjs +2 -1
- package/dist/src/shapes/Text/constants.mjs.map +1 -1
- package/dist/src/shapes/Textbox.d.ts +8 -1
- package/dist/src/shapes/Textbox.d.ts.map +1 -1
- package/dist/src/shapes/Textbox.min.mjs +1 -1
- package/dist/src/shapes/Textbox.min.mjs.map +1 -1
- package/dist/src/shapes/Textbox.mjs +406 -63
- package/dist/src/shapes/Textbox.mjs.map +1 -1
- package/dist/src/text/hitTest.min.mjs +1 -1
- package/dist/src/text/hitTest.min.mjs.map +1 -1
- package/dist/src/text/hitTest.mjs +1 -198
- package/dist/src/text/hitTest.mjs.map +1 -1
- package/dist/src/text/layout.min.mjs +1 -1
- package/dist/src/text/layout.min.mjs.map +1 -1
- package/dist/src/text/layout.mjs +122 -5
- package/dist/src/text/layout.mjs.map +1 -1
- package/dist/src/text/overlayEditor.min.mjs +1 -1
- package/dist/src/text/overlayEditor.min.mjs.map +1 -1
- package/dist/src/text/overlayEditor.mjs +132 -142
- package/dist/src/text/overlayEditor.mjs.map +1 -1
- package/dist/src/text/unicode.d.ts +28 -0
- package/dist/src/text/unicode.d.ts.map +1 -1
- package/dist/src/text/unicode.min.mjs +1 -1
- package/dist/src/text/unicode.min.mjs.map +1 -1
- package/dist/src/text/unicode.mjs +294 -1
- package/dist/src/text/unicode.mjs.map +1 -1
- package/dist-extensions/src/shapes/IText/IText.d.ts +31 -6
- package/dist-extensions/src/shapes/IText/IText.d.ts.map +1 -1
- package/dist-extensions/src/shapes/IText/ITextBehavior.d.ts +12 -0
- package/dist-extensions/src/shapes/IText/ITextBehavior.d.ts.map +1 -1
- package/dist-extensions/src/shapes/IText/ITextClickBehavior.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Text/Text.d.ts +69 -1
- package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Text/constants.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Textbox.d.ts +8 -1
- package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
- package/dist-extensions/src/text/unicode.d.ts +28 -0
- package/dist-extensions/src/text/unicode.d.ts.map +1 -1
- package/package.json +164 -164
- package/rtl-debug.html +358 -200
- package/src/shapes/IText/IText.ts +524 -110
- package/src/shapes/IText/ITextBehavior.ts +174 -80
- package/src/shapes/IText/ITextClickBehavior.ts +20 -6
- package/src/shapes/IText/ITextKeyBehavior.ts +15 -15
- package/src/shapes/Text/Text.ts +488 -107
- package/src/shapes/Text/constants.ts +4 -2
- package/src/shapes/Textbox.ts +414 -65
- package/src/text/layout.ts +150 -23
- package/src/text/overlayEditor.ts +148 -148
- package/src/text/unicode.ts +177 -2
package/dist/index.mjs
CHANGED
|
@@ -354,7 +354,7 @@ class Cache {
|
|
|
354
354
|
}
|
|
355
355
|
const cache = new Cache();
|
|
356
356
|
|
|
357
|
-
var version = "7.0.1-
|
|
357
|
+
var version = "7.0.1-beta16";
|
|
358
358
|
|
|
359
359
|
// use this syntax so babel plugin see this import here
|
|
360
360
|
const VERSION = version;
|
|
@@ -4800,7 +4800,7 @@ function getSvgRegex(arr) {
|
|
|
4800
4800
|
const TEXT_DECORATION_THICKNESS = 'textDecorationThickness';
|
|
4801
4801
|
const fontProperties = ['fontSize', 'fontWeight', 'fontFamily', 'fontStyle'];
|
|
4802
4802
|
const textDecorationProperties = ['underline', 'overline', 'linethrough'];
|
|
4803
|
-
const textLayoutProperties = [...fontProperties, 'lineHeight', 'text', 'charSpacing', 'textAlign', 'styles', 'path', 'pathStartOffset', 'pathSide', 'pathAlign', 'wrap', 'ellipsis', 'letterSpacing', 'enableAdvancedLayout', 'verticalAlign'];
|
|
4803
|
+
const textLayoutProperties = [...fontProperties, 'lineHeight', 'text', 'charSpacing', 'textAlign', 'styles', 'path', 'pathStartOffset', 'pathSide', 'pathAlign', 'wrap', 'ellipsis', 'letterSpacing', 'enableAdvancedLayout', 'verticalAlign', 'kashida'];
|
|
4804
4804
|
const additionalProps = [...textLayoutProperties, ...textDecorationProperties, 'textBackgroundColor', 'direction', TEXT_DECORATION_THICKNESS, 'useOverlayEditing'];
|
|
4805
4805
|
const styleProperties = [...fontProperties, ...textDecorationProperties, STROKE, 'strokeWidth', FILL, 'deltaY', 'textBackgroundColor', TEXT_DECORATION_THICKNESS];
|
|
4806
4806
|
|
|
@@ -4837,6 +4837,7 @@ const textDefaultValues = {
|
|
|
4837
4837
|
letterSpacing: 0,
|
|
4838
4838
|
enableAdvancedLayout: false,
|
|
4839
4839
|
verticalAlign: 'top',
|
|
4840
|
+
kashida: 'none',
|
|
4840
4841
|
// Overlay editor properties
|
|
4841
4842
|
useOverlayEditing: false,
|
|
4842
4843
|
CACHE_FONT_SIZE: 400,
|
|
@@ -19669,6 +19670,14 @@ function fontLacksEnglishGlyphsCached(fontFamily) {
|
|
|
19669
19670
|
* and grapheme cluster boundary detection.
|
|
19670
19671
|
*/
|
|
19671
19672
|
|
|
19673
|
+
// Unicode character categories for text processing
|
|
19674
|
+
const UNICODE_CATEGORIES = {
|
|
19675
|
+
// Bidirectional types
|
|
19676
|
+
L: /[\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]/,
|
|
19677
|
+
R: /[\u05BE\u05C0\u05C3\u05C6\u05D0-\u05EA\u05F0-\u05F4\u0608\u060B\u060D]/,
|
|
19678
|
+
AL: /[\u0627\u0629-\u063A\u0641-\u064A\u066D-\u066F\u0671-\u06D3\u06D5]/,
|
|
19679
|
+
EN: /[\u0030-\u0039\u00B2\u00B3\u00B9\u06F0-\u06F9]/,
|
|
19680
|
+
AN: /[\u0660-\u0669\u066B\u066C]/};
|
|
19672
19681
|
|
|
19673
19682
|
/**
|
|
19674
19683
|
* Enhanced grapheme segmentation using Intl.Segmenter when available
|
|
@@ -19692,6 +19701,291 @@ function segmentGraphemes(text) {
|
|
|
19692
19701
|
return graphemeSplit(text);
|
|
19693
19702
|
}
|
|
19694
19703
|
|
|
19704
|
+
/**
|
|
19705
|
+
* Analyze text for bidirectional runs using Unicode BiDi algorithm (simplified)
|
|
19706
|
+
*/
|
|
19707
|
+
function analyzeBiDi(text) {
|
|
19708
|
+
let baseDirection = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'ltr';
|
|
19709
|
+
if (!text) return [];
|
|
19710
|
+
const runs = [];
|
|
19711
|
+
const chars = Array.from(text);
|
|
19712
|
+
let currentRun = null;
|
|
19713
|
+
for (let i = 0; i < chars.length; i++) {
|
|
19714
|
+
const char = chars[i];
|
|
19715
|
+
const charDirection = getBidiDirection(char, baseDirection);
|
|
19716
|
+
|
|
19717
|
+
// Start new run if direction changes
|
|
19718
|
+
if (!currentRun || currentRun.direction !== charDirection) {
|
|
19719
|
+
if (currentRun) {
|
|
19720
|
+
runs.push(currentRun);
|
|
19721
|
+
}
|
|
19722
|
+
currentRun = {
|
|
19723
|
+
text: char,
|
|
19724
|
+
direction: charDirection,
|
|
19725
|
+
level: charDirection === 'rtl' ? 1 : 0,
|
|
19726
|
+
start: i,
|
|
19727
|
+
end: i + 1
|
|
19728
|
+
};
|
|
19729
|
+
} else {
|
|
19730
|
+
// Continue current run
|
|
19731
|
+
currentRun.text += char;
|
|
19732
|
+
currentRun.end = i + 1;
|
|
19733
|
+
}
|
|
19734
|
+
}
|
|
19735
|
+
|
|
19736
|
+
// Add final run
|
|
19737
|
+
if (currentRun) {
|
|
19738
|
+
runs.push(currentRun);
|
|
19739
|
+
}
|
|
19740
|
+
return runs.length > 0 ? runs : [{
|
|
19741
|
+
text,
|
|
19742
|
+
direction: baseDirection,
|
|
19743
|
+
level: baseDirection === 'rtl' ? 1 : 0,
|
|
19744
|
+
start: 0,
|
|
19745
|
+
end: text.length
|
|
19746
|
+
}];
|
|
19747
|
+
}
|
|
19748
|
+
|
|
19749
|
+
/**
|
|
19750
|
+
* Character classification functions
|
|
19751
|
+
*/
|
|
19752
|
+
function isWhitespace(grapheme) {
|
|
19753
|
+
return /\s/.test(grapheme);
|
|
19754
|
+
}
|
|
19755
|
+
|
|
19756
|
+
/**
|
|
19757
|
+
* Get bidirectional character type
|
|
19758
|
+
*/
|
|
19759
|
+
function getBidiDirection(char, baseDirection) {
|
|
19760
|
+
// Strong RTL characters
|
|
19761
|
+
if (UNICODE_CATEGORIES.R.test(char) || UNICODE_CATEGORIES.AL.test(char)) {
|
|
19762
|
+
return 'rtl';
|
|
19763
|
+
}
|
|
19764
|
+
|
|
19765
|
+
// Strong LTR characters
|
|
19766
|
+
if (UNICODE_CATEGORIES.L.test(char)) {
|
|
19767
|
+
return 'ltr';
|
|
19768
|
+
}
|
|
19769
|
+
|
|
19770
|
+
// Numbers follow base direction in simplified algorithm
|
|
19771
|
+
if (UNICODE_CATEGORIES.EN.test(char) || UNICODE_CATEGORIES.AN.test(char)) {
|
|
19772
|
+
return baseDirection;
|
|
19773
|
+
}
|
|
19774
|
+
|
|
19775
|
+
// Neutral characters follow context
|
|
19776
|
+
return baseDirection;
|
|
19777
|
+
}
|
|
19778
|
+
|
|
19779
|
+
// ============================================================================
|
|
19780
|
+
// Arabic Kashida (Tatweel) Support
|
|
19781
|
+
// ============================================================================
|
|
19782
|
+
|
|
19783
|
+
/**
|
|
19784
|
+
* Arabic Tatweel (kashida) character used for justification
|
|
19785
|
+
*/
|
|
19786
|
+
const ARABIC_TATWEEL = '\u0640';
|
|
19787
|
+
|
|
19788
|
+
/**
|
|
19789
|
+
* Arabic letters that do NOT connect to the following letter (non-connecting on left).
|
|
19790
|
+
* These letters cannot have kashida inserted after them.
|
|
19791
|
+
* ا (Alef), د (Dal), ذ (Thal), ر (Ra), ز (Zay), و (Waw), ة (Teh Marbuta), ء (Hamza)
|
|
19792
|
+
*/
|
|
19793
|
+
const ARABIC_NON_CONNECTING = new Set(['\u0627',
|
|
19794
|
+
// Alef
|
|
19795
|
+
'\u062F',
|
|
19796
|
+
// Dal
|
|
19797
|
+
'\u0630',
|
|
19798
|
+
// Thal
|
|
19799
|
+
'\u0631',
|
|
19800
|
+
// Ra
|
|
19801
|
+
'\u0632',
|
|
19802
|
+
// Zay
|
|
19803
|
+
'\u0648',
|
|
19804
|
+
// Waw
|
|
19805
|
+
'\u0629',
|
|
19806
|
+
// Teh Marbuta
|
|
19807
|
+
'\u0621',
|
|
19808
|
+
// Hamza
|
|
19809
|
+
'\u0622',
|
|
19810
|
+
// Alef with Madda
|
|
19811
|
+
'\u0623',
|
|
19812
|
+
// Alef with Hamza Above
|
|
19813
|
+
'\u0625',
|
|
19814
|
+
// Alef with Hamza Below
|
|
19815
|
+
'\u0672',
|
|
19816
|
+
// Alef with Wavy Hamza Above
|
|
19817
|
+
'\u0673',
|
|
19818
|
+
// Alef with Wavy Hamza Below
|
|
19819
|
+
'\u0675',
|
|
19820
|
+
// High Hamza Alef
|
|
19821
|
+
'\u0688',
|
|
19822
|
+
// Dal with Small Tah
|
|
19823
|
+
'\u0689',
|
|
19824
|
+
// Dal with Ring
|
|
19825
|
+
'\u068A',
|
|
19826
|
+
// Dal with Dot Below
|
|
19827
|
+
'\u068B',
|
|
19828
|
+
// Dal with Dot Below and Small Tah
|
|
19829
|
+
'\u068C',
|
|
19830
|
+
// Dahal
|
|
19831
|
+
'\u068D',
|
|
19832
|
+
// Ddahal
|
|
19833
|
+
'\u068E',
|
|
19834
|
+
// Dul
|
|
19835
|
+
'\u068F',
|
|
19836
|
+
// Dal with Three Dots Above Downwards
|
|
19837
|
+
'\u0690',
|
|
19838
|
+
// Dal with Four Dots Above
|
|
19839
|
+
'\u0691',
|
|
19840
|
+
// Rreh
|
|
19841
|
+
'\u0692',
|
|
19842
|
+
// Reh with Small V
|
|
19843
|
+
'\u0693',
|
|
19844
|
+
// Reh with Ring
|
|
19845
|
+
'\u0694',
|
|
19846
|
+
// Reh with Dot Below
|
|
19847
|
+
'\u0695',
|
|
19848
|
+
// Reh with Small V Below
|
|
19849
|
+
'\u0696',
|
|
19850
|
+
// Reh with Dot Below and Dot Above
|
|
19851
|
+
'\u0697',
|
|
19852
|
+
// Reh with Two Dots Above
|
|
19853
|
+
'\u0698',
|
|
19854
|
+
// Jeh
|
|
19855
|
+
'\u0699',
|
|
19856
|
+
// Reh with Four Dots Above
|
|
19857
|
+
'\u06C4',
|
|
19858
|
+
// Waw with Ring
|
|
19859
|
+
'\u06C5',
|
|
19860
|
+
// Kirghiz Oe
|
|
19861
|
+
'\u06C6',
|
|
19862
|
+
// Oe
|
|
19863
|
+
'\u06C7',
|
|
19864
|
+
// U
|
|
19865
|
+
'\u06C8',
|
|
19866
|
+
// Yu
|
|
19867
|
+
'\u06C9',
|
|
19868
|
+
// Kirghiz Yu
|
|
19869
|
+
'\u06CA',
|
|
19870
|
+
// Waw with Two Dots Above
|
|
19871
|
+
'\u06CB',
|
|
19872
|
+
// Ve
|
|
19873
|
+
'\u06CD',
|
|
19874
|
+
// Yeh with Tail
|
|
19875
|
+
'\u06CF' // Waw with Dot Above
|
|
19876
|
+
]);
|
|
19877
|
+
|
|
19878
|
+
/**
|
|
19879
|
+
* Check if a character is an Arabic letter (main Arabic block + extended)
|
|
19880
|
+
*/
|
|
19881
|
+
function isArabicLetter(char) {
|
|
19882
|
+
if (!char) return false;
|
|
19883
|
+
const code = char.charCodeAt(0);
|
|
19884
|
+
// Arabic: U+0600-U+06FF (main block)
|
|
19885
|
+
// Arabic Supplement: U+0750-U+077F
|
|
19886
|
+
// Arabic Extended-A: U+08A0-U+08FF
|
|
19887
|
+
return code >= 0x0620 && code <= 0x064A ||
|
|
19888
|
+
// Main letters
|
|
19889
|
+
code >= 0x066E && code <= 0x06D3 ||
|
|
19890
|
+
// Extended letters
|
|
19891
|
+
code >= 0x0750 && code <= 0x077F ||
|
|
19892
|
+
// Arabic Supplement
|
|
19893
|
+
code >= 0x08A0 && code <= 0x08FF // Arabic Extended-A
|
|
19894
|
+
;
|
|
19895
|
+
}
|
|
19896
|
+
|
|
19897
|
+
/**
|
|
19898
|
+
* Check if kashida can be inserted between two characters.
|
|
19899
|
+
* Kashida can only be inserted:
|
|
19900
|
+
* - Between two Arabic letters
|
|
19901
|
+
* - After a letter that connects to the next (not in ARABIC_NON_CONNECTING)
|
|
19902
|
+
* - Not at word boundaries (no whitespace before/after)
|
|
19903
|
+
*/
|
|
19904
|
+
// Alef variants that form ligatures with lam
|
|
19905
|
+
const ARABIC_ALEF_VARIANTS = new Set(['\u0627',
|
|
19906
|
+
// ا ALEF
|
|
19907
|
+
'\u0623',
|
|
19908
|
+
// أ ALEF WITH HAMZA ABOVE
|
|
19909
|
+
'\u0625',
|
|
19910
|
+
// إ ALEF WITH HAMZA BELOW
|
|
19911
|
+
'\u0622',
|
|
19912
|
+
// آ ALEF WITH MADDA ABOVE
|
|
19913
|
+
'\u0671' // ٱ ALEF WASLA
|
|
19914
|
+
]);
|
|
19915
|
+
|
|
19916
|
+
// Lam character
|
|
19917
|
+
const ARABIC_LAM = '\u0644'; // ل
|
|
19918
|
+
|
|
19919
|
+
function canInsertKashida(prevChar, nextChar) {
|
|
19920
|
+
if (!prevChar || !nextChar) return false;
|
|
19921
|
+
|
|
19922
|
+
// Can't insert at whitespace boundaries
|
|
19923
|
+
if (/\s/.test(prevChar) || /\s/.test(nextChar)) return false;
|
|
19924
|
+
|
|
19925
|
+
// Both must be Arabic letters
|
|
19926
|
+
if (!isArabicLetter(prevChar) || !isArabicLetter(nextChar)) return false;
|
|
19927
|
+
|
|
19928
|
+
// Previous char must connect to the next (not be non-connecting)
|
|
19929
|
+
if (ARABIC_NON_CONNECTING.has(prevChar)) return false;
|
|
19930
|
+
|
|
19931
|
+
// NEVER insert kashida between lam and alef - they form a ligature (لا)
|
|
19932
|
+
if (prevChar === ARABIC_LAM && ARABIC_ALEF_VARIANTS.has(nextChar)) return false;
|
|
19933
|
+
return true;
|
|
19934
|
+
}
|
|
19935
|
+
|
|
19936
|
+
/**
|
|
19937
|
+
* Represents a valid kashida insertion point
|
|
19938
|
+
*/
|
|
19939
|
+
|
|
19940
|
+
/**
|
|
19941
|
+
* Find all valid kashida insertion points in a line of text.
|
|
19942
|
+
* Returns points sorted by priority (highest first).
|
|
19943
|
+
*
|
|
19944
|
+
* Priority rules (similar to Adobe Illustrator):
|
|
19945
|
+
* 1. Between connected letters (ب + ب = highest)
|
|
19946
|
+
* 2. Prefer middle of words over edges
|
|
19947
|
+
* 3. Avoid inserting right before/after spaces
|
|
19948
|
+
*/
|
|
19949
|
+
function findKashidaPoints(graphemes) {
|
|
19950
|
+
const points = [];
|
|
19951
|
+
for (let i = 0; i < graphemes.length - 1; i++) {
|
|
19952
|
+
const prev = graphemes[i];
|
|
19953
|
+
const next = graphemes[i + 1];
|
|
19954
|
+
if (canInsertKashida(prev, next)) {
|
|
19955
|
+
// Calculate priority based on position in word
|
|
19956
|
+
let priority = 1;
|
|
19957
|
+
|
|
19958
|
+
// Find word boundaries
|
|
19959
|
+
let wordStart = i;
|
|
19960
|
+
let wordEnd = i + 1;
|
|
19961
|
+
while (wordStart > 0 && !isWhitespace(graphemes[wordStart - 1])) {
|
|
19962
|
+
wordStart--;
|
|
19963
|
+
}
|
|
19964
|
+
while (wordEnd < graphemes.length && !isWhitespace(graphemes[wordEnd])) {
|
|
19965
|
+
wordEnd++;
|
|
19966
|
+
}
|
|
19967
|
+
const wordLength = wordEnd - wordStart;
|
|
19968
|
+
const posInWord = i - wordStart;
|
|
19969
|
+
|
|
19970
|
+
// Higher priority for middle positions
|
|
19971
|
+
const distFromEdge = Math.min(posInWord, wordLength - 1 - posInWord);
|
|
19972
|
+
priority = distFromEdge + 1;
|
|
19973
|
+
|
|
19974
|
+
// Boost priority for longer words
|
|
19975
|
+
if (wordLength > 4) priority += 1;
|
|
19976
|
+
if (wordLength > 6) priority += 1;
|
|
19977
|
+
points.push({
|
|
19978
|
+
charIndex: i,
|
|
19979
|
+
priority
|
|
19980
|
+
});
|
|
19981
|
+
}
|
|
19982
|
+
}
|
|
19983
|
+
|
|
19984
|
+
// Sort by priority descending
|
|
19985
|
+
points.sort((a, b) => b.priority - a.priority);
|
|
19986
|
+
return points;
|
|
19987
|
+
}
|
|
19988
|
+
|
|
19695
19989
|
/**
|
|
19696
19990
|
* Ellipsis Text Truncation System
|
|
19697
19991
|
*
|
|
@@ -20050,7 +20344,7 @@ function layoutSingleLine(text, options) {
|
|
|
20050
20344
|
let lineHeight = 0;
|
|
20051
20345
|
let charIndex = textOffset; // Track character position in original text
|
|
20052
20346
|
|
|
20053
|
-
// Measure each grapheme
|
|
20347
|
+
// Measure each grapheme in logical order
|
|
20054
20348
|
for (let i = 0; i < graphemes.length; i++) {
|
|
20055
20349
|
const grapheme = graphemes[i];
|
|
20056
20350
|
const prevGrapheme = i > 0 ? graphemes[i - 1] : undefined;
|
|
@@ -20066,12 +20360,14 @@ function layoutSingleLine(text, options) {
|
|
|
20066
20360
|
bounds.push({
|
|
20067
20361
|
grapheme,
|
|
20068
20362
|
x,
|
|
20363
|
+
// Will be updated by BiDi reordering
|
|
20069
20364
|
y: 0,
|
|
20070
20365
|
// Will be adjusted later
|
|
20071
20366
|
width: measurement.width,
|
|
20072
20367
|
height: measurement.height,
|
|
20073
20368
|
kernedWidth: measurement.kernedWidth,
|
|
20074
20369
|
left: x,
|
|
20370
|
+
// Logical position (cumulative)
|
|
20075
20371
|
baseline: measurement.baseline,
|
|
20076
20372
|
charIndex: charIndex,
|
|
20077
20373
|
// Character position in original text
|
|
@@ -20085,6 +20381,9 @@ function layoutSingleLine(text, options) {
|
|
|
20085
20381
|
lineHeight = Math.max(lineHeight, measurement.height);
|
|
20086
20382
|
}
|
|
20087
20383
|
|
|
20384
|
+
// Note: BiDi visual reordering is handled by the browser's canvas fillText
|
|
20385
|
+
// The layout stores positions in logical order; hit testing handles the visual mapping
|
|
20386
|
+
|
|
20088
20387
|
// Remove trailing spacing from total width (but keep in bounds for rendering)
|
|
20089
20388
|
if (bounds.length > 0) {
|
|
20090
20389
|
options.letterSpacing || 0;
|
|
@@ -20095,7 +20394,9 @@ function layoutSingleLine(text, options) {
|
|
|
20095
20394
|
}
|
|
20096
20395
|
|
|
20097
20396
|
// Apply line height
|
|
20098
|
-
|
|
20397
|
+
// Note: Fabric.js uses _fontSizeMult = 1.13 for line height calculation
|
|
20398
|
+
const fontSizeMult = 1.13;
|
|
20399
|
+
const finalHeight = lineHeight * options.lineHeight * fontSizeMult;
|
|
20099
20400
|
return {
|
|
20100
20401
|
text,
|
|
20101
20402
|
graphemes,
|
|
@@ -20165,11 +20466,119 @@ function wrapByCharacters(text, maxWidth, options) {
|
|
|
20165
20466
|
return lines.length > 0 ? lines : [''];
|
|
20166
20467
|
}
|
|
20167
20468
|
|
|
20469
|
+
/**
|
|
20470
|
+
* Apply BiDi visual reordering to calculate correct visual X positions
|
|
20471
|
+
* This implements the Unicode Bidirectional Algorithm for character placement
|
|
20472
|
+
*/
|
|
20473
|
+
function applyBiDiVisualReordering(line, options) {
|
|
20474
|
+
const baseDirection = options.direction === 'inherit' ? 'ltr' : options.direction;
|
|
20475
|
+
|
|
20476
|
+
// Quick check: if all characters are same direction as base, no reordering needed
|
|
20477
|
+
const runs = analyzeBiDi(line.text, baseDirection);
|
|
20478
|
+
const hasMixedBiDi = runs.length > 1 || runs.length === 1 && runs[0].direction !== baseDirection;
|
|
20479
|
+
if (!hasMixedBiDi) {
|
|
20480
|
+
// For pure LTR or pure RTL, just set visual x = logical left
|
|
20481
|
+
// For RTL base direction, we need to flip positions
|
|
20482
|
+
if (baseDirection === 'rtl') {
|
|
20483
|
+
// RTL: rightmost character should be at x=0, leftmost at x=lineWidth
|
|
20484
|
+
line.bounds.forEach(bound => {
|
|
20485
|
+
bound.x = line.width - bound.left - bound.kernedWidth;
|
|
20486
|
+
});
|
|
20487
|
+
}
|
|
20488
|
+
// For LTR, x is already correct (same as left)
|
|
20489
|
+
return line;
|
|
20490
|
+
}
|
|
20491
|
+
|
|
20492
|
+
// Mixed BiDi text - need to reorder runs visually
|
|
20493
|
+
// 1. Build mapping from grapheme index to run
|
|
20494
|
+
const graphemeToRun = [];
|
|
20495
|
+
let runGraphemeStart = 0;
|
|
20496
|
+
for (let runIdx = 0; runIdx < runs.length; runIdx++) {
|
|
20497
|
+
const run = runs[runIdx];
|
|
20498
|
+
const runGraphemes = segmentGraphemes(run.text);
|
|
20499
|
+
for (let i = 0; i < runGraphemes.length; i++) {
|
|
20500
|
+
graphemeToRun.push(runIdx);
|
|
20501
|
+
}
|
|
20502
|
+
runGraphemeStart += runGraphemes.length;
|
|
20503
|
+
}
|
|
20504
|
+
|
|
20505
|
+
// 2. Calculate run widths and positions
|
|
20506
|
+
const runWidths = [];
|
|
20507
|
+
const runStartIndices = [];
|
|
20508
|
+
let currentIdx = 0;
|
|
20509
|
+
for (const run of runs) {
|
|
20510
|
+
runStartIndices.push(currentIdx);
|
|
20511
|
+
const runGraphemes = segmentGraphemes(run.text);
|
|
20512
|
+
let runWidth = 0;
|
|
20513
|
+
for (let i = 0; i < runGraphemes.length; i++) {
|
|
20514
|
+
if (currentIdx + i < line.bounds.length) {
|
|
20515
|
+
const letterSpacing = options.letterSpacing || 0;
|
|
20516
|
+
const charSpacing = options.charSpacing ? options.fontSize * options.charSpacing / 1000 : 0;
|
|
20517
|
+
runWidth += line.bounds[currentIdx + i].kernedWidth + letterSpacing + charSpacing;
|
|
20518
|
+
}
|
|
20519
|
+
}
|
|
20520
|
+
runWidths.push(runWidth);
|
|
20521
|
+
currentIdx += runGraphemes.length;
|
|
20522
|
+
}
|
|
20523
|
+
|
|
20524
|
+
// 3. Determine visual order of runs based on base direction
|
|
20525
|
+
// RTL base: runs display right-to-left (first run on right)
|
|
20526
|
+
// LTR base: runs display left-to-right (first run on left)
|
|
20527
|
+
const visualRunOrder = runs.map((_, i) => i);
|
|
20528
|
+
if (baseDirection === 'rtl') {
|
|
20529
|
+
visualRunOrder.reverse();
|
|
20530
|
+
}
|
|
20531
|
+
|
|
20532
|
+
// 4. Calculate visual X position for each run
|
|
20533
|
+
const runVisualX = new Array(runs.length);
|
|
20534
|
+
let currentX = 0;
|
|
20535
|
+
for (const runIdx of visualRunOrder) {
|
|
20536
|
+
runVisualX[runIdx] = currentX;
|
|
20537
|
+
currentX += runWidths[runIdx];
|
|
20538
|
+
}
|
|
20539
|
+
|
|
20540
|
+
// 5. Assign visual X positions to each grapheme
|
|
20541
|
+
for (let i = 0; i < line.bounds.length; i++) {
|
|
20542
|
+
const runIdx = graphemeToRun[i];
|
|
20543
|
+
if (runIdx === undefined) continue;
|
|
20544
|
+
const run = runs[runIdx];
|
|
20545
|
+
const runStart = runStartIndices[runIdx];
|
|
20546
|
+
|
|
20547
|
+
// Calculate spacing once
|
|
20548
|
+
const letterSpacing = options.letterSpacing || 0;
|
|
20549
|
+
const charSpacing = options.charSpacing ? options.fontSize * options.charSpacing / 1000 : 0;
|
|
20550
|
+
const totalSpacing = letterSpacing + charSpacing;
|
|
20551
|
+
|
|
20552
|
+
// Calculate offset within run (sum of widths of chars before this one)
|
|
20553
|
+
let offsetInRun = 0;
|
|
20554
|
+
for (let j = runStart; j < i; j++) {
|
|
20555
|
+
offsetInRun += line.bounds[j].kernedWidth + totalSpacing;
|
|
20556
|
+
}
|
|
20557
|
+
|
|
20558
|
+
// Character width including spacing
|
|
20559
|
+
const charWidth = line.bounds[i].kernedWidth + totalSpacing;
|
|
20560
|
+
|
|
20561
|
+
// For RTL runs, characters within the run are reversed visually
|
|
20562
|
+
// First logical char appears on the right, last on the left
|
|
20563
|
+
if (run.direction === 'rtl') {
|
|
20564
|
+
// Visual X = run right edge - cumulative width including this char
|
|
20565
|
+
// This places first char at right side of run, last char at left side
|
|
20566
|
+
line.bounds[i].x = runVisualX[runIdx] + runWidths[runIdx] - offsetInRun - charWidth;
|
|
20567
|
+
} else {
|
|
20568
|
+
// LTR run: visual position is run start + offset within run
|
|
20569
|
+
line.bounds[i].x = runVisualX[runIdx] + offsetInRun;
|
|
20570
|
+
}
|
|
20571
|
+
}
|
|
20572
|
+
return line;
|
|
20573
|
+
}
|
|
20574
|
+
|
|
20168
20575
|
/**
|
|
20169
20576
|
* Apply text alignment to lines
|
|
20170
20577
|
*/
|
|
20171
20578
|
function applyAlignment(lines, align, containerWidth, options) {
|
|
20172
20579
|
return lines.map(line => {
|
|
20580
|
+
// First apply BiDi reordering to get correct visual X positions
|
|
20581
|
+
applyBiDiVisualReordering(line, options);
|
|
20173
20582
|
let offsetX = 0;
|
|
20174
20583
|
switch (align) {
|
|
20175
20584
|
case 'center':
|
|
@@ -20189,7 +20598,7 @@ function applyAlignment(lines, align, containerWidth, options) {
|
|
|
20189
20598
|
break;
|
|
20190
20599
|
}
|
|
20191
20600
|
|
|
20192
|
-
// Apply offset to all bounds
|
|
20601
|
+
// Apply offset to all bounds (both visual x and logical left for alignment)
|
|
20193
20602
|
if (offsetX !== 0) {
|
|
20194
20603
|
line.bounds.forEach(bound => {
|
|
20195
20604
|
bound.x += offsetX;
|
|
@@ -20273,7 +20682,9 @@ function handleHeightOverflow(existingLines, overflowLine, remainingHeight, opti
|
|
|
20273
20682
|
* Create empty line for empty paragraphs
|
|
20274
20683
|
*/
|
|
20275
20684
|
function createEmptyLine(options) {
|
|
20276
|
-
|
|
20685
|
+
// Fabric.js uses _fontSizeMult = 1.13 for line height calculation
|
|
20686
|
+
const fontSizeMult = 1.13;
|
|
20687
|
+
const height = options.fontSize * options.lineHeight * fontSizeMult;
|
|
20277
20688
|
return {
|
|
20278
20689
|
text: '',
|
|
20279
20690
|
graphemes: [],
|
|
@@ -20759,6 +21170,12 @@ class FabricText extends StyledText {
|
|
|
20759
21170
|
* @protected
|
|
20760
21171
|
*/
|
|
20761
21172
|
_defineProperty(this, "__charBounds", []);
|
|
21173
|
+
/**
|
|
21174
|
+
* contains kashida extension info for each line.
|
|
21175
|
+
* Each entry contains { charIndex, width } for characters that have kashida extensions.
|
|
21176
|
+
* @protected
|
|
21177
|
+
*/
|
|
21178
|
+
_defineProperty(this, "__kashidaInfo", []);
|
|
20762
21179
|
Object.assign(this, FabricText.ownDefaults);
|
|
20763
21180
|
this.setOptions(options);
|
|
20764
21181
|
if (!this.styles) {
|
|
@@ -20877,11 +21294,31 @@ class FabricText extends StyledText {
|
|
|
20877
21294
|
}
|
|
20878
21295
|
|
|
20879
21296
|
/**
|
|
20880
|
-
* Enlarge space boxes and shift the others for justify alignment
|
|
21297
|
+
* Enlarge space boxes and shift the others for justify alignment.
|
|
21298
|
+
* Supports Arabic kashida (tatweel) justification when kashida property is set.
|
|
21299
|
+
* When kashida is enabled, actual tatweel characters are inserted into the text.
|
|
20881
21300
|
*/
|
|
20882
21301
|
enlargeSpaces() {
|
|
20883
|
-
|
|
21302
|
+
// console.log('=== enlargeSpaces START ===');
|
|
21303
|
+
// console.log('this.kashida:', this.kashida);
|
|
21304
|
+
|
|
21305
|
+
// Kashida ratios: proportion of extra space distributed via kashida vs space expansion
|
|
21306
|
+
const kashidaRatios = {
|
|
21307
|
+
none: 0,
|
|
21308
|
+
short: 0.25,
|
|
21309
|
+
medium: 0.5,
|
|
21310
|
+
long: 0.75,
|
|
21311
|
+
stylistic: 1.0
|
|
21312
|
+
};
|
|
21313
|
+
const kashidaRatio = kashidaRatios[this.kashida] || 0;
|
|
21314
|
+
// console.log('kashidaRatio:', kashidaRatio);
|
|
21315
|
+
|
|
21316
|
+
// Reset kashida info
|
|
21317
|
+
this.__kashidaInfo = [];
|
|
20884
21318
|
for (let i = 0, len = this._textLines.length; i < len; i++) {
|
|
21319
|
+
// Initialize kashida info for this line
|
|
21320
|
+
this.__kashidaInfo[i] = [];
|
|
21321
|
+
|
|
20885
21322
|
// Check if this line should be justified
|
|
20886
21323
|
const hasTextAfter = this._textLines.slice(i + 1).some(line => {
|
|
20887
21324
|
const lineText = Array.isArray(line) ? line.join('') : line;
|
|
@@ -20891,33 +21328,121 @@ class FabricText extends StyledText {
|
|
|
20891
21328
|
const isLastLine = i === len - 1 || this.isEndOfWrapping(i) || isVisualLastLine;
|
|
20892
21329
|
const shouldJustifyLine = this.textAlign.includes('justify') && !isLastLine;
|
|
20893
21330
|
if (!shouldJustifyLine) {
|
|
21331
|
+
// console.log(` Line ${i}: skipped (not justified)`);
|
|
20894
21332
|
continue;
|
|
20895
21333
|
}
|
|
20896
|
-
|
|
20897
|
-
|
|
20898
|
-
|
|
20899
|
-
|
|
20900
|
-
|
|
20901
|
-
|
|
20902
|
-
|
|
20903
|
-
|
|
20904
|
-
|
|
20905
|
-
|
|
20906
|
-
|
|
20907
|
-
|
|
20908
|
-
|
|
20909
|
-
|
|
20910
|
-
|
|
20911
|
-
|
|
20912
|
-
|
|
20913
|
-
|
|
20914
|
-
|
|
20915
|
-
|
|
21334
|
+
const line = this._textLines[i];
|
|
21335
|
+
const currentLineWidth = this.getLineWidth(i);
|
|
21336
|
+
const totalExtraSpace = this.width - currentLineWidth;
|
|
21337
|
+
// console.log(` Line ${i}: width=${this.width}, lineWidth=${currentLineWidth}, extraSpace=${totalExtraSpace}`);
|
|
21338
|
+
|
|
21339
|
+
if (totalExtraSpace <= 0) {
|
|
21340
|
+
// console.log(` Line ${i}: skipped (no extra space)`);
|
|
21341
|
+
continue;
|
|
21342
|
+
}
|
|
21343
|
+
|
|
21344
|
+
// Find spaces for space expansion
|
|
21345
|
+
const spaces = this.textLines[i].match(this._reSpacesAndTabs);
|
|
21346
|
+
const numberOfSpaces = spaces ? spaces.length : 0;
|
|
21347
|
+
|
|
21348
|
+
// Find kashida points if enabled
|
|
21349
|
+
const kashidaPoints = kashidaRatio > 0 ? findKashidaPoints(line) : [];
|
|
21350
|
+
const hasKashidaPoints = kashidaPoints.length > 0;
|
|
21351
|
+
|
|
21352
|
+
// Calculate space distribution
|
|
21353
|
+
let kashidaSpace = 0;
|
|
21354
|
+
if (hasKashidaPoints && kashidaRatio > 0) {
|
|
21355
|
+
// Distribute between kashida and spaces
|
|
21356
|
+
kashidaSpace = totalExtraSpace * kashidaRatio;
|
|
21357
|
+
}
|
|
21358
|
+
|
|
21359
|
+
// Calculate per-kashida and per-space widths
|
|
21360
|
+
const perKashidaWidth = hasKashidaPoints ? kashidaSpace / kashidaPoints.length : 0;
|
|
21361
|
+
|
|
21362
|
+
// If kashida is enabled, insert tatweel characters into the text
|
|
21363
|
+
if (hasKashidaPoints && perKashidaWidth > 0) {
|
|
21364
|
+
// console.log(`=== Inserting kashida for line ${i} ===`);
|
|
21365
|
+
// console.log(` kashidaPoints: ${kashidaPoints.length}, perKashidaWidth: ${perKashidaWidth}`);
|
|
21366
|
+
|
|
21367
|
+
// Sort by charIndex descending to insert from end (so indices stay valid)
|
|
21368
|
+
const sortedPoints = [...kashidaPoints].sort((a, b) => b.charIndex - a.charIndex);
|
|
21369
|
+
|
|
21370
|
+
// Calculate how many tatweels to insert per point
|
|
21371
|
+
// Measure tatweel width to determine count
|
|
21372
|
+
const ctx = getMeasuringContext();
|
|
21373
|
+
// console.log(` getMeasuringContext: ${ctx ? 'OK' : 'NULL'}`);
|
|
21374
|
+
|
|
21375
|
+
if (ctx) {
|
|
21376
|
+
ctx.font = this._getFontDeclaration();
|
|
21377
|
+
const tatweelWidth = ctx.measureText(ARABIC_TATWEEL).width;
|
|
21378
|
+
// console.log(` tatweelWidth: ${tatweelWidth}`);
|
|
21379
|
+
|
|
21380
|
+
if (tatweelWidth > 0) {
|
|
21381
|
+
const newLine = [...line];
|
|
21382
|
+
for (const point of sortedPoints) {
|
|
21383
|
+
const tatweelCount = Math.max(1, Math.round(perKashidaWidth / tatweelWidth));
|
|
21384
|
+
// console.log(` Point ${point.charIndex}: inserting ${tatweelCount} tatweels`);
|
|
21385
|
+
|
|
21386
|
+
// Insert tatweels after the character
|
|
21387
|
+
for (let t = 0; t < tatweelCount; t++) {
|
|
21388
|
+
newLine.splice(point.charIndex + 1, 0, ARABIC_TATWEEL);
|
|
21389
|
+
}
|
|
21390
|
+
|
|
21391
|
+
// Store kashida info with updated indices and tatweel count
|
|
21392
|
+
this.__kashidaInfo[i].push({
|
|
21393
|
+
charIndex: point.charIndex,
|
|
21394
|
+
width: perKashidaWidth,
|
|
21395
|
+
tatweelCount: tatweelCount
|
|
21396
|
+
});
|
|
21397
|
+
}
|
|
21398
|
+
|
|
21399
|
+
// console.log(` Total inserted: ${insertedCount} tatweels`);
|
|
21400
|
+
// console.log(` Original line length: ${line.length}, new line length: ${newLine.length}`);
|
|
21401
|
+
// console.log(` New line: ${newLine.join('')}`);
|
|
21402
|
+
|
|
21403
|
+
// Update _textLines with the new line containing tatweels
|
|
21404
|
+
this._textLines[i] = newLine;
|
|
21405
|
+
|
|
21406
|
+
// Update textLines string version
|
|
21407
|
+
if (this.textLines && this.textLines[i] !== undefined) {
|
|
21408
|
+
this.textLines[i] = newLine.join('');
|
|
20916
21409
|
}
|
|
21410
|
+
|
|
21411
|
+
// Recalculate charBounds for this line since text changed
|
|
21412
|
+
this.__charBounds[i] = [];
|
|
21413
|
+
this.__lineWidths[i] = undefined;
|
|
21414
|
+
this._measureLine(i);
|
|
21415
|
+
|
|
21416
|
+
// console.log(` After remeasure, lineWidth: ${this.__lineWidths[i]}`);
|
|
21417
|
+
}
|
|
21418
|
+
}
|
|
21419
|
+
}
|
|
21420
|
+
|
|
21421
|
+
// Now apply space expansion to remaining extra space
|
|
21422
|
+
const newLineWidth = this.getLineWidth(i);
|
|
21423
|
+
const remainingSpace = this.width - newLineWidth;
|
|
21424
|
+
if (remainingSpace > 0 && numberOfSpaces > 0) {
|
|
21425
|
+
const extraPerSpace = remainingSpace / numberOfSpaces;
|
|
21426
|
+
let accumulatedOffset = 0;
|
|
21427
|
+
for (let j = 0; j < this._textLines[i].length; j++) {
|
|
21428
|
+
const charBound = this.__charBounds[i][j];
|
|
21429
|
+
if (!charBound) continue;
|
|
21430
|
+
charBound.left += accumulatedOffset;
|
|
21431
|
+
if (this._reSpaceAndTab.test(this._textLines[i][j])) {
|
|
21432
|
+
charBound.width += extraPerSpace;
|
|
21433
|
+
charBound.kernedWidth += extraPerSpace;
|
|
21434
|
+
accumulatedOffset += extraPerSpace;
|
|
20917
21435
|
}
|
|
20918
21436
|
}
|
|
20919
21437
|
}
|
|
20920
21438
|
}
|
|
21439
|
+
|
|
21440
|
+
// Final debug log showing kashida state
|
|
21441
|
+
// console.log('=== enlargeSpaces END ===');
|
|
21442
|
+
// console.log('Final __kashidaInfo:', JSON.stringify(this.__kashidaInfo.map((lineInfo, i) => ({
|
|
21443
|
+
// line: i,
|
|
21444
|
+
// entries: lineInfo.map(k => ({ charIndex: k.charIndex, tatweelCount: k.tatweelCount }))
|
|
21445
|
+
// }))));
|
|
20921
21446
|
}
|
|
20922
21447
|
|
|
20923
21448
|
/**
|
|
@@ -20937,7 +21462,9 @@ class FabricText extends StyledText {
|
|
|
20937
21462
|
return {
|
|
20938
21463
|
text: this.text,
|
|
20939
21464
|
width: this.width,
|
|
20940
|
-
height
|
|
21465
|
+
// Don't pass height constraint to allow vertical auto-expansion
|
|
21466
|
+
// Only pass height if ellipsis is enabled (need to truncate)
|
|
21467
|
+
height: this.ellipsis ? this.height : undefined,
|
|
20941
21468
|
wrap: this.wrap || 'word',
|
|
20942
21469
|
align: this._mapTextAlignToAlign(this.textAlign),
|
|
20943
21470
|
ellipsis: this.ellipsis || false,
|
|
@@ -20992,9 +21519,13 @@ class FabricText extends StyledText {
|
|
|
20992
21519
|
// Convert layout to legacy format for compatibility
|
|
20993
21520
|
this._convertLayoutToLegacyFormat(layout);
|
|
20994
21521
|
|
|
20995
|
-
//
|
|
20996
|
-
//
|
|
20997
|
-
|
|
21522
|
+
// Apply kashida if enabled for justify alignment
|
|
21523
|
+
// This must be called after _convertLayoutToLegacyFormat to ensure __charBounds exists
|
|
21524
|
+
if (this.textAlign.includes(JUSTIFY) && this.kashida && this.kashida !== 'none') {
|
|
21525
|
+
if (this.__charBounds && this.__charBounds.length > 0) {
|
|
21526
|
+
this.enlargeSpaces();
|
|
21527
|
+
}
|
|
21528
|
+
}
|
|
20998
21529
|
this.dirty = true;
|
|
20999
21530
|
}
|
|
21000
21531
|
|
|
@@ -21006,16 +21537,27 @@ class FabricText extends StyledText {
|
|
|
21006
21537
|
this._textLines = layout.lines.map(line => line.graphemes);
|
|
21007
21538
|
this.textLines = layout.lines.map(line => line.text);
|
|
21008
21539
|
|
|
21540
|
+
// Set _text as flat array of all graphemes (required for editing)
|
|
21541
|
+
this._text = layout.lines.flatMap(line => line.graphemes);
|
|
21542
|
+
|
|
21009
21543
|
// Convert bounds to legacy format
|
|
21544
|
+
// IMPORTANT: Preserve both logical (left) and visual (renderLeft) positions
|
|
21545
|
+
// - left: cumulative logical offset (for text editing operations)
|
|
21546
|
+
// - renderLeft: actual visual X position after BiDi reordering and alignment
|
|
21547
|
+
// The renderLeft is critical for correct cursor/selection hit testing in mixed RTL/LTR text
|
|
21010
21548
|
this.__charBounds = layout.lines.map(line => line.bounds.map(bound => ({
|
|
21011
21549
|
left: bound.left,
|
|
21012
21550
|
top: bound.y,
|
|
21013
21551
|
width: bound.width,
|
|
21014
21552
|
height: bound.height,
|
|
21015
21553
|
kernedWidth: bound.kernedWidth,
|
|
21016
|
-
deltaY: bound.deltaY || 0
|
|
21554
|
+
deltaY: bound.deltaY || 0,
|
|
21555
|
+
renderLeft: bound.x // Visual X position for hit testing
|
|
21017
21556
|
})));
|
|
21018
21557
|
|
|
21558
|
+
// Populate line widths cache to prevent getLineWidth from triggering legacy measurement
|
|
21559
|
+
this.__lineWidths = layout.lines.map(line => line.width);
|
|
21560
|
+
|
|
21019
21561
|
// Update grapheme info for compatibility
|
|
21020
21562
|
if (layout.lines.length > 0) {
|
|
21021
21563
|
this._unwrappedTextLines = layout.lines.map(line => line.graphemes);
|
|
@@ -21183,6 +21725,48 @@ class FabricText extends StyledText {
|
|
|
21183
21725
|
this._renderChars(method, ctx, line, left, top, lineIndex);
|
|
21184
21726
|
}
|
|
21185
21727
|
|
|
21728
|
+
/**
|
|
21729
|
+
* Build display text lines with kashida characters inserted.
|
|
21730
|
+
* This creates a version of _textLines with tatweel characters added at kashida points.
|
|
21731
|
+
* @private
|
|
21732
|
+
*/
|
|
21733
|
+
_buildKashidaDisplayLines() {
|
|
21734
|
+
if (this.kashida === 'none' || !this.__kashidaInfo) {
|
|
21735
|
+
return this._textLines;
|
|
21736
|
+
}
|
|
21737
|
+
const displayLines = [];
|
|
21738
|
+
for (let lineIndex = 0; lineIndex < this._textLines.length; lineIndex++) {
|
|
21739
|
+
const line = this._textLines[lineIndex];
|
|
21740
|
+
const kashidaInfo = this.__kashidaInfo[lineIndex];
|
|
21741
|
+
if (!kashidaInfo || kashidaInfo.length === 0) {
|
|
21742
|
+
displayLines.push([...line]);
|
|
21743
|
+
continue;
|
|
21744
|
+
}
|
|
21745
|
+
|
|
21746
|
+
// Sort kashida points by charIndex descending so we can insert from the end
|
|
21747
|
+
const sortedKashida = [...kashidaInfo].sort((a, b) => b.charIndex - a.charIndex);
|
|
21748
|
+
|
|
21749
|
+
// Calculate how many tatweels to insert based on width
|
|
21750
|
+
const newLine = [...line];
|
|
21751
|
+
for (const {
|
|
21752
|
+
charIndex,
|
|
21753
|
+
width
|
|
21754
|
+
} of sortedKashida) {
|
|
21755
|
+
if (width <= 0 || charIndex >= newLine.length) continue;
|
|
21756
|
+
|
|
21757
|
+
// Calculate number of tatweel characters based on width
|
|
21758
|
+
// Each tatweel is approximately 5px at font size 24
|
|
21759
|
+
const tatweelCount = Math.max(1, Math.round(width / 3));
|
|
21760
|
+
const tatweels = ARABIC_TATWEEL.repeat(tatweelCount);
|
|
21761
|
+
|
|
21762
|
+
// Insert tatweels after the character at charIndex
|
|
21763
|
+
newLine.splice(charIndex + 1, 0, tatweels);
|
|
21764
|
+
}
|
|
21765
|
+
displayLines.push(newLine);
|
|
21766
|
+
}
|
|
21767
|
+
return displayLines;
|
|
21768
|
+
}
|
|
21769
|
+
|
|
21186
21770
|
/**
|
|
21187
21771
|
* Renders the text background for lines, taking care of style
|
|
21188
21772
|
* @private
|
|
@@ -21263,10 +21847,13 @@ class FabricText extends StyledText {
|
|
|
21263
21847
|
const fontCache = cache.getFontCache(charStyle),
|
|
21264
21848
|
fontDeclaration = this._getFontDeclaration(charStyle),
|
|
21265
21849
|
couple = previousChar + _char,
|
|
21266
|
-
|
|
21850
|
+
// Skip kerning for tatweel (kashida) characters - they extend connections
|
|
21851
|
+
// and kerning would make the following character appear too narrow
|
|
21852
|
+
isTatweel = previousChar === '\u0640',
|
|
21853
|
+
stylesAreEqual = previousChar && !isTatweel && fontDeclaration === this._getFontDeclaration(prevCharStyle),
|
|
21267
21854
|
fontMultiplier = charStyle.fontSize / this.CACHE_FONT_SIZE;
|
|
21268
21855
|
let width, coupleWidth, previousWidth, kernedWidth;
|
|
21269
|
-
if (previousChar && fontCache[previousChar] !== undefined) {
|
|
21856
|
+
if (previousChar && !isTatweel && fontCache[previousChar] !== undefined) {
|
|
21270
21857
|
previousWidth = fontCache[previousChar];
|
|
21271
21858
|
}
|
|
21272
21859
|
if (fontCache[_char] !== undefined) {
|
|
@@ -21284,11 +21871,11 @@ class FabricText extends StyledText {
|
|
|
21284
21871
|
kernedWidth = width = ctx.measureText(_char).width;
|
|
21285
21872
|
fontCache[_char] = width;
|
|
21286
21873
|
}
|
|
21287
|
-
if (previousWidth === undefined && stylesAreEqual && previousChar) {
|
|
21874
|
+
if (previousWidth === undefined && stylesAreEqual && previousChar && !isTatweel) {
|
|
21288
21875
|
previousWidth = ctx.measureText(previousChar).width;
|
|
21289
21876
|
fontCache[previousChar] = previousWidth;
|
|
21290
21877
|
}
|
|
21291
|
-
if (stylesAreEqual && coupleWidth === undefined) {
|
|
21878
|
+
if (stylesAreEqual && coupleWidth === undefined && !isTatweel) {
|
|
21292
21879
|
// we can measure the kerning couple and subtract the width of the previous character
|
|
21293
21880
|
coupleWidth = ctx.measureText(couple).width;
|
|
21294
21881
|
fontCache[couple] = coupleWidth;
|
|
@@ -21335,10 +21922,7 @@ class FabricText extends StyledText {
|
|
|
21335
21922
|
*/
|
|
21336
21923
|
_measureLine(lineIndex) {
|
|
21337
21924
|
// Debug: detect if measureLine is called after justify was applied
|
|
21338
|
-
if (this._justifyApplied)
|
|
21339
|
-
console.warn(`WARNING: _measureLine called for line ${lineIndex} AFTER justify was applied! This will overwrite justified charBounds.`);
|
|
21340
|
-
console.trace('Stack trace:');
|
|
21341
|
-
}
|
|
21925
|
+
if (this._justifyApplied) ;
|
|
21342
21926
|
let width = 0,
|
|
21343
21927
|
prevGrapheme,
|
|
21344
21928
|
graphemeInfo;
|
|
@@ -21513,13 +22097,7 @@ class FabricText extends StyledText {
|
|
|
21513
22097
|
top = this._getTopOffset();
|
|
21514
22098
|
|
|
21515
22099
|
// Debug: log once per render
|
|
21516
|
-
if (method === 'fillText' && (_this$textAlign = this.textAlign) !== null && _this$textAlign !== void 0 && _this$textAlign.includes('justify'))
|
|
21517
|
-
console.log('=== RENDER DEBUG ===');
|
|
21518
|
-
console.log('direction:', this.direction);
|
|
21519
|
-
console.log('textAlign:', this.textAlign);
|
|
21520
|
-
console.log('width:', this.width);
|
|
21521
|
-
console.log('_getLeftOffset:', left);
|
|
21522
|
-
}
|
|
22100
|
+
if (method === 'fillText' && (_this$textAlign = this.textAlign) !== null && _this$textAlign !== void 0 && _this$textAlign.includes('justify')) ;
|
|
21523
22101
|
for (let i = 0, len = this._textLines.length; i < len; i++) {
|
|
21524
22102
|
var _this$textAlign2;
|
|
21525
22103
|
const heightOfLine = this.getHeightOfLine(i),
|
|
@@ -21528,8 +22106,8 @@ class FabricText extends StyledText {
|
|
|
21528
22106
|
|
|
21529
22107
|
// Debug: log line offsets for justify
|
|
21530
22108
|
if (method === 'fillText' && (_this$textAlign2 = this.textAlign) !== null && _this$textAlign2 !== void 0 && _this$textAlign2.includes('justify')) {
|
|
21531
|
-
|
|
21532
|
-
console.log(`Line ${i}: leftOffset=${leftOffset.toFixed(2)}, lineWidth=${lineWidth.toFixed(2)}, renderAt=${(left + leftOffset).toFixed(2)}`);
|
|
22109
|
+
this.getLineWidth(i);
|
|
22110
|
+
// console.log(`Line ${i}: leftOffset=${leftOffset.toFixed(2)}, lineWidth=${lineWidth.toFixed(2)}, renderAt=${(left + leftOffset).toFixed(2)}`);
|
|
21533
22111
|
}
|
|
21534
22112
|
this._renderTextLine(method, ctx, this._textLines[i], left + leftOffset, top + lineHeights + maxHeight, i);
|
|
21535
22113
|
lineHeights += heightOfLine;
|
|
@@ -21580,12 +22158,18 @@ class FabricText extends StyledText {
|
|
|
21580
22158
|
const lineHeight = this.getHeightOfLine(lineIndex),
|
|
21581
22159
|
isJustify = this.textAlign.includes(JUSTIFY),
|
|
21582
22160
|
path = this.path,
|
|
21583
|
-
shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path,
|
|
21584
22161
|
isLtr = this.direction === 'ltr',
|
|
21585
22162
|
sign = this.direction === 'ltr' ? 1 : -1,
|
|
21586
22163
|
// this was changed in the PR #7674
|
|
21587
22164
|
// currentDirection = ctx.canvas.getAttribute('dir');
|
|
21588
22165
|
currentDirection = ctx.direction;
|
|
22166
|
+
|
|
22167
|
+
// Check if we should use BiDi-aware rendering with pre-calculated positions
|
|
22168
|
+
// This is needed for advanced layout with RTL or mixed BiDi text
|
|
22169
|
+
const chars = this.__charBounds[lineIndex];
|
|
22170
|
+
this.enableAdvancedLayout && (chars === null || chars === void 0 ? void 0 : chars.length) > 0 && chars[0].renderLeft !== undefined;
|
|
22171
|
+
|
|
22172
|
+
const shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path;
|
|
21589
22173
|
let actualStyle,
|
|
21590
22174
|
nextStyle,
|
|
21591
22175
|
charsToRender = '',
|
|
@@ -21594,6 +22178,9 @@ class FabricText extends StyledText {
|
|
|
21594
22178
|
timeToRender,
|
|
21595
22179
|
drawingLeft;
|
|
21596
22180
|
ctx.save();
|
|
22181
|
+
|
|
22182
|
+
// For BiDi rendering with pre-calculated positions, disable browser BiDi
|
|
22183
|
+
// and render each character at its calculated visual position
|
|
21597
22184
|
if (currentDirection !== this.direction) {
|
|
21598
22185
|
ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl');
|
|
21599
22186
|
ctx.direction = isLtr ? 'ltr' : 'rtl';
|
|
@@ -21613,12 +22200,12 @@ class FabricText extends StyledText {
|
|
|
21613
22200
|
}
|
|
21614
22201
|
// Debug: Log charBounds being used for first line only during justify
|
|
21615
22202
|
if (isJustify && lineIndex === 0 && method === 'fillText') {
|
|
21616
|
-
console.log(`\n=== RENDER _renderChars line ${lineIndex} ===`);
|
|
21617
|
-
console.log('Initial left:', left.toFixed(2), 'sign:', sign);
|
|
21618
|
-
console.log('_justifyApplied flag:', this._justifyApplied);
|
|
22203
|
+
// console.log(`\n=== RENDER _renderChars line ${lineIndex} ===`);
|
|
22204
|
+
// console.log('Initial left:', left.toFixed(2), 'sign:', sign);
|
|
22205
|
+
// console.log('_justifyApplied flag:', (this as any)._justifyApplied);
|
|
21619
22206
|
const lineBounds = this.__charBounds[lineIndex];
|
|
21620
|
-
|
|
21621
|
-
console.log('Total kernedWidth in charBounds:', totalKW.toFixed(2), '(should be ~300 if justify was applied)');
|
|
22207
|
+
(lineBounds === null || lineBounds === void 0 ? void 0 : lineBounds.reduce((s, b) => s + ((b === null || b === void 0 ? void 0 : b.kernedWidth) || 0), 0)) || 0;
|
|
22208
|
+
// console.log('Total kernedWidth in charBounds:', totalKW.toFixed(2), '(should be ~300 if justify was applied)');
|
|
21622
22209
|
// Log first few space widths to verify expansion
|
|
21623
22210
|
const spaceIndices = [3, 9, 15, 23, 31];
|
|
21624
22211
|
spaceIndices.forEach(idx => {
|
|
@@ -21667,10 +22254,6 @@ class FabricText extends StyledText {
|
|
|
21667
22254
|
// For RTL with textAlign='right': x is the right edge, so drawingLeft = left
|
|
21668
22255
|
// Both cases: drawingLeft = left (the text alignment handles the edge correctly)
|
|
21669
22256
|
drawingLeft = left;
|
|
21670
|
-
// Debug: log first chunk positioning for justify
|
|
21671
|
-
if (isJustify && lineIndex === 0 && method === 'fillText' && i < 5) {
|
|
21672
|
-
console.log(` Chunk ending at char ${i}: left=${left.toFixed(2)}, boxWidth=${boxWidth.toFixed(2)}, drawingLeft=${drawingLeft.toFixed(2)}, textAlign=${isLtr ? 'left' : 'right'}`);
|
|
21673
|
-
}
|
|
21674
22257
|
this._renderChar(method, ctx, lineIndex, i, charsToRender, drawingLeft, top);
|
|
21675
22258
|
}
|
|
21676
22259
|
charsToRender = '';
|
|
@@ -21679,11 +22262,6 @@ class FabricText extends StyledText {
|
|
|
21679
22262
|
boxWidth = 0;
|
|
21680
22263
|
}
|
|
21681
22264
|
}
|
|
21682
|
-
// Debug: log final position for justify
|
|
21683
|
-
if (isJustify && lineIndex === 0 && method === 'fillText') {
|
|
21684
|
-
console.log('Final left position after rendering:', left.toFixed(2));
|
|
21685
|
-
console.log('Expected final position:', (sign > 0 ? this.width / 2 : -this.width / 2).toFixed(2));
|
|
21686
|
-
}
|
|
21687
22265
|
ctx.restore();
|
|
21688
22266
|
}
|
|
21689
22267
|
|
|
@@ -21915,12 +22493,159 @@ class FabricText extends StyledText {
|
|
|
21915
22493
|
* @private
|
|
21916
22494
|
*/
|
|
21917
22495
|
_clearCache() {
|
|
22496
|
+
// console.log('🗑️ _clearCache called');
|
|
22497
|
+
// console.trace('🗑️ _clearCache stack trace');
|
|
21918
22498
|
this._forceClearCache = false;
|
|
21919
22499
|
this.__lineWidths = [];
|
|
21920
22500
|
this.__lineHeights = [];
|
|
21921
22501
|
this.__charBounds = [];
|
|
22502
|
+
this.__kashidaInfo = [];
|
|
21922
22503
|
// Reset justify applied flag
|
|
21923
22504
|
this._justifyApplied = false;
|
|
22505
|
+
// Reset dimension state to force recalculation
|
|
22506
|
+
this._lastDimensionState = null;
|
|
22507
|
+
}
|
|
22508
|
+
|
|
22509
|
+
/**
|
|
22510
|
+
* Convert a display character index (in _textLines with tatweels) to original text index.
|
|
22511
|
+
* When kashida is applied, _textLines contains extra tatweel characters that don't exist
|
|
22512
|
+
* in the original text. This method maps back to the original index.
|
|
22513
|
+
* @param lineIndex - The line index
|
|
22514
|
+
* @param displayCharIndex - Character index in the display text (with tatweels)
|
|
22515
|
+
* @returns Original character index (without tatweels)
|
|
22516
|
+
*/
|
|
22517
|
+
_displayToOriginalIndex(lineIndex, displayCharIndex) {
|
|
22518
|
+
var _this$__kashidaInfo;
|
|
22519
|
+
// console.log(`🔄 _displayToOriginalIndex called: line=${lineIndex}, displayIdx=${displayCharIndex}`);
|
|
22520
|
+
// console.log(`🔄 __kashidaInfo exists: ${!!this.__kashidaInfo}, length: ${this.__kashidaInfo?.length}`);
|
|
22521
|
+
// console.log(`🔄 __kashidaInfo raw:`, JSON.stringify(this.__kashidaInfo));
|
|
22522
|
+
|
|
22523
|
+
const kashidaInfo = (_this$__kashidaInfo = this.__kashidaInfo) === null || _this$__kashidaInfo === void 0 ? void 0 : _this$__kashidaInfo[lineIndex];
|
|
22524
|
+
if (!kashidaInfo || kashidaInfo.length === 0) {
|
|
22525
|
+
// No kashida on this line, indices are the same
|
|
22526
|
+
// console.log(`🔄 No kashida info for line ${lineIndex}, returning same index`);
|
|
22527
|
+
return displayCharIndex;
|
|
22528
|
+
}
|
|
22529
|
+
|
|
22530
|
+
// Sort kashida info by charIndex ascending for proper traversal
|
|
22531
|
+
const sortedKashida = [...kashidaInfo].sort((a, b) => a.charIndex - b.charIndex);
|
|
22532
|
+
|
|
22533
|
+
// console.log(`🔄 _displayToOriginalIndex: line=${lineIndex}, displayIdx=${displayCharIndex}`);
|
|
22534
|
+
// console.log(`🔄 kashidaInfo:`, sortedKashida.map(k => `{charIdx:${k.charIndex}, cnt:${k.tatweelCount}}`).join(', '));
|
|
22535
|
+
|
|
22536
|
+
let tatweelsBeforeIndex = 0;
|
|
22537
|
+
for (const k of sortedKashida) {
|
|
22538
|
+
const tatweelCount = k.tatweelCount || 0;
|
|
22539
|
+
// Position where tatweels start (after the original character)
|
|
22540
|
+
const tatweelStartPos = k.charIndex + 1 + tatweelsBeforeIndex;
|
|
22541
|
+
const tatweelEndPos = tatweelStartPos + tatweelCount;
|
|
22542
|
+
|
|
22543
|
+
// console.log(`🔄 k.charIndex=${k.charIndex}, tatweelStartPos=${tatweelStartPos}, tatweelEndPos=${tatweelEndPos}, tatweelsBeforeIndex=${tatweelsBeforeIndex}`);
|
|
22544
|
+
|
|
22545
|
+
if (displayCharIndex < tatweelStartPos) {
|
|
22546
|
+
// Before this kashida point
|
|
22547
|
+
// console.log(`🔄 displayIdx < tatweelStartPos, break`);
|
|
22548
|
+
break;
|
|
22549
|
+
} else if (displayCharIndex < tatweelEndPos) {
|
|
22550
|
+
// Within tatweel characters - map to the character before tatweels
|
|
22551
|
+
// console.log(`🔄 Within tatweel, return ${k.charIndex + 1}`);
|
|
22552
|
+
return k.charIndex + 1;
|
|
22553
|
+
} else {
|
|
22554
|
+
// After this kashida point
|
|
22555
|
+
tatweelsBeforeIndex += tatweelCount;
|
|
22556
|
+
// console.log(`🔄 After this kashida, tatweelsBeforeIndex now=${tatweelsBeforeIndex}`);
|
|
22557
|
+
}
|
|
22558
|
+
}
|
|
22559
|
+
|
|
22560
|
+
// Subtract all tatweels that come before this position
|
|
22561
|
+
const result = displayCharIndex - tatweelsBeforeIndex;
|
|
22562
|
+
// console.log(`🔄 Final result: ${displayCharIndex} - ${tatweelsBeforeIndex} = ${result}`);
|
|
22563
|
+
return result;
|
|
22564
|
+
}
|
|
22565
|
+
|
|
22566
|
+
/**
|
|
22567
|
+
* Convert an original text character index to display index (in _textLines with tatweels).
|
|
22568
|
+
* @param lineIndex - The line index
|
|
22569
|
+
* @param originalCharIndex - Character index in the original text (without tatweels)
|
|
22570
|
+
* @returns Display character index (with tatweels)
|
|
22571
|
+
*/
|
|
22572
|
+
_originalToDisplayIndex(lineIndex, originalCharIndex) {
|
|
22573
|
+
var _this$__kashidaInfo2;
|
|
22574
|
+
const kashidaInfo = (_this$__kashidaInfo2 = this.__kashidaInfo) === null || _this$__kashidaInfo2 === void 0 ? void 0 : _this$__kashidaInfo2[lineIndex];
|
|
22575
|
+
if (!kashidaInfo || kashidaInfo.length === 0) {
|
|
22576
|
+
// No kashida on this line, indices are the same
|
|
22577
|
+
return originalCharIndex;
|
|
22578
|
+
}
|
|
22579
|
+
|
|
22580
|
+
// Sort kashida info by charIndex ascending
|
|
22581
|
+
const sortedKashida = [...kashidaInfo].sort((a, b) => a.charIndex - b.charIndex);
|
|
22582
|
+
let tatweelsBeforeIndex = 0;
|
|
22583
|
+
for (const k of sortedKashida) {
|
|
22584
|
+
const tatweelCount = k.tatweelCount || 0;
|
|
22585
|
+
// If the original char index is after this kashida insertion point,
|
|
22586
|
+
// add the tatweels to the offset
|
|
22587
|
+
if (originalCharIndex > k.charIndex) {
|
|
22588
|
+
tatweelsBeforeIndex += tatweelCount;
|
|
22589
|
+
} else {
|
|
22590
|
+
break;
|
|
22591
|
+
}
|
|
22592
|
+
}
|
|
22593
|
+
return originalCharIndex + tatweelsBeforeIndex;
|
|
22594
|
+
}
|
|
22595
|
+
|
|
22596
|
+
/**
|
|
22597
|
+
* Check if a display character index points to a tatweel character.
|
|
22598
|
+
* @param lineIndex - The line index
|
|
22599
|
+
* @param displayCharIndex - Character index in the display text
|
|
22600
|
+
* @returns True if the character at this index is a tatweel
|
|
22601
|
+
*/
|
|
22602
|
+
_isTatweelAtDisplayIndex(lineIndex, displayCharIndex) {
|
|
22603
|
+
var _this$__kashidaInfo3;
|
|
22604
|
+
const kashidaInfo = (_this$__kashidaInfo3 = this.__kashidaInfo) === null || _this$__kashidaInfo3 === void 0 ? void 0 : _this$__kashidaInfo3[lineIndex];
|
|
22605
|
+
if (!kashidaInfo || kashidaInfo.length === 0) {
|
|
22606
|
+
return false;
|
|
22607
|
+
}
|
|
22608
|
+
|
|
22609
|
+
// Sort kashida info by charIndex ascending
|
|
22610
|
+
const sortedKashida = [...kashidaInfo].sort((a, b) => a.charIndex - b.charIndex);
|
|
22611
|
+
let tatweelsBeforeIndex = 0;
|
|
22612
|
+
for (const k of sortedKashida) {
|
|
22613
|
+
const tatweelCount = k.tatweelCount || 0;
|
|
22614
|
+
const tatweelStartPos = k.charIndex + 1 + tatweelsBeforeIndex;
|
|
22615
|
+
const tatweelEndPos = tatweelStartPos + tatweelCount;
|
|
22616
|
+
if (displayCharIndex >= tatweelStartPos && displayCharIndex < tatweelEndPos) {
|
|
22617
|
+
return true;
|
|
22618
|
+
}
|
|
22619
|
+
tatweelsBeforeIndex += tatweelCount;
|
|
22620
|
+
}
|
|
22621
|
+
return false;
|
|
22622
|
+
}
|
|
22623
|
+
|
|
22624
|
+
/**
|
|
22625
|
+
* Get the total number of tatweel characters inserted in a line.
|
|
22626
|
+
* @param lineIndex - The line index
|
|
22627
|
+
* @returns Total number of tatweels in this line
|
|
22628
|
+
*/
|
|
22629
|
+
_getTatweelCountForLine(lineIndex) {
|
|
22630
|
+
var _this$__kashidaInfo4;
|
|
22631
|
+
const kashidaInfo = (_this$__kashidaInfo4 = this.__kashidaInfo) === null || _this$__kashidaInfo4 === void 0 ? void 0 : _this$__kashidaInfo4[lineIndex];
|
|
22632
|
+
if (!kashidaInfo || kashidaInfo.length === 0) {
|
|
22633
|
+
return 0;
|
|
22634
|
+
}
|
|
22635
|
+
return kashidaInfo.reduce((sum, k) => sum + (k.tatweelCount || 0), 0);
|
|
22636
|
+
}
|
|
22637
|
+
|
|
22638
|
+
/**
|
|
22639
|
+
* Get the original line length (without tatweels).
|
|
22640
|
+
* When kashida is applied, _textLines contains extra tatweel characters.
|
|
22641
|
+
* This returns the length as it would be in the original text.
|
|
22642
|
+
* @param lineIndex - The line index
|
|
22643
|
+
* @returns Original line length without tatweels
|
|
22644
|
+
*/
|
|
22645
|
+
_getOriginalLineLength(lineIndex) {
|
|
22646
|
+
var _this$_textLines$line;
|
|
22647
|
+
const displayLength = ((_this$_textLines$line = this._textLines[lineIndex]) === null || _this$_textLines$line === void 0 ? void 0 : _this$_textLines$line.length) || 0;
|
|
22648
|
+
return displayLength - this._getTatweelCountForLine(lineIndex);
|
|
21924
22649
|
}
|
|
21925
22650
|
|
|
21926
22651
|
/**
|
|
@@ -22964,7 +23689,9 @@ class OverlayEditor {
|
|
|
22964
23689
|
requestAnimationFrame(() => {
|
|
22965
23690
|
if (!this.isDestroyed) {
|
|
22966
23691
|
this.applyOverlayStyle();
|
|
22967
|
-
console.log(
|
|
23692
|
+
// console.log(
|
|
23693
|
+
// '📐 Height changed - rechecking alignment after repositioning:',
|
|
23694
|
+
// );
|
|
22968
23695
|
}
|
|
22969
23696
|
});
|
|
22970
23697
|
}
|
|
@@ -23060,7 +23787,7 @@ class OverlayEditor {
|
|
|
23060
23787
|
|
|
23061
23788
|
// Special handling for text objects loaded from JSON - ensure they're properly initialized
|
|
23062
23789
|
if (target.dirty !== false && target.initDimensions) {
|
|
23063
|
-
console.log('🔧 Ensuring text object is properly initialized before overlay editing');
|
|
23790
|
+
// console.log('🔧 Ensuring text object is properly initialized before overlay editing');
|
|
23064
23791
|
// Force re-initialization if the text object seems to be in a dirty state
|
|
23065
23792
|
target.initDimensions();
|
|
23066
23793
|
}
|
|
@@ -23077,11 +23804,11 @@ class OverlayEditor {
|
|
|
23077
23804
|
const autoDetectedDirection = this.firstStrongDir(this.textarea.value || '');
|
|
23078
23805
|
|
|
23079
23806
|
// DEBUG: Log alignment details
|
|
23080
|
-
console.log('🔍 ALIGNMENT DEBUG:');
|
|
23081
|
-
console.log(' Fabric textAlign:', textAlign);
|
|
23082
|
-
console.log(' Fabric direction:', target.direction);
|
|
23083
|
-
console.log(' Text content:', JSON.stringify(target.text));
|
|
23084
|
-
console.log(' Detected direction:', autoDetectedDirection);
|
|
23807
|
+
// console.log('🔍 ALIGNMENT DEBUG:');
|
|
23808
|
+
// console.log(' Fabric textAlign:', textAlign);
|
|
23809
|
+
// console.log(' Fabric direction:', (target as any).direction);
|
|
23810
|
+
// console.log(' Text content:', JSON.stringify(target.text));
|
|
23811
|
+
// console.log(' Detected direction:', autoDetectedDirection);
|
|
23085
23812
|
|
|
23086
23813
|
// Map fabric.js justify to CSS
|
|
23087
23814
|
if (textAlign.includes('justify')) {
|
|
@@ -23099,7 +23826,7 @@ class OverlayEditor {
|
|
|
23099
23826
|
// If text is RTL but fabric says justify-left, override to justify-right for better UX
|
|
23100
23827
|
if (autoDetectedDirection === 'rtl') {
|
|
23101
23828
|
this.textarea.style.textAlignLast = 'right';
|
|
23102
|
-
console.log(' → Overrode justify-left to justify-right for RTL text');
|
|
23829
|
+
// console.log(' → Overrode justify-left to justify-right for RTL text');
|
|
23103
23830
|
} else {
|
|
23104
23831
|
this.textarea.style.textAlignLast = 'left';
|
|
23105
23832
|
}
|
|
@@ -23107,7 +23834,7 @@ class OverlayEditor {
|
|
|
23107
23834
|
// If text is LTR but fabric says justify-right, override to justify-left for better UX
|
|
23108
23835
|
if (autoDetectedDirection === 'ltr') {
|
|
23109
23836
|
this.textarea.style.textAlignLast = 'left';
|
|
23110
|
-
console.log(' → Overrode justify-right to justify-left for LTR text');
|
|
23837
|
+
// console.log(' → Overrode justify-right to justify-left for LTR text');
|
|
23111
23838
|
} else {
|
|
23112
23839
|
this.textarea.style.textAlignLast = 'right';
|
|
23113
23840
|
}
|
|
@@ -23126,16 +23853,17 @@ class OverlayEditor {
|
|
|
23126
23853
|
// Try to force better justify behavior
|
|
23127
23854
|
this.textarea.style.textJustifyTrim = 'none';
|
|
23128
23855
|
this.textarea.style.textAutospace = 'none';
|
|
23129
|
-
|
|
23856
|
+
|
|
23857
|
+
// console.log(' → Applied justify alignment:', textAlign, 'with last-line:', this.textarea.style.textAlignLast);
|
|
23130
23858
|
} catch (error) {
|
|
23131
|
-
console.warn(' → Justify setup failed, falling back to standard alignment:', error);
|
|
23859
|
+
// console.warn(' → Justify setup failed, falling back to standard alignment:', error);
|
|
23132
23860
|
cssTextAlign = textAlign.replace('justify-', '').replace('justify', 'left');
|
|
23133
23861
|
}
|
|
23134
23862
|
} else {
|
|
23135
23863
|
this.textarea.style.textAlignLast = 'auto';
|
|
23136
23864
|
this.textarea.style.textJustify = 'auto';
|
|
23137
23865
|
this.textarea.style.wordSpacing = 'normal';
|
|
23138
|
-
console.log(' → Applied standard alignment:', cssTextAlign);
|
|
23866
|
+
// console.log(' → Applied standard alignment:', cssTextAlign);
|
|
23139
23867
|
}
|
|
23140
23868
|
this.textarea.style.textAlign = cssTextAlign;
|
|
23141
23869
|
this.textarea.style.color = ((_target$fill = target.fill) === null || _target$fill === void 0 ? void 0 : _target$fill.toString()) || '#000';
|
|
@@ -23161,37 +23889,31 @@ class OverlayEditor {
|
|
|
23161
23889
|
this.textarea.style.hyphens = 'none';
|
|
23162
23890
|
|
|
23163
23891
|
// DEBUG: Log final CSS properties
|
|
23164
|
-
console.log('🎨 FINAL TEXTAREA CSS:');
|
|
23165
|
-
console.log(' textAlign:', this.textarea.style.textAlign);
|
|
23166
|
-
console.log(' textAlignLast:', this.textarea.style.textAlignLast);
|
|
23167
|
-
console.log(' direction:', this.textarea.style.direction);
|
|
23168
|
-
console.log(' unicodeBidi:', this.textarea.style.unicodeBidi);
|
|
23169
|
-
console.log(' width:', this.textarea.style.width);
|
|
23170
|
-
console.log(' textJustify:', this.textarea.style.textJustify);
|
|
23171
|
-
console.log(' wordSpacing:', this.textarea.style.wordSpacing);
|
|
23172
|
-
console.log(' whiteSpace:', this.textarea.style.whiteSpace);
|
|
23892
|
+
// console.log('🎨 FINAL TEXTAREA CSS:');
|
|
23893
|
+
// console.log(' textAlign:', this.textarea.style.textAlign);
|
|
23894
|
+
// console.log(' textAlignLast:', this.textarea.style.textAlignLast);
|
|
23895
|
+
// console.log(' direction:', this.textarea.style.direction);
|
|
23896
|
+
// console.log(' unicodeBidi:', this.textarea.style.unicodeBidi);
|
|
23897
|
+
// console.log(' width:', this.textarea.style.width);
|
|
23898
|
+
// console.log(' textJustify:', (this.textarea.style as any).textJustify);
|
|
23899
|
+
// console.log(' wordSpacing:', (this.textarea.style as any).wordSpacing);
|
|
23900
|
+
// console.log(' whiteSpace:', this.textarea.style.whiteSpace);
|
|
23173
23901
|
|
|
23174
23902
|
// If justify, log Fabric object dimensions for comparison
|
|
23175
|
-
if (textAlign.includes('justify'))
|
|
23176
|
-
var _calcTextWidth, _ref;
|
|
23177
|
-
console.log('🔧 FABRIC OBJECT JUSTIFY INFO:');
|
|
23178
|
-
console.log(' Fabric width:', target.width);
|
|
23179
|
-
console.log(' Fabric calcTextWidth:', (_calcTextWidth = (_ref = target).calcTextWidth) === null || _calcTextWidth === void 0 ? void 0 : _calcTextWidth.call(_ref));
|
|
23180
|
-
console.log(' Fabric textAlign:', target.textAlign);
|
|
23181
|
-
console.log(' Text lines:', target.textLines);
|
|
23182
|
-
}
|
|
23903
|
+
if (textAlign.includes('justify')) ;
|
|
23183
23904
|
|
|
23184
23905
|
// Debug font properties matching
|
|
23185
|
-
console.log('🔤 FONT PROPERTIES COMPARISON:');
|
|
23186
|
-
console.log(' Fabric fontFamily:', target.fontFamily);
|
|
23187
|
-
console.log(' Fabric fontWeight:', target.fontWeight);
|
|
23188
|
-
console.log(' Fabric fontStyle:', target.fontStyle);
|
|
23189
|
-
console.log(' Fabric fontSize:', target.fontSize);
|
|
23190
|
-
console.log(' → Textarea fontFamily:', this.textarea.style.fontFamily);
|
|
23191
|
-
console.log(' → Textarea fontWeight:', this.textarea.style.fontWeight);
|
|
23192
|
-
console.log(' → Textarea fontStyle:', this.textarea.style.fontStyle);
|
|
23193
|
-
console.log(' → Textarea fontSize:', this.textarea.style.fontSize);
|
|
23194
|
-
|
|
23906
|
+
// console.log('🔤 FONT PROPERTIES COMPARISON:');
|
|
23907
|
+
// console.log(' Fabric fontFamily:', target.fontFamily);
|
|
23908
|
+
// console.log(' Fabric fontWeight:', target.fontWeight);
|
|
23909
|
+
// console.log(' Fabric fontStyle:', target.fontStyle);
|
|
23910
|
+
// console.log(' Fabric fontSize:', target.fontSize);
|
|
23911
|
+
// console.log(' → Textarea fontFamily:', this.textarea.style.fontFamily);
|
|
23912
|
+
// console.log(' → Textarea fontWeight:', this.textarea.style.fontWeight);
|
|
23913
|
+
// console.log(' → Textarea fontStyle:', this.textarea.style.fontStyle);
|
|
23914
|
+
// console.log(' → Textarea fontSize:', this.textarea.style.fontSize);
|
|
23915
|
+
|
|
23916
|
+
// console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
23195
23917
|
|
|
23196
23918
|
// Enhanced font rendering to better match fabric.js canvas rendering
|
|
23197
23919
|
// Default to auto for more natural rendering
|
|
@@ -23206,11 +23928,12 @@ class OverlayEditor {
|
|
|
23206
23928
|
if (isBold) {
|
|
23207
23929
|
this.textarea.style.webkitFontSmoothing = 'subpixel-antialiased';
|
|
23208
23930
|
this.textarea.style.mozOsxFontSmoothing = 'unset';
|
|
23209
|
-
console.log('🔤 Applied enhanced bold rendering for better thickness matching');
|
|
23931
|
+
// console.log('🔤 Applied enhanced bold rendering for better thickness matching');
|
|
23210
23932
|
}
|
|
23211
|
-
|
|
23212
|
-
console.log('
|
|
23213
|
-
console.log('
|
|
23933
|
+
|
|
23934
|
+
// console.log('🎨 FONT SMOOTHING APPLIED:');
|
|
23935
|
+
// console.log(' webkitFontSmoothing:', (this.textarea.style as any).webkitFontSmoothing);
|
|
23936
|
+
// console.log(' mozOsxFontSmoothing:', (this.textarea.style as any).mozOsxFontSmoothing);
|
|
23214
23937
|
|
|
23215
23938
|
// Initial bounds are set correctly by Fabric.js - don't force update here
|
|
23216
23939
|
}
|
|
@@ -23239,137 +23962,130 @@ class OverlayEditor {
|
|
|
23239
23962
|
width: canvasBounds.width * zoom,
|
|
23240
23963
|
height: canvasBounds.height * zoom
|
|
23241
23964
|
};
|
|
23242
|
-
|
|
23243
|
-
console.log('
|
|
23244
|
-
|
|
23245
|
-
|
|
23246
|
-
|
|
23247
|
-
|
|
23248
|
-
|
|
23249
|
-
|
|
23250
|
-
|
|
23251
|
-
|
|
23252
|
-
|
|
23253
|
-
|
|
23254
|
-
|
|
23255
|
-
|
|
23256
|
-
|
|
23257
|
-
|
|
23258
|
-
|
|
23259
|
-
|
|
23260
|
-
|
|
23261
|
-
|
|
23965
|
+
|
|
23966
|
+
// console.log('🔍 BOUNDING BOX COMPARISON:');
|
|
23967
|
+
// console.log('📦 Textarea Rect:', {
|
|
23968
|
+
// left: Math.round(textareaRect.left * 100) / 100,
|
|
23969
|
+
// top: Math.round(textareaRect.top * 100) / 100,
|
|
23970
|
+
// width: Math.round(textareaRect.width * 100) / 100,
|
|
23971
|
+
// height: Math.round(textareaRect.height * 100) / 100,
|
|
23972
|
+
// });
|
|
23973
|
+
// console.log('📦 Host Div Rect:', {
|
|
23974
|
+
// left: Math.round(hostRect.left * 100) / 100,
|
|
23975
|
+
// top: Math.round(hostRect.top * 100) / 100,
|
|
23976
|
+
// width: Math.round(hostRect.width * 100) / 100,
|
|
23977
|
+
// height: Math.round(hostRect.height * 100) / 100,
|
|
23978
|
+
// });
|
|
23979
|
+
// console.log('📦 Canvas Object Bounds (screen):', {
|
|
23980
|
+
// left: Math.round(screenObjectBounds.left * 100) / 100,
|
|
23981
|
+
// top: Math.round(screenObjectBounds.top * 100) / 100,
|
|
23982
|
+
// width: Math.round(screenObjectBounds.width * 100) / 100,
|
|
23983
|
+
// height: Math.round(screenObjectBounds.height * 100) / 100,
|
|
23984
|
+
// });
|
|
23985
|
+
// console.log('📦 Canvas Object Bounds (canvas):', canvasBounds);
|
|
23262
23986
|
|
|
23263
23987
|
// Calculate differences
|
|
23264
|
-
|
|
23988
|
+
({
|
|
23265
23989
|
leftDiff: Math.round((hostRect.left - screenObjectBounds.left) * 100) / 100,
|
|
23266
23990
|
topDiff: Math.round((hostRect.top - screenObjectBounds.top) * 100) / 100,
|
|
23267
23991
|
widthDiff: Math.round((hostRect.width - screenObjectBounds.width) * 100) / 100,
|
|
23268
23992
|
heightDiff: Math.round((hostRect.height - screenObjectBounds.height) * 100) / 100
|
|
23269
|
-
};
|
|
23270
|
-
|
|
23993
|
+
});
|
|
23994
|
+
({
|
|
23271
23995
|
leftDiff: Math.round((textareaRect.left - screenObjectBounds.left) * 100) / 100,
|
|
23272
23996
|
topDiff: Math.round((textareaRect.top - screenObjectBounds.top) * 100) / 100,
|
|
23273
23997
|
widthDiff: Math.round((textareaRect.width - screenObjectBounds.width) * 100) / 100,
|
|
23274
23998
|
heightDiff: Math.round((textareaRect.height - screenObjectBounds.height) * 100) / 100
|
|
23275
|
-
};
|
|
23276
|
-
console.log('📏 Host Div vs Canvas Object Diff:', hostVsObject);
|
|
23277
|
-
console.log('📏 Textarea vs Canvas Object Diff:', textareaVsObject);
|
|
23999
|
+
});
|
|
23278
24000
|
|
|
23279
|
-
//
|
|
23280
|
-
|
|
23281
|
-
|
|
23282
|
-
|
|
23283
|
-
|
|
23284
|
-
console.log(
|
|
23285
|
-
|
|
23286
|
-
|
|
24001
|
+
// console.log(
|
|
24002
|
+
// hostAligned
|
|
24003
|
+
// ? '✅ Host Div ALIGNED with canvas object'
|
|
24004
|
+
// : '❌ Host Div MISALIGNED with canvas object',
|
|
24005
|
+
// );
|
|
24006
|
+
// console.log(
|
|
24007
|
+
// textareaAligned
|
|
24008
|
+
// ? '✅ Textarea ALIGNED with canvas object'
|
|
24009
|
+
// : '❌ Textarea MISALIGNED with canvas object',
|
|
24010
|
+
// );
|
|
24011
|
+
// console.log('🔍 Zoom:', zoom, 'Viewport Transform:', vpt);
|
|
24012
|
+
// console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
23287
24013
|
}
|
|
23288
24014
|
|
|
23289
24015
|
/**
|
|
23290
24016
|
* Debug method to compare text wrapping between textarea and Fabric text object
|
|
23291
24017
|
*/
|
|
23292
24018
|
debugTextWrapping() {
|
|
23293
|
-
|
|
24019
|
+
this.target;
|
|
23294
24020
|
const text = this.textarea.value;
|
|
23295
|
-
|
|
23296
|
-
console.log('
|
|
23297
|
-
console.log('📄 Text
|
|
24021
|
+
|
|
24022
|
+
// console.log('📝 TEXT WRAPPING COMPARISON:');
|
|
24023
|
+
// console.log('📄 Text Content:', `"${text}"`);
|
|
24024
|
+
// console.log('📄 Text Length:', text.length);
|
|
23298
24025
|
|
|
23299
24026
|
// Analyze line breaks
|
|
23300
24027
|
const explicitLines = text.split('\n');
|
|
23301
|
-
console.log('📄 Explicit Lines (\\n):', explicitLines.length);
|
|
24028
|
+
// console.log('📄 Explicit Lines (\\n):', explicitLines.length);
|
|
23302
24029
|
explicitLines.forEach((line, i) => {
|
|
23303
|
-
console.log(` Line ${i + 1}: "${line}" (${line.length} chars)`);
|
|
24030
|
+
// console.log(` Line ${i + 1}: "${line}" (${line.length} chars)`);
|
|
23304
24031
|
});
|
|
23305
24032
|
|
|
23306
24033
|
// Get textarea computed styles for wrapping analysis
|
|
23307
24034
|
const textareaStyles = window.getComputedStyle(this.textarea);
|
|
23308
|
-
console.log('📐 Textarea Wrapping Styles:');
|
|
23309
|
-
console.log(' width:', textareaStyles.width);
|
|
23310
|
-
console.log(' fontSize:', textareaStyles.fontSize);
|
|
23311
|
-
console.log(' fontFamily:', textareaStyles.fontFamily);
|
|
23312
|
-
console.log(' fontWeight:', textareaStyles.fontWeight);
|
|
23313
|
-
console.log(' letterSpacing:', textareaStyles.letterSpacing);
|
|
23314
|
-
console.log(' lineHeight:', textareaStyles.lineHeight);
|
|
23315
|
-
console.log(' whiteSpace:', textareaStyles.whiteSpace);
|
|
23316
|
-
console.log(' wordWrap:', textareaStyles.wordWrap);
|
|
23317
|
-
console.log(' overflowWrap:', textareaStyles.overflowWrap);
|
|
23318
|
-
console.log(' direction:', textareaStyles.direction);
|
|
23319
|
-
console.log(' textAlign:', textareaStyles.textAlign);
|
|
24035
|
+
// console.log('📐 Textarea Wrapping Styles:');
|
|
24036
|
+
// console.log(' width:', textareaStyles.width);
|
|
24037
|
+
// console.log(' fontSize:', textareaStyles.fontSize);
|
|
24038
|
+
// console.log(' fontFamily:', textareaStyles.fontFamily);
|
|
24039
|
+
// console.log(' fontWeight:', textareaStyles.fontWeight);
|
|
24040
|
+
// console.log(' letterSpacing:', textareaStyles.letterSpacing);
|
|
24041
|
+
// console.log(' lineHeight:', textareaStyles.lineHeight);
|
|
24042
|
+
// console.log(' whiteSpace:', textareaStyles.whiteSpace);
|
|
24043
|
+
// console.log(' wordWrap:', textareaStyles.wordWrap);
|
|
24044
|
+
// console.log(' overflowWrap:', textareaStyles.overflowWrap);
|
|
24045
|
+
// console.log(' direction:', textareaStyles.direction);
|
|
24046
|
+
// console.log(' textAlign:', textareaStyles.textAlign);
|
|
23320
24047
|
|
|
23321
24048
|
// Get Fabric text object properties for comparison
|
|
23322
|
-
console.log('📐 Fabric Text Object Properties:');
|
|
23323
|
-
console.log(' width:', target.width);
|
|
23324
|
-
console.log(' fontSize:', target.fontSize);
|
|
23325
|
-
console.log(' fontFamily:', target.fontFamily);
|
|
23326
|
-
console.log(' fontWeight:', target.fontWeight);
|
|
23327
|
-
console.log(' charSpacing:', target.charSpacing);
|
|
23328
|
-
console.log(' lineHeight:', target.lineHeight);
|
|
23329
|
-
console.log(' direction:', target.direction);
|
|
23330
|
-
console.log(' textAlign:', target.textAlign);
|
|
23331
|
-
console.log(' scaleX:', target.scaleX);
|
|
23332
|
-
console.log(' scaleY:', target.scaleY);
|
|
24049
|
+
// console.log('📐 Fabric Text Object Properties:');
|
|
24050
|
+
// console.log(' width:', (target as any).width);
|
|
24051
|
+
// console.log(' fontSize:', target.fontSize);
|
|
24052
|
+
// console.log(' fontFamily:', target.fontFamily);
|
|
24053
|
+
// console.log(' fontWeight:', target.fontWeight);
|
|
24054
|
+
// console.log(' charSpacing:', target.charSpacing);
|
|
24055
|
+
// console.log(' lineHeight:', target.lineHeight);
|
|
24056
|
+
// console.log(' direction:', (target as any).direction);
|
|
24057
|
+
// console.log(' textAlign:', (target as any).textAlign);
|
|
24058
|
+
// console.log(' scaleX:', target.scaleX);
|
|
24059
|
+
// console.log(' scaleY:', target.scaleY);
|
|
23333
24060
|
|
|
23334
24061
|
// Calculate effective dimensions for comparison - use actual rendered width
|
|
23335
24062
|
// **THE FIX:** Use getBoundingRect to get the *actual rendered width* of the Fabric object.
|
|
23336
|
-
|
|
24063
|
+
this.target.getBoundingRect().width;
|
|
23337
24064
|
// Use the exact width set on textarea for comparison
|
|
23338
24065
|
const textareaComputedWidth = parseFloat(window.getComputedStyle(this.textarea).width);
|
|
23339
|
-
|
|
23340
|
-
|
|
23341
|
-
console.log('
|
|
23342
|
-
console.log('
|
|
23343
|
-
console.log('
|
|
23344
|
-
console.log('
|
|
23345
|
-
console.log(
|
|
23346
|
-
|
|
23347
|
-
// Check text direction and bidi handling
|
|
23348
|
-
const hasRTLText = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
23349
|
-
const hasBidiText = /[\u0590-\u06FF]/.test(text) && /[a-zA-Z]/.test(text);
|
|
23350
|
-
console.log('🌍 Text Direction Analysis:');
|
|
23351
|
-
console.log(' Has RTL characters:', hasRTLText);
|
|
23352
|
-
console.log(' Has mixed Bidi text:', hasBidiText);
|
|
23353
|
-
console.log(' Textarea direction:', textareaStyles.direction);
|
|
23354
|
-
console.log(' Fabric direction:', target.direction || 'auto');
|
|
23355
|
-
console.log(' Textarea unicodeBidi:', textareaStyles.unicodeBidi);
|
|
24066
|
+
textareaComputedWidth / this.canvas.getZoom();
|
|
24067
|
+
|
|
24068
|
+
// console.log('🌍 Text Direction Analysis:');
|
|
24069
|
+
// console.log(' Has RTL characters:', hasRTLText);
|
|
24070
|
+
// console.log(' Has mixed Bidi text:', hasBidiText);
|
|
24071
|
+
// console.log(' Textarea direction:', textareaStyles.direction);
|
|
24072
|
+
// console.log(' Fabric direction:', (target as any).direction || 'auto');
|
|
24073
|
+
// console.log(' Textarea unicodeBidi:', textareaStyles.unicodeBidi);
|
|
23356
24074
|
|
|
23357
24075
|
// Measure actual rendered line count
|
|
23358
24076
|
const textareaScrollHeight = this.textarea.scrollHeight;
|
|
23359
24077
|
const textareaLineHeight = parseFloat(textareaStyles.lineHeight) || parseFloat(textareaStyles.fontSize) * 1.2;
|
|
23360
24078
|
const estimatedTextareaLines = Math.round(textareaScrollHeight / textareaLineHeight);
|
|
23361
|
-
|
|
23362
|
-
console.log('
|
|
23363
|
-
console.log(' Textarea
|
|
23364
|
-
console.log('
|
|
23365
|
-
console.log('
|
|
23366
|
-
|
|
23367
|
-
|
|
23368
|
-
|
|
23369
|
-
|
|
23370
|
-
|
|
23371
|
-
}
|
|
23372
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
24079
|
+
|
|
24080
|
+
// console.log('📊 Line Count Analysis:');
|
|
24081
|
+
// console.log(' Textarea scrollHeight:', textareaScrollHeight);
|
|
24082
|
+
// console.log(' Textarea lineHeight:', textareaLineHeight);
|
|
24083
|
+
// console.log(' Estimated rendered lines:', estimatedTextareaLines);
|
|
24084
|
+
// console.log(' Explicit line breaks:', explicitLines.length);
|
|
24085
|
+
|
|
24086
|
+
if (estimatedTextareaLines > explicitLines.length) ;
|
|
24087
|
+
|
|
24088
|
+
// console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
23373
24089
|
}
|
|
23374
24090
|
|
|
23375
24091
|
/**
|
|
@@ -23458,17 +24174,16 @@ class OverlayEditor {
|
|
|
23458
24174
|
|
|
23459
24175
|
// Only update direction when not explicitly set on the object
|
|
23460
24176
|
if (!hasExplicitDirection && detectedDirection && detectedDirection !== currentDirection) {
|
|
23461
|
-
console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
|
|
23462
|
-
console.log(` Text content: "${finalText.substring(0, 50)}..."`);
|
|
24177
|
+
// console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
|
|
24178
|
+
// console.log(` Text content: "${finalText.substring(0, 50)}..."`);
|
|
23463
24179
|
|
|
23464
24180
|
// Update the fabric object's direction
|
|
23465
24181
|
this.target.set('direction', detectedDirection);
|
|
23466
24182
|
|
|
23467
24183
|
// Force a re-render to apply the direction change
|
|
23468
24184
|
this.canvas.requestRenderAll();
|
|
23469
|
-
|
|
23470
|
-
|
|
23471
|
-
console.log(`📝 Overlay Exit: Direction unchanged (${currentDirection}), text: "${finalText.substring(0, 30)}..."`);
|
|
24185
|
+
|
|
24186
|
+
// console.log(`✅ Fabric object direction updated to: ${detectedDirection}`);
|
|
23472
24187
|
}
|
|
23473
24188
|
if (this.onCommit) {
|
|
23474
24189
|
this.onCommit(finalText);
|
|
@@ -23678,7 +24393,11 @@ function enterTextOverlayEdit(canvas, target, options) {
|
|
|
23678
24393
|
* - `\-` Matches a "-" character (char code 45).
|
|
23679
24394
|
*/
|
|
23680
24395
|
// eslint-disable-next-line no-useless-escape
|
|
23681
|
-
|
|
24396
|
+
// Word boundary characters for Latin, Arabic, and Hebrew
|
|
24397
|
+
// Latin: space, newline, punctuation
|
|
24398
|
+
// Arabic: ، (comma U+060C), ؛ (semicolon U+061B), ؟ (question U+061F), ۔ (full stop U+06D4), ـ (tatweel U+0640)
|
|
24399
|
+
// Hebrew: ׃ (sof pasuq U+05C3), ״ (gershayim U+05F4)
|
|
24400
|
+
const reNonWord = /[ \n\.,;!\?\-\u060C\u061B\u061F\u06D4\u0640\u05C3\u05F4\u2000-\u206F]/;
|
|
23682
24401
|
class ITextBehavior extends FabricText {
|
|
23683
24402
|
constructor() {
|
|
23684
24403
|
super(...arguments);
|
|
@@ -23899,12 +24618,99 @@ class ITextBehavior extends FabricText {
|
|
|
23899
24618
|
|
|
23900
24619
|
/**
|
|
23901
24620
|
* Finds index corresponding to beginning or end of a word
|
|
24621
|
+
* Uses Intl.Segmenter for proper Unicode word segmentation when available,
|
|
24622
|
+
* falls back to regex-based detection for older browsers.
|
|
23902
24623
|
* @param {Number} selectionStart Index of a character
|
|
23903
24624
|
* @param {Number} direction 1 or -1
|
|
23904
24625
|
* @return {Number} Index of the beginning or end of a word
|
|
23905
24626
|
*/
|
|
23906
24627
|
searchWordBoundary(selectionStart, direction) {
|
|
23907
|
-
|
|
24628
|
+
// Try to use Intl.Segmenter for proper Unicode word segmentation
|
|
24629
|
+
if (typeof Intl !== 'undefined' && Intl.Segmenter) {
|
|
24630
|
+
return this._searchWordBoundaryWithSegmenter(selectionStart, direction);
|
|
24631
|
+
}
|
|
24632
|
+
// Fallback to regex-based detection
|
|
24633
|
+
return this._searchWordBoundaryWithRegex(selectionStart, direction);
|
|
24634
|
+
}
|
|
24635
|
+
|
|
24636
|
+
/**
|
|
24637
|
+
* Word boundary search using Intl.Segmenter (proper Unicode support)
|
|
24638
|
+
* Works on original text (this.text) since selectionStart is in original text space
|
|
24639
|
+
*/
|
|
24640
|
+
_searchWordBoundaryWithSegmenter(selectionStart, direction) {
|
|
24641
|
+
// Use original text (without kashida) since indices are in original text space
|
|
24642
|
+
const originalText = this.text;
|
|
24643
|
+
const SegmenterClass = Intl.Segmenter;
|
|
24644
|
+
const segmenter = new SegmenterClass(undefined, {
|
|
24645
|
+
granularity: 'word'
|
|
24646
|
+
});
|
|
24647
|
+
const segments = Array.from(segmenter.segment(originalText));
|
|
24648
|
+
if (segments.length === 0) {
|
|
24649
|
+
return direction === -1 ? 0 : originalText.length;
|
|
24650
|
+
}
|
|
24651
|
+
|
|
24652
|
+
// Find the segment containing the cursor position
|
|
24653
|
+
let currentSegmentIdx = 0;
|
|
24654
|
+
for (let i = 0; i < segments.length; i++) {
|
|
24655
|
+
const seg = segments[i];
|
|
24656
|
+
if (selectionStart >= seg.index && selectionStart < seg.index + seg.segment.length) {
|
|
24657
|
+
currentSegmentIdx = i;
|
|
24658
|
+
break;
|
|
24659
|
+
}
|
|
24660
|
+
if (selectionStart >= seg.index + seg.segment.length) {
|
|
24661
|
+
currentSegmentIdx = i;
|
|
24662
|
+
}
|
|
24663
|
+
}
|
|
24664
|
+
|
|
24665
|
+
// Find word boundaries
|
|
24666
|
+
if (direction === -1) {
|
|
24667
|
+
// Search backwards for word start
|
|
24668
|
+
let targetIdx = currentSegmentIdx;
|
|
24669
|
+
|
|
24670
|
+
// If cursor is at the start of a segment, look at previous segment
|
|
24671
|
+
if (selectionStart === segments[targetIdx].index && targetIdx > 0) {
|
|
24672
|
+
targetIdx--;
|
|
24673
|
+
}
|
|
24674
|
+
|
|
24675
|
+
// Skip non-word segments
|
|
24676
|
+
while (targetIdx > 0 && !segments[targetIdx].isWordLike) {
|
|
24677
|
+
targetIdx--;
|
|
24678
|
+
}
|
|
24679
|
+
|
|
24680
|
+
// Return the start of the word segment
|
|
24681
|
+
if (segments[targetIdx].isWordLike) {
|
|
24682
|
+
return segments[targetIdx].index;
|
|
24683
|
+
}
|
|
24684
|
+
return 0;
|
|
24685
|
+
} else {
|
|
24686
|
+
// Search forwards for word end
|
|
24687
|
+
let targetIdx = currentSegmentIdx;
|
|
24688
|
+
|
|
24689
|
+
// If we're in a word, find its end
|
|
24690
|
+
if (segments[targetIdx].isWordLike) {
|
|
24691
|
+
return segments[targetIdx].index + segments[targetIdx].segment.length;
|
|
24692
|
+
}
|
|
24693
|
+
|
|
24694
|
+
// Skip non-word segments to find next word
|
|
24695
|
+
while (targetIdx < segments.length && !segments[targetIdx].isWordLike) {
|
|
24696
|
+
targetIdx++;
|
|
24697
|
+
}
|
|
24698
|
+
|
|
24699
|
+
// Return the end of the next word segment
|
|
24700
|
+
if (targetIdx < segments.length && segments[targetIdx].isWordLike) {
|
|
24701
|
+
return segments[targetIdx].index + segments[targetIdx].segment.length;
|
|
24702
|
+
}
|
|
24703
|
+
return originalText.length;
|
|
24704
|
+
}
|
|
24705
|
+
}
|
|
24706
|
+
|
|
24707
|
+
/**
|
|
24708
|
+
* Word boundary search using regex (fallback for older browsers)
|
|
24709
|
+
* Works on original text (this.text) since selectionStart is in original text space
|
|
24710
|
+
*/
|
|
24711
|
+
_searchWordBoundaryWithRegex(selectionStart, direction) {
|
|
24712
|
+
// Use original text as an array of characters
|
|
24713
|
+
const text = Array.from(this.text);
|
|
23908
24714
|
// if we land on a space we move the cursor backwards
|
|
23909
24715
|
// if we are searching boundary end we move the cursor backwards ONLY if we don't land on a line break
|
|
23910
24716
|
let index = selectionStart > 0 && this._reSpace.test(text[selectionStart]) && (direction === -1 || !reNewline.test(text[selectionStart - 1])) ? selectionStart - 1 : selectionStart,
|
|
@@ -24019,8 +24825,8 @@ class ITextBehavior extends FabricText {
|
|
|
24019
24825
|
});
|
|
24020
24826
|
}
|
|
24021
24827
|
|
|
24022
|
-
/**
|
|
24023
|
-
* Commit overlay editing changes
|
|
24828
|
+
/**
|
|
24829
|
+
* Commit overlay editing changes
|
|
24024
24830
|
*/
|
|
24025
24831
|
commitOverlayEdit(text) {
|
|
24026
24832
|
// Preserve geometry to avoid nudge when layout recalculates
|
|
@@ -24031,17 +24837,17 @@ class ITextBehavior extends FabricText {
|
|
|
24031
24837
|
const prevUsingBrowserWrap = this._usingBrowserWrapping;
|
|
24032
24838
|
const hadLock = this.lockDynamicMinWidth;
|
|
24033
24839
|
this.lockDynamicMinWidth = true;
|
|
24034
|
-
|
|
24035
|
-
|
|
24036
|
-
|
|
24037
|
-
|
|
24038
|
-
|
|
24039
|
-
|
|
24040
|
-
|
|
24041
|
-
|
|
24042
|
-
|
|
24043
|
-
|
|
24044
|
-
|
|
24840
|
+
// console.log('[OverlayCommit] pre-layout', {
|
|
24841
|
+
// textLength: text?.length,
|
|
24842
|
+
// kashidas: countKashida(text),
|
|
24843
|
+
// prevWidth,
|
|
24844
|
+
// prevMinWidth,
|
|
24845
|
+
// prevUsingBrowserWrap,
|
|
24846
|
+
// hadLock,
|
|
24847
|
+
// dir: (this as any).direction,
|
|
24848
|
+
// align: (this as any).textAlign,
|
|
24849
|
+
// });
|
|
24850
|
+
|
|
24045
24851
|
const overlayEditor = this.__overlayEditor;
|
|
24046
24852
|
if (overlayEditor) {
|
|
24047
24853
|
// Extract browser lines for pixel-perfect rendering
|
|
@@ -24049,7 +24855,7 @@ class ITextBehavior extends FabricText {
|
|
|
24049
24855
|
const result = extractLinesFromDOM(overlayEditor.textareaElement);
|
|
24050
24856
|
storeBrowserLines(this, result.lines);
|
|
24051
24857
|
} catch (error) {
|
|
24052
|
-
console.warn('Failed to extract browser lines:', error);
|
|
24858
|
+
// console.warn('Failed to extract browser lines:', error);
|
|
24053
24859
|
}
|
|
24054
24860
|
}
|
|
24055
24861
|
|
|
@@ -24065,15 +24871,15 @@ class ITextBehavior extends FabricText {
|
|
|
24065
24871
|
}
|
|
24066
24872
|
this.dirty = true;
|
|
24067
24873
|
this.initDimensions();
|
|
24068
|
-
console.log('[OverlayCommit] post-layout', {
|
|
24069
|
-
|
|
24070
|
-
|
|
24071
|
-
|
|
24072
|
-
|
|
24073
|
-
|
|
24074
|
-
|
|
24075
|
-
|
|
24076
|
-
});
|
|
24874
|
+
// console.log('[OverlayCommit] post-layout', {
|
|
24875
|
+
// width: this.get('width'),
|
|
24876
|
+
// dynMinWidth: (this as any).dynamicMinWidth,
|
|
24877
|
+
// usingBrowserWrap: (this as any)._usingBrowserWrapping,
|
|
24878
|
+
// lockDynamicMinWidth: (this as any).lockDynamicMinWidth,
|
|
24879
|
+
// kashidas: countKashida(this.text),
|
|
24880
|
+
// left: this.left,
|
|
24881
|
+
// top: this.top,
|
|
24882
|
+
// });
|
|
24077
24883
|
// Restore geometry after layout so the object doesn't drift
|
|
24078
24884
|
this.set({
|
|
24079
24885
|
left: prevLeft,
|
|
@@ -24083,13 +24889,13 @@ class ITextBehavior extends FabricText {
|
|
|
24083
24889
|
this.setCoords();
|
|
24084
24890
|
this.exitEditing();
|
|
24085
24891
|
this.lockDynamicMinWidth = hadLock;
|
|
24086
|
-
console.log('[OverlayCommit] final', {
|
|
24087
|
-
|
|
24088
|
-
|
|
24089
|
-
|
|
24090
|
-
|
|
24091
|
-
|
|
24092
|
-
});
|
|
24892
|
+
// console.log('[OverlayCommit] final', {
|
|
24893
|
+
// width: this.get('width'),
|
|
24894
|
+
// dynMinWidth: (this as any).dynamicMinWidth,
|
|
24895
|
+
// lockRestored: hadLock,
|
|
24896
|
+
// left: this.left,
|
|
24897
|
+
// top: this.top,
|
|
24898
|
+
// });
|
|
24093
24899
|
this.fire('changed');
|
|
24094
24900
|
this.canvas && this.canvas.requestRenderAll();
|
|
24095
24901
|
}
|
|
@@ -24189,7 +24995,7 @@ class ITextBehavior extends FabricText {
|
|
|
24189
24995
|
* @private
|
|
24190
24996
|
*/
|
|
24191
24997
|
_updateTextarea() {
|
|
24192
|
-
console.log('🔤 _updateTextarea called with fabric text:', this.text);
|
|
24998
|
+
// console.log('🔤 _updateTextarea called with fabric text:', this.text);
|
|
24193
24999
|
this.cursorOffsetCache = {};
|
|
24194
25000
|
if (!this.hiddenTextarea) {
|
|
24195
25001
|
return;
|
|
@@ -24198,9 +25004,9 @@ class ITextBehavior extends FabricText {
|
|
|
24198
25004
|
// Sync textarea content with fabric text to prevent double-keypress issues
|
|
24199
25005
|
const currentFabricText = this.text;
|
|
24200
25006
|
if (this.hiddenTextarea.value !== currentFabricText) {
|
|
24201
|
-
console.log('🔤 _updateTextarea: syncing textarea to fabric text');
|
|
24202
|
-
console.log('🔤 _updateTextarea: textarea was:', this.hiddenTextarea.value);
|
|
24203
|
-
console.log('🔤 _updateTextarea: fabric is:', currentFabricText);
|
|
25007
|
+
// console.log('🔤 _updateTextarea: syncing textarea to fabric text');
|
|
25008
|
+
// console.log('🔤 _updateTextarea: textarea was:', this.hiddenTextarea.value);
|
|
25009
|
+
// console.log('🔤 _updateTextarea: fabric is:', currentFabricText);
|
|
24204
25010
|
this.hiddenTextarea.value = currentFabricText;
|
|
24205
25011
|
}
|
|
24206
25012
|
if (!this.inCompositionMode) {
|
|
@@ -24851,34 +25657,29 @@ class ITextKeyBehavior extends ITextBehavior {
|
|
|
24851
25657
|
}
|
|
24852
25658
|
|
|
24853
25659
|
// Debug log to track the double keypress issue
|
|
24854
|
-
console.log('🔤 onInput debug:', {
|
|
24855
|
-
|
|
24856
|
-
|
|
24857
|
-
|
|
24858
|
-
|
|
24859
|
-
|
|
24860
|
-
|
|
24861
|
-
|
|
24862
|
-
start: selectionStart,
|
|
24863
|
-
end: selectionEnd
|
|
24864
|
-
},
|
|
24865
|
-
fromPaste,
|
|
24866
|
-
inComposition: this.inCompositionMode
|
|
24867
|
-
});
|
|
25660
|
+
// console.log('🔤 onInput debug:', {
|
|
25661
|
+
// fabricText: this.text,
|
|
25662
|
+
// textareaValue: value,
|
|
25663
|
+
// fabricSelection: { start: this.selectionStart, end: this.selectionEnd },
|
|
25664
|
+
// textareaSelection: { start: selectionStart, end: selectionEnd },
|
|
25665
|
+
// fromPaste,
|
|
25666
|
+
// inComposition: this.inCompositionMode
|
|
25667
|
+
// });
|
|
24868
25668
|
|
|
24869
25669
|
// Immediate sync for simple character replacement - fix for double keypress issue
|
|
24870
25670
|
if (this.text !== value && !this.inCompositionMode) {
|
|
24871
|
-
console.log('🔤 Immediate sync: fabric text differs from textarea, syncing immediately');
|
|
24872
|
-
console.log('🔤 Before sync - fabric text:', this.text);
|
|
24873
|
-
console.log('🔤 Before sync - textarea value:', value);
|
|
24874
|
-
console.log('🔤 fromPaste:', fromPaste);
|
|
25671
|
+
// console.log('🔤 Immediate sync: fabric text differs from textarea, syncing immediately');
|
|
25672
|
+
// console.log('🔤 Before sync - fabric text:', this.text);
|
|
25673
|
+
// console.log('🔤 Before sync - textarea value:', value);
|
|
25674
|
+
// console.log('🔤 fromPaste:', fromPaste);
|
|
24875
25675
|
|
|
24876
25676
|
// Clear all relevant caches that might prevent visual updates
|
|
24877
25677
|
this.cursorOffsetCache = {};
|
|
24878
25678
|
this._browserWrapCache = null;
|
|
24879
25679
|
this._lastDimensionState = null;
|
|
24880
25680
|
this._forceClearCache = true;
|
|
24881
|
-
|
|
25681
|
+
|
|
25682
|
+
// console.log('🔤 Cleared all caches');
|
|
24882
25683
|
|
|
24883
25684
|
// Use the same logic as updateAndFire but immediately
|
|
24884
25685
|
this.updateFromTextArea();
|
|
@@ -24891,8 +25692,9 @@ class ITextKeyBehavior extends ITextBehavior {
|
|
|
24891
25692
|
// Remove requestRenderAll() which queues for next animation frame
|
|
24892
25693
|
this.canvas.renderAll();
|
|
24893
25694
|
}
|
|
24894
|
-
|
|
24895
|
-
console.log('🔤
|
|
25695
|
+
|
|
25696
|
+
// console.log('🔤 After updateFromTextArea - fabric text:', this.text);
|
|
25697
|
+
// console.log('🔤 Sync complete, caches cleared, synchronous render only');
|
|
24896
25698
|
return;
|
|
24897
25699
|
}
|
|
24898
25700
|
const updateAndFire = () => {
|
|
@@ -25536,8 +26338,10 @@ class ITextClickBehavior extends ITextKeyBehavior {
|
|
|
25536
26338
|
// use character left positions which are always increasing even with RTL segments
|
|
25537
26339
|
|
|
25538
26340
|
for (let j = 0; j < charLength; j++) {
|
|
26341
|
+
var _chars$left, _chars;
|
|
25539
26342
|
const charStart = lineLeftOffset + chars[j].left;
|
|
25540
|
-
|
|
26343
|
+
// For last character, use its width to calculate end position
|
|
26344
|
+
const charEnd = lineLeftOffset + ((_chars$left = (_chars = chars[j + 1]) === null || _chars === void 0 ? void 0 : _chars.left) !== null && _chars$left !== void 0 ? _chars$left : chars[j].left + chars[j].kernedWidth);
|
|
25541
26345
|
const charMiddle = (charStart + charEnd) / 2;
|
|
25542
26346
|
if (mouseOffset.x <= charMiddle) {
|
|
25543
26347
|
charIndex = lineStartIndex + j;
|
|
@@ -25548,9 +26352,24 @@ class ITextClickBehavior extends ITextKeyBehavior {
|
|
|
25548
26352
|
}
|
|
25549
26353
|
charIndex = lineStartIndex + charLength;
|
|
25550
26354
|
}
|
|
25551
|
-
|
|
25552
|
-
|
|
25553
|
-
|
|
26355
|
+
let lineCharIndex = charIndex - lineStartIndex;
|
|
26356
|
+
|
|
26357
|
+
// Handle flipX
|
|
26358
|
+
if (this.flipX) {
|
|
26359
|
+
lineCharIndex = charLength - lineCharIndex;
|
|
26360
|
+
}
|
|
26361
|
+
|
|
26362
|
+
// Convert display index to original index (handles kashida)
|
|
26363
|
+
const originalLineCharIndex = this._displayToOriginalIndex(lineIndex, lineCharIndex);
|
|
26364
|
+
|
|
26365
|
+
// Calculate original line start (sum of original line lengths before this line)
|
|
26366
|
+
let originalLineStart = 0;
|
|
26367
|
+
for (let i = 0; i < lineIndex; i++) {
|
|
26368
|
+
const originalLineLength = this._getOriginalLineLength(i);
|
|
26369
|
+
originalLineStart += originalLineLength + this.missingNewlineOffset(i);
|
|
26370
|
+
}
|
|
26371
|
+
const originalIndex = originalLineStart + originalLineCharIndex;
|
|
26372
|
+
return Math.min(originalIndex, this.text.length);
|
|
25554
26373
|
}
|
|
25555
26374
|
}
|
|
25556
26375
|
|
|
@@ -25561,50 +26380,6 @@ class ITextClickBehavior extends ITextKeyBehavior {
|
|
|
25561
26380
|
* for interactive text editing with grapheme-aware boundaries.
|
|
25562
26381
|
*/
|
|
25563
26382
|
|
|
25564
|
-
/**
|
|
25565
|
-
* Hit test a point against laid out text to find insertion position
|
|
25566
|
-
*/
|
|
25567
|
-
function hitTest(x, y, layout, options) {
|
|
25568
|
-
if (layout.lines.length === 0) {
|
|
25569
|
-
return {
|
|
25570
|
-
lineIndex: 0,
|
|
25571
|
-
charIndex: 0,
|
|
25572
|
-
graphemeIndex: 0,
|
|
25573
|
-
isAtLineEnd: true,
|
|
25574
|
-
isAtTextEnd: true,
|
|
25575
|
-
insertionIndex: 0
|
|
25576
|
-
};
|
|
25577
|
-
}
|
|
25578
|
-
|
|
25579
|
-
// Find the line containing the y coordinate
|
|
25580
|
-
const lineResult = findLineAtY(y, layout.lines);
|
|
25581
|
-
const line = layout.lines[lineResult.lineIndex];
|
|
25582
|
-
if (!line || line.bounds.length === 0) {
|
|
25583
|
-
return {
|
|
25584
|
-
lineIndex: lineResult.lineIndex,
|
|
25585
|
-
charIndex: 0,
|
|
25586
|
-
graphemeIndex: 0,
|
|
25587
|
-
isAtLineEnd: true,
|
|
25588
|
-
isAtTextEnd: lineResult.lineIndex >= layout.lines.length - 1,
|
|
25589
|
-
insertionIndex: calculateInsertionIndex(lineResult.lineIndex, 0, layout)
|
|
25590
|
-
};
|
|
25591
|
-
}
|
|
25592
|
-
|
|
25593
|
-
// Find the character position within the line
|
|
25594
|
-
const charResult = findCharAtX(x, line);
|
|
25595
|
-
|
|
25596
|
-
// Calculate total insertion index
|
|
25597
|
-
const insertionIndex = calculateInsertionIndex(lineResult.lineIndex, charResult.graphemeIndex, layout);
|
|
25598
|
-
return {
|
|
25599
|
-
lineIndex: lineResult.lineIndex,
|
|
25600
|
-
charIndex: charResult.charIndex,
|
|
25601
|
-
graphemeIndex: charResult.graphemeIndex,
|
|
25602
|
-
isAtLineEnd: charResult.isAtLineEnd,
|
|
25603
|
-
isAtTextEnd: lineResult.lineIndex >= layout.lines.length - 1 && charResult.isAtLineEnd,
|
|
25604
|
-
insertionIndex,
|
|
25605
|
-
closestBound: charResult.closestBound
|
|
25606
|
-
};
|
|
25607
|
-
}
|
|
25608
26383
|
|
|
25609
26384
|
/**
|
|
25610
26385
|
* Get cursor rectangle for a given insertion index
|
|
@@ -25652,159 +26427,6 @@ function getCursorRect(insertionIndex, layout, options) {
|
|
|
25652
26427
|
};
|
|
25653
26428
|
}
|
|
25654
26429
|
|
|
25655
|
-
// Private helper functions
|
|
25656
|
-
|
|
25657
|
-
/**
|
|
25658
|
-
* Find which line contains the given Y coordinate
|
|
25659
|
-
*/
|
|
25660
|
-
function findLineAtY(y, lines) {
|
|
25661
|
-
var _lines;
|
|
25662
|
-
let currentY = 0;
|
|
25663
|
-
for (let i = 0; i < lines.length; i++) {
|
|
25664
|
-
const line = lines[i];
|
|
25665
|
-
if (y >= currentY && y < currentY + line.height) {
|
|
25666
|
-
return {
|
|
25667
|
-
lineIndex: i,
|
|
25668
|
-
offsetY: y - currentY
|
|
25669
|
-
};
|
|
25670
|
-
}
|
|
25671
|
-
currentY += line.height;
|
|
25672
|
-
}
|
|
25673
|
-
|
|
25674
|
-
// Y is past all lines - return last line
|
|
25675
|
-
return {
|
|
25676
|
-
lineIndex: lines.length - 1,
|
|
25677
|
-
offsetY: ((_lines = lines[lines.length - 1]) === null || _lines === void 0 ? void 0 : _lines.height) || 0
|
|
25678
|
-
};
|
|
25679
|
-
}
|
|
25680
|
-
|
|
25681
|
-
/**
|
|
25682
|
-
* Find character position within a line at given X coordinate
|
|
25683
|
-
*/
|
|
25684
|
-
function findCharAtX(x, line, options) {
|
|
25685
|
-
if (line.bounds.length === 0) {
|
|
25686
|
-
return {
|
|
25687
|
-
charIndex: 0,
|
|
25688
|
-
graphemeIndex: 0,
|
|
25689
|
-
isAtLineEnd: true
|
|
25690
|
-
};
|
|
25691
|
-
}
|
|
25692
|
-
|
|
25693
|
-
// Create visual ordering: sort bounds by visual X position (left-to-right)
|
|
25694
|
-
// This handles mixed LTR/RTL content where visual order != logical order
|
|
25695
|
-
const visualBounds = line.bounds.map((bound, logicalIndex) => ({
|
|
25696
|
-
bound,
|
|
25697
|
-
logicalIndex,
|
|
25698
|
-
visualX: bound.x,
|
|
25699
|
-
visualXEnd: bound.x + bound.kernedWidth
|
|
25700
|
-
})).sort((a, b) => a.visualX - b.visualX);
|
|
25701
|
-
|
|
25702
|
-
// Find leftmost and rightmost visual positions
|
|
25703
|
-
const leftmostX = visualBounds[0].visualX;
|
|
25704
|
-
const rightmostX = visualBounds[visualBounds.length - 1].visualXEnd;
|
|
25705
|
-
|
|
25706
|
-
// Handle clicks before the line starts
|
|
25707
|
-
if (x < leftmostX) {
|
|
25708
|
-
// Find the character that appears visually first
|
|
25709
|
-
const firstVisualBound = visualBounds[0];
|
|
25710
|
-
return {
|
|
25711
|
-
charIndex: firstVisualBound.bound.charIndex,
|
|
25712
|
-
graphemeIndex: firstVisualBound.bound.graphemeIndex,
|
|
25713
|
-
isAtLineEnd: false,
|
|
25714
|
-
closestBound: firstVisualBound.bound
|
|
25715
|
-
};
|
|
25716
|
-
}
|
|
25717
|
-
|
|
25718
|
-
// Handle clicks after the line ends
|
|
25719
|
-
if (x >= rightmostX) {
|
|
25720
|
-
// Find the character that appears visually last
|
|
25721
|
-
const lastVisualBound = visualBounds[visualBounds.length - 1];
|
|
25722
|
-
return {
|
|
25723
|
-
charIndex: lastVisualBound.bound.charIndex + 1,
|
|
25724
|
-
graphemeIndex: lastVisualBound.bound.graphemeIndex + 1,
|
|
25725
|
-
isAtLineEnd: true,
|
|
25726
|
-
closestBound: lastVisualBound.bound
|
|
25727
|
-
};
|
|
25728
|
-
}
|
|
25729
|
-
|
|
25730
|
-
// Find the character containing the X coordinate
|
|
25731
|
-
for (let i = 0; i < visualBounds.length; i++) {
|
|
25732
|
-
const {
|
|
25733
|
-
bound,
|
|
25734
|
-
visualX,
|
|
25735
|
-
visualXEnd
|
|
25736
|
-
} = visualBounds[i];
|
|
25737
|
-
if (x >= visualX && x < visualXEnd) {
|
|
25738
|
-
// Determine if closer to start or end of character
|
|
25739
|
-
const midpoint = visualX + (visualXEnd - visualX) / 2;
|
|
25740
|
-
const insertBeforeChar = x < midpoint;
|
|
25741
|
-
if (insertBeforeChar) {
|
|
25742
|
-
return {
|
|
25743
|
-
charIndex: bound.charIndex,
|
|
25744
|
-
graphemeIndex: bound.graphemeIndex,
|
|
25745
|
-
isAtLineEnd: false,
|
|
25746
|
-
closestBound: bound
|
|
25747
|
-
};
|
|
25748
|
-
} else {
|
|
25749
|
-
// Insert after this character
|
|
25750
|
-
return {
|
|
25751
|
-
charIndex: bound.charIndex + 1,
|
|
25752
|
-
graphemeIndex: bound.graphemeIndex + 1,
|
|
25753
|
-
isAtLineEnd: false,
|
|
25754
|
-
closestBound: bound
|
|
25755
|
-
};
|
|
25756
|
-
}
|
|
25757
|
-
}
|
|
25758
|
-
|
|
25759
|
-
// Check if x is in the gap between this character and the next
|
|
25760
|
-
if (i < visualBounds.length - 1) {
|
|
25761
|
-
const nextVisual = visualBounds[i + 1];
|
|
25762
|
-
if (x >= visualXEnd && x < nextVisual.visualX) {
|
|
25763
|
-
// Click in gap - place cursor after current character
|
|
25764
|
-
return {
|
|
25765
|
-
charIndex: bound.charIndex + 1,
|
|
25766
|
-
graphemeIndex: bound.graphemeIndex + 1,
|
|
25767
|
-
isAtLineEnd: false,
|
|
25768
|
-
closestBound: bound
|
|
25769
|
-
};
|
|
25770
|
-
}
|
|
25771
|
-
}
|
|
25772
|
-
}
|
|
25773
|
-
|
|
25774
|
-
// Fallback - find closest character
|
|
25775
|
-
const closestBound = visualBounds.reduce((closest, current) => {
|
|
25776
|
-
const closestDistance = Math.abs((closest.visualX + closest.visualXEnd) / 2 - x);
|
|
25777
|
-
const currentDistance = Math.abs((current.visualX + current.visualXEnd) / 2 - x);
|
|
25778
|
-
return currentDistance < closestDistance ? current : closest;
|
|
25779
|
-
});
|
|
25780
|
-
return {
|
|
25781
|
-
charIndex: closestBound.bound.charIndex,
|
|
25782
|
-
graphemeIndex: closestBound.bound.graphemeIndex,
|
|
25783
|
-
isAtLineEnd: false,
|
|
25784
|
-
closestBound: closestBound.bound
|
|
25785
|
-
};
|
|
25786
|
-
}
|
|
25787
|
-
|
|
25788
|
-
/**
|
|
25789
|
-
* Calculate total insertion index from line and character indices
|
|
25790
|
-
*/
|
|
25791
|
-
function calculateInsertionIndex(lineIndex, graphemeIndex, layout) {
|
|
25792
|
-
let insertionIndex = 0;
|
|
25793
|
-
|
|
25794
|
-
// Add characters from all previous lines
|
|
25795
|
-
for (let i = 0; i < lineIndex && i < layout.lines.length; i++) {
|
|
25796
|
-
insertionIndex += layout.lines[i].graphemes.length;
|
|
25797
|
-
// Add newline character (except for last line)
|
|
25798
|
-
if (i < layout.lines.length - 1) {
|
|
25799
|
-
insertionIndex += 1; // \n character
|
|
25800
|
-
}
|
|
25801
|
-
}
|
|
25802
|
-
|
|
25803
|
-
// Add characters within current line
|
|
25804
|
-
insertionIndex += graphemeIndex;
|
|
25805
|
-
return insertionIndex;
|
|
25806
|
-
}
|
|
25807
|
-
|
|
25808
26430
|
/**
|
|
25809
26431
|
* Find line and grapheme position from insertion index
|
|
25810
26432
|
*/
|
|
@@ -26018,6 +26640,20 @@ class IText extends ITextClickBehavior {
|
|
|
26018
26640
|
...IText.ownDefaults,
|
|
26019
26641
|
...options
|
|
26020
26642
|
});
|
|
26643
|
+
/**
|
|
26644
|
+
* Index where text selection starts (or where cursor is when there is no selection)
|
|
26645
|
+
* @type Number
|
|
26646
|
+
*/
|
|
26647
|
+
/**
|
|
26648
|
+
* Index where text selection ends
|
|
26649
|
+
* @type Number
|
|
26650
|
+
*/
|
|
26651
|
+
/**
|
|
26652
|
+
* Cache for visual positions per line to ensure consistency
|
|
26653
|
+
* during selection operations
|
|
26654
|
+
* @private
|
|
26655
|
+
*/
|
|
26656
|
+
_defineProperty(this, "_visualPositionsCache", new Map());
|
|
26021
26657
|
this.initBehavior();
|
|
26022
26658
|
}
|
|
26023
26659
|
|
|
@@ -26141,6 +26777,8 @@ class IText extends ITextClickBehavior {
|
|
|
26141
26777
|
// clear the cursorOffsetCache, so we ensure to calculate once per renderCursor
|
|
26142
26778
|
// the correct position but not at every cursor animation.
|
|
26143
26779
|
this.cursorOffsetCache = {};
|
|
26780
|
+
// Clear visual positions cache on full render since dimensions may have changed
|
|
26781
|
+
this._clearVisualPositionsCache();
|
|
26144
26782
|
this.renderCursorOrSelection();
|
|
26145
26783
|
}
|
|
26146
26784
|
|
|
@@ -26168,6 +26806,9 @@ class IText extends ITextClickBehavior {
|
|
|
26168
26806
|
if (!ctx) {
|
|
26169
26807
|
return;
|
|
26170
26808
|
}
|
|
26809
|
+
// Clear cache to ensure fresh cursor position calculation
|
|
26810
|
+
// This is important during selection drag when positions change frequently
|
|
26811
|
+
this.cursorOffsetCache = {};
|
|
26171
26812
|
const boundaries = this._getCursorBoundaries();
|
|
26172
26813
|
const ancestors = this.findAncestorsWithClipPath();
|
|
26173
26814
|
const hasAncestorsWithClipping = ancestors.length > 0;
|
|
@@ -26245,12 +26886,8 @@ class IText extends ITextClickBehavior {
|
|
|
26245
26886
|
_getCursorBoundaries() {
|
|
26246
26887
|
let index = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.selectionStart;
|
|
26247
26888
|
let skipCaching = arguments.length > 1 ? arguments[1] : undefined;
|
|
26248
|
-
//
|
|
26249
|
-
|
|
26250
|
-
return this._getCursorBoundariesAdvanced(index);
|
|
26251
|
-
}
|
|
26252
|
-
|
|
26253
|
-
// Fall back to original method
|
|
26889
|
+
// Always use original method which uses __charBounds directly
|
|
26890
|
+
// and has proper RTL handling built-in
|
|
26254
26891
|
return this._getCursorBoundariesOriginal(index, skipCaching);
|
|
26255
26892
|
}
|
|
26256
26893
|
|
|
@@ -26289,19 +26926,261 @@ class IText extends ITextClickBehavior {
|
|
|
26289
26926
|
}
|
|
26290
26927
|
|
|
26291
26928
|
/**
|
|
26292
|
-
*
|
|
26293
|
-
*
|
|
26929
|
+
* Override selection to use measureText-based visual positions
|
|
26930
|
+
* This ensures hit testing matches actual browser BiDi rendering
|
|
26294
26931
|
*/
|
|
26295
26932
|
getSelectionStartFromPointer(e) {
|
|
26296
|
-
|
|
26297
|
-
|
|
26933
|
+
// Get mouse position in object-local coordinates (origin at center)
|
|
26934
|
+
const scenePoint = this.canvas.getScenePoint(e);
|
|
26935
|
+
const localPoint = scenePoint.transform(invertTransform(this.calcTransformMatrix()));
|
|
26936
|
+
|
|
26937
|
+
// Convert to top-left origin coordinates
|
|
26938
|
+
const mouseX = localPoint.x + this.width / 2;
|
|
26939
|
+
const mouseY = localPoint.y + this.height / 2;
|
|
26940
|
+
|
|
26941
|
+
// Find the line based on Y position
|
|
26942
|
+
let height = 0,
|
|
26943
|
+
lineIndex = 0;
|
|
26944
|
+
for (let i = 0; i < this._textLines.length; i++) {
|
|
26945
|
+
const lineHeight = this.getHeightOfLine(i);
|
|
26946
|
+
if (mouseY >= height && mouseY < height + lineHeight) {
|
|
26947
|
+
lineIndex = i;
|
|
26948
|
+
break;
|
|
26949
|
+
}
|
|
26950
|
+
height += lineHeight;
|
|
26951
|
+
if (i === this._textLines.length - 1) {
|
|
26952
|
+
lineIndex = i;
|
|
26953
|
+
}
|
|
26298
26954
|
}
|
|
26299
|
-
const mouseOffset = this.canvas.getScenePoint(e).transform(invertTransform(this.calcTransformMatrix())).add(new Point(-this._getLeftOffset(), -this._getTopOffset()));
|
|
26300
26955
|
|
|
26301
|
-
//
|
|
26302
|
-
|
|
26303
|
-
|
|
26304
|
-
|
|
26956
|
+
// Calculate line start index using ORIGINAL line lengths (without tatweels)
|
|
26957
|
+
// This ensures selection indices refer to the original text, not the display text
|
|
26958
|
+
let lineStartIndex = 0;
|
|
26959
|
+
for (let i = 0; i < lineIndex; i++) {
|
|
26960
|
+
const origLen = this._getOriginalLineLength(i);
|
|
26961
|
+
const newlineOffset = this.missingNewlineOffset(i);
|
|
26962
|
+
console.log(`📍 Line ${i}: origLen=${origLen}, displayLen=${this._textLines[i].length}, tatweels=${this._getTatweelCountForLine(i)}, newlineOffset=${newlineOffset}`);
|
|
26963
|
+
lineStartIndex += origLen + newlineOffset;
|
|
26964
|
+
}
|
|
26965
|
+
console.log(`📍 Click on line ${lineIndex}, lineStartIndex=${lineStartIndex}`);
|
|
26966
|
+
const line = this._textLines[lineIndex];
|
|
26967
|
+
const lineText = line.join('');
|
|
26968
|
+
const displayCharLength = line.length;
|
|
26969
|
+
const originalCharLength = this._getOriginalLineLength(lineIndex);
|
|
26970
|
+
if (displayCharLength === 0) {
|
|
26971
|
+
return lineStartIndex;
|
|
26972
|
+
}
|
|
26973
|
+
|
|
26974
|
+
// Use measureText to get actual visual character positions
|
|
26975
|
+
// This matches exactly how the canvas renders BiDi text
|
|
26976
|
+
const visualPositions = this._measureVisualPositions(lineIndex, lineText);
|
|
26977
|
+
|
|
26978
|
+
// Calculate line offset based on alignment
|
|
26979
|
+
const lineWidth = this.getLineWidth(lineIndex);
|
|
26980
|
+
let lineStartX = 0;
|
|
26981
|
+
if (this.textAlign === 'center' || this.textAlign === 'justify-center') {
|
|
26982
|
+
lineStartX = (this.width - lineWidth) / 2;
|
|
26983
|
+
} else if (this.textAlign === 'right' || this.textAlign === 'justify-right') {
|
|
26984
|
+
lineStartX = this.width - lineWidth;
|
|
26985
|
+
} else if (this.direction === 'rtl' && (this.textAlign === 'justify' || this.textAlign === 'left')) {
|
|
26986
|
+
// For RTL with left/justify, text starts from right
|
|
26987
|
+
lineStartX = this.width - lineWidth;
|
|
26988
|
+
}
|
|
26989
|
+
|
|
26990
|
+
// Find which character was clicked based on visual position
|
|
26991
|
+
const clickX = mouseX - lineStartX;
|
|
26992
|
+
|
|
26993
|
+
// Sort positions by visual X for hit testing
|
|
26994
|
+
const sortedPositions = [...visualPositions].sort((a, b) => a.visualX - b.visualX);
|
|
26995
|
+
|
|
26996
|
+
// Handle click before first character
|
|
26997
|
+
if (sortedPositions.length > 0 && clickX < sortedPositions[0].visualX) {
|
|
26998
|
+
// Before first visual character - cursor at visual left edge
|
|
26999
|
+
// For RTL base direction, this means logical end of line
|
|
27000
|
+
return this.direction === 'rtl' ? lineStartIndex + originalCharLength : lineStartIndex;
|
|
27001
|
+
}
|
|
27002
|
+
|
|
27003
|
+
// Handle click after last character
|
|
27004
|
+
if (sortedPositions.length > 0) {
|
|
27005
|
+
const lastPos = sortedPositions[sortedPositions.length - 1];
|
|
27006
|
+
if (clickX > lastPos.visualX + lastPos.width) {
|
|
27007
|
+
// After last visual character - cursor at visual right edge
|
|
27008
|
+
// For RTL base direction, this means logical start of line
|
|
27009
|
+
return this.direction === 'rtl' ? lineStartIndex : lineStartIndex + originalCharLength;
|
|
27010
|
+
}
|
|
27011
|
+
}
|
|
27012
|
+
|
|
27013
|
+
// Find the character at click position
|
|
27014
|
+
for (let i = 0; i < sortedPositions.length; i++) {
|
|
27015
|
+
const pos = sortedPositions[i];
|
|
27016
|
+
const charEnd = pos.visualX + pos.width;
|
|
27017
|
+
if (clickX >= pos.visualX && clickX <= charEnd) {
|
|
27018
|
+
// Convert display index to original index
|
|
27019
|
+
// This also handles tatweels - they map to the character they extend
|
|
27020
|
+
const originalCharIndex = this._displayToOriginalIndex(lineIndex, pos.logicalIndex);
|
|
27021
|
+
|
|
27022
|
+
// Check if this is a tatweel - if so, treat click as clicking on the extended character
|
|
27023
|
+
const isTatweel = this._isTatweelAtDisplayIndex(lineIndex, pos.logicalIndex);
|
|
27024
|
+
console.log(`📍 Hit char: displayIdx=${pos.logicalIndex}, origIdx=${originalCharIndex}, isTatweel=${isTatweel}, char="${this._textLines[lineIndex][pos.logicalIndex]}"`);
|
|
27025
|
+
const charMiddle = pos.visualX + pos.width / 2;
|
|
27026
|
+
const clickedLeftHalf = clickX <= charMiddle;
|
|
27027
|
+
|
|
27028
|
+
// For tatweels, clicking anywhere on it should place cursor after the extended character
|
|
27029
|
+
if (isTatweel) {
|
|
27030
|
+
// Tatweel extends the character before it, so cursor goes after that character
|
|
27031
|
+
// originalCharIndex from _displayToOriginalIndex already maps tatweel to char+1
|
|
27032
|
+
const result = lineStartIndex + originalCharIndex;
|
|
27033
|
+
console.log(`📍 Tatweel click result: ${result}`);
|
|
27034
|
+
return result;
|
|
27035
|
+
}
|
|
27036
|
+
|
|
27037
|
+
// For RTL characters: left visual half means cursor AFTER (higher logical index)
|
|
27038
|
+
// For LTR characters: left visual half means cursor BEFORE (lower logical index)
|
|
27039
|
+
if (pos.isRtl) {
|
|
27040
|
+
// RTL character
|
|
27041
|
+
const result = lineStartIndex + (clickedLeftHalf ? originalCharIndex + 1 : originalCharIndex);
|
|
27042
|
+
console.log(`📍 RTL char result: ${result} (clickedLeftHalf=${clickedLeftHalf})`);
|
|
27043
|
+
return result;
|
|
27044
|
+
} else {
|
|
27045
|
+
// LTR character
|
|
27046
|
+
const result = lineStartIndex + (clickedLeftHalf ? originalCharIndex : originalCharIndex + 1);
|
|
27047
|
+
console.log(`📍 LTR char result: ${result} (clickedLeftHalf=${clickedLeftHalf})`);
|
|
27048
|
+
return result;
|
|
27049
|
+
}
|
|
27050
|
+
}
|
|
27051
|
+
}
|
|
27052
|
+
|
|
27053
|
+
// console.log(`📍 No match, returning end: ${lineStartIndex + originalCharLength}`);
|
|
27054
|
+
return lineStartIndex + originalCharLength;
|
|
27055
|
+
}
|
|
27056
|
+
|
|
27057
|
+
/**
|
|
27058
|
+
* Clear the visual positions cache
|
|
27059
|
+
* Should be called when text content or dimensions change
|
|
27060
|
+
*/
|
|
27061
|
+
_clearVisualPositionsCache() {
|
|
27062
|
+
this._visualPositionsCache.clear();
|
|
27063
|
+
}
|
|
27064
|
+
|
|
27065
|
+
/**
|
|
27066
|
+
* Measure visual character positions for hit testing using BiDi analysis
|
|
27067
|
+
* This properly handles mixed RTL/LTR text by analyzing BiDi runs
|
|
27068
|
+
* Results are cached per line for consistency during selection operations
|
|
27069
|
+
*/
|
|
27070
|
+
_measureVisualPositions(lineIndex, lineText) {
|
|
27071
|
+
// Check cache first
|
|
27072
|
+
if (this._visualPositionsCache.has(lineIndex)) {
|
|
27073
|
+
return this._visualPositionsCache.get(lineIndex);
|
|
27074
|
+
}
|
|
27075
|
+
const line = this._textLines[lineIndex];
|
|
27076
|
+
const positions = [];
|
|
27077
|
+
const chars = this.__charBounds[lineIndex];
|
|
27078
|
+
if (!chars || chars.length === 0) {
|
|
27079
|
+
this._visualPositionsCache.set(lineIndex, positions);
|
|
27080
|
+
return positions;
|
|
27081
|
+
}
|
|
27082
|
+
|
|
27083
|
+
// For LTR direction, use logical positions directly
|
|
27084
|
+
if (this.direction !== 'rtl') {
|
|
27085
|
+
for (let i = 0; i < line.length; i++) {
|
|
27086
|
+
var _chars$i, _chars$i2;
|
|
27087
|
+
positions.push({
|
|
27088
|
+
logicalIndex: i,
|
|
27089
|
+
visualX: ((_chars$i = chars[i]) === null || _chars$i === void 0 ? void 0 : _chars$i.left) || 0,
|
|
27090
|
+
width: ((_chars$i2 = chars[i]) === null || _chars$i2 === void 0 ? void 0 : _chars$i2.kernedWidth) || 0,
|
|
27091
|
+
isRtl: false
|
|
27092
|
+
});
|
|
27093
|
+
}
|
|
27094
|
+
this._visualPositionsCache.set(lineIndex, positions);
|
|
27095
|
+
return positions;
|
|
27096
|
+
}
|
|
27097
|
+
|
|
27098
|
+
// For RTL, use BiDi analysis to determine visual positions
|
|
27099
|
+
const runs = analyzeBiDi(lineText, 'rtl');
|
|
27100
|
+
|
|
27101
|
+
// Build mapping from string position to grapheme index
|
|
27102
|
+
// This is needed because analyzeBiDi works on string positions (code points)
|
|
27103
|
+
// but we need grapheme indices for charBounds
|
|
27104
|
+
const stringPosToGrapheme = [];
|
|
27105
|
+
let strPos = 0;
|
|
27106
|
+
for (let gi = 0; gi < line.length; gi++) {
|
|
27107
|
+
const grapheme = line[gi];
|
|
27108
|
+
for (let j = 0; j < grapheme.length; j++) {
|
|
27109
|
+
stringPosToGrapheme[strPos + j] = gi;
|
|
27110
|
+
}
|
|
27111
|
+
strPos += grapheme.length;
|
|
27112
|
+
}
|
|
27113
|
+
|
|
27114
|
+
// Calculate width for each run
|
|
27115
|
+
|
|
27116
|
+
const runInfos = [];
|
|
27117
|
+
for (const run of runs) {
|
|
27118
|
+
const runChars = [];
|
|
27119
|
+
let runWidth = 0;
|
|
27120
|
+
const seenGraphemes = new Set();
|
|
27121
|
+
|
|
27122
|
+
// Map string positions in this run to grapheme indices
|
|
27123
|
+
for (let sp = run.start; sp < run.end; sp++) {
|
|
27124
|
+
const gi = stringPosToGrapheme[sp];
|
|
27125
|
+
if (gi !== undefined && !seenGraphemes.has(gi)) {
|
|
27126
|
+
var _chars$gi;
|
|
27127
|
+
seenGraphemes.add(gi);
|
|
27128
|
+
runChars.push(gi);
|
|
27129
|
+
runWidth += ((_chars$gi = chars[gi]) === null || _chars$gi === void 0 ? void 0 : _chars$gi.kernedWidth) || 0;
|
|
27130
|
+
}
|
|
27131
|
+
}
|
|
27132
|
+
runInfos.push({
|
|
27133
|
+
run,
|
|
27134
|
+
width: runWidth,
|
|
27135
|
+
charIndices: runChars
|
|
27136
|
+
});
|
|
27137
|
+
}
|
|
27138
|
+
|
|
27139
|
+
// For RTL base direction, runs are displayed right-to-left
|
|
27140
|
+
// So first run appears on the right, last run on the left
|
|
27141
|
+
const totalWidth = this.getLineWidth(lineIndex);
|
|
27142
|
+
let visualX = totalWidth; // Start from right edge
|
|
27143
|
+
|
|
27144
|
+
for (const runInfo of runInfos) {
|
|
27145
|
+
visualX -= runInfo.width; // Move left by run width
|
|
27146
|
+
|
|
27147
|
+
const isRtlRun = runInfo.run.direction === 'rtl';
|
|
27148
|
+
if (isRtlRun) {
|
|
27149
|
+
// RTL run: characters displayed right-to-left within run
|
|
27150
|
+
// First char of run at visual right of run, last at visual left
|
|
27151
|
+
let charX = visualX + runInfo.width;
|
|
27152
|
+
for (const idx of runInfo.charIndices) {
|
|
27153
|
+
var _chars$idx;
|
|
27154
|
+
const charWidth = ((_chars$idx = chars[idx]) === null || _chars$idx === void 0 ? void 0 : _chars$idx.kernedWidth) || 0;
|
|
27155
|
+
charX -= charWidth;
|
|
27156
|
+
positions.push({
|
|
27157
|
+
logicalIndex: idx,
|
|
27158
|
+
visualX: charX,
|
|
27159
|
+
width: charWidth,
|
|
27160
|
+
isRtl: true
|
|
27161
|
+
});
|
|
27162
|
+
}
|
|
27163
|
+
} else {
|
|
27164
|
+
// LTR run: characters displayed left-to-right within run
|
|
27165
|
+
// First char of run at visual left of run, last at visual right
|
|
27166
|
+
let charX = visualX;
|
|
27167
|
+
for (const idx of runInfo.charIndices) {
|
|
27168
|
+
var _chars$idx2;
|
|
27169
|
+
const charWidth = ((_chars$idx2 = chars[idx]) === null || _chars$idx2 === void 0 ? void 0 : _chars$idx2.kernedWidth) || 0;
|
|
27170
|
+
positions.push({
|
|
27171
|
+
logicalIndex: idx,
|
|
27172
|
+
visualX: charX,
|
|
27173
|
+
width: charWidth,
|
|
27174
|
+
isRtl: false
|
|
27175
|
+
});
|
|
27176
|
+
charX += charWidth;
|
|
27177
|
+
}
|
|
27178
|
+
}
|
|
27179
|
+
}
|
|
27180
|
+
|
|
27181
|
+
// Cache the result
|
|
27182
|
+
this._visualPositionsCache.set(lineIndex, positions);
|
|
27183
|
+
return positions;
|
|
26305
27184
|
}
|
|
26306
27185
|
|
|
26307
27186
|
/**
|
|
@@ -26321,40 +27200,140 @@ class IText extends ITextClickBehavior {
|
|
|
26321
27200
|
}
|
|
26322
27201
|
|
|
26323
27202
|
/**
|
|
26324
|
-
* Calculates cursor left/top offset relative to
|
|
27203
|
+
* Calculates cursor left/top offset relative to _getLeftOffset()
|
|
27204
|
+
* Uses visual positions for BiDi text support
|
|
27205
|
+
* Handles kashida by converting original indices to display indices
|
|
26325
27206
|
* @private
|
|
26326
|
-
* @param {number} index index from start
|
|
27207
|
+
* @param {number} index index from start (in original text space, without tatweels)
|
|
26327
27208
|
*/
|
|
26328
27209
|
__getCursorBoundariesOffsets(index) {
|
|
26329
|
-
let topOffset = 0
|
|
26330
|
-
|
|
26331
|
-
|
|
26332
|
-
|
|
26333
|
-
|
|
26334
|
-
|
|
27210
|
+
let topOffset = 0;
|
|
27211
|
+
|
|
27212
|
+
// Find line index and original char index using original line lengths
|
|
27213
|
+
let lineIndex = 0;
|
|
27214
|
+
let originalCharIndex = index;
|
|
27215
|
+
for (let i = 0; i < this._textLines.length; i++) {
|
|
27216
|
+
const originalLineLength = this._getOriginalLineLength(i);
|
|
27217
|
+
if (originalCharIndex <= originalLineLength) {
|
|
27218
|
+
lineIndex = i;
|
|
27219
|
+
break;
|
|
27220
|
+
}
|
|
27221
|
+
originalCharIndex -= originalLineLength + this.missingNewlineOffset(i);
|
|
27222
|
+
lineIndex = i + 1;
|
|
27223
|
+
}
|
|
27224
|
+
|
|
27225
|
+
// Clamp lineIndex to valid range
|
|
27226
|
+
if (lineIndex >= this._textLines.length) {
|
|
27227
|
+
lineIndex = this._textLines.length - 1;
|
|
27228
|
+
originalCharIndex = this._getOriginalLineLength(lineIndex);
|
|
27229
|
+
}
|
|
26335
27230
|
for (let i = 0; i < lineIndex; i++) {
|
|
26336
27231
|
topOffset += this.getHeightOfLine(i);
|
|
26337
27232
|
}
|
|
26338
|
-
|
|
26339
|
-
|
|
26340
|
-
|
|
26341
|
-
|
|
26342
|
-
|
|
27233
|
+
|
|
27234
|
+
// Convert original char index to display char index for visual lookup
|
|
27235
|
+
const displayCharIndex = this._originalToDisplayIndex(lineIndex, originalCharIndex);
|
|
27236
|
+
|
|
27237
|
+
// Get visual positions for cursor placement
|
|
27238
|
+
const lineText = this._textLines[lineIndex].join('');
|
|
27239
|
+
const visualPositions = this._measureVisualPositions(lineIndex, lineText);
|
|
27240
|
+
const lineWidth = this.getLineWidth(lineIndex);
|
|
27241
|
+
this._textLines[lineIndex].length;
|
|
27242
|
+
const originalLineLength = this._getOriginalLineLength(lineIndex);
|
|
27243
|
+
|
|
27244
|
+
// Find visual X position for cursor (0 to lineWidth, from visual left)
|
|
27245
|
+
let visualX = 0;
|
|
27246
|
+
if (visualPositions.length === 0) {
|
|
27247
|
+
// Fallback for empty line
|
|
27248
|
+
return {
|
|
27249
|
+
top: topOffset,
|
|
27250
|
+
left: 0
|
|
27251
|
+
};
|
|
26343
27252
|
}
|
|
26344
|
-
|
|
26345
|
-
|
|
26346
|
-
|
|
26347
|
-
|
|
26348
|
-
|
|
26349
|
-
|
|
26350
|
-
|
|
26351
|
-
}
|
|
26352
|
-
|
|
26353
|
-
|
|
26354
|
-
|
|
27253
|
+
if (originalCharIndex === 0) {
|
|
27254
|
+
// Cursor at logical start
|
|
27255
|
+
// For RTL base direction, logical start is at visual right
|
|
27256
|
+
if (this.direction === 'rtl') {
|
|
27257
|
+
visualX = lineWidth; // Right edge
|
|
27258
|
+
} else {
|
|
27259
|
+
visualX = 0; // Left edge
|
|
27260
|
+
}
|
|
27261
|
+
} else if (originalCharIndex >= originalLineLength) {
|
|
27262
|
+
// Cursor at logical end
|
|
27263
|
+
// For RTL base direction, logical end is at visual left
|
|
27264
|
+
if (this.direction === 'rtl') {
|
|
27265
|
+
visualX = 0; // Left edge
|
|
27266
|
+
} else {
|
|
27267
|
+
visualX = lineWidth; // Right edge
|
|
27268
|
+
}
|
|
27269
|
+
} else {
|
|
27270
|
+
// Cursor between characters - find visual position of character at displayCharIndex
|
|
27271
|
+
const charPos = visualPositions.find(p => p.logicalIndex === displayCharIndex);
|
|
27272
|
+
if (charPos) {
|
|
27273
|
+
// Use character's direction to determine cursor position
|
|
27274
|
+
// For RTL char: cursor "before" it appears at its right visual edge
|
|
27275
|
+
// For LTR char: cursor "before" it appears at its left visual edge
|
|
27276
|
+
if (charPos.isRtl) {
|
|
27277
|
+
visualX = charPos.visualX + charPos.width;
|
|
27278
|
+
} else {
|
|
27279
|
+
visualX = charPos.visualX;
|
|
27280
|
+
}
|
|
27281
|
+
} else {
|
|
27282
|
+
// Fallback - try the previous character in display space
|
|
27283
|
+
const prevDisplayIndex = displayCharIndex > 0 ? displayCharIndex - 1 : 0;
|
|
27284
|
+
const prevCharPos = visualPositions.find(p => p.logicalIndex === prevDisplayIndex);
|
|
27285
|
+
if (prevCharPos) {
|
|
27286
|
+
// Cursor after previous character
|
|
27287
|
+
if (prevCharPos.isRtl) {
|
|
27288
|
+
visualX = prevCharPos.visualX;
|
|
27289
|
+
} else {
|
|
27290
|
+
visualX = prevCharPos.visualX + prevCharPos.width;
|
|
27291
|
+
}
|
|
27292
|
+
} else {
|
|
27293
|
+
// Ultimate fallback
|
|
27294
|
+
const bound = this.__charBounds[lineIndex][displayCharIndex];
|
|
27295
|
+
visualX = (bound === null || bound === void 0 ? void 0 : bound.left) || 0;
|
|
27296
|
+
}
|
|
26355
27297
|
}
|
|
26356
27298
|
}
|
|
26357
|
-
|
|
27299
|
+
|
|
27300
|
+
// Calculate alignment offset (how much line is shifted from left edge)
|
|
27301
|
+
let alignOffset = 0;
|
|
27302
|
+
if (this.textAlign === 'center' || this.textAlign === 'justify-center') {
|
|
27303
|
+
alignOffset = (this.width - lineWidth) / 2;
|
|
27304
|
+
} else if (this.textAlign === 'right' || this.textAlign === 'justify-right') {
|
|
27305
|
+
alignOffset = this.width - lineWidth;
|
|
27306
|
+
} else if (this.direction === 'rtl' && (this.textAlign === 'justify' || this.textAlign === 'left')) {
|
|
27307
|
+
alignOffset = this.width - lineWidth;
|
|
27308
|
+
}
|
|
27309
|
+
|
|
27310
|
+
// The returned left value is added to _getLeftOffset() in _getCursorBoundaries
|
|
27311
|
+
// _getLeftOffset() returns -width/2 for LTR, +width/2 for RTL
|
|
27312
|
+
// Final cursor X = _getLeftOffset() + leftOffset
|
|
27313
|
+
//
|
|
27314
|
+
// For LTR: cursor X = -width/2 + (alignOffset + visualX)
|
|
27315
|
+
// For RTL: cursor X = +width/2 + leftOffset
|
|
27316
|
+
// We want cursor at: -width/2 + alignOffset + visualX
|
|
27317
|
+
// So leftOffset = -width/2 + alignOffset + visualX - width/2 = alignOffset + visualX - width
|
|
27318
|
+
|
|
27319
|
+
let leftOffset;
|
|
27320
|
+
if (this.direction === 'rtl') {
|
|
27321
|
+
// For RTL, _getLeftOffset() = +width/2
|
|
27322
|
+
// We want final X = -width/2 + alignOffset + visualX
|
|
27323
|
+
// So: +width/2 + leftOffset = -width/2 + alignOffset + visualX
|
|
27324
|
+
// leftOffset = -width + alignOffset + visualX
|
|
27325
|
+
leftOffset = -this.width + alignOffset + visualX;
|
|
27326
|
+
} else {
|
|
27327
|
+
// For LTR, _getLeftOffset() = -width/2
|
|
27328
|
+
// We want final X = -width/2 + alignOffset + visualX
|
|
27329
|
+
// So: -width/2 + leftOffset = -width/2 + alignOffset + visualX
|
|
27330
|
+
// leftOffset = alignOffset + visualX
|
|
27331
|
+
leftOffset = alignOffset + visualX;
|
|
27332
|
+
}
|
|
27333
|
+
return {
|
|
27334
|
+
top: topOffset,
|
|
27335
|
+
left: leftOffset
|
|
27336
|
+
};
|
|
26358
27337
|
}
|
|
26359
27338
|
|
|
26360
27339
|
/**
|
|
@@ -26446,51 +27425,119 @@ class IText extends ITextClickBehavior {
|
|
|
26446
27425
|
}
|
|
26447
27426
|
|
|
26448
27427
|
/**
|
|
26449
|
-
* Renders text selection
|
|
27428
|
+
* Renders text selection using visual positions for BiDi support
|
|
27429
|
+
* Handles kashida by converting original indices to display indices
|
|
26450
27430
|
* @private
|
|
26451
|
-
* @param {{ selectionStart: number, selectionEnd: number }} selection
|
|
27431
|
+
* @param {{ selectionStart: number, selectionEnd: number }} selection (in original text space)
|
|
26452
27432
|
* @param {Object} boundaries Object with left/top/leftOffset/topOffset
|
|
26453
27433
|
* @param {CanvasRenderingContext2D} ctx transformed context to draw on
|
|
26454
27434
|
*/
|
|
26455
27435
|
_renderSelection(ctx, selection, boundaries) {
|
|
26456
27436
|
const selectionStart = selection.selectionStart,
|
|
26457
27437
|
selectionEnd = selection.selectionEnd,
|
|
26458
|
-
isJustify = this.textAlign.includes(JUSTIFY)
|
|
26459
|
-
|
|
26460
|
-
|
|
26461
|
-
|
|
26462
|
-
|
|
26463
|
-
|
|
26464
|
-
|
|
27438
|
+
isJustify = this.textAlign.includes(JUSTIFY);
|
|
27439
|
+
|
|
27440
|
+
// Convert selection indices to line/char using original text space
|
|
27441
|
+
// This handles kashida properly since selection indices don't include tatweels
|
|
27442
|
+
let startLine = 0,
|
|
27443
|
+
endLine = 0;
|
|
27444
|
+
let originalStartChar = selectionStart,
|
|
27445
|
+
originalEndChar = selectionEnd;
|
|
27446
|
+
|
|
27447
|
+
// Find start line and char
|
|
27448
|
+
let charCount = 0;
|
|
27449
|
+
for (let i = 0; i < this._textLines.length; i++) {
|
|
27450
|
+
const originalLineLength = this._getOriginalLineLength(i);
|
|
27451
|
+
if (charCount + originalLineLength >= selectionStart) {
|
|
27452
|
+
startLine = i;
|
|
27453
|
+
originalStartChar = selectionStart - charCount;
|
|
27454
|
+
break;
|
|
27455
|
+
}
|
|
27456
|
+
charCount += originalLineLength + this.missingNewlineOffset(i);
|
|
27457
|
+
}
|
|
27458
|
+
|
|
27459
|
+
// Find end line and char
|
|
27460
|
+
charCount = 0;
|
|
27461
|
+
for (let i = 0; i < this._textLines.length; i++) {
|
|
27462
|
+
const originalLineLength = this._getOriginalLineLength(i);
|
|
27463
|
+
if (charCount + originalLineLength >= selectionEnd) {
|
|
27464
|
+
endLine = i;
|
|
27465
|
+
originalEndChar = selectionEnd - charCount;
|
|
27466
|
+
break;
|
|
27467
|
+
}
|
|
27468
|
+
charCount += originalLineLength + this.missingNewlineOffset(i);
|
|
27469
|
+
if (i === this._textLines.length - 1) {
|
|
27470
|
+
endLine = i;
|
|
27471
|
+
originalEndChar = originalLineLength;
|
|
27472
|
+
}
|
|
27473
|
+
}
|
|
26465
27474
|
for (let i = startLine; i <= endLine; i++) {
|
|
26466
|
-
const lineOffset = this._getLineLeftOffset(i) || 0;
|
|
26467
27475
|
let lineHeight = this.getHeightOfLine(i),
|
|
26468
|
-
realLineHeight = 0
|
|
26469
|
-
boxStart = 0,
|
|
26470
|
-
boxEnd = 0;
|
|
27476
|
+
realLineHeight = 0;
|
|
26471
27477
|
|
|
26472
|
-
//
|
|
27478
|
+
// Get visual positions for this line
|
|
27479
|
+
const lineText = this._textLines[i].join('');
|
|
27480
|
+
const visualPositions = this._measureVisualPositions(i, lineText);
|
|
27481
|
+
this._textLines[i].length;
|
|
27482
|
+
const originalLineLength = this._getOriginalLineLength(i);
|
|
27483
|
+
|
|
27484
|
+
// Calculate selection bounds in original space, then convert to display
|
|
27485
|
+
let originalLineStartChar = 0;
|
|
27486
|
+
let originalLineEndChar = originalLineLength;
|
|
26473
27487
|
if (i === startLine) {
|
|
26474
|
-
|
|
27488
|
+
originalLineStartChar = originalStartChar;
|
|
27489
|
+
}
|
|
27490
|
+
if (i === endLine) {
|
|
27491
|
+
originalLineEndChar = originalEndChar;
|
|
27492
|
+
}
|
|
27493
|
+
|
|
27494
|
+
// Convert original char indices to display indices for visual lookup
|
|
27495
|
+
const displayLineStartChar = this._originalToDisplayIndex(i, originalLineStartChar);
|
|
27496
|
+
const displayLineEndChar = this._originalToDisplayIndex(i, originalLineEndChar);
|
|
27497
|
+
|
|
27498
|
+
// Get visual X positions for selection range
|
|
27499
|
+
let minVisualX = Infinity;
|
|
27500
|
+
let maxVisualX = -Infinity;
|
|
27501
|
+
for (const pos of visualPositions) {
|
|
27502
|
+
if (pos.logicalIndex >= displayLineStartChar && pos.logicalIndex < displayLineEndChar) {
|
|
27503
|
+
minVisualX = Math.min(minVisualX, pos.visualX);
|
|
27504
|
+
maxVisualX = Math.max(maxVisualX, pos.visualX + pos.width);
|
|
27505
|
+
}
|
|
26475
27506
|
}
|
|
26476
|
-
|
|
26477
|
-
|
|
26478
|
-
|
|
26479
|
-
if (
|
|
26480
|
-
|
|
27507
|
+
|
|
27508
|
+
// Handle edge cases
|
|
27509
|
+
if (minVisualX === Infinity || maxVisualX === -Infinity) {
|
|
27510
|
+
if (i >= startLine && i < endLine) {
|
|
27511
|
+
// Full line selection
|
|
27512
|
+
minVisualX = 0;
|
|
27513
|
+
maxVisualX = isJustify && !this.isEndOfWrapping(i) ? this.width : this.getLineWidth(i) || 5;
|
|
26481
27514
|
} else {
|
|
26482
|
-
|
|
26483
|
-
boxEnd = this.__charBounds[endLine][endChar - 1].left + this.__charBounds[endLine][endChar - 1].width - charSpacing;
|
|
27515
|
+
continue; // No selection on this line
|
|
26484
27516
|
}
|
|
26485
27517
|
}
|
|
26486
27518
|
realLineHeight = lineHeight;
|
|
26487
27519
|
if (this.lineHeight < 1 || i === endLine && this.lineHeight > 1) {
|
|
26488
27520
|
lineHeight /= this.lineHeight;
|
|
26489
27521
|
}
|
|
26490
|
-
|
|
26491
|
-
|
|
26492
|
-
|
|
26493
|
-
|
|
27522
|
+
|
|
27523
|
+
// Calculate draw position
|
|
27524
|
+
// Visual positions are relative to line start (0 to lineWidth)
|
|
27525
|
+
// Need to add alignment offset
|
|
27526
|
+
const lineWidth = this.getLineWidth(i);
|
|
27527
|
+
let alignOffset = 0;
|
|
27528
|
+
if (this.textAlign === 'center' || this.textAlign === 'justify-center') {
|
|
27529
|
+
alignOffset = (this.width - lineWidth) / 2;
|
|
27530
|
+
} else if (this.textAlign === 'right' || this.textAlign === 'justify-right') {
|
|
27531
|
+
alignOffset = this.width - lineWidth;
|
|
27532
|
+
} else if (this.direction === 'rtl' && (this.textAlign === 'justify' || this.textAlign === 'left')) {
|
|
27533
|
+
alignOffset = this.width - lineWidth;
|
|
27534
|
+
}
|
|
27535
|
+
|
|
27536
|
+
// Draw from center origin (-width/2 to width/2)
|
|
27537
|
+
const drawStart = -this.width / 2 + alignOffset + minVisualX;
|
|
27538
|
+
const drawWidth = maxVisualX - minVisualX;
|
|
27539
|
+
let drawHeight = lineHeight;
|
|
27540
|
+
let extraTop = 0;
|
|
26494
27541
|
if (this.inCompositionMode) {
|
|
26495
27542
|
ctx.fillStyle = this.compositionColor || 'black';
|
|
26496
27543
|
drawHeight = 1;
|
|
@@ -26498,15 +27545,6 @@ class IText extends ITextClickBehavior {
|
|
|
26498
27545
|
} else {
|
|
26499
27546
|
ctx.fillStyle = this.selectionColor;
|
|
26500
27547
|
}
|
|
26501
|
-
if (this.direction === 'rtl') {
|
|
26502
|
-
if (this.textAlign === RIGHT || this.textAlign === JUSTIFY || this.textAlign === JUSTIFY_RIGHT) {
|
|
26503
|
-
drawStart = this.width - drawStart - drawWidth;
|
|
26504
|
-
} else if (this.textAlign === LEFT || this.textAlign === JUSTIFY_LEFT) {
|
|
26505
|
-
drawStart = boundaries.left + lineOffset - boxEnd;
|
|
26506
|
-
} else if (this.textAlign === CENTER || this.textAlign === JUSTIFY_CENTER) {
|
|
26507
|
-
drawStart = boundaries.left + lineOffset - boxEnd;
|
|
26508
|
-
}
|
|
26509
|
-
}
|
|
26510
27548
|
ctx.fillRect(drawStart, boundaries.top + boundaries.topOffset + extraTop, drawWidth, drawHeight);
|
|
26511
27549
|
boundaries.topOffset += realLineHeight;
|
|
26512
27550
|
}
|
|
@@ -26555,53 +27593,6 @@ class IText extends ITextClickBehavior {
|
|
|
26555
27593
|
super.dispose();
|
|
26556
27594
|
}
|
|
26557
27595
|
}
|
|
26558
|
-
/**
|
|
26559
|
-
* Index where text selection starts (or where cursor is when there is no selection)
|
|
26560
|
-
* @type Number
|
|
26561
|
-
*/
|
|
26562
|
-
/**
|
|
26563
|
-
* Index where text selection ends
|
|
26564
|
-
* @type Number
|
|
26565
|
-
*/
|
|
26566
|
-
/**
|
|
26567
|
-
* Color of text selection
|
|
26568
|
-
* @type String
|
|
26569
|
-
*/
|
|
26570
|
-
/**
|
|
26571
|
-
* Indicates whether text is in editing mode
|
|
26572
|
-
* @type Boolean
|
|
26573
|
-
*/
|
|
26574
|
-
/**
|
|
26575
|
-
* Indicates whether a text can be edited
|
|
26576
|
-
* @type Boolean
|
|
26577
|
-
*/
|
|
26578
|
-
/**
|
|
26579
|
-
* Border color of text object while it's in editing mode
|
|
26580
|
-
* @type String
|
|
26581
|
-
*/
|
|
26582
|
-
/**
|
|
26583
|
-
* Width of cursor (in px)
|
|
26584
|
-
* @type Number
|
|
26585
|
-
*/
|
|
26586
|
-
/**
|
|
26587
|
-
* Color of text cursor color in editing mode.
|
|
26588
|
-
* if not set (default) will take color from the text.
|
|
26589
|
-
* if set to a color value that fabric can understand, it will
|
|
26590
|
-
* be used instead of the color of the text at the current position.
|
|
26591
|
-
* @type String
|
|
26592
|
-
*/
|
|
26593
|
-
/**
|
|
26594
|
-
* Delay between cursor blink (in ms)
|
|
26595
|
-
* @type Number
|
|
26596
|
-
*/
|
|
26597
|
-
/**
|
|
26598
|
-
* Duration of cursor fade in (in ms)
|
|
26599
|
-
* @type Number
|
|
26600
|
-
*/
|
|
26601
|
-
/**
|
|
26602
|
-
* Indicates whether internal text char widths can be cached
|
|
26603
|
-
* @type Boolean
|
|
26604
|
-
*/
|
|
26605
27596
|
_defineProperty(IText, "ownDefaults", iTextDefaultValues);
|
|
26606
27597
|
_defineProperty(IText, "type", 'IText');
|
|
26607
27598
|
classRegistry.setClass(IText);
|
|
@@ -26672,7 +27663,7 @@ class Textbox extends IText {
|
|
|
26672
27663
|
}
|
|
26673
27664
|
|
|
26674
27665
|
// Skip if nothing changed
|
|
26675
|
-
const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
|
|
27666
|
+
const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}|${this.kashida}`;
|
|
26676
27667
|
if (this._lastDimensionState === currentState && this._textLines && this._textLines.length > 0) {
|
|
26677
27668
|
return;
|
|
26678
27669
|
}
|
|
@@ -26771,12 +27762,18 @@ class Textbox extends IText {
|
|
|
26771
27762
|
}
|
|
26772
27763
|
|
|
26773
27764
|
// Use new layout engine
|
|
27765
|
+
// When kashida is enabled, don't let layout engine apply justify - we'll handle it with kashida
|
|
27766
|
+
const useKashidaJustify = this.kashida !== 'none' && this.textAlign.includes(JUSTIFY);
|
|
27767
|
+
const effectiveAlign = useKashidaJustify ? this.direction === 'rtl' ? 'right' : 'left' // Natural alignment, kashida will justify
|
|
27768
|
+
: this._mapTextAlignToAlign(this.textAlign);
|
|
26774
27769
|
const layout = layoutText({
|
|
26775
27770
|
text: this.text,
|
|
26776
27771
|
width: this.width,
|
|
26777
|
-
height
|
|
27772
|
+
// Don't pass height constraint to allow vertical auto-expansion
|
|
27773
|
+
// Only pass height if explicitly set to constrain (e.g., for ellipsis)
|
|
27774
|
+
height: this.ellipsis ? this.height : undefined,
|
|
26778
27775
|
wrap: this.wrap || 'word',
|
|
26779
|
-
align:
|
|
27776
|
+
align: effectiveAlign,
|
|
26780
27777
|
ellipsis: this.ellipsis || false,
|
|
26781
27778
|
fontSize: this.fontSize,
|
|
26782
27779
|
lineHeight: this.lineHeight,
|
|
@@ -26814,9 +27811,264 @@ class Textbox extends IText {
|
|
|
26814
27811
|
|
|
26815
27812
|
// Generate style map for compatibility
|
|
26816
27813
|
this._styleMap = this._generateStyleMapFromLayout(layout);
|
|
27814
|
+
|
|
27815
|
+
// Apply kashida for justified text in advanced layout mode
|
|
27816
|
+
if (this.textAlign.includes(JUSTIFY) && this.kashida !== 'none') {
|
|
27817
|
+
this._applyKashidaToLayout();
|
|
27818
|
+
}
|
|
26817
27819
|
this.dirty = true;
|
|
26818
27820
|
}
|
|
26819
27821
|
|
|
27822
|
+
/**
|
|
27823
|
+
* Apply kashida (tatweel) characters to layout for Arabic text justification.
|
|
27824
|
+
* This method INSERTS actual tatweel characters into the text lines.
|
|
27825
|
+
* @private
|
|
27826
|
+
*/
|
|
27827
|
+
_applyKashidaToLayout() {
|
|
27828
|
+
if (!this._textLines || !this.__charBounds) {
|
|
27829
|
+
return;
|
|
27830
|
+
}
|
|
27831
|
+
|
|
27832
|
+
// Clear visual positions cache - it becomes stale when kashida is applied
|
|
27833
|
+
// Check if cache exists (it's initialized in IText constructor which runs after this during construction)
|
|
27834
|
+
if (this._visualPositionsCache) {
|
|
27835
|
+
this._clearVisualPositionsCache();
|
|
27836
|
+
}
|
|
27837
|
+
const kashidaRatios = {
|
|
27838
|
+
none: 0,
|
|
27839
|
+
short: 0.25,
|
|
27840
|
+
medium: 0.5,
|
|
27841
|
+
long: 0.75,
|
|
27842
|
+
stylistic: 1.0
|
|
27843
|
+
};
|
|
27844
|
+
const kashidaRatio = kashidaRatios[this.kashida] || 0;
|
|
27845
|
+
if (kashidaRatio === 0) {
|
|
27846
|
+
return;
|
|
27847
|
+
}
|
|
27848
|
+
|
|
27849
|
+
// Calculate tatweel width once
|
|
27850
|
+
const canvas = document.createElement('canvas');
|
|
27851
|
+
const ctx = canvas.getContext('2d');
|
|
27852
|
+
if (!ctx) {
|
|
27853
|
+
return;
|
|
27854
|
+
}
|
|
27855
|
+
ctx.font = this._getFontDeclaration();
|
|
27856
|
+
const tatweelWidth = ctx.measureText(ARABIC_TATWEEL).width;
|
|
27857
|
+
if (tatweelWidth <= 0) {
|
|
27858
|
+
return;
|
|
27859
|
+
}
|
|
27860
|
+
|
|
27861
|
+
// Reset kashida info
|
|
27862
|
+
this.__kashidaInfo = [];
|
|
27863
|
+
const totalLines = this._textLines.length;
|
|
27864
|
+
for (let lineIndex = 0; lineIndex < totalLines; lineIndex++) {
|
|
27865
|
+
this.__kashidaInfo[lineIndex] = [];
|
|
27866
|
+
const line = this._textLines[lineIndex];
|
|
27867
|
+
if (!this.__charBounds || !this.__charBounds[lineIndex]) {
|
|
27868
|
+
continue;
|
|
27869
|
+
}
|
|
27870
|
+
|
|
27871
|
+
// Don't apply kashida to the last line
|
|
27872
|
+
const isLastLine = lineIndex === totalLines - 1;
|
|
27873
|
+
if (isLastLine) {
|
|
27874
|
+
continue;
|
|
27875
|
+
}
|
|
27876
|
+
const lineBounds = this.__charBounds[lineIndex];
|
|
27877
|
+
const lastBound = lineBounds[lineBounds.length - 1];
|
|
27878
|
+
|
|
27879
|
+
// Calculate current line width
|
|
27880
|
+
const currentLineWidth = lastBound ? lastBound.left + lastBound.kernedWidth : 0;
|
|
27881
|
+
const totalExtraSpace = this.width - currentLineWidth;
|
|
27882
|
+
|
|
27883
|
+
// Only apply kashida if there's significant extra space to fill
|
|
27884
|
+
if (totalExtraSpace <= 2) {
|
|
27885
|
+
continue;
|
|
27886
|
+
}
|
|
27887
|
+
|
|
27888
|
+
// Find kashida points
|
|
27889
|
+
const kashidaPoints = findKashidaPoints(line);
|
|
27890
|
+
if (kashidaPoints.length === 0) {
|
|
27891
|
+
continue;
|
|
27892
|
+
}
|
|
27893
|
+
|
|
27894
|
+
// Calculate kashida space
|
|
27895
|
+
const kashidaSpace = totalExtraSpace * kashidaRatio;
|
|
27896
|
+
|
|
27897
|
+
// Calculate how many tatweels can fit
|
|
27898
|
+
const totalTatweels = Math.floor(kashidaSpace / tatweelWidth);
|
|
27899
|
+
if (totalTatweels === 0) {
|
|
27900
|
+
continue;
|
|
27901
|
+
}
|
|
27902
|
+
|
|
27903
|
+
// Limit kashida points
|
|
27904
|
+
const maxKashidaPoints = Math.min(kashidaPoints.length, totalTatweels);
|
|
27905
|
+
const usedKashidaPoints = kashidaPoints.slice(0, maxKashidaPoints);
|
|
27906
|
+
|
|
27907
|
+
// Distribute tatweels evenly
|
|
27908
|
+
const tatweelsPerPoint = Math.floor(totalTatweels / maxKashidaPoints);
|
|
27909
|
+
const extraTatweels = totalTatweels % maxKashidaPoints;
|
|
27910
|
+
|
|
27911
|
+
// console.log(`=== Inserting Kashida into line ${lineIndex} ===`);
|
|
27912
|
+
// console.log(` totalTatweels: ${totalTatweels}, usedPoints: ${usedKashidaPoints.length}`);
|
|
27913
|
+
|
|
27914
|
+
// Sort by charIndex descending so we insert from the end (prevents index shifting issues)
|
|
27915
|
+
const sortedPoints = [...usedKashidaPoints].sort((a, b) => b.charIndex - a.charIndex);
|
|
27916
|
+
|
|
27917
|
+
// Create new line with tatweels inserted
|
|
27918
|
+
const newLine = [...line];
|
|
27919
|
+
for (let i = 0; i < sortedPoints.length; i++) {
|
|
27920
|
+
const point = sortedPoints[i];
|
|
27921
|
+
const originalIndex = usedKashidaPoints.indexOf(point);
|
|
27922
|
+
const count = tatweelsPerPoint + (originalIndex < extraTatweels ? 1 : 0);
|
|
27923
|
+
if (count > 0) {
|
|
27924
|
+
// Insert tatweels AFTER the character at charIndex
|
|
27925
|
+
const tatweels = Array(count).fill(ARABIC_TATWEEL);
|
|
27926
|
+
newLine.splice(point.charIndex + 1, 0, ...tatweels);
|
|
27927
|
+
// console.log(` Inserted ${count} tatweels after char ${point.charIndex}`);
|
|
27928
|
+
|
|
27929
|
+
// Store kashida info for index conversion
|
|
27930
|
+
this.__kashidaInfo[lineIndex].push({
|
|
27931
|
+
charIndex: point.charIndex,
|
|
27932
|
+
width: count * tatweelWidth,
|
|
27933
|
+
tatweelCount: count
|
|
27934
|
+
});
|
|
27935
|
+
}
|
|
27936
|
+
}
|
|
27937
|
+
|
|
27938
|
+
// Update _textLines with the new line containing tatweels
|
|
27939
|
+
this._textLines[lineIndex] = newLine;
|
|
27940
|
+
|
|
27941
|
+
// Update textLines (string version)
|
|
27942
|
+
if (this.textLines) {
|
|
27943
|
+
this.textLines[lineIndex] = newLine.join('');
|
|
27944
|
+
}
|
|
27945
|
+
|
|
27946
|
+
// Clear and recalculate charBounds for this line
|
|
27947
|
+
this.__charBounds[lineIndex] = [];
|
|
27948
|
+
this.__lineWidths[lineIndex] = undefined;
|
|
27949
|
+
this._measureLine(lineIndex);
|
|
27950
|
+
|
|
27951
|
+
// Now expand spaces to fill any remaining gap
|
|
27952
|
+
let newLineBounds = this.__charBounds[lineIndex];
|
|
27953
|
+
if (newLineBounds && newLineBounds.length > 0) {
|
|
27954
|
+
let newLastBound = newLineBounds[newLineBounds.length - 1];
|
|
27955
|
+
let newLineWidth = newLastBound ? newLastBound.left + newLastBound.kernedWidth : 0;
|
|
27956
|
+
let remainingGap = this.width - newLineWidth;
|
|
27957
|
+
if (remainingGap > 0.5) {
|
|
27958
|
+
// Count spaces in the new line
|
|
27959
|
+
let spaceCount = 0;
|
|
27960
|
+
for (let i = 0; i < newLine.length; i++) {
|
|
27961
|
+
if (/\s/.test(newLine[i])) {
|
|
27962
|
+
spaceCount++;
|
|
27963
|
+
}
|
|
27964
|
+
}
|
|
27965
|
+
if (spaceCount > 0) {
|
|
27966
|
+
const extraPerSpace = remainingGap / spaceCount;
|
|
27967
|
+
let accumulatedExtra = 0;
|
|
27968
|
+
|
|
27969
|
+
// Expand space widths AND update left positions for subsequent chars
|
|
27970
|
+
for (let i = 0; i < newLineBounds.length; i++) {
|
|
27971
|
+
const bound = newLineBounds[i];
|
|
27972
|
+
if (!bound) continue;
|
|
27973
|
+
|
|
27974
|
+
// Update left position to account for previous space expansions
|
|
27975
|
+
bound.left += accumulatedExtra;
|
|
27976
|
+
|
|
27977
|
+
// If this is a space, expand it
|
|
27978
|
+
if (/\s/.test(newLine[i])) {
|
|
27979
|
+
bound.width += extraPerSpace;
|
|
27980
|
+
bound.kernedWidth += extraPerSpace;
|
|
27981
|
+
accumulatedExtra += extraPerSpace;
|
|
27982
|
+
}
|
|
27983
|
+
}
|
|
27984
|
+
// Update the extra entry at the end (cursor position)
|
|
27985
|
+
if (newLineBounds[newLine.length]) {
|
|
27986
|
+
newLineBounds[newLine.length].left += accumulatedExtra;
|
|
27987
|
+
}
|
|
27988
|
+
|
|
27989
|
+
// Recalculate remaining gap after space expansion
|
|
27990
|
+
newLastBound = newLineBounds[newLineBounds.length - 1];
|
|
27991
|
+
newLineWidth = newLastBound ? newLastBound.left + newLastBound.kernedWidth : 0;
|
|
27992
|
+
remainingGap = this.width - newLineWidth;
|
|
27993
|
+
}
|
|
27994
|
+
}
|
|
27995
|
+
|
|
27996
|
+
// If there's still a gap after space expansion, distribute it across all kashida points
|
|
27997
|
+
if (remainingGap > 0.5 && this.__kashidaInfo[lineIndex].length > 0) {
|
|
27998
|
+
const kashidaPointCount = this.__kashidaInfo[lineIndex].length;
|
|
27999
|
+
const extraPerKashida = remainingGap / kashidaPointCount;
|
|
28000
|
+
|
|
28001
|
+
// Find kashida positions in newLine and expand their widths
|
|
28002
|
+
let kashidaIndex = 0;
|
|
28003
|
+
let accumulatedExtra = 0;
|
|
28004
|
+
for (let i = 0; i < newLineBounds.length; i++) {
|
|
28005
|
+
const bound = newLineBounds[i];
|
|
28006
|
+
if (!bound) continue;
|
|
28007
|
+
|
|
28008
|
+
// Update left position for accumulated expansion
|
|
28009
|
+
bound.left += accumulatedExtra;
|
|
28010
|
+
|
|
28011
|
+
// Check if this is a tatweel character
|
|
28012
|
+
if (newLine[i] === ARABIC_TATWEEL) {
|
|
28013
|
+
var _this$__kashidaInfo$l;
|
|
28014
|
+
// Distribute extra width among tatweels
|
|
28015
|
+
const extraForThis = extraPerKashida / (((_this$__kashidaInfo$l = this.__kashidaInfo[lineIndex][kashidaIndex]) === null || _this$__kashidaInfo$l === void 0 ? void 0 : _this$__kashidaInfo$l.tatweelCount) || 1);
|
|
28016
|
+
bound.width += extraForThis;
|
|
28017
|
+
bound.kernedWidth += extraForThis;
|
|
28018
|
+
accumulatedExtra += extraForThis;
|
|
28019
|
+
|
|
28020
|
+
// Move to next kashida info when we've passed this group
|
|
28021
|
+
const currentKashidaInfo = this.__kashidaInfo[lineIndex][kashidaIndex];
|
|
28022
|
+
if (currentKashidaInfo && i > 0) {
|
|
28023
|
+
// Check if next char is not tatweel - means we're done with this group
|
|
28024
|
+
if (i + 1 >= newLine.length || newLine[i + 1] !== ARABIC_TATWEEL) {
|
|
28025
|
+
kashidaIndex++;
|
|
28026
|
+
}
|
|
28027
|
+
}
|
|
28028
|
+
}
|
|
28029
|
+
}
|
|
28030
|
+
|
|
28031
|
+
// Update the extra entry at the end
|
|
28032
|
+
if (newLineBounds[newLine.length]) {
|
|
28033
|
+
newLineBounds[newLine.length].left += accumulatedExtra;
|
|
28034
|
+
}
|
|
28035
|
+
}
|
|
28036
|
+
}
|
|
28037
|
+
|
|
28038
|
+
// Set line width to textbox width (for justified lines)
|
|
28039
|
+
this.__lineWidths[lineIndex] = this.width;
|
|
28040
|
+
|
|
28041
|
+
// console.log(` New line length: ${newLine.length}, text: ${newLine.join('')}`);
|
|
28042
|
+
}
|
|
28043
|
+
|
|
28044
|
+
// For justified lines with kashida, line width should equal textbox width
|
|
28045
|
+
// Only set undefined widths (non-justified lines without kashida)
|
|
28046
|
+
for (let i = 0; i < this._textLines.length; i++) {
|
|
28047
|
+
if (this.__lineWidths[i] === undefined && this.__charBounds[i]) {
|
|
28048
|
+
const bounds = this.__charBounds[i];
|
|
28049
|
+
const lastBound = bounds[bounds.length - 1];
|
|
28050
|
+
if (lastBound) {
|
|
28051
|
+
this.__lineWidths[i] = lastBound.left + lastBound.kernedWidth;
|
|
28052
|
+
}
|
|
28053
|
+
}
|
|
28054
|
+
}
|
|
28055
|
+
|
|
28056
|
+
// Update _text to match the new _textLines (required for editing)
|
|
28057
|
+
this._text = this._textLines.flat();
|
|
28058
|
+
|
|
28059
|
+
// DON'T update this.text - keep the original text intact
|
|
28060
|
+
// The tatweels are in _textLines and _text for rendering purposes only
|
|
28061
|
+
|
|
28062
|
+
this._justifyApplied = true;
|
|
28063
|
+
|
|
28064
|
+
// Debug log final kashida state
|
|
28065
|
+
// console.log('=== _applyKashidaToLayout END ===');
|
|
28066
|
+
// console.log('Final __kashidaInfo:', JSON.stringify(this.__kashidaInfo.map((lineInfo, i) => ({
|
|
28067
|
+
// line: i,
|
|
28068
|
+
// entries: lineInfo.map(k => ({ charIndex: k.charIndex, tatweelCount: k.tatweelCount }))
|
|
28069
|
+
// }))));
|
|
28070
|
+
}
|
|
28071
|
+
|
|
26820
28072
|
/**
|
|
26821
28073
|
* Generate style map from new layout format
|
|
26822
28074
|
* @private
|
|
@@ -27339,84 +28591,100 @@ class Textbox extends IText {
|
|
|
27339
28591
|
* @private
|
|
27340
28592
|
*/
|
|
27341
28593
|
_extractJustifySpaceMeasurements(element, lines) {
|
|
27342
|
-
console.log('=== _extractJustifySpaceMeasurements START ===');
|
|
27343
|
-
console.log('Textbox width:', this.width);
|
|
27344
|
-
console.log('Lines count:', lines.length);
|
|
28594
|
+
// console.log('=== _extractJustifySpaceMeasurements START ===');
|
|
28595
|
+
// console.log('Textbox width:', this.width);
|
|
28596
|
+
// console.log('Lines count:', lines.length);
|
|
28597
|
+
|
|
27345
28598
|
const measureCtx = this._browserMeasureCtx || (this._browserMeasureCtx = document.createElement('canvas').getContext('2d'));
|
|
27346
28599
|
if (!measureCtx) {
|
|
27347
|
-
console.log('ERROR: No measure context');
|
|
28600
|
+
// console.log('ERROR: No measure context');
|
|
27348
28601
|
return [];
|
|
27349
28602
|
}
|
|
27350
28603
|
measureCtx.font = `${this.fontStyle || 'normal'} ${this.fontWeight || 'normal'} ${this.fontSize}px "${this.fontFamily}"`;
|
|
27351
28604
|
const normalSpaceWidth = measureCtx.measureText(' ').width || 6;
|
|
27352
|
-
console.log('Font:', measureCtx.font);
|
|
27353
|
-
console.log('Normal space width:', normalSpaceWidth);
|
|
28605
|
+
// console.log('Font:', measureCtx.font);
|
|
28606
|
+
// console.log('Normal space width:', normalSpaceWidth);
|
|
28607
|
+
|
|
27354
28608
|
const spaceWidths = [];
|
|
27355
28609
|
lines.forEach((line, lineIndex) => {
|
|
27356
28610
|
const lineSpaces = [];
|
|
27357
28611
|
const spaceCount = (line.match(/\s/g) || []).length;
|
|
27358
28612
|
const isLastLine = lineIndex === lines.length - 1;
|
|
27359
|
-
|
|
28613
|
+
|
|
28614
|
+
// console.log(`\nLine ${lineIndex}: "${line.substring(0, 50)}..." spaces: ${spaceCount}, isLast: ${isLastLine}`);
|
|
28615
|
+
|
|
27360
28616
|
if (spaceCount > 0 && !isLastLine) {
|
|
27361
28617
|
// Don't justify last line
|
|
27362
28618
|
const naturalWidth = measureCtx.measureText(line).width;
|
|
27363
28619
|
const remainingSpace = this.width - naturalWidth;
|
|
27364
28620
|
const extraPerSpace = remainingSpace > 0 ? remainingSpace / spaceCount : 0;
|
|
27365
28621
|
const expandedSpaceWidth = normalSpaceWidth + extraPerSpace;
|
|
27366
|
-
|
|
27367
|
-
console.log(`
|
|
28622
|
+
|
|
28623
|
+
// console.log(` Natural width: ${naturalWidth.toFixed(2)}, Remaining: ${remainingSpace.toFixed(2)}`);
|
|
28624
|
+
// console.log(` Extra per space: ${extraPerSpace.toFixed(2)}, Expanded space: ${expandedSpaceWidth.toFixed(2)}`);
|
|
28625
|
+
|
|
27368
28626
|
const safeWidth = Math.max(normalSpaceWidth, expandedSpaceWidth);
|
|
27369
28627
|
for (let i = 0; i < spaceCount; i++) {
|
|
27370
28628
|
lineSpaces.push(safeWidth);
|
|
27371
28629
|
}
|
|
27372
28630
|
} else if (spaceCount > 0) {
|
|
27373
28631
|
// Last line: keep natural space width
|
|
27374
|
-
console.log(` Last line - using normal space width: ${normalSpaceWidth}`);
|
|
28632
|
+
// console.log(` Last line - using normal space width: ${normalSpaceWidth}`);
|
|
27375
28633
|
for (let i = 0; i < spaceCount; i++) {
|
|
27376
28634
|
lineSpaces.push(normalSpaceWidth);
|
|
27377
28635
|
}
|
|
27378
28636
|
}
|
|
27379
28637
|
spaceWidths.push(lineSpaces);
|
|
27380
28638
|
});
|
|
27381
|
-
|
|
27382
|
-
console.log('
|
|
28639
|
+
|
|
28640
|
+
// console.log('\nFinal spaceWidths:', spaceWidths);
|
|
28641
|
+
// console.log('=== _extractJustifySpaceMeasurements END ===\n');
|
|
27383
28642
|
return spaceWidths;
|
|
27384
28643
|
}
|
|
27385
28644
|
|
|
27386
28645
|
/**
|
|
27387
|
-
* Apply justify space expansion using actual charBounds measurements
|
|
28646
|
+
* Apply justify space expansion using actual charBounds measurements.
|
|
28647
|
+
* Supports Arabic kashida (tatweel) justification when kashida property is set.
|
|
27388
28648
|
* @private
|
|
27389
28649
|
*/
|
|
27390
28650
|
_applyBrowserJustifySpaces() {
|
|
27391
|
-
var _this$_textLines, _this$__charBounds;
|
|
27392
|
-
console.log('=== _applyBrowserJustifySpaces START ===');
|
|
27393
|
-
console.log('_textLines:', (_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length, 'lines');
|
|
27394
|
-
console.log('__charBounds:', (_this$__charBounds = this.__charBounds) === null || _this$__charBounds === void 0 ? void 0 : _this$__charBounds.length, 'lines');
|
|
27395
|
-
console.log('textbox width:', this.width);
|
|
27396
28651
|
if (!this._textLines || !this.__charBounds) {
|
|
27397
|
-
console.log('EARLY RETURN: _textLines or __charBounds missing');
|
|
27398
28652
|
return;
|
|
27399
28653
|
}
|
|
28654
|
+
|
|
28655
|
+
// Kashida ratios: proportion of extra space distributed via kashida vs space expansion
|
|
28656
|
+
const kashidaRatios = {
|
|
28657
|
+
none: 0,
|
|
28658
|
+
short: 0.25,
|
|
28659
|
+
medium: 0.5,
|
|
28660
|
+
long: 0.75,
|
|
28661
|
+
stylistic: 1.0
|
|
28662
|
+
};
|
|
28663
|
+
const kashidaRatio = kashidaRatios[this.kashida] || 0;
|
|
28664
|
+
|
|
28665
|
+
// Reset kashida info
|
|
28666
|
+
this.__kashidaInfo = [];
|
|
27400
28667
|
const totalLines = this._textLines.length;
|
|
27401
28668
|
this._textLines.forEach((line, lineIndex) => {
|
|
27402
|
-
|
|
27403
|
-
|
|
27404
|
-
console.log(`\n--- Line ${lineIndex}: "${lineText}" isLast: ${isLastLine} ---`);
|
|
28669
|
+
// Initialize kashida info for this line
|
|
28670
|
+
this.__kashidaInfo[lineIndex] = [];
|
|
27405
28671
|
if (!this.__charBounds || !this.__charBounds[lineIndex]) {
|
|
27406
|
-
console.log(' SKIP: No charBounds for this line');
|
|
27407
28672
|
return;
|
|
27408
28673
|
}
|
|
27409
28674
|
|
|
27410
28675
|
// Don't justify the last line
|
|
28676
|
+
const isLastLine = lineIndex === totalLines - 1;
|
|
27411
28677
|
if (isLastLine) {
|
|
27412
|
-
console.log(' SKIP: Last line - no justify');
|
|
27413
28678
|
return;
|
|
27414
28679
|
}
|
|
27415
28680
|
const lineBounds = this.__charBounds[lineIndex];
|
|
27416
28681
|
|
|
27417
28682
|
// Calculate current line width from charBounds
|
|
27418
28683
|
const currentLineWidth = lineBounds.reduce((sum, b) => sum + ((b === null || b === void 0 ? void 0 : b.kernedWidth) || 0), 0);
|
|
27419
|
-
|
|
28684
|
+
const totalExtraSpace = this.width - currentLineWidth;
|
|
28685
|
+
if (totalExtraSpace <= 0) {
|
|
28686
|
+
return;
|
|
28687
|
+
}
|
|
27420
28688
|
|
|
27421
28689
|
// Count spaces and find space indices
|
|
27422
28690
|
const spaceIndices = [];
|
|
@@ -27426,53 +28694,118 @@ class Textbox extends IText {
|
|
|
27426
28694
|
}
|
|
27427
28695
|
}
|
|
27428
28696
|
const spaceCount = spaceIndices.length;
|
|
27429
|
-
|
|
27430
|
-
|
|
27431
|
-
|
|
27432
|
-
|
|
28697
|
+
|
|
28698
|
+
// Find kashida points if enabled
|
|
28699
|
+
const kashidaPoints = kashidaRatio > 0 ? findKashidaPoints(line) : [];
|
|
28700
|
+
const hasKashidaPoints = kashidaPoints.length > 0;
|
|
28701
|
+
|
|
28702
|
+
// Calculate space distribution
|
|
28703
|
+
let kashidaSpace = 0;
|
|
28704
|
+
if (hasKashidaPoints && kashidaRatio > 0) {
|
|
28705
|
+
// Distribute between kashida and spaces
|
|
28706
|
+
kashidaSpace = totalExtraSpace * kashidaRatio;
|
|
27433
28707
|
}
|
|
27434
28708
|
|
|
27435
|
-
// Calculate
|
|
27436
|
-
const
|
|
27437
|
-
|
|
27438
|
-
|
|
27439
|
-
|
|
27440
|
-
|
|
28709
|
+
// Calculate per-kashida and per-space widths
|
|
28710
|
+
const perKashidaWidth = hasKashidaPoints ? kashidaSpace / kashidaPoints.length : 0;
|
|
28711
|
+
|
|
28712
|
+
// If kashida is enabled, insert actual tatweel characters
|
|
28713
|
+
if (hasKashidaPoints && perKashidaWidth > 0) {
|
|
28714
|
+
// console.log(`=== Inserting kashida in _applyBrowserJustifySpaces line ${lineIndex} ===`);
|
|
28715
|
+
|
|
28716
|
+
// Sort by charIndex descending to insert from end
|
|
28717
|
+
const sortedPoints = [...kashidaPoints].sort((a, b) => b.charIndex - a.charIndex);
|
|
28718
|
+
|
|
28719
|
+
// Calculate tatweel width
|
|
28720
|
+
const canvas = document.createElement('canvas');
|
|
28721
|
+
const ctx = canvas.getContext('2d');
|
|
28722
|
+
if (ctx) {
|
|
28723
|
+
ctx.font = this._getFontDeclaration();
|
|
28724
|
+
const tatweelWidth = ctx.measureText(ARABIC_TATWEEL).width;
|
|
28725
|
+
// console.log(` tatweelWidth: ${tatweelWidth}`);
|
|
28726
|
+
|
|
28727
|
+
if (tatweelWidth > 0) {
|
|
28728
|
+
const newLine = [...line];
|
|
28729
|
+
for (const point of sortedPoints) {
|
|
28730
|
+
const tatweelCount = Math.max(1, Math.round(perKashidaWidth / tatweelWidth));
|
|
28731
|
+
// console.log(` Point ${point.charIndex}: inserting ${tatweelCount} tatweels`);
|
|
28732
|
+
|
|
28733
|
+
// Insert tatweels after the character
|
|
28734
|
+
for (let t = 0; t < tatweelCount; t++) {
|
|
28735
|
+
newLine.splice(point.charIndex + 1, 0, ARABIC_TATWEEL);
|
|
28736
|
+
}
|
|
28737
|
+
|
|
28738
|
+
// Store kashida info with tatweelCount for index conversion
|
|
28739
|
+
this.__kashidaInfo[lineIndex].push({
|
|
28740
|
+
charIndex: point.charIndex,
|
|
28741
|
+
width: perKashidaWidth,
|
|
28742
|
+
tatweelCount: tatweelCount
|
|
28743
|
+
});
|
|
28744
|
+
}
|
|
28745
|
+
|
|
28746
|
+
// console.log(` New line: ${newLine.join('')}`);
|
|
28747
|
+
|
|
28748
|
+
// Update _textLines with kashida
|
|
28749
|
+
this._textLines[lineIndex] = newLine;
|
|
28750
|
+
|
|
28751
|
+
// Update textLines string version
|
|
28752
|
+
if (this.textLines && this.textLines[lineIndex] !== undefined) {
|
|
28753
|
+
this.textLines[lineIndex] = newLine.join('');
|
|
28754
|
+
}
|
|
28755
|
+
|
|
28756
|
+
// Recalculate charBounds
|
|
28757
|
+
this.__charBounds[lineIndex] = [];
|
|
28758
|
+
this.__lineWidths[lineIndex] = undefined;
|
|
28759
|
+
this._measureLine(lineIndex);
|
|
28760
|
+
}
|
|
28761
|
+
}
|
|
28762
|
+
} else {
|
|
28763
|
+
// No kashida - just store info for reference (tatweelCount is 0 since no tatweels inserted)
|
|
28764
|
+
for (const point of kashidaPoints) {
|
|
28765
|
+
this.__kashidaInfo[lineIndex].push({
|
|
28766
|
+
charIndex: point.charIndex,
|
|
28767
|
+
width: perKashidaWidth,
|
|
28768
|
+
tatweelCount: 0
|
|
28769
|
+
});
|
|
28770
|
+
}
|
|
27441
28771
|
}
|
|
27442
|
-
|
|
27443
|
-
|
|
27444
|
-
|
|
27445
|
-
|
|
27446
|
-
|
|
27447
|
-
|
|
27448
|
-
const
|
|
27449
|
-
|
|
27450
|
-
|
|
27451
|
-
|
|
27452
|
-
|
|
27453
|
-
|
|
27454
|
-
|
|
27455
|
-
|
|
27456
|
-
|
|
27457
|
-
|
|
27458
|
-
|
|
27459
|
-
|
|
27460
|
-
|
|
27461
|
-
console.log(` Space at char ${charIndex}: ${oldWidth.toFixed(2)} -> ${newWidth.toFixed(2)} (accumulated: ${accumulated.toFixed(2)})`);
|
|
28772
|
+
|
|
28773
|
+
// Now apply space expansion to remaining extra space
|
|
28774
|
+
const newLineBounds = this.__charBounds[lineIndex];
|
|
28775
|
+
const newLineWidth = newLineBounds.reduce((sum, b) => sum + ((b === null || b === void 0 ? void 0 : b.kernedWidth) || 0), 0);
|
|
28776
|
+
const remainingSpace = this.width - newLineWidth;
|
|
28777
|
+
if (remainingSpace > 0 && spaceCount > 0) {
|
|
28778
|
+
const extraPerSpace = remainingSpace / spaceCount;
|
|
28779
|
+
let accumulated = 0;
|
|
28780
|
+
for (let charIndex = 0; charIndex < this._textLines[lineIndex].length; charIndex++) {
|
|
28781
|
+
const bound = newLineBounds[charIndex];
|
|
28782
|
+
if (!bound) continue;
|
|
28783
|
+
bound.left += accumulated;
|
|
28784
|
+
|
|
28785
|
+
// Check if this is a space (need to check against the updated line)
|
|
28786
|
+
if (/\s/.test(this._textLines[lineIndex][charIndex])) {
|
|
28787
|
+
bound.width += extraPerSpace;
|
|
28788
|
+
bound.kernedWidth += extraPerSpace;
|
|
28789
|
+
accumulated += extraPerSpace;
|
|
28790
|
+
}
|
|
27462
28791
|
}
|
|
27463
28792
|
}
|
|
27464
28793
|
|
|
27465
28794
|
// Update cached line width
|
|
27466
|
-
const
|
|
28795
|
+
const finalLineBounds = this.__charBounds[lineIndex];
|
|
28796
|
+
const finalLineWidth = finalLineBounds.reduce((max, b) => Math.max(max, ((b === null || b === void 0 ? void 0 : b.left) || 0) + ((b === null || b === void 0 ? void 0 : b.width) || 0)), 0);
|
|
27467
28797
|
this.__lineWidths[lineIndex] = finalLineWidth;
|
|
27468
|
-
console.log(' Final line width:', finalLineWidth.toFixed(2), 'target:', this.width);
|
|
27469
28798
|
});
|
|
27470
|
-
console.log('=== _applyBrowserJustifySpaces END ===\n');
|
|
27471
28799
|
this.dirty = true;
|
|
27472
28800
|
// Mark that justify has been applied - for debugging to detect if measureLine overwrites it
|
|
27473
28801
|
this._justifyApplied = true;
|
|
27474
|
-
|
|
27475
|
-
//
|
|
28802
|
+
|
|
28803
|
+
// Debug log final kashida state
|
|
28804
|
+
// console.log('=== _applyBrowserJustifySpaces END ===');
|
|
28805
|
+
// console.log('Final __kashidaInfo:', JSON.stringify(this.__kashidaInfo.map((lineInfo, i) => ({
|
|
28806
|
+
// line: i,
|
|
28807
|
+
// entries: lineInfo.map(k => ({ charIndex: k.charIndex, tatweelCount: k.tatweelCount }))
|
|
28808
|
+
// }))));
|
|
27476
28809
|
}
|
|
27477
28810
|
|
|
27478
28811
|
/**
|