---
name: konva-text-typography-fx
description: دليل مهارة تصيير النصوص العربية واللاتينية في Konva — دورة الارتفاع والالتفاف، جاهزية الخط وإعادة القياس، مطابقة محرر النص للكانفس، النص المنحني، وفلتر التمدد الشكلي للحدود
---

# ✍️ مهارة فنون وتصيير النصوص المتقدمة (Konva Typography & Text Effects)

يوفر هذا الدليل المعايير الصارمة لرسم النصوص والخطوط الاحترافية وتأثيرات الحدود والتوهج في Grido Studio. المرجع التنفيذي هو `frontend/src/components/editor/konva/elements/text-node.tsx`، ويجب أن يطابق أي مسار نصي آخر (المحرر، التصدير، الطباعة، الملصقات) هذه الثوابت بدل إعادة اختراعها.

---

## 🖋️ 1. معيار فلتر التمدد الشكلي للحدود (Morphological Text Stroke Dilation)

### القاعدة الصارمة:
يُمنع إجبارياً استخدام `ctx.strokeText` أو خاصية `stroke` الافتراضية في Konva للنصوص المتصلة (مثل الخط العربي أو الخطوط الإنجليزية المتصلة Cursive).
- **السبب:** رسم الحدود بشكل منفرد لكل حرف (Glyph) يولد زوايا حادة تقطع وصلات الحروف وتنزف داخل الأحرف المجاورة.
- **الحل المعتمد:** تطبيق الحدود عبر فلتر التمدد الشكلي الموحد المطبق على النص ككتلة واحدة مجمعة (`ensureTextStrokeFilter`).

```typescript
// النواة المطبقة حالياً: فلتر واحد يلف رسم النص كاملاً، لا لكل حرف
const filterId = ensureTextStrokeFilter(strokeWidth, strokeColor);
if (filterId) {
  const ctx = context._context;
  ctx.save();
  ctx.filter = `url(#${filterId})`;
  Konva.Text.prototype._sceneFunc.call(shape, context);
  ctx.restore(); // يعيد ctx.filter إلى حالته السابقة
}
```

### النص المنحني:
لا تضع `ctx.filter` حول حلقة `fillText` لكل محرف؛ لأن الفلتر يُطبَّق على كل عملية رسم منفصلة، فتظهر حدود داخلية عند تلامس الحروف. ارسم النص المنحني كاملاً على canvas وسيط بلا فلتر، ثم طبّق فلتر التمدد مرة واحدة على الصورة الناتجة.

---

## 📐 2. الالتفاف التلقائي للنصوص بدون تذبذب (Auto-wrapping Invariants)

1. استخدم `wrap="word"` مع `ellipsis={false}` كافتراضي. لا تفعّل `char` أو `none` إلا بقرار صريح من المستخدم، ولا تفعّل `ellipsis` إلا مع صندوق ثابت الأبعاد.
2. مرر `width` فقط بدون فرض `height` مسبق إلى عقدة `<Text>` في وضع النمو التلقائي.
3. لا تستخدم `useState` محلياً لتخزين ارتفاع النص؛ بل اقرأ الارتفاع في `useEffect` وادفع القيمة المطبعة للـ Store مباشرة.

### تحذير الارتفاع (عيب موثّق):
`textHeight` في Konva 10.x ليس ارتفاع الكتلة؛ بل يساوي `fontSize` فقط، و`getTextHeight()` مهجورة. الدورة التالية تسبب اقتطاع كل نص ملتف أو متعدد الأسطر إلى سطر واحد:

```typescript
// ممنوع
const h = node.textHeight / canvasHeight; // = fontSize فقط
onChange({ height: h });
// ثم
<Text height={h} />
// Konva يتوقف عن إضافة الأسطر عند تجاوز maxHeightPx
```

### الطريقة الصحيحة:
- في وضع النمو التلقائي: لا تمرر `height` إلى عقدة العرض. قس الارتفاع على عقدة `Konva.Text` معزولة بـ `height='auto'` بنفس الخط/الوزن/العرض/الالتفاف/الاتجاه عبر `measureTextHeightPx` (`frontend/src/lib/canvas/text-measure.ts`)، ثم ادفع الناتج للـ Store.
- اعرض النص بلا `height` إطلاقاً ومع `verticalAlign="top"`؛ فارتفاع الصندوق دائماً مشتق من المحتوى (`layoutHeightPx`) ولا يُضبط يدوياً من المقابض أو لوحة الخصائص.
- احمِ مزامنة الارتفاع من الحلقات بمقارنة القيمة الهدف مع `element.height` المخزَّنة نفسها، لا بـ `lastSetHeightRef`؛ هذا يبقي Undo/Redo وتغيير الارتفاع اليدوي سليمين.
- أضف اختبار انحدار: نص من 3 أسطر ملتفة يبقى 3 أسطر بعد مزامنة الارتفاع، ونص فيه `\n` لا يُقتطع.

### سقف العرض التلقائي وهندسة الاختبارات (`AUTO_TEXT_MAX_WIDTH_RATIO`):
- دالة `clampAutoTextWidthPx` تفرض سقفاً أقصى لعرض الصندوق التلقائي هو **95% من عرض الكانفاس** (`AUTO_TEXT_MAX_WIDTH_RATIO = 0.95`).
- عند اختبار سلوك وضع `auto` ومقارنته بـ `fixed`، يجب أن يكون عرض النص الطبيعي أكبر من عرض الصندوق الثابت (ليلتف في `fixed`) ولكن **أقل قطعاً من 95% من عرض الكانفاس** حتى لا يلتف قسرياً ويفشل فحص الارتفاع أحادي السطر (`fontSize * lineHeight`).

---

## 3. جاهزية الخط وإعادة القياس (Font Readiness)

Canvas يقيس الخط النشط لحظة الرسم؛ إن لم يكتمل تحميل خط الويب فسيقيس الخط البديل بصمت.

1. انتظر الخط المطلوب بوزنه الفعلي، لا تكتفِ بـ `document.fonts.ready`:
   ```typescript
   await document.fonts.load(`${style} ${weight} 16px "${family}"`);
   ```
   - مفتاح كاش التحميل يجب أن يجمع `family + weight + style`؛ مفتاح العائلة وحدها يعيد وعداً محمّلاً لوزن آخر فيبقى القياس على الخط البديل.
   - الخطوط النظامية/العامة (Arial، Segoe UI، sans-serif، ui-*) تُعتبر جاهزة فوراً: لا تُحمَّل ولا تُحقن لها ورقة Google.
   - الخطوط المضمنة offline يجب أن تمر بـ `document.fonts.load` أيضاً ثم تُطلق إشعار الجاهزية، لا أن تعود بصمت.
2. لا تسجّل الخط كمحمّل قبل نجاح التحميل؛ احذف مفتاح الكاش عند الفشل كي تُعاد المحاولة ولا يبقى وعد مرفوض معلّقاً.
3. بعد الجاهزية أعد قياس النص (تغيير خاصية عامة، أو `_setTextData` المحمي بإصدار Konva المثبت)، ثم:
   - أعد رسم الطبقة بـ `batchDraw`.
   - حدّث `height` و`width` في الـ Store.
   - أعد تشغيل ملاءمة العرض إن كانت مفعّلة.
4. انتظر الخطوط قبل التصدير والطباعة؛ لا تلتقط Stage أثناء تحميل خط.
5. راعِ الأوزان المتاحة فعلياً: الخطوط المضمنة قد توفر 400 و700 فقط، وعرض أوزان 100–900 لا يعني وجود ملف لكل وزن.

---

## 4. مطابقة محرر النص للكانفس (WYSIWYG Overlay)

محرر DOM (textarea) فوق الكانفس هو الحل الصحيح، لكن يجب أن يطابق هندسة النص المرسوم.

- **العرض:** لا تجعل الحشوة داخلية تقلّص عرض المحتوى. إذا كانت شارة الكانفس تُرسم خارج صندوق النص، فأضف حشوتها إلى عرض المحرر أو استخدم `content-box` بما يطابق `maxWidth` في Konva.
- **البيانات:** طبّق `sanitizeText`، وتحويل الأرقام المشرقية، و`textTransform`، و`letterSpacing/wordSpacing` بنفس شروط المسار المرسوم.
- **المظهر:** اعرض التعبئة (لون/تدرج/صورة)، والحدود، والظل، و`verticalAlign`، ولون الخلفية. لا تُظهر نصاً مصمتاً بينما الكانفس يعرض تدرجاً أو صورة.
- **المنحني:** لا تحرّر نصاً منحنياً كسطر مستقيم دون تنبيه؛ إما اعرض معاينة منحنية، أو امنع التحرير المباشر واعرض شارة «نص منحني» مع تحرير المحتوى فقط.
- **الإدخال:** عرّف سلوك `Enter` و`Escape` بوضوح وثبّته باختبار. إن كان `Escape` يلتزم بالتعديل فيجب أن يكون ذلك قراراً مقصوداً لا مفاجأة للمستخدم.
- لا تعتمد على مهلة زمنية ثابتة وحدها لإظهار المحرر؛ اجعل جاهزية الخط شرطاً فعلياً.

---

## 5. العربية والاتجاه المختلط والنص المنحني

- لا تضبط اتجاه العقدة كلها `rtl` لمجرد وجود أي محرف عربي. استخدم أول محرف قوي (`resolveTextDirection`) لكل من عقدة Konva و`ctx.direction` و`dir` في محرر DOM، ولا تعتمد على فحص «يحتوي عربي».
- `direction="inherit"` مسموح فقط إذا كانت حاوية Stage مضبوطة صراحة `dir="ltr"`؛ وإلا مرر `ltr` للاتيني صراحة.
- تباعد الحروف للعربية: `resolveTextRenderState` يصفّر التباعد في العربية **للاتجاهين** (الموجب يُعوَّض بمسافات كلمات، والسالب يُلغى لأن تقريب الحروف يكسر الوصلات)، ويُترك التباعد للاتيني فقط. لا تعِد تطبيق `Math.min(0, letterSpacing)` أو أي تباعد عربي داخل رسام النص المنحني؛ الاعتماد على الحالة المشتركة وحدها.
- النص المنحني الحالي يدعم سطراً واحداً ولا يدعم `lineHeight` أو `textDecoration` أو `verticalAlign`. يدعم ترتيب bidi المبسّط بعكس المقاطع المخالفة لاتجاه الفقرة (`buildCurvedGlyphs`) والحدود الموحدة عبر كانفاس وسيط، لكنه ليس تنفيذاً كاملاً لـ UAX #9؛ لا تدّعِ أكثر من ذلك.
- `TextPath` ليس بديلاً عن تشكيل العربية المتصلة؛ فهو يرسم المحارف منفردة ولا يطبق bidi كاملاً. أبقِ الرسام اليدوي للعربية، أو أضف تشكيلاً وترتيباً صريحين.
- مسار الملصقات (SVG) يستخدم أول محرف قوي؛ حافظ على تطابق منطقه مع الكانفس بدل وجود سلوكين مختلفين.

---

## 6. تطابق التصدير والطباعة

1. **المسار الأساسي:** التقاط Stage بدقة الهدف هو الضمانة الوحيدة للتطابق الكامل، لأنه يعيد استخدام نفس تخطيط Konva وتأثيراته. أبقِه المسار الافتراضي للنص.
2. **المسار الاحتياطي:** إن فشل الالتقاط، يجب أن يعيد استخدام نفس منطق التخطيط لا نسخة مبسطة. تحديداً:
   - كسر الكلمات الطويلة وتقطيع الكلمة الأعرض من السطر كما يفعل Konva (وليس تركها تتجاوز الصندوق).
   - اشتقاق ارتفاع الكتلة من المحتوى (`layoutH = contentHeight` للمستقيم و`measureTextContentHeightPx` للمنحني) دون إزاحة رأسية يدوية (`alignY = 0`).
   - خط أساس `alphabetic` مع إزاحة `(ascent - descent) / 2 + lineHeight / 2` بدل `textBaseline='middle'`؛ هذا يطابق `_sceneFunc` في Konva ويحافظ على موضع التزيين.
   - سماكة التزيين `fontSize / 15` وإزاحته `fontSize / 4`، لا `fontSize / 16` ولا `fontSize / 2`.
   - دمج `shadowOpacity` في لون الظل عبر `colorWithAlpha`؛ Canvas 2D لا يملك خاصية مستقلة، وإغفالها يجعل الظل معتماً في التصدير فقط.
   - نفس `lineHeight` و`letterSpacing` و`direction` (أول محرف قوي) و`ctx.direction`.
3. **الطباعة:** أي نص يجعل الصفحة غير مؤهلة للمسار السريع في Go، فتُلتقط كاملة بدقة الطباعة. لا تضف مسار نص أصلي في Go قبل مطابقة التشكيل العربي والالتفاف والاتجاه.
4. لا تبدأ التصدير أو الطباعة قبل جاهزية الخطوط وإعادة القياس.

---

## 7. نموذج تحجيم النص ومحوّله (Text Sizing & Transformer)

### 7.1 نموذج `textSizing`

- `textSizing: 'auto' | 'fixed'` هو مصدر سلوك الصندوق. المستندات القديمة بلا الحقل تُقرأ `fixed` عبر `resolveTextSizing` — لا تفترض `auto` أبداً.
- **الارتفاع مشتق من المحتوى في الوضعين** ولا يُخزَّن كقيمة يدوية. `auto` يعرض بأطول سطر طبيعي مقيس، و`fixed` يثبّت العرض ويلتف النص داخله فيتغيّر الارتفاع.
- في `auto`: `clampAutoTextWidthPx` تحصر العرض بين `AUTO_TEXT_MIN_WIDTH_PX` (24px) و`AUTO_TEXT_MAX_WIDTH_RATIO` (95% من عرض الورقة). لا تتجاوز الورقة حتى ولو كان السطر أطول.
- **التوسّع يحافظ على المركز:** عند تغيّر العرض بمقدار `Δw` والدوران `θ`، أزح الأصل بمقدار `−R(θ)·(Δw/2, 0)`:
  `x -= cos(θ)·Δw/2` و`y -= sin(θ)·Δw/2`. الإشارة السالبة إلزامية — الجمع يزيح المركز إلى `0.79` بدل `0.25` في اختبار الانحدار.
- في `fixed`: تغيير حجم الخط أو الوزن لا يغيّر العرض المخزَّن، بل يعيد حساب الارتفاع بالالتفاف فقط.
- المصدر الوحيد للحساب هو `fitTextGeometry` في `frontend/src/lib/canvas/text-fit.ts`، ويستهلكه المتجر (`fitTextToContent`) والعقدة والمحرر المباشر وتصدير الكانفاس. لا تكرّر معادلة العرض/المركز في مكوّن جديد.

### 7.2 محوّل التحديد (Transformer)

- للنص **كل المقابض الثمانية** (زوايا + منتصفات حواف) + مقبض التدوير. تقييد المقابض بالحواف الجانبية يجعل المستخدم يظن أن المربع فقد أركانه — لا تفعل ذلك.
- **الزوايا الأربع (`isCornerAnchor`):** تحجّم كتلة النص تناسبياً (`keepRatio`)، وعند `transformend` تُترجم نسبة التحجيم (`absScaleY`) إلى `fontSize` و`width` و`height` مع حفظ الركن المقابل في `textResizeAnchor` (`oppositeCornerAnchor`) ليبقى ذلك الركن ثابتاً عبر أي إعادة قياس لاحقة؛ وتعديل مقاييس الخط من لوحة الخصائص أو المحرر يصفّر `textResizeAnchor`.
- **منتصفات الحواف (`middle-left/right`, `top/bottom-center`):** تضبط العرض فقط مع تثبيت الارتفاع وإسقاط إزاحة الأصل على المحور المحلي `x` (بحيث لا تنزاح الحافة العليا رأسياً عند سحب `top-center`)، وتكتب `{ width, textSizing: 'fixed', textResizeAnchor: undefined }` عند `transformend`؛ وكذلك يفعل تعديل العرض `W` من تبويب «ترتيب» بينما يبقى حقل `H` معطّلاً للنص.
- المحوّل يعرض أبعاداً بوحدات مساحة العرض (مضروبة في `stageScale`). في الاختبارات طبّعها عبر `tr.width() / stage.scaleX()` قبل مقارنتها بـ `node.width()`.
- بعد أي تغيير هندسي من المتجر (لوحة/أسهم) استدعِ `forceUpdate` تحت حاجز `!isTransforming && !isDragging`، وإلا بقي الصندوق على الحجم القديم.

### 7.3 لوحة الخصائص

- «تلقائي» يستدعي `fitTextToContent(id)` (auto + قياس + توسيط)، و«ثابت» يكتب `textSizing:'fixed'` فقط. خيارات الالتفاف (كلمات/أحرف/بلا) تظهر في وضع الثبات وحده.
- حجم الخط والوزن والمحاذاة كلها تُبقي النموذج صالحاً: `auto` يعيد الاتساع تلقائياً، و`fixed` يعيد الالتفاف.

---

## 8. قائمة فحص مراجعة النصوص

- [ ] لا يوجد أي استخدام لـ `textHeight` أو `getTextHeight()` لحساب ارتفاع الصندوق.
- [ ] القياس يتم على عقدة معزولة `height='auto'`، وعقدة العرض لا تستلم `height` ثابتاً.
- [ ] كاش تحميل الخطوط يميّز الوزن والنمط، والخطوط النظامية لا تُحقن لها ورقة Google.
- [ ] نص من 3 أسطر يبقى 3 أسطر بعد مزامنة الارتفاع وUndo/Redo.
- [ ] تحميل خط بارد (خاصة offline) يعيد القياس ويحدّث ارتفاع وعرض العنصر في الـ Store.
- [ ] عرض محتوى المحرر وحشوته يطابقان الكانفس، بما في ذلك شارة الخلفية.
- [ ] الأرقام المشرقية والتدرج والحدود والظل والنص المنحني متطابقة بين المعاينة والتحرير.
- [ ] النص المختلط عربي/لاتيني يحافظ على ترتيب bidi وعلامات الترقيم في المستقيم والمنحني.
- [ ] التصدير الفوري ينتظر جاهزية خطوط عناصر النص (`ensureElementsFontsLoaded`) ويطابق المعاينة.
- [ ] `auto` يوسّع الصندوق حول المركز (فرق < 1.5px) بعد تغيير حجم الخط، و`fixed` يثبّت العرض ويزيد الارتفاع بالالتفاف.
- [ ] كل مقابض محوّل النص الثمانية ظاهرة وقابلة للنقر: سحب الزاوية يحجّم حجم الخط تناسبياً مع تثبيت الركن المقابل، وسحب منتصف الحافة يغيّر العرض فقط ويحوّل النص إلى `fixed`.
- [ ] النص بلا حقل `textSizing` (مستند قديم) يُفتح بعرض ثابت ولا يقفز إلى الحضن تلقائياً.
- [ ] اختبارات النصوص (الالتفاف، الارتفاع، جاهزية الخط، المحرر، التصدير) تعمل قبل الإغلاق.
