3D シーンビューポートの埋め込み
Nodaro の 3D シーンビューポートを自分のアプリにフレームとして埋め込み、postMessage で操作する方法を、ハンドシェイクや state メッセージから、編集イベント、アセット転送、上限まで説明します。
3D シーンビューポートは、Nodaro の 3D プレビジュアライゼーションビューアーで、/embed/scene3d から、自分のアプリの中にフレームとして埋め込めます。これは、Nodaro のエディター自体が使っているのと同じビューアーで、three.js のビューポート、再生コントロール、オブジェクトの一覧、数値によるポーズエディター、リビジョン履歴を備えています。フレームはすべて postMessage によって操作されます。シーンを保持するのは自分のページの役割で、フレームはそれを描画し、ユーザーが行った操作を報告します。
これはミニアプリの埋め込みとは異なります。ミニアプリは公開されたワークフローを実行しますが、ビューポートは何も実行しません。違いについては埋め込みを、描画するデータについては 3D シーン形式を参照してください。
フレームが行うこと、行わないこと
| 読み取る内容 | URL 内の parentOrigin と channel、そして、その正確なオリジンからの state メッセージだけです。 |
| 描画する内容 | 送られたシーンです。ビューポート、再生とスクラブ、オブジェクトの一覧、各オブジェクトとカメラのポーズエディター、リビジョン履歴、保留中リビジョンの通知です。ベイク済み(バージョン 2)のシーンの場合は、ベイク済みのカメラ、ショットナビゲーション、識別用の色を持つセマンティックエンティティ、オーバーレイのコントロールも含みます。 |
| 送信する内容 | 待ち受けを始めたら ready を、その後はユーザーの操作ごとに 1 つの event を、バージョン 2 のシーンで宣言されたアセットごとに 1 つの asset-request を送信します。 |
| 決して行わないこと | 認証、ストレージの読み書き、/v1 エンドポイントの呼び出し、リロードをまたいだ状態の保持、編集後に自分の表示を勝手に進めることです。 |
フレームは、設計上ステートレスです。ユーザーが編集を確定すると、フレームは新しい不変のリビジョンを送り、その結果を自分のページが送り返すまで、古いリビジョンを表示したままにします。フレームが親より先に進んでしまうと、保存されないリビジョンを表示してしまう可能性があるためです。
フレームは秘密情報を一切持たないため、完全には管理できないページに読み込んでも安全です。送信するシーンデータの機密性は、送信先として指定したオリジンの機密性以上には保たれません。そのため、必ず正確なオリジンへ送信してください。親ページのチェックリストを参照してください。
ビューポートが利用できるか確認する
ビューポートを含むデプロイ環境では、3D シーン生成(Generate 3D Scene)ノードが、GET /v1/nodes の中で scene3d-embed-v1 という機能(capability)を示します。埋め込みエディターを提供する前に確認してください。
URL
https://app.nodaro.ai/embed/scene3d?parentOrigin=<origin>&channel=<uuid>| パラメーター | 必須 | ルール |
|---|---|---|
parentOrigin | はい | フレームを表示するページの、正規化された正確なオリジンです。スキーム、ホスト、任意のポートのみで、それ以外は含みません。new URL(value).origin の結果が、この値と一致する必要があります。http: と https: のみで、最大 255 文字です。末尾のスラッシュ、パス、クエリ、ユーザー情報、null、その他のスキームは拒否されます。 |
channel | はい | crypto.randomUUID() から生成した、新しいランダムな UUID で、マウントされたフレームごとに 1 つです。同じページ内の 2 つのビューポートや、閉じたダイアログの古いフレームが、互いの描画を上書きすることを防ぎます。 |
MessageEvent.origin は、ブラウザーによって正規化された状態で届きます。パラメーターに同じ表記を要求することで、フレームは推測に頼らず、両者を正確に比較できます。パラメーターが欠けているか、形式が正しくない場合、フレームはエラーを表示し、何も送信せず、何も待ち受けません。
ハンドシェイク
- 自分のページが、URL を指定してフレームを追加します。
- フレームがマウントされ、待ち受けを開始します。
nodaro:scene3d:readyを送信し、応答があるまでそれを繰り返します。 - 自分のページが
nodaro:scene3d:stateメッセージで応答します。フレームはシーンを検証し、それを描画します。 - ユーザーが操作すると、フレームは
nodaro:scene3d:eventを送信します。 - 自分のページが
expectedRevisionIdを確認し、イベントを適用して、新しいstate(新しい正となる情報)を送信します。
最初の state は、iframe の load イベントではなく、ready への応答として送信してください。load は、フレームのリスナーが準備できる前に発生するため、そのタイミングに合わせたメッセージは失われることがあります。フレームは ready を、短い間隔で、回数を限って繰り返します。これは、待ち受けの開始が少し遅れた親ページにも対応するためです。フレームは、自分宛てに送られた最初のメッセージを受け取った後、それが拒否するものであっても、繰り返しを止めます。拒否であっても、待ち受けていることの証明にはなるからです。
親からフレームへ:state
メッセージの種類は 1 つだけで、完全なスナップショットを送ります。部分的な更新はありません。描画してほしい状態は、常に完全な形で送信してください。
{
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 は、@nodaro/shared パッケージの公開された Scene3DPlan です。3D シーン生成または 3D シーン編集(Edit 3D Scene)のジョブが output_data.scenePlan に返すものと、同じ形をしています。
readOnlyのデフォルトはtrueです。何も指定しないということは、見るだけで触れないという意味です。編集可能なフレームにするには、readOnly: falseを指定する必要があります。contextはpromptだけを保持します。これは復元ボタンのツールチップになります。context内のほかのキーは、黙って捨てられます。それ以外の場所にある未知のキーは拒否されます。バージョニングを参照してください。isGenerating: trueは、リビジョンを生成中であるという注記を追加するだけです。シーンはそのまま操作可能で、読み取り専用でない場合は編集もできます。pendingPlanは、ユーザーが編集した後に完了したジョブを扱います。フレームは通知を表示し、編集可能な場合は、どちらのリビジョンを使うか選ぶための 2 つのボタンを表示します。
フレームが無視すること、拒否すること
| フレームが受け取るもの | 動作 |
|---|---|
event.source がフレームを表示しているウィンドウではないメッセージ | 黙って無視します。 |
event.origin が parentOrigin と異なるメッセージ | 黙って無視します。 |
別の type、別の channel、またはオブジェクトではないペイロード | 黙って無視します。 |
未知の version | 目に見える形で拒否します。 |
| 不正な形式のエンベロープ、未知のキー、上限を超えたリスト | 目に見える形で拒否します。 |
検証に失敗した scenePlan、pendingPlan、または history[].scenePlan | 目に見える形で拒否します。 |
無視が黙って行われるのは、ほかのフレーム宛てのトラフィックは、ユーザーの問題ではないからです。拒否が目に見える形で行われるのは、誤っていたのが自分のページのメッセージだからです。拒否によって、受け入れ済みのデータが失われることはありません。直前に受け入れられたスナップショットは、理由を示すバナーとともに画面に残り、次に有効な state が届くと消えます。
フレームから親へ:ready と event
フレームは、どちらのメッセージも、自分の URL の正確な 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 が追加するものはすべて、バージョン 1 の親ページが読み取らないフィールドの中で通知されます。
{
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 は、ユーザーが操作したときにフレームが表示していたリビジョンで、その操作の計算元になったものです。イベントを適用するのは、これが自分の現在の scenePlan.revisionId と等しい場合だけにし、それ以外は捨ててください。この 1 つの確認によって、すでに置き換えたスナップショットへのクリックが、気づかれないロールバックにならず、何も起こらない操作になります。null になるのは、フレームがまだ一度も state を受け入れていないときだけです。
kind | 意味 | 行うこと |
|---|---|---|
plan | 数値によるポーズや色の変更といったローカルな編集が、新しい不変のリビジョンを生成しました。plan.parentRevisionId が expectedRevisionId です。モデルは実行されておらず、課金もされていません。 | plan を現在のリビジョンにし、履歴に追加して、新しい state を送信します。 |
selection | ユーザーがオブジェクトを選択、または選択解除しました。 | それを反映し、state を送信するか、単に記録しておきます。 |
locks | ユーザーがオブジェクトをロック、またはロック解除しました。ロックされたオブジェクトは、編集ジョブがバイト単位で変更してはならないオブジェクトです。 | それを保存し、state を送信します。 |
restore | ユーザーが、送信済みの履歴から、以前のリビジョンを要求しました。 | それを現在のリビジョンにし、state を送信します。 |
resolve-pending | adopt: true は pendingPlan を使い、false は現在のプランを維持します。 | それを解決し、pendingPlan を含めずに state を送信します。 |
edit-operations | バージョン 2 のオーバーレイ編集を、操作として渡します。フレームはこれを自分では適用しません。 | 共有パッケージの適用関数でこれを適用し、結果を保存して、保存したプランを送り返します。ベイク済みのシーンを編集するを参照してください。 |
読み取り専用モード
readOnly は、デフォルトで true になっており、ビューアー全体はそのまま使えますが、書き込みはすべて行われません。
| 引き続き使えるもの | 行われないもの |
|---|---|
| 再生、一時停止、スクラブ | オブジェクトとカメラの、数値によるポーズの確定 |
| ビューポートと一覧での選択 | オブジェクトと背景の色の変更 |
| オブジェクトの一覧、ロックバッジ、リビジョン履歴の閲覧 | ロックの切り替え |
| 保留中リビジョンの通知 | 復元と、保留中リビジョンのボタン |
読み取り専用のフレームが送信するイベントは selection だけです。このルールは二重に守られています。シーンを変更するはずのコントロールは無効化または非表示になり、コントロールが直接操作されたとしても、フレームは変更を送信することを拒みます。読み取り専用は、見た目の問題ではなく、フレームの性質です。
ベイク済みのシーンとアセット転送
バージョン 1 のシーンは単体で完結しています。プリミティブ、キーフレーム、カメラが、すべて送信するプランの中に含まれます。バージョン 2 のシーンは、ベイク済みのジオメトリです。そのマニフェストは、セマンティックエンティティ、ショット、ベイク済みのカメラトラック、SHA-256 ダイジェスト付きのアセット ID の一覧を含みますが、バイト自体は、Nodaro の認証が必要な API の向こう側にとどまります。
フレームには、依然としてセッションもなく、ネットワーク呼び出しも行いません。代わりに、マニフェストが宣言しているアセットだけを自分のページに要求し、自分のページが、自分のセッションでそれを取得します。
有効にする
自分の state メッセージで version: 2 を送信します。version: 1 のメッセージにバージョン 2 のシーンを乗せると、scenePlan — this scene uses schema version 2, which needs embed protocol version 2 (asset transport) というメッセージで拒否されます。バージョン 1 の親ページには、アセットリクエストを処理する仕組みがなく、フレームが永遠に待ち続けてしまうからです。バージョン 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 は、プランの現在保持されているリビジョンです。保持された各リビジョンは、以前のリビジョンから再利用したバイトも含め、そのすべてのアセットを固定します。
フレームが確認すること
フレームは、自分宛てではない通常のトラフィックを、黙って無視します。
- 別のウィンドウ、別のオリジン、別の
channelからの応答。 - 応答待ちではない
requestId。タイムアウト後に届いた遅い応答、重複、誰も要求していないバイトなど。
次の場合には、そのアセットだけを、目に見える形で失敗させます。
versionが2ではない、またはフィールドが未知か欠けている場合。revisionIdまたはassetIdが、要求したものと異なる場合。- 応答が
ok: falseの場合。フレームは、渡されたerrorを短縮して表示します。 bytesがArrayBufferではない場合。- 長さが、マニフェストの
byteLengthと異なる場合。 - SHA-256 ダイジェストが、マニフェストのダイジェストと異なる場合。
重要なのは、このダイジェストの確認です。これによって、「ホストが何らかのバイトを届けた」ということと、「これがこのリビジョンを構成するバイトである」ということを区別できます。レンダラーは、パースする前に、もう一度ダイジェストを確認します。そのため、検証されていないジオメトリが描画される経路はありません。
フレームは、自身にも次の制限を課します。
- 同時に処理できるのは最大 4 件のリクエストまでで、残りはキューで待機します。
- 1 つのリビジョンあたり、最大 64 件のリクエスト、64 MiB までです。
- 各リクエストの応答時間は 20 秒です。
- 新しいリビジョンが届くか、フレームがアンマウントされると、応答待ちのリクエストはすべて拒否されます。そのため、ユーザーが離れたシーンの応答が描画されることはありません。
- 同じ ID と同じダイジェストを持つ、同一のアセットは 1 回しか要求されません。そのため、GLB を再利用するオーバーレイのリビジョンが、それを再度ダウンロードすることはありません。
親ページのルール
- 送信した内容と照らし合わせて認可します。応答するのは、
request.revisionIdが送信したリビジョン(plan.revisionId)であり、request.assetIdがそのプランのassetsに含まれ、byteLengthとsha256が一致する場合だけです。それ以外はok: falseで応答します。この確認を省略すると、自分のセッションがオラクルになってしまいます。フレーム内のページを制御できる者なら誰でも、任意のリビジョンやアセットを要求し、その応答を読み取れてしまうからです。 - 認証情報を絶対に送らないでください。トークンも、クッキーも、署名付き URL も送りません。アクセスを与える URL は、表記が違うだけのベアラートークンです。送るのはバイトだけです。
- フレームの正確なオリジンへ送信してください。
"*"へは決して送らないでください。 - 保持しておくバッファーを転送しないでください。構造化クローンはバッファーをコピーしますが、転送リストは、それを自分の手元から取り去ってしまいます。
- 置き換え済みのリビジョンへのリクエストは捨ててください。リビジョンとアセットごとに、同時に実行中のフェッチは最大 1 件にしてください。
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 の編集はオーバーレイであり、フレームがそれを自分で適用することはありません。自分のサーバーが保存していないバージョン 2 のリビジョンは、保存済みのように見えても、そのアセットは元になったリビジョンに属したままになってしまいます。そのため、フレームは操作の内容を送信するだけで、送信したリビジョンを表示し続けます。
// 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
}リビジョンの古さを確認する 3 つの値をすべて、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, ...オーバーライドに含まれるのは、変更するチャンネルだけです。フレームは意図的にそのように構築します。アセットエンティティの場合、GLB ファイル内のノードトランスフォームが正となるため、何かを移動させた編集が、触れてもいない回転まで書き換えてしまってはいけません。生成ジョブを使わずに編集を保存するには、POST /v1/3d-scene/revisions/:revisionId/edits を呼び出すか、SDK の client.scene3d.applyEdits() を使います。3D シーン API を参照してください。
検証と上限
scenePlan、pendingPlan、そしてすべての history[].scenePlan は、@nodaro/shared の公開された scene3DPlanSchema でパースされます。この確認は、構造と、フィールドをまたいだルールを対象とします。親の循環参照、存在しない親、最終フレームより後にあるキーフレーム、長さの上限です。検証に失敗したプランは、全体が拒否されます。
| 上限 | 値 |
|---|---|
history のエントリ数 | 12 |
selectedObjectIds または lockedObjectIds のエントリ数 | 100 |
| オブジェクト ID の長さ | 64 文字 |
changeSummary と context.prompt の長さ | 2,000 文字 |
createdAt の長さ | 64 文字 |
parentOrigin の長さ | 255 文字 |
| フレームの幅と高さ | 各軸 100〜2,560 px |
| シーンあたりのオブジェクト数 | 100 |
| オブジェクトまたはカメラトラックあたりのキーフレーム数 | 240 |
| バージョン 2 のシーンのエンティティ数 | 100 |
| バージョン 2 のシーンのアセット、ショット、オーバーライドの数 | 64、32、200 |
| バージョン 2 のシーンが宣言できるアセットのバイト数 | 64 MiB |
フレームは、何かをパースする前に、リストの長さを確認します。そのため、サイズが大きすぎるペイロードは、読み取られることなく拒否されます。バージョン 2 のマニフェストは、バイトについての約束であるため、宣言された合計値は、アセットを 1 つ要求する前に確認されます。長辺が 1,920 px を超えるフレームを MP4 にレンダリングすると、料金が高くなります。動画レンダリング(Render Video)を参照してください。
親ページのチェックリスト
フレームは、自分の受信箱を自分で守ります。自分のページの受信箱を守れるのは、自分のページだけです。
- フレームごとに、新しい
channelをcrypto.randomUUID()で作成し、保持します。 - URL は、自分で組み立てた文字列ではなく、
window.location.originから得た、自分の正確なオリジンで組み立てます。 - 受け取るメッセージごとに、次の 4 点を確認します。
event.source === iframe.contentWindowであること。event.originが、フレームに指定した Nodaro のオリジンと正確に一致すること。data.channelが自分の channel であること。data.versionが、自分が実装しているバージョン(1。アセットを提供する場合は2も)であること。
plan、restore、resolve-pendingのイベントを適用する前に、expectedRevisionIdを自分の現在のリビジョンと比較して確認し、一致しない場合はそのイベントを捨てます。- 保存する前に、
planをscene3DPlanSchemaでもう一度検証します。フレームが検証するのは自身が描画するものであり、保存するものに責任を持つのは自分のページです。 readyを受け取ったときと、受け入れた変更のたびに、stateを送信します。"*"へは決して送信しないでください。Nodaro のオリジンを明示的に指定します。- メッセージには、秘密情報を一切含めないでください。このプロトコルでやり取りするのは、シーンのジオメトリとリビジョン ID です。トークン、ユーザー識別子、シーンの参照に含めるつもりのない URL は含めません。
- ダイアログを閉じたら、リスナーを削除します。古いリスナーと、再利用された channel が組み合わさると、2 つのダイアログが互いに応答し合ってしまいます。
- バージョン 2 のシーンを提供する場合は、親ページのルールで説明されているとおり、送信したプランに対して、すべてのアセットリクエストを認可してください。
実装例
import { scene3DPlanSchema, type Scene3DPlan } from "@nodaro/shared"
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
let active: Scene3DPlan = initialPlan // your current revision
let history: RevisionEntry[] = [] // newest last, at most 12
function pushState() {
iframe.contentWindow?.postMessage(
{
type: "nodaro:scene3d:state",
version: 1,
channel,
scenePlan: active,
selectedObjectIds: selection,
lockedObjectIds: locks,
history: history.slice(-12),
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",
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 ジョブです。3D シーン生成、3D シーン編集、動画レンダリングを参照してください。ビューポートはあくまでビューアーであり、ジョブを開始することも、何かを消費することも決してありません。
バージョニング
version: 1 は、最初に固定された契約です。state エンベロープは厳格で、トップレベルの未知のキーや、履歴エントリ内の未知のキーは、無視されるのではなく拒否されます。そのため、親ページとフレームが、メッセージの意味について、半端に合意してしまうことはありません。プロトコルは version を上げることで拡張され、あるバージョンを実装していないフレームは、推測するのではなく、目に見える形でメッセージを拒否します。
version: 2 はバージョン 1 に追加するだけで、何も置き換えません。ベイク済みのシーン、アセット転送、edit-operations イベントが加わり、バージョン 1 にしか対応していない親ページも、変更なしに動作し続けます。フレームは、自分にできることを ready.protocolVersions と ready.capabilities で通知します。推測するのではなく、これらのフィールドを読み取ってください。そうすれば、次のバージョンも追加だけの拡張にとどめられます。存在しないフィールドが必要な場合は、そのまま送信するのではなく、公開リポジトリで issue を立ててください。メッセージが拒否されるのは、契約が正しく機能している証拠です。
よくある質問
関連ページ
3D シーン形式
埋め込み
3D シーン生成
3D シーン
3D シーン
最終更新