154 lines
5.8 KiB
JavaScript
154 lines
5.8 KiB
JavaScript
// 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, weld, simplify } from "@gltf-transform/functions";
|
|
import { MeshoptSimplifier } from "meshoptimizer";
|
|
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";
|
|
|
|
await MeshoptSimplifier.ready;
|
|
|
|
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 = await 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.`);
|
|
}
|
|
|
|
// 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
|
|
// ~2000 estruturas são objetos pequenos (ossos individuais, contornos
|
|
// de inserção muscular) com poucos triângulos desde o início — um
|
|
// ratio agressivo (testamos 0.15) colapsa esses objetos pequenos a
|
|
// quase nada, embora preserve bem os objetos grandes. 0.5 é bem mais
|
|
// conservador: ainda reduz o tamanho dos objetos grandes (que dominam
|
|
// o tamanho do arquivo), mas praticamente não afeta os pequenos.
|
|
await doc.transform(
|
|
weld(),
|
|
simplify({ simplifier: MeshoptSimplifier, ratio: 0.5, error: 0.01 }),
|
|
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.`);
|