tegumentar3
This commit is contained in:
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 |
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
@@ -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)}
|
||||
|
||||
Reference in new issue
Block a user