mitose e meiose
Signed-off-by: Rodrigo Bertoldo de Sousa Martins <rodrigo@martinsevieira.adv.br>
This commit is contained in:
1 parent
a972670b7d
commit
f32833c023
6 files changed
+778
-1
No files matched your search
@@ -9,6 +9,7 @@ import { Layout } from "./components/Layout.js";
|
||||
import AtlasPage from "./routes/AtlasPage.js";
|
||||
const PlanesPage = lazy(() => import("./routes/PlanesPage.js"));
|
||||
const PhysicalExamPage = lazy(() => import("./routes/PhysicalExamPage.js"));
|
||||
const CellDivisionPage = lazy(() => import("./routes/CellDivisionPage.js"));
|
||||
const QuizPage = lazy(() => import("./routes/QuizPage.js"));
|
||||
const AdminStatsPage = lazy(() => import("./routes/AdminStatsPage.js"));
|
||||
|
||||
@@ -20,6 +21,7 @@ export default function App() {
|
||||
<Route path="/" element={<AtlasPage />} />
|
||||
<Route path="/planos-anatomicos" element={<PlanesPage />} />
|
||||
<Route path="/exame-fisico" element={<PhysicalExamPage />} />
|
||||
<Route path="/divisao-celular" element={<CellDivisionPage />} />
|
||||
<Route path="/quiz" element={<QuizPage />} />
|
||||
</Route>
|
||||
{/* Fora do Layout: painel interno, sem o menu/nav do atlas. */}
|
||||
|
||||
@@ -32,6 +32,9 @@ export function Layout() {
|
||||
<NavLink to="/exame-fisico" className={linkClass}>
|
||||
Exame Físico
|
||||
</NavLink>
|
||||
<NavLink to="/divisao-celular" className={linkClass}>
|
||||
Mitose e Meiose
|
||||
</NavLink>
|
||||
<NavLink to="/quiz" className={linkClass}>
|
||||
Quiz
|
||||
</NavLink>
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
import { useMemo, useRef } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import * as THREE from "three";
|
||||
import type { Stage } from "../../data/cellDivisionStages.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.
|
||||
|
||||
const MAX_CELLS = 4;
|
||||
|
||||
// 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
|
||||
// sem "overshoot" e sem depender de quantos FPS o navegador está rendendo.
|
||||
function smoothing(delta: number, halfLifeSeconds = 0.18) {
|
||||
return 1 - Math.pow(0.5, delta / halfLifeSeconds);
|
||||
}
|
||||
|
||||
function ChromatidShape({ color }: { color: string }) {
|
||||
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} />
|
||||
</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} />
|
||||
</mesh>
|
||||
<mesh>
|
||||
<sphereGeometry args={[0.03, 10, 10]} />
|
||||
<meshStandardMaterial color={color} roughness={0.5} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
interface ChromatidTargetWorld {
|
||||
pos: THREE.Vector3;
|
||||
rotDeg: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
function AnimatedChromatid({ id, targets }: { id: string; targets: Map<string, ChromatidTargetWorld> }) {
|
||||
// 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
|
||||
// vez que o mapa `targets` mudasse de identidade (ou seja, a cada troca
|
||||
// de estágio), brigando com o lerp suave abaixo. No primeiro quadro o
|
||||
// grupo nasce em (0,0,0) e "voa" suavemente até a posição inicial —
|
||||
// efeito colateral aceitável (só acontece uma vez, ao montar).
|
||||
const ref = useRef<THREE.Group>(null);
|
||||
const initialColor = targets.get(id)?.color ?? "#94a3b8";
|
||||
|
||||
useFrame((_, delta) => {
|
||||
const target = targets.get(id);
|
||||
if (!ref.current || !target) return;
|
||||
const k = smoothing(delta);
|
||||
ref.current.position.lerp(target.pos, k);
|
||||
const targetRad = THREE.MathUtils.degToRad(target.rotDeg);
|
||||
ref.current.rotation.z = THREE.MathUtils.lerp(ref.current.rotation.z, targetRad, k);
|
||||
});
|
||||
|
||||
return (
|
||||
<group ref={ref}>
|
||||
<ChromatidShape color={initialColor} />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
interface CellSlotState {
|
||||
position: THREE.Vector3;
|
||||
radius: { value: number };
|
||||
envelope: { value: number };
|
||||
spindle: { value: number };
|
||||
elongated: { value: number };
|
||||
}
|
||||
|
||||
function AnimatedCellSlot({
|
||||
target,
|
||||
state,
|
||||
}: {
|
||||
target: { center: [number, number, number]; radius: number; envelope: number; spindle: boolean; elongated: number } | null;
|
||||
state: CellSlotState;
|
||||
}) {
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const membraneRef = useRef<THREE.Mesh>(null);
|
||||
const membraneMatRef = useRef<THREE.MeshStandardMaterial>(null);
|
||||
const envelopeMatRef = useRef<THREE.MeshStandardMaterial>(null);
|
||||
const spindleRef = useRef<THREE.Group>(null);
|
||||
|
||||
useFrame((_, delta) => {
|
||||
const k = smoothing(delta);
|
||||
const targetPos = target ? new THREE.Vector3(...target.center) : state.position.clone();
|
||||
const targetRadius = target ? target.radius : 0;
|
||||
const targetEnvelope = target ? target.envelope : 0;
|
||||
const targetSpindle = target?.spindle ? 1 : 0;
|
||||
const targetElongated = target ? target.elongated : 0;
|
||||
|
||||
state.position.lerp(targetPos, k);
|
||||
state.radius.value = THREE.MathUtils.lerp(state.radius.value, targetRadius, k);
|
||||
state.envelope.value = THREE.MathUtils.lerp(state.envelope.value, targetEnvelope, k);
|
||||
state.spindle.value = THREE.MathUtils.lerp(state.spindle.value, targetSpindle, k);
|
||||
state.elongated.value = THREE.MathUtils.lerp(state.elongated.value, targetElongated, k);
|
||||
|
||||
if (groupRef.current) groupRef.current.position.copy(state.position);
|
||||
if (membraneRef.current) {
|
||||
const r = state.radius.value;
|
||||
const stretch = 1 + state.elongated.value * 0.7;
|
||||
membraneRef.current.scale.set(r / 0.8, (r / 0.8) * stretch, r / 0.8);
|
||||
}
|
||||
if (membraneMatRef.current) membraneMatRef.current.opacity = Math.min(0.22, state.radius.value * 0.28);
|
||||
if (envelopeMatRef.current) envelopeMatRef.current.opacity = state.envelope.value * 0.35;
|
||||
if (spindleRef.current) spindleRef.current.visible = state.spindle.value > 0.05;
|
||||
if (spindleRef.current) spindleRef.current.scale.setScalar(state.spindle.value);
|
||||
});
|
||||
|
||||
return (
|
||||
<group ref={groupRef}>
|
||||
{/* membrana plasmática — translúcida, não recebe clique (só decorativa) */}
|
||||
<mesh ref={membraneRef} raycast={() => null}>
|
||||
<sphereGeometry args={[0.8, 28, 28]} />
|
||||
<meshStandardMaterial
|
||||
ref={membraneMatRef}
|
||||
color="#93c5fd"
|
||||
transparent
|
||||
opacity={0.15}
|
||||
side={THREE.DoubleSide}
|
||||
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} />
|
||||
</mesh>
|
||||
{/* fuso mitótico: 2 centrossomos + fibras finas em leque */}
|
||||
<group ref={spindleRef}>
|
||||
{[-1, 1].map((side) => (
|
||||
<group key={side} position={[side * 0.65, 0, 0]}>
|
||||
<mesh>
|
||||
<sphereGeometry args={[0.035, 10, 10]} />
|
||||
<meshStandardMaterial color="#f8fafc" />
|
||||
</mesh>
|
||||
{[-0.25, -0.1, 0.1, 0.25].map((offset, i) => (
|
||||
<mesh
|
||||
key={i}
|
||||
position={[side * -0.32, offset * 0.6, offset]}
|
||||
rotation={[0, 0, side > 0 ? Math.PI / 2 + offset * 0.3 : -Math.PI / 2 + offset * 0.3]}
|
||||
>
|
||||
<cylinderGeometry args={[0.004, 0.004, 0.64, 4]} />
|
||||
<meshStandardMaterial color="#f8fafc" transparent opacity={0.4} />
|
||||
</mesh>
|
||||
))}
|
||||
</group>
|
||||
))}
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export function CellDivisionScene({ stage }: { stage: Stage }) {
|
||||
// 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.
|
||||
const cellStates = useMemo<CellSlotState[]>(
|
||||
() =>
|
||||
Array.from({ length: MAX_CELLS }, () => ({
|
||||
position: new THREE.Vector3(0, 0, 0),
|
||||
radius: { value: 0 },
|
||||
envelope: { value: 0 },
|
||||
spindle: { value: 0 },
|
||||
elongated: { value: 0 },
|
||||
})),
|
||||
[],
|
||||
);
|
||||
|
||||
const chromatidTargets = useMemo(() => {
|
||||
const map = new Map<string, ChromatidTargetWorld>();
|
||||
for (const c of stage.chromatids) {
|
||||
const cell = stage.cells[c.cell];
|
||||
if (!cell) continue;
|
||||
map.set(c.id, {
|
||||
pos: new THREE.Vector3(cell.center[0] + c.pos[0], cell.center[1] + c.pos[1], cell.center[2] + c.pos[2]),
|
||||
rotDeg: c.rot,
|
||||
color: c.color,
|
||||
});
|
||||
}
|
||||
return map;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [stage.id]);
|
||||
|
||||
// IDs fixos (as mesmas 8 cromátides existem em todo estágio de mitose OU
|
||||
// meiose) — pegos do primeiro estágio pra não remontar componentes ao
|
||||
// trocar de estágio (só a posição-alvo muda).
|
||||
const chromatidIds = useMemo(() => stage.chromatids.map((c) => c.id), []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<>
|
||||
<color attach="background" args={["#0b1220"]} />
|
||||
<ambientLight intensity={0.75} />
|
||||
<directionalLight position={[2, 3, 2]} intensity={1} />
|
||||
<directionalLight position={[-2, -1, -2]} intensity={0.3} />
|
||||
|
||||
{cellStates.map((state, i) => (
|
||||
<AnimatedCellSlot key={i} target={stage.cells[i] ?? null} state={state} />
|
||||
))}
|
||||
|
||||
{chromatidIds.map((id) => (
|
||||
<AnimatedChromatid key={id} id={id} targets={chromatidTargets} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
// Modelo ILUSTRATIVO (mesma filosofia do CellDiagram3D.tsx e do Mannequin.tsx
|
||||
// — não existe malha 3D real de mitose/meiose pra estudo, então desenhamos
|
||||
// à mão com formas simples do Three.js) de divisão celular, simplificado
|
||||
// pra uma célula humana com 2n=4 (2 pares de cromossomos homólogos) em vez
|
||||
// de 46 — o suficiente pra mostrar os conceitos-chave (condensação,
|
||||
// alinhamento, separação, citocinese; em meiose: pareamento dos homólogos,
|
||||
// permutação genética/crossing-over, e a divisão reducional) sem virar
|
||||
// uma bagunça visual com dezenas de cromossomos.
|
||||
//
|
||||
// Cada cromossomo tem 2 cromátides-irmãs (mesma cor, já que são cópias
|
||||
// idênticas uma da outra) que existem como entidades independentes durante
|
||||
// TODA a animação — antes de se separarem elas simplesmente ocupam a
|
||||
// MESMA posição (e por isso se desenham sobrepostas, formando o "X"
|
||||
// clássico do cromossomo duplicado); depois de se separarem, cada uma
|
||||
// anima pra uma posição diferente e passa a se parecer com um cromossomo
|
||||
// simples. Isso evita precisar de um estado "duplicado: sim/não" à parte
|
||||
// — a posição já conta a história.
|
||||
|
||||
export interface ChromatidTarget {
|
||||
id: string; // estável durante toda a animação (mitose OU meiose)
|
||||
color: string;
|
||||
cell: number; // índice em `cells` neste estágio
|
||||
pos: [number, number, number]; // posição local ao centro da célula
|
||||
rot: number; // rotação (graus) em torno de Z — só estética
|
||||
}
|
||||
|
||||
export interface CellTarget {
|
||||
center: [number, number, number];
|
||||
radius: number;
|
||||
envelope: number; // opacidade do envelope nuclear (0 = dissolvido, 1 = intacto)
|
||||
spindle: boolean; // mostra fuso mitótico (centrossomos + fibras) neste estágio
|
||||
elongated: number; // 0 = esférica, 1 = alongada com sulco de clivagem (citocinese em andamento)
|
||||
}
|
||||
|
||||
export interface Stage {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
cells: CellTarget[];
|
||||
chromatids: ChromatidTarget[];
|
||||
}
|
||||
|
||||
// ---- Cromossomos-base (2n = 4) ----------------------------------------
|
||||
// b/c = par homólogo 1 (materno/paterno); o/y = par homólogo 2.
|
||||
const COLOR_B = "#4f7cff"; // azul
|
||||
const COLOR_C = "#22d3ee"; // ciano — homólogo de B
|
||||
const COLOR_O = "#f59e0b"; // laranja
|
||||
const COLOR_Y = "#eab308"; // amarelo — homólogo de O
|
||||
|
||||
const R = 0.8; // raio "padrão" da célula (membrana)
|
||||
const POLE = 0.55;
|
||||
const FINAL_POLE = 1.2;
|
||||
|
||||
function pair(idA: string, idB: string, color: string, cell: number, pos: [number, number, number]): ChromatidTarget[] {
|
||||
return [
|
||||
{ id: idA, color, cell, pos, rot: 0 },
|
||||
{ id: idB, color, cell, pos, rot: 180 },
|
||||
];
|
||||
}
|
||||
|
||||
function single(id: string, color: string, cell: number, pos: [number, number, number], rot: number): ChromatidTarget {
|
||||
return { id, color, cell, pos, rot };
|
||||
}
|
||||
|
||||
// ============================= MITOSE ===================================
|
||||
// As 4 cromátides-irmãs (uma de cada cromossomo) vão pro polo A, as outras
|
||||
// 4 pro polo B — as duas células-filhas ficam geneticamente IDÊNTICAS à
|
||||
// célula-mãe (e entre si), com o mesmo número de cromossomos (2n=4).
|
||||
|
||||
export const MITOSIS_STAGES: Stage[] = [
|
||||
{
|
||||
id: "interfase",
|
||||
label: "Interfase",
|
||||
description:
|
||||
"A célula não está se dividindo: cresce e, na fase S, duplica todo o seu DNA. Os cromossomos já estão duplicados, mas ainda descondensados (finos, dispersos) — por isso não aparecem como bastões nítidos ainda.",
|
||||
cells: [{ center: [0, 0, 0], radius: R, envelope: 1, spindle: false, elongated: 0 }],
|
||||
chromatids: [
|
||||
...pair("b1a", "b1b", COLOR_B, 0, [-0.28, 0.22, 0.1]),
|
||||
...pair("c1a", "c1b", COLOR_C, 0, [0.05, 0.3, -0.15]),
|
||||
...pair("o1a", "o1b", COLOR_O, 0, [0.25, -0.15, 0.2]),
|
||||
...pair("y1a", "y1b", COLOR_Y, 0, [-0.1, -0.28, -0.1]),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "prof",
|
||||
label: "Prófase",
|
||||
description:
|
||||
"A cromatina se condensa em cromossomos visíveis (cada um já duplicado, com 2 cromátides-irmãs unidas pelo centrômero — o \"X\"). Os centrossomos começam a migrar para polos opostos e o envelope nuclear começa a se desfazer.",
|
||||
cells: [{ center: [0, 0, 0], radius: R, envelope: 0.6, spindle: true, elongated: 0 }],
|
||||
chromatids: [
|
||||
...pair("b1a", "b1b", COLOR_B, 0, [-0.26, 0.2, 0.08]),
|
||||
...pair("c1a", "c1b", COLOR_C, 0, [0.06, 0.28, -0.12]),
|
||||
...pair("o1a", "o1b", COLOR_O, 0, [0.24, -0.14, 0.18]),
|
||||
...pair("y1a", "y1b", COLOR_Y, 0, [-0.1, -0.26, -0.08]),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "prometaf",
|
||||
label: "Prometáfase",
|
||||
description:
|
||||
"O envelope nuclear se dissolve por completo. As fibras do fuso mitótico (vindas dos dois centrossomos) se prendem ao centrômero de cada cromossomo e começam a puxá-los em direção ao centro da célula.",
|
||||
cells: [{ center: [0, 0, 0], radius: R, envelope: 0, spindle: true, elongated: 0 }],
|
||||
chromatids: [
|
||||
...pair("b1a", "b1b", COLOR_B, 0, [-0.3, 0.05, 0.05]),
|
||||
...pair("c1a", "c1b", COLOR_C, 0, [-0.12, -0.05, -0.05]),
|
||||
...pair("o1a", "o1b", COLOR_O, 0, [0.12, 0.05, 0.05]),
|
||||
...pair("y1a", "y1b", COLOR_Y, 0, [0.3, -0.05, -0.05]),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "metaf",
|
||||
label: "Metáfase",
|
||||
description:
|
||||
"Os cromossomos ficam alinhados no \"equador\" da célula (placa metafásica), cada um preso a fibras do fuso vindas dos dois polos — é o ponto de checagem antes da separação começar.",
|
||||
cells: [{ center: [0, 0, 0], radius: R, envelope: 0, spindle: true, elongated: 0 }],
|
||||
chromatids: [
|
||||
...pair("b1a", "b1b", COLOR_B, 0, [-0.3, 0, 0]),
|
||||
...pair("c1a", "c1b", COLOR_C, 0, [-0.1, 0, 0]),
|
||||
...pair("o1a", "o1b", COLOR_O, 0, [0.1, 0, 0]),
|
||||
...pair("y1a", "y1b", COLOR_Y, 0, [0.3, 0, 0]),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "anaf",
|
||||
label: "Anáfase",
|
||||
description:
|
||||
"As cromátides-irmãs se separam no centrômero e são puxadas para polos opostos — cada uma agora é um cromossomo independente. A célula começa a se alongar.",
|
||||
cells: [{ center: [0, 0, 0], radius: R, envelope: 0, spindle: true, elongated: 0.4 }],
|
||||
chromatids: [
|
||||
single("b1a", COLOR_B, 0, [-POLE, 0.12, 0.08], 0),
|
||||
single("c1a", COLOR_C, 0, [-POLE, -0.05, -0.1], 20),
|
||||
single("o1a", COLOR_O, 0, [-POLE, 0.05, -0.08], -15),
|
||||
single("y1a", COLOR_Y, 0, [-POLE, -0.15, 0.05], 10),
|
||||
single("b1b", COLOR_B, 0, [POLE, -0.12, -0.08], 180),
|
||||
single("c1b", COLOR_C, 0, [POLE, 0.05, 0.1], 160),
|
||||
single("o1b", COLOR_O, 0, [POLE, -0.05, 0.08], 195),
|
||||
single("y1b", COLOR_Y, 0, [POLE, 0.15, -0.05], 170),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "telof",
|
||||
label: "Telófase",
|
||||
description:
|
||||
"Um conjunto completo de cromossomos chega a cada polo e um novo envelope nuclear se forma ao redor de cada um. Os cromossomos começam a descondensar de novo e o fuso desaparece.",
|
||||
cells: [{ center: [0, 0, 0], radius: R, envelope: 0.7, spindle: false, elongated: 0.85 }],
|
||||
chromatids: [
|
||||
single("b1a", COLOR_B, 0, [-1.0, 0.1, 0.06], 0),
|
||||
single("c1a", COLOR_C, 0, [-1.0, -0.04, -0.08], 20),
|
||||
single("o1a", COLOR_O, 0, [-1.0, 0.04, -0.06], -15),
|
||||
single("y1a", COLOR_Y, 0, [-1.0, -0.12, 0.04], 10),
|
||||
single("b1b", COLOR_B, 0, [1.0, -0.1, -0.06], 180),
|
||||
single("c1b", COLOR_C, 0, [1.0, 0.04, 0.08], 160),
|
||||
single("o1b", COLOR_O, 0, [1.0, -0.04, 0.06], 195),
|
||||
single("y1b", COLOR_Y, 0, [1.0, 0.12, -0.04], 170),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "cito",
|
||||
label: "Citocinese",
|
||||
description:
|
||||
"A membrana se estrangula ao meio (sulco de clivagem) até formar DUAS células-filhas, cada uma com um conjunto completo e IDÊNTICO de 4 cromossomos (2n=4) — geneticamente iguais entre si e à célula original.",
|
||||
cells: [
|
||||
{ center: [-FINAL_POLE, 0, 0], radius: 0.62, envelope: 0.9, spindle: false, elongated: 0 },
|
||||
{ center: [FINAL_POLE, 0, 0], radius: 0.62, envelope: 0.9, spindle: false, elongated: 0 },
|
||||
],
|
||||
chromatids: [
|
||||
single("b1a", COLOR_B, 0, [0.1, 0.1, 0.06], 0),
|
||||
single("c1a", COLOR_C, 0, [-0.04, -0.04, -0.08], 20),
|
||||
single("o1a", COLOR_O, 0, [0.04, 0.04, -0.06], -15),
|
||||
single("y1a", COLOR_Y, 0, [-0.12, -0.12, 0.04], 10),
|
||||
single("b1b", COLOR_B, 1, [-0.1, -0.1, -0.06], 180),
|
||||
single("c1b", COLOR_C, 1, [0.04, 0.04, 0.08], 160),
|
||||
single("o1b", COLOR_O, 1, [-0.04, -0.04, 0.06], 195),
|
||||
single("y1b", COLOR_Y, 1, [0.12, 0.12, -0.04], 170),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ============================= MEIOSE ====================================
|
||||
// Meiose I (reducional): os HOMÓLOGOS se separam (não as cromátides-irmãs).
|
||||
// Pra ilustrar "sortimento independente" (cada par se orienta de forma
|
||||
// independente dos outros pares), mandamos b+y para um polo e c+o para o
|
||||
// outro — uma combinação nova, diferente da materna/paterna original.
|
||||
// Meiose II (equacional): igual a uma mitose, mas partindo de células
|
||||
// HAPLOIDES já duplicadas — sem nova duplicação do DNA antes.
|
||||
|
||||
export const MEIOSIS_STAGES: Stage[] = [
|
||||
{
|
||||
id: "interfase",
|
||||
label: "Interfase",
|
||||
description:
|
||||
"Igual à mitose: a célula duplica todo o seu DNA na fase S. Os cromossomos já duplicados continuam descondensados.",
|
||||
cells: [{ center: [0, 0, 0], radius: R, envelope: 1, spindle: false, elongated: 0 }],
|
||||
chromatids: [
|
||||
...pair("b1a", "b1b", COLOR_B, 0, [-0.28, 0.22, 0.1]),
|
||||
...pair("c1a", "c1b", COLOR_C, 0, [0.05, 0.3, -0.15]),
|
||||
...pair("o1a", "o1b", COLOR_O, 0, [0.25, -0.15, 0.2]),
|
||||
...pair("y1a", "y1b", COLOR_Y, 0, [-0.1, -0.28, -0.1]),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "prof1",
|
||||
label: "Prófase I",
|
||||
description:
|
||||
"Os cromossomos se condensam E os homólogos se emparelham lado a lado (sinapse), formando uma tétrade (4 cromátides). É aqui que ocorre o crossing-over — trocas de pedaços entre cromátides homólogas, que embaralham genes maternos e paternos e aumentam a variabilidade genética da espécie.",
|
||||
cells: [{ center: [0, 0, 0], radius: R, envelope: 0.6, spindle: true, elongated: 0 }],
|
||||
chromatids: [
|
||||
...pair("b1a", "b1b", COLOR_B, 0, [-0.22, 0.1, 0.05]),
|
||||
...pair("c1a", "c1b", COLOR_C, 0, [-0.09, 0.1, 0.05]),
|
||||
...pair("o1a", "o1b", COLOR_O, 0, [0.09, -0.1, -0.05]),
|
||||
...pair("y1a", "y1b", COLOR_Y, 0, [0.22, -0.1, -0.05]),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "metaf1",
|
||||
label: "Metáfase I",
|
||||
description:
|
||||
"As TÉTRADES (pares de homólogos, não cromossomos isolados como na mitose) se alinham juntas na placa metafásica — essa é a principal diferença visual em relação à mitose.",
|
||||
cells: [{ center: [0, 0, 0], radius: R, envelope: 0, spindle: true, elongated: 0 }],
|
||||
chromatids: [
|
||||
...pair("b1a", "b1b", COLOR_B, 0, [-0.27, 0, 0]),
|
||||
...pair("c1a", "c1b", COLOR_C, 0, [-0.13, 0, 0]),
|
||||
...pair("y1a", "y1b", COLOR_Y, 0, [0.13, 0, 0]),
|
||||
...pair("o1a", "o1b", COLOR_O, 0, [0.27, 0, 0]),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "anaf1",
|
||||
label: "Anáfase I",
|
||||
description:
|
||||
"Os HOMÓLOGOS se separam um do outro e vão para polos opostos — cada um ainda com suas 2 cromátides-irmãs unidas (diferente da mitose, onde são as cromátides-irmãs que se separam). É a divisão reducional: o número de cromossomos vai cair pela metade.",
|
||||
cells: [{ center: [0, 0, 0], radius: R, envelope: 0, spindle: true, elongated: 0.4 }],
|
||||
chromatids: [
|
||||
...pair("b1a", "b1b", COLOR_B, 0, [-POLE, 0.12, 0.05]),
|
||||
...pair("y1a", "y1b", COLOR_Y, 0, [-POLE, -0.12, -0.05]),
|
||||
...pair("c1a", "c1b", COLOR_C, 0, [POLE, 0.12, -0.05]),
|
||||
...pair("o1a", "o1b", COLOR_O, 0, [POLE, -0.12, 0.05]),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "cito1",
|
||||
label: "Telófase I + Citocinese I",
|
||||
description:
|
||||
"Formam-se DUAS células, cada uma já HAPLOIDE (só 1 cromossomo de cada par, não mais 2) — mas cada cromossomo ainda duplicado (2 cromátides). Repare que a combinação em cada célula é NOVA (nem só materna, nem só paterna): é o sortimento independente em ação.",
|
||||
cells: [
|
||||
{ center: [-FINAL_POLE, 0, 0], radius: 0.62, envelope: 0.7, spindle: false, elongated: 0 },
|
||||
{ center: [FINAL_POLE, 0, 0], radius: 0.62, envelope: 0.7, spindle: false, elongated: 0 },
|
||||
],
|
||||
chromatids: [
|
||||
...pair("b1a", "b1b", COLOR_B, 0, [-0.13, 0.1, 0.05]),
|
||||
...pair("y1a", "y1b", COLOR_Y, 0, [0.13, -0.1, -0.05]),
|
||||
...pair("c1a", "c1b", COLOR_C, 1, [-0.13, 0.1, -0.05]),
|
||||
...pair("o1a", "o1b", COLOR_O, 1, [0.13, -0.1, 0.05]),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "metaf2",
|
||||
label: "Metáfase II",
|
||||
description:
|
||||
"Sem nova duplicação de DNA, cada uma das 2 células (agora haploides) monta seu próprio fuso e alinha seus cromossomos (ainda duplicados) na placa metafásica — igual a uma mitose comum, só que partindo de metade dos cromossomos.",
|
||||
cells: [
|
||||
{ center: [-FINAL_POLE, 0, 0], radius: 0.62, envelope: 0, spindle: true, elongated: 0 },
|
||||
{ center: [FINAL_POLE, 0, 0], radius: 0.62, envelope: 0, spindle: true, elongated: 0 },
|
||||
],
|
||||
chromatids: [
|
||||
...pair("b1a", "b1b", COLOR_B, 0, [-0.16, 0, 0]),
|
||||
...pair("y1a", "y1b", COLOR_Y, 0, [0.16, 0, 0]),
|
||||
...pair("c1a", "c1b", COLOR_C, 1, [-0.16, 0, 0]),
|
||||
...pair("o1a", "o1b", COLOR_O, 1, [0.16, 0, 0]),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "anaf2",
|
||||
label: "Anáfase II",
|
||||
description:
|
||||
"Agora sim as cromátides-irmãs se separam (igual à anáfase da mitose) dentro de cada uma das 2 células, indo para polos opostos.",
|
||||
cells: [
|
||||
{ center: [-FINAL_POLE, 0, 0], radius: 0.62, envelope: 0, spindle: true, elongated: 0.4 },
|
||||
{ center: [FINAL_POLE, 0, 0], radius: 0.62, envelope: 0, spindle: true, elongated: 0.4 },
|
||||
],
|
||||
chromatids: [
|
||||
single("b1a", COLOR_B, 0, [-0.4, 0.1, 0], 0),
|
||||
single("y1a", COLOR_Y, 0, [-0.4, -0.1, 0], 10),
|
||||
single("b1b", COLOR_B, 0, [0.4, -0.1, 0], 180),
|
||||
single("y1b", COLOR_Y, 0, [0.4, 0.1, 0], 170),
|
||||
single("c1a", COLOR_C, 1, [-0.4, 0.1, 0], 20),
|
||||
single("o1a", COLOR_O, 1, [-0.4, -0.1, 0], -15),
|
||||
single("c1b", COLOR_C, 1, [0.4, -0.1, 0], 160),
|
||||
single("o1b", COLOR_O, 1, [0.4, 0.1, 0], 195),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "cito2",
|
||||
label: "Telófase II + Citocinese II",
|
||||
description:
|
||||
"Cada uma das 2 células se divide em 2 — resultando em 4 células HAPLOIDES no total, cada uma com metade dos cromossomos da célula original e uma combinação genética própria. Em humanos, essas são as células germinativas (óvulos ou espermatozoides).",
|
||||
cells: [
|
||||
{ center: [-1.8, 0, 0], radius: 0.5, envelope: 0.9, spindle: false, elongated: 0 },
|
||||
{ center: [-0.6, 0, 0], radius: 0.5, envelope: 0.9, spindle: false, elongated: 0 },
|
||||
{ center: [0.6, 0, 0], radius: 0.5, envelope: 0.9, spindle: false, elongated: 0 },
|
||||
{ center: [1.8, 0, 0], radius: 0.5, envelope: 0.9, spindle: false, elongated: 0 },
|
||||
],
|
||||
chromatids: [
|
||||
single("b1a", COLOR_B, 0, [0, 0.08, 0], 0),
|
||||
single("y1a", COLOR_Y, 0, [0, -0.08, 0], 10),
|
||||
single("b1b", COLOR_B, 1, [0, 0.08, 0], 180),
|
||||
single("y1b", COLOR_Y, 1, [0, -0.08, 0], 170),
|
||||
single("c1a", COLOR_C, 2, [0, 0.08, 0], 20),
|
||||
single("o1a", COLOR_O, 2, [0, -0.08, 0], -15),
|
||||
single("c1b", COLOR_C, 3, [0, 0.08, 0], 160),
|
||||
single("o1b", COLOR_O, 3, [0, -0.08, 0], 195),
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,241 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { CameraControls } from "@react-three/drei";
|
||||
import { CellDivisionScene } from "../components/cellDivision/CellDivisionScene.js";
|
||||
import { MITOSIS_STAGES, MEIOSIS_STAGES } from "../data/cellDivisionStages.js";
|
||||
|
||||
type Mode = "mitose" | "meiose";
|
||||
|
||||
const AUTOPLAY_MS = 3200;
|
||||
|
||||
export default function CellDivisionPage() {
|
||||
const [mode, setMode] = useState<Mode>("mitose");
|
||||
const [stageIndex, setStageIndex] = useState(0);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [leftOpen, setLeftOpen] = useState(false);
|
||||
const [rightOpen, setRightOpen] = useState(false);
|
||||
|
||||
const stages = mode === "mitose" ? MITOSIS_STAGES : MEIOSIS_STAGES;
|
||||
const stage = stages[stageIndex];
|
||||
|
||||
// Trocar de modo reinicia o estágio — misturar índice de mitose (7
|
||||
// estágios) com o array de meiose (9 estágios) fora de ordem não faz
|
||||
// sentido pedagogicamente.
|
||||
function changeMode(next: Mode) {
|
||||
setMode(next);
|
||||
setStageIndex(0);
|
||||
setPlaying(false);
|
||||
}
|
||||
|
||||
function goTo(index: number) {
|
||||
setStageIndex(Math.max(0, Math.min(stages.length - 1, index)));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!playing) return;
|
||||
const id = setInterval(() => {
|
||||
setStageIndex((i) => {
|
||||
if (i >= stages.length - 1) {
|
||||
setPlaying(false);
|
||||
return i;
|
||||
}
|
||||
return i + 1;
|
||||
});
|
||||
}, AUTOPLAY_MS);
|
||||
return () => clearInterval(id);
|
||||
}, [playing, stages.length]);
|
||||
|
||||
// Reinicia autoplay do começo se já estava no fim.
|
||||
function togglePlay() {
|
||||
setPlaying((p) => {
|
||||
if (!p && stageIndex >= stages.length - 1) setStageIndex(0);
|
||||
return !p;
|
||||
});
|
||||
}
|
||||
|
||||
const cellCount = stage.cells.length;
|
||||
|
||||
return (
|
||||
<div className="h-full relative md:grid md:grid-cols-[260px_1fr_320px]">
|
||||
<aside
|
||||
className={`fixed md:static inset-y-0 left-0 z-30 w-[86vw] max-w-[280px] md:w-auto
|
||||
border-r border-white/10 bg-panel p-4 space-y-4 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>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xs uppercase tracking-wide text-slate-400 mb-2">Processo</h3>
|
||||
<div className="flex rounded-lg overflow-hidden border border-white/10">
|
||||
<button
|
||||
onClick={() => changeMode("mitose")}
|
||||
className={`flex-1 text-sm py-2 ${mode === "mitose" ? "bg-accent text-white" : "bg-white/5 text-slate-300 hover:bg-white/10"}`}
|
||||
>
|
||||
Mitose
|
||||
</button>
|
||||
<button
|
||||
onClick={() => changeMode("meiose")}
|
||||
className={`flex-1 text-sm py-2 ${mode === "meiose" ? "bg-accent text-white" : "bg-white/5 text-slate-300 hover:bg-white/10"}`}
|
||||
>
|
||||
Meiose
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-500 mt-2 leading-relaxed">
|
||||
{mode === "mitose"
|
||||
? "1 célula (2n=4, simplificado) → 2 células-filhas idênticas."
|
||||
: "1 célula (2n=4) → 4 células-filhas haploides, geneticamente diferentes entre si."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xs uppercase tracking-wide text-slate-400 mb-2">Fases</h3>
|
||||
<ol className="space-y-1">
|
||||
{stages.map((s, i) => (
|
||||
<li key={s.id}>
|
||||
<button
|
||||
onClick={() => {
|
||||
goTo(i);
|
||||
setPlaying(false);
|
||||
}}
|
||||
className={`w-full text-left text-sm px-2.5 py-1.5 rounded flex items-center gap-2 ${
|
||||
i === stageIndex ? "bg-accent text-white" : "text-slate-300 hover:bg-white/5"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-flex items-center justify-center w-5 h-5 rounded-full text-[10px] shrink-0 ${
|
||||
i === stageIndex ? "bg-white/25" : "bg-white/10"
|
||||
}`}
|
||||
>
|
||||
{i + 1}
|
||||
</span>
|
||||
{s.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
☰ Fases
|
||||
</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 dpr={[1, 1.5]} camera={{ position: [0, 1.4, 5.4], fov: 42 }}>
|
||||
<CellDivisionScene stage={stage} />
|
||||
<CameraControls makeDefault minDistance={2} maxDistance={12} />
|
||||
</Canvas>
|
||||
|
||||
{/* Controles de reprodução — barra fixa embaixo do canvas */}
|
||||
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 z-10 flex items-center gap-2 bg-panel/90 border border-white/10 rounded-full px-3 py-2 shadow-lg">
|
||||
<button
|
||||
onClick={() => {
|
||||
goTo(stageIndex - 1);
|
||||
setPlaying(false);
|
||||
}}
|
||||
disabled={stageIndex === 0}
|
||||
className="w-8 h-8 flex items-center justify-center rounded-full text-slate-300 hover:bg-white/10 disabled:opacity-30 disabled:hover:bg-transparent"
|
||||
aria-label="Fase anterior"
|
||||
>
|
||||
⏮
|
||||
</button>
|
||||
<button
|
||||
onClick={togglePlay}
|
||||
className="w-9 h-9 flex items-center justify-center rounded-full bg-accent text-white hover:brightness-110"
|
||||
aria-label={playing ? "Pausar" : "Reproduzir"}
|
||||
>
|
||||
{playing ? "⏸" : "▶"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
goTo(stageIndex + 1);
|
||||
setPlaying(false);
|
||||
}}
|
||||
disabled={stageIndex === stages.length - 1}
|
||||
className="w-8 h-8 flex items-center justify-center rounded-full text-slate-300 hover:bg-white/10 disabled:opacity-30 disabled:hover:bg-transparent"
|
||||
aria-label="Próxima fase"
|
||||
>
|
||||
⏭
|
||||
</button>
|
||||
<span className="text-xs text-slate-400 px-1 tabular-nums">
|
||||
{stageIndex + 1}/{stages.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-3 right-3 z-10 hidden md:block bg-panel/90 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-slate-300 shadow-lg">
|
||||
{cellCount} {cellCount === 1 ? "célula" : "células"}
|
||||
</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">
|
||||
{mode === "mitose" ? "Mitose" : "Meiose"}
|
||||
</span>
|
||||
<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-1.5">
|
||||
<p className="text-[11px] uppercase tracking-wide text-slate-500">Legenda das cores</p>
|
||||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||||
<span className="w-2.5 h-2.5 rounded-full inline-block" style={{ background: "#4f7cff" }} />
|
||||
Cromossomo 1 (materno) <span className="text-slate-600">— homólogo do ciano</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||||
<span className="w-2.5 h-2.5 rounded-full inline-block" style={{ background: "#22d3ee" }} />
|
||||
Cromossomo 1 (paterno)
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||||
<span className="w-2.5 h-2.5 rounded-full inline-block" style={{ background: "#f59e0b" }} />
|
||||
Cromossomo 2 (materno) <span className="text-slate-600">— homólogo do amarelo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||||
<span className="w-2.5 h-2.5 rounded-full inline-block" style={{ background: "#eab308" }} />
|
||||
Cromossomo 2 (paterno)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-slate-500 pt-2 border-t border-white/10 leading-relaxed">
|
||||
Modelo ilustrativo e simplificado (2n=4 em vez dos 46 cromossomos humanos reais) — pensado pra mostrar os conceitos-chave de cada fase, não uma simulação biofísica exata. Arraste pra girar, role pra dar zoom.
|
||||
</p>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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/viewer/exammarkers.tsx","./src/components/viewer/mannequin.tsx","./src/components/viewer/scene.tsx","./src/components/viewer/systemmodel.tsx","./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/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/celldivisionstages.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"}
|
||||
Reference in new issue
Block a user