diff --git a/data-pipeline/scripts/extract-manifest.mjs b/data-pipeline/scripts/extract-manifest.mjs index b517339..fa033ff 100644 --- a/data-pipeline/scripts/extract-manifest.mjs +++ b/data-pipeline/scripts/extract-manifest.mjs @@ -87,6 +87,77 @@ function resolveSkeletalBone(rawName) { return { canonicalKey: entry.canonicalKey, pt: entry.pt, side: "single" }; } +// "Sistema Nervoso" e "Regiões do corpo" não têm uma lista de exclusão +// como o esquelético (206 ossos) — lá, QUALQUER malha nomeada vira uma +// estrutura. O problema: o Z-Anatomy original usa "linhas de chamada" +// (pontos apontando de uma estrutura pra um rótulo de texto, como nos +// diagramas didáticos) que às vezes são a PRÓPRIA malha da estrutura (não +// dá pra filtrar por nome — o usuário confirmou que clicar numa dessas +// linhas resolve num nome real, ex: "Right Anterior region of forearm"). +// Em vez de nome, detectamos pela GEOMETRIA: uma linha de chamada é um +// segmento reto e fino (baixíssimo desvio perpendicular ao seu próprio +// eixo mais longo) — bem diferente de uma estrutura anatômica de verdade +// (mesmo um nervo fino tem curvas/ramificações reais). Isso não depende +// de nome nenhum, então não arrisca excluir a estrutura errada. +const LEADER_LINE_SOURCE_KEYS = new Set(["nervous", "regions"]); + +function isLeaderLineMesh(mesh) { + try { + const prims = mesh.listPrimitives(); + if (prims.length === 0) return false; + return prims.every((prim) => isLeaderLinePrimitive(prim)); + } catch { + return false; + } +} + +function isLeaderLinePrimitive(prim) { + const accessor = prim.getAttribute("POSITION"); + if (!accessor) return false; + const count = accessor.getCount(); + if (count < 2) return false; + const arr = accessor.getArray(); + if (!arr) return false; + + const at = (i) => [arr[i * 3], arr[i * 3 + 1], arr[i * 3 + 2]]; + const dist2 = (a, b) => { + const dx = a[0] - b[0], dy = a[1] - b[1], dz = a[2] - b[2]; + return dx * dx + dy * dy + dz * dz; + }; + + // Acha os dois pontos mais distantes entre si (técnica padrão e barata + // pra aproximar o "eixo" mais longo de uma nuvem de pontos): parte de + // um ponto qualquer, acha o mais distante dele (a), depois o mais + // distante de "a" (b) — "a" e "b" aproximam as duas pontas do objeto. + let a = at(0), bestD = 0; + for (let i = 1; i < count; i++) { + const d = dist2(a, at(i)); + if (d > bestD) { bestD = d; a = at(i); } + } + let b = a; bestD = 0; + for (let i = 0; i < count; i++) { + const d = dist2(a, at(i)); + if (d > bestD) { bestD = d; b = at(i); } + } + const axisLenSq = dist2(a, b); + const axisLen = Math.sqrt(axisLenSq); + if (axisLen < 0.02) return false; // curto demais pra ser uma linha de chamada (poste real de âncora) + + const ax = [(b[0] - a[0]) / axisLen, (b[1] - a[1]) / axisLen, (b[2] - a[2]) / axisLen]; + let maxPerp = 0; + for (let i = 0; i < count; i++) { + const p = at(i); + const v = [p[0] - a[0], p[1] - a[1], p[2] - a[2]]; + const t = v[0] * ax[0] + v[1] * ax[1] + v[2] * ax[2]; + const perp2 = + (v[0] - t * ax[0]) ** 2 + (v[1] - t * ax[1]) ** 2 + (v[2] - t * ax[2]) ** 2; + if (perp2 > maxPerp) maxPerp = perp2; + } + // "Linha reta e fina": todos os pontos ficam muito perto do próprio eixo + // mais longo (desvio perpendicular pequeno comparado ao comprimento). + return Math.sqrt(maxPerp) < axisLen * 0.08; +} + function classifySystem(key, rawName) { const target = SOURCE_TARGETS[key]; if (target.system) return target.system; @@ -165,6 +236,15 @@ for (const { fbx, key } of SOURCES) { namePt = `${resolved.pt}${sideLabel}`; nameEn = `${sideWord}${resolved.canonicalKey}`; description = BONE_DESCRIPTIONS[resolved.canonicalKey] ?? ""; + } else if (LEADER_LINE_SOURCE_KEYS.has(key) && isLeaderLineMesh(mesh)) { + // Segmento reto e fino (linha de chamada de rótulo do app + // original) — mesmo tendo um nome "real", não é uma estrutura + // anatômica visível/clicável de verdade. Marcamos excluído (não + // vira estrutura, mesh fica no .glb mas oculto) do mesmo jeito + // que o esquelético já faz pros seus "acidentes ósseos". + node.setExtras({ excluded: true }); + for (const child of node.listChildren()) visit(child); + return; } else { systemSlug = classifySystem(key, rawName); // Nomes de muscular/regiões/referências vêm com sufixo ".l"/".r"/