tegumentar2
This commit is contained in:
1 parent
077e451902
commit
063bd629c2
4 files changed
+204
-75
No files matched your search
@@ -63,7 +63,12 @@ for (const [systemSlug, items] of bySystem) {
|
|||||||
.createMaterial(hex)
|
.createMaterial(hex)
|
||||||
.setBaseColorFactor(hexToRgb(hex))
|
.setBaseColorFactor(hexToRgb(hex))
|
||||||
.setRoughnessFactor(0.6)
|
.setRoughnessFactor(0.6)
|
||||||
.setMetallicFactor(0.05);
|
.setMetallicFactor(0.05)
|
||||||
|
// O bloco de pele usa caixas com a face da frente removida (ver
|
||||||
|
// openFrontBox em geometry.mjs) pra dar pra ver o conteúdo por
|
||||||
|
// dentro — sem dupla face, as paredes de trás/lados ficariam
|
||||||
|
// invisíveis quando vistas "de dentro" da caixa aberta.
|
||||||
|
.setDoubleSided(true);
|
||||||
materialCache.set(hex, mat);
|
materialCache.set(hex, mat);
|
||||||
return mat;
|
return mat;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
// Minimal parametric mesh generators (box/sphere/cylinder), dependency-free.
|
// Minimal parametric mesh generators (box/sphere/cylinder), dependency-free.
|
||||||
// Returns { positions: Float32Array, normals: Float32Array, indices: Uint32Array }.
|
// Returns { positions: Float32Array, normals: Float32Array, indices: Uint32Array }.
|
||||||
|
|
||||||
export function box(w, h, d) {
|
// skipNormals: normais (como "0,-1,0") das faces a NÃO gerar — usado pelo
|
||||||
|
// bloco de pele em corte (openFrontBox abaixo) pra deixar a face da frente
|
||||||
|
// aberta, como uma caixa de diorama, revelando o conteúdo interno em vez de
|
||||||
|
// escondê-lo atrás de uma parede opaca.
|
||||||
|
export function box(w, h, d, skipNormals = []) {
|
||||||
const x = w / 2, y = h / 2, z = d / 2;
|
const x = w / 2, y = h / 2, z = d / 2;
|
||||||
const positions = [];
|
const positions = [];
|
||||||
const normals = [];
|
const normals = [];
|
||||||
@@ -17,7 +21,10 @@ export function box(w, h, d) {
|
|||||||
[[-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]]],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const skip = new Set(skipNormals.map((n) => n.join(",")));
|
||||||
|
|
||||||
for (const [n, corners] of faces) {
|
for (const [n, corners] of faces) {
|
||||||
|
if (skip.has(n.join(","))) continue;
|
||||||
const base = positions.length / 3;
|
const base = positions.length / 3;
|
||||||
for (const c of corners) {
|
for (const c of corners) {
|
||||||
positions.push(...c);
|
positions.push(...c);
|
||||||
@@ -33,6 +40,14 @@ export function box(w, h, d) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Caixa "de diorama": todas as faces normais, exceto a de baixo em Y
|
||||||
|
// (nosso eixo "frente/trás" no bloco de pele) — deixa a frente aberta pra
|
||||||
|
// dar pra ver o conteúdo (folículos, glândulas, vasos) por dentro, em vez
|
||||||
|
// de uma parede opaca escondendo tudo.
|
||||||
|
export function openFrontBox(w, h, d) {
|
||||||
|
return box(w, h, d, [[0, -1, 0]]);
|
||||||
|
}
|
||||||
|
|
||||||
export function sphere(radius, widthSegments = 16, heightSegments = 12) {
|
export function sphere(radius, widthSegments = 16, heightSegments = 12) {
|
||||||
const positions = [];
|
const positions = [];
|
||||||
const normals = [];
|
const normals = [];
|
||||||
@@ -123,6 +138,7 @@ export function cylinder(radiusTop, radiusBottom, height, radialSegments = 12) {
|
|||||||
export function buildGeometry(shape) {
|
export function buildGeometry(shape) {
|
||||||
switch (shape.type) {
|
switch (shape.type) {
|
||||||
case "box": return box(shape.w, shape.h, shape.d);
|
case "box": return box(shape.w, shape.h, shape.d);
|
||||||
|
case "openFrontBox": return openFrontBox(shape.w, shape.h, shape.d);
|
||||||
case "sphere": return sphere(shape.r);
|
case "sphere": return sphere(shape.r);
|
||||||
case "cylinder": return cylinder(shape.rt, shape.rb, shape.h);
|
case "cylinder": return cylinder(shape.rt, shape.rb, shape.h);
|
||||||
default: throw new Error(`Unknown shape type: ${shape.type}`);
|
default: throw new Error(`Unknown shape type: ${shape.type}`);
|
||||||
|
|||||||
@@ -1,28 +1,24 @@
|
|||||||
// Gera manifest-tegumentar.json: o Z-Anatomy-Sample não inclui camadas de
|
// Gera manifest-tegumentar.json: o Z-Anatomy-Sample não inclui camadas de
|
||||||
// pele (epiderme/derme/hipoderme) nem seus anexos (folículo piloso, glândulas
|
// pele (epiderme/derme/hipoderme) nem seus anexos (folículo piloso, glândulas
|
||||||
// etc.) — são estruturas microscópicas, nenhum dataset de corpo inteiro as
|
// etc.) — confirmado consultando a listagem completa de
|
||||||
|
// Assets/Models/1.0 Models no repositório (só Skeletal, Muscular, Nervous,
|
||||||
|
// CardioVascular, Visceral, Joints, LymphoidOrgans, Regions e Reference
|
||||||
|
// lines) — são estruturas microscópicas, nenhum dataset de corpo inteiro as
|
||||||
// modela como malha. Por isso o sistema tegumentar (só ele — os demais
|
// modela como malha. Por isso o sistema tegumentar (só ele — os demais
|
||||||
// sistemas continuam vindo de malhas reais via scripts/extract-manifest.mjs,
|
// sistemas continuam vindo de malhas reais via scripts/extract-manifest.mjs,
|
||||||
// sem nenhuma mudança) usa um "bloco" esquemático construído com geometria
|
// sem nenhuma mudança) usa um "bloco" esquemático construído com geometria
|
||||||
// paramétrica simples (ver scripts/geometry.mjs e scripts/build-seed-models.mjs,
|
// paramétrica simples (ver scripts/geometry.mjs e scripts/build-seed-models.mjs,
|
||||||
// também exclusivos do tegumentar — só o script "build:tegumentar" os chama).
|
// também exclusivos do tegumentar — só o script "build:tegumentar" os chama).
|
||||||
//
|
//
|
||||||
// MODELO EM DEGRAUS ("staircase cutaway"), como os blocos didáticos de pele
|
// MODELO "DIORAMA": uma caixa sólida (as 3 camadas empilhadas, cada uma uma
|
||||||
// usados em aula: cada camada mais profunda tem uma extensão maior no eixo Y
|
// openFrontBox — ver geometry.mjs) com a face da frente removida, como uma
|
||||||
// do que a camada acima dela, então a camada de cima NÃO cobre a de baixo
|
// caixa de vidro sem uma parede, no estilo do desenho clássico de corte de
|
||||||
// inteira — sobra um "degrau" onde a superfície da camada mais profunda fica
|
// pele (Netter/Sobotta): de cima dá pra ver a superfície da epiderme com os
|
||||||
// exposta e visível de cima, em vez de tudo empilhado e escondido atrás da
|
// pelos saindo; do lado dá pra ver a "banda" de cor de cada camada; e pela
|
||||||
// camada mais externa (o problema do modelo anterior, que empilhava 3 caixas
|
// frente aberta dá pra ver os componentes (folículos, glândulas, vasos,
|
||||||
// finas exatamente uma sobre a outra).
|
// corpúsculos) soltos dentro do volume da derme/hipoderme, sem nada opaco
|
||||||
// epiderme: ████ (degrau 1 — só ela)
|
// na frente escondendo-os. Gire o modelo com o mouse pra explorar de vários
|
||||||
// derme: ████ ████ (degraus 1+2 — expõe o degrau 2)
|
// ângulos.
|
||||||
// hipoderme: ████ ████ ████ (degraus 1+2+3 — expõe o degrau 3)
|
|
||||||
// A parede vertical no degrau 1 (voltada para -Y) mostra as 3 camadas
|
|
||||||
// empilhadas de uma vez, como um corte transversal — é ali na frente que
|
|
||||||
// ficam os componentes (pelo, glândulas, vasos, corpúsculos), flutuando bem
|
|
||||||
// próximos da parede (fora do bloco opaco, pra nunca ficarem escondidos
|
|
||||||
// dentro dele) e alinhados em X e Z com a camada a que pertencem. Gire o
|
|
||||||
// modelo com o mouse pra ver o bloco de todos os ângulos.
|
|
||||||
import { writeFileSync } from "node:fs";
|
import { writeFileSync } from "node:fs";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
@@ -31,18 +27,11 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|||||||
|
|
||||||
const structures = [];
|
const structures = [];
|
||||||
|
|
||||||
// O Z-Anatomy-Sample (verificado na íntegra em Assets/Models/1.0 Models,
|
|
||||||
// via API do GitHub) NÃO tem nenhum arquivo de pele/tegumento — só
|
|
||||||
// SkeletalSystem, MuscularSystem, NervousSystem, CardioVascular,
|
|
||||||
// VisceralSystem, Joints, LymphoidOrgans, Regions e Reference lines —
|
|
||||||
// confirma o que já estava documentado: não existe dataset de corpo
|
|
||||||
// inteiro com camadas de pele, daqui vem a necessidade do bloco esquemático.
|
|
||||||
//
|
|
||||||
// Todo o desenho abaixo é escrito em "unidades de projeto" (a escala
|
// Todo o desenho abaixo é escrito em "unidades de projeto" (a escala
|
||||||
// original, pensada peça por peça) e só na hora de gerar a estrutura final
|
// original, pensada peça por peça) e só na hora de gerar a estrutura final
|
||||||
// o add() aplica o fator SCALE — assim dá pra aumentar/diminuir o bloco
|
// o add() aplica o fator SCALE — assim dá pra aumentar/diminuir o bloco
|
||||||
// inteiro (pedido do usuário: "aumente o tamanho dele") mudando um único
|
// inteiro mudando um único número, sem recalcular à mão a posição de cada
|
||||||
// número, sem precisar recalcular à mão a posição de cada uma das ~20 peças.
|
// uma das peças.
|
||||||
const SCALE = 3;
|
const SCALE = 3;
|
||||||
|
|
||||||
function scaleShape(shape) {
|
function scaleShape(shape) {
|
||||||
@@ -78,7 +67,7 @@ function add(system, slug, namePt, nameEn, region, shape, position, opts = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const TX = ["Sistema Tegumentar", "Corte Transversal da Pele (em Degraus)"];
|
const TX = ["Sistema Tegumentar", "Corte Transversal da Pele (Diorama)"];
|
||||||
|
|
||||||
// Centro/âncora do bloco (coordenadas do mundo, mesma escala do corpo
|
// Centro/âncora do bloco (coordenadas do mundo, mesma escala do corpo
|
||||||
// Z-Anatomy — colado na face do antebraço, como antes).
|
// Z-Anatomy — colado na face do antebraço, como antes).
|
||||||
@@ -86,71 +75,69 @@ const CX = 0.24;
|
|||||||
const CY = 0.85;
|
const CY = 0.85;
|
||||||
|
|
||||||
const BLOCK_W = 0.14; // extensão em X (largura do bloco)
|
const BLOCK_W = 0.14; // extensão em X (largura do bloco)
|
||||||
const STEP = 0.05; // profundidade em Y de cada degrau
|
const Y_DEPTH = 0.12; // profundidade em Y (frente-trás) — a mesma pras 3 camadas
|
||||||
const Y_FRONT = CY - 1.5 * STEP; // borda frontal do bloco (degrau 1), onde fica a parede de corte
|
const Y_FRONT = CY - Y_DEPTH / 2; // onde fica a abertura (face removida)
|
||||||
|
|
||||||
// Espessura (eixo Z) de cada camada e a profundidade onde cada uma começa/
|
// Espessura (eixo Z) de cada camada e a profundidade onde cada uma começa/
|
||||||
// termina — definidas uma vez aqui pra tudo (camadas E componentes) usar os
|
// termina — definidas uma vez aqui pra tudo (camadas E componentes) usar os
|
||||||
// mesmos números e não desalinhar.
|
// mesmos números e não desalinhar. Proporções exageradas (derme/hipoderme
|
||||||
|
// bem mais grossas que na realidade) de propósito, pra caber e destacar os
|
||||||
|
// componentes — como em qualquer diagrama didático de pele.
|
||||||
const Z_TOP = 0.038; // superfície da epiderme
|
const Z_TOP = 0.038; // superfície da epiderme
|
||||||
const D_EPI = 0.005;
|
const D_EPI = 0.012;
|
||||||
const D_DERME = 0.012;
|
const D_DERME = 0.035;
|
||||||
const D_HIPO = 0.016;
|
const D_HIPO = 0.053;
|
||||||
const Z_EPI_BOTTOM = Z_TOP - D_EPI; // 0.033 = limite epiderme/derme
|
const Z_EPI_BOTTOM = Z_TOP - D_EPI; // limite epiderme/derme
|
||||||
const Z_DERME_BOTTOM = Z_EPI_BOTTOM - D_DERME; // 0.021 = limite derme/hipoderme
|
const Z_DERME_BOTTOM = Z_EPI_BOTTOM - D_DERME; // limite derme/hipoderme
|
||||||
const Z_HIPO_BOTTOM = Z_DERME_BOTTOM - D_HIPO; // 0.005 = base do bloco
|
const Z_HIPO_BOTTOM = Z_DERME_BOTTOM - D_HIPO; // base do bloco
|
||||||
|
|
||||||
// Os componentes (pelo, glândulas, vasos, corpúsculos...) ficam flutuando
|
|
||||||
// bem à frente da parede de corte (fora do volume opaco do bloco, pra nunca
|
|
||||||
// serem encobertos por ele) — cada "coluna" X é um componente diferente,
|
|
||||||
// como um desenho de corte histológico visto de frente.
|
|
||||||
const FRONT_Y = Y_FRONT - 0.01;
|
|
||||||
const ROT_Z = [Math.PI / 2, 0, 0]; // deixa cilindros na vertical (eixo nasce em Y, isso gira pra Z)
|
const ROT_Z = [Math.PI / 2, 0, 0]; // deixa cilindros na vertical (eixo nasce em Y, isso gira pra Z)
|
||||||
|
|
||||||
// ---- As 3 camadas da pele, em degraus ----
|
// ---- As 3 camadas da pele, como uma caixa de diorama (frente aberta) ----
|
||||||
add(
|
add(
|
||||||
"tegumentar", "epiderme", "Epiderme", "Epidermis", [...TX],
|
"tegumentar", "epiderme", "Epiderme", "Epidermis", [...TX],
|
||||||
{ type: "box", w: BLOCK_W, h: STEP, d: D_EPI },
|
{ type: "openFrontBox", w: BLOCK_W, h: Y_DEPTH, d: D_EPI },
|
||||||
[CX, Y_FRONT + STEP / 2, Z_TOP - D_EPI / 2],
|
[CX, CY, Z_TOP - D_EPI / 2],
|
||||||
{
|
{
|
||||||
color: "#e0a96d",
|
color: "#e0a96d",
|
||||||
description:
|
description:
|
||||||
"Camada mais externa e avascular da pele, formada por epitélio pavimentoso estratificado queratinizado; sua função é a principal barreira contra perda de água, atrito, micro-organismos e raios UV (com a ajuda da melanina).",
|
"Camada mais externa e avascular da pele, formada por epitélio pavimentoso estratificado queratinizado; sua função é a principal barreira contra perda de água, atrito, micro-organismos e raios UV (com a ajuda da melanina). É a face de cima do bloco, de onde saem os pelos.",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
add(
|
add(
|
||||||
"tegumentar", "derme", "Derme", "Dermis", [...TX],
|
"tegumentar", "derme", "Derme", "Dermis", [...TX],
|
||||||
{ type: "box", w: BLOCK_W, h: 2 * STEP, d: D_DERME },
|
{ type: "openFrontBox", w: BLOCK_W, h: Y_DEPTH, d: D_DERME },
|
||||||
[CX, Y_FRONT + STEP, Z_EPI_BOTTOM - D_DERME / 2],
|
[CX, CY, Z_EPI_BOTTOM - D_DERME / 2],
|
||||||
{
|
{
|
||||||
color: "#c97b4a",
|
color: "#c97b4a",
|
||||||
description:
|
description:
|
||||||
"Camada de tecido conjuntivo rica em colágeno e elastina, vasos sanguíneos, terminações nervosas e os anexos cutâneos (folículos pilosos, glândulas); sustenta a epiderme e dá à pele resistência e elasticidade. Repare no degrau exposto: é a derme aparecendo onde a epiderme já não cobre mais.",
|
"Camada de tecido conjuntivo rica em colágeno e elastina, vasos sanguíneos, terminações nervosas e os anexos cutâneos (folículos pilosos, glândulas); sustenta a epiderme e dá à pele resistência e elasticidade. Visível como a faixa do meio no lado do bloco.",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
add(
|
add(
|
||||||
"tegumentar", "hipoderme", "Hipoderme (Tecido Subcutâneo)", "Hypodermis (Subcutaneous Tissue)", [...TX],
|
"tegumentar", "hipoderme", "Hipoderme (Tecido Subcutâneo)", "Hypodermis (Subcutaneous Tissue)", [...TX],
|
||||||
{ type: "box", w: BLOCK_W, h: 3 * STEP, d: D_HIPO },
|
{ type: "openFrontBox", w: BLOCK_W, h: Y_DEPTH, d: D_HIPO },
|
||||||
[CX, CY, Z_DERME_BOTTOM - D_HIPO / 2],
|
[CX, CY, Z_DERME_BOTTOM - D_HIPO / 2],
|
||||||
{
|
{
|
||||||
color: "#f2d4a3",
|
color: "#f2d4a3",
|
||||||
description:
|
description:
|
||||||
"Camada mais profunda, rica em tecido adiposo; isola termicamente, absorve impactos e serve de reserva de energia — não é tecnicamente parte da pele, mas a conecta aos músculos e ossos subjacentes. É o degrau mais largo, exposto onde nem epiderme nem derme mais chegam.",
|
"Camada mais profunda, rica em tecido adiposo; isola termicamente, absorve impactos e serve de reserva de energia — não é tecnicamente parte da pele, mas a conecta aos músculos e ossos subjacentes. É a faixa mais grossa, na base do bloco.",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---- 6 "colunas" de componentes, cada uma numa posição X própria, todas
|
// ---- 6 "colunas" de componentes, cada uma numa posição X própria, dentro
|
||||||
// flutuando no mesmo plano frontal (FRONT_Y) — a altura (Z) de cada peça
|
// do volume aberto do bloco — a altura (Z) de cada peça já indica em qual
|
||||||
// já indica em qual camada ela vive. ----
|
// camada ela vive; um leve desvio em Y dá sensação de profundidade. ----
|
||||||
const COL = (i) => CX - 0.06 + i * 0.024; // 6 colunas igualmente espaçadas
|
const COL = (i) => CX - 0.06 + i * 0.024; // 6 colunas igualmente espaçadas
|
||||||
|
|
||||||
// ---- Coluna 1: complexo pilossebáceo (pelo + folículo + músculo eretor + glândula sebácea) ----
|
// ---- Coluna 1: complexo pilossebáceo (pelo + folículo + músculo eretor + glândula sebácea) ----
|
||||||
const HX = COL(0);
|
const HX = COL(0);
|
||||||
|
const HY = CY - 0.02;
|
||||||
const HAIR_TILT = [Math.PI / 2 + 0.3, 0.2, 0];
|
const HAIR_TILT = [Math.PI / 2 + 0.3, 0.2, 0];
|
||||||
|
|
||||||
add(
|
add(
|
||||||
"tegumentar", "pelo", "Pelo (Haste Capilar)", "Hair Shaft", [...TX, "Complexo Pilossebáceo"],
|
"tegumentar", "pelo", "Pelo (Haste Capilar)", "Hair Shaft", [...TX, "Complexo Pilossebáceo"],
|
||||||
{ type: "cylinder", rt: 0.0006, rb: 0.001, h: 0.02 }, [HX, FRONT_Y, Z_TOP + 0.01],
|
{ type: "cylinder", rt: 0.0006, rb: 0.001, h: 0.02 }, [HX, HY, Z_TOP + 0.01],
|
||||||
{
|
{
|
||||||
color: "#4a3527",
|
color: "#4a3527",
|
||||||
rotation: HAIR_TILT,
|
rotation: HAIR_TILT,
|
||||||
@@ -160,7 +147,7 @@ add(
|
|||||||
);
|
);
|
||||||
add(
|
add(
|
||||||
"tegumentar", "foliculo-piloso", "Folículo Piloso", "Hair Follicle", [...TX, "Complexo Pilossebáceo"],
|
"tegumentar", "foliculo-piloso", "Folículo Piloso", "Hair Follicle", [...TX, "Complexo Pilossebáceo"],
|
||||||
{ type: "cylinder", rt: 0.0022, rb: 0.0022, h: 0.03 }, [HX, FRONT_Y, 0.023],
|
{ type: "cylinder", rt: 0.0022, rb: 0.0022, h: 0.058 }, [HX, HY, 0.009],
|
||||||
{
|
{
|
||||||
color: "#8a6a52",
|
color: "#8a6a52",
|
||||||
rotation: HAIR_TILT,
|
rotation: HAIR_TILT,
|
||||||
@@ -170,7 +157,7 @@ add(
|
|||||||
);
|
);
|
||||||
add(
|
add(
|
||||||
"tegumentar", "bulbo-piloso", "Bulbo Piloso", "Hair Bulb", [...TX, "Complexo Pilossebáceo"],
|
"tegumentar", "bulbo-piloso", "Bulbo Piloso", "Hair Bulb", [...TX, "Complexo Pilossebáceo"],
|
||||||
{ type: "sphere", r: 0.003 }, [HX - 0.004, FRONT_Y, 0.008],
|
{ type: "sphere", r: 0.004 }, [HX - 0.005, HY, -0.02],
|
||||||
{
|
{
|
||||||
color: "#6b4c38",
|
color: "#6b4c38",
|
||||||
description:
|
description:
|
||||||
@@ -179,7 +166,7 @@ add(
|
|||||||
);
|
);
|
||||||
add(
|
add(
|
||||||
"tegumentar", "musculo-eretor-do-pelo", "Músculo Eretor do Pelo", "Arrector Pili Muscle", [...TX, "Complexo Pilossebáceo"],
|
"tegumentar", "musculo-eretor-do-pelo", "Músculo Eretor do Pelo", "Arrector Pili Muscle", [...TX, "Complexo Pilossebáceo"],
|
||||||
{ type: "box", w: 0.0012, h: 0.014, d: 0.003 }, [HX + 0.004, FRONT_Y, 0.0285],
|
{ type: "box", w: 0.0012, h: 0.014, d: 0.003 }, [HX + 0.005, HY, 0.017],
|
||||||
{
|
{
|
||||||
color: "#b5433a",
|
color: "#b5433a",
|
||||||
rotation: [1.05, 0, 0],
|
rotation: [1.05, 0, 0],
|
||||||
@@ -189,7 +176,7 @@ add(
|
|||||||
);
|
);
|
||||||
add(
|
add(
|
||||||
"tegumentar", "glandula-sebacea", "Glândula Sebácea", "Sebaceous Gland", [...TX, "Complexo Pilossebáceo"],
|
"tegumentar", "glandula-sebacea", "Glândula Sebácea", "Sebaceous Gland", [...TX, "Complexo Pilossebáceo"],
|
||||||
{ type: "sphere", r: 0.004 }, [HX + 0.007, FRONT_Y, 0.031],
|
{ type: "sphere", r: 0.005 }, [HX + 0.008, HY, 0.022],
|
||||||
{
|
{
|
||||||
color: "#f2c14e",
|
color: "#f2c14e",
|
||||||
description:
|
description:
|
||||||
@@ -197,11 +184,34 @@ add(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ---- Fios de pelo extras, só decorativos (mesma descrição do "pelo"), pra
|
||||||
|
// a superfície do bloco não ficar com um pelo solitário — no desenho de
|
||||||
|
// referência vários pelos saem da epiderme ao mesmo tempo. ----
|
||||||
|
const EXTRA_HAIRS = [
|
||||||
|
[COL(1) + 0.01, CY - 0.03],
|
||||||
|
[COL(2) - 0.008, CY - 0.015],
|
||||||
|
[COL(4) + 0.006, CY - 0.025],
|
||||||
|
];
|
||||||
|
EXTRA_HAIRS.forEach(([x, y], i) => {
|
||||||
|
add(
|
||||||
|
"tegumentar", `pelo-extra-${i + 1}`, "Pelo (Haste Capilar)", "Hair Shaft",
|
||||||
|
[...TX, "Complexo Pilossebáceo"],
|
||||||
|
{ type: "cylinder", rt: 0.0006, rb: 0.001, h: 0.02 }, [x, y, Z_TOP + 0.01],
|
||||||
|
{
|
||||||
|
color: "#4a3527",
|
||||||
|
rotation: [Math.PI / 2 + (i - 1) * 0.15, 0.15, 0],
|
||||||
|
description:
|
||||||
|
"Porção do pelo visível acima da superfície da pele, formada por células queratinizadas mortas; sua cor vem da melanina produzida no bulbo piloso.",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// ---- Coluna 2: glândula sudorípara (écrina) + ducto + poro ----
|
// ---- Coluna 2: glândula sudorípara (écrina) + ducto + poro ----
|
||||||
const SX = COL(1);
|
const SX = COL(1);
|
||||||
|
const SY = CY;
|
||||||
add(
|
add(
|
||||||
"tegumentar", "glandula-sudoripara", "Glândula Sudorípara (Écrina)", "Eccrine Sweat Gland", [...TX, "Glândula Sudorípara"],
|
"tegumentar", "glandula-sudoripara", "Glândula Sudorípara (Écrina)", "Eccrine Sweat Gland", [...TX, "Glândula Sudorípara"],
|
||||||
{ type: "sphere", r: 0.005 }, [SX, FRONT_Y, 0.012],
|
{ type: "sphere", r: 0.006 }, [SX, SY, -0.045],
|
||||||
{
|
{
|
||||||
color: "#4f7cff",
|
color: "#4f7cff",
|
||||||
description:
|
description:
|
||||||
@@ -210,7 +220,7 @@ add(
|
|||||||
);
|
);
|
||||||
add(
|
add(
|
||||||
"tegumentar", "ducto-sudoriparo", "Ducto Sudoríparo", "Sweat Duct", [...TX, "Glândula Sudorípara"],
|
"tegumentar", "ducto-sudoriparo", "Ducto Sudoríparo", "Sweat Duct", [...TX, "Glândula Sudorípara"],
|
||||||
{ type: "cylinder", rt: 0.0012, rb: 0.0012, h: 0.026 }, [SX, FRONT_Y, 0.025],
|
{ type: "cylinder", rt: 0.0012, rb: 0.0012, h: 0.083 }, [SX, SY, -0.0035],
|
||||||
{
|
{
|
||||||
color: "#7ba6ff",
|
color: "#7ba6ff",
|
||||||
rotation: ROT_Z,
|
rotation: ROT_Z,
|
||||||
@@ -220,7 +230,7 @@ add(
|
|||||||
);
|
);
|
||||||
add(
|
add(
|
||||||
"tegumentar", "poro-sudoriparo", "Poro Sudoríparo", "Sweat Pore", [...TX, "Glândula Sudorípara"],
|
"tegumentar", "poro-sudoriparo", "Poro Sudoríparo", "Sweat Pore", [...TX, "Glândula Sudorípara"],
|
||||||
{ type: "sphere", r: 0.0012 }, [SX, FRONT_Y, Z_TOP + 0.0005],
|
{ type: "sphere", r: 0.0012 }, [SX, SY, Z_TOP + 0.0005],
|
||||||
{
|
{
|
||||||
color: "#a9c6ff",
|
color: "#a9c6ff",
|
||||||
description: "Abertura do ducto sudoríparo na superfície da epiderme, por onde o suor é liberado.",
|
description: "Abertura do ducto sudoríparo na superfície da epiderme, por onde o suor é liberado.",
|
||||||
@@ -229,9 +239,10 @@ add(
|
|||||||
|
|
||||||
// ---- Coluna 3: plexo vascular dérmico ----
|
// ---- Coluna 3: plexo vascular dérmico ----
|
||||||
const VX = COL(2);
|
const VX = COL(2);
|
||||||
|
const VY = CY - 0.01;
|
||||||
add(
|
add(
|
||||||
"tegumentar", "arteriola-dermica", "Arteríola Dérmica", "Dermal Arteriole", [...TX, "Plexo Vascular Dérmico"],
|
"tegumentar", "arteriola-dermica", "Arteríola Dérmica", "Dermal Arteriole", [...TX, "Plexo Vascular Dérmico"],
|
||||||
{ type: "cylinder", rt: 0.0012, rb: 0.0012, h: 0.01 }, [VX - 0.004, FRONT_Y, 0.026],
|
{ type: "cylinder", rt: 0.0012, rb: 0.0012, h: 0.02 }, [VX - 0.005, VY, 0.008],
|
||||||
{
|
{
|
||||||
color: "#c1121f",
|
color: "#c1121f",
|
||||||
rotation: ROT_Z,
|
rotation: ROT_Z,
|
||||||
@@ -240,7 +251,7 @@ add(
|
|||||||
);
|
);
|
||||||
add(
|
add(
|
||||||
"tegumentar", "venula-dermica", "Vênula Dérmica", "Dermal Venule", [...TX, "Plexo Vascular Dérmico"],
|
"tegumentar", "venula-dermica", "Vênula Dérmica", "Dermal Venule", [...TX, "Plexo Vascular Dérmico"],
|
||||||
{ type: "cylinder", rt: 0.0012, rb: 0.0012, h: 0.01 }, [VX + 0.004, FRONT_Y, 0.026],
|
{ type: "cylinder", rt: 0.0012, rb: 0.0012, h: 0.02 }, [VX + 0.005, VY, 0.008],
|
||||||
{
|
{
|
||||||
color: "#3457d5",
|
color: "#3457d5",
|
||||||
rotation: ROT_Z,
|
rotation: ROT_Z,
|
||||||
@@ -249,7 +260,7 @@ add(
|
|||||||
);
|
);
|
||||||
add(
|
add(
|
||||||
"tegumentar", "alca-capilar-dermica", "Alça Capilar Dérmica", "Dermal Capillary Loop", [...TX, "Plexo Vascular Dérmico"],
|
"tegumentar", "alca-capilar-dermica", "Alça Capilar Dérmica", "Dermal Capillary Loop", [...TX, "Plexo Vascular Dérmico"],
|
||||||
{ type: "cylinder", rt: 0.0006, rb: 0.0006, h: 0.006 }, [VX, FRONT_Y, 0.033],
|
{ type: "cylinder", rt: 0.0006, rb: 0.0006, h: 0.012 }, [VX, VY, 0.024],
|
||||||
{
|
{
|
||||||
color: "#e0464f",
|
color: "#e0464f",
|
||||||
rotation: ROT_Z,
|
rotation: ROT_Z,
|
||||||
@@ -260,7 +271,7 @@ add(
|
|||||||
// ---- Coluna 4: corpúsculo de Meissner (toque leve) ----
|
// ---- Coluna 4: corpúsculo de Meissner (toque leve) ----
|
||||||
add(
|
add(
|
||||||
"tegumentar", "corpusculo-de-meissner", "Corpúsculo de Meissner", "Meissner's Corpuscle", [...TX, "Receptores Sensoriais"],
|
"tegumentar", "corpusculo-de-meissner", "Corpúsculo de Meissner", "Meissner's Corpuscle", [...TX, "Receptores Sensoriais"],
|
||||||
{ type: "sphere", r: 0.0022 }, [COL(3), FRONT_Y, 0.033],
|
{ type: "sphere", r: 0.003 }, [COL(3), CY - 0.015, 0.024],
|
||||||
{
|
{
|
||||||
color: "#34d399",
|
color: "#34d399",
|
||||||
description:
|
description:
|
||||||
@@ -271,7 +282,7 @@ add(
|
|||||||
// ---- Coluna 5: corpúsculo de Pacini (pressão profunda) ----
|
// ---- Coluna 5: corpúsculo de Pacini (pressão profunda) ----
|
||||||
add(
|
add(
|
||||||
"tegumentar", "corpusculo-de-pacini", "Corpúsculo de Pacini", "Pacinian Corpuscle", [...TX, "Receptores Sensoriais"],
|
"tegumentar", "corpusculo-de-pacini", "Corpúsculo de Pacini", "Pacinian Corpuscle", [...TX, "Receptores Sensoriais"],
|
||||||
{ type: "sphere", r: 0.006 }, [COL(4), FRONT_Y, 0.012],
|
{ type: "sphere", r: 0.008 }, [COL(4), CY + 0.02, -0.045],
|
||||||
{
|
{
|
||||||
color: "#22d3ee",
|
color: "#22d3ee",
|
||||||
description:
|
description:
|
||||||
@@ -279,16 +290,18 @@ add(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---- Coluna 6: lóbulos de tecido adiposo da hipoderme ----
|
// ---- Coluna 6: lóbulos de tecido adiposo, espalhados pela hipoderme ----
|
||||||
const FX = COL(5);
|
const FX = COL(5);
|
||||||
|
const FY = CY + 0.01;
|
||||||
const FAT_OFFSETS = [
|
const FAT_OFFSETS = [
|
||||||
[0, 0.013], [0.007, 0.017], [-0.006, 0.010], [0.003, 0.019],
|
[0, -0.02], [0.012, -0.03], [-0.01, -0.025],
|
||||||
|
[0.005, -0.04], [-0.008, -0.045], [0.01, -0.052],
|
||||||
];
|
];
|
||||||
FAT_OFFSETS.forEach(([dx, dz], i) => {
|
FAT_OFFSETS.forEach(([dx, dz], i) => {
|
||||||
add(
|
add(
|
||||||
"tegumentar", `adipocito-${i + 1}`, `Lóbulo de Tecido Adiposo ${i + 1}`, `Adipose Tissue Lobule ${i + 1}`,
|
"tegumentar", `adipocito-${i + 1}`, `Lóbulo de Tecido Adiposo ${i + 1}`, `Adipose Tissue Lobule ${i + 1}`,
|
||||||
[...TX, "Tecido Adiposo"],
|
[...TX, "Tecido Adiposo"],
|
||||||
{ type: "sphere", r: 0.006 }, [FX + dx, FRONT_Y, dz],
|
{ type: "sphere", r: 0.007 }, [FX + dx, FY, dz],
|
||||||
{
|
{
|
||||||
color: "#f7e08c",
|
color: "#f7e08c",
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
|
||||||
import { useAtlasStore, type BodyRegion } from "../store/useAtlasStore.js";
|
import { useAtlasStore, type BodyRegion } from "../store/useAtlasStore.js";
|
||||||
|
|
||||||
const REGIONS: { region: BodyRegion; label: string }[] = [
|
const REGIONS: { region: BodyRegion; label: string }[] = [
|
||||||
@@ -11,18 +12,111 @@ const REGIONS: { region: BodyRegion; label: string }[] = [
|
|||||||
{ region: "corpo_inteiro", label: "Corpo Inteiro" },
|
{ region: "corpo_inteiro", label: "Corpo Inteiro" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const STORAGE_KEY = "atlas.quickRegionNav.pos";
|
||||||
|
|
||||||
|
function loadStoredPos(): { x: number; y: number } | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
return raw ? JSON.parse(raw) : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Navegação rápida por região — em vez de precisar acertar o clique numa
|
// Navegação rápida por região — em vez de precisar acertar o clique numa
|
||||||
// estrutura minúscula pra girar/aproximar a câmera manualmente, o usuário
|
// estrutura minúscula pra girar/aproximar a câmera manualmente, o usuário
|
||||||
// pede direto pra "ir" numa região do corpo.
|
// pede direto pra "ir" numa região do corpo. Arrastável pela alcinha (⠿),
|
||||||
|
// pois a barra pode cair em cima de uma parte do modelo que o usuário
|
||||||
|
// queira ver — a posição escolhida fica salva entre visitas.
|
||||||
export function QuickRegionNav() {
|
export function QuickRegionNav() {
|
||||||
const focusOnRegion = useAtlasStore((s) => s.focusOnRegion);
|
const focusOnRegion = useAtlasStore((s) => s.focusOnRegion);
|
||||||
|
const [pos, setPos] = useState<{ x: number; y: number } | null>(() => loadStoredPos());
|
||||||
|
const elRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dragRef = useRef<{ startX: number; startY: number; origX: number; origY: number } | null>(null);
|
||||||
|
|
||||||
|
function clamp(x: number, y: number) {
|
||||||
|
const el = elRef.current;
|
||||||
|
const parent = el?.offsetParent as HTMLElement | null;
|
||||||
|
if (!el || !parent) return { x, y };
|
||||||
|
const maxX = Math.max(0, parent.clientWidth - el.offsetWidth);
|
||||||
|
const maxY = Math.max(0, parent.clientHeight - el.offsetHeight);
|
||||||
|
return { x: Math.min(Math.max(0, x), maxX), y: Math.min(Math.max(0, y), maxY) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGripPointerDown(e: ReactPointerEvent) {
|
||||||
|
const el = elRef.current;
|
||||||
|
const parent = el?.offsetParent as HTMLElement | null;
|
||||||
|
if (!el || !parent) return;
|
||||||
|
// Antes do primeiro arrasto a barra fica centralizada via CSS transform
|
||||||
|
// (translate-x-1/2) — offsetLeft/offsetTop ignoram transform, então
|
||||||
|
// calculamos a posição visual real a partir do bounding rect pra não
|
||||||
|
// dar aquele "pulo" no primeiro clique.
|
||||||
|
const elRect = el.getBoundingClientRect();
|
||||||
|
const parentRect = parent.getBoundingClientRect();
|
||||||
|
dragRef.current = {
|
||||||
|
startX: e.clientX,
|
||||||
|
startY: e.clientY,
|
||||||
|
origX: elRect.left - parentRect.left,
|
||||||
|
origY: elRect.top - parentRect.top,
|
||||||
|
};
|
||||||
|
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGripPointerMove(e: ReactPointerEvent) {
|
||||||
|
if (!dragRef.current) return;
|
||||||
|
const dx = e.clientX - dragRef.current.startX;
|
||||||
|
const dy = e.clientY - dragRef.current.startY;
|
||||||
|
setPos(clamp(dragRef.current.origX + dx, dragRef.current.origY + dy));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGripPointerUp() {
|
||||||
|
dragRef.current = null;
|
||||||
|
setPos((current) => {
|
||||||
|
if (current) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(current));
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se a janela mudar de tamanho (ex: girar o celular), reaplica o clamp
|
||||||
|
// pra barra não ficar presa fora da área visível.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pos) return;
|
||||||
|
function onResize() {
|
||||||
|
setPos((current) => (current ? clamp(current.x, current.y) : current));
|
||||||
|
}
|
||||||
|
window.addEventListener("resize", onResize);
|
||||||
|
return () => window.removeEventListener("resize", onResize);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="absolute top-14 md:top-3 left-1/2 -translate-x-1/2 flex gap-1.5
|
ref={elRef}
|
||||||
overflow-x-auto md:flex-wrap md:justify-center bg-panel/90 border border-white/10
|
style={pos ? { position: "absolute", left: pos.x, top: pos.y } : undefined}
|
||||||
rounded-lg px-3 py-2 shadow-lg max-w-[92vw] md:max-w-[90%]"
|
className={
|
||||||
|
"flex items-center gap-1.5 bg-panel/90 border border-white/10 rounded-lg pl-1.5 pr-3 py-2 shadow-lg " +
|
||||||
|
(pos
|
||||||
|
? "max-w-[92vw]"
|
||||||
|
: "absolute top-14 md:top-3 left-1/2 -translate-x-1/2 max-w-[92vw] md:max-w-[90%]")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
onPointerDown={onGripPointerDown}
|
||||||
|
onPointerMove={onGripPointerMove}
|
||||||
|
onPointerUp={onGripPointerUp}
|
||||||
|
className="shrink-0 cursor-move select-none text-slate-500 hover:text-slate-300 px-1 touch-none"
|
||||||
|
title="Arrastar"
|
||||||
|
aria-label="Arrastar menu de regiões"
|
||||||
|
>
|
||||||
|
⠿
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1.5 overflow-x-auto md:flex-wrap">
|
||||||
{REGIONS.map((r) => (
|
{REGIONS.map((r) => (
|
||||||
<button
|
<button
|
||||||
key={r.region}
|
key={r.region}
|
||||||
@@ -33,5 +127,6 @@ export function QuickRegionNav() {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user