@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.
Files changed (95) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/dist/index.js +1982 -649
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.min.js +1 -1
  5. package/dist/index.min.js.map +1 -1
  6. package/dist/index.min.mjs +1 -1
  7. package/dist/index.min.mjs.map +1 -1
  8. package/dist/index.mjs +1982 -649
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/index.node.cjs +1982 -649
  11. package/dist/index.node.cjs.map +1 -1
  12. package/dist/index.node.mjs +1982 -649
  13. package/dist/index.node.mjs.map +1 -1
  14. package/dist/package.json.min.mjs +1 -1
  15. package/dist/package.json.mjs +1 -1
  16. package/dist/src/shapes/IText/IText.d.ts +31 -6
  17. package/dist/src/shapes/IText/IText.d.ts.map +1 -1
  18. package/dist/src/shapes/IText/IText.min.mjs +1 -1
  19. package/dist/src/shapes/IText/IText.min.mjs.map +1 -1
  20. package/dist/src/shapes/IText/IText.mjs +495 -126
  21. package/dist/src/shapes/IText/IText.mjs.map +1 -1
  22. package/dist/src/shapes/IText/ITextBehavior.d.ts +12 -0
  23. package/dist/src/shapes/IText/ITextBehavior.d.ts.map +1 -1
  24. package/dist/src/shapes/IText/ITextBehavior.min.mjs +1 -1
  25. package/dist/src/shapes/IText/ITextBehavior.min.mjs.map +1 -1
  26. package/dist/src/shapes/IText/ITextBehavior.mjs +127 -36
  27. package/dist/src/shapes/IText/ITextBehavior.mjs.map +1 -1
  28. package/dist/src/shapes/IText/ITextClickBehavior.d.ts.map +1 -1
  29. package/dist/src/shapes/IText/ITextClickBehavior.min.mjs +1 -1
  30. package/dist/src/shapes/IText/ITextClickBehavior.min.mjs.map +1 -1
  31. package/dist/src/shapes/IText/ITextClickBehavior.mjs +21 -4
  32. package/dist/src/shapes/IText/ITextClickBehavior.mjs.map +1 -1
  33. package/dist/src/shapes/IText/ITextKeyBehavior.min.mjs +1 -1
  34. package/dist/src/shapes/IText/ITextKeyBehavior.min.mjs.map +1 -1
  35. package/dist/src/shapes/IText/ITextKeyBehavior.mjs +17 -21
  36. package/dist/src/shapes/IText/ITextKeyBehavior.mjs.map +1 -1
  37. package/dist/src/shapes/Text/Text.d.ts +69 -1
  38. package/dist/src/shapes/Text/Text.d.ts.map +1 -1
  39. package/dist/src/shapes/Text/Text.min.mjs +1 -1
  40. package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
  41. package/dist/src/shapes/Text/Text.mjs +374 -60
  42. package/dist/src/shapes/Text/Text.mjs.map +1 -1
  43. package/dist/src/shapes/Text/constants.d.ts.map +1 -1
  44. package/dist/src/shapes/Text/constants.min.mjs +1 -1
  45. package/dist/src/shapes/Text/constants.min.mjs.map +1 -1
  46. package/dist/src/shapes/Text/constants.mjs +2 -1
  47. package/dist/src/shapes/Text/constants.mjs.map +1 -1
  48. package/dist/src/shapes/Textbox.d.ts +8 -1
  49. package/dist/src/shapes/Textbox.d.ts.map +1 -1
  50. package/dist/src/shapes/Textbox.min.mjs +1 -1
  51. package/dist/src/shapes/Textbox.min.mjs.map +1 -1
  52. package/dist/src/shapes/Textbox.mjs +406 -63
  53. package/dist/src/shapes/Textbox.mjs.map +1 -1
  54. package/dist/src/text/hitTest.min.mjs +1 -1
  55. package/dist/src/text/hitTest.min.mjs.map +1 -1
  56. package/dist/src/text/hitTest.mjs +1 -198
  57. package/dist/src/text/hitTest.mjs.map +1 -1
  58. package/dist/src/text/layout.min.mjs +1 -1
  59. package/dist/src/text/layout.min.mjs.map +1 -1
  60. package/dist/src/text/layout.mjs +122 -5
  61. package/dist/src/text/layout.mjs.map +1 -1
  62. package/dist/src/text/overlayEditor.min.mjs +1 -1
  63. package/dist/src/text/overlayEditor.min.mjs.map +1 -1
  64. package/dist/src/text/overlayEditor.mjs +132 -142
  65. package/dist/src/text/overlayEditor.mjs.map +1 -1
  66. package/dist/src/text/unicode.d.ts +28 -0
  67. package/dist/src/text/unicode.d.ts.map +1 -1
  68. package/dist/src/text/unicode.min.mjs +1 -1
  69. package/dist/src/text/unicode.min.mjs.map +1 -1
  70. package/dist/src/text/unicode.mjs +294 -1
  71. package/dist/src/text/unicode.mjs.map +1 -1
  72. package/dist-extensions/src/shapes/IText/IText.d.ts +31 -6
  73. package/dist-extensions/src/shapes/IText/IText.d.ts.map +1 -1
  74. package/dist-extensions/src/shapes/IText/ITextBehavior.d.ts +12 -0
  75. package/dist-extensions/src/shapes/IText/ITextBehavior.d.ts.map +1 -1
  76. package/dist-extensions/src/shapes/IText/ITextClickBehavior.d.ts.map +1 -1
  77. package/dist-extensions/src/shapes/Text/Text.d.ts +69 -1
  78. package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
  79. package/dist-extensions/src/shapes/Text/constants.d.ts.map +1 -1
  80. package/dist-extensions/src/shapes/Textbox.d.ts +8 -1
  81. package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
  82. package/dist-extensions/src/text/unicode.d.ts +28 -0
  83. package/dist-extensions/src/text/unicode.d.ts.map +1 -1
  84. package/package.json +164 -164
  85. package/rtl-debug.html +358 -200
  86. package/src/shapes/IText/IText.ts +524 -110
  87. package/src/shapes/IText/ITextBehavior.ts +174 -80
  88. package/src/shapes/IText/ITextClickBehavior.ts +20 -6
  89. package/src/shapes/IText/ITextKeyBehavior.ts +15 -15
  90. package/src/shapes/Text/Text.ts +488 -107
  91. package/src/shapes/Text/constants.ts +4 -2
  92. package/src/shapes/Textbox.ts +414 -65
  93. package/src/text/layout.ts +150 -23
  94. package/src/text/overlayEditor.ts +148 -148
  95. package/src/text/unicode.ts +177 -2
package/rtl-debug.html CHANGED
@@ -1,5 +1,6 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
+
3
4
  <head>
4
5
  <title>RTL Text Debug - Visual Bounding Box and Text Position</title>
5
6
  <style>
@@ -10,7 +11,7 @@
10
11
  font-style: normal;
11
12
  font-display: swap;
12
13
  }
13
-
14
+
14
15
  @font-face {
15
16
  font-family: 'STV';
16
17
  src: url('./fonts/STV Bold.ttf') format('truetype');
@@ -18,7 +19,7 @@
18
19
  font-style: normal;
19
20
  font-display: swap;
20
21
  }
21
-
22
+
22
23
  @font-face {
23
24
  font-family: 'STV';
24
25
  src: url('./fonts/STV Light.ttf') format('truetype');
@@ -26,29 +27,35 @@
26
27
  font-style: normal;
27
28
  font-display: swap;
28
29
  }
29
-
30
- body {
31
- font-family: Arial, sans-serif;
30
+
31
+ body {
32
+ font-family: Arial, sans-serif;
32
33
  margin: 20px;
33
34
  background: #f5f5f5;
34
35
  }
35
- canvas {
36
- border: 1px solid #ccc;
36
+
37
+ canvas {
38
+ border: 1px solid #ccc;
37
39
  margin: 10px;
38
40
  background: white;
39
41
  }
42
+
40
43
  .canvas-container {
41
44
  position: relative;
42
45
  }
46
+
43
47
  .canvas-container .upper-canvas {
44
48
  pointer-events: auto;
45
49
  }
50
+
46
51
  .lower-canvas {
47
52
  z-index: 1;
48
53
  }
54
+
49
55
  .upper-canvas {
50
56
  z-index: 2;
51
57
  }
58
+
52
59
  .debug-info {
53
60
  font-family: monospace;
54
61
  background: #333;
@@ -58,13 +65,15 @@
58
65
  border-radius: 4px;
59
66
  white-space: pre-wrap;
60
67
  }
68
+
61
69
  .controls {
62
70
  background: white;
63
71
  padding: 15px;
64
72
  border-radius: 4px;
65
73
  margin: 10px 0;
66
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
74
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
67
75
  }
76
+
68
77
  button {
69
78
  padding: 8px 16px;
70
79
  margin: 5px;
@@ -74,7 +83,11 @@
74
83
  border-radius: 4px;
75
84
  cursor: pointer;
76
85
  }
77
- button:hover { background: #0056b3; }
86
+
87
+ button:hover {
88
+ background: #0056b3;
89
+ }
90
+
78
91
  .text-sample {
79
92
  margin: 5px 0;
80
93
  padding: 5px;
@@ -83,74 +96,96 @@
83
96
  }
84
97
  </style>
85
98
  </head>
99
+
86
100
  <body>
87
101
  <h1>RTL Text Debug - Visual Bounding Box and Text Position</h1>
88
-
102
+
89
103
  <div class="controls">
90
104
  <h3>Test Cases:</h3>
91
105
  <div class="text-sample">Arabic: مرحبا بك في العالم</div>
92
106
  <div class="text-sample">Hebrew: שלום עולם</div>
93
- <div class="text-sample">Mixed: Hello مرحبا World</div>
94
- <div class="text-sample">English LTR: Hello World</div>
95
-
96
- <br>
97
- <button onclick="showRTLText()">Show RTL Arabic Text</button>
98
- <button onclick="showLTRText()">Show LTR English Text</button>
99
- <button onclick="showMixedText()">Show Mixed Text</button>
100
- <button onclick="showSimpleTest()">Simple Test</button>
101
- <button onclick="showSTVFont()">Show STV Regular</button>
102
- <button onclick="showSTVBold()">Show STV Bold</button>
103
- <button onclick="showSTVLight()">Show STV Light</button>
104
- <button onclick="clearCanvas()">Clear Canvas</button>
105
- <button onclick="toggleDebugMode()">Toggle Debug Mode</button>
106
- <button onclick="exportJSON()">Export JSON</button>
107
- <input type="file" id="jsonFile" accept=".json" style="display: none;" onchange="loadJSONFile(event)">
108
- <button onclick="document.getElementById('jsonFile').click()">Import JSON</button>
109
- <div style="margin-top:8px; display:flex; gap:6px; flex-wrap:wrap;">
110
- <button onclick="setAlign('left')">Align Left</button>
111
- <button onclick="setAlign('center')">Align Center</button>
112
- <button onclick="setAlign('right')">Align Right</button>
113
- <button onclick="setAlign('justify')">Justify</button>
114
- <button onclick="setAlign('justify-left')">Justify-Left</button>
115
- <button onclick="setAlign('justify-center')">Justify-Center</button>
116
- <button onclick="setAlign('justify-right')">Justify-Right</button>
117
- </div>
118
- </div>
119
-
120
- <canvas id="canvas" width="1080" height="1080"></canvas>
121
-
122
- <div id="debug-info" class="debug-info">Debug information will appear here...</div>
107
+ <div class="text-sample">Mixed: Hello مرحبا World</div>
108
+ <div class="text-sample">English LTR: Hello World</div>
109
+
110
+ <br>
111
+ <button onclick="showRTLText()">Show RTL Arabic Text</button>
112
+ <button onclick="showLTRText()">Show LTR English Text</button>
113
+ <button onclick="showMixedText()">Show Mixed Text</button>
114
+ <button onclick="showSimpleTest()">Simple Test</button>
115
+ <button onclick="showSTVFont()">Show STV Regular</button>
116
+ <button onclick="showSTVBold()">Show STV Bold</button>
117
+ <button onclick="showSTVLight()">Show STV Light</button>
118
+ <button onclick="clearCanvas()">Clear Canvas</button>
119
+ <button onclick="toggleDebugMode()">Toggle Debug Mode</button>
120
+ <button onclick="exportJSON()">Export JSON</button>
121
+ <input type="file" id="jsonFile" accept=".json" style="display: none;" onchange="loadJSONFile(event)">
122
+ <button onclick="document.getElementById('jsonFile').click()">Import JSON</button>
123
+ <div style="margin-top:8px; display:flex; gap:6px; flex-wrap:wrap;">
124
+ <button onclick="setAlign('left')">Align Left</button>
125
+ <button onclick="setAlign('center')">Align Center</button>
126
+ <button onclick="setAlign('right')">Align Right</button>
127
+ <button onclick="setAlign('justify')">Justify</button>
128
+ <button onclick="setAlign('justify-left')">Justify-Left</button>
129
+ <button onclick="setAlign('justify-center')">Justify-Center</button>
130
+ <button onclick="setAlign('justify-right')">Justify-Right</button>
131
+ </div>
132
+ <div style="margin-top:8px;">
133
+ <strong>Kashida Test (Arabic Justification):</strong>
134
+ </div>
135
+ <div style="margin-top:4px; display:flex; gap:6px; flex-wrap:wrap;">
136
+ <button onclick="showKashidaTest('none')" style="background:#6c757d;">Kashida: None</button>
137
+ <button onclick="showKashidaTest('short')" style="background:#17a2b8;">Kashida: Short (25%)</button>
138
+ <button onclick="showKashidaTest('medium')" style="background:#28a745;">Kashida: Medium (50%)</button>
139
+ <button onclick="showKashidaTest('long')" style="background:#ffc107; color:#333;">Kashida: Long
140
+ (75%)</button>
141
+ <button onclick="showKashidaTest('stylistic')" style="background:#dc3545;">Kashida: Stylistic
142
+ (100%)</button>
143
+ </div>
144
+ <div style="margin-top:4px; display:flex; gap:6px; flex-wrap:wrap;">
145
+ <button onclick="setKashida('none')">Set None</button>
146
+ <button onclick="setKashida('short')">Set Short</button>
147
+ <button onclick="setKashida('medium')">Set Medium</button>
148
+ <button onclick="setKashida('long')">Set Long</button>
149
+ <button onclick="setKashida('stylistic')">Set Stylistic</button>
150
+ </div>
151
+ </div>
152
+
153
+ <canvas id="canvas" width="1080" height="1080"></canvas>
154
+
155
+ <div id="debug-info" class="debug-info">Debug information will appear here...</div>
123
156
  <div id="click-info" class="debug-info">Click on text to see selection coordinates...</div>
124
157
 
125
- <script src="./dist/index.js"></script>
158
+ <script src="./dist/index.js?v=34"></script>
126
159
  <script>
160
+ console.log("DEBUG: Updated rtl-debug.html loaded");
161
+ // alert("DEBUG: Updated RTL Page Loaded - Please check console for 'Hit Testing Debug' when clicking");
127
162
  // Check if fabric.js loaded
128
163
  console.log('fabric object:', typeof fabric);
129
164
  console.log('fabric.Canvas:', typeof fabric.Canvas);
130
165
  console.log('fabric.Text:', typeof fabric.Text);
131
166
  console.log('fabric.IText:', typeof fabric.IText);
132
-
167
+
133
168
  if (typeof fabric === 'undefined') {
134
169
  document.getElementById('debug-info').textContent = 'ERROR: fabric.js not loaded! Check the script path.';
135
170
  }
136
-
171
+
137
172
  const canvas = new fabric.Canvas('canvas', {
138
173
  backgroundColor: 'white',
139
174
  selection: true,
140
175
  preserveObjectStacking: true
141
176
  });
142
-
177
+
143
178
  // Ensure upper canvas is transparent
144
179
  canvas.upperCanvasEl.style.backgroundColor = 'transparent';
145
180
  let debugMode = true;
146
181
  let currentText = null;
147
182
  let debugInfo = document.getElementById('debug-info');
148
183
  let clickInfo = document.getElementById('click-info');
149
-
184
+
150
185
  // Test canvas rendering immediately
151
186
  console.log('Canvas created:', canvas);
152
187
  console.log('Canvas element:', canvas.getElement());
153
-
188
+
154
189
  // Add immediate visual test
155
190
  const testCircle = new fabric.Circle({
156
191
  left: 10,
@@ -177,6 +212,8 @@ Text Properties:
177
212
  ├─ Text: "${text.text}"
178
213
  ├─ Direction: ${text.direction}
179
214
  ├─ TextAlign: ${text.textAlign}
215
+ ├─ Kashida: ${text.kashida || 'none'}
216
+ ├─ enableAdvancedLayout: ${text.enableAdvancedLayout}
180
217
  ├─ Width: ${text.width.toFixed(2)}
181
218
  ├─ Height: ${text.height.toFixed(2)}
182
219
  ├─ Left: ${text.left.toFixed(2)}
@@ -202,11 +239,18 @@ Line Left Offsets:
202
239
  ${text._textLines.map((line, i) => `├─ Line ${i}: ${text._getLineLeftOffset(i).toFixed(2)}`).join('\n')}
203
240
 
204
241
  Character Bounds (first line):
205
- ${text.__charBounds && text.__charBounds[0] ?
206
- text.__charBounds[0].map((char, i) =>
207
- `├─ Char ${i}: left=${char.left.toFixed(2)}, width=${char.width.toFixed(2)}, kernedWidth=${char.kernedWidth.toFixed(2)}`
208
- ).join('\n')
209
- : '├─ No character bounds available'}
242
+ ${text.__charBounds && text.__charBounds[0] ?
243
+ text.__charBounds[0].map((char, i) =>
244
+ `├─ Char ${i}: left=${char.left.toFixed(2)}, width=${char.width.toFixed(2)}, kernedWidth=${char.kernedWidth.toFixed(2)}`
245
+ ).join('\n')
246
+ : '├─ No character bounds available'}
247
+
248
+ Kashida Points (first line):
249
+ ${text.__kashidaInfo && text.__kashidaInfo[0] && text.__kashidaInfo[0].length > 0 ?
250
+ text.__kashidaInfo[0].map((k, i) =>
251
+ `├─ Point ${i}: charIndex=${k.charIndex}, width=${k.width.toFixed(2)}`
252
+ ).join('\n')
253
+ : '├─ No kashida points (kashida: ' + (text.kashida || 'none') + ')'}
210
254
  `;
211
255
  debugInfo.textContent = info;
212
256
  }
@@ -215,7 +259,7 @@ ${text.__charBounds && text.__charBounds[0] ?
215
259
  if (!debugMode || !text) return;
216
260
 
217
261
  const bbox = text.getBoundingRect();
218
-
262
+
219
263
  // Draw bounding box rectangle
220
264
  const bboxRect = new fabric.Rect({
221
265
  left: bbox.left,
@@ -246,9 +290,9 @@ ${text.__charBounds && text.__charBounds[0] ?
246
290
 
247
291
  // Draw text width visualization
248
292
  const textWidthLine = new fabric.Line([
249
- text.left - text.width/2,
293
+ text.left - text.width / 2,
250
294
  text.top,
251
- text.left + text.width/2,
295
+ text.left + text.width / 2,
252
296
  text.top
253
297
  ], {
254
298
  stroke: 'green',
@@ -296,19 +340,21 @@ ${text.__charBounds && text.__charBounds[0] ?
296
340
  currentText = new fabric.Textbox('تجــــربه مرحبا بك في العالم الرقمي', {
297
341
  left: 400,
298
342
  top: 150,
299
- width: 300,
343
+ width: 600,
300
344
  fontFamily: 'Arial',
301
345
  fontSize: 24,
302
346
  direction: 'rtl',
303
347
  textAlign: 'right',
304
348
  fill: '#333',
305
349
  editable: true,
306
- selectable: true
350
+ selectable: true,
351
+ enableAdvancedLayout: true,
352
+ splitByGrapheme: true
307
353
  });
308
-
354
+
309
355
  canvas.add(currentText);
310
356
  canvas.renderAll();
311
-
357
+
312
358
  setTimeout(() => {
313
359
  drawBoundingBoxOverlay(currentText);
314
360
  updateDebugInfo(currentText, 'RTL Arabic Text');
@@ -329,10 +375,10 @@ ${text.__charBounds && text.__charBounds[0] ?
329
375
  editable: true,
330
376
  selectable: true
331
377
  });
332
-
378
+
333
379
  canvas.add(currentText);
334
380
  canvas.renderAll();
335
-
381
+
336
382
  setTimeout(() => {
337
383
  drawBoundingBoxOverlay(currentText);
338
384
  updateDebugInfo(currentText, 'LTR English Text');
@@ -351,12 +397,13 @@ ${text.__charBounds && text.__charBounds[0] ?
351
397
  textAlign: 'center',
352
398
  fill: '#333',
353
399
  editable: true,
354
- selectable: true
400
+ selectable: true,
401
+ enableAdvancedLayout: true
355
402
  });
356
-
403
+
357
404
  canvas.add(currentText);
358
405
  canvas.renderAll();
359
-
406
+
360
407
  setTimeout(() => {
361
408
  drawBoundingBoxOverlay(currentText);
362
409
  updateDebugInfo(currentText, 'Mixed RTL/LTR Text');
@@ -365,7 +412,7 @@ ${text.__charBounds && text.__charBounds[0] ?
365
412
 
366
413
  function showSimpleTest() {
367
414
  clearCanvas();
368
-
415
+
369
416
  // Add a simple visible rectangle first to test canvas
370
417
  const rect = new fabric.Rect({
371
418
  left: 100,
@@ -375,7 +422,7 @@ ${text.__charBounds && text.__charBounds[0] ?
375
422
  fill: 'red'
376
423
  });
377
424
  canvas.add(rect);
378
-
425
+
379
426
  // Test different text approaches
380
427
  console.log('Creating basic fabric.Text...');
381
428
  const basicText = new fabric.Text('BASIC TEXT', {
@@ -387,7 +434,7 @@ ${text.__charBounds && text.__charBounds[0] ?
387
434
  });
388
435
  canvas.add(basicText);
389
436
  console.log('Basic text added:', basicText.getBoundingRect());
390
-
437
+
391
438
  console.log('Creating fabric.IText...');
392
439
  currentText = new fabric.IText('ITEXT HERE', {
393
440
  left: 50,
@@ -399,7 +446,7 @@ ${text.__charBounds && text.__charBounds[0] ?
399
446
  });
400
447
  canvas.add(currentText);
401
448
  console.log('IText added:', currentText.getBoundingRect());
402
-
449
+
403
450
  console.log('Creating RTL text...');
404
451
  const rtlText = new fabric.IText('مرحبا', {
405
452
  left: 50,
@@ -412,16 +459,16 @@ ${text.__charBounds && text.__charBounds[0] ?
412
459
  });
413
460
  canvas.add(rtlText);
414
461
  console.log('RTL text added:', rtlText.getBoundingRect());
415
-
462
+
416
463
  // Force canvas to render and log all objects
417
464
  canvas.renderAll();
418
465
  console.log('Canvas objects:', canvas.getObjects().length);
419
466
  console.log('Canvas dimensions:', canvas.width, 'x', canvas.height);
420
-
467
+
421
468
  // Try to zoom out to see if text is outside viewport
422
469
  canvas.setZoom(0.5);
423
470
  canvas.renderAll();
424
-
471
+
425
472
  updateDebugInfo(currentText, 'Simple Test - Check Console');
426
473
  }
427
474
 
@@ -446,14 +493,14 @@ ${text.__charBounds && text.__charBounds[0] ?
446
493
  }
447
494
 
448
495
  // Handle canvas clicks to show selection information
449
- canvas.on('mouse:down', function(options) {
496
+ canvas.on('mouse:down', function (options) {
450
497
  if (!options.target || !options.target.type || !options.target.type.includes('text')) return;
451
-
498
+
452
499
  const text = options.target;
453
500
  const pointer = canvas.getScenePoint(options.e);
454
501
  const selection = text.getSelectionStartFromPointer(options.e);
455
502
  const cursorBounds = text._getCursorBoundaries(selection);
456
-
503
+
457
504
  const clickDebugInfo = `
458
505
  Click Debug Information:
459
506
  ════════════════════════════════════════════════
@@ -478,15 +525,15 @@ Text Direction: ${text.direction}
478
525
  Text Align: ${text.textAlign}
479
526
  `;
480
527
  clickInfo.textContent = clickDebugInfo;
481
-
528
+
482
529
  // Draw cursor position and character bounds
483
530
  clearDebugOverlays();
484
- drawBoundingBoxOverlay(text);
485
- drawCharacterBounds(text);
486
-
487
- const cursorX = cursorBounds.left + cursorBounds.leftOffset;
488
- const cursorY = cursorBounds.top + cursorBounds.topOffset;
489
-
531
+ drawBoundingBoxOverlay(text);
532
+ drawCharacterBounds(text);
533
+
534
+ const cursorX = cursorBounds.left + cursorBounds.leftOffset;
535
+ const cursorY = cursorBounds.top + cursorBounds.topOffset;
536
+
490
537
  const cursorLine = new fabric.Line([cursorX, cursorY - 10, cursorX, cursorY + 20], {
491
538
  stroke: 'orange',
492
539
  strokeWidth: 3,
@@ -494,7 +541,7 @@ Text Align: ${text.textAlign}
494
541
  evented: false,
495
542
  excludeFromExport: true
496
543
  });
497
-
544
+
498
545
  const cursorLabel = new fabric.Text(`Cursor: (${cursorX.toFixed(0)},${cursorY.toFixed(0)})`, {
499
546
  left: cursorX + 5,
500
547
  top: cursorY - 25,
@@ -504,117 +551,161 @@ Text Align: ${text.textAlign}
504
551
  evented: false,
505
552
  excludeFromExport: true
506
553
  });
507
-
508
- canvas.add(cursorLine, cursorLabel);
509
- });
510
-
511
- // Alignment helper
512
- function setAlign(align) {
513
- if (!currentText) {
514
- alert('Add a text object first.');
515
- return;
516
- }
517
- currentText.set({ textAlign: align });
518
- canvas.requestRenderAll();
519
- clearDebugOverlays();
520
- drawBoundingBoxOverlay(currentText);
521
- updateDebugInfo(currentText, `Align: ${align}`);
522
- }
523
-
554
+
555
+ canvas.add(cursorLine, cursorLabel);
556
+ });
557
+
558
+ // Alignment helper
559
+ function setAlign(align) {
560
+ if (!currentText) {
561
+ alert('Add a text object first.');
562
+ return;
563
+ }
564
+ currentText.set({ textAlign: align });
565
+ canvas.requestRenderAll();
566
+ clearDebugOverlays();
567
+ drawBoundingBoxOverlay(currentText);
568
+ updateDebugInfo(currentText, `Align: ${align}`);
569
+ }
570
+
571
+ // BiDi direction detection
572
+ function getBidiCharDir(char, baseDir) {
573
+ if (/[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(char)) {
574
+ return 'rtl';
575
+ }
576
+ if (/[A-Za-z\u00C0-\u024F\u0370-\u03FF\u0400-\u04FF]/.test(char)) {
577
+ return 'ltr';
578
+ }
579
+ return baseDir; // Neutral inherits base
580
+ }
581
+
582
+ // Analyze BiDi runs
583
+ function getBiDiRuns(text, baseDir) {
584
+ if (!text) return [];
585
+ const runs = [];
586
+ let current = null;
587
+ for (let i = 0; i < text.length; i++) {
588
+ const dir = getBidiCharDir(text[i], baseDir);
589
+ if (!current || current.direction !== dir) {
590
+ if (current) runs.push(current);
591
+ current = { direction: dir, start: i, end: i + 1, text: text[i] };
592
+ } else {
593
+ current.end = i + 1;
594
+ current.text += text[i];
595
+ }
596
+ }
597
+ if (current) runs.push(current);
598
+ return runs;
599
+ }
600
+
524
601
  function drawCharacterBounds(text) {
525
602
  if (!debugMode || !text || !text.__charBounds || !text.__charBounds[0]) return;
526
-
527
- console.log('=== Drawing character bounds ===');
528
- console.log('Text object:', text);
529
- console.log('Text position:', text.left, text.top);
530
- console.log('Text width/height:', text.width, text.height);
531
- console.log('Text direction:', text.direction);
532
- console.log('Text align:', text.textAlign);
533
- console.log('Font family:', text.fontFamily);
534
-
603
+
604
+ console.log('=== Drawing character bounds (BiDi) ===');
535
605
  const bbox = text.getBoundingRect();
536
- console.log('Bounding rect:', bbox);
537
-
538
- const textLeft = text._getLeftOffset() + text.left;
539
- const textTop = text._getTopOffset() + text.top;
606
+ const baseY = bbox.top;
607
+ const isRtl = text.direction === 'rtl';
608
+ const chars = text.__charBounds[0];
540
609
  const lineLeftOffset = text._getLineLeftOffset(0);
541
-
542
- console.log('Calculated offsets:');
543
- console.log(' _getLeftOffset():', text._getLeftOffset());
544
- console.log(' _getTopOffset():', text._getTopOffset());
545
- console.log(' _getLineLeftOffset(0):', lineLeftOffset);
546
- console.log(' Final textLeft:', textLeft);
547
- console.log(' Final textTop:', textTop);
548
-
549
- const isSTVFont = text.fontFamily && text.fontFamily.includes('STV');
550
- console.log('STV font detected:', isSTVFont, '- BiDi processing should be disabled');
551
-
552
- // Draw each character's bounds as they should appear
553
- text.__charBounds[0].forEach((charBound, i) => {
554
- const char = text.text[i] || '';
555
-
556
- // Use bounding box as the base position since that's where text actually renders
557
- const baseX = bbox.left;
558
- const baseY = bbox.top;
559
-
560
- let charX, charWidth;
561
-
562
- if (text.direction === 'rtl') {
563
- // For RTL, start from the right edge of the bounding box
564
- // and work backwards (right to left)
565
- charX = baseX + bbox.width - charBound.left - Math.abs(charBound.kernedWidth);
566
- charWidth = Math.abs(charBound.kernedWidth);
610
+ const lineText = text._textLines[0] ? text._textLines[0].join('') : text.text;
611
+
612
+ console.log('Direction:', text.direction, 'Text:', lineText);
613
+
614
+ // For LTR, use simple positions
615
+ if (!isRtl) {
616
+ chars.forEach((charBound, i) => {
617
+ const charText = text._textLines[0]?.[i] || '';
618
+ const charX = bbox.left + lineLeftOffset + charBound.left;
619
+ const charWidth = charBound.kernedWidth || charBound.width;
620
+ drawCharBox(charX, baseY, charWidth, text.fontSize, i, charText);
621
+ });
622
+ return;
623
+ }
624
+
625
+ // For RTL: analyze BiDi runs and calculate visual positions
626
+ const runs = getBiDiRuns(lineText, 'rtl');
627
+ console.log('BiDi runs:', runs);
628
+
629
+ // Build char info
630
+ const charInfos = chars.map((c, i) => ({
631
+ charIndex: i,
632
+ width: c.kernedWidth || c.width,
633
+ grapheme: text._textLines[0]?.[i] || ''
634
+ }));
635
+
636
+ // Group chars by runs
637
+ const runInfos = [];
638
+ for (const run of runs) {
639
+ const runChars = [];
640
+ let totalWidth = 0;
641
+ let strPos = 0;
642
+ for (let i = 0; i < charInfos.length; i++) {
643
+ const gLen = charInfos[i].grapheme.length || 1;
644
+ if (strPos >= run.start && strPos < run.end) {
645
+ runChars.push(charInfos[i]);
646
+ totalWidth += charInfos[i].width;
647
+ }
648
+ strPos += gLen;
649
+ }
650
+ if (runChars.length > 0) {
651
+ runInfos.push({ direction: run.direction, chars: runChars, totalWidth });
652
+ }
653
+ }
654
+
655
+ // Reverse runs for RTL base
656
+ runInfos.reverse();
657
+
658
+ // Calculate visual positions
659
+ const visualPositions = new Map();
660
+ let visualX = 0;
661
+ for (const runInfo of runInfos) {
662
+ if (runInfo.direction === 'rtl') {
663
+ let runX = visualX + runInfo.totalWidth;
664
+ for (const c of runInfo.chars) {
665
+ runX -= c.width;
666
+ visualPositions.set(c.charIndex, { x: runX, width: c.width });
667
+ }
567
668
  } else {
568
- // For LTR, start from left edge and add character offset
569
- charX = baseX + charBound.left;
570
- charWidth = charBound.kernedWidth;
669
+ let runX = visualX;
670
+ for (const c of runInfo.chars) {
671
+ visualPositions.set(c.charIndex, { x: runX, width: c.width });
672
+ runX += c.width;
673
+ }
571
674
  }
572
-
573
- // Use different colors for STV vs standard fonts
574
- const fillColor = isSTVFont ?
575
- 'rgba(255, 165, 0, 0.3)' : // Orange for STV (no BiDi)
576
- (i % 2 === 0 ? 'rgba(255, 0, 255, 0.2)' : 'rgba(0, 255, 255, 0.2)'); // Magenta/Cyan for others
577
- const strokeColor = isSTVFont ? 'orange' : (i % 2 === 0 ? 'magenta' : 'cyan');
578
-
579
- // Draw character boundary rectangle
580
- const charRect = new fabric.Rect({
581
- left: charX,
582
- top: baseY,
583
- width: charWidth,
584
- height: text.fontSize,
585
- fill: fillColor,
586
- stroke: strokeColor,
587
- strokeWidth: isSTVFont ? 2 : 1,
588
- selectable: false,
589
- evented: false,
590
- excludeFromExport: true
591
- });
592
-
593
- // Add character label
594
- const charLabel = new fabric.Text(`${isSTVFont ? 'STV-' : ''}${i}:"${char}"`, {
595
- left: charX,
596
- top: baseY - 15,
597
- fontSize: 10,
598
- fill: isSTVFont ? 'darkorange' : 'purple',
599
- fontFamily: 'Arial', // Use Arial for labels to ensure readability
600
- selectable: false,
601
- evented: false,
602
- excludeFromExport: true
603
- });
604
-
605
- canvas.add(charRect, charLabel);
606
-
607
- console.log(`${isSTVFont ? 'STV ' : ''}Char ${i} "${char}":`, {
608
- boundLeft: charBound.left,
609
- kernedWidth: charBound.kernedWidth,
610
- calculatedX: charX,
611
- width: charWidth
612
- });
675
+ visualX += runInfo.totalWidth;
676
+ }
677
+
678
+ // Draw character boxes
679
+ chars.forEach((charBound, i) => {
680
+ const charText = charInfos[i]?.grapheme || '';
681
+ const pos = visualPositions.get(i);
682
+ if (!pos) return;
683
+ const charX = bbox.left + lineLeftOffset + pos.x;
684
+ drawCharBox(charX, baseY, pos.width, text.fontSize, i, charText);
685
+ console.log(`Char ${i} "${charText}": visualX=${pos.x.toFixed(2)}, canvasX=${charX.toFixed(2)}`);
686
+ });
687
+ }
688
+
689
+ function drawCharBox(x, y, width, height, index, charText) {
690
+ const fillColor = index % 2 === 0 ? 'rgba(255, 0, 255, 0.2)' : 'rgba(0, 255, 255, 0.2)';
691
+ const strokeColor = index % 2 === 0 ? 'magenta' : 'cyan';
692
+
693
+ const charRect = new fabric.Rect({
694
+ left: x, top: y, width: width, height: height,
695
+ fill: fillColor, stroke: strokeColor, strokeWidth: 1,
696
+ selectable: false, evented: false, excludeFromExport: true
697
+ });
698
+
699
+ const charLabel = new fabric.Text(`${index}:"${charText}"`, {
700
+ left: x, top: y - 15, fontSize: 10, fill: 'purple',
701
+ fontFamily: 'Arial', selectable: false, evented: false, excludeFromExport: true
613
702
  });
703
+
704
+ canvas.add(charRect, charLabel);
614
705
  }
615
706
 
616
707
  // Update debug info when text properties change
617
- canvas.on('object:modified', function(options) {
708
+ canvas.on('object:modified', function (options) {
618
709
  if (options.target && options.target.type.includes('text')) {
619
710
  clearDebugOverlays();
620
711
  drawBoundingBoxOverlay(options.target);
@@ -638,16 +729,16 @@ Text Align: ${text.textAlign}
638
729
  editable: true,
639
730
  selectable: true
640
731
  });
641
-
732
+
642
733
  canvas.add(currentText);
643
734
  canvas.renderAll();
644
-
735
+
645
736
  setTimeout(() => {
646
737
  drawBoundingBoxOverlay(currentText);
647
738
  updateDebugInfo(currentText, 'STV Regular Font Arabic Text');
648
739
  }, 300);
649
740
  }
650
-
741
+
651
742
  function showSTVBold() {
652
743
  clearCanvas();
653
744
  currentText = new fabric.Textbox('هذا نص تجريبي بخط STV الغامق للاختبار', {
@@ -663,16 +754,16 @@ Text Align: ${text.textAlign}
663
754
  editable: true,
664
755
  selectable: true
665
756
  });
666
-
757
+
667
758
  canvas.add(currentText);
668
759
  canvas.renderAll();
669
-
760
+
670
761
  setTimeout(() => {
671
762
  drawBoundingBoxOverlay(currentText);
672
763
  updateDebugInfo(currentText, 'STV Bold Font Arabic Text');
673
764
  }, 300);
674
765
  }
675
-
766
+
676
767
  function showSTVLight() {
677
768
  clearCanvas();
678
769
  currentText = new fabric.Textbox('هذا نص تجريبي بخط STV الخفيف للاختبار', {
@@ -688,10 +779,10 @@ Text Align: ${text.textAlign}
688
779
  editable: true,
689
780
  selectable: true
690
781
  });
691
-
782
+
692
783
  canvas.add(currentText);
693
784
  canvas.renderAll();
694
-
785
+
695
786
  setTimeout(() => {
696
787
  drawBoundingBoxOverlay(currentText);
697
788
  updateDebugInfo(currentText, 'STV Light Font Arabic Text');
@@ -702,17 +793,17 @@ Text Align: ${text.textAlign}
702
793
  function exportJSON() {
703
794
  const json = JSON.stringify(canvas.toJSON(['fontFamily', 'direction', 'textAlign']));
704
795
  console.log('Canvas JSON:', json);
705
-
796
+
706
797
  // Create download link
707
798
  const link = document.createElement('a');
708
799
  link.download = `rtl-debug-canvas-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
709
800
  link.href = 'data:text/json;charset=utf-8,' + encodeURIComponent(json);
710
-
801
+
711
802
  // Trigger download
712
803
  document.body.appendChild(link);
713
804
  link.click();
714
805
  document.body.removeChild(link);
715
-
806
+
716
807
  debugInfo.textContent = 'Canvas exported as JSON file';
717
808
  return json;
718
809
  }
@@ -723,7 +814,7 @@ Text Align: ${text.textAlign}
723
814
  if (!file) return;
724
815
 
725
816
  const reader = new FileReader();
726
- reader.onload = function(e) {
817
+ reader.onload = function (e) {
727
818
  importJSON(e.target.result);
728
819
  };
729
820
  reader.readAsText(file);
@@ -733,15 +824,15 @@ Text Align: ${text.textAlign}
733
824
  try {
734
825
  console.log('Loading JSON into canvas...');
735
826
  const parsed = JSON.parse(jsonString);
736
-
737
- canvas.loadFromJSON(parsed, function() {
827
+
828
+ canvas.loadFromJSON(parsed, function () {
738
829
  canvas.requestRenderAll();
739
830
  console.log('JSON loaded successfully');
740
-
831
+
741
832
  // Find the first text object
742
833
  const objects = canvas.getObjects();
743
834
  currentText = objects.find(obj => obj.type === 'textbox' || obj.type === 'text' || obj.type === 'i-text');
744
-
835
+
745
836
  if (currentText) {
746
837
  console.log('Found text object:', currentText);
747
838
  setTimeout(() => {
@@ -751,7 +842,7 @@ Text Align: ${text.textAlign}
751
842
  updateDebugInfo(currentText, 'Loaded from JSON');
752
843
  }, 300); // Extra time for font loading
753
844
  }
754
-
845
+
755
846
  debugInfo.textContent = `JSON loaded successfully. Found ${objects.length} objects.`;
756
847
  });
757
848
  } catch (error) {
@@ -760,8 +851,75 @@ Text Align: ${text.textAlign}
760
851
  }
761
852
  }
762
853
 
854
+ // Kashida test function
855
+ function showKashidaTest(kashidaLevel) {
856
+ clearCanvas();
857
+
858
+ // Create multiple lines of Arabic text to test justify with kashida
859
+ const arabicText = 'هذا نص تجريبي طويل لاختبار خاصية الكاشيدة في المحاذاة المتساوية للنصوص العربية وهو يحتوي على عدة كلمات';
860
+
861
+ currentText = new fabric.Textbox(arabicText, {
862
+ left: 400,
863
+ top: 150,
864
+ width: 350,
865
+ fontFamily: 'Arial',
866
+ fontSize: 24,
867
+ direction: 'rtl',
868
+ textAlign: 'justify',
869
+ fill: '#333',
870
+ editable: true,
871
+ selectable: true,
872
+ kashida: kashidaLevel,
873
+ enableAdvancedLayout: true
874
+ });
875
+
876
+ canvas.add(currentText);
877
+ canvas.renderAll();
878
+
879
+ setTimeout(() => {
880
+ drawBoundingBoxOverlay(currentText);
881
+ updateDebugInfo(currentText, `Kashida: ${kashidaLevel}`);
882
+
883
+ // Show kashida info in debug
884
+ if (currentText.__kashidaInfo) {
885
+ console.log('Kashida Info:', currentText.__kashidaInfo);
886
+ }
887
+ }, 100);
888
+ }
889
+
890
+ // Set kashida on current text
891
+ function setKashida(level) {
892
+ if (!currentText) {
893
+ alert('Add a text object first.');
894
+ return;
895
+ }
896
+
897
+ // Kashida only works with justify alignment
898
+ if (!currentText.textAlign.includes('justify')) {
899
+ currentText.set({ textAlign: 'justify' });
900
+ console.log('Changed textAlign to justify (required for kashida)');
901
+ }
902
+
903
+ currentText.set({ kashida: level });
904
+
905
+ // Force clear cache and reset dimension state to force recalculation
906
+ currentText._clearCache();
907
+ currentText._lastDimensionState = null; // Force initDimensions to recalculate
908
+ currentText.initDimensions();
909
+ currentText.setCoords();
910
+
911
+ canvas.requestRenderAll();
912
+ clearDebugOverlays();
913
+ drawBoundingBoxOverlay(currentText);
914
+ updateDebugInfo(currentText, `Kashida: ${level}`);
915
+
916
+ console.log('Kashida set to:', level);
917
+ console.log('Kashida Info:', currentText.__kashidaInfo);
918
+ }
919
+
763
920
  // Initialize with RTL text
764
921
  showRTLText();
765
922
  </script>
766
923
  </body>
767
- </html>
924
+
925
+ </html>