fix camera
This commit is contained in:
1 parent
204e9f8a39
commit
76ac1df116
3 files changed
+77
-1
No files matched your search
@@ -30,6 +30,7 @@ function loadStoredPos(): { x: number; y: number } | null {
|
||||
// queira ver — a posição escolhida fica salva entre visitas.
|
||||
export function QuickRegionNav() {
|
||||
const focusOnRegion = useAtlasStore((s) => s.focusOnRegion);
|
||||
const focusOnViewAngle = useAtlasStore((s) => s.focusOnViewAngle);
|
||||
const [pos, setPos] = useState<{ x: number; y: number } | null>(() => loadStoredPos());
|
||||
const elRef = useRef<HTMLDivElement>(null);
|
||||
const dragRef = useRef<{ startX: number; startY: number; origX: number; origY: number } | null>(null);
|
||||
@@ -127,6 +128,23 @@ export function QuickRegionNav() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="shrink-0 h-6 w-px bg-white/10" aria-hidden="true" />
|
||||
<div className="flex gap-1.5 shrink-0">
|
||||
<button
|
||||
onClick={() => focusOnViewAngle("anterior")}
|
||||
title="Vista anterior (de frente)"
|
||||
className="shrink-0 text-xs px-3 py-1.5 rounded-full bg-white/5 hover:bg-accent hover:text-white text-slate-300 transition-colors"
|
||||
>
|
||||
Anterior
|
||||
</button>
|
||||
<button
|
||||
onClick={() => focusOnViewAngle("posterior")}
|
||||
title="Vista posterior (de costas)"
|
||||
className="shrink-0 text-xs px-3 py-1.5 rounded-full bg-white/5 hover:bg-accent hover:text-white text-slate-300 transition-colors"
|
||||
>
|
||||
Posterior
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -59,11 +59,20 @@ function useClippingPlanes(): THREE.Plane[] {
|
||||
}
|
||||
|
||||
|
||||
// Ângulo polar "de pé, olhando na horizontal" (não de cima nem de baixo) —
|
||||
// usado tanto pro enquadramento inicial quanto pelos botões Anterior/
|
||||
// Posterior, pra sempre ancorar numa vista de frente/costas limpa em vez de
|
||||
// um ângulo 3/4 arbitrário.
|
||||
const EYE_LEVEL_POLAR = Math.PI / 2;
|
||||
const ANTERIOR_AZIMUTH = 0;
|
||||
const POSTERIOR_AZIMUTH = Math.PI;
|
||||
|
||||
function CameraController({ modelFiles }: { modelFiles: string[] }) {
|
||||
const controlsRef = useRef<CameraControls>(null);
|
||||
const { scene, camera } = useThree();
|
||||
const focusRequest = useAtlasStore((s) => s.focusRequest);
|
||||
const regionFocusRequest = useAtlasStore((s) => s.regionFocusRequest);
|
||||
const viewAngleRequest = useAtlasStore((s) => s.viewAngleRequest);
|
||||
const setContentBox = useAtlasStore((s) => s.setContentBox);
|
||||
|
||||
// focusRequest/regionFocusRequest moram no store GLOBAL (compartilhado
|
||||
@@ -75,6 +84,13 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) {
|
||||
// da câmera já existe.
|
||||
const seenFocusNonce = useRef(focusRequest?.nonce);
|
||||
const seenRegionNonce = useRef(regionFocusRequest?.nonce);
|
||||
const seenViewAngleNonce = useRef(viewAngleRequest?.nonce);
|
||||
|
||||
// Último box enquadrado (corpo inteiro, região ou estrutura) — os botões
|
||||
// Anterior/Posterior giram a câmera em torno DESSE enquadramento, não
|
||||
// sempre do corpo inteiro, senão trocar de lado enquanto o usuário está
|
||||
// olhando uma região específica (ex: "Pelve") chutaria o zoom pra fora.
|
||||
const lastFitBoxRef = useRef<THREE.Box3 | null>(null);
|
||||
|
||||
// Malhas reais (Z-Anatomy) não seguem a mesma escala/origem que os
|
||||
// modelos esquemáticos antigos (podem vir em cm, ou centralizadas em
|
||||
@@ -122,7 +138,17 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) {
|
||||
camera.near = Math.max(size / 1000, 0.001);
|
||||
camera.far = size * 100;
|
||||
if (camera instanceof THREE.PerspectiveCamera) camera.updateProjectionMatrix();
|
||||
controlsRef.current.fitToBox(box, true, {
|
||||
lastFitBoxRef.current = box.clone();
|
||||
// fitToBox arredonda o ângulo ATUAL da câmera pro eixo mais próximo
|
||||
// (frente/trás/lado/cima/baixo) em vez de manter um 3/4 arbitrário —
|
||||
// na carga inicial isso dependia de quão perto a posição default
|
||||
// [1.2, 1.4, 1.6] ficava de um múltiplo de 90°, o que podia (ex: após
|
||||
// qualquer ajuste futuro nesse valor) arredondar pra uma vista de
|
||||
// cima/baixo e enquadrar só os pés ou só a cabeça. Fixamos o ângulo
|
||||
// explicitamente numa vista frontal, na altura dos olhos, ANTES do
|
||||
// primeiro fitToBox, pra carga inicial ser sempre determinística.
|
||||
controlsRef.current.rotateTo(ANTERIOR_AZIMUTH, EYE_LEVEL_POLAR, false);
|
||||
controlsRef.current.fitToBox(box, false, {
|
||||
paddingLeft: 0.2,
|
||||
paddingRight: 0.2,
|
||||
paddingTop: 0.2,
|
||||
@@ -146,6 +172,7 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) {
|
||||
if (!obj) return;
|
||||
const box = computeVisibleBoundingBox(obj);
|
||||
if (box.isEmpty()) return;
|
||||
lastFitBoxRef.current = box.clone();
|
||||
controlsRef.current.fitToBox(box, true, {
|
||||
paddingLeft: 0.4,
|
||||
paddingRight: 0.4,
|
||||
@@ -162,6 +189,7 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) {
|
||||
if (total.isEmpty()) return;
|
||||
const box = regionSubBox(total, regionFocusRequest.region);
|
||||
if (box.isEmpty()) return;
|
||||
lastFitBoxRef.current = box.clone();
|
||||
controlsRef.current.fitToBox(box, true, {
|
||||
paddingLeft: 0.3,
|
||||
paddingRight: 0.3,
|
||||
@@ -170,6 +198,25 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) {
|
||||
});
|
||||
}, [regionFocusRequest, scene]);
|
||||
|
||||
// Vista Anterior/Posterior — gira a câmera 180° em torno do último
|
||||
// enquadramento (corpo inteiro, região ou estrutura), sem perder o zoom
|
||||
// atual, pra comparar a face anterior e posterior de uma mesma parte.
|
||||
useEffect(() => {
|
||||
if (!viewAngleRequest || !controlsRef.current) return;
|
||||
if (viewAngleRequest.nonce === seenViewAngleNonce.current) return;
|
||||
seenViewAngleNonce.current = viewAngleRequest.nonce;
|
||||
const box = lastFitBoxRef.current ?? computeVisibleBoundingBox(scene);
|
||||
if (box.isEmpty()) return;
|
||||
const azimuth = viewAngleRequest.angle === "anterior" ? ANTERIOR_AZIMUTH : POSTERIOR_AZIMUTH;
|
||||
controlsRef.current.rotateTo(azimuth, EYE_LEVEL_POLAR, true);
|
||||
controlsRef.current.fitToBox(box, true, {
|
||||
paddingLeft: 0.25,
|
||||
paddingRight: 0.25,
|
||||
paddingTop: 0.25,
|
||||
paddingBottom: 0.25,
|
||||
});
|
||||
}, [viewAngleRequest, scene]);
|
||||
|
||||
return (
|
||||
<CameraControls
|
||||
ref={controlsRef}
|
||||
|
||||
@@ -26,6 +26,11 @@ interface AtlasState {
|
||||
regionFocusRequest: { region: BodyRegion; nonce: number } | null;
|
||||
focusOnRegion: (region: BodyRegion) => void;
|
||||
|
||||
// Gira a câmera pra ver o corpo de frente (anterior) ou de costas
|
||||
// (posterior), mantendo o enquadramento atual (mesma região focada).
|
||||
viewAngleRequest: { angle: "anterior" | "posterior"; nonce: number } | null;
|
||||
focusOnViewAngle: (angle: "anterior" | "posterior") => void;
|
||||
|
||||
// tiltX/tiltZ (graus) inclinam o plano de corte a partir do eixo base —
|
||||
// é assim que se obtém um corte oblíquo em vez de só sagital/coronal/
|
||||
// transversal puros.
|
||||
@@ -66,6 +71,12 @@ export const useAtlasStore = create<AtlasState>((set) => ({
|
||||
regionFocusRequest: { region, nonce: (state.regionFocusRequest?.nonce ?? 0) + 1 },
|
||||
})),
|
||||
|
||||
viewAngleRequest: null,
|
||||
focusOnViewAngle: (angle) =>
|
||||
set((state) => ({
|
||||
viewAngleRequest: { angle, nonce: (state.viewAngleRequest?.nonce ?? 0) + 1 },
|
||||
})),
|
||||
|
||||
clipping: { enabled: false, axis: "sagittal", position: 0, tiltX: 0, tiltZ: 0 },
|
||||
setClippingEnabled: (enabled) => set((state) => ({ clipping: { ...state.clipping, enabled } })),
|
||||
setClipAxis: (axis) => set((state) => ({ clipping: { ...state.clipping, axis } })),
|
||||
|
||||
Reference in new issue
Block a user