1 Ativos, formatos de troca e orçamento de cena — Infográfico do código

O código deste módulo traz para a cena uma peça feita fora do projeto, desenha centenas de cópias repetidas num pedido só à placa de vídeo e conta o que a cena custa. Este infográfico acompanha uma execução só: da abertura da página do módulo, a que esse código monta no navegador, com a morsa chegando, até um clique em Medir agora.

1.1 O caminho

O arquivo da morsa entra à esquerda e vira a primeira das quatro partes da folha (o texto que o programa escreve na tela, abaixo do desenho); as outras três se juntam a essa primeira em montarAtivosECusto(), e a folha sai à direita com os números desta execução.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart LR
  A["o arquivo da morsa<br/>18 × 12 × 17 unidades"] -->|"url e o que quem<br/>importa declarou"| B["'importarAtivo()'<br/>gira, escala, assenta<br/>'linhasDoAtivo()'<br/>escreve o laudo"]
  B --> F["'montarAtivosECusto()'<br/>monta a folha"]
  F --> K["a folha na tela<br/>fator 0,0100<br/>três divergências<br/>206 chamadas contra 1<br/>224 e 110 triângulos<br/>13.484 no censo<br/>8.108 na leitura"]
  D["'disporNaBorda()'<br/>'custoDaRepeticao()'<br/>'linhasDaRepeticao()'"] --> F
  G["'simplificar()'<br/>'montarPrateleira()'<br/>'linhas()'"] --> F
  I["clique em Medir agora<br/>'censoDaCena()', 'ler()'<br/>'linhasDaMedicao()'"] --> F

Figura 1: O arquivo da morsa é o ativo: a peça pronta, feita fora do projeto, que a cena carrega. A folha abre com o laudo, o registro do que chegou, do que ficou e do que foi corrigido. Uma chamada de desenho é o pedido que o programa faz à placa de vídeo para desenhar um objeto. O censo soma o que existe na cena, à vista ou não, e a leitura conta o que a placa desenhou no quadro que acabou de sair; quadro é cada imagem que o aparelho mostra.

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
ativos.ts CONVENCAO_DA_CENA, os tipos AjusteDeImportacao, DimensoesM, DiagnosticoDeImportacao e AtivoImportado, e as funções importarAtivo(), apontarDivergencias() e linhasDoAtivo() contarTriangulos(), do módulo de modelagem; o carregador GLTFLoader, da biblioteca
repeticao.ts ESPACAMENTO_DOS_PARAFUSOS (0,02 m), DISTANCIA_DE_TROCA (2,2 m), os tipos CustoDaRepeticao, NivelSimplificado e Prateleira, e as funções disporNaBorda(), montarLoteInstanciado(), custoDaRepeticao(), simplificar(), montarPrateleira() e linhasDaRepeticao() contarTriangulos() e contarVertices(), do módulo de modelagem; mergeVertices() e SimplifyModifier, da biblioteca
medicao.ts MAQUINA_DE_REFERENCIA, o tipo Censo e as funções censoDaCena(), identificarPlaca() e linhasDaMedicao() contarTriangulos(), do módulo de modelagem; o tipo LeituraDoOrcamento, do módulo do grafo de cena
custo.ts montarAtivosECusto() paragrafo() e subtitulo(), do módulo de dispositivos
morsa.gltf o ativo: quatro malhas, cada uma a forma de uma parte guardada em listas de números, os materiais FerroFundido e AcoUsinado (material é a descrição da superfície que a biblioteca desenha: cor, brilho metálico e aspereza), a animação AbrirFechar e, no campo de texto livre asset.generator, a frase “unidade: centimetro; eixo vertical: Z”, que nenhum programa é obrigado a ler —
05_demo.ts a página do módulo: as constantes TETO_DO_QUADRO_MS (16,7 ms, o mesmo valor de TETO_DESKTOP_MS), MAIOR_LADO_DA_MORSA_M (0,18), POSICAO_DA_MORSA, PECAS_DE_REPOSICAO (24), REMOCAO_NO_NIVEL_DISTANTE (0,5), ALVO_DA_BANCADA, ALVO_DA_PRATELEIRA e DISTANCIA_DA_PRATELEIRA_M (1,2 m), os dois botões e a ordem em que as funções rodam montarPalco(), montarCena(), montarLaco(), Orcamento e TETO_DESKTOP_MS, do módulo do grafo de cena; vestirCena(), extrudarPerfil() e perfilRegular(), do módulo de modelagem

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 importação

A convenção da cena é o conjunto de escolhas que todo objeto da cena obedece, escrito uma vez e num lugar só:

ativos.ts
export const CONVENCAO_DA_CENA = {
  unidade: 'metro',
  eixoVertical: 'Y',
  origemDaPeca: 'no ponto em que ela toca a superfície de apoio',
  frente: '+Z',
} as const;

O ajuste de importação tem duas metades. O tipo AjusteDeImportacao guarda só a declarada, o que quem importa escreve porque o arquivo não responde; a outra metade, quanto escalar e quanto deslocar para assentar, o programa mede:

ativos.ts
/**
 * O que quem importa precisa declarar, porque o arquivo não responde.
 *
 * `maiorLadoRealM` é a medida da peça no mundo, em metros, informada por quem
 * a conhece. O fator de escala sai dela por divisão contra o maior lado medido
 * no arquivo, e por isso não é digitado: um 0.01 escrito à mão serve ao arquivo
 * em centímetro e erra, sem aviso, no primeiro ativo em milímetro.
 */
export interface AjusteDeImportacao {
  readonly maiorLadoRealM: number;

O laudo é este tipo:

ativos.ts
export interface DiagnosticoDeImportacao {
  readonly arquivo: string;
  readonly comoChegou: DimensoesM;
  readonly comoFicou: DimensoesM;
  readonly fatorDeEscala: number;
  readonly giroEmXGraus: number;
  readonly deslocamentoCorrigidoM: Vector3;
  readonly divergencias: readonly string[];
}
Tipo e campo O que guarda Nesta execução
AjusteDeImportacao.maiorLadoRealM a medida da peça no mundo, em metros, escrita por quem a conhece 0,18
AjusteDeImportacao.giroEmXGraus o giro em torno de X que traz o vertical do autor ao vertical da cena −90
AjusteDeImportacao.assentarNaBase se a base vai ao zero do objeto em que a peça fica pendurada true
AjusteDeImportacao.centrarNoPlano se o centro vai ao zero em X e em Z true
DiagnosticoDeImportacao.arquivo de onde a peça veio '/ativos/morsa.gltf'
DiagnosticoDeImportacao.comoChegou em unidades do arquivo, a caixa envolvente do que chegou: a menor caixa, com as arestas paralelas aos três eixos, que contém todos os vértices (os cantos dos triângulos) 18 × 12 × 17
DiagnosticoDeImportacao.comoFicou a mesma caixa depois de girar, escalar e assentar, em metros 0,18 × 0,17 × 0,12
DiagnosticoDeImportacao.fatorDeEscala o fator de escala, o número que multiplica todas as medidas da peça 0,0100
DiagnosticoDeImportacao.giroEmXGraus o giro aplicado −90
DiagnosticoDeImportacao.deslocamentoCorrigidoM quanto a peça andou para a base chegar ao zero 0 em X, −0,30 m em Y, 0 em Z
DiagnosticoDeImportacao.divergencias uma frase por divergência, a diferença entre o que a peça tem e o que a cena esperaria três frases: unidade, eixo vertical e origem
AtivoImportado.malhas, triangulos, materiais o que o arquivo trouxe quatro malhas, 48 triângulos, FerroFundido e AcoUsinado
AtivoImportado.animacoes as animações que vieram no arquivo AbrirFechar, de 3,0 s
AtivoImportado.avancar, tocar avançar as animações pelo intervalo entre um quadro e o seguinte, e ligar uma pelo nome avancar roda a cada quadro; a página do módulo não chama tocar

1.3.2 A repetição

Um lote é o conjunto de cópias de uma peça desenhado numa chamada só, e cada cópia do lote é uma instância; instanciar é desenhar as cópias assim, num lote. O nível de detalhe é, para a peça que está longe, uma versão com menos triângulos.

Tipo e campo O que guarda Nesta execução
CustoDaRepeticao.objetos quantos parafusos há na borda 206
CustoDaRepeticao.chamadasSemInstanciar, chamadasInstanciando as chamadas de desenho sem instanciar e instanciando 206 e 1
CustoDaRepeticao.triangulosPorObjeto, triangulosTotais os triângulos de um parafuso e da borda inteira, que a instanciação não muda 24 e 4.944
NivelSimplificado.triangulos, vertices a malha do nível de longe 110 triângulos e 57 vértices
NivelSimplificado.simplificou, observacao se a ferramenta de simplificação da biblioteca, SimplifyModifier, entregou malha utilizável, e a frase que diz o que aconteceu true, “simplificada pela ferramenta, com as normais recalculadas”
Prateleira.quantidade as peças de reposição 24
Prateleira.triangulosPerto, triangulosLonge os dois níveis somados nas 24 peças 5.376 e 2.640
Prateleira.atualizar, linhas escolher o nível pela câmera, e escrever as linhas da folha atualizar roda a cada quadro

1.3.3 A medição

A máquina de referência é o aparelho contra o qual a cena é orçada, e a definição pede que seja a mais modesta das previstas; o código declara esta, e ninguém mediu ainda se ela é a mais modesta:

medicao.ts
export const MAQUINA_DE_REFERENCIA = {
  descricao: 'desktop x64 do laboratório, com vídeo integrado e sem placa dedicada',
  navegador: 'Chrome com WebGL 2 habilitado',
  observacao:
    'é o limite duro do laboratório: nenhum número deste ambiente pode depender de placa dedicada',
} as const;

Uma malha solta é a malha que não está em lote nenhum; o censo conta malhas soltas e lotes em campos separados:

medicao.ts
export interface Censo {
  readonly malhas: number;
  readonly lotesInstanciados: number;
  readonly instancias: number;
  readonly triangulos: number;
  readonly materiaisDistintos: number;
  /**
   * Chamadas de desenho previstas, no mínimo: uma por malha e uma por lote, e
   * não uma por instância.
   */
  readonly chamadasPrevistas: number;
}
Campo de Censo O que guarda Nesta execução
malhas as malhas soltas 12
lotesInstanciados os lotes 3
instancias as cópias dentro dos lotes 254
triangulos o total, com cada lote contado como triângulos por cópia vezes o número de cópias 13.484
materiaisDistintos os materiais diferentes 6
chamadasPrevistas no mínimo, uma chamada por malha solta e uma por lote, e nenhuma por cópia 15

1.3.4 A árvore da cena

Cada objeto da cena é um nó pendurado num pai, e o conjunto dos nós é a árvore; é nela que o programa guarda o que esta execução montou.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart LR
  S["'sala'"] --> BA["'bancada'"]
  S --> PR["'prateleira-de-reposicao'<br/>ao fundo da sala<br/>y 0,4 m, z −2,8 m"]
  BA --> T["'tampo'<br/>malha solta, 12 triângulos"]
  BA --> CA["'cavalete'<br/>malha solta, 12 triângulos"]
  T --> AN["o suporte e as cinco peças<br/>6 malhas soltas, 452 triângulos"]
  T --> PB["'parafusos-da-borda'<br/>um lote, 206 cópias<br/>4.944 triângulos"]
  T --> MO["'morsa.gltf'<br/>4 malhas soltas, 48 triângulos<br/>base no lado de cima do tampo"]
  PR --> PP["'reposicao-perto'<br/>lote de 24, 5.376 triângulos<br/>desligado"]
  PR --> PL["'reposicao-longe'<br/>lote de 24, 2.640 triângulos<br/>ligado"]

Figura 2: O módulo acrescenta três ramos: o lote da borda e a morsa, pendurados no tampo, e a prateleira, pendurada na sala. Com a câmera onde a página do módulo a põe, o lote de perto está desligado e o de longe, ligado.

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

Recebe Decide Devolve
'/ativos/morsa.gltf' e o que quem importa declarou, 0,18 m e −90° a ordem fixa: girar, escalar e só então medir o deslocamento que falta; caixa de maior lado zero não tem divisão, e o fator fica 1 um AtivoImportado com fator 0,0100 e a peça medindo 18,0 × 17,0 × 12,0 cm
ativos.ts
// 1. Orientação, declarada: o formato fixa o vertical em Y, e nenhum campo que
//    o importador seja obrigado a ler diz que o autor usou outro eixo.
no.rotation.x = MathUtils.degToRad(ajuste.giroEmXGraus);
no.updateMatrixWorld(true);

// 2. Escala, medida: o maior lado real dividido pelo maior lado do que chegou,
//    medido depois do giro. Caixa de lado zero não tem divisão: o fator fica 1.
const medidoAposGirar: number = maiorLado(dimensoes(new Box3().setFromObject(no)));
const fator: number = medidoAposGirar === 0 ? 1 : ajuste.maiorLadoRealM / medidoAposGirar;
no.scale.setScalar(fator);
no.updateMatrixWorld(true);

// 3. Origem, medida sobre a caixa já girada e escalada: cada correção pedida
//    leva ao zero do pai a base (em Y) ou o centro (em X e Z).
const caixaAjustada: Box3 = new Box3().setFromObject(no);
const centro: Vector3 = new Vector3();
caixaAjustada.getCenter(centro);
const correcao: Vector3 = new Vector3(
  ajuste.centrarNoPlano ? -centro.x : 0,
  ajuste.assentarNaBase ? -caixaAjustada.min.y : 0,
  ajuste.centrarNoPlano ? -centro.z : 0,
);
no.position.add(correcao);
no.updateMatrixWorld(true);

%%{init: {"flowchart": {"useMaxWidth": false, "nodeSpacing": 24, "rankSpacing": 28}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart LR
  subgraph girar["(1) e (2) gira e escala"]
    direction TB
    ini["05_demo.ts chama<br/>importarAtivo('/ativos/morsa.gltf',<br/>ajuste)"] --> carga["new<br/>GLTFLoader().loadAsync(url)"]
    carga --> chegou["dimensoes(new<br/>Box3().setFromObject(no))<br/>devolve comoChegou:<br/>18.00 x 12.00 x 17.00"]
    chegou --> giro["(1) orientação<br/>DECLARADA:<br/>degToRad(ajuste.giroEmXGraus),<br/>-90 graus em X"]
    giro --> remede["(2) escala MEDIDA depois<br/>do giro: maiorLado(...)<br/>devolve 18.00"]
    remede --> zero{"medidoAposGirar === 0"}
    zero -->|"true"| um["fator = 1: sem medida<br/>não há divisão possível"]
    zero -->|"false"| div["fator = 0.18 / 18.00 =<br/>0.0100"]
    um --> aplica["no.scale.setScalar(fator)"]
    div --> aplica
  end
  subgraph origem["(3) origem medida"]
    direction TB
    res["caixaAjustada.getCenter(centro),<br/>sobre a caixa já girada<br/>e escalada"] --> base{"ajuste.assentarNaBase"}
    base -->|"true"| sobe["y da correção =<br/>-caixaAjustada.min.y"]
    base -->|"false"| nada["y da correção = 0"]
    sobe --> plano{"ajuste.centrarNoPlano"}
    nada --> plano
    plano -->|"true"| centra["x e z da correção =<br/>-centro.x e -centro.z"]
    plano -->|"false"| fica["x e z da correção = 0"]
    centra --> soma["no.position.add(correcao):<br/>0.30 m"]
    fica --> soma
    soma --> ficou["comoFicou: 18.0 x 17.0 x<br/>12.0 cm"]
  end
  subgraph conta["conta e entrega"]
    direction TB
    censo["no.traverse conta o que<br/>chegou"] --> emalha{"filho instanceof Mesh"}
    emalha -->|"sim"| soma2["contarTriangulos(filho.geometry)<br/>e<br/>materiais.add(item.name)"]
    emalha -->|"não"| segue["nada a somar neste nó"]
    soma2 --> censo
    segue --> censo
    censo -->|"árvore percorrida"| total["4 malhas, 48 triângulos,<br/>2 materiais:<br/>FerroFundido e<br/>AcoUsinado"]
    total --> mistura["new AnimationMixer(no),<br/>que avancar(delta) usará<br/>no laço"]
    mistura --> saida["AtivoImportado com no,<br/>diagnostico, animacoes e<br/>avancar()"]
  end
  girar --> origem --> conta

Figura 3: Cada passo mede de novo a caixa envolvente que o passo anterior deixou: depois do giro, o maior lado é 18; depois da escala, a base fica 0,30 m acima do zero.

1.4.2 apontarDivergencias()

Recebe Decide Devolve
a caixa como chegou, o fator, o que quem importa declarou e a correção três testes independentes (fator fora de 0,5 a 2, giro diferente de zero, correção maior que 1 cm) e um quarto, o da lista vazia, que só escreve quando nenhum dos três escreveu três frases: 100 unidades por metro, giro de −90 graus e deslocamento de 0,30 m
ativos.ts
const divergencias: string[] = [];

if (fator < 0.5 || fator > 2) {
  const unidadesPorMetro: number = 1 / fator;
  divergencias.push(
    `Unidade divergente: o arquivo mede ${maiorLado(comoChegou).toFixed(2)} para uma peça de ` +
      `${ajuste.maiorLadoRealM.toFixed(2)} m, o que dá ${unidadesPorMetro.toFixed(0)} unidades ` +
      `por metro. O formato de troca fixa metro, e o exportador não obedeceu.`,
  );
}

if (ajuste.giroEmXGraus !== 0) {
  divergencias.push(
    `Eixo vertical divergente: o ativo veio com o vertical em outro eixo, e foi girado ` +
      `${ajuste.giroEmXGraus} graus em X para o vertical da cena ser ` +
      `${CONVENCAO_DA_CENA.eixoVertical}.`,
  );
}

if (correcao.length() > 0.01) {
  divergencias.push(
    `Origem divergente: a peça vinha deslocada ${correcao.length().toFixed(2)} m da própria ` +
      `base, e a correção a assentou na superfície de apoio.`,
  );
}

if (divergencias.length === 0) {
  divergencias.push('Nenhuma divergência acima da tolerância: o ativo chegou na convenção.');
}

return divergencias;

%%{init: {"flowchart": {"useMaxWidth": false, "nodeSpacing": 24, "rankSpacing": 28}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart LR
  subgraph unidade["unidade"]
    direction TB
    ini["importarAtivo() chama<br/>apontarDivergencias(comoChegou,<br/>fator, ajuste, correcao)"] --> uni{"fator < 0.5 || fator > 2"}
    uni -->|"0.0100, fora da faixa"| conta["unidadesPorMetro = 1 /<br/>fator = 100"]
    uni -->|"dentro da faixa"| pulaUni["nada a acusar sobre<br/>unidade"]
    conta --> msgUni["'Unidade divergente: o<br/>arquivo mede 18.00 para<br/>uma peça de 0.18 m, o<br/>que dá 100 unidades por<br/>metro'"]
  end
  subgraph eixoEOrigem["eixo e origem"]
    direction TB
    eixo{"ajuste.giroEmXGraus !==<br/>0"} -->|"-90"| msgEixo["'Eixo vertical<br/>divergente: girado -90<br/>graus em X para o<br/>vertical da cena ser Y',<br/>lido de<br/>CONVENCAO_DA_CENA.eixoVertical"]
    eixo -->|"0"| pulaEixo["o autor já usava o<br/>vertical da cena"]
    msgEixo --> org{"correcao.length() > 0.01"}
    pulaEixo --> org
    org -->|"0.30 m"| msgOrg["'Origem divergente: a<br/>peça vinha deslocada<br/>0.30 m da própria base'"]
    org -->|"abaixo da tolerância"| pulaOrg["ruído de exportação, que<br/>a lista não reclama"]
  end
  subgraph folha["a lista vazia e o laudo"]
    direction TB
    vazia{"divergencias.length ===<br/>0"} -->|"true"| ok["'Nenhuma divergência<br/>acima da tolerância: o<br/>ativo chegou na<br/>convenção'"]
    vazia -->|"false, três itens"| lista["as três frases, uma por<br/>divergência"]
    ok --> laudo["linhasDoAtivo(ativo)<br/>escreve as linhas do laudo"]
    lista --> laudo
    laudo --> cm["cm(v) converte comoFicou<br/>para centímetro;<br/>comoChegou fica na<br/>unidade do arquivo"]
    cm --> fim["nove linhas na tela,<br/>terminadas pelas<br/>divergências"]
  end
  unidade --> eixoEOrigem --> folha

Figura 4: A tolerância é a diferença pequena que a lista aceita sem acusar. A morsa passa da tolerância nos dois testes que a têm, o da unidade e o da origem; o do eixo acusa porque o giro declarado não é zero, e o quarto, o da lista vazia, não escreve nada.

1.4.3 linhasDoAtivo()

Recebe Decide Devolve
o AtivoImportado escreve a linha “Como chegou” em unidades do arquivo e a linha “Como ficou” em centímetros; escreve “nenhuma” quando não há animação as linhas do laudo: seis fixas e as três divergências desta execução
ativos.ts
/**
 * As linhas do laudo, na ordem em que a página do módulo as escreve. A linha
 * "Como chegou" fica em unidades do arquivo, e a linha "Como ficou", em
 * centímetros: com as duas em unidades diferentes, a troca de unidade aparece
 * na própria folha.
 */
export function linhasDoAtivo(ativo: AtivoImportado): string[] {
  const d: DiagnosticoDeImportacao = ativo.diagnostico;
  const cm = (v: number): string => (v * 100).toFixed(1);
  return [
    `Arquivo: ${d.arquivo}`,
    `Como chegou: ${d.comoChegou.largura.toFixed(2)} x ${d.comoChegou.altura.toFixed(2)} x ` +
      `${d.comoChegou.profundidade.toFixed(2)} unidades do arquivo`,
    `Como ficou: ${cm(d.comoFicou.largura)} x ${cm(d.comoFicou.altura)} x ` +
      `${cm(d.comoFicou.profundidade)} cm`,
    `Fator de escala aplicado: ${d.fatorDeEscala.toFixed(4)} (medido, não digitado)`,
    `Conteúdo: ${ativo.malhas} malhas, ${ativo.triangulos} triângulos, ` +
      `${ativo.materiais.length} materiais (${ativo.materiais.join(', ')})`,
    `Animações no arquivo: ${
      ativo.animacoes.length === 0
        ? 'nenhuma'
        : ativo.animacoes.map((a) => `${a.name} (${a.duration.toFixed(1)} s)`).join(', ')
    }`,
    ...d.divergencias,
  ];
}

1.4.4 disporNaBorda()

Recebe Decide Devolve
o tampo de 1,4 × 0,8 m, o espaçamento de 0,02 m, o recuo de 0,02 m e a altura de 0,026 m quantas posições por lado, pela divisão do comprimento pelo espaçamento, e os cantos contados uma vez só 206 posições: 67 em cada lado comprido e 36 em cada lado curto
repeticao.ts
export function disporNaBorda(
  largura: number,
  profundidade: number,
  espacamento: number,
  recuo: number,
  altura: number,
): Vector3[] {
  const x: number = largura / 2 - recuo;
  const z: number = profundidade / 2 - recuo;
  const porLado: number = Math.max(2, Math.floor((x * 2) / espacamento));
  const porTopo: number = Math.max(2, Math.floor((z * 2) / espacamento));
  const posicoes: Vector3[] = [];

  for (let i: number = 0; i < porLado; i += 1) {
    const t: number = -x + (i * (x * 2)) / (porLado - 1);
    posicoes.push(new Vector3(t, altura, -z));
    posicoes.push(new Vector3(t, altura, z));
  }
  // Os cantos já entraram no laço anterior: este começa em 1 e para um antes do
  // fim, para não repetir posição.
  for (let i: number = 1; i < porTopo - 1; i += 1) {
    const t: number = -z + (i * (z * 2)) / (porTopo - 1);
    posicoes.push(new Vector3(-x, altura, t));
    posicoes.push(new Vector3(x, altura, t));
  }

  return posicoes;
}

%%{init: {"flowchart": {"useMaxWidth": false, "nodeSpacing": 24, "rankSpacing": 28}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart LR
  subgraph borda["as posições na borda"]
    direction TB
    ini["05_demo.ts chama<br/>disporNaBorda(1.4, 0.8,<br/>ESPACAMENTO_DOS_PARAFUSOS,<br/>0.02, 0.026)"] --> meia["x = largura / 2 - recuo<br/>= 0.68 e z =<br/>profundidade / 2 - recuo<br/>= 0.38"]
    meia --> conta["porLado = Math.max(2,<br/>Math.floor((x * 2) /<br/>espacamento)) = 67 e<br/>porTopo = 38"]
    conta --> lados["primeiro laço: i de 0 a<br/>porLado - 1, duas<br/>posições por volta, em<br/>-z e em z"]
    lados --> topos["segundo laço: i de 1 a<br/>porTopo - 2, sem repetir<br/>os cantos, em -x e em x"]
    topos --> pos["posicoes = 2 * 67 + 2 *<br/>36 = 206"]
  end
  subgraph lote["um lote só"]
    direction TB
    perfil["extrudarPerfil(perfilRegular(6,<br/>0.004), 0.003,<br/>'parafuso'): a cabeça<br/>sextavada"] --> montar["montarLoteInstanciado(geometriaDoParafuso,<br/>acoEscovado.material,<br/>posicoes,<br/>'parafusos-da-borda')"]
    montar --> novo["new<br/>InstancedMesh(geometria,<br/>material,<br/>posicoes.length)"]
    novo --> volta{"i < posicoes.length"}
    volta -->|"true"| compoe["matriz.compose(posicao,<br/>semGiro, semEscala) e<br/>lote.setMatrixAt(i,<br/>matriz)"]
    compoe --> volta
    volta -->|"false, 206 matrizes<br/>escritas"| envia["lote.instanceMatrix.needsUpdate<br/>= true, uma vez só"]
  end
  subgraph custo["o custo na folha"]
    direction TB
    pendura["tampo.add(parafusos)"] --> calcula["custoDaRepeticao(geometriaDoParafuso,<br/>206)"]
    calcula --> num["contarTriangulos(geometria)<br/>= 24:<br/>chamadasSemInstanciar<br/>206,<br/>chamadasInstanciando 1,<br/>triangulosTotais 4944"]
    num --> linhas["linhasDaRepeticao(custoDosParafusos):<br/>quatro linhas na folha"]
  end
  borda --> lote --> custo

Figura 5: No lado comprido, o comprimento recuado, que no papel é 1,36, sai do computador como 1,3599999999999999; dividido por 0,02, dá 67,99999999999999, e o arredondamento para baixo deixa 67. No lado curto, 0,76 ÷ 0,02 dá 38, e o segundo for deixa de fora os dois cantos, que o primeiro já pôs: sobram 36. O mesmo fluxo segue por montarLoteInstanciado(), custoDaRepeticao() e linhasDaRepeticao().

1.4.5 montarLoteInstanciado()

Recebe Decide Devolve
a malha do parafuso, o material, as 206 posições e o nome 'parafusos-da-borda' pula a entrada vazia da lista de posições; escreve uma matriz por cópia, a que diz onde ela fica, e avisa a placa uma vez só, no fim um lote com 206 cópias, que a página do módulo pendura no tampo
repeticao.ts
/**
 * Monta um lote instanciado a partir de uma malha e de uma lista de posições.
 *
 * O lote inteiro é desenhado numa chamada só. A matriz de cada cópia é escrita
 * uma vez, na montagem, e não a cada quadro: o aviso `needsUpdate`, dado ao fim,
 * manda a tabela de matrizes para a placa, e como nada a altera depois, a
 * biblioteca não volta a enviá-la.
 */
export function montarLoteInstanciado(
  geometria: BufferGeometry,
  material: Material,
  posicoes: readonly Vector3[],
  nome: string,
): InstancedMesh {
  const lote: InstancedMesh = new InstancedMesh(geometria, material, posicoes.length);
  lote.name = nome;
  const matriz: Matrix4 = new Matrix4();
  const semGiro: Quaternion = new Quaternion();
  const semEscala: Vector3 = new Vector3(1, 1, 1);

  for (let i: number = 0; i < posicoes.length; i += 1) {
    const posicao: Vector3 | undefined = posicoes[i];
    if (posicao === undefined) {
      continue;
    }
    matriz.compose(posicao, semGiro, semEscala);
    lote.setMatrixAt(i, matriz);
  }
  lote.instanceMatrix.needsUpdate = true;
  return lote;
}

1.4.6 custoDaRepeticao() e linhasDaRepeticao()

Recebe Decide Devolve
a malha do parafuso e a quantidade, 206 sem olhar para o lote, põe lado a lado a coluna das chamadas e a dos triângulos: instanciar muda a primeira e deixa a segunda igual 206 chamadas sem instanciar, 1 instanciando, 24 triângulos por parafuso e 4.944 no total, que linhasDaRepeticao() escreve na folha
repeticao.ts
/**
 * O que a instanciação muda, em número: as chamadas de desenho passam de uma
 * por objeto a uma só, e os triângulos ficam iguais, porque a placa continua
 * desenhando cada um.
 *
 * A medida certa aqui é a chamada, e não o triângulo. O que a instanciação tira
 * da conta é a preparação fixa de cada objeto, que não depende do tamanho dele.
 */
export function custoDaRepeticao(
  geometria: BufferGeometry,
  quantidade: number,
): CustoDaRepeticao {
  const porObjeto: number = contarTriangulos(geometria);
  return {
    objetos: quantidade,
    chamadasSemInstanciar: quantidade,
    chamadasInstanciando: 1,
    triangulosPorObjeto: porObjeto,
    triangulosTotais: porObjeto * quantidade,
  };
}

1.4.7 simplificar()

Recebe Decide Devolve
a malha da engrenagem grande e a proporção 0,5 duas conferências: menos de um vértice a tirar deixa o original; malha vazia ou com coordenada que não é número de verdade também o nível de longe: 110 triângulos e 57 vértices, com as normais recalculadas (a normal é a direção que a luz consulta em cada vértice)
repeticao.ts
const soPosicao: BufferGeometry = geometria.clone();
soPosicao.deleteAttribute('normal');
soPosicao.deleteAttribute('uv');
const unida: BufferGeometry = mergeVertices(soPosicao);
soPosicao.dispose();
const aRemover: number = Math.floor(contarVertices(unida) * proporcaoRemovida);

if (aRemover < 1) {
  return {
    geometria,
    triangulos: contarTriangulos(geometria),
    vertices: contarVertices(geometria),
    simplificou: false,
    observacao: 'malha pequena demais para valer a simplificação; o original ficou',
  };
}

const candidata: BufferGeometry = new SimplifyModifier().modify(unida, aRemover);
const posicoes: ArrayLike<number> = candidata.getAttribute('position').array;
let naoFinitos: number = 0;
for (let i: number = 0; i < posicoes.length; i += 1) {
  if (!Number.isFinite(posicoes[i] ?? Number.NaN)) {
    naoFinitos += 1;
  }
}

if (contarTriangulos(candidata) === 0 || naoFinitos > 0) {
  candidata.dispose();
  return {
    geometria,
    triangulos: contarTriangulos(geometria),
    vertices: contarVertices(geometria),
    simplificou: false,
    observacao: 'a ferramenta não devolveu malha utilizável; o original ficou no lugar',
  };
}

%%{init: {"flowchart": {"useMaxWidth": false, "nodeSpacing": 24, "rankSpacing": 28}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart LR
  subgraph simplifica["o nível de longe"]
    direction TB
    ini["05_demo.ts chama<br/>montarPrateleira(engrenagem.geometry,<br/>latao.material,<br/>PECAS_DE_REPOSICAO,<br/>REMOCAO_NO_NIVEL_DISTANTE)"] --> simp["simplificar(geometria,<br/>0.5)"]
    simp --> tira["soPosicao =<br/>geometria.clone(), sem<br/>normal e sem uv"]
    tira --> une["mergeVertices(soPosicao):<br/>os vértices na mesma<br/>posição viram um só, 338<br/>viram 114"]
    une --> quanto["aRemover =<br/>Math.floor(contarVertices(unida)<br/>* proporcaoRemovida)"]
    quanto --> pouco{"aRemover < 1"}
    pouco -->|"false"| modifica["new<br/>SimplifyModifier().modify(unida,<br/>aRemover)"]
    modifica --> confere{"contarTriangulos(candidata)<br/>=== 0 || naoFinitos > 0"}
    confere -->|"false"| normais["candidata.computeVertexNormals():<br/>simplificou true, 110<br/>triângulos por peça"]
    pouco -->|"true"| original["o original fica: malha<br/>pequena demais"]
    confere -->|"true"| original
  end
  subgraph monta["os dois lotes"]
    direction TB
    posicoes["24 posições, porFileira<br/>6, em quatro fileiras"] --> perto["montarLoteInstanciado(geometria,<br/>..., 'reposicao-perto'):<br/>224 triângulos por peça"]
    perto --> longe["montarLoteInstanciado(nivel.geometria,<br/>..., 'reposicao-longe'),<br/>longe.visible = false"]
    longe --> grupo["no = new Group() com os<br/>dois lotes; quem o pendura<br/>na sala é 05_demo.ts:<br/>cena.sala.add(prateleira.no)"]
  end
  subgraph troca["a troca, a cada quadro"]
    direction TB
    passo["no laço:<br/>prateleira.atualizar(palco.camera)"] --> dist{"daCamera.distanceTo(centro)<br/>> DISTANCIA_DE_TROCA"}
    dist -->|"true: a câmera na<br/>posição em que o palco a<br/>pôs"| ligaLonge["perto.visible = false e<br/>longe.visible = true"]
    dist -->|"false: depois de<br/>'Aproximar da prateleira<br/>do fundo', a câmera a<br/>1.29 m do centro"| ligaPerto["perto.visible = true e<br/>longe.visible = false"]
    ligaLonge --> linhas["prateleira.linhas():<br/>5376 de perto, 2640 de<br/>longe, 51% a menos"]
    ligaPerto --> linhas
  end
  simplifica -->|"a simplificada, ou o<br/>original"| monta --> troca

Figura 6: Antes de simplificar, a função une os vértices que ocupam a mesma posição: cada lado plano da engrenagem guarda cópias próprias dos pontos dos cantos, e a união troca as cópias de cada ponto por um vértice só. Os 338 vértices viram 114, e metade deles, 57, é o que a ferramenta tira. O fluxo segue por montarPrateleira() e pelo atualizar() de cada quadro.

1.4.8 montarPrateleira()

Recebe Decide Devolve
a malha da engrenagem, o latão, 24 peças e a proporção 0,5 dois lotes com as mesmas posições, seis por fileira; o de longe começa desligado; a cada quadro, atualizar() liga o de longe quando a câmera está a mais de 2,2 m do centro uma Prateleira com 5.376 triângulos de perto e 2.640 de longe, 51% a menos; com a câmera a 4,45 m, atualizar() devolve true
repeticao.ts
const nivel: NivelSimplificado = simplificar(geometria, proporcaoRemovida);
const posicoes: Vector3[] = [];
const porFileira: number = 6;
for (let i: number = 0; i < quantidade; i += 1) {
  const coluna: number = i % porFileira;
  const fileira: number = Math.floor(i / porFileira);
  posicoes.push(new Vector3(-0.25 + coluna * 0.1, 0.02 + fileira * 0.18, 0));
}

const perto: InstancedMesh = montarLoteInstanciado(
  geometria,
  material,
  posicoes,
  'reposicao-perto',
);
const longe: InstancedMesh = montarLoteInstanciado(
  nivel.geometria,
  material,
  posicoes,
  'reposicao-longe',
);
longe.visible = false;

const no: Group = new Group();
no.name = 'prateleira-de-reposicao';
no.add(perto);
no.add(longe);
repeticao.ts
atualizar(camera: Camera): boolean {
  no.getWorldPosition(centro);
  camera.getWorldPosition(daCamera);
  const distante: boolean = daCamera.distanceTo(centro) > DISTANCIA_DE_TROCA;
  perto.visible = !distante;
  longe.visible = distante;
  return distante;
},

1.4.9 censoDaCena()

Recebe Decide Devolve
cena.sala, a raiz da árvore para cada nó, um de três casos: não é malha, é malha solta ou é lote um Censo com 13.484 triângulos e 15 chamadas previstas
medicao.ts
export function censoDaCena(raiz: Object3D): Censo {
  let malhas: number = 0;
  let lotes: number = 0;
  let instancias: number = 0;
  let triangulos: number = 0;
  const materiais: Set<string> = new Set<string>();

  raiz.traverse((no: Object3D) => {
    if (!(no instanceof Mesh)) {
      return;
    }
    const porMalha: number = contarTriangulos(no.geometry);
    if (no instanceof InstancedMesh) {
      lotes += 1;
      instancias += no.count;
      triangulos += porMalha * no.count;
    } else {
      malhas += 1;
      triangulos += porMalha;
    }
    // Material contado pelo `uuid`, que identifica cada objeto de material, e não
    // pelo nome, que dois materiais diferentes podem repetir.
    const material: Material | Material[] = no.material;
    for (const item of Array.isArray(material) ? material : [material]) {
      materiais.add(item.uuid);
    }
  });

  return {
    malhas,
    lotesInstanciados: lotes,
    instancias,
    triangulos,
    materiaisDistintos: materiais.size,
    chamadasPrevistas: malhas + lotes,
  };
}

%%{init: {"flowchart": {"useMaxWidth": false, "nodeSpacing": 24, "rankSpacing": 28}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart LR
  subgraph percorre["percorre a árvore"]
    direction TB
    ini["clique em 'Medir agora'<br/>chama<br/>censoDaCena(cena.sala)"] --> anda["raiz.traverse, um nó por<br/>vez"]
    anda --> ehMalha{"no instanceof Mesh"}
    ehMalha -->|"false"| volta["nada a somar: luz, grupo<br/>ou eixo"]
    volta --> anda
    ehMalha -->|"true"| pm["porMalha =<br/>contarTriangulos(no.geometry)"]
    pm --> lote{"no instanceof<br/>InstancedMesh"}
    lote -->|"true"| somaLote["lotes += 1, instâncias<br/>+= no.count, triângulos<br/>+= porMalha * no.count"]
    lote -->|"false"| somaSolta["malhas += 1, triângulos<br/>+= porMalha"]
    somaLote --> mat["materiais.add(item.uuid),<br/>uma vez por material do<br/>nó"]
    somaSolta --> mat
    mat --> anda
  end
  subgraph resultado["o censo"]
    direction TB
    saida["Censo: 13484 triângulos,<br/>12 malhas soltas, 3<br/>lotes, 254 instâncias, 6<br/>materiaisDistintos"] --> prev["chamadasPrevistas =<br/>malhas + lotes = 12 + 3<br/>= 15, uma por lote e não<br/>uma por instância"]
  end
  percorre -->|"árvore percorrida"| resultado

Figura 7: O teste de lote vem depois do teste de malha, porque um lote também é uma malha.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart LR
  subgraph K1["caso 1: o nó não é malha"]
    direction TB
    A1["'no instanceof Mesh' é falso"] --> A2["volta sem somar nada<br/>14 dos 29 nós desta cena<br/>luzes, grupos, contornos"]
  end
  subgraph K2["caso 2: malha solta"]
    direction TB
    B1["'no instanceof InstancedMesh' é falso"] --> B2["malhas mais 1<br/>triângulos mais os da malha<br/>12 malhas, 524 triângulos<br/>12 chamadas previstas"]
  end
  subgraph K3["caso 3: lote"]
    direction TB
    C1["'no instanceof InstancedMesh' é verdadeiro"] --> C2["lotes mais 1<br/>instâncias mais 'no.count'<br/>triângulos mais os de uma cópia vezes 'no.count'<br/>3 lotes, 254 instâncias, 12.960 triângulos<br/>3 chamadas previstas"]
  end
  K1 ~~~ K2 ~~~ K3

Figura 8: Os três casos lado a lado, com o que cada um custou nesta cena: as 12 malhas soltas somam 524 triângulos e 12 chamadas previstas, e os três lotes somam 12.960 triângulos e três chamadas previstas, uma por lote e nenhuma por instância.

1.4.10 identificarPlaca()

Recebe Decide Devolve
palco.renderer, que o módulo do grafo de cena montou três falhas possíveis (a extensão que revela o nome não existe ou vem bloqueada, a chave dela não é número, o nome vem vazio) e, em todas, a recusa escrita por extenso o nome que este navegador informou: “ANGLE (Intel, Intel(R) Graphics (0x00007D67) Direct3D11 vs_5_0 ps_5_0, D3D11)”
medicao.ts
export function identificarPlaca(renderer: WebGLRenderer): string {
  const contexto: WebGLRenderingContext | WebGL2RenderingContext = renderer.getContext();
  const extensao: unknown = contexto.getExtension('WEBGL_debug_renderer_info');
  if (extensao === null || typeof extensao !== 'object') {
    return 'placa não identificada (o navegador não expõe o nome do adaptador)';
  }
  const chave: unknown = (extensao as Record<string, unknown>)['UNMASKED_RENDERER_WEBGL'];
  if (typeof chave !== 'number') {
    return 'placa não identificada (o navegador não expõe o nome do adaptador)';
  }
  const nome: unknown = contexto.getParameter(chave);
  return typeof nome === 'string' && nome !== '' ? nome : 'placa não identificada';
}

%%{init: {"flowchart": {"useMaxWidth": false, "nodeSpacing": 24, "rankSpacing": 28}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart LR
  subgraph placa["a placa"]
    direction TB
    ini["clique em 'Medir agora'<br/>chama<br/>identificarPlaca(palco.renderer)"] --> ctx["renderer.getContext() e<br/>getExtension('WEBGL_debug_renderer_info')"]
    ctx --> temExt{"extensao === null ||<br/>typeof extensao !==<br/>'object'"}
    temExt -->|"false"| chave["chave =<br/>extensao['UNMASKED_RENDERER_WEBGL']"]
    chave --> ehNum{"typeof chave !==<br/>'number'"}
    ehNum -->|"false"| nome["contexto.getParameter(chave)"]
    nome --> ehStr{"typeof nome === 'string'<br/>&& nome !== ''"}
    ehStr -->|"true"| revela["o nome relatado por este<br/>aparelho"]
    temExt -->|"true"| naoSabe["'placa não identificada<br/>(o navegador não expõe o<br/>nome do adaptador)'"]
    ehNum -->|"true"| naoSabe
    ehStr -->|"false"| naoSabe
  end
  subgraph cabecalho["a máquina antes do número"]
    direction TB
    folha["linhasDaMedicao(censo,<br/>orcamento.ler(), placa)"] --> cab["MAQUINA_DE_REFERENCIA.descricao,<br/>.navegador e o teto de<br/>16.7 ms na mesma folha"]
    cab --> temQuadro{"leitura.quadrosMedidos<br/>=== 0"}
    temQuadro -->|"true"| adiar["'Ainda sem quadros<br/>medidos: a leitura<br/>dinâmica entra depois do<br/>primeiro quadro.'"]
  end
  subgraph leitura["a leitura"]
    direction TB
    folga["folga = leitura.tetoMs -<br/>leitura.intervaloMedioMs"] --> sinal{"folga >= 0"}
    sinal -->|"true"| ms["'… ms de folga'"]
    sinal -->|"false"| estouro["'… ms acima do teto',<br/>com o sinal invertido<br/>para não imprimir menos"]
    ms --> pior["pior intervalo da<br/>janela, que a média<br/>esconderia"]
    estouro --> pior
    pior --> dif["censo.triangulos -<br/>leitura.triangulos =<br/>13484 - 8108 = 5376, o<br/>nível de perto da<br/>prateleira, desligado"]
  end
  placa -->|"o nome ou a recusa"| cabecalho
  cabecalho -->|"false, 120 quadros"| leitura

Figura 9: O mesmo fluxo segue por linhasDaMedicao(), que escreve a máquina antes de qualquer número.

1.4.11 linhasDaMedicao()

Recebe Decide Devolve
o censo, o que orcamento.ler() devolveu e o nome da placa o cabeçalho com a máquina sai sempre; sem quadro medido, termina antes da média; a diferença entre o teto (o tempo que o programa tem para preparar cada quadro, escrito no código como número) e o intervalo médio entre um quadro e o seguinte sai em milissegundos, escrita como “de folga” ou como “acima do teto” as linhas da medição: o teto de 16,7 ms; 120 quadros medidos; 14 chamadas e 8.108 triângulos desenhados
medicao.ts
export function linhasDaMedicao(
  censo: Censo,
  leitura: LeituraDoOrcamento,
  placa: string,
): string[] {
  const linhas: string[] = [
    `Máquina de referência: ${MAQUINA_DE_REFERENCIA.descricao}`,
    `Navegador previsto: ${MAQUINA_DE_REFERENCIA.navegador}`,
    `Placa relatada por este aparelho: ${placa}`,
    `Teto declarado por quadro: ${leitura.tetoMs.toFixed(1)} ms`,
    '',
    `Censo estático da cena: ${censo.triangulos} triângulos em ${censo.malhas} malhas soltas e ` +
      `${censo.lotesInstanciados} lotes instanciados (${censo.instancias} instâncias)`,
    `Chamadas de desenho previstas pelo censo: ${censo.chamadasPrevistas}`,
    `Materiais distintos: ${censo.materiaisDistintos}`,
  ];

  if (leitura.quadrosMedidos === 0) {
    linhas.push('', 'Ainda sem quadros medidos: a leitura dinâmica entra depois do primeiro quadro.');
    return linhas;
  }

  // A folga sai em milissegundos, a unidade em que se decide o que ainda cabe no
  // quadro, e não em fração do teto.
  const folga: number = leitura.tetoMs - leitura.intervaloMedioMs;
  linhas.push(
    '',
    `Medido em ${leitura.quadrosMedidos} quadros: ${leitura.chamadasDeDesenho} ${leitura.chamadasDeDesenho === 1 ? 'chamada' : 'chamadas'} e ` +
      `${leitura.triangulos} triângulos efetivamente desenhados`,
    `Intervalo médio: ${leitura.intervaloMedioMs.toFixed(1)} ms — ` +
      `${folga >= 0 ? `${folga.toFixed(1)} ms de folga` : `${(-folga).toFixed(1)} ms acima do teto`}`,
    `Pior intervalo da janela: ${leitura.piorIntervaloMs.toFixed(1)} ms`,
    `Diferença entre censo e medição: ${censo.triangulos - leitura.triangulos} triângulos não foram ` +
      'desenhados neste quadro, por estarem fora do campo de visão ou num nível de detalhe desligado, e voltam quando a câmera se move',
  );
  return linhas;
}

1.4.12 montarAtivosECusto()

Recebe Decide Devolve
o lugar da tela em que a folha é escrita e as quatro listas de linhas a ordem das quatro partes, sempre a mesma, e a medição num bloco de texto que guarda as quebras de linha nada: reescreve a folha inteira, o que nesta execução acontece três vezes
custo.ts
/**
 * A folha de ativos e de custo: o laudo da importação, o que a instanciação
 * comprou, os dois níveis da prateleira e a medição com a máquina ao lado.
 *
 * As quatro partes ficam numa folha só para que o número de desempenho nunca
 * apareça longe da máquina em que foi obtido. A medição chega como linhas
 * unidas por quebra de linha, e por isso vai num bloco `pre`.
 */
export function montarAtivosECusto(
  raiz: HTMLElement,
  ativo: readonly string[],
  repeticao: readonly string[],
  niveis: readonly string[],
  medicao: readonly string[],
): void {
  raiz.replaceChildren();

  raiz.appendChild(subtitulo('O ativo que veio de fora, e o que foi ajustado nele'));
  for (const linha of ativo) {
    raiz.appendChild(paragrafo(linha));
  }

  raiz.appendChild(subtitulo('Repetição tratada como repetição'));
  for (const linha of repeticao) {
    raiz.appendChild(paragrafo(linha));
  }

  raiz.appendChild(subtitulo('Detalhe cobrado por distância'));
  for (const linha of niveis) {
    raiz.appendChild(paragrafo(linha));
  }

  raiz.appendChild(subtitulo('A medição, e a máquina em que ela foi obtida'));
  const bloco: HTMLPreElement = document.createElement('pre');
  bloco.textContent = medicao.join('\n');
  raiz.appendChild(bloco);
}

1.5 Uma execução, passo a passo

As abas seguem a execução desde a abertura da página do módulo, sem mover a câmera, até o clique em Medir agora. As três primeiras são da montagem, o momento em que a cena é construída, antes do primeiro quadro. Em cada aba, o que roda naquele passo tem borda grossa e fundo amarelo, o que entra na cena ou na folha tem borda mais grossa e fundo verde, e o que já rodou tem borda tracejada e fundo cinza.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph SB["montagem: a borda"]
    direction LR
    B0["'disporNaBorda()'<br/>206 posições na borda"] --> B1["'montarLoteInstanciado()'<br/>um lote com 206 cópias no tampo"] --> B2["'custoDaRepeticao()'<br/>206 chamadas contra 1"]
  end
  subgraph SP["a prateleira e a 1ª folha"]
    direction LR
    P0["'simplificar()'<br/>338 vértices, 114 unidos, 57 a tirar"] --> P1["'montarPrateleira()'<br/>dois lotes de 24 peças"] --> F0["'montarAtivosECusto()'<br/>a folha, ainda sem laudo"]
  end
  subgraph SL["o laço, a cada quadro"]
    direction LR
    L0["'atualizar()'<br/>câmera a 4,45 m: lote de longe"]
  end
  subgraph SA["a morsa chega e se ajusta"]
    direction LR
    M0["caixa como chegou<br/>18 × 12 × 17"] --> M1["giro de −90° em X<br/>caixa 18 × 17 × 12"] --> M2["fator = 0,18 ÷ 18 = 0,0100<br/>caixa 0,18 × 0,17 × 0,12 m"]
  end
  subgraph SD["assenta e ganha o laudo"]
    direction LR
    M3["base 0,30 m acima do zero<br/>correção de 0,30 m para baixo"] --> M4["'apontarDivergencias()'<br/>três divergências; morsa no tampo"]
  end
  subgraph SC["clique em Medir agora"]
    direction LR
    C0["'censoDaCena()'<br/>13.484 triângulos, 15 chamadas"] --> C2["'linhasDaMedicao()'<br/>a medição entra na folha"]
    C1["'ler()'<br/>14 chamadas, 8.108 triângulos"] --> C2
  end
  SB --> SP --> SL --> SA --> SD --> SC
  classDef ativo fill:#fff1b8,stroke:#8a5a00,stroke-width:3px,color:#111
  classDef usado fill:#ececec,stroke:#7a7a7a,stroke-width:1px,stroke-dasharray:5 3,color:#444
  classDef novo fill:#d3efd3,stroke:#1d6b1d,stroke-width:4px,color:#111
  class B0,B2 ativo
  class B1 novo

Figura 10: disporNaBorda() devolve 206 posições, montarLoteInstanciado() monta com elas um lote de 206 cópias, que a página do módulo pendura no tampo, e custoDaRepeticao() guarda 206 chamadas sem instanciar contra 1 instanciando.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph SB["montagem: a borda"]
    direction LR
    B0["'disporNaBorda()'<br/>206 posições na borda"] --> B1["'montarLoteInstanciado()'<br/>um lote com 206 cópias no tampo"] --> B2["'custoDaRepeticao()'<br/>206 chamadas contra 1"]
  end
  subgraph SP["a prateleira e a 1ª folha"]
    direction LR
    P0["'simplificar()'<br/>338 vértices, 114 unidos, 57 a tirar"] --> P1["'montarPrateleira()'<br/>dois lotes de 24 peças"] --> F0["'montarAtivosECusto()'<br/>a folha, ainda sem laudo"]
  end
  subgraph SL["o laço, a cada quadro"]
    direction LR
    L0["'atualizar()'<br/>câmera a 4,45 m: lote de longe"]
  end
  subgraph SA["a morsa chega e se ajusta"]
    direction LR
    M0["caixa como chegou<br/>18 × 12 × 17"] --> M1["giro de −90° em X<br/>caixa 18 × 17 × 12"] --> M2["fator = 0,18 ÷ 18 = 0,0100<br/>caixa 0,18 × 0,17 × 0,12 m"]
  end
  subgraph SD["assenta e ganha o laudo"]
    direction LR
    M3["base 0,30 m acima do zero<br/>correção de 0,30 m para baixo"] --> M4["'apontarDivergencias()'<br/>três divergências; morsa no tampo"]
  end
  subgraph SC["clique em Medir agora"]
    direction LR
    C0["'censoDaCena()'<br/>13.484 triângulos, 15 chamadas"] --> C2["'linhasDaMedicao()'<br/>a medição entra na folha"]
    C1["'ler()'<br/>14 chamadas, 8.108 triângulos"] --> C2
  end
  SB --> SP --> SL --> SA --> SD --> SC
  classDef ativo fill:#fff1b8,stroke:#8a5a00,stroke-width:3px,color:#111
  classDef usado fill:#ececec,stroke:#7a7a7a,stroke-width:1px,stroke-dasharray:5 3,color:#444
  classDef novo fill:#d3efd3,stroke:#1d6b1d,stroke-width:4px,color:#111
  class B0,B2,B1 usado
  class P0 ativo
  class P1 novo

Figura 11: simplificar() une os 338 vértices da engrenagem em 114 e tira 57, e cada peça passa de 224 a 110 triângulos. montarPrateleira() monta dois lotes de 24 peças, com o de longe desligado, e a página do módulo os pendura na sala.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph SB["montagem: a borda"]
    direction LR
    B0["'disporNaBorda()'<br/>206 posições na borda"] --> B1["'montarLoteInstanciado()'<br/>um lote com 206 cópias no tampo"] --> B2["'custoDaRepeticao()'<br/>206 chamadas contra 1"]
  end
  subgraph SP["a prateleira e a 1ª folha"]
    direction LR
    P0["'simplificar()'<br/>338 vértices, 114 unidos, 57 a tirar"] --> P1["'montarPrateleira()'<br/>dois lotes de 24 peças"] --> F0["'montarAtivosECusto()'<br/>a folha, ainda sem laudo"]
  end
  subgraph SL["o laço, a cada quadro"]
    direction LR
    L0["'atualizar()'<br/>câmera a 4,45 m: lote de longe"]
  end
  subgraph SA["a morsa chega e se ajusta"]
    direction LR
    M0["caixa como chegou<br/>18 × 12 × 17"] --> M1["giro de −90° em X<br/>caixa 18 × 17 × 12"] --> M2["fator = 0,18 ÷ 18 = 0,0100<br/>caixa 0,18 × 0,17 × 0,12 m"]
  end
  subgraph SD["assenta e ganha o laudo"]
    direction LR
    M3["base 0,30 m acima do zero<br/>correção de 0,30 m para baixo"] --> M4["'apontarDivergencias()'<br/>três divergências; morsa no tampo"]
  end
  subgraph SC["clique em Medir agora"]
    direction LR
    C0["'censoDaCena()'<br/>13.484 triângulos, 15 chamadas"] --> C2["'linhasDaMedicao()'<br/>a medição entra na folha"]
    C1["'ler()'<br/>14 chamadas, 8.108 triângulos"] --> C2
  end
  SB --> SP --> SL --> SA --> SD --> SC
  classDef ativo fill:#fff1b8,stroke:#8a5a00,stroke-width:3px,color:#111
  classDef usado fill:#ececec,stroke:#7a7a7a,stroke-width:1px,stroke-dasharray:5 3,color:#444
  classDef novo fill:#d3efd3,stroke:#1d6b1d,stroke-width:4px,color:#111
  class B0,B2,B1,P0,P1 usado
  class F0 novo

Figura 12: montarAtivosECusto() escreve a folha pela primeira vez: o laudo diz “O ativo externo ainda não foi trazido.”, e a medição diz que sai no clique em “Medir agora”. A página do módulo pede a morsa e segue sem esperar por ela.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph SB["montagem: a borda"]
    direction LR
    B0["'disporNaBorda()'<br/>206 posições na borda"] --> B1["'montarLoteInstanciado()'<br/>um lote com 206 cópias no tampo"] --> B2["'custoDaRepeticao()'<br/>206 chamadas contra 1"]
  end
  subgraph SP["a prateleira e a 1ª folha"]
    direction LR
    P0["'simplificar()'<br/>338 vértices, 114 unidos, 57 a tirar"] --> P1["'montarPrateleira()'<br/>dois lotes de 24 peças"] --> F0["'montarAtivosECusto()'<br/>a folha, ainda sem laudo"]
  end
  subgraph SL["o laço, a cada quadro"]
    direction LR
    L0["'atualizar()'<br/>câmera a 4,45 m: lote de longe"]
  end
  subgraph SA["a morsa chega e se ajusta"]
    direction LR
    M0["caixa como chegou<br/>18 × 12 × 17"] --> M1["giro de −90° em X<br/>caixa 18 × 17 × 12"] --> M2["fator = 0,18 ÷ 18 = 0,0100<br/>caixa 0,18 × 0,17 × 0,12 m"]
  end
  subgraph SD["assenta e ganha o laudo"]
    direction LR
    M3["base 0,30 m acima do zero<br/>correção de 0,30 m para baixo"] --> M4["'apontarDivergencias()'<br/>três divergências; morsa no tampo"]
  end
  subgraph SC["clique em Medir agora"]
    direction LR
    C0["'censoDaCena()'<br/>13.484 triângulos, 15 chamadas"] --> C2["'linhasDaMedicao()'<br/>a medição entra na folha"]
    C1["'ler()'<br/>14 chamadas, 8.108 triângulos"] --> C2
  end
  SB --> SP --> SL --> SA --> SD --> SC
  classDef ativo fill:#fff1b8,stroke:#8a5a00,stroke-width:3px,color:#111
  classDef usado fill:#ececec,stroke:#7a7a7a,stroke-width:1px,stroke-dasharray:5 3,color:#444
  classDef novo fill:#d3efd3,stroke:#1d6b1d,stroke-width:4px,color:#111
  class B0,B2,B1,P0,P1,F0 usado
  class L0 ativo

Figura 13: O laço, a parte do programa que a cada quadro atualiza o que mudou e manda desenhar de novo, começa antes de a morsa chegar. A cada quadro, atualizar() mede 4,45 m da câmera ao centro da prateleira; como 4,45 passa de 2,2, o lote de perto fica desligado e o de longe, ligado.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph SB["montagem: a borda"]
    direction LR
    B0["'disporNaBorda()'<br/>206 posições na borda"] --> B1["'montarLoteInstanciado()'<br/>um lote com 206 cópias no tampo"] --> B2["'custoDaRepeticao()'<br/>206 chamadas contra 1"]
  end
  subgraph SP["a prateleira e a 1ª folha"]
    direction LR
    P0["'simplificar()'<br/>338 vértices, 114 unidos, 57 a tirar"] --> P1["'montarPrateleira()'<br/>dois lotes de 24 peças"] --> F0["'montarAtivosECusto()'<br/>a folha, ainda sem laudo"]
  end
  subgraph SL["o laço, a cada quadro"]
    direction LR
    L0["'atualizar()'<br/>câmera a 4,45 m: lote de longe"]
  end
  subgraph SA["a morsa chega e se ajusta"]
    direction LR
    M0["caixa como chegou<br/>18 × 12 × 17"] --> M1["giro de −90° em X<br/>caixa 18 × 17 × 12"] --> M2["fator = 0,18 ÷ 18 = 0,0100<br/>caixa 0,18 × 0,17 × 0,12 m"]
  end
  subgraph SD["assenta e ganha o laudo"]
    direction LR
    M3["base 0,30 m acima do zero<br/>correção de 0,30 m para baixo"] --> M4["'apontarDivergencias()'<br/>três divergências; morsa no tampo"]
  end
  subgraph SC["clique em Medir agora"]
    direction LR
    C0["'censoDaCena()'<br/>13.484 triângulos, 15 chamadas"] --> C2["'linhasDaMedicao()'<br/>a medição entra na folha"]
    C1["'ler()'<br/>14 chamadas, 8.108 triângulos"] --> C2
  end
  SB --> SP --> SL --> SA --> SD --> SC
  classDef ativo fill:#fff1b8,stroke:#8a5a00,stroke-width:3px,color:#111
  classDef usado fill:#ececec,stroke:#7a7a7a,stroke-width:1px,stroke-dasharray:5 3,color:#444
  classDef novo fill:#d3efd3,stroke:#1d6b1d,stroke-width:4px,color:#111
  class B0,B2,B1,P0,P1,F0,L0 usado
  class M0 ativo

Figura 14: O arquivo chega sem erro, e importarAtivo() mede a caixa envolvente do que veio: 18 × 12 × 17 unidades do arquivo, em X, Y e Z. É a linha “Como chegou” do laudo.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph SB["montagem: a borda"]
    direction LR
    B0["'disporNaBorda()'<br/>206 posições na borda"] --> B1["'montarLoteInstanciado()'<br/>um lote com 206 cópias no tampo"] --> B2["'custoDaRepeticao()'<br/>206 chamadas contra 1"]
  end
  subgraph SP["a prateleira e a 1ª folha"]
    direction LR
    P0["'simplificar()'<br/>338 vértices, 114 unidos, 57 a tirar"] --> P1["'montarPrateleira()'<br/>dois lotes de 24 peças"] --> F0["'montarAtivosECusto()'<br/>a folha, ainda sem laudo"]
  end
  subgraph SL["o laço, a cada quadro"]
    direction LR
    L0["'atualizar()'<br/>câmera a 4,45 m: lote de longe"]
  end
  subgraph SA["a morsa chega e se ajusta"]
    direction LR
    M0["caixa como chegou<br/>18 × 12 × 17"] --> M1["giro de −90° em X<br/>caixa 18 × 17 × 12"] --> M2["fator = 0,18 ÷ 18 = 0,0100<br/>caixa 0,18 × 0,17 × 0,12 m"]
  end
  subgraph SD["assenta e ganha o laudo"]
    direction LR
    M3["base 0,30 m acima do zero<br/>correção de 0,30 m para baixo"] --> M4["'apontarDivergencias()'<br/>três divergências; morsa no tampo"]
  end
  subgraph SC["clique em Medir agora"]
    direction LR
    C0["'censoDaCena()'<br/>13.484 triângulos, 15 chamadas"] --> C2["'linhasDaMedicao()'<br/>a medição entra na folha"]
    C1["'ler()'<br/>14 chamadas, 8.108 triângulos"] --> C2
  end
  SB --> SP --> SL --> SA --> SD --> SC
  classDef ativo fill:#fff1b8,stroke:#8a5a00,stroke-width:3px,color:#111
  classDef usado fill:#ececec,stroke:#7a7a7a,stroke-width:1px,stroke-dasharray:5 3,color:#444
  classDef novo fill:#d3efd3,stroke:#1d6b1d,stroke-width:4px,color:#111
  class B0,B2,B1,P0,P1,F0,L0,M0 usado
  class M1 ativo

Figura 15: O giro declarado, −90° em X, põe a morsa em pé. Medida de novo, a caixa dá 18 × 17 × 12: a altura e a profundidade trocaram de lugar.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph SB["montagem: a borda"]
    direction LR
    B0["'disporNaBorda()'<br/>206 posições na borda"] --> B1["'montarLoteInstanciado()'<br/>um lote com 206 cópias no tampo"] --> B2["'custoDaRepeticao()'<br/>206 chamadas contra 1"]
  end
  subgraph SP["a prateleira e a 1ª folha"]
    direction LR
    P0["'simplificar()'<br/>338 vértices, 114 unidos, 57 a tirar"] --> P1["'montarPrateleira()'<br/>dois lotes de 24 peças"] --> F0["'montarAtivosECusto()'<br/>a folha, ainda sem laudo"]
  end
  subgraph SL["o laço, a cada quadro"]
    direction LR
    L0["'atualizar()'<br/>câmera a 4,45 m: lote de longe"]
  end
  subgraph SA["a morsa chega e se ajusta"]
    direction LR
    M0["caixa como chegou<br/>18 × 12 × 17"] --> M1["giro de −90° em X<br/>caixa 18 × 17 × 12"] --> M2["fator = 0,18 ÷ 18 = 0,0100<br/>caixa 0,18 × 0,17 × 0,12 m"]
  end
  subgraph SD["assenta e ganha o laudo"]
    direction LR
    M3["base 0,30 m acima do zero<br/>correção de 0,30 m para baixo"] --> M4["'apontarDivergencias()'<br/>três divergências; morsa no tampo"]
  end
  subgraph SC["clique em Medir agora"]
    direction LR
    C0["'censoDaCena()'<br/>13.484 triângulos, 15 chamadas"] --> C2["'linhasDaMedicao()'<br/>a medição entra na folha"]
    C1["'ler()'<br/>14 chamadas, 8.108 triângulos"] --> C2
  end
  SB --> SP --> SL --> SA --> SD --> SC
  classDef ativo fill:#fff1b8,stroke:#8a5a00,stroke-width:3px,color:#111
  classDef usado fill:#ececec,stroke:#7a7a7a,stroke-width:1px,stroke-dasharray:5 3,color:#444
  classDef novo fill:#d3efd3,stroke:#1d6b1d,stroke-width:4px,color:#111
  class B0,B2,B1,P0,P1,F0,L0,M0,M1 usado
  class M2 ativo

Figura 16: O fator sai da caixa já girada: o maior lado real dividido pelo maior lado medido, 0,18 ÷ 18 = 0,0100. A morsa passa a medir 0,18 × 0,17 × 0,12 m, e ninguém digitou o 0,0100.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph SB["montagem: a borda"]
    direction LR
    B0["'disporNaBorda()'<br/>206 posições na borda"] --> B1["'montarLoteInstanciado()'<br/>um lote com 206 cópias no tampo"] --> B2["'custoDaRepeticao()'<br/>206 chamadas contra 1"]
  end
  subgraph SP["a prateleira e a 1ª folha"]
    direction LR
    P0["'simplificar()'<br/>338 vértices, 114 unidos, 57 a tirar"] --> P1["'montarPrateleira()'<br/>dois lotes de 24 peças"] --> F0["'montarAtivosECusto()'<br/>a folha, ainda sem laudo"]
  end
  subgraph SL["o laço, a cada quadro"]
    direction LR
    L0["'atualizar()'<br/>câmera a 4,45 m: lote de longe"]
  end
  subgraph SA["a morsa chega e se ajusta"]
    direction LR
    M0["caixa como chegou<br/>18 × 12 × 17"] --> M1["giro de −90° em X<br/>caixa 18 × 17 × 12"] --> M2["fator = 0,18 ÷ 18 = 0,0100<br/>caixa 0,18 × 0,17 × 0,12 m"]
  end
  subgraph SD["assenta e ganha o laudo"]
    direction LR
    M3["base 0,30 m acima do zero<br/>correção de 0,30 m para baixo"] --> M4["'apontarDivergencias()'<br/>três divergências; morsa no tampo"]
  end
  subgraph SC["clique em Medir agora"]
    direction LR
    C0["'censoDaCena()'<br/>13.484 triângulos, 15 chamadas"] --> C2["'linhasDaMedicao()'<br/>a medição entra na folha"]
    C1["'ler()'<br/>14 chamadas, 8.108 triângulos"] --> C2
  end
  SB --> SP --> SL --> SA --> SD --> SC
  classDef ativo fill:#fff1b8,stroke:#8a5a00,stroke-width:3px,color:#111
  classDef usado fill:#ececec,stroke:#7a7a7a,stroke-width:1px,stroke-dasharray:5 3,color:#444
  classDef novo fill:#d3efd3,stroke:#1d6b1d,stroke-width:4px,color:#111
  class B0,B2,B1,P0,P1,F0,L0,M0,M1,M2 usado
  class M3 ativo

Figura 17: Na caixa já girada e escalada, a base está 0,30 m acima do zero e o centro já está no zero em X e em Z. A correção desce a peça 0,30 m, e as medidas continuam 18,0 × 17,0 × 12,0 cm: é a linha “Como ficou” do laudo.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph SB["montagem: a borda"]
    direction LR
    B0["'disporNaBorda()'<br/>206 posições na borda"] --> B1["'montarLoteInstanciado()'<br/>um lote com 206 cópias no tampo"] --> B2["'custoDaRepeticao()'<br/>206 chamadas contra 1"]
  end
  subgraph SP["a prateleira e a 1ª folha"]
    direction LR
    P0["'simplificar()'<br/>338 vértices, 114 unidos, 57 a tirar"] --> P1["'montarPrateleira()'<br/>dois lotes de 24 peças"] --> F0["'montarAtivosECusto()'<br/>a folha, ainda sem laudo"]
  end
  subgraph SL["o laço, a cada quadro"]
    direction LR
    L0["'atualizar()'<br/>câmera a 4,45 m: lote de longe"]
  end
  subgraph SA["a morsa chega e se ajusta"]
    direction LR
    M0["caixa como chegou<br/>18 × 12 × 17"] --> M1["giro de −90° em X<br/>caixa 18 × 17 × 12"] --> M2["fator = 0,18 ÷ 18 = 0,0100<br/>caixa 0,18 × 0,17 × 0,12 m"]
  end
  subgraph SD["assenta e ganha o laudo"]
    direction LR
    M3["base 0,30 m acima do zero<br/>correção de 0,30 m para baixo"] --> M4["'apontarDivergencias()'<br/>três divergências; morsa no tampo"]
  end
  subgraph SC["clique em Medir agora"]
    direction LR
    C0["'censoDaCena()'<br/>13.484 triângulos, 15 chamadas"] --> C2["'linhasDaMedicao()'<br/>a medição entra na folha"]
    C1["'ler()'<br/>14 chamadas, 8.108 triângulos"] --> C2
  end
  SB --> SP --> SL --> SA --> SD --> SC
  classDef ativo fill:#fff1b8,stroke:#8a5a00,stroke-width:3px,color:#111
  classDef usado fill:#ececec,stroke:#7a7a7a,stroke-width:1px,stroke-dasharray:5 3,color:#444
  classDef novo fill:#d3efd3,stroke:#1d6b1d,stroke-width:4px,color:#111
  class B0,B2,B1,P0,P1,F0,L0,M0,M1,M2,M3 usado
  class M4 novo

Figura 18: apontarDivergencias() escreve três divergências: 100 unidades por metro, o giro de −90 graus e o deslocamento de 0,30 m. A morsa entra no tampo, com a base encostada no lado de cima dele, e a folha é reescrita com o laudo.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph SB["montagem: a borda"]
    direction LR
    B0["'disporNaBorda()'<br/>206 posições na borda"] --> B1["'montarLoteInstanciado()'<br/>um lote com 206 cópias no tampo"] --> B2["'custoDaRepeticao()'<br/>206 chamadas contra 1"]
  end
  subgraph SP["a prateleira e a 1ª folha"]
    direction LR
    P0["'simplificar()'<br/>338 vértices, 114 unidos, 57 a tirar"] --> P1["'montarPrateleira()'<br/>dois lotes de 24 peças"] --> F0["'montarAtivosECusto()'<br/>a folha, ainda sem laudo"]
  end
  subgraph SL["o laço, a cada quadro"]
    direction LR
    L0["'atualizar()'<br/>câmera a 4,45 m: lote de longe"]
  end
  subgraph SA["a morsa chega e se ajusta"]
    direction LR
    M0["caixa como chegou<br/>18 × 12 × 17"] --> M1["giro de −90° em X<br/>caixa 18 × 17 × 12"] --> M2["fator = 0,18 ÷ 18 = 0,0100<br/>caixa 0,18 × 0,17 × 0,12 m"]
  end
  subgraph SD["assenta e ganha o laudo"]
    direction LR
    M3["base 0,30 m acima do zero<br/>correção de 0,30 m para baixo"] --> M4["'apontarDivergencias()'<br/>três divergências; morsa no tampo"]
  end
  subgraph SC["clique em Medir agora"]
    direction LR
    C0["'censoDaCena()'<br/>13.484 triângulos, 15 chamadas"] --> C2["'linhasDaMedicao()'<br/>a medição entra na folha"]
    C1["'ler()'<br/>14 chamadas, 8.108 triângulos"] --> C2
  end
  SB --> SP --> SL --> SA --> SD --> SC
  classDef ativo fill:#fff1b8,stroke:#8a5a00,stroke-width:3px,color:#111
  classDef usado fill:#ececec,stroke:#7a7a7a,stroke-width:1px,stroke-dasharray:5 3,color:#444
  classDef novo fill:#d3efd3,stroke:#1d6b1d,stroke-width:4px,color:#111
  class B0,B2,B1,P0,P1,F0,L0,M0,M1,M2,M3,M4 usado
  class C0,C1 ativo
  class C2 novo

Figura 19: O medidor, a parte do programa que compara o tempo de cada quadro com o envelope (o tempo que o programa tem para preparar um quadro), guarda a janela dos 120 quadros mais recentes. O censo conta 13.484 triângulos e 15 chamadas previstas, e a leitura, 14 chamadas e 8.108 triângulos desenhados; a diferença, 5.376 triângulos e uma chamada, é o lote de perto, que está na árvore e desligado.

A folha depois do clique, como a página do módulo a escreve. As duas linhas de intervalo (o tempo entre um quadro e o seguinte) vêm com … no lugar dos números, porque medem a máquina em que a página rodou, e não o código; pela mesma razão, a folga pode sair como “acima do teto”, e a linha da placa muda de um aparelho para outro:

O ativo que veio de fora, e o que foi ajustado nele

Arquivo: /ativos/morsa.gltf

Como chegou: 18.00 x 12.00 x 17.00 unidades do arquivo

Como ficou: 18.0 x 17.0 x 12.0 cm

Fator de escala aplicado: 0.0100 (medido, não digitado)

Conteúdo: 4 malhas, 48 triângulos, 2 materiais (FerroFundido, AcoUsinado)

Animações no arquivo: AbrirFechar (3.0 s)

Unidade divergente: o arquivo mede 18.00 para uma peça de 0.18 m, o que dá 100 unidades por metro. O formato de troca fixa metro, e o exportador não obedeceu.

Eixo vertical divergente: o ativo veio com o vertical em outro eixo, e foi girado -90 graus em X para o vertical da cena ser Y.

Origem divergente: a peça vinha deslocada 0.30 m da própria base, e a correção a assentou na superfície de apoio.

Repetição tratada como repetição

Parafusos na borda: 206, a cada 2 cm de perímetro

Chamadas de desenho sem instanciar: 206

Chamadas de desenho instanciando: 1

Triângulos: 24 por parafuso, 4944 no total — e este número não muda com a instanciação, porque ela não é sobre triângulo

Detalhe cobrado por distância

Peças de reposição na prateleira: 24, em dois níveis instanciados

Nível de perto: 224 triângulos por peça, 5376 no total

Nível de longe: 110 triângulos por peça, 2640 no total (51% a menos)

Troca a 2.2 m da câmera — simplificada pela ferramenta, com as normais recalculadas

A engrenagem da bancada é a mesma malha e não recebe nível nenhum: ela fica a meio metro do olho, e simplificar o que está perto é trabalho jogado fora.

A medição, e a máquina em que ela foi obtida
Máquina de referência: desktop x64 do laboratório, com vídeo integrado e sem placa dedicada
Navegador previsto: Chrome com WebGL 2 habilitado
Placa relatada por este aparelho: ANGLE (Intel, Intel(R) Graphics (0x00007D67) Direct3D11 vs_5_0 ps_5_0, D3D11)
Teto declarado por quadro: 16.7 ms

Censo estático da cena: 13484 triângulos em 12 malhas soltas e 3 lotes instanciados (254 instâncias)
Chamadas de desenho previstas pelo censo: 15
Materiais distintos: 6

Medido em 120 quadros: 14 chamadas e 8108 triângulos efetivamente desenhados
Intervalo médio: … ms — … ms de folga
Pior intervalo da janela: … ms
Diferença entre censo e medição: 5376 triângulos não foram desenhados neste quadro, por estarem fora do campo de visão ou num nível de detalhe desligado, e voltam quando a câmera se move

1.6 Por que o código é assim

As decisões que o próprio código registra, cada uma no identificador em que mora; a última, que não mora num identificador só, na página do módulo.

NotaCONVENCAO_DA_CENA

Toda importação é medida contra a convenção da cena, e quem se ajusta é o ativo que chega, nunca a convenção. Sem ela escrita, não há contra o que acusar divergência.

NotamaiorLadoRealM

O fator de escala sai da medida real da peça por divisão, e por isso não é digitado: um 0,01 escrito à mão serve ao arquivo em centímetro e erra, sem aviso, no primeiro ativo em milímetro.

NotaimportarAtivo()

A ordem é fixa: girar, escalar e só então medir o deslocamento. Assentar antes de girar apoia a peça pelo lado que vai virar lateral, e ela fica flutuando ou enterrada, sem nenhum aviso.

NotaapontarDivergencias()

O critério é de tolerância, e não de igualdade: um fator de 0,998 é ruído de exportação, e a lista existe para acusar troca de unidade, que erra por ordem de grandeza. O eixo trocado é acusado sempre que o giro declarado não é zero.

NotalinhasDoAtivo()

A linha “Como chegou” fica em unidades do arquivo, e a linha “Como ficou”, em centímetros: com as duas em unidades diferentes, a troca de unidade aparece na própria folha.

NotamontarLoteInstanciado() e custoDaRepeticao()

A matriz de cada cópia é escrita uma vez, na montagem da cena, antes do primeiro quadro, e não a cada quadro. A medida certa da repetição é a chamada, e não o triângulo: o que a instanciação tira da conta é a preparação fixa de cada objeto.

Notasimplificar()

A ferramenta só sabe que dois lados planos se tocam quando os dois usam os mesmos vértices na aresta comum, e a malha desta cena dá a cada lado plano cópias próprias dos pontos dos cantos. A união da biblioteca só junta vértices iguais em tudo, e as cópias de um canto diferem na normal: por isso a normal e a coordenada de textura (o ponto da imagem colada que cai sobre o vértice) saem antes de unir. Depois, as normais se recalculam: a antiga daria uma iluminação que não corresponde à forma, e o defeito apareceria como mancha.

NotamontarPrateleira()

A troca liga um lote e desliga o outro. Trocar objeto por objeto pediria um objeto por peça, uma chamada por peça, e desfaria a instanciação. A distância medida é a da câmera ao centro da prateleira: com meio metro de largura, a diferença entre a peça mais próxima e a mais distante não muda a decisão.

NotacensoDaCena()

Lote e malha solta ficam em campos separados, porque somados num número só não mostrariam o que a instanciação mudou. O material se conta pelo uuid, o identificador de cada objeto de material, e não pelo nome, que dois materiais diferentes podem repetir.

NotaidentificarPlaca() e linhasDaMedicao()

Um nome de placa inventado faria quem lê acreditar que a medição pode ser repetida, e por isso a recusa sai por extenso. A máquina sai antes de qualquer número, e sem quadro medido a função para antes da média, em vez de imprimir 0,0 ms, que se leria como medição ótima.

NotaTETO_DO_QUADRO_MS

A página do módulo roda só na tela do computador, e por isso mede contra o teto da tela do computador, 16,7 ms. Numa tela de sessenta quadros por segundo, o navegador entrega um quadro a cada 16,7 ms, por mais leve que a cena seja, e contra os 11,1 ms do visor (o aparelho preso à cabeça que cobre os olhos), o teto que a cena adota quando roda nos dois aparelhos, todo quadro passaria do teto.

NotaA página do módulo

A cena é montada e desenhada antes de o ativo chegar, e continua de pé se ele não chegar. A medição sai no clique, e não na abertura da página, porque ela só tem valor depois de alguns quadros desenhados de verdade.

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, e o botão de aproximar, seguido de um segundo clique em Medir agora, mostra a troca de nível.

O que confere O que traz Quando algo falha Nesta execução
o laudo as linhas “Como chegou” e “Como ficou”, em unidades diferentes, e a lista de divergências, que, sem nenhuma (fator e deslocamento dentro da tolerância e giro zero), escreve “Nenhuma divergência acima da tolerância: o ativo chegou na convenção.” ativo que não chega: a folha escreve “O ativo externo não pôde ser trazido.” e a mensagem do erro 18,00 × 12,00 × 17,00 unidades contra 18,0 × 17,0 × 12,0 cm, e três divergências
as linhas da repetição as chamadas sem instanciar e instanciando, e os triângulos, que não mudam — 206 chamadas contra 1; 4.944 triângulos nas duas
as linhas dos níveis os triângulos dos dois níveis, e a observação de simplificar() malha pequena: “malha pequena demais para valer a simplificação; o original ficou”; ferramenta sem malha utilizável: “a ferramenta não devolveu malha utilizável; o original ficou no lugar” 224 e 110 triângulos por peça, “simplificada pela ferramenta, com as normais recalculadas”
o censo contra a leitura a diferença, que é o que não foi desenhado, e nunca erro de contagem sem quadro medido: “Ainda sem quadros medidos: a leitura dinâmica entra depois do primeiro quadro.”; placa que o navegador não informa: “placa não identificada” 13.484 − 8.108 = 5.376, o lote de perto desligado
o botão Aproximar da prateleira do fundo, seguido de Medir agora a câmera a 1,2 m do ponto para onde ela olha na prateleira, e a 1,29 m do centro dela, abaixo dos 2,2 m da troca — “Medido em 120 quadros: 1 chamada e 5376 triângulos efetivamente desenhados”: só o lote de perto, ligado

Exemplo deste infográfico: da abertura da página do módulo, sem mover a câmera, com a morsa chegando, até um clique em Medir agora, e o botão de aproximar na última conferência; execução de 27/10/2026.