# הטמעת התצוגה התלת־ממדית

> הטמיעו את התצוגה התלת־ממדית של Nodaro באפליקציה שלכם ושלטו בה עם postMessage: לחיצת יד, הודעות מצב, אירועי עריכה, העברת קבצים ומגבלות.

Source: https://nodaro.ai/he/docs/developers/embed/scene3d

**התצוגה התלת־ממדית** היא הכלי של Nodaro לצפייה בהדמיות מוקדמות בתלת־ממד, ואפשר להטמיע אותה ב-iframe באפליקציה שלכם, בכתובת `/embed/scene3d`. זו אותה תצוגה שהעורך של Nodaro משתמש בה: אזור תצוגה של three.js, בקרות ניגון, רשימת אובייקטים, עורכי תנוחה מספריים והיסטוריית גרסאות. שולטים ב-iframe אך ורק דרך `postMessage`: הדף שלכם שומר את הסצנה, וה-iframe מציג אותה ומדווח מה המשתמש עשה.

זו לא הטמעה של מיני־אפליקציה. מיני־אפליקציה מריצה תהליך שפורסם; התצוגה התלת־ממדית לא מריצה כלום. ראו את הדף [הטמעות](https://nodaro.ai/docs/developers/embed) להבדל בין השתיים, ואת הדף [פורמט תוכנית הסצנה](https://nodaro.ai/docs/developers/embed/scene3d-format) לנתונים שהתצוגה מציגה.

## מה ה-iframe עושה ומה לא
| | |
| --- | --- |
| **קורא** | רק את `parentOrigin` ואת `channel` שבכתובת שלו, והודעות `state` מאותו origin בדיוק. |
| **מציג** | את הסצנה שאתם שולחים: אזור התצוגה, ניגון וגלילה בציר הזמן, רשימת האובייקטים, עורכי התנוחה של כל אובייקט ושל המצלמה, היסטוריית הגרסאות וההודעה על גרסה ממתינה. בסצנה אפויה (בגרסת פורמט 2): המצלמה האפויה, ניווט בין שוטים, הישויות הסמנטיות עם צבעי הזיהוי שלהן והבקרות של שכבות־העל. |
| **שולח** | `ready` ברגע שהוא מאזין, ואחר כך `event` אחד לכל פעולה של המשתמש ו-`asset-request` אחד לכל קובץ שסצנה בגרסת פורמט 2 מצהירה עליו. |
| **אף פעם לא** | מבצע אימות, קורא מהאחסון או כותב אליו, שולח בקשה לנקודת קצה של `/v1`, שומר משהו אחרי טעינה מחדש או מתקדם בעצמו לגרסה החדשה אחרי עריכה. |

ה-iframe **לא שומר מצב, וזה במכוון**. כשהמשתמש מסיים עריכה, ה-iframe שולח לכם גרסה חדשה שאי אפשר לשנות, וממשיך להציג את הגרסה הישנה עד ש*אתם* שולחים את התוצאה בחזרה. אם ה-iframe היה מקדים את הדף המארח, הוא היה עלול להציג גרסה שאף פעם לא תישמר.

אין ב-iframe שום סוד, ואפשר לטעון אותו בבטחה גם בדף שאין לכם שליטה מלאה עליו. רמת הפרטיות של נתוני הסצנה שאתם שולחים אליו נקבעת לפי ה-origin שאליו אתם שולחים אותם, ולכן תמיד שלחו ל-origin מדויק; ראו את הסעיף [רשימת בדיקות לדף המארח](#parent-checklist).

## בדיקת הזמינות של התצוגה התלת־ממדית
בפריסה שכוללת את התצוגה התלת־ממדית, רכיב [יצירת סצנת תלת־ממד](https://nodaro.ai/docs/nodes/video/generate-3d-scene) מציין את היכולת `scene3d-embed-v1` בתשובה של `GET /v1/nodes`. בדקו את היכולת הזו לפני שאתם מציעים את העורך המוטמע.

## הכתובת
```text
https://app.nodaro.ai/embed/scene3d?parentOrigin=<origin>&channel=<uuid>
```

| פרמטר | נדרש | כלל |
| --- | --- | --- |
| `parentOrigin` | כן | ה-origin המדויק והמנורמל של הדף המארח: פרוטוקול, שם מארח ופורט אופציונלי, ותו לא. הערך של `new URL(value).origin` חייב להיות שווה לערך עצמו. רק `http:` ו-`https:`, עד 255 תווים. לוכסן בסוף, נתיב, מחרוזת שאילתה, פרטי משתמש, `null` או כל פרוטוקול אחר נדחים. |
| `channel` | כן | מזהה UUID אקראי וחדש מ-`crypto.randomUUID()`, אחד לכל iframe שנמצא בדף. הוא מונע מצב שבו שתי תצוגות תלת־ממדיות באותו דף, או iframe ישן של תיבת דו־שיח שנסגרה, משבשים זה את התצוגה של זה. |

הדפדפן מנרמל את `MessageEvent.origin` לפני שהוא מגיע. הדרישה שהפרמטר ייכתב באותו אופן בדיוק מאפשרת ל-iframe להשוות בין השניים במדויק, בלי לנחש. אם פרמטר חסר או פגום, ה-iframe מציג שגיאה, לא שולח כלום ולא מאזין לשום דבר.

## לחיצת היד
1. הדף שלכם מוסיף את ה-iframe עם הכתובת שלו.
2. ה-iframe נטען ומתחיל להאזין. הוא שולח `nodaro:scene3d:ready`, ושולח אותו שוב ושוב עד שאתם עונים.
3. הדף שלכם עונה בהודעת `nodaro:scene3d:state`. ה-iframe בודק את תקינות הסצנה ומציג אותה.
4. כשהמשתמש מבצע פעולה, ה-iframe שולח `nodaro:scene3d:event`.
5. הדף שלכם בודק את `expectedRevisionId`, מחיל את האירוע ושולח `state` חדש, שהוא מקור האמת החדש.

**שלחו את ה-`state` הראשון כתשובה ל-`ready`, לא באירוע `load` של ה-iframe.** האירוע `load` מתרחש לפני שהמאזין של ה-iframe מוכן, ולכן הודעה שנשלחת כשהוא מתרחש עלולה ללכת לאיבוד. ה-iframe שולח שוב את `ready` לפי לוח זמנים קצר ומוגבל, וכך גם דף מארח שמתחיל להאזין קצת באיחור לא מפספס אותו. השליחה החוזרת נעצרת אחרי ההודעה הראשונה שלכם שמיועדת ל-iframe, גם אם הוא דוחה אותה: גם דחייה מוכיחה שאתם מאזינים.

## מהדף המארח ל-iframe: הודעת state
יש סוג הודעה אחד, והוא מעביר תמונת מצב מלאה. אין עדכון חלקי: תמיד שלחו את כל המצב שאתם רוצים שיוצג.

```ts
{
type: "nodaro:scene3d:state",
version: 1 | 2,                     // 2 enables baked (version 2) scenes, see below
channel: string,                    // must equal the channel in the URL
scenePlan: Scene3DPlan,             // required, validated
selectedObjectIds?: string[],       // default []
lockedObjectIds?: string[],         // default []
history?: Scene3DRevisionEntry[],   // default []
pendingPlan?: Scene3DPlan,          // default absent
isGenerating?: boolean,             // default false
readOnly?: boolean,                 // default TRUE
}
```

רשומה בהיסטוריה:

```ts
{
revisionId: string,                     // UUID
scenePlan: Scene3DPlan,                 // validated like the active plan
source: "generate" | "edit" | "manual" | "upstream",
createdAt: string,                      // your timestamp, shown as it is
changeSummary?: string,
context?: { prompt?: string },          // only prompt is kept
}
```

השדה `scenePlan` הוא הטיפוס הציבורי `Scene3DPlan` מהחבילה `@nodaro/shared`: אותו מבנה שמשימה של רכיב יצירת סצנת תלת־ממד או של רכיב עריכת סצנת תלת־ממד מחזירה ב-`output_data.scenePlan`.

- **ברירת המחדל של `readOnly` היא `true`.** כשלא מציינים כלום, אפשר להסתכל ואסור לגעת. כדי שאפשר יהיה לערוך ב-iframe, ההודעה חייבת לכלול `readOnly: false`.
- **ב-`context` נשמר רק `prompt`,** שהופך לטקסט שמופיע כשמרחפים מעל כפתור השחזור. מפתחות אחרים בתוך `context` מושמטים בשקט. מפתחות לא מוכרים בכל מקום אחר נדחים; ראו את הסעיף [גרסאות הפרוטוקול](#versioning).
- **הערך `isGenerating: true`** רק מוסיף הערה שגרסה חדשה נוצרת כעת. הסצנה נשארת פעילה, וכשהיא לא במצב צפייה בלבד, אפשר גם לערוך אותה.
- **השדה `pendingPlan`** מטפל במקרה שבו משימה הסתיימה אחרי שהמשתמש ערך את הסצנה. ה-iframe מציג הודעה, וכשאפשר לערוך, גם שני כפתורים לבחירת גרסה.

### ממה ה-iframe מתעלם ומה הוא דוחה
| ה-iframe מקבל | מה הוא עושה |
| --- | --- |
| הודעה שה-`event.source` שלה הוא לא החלון של הדף המארח | מתעלם ממנה בשקט. |
| הודעה שה-`event.origin` שלה הוא לא `parentOrigin` | מתעלם ממנה בשקט. |
| הודעה עם `type` אחר או `channel` אחר, או הודעה שהמטען שלה הוא לא אובייקט | מתעלם ממנה בשקט. |
| הודעה עם `version` לא מוכר | דוחה אותה באופן גלוי. |
| הודעה עם מעטפת פגומה, מפתח לא מוכר או רשימה שחורגת מהמגבלה שלה | דוחה אותה באופן גלוי. |
| הודעה שבה `scenePlan`, `pendingPlan` או אחד מה-`history[].scenePlan` לא עוברים את בדיקת התקינות | דוחה אותה באופן גלוי. |

ההתעלמות שקטה, כי התעבורה של iframe אחר היא לא הבעיה של המשתמש. הדחייה גלויה, כי ההודעה *שלכם* הייתה שגויה. **דחייה אף פעם לא מוחקת נתונים שכבר התקבלו:** תמונת המצב האחרונה שהתקבלה נשארת על המסך מתחת לבאנר שמציין את הסיבה, וה-`state` התקין הבא מסיר את הבאנר.

## מה-iframe לדף המארח: ההודעות ready ו-event
ה-iframe שולח את שתי ההודעות ל-`parentOrigin` המדויק שבכתובת שלו, אף פעם לא ל-`"*"`.

```ts
{
type: "nodaro:scene3d:ready",
version: 1,                              // the baseline, always 1
channel: string,
protocolVersions: [1, 2],                // every version this frame accepts
capabilities: {
assetTransport: true,                  // it can ask you for asset bytes
sceneSchemaVersions: [1, 2],           // the scene versions it can draw
},
}
```

**זהו את `ready` לפי `type` ו-`channel`, ובדקו את `protocolVersions`.** אל תשוו את ההודעה כולה. הערך של `version` נשאר `1`, כך שדף מארח שנכתב לגרסת פרוטוקול 1 ממשיך לעבוד; כל מה שגרסת פרוטוקול 2 מוסיפה מופיע בשדות שדף מארח כזה לא קורא.

```ts
{
type: "nodaro:scene3d:event",
version: 1 | 2,                     // 2 ONLY for edit-operations
channel: string,
expectedRevisionId: string | null,
event:
| { kind: "plan", plan: Scene3DPlan, changeSummary: string }
| { kind: "selection", objectIds: string[] }
| { kind: "locks", objectIds: string[] }
| { kind: "restore", revisionId: string }
| { kind: "resolve-pending", adopt: boolean }
// version 2 scenes only, see "Baked scenes" below
| { kind: "edit-operations", operations: Scene3DV2EditOperation[], expectedContentHash: string },
}
```

במעטפת מופיע `version: 2` רק באירועי `edit-operations`. בכל סוג אירוע אחר עדיין מופיע `1`, כך שדף מארח של גרסת פרוטוקול 1 אף פעם לא נתקל בגרסת פרוטוקול לא מוכרת בהודעה שהוא מבין.

**השדה `expectedRevisionId` הוא מזהה הגרסה שה-iframe הציג כשהמשתמש פעל,** כלומר הבסיס שממנו הפעולה חושבה. החילו את האירוע רק אם הערך הזה עדיין שווה ל-`scenePlan.revisionId` הפעיל שלכם, ואחרת התעלמו מהאירוע. הבדיקה האחת הזו הופכת לחיצה על תמונת מצב שכבר החלפתם לפעולה שלא עושה כלום, במקום לחזרה שקטה לגרסה קודמת. הערך הוא `null` רק לפני שה-iframe קיבל `state` תקין כלשהו.

| `kind` | משמעות | מה עושים |
| --- | --- | --- |
| `plan` | עריכה מקומית, כגון שינוי מספרי של תנוחה או שינוי צבע, יצרה גרסה חדשה שאי אפשר לשנות. הערך של `plan.parentRevisionId` הוא `expectedRevisionId`. שום מודל לא רץ ולא נגבו קרדיטים. | הפכו את `plan` לגרסה הפעילה, הוסיפו אותה להיסטוריה ושלחו `state` חדש. |
| `selection` | המשתמש בחר אובייקטים או ביטל את הבחירה בהם. | עדכנו את הבחירה גם אצלכם, ושלחו `state` או רק שמרו אותה. |
| `locks` | המשתמש נעל אובייקטים או ביטל את הנעילה שלהם. אובייקט נעול הוא אובייקט שמשימת עריכה חייבת להשאיר זהה בייט־לבייט. | שמרו את הנעילות ושלחו `state`. |
| `restore` | המשתמש ביקש גרסה קודמת מההיסטוריה ששלחתם. | הפכו אותה לגרסה הפעילה ושלחו `state`. |
| `resolve-pending` | הערך `adopt: true` בוחר ב-`pendingPlan`; הערך `false` משאיר את התוכנית הנוכחית. | יישמו את הבחירה ושלחו `state` בלי `pendingPlan`. |
| `edit-operations` | עריכה של שכבת־על בגרסת פורמט 2, בצורת פעולות. ה-iframe לא מחיל אותה. | החילו אותה עם פונקציית ההחלה המשותפת, שמרו את התוצאה ושלחו בחזרה את התוכנית השמורה. ראו את הסעיף [עריכת סצנה אפויה](#edit-a-baked-scene). |

## מצב צפייה בלבד
השדה `readOnly`, שברירת המחדל שלו היא `true`, משאיר את כל התצוגה זמינה וחוסם כל פעולת כתיבה:

| עדיין עובד | חסום |
| --- | --- |
| ניגון, השהיה וגלילה בציר הזמן | שמירת ערכי תנוחה מספריים, לאובייקטים ולמצלמה |
| בחירה באזור התצוגה וברשימה | צבעי האובייקטים והרקע |
| קריאת רשימת האובייקטים, תגי הנעילה והיסטוריית הגרסאות | מתגי הנעילה |
| ההודעה על גרסה ממתינה | שחזור, והכפתורים של הגרסה הממתינה |

האירוע היחיד ש-iframe במצב צפייה בלבד שולח הוא `selection`. הכלל חל משני הכיוונים: הבקרות שהיו משנות את הסצנה מושבתות או מוסתרות, וה-iframe מסרב לשלוח שינוי גם אם מפעילים בקרה ישירות. מצב צפייה בלבד הוא תכונה של ה-iframe, לא עניין של עיצוב.

## סצנות אפויות ומנגנון העברת הקבצים
סצנה ב**גרסת פורמט 1** שלמה בפני עצמה: צורות פשוטות, פריימי מפתח ומצלמה, והכול בתוך התוכנית שאתם שולחים. סצנה ב**גרסת פורמט 2** היא גאומטריה אפויה. במניפסט שלה מופיעים ישויות סמנטיות, שוטים, מסלול מצלמה אפוי ורשימה של מזהי קבצים עם גיבובי SHA-256, ואילו הבייטים עצמם נשארים מאחורי ה-API של Nodaro, שדורש אימות.

גם כאן אין ל-iframe סשן, והוא לא שולח שום בקשת רשת. במקום זאת הוא מבקש *מכם* בדיוק את הקבצים שהמניפסט מצהיר עליהם, ואתם מורידים אותם עם הסשן שלכם.

### הפעלת גרסת פרוטוקול 2
שלחו `version: 2` בהודעת ה-`state` שלכם. סצנה בגרסת פורמט 2 בהודעה עם `version: 1` נדחית עם השגיאה `scenePlan — this scene uses schema version 2, which needs embed protocol version 2 (asset transport)` (הסצנה הזו בגרסת פורמט 2, שדורשת את גרסת פרוטוקול ההטמעה 2, עם העברת קבצים). הסיבה: לדף מארח של גרסת פרוטוקול 1 אין קוד שמטפל בבקשות קבצים, וה-iframe היה ממתין לנצח. סצנות בגרסת פורמט 1 עובדות עם כל אחת משתי גרסאות הפרוטוקול.

### ההודעות
```ts
// frame to parent
{
type: "nodaro:scene3d:asset-request",
version: 2,
channel: string,
requestId: string,                 // new for each request; send it back unchanged
revisionId: string,                // plan.revisionId
assetId: string,                   // an opaque id from plan.assets
kind: "glb" | "camera-track-json",
byteLength: number,                // what the manifest declares
sha256: string,                    // 64 lower-case hex characters
}

// parent to frame, success
{
type: "nodaro:scene3d:asset-response",
version: 2,
channel: string,
requestId: string,                 // echoed
revisionId: string,                // echoed
assetId: string,                   // echoed
ok: true,
bytes: ArrayBuffer,                // a structured clone: NOT a URL, NOT base64
}

// parent to frame, failure
{ /* same envelope */ ok: false, error: "short reason" }
```

השדה `revisionId` הוא מזהה הגרסה השמורה הנוכחית של התוכנית. כל גרסה שמורה מקבעת את כל הקבצים שלה, כולל בייטים שהיא משתמשת בהם שוב מגרסה קודמת.

### מה ה-iframe בודק
ה-iframe מתעלם בשקט מתעבורה רגילה שלא מיועדת לו:

- תשובה מחלון אחר, מ-origin אחר או עם `channel` אחר.
- תשובה עם `requestId` של בקשה שכבר לא ממתינה לתשובה, כגון תשובה מאוחרת אחרי פסק זמן, תשובה כפולה או בייטים שאף אחד לא ביקש.

במקרים האלה הטעינה של הקובץ הזה בלבד נכשלת, והכשל גלוי:

- הערך של `version` הוא לא `2`, או שיש שדה לא מוכר או ששדה חסר.
- הערך של `revisionId` או של `assetId` הוא לא זה שהתבקש.
- התשובה היא `ok: false`. ה-iframe מציג את ה-`error` שלכם בצורה מקוצרת.
- השדה `bytes` הוא לא `ArrayBuffer`.
- האורך שונה מה-`byteLength` שבמניפסט.
- **גיבוב ה-SHA-256 שונה מהגיבוב שבמניפסט.**

בדיקת הגיבוב היא הבדיקה החשובה: היא מבדילה בין ״המארח מסר בייטים כלשהם״ לבין ״אלה הבייטים שהגרסה הזו בנויה מהם״. מנוע הרינדור בודק את הגיבוב פעם שנייה לפני הפענוח, כך שאף נתיב בקוד לא מציג גאומטריה שלא נבדקה.

ה-iframe גם מגביל את עצמו:

- לכל היותר **4 בקשות נמצאות בטיפול בו־זמנית**, והשאר ממתינות בתור.
- לכל גרסה מותרות לכל היותר **64 בקשות** ו-**64 MiB**.
- כל בקשה צריכה לקבל תשובה בתוך **20 שניות**.
- כל בקשה שעדיין ממתינה לתשובה נדחית כשמגיעה גרסה חדשה או כשה-iframe מוסר מהדף, כך שתשובה לסצנה שהמשתמש כבר עזב אף פעם לא מוצגת.
- קבצים זהים, עם אותו מזהה ואותו גיבוב, מתבקשים פעם אחת בלבד, כך שגרסה שנוצרה מעריכה של שכבת־על ומשתמשת מחדש בקובץ GLB לא מורידה אותו פעם נוספת.

### כללים לדף שלכם
1. **אשרו בקשות רק לפי מה ששלחתם.** ענו רק כש-`request.revisionId` הוא הגרסה ששלחתם (`plan.revisionId`) וכש-`request.assetId` מופיע ב-`assets` של אותה תוכנית עם אותם `byteLength` ו-`sha256`. אחרת, ענו `ok: false`. אם תדלגו על הבדיקה הזו, הסשן שלכם יהפוך לאורקל: מי ששולט בדף שבתוך ה-iframe יוכל לבקש כל גרסה או כל קובץ ולקרוא את התשובה.
2. **אף פעם אל תשלחו פרטי גישה.** לא אסימון, לא cookie ולא כתובת URL חתומה: כתובת URL שנותנת גישה היא בסך הכול אסימון Bearer בצורה אחרת. שלחו בייטים.
3. **שלחו ל-origin המדויק של ה-iframe,** אף פעם לא ל-`"*"`.
4. **אל תוסיפו ל-transfer list באפר שאתם שומרים אצלכם.** שליחה רגילה, כ-structured clone, מעתיקה את הבאפר; העברה ב-transfer list הייתה לוקחת אותו מכם.
5. **התעלמו מבקשות לגרסה שכבר החלפתם,** ולכל צירוף של גרסה וקובץ אל תשאירו בטיפול יותר מהורדה אחת.

```ts

const client = createClient({ baseUrl: "https://app.nodaro.ai", auth: myAuth })

/** The plan you sent most recently. */
let active: Scene3DPlanV2

window.addEventListener("message", async (event) => {
if (event.source !== iframe.contentWindow) return
if (event.origin !== NODARO_ORIGIN) return
const data = event.data
if (data?.type !== "nodaro:scene3d:asset-request") return
if (data.channel !== channel || data.version !== 2) return

const reply = (body: Record<string, unknown>) =>
iframe.contentWindow?.postMessage(
{
type: "nodaro:scene3d:asset-response",
version: 2,
channel,
requestId: data.requestId,
revisionId: data.revisionId,
assetId: data.assetId,
...body,
},
NODARO_ORIGIN,
)

// 1. Authorize: the asset must belong to the plan we sent, and the request
//    must name the revision that plan pins the bytes to.
const asset = active.assets.find((a) => a.assetId === data.assetId)
const pinnedTo = active.revisionId
if (
!asset ||
data.revisionId !== pinnedTo ||
asset.byteLength !== data.byteLength ||
asset.sha256 !== data.sha256
) {
reply({ ok: false, error: "unknown asset" })
return
}

// 2. Fetch with OUR session, and send the bytes, never the URL or the token.
try {
const bytes = await client.scene3d.assetBytes(pinnedTo, asset)
reply({ ok: true, bytes })
} catch (error) {
reply({ ok: false, error: error instanceof Error ? error.message : "fetch failed" })
}
})
```

### עריכת סצנה אפויה
עריכות בגרסת פורמט 2 הן **שכבות־על**, וה-iframe אף פעם לא מחיל אחת מהן בעצמו. גרסה חדשה כזו, שהשרת שלכם לא שמר, הייתה נראית שמורה, בזמן שהקבצים שלה עדיין שייכים לגרסה שממנה היא נוצרה. לכן ה-iframe שולח את הפעולות וממשיך להציג את הגרסה ששלחתם:

```ts
// event.event
{
kind: "edit-operations",
operations: [
{ op: "set-override", override: { kind: "entity-transform", entityId: "hero", space: "local", position: [3, 0.5, 0] } },
],
expectedContentHash: "<64 hex characters>",   // plan.provenance.contentHash
}
```

העבירו לפונקציית ההחלה המשותפת, אותה פונקציה שה-API משתמש בה, את שלושת הערכים שבודקים שהגרסה עדכנית, ואז שמרו את התוצאה ושלחו אותה בחזרה:

```ts

const result = await applyScene3DV2EditOperations(active, message.event.operations, {
expectedRevisionId: message.expectedRevisionId,          // from the envelope
expectedContentHash: message.event.expectedContentHash,
lockedObjectIds,
})
if (!result.ok) return showError(result.message)           // stale_revision, locked, ...
```

דריסה מציינת רק את הערוצים שהיא משנה, וה-iframe בונה אותה כך במכוון. בישות שמבוססת על קובץ, הטרנספורמציה שמוגדרת לה בקובץ ה-GLB היא הקובעת, ולכן אסור שעריכה שהזיזה משהו תכתוב מחדש גם סיבוב שהיא לא נגעה בו. כדי לשמור עריכה בלי משימת יצירה, קראו ל-`POST /v1/3d-scene/revisions/:revisionId/edits` או למתודה `client.scene3d.applyEdits()` ב-SDK; ראו את דף ה-API [סצנות תלת־ממד](https://nodaro.ai/docs/developers/api/3d-scenes).

## בדיקת תקינות ומגבלות
השדות `scenePlan` ו-`pendingPlan`, וכל `history[].scenePlan`, מפוענחים עם הסכמה הציבורית `scene3DAnyPlanSchema` של `@nodaro/shared`, שמחילה על כל תוכנית את הסכמה של הגרסה שלה. הבדיקה מכסה את המבנה ואת הכללים שחלים על כמה שדות יחד: מעגלים ביחסי האב, אבות שלא קיימים, פריימי מפתח אחרי הפריים האחרון ותקרת האורך. תוכנית שנכשלת בבדיקה נדחית כולה.

| מגבלה | ערך |
| --- | --- |
| רשומות ב-`history` | 12 |
| רשומות ב-`selectedObjectIds` או ב-`lockedObjectIds` | 100 |
| אורך המזהה של אובייקט | 64 תווים |
| אורך השדות `changeSummary` ו-`context.prompt` | 2,000 תווים |
| אורך השדה `createdAt` | 64 תווים |
| אורך הפרמטר `parentOrigin` | 255 תווים |
| רוחב וגובה הפריים | 100–2,560 פיקסלים בכל ציר |
| אובייקטים בסצנה | 100 |
| פריימי מפתח לאובייקט או למסלול המצלמה | 240 |
| ישויות בסצנה בגרסת פורמט 2 | 100 |
| קבצים, שוטים ודריסות בסצנה בגרסת פורמט 2 | 64, 32 ו-200 |
| סך הבייטים של הקבצים שסצנה בגרסת פורמט 2 יכולה להצהיר עליהם | 64 MiB |

ה-iframe בודק את אורכי הרשימות לפני שהוא מפענח משהו, כך שמטען גדול מדי נדחה בלי שהוא נקרא בכלל. מניפסט בגרסת פורמט 2 הוא הבטחה לגבי בייטים, ולכן הסכומים המוצהרים נבדקים עוד לפני שמבקשים ולו קובץ אחד. פריים שהצד הארוך שלו גדול מ-1,920 פיקסלים מרונדר ל-MP4 במחיר גבוה יותר; ראו את הדף [רינדור וידאו](https://nodaro.ai/docs/nodes/video/render-video).

## רשימת בדיקות לדף המארח
ה-iframe מגן על ההודעות שנכנסות אליו. רק הדף שלכם יכול להגן על ההודעות שנכנסות אליכם.

1. **צרו `channel` חדש** לכל iframe עם `crypto.randomUUID()`, ושמרו אותו.
2. **בנו את הכתובת עם ה-origin המדויק שלכם,** מ-`window.location.origin`, ולא ממחרוזת שהרכבתם בעצמכם.
3. **בדקו ארבעה דברים בכל הודעה שאתם מקבלים.**
   - `event.source === iframe.contentWindow`.
   - הערך של `event.origin` הוא בדיוק ה-origin של Nodaro שהטמעתם.
   - הערך של `data.channel` הוא הערוץ שלכם.
   - הערך של `data.version` הוא גרסת פרוטוקול שמימשתם: `1`, או `2` אם אתם מספקים קבצים.
4. **בדקו את `expectedRevisionId`** מול הגרסה הפעילה שלכם לפני שאתם מחילים אירוע `plan`, `restore` או `resolve-pending`, והתעלמו מהאירוע אם אין התאמה.
5. **בדקו שוב את התקינות של `plan` עם `scene3DPlanSchema`** לפני שאתם שומרים אותו. ה-iframe בודק את מה שהוא מציג; אתם אחראים למה שאתם שומרים.
6. **שלחו `state` כשמגיע `ready`,** ושוב אחרי כל שינוי שאתם מקבלים.
7. **אף פעם אל תשלחו ל-`"*"`.** ציינו במפורש את ה-origin של Nodaro.
8. **אל תכניסו שום סוד להודעה.** הפרוטוקול מעביר גאומטריה של סצנה ומזהי גרסאות: לא אסימונים, לא מזהי משתמשים ולא כתובת URL שלא הייתם מכניסים לייחוס של סצנה.
9. **הסירו את המאזין כשתיבת הדו־שיח נסגרת.** מאזין ישן יחד עם ערוץ שמשמש שוב הם בדיוק מה שגורם לשתי תיבות דו־שיח להתחיל לענות זו לזו.
10. **אם אתם מספקים סצנות בגרסת פורמט 2, אשרו כל בקשת קובץ** רק לפי התוכנית ששלחתם, כמו שמתואר בסעיף [כללים לדף שלכם](#rules-for-your-page).

## דוגמה מעשית
```ts

// A history entry, as described in "Parent to frame: state"
type RevisionEntry = {
revisionId: string
scenePlan: Scene3DPlan
source: "generate" | "edit" | "manual" | "upstream"
createdAt: string
changeSummary?: string
context?: { prompt?: string }
}

const NODARO_ORIGIN = "https://app.nodaro.ai"
const channel = crypto.randomUUID()

const iframe = document.createElement("iframe")
iframe.src =
`${NODARO_ORIGIN}/embed/scene3d` +
`?parentOrigin=${encodeURIComponent(window.location.origin)}` +
`&channel=${channel}`
iframe.allow = "" // the frame needs no permissions

declare const initialPlan: Scene3DPlan               // the revision you load from your own store
let active: Scene3DPlan = initialPlan                // your current revision
let history: RevisionEntry[] = []                    // newest last, at most 12
let pending: Scene3DPlan | undefined                 // set it when a job finishes after the user edited
let selection: string[] = []                         // the selected object ids
let locks: string[] = []                             // the locked object ids

function pushState() {
iframe.contentWindow?.postMessage(
{
type: "nodaro:scene3d:state",
version: 1,
channel,
scenePlan: active,
selectedObjectIds: selection,
lockedObjectIds: locks,
history: history.slice(-12),
pendingPlan: pending,     // undefined when there is nothing to choose
readOnly: false,          // leave it out and the frame is view-only
},
NODARO_ORIGIN,              // never "*"
)
}

function onMessage(e: MessageEvent) {
if (e.source !== iframe.contentWindow) return
if (e.origin !== NODARO_ORIGIN) return
const data = e.data
if (!data || typeof data !== "object") return
if (data.channel !== channel || data.version !== 1) return

if (data.type === "nodaro:scene3d:ready") {
pushState()                 // the frame listens: send the truth
return
}
if (data.type !== "nodaro:scene3d:event") return

// The action was computed from a revision we may have replaced already.
const mutates = data.event.kind !== "selection"
if (mutates && data.expectedRevisionId !== active.revisionId) return

switch (data.event.kind) {
case "selection":
selection = data.event.objectIds
return                    // no new state needed; the frame shows it already
case "locks":
locks = data.event.objectIds
break
case "plan": {
// Trust nothing you save.
const parsed = scene3DPlanSchema.safeParse(data.event.plan)
if (!parsed.success) return
active = parsed.data
history = [...history, {
revisionId: active.revisionId,
scenePlan: active,
source: "manual" as const,
changeSummary: data.event.changeSummary,
createdAt: new Date().toISOString(),
}].slice(-12)
break
}
case "restore": {
const entry = history.find((h) => h.revisionId === data.event.revisionId)
if (!entry) return
active = entry.scenePlan
break
}
case "resolve-pending":
active = data.event.adopt ? pending! : active
pending = undefined
break
}
pushState()
}

window.addEventListener("message", onMessage)
document.body.appendChild(iframe)

// On teardown:
//   window.removeEventListener("message", onMessage)
//   iframe.remove()
```

יצירה ועריכה של סצנות עם מודל ורינדור של סצנה ל-MP4 הן משימות API רגילות: ראו את הדפים [יצירת סצנת תלת־ממד](https://nodaro.ai/docs/nodes/video/generate-3d-scene), [עריכת סצנת תלת־ממד](https://nodaro.ai/docs/nodes/video/edit-3d-scene) ו[רינדור וידאו](https://nodaro.ai/docs/nodes/video/render-video). התצוגה התלת־ממדית רק מציגה: היא אף פעם לא מתחילה משימה ואף פעם לא מוציאה קרדיטים.

## גרסאות הפרוטוקול
גרסת הפרוטוקול `version: 1` היא החוזה הראשון, והיא קפואה. המעטפת של `state` קפדנית: כשיש בה מפתח לא מוכר ברמה העליונה, או מפתח לא מוכר בתוך רשומה בהיסטוריה, ה-iframe דוחה את ההודעה ולא מתעלם מהמפתח, וכך דף מארח ו-iframe אף פעם לא מגיעים להסכמה חלקית על המשמעות של הודעה. הפרוטוקול מתרחב כשמעלים את `version`, ו-iframe שלא מממש גרסת פרוטוקול מסוימת דוחה את ההודעה באופן גלוי במקום לנחש.

גרסת הפרוטוקול `version: 2` מוסיפה על גרסת פרוטוקול 1 ולא מחליפה בה שום דבר. היא מביאה את הסצנות האפויות, את מנגנון העברת הקבצים ואת האירוע `edit-operations`, ודף מארח שמדבר רק בגרסת פרוטוקול 1 ממשיך לעבוד בלי שינוי. ה-iframe מודיע מה הוא יודע לעשות ב-`ready.protocolVersions` וב-`ready.capabilities`: בדקו את השדות האלה במקום להניח, וכך גם גרסת הפרוטוקול הבאה רק תוסיף ולא תחליף. אם אתם צריכים שדה שלא קיים, פתחו Issue ב[מאגר הקוד הציבורי](https://github.com/nodaroai/app.nodaro.ai/issues) במקום לשלוח אותו בכל זאת: הודעה שנדחתה היא סימן שהחוזה עובד.

## שאלות נפוצות

### האם התצוגה התלת־ממדית צריכה אסימון או סשן?

לא. ה-iframe של התצוגה התלת־ממדית אף פעם לא מבצע אימות, לא קורא מהאחסון ולא כותב אליו, ולא פונה ל-API של Nodaro. הדף שלכם שולח את הסצנה עם postMessage, ובסצנה אפויה הדף מוריד את הבייטים של הקבצים עם הסשן שלו ושולח אותם.

### למה התצוגה התלת־ממדית ממשיכה להציג את הגרסה הישנה אחרי עריכה?

זה במכוון. ה-iframe של התצוגה התלת־ממדית שולח לכם את הגרסה החדשה כאירוע וממתין. הוא מציג את הגרסה החדשה רק אחרי שהדף שלכם שומר אותה ושולח אותה בחזרה בהודעת state, ולכן ה-iframe אף פעם לא יכול להציג גרסה שלא נשמרה.

### האם אפשר לערוך בתצוגה התלת־ממדית המוטמעת?

אפשר לערוך בתצוגה התלת־ממדית המוטמעת רק כשבהודעת ה-state שלכם readOnly הוא false. ברירת המחדל היא מצב צפייה בלבד, שבו אפשר לנגן את הסצנה, לגלול בציר הזמן ולבחור אובייקטים, וה-iframe שולח רק אירועי בחירה.

### מתי לשלוח את הודעת ה-state הראשונה?

את הודעת ה-state הראשונה שולחים כתשובה להודעת ready של התצוגה התלת־ממדית, לא באירוע load של ה-iframe. האירוע load מתרחש לפני שה-iframe מאזין, ולכן הודעה שנשלחת ברגע הזה עלולה ללכת לאיבוד.

### איך יודעים אם פריסה מציעה את התצוגה התלת־ממדית?

בפריסה שכוללת את התצוגה התלת־ממדית, רכיב יצירת סצנת תלת־ממד מציין את היכולת scene3d-embed-v1 בתשובה של GET /v1/nodes. בדקו את היכולת הזו לפני שאתם מציעים את העורך המוטמע.
