flowchart TD
A["a pessoa dá um passo<br/>à frente"] --> B["ouvido interno relata<br/>DESLOCAMENTO"]
A --> C["sistema mede só<br/>a orientação"]
C --> D["a cena desenhada<br/>fica onde estava"]
D --> E["olhos relatam<br/>CORPO PARADO"]
B --> F{"dois relatos<br/>incompatíveis"}
E --> F
F --> G["mal-estar"]
1 Dispositivos, rastreamento e graus de liberdade
Antes de desenhar qualquer coisa, o aparelho precisa descobrir onde ele mesmo está — e essa descoberta falha de maneiras previsíveis.
O que fica de pé ao fim destas páginas. Por que um sistema que gira sem transladar dá enjoo. Os dois arranjos possíveis para medir a pose de um corpo. Como um aparelho desenha o mapa da sala enquanto a atravessa. E como perguntar ao aparelho o que ele oferece, em vez de supor.
Ponha um visor no rosto de alguém que nunca usou um. Peça que essa pessoa olhe em volta, para cima, para trás. Ela vai sorrir: o mundo desenhado gira com a cabeça dela, na direção certa e na hora certa. Agora peça que ela dê um passo à frente.
O mundo fica onde estava.
Ela avança meio metro e a bancada desenhada não se aproxima um milímetro. Em alguns segundos vem o desconforto; em alguns minutos, algo pior. Quem monta a demonstração aprende rápido a contornar o assunto. Pede que a pessoa fique parada. Apoia a mão no ombro dela. Sugere que sente. O sintoma é tratado como fragilidade de quem usa, e é uma conta faltando.
Aquele aparelho media três coisas sobre a cabeça de quem o vestia e ignorava outras três. As três medidas dizem para onde a cabeça aponta; as três ignoradas diriam onde ela está. Descobrir onde se está é a operação mais silenciosa de um ambiente tridimensional. Nada na tela a anuncia. Ela acontece dezenas de vezes por segundo enquanto ninguém pensa nela. Quando funciona, é invisível. Quando falha, o que chega até quem programa é uma linha a menos numa estrutura de dados.
1.1 O corpo percebe antes de você conseguir nomear
O mal-estar tem causa geométrica, nome técnico e correção conhecida.
Dentro de cada ouvido interno há estruturas que medem movimento. Uma parte responde a rotação: virar a cabeça para a esquerda produz um sinal de rotação para a esquerda. Outra parte responde a aceleração linear: dar um passo à frente produz um sinal de deslocamento. Os dois sinais chegam ao cérebro junto com o que os olhos estão vendo. A expectativa de coerência entre eles é permanente.
Quando a pessoa virou a cabeça, tudo bateu. O ouvido relatou rotação, os olhos viram a cena girar, e as duas informações contaram a mesma história. Quando ela deu o passo, a coerência se rompeu. O ouvido relatou deslocamento. Os olhos relataram uma cena imóvel, que é o que se vê quando o corpo está parado. As duas coisas não podem ser ambas verdadeiras, e o cérebro não tem como escolher qual descartar.
Repare no que essa explicação implica. O desconforto não depende de a cena ser feia, nem de o visor ser barato, nem de a pessoa ser sensível. Um aparelho caríssimo com a mesma lacuna produz o mesmo efeito, com melhor resolução. E daí sai uma consequência que contraria o instinto: se o sistema não sabe para onde a pessoa se deslocou, a atitude correta é não mover a cena. Uma cena movida por suposição erra em direção e em quantidade. Acrescenta um terceiro relato ao desacordo que já havia entre dois.
Antes de seguir. Pense num aplicativo que exibe uma fotografia esférica: você gira o celular e vê a paisagem toda em volta. O que mudaria nele se o sistema passasse a rastrear também a sua posição? Se a resposta for “nada”, você acabou de descobrir para que serve um sistema de três graus de liberdade.
Duas coisas que a conversa comum mistura precisam ficar separadas. Um sistema pode não saber onde a pessoa está, e um sistema pode saber com atraso. A ignorância deixa a cena parada enquanto o corpo anda. O atraso deixa a cena chegar depois — na direção certa, no instante errado. A segunda é a mais traiçoeira, porque tudo parece funcionar. Quem usa diz apenas que “está um pouco estranho”. Não consegue nomear o defeito: nada do que ele vê está no lugar errado. Está no tempo errado, e o corpo mede tempo com uma precisão que a linguagem não acompanha.
A assimetria entre as duas importa na hora de corrigir. Atraso é problema de engenharia dentro de um sistema que já mede a grandeza certa. Melhora por graus: menos trabalho por quadro, caminho mais curto entre sensor e imagem. Ausência de medida não melhora por graus. É uma metade da pose que ninguém está estimando, e nenhum ajuste no resto do programa a produz.
1.2 Seis números, e a plataforma que não os informa
A briga toda entre visores caros e baratos cabe numa contagem de parâmetros independentes.
Seis. É quanto se precisa para dizer onde um corpo rígido está e para que lado ele aponta. Pegue um livro sobre a mesa. Três números localizam uma quina dele — distância até duas paredes e até o chão, escolhida uma origem qualquer. Nenhum deles se deduz dos outros. Mas o livro ainda pode girar em torno daquela quina. Deitado, em pé ou de cabeça para baixo, ele tem os mesmos três primeiros números. Faltam mais três para a orientação.
A pose de um corpo rígido é o par formado pela sua posição e pela sua orientação. Cada uma das duas metades tem três parâmetros independentes, e cada parâmetro independente é um grau de liberdade.
Um sistema que estima só a orientação rastreia três graus de liberdade; um que estima as duas metades rastreia seis. A distinção é de quantos parâmetros o sistema mede, e não de quantos o corpo tem: o corpo tem sempre seis.
flowchart LR
P["pose de um<br/>corpo rígido"] --> R["orientação<br/>3 parâmetros"]
P --> T["posição<br/>3 parâmetros"]
R --> S3["sistema de TRÊS graus<br/>estima só a orientação"]
R --> S6["sistema de SEIS graus<br/>estima as duas metades"]
T --> S6
S3 -.-> N["andar é invisível:<br/>a cena não se move"]
S6 -.-> M["andar move a cena<br/>na direção e na medida certas"]
A cabeça da pessoa da demonstração tinha seis graus o tempo todo, porque cabeças têm. O sistema media três. E note que a definição não diz nada sobre precisão, frequência ou método: um sistema de seis graus pode estimar mal as duas metades e continuar sendo de seis graus. A frase “um visor de três graus de liberdade” é sobre o sistema de medida. No uso comum, virou uma frase sobre o aparelho. Como se o objeto físico tivesse menos liberdade. O objeto tem os mesmos seis de sempre; o que fica menor é a descrição que o sistema consegue produzir dele.
Um sistema de três graus não está quebrado. Ele foi projetado assim, e há aplicações inteiras em que a escolha é correta. A fotografia esférica do card acima é uma delas. A imagem está no infinito. Andar meio metro em direção ao horizonte não muda o horizonte. O problema aparece quando a cena tem objetos a distâncias finitas e reconhecíveis. Uma bancada a um metro de quem observa é um objeto assim. Aproximar-se dela deveria aumentar o tamanho aparente dela. O corpo espera isso com a mesma precisão com que alguém acerta a mão numa maçaneta no escuro.
Aí a saída honesta é admitir o limite em vez de disfarçá-lo. Sabendo que a posição não é rastreada, dá para projetar um ambiente que não dependa dela: objetos distantes, interação por direção do olhar em vez de aproximação, nenhuma promessa de escala corporal. O resultado é mais modesto e funciona. A saída desonesta é a que se vê com mais frequência. Construir como se a posição existisse. Entregar ao aparelho que não a mede. E chamar de “limitação do hardware” o desconforto que se segue.
1.2.1 A pergunta que a plataforma se recusa a responder
Agora a parte incômoda: a plataforma não informa quantos graus de liberdade o aparelho rastreia. Não há propriedade a ler, nem consulta que devolva três ou seis. A omissão tem razão. A camada que responde ao programa não sabe o que o sensor faz: sabe o que o sistema do aparelho aceitou entregar. O que sobra é inferir a partir dos espaços de referência que a sessão concede. Um espaço ancorado no chão exige que o aparelho saiba onde o chão está em relação a quem observa. Um aparelho que só mede orientação não sustenta essa relação enquanto a pessoa caminha.
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';
}A linha que decide tudo é a última: a função devolve um terceiro valor, indeterminado, para o caso em que a evidência não decide. Um programa que devolvesse apenas tres ou seis teria de escolher um lado no caso ambíguo — e escolher ali é apostar. A regra da inferência está escrita por extenso dentro do próprio arquivo, em comentário, e vale ser lida linha a linha.
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.
*/Comentário envelhece e engana quem confia nele, então escrevê-lo tem custo. Aceitei o custo por um motivo específico: isto é uma inferência, e inferência sem premissa exposta não pode ser contestada. Quem lê pode discordar do critério e propor outro, o que seria impossível diante de três condicionais mudas.
E há um limite da inferência que convém dizer antes que alguém confie demais nela. Ela lê concessões de espaço de referência. Concessão é decisão da plataforma, tomada por razões que podem não ter nada a ver com o sensor. Uma plataforma que concedesse o espaço ancorado no chão a um aparelho de três graus, simulando um chão fixo abaixo de quem observa, faria a inferência devolver seis para um sistema que mede três. Nada impede tecnicamente esse cenário. Por isso a função devolve uma leitura, e não um veredito.
1.3 Duas maneiras de saber onde se está
Posição é sempre posição em relação a alguma coisa, e alguém tem de decidir onde essa coisa mora.
Há duas respostas possíveis, e elas esgotam o assunto. Na primeira, o que mede fica no ambiente e o corpo é observado de fora: câmeras em tripés, emissores fixos nas paredes, ou uma estrutura mecânica presa ao teto. Na segunda, o que mede viaja junto com o corpo. Câmeras montadas na carcaça do aparelho — olhando para fora — mais sensores de movimento embarcados.
Um arranjo de rastreamento é a disposição física do que mede a pose, e admite dois casos. No arranjo por infraestrutura externa, o que mede está fixo no ambiente e o corpo rastreado é observado de fora. No arranjo por sensores embarcados, o que mede viaja com o corpo. A pose é estimada a partir do que esses sensores observam do ambiente.
A classificação é pela posição do que mede. Ela não diz nada sobre precisão, custo ou geração do equipamento.
Essa última frase impede o mal-entendido mais comum. Existem arranjos externos imprecisos e arranjos embarcados excelentes; a ordem entre dois equipamentos é pergunta sobre produtos, não sobre a classificação.
flowchart TB
Q["onde está o que mede<br/>a pose?"] --> E["INFRAESTRUTURA EXTERNA<br/>fixo no ambiente,<br/>observa o corpo de fora"]
Q --> I["SENSORES EMBARCADOS<br/>viajam com o corpo,<br/>observam o ambiente"]
E --> E1["funciona só no<br/>espaço preparado"]
E --> E2["falha por oclusão:<br/>algo entre instrumento e corpo"]
E --> E3["a região ruim é da SALA<br/>e fica onde está"]
I --> I1["funciona em qualquer<br/>ambiente que ofereça<br/>o que observar"]
I --> I2["falha por ambiente<br/>pobre em referências"]
I --> I3["a região ruim ANDA<br/>junto com a pessoa"]
1.3.1 O braço que segurava e media
O ancestral direto do primeiro arranjo é de 1968. O visor que Ivan Sutherland montou naquele ano era 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 só para segurar peso. Ele media a posição da cabeça de quem vestia o aparelho, lendo os ângulos das próprias juntas. Uma estrutura rígida tem geometria determinada: sabendo o comprimento de cada segmento e o ângulo de cada junta, a posição da ponta sai por trigonometria, sem estimativa e sem erro acumulado.
O preço está no mesmo parágrafo. Quem usa está preso ao aparelho, e o alcance do movimento é o alcance do braço. A certeza foi comprada com a única moeda disponível: a liberdade de andar. É a mesma moeda — a de hoje — dos arranjos de infraestrutura externa. Note que o braço não foi adotado por ser a melhor forma concebível de medir uma cabeça. Ele já estava lá, sustentando um peso que nenhum pescoço aguentaria. Ler os ângulos de uma estrutura rígida que já existia era a informação mais barata disponível. O apelido “espada de Dâmocles” veio depois, dado por outras pessoas.
| Dimensão | Infraestrutura externa | Sensores embarcados |
|---|---|---|
| Onde funciona | só no espaço preparado | em qualquer ambiente que ofereça o que observar |
| Trabalho prévio | montagem, alinhamento e calibração | nenhum, além de ligar o aparelho |
| Origem do erro | oclusão entre o corpo e o instrumento | ambiente pobre em referências observáveis |
| Ao falhar | a região ruim é da sala e fica onde está | a região ruim acompanha quem se move |
Repare na última linha, que é a que mais muda o projeto. Num arranjo externo, descoberta uma vez, a região problemática continua no mesmo lugar amanhã, e dá para desenhar o ambiente evitando-a. Num arranjo embarcado, ela viaja com a pessoa e depende de para onde ela aponta os olhos naquele instante. Com instalação fixa, você testa a sala uma vez e afirma que o sistema funciona nela; sem instalação, cada sala é um caso novo, e o que se pode afirmar é como o ambiente se comporta diante da degradação.
1.3.2 O cartógrafo no escuro
E aqui está a razão de o segundo arranjo dominar os aparelhos que se abrem por uma URL. Ele dispensa a sala preparada — o que é exatamente o que permite abrir o mesmo endereço em três aparelhos diferentes e esperar que funcione. Em troca, ele transfere ao próprio aparelho a tarefa de descobrir onde está, sem referencial dado, a partir apenas do que as câmeras veem. 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.
Essa 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 abandona a exigência de ordem: as duas tarefas passam a ser feitas ao mesmo tempo, e as duas produzem estimativas. A posição é a melhor aposta dado o mapa atual; o mapa é a melhor aposta dada a trajetória suposta.
Dado um corpo que se desloca por um ambiente desconhecido, o problema de localização e mapeamento simultâneos consiste em estimar, ao mesmo tempo, a trajetória do corpo e um mapa do ambiente, a partir apenas das observações feitas pelos sensores que viajam com ele.
As duas estimativas são mutuamente dependentes, e ambas são revisáveis — uma observação nova pode alterar retroativamente o que já se julgava conhecido.
flowchart LR
subgraph CIRC["o círculo, desfeito pela simultaneidade"]
POS["posição estimada<br/>melhor aposta atual"] --> MAP["mapa estimado<br/>melhor aposta atual"]
MAP --> POS
end
OBS["observação nova<br/>do sensor"] --> CIRC
CIRC --> REC{"reconhece um lugar<br/>por onde já passou?"}
REC -->|"não"| ACU["o erro pequeno<br/>continua acumulando"]
REC -->|"sim, e é o lugar certo"| COR["correção RETROATIVA:<br/>mapa e trajetória revistos"]
REC -->|"sim, mas é lugar errado"| DOB["o mapa dobra<br/>sobre si mesmo"]
Como se reconhece um lugar? Comparar a vista atual contra tudo o que já foi visto está descartada por aritmética: essa comparação cresce com o percurso, e o resultado precisa sair dentro de um quadro. Guarda-se, então, uma descrição compacta de cada lugar; compara-se descrições, o que é barato; e verifica-se em detalhe só os poucos candidatos que sobrevivem ao filtro. Disso decorre algo que ninguém adivinharia: reconhecer é mais provável quando se olha o lugar do mesmo ângulo de antes, porque a descrição guarda o que se via dali.
O reconhecimento erra de dois jeitos desiguais. Deixar de reconhecer um lugar conhecido custa uma oportunidade de correção. Reconhecer um lugar onde nunca se esteve custa o mapa inteiro: o sistema dobra o mapa sobre si mesmo, e o estrago vale retroativamente para tudo o que veio antes. Por isso esses algoritmos são desconfiados por escolha, e exigem evidência forte antes de aceitar um reencontro. Quem observa de fora vê um aparelho que demora a se recuperar e conclui que ele é lento — quando baixar a exigência aceleraria a recuperação e admitiria reencontros falsos na mesma proporção.
Aceita a correção retroativa, uma consequência desconfortável se instala: a posição que o aparelho informa é estimativa sujeita a revisão, e não medida de régua. Duas leituras no mesmo lugar físico, com meia hora de diferença, podem divergir sem que nenhuma esteja errada. Entre uma e outra, o aparelho reconheceu um canto e corrigiu o mapa. O que se moveu foi o sistema de coordenadas.
Daí sai a resposta a uma pergunta que aparece cedo em qualquer projeto aumentado: como prender um objeto virtual a um ponto do mundo real? A tentativa natural é anotar as coordenadas, e ela funciona por alguns minutos e depois escorrega — porque as coordenadas anotadas envelhecem quando o mapa que as define é corrigido embaixo delas. Ancorar consiste em pedir ao aparelho que mantenha o objeto preso a um ponto do mapa e corrija as coordenadas sozinho. Quem corrige o mapa é quem tem de corrigir o objeto.
O que a plataforma entrega do mapa, e o que ela não entrega
Não há nuvem de pontos a inspecionar, não há contagem de pontos reconhecidos, e não existe aviso de que uma correção acabou de acontecer. O que chega até a página são quatro efeitos: a pose de quem observa a cada quadro, o resultado de lançar um raio contra o que o aparelho considera superfície real, as superfícies planas reconhecidas no ambiente, e a âncora.
Disso decorre a disciplina de projeto: não guardar coordenadas absolutas por conta própria, não supor a origem do mundo parada, e tratar toda posição recebida como válida apenas naquele quadro. Um ambiente escrito assim continua correto depois de uma correção do mapa. Um ambiente escrito sem isso passa a desenhar num lugar que já não existe.
Uma ressalva sobre a correção, para não esperar dela o que ela não faz. Reconhecer um canto melhora a coerência do espaço já mapeado; não melhora a precisão do próximo passo. O aparelho fica mais consistente sobre onde esteve, e não mais confiável sobre onde vai.
1.4 A sala limpa é a pior sala
Um aparelho que se orienta por cantos precisa de cantos. São três as condições que o deixam sem eles, e duas delas descrevem o que a maioria das pessoas chamaria de sala bem cuidada.
A primeira é a superfície sem textura. Uma parede lisa, uniforme, sem quadro, sem tomada e sem rodapé no campo da câmera não oferece nenhum ponto distinguível dos vizinhos: qualquer pedaço é igual a qualquer outro, e não há o que anotar nem o que reconhecer depois. A segunda é a iluminação pobre, e aqui o problema é o contraste. Mesmo com textura na parede, a câmera precisa distinguir claros e escuros para extrair um canto. Sob luz fraca, a imagem perde essa distinção antes de perder o brilho geral.
A terceira é o movimento brusco. Um giro rápido da cabeça borra a imagem capturada durante a exposição, e um canto borrado deixa de ser um canto. Em alguns quadros apenas, o que rompe a continuidade entre um quadro e o seguinte no momento em que ela é mais necessária. O aparelho perde a referência exatamente quando quem o usa está se movendo mais.
As três combinadas produzem o pior caso, e ele é uma sala recém-pintada de branco, bem iluminada por luz difusa. Qualquer pessoa a descreveria como limpa e adequada. Para um aparelho que se orienta por cantos, é uma sala sem informação, e a luz abundante não compensa nada — ela ilumina bem uma superfície que não tem o que mostrar.
As três têm relação diferente com o tempo, e isso muda o que se faz com cada uma. Falta de textura é permanente enquanto ninguém mexer na sala, e some quando alguém prega um cartaz. Luz fraca é intermitente e obedece ao relógio, o que produz o relato irreprodutível de que “às vezes trava”. Movimento brusco é a única que depende de quem usa, e por isso a única sobre a qual a interface consegue agir. Um ambiente que põe o objeto de interesse atrás de quem observa está pedindo o giro rápido que degrada o rastreamento.
Do lado de quem programa, nada disso é visível. Não há aviso de degradação, não há indicador de qualidade, não há evento de perda. A plataforma oferece um único indício: o quadro chega e a pose de quem observa não vem. O laço continua rodando, a sessão continua viva, e ao perguntar onde está a cabeça o programa recebe nada. Um quadro assim, isolado, é normal; uma sequência deles é a degradação acontecendo, e a diferença entre as duas coisas é contagem que o programa tem de fazer sozinho.
flowchart TD
T["superfície sem textura"] --> SC["nenhum canto<br/>a reconhecer"]
L["iluminação pobre"] --> SC
M["movimento brusco"] --> SC
SC --> QSP["o quadro chega<br/>e a pose não vem"]
MEN["menu do sistema aberto:<br/>a sessão saiu de foco"] --> QSP
QSP --> C{"a sessão estava<br/>visível naquele quadro?"}
C -->|"sim"| DEG["conta como<br/>degradação de rastreamento"]
C -->|"não"| NOR["comportamento normal<br/>do sistema, não conta"]
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 segundo parâmetro impede um diagnóstico falso. Quando alguém abre o menu do sistema do visor para ver a hora, a sessão continua viva, os quadros continuam chegando e a pose deixa de vir. O sintoma é idêntico ao da perda de rastreamento, e a causa é comportamento normal do sistema operacional. Um contador que ignorasse a diferença acusaria o aparelho de um defeito toda vez que alguém consultasse o relógio.
Há uma segunda decisão embutida naquele arquivo. O contador conta quadros com pose, quadros sem pose e quadros ocultos, e não faz mais nada: ele não decide se o rastreamento está bom. Essa interpretação mora numa função separada. A separação parece burocrática e resolve um problema real — um medidor que interpreta embute, no ato de medir, a expectativa de quem o escreveu, e o resultado é um medidor que sempre concorda com quem o programou. Mantendo a contagem burra, o número pode ser usado como argumento contra quem o produziu.
A frase de diagnóstico, do outro lado da separação, também se recusa a apontar qual das três causas ocorreu. Daqui não se distingue textura de luz e de movimento, e nenhuma combinação dos contadores distingue. Escolher uma seria inventar o diagnóstico junto com a medida.
1.5 O que se pergunta a um aparelho
Três perguntas substituem qualquer taxonomia de aparelho — e as três se fazem enquanto o programa roda.
Quantas classes de aparelho existem? A pergunta pede uma taxonomia, e taxonomia não sobrevive a dois anos de mercado. A formulação melhor troca a lista de famílias por três perguntas que se fazem ao aparelho enquanto o programa roda nele.
Uma capacidade declarada é uma propriedade que a plataforma responde quando interrogada sobre o aparelho, e não uma propriedade do aparelho. São três as famílias interrogáveis: os tipos de sessão suportados, os recursos opcionais concedidos a uma sessão aberta e as fontes de entrada que a sessão apresenta.
A resposta pertence à camada que responde. Um aparelho pode ter um sensor cuja capacidade correspondente a plataforma não declara, e o programa não tem como usar o que não foi declarado.
O exemplo mínimo é o celular com câmera traseira e sensores de movimento, aberto num navegador que não implementa a interface de sessões imersivas. O aparelho tem tudo o que seria necessário; a camada não declara nada; para o programa, a capacidade não existe. E a tentação é justamente completar a declaração por dedução — este visor é da geração tal, logo deve ter aquilo —, que é a origem de uma família inteira de defeitos que só aparecem no aparelho de outra pessoa.
flowchart TB
F["fora de qualquer sessão"] --> P1["que tipos de sessão<br/>este aparelho aceita abrir?"]
P1 --> R1["sim / não / não sei"]
G["gesto de quem usa<br/>abre a sessão"] --> D["dentro da sessão"]
D --> P2["que recursos opcionais<br/>foram concedidos?"]
D --> P3["que fontes de entrada<br/>a sessão apresenta?"]
P2 --> R2["concedido / negado / indeterminado"]
P3 --> R3["tem raio? tem pose própria?"]
F -.->|"perguntar aqui o que<br/>só existe lá produz suposição"| D
Perceba que a terceira família é a que mais se supõe. Fontes de entrada são os objetos com que a sessão diz que a pessoa vai agir sobre a cena. Ela as apresenta como uma lista que muda durante o uso: um controle ligado no meio da sessão aparece nela; um que fica sem bateria some. Duas propriedades de cada fonte decidem quase tudo: se ela tem raio, isto é, direção de apontamento associada, e se ela tem pose própria, isto é, se é rastreada no espaço. Um dedo na tela tem a primeira e não tem a segunda; um controle empunhado tem as duas; um mouse não tem nenhuma, e produz apontamento por um cálculo feito a partir de coordenadas de tela.
A consulta sobre tipos de sessão parece binária — suporta ou não suporta —, e bastaria um valor lógico. Bastaria, se a consulta sempre respondesse. Um navegador que não implementa a interface não responde que o aparelho não serve: ele não tem como responder coisa alguma.
src/bancada/modes/verificacao.ts
/**
* `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';Uma linha de tipo, e ela decide o comportamento de um relatório inteiro. O terceiro valor obriga todo consumidor a tratar o caso em que não se sabe, e o verificador de tipos reclama de quem esquecer. Um valor lógico teria colapsado esse caso no não, em silêncio, no ato de escrever a variável. Um relatório que diz não suporta onde deveria dizer não sei afirma, sobre o aparelho de quem está lendo, um fato errado.
Falta uma restrição que decide a forma de qualquer sonda: metade das respostas só existe dentro da 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. Antes de a sessão abrir, elas não têm valor a informar. A consulta externa é a armadilha mais confortável de todas, porque tem todas as qualidades de uma boa solução: responde rápido, funciona no computador de mesa, dispensa aparelho e gesto, e produz resultado na primeira execução. O que ela devolve é a lista do que o navegador diz suportar.
Perceba que 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 — disfarce perfeito para um defeito de código inexistente. As duas restrições juntas ditam a forma da sonda: uma função disparada por botão, e não algo que roda ao carregar a página. E há um terceiro problema que derruba mais demonstrações que os dois somados: sessões imersivas exigem contexto seguro, e um servidor de desenvolvimento com certificado autoassinado produz um aviso que precisa ser aceito em cada aparelho, uma vez. Antes disso, o botão não reage, e o sintoma não diz por quê.
1.6 Concedido, negado, e o que ninguém respondeu
Uma sessão aberta com pedido de recursos opcionais devolve a lista do que foi habilitado. O que fazer com o que ficou de fora dessa lista? A resposta óbvia é registrar como negado, e ela está errada num caso frequente.
Os dois casos limpos primeiro. Você pediu, a sessão abriu, e o nome está na lista: concedido, pode usar. Você pediu, a lista veio, e o nome não está nela: negado — pelo aparelho ou pelo navegador —, e a razão não é exposta. A plataforma não deve justificativa a ninguém, e um programa que registrasse o motivo estaria inventando o motivo junto com o registro.
Veja o terceiro caso, que estraga o esquema binário. A sessão abriu e não reportou lista alguma. Não há concessão, não há recusa, não há nada; o que existe é ausência de resposta, e registrar isso como negado equivale a preencher uma célula vazia com o valor que dá menos trabalho depois.
flowchart TD
A["a sessão abriu"] --> B{"ela reportou a lista<br/>de recursos habilitados?"}
B -->|"não reportou"| I["INDETERMINADO<br/>ausência de resposta"]
B -->|"reportou"| C{"o nome do recurso<br/>está na lista?"}
C -->|"sim"| G["CONCEDIDO<br/>pode ser usado"]
C -->|"não"| N["NEGADO<br/>recusado, e sem razão exposta"]
I -.->|"o atalho que estraga tudo"| N
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 que se sabe sobre um recurso opcional, depois de a sessão abrir, assume exatamente um de três valores. Concedido: a sessão reportou a lista dos recursos habilitados e o nome está nela. Negado: a sessão reportou a lista e o nome não está nela. Indeterminado: a sessão não reportou lista alguma.
O terceiro valor é ausência de resposta, e não resposta negativa. Confundi-los produz um relatório que afirma, com a mesma sintaxe das outras linhas, um fato que ninguém verificou.
O exemplo mínimo é o mesmo endereço aberto em dois navegadores. Num deles a lista vem completa e três recursos aparecem como negados; no outro a lista não vem, e os mesmos três aparecem como indeterminados. As duas telas se parecem e afirmam coisas diferentes. O custo de não distinguir demora a aparecer, e é essa demora que torna o defeito caro. A anotação errada é feita hoje, num relatório bem formatado que ninguém questiona. A decisão apoiada nela vem semanas depois, quando alguém resolve não oferecer um regime porque o registro dizia que o aparelho o recusava.
O padrão geral extrapola o assunto. Toda vez que um valor lógico registra o resultado de uma consulta que pode não responder, o estado de ignorância vira negativa em silêncio. E o sintoma tem assinatura reconhecível: o sistema fica mais confiante quanto menos informação recebe, porque toda ausência vira afirmação negativa segura. A defesa custa pouco — represente a ignorância como um estado e deixe o verificador de tipos cobrar de quem esquecer —, e a distinção só sobrevive se os três estados forem carregados juntos até o ponto de decisão. Qualquer camada intermediária que simplifique para dois destrói a informação sem aviso e sem erro de compilação.
Pedir um recurso exige escrever o nome exato que a especificação define: cadeia de texto sem tradução, sem sinônimo e sem tolerância a erro de digitação. Escrever errado produz um pedido que simplesmente não é atendido, sem mensagem. Isso empurra o projeto para um catálogo — e a parte que muda o comportamento de quem lê está no segundo campo dele.
o catálogo de recursos, em src/bancada/devices/recursos.ts
Cada entrada carrega, junto do nome, uma frase dizendo o que aquele recurso habilita. O campo é opcional para a máquina e é o que impede o catálogo de crescer sozinho: um recurso que ninguém justifica em uma frase entrou por precaução, e precaução é como listas ficam longas. A conta é concreta — cada recurso pedido é uma coisa a mais que a sessão pode negar, uma linha a mais no relatório e um caso a mais a tratar. Some-se que a lista precisa estar decidida antes de a sessão existir: não dá para abrir, olhar em volta e pedir mais coisas depois.
Sobra a linha que vale mais que todas as outras num relatório de capacidades, porque é a única capaz de contradizer quem o escreveu. Ela compara uma declaração feita antes contra o que a sessão informou depois. Quando os dois valores batem, a declaração foi confirmada — resultado morno e útil. Quando não batem, a frase diz qual dos dois estava errado, e não é o aparelho. O desmentido é o resultado mais valioso dos dois: custa uma correção de uma linha agora e evita construir duas etapas sobre uma premissa falsa.
1.7 O segundo estado da Bancada
A mesma página, aberta em três aparelhos, devolvendo três relatórios diferentes — e nenhum polígono ainda.
A Bancada — a oficina de montagem servida em três regimes pela mesma URL — continua sem desenhar coisa alguma. O que ela ganha aqui é a capacidade de perguntar. Um botão abre a sessão, a sonda interroga o que a sessão concede, e o resultado é escrito onde alguém consiga lê-lo.
A sonda mora em três arquivos com responsabilidades separadas. Um catálogo declara os recursos opcionais que o ambiente pode vir a pedir, com o nome literal de cada um. Um módulo de inferência lê os espaços de referência concedidos e devolve a contagem de graus, com o caso indeterminado preservado. Um contador acumula quadros com pose e quadros sem pose, sem opinar sobre o que os números significam.
O ponto de entrada monta um título, um botão e dois lugares para escrever. Antes de qualquer sondagem, ele confere se a página está em contexto seguro e avisa quando não está — e essa ordem foi escolhida. Sem conexão cifrada, a interface de sessões imersivas não é exposta, e o aparelho responderia como se não tivesse suporte nenhum. Seria uma mentira sobre o aparelho, causada pela URL.
o ponto de entrada do segundo marco, em 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 botão não é escolha de interface. O navegador recusa abrir sessão imersiva a partir de código que não veio de um gesto, e a recusa chega como exceção com nome em inglês — o disfarce perfeito para um defeito de código inexistente. A sonda é uma função disparada por um clique porque a plataforma não admite outra forma.
1.7.1 O canal que não existe dentro de um visor
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.
A consequência muda o desenho do relatório antes de mudar o seu conteúdo. 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. Texto miúdo, contraste baixo e tabela larga que exige rolagem lateral transformam um relatório correto em relatório inútil dentro do aparelho.
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 na página. Mensagem perdida por não ter onde ser escrita é a pior categoria de mensagem: ela some justamente no caso em que era mais necessária.
O outro serviço do arquivo é traduzir exceção em frase legível. Recusa de sessão chega como erro, e o erro cru — nome de classe mais uma frase em inglês — é o que faz alguém 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.
O espelho no console continua existindo, e não por hábito. Com o aparelho 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, ainda assim, é a página.
1.7.2 A linha que pode desmentir a declaração anterior
O primeiro estado da Bancada declarou, regime por regime, o que se esperava que acontecesse com o fundo. Aquele campo foi escrito como expectativa num momento em que não existia sessão para contradizê-lo. Agora existe.
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, mais adiante, sem tocar numa linha da sonda: quando a bancada existir de fato, este arquivo muda de lugar, e nada mais muda com ele.
O painel definitivo será diegético — um cartaz preso à própria bancada, lido de dentro do mundo. Ele continua sem poder existir, pela mesma razão do estado anterior: não há mundo. Enquanto o grafo de cena não chega, o relatório sai em tabela comum na página, e o arquivo diz em comentário que isso é provisório. Provisório sem nota vira decisão de projeto duas etapas adiante.
A demonstração tem uma coreografia, e ela é a evidência mais barata deste ponto do percurso. A mesma página 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, onde os espaços de referência com origem no chão aparecem e a inferência de graus devolve seis.
Um endereço, três aparelhos, três relatórios que não coincidem. Se os três coincidirem, há duas causas possíveis e nenhuma delas é o aparelho: ou a página não está em contexto seguro, ou a sonda está respondendo de fora da sessão. As duas se distinguem pela primeira linha do diário.
1.8 O que fica decidido antes do primeiro triângulo
Quatro perguntas precedem o primeiro polígono. Nenhuma delas tem resposta no código de quem projeta.
Volte à pessoa do começo, com o visor no rosto e o passo dado. Cada parte daquilo tem nome agora: o sistema estimava a orientação e ignorava a posição, o ouvido interno relatou uma translação que os olhos não confirmaram, e o mal-estar veio do desacordo. Se aquele aparelho medisse as duas metades, ele o faria por um de dois arranjos. Ou algo fixo na sala o observaria de fora, com a precisão de uma geometria conhecida e o preço da imobilidade. Ou ele mesmo se viraria, olhando o ambiente e desenhando a planta enquanto a atravessa.
Nada disso é decidido por quem escreve o ambiente. Chega pronto, do outro lado de uma interface, e a única alavanca disponível é perguntar antes de supor. É por isso que o primeiro artefato executável de um ambiente tridimensional pode não desenhar coisa alguma: ele pergunta, anota e mostra a resposta.
As quatro perguntas que precedem o primeiro polígono
Quantos parâmetros da pose o sistema estima? Três, seis, ou indeterminado — inferido dos espaços de referência concedidos, nunca lido de uma propriedade.
Onde está o que mede? Fixo no ambiente ou embarcado no corpo, com as consequências sobre onde o sistema funciona e como ele falha.
O que a sessão concedeu? A lista dos recursos habilitados, lida depois de a sessão abrir.
O que se sabe sobre o que não veio na lista? Negado, se houve lista; indeterminado, se não houve — e os dois seguem separados até o ponto de decisão.
Tudo o que foi definido aqui descreve o sistema de medida, e nada descreve o aparelho. É essa insistência que faz o vocabulário durar: descrições de aparelho envelhecem com o aparelho, e o número de maneiras de estimar uma pose no espaço não se renova a cada temporada de lançamentos. Sabendo onde está quem observa, resta decidir onde ficam as coisas que ele vai olhar — em relação a quê, em que ordem e com que custo por quadro.
O que segue leva essas quatro perguntas para dentro de um artefato que roda.
Tarefa 1: Construir a sonda de capacidades
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.
Tarefa 2: Tornar o relatório visível
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.