@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
|
@@ -5,6 +5,7 @@ import { createTextboxDefaultControls } from '../controls/commonControls.mjs';
|
|
|
5
5
|
import { JUSTIFY } from './Text/constants.mjs';
|
|
6
6
|
import { fontLacksEnglishGlyphsCached } from '../text/measure.mjs';
|
|
7
7
|
import { layoutText } from '../text/layout.mjs';
|
|
8
|
+
import { ARABIC_TATWEEL, findKashidaPoints } from '../text/unicode.mjs';
|
|
8
9
|
|
|
9
10
|
// @TODO: Many things here are configuration related and shouldn't be on the class nor prototype
|
|
10
11
|
// regexes, list of properties that are not suppose to change by instances, magic consts.
|
|
@@ -70,7 +71,7 @@ class Textbox extends IText {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
// Skip if nothing changed
|
|
73
|
-
const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
|
|
74
|
+
const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}|${this.kashida}`;
|
|
74
75
|
if (this._lastDimensionState === currentState && this._textLines && this._textLines.length > 0) {
|
|
75
76
|
return;
|
|
76
77
|
}
|
|
@@ -169,12 +170,18 @@ class Textbox extends IText {
|
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
// Use new layout engine
|
|
173
|
+
// When kashida is enabled, don't let layout engine apply justify - we'll handle it with kashida
|
|
174
|
+
const useKashidaJustify = this.kashida !== 'none' && this.textAlign.includes(JUSTIFY);
|
|
175
|
+
const effectiveAlign = useKashidaJustify ? this.direction === 'rtl' ? 'right' : 'left' // Natural alignment, kashida will justify
|
|
176
|
+
: this._mapTextAlignToAlign(this.textAlign);
|
|
172
177
|
const layout = layoutText({
|
|
173
178
|
text: this.text,
|
|
174
179
|
width: this.width,
|
|
175
|
-
height
|
|
180
|
+
// Don't pass height constraint to allow vertical auto-expansion
|
|
181
|
+
// Only pass height if explicitly set to constrain (e.g., for ellipsis)
|
|
182
|
+
height: this.ellipsis ? this.height : undefined,
|
|
176
183
|
wrap: this.wrap || 'word',
|
|
177
|
-
align:
|
|
184
|
+
align: effectiveAlign,
|
|
178
185
|
ellipsis: this.ellipsis || false,
|
|
179
186
|
fontSize: this.fontSize,
|
|
180
187
|
lineHeight: this.lineHeight,
|
|
@@ -212,9 +219,264 @@ class Textbox extends IText {
|
|
|
212
219
|
|
|
213
220
|
// Generate style map for compatibility
|
|
214
221
|
this._styleMap = this._generateStyleMapFromLayout(layout);
|
|
222
|
+
|
|
223
|
+
// Apply kashida for justified text in advanced layout mode
|
|
224
|
+
if (this.textAlign.includes(JUSTIFY) && this.kashida !== 'none') {
|
|
225
|
+
this._applyKashidaToLayout();
|
|
226
|
+
}
|
|
215
227
|
this.dirty = true;
|
|
216
228
|
}
|
|
217
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Apply kashida (tatweel) characters to layout for Arabic text justification.
|
|
232
|
+
* This method INSERTS actual tatweel characters into the text lines.
|
|
233
|
+
* @private
|
|
234
|
+
*/
|
|
235
|
+
_applyKashidaToLayout() {
|
|
236
|
+
if (!this._textLines || !this.__charBounds) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Clear visual positions cache - it becomes stale when kashida is applied
|
|
241
|
+
// Check if cache exists (it's initialized in IText constructor which runs after this during construction)
|
|
242
|
+
if (this._visualPositionsCache) {
|
|
243
|
+
this._clearVisualPositionsCache();
|
|
244
|
+
}
|
|
245
|
+
const kashidaRatios = {
|
|
246
|
+
none: 0,
|
|
247
|
+
short: 0.25,
|
|
248
|
+
medium: 0.5,
|
|
249
|
+
long: 0.75,
|
|
250
|
+
stylistic: 1.0
|
|
251
|
+
};
|
|
252
|
+
const kashidaRatio = kashidaRatios[this.kashida] || 0;
|
|
253
|
+
if (kashidaRatio === 0) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Calculate tatweel width once
|
|
258
|
+
const canvas = document.createElement('canvas');
|
|
259
|
+
const ctx = canvas.getContext('2d');
|
|
260
|
+
if (!ctx) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
ctx.font = this._getFontDeclaration();
|
|
264
|
+
const tatweelWidth = ctx.measureText(ARABIC_TATWEEL).width;
|
|
265
|
+
if (tatweelWidth <= 0) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Reset kashida info
|
|
270
|
+
this.__kashidaInfo = [];
|
|
271
|
+
const totalLines = this._textLines.length;
|
|
272
|
+
for (let lineIndex = 0; lineIndex < totalLines; lineIndex++) {
|
|
273
|
+
this.__kashidaInfo[lineIndex] = [];
|
|
274
|
+
const line = this._textLines[lineIndex];
|
|
275
|
+
if (!this.__charBounds || !this.__charBounds[lineIndex]) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Don't apply kashida to the last line
|
|
280
|
+
const isLastLine = lineIndex === totalLines - 1;
|
|
281
|
+
if (isLastLine) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const lineBounds = this.__charBounds[lineIndex];
|
|
285
|
+
const lastBound = lineBounds[lineBounds.length - 1];
|
|
286
|
+
|
|
287
|
+
// Calculate current line width
|
|
288
|
+
const currentLineWidth = lastBound ? lastBound.left + lastBound.kernedWidth : 0;
|
|
289
|
+
const totalExtraSpace = this.width - currentLineWidth;
|
|
290
|
+
|
|
291
|
+
// Only apply kashida if there's significant extra space to fill
|
|
292
|
+
if (totalExtraSpace <= 2) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Find kashida points
|
|
297
|
+
const kashidaPoints = findKashidaPoints(line);
|
|
298
|
+
if (kashidaPoints.length === 0) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Calculate kashida space
|
|
303
|
+
const kashidaSpace = totalExtraSpace * kashidaRatio;
|
|
304
|
+
|
|
305
|
+
// Calculate how many tatweels can fit
|
|
306
|
+
const totalTatweels = Math.floor(kashidaSpace / tatweelWidth);
|
|
307
|
+
if (totalTatweels === 0) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Limit kashida points
|
|
312
|
+
const maxKashidaPoints = Math.min(kashidaPoints.length, totalTatweels);
|
|
313
|
+
const usedKashidaPoints = kashidaPoints.slice(0, maxKashidaPoints);
|
|
314
|
+
|
|
315
|
+
// Distribute tatweels evenly
|
|
316
|
+
const tatweelsPerPoint = Math.floor(totalTatweels / maxKashidaPoints);
|
|
317
|
+
const extraTatweels = totalTatweels % maxKashidaPoints;
|
|
318
|
+
|
|
319
|
+
// console.log(`=== Inserting Kashida into line ${lineIndex} ===`);
|
|
320
|
+
// console.log(` totalTatweels: ${totalTatweels}, usedPoints: ${usedKashidaPoints.length}`);
|
|
321
|
+
|
|
322
|
+
// Sort by charIndex descending so we insert from the end (prevents index shifting issues)
|
|
323
|
+
const sortedPoints = [...usedKashidaPoints].sort((a, b) => b.charIndex - a.charIndex);
|
|
324
|
+
|
|
325
|
+
// Create new line with tatweels inserted
|
|
326
|
+
const newLine = [...line];
|
|
327
|
+
for (let i = 0; i < sortedPoints.length; i++) {
|
|
328
|
+
const point = sortedPoints[i];
|
|
329
|
+
const originalIndex = usedKashidaPoints.indexOf(point);
|
|
330
|
+
const count = tatweelsPerPoint + (originalIndex < extraTatweels ? 1 : 0);
|
|
331
|
+
if (count > 0) {
|
|
332
|
+
// Insert tatweels AFTER the character at charIndex
|
|
333
|
+
const tatweels = Array(count).fill(ARABIC_TATWEEL);
|
|
334
|
+
newLine.splice(point.charIndex + 1, 0, ...tatweels);
|
|
335
|
+
// console.log(` Inserted ${count} tatweels after char ${point.charIndex}`);
|
|
336
|
+
|
|
337
|
+
// Store kashida info for index conversion
|
|
338
|
+
this.__kashidaInfo[lineIndex].push({
|
|
339
|
+
charIndex: point.charIndex,
|
|
340
|
+
width: count * tatweelWidth,
|
|
341
|
+
tatweelCount: count
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Update _textLines with the new line containing tatweels
|
|
347
|
+
this._textLines[lineIndex] = newLine;
|
|
348
|
+
|
|
349
|
+
// Update textLines (string version)
|
|
350
|
+
if (this.textLines) {
|
|
351
|
+
this.textLines[lineIndex] = newLine.join('');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Clear and recalculate charBounds for this line
|
|
355
|
+
this.__charBounds[lineIndex] = [];
|
|
356
|
+
this.__lineWidths[lineIndex] = undefined;
|
|
357
|
+
this._measureLine(lineIndex);
|
|
358
|
+
|
|
359
|
+
// Now expand spaces to fill any remaining gap
|
|
360
|
+
let newLineBounds = this.__charBounds[lineIndex];
|
|
361
|
+
if (newLineBounds && newLineBounds.length > 0) {
|
|
362
|
+
let newLastBound = newLineBounds[newLineBounds.length - 1];
|
|
363
|
+
let newLineWidth = newLastBound ? newLastBound.left + newLastBound.kernedWidth : 0;
|
|
364
|
+
let remainingGap = this.width - newLineWidth;
|
|
365
|
+
if (remainingGap > 0.5) {
|
|
366
|
+
// Count spaces in the new line
|
|
367
|
+
let spaceCount = 0;
|
|
368
|
+
for (let i = 0; i < newLine.length; i++) {
|
|
369
|
+
if (/\s/.test(newLine[i])) {
|
|
370
|
+
spaceCount++;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (spaceCount > 0) {
|
|
374
|
+
const extraPerSpace = remainingGap / spaceCount;
|
|
375
|
+
let accumulatedExtra = 0;
|
|
376
|
+
|
|
377
|
+
// Expand space widths AND update left positions for subsequent chars
|
|
378
|
+
for (let i = 0; i < newLineBounds.length; i++) {
|
|
379
|
+
const bound = newLineBounds[i];
|
|
380
|
+
if (!bound) continue;
|
|
381
|
+
|
|
382
|
+
// Update left position to account for previous space expansions
|
|
383
|
+
bound.left += accumulatedExtra;
|
|
384
|
+
|
|
385
|
+
// If this is a space, expand it
|
|
386
|
+
if (/\s/.test(newLine[i])) {
|
|
387
|
+
bound.width += extraPerSpace;
|
|
388
|
+
bound.kernedWidth += extraPerSpace;
|
|
389
|
+
accumulatedExtra += extraPerSpace;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Update the extra entry at the end (cursor position)
|
|
393
|
+
if (newLineBounds[newLine.length]) {
|
|
394
|
+
newLineBounds[newLine.length].left += accumulatedExtra;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Recalculate remaining gap after space expansion
|
|
398
|
+
newLastBound = newLineBounds[newLineBounds.length - 1];
|
|
399
|
+
newLineWidth = newLastBound ? newLastBound.left + newLastBound.kernedWidth : 0;
|
|
400
|
+
remainingGap = this.width - newLineWidth;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// If there's still a gap after space expansion, distribute it across all kashida points
|
|
405
|
+
if (remainingGap > 0.5 && this.__kashidaInfo[lineIndex].length > 0) {
|
|
406
|
+
const kashidaPointCount = this.__kashidaInfo[lineIndex].length;
|
|
407
|
+
const extraPerKashida = remainingGap / kashidaPointCount;
|
|
408
|
+
|
|
409
|
+
// Find kashida positions in newLine and expand their widths
|
|
410
|
+
let kashidaIndex = 0;
|
|
411
|
+
let accumulatedExtra = 0;
|
|
412
|
+
for (let i = 0; i < newLineBounds.length; i++) {
|
|
413
|
+
const bound = newLineBounds[i];
|
|
414
|
+
if (!bound) continue;
|
|
415
|
+
|
|
416
|
+
// Update left position for accumulated expansion
|
|
417
|
+
bound.left += accumulatedExtra;
|
|
418
|
+
|
|
419
|
+
// Check if this is a tatweel character
|
|
420
|
+
if (newLine[i] === ARABIC_TATWEEL) {
|
|
421
|
+
var _this$__kashidaInfo$l;
|
|
422
|
+
// Distribute extra width among tatweels
|
|
423
|
+
const extraForThis = extraPerKashida / (((_this$__kashidaInfo$l = this.__kashidaInfo[lineIndex][kashidaIndex]) === null || _this$__kashidaInfo$l === void 0 ? void 0 : _this$__kashidaInfo$l.tatweelCount) || 1);
|
|
424
|
+
bound.width += extraForThis;
|
|
425
|
+
bound.kernedWidth += extraForThis;
|
|
426
|
+
accumulatedExtra += extraForThis;
|
|
427
|
+
|
|
428
|
+
// Move to next kashida info when we've passed this group
|
|
429
|
+
const currentKashidaInfo = this.__kashidaInfo[lineIndex][kashidaIndex];
|
|
430
|
+
if (currentKashidaInfo && i > 0) {
|
|
431
|
+
// Check if next char is not tatweel - means we're done with this group
|
|
432
|
+
if (i + 1 >= newLine.length || newLine[i + 1] !== ARABIC_TATWEEL) {
|
|
433
|
+
kashidaIndex++;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Update the extra entry at the end
|
|
440
|
+
if (newLineBounds[newLine.length]) {
|
|
441
|
+
newLineBounds[newLine.length].left += accumulatedExtra;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Set line width to textbox width (for justified lines)
|
|
447
|
+
this.__lineWidths[lineIndex] = this.width;
|
|
448
|
+
|
|
449
|
+
// console.log(` New line length: ${newLine.length}, text: ${newLine.join('')}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// For justified lines with kashida, line width should equal textbox width
|
|
453
|
+
// Only set undefined widths (non-justified lines without kashida)
|
|
454
|
+
for (let i = 0; i < this._textLines.length; i++) {
|
|
455
|
+
if (this.__lineWidths[i] === undefined && this.__charBounds[i]) {
|
|
456
|
+
const bounds = this.__charBounds[i];
|
|
457
|
+
const lastBound = bounds[bounds.length - 1];
|
|
458
|
+
if (lastBound) {
|
|
459
|
+
this.__lineWidths[i] = lastBound.left + lastBound.kernedWidth;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Update _text to match the new _textLines (required for editing)
|
|
465
|
+
this._text = this._textLines.flat();
|
|
466
|
+
|
|
467
|
+
// DON'T update this.text - keep the original text intact
|
|
468
|
+
// The tatweels are in _textLines and _text for rendering purposes only
|
|
469
|
+
|
|
470
|
+
this._justifyApplied = true;
|
|
471
|
+
|
|
472
|
+
// Debug log final kashida state
|
|
473
|
+
// console.log('=== _applyKashidaToLayout END ===');
|
|
474
|
+
// console.log('Final __kashidaInfo:', JSON.stringify(this.__kashidaInfo.map((lineInfo, i) => ({
|
|
475
|
+
// line: i,
|
|
476
|
+
// entries: lineInfo.map(k => ({ charIndex: k.charIndex, tatweelCount: k.tatweelCount }))
|
|
477
|
+
// }))));
|
|
478
|
+
}
|
|
479
|
+
|
|
218
480
|
/**
|
|
219
481
|
* Generate style map from new layout format
|
|
220
482
|
* @private
|
|
@@ -737,84 +999,100 @@ class Textbox extends IText {
|
|
|
737
999
|
* @private
|
|
738
1000
|
*/
|
|
739
1001
|
_extractJustifySpaceMeasurements(element, lines) {
|
|
740
|
-
console.log('=== _extractJustifySpaceMeasurements START ===');
|
|
741
|
-
console.log('Textbox width:', this.width);
|
|
742
|
-
console.log('Lines count:', lines.length);
|
|
1002
|
+
// console.log('=== _extractJustifySpaceMeasurements START ===');
|
|
1003
|
+
// console.log('Textbox width:', this.width);
|
|
1004
|
+
// console.log('Lines count:', lines.length);
|
|
1005
|
+
|
|
743
1006
|
const measureCtx = this._browserMeasureCtx || (this._browserMeasureCtx = document.createElement('canvas').getContext('2d'));
|
|
744
1007
|
if (!measureCtx) {
|
|
745
|
-
console.log('ERROR: No measure context');
|
|
1008
|
+
// console.log('ERROR: No measure context');
|
|
746
1009
|
return [];
|
|
747
1010
|
}
|
|
748
1011
|
measureCtx.font = `${this.fontStyle || 'normal'} ${this.fontWeight || 'normal'} ${this.fontSize}px "${this.fontFamily}"`;
|
|
749
1012
|
const normalSpaceWidth = measureCtx.measureText(' ').width || 6;
|
|
750
|
-
console.log('Font:', measureCtx.font);
|
|
751
|
-
console.log('Normal space width:', normalSpaceWidth);
|
|
1013
|
+
// console.log('Font:', measureCtx.font);
|
|
1014
|
+
// console.log('Normal space width:', normalSpaceWidth);
|
|
1015
|
+
|
|
752
1016
|
const spaceWidths = [];
|
|
753
1017
|
lines.forEach((line, lineIndex) => {
|
|
754
1018
|
const lineSpaces = [];
|
|
755
1019
|
const spaceCount = (line.match(/\s/g) || []).length;
|
|
756
1020
|
const isLastLine = lineIndex === lines.length - 1;
|
|
757
|
-
|
|
1021
|
+
|
|
1022
|
+
// console.log(`\nLine ${lineIndex}: "${line.substring(0, 50)}..." spaces: ${spaceCount}, isLast: ${isLastLine}`);
|
|
1023
|
+
|
|
758
1024
|
if (spaceCount > 0 && !isLastLine) {
|
|
759
1025
|
// Don't justify last line
|
|
760
1026
|
const naturalWidth = measureCtx.measureText(line).width;
|
|
761
1027
|
const remainingSpace = this.width - naturalWidth;
|
|
762
1028
|
const extraPerSpace = remainingSpace > 0 ? remainingSpace / spaceCount : 0;
|
|
763
1029
|
const expandedSpaceWidth = normalSpaceWidth + extraPerSpace;
|
|
764
|
-
|
|
765
|
-
console.log(`
|
|
1030
|
+
|
|
1031
|
+
// console.log(` Natural width: ${naturalWidth.toFixed(2)}, Remaining: ${remainingSpace.toFixed(2)}`);
|
|
1032
|
+
// console.log(` Extra per space: ${extraPerSpace.toFixed(2)}, Expanded space: ${expandedSpaceWidth.toFixed(2)}`);
|
|
1033
|
+
|
|
766
1034
|
const safeWidth = Math.max(normalSpaceWidth, expandedSpaceWidth);
|
|
767
1035
|
for (let i = 0; i < spaceCount; i++) {
|
|
768
1036
|
lineSpaces.push(safeWidth);
|
|
769
1037
|
}
|
|
770
1038
|
} else if (spaceCount > 0) {
|
|
771
1039
|
// Last line: keep natural space width
|
|
772
|
-
console.log(` Last line - using normal space width: ${normalSpaceWidth}`);
|
|
1040
|
+
// console.log(` Last line - using normal space width: ${normalSpaceWidth}`);
|
|
773
1041
|
for (let i = 0; i < spaceCount; i++) {
|
|
774
1042
|
lineSpaces.push(normalSpaceWidth);
|
|
775
1043
|
}
|
|
776
1044
|
}
|
|
777
1045
|
spaceWidths.push(lineSpaces);
|
|
778
1046
|
});
|
|
779
|
-
|
|
780
|
-
console.log('
|
|
1047
|
+
|
|
1048
|
+
// console.log('\nFinal spaceWidths:', spaceWidths);
|
|
1049
|
+
// console.log('=== _extractJustifySpaceMeasurements END ===\n');
|
|
781
1050
|
return spaceWidths;
|
|
782
1051
|
}
|
|
783
1052
|
|
|
784
1053
|
/**
|
|
785
|
-
* Apply justify space expansion using actual charBounds measurements
|
|
1054
|
+
* Apply justify space expansion using actual charBounds measurements.
|
|
1055
|
+
* Supports Arabic kashida (tatweel) justification when kashida property is set.
|
|
786
1056
|
* @private
|
|
787
1057
|
*/
|
|
788
1058
|
_applyBrowserJustifySpaces() {
|
|
789
|
-
var _this$_textLines, _this$__charBounds;
|
|
790
|
-
console.log('=== _applyBrowserJustifySpaces START ===');
|
|
791
|
-
console.log('_textLines:', (_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length, 'lines');
|
|
792
|
-
console.log('__charBounds:', (_this$__charBounds = this.__charBounds) === null || _this$__charBounds === void 0 ? void 0 : _this$__charBounds.length, 'lines');
|
|
793
|
-
console.log('textbox width:', this.width);
|
|
794
1059
|
if (!this._textLines || !this.__charBounds) {
|
|
795
|
-
console.log('EARLY RETURN: _textLines or __charBounds missing');
|
|
796
1060
|
return;
|
|
797
1061
|
}
|
|
1062
|
+
|
|
1063
|
+
// Kashida ratios: proportion of extra space distributed via kashida vs space expansion
|
|
1064
|
+
const kashidaRatios = {
|
|
1065
|
+
none: 0,
|
|
1066
|
+
short: 0.25,
|
|
1067
|
+
medium: 0.5,
|
|
1068
|
+
long: 0.75,
|
|
1069
|
+
stylistic: 1.0
|
|
1070
|
+
};
|
|
1071
|
+
const kashidaRatio = kashidaRatios[this.kashida] || 0;
|
|
1072
|
+
|
|
1073
|
+
// Reset kashida info
|
|
1074
|
+
this.__kashidaInfo = [];
|
|
798
1075
|
const totalLines = this._textLines.length;
|
|
799
1076
|
this._textLines.forEach((line, lineIndex) => {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
console.log(`\n--- Line ${lineIndex}: "${lineText}" isLast: ${isLastLine} ---`);
|
|
1077
|
+
// Initialize kashida info for this line
|
|
1078
|
+
this.__kashidaInfo[lineIndex] = [];
|
|
803
1079
|
if (!this.__charBounds || !this.__charBounds[lineIndex]) {
|
|
804
|
-
console.log(' SKIP: No charBounds for this line');
|
|
805
1080
|
return;
|
|
806
1081
|
}
|
|
807
1082
|
|
|
808
1083
|
// Don't justify the last line
|
|
1084
|
+
const isLastLine = lineIndex === totalLines - 1;
|
|
809
1085
|
if (isLastLine) {
|
|
810
|
-
console.log(' SKIP: Last line - no justify');
|
|
811
1086
|
return;
|
|
812
1087
|
}
|
|
813
1088
|
const lineBounds = this.__charBounds[lineIndex];
|
|
814
1089
|
|
|
815
1090
|
// Calculate current line width from charBounds
|
|
816
1091
|
const currentLineWidth = lineBounds.reduce((sum, b) => sum + ((b === null || b === void 0 ? void 0 : b.kernedWidth) || 0), 0);
|
|
817
|
-
|
|
1092
|
+
const totalExtraSpace = this.width - currentLineWidth;
|
|
1093
|
+
if (totalExtraSpace <= 0) {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
818
1096
|
|
|
819
1097
|
// Count spaces and find space indices
|
|
820
1098
|
const spaceIndices = [];
|
|
@@ -824,53 +1102,118 @@ class Textbox extends IText {
|
|
|
824
1102
|
}
|
|
825
1103
|
}
|
|
826
1104
|
const spaceCount = spaceIndices.length;
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1105
|
+
|
|
1106
|
+
// Find kashida points if enabled
|
|
1107
|
+
const kashidaPoints = kashidaRatio > 0 ? findKashidaPoints(line) : [];
|
|
1108
|
+
const hasKashidaPoints = kashidaPoints.length > 0;
|
|
1109
|
+
|
|
1110
|
+
// Calculate space distribution
|
|
1111
|
+
let kashidaSpace = 0;
|
|
1112
|
+
if (hasKashidaPoints && kashidaRatio > 0) {
|
|
1113
|
+
// Distribute between kashida and spaces
|
|
1114
|
+
kashidaSpace = totalExtraSpace * kashidaRatio;
|
|
831
1115
|
}
|
|
832
1116
|
|
|
833
|
-
// Calculate
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1117
|
+
// Calculate per-kashida and per-space widths
|
|
1118
|
+
const perKashidaWidth = hasKashidaPoints ? kashidaSpace / kashidaPoints.length : 0;
|
|
1119
|
+
|
|
1120
|
+
// If kashida is enabled, insert actual tatweel characters
|
|
1121
|
+
if (hasKashidaPoints && perKashidaWidth > 0) {
|
|
1122
|
+
// console.log(`=== Inserting kashida in _applyBrowserJustifySpaces line ${lineIndex} ===`);
|
|
1123
|
+
|
|
1124
|
+
// Sort by charIndex descending to insert from end
|
|
1125
|
+
const sortedPoints = [...kashidaPoints].sort((a, b) => b.charIndex - a.charIndex);
|
|
1126
|
+
|
|
1127
|
+
// Calculate tatweel width
|
|
1128
|
+
const canvas = document.createElement('canvas');
|
|
1129
|
+
const ctx = canvas.getContext('2d');
|
|
1130
|
+
if (ctx) {
|
|
1131
|
+
ctx.font = this._getFontDeclaration();
|
|
1132
|
+
const tatweelWidth = ctx.measureText(ARABIC_TATWEEL).width;
|
|
1133
|
+
// console.log(` tatweelWidth: ${tatweelWidth}`);
|
|
1134
|
+
|
|
1135
|
+
if (tatweelWidth > 0) {
|
|
1136
|
+
const newLine = [...line];
|
|
1137
|
+
for (const point of sortedPoints) {
|
|
1138
|
+
const tatweelCount = Math.max(1, Math.round(perKashidaWidth / tatweelWidth));
|
|
1139
|
+
// console.log(` Point ${point.charIndex}: inserting ${tatweelCount} tatweels`);
|
|
1140
|
+
|
|
1141
|
+
// Insert tatweels after the character
|
|
1142
|
+
for (let t = 0; t < tatweelCount; t++) {
|
|
1143
|
+
newLine.splice(point.charIndex + 1, 0, ARABIC_TATWEEL);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Store kashida info with tatweelCount for index conversion
|
|
1147
|
+
this.__kashidaInfo[lineIndex].push({
|
|
1148
|
+
charIndex: point.charIndex,
|
|
1149
|
+
width: perKashidaWidth,
|
|
1150
|
+
tatweelCount: tatweelCount
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// console.log(` New line: ${newLine.join('')}`);
|
|
1155
|
+
|
|
1156
|
+
// Update _textLines with kashida
|
|
1157
|
+
this._textLines[lineIndex] = newLine;
|
|
1158
|
+
|
|
1159
|
+
// Update textLines string version
|
|
1160
|
+
if (this.textLines && this.textLines[lineIndex] !== undefined) {
|
|
1161
|
+
this.textLines[lineIndex] = newLine.join('');
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Recalculate charBounds
|
|
1165
|
+
this.__charBounds[lineIndex] = [];
|
|
1166
|
+
this.__lineWidths[lineIndex] = undefined;
|
|
1167
|
+
this._measureLine(lineIndex);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
} else {
|
|
1171
|
+
// No kashida - just store info for reference (tatweelCount is 0 since no tatweels inserted)
|
|
1172
|
+
for (const point of kashidaPoints) {
|
|
1173
|
+
this.__kashidaInfo[lineIndex].push({
|
|
1174
|
+
charIndex: point.charIndex,
|
|
1175
|
+
width: perKashidaWidth,
|
|
1176
|
+
tatweelCount: 0
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
839
1179
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
console.log(` Space at char ${charIndex}: ${oldWidth.toFixed(2)} -> ${newWidth.toFixed(2)} (accumulated: ${accumulated.toFixed(2)})`);
|
|
1180
|
+
|
|
1181
|
+
// Now apply space expansion to remaining extra space
|
|
1182
|
+
const newLineBounds = this.__charBounds[lineIndex];
|
|
1183
|
+
const newLineWidth = newLineBounds.reduce((sum, b) => sum + ((b === null || b === void 0 ? void 0 : b.kernedWidth) || 0), 0);
|
|
1184
|
+
const remainingSpace = this.width - newLineWidth;
|
|
1185
|
+
if (remainingSpace > 0 && spaceCount > 0) {
|
|
1186
|
+
const extraPerSpace = remainingSpace / spaceCount;
|
|
1187
|
+
let accumulated = 0;
|
|
1188
|
+
for (let charIndex = 0; charIndex < this._textLines[lineIndex].length; charIndex++) {
|
|
1189
|
+
const bound = newLineBounds[charIndex];
|
|
1190
|
+
if (!bound) continue;
|
|
1191
|
+
bound.left += accumulated;
|
|
1192
|
+
|
|
1193
|
+
// Check if this is a space (need to check against the updated line)
|
|
1194
|
+
if (/\s/.test(this._textLines[lineIndex][charIndex])) {
|
|
1195
|
+
bound.width += extraPerSpace;
|
|
1196
|
+
bound.kernedWidth += extraPerSpace;
|
|
1197
|
+
accumulated += extraPerSpace;
|
|
1198
|
+
}
|
|
860
1199
|
}
|
|
861
1200
|
}
|
|
862
1201
|
|
|
863
1202
|
// Update cached line width
|
|
864
|
-
const
|
|
1203
|
+
const finalLineBounds = this.__charBounds[lineIndex];
|
|
1204
|
+
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);
|
|
865
1205
|
this.__lineWidths[lineIndex] = finalLineWidth;
|
|
866
|
-
console.log(' Final line width:', finalLineWidth.toFixed(2), 'target:', this.width);
|
|
867
1206
|
});
|
|
868
|
-
console.log('=== _applyBrowserJustifySpaces END ===\n');
|
|
869
1207
|
this.dirty = true;
|
|
870
1208
|
// Mark that justify has been applied - for debugging to detect if measureLine overwrites it
|
|
871
1209
|
this._justifyApplied = true;
|
|
872
|
-
|
|
873
|
-
//
|
|
1210
|
+
|
|
1211
|
+
// Debug log final kashida state
|
|
1212
|
+
// console.log('=== _applyBrowserJustifySpaces END ===');
|
|
1213
|
+
// console.log('Final __kashidaInfo:', JSON.stringify(this.__kashidaInfo.map((lineInfo, i) => ({
|
|
1214
|
+
// line: i,
|
|
1215
|
+
// entries: lineInfo.map(k => ({ charIndex: k.charIndex, tatweelCount: k.tatweelCount }))
|
|
1216
|
+
// }))));
|
|
874
1217
|
}
|
|
875
1218
|
|
|
876
1219
|
/**
|