1 Dispositivos, rastreamento e graus de liberdade — 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

Segundo módulo, segundo artefato que não desenha um triângulo. Desta vez é o assunto que exige isso.

O módulo anterior perguntou ao navegador quais regimes ele dizia suportar, e a pergunta foi feita de fora de qualquer sessão. A resposta que voltou era grossa e tinha três valores: sim, não, não sei. Aqui a sessão abre de verdade, e o que se descobre lá dentro tem outra granularidade — quais espaços de referência o aparelho entrega quando alguém os pede, quais recursos opcionais foram concedidos, o que a plataforma faz com o fundo, quantos quadros chegam sem que o aparelho saiba onde está a cabeça de quem observa.

A distância entre as duas respostas é o conteúdo deste módulo. Um aparelho declara suporte a um modo e, uma vez dentro dele, nega metade do que se pediu, sem justificativa — a especificação não obriga ninguém a dar uma. Quem projeta contra o que supõe existir descobre o engano três módulos adiante, quando o teleporte não encontra chão e o rastro do defeito já passou por quatro arquivos.

As duas tarefas do módulo resolvem exatamente isso. A primeira constrói a consulta e guarda o resultado numa estrutura que o resto do ambiente possa ler. A segunda coloca esse resultado onde alguém consiga lê-lo com o aparelho no rosto, que é uma condição menos óbvia do que parece e mais decisiva do que a primeira.

Além delas, o módulo tem teoria que não cabe em tarefa nenhuma: graus de liberdade, a diferença entre rastrear por dentro e rastrear por fora, e o mapeamento simultâneo do ambiente. Nenhuma das três se resolve com uma função, e as três aparecem abaixo — duas encarnadas em código que já existe por outro motivo, uma declarada como tópico sem implementação em vez de disfarçada num exemplo fabricado.

1.2 Graus de liberdade, e o que a plataforma se recusa a responder

Seis graus de liberdade: três de rotação, três de translação. O visor que acompanha apenas os três primeiros gira com a cabeça de quem o veste e ignora o passo à frente — a cena permanece imóvel enquanto o corpo se desloca, e o desacordo entre o que o ouvido interno relata e o que os olhos veem produz mal-estar em minutos. É o defeito da geração de visores que fez a área ganhar má fama antes de merecê-la.

Agora a parte incômoda. A API não informa quantos graus o aparelho rastreia. Não há propriedade a ler, e a omissão tem razão: o navegador não sabe o que o sensor faz, sabe o que o sistema do aparelho aceitou entregar. O que sobra para inferir é o conjunto de espaços de referência concedidos, e a inferência tem limite.

a regra da inferência, em src/bancada/devices/graus.ts
/**
 * Regra da inferência, escrita por extenso porque é ela que você precisa
 * poder contestar:
 *
 * - `local-floor` ou `bounded-floor` concedidos exigem que o aparelho saiba onde
 *   está o chão em relação a quem observa. Um visor que só gira não tem como
 *   sustentar isso, então a concessão é evidência forte de posição rastreada.
 * - `viewer` sozinho é o mínimo que qualquer sessão entrega: a origem acompanha
 *   quem observa, e translação alguma é observável a partir dela. Evidência forte
 *   da ausência.
 * - `local` no meio do caminho é ambíguo de verdade. A especificação o descreve
 *   como origem próxima de quem observa no início da sessão, e um aparelho de três
 *   graus pode concedê-lo mantendo a posição sempre na origem. Daí
 *   `indeterminado`, e não uma aposta.
 */

A regra está escrita por extenso dentro do próprio arquivo, e essa escolha é de projeto: quem estuda o código precisa poder contestar a inferência, e para contestá-la precisa vê-la. Um espaço de referência com origem no chão exige que o aparelho saiba onde o chão está em relação a quem observa, e um visor que apenas gira não tem como sustentar isso. Um espaço cuja origem acompanha quem observa é o mínimo que qualquer sessão entrega, e translação alguma se observa a partir dele.

O caso do meio é o que decide a qualidade do relatório. O espaço local, com origem próxima de quem observa no início da sessão, pode ser concedido por um aparelho de três graus que mantenha a posição sempre na origem. A evidência ali é fraca dos dois lados, e a função devolve isso em vez de escolher um lado.

a inferência, em src/bancada/devices/graus.ts
export function grausDeLiberdade(leitura: LeituraDeEspacos): GrausDeLiberdade {
  const temChao: boolean =
    leitura.concedidos.includes('local-floor') ||
    leitura.concedidos.includes('bounded-floor') ||
    leitura.concedidos.includes('unbounded');
  if (temChao) {
    return 'seis';
  }
  if (leitura.concedidos.length === 1 && leitura.concedidos[0] === 'viewer') {
    return 'tres';
  }
  return 'indeterminado';
}

O terceiro valor de retorno é a decisão que mais rende no módulo inteiro. Um relatório que afirma “seis graus de liberdade” sobre evidência fraca será citado por quem o ler; um relatório que diz “daqui não dá para saber” será verificado. A segunda reação é a que se quer produzir, e ela custa um caso a mais no tipo de retorno.

O arquivo completo classifica também a classe do aparelho, e o faz sem consultar a cadeia de identificação do navegador. Aquela cadeia é editável por quem usa, imitada por aparelhos que querem passar por outros e envelhece a cada versão do sistema — construir sobre ela um relatório de capacidades seria escolher a fonte menos confiável disponível havendo uma melhor à mão. O que a sessão concede é o que o aparelho faz agora, na mão de quem está usando.

src/bancada/devices/graus.ts
// ---------------------------------------------------------------------------
// Graus de liberdade e classe do aparelho, inferidos do que foi concedido.
//
// A API XR não expõe um número de graus de liberdade. Não há propriedade a ler,
// e isso não é omissão: o navegador não sabe o que o sensor faz, sabe o que o
// runtime do aparelho aceitou entregar. O que existe para inferir é o conjunto de
// espaços de referência concedidos, e a inferência tem limites que este arquivo
// registra em vez de esconder.
//
// O porquê de a inferência ser conservadora: um relatório que afirma "seis graus
// de liberdade" com base em evidência fraca é pior que um relatório que diz "não
// dá para saber daqui". O primeiro será citado; o segundo, verificado.
// ---------------------------------------------------------------------------

/**
 * O que se pode afirmar sobre o rastreamento de posição a partir do que a sessão
 * concedeu — três graus (só orientação) contra seis (orientação e posição).
 */
export type GrausDeLiberdade = 'tres' | 'seis' | 'indeterminado';

/**
 * Classe do aparelho deduzida da capacidade declarada, e não do nome que o
 * navegador diz ter.
 *
 * A escolha é deliberada e é o conteúdo do módulo: a cadeia de identificação do
 * navegador é editável, imitada por outros aparelhos e envelhece a cada versão.
 * O que a sessão concede é o que o aparelho faz agora, na mão de quem está
 * usando.
 */
export type ClasseDeAparelho =
  | 'sem-api'
  | 'somente-janela'
  | 'visor-sem-posicao'
  | 'visor-com-posicao'
  | 'aparelho-de-mao-com-camera';

export interface LeituraDeEspacos {
  /** Espaços de referência que a sessão de fato entregou quando pedidos. */
  readonly concedidos: readonly string[];
  /** O modo de sessão em que a leitura foi feita. */
  readonly modo: 'immersive-vr' | 'immersive-ar';
  /** Havia alguma fonte de entrada com pose de punho declarada. */
  readonly comPoseDePunho: boolean;
}

// recorte:inicio regra-da-inferencia-por-extenso
/**
 * Regra da inferência, escrita por extenso porque é ela que o estudante precisa
 * poder contestar:
 *
 * - `local-floor` ou `bounded-floor` concedidos exigem que o aparelho saiba onde
 *   está o chão em relação a quem observa. Um visor que só gira não tem como
 *   sustentar isso, então a concessão é evidência forte de posição rastreada.
 * - `viewer` sozinho é o mínimo que qualquer sessão entrega: a origem acompanha
 *   quem observa, e translação alguma é observável a partir dela. Evidência forte
 *   da ausência.
 * - `local` no meio do caminho é ambíguo de verdade. A especificação o descreve
 *   como origem próxima de quem observa no início da sessão, e um aparelho de três
 *   graus pode concedê-lo mantendo a posição sempre na origem. Daí
 *   `indeterminado`, e não uma aposta.
 */
// recorte:fim regra-da-inferencia-por-extenso
// recorte:inicio inferir-graus-sem-apostar
export function grausDeLiberdade(leitura: LeituraDeEspacos): GrausDeLiberdade {
  const temChao: boolean =
    leitura.concedidos.includes('local-floor') ||
    leitura.concedidos.includes('bounded-floor') ||
    leitura.concedidos.includes('unbounded');
  if (temChao) {
    return 'seis';
  }
  if (leitura.concedidos.length === 1 && leitura.concedidos[0] === 'viewer') {
    return 'tres';
  }
  return 'indeterminado';
}
// recorte:fim inferir-graus-sem-apostar

/**
 * Classifica o aparelho pela combinação de modo de sessão, posição rastreada e
 * tipo de mira das fontes de entrada.
 *
 * `modosSuportados` vem da consulta feita sem sessão alguma — a mesma do módulo
 * anterior —, e por isso esta função responde mesmo quando nenhuma sessão chegou
 * a abrir.
 */
export function classificarAparelho(
  modosSuportados: readonly string[],
  graus: GrausDeLiberdade,
  temApiXr: boolean,
): ClasseDeAparelho {
  if (!temApiXr) {
    return 'sem-api';
  }
  const suportaVr: boolean = modosSuportados.includes('immersive-vr');
  const suportaAr: boolean = modosSuportados.includes('immersive-ar');

  if (!suportaVr && !suportaAr) {
    return 'somente-janela';
  }
  // Aparelho que faz realidade aumentada e não faz sessão imersiva completa é o
  // celular: a câmera vê o mundo, mas ninguém veste a tela no rosto.
  if (suportaAr && !suportaVr) {
    return 'aparelho-de-mao-com-camera';
  }
  return graus === 'tres' ? 'visor-sem-posicao' : 'visor-com-posicao';
}

/** Frase curta e legível para cada classe, usada no relatório. */
export function descreverClasse(classe: ClasseDeAparelho): string {
  switch (classe) {
    case 'sem-api':
      return 'Navegador sem a API XR, ou página fora de contexto seguro.';
    case 'somente-janela':
      return 'Aparelho que só sustenta o regime de janela — é o caso do desktop do laboratório.';
    case 'visor-sem-posicao':
      return 'Visor que acompanha a rotação da cabeça e não acompanha o deslocamento.';
    case 'visor-com-posicao':
      return 'Visor que acompanha rotação e deslocamento, com o chão do ambiente como referência.';
    case 'aparelho-de-mao-com-camera':
      return 'Aparelho de mão que compõe o virtual sobre a imagem da própria câmera.';
  }
}

1.3 De onde vem a pose: por dentro e por fora

Há dois arranjos para descobrir onde um aparelho está, e a diferença entre eles decide preço, mobilidade e modo de falhar.

No rastreamento por infraestrutura externa, o ambiente é instrumentado: emissores fixos nas paredes, câmeras montadas em tripés ou, no caso mais antigo de todos, um braço mecânico preso ao teto. O ancestral direto do arranjo é o visor que Ivan Sutherland montou em 1968, pesado a ponto de precisar ficar suspenso do teto do laboratório por um braço mecânico articulado. O braço não estava ali apenas para segurar peso: ele media a posição da cabeça de quem vestia o aparelho. Geometria rígida conhecida no lugar de sensor que estima, com a precisão paga em imobilidade — e um apelido, “espada de Dâmocles”, que descreve com exatidão a imagem de quem entrava embaixo dele.

No rastreamento por sensores embarcados, o aparelho olha para fora e se localiza sozinho, cruzando o que as câmeras veem com o que acelerômetros e giroscópios relatam. Ele anda para onde quiser e dispensa sala preparada. Em troca, erra quando o ambiente não coopera, e os três aparelhos do laboratório são todos deste tipo.

Este tópico não vira código, e forçar um exemplo executável para ele seria fabricar conteúdo. A API XR não expõe o arranjo de rastreamento; expõe o resultado. O papel dele no projeto está na seção seguinte, como explicação do que a sonda mede: um aparelho que se localiza sozinho tem como se perder, e o braço de 1968 não tinha.

1.4 O cartógrafo no escuro, e o quadro em que ele se perde

O aparelho desenha a planta da sala enquanto atravessa a sala, e corrige o desenho toda vez que reconhece um canto por onde já passou.

A frase resolve um círculo aparente. Para saber onde está, o aparelho precisa do mapa; para desenhar o mapa, precisa saber onde está. A saída é fazer as duas coisas ao mesmo tempo, aceitando que ambas sejam estimativas revisáveis: a posição é a melhor aposta atual, o mapa é a melhor aposta atual, e o reencontro de um canto já visto corrige as duas retroativamente. Nada disso a plataforma entrega diretamente. O que ela entrega são os efeitos — o raio lançado contra uma superfície real, os planos reconhecidos no ambiente, a âncora que o aparelho corrige sozinho quando muda de ideia sobre onde as coisas estavam.

O cartógrafo também se perde, e é aqui que o módulo cobra a antecipação da falha. Superfície sem textura não oferece canto a reconhecer; iluminação pobre não oferece contraste para extrair canto algum; movimento brusco borra a imagem e o canto some entre um quadro e o seguinte. Uma parede branca bem iluminada é o pior cenário possível para um aparelho que se orienta por cantos, o que faz do laboratório recém-pintado um adversário à altura.

O sintoma disso se lê de dentro do código, sem instrumento e sem sensor extra: o quadro chega e a pose de quem observa não vem. É o único indício de degradação que a plataforma oferece, e por isso ele virou contagem.

src/bancada/devices/estabilidade.ts
// ---------------------------------------------------------------------------
// A falha como medida, e não como anedota.
//
// A pose de quem observa pode simplesmente não vir. A API prevê isso: o quadro
// entrega a pose ou entrega nada, e nada significa que o aparelho perdeu, naquele
// instante, a conta de onde está. É o único sintoma de degradação de rastreamento
// que se lê de dentro do código, sem instrumento e sem sensor extra.
//
// Este contador existe porque o módulo cobra prever a falha, e previsão que não
// se confronta com contagem alguma é opinião. Ele é deliberadamente burro: conta
// quadros com pose e quadros sem, e mais nada. A interpretação fica na função de
// diagnóstico, separada de propósito — misturar contagem e julgamento é o que
// produz medidor que sempre concorda com quem o escreveu.
// ---------------------------------------------------------------------------

export interface Estabilidade {
  readonly quadros: number;
  readonly quadrosSemPose: number;
  /** Maior sequência ininterrupta de quadros sem pose. */
  readonly maiorLacuna: number;
  /** Quadros descartados por a sessão não estar em primeiro plano. */
  readonly quadrosOcultos: number;
}

// recorte:inicio perda-de-pose-nao-e-menu-aberto
/**
 * Acumula a contagem quadro a quadro.
 *
 * O parâmetro `visivel` é o que separa perda de rastreamento de sessão que saiu
 * de foco. Quando quem usa abre o menu do sistema do visor, a sessão continua
 * viva, os quadros continuam chegando e a pose deixa de vir — e contar isso como
 * falha de sensor acusaria o aparelho de um defeito que é comportamento normal
 * do sistema operacional.
 */
export class ContadorDeEstabilidade {
  private quadros: number = 0;
  private quadrosSemPose: number = 0;
  private quadrosOcultos: number = 0;
  private maiorLacuna: number = 0;
  private lacunaCorrente: number = 0;

  public registrar(temPose: boolean, visivel: boolean): void {
// recorte:fim perda-de-pose-nao-e-menu-aberto
    this.quadros += 1;

    if (!visivel) {
      this.quadrosOcultos += 1;
      this.lacunaCorrente = 0;
      return;
    }

    if (temPose) {
      this.lacunaCorrente = 0;
      return;
    }

    this.quadrosSemPose += 1;
    this.lacunaCorrente += 1;
    if (this.lacunaCorrente > this.maiorLacuna) {
      this.maiorLacuna = this.lacunaCorrente;
    }
  }

  public resultado(): Estabilidade {
    return {
      quadros: this.quadros,
      quadrosSemPose: this.quadrosSemPose,
      maiorLacuna: this.maiorLacuna,
      quadrosOcultos: this.quadrosOcultos,
    };
  }
}

/**
 * Traduz a contagem numa frase, com as condições que a bibliografia da disciplina
 * aponta como causas de degradação de rastreamento óptico: superfície sem textura
 * (não há canto a reconhecer), iluminação pobre (não há contraste para extrair
 * canto algum) e movimento brusco (a imagem borra e o canto some entre quadros).
 *
 * A frase não afirma qual das três aconteceu. Não dá para saber daqui, e escolher
 * uma seria inventar o diagnóstico junto com a medida.
 */
export function diagnosticar(estabilidade: Estabilidade): string {
  if (estabilidade.quadros === 0) {
    return 'Nenhum quadro foi entregue — a sessão não chegou a produzir imagem.';
  }
  if (estabilidade.quadrosSemPose === 0) {
    return 'A pose veio em todos os quadros observados. A janela de observação é curta, e ausência de falha aqui não é promessa de estabilidade em uso prolongado.';
  }
  const proporcao: number = Math.round(
    (estabilidade.quadrosSemPose / estabilidade.quadros) * 100,
  );
  return (
    `A pose faltou em ${proporcao}% dos quadros, com lacuna máxima de ` +
    `${estabilidade.maiorLacuna} quadros seguidos. As causas prováveis são superfície ` +
    'sem textura, iluminação pobre ou movimento brusco — e daqui não se distingue qual delas.'
  );
}

O contador é burro por decisão. Ele conta quadros com pose e quadros sem pose, e mais nada; o julgamento fica numa função separada. Misturar medição e interpretação no mesmo lugar produz o medidor que sempre concorda com quem o escreveu, e um medidor assim não serve de argumento contra ninguém — a começar por quem o programou.

a distinção que salva o diagnóstico, em src/bancada/devices/estabilidade.ts
/**
 * Acumula a contagem quadro a quadro.
 *
 * O parâmetro `visivel` é o que separa perda de rastreamento de sessão que saiu
 * de foco. Quando quem usa abre o menu do sistema do visor, a sessão continua
 * viva, os quadros continuam chegando e a pose deixa de vir — e contar isso como
 * falha de sensor acusaria o aparelho de um defeito que é comportamento normal
 * do sistema operacional.
 */
export class ContadorDeEstabilidade {
  private quadros: number = 0;
  private quadrosSemPose: number = 0;
  private quadrosOcultos: number = 0;
  private maiorLacuna: number = 0;
  private lacunaCorrente: number = 0;

  public registrar(temPose: boolean, visivel: boolean): void {

O parâmetro de visibilidade separa perda de rastreamento de sessão que saiu de foco. Quando quem usa abre o menu do sistema do visor, a sessão continua viva, os quadros continuam chegando e a pose deixa de vir. Contar isso como falha de sensor acusaria o aparelho de um defeito que é comportamento normal do sistema operacional, e o relatório passaria a reprovar o visor toda vez que alguém conferisse a hora.

A frase de diagnóstico também se recusa a apontar qual das três causas ocorreu. Daqui não se distingue textura de luz e de movimento, e escolher uma seria inventar o diagnóstico junto com a medida.

1.5 Tarefa 1: Construir a sonda de capacidades

Enunciado da tarefa

A primeira peça executável do ambiente não desenha coisa alguma. Ela pergunta ao aparelho o que ele oferece — que tipos de sessão suporta, que recursos opcionais concede, que fontes de entrada declara, quantos graus de liberdade rastreia — e guarda a resposta numa estrutura que o resto do ambiente possa consultar.

O que fica pronto é essa consulta feita de verdade contra o aparelho, com a distinção preservada entre recurso ausente e recurso negado. Assumir capacidade que o aparelho nunca declarou é a origem da maior parte das falhas silenciosas dos capítulos seguintes.

1.5.1 A distinção que o enunciado cobra, e que a API não entrega pronta

O enunciado pede que recurso ausente e recurso negado sejam tratados de forma diferente. A plataforma não ajuda: os dois passam pela mesma lista de recursos opcionais no pedido de sessão e, depois, simplesmente não aparecem entre os concedidos. A distinção precisa ser feita à mão, e é o que este arquivo faz.

os três estados, em src/bancada/devices/recursos.ts
/**
 * O que se sabe sobre um recurso depois de a sessão abrir.
 *
 * - `concedido`: o nome está em `enabledFeatures`, e o recurso pode ser usado.
 * - `negado`: a sessão reportou a lista e o nome não está nela — o aparelho ou o
 *   navegador recusou, e a razão não é exposta.
 * - `indeterminado`: a sessão não reportou lista alguma. Não é negativa; é
 *   ausência de resposta, e tratá-la como negativa produz relatório confiante e
 *   errado.
 */
export type EstadoDeRecurso = 'concedido' | 'negado' | 'indeterminado';

O terceiro estado dá mais trabalho que os dois primeiros somados e evita mais erro que eles. A sessão só informa o que concedeu através de uma propriedade que a especificação declara opcional — um navegador pode entrar em sessão sem dizer o que ligou. Sem o terceiro estado, esse navegador apareceria no relatório como aparelho que negou tudo, e o grupo passaria a tarde caçando um defeito de hardware inexistente.

O catálogo de recursos consultados carrega, ao lado de cada nome, o motivo de ele estar ali.

o recurso e o motivo, em src/bancada/devices/recursos.ts
/** Um recurso opcional da API XR, com o motivo de ele estar no catálogo. */
export interface RecursoOpcional {
  /** O nome exato aceito por `optionalFeatures` — não traduzir. */
  readonly nome: string;
  /** O que ele habilita no percurso, em uma frase. */
  readonly paraQueServe: string;
}

O campo do motivo parece cortesia e é trava. Ele obriga quem acrescenta um recurso ao catálogo a escrever para que ele serve no percurso, e um recurso sem resposta a essa pergunta é um recurso que entrou porque o nome soava interessante. A lista é curta pela mesma razão: pedir tudo o que a especificação prevê faria a sonda demorar mais e algumas plataformas recusarem a sessão inteira por causa de um item exótico.

Um recurso ficou de fora com justificativa escrita no arquivo. A detecção de profundidade exige um dicionário próprio de configuração dentro do pedido de sessão, e um pedido malformado derruba a sessão toda em vez de negar apenas aquele item. Consultá-lo custaria o risco de perder tudo o mais, o que é um preço alto por uma linha de relatório.

src/bancada/devices/recursos.ts
// ---------------------------------------------------------------------------
// Catálogo dos recursos opcionais que a sonda consulta, e a classificação do
// que o aparelho respondeu sobre cada um.
//
// Este arquivo existe para separar duas coisas que o vocabulário corrente
// confunde: o recurso que o aparelho NÃO TEM e o recurso que ele TEM e NÃO
// CONCEDEU. A API XR trata os dois de um jeito só na hora de pedir — passam
// ambos por `optionalFeatures` e simplesmente não aparecem depois —, e é por
// isso que a distinção precisa ser feita aqui, à mão, em vez de esperada da
// plataforma.
//
// O terceiro estado é o que mais dá trabalho e o que mais evita erro:
// `indeterminado`. A sessão só reporta o que concedeu através de
// `enabledFeatures`, e essa propriedade é opcional na especificação — um
// navegador pode entrar em sessão sem dizer o que ligou. Sem o terceiro estado,
// esse navegador apareceria no relatório como aparelho que negou tudo.
// ---------------------------------------------------------------------------

// recorte:inicio ausente-negado-e-a-terceira-coisa
/**
 * O que se sabe sobre um recurso depois de a sessão abrir.
 *
 * - `concedido`: o nome está em `enabledFeatures`, e o recurso pode ser usado.
 * - `negado`: a sessão reportou a lista e o nome não está nela — o aparelho ou o
 *   navegador recusou, e a razão não é exposta.
 * - `indeterminado`: a sessão não reportou lista alguma. Não é negativa; é
 *   ausência de resposta, e tratá-la como negativa produz relatório confiante e
 *   errado.
 */
export type EstadoDeRecurso = 'concedido' | 'negado' | 'indeterminado';
// recorte:fim ausente-negado-e-a-terceira-coisa

// recorte:inicio recurso-com-motivo-no-catalogo
/** Um recurso opcional da API XR, com o motivo de ele estar no catálogo. */
export interface RecursoOpcional {
  /** O nome exato aceito por `optionalFeatures` — não traduzir. */
  readonly nome: string;
  /** O que ele habilita no percurso, em uma frase. */
  readonly paraQueServe: string;
}
// recorte:fim recurso-com-motivo-no-catalogo

/**
 * Os recursos que a Bancada consulta.
 *
 * A lista é curta de propósito: pedir tudo o que a especificação prevê faria a
 * sonda demorar mais e algumas plataformas recusarem a sessão inteira por causa
 * de um item exótico. Cada entrada aqui é um recurso de que algum módulo adiante
 * realmente depende.
 *
 * `depth-sensing` ficou de fora, e a razão é técnica: ele exige um dicionário de
 * configuração próprio no pedido de sessão (formato de dado e ordem de uso), e um
 * pedido malformado derruba a sessão inteira em vez de apenas negar o recurso.
 * Consultá-lo custaria, aqui, o risco de perder tudo o mais.
 */
export const RECURSOS_CONSULTADOS: readonly RecursoOpcional[] = [
  {
    nome: 'local-floor',
    paraQueServe:
      'origem no chão do espaço físico — é o que faz a bancada nascer na altura certa',
  },
  {
    nome: 'bounded-floor',
    paraQueServe:
      'origem no chão mais os limites da área livre que o aparelho conhece',
  },
  {
    nome: 'unbounded',
    paraQueServe: 'espaço sem fronteira declarada, para percursos longos',
  },
  {
    nome: 'hit-test',
    paraQueServe:
      'lançar um raio contra as superfícies reais que o aparelho encontrou',
  },
  {
    nome: 'anchors',
    paraQueServe:
      'prender um objeto virtual a um ponto do mapa e deixar o aparelho corrigi-lo',
  },
  {
    nome: 'plane-detection',
    paraQueServe: 'receber os planos que o aparelho reconheceu no ambiente',
  },
  {
    nome: 'hand-tracking',
    paraQueServe:
      'pose das mãos sem controle — fora do núcleo do percurso, e consultado só para registro',
  },
];

/**
 * Classifica um recurso contra a lista que a sessão reportou.
 *
 * `concedidos` vem de `XRSession.enabledFeatures`, que é opcional na
 * especificação: `undefined` significa "esta sessão não diz", e é o que produz
 * `indeterminado`.
 */
export function estadoDoRecurso(
  nome: string,
  concedidos: readonly string[] | undefined,
): EstadoDeRecurso {
  if (concedidos === undefined) {
    return 'indeterminado';
  }
  return concedidos.includes(nome) ? 'concedido' : 'negado';
}

1.5.2 A sonda, e as duas restrições que moldam o arquivo inteiro

Duas restrições da plataforma decidem a forma deste arquivo, e nenhuma delas se contorna.

A primeira: metade das respostas só existe dentro de uma sessão. Recursos concedidos, modo de composição do fundo, espaços de referência entregues e fontes de entrada são propriedades da sessão, não do navegador. Sondar de fora devolve uma lista de suposições bem formatadas.

A segunda: abrir sessão imersiva exige gesto de quem usa. O navegador recusa o pedido que não venha de um clique, e a recusa chega como exceção com nome em inglês — o disfarce perfeito para um defeito de código que não existe. Daí a sonda ser uma função chamada por botão, em vez de algo que roda ao carregar a página.

src/bancada/devices/sonda.ts
// ---------------------------------------------------------------------------
// A sonda de capacidades — a Tarefa 1 deste módulo.
//
// Ela pergunta ao aparelho o que ele oferece e guarda a resposta numa estrutura
// que o resto do ambiente possa consultar. Não desenha, não carrega malha e não
// monta cena: o que ela produz é conhecimento sobre o aparelho, e é isso que os
// módulos seguintes consomem para decidir o que sequer tentar.
//
// Duas restrições da plataforma moldam o arquivo inteiro, e nenhuma delas é
// contornável:
//
// 1. Metade das respostas só existe DENTRO de uma sessão. Recursos concedidos,
//    modo de composição do fundo, espaços de referência entregues e fontes de
//    entrada são propriedades da sessão, não do navegador. Sondar de fora
//    devolve uma lista de suposições.
// 2. Abrir sessão imersiva exige gesto de quem usa. O navegador recusa o pedido
//    que não venha de um clique, e a recusa se parece com defeito do código.
//    Daí a sonda ser função chamada por botão, e não coisa que roda ao carregar.
// ---------------------------------------------------------------------------

import { REGIMES, type Regime, type RegimeId } from '../modes/regimes';
import { levantarRelatorio, type LinhaDoRelatorio } from '../modes/verificacao';
import {
  RECURSOS_CONSULTADOS,
  estadoDoRecurso,
  type EstadoDeRecurso,
} from './recursos';
import {
  ContadorDeEstabilidade,
  diagnosticar,
  type Estabilidade,
} from './estabilidade';
import {
  classificarAparelho,
  grausDeLiberdade,
  type ClasseDeAparelho,
  type GrausDeLiberdade,
} from './graus';

/** Modos em que uma sessão de sondagem pode ser aberta. */
export type ModoSondavel = 'immersive-vr' | 'immersive-ar';

/** Espaços de referência que a sonda tenta obter, do mais exigente ao mínimo. */
const ESPACOS_TENTADOS: readonly XRReferenceSpaceType[] = [
  'bounded-floor',
  'local-floor',
  'unbounded',
  'local',
  'viewer',
];

/** Quantos quadros a sonda observa antes de encerrar a sessão. */
const QUADROS_OBSERVADOS: number = 90;

export interface RecursoSondado {
  readonly nome: string;
  readonly paraQueServe: string;
  readonly estado: EstadoDeRecurso;
}

export interface FonteDeEntradaSondada {
  /** Lado declarado: `left`, `right` ou `none`. */
  readonly lado: string;
  /** Como a mira é produzida: raio de controle, olhar ou toque na tela. */
  readonly mira: string;
  /** Há pose de punho — objeto rastreado no espaço, não apenas uma direção. */
  readonly temPoseDePunho: boolean;
  /** Há pose de mão articulada. */
  readonly temMao: boolean;
  /** Perfis declarados pelo aparelho, do mais específico ao mais genérico. */
  readonly perfis: readonly string[];
}

/** O que a sonda descobre sem abrir sessão alguma. */
export interface SondaSemSessao {
  readonly temApiXr: boolean;
  readonly contextoSeguro: boolean;
  readonly regimes: readonly LinhaDoRelatorio[];
  readonly modosSuportados: readonly string[];
}

/** O que só a sessão responde. */
export interface SondaEmSessao {
  readonly modo: ModoSondavel;
  readonly recursos: readonly RecursoSondado[];
  readonly espacosConcedidos: readonly string[];
  readonly composicaoObservada: XREnvironmentBlendMode;
  readonly fontesDeEntrada: readonly FonteDeEntradaSondada[];
  readonly graus: GrausDeLiberdade;
  readonly estabilidade: Estabilidade;
  readonly diagnostico: string;
}

export interface ResultadoDaSonda {
  readonly semSessao: SondaSemSessao;
  readonly emSessao: SondaEmSessao | undefined;
  /** Por que não houve sessão, quando não houve. */
  readonly motivoSemSessao: string | undefined;
  readonly classe: ClasseDeAparelho;
}

// A API XR não é exposta fora de contexto seguro. O sintoma é idêntico ao de um
// aparelho sem suporte, e a causa é a URL — o erro de laboratório mais frequente
// do percurso, e o que mais custa tempo de aula por parecer defeito de código.
function contextoSeguro(): boolean {
  return window.isSecureContext;
}

export async function sondarSemSessao(): Promise<SondaSemSessao> {
  const regimes: LinhaDoRelatorio[] = await levantarRelatorio();
  const suportados: string[] = regimes
    .filter((linha) => linha.suporte === 'sim')
    .map((linha) => linha.regime.id);

  return {
    temApiXr: navigator.xr !== undefined,
    contextoSeguro: contextoSeguro(),
    regimes,
    modosSuportados: suportados,
  };
}

/**
 * Tenta obter cada espaço de referência e devolve os que vieram.
 *
 * O pedido rejeita quando o espaço não é concedido, e é essa rejeição que
 * informa — o `catch` vazio aqui não esconde erro algum: ele é a leitura.
 */
async function espacosConcedidos(sessao: XRSession): Promise<string[]> {
  const obtidos: string[] = [];
  for (const tipo of ESPACOS_TENTADOS) {
    try {
      await sessao.requestReferenceSpace(tipo);
      obtidos.push(tipo);
    } catch {
      // Espaço não concedido. É resposta, não falha.
    }
  }
  return obtidos;
}

function lerFontesDeEntrada(sessao: XRSession): FonteDeEntradaSondada[] {
  const fontes: FonteDeEntradaSondada[] = [];
  for (const fonte of sessao.inputSources) {
    fontes.push({
      lado: fonte.handedness,
      mira: fonte.targetRayMode,
      temPoseDePunho: fonte.gripSpace !== undefined,
      temMao: fonte.hand !== undefined,
      perfis: [...fonte.profiles],
    });
  }
  return fontes;
}

/**
 * A camada de composição mínima.
 *
 * A especificação só entrega quadros a uma sessão que tenha superfície de
 * composição declarada. Não desenhamos nada nela: ela é a condição para o laço de
 * quadros existir. A distinção entre a superfície e a cena é exatamente o que
 * este módulo ainda não tem, e declará-la aqui evita que alguém leia este trecho,
 * dois módulos adiante, como início de um renderizador paralelo.
 */
function camadaMinima(sessao: XRSession): void {
  const tela: HTMLCanvasElement = document.createElement('canvas');
  const gl: WebGL2RenderingContext | null = tela.getContext('webgl2', {
    xrCompatible: true,
  });
  if (gl === null) {
    throw new Error('Este navegador não entregou contexto WebGL 2 compatível com XR.');
  }
  sessao.updateRenderState({ baseLayer: new XRWebGLLayer(sessao, gl) });
}

/** Observa alguns quadros, contando os que vieram sem pose de quem observa. */
function observarQuadros(
  sessao: XRSession,
  referencia: XRReferenceSpace,
): Promise<Estabilidade> {
  return new Promise<Estabilidade>((resolver) => {
    const contador: ContadorDeEstabilidade = new ContadorDeEstabilidade();
    let restantes: number = QUADROS_OBSERVADOS;

    const passo: XRFrameRequestCallback = (_tempo: number, quadro: XRFrame): void => {
      const pose: XRViewerPose | undefined = quadro.getViewerPose(referencia);
      contador.registrar(pose !== undefined, sessao.visibilityState === 'visible');
      restantes -= 1;
      if (restantes > 0) {
        sessao.requestAnimationFrame(passo);
        return;
      }
      resolver(contador.resultado());
    };

    sessao.requestAnimationFrame(passo);
  });
}

export async function sondarEmSessao(modo: ModoSondavel): Promise<SondaEmSessao> {
  const xr: XRSystem | undefined = navigator.xr;
  if (xr === undefined) {
    throw new Error('Não há API XR neste navegador.');
  }

  // Todo recurso vai como opcional. Passar qualquer um deles como obrigatório
  // faria o aparelho recusar a sessão inteira por causa de um item — e a sonda
  // perderia justamente a informação que veio buscar.
  const sessao: XRSession = await xr.requestSession(modo, {
    optionalFeatures: RECURSOS_CONSULTADOS.map((recurso) => recurso.nome),
  });

  try {
    camadaMinima(sessao);
    const concedidos: readonly string[] | undefined = sessao.enabledFeatures;
    const espacos: string[] = await espacosConcedidos(sessao);
    const referencia: XRReferenceSpace = await sessao.requestReferenceSpace(
      espacos.includes('local-floor') ? 'local-floor' : 'viewer',
    );
    const estabilidade: Estabilidade = await observarQuadros(sessao, referencia);
    const fontes: FonteDeEntradaSondada[] = lerFontesDeEntrada(sessao);

    return {
      modo,
      recursos: RECURSOS_CONSULTADOS.map((recurso) => ({
        nome: recurso.nome,
        paraQueServe: recurso.paraQueServe,
        estado: estadoDoRecurso(recurso.nome, concedidos),
      })),
      espacosConcedidos: espacos,
      composicaoObservada: sessao.environmentBlendMode,
      fontesDeEntrada: fontes,
      graus: grausDeLiberdade({
        concedidos: espacos,
        modo,
        comPoseDePunho: fontes.some((fonte) => fonte.temPoseDePunho),
      }),
      estabilidade,
      diagnostico: diagnosticar(estabilidade),
    };
  } finally {
    // A sessão precisa terminar mesmo quando a sondagem falha no meio. Sessão
    // imersiva viva com página parada prende o visor numa tela vazia, e quem está
    // com o aparelho no rosto só sai pelo menu do sistema.
    await sessao.end();
  }
}

/** Escolhe o modo mais informativo entre os que o aparelho declara suportar. */
export function modoPreferido(
  modosSuportados: readonly string[],
): ModoSondavel | undefined {
  const ordem: readonly ModoSondavel[] = ['immersive-ar', 'immersive-vr'];
  return ordem.find((modo) => modosSuportados.includes(modo));
}

export async function sondar(): Promise<ResultadoDaSonda> {
  const semSessao: SondaSemSessao = await sondarSemSessao();
  const modo: ModoSondavel | undefined = modoPreferido(semSessao.modosSuportados);

  if (modo === undefined) {
    return {
      semSessao,
      emSessao: undefined,
      motivoSemSessao: semSessao.temApiXr
        ? 'Este aparelho não declara sessão imersiva alguma, e metade da sonda não tem onde acontecer. É informação sobre o aparelho, não defeito do código.'
        : 'Sem API XR neste navegador. Se a página não está em contexto seguro, a causa é a URL, e não o aparelho.',
      classe: classificarAparelho(
        semSessao.modosSuportados,
        'indeterminado',
        semSessao.temApiXr,
      ),
    };
  }

  const emSessao: SondaEmSessao = await sondarEmSessao(modo);
  return {
    semSessao,
    emSessao,
    motivoSemSessao: undefined,
    classe: classificarAparelho(
      semSessao.modosSuportados,
      emSessao.graus,
      semSessao.temApiXr,
    ),
  };
}

/**
 * Confronta a composição declarada no módulo anterior com a que a sessão
 * informou. É o primeiro ponto do percurso em que uma declaração nossa pode ser
 * desmentida pelo aparelho, e o desmentido é o resultado mais valioso dos dois.
 */
export function conferirComposicao(emSessao: SondaEmSessao): string {
  const id: RegimeId = emSessao.modo;
  const regime: Regime | undefined = REGIMES.find((candidato) => candidato.id === id);
  if (regime === undefined) {
    return 'O regime sondado não consta da declaração de regimes.';
  }
  if (regime.composicaoEsperada === emSessao.composicaoObservada) {
    return `A composição declarada (${regime.composicaoEsperada}) foi confirmada pela sessão.`;
  }
  return (
    `Declaramos composição ${regime.composicaoEsperada} e a sessão informou ` +
    `${emSessao.composicaoObservada}. A declaração estava errada, e quem tem razão é o aparelho.`
  );
}

Três decisões dentro do arquivo merecem justificativa. A primeira é passar todo recurso como opcional. Marcar qualquer um deles como obrigatório faria o aparelho recusar a sessão inteira por causa de um item, e a sonda perderia justamente a informação que veio buscar — o levantamento de capacidades morreria pela primeira capacidade ausente.

A segunda é a captura vazia em torno do pedido de espaço de referência. Um bloco que engole exceção costuma ser sinal de descuido; aqui a rejeição é a leitura, e o comentário no código diz isso para que ninguém o remova achando que está limpando sujeira.

A terceira é a camada mínima de composição. A especificação só entrega quadros a uma sessão com superfície de composição declarada, e sem quadros não há como observar a estabilidade da pose. Nada é desenhado nela. Declarar isso em comentário evita que o trecho seja lido, dois módulos adiante, como o começo de um renderizador paralelo ao que ainda vai nascer.

O encerramento da sessão fica num bloco que roda mesmo quando a sondagem falha no meio. A razão é física: sessão imersiva viva com a página parada prende o visor numa tela vazia, e quem está com o aparelho no rosto só escapa pelo menu do sistema.

1.5.3 O confronto que fecha a declaração do módulo anterior

O módulo anterior declarou, por regime, o modo de composição do fundo esperado. Aquele campo foi escrito como expectativa justamente para poder ser desmentido, e agora existe sessão para desmenti-lo.

A função de conferência compara o valor declarado com o que a sessão informou e devolve uma frase. Quando os dois batem, a declaração foi confirmada. Quando não batem, a frase diz sem eufemismo qual dos dois estava errado, e não é o aparelho.

Este é o primeiro ponto do percurso em que uma afirmação nossa é submetida ao veredito da plataforma. O resultado mais valioso dos dois é o desmentido — ele custa uma correção de uma linha agora e evita a construção de dois módulos sobre uma premissa falsa. Um grupo cuja declaração inicial sobreviva ao confronto sem uma correção sequer merece desconfiança antes de merecer aplauso.

1.5.4 Onde é fácil errar, e como conferir

O erro mais comum desta tarefa é sondar de fora e declarar vitória. A consulta sem sessão responde rápido, funciona no computador de mesa e produz uma tabela bonita, o que a torna irresistível. Ela apenas não responde a pergunta do módulo, que é o que o aparelho concede, e não o que ele diz suportar.

O segundo erro é tratar ausência de resposta como negativa, e ele se detecta lendo o relatório num navegador que não reporta a lista de recursos concedidos: se todos aparecem como negados, o terceiro estado não está sendo respeitado em algum ponto do caminho.

A conferência executável cabe em três passos. Abrir a página em contexto seguro e tocar o botão em cada classe de aparelho; comparar as linhas de recurso entre o visor e o celular, que devem divergir; e ler a frase de confronto de composição, que é a única linha do relatório capaz de acusar quem a escreveu.

1.6 Tarefa 2: Tornar o relatório visível

Enunciado da tarefa

Um relatório que só existe no console de depuração serve a quem escreveu o código e a mais ninguém. O resultado da sonda precisa aparecer em algum lugar que qualquer pessoa consiga ler no próprio aparelho.

A tarefa está cumprida quando o mesmo endereço, aberto em aparelhos de classes diferentes, produz relatórios diferentes e legíveis. É o instante em que a palavra dispositivo deixa de ser tópico narrado e passa a ser algo que se lê na tela.

1.6.1 O canal que não existe dentro de um visor

Esta é a metade menos vistosa do módulo e a que mais decide se a aula funciona. Não há console de depuração dentro de um visor. Quem está com o aparelho no rosto não abre painel de desenvolvedor, não vê aviso de rede e não lê exceção — para essa pessoa, um erro registrado no console é indistinguível de um programa que decidiu não fazer nada.

Tudo o que a sondagem tem a dizer precisa aparecer na própria página, em corpo de texto que se leia a um braço de distância.

src/bancada/relatorio/diario.ts
// ---------------------------------------------------------------------------
// O diário da sondagem — o que aconteceu, escrito onde se possa ler.
//
// Este arquivo é a metade menos vistosa da Tarefa 2, e a que mais decide se o
// módulo funciona em sala. O console de depuração não existe dentro de um visor:
// quem está com o aparelho no rosto não abre painel de desenvolvedor, não lê
// aviso de rede e não vê exceção. Tudo o que a sondagem tiver a dizer precisa
// aparecer na própria página, em corpo de texto que se leia a um braço de
// distância.
//
// O espelho no console continua existindo, e não por hábito: quando o aparelho
// está ligado ao computador por depuração remota, o mesmo texto em dois lugares
// é o que permite comparar o que a página mostrou com o que o navegador
// registrou.
// ---------------------------------------------------------------------------

export type Severidade = 'nota' | 'alerta' | 'falha';

export interface Entrada {
  readonly severidade: Severidade;
  readonly texto: string;
}

/**
 * Acumula as entradas e as escreve num elemento da página.
 *
 * O acúmulo é o que permite ao diário existir antes de o elemento existir: a
 * sondagem pode falhar durante o carregamento, e uma mensagem perdida por não ter
 * onde ser escrita é a pior das mensagens.
 */
export class Diario {
  private readonly entradas: Entrada[] = [];
  private destino: HTMLElement | undefined = undefined;

  public fixarDestino(destino: HTMLElement): void {
    this.destino = destino;
    this.redesenhar();
  }

  public nota(texto: string): void {
    this.registrar({ severidade: 'nota', texto });
  }

  public alerta(texto: string): void {
    this.registrar({ severidade: 'alerta', texto });
  }

  public falha(texto: string): void {
    this.registrar({ severidade: 'falha', texto });
  }

  private registrar(entrada: Entrada): void {
    this.entradas.push(entrada);
    // O espelho no console serve à depuração remota, quando o aparelho está
    // ligado ao computador. Nunca é o canal principal.
    console.info(`[bancada:${entrada.severidade}] ${entrada.texto}`);
    this.redesenhar();
  }

  private redesenhar(): void {
    const destino: HTMLElement | undefined = this.destino;
    if (destino === undefined) {
      return;
    }
    destino.replaceChildren();
    for (const entrada of this.entradas) {
      const linha: HTMLParagraphElement = document.createElement('p');
      linha.className = `diario diario-${entrada.severidade}`;
      linha.textContent = entrada.texto;
      destino.appendChild(linha);
    }
  }
}

/**
 * Traduz o que veio de um `catch` em texto legível.
 *
 * A recusa de sessão chega como erro, e o erro cru — nome da classe e uma frase
 * em inglês — é exatamente o que faz um grupo concluir que o código quebrou
 * quando o que houve foi o aparelho dizendo não.
 */
export function explicarFalha(erro: unknown): string {
  if (erro instanceof DOMException && erro.name === 'NotSupportedError') {
    return 'O aparelho recusou a sessão neste modo. Ele não a sustenta, e o pedido foi respondido.';
  }
  if (erro instanceof DOMException && erro.name === 'SecurityError') {
    return 'O navegador recusou o pedido por falta de gesto de quem usa ou por contexto inseguro. O botão precisa ser tocado, e a página precisa estar em conexão cifrada.';
  }
  if (erro instanceof DOMException && erro.name === 'InvalidStateError') {
    return 'Já existe uma sessão aberta neste navegador. Encerre a anterior antes de sondar de novo.';
  }
  if (erro instanceof Error) {
    return `A sondagem parou: ${erro.message}`;
  }
  return 'A sondagem parou por um motivo que o navegador não descreveu.';
}

O diário acumula as entradas antes de saber onde escrevê-las, e o acúmulo tem uso preciso: a sondagem pode falhar durante o carregamento, antes de o elemento de destino existir. Mensagem perdida por não ter onde ser escrita é a pior categoria de mensagem, porque some justamente no caso em que era mais necessária.

O espelho no console continua existindo, e não por hábito. Quando o aparelho está ligado ao computador por depuração remota, o mesmo texto em dois lugares permite comparar o que a página mostrou com o que o navegador registrou. O canal principal, porém, é a página.

A tradução de exceção em frase legível é o outro serviço do arquivo. Recusa de sessão chega como erro, e o erro cru — nome de classe e uma frase em inglês — é exatamente o que faz um grupo concluir que o código quebrou quando o que houve foi o aparelho respondendo que não. Cada nome de exceção previsto vira uma frase que diz o que aconteceu e o que fazer a respeito, incluindo os dois casos de laboratório: falta de gesto e contexto inseguro.

1.6.2 A apresentação, e o que ela ainda não pode ser

O painel definitivo da Bancada é diegético: um cartaz preso à própria bancada, dentro do mundo, lido de dentro do ambiente. Ele continua sem poder existir, pela mesma razão do módulo anterior — não há mundo. Enquanto o grafo de cena não chega, o relatório sai em tabela comum na página.

src/bancada/relatorio/capacidades.ts
import type { EstadoDeRecurso } from '../devices/recursos';
import { descreverClasse, type GrausDeLiberdade } from '../devices/graus';
import type { ResultadoDaSonda, SondaEmSessao } from '../devices/sonda';
import { celula } from './comum';

// ---------------------------------------------------------------------------
// Acréscimo deste módulo: a apresentação da sonda de capacidades.
//
// O relatório de regimes, em relatorio.ts, continua onde estava — ele responde
// "este aparelho entra?", que é a pergunta grossa. O que está neste arquivo
// responde "e uma vez dentro, o que ele concede?", que é a pergunta do módulo de
// dispositivos e que só a sessão responde.
//
// A apresentação aqui é provisória pelo mesmo motivo que a de relatorio.ts: o
// painel definitivo da Bancada é diegético — um cartaz preso à própria bancada,
// lido de dentro do mundo —, e não há mundo enquanto o grafo de cena não chega.
// Até lá, tabela em HTML comum. Vale declarar em vez de esconder: quando a
// bancada existir, é este arquivo que muda de lugar, e nada mais.
// ---------------------------------------------------------------------------

function rotuloDoEstado(estado: EstadoDeRecurso): string {
  switch (estado) {
    case 'concedido':
      return 'concedido';
    case 'negado':
      return 'não concedido';
    case 'indeterminado':
      return 'sem resposta';
  }
}

function rotuloDosGraus(graus: GrausDeLiberdade): string {
  switch (graus) {
    case 'tres':
      return 'três graus de liberdade — o aparelho acompanha para onde a cabeça aponta e não acompanha para onde ela vai';
    case 'seis':
      return 'seis graus de liberdade — o aparelho acompanha orientação e deslocamento';
    case 'indeterminado':
      return 'indeterminado — os espaços concedidos não bastam para afirmar nem uma coisa nem outra';
  }
}

function tabelaDeRecursos(sonda: SondaEmSessao): HTMLTableElement {
  const tabela: HTMLTableElement = document.createElement('table');
  const cabecalho: HTMLTableRowElement = tabela.insertRow();
  for (const titulo of ['Recurso', 'Para que serve', 'Neste aparelho']) {
    cabecalho.appendChild(celula(titulo, true));
  }
  for (const recurso of sonda.recursos) {
    const fileira: HTMLTableRowElement = tabela.insertRow();
    fileira.appendChild(celula(recurso.nome));
    fileira.appendChild(celula(recurso.paraQueServe));
    fileira.appendChild(celula(rotuloDoEstado(recurso.estado)));
  }
  return tabela;
}

function tabelaDeFontes(sonda: SondaEmSessao): HTMLElement {
  if (sonda.fontesDeEntrada.length === 0) {
    const vazio: HTMLParagraphElement = document.createElement('p');
    vazio.textContent =
      'Nenhuma fonte de entrada foi declarada durante a sondagem. Num visor, isso costuma significar controle desligado ou fora de alcance; num aparelho de mão, é o esperado até a primeira toque na tela.';
    return vazio;
  }
  const tabela: HTMLTableElement = document.createElement('table');
  const cabecalho: HTMLTableRowElement = tabela.insertRow();
  for (const titulo of ['Lado', 'Mira', 'Pose de punho', 'Mão articulada', 'Perfis']) {
    cabecalho.appendChild(celula(titulo, true));
  }
  for (const fonte of sonda.fontesDeEntrada) {
    const fileira: HTMLTableRowElement = tabela.insertRow();
    fileira.appendChild(celula(fonte.lado));
    fileira.appendChild(celula(fonte.mira));
    fileira.appendChild(celula(fonte.temPoseDePunho ? 'sim' : 'não'));
    fileira.appendChild(celula(fonte.temMao ? 'sim' : 'não'));
    fileira.appendChild(celula(fonte.perfis.join(', ')));
  }
  return tabela;
}

// ---------------------------------------------------------------------------
// O parágrafo e o subtítulo nascem aqui, e por isso são exportados daqui.
//
// Da folha da sonda em diante toda folha do relatório é feita destas duas
// peças, e os módulos seguintes as importam deste arquivo. Mudá-las de lugar
// para um arquivo neutro faria elas aparecerem no módulo anterior àquele que as
// introduziu, que é exatamente o que o mapa de marcos existe para impedir.
// ---------------------------------------------------------------------------

export function paragrafo(texto: string): HTMLParagraphElement {
  const elemento: HTMLParagraphElement = document.createElement('p');
  elemento.textContent = texto;
  return elemento;
}

export function subtitulo(texto: string): HTMLHeadingElement {
  const elemento: HTMLHeadingElement = document.createElement('h3');
  elemento.textContent = texto;
  return elemento;
}

/**
 * Escreve o resultado da sonda no elemento indicado.
 *
 * `confronto` é a frase que compara a composição declarada no módulo anterior com
 * a que a sessão informou, e ela vem pronta de fora porque quem a produz é o
 * módulo de dispositivos, não a apresentação.
 */
export function montarSonda(
  raiz: HTMLElement,
  resultado: ResultadoDaSonda,
  confronto: string | undefined,
): void {
  raiz.replaceChildren();

  const titulo: HTMLHeadingElement = document.createElement('h2');
  titulo.textContent = 'Sonda de capacidades';
  raiz.appendChild(titulo);

  raiz.appendChild(paragrafo(descreverClasse(resultado.classe)));
  raiz.appendChild(
    paragrafo(
      resultado.semSessao.contextoSeguro
        ? 'A página está em contexto seguro, então a ausência de um recurso é resposta do aparelho.'
        : 'A página NÃO está em contexto seguro. Nada abaixo é informação sobre o aparelho: é a URL impedindo a pergunta.',
    ),
  );

  const sonda: SondaEmSessao | undefined = resultado.emSessao;
  if (sonda === undefined) {
    raiz.appendChild(
      paragrafo(resultado.motivoSemSessao ?? 'Não houve sessão, e o motivo não foi registrado.'),
    );
    return;
  }

  raiz.appendChild(subtitulo(`Recursos opcionais pedidos em ${sonda.modo}`));
  raiz.appendChild(tabelaDeRecursos(sonda));

  raiz.appendChild(subtitulo('Espaços de referência e graus de liberdade'));
  raiz.appendChild(
    paragrafo(
      sonda.espacosConcedidos.length === 0
        ? 'Nenhum espaço de referência foi concedido.'
        : `Concedidos: ${sonda.espacosConcedidos.join(', ')}.`,
    ),
  );
  raiz.appendChild(paragrafo(rotuloDosGraus(sonda.graus)));

  raiz.appendChild(subtitulo('Fontes de entrada declaradas'));
  raiz.appendChild(tabelaDeFontes(sonda));

  raiz.appendChild(subtitulo('Composição do fundo'));
  raiz.appendChild(paragrafo(`A sessão informou composição ${sonda.composicaoObservada}.`));
  if (confronto !== undefined) {
    raiz.appendChild(paragrafo(confronto));
  }

  raiz.appendChild(subtitulo('Estabilidade do rastreamento na janela observada'));
  raiz.appendChild(
    paragrafo(
      `${sonda.estabilidade.quadros} quadros observados, ` +
        `${sonda.estabilidade.quadrosSemPose} sem pose, ` +
        `${sonda.estabilidade.quadrosOcultos} com a sessão fora de primeiro plano.`,
    ),
  );
  raiz.appendChild(paragrafo(sonda.diagnostico));
}

A função de apresentação recebe tudo pronto e não sabe nada sobre sessões, recursos ou inferência de graus. Essa ignorância é o que permite trocá-la inteira, três módulos adiante, sem tocar em uma linha da sonda: quando a bancada existir, este arquivo é o que muda de lugar, e nada mais.

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

1.6.3 Onde é fácil errar, e como conferir

O erro previsível é o que o próprio enunciado antecipa: escrever o relatório no console e declarar a tarefa cumprida. Ele funciona na máquina de quem programou e some no aparelho, que é onde precisava ser lido. O sintoma é sempre o mesmo em aula — o grupo insiste que a sonda funciona, e no visor não aparece nada.

O segundo erro é mais sutil e aparece na leitura em pé. Texto pequeno demais, contraste baixo e tabela larga que exige rolagem horizontal transformam um relatório correto em relatório inútil dentro de um aparelho. A régua é simples: quem está de visor lê o que caberia num cartaz a um braço de distância.

A conferência é a do enunciado, e ela é a mais barata do percurso. Abrir o mesmo endereço no computador de mesa, no celular e no visor, e ler as três respostas. Se as três forem iguais, ou a página não está em contexto seguro, ou a sonda está respondendo de fora da sessão — e as duas causas se distinguem pela primeira linha do diário.

1.7 A página que se abre em aula

O ponto de entrada deste módulo monta três coisas na página: um título, um botão e dois lugares para escrever. O botão existe por imposição da plataforma, e o diário começa a falar antes de qualquer sondagem.

src/demos/02_demo.ts
// ---------------------------------------------------------------------------
// Marco 02 — o que este aparelho concede, e não o que ele declara.
//
// O módulo anterior perguntou ao navegador quais regimes ele diz suportar. Este
// abre uma sessão de verdade e confere: quais espaços de referência o aparelho
// entrega, quantos graus de liberdade ele rastreia, quais recursos foram
// concedidos e quais foram pedidos em vão. A diferença entre as duas respostas é
// o assunto do módulo.
//
// O diário existe porque quem está de visor não abre console de depuração:
// mensagem que só aparece lá não aparece.
//
// Ponto de entrada do MARCO 02, imutável (`specs/marcos-executaveis.md`).
// ---------------------------------------------------------------------------

import { sondar, conferirComposicao, type ResultadoDaSonda } from '../bancada/devices/sonda';
import { Diario, explicarFalha } from '../bancada/relatorio/diario';
import { montarSonda } from '../bancada/relatorio/capacidades';

function criarNo<T extends HTMLElement>(tag: string, pai: HTMLElement, id?: string): T {
  const no: T = document.createElement(tag) as T;
  if (id !== undefined) {
    no.id = id;
  }
  pai.appendChild(no);
  return no;
}

const raiz: HTMLElement = document.getElementById('bancada') ?? document.body;

const titulo: HTMLHeadingElement = criarNo<HTMLHeadingElement>('h1', raiz);
titulo.textContent = 'Bancada — o que este aparelho concede quando lhe perguntam de dentro';

const botao: HTMLButtonElement = criarNo<HTMLButtonElement>('button', raiz, 'sondar');
botao.type = 'button';
botao.textContent = 'Sondar este aparelho';

const raizDiario: HTMLElement = criarNo<HTMLElement>('div', raiz, 'diario');
const raizSonda: HTMLElement = criarNo<HTMLElement>('div', raiz, 'sonda');

const diario: Diario = new Diario();
diario.fixarDestino(raizDiario);

// Contexto inseguro não expõe a API XR, e o aparelho responderia como se não
// tivesse suporte — o que seria mentira sobre o aparelho, não sobre a página.
if (!window.isSecureContext) {
  diario.alerta(
    'Esta página não está em contexto seguro. A API XR não é exposta aqui, e a sonda vai responder como se o aparelho não tivesse suporte.',
  );
}

async function executarSonda(): Promise<void> {
  diario.nota('Sondando. Se um visor pedir permissão, aceite: sem ela a sessão não abre.');
  try {
    const resultado: ResultadoDaSonda = await sondar();
    const confronto: string | undefined =
      resultado.emSessao === undefined ? undefined : conferirComposicao(resultado.emSessao);
    montarSonda(raizSonda, resultado, confronto);
    diario.nota('Sondagem concluída e sessão encerrada.');
  } catch (erro: unknown) {
    // A falha é resultado, não acidente: ela responde a pergunta do módulo.
    diario.falha(explicarFalha(erro));
  }
}

// A sonda espera um toque porque o navegador recusa pedido de sessão imersiva
// que não venha de gesto de quem usa. Não é cerimônia da página: é regra da API.
botao.addEventListener('click', () => {
  void executarSonda();
});

diario.nota('A sonda completa espera um toque no botão.');

O aviso de contexto inseguro é a primeira coisa que o arquivo faz depois de fixar o destino do diário, e a ordem foi escolhida. Sem conexão cifrada a API XR não é exposta, e o aparelho responderia como se não tivesse suporte algum — uma mentira sobre o aparelho, causada pela URL. Esse é o erro que mais custa tempo de aula em todo o percurso, e ele se apresenta com a cara de um defeito de código.

A demonstração em sala tem uma coreografia. A mesma página é aberta no computador de mesa, onde nenhuma sessão imersiva abre e o relatório diz isso com todas as letras; depois no celular, onde a sessão aumentada abre e a composição do fundo muda de valor; depois no visor, com espelhamento no projetor, onde os espaços de referência com origem no chão aparecem e a inferência de graus finalmente devolve seis. Três aparelhos, um endereço, três relatórios diferentes.

Este ponto de entrada pertence ao módulo 02 e não será tocado por módulo posterior algum. O seguinte escreve o seu ao lado, e esta demonstração continuará rodando no fim do semestre exatamente como roda hoje.

1.8 Por dentro da implementação

Oito coisas entraram neste módulo, e cada uma muda o caminho que a execução percorre:

alteração o que ela passou a fazer
sonda-sem-sessao a consulta fora de sessão escolhe o modo a abrir, ou conclui que não há nenhum
sessao-de-sondagem a sessão abre de verdade, recebe uma camada mínima e encerra no finally
estado-do-recurso recurso não concedido e recurso sem resposta deixam de ser a mesma coisa
graus-inferidos os graus de liberdade saem dos espaços concedidos, e admitem não saber
estabilidade-medida a perda de pose vira contagem quadro a quadro, separada do menu do sistema
confronto-de-composicao a composição declarada no módulo anterior é confrontada com a informada
diario-na-pagina as mensagens passam a ser escritas onde quem está de visor consiga lê-las
folha-da-sonda o resultado da sonda vira uma folha do relatório, com tabelas próprias

A costura com o que já existia é uma só, e ela é literal. sondarSemSessao() chama levantarRelatorio(), a mesma função que montava a tabela de regimes, e filtra dela os regimes cujo suporte veio como 'sim' — é assim que modosSuportados nasce. Nada do módulo anterior foi reescrito para caber aqui.

Duas das oito têm cenário nas páginas que seguem, e as outras seis esperam o aparelho. As execuções descritas abaixo são as que a página do marco produz num computador de mesa, que é a máquina que toda a turma tem à mão. As seis restantes, da abertura da sessão ao confronto de composição, só executam dentro de uma sessão imersiva, e no desktop elas não chegam a ser chamadas. Desenhá-las a partir da leitura do código produziria um percurso plausível e não conferido, que é justamente o desenho que parece verificado por estar desenhado. O cenário delas sai da execução no visor, e é ali que os dois diagramas de cada uma se fecham.

1.8.1 A sonda que decide antes de pedir qualquer coisa

O cenário é o desktop do laboratório, a página aberta em contexto seguro, sem visor conectado, e alguém aperta o botão Sondar este aparelho. A execução terminou sem abrir sessão alguma e escreveu três frases na página: Aparelho que só sustenta o regime de janela — é o caso do desktop do laboratório., depois A página está em contexto seguro, então a ausência de um recurso é resposta do aparelho., e por fim Este aparelho não declara sessão imersiva alguma, e metade da sonda não tem onde acontecer. Um resultado pobre, e é o resultado que interessa: metade do código deste módulo não roda, e a página explica por quê em vez de ficar em branco.

flowchart TD
  A["executarSonda()"] --> B["sondar()"]
  B --> C["sondarSemSessao()"]
  C -->|"reusa o módulo anterior"| D["levantarRelatorio()"]
  D --> E["três linhas: inline 'sim', immersive-vr 'nao', immersive-ar 'nao'"]
  E --> F["modosSuportados = ['inline']"]
  F --> G["modoPreferido()"]
  G --> H{"modo === undefined"}
  H -->|"true — o caminho desta execução"| I["classificarAparelho()"]
  H -->|"false — ramo com sessão, não tomado aqui"| J["ramo imersivo, que exige visor na sala"]
  I --> K["classe = 'somente-janela'"]
  K --> L["ResultadoDaSonda com emSessao undefined<br/>e motivoSemSessao preenchido"]
  L --> M["montarSonda()"]
  M --> N["descreverClasse()"]
  N --> O{"resultado.emSessao === undefined"}
  O -->|"true"| P["paragrafo() com motivoSemSessao, e retorno"]

O losango divide o módulo em dois. À esquerda, o ramo que esta execução tomou, com três chamadas; à direita, o ramo com sessão, que só existe quando há visor na sala. A decisão é de modoPreferido(), que procura 'immersive-ar' e depois 'immersive-vr' na lista recebida, nessa ordem, e devolve o primeiro que encontrar. Aqui a lista trazia apenas 'inline', e o retorno foi undefined.

sequenceDiagram
    participant demo as demos/02_demo
    participant sonda as devices/sonda
    participant ver as modes/verificacao
    participant xr as navigator.xr
    participant graus as devices/graus
    participant cap as relatorio/capacidades
    demo->>sonda: sondar()
    sonda->>sonda: sondarSemSessao()
    sonda->>ver: levantarRelatorio()
    ver->>xr: isSessionSupported('inline')
    xr-->>ver: true
    ver->>xr: isSessionSupported('immersive-vr')
    xr-->>ver: false
    ver->>xr: isSessionSupported('immersive-ar')
    xr-->>ver: false
    ver-->>sonda: três linhas, suporte 'sim', 'nao', 'nao'
    sonda->>sonda: modoPreferido(['inline'])
    sonda-->>sonda: undefined
    sonda->>graus: classificarAparelho(['inline'], 'indeterminado', true)
    graus-->>sonda: 'somente-janela'
    sonda-->>demo: emSessao undefined, motivoSemSessao preenchido
    demo->>cap: montarSonda(raizSonda, resultado, undefined)
    cap->>graus: descreverClasse('somente-janela')
    graus-->>cap: 'Aparelho que só sustenta o regime de janela...'
    cap-->>demo: três parágrafos escritos na página

A ordem das mensagens mostra que a decisão acontece cedo, antes de qualquer permissão ser pedida a quem usa o aparelho. Repare que classificarAparelho() recebe 'indeterminado' no lugar dos graus de liberdade e responde mesmo assim, porque a classificação se apoia nos modos declarados, e esses vieram sem sessão. Um aparelho que não abre sessão continua sendo classificável; o que se perde é a distinção entre visor com posição e visor sem posição, que exige justamente a leitura que não aconteceu.

A escolha do modo mais informativo, e o que ela custou. A alternativa era tentar 'immersive-vr' primeiro, que é o modo que quase todo exemplo introdutório abre. O custo aparece no celular: ele sustenta a sessão aumentada e não a imersiva completa, e a ordem invertida faria a sonda desistir num aparelho que tinha o que responder. A ordem escolhida pergunta primeiro pelo modo que devolve mais informação, e cai para o outro quando ele não existe.

1.8.2 O diário, na execução em que ele é o único canal

O cenário é a mesma página, agora aberta pelo endereço de rede do computador, sem conexão cifrada. A API XR não é exposta fora de contexto seguro, e a execução respondeu como se o aparelho não tivesse suporte nenhum — que é o erro de laboratório mais caro do percurso, porque tem a cara de um defeito de código. O diário disse o contrário antes de qualquer sondagem: Esta página não está em contexto seguro. A API XR não é exposta aqui, e a sonda vai responder como se o aparelho não tivesse suporte. Depois do clique, a folha da sonda abriu com Navegador sem a API XR, ou página fora de contexto seguro. e seguiu com A página NÃO está em contexto seguro. Nada abaixo é informação sobre o aparelho: é a URL impedindo a pergunta.

flowchart TD
  A["fixarDestino(raizDiario)"] --> B["redesenhar()"]
  C["alerta(texto) — contexto inseguro, esta execução"] --> D["registrar(entrada)"]
  E["nota(texto) — três vezes nesta execução"] --> D
  F["falha(texto) — não ocorreu aqui"] --> D
  D --> G["entradas.push(entrada)"]
  G --> H["console.info espelha a entrada para depuração remota"]
  H --> B
  B --> I{"destino === undefined"}
  I -->|"true"| J["a entrada fica acumulada, e nada é escrito"]
  I -->|"false"| K["destino.replaceChildren, e um p por entrada acumulada"]

As três portas de entrada terminam na mesma função privada, e o desenho fecha um laço: registrar() acumula, espelha no console e chama redesenhar(), que reescreve a lista inteira. A guarda de redesenhar() é o que permite ao diário existir antes do elemento existir — mensagem produzida cedo demais fica acumulada em vez de se perder, e aparece assim que houver destino.

sequenceDiagram
    participant demo as demos/02_demo
    participant diario as relatorio/diario
    participant pagina as div do diário
    participant sonda as devices/sonda
    participant cap as relatorio/capacidades
    participant graus as devices/graus
    demo->>diario: fixarDestino(raizDiario)
    diario->>pagina: redesenhar(), com zero entradas acumuladas
    demo->>diario: alerta('Esta página não está em contexto seguro...')
    diario->>pagina: uma entrada, classe diario-alerta
    demo->>diario: nota('A sonda completa espera um toque no botão.')
    diario->>pagina: duas entradas, a lista inteira reescrita
    demo->>diario: nota('Sondando. Se um visor pedir permissão, aceite...')
    diario->>pagina: três entradas
    demo->>sonda: sondar()
    sonda-->>demo: classe 'sem-api', modosSuportados vazio
    demo->>cap: montarSonda(raizSonda, resultado, undefined)
    cap->>graus: descreverClasse('sem-api')
    graus-->>cap: 'Navegador sem a API XR, ou página fora de contexto seguro.'
    cap-->>demo: três parágrafos escritos na página
    demo->>diario: nota('Sondagem concluída e sessão encerrada.')
    diario->>pagina: quatro entradas

Quatro entradas foram escritas nesta execução, e a primeira delas é a que resolve o caso. Nenhuma das quatro chegou por exceção: sondar() não falhou aqui, apenas respondeu pouco.

O diário na página, e o que ele custou. A alternativa era escrever as mensagens no console de depuração, que é onde elas naturalmente iriam. O custo é simples de enunciar e fácil de esquecer enquanto se programa sentado: quem está de visor não abre console, e mensagem que só aparece lá não aparece. O espelho no console continua, e serve à depuração remota, quando o aparelho está ligado ao computador por cabo.

1.8.3 As decisões do ramo que o desktop não alcança

As seis alterações sem cenário aqui têm as decisões registradas, e elas se conferem no fonte mesmo sem execução.

Todo recurso pedido como opcional, e o que isso custou. A alternativa era listar os recursos em requiredFeatures, que é o campo que a maioria dos exemplos de introdução usa. O ganho seria uma garantia: sessão aberta significa recurso disponível. O custo inviabiliza a sonda — o aparelho recusa a sessão inteira quando um único item obrigatório não pode ser concedido, e o que se perde na recusa é exatamente a informação que a sonda foi buscar.

O encerramento no finally, e o que isso custou. A alternativa era chamar sessao.end() no fim do caminho feliz, que é mais curto de escrever e funciona em toda demonstração bem-comportada. O custo aparece quando a sondagem falha no meio: a sessão imersiva continua viva com a página parada, e quem está com o visor no rosto vê uma tela vazia da qual só se sai pelo menu do sistema.

A rejeição lida como resposta, e o que isso custou. Em espacosConcedidos() há um bloco de captura vazio, que em qualquer revisão de código honesta acenderia uma luz. Ele está certo aqui: o pedido de um espaço de referência rejeita quando o espaço não é concedido, e a rejeição é a leitura. A alternativa era deixar a rejeição propagar, e o custo é que o primeiro espaço negado abortaria a consulta dos demais.

A inferência que se recusa a apostar, e o que isso custou. grausDeLiberdade() devolve 'seis' quando algum espaço com chão foi concedido, 'tres' quando só o espaço do observador veio, e 'indeterminado' em todo o resto. A alternativa era tratar a ausência de chão como três graus, que acerta na maioria dos aparelhos. O custo é o mesmo do módulo anterior, agora um nível acima: a tabela afirmaria sobre o aparelho o que ele não disse, e a afirmação chegaria ao relatório com a mesma cara de fato que as medidas verdadeiras.

A contagem separada do julgamento, e o que isso custou. ContadorDeEstabilidade conta quadros com pose e quadros sem, e mais nada; diagnosticar() interpreta depois. A alternativa era o contador já devolver a frase pronta. O custo é conhecido: medidor que julga enquanto mede é medidor que concorda com quem o escreveu, e a contagem deixa de poder ser conferida contra outra leitura.

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

Verificação Como se confere
A sonda abre sessão de verdade o relatório traz recursos concedidos, e não apenas modos suportados
Recurso negado e recurso sem resposta aparecem distintos ler a coluna de estado num navegador que não reporta a lista
A inferência de graus admite não saber espaço local sozinho produz resultado indeterminado
A sessão encerra mesmo quando a sondagem falha interromper a sondagem e conferir que o visor volta ao menu
O relatório se lê dentro do aparelho abrir no visor, sem console e sem depuração remota
A composição declarada foi confrontada a frase de confronto aparece, confirmando ou desmentindo
O código analisa limpo em modo estrito verificação de tipos do projeto, sem emissão

A quarta linha é a que costuma passar despercebida e a que mais irrita quem usa o aparelho. Uma sondagem que falha no meio sem encerrar a sessão deixa o visor numa tela preta de onde só se sai pelo menu do sistema, e o grupo que descobre isso descobre no colega que estava com o aparelho no rosto.

O que fica pronto ao fim deste módulo continua sem desenhar nada, e agora por um motivo diferente do anterior. No módulo passado a razão era contenção de escopo; aqui a razão é o assunto, porque o aparelho responde antes de qualquer cena existir. O grafo de cena entra no módulo seguinte, e entrará sabendo em que máquina está sendo montado — que era, desde o início, a única coisa que a sonda precisava garantir.