@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
|
@@ -29,10 +29,11 @@ export const textLayoutProperties: string[] = [
|
|
|
29
29
|
'pathSide',
|
|
30
30
|
'pathAlign',
|
|
31
31
|
'wrap',
|
|
32
|
-
'ellipsis',
|
|
32
|
+
'ellipsis',
|
|
33
33
|
'letterSpacing',
|
|
34
34
|
'enableAdvancedLayout',
|
|
35
35
|
'verticalAlign',
|
|
36
|
+
'kashida',
|
|
36
37
|
];
|
|
37
38
|
|
|
38
39
|
export const additionalProps = [
|
|
@@ -104,7 +105,8 @@ export const textDefaultValues: Partial<TClassProperties<FabricText>> = {
|
|
|
104
105
|
letterSpacing: 0,
|
|
105
106
|
enableAdvancedLayout: false,
|
|
106
107
|
verticalAlign: 'top' as const,
|
|
107
|
-
|
|
108
|
+
kashida: 'none' as const,
|
|
109
|
+
|
|
108
110
|
// Overlay editor properties
|
|
109
111
|
useOverlayEditing: false,
|
|
110
112
|
|
package/src/shapes/Textbox.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { TextLinesInfo } from './Text/Text';
|
|
|
10
10
|
import type { Control } from '../controls/Control';
|
|
11
11
|
import { fontLacksEnglishGlyphsCached } from '../text/measure';
|
|
12
12
|
import { layoutText } from '../text/layout';
|
|
13
|
+
import { findKashidaPoints, ARABIC_TATWEEL } from '../text/unicode';
|
|
13
14
|
|
|
14
15
|
// @TODO: Many things here are configuration related and shouldn't be on the class nor prototype
|
|
15
16
|
// regexes, list of properties that are not suppose to change by instances, magic consts.
|
|
@@ -137,7 +138,7 @@ export class Textbox<
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
// Skip if nothing changed
|
|
140
|
-
const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
|
|
141
|
+
const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}|${this.kashida}`;
|
|
141
142
|
if (
|
|
142
143
|
(this as any)._lastDimensionState === currentState &&
|
|
143
144
|
this._textLines &&
|
|
@@ -255,12 +256,20 @@ export class Textbox<
|
|
|
255
256
|
}
|
|
256
257
|
|
|
257
258
|
// Use new layout engine
|
|
259
|
+
// When kashida is enabled, don't let layout engine apply justify - we'll handle it with kashida
|
|
260
|
+
const useKashidaJustify = this.kashida !== 'none' && this.textAlign.includes(JUSTIFY);
|
|
261
|
+
const effectiveAlign = useKashidaJustify
|
|
262
|
+
? (this.direction === 'rtl' ? 'right' : 'left') // Natural alignment, kashida will justify
|
|
263
|
+
: (this as any)._mapTextAlignToAlign(this.textAlign);
|
|
264
|
+
|
|
258
265
|
const layout = layoutText({
|
|
259
266
|
text: this.text,
|
|
260
267
|
width: this.width,
|
|
261
|
-
height
|
|
268
|
+
// Don't pass height constraint to allow vertical auto-expansion
|
|
269
|
+
// Only pass height if explicitly set to constrain (e.g., for ellipsis)
|
|
270
|
+
height: this.ellipsis ? this.height : undefined,
|
|
262
271
|
wrap: this.wrap || 'word',
|
|
263
|
-
align:
|
|
272
|
+
align: effectiveAlign,
|
|
264
273
|
ellipsis: this.ellipsis || false,
|
|
265
274
|
fontSize: this.fontSize,
|
|
266
275
|
lineHeight: this.lineHeight,
|
|
@@ -299,9 +308,275 @@ export class Textbox<
|
|
|
299
308
|
|
|
300
309
|
// Generate style map for compatibility
|
|
301
310
|
this._styleMap = this._generateStyleMapFromLayout(layout);
|
|
311
|
+
|
|
312
|
+
// Apply kashida for justified text in advanced layout mode
|
|
313
|
+
if (this.textAlign.includes(JUSTIFY) && this.kashida !== 'none') {
|
|
314
|
+
this._applyKashidaToLayout();
|
|
315
|
+
}
|
|
316
|
+
|
|
302
317
|
this.dirty = true;
|
|
303
318
|
}
|
|
304
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Apply kashida (tatweel) characters to layout for Arabic text justification.
|
|
322
|
+
* This method INSERTS actual tatweel characters into the text lines.
|
|
323
|
+
* @private
|
|
324
|
+
*/
|
|
325
|
+
_applyKashidaToLayout() {
|
|
326
|
+
if (!this._textLines || !this.__charBounds) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Clear visual positions cache - it becomes stale when kashida is applied
|
|
331
|
+
// Check if cache exists (it's initialized in IText constructor which runs after this during construction)
|
|
332
|
+
if ((this as any)._visualPositionsCache) {
|
|
333
|
+
this._clearVisualPositionsCache();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const kashidaRatios: Record<string, number> = {
|
|
337
|
+
none: 0,
|
|
338
|
+
short: 0.25,
|
|
339
|
+
medium: 0.5,
|
|
340
|
+
long: 0.75,
|
|
341
|
+
stylistic: 1.0,
|
|
342
|
+
};
|
|
343
|
+
const kashidaRatio = kashidaRatios[this.kashida] || 0;
|
|
344
|
+
|
|
345
|
+
if (kashidaRatio === 0) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Calculate tatweel width once
|
|
350
|
+
const canvas = document.createElement('canvas');
|
|
351
|
+
const ctx = canvas.getContext('2d');
|
|
352
|
+
if (!ctx) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
ctx.font = this._getFontDeclaration();
|
|
356
|
+
const tatweelWidth = ctx.measureText(ARABIC_TATWEEL).width;
|
|
357
|
+
|
|
358
|
+
if (tatweelWidth <= 0) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Reset kashida info
|
|
363
|
+
this.__kashidaInfo = [];
|
|
364
|
+
|
|
365
|
+
const totalLines = this._textLines.length;
|
|
366
|
+
|
|
367
|
+
for (let lineIndex = 0; lineIndex < totalLines; lineIndex++) {
|
|
368
|
+
this.__kashidaInfo[lineIndex] = [];
|
|
369
|
+
const line = this._textLines[lineIndex];
|
|
370
|
+
|
|
371
|
+
if (!this.__charBounds || !this.__charBounds[lineIndex]) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Don't apply kashida to the last line
|
|
376
|
+
const isLastLine = lineIndex === totalLines - 1;
|
|
377
|
+
if (isLastLine) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const lineBounds = this.__charBounds[lineIndex];
|
|
382
|
+
const lastBound = lineBounds[lineBounds.length - 1];
|
|
383
|
+
|
|
384
|
+
// Calculate current line width
|
|
385
|
+
const currentLineWidth = lastBound ? (lastBound.left + lastBound.kernedWidth) : 0;
|
|
386
|
+
const totalExtraSpace = this.width - currentLineWidth;
|
|
387
|
+
|
|
388
|
+
// Only apply kashida if there's significant extra space to fill
|
|
389
|
+
if (totalExtraSpace <= 2) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Find kashida points
|
|
394
|
+
const kashidaPoints = findKashidaPoints(line);
|
|
395
|
+
if (kashidaPoints.length === 0) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Calculate kashida space
|
|
400
|
+
const kashidaSpace = totalExtraSpace * kashidaRatio;
|
|
401
|
+
|
|
402
|
+
// Calculate how many tatweels can fit
|
|
403
|
+
const totalTatweels = Math.floor(kashidaSpace / tatweelWidth);
|
|
404
|
+
if (totalTatweels === 0) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Limit kashida points
|
|
409
|
+
const maxKashidaPoints = Math.min(kashidaPoints.length, totalTatweels);
|
|
410
|
+
const usedKashidaPoints = kashidaPoints.slice(0, maxKashidaPoints);
|
|
411
|
+
|
|
412
|
+
// Distribute tatweels evenly
|
|
413
|
+
const tatweelsPerPoint = Math.floor(totalTatweels / maxKashidaPoints);
|
|
414
|
+
const extraTatweels = totalTatweels % maxKashidaPoints;
|
|
415
|
+
|
|
416
|
+
// console.log(`=== Inserting Kashida into line ${lineIndex} ===`);
|
|
417
|
+
// console.log(` totalTatweels: ${totalTatweels}, usedPoints: ${usedKashidaPoints.length}`);
|
|
418
|
+
|
|
419
|
+
// Sort by charIndex descending so we insert from the end (prevents index shifting issues)
|
|
420
|
+
const sortedPoints = [...usedKashidaPoints].sort((a, b) => b.charIndex - a.charIndex);
|
|
421
|
+
|
|
422
|
+
// Create new line with tatweels inserted
|
|
423
|
+
const newLine = [...line];
|
|
424
|
+
for (let i = 0; i < sortedPoints.length; i++) {
|
|
425
|
+
const point = sortedPoints[i];
|
|
426
|
+
const originalIndex = usedKashidaPoints.indexOf(point);
|
|
427
|
+
const count = tatweelsPerPoint + (originalIndex < extraTatweels ? 1 : 0);
|
|
428
|
+
|
|
429
|
+
if (count > 0) {
|
|
430
|
+
// Insert tatweels AFTER the character at charIndex
|
|
431
|
+
const tatweels = Array(count).fill(ARABIC_TATWEEL);
|
|
432
|
+
newLine.splice(point.charIndex + 1, 0, ...tatweels);
|
|
433
|
+
// console.log(` Inserted ${count} tatweels after char ${point.charIndex}`);
|
|
434
|
+
|
|
435
|
+
// Store kashida info for index conversion
|
|
436
|
+
this.__kashidaInfo[lineIndex].push({
|
|
437
|
+
charIndex: point.charIndex,
|
|
438
|
+
width: count * tatweelWidth,
|
|
439
|
+
tatweelCount: count,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Update _textLines with the new line containing tatweels
|
|
445
|
+
this._textLines[lineIndex] = newLine;
|
|
446
|
+
|
|
447
|
+
// Update textLines (string version)
|
|
448
|
+
if (this.textLines) {
|
|
449
|
+
(this as any).textLines[lineIndex] = newLine.join('');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Clear and recalculate charBounds for this line
|
|
453
|
+
this.__charBounds[lineIndex] = [];
|
|
454
|
+
this.__lineWidths[lineIndex] = undefined as any;
|
|
455
|
+
this._measureLine(lineIndex);
|
|
456
|
+
|
|
457
|
+
// Now expand spaces to fill any remaining gap
|
|
458
|
+
let newLineBounds = this.__charBounds[lineIndex];
|
|
459
|
+
if (newLineBounds && newLineBounds.length > 0) {
|
|
460
|
+
let newLastBound = newLineBounds[newLineBounds.length - 1];
|
|
461
|
+
let newLineWidth = newLastBound ? (newLastBound.left + newLastBound.kernedWidth) : 0;
|
|
462
|
+
let remainingGap = this.width - newLineWidth;
|
|
463
|
+
|
|
464
|
+
if (remainingGap > 0.5) {
|
|
465
|
+
// Count spaces in the new line
|
|
466
|
+
let spaceCount = 0;
|
|
467
|
+
for (let i = 0; i < newLine.length; i++) {
|
|
468
|
+
if (/\s/.test(newLine[i])) {
|
|
469
|
+
spaceCount++;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (spaceCount > 0) {
|
|
474
|
+
const extraPerSpace = remainingGap / spaceCount;
|
|
475
|
+
let accumulatedExtra = 0;
|
|
476
|
+
|
|
477
|
+
// Expand space widths AND update left positions for subsequent chars
|
|
478
|
+
for (let i = 0; i < newLineBounds.length; i++) {
|
|
479
|
+
const bound = newLineBounds[i];
|
|
480
|
+
if (!bound) continue;
|
|
481
|
+
|
|
482
|
+
// Update left position to account for previous space expansions
|
|
483
|
+
bound.left += accumulatedExtra;
|
|
484
|
+
|
|
485
|
+
// If this is a space, expand it
|
|
486
|
+
if (/\s/.test(newLine[i])) {
|
|
487
|
+
bound.width += extraPerSpace;
|
|
488
|
+
bound.kernedWidth += extraPerSpace;
|
|
489
|
+
accumulatedExtra += extraPerSpace;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Update the extra entry at the end (cursor position)
|
|
493
|
+
if (newLineBounds[newLine.length]) {
|
|
494
|
+
newLineBounds[newLine.length].left += accumulatedExtra;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Recalculate remaining gap after space expansion
|
|
498
|
+
newLastBound = newLineBounds[newLineBounds.length - 1];
|
|
499
|
+
newLineWidth = newLastBound ? (newLastBound.left + newLastBound.kernedWidth) : 0;
|
|
500
|
+
remainingGap = this.width - newLineWidth;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// If there's still a gap after space expansion, distribute it across all kashida points
|
|
505
|
+
if (remainingGap > 0.5 && this.__kashidaInfo[lineIndex].length > 0) {
|
|
506
|
+
const kashidaPointCount = this.__kashidaInfo[lineIndex].length;
|
|
507
|
+
const extraPerKashida = remainingGap / kashidaPointCount;
|
|
508
|
+
|
|
509
|
+
// Find kashida positions in newLine and expand their widths
|
|
510
|
+
let kashidaIndex = 0;
|
|
511
|
+
let accumulatedExtra = 0;
|
|
512
|
+
|
|
513
|
+
for (let i = 0; i < newLineBounds.length; i++) {
|
|
514
|
+
const bound = newLineBounds[i];
|
|
515
|
+
if (!bound) continue;
|
|
516
|
+
|
|
517
|
+
// Update left position for accumulated expansion
|
|
518
|
+
bound.left += accumulatedExtra;
|
|
519
|
+
|
|
520
|
+
// Check if this is a tatweel character
|
|
521
|
+
if (newLine[i] === ARABIC_TATWEEL) {
|
|
522
|
+
// Distribute extra width among tatweels
|
|
523
|
+
const extraForThis = extraPerKashida / (this.__kashidaInfo[lineIndex][kashidaIndex]?.tatweelCount || 1);
|
|
524
|
+
bound.width += extraForThis;
|
|
525
|
+
bound.kernedWidth += extraForThis;
|
|
526
|
+
accumulatedExtra += extraForThis;
|
|
527
|
+
|
|
528
|
+
// Move to next kashida info when we've passed this group
|
|
529
|
+
const currentKashidaInfo = this.__kashidaInfo[lineIndex][kashidaIndex];
|
|
530
|
+
if (currentKashidaInfo && i > 0) {
|
|
531
|
+
// Check if next char is not tatweel - means we're done with this group
|
|
532
|
+
if (i + 1 >= newLine.length || newLine[i + 1] !== ARABIC_TATWEEL) {
|
|
533
|
+
kashidaIndex++;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Update the extra entry at the end
|
|
540
|
+
if (newLineBounds[newLine.length]) {
|
|
541
|
+
newLineBounds[newLine.length].left += accumulatedExtra;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Set line width to textbox width (for justified lines)
|
|
547
|
+
this.__lineWidths[lineIndex] = this.width;
|
|
548
|
+
|
|
549
|
+
// console.log(` New line length: ${newLine.length}, text: ${newLine.join('')}`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// For justified lines with kashida, line width should equal textbox width
|
|
553
|
+
// Only set undefined widths (non-justified lines without kashida)
|
|
554
|
+
for (let i = 0; i < this._textLines.length; i++) {
|
|
555
|
+
if (this.__lineWidths[i] === undefined && this.__charBounds[i]) {
|
|
556
|
+
const bounds = this.__charBounds[i];
|
|
557
|
+
const lastBound = bounds[bounds.length - 1];
|
|
558
|
+
if (lastBound) {
|
|
559
|
+
this.__lineWidths[i] = lastBound.left + lastBound.kernedWidth;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Update _text to match the new _textLines (required for editing)
|
|
565
|
+
this._text = this._textLines.flat();
|
|
566
|
+
|
|
567
|
+
// DON'T update this.text - keep the original text intact
|
|
568
|
+
// The tatweels are in _textLines and _text for rendering purposes only
|
|
569
|
+
|
|
570
|
+
(this as any)._justifyApplied = true;
|
|
571
|
+
|
|
572
|
+
// Debug log final kashida state
|
|
573
|
+
// console.log('=== _applyKashidaToLayout END ===');
|
|
574
|
+
// console.log('Final __kashidaInfo:', JSON.stringify(this.__kashidaInfo.map((lineInfo, i) => ({
|
|
575
|
+
// line: i,
|
|
576
|
+
// entries: lineInfo.map(k => ({ charIndex: k.charIndex, tatweelCount: k.tatweelCount }))
|
|
577
|
+
// }))));
|
|
578
|
+
}
|
|
579
|
+
|
|
305
580
|
/**
|
|
306
581
|
* Generate style map from new layout format
|
|
307
582
|
* @private
|
|
@@ -876,9 +1151,9 @@ export class Textbox<
|
|
|
876
1151
|
* @private
|
|
877
1152
|
*/
|
|
878
1153
|
_extractJustifySpaceMeasurements(element: HTMLElement, lines: string[]) {
|
|
879
|
-
console.log('=== _extractJustifySpaceMeasurements START ===');
|
|
880
|
-
console.log('Textbox width:', this.width);
|
|
881
|
-
console.log('Lines count:', lines.length);
|
|
1154
|
+
// console.log('=== _extractJustifySpaceMeasurements START ===');
|
|
1155
|
+
// console.log('Textbox width:', this.width);
|
|
1156
|
+
// console.log('Lines count:', lines.length);
|
|
882
1157
|
|
|
883
1158
|
const measureCtx =
|
|
884
1159
|
(this as any)._browserMeasureCtx ||
|
|
@@ -886,13 +1161,13 @@ export class Textbox<
|
|
|
886
1161
|
.createElement('canvas')
|
|
887
1162
|
.getContext('2d'));
|
|
888
1163
|
if (!measureCtx) {
|
|
889
|
-
console.log('ERROR: No measure context');
|
|
1164
|
+
// console.log('ERROR: No measure context');
|
|
890
1165
|
return [];
|
|
891
1166
|
}
|
|
892
1167
|
measureCtx.font = `${this.fontStyle || 'normal'} ${this.fontWeight || 'normal'} ${this.fontSize}px "${this.fontFamily}"`;
|
|
893
1168
|
const normalSpaceWidth = measureCtx.measureText(' ').width || 6;
|
|
894
|
-
console.log('Font:', measureCtx.font);
|
|
895
|
-
console.log('Normal space width:', normalSpaceWidth);
|
|
1169
|
+
// console.log('Font:', measureCtx.font);
|
|
1170
|
+
// console.log('Normal space width:', normalSpaceWidth);
|
|
896
1171
|
|
|
897
1172
|
const spaceWidths: number[][] = [];
|
|
898
1173
|
|
|
@@ -901,7 +1176,7 @@ export class Textbox<
|
|
|
901
1176
|
const spaceCount = (line.match(/\s/g) || []).length;
|
|
902
1177
|
const isLastLine = lineIndex === lines.length - 1;
|
|
903
1178
|
|
|
904
|
-
console.log(`\nLine ${lineIndex}: "${line.substring(0, 50)}..." spaces: ${spaceCount}, isLast: ${isLastLine}`);
|
|
1179
|
+
// console.log(`\nLine ${lineIndex}: "${line.substring(0, 50)}..." spaces: ${spaceCount}, isLast: ${isLastLine}`);
|
|
905
1180
|
|
|
906
1181
|
if (spaceCount > 0 && !isLastLine) {
|
|
907
1182
|
// Don't justify last line
|
|
@@ -910,8 +1185,8 @@ export class Textbox<
|
|
|
910
1185
|
const extraPerSpace = remainingSpace > 0 ? remainingSpace / spaceCount : 0;
|
|
911
1186
|
const expandedSpaceWidth = normalSpaceWidth + extraPerSpace;
|
|
912
1187
|
|
|
913
|
-
console.log(` Natural width: ${naturalWidth.toFixed(2)}, Remaining: ${remainingSpace.toFixed(2)}`);
|
|
914
|
-
console.log(` Extra per space: ${extraPerSpace.toFixed(2)}, Expanded space: ${expandedSpaceWidth.toFixed(2)}`);
|
|
1188
|
+
// console.log(` Natural width: ${naturalWidth.toFixed(2)}, Remaining: ${remainingSpace.toFixed(2)}`);
|
|
1189
|
+
// console.log(` Extra per space: ${extraPerSpace.toFixed(2)}, Expanded space: ${expandedSpaceWidth.toFixed(2)}`);
|
|
915
1190
|
|
|
916
1191
|
const safeWidth = Math.max(normalSpaceWidth, expandedSpaceWidth);
|
|
917
1192
|
for (let i = 0; i < spaceCount; i++) {
|
|
@@ -919,7 +1194,7 @@ export class Textbox<
|
|
|
919
1194
|
}
|
|
920
1195
|
} else if (spaceCount > 0) {
|
|
921
1196
|
// Last line: keep natural space width
|
|
922
|
-
console.log(` Last line - using normal space width: ${normalSpaceWidth}`);
|
|
1197
|
+
// console.log(` Last line - using normal space width: ${normalSpaceWidth}`);
|
|
923
1198
|
for (let i = 0; i < spaceCount; i++) {
|
|
924
1199
|
lineSpaces.push(normalSpaceWidth);
|
|
925
1200
|
}
|
|
@@ -928,42 +1203,47 @@ export class Textbox<
|
|
|
928
1203
|
spaceWidths.push(lineSpaces);
|
|
929
1204
|
});
|
|
930
1205
|
|
|
931
|
-
console.log('\nFinal spaceWidths:', spaceWidths);
|
|
932
|
-
console.log('=== _extractJustifySpaceMeasurements END ===\n');
|
|
1206
|
+
// console.log('\nFinal spaceWidths:', spaceWidths);
|
|
1207
|
+
// console.log('=== _extractJustifySpaceMeasurements END ===\n');
|
|
933
1208
|
return spaceWidths;
|
|
934
1209
|
}
|
|
935
1210
|
|
|
936
1211
|
/**
|
|
937
|
-
* Apply justify space expansion using actual charBounds measurements
|
|
1212
|
+
* Apply justify space expansion using actual charBounds measurements.
|
|
1213
|
+
* Supports Arabic kashida (tatweel) justification when kashida property is set.
|
|
938
1214
|
* @private
|
|
939
1215
|
*/
|
|
940
1216
|
_applyBrowserJustifySpaces() {
|
|
941
|
-
console.log('=== _applyBrowserJustifySpaces START ===');
|
|
942
|
-
console.log('_textLines:', this._textLines?.length, 'lines');
|
|
943
|
-
console.log('__charBounds:', this.__charBounds?.length, 'lines');
|
|
944
|
-
console.log('textbox width:', this.width);
|
|
945
|
-
|
|
946
1217
|
if (!this._textLines || !this.__charBounds) {
|
|
947
|
-
console.log('EARLY RETURN: _textLines or __charBounds missing');
|
|
948
1218
|
return;
|
|
949
1219
|
}
|
|
950
1220
|
|
|
1221
|
+
// Kashida ratios: proportion of extra space distributed via kashida vs space expansion
|
|
1222
|
+
const kashidaRatios: Record<string, number> = {
|
|
1223
|
+
none: 0,
|
|
1224
|
+
short: 0.25,
|
|
1225
|
+
medium: 0.5,
|
|
1226
|
+
long: 0.75,
|
|
1227
|
+
stylistic: 1.0,
|
|
1228
|
+
};
|
|
1229
|
+
const kashidaRatio = kashidaRatios[this.kashida] || 0;
|
|
1230
|
+
|
|
1231
|
+
// Reset kashida info
|
|
1232
|
+
this.__kashidaInfo = [];
|
|
1233
|
+
|
|
951
1234
|
const totalLines = this._textLines.length;
|
|
952
1235
|
|
|
953
1236
|
this._textLines.forEach((line, lineIndex) => {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
console.log(`\n--- Line ${lineIndex}: "${lineText}" isLast: ${isLastLine} ---`);
|
|
1237
|
+
// Initialize kashida info for this line
|
|
1238
|
+
this.__kashidaInfo[lineIndex] = [];
|
|
958
1239
|
|
|
959
1240
|
if (!this.__charBounds || !this.__charBounds[lineIndex]) {
|
|
960
|
-
console.log(' SKIP: No charBounds for this line');
|
|
961
1241
|
return;
|
|
962
1242
|
}
|
|
963
1243
|
|
|
964
1244
|
// Don't justify the last line
|
|
1245
|
+
const isLastLine = lineIndex === totalLines - 1;
|
|
965
1246
|
if (isLastLine) {
|
|
966
|
-
console.log(' SKIP: Last line - no justify');
|
|
967
1247
|
return;
|
|
968
1248
|
}
|
|
969
1249
|
|
|
@@ -971,7 +1251,11 @@ export class Textbox<
|
|
|
971
1251
|
|
|
972
1252
|
// Calculate current line width from charBounds
|
|
973
1253
|
const currentLineWidth = lineBounds.reduce((sum, b) => sum + (b?.kernedWidth || 0), 0);
|
|
974
|
-
|
|
1254
|
+
const totalExtraSpace = this.width - currentLineWidth;
|
|
1255
|
+
|
|
1256
|
+
if (totalExtraSpace <= 0) {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
975
1259
|
|
|
976
1260
|
// Count spaces and find space indices
|
|
977
1261
|
const spaceIndices: number[] = [];
|
|
@@ -980,59 +1264,124 @@ export class Textbox<
|
|
|
980
1264
|
spaceIndices.push(i);
|
|
981
1265
|
}
|
|
982
1266
|
}
|
|
983
|
-
|
|
984
1267
|
const spaceCount = spaceIndices.length;
|
|
985
|
-
console.log(' Space count:', spaceCount, 'at indices:', spaceIndices);
|
|
986
1268
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1269
|
+
// Find kashida points if enabled
|
|
1270
|
+
const kashidaPoints = kashidaRatio > 0 ? findKashidaPoints(line) : [];
|
|
1271
|
+
const hasKashidaPoints = kashidaPoints.length > 0;
|
|
1272
|
+
|
|
1273
|
+
// Calculate space distribution
|
|
1274
|
+
let kashidaSpace = 0;
|
|
1275
|
+
let spaceExpansion = totalExtraSpace;
|
|
1276
|
+
|
|
1277
|
+
if (hasKashidaPoints && kashidaRatio > 0) {
|
|
1278
|
+
// Distribute between kashida and spaces
|
|
1279
|
+
kashidaSpace = totalExtraSpace * kashidaRatio;
|
|
1280
|
+
spaceExpansion = totalExtraSpace * (1 - kashidaRatio);
|
|
990
1281
|
}
|
|
991
1282
|
|
|
992
|
-
// Calculate
|
|
993
|
-
const
|
|
994
|
-
|
|
1283
|
+
// Calculate per-kashida and per-space widths
|
|
1284
|
+
const perKashidaWidth = hasKashidaPoints ? kashidaSpace / kashidaPoints.length : 0;
|
|
1285
|
+
const perSpaceWidth = spaceCount > 0 ? spaceExpansion / spaceCount : 0;
|
|
1286
|
+
|
|
1287
|
+
// If kashida is enabled, insert actual tatweel characters
|
|
1288
|
+
if (hasKashidaPoints && perKashidaWidth > 0) {
|
|
1289
|
+
// console.log(`=== Inserting kashida in _applyBrowserJustifySpaces line ${lineIndex} ===`);
|
|
1290
|
+
|
|
1291
|
+
// Sort by charIndex descending to insert from end
|
|
1292
|
+
const sortedPoints = [...kashidaPoints].sort((a, b) => b.charIndex - a.charIndex);
|
|
1293
|
+
|
|
1294
|
+
// Calculate tatweel width
|
|
1295
|
+
const canvas = document.createElement('canvas');
|
|
1296
|
+
const ctx = canvas.getContext('2d');
|
|
1297
|
+
if (ctx) {
|
|
1298
|
+
ctx.font = this._getFontDeclaration();
|
|
1299
|
+
const tatweelWidth = ctx.measureText(ARABIC_TATWEEL).width;
|
|
1300
|
+
// console.log(` tatweelWidth: ${tatweelWidth}`);
|
|
1301
|
+
|
|
1302
|
+
if (tatweelWidth > 0) {
|
|
1303
|
+
const newLine = [...line];
|
|
1304
|
+
|
|
1305
|
+
for (const point of sortedPoints) {
|
|
1306
|
+
const tatweelCount = Math.max(1, Math.round(perKashidaWidth / tatweelWidth));
|
|
1307
|
+
// console.log(` Point ${point.charIndex}: inserting ${tatweelCount} tatweels`);
|
|
1308
|
+
|
|
1309
|
+
// Insert tatweels after the character
|
|
1310
|
+
for (let t = 0; t < tatweelCount; t++) {
|
|
1311
|
+
newLine.splice(point.charIndex + 1, 0, ARABIC_TATWEEL);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Store kashida info with tatweelCount for index conversion
|
|
1315
|
+
this.__kashidaInfo[lineIndex].push({
|
|
1316
|
+
charIndex: point.charIndex,
|
|
1317
|
+
width: perKashidaWidth,
|
|
1318
|
+
tatweelCount: tatweelCount,
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
995
1321
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1322
|
+
// console.log(` New line: ${newLine.join('')}`);
|
|
1323
|
+
|
|
1324
|
+
// Update _textLines with kashida
|
|
1325
|
+
this._textLines[lineIndex] = newLine;
|
|
1326
|
+
|
|
1327
|
+
// Update textLines string version
|
|
1328
|
+
if (this.textLines && this.textLines[lineIndex] !== undefined) {
|
|
1329
|
+
(this as any).textLines[lineIndex] = newLine.join('');
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Recalculate charBounds
|
|
1333
|
+
this.__charBounds[lineIndex] = [];
|
|
1334
|
+
this.__lineWidths[lineIndex] = undefined as any;
|
|
1335
|
+
this._measureLine(lineIndex);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
} else {
|
|
1339
|
+
// No kashida - just store info for reference (tatweelCount is 0 since no tatweels inserted)
|
|
1340
|
+
for (const point of kashidaPoints) {
|
|
1341
|
+
this.__kashidaInfo[lineIndex].push({ charIndex: point.charIndex, width: perKashidaWidth, tatweelCount: 0 });
|
|
1342
|
+
}
|
|
999
1343
|
}
|
|
1000
1344
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1345
|
+
// Now apply space expansion to remaining extra space
|
|
1346
|
+
const newLineBounds = this.__charBounds[lineIndex];
|
|
1347
|
+
const newLineWidth = newLineBounds.reduce((sum, b) => sum + (b?.kernedWidth || 0), 0);
|
|
1348
|
+
const remainingSpace = this.width - newLineWidth;
|
|
1349
|
+
|
|
1350
|
+
if (remainingSpace > 0 && spaceCount > 0) {
|
|
1351
|
+
const extraPerSpace = remainingSpace / spaceCount;
|
|
1352
|
+
let accumulated = 0;
|
|
1353
|
+
|
|
1354
|
+
for (let charIndex = 0; charIndex < this._textLines[lineIndex].length; charIndex++) {
|
|
1355
|
+
const bound = newLineBounds[charIndex];
|
|
1356
|
+
if (!bound) continue;
|
|
1357
|
+
|
|
1358
|
+
bound.left += accumulated;
|
|
1359
|
+
|
|
1360
|
+
// Check if this is a space (need to check against the updated line)
|
|
1361
|
+
if (/\s/.test(this._textLines[lineIndex][charIndex])) {
|
|
1362
|
+
bound.width += extraPerSpace;
|
|
1363
|
+
bound.kernedWidth += extraPerSpace;
|
|
1364
|
+
accumulated += extraPerSpace;
|
|
1365
|
+
}
|
|
1021
1366
|
}
|
|
1022
1367
|
}
|
|
1023
1368
|
|
|
1024
1369
|
// Update cached line width
|
|
1025
|
-
const
|
|
1370
|
+
const finalLineBounds = this.__charBounds[lineIndex];
|
|
1371
|
+
const finalLineWidth = finalLineBounds.reduce((max, b) => Math.max(max, (b?.left || 0) + (b?.width || 0)), 0);
|
|
1026
1372
|
this.__lineWidths[lineIndex] = finalLineWidth;
|
|
1027
|
-
console.log(' Final line width:', finalLineWidth.toFixed(2), 'target:', this.width);
|
|
1028
1373
|
});
|
|
1029
1374
|
|
|
1030
|
-
console.log('=== _applyBrowserJustifySpaces END ===\n');
|
|
1031
1375
|
this.dirty = true;
|
|
1032
1376
|
// Mark that justify has been applied - for debugging to detect if measureLine overwrites it
|
|
1033
1377
|
(this as any)._justifyApplied = true;
|
|
1034
|
-
|
|
1035
|
-
//
|
|
1378
|
+
|
|
1379
|
+
// Debug log final kashida state
|
|
1380
|
+
// console.log('=== _applyBrowserJustifySpaces END ===');
|
|
1381
|
+
// console.log('Final __kashidaInfo:', JSON.stringify(this.__kashidaInfo.map((lineInfo, i) => ({
|
|
1382
|
+
// line: i,
|
|
1383
|
+
// entries: lineInfo.map(k => ({ charIndex: k.charIndex, tatweelCount: k.tatweelCount }))
|
|
1384
|
+
// }))));
|
|
1036
1385
|
}
|
|
1037
1386
|
|
|
1038
1387
|
/**
|