@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
|
@@ -8,7 +8,7 @@ import { classRegistry } from '../../ClassRegistry.mjs';
|
|
|
8
8
|
import { graphemeSplit } from '../../util/lang_string.mjs';
|
|
9
9
|
import { createCanvasElementFor } from '../../util/misc/dom.mjs';
|
|
10
10
|
import { layoutText } from '../../text/layout.mjs';
|
|
11
|
-
import { segmentGraphemes } from '../../text/unicode.mjs';
|
|
11
|
+
import { findKashidaPoints, ARABIC_TATWEEL, segmentGraphemes } from '../../text/unicode.mjs';
|
|
12
12
|
import { hasStyleChanged, stylesToArray, stylesFromArray } from '../../util/misc/textStyles.mjs';
|
|
13
13
|
import { getPathSegmentsInfo, getPointOnPath } from '../../util/path/index.mjs';
|
|
14
14
|
import '../Object/FabricObject.mjs';
|
|
@@ -64,6 +64,12 @@ class FabricText extends StyledText {
|
|
|
64
64
|
* @protected
|
|
65
65
|
*/
|
|
66
66
|
_defineProperty(this, "__charBounds", []);
|
|
67
|
+
/**
|
|
68
|
+
* contains kashida extension info for each line.
|
|
69
|
+
* Each entry contains { charIndex, width } for characters that have kashida extensions.
|
|
70
|
+
* @protected
|
|
71
|
+
*/
|
|
72
|
+
_defineProperty(this, "__kashidaInfo", []);
|
|
67
73
|
Object.assign(this, FabricText.ownDefaults);
|
|
68
74
|
this.setOptions(options);
|
|
69
75
|
if (!this.styles) {
|
|
@@ -182,11 +188,31 @@ class FabricText extends StyledText {
|
|
|
182
188
|
}
|
|
183
189
|
|
|
184
190
|
/**
|
|
185
|
-
* Enlarge space boxes and shift the others for justify alignment
|
|
191
|
+
* Enlarge space boxes and shift the others for justify alignment.
|
|
192
|
+
* Supports Arabic kashida (tatweel) justification when kashida property is set.
|
|
193
|
+
* When kashida is enabled, actual tatweel characters are inserted into the text.
|
|
186
194
|
*/
|
|
187
195
|
enlargeSpaces() {
|
|
188
|
-
|
|
196
|
+
// console.log('=== enlargeSpaces START ===');
|
|
197
|
+
// console.log('this.kashida:', this.kashida);
|
|
198
|
+
|
|
199
|
+
// Kashida ratios: proportion of extra space distributed via kashida vs space expansion
|
|
200
|
+
const kashidaRatios = {
|
|
201
|
+
none: 0,
|
|
202
|
+
short: 0.25,
|
|
203
|
+
medium: 0.5,
|
|
204
|
+
long: 0.75,
|
|
205
|
+
stylistic: 1.0
|
|
206
|
+
};
|
|
207
|
+
const kashidaRatio = kashidaRatios[this.kashida] || 0;
|
|
208
|
+
// console.log('kashidaRatio:', kashidaRatio);
|
|
209
|
+
|
|
210
|
+
// Reset kashida info
|
|
211
|
+
this.__kashidaInfo = [];
|
|
189
212
|
for (let i = 0, len = this._textLines.length; i < len; i++) {
|
|
213
|
+
// Initialize kashida info for this line
|
|
214
|
+
this.__kashidaInfo[i] = [];
|
|
215
|
+
|
|
190
216
|
// Check if this line should be justified
|
|
191
217
|
const hasTextAfter = this._textLines.slice(i + 1).some(line => {
|
|
192
218
|
const lineText = Array.isArray(line) ? line.join('') : line;
|
|
@@ -196,33 +222,121 @@ class FabricText extends StyledText {
|
|
|
196
222
|
const isLastLine = i === len - 1 || this.isEndOfWrapping(i) || isVisualLastLine;
|
|
197
223
|
const shouldJustifyLine = this.textAlign.includes('justify') && !isLastLine;
|
|
198
224
|
if (!shouldJustifyLine) {
|
|
225
|
+
// console.log(` Line ${i}: skipped (not justified)`);
|
|
199
226
|
continue;
|
|
200
227
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
228
|
+
const line = this._textLines[i];
|
|
229
|
+
const currentLineWidth = this.getLineWidth(i);
|
|
230
|
+
const totalExtraSpace = this.width - currentLineWidth;
|
|
231
|
+
// console.log(` Line ${i}: width=${this.width}, lineWidth=${currentLineWidth}, extraSpace=${totalExtraSpace}`);
|
|
232
|
+
|
|
233
|
+
if (totalExtraSpace <= 0) {
|
|
234
|
+
// console.log(` Line ${i}: skipped (no extra space)`);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Find spaces for space expansion
|
|
239
|
+
const spaces = this.textLines[i].match(this._reSpacesAndTabs);
|
|
240
|
+
const numberOfSpaces = spaces ? spaces.length : 0;
|
|
241
|
+
|
|
242
|
+
// Find kashida points if enabled
|
|
243
|
+
const kashidaPoints = kashidaRatio > 0 ? findKashidaPoints(line) : [];
|
|
244
|
+
const hasKashidaPoints = kashidaPoints.length > 0;
|
|
245
|
+
|
|
246
|
+
// Calculate space distribution
|
|
247
|
+
let kashidaSpace = 0;
|
|
248
|
+
if (hasKashidaPoints && kashidaRatio > 0) {
|
|
249
|
+
// Distribute between kashida and spaces
|
|
250
|
+
kashidaSpace = totalExtraSpace * kashidaRatio;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Calculate per-kashida and per-space widths
|
|
254
|
+
const perKashidaWidth = hasKashidaPoints ? kashidaSpace / kashidaPoints.length : 0;
|
|
255
|
+
|
|
256
|
+
// If kashida is enabled, insert tatweel characters into the text
|
|
257
|
+
if (hasKashidaPoints && perKashidaWidth > 0) {
|
|
258
|
+
// console.log(`=== Inserting kashida for line ${i} ===`);
|
|
259
|
+
// console.log(` kashidaPoints: ${kashidaPoints.length}, perKashidaWidth: ${perKashidaWidth}`);
|
|
260
|
+
|
|
261
|
+
// Sort by charIndex descending to insert from end (so indices stay valid)
|
|
262
|
+
const sortedPoints = [...kashidaPoints].sort((a, b) => b.charIndex - a.charIndex);
|
|
263
|
+
|
|
264
|
+
// Calculate how many tatweels to insert per point
|
|
265
|
+
// Measure tatweel width to determine count
|
|
266
|
+
const ctx = getMeasuringContext();
|
|
267
|
+
// console.log(` getMeasuringContext: ${ctx ? 'OK' : 'NULL'}`);
|
|
268
|
+
|
|
269
|
+
if (ctx) {
|
|
270
|
+
ctx.font = this._getFontDeclaration();
|
|
271
|
+
const tatweelWidth = ctx.measureText(ARABIC_TATWEEL).width;
|
|
272
|
+
// console.log(` tatweelWidth: ${tatweelWidth}`);
|
|
273
|
+
|
|
274
|
+
if (tatweelWidth > 0) {
|
|
275
|
+
const newLine = [...line];
|
|
276
|
+
for (const point of sortedPoints) {
|
|
277
|
+
const tatweelCount = Math.max(1, Math.round(perKashidaWidth / tatweelWidth));
|
|
278
|
+
// console.log(` Point ${point.charIndex}: inserting ${tatweelCount} tatweels`);
|
|
279
|
+
|
|
280
|
+
// Insert tatweels after the character
|
|
281
|
+
for (let t = 0; t < tatweelCount; t++) {
|
|
282
|
+
newLine.splice(point.charIndex + 1, 0, ARABIC_TATWEEL);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Store kashida info with updated indices and tatweel count
|
|
286
|
+
this.__kashidaInfo[i].push({
|
|
287
|
+
charIndex: point.charIndex,
|
|
288
|
+
width: perKashidaWidth,
|
|
289
|
+
tatweelCount: tatweelCount
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// console.log(` Total inserted: ${insertedCount} tatweels`);
|
|
294
|
+
// console.log(` Original line length: ${line.length}, new line length: ${newLine.length}`);
|
|
295
|
+
// console.log(` New line: ${newLine.join('')}`);
|
|
296
|
+
|
|
297
|
+
// Update _textLines with the new line containing tatweels
|
|
298
|
+
this._textLines[i] = newLine;
|
|
299
|
+
|
|
300
|
+
// Update textLines string version
|
|
301
|
+
if (this.textLines && this.textLines[i] !== undefined) {
|
|
302
|
+
this.textLines[i] = newLine.join('');
|
|
221
303
|
}
|
|
304
|
+
|
|
305
|
+
// Recalculate charBounds for this line since text changed
|
|
306
|
+
this.__charBounds[i] = [];
|
|
307
|
+
this.__lineWidths[i] = undefined;
|
|
308
|
+
this._measureLine(i);
|
|
309
|
+
|
|
310
|
+
// console.log(` After remeasure, lineWidth: ${this.__lineWidths[i]}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Now apply space expansion to remaining extra space
|
|
316
|
+
const newLineWidth = this.getLineWidth(i);
|
|
317
|
+
const remainingSpace = this.width - newLineWidth;
|
|
318
|
+
if (remainingSpace > 0 && numberOfSpaces > 0) {
|
|
319
|
+
const extraPerSpace = remainingSpace / numberOfSpaces;
|
|
320
|
+
let accumulatedOffset = 0;
|
|
321
|
+
for (let j = 0; j < this._textLines[i].length; j++) {
|
|
322
|
+
const charBound = this.__charBounds[i][j];
|
|
323
|
+
if (!charBound) continue;
|
|
324
|
+
charBound.left += accumulatedOffset;
|
|
325
|
+
if (this._reSpaceAndTab.test(this._textLines[i][j])) {
|
|
326
|
+
charBound.width += extraPerSpace;
|
|
327
|
+
charBound.kernedWidth += extraPerSpace;
|
|
328
|
+
accumulatedOffset += extraPerSpace;
|
|
222
329
|
}
|
|
223
330
|
}
|
|
224
331
|
}
|
|
225
332
|
}
|
|
333
|
+
|
|
334
|
+
// Final debug log showing kashida state
|
|
335
|
+
// console.log('=== enlargeSpaces END ===');
|
|
336
|
+
// console.log('Final __kashidaInfo:', JSON.stringify(this.__kashidaInfo.map((lineInfo, i) => ({
|
|
337
|
+
// line: i,
|
|
338
|
+
// entries: lineInfo.map(k => ({ charIndex: k.charIndex, tatweelCount: k.tatweelCount }))
|
|
339
|
+
// }))));
|
|
226
340
|
}
|
|
227
341
|
|
|
228
342
|
/**
|
|
@@ -242,7 +356,9 @@ class FabricText extends StyledText {
|
|
|
242
356
|
return {
|
|
243
357
|
text: this.text,
|
|
244
358
|
width: this.width,
|
|
245
|
-
height
|
|
359
|
+
// Don't pass height constraint to allow vertical auto-expansion
|
|
360
|
+
// Only pass height if ellipsis is enabled (need to truncate)
|
|
361
|
+
height: this.ellipsis ? this.height : undefined,
|
|
246
362
|
wrap: this.wrap || 'word',
|
|
247
363
|
align: this._mapTextAlignToAlign(this.textAlign),
|
|
248
364
|
ellipsis: this.ellipsis || false,
|
|
@@ -297,9 +413,13 @@ class FabricText extends StyledText {
|
|
|
297
413
|
// Convert layout to legacy format for compatibility
|
|
298
414
|
this._convertLayoutToLegacyFormat(layout);
|
|
299
415
|
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
|
|
416
|
+
// Apply kashida if enabled for justify alignment
|
|
417
|
+
// This must be called after _convertLayoutToLegacyFormat to ensure __charBounds exists
|
|
418
|
+
if (this.textAlign.includes(JUSTIFY) && this.kashida && this.kashida !== 'none') {
|
|
419
|
+
if (this.__charBounds && this.__charBounds.length > 0) {
|
|
420
|
+
this.enlargeSpaces();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
303
423
|
this.dirty = true;
|
|
304
424
|
}
|
|
305
425
|
|
|
@@ -311,16 +431,27 @@ class FabricText extends StyledText {
|
|
|
311
431
|
this._textLines = layout.lines.map(line => line.graphemes);
|
|
312
432
|
this.textLines = layout.lines.map(line => line.text);
|
|
313
433
|
|
|
434
|
+
// Set _text as flat array of all graphemes (required for editing)
|
|
435
|
+
this._text = layout.lines.flatMap(line => line.graphemes);
|
|
436
|
+
|
|
314
437
|
// Convert bounds to legacy format
|
|
438
|
+
// IMPORTANT: Preserve both logical (left) and visual (renderLeft) positions
|
|
439
|
+
// - left: cumulative logical offset (for text editing operations)
|
|
440
|
+
// - renderLeft: actual visual X position after BiDi reordering and alignment
|
|
441
|
+
// The renderLeft is critical for correct cursor/selection hit testing in mixed RTL/LTR text
|
|
315
442
|
this.__charBounds = layout.lines.map(line => line.bounds.map(bound => ({
|
|
316
443
|
left: bound.left,
|
|
317
444
|
top: bound.y,
|
|
318
445
|
width: bound.width,
|
|
319
446
|
height: bound.height,
|
|
320
447
|
kernedWidth: bound.kernedWidth,
|
|
321
|
-
deltaY: bound.deltaY || 0
|
|
448
|
+
deltaY: bound.deltaY || 0,
|
|
449
|
+
renderLeft: bound.x // Visual X position for hit testing
|
|
322
450
|
})));
|
|
323
451
|
|
|
452
|
+
// Populate line widths cache to prevent getLineWidth from triggering legacy measurement
|
|
453
|
+
this.__lineWidths = layout.lines.map(line => line.width);
|
|
454
|
+
|
|
324
455
|
// Update grapheme info for compatibility
|
|
325
456
|
if (layout.lines.length > 0) {
|
|
326
457
|
this._unwrappedTextLines = layout.lines.map(line => line.graphemes);
|
|
@@ -488,6 +619,48 @@ class FabricText extends StyledText {
|
|
|
488
619
|
this._renderChars(method, ctx, line, left, top, lineIndex);
|
|
489
620
|
}
|
|
490
621
|
|
|
622
|
+
/**
|
|
623
|
+
* Build display text lines with kashida characters inserted.
|
|
624
|
+
* This creates a version of _textLines with tatweel characters added at kashida points.
|
|
625
|
+
* @private
|
|
626
|
+
*/
|
|
627
|
+
_buildKashidaDisplayLines() {
|
|
628
|
+
if (this.kashida === 'none' || !this.__kashidaInfo) {
|
|
629
|
+
return this._textLines;
|
|
630
|
+
}
|
|
631
|
+
const displayLines = [];
|
|
632
|
+
for (let lineIndex = 0; lineIndex < this._textLines.length; lineIndex++) {
|
|
633
|
+
const line = this._textLines[lineIndex];
|
|
634
|
+
const kashidaInfo = this.__kashidaInfo[lineIndex];
|
|
635
|
+
if (!kashidaInfo || kashidaInfo.length === 0) {
|
|
636
|
+
displayLines.push([...line]);
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Sort kashida points by charIndex descending so we can insert from the end
|
|
641
|
+
const sortedKashida = [...kashidaInfo].sort((a, b) => b.charIndex - a.charIndex);
|
|
642
|
+
|
|
643
|
+
// Calculate how many tatweels to insert based on width
|
|
644
|
+
const newLine = [...line];
|
|
645
|
+
for (const {
|
|
646
|
+
charIndex,
|
|
647
|
+
width
|
|
648
|
+
} of sortedKashida) {
|
|
649
|
+
if (width <= 0 || charIndex >= newLine.length) continue;
|
|
650
|
+
|
|
651
|
+
// Calculate number of tatweel characters based on width
|
|
652
|
+
// Each tatweel is approximately 5px at font size 24
|
|
653
|
+
const tatweelCount = Math.max(1, Math.round(width / 3));
|
|
654
|
+
const tatweels = ARABIC_TATWEEL.repeat(tatweelCount);
|
|
655
|
+
|
|
656
|
+
// Insert tatweels after the character at charIndex
|
|
657
|
+
newLine.splice(charIndex + 1, 0, tatweels);
|
|
658
|
+
}
|
|
659
|
+
displayLines.push(newLine);
|
|
660
|
+
}
|
|
661
|
+
return displayLines;
|
|
662
|
+
}
|
|
663
|
+
|
|
491
664
|
/**
|
|
492
665
|
* Renders the text background for lines, taking care of style
|
|
493
666
|
* @private
|
|
@@ -568,10 +741,13 @@ class FabricText extends StyledText {
|
|
|
568
741
|
const fontCache = cache.getFontCache(charStyle),
|
|
569
742
|
fontDeclaration = this._getFontDeclaration(charStyle),
|
|
570
743
|
couple = previousChar + _char,
|
|
571
|
-
|
|
744
|
+
// Skip kerning for tatweel (kashida) characters - they extend connections
|
|
745
|
+
// and kerning would make the following character appear too narrow
|
|
746
|
+
isTatweel = previousChar === '\u0640',
|
|
747
|
+
stylesAreEqual = previousChar && !isTatweel && fontDeclaration === this._getFontDeclaration(prevCharStyle),
|
|
572
748
|
fontMultiplier = charStyle.fontSize / this.CACHE_FONT_SIZE;
|
|
573
749
|
let width, coupleWidth, previousWidth, kernedWidth;
|
|
574
|
-
if (previousChar && fontCache[previousChar] !== undefined) {
|
|
750
|
+
if (previousChar && !isTatweel && fontCache[previousChar] !== undefined) {
|
|
575
751
|
previousWidth = fontCache[previousChar];
|
|
576
752
|
}
|
|
577
753
|
if (fontCache[_char] !== undefined) {
|
|
@@ -589,11 +765,11 @@ class FabricText extends StyledText {
|
|
|
589
765
|
kernedWidth = width = ctx.measureText(_char).width;
|
|
590
766
|
fontCache[_char] = width;
|
|
591
767
|
}
|
|
592
|
-
if (previousWidth === undefined && stylesAreEqual && previousChar) {
|
|
768
|
+
if (previousWidth === undefined && stylesAreEqual && previousChar && !isTatweel) {
|
|
593
769
|
previousWidth = ctx.measureText(previousChar).width;
|
|
594
770
|
fontCache[previousChar] = previousWidth;
|
|
595
771
|
}
|
|
596
|
-
if (stylesAreEqual && coupleWidth === undefined) {
|
|
772
|
+
if (stylesAreEqual && coupleWidth === undefined && !isTatweel) {
|
|
597
773
|
// we can measure the kerning couple and subtract the width of the previous character
|
|
598
774
|
coupleWidth = ctx.measureText(couple).width;
|
|
599
775
|
fontCache[couple] = coupleWidth;
|
|
@@ -640,10 +816,7 @@ class FabricText extends StyledText {
|
|
|
640
816
|
*/
|
|
641
817
|
_measureLine(lineIndex) {
|
|
642
818
|
// Debug: detect if measureLine is called after justify was applied
|
|
643
|
-
if (this._justifyApplied)
|
|
644
|
-
console.warn(`WARNING: _measureLine called for line ${lineIndex} AFTER justify was applied! This will overwrite justified charBounds.`);
|
|
645
|
-
console.trace('Stack trace:');
|
|
646
|
-
}
|
|
819
|
+
if (this._justifyApplied) ;
|
|
647
820
|
let width = 0,
|
|
648
821
|
prevGrapheme,
|
|
649
822
|
graphemeInfo;
|
|
@@ -818,13 +991,7 @@ class FabricText extends StyledText {
|
|
|
818
991
|
top = this._getTopOffset();
|
|
819
992
|
|
|
820
993
|
// Debug: log once per render
|
|
821
|
-
if (method === 'fillText' && (_this$textAlign = this.textAlign) !== null && _this$textAlign !== void 0 && _this$textAlign.includes('justify'))
|
|
822
|
-
console.log('=== RENDER DEBUG ===');
|
|
823
|
-
console.log('direction:', this.direction);
|
|
824
|
-
console.log('textAlign:', this.textAlign);
|
|
825
|
-
console.log('width:', this.width);
|
|
826
|
-
console.log('_getLeftOffset:', left);
|
|
827
|
-
}
|
|
994
|
+
if (method === 'fillText' && (_this$textAlign = this.textAlign) !== null && _this$textAlign !== void 0 && _this$textAlign.includes('justify')) ;
|
|
828
995
|
for (let i = 0, len = this._textLines.length; i < len; i++) {
|
|
829
996
|
var _this$textAlign2;
|
|
830
997
|
const heightOfLine = this.getHeightOfLine(i),
|
|
@@ -833,8 +1000,8 @@ class FabricText extends StyledText {
|
|
|
833
1000
|
|
|
834
1001
|
// Debug: log line offsets for justify
|
|
835
1002
|
if (method === 'fillText' && (_this$textAlign2 = this.textAlign) !== null && _this$textAlign2 !== void 0 && _this$textAlign2.includes('justify')) {
|
|
836
|
-
|
|
837
|
-
console.log(`Line ${i}: leftOffset=${leftOffset.toFixed(2)}, lineWidth=${lineWidth.toFixed(2)}, renderAt=${(left + leftOffset).toFixed(2)}`);
|
|
1003
|
+
this.getLineWidth(i);
|
|
1004
|
+
// console.log(`Line ${i}: leftOffset=${leftOffset.toFixed(2)}, lineWidth=${lineWidth.toFixed(2)}, renderAt=${(left + leftOffset).toFixed(2)}`);
|
|
838
1005
|
}
|
|
839
1006
|
this._renderTextLine(method, ctx, this._textLines[i], left + leftOffset, top + lineHeights + maxHeight, i);
|
|
840
1007
|
lineHeights += heightOfLine;
|
|
@@ -885,12 +1052,18 @@ class FabricText extends StyledText {
|
|
|
885
1052
|
const lineHeight = this.getHeightOfLine(lineIndex),
|
|
886
1053
|
isJustify = this.textAlign.includes(JUSTIFY),
|
|
887
1054
|
path = this.path,
|
|
888
|
-
shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path,
|
|
889
1055
|
isLtr = this.direction === 'ltr',
|
|
890
1056
|
sign = this.direction === 'ltr' ? 1 : -1,
|
|
891
1057
|
// this was changed in the PR #7674
|
|
892
1058
|
// currentDirection = ctx.canvas.getAttribute('dir');
|
|
893
1059
|
currentDirection = ctx.direction;
|
|
1060
|
+
|
|
1061
|
+
// Check if we should use BiDi-aware rendering with pre-calculated positions
|
|
1062
|
+
// This is needed for advanced layout with RTL or mixed BiDi text
|
|
1063
|
+
const chars = this.__charBounds[lineIndex];
|
|
1064
|
+
this.enableAdvancedLayout && (chars === null || chars === void 0 ? void 0 : chars.length) > 0 && chars[0].renderLeft !== undefined;
|
|
1065
|
+
|
|
1066
|
+
const shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path;
|
|
894
1067
|
let actualStyle,
|
|
895
1068
|
nextStyle,
|
|
896
1069
|
charsToRender = '',
|
|
@@ -899,6 +1072,9 @@ class FabricText extends StyledText {
|
|
|
899
1072
|
timeToRender,
|
|
900
1073
|
drawingLeft;
|
|
901
1074
|
ctx.save();
|
|
1075
|
+
|
|
1076
|
+
// For BiDi rendering with pre-calculated positions, disable browser BiDi
|
|
1077
|
+
// and render each character at its calculated visual position
|
|
902
1078
|
if (currentDirection !== this.direction) {
|
|
903
1079
|
ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl');
|
|
904
1080
|
ctx.direction = isLtr ? 'ltr' : 'rtl';
|
|
@@ -918,12 +1094,12 @@ class FabricText extends StyledText {
|
|
|
918
1094
|
}
|
|
919
1095
|
// Debug: Log charBounds being used for first line only during justify
|
|
920
1096
|
if (isJustify && lineIndex === 0 && method === 'fillText') {
|
|
921
|
-
console.log(`\n=== RENDER _renderChars line ${lineIndex} ===`);
|
|
922
|
-
console.log('Initial left:', left.toFixed(2), 'sign:', sign);
|
|
923
|
-
console.log('_justifyApplied flag:', this._justifyApplied);
|
|
1097
|
+
// console.log(`\n=== RENDER _renderChars line ${lineIndex} ===`);
|
|
1098
|
+
// console.log('Initial left:', left.toFixed(2), 'sign:', sign);
|
|
1099
|
+
// console.log('_justifyApplied flag:', (this as any)._justifyApplied);
|
|
924
1100
|
const lineBounds = this.__charBounds[lineIndex];
|
|
925
|
-
|
|
926
|
-
console.log('Total kernedWidth in charBounds:', totalKW.toFixed(2), '(should be ~300 if justify was applied)');
|
|
1101
|
+
(lineBounds === null || lineBounds === void 0 ? void 0 : lineBounds.reduce((s, b) => s + ((b === null || b === void 0 ? void 0 : b.kernedWidth) || 0), 0)) || 0;
|
|
1102
|
+
// console.log('Total kernedWidth in charBounds:', totalKW.toFixed(2), '(should be ~300 if justify was applied)');
|
|
927
1103
|
// Log first few space widths to verify expansion
|
|
928
1104
|
const spaceIndices = [3, 9, 15, 23, 31];
|
|
929
1105
|
spaceIndices.forEach(idx => {
|
|
@@ -972,10 +1148,6 @@ class FabricText extends StyledText {
|
|
|
972
1148
|
// For RTL with textAlign='right': x is the right edge, so drawingLeft = left
|
|
973
1149
|
// Both cases: drawingLeft = left (the text alignment handles the edge correctly)
|
|
974
1150
|
drawingLeft = left;
|
|
975
|
-
// Debug: log first chunk positioning for justify
|
|
976
|
-
if (isJustify && lineIndex === 0 && method === 'fillText' && i < 5) {
|
|
977
|
-
console.log(` Chunk ending at char ${i}: left=${left.toFixed(2)}, boxWidth=${boxWidth.toFixed(2)}, drawingLeft=${drawingLeft.toFixed(2)}, textAlign=${isLtr ? 'left' : 'right'}`);
|
|
978
|
-
}
|
|
979
1151
|
this._renderChar(method, ctx, lineIndex, i, charsToRender, drawingLeft, top);
|
|
980
1152
|
}
|
|
981
1153
|
charsToRender = '';
|
|
@@ -984,11 +1156,6 @@ class FabricText extends StyledText {
|
|
|
984
1156
|
boxWidth = 0;
|
|
985
1157
|
}
|
|
986
1158
|
}
|
|
987
|
-
// Debug: log final position for justify
|
|
988
|
-
if (isJustify && lineIndex === 0 && method === 'fillText') {
|
|
989
|
-
console.log('Final left position after rendering:', left.toFixed(2));
|
|
990
|
-
console.log('Expected final position:', (sign > 0 ? this.width / 2 : -this.width / 2).toFixed(2));
|
|
991
|
-
}
|
|
992
1159
|
ctx.restore();
|
|
993
1160
|
}
|
|
994
1161
|
|
|
@@ -1220,12 +1387,159 @@ class FabricText extends StyledText {
|
|
|
1220
1387
|
* @private
|
|
1221
1388
|
*/
|
|
1222
1389
|
_clearCache() {
|
|
1390
|
+
// console.log('🗑️ _clearCache called');
|
|
1391
|
+
// console.trace('🗑️ _clearCache stack trace');
|
|
1223
1392
|
this._forceClearCache = false;
|
|
1224
1393
|
this.__lineWidths = [];
|
|
1225
1394
|
this.__lineHeights = [];
|
|
1226
1395
|
this.__charBounds = [];
|
|
1396
|
+
this.__kashidaInfo = [];
|
|
1227
1397
|
// Reset justify applied flag
|
|
1228
1398
|
this._justifyApplied = false;
|
|
1399
|
+
// Reset dimension state to force recalculation
|
|
1400
|
+
this._lastDimensionState = null;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* Convert a display character index (in _textLines with tatweels) to original text index.
|
|
1405
|
+
* When kashida is applied, _textLines contains extra tatweel characters that don't exist
|
|
1406
|
+
* in the original text. This method maps back to the original index.
|
|
1407
|
+
* @param lineIndex - The line index
|
|
1408
|
+
* @param displayCharIndex - Character index in the display text (with tatweels)
|
|
1409
|
+
* @returns Original character index (without tatweels)
|
|
1410
|
+
*/
|
|
1411
|
+
_displayToOriginalIndex(lineIndex, displayCharIndex) {
|
|
1412
|
+
var _this$__kashidaInfo;
|
|
1413
|
+
// console.log(`🔄 _displayToOriginalIndex called: line=${lineIndex}, displayIdx=${displayCharIndex}`);
|
|
1414
|
+
// console.log(`🔄 __kashidaInfo exists: ${!!this.__kashidaInfo}, length: ${this.__kashidaInfo?.length}`);
|
|
1415
|
+
// console.log(`🔄 __kashidaInfo raw:`, JSON.stringify(this.__kashidaInfo));
|
|
1416
|
+
|
|
1417
|
+
const kashidaInfo = (_this$__kashidaInfo = this.__kashidaInfo) === null || _this$__kashidaInfo === void 0 ? void 0 : _this$__kashidaInfo[lineIndex];
|
|
1418
|
+
if (!kashidaInfo || kashidaInfo.length === 0) {
|
|
1419
|
+
// No kashida on this line, indices are the same
|
|
1420
|
+
// console.log(`🔄 No kashida info for line ${lineIndex}, returning same index`);
|
|
1421
|
+
return displayCharIndex;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// Sort kashida info by charIndex ascending for proper traversal
|
|
1425
|
+
const sortedKashida = [...kashidaInfo].sort((a, b) => a.charIndex - b.charIndex);
|
|
1426
|
+
|
|
1427
|
+
// console.log(`🔄 _displayToOriginalIndex: line=${lineIndex}, displayIdx=${displayCharIndex}`);
|
|
1428
|
+
// console.log(`🔄 kashidaInfo:`, sortedKashida.map(k => `{charIdx:${k.charIndex}, cnt:${k.tatweelCount}}`).join(', '));
|
|
1429
|
+
|
|
1430
|
+
let tatweelsBeforeIndex = 0;
|
|
1431
|
+
for (const k of sortedKashida) {
|
|
1432
|
+
const tatweelCount = k.tatweelCount || 0;
|
|
1433
|
+
// Position where tatweels start (after the original character)
|
|
1434
|
+
const tatweelStartPos = k.charIndex + 1 + tatweelsBeforeIndex;
|
|
1435
|
+
const tatweelEndPos = tatweelStartPos + tatweelCount;
|
|
1436
|
+
|
|
1437
|
+
// console.log(`🔄 k.charIndex=${k.charIndex}, tatweelStartPos=${tatweelStartPos}, tatweelEndPos=${tatweelEndPos}, tatweelsBeforeIndex=${tatweelsBeforeIndex}`);
|
|
1438
|
+
|
|
1439
|
+
if (displayCharIndex < tatweelStartPos) {
|
|
1440
|
+
// Before this kashida point
|
|
1441
|
+
// console.log(`🔄 displayIdx < tatweelStartPos, break`);
|
|
1442
|
+
break;
|
|
1443
|
+
} else if (displayCharIndex < tatweelEndPos) {
|
|
1444
|
+
// Within tatweel characters - map to the character before tatweels
|
|
1445
|
+
// console.log(`🔄 Within tatweel, return ${k.charIndex + 1}`);
|
|
1446
|
+
return k.charIndex + 1;
|
|
1447
|
+
} else {
|
|
1448
|
+
// After this kashida point
|
|
1449
|
+
tatweelsBeforeIndex += tatweelCount;
|
|
1450
|
+
// console.log(`🔄 After this kashida, tatweelsBeforeIndex now=${tatweelsBeforeIndex}`);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// Subtract all tatweels that come before this position
|
|
1455
|
+
const result = displayCharIndex - tatweelsBeforeIndex;
|
|
1456
|
+
// console.log(`🔄 Final result: ${displayCharIndex} - ${tatweelsBeforeIndex} = ${result}`);
|
|
1457
|
+
return result;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* Convert an original text character index to display index (in _textLines with tatweels).
|
|
1462
|
+
* @param lineIndex - The line index
|
|
1463
|
+
* @param originalCharIndex - Character index in the original text (without tatweels)
|
|
1464
|
+
* @returns Display character index (with tatweels)
|
|
1465
|
+
*/
|
|
1466
|
+
_originalToDisplayIndex(lineIndex, originalCharIndex) {
|
|
1467
|
+
var _this$__kashidaInfo2;
|
|
1468
|
+
const kashidaInfo = (_this$__kashidaInfo2 = this.__kashidaInfo) === null || _this$__kashidaInfo2 === void 0 ? void 0 : _this$__kashidaInfo2[lineIndex];
|
|
1469
|
+
if (!kashidaInfo || kashidaInfo.length === 0) {
|
|
1470
|
+
// No kashida on this line, indices are the same
|
|
1471
|
+
return originalCharIndex;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Sort kashida info by charIndex ascending
|
|
1475
|
+
const sortedKashida = [...kashidaInfo].sort((a, b) => a.charIndex - b.charIndex);
|
|
1476
|
+
let tatweelsBeforeIndex = 0;
|
|
1477
|
+
for (const k of sortedKashida) {
|
|
1478
|
+
const tatweelCount = k.tatweelCount || 0;
|
|
1479
|
+
// If the original char index is after this kashida insertion point,
|
|
1480
|
+
// add the tatweels to the offset
|
|
1481
|
+
if (originalCharIndex > k.charIndex) {
|
|
1482
|
+
tatweelsBeforeIndex += tatweelCount;
|
|
1483
|
+
} else {
|
|
1484
|
+
break;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return originalCharIndex + tatweelsBeforeIndex;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
/**
|
|
1491
|
+
* Check if a display character index points to a tatweel character.
|
|
1492
|
+
* @param lineIndex - The line index
|
|
1493
|
+
* @param displayCharIndex - Character index in the display text
|
|
1494
|
+
* @returns True if the character at this index is a tatweel
|
|
1495
|
+
*/
|
|
1496
|
+
_isTatweelAtDisplayIndex(lineIndex, displayCharIndex) {
|
|
1497
|
+
var _this$__kashidaInfo3;
|
|
1498
|
+
const kashidaInfo = (_this$__kashidaInfo3 = this.__kashidaInfo) === null || _this$__kashidaInfo3 === void 0 ? void 0 : _this$__kashidaInfo3[lineIndex];
|
|
1499
|
+
if (!kashidaInfo || kashidaInfo.length === 0) {
|
|
1500
|
+
return false;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// Sort kashida info by charIndex ascending
|
|
1504
|
+
const sortedKashida = [...kashidaInfo].sort((a, b) => a.charIndex - b.charIndex);
|
|
1505
|
+
let tatweelsBeforeIndex = 0;
|
|
1506
|
+
for (const k of sortedKashida) {
|
|
1507
|
+
const tatweelCount = k.tatweelCount || 0;
|
|
1508
|
+
const tatweelStartPos = k.charIndex + 1 + tatweelsBeforeIndex;
|
|
1509
|
+
const tatweelEndPos = tatweelStartPos + tatweelCount;
|
|
1510
|
+
if (displayCharIndex >= tatweelStartPos && displayCharIndex < tatweelEndPos) {
|
|
1511
|
+
return true;
|
|
1512
|
+
}
|
|
1513
|
+
tatweelsBeforeIndex += tatweelCount;
|
|
1514
|
+
}
|
|
1515
|
+
return false;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
/**
|
|
1519
|
+
* Get the total number of tatweel characters inserted in a line.
|
|
1520
|
+
* @param lineIndex - The line index
|
|
1521
|
+
* @returns Total number of tatweels in this line
|
|
1522
|
+
*/
|
|
1523
|
+
_getTatweelCountForLine(lineIndex) {
|
|
1524
|
+
var _this$__kashidaInfo4;
|
|
1525
|
+
const kashidaInfo = (_this$__kashidaInfo4 = this.__kashidaInfo) === null || _this$__kashidaInfo4 === void 0 ? void 0 : _this$__kashidaInfo4[lineIndex];
|
|
1526
|
+
if (!kashidaInfo || kashidaInfo.length === 0) {
|
|
1527
|
+
return 0;
|
|
1528
|
+
}
|
|
1529
|
+
return kashidaInfo.reduce((sum, k) => sum + (k.tatweelCount || 0), 0);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
/**
|
|
1533
|
+
* Get the original line length (without tatweels).
|
|
1534
|
+
* When kashida is applied, _textLines contains extra tatweel characters.
|
|
1535
|
+
* This returns the length as it would be in the original text.
|
|
1536
|
+
* @param lineIndex - The line index
|
|
1537
|
+
* @returns Original line length without tatweels
|
|
1538
|
+
*/
|
|
1539
|
+
_getOriginalLineLength(lineIndex) {
|
|
1540
|
+
var _this$_textLines$line;
|
|
1541
|
+
const displayLength = ((_this$_textLines$line = this._textLines[lineIndex]) === null || _this$_textLines$line === void 0 ? void 0 : _this$_textLines$line.length) || 0;
|
|
1542
|
+
return displayLength - this._getTatweelCountForLine(lineIndex);
|
|
1229
1543
|
}
|
|
1230
1544
|
|
|
1231
1545
|
/**
|