// ─────────────────────────────────────────────────────────────────────
// Props3D — os objetos das caixas de ilustração, em três dimensões.
//
// As caixas ao lado das perguntas de estilo de vida mostravam desenhos de
// traço. Aqui os mesmos onze assuntos viram volume: um pote de suplemento que
// gira, uma taça, um despertador.
//
// Por que construídos e não baixados: procurei conjunto pronto em Poly Haven,
// Kenney e Quaternius, que são as bibliotecas CC0 de verdade. As três servem
// jogo — masmorra, pirata, nave — ou props fotorrealistas. Nenhuma tem cápsula,
// pote de suplemento, colchão e cigarro no mesmo traço. Montar os onze de
// autores diferentes quebraria a única coisa que os desenhos atuais têm de bom,
// que é falarem a mesma língua. Feitos de primitivas eles saem no mesmo material
// e na mesma paleta, custam zero byte de download e não têm licença a resolver.
//
// O acabamento é o do manequim de postura, não inventado aqui: corpo quase
// branco, contorno escuro por fora e o mesmo rig de quatro luzes, número por
// número. A primeira versão saiu em plástico coral com luz própria e parecia de
// outro aplicativo — os dois aparecem na mesma etapa, um do lado do outro.
//
// A sombra no chão é a única coisa que o manequim não tem. Sem ela o objeto
// flutua no meio da caixa.
//
// UM contexto WebGL para todos. O navegador dá por volta de dezesseis por aba e
// já batemos nesse teto uma vez; como só uma pergunta aparece por vez, o mesmo
// renderizador é emprestado para a caixa que estiver na tela e a cena é trocada.
// ─────────────────────────────────────────────────────────────────────
const { useEffect: useEffectP3, useRef: useRefP3 } = React;

const P3_TINTA = {
  corpo: 0xF7F8F8,
  corpoFundo: 0xE2E0DD,
  coral: 0xE8734A,
  coralFundo: 0xC15733,
  contorno: 0x241713,
};

/**
 * Contorno ligado ou desligado.
 *
 * Desligado, a forma passa a vir da luz: as peças projetam sombra umas nas
 * outras e em si mesmas, e é essa sombra interna que separa o braço da cadeira
 * do encosto, ou a tampa do pote do corpo dele. Com o contorno, essa separação
 * vinha da linha e a luz podia ser chapada; sem ele, a luz precisa de contraste
 * de volta, senão a peça vira uma silhueta de uma cor só.
 */
const P3_CONTORNO = false;

/**
 * Quanto cada peça ocupa da caixa.
 *
 * Sem isto todas saem do mesmo tamanho, porque a normalização encaixa qualquer
 * uma na mesma medida — e um pote de suplemento chegava do tamanho de uma cama.
 * O número é fração do lado da caixa.
 */
const P3_ESCALA = {
  suplemento: 0.68,
  disturbio: 0.8,
  medicamento: 0.92,
  frasco: 0.82,
  alcool: 0.86,
  acordar: 0.88,
};

/**
 * As quatro peças que vêm de malha pronta, e onde o coral entra em cada uma.
 *
 * Estas quatro saíram melhores geradas do que montadas de primitivas: o pote
 * tem tampa de rosca, a taça tem bojo e haste de verdade, a prancheta tem
 * proporção de prancheta e o colchão tem capitonê. As outras sete saíram
 * piores — o sol virou engrenagem, o cigarro perdeu a fumaça — e continuam
 * montadas aqui.
 *
 * A malha vem inteira, com um material só: pintar a tampa de coral não é
 * possível por peça, porque peça não existe. Então o coral entra por faixa de
 * altura — cada triângulo cai no branco ou no coral conforme onde o seu centro
 * está entre o pé e o topo da malha. `faixas` são esses trechos, em fração da
 * altura, e `giro` alinha a frente do modelo com a câmera, que cada geração
 * entrega virada para um lado.
 */
const P3_MALHA = {
  suplemento: { url: "props/pote.glb", faixas: [[0.86, 1.01], [0.34, 0.58]], giro: 0 },
  // O vinho: a faixa de fora é a linha que se vê pela parede; `interiores`
  // sobe pela parede de dentro até a borda, para quem olha a taça de cima ver
  // o bojo laranja e não o branco do vidro.
  alcool: { url: "props/taca.glb", faixas: [[0.52, 0.68]], interiores: [[0.52, 1.01]], giro: 0 },
  // A prancheta veio como um bloco de 0,56 x 0,98 x 0,68 — proporção de
  // tijolo, não de prancheta. De lado, a espessura somada à da folha lia como
  // duas pranchetas grudadas. `achatar` encolhe a peça na direção da chapa;
  // achei o eixo pela menor variância dos vértices no plano do chão, que caiu
  // quase em cima do X. Encolher fecha a peça, ao contrário de cortar: um
  // corte abriria a traseira e a casca do contorno apareceria por dentro,
  // preta, toda vez que a peça girasse de costas.
  tratamento: { url: "props/prancheta.glb", faixas: [[0.9, 1.01]], giro: 0, achatar: ["x", 0.32] },
  colchao: { url: "props/colchao.glb", faixas: [[0.42, 0.66]], giro: 0 },
  // Cadeira de escritório, para a etapa do impacto na qualidade de vida. Ali
  // havia um bloco laranja com bolhas: enfeite bonito que não dizia nada sobre
  // a pergunta. O assento é a faixa coral — é nele que a pessoa passa o dia.
  cadeira: { url: "props/cadeira.glb", faixas: [[0.4, 0.56]], giro: 0 },
  // O sol voltou para o código, e o arquivo saiu junto. A malha gerada tinha
  // dois defeitos que não se corrigem por parâmetro: nascia deitada no plano
  // errado — de frente virava uma lâmina, que foi o que apareceu na tela — e,
  // endireitada, o corte por raio que pinta o miolo de branco caía em cima dos
  // triângulos dela e deixava o miolo rasgado, com a borda serrilhada. O sol
  // desenhado em código nasce de frente e tem o miolo redondo.
  // Os objetos das perguntas da etapa do impacto.
  // O cérebro não leva faixa: uma listra horizontal no meio dele não é nada —
  // não é sulco, não é lobo, não é corte anatômico —, e num órgão cheio de
  // dobras a listra ainda competia com elas. O coral sai da superfície e vai
  // para a borda: contorno, que é onde uma cor forte marca a silhueta sem
  // fingir que descreve a anatomia.
  cerebro: { url: "props/cerebro.glb", faixas: [], contorno: "coral", contornoOpacidade: 0.6, giro: 0 },
  // Mesma decisão do cérebro, e pela mesma razão: a faixa cortava o órgão numa
  // altura que não corresponde a nada dele — nem átrio, nem ventrículo, nem
  // sulco coronário.
  coracao: { url: "props/coracao.glb", faixas: [], contorno: "coral", contornoOpacidade: 0.6, giro: 0 },
  // Maçã inteira em coral, menos o cabo e a folha: uma faixa no meio a cortava
  // em duas metades, e maçã de duas cores não é maçã.
  //
  // O teto é 0,75, e a margem é estreita dos dois lados: o corte é por altura,
  // e o pé do cabo está quase na mesma altura da borda da covinha onde ele
  // nasce. Em 0,82 sobrava um pedaço coral subindo pelo cabo; em 0,72 a covinha
  // já embranquece e a maçã ganha uma touca. Medido na malha, o pescoço — onde
  // o raio é mínimo — fica em 0,79.
  maca: { url: "props/maca.glb", faixas: [[0, 0.75]], giro: 0 },
  // A lupa veio deitada; `inclinar` a põe em pé. O aro vai de coral.
  lupa: { url: "props/lupa.glb", faixas: [], raio: [[0.5, 0.78]], giro: 0.4, inclinar: -1.35 },
  cama: { url: "props/cama.glb", faixas: [[0.3, 0.46]], giro: 0 },
};

/** Uma busca por arquivo, guardada: trocar de pergunta não rebaixa o modelo. */
const p3Malhas = new Map();

function p3Carregar(url) {
  if (p3Malhas.has(url)) return p3Malhas.get(url);
  const promessa = new Promise((resolve, reject) => {
    const { GLTFLoader } = window.GLTF_NS || {};
    if (!GLTFLoader) { reject(new Error("sem carregador")); return; }
    const l = new GLTFLoader();
    if (window.GLTF_NS.dracoLoader) l.setDRACOLoader(window.GLTF_NS.dracoLoader);
    l.load(url, resolve, undefined, reject);
  }).catch((e) => { p3Malhas.delete(url); throw e; });
  p3Malhas.set(url, promessa);
  return promessa;
}

/**
 * Corta a malha em duas pela altura: o que cai nas faixas vira coral.
 *
 * O corte é no plano, triângulo a triângulo, e não por qual lado o centro do
 * triângulo caiu. Separar por centro é uma linha de código, mas a borda passa
 * a seguir a malha em vez do plano: a faixa do rótulo saía com dente de serra,
 * porque o triângulo inteiro ia para um lado ou para o outro. Aqui o triângulo
 * que atravessa o plano é recortado nele, e a emenda fica reta.
 *
 * A normal de cada vértice novo é interpolada junto com a posição, senão a
 * quina inventada pelo corte aparece como um vinco na superfície lisa.
 */
function p3RecortarY(poli, yCorte, ficarAcima) {
  const saida = [];
  for (let i = 0; i < poli.length; i++) {
    const a = poli[i];
    const b = poli[(i + 1) % poli.length];
    const da = (a.p[1] - yCorte) * (ficarAcima ? 1 : -1);
    const db = (b.p[1] - yCorte) * (ficarAcima ? 1 : -1);
    if (da >= 0) saida.push(a);
    if ((da >= 0) !== (db >= 0)) {
      const t = da / (da - db);
      saida.push({
        p: [a.p[0] + (b.p[0] - a.p[0]) * t, yCorte, a.p[2] + (b.p[2] - a.p[2]) * t],
        n: [a.n[0] + (b.n[0] - a.n[0]) * t, a.n[1] + (b.n[1] - a.n[1]) * t, a.n[2] + (b.n[2] - a.n[2]) * t],
      });
    }
  }
  return saida.length >= 3 ? saida : null;
}

/**
 * Um triângulo olha para dentro se a sua normal aponta contra o raio.
 *
 * Numa peça girada — taça, pote, frasco — a parede de dentro e a de fora estão
 * na mesma altura, então faixa de altura sozinha pinta as duas. Comparar a
 * normal com a direção que sai do eixo separa uma da outra.
 */
function p3OlhaParaDentro(poli) {
  let nx = 0; let nz = 0; let x = 0; let z = 0;
  for (let i = 0; i < poli.length; i++) {
    nx += poli[i].n[0]; nz += poli[i].n[2];
    x += poli[i].p[0]; z += poli[i].p[2];
  }
  // Perto do eixo o raio não tem direção confiável; ali nada é "de dentro".
  if (x * x + z * z < 1e-6) return false;
  return nx * x + nz * z < 0;
}

/**
 * Faixa medida do centro para fora, no plano da peça.
 *
 * Serve para o que é redondo de frente — um alvo, um mostrador. O plano é o
 * dos dois maiores lados do envelope: num disco em pé são a largura e a
 * altura, e a distância ao centro deles é o raio que se vê.
 *
 * Várias faixas dão anéis concêntricos, que é o que faz um alvo parecer um
 * alvo: os sulcos da malha desenham a rosca, mas rosca sem cor alternada é
 * textura, não alvo.
 */
/**
 * A normal de uma nuvem de pontos: a direção em que ela é mais fina.
 *
 * Jacobi na matriz de covariância. Três rotações por varredura, uma por par de
 * eixos, até o que está fora da diagonal virar pó — aí as colunas guardam as
 * direções e a diagonal, o quanto a nuvem se espalha em cada uma. A menor é a
 * normal.
 */
function p3NormalDaNuvem(pontos, centro) {
  const n = pontos.length / 3;
  const C = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
  for (let i = 0; i < pontos.length; i += 3) {
    const d = [pontos[i] - centro[0], pontos[i + 1] - centro[1], pontos[i + 2] - centro[2]];
    for (let a = 0; a < 3; a++) for (let b = 0; b < 3; b++) C[a][b] += (d[a] * d[b]) / n;
  }
  const V = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
  for (let varredura = 0; varredura < 24; varredura++) {
    let maior = 0;
    for (let a = 0; a < 2; a++) for (let b = a + 1; b < 3; b++) maior = Math.max(maior, Math.abs(C[a][b]));
    if (maior < 1e-12) break;
    for (let a = 0; a < 2; a++) {
      for (let b = a + 1; b < 3; b++) {
        if (Math.abs(C[a][b]) < 1e-14) continue;
        const t = (C[b][b] - C[a][a]) / (2 * C[a][b]);
        const sinal = t >= 0 ? 1 : -1;
        const tg = sinal / (Math.abs(t) + Math.sqrt(t * t + 1));
        const cos = 1 / Math.sqrt(tg * tg + 1);
        const sen = tg * cos;
        for (let k = 0; k < 3; k++) {
          const ka = C[k][a]; const kb = C[k][b];
          C[k][a] = cos * ka - sen * kb;
          C[k][b] = sen * ka + cos * kb;
        }
        for (let k = 0; k < 3; k++) {
          const ak = C[a][k]; const bk = C[b][k];
          C[a][k] = cos * ak - sen * bk;
          C[b][k] = sen * ak + cos * bk;
        }
        for (let k = 0; k < 3; k++) {
          const ka = V[k][a]; const kb = V[k][b];
          V[k][a] = cos * ka - sen * kb;
          V[k][b] = sen * ka + cos * kb;
        }
      }
    }
  }
  let menor = 0;
  for (let a = 1; a < 3; a++) if (C[a][a] < C[menor][menor]) menor = a;
  return [V[0][menor], V[1][menor], V[2][menor]];
}

/**
 * Faixa medida do centro para fora, na cara da peça.
 *
 * Serve para o que é redondo — um alvo, um mostrador. O plano vem da normal da
 * própria nuvem de pontos, e não de um eixo do envelope: o alvo se apoia
 * inclinado no tripé, então nenhum eixo alinhado é a cara dele, e círculos
 * medidos num plano torto chegam ao disco como espiral.
 *
 * Várias faixas dão anéis concêntricos, que é o que faz um alvo parecer um
 * alvo: os sulcos da malha desenham a rosca, mas rosca sem cor alternada é
 * textura, não alvo.
 *
 * `soAlto` mede só a parte de cima. O envelope inteiro do alvo inclui o tripé,
 * que desce bem abaixo do disco: o centro caía embaixo da mosca e os anéis
 * saíam grandes de um lado e comidos do outro.
 */
function p3PorRaio(geo, faixas, soAlto) {
  const THREE = window.THREE;
  const g = geo.index ? geo.toNonIndexed() : geo;
  g.computeBoundingBox();
  const pos = g.attributes.position;
  const nor = g.attributes.normal;

  const limite = soAlto
    ? g.boundingBox.min.y + (g.boundingBox.max.y - g.boundingBox.min.y) * 0.35
    : -Infinity;
  const nuvem = [];
  for (let i = 0; i < pos.count; i++) {
    if (pos.getY(i) < limite) continue;
    nuvem.push(pos.getX(i), pos.getY(i), pos.getZ(i));
  }
  if (!nuvem.length) for (let i = 0; i < pos.count; i++) nuvem.push(pos.getX(i), pos.getY(i), pos.getZ(i));

  const centro = [0, 0, 0];
  for (let i = 0; i < nuvem.length; i += 3) {
    centro[0] += nuvem[i]; centro[1] += nuvem[i + 1]; centro[2] += nuvem[i + 2];
  }
  const qtd = nuvem.length / 3;
  centro[0] /= qtd; centro[1] /= qtd; centro[2] /= qtd;

  const N = p3NormalDaNuvem(nuvem, centro);
  const raioDe = (x, y, z) => {
    const d = [x - centro[0], y - centro[1], z - centro[2]];
    const proj = d[0] * N[0] + d[1] * N[1] + d[2] * N[2];
    const a = d[0] - proj * N[0]; const b = d[1] - proj * N[1]; const c = d[2] - proj * N[2];
    return Math.sqrt(a * a + b * b + c * c);
  };

  let escala = 0;
  for (let i = 0; i < nuvem.length; i += 3) {
    const r = raioDe(nuvem[i], nuvem[i + 1], nuvem[i + 2]);
    if (r > escala) escala = r;
  }
  escala = escala || 1;

  const dados = { branco: [], coral: [] };
  for (let t = 0; t + 2 < pos.count; t += 3) {
    let d = 0;
    for (let k = 0; k < 3; k++) d += raioDe(pos.getX(t + k), pos.getY(t + k), pos.getZ(t + k)) / 3;
    const f = d / escala;
    let coral = false;
    for (let k = 0; k < faixas.length; k++) {
      if (f >= faixas[k][0] && f <= faixas[k][1]) { coral = true; break; }
    }
    const alvo = coral ? dados.coral : dados.branco;
    for (let k = 0; k < 3; k++) {
      alvo.push(pos.getX(t + k), pos.getY(t + k), pos.getZ(t + k));
      alvo.push(nor ? nor.getX(t + k) : 0, nor ? nor.getY(t + k) : 1, nor ? nor.getZ(t + k) : 0);
    }
  }
  const montar = (arr) => {
    if (!arr.length) return null;
    const p = new Float32Array(arr.length / 2);
    const n = new Float32Array(arr.length / 2);
    for (let i = 0, k = 0; i < arr.length; i += 6, k += 3) {
      p[k] = arr[i]; p[k + 1] = arr[i + 1]; p[k + 2] = arr[i + 2];
      n[k] = arr[i + 3]; n[k + 1] = arr[i + 4]; n[k + 2] = arr[i + 5];
    }
    const b = new THREE.BufferGeometry();
    b.setAttribute("position", new THREE.BufferAttribute(p, 3));
    b.setAttribute("normal", new THREE.BufferAttribute(n, 3));
    return b;
  };
  return { branco: montar(dados.branco), coral: montar(dados.coral) };
}

function p3PorAltura(geo, faixas, interiores, vidros) {
  const THREE = window.THREE;
  const g = geo.index ? geo.toNonIndexed() : geo;
  g.computeBoundingBox();
  const base = g.boundingBox.min.y;
  const alt = Math.max(1e-6, g.boundingBox.max.y - base);
  const pos = g.attributes.position;
  const nor = g.attributes.normal;
  const dados = { branco: [], coral: [], vidro: [] };

  const despejar = (destino, poli) => {
    // Leque a partir do primeiro vértice: o recorte de um triângulo por dois
    // planos paralelos é sempre convexo, então o leque não se dobra.
    for (let k = 1; k + 1 < poli.length; k++) {
      [poli[0], poli[k], poli[k + 1]].forEach((v) => {
        destino.push(v.p[0], v.p[1], v.p[2], v.n[0], v.n[1], v.n[2]);
      });
    }
  };

  for (let t = 0; t + 2 < pos.count; t += 3) {
    let restos = [[0, 1, 2].map((k) => ({
      p: [pos.getX(t + k), pos.getY(t + k), pos.getZ(t + k)],
      n: nor ? [nor.getX(t + k), nor.getY(t + k), nor.getZ(t + k)] : [0, 1, 0],
    }))];
    const aplicar = (lista, soDeDentro, destino) => {
      (lista || []).forEach((faixa) => {
        const yA = base + faixa[0] * alt;
        const yB = base + faixa[1] * alt;
        const sobra = [];
        restos.forEach((poli) => {
          const abaixo = p3RecortarY(poli, yA, false);
          if (abaixo) sobra.push(abaixo);
          const doMeioPraCima = p3RecortarY(poli, yA, true);
          if (!doMeioPraCima) return;
          const naFaixa = p3RecortarY(doMeioPraCima, yB, false);
          if (naFaixa) {
            if (!soDeDentro || p3OlhaParaDentro(naFaixa)) despejar(destino, naFaixa);
            else sobra.push(naFaixa);
          }
          const acima = p3RecortarY(doMeioPraCima, yB, true);
          if (acima) sobra.push(acima);
        });
        restos = sobra;
      });
    };
    aplicar(faixas, false, dados.coral);
    aplicar(interiores, true, dados.coral);
    aplicar(vidros, false, dados.vidro);
    restos.forEach((poli) => despejar(dados.branco, poli));
  }

  const montar = (arr) => {
    if (!arr.length) return null;
    const p = new Float32Array(arr.length / 2);
    const n = new Float32Array(arr.length / 2);
    for (let i = 0, k = 0; i < arr.length; i += 6, k += 3) {
      p[k] = arr[i]; p[k + 1] = arr[i + 1]; p[k + 2] = arr[i + 2];
      n[k] = arr[i + 3]; n[k + 1] = arr[i + 4]; n[k + 2] = arr[i + 5];
    }
    const b = new THREE.BufferGeometry();
    b.setAttribute("position", new THREE.BufferAttribute(p, 3));
    b.setAttribute("normal", new THREE.BufferAttribute(n, 3));
    return b;
  };
  return { branco: montar(dados.branco), coral: montar(dados.coral), vidro: montar(dados.vidro) };
}

/**
 * Peças que são símbolo chapado, e não objeto.
 *
 * O alvo é o único que sobrou: ele existe de frente, e as costas dele são um
 * disco liso. Girando a volta inteira ele passaria meio ciclo de perfil,
 * virando uma lâmina. Estas balançam em torno da frente em vez de dar a volta,
 * e o dedo também é contido: continua sendo volume, só não some.
 *
 * O sol morava aqui pelo mesmo motivo. Saiu quando a pergunta do acordar
 * trocou de objeto: caneca tem os quatro lados, e girar é o que a mostra.
 */
const P3_CHAPADO = { alvo: 0.5 };

/** Um renderizador só, emprestado para a caixa que estiver montada. */
const P3 = { renderer: null, cena: null, camera: null, dono: null, laco: null, alvo: null };

function p3Iniciar(externo) {
  const THREE = window.THREE;
  if (!THREE) return false;
  if (P3.renderer && !externo) return true;

  let r = externo;
  if (!r) {
    try {
      r = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    } catch (_) {
      return false; // Sem WebGL a caixa cai no desenho de traço.
    }
    r.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    r.setClearColor(0x000000, 0);
    r.domElement.style.cssText = "width:100%;height:100%;display:block;touch-action:none;cursor:grab;";
  }
  r.outputColorSpace = THREE.SRGBColorSpace;
  // O manequim usa NoToneMapping. Com ACES o branco dele fica cinza, e os dois
  // aparecem na mesma etapa — a diferença saltaria.
  r.toneMapping = THREE.NoToneMapping;
  r.shadowMap.enabled = true;
  r.shadowMap.type = THREE.PCFSoftShadowMap;

  const cena = new THREE.Scene();

  // Sem contorno, quem desenha a peça é a luz — então ela volta a ter
  // contraste, e é a mesma luz que projeta. Antes havia duas: as quatro
  // chapadas e uma quinta invisível só para lançar sombra no chão. Uma luz que
  // ilumina e projeta é o que faz a sombra cair de onde a claridade vem, e é o
  // que permite a peça sombrear a si mesma.
  cena.add(new THREE.HemisphereLight(0xffffff, 0xEBD6C9, 0.72));
  const chave = new THREE.DirectionalLight(0xffffff, 1.65);
  chave.position.set(2.6, 4.4, 3.4);
  chave.castShadow = true;
  chave.shadow.mapSize.set(2048, 2048);
  chave.shadow.radius = 3;
  chave.shadow.bias = -0.0011;
  // Peça fina (o cigarro, os ponteiros, as marcas do frasco) sombreia a si
  // mesma em faixas se a normal não empurrar o suficiente. 0.02 deixava um
  // risco no cilindro do cigarro.
  chave.shadow.normalBias = 0.05;
  const cs = chave.shadow.camera;
  cs.left = -2.4; cs.right = 2.4; cs.top = 2.4; cs.bottom = -2.4; cs.near = 0.5; cs.far = 16;
  cs.updateProjectionMatrix();
  cena.add(chave);
  const preencher = new THREE.DirectionalLight(0xffffff, 0.42);
  preencher.position.set(-4, 1.6, 2.4);
  cena.add(preencher);
  // A luz de frente ficou mais forte por causa do miolo do sol: superfície
  // chapada virada para a câmera só pegava ambiente e saía cinza, quando o
  // pedido era branco.
  const frente = new THREE.DirectionalLight(0xffffff, 0.62);
  frente.position.set(0, 0.6, 6);
  cena.add(frente);
  cena.add(new THREE.AmbientLight(0xffffff, 0.34));

  // Chão invisível que só recebe a sombra. Fica na cena e não no grupo que
  // gira, senão a sombra giraria junto e o objeto voltaria a flutuar.
  const chao = new THREE.Mesh(
    new THREE.PlaneGeometry(9, 9),
    // Cinza neutro, e não o marrom da paleta: sombra é ausência de luz, então
    // ela dessatura o que está embaixo em vez de tingir de cor.
    new THREE.ShadowMaterial({ opacity: 0.26, color: 0x8C8C8C }),
  );
  chao.rotation.x = -Math.PI / 2;
  chao.position.y = -1.44;
  chao.receiveShadow = true;
  cena.add(chao);

  const camera = new THREE.PerspectiveCamera(30, 1, 0.1, 60);
  camera.position.set(0, 0.55, 6.7);
  camera.lookAt(0, -0.16, 0);

  P3.renderer = r; P3.cena = cena; P3.camera = camera;
  return true;
}

function p3Corpo(cor) {
  return new window.THREE.MeshStandardMaterial({
    color: new window.THREE.Color(cor),
    roughness: 0.72,
    metalness: 0,
  });
}

function p3Vidro(cor, opacidade) {
  const m = new window.THREE.MeshStandardMaterial({
    color: new window.THREE.Color(cor),
    roughness: 0.14,
    metalness: 0,
    transparent: true,
    opacity: opacidade,
  });
  m.side = window.THREE.DoubleSide;
  // Vidro que escreve profundidade esconde o que está dentro dele: o líquido
  // da taça sumia e ela saía vazia. Sem escrever, e desenhado por último, a
  // parede fica na frente sem apagar o conteúdo.
  m.depthWrite = false;
  return m;
}

/**
 * Casca do contorno: cada vértice sai um tanto na direção da sua normal.
 *
 * É a mesma técnica do manequim, e pelo mesmo motivo: escalar a cópia não
 * serve porque as peças não têm a origem no próprio centro, então a cópia
 * escalada sai de lugar em vez de engrossar.
 */
function p3Engrossar(geo, distancia) {
  const copia = geo.clone();
  if (!copia.attributes.normal) copia.computeVertexNormals();
  const pos = copia.attributes.position;
  const nor = copia.attributes.normal;
  for (let i = 0; i < pos.count; i++) {
    pos.setXYZ(
      i,
      pos.getX(i) + nor.getX(i) * distancia,
      pos.getY(i) + nor.getY(i) * distancia,
      pos.getZ(i) + nor.getZ(i) * distancia,
    );
  }
  pos.needsUpdate = true;
  copia.computeVertexNormals();
  return copia;
}

/**
 * Um tubo que afina ao longo do caminho.
 *
 * `TubeGeometry` só sabe raio constante, e raio constante é mangueira: serve
 * para cano, não para vapor nem para a orelha de uma xícara, que engrossa no
 * meio e some onde encosta. Aqui os anéis gerados são reescalados em volta da
 * própria espinha, cada um pelo fator que a função devolver para a posição
 * dele no caminho.
 */
function p3TuboAfinado(THREE, curva, segmentos, raio, perfil) {
  const RADIAIS = 10;
  const g = new THREE.TubeGeometry(curva, segmentos, raio, RADIAIS, false);
  const pos = g.attributes.position;
  for (let i = 0; i <= segmentos; i++) {
    const centro = curva.getPointAt(Math.min(1, i / segmentos));
    const fator = perfil(i / segmentos);
    for (let j = 0; j <= RADIAIS; j++) {
      const k = i * (RADIAIS + 1) + j;
      pos.setXYZ(k,
        centro.x + (pos.getX(k) - centro.x) * fator,
        centro.y + (pos.getY(k) - centro.y) * fator,
        centro.z + (pos.getZ(k) - centro.z) * fator);
    }
  }
  g.computeVertexNormals();
  return g;
}

/**
 * Chapa de cantos redondos, com espessura e bisel.
 *
 * `BoxGeometry` é o caminho curto para qualquer coisa retangular, e o preço é
 * a quina viva: empilhadas, as caixas viram maquete de papelão. Uma forma
 * extrudada com bisel pega luz na borda, e é a borda iluminada que faz a peça
 * parecer objeto.
 *
 * `buracos` são furos de verdade na chapa, e não sombras pintadas: é por eles
 * que passa a argola do calendário.
 */
function p3Chapa(THREE, largura, altura, raio, espessura, bisel, buracos) {
  const s = new THREE.Shape();
  const x = -largura / 2;
  const y = -altura / 2;
  s.moveTo(x + raio, y);
  s.lineTo(x + largura - raio, y);
  s.quadraticCurveTo(x + largura, y, x + largura, y + raio);
  s.lineTo(x + largura, y + altura - raio);
  s.quadraticCurveTo(x + largura, y + altura, x + largura - raio, y + altura);
  s.lineTo(x + raio, y + altura);
  s.quadraticCurveTo(x, y + altura, x, y + altura - raio);
  s.lineTo(x, y + raio);
  s.quadraticCurveTo(x, y, x + raio, y);
  (buracos || []).forEach(([bx, by, br]) => {
    const furo = new THREE.Path();
    furo.absarc(bx, by, br, 0, Math.PI * 2, true);
    s.holes.push(furo);
  });
  return new THREE.ExtrudeGeometry(s, {
    depth: espessura, bevelEnabled: true, bevelSize: bisel,
    bevelThickness: bisel, bevelSegments: 3, curveSegments: 14,
  });
}

/**
 * As onze peças, descritas como lista de primitivas.
 *
 * Nada aqui é anatomia — são objetos do dia a dia que a pergunta menciona, e o
 * ponto é nomear o assunto enquanto a pessoa lê, não ilustrar diagnóstico.
 */
function p3Pecas(nome, THREE, mat) {
  const { corpo, corpoFundo, coral, coralFundo } = mat;
  const lista = [];
  const add = (geo, m, x, y, z, rx, ry, rz) => {
    const p = { geo, m, p: [x || 0, y || 0, z || 0], r: [rx || 0, ry || 0, rz || 0] };
    lista.push(p);
    return p;
  };

  if (nome === "tratamento") {
    // Prancheta de acompanhamento clínico.
    add(new THREE.BoxGeometry(1.4, 1.8, 0.1), corpoFundo, 0, -0.06, 0);
    add(new THREE.BoxGeometry(1.2, 1.48, 0.05), corpo, 0, -0.16, 0.07);
    add(new THREE.BoxGeometry(0.58, 0.22, 0.2), coral, 0, 0.9, 0.03);
    add(new THREE.TorusGeometry(0.11, 0.04, 10, 22, Math.PI), coralFundo, 0, 1.02, 0.03);
    [0.4, 0.14, -0.12].forEach((y, i) => {
      add(new THREE.BoxGeometry(i === 2 ? 0.5 : 0.8, 0.06, 0.03),
        corpoFundo, i === 2 ? -0.15 : 0, y, 0.1);
    });
    add(new THREE.CylinderGeometry(0.28, 0.28, 0.09, 30), coral, 0.28, -0.56, 0.12, Math.PI / 2);
    add(new THREE.BoxGeometry(0.28, 0.09, 0.05), corpo, 0.28, -0.56, 0.17);
    add(new THREE.BoxGeometry(0.09, 0.28, 0.05), corpo, 0.28, -0.56, 0.17);
  } else if (nome === "medicamento") {
    // Cápsula: um corpo só, com a tampa colorida deslizando por cima da
    // metade de cima — que é como a de verdade é montada. Antes eram duas
    // cápsulas inteiras encostadas uma na outra, e de qualquer ângulo aquilo
    // lia como dois caroços, não como um comprimido.
    const R = 0.38;          // raio do corpo
    const L = 0.78;          // trecho reto, sem as calotas
    const inc = 0.42;        // inclinação na caixa
    const cx = -0.14; const cy = 0.3;
    // Ponto a `d` do centro, ao longo do eixo já inclinado da cápsula.
    const eixo = (d) => [cx - Math.sin(inc) * d, cy + Math.cos(inc) * d];

    add(new THREE.CapsuleGeometry(R, L, 14, 36), corpo, cx, cy, 0, 0, 0, inc);

    // A tampa é um fio mais larga, senão ela e o corpo brigam pelo mesmo
    // pixel e a superfície fica manchada onde as duas se encostam.
    const Rt = R * 1.05;
    const meio = eixo(L / 4);
    add(new THREE.CylinderGeometry(Rt, Rt, L / 2, 36), coral, meio[0], meio[1], 0, 0, 0, inc);
    const topo = eixo(L / 2);
    add(new THREE.SphereGeometry(Rt, 36, 24, 0, Math.PI * 2, 0, Math.PI / 2),
      coral, topo[0], topo[1], 0, 0, 0, inc);

    // O comprimido, com o sulco de partir.
    add(new THREE.CylinderGeometry(0.5, 0.5, 0.22, 36), corpo, 0.52, -0.58, 0.14, Math.PI / 2.2, 0, 0.2);
    add(new THREE.BoxGeometry(0.045, 0.24, 0.84), coralFundo, 0.52, -0.54, 0.16, Math.PI / 2.2, 0, 0.2);
  } else if (nome === "suplemento") {
    // Pote com tampa e rótulo.
    add(new THREE.CylinderGeometry(0.58, 0.58, 1.3, 42), corpo, 0, -0.2, 0);
    add(new THREE.CylinderGeometry(0.586, 0.586, 0.54, 42), coral, 0, -0.14, 0);
    add(new THREE.CylinderGeometry(0.62, 0.62, 0.34, 42), coralFundo, 0, 0.62, 0);
    add(new THREE.CylinderGeometry(0.565, 0.565, 0.12, 42), corpoFundo, 0, 0.44, 0);
    add(new THREE.BoxGeometry(0.44, 0.055, 0.04), corpo, 0, -0.04, 0.59);
    add(new THREE.BoxGeometry(0.28, 0.055, 0.04), corpo, -0.08, -0.22, 0.59);
  } else if (nome === "atividade") {
    // Halteres. A primeira tentativa foi uma figura correndo montada de
    // cápsulas: a 90px de largura, que é o tamanho real da caixa, virava
    // cápsula solta e ninguém reconhecia. Peso se lê de longe.
    add(new THREE.CylinderGeometry(0.15, 0.15, 1.44, 28), corpoFundo, 0, 0, 0, 0, 0, Math.PI / 2);
    [-1, 1].forEach((lado) => {
      add(new THREE.CylinderGeometry(0.5, 0.5, 0.36, 38), coral, lado * 0.58, 0, 0, 0, 0, Math.PI / 2);
      add(new THREE.CylinderGeometry(0.4, 0.4, 0.32, 38), coralFundo, lado * 0.86, 0, 0, 0, 0, Math.PI / 2);
      add(new THREE.CylinderGeometry(0.22, 0.22, 0.13, 26), corpo, lado * 1.04, 0, 0, 0, 0, Math.PI / 2);
    });
    [-0.19, 0, 0.19].forEach((x) => {
      add(new THREE.TorusGeometry(0.168, 0.032, 12, 28), corpo, x, 0, 0, 0, Math.PI / 2);
    });
  } else if (nome === "sono") {
    // Cama de dormir. O estrado tinha dois pés escondidos atrás do
    // colchão; agora são quatro, nos cantos, e aparecem.
    // A cabeceira antes cruzava o estrado e aparecia uma lasca dela por baixo
    // do colchão; agora ela nasce no estrado e sobe.
    add(new THREE.BoxGeometry(1.84, 0.22, 1.02), corpoFundo, 0.1, -0.5, 0);   // estrado
    add(new THREE.BoxGeometry(1.8, 0.34, 0.96), corpo, 0.1, -0.22, 0);        // colchão
    add(new THREE.BoxGeometry(1.82, 0.1, 0.98), coral, 0.1, -0.37, 0);        // debrum
    // Coberta dobrada no pé da cama: sem ela o colchão era um bloco liso e a
    // cama não se distinguia de um banco.
    add(new THREE.BoxGeometry(0.66, 0.12, 0.98), coral, 0.68, -0.01, 0);
    add(new THREE.BoxGeometry(0.62, 0.09, 0.94), corpoFundo, 0.68, 0.07, 0);
    // Travesseiro com vinco no meio.
    add(new THREE.BoxGeometry(0.58, 0.26, 0.62), corpo, -0.5, 0.09, 0);
    add(new THREE.BoxGeometry(0.05, 0.2, 0.56), corpoFundo, -0.5, 0.11, 0);
    add(new THREE.BoxGeometry(0.2, 1.06, 1.02), corpoFundo, -0.92, -0.08, 0); // cabeceira
    add(new THREE.BoxGeometry(0.16, 0.56, 1.02), corpoFundo, 1.06, -0.42, 0); // pé da cama
    // Pernas curtas e grossas. Compridas e finas, a cama virava mesa alta.
    [[-0.86, 0.41], [-0.86, -0.41], [0.99, 0.41], [0.99, -0.41]].forEach((c) => {
      add(new THREE.CylinderGeometry(0.12, 0.105, 0.3, 16), corpoFundo, c[0], -0.74, c[1]);
    });
  } else if (nome === "disturbio") {
    // Despertador da madrugada.
    // Caixa coral e mostrador branco: o aro fino de antes era um tubo de
    // 0.055 que sumia contra o corpo da mesma cor.
    add(new THREE.CylinderGeometry(0.84, 0.84, 0.42, 48), coral, 0, 0, 0, Math.PI / 2);
    add(new THREE.CylinderGeometry(0.7, 0.7, 0.46, 48), corpo, 0, 0, 0.03, Math.PI / 2);
    add(new THREE.CylinderGeometry(0.6, 0.6, 0.1, 48), corpoFundo, 0, 0, 0.25, Math.PI / 2);
    add(new THREE.BoxGeometry(0.08, 0.44, 0.06), coralFundo, 0, 0.2, 0.32);
    add(new THREE.BoxGeometry(0.32, 0.08, 0.06), coralFundo, 0.14, 0, 0.32);
    add(new THREE.CylinderGeometry(0.07, 0.07, 0.08, 16), coralFundo, 0, 0, 0.34, Math.PI / 2);
    // As campainhas ficavam boiando ao lado da caixa. Agora têm haste, e o
    // martelo entre elas fecha o desenho do despertador.
    // Campainhas com haste, e entre elas o martelo.
    [-1, 1].forEach((lado) => {
      add(new THREE.CylinderGeometry(0.09, 0.09, 0.3, 16), corpoFundo,
        lado * 0.46, 0.6, 0, 0, 0, lado * -0.62);
      add(new THREE.SphereGeometry(0.29, 28, 22), coral, lado * 0.62, 0.76, 0);
    });
    add(new THREE.CylinderGeometry(0.06, 0.06, 0.26, 14), corpoFundo, 0, 0.88, 0);
    add(new THREE.SphereGeometry(0.14, 20, 16), coralFundo, 0, 1.04, 0);
    // Pés: haste inclinada e uma sapata deitada, que é o que encosta no chão.
    // Antes eram dois tubos finos boiando embaixo da caixa.
    [-1, 1].forEach((lado) => {
      add(new THREE.CylinderGeometry(0.11, 0.11, 0.36, 16), corpoFundo,
        lado * 0.52, -0.82, 0, 0, 0, lado * -0.46);
      add(new THREE.BoxGeometry(0.38, 0.14, 0.3), corpo, lado * 0.66, -1.0, 0, 0, 0, lado * -0.1);
    });
  } else if (nome === "acordar") {
    // Xícara de café, no pires.
    //
    // Era um sol, e sol responde "de manhã", que a pergunta já diz. O que ela
    // pergunta é como a pessoa levanta — se acorda inteira ou arrastada —, e
    // disso um sol no céu não fala. A xícara fala: é o primeiro gesto de quem
    // levantou, e vem com vapor, que é calor e é agora. Fecha a sequência das
    // três perguntas de sono da etapa: cama, despertador, xícara.
    //
    // O modelo é a xícara empilhável de cafeteria, e a forma dela tem regra:
    // parede quase reta, um degrau discreto perto do pé (é nele que uma encaixa
    // na outra) e base chata. As tentativas anteriores erraram por aí — uma
    // saiu cônica, com pé de taça, e virou vaso; outra saiu como tigela.
    //
    // A peça é uma superfície só, torneada, e não uma pilha de cilindros. O que
    // denunciava a pilha era a emenda: parede reta encontrando outra parede
    // reta num degrau, coisa que louça nenhuma tem. Um perfil desenhado como
    // curva e girado em volta do eixo não tem emenda: pé, parede, borda
    // enrolada e parede de dentro são o mesmo traço.
    //
    // Louça branca e café coral, e não o contrário: a cor forte fica na bebida,
    // que é o assunto, e a xícara é só o que a segura.
    const V2 = (x, y) => new THREE.Vector2(x, y);

    // Pires: prato raso, com beirada erguida e o anel onde a xícara assenta.
    add(new THREE.LatheGeometry(new THREE.SplineCurve([
      V2(0, -0.74), V2(0.4, -0.74), V2(0.8, -0.735), V2(1.1, -0.718),
      V2(1.22, -0.69), V2(1.245, -0.664),
      V2(1.195, -0.655), V2(1.06, -0.676), V2(0.78, -0.69),
      V2(0.62, -0.686), V2(0.56, -0.7), V2(0.5, -0.7), V2(0.3, -0.702), V2(0, -0.704),
    ]).getPoints(80), 64), corpo);

    // Corpo: base chata, degrau de empilhar, parede reta até a borda, e a
    // volta pela parede de dentro. É a parede de dentro que faz parecer louça
    // em vez de bloco maciço.
    add(new THREE.LatheGeometry(new THREE.SplineCurve([
      V2(0, -0.66), V2(0.4, -0.66), V2(0.465, -0.64), V2(0.48, -0.59),
      V2(0.487, -0.545), V2(0.52, -0.52),
      V2(0.6, -0.505), V2(0.618, -0.47),
      V2(0.63, -0.24), V2(0.638, 0.03), V2(0.643, 0.23), V2(0.645, 0.288),
      V2(0.63, 0.3), V2(0.608, 0.286),
      V2(0.6, 0.03), V2(0.592, -0.24), V2(0.578, -0.47),
      V2(0.5, -0.54), V2(0.34, -0.585), V2(0, -0.6),
    ]).getPoints(96), 64), corpo);

    // O café: um disco logo abaixo da borda. Mais fundo, sobrava parede branca
    // demais e a xícara parecia vazia.
    add(new THREE.CircleGeometry(0.6, 64), coral, 0, 0.225, 0, -Math.PI / 2);

    // A asa é um laço fechado: as duas pontas param em raio 0,56, menor que os
    // 0,63 da parede naquela altura, então entram na louça em vez de ficar
    // boiando. Antes ela era meia volta de torus e a ponta de baixo sobrava
    // solta no ar, apontando para fora da xícara.
    add(p3TuboAfinado(THREE, new THREE.CatmullRomCurve3([
      new THREE.Vector3(0.56, 0.17, 0), new THREE.Vector3(0.84, 0.15, 0),
      new THREE.Vector3(0.95, 0.02, 0), new THREE.Vector3(0.92, -0.13, 0),
      new THREE.Vector3(0.76, -0.21, 0), new THREE.Vector3(0.56, -0.215, 0),
    ]), 64, 0.08, (t) => 0.85 + 0.15 * Math.sin(Math.PI * t)), corpo);

    // Três fios de vapor, que nascem no café e somem no ar. Alturas e fases
    // diferentes — iguais viravam pente, e um só se lia como haste.
    [[-0.18, 0, 0.78], [0.03, 1.2, 1], [0.21, 2.3, 0.7]].forEach(([dx, fase, alt]) => {
      const pontos = [0, 1, 2, 3, 4, 5].map((i) => new THREE.Vector3(
        dx + Math.sin(i * 1 + fase) * 0.11,
        0.245 + (i / 5) * alt,
        Math.cos(i * 0.8 + fase) * 0.05,
      ));
      add(p3TuboAfinado(THREE, new THREE.CatmullRomCurve3(pontos), 70, 0.052,
        (t) => Math.max(0.06, Math.sin(Math.PI * (0.12 + t * 0.88)) * (1 - t * 0.55))), corpo);
    });
  } else if (nome === "calendario") {
    // Calendário de mesa, para "se não tratar, como estará em 12 meses?".
    //
    // Antes era uma ampulheta. Ampulheta diz "tempo passando", que a pergunta
    // já diz — e diz só isso. Calendário diz o prazo de que a pergunta fala,
    // que é outra coisa: doze meses, contados, e não areia escorrendo.
    //
    // Cheguei a desenhar a alternativa óbvia, um gráfico de barras caindo com
    // seta. Lia na hora, e por isso mesmo não serve: a pergunta pede que o
    // paciente imagine como estará, e um gráfico descendo responde por ele,
    // de antemão e para baixo. A ilustração marca o assunto; quem prevê é
    // quem responde.
    //
    // É um cavalete: duas folhas encostadas no alto, presas por uma espiral de
    // arame. As duas primeiras tentativas foram cartões de frente — e cartão
    // de frente é ícone, não objeto: de qualquer ângulo que a peça girasse ela
    // continuava sendo uma placa. Duas folhas em ângulo têm silhueta própria, e
    // a espiral por cima é o que se reconhece como calendário de mesa antes de
    // ler qualquer coisa nele.
    //
    // A décima segunda marca vai de coral: é o mês que a pergunta cita.
    const INC = 0.3;                  // o quanto cada folha se abre da vertical
    const ALT = 1.32;
    const LARG = 1.5;
    const CRISTA = 0.66;              // onde as duas folhas se encontram

    /**
     * Um ponto na folha da frente: `u` para o lado, `desce` a partir da
     * crista, `sai` para fora da folha. Sem isto cada peça da grade precisaria
     * do seno e do cosseno escritos na mão.
     */
    const naFolha = (u, desce, sai) => [
      u,
      CRISTA - desce * Math.cos(INC) + sai * Math.sin(INC),
      desce * Math.sin(INC) + sai * Math.cos(INC),
    ];

    // Folha de trás, que é o que sustenta o cavalete.
    add(p3Chapa(THREE, LARG, ALT, 0.07, 0.045, 0.018), corpoFundo,
      0, CRISTA - (ALT / 2) * Math.cos(INC), -((ALT / 2) * Math.sin(INC)), INC);
    // Folha da frente.
    add(p3Chapa(THREE, LARG, ALT, 0.07, 0.05, 0.02), corpo, ...naFolha(0, ALT / 2, 0), -INC);
    // Faixa do mês.
    add(p3Chapa(THREE, LARG * 0.86, 0.26, 0.05, 0.035, 0.014), coral, ...naFolha(0, 0.26, 0.055), -INC);

    // Doze marcas em relevo, quatro por linha.
    let marca = 0;
    for (let linha = 0; linha < 3; linha += 1) {
      for (let col = 0; col < 4; col += 1) {
        marca += 1;
        add(p3Chapa(THREE, 0.235, 0.15, 0.045, 0.03, 0.012), marca === 12 ? coral : corpoFundo,
          ...naFolha(-0.44 + col * 0.295, 0.6 + linha * 0.245, 0.05), -INC);
      }
    }

    // A espiral de arame, ao longo da crista. Começa e termina apontando para
    // baixo, entre as duas folhas: começando de frente, sobrava uma ponta de
    // arame no ar na beirada, que é o tipo de detalhe que denuncia o desenho.
    const VOLTAS = 11;
    const PASSOS = VOLTAS * 22;
    const arame = [];
    for (let i = 0; i <= PASSOS; i += 1) {
      const t = i / PASSOS;
      const ang = Math.PI + t * VOLTAS * Math.PI * 2;
      arame.push(new THREE.Vector3(
        -LARG * 0.44 + t * LARG * 0.88,
        CRISTA + Math.cos(ang) * 0.082,
        Math.sin(ang) * 0.082,
      ));
    }
    add(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(arame), PASSOS * 2, 0.021, 8, false), corpoFundo);
  } else if (nome === "alvo") {
    // Alvo, desenhado aqui.
    //
    // O modelo gerado trazia os sulcos em espiral, e faixa circular sobre
    // superfície espiralada lê como redemoinho por mais exata que a conta
    // seja. Empilhando cilindros de raios decrescentes, os anéis são redondos
    // por construção — não há o que sair torto.
    //
    // Cada anel vem um pouco à frente do anterior. Coplanares, as faces
    // brigariam pelo mesmo pixel; escalonados, o alvo ganha o relevo de um
    // alvo de verdade e a luz encontra os degraus.
    const ANEIS = [
      [1.0, corpo], [0.82, coral], [0.64, corpo],
      [0.46, coral], [0.3, corpo], [0.15, coral],
    ];
    ANEIS.forEach(([r, m], i) => {
      add(new THREE.CylinderGeometry(r, r, 0.16, 56), m, 0, 0.34, i * 0.022, Math.PI / 2);
    });

    // Cavalete: só as duas pernas de trás. A terceira, na frente, cruzava o
    // disco na altura dos anéis e cortava o alvo ao meio.
    [-1, 1].forEach((lado) => {
      add(new THREE.CylinderGeometry(0.055, 0.045, 1.55, 14), corpoFundo,
        lado * 0.44, -0.44, -0.28, 0.3, 0, lado * 0.5);
    });
    add(new THREE.BoxGeometry(1.0, 0.05, 0.05), corpoFundo, 0, -0.92, -0.32);

    // Flecha na mosca: haste, ponta enterrada e três penas.
    add(new THREE.CylinderGeometry(0.035, 0.035, 0.9, 14), corpo, 0, 0.34, 0.58, Math.PI / 2);
    add(new THREE.ConeGeometry(0.06, 0.16, 14), coralFundo, 0, 0.34, 0.2, -Math.PI / 2);
    [0, 2.09, 4.19].forEach((a) => {
      add(new THREE.BoxGeometry(0.02, 0.17, 0.24), coral,
        Math.sin(a) * 0.05, 0.34 + Math.cos(a) * 0.05, 0.92, 0, 0, a);
    });
  } else if (nome === "colchao") {
    // Cama, com o colchão sendo o assunto.
    //
    // Era um colchão solto no ar, um bloco com bolinhas em cima — de longe,
    // uma caixa. A pergunta é sobre o colchão, então ele continua o herói:
    // fica em coral, deslocado do estrado, com o capitonê aparecendo de lado.
    // A cama em volta é o que diz que aquilo é um colchão e não um tijolo.
    add(new THREE.BoxGeometry(2.0, 0.22, 1.08), corpoFundo, 0, -0.44, 0);      // estrado
    add(new THREE.BoxGeometry(0.18, 0.94, 1.08), corpoFundo, -1.0, -0.14, 0);  // cabeceira
    add(new THREE.BoxGeometry(0.15, 0.48, 1.08), corpoFundo, 1.07, -0.42, 0);  // pé da cama
    // Pernas curtas e grossas, pelo mesmo motivo da outra cama.
    [[-0.9, 0.44], [-0.9, -0.44], [1.0, 0.44], [1.0, -0.44]].forEach((c) => {
      add(new THREE.CylinderGeometry(0.12, 0.105, 0.3, 16), corpoFundo, c[0], -0.7, c[1]);
    });
    add(new THREE.BoxGeometry(1.94, 0.42, 1.02), coral, 0.02, -0.11, 0);       // colchão
    add(new THREE.BoxGeometry(1.96, 0.09, 1.04), corpo, 0.02, 0.09, 0);        // capa de cima
    // Os botões do capitonê, na face que se vê.
    for (let ix = -1; ix <= 1; ix++) {
      add(new THREE.SphereGeometry(0.065, 16, 12), coralFundo, 0.02 + ix * 0.58, 0.12, 0.3);
      add(new THREE.SphereGeometry(0.065, 16, 12), coralFundo, 0.02 + ix * 0.58, 0.12, -0.3);
    }
  } else if (nome === "fumo") {
    // Cigarro e a fumaça.
    // O corpo termina onde o filtro começa, e não por baixo dele.
    //
    // Antes o cilindro branco atravessava o filtro inteiro e as duas tampas
    // planas caíam no mesmo ponto: dois discos na mesma profundidade brigando
    // pelo mesmo pixel, que é o leque de triângulos piscando que aparecia na
    // ponta. Encostados topo a topo, cada um tem o seu lugar.
    add(new THREE.CylinderGeometry(0.15, 0.15, 1.04, 30), corpo, 0.14, -0.58, 0, 0, 0, Math.PI / 2);
    add(new THREE.CylinderGeometry(0.156, 0.156, 0.48, 30), coralFundo, -0.62, -0.58, 0, 0, 0, Math.PI / 2);
    add(new THREE.CylinderGeometry(0.146, 0.146, 0.13, 30), coral, 0.72, -0.58, 0, 0, 0, Math.PI / 2);
    // A fumaça é o trajeto, não uma pilha de argolas.
    //
    // Eram três rosquinhas empilhadas no ar: parada, uma argola não conta que
    // algo está subindo. Dois fios que saem da brasa e sobem serpenteando
    // dizem o caminho, e giram bem porque a serpentina sai do plano.
    // Um fio só, que afina enquanto sobe.
    //
    // Eram dois, e cruzavam: de longe pareciam dois arames torcidos, não
    // fumaça. Um fio que sai da brasa, serpenteia e vai sumindo lê como
    // trajeto — e a espessura decrescente é o que faz o olho ler subida.
    const raio = (t) => 0.055 * (1 - t * 0.72);
    const pts = [];
    for (let i = 0; i <= 8; i++) {
      const t = i / 8;
      pts.push(new THREE.Vector3(
        0.78 + Math.sin(t * 4.2) * 0.26 * t,
        -0.44 + t * 2.0,
        Math.cos(t * 3.4) * 0.2 * t,
      ));
    }
    const curva = new THREE.CatmullRomCurve3(pts);
    const tubo = new THREE.TubeGeometry(curva, 60, 1, 10, false);
    // O TubeGeometry só faz raio constante; aqui cada anel é encolhido no
    // lugar, pela altura em que caiu.
    const pos = tubo.attributes.position;
    const eixo = new THREE.Vector3();
    for (let i = 0; i < pos.count; i++) {
      const t = Math.min(1, Math.max(0, (pos.getY(i) + 0.44) / 2.0));
      curva.getPointAt(t, eixo);
      pos.setXYZ(i,
        eixo.x + (pos.getX(i) - eixo.x) * raio(t),
        eixo.y + (pos.getY(i) - eixo.y) * raio(t),
        eixo.z + (pos.getZ(i) - eixo.z) * raio(t));
    }
    pos.needsUpdate = true;
    tubo.computeVertexNormals();
    // Fumaça não projeta: o fio é fino e a sombra dele caía riscando o
    // cigarro, como se alguém tivesse arranhado o modelo.
    add(tubo, corpoFundo, 0, 0, 0).semSombra = true;
  } else if (nome === "alcool") {
    // Taça — perfil girado, que é o jeito honesto de fazer vidro.
    const perfil = [
      [0.001, 0], [0.48, 0], [0.48, 0.07], [0.075, 0.11], [0.075, 0.72],
      [0.14, 0.79], [0.37, 0.93], [0.51, 1.17], [0.55, 1.5], [0.555, 1.52],
    ].map((p) => new THREE.Vector2(p[0], p[1] - 0.74));
    add(new THREE.LatheGeometry(perfil, 50), mat.vidro, 0, 0, 0).ordem = 2;
    const liquido = [
      [0.001, 0], [0.33, 0.17], [0.46, 0.36], [0.465, 0.37],
    ].map((p) => new THREE.Vector2(p[0], p[1] + 0.06));
    add(new THREE.LatheGeometry(liquido, 50), mat.liquido, 0, 0, 0).ordem = 1;
  } else if (nome === "frasco") {
    // Erlenmeyer. O anterior era um cone com um tubo em cima e um anel solto:
    // sem ombro nem gargalo, lia como pesinho de balança.
    const perfil = [
      [0.001, 0], [0.66, 0], [0.68, 0.05], [0.66, 0.1],   // pé com aba
      [0.3, 0.72], [0.19, 0.9],                            // ombro que fecha
      [0.19, 1.24], [0.235, 1.3], [0.235, 1.36],           // gargalo com bocal
    ].map((q) => new THREE.Vector2(q[0], q[1] - 0.68));
    add(new THREE.LatheGeometry(perfil, 52), mat.vidro, 0, 0, 0).ordem = 2;
    const liquido = [
      [0.001, 0], [0.6, 0], [0.6, 0.04], [0.37, 0.42], [0.375, 0.43],
    ].map((q) => new THREE.Vector2(q[0], q[1] - 0.66));
    add(new THREE.LatheGeometry(liquido, 52), mat.liquido, 0, 0, 0).ordem = 1;
    // Rolha, que é o que diz que é frasco e não taça.
    add(new THREE.CylinderGeometry(0.2, 0.17, 0.26, 26), coralFundo, 0, 0.78, 0);
    add(new THREE.CylinderGeometry(0.215, 0.215, 0.07, 26), coral, 0, 0.88, 0);
    // Duas marcas de volume, na parede.
    [0.0, -0.16].forEach((y) => {
      add(new THREE.BoxGeometry(0.2, 0.035, 0.03), corpoFundo, 0.3, y, 0.42, 0, 0.72, 0);
    });
  } else {
    return null;
  }

  return lista;
}

/** Contorno, sombra e escala — o acabamento que vale para as onze. */
function p3Acabar(g, nome, contorno) {
  const THREE = window.THREE;
  const caixa = new THREE.Box3().setFromObject(g);
  const tam = caixa.getSize(new THREE.Vector3());
  const maior = Math.max(tam.x, tam.y, tam.z);
  const grossura = maior * 0.0105;

  // Sem contorno, a peça precisa receber a própria sombra: é ela que separa o
  // braço da cadeira do encosto, e a tampa do pote do corpo.
  g.children.forEach((m) => { m.receiveShadow = true; });

  // O contorno geral está desligado, mas uma peça pode pedir o seu — e numa
  // cor própria. É o caso do cérebro: sem faixa na superfície, o coral dele
  // mora na borda.
  const pedido = (P3_MALHA[nome] || {}).contorno;
  // A casca pode vir translúcida. No cérebro ela entra em cada sulco, e opaca
  // desenhava o giro inteiro com a mesma força da silhueta — muita linha para
  // uma peça de 200px. Rebaixada, ela pinta a borda e deixa a dobra insinuada.
  const opacidade = (P3_MALHA[nome] || {}).contornoOpacidade;
  const tinta = pedido
    ? new THREE.MeshBasicMaterial({
      color: new THREE.Color(P3_TINTA[pedido] || P3_TINTA.contorno),
      side: THREE.BackSide,
      transparent: opacidade !== undefined && opacidade < 1,
      opacity: opacidade === undefined ? 1 : opacidade,
      // Sem escrever profundidade, senão as próprias faces da casca disputam
      // o pixel entre si e a translucidez sai manchada.
      depthWrite: opacidade === undefined || opacidade >= 1,
    })
    : contorno;
  if (!P3_CONTORNO && !pedido) return p3Encaixar(g, nome, caixa, maior);

  const dim = new THREE.Vector3();
  g.children.slice().forEach((m) => {
    if (m.userData.semContorno) return;
    // A grossura também não passa de um sexto da menor dimensão da peça.
    // Sem esse teto, o aro do despertador (tubo de 0.055) levava um contorno
    // de 0.021 — quase quarenta por cento — e a casca escura comia o aro por
    // dentro, deixando um pontilhado no lugar do metal.
    m.geometry.computeBoundingBox();
    m.geometry.boundingBox.getSize(dim);
    const menor = Math.max(0.02, Math.min(dim.x, dim.y, dim.z));
    const casca = new THREE.Mesh(p3Engrossar(m.geometry, Math.min(grossura, menor / 6)), tinta);
    casca.position.copy(m.position);
    casca.rotation.copy(m.rotation);
    // A casca não projeta: somada ao corpo, a sombra saía engordada.
    casca.castShadow = false;
    g.add(casca);
  });

  return p3Encaixar(g, nome, caixa, maior);
}

/** Centra a peça e a encaixa na medida da caixa. */
function p3Encaixar(g, nome, caixa, maior) {
  const THREE = window.THREE;
  g.position.sub(caixa.getCenter(new THREE.Vector3()));
  const envolve = new THREE.Group();
  envolve.add(g);
  envolve.scale.setScalar((2.35 * (P3_ESCALA[nome] || 1)) / maior);
  return envolve;
}

/** Monta a peça: corpo, contorno por fora, e a escala da caixa. */
async function p3Construir(nome) {
  const THREE = window.THREE;
  const mat = {
    corpo: p3Corpo(P3_TINTA.corpo),
    corpoFundo: p3Corpo(P3_TINTA.corpoFundo),
    coral: p3Corpo(P3_TINTA.coral),
    coralFundo: p3Corpo(P3_TINTA.coralFundo),
    vidro: p3Vidro(0xFFFDFB, 0.4),
    liquido: p3Vidro(P3_TINTA.coral, 0.92),
  };

  const contorno = P3_CONTORNO
    ? new THREE.MeshBasicMaterial({ color: new THREE.Color(P3_TINTA.contorno), side: THREE.BackSide })
    : null;

  // As quatro que vêm de arquivo entram por aqui.
  const pronta = P3_MALHA[nome];
  if (pronta) {
    const gltf = await p3Carregar(pronta.url);
    const g = new THREE.Group();
    gltf.scene.traverse((o) => {
      if (!o.isMesh) return;
      o.updateWorldMatrix(true, false);
      let base = o.geometry;
      if (pronta.achatar) {
        const [eixo, fator] = pronta.achatar;
        base = base.clone();
        base.scale(eixo === "x" ? fator : 1, eixo === "y" ? fator : 1, eixo === "z" ? fator : 1);
        base.computeVertexNormals();
      }
      const partes = pronta.raio
        ? p3PorRaio(base, pronta.raio, pronta.raioAlto)
        : p3PorAltura(base, pronta.faixas, pronta.interiores, pronta.vidro);
      [["branco", mat.corpo], ["coral", mat.coral]].forEach(([chave, material]) => {
        if (!partes[chave]) return;
        const m = new THREE.Mesh(partes[chave], material);
        m.applyMatrix4(o.matrixWorld);
        m.castShadow = true;
        g.add(m);
      });

      // O vidro vai em duas passagens: primeiro as faces de trás, depois as da
      // frente.
      //
      // Numa malha só, com as duas paredes misturadas, os triângulos saem na
      // ordem do arquivo — e como vidro não escreve profundidade, a parede de
      // trás às vezes é pintada por cima da da frente. A peça fica então de
      // vidro de um lado e sólida do outro, mudando conforme ela gira.
      // Separadas em duas passagens, o de trás sempre fica atrás.
      if (partes.vidro) {
        // A parede da frente é mais opaca que a de trás.
        //
        // Com as duas na mesma transparência, a da frente somia e o olho lia a
        // peça como aberta: via-se a superfície de dentro do outro lado e nada
        // dizia que havia vidro por perto. A malha é fechada — contei as
        // arestas, 12.300, todas usadas duas vezes, nenhuma de borda —, então
        // o buraco era de leitura, não de geometria. Vidro de verdade se
        // enxerga assim: a parede próxima marca presença e a de longe fica um
        // fantasma atrás dela.
        [[THREE.BackSide, 2, 0.2], [THREE.FrontSide, 3, 0.56]].forEach(([lado, ordem, opacidade]) => {
          const mv = mat.vidro.clone();
          mv.side = lado;
          mv.opacity = opacidade;
          const m = new THREE.Mesh(partes.vidro, mv);
          m.applyMatrix4(o.matrixWorld);
          // Vidro não projeta: a sombra de um bojo transparente sai sólida e a
          // peça ganha no chão uma mancha que ela não faria.
          m.castShadow = false;
          m.renderOrder = ordem;
          g.add(m);
        });
      }
    });
    if (pronta.giro) g.rotation.y = pronta.giro;
    if (pronta.inclinar) g.rotation.x = pronta.inclinar;
    return p3Acabar(g, nome, contorno);
  }

  const pecas = p3Pecas(nome, THREE, mat);
  if (!pecas) return null;

  const g = new THREE.Group();
  pecas.forEach((p) => {
    const m = new THREE.Mesh(p.geo, p.m);
    m.position.set(p.p[0], p.p[1], p.p[2]);
    m.rotation.set(p.r[0], p.r[1], p.r[2]);
    m.castShadow = !p.semSombra;
    if (p.ordem) { m.renderOrder = p.ordem; m.userData.semContorno = true; }
    else m.userData.semContorno = !!p.semContorno;
    g.add(m);
  });

  return p3Acabar(g, nome, contorno);
}

/** A cena pronta, para quem quiser desenhar as peças fora da caixa. */
function p3MontarCena(renderer) {
  p3Iniciar(renderer);
  return P3.cena;
}

/**
 * A caixa. Empresta o renderizador compartilhado enquanto estiver na tela.
 *
 * Gira sozinha devagar e aceita o dedo. Quem pediu menos movimento no sistema
 * fica com a peça parada, numa pose de três quartos — o objeto continua sendo
 * volume, só não roda sozinho.
 */
function CaixaProp3D({ nome, aoFalhar }) {
  const ref = useRefP3(null);

  useEffectP3(() => {
    let vivo = true;
    let solto = null;

    const montar = async () => {
      if (!vivo || !ref.current) return;
      if (!p3Iniciar()) { if (aoFalhar) aoFalhar(); return; }
      let peca = null;
      try {
        peca = await p3Construir(nome);
      } catch (_) {
        // Arquivo que não veio: a caixa cai no desenho de traço em vez de
        // ficar um retângulo vazio ao lado da pergunta.
        if (aoFalhar) aoFalhar();
        return;
      }
      // Quatro das onze vêm de arquivo, então entre o pedido e a resposta a
      // pessoa pode ter trocado de pergunta ou saído da etapa.
      if (!vivo || !ref.current || !peca) { if (!peca && aoFalhar) aoFalhar(); return; }

      if (P3.alvo) P3.cena.remove(P3.alvo);
      P3.cena.add(peca);
      P3.alvo = peca;
      P3.dono = ref.current;
      ref.current.appendChild(P3.renderer.domElement);

      const parado = window.matchMedia
        && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
      const limite = P3_CHAPADO[nome] || 0;
      peca.rotation.y = limite ? 0 : -0.5;
      peca.rotation.x = limite ? 0.06 : 0.1;
      let fase = 0;

      let girando = false; let ultimoX = 0; let ultimoY = 0; let impulso = 0;
      const el = P3.renderer.domElement;
      const pegar = (e) => {
        girando = true; ultimoX = e.clientX; ultimoY = e.clientY; impulso = 0;
        el.style.cursor = "grabbing";
        try { el.setPointerCapture(e.pointerId); } catch (_) { /* sem captura */ }
      };
      const mover = (e) => {
        if (!girando) return;
        impulso = (e.clientX - ultimoX) * 0.01;
        peca.rotation.y = limite
          ? Math.max(-limite, Math.min(limite, peca.rotation.y + impulso))
          : peca.rotation.y + impulso;
        peca.rotation.x = Math.max(-0.5, Math.min(0.5, peca.rotation.x + (e.clientY - ultimoY) * 0.006));
        ultimoX = e.clientX; ultimoY = e.clientY;
      };
      const largar = (e) => {
        girando = false;
        el.style.cursor = "grab";
        try { el.releasePointerCapture(e.pointerId); } catch (_) { /* já solto */ }
      };
      el.addEventListener("pointerdown", pegar);
      el.addEventListener("pointermove", mover);
      el.addEventListener("pointerup", largar);
      el.addEventListener("pointercancel", largar);

      const medir = () => {
        const c = P3.dono;
        if (!c) return;
        const l = c.clientWidth || 1; const a = c.clientHeight || 1;
        P3.renderer.setSize(l, a, false);
        P3.camera.aspect = l / a;
        P3.camera.updateProjectionMatrix();
      };
      medir();
      const obs = new ResizeObserver(medir);
      obs.observe(ref.current);

      const quadro = () => {
        if (!vivo) return;
        if (!girando) {
          if (limite) {
            // Balanço: volta sozinha para a frente e oscila em torno dela.
            if (!parado) { fase += 0.006; peca.rotation.y = Math.sin(fase) * limite; }
          } else if (Math.abs(impulso) > 0.0004) {
            peca.rotation.y += impulso; impulso *= 0.94;
          } else if (!parado) {
            peca.rotation.y += 0.0038;
          }
        }
        P3.renderer.render(P3.cena, P3.camera);
        P3.laco = requestAnimationFrame(quadro);
      };
      if (P3.laco) cancelAnimationFrame(P3.laco);
      quadro();

      solto = () => {
        obs.disconnect();
        el.removeEventListener("pointerdown", pegar);
        el.removeEventListener("pointermove", mover);
        el.removeEventListener("pointerup", largar);
        el.removeEventListener("pointercancel", largar);
        if (P3.laco) { cancelAnimationFrame(P3.laco); P3.laco = null; }
        P3.cena.remove(peca);
        // Geometria e material são feitos a cada montagem; sem soltar, trocar
        // de pergunta vinte vezes deixa vinte cópias na memória da placa.
        peca.traverse((o) => {
          if (o.geometry) o.geometry.dispose();
          if (o.material) o.material.dispose();
        });
        if (P3.alvo === peca) P3.alvo = null;
        if (P3.dono === ref.current) P3.dono = null;
      };
    };

    if (window.THREE) montar();
    else {
      const aoPronto = () => montar();
      window.addEventListener("three-ready", aoPronto, { once: true });
      solto = () => window.removeEventListener("three-ready", aoPronto);
    }

    // `solto` pode ainda não existir quando a etapa é deixada durante o
    // carregamento; `vivo` já barrou a montagem, e o que sobrar é recolhido
    // aqui assim que ela terminar de desistir.
    return () => { vivo = false; if (solto) solto(); };
  }, [nome]);

  return <div ref={ref} style={{ width: "100%", height: "100%" }} aria-hidden="true" />;
}

window.Props3D = { CaixaProp3D, construir: p3Construir, montarCena: p3MontarCena };
