first commit

This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-09-07 20:46:08 -03:00
commit 0e854110d3
49 files changed
+2292

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
FROM node:20-alpine
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"]
+15
View File
@@ -0,0 +1,15 @@
{
"name": "anatomia-data-pipeline",
"version": "1.0.0",
"type": "module",
"private": true,
"scripts": {
"build:seed-models": "node scripts/build-seed-models.mjs",
"seed:db": "node scripts/seed-db.mjs"
},
"dependencies": {
"@gltf-transform/core": "^4.0.4",
"@gltf-transform/functions": "^4.0.4",
"pg": "^8.12.0"
}
}
@@ -0,0 +1,97 @@
import { Document, NodeIO } from "@gltf-transform/core";
import { readFileSync, mkdirSync } from "node:fs";
import path from "node:path";
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 outDir = path.resolve(__dirname, "../seed/models");
mkdirSync(outDir, { recursive: true });
const { structures } = JSON.parse(readFileSync(manifestPath, "utf-8"));
function eulerToQuat([x, y, z]) {
const cx = Math.cos(x / 2), sx = Math.sin(x / 2);
const cy = Math.cos(y / 2), sy = Math.sin(y / 2);
const cz = Math.cos(z / 2), sz = Math.sin(z / 2);
return [
sx * cy * cz - cx * sy * sz,
cx * sy * cz + sx * cy * sz,
cx * cy * sz - sx * sy * cz,
cx * cy * cz + sx * sy * sz,
];
}
function hexToRgb(hex) {
const n = parseInt(hex.replace("#", ""), 16);
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255, 1];
}
const bySystem = new Map();
for (const s of structures) {
if (!bySystem.has(s.system)) bySystem.set(s.system, []);
bySystem.get(s.system).push(s);
}
const systemColors = {
esqueletico: "#e8e2d0",
muscular: "#b5433a",
nervoso: "#f2c14e",
cardiovascular: "#c1121f",
respiratorio: "#4f7cff",
tegumentar: "#e0a96d",
};
for (const [systemSlug, items] of bySystem) {
const doc = new Document();
const buffer = doc.createBuffer();
const scene = doc.createScene(systemSlug);
doc.getRoot().setDefaultScene(scene);
const material = doc
.createMaterial(systemSlug)
.setBaseColorFactor(hexToRgb(systemColors[systemSlug] ?? "#cccccc"))
.setRoughnessFactor(0.6)
.setMetallicFactor(0.05);
for (const item of items) {
const geo = buildGeometry(item.shape);
const posAccessor = doc
.createAccessor()
.setType("VEC3")
.setArray(geo.positions)
.setBuffer(buffer);
const normAccessor = doc
.createAccessor()
.setType("VEC3")
.setArray(geo.normals)
.setBuffer(buffer);
const idxAccessor = doc
.createAccessor()
.setType("SCALAR")
.setArray(geo.indices)
.setBuffer(buffer);
const prim = doc
.createPrimitive()
.setAttribute("POSITION", posAccessor)
.setAttribute("NORMAL", normAccessor)
.setIndices(idxAccessor)
.setMaterial(material);
const mesh = doc.createMesh(item.slug).addPrimitive(prim);
const node = doc
.createNode(item.slug)
.setMesh(mesh)
.setTranslation(item.position)
.setRotation(eulerToQuat(item.rotation ?? [0, 0, 0]));
scene.addChild(node);
}
const io = new NodeIO();
const outFile = path.join(outDir, `${systemSlug}.glb`);
await io.write(outFile, doc);
console.log(`✓ ${outFile} (${items.length} estruturas)`);
}
+130
View File
@@ -0,0 +1,130 @@
// Minimal parametric mesh generators (box/sphere/cylinder), dependency-free.
// Returns { positions: Float32Array, normals: Float32Array, indices: Uint32Array }.
export function box(w, h, d) {
const x = w / 2, y = h / 2, z = d / 2;
const positions = [];
const normals = [];
const indices = [];
const faces = [
// [normal, 4 corners CCW]
[[0, 0, 1], [[-x, -y, z], [x, -y, z], [x, y, z], [-x, y, z]]],
[[0, 0, -1], [[x, -y, -z], [-x, -y, -z], [-x, y, -z], [x, y, -z]]],
[[0, 1, 0], [[-x, y, z], [x, y, z], [x, y, -z], [-x, y, -z]]],
[[0, -1, 0], [[-x, -y, -z], [x, -y, -z], [x, -y, z], [-x, -y, z]]],
[[1, 0, 0], [[x, -y, z], [x, -y, -z], [x, y, -z], [x, y, z]]],
[[-1, 0, 0], [[-x, -y, -z], [-x, -y, z], [-x, y, z], [-x, y, -z]]],
];
for (const [n, corners] of faces) {
const base = positions.length / 3;
for (const c of corners) {
positions.push(...c);
normals.push(...n);
}
indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
}
return {
positions: new Float32Array(positions),
normals: new Float32Array(normals),
indices: new Uint32Array(indices),
};
}
export function sphere(radius, widthSegments = 16, heightSegments = 12) {
const positions = [];
const normals = [];
const indices = [];
for (let y = 0; y <= heightSegments; y++) {
const v = y / heightSegments;
const theta = v * Math.PI;
for (let x = 0; x <= widthSegments; x++) {
const u = x / widthSegments;
const phi = u * Math.PI * 2;
const nx = -Math.cos(phi) * Math.sin(theta);
const ny = Math.cos(theta);
const nz = Math.sin(phi) * Math.sin(theta);
positions.push(radius * nx, radius * ny, radius * nz);
normals.push(nx, ny, nz);
}
}
const rowSize = widthSegments + 1;
for (let y = 0; y < heightSegments; y++) {
for (let x = 0; x < widthSegments; x++) {
const a = y * rowSize + x;
const b = a + rowSize;
indices.push(a, b, a + 1, b, b + 1, a + 1);
}
}
return {
positions: new Float32Array(positions),
normals: new Float32Array(normals),
indices: new Uint32Array(indices),
};
}
export function cylinder(radiusTop, radiusBottom, height, radialSegments = 12) {
const positions = [];
const normals = [];
const indices = [];
const halfH = height / 2;
// Side wall.
for (let y = 0; y <= 1; y++) {
const r = y === 0 ? radiusTop : radiusBottom;
const py = y === 0 ? halfH : -halfH;
for (let x = 0; x <= radialSegments; x++) {
const u = (x / radialSegments) * Math.PI * 2;
const nx = Math.sin(u);
const nz = Math.cos(u);
positions.push(r * nx, py, r * nz);
const slope = (radiusBottom - radiusTop) / height;
const len = Math.sqrt(1 + slope * slope);
normals.push(nx / len, -slope / len, nz / len);
}
}
const rowSize = radialSegments + 1;
for (let x = 0; x < radialSegments; x++) {
const a = x, b = a + rowSize;
indices.push(a, b, a + 1, b, b + 1, a + 1);
}
// Caps.
function cap(y, r, py, flip) {
const base = positions.length / 3;
const ny = flip ? 1 : -1;
positions.push(0, py, 0);
normals.push(0, ny, 0);
for (let x = 0; x <= radialSegments; x++) {
const u = (x / radialSegments) * Math.PI * 2;
positions.push(r * Math.sin(u), py, r * Math.cos(u));
normals.push(0, ny, 0);
}
for (let x = 0; x < radialSegments; x++) {
if (flip) indices.push(base, base + x + 2, base + x + 1);
else indices.push(base, base + x + 1, base + x + 2);
}
}
if (radiusTop > 0) cap(0, radiusTop, halfH, true);
if (radiusBottom > 0) cap(1, radiusBottom, -halfH, false);
return {
positions: new Float32Array(positions),
normals: new Float32Array(normals),
indices: new Uint32Array(indices),
};
}
export function buildGeometry(shape) {
switch (shape.type) {
case "box": return box(shape.w, shape.h, shape.d);
case "sphere": return sphere(shape.r);
case "cylinder": return cylinder(shape.rt, shape.rb, shape.h);
default: throw new Error(`Unknown shape type: ${shape.type}`);
}
}
+98
View File
@@ -0,0 +1,98 @@
// Populates the database from data-pipeline/seed/manifest.json.
// Run after `npm run build:seed-models` and after backend Prisma migrations
// have been applied (this script uses plain SQL, not Prisma, so the
// data-pipeline stays independent of the backend's toolchain).
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import pg from "pg";
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 connectionString = process.env.DATABASE_URL;
if (!connectionString) {
console.error("DATABASE_URL não definida.");
process.exit(1);
}
const client = new pg.Client({ connectionString });
await client.connect();
// The "api" service applies Prisma migrations on startup; if this pipeline
// runs first (e.g. right after `docker compose up`), wait for the table.
async function waitForTable(name, maxAttempts = 30) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const { rows } = await client.query(
`SELECT to_regclass($1) AS exists`,
[name],
);
if (rows[0]?.exists) return;
console.log(`Aguardando migrations do backend criarem "${name}" (${attempt}/${maxAttempts})...`);
await new Promise((r) => setTimeout(r, 2000));
}
throw new Error(`Tabela "${name}" não apareceu — rode "docker compose up -d api" antes do pipeline.`);
}
await waitForTable("structures");
function cuid(prefix) {
return `${prefix}_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`;
}
try {
await client.query("BEGIN");
const systemIds = new Map();
for (const s of systems) {
const id = cuid("sys");
systemIds.set(s.slug, id);
await client.query(
`INSERT INTO anatomical_systems (id, slug, "namePt", "nameEn", "colorHex", "order")
VALUES ($1,$2,$3,$4,$5,$6)
ON CONFLICT (slug) DO UPDATE SET "namePt"=$3, "nameEn"=$4, "colorHex"=$5, "order"=$6
RETURNING id`,
[id, s.slug, s.namePt, s.nameEn, s.colorHex, s.order],
);
}
// Re-read ids (in case of ON CONFLICT they may differ from freshly generated ones).
const { rows: existingSystems } = await client.query(
`SELECT id, slug FROM anatomical_systems`,
);
for (const row of existingSystems) systemIds.set(row.slug, row.id);
for (const st of structures) {
const systemId = systemIds.get(st.system);
if (!systemId) throw new Error(`Sistema desconhecido: ${st.system}`);
await client.query(
`INSERT INTO structures (id, "fmaId", slug, "namePt", "nameEn", synonyms, description, "regionPath", "modelFile", "meshName", "systemId", "updatedAt")
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11, now())
ON CONFLICT (slug) DO UPDATE SET
"namePt"=$4, "nameEn"=$5, synonyms=$6, description=$7, "regionPath"=$8,
"modelFile"=$9, "meshName"=$10, "systemId"=$11, "updatedAt"=now()`,
[
cuid("str"),
null,
st.slug,
st.namePt,
st.nameEn,
st.synonyms ?? [],
st.description ?? null,
st.regionPath ?? [],
`${st.system}.glb`,
st.slug,
systemId,
],
);
}
await client.query("COMMIT");
console.log(`Seed concluído: ${systems.length} sistemas, ${structures.length} estruturas.`);
} catch (err) {
await client.query("ROLLBACK");
console.error(err);
process.exit(1);
} finally {
await client.end();
}
+165
View File
@@ -0,0 +1,165 @@
// 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.
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 = {}) {
structures.push({
system,
slug,
namePt,
nameEn,
synonyms: opts.synonyms ?? [],
description: opts.description ?? "",
regionPath: region,
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),
);
console.log(`manifest.json gerado com ${structures.length} estruturas em ${systems.length} sistemas.`);