tegumentar3

This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-09-10 19:42:05 -03:00
1 parent 063bd629c2
commit 9f2fe75ff6
11 files changed
+221 -537

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

+69
View File
@@ -0,0 +1,69 @@
import { useAtlasStore } from "../store/useAtlasStore.js";
import { SKIN_HOTSPOTS } from "../data/skinHotspots.js";
import type { Structure } from "../lib/api.js";
// O sistema tegumentar não tem malha 3D (Z-Anatomy não cobre pele — ver
// nota em data-pipeline/seed/build-manifest.mjs) e duas tentativas de bloco
// 3D esquemático não ficaram boas o bastante pra estudo. Em vez disso, é um
// diagrama 2D clássico (domínio público, NCI/SEER) com pontos clicáveis —
// cada um abre o mesmo StructureInfoPanel usado pelos outros sistemas.
export function SkinDiagram({ bySlug }: { bySlug: Map<string, Structure> }) {
const selectedStructureId = useAtlasStore((s) => s.selectedStructureId);
const selectStructure = useAtlasStore((s) => s.selectStructure);
const toggleSystem = useAtlasStore((s) => s.toggleSystem);
const tegumentar = [...bySlug.values()].filter((s) => s.system.slug === "tegumentar");
if (tegumentar.length === 0) return null;
return (
<div className="absolute inset-0 z-10 flex items-center justify-center p-4 bg-ink/70">
<div className="relative bg-panel border border-white/10 rounded-xl shadow-2xl p-3 sm:p-4 max-w-2xl w-full">
<div className="flex items-center justify-between mb-2">
<h2 className="text-sm font-semibold text-white">
Corte Transversal da Pele — clique nos pontos pra explorar
</h2>
<button
onClick={() => toggleSystem("tegumentar")}
className="text-slate-400 hover:text-white text-sm px-2 py-1"
aria-label="Fechar diagrama"
>
✕
</button>
</div>
<div className="relative w-full select-none">
<img
src="/tegumentar/skin-cross-section.jpg"
alt="Corte transversal da pele mostrando epiderme, derme, hipoderme e seus anexos"
className="w-full h-auto rounded-lg border border-white/10"
draggable={false}
/>
{tegumentar.map((structure) => {
const hotspot = SKIN_HOTSPOTS[structure.slug];
if (!hotspot) return null;
const isSelected = structure.id === selectedStructureId;
return (
<button
key={structure.id}
onClick={() => selectStructure(structure.id)}
title={structure.namePt}
aria-label={structure.namePt}
style={{ left: `${hotspot.x}%`, top: `${hotspot.y}%` }}
className={`absolute -translate-x-1/2 -translate-y-1/2 w-4 h-4 rounded-full border-2 transition-all
${
isSelected
? "bg-accent border-white scale-125 shadow-[0_0_0_4px_rgba(79,124,255,0.35)]"
: "bg-accent/70 border-white/70 hover:scale-125 hover:bg-accent"
}`}
/>
);
})}
</div>
<p className="text-[11px] text-slate-500 mt-2">
Imagem de domínio público — National Cancer Institute / SEER Training Modules.
</p>
</div>
</div>
);
}
+4 -1
View File
@@ -189,7 +189,10 @@ export function Scene({ bySlug }: { bySlug: Map<string, Structure> }) {
// um sistema visível — a visibilidade por malha é resolvida dentro de
// cada SystemModel.
const structuresByFile = useMemo(() => {
const structuresList = [...bySlug.values()];
// Tegumentar não tem malha 3D (é um diagrama 2D — ver SkinDiagram.tsx),
// então nunca entra na busca de .glb, senão o GLTFLoader tentaria
// carregar o modelFile placeholder ("n/a") dessas estruturas.
const structuresList = [...bySlug.values()].filter((s) => s.system.slug !== "tegumentar");
const map = new Map<string, Structure[]>();
for (const s of structuresList) {
if (!map.has(s.modelFile)) map.set(s.modelFile, []);
@@ -41,6 +41,13 @@ export function SystemModel({
const mesh = obj as THREE.Mesh;
if (mesh.isMesh && mesh.material) {
mesh.material = (mesh.material as THREE.MeshStandardMaterial).clone();
// Malhas finas tipo lâmina (fáscias, aponeuroses) saem da simplificação
// do pipeline (weld + meshopt) com normais corrompidas — a frente e o
// verso dessas "folhas" quase se tocam, então o weld mistura normais
// opostas num vértice só, gerando aquele efeito de iluminação
// "manchado"/ruidoso. Recalcular a partir da geometria final (já
// soldada) sempre dá um resultado consistente com a malha real.
mesh.geometry.computeVertexNormals();
}
});
clonedRef.current = clone;
+28
View File
@@ -0,0 +1,28 @@
// Posição (em % da largura/altura da imagem) de cada marcador clicável
// sobre frontend/public/tegumentar/skin-cross-section.jpg — medidas
// diretamente sobre a imagem (domínio público, NCI/SEER Training Modules).
// Isso é só apresentação (onde desenhar o ponto em cima da figura); os
// dados da estrutura em si (nome, descrição) vêm do banco, como qualquer
// outra estrutura do atlas — ver data-pipeline/seed/build-manifest.mjs.
export const SKIN_HOTSPOTS: Record<string, { x: number; y: number }> = {
"estrato-corneo": { x: 37.9, y: 15.0 },
"camada-pigmentar": { x: 37.5, y: 18.6 },
"papila-dermica": { x: 48.3, y: 27.2 },
epiderme: { x: 75.0, y: 32.2 },
"terminacao-nervosa-tato": { x: 74.6, y: 38.7 },
derme: { x: 75.4, y: 45.8 },
"glandula-sebacea": { x: 69.2, y: 47.3 },
"foliculo-piloso": { x: 65.8, y: 53.7 },
"papila-do-pelo": { x: 65.8, y: 58.7 },
hipoderme: { x: 75.4, y: 65.9 },
"estrato-germinativo": { x: 37.9, y: 33.7 },
"musculo-eretor-do-pelo": { x: 42.1, y: 43.0 },
"fibra-nervosa": { x: 38.8, y: 49.4 },
"vasos-sanguineos-linfaticos": { x: 35.0, y: 78.1 },
"glandula-sudoripara": { x: 47.1, y: 80.2 },
"corpusculo-de-pacini": { x: 51.3, y: 71.6 },
arteria: { x: 70.4, y: 78.1 },
veia: { x: 75.0, y: 80.9 },
pelo: { x: 65.8, y: 9.3 },
"poro-sudoriparo": { x: 69.2, y: 27.9 },
};
+2
View File
@@ -7,6 +7,7 @@ import { ClippingControls } from "../components/ClippingControls.js";
import { SearchNav } from "../components/SearchNav.js";
import { StructureInfoPanel } from "../components/StructureInfoPanel.js";
import { QuickRegionNav } from "../components/QuickRegionNav.js";
import { SkinDiagram } from "../components/SkinDiagram.js";
// Em telas pequenas não há espaço para os 3 painéis lado a lado — os
// laterais viram gavetas (drawers) que deslizam por cima do visualizador
@@ -61,6 +62,7 @@ export default function AtlasPage() {
<div className="relative h-full min-h-0">
<Scene bySlug={bySlug} />
<QuickRegionNav />
<SkinDiagram bySlug={bySlug} />
<button
onClick={() => setLeftOpen(true)}