Nodaro ドキュメント
ドキュメントノードリファレンスモデルAI エージェント(MCP)開発者向けセルフホスティングリサーチ
埋め込み

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 は、ブラウザーによって正規化された状態で届きます。パラメーターに同じ表記を要求することで、フレームは推測に頼らず、両者を正確に比較できます。パラメーターが欠けているか、形式が正しくない場合、フレームはエラーを表示し、何も送信せず、何も待ち受けません。

ハンドシェイク

  1. 自分のページが、URL を指定してフレームを追加します。
  2. フレームがマウントされ、待ち受けを開始します。nodaro:scene3d:ready を送信し、応答があるまでそれを繰り返します。
  3. 自分のページが nodaro:scene3d:state メッセージで応答します。フレームはシーンを検証し、それを描画します。
  4. ユーザーが操作すると、フレームは nodaro:scene3d:event を送信します。
  5. 自分のページが 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-pendingadopt: 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 を再利用するオーバーレイのリビジョンが、それを再度ダウンロードすることはありません。

親ページのルール

  1. 送信した内容と照らし合わせて認可します。応答するのは、request.revisionId が送信したリビジョン(plan.revisionId)であり、request.assetId がそのプランの assets に含まれ、byteLength と sha256 が一致する場合だけです。それ以外は ok: false で応答します。この確認を省略すると、自分のセッションがオラクルになってしまいます。フレーム内のページを制御できる者なら誰でも、任意のリビジョンやアセットを要求し、その応答を読み取れてしまうからです。
  2. 認証情報を絶対に送らないでください。トークンも、クッキーも、署名付き URL も送りません。アクセスを与える URL は、表記が違うだけのベアラートークンです。送るのはバイトだけです。
  3. フレームの正確なオリジンへ送信してください。"*" へは決して送らないでください。
  4. 保持しておくバッファーを転送しないでください。構造化クローンはバッファーをコピーしますが、転送リストは、それを自分の手元から取り去ってしまいます。
  5. 置き換え済みのリビジョンへのリクエストは捨ててください。リビジョンとアセットごとに、同時に実行中のフェッチは最大 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)を参照してください。

親ページのチェックリスト

フレームは、自分の受信箱を自分で守ります。自分のページの受信箱を守れるのは、自分のページだけです。

  1. フレームごとに、新しい channel を crypto.randomUUID() で作成し、保持します。
  2. URL は、自分で組み立てた文字列ではなく、window.location.origin から得た、自分の正確なオリジンで組み立てます。
  3. 受け取るメッセージごとに、次の 4 点を確認します。
    • event.source === iframe.contentWindow であること。
    • event.origin が、フレームに指定した Nodaro のオリジンと正確に一致すること。
    • data.channel が自分の channel であること。
    • data.version が、自分が実装しているバージョン(1。アセットを提供する場合は 2 も)であること。
  4. plan、restore、resolve-pending のイベントを適用する前に、expectedRevisionId を自分の現在のリビジョンと比較して確認し、一致しない場合はそのイベントを捨てます。
  5. 保存する前に、plan を scene3DPlanSchema でもう一度検証します。フレームが検証するのは自身が描画するものであり、保存するものに責任を持つのは自分のページです。
  6. ready を受け取ったときと、受け入れた変更のたびに、state を送信します。
  7. "*" へは決して送信しないでください。Nodaro のオリジンを明示的に指定します。
  8. メッセージには、秘密情報を一切含めないでください。このプロトコルでやり取りするのは、シーンのジオメトリとリビジョン ID です。トークン、ユーザー識別子、シーンの参照に含めるつもりのない URL は含めません。
  9. ダイアログを閉じたら、リスナーを削除します。古いリスナーと、再利用された channel が組み合わさると、2 つのダイアログが互いに応答し合ってしまいます。
  10. バージョン 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 を立ててください。メッセージが拒否されるのは、契約が正しく機能している証拠です。

よくある質問

最終更新

目次