Melhorou UI e medições XR
X-Lovable-Edit-ID: edt-f9fa9898-79ad-42ba-b1c0-1c77adeec24a Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -529,7 +529,7 @@ function HoverDetector() {
|
||||
if (!inMeasure) {
|
||||
hideTimer.current = setTimeout(() => setHoverInfo(null), 4000);
|
||||
}
|
||||
}, 2000);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const onLeave = () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useRef, useState } from 'react';
|
||||
import { useFrame, useThree } from '@react-three/fiber';
|
||||
import * as THREE from 'three';
|
||||
import { useModelStore } from '@/stores/useModelStore';
|
||||
import { resolveSnap } from './SmartMeasure';
|
||||
import { resolveSnap, detectCircularEdgeAtPoint, findNearestEdgeSegment } from './SmartMeasure';
|
||||
|
||||
const TRIG_ON = 0.7;
|
||||
const TRIG_OFF = 0.3;
|
||||
@@ -44,6 +44,11 @@ export function XRControllerMeasure() {
|
||||
const laserMat = useRef(new THREE.LineBasicMaterial({ color: '#22c55e', transparent: true, opacity: 0.7, depthTest: false }));
|
||||
const tipColor = useRef(new THREE.Color('#22c55e'));
|
||||
|
||||
// Dwell detection for hover-based smart measurement (1 s)
|
||||
const dwellPos = useRef(new THREE.Vector3(Infinity, Infinity, Infinity));
|
||||
const dwellStart = useRef(0);
|
||||
const dwellFired = useRef(false);
|
||||
|
||||
useFrame((_state, _dt, frame: XRFrame | undefined) => {
|
||||
const measureMode = useModelStore.getState().measureMode;
|
||||
if (laserRef.current) laserRef.current.visible = false;
|
||||
@@ -101,32 +106,90 @@ export function XRControllerMeasure() {
|
||||
});
|
||||
|
||||
let snappedPoint: THREE.Vector3 | null = null;
|
||||
let snapKind: 'vertex' | 'edge' | 'surface' = '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;
|
||||
|
||||
if (hit && hit.object instanceof THREE.Mesh) {
|
||||
if (snapEnabled) {
|
||||
// Use a virtual canvas size based on session — fall back to current renderer size
|
||||
const size = gl.getSize(new THREE.Vector2());
|
||||
const fakeCam = new THREE.PerspectiveCamera(60, 1, 0.01, 100);
|
||||
fakeCam.position.copy(tmpOrigin.current);
|
||||
fakeCam.quaternion.copy(tmpQuat.current);
|
||||
fakeCam.updateMatrixWorld(true);
|
||||
const snap = resolveSnap(
|
||||
hit.object,
|
||||
hit.point,
|
||||
fakeCam,
|
||||
{ width: size.x || 1024, height: size.y || 1024 },
|
||||
14,
|
||||
18
|
||||
);
|
||||
snappedPoint = snap.point;
|
||||
snapKind = snap.type;
|
||||
const size = gl.getSize(new THREE.Vector2());
|
||||
const canvasSize = { width: size.x || 1024, height: size.y || 1024 };
|
||||
const fakeCam = new THREE.PerspectiveCamera(60, 1, 0.01, 100);
|
||||
fakeCam.position.copy(tmpOrigin.current);
|
||||
fakeCam.quaternion.copy(tmpQuat.current);
|
||||
fakeCam.updateMatrixWorld(true);
|
||||
|
||||
// Snap to existing registered hole centers first (within ~30px screen)
|
||||
const existing = useModelStore.getState().measurements;
|
||||
let bestHoleCenter: THREE.Vector3 | null = null;
|
||||
let bestHolePx = Infinity;
|
||||
const hitProj = hit.point.clone().project(fakeCam);
|
||||
const hx = (hitProj.x * 0.5 + 0.5) * canvasSize.width;
|
||||
const hy = (-hitProj.y * 0.5 + 0.5) * canvasSize.height;
|
||||
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';
|
||||
} else if (snapEnabled) {
|
||||
// Try detecting a circular edge (hole) at hit
|
||||
const circle = detectCircularEdgeAtPoint(hit.object, hit.point, fakeCam, canvasSize, 60);
|
||||
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);
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dwell detection (1 s) to auto-register hovered hole/edge ─────
|
||||
const measureModeNow = useModelStore.getState().measureMode;
|
||||
const nowT = performance.now();
|
||||
if (snappedPoint && hoverDetected) {
|
||||
const dist = dwellPos.current.distanceTo(snappedPoint);
|
||||
if (dist > 0.005) {
|
||||
dwellPos.current.copy(snappedPoint);
|
||||
dwellStart.current = nowT;
|
||||
dwellFired.current = false;
|
||||
} else if (!dwellFired.current && nowT - dwellStart.current > 1000) {
|
||||
dwellFired.current = true;
|
||||
if (measureModeNow) {
|
||||
useModelStore.getState().registerHoverMeasurement({
|
||||
type: hoverDetected.kind,
|
||||
value: hoverDetected.value,
|
||||
position: [hoverDetected.position.x, hoverDetected.position.y, hoverDetected.position.z],
|
||||
endpoints: hoverDetected.endpoints && {
|
||||
a: { x: hoverDetected.endpoints.a.x, y: hoverDetected.endpoints.a.y, z: hoverDetected.endpoints.a.z },
|
||||
b: { x: hoverDetected.endpoints.b.x, y: hoverDetected.endpoints.b.y, z: hoverDetected.endpoints.b.z },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dwellFired.current = false;
|
||||
dwellStart.current = nowT;
|
||||
}
|
||||
|
||||
// ── Update laser visual ───────────────────────────────────────────
|
||||
if (laserRef.current && tipRef.current) {
|
||||
const end = snappedPoint ?? tmpOrigin.current.clone().add(tmpDir.current.clone().multiplyScalar(MAX_RAY));
|
||||
@@ -136,7 +199,7 @@ export function XRControllerMeasure() {
|
||||
positions.needsUpdate = true;
|
||||
laserGeom.current.computeBoundingSphere();
|
||||
|
||||
const color = snapKind === 'vertex' ? '#22c55e' : snapKind === 'edge' ? '#3b82f6' : '#eab308';
|
||||
const color = snapKind === 'hole' ? '#f59e0b' : snapKind === 'vertex' ? '#22c55e' : snapKind === 'edge' ? '#3b82f6' : '#eab308';
|
||||
tipColor.current.set(color);
|
||||
(laserRef.current.material as THREE.LineBasicMaterial).color.set(color);
|
||||
((tipRef.current.material as THREE.MeshBasicMaterial)).color.copy(tipColor.current);
|
||||
|
||||
+12
-6
@@ -296,13 +296,19 @@ function XRMeasurementOverlay() {
|
||||
const a: [number, number, number] = [m.pointA.x, m.pointA.y, m.pointA.z];
|
||||
const b: [number, number, number] = [m.pointB.x, m.pointB.y, m.pointB.z];
|
||||
const mid: [number, number, number] = [(a[0]+b[0])/2, (a[1]+b[1])/2, (a[2]+b[2])/2];
|
||||
const isHole = m.kind === 'hole';
|
||||
const color = isHole ? '#f59e0b' : '#3b82f6';
|
||||
const text = m.label ?? `${m.distanceMM.toFixed(1)} mm`;
|
||||
return (
|
||||
<group key={m.id}>
|
||||
<mesh position={a}><sphereGeometry args={[0.003, 16, 16]} /><meshBasicMaterial color="#22c55e" /></mesh>
|
||||
<mesh position={b}><sphereGeometry args={[0.003, 16, 16]} /><meshBasicMaterial color="#22c55e" /></mesh>
|
||||
<Line points={[a, b]} color="#22c55e" lineWidth={2} />
|
||||
{!isHole && (
|
||||
<>
|
||||
<mesh position={a}><sphereGeometry args={[0.003, 16, 16]} /><meshBasicMaterial color="#22c55e" /></mesh>
|
||||
<mesh position={b}><sphereGeometry args={[0.003, 16, 16]} /><meshBasicMaterial color="#22c55e" /></mesh>
|
||||
<Line points={[a, b]} color="#22c55e" lineWidth={2} />
|
||||
</>
|
||||
)}
|
||||
<group position={mid}>
|
||||
{/* Background plate */}
|
||||
<mesh position={[0, 0, 0]} renderOrder={998}>
|
||||
<planeGeometry args={[0.09, 0.024]} />
|
||||
<meshBasicMaterial color="#0b1220" transparent opacity={0.85} depthTest={false} />
|
||||
@@ -310,13 +316,13 @@ function XRMeasurementOverlay() {
|
||||
<Text
|
||||
position={[0, 0, 0.001]}
|
||||
fontSize={0.012}
|
||||
color="#3b82f6"
|
||||
color={color}
|
||||
anchorX="center"
|
||||
anchorY="middle"
|
||||
renderOrder={999}
|
||||
material-depthTest={false}
|
||||
>
|
||||
{`${m.distanceMM.toFixed(1)} mm`}
|
||||
{text}
|
||||
</Text>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
Reference in New Issue
Block a user