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.
|
// queira ver — a posição escolhida fica salva entre visitas.
|
||||||
export function QuickRegionNav() {
|
export function QuickRegionNav() {
|
||||||
const focusOnRegion = useAtlasStore((s) => s.focusOnRegion);
|
const focusOnRegion = useAtlasStore((s) => s.focusOnRegion);
|
||||||
|
const focusOnViewAngle = useAtlasStore((s) => s.focusOnViewAngle);
|
||||||
const [pos, setPos] = useState<{ x: number; y: number } | null>(() => loadStoredPos());
|
const [pos, setPos] = useState<{ x: number; y: number } | null>(() => loadStoredPos());
|
||||||
const elRef = useRef<HTMLDivElement>(null);
|
const elRef = useRef<HTMLDivElement>(null);
|
||||||
const dragRef = useRef<{ startX: number; startY: number; origX: number; origY: number } | null>(null);
|
const dragRef = useRef<{ startX: number; startY: number; origX: number; origY: number } | null>(null);
|
||||||
@@ -127,6 +128,23 @@ export function QuickRegionNav() {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
</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[] }) {
|
function CameraController({ modelFiles }: { modelFiles: string[] }) {
|
||||||
const controlsRef = useRef<CameraControls>(null);
|
const controlsRef = useRef<CameraControls>(null);
|
||||||
const { scene, camera } = useThree();
|
const { scene, camera } = useThree();
|
||||||
const focusRequest = useAtlasStore((s) => s.focusRequest);
|
const focusRequest = useAtlasStore((s) => s.focusRequest);
|
||||||
const regionFocusRequest = useAtlasStore((s) => s.regionFocusRequest);
|
const regionFocusRequest = useAtlasStore((s) => s.regionFocusRequest);
|
||||||
|
const viewAngleRequest = useAtlasStore((s) => s.viewAngleRequest);
|
||||||
const setContentBox = useAtlasStore((s) => s.setContentBox);
|
const setContentBox = useAtlasStore((s) => s.setContentBox);
|
||||||
|
|
||||||
// focusRequest/regionFocusRequest moram no store GLOBAL (compartilhado
|
// focusRequest/regionFocusRequest moram no store GLOBAL (compartilhado
|
||||||
@@ -75,6 +84,13 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) {
|
|||||||
// da câmera já existe.
|
// da câmera já existe.
|
||||||
const seenFocusNonce = useRef(focusRequest?.nonce);
|
const seenFocusNonce = useRef(focusRequest?.nonce);
|
||||||
const seenRegionNonce = useRef(regionFocusRequest?.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
|
// Malhas reais (Z-Anatomy) não seguem a mesma escala/origem que os
|
||||||
// modelos esquemáticos antigos (podem vir em cm, ou centralizadas em
|
// 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.near = Math.max(size / 1000, 0.001);
|
||||||
camera.far = size * 100;
|
camera.far = size * 100;
|
||||||
if (camera instanceof THREE.PerspectiveCamera) camera.updateProjectionMatrix();
|
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,
|
paddingLeft: 0.2,
|
||||||
paddingRight: 0.2,
|
paddingRight: 0.2,
|
||||||
paddingTop: 0.2,
|
paddingTop: 0.2,
|
||||||
@@ -146,6 +172,7 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) {
|
|||||||
if (!obj) return;
|
if (!obj) return;
|
||||||
const box = computeVisibleBoundingBox(obj);
|
const box = computeVisibleBoundingBox(obj);
|
||||||
if (box.isEmpty()) return;
|
if (box.isEmpty()) return;
|
||||||
|
lastFitBoxRef.current = box.clone();
|
||||||
controlsRef.current.fitToBox(box, true, {
|
controlsRef.current.fitToBox(box, true, {
|
||||||
paddingLeft: 0.4,
|
paddingLeft: 0.4,
|
||||||
paddingRight: 0.4,
|
paddingRight: 0.4,
|
||||||
@@ -162,6 +189,7 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) {
|
|||||||
if (total.isEmpty()) return;
|
if (total.isEmpty()) return;
|
||||||
const box = regionSubBox(total, regionFocusRequest.region);
|
const box = regionSubBox(total, regionFocusRequest.region);
|
||||||
if (box.isEmpty()) return;
|
if (box.isEmpty()) return;
|
||||||
|
lastFitBoxRef.current = box.clone();
|
||||||
controlsRef.current.fitToBox(box, true, {
|
controlsRef.current.fitToBox(box, true, {
|
||||||
paddingLeft: 0.3,
|
paddingLeft: 0.3,
|
||||||
paddingRight: 0.3,
|
paddingRight: 0.3,
|
||||||
@@ -170,6 +198,25 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) {
|
|||||||
});
|
});
|
||||||
}, [regionFocusRequest, scene]);
|
}, [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 (
|
return (
|
||||||
<CameraControls
|
<CameraControls
|
||||||
ref={controlsRef}
|
ref={controlsRef}
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ interface AtlasState {
|
|||||||
regionFocusRequest: { region: BodyRegion; nonce: number } | null;
|
regionFocusRequest: { region: BodyRegion; nonce: number } | null;
|
||||||
focusOnRegion: (region: BodyRegion) => void;
|
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 —
|
// 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/
|
// é assim que se obtém um corte oblíquo em vez de só sagital/coronal/
|
||||||
// transversal puros.
|
// transversal puros.
|
||||||
@@ -66,6 +71,12 @@ export const useAtlasStore = create<AtlasState>((set) => ({
|
|||||||
regionFocusRequest: { region, nonce: (state.regionFocusRequest?.nonce ?? 0) + 1 },
|
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 },
|
clipping: { enabled: false, axis: "sagittal", position: 0, tiltX: 0, tiltZ: 0 },
|
||||||
setClippingEnabled: (enabled) => set((state) => ({ clipping: { ...state.clipping, enabled } })),
|
setClippingEnabled: (enabled) => set((state) => ({ clipping: { ...state.clipping, enabled } })),
|
||||||
setClipAxis: (axis) => set((state) => ({ clipping: { ...state.clipping, axis } })),
|
setClipAxis: (axis) => set((state) => ({ clipping: { ...state.clipping, axis } })),
|
||||||
|
|||||||
Reference in new issue
Block a user