tegumentar2
This commit is contained in:
1 parent
077e451902
commit
063bd629c2
4 files changed
+213
-84
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
|
||||
import { useAtlasStore, type BodyRegion } from "../store/useAtlasStore.js";
|
||||
|
||||
const REGIONS: { region: BodyRegion; label: string }[] = [
|
||||
@@ -11,27 +12,121 @@ const REGIONS: { region: BodyRegion; label: string }[] = [
|
||||
{ region: "corpo_inteiro", label: "Corpo Inteiro" },
|
||||
];
|
||||
|
||||
const STORAGE_KEY = "atlas.quickRegionNav.pos";
|
||||
|
||||
function loadStoredPos(): { x: number; y: number } | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// pede direto pra "ir" numa região do corpo. Arrastável pela alcinha (⠿),
|
||||
// pois a barra pode cair em cima de uma parte do modelo que o usuário
|
||||
// queira ver — a posição escolhida fica salva entre visitas.
|
||||
export function QuickRegionNav() {
|
||||
const focusOnRegion = useAtlasStore((s) => s.focusOnRegion);
|
||||
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);
|
||||
|
||||
function clamp(x: number, y: number) {
|
||||
const el = elRef.current;
|
||||
const parent = el?.offsetParent as HTMLElement | null;
|
||||
if (!el || !parent) return { x, y };
|
||||
const maxX = Math.max(0, parent.clientWidth - el.offsetWidth);
|
||||
const maxY = Math.max(0, parent.clientHeight - el.offsetHeight);
|
||||
return { x: Math.min(Math.max(0, x), maxX), y: Math.min(Math.max(0, y), maxY) };
|
||||
}
|
||||
|
||||
function onGripPointerDown(e: ReactPointerEvent) {
|
||||
const el = elRef.current;
|
||||
const parent = el?.offsetParent as HTMLElement | null;
|
||||
if (!el || !parent) return;
|
||||
// Antes do primeiro arrasto a barra fica centralizada via CSS transform
|
||||
// (translate-x-1/2) — offsetLeft/offsetTop ignoram transform, então
|
||||
// calculamos a posição visual real a partir do bounding rect pra não
|
||||
// dar aquele "pulo" no primeiro clique.
|
||||
const elRect = el.getBoundingClientRect();
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
dragRef.current = {
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
origX: elRect.left - parentRect.left,
|
||||
origY: elRect.top - parentRect.top,
|
||||
};
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
}
|
||||
|
||||
function onGripPointerMove(e: ReactPointerEvent) {
|
||||
if (!dragRef.current) return;
|
||||
const dx = e.clientX - dragRef.current.startX;
|
||||
const dy = e.clientY - dragRef.current.startY;
|
||||
setPos(clamp(dragRef.current.origX + dx, dragRef.current.origY + dy));
|
||||
}
|
||||
|
||||
function onGripPointerUp() {
|
||||
dragRef.current = null;
|
||||
setPos((current) => {
|
||||
if (current) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(current));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return current;
|
||||
});
|
||||
}
|
||||
|
||||
// Se a janela mudar de tamanho (ex: girar o celular), reaplica o clamp
|
||||
// pra barra não ficar presa fora da área visível.
|
||||
useEffect(() => {
|
||||
if (!pos) return;
|
||||
function onResize() {
|
||||
setPos((current) => (current ? clamp(current.x, current.y) : current));
|
||||
}
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute top-14 md:top-3 left-1/2 -translate-x-1/2 flex gap-1.5
|
||||
overflow-x-auto md:flex-wrap md:justify-center bg-panel/90 border border-white/10
|
||||
rounded-lg px-3 py-2 shadow-lg max-w-[92vw] md:max-w-[90%]"
|
||||
ref={elRef}
|
||||
style={pos ? { position: "absolute", left: pos.x, top: pos.y } : undefined}
|
||||
className={
|
||||
"flex items-center gap-1.5 bg-panel/90 border border-white/10 rounded-lg pl-1.5 pr-3 py-2 shadow-lg " +
|
||||
(pos
|
||||
? "max-w-[92vw]"
|
||||
: "absolute top-14 md:top-3 left-1/2 -translate-x-1/2 max-w-[92vw] md:max-w-[90%]")
|
||||
}
|
||||
>
|
||||
{REGIONS.map((r) => (
|
||||
<button
|
||||
key={r.region}
|
||||
onClick={() => focusOnRegion(r.region)}
|
||||
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"
|
||||
>
|
||||
{r.label}
|
||||
</button>
|
||||
))}
|
||||
<div
|
||||
onPointerDown={onGripPointerDown}
|
||||
onPointerMove={onGripPointerMove}
|
||||
onPointerUp={onGripPointerUp}
|
||||
className="shrink-0 cursor-move select-none text-slate-500 hover:text-slate-300 px-1 touch-none"
|
||||
title="Arrastar"
|
||||
aria-label="Arrastar menu de regiões"
|
||||
>
|
||||
⠿
|
||||
</div>
|
||||
<div className="flex gap-1.5 overflow-x-auto md:flex-wrap">
|
||||
{REGIONS.map((r) => (
|
||||
<button
|
||||
key={r.region}
|
||||
onClick={() => focusOnRegion(r.region)}
|
||||
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"
|
||||
>
|
||||
{r.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user