דילוג לתוכן
תיעוד Nodaro
תיעודמדריך הרכיביםמודליםתבניותסוכני AI (MCP)מפתחיםאירוח עצמימחקרהשוואות
הטמעות

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

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

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

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

מה ה-iframe עושה ומה לא

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

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

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

בדיקת הזמינות של התצוגה התלת־ממדית

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

הכתובת

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

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

{
  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
}

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

{
  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 מושמטים בשקט. מפתחות לא מוכרים בכל מקום אחר נדחים; ראו את הסעיף גרסאות הפרוטוקול.
  • הערך 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 המדויק שבכתובת שלו, אף פעם לא ל-"*".

{
  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 מוסיפה מופיע בשדות שדף מארח כזה לא קורא.

{
  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 לא מחיל אותה.החילו אותה עם פונקציית ההחלה המשותפת, שמרו את התוצאה ושלחו בחזרה את התוכנית השמורה. ראו את הסעיף עריכת סצנה אפויה.

מצב צפייה בלבד

השדה 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 עובדות עם כל אחת משתי גרסאות הפרוטוקול.

ההודעות

// 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. התעלמו מבקשות לגרסה שכבר החלפתם, ולכל צירוף של גרסה וקובץ אל תשאירו בטיפול יותר מהורדה אחת.
import { createClient } from "@nodaro/sdk"
import type { Scene3DPlanV2 } from "@nodaro/shared"

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 שולח את הפעולות וממשיך להציג את הגרסה ששלחתם:

// 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 משתמש בה, את שלושת הערכים שבודקים שהגרסה עדכנית, ואז שמרו את התוצאה ושלחו אותה בחזרה:

import { applyScene3DV2EditOperations } from "@nodaro/shared"

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 סצנות תלת־ממד.

בדיקת תקינות ומגבלות

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

מגבלהערך
רשומות ב-history12
רשומות ב-selectedObjectIds או ב-lockedObjectIds100
אורך המזהה של אובייקט64 תווים
אורך השדות changeSummary ו-context.prompt2,000 תווים
אורך השדה createdAt64 תווים
אורך הפרמטר parentOrigin255 תווים
רוחב וגובה הפריים100–2,560 פיקסלים בכל ציר
אובייקטים בסצנה100
פריימי מפתח לאובייקט או למסלול המצלמה240
ישויות בסצנה בגרסת פורמט 2100
קבצים, שוטים ודריסות בסצנה בגרסת פורמט 264, 32 ו-200
סך הבייטים של הקבצים שסצנה בגרסת פורמט 2 יכולה להצהיר עליהם64 MiB

ה-iframe בודק את אורכי הרשימות לפני שהוא מפענח משהו, כך שמטען גדול מדי נדחה בלי שהוא נקרא בכלל. מניפסט בגרסת פורמט 2 הוא הבטחה לגבי בייטים, ולכן הסכומים המוצהרים נבדקים עוד לפני שמבקשים ולו קובץ אחד. פריים שהצד הארוך שלו גדול מ-1,920 פיקסלים מרונדר ל-MP4 במחיר גבוה יותר; ראו את הדף רינדור וידאו.

רשימת בדיקות לדף המארח

ה-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, אשרו כל בקשת קובץ רק לפי התוכנית ששלחתם, כמו שמתואר בסעיף כללים לדף שלכם.

דוגמה מעשית

import { scene3DPlanSchema, type Scene3DPlan } from "@nodaro/shared"

// 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 רגילות: ראו את הדפים יצירת סצנת תלת־ממד, עריכת סצנת תלת־ממד ורינדור וידאו. התצוגה התלת־ממדית רק מציגה: היא אף פעם לא מתחילה משימה ואף פעם לא מוציאה קרדיטים.

גרסאות הפרוטוקול

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

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

שאלות נפוצות

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

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

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

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

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

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

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

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

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

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

עודכן לאחרונה

בדף הזה