fibras do fuso

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:49:53 -03:00
1 parent 3195bdd724
commit 2dc5a167ed
1 file changed
+53 -25
@@ -23,6 +23,38 @@ function smoothing(delta: number, halfLifeSeconds = 0.18) {
return 1 - Math.pow(0.5, delta / halfLifeSeconds); return 1 - Math.pow(0.5, delta / halfLifeSeconds);
} }
// Geometria das fibras do fuso: cada cilindro vai de A até B de verdade
// (não só "perto" do centrossomo com um ângulo aproximado) — calculamos
// a posição (ponto médio), o comprimento e a rotação (quaternion) direto
// dos dois pontos, então a fibra sempre nasce exatamente na esfera do
// centrossomo e se afunila em leque até perto do centro da célula, como
// o fuso mitótico de verdade.
const CENTROSOME_X = 0.65;
const FIBER_FAN = [-0.26, -0.12, 0.12, 0.26];
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), // eixo padrão do CylinderGeometry
direction.clone().normalize(),
);
return { position, quaternion, length };
}
// Um leque de fibras por polo, todas nascendo EXATAMENTE no centrossomo
// (0, ±CENTROSOME_X ao longo de X) e convergindo pra perto do centro da
// célula — computado uma única vez (é geometria fixa, não depende de
// estado/props) em vez de recalcular a cada render.
const FIBER_DEFS = ([-1, 1] as const).flatMap((side) => {
const from = new THREE.Vector3(side * CENTROSOME_X, 0, 0);
return FIBER_FAN.map((offset) => {
const to = new THREE.Vector3(side * 0.12, offset * 0.55, offset * 0.55);
return { side, ...cylinderBetween(from, to) };
});
});
interface SelectionProps { interface SelectionProps {
selected: CellComponentId | null; selected: CellComponentId | null;
onSelect: (id: CellComponentId) => void; onSelect: (id: CellComponentId) => void;
@@ -220,31 +252,27 @@ function AnimatedCellSlot({
</group> </group>
{/* fibras do fuso — só a partir da prófase (quando `spindle: true`) */} {/* fibras do fuso — só a partir da prófase (quando `spindle: true`) */}
<group ref={fibersRef}> <group ref={fibersRef}>
{[-1, 1].map((side) => ( {FIBER_DEFS.map((fiber, i) => (
<group key={side}> <mesh
{[-0.25, -0.1, 0.1, 0.25].map((offset, i) => ( key={i}
<mesh position={fiber.position}
key={i} quaternion={fiber.quaternion}
position={[side * 0.65 + side * -0.32, offset * 0.6, offset]} onClick={(e) => {
rotation={[0, 0, side > 0 ? Math.PI / 2 + offset * 0.3 : -Math.PI / 2 + offset * 0.3]} e.stopPropagation();
onClick={(e) => { onSelect("fibras");
e.stopPropagation(); }}
onSelect("fibras"); >
}} {/* raio um pouco maior só pra facilitar o clique — a fibra
> visual continua fina, isso é puramente a área alvo */}
{/* raio um pouco maior só pra facilitar o clique — a fibra <cylinderGeometry args={[0.018, 0.018, fiber.length, 6]} />
visual continua fina, isso é puramente a área alvo */} <meshStandardMaterial
<cylinderGeometry args={[0.02, 0.02, 0.64, 6]} /> color="#f8fafc"
<meshStandardMaterial transparent
color="#f8fafc" opacity={fiberSelected ? 0.9 : 0.4}
transparent emissive={fiberSelected ? HIGHLIGHT : BLACK}
opacity={fiberSelected ? 0.9 : 0.4} emissiveIntensity={fiberSelected ? 0.6 : 0}
emissive={fiberSelected ? HIGHLIGHT : BLACK} />
emissiveIntensity={fiberSelected ? 0.6 : 0} </mesh>
/>
</mesh>
))}
</group>
))} ))}
</group> </group>
</group> </group>