mudanca de camera
This commit is contained in:
1 parent
b7facab432
commit
c9fc1ad6c7
4 files changed
+95
-1
No files matched your search
@@ -0,0 +1,33 @@
|
||||
import { useAtlasStore, type BodyRegion } from "../store/useAtlasStore.js";
|
||||
|
||||
const REGIONS: { region: BodyRegion; label: string }[] = [
|
||||
{ region: "cabeca", label: "Cabeça" },
|
||||
{ region: "pescoco", label: "Pescoço" },
|
||||
{ region: "torax", label: "Tórax" },
|
||||
{ region: "abdome", label: "Abdome" },
|
||||
{ region: "pelve", label: "Pelve" },
|
||||
{ region: "membros_superiores", label: "Membros Sup." },
|
||||
{ region: "membros_inferiores", label: "Membros Inf." },
|
||||
{ region: "corpo_inteiro", label: "Corpo Inteiro" },
|
||||
];
|
||||
|
||||
// Navegação rápida por região — em vez de precisar acertar o clique numa
|
||||
// estrutura minúscula pra girar/aproximar a câmera manualmente, o usuário
|
||||
// pede direto pra "ir" numa região do corpo.
|
||||
export function QuickRegionNav() {
|
||||
const focusOnRegion = useAtlasStore((s) => s.focusOnRegion);
|
||||
|
||||
return (
|
||||
<div className="absolute top-3 left-1/2 -translate-x-1/2 flex flex-wrap gap-1.5 justify-center bg-panel/90 border border-white/10 rounded-lg px-3 py-2 shadow-lg max-w-[90%]">
|
||||
{REGIONS.map((r) => (
|
||||
<button
|
||||
key={r.region}
|
||||
onClick={() => focusOnRegion(r.region)}
|
||||
className="text-xs px-2.5 py-1 rounded-full bg-white/5 hover:bg-accent hover:text-white text-slate-300 transition-colors"
|
||||
>
|
||||
{r.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Suspense, useEffect, useMemo, useRef } from "react";
|
||||
import { Canvas, useThree } from "@react-three/fiber";
|
||||
import { CameraControls, Grid } from "@react-three/drei";
|
||||
import * as THREE from "three";
|
||||
import { useAtlasStore, type ClipAxis } from "../../store/useAtlasStore.js";
|
||||
import { useAtlasStore, type BodyRegion, type ClipAxis } from "../../store/useAtlasStore.js";
|
||||
import { SystemModel } from "./SystemModel.js";
|
||||
import type { Structure } from "../../lib/api.js";
|
||||
|
||||
@@ -47,10 +47,36 @@ function computeVisibleBoundingBox(root: THREE.Object3D): THREE.Box3 {
|
||||
return union;
|
||||
}
|
||||
|
||||
// Faixas aproximadas de altura (0 = pés, 1 = topo da cabeça) usadas pela
|
||||
// navegação rápida por região — proporções corporais padrão de um adulto
|
||||
// em pé. Regiões dos membros usam a largura toda (os braços ficam nas
|
||||
// bordas laterais dessa faixa de altura).
|
||||
const BODY_REGION_Y_RANGES: Record<Exclude<BodyRegion, "corpo_inteiro">, [number, number]> = {
|
||||
cabeca: [0.85, 1.0],
|
||||
pescoco: [0.8, 0.87],
|
||||
torax: [0.62, 0.83],
|
||||
abdome: [0.52, 0.65],
|
||||
pelve: [0.42, 0.55],
|
||||
membros_superiores: [0.55, 0.88],
|
||||
membros_inferiores: [0.0, 0.45],
|
||||
};
|
||||
|
||||
function regionSubBox(total: THREE.Box3, region: BodyRegion): THREE.Box3 {
|
||||
if (region === "corpo_inteiro") return total;
|
||||
const [t0, t1] = BODY_REGION_Y_RANGES[region];
|
||||
const yMin = THREE.MathUtils.lerp(total.min.y, total.max.y, t0);
|
||||
const yMax = THREE.MathUtils.lerp(total.min.y, total.max.y, t1);
|
||||
return new THREE.Box3(
|
||||
new THREE.Vector3(total.min.x, yMin, total.min.z),
|
||||
new THREE.Vector3(total.max.x, yMax, total.max.z),
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Malhas reais (Z-Anatomy) não seguem a mesma escala/origem que os
|
||||
// modelos esquemáticos antigos (podem vir em cm, ou centralizadas em
|
||||
@@ -93,6 +119,20 @@ function CameraController({ modelFiles }: { modelFiles: string[] }) {
|
||||
});
|
||||
}, [focusRequest, scene]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!regionFocusRequest || !controlsRef.current) return;
|
||||
const total = computeVisibleBoundingBox(scene);
|
||||
if (total.isEmpty()) return;
|
||||
const box = regionSubBox(total, regionFocusRequest.region);
|
||||
if (box.isEmpty()) return;
|
||||
controlsRef.current.fitToBox(box, true, {
|
||||
paddingLeft: 0.3,
|
||||
paddingRight: 0.3,
|
||||
paddingTop: 0.3,
|
||||
paddingBottom: 0.3,
|
||||
});
|
||||
}, [regionFocusRequest, scene]);
|
||||
|
||||
return (
|
||||
<CameraControls
|
||||
ref={controlsRef}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { LayerToggle } from "../components/LayerToggle.js";
|
||||
import { ClippingControls } from "../components/ClippingControls.js";
|
||||
import { SearchNav } from "../components/SearchNav.js";
|
||||
import { StructureInfoPanel } from "../components/StructureInfoPanel.js";
|
||||
import { QuickRegionNav } from "../components/QuickRegionNav.js";
|
||||
|
||||
export default function AtlasPage() {
|
||||
const visibleSystems = useAtlasStore((s) => s.visibleSystems);
|
||||
@@ -22,6 +23,7 @@ export default function AtlasPage() {
|
||||
|
||||
<div className="relative">
|
||||
<Scene bySlug={bySlug} />
|
||||
<QuickRegionNav />
|
||||
</div>
|
||||
|
||||
<aside className="border-l border-white/10 bg-panel p-4 overflow-y-auto">
|
||||
|
||||
@@ -2,6 +2,16 @@ import { create } from "zustand";
|
||||
|
||||
export type ClipAxis = "sagittal" | "coronal" | "transverse";
|
||||
|
||||
export type BodyRegion =
|
||||
| "cabeca"
|
||||
| "pescoco"
|
||||
| "torax"
|
||||
| "abdome"
|
||||
| "pelve"
|
||||
| "membros_superiores"
|
||||
| "membros_inferiores"
|
||||
| "corpo_inteiro";
|
||||
|
||||
interface AtlasState {
|
||||
visibleSystems: Set<string>;
|
||||
toggleSystem: (slug: string) => void;
|
||||
@@ -13,6 +23,9 @@ interface AtlasState {
|
||||
focusRequest: { slug: string; nonce: number } | null;
|
||||
focusOnStructure: (slug: string) => void;
|
||||
|
||||
regionFocusRequest: { region: BodyRegion; nonce: number } | null;
|
||||
focusOnRegion: (region: BodyRegion) => void;
|
||||
|
||||
clipping: { enabled: boolean; axis: ClipAxis; position: number };
|
||||
setClippingEnabled: (enabled: boolean) => void;
|
||||
setClipAxis: (axis: ClipAxis) => void;
|
||||
@@ -37,6 +50,12 @@ export const useAtlasStore = create<AtlasState>((set) => ({
|
||||
focusOnStructure: (slug) =>
|
||||
set((state) => ({ focusRequest: { slug, nonce: (state.focusRequest?.nonce ?? 0) + 1 } })),
|
||||
|
||||
regionFocusRequest: null,
|
||||
focusOnRegion: (region) =>
|
||||
set((state) => ({
|
||||
regionFocusRequest: { region, nonce: (state.regionFocusRequest?.nonce ?? 0) + 1 },
|
||||
})),
|
||||
|
||||
clipping: { enabled: false, axis: "sagittal", position: 0 },
|
||||
setClippingEnabled: (enabled) => set((state) => ({ clipping: { ...state.clipping, enabled } })),
|
||||
setClipAxis: (axis) => set((state) => ({ clipping: { ...state.clipping, axis } })),
|
||||
|
||||
Reference in new issue
Block a user