citologia e histologia

This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-09-11 14:58:41 -03:00
1 parent 7994e56cd0
commit 65a4fdea9e
17 files changed
+431 -7

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

@@ -0,0 +1,126 @@
import { useMemo, useState } from "react";
import { useAtlasStore } from "../store/useAtlasStore.js";
import { MICROSCOPY_HOTSPOTS } from "../data/microscopyHotspots.js";
import { MICROSCOPY_CREDITS } from "../data/microscopyCredits.js";
import type { Structure } from "../lib/api.js";
// Generalização do SkinDiagram.tsx pra sistemas sem malha 3D que precisam
// de MAIS de uma imagem (citologia/histologia têm uma fotomicrografia real
// por célula/tecido, ao contrário do tegumentar, que é uma imagem só) — ver
// a decisão de usar fotos reais em vez de geometria procedural em
// data-pipeline/seed/build-manifest-microscopy.mjs.
export function MicroscopyDiagram({
bySlug,
systemSlug,
title,
}: {
bySlug: Map<string, Structure>;
systemSlug: string;
title: string;
}) {
const selectedStructureId = useAtlasStore((s) => s.selectedStructureId);
const selectStructure = useAtlasStore((s) => s.selectStructure);
const toggleSystem = useAtlasStore((s) => s.toggleSystem);
const structures = useMemo(
() => [...bySlug.values()].filter((s) => s.system.slug === systemSlug),
[bySlug, systemSlug],
);
// Cada "modelFile" aqui não é uma malha — é o caminho da fotomicrografia
// que aquela estrutura pertence (ver build-manifest-microscopy.mjs).
// Agrupar por ele é o que separa as estruturas em "lâminas" navegáveis.
const byImage = useMemo(() => {
const map = new Map<string, Structure[]>();
for (const s of structures) {
if (!map.has(s.modelFile)) map.set(s.modelFile, []);
map.get(s.modelFile)!.push(s);
}
return map;
}, [structures]);
const images = useMemo(() => [...byImage.keys()], [byImage]);
const [activeImage, setActiveImage] = useState<string | null>(null);
const currentImage = activeImage && byImage.has(activeImage) ? activeImage : images[0];
if (structures.length === 0 || !currentImage) return null;
const currentStructures = byImage.get(currentImage) ?? [];
// O último trecho do regionPath é o "nome da lâmina" (ex: "Tecido Ósseo")
// — mesma lista usada pra dar nome de cada estrutura, reaproveitada aqui
// só como rótulo da aba, pra não precisar de mais um arquivo de dados.
const tabLabel = (image: string) => {
const first = byImage.get(image)?.[0];
return first?.regionPath[first.regionPath.length - 1] ?? image;
};
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 max-h-full overflow-y-auto">
<div className="flex items-center justify-between mb-2">
<h2 className="text-sm font-semibold text-white">{title} — clique nos pontos pra explorar</h2>
<button
onClick={() => toggleSystem(systemSlug)}
className="text-slate-400 hover:text-white text-sm px-2 py-1"
aria-label="Fechar diagrama"
>
✕
</button>
</div>
{images.length > 1 && (
<div className="flex flex-wrap gap-1.5 mb-3">
{images.map((image) => (
<button
key={image}
onClick={() => setActiveImage(image)}
className={`text-xs px-2.5 py-1 rounded-full border transition-colors
${
image === currentImage
? "bg-accent border-accent text-white"
: "border-white/15 text-slate-300 hover:border-white/30"
}`}
>
{tabLabel(image)}
</button>
))}
</div>
)}
<div className="relative w-full select-none">
<img
key={currentImage}
src={currentImage}
alt={tabLabel(currentImage)}
className="w-full h-auto rounded-lg border border-white/10"
draggable={false}
/>
{currentStructures.map((structure) => {
const hotspot = MICROSCOPY_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">
Fotomicrografia real (não ilustração). {MICROSCOPY_CREDITS[currentImage] ?? ""}
</p>
</div>
</div>
);
}
+13 -4
View File
@@ -6,6 +6,11 @@ import { useAtlasStore, type BodyRegion, type ClipAxis } from "../../store/useAt
import { SystemModel } from "./SystemModel.js";
import type { Structure } from "../../lib/api.js";
// Sistemas apresentados como diagrama 2D (fotomicrografia real com
// hotspots) em vez de malha 3D — ver SkinDiagram.tsx e
// MicroscopyDiagram.tsx.
const NON_3D_SYSTEMS = new Set(["tegumentar", "citologia", "histologia"]);
// Normais de referência em anatomia: sagital separa esq./dir., coronal
// (= frontal) separa ant./post., transversal separa sup./inf.
const AXIS_NORMAL: Record<ClipAxis, [number, number, number]> = {
@@ -304,10 +309,14 @@ export function Scene({
// um sistema visível — a visibilidade por malha é resolvida dentro de
// cada SystemModel.
const structuresByFile = useMemo(() => {
// 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");
// Tegumentar/Citologia/Histologia não têm malha 3D (são diagramas 2D
// com fotomicrografias reais — ver SkinDiagram.tsx e
// MicroscopyDiagram.tsx), então nunca entram na busca de .glb, senão o
// GLTFLoader tentaria carregar o modelFile dessas estruturas (que é o
// caminho de uma imagem, não um .glb).
const structuresList = [...bySlug.values()].filter(
(s) => !NON_3D_SYSTEMS.has(s.system.slug),
);
const map = new Map<string, Structure[]>();
for (const s of structuresList) {
if (!map.has(s.modelFile)) map.set(s.modelFile, []);
+23
View File
@@ -0,0 +1,23 @@
// Crédito/licença de cada fotomicrografia real usada em Citologia/Histologia
// — igual à nota de crédito já usada no diagrama do tegumentar, só que aqui
// tem uma imagem por lâmina em vez de uma imagem única. Ver a curadoria
// completa (com link de origem) em
// data-pipeline/seed/build-manifest-microscopy.mjs.
export const MICROSCOPY_CREDITS: Record<string, string> = {
"/citologia/nucleo-celular-met.png":
"Micrografia eletrônica de transmissão real. Domínio público (Wikimedia Commons).",
"/histologia/epitelio-simples-colunar.jpg":
"Mucosa gástrica normal, H&E. Nephron, CC BY-SA 3.0 (Wikimedia Commons).",
"/histologia/epitelio-estratificado-pavimentoso.jpg":
"Biópsia normal, H&E. Samir@enwiki, CC BY-SA 3.0 (Wikimedia Commons).",
"/histologia/tecido-adiposo.jpg":
"Corte de parafina (lipídios dissolvidos no processamento). CC BY-SA 3.0 (Wikimedia Commons).",
"/histologia/cartilagem-hialina.jpg":
"Bautista et al., CC0 (Wikimedia Commons).",
"/histologia/osso-compacto.jpg":
"Osso desgastado (ground section). Athikhun.suw, CC BY-SA 4.0 (Wikimedia Commons).",
"/histologia/tecido-muscular.jpg":
"OpenStax College, CC BY 4.0 (Wikimedia Commons).",
"/histologia/tecido-nervoso.jpg":
"Medula espinhal de coelho. John Alan Elson, CC BY-SA 4.0 (Wikimedia Commons).",
};
+52
View File
@@ -0,0 +1,52 @@
// Posição (em % da largura/altura de CADA imagem) de cada marcador
// clicável dos sistemas "citologia" e "histologia" — mesmo esquema do
// tegumentar (ver skinHotspots.ts), mas aqui um sistema pode ter VÁRIAS
// imagens (uma por lâmina/tecido), então a imagem de cada estrutura é o
// próprio structure.modelFile (ver data-pipeline/seed/build-manifest-microscopy.mjs)
// — este arquivo só guarda x/y dentro daquela imagem.
export const MICROSCOPY_HOTSPOTS: Record<string, { x: number; y: number }> = {
// Citologia — /citologia/nucleo-celular-met.png
"nucleo-citologia": { x: 46, y: 52 },
"envoltorio-nuclear": { x: 70, y: 48 },
cromatina: { x: 42, y: 60 },
"citoplasma-citologia": { x: 14, y: 18 },
// Histologia — /histologia/epitelio-simples-colunar.jpg
"epitelio-superficie-gastrico": { x: 28, y: 9 },
"fovéola-gastrica": { x: 47, y: 16 },
"glandulas-gastricas": { x: 50, y: 68 },
"lamina-propria-gastrica": { x: 18, y: 46 },
// Histologia — /histologia/epitelio-estratificado-pavimentoso.jpg
"camada-basal-epitelio": { x: 50, y: 90 },
"camadas-intermediarias-epitelio": { x: 50, y: 55 },
"camada-superficial-epitelio": { x: 50, y: 12 },
"membrana-basal-epitelio": { x: 22, y: 96 },
// Histologia — /histologia/tecido-adiposo.jpg
adipocito: { x: 35, y: 45 },
"nucleo-adipocito": { x: 39, y: 41 },
"septo-conjuntivo-adiposo": { x: 30, y: 78 },
// Histologia — /histologia/cartilagem-hialina.jpg (3 painéis empilhados)
"zona-superficial-cartilagem": { x: 50, y: 15 },
"zona-intermediaria-cartilagem": { x: 50, y: 48 },
"zona-profunda-cartilagem": { x: 50, y: 82 },
condrocito: { x: 28, y: 86 },
// Histologia — /histologia/osso-compacto.jpg
"sistema-de-havers": { x: 48, y: 42 },
"lacuna-osteocito": { x: 90, y: 12 },
"canaliculos-osseos": { x: 78, y: 22 },
"canal-de-volkmann": { x: 62, y: 80 },
// Histologia — /histologia/tecido-muscular.jpg (3 painéis: a/b/c)
"musculo-esqueletico-histologia": { x: 50, y: 14 },
"musculo-liso-histologia": { x: 50, y: 48 },
"musculo-cardiaco-histologia": { x: 50, y: 83 },
// Histologia — /histologia/tecido-nervoso.jpg
"substancia-branca-histologia": { x: 20, y: 25 },
"substancia-cinzenta-histologia": { x: 65, y: 35 },
"corpo-neuronal-histologia": { x: 66, y: 30 },
};
+3
View File
@@ -8,6 +8,7 @@ import { SearchNav } from "../components/SearchNav.js";
import { StructureInfoPanel } from "../components/StructureInfoPanel.js";
import { QuickRegionNav } from "../components/QuickRegionNav.js";
import { SkinDiagram } from "../components/SkinDiagram.js";
import { MicroscopyDiagram } from "../components/MicroscopyDiagram.js";
import { trackVisit } from "../lib/analytics.js";
// Em telas pequenas não há espaço para os 3 painéis lado a lado — os
@@ -72,6 +73,8 @@ export default function AtlasPage() {
<Scene bySlug={bySlug} />
<QuickRegionNav />
<SkinDiagram bySlug={bySlug} />
<MicroscopyDiagram bySlug={bySlug} systemSlug="citologia" title="Citologia (A Célula)" />
<MicroscopyDiagram bySlug={bySlug} systemSlug="histologia" title="Histologia (Tecidos)" />
<button
onClick={() => setLeftOpen(true)}