---
name: konva-rendering-invariants
description: دليل مهارة موحد لثوابت الرسم والأداء في Konva (الطبقات، الكاش و VRAM، الزوم السلس، المجموعات الذرية، النصوص، الفلاتر، الكولاج الحر) في Grido Studio
---

# ⚡ مهارة ثوابت الرسم والأداء في Konva (Konva Rendering Invariants)

يوحّد هذا الدليل المهارات القديمة (`konva-canvas-optimizer`, `konva-gesture-touch-transformer`, `konva-layer-composite-filters`, `konva-freeform-collage-engine`) في مرجع واحد. القواعد المشتركة مع `.agents/AGENTS.md` موجودة هنا بصيغة مفصلة، وAGENTS.md هو الملخص الإلزامي — عند أي تعديل على قاعدة، عدّلها هنا أولاً ثم زامن الملخص في AGENTS.md.

---

## 🧱 1. الكاش وذاكرة VRAM (Cache & VRAM Rules)

1. **حظر إدراج إحداثيات السحب في تبعيات الكاش:** يُمنع إضافة `x`, `y`, `dragX`, `dragY`, أو `zoom` كمصفوفة تبعيات للـ `useEffect` الذي يستدعي `node.cache()`. استدعاء الكاش أثناء السحب يعيد إنشاء القوام (Texture) في VRAM بكل فريم ويؤدي للتقطيع.
2. **التعديل الذكي للـ Cache:** فعّل `node.cache()` فقط على العناصر التي تحتوي فلاتر معقدة (مثل CSS Filters أو أغطية الألوان)، وتجنّب استدعاءه أثناء حركات السحب والإسقاط النشطة.
3. **تثبيت الـ pixelRatio:** يُمنع تعديل أو خفض `pixelRatio` الخاص بـ Konva Stage ديناميكياً أثناء Zoom/Pan لتفادي إعادة تخصيص VRAM ومسح السياق وتذبذب البكسلات الفرعية (Subpixel Jitter). الاعتماد دائماً على التحجيم الأسي الناعم للـ CSS/Stage.

---

## 🚫 2. الطبقات المهملة وفصل الاهتمامات (Layers)

1. **استبدال FastLayer:** استخدام `<Layer listening={false}>` دائماً بدلاً من `FastLayer` المهملة (Deprecated) لتفادي تحذيرات الـ Console وضمان التوافقية مع محرك Konva الحديث.
2. **فصل الطبقة الثابتة عن التفاعلية:** احتفظ بعناصر الشبكة والمساطر في `Layer` منفصل عن عناصر الصور والنصوص التفاعلية، مع `listening={false}` على طبقة الشبكة والمساطر لمنع حساب تداخل المؤشر (Hit Graph) على آلاف النقاط.

---

## 🖐️ 3. التكبير السلس غير الخانق (Smooth Accumulated Zooming)

* يُمنع قراءة Zoom من الـ Store مباشرة أثناء الـ Wheel؛ يتم تجميع الهدف المحسوب محلياً واستخدام التقريب الأسي:

$$\text{Zoom}_{\text{new}} = \text{Zoom}_{\text{target}} \times e^{-\Delta y \times \text{factor}}$$

```typescript
let accumulatedZoom = currentZoom;
export function handleSmoothWheel(e: KonvaEventObject<WheelEvent>, stage: Konva.Stage) {
  e.evt.preventDefault();
  const factor = 0.0015;
  accumulatedZoom *= Math.exp(-e.evt.deltaY * factor);
  accumulatedZoom = Math.min(Math.max(accumulatedZoom, 0.1), 10);
  requestAnimationFrame(() => {
    stage.scale({ x: accumulatedZoom, y: accumulatedZoom });
    stage.batchDraw();
  });
}
```

* استخدم `transform: scale(zoom)` على الحاوية الخارجية (Wrapper Div) عند تدوير عجلة الفأرة لتجنب إعادة حساب أبعاد كافة عناصر Stage في React.
* فعّل `listening={false}` لجميع العناصر أثناء الـ Pinch-to-zom لضمان 60 إطاراً في الثانية.

---

## 📦 4. المجموعات الذرية والسحب المجمّع (Atomic Groups & Composite Dragging)

1. **الذرات المجمعة:** عند إضافة خلفية `<Rect>` أو قناع حول صورة، غلّفهما إجبارياً داخل `<Group>` واحد مع وضع العناصر الداخلية في `x=0, y=0`، وتعيين الـ `ref` ومقابض السحب والتحويل للمجموعة نفسها لضمان تحركها ككتلة ذرية واحدة دون انفصال.
2. **عزل إحداثيات الأنيميشن الداخلية:** أي أنيميشن (مثل مؤشر المسح الذكي `MagicAiScanner`) موضوع داخل `<Group>` ذي موضع `(x, y)` يجب أن يظل عند الإحداثيات المحلية `(0, 0)` لمنع الإزاحة المزدوجة (Double Offset).
3. **سحب المجموعات المجمعة:** عند تجميع عدة عناصر في `groupId`، تعامل مع المجموعة ككتلة ذرية واحدة (`Composite Bounding Box`)؛ تُزاح جميع العناصر بنفس المقدار النسبي `(dx, dy)` دون تطبيق المحاذاة على كل عنصر فرعي لمنع التداخل أو قفز العناصر.
4. **أبعاد المجموعات القابلة للتحجيم:** عقد `<Group>` لا تستنتج أبعادها تلقائياً للمحولات (Transformers)؛ مرّر `width` و`height` صراحةً، وفي `onTransformEnd` استخدم المسار الاحتياطي `(typeof node.width === "function" && node.width() > 0) ? node.width() : el.width * canvasWidth`.

---

## ✍️ 5. النصوص والتايبوغرافي (Text & Typography)

1. **فلتر التمدد الشكلي للحدود (Morphological Stroke Dilation):** يُمنع إجبارياً استخدام `ctx.strokeText` أو خاصية `stroke` الافتراضية في Konva للنصوص المتصلة (العربية أو Cursive)؛ رسم الحدود لكل حرف (Glyph) يولّد أوتاداً حادة تقطع وصلات الحروف. الحل المعتمد هو فلتر التمدد الشكلي الموحد على قناة الشفافية المجمعة (`SourceAlpha` عبر `ensureTextStrokeFilter`) للكلمة ككتلة واحدة، في الكانفاس المباشر وعند التصدير.
2. **الالتفاف التلقائي بدون تذبذب:** استخدم دائماً `wrap="word"` مع `ellipsis={false}` (ويُمنع `wrap="none"` + `ellipsis={true}` الذي يقطع النصوص بنقاط حذف)، ومرّر `width` فقط بدون فرض `height` مسبق.
3. **منع useState للأبعاد التلقائية:** لا تخزّن ارتفاع النص المحسوب في `useState` محلي (دورات render مزدوجة وتذبذب بصري)؛ اقرأ `node.height()` داخل `useEffect` وادفع القيمة المُطبَّعة مباشرة للـ Store عبر `onChange({ height: pixelHeight / canvasHeight })`.

---

## 🎨 6. تجميع المسارات والسياق (Path Batching & Canvas Context)

1. **التجميع في المسارات:** عند رسم شبكات أو خطوط متكررة، اجمع المسارات في `beginPath()` واحد واستدعِ `fill()`/`stroke()` مرة واحدة خارج التكرار. يُمنع الاستدعاء داخل الحلقة. (نصيحة: استخدم `context.moveTo(x + r, y)` قبل كل `arc` لكسر المسار يدوياً ورسم آلاف النقاط بـ fill واحد).
2. **تثبيت خصائص السياق خارج الحلقة:** لا تغيّر `globalAlpha` أو `fillStyle` داخل الحلقة بدون ضرورة.
3. **سياق القراءة المتكرر:** عند إنشاء سياق 2D مخصص للمعالجة البكسلية استخدم `{ willReadFrequently: true }`، واستخدم `OffscreenCanvas` في الـ Worker Threads للحسابات الثقيلة (الألوان السائدة، المسح الذكي).

---

## 📐 7. هندسة الكولاج الحر (Freeform Collage Engine)

### 7.1 كاشف العوائق الاتجاهي (Directional Obstacle Detection)
يُمنع استخدام خوارزميات دفع عامة لا تميز اتجاه الحركة (`pushOutOfOthers`). الحركة نمطان صارمان:

1. **الانكماش والتصغير (Shrinking):** يتحرك الضلع بسلاسة تامة ومستمرة وفق إحداثيات الفأرة **دون أي فحص للعقبات** (التصغير يبتعد بطبيعته عن الحواف ولا يخلق اصطداماً جديداً)، مع الالتزام بحد `MIN_SIZE` ومحيط الورقة فقط.
2. **التوسيع والتمدد (Expanding):** تفحص الخوارزمية حصراً العقبات الواقعة في مسار الضلع المتوسع (أمام الحافة مباشرة مع تقاطع على المحور العمودي)، ويتوقف التمدد بسلاسة عند ملامسة حافة الجار دون قفز أو تراجع قسري.

```typescript
export function computeDirectionalBounds(
  activeSlot: CollageSlot,
  handle: 'top' | 'bottom' | 'left' | 'right',
  delta: number,
  allSlots: CollageSlot[],
  minSize: number = 0.05
): number {
  if (delta < 0) {
    // تصغير سلس ومستمر
    return Math.max(activeSlot.size + delta, minSize);
  }

  // توسيع: كشف أقرب عقبة في المسار المباشر فقط
  const obstacleDistance = findNearestObstacleInDirection(activeSlot, handle, allSlots);
  return Math.min(activeSlot.size + delta, obstacleDistance);
}
```

### 7.2 التقسيم الذكي للحاويات (Binary Space Partitioning)
عند تقسيم خلية إلى خليتين (أفقياً أو رأسياً):
- احتفظ بالهوية الذرية للخلية الأصلية وقسّم المساحة النسبية بدقة `(0.0 - 1.0)`.
- احسب إزاحة التباعد (Gap Spacing) كإزاحة بكسلية حقيقية مستقلة عن عامل التكبير.
- تأكد أن مجموع أبعاد الخلايا الناتجة مع الفواصل يطابق أبعاد الحاوية الأصلية 100% لمنع تسريب البكسلات الفرعية.

---

## 🖨️ 8. تصدير الفلاتر بدقة عالية (High-DPI Filter Export)

1. ارتقِ بدقة كاش كافة العناصر المفلترة (`node.isCached()`) إلى `pixelRatio: Math.max(1, targetPixelRatio)` قبل التقاط `stage.toCanvas()`.
2. استعد كاش الشاشة الأصلي الخفيف (`restoreScreenCache` / `pixelRatio: 1`) في كتلة `finally` لحماية VRAM أثناء التحرير.

---

## 🔍 9. قائمة فحص سريعة عند تعديل أي مكون Konva

- [ ] هل `useEffect` الكاش يراقب إحداثيات سحب أو زوم؟ → اعزلها فوراً.
- [ ] هل يوجد `FastLayer`؟ → استبدلها بـ `<Layer listening={false}>`.
- [ ] هل الزوم يقرأ من الـ Store داخل الـ Wheel؟ → جمّع الهدف محلياً بالتقريب الأسي.
- [ ] هل خلفية/قناع خارج `<Group>` ذري؟ → غلّفهما مع `x=0, y=0` داخلياً.
- [ ] هل نص يستخدم `wrap="none"` أو `useState` لارتفاع محسوب؟ → طبّق قواعد القسم 5.
- [ ] هل `fill()/stroke()` داخل حلقة؟ → جمّع المسارات في مسار واحد.
- [ ] هل تحجيم خلية كولاج يفحص العقبات عند التصغير؟ → أزل الفحص (القسم 7.1).
- [ ] هل السحب يعيد رسم الطبقة كاملة كل إطار؟ → ارفع للطبقة المخصصة (القسم 10.1).
- [ ] هل شكل بحدود وظل يدفع الرسم المضاعف؟ → عطّل ظل الحدود (القسم 10.2).
- [ ] هل صورة ضخمة تُرفع للـ GPU بأبعادها الكاملة؟ → نسخة العرض المخفّضة (القسم 10.3).
- [ ] هل تُحدَّث عقد المحوّل/صندوقه أثناء إيماءة نشطة؟ → احترم حاجز `isTransforming()/isDragging()` (القسم 10.7).
- [ ] هل مكوّن أنيميشن يقرأ إحداثيات أبيه؟ → أبقِه محلياً عند (0,0) واحسم الرسم بـ `batchDraw`.

---

## 🚀 10. دروس جلسة الصواريخ (2026-09-24 — مراجعة التوثيق الرسمي الكاملة)

> المصدر الحاكم: [All Performance Tips](https://konvajs.org/docs/performance/All_Performance_Tips.html) + [Optimize Strokes](https://konvajs.org/docs/performance/Optimize_Strokes.html). عند أي تعارض بين عادة محلية ونص رسمي، **النص الرسمي يفوز** — لا لفائف التفافية ولا اختراع عجلات.

### 10.1 طبقة السحب الرسمية (Official Drag-Layer — تُطبَّق حرفياً)
- نص المحرك: *«move the shape to a dedicated layer while dragging, then move it back at drag end»*. التنفيذ المعتمد في `frontend/src/components/editor/konva/drag-layer.ts`:
  - `liftToDragLayer(nodes)` عند `onDragStart` (العقد المحددة + المحوّل + شارة الأبعاد عبر `registerDragExtrasProvider`)، و`dropFromDragLayer()` **أولاً** في `onDragEnd` قبل أي كتابة للـ store (فلا ترى React العقد خارج موضعها ولو لإطار واحد).
  - حفظ `{parent, zIndex}` لكل عقدة واستعادته بدقة؛ عقد بلا مسرح (unmount أثناء السحب) تُتجاهل ولا تُعاد إضافتها أبداً.
  - جذر الرفع: عقدة العنصر في الوضع الحر، ومجموعة الخانة `slot-<id>` في الكولاج (القص clip يسافر معها) — الدالة `findLiftRoot`.
  - شرط السلامة: كل الطبقات بلا تحويلات (identity) فالإحداثيات المطلقة محفوظة عند النقل.
  - ميزانية الطبقات الرسمية: كل طبقة = كانفاسان (مشهد + إصابة، ~41MB على شاشة ريتنا 1080p) والتحذير فوق 5 طبقات — Grido يعمل بحد أقصى 3 (خلفية + محتوى + سحب) فهو ضمن الحد الآمن.
  - ملاحظة Hit: `Konva.hitOnDragEnabled = false` افتراضياً (لا كشف إصابة أثناء السحب) — لا تفعّله إلا لتظليل هدف إسقاط مع قبول التكلفة.

### 10.2 ظل الحدود مطفأ دائماً (Official Optimize Strokes)
- الشكل ذو الحدود والظل معاً يدفع رسماً داخلياً مضاعفاً. القاعدة: `shadowForStrokeEnabled={false}` على كل الأشكال والنصوص ذات الحدود — يبقى ظل التعبئة ويلغى الرسم الزائد. لا استثناء إلا بتوثيق بصري مقصود.

### 10.3 نسخة العرض المخفّضة (Display Proxy — سقف 2048px)
- صور الاستيراد الكبيرة تُرسم للتحرير عبر `getDisplayImage` في `frontend/src/lib/canvas/display-image.ts` (كانفاس بسقف 2048px + كاش LRU بسقف 24 عنصراً، متزامن عبر `useMemo`).
- **خط أحمر:** التصدير والطباعة والذكاء الاصطناعي تبقى على المصدر الكامل دائماً — النسخة للعرض فقط. `imageSrc` نفسه لا يُقلَّص أبداً عند الاستيراد لهذا السبب.

### 10.4 السحب بلا ظلال (Shadowless Drag)
- تمريرة الضبابية خارج الشاشة أغلى عملية/إطار: تُطفأ الظلال أمرياً (`shadowEnabled(false)` عبر `drag-shadow.ts`) أثناء السحب وتُعاد بعده، مع صمام `pointerup/blur` لأن Konva قد لا يطلق `dragend` خارج النافذة. ظل الرفع الجمالي مخفّض (blur 8) لا ملغى.

### 10.5 تمريرة سناب واحدة + شارة مخنوقة + مزامنة ذكية
- `dragBoundFunc` تحسب الموضع والأدلة معاً و`onDragMove` يستهلك الأدلة من `ref` (لا إعادة حساب).
- شارة المحوّل تُحدَّث بحد أقصى إطار واحد (rAF) مع تحديث إجباري عند الاستقرار؛ مزامنة `nodes()` تُتخطى عند ثبات الهويات.
- فواصل الكولاج بنمط معاينة/تثبيت (`previewCollageGap/Margin` أثناء السحب + `commitCollageSpacing` عند الإفلات)؛ معاينة الطباعة تُولَّد عند الفتح فقط.

### 10.6 مرفوضات موثقة (لا تعيد اكتشافها)
- ❌ خفض `pixelRatio` أثناء السحب: يعيد تخصيص VRAM ويمسح السياق (القسم 1.3) — البديل المعتمد: إطفاء الظلال/الفلاتر أثناء الحركة.
- ❌ `listening={false}` على عقد قابلة للتحديد: يكسر النقر للتحديد — يُطبَّق على الزخارف والطبقات فقط.
- ❌ تغيير `strokeScaleEnabled`: يغيّر المظهر البصري — ليس تحسيناً محايداً.

### 10.7 صندوق المحوّل عند ثبات العقد + رسم الأنيميشن (2026-09-24)
- **حاجز الإيماءة إلزامي عند تحديث صندوق المحوّل:** في `Transformer.setNodes` يشترك المحوّل في أحداث `width/height/scale/rotation/offset…Change` و`absoluteTransformChange` و`_attrsAffectingSize`، ثم يستدعي `_resetTransformCache()` + `update()` **بشرط `!this._transforming && !this.isDragging()`** — لأن تحريك المقابض تحت المؤشر في منتصف الإيماءة عيب بصري. وأي استدعاء يدوي لـ `forceUpdate()` **يتخطى هذا الحاجز**، فيجب لفظه بـ `if (!tr.isTransforming() && !tr.isDragging())` (التنفيذ: `konva-canvas.tsx` في مسار `sameNodes`). تغيّر أبعاد/موضع العقدة المربوطة يلتقطه Konva ذاتياً، والاستدعاء اليدوي شبكة أمان للهندسة التي تتغيّر من الأبناء بلا حدث على العقدة نفسها — والعقد مقفول في `e2e/transformer-box-follows-geometry.spec.ts` (Alt+ArrowUp تكبير وArrowRight إزاحة من المتجر مع ثبات هوية العقد).
- **أبعاد محوّل المجموعات تُثبَّت على العقدة عند `onTransformEnd`:** بعد تصفير `scaleX/scaleY` يُكتب `node.width(newW)` (و`node.height` لكل نوع عدا النص)، لأن مجموعات Konva لا تستنتج مقاسها للمحوّل وشارة الأبعاد تقرأ `node.width() * scaleX()`. والنص يُستثنى من الارتفاع ليبقى محسوباً من الالتفاف (القسم 5).
- **`Konva.Animation` بلا وسيط `layers` لا يرسم شيئاً:** `Animation._runFrames` يرسم الطبقات الممرّرة فقط (`layer.batchDraw()` لكل طبقة في القائمة)؛ فمكوّن أنيميشن يمرّر `undefined` يجب أن يرسم بنفسه داخل الكولباك (`group.getLayer()?.batchDraw()`)، وهي تتبع العقدة عند رفعها إلى طبقة السحب. وينطبق عليه ثابت «عزل إحداثيات الأنيميشن»: يبقى محلياً عند (0,0) ولا يقرأ إحداثيات أب. وأي واجهة عامة لمكوّن الأنيميشن لا يستدعيها مكوّن إنتاجي تُزال (الدليل: `magic-ai-scanner.tsx` بعد إزالة `targetNodeRef`).
- **لا تُخترع واجهات احتياطية بلا مستدعٍ:** إبقاء خاصية «مزامنة مع عقدة شقيقة» في مكوّن الأنيميشن بعد أن أصبح ابن مجموعة العنصر = كود ميت يُخفي الثابت ويوهم بسلوك غير قائم؛ الحذف هو الإصلاح لا التوثيق.
