Melhorou snap e botões AR
X-Lovable-Edit-ID: edt-fe7c72e3-ce24-4acd-be0e-85f2e7f6eca3 Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -310,6 +310,19 @@ export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTog
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Allow scale (two-hand zoom) toggle */}
|
||||
{onToggleAllowScale && (
|
||||
<Button
|
||||
variant={allowScale ? 'default' : 'outline'}
|
||||
size="sm" className="h-8 gap-1.5 text-[10px] font-mono"
|
||||
onClick={onToggleAllowScale}
|
||||
title="Zoom por grip duplo (afastar/aproximar as mãos)"
|
||||
>
|
||||
<RefreshCw className="h-3.5 w-3.5" />
|
||||
{allowScale ? 'Zoom ON' : 'Zoom'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Scale presets */}
|
||||
<ScaleSelector variant="compact" />
|
||||
|
||||
|
||||
@@ -149,18 +149,20 @@ export function XRControllerMeasure() {
|
||||
snappedPoint = bestHoleCenter;
|
||||
snapKind = 'hole';
|
||||
} else if (snapEnabled) {
|
||||
// Try detecting a circular edge (hole) at hit
|
||||
const circle = detectCircularEdgeAtPoint(hit.object, hit.point, fakeCam, canvasSize, 60);
|
||||
// Try detecting a circular edge (hole) at hit — radius mais generoso
|
||||
const circle = detectCircularEdgeAtPoint(hit.object, hit.point, fakeCam, canvasSize, 90);
|
||||
if (circle) {
|
||||
snappedPoint = circle.center;
|
||||
snapKind = 'hole';
|
||||
hoverDetected = { kind: 'hole', value: circle.diameterMM, position: circle.center };
|
||||
} else {
|
||||
const snap = resolveSnap(hit.object, hit.point, fakeCam, canvasSize, 14, 18);
|
||||
// Snap mais "magnético": raios maiores de busca em pixels
|
||||
// vértice: 32px (antes 14), aresta: 48px (antes 18)
|
||||
const snap = resolveSnap(hit.object, hit.point, fakeCam, canvasSize, 32, 48);
|
||||
snappedPoint = snap.point;
|
||||
snapKind = snap.type;
|
||||
if (snap.type === 'edge') {
|
||||
const seg = findNearestEdgeSegment(hit.object, hit.point, fakeCam, canvasSize, 18);
|
||||
const seg = findNearestEdgeSegment(hit.object, hit.point, fakeCam, canvasSize, 48);
|
||||
if (seg) {
|
||||
const lenMM = seg.a.distanceTo(seg.b) * 1000;
|
||||
hoverDetected = { kind: 'edge', value: lenMM, position: seg.midpoint, endpoints: { a: seg.a, b: seg.b } };
|
||||
@@ -172,7 +174,9 @@ export function XRControllerMeasure() {
|
||||
snapKind = 'surface';
|
||||
}
|
||||
}
|
||||
// Cache for non-heavy frames so the laser + trigger still have a target
|
||||
// Cache for non-heavy frames so the laser + trigger still have a target.
|
||||
// Sticky: se conseguimos um snap "forte" (vertex/edge/hole), preservamos
|
||||
// ele entre frames leves para evitar cintilação.
|
||||
cachedSnappedPoint.current = snappedPoint ? snappedPoint.clone() : null;
|
||||
cachedSnapKind.current = snapKind;
|
||||
} else {
|
||||
@@ -227,9 +231,13 @@ export function XRControllerMeasure() {
|
||||
if (snappedPoint) {
|
||||
tipRef.current.visible = true;
|
||||
tipRef.current.position.copy(snappedPoint);
|
||||
// Scale tip with distance from controller (~12mm at 1m)
|
||||
// Tip scales with distance from controller. Snap "forte" (vértice,
|
||||
// aresta, furo) recebe um marcador ~3× maior do que uma superfície
|
||||
// genérica, para que o ponto destacado fique bem visível.
|
||||
const dist = tmpOrigin.current.distanceTo(snappedPoint);
|
||||
const s = Math.max(0.004, dist * 0.012);
|
||||
const strongSnap = snapKind === 'vertex' || snapKind === 'edge' || snapKind === 'hole';
|
||||
const factor = strongSnap ? 0.030 : 0.012;
|
||||
const s = Math.max(strongSnap ? 0.010 : 0.004, dist * factor);
|
||||
tipRef.current.scale.setScalar(s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import { useModelStore } from '@/stores/useModelStore';
|
||||
|
||||
interface XRGrabbableProps {
|
||||
/**
|
||||
* @deprecated Mantido por compatibilidade — agora o zoom por duas mãos
|
||||
* está SEMPRE ativo (faz parte do mapeamento padrão do grip).
|
||||
* Quando true (padrão), o grip duplo aplica ZOOM uniforme além da rotação.
|
||||
* Quando false, o grip duplo só ORBITA — a escala da peça é preservada.
|
||||
*/
|
||||
allowScale?: boolean;
|
||||
/** When true, the active model is locked — grab is ignored entirely */
|
||||
@@ -51,7 +51,7 @@ const _tmpQuat = new THREE.Quaternion();
|
||||
*
|
||||
* Trigger não interfere aqui (reservado para seleção/medição).
|
||||
*/
|
||||
export function XRGrabbable({ lockedActive = false, onGrabStart, children }: XRGrabbableProps) {
|
||||
export function XRGrabbable({ allowScale = true, lockedActive = false, onGrabStart, children }: XRGrabbableProps) {
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const grab = useControllerGrab();
|
||||
|
||||
@@ -121,8 +121,10 @@ export function XRGrabbable({ lockedActive = false, onGrabStart, children }: XRG
|
||||
|
||||
const d = dual.current;
|
||||
const deltaQuat = new THREE.Quaternion().setFromUnitVectors(d.startAxis, axisNow);
|
||||
// ZOOM sempre ativo no grip duplo
|
||||
const scaleRatio = THREE.MathUtils.clamp(distNow / d.startDist, 0.1, 10);
|
||||
// ZOOM só quando allowScale=true. Caso contrário, mantém escala 1× (só orbita).
|
||||
const scaleRatio = allowScale
|
||||
? THREE.MathUtils.clamp(distNow / d.startDist, 0.1, 10)
|
||||
: 1;
|
||||
|
||||
const tToOrigin = new THREE.Matrix4().makeTranslation(-d.startMid.x, -d.startMid.y, -d.startMid.z);
|
||||
const tBack = new THREE.Matrix4().makeTranslation(midNow.x, midNow.y, midNow.z);
|
||||
|
||||
@@ -328,6 +328,15 @@ function ToolsTab(p: XRHudInWorldProps) {
|
||||
const setShowGrid = useModelStore((s) => s.setShowGrid);
|
||||
const measureMode = useModelStore((s) => s.measureMode);
|
||||
const setMeasureMode = useModelStore((s) => s.setMeasureMode);
|
||||
const selectionMode = useModelStore((s) => s.selectionMode);
|
||||
const setSelectionMode = useModelStore((s) => s.setSelectionMode);
|
||||
const selectedCount = useModelStore((s) => s.selectedElementKeys.size);
|
||||
const hideSelectedElements = useModelStore((s) => s.hideSelectedElements);
|
||||
const isolateSelectedElements = useModelStore((s) => s.isolateSelectedElements);
|
||||
const showAllElements = useModelStore((s) => s.showAllElements);
|
||||
const hasHidden = useModelStore((s) =>
|
||||
s.hiddenElementKeys.size > 0 || s.isolatedElementKeys !== null
|
||||
);
|
||||
const scaleRatio = useModelStore((s) => s.scaleRatio);
|
||||
const setScaleRatio = useModelStore((s) => s.setScaleRatio);
|
||||
const resetScale = useModelStore((s) => s.resetScale);
|
||||
@@ -353,52 +362,61 @@ function ToolsTab(p: XRHudInWorldProps) {
|
||||
<XR3DButton position={[0.08, 0.025, 0]} size={[0.075, 0.022]}
|
||||
label={p.placementMode ? 'Posic…' : 'Reposic.'}
|
||||
active={p.placementMode} onClick={p.onTogglePlacement} />
|
||||
<XR3DButton position={[0.16, 0.025, 0]} size={[0.075, 0.022]}
|
||||
label={`Zoom ${p.allowScale ? 'ON' : 'OFF'}`}
|
||||
active={p.allowScale} onClick={p.onToggleAllowScale} />
|
||||
|
||||
<Text position={[-0.24, -0.02, 0]} fontSize={0.008} color="#94a3b8" anchorX="left">
|
||||
{/* Selecionar — paridade com a tela inicial */}
|
||||
<Text position={[-0.24, -0.005, 0]} fontSize={0.008} color="#94a3b8" anchorX="left">Selecionar</Text>
|
||||
<XR3DButton position={[-0.16, -0.03, 0]} size={[0.075, 0.022]}
|
||||
label={selectionMode ? (selectedCount > 0 ? `${selectedCount} sel.` : 'Selec…') : 'Selec'}
|
||||
active={selectionMode} onClick={() => setSelectionMode(!selectionMode)} />
|
||||
<XR3DButton position={[-0.08, -0.03, 0]} size={[0.075, 0.022]} label="Esconder"
|
||||
onClick={hideSelectedElements} />
|
||||
<XR3DButton position={[0.0, -0.03, 0]} size={[0.075, 0.022]} label="Isolar"
|
||||
onClick={isolateSelectedElements} />
|
||||
<XR3DButton position={[0.08, -0.03, 0]} size={[0.075, 0.022]} label="Mostrar"
|
||||
active={hasHidden} onClick={showAllElements} />
|
||||
|
||||
<Text position={[-0.24, -0.06, 0]} fontSize={0.008} color="#94a3b8" anchorX="left">
|
||||
Opacidade {Math.round(opacity * 100)}%
|
||||
</Text>
|
||||
<XR3DButton position={[-0.16, -0.045, 0]} size={[0.05, 0.022]} label="−25%"
|
||||
<XR3DButton position={[-0.16, -0.085, 0]} size={[0.05, 0.022]} label="−25%"
|
||||
onClick={() => setOpacity(Math.max(0, opacity - 0.25))} />
|
||||
<XR3DButton position={[-0.105, -0.045, 0]} size={[0.05, 0.022]} label="−5%"
|
||||
<XR3DButton position={[-0.105, -0.085, 0]} size={[0.05, 0.022]} label="−5%"
|
||||
onClick={() => setOpacity(Math.max(0, opacity - 0.05))} />
|
||||
<XR3DButton position={[-0.05, -0.045, 0]} size={[0.05, 0.022]} label="+5%"
|
||||
<XR3DButton position={[-0.05, -0.085, 0]} size={[0.05, 0.022]} label="+5%"
|
||||
onClick={() => setOpacity(Math.min(1, opacity + 0.05))} />
|
||||
<XR3DButton position={[0.005, -0.045, 0]} size={[0.05, 0.022]} label="+25%"
|
||||
<XR3DButton position={[0.005, -0.085, 0]} size={[0.05, 0.022]} label="+25%"
|
||||
onClick={() => setOpacity(Math.min(1, opacity + 0.25))} />
|
||||
<XR3DButton position={[0.06, -0.045, 0]} size={[0.05, 0.022]} label="100%"
|
||||
<XR3DButton position={[0.06, -0.085, 0]} size={[0.05, 0.022]} label="100%"
|
||||
onClick={() => setOpacity(1)} />
|
||||
|
||||
<Text position={[-0.24, -0.085, 0]} fontSize={0.008} color="#94a3b8" anchorX="left">
|
||||
<Text position={[-0.24, -0.115, 0]} fontSize={0.008} color="#94a3b8" anchorX="left">
|
||||
Escala {scaleRatio.label}
|
||||
</Text>
|
||||
{SCALE_LABELS.map((lbl, i) => {
|
||||
const preset = SCALE_PRESETS.find((sp) => sp.label === lbl)!;
|
||||
return (
|
||||
<XR3DButton key={lbl}
|
||||
position={[-0.16 + i * 0.06, -0.11, 0]} size={[0.055, 0.022]} label={lbl}
|
||||
position={[-0.16 + i * 0.06, -0.14, 0]} size={[0.055, 0.022]} label={lbl}
|
||||
active={scaleRatio.label === lbl}
|
||||
onClick={() => setScaleRatio(preset)} />
|
||||
);
|
||||
})}
|
||||
{/* Reset Escala — devolve a peça ao tamanho 1:1 (cancela qualquer
|
||||
escala manual feita com duas mãos). */}
|
||||
<XR3DButton position={[0.10, -0.11, 0]} size={[0.062, 0.022]}
|
||||
<XR3DButton position={[0.10, -0.14, 0]} size={[0.062, 0.022]}
|
||||
label="↺ Reset" color="#dc2626"
|
||||
onClick={resetScale} fontSize={0.0085} />
|
||||
{/* Limpa todos os labels de medição. */}
|
||||
<XR3DButton position={[0.17, -0.11, 0]} size={[0.062, 0.022]}
|
||||
<XR3DButton position={[0.17, -0.14, 0]} size={[0.062, 0.022]}
|
||||
label="✕ Medidas" color="#dc2626"
|
||||
onClick={clearMeasurements} fontSize={0.0085} />
|
||||
|
||||
{/* Legenda dos controles para Medir */}
|
||||
{measureMode && (
|
||||
<Text position={[-0.24, -0.145, 0]} fontSize={0.0065} color="#a3e635" anchorX="left" maxWidth={0.5}>
|
||||
<Text position={[-0.24, -0.17, 0]} fontSize={0.0065} color="#a3e635" anchorX="left" maxWidth={0.5}>
|
||||
Gatilho D: marcar · A: desfazer · B: limpar · Gatilho E: alternar snap
|
||||
</Text>
|
||||
)}
|
||||
|
||||
|
||||
{/* Grid floor controls */}
|
||||
<GridFloorRow />
|
||||
</group>
|
||||
);
|
||||
|
||||
@@ -471,9 +471,8 @@ const XRSession = () => {
|
||||
const [freeMove, setFreeMove] = useState(true);
|
||||
const [placementMode, setPlacementMode] = useState(true); // start in placement mode
|
||||
const [snapToPlanes, setSnapToPlanes] = useState(true);
|
||||
// Zoom por grip duplo é SEMPRE ligado (parte do mapeamento padrão).
|
||||
const allowScale = true;
|
||||
const setAllowScale = (_v?: boolean) => {};
|
||||
// Zoom por grip duplo — toggle real, padrão ligado.
|
||||
const [allowScale, setAllowScale] = useState(true);
|
||||
const [liveCode, setLiveCode] = useState<string | null>(null);
|
||||
const [liveViewers, setLiveViewers] = useState(0);
|
||||
// Rig que envolve <XROrigin/> — locomoção por joystick escreve aqui.
|
||||
|
||||
Reference in New Issue
Block a user