citologia e histologia
This commit is contained in:
1 parent
7994e56cd0
commit
65a4fdea9e
17 files changed
+431
-7
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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, []);
|
||||
|
||||
Reference in new issue
Block a user