1 O contínuo entre o sintético e o real — Projeto do Professor

Este é o projeto de referência resolvido pelo professor: a implementação que serve de modelo do que cada grupo deve produzir no Projeto Integrador. Ele existe para ser estudado, discutido e contestado — nunca para ser copiado. O que está aqui é uma solução defensável, com cada decisão explicada e assinada; a solução de cada grupo será outra, e terá de ser defendida do mesmo jeito.

1.1 Visão geral

Três tarefas que não desenham nada, e a razão de duas delas mesmo assim virarem código.

As três tarefas deste módulo pedem declarações. A primeira pede a escolha da cena e a delimitação de um domínio; a segunda, o registro do que cada regime faz com o mundo de quem observa; a terceira, a especificação inteira do projeto, em quatorze seções. Nenhuma das três pede um polígono na tela, e um módulo de realidade virtual que não desenha um triângulo sequer soa como aula de natação em terra firme. A analogia serve, inclusive na parte incômoda: ninguém aprende a nadar sendo jogado na água funda no primeiro dia.

A tentação é resolver as três num documento de texto e voltar ao código no módulo seguinte. Resistimos a ela pela metade, e a metade tem critério. Declaração que o código pode conferir vai para o código: nome de peça que não existe, encaixe órfão, regime declarado sem espaço de referência são erros que o compilador pega antes de qualquer execução, e guardá-los num documento é abrir mão dessa checagem de graça. Declaração que o código não pode conferir agora — quanto a cena vai custar de máquina, que folga de encaixe será aceita, o que sai primeiro quando não couber — vai para o documento, porque esperar não melhora nenhuma delas: só empurra a escolha para depois, com a cena meio construída e o custo dobrado.

Daí a divisão que este módulo entrega. Duas tarefas viram dados tipados que o compilador confere e que a página carrega; a terceira vira docs/especificacao.md, versionado ao lado do código, com números onde cabem números. A Bancada começa aqui, e começa sem renderizar: o que ela entrega é a descrição do domínio, a declaração dos três regimes, uma página que pergunta ao aparelho em que está aberta se ele entra em cada um deles, e o documento que diz o que tudo isso terá de ser. A mesma URL responde diferente no computador de mesa, no celular e no visor, e essa diferença é a primeira coisa observável do percurso inteiro.

Vale dizer o que ficou de fora, porque a lista é grande e a contenção é o conteúdo. Não há grafo de cena, não há malha, não há sessão aberta, não há entrada de usuário. Cada uma dessas peças pertence a um módulo com nome, e antecipar qualquer uma delas roubaria o assunto de uma aula que ainda vai acontecer.

1.2 O eixo que substituiu a pergunta “isso é realidade virtual?”

Paul Milgram e Fumio Kishino, em 1994, pararam de perguntar se um visor de ver-através “era” realidade virtual. Passaram a perguntar quanto de cada lado o sistema exibia. A troca parece pequena e é a origem do eixo que organiza a área — e o marketing de aparelho, que continua preferindo a pergunta antiga, ganhou trinta anos de vocabulário para chamar a mesma coisa de nome novo a cada temporada.

A troca tem consequência direta no desenho do código, e ela é dura. Os três regimes da Bancada não podem ser três programas. Se cada regime for um programa, a comparação entre eles vira comparação entre implementações, e quem estuda conclui — com razão — que a diferença está no código, e não no conceito. O que queremos exibir é o oposto: o mesmo domínio, a mesma lógica, e apenas a relação com o mundo mudando.

Daí a declaração de regime ser uma lista de registros com os mesmos campos. Cada regime declara o que faz com o ambiente de quem observa, qual espaço de referência pretende, o que rastreia e contra o que a cena é registrada. Um regime que precisasse de um campo próprio, ausente nos outros dois, seria o sinal de que voltamos a tratar o contínuo como caixas.

O campo que carrega o contínuo tem três valores e cabe em uma linha.

tratamento do mundo, em src/bancada/modes/regimes.ts
/**
 * O que o regime faz com o ambiente de quem observa — a distinção que separa os
 * três antes de qualquer detalhe técnico.
 */
export type TratamentoDoMundo =
  | 'substitui'   // o ambiente sintético toma o lugar do ambiente real
  | 'preserva'    // o ambiente real permanece visível e recebe o sintético sobre si
  | 'exibe';      // o ambiente sintético é mostrado por uma janela, sem tocar o real

Repare que os três valores estão em ordem de quanto do mundo real sobrevive: substitui apaga tudo, preserva deixa passar, exibe sequer encosta. Escrever isso como três constantes de texto, em vez de três subclasses, é uma decisão de projeto — herança aqui produziria três hierarquias paralelas, uma por regime, que é o desenho que a taxonomia de 1994 desaconselha.

Há um detalhe da API que vale mais que qualquer diagrama nesta altura. Uma sessão informa o modo de composição do fundo, e os valores possíveis são fundo opaco, aditivo e mesclado por transparência.

composição do fundo, em src/bancada/modes/regimes.ts
/**
 * Modo de composição do fundo, tal como a API XR o nomeia. É o ponto em que o
 * contínuo entre o real e o sintético deixa de ser desenho de livro e vira um
 * valor que a sessão informa: fundo opaco esconde o mundo, os outros dois o
 * deixam passar. Só é legível com uma sessão ativa, e por isso aqui ele é o
 * valor ESPERADO — a leitura do valor real chega quando houver sessão.
 */
export type ModoDeComposicao = 'opaque' | 'additive' | 'alpha-blend';

O contínuo, ali, é um campo de dado que a plataforma preenche. Fundo opaco esconde o mundo; os outros dois o deixam passar, com qualidades diferentes de passagem. E repare no que fizemos com esse campo: ele está declarado como esperado, e não como lido. O valor real só existe com uma sessão ativa, e sessão é assunto de módulos adiante. Declarar agora o que se espera, e ler depois o que veio, transforma a tarefa numa aposta verificável.

1.3 Tarefa 1: Escolher a cena e delimitar o domínio

Enunciado da tarefa

O ambiente que acompanha a leitura precisa de uma cena antes de precisar de uma linha de código, e a cena não é escolha de gosto: cada uma endurece uma parte diferente do trabalho e amolece outra. Serve qualquer cena em que objetos sejam selecionados, movidos, orientados e postos em algum lugar certo — uma das propostas no Projeto Integrador, com o custo já mapeado ao lado, ou uma inventada do zero, desde que ela responda em duas linhas cada às três perguntas que fecham aquela lista.

O que fica pronto é a cena escolhida com a razão da escolha escrita em termos de custo — que parte do trabalho ela endurece, e qual é a armadilha que ela traz junto —, mais a delimitação do domínio: a tarefa que o ambiente suporta enunciada em uma frase, os objetos que a compõem e o estado que caracteriza a tarefa concluída.

1.3.1 Por que uma cena própria, e não uma das dez

A Bancada é uma cena inventada do zero, e a decisão precisa ser justificada antes de qualquer outra, porque ela é a mais cara de desfazer do percurso inteiro.

O primeiro motivo é de economia da turma. As cenas propostas existem para quem vai construir o próprio projeto, e uma solução de referência que ocupasse uma delas gastaria um assunto e transformaria o worked example em gabarito — quem escolhesse a mesma cena teria a resolução pronta, e quem escolhesse outra teria um modelo que não se transfere.

O segundo é de honestidade com o caminho. O Projeto Integrador só aceita cena própria com as três perguntas que fecham a lista das dez respondidas, e responder a elas custa mais que herdar as respostas de uma cena que já vem com o custo mapeado. Um modelo que tomasse o caminho barato não serviria de modelo para quem for tomar o caro. As três respostas, em duas linhas cada, estão na seção 2 do documento de especificação, e são estas: apanhar, girar até a orientação em que a peça entra e assentá-la é o que se faz com as mãos, e nenhuma das três tem equivalente em menu; com o visor, a bancada ganha altura de bancada e o alcance do braço passa a existir, o que transforma a ordem de montagem em percurso físico; e contra uma mesa de verdade, o suporte depositado ali tem de continuar naquele ponto da mesa enquanto se caminha em volta.

O custo e a armadilha, declarados de saída. O que a Bancada endurece é o encaixe com ordem de dependência: cinco peças, cinco encaixes, uma ordem em que cada peça só entra depois da anterior. A armadilha que ela traz é geométrica — peças de mecanismo são sólidos regulares, fáceis demais de construir por código, e uma Bancada inteiramente paramétrica atravessaria o percurso sem importar um único arquivo de troca. A contramedida está escrita: a bancada, o suporte e as ferramentas de cenário vêm de arquivo importado, e só as cinco peças nascem de código.

1.3.2 A escolha, e os três critérios que a decidiram

Escolhida a cena, o domínio dela se delimita. Sobre uma bancada de trabalho estão as peças de um mecanismo e um suporte com encaixes; quem usa o ambiente monta o mecanismo peça por peça.

O critério de escolha não foi estético. Precisávamos de um domínio em que objetos sejam selecionados, apanhados, orientados e postos num lugar certo, porque são essas quatro ações que os módulos de interação e manipulação vão exercitar. Uma maquete de cidade que se sobrevoa é bonita e não exercita nenhuma das quatro — ela exercita a admiração, que é gratuita e não rende arguição.

O segundo critério foi a existência de um estado final verificável. Montar tem fim; passear não tem. Um domínio sem fim obriga a inventar, mais adiante, um critério artificial de sucesso, e todo critério artificial acaba virando uma pontuação em que ninguém acredita, nem quem a programou.

O terceiro critério foi a escala. A bancada cabe no alcance dos braços de quem está de pé diante dela. Isso pesa porque o regime imersivo é o único em que alcance de braço existe, e um domínio que exigisse locomoção constante desperdiçaria exatamente a diferença que queremos exibir.

Os dois primeiros critérios se encontram num par de campos que andam juntos no código.

tarefa e estado final, em src/bancada/dominio/dominio.ts
/**
 * A tarefa que o ambiente suporta, enunciada em uma frase, mais o estado que a
 * caracteriza concluída. Os dois campos andam juntos de propósito: tarefa sem
 * estado final é intenção, e é o que produz a cena bonita e vazia.
 */
export interface TarefaDoAmbiente {
  readonly enunciado: string;
  readonly estadoFinal: string;
}

Separar os dois campos seria mais arrumado e pior. Tarefa sem estado final é intenção, e intenção não se confere; estado final sem tarefa é uma condição de vitória sem jogo. Mantê-los no mesmo registro força quem edita um a olhar para o outro.

1.3.3 A delimitação inteira, escrita como dado

O módulo de domínio carrega a descrição, a tarefa em uma frase, o estado que a caracteriza concluída, as cinco peças e os cinco encaixes. Os identificadores de peça e de encaixe são tipos de união literal, o que faz o compilador recusar um nome inventado antes de qualquer execução.

src/bancada/dominio/dominio.ts
// ---------------------------------------------------------------------------
// Delimitação do domínio da Bancada.
//
// Este arquivo é a Tarefa 1 do Projeto Integrador escrita como dado, e não como
// texto solto num documento à parte. O motivo é operacional: a delimitação
// precisa ser confrontável com o que o ambiente faz mais adiante, e texto em
// documento não se confronta com nada. Aqui ela é tipada, e o compilador passa
// a cobrar o que antes dependia de alguém reler a ata da reunião.
//
// Nada nesta etapa desenha, carrega malha ou abre sessão. A cena chega quando
// houver grafo de cena; o encaixe, quando houver interação. O que existe agora é
// a descrição do que aquele ambiente terá de suportar.
// ---------------------------------------------------------------------------

/** Identificador de uma peça manipulável da oficina. */
export type PecaId =
  | 'corpo'
  | 'eixo'
  | 'engrenagem-grande'
  | 'engrenagem-pequena'
  | 'tampa';

/** Identificador de um encaixe do suporte de montagem. */
export type SocketId =
  | 'base-do-suporte'
  | 'furo-do-eixo'
  | 'dente-maior'
  | 'dente-menor'
  | 'topo';

/**
 * Uma peça do mecanismo. `sockets` lista os encaixes que a aceitam — uma peça
 * que não serve em lugar nenhum é um objeto decorativo, e a delimitação existe
 * justamente para não deixar objeto decorativo entrar como se fosse conteúdo.
 */
export interface Peca {
  readonly id: PecaId;
  readonly nome: string;
  readonly sockets: readonly SocketId[];
}

// recorte:inicio tarefa-com-estado-final
/**
 * A tarefa que o ambiente suporta, enunciada em uma frase, mais o estado que a
 * caracteriza concluída. Os dois campos andam juntos de propósito: tarefa sem
 * estado final é intenção, e é o que produz a cena bonita e vazia.
 */
export interface TarefaDoAmbiente {
  readonly enunciado: string;
  readonly estadoFinal: string;
}
// recorte:fim tarefa-com-estado-final

export interface Dominio {
  readonly nome: string;
  readonly descricao: string;
  readonly tarefa: TarefaDoAmbiente;
  readonly pecas: readonly Peca[];
  readonly sockets: readonly SocketId[];
}

export const BANCADA: Dominio = {
  nome: 'Bancada',
  descricao:
    'Uma oficina de montagem: sobre uma bancada de trabalho estão as peças de um ' +
    'mecanismo e um suporte com encaixes, e quem usa o ambiente monta o mecanismo ' +
    'peça por peça.',
// recorte:inicio dominio-declarado-como-dado
    tarefa: {
    enunciado:
      'Montar o mecanismo encaixando cada peça no suporte, na ordem em que uma ' +
      'depende da outra.',
    estadoFinal:
      'As cinco peças estão encaixadas nos sockets compatíveis, na ordem válida, ' +
      'e o mecanismo montado gira.',
  },
  pecas: [
    { id: 'corpo', nome: 'Corpo', sockets: ['base-do-suporte'] },
    { id: 'eixo', nome: 'Eixo', sockets: ['furo-do-eixo'] },
    { id: 'engrenagem-grande', nome: 'Engrenagem grande', sockets: ['dente-maior'] },
    { id: 'engrenagem-pequena', nome: 'Engrenagem pequena', sockets: ['dente-menor'] },
    { id: 'tampa', nome: 'Tampa', sockets: ['topo'] },
  // recorte:fim dominio-declarado-como-dado
  ],
  sockets: ['base-do-suporte', 'furo-do-eixo', 'dente-maior', 'dente-menor', 'topo'],
};

/**
 * Confere que todo socket citado por alguma peça existe no suporte e que todo
 * socket do suporte recebe alguma peça. Devolve a lista de inconsistências, que
 * é vazia quando o domínio fecha.
 *
 * O porquê de isto ser código, e não conferência a olho: a delimitação vai ser
 * editada muitas vezes ao longo do percurso, e um socket órfão sobrevive a
 * qualquer releitura distraída — some só quando alguém tenta encaixar, muitos
 * módulos adiante, quando trocar de domínio já custa caro.
 */
// recorte:inicio dominio-que-se-confere
export function inconsistenciasDoDominio(dominio: Dominio): string[] {
  const declarados: ReadonlySet<SocketId> = new Set(dominio.sockets);
  const usados: Set<SocketId> = new Set();
  const problemas: string[] = [];

  for (const peca of dominio.pecas) {
    if (peca.sockets.length === 0) {
      problemas.push(`A peça "${peca.nome}" não encaixa em socket nenhum.`);
    }
    for (const socket of peca.sockets) {
      if (!declarados.has(socket)) {
        problemas.push(
          `A peça "${peca.nome}" cita o socket "${socket}", que o suporte não declara.`,
        );
      }
      usados.add(socket);
    }
  }

  for (const socket of dominio.sockets) {
    if (!usados.has(socket)) {
      problemas.push(`O socket "${socket}" não recebe peça alguma.`);
    }
  }

  return problemas;
}
// recorte:fim dominio-que-se-confere

Duas decisões merecem justificativa.

A primeira é a lista de encaixes compatíveis dentro de cada peça, em vez de uma tabela separada de compatibilidades. Ela nos deixa perguntar, olhando para uma peça só, onde ela serve. Uma peça que não serve em lugar nenhum aparece de imediato, e uma peça assim é decoração entrando no projeto disfarçada de conteúdo.

A segunda é a função que devolve as inconsistências do domínio. Ela existe porque a delimitação será editada dezenas de vezes ao longo do percurso, e um encaixe órfão sobrevive a qualquer releitura distraída. Ele só apareceria no módulo de manipulação, quando trocar de domínio já custa caro. A conferência custa trinta linhas agora e evita um retrabalho de dias depois.

Repare no que essa função não faz: ela não valida a ordem da montagem. A ordem é conteúdo do módulo de encaixe, e antecipá-la aqui produziria uma regra sem comportamento algum contra o qual testá-la.

1.3.4 Onde é fácil errar, e como conferir

O erro mais comum nesta tarefa é o excesso. Nada custa nada quando ainda não há código, e o domínio cresce sem resistência: vinte peças, uma narrativa de ficção científica, três salas e um elevador. O custo aparece três módulos adiante, quando cada peça precisa de malha, de volume de colisão e de tolerância de encaixe — e aí a estação espacial vira cinco cubos cinzentos com nomes ambiciosos.

A conferência cabe em três perguntas. A tarefa está enunciada em uma frase? O estado final é observável por alguém que olhe a cena, sem consultar uma variável interna? Toda peça declarada encaixa em algum lugar, e todo encaixe recebe alguma peça?

A terceira é a única que a máquina responde sozinha, e por isso virou função. As duas primeiras dependem de leitura, e é nelas que a tutoria costuma encontrar o problema real. Quando um grupo demora a responder a segunda, o domínio quase sempre precisa mudar.

1.4 Tarefa 2: Declarar o que cada regime faz com o mundo

Enunciado da tarefa

Os três regimes do ambiente diferem pelo que fazem com o ambiente de quem observa: um o substitui inteiro, outro o mantém e deposita sobre ele, o terceiro o mostra por uma janela sem tocá-lo. Essa distinção precisa estar escrita antes de existir código, porque ela decide o que cada regime terá de provar depois.

O que fica pronto é o registro, por regime, do espaço de referência pretendido, do que será rastreado e contra o que a cena será registrada. Ao longo do percurso essa declaração vira comportamento observável, e comparar o que se escreveu aqui com o que o ambiente faz adiante é o exercício mais barato de honestidade técnica que o projeto oferece.

1.4.1 Os três registros

src/bancada/modes/regimes.ts
// ---------------------------------------------------------------------------
// Declaração dos três regimes da Bancada.
//
// Este arquivo é a Tarefa 2 do Projeto Integrador. Ele não abre sessão, não
// renderiza e não detecta nada: declara, regime a regime, o espaço de referência
// pretendido, o que será rastreado e contra o que a cena será registrada.
//
// Escrever isso antes do código tem uma consequência barata e boa: mais adiante,
// quando os regimes existirem de fato, a declaração pode ser confrontada com o
// comportamento observado. Declaração que ninguém guarda não se confronta com
// coisa alguma, e o projeto perde o único registro de qual era a intenção.
// ---------------------------------------------------------------------------

/**
 * Os três regimes do ambiente. Os dois primeiros nomes coincidem com os modos de
 * sessão da API XR do navegador de propósito — é o que permite perguntar ao
 * aparelho, sem tradução no meio, se ele suporta o que declaramos.
 */
export type RegimeId = 'inline' | 'immersive-vr' | 'immersive-ar';

// recorte:inicio tratamento-do-mundo
/**
 * O que o regime faz com o ambiente de quem observa — a distinção que separa os
 * três antes de qualquer detalhe técnico.
 */
export type TratamentoDoMundo =
  | 'substitui'   // o ambiente sintético toma o lugar do ambiente real
  | 'preserva'    // o ambiente real permanece visível e recebe o sintético sobre si
  | 'exibe';      // o ambiente sintético é mostrado por uma janela, sem tocar o real
// recorte:fim tratamento-do-mundo

// recorte:inicio composicao-do-fundo
/**
 * Modo de composição do fundo, tal como a API XR o nomeia. É o ponto em que o
 * contínuo entre o real e o sintético deixa de ser desenho de livro e vira um
 * valor que a sessão informa: fundo opaco esconde o mundo, os outros dois o
 * deixam passar. Só é legível com uma sessão ativa, e por isso aqui ele é o
 * valor ESPERADO — a leitura do valor real chega quando houver sessão.
 */
export type ModoDeComposicao = 'opaque' | 'additive' | 'alpha-blend';
// recorte:fim composicao-do-fundo

// recorte:inicio regime-declarado-campo-a-campo
export interface Regime {
  readonly id: RegimeId;
  readonly nome: string;
  readonly tratamentoDoMundo: TratamentoDoMundo;
  /** Espaço de referência pretendido, no vocabulário da API XR. */
  readonly espacoDeReferencia: 'viewer' | 'local' | 'local-floor' | 'unbounded';
  /** O que o sistema rastreia neste regime, em uma frase. */
  readonly rastreia: string;
  /** Contra o que a cena é registrada — a origem do mundo virtual. */
  readonly registroContra: string;
  readonly composicaoEsperada: ModoDeComposicao;
  /** Por que este regime existe no projeto, e não como enfeite comparativo. */
  readonly papel: string;
}
// recorte:fim regime-declarado-campo-a-campo

export const REGIMES: readonly Regime[] = [
  {
    id: 'inline',
    nome: 'Realidade virtual não imersiva',
    tratamentoDoMundo: 'exibe',
    espacoDeReferencia: 'viewer',
    rastreia: 'nada do corpo; a câmera obedece ao mouse',
    registroContra: 'a origem arbitrária da própria cena, fixada por quem a modelou',
    composicaoEsperada: 'opaque',
    papel:
      'É o caso base e o destino de quem não tem headset: a bancada inteira precisa ' +
      'ser montável aqui.',
  },
  {
    id: 'immersive-vr',
    nome: 'Realidade virtual imersiva',
    tratamentoDoMundo: 'substitui',
    espacoDeReferencia: 'local-floor',
    rastreia: 'a pose da cabeça e a das duas mãos, com seis graus de liberdade',
    registroContra:
      'o chão do espaço físico onde a pessoa está, o que faz a bancada nascer na ' +
      'altura certa em vez de flutuar',
    composicaoEsperada: 'opaque',
    papel:
      'É onde escala corporal e alcance de braço passam a existir — e nenhum dos ' +
      'dois tem equivalente na janela do desktop.',
  },
  {
    id: 'immersive-ar',
    nome: 'Realidade aumentada',
    tratamentoDoMundo: 'preserva',
    espacoDeReferencia: 'local-floor',
    rastreia:
      'a pose da cabeça, a das mãos e as superfícies que o aparelho encontra no ' +
      'ambiente',
    registroContra:
      'uma superfície real escolhida no ambiente, à qual a bancada permanece presa ' +
      'enquanto a pessoa caminha em volta',
    composicaoEsperada: 'alpha-blend',
    papel:
      'É o único regime em que errar o registro é visível a olho nu: a bancada ' +
      'desliza sobre a mesa, e ninguém precisa de instrumento para notar.',
  },
];

// diagrama:adiado quem a chama é o roteamento de regime do módulo 06 e o regime aumentado do módulo 12
export function regimePorId(id: RegimeId): Regime {
  const encontrado: Regime | undefined = REGIMES.find((regime) => regime.id === id);
  if (encontrado === undefined) {
    // Inalcançável enquanto REGIMES cobrir RegimeId, e é o compilador que garante
    // isso ao construir a lista. O lançamento existe para o caso de alguém
    // acrescentar um id ao tipo e esquecer a entrada correspondente.
    throw new Error(`Regime não declarado: ${id}`);
  }
  return encontrado;
}

Os identificadores dos regimes coincidem com os modos de sessão da API do navegador. Podíamos ter batizado os três de nomes próprios, que leem melhor em português e ficariam ótimos num relatório. Perderíamos a possibilidade de perguntar ao aparelho, sem tradução no meio, se ele suporta aquilo que declaramos. Nome bonito que exige tabela de conversão é nome caro, e a conta chega no módulo de sessão.

A função que busca um regime pelo identificador lança exceção num caminho que o compilador torna inalcançável. Isso parece desperdício e tem uso: ela protege contra o dia em que alguém acrescentar um quarto identificador ao tipo e esquecer a entrada correspondente na lista. O erro apareceria como um objeto indefinido três camadas adiante, e a mensagem que ele produziria não citaria a lista de regimes.

1.4.2 A escada do registro

O campo mais importante dos três registros é o último a ser lido e o primeiro a ser esquecido: contra o que a cena é registrada.

No regime de janela, a origem do mundo virtual é arbitrária — quem modelou a cena a fixou onde quis, e ninguém tem como discordar. No regime imersivo, ela passa a ser o chão do espaço físico onde a pessoa está, e é por isso que a bancada nasce na altura certa em vez de flutuar na altura dos olhos. Na realidade aumentada, passa a ser uma superfície real escolhida no ambiente.

Regime Origem do mundo virtual Quem decide a origem
Janela ponto arbitrário da cena quem modelou
Imersivo chão do espaço físico o rastreamento do aparelho
Aumentado superfície real escolhida o ambiente, medido pelo aparelho

A escada é o conteúdo do módulo. À medida que se sobe por ela, o mundo virtual perde liberdade de escolher a própria origem e passa a depender de uma medida do mundo físico. Registro, aqui, é exatamente isso: o quanto a origem do virtual está presa a algo do real. Um grupo que declare os três regimes com a mesma origem acabou de escrever uma cena só com três rótulos.

1.4.3 Confrontar a declaração com o aparelho

A declaração sozinha envelhece de um jeito perverso: continua parecendo correta por um bom tempo depois de ter deixado de ser. O módulo de verificação faz a pergunta correspondente ao navegador em que a página está aberta.

src/bancada/modes/verificacao.ts
// ---------------------------------------------------------------------------
// Confronto entre o que declaramos e o que o aparelho oferece.
//
// A Tarefa 2 termina numa declaração de intenção, e intenção envelhece bem
// demais: continua parecendo certa muito depois de ter deixado de ser. Este
// arquivo faz a pergunta correspondente ao navegador em que a página está aberta
// e devolve as duas colunas lado a lado.
//
// O que ele NÃO faz, e a distinção importa: não abre sessão, não mede graus de
// liberdade, não enumera fontes de entrada nem lista capacidades opcionais. Isso
// é a sonda de capacidades, que pertence ao módulo de dispositivos. Aqui a
// pergunta é a mais grossa possível — "este aparelho entra neste regime?" —, que
// é exatamente a que o conceito deste módulo suporta.
// ---------------------------------------------------------------------------

import { REGIMES, type Regime, type RegimeId } from './regimes';

// recorte:inicio resposta-em-tres-valores
/**
 * `desconhecido` não é sinônimo de `nao`. O navegador sem a API XR não está
 * dizendo que o aparelho não serve — está dizendo que não sabe responder, e
 * tratar as duas coisas como a mesma é o erro que faz um relatório honesto
 * virar um relatório confiante e errado.
 */
export type Suporte = 'sim' | 'nao' | 'desconhecido';
// recorte:fim resposta-em-tres-valores

export interface LinhaDoRelatorio {
  readonly regime: Regime;
  readonly suporte: Suporte;
  readonly observacao: string;
}

/** A API XR do navegador, quando existe. */
function sistemaXr(): XRSystem | undefined {
  return navigator.xr;
}

async function suporteDe(id: RegimeId): Promise<Suporte> {
  const xr: XRSystem | undefined = sistemaXr();
  if (xr === undefined) {
    return 'desconhecido';
  }
  try {
    const suportado: boolean = await xr.isSessionSupported(id);
    return suportado ? 'sim' : 'nao';
  } catch {
    // Alguns navegadores rejeitam a promessa em vez de devolver `false` — para
    // um modo de sessão que não reconhecem, ou fora de contexto seguro. Nos dois
    // casos o que se sabe é que não houve resposta utilizável.
    return 'desconhecido';
  }
}

function observacaoDe(regime: Regime, suporte: Suporte): string {
  if (suporte === 'sim') {
    return `Declarado com registro contra ${regime.registroContra}. Falta confrontar em sessão.`;
  }
  if (suporte === 'nao') {
    return 'Este aparelho não entra neste regime. É informação sobre o aparelho, não defeito do código.';
  }
  return 'Sem API XR neste navegador, ou página fora de contexto seguro (HTTPS).';
}

export async function levantarRelatorio(): Promise<LinhaDoRelatorio[]> {
  const linhas: LinhaDoRelatorio[] = [];
  for (const regime of REGIMES) {
    const suporte: Suporte = await suporteDe(regime.id);
    linhas.push({ regime, suporte, observacao: observacaoDe(regime, suporte) });
  }
  return linhas;
}

A pergunta é a mais grossa possível: este aparelho entra neste regime? Não medimos graus de liberdade, não enumeramos fontes de entrada e não listamos capacidades opcionais. Tudo isso é a sonda de capacidades, e a sonda pertence ao módulo de dispositivos. Antecipá-la aqui roubaria o conteúdo da aula seguinte e deixaria esta com um artefato grande demais para o conceito que ele sustenta.

A decisão de desenho que mais rende nesta tarefa é o terceiro valor de resposta. Um navegador sem a API não está afirmando que o aparelho não serve; está informando que não sabe responder. Tratar as duas situações como a mesma produz um relatório confiante e errado, e o computador de mesa do laboratório apareceria como aparelho que “não suporta realidade aumentada” quando o que houve foi ausência de resposta.

O mesmo vale para a página aberta fora de contexto seguro. Sem conexão cifrada, a API não está disponível, e o relatório precisa dizer isso em vez de acusar o aparelho de uma limitação que ele não tem.

1.4.4 Onde é fácil errar, e como conferir

O erro previsível desta tarefa é declarar o que se gostaria de ter em vez do que se pretende construir. Um grupo declara rastreamento de mãos no regime imersivo porque a palavra soa bem numa apresentação, e passa os módulos seguintes carregando uma promessa que ninguém cobrou e que ninguém consegue retirar sem parecer derrota.

A conferência é comparar as três linhas entre si. Se dois regimes declaram o mesmo espaço de referência, o mesmo rastreamento e o mesmo registro, então são a mesma cena com um rótulo trocado, e o projeto ainda não tem os dois modos de percorrer o ambiente que a disciplina exige.

A conferência executável é abrir a página em três aparelhos e ler a última coluna. É o exercício mais barato do percurso, e o único deste módulo que produz evidência em vez de argumento.

1.5 Tarefa 3: Especificar o projeto em quatorze seções

Enunciado da tarefa

A cena escolhida e os regimes declarados respondem por onde o projeto começa, e não pelo que ele será. O resto das decisões — quantos objetos existem, de onde eles vêm, que tamanho têm em metros, o que o ambiente aceita, o que recusa e como avisa, quanto disso cabe na máquina e o que sai primeiro quando não couber — ou é decidido por escrito agora, ou é decidido por acidente depois.

O que fica pronto é um documento em Markdown, em docs/especificacao.md, versionado junto do código, com quatorze seções em quatro blocos: a cena, as regras, a máquina e o trabalho. O teste de cada frase é mecânico: ela pode estar errada?

1.5.1 O documento existe, e está no repositório

A especificação da Bancada está em docs/especificacao.md, ao lado do código e no mesmo histórico de versões. O lugar não é detalhe de arrumação. Um documento que vive num editor de textos na nuvem envelhece sem deixar rastro: ninguém sabe quando a tolerância mudou de três para dois centímetros, nem por quê. No repositório, a mudança tem data, autor e a alteração de código que a acompanhou — e é essa correspondência que transforma o documento em registro, e não em intenção antiga.

Três seções dele merecem ser lidas aqui, porque as três carregam decisões que se pagam adiante. As demais estão escritas com o mesmo critério e não precisam de comentário.

1.5.2 A seção 9 veio do código, campo a campo

A tabela dos três regimes já existia em código, campo a campo, antes de o documento existir. Escrevê-la de novo em prosa criaria duas fontes para a mesma decisão, e a segunda é sempre a que diverge — em três semanas, uma diria local-floor e a outra diria “chão da sala”, e ninguém saberia qual valia.

De docs/especificacao.md, seção 9:

Aspecto Na janela No visor Pela câmera
Como se olha câmera em órbita, mouse pose da cabeça, seis graus pose do aparelho na mão
Como se aponta e age raio do cursor, clique raio do controle, gatilho toque na tela sobre o objeto
Escala da cena 1:1 1:1, corpo de pé 1:2 sobre a mesa
Origem do mundo virtual ponto arbitrário da cena chão do espaço físico superfície real detectada
O que a cena faz de diferente nada além do essencial alcance de braço passa a existir o suporte fica preso à mesa real
O que não existe aqui escala corporal, alcance de braço o mundo real o cenário em volta, e o chão virtual

A última linha é a que o documento acrescenta ao código, e é a mais reveladora das seis: dizer o que não existe em cada regime. O código declara o que cada um faz; só o documento declara o que cada um deixa de fazer, e é essa declaração que impede a promessa de três vezes a mesma coisa com palavras diferentes.

O documento diz, na abertura da seção, quem perde quando os dois divergirem: o documento. A regra vale enquanto a declaração for espelho do código. Quando o comportamento observado contradisser o campo declarado, aí quem muda é o código, e a contradição já terá cumprido a função dela.

1.5.3 As tolerâncias: quatro números que ainda não foram medidos

A seção 7 é a que mais desconforto causa de escrever, porque pede número onde ainda não há medida. A saída é escrever o número assim mesmo, com a etiqueta de provisório e o raciocínio ao lado; adiar só adia.

De docs/especificacao.md, seção 7:

Encaixe Folga de posição Folga de ângulo Por quê
Corpo na base 3,0 cm 15° peça grande, encaixe único, sem vizinho para confundir
Eixo no furo 1,5 cm 10° furo estreito; folga maior faria o eixo saltar sozinho
Engrenagens nos dentes 2,0 cm 8° os dois encaixes são vizinhos, e folga grande troca um pelo outro
Tampa no topo 3,0 cm 20° último passo; apertar aqui só produz irritação

Todos os valores são provisórios e serão medidos no aparelho. O que se sabe antes de medir é a forma do erro nos dois extremos: folga grande demais faz a peça saltar para o lugar sem que a pessoa tenha acertado, e a montagem perde sentido; folga pequena demais transforma o encaixe em tortura de precisão. O valor final é o que sobreviver a esse intervalo, e o registro do que foi testado antes de escolhê-lo vale tanto quanto o número escolhido.

Repare que os quatro valores não são o mesmo número repetido, e que a coluna do porquê é o que os separa. O eixo entra num furo estreito e ganha a folga mais apertada da tabela; a tampa é o último passo e ganha a mais larga, porque exigir precisão no gesto final só produz irritação. As duas engrenagens têm encaixes vizinhos, e folga grande ali não erra a posição: troca uma peça pela outra, que é um defeito diferente e pior.

Nenhum desses valores sobreviverá intacto ao aparelho, e sobreviver é que seria o defeito: a tabela existe para ser corrigida pela medida. O que não pode acontecer é chegar ao módulo de encaixe sem número algum, porque aí não há o que medir, e a calibragem vira tentativa às cegas com um valor inventado na hora.

1.5.4 A ordem de degradação, decidida com calma em vez de com a cena travando

A seção 10 fecha com a pergunta que ninguém quer responder no início: se não couber, o que sai primeiro?

De docs/especificacao.md, seção 10:

Ordem de degradação — o que sai primeiro quando não couber:

  1. as seis ferramentas de cenário, que não têm função na tarefa;
  2. o chão e as paredes, substituídos por um plano liso;
  3. o detalhe dos dentes das engrenagens, que caem para uma forma simplificada;
  4. a bancada importada, substituída pela caixa paramétrica que ela representa.

O que não sai em nenhum caso: as cinco peças, os cinco encaixes e o retorno visual do encaixe. Degradar até perder a tarefa não é degradar, é desistir.

A ordem tem uma lógica, e ela é a distância até a tarefa. Sai primeiro o que não participa dela — as ferramentas de cenário existem para a mesa não parecer um vazio, e é a única função que têm. Depois sai o ambiente em volta. Depois o detalhe do que participa. Por último o objeto importado, que é substituído pela forma paramétrica que ele representa — e essa é a linha mais dolorosa da lista, porque desfaz justamente a contramedida da armadilha declarada na primeira tarefa. Está ali mesmo assim, porque uma ordem de degradação que só corta o indolor não é uma ordem de degradação.

O que não degrada está escrito em negrito de propósito. É onde essa lista costuma se perder: cortando na direção do que é fácil de cortar, chega-se, sem nenhum passo obviamente errado, a um ambiente que abre, roda liso e não sustenta mais a tarefa que o justificava.

1.5.5 Onde é fácil errar, e como conferir

O erro previsível desta tarefa é o texto bonito, e quase nunca o texto curto. Uma especificação com adjetivos — ambiente imersivo, experiência realista, interação intuitiva — atravessa qualquer leitura sem dizer nada verificável, e quem a escreveu chega ao primeiro dia de construção sem saber por onde começar, porque nenhuma decisão foi tomada de fato.

A conferência é mecânica e cabe numa pergunta por frase: esta frase pode estar errada? “O eixo encaixa com folga de um centímetro e meio” pode, e é por isso que serve. “A experiência será fluida e envolvente” não pode, e é por isso que não serve. Passar o documento inteiro por esse crivo é o exercício mais rápido de melhoria de especificação que existe, e ele funciona porque não depende de julgar se a frase está certa — só se ela afirma alguma coisa.

Duas conferências de coerência interna fecham a leitura, e as duas são aritméticas. O total de objetos da seção 3 combina com o orçamento da seção 10? As escalas da seção 4 combinam com a linha de escala da tabela dos regimes? Documento que se contradiz nessas duas contas foi escrito seção a seção, sem ninguém lê-lo inteiro depois — que é exatamente o modo como um documento de quatorze seções nasce errado.

1.6 Imersão, presença e registro, pelo que o sistema faz

Três palavras costumam ser usadas como sinônimos, e as três descrevem coisas diferentes. A confusão entre elas é o que faz uma turma inteira acreditar que basta comprar uma placa de vídeo melhor.

Imersão é propriedade do sistema, e é mensurável: campo de visão, latência, graus de liberdade rastreados, cadência de imagem. Dois aparelhos podem ser comparados por ela sem que ninguém os vista, o que a torna a única das três que cabe numa ficha técnica.

Presença é a resposta da pessoa a esse sistema, e ela não decorre da fidelidade da imagem. Presença é o palco acertando escala e tempo; fidelidade é a pintura do cenário, e cenário pintado não segura ninguém no lugar. É a distinção que mais resiste à intuição, porque quem chega à disciplina chega convencido de que o que convence o corpo é a qualidade da imagem. Frederick Brooks, que trabalhava na área desde os anos 1960, escreveu no balanço de 1999 que a realidade virtual havia passado de “quase funciona” a “mal e mal funciona” — e dizia isso de sistemas cuja imagem já era melhor que a de qualquer coisa exibida numa feira dez anos antes.

Registro é a relação geométrica entre o que é desenhado e o mundo físico. Ele só existe onde há mundo físico visível, o que o torna assunto do regime aumentado e de mais nenhum.

Nenhuma das três admite implementação neste ponto do percurso, e é melhor dizer isso em voz alta do que fabricar um exemplo para preencher a lacuna. O que fizemos foi menos ambicioso e mais útil: os campos de espaço de referência, de rastreamento e de registro, na declaração de regime, são a forma operacional das três — escritos de modo que o comportamento futuro possa contradizê-los, campo por campo.

1.7 O visor pendurado no teto, e o que ele não vira

Um tópico teórico sem código, declarado como tal em vez de disfarçado.

O visor que Ivan Sutherland montou em 1968 era pesado a ponto de precisar ficar suspenso do teto do laboratório por um braço mecânico articulado, que também media a posição da cabeça de quem o vestia. O apelido que o aparelho ganhou depois, “espada de Dâmocles”, descreve com precisão a imagem de quem entrava embaixo dele — e poucos campos técnicos escolheram para si uma alegoria de morte iminente e depois a adotaram com tanto carinho. O que ele exibia eram arestas de arame, e ainda assim é o ponto em que a área começa.

Este tópico não admite implementação, e forçar um exemplo executável para ele seria fabricar conteúdo. O papel dele no projeto é outro: é o critério contra o encantamento com a demonstração bonita, e por isso aparece no primeiro módulo em vez de num apêndice histórico ao final.

A ligação com o que construímos aqui é direta. A distância entre a ambição declarada e o que o equipamento entrega é o que a última coluna do relatório mede, aparelho por aparelho. Sutherland anunciou o visor definitivo e entregou arestas de arame; a Bancada declara três regimes e informa, sem eufemismo, quais deles aquele aparelho não sustenta.

1.8 A página que se abre em aula

O painel definitivo da Bancada é diegético: um cartaz preso à própria bancada, dentro do mundo, lido de dentro do ambiente. Ele não pode existir agora, pelo motivo mais simples possível — não há mundo. Enquanto o grafo de cena não chega, o relatório sai em texto comum na página, montado por uma função de apresentação que não sabe nada sobre regimes e recebe tudo pronto.

O que se abre diante da turma é o ponto de entrada deste módulo, e ele é curto.

src/demos/01_demo.ts
// ---------------------------------------------------------------------------
// Marco 01 — o estado demonstrável da Bancada ao fim do primeiro módulo.
//
// Aqui a Bancada ainda não desenha nada. O que existe é o que se decidiu antes
// de desenhar: o domínio delimitado (quais peças, quais encaixes, qual tarefa),
// os três regimes de sessão declarados, e a conferência do que este aparelho
// responde de fato — que é a pergunta que decide se o resto do percurso tem onde
// rodar.
//
// Este arquivo é o ponto de entrada do MARCO 01 e não é tocado por módulo
// nenhum depois deste (`specs/marcos-executaveis.md`). O módulo 02 escreve o
// seu, ao lado, e o 01 continua rodando exatamente como roda hoje.
// ---------------------------------------------------------------------------

import { BANCADA, inconsistenciasDoDominio } from '../bancada/dominio/dominio';
import { levantarRelatorio, type LinhaDoRelatorio } from '../bancada/modes/verificacao';
import { montarRelatorio } from '../bancada/relatorio/relatorio';

function exigirElemento(id: string): HTMLElement {
  const elemento: HTMLElement | null = document.getElementById(id);
  if (elemento === null) {
    throw new Error(`O marco 01 espera um elemento #${id} na página.`);
  }
  return elemento;
}

async function demonstrar(): Promise<void> {
  const raiz: HTMLElement = exigirElemento('bancada');

  // A conferência do domínio roda antes de qualquer coisa: um encaixe que aponta
  // para peça inexistente é defeito de modelagem, e o percurso inteiro se apoia
  // nesse modelo. Vale a pena descobrir agora, e não três módulos adiante.
  const problemas: readonly string[] = inconsistenciasDoDominio(BANCADA);

  // O levantamento é assíncrono porque perguntar ao navegador quais regimes de
  // sessão ele aceita é uma consulta ao aparelho, não uma constante.
  const linhas: readonly LinhaDoRelatorio[] = await levantarRelatorio();

  montarRelatorio(raiz, BANCADA, problemas, linhas);
}

void demonstrar();

Três coisas acontecem nessa ordem, e a ordem foi escolhida. A conferência do domínio roda primeiro, porque um encaixe apontando para peça inexistente é defeito de modelagem e o percurso inteiro se apoia nesse modelo. O levantamento de suporte vem depois, e é assíncrono porque perguntar ao navegador quais regimes ele aceita é consulta ao aparelho, e não leitura de constante. A montagem do relatório vem por último, e recebe os dois resultados já prontos.

Esse ponto de entrada pertence a este módulo e não será tocado por nenhum módulo posterior. O módulo seguinte escreve o seu ao lado, e esta demonstração continuará rodando no fim do semestre exatamente como roda hoje — o que permite projetar, na aula de composição final, o primeiro estado ao lado do último sem mexer em uma linha de histórico.

Declarar o painel provisório como provisório, em comentário no próprio arquivo, tem função além da boa educação. Sem essa nota, o arquivo seria lido, três módulos adiante, como decisão de projeto a ser preservada — e alguém gastaria uma tarde tentando conciliar dois painéis que nunca deveriam coexistir. O provisório sobrevive a três módulos quando ninguém escreve que ele é provisório.

1.9 Por dentro da implementação

O cenário é o mais pobre do percurso, e é justamente por isso que ele serve: a página do marco aberta no navegador do desktop, em contexto seguro, sem visor conectado. Ninguém clica em nada. A página carrega, o domínio é conferido, três perguntas são feitas ao aparelho e o relatório aparece. A execução descrita aqui devolveu na página o inventário 5 peças e 5 encaixes declarados. Nenhuma inconsistência entre peças e encaixes., seguido da tabela em que a realidade virtual não imersiva sai como suportado e os dois regimes imersivos saem como não suportado. Os números e os valores citados adiante são os dessa execução.

Este é o primeiro módulo, então a costura com o que existia antes cabe em uma frase: não existia nada antes. O ponto de contato entre as três alterações é demonstrar(), que faz três chamadas em ordem escolhida e não conhece o interior de nenhuma delas.

alteração o que ela passou a fazer
dominio-que-se-confere a delimitação deixa de ser texto e passa a ser conferida em duas varreduras, que devolvem a lista de inconsistências
pergunta-em-tres-valores o aparelho é consultado regime a regime, e a ausência de resposta deixa de virar recusa
relatorio-fora-da-cena o confronto vira tabela na página, montada por quem não sabe de onde os números vieram

Falta uma quarta função no percurso, e vale dizer por quê. regimePorId() decide — procura na lista e lança quando o identificador não estiver declarado —, mas nenhum caminho desta demonstração passa por ela: quem a chama é o roteamento do regime de janela e o regime aumentado, dois módulos bem adiante. Desenhar a execução dela aqui exigiria inventar a chamada, e a alternativa honesta é esperar o módulo que a exercita.

1.9.1 A delimitação que se confere sozinha

inconsistenciasDoDominio(BANCADA) roda primeiro, antes de qualquer consulta ao aparelho, e devolve a lista vazia nesta execução. São duas varreduras, e a segunda é a que não se faz a olho.

flowchart TD
  ini["inconsistenciasDoDominio(BANCADA)"] --> prep["declarados = new Set(dominio.sockets)"]
  prep --> pecas{"para cada peca de dominio.pecas"}
  pecas -->|"peca.sockets.length === 0"| orfa["problemas.push(peca nao encaixa em socket nenhum)"]
  pecas -->|"peca declara sockets"| cada{"para cada socket de peca.sockets"}
  cada -->|"declarados.has(socket) === false"| fantasma["problemas.push(socket que o suporte nao declara)"]
  cada -->|"declarados.has(socket) === true"| usa["usados.add(socket)"]
  fantasma --> usa
  usa --> cada
  cada -->|"acabaram os sockets desta peca"| pecas
  orfa --> pecas
  pecas -->|"acabaram as 5 pecas"| suportes{"para cada socket de dominio.sockets"}
  suportes -->|"usados.has(socket) === false"| ninguem["problemas.push(socket nao recebe peca alguma)"]
  suportes -->|"usados.has(socket) === true"| segue["nada a registrar"]
  ninguem --> suportes
  segue --> suportes
  suportes -->|"acabaram os 5 sockets do suporte"| fim["devolve problemas, vazio nesta execucao"]

A primeira varredura percorre dominio.pecas e faz duas perguntas por peça: ela cita algum encaixe, e cada encaixe citado consta de declarados? A segunda percorre dominio.sockets e pergunta o inverso, contra o conjunto usados que a primeira encheu. O conjunto é o que liga as duas: sem ele, a segunda pergunta exigiria varrer todas as peças de novo a cada encaixe.

sequenceDiagram
  autonumber
  participant demo as demos/01_demo.ts
  participant dom as dominio/dominio.ts
  demo->>dom: inconsistenciasDoDominio(BANCADA)
  dom->>dom: declarados recebe os 5 SocketId de BANCADA.sockets
  dom->>dom: peca corpo, socket base-do-suporte, declarados.has(socket) true
  dom->>dom: peca eixo, socket furo-do-eixo, declarados.has(socket) true
  dom->>dom: peca tampa, socket topo, declarados.has(socket) true
  dom->>dom: usados fecha com os mesmos 5 SocketId
  dom->>dom: usados.has(socket) true para os 5 de dominio.sockets
  dom-->>demo: [] , nenhuma inconsistencia

As cinco peças e os cinco encaixes se emparelham um a um, e é por isso que a função sai calada. O resultado é sem graça de propósito: ele só ficará interessante no dia em que alguém acrescentar uma sexta peça e esquecer o encaixe dela.

Devolver a lista, e não lançar na primeira falha. A alternativa era lançar uma exceção assim que o primeiro encaixe órfão aparecesse, que é mais curto de escrever e chama mais atenção. O custo tem duas partes. A primeira é que a exceção esconde as outras inconsistências: quem edita a delimitação corrige uma, roda de novo, descobre a segunda, e paga uma volta inteira por defeito. A segunda é pior neste módulo em particular — a página deixaria de abrir, e com ela desapareceria a única evidência observável que o módulo tem para mostrar, que é a tabela dos três aparelhos.

1.9.2 As três perguntas, e o valor que não é recusa

levantarRelatorio() itera sobre REGIMES e chama suporteDe(id) para cada um. Ali dentro está a única decisão de caminho deste módulo.

flowchart TD
  ini["levantarRelatorio()"] --> laco{"para cada regime de REGIMES"}
  laco -->|"regime.id"| sup["suporteDe(id)"]
  sup --> sis["sistemaXr()"]
  sis --> temApi{"xr === undefined"}
  temApi -->|"true"| desc["devolve 'desconhecido'"]
  temApi -->|"false"| pergunta["await xr.isSessionSupported(id)"]
  pergunta -->|"suportado ? 'sim' : 'nao'"| obs["observacaoDe(regime, suporte)"]
  pergunta -->|"a promessa foi rejeitada"| desc
  desc --> obs
  obs --> qual{"suporte"}
  qual -->|"'sim'"| t1["cita regime.registroContra e diz que falta confrontar em sessao"]
  qual -->|"'nao'"| t2["informacao sobre o aparelho, nao defeito do codigo"]
  qual -->|"'desconhecido'"| t3["sem API XR neste navegador, ou fora de contexto seguro"]
  t1 --> push["linhas.push({ regime, suporte, observacao })"]
  t2 --> push
  t3 --> push
  push --> laco
  laco -->|"acabaram os 3 regimes"| fim["devolve linhas, 3 LinhaDoRelatorio"]

Repare que dois caminhos diferentes chegam ao mesmo 'desconhecido', e que nenhum deles passa por 'nao': a ausência da interface XR e a promessa rejeitada são situações distintas que produzem a mesma informação, que é a de não haver resposta utilizável. O losango seguinte, sobre o valor de suporte, é onde observacaoDe(regime, suporte) transforma cada um dos três em uma frase que o leitor da tabela entende sem consultar o código.

sequenceDiagram
  autonumber
  participant demo as demos/01_demo.ts
  participant ver as modes/verificacao.ts
  participant reg as modes/regimes.ts
  participant xr as navigator.xr
  demo->>ver: levantarRelatorio()
  ver->>reg: le REGIMES
  reg-->>ver: inline, immersive-vr, immersive-ar
  ver->>ver: suporteDe('inline')
  ver->>xr: isSessionSupported('inline')
  xr-->>ver: true
  ver->>ver: observacaoDe(regime, 'sim')
  ver->>ver: suporteDe('immersive-vr')
  ver->>xr: isSessionSupported('immersive-vr')
  xr-->>ver: false
  ver->>ver: observacaoDe(regime, 'nao')
  ver->>ver: suporteDe('immersive-ar')
  ver->>xr: isSessionSupported('immersive-ar')
  xr-->>ver: false
  ver->>ver: observacaoDe(regime, 'nao')
  ver-->>demo: 3 LinhaDoRelatorio com suporte 'sim', 'nao', 'nao'

Nesta execução navigator.xr existia, e as três respostas voltaram true, false e false, nessa ordem. O desktop do laboratório entra no regime de janela e em mais nenhum. Vale notar o que não aparece no diagrama: nenhuma sessão foi aberta, e nenhuma permissão foi pedida a quem estava diante da página. A pergunta é a mais grossa que a interface aceita, e é a mais grossa que este módulo sustenta.

Capturar a rejeição em vez de deixá-la subir. A alternativa era não escrever o bloco de captura e deixar a promessa rejeitada propagar. O código ficaria três linhas menor e, no desktop, se comportaria exatamente igual — o que é precisamente o problema. O custo aparece no aparelho que rejeita: uma única rejeição no meio do laço derruba levantarRelatorio() inteira, montarRelatorio() nunca é chamada, e a página fica em branco. O aparelho com maior chance de rejeitar é o celular do estudante, que é o que mais varia na turma, e uma página em branco não diz a ele que a resposta faltou; diz que o exemplo do professor não funciona.

1.9.3 O relatório montado por quem não sabe de onde vieram os números

montarRelatorio(raiz, BANCADA, problemas, linhas) recebe os quatro argumentos prontos e não consulta nada. Ela limpa a raiz, monta o bloco do domínio e delega a tabela.

flowchart TD
  ini["montarRelatorio(raiz, BANCADA, problemas, linhas)"] --> limpa["raiz.replaceChildren()"]
  limpa --> bloco["blocoDoDominio(dominio, problemas)"]
  bloco --> conta{"problemas.length === 0"}
  conta -->|"true"| ok["Nenhuma inconsistencia entre pecas e encaixes"]
  conta -->|"false"| lista["Inconsistencias mais problemas.join"]
  ok --> titulo["tituloRegimes entra em raiz"]
  lista --> titulo
  titulo --> tab["tabelaDeRegimes(linhas)"]
  tab --> cab["celula(titulo, true) para os 6 titulos"]
  cab --> fileiras{"para cada linha de linhas"}
  fileiras -->|"5 campos de linha.regime"| campos["celula(texto)"]
  campos --> rot["rotuloDoSuporte(linha.suporte)"]
  rot --> ultima{"switch (suporte)"}
  ultima -->|"case 'sim'"| s1["suportado"]
  ultima -->|"case 'nao'"| s2["nao suportado"]
  ultima -->|"case 'desconhecido'"| s3["sem resposta"]
  s1 --> junta["celula(rotulo mais linha.observacao)"]
  s2 --> junta
  s3 --> junta
  junta --> fileiras
  fileiras -->|"acabaram as 3 linhas"| fim["raiz.appendChild(tabela)"]

rotuloDoSuporte(linha.suporte) é um switch sobre os três valores, sem ramo padrão. Isso não é descuido: a união literal tem exatamente três habitantes, e o compilador confere que os três estão cobertos.

sequenceDiagram
  autonumber
  participant demo as demos/01_demo.ts
  participant rel as relatorio/relatorio.ts
  participant com as relatorio/comum.ts
  participant doc as document
  demo->>rel: montarRelatorio(raiz, BANCADA, problemas, linhas)
  rel->>rel: blocoDoDominio(BANCADA, [])
  rel->>doc: document.createElement('section')
  doc-->>rel: 5 pecas e 5 encaixes declarados, nenhuma inconsistencia
  rel->>rel: tabelaDeRegimes(linhas)
  rel->>com: celula(titulo, true), 6 vezes
  com-->>rel: 6 th no cabecalho
  rel->>rel: rotuloDoSuporte('sim')
  rel-->>rel: suportado
  rel->>rel: rotuloDoSuporte('nao')
  rel-->>rel: nao suportado
  rel->>com: celula(texto), 18 vezes
  com-->>rel: 18 td nas 3 fileiras
  rel-->>demo: void, com o relatorio ja na pagina

São seis chamadas a celula(titulo, true) no cabeçalho e dezoito a celula(texto) nas três fileiras, e é essa contagem que explica por que a célula saiu para arquivo próprio antes de haver uma segunda tabela para justificá-la.

A apresentação sem ramo padrão, e o que isso custou. A alternativa era um if com um retorno final de reserva, que nunca falha e sempre compila. O custo chega no módulo seguinte, quando a sonda de capacidades quiser um quarto estado: com o ramo padrão, o valor novo apareceria na tabela com o rótulo de reserva, sem aviso nenhum; sem ele, o compilador aponta a linha exata que ficou de fora.

A apresentação que não consulta nada, e o que isso custou. A alternativa era esta função chamar levantarRelatorio() ela mesma, poupando dois parâmetros e uma linha no ponto de entrada. O custo se paga adiante: o painel definitivo é diegético, e no dia em que ele substituir esta folha, a consulta ao aparelho iria junto para dentro da cena — e o que hoje é uma pergunta feita uma vez, na carga da página, viraria uma pergunta feita de dentro do laço de quadro.

1.10 O que precisa estar verdadeiro antes do módulo seguinte

Verificação Como se confere
A tarefa do domínio cabe em uma frase leitura do campo correspondente
O estado final é observável na cena leitura, sem consultar variável interna
Nenhuma peça ou encaixe fica órfão a função de inconsistências devolve lista vazia
Os três regimes diferem em pelo menos um campo cada comparação das três linhas entre si
O código analisa limpo em modo estrito verificação de tipos do projeto, sem emissão
A página responde diferente em aparelhos diferentes abrir a mesma URL no desktop, no celular e no visor
As quatorze seções existem, e nenhuma está vazia leitura do sumário de docs/especificacao.md
Toda frase do documento pode estar errada leitura frase a frase, procurando o que não afirma nada
A tabela de regimes do documento é a do código comparação campo a campo entre a seção 9 e a declaração
O inventário, as escalas e o orçamento não se contradizem as duas contas das seções 3, 4 e 10

A última linha costuma falhar por um motivo que não é de código. Sem contexto seguro, a API não existe e os três aparelhos respondem igual, com a mesma cara de quem não entendeu a pergunta. Quem descobre isso na véspera da aula de imersão perde a aula inteira, e é por isso que o servidor de desenvolvimento já sobe com certificado desde o andaime de ambiente.

O que fica pronto ao fim deste módulo é modesto por escolha: uma cena escolhida com a razão escrita, um domínio delimitado, três regimes declarados, uma resposta honesta sobre o que este aparelho faz e um documento de quatorze seções que diz, com números, o que tudo isso terá de ser. Nada disso desenha. Tudo isso será contradito, campo por campo, pelo comportamento que vem adiante — e foi escrito para poder ser contradito. Um grupo que chegue ao fim do percurso com a declaração do primeiro módulo intacta não acertou tudo na primeira tentativa; provavelmente parou de conferir.