1 Técnicas de modelagem de ambientes virtuais — Infográfico do código

O código deste módulo escreve ponto a ponto a forma de três peças de um ambiente virtual (um lugar desenhado pelo computador, em que dá para olhar em volta e mexer nas coisas), declara de que cada peça é feita e guarda, para cada peça que alguém vai pegar, uma caixa invisível contra a qual se testa se a mão encostou nela. Este infográfico acompanha uma execução só: a abertura da página do módulo, a que esse código monta no navegador, até um clique em Mostrar os volumes de contato.

1.1 O caminho

Da figura plana da engrenagem grande, no alto, até o texto que a página do módulo escreve embaixo do desenho, 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 TD
  subgraph S1["a forma da grande"]
    direction LR
    P["'perfilDeEngrenagem()'<br/>14 dentes, 56 pontos"] --> E["'extrudarPerfil()'<br/>338 vértices<br/>224 triângulos"]
  end
  subgraph S2["a árvore vestida"]
    direction LR
    A["'montarCena()'<br/>a árvore do módulo anterior"] --> C["'vestirCena()'<br/>forma, superfície<br/>e volume de cada peça"]
  end
  subgraph S3["o teste e o relatório"]
    direction LR
    D["'compararFolgas()'<br/>um ponto a 12 cm"] --> F["'montarConteudo()'<br/>escreve o relatório"]
  end
  S1 --> S2 --> S3
  S3 --> K["o relatório na tela<br/>440 triângulos nas cinco peças, 476 na cena<br/>4 de 4 superfícies, aplicadas a 8 malhas<br/>5 caixas, cada folga com a razão<br/>folga de 2,0 cm: não captura<br/>folga de 6,0 cm: captura"]

Figura 1: O perfil é a figura plana da peça vista de cima, guardada como uma sequência de pontos. A malha é a peça guardada como quatro listas de números, que a biblioteca gráfica sabe desenhar: é ela que desenha a cena (tudo o que existe no ambiente: as peças, a mesa, as luzes e a câmera). O volume de contato é a caixa invisível de cada peça que alguém vai pegar: a forma simples contra a qual se testa se o ponteiro (a seta do mouse, o dedo na tela do celular ou a linha reta que sai do controle) encostou. O relatório é o texto que o próprio programa escreve sobre a cena.

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
malha.ts o tipo Ponto2; a classe ConstrutorDeMalha, com vertice(), triangulo(), quadrilatero() e finalizar(); as funções contarTriangulos(), contarVertices(), extrudarPerfil(), perfilDeEngrenagem() e perfilRegular(); e as auxiliares polar(), ponto() e indiceDe() BufferGeometry e Float32BufferAttribute, da biblioteca
materiais.ts os tipos DescricaoDeMaterial, MaterialDaOficina, Catalogo, UsoDaSuperficie e UsoDoCatalogo, e as funções superficie(), montarCatalogo(), censoDoCatalogo(), itensDoCatalogo() e linhasDoCatalogo() MeshStandardMaterial, da biblioteca
colisao.ts o tipo VolumeDeContato; as funções volumeDaMalha(), volumeDeclarado(), contemPonto(), distanciaAoCentro() e contornoDoVolume(); e a classe RegistroDeContato, com registrar(), volume(), alternarExibicao() e linhas() Box3, EdgesGeometry e LineSegments, da biblioteca
pecas.ts os tipos LinhaDoInventario, Conteudo e ComparacaoDeFolga; as constantes ESPESSURA_DA_ENGRENAGEM (0,016 m) e FOLGAS; e as funções exigirMalha(), vestirCena(), trocarMaterial(), trocarMalha() e compararFolgas() o tipo CenaDaBancada, do módulo do grafo de cena; o tipo PecaId, do primeiro módulo
conteudo.ts montarConteudo(), que escreve o relatório paragrafo() e subtitulo(), do módulo de dispositivos
cena.ts uma versão nova do arquivo do módulo do grafo de cena, com uma mudança só: repousoDaPeca() devolve, para a engrenagem pequena, x = 47 cm, 2 cm mais longe da grande montarCena(), que chama repousoDaPeca(), e o resto do arquivo, como estavam
04_demo.ts a página do módulo: as constantes TETO_DO_QUADRO_MS (o mesmo valor de TETO_DESKTOP_MS), DISTANCIA_DE_TESTE_M (0,12) e FOLGA_GENEROSA_M (0,06); os seis botões; as funções criarNo(), criarBotao(), frasesDaFolga(), exigirMalhaDaCena(), exigirLuz(), geometriaComCascaInvertida() e enquadrar(); e o que roda a cada quadro, cada imagem que o aparelho mostra: o eixo gira, e a engrenagem grande também, quando Girar as peças está ligado montarPalco(), montarCena(), Relogio, Orcamento, TETO_DESKTOP_MS e montarLaco(), do módulo do grafo de cena

volumeDeclarado() e distanciaAoCentro() estão escritas e não rodam na página do módulo: nenhum código deste módulo as chama.

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 malha

Cada canto da malha é um vértice. Enquanto a malha é escrita, ConstrutorDeMalha guarda as quatro listas dela: três listas paralelas (uma entrada por vértice em cada uma, e a linha 2 de cada uma fala do mesmo vértice) e a lista de triângulos. Um vértice entra nas três de uma vez, e vertice() devolve o número dele:

malha.ts
vertice(
  x: number,
  y: number,
  z: number,
  nx: number,
  ny: number,
  nz: number,
  u: number,
  v: number,
): number {
  this.posicoes.push(x, y, z);
  this.normais.push(nx, ny, nz);
  this.uvs.push(u, v);
  return this.posicoes.length / 3 - 1;
}
Lista O que guarda por entrada Na engrenagem grande
posicoes a posição: onde o ponto está, em x, y e z 338 vértices, 1.014 números
normais a normal: para que lado a superfície está virada naquele ponto; é essa direção que a luz consulta 338 direções
uvs a coordenada de textura: que ponto de uma imagem colada na peça cai sobre o vértice; a imagem colada é a textura, e nenhuma peça tem textura ainda 338 pares
indices cada triângulo (na malha, a face) pelos índices dos seus três vértices: o índice é o número de uma linha das outras três listas 224 triângulos, 672 números

A malha da grande, na ordem em que extrudarPerfil() pede cada vértice a vertice(): as duas tampas (o lado plano de cima e o de baixo) e a lateral, ou casca.

Vértices Onde ficam Normal Nesta execução
0 no centro da tampa de cima (0, 1, 0): para cima posição (0; 0,8; 0) cm, coordenada de textura (0,5; 0,5)
1 a 56 na borda da tampa de cima, um por ponto do perfil (0, 1, 0) o 1 em (6,8; 0,8; 0) cm
57 no centro da tampa de baixo (0, −1, 0): para baixo posição (0; −0,8; 0) cm
58 a 113 na borda da tampa de baixo (0, −1, 0) o 58 em (6,8; −0,8; 0) cm
114 a 337 na casca, quatro por trecho da borda a do trecho, deitada no plano do chão 114 a 117, o primeiro retângulo, com a normal (0,68; 0; −0,74)
Triângulos Onde ficam Nesta execução
0 a 55 na tampa de cima o 0 é (0, 2, 1): centro, ponto seguinte, ponto atual
56 a 111 na tampa de baixo o 56 é (57, 58, 59): centro, ponto atual, ponto seguinte
112 a 223 na casca, dois por retângulo o 112 e o 113 são (114, 115, 116) e (114, 116, 117)

Um ponto do perfil é um Ponto2, com x e z no plano do chão: o primeiro ponto da grande é (6,8; 0) cm.

1.3.2 As superfícies

A superfície de uma peça se declara pelo que ela é, com dois números de 0 a 1: a metalicidade diz quanto ela se comporta como metal, que reflete o entorno, e a rugosidade, quanto ela espalha o reflexo, de perto de 0, como um espelho, a perto de 1, como papel. A biblioteca chama essa descrição de material, e quatro superfícies são quatro materiais. Cada uma carrega duas frases, o que entrega e o que cobra:

materiais.ts
export interface DescricaoDeMaterial {
  readonly nome: string;
  /** O que o material entrega ao olho, em uma frase. */
  readonly entrega: string;
  /** O que ele cobra em tempo real, em uma frase. Sem esta coluna a escolha é palpite. */
  readonly custo: string;
}
Tipo e campo O que guarda Nesta execução
DescricaoDeMaterial.nome, entrega, custo o nome, o que a superfície entrega ao olho e o que ela cobra quatro descrições: aco-escovado, latao, plastico-fosco e madeira
MaterialDaOficina.material a instância: cada material que o programa cria; duas peças podem apontar para a mesma, como dois cartões com o mesmo endereço quatro instâncias, uma por superfície
Catalogo o catálogo: as superfícies que o programa criou acoEscovado, latao, plasticoFosco e madeira
UsoDaSuperficie.malhas as malhas da cena que apontam para a instância latao: engrenagem-grande e engrenagem-pequena
UsoDoCatalogo.foraDoCatalogo as malhas cuja superfície não veio do catálogo nenhuma
Superfície Rugosidade Metalicidade Malhas que apontam para ela
aco-escovado 0,35 0,90 base-do-suporte e eixo
latao 0,45 0,75 engrenagem-grande e engrenagem-pequena
plastico-fosco 0,85 0,05 corpo e tampa, a tampa do mecanismo
madeira 0,80 0,00 tampo e cavalete

1.3.3 Os volumes

A caixa de contato soma uma margem de cada lado da peça, e essa margem se chama folga. O volume guarda a caixa já com a folga, e a razão da folga ao lado do número:

colisao.ts
export interface VolumeDeContato {
  /** O nó cuja forma visível este volume representa. */
  readonly no: Mesh;
  /** Meia-extensão da caixa, no espaço local do nó, já com a folga somada. */
  readonly meiaExtensao: Vector3;
  /** Centro da caixa no espaço local do nó. Raramente é a origem: malha assimétrica desloca. */
  readonly centroLocal: Vector3;
  /** Folga adotada, em metros, e a justificativa dela em uma frase. */
  readonly folga: number;
  readonly razaoDaFolga: string;
}

Nas razões, a silhueta é o recorte que sobraria se a peça fosse pintada de preto contra um fundo branco, e o “raio” da razão do eixo é a linha reta que sai do controle, e não o raio de um círculo.

Campo O que guarda Nesta execução, na engrenagem grande
no a peça cuja forma visível o volume representa engrenagem-grande
meiaExtensao a meia-extensão da caixa em cada direção, a distância do centro até a parede, já com a folga 9,75 × 2,8 × 9,80 cm
centroLocal o centro da caixa no espaço local da peça, em que tudo se mede a partir da própria peça: do zero dela e das três direções dela (0, 0, 0); na pequena, que não é simétrica, 0,34 mm para o lado negativo de x e 0,23 mm para o positivo de z
folga, razaoDaFolga a folga, em metros, e a frase que a justifica 0,02 e “a peça mais larga que a mão apanha inteira, fechada sobre a silhueta: leva a maior folga”

RegistroDeContato guarda os cinco volumes pela chave da peça, os cinco contornos de depuração (os desenhos em linhas que tornam cada caixa visível enquanto alguém constrói a cena) e exibindo, que começa false. As folgas saem de FOLGAS, uma por peça, cada uma com a sua razão:

Peça Folga Razão escrita no código
corpo 1,2 cm peça de apoio, apanhada pela lateral com a mão inteira
eixo 0,8 cm peça fina: com folga maior, ela capturaria de longe o raio dirigido à vizinha
engrenagem-grande 2,0 cm a peça mais larga que a mão apanha inteira, fechada sobre a silhueta: leva a maior folga
engrenagem-pequena 1,8 cm mesma pega da grande, com um pouco menos por conviver perto dela
tampa, a tampa do mecanismo 1,2 cm placa chata: sem folga vertical o dedo passa por cima sem tocar
Tipo e campo O que guarda Nesta execução
LinhaDoInventario a peça, a origem da forma e as contagens; autorada é a forma escrita pelo programa, e primitiva, a caixa pedida pronta à biblioteca cinco linhas; a da grande: autorada, 224 triângulos, 338 vértices
Conteudo.totalDeTriangulos, triangulosDaCena os triângulos das cinco peças e os da cena inteira, com o mobiliário 440 e 476
ComparacaoDeFolga a distância do ponto de teste, as duas folgas e o veredito de cada uma: o volume captura o ponto quando ele fica dentro da caixa 0,12 m; 0,02 m, false; 0,06 m, true

1.3.4 A árvore da cena

Cada objeto da cena é um nó. Um nó pode ficar pendurado em outro, o pai dele, e acompanha o pai quando ele se move; os nós pendurados uns nos outros formam a árvore da cena. Esta é a árvore desta execução, antes do clique:

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart LR
  S["'sala'"] --> L["duas luzes<br/>'luz-ambiente', 'luz-direcional'"]
  S --> B["'bancada'"]
  B --> T["'tampo'<br/>madeira"]
  B --> CA["'cavalete'<br/>madeira"]
  T --> SU["'suporte'"] --> BS["'base-do-suporte'<br/>aço escovado"]
  T --> PE["as cinco peças<br/>'corpo', 'eixo', 'engrenagem-grande',<br/>'engrenagem-pequena', 'tampa'"]
  PE --> VO["um contorno em cada peça<br/>'volume-corpo' … 'volume-tampa'<br/>escondidos"]
  T --> PA["'suporte-do-painel'"]
  T --> DE["'engrenagem-grande-com-defeito'<br/>escondida"]
  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 VO,DE novo

Figura 2: O módulo não cria, não tira e não troca o pai de nenhum nó que veio do módulo anterior: só troca o que há dentro deles. O que ele pendura tem borda grossa e fundo verde: um contorno em cada uma das cinco peças, que fica sendo filho dela, e a cópia com defeito da engrenagem grande, filha do tampo. Os seis nascem escondidos.

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 perfilDeEngrenagem() e perfilRegular()

Recebe Decide Devolve
14 dentes, raiz de 0,068 m e crista de 0,078 m; no eixo, 12 lados e raio de 0,012 m cada dente ocupa uma fatia igual da volta e ganha quatro pontos: no começo da fatia, na raiz, a 6,8 cm do centro; a 25% e a 55% da fatia, na crista, a 7,8 cm; e a 80%, de volta à raiz. perfilRegular() põe um ponto a cada fatia igual da volta 56 pontos na grande, 36 na pequena e 12 no eixo, percorridos no sentido horário vistos de cima

No código, a fatia de cada dente se chama passo, e o raio da crista, raioDoTopo:

malha.ts
/**
 * Perfil de uma engrenagem de dentes retos, com quatro pontos por dente: sobe ao
 * topo, atravessa a crista, desce à raiz e atravessa o vale.
 *
 * O número de dentes NÃO é botão de densidade: é forma. Ele fixa, junto com o
 * da engrenagem parceira, a razão de transmissão do mecanismo, e trocá-lo para
 * poupar triângulos muda o mecanismo. O custo existe do mesmo jeito — cada dente
 * acrescenta dezesseis triângulos à malha final —, mas é custo de forma, pago
 * porque a peça é aquela. Resolução, aqui, são os quatro pontos por dente: mais
 * pontos arredondariam o flanco sem mudar o que a peça é.
 */
export function perfilDeEngrenagem(
  dentes: number,
  raioDaRaiz: number,
  raioDoTopo: number,
): Ponto2[] {
  const pontos: Ponto2[] = [];
  const passo: number = (Math.PI * 2) / dentes;
  for (let k = 0; k < dentes; k += 1) {
    const inicio: number = k * passo;
    pontos.push(polar(raioDaRaiz, inicio));
    pontos.push(polar(raioDoTopo, inicio + passo * 0.25));
    pontos.push(polar(raioDoTopo, inicio + passo * 0.55));
    pontos.push(polar(raioDaRaiz, inicio + passo * 0.8));
  }
  return pontos;
}

/** Perfil de um prisma regular. É dele que o eixo do mecanismo tira os seus doze lados. */
export function perfilRegular(lados: number, raio: number): Ponto2[] {
  const pontos: Ponto2[] = [];
  for (let k = 0; k < lados; k += 1) {
    pontos.push(polar(raio, (k * Math.PI * 2) / lados));
  }
  return pontos;
}

1.4.2 extrudarPerfil()

Recebe Decide Devolve
o perfil de 56 pontos, a espessura de 0,016 m e o nome 'engrenagem-grande' três blocos, nesta ordem: a tampa de cima, com a ordem dos pontos invertida; a de baixo, na ordem natural; e a casca, com a normal de cada trecho apontando para fora e a coordenada de textura acompanhando a distância percorrida na borda uma malha de 338 vértices e 224 triângulos

As duas tampas saem deste trecho; no primeiro laço, o da tampa de cima, cada triângulo cita o ponto seguinte antes do atual, e no último, o da tampa de baixo, o atual antes do seguinte:

malha.ts
for (let i = 0; i < n; i += 1) {
  construtor.triangulo(centroTopo, indiceDe(topo, i + 1), indiceDe(topo, i));
}

const centroBase: number = construtor.vertice(0, -meia, 0, 0, -1, 0, 0.5, 0.5);
const base: number[] = [];
for (let i = 0; i < n; i += 1) {
  const p: Ponto2 = ponto(perfil, i);
  base.push(
    construtor.vertice(
      p.x,
      -meia,
      p.z,
      0,
      -1,
      0,
      0.5 + p.x / (2 * raioMaximo),
      0.5 - p.z / (2 * raioMaximo),
    ),
  );
}
for (let i = 0; i < n; i += 1) {
  construtor.triangulo(centroBase, indiceDe(base, i), indiceDe(base, i + 1));
}

A casca sai deste outro; nx e nz são as duas linhas da normal:

malha.ts
// Casca lateral. Cada aresta do perfil vira um retângulo com normal própria,
// perpendicular a ela: é isso que dá o dente com flanco chapado em vez de
// ombro arredondado. A coordenada horizontal da textura acompanha o perímetro
// percorrido, e a vertical vai da base ao topo.
let percorrido: number = 0;
for (let i = 0; i < n; i += 1) {
  const p: Ponto2 = ponto(perfil, i);
  const q: Ponto2 = ponto(perfil, i + 1);
  const comprimento: number = Math.hypot(q.x - p.x, q.z - p.z);

  // Normal para fora: o produto vetorial da direção vertical pela direção da
  // aresta. Na ordem inversa ela apontaria para dentro da peça, e a luz
  // acertaria a face pelo lado errado — a peça fica escura e ninguém acha o
  // motivo, porque a silhueta continua perfeita.
  const nx: number = (q.z - p.z) / comprimento;
  const nz: number = -(q.x - p.x) / comprimento;

  const u0: number = percorrido / perimetro;
  const u1: number = (percorrido + comprimento) / perimetro;

  const a: number = construtor.vertice(p.x, meia, p.z, nx, 0, nz, u0, 1);
  const b: number = construtor.vertice(q.x, meia, q.z, nx, 0, nz, u1, 1);
  const c: number = construtor.vertice(q.x, -meia, q.z, nx, 0, nz, u1, 0);
  const d: number = construtor.vertice(p.x, -meia, p.z, nx, 0, nz, u0, 0);
  construtor.quadrilatero(a, b, c, d);

  percorrido += comprimento;
}

%%{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: a tampa de cima"]
    direction TB
    A1["um vértice no centro e um em cada ponto<br/>normal para cima, y = 0,8 cm"] --> A2["um triângulo por trecho da borda<br/>centro, ponto seguinte, ponto atual<br/>57 vértices, 56 triângulos"]
  end
  subgraph K2["caso 2: a tampa de baixo"]
    direction TB
    B1["um vértice no centro e um em cada ponto<br/>normal para baixo, y = −0,8 cm"] --> B2["um triângulo por trecho da borda<br/>centro, ponto atual, ponto seguinte<br/>57 vértices, 56 triângulos"]
  end
  subgraph K3["caso 3: a casca"]
    direction TB
    C1["um retângulo em pé por trecho<br/>quatro vértices próprios<br/>a mesma normal nos quatro"] --> C2["'quadrilatero()': dois triângulos<br/>224 vértices, 112 triângulos"]
  end
  K1 ~~~ K2 ~~~ K3

Figura 3: Os três casos lado a lado, com o que cada um custa na engrenagem grande. Na pequena, de 36 pontos, eles dão 37, 37 e 144 vértices; no eixo, de 12, dão 13, 13 e 48. A extrusão é isto: levantar o perfil numa espessura, fechar as duas tampas e montar a casca.

1.4.3 contarTriangulos()

Recebe Decide Devolve
a malha de uma peça dois casos: com lista de índices, os triângulos são os índices divididos por 3; sem ela, as posições divididas por 3 224 na grande; nesta execução toda malha tem lista de índices, e o segundo caso não roda
malha.ts
export function contarTriangulos(geometria: BufferGeometry): number {
  const indice = geometria.getIndex();
  if (indice === null) {
    return geometria.getAttribute('position').count / 3;
  }
  return indice.count / 3;
}

1.4.4 vestirCena() e exigirMalha()

Trocar o que há dentro de nós que já existem é o que o código chama de vestir a árvore.

Recebe Decide Devolve
a cena que montarCena() montou exigirMalha() para a execução se a cena não trouxer a peça como malha; a grande, a pequena e o eixo ganham malha escrita, e o corpo e a tampa do mecanismo ficam com a caixa pronta; cada peça busca a sua folga em FOLGAS, e peça sem folga para a execução; a origem é 'primitiva' para o corpo e a tampa do mecanismo, e 'autorada' para as outras três um Conteudo com cinco linhas de inventário, 440 triângulos nas cinco peças e 476 na cena

A busca da folga, peça a peça, e o inventário:

pecas.ts
for (const [id, malha] of [
  ['corpo', corpo],
  ['eixo', eixo],
  ['engrenagem-grande', grande],
  ['engrenagem-pequena', pequena],
  ['tampa', tampa],
] as ReadonlyArray<readonly [PecaId, Mesh]>) {
  const folga = FOLGAS.get(id);
  if (folga === undefined) {
    throw new Error(`A peça "${id}" está na cena e não tem folga de contato declarada.`);
  }
  contato.registrar(id, volumeDaMalha(malha, folga.metros, folga.razao));
  inventario.push({
    peca: id,
    origem: id === 'corpo' || id === 'tampa' ? 'primitiva' : 'autorada',
    triangulos: contarTriangulos(malha.geometry),
    vertices: contarVertices(malha.geometry),
  });
}

1.4.5 trocarMalha() e trocarMaterial()

Liberar uma malha é avisar à biblioteca que a memória dela pode ser devolvida.

Recebe Decide Devolve
o nó e a malha nova, ou o material novo trocarMalha() libera a malha antiga antes de pôr a nova; trocarMaterial() libera o material antigo só quando ele não é o novo nada; nesta execução, três malhas trocadas e oito materiais trocados; os oito materiais antigos foram liberados, porque nenhum deles era o novo
pecas.ts
/**
 * Troca o material de um nó liberando o anterior, pela mesma razão de
 * `trocarMalha`. Liberar é seguro aqui porque cada material provisório da cena
 * do módulo anterior pertence a uma malha só; material compartilhado não se
 * libera na troca de um dos donos.
 */
function trocarMaterial(no: Mesh, novo: MeshStandardMaterial): void {
  const antigos: readonly Material[] = Array.isArray(no.material) ? no.material : [no.material];
  for (const antigo of antigos) {
    if (antigo !== novo) {
      antigo.dispose();
    }
  }
  no.material = novo;
}

/**
 * Troca a malha de um nó liberando a anterior.
 *
 * Liberar não é zelo: geometria vive na memória da placa, e trocar sem liberar
 * deixa a antiga lá. Numa troca isso não se nota; num ambiente que recarrega
 * conteúdo várias vezes por sessão, a memória de vídeo acaba — e o sintoma é o
 * navegador perder o contexto de desenho, que é a tela ficando preta sem
 * mensagem de erro.
 */
function trocarMalha(no: Mesh, nova: Mesh['geometry']): void {
  no.geometry.dispose();
  no.geometry = nova;
}

1.4.7 censoDoCatalogo() e linhasDoCatalogo()

Recebe Decide Devolve
o catálogo e a sala, o nó de onde a árvore inteira pende para cada nó: se não é malha, pula; se aponta para uma instância do catálogo, entra na lista dela; se aponta para outra, vai para foraDoCatalogo. A conferência é por identidade, e um latão igual em valor, criado à parte, cairia fora. linhasDoCatalogo() conta só as superfícies em uso e monta a linha das malhas de fora do catálogo só quando ela não está vazia oito malhas nas quatro superfícies, nenhuma fora; a linha “Superfícies do catálogo em uso na cena: 4 de 4, aplicadas a 8 malhas”
materiais.ts
/**
 * Percorre a cena e anota, para cada superfície do catálogo, quais malhas
 * apontam para a instância dela. A conferência é por identidade: um latão
 * idêntico em valor, mas criado à parte, cai em `foraDoCatalogo`.
 */
export function censoDoCatalogo(catalogo: Catalogo, raiz: Object3D): UsoDoCatalogo {
  const itens: readonly MaterialDaOficina[] = itensDoCatalogo(catalogo);
  const porItem: Map<MaterialDaOficina, string[]> = new Map<MaterialDaOficina, string[]>();
  for (const item of itens) {
    porItem.set(item, []);
  }
  const fora: string[] = [];

  raiz.traverse((no: Object3D) => {
    if (!(no instanceof Mesh)) {
      return;
    }
    const materiais: readonly Material[] = Array.isArray(no.material) ? no.material : [no.material];
    for (const material of materiais) {
      const item: MaterialDaOficina | undefined = itens.find((i) => i.material === material);
      if (item === undefined) {
        fora.push(no.name);
      } else {
        porItem.get(item)?.push(no.name);
      }
    }
  });

  return {
    superficies: itens.map((item) => ({ item, malhas: porItem.get(item) ?? [] })),
    foraDoCatalogo: fora,
  };
}

O censo, a contagem que o próprio programa faz do que a cena construída tem, roda antes de a cópia com defeito existir: por isso ela, que aponta para o mesmo latão, não está nas oito.

%%{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: não é malha"]
    direction TB
    A1["'no instanceof Mesh' é falso"] --> A2["volta sem anotar nada<br/>11 dos 19 nós desta cena<br/>sala, luzes, grupos, contornos"]
  end
  subgraph K2["caso 2: do catálogo"]
    direction TB
    B1["'itens.find' acha a instância"] --> B2["a malha entra na lista dela<br/>8 malhas em 4 superfícies"]
  end
  subgraph K3["caso 3: fora do catálogo"]
    direction TB
    C1["'itens.find' não acha"] --> C2["a malha vai para 'foraDoCatalogo'<br/>nenhuma nesta execução"]
  end
  K1 ~~~ K2 ~~~ K3

Figura 4: Os três casos de cada nó, com o que cada um somou nesta cena: 19 nós no momento do censo, 11 que não são malha e oito malhas, todas com a instância de uma das quatro superfícies.

1.4.8 volumeDaMalha()

Aqui, a geometria é a forma da peça, guardada na malha.

Recebe Decide Devolve
a malha da grande, a folga de 0,02 m e a razão a caixa sai da caixa envolvente da geometria, a menor caixa alinhada às direções da peça que contém todos os vértices; malha sem caixa envolvente para a execução; a folga se soma uma vez, ali meia-extensão de 9,75 × 2,8 × 9,80 cm, que é a caixa de 19,5 × 5,6 × 19,6 cm do relatório
colisao.ts
export function volumeDaMalha(no: Mesh, folga: number, razaoDaFolga: string): VolumeDeContato {
  // A caixa sai da geometria, e não do nó montado na cena. Duas razões. A caixa
  // da geometria já está no espaço local da peça, que é onde o volume tem de
  // viver — assim ele gira, escala e é reparentado junto, de graça. E ela ignora
  // o que estiver pendurado no nó, inclusive o contorno de depuração que este
  // mesmo arquivo acrescenta: volume derivado do nó inteiro engordaria a cada
  // vez que fosse recalculado.
  no.geometry.computeBoundingBox();
  const caixa: Box3 | null = no.geometry.boundingBox;
  if (caixa === null) {
    throw new Error(`A malha de "${no.name}" não tem caixa envolvente, e o volume sai dela.`);
  }
  const centroLocal: Vector3 = caixa.getCenter(new Vector3());
  const tamanho: Vector3 = caixa.getSize(new Vector3());

  return {
    no,
    meiaExtensao: new Vector3(
      tamanho.x / 2 + folga,
      tamanho.y / 2 + folga,
      tamanho.z / 2 + folga,
    ),
    centroLocal,
    folga,
    razaoDaFolga,
  };
}

1.4.9 contemPonto()

Recebe Decide Devolve
um volume e um ponto em coordenadas de mundo, em que tudo se mede a partir da cena leva o ponto ao espaço local da peça, desconta o centro e compara cada coordenada, sem o sinal, com a meia-extensão: o ponto está dentro só se as três couberem false contra a folga adotada e true contra a generosa
colisao.ts
/**
 * Um ponto do mundo está dentro do volume?
 *
 * O ponto é levado ao espaço local do nó antes do teste, e é essa conversão que
 * torna o volume solidário à peça: a peça girada gira o volume junto, de graça.
 * Testar em coordenadas de mundo obrigaria a recalcular a caixa a cada quadro, e
 * a caixa recalculada de um objeto girado cresce — o volume aumenta sozinho
 * quando a peça inclina, e a captura fica maior num ângulo do que no outro.
 */
export function contemPonto(volume: VolumeDeContato, pontoDeMundo: Vector3): boolean {
  volume.no.updateWorldMatrix(true, false);
  const local: Vector3 = volume.no.worldToLocal(pontoDeMundo.clone()).sub(volume.centroLocal);
  return (
    Math.abs(local.x) <= volume.meiaExtensao.x &&
    Math.abs(local.y) <= volume.meiaExtensao.y &&
    Math.abs(local.z) <= volume.meiaExtensao.z
  );
}

1.4.10 contornoDoVolume() e RegistroDeContato

Recebe Decide Devolve
um volume contornoDoVolume() monta as 12 arestas da caixa em linhas verdes, pendura o contorno no nó da peça e o deixa escondido; registrar() guarda o volume e o contorno; alternarExibicao() inverte exibindo e aplica o valor aos cinco contornos; linhas() monta uma linha de texto por volume, em centímetros, com a folga e a razão cinco contornos escondidos; no clique, alternarExibicao() devolve true
colisao.ts
/**
 * A exibição de depuração dos volumes: um contorno em linha, filho do próprio nó.
 *
 * Isto não é enfeite. O volume é a única parte do ambiente que decide
 * comportamento sem aparecer, e um recurso barato que o torna visível durante a
 * construção poupa a confusão de quando a manipulação chegar — a peça que gruda
 * longe e a que atravessa o encaixe têm o mesmo sintoma para quem olha só a tela.
 */
export function contornoDoVolume(volume: VolumeDeContato): LineSegments {
  const caixa: BoxGeometry = new BoxGeometry(
    volume.meiaExtensao.x * 2,
    volume.meiaExtensao.y * 2,
    volume.meiaExtensao.z * 2,
  );
  const contorno: LineSegments = new LineSegments(
    new EdgesGeometry(caixa),
    new LineBasicMaterial({ color: 0x4ade80 }),
  );
  contorno.name = `volume-${volume.no.name}`;
  contorno.position.copy(volume.centroLocal);
  contorno.visible = false;
  volume.no.add(contorno);
  caixa.dispose();
  return contorno;
}
colisao.ts
alternarExibicao(): boolean {
  this.exibindo = !this.exibindo;
  for (const contorno of this.contornos) {
    contorno.visible = this.exibindo;
  }
  return this.exibindo;
}

1.4.11 compararFolgas() e frasesDaFolga()

Recebe Decide Devolve
o conteúdo, 0,12 m e 0,06 m sem volume da grande, para a execução; põe o ponto de teste 12 cm adiante do centro da grande, na direção x, e o testa contra o volume registrado e contra um volume novo, de folga 6,0 cm; frasesDaFolga() monta a frase com “captura” ou “não captura”, conforme cada resultado capturaComAFolgaAdotada igual a false, capturaComAFolgaGenerosa igual a true, e a frase do relatório
pecas.ts
/**
 * A demonstração dos dois defeitos simétricos, medida em vez de descrita.
 *
 * Um ponto de teste é colocado a uma distância declarada da engrenagem grande,
 * ao lado dela, e a pergunta é se o volume o captura. Com a folga adotada, não
 * captura: o ponto está fora, e é isso o que se quer, porque ali a mão ainda não
 * encostou. Com uma folga generosa, captura — e a peça passa a grudar em quem
 * apenas passou perto.
 *
 * O defeito oposto não precisa de demonstração própria: é o mesmo teste com o
 * ponto encostado na ponta de um dente e a folga em zero: a caixa vira a
 * extensão exata da malha, e o ponto fica na borda dela, recusado ao menor desvio.
 */
export function compararFolgas(
  conteudo: Conteudo,
  distanciaDeTeste: number,
  folgaGenerosa: number,
): ComparacaoDeFolga {
  const volume: VolumeDeContato | undefined = conteudo.contato.volume('engrenagem-grande');
  if (volume === undefined) {
    throw new Error('A engrenagem grande não tem volume registrado, e a comparação depende dele.');
  }

  const centro: Vector3 = volume.no.localToWorld(volume.centroLocal.clone());
  const pontoDeTeste: Vector3 = centro.clone().add(new Vector3(distanciaDeTeste, 0, 0));

  const generoso: VolumeDeContato = volumeDaMalha(
    volume.no,
    folgaGenerosa,
    'folga deliberadamente exagerada, só para a comparação',
  );

  return {
    distanciaDeTeste,
    folgaAdotada: volume.folga,
    capturaComAFolgaAdotada: contemPonto(volume, pontoDeTeste),
    folgaGenerosa,
    capturaComAFolgaGenerosa: contemPonto(generoso, pontoDeTeste),
  };
}

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart LR
  subgraph K1["folga adotada: 2,0 cm"]
    direction TB
    A1["meia-extensão na direção x<br/>7,75 + 2,0 = 9,75 cm"] --> A2["o ponto está em x = 12 cm<br/>12 passa de 9,75"] --> A3["'contemPonto()' devolve false<br/>não captura"]
  end
  subgraph K2["folga generosa: 6,0 cm"]
    direction TB
    B1["meia-extensão na direção x<br/>7,75 + 6,0 = 13,75 cm"] --> B2["o ponto está em x = 12 cm<br/>12 cabe em 13,75; y e z são 0"] --> B3["'contemPonto()' devolve true<br/>captura"]
  end
  K1 ~~~ K2

Figura 5: O mesmo ponto contra as duas caixas. Na direção x, a caixa envolvente da grande vai a 7,75 cm do centro, e não aos 7,8 cm da crista, porque a ponta de dente mais próxima dessa direção fica 6,4 graus fora dela.

1.4.12 montarConteudo()

Recebe Decide Devolve
o lugar do relatório na tela, as linhas do inventário, as do catálogo, as dos volumes e a frase da comparação apaga o que havia e escreve três blocos, sempre nesta ordem, cada um com o seu título nada; o relatório é escrito uma vez, e nenhum botão o reescreve
conteudo.ts
/**
 * O inventário do conteúdo do módulo de modelagem: de onde veio a forma de cada
 * peça, quanto ela custa em triângulos, que superfícies a cena usa e — o que
 * nenhuma outra seção mostra — qual é o volume de contato de cada peça e por que
 * a folga é aquela.
 *
 * O volume vive aqui em texto porque ele decide comportamento sem aparecer na
 * tela. Ler a caixa em centímetros ao lado da razão escrita é o que permite
 * discutir a escolha antes de o encaixe existir para reclamar dela.
 */
export function montarConteudo(
  raiz: HTMLElement,
  inventario: readonly string[],
  materiais: readonly string[],
  volumes: readonly string[],
  comparacao: string,
): void {
  raiz.replaceChildren();

  raiz.appendChild(subtitulo('De onde veio a forma de cada peça'));
  for (const linha of inventario) {
    raiz.appendChild(paragrafo(linha));
  }

  raiz.appendChild(subtitulo('As superfícies, e o que cada uma cobra'));
  for (const linha of materiais) {
    raiz.appendChild(paragrafo(linha));
  }

  raiz.appendChild(subtitulo('O que se vê e o que colide'));
  for (const linha of volumes) {
    raiz.appendChild(paragrafo(linha));
  }
  raiz.appendChild(paragrafo(comparacao));
}

1.4.13 geometriaComCascaInvertida()

Recebe Decide Devolve
a malha da grande e o nome 'engrenagem-grande-com-defeito' quantos pontos tem o perfil, pelo total de vértices: (338 − 2) ÷ 6 = 56, e para se a conta não der inteira; confere, vértice a vértice, que a normal vertical está onde a ordem da extrusão a põe; inverte a normal do vértice 114 em diante, onde começa a casca uma cópia com as mesmas posições e 224 normais invertidas
04_demo.ts
/**
 * Cópia da geometria com a normal da casca lateral multiplicada por −1.
 *
 * Posições, faces e coordenadas de textura ficam idênticas: a silhueta das duas
 * peças coincide ponto a ponto, e só a resposta à luz muda. A casca se localiza
 * pela ordem em que `extrudarPerfil` emite os vértices — centro e n pontos do
 * topo, centro e n pontos da base, depois quatro por aresta —, então para um
 * perfil de n pontos ela ocupa os índices de 2n + 2 até 6n + 2, exclusivo. A
 * normal vertical de topo e base é conferida antes da inversão, para que uma
 * mudança naquela ordem quebre aqui com mensagem em vez de inverter a face errada.
 */
function geometriaComCascaInvertida(original: BufferGeometry, nome: string): BufferGeometry {
  const copia: BufferGeometry = original.clone();
  copia.name = nome;
  const normal = copia.getAttribute('normal');
  if (!(normal instanceof BufferAttribute)) {
    throw new Error('A geometria da engrenagem não tem atributo de normal editável.');
  }
  const total: number = normal.count;
  const pontosDoPerfil: number = (total - 2) / 6;
  if (!Number.isInteger(pontosDoPerfil)) {
    throw new Error(`${total} vértices não correspondem a uma extrusão de perfil fechado.`);
  }
  const inicioDaCasca: number = 2 * pontosDoPerfil + 2;
  for (let i = 0; i < total; i += 1) {
    const naCasca: boolean = i >= inicioDaCasca;
    if (naCasca !== (normal.getY(i) === 0)) {
      throw new Error(`O vértice ${i} não está onde a ordem da extrusão o coloca.`);
    }
    if (naCasca) {
      normal.setXYZ(i, -normal.getX(i), 0, -normal.getZ(i));
    }
  }
  normal.needsUpdate = true;
  return copia;
}

1.4.14 enquadrar(), exigirMalhaDaCena() e exigirLuz()

Recebe Decide Devolve
nada; enquadrar() lê dois estados, o par à vista e o eixo pedido com o eixo pedido, a câmera vai para perto do eixo, mesmo com o par à vista; só com o par, vai para diante do par; sem nenhum, volta ao ponto guardado. exigirMalhaDaCena() e exigirLuz() param a execução se a cena não trouxer o nó do tipo esperado as duas últimas acham a engrenagem grande e as duas luzes; enquadrar() só roda nos cliques da seção de conferência
04_demo.ts
// A câmera da cena é fixa; as duas aproximações a movem e a devolvem ao ponto
// guardado aqui. O eixo tem precedência sobre o par quando os dois estão
// ligados, porque a faceta só se vê a poucos centímetros.
const posicaoInicial: Vector3 = palco.camera.position.clone();
const orientacaoInicial: Quaternion = palco.camera.quaternion.clone();
let parVisivel: boolean = false;
let pertoDoEixo: boolean = false;
let girando: boolean = false;

function enquadrar(): void {
  const camera = palco.camera;
  if (pertoDoEixo) {
    // Nove centímetros à frente do eixo e um pouco acima do centro dele: perto
    // o bastante para que cada um dos doze lados ocupe uma faixa visível, e
    // longe o bastante para não cruzar o plano de corte próximo, de 5 cm.
    const alvo: Vector3 = cena.eixo.getWorldPosition(new Vector3());
    camera.position.copy(alvo).add(new Vector3(0, 0.03, 0.09));
    camera.lookAt(alvo);
  } else if (parVisivel) {
    // Trinta centímetros à frente do ponto médio do par, a uns 21 graus de
    // elevação: alto o bastante para ver as duas tampas de cima, baixo e perto o
    // bastante para que a casca lateral, de 1,6 cm, apareça como faixa e não
    // como linha.
    const alvo: Vector3 = grande
      .getWorldPosition(new Vector3())
      .add(defeituosa.getWorldPosition(new Vector3()))
      .multiplyScalar(0.5);
    camera.position.copy(alvo).add(new Vector3(0, 0.11, 0.28));
    camera.lookAt(alvo);
  } else {
    camera.position.copy(posicaoInicial);
    camera.quaternion.copy(orientacaoInicial);
  }
}

1.4.15 repousoDaPeca()

Recebe Decide Devolve
o nome da peça um caso por peça: onde ela repousa sobre o tampo para a engrenagem pequena, (47; 3,3; −5) cm, 2 cm mais longe da grande do que no módulo anterior
cena.ts
/** Onde cada peça repousa sobre o tampo, antes de qualquer montagem. */
function repousoDaPeca(id: PecaId): [number, number, number] {
  switch (id) {
    case 'corpo':
      return [0, 0.055, 0];
    case 'eixo':
      return [-0.32, 0.135, 0.12];
    case 'engrenagem-grande':
      return [0.3, 0.033, 0.1];
    case 'engrenagem-pequena':
      // Dois centímetros mais longe da grande do que na etapa anterior: com a
      // geometria de dentes e as folgas de contato, a 45 cm as duas caixas se
      // cruzavam em repouso (1,2 cm em x, 1,3 cm em z).
      return [0.47, 0.033, -0.05];
    case 'tampa':
      return [-0.15, 0.031, -0.18];
  }
}

1.5 Uma execução, passo a passo

As abas seguem a abertura da página do módulo até o primeiro clique em Mostrar os volumes de contato. Em cada aba, o que roda naquela aba tem borda grossa e fundo amarelo, o que fica pronto nela 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 R1["a cena e o perfil"]
    direction LR
    A0["'montarCena()'<br/>a árvore do módulo anterior<br/>'repousoDaPeca()': a pequena em x = 47 cm"] --> P0["'perfilDeEngrenagem()'<br/>14 dentes, 4 pontos por dente<br/>56 pontos"] --> P1["tampa de cima<br/>57 vértices, 56 triângulos<br/>ordem inversa"]
  end
  subgraph R2["a malha da grande"]
    direction LR
    P2["tampa de baixo<br/>57 vértices, 56 triângulos<br/>ordem natural"] --> P3["casca<br/>56 retângulos em pé<br/>224 vértices, 112 triângulos"] --> M["'finalizar()'<br/>a malha da grande<br/>338 vértices, 224 triângulos"]
  end
  subgraph R3["vestir e dar as caixas"]
    direction LR
    V0["a grande<br/>'trocarMalha()': sai o cilindro<br/>'trocarMaterial()': latão"] --> V1["as outras sete malhas<br/>a pequena com o mesmo latão<br/>eixo de 12 lados, de aço<br/>corpo, tampa do mecanismo e mobiliário"] --> C0["'volumeDaMalha()' nas cinco peças<br/>a grande: 19,5 × 5,6 × 19,6 cm<br/>'contornoDoVolume()': contorno escondido"]
  end
  subgraph R4["contar, testar e escrever"]
    direction LR
    C1["inventário: 440 triângulos<br/>cena inteira: 476<br/>'censoDoCatalogo()': 4 de 4, 8 malhas"] --> D0["'compararFolgas()'<br/>ponto a 12 cm do centro<br/>2,0 cm: não captura<br/>6,0 cm: captura"] --> D1["'montarConteudo()'<br/>três blocos de texto"] --> F["o relatório na tela"]
  end
  subgraph R5["depois do relatório"]
    direction LR
    E0["'geometriaComCascaInvertida()'<br/>224 normais invertidas<br/>a cópia, escondida, 20 cm à direita"] --> E1["clique em Mostrar os volumes<br/>'alternarExibicao()' devolve true<br/>cinco contornos à vista"]
  end
  R1 --> R2 --> R3 --> R4 --> R5
  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 A0 ativo

Figura 6: montarCena(), do módulo anterior, monta a árvore com as cinco peças provisórias, caixas e cilindros pedidos prontos à biblioteca. Na versão deste módulo, repousoDaPeca() devolve para a engrenagem pequena x = 47 cm, e montarCena() a põe ali, porque a 45 cm a caixa dela e a da grande se cruzavam.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph R1["a cena e o perfil"]
    direction LR
    A0["'montarCena()'<br/>a árvore do módulo anterior<br/>'repousoDaPeca()': a pequena em x = 47 cm"] --> P0["'perfilDeEngrenagem()'<br/>14 dentes, 4 pontos por dente<br/>56 pontos"] --> P1["tampa de cima<br/>57 vértices, 56 triângulos<br/>ordem inversa"]
  end
  subgraph R2["a malha da grande"]
    direction LR
    P2["tampa de baixo<br/>57 vértices, 56 triângulos<br/>ordem natural"] --> P3["casca<br/>56 retângulos em pé<br/>224 vértices, 112 triângulos"] --> M["'finalizar()'<br/>a malha da grande<br/>338 vértices, 224 triângulos"]
  end
  subgraph R3["vestir e dar as caixas"]
    direction LR
    V0["a grande<br/>'trocarMalha()': sai o cilindro<br/>'trocarMaterial()': latão"] --> V1["as outras sete malhas<br/>a pequena com o mesmo latão<br/>eixo de 12 lados, de aço<br/>corpo, tampa do mecanismo e mobiliário"] --> C0["'volumeDaMalha()' nas cinco peças<br/>a grande: 19,5 × 5,6 × 19,6 cm<br/>'contornoDoVolume()': contorno escondido"]
  end
  subgraph R4["contar, testar e escrever"]
    direction LR
    C1["inventário: 440 triângulos<br/>cena inteira: 476<br/>'censoDoCatalogo()': 4 de 4, 8 malhas"] --> D0["'compararFolgas()'<br/>ponto a 12 cm do centro<br/>2,0 cm: não captura<br/>6,0 cm: captura"] --> D1["'montarConteudo()'<br/>três blocos de texto"] --> F["o relatório na tela"]
  end
  subgraph R5["depois do relatório"]
    direction LR
    E0["'geometriaComCascaInvertida()'<br/>224 normais invertidas<br/>a cópia, escondida, 20 cm à direita"] --> E1["clique em Mostrar os volumes<br/>'alternarExibicao()' devolve true<br/>cinco contornos à vista"]
  end
  R1 --> R2 --> R3 --> R4 --> R5
  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 A0 usado
  class P0 ativo

Figura 7: perfilDeEngrenagem(14, 0.068, 0.078) devolve 56 pontos, quatro por dente, no sentido horário vistos de cima: o primeiro na raiz, em (6,8; 0) cm, e o segundo já na crista, a 7,8 cm do centro.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph R1["a cena e o perfil"]
    direction LR
    A0["'montarCena()'<br/>a árvore do módulo anterior<br/>'repousoDaPeca()': a pequena em x = 47 cm"] --> P0["'perfilDeEngrenagem()'<br/>14 dentes, 4 pontos por dente<br/>56 pontos"] --> P1["tampa de cima<br/>57 vértices, 56 triângulos<br/>ordem inversa"]
  end
  subgraph R2["a malha da grande"]
    direction LR
    P2["tampa de baixo<br/>57 vértices, 56 triângulos<br/>ordem natural"] --> P3["casca<br/>56 retângulos em pé<br/>224 vértices, 112 triângulos"] --> M["'finalizar()'<br/>a malha da grande<br/>338 vértices, 224 triângulos"]
  end
  subgraph R3["vestir e dar as caixas"]
    direction LR
    V0["a grande<br/>'trocarMalha()': sai o cilindro<br/>'trocarMaterial()': latão"] --> V1["as outras sete malhas<br/>a pequena com o mesmo latão<br/>eixo de 12 lados, de aço<br/>corpo, tampa do mecanismo e mobiliário"] --> C0["'volumeDaMalha()' nas cinco peças<br/>a grande: 19,5 × 5,6 × 19,6 cm<br/>'contornoDoVolume()': contorno escondido"]
  end
  subgraph R4["contar, testar e escrever"]
    direction LR
    C1["inventário: 440 triângulos<br/>cena inteira: 476<br/>'censoDoCatalogo()': 4 de 4, 8 malhas"] --> D0["'compararFolgas()'<br/>ponto a 12 cm do centro<br/>2,0 cm: não captura<br/>6,0 cm: captura"] --> D1["'montarConteudo()'<br/>três blocos de texto"] --> F["o relatório na tela"]
  end
  subgraph R5["depois do relatório"]
    direction LR
    E0["'geometriaComCascaInvertida()'<br/>224 normais invertidas<br/>a cópia, escondida, 20 cm à direita"] --> E1["clique em Mostrar os volumes<br/>'alternarExibicao()' devolve true<br/>cinco contornos à vista"]
  end
  R1 --> R2 --> R3 --> R4 --> R5
  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 A0,P0 usado
  class P1,P2 ativo

Figura 8: Cada tampa ganha um vértice no centro e um em cada ponto: 57 vértices e 56 triângulos. A de cima cita os pontos na ordem inversa (centro, ponto seguinte, ponto atual), e a de baixo, na natural, para que cada uma gire no sentido anti-horário vista do seu próprio lado de fora.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph R1["a cena e o perfil"]
    direction LR
    A0["'montarCena()'<br/>a árvore do módulo anterior<br/>'repousoDaPeca()': a pequena em x = 47 cm"] --> P0["'perfilDeEngrenagem()'<br/>14 dentes, 4 pontos por dente<br/>56 pontos"] --> P1["tampa de cima<br/>57 vértices, 56 triângulos<br/>ordem inversa"]
  end
  subgraph R2["a malha da grande"]
    direction LR
    P2["tampa de baixo<br/>57 vértices, 56 triângulos<br/>ordem natural"] --> P3["casca<br/>56 retângulos em pé<br/>224 vértices, 112 triângulos"] --> M["'finalizar()'<br/>a malha da grande<br/>338 vértices, 224 triângulos"]
  end
  subgraph R3["vestir e dar as caixas"]
    direction LR
    V0["a grande<br/>'trocarMalha()': sai o cilindro<br/>'trocarMaterial()': latão"] --> V1["as outras sete malhas<br/>a pequena com o mesmo latão<br/>eixo de 12 lados, de aço<br/>corpo, tampa do mecanismo e mobiliário"] --> C0["'volumeDaMalha()' nas cinco peças<br/>a grande: 19,5 × 5,6 × 19,6 cm<br/>'contornoDoVolume()': contorno escondido"]
  end
  subgraph R4["contar, testar e escrever"]
    direction LR
    C1["inventário: 440 triângulos<br/>cena inteira: 476<br/>'censoDoCatalogo()': 4 de 4, 8 malhas"] --> D0["'compararFolgas()'<br/>ponto a 12 cm do centro<br/>2,0 cm: não captura<br/>6,0 cm: captura"] --> D1["'montarConteudo()'<br/>três blocos de texto"] --> F["o relatório na tela"]
  end
  subgraph R5["depois do relatório"]
    direction LR
    E0["'geometriaComCascaInvertida()'<br/>224 normais invertidas<br/>a cópia, escondida, 20 cm à direita"] --> E1["clique em Mostrar os volumes<br/>'alternarExibicao()' devolve true<br/>cinco contornos à vista"]
  end
  R1 --> R2 --> R3 --> R4 --> R5
  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 A0,P0,P1,P2 usado
  class P3 ativo
  class M novo

Figura 9: Cada um dos 56 trechos da borda vira um retângulo em pé, com quatro vértices próprios e a normal do trecho: 224 vértices e 112 triângulos. finalizar() entrega a malha pronta, com 57 + 57 + 224 = 338 vértices e 56 + 56 + 112 = 224 triângulos.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph R1["a cena e o perfil"]
    direction LR
    A0["'montarCena()'<br/>a árvore do módulo anterior<br/>'repousoDaPeca()': a pequena em x = 47 cm"] --> P0["'perfilDeEngrenagem()'<br/>14 dentes, 4 pontos por dente<br/>56 pontos"] --> P1["tampa de cima<br/>57 vértices, 56 triângulos<br/>ordem inversa"]
  end
  subgraph R2["a malha da grande"]
    direction LR
    P2["tampa de baixo<br/>57 vértices, 56 triângulos<br/>ordem natural"] --> P3["casca<br/>56 retângulos em pé<br/>224 vértices, 112 triângulos"] --> M["'finalizar()'<br/>a malha da grande<br/>338 vértices, 224 triângulos"]
  end
  subgraph R3["vestir e dar as caixas"]
    direction LR
    V0["a grande<br/>'trocarMalha()': sai o cilindro<br/>'trocarMaterial()': latão"] --> V1["as outras sete malhas<br/>a pequena com o mesmo latão<br/>eixo de 12 lados, de aço<br/>corpo, tampa do mecanismo e mobiliário"] --> C0["'volumeDaMalha()' nas cinco peças<br/>a grande: 19,5 × 5,6 × 19,6 cm<br/>'contornoDoVolume()': contorno escondido"]
  end
  subgraph R4["contar, testar e escrever"]
    direction LR
    C1["inventário: 440 triângulos<br/>cena inteira: 476<br/>'censoDoCatalogo()': 4 de 4, 8 malhas"] --> D0["'compararFolgas()'<br/>ponto a 12 cm do centro<br/>2,0 cm: não captura<br/>6,0 cm: captura"] --> D1["'montarConteudo()'<br/>três blocos de texto"] --> F["o relatório na tela"]
  end
  subgraph R5["depois do relatório"]
    direction LR
    E0["'geometriaComCascaInvertida()'<br/>224 normais invertidas<br/>a cópia, escondida, 20 cm à direita"] --> E1["clique em Mostrar os volumes<br/>'alternarExibicao()' devolve true<br/>cinco contornos à vista"]
  end
  R1 --> R2 --> R3 --> R4 --> R5
  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 A0,P0,P1,P2,P3,M usado
  class V0 ativo

Figura 10: montarCatalogo() já criou as quatro superfícies. trocarMalha() libera o cilindro provisório da engrenagem grande e põe no lugar a malha de 338 vértices; trocarMaterial() libera o material provisório e aponta a peça para o latão.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph R1["a cena e o perfil"]
    direction LR
    A0["'montarCena()'<br/>a árvore do módulo anterior<br/>'repousoDaPeca()': a pequena em x = 47 cm"] --> P0["'perfilDeEngrenagem()'<br/>14 dentes, 4 pontos por dente<br/>56 pontos"] --> P1["tampa de cima<br/>57 vértices, 56 triângulos<br/>ordem inversa"]
  end
  subgraph R2["a malha da grande"]
    direction LR
    P2["tampa de baixo<br/>57 vértices, 56 triângulos<br/>ordem natural"] --> P3["casca<br/>56 retângulos em pé<br/>224 vértices, 112 triângulos"] --> M["'finalizar()'<br/>a malha da grande<br/>338 vértices, 224 triângulos"]
  end
  subgraph R3["vestir e dar as caixas"]
    direction LR
    V0["a grande<br/>'trocarMalha()': sai o cilindro<br/>'trocarMaterial()': latão"] --> V1["as outras sete malhas<br/>a pequena com o mesmo latão<br/>eixo de 12 lados, de aço<br/>corpo, tampa do mecanismo e mobiliário"] --> C0["'volumeDaMalha()' nas cinco peças<br/>a grande: 19,5 × 5,6 × 19,6 cm<br/>'contornoDoVolume()': contorno escondido"]
  end
  subgraph R4["contar, testar e escrever"]
    direction LR
    C1["inventário: 440 triângulos<br/>cena inteira: 476<br/>'censoDoCatalogo()': 4 de 4, 8 malhas"] --> D0["'compararFolgas()'<br/>ponto a 12 cm do centro<br/>2,0 cm: não captura<br/>6,0 cm: captura"] --> D1["'montarConteudo()'<br/>três blocos de texto"] --> F["o relatório na tela"]
  end
  subgraph R5["depois do relatório"]
    direction LR
    E0["'geometriaComCascaInvertida()'<br/>224 normais invertidas<br/>a cópia, escondida, 20 cm à direita"] --> E1["clique em Mostrar os volumes<br/>'alternarExibicao()' devolve true<br/>cinco contornos à vista"]
  end
  R1 --> R2 --> R3 --> R4 --> R5
  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 A0,P0,P1,P2,P3,M,V0 usado
  class V1 ativo

Figura 11: A pequena sai do mesmo caminho, com nove dentes, e aponta para a mesma instância de latão. O eixo sai de perfilRegular(12, 0.012) e fica de aço. O corpo e a tampa do mecanismo continuam caixas prontas e ganham o plástico; o tampo e o cavalete ganham a madeira, e a base do suporte, o aço.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph R1["a cena e o perfil"]
    direction LR
    A0["'montarCena()'<br/>a árvore do módulo anterior<br/>'repousoDaPeca()': a pequena em x = 47 cm"] --> P0["'perfilDeEngrenagem()'<br/>14 dentes, 4 pontos por dente<br/>56 pontos"] --> P1["tampa de cima<br/>57 vértices, 56 triângulos<br/>ordem inversa"]
  end
  subgraph R2["a malha da grande"]
    direction LR
    P2["tampa de baixo<br/>57 vértices, 56 triângulos<br/>ordem natural"] --> P3["casca<br/>56 retângulos em pé<br/>224 vértices, 112 triângulos"] --> M["'finalizar()'<br/>a malha da grande<br/>338 vértices, 224 triângulos"]
  end
  subgraph R3["vestir e dar as caixas"]
    direction LR
    V0["a grande<br/>'trocarMalha()': sai o cilindro<br/>'trocarMaterial()': latão"] --> V1["as outras sete malhas<br/>a pequena com o mesmo latão<br/>eixo de 12 lados, de aço<br/>corpo, tampa do mecanismo e mobiliário"] --> C0["'volumeDaMalha()' nas cinco peças<br/>a grande: 19,5 × 5,6 × 19,6 cm<br/>'contornoDoVolume()': contorno escondido"]
  end
  subgraph R4["contar, testar e escrever"]
    direction LR
    C1["inventário: 440 triângulos<br/>cena inteira: 476<br/>'censoDoCatalogo()': 4 de 4, 8 malhas"] --> D0["'compararFolgas()'<br/>ponto a 12 cm do centro<br/>2,0 cm: não captura<br/>6,0 cm: captura"] --> D1["'montarConteudo()'<br/>três blocos de texto"] --> F["o relatório na tela"]
  end
  subgraph R5["depois do relatório"]
    direction LR
    E0["'geometriaComCascaInvertida()'<br/>224 normais invertidas<br/>a cópia, escondida, 20 cm à direita"] --> E1["clique em Mostrar os volumes<br/>'alternarExibicao()' devolve true<br/>cinco contornos à vista"]
  end
  R1 --> R2 --> R3 --> R4 --> R5
  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 A0,P0,P1,P2,P3,M,V0,V1 usado
  class C0 ativo

Figura 12: Para cada uma das cinco peças, vestirCena() acha a folga em FOLGAS, volumeDaMalha() mede o volume e registrar() o guarda: na grande, a caixa envolvente de 15,5 × 1,6 × 15,6 cm, mais 2,0 cm de cada lado, dá 19,5 × 5,6 × 19,6 cm. contornoDoVolume() pendura em cada peça o contorno dela, escondido.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph R1["a cena e o perfil"]
    direction LR
    A0["'montarCena()'<br/>a árvore do módulo anterior<br/>'repousoDaPeca()': a pequena em x = 47 cm"] --> P0["'perfilDeEngrenagem()'<br/>14 dentes, 4 pontos por dente<br/>56 pontos"] --> P1["tampa de cima<br/>57 vértices, 56 triângulos<br/>ordem inversa"]
  end
  subgraph R2["a malha da grande"]
    direction LR
    P2["tampa de baixo<br/>57 vértices, 56 triângulos<br/>ordem natural"] --> P3["casca<br/>56 retângulos em pé<br/>224 vértices, 112 triângulos"] --> M["'finalizar()'<br/>a malha da grande<br/>338 vértices, 224 triângulos"]
  end
  subgraph R3["vestir e dar as caixas"]
    direction LR
    V0["a grande<br/>'trocarMalha()': sai o cilindro<br/>'trocarMaterial()': latão"] --> V1["as outras sete malhas<br/>a pequena com o mesmo latão<br/>eixo de 12 lados, de aço<br/>corpo, tampa do mecanismo e mobiliário"] --> C0["'volumeDaMalha()' nas cinco peças<br/>a grande: 19,5 × 5,6 × 19,6 cm<br/>'contornoDoVolume()': contorno escondido"]
  end
  subgraph R4["contar, testar e escrever"]
    direction LR
    C1["inventário: 440 triângulos<br/>cena inteira: 476<br/>'censoDoCatalogo()': 4 de 4, 8 malhas"] --> D0["'compararFolgas()'<br/>ponto a 12 cm do centro<br/>2,0 cm: não captura<br/>6,0 cm: captura"] --> D1["'montarConteudo()'<br/>três blocos de texto"] --> F["o relatório na tela"]
  end
  subgraph R5["depois do relatório"]
    direction LR
    E0["'geometriaComCascaInvertida()'<br/>224 normais invertidas<br/>a cópia, escondida, 20 cm à direita"] --> E1["clique em Mostrar os volumes<br/>'alternarExibicao()' devolve true<br/>cinco contornos à vista"]
  end
  R1 --> R2 --> R3 --> R4 --> R5
  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 A0,P0,P1,P2,P3,M,V0,V1,C0 usado
  class C1 ativo

Figura 13: O inventário soma 440 triângulos nas cinco peças: 224 + 144 + 48 + 12 + 12. A cena inteira soma 476, com as três caixas de 12 do tampo, do cavalete e da base do suporte. censoDoCatalogo() acha as oito malhas nas quatro superfícies, e nenhuma fora do catálogo.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph R1["a cena e o perfil"]
    direction LR
    A0["'montarCena()'<br/>a árvore do módulo anterior<br/>'repousoDaPeca()': a pequena em x = 47 cm"] --> P0["'perfilDeEngrenagem()'<br/>14 dentes, 4 pontos por dente<br/>56 pontos"] --> P1["tampa de cima<br/>57 vértices, 56 triângulos<br/>ordem inversa"]
  end
  subgraph R2["a malha da grande"]
    direction LR
    P2["tampa de baixo<br/>57 vértices, 56 triângulos<br/>ordem natural"] --> P3["casca<br/>56 retângulos em pé<br/>224 vértices, 112 triângulos"] --> M["'finalizar()'<br/>a malha da grande<br/>338 vértices, 224 triângulos"]
  end
  subgraph R3["vestir e dar as caixas"]
    direction LR
    V0["a grande<br/>'trocarMalha()': sai o cilindro<br/>'trocarMaterial()': latão"] --> V1["as outras sete malhas<br/>a pequena com o mesmo latão<br/>eixo de 12 lados, de aço<br/>corpo, tampa do mecanismo e mobiliário"] --> C0["'volumeDaMalha()' nas cinco peças<br/>a grande: 19,5 × 5,6 × 19,6 cm<br/>'contornoDoVolume()': contorno escondido"]
  end
  subgraph R4["contar, testar e escrever"]
    direction LR
    C1["inventário: 440 triângulos<br/>cena inteira: 476<br/>'censoDoCatalogo()': 4 de 4, 8 malhas"] --> D0["'compararFolgas()'<br/>ponto a 12 cm do centro<br/>2,0 cm: não captura<br/>6,0 cm: captura"] --> D1["'montarConteudo()'<br/>três blocos de texto"] --> F["o relatório na tela"]
  end
  subgraph R5["depois do relatório"]
    direction LR
    E0["'geometriaComCascaInvertida()'<br/>224 normais invertidas<br/>a cópia, escondida, 20 cm à direita"] --> E1["clique em Mostrar os volumes<br/>'alternarExibicao()' devolve true<br/>cinco contornos à vista"]
  end
  R1 --> R2 --> R3 --> R4 --> R5
  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 A0,P0,P1,P2,P3,M,V0,V1,C0,C1 usado
  class D0 ativo

Figura 14: compararFolgas() põe um ponto 12 cm adiante do centro da engrenagem grande, na direção x. Com a folga adotada, a caixa vai a 9,75 cm do centro nessa direção, e o ponto fica fora: não captura. Com a folga de 6,0 cm, ela vai a 13,75 cm, e o ponto fica dentro: captura.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph R1["a cena e o perfil"]
    direction LR
    A0["'montarCena()'<br/>a árvore do módulo anterior<br/>'repousoDaPeca()': a pequena em x = 47 cm"] --> P0["'perfilDeEngrenagem()'<br/>14 dentes, 4 pontos por dente<br/>56 pontos"] --> P1["tampa de cima<br/>57 vértices, 56 triângulos<br/>ordem inversa"]
  end
  subgraph R2["a malha da grande"]
    direction LR
    P2["tampa de baixo<br/>57 vértices, 56 triângulos<br/>ordem natural"] --> P3["casca<br/>56 retângulos em pé<br/>224 vértices, 112 triângulos"] --> M["'finalizar()'<br/>a malha da grande<br/>338 vértices, 224 triângulos"]
  end
  subgraph R3["vestir e dar as caixas"]
    direction LR
    V0["a grande<br/>'trocarMalha()': sai o cilindro<br/>'trocarMaterial()': latão"] --> V1["as outras sete malhas<br/>a pequena com o mesmo latão<br/>eixo de 12 lados, de aço<br/>corpo, tampa do mecanismo e mobiliário"] --> C0["'volumeDaMalha()' nas cinco peças<br/>a grande: 19,5 × 5,6 × 19,6 cm<br/>'contornoDoVolume()': contorno escondido"]
  end
  subgraph R4["contar, testar e escrever"]
    direction LR
    C1["inventário: 440 triângulos<br/>cena inteira: 476<br/>'censoDoCatalogo()': 4 de 4, 8 malhas"] --> D0["'compararFolgas()'<br/>ponto a 12 cm do centro<br/>2,0 cm: não captura<br/>6,0 cm: captura"] --> D1["'montarConteudo()'<br/>três blocos de texto"] --> F["o relatório na tela"]
  end
  subgraph R5["depois do relatório"]
    direction LR
    E0["'geometriaComCascaInvertida()'<br/>224 normais invertidas<br/>a cópia, escondida, 20 cm à direita"] --> E1["clique em Mostrar os volumes<br/>'alternarExibicao()' devolve true<br/>cinco contornos à vista"]
  end
  R1 --> R2 --> R3 --> R4 --> R5
  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 A0,P0,P1,P2,P3,M,V0,V1,C0,C1,D0 usado
  class D1 ativo
  class F novo

Figura 15: montarConteudo() escreve os três blocos: de onde veio a forma de cada peça, as superfícies e os volumes, com a frase da comparação no fim. É o texto que aparece abaixo destas abas.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph R1["a cena e o perfil"]
    direction LR
    A0["'montarCena()'<br/>a árvore do módulo anterior<br/>'repousoDaPeca()': a pequena em x = 47 cm"] --> P0["'perfilDeEngrenagem()'<br/>14 dentes, 4 pontos por dente<br/>56 pontos"] --> P1["tampa de cima<br/>57 vértices, 56 triângulos<br/>ordem inversa"]
  end
  subgraph R2["a malha da grande"]
    direction LR
    P2["tampa de baixo<br/>57 vértices, 56 triângulos<br/>ordem natural"] --> P3["casca<br/>56 retângulos em pé<br/>224 vértices, 112 triângulos"] --> M["'finalizar()'<br/>a malha da grande<br/>338 vértices, 224 triângulos"]
  end
  subgraph R3["vestir e dar as caixas"]
    direction LR
    V0["a grande<br/>'trocarMalha()': sai o cilindro<br/>'trocarMaterial()': latão"] --> V1["as outras sete malhas<br/>a pequena com o mesmo latão<br/>eixo de 12 lados, de aço<br/>corpo, tampa do mecanismo e mobiliário"] --> C0["'volumeDaMalha()' nas cinco peças<br/>a grande: 19,5 × 5,6 × 19,6 cm<br/>'contornoDoVolume()': contorno escondido"]
  end
  subgraph R4["contar, testar e escrever"]
    direction LR
    C1["inventário: 440 triângulos<br/>cena inteira: 476<br/>'censoDoCatalogo()': 4 de 4, 8 malhas"] --> D0["'compararFolgas()'<br/>ponto a 12 cm do centro<br/>2,0 cm: não captura<br/>6,0 cm: captura"] --> D1["'montarConteudo()'<br/>três blocos de texto"] --> F["o relatório na tela"]
  end
  subgraph R5["depois do relatório"]
    direction LR
    E0["'geometriaComCascaInvertida()'<br/>224 normais invertidas<br/>a cópia, escondida, 20 cm à direita"] --> E1["clique em Mostrar os volumes<br/>'alternarExibicao()' devolve true<br/>cinco contornos à vista"]
  end
  R1 --> R2 --> R3 --> R4 --> R5
  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 A0,P0,P1,P2,P3,M,V0,V1,C0,C1,D0,D1,F usado
  class E0 ativo

Figura 16: geometriaComCascaInvertida() copia a malha da grande e inverte as normais dos vértices 114 a 337, os 224 da casca. A cópia, com o mesmo latão, fica 20 cm à direita da grande, escondida até o botão Mostrar o par com defeito.

%%{init: {"flowchart": {"useMaxWidth": false, "rankSpacing": 30, "nodeSpacing": 30}, "sequence": {"useMaxWidth": false}, "class": {"useMaxWidth": false}, "state": {"useMaxWidth": false}, "er": {"useMaxWidth": false}}}%%
flowchart TD
  subgraph R1["a cena e o perfil"]
    direction LR
    A0["'montarCena()'<br/>a árvore do módulo anterior<br/>'repousoDaPeca()': a pequena em x = 47 cm"] --> P0["'perfilDeEngrenagem()'<br/>14 dentes, 4 pontos por dente<br/>56 pontos"] --> P1["tampa de cima<br/>57 vértices, 56 triângulos<br/>ordem inversa"]
  end
  subgraph R2["a malha da grande"]
    direction LR
    P2["tampa de baixo<br/>57 vértices, 56 triângulos<br/>ordem natural"] --> P3["casca<br/>56 retângulos em pé<br/>224 vértices, 112 triângulos"] --> M["'finalizar()'<br/>a malha da grande<br/>338 vértices, 224 triângulos"]
  end
  subgraph R3["vestir e dar as caixas"]
    direction LR
    V0["a grande<br/>'trocarMalha()': sai o cilindro<br/>'trocarMaterial()': latão"] --> V1["as outras sete malhas<br/>a pequena com o mesmo latão<br/>eixo de 12 lados, de aço<br/>corpo, tampa do mecanismo e mobiliário"] --> C0["'volumeDaMalha()' nas cinco peças<br/>a grande: 19,5 × 5,6 × 19,6 cm<br/>'contornoDoVolume()': contorno escondido"]
  end
  subgraph R4["contar, testar e escrever"]
    direction LR
    C1["inventário: 440 triângulos<br/>cena inteira: 476<br/>'censoDoCatalogo()': 4 de 4, 8 malhas"] --> D0["'compararFolgas()'<br/>ponto a 12 cm do centro<br/>2,0 cm: não captura<br/>6,0 cm: captura"] --> D1["'montarConteudo()'<br/>três blocos de texto"] --> F["o relatório na tela"]
  end
  subgraph R5["depois do relatório"]
    direction LR
    E0["'geometriaComCascaInvertida()'<br/>224 normais invertidas<br/>a cópia, escondida, 20 cm à direita"] --> E1["clique em Mostrar os volumes<br/>'alternarExibicao()' devolve true<br/>cinco contornos à vista"]
  end
  R1 --> R2 --> R3 --> R4 --> R5
  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 A0,P0,P1,P2,P3,M,V0,V1,C0,C1,D0,D1,F,E0 usado
  class E1 ativo

Figura 17: No clique em Mostrar os volumes de contato, alternarExibicao() passa exibindo de false para true e mostra os cinco contornos. O botão passa a dizer “Esconder os volumes de contato”, e o relatório fica como estava.

O relatório desta execução, como a página do módulo o escreve. O nome das peças e das superfícies sai sem acento e com hífen, como o programa o guarda, e o número, com ponto no lugar da vírgula:

De onde veio a forma de cada peça
corpo: forma primitiva, 12 triângulos e 24 vértices.
eixo: forma autorada, 48 triângulos e 74 vértices.
engrenagem-grande: forma autorada, 224 triângulos e 338 vértices.
engrenagem-pequena: forma autorada, 144 triângulos e 218 vértices.
tampa: forma primitiva, 12 triângulos e 24 vértices.
Total das cinco peças: 440 triângulos. A cena inteira, com tampo, cavalete e base do suporte: 476.

As superfícies, e o que cada uma cobra
Superfícies do catálogo em uso na cena: 4 de 4, aplicadas a 8 malhas
aco-escovado: rugosidade 0.35, metalicidade 0.90, em base-do-suporte, eixo
latao: rugosidade 0.45, metalicidade 0.75, em engrenagem-grande, engrenagem-pequena
plastico-fosco: rugosidade 0.85, metalicidade 0.05, em corpo, tampa
madeira: rugosidade 0.80, metalicidade 0.00, em tampo, cavalete

O que se vê e o que colide
corpo: caixa de 14.4 x 8.4 x 14.4 cm, folga de 1.2 cm — peça de apoio, apanhada pela lateral com a mão inteira
eixo: caixa de 4.0 x 23.6 x 4.0 cm, folga de 0.8 cm — peça fina: com folga maior, ela capturaria de longe o raio dirigido à vizinha
engrenagem-grande: caixa de 19.5 x 5.6 x 19.6 cm, folga de 2.0 cm — a peça mais larga que a mão apanha inteira, fechada sobre a silhueta: leva a maior folga
engrenagem-pequena: caixa de 12.9 x 5.2 x 13.0 cm, folga de 1.8 cm — mesma pega da grande, com um pouco menos por conviver perto dela
tampa: caixa de 15.4 x 3.6 x 15.4 cm, folga de 1.2 cm — placa chata: sem folga vertical o dedo passa por cima sem tocar
Um ponto a 12 cm do centro da engrenagem grande: com a folga adotada de 2.0 cm, o volume não captura; com uma folga de 6.0 cm, captura. O segundo caso é a peça grudando em quem só passou perto.

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.

NotaConstrutorDeMalha

Cada lado plano da peça ganha cópias próprias dos seus vértices, sempre. A engrenagem sai com mais vértices, e em troca o canto fica vivo: um vértice usado por dois lados teria uma normal só para os dois, e a luz arredondaria o canto. Peça mecânica com canto macio parece brinquedo.

Notafinalizar()

A esfera envolvente, a esfera que envolve a malha inteira, é o que a biblioteca consulta para decidir se a peça entra no campo de visão da câmera. Sem ela, a biblioteca a calcula sozinha no primeiro teste, no meio do desenho de um quadro; finalizar() a calcula na construção da malha, uma vez.

NotaextrudarPerfil(): a ordem das tampas

A biblioteca desenha só a face cuja borda gira no sentido anti-horário vista de fora, e a face no sentido errado não dá erro nem aviso: abre um buraco que só aparece de um certo ângulo. O perfil gira no sentido horário visto de cima, e por isso no anti-horário visto de baixo: a tampa de baixo usa a ordem natural, e a de cima inverte a ordem.

NotaextrudarPerfil(): a normal da casca

nx e nz dão a normal que aponta para fora. Com o sinal de menos no outro termo, ela apontaria para dentro da peça, e a luz acertaria a lateral pelo lado errado: a peça fica escura com a silhueta perfeita, e ninguém acha o motivo olhando a forma.

NotaextrudarPerfil(): a coordenada de textura

A coordenada horizontal acompanha a distância percorrida na borda, e a vertical vai da base ao topo, embora nenhuma peça tenha textura ainda. Deixada para depois, a coordenada mal distribuída só apareceria com a primeira imagem, como desenho esticado, e seria procurada na imagem, e não na malha.

NotaperfilDeEngrenagem()

O número de dentes não é parâmetro de resolução, a classe de parâmetros que muda a densidade (quantos triângulos a malha tem) sem mudar a forma. Com o da engrenagem parceira, ele fixa a razão de transmissão, a proporção em que as duas giram, e cada dente custa 16 triângulos porque a peça é aquela. Resolução, aqui, são os quatro pontos por dente.

Notasuperficie() e Catalogo

A superfície se declara pelo que é, metálica e pouco rugosa, e a luz que a cena declara decide a aparência: o projeto não estima a luz da sala real, e no regime aumentado (o celular que mostra a sala real com as peças desenhadas por cima) a peça recebe as mesmas duas luzes virtuais da tela do computador. Quatro superfícies bastam para a cena inteira, porque cada superfície diferente em uso é pelo menos uma troca de estado a mais por quadro: a passagem de uma superfície declarada para outra, entre desenhar uma peça e desenhar a seguinte.

NotaDescricaoDeMaterial

custo é obrigatório ao lado de entrega: sem a frase do que a superfície cobra, escolher entre aço e plástico é palpite, e o programa nem compila sem ela.

NotavestirCena(): um latão para as duas engrenagens

As duas engrenagens apontam para a mesma instância de latão, e não para dois latões iguais. A biblioteca compara identidade, e não valores: com dois latões, ela reenviaria à placa de vídeo (a parte do computador que faz o desenho) os valores de cor, rugosidade e metalicidade do segundo ao passar de uma engrenagem para a outra.

NotacensoDoCatalogo() e linhasDoCatalogo()

O censo confere por identidade, e por isso um latão igual em valor, criado à parte, aparece fora do catálogo. A primeira linha conta as superfícies em uso nas malhas da cena, e não as do catálogo, porque é esse número que diz quantas trocas de estado o quadro paga, no mínimo.

NotavolumeDaMalha()

A caixa sai da geometria da peça, e não do nó montado na cena, por duas razões. A geometria já está no espaço local da peça, e o volume gira, muda de tamanho e troca de pai junto com ela. E a caixa da geometria ignora o que está pendurado no nó, inclusive o contorno de depuração: tirado do nó inteiro, o volume engordaria a cada recálculo.

NotacontemPonto()

O ponto é levado ao espaço local da peça antes do teste. Testar em coordenadas de mundo obrigaria a recalcular a caixa a cada quadro, e a caixa recalculada de uma peça inclinada cresce: o volume aumentaria sozinho, e a captura seria maior num ângulo do que no outro.

NotacontornoDoVolume()

O volume é a única parte do ambiente que decide comportamento sem aparecer. Desenhá-lo, escondido e ligado por um botão, é o recurso barato que separa a peça que gruda de longe da peça que deixa passar, sem prender, o que já encostou nela: sem o desenho, os dois defeitos chegam a quem olha só a tela como a mesma queixa.

NotaFOLGAS

As folgas não são iguais, de propósito. A que serve para apanhar uma engrenagem de 15 cm afogaria o eixo, de 2,4 cm de largura: a caixa dele seria quase toda folga, e capturaria qualquer raio do controle que cruzasse a vizinhança. Cada número leva a razão ao lado, e é ela que o relatório mostra.

NotaexigirMalha() e a busca em FOLGAS

A peça que não chega como malha, ou que não tem folga declarada, para a execução com uma mensagem que diz o nome dela. Seguir em silêncio deixaria a peça com a forma provisória, ou com uma folga que ninguém escolheu.

NotatrocarMalha() e trocarMaterial()

A malha antiga continua na memória da placa de vídeo se ninguém a liberar. Numa troca isso não se nota; num ambiente que recarrega o conteúdo várias vezes, a memória de vídeo acaba, e o navegador deixa a tela preta, sem mensagem de erro. O material provisório se libera porque cada um pertence a uma malha só; material compartilhado não se libera na troca de um dos donos.

NotavestirCena(): vestir depois

A árvore do módulo anterior nasce igual e só depois é vestida: assim a cena de caixas e a cena vestida podem ser mostradas lado a lado. O corpo e a tampa do mecanismo ficam com a caixa pronta por decisão escrita: escrever à mão um bloco retangular é trabalho sem retorno, e a forma escrita vai onde ela significa alguma coisa, no dente e na faceta do eixo (o lado plano que se distingue do vizinho).

NotacompararFolgas()

O defeito da folga grande é medido, e não descrito: o mesmo ponto, ao lado da peça, testado contra as duas caixas. O defeito oposto não tem teste próprio, porque é o mesmo teste com o ponto encostado na ponta de um dente e a folga em zero: o ponto fica na parede da caixa e é recusado ao menor desvio.

NotarepousoDaPeca()

A engrenagem pequena repousa 2 cm mais longe da grande do que no módulo anterior. Com os dentes e as folgas, a 45 cm as caixas das duas se cruzavam com as peças paradas: 1,2 cm na direção x e 1,3 cm na direção z.

NotageometriaComCascaInvertida()

A cópia usa a mesma instância de latão da peça certa, e por isso a única diferença entre as duas é a normal da casca. Antes de inverter, a função confere que cada normal vertical está onde a ordem da extrusão a põe: se essa ordem mudar, a função para com uma mensagem, em vez de inverter a normal errada.

NotaalternarExibicao()

A função devolve o novo valor de exibindo, e é dele que o botão tira o rótulo: essa é a única razão de ela devolver alguma coisa.

1.7 Como o código se confere

O módulo não tem bateria de testes: quem confere o código é o próprio relatório, as paradas com mensagem, os botões de olhar e a verificação de tipos.

O que confere O que traz Quando algo falha Nesta execução
o inventário uma linha por peça, com a origem e as contagens, e os dois totais a contagem que não fecha à mão com o perfil (57 + 57 + 224 = 338 na grande) aponta a extrusão 440 triângulos nas cinco peças e 476 na cena
a primeira linha do catálogo as superfícies em uso e as malhas que apontam para elas uma malha com material criado à parte ganha a linha “Malhas com material fora do catálogo” “4 de 4, aplicadas a 8 malhas”, e nenhuma linha de fora do catálogo
as linhas dos volumes a caixa em centímetros, a folga e a razão de cada peça — cinco linhas, da caixa de 4,0 × 23,6 × 4,0 cm do eixo à de 19,5 × 5,6 × 19,6 cm da grande
a frase da comparação o veredito das duas folgas para o mesmo ponto — 2,0 cm: não captura; 6,0 cm: captura
as paradas com mensagem, em exigirMalha(), na busca em FOLGAS, em volumeDaMalha(), em compararFolgas() e em geometriaComCascaInvertida() a execução para e nomeia o que faltou por exemplo, “A peça”…” está na cena e não tem folga de contato declarada.” nenhuma parada, e nenhum erro no navegador
os botões Mostrar o par com defeito, Aproximar do eixo, Trocar a luz de lugar e Deixar uma luz só a conferência a olho da normal, da faceta e da dependência da luz — a cópia aparece e some; a luz direcional (a que vem de uma direção só, como a luz do sol) vai de x = 1,2 m para −1,2 m; a luz hemisférica (a que ilumina cada face pelo lado para onde ela olha, com cor de céu em cima e de chão embaixo) apaga; cada botão troca o próprio rótulo, e o relatório não muda
a verificação de tipos do TypeScript, em modo estrito todo o código do módulo, junto com o dos módulos anteriores um erro de tipo impede o programa de compilar nenhum erro

Exemplo deste infográfico: a abertura da página do módulo, até um clique em Mostrar os volumes de contato, e os botões de olhar na última conferência; execução de 27/10/2026.