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

+8 -2
View File
@@ -1,6 +1,12 @@
FROM node:20-alpine
# Base Debian (não Alpine): precisamos do pacote "assimp-utils" (binário
# `assimp`) para converter os FBX reais do Z-Anatomy para glTF/GLB — no
# Alpine essa dependência é bem mais dolorosa de resolver.
FROM node:20-bookworm
RUN apt-get update \
&& apt-get install -y --no-install-recommends assimp-utils ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
CMD ["sh", "-c", "node seed/build-manifest.mjs && node scripts/build-seed-models.mjs && node scripts/seed-db.mjs"]
CMD ["npm", "run", "run:all"]
+8 -2
View File
@@ -4,11 +4,17 @@
"type": "module",
"private": true,
"scripts": {
"build:seed-models": "node scripts/build-seed-models.mjs",
"seed:db": "node scripts/seed-db.mjs"
"fetch:models": "node scripts/fetch-real-models.mjs",
"convert:models": "node scripts/convert-fbx.mjs",
"extract:manifest": "node scripts/extract-manifest.mjs",
"build:tegumentar": "node seed/build-manifest.mjs && node scripts/build-seed-models.mjs",
"merge:manifest": "node scripts/merge-manifests.mjs",
"seed:db": "node scripts/seed-db.mjs",
"run:all": "npm run fetch:models && npm run convert:models && npm run extract:manifest && npm run build:tegumentar && npm run merge:manifest && npm run seed:db"
},
"dependencies": {
"@gltf-transform/core": "^4.0.4",
"@gltf-transform/extensions": "^4.0.4",
"@gltf-transform/functions": "^4.0.4",
"pg": "^8.12.0"
}
+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;
}
+9 -134
View File
@@ -1,40 +1,13 @@
// Generates manifest.json: the curated seed dataset of anatomical structures.
//
// IMPORTANT — read this before treating the geometry as "real anatomy":
// The BodyParts3D dataset (lifesciencedb.jp/bp3d) does not expose a scriptable
// download endpoint (it's served through a legacy Ext.js viewer app with no
// stable direct-download URLs per structure). Rather than block the whole
// platform on manually clicking through thousands of downloads, this seed
// dataset uses simple parametric primitives (box/sphere/cylinder) placed at
// anatomically correct positions/proportions on a standing human figure
// (1.8 m tall, origin at the feet, y-up, +z = anterior).
//
// The names, systems, regions and relationships ARE real anatomy — only the
// mesh shapes are schematic stand-ins. To upgrade to real segmented meshes:
// obtain BodyParts3D OBJ files (manually exported from the viewer, or via
// Z-Anatomy's Blender project which re-packages the same CC BY-SA dataset
// with saner file names), then replace the shape generator's output for a
// given `slug` with `scripts/convert-obj.mjs <file.obj> <slug>` (see
// data-pipeline/README.md). No frontend/backend code needs to change.
// Gera manifest-tegumentar.json: o Z-Anatomy-Sample não inclui camadas de
// pele (epiderme/derme/hipoderme), então para o sistema tegumentar seguimos
// usando geometria esquemática simples (ver data-pipeline/README rodapé).
// Todos os outros sistemas vêm de malhas reais — veja scripts/extract-manifest.mjs.
import { writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/** @typedef {{type:'box',w:number,h:number,d:number}|{type:'sphere',r:number}|{type:'cylinder',rt:number,rb:number,h:number}} Shape */
const systems = [
{ slug: "esqueletico", namePt: "Sistema Esquelético", nameEn: "Skeletal System", colorHex: "#e8e2d0", order: 1 },
{ slug: "muscular", namePt: "Sistema Muscular", nameEn: "Muscular System", colorHex: "#b5433a", order: 2 },
{ slug: "nervoso", namePt: "Sistema Nervoso", nameEn: "Nervous System", colorHex: "#f2c14e", order: 3 },
{ slug: "cardiovascular", namePt: "Sistema Cardiovascular", nameEn: "Cardiovascular System", colorHex: "#c1121f", order: 4 },
{ slug: "respiratorio", namePt: "Sistema Respiratório", nameEn: "Respiratory System", colorHex: "#4f7cff", order: 5 },
{ slug: "tegumentar", namePt: "Sistema Tegumentar", nameEn: "Integumentary System", colorHex: "#e0a96d", order: 6 },
];
/** @type {any[]} */
const structures = [];
function add(system, slug, namePt, nameEn, region, shape, position, opts = {}) {
@@ -46,120 +19,22 @@ function add(system, slug, namePt, nameEn, region, shape, position, opts = {}) {
synonyms: opts.synonyms ?? [],
description: opts.description ?? "",
regionPath: region,
modelFile: `${system}.glb`,
meshName: slug,
shape,
position,
rotation: opts.rotation ?? [0, 0, 0],
color: opts.color,
});
}
// Adds a left/right pair mirrored across the sagittal (x=0) plane.
function addBilateral(system, baseSlug, baseNamePt, baseNameEn, region, shape, position, opts = {}) {
const [x, y, z] = position;
add(system, `${baseSlug}_direito`, `${baseNamePt} Direito`, `Right ${baseNameEn}`, region, shape, [x, y, z], opts);
add(system, `${baseSlug}_esquerdo`, `${baseNamePt} Esquerdo`, `Left ${baseNameEn}`, region, shape, [-x, y, z], opts);
}
// ---------------------------------------------------------------------------
// SISTEMA ESQUELÉTICO
// ---------------------------------------------------------------------------
const AX = ["Sistema Esquelético"];
add("esqueletico", "cranio", "Crânio", "Skull", [...AX, "Cabeça"], { type: "sphere", r: 0.11 }, [0, 1.72, 0]);
add("esqueletico", "mandibula", "Mandíbula", "Mandible", [...AX, "Cabeça"], { type: "box", w: 0.09, h: 0.04, d: 0.08 }, [0, 1.62, 0.03]);
add("esqueletico", "coluna_cervical", "Coluna Cervical", "Cervical Spine", [...AX, "Pescoço"], { type: "cylinder", rt: 0.02, rb: 0.02, h: 0.12 }, [0, 1.55, -0.02]);
add("esqueletico", "coluna_toracica", "Coluna Torácica", "Thoracic Spine", [...AX, "Tronco"], { type: "cylinder", rt: 0.025, rb: 0.025, h: 0.35 }, [0, 1.3, -0.03]);
add("esqueletico", "coluna_lombar", "Coluna Lombar", "Lumbar Spine", [...AX, "Tronco"], { type: "cylinder", rt: 0.03, rb: 0.03, h: 0.18 }, [0, 1.0, -0.02]);
add("esqueletico", "sacro", "Sacro", "Sacrum", [...AX, "Pelve"], { type: "box", w: 0.1, h: 0.08, d: 0.05 }, [0, 0.85, -0.03]);
add("esqueletico", "gradeado_costal", "Gradeado Costal (Costelas)", "Rib Cage", [...AX, "Tórax"], { type: "box", w: 0.3, h: 0.3, d: 0.18 }, [0, 1.25, 0]);
add("esqueletico", "esterno", "Esterno", "Sternum", [...AX, "Tórax"], { type: "box", w: 0.05, h: 0.2, d: 0.02 }, [0, 1.28, 0.1]);
addBilateral("esqueletico", "clavicula", "Clavícula", "Clavicle", [...AX, "Cintura Escapular"], { type: "cylinder", rt: 0.012, rb: 0.012, h: 0.16 }, [0.12, 1.42, 0.04], { rotation: [0, 0, 1.3] });
addBilateral("esqueletico", "escapula", "Escápula", "Scapula", [...AX, "Cintura Escapular"], { type: "box", w: 0.1, h: 0.14, d: 0.015 }, [0.15, 1.35, -0.06]);
addBilateral("esqueletico", "umero", "Úmero", "Humerus", [...AX, "Membro Superior", "Braço"], { type: "cylinder", rt: 0.025, rb: 0.02, h: 0.28 }, [0.22, 1.15, 0]);
addBilateral("esqueletico", "radio", "Rádio", "Radius", [...AX, "Membro Superior", "Antebraço"], { type: "cylinder", rt: 0.015, rb: 0.013, h: 0.24 }, [0.24, 0.85, 0.02]);
addBilateral("esqueletico", "ulna", "Ulna", "Ulna", [...AX, "Membro Superior", "Antebraço"], { type: "cylinder", rt: 0.016, rb: 0.013, h: 0.25 }, [0.24, 0.85, -0.02]);
addBilateral("esqueletico", "carpo", "Ossos do Carpo", "Carpal Bones", [...AX, "Membro Superior", "Mão"], { type: "sphere", r: 0.02 }, [0.24, 0.68, 0]);
addBilateral("esqueletico", "mao", "Metacarpo e Falanges da Mão", "Hand Metacarpals/Phalanges", [...AX, "Membro Superior", "Mão"], { type: "box", w: 0.05, h: 0.1, d: 0.015 }, [0.24, 0.6, 0]);
add("esqueletico", "pelve", "Pelve (Ossos do Quadril)", "Pelvis (Hip Bones)", [...AX, "Pelve"], { type: "box", w: 0.28, h: 0.15, d: 0.15 }, [0, 0.78, 0]);
addBilateral("esqueletico", "femur", "Fêmur", "Femur", [...AX, "Membro Inferior", "Coxa"], { type: "cylinder", rt: 0.032, rb: 0.026, h: 0.42 }, [0.09, 0.5, 0]);
addBilateral("esqueletico", "patela", "Patela", "Patella", [...AX, "Membro Inferior", "Joelho"], { type: "sphere", r: 0.025 }, [0.09, 0.28, 0.04]);
addBilateral("esqueletico", "tibia", "Tíbia", "Tibia", [...AX, "Membro Inferior", "Perna"], { type: "cylinder", rt: 0.024, rb: 0.018, h: 0.35 }, [0.09, 0.12, 0]);
addBilateral("esqueletico", "fibula", "Fíbula", "Fibula", [...AX, "Membro Inferior", "Perna"], { type: "cylinder", rt: 0.012, rb: 0.01, h: 0.34 }, [0.13, 0.12, -0.01]);
addBilateral("esqueletico", "tarso", "Ossos do Tarso", "Tarsal Bones", [...AX, "Membro Inferior", "Pé"], { type: "sphere", r: 0.025 }, [0.09, 0.03, 0.02]);
addBilateral("esqueletico", "pe", "Metatarso e Falanges do Pé", "Foot Metatarsals/Phalanges", [...AX, "Membro Inferior", "Pé"], { type: "box", w: 0.08, h: 0.04, d: 0.18 }, [0.09, 0.02, 0.1]);
// ---------------------------------------------------------------------------
// SISTEMA MUSCULAR
// ---------------------------------------------------------------------------
const MX = ["Sistema Muscular"];
add("muscular", "musculo_frontal", "Músculo Frontal", "Frontalis Muscle", [...MX, "Cabeça"], { type: "box", w: 0.1, h: 0.04, d: 0.01 }, [0, 1.78, 0.08]);
add("muscular", "musculo_temporal", "Músculo Temporal", "Temporalis Muscle", [...MX, "Cabeça"], { type: "box", w: 0.03, h: 0.06, d: 0.06 }, [0.1, 1.74, 0]);
add("muscular", "musculo_masseter", "Músculo Masseter", "Masseter Muscle", [...MX, "Cabeça"], { type: "box", w: 0.02, h: 0.04, d: 0.04 }, [0.09, 1.63, 0.02]);
addBilateral("muscular", "esternocleidomastoideo", "Esternocleidomastóideo", "Sternocleidomastoid", [...MX, "Pescoço"], { type: "cylinder", rt: 0.015, rb: 0.02, h: 0.12 }, [0.04, 1.53, 0.03], { rotation: [0.3, 0, 0.15] });
add("muscular", "trapezio", "Trapézio", "Trapezius", [...MX, "Dorso"], { type: "box", w: 0.32, h: 0.28, d: 0.03 }, [0, 1.42, -0.08]);
addBilateral("muscular", "deltoide", "Deltoide", "Deltoid", [...MX, "Ombro"], { type: "sphere", r: 0.06 }, [0.22, 1.34, 0]);
add("muscular", "peitoral_maior", "Peitoral Maior", "Pectoralis Major", [...MX, "Tórax"], { type: "box", w: 0.26, h: 0.18, d: 0.04 }, [0, 1.28, 0.1]);
addBilateral("muscular", "biceps_braquial", "Bíceps Braquial", "Biceps Brachii", [...MX, "Membro Superior", "Braço"], { type: "cylinder", rt: 0.03, rb: 0.025, h: 0.24 }, [0.23, 1.15, 0.02]);
addBilateral("muscular", "triceps_braquial", "Tríceps Braquial", "Triceps Brachii", [...MX, "Membro Superior", "Braço"], { type: "cylinder", rt: 0.028, rb: 0.024, h: 0.24 }, [0.23, 1.15, -0.02]);
add("muscular", "reto_abdominal", "Reto Abdominal", "Rectus Abdominis", [...MX, "Abdome"], { type: "box", w: 0.14, h: 0.22, d: 0.03 }, [0, 1.02, 0.1]);
add("muscular", "obliquo_externo", "Oblíquo Externo do Abdome", "External Oblique", [...MX, "Abdome"], { type: "box", w: 0.28, h: 0.2, d: 0.03 }, [0, 1.02, 0.06]);
add("muscular", "latissimo_do_dorso", "Latíssimo do Dorso", "Latissimus Dorsi", [...MX, "Dorso"], { type: "box", w: 0.3, h: 0.24, d: 0.03 }, [0, 1.15, -0.09]);
add("muscular", "gluteo_maximo", "Glúteo Máximo", "Gluteus Maximus", [...MX, "Pelve"], { type: "box", w: 0.28, h: 0.16, d: 0.08 }, [0, 0.75, -0.08]);
addBilateral("muscular", "quadriceps_femoral", "Quadríceps Femoral", "Quadriceps Femoris", [...MX, "Membro Inferior", "Coxa"], { type: "cylinder", rt: 0.055, rb: 0.04, h: 0.38 }, [0.09, 0.5, 0.03]);
addBilateral("muscular", "isquiotibiais", "Isquiotibiais", "Hamstrings", [...MX, "Membro Inferior", "Coxa"], { type: "cylinder", rt: 0.05, rb: 0.035, h: 0.38 }, [0.09, 0.5, -0.04]);
addBilateral("muscular", "gastrocnemio", "Gastrocnêmio", "Gastrocnemius", [...MX, "Membro Inferior", "Perna"], { type: "cylinder", rt: 0.04, rb: 0.02, h: 0.22 }, [0.09, 0.18, -0.02]);
addBilateral("muscular", "tibial_anterior", "Tibial Anterior", "Tibialis Anterior", [...MX, "Membro Inferior", "Perna"], { type: "cylinder", rt: 0.02, rb: 0.012, h: 0.22 }, [0.1, 0.18, 0.02]);
// ---------------------------------------------------------------------------
// SISTEMA NERVOSO
// ---------------------------------------------------------------------------
const NX = ["Sistema Nervoso"];
add("nervoso", "encefalo", "Encéfalo (Cérebro)", "Brain (Cerebrum)", [...NX, "Sistema Nervoso Central", "Cabeça"], { type: "sphere", r: 0.09 }, [0, 1.74, -0.01], { description: "Inclui os hemisférios cerebrais e o córtex." });
add("nervoso", "cerebelo", "Cerebelo", "Cerebellum", [...NX, "Sistema Nervoso Central", "Cabeça"], { type: "sphere", r: 0.04 }, [0, 1.68, -0.06]);
add("nervoso", "tronco_encefalico", "Tronco Encefálico", "Brainstem", [...NX, "Sistema Nervoso Central", "Cabeça"], { type: "cylinder", rt: 0.02, rb: 0.02, h: 0.06 }, [0, 1.62, -0.03]);
add("nervoso", "medula_espinhal", "Medula Espinhal", "Spinal Cord", [...NX, "Sistema Nervoso Central", "Tronco"], { type: "cylinder", rt: 0.009, rb: 0.009, h: 0.6 }, [0, 1.2, -0.035]);
addBilateral("nervoso", "plexo_braquial", "Plexo Braquial", "Brachial Plexus", [...NX, "Sistema Nervoso Periférico", "Ombro"], { type: "sphere", r: 0.02 }, [0.16, 1.38, 0]);
addBilateral("nervoso", "nervo_frenico", "Nervo Frênico", "Phrenic Nerve", [...NX, "Sistema Nervoso Periférico", "Pescoço"], { type: "cylinder", rt: 0.004, rb: 0.004, h: 0.25 }, [0.05, 1.35, 0.01]);
addBilateral("nervoso", "nervo_mediano", "Nervo Mediano", "Median Nerve", [...NX, "Sistema Nervoso Periférico", "Membro Superior"], { type: "cylinder", rt: 0.004, rb: 0.004, h: 0.4 }, [0.24, 0.95, 0.01]);
addBilateral("nervoso", "nervo_radial", "Nervo Radial", "Radial Nerve", [...NX, "Sistema Nervoso Periférico", "Membro Superior"], { type: "cylinder", rt: 0.004, rb: 0.004, h: 0.4 }, [0.25, 0.95, -0.01]);
addBilateral("nervoso", "nervo_ulnar", "Nervo Ulnar", "Ulnar Nerve", [...NX, "Sistema Nervoso Periférico", "Membro Superior"], { type: "cylinder", rt: 0.004, rb: 0.004, h: 0.4 }, [0.23, 0.95, -0.02]);
addBilateral("nervoso", "nervo_isquiatico", "Nervo Isquiático (Ciático)", "Sciatic Nerve", [...NX, "Sistema Nervoso Periférico", "Membro Inferior"], { type: "cylinder", rt: 0.007, rb: 0.006, h: 0.4 }, [0.09, 0.45, -0.03], { synonyms: ["nervo ciático"] });
addBilateral("nervoso", "nervo_femoral", "Nervo Femoral", "Femoral Nerve", [...NX, "Sistema Nervoso Periférico", "Membro Inferior"], { type: "cylinder", rt: 0.005, rb: 0.005, h: 0.3 }, [0.11, 0.55, 0.02]);
// ---------------------------------------------------------------------------
// SISTEMA CARDIOVASCULAR
// ---------------------------------------------------------------------------
const CX = ["Sistema Cardiovascular"];
add("cardiovascular", "coracao", "Coração", "Heart", [...CX, "Tórax"], { type: "sphere", r: 0.07 }, [0.03, 1.27, 0.04]);
add("cardiovascular", "aorta", "Aorta", "Aorta", [...CX, "Tórax", "Abdome"], { type: "cylinder", rt: 0.014, rb: 0.01, h: 0.45 }, [0, 1.1, -0.02]);
add("cardiovascular", "veia_cava_superior", "Veia Cava Superior", "Superior Vena Cava", [...CX, "Tórax"], { type: "cylinder", rt: 0.012, rb: 0.012, h: 0.1 }, [0.03, 1.36, 0]);
add("cardiovascular", "veia_cava_inferior", "Veia Cava Inferior", "Inferior Vena Cava", [...CX, "Tórax", "Abdome"], { type: "cylinder", rt: 0.013, rb: 0.013, h: 0.35 }, [0.03, 1.1, -0.03]);
addBilateral("cardiovascular", "arteria_carotida", "Artéria Carótida", "Carotid Artery", [...CX, "Pescoço"], { type: "cylinder", rt: 0.006, rb: 0.006, h: 0.14 }, [0.03, 1.53, 0.01]);
addBilateral("cardiovascular", "arteria_subclavia", "Artéria Subclávia", "Subclavian Artery", [...CX, "Ombro"], { type: "cylinder", rt: 0.006, rb: 0.006, h: 0.12 }, [0.14, 1.4, 0]);
addBilateral("cardiovascular", "veia_jugular", "Veia Jugular", "Jugular Vein", [...CX, "Pescoço"], { type: "cylinder", rt: 0.006, rb: 0.006, h: 0.14 }, [0.045, 1.53, -0.01]);
addBilateral("cardiovascular", "arteria_braquial", "Artéria Braquial", "Brachial Artery", [...CX, "Membro Superior", "Braço"], { type: "cylinder", rt: 0.005, rb: 0.004, h: 0.24 }, [0.23, 1.15, 0.01]);
addBilateral("cardiovascular", "arteria_femoral", "Artéria Femoral", "Femoral Artery", [...CX, "Membro Inferior", "Coxa"], { type: "cylinder", rt: 0.006, rb: 0.005, h: 0.4 }, [0.09, 0.5, 0.05]);
// ---------------------------------------------------------------------------
// SISTEMA RESPIRATÓRIO
// ---------------------------------------------------------------------------
const RX = ["Sistema Respiratório"];
add("respiratorio", "laringe", "Laringe", "Larynx", [...RX, "Pescoço"], { type: "cylinder", rt: 0.02, rb: 0.02, h: 0.05 }, [0, 1.48, 0.02]);
add("respiratorio", "traqueia", "Traqueia", "Trachea", [...RX, "Pescoço", "Tórax"], { type: "cylinder", rt: 0.012, rb: 0.012, h: 0.13 }, [0, 1.4, 0.02]);
addBilateral("respiratorio", "bronquio_principal", "Brônquio Principal", "Main Bronchus", [...RX, "Tórax"], { type: "cylinder", rt: 0.008, rb: 0.006, h: 0.06 }, [0.03, 1.32, 0], { rotation: [0, 0, -0.5] });
addBilateral("respiratorio", "pulmao", "Pulmão", "Lung", [...RX, "Tórax"], { type: "box", w: 0.14, h: 0.28, d: 0.16 }, [0.13, 1.22, -0.02]);
add("respiratorio", "diafragma", "Diafragma", "Diaphragm", [...RX, "Tórax", "Abdome"], { type: "box", w: 0.32, h: 0.02, d: 0.2 }, [0, 1.08, 0]);
// ---------------------------------------------------------------------------
// SISTEMA TEGUMENTAR
// ---------------------------------------------------------------------------
const TX = ["Sistema Tegumentar"];
add("tegumentar", "epiderme", "Epiderme", "Epidermis", [...TX, "Membro Superior", "Antebraço"], { type: "box", w: 0.09, h: 0.2, d: 0.005 }, [0.24, 0.85, 0.035], { description: "Camada mais externa da pele, avascular." });
add("tegumentar", "derme", "Derme", "Dermis", [...TX, "Membro Superior", "Antebraço"], { type: "box", w: 0.09, h: 0.2, d: 0.01 }, [0.24, 0.85, 0.028], { description: "Camada intermediária, rica em vasos, nervos e anexos cutâneos." });
add("tegumentar", "hipoderme", "Hipoderme (Tecido Subcutâneo)", "Hypodermis (Subcutaneous Tissue)", [...TX, "Membro Superior", "Antebraço"], { type: "box", w: 0.09, h: 0.2, d: 0.015 }, [0.24, 0.85, 0.018], { description: "Camada mais profunda, rica em tecido adiposo." });
writeFileSync(
path.resolve(__dirname, "manifest.json"),
JSON.stringify({ systems, structures }, null, 2),
path.resolve(__dirname, "manifest-tegumentar.json"),
JSON.stringify({ structures }, null, 2),
);
console.log(`manifest.json gerado com ${structures.length} estruturas em ${systems.length} sistemas.`);
console.log(`manifest-tegumentar.json gerado com ${structures.length} estruturas.`);
+14
View File
@@ -0,0 +1,14 @@
// Fonte única de verdade para os sistemas anatômicos — usada tanto pela
// extração dos modelos reais (Z-Anatomy) quanto pelo gerador esquemático
// do sistema tegumentar (sem fonte de malha real disponível).
export const SYSTEMS = [
{ slug: "esqueletico", namePt: "Sistema Esquelético", nameEn: "Skeletal System", colorHex: "#e8e2d0", order: 1 },
{ slug: "muscular", namePt: "Sistema Muscular", nameEn: "Muscular System", colorHex: "#b5433a", order: 2 },
{ slug: "nervoso", namePt: "Sistema Nervoso", nameEn: "Nervous System", colorHex: "#f2c14e", order: 3 },
{ slug: "cardiovascular", namePt: "Sistema Cardiovascular", nameEn: "Cardiovascular System", colorHex: "#c1121f", order: 4 },
{ slug: "respiratorio", namePt: "Sistema Respiratório", nameEn: "Respiratory System", colorHex: "#4f7cff", order: 5 },
{ slug: "visceral", namePt: "Órgãos Viscerais (Digestório, Urinário, Endócrino, Reprodutor)", nameEn: "Visceral Organs", colorHex: "#8b5cf6", order: 6 },
{ slug: "articular", namePt: "Articulações", nameEn: "Joints", colorHex: "#38bdf8", order: 7 },
{ slug: "linfatico", namePt: "Sistema Linfático", nameEn: "Lymphatic System", colorHex: "#34d399", order: 8 },
{ slug: "tegumentar", namePt: "Sistema Tegumentar", nameEn: "Integumentary System", colorHex: "#e0a96d", order: 9 },
];