adiciona modelo real
This commit is contained in:
1 parent
71d13753c9
commit
3a659a52c7
18 files changed
+612
-176
No files matched your search
@@ -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.`);
|
||||
Reference in new issue
Block a user