componentes da celula

Signed-off-by: Rodrigo Bertoldo de Sousa Martins <rodrigo@martinsevieira.adv.br>
This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-09-18 10:20:10 -03:00
1 parent 84ee99aabf
commit 3195bdd724
4 files changed
+179 -20

No files matched your search

@@ -2,15 +2,19 @@ import { useMemo, useRef } from "react";
import { useFrame } from "@react-three/fiber";
import * as THREE from "three";
import type { Stage } from "../../data/cellDivisionStages.js";
import type { CellComponentId } from "../../data/cellDivisionComponents.js";
// Cena 3D ilustrativa (mesmo princípio do CellDiagram3D.tsx e do
// Mannequin.tsx: sem malha real disponível pra esse conteúdo, então
// desenhamos à mão com primitivas do Three.js) — recebe só o ESTÁGIO alvo
// atual (`stage`) e anima suavemente toda célula/cromátide até lá sempre
// que o estágio muda, via interpolação por frame (useFrame), sem precisar
// de nenhuma biblioteca de animação.
// de nenhuma biblioteca de animação. Também aceita clique nos componentes
// (cromossomo, centrossomo, fibra) — ver `onSelect`.
const MAX_CELLS = 4;
const HIGHLIGHT = new THREE.Color("#22d3ee");
const BLACK = new THREE.Color(0x000000);
// Fator de suavização independente de framerate: a cada frame, anda uma
// fração `k` do caminho que falta até o alvo — converge suave em ~0.4s
@@ -19,20 +23,31 @@ function smoothing(delta: number, halfLifeSeconds = 0.18) {
return 1 - Math.pow(0.5, delta / halfLifeSeconds);
}
function ChromatidShape({ color }: { color: string }) {
interface SelectionProps {
selected: CellComponentId | null;
onSelect: (id: CellComponentId) => void;
}
function ChromatidShape({ color, isSelected }: { color: string; isSelected: boolean }) {
const matProps = {
color,
roughness: 0.5,
emissive: isSelected ? HIGHLIGHT : BLACK,
emissiveIntensity: isSelected ? 0.8 : 0,
};
return (
<group>
<mesh position={[-0.045, 0.08, 0]} rotation={[0, 0, Math.PI / 7]}>
<capsuleGeometry args={[0.022, 0.14, 3, 6]} />
<meshStandardMaterial color={color} roughness={0.5} />
<meshStandardMaterial {...matProps} />
</mesh>
<mesh position={[0.045, 0.08, 0]} rotation={[0, 0, -Math.PI / 7]}>
<capsuleGeometry args={[0.022, 0.14, 3, 6]} />
<meshStandardMaterial color={color} roughness={0.5} />
<meshStandardMaterial {...matProps} />
</mesh>
<mesh>
<sphereGeometry args={[0.03, 10, 10]} />
<meshStandardMaterial color={color} roughness={0.5} />
<meshStandardMaterial {...matProps} />
</mesh>
</group>
);
@@ -44,7 +59,15 @@ interface ChromatidTargetWorld {
color: string;
}
function AnimatedChromatid({ id, targets }: { id: string; targets: Map<string, ChromatidTargetWorld> }) {
function AnimatedChromatid({
id,
targets,
selected,
onSelect,
}: {
id: string;
targets: Map<string, ChromatidTargetWorld>;
} & SelectionProps) {
// Posição/rotação são controladas 100% via useFrame (nunca como prop
// JSX reativa) — se passássemos `position={...}` no JSX, o
// react-three-fiber reaplicaria (e "teleportaria") o objeto pra lá toda
@@ -65,8 +88,14 @@ function AnimatedChromatid({ id, targets }: { id: string; targets: Map<string, C
});
return (
<group ref={ref}>
<ChromatidShape color={initialColor} />
<group
ref={ref}
onClick={(e) => {
e.stopPropagation();
onSelect("cromossomo");
}}
>
<ChromatidShape color={initialColor} isSelected={selected === "cromossomo"} />
</group>
);
}
@@ -83,6 +112,8 @@ interface CellSlotState {
function AnimatedCellSlot({
target,
state,
selected,
onSelect,
}: {
target: {
center: [number, number, number];
@@ -93,7 +124,7 @@ function AnimatedCellSlot({
elongated: number;
} | null;
state: CellSlotState;
}) {
} & SelectionProps) {
const groupRef = useRef<THREE.Group>(null);
const membraneRef = useRef<THREE.Mesh>(null);
const membraneMatRef = useRef<THREE.MeshStandardMaterial>(null);
@@ -139,9 +170,17 @@ function AnimatedCellSlot({
}
});
const centrosomeSelected = selected === "centrossomo";
const fiberSelected = selected === "fibras";
return (
<group ref={groupRef}>
{/* membrana plasmática — translúcida, não recebe clique (só decorativa) */}
{/* membrana plasmática e envelope nuclear: sem raycast (`raycast={() =>
null}`) de propósito — sendo "cascas" que envolvem tudo o resto,
elas sempre seriam o primeiro acerto de qualquer raio de clique,
impedindo o clique de alcançar cromossomo/centrossomo/fibra por
dentro. Continuam explicáveis pela lista de componentes no
painel (CellDivisionPage.tsx), só não são clicáveis AQUI. */}
<mesh ref={membraneRef} raycast={() => null}>
<sphereGeometry args={[0.8, 28, 28]} />
<meshStandardMaterial
@@ -153,7 +192,6 @@ function AnimatedCellSlot({
depthWrite={false}
/>
</mesh>
{/* envelope nuclear — esfera um pouco menor, desfaz/refaz conforme a fase */}
<mesh raycast={() => null}>
<sphereGeometry args={[0.42, 24, 24]} />
<meshStandardMaterial ref={envelopeMatRef} color="#c4b5fd" transparent opacity={0} depthWrite={false} />
@@ -163,9 +201,20 @@ function AnimatedCellSlot({
da prófase. */}
<group ref={centrosomesRef}>
{[-1, 1].map((side) => (
<mesh key={side} position={[side * 0.65, 0, 0]}>
<mesh
key={side}
position={[side * 0.65, 0, 0]}
onClick={(e) => {
e.stopPropagation();
onSelect("centrossomo");
}}
>
<sphereGeometry args={[0.035, 10, 10]} />
<meshStandardMaterial color="#f8fafc" />
<meshStandardMaterial
color="#f8fafc"
emissive={centrosomeSelected ? HIGHLIGHT : BLACK}
emissiveIntensity={centrosomeSelected ? 0.8 : 0}
/>
</mesh>
))}
</group>
@@ -178,9 +227,21 @@ function AnimatedCellSlot({
key={i}
position={[side * 0.65 + side * -0.32, offset * 0.6, offset]}
rotation={[0, 0, side > 0 ? Math.PI / 2 + offset * 0.3 : -Math.PI / 2 + offset * 0.3]}
onClick={(e) => {
e.stopPropagation();
onSelect("fibras");
}}
>
<cylinderGeometry args={[0.004, 0.004, 0.64, 4]} />
<meshStandardMaterial color="#f8fafc" transparent opacity={0.4} />
{/* raio um pouco maior só pra facilitar o clique — a fibra
visual continua fina, isso é puramente a área alvo */}
<cylinderGeometry args={[0.02, 0.02, 0.64, 6]} />
<meshStandardMaterial
color="#f8fafc"
transparent
opacity={fiberSelected ? 0.9 : 0.4}
emissive={fiberSelected ? HIGHLIGHT : BLACK}
emissiveIntensity={fiberSelected ? 0.6 : 0}
/>
</mesh>
))}
</group>
@@ -190,7 +251,7 @@ function AnimatedCellSlot({
);
}
export function CellDivisionScene({ stage }: { stage: Stage }) {
export function CellDivisionScene({ stage, selected, onSelect }: { stage: Stage } & SelectionProps) {
// Estado imperativo (fora do React) por slot de célula — persiste entre
// mudanças de estágio pra animação continuar de onde estava, sem exigir
// que o número de células seja o mesmo do estágio anterior.
@@ -235,11 +296,11 @@ export function CellDivisionScene({ stage }: { stage: Stage }) {
<directionalLight position={[-2, -1, -2]} intensity={0.3} />
{cellStates.map((state, i) => (
<AnimatedCellSlot key={i} target={stage.cells[i] ?? null} state={state} />
<AnimatedCellSlot key={i} target={stage.cells[i] ?? null} state={state} selected={selected} onSelect={onSelect} />
))}
{chromatidIds.map((id) => (
<AnimatedChromatid key={id} id={id} targets={chromatidTargets} />
<AnimatedChromatid key={id} id={id} targets={chromatidTargets} selected={selected} onSelect={onSelect} />
))}
</>
);
@@ -0,0 +1,67 @@
// Glossário dos componentes que aparecem na cena 3D de mitose/meiose —
// clicável tanto direto no modelo 3D (cromossomos, centrossomos, fibras)
// quanto pela lista no painel (que também cobre membrana e envelope
// nuclear, que por design não recebem clique 3D — ver nota em
// CellDivisionScene.tsx sobre por que a "casca" que envolve tudo não pode
// ter raycast, senão intercepta o clique destinado ao que está dentro).
export type CellComponentId =
| "membrana"
| "envelope"
| "cromossomo"
| "centromero"
| "centrossomo"
| "fibras";
export interface CellComponentInfo {
id: CellComponentId;
label: string;
clickable3d: boolean;
description: string;
}
export const CELL_COMPONENTS: CellComponentInfo[] = [
{
id: "membrana",
label: "Membrana Plasmática",
clickable3d: false,
description:
"Envolve toda a célula e controla o que entra e sai dela. Na citocinese, ela se estrangula ao meio (sulco de clivagem) até se dividir em duas membranas independentes, uma pra cada célula-filha.",
},
{
id: "envelope",
label: "Envelope Nuclear",
clickable3d: false,
description:
"Membrana dupla que separa o DNA do restante da célula durante a interfase. Se desfaz na prófase — só assim as fibras do fuso, que ficam no citoplasma, conseguem alcançar os cromossomos — e se reforma na telófase ao redor de cada novo conjunto de cromossomos.",
},
{
id: "cromossomo",
label: "Cromossomo (cromátide)",
clickable3d: true,
description:
"DNA condensado e enrolado em proteínas (histonas), carregando os genes. Cada cor no modelo representa um cromossomo diferente. Quando duplicado (fase S), tem 2 cromátides-irmãs idênticas — o \"X\" clássico; depois de se separarem (anáfase), cada cromátide passa a ser um cromossomo independente.",
},
{
id: "centromero",
label: "Centrômero",
clickable3d: false,
description:
"A região que mantém as duas cromátides-irmãs de um cromossomo duplicado unidas (o \"cruzamento\" do X). É ali que as fibras do fuso se prendem (numa estrutura chamada cinetócoro) pra puxar cada cromátide pra um polo na anáfase.",
},
{
id: "centrossomo",
label: "Centrossomo",
clickable3d: true,
description:
"Organiza os microtúbulos da célula. Se duplica na interfase (por isso aparecem 2, desde a G2) e migra pra polos opostos, virando o ponto de partida das fibras do fuso.",
},
{
id: "fibras",
label: "Fibras do Fuso (microtúbulos)",
clickable3d: true,
description:
"\"Cabos\" de microtúbulos que saem dos centrossomos e se prendem ao centrômero de cada cromossomo. São elas que empurram/puxam os cromossomos — primeiro alinhando-os na placa metafásica, depois puxando as cromátides-irmãs (ou os homólogos, na Meiose I) pra polos opostos na anáfase.",
},
];
export const CELL_COMPONENT_BY_ID = new Map(CELL_COMPONENTS.map((c) => [c.id, c]));
+32 -1
View File
@@ -9,6 +9,7 @@ import {
MEIOSIS_II_ERROR_STAGES,
} from "../data/cellDivisionStages.js";
import { GENETIC_SYNDROMES, ERROR_MECHANISM_LABELS, type ErrorMechanism } from "../data/geneticSyndromes.js";
import { CELL_COMPONENTS, CELL_COMPONENT_BY_ID, type CellComponentId } from "../data/cellDivisionComponents.js";
type Mode = "mitose" | "meiose" | "erros";
type ErrorType = "meiose1" | "meiose2";
@@ -69,6 +70,7 @@ export default function CellDivisionPage() {
const [leftOpen, setLeftOpen] = useState(false);
const [rightOpen, setRightOpen] = useState(false);
const [showSyndromeModal, setShowSyndromeModal] = useState(false);
const [selectedComponent, setSelectedComponent] = useState<CellComponentId | null>(null);
const stages = useMemo(() => {
if (mode === "mitose") return MITOSIS_STAGES;
@@ -250,7 +252,7 @@ export default function CellDivisionPage() {
</button>
<Canvas dpr={[1, 1.5]} camera={{ position: [0, 1.4, 5.4], fov: 42 }}>
<CellDivisionScene stage={stage} />
<CellDivisionScene stage={stage} selected={selectedComponent} onSelect={setSelectedComponent} />
<CameraControls makeDefault minDistance={2} maxDistance={12} />
</Canvas>
@@ -320,6 +322,35 @@ export default function CellDivisionPage() {
<h2 className="text-lg font-semibold text-white leading-snug">{stage.label}</h2>
<p className="text-sm text-slate-300 leading-relaxed">{stage.description}</p>
<div className="pt-3 border-t border-white/10 space-y-2">
<p className="text-[11px] uppercase tracking-wide text-slate-500">
Componentes da célula <span className="normal-case text-slate-600">— clique num deles (na cena ou aqui)</span>
</p>
<div className="flex flex-wrap gap-1.5">
{CELL_COMPONENTS.map((c) => (
<button
key={c.id}
onClick={() => setSelectedComponent(c.id)}
className={`text-xs px-2.5 py-1.5 rounded-full border ${
selectedComponent === c.id
? "bg-accent text-white border-accent"
: "border-white/10 bg-white/5 text-slate-300 hover:bg-white/10"
}`}
>
{c.label}
</button>
))}
</div>
{selectedComponent && (
<div className="rounded-lg border border-accent/30 bg-accent/10 p-2.5 mt-1">
<p className="text-sm font-medium text-white">{CELL_COMPONENT_BY_ID.get(selectedComponent)?.label}</p>
<p className="text-xs text-slate-300 mt-1 leading-relaxed">
{CELL_COMPONENT_BY_ID.get(selectedComponent)?.description}
</p>
</div>
)}
</div>
{mode === "erros" && (
<div className="pt-3 border-t border-white/10 space-y-2">
<p className="text-[11px] uppercase tracking-wide text-slate-500">Síndromes ligadas a este erro</p>
+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/mannequin.tsx","./src/components/viewer/scene.tsx","./src/components/viewer/systemmodel.tsx","./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/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/celldivision/celldivisionscene.tsx","./src/components/viewer/exammarkers.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/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"}