adiciona modelo real

This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-09-07 21:30:18 -03:00
1 parent 71d13753c9
commit 3a659a52c7
18 files changed
+612 -176

No files matched your search

+6 -3
View File
@@ -5,7 +5,10 @@ import { fileURLToPath } from "node:url";
import { buildGeometry } from "./geometry.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const manifestPath = path.resolve(__dirname, "../seed/manifest.json");
const manifestPath = path.resolve(
__dirname,
process.argv[2] ?? "../seed/manifest-tegumentar.json",
);
const outDir = path.resolve(__dirname, "../seed/models");
mkdirSync(outDir, { recursive: true });
@@ -80,9 +83,9 @@ for (const [systemSlug, items] of bySystem) {
.setIndices(idxAccessor)
.setMaterial(material);
const mesh = doc.createMesh(item.slug).addPrimitive(prim);
const mesh = doc.createMesh(item.meshName).addPrimitive(prim);
const node = doc
.createNode(item.slug)
.createNode(item.meshName)
.setMesh(mesh)
.setTranslation(item.position)
.setRotation(eulerToQuat(item.rotation ?? [0, 0, 0]));
+24
View File
@@ -0,0 +1,24 @@
import { execFileSync } from "node:child_process";
import { existsSync, mkdirSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { SOURCES } from "./sources.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const RAW_DIR = path.resolve(__dirname, "../raw");
const CONVERTED_DIR = path.resolve(__dirname, "../converted");
mkdirSync(CONVERTED_DIR, { recursive: true });
for (const { fbx, key } of SOURCES) {
const inPath = path.join(RAW_DIR, fbx);
const outPath = path.join(CONVERTED_DIR, `${key}.glb`);
if (existsSync(outPath)) {
console.log(`✓ já convertido: ${fbx}`);
continue;
}
console.log(`⚙ convertendo ${fbx} → ${key}.glb ...`);
execFileSync("assimp", ["export", inPath, outPath, "-f", "glb2"], { stdio: "inherit" });
console.log(`✓ ${key}.glb gerado`);
}
console.log("Conversão FBX → glTF concluída.");
+136
View File
@@ -0,0 +1,136 @@
// Lê os .glb convertidos do FBX real (Z-Anatomy), recolore por sistema,
// garante nomes de nó únicos por arquivo (Z-Anatomy tem estruturas
// repetidas — costelas, vértebras, falanges — que podem compartilhar o
// mesmo nome bruto) e gera:
// - data-pipeline/seed/models/<arquivo>.glb (final, servido pela API)
// - data-pipeline/seed/manifest-real.json (entradas para o seed do banco)
import { NodeIO } from "@gltf-transform/core";
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
import { prune, dedup } from "@gltf-transform/functions";
import { writeFileSync, mkdirSync } from "node:fs";
import path from "node:path";
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";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONVERTED_DIR = path.resolve(__dirname, "../converted");
const MODELS_OUT_DIR = path.resolve(__dirname, "../seed/models");
mkdirSync(MODELS_OUT_DIR, { recursive: true });
const systemBySlug = new Map(SYSTEMS.map((s) => [s.slug, s]));
function hexToRgb(hex) {
const n = parseInt(hex.replace("#", ""), 16);
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255, 1];
}
const COMBINING_MARKS = new RegExp("[\\u0300-\\u036f]", "g");
function slugify(s) {
return s
.normalize("NFD")
.replace(COMBINING_MARKS, "") // remove diacríticos (á -> a, ç -> c, ...)
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
function classifySystem(key, rawName) {
const target = SOURCE_TARGETS[key];
if (target.system) return target.system;
// Só o "visceral" tem system === null: decide por palavra-chave.
const lower = rawName.toLowerCase();
return RESPIRATORY_KEYWORDS.some((kw) => lower.includes(kw)) ? "respiratorio" : "visceral";
}
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
const manifest = [];
let totalStructures = 0;
for (const { fbx, key } of SOURCES) {
const inPath = path.join(CONVERTED_DIR, `${key}.glb`);
const target = SOURCE_TARGETS[key];
console.log(`Processando ${fbx} (${key})...`);
const doc = io.read(inPath);
const root = doc.getRoot();
const scene = root.getDefaultScene() ?? root.listScenes()[0];
if (!scene) {
console.warn(` ⚠ nenhuma cena encontrada em ${inPath}, pulando.`);
continue;
}
// Um material por sistema alvo deste arquivo (recoloração consistente,
// independente de como a FBX/Unity original colorizava as malhas).
const materialCache = new Map();
function materialFor(systemSlug) {
if (materialCache.has(systemSlug)) return materialCache.get(systemSlug);
const sys = systemBySlug.get(systemSlug);
const mat = doc
.createMaterial(systemSlug)
.setBaseColorFactor(hexToRgb(sys?.colorHex ?? "#cccccc"))
.setRoughnessFactor(0.55)
.setMetallicFactor(0.05);
materialCache.set(systemSlug, mat);
return mat;
}
const nameCounts = new Map(); // dedupe de nomes brutos duplicados no MESMO arquivo
let countThisFile = 0;
function visit(node) {
const mesh = node.getMesh();
if (mesh) {
const rawName = (node.getName() || mesh.getName() || "Estrutura sem nome").trim();
const n = (nameCounts.get(rawName) ?? 0) + 1;
nameCounts.set(rawName, n);
const uniqueMeshName = n === 1 ? rawName : `${rawName} (${n})`;
node.setName(uniqueMeshName);
const systemSlug = classifySystem(key, rawName);
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 slug = `${systemSlug}-${slugPart}`;
manifest.push({
system: systemSlug,
slug,
namePt: translateToPt(rawName) + (n > 1 ? ` (${n})` : ""),
nameEn: rawName + (n > 1 ? ` (${n})` : ""),
synonyms: [],
description: "",
regionPath: [sys?.namePt ?? systemSlug],
modelFile: target.modelFile,
meshName: uniqueMeshName,
});
countThisFile += 1;
totalStructures += 1;
}
for (const child of node.listChildren()) visit(child);
}
for (const topNode of scene.listChildren()) visit(topNode);
console.log(` ${countThisFile} estruturas nomeadas encontradas.`);
if (countThisFile === 0) {
console.warn(` ⚠ nenhuma malha nomeada encontrada em ${fbx} — confira o log do assimp/conversão.`);
}
await doc.transform(prune(), dedup());
const outPath = path.join(MODELS_OUT_DIR, target.modelFile);
await io.write(outPath, doc);
console.log(` ✓ ${target.modelFile} salvo em seed/models/`);
}
writeFileSync(
path.resolve(__dirname, "../seed/manifest-real.json"),
JSON.stringify({ structures: manifest }, null, 2),
);
console.log(`\nTotal: ${totalStructures} estruturas reais extraídas do Z-Anatomy.`);
@@ -0,0 +1,32 @@
import { createWriteStream, existsSync, mkdirSync, statSync } from "node:fs";
import { pipeline } from "node:stream/promises";
import { Readable } from "node:stream";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { RAW_BASE, SOURCES } from "./sources.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const RAW_DIR = path.resolve(__dirname, "../raw");
mkdirSync(RAW_DIR, { recursive: true });
async function download(fbx) {
const dest = path.join(RAW_DIR, fbx);
if (existsSync(dest) && statSync(dest).size > 0) {
console.log(`✓ já baixado: ${fbx}`);
return;
}
const url = RAW_BASE + fbx;
console.log(`↓ baixando ${fbx}...`);
const res = await fetch(url);
if (!res.ok || !res.body) {
throw new Error(`Falha ao baixar ${fbx}: HTTP ${res.status} (${url})`);
}
await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
console.log(`✓ ${fbx} baixado (${(statSync(dest).size / 1024 / 1024).toFixed(1)} MB)`);
}
for (const { fbx } of SOURCES) {
await download(fbx);
}
console.log("Todos os modelos-fonte baixados.");
+18
View File
@@ -0,0 +1,18 @@
import { readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SEED_DIR = path.resolve(__dirname, "../seed");
const real = JSON.parse(readFileSync(path.join(SEED_DIR, "manifest-real.json"), "utf-8"));
const tegumentar = JSON.parse(readFileSync(path.join(SEED_DIR, "manifest-tegumentar.json"), "utf-8"));
const structures = [...real.structures, ...tegumentar.structures];
writeFileSync(
path.join(SEED_DIR, "manifest.json"),
JSON.stringify({ structures }, null, 2),
);
console.log(`manifest.json final: ${structures.length} estruturas (${real.structures.length} reais + ${tegumentar.structures.length} tegumentar).`);
+4 -3
View File
@@ -6,10 +6,11 @@ import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import pg from "pg";
import { SYSTEMS as systems } from "../seed/systems.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const manifestPath = path.resolve(__dirname, "../seed/manifest.json");
const { systems, structures } = JSON.parse(readFileSync(manifestPath, "utf-8"));
const { structures } = JSON.parse(readFileSync(manifestPath, "utf-8"));
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
@@ -80,8 +81,8 @@ try {
st.synonyms ?? [],
st.description ?? null,
st.regionPath ?? [],
`${st.system}.glb`,
st.slug,
st.modelFile,
st.meshName,
systemId,
],
);
+47
View File
@@ -0,0 +1,47 @@
// Fonte real dos modelos: Z-Anatomy-Sample (github.com/LluisV/Z-Anatomy-Sample),
// CC-BY-SA 4.0 — reempacota o dataset BodyParts3D/FMA em malhas organizadas
// por sistema, com nomes de objeto reais em inglês. Servidos como blobs FBX
// binários comuns (não Git LFS), então dá para baixar via raw.githubusercontent.com.
//
// Deixamos de fora "Regions of human body100.fbx" e "Reference lines,
// planes and movements 1.fbx" nesta primeira importação (nomes com espaço,
// conteúdo mais voltado a navegação/didática do que estruturas discretas) —
// dá para adicionar depois seguindo o mesmo padrão.
export const RAW_BASE =
"https://raw.githubusercontent.com/LluisV/Z-Anatomy-Sample/main/Assets/Models/1.0%20Models/";
export const SOURCES = [
{ fbx: "SkeletalSystem100.fbx", key: "skeletal" },
{ fbx: "MuscularSystem100.fbx", key: "muscular" },
{ fbx: "NervousSystem100.fbx", key: "nervous" },
{ fbx: "CardioVascular41.fbx", key: "cardio" },
{ fbx: "VisceralSystem100.fbx", key: "visceral" },
{ fbx: "Joints100.fbx", key: "joints" },
{ fbx: "LymphoidOrgans100.fbx", key: "lymphoid" },
];
// Palavras-chave (substring, case-insensitive) que classificam um nó do
// VisceralSystem100.fbx como respiratório em vez de "visceral genérico".
export const RESPIRATORY_KEYWORDS = [
"lung",
"trachea",
"bronch",
"larynx",
"pharynx",
"diaphragm",
"pleura",
"alveol",
];
// key (fonte) -> { modelFile, system } — "visceral" é especial: o system
// real é decidido por nó (respiratorio vs visceral) via RESPIRATORY_KEYWORDS,
// mas os dois compartilham o mesmo arquivo .glb final.
export const SOURCE_TARGETS = {
skeletal: { modelFile: "esqueletico.glb", system: "esqueletico" },
muscular: { modelFile: "muscular.glb", system: "muscular" },
nervous: { modelFile: "nervoso.glb", system: "nervoso" },
cardio: { modelFile: "cardiovascular.glb", system: "cardiovascular" },
visceral: { modelFile: "visceral-organs.glb", system: null },
joints: { modelFile: "articular.glb", system: "articular" },
lymphoid: { modelFile: "linfatico.glb", system: "linfatico" },
};
+174
View File
@@ -0,0 +1,174 @@
// Tradução EN→PT-BR por correspondência EXATA (não palavra-por-palavra).
//
// Decisão deliberada: este é um app de estudo para prova de anatomia — uma
// tradução errada de um termo médico é pior do que nenhuma tradução. Por
// isso só traduzimos quando o nome bate 100% (após normalizar espaços e
// remover prefixo/sufixo "Left"/"Right") com uma entrada conhecida deste
// dicionário. Fora daqui, o nome original em inglês/latim é mantido —
// nomenclatura anatômica internacional também é usada normalmente no
// ensino de anatomia no Brasil, então isso não compromete o estudo.
const EXACT_PT = {
// Ossos
"skull": "Crânio",
"mandible": "Mandíbula",
"maxilla": "Maxila",
"frontal bone": "Osso Frontal",
"parietal bone": "Osso Parietal",
"occipital bone": "Osso Occipital",
"temporal bone": "Osso Temporal",
"sphenoid bone": "Osso Esfenoide",
"ethmoid bone": "Osso Etmoide",
"zygomatic bone": "Osso Zigomático",
"nasal bone": "Osso Nasal",
"hyoid bone": "Osso Hioide",
"clavicle": "Clavícula",
"scapula": "Escápula",
"sternum": "Esterno",
"rib": "Costela",
"humerus": "Úmero",
"radius": "Rádio",
"ulna": "Ulna",
"femur": "Fêmur",
"patella": "Patela",
"tibia": "Tíbia",
"fibula": "Fíbula",
"pelvis": "Pelve",
"sacrum": "Sacro",
"coccyx": "Cóccix",
"ilium": "Ílio",
"ischium": "Ísquio",
"pubis": "Púbis",
"vertebra": "Vértebra",
"atlas": "Atlas (C1)",
"axis": "Áxis (C2)",
// Músculos
"deltoid": "Deltoide",
"trapezius": "Trapézio",
"biceps brachii": "Bíceps Braquial",
"triceps brachii": "Tríceps Braquial",
"pectoralis major": "Peitoral Maior",
"pectoralis minor": "Peitoral Menor",
"rectus abdominis": "Reto Abdominal",
"external oblique": "Oblíquo Externo do Abdome",
"internal oblique": "Oblíquo Interno do Abdome",
"latissimus dorsi": "Latíssimo do Dorso",
"gluteus maximus": "Glúteo Máximo",
"gluteus medius": "Glúteo Médio",
"gluteus minimus": "Glúteo Mínimo",
"sartorius": "Sartório",
"gracilis": "Grácil",
"biceps femoris": "Bíceps Femoral",
"semitendinosus": "Semitendíneo",
"semimembranosus": "Semimembranáceo",
"gastrocnemius": "Gastrocnêmio",
"soleus": "Sóleo",
"tibialis anterior": "Tibial Anterior",
"sternocleidomastoid": "Esternocleidomastóideo",
"masseter": "Masseter",
"temporalis": "Temporal (músculo)",
"frontalis": "Frontal (músculo)",
"diaphragm": "Diafragma",
// Sistema nervoso
"brain": "Encéfalo",
"cerebrum": "Cérebro",
"cerebellum": "Cerebelo",
"brainstem": "Tronco Encefálico",
"spinal cord": "Medula Espinhal",
"sciatic nerve": "Nervo Isquiático (Ciático)",
"femoral nerve": "Nervo Femoral",
"median nerve": "Nervo Mediano",
"ulnar nerve": "Nervo Ulnar",
"radial nerve": "Nervo Radial",
"phrenic nerve": "Nervo Frênico",
"vagus nerve": "Nervo Vago",
"facial nerve": "Nervo Facial",
"trigeminal nerve": "Nervo Trigêmeo",
"optic nerve": "Nervo Óptico",
"olfactory nerve": "Nervo Olfatório",
"brachial plexus": "Plexo Braquial",
// Cardiovascular
"heart": "Coração",
"aorta": "Aorta",
"superior vena cava": "Veia Cava Superior",
"inferior vena cava": "Veia Cava Inferior",
"carotid artery": "Artéria Carótida",
"jugular vein": "Veia Jugular",
"subclavian artery": "Artéria Subclávia",
"brachial artery": "Artéria Braquial",
"femoral artery": "Artéria Femoral",
"pulmonary artery": "Artéria Pulmonar",
"pulmonary vein": "Veia Pulmonar",
"coronary artery": "Artéria Coronária",
// Respiratório
"trachea": "Traqueia",
"larynx": "Laringe",
"pharynx": "Faringe",
"lung": "Pulmão",
"bronchus": "Brônquio",
// Vísceras
"liver": "Fígado",
"stomach": "Estômago",
"kidney": "Rim",
"bladder": "Bexiga",
"pancreas": "Pâncreas",
"spleen": "Baço",
"esophagus": "Esôfago",
"colon": "Cólon",
"small intestine": "Intestino Delgado",
"large intestine": "Intestino Grosso",
"thyroid": "Tireoide",
"adrenal gland": "Glândula Suprarrenal",
"ovary": "Ovário",
"testis": "Testículo",
"uterus": "Útero",
"prostate": "Próstata",
// Articulações / linfático
"knee joint": "Articulação do Joelho",
"elbow joint": "Articulação do Cotovelo",
"shoulder joint": "Articulação do Ombro",
"hip joint": "Articulação do Quadril",
"ankle joint": "Articulação do Tornozelo",
"wrist joint": "Articulação do Punho",
"lymph node": "Linfonodo",
"thymus": "Timo",
"tonsil": "Tonsila",
};
function normalize(s) {
return s.trim().toLowerCase().replace(/\s+/g, " ");
}
/**
* Traduz um nome anatômico em inglês para PT-BR quando há correspondência
* exata (ignorando prefixo/sufixo Left/Right), senão retorna o original.
*/
export function translateToPt(rawName) {
const trimmed = rawName.trim().replace(/\s+/g, " ");
const norm = normalize(trimmed);
let side = null;
let base = norm;
if (/^left\b/.test(norm)) {
side = "Esquerdo";
base = norm.replace(/^left\s*/, "");
} else if (/^right\b/.test(norm)) {
side = "Direito";
base = norm.replace(/^right\s*/, "");
} else if (/\bleft$/.test(norm)) {
side = "Esquerdo";
base = norm.replace(/\s*left$/, "");
} else if (/\bright$/.test(norm)) {
side = "Direito";
base = norm.replace(/\s*right$/, "");
}
const translated = EXACT_PT[base];
if (!translated) return trimmed;
return side ? `${translated} ${side}` : translated;
}