1 Realidade virtual não imersiva — Infográfico do código
O código deste módulo faz da câmera da cena uma câmera em órbita, que gira em volta de um ponto, o alvo, sempre olhando para ele; confere, peça a peça, se essa câmera consegue mostrar inteira cada peça do mecanismo; e escreve na página o que o regime em janela (a cena mostrada numa tela comum, de computador ou de celular) não oferece. Este infográfico acompanha uma execução só: da abertura da página do módulo, a que esse código monta no navegador, passando por um arrasto, dois passos da roda do mouse, uma rolagem de lado, dois dedos na tela e dois botões, com um clique em “Medir agora” depois de cada gesto e outro no fim.
1.1 O caminho
Os gestos e os botões entram em cima e mudam os números da órbita. Embaixo fica a folha, o texto que o programa escreve na tela, abaixo do desenho, com os números desta execução: ela só é reescrita nos momentos da segunda fileira de entradas, e é nessa hora que lê os números da câmera (a seta pontilhada).
custoContraOTeto() só roda no clique em “Medir agora”.
1.2 Os arquivos
Cada arquivo do módulo, o que ele declara e o que ele usa de módulos anteriores.
| Arquivo | O que declara | O que usa de antes |
|---|---|---|
orbita.ts |
as travas em TRAVAS_DA_ORBITA (o menor e o maior valor que a órbita aceita: 0° e 85° para a elevação, 0,35 m e 8 m para a distância), GRAUS_POR_PIXEL (0,35), FATOR_DA_RODA (1,12), MARGEM_DE_ENQUADRAMENTO (1,25), ALVO_INICIAL, INSTRUCAO_DA_ORBITA, os tipos EstadoDaOrbita, Enquadramento e Orbita, e as funções caixaDoQueSeVe(), planejarEnquadramento() e montarOrbita(); esta guarda dentro de si a contagem escritas e as funções aplicar(), girar(), aproximar(), olharPara(), distanciaEntreDoisDedos(), aoDescer(), aoMover(), aoSubir() e aoRodar(), e devolve olharPara(), enquadrar(), estado(), descrever() e encerrar() |
ALTURA_DO_TAMPO, do módulo do grafo de cena; da biblioteca, Box3, Sphere, MathUtils, Mesh, InstancedMesh, Object3D, Vector3 e a câmera PerspectiveCamera |
janela.ts, o do regime |
os tipos CoberturaDaPeca, EntregaDaJanela, Afericao, LimiteDoRegime e LimiteAferido, FATOR_DE_IMAGEM_PERDIDA (1,5), LIMITES_DA_JANELA e as funções medirEntrega(), custoContraOTeto(), aferirLimites() e linhasDaJanela() |
caixaDoQueSeVe(), planejarEnquadramento() e TRAVAS_DA_ORBITA, de orbita.ts; regimePorId() e o tipo Peca, do módulo do contínuo; o tipo LeituraDoOrcamento, do módulo do grafo de cena; da biblioteca, Object3D, PerspectiveCamera e WebGLRenderer |
janela.ts, o da folha |
montarRegimeEmJanela() |
paragrafo() e subtitulo(), do módulo de dispositivos |
06_demo.ts |
a página do módulo: TETO_DO_QUADRO_MS (16,7 ms, o mesmo valor de TETO_DESKTOP_MS), MAIOR_LADO_DA_MORSA_M, POSICAO_DA_MORSA, PECAS_DE_REPOSICAO e REMOCAO_NO_NIVEL_DISTANTE, com os valores do módulo anterior; a lista pedidos, com um botão de enquadramento por entrada; o botão “Medir agora”; atualizarFolhas(); e a ordem em que as funções rodam |
montarPalco(), montarCena(), montarLaco(), Orcamento e TETO_DESKTOP_MS, do módulo do grafo de cena; BANCADA, do módulo do contínuo; vestirCena(), extrudarPerfil() e perfilRegular(), do módulo de modelagem; paragrafo(), do módulo de dispositivos; a morsa, a borda parafusada, a prateleira, a medição e a folha do módulo sobre ativos e orçamento de cena, com importarAtivo(), montarPrateleira(), linhasDaMedicao() e montarAtivosECusto(), entre outras |
1.3 Os dados na memória
Os tipos que o módulo define, campo a campo, com o valor que cada campo guarda nesta execução.
1.3.1 A órbita
Os números da órbita não ficam num objeto com campos: são variáveis de montarOrbita(), que só as funções declaradas ali dentro leem e escrevem. O tipo EstadoDaOrbita copia o alvo, a distância, os dois ângulos e a contagem de escritas para quem os pedir por estado(), que a página do módulo não chama. As travas são uma constante, e a da distância máxima fica abaixo dos 50 m em que o palco corta o desenho (o palco é a parte do programa que cria a tela de desenho e a câmera):
orbita.ts
/** As travas da órbita: até onde ela deixa a câmera ir. Ângulos em graus, distâncias em metros. */
export const TRAVAS_DA_ORBITA = {
/** A câmera não desce abaixo da altura do alvo: dali para baixo, o tampo fica entre ela e as peças. */
elevacaoMinimaGraus: 0,
/**
* Para cinco graus antes de ficar exatamente acima do alvo. Ali, olhar para o
* alvo deixa de dizer qual lado da imagem fica em cima, e passar dali vira a
* imagem de cabeça para baixo e inverte o sentido do arrasto.
*/
elevacaoMaximaGraus: 85,
/** O alvo inicial fica dentro do corpo, a peça do meio do tampo; sem esta trava, a câmera entraria nele. */
raioMinimoM: 0.35,
/** Sem esta trava, a roda levaria a câmera além dos 50 m em que o palco corta o desenho, e a cena sumiria. */
raioMaximoM: 8,
} as const;
| Variável | O que guarda | Nesta execução |
|---|---|---|
alvo |
o ponto para onde a câmera olha | ao fim, (0,00; 0,57; −1,24), o ponto que o botão “A cena inteira” pôs como alvo; na abertura, o alvo inicial, (0; 0,95; 0) |
raio |
a distância da câmera ao alvo, que o código chama de raio | ao fim, 5,07 m |
azimute, elevacao |
os dois ângulos, em graus | ao fim, −35,0 e 35,8 |
escritas |
quantas vezes a órbita escreveu a posição da câmera desde que foi instalada | ao fim, 25 |
ponteiros |
o mapa de cada ponteiro apertado agora (o mouse, cada dedo, a caneta), pelo número que o navegador dá a ele, com a última posição na tela | os dois dedos, durante a pinça, o gesto de afastar ou aproximar dois dedos na tela |
distanciaAnterior |
a distância entre os dois dedos, em pixels, no aviso anterior (cada aviso do navegador, o evento do código, traz o número do ponteiro e a posição dele na tela) |
100 px quando o segundo dedo pousou, 200 px no último movimento |
aplicar() escreve a posição da câmera. A prateleira do fundo lê a câmera a cada quadro, cada imagem que o aparelho mostra, para escolher entre a versão de perto das peças de reposição e a de longe, com menos triângulos.
1.3.2 O enquadramento
Para enquadrar, a órbita põe o alvo no centro da esfera envolvente e leva a câmera à distância em que a esfera cabe na altura da tela. A esfera sai da caixa envolvente do objeto, a menor caixa, com as arestas paralelas às direções x, y e z, que contém todos os cantos dos triângulos que formam a superfície dele: o centro da esfera é o centro dessa caixa, e o raio é a metade da diagonal dela. A distância pedida é a distância em que a esfera encosta nas bordas de cima e de baixo da tela, multiplicada pela margem, 1,25, para a cena não encostar nelas; o campo de visão vertical, o ângulo que a tela mostra de cima a baixo, 55° nesta câmera, entra nessa conta. A distância concedida é a pedida presa entre as travas de distância, e é a que a câmera usa; a fração da altura é quanto da altura da tela o diâmetro da esfera ocupa na distância concedida.
Campo de Enquadramento |
A cena inteira | A engrenagem pequena |
|---|---|---|
centro, que vira o alvo |
(0,00; 0,57; −1,24) | (0,47; 0,93; −0,05) |
raioDaEsferaM |
1,87 m | cerca de 0,067 m, que a folha não escreve: sai da distância pedida, 0,18 × 0,462 ÷ 1,25, com 0,462 o seno de 27,5°, a metade do campo de visão |
distanciaPedidaM |
5,07 m | 0,18 m |
distanciaConcedidaM |
5,07 m, entre as travas | 0,35 m, a trava mínima |
fracaoDaAltura |
0,76, que a folha também não escreve: com a distância pedida dentro das travas, a conta dá sempre 0,76 | 0,37 |
1.3.3 A conferência da entrega
O nome que o projeto dá à passagem de cada peça do domínio pela conta do enquadramento é conferência da entrega. medirEntrega() guarda o resultado numa EntregaDaJanela, com uma CoberturaDaPeca por peça.
| Campo | O que guarda | Nesta execução |
|---|---|---|
EntregaDaJanela.regime |
a declaração do regime em janela, lida por regimePorId('inline') |
“Realidade virtual não imersiva” |
EntregaDaJanela.cena |
o enquadramento da cena inteira | o da tabela acima |
EntregaDaJanela.pecas |
uma CoberturaDaPeca por peça, com peca, enquadramento e coberta |
cinco, na tabela abaixo |
EntregaDaJanela.cobertas |
quantas peças estão cobertas pela órbita: a cobertura do domínio | 5 |
| Peça | Distância pedida | Distância concedida | Fração da altura | coberta |
|---|---|---|---|---|
| Corpo | 0,24 m | 0,35 m | 51% | true |
| Eixo | 0,30 m | 0,35 m | 65% | true |
| Engrenagem grande | 0,30 m | 0,35 m | 64% | true |
| Engrenagem pequena | 0,18 m | 0,35 m | 37% | true |
| Tampa | 0,25 m | 0,35 m | 52% | true |
1.3.4 Os limites
Cada um dos três limites do regime fala de uma destas coisas: a profundidade inferida é a distância que quem olha calcula a partir de pistas da própria imagem; a escala corporal é a comparação entre o tamanho das coisas da cena e o corpo de quem as vê; e o alcance de braço é a distância até onde a mão chega sem dar um passo. Cada limite tem quatro partes:
janela.ts (o regime)
export interface LimiteDoRegime {
readonly id: 'profundidade-inferida' | 'sem-escala-corporal' | 'sem-alcance-de-braco';
/** O que falta, numa frase que quem lê consegue conferir. */
readonly enunciado: string;
/** Por que falta: a razão técnica. */
readonly porque: string;
/** O que a falta impede de demonstrar neste regime. */
readonly impedeDemonstrar: string;
}
LimiteDoRegime.id |
O que a falta impede de demonstrar | LimiteAferido.afericao |
Por quê, nesta execução |
|---|---|---|---|
'profundidade-inferida' |
julgar, sem girar a câmera, qual de duas peças próximas está mais perto | 'confirmado' |
a biblioteca entrega uma imagem por quadro, para a tela, e não uma para cada olho |
'sem-escala-corporal' |
que o tampo tem a altura certa para trabalhar de pé | 'confirmado' |
o espaço de referência declarado para o regime (o sistema de coordenadas em que as posições do ambiente são expressas, junto com a regra que fixa a origem) é o 'viewer', que acompanha o olhar e não tem chão; e nenhum visor (o aparelho preso à cabeça que cobre os olhos) está apresentando imagem |
'sem-alcance-de-braco' |
que uma peça está longe demais para ser apanhada sem dar um passo até ela | 'inconclusivo' |
nada no programa mede o braço de quem usa |
1.4 As funções
Cada função que decide alguma coisa, com o que recebe, o que decide e o que devolve nesta execução.
1.4.1 montarOrbita()
| Recebe | Decide | Devolve |
|---|---|---|
palco.camera, a câmera que o palco pôs em (0; 1,55; 1,50), e a tela de desenho, que recebe os gestos |
a partida: mede o caminho do alvo inicial, em (0; 0,95; 0), até a câmera (o deslocamento do código), que dá (0; 0,60; 1,50), e tira desse caminho os três números, com a distância e a elevação já presas às travas; depois põe os ouvintes dos gestos e escreve a câmera uma vez |
uma Orbita, com 1,62 m, 21,8° e azimute 0,0°, e a contagem em 1 |
orbita.ts
// A órbita começa onde a câmera já está. Os três números saem da posição que o
// palco deu à câmera, medida a partir do alvo, e não de valores escritos aqui.
const alvo: Vector3 = ALVO_INICIAL.clone();
const deslocamento: Vector3 = camera.position.clone().sub(alvo);
let raio: number = MathUtils.clamp(
deslocamento.length(),
TRAVAS_DA_ORBITA.raioMinimoM,
TRAVAS_DA_ORBITA.raioMaximoM,
);
let azimute: number = MathUtils.radToDeg(Math.atan2(deslocamento.x, deslocamento.z));
let elevacao: number = MathUtils.clamp(
MathUtils.radToDeg(Math.asin(deslocamento.y / deslocamento.length())),
TRAVAS_DA_ORBITA.elevacaoMinimaGraus,
TRAVAS_DA_ORBITA.elevacaoMaximaGraus,
);
let escritas: number = 0;
1.4.2 aplicar()
| Recebe | Decide | Devolve |
|---|---|---|
nenhum parâmetro; lê de montarOrbita() o alvo, a distância (raio) e os dois ângulos |
a ordem: primeiro a posição, que sai sempre dos três números; depois a direção do olhar, para o alvo; por fim, mais um na contagem | nada: escreve a câmera, o que nesta execução acontece 25 vezes |
orbita.ts
function aplicar(): void {
const a: number = MathUtils.degToRad(azimute);
const e: number = MathUtils.degToRad(elevacao);
// O raio visto de cima, a sombra dele no chão. É esse comprimento que o
// azimute reparte entre x e z.
const sombra: number = raio * Math.cos(e);
camera.position.set(
alvo.x + sombra * Math.sin(a),
alvo.y + raio * Math.sin(e),
alvo.z + sombra * Math.cos(a),
);
camera.lookAt(alvo);
escritas += 1;
}
1.4.3 girar()
| Recebe | Decide | Devolve |
|---|---|---|
dx e dy, os pixels que o ponteiro andou desde o aviso anterior; no arrasto, 10 e 4 a cada movimento |
o sinal: o azimute diminui quando a mão vai para a direita, e a elevação sobe quando a mão vai para baixo; a elevação fica presa entre 0° e 85°, e o azimute não tem trava | nada: chama aplicar(); depois dos dez movimentos, azimute −35,0° e elevação 35,8° |
orbita.ts
function girar(dx: number, dy: number): void {
// Arrastar para a direita leva a câmera para a esquerda em volta do alvo, e a
// cena parece girar para a direita, junto com a mão.
azimute -= dx * GRAUS_POR_PIXEL;
elevacao = MathUtils.clamp(
elevacao + dy * GRAUS_POR_PIXEL,
TRAVAS_DA_ORBITA.elevacaoMinimaGraus,
TRAVAS_DA_ORBITA.elevacaoMaximaGraus,
);
aplicar();
}
girar() e termina em aplicar(). A elevação vai a 21,8 + 40 × 0,35 = 35,8 graus, abaixo da trava de 85, que não age. O mesmo fluxo segue pelos dois passos da roda, que aoRodar() atende.
1.4.4 aoDescer() e aoMover()
| Recebe | Decide | Devolve |
|---|---|---|
| um aviso do navegador | aoDescer() guarda o ponteiro no mapa ponteiros e mede a referência da pinça no pouso, com distanciaEntreDoisDedos(), que devolve 0 enquanto não há dois ponteiros no mapa; aoMover() escolhe um de três casos: com o ponteiro fora do mapa, sai sem fazer nada; com dois ou mais no mapa, chama aproximar() com a distância anterior entre os dedos dividida pela de agora, quando as duas passam de zero; com um só, chama girar(). aoSubir() tira o ponteiro do mapa e mede de novo a distância entre os dedos |
nada; nesta execução, girar() dez vezes no arrasto e aproximar() dez vezes com os dois dedos, de 1,62 m a 0,81 m |
orbita.ts
// Os ponteiros apertados agora, pelo número que o navegador dá a cada um. Com
// um só, o arrasto gira; com dois dedos na tela, a distância entre eles muda o
// raio, e medir essa distância exige guardar os dois.
const ponteiros: Map<number, { x: number; y: number }> = new Map();
let distanciaAnterior: number = 0;
function distanciaEntreDoisDedos(): number {
const [primeiro, segundo] = [...ponteiros.values()];
if (primeiro === undefined || segundo === undefined) {
return 0;
}
return Math.hypot(primeiro.x - segundo.x, primeiro.y - segundo.y);
}
orbita.ts
function aoDescer(evento: PointerEvent): void {
ponteiros.set(evento.pointerId, { x: evento.clientX, y: evento.clientY });
// A referência da pinça é medida no instante em que o segundo dedo pousa.
// Medida só no primeiro movimento, o trecho inicial do gesto se perderia.
distanciaAnterior = distanciaEntreDoisDedos();
// Os movimentos deste ponteiro continuam chegando a este elemento mesmo se o
// cursor sair dele no meio do arrasto.
elemento.setPointerCapture(evento.pointerId);
}
orbita.ts
function aoMover(evento: PointerEvent): void {
const anterior: { x: number; y: number } | undefined = ponteiros.get(evento.pointerId);
// Mouse passando sem botão apertado: não há arrasto a seguir.
if (anterior === undefined) {
return;
}
const dx: number = evento.clientX - anterior.x;
const dy: number = evento.clientY - anterior.y;
ponteiros.set(evento.pointerId, { x: evento.clientX, y: evento.clientY });
if (ponteiros.size >= 2) {
const agora: number = distanciaEntreDoisDedos();
// Dedos que se afastam dão razão menor que 1, e o raio encolhe: a câmera
// chega perto. A razão, e não a diferença em pixels, faz o gesto render o
// mesmo perto e longe do alvo.
if (distanciaAnterior > 0 && agora > 0) {
aproximar(distanciaAnterior / agora);
}
distanciaAnterior = agora;
return;
}
girar(dx, dy);
}
aoMover() lado a lado: o mouse que passa sem botão não escreve nada; nesta execução, o arrasto escreveu dez vezes e os dois dedos, dez.
1.4.5 aoRodar() e aproximar()
| Recebe | Decide | Devolve |
|---|---|---|
um passo da roda, com deltaY, o valor vertical |
primeiro o zero: a rolagem de lado chega com deltaY zero, e aoRodar() sai sem mexer na distância; depois o sinal: para baixo, aproximar(FATOR_DA_RODA), e para cima, aproximar(1 / FATOR_DA_RODA). aproximar() multiplica a distância pelo número que recebe e a prende entre as travas: com 1,12, apesar do nome, ela afasta a câmera |
nada; 1,62 m vira 1,81 m e volta a 1,62 m, e a rolagem de lado deixa a distância em 1,62 m e a contagem em 13 |
orbita.ts
function aoRodar(evento: WheelEvent): void {
// Sem isto, a roda também rolaria a página enquanto a câmera se move.
evento.preventDefault();
// A rolagem de lado, que o trackpad produz, chega com deltaY zero.
if (evento.deltaY === 0) {
return;
}
// Afastar multiplica e aproximar divide pelo mesmo fator: um passo para cada
// lado devolve a câmera à mesma distância.
aproximar(evento.deltaY > 0 ? FATOR_DA_RODA : 1 / FATOR_DA_RODA);
}
aproximar() e olharPara(), que o enquadramento também usa, prendem a distância nas mesmas travas antes de chamar aplicar():
orbita.ts
function aproximar(fator: number): void {
raio = MathUtils.clamp(raio * fator, TRAVAS_DA_ORBITA.raioMinimoM, TRAVAS_DA_ORBITA.raioMaximoM);
aplicar();
}
function olharPara(novoAlvo: Vector3, novoRaio: number): void {
alvo.copy(novoAlvo);
raio = MathUtils.clamp(novoRaio, TRAVAS_DA_ORBITA.raioMinimoM, TRAVAS_DA_ORBITA.raioMaximoM);
aplicar();
}
1.4.6 caixaDoQueSeVe()
| Recebe | Decide | Devolve |
|---|---|---|
um nó da cena (cada objeto da cena é um nó, e um nó pode ficar pendurado em outro): cada peça do domínio e a raiz cena.sala, toda vez que a folha é reescrita, e o nó de cada botão clicado |
só os nós visíveis entram; para cada um, um de três casos: lote (o conjunto de cópias desenhado numa só chamada de desenho, o pedido que o programa faz à placa de vídeo para desenhar um objeto), com a caixa que cobre todas as cópias; nó de malha (o nó que carrega uma malha, a forma de uma peça guardada em listas de números), com a caixa da malha levada ao mundo, as coordenadas da cena inteira, no lugar que o nó ocupa nela; nenhum dos dois, nada | a caixa do que aparece na tela |
orbita.ts
export function caixaDoQueSeVe(no: Object3D): Box3 {
const caixa: Box3 = new Box3();
const parte: Box3 = new Box3();
no.updateWorldMatrix(true, true);
// Nó escondido é pulado junto com tudo o que está pendurado nele.
no.traverseVisible((filho: Object3D) => {
if (filho instanceof InstancedMesh) {
// O lote tem caixa própria, que cobre todas as cópias de uma vez.
filho.computeBoundingBox();
if (filho.boundingBox !== null) {
caixa.union(parte.copy(filho.boundingBox).applyMatrix4(filho.matrixWorld));
}
return;
}
if (filho instanceof Mesh) {
if (filho.geometry.boundingBox === null) {
filho.geometry.computeBoundingBox();
}
if (filho.geometry.boundingBox !== null) {
caixa.union(parte.copy(filho.geometry.boundingBox).applyMatrix4(filho.matrixWorld));
}
}
});
return caixa;
}
1.4.7 planejarEnquadramento() e enquadrar()
| Recebe | Decide | Devolve |
|---|---|---|
| a caixa e o campo de visão da câmera, 55° | com a caixa vazia, devolve undefined, e enquadrar() devolve false sem mover a câmera; nas outras, a distância pedida pela esfera, a concedida pelas travas e a fração da altura |
um Enquadramento: na cena inteira, esfera de 1,87 m de raio e 5,07 m pedidos e concedidos; na engrenagem pequena, 0,18 m pedidos e 0,35 m concedidos. enquadrar() passa o centro e a distância concedida a olharPara(), que muda o alvo e a distância e chama aplicar() |
orbita.ts
export function planejarEnquadramento(
caixa: Box3,
campoVerticalGraus: number,
): Enquadramento | undefined {
// Caixa vazia não tem centro: a biblioteca devolveria uma esfera de raio -1.
if (caixa.isEmpty()) {
return undefined;
}
const esfera: Sphere = caixa.getBoundingSphere(new Sphere());
const meioCampo: number = MathUtils.degToRad(campoVerticalGraus / 2);
// A esfera encosta na borda de cima e na de baixo da tela quando a distância
// até o centro dela é o raio dividido pelo seno da metade do campo de visão.
const pedida: number = (esfera.radius / Math.sin(meioCampo)) * MARGEM_DE_ENQUADRAMENTO;
const concedida: number = MathUtils.clamp(
pedida,
TRAVAS_DA_ORBITA.raioMinimoM,
TRAVAS_DA_ORBITA.raioMaximoM,
);
// Vista da câmera, a esfera se abre num ângulo cujo seno é raio sobre
// distância; na tela, conta a tangente desse ângulo contra a do meio campo.
// A conta mede só a altura da tela, e vale porque a tela de desenho da página
// tem proporção 16 por 9: mais larga do que alta, é a altura que encosta
// primeiro. Numa tela mais alta do que larga, a esfera sairia pelos lados com
// a fração ainda abaixo de 1.
const angulo: number = Math.asin(Math.min(1, esfera.radius / concedida));
return {
centro: esfera.center.clone(),
raioDaEsferaM: esfera.radius,
distanciaPedidaM: pedida,
distanciaConcedidaM: concedida,
fracaoDaAltura: Math.tan(angulo) / Math.tan(meioCampo),
};
}
orbita.ts
enquadrar(caixa: Box3): boolean {
const plano: Enquadramento | undefined = planejarEnquadramento(caixa, camera.fov);
// Sem esta saída, a câmera iria para perto da origem do mundo, no chão
// embaixo da bancada.
if (plano === undefined) {
return false;
}
olharPara(plano.centro, plano.distanciaConcedidaM);
return true;
},
1.4.8 medirEntrega()
| Recebe | Decide | Devolve |
|---|---|---|
cena.sala, a lista das cinco peças do domínio e a câmera |
para cada peça, procura o nó pelo nome; sem nó, a peça entra na lista como não coberta, sem interromper as outras; com nó, passa pela mesma planejarEnquadramento() que a órbita usa para mover a câmera, e está coberta quando a fração da altura é no máximo 1 |
uma EntregaDaJanela com 5 de 5 peças cobertas |
janela.ts (o regime)
export function medirEntrega(
raiz: Object3D,
pecas: readonly Peca[],
camera: PerspectiveCamera,
): EntregaDaJanela {
const coberturas: CoberturaDaPeca[] = [];
let cobertas: number = 0;
for (const peca of pecas) {
const no: Object3D | undefined = raiz.getObjectByName(peca.id);
const plano: Enquadramento | undefined =
no === undefined ? undefined : planejarEnquadramento(caixaDoQueSeVe(no), camera.fov);
const coberta: boolean = plano !== undefined && plano.fracaoDaAltura <= 1;
if (coberta) {
cobertas += 1;
}
coberturas.push({ peca, enquadramento: plano, coberta });
}
return {
regime: regimePorId('inline'),
cena: planejarEnquadramento(caixaDoQueSeVe(raiz), camera.fov),
pecas: coberturas,
cobertas,
};
}
cobertas; a cena inteira passa pela conta no fim, a 5,07 m. A peça que não estivesse na cena seguiria para a lista sem enquadramento, e a folha a escreveria como “não está na cena”.
1.4.9 custoContraOTeto()
custoContraOTeto() julga o trabalho do código contra o tempo disponível para cada quadro. O custo de um quadro é o tempo que o trabalho do próprio programa consome no processador; o intervalo, o tempo real entre duas imagens apresentadas; e o teto, o tempo que o programa tem para preparar um quadro antes que o aparelho precise do seguinte, 16,7 ms na página do módulo. O medidor é a parte do programa que compara o tempo de cada quadro com o teto e guarda os números dos últimos 120 quadros.
| Recebe | Decide | Devolve |
|---|---|---|
o que o medidor devolve, orcamento.ler() |
sem quadro medido, uma linha só; nos outros casos, a folga (o teto menos o custo médio, em milissegundos), escrita como “dentro do teto” ou “acima do teto”; e uma terceira linha quando o pior intervalo (o mais longo dos últimos 120 quadros) passa de uma vez e meia o teto, 25,05 ms: é o sinal de imagem perdida, a imagem que não ficou pronta a tempo e fez a tela repetir a anterior; com imagem perdida e o custo médio dentro do teto, essa linha diz onde procurar o atraso, e um dos lugares é o compositor do sistema (a parte do sistema operacional que junta na tela o que todos os programas desenham) | duas linhas nesta execução, com 120 quadros medidos e o custo médio dentro do teto; os tempos são da máquina que rodou a página, e não aparecem neste infográfico |
janela.ts (o regime)
export function custoContraOTeto(medida: LeituraDoOrcamento): string[] {
if (medida.quadrosMedidos === 0) {
return ['Custo do quadro: ainda sem quadros medidos.'];
}
const folga: number = medida.tetoMs - medida.custoMedioMs;
const veredito: string =
folga >= 0
? `dentro do teto de ${medida.tetoMs.toFixed(1)} ms, com ${folga.toFixed(1)} ms de folga`
: `acima do teto de ${medida.tetoMs.toFixed(1)} ms em ${(-folga).toFixed(1)} ms`;
const linhas: string[] = [
`Trabalho do código por quadro: ${medida.custoMedioMs.toFixed(2)} ms em média, ` +
`em ${medida.quadrosMedidos} ${medida.quadrosMedidos === 1 ? 'quadro' : 'quadros'}, ${veredito}`,
`Intervalo entre imagens: ${medida.intervaloMedioMs.toFixed(1)} ms em média, ` +
`o pior ${medida.piorIntervaloMs.toFixed(1)} ms`,
];
if (medida.piorIntervaloMs > medida.tetoMs * FATOR_DE_IMAGEM_PERDIDA) {
linhas.push(
folga >= 0
? 'Houve imagem perdida com o custo médio dentro do teto: a média não mostra o quadro ' +
'perdido, e o atraso pode ter vindo de um quadro em que o código passou do teto ou de ' +
'fora do código medido, da placa de vídeo, do compositor do sistema ou de outro programa'
: 'Houve imagem perdida, e o trabalho do código sozinho já passa do teto',
);
}
return linhas;
}
1.4.10 aferirLimites()
| Recebe | Decide | Devolve |
|---|---|---|
palco.renderer, o renderizador, a parte da biblioteca que desenha a cena |
um caso por limite, sem caso padrão: o limite da profundidade inferida sai confirmado quando o renderizador entrega uma imagem por quadro, e contrariado com um visor apresentando; o da falta de escala corporal, confirmado sem visor e com o regime declarado no espaço de referência 'viewer'; o da falta de alcance de braço, inconclusivo sem consultar nada |
três LimiteAferido: confirmado, confirmado e inconclusivo |
janela.ts (o regime)
export function aferirLimites(renderer: WebGLRenderer): LimiteAferido[] {
// Verdadeiro só quando o renderizador entrega imagem a um visor, uma para cada olho.
const emVisor: boolean = renderer.xr.isPresenting;
const declarado: Regime = regimePorId('inline');
return LIMITES_DA_JANELA.map((limite: LimiteDoRegime): LimiteAferido => {
switch (limite.id) {
case 'profundidade-inferida':
return {
limite,
afericao: emVisor ? 'contrariado' : 'confirmado',
observacao: emVisor
? 'o renderizador está entregando uma imagem para cada olho'
: 'o renderizador desenha uma imagem por quadro, a da câmera da página',
};
case 'sem-escala-corporal':
return {
limite,
afericao: !emVisor && declarado.espacoDeReferencia === 'viewer' ? 'confirmado' : 'contrariado',
observacao:
`o regime foi declarado com o espaço de referência '${declarado.espacoDeReferencia}', ` +
'que acompanha o olhar e não tem chão' +
(emVisor ? ', mas há um visor apresentando, e com ele um chão' : ', e nenhum visor está apresentando'),
};
case 'sem-alcance-de-braco':
return {
limite,
afericao: 'inconclusivo',
observacao: 'nada no programa mede o braço de quem usa: o cursor não tem corpo',
};
}
});
}
switch, um por ramo, com o resultado desta execução, sem visor: confirmado, confirmado e inconclusivo.
1.4.11 descrever() e linhasDaJanela()
| Recebe | Decide | Devolve |
|---|---|---|
descrever(): nenhum parâmetro, e lê de montarOrbita() o alvo, os três números e a contagem; linhasDaJanela(): a entrega e os três limites aferidos |
descrever() escreve “vez” ou “vezes” pela contagem e tira o sinal de um zero que sairia −0.00; linhasDaJanela() só escreve a linha da cena inteira quando ela tem enquadramento, escreve “não está na cena” para a peça sem enquadramento e fecha com a frase da declaração do regime imersivo, o do visor |
as linhas da folha: as quatro da órbita e as da entrega e dos limites |
orbita.ts
descrever(): string[] {
return [
// Um valor como -0,001 sairia escrito -0.00; a troca tira o sinal desse zero.
`Alvo da órbita: ${[alvo.x, alvo.y, alvo.z].map((v: number) => v.toFixed(2).replace('-0.00', '0.00')).join(', ')} m`,
`Azimute ${azimute.toFixed(1)} graus, elevação ${elevacao.toFixed(1)} graus, ` +
`distância ${raio.toFixed(2)} m`,
`Posição da câmera escrita ${escritas} ${escritas === 1 ? 'vez' : 'vezes'} desde que a página abriu`,
`Travas: elevação de ${TRAVAS_DA_ORBITA.elevacaoMinimaGraus} a ` +
`${TRAVAS_DA_ORBITA.elevacaoMaximaGraus} graus, distância de ` +
`${TRAVAS_DA_ORBITA.raioMinimoM.toFixed(2)} a ${TRAVAS_DA_ORBITA.raioMaximoM.toFixed(0)} m`,
];
},
janela.ts (o regime)
export function linhasDaJanela(entrega: EntregaDaJanela, aferidos: readonly LimiteAferido[]): string[] {
const linhas: string[] = [
`Regime: ${entrega.regime.nome}. ${entrega.regime.papel}`,
`O que ele rastreia, como foi declarado: ${entrega.regime.rastreia}`,
];
if (entrega.cena !== undefined) {
linhas.push(
`A cena inteira cabe na tela a ${entrega.cena.distanciaConcedidaM.toFixed(2)} m do centro dela ` +
`(esfera de ${entrega.cena.raioDaEsferaM.toFixed(2)} m de raio; a órbita vai até ` +
`${TRAVAS_DA_ORBITA.raioMaximoM} m)`,
);
}
linhas.push('', `Peças do domínio cobertas pela órbita: ${entrega.cobertas} de ${entrega.pecas.length}`);
for (const cobertura of entrega.pecas) {
const plano: Enquadramento | undefined = cobertura.enquadramento;
if (plano === undefined) {
linhas.push(` ${cobertura.peca.nome}: não está na cena`);
continue;
}
linhas.push(
` ${cobertura.peca.nome}: a ${plano.distanciaConcedidaM.toFixed(2)} m ` +
`(a conta pedia ${plano.distanciaPedidaM.toFixed(2)} m), ocupa ` +
`${(plano.fracaoDaAltura * 100).toFixed(0)}% da altura da tela`,
);
}
linhas.push('', 'O que este regime não oferece:');
for (const aferido of aferidos) {
linhas.push(
`${aferido.limite.enunciado} [${aferido.afericao}]`,
` Por quê: ${aferido.limite.porque}.`,
` Impede demonstrar: ${aferido.limite.impedeDemonstrar}.`,
` Aferição: ${aferido.observacao}.`,
);
}
linhas.push('', `Contraste, como foi declarado para o regime imersivo: ${regimePorId('immersive-vr').papel}`);
return linhas;
}
1.4.12 montarRegimeEmJanela()
| Recebe | Decide | Devolve |
|---|---|---|
| o lugar da tela em que a folha é escrita, as linhas da órbita e as do regime | a ordem: primeiro a câmera em órbita, em parágrafos; depois o que a janela entrega e não oferece, num bloco de texto que guarda as quebras de linha | nada: reescreve a folha inteira, o que nesta execução acontece dez vezes: uma na abertura, uma na chegada da morsa, duas nos botões e seis nos cliques em “Medir agora” |
janela.ts (a folha)
export function montarRegimeEmJanela(
raiz: HTMLElement,
orbita: readonly string[],
regime: readonly string[],
): void {
raiz.replaceChildren();
raiz.appendChild(subtitulo('A câmera em órbita'));
for (const linha of orbita) {
raiz.appendChild(paragrafo(linha));
}
raiz.appendChild(subtitulo('O que a janela entrega, e o que ela não oferece'));
const bloco: HTMLPreElement = document.createElement('pre');
bloco.textContent = regime.join('\n');
raiz.appendChild(bloco);
}
1.4.13 Os botões de enquadramento e atualizarFolhas()
| Recebe | Decide | Devolve |
|---|---|---|
| a lista das peças do domínio e os nós da cena | uma entrada para a cena inteira, uma para cada peça que está na cena e uma para a prateleira; peça fora da cena não ganha botão; cada clique pede o enquadramento à órbita e chama atualizarFolhas(), que reescreve a folha |
sete botões: A cena inteira, Corpo, Eixo, Engrenagem grande, Engrenagem pequena, Tampa e A prateleira do fundo |
06_demo.ts
// Cada botão de enquadramento pede à órbita um nó da cena, e a distância sai da
// caixa dele, e não de um número escrito aqui. A câmera só se move pela órbita:
// escrever a posição dela direto daqui seria desfeito no primeiro arrasto.
const pedidos: { rotulo: string; no: Object3D }[] = [{ rotulo: 'A cena inteira', no: cena.sala }];
for (const peca of BANCADA.pecas) {
const no: Object3D | undefined = cena.pecas.get(peca.id);
if (no !== undefined) {
pedidos.push({ rotulo: peca.nome, no });
}
}
pedidos.push({ rotulo: 'A prateleira do fundo', no: prateleira.no });
for (const pedido of pedidos) {
const botao: HTMLButtonElement = criarNo<HTMLButtonElement>('button', barraDeEnquadramento);
botao.type = 'button';
botao.textContent = pedido.rotulo;
botao.addEventListener('click', () => {
orbita.enquadrar(caixaDoQueSeVe(pedido.no));
atualizarFolhas();
});
}
1.5 Uma execução, passo a passo
As abas seguem uma execução desde a abertura da página do módulo até o último clique em “Medir agora”. A folha só é reescrita em quatro momentos: na abertura, quando chega a morsa (a peça trazida de fora no módulo sobre ativos), a cada botão e a cada clique em “Medir agora”. Por isso, depois de cada gesto, um clique em “Medir agora” a reescreveu, e é dessa folha que vem cada número das abas. Os desenhos das abas mostram só o gesto, e esses cliques de leitura não aparecem destacados neles. Em cada desenho, a órbita fica em cima e a folha embaixo. O que roda tem borda grossa e fundo amarelo; o que a aba muda tem borda grossa, fundo verde e letra em negrito; o que rodou numa aba anterior tem borda tracejada e fundo cinza; e o que ainda não rodou fica sem destaque.
montarOrbita() mede o caminho do alvo inicial, em (0; 0,95; 0), até a câmera, (0; 0,60; 1,50), e guarda a distância de 1,62 m, a elevação de 21,8° e o azimute de 0,0°. aplicar() escreve a câmera em (0; 1,55; 1,50), o lugar em que ela já estava, e a contagem de escritas fica em 1.
medirEntrega() passa cada peça do domínio pela conta do enquadramento: as cinco pedem menos que a trava de 0,35 m, ficam a 0,35 m e ocupam de 37% a 65% da altura da tela, e a folha escreve 5 de 5. aferirLimites() dá confirmado, confirmado e inconclusivo. Quando a morsa chega, a folha é reescrita, e os números desta aba são os da folha reescrita quando a morsa chega, que o desenho não mostra.
aoMover(), que acha um ponteiro só e chama girar(10, 4) dez vezes. O azimute vai a −35,0° e a elevação a 35,8°; a distância continua 1,62 m, e a contagem vai a 11.
aoRodar() acha deltaY maior que zero e chama aproximar(FATOR_DA_RODA), que multiplica a distância por 1,12 e, apesar do nome, afasta a câmera: 1,62 × 1,12 = 1,81 m. A contagem vai a 12.
deltaY menor que zero, aproximar(1 / FATOR_DA_RODA) divide a distância por 1,12, e 1,81 m volta a 1,62 m, a distância da abertura. A contagem vai a 13.
deltaY zero, e aoRodar() sai antes do teste do sinal, sem chamar aproximar(). Nenhum número muda: a distância continua 1,62 m, e a contagem, 13.
aoDescer(), que o desenho não mostra, guarda essa distância. A cada aviso, um dos dedos se afasta dez pixels do outro, até 200, e aoMover() acha dois ponteiros e chama aproximar() dez vezes, cada vez pela razão daquele trecho. O produto das razões é 100/200: a distância vai de 1,62 m a 0,81 m, e a contagem, a 23.
enquadrar() pede a conta a planejarEnquadramento(), e a distância pedida para a engrenagem pequena é 0,18 m, e aqui a trava decide: a distância concedida é 0,35 m, mais longe do que a conta pediu, e a peça ocupa 37% da altura da tela. olharPara() leva o alvo a (0,47; 0,93; −0,05) e a distância a 0,35 m, sem mudar o azimute nem a elevação. A contagem vai a 24, e a folha é reescrita.
custoContraOTeto() lê o medidor, com 120 quadros medidos, todos os que ele guarda, e escreve o custo e a folga; na máquina que rodou a página, o pior intervalo não passou de 25,05 ms, e a linha da imagem perdida não saiu. A contagem continua 25: medir não move a câmera.
A folha do regime depois do último clique, como a página do módulo a escreve. A linha dos quadros desenhados e as duas linhas de custo vêm com … no lugar dos tempos medidos e da contagem de quadros desenhados, porque medem a máquina em que a página rodou, e não o código; pela mesma razão, em outra máquina a folga pode sair como “acima do teto”, e a linha da imagem perdida pode aparecer. Abaixo desta folha, a página do módulo mantém a do módulo sobre ativos e orçamento de cena.
A câmera em órbita
Alvo da órbita: 0.00, 0.57, -1.24 m
Azimute -35.0 graus, elevação 35.8 graus, distância 5.07 m
Posição da câmera escrita 25 vezes desde que a página abriu
Travas: elevação de 0 a 85 graus, distância de 0.35 a 8 m
Quadros desenhados desde que a página abriu: …
O que a janela entrega, e o que ela não oferece
Regime: Realidade virtual não imersiva. É o caso base e o destino de quem não tem headset: a bancada inteira precisa ser montável aqui.
O que ele rastreia, como foi declarado: nada do corpo; a câmera obedece ao mouse
A cena inteira cabe na tela a 5.07 m do centro dela (esfera de 1.87 m de raio; a órbita vai até 8 m)
Peças do domínio cobertas pela órbita: 5 de 5
Corpo: a 0.35 m (a conta pedia 0.24 m), ocupa 51% da altura da tela
Eixo: a 0.35 m (a conta pedia 0.30 m), ocupa 65% da altura da tela
Engrenagem grande: a 0.35 m (a conta pedia 0.30 m), ocupa 64% da altura da tela
Engrenagem pequena: a 0.35 m (a conta pedia 0.18 m), ocupa 37% da altura da tela
Tampa: a 0.35 m (a conta pedia 0.25 m), ocupa 52% da altura da tela
O que este regime não oferece:
A profundidade é inferida da imagem, e não vista com os dois olhos. [confirmado]
Por quê: a tela mostra a mesma imagem aos dois olhos; a distância se infere da perspectiva, de qual peça tampa qual e do tamanho relativo, e não da diferença entre duas vistas.
Impede demonstrar: julgar, sem girar a câmera, qual de duas peças próximas está mais perto, que é o julgamento que encaixar uma peça na outra pede.
Aferição: o renderizador desenha uma imagem por quadro, a da câmera da página.
Não há escala corporal. [confirmado]
Por quê: o tampo está a 90 cm do chão da cena, mas o ambiente não sabe onde fica o chão da sala nem a altura dos olhos de quem olha; a bancada aparece do tamanho que a janela tiver.
Impede demonstrar: que a bancada tem a altura certa para trabalhar de pé diante dela.
Aferição: o regime foi declarado com o espaço de referência 'viewer', que acompanha o olhar e não tem chão, e nenhum visor está apresentando.
Não há alcance de braço. [inconclusivo]
Por quê: o cursor chega a qualquer ponto da tela com o mesmo gesto, esteja a peça a 0.35 m ou a 8 m da câmera.
Impede demonstrar: que uma peça está longe demais para ser apanhada sem dar um passo até ela.
Aferição: nada no programa mede o braço de quem usa: o cursor não tem corpo.
Contraste, como foi declarado para o regime imersivo: É onde escala corporal e alcance de braço passam a existir — e nenhum dos dois tem equivalente na janela do desktop.
Trabalho do código por quadro: … ms em média, em 120 quadros, dentro do teto de 16.7 ms, com … ms de folga
Intervalo entre imagens: … ms em média, o pior … ms1.6 Por que o código é assim
As decisões que o próprio código registra, cada uma no identificador em que mora.
TRAVAS_DA_ORBITA
Cada trava existe por um defeito que ela evita. A câmera não desce abaixo da altura do alvo, porque dali para baixo o tampo fica entre ela e as peças. A trava de cima da elevação, 85°, fica cinco graus antes de a câmera ficar exatamente sobre o alvo: ali, olhar para o alvo deixa de dizer qual lado da imagem fica em cima, e passar dali vira a imagem de cabeça para baixo e inverte o sentido do arrasto. O alvo inicial fica dentro do corpo, a peça do meio do tampo, e sem a distância mínima a câmera entraria nessa peça; sem a máxima, a roda a levaria além dos 50 m em que o desenho é cortado, e a cena sumiria. Os dois valores de distância são escolha do projeto, e não saem de conta nenhuma.
montarOrbita()
A órbita começa onde a câmera já está: os três números saem da posição que o palco deu à câmera, e não de valores escritos no arquivo da órbita, que fariam a câmera saltar ao abrir. Sobra uma ligação: se ALVO_INICIAL e o ponto para onde o palco aponta a câmera divergirem, a primeira escrita reorienta a câmera, e a vista muda sozinha ao abrir. E montarOrbita() põe touchAction = 'none' na tela de desenho: sem isso, o navegador trata o arrasto de um dedo como rolagem da página e os dois dedos como aumento da página inteira, e cancela os ponteiros da órbita.
aplicar()
A câmera é escrita quando alguém mexe, e não a cada quadro: parada, a órbita produziria em todos os quadros o mesmo valor de antes. O ganho é de diagnóstico, e não de velocidade: com a escrita só no gesto e no botão, a contagem diz quantas vezes alguém moveu a câmera, e uma contagem que cresce com a página parada denuncia código que escreve a câmera sem ninguém pedir.
girar()
Arrastar para a direita leva a câmera para a esquerda em volta do alvo, e a cena parece girar para a direita, junto com a mão: por isso o azimute diminui quando a mão vai para a direita.
aoDescer() e aoMover()
A referência da pinça é medida no instante em que o segundo dedo pousa. Medida só no primeiro movimento, o primeiro trecho do gesto se perderia, e a mesma pinça pararia em 1,62 × 110/200 = 0,89 m, e não em 0,81. A distância da câmera ao alvo muda pela razão entre as distâncias dos dedos, e não pela diferença em pixels, para o gesto render igual perto e longe do alvo: a pinça que levou 1,62 m a 0,81 m levaria 8 m a 4 m, metade nos dois casos. E setPointerCapture() faz os movimentos de um ponteiro continuarem chegando à tela de desenho mesmo com o cursor fora dela no meio do arrasto.
aoRodar()
A roda para baixo multiplica a distância pelo fator, e a roda para cima a divide pelo mesmo fator, e um passo para cada lado devolve a câmera à mesma distância: 1,62, 1,81 e 1,62 m nesta execução. O teste do zero vem antes do teste do sinal porque a rolagem de lado chega com deltaY zero: sem ele, ela cairia no caso da roda para cima e aproximaria a câmera. preventDefault() impede a página de rolar enquanto a câmera se move.
caixaDoQueSeVe()
A caixa pronta da biblioteca soma também o que está escondido, e cada peça desta cena carrega, escondido, o desenho de uma forma simples, maior que ela, contra a qual se testa proximidade e toque: por isso só os nós visíveis entram. O lote é testado antes da malha porque um lote também é um nó de malha, e na ordem contrária a prateleira entraria pela caixa de uma cópia só. As posições no mundo (onde cada peça fica na cena inteira, conta que a biblioteca guarda e refaz a cada quadro desenhado) se atualizam antes de medir: antes do primeiro quadro, essa conta ainda não foi feita, e uma peça presa ao tampo sairia medida sem a altura dele, 0,90 m abaixo.
planejarEnquadramento() e enquadrar()
Caixa vazia não tem centro, e a biblioteca devolveria para ela uma esfera de raio −1; sem a saída em enquadrar(), a câmera iria para perto da origem do mundo, no chão embaixo da mesa. A esfera, e não a caixa, porque a esfera ocupa a mesma altura na tela vista de qualquer direção, e a órbita, que mantém o ângulo de quem olha, não precisa refazer a conta a cada ângulo. A conta mede só a altura da tela, e vale porque a tela de desenho tem proporção 16 por 9: numa tela mais alta do que larga, a esfera sairia pelos lados com a fração ainda abaixo de 1.
medirEntrega()
A conferência da entrega usa a mesma conta que a órbita usa para se mover, porque uma conferência com conta própria poderia aprovar uma peça que a câmera não consegue mostrar. A lista de entrada é a das peças do domínio, e não a dos nós da cena: a peça que falta na cena não aparece num percurso pela cena, e a falta passaria sem aviso.
custoContraOTeto()
A folga sai do custo, e não do intervalo: o intervalo acompanha o ritmo da tela e não desce abaixo dele, por mais leve que a cena seja, e serve para mostrar imagem perdida, e não para medir quanto ainda cabe. FATOR_DE_IMAGEM_PERDIDA é 1,5 porque o intervalo anda em múltiplos do ritmo da tela: a 60 imagens por segundo, perder uma leva o intervalo de 16,7 ms a 33,3 ms, e o valor do meio separa a perda do tremor de décimos de milissegundo que a medição tem. Com imagem perdida e o custo médio dentro do teto, a linha que custoContraOTeto() escreve aponta dois lugares onde procurar, porque o medidor guarda o pior intervalo, e não o pior custo: um quadro em que o código passou do teto, ou o que fica fora do código medido, como a placa de vídeo ou o compositor do sistema.
LIMITES_DA_JANELA
Os limites ficam escritos como dado, e não como parágrafo num documento à parte: cada um traz a razão técnica e o que a falta impede de demonstrar, e recebe uma aferição feita no programa rodando. Nenhum é defeito de implementação: os três vêm de o ambiente aparecer numa superfície plana, a uma distância que não é a do corpo de quem olha. Na frase do alcance de braço, 0,35 m e 8 m saem das travas da órbita, e mudam junto com elas.
aferirLimites()
A aferição tem três resultados possíveis, e um deles admite que o programa não tem como medir: nada no programa mede o braço de quem usa, e a função diz isso em vez de inventar um indicador. O switch não tem caso padrão, e por isso um quarto limite acrescentado ao tipo deixa a função com um caminho sem resultado, que a compilação acusa.
montarRegimeEmJanela()
Os limites ficam na mesma página do ambiente, e não num documento separado: é assim que chegam a quem abre o endereço sem ninguém por perto para explicar.
INSTRUCAO_DA_ORBITA
O que a página diz a quem chega fica escrito ao lado do código que atende cada gesto, e quem muda um gesto vê logo ao lado a frase que o descreve. A página do módulo escreve essa instrução antes da cena, para quem chega ler o que fazer antes de tentar, e não depois de desistir.
pedidos
Cada botão de enquadramento pede à órbita o enquadramento de um nó da cena, e a distância sai da caixa desse nó, e não de um número escrito na página do módulo. A câmera só se move pela órbita: um botão que escrevesse a posição dela direto teria o trabalho desfeito no primeiro arrasto.
1.7 Como o código se confere
O módulo não tem bateria de testes: quem confere o código é a própria folha, reescrita a cada botão e a cada clique em “Medir agora”, e, num caso só, o compilador.
| O que confere | O que traz | Quando algo falha | Nesta execução |
|---|---|---|---|
| a contagem de escritas | quantas vezes a órbita escreveu a câmera, ao lado dos quadros desenhados | uma contagem que cresce com a página parada denuncia código que escreve a câmera sem pedido | 1 na abertura; 25 no fim, depois de 24 escritas pedidas: dez do arrasto, duas da roda, dez dos dois dedos e duas dos botões |
| a conferência da entrega | cada peça do domínio pela mesma conta do enquadramento, e a cobertura do domínio | peça que não está na cena: a linha “não está na cena” e a contagem em 4 de 5; peça cuja fração da altura passa de 1: fica fora da contagem | 5 de 5, de 37% a 65% da altura da tela |
| a linha do custo | o custo médio contra o teto, e a folga | sem quadro medido: “Custo do quadro: ainda sem quadros medidos.”; pior intervalo acima de 25,05 ms: a linha de imagem perdida, com a frase que depende da folga | 120 quadros medidos e, na máquina que rodou a página, o custo médio dentro do teto e nenhuma linha de imagem perdida |
| a aferição | os três limites, cada um com o resultado e a razão | um quarto limite sem caso no switch não compila; com um visor apresentando, os limites da profundidade e da escala corporal saem contrariados, e isso descreve o regime do visor, e não uma falha |
confirmado, confirmado e inconclusivo |
Exemplo deste infográfico: a página do módulo aberta num navegador que roda sem mostrar a tela, um arrasto de 100 por 40 pixels em dez movimentos, um passo da roda para baixo e um para cima, uma rolagem de lado, dois dedos que se afastam de 100 para 200 pixels, os botões “Engrenagem pequena” e “A cena inteira” e um último clique em “Medir agora”, com um clique em “Medir agora” depois de cada gesto para a folha mostrá-lo; execução de 03/11/2026.