Corrigiu botões e throttle AR
X-Lovable-Edit-ID: edt-087c5038-cfa1-4ba9-93fe-3ad396e4c693 Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Eye, LayoutGrid, Grid3X3, Box, Ruler, Trash2, Camera,
|
Eye, LayoutGrid, Grid3X3, Box, Ruler, Trash2, Camera,
|
||||||
Move, RotateCw, RefreshCw, ChevronUp, ChevronDown, Grip,
|
Move, RotateCw, RefreshCw, ChevronUp, ChevronDown,
|
||||||
ClipboardCheck, X, Crosshair, Magnet, HelpCircle, Maximize2,
|
ClipboardCheck, X, Crosshair, Magnet, HelpCircle,
|
||||||
MousePointerClick, EyeOff, Focus,
|
MousePointerClick, EyeOff, Focus,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -310,19 +310,6 @@ export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTog
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Two-hand scale toggle */}
|
|
||||||
{onToggleAllowScale && (
|
|
||||||
<Button
|
|
||||||
variant={allowScale ? 'default' : 'outline'}
|
|
||||||
size="sm" className="h-8 gap-1.5 text-[10px] font-mono"
|
|
||||||
onClick={onToggleAllowScale}
|
|
||||||
title="Permite escalar a peça com gesto de duas mãos (afastando os controles)"
|
|
||||||
>
|
|
||||||
<Maximize2 className="h-3.5 w-3.5" />
|
|
||||||
{allowScale ? 'Escala ON' : 'Escala'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Scale presets */}
|
{/* Scale presets */}
|
||||||
<ScaleSelector variant="compact" />
|
<ScaleSelector variant="compact" />
|
||||||
|
|
||||||
@@ -332,15 +319,6 @@ export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTog
|
|||||||
{/* Share live screen */}
|
{/* Share live screen */}
|
||||||
<ShareButton variant="compact" />
|
<ShareButton variant="compact" />
|
||||||
|
|
||||||
<Button
|
|
||||||
variant={freeMove ? 'default' : 'outline'}
|
|
||||||
size="sm" className="h-8 gap-1.5 text-[10px] font-mono"
|
|
||||||
onClick={onToggleFreeMove}
|
|
||||||
>
|
|
||||||
<Grip className="h-3.5 w-3.5" />
|
|
||||||
{freeMove ? 'Mover ON' : 'Mover'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant={showGrid ? 'default' : 'outline'}
|
variant={showGrid ? 'default' : 'outline'}
|
||||||
size="icon" className="h-8 w-8"
|
size="icon" className="h-8 w-8"
|
||||||
|
|||||||
@@ -49,12 +49,21 @@ export function XRControllerMeasure() {
|
|||||||
const dwellStart = useRef(0);
|
const dwellStart = useRef(0);
|
||||||
const dwellFired = useRef(false);
|
const dwellFired = useRef(false);
|
||||||
|
|
||||||
|
// Throttling: raycast + snap analysis are heavy on dense IFC models and
|
||||||
|
// running them every frame at 72fps can stall the Quest right after
|
||||||
|
// toggling "Medir" from the Ferramentas tab. We cap heavy work to ~24Hz.
|
||||||
|
// Trigger/A/B/L-trigger polling continues every frame for responsiveness.
|
||||||
|
const heavyFrame = useRef(0);
|
||||||
|
const cachedSnappedPoint = useRef<THREE.Vector3 | null>(null);
|
||||||
|
const cachedSnapKind = useRef<'vertex' | 'edge' | 'surface' | 'hole'>('surface');
|
||||||
|
|
||||||
useFrame((_state, _dt, frame: XRFrame | undefined) => {
|
useFrame((_state, _dt, frame: XRFrame | undefined) => {
|
||||||
const measureMode = useModelStore.getState().measureMode;
|
const measureMode = useModelStore.getState().measureMode;
|
||||||
const selectionMode = useModelStore.getState().selectionMode;
|
const selectionMode = useModelStore.getState().selectionMode;
|
||||||
if (laserRef.current) laserRef.current.visible = false;
|
if (laserRef.current) laserRef.current.visible = false;
|
||||||
if (tipRef.current) tipRef.current.visible = false;
|
if (tipRef.current) tipRef.current.visible = false;
|
||||||
if ((!measureMode && !selectionMode) || !frame) return;
|
if ((!measureMode && !selectionMode) || !frame) return;
|
||||||
|
const doHeavy = (heavyFrame.current++ % 3) === 0;
|
||||||
|
|
||||||
const session = frame.session;
|
const session = frame.session;
|
||||||
const refSpace = gl.xr.getReferenceSpace();
|
const refSpace = gl.xr.getReferenceSpace();
|
||||||
@@ -95,72 +104,81 @@ export function XRControllerMeasure() {
|
|||||||
raycaster.current.set(tmpOrigin.current, tmpDir.current);
|
raycaster.current.set(tmpOrigin.current, tmpDir.current);
|
||||||
raycaster.current.far = MAX_RAY;
|
raycaster.current.far = MAX_RAY;
|
||||||
|
|
||||||
const hits = raycaster.current.intersectObjects(scene.children, true);
|
|
||||||
const hit = hits.find((h) => {
|
|
||||||
const o = h.object;
|
|
||||||
if (!(o instanceof THREE.Mesh)) return false;
|
|
||||||
if (o.userData.__edgeLine) return false;
|
|
||||||
if (o.geometry instanceof THREE.SphereGeometry) return false;
|
|
||||||
if (o.geometry instanceof THREE.RingGeometry) return false;
|
|
||||||
if (o.geometry instanceof THREE.PlaneGeometry) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
let snappedPoint: THREE.Vector3 | null = null;
|
let snappedPoint: THREE.Vector3 | null = null;
|
||||||
let snapKind: 'vertex' | 'edge' | 'surface' | 'hole' = 'surface';
|
let snapKind: 'vertex' | 'edge' | 'surface' | 'hole' = 'surface';
|
||||||
let hoverDetected: { kind: 'hole' | 'edge'; value: number; position: THREE.Vector3; endpoints?: { a: THREE.Vector3; b: THREE.Vector3 } } | null = null;
|
let hoverDetected: { kind: 'hole' | 'edge'; value: number; position: THREE.Vector3; endpoints?: { a: THREE.Vector3; b: THREE.Vector3 } } | null = null;
|
||||||
|
|
||||||
if (hit && hit.object instanceof THREE.Mesh) {
|
if (doHeavy) {
|
||||||
const size = gl.getSize(new THREE.Vector2());
|
const hits = raycaster.current.intersectObjects(scene.children, true);
|
||||||
const canvasSize = { width: size.x || 1024, height: size.y || 1024 };
|
const hit = hits.find((h) => {
|
||||||
const fakeCam = new THREE.PerspectiveCamera(60, 1, 0.01, 100);
|
const o = h.object;
|
||||||
fakeCam.position.copy(tmpOrigin.current);
|
if (!(o instanceof THREE.Mesh)) return false;
|
||||||
fakeCam.quaternion.copy(tmpQuat.current);
|
if (o.userData.__edgeLine) return false;
|
||||||
fakeCam.updateMatrixWorld(true);
|
if (o.geometry instanceof THREE.SphereGeometry) return false;
|
||||||
|
if (o.geometry instanceof THREE.RingGeometry) return false;
|
||||||
|
if (o.geometry instanceof THREE.PlaneGeometry) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// Snap to existing registered hole centers first (within ~30px screen)
|
if (hit && hit.object instanceof THREE.Mesh) {
|
||||||
const existing = useModelStore.getState().measurements;
|
const size = gl.getSize(new THREE.Vector2());
|
||||||
let bestHoleCenter: THREE.Vector3 | null = null;
|
const canvasSize = { width: size.x || 1024, height: size.y || 1024 };
|
||||||
let bestHolePx = Infinity;
|
const fakeCam = new THREE.PerspectiveCamera(60, 1, 0.01, 100);
|
||||||
const hitProj = hit.point.clone().project(fakeCam);
|
fakeCam.position.copy(tmpOrigin.current);
|
||||||
const hx = (hitProj.x * 0.5 + 0.5) * canvasSize.width;
|
fakeCam.quaternion.copy(tmpQuat.current);
|
||||||
const hy = (-hitProj.y * 0.5 + 0.5) * canvasSize.height;
|
fakeCam.updateMatrixWorld(true);
|
||||||
for (const m of existing) {
|
|
||||||
if (m.kind !== 'hole') continue;
|
|
||||||
const c = new THREE.Vector3(m.pointA.x, m.pointA.y, m.pointA.z);
|
|
||||||
const p = c.clone().project(fakeCam);
|
|
||||||
const sx = (p.x * 0.5 + 0.5) * canvasSize.width;
|
|
||||||
const sy = (-p.y * 0.5 + 0.5) * canvasSize.height;
|
|
||||||
const d = Math.hypot(sx - hx, sy - hy);
|
|
||||||
if (d < 30 && d < bestHolePx) { bestHolePx = d; bestHoleCenter = c; }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapEnabled && bestHoleCenter) {
|
// Snap to existing registered hole centers first (within ~30px screen)
|
||||||
snappedPoint = bestHoleCenter;
|
const existing = useModelStore.getState().measurements;
|
||||||
snapKind = 'hole';
|
let bestHoleCenter: THREE.Vector3 | null = null;
|
||||||
} else if (snapEnabled) {
|
let bestHolePx = Infinity;
|
||||||
// Try detecting a circular edge (hole) at hit
|
const hitProj = hit.point.clone().project(fakeCam);
|
||||||
const circle = detectCircularEdgeAtPoint(hit.object, hit.point, fakeCam, canvasSize, 60);
|
const hx = (hitProj.x * 0.5 + 0.5) * canvasSize.width;
|
||||||
if (circle) {
|
const hy = (-hitProj.y * 0.5 + 0.5) * canvasSize.height;
|
||||||
snappedPoint = circle.center;
|
for (const m of existing) {
|
||||||
|
if (m.kind !== 'hole') continue;
|
||||||
|
const c = new THREE.Vector3(m.pointA.x, m.pointA.y, m.pointA.z);
|
||||||
|
const p = c.clone().project(fakeCam);
|
||||||
|
const sx = (p.x * 0.5 + 0.5) * canvasSize.width;
|
||||||
|
const sy = (-p.y * 0.5 + 0.5) * canvasSize.height;
|
||||||
|
const d = Math.hypot(sx - hx, sy - hy);
|
||||||
|
if (d < 30 && d < bestHolePx) { bestHolePx = d; bestHoleCenter = c; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapEnabled && bestHoleCenter) {
|
||||||
|
snappedPoint = bestHoleCenter;
|
||||||
snapKind = 'hole';
|
snapKind = 'hole';
|
||||||
hoverDetected = { kind: 'hole', value: circle.diameterMM, position: circle.center };
|
} else if (snapEnabled) {
|
||||||
} else {
|
// Try detecting a circular edge (hole) at hit
|
||||||
const snap = resolveSnap(hit.object, hit.point, fakeCam, canvasSize, 14, 18);
|
const circle = detectCircularEdgeAtPoint(hit.object, hit.point, fakeCam, canvasSize, 60);
|
||||||
snappedPoint = snap.point;
|
if (circle) {
|
||||||
snapKind = snap.type;
|
snappedPoint = circle.center;
|
||||||
if (snap.type === 'edge') {
|
snapKind = 'hole';
|
||||||
const seg = findNearestEdgeSegment(hit.object, hit.point, fakeCam, canvasSize, 18);
|
hoverDetected = { kind: 'hole', value: circle.diameterMM, position: circle.center };
|
||||||
if (seg) {
|
} else {
|
||||||
const lenMM = seg.a.distanceTo(seg.b) * 1000;
|
const snap = resolveSnap(hit.object, hit.point, fakeCam, canvasSize, 14, 18);
|
||||||
hoverDetected = { kind: 'edge', value: lenMM, position: seg.midpoint, endpoints: { a: seg.a, b: seg.b } };
|
snappedPoint = snap.point;
|
||||||
|
snapKind = snap.type;
|
||||||
|
if (snap.type === 'edge') {
|
||||||
|
const seg = findNearestEdgeSegment(hit.object, hit.point, fakeCam, canvasSize, 18);
|
||||||
|
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 } };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
snappedPoint = hit.point.clone();
|
||||||
|
snapKind = 'surface';
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
snappedPoint = hit.point.clone();
|
|
||||||
snapKind = 'surface';
|
|
||||||
}
|
}
|
||||||
|
// Cache for non-heavy frames so the laser + trigger still have a target
|
||||||
|
cachedSnappedPoint.current = snappedPoint ? snappedPoint.clone() : null;
|
||||||
|
cachedSnapKind.current = snapKind;
|
||||||
|
} else {
|
||||||
|
// Light frame: reuse the last computed snap so visuals stay smooth
|
||||||
|
snappedPoint = cachedSnappedPoint.current ? cachedSnappedPoint.current.clone() : null;
|
||||||
|
snapKind = cachedSnapKind.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Dwell detection (1 s) to auto-register hovered hole/edge ─────
|
// ── Dwell detection (1 s) to auto-register hovered hole/edge ─────
|
||||||
@@ -221,21 +239,33 @@ export function XRControllerMeasure() {
|
|||||||
if (!trigState.current && trigVal > TRIG_ON) {
|
if (!trigState.current && trigVal > TRIG_ON) {
|
||||||
trigState.current = true;
|
trigState.current = true;
|
||||||
const st = useModelStore.getState();
|
const st = useModelStore.getState();
|
||||||
if (st.selectionMode && hit && hit.object instanceof THREE.Mesh) {
|
if (st.selectionMode) {
|
||||||
// Walk up to find ifcElement + modelId
|
// Fresh raycast on the press itself (not every frame) for selection
|
||||||
let cur: THREE.Object3D | null = hit.object;
|
const triggerHits = raycaster.current.intersectObjects(scene.children, true);
|
||||||
let element: THREE.Object3D | null = null;
|
const triggerHit = triggerHits.find((h) => {
|
||||||
let modelId: string | null = null;
|
const o = h.object;
|
||||||
while (cur) {
|
if (!(o instanceof THREE.Mesh)) return false;
|
||||||
if (!element && cur.userData?.ifcElement) element = cur;
|
if (o.userData.__edgeLine) return false;
|
||||||
if (!modelId && cur.userData?.modelId) modelId = cur.userData.modelId as string;
|
if (o.geometry instanceof THREE.SphereGeometry) return false;
|
||||||
cur = cur.parent;
|
if (o.geometry instanceof THREE.RingGeometry) return false;
|
||||||
}
|
if (o.geometry instanceof THREE.PlaneGeometry) return false;
|
||||||
if (!element) element = hit.object;
|
return true;
|
||||||
if (!modelId) modelId = st.activeModelId;
|
});
|
||||||
if (modelId && element) {
|
if (triggerHit && triggerHit.object instanceof THREE.Mesh) {
|
||||||
const id = element.userData?.ifcId ?? element.name ?? element.uuid;
|
let cur: THREE.Object3D | null = triggerHit.object;
|
||||||
st.toggleElementSelection(`${modelId}:${id}`);
|
let element: THREE.Object3D | null = null;
|
||||||
|
let modelId: string | null = null;
|
||||||
|
while (cur) {
|
||||||
|
if (!element && cur.userData?.ifcElement) element = cur;
|
||||||
|
if (!modelId && cur.userData?.modelId) modelId = cur.userData.modelId as string;
|
||||||
|
cur = cur.parent;
|
||||||
|
}
|
||||||
|
if (!element) element = triggerHit.object;
|
||||||
|
if (!modelId) modelId = st.activeModelId;
|
||||||
|
if (modelId && element) {
|
||||||
|
const id = element.userData?.ifcId ?? element.name ?? element.uuid;
|
||||||
|
st.toggleElementSelection(`${modelId}:${id}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (snappedPoint) {
|
} else if (snappedPoint) {
|
||||||
st.addMeasurePoint({
|
st.addMeasurePoint({
|
||||||
|
|||||||
@@ -350,9 +350,7 @@ function ToolsTab(p: XRHudInWorldProps) {
|
|||||||
active={measureMode} onClick={() => setMeasureMode(!measureMode)} />
|
active={measureMode} onClick={() => setMeasureMode(!measureMode)} />
|
||||||
<XR3DButton position={[0.0, 0.025, 0]} size={[0.075, 0.022]} label={`Snap ${p.snapToPlanes ? 'ON' : 'OFF'}`}
|
<XR3DButton position={[0.0, 0.025, 0]} size={[0.075, 0.022]} label={`Snap ${p.snapToPlanes ? 'ON' : 'OFF'}`}
|
||||||
active={p.snapToPlanes} onClick={p.onToggleSnap} />
|
active={p.snapToPlanes} onClick={p.onToggleSnap} />
|
||||||
<XR3DButton position={[0.08, 0.025, 0]} size={[0.075, 0.022]} label={`Escala ${p.allowScale ? 'ON' : 'OFF'}`}
|
<XR3DButton position={[0.08, 0.025, 0]} size={[0.075, 0.022]}
|
||||||
active={p.allowScale} onClick={p.onToggleAllowScale} />
|
|
||||||
<XR3DButton position={[0.16, 0.025, 0]} size={[0.075, 0.022]}
|
|
||||||
label={p.placementMode ? 'Posic…' : 'Reposic.'}
|
label={p.placementMode ? 'Posic…' : 'Reposic.'}
|
||||||
active={p.placementMode} onClick={p.onTogglePlacement} />
|
active={p.placementMode} onClick={p.onTogglePlacement} />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user