This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-10-23 13:22:24 -03:00
1 parent 1e1d2a9473
commit 157993d755
3 files changed
+71 -10

No files matched your search

+61 -10
View File
@@ -32,6 +32,12 @@ export function QuickRegionNav() {
const focusOnRegion = useAtlasStore((s) => s.focusOnRegion); const focusOnRegion = useAtlasStore((s) => s.focusOnRegion);
const focusOnViewAngle = useAtlasStore((s) => s.focusOnViewAngle); 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());
// Só pra realçar visualmente o último botão clicado em cada grupo — o
// enquadramento inicial já é "Corpo Inteiro" + "Anterior" (ver Scene.tsx),
// então começamos os dois já marcados como ativos pra bater com o que a
// câmera mostra ao abrir.
const [activeRegion, setActiveRegion] = useState<BodyRegion>("corpo_inteiro");
const [activeAngle, setActiveAngle] = useState<"anterior" | "posterior">("anterior");
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);
@@ -111,40 +117,85 @@ export function QuickRegionNav() {
onPointerDown={onGripPointerDown} onPointerDown={onGripPointerDown}
onPointerMove={onGripPointerMove} onPointerMove={onGripPointerMove}
onPointerUp={onGripPointerUp} onPointerUp={onGripPointerUp}
className="shrink-0 cursor-move select-none text-slate-500 hover:text-slate-300 px-1 touch-none" className="shrink-0 cursor-move select-none text-slate-500 hover:text-slate-300 px-1 touch-none self-stretch flex items-center"
title="Arrastar" title="Arrastar"
aria-label="Arrastar menu de regiões" aria-label="Arrastar menu de regiões"
> >
⠿ ⠿
</div> </div>
<div className="flex gap-1.5 overflow-x-auto md:flex-wrap">
{/* Duas linhas em vez de um único flex-wrap: região e ângulo de vista
são decisões diferentes (onde olhar x de que lado olhar), e
misturadas numa fileira só quebravam de um jeito confuso (ex:
"Corpo Inteiro" ficava orfão numa segunda linha, e Anterior/
Posterior se perdiam no meio dos pills de região). */}
<div className="flex flex-col gap-1.5 min-w-0">
<div className="flex items-center gap-1.5">
<span className="shrink-0 text-[10px] font-medium uppercase tracking-wide text-slate-500 w-14">
Região
</span>
<div className="flex gap-1.5 overflow-x-auto md:flex-wrap md:max-w-[40vw]">
{REGIONS.map((r) => ( {REGIONS.map((r) => (
<button <button
key={r.region} key={r.region}
onClick={() => focusOnRegion(r.region)} onClick={() => {
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" setActiveRegion(r.region);
focusOnRegion(r.region);
}}
className={
"shrink-0 text-xs px-3 py-1.5 rounded-full transition-colors " +
(activeRegion === r.region
? "bg-accent text-white"
: "bg-white/5 hover:bg-accent hover:text-white text-slate-300")
}
> >
{r.label} {r.label}
</button> </button>
))} ))}
</div> </div>
<div className="shrink-0 h-6 w-px bg-white/10" aria-hidden="true" /> </div>
<div className="flex gap-1.5 shrink-0">
<div className="flex items-center gap-1.5">
<span className="shrink-0 text-[10px] font-medium uppercase tracking-wide text-slate-500 w-14">
Vista
</span>
{/* Toggle segmentado (2 opções, uma cápsula só) em vez de dois
pills soltos — deixa claro que é uma escolha binária (frente
OU costas), não mais um item de uma lista como as regiões. */}
<div className="inline-flex shrink-0 rounded-full bg-white/5 p-0.5">
<button <button
onClick={() => focusOnViewAngle("anterior")} onClick={() => {
setActiveAngle("anterior");
focusOnViewAngle("anterior");
}}
title="Vista anterior (de frente)" 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" className={
"text-xs px-3 py-1 rounded-full transition-colors " +
(activeAngle === "anterior"
? "bg-accent text-white"
: "text-slate-300 hover:text-white")
}
> >
Anterior Anterior
</button> </button>
<button <button
onClick={() => focusOnViewAngle("posterior")} onClick={() => {
setActiveAngle("posterior");
focusOnViewAngle("posterior");
}}
title="Vista posterior (de costas)" 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" className={
"text-xs px-3 py-1 rounded-full transition-colors " +
(activeAngle === "posterior"
? "bg-accent text-white"
: "text-slate-300 hover:text-white")
}
> >
Posterior Posterior
</button> </button>
</div> </div>
</div> </div>
</div>
</div>
); );
} }
+1
View File
@@ -382,6 +382,7 @@ export function Scene({
sectionColor="#334155" sectionColor="#334155"
fadeDistance={6} fadeDistance={6}
infiniteGrid infiniteGrid
userData={{ excludeFromFit: true }}
/> />
<CameraController modelFiles={modelFiles} /> <CameraController modelFiles={modelFiles} />
<IntercostalLabels bySlug={bySlug} /> <IntercostalLabels bySlug={bySlug} />
+9
View File
@@ -15,12 +15,21 @@ function isFiniteVec3(v: THREE.Vector3): boolean {
// vértices degenerados/NaN, e Box3.setFromObject(scene) ingenuamente // vértices degenerados/NaN, e Box3.setFromObject(scene) ingenuamente
// incluiria isso, distorcendo o enquadramento (ou escondendo tudo o mais // incluiria isso, distorcendo o enquadramento (ou escondendo tudo o mais
// fora do "zoom" resultante). // fora do "zoom" resultante).
//
// Também pulamos objetos marcados com userData.excludeFromFit — o <Grid>
// decorativo do chão (ver Scene.tsx) é uma malha de verdade (um
// <planeGeometry args={[4,4]}/> por baixo do shader que faz ele PARECER
// infinito), então sem essa exclusão ele entrava na conta do
// computeVisibleBoundingBox(scene) e distorcia o enquadramento inicial da
// câmera (cálculo de distância/aspecto erradOS, câmera ficando perto
// demais ou longe demais do corpo de verdade).
export function computeVisibleBoundingBox(root: THREE.Object3D): THREE.Box3 { export function computeVisibleBoundingBox(root: THREE.Object3D): THREE.Box3 {
const union = new THREE.Box3(); const union = new THREE.Box3();
const tmp = new THREE.Box3(); const tmp = new THREE.Box3();
root.traverse((obj) => { root.traverse((obj) => {
const mesh = obj as THREE.Mesh; const mesh = obj as THREE.Mesh;
if (!mesh.isMesh || !mesh.visible) return; if (!mesh.isMesh || !mesh.visible) return;
if (mesh.userData?.excludeFromFit) return;
tmp.setFromObject(mesh); tmp.setFromObject(mesh);
if (tmp.isEmpty()) return; if (tmp.isEmpty()) return;
if (!isFiniteVec3(tmp.min) || !isFiniteVec3(tmp.max)) return; if (!isFiniteVec3(tmp.min) || !isFiniteVec3(tmp.max)) return;