first commit
This commit is contained in:
commit
0e854110d3
49 files changed
+2292
No files matched your search
@@ -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)`);
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user