refatoraçao citologia e histologia

This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-10-15 15:26:16 -03:00
1 parent bd07618097
commit 204e9f8a39
18 files changed
+993 -48

No files matched your search

+136 -2
View File
@@ -16,7 +16,7 @@ import path from "node:path";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
function structure(slug, namePt, nameEn, description) { function structure(slug, namePt, nameEn, description, regionPath = ["Citologia", "Célula (modelo ilustrativo)"]) {
return { return {
system: "citologia", system: "citologia",
slug, slug,
@@ -24,12 +24,20 @@ function structure(slug, namePt, nameEn, description) {
nameEn, nameEn,
synonyms: [], synonyms: [],
description, description,
regionPath: ["Citologia", "Célula (modelo ilustrativo)"], regionPath,
modelFile: "n/a", modelFile: "n/a",
meshName: slug, meshName: slug,
}; };
} }
// Galeria de tipos celulares especializados — cada um ilustrativo (mesmo
// princípio da célula genérica acima: sem malha 3D real disponível pra
// isso), mostrando o que torna aquele tipo reconhecível/diferente de uma
// célula genérica. Ver frontend/src/routes/CellTypesPage.tsx.
function cellType(group, slug, namePt, nameEn, description) {
return structure(slug, namePt, nameEn, description, ["Citologia", "Tipos Celulares", group]);
}
const structures = [ const structures = [
structure( structure(
"celula-membrana-plasmatica", "celula-membrana-plasmatica",
@@ -85,6 +93,132 @@ const structures = [
"Lysosomes", "Lysosomes",
"Vesículas com enzimas digestivas (hidrolases ácidas) que degradam material vindo de dentro ou de fora da célula — o \"sistema de reciclagem e digestão\" celular.", "Vesículas com enzimas digestivas (hidrolases ácidas) que degradam material vindo de dentro ou de fora da célula — o \"sistema de reciclagem e digestão\" celular.",
), ),
structure(
"celula-centriolos",
"Centríolos",
"Centrioles",
"Par de estruturas cilíndricas (feitas de microtúbulos dispostos em 9 trincas) perto do núcleo, formando o centrossomo. Se duplicam na interfase e migram para os polos opostos antes da divisão celular, organizando as fibras do fuso mitótico/meiótico — ver o módulo de Mitose e Meiose.",
),
structure(
"celula-citoesqueleto",
"Citoesqueleto",
"Cytoskeleton",
"Rede de filamentos proteicos (microtúbulos, filamentos de actina e filamentos intermediários) que dá forma e sustentação interna à célula, organiza o posicionamento das organelas e serve de \"trilho\" para o transporte intracelular e para o movimento da própria célula.",
),
structure(
"celula-peroxissomos",
"Peroxissomos",
"Peroxisomes",
"Vesículas menores que os lisossomos, com enzimas oxidativas que degradam ácidos graxos de cadeia longa e neutralizam peróxido de hidrogênio (um subproduto tóxico dessas reações) usando a enzima catalase.",
),
// ---- Neurônio ----
cellType(
"Neurônio",
"neuronio-corpo-celular",
"Corpo Celular (Soma)",
"Cell Body (Soma)",
"Contém o núcleo e a maior parte das organelas do neurônio (retículo endoplasmático rugoso aqui é chamado de corpúsculo de Nissl); é onde a maior parte das proteínas e neurotransmissores é sintetizada.",
),
cellType(
"Neurônio",
"neuronio-dendritos",
"Dendritos",
"Dendrites",
"Prolongamentos curtos e ramificados que recebem sinais de outros neurônios (nas sinapses) e os conduzem em direção ao corpo celular.",
),
cellType(
"Neurônio",
"neuronio-axonio",
"Axônio",
"Axon",
"Prolongamento único, longo (pode chegar a mais de 1 metro nos maiores neurônios do corpo), que conduz o impulso nervoso do corpo celular até o terminal sináptico.",
),
cellType(
"Neurônio",
"neuronio-bainha-mielina",
"Bainha de Mielina",
"Myelin Sheath",
"Camada isolante (produzida por células de Schwann no sistema nervoso periférico, ou oligodendrócitos no central) que envolve o axônio em segmentos, aumentando MUITO a velocidade de condução do impulso. Os intervalos sem mielina entre os segmentos são os nódulos de Ranvier — é ali que o impulso \"salta\" de um nódulo a outro (condução saltatória).",
),
cellType(
"Neurônio",
"neuronio-terminal-sinaptico",
"Terminal Sináptico",
"Synaptic Terminal",
"Extremidade do axônio, dilatada em botões sinápticos cheios de vesículas com neurotransmissor — é aqui que o sinal elétrico vira sinal químico, liberado na fenda sináptica para o próximo neurônio (ou célula-alvo).",
),
// ---- Hemácia (eritrócito) ----
cellType(
"Hemácia (Eritrócito)",
"hemacia-eritrocito",
"Hemácia (Eritrócito)",
"Red Blood Cell (Erythrocyte)",
"Célula mais numerosa do sangue, especializada em transportar oxigênio (ligado à hemoglobina). Formato de disco bicôncavo (aumenta a área de superfície para troca de gases) e, diferente de quase toda célula humana, NÃO TEM NÚCLEO nem a maioria das organelas na fase madura — são expelidos durante a formação, abrindo mais espaço pra hemoglobina.",
),
// ---- Célula Muscular Esquelética ----
cellType(
"Célula Muscular Esquelética",
"musculo-fibra",
"Fibra Muscular Esquelética",
"Skeletal Muscle Fiber",
"Célula longa e cilíndrica (uma \"fibra\"), formada pela fusão de várias células precursoras durante o desenvolvimento — por isso é MULTINUCLEADA. Contém miofibrilas (feixes de filamentos de actina e miosina) que se contraem sob comando do sistema nervoso.",
),
cellType(
"Célula Muscular Esquelética",
"musculo-nucleos-perifericos",
"Núcleos Periféricos",
"Peripheral Nuclei",
"Vários núcleos (um resquício da fusão das células precursoras) posicionados na periferia da fibra, logo abaixo da membrana — diferente da maioria das células, que tem só 1 núcleo central.",
),
cellType(
"Célula Muscular Esquelética",
"musculo-estriacoes",
"Estriações Transversais",
"Cross Striations",
"Padrão de faixas claras e escuras alternadas, visível ao microscópio, formado pela organização regular dos filamentos de actina e miosina em sarcômeros — é o que dá o nome \"músculo estriado\" (esquelético e cardíaco; o liso não tem essas faixas).",
),
// ---- Espermatozoide ----
cellType(
"Espermatozoide",
"espermatozoide-cabeca",
"Cabeça",
"Head",
"Contém o núcleo com o DNA extremamente condensado, coberto na metade anterior pelo acrossomo — uma vesícula com enzimas que digerem a camada externa do óvulo na fecundação.",
),
cellType(
"Espermatozoide",
"espermatozoide-peca-intermediaria",
"Peça Intermediária",
"Midpiece",
"Repleta de mitocôndrias organizadas em espiral ao redor do flagelo — fornecem o ATP necessário para o batimento do flagelo durante toda a jornada até o óvulo.",
),
cellType(
"Espermatozoide",
"espermatozoide-flagelo",
"Flagelo (Cauda)",
"Flagellum (Tail)",
"Longo prolongamento que bate em movimento ondulatório, impulsionando o espermatozoide — a única célula humana com flagelo verdadeiro.",
),
// ---- Leucócito (neutrófilo como representante) ----
cellType(
"Leucócito (Neutrófilo)",
"leucocito-nucleo-lobulado",
"Núcleo Lobulado (Polimorfonuclear)",
"Lobulated Nucleus (Polymorphonuclear)",
"Núcleo dividido em 2 a 5 lóbulos conectados por filamentos finos de cromatina — a característica que dá ao neutrófilo (e a outros granulócitos) o nome \"polimorfonuclear\".",
),
cellType(
"Leucócito (Neutrófilo)",
"leucocito-granulos",
"Grânulos Citoplasmáticos",
"Cytoplasmic Granules",
"Pequenas vesículas espalhadas pelo citoplasma, cheias de enzimas antimicrobianas — são liberadas sobre bactérias fagocitadas (ou na área da infecção) como parte da resposta imune inata.",
),
]; ];
writeFileSync( writeFileSync(
@@ -40,6 +40,36 @@
// Medula espinhal de coelho. Autor: John Alan Elson. CC BY-SA 4.0. // Medula espinhal de coelho. Autor: John Alan Elson. CC BY-SA 4.0.
// https://commons.wikimedia.org/wiki/File:Rabbitspinalcord100x1.jpg // https://commons.wikimedia.org/wiki/File:Rabbitspinalcord100x1.jpg
// //
// histologia/epitelio-simples-pavimentoso.jpg
// Montagem total (whole mount), padrão "pedra de calçamento" típico do
// epitélio pavimentoso visto de cima. Berkshire Community College
// Bioscience Image Library (BCCB). CC0.
// https://commons.wikimedia.org/wiki/File:Epithelial_Tissues_Simple_Squamous_Epithelium_(40823230315).jpg
// histologia/epitelio-simples-cuboidal.jpg
// Túbulos renais em corte transversal, H&E. BCCB. CC0.
// https://commons.wikimedia.org/wiki/File:Epithelial_Tissues_Simple_Cuboidal_Epithelium_(41681552432).jpg
// histologia/epitelio-pseudoestratificado.jpg
// Traqueia de mamífero em corte transversal, H&E, mostrando o epitélio
// pseudoestratificado ciliado. Autor: Magscope. CC BY-SA 4.0.
// https://commons.wikimedia.org/wiki/File:Trachea_(mammal)_histology_cross-section.png
// histologia/tecido-conjuntivo-frouxo.jpg
// Montagem total (whole mount) de tecido conjuntivo frouxo/areolar,
// mostrando a rede de fibras colágenas onduladas. BCCB. CC0.
// https://commons.wikimedia.org/wiki/File:Connective_Tissue_Loose_Aerolar_(41743649782).jpg
// histologia/cartilagem-elastica.jpg
// Corte transversal de cartilagem elástica de mamífero (coloração
// especial pra fibras elásticas). BCCB. CC0.
// https://commons.wikimedia.org/wiki/File:Connective_Tissue_Elastic_Cartilage_(41812803082).jpg
// histologia/fibrocartilagem.jpg
// Fibrocartilagem de mamífero, tricrômico (colágeno em vermelho,
// matriz cartilaginosa em azul). Autora: Magda Yurchishin
// (fotografada numa aula de biologia do ensino médio). CC BY 4.0.
// https://commons.wikimedia.org/wiki/File:Microscopic_Image_of_Mammal_Fibrocartilage_Tissue.jpg
// histologia/esfregaco-sanguineo.jpg
// Esfregaço sanguíneo periférico corado (Wright), neutrófilo ao centro
// entre hemácias e plaquetas. Autor: Ajay Kumar Chaurasiya. CC BY-SA 4.0.
// https://commons.wikimedia.org/wiki/File:WBC_(neutrophil)_at_centre,_numerous_erythrocytes_and_platelets_(dot_like_bodies)_in_Wright's_stained_peripheral_blood_smear_(PBS)_microscopy.jpg
//
// As posições dos hotspots (x/y em % de cada imagem) ficam em // As posições dos hotspots (x/y em % de cada imagem) ficam em
// frontend/src/data/microscopyHotspots.ts — só apresentação, como no // frontend/src/data/microscopyHotspots.ts — só apresentação, como no
// tegumentar. Este arquivo só gera os DADOS da estrutura (nome, descrição). // tegumentar. Este arquivo só gera os DADOS da estrutura (nome, descrição).
@@ -178,6 +208,86 @@ add("histologia", NERV, "corpo-neuronal-histologia", "Corpo do Neurônio (Soma)"
"Região do neurônio que contém o núcleo e a maquinaria de síntese proteica; a partir dela partem os prolongamentos (dendritos e axônio) que fazem a célula se comunicar com outras.", "Região do neurônio que contém o núcleo e a maquinaria de síntese proteica; a partir dela partem os prolongamentos (dendritos e axônio) que fazem a célula se comunicar com outras.",
["Tecido Nervoso"]); ["Tecido Nervoso"]);
// ---------------------------------------------------------------------
// HISTOLOGIA — Epitélio simples pavimentoso
// ---------------------------------------------------------------------
const EP_PAV = "/histologia/epitelio-simples-pavimentoso.jpg";
add("histologia", EP_PAV, "epitelio-simples-pavimentoso", "Epitélio Simples Pavimentoso", "Simple Squamous Epithelium",
"Uma única camada de células achatadas (finas, largas), com núcleo também achatado — forma ideal pra difusão rápida de gases e líquidos. Reveste alvéolos pulmonares, cápsula de Bowman no rim, e forma o mesotélio e o endotélio dos vasos sanguíneos.",
["Tecido Epitelial"]);
add("histologia", EP_PAV, "nucleo-pavimentoso", "Núcleo da Célula Pavimentosa", "Squamous Cell Nucleus",
"Núcleo achatado, acompanhando o formato baixo e largo da própria célula — diferente do núcleo arredondado típico de células cúbicas ou colunares.",
["Tecido Epitelial"]);
// ---------------------------------------------------------------------
// HISTOLOGIA — Epitélio simples cuboidal (túbulos renais)
// ---------------------------------------------------------------------
const EP_CUB = "/histologia/epitelio-simples-cuboidal.jpg";
add("histologia", EP_CUB, "epitelio-simples-cuboidal", "Epitélio Simples Cuboidal", "Simple Cuboidal Epithelium",
"Uma única camada de células com altura e largura parecidas ('cúbicas'), núcleo arredondado e central. Reveste túbulos renais, folículos da tireoide e ductos de várias glândulas — funções de absorção e secreção.",
["Tecido Epitelial"]);
add("histologia", EP_CUB, "lumen-tubular-renal", "Luz do Túbulo Renal", "Renal Tubule Lumen",
"Espaço central e oco de cada túbulo, por onde passa o filtrado/urina em formação — delimitado ao redor por todo o anel de células cuboidais.",
["Tecido Epitelial"]);
// ---------------------------------------------------------------------
// HISTOLOGIA — Epitélio pseudoestratificado ciliado (traqueia)
// ---------------------------------------------------------------------
const EP_PSEUDO = "/histologia/epitelio-pseudoestratificado.jpg";
add("histologia", EP_PSEUDO, "epitelio-pseudoestratificado", "Epitélio Pseudoestratificado Ciliado", "Pseudostratified Ciliated Epithelium",
"Parece ter várias camadas (\"pseudo\"-estratificado), mas é uma ÚNICA camada de células — só que de alturas diferentes, com nem todas alcançando a superfície, o que dá essa falsa impressão. As células que chegam à superfície têm cílios, que varrem o muco (com partículas presas) pra fora das vias respiratórias.",
["Tecido Epitelial"]);
add("histologia", EP_PSEUDO, "lamina-propria-traqueia", "Lâmina Própria", "Lamina Propria",
"Camada de tecido conjuntivo logo abaixo do epitélio, dando sustentação e contendo vasos sanguíneos, glândulas mucosas e fibras elásticas.",
["Tecido Epitelial"]);
// ---------------------------------------------------------------------
// HISTOLOGIA — Tecido conjuntivo frouxo (areolar)
// ---------------------------------------------------------------------
const CONJ_FROUXO = "/histologia/tecido-conjuntivo-frouxo.jpg";
add("histologia", CONJ_FROUXO, "tecido-conjuntivo-frouxo", "Tecido Conjuntivo Frouxo (Areolar)", "Loose (Areolar) Connective Tissue",
"Tecido conjuntivo mais comum do corpo: poucas fibras (colágenas e elásticas), dispostas de forma solta e desorganizada numa matriz com bastante substância fundamental — preenche espaços entre órgãos e sob a pele, com boa flexibilidade e vascularização.",
["Tecido Conjuntivo"]);
add("histologia", CONJ_FROUXO, "fibroblasto-frouxo", "Fibroblasto", "Fibroblast",
"Célula mais numerosa do tecido conjuntivo, responsável por produzir e manter as fibras (colágeno, elastina) e a substância fundamental da matriz extracelular.",
["Tecido Conjuntivo"]);
// ---------------------------------------------------------------------
// HISTOLOGIA — Cartilagem elástica
// ---------------------------------------------------------------------
const CART_ELAST = "/histologia/cartilagem-elastica.jpg";
add("histologia", CART_ELAST, "cartilagem-elastica", "Cartilagem Elástica", "Elastic Cartilage",
"Tipo de cartilagem com uma densa rede de fibras ELÁSTICAS na matriz (além do colágeno), o que dá flexibilidade extra, voltando à forma original depois de dobrada — encontrada na orelha externa (pavilhão auricular) e na epiglote.",
["Tecido Cartilaginoso"]);
add("histologia", CART_ELAST, "condrocito-elastica", "Condrócito", "Chondrocyte",
"Célula madura da cartilagem, alojada numa lacuna dentro da matriz que ela mesma produziu e mantém — ao contrário do osso, a cartilagem não tem vasos sanguíneos, então os condrócitos dependem de difusão pra se nutrir.",
["Tecido Cartilaginoso"]);
// ---------------------------------------------------------------------
// HISTOLOGIA — Fibrocartilagem
// ---------------------------------------------------------------------
const FIBROCART = "/histologia/fibrocartilagem.jpg";
add("histologia", FIBROCART, "fibrocartilagem", "Fibrocartilagem", "Fibrocartilage",
"Tipo de cartilagem mais resistente à tração, com grossos feixes de colágeno tipo I visíveis na matriz (ao lado dos condrócitos) — diferente da hialina e da elástica, não tem pericôndrio. Encontrada nos discos intervertebrais e na sínfise púbica, onde precisa suportar bastante carga mecânica.",
["Tecido Cartilaginoso"]);
add("histologia", FIBROCART, "fibras-colagenas-fibrocartilagem", "Feixes de Colágeno", "Collagen Bundles",
"Feixes grossos e densos de colágeno tipo I, organizados em paralelo — dão à fibrocartilagem a resistência à tração que a hialina e a elástica não têm.",
["Tecido Cartilaginoso"]);
// ---------------------------------------------------------------------
// HISTOLOGIA — Esfregaço sanguíneo periférico
// ---------------------------------------------------------------------
const SANGUE = "/histologia/esfregaco-sanguineo.jpg";
add("histologia", SANGUE, "neutrofilo-esfregaco", "Neutrófilo", "Neutrophil",
"Leucócito mais comum do sangue, com núcleo polimorfonuclear (2 a 5 lóbulos) — primeira linha de defesa celular contra infecções bacterianas, fagocitando os micro-organismos.",
["Sangue"]);
add("histologia", SANGUE, "hemacias-esfregaco", "Hemácias", "Red Blood Cells",
"Células mais numerosas do esfregaço, sem núcleo, com uma área mais clara no centro (o reflexo do formato bicôncavo visto de cima) — transportam oxigênio ligado à hemoglobina.",
["Sangue"]);
add("histologia", SANGUE, "plaquetas-esfregaco", "Plaquetas", "Platelets",
"Fragmentos celulares pequenos (bem menores que uma hemácia), vistos como pontinhos escuros espalhados pelo esfregaço — essenciais pra coagulação sanguínea, se agregando no local de uma lesão vascular.",
["Sangue"]);
writeFileSync( writeFileSync(
path.resolve(__dirname, "manifest-microscopy.json"), path.resolve(__dirname, "manifest-microscopy.json"),
JSON.stringify({ structures }, null, 2), JSON.stringify({ structures }, null, 2),
Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

+2
View File
@@ -10,6 +10,7 @@ import AtlasPage from "./routes/AtlasPage.js";
const PlanesPage = lazy(() => import("./routes/PlanesPage.js")); const PlanesPage = lazy(() => import("./routes/PlanesPage.js"));
const PhysicalExamPage = lazy(() => import("./routes/PhysicalExamPage.js")); const PhysicalExamPage = lazy(() => import("./routes/PhysicalExamPage.js"));
const CellDivisionPage = lazy(() => import("./routes/CellDivisionPage.js")); const CellDivisionPage = lazy(() => import("./routes/CellDivisionPage.js"));
const CellTypesPage = lazy(() => import("./routes/CellTypesPage.js"));
const QuizPage = lazy(() => import("./routes/QuizPage.js")); const QuizPage = lazy(() => import("./routes/QuizPage.js"));
const AdminStatsPage = lazy(() => import("./routes/AdminStatsPage.js")); const AdminStatsPage = lazy(() => import("./routes/AdminStatsPage.js"));
@@ -22,6 +23,7 @@ export default function App() {
<Route path="/planos-anatomicos" element={<PlanesPage />} /> <Route path="/planos-anatomicos" element={<PlanesPage />} />
<Route path="/exame-fisico" element={<PhysicalExamPage />} /> <Route path="/exame-fisico" element={<PhysicalExamPage />} />
<Route path="/divisao-celular" element={<CellDivisionPage />} /> <Route path="/divisao-celular" element={<CellDivisionPage />} />
<Route path="/tipos-celulares" element={<CellTypesPage />} />
<Route path="/quiz" element={<QuizPage />} /> <Route path="/quiz" element={<QuizPage />} />
</Route> </Route>
{/* Fora do Layout: painel interno, sem o menu/nav do atlas. */} {/* Fora do Layout: painel interno, sem o menu/nav do atlas. */}
+74 -45
View File
@@ -1,9 +1,10 @@
import { useMemo, type ReactNode } from "react"; import { useMemo } from "react";
import { Canvas } from "@react-three/fiber"; import { Canvas } from "@react-three/fiber";
import { CameraControls } from "@react-three/drei"; import { CameraControls } from "@react-three/drei";
import * as THREE from "three"; import * as THREE from "three";
import { useAtlasStore } from "../store/useAtlasStore.js"; import { useAtlasStore } from "../store/useAtlasStore.js";
import type { Structure } from "../lib/api.js"; import type { Structure } from "../lib/api.js";
import { materialProps, cylinderBetween, Organelle, HIGHLIGHT, BLACK } from "./cell/cellShared.js";
// Não existe (ver discussão com o usuário) uma malha 3D real e aberta de // Não existe (ver discussão com o usuário) uma malha 3D real e aberta de
// célula com todas as organelas prontas pra estudo — chegamos a integrar // célula com todas as organelas prontas pra estudo — chegamos a integrar
@@ -14,23 +15,9 @@ import type { Structure } from "../lib/api.js";
// pedido do usuário, citologia agora é um modelo 3D ILUSTRATIVO — desenhado // pedido do usuário, citologia agora é um modelo 3D ILUSTRATIVO — desenhado
// à mão com primitivas do Three.js, mesmo princípio já usado no boneco // à mão com primitivas do Three.js, mesmo princípio já usado no boneco
// esquemático de Planos Anatômicos (ver Mannequin.tsx) — mais reconhecível // esquemático de Planos Anatômicos (ver Mannequin.tsx) — mais reconhecível
// pra quem está estudando, mesmo não vindo de dado real. // pra quem está estudando, mesmo não vindo de dado real. `materialProps`,
const HIGHLIGHT = new THREE.Color("#22d3ee"); // `cylinderBetween` e `Organelle` são compartilhados com
const BLACK = new THREE.Color(0x000000); // routes/CellTypesPage.tsx — ver cell/cellShared.tsx.
// Função comum (NÃO hook) de propósito — é chamada dentro de .map() e de
// callbacks de render-prop abaixo, onde um hook (useMemo/useState) violaria
// as Regras de Hooks do React (número de chamadas por render precisa ser
// sempre igual e não pode depender de laços/condicionais).
function materialProps(baseColor: string, selected: boolean, opacity = 1) {
return {
color: baseColor,
transparent: opacity < 1,
opacity,
emissive: selected ? HIGHLIGHT : BLACK,
emissiveIntensity: selected ? 0.7 : 0,
};
}
// Corte transversal ampliado da bicamada fosfolipídica (cabeça+cauda, as // Corte transversal ampliado da bicamada fosfolipídica (cabeça+cauda, as
// duas camadas com as caudas hidrofóbicas viradas pra dentro, uma pra // duas camadas com as caudas hidrofóbicas viradas pra dentro, uma pra
@@ -92,33 +79,6 @@ function MembraneCrossSection({ selected }: { selected: boolean }) {
); );
} }
function Organelle({
slug,
bySlug,
onSelect,
children,
}: {
slug: string;
bySlug: Map<string, Structure>;
onSelect: (id: string) => void;
children: (isSelected: boolean) => ReactNode;
}) {
const selectedStructureId = useAtlasStore((s) => s.selectedStructureId);
const structure = bySlug.get(slug);
const isSelected = !!structure && structure.id === selectedStructureId;
return (
<group
onClick={(e) => {
e.stopPropagation();
if (structure) onSelect(structure.id);
}}
>
{children(isSelected)}
</group>
);
}
// Pontos de mitocôndrias/lisossomos espalhados pelo citoplasma — fixos // Pontos de mitocôndrias/lisossomos espalhados pelo citoplasma — fixos
// (não aleatórios a cada render) pra não "pular" toda vez que o componente // (não aleatórios a cada render) pra não "pular" toda vez que o componente
// atualiza. // atualiza.
@@ -135,6 +95,17 @@ const RIBOSOME_SPOTS: [number, number, number][] = [
[-0.5, -0.2, -0.5], [0.55, -0.35, 0.1], [-0.2, 0.6, -0.3], [0.4, 0.1, -0.55], [-0.5, -0.2, -0.5], [0.55, -0.35, 0.1], [-0.2, 0.6, -0.3], [0.4, 0.1, -0.55],
[-0.55, 0.3, 0.25], [0.1, -0.6, -0.25], [-0.55, 0.3, 0.25], [0.1, -0.6, -0.25],
]; ];
const PEROXISSOMO_SPOTS: [number, number, number][] = [
[0.45, 0.45, -0.3], [-0.2, -0.4, 0.5], [0.15, -0.55, -0.35], [-0.55, 0.1, 0.4],
];
// "Raios" do citoesqueleto saindo de perto do núcleo até perto da
// membrana, em direções fixas (não aleatórias) — só pra dar a ideia de
// rede de filamentos preenchendo o citoplasma, sem tentar reproduzir
// microtúbulo/actina/filamento intermediário individualmente.
const CYTOSKELETON_RAYS: [number, number, number][] = [
[0.7, 0.5, -0.1], [-0.65, 0.4, 0.35], [0.5, -0.55, 0.4], [-0.5, -0.5, -0.45],
[0.75, -0.1, -0.5], [-0.75, 0.05, 0.2], [0.15, 0.7, 0.45], [-0.2, -0.75, 0.1],
];
export function CellDiagram3D({ bySlug: bySlugAll }: { bySlug: Map<string, Structure> }) { export function CellDiagram3D({ bySlug: bySlugAll }: { bySlug: Map<string, Structure> }) {
const toggleSystem = useAtlasStore((s) => s.toggleSystem); const toggleSystem = useAtlasStore((s) => s.toggleSystem);
@@ -272,6 +243,64 @@ export function CellDiagram3D({ bySlug: bySlugAll }: { bySlug: Map<string, Struc
)} )}
</Organelle> </Organelle>
{/* Centríolos — par perpendicular perto do núcleo (centrossomo).
Mesma posição conceitual de onde os centrossomos aparecem
no módulo de Mitose e Meiose: organizam o fuso quando a
célula se divide. */}
<Organelle slug="celula-centriolos" bySlug={bySlug} onSelect={selectStructure}>
{(sel) => (
<group position={[0.05, -0.32, 0.22]}>
<mesh rotation={[0, 0, 0]}>
<cylinderGeometry args={[0.02, 0.02, 0.12, 10]} />
<meshStandardMaterial {...materialProps("#f97316", sel)} />
</mesh>
<mesh position={[0.09, 0, 0.02]} rotation={[0, 0, Math.PI / 2]}>
<cylinderGeometry args={[0.02, 0.02, 0.12, 10]} />
<meshStandardMaterial {...materialProps("#f97316", sel)} />
</mesh>
</group>
)}
</Organelle>
{/* Peroxissomos — vesículas pequenas, cor própria (não
confundir com os lisossomos amarelos) */}
<Organelle slug="celula-peroxissomos" bySlug={bySlug} onSelect={selectStructure}>
{(sel) => (
<>
{PEROXISSOMO_SPOTS.map(([x, y, z], i) => (
<mesh key={i} position={[x, y, z]}>
<sphereGeometry args={[0.04, 12, 12]} />
<meshStandardMaterial {...materialProps("#5eead4", sel, 0.95)} />
</mesh>
))}
</>
)}
</Organelle>
{/* Citoesqueleto — rede de filamentos finos saindo de perto do
núcleo até perto da membrana, preenchendo o citoplasma */}
<Organelle slug="celula-citoesqueleto" bySlug={bySlug} onSelect={selectStructure}>
{(sel) => {
const center = new THREE.Vector3(0.1, 0, 0);
return (
<>
{CYTOSKELETON_RAYS.map((target, i) => {
const { position, quaternion, length } = cylinderBetween(
center,
new THREE.Vector3(...target),
);
return (
<mesh key={i} position={position} quaternion={quaternion}>
<cylinderGeometry args={[0.006, 0.006, length, 5]} />
<meshStandardMaterial {...materialProps("#94a3b8", sel, 0.55)} />
</mesh>
);
})}
</>
);
}}
</Organelle>
{/* Membrana plasmática — um RECORTE da casca (não a esfera {/* Membrana plasmática — um RECORTE da casca (não a esfera
inteira) num canto de trás, tipo uma "janela" mostrando inteira) num canto de trás, tipo uma "janela" mostrando
onde ela fica. Uma esfera fechada ao redor de tudo (como onde ela fica. Uma esfera fechada ao redor de tudo (como
+3
View File
@@ -35,6 +35,9 @@ export function Layout() {
<NavLink to="/divisao-celular" className={linkClass}> <NavLink to="/divisao-celular" className={linkClass}>
Mitose e Meiose Mitose e Meiose
</NavLink> </NavLink>
<NavLink to="/tipos-celulares" className={linkClass}>
Tipos Celulares
</NavLink>
<NavLink to="/quiz" className={linkClass}> <NavLink to="/quiz" className={linkClass}>
Quiz Quiz
</NavLink> </NavLink>
@@ -0,0 +1,368 @@
import * as THREE from "three";
import type { Structure } from "../../lib/api.js";
import { materialProps, cylinderBetween, Organelle } from "./cellShared.js";
// Cada função abaixo desenha UM tipo celular — todas ilustrativas (sem
// malha 3D real disponível, mesmo princípio de CellDiagram3D.tsx),
// pensadas pra destacar o que torna aquele tipo reconhecível/diferente de
// uma célula genérica. `bySlug`/`onSelect` seguem o mesmo contrato de
// Organelle usado na célula genérica — clicar numa parte abre a descrição
// real (ver build-manifest-cell.mjs).
interface CellTypeProps {
bySlug: Map<string, Structure>;
onSelect: (id: string) => void;
}
// ---------------------------------------------------------------------
// Neurônio
// ---------------------------------------------------------------------
const DENDRITE_DIRECTIONS: [number, number, number][] = [
[-0.55, 0.35, 0.1], [-0.5, -0.1, 0.4], [-0.35, 0.5, -0.3],
[-0.6, -0.3, -0.2], [-0.2, 0.55, 0.35], [-0.45, 0.05, -0.5],
];
const AXON_DIRECTION = new THREE.Vector3(1.4, -0.15, 0.1);
const MYELIN_SEGMENTS = [0.25, 0.42, 0.59, 0.76, 0.93]; // posições ao longo do axônio (fração 0-1), com lacunas = nódulos de Ranvier
export function NeuronModel({ bySlug, onSelect }: CellTypeProps) {
const soma = new THREE.Vector3(0, 0, 0);
const axonEnd = soma.clone().add(AXON_DIRECTION);
return (
<>
<Organelle slug="neuronio-corpo-celular" bySlug={bySlug} onSelect={onSelect}>
{(sel) => (
<>
<mesh>
<sphereGeometry args={[0.32, 28, 28]} />
<meshStandardMaterial {...materialProps("#a78bfa", sel, 0.92)} />
</mesh>
<mesh position={[0.05, 0.03, 0.06]}>
<sphereGeometry args={[0.14, 20, 20]} />
<meshStandardMaterial {...materialProps("#6d28d9", sel)} />
</mesh>
</>
)}
</Organelle>
<Organelle slug="neuronio-dendritos" bySlug={bySlug} onSelect={onSelect}>
{(sel) => (
<>
{DENDRITE_DIRECTIONS.map((dir, i) => {
const to = new THREE.Vector3(...dir);
const { position, quaternion, length } = cylinderBetween(soma, to);
return (
<group key={i}>
<mesh position={position} quaternion={quaternion}>
<cylinderGeometry args={[0.025, 0.012, length, 6]} />
<meshStandardMaterial {...materialProps("#c4b5fd", sel)} />
</mesh>
{/* sub-ramo curto, só pra sugerir ramificação */}
{(() => {
const mid = soma.clone().lerp(to, 0.6);
const branchTo = mid.clone().add(new THREE.Vector3(dir[2] * 0.2, dir[0] * 0.15, -dir[1] * 0.2));
const branch = cylinderBetween(mid, branchTo);
return (
<mesh position={branch.position} quaternion={branch.quaternion}>
<cylinderGeometry args={[0.012, 0.006, branch.length, 5]} />
<meshStandardMaterial {...materialProps("#c4b5fd", sel)} />
</mesh>
);
})()}
</group>
);
})}
</>
)}
</Organelle>
<Organelle slug="neuronio-axonio" bySlug={bySlug} onSelect={onSelect}>
{(sel) => {
const { position, quaternion, length } = cylinderBetween(soma, axonEnd);
return (
<mesh position={position} quaternion={quaternion}>
<cylinderGeometry args={[0.035, 0.03, length, 8]} />
<meshStandardMaterial {...materialProps("#f472b6", sel)} />
</mesh>
);
}}
</Organelle>
<Organelle slug="neuronio-bainha-mielina" bySlug={bySlug} onSelect={onSelect}>
{(sel) => (
<>
{MYELIN_SEGMENTS.map((t, i) => {
const center = soma.clone().lerp(axonEnd, t);
return (
<mesh key={i} position={center} rotation={[0, 0, Math.atan2(AXON_DIRECTION.y, AXON_DIRECTION.x)]}>
<capsuleGeometry args={[0.065, 0.1, 4, 10]} />
<meshStandardMaterial {...materialProps("#fcd34d", sel, 0.85)} />
</mesh>
);
})}
</>
)}
</Organelle>
<Organelle slug="neuronio-terminal-sinaptico" bySlug={bySlug} onSelect={onSelect}>
{(sel) => (
<>
{[
[0.08, 0.06, 0],
[0.1, -0.05, 0.06],
[0.07, -0.02, -0.07],
].map(([dx, dy, dz], i) => (
<mesh key={i} position={[axonEnd.x + dx, axonEnd.y + dy, axonEnd.z + dz]}>
<sphereGeometry args={[0.055, 14, 14]} />
<meshStandardMaterial {...materialProps("#fb7185", sel, 0.95)} />
</mesh>
))}
</>
)}
</Organelle>
</>
);
}
// ---------------------------------------------------------------------
// Hemácia (eritrócito) — disco bicôncavo de verdade, via perfil revolvido
// (mesma técnica usada no torso do Mannequin.tsx: LatheGeometry)
// ---------------------------------------------------------------------
function useBiconcaveProfile() {
// A primeira versão traçava só UMA superfície (centro->borda), o que o
// Lathe revolve numa tigela/pires de um lado só, não um disco de
// verdade — parecia uma lasca fina vista de qualquer ângulo mais de
// lado. Um disco de verdade precisa de um contorno FECHADO no corte
// transversal: sobe pela face de CIMA (centro->borda) e desce pela face
// de BAIXO (borda->centro), com a borda fina unindo as duas — só assim
// o Lathe fecha um sólido com as duas faces côncavas.
const halfThicknessByRadius: [radius: number, halfThickness: number][] = [
[0.0, 0.015], // centro: bem fino (a concavidade de verdade)
[0.1, 0.045],
[0.24, 0.1], // anel bem mais grosso que o centro
[0.35, 0.065],
[0.4, 0.0], // borda fina, as duas faces se encontram aqui
];
const top = halfThicknessByRadius.map(([r, h]) => new THREE.Vector2(r, h));
const bottom = [...halfThicknessByRadius]
.reverse()
.slice(1) // não repete o ponto da borda (já é o último do "top")
.map(([r, h]) => new THREE.Vector2(r, -h));
const geo = new THREE.LatheGeometry([...top, ...bottom], 40);
geo.computeVertexNormals();
return geo;
}
export function ErythrocyteModel({ bySlug, onSelect }: CellTypeProps) {
const geometry = useBiconcaveProfile();
return (
<Organelle slug="hemacia-eritrocito" bySlug={bySlug} onSelect={onSelect}>
{(sel) => (
<mesh geometry={geometry}>
<meshStandardMaterial {...materialProps("#dc2626", sel, 0.96)} />
</mesh>
)}
</Organelle>
);
}
// ---------------------------------------------------------------------
// Célula Muscular Esquelética — fibra longa, núcleos periféricos,
// estriações transversais
// ---------------------------------------------------------------------
const FIBER_LENGTH = 1.7;
const FIBER_RADIUS = 0.22;
const PERIPHERAL_NUCLEI: [number, number][] = [
[-0.55, 0.9], [-0.1, -0.95], [0.4, 0.88], [0.75, -0.85], [-0.8, -0.3],
];
const STRIATION_COUNT = 14;
export function MuscleFiberModel({ bySlug, onSelect }: CellTypeProps) {
return (
<>
<Organelle slug="musculo-fibra" bySlug={bySlug} onSelect={onSelect}>
{(sel) => (
<mesh rotation={[0, 0, Math.PI / 2]}>
<capsuleGeometry args={[FIBER_RADIUS, FIBER_LENGTH - FIBER_RADIUS * 2, 4, 16]} />
<meshStandardMaterial {...materialProps("#f87171", sel, 0.9)} />
</mesh>
)}
</Organelle>
<Organelle slug="musculo-estriacoes" bySlug={bySlug} onSelect={onSelect}>
{(sel) => (
<>
{Array.from({ length: STRIATION_COUNT }, (_, i) => {
const x = -FIBER_LENGTH / 2 + 0.15 + (i / (STRIATION_COUNT - 1)) * (FIBER_LENGTH - 0.3);
const dark = i % 2 === 0;
return (
<mesh key={i} position={[x, 0, 0]} rotation={[0, 0, Math.PI / 2]}>
<cylinderGeometry args={[FIBER_RADIUS * 1.001, FIBER_RADIUS * 1.001, 0.045, 20, 1, true]} />
<meshStandardMaterial
{...materialProps(dark ? "#7f1d1d" : "#fca5a5", sel, 0.7)}
side={THREE.DoubleSide}
/>
</mesh>
);
})}
</>
)}
</Organelle>
<Organelle slug="musculo-nucleos-perifericos" bySlug={bySlug} onSelect={onSelect}>
{(sel) => (
<>
{PERIPHERAL_NUCLEI.map(([x, angleFrac], i) => {
const angle = angleFrac * Math.PI;
const y = Math.cos(angle) * (FIBER_RADIUS - 0.03);
const z = Math.sin(angle) * (FIBER_RADIUS - 0.03);
return (
<mesh key={i} position={[x, y, z]} scale={[1.4, 0.7, 0.7]}>
<sphereGeometry args={[0.07, 14, 14]} />
<meshStandardMaterial {...materialProps("#7c2d92", sel)} />
</mesh>
);
})}
</>
)}
</Organelle>
</>
);
}
// ---------------------------------------------------------------------
// Espermatozoide — cabeça (+ acrossomo), peça intermediária (+
// mitocôndrias em espiral), flagelo
// ---------------------------------------------------------------------
const HEAD_CENTER = new THREE.Vector3(-0.6, 0, 0);
const HEAD_RADIUS = 0.22;
// A peça intermediária começa na BORDA da cabeça, não no centro dela —
// começar no centro fazia a peça intermediária e as mitocôndrias nascerem
// "dentro" da cabeça, virando uma bola confusa de formas sobrepostas.
const MIDPIECE_START = new THREE.Vector3(HEAD_CENTER.x + HEAD_RADIUS, 0, 0);
const MIDPIECE_END = new THREE.Vector3(0.05, 0, 0);
const TAIL_END = new THREE.Vector3(1.3, 0.05, 0);
export function SpermModel({ bySlug, onSelect }: CellTypeProps) {
return (
<>
<Organelle slug="espermatozoide-cabeca" bySlug={bySlug} onSelect={onSelect}>
{(sel) => (
<>
<mesh position={HEAD_CENTER} scale={[1, 0.62, 0.8]}>
<sphereGeometry args={[HEAD_RADIUS, 24, 24]} />
<meshStandardMaterial {...materialProps("#38bdf8", sel, 0.95)} />
</mesh>
{/* acrossomo — capuz na metade anterior (frente) da cabeça */}
<mesh position={[HEAD_CENTER.x - 0.1, 0, 0]} scale={[1, 0.6, 0.78]}>
<sphereGeometry args={[0.16, 20, 20, 0, Math.PI]} />
<meshStandardMaterial {...materialProps("#0ea5e9", sel, 0.95)} side={THREE.DoubleSide} />
</mesh>
</>
)}
</Organelle>
<Organelle slug="espermatozoide-peca-intermediaria" bySlug={bySlug} onSelect={onSelect}>
{(sel) => {
const { position, quaternion, length } = cylinderBetween(MIDPIECE_START, MIDPIECE_END);
return (
<>
<mesh position={position} quaternion={quaternion}>
<cylinderGeometry args={[0.045, 0.04, length, 10]} />
<meshStandardMaterial {...materialProps("#facc15", sel, 0.5)} />
</mesh>
{/* mitocôndrias em espiral ao redor */}
{Array.from({ length: 8 }, (_, i) => {
const t = i / 7;
const x = THREE.MathUtils.lerp(MIDPIECE_START.x, MIDPIECE_END.x, t);
const angle = t * Math.PI * 4;
const y = Math.cos(angle) * 0.055;
const z = Math.sin(angle) * 0.055;
return (
<mesh key={i} position={[x, y, z]} scale={[0.7, 0.45, 0.45]}>
<sphereGeometry args={[0.03, 10, 10]} />
<meshStandardMaterial {...materialProps("#ef4444", sel)} />
</mesh>
);
})}
</>
);
}}
</Organelle>
<Organelle slug="espermatozoide-flagelo" bySlug={bySlug} onSelect={onSelect}>
{(sel) => {
const { position, quaternion, length } = cylinderBetween(MIDPIECE_END, TAIL_END);
return (
<mesh position={position} quaternion={quaternion}>
<cylinderGeometry args={[0.014, 0.004, length, 6]} />
<meshStandardMaterial {...materialProps("#7dd3fc", sel)} />
</mesh>
);
}}
</Organelle>
</>
);
}
// ---------------------------------------------------------------------
// Leucócito (neutrófilo) — núcleo polimorfonuclear + grânulos
// ---------------------------------------------------------------------
const NUCLEUS_LOBES: [number, number, number][] = [
[-0.12, 0.1, 0], [0.08, 0.14, 0.05], [0.14, -0.08, -0.06], [-0.05, -0.14, 0.08],
];
const GRANULE_SPOTS: [number, number, number][] = [
[0.3, 0.25, 0.1], [-0.3, 0.2, -0.15], [0.25, -0.3, 0.2], [-0.25, -0.25, -0.1],
[0.05, 0.35, -0.25], [-0.1, -0.35, 0.2], [0.35, 0.0, -0.1], [-0.35, 0.05, 0.15],
];
export function LeukocyteModel({ bySlug, onSelect }: CellTypeProps) {
return (
<>
{/* citoplasma — só contexto visual, sem clique (mesma razão da
membrana da célula genérica: é a casca que envolve tudo) */}
<mesh raycast={() => null}>
<sphereGeometry args={[0.45, 24, 24]} />
<meshStandardMaterial color="#fde68a" transparent opacity={0.14} side={THREE.DoubleSide} depthWrite={false} />
</mesh>
<Organelle slug="leucocito-nucleo-lobulado" bySlug={bySlug} onSelect={onSelect}>
{(sel) => (
<>
{NUCLEUS_LOBES.map(([x, y, z], i) => (
<mesh key={i} position={[x, y, z]}>
<sphereGeometry args={[0.13, 18, 18]} />
<meshStandardMaterial {...materialProps("#7c3aed", sel, 0.92)} />
</mesh>
))}
{/* filamentos finos de cromatina conectando os lóbulos */}
{NUCLEUS_LOBES.slice(1).map(([x, y, z], i) => {
const from = new THREE.Vector3(...NUCLEUS_LOBES[i]);
const to = new THREE.Vector3(x, y, z);
const { position, quaternion, length } = cylinderBetween(from, to);
return (
<mesh key={i} position={position} quaternion={quaternion}>
<cylinderGeometry args={[0.02, 0.02, length, 6]} />
<meshStandardMaterial {...materialProps("#7c3aed", sel, 0.92)} />
</mesh>
);
})}
</>
)}
</Organelle>
<Organelle slug="leucocito-granulos" bySlug={bySlug} onSelect={onSelect}>
{(sel) => (
<>
{GRANULE_SPOTS.map(([x, y, z], i) => (
<mesh key={i} position={[x, y, z]}>
<sphereGeometry args={[0.03, 10, 10]} />
<meshStandardMaterial {...materialProps("#fb923c", sel, 0.95)} />
</mesh>
))}
</>
)}
</Organelle>
</>
);
}
@@ -0,0 +1,67 @@
import type { ReactNode } from "react";
import * as THREE from "three";
import { useAtlasStore } from "../../store/useAtlasStore.js";
import type { Structure } from "../../lib/api.js";
// Utilitários compartilhados entre CellDiagram3D.tsx (a célula genérica)
// e CellTypesPage.tsx (galeria de tipos celulares especializados) — os
// dois são diagramas 3D ILUSTRATIVOS (sem malha real disponível pra isso),
// desenhados à mão com primitivas do Three.js, mesmo princípio do boneco
// esquemático de Planos Anatômicos (ver Mannequin.tsx).
export const HIGHLIGHT = new THREE.Color("#22d3ee");
export const BLACK = new THREE.Color(0x000000);
// Função comum (NÃO hook) de propósito — é chamada dentro de .map() e de
// callbacks de render-prop, onde um hook (useMemo/useState) violaria as
// Regras de Hooks do React (número de chamadas por render precisa ser
// sempre igual e não pode depender de laços/condicionais).
export function materialProps(baseColor: string, selected: boolean, opacity = 1) {
return {
color: baseColor,
transparent: opacity < 1,
opacity,
emissive: selected ? HIGHLIGHT : BLACK,
emissiveIntensity: selected ? 0.7 : 0,
};
}
// Posiciona/orienta um cilindro pra ir de verdade do ponto A ao ponto B
// (não só "perto" com um ângulo aproximado) — usado em qualquer prolongamento
// fino (filamento do citoesqueleto, axônio, flagelo...).
export function cylinderBetween(from: THREE.Vector3, to: THREE.Vector3) {
const direction = new THREE.Vector3().subVectors(to, from);
const length = direction.length();
const position = new THREE.Vector3().addVectors(from, to).multiplyScalar(0.5);
const quaternion = new THREE.Quaternion().setFromUnitVectors(
new THREE.Vector3(0, 1, 0),
direction.clone().normalize(),
);
return { position, quaternion, length };
}
export function Organelle({
slug,
bySlug,
onSelect,
children,
}: {
slug: string;
bySlug: Map<string, Structure>;
onSelect: (id: string) => void;
children: (isSelected: boolean) => ReactNode;
}) {
const selectedStructureId = useAtlasStore((s) => s.selectedStructureId);
const structure = bySlug.get(slug);
const isSelected = !!structure && structure.id === selectedStructureId;
return (
<group
onClick={(e) => {
e.stopPropagation();
if (structure) onSelect(structure.id);
}}
>
{children(isSelected)}
</group>
);
}
+14
View File
@@ -18,4 +18,18 @@ export const MICROSCOPY_CREDITS: Record<string, string> = {
"OpenStax College, CC BY 4.0 (Wikimedia Commons).", "OpenStax College, CC BY 4.0 (Wikimedia Commons).",
"/histologia/tecido-nervoso.jpg": "/histologia/tecido-nervoso.jpg":
"Medula espinhal de coelho. John Alan Elson, CC BY-SA 4.0 (Wikimedia Commons).", "Medula espinhal de coelho. John Alan Elson, CC BY-SA 4.0 (Wikimedia Commons).",
"/histologia/epitelio-simples-pavimentoso.jpg":
"Berkshire Community College Bioscience Image Library (BCCB), CC0 (Wikimedia Commons).",
"/histologia/epitelio-simples-cuboidal.jpg":
"Túbulos renais. BCCB, CC0 (Wikimedia Commons).",
"/histologia/epitelio-pseudoestratificado.jpg":
"Traqueia de mamífero. Magscope, CC BY-SA 4.0 (Wikimedia Commons).",
"/histologia/tecido-conjuntivo-frouxo.jpg":
"BCCB, CC0 (Wikimedia Commons).",
"/histologia/cartilagem-elastica.jpg":
"BCCB, CC0 (Wikimedia Commons).",
"/histologia/fibrocartilagem.jpg":
"Magda Yurchishin, CC BY 4.0 (Wikimedia Commons).",
"/histologia/esfregaco-sanguineo.jpg":
"Ajay Kumar Chaurasiya, CC BY-SA 4.0 (Wikimedia Commons).",
}; };
+29
View File
@@ -44,4 +44,33 @@ export const MICROSCOPY_HOTSPOTS: Record<string, { x: number; y: number }> = {
"substancia-branca-histologia": { x: 20, y: 25 }, "substancia-branca-histologia": { x: 20, y: 25 },
"substancia-cinzenta-histologia": { x: 65, y: 35 }, "substancia-cinzenta-histologia": { x: 65, y: 35 },
"corpo-neuronal-histologia": { x: 66, y: 30 }, "corpo-neuronal-histologia": { x: 66, y: 30 },
// Histologia — /histologia/epitelio-simples-pavimentoso.jpg
"epitelio-simples-pavimentoso": { x: 50, y: 50 },
"nucleo-pavimentoso": { x: 22, y: 22 },
// Histologia — /histologia/epitelio-simples-cuboidal.jpg
"epitelio-simples-cuboidal": { x: 30, y: 65 },
"lumen-tubular-renal": { x: 48, y: 22 },
// Histologia — /histologia/epitelio-pseudoestratificado.jpg
"epitelio-pseudoestratificado": { x: 15, y: 12 },
"lamina-propria-traqueia": { x: 40, y: 60 },
// Histologia — /histologia/tecido-conjuntivo-frouxo.jpg
"tecido-conjuntivo-frouxo": { x: 40, y: 55 },
"fibroblasto-frouxo": { x: 58, y: 43 },
// Histologia — /histologia/cartilagem-elastica.jpg
"cartilagem-elastica": { x: 50, y: 45 },
"condrocito-elastica": { x: 30, y: 43 },
// Histologia — /histologia/fibrocartilagem.jpg
"fibrocartilagem": { x: 35, y: 55 },
"fibras-colagenas-fibrocartilagem": { x: 70, y: 15 },
// Histologia — /histologia/esfregaco-sanguineo.jpg
"neutrofilo-esfregaco": { x: 48, y: 42 },
"hemacias-esfregaco": { x: 20, y: 20 },
"plaquetas-esfregaco": { x: 10, y: 8 },
}; };
+189
View File
@@ -0,0 +1,189 @@
import { useMemo, useState } from "react";
import { Canvas } from "@react-three/fiber";
import { CameraControls } from "@react-three/drei";
import { useSystemStructures } from "../hooks/useSystemStructures.js";
import { useAtlasStore } from "../store/useAtlasStore.js";
import {
NeuronModel,
ErythrocyteModel,
MuscleFiberModel,
SpermModel,
LeukocyteModel,
} from "../components/cell/CellTypeModels.js";
// Galeria de tipos celulares especializados — complementa a célula
// genérica (ver CellDiagram3D.tsx, aberta a partir do Atlas) com modelos
// 3D ilustrativos de células que têm uma forma/organela bem diferente do
// "padrão", pensados pra reconhecer cada tipo pelas suas características.
// Mesmo princípio de todo diagrama ilustrativo deste app: sem malha 3D
// real disponível pra isso, desenhado à mão com primitivas do Three.js.
type CellTypeKey = "neuronio" | "hemacia" | "musculo" | "espermatozoide" | "leucocito";
interface CellTypeDef {
key: CellTypeKey;
label: string;
hint: string;
camera: [number, number, number];
Model: typeof NeuronModel;
}
const CELL_TYPES: CellTypeDef[] = [
{
key: "neuronio",
label: "Neurônio",
hint: "Transmite impulsos nervosos — repare nos prolongamentos (dendritos/axônio) e na bainha de mielina.",
camera: [1.2, 0.8, 1.6],
Model: NeuronModel,
},
{
key: "hemacia",
label: "Hemácia (Eritrócito)",
hint: "Transporta oxigênio — a única célula sem núcleo, com o formato de disco bicôncavo característico.",
camera: [0.35, 0.55, 0.95],
Model: ErythrocyteModel,
},
{
key: "musculo",
label: "Célula Muscular Esquelética",
hint: "Fibra longa, multinucleada, com estriações — repare nos núcleos na periferia, não no centro.",
camera: [1.6, 0.6, 1.6],
Model: MuscleFiberModel,
},
{
key: "espermatozoide",
label: "Espermatozoide",
hint: "A única célula humana com flagelo verdadeiro — cabeça, peça intermediária e cauda.",
camera: [0.3, 0.6, 1.3],
Model: SpermModel,
},
{
key: "leucocito",
label: "Leucócito (Neutrófilo)",
hint: "Núcleo polimorfonuclear (vários lóbulos) e grânulos citoplasmáticos com enzimas antimicrobianas.",
camera: [0.9, 0.7, 0.9],
Model: LeukocyteModel,
},
];
const VISIBLE_SYSTEMS = new Set(["citologia"]);
export default function CellTypesPage() {
const [typeKey, setTypeKey] = useState<CellTypeKey>("neuronio");
const [leftOpen, setLeftOpen] = useState(false);
const [rightOpen, setRightOpen] = useState(false);
const selectedStructureId = useAtlasStore((s) => s.selectedStructureId);
const selectStructure = useAtlasStore((s) => s.selectStructure);
const { bySlug: bySlugAll } = useSystemStructures(VISIBLE_SYSTEMS);
const bySlug = useMemo(
() => new Map([...bySlugAll.values()].filter((s) => s.regionPath[1] === "Tipos Celulares").map((s) => [s.slug, s])),
[bySlugAll],
);
const current = CELL_TYPES.find((c) => c.key === typeKey)!;
const selected = selectedStructureId
? [...bySlug.values()].find((s) => s.id === selectedStructureId)
: null;
function changeType(key: CellTypeKey) {
setTypeKey(key);
selectStructure(null);
}
return (
<div className="h-full relative md:grid md:grid-cols-[240px_1fr_320px]">
<aside
className={`fixed md:static inset-y-0 left-0 z-30 w-[86vw] max-w-[260px] md:w-auto
border-r border-white/10 bg-panel p-4 space-y-1.5 overflow-y-auto
transition-transform duration-200 md:translate-x-0
${leftOpen ? "translate-x-0" : "-translate-x-full"}`}
>
<button
onClick={() => setLeftOpen(false)}
className="md:hidden float-right text-slate-400 hover:text-white text-sm px-2 py-1"
aria-label="Fechar painel"
>
✕ Fechar
</button>
<h3 className="text-xs uppercase tracking-wide text-slate-400 mb-2">Tipos Celulares</h3>
{CELL_TYPES.map((c) => (
<button
key={c.key}
onClick={() => changeType(c.key)}
className={`w-full text-left text-sm px-2.5 py-2 rounded ${
c.key === typeKey ? "bg-accent text-white" : "text-slate-300 hover:bg-white/5"
}`}
>
{c.label}
</button>
))}
</aside>
{(leftOpen || rightOpen) && (
<div
className="fixed inset-0 bg-black/50 z-20 md:hidden"
onClick={() => {
setLeftOpen(false);
setRightOpen(false);
}}
/>
)}
<div className="relative h-full min-h-0">
<button
onClick={() => setLeftOpen(true)}
className="md:hidden absolute top-3 left-3 z-10 bg-panel/90 border border-white/10 rounded-lg px-3 py-2 text-xs shadow-lg"
>
☰ Tipos
</button>
<button
onClick={() => setRightOpen(true)}
className="md:hidden absolute top-3 right-3 z-10 bg-panel/90 border border-white/10 rounded-lg px-3 py-2 text-xs shadow-lg"
>
📖 Detalhes
</button>
<Canvas key={typeKey} dpr={[1, 1.5]} camera={{ position: current.camera, fov: 45 }}>
<color attach="background" args={["#0b1220"]} />
<ambientLight intensity={0.7} />
<directionalLight position={[2, 3, 2]} intensity={1} />
<directionalLight position={[-2, -1, -2]} intensity={0.3} />
<current.Model bySlug={bySlug} onSelect={selectStructure} />
<CameraControls makeDefault minDistance={0.3} maxDistance={6} />
</Canvas>
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 z-10 bg-panel/90 border border-white/10 rounded-lg px-3 py-2 text-xs text-slate-300 shadow-lg max-w-[90vw] text-center">
{current.hint}
</div>
</div>
<aside
className={`fixed md:static inset-y-0 right-0 z-30 w-[86vw] max-w-[320px] md:w-auto
border-l border-white/10 bg-panel p-4 overflow-y-auto space-y-3
transition-transform duration-200 md:translate-x-0
${rightOpen ? "translate-x-0" : "translate-x-full"}`}
>
<button
onClick={() => setRightOpen(false)}
className="md:hidden text-slate-400 hover:text-white text-sm px-2 py-1"
aria-label="Fechar painel"
>
✕ Fechar
</button>
<span className="inline-block text-xs px-2 py-1 rounded-full bg-accent/20 text-accent">{current.label}</span>
{selected ? (
<>
<h2 className="text-lg font-semibold text-white leading-snug">{selected.namePt}</h2>
<p className="text-sm text-slate-300 leading-relaxed">{selected.description}</p>
</>
) : (
<p className="text-sm text-slate-400">Clique numa parte do modelo 3D pra ver o que ela é.</p>
)}
<p className="text-[11px] text-slate-500 pt-3 border-t border-white/10 leading-relaxed">
Modelo ilustrativo (não derivado de dado real) — proporções e posições relativas são esquemáticas, pensadas pra reconhecer cada parte. Arraste pra girar, role pra dar zoom.
</p>
</aside>
</div>
);
}
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/celldiagram3d.tsx","./src/components/clippingcontrols.tsx","./src/components/cookieconsent.tsx","./src/components/feedbackwidget.tsx","./src/components/layertoggle.tsx","./src/components/layout.tsx","./src/components/microscopydiagram.tsx","./src/components/onlinecounter.tsx","./src/components/quickregionnav.tsx","./src/components/searchnav.tsx","./src/components/skindiagram3d.tsx","./src/components/structureinfopanel.tsx","./src/components/celldivision/celldivisionscene.tsx","./src/components/viewer/exammarkers.tsx","./src/components/viewer/intercostallabels.tsx","./src/components/viewer/mannequin.tsx","./src/components/viewer/scene.tsx","./src/components/viewer/systemmodel.tsx","./src/data/celldivisioncomponents.ts","./src/data/celldivisionstages.ts","./src/data/geneticsyndromes.ts","./src/data/microscopycredits.ts","./src/data/microscopyhotspots.ts","./src/data/musclereferenceimages.ts","./src/data/physicalexam.ts","./src/hooks/usestructures.ts","./src/hooks/usesystemstructures.ts","./src/lib/analytics.ts","./src/lib/api.ts","./src/lib/bodyregionbox.ts","./src/routes/adminstatspage.tsx","./src/routes/atlaspage.tsx","./src/routes/celldivisionpage.tsx","./src/routes/physicalexampage.tsx","./src/routes/planespage.tsx","./src/routes/quizpage.tsx","./src/store/useatlasstore.ts"],"version":"5.9.3"} {"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/celldiagram3d.tsx","./src/components/clippingcontrols.tsx","./src/components/cookieconsent.tsx","./src/components/feedbackwidget.tsx","./src/components/layertoggle.tsx","./src/components/layout.tsx","./src/components/microscopydiagram.tsx","./src/components/onlinecounter.tsx","./src/components/quickregionnav.tsx","./src/components/searchnav.tsx","./src/components/skindiagram3d.tsx","./src/components/structureinfopanel.tsx","./src/components/cell/celltypemodels.tsx","./src/components/cell/cellshared.tsx","./src/components/celldivision/celldivisionscene.tsx","./src/components/viewer/exammarkers.tsx","./src/components/viewer/intercostallabels.tsx","./src/components/viewer/mannequin.tsx","./src/components/viewer/scene.tsx","./src/components/viewer/systemmodel.tsx","./src/data/celldivisioncomponents.ts","./src/data/celldivisionstages.ts","./src/data/geneticsyndromes.ts","./src/data/microscopycredits.ts","./src/data/microscopyhotspots.ts","./src/data/musclereferenceimages.ts","./src/data/physicalexam.ts","./src/hooks/usestructures.ts","./src/hooks/usesystemstructures.ts","./src/lib/analytics.ts","./src/lib/api.ts","./src/lib/bodyregionbox.ts","./src/routes/adminstatspage.tsx","./src/routes/atlaspage.tsx","./src/routes/celldivisionpage.tsx","./src/routes/celltypespage.tsx","./src/routes/physicalexampage.tsx","./src/routes/planespage.tsx","./src/routes/quizpage.tsx","./src/store/useatlasstore.ts"],"version":"5.9.3"}