diff --git a/data-pipeline/scripts/extract-manifest.mjs b/data-pipeline/scripts/extract-manifest.mjs index 1e3e770..9a75b6d 100644 --- a/data-pipeline/scripts/extract-manifest.mjs +++ b/data-pipeline/scripts/extract-manifest.mjs @@ -175,6 +175,105 @@ function isLeaderLinePointCloud(points) { return p90 < axisLen * 0.1; } +// Cobre um caso que isLeaderLineMesh() sozinho não pega: quando a linha de +// chamada NÃO é um nó separado, e sim geometria extra soldada dentro do +// MESMO primitivo de uma estrutura real (ex: a malha de uma região da pele +// vem com um "espinho" fino grudado nela desde a exportação do FBX +// original). Como a maior parte da malha nesse caso é a estrutura de +// verdade (não uma linha), o teste de colinearidade da malha INTEIRA +// reprova, e o espinho sobrevive grudado — foi exatamente o que apareceu +// como tracinhos saindo do corpo (cabeça, mão, canela) mesmo depois do +// filtro por malha. A correção certa é olhar por PEDAÇO CONECTADO da +// malha (componentes que não compartilham nenhum vértice entre si), não a +// malha toda: cada pedaço geometricamente desconectado é testado +// separadamente, e só os que passam no teste de "reto e fino" são +// removidos — a estrutura real (um componente só, com volume) nunca é. +function stripLeaderLineComponents(mesh) { + try { + stripLeaderLineComponentsUnsafe(mesh); + } catch { + // Nunca deixa uma malha atípica derrubar o pipeline inteiro — na + // dúvida, mantém a estrutura como veio (mesmo comportamento de antes + // desta função existir). + } +} + +function stripLeaderLineComponentsUnsafe(mesh) { + for (const prim of mesh.listPrimitives()) { + const posAccessor = prim.getAttribute("POSITION"); + const indicesAccessor = prim.getIndices(); + if (!posAccessor || !indicesAccessor) continue; + const indices = indicesAccessor.getArray(); + const posArr = posAccessor.getArray(); + if (!indices || !posArr || indices.length < 3) continue; + const at = (vi) => [posArr[vi * 3], posArr[vi * 3 + 1], posArr[vi * 3 + 2]]; + + // Union-Find simples pra agrupar vértices que compartilham alguma face + // (= estão no mesmo pedaço fisicamente conectado da malha). + const parent = new Map(); + function find(x) { + let root = x; + while (parent.get(root) !== root) root = parent.get(root); + while (parent.get(x) !== root) { + const next = parent.get(x); + parent.set(x, root); + x = next; + } + return root; + } + function union(a, b) { + const ra = find(a), rb = find(b); + if (ra !== rb) parent.set(ra, rb); + } + + const faceCount = Math.floor(indices.length / 3); + for (let f = 0; f < faceCount; f++) { + const a = indices[f * 3], b = indices[f * 3 + 1], c = indices[f * 3 + 2]; + if (!parent.has(a)) parent.set(a, a); + if (!parent.has(b)) parent.set(b, b); + if (!parent.has(c)) parent.set(c, c); + union(a, b); + union(b, c); + } + + const facesByRoot = new Map(); + for (let f = 0; f < faceCount; f++) { + const root = find(indices[f * 3]); + if (!facesByRoot.has(root)) facesByRoot.set(root, []); + facesByRoot.get(root).push(f); + } + // Malha inteira é UM pedaço só conectado — nada pra separar/remover. + if (facesByRoot.size <= 1) continue; + + const keptFaces = []; + let removedAny = false; + for (const faceList of facesByRoot.values()) { + const vertSet = new Set(); + for (const f of faceList) { + vertSet.add(indices[f * 3]); + vertSet.add(indices[f * 3 + 1]); + vertSet.add(indices[f * 3 + 2]); + } + if (isLeaderLinePointCloud([...vertSet].map(at))) { + removedAny = true; + } else { + keptFaces.push(...faceList); + } + } + if (!removedAny || keptFaces.length === 0) continue; + + keptFaces.sort((x, y) => x - y); + const newIndices = new indices.constructor(keptFaces.length * 3); + let w = 0; + for (const f of keptFaces) { + newIndices[w++] = indices[f * 3]; + newIndices[w++] = indices[f * 3 + 1]; + newIndices[w++] = indices[f * 3 + 2]; + } + indicesAccessor.setArray(newIndices); + } +} + function classifySystem(key, rawName) { const target = SOURCE_TARGETS[key]; if (target.system) return target.system; @@ -263,6 +362,7 @@ for (const { fbx, key } of SOURCES) { for (const child of node.listChildren()) visit(child); return; } else { + if (LEADER_LINE_SOURCE_KEYS.has(key)) stripLeaderLineComponents(mesh); systemSlug = classifySystem(key, rawName); // Nomes de muscular/regiões/referências vêm com sufixo ".l"/".r"/ // ".j" (lateralidade) em vez da palavra "Left"/"Right" — sem tirar