הטמעת התצוגה התלת־ממדית
הטמיעו את התצוגה התלת־ממדית של 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 מציג שגיאה, לא שולח כלום ולא מאזין לשום דבר.
לחיצת היד
- הדף שלכם מוסיף את ה-iframe עם הכתובת שלו.
- ה-iframe נטען ומתחיל להאזין. הוא שולח
nodaro:scene3d:ready, ושולח אותו שוב ושוב עד שאתם עונים. - הדף שלכם עונה בהודעת
nodaro:scene3d:state. ה-iframe בודק את תקינות הסצנה ומציג אותה. - כשהמשתמש מבצע פעולה, ה-iframe שולח
nodaro:scene3d:event. - הדף שלכם בודק את
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 לא מורידה אותו פעם נוספת.
כללים לדף שלכם
- אשרו בקשות רק לפי מה ששלחתם. ענו רק כש-
request.revisionIdהוא הגרסה ששלחתם (plan.revisionId) וכש-request.assetIdמופיע ב-assetsשל אותה תוכנית עם אותםbyteLengthו-sha256. אחרת, ענוok: false. אם תדלגו על הבדיקה הזו, הסשן שלכם יהפוך לאורקל: מי ששולט בדף שבתוך ה-iframe יוכל לבקש כל גרסה או כל קובץ ולקרוא את התשובה. - אף פעם אל תשלחו פרטי גישה. לא אסימון, לא cookie ולא כתובת URL חתומה: כתובת URL שנותנת גישה היא בסך הכול אסימון Bearer בצורה אחרת. שלחו בייטים.
- שלחו ל-origin המדויק של ה-iframe, אף פעם לא ל-
"*". - אל תוסיפו ל-transfer list באפר שאתם שומרים אצלכם. שליחה רגילה, כ-structured clone, מעתיקה את הבאפר; העברה ב-transfer list הייתה לוקחת אותו מכם.
- התעלמו מבקשות לגרסה שכבר החלפתם, ולכל צירוף של גרסה וקובץ אל תשאירו בטיפול יותר מהורדה אחת.
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, שמחילה על כל תוכנית את הסכמה של הגרסה שלה. הבדיקה מכסה את המבנה ואת הכללים שחלים על כמה שדות יחד: מעגלים ביחסי האב, אבות שלא קיימים, פריימי מפתח אחרי הפריים האחרון ותקרת האורך. תוכנית שנכשלת בבדיקה נדחית כולה.
| מגבלה | ערך |
|---|---|
רשומות ב-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 במחיר גבוה יותר; ראו את הדף רינדור וידאו.
רשימת בדיקות לדף המארח
ה-iframe מגן על ההודעות שנכנסות אליו. רק הדף שלכם יכול להגן על ההודעות שנכנסות אליכם.
- צרו
channelחדש לכל iframe עםcrypto.randomUUID(), ושמרו אותו. - בנו את הכתובת עם ה-origin המדויק שלכם, מ-
window.location.origin, ולא ממחרוזת שהרכבתם בעצמכם. - בדקו ארבעה דברים בכל הודעה שאתם מקבלים.
event.source === iframe.contentWindow.- הערך של
event.originהוא בדיוק ה-origin של Nodaro שהטמעתם. - הערך של
data.channelהוא הערוץ שלכם. - הערך של
data.versionהוא גרסת פרוטוקול שמימשתם:1, או2אם אתם מספקים קבצים.
- בדקו את
expectedRevisionIdמול הגרסה הפעילה שלכם לפני שאתם מחילים אירועplan,restoreאוresolve-pending, והתעלמו מהאירוע אם אין התאמה. - בדקו שוב את התקינות של
planעםscene3DPlanSchemaלפני שאתם שומרים אותו. ה-iframe בודק את מה שהוא מציג; אתם אחראים למה שאתם שומרים. - שלחו
stateכשמגיעready, ושוב אחרי כל שינוי שאתם מקבלים. - אף פעם אל תשלחו ל-
"*". ציינו במפורש את ה-origin של Nodaro. - אל תכניסו שום סוד להודעה. הפרוטוקול מעביר גאומטריה של סצנה ומזהי גרסאות: לא אסימונים, לא מזהי משתמשים ולא כתובת URL שלא הייתם מכניסים לייחוס של סצנה.
- הסירו את המאזין כשתיבת הדו־שיח נסגרת. מאזין ישן יחד עם ערוץ שמשמש שוב הם בדיוק מה שגורם לשתי תיבות דו־שיח להתחיל לענות זו לזו.
- אם אתם מספקים סצנות בגרסת פורמט 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. בדקו את היכולת הזו לפני שאתם מציעים את העורך המוטמע.
דפים קשורים
פורמט תוכנית הסצנה
הטמעות
יצירת סצנת תלת־ממד
סצנות תלת־ממד
סצנות תלת־ממד
עודכן לאחרונה
הטמעת מיני־אפליקציה
הריצו במוצר שלכם מיני־אפליקציה של Nodaro שפורסמה, עם קוד ההטמעה או עם טופס משלכם על ה-REST API: מקריאת הקלטים ועד הרצות, תוצאות ושגיאות.
פורמט תוכנית הסצנה
איך בנויה תוכנית סצנה תלת־ממדית ב-Nodaro: צורות פשוטות ופריימי מפתח בפורמט 1, וקובצי GLB, ישויות, מסלול מצלמה, שוטים ושכבות־על בפורמט 2.