This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-09-08 14:32:32 -03:00
1 parent 0bc9708829
commit d62f4a3227
2 files changed
+236 -4

No files matched your search

+83 -4
View File
@@ -14,6 +14,7 @@ import { fileURLToPath } from "node:url";
import { SOURCES, SOURCE_TARGETS, RESPIRATORY_KEYWORDS } from "./sources.mjs";
import { translateToPt } from "./translate-pt.mjs";
import { SYSTEMS } from "../seed/systems.mjs";
import { SKELETAL_BONES } from "./skeletal-whitelist.mjs";
await MeshoptSimplifier.ready;
@@ -40,6 +41,48 @@ function slugify(s) {
.replace(/^-+|-+$/g, "");
}
// O arquivo esquelético real do Z-Anatomy mistura ossos de verdade com
// centenas de "acidentes ósseos" (pontos de referência NO osso, ex:
// "Deltoid tuberosity" — não são ossos) e marcadores de origem/inserção
// muscular (ex: "Pectoralis major muscle.ol" — pertencem ao músculo).
// Por isso usamos uma lista de INCLUSÃO com os 206 ossos reais em vez de
// aceitar tudo: só o que bate exatamente vira uma estrutura esquelética.
const SKELETAL_ALIASES = { atlas: "Atlas (C1)", axis: "Axis (C2)" };
const skeletalLookup = new Map(
Object.entries(SKELETAL_BONES).map(([key, info]) => [
key.toLowerCase(),
{ canonicalKey: key, ...info },
]),
);
function stripSideSuffix(rawName) {
const m = rawName.match(/^(.*)\.([lrij])$/i);
if (m) return { base: m[1].trim(), suffix: m[2].toLowerCase() };
return { base: rawName.trim(), suffix: null };
}
// Retorna null se o nome bruto não corresponder a nenhum dos 206 ossos —
// nesse caso a malha é ignorada (não vira estrutura, mas continua no
// arquivo .glb; como não há "structure" apontando pra ela, o frontend já
// a mantém oculta automaticamente).
function resolveSkeletalBone(rawName) {
const { base, suffix } = stripSideSuffix(rawName);
const normBase = base.toLowerCase();
const aliasTarget = SKELETAL_ALIASES[normBase];
const lookupKey = (aliasTarget ?? base).toLowerCase();
const entry = skeletalLookup.get(lookupKey);
if (!entry) return null;
if (entry.paired) {
// Ignora variantes ".i"/".j"/sem sufixo para ossos pareados — o
// Z-Anatomy parece manter cópias duplicadas (visão isolada vs.
// esqueleto montado); ".l"/".r" é a marcação inequívoca de lado.
if (suffix !== "l" && suffix !== "r") return null;
return { canonicalKey: entry.canonicalKey, pt: entry.pt, side: suffix };
}
return { canonicalKey: entry.canonicalKey, pt: entry.pt, side: "single" };
}
function classifySystem(key, rawName) {
const target = SOURCE_TARGETS[key];
if (target.system) return target.system;
@@ -81,6 +124,7 @@ for (const { fbx, key } of SOURCES) {
}
const nameCounts = new Map(); // dedupe de nomes brutos duplicados no MESMO arquivo
const includedBones = new Set(); // dedupe de osso+lado no sistema esquelético
let countThisFile = 0;
function visit(node) {
@@ -92,12 +136,32 @@ for (const { fbx, key } of SOURCES) {
const uniqueMeshName = n === 1 ? rawName : `${rawName} (${n})`;
node.setName(uniqueMeshName);
const systemSlug = classifySystem(key, rawName);
let systemSlug, namePt, nameEn;
if (key === "skeletal") {
const resolved = resolveSkeletalBone(rawName);
const dedupeKey = resolved ? `${resolved.canonicalKey}::${resolved.side}` : null;
if (!resolved || includedBones.has(dedupeKey)) {
for (const child of node.listChildren()) visit(child);
return;
}
includedBones.add(dedupeKey);
systemSlug = "esqueletico";
const sideLabel = resolved.side === "l" ? " Esquerdo" : resolved.side === "r" ? " Direito" : "";
const sideWord = resolved.side === "l" ? "Left " : resolved.side === "r" ? "Right " : "";
namePt = `${resolved.pt}${sideLabel}`;
nameEn = `${sideWord}${resolved.canonicalKey}`;
} else {
systemSlug = classifySystem(key, rawName);
namePt = translateToPt(rawName) + (n > 1 ? ` (${n})` : "");
nameEn = rawName + (n > 1 ? ` (${n})` : "");
}
const material = materialFor(systemSlug);
for (const prim of mesh.listPrimitives()) prim.setMaterial(material);
const sys = systemBySlug.get(systemSlug);
const slugPart = slugify(uniqueMeshName) || `sem-nome-${totalStructures}`;
const slugPart = slugify(key === "skeletal" ? namePt : uniqueMeshName) || `sem-nome-${totalStructures}`;
const slug = `${systemSlug}-${slugPart}`;
// Gravamos o slug como "extras" do nó (→ vira object.userData no
@@ -109,8 +173,8 @@ for (const { fbx, key } of SOURCES) {
manifest.push({
system: systemSlug,
slug,
namePt: translateToPt(rawName) + (n > 1 ? ` (${n})` : ""),
nameEn: rawName + (n > 1 ? ` (${n})` : ""),
namePt,
nameEn,
synonyms: [],
description: "",
regionPath: [sys?.namePt ?? systemSlug],
@@ -130,6 +194,21 @@ for (const { fbx, key } of SOURCES) {
console.warn(` ⚠ nenhuma malha nomeada encontrada em ${fbx} — confira o log do assimp/conversão.`);
}
if (key === "skeletal") {
const missing = [];
for (const [boneKey, info] of Object.entries(SKELETAL_BONES)) {
const sides = info.paired ? ["l", "r"] : ["single"];
for (const side of sides) {
if (!includedBones.has(`${boneKey}::${side}`)) missing.push(`${boneKey} (${side})`);
}
}
console.log(` → ${includedBones.size}/206 ossos da lista de referência encontrados.`);
if (missing.length > 0) {
console.warn(` ⚠ ${missing.length} ossos da lista NÃO encontrados no arquivo:`);
for (const m of missing) console.warn(` - ${m}`);
}
}
// Malhas do Z-Anatomy vêm pensadas para um app desktop (Unity), com
// densidade de polígonos bem maior do que um visualizador web aguenta
// com dezenas de estruturas na tela ao mesmo tempo. MAS: muitas das