# Incorporar o viewport de cena 3D

> Incorpore o viewport de cena 3D do Nodaro em um iframe e controle-o com postMessage: handshake, mensagens de estado, eventos de edição, assets e limites.

Source: https://nodaro.ai/pt-BR/docs/developers/embed/scene3d

O **viewport de cena 3D** é o visualizador 3D de pré-visualização do Nodaro, que você pode incorporar em um iframe dentro do seu próprio app, em `/embed/scene3d`. É o mesmo visualizador que o editor do Nodaro usa: um viewport three.js, controles de reprodução, uma lista de objetos, editores numéricos de pose e um histórico de revisões. O iframe é controlado inteiramente por `postMessage`: sua página guarda a cena, e o iframe a desenha e informa o que o usuário fez.

Esta não é a incorporação de miniapp. Um miniapp executa um workflow publicado; o viewport não executa nada. Veja [Incorporações](https://nodaro.ai/docs/developers/embed) para a diferença, e [Formato de cena 3D](https://nodaro.ai/docs/developers/embed/scene3d-format) para os dados que ele desenha.

## O que o iframe faz e não faz
| | |
| --- | --- |
| **Lê** | Só o `parentOrigin` e o `channel` da própria URL, e as mensagens `state` dessa origem exata. |
| **Desenha** | A cena que você envia: o viewport, a reprodução e a navegação na linha do tempo, a lista de objetos, os editores de pose de cada objeto e da câmera, o histórico de revisões e o aviso de revisão pendente. Para uma cena pré-calculada (versão 2): a câmera pré-calculada, a navegação entre tomadas, as entidades semânticas com as cores de identificação delas e os controles de sobreposição. |
| **Envia** | `ready` quando começa a escutar, depois um `event` por ação do usuário e um `asset-request` por asset declarado de uma cena da versão 2. |
| **Nunca faz** | Autenticar-se, ler ou gravar no armazenamento, chamar um endpoint `/v1`, guardar algo de um carregamento para outro, ou avançar a própria visualização depois de uma edição. |

O iframe é **sem estado por design**. Quando o usuário confirma uma edição, o iframe envia para você uma nova revisão imutável e continua mostrando a antiga até que *você* envie o resultado de volta. Um iframe que se adiantasse à página pai poderia mostrar uma revisão que nunca chega a ser salva.

O iframe não guarda nenhum segredo e pode ser carregado com segurança em uma página que você não controla totalmente. Os dados de cena que você envia a ele só são tão privados quanto a origem para a qual você os endereça, então sempre envie para uma origem exata; veja o [checklist da página pai](#parent-checklist).

## Verificar se o viewport está disponível
Em uma implantação que inclui o viewport, o nó [**Gerar cena 3D** (Generate 3D Scene)](https://nodaro.ai/docs/nodes/video/generate-3d-scene) anuncia o recurso `scene3d-embed-v1` em `GET /v1/nodes`. Verifique esse recurso antes de oferecer o editor incorporado.

## A URL
```text
https://app.nodaro.ai/embed/scene3d?parentOrigin=<origin>&channel=<uuid>
```

| Parâmetro | Obrigatório | Regra |
| --- | --- | --- |
| `parentOrigin` | Sim | A origem exata e normalizada da página que incorpora o iframe: um esquema, um host e uma porta opcional, nada mais. `new URL(value).origin` precisa ser igual ao valor. Só `http:` e `https:`, até 255 caracteres. Uma barra no final, um caminho, uma query, informações de usuário, `null` ou qualquer outro esquema são recusados. |
| `channel` | Sim | Um UUID aleatório novo, de `crypto.randomUUID()`, um por iframe montado. Ele impede que dois viewports na mesma página, ou um iframe antigo de uma caixa de diálogo fechada, redesenhem um ao outro. |

`MessageEvent.origin` chega normalizado pelo navegador. Exigir o parâmetro com a mesma grafia permite que o iframe compare os dois exatamente, em vez de adivinhar. Se um parâmetro estiver ausente ou malformado, o iframe mostra um erro, não envia nada e não escuta nada.

## O handshake
1. Sua página adiciona o iframe com a URL dele.
2. O iframe é montado e começa a escutar. Ele envia `nodaro:scene3d:ready` e repete o envio até você responder.
3. Sua página responde com uma mensagem `nodaro:scene3d:state`. O iframe valida a cena e a desenha.
4. Quando o usuário age, o iframe envia um `nodaro:scene3d:event`.
5. Sua página verifica `expectedRevisionId`, aplica o evento e envia um novo `state`: a nova verdade.

**Envie o seu primeiro `state` em resposta ao `ready`, não no evento `load` do iframe.** O `load` dispara antes de o listener do iframe estar pronto, então uma mensagem enviada nesse momento pode ser descartada. O iframe repete o `ready` em um intervalo curto e limitado, o que cobre uma página pai que começa a escutar um pouco tarde. Ele para depois da sua primeira mensagem endereçada a ele, mesmo uma que ele recuse: uma recusa ainda prova que você está escutando.

## Da página pai para o iframe: state
Existe um único tipo de mensagem, e ele carrega um snapshot completo. Não existe atualização parcial: sempre envie o estado completo que você quer desenhado.

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

Uma entrada de histórico:

```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` é o `Scene3DPlan` público do pacote `@nodaro/shared`: o mesmo formato que um job de Gerar cena 3D ou de **Editar cena 3D** (Edit 3D Scene) retorna em `output_data.scenePlan`.

- **`readOnly` vem como `true` por padrão.** O silêncio significa: olhe, mas não toque. Um iframe editável precisa dizer `readOnly: false`.
- **`context` mantém só `prompt`,** que vira a dica (tooltip) do botão de restaurar. Outras chaves dentro de `context` são descartadas em silêncio. Chaves desconhecidas em qualquer outro lugar são recusadas; veja [Versionamento](#versioning).
- **`isGenerating: true`** só adiciona um aviso de que uma revisão está sendo gerada. A cena continua ativa e, quando não é somente leitura, editável.
- **`pendingPlan`** cobre um job que terminou depois que o usuário editou. O iframe mostra um aviso e, quando editável, dois botões para escolher uma revisão.

### O que o iframe ignora e o que ele recusa
| O iframe recebe | Ele |
| --- | --- |
| Uma mensagem cujo `event.source` não é a janela que incorpora o iframe | Ignora a mensagem, em silêncio. |
| Uma mensagem cujo `event.origin` não é `parentOrigin` | Ignora a mensagem, em silêncio. |
| Outro `type`, outro `channel` ou um payload que não é um objeto | Ignora a mensagem, em silêncio. |
| Uma `version` desconhecida | Recusa a mensagem, de forma visível. |
| Um envelope malformado, uma chave desconhecida ou uma lista acima do limite | Recusa a mensagem, de forma visível. |
| Um `scenePlan`, um `pendingPlan` ou qualquer `history[].scenePlan` que falhe na validação | Recusa a mensagem, de forma visível. |

Ignorar é silencioso, porque o tráfego de outro iframe não é problema do usuário. Recusar é visível, porque foi a *sua* mensagem que estava errada. **Uma recusa nunca destrói dados aceitos:** o último snapshot aceito continua na tela, sob um banner que informa o motivo, e o próximo `state` válido o remove.

## Do iframe para a página pai: ready e event
O iframe envia as duas mensagens para o `parentOrigin` exato da URL dele, nunca para `"*"`.

```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
},
}
```

**Identifique o `ready` pelo `type` e pelo `channel`, e leia `protocolVersions`.** Não compare a mensagem inteira. `version` continua `1`, então uma página pai escrita para a versão 1 continua funcionando; tudo o que a versão 2 acrescenta é anunciado em campos que essa página pai não lê.

```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 },
}
```

O envelope diz `version: 2` só para `edit-operations`. Todos os outros tipos de evento continuam dizendo `1`, então uma página pai da versão 1 nunca encontra uma versão desconhecida em uma mensagem que ela entende.

**`expectedRevisionId` é a revisão que o iframe mostrava quando o usuário agiu,** a base a partir da qual a ação foi calculada. Aplique o evento só se ele ainda for igual ao seu `scenePlan.revisionId` ativo; caso contrário, descarte-o. Essa única verificação faz com que um clique em um snapshot que você já substituiu não tenha efeito, em vez de causar uma reversão silenciosa. Ele só é `null` antes de o iframe aceitar algum estado.

| `kind` | Significado | O que você faz |
| --- | --- | --- |
| `plan` | Uma edição local, como uma pose numérica ou uma mudança de cor, produziu uma nova revisão imutável. `plan.parentRevisionId` é `expectedRevisionId`. Nenhum modelo foi executado e nada foi cobrado. | Torne `plan` a revisão ativa, adicione-a ao histórico e envie um novo `state`. |
| `selection` | O usuário selecionou ou desmarcou objetos. | Reproduza a seleção e envie `state`, ou apenas guarde-a. |
| `locks` | O usuário bloqueou ou desbloqueou objetos. Um objeto bloqueado é um objeto que um job de edição precisa deixar idêntico byte a byte. | Salve a alteração e envie `state`. |
| `restore` | O usuário pediu uma revisão anterior do histórico que você enviou. | Torne essa revisão ativa e envie `state`. |
| `resolve-pending` | `adopt: true` usa o `pendingPlan`; `false` mantém o plano atual. | Resolva e envie `state` sem `pendingPlan`. |
| `edit-operations` | Uma edição de sobreposição da versão 2, na forma de operações. O iframe não a aplica. | Aplique a edição com o aplicador compartilhado, salve o resultado e envie de volta o plano salvo. Veja [Editar uma cena pré-calculada](#edit-a-baked-scene). |

## Modo somente leitura
`readOnly`, que é `true` por padrão, mantém o visualizador inteiro e bloqueia todas as gravações:

| Continua funcionando | Bloqueado |
| --- | --- |
| Reproduzir, pausar e percorrer a linha do tempo | Confirmações numéricas de pose, de objetos e da câmera |
| Selecionar no viewport e na lista | Cores de objetos e do fundo |
| Ler a lista de objetos, os selos de bloqueio e o histórico de revisões | Botões de bloqueio |
| O aviso de revisão pendente | Restaurar e os botões de revisão pendente |

O único evento que um iframe somente leitura emite é `selection`. A regra vale nos dois sentidos: os controles que alterariam a cena ficam desativados ou ocultos, e o iframe se recusa a emitir uma alteração mesmo que um controle seja acionado diretamente. Somente leitura é uma propriedade do iframe, não um estilo.

## Cenas pré-calculadas e o transporte de assets
Uma cena da **versão 1** é autossuficiente: primitivas, quadros-chave e uma câmera, tudo dentro do plano que você envia. Uma cena da **versão 2** é geometria pré-calculada (baked). O manifesto dela traz entidades semânticas, tomadas, uma trilha de câmera pré-calculada e uma lista de ids de assets com hashes SHA-256, enquanto os bytes em si ficam atrás da API autenticada do Nodaro.

O iframe continua sem sessão e não faz nenhuma chamada de rede. Em vez disso, ele pede a *você* exatamente os assets que o manifesto declara, e você os busca com a sua própria sessão.

### Ativar
Envie `version: 2` na sua mensagem `state`. Uma cena da versão 2 em uma mensagem `version: 1` é recusada com `scenePlan — this scene uses schema version 2, which needs embed protocol version 2 (asset transport)`: uma página pai da versão 1 não tem tratamento para pedidos de assets, e o iframe ficaria esperando para sempre. As cenas da versão 1 funcionam com qualquer uma das versões.

### As mensagens
```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` é a revisão armazenada atual do plano. Cada revisão armazenada fixa todos os seus assets, incluindo os bytes que reaproveita de uma revisão anterior.

### O que o iframe verifica
O iframe ignora em silêncio o tráfego normal que não é para ele:

- Uma resposta de outra janela, de outra origem ou de outro `channel`.
- Um `requestId` que não está pendente, como uma resposta atrasada depois de um tempo limite, uma duplicata ou bytes que ninguém pediu.

Ele faz aquele asset falhar, de forma visível, nestes casos:

- `version` não é `2`, ou um campo é desconhecido ou está ausente.
- `revisionId` ou `assetId` não é o que foi pedido.
- A resposta é `ok: false`. O iframe mostra o seu `error`, encurtado.
- `bytes` não é um `ArrayBuffer`.
- O tamanho não é o `byteLength` do manifesto.
- **O hash SHA-256 não é o hash do manifesto.**

A verificação do hash é a que importa: ela separa “o host entregou alguns bytes” de “estes são os bytes de que esta revisão é feita”. O renderizador verifica o hash uma segunda vez antes do parsing, então nenhum caminho desenha geometria não verificada.

O iframe também impõe limites a si mesmo:

- No máximo **4 pedidos ficam em andamento**, e o resto espera em uma fila.
- No máximo **64 pedidos** e **64 MiB** são permitidos por revisão.
- Cada pedido tem **20 segundos** para ser respondido.
- Todo pedido pendente é rejeitado quando chega uma nova revisão ou quando o iframe é desmontado, então uma resposta para uma cena que o usuário deixou nunca é desenhada.
- Assets idênticos, com o mesmo id e o mesmo hash, são pedidos uma vez, então uma revisão de sobreposição que reaproveita um GLB não o baixa de novo.

### Regras para a sua página
1. **Autorize com base no que você enviou.** Só responda quando `request.revisionId` for a revisão que você enviou (`plan.revisionId`) e `request.assetId` estiver nos `assets` desse plano, com o mesmo `byteLength` e o mesmo `sha256`. Caso contrário, responda `ok: false`. Se você pular essa verificação, a sua sessão vira um oráculo: quem controlar a página dentro do iframe pode pedir qualquer revisão ou asset e ler a resposta.
2. **Nunca envie uma credencial.** Nem token, nem cookie, nem URL assinada: uma URL que concede acesso é um token bearer escrito de outro jeito. Envie bytes.
3. **Envie para a origem exata do iframe,** nunca para `"*"`.
4. **Não transfira um buffer que você mantém.** Um structured clone o copia; uma lista de transferência o tiraria de você.
5. **Descarte pedidos de uma revisão que você já substituiu,** e mantenha no máximo uma busca em andamento por revisão e asset.

```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" })
}
})
```

### Editar uma cena pré-calculada
As edições da versão 2 são **sobreposições**, e o iframe nunca aplica uma sozinho. Uma revisão da versão 2 que o seu servidor ainda não armazenou pareceria salva enquanto os assets dela ainda pertencem à revisão de origem. Por isso, o iframe envia as operações e continua mostrando a revisão que você enviou:

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

Passe os três valores de verificação de revisão desatualizada para o aplicador compartilhado, o mesmo que a API usa, e depois salve o resultado e envie-o de volta:

```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, ...
```

Uma substituição nomeia só os canais que altera, e o iframe a monta assim de propósito. Em uma entidade de asset, a transformação do nó no arquivo GLB é a referência, então uma edição que moveu algo não deve também repetir uma rotação que nunca tocou. Para salvar uma edição sem um job de geração, chame `POST /v1/3d-scene/revisions/:revisionId/edits`, ou `client.scene3d.applyEdits()` no SDK; veja a [API de cenas 3D](https://nodaro.ai/docs/developers/api/3d-scenes).

## Validação e limites
`scenePlan`, `pendingPlan` e cada `history[].scenePlan` são analisados com o `scene3DPlanSchema` público de `@nodaro/shared`. A verificação cobre a estrutura e as regras entre campos: ciclos de pais, pais ausentes, quadros-chave depois do último quadro e o teto de duração. Um plano que falha é recusado por inteiro.

| Limite | Valor |
| --- | --- |
| Entradas de `history` | 12 |
| Entradas de `selectedObjectIds` ou de `lockedObjectIds` | 100 |
| Tamanho do id de um objeto | 64 caracteres |
| Tamanho de `changeSummary` e de `context.prompt` | 2.000 caracteres |
| Tamanho de `createdAt` | 64 caracteres |
| Tamanho de `parentOrigin` | 255 caracteres |
| Largura e altura do quadro | 100 a 2560 px em cada eixo |
| Objetos por cena | 100 |
| Quadros-chave por objeto ou por trilha de câmera | 240 |
| Entidades em uma cena da versão 2 | 100 |
| Assets, tomadas e substituições em uma cena da versão 2 | 64, 32 e 200 |
| Bytes de assets que uma cena da versão 2 pode declarar | 64 MiB |

O iframe verifica o tamanho das listas antes de analisar qualquer coisa, então um payload grande demais é recusado sem ser lido. Um manifesto da versão 2 é uma promessa sobre bytes, então os totais declarados são verificados antes de qualquer asset ser pedido. Um quadro com mais de 1920 px no lado mais longo é renderizado em MP4 por um preço maior; veja [**Renderizar vídeo** (Render Video)](https://nodaro.ai/docs/nodes/video/render-video).

## Checklist da página pai
O iframe protege a própria caixa de entrada. Só a sua página pode proteger a sua.

1. **Crie um `channel` novo** por iframe com `crypto.randomUUID()`, e guarde-o.
2. **Monte a URL com a sua própria origem exata,** a partir de `window.location.origin`, não de uma string que você montou.
3. **Verifique quatro coisas em toda mensagem que você recebe.**
   - `event.source === iframe.contentWindow`.
   - `event.origin` é exatamente a origem do Nodaro que você incorporou.
   - `data.channel` é o seu channel.
   - `data.version` é uma versão que você implementa: `1`, ou `2` se você fornece assets.
4. **Compare `expectedRevisionId`** com a sua revisão ativa antes de aplicar um evento `plan`, `restore` ou `resolve-pending`, e descarte o evento se não corresponder.
5. **Valide `plan` de novo com `scene3DPlanSchema`** antes de armazená-lo. O iframe valida o que desenha; você é responsável pelo que salva.
6. **Envie `state` ao receber `ready`,** e de novo depois de cada alteração que você aceitar.
7. **Nunca envie para `"*"`.** Enderece a origem do Nodaro de forma explícita.
8. **Não coloque nada secreto em uma mensagem.** O protocolo transporta geometria de cena e ids de revisão: nada de tokens, de identificadores de usuário ou de URLs que você não colocaria em uma referência de cena.
9. **Remova o listener quando a caixa de diálogo fechar.** Um listener antigo somado a um channel reutilizado é como duas caixas de diálogo começam a responder uma à outra.
10. **Se você fornece cenas da versão 2, autorize cada pedido de asset** com base no plano que você enviou, como descrito em [Regras para a sua página](#rules-for-your-page).

## Exemplo completo
```ts

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()
```

Gerar e editar cenas com um modelo, e renderizar uma cena em MP4, são jobs comuns da API: veja [Gerar cena 3D](https://nodaro.ai/docs/nodes/video/generate-3d-scene), [Editar cena 3D](https://nodaro.ai/docs/nodes/video/edit-3d-scene) e [Renderizar vídeo](https://nodaro.ai/docs/nodes/video/render-video). O viewport é só o visualizador: ele nunca inicia um job e nunca gasta nada.

## Versionamento
`version: 1` é o primeiro contrato, congelado. O envelope `state` é estrito: uma chave desconhecida no nível superior, ou dentro de uma entrada de histórico, é recusada em vez de ignorada. Assim, uma página pai e um iframe nunca concordam só pela metade sobre o significado de uma mensagem. O protocolo cresce aumentando a `version`, e um iframe que não implementa uma versão recusa a mensagem de forma visível, em vez de adivinhar.

`version: 2` acrescenta à versão 1 e não substitui nada. Ela traz as cenas pré-calculadas, o transporte de assets e o evento `edit-operations`, e uma página pai que só fala a versão 1 continua funcionando sem mudanças. O iframe anuncia o que consegue fazer em `ready.protocolVersions` e `ready.capabilities`: leia esses campos em vez de supor, o que mantém a próxima versão também aditiva. Se você precisar de um campo que não existe, abra uma issue no [repositório público](https://github.com/nodaroai/app.nodaro.ai/issues) em vez de enviá-lo mesmo assim: uma mensagem recusada é o contrato funcionando.

## Frequently asked questions

### O viewport de cena 3D precisa de um token ou de uma sessão?

Não. O iframe nunca se autentica, nunca lê nem grava no armazenamento e nunca chama a API do Nodaro. Sua página envia a cena com postMessage e, para uma cena pré-calculada, busca os bytes dos assets com a própria sessão e envia os bytes.

### Por que o viewport continua mostrando a revisão antiga depois de uma edição?

Por design. O iframe envia a nova revisão para você como um evento e aguarda. Ele só mostra a nova revisão depois que sua página a salva e a envia de volta em uma mensagem de estado, então o iframe nunca pode mostrar uma revisão que não foi salva.

### O viewport incorporado é editável?

Só quando a sua mensagem de estado diz readOnly false. O padrão é somente leitura: os usuários podem reproduzir, percorrer a linha do tempo e selecionar, e o iframe só emite eventos de seleção.

### Quando devo enviar a primeira mensagem de estado?

Em resposta à mensagem ready do iframe, não no evento load do iframe. O evento load dispara antes de o iframe começar a escutar, então uma mensagem enviada nesse momento pode se perder.

### Como sei se uma implantação oferece o viewport?

Em uma implantação que inclui o viewport, o nó Gerar cena 3D (Generate 3D Scene) anuncia o recurso scene3d-embed-v1 em GET /v1/nodes. Verifique esse recurso antes de oferecer o editor incorporado.
