🚀 Auto-deploy: melhoria no snap e medição AR em 22/05/2026 14:53:05
This commit is contained in:
@@ -424,3 +424,206 @@ export function resolveSnap(
|
||||
if (e) return { point: e.midpoint, type: 'edge' };
|
||||
return { point: hitPoint.clone(), type: 'surface' };
|
||||
}
|
||||
|
||||
const _ab = new THREE.Vector3();
|
||||
const _ap = new THREE.Vector3();
|
||||
const _closest = new THREE.Vector3();
|
||||
|
||||
function pointToSegmentDistSq3D(p: THREE.Vector3, a: THREE.Vector3, b: THREE.Vector3): number {
|
||||
_ab.subVectors(b, a);
|
||||
_ap.subVectors(p, a);
|
||||
const abLenSq = _ab.lengthSq();
|
||||
if (abLenSq < 1e-6) return _ap.lengthSq();
|
||||
|
||||
let t = _ap.dot(_ab) / abLenSq;
|
||||
t = Math.max(0, Math.min(1, t));
|
||||
_closest.copy(a).addScaledVector(_ab, t);
|
||||
return p.distanceToSquared(_closest);
|
||||
}
|
||||
|
||||
export function findNearestVertex3D(
|
||||
mesh: THREE.Mesh,
|
||||
worldPoint: THREE.Vector3,
|
||||
thresholdMeters: number = 0.035
|
||||
): THREE.Vector3 | null {
|
||||
const geo = mesh.geometry;
|
||||
const posAttr = geo.attributes.position;
|
||||
if (!posAttr) return null;
|
||||
|
||||
const invMatrix = new THREE.Matrix4().copy(mesh.matrixWorld).invert();
|
||||
const localPoint = worldPoint.clone().applyMatrix4(invMatrix);
|
||||
|
||||
let bestDistSq = Infinity;
|
||||
let bestIndex = -1;
|
||||
const thresholdSq = thresholdMeters * thresholdMeters;
|
||||
|
||||
for (let i = 0; i < posAttr.count; i++) {
|
||||
const vx = posAttr.getX(i);
|
||||
const vy = posAttr.getY(i);
|
||||
const vz = posAttr.getZ(i);
|
||||
|
||||
const dx = vx - localPoint.x;
|
||||
const dy = vy - localPoint.y;
|
||||
const dz = vz - localPoint.z;
|
||||
const distSq = dx * dx + dy * dy + dz * dz;
|
||||
|
||||
if (distSq < bestDistSq) {
|
||||
bestDistSq = distSq;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestIndex !== -1 && bestDistSq <= thresholdSq) {
|
||||
const result = new THREE.Vector3(
|
||||
posAttr.getX(bestIndex),
|
||||
posAttr.getY(bestIndex),
|
||||
posAttr.getZ(bestIndex)
|
||||
);
|
||||
result.applyMatrix4(mesh.matrixWorld);
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findNearestEdgeSegment3D(
|
||||
mesh: THREE.Mesh,
|
||||
worldPoint: THREE.Vector3,
|
||||
thresholdMeters: number = 0.05
|
||||
): { midpoint: THREE.Vector3; lengthMM: number; a: THREE.Vector3; b: THREE.Vector3 } | null {
|
||||
let edgeLines: THREE.LineSegments | null = null;
|
||||
mesh.children.forEach(c => {
|
||||
if (c.userData.__edgeLine && c instanceof THREE.LineSegments) {
|
||||
edgeLines = c;
|
||||
}
|
||||
});
|
||||
|
||||
const geo = edgeLines?.geometry ?? new THREE.EdgesGeometry(mesh.geometry, 15);
|
||||
const posAttr = geo.attributes.position;
|
||||
if (!posAttr) return null;
|
||||
|
||||
const matrix = edgeLines ? edgeLines.matrixWorld.clone().premultiply(mesh.matrixWorld) : mesh.matrixWorld;
|
||||
const invMatrix = new THREE.Matrix4().copy(matrix).invert();
|
||||
const localPoint = worldPoint.clone().applyMatrix4(invMatrix);
|
||||
|
||||
let bestDistSq = Infinity;
|
||||
let bestIndex = -1;
|
||||
const thresholdSq = thresholdMeters * thresholdMeters;
|
||||
|
||||
const segCount = posAttr.count / 2;
|
||||
const aLocal = new THREE.Vector3();
|
||||
const bLocal = new THREE.Vector3();
|
||||
|
||||
for (let i = 0; i < segCount; i++) {
|
||||
aLocal.fromBufferAttribute(posAttr, i * 2);
|
||||
bLocal.fromBufferAttribute(posAttr, i * 2 + 1);
|
||||
|
||||
const distSq = pointToSegmentDistSq3D(localPoint, aLocal, bLocal);
|
||||
|
||||
if (distSq < bestDistSq) {
|
||||
bestDistSq = distSq;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (!edgeLines) geo.dispose();
|
||||
|
||||
if (bestIndex !== -1 && bestDistSq <= thresholdSq) {
|
||||
const a = new THREE.Vector3().fromBufferAttribute(posAttr, bestIndex * 2).applyMatrix4(matrix);
|
||||
const b = new THREE.Vector3().fromBufferAttribute(posAttr, bestIndex * 2 + 1).applyMatrix4(matrix);
|
||||
const midpoint = a.clone().add(b).multiplyScalar(0.5);
|
||||
const lengthMM = a.distanceTo(b) * 1000;
|
||||
return { midpoint, lengthMM, a, b };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function detectCircularEdgeAtPoint3D(
|
||||
mesh: THREE.Mesh,
|
||||
worldPoint: THREE.Vector3,
|
||||
radiusMeters: number = 0.08
|
||||
): { center: THREE.Vector3; diameterMM: number } | null {
|
||||
let edgeLines: THREE.LineSegments | null = null;
|
||||
mesh.children.forEach(c => {
|
||||
if (c.userData.__edgeLine && c instanceof THREE.LineSegments) edgeLines = c;
|
||||
});
|
||||
const geo = edgeLines?.geometry ?? new THREE.EdgesGeometry(mesh.geometry, 15);
|
||||
const posAttr = geo.attributes.position;
|
||||
if (!posAttr) return null;
|
||||
|
||||
const matrix = edgeLines ? edgeLines.matrixWorld.clone().premultiply(mesh.matrixWorld) : mesh.matrixWorld;
|
||||
const invMatrix = new THREE.Matrix4().copy(matrix).invert();
|
||||
const localPoint = worldPoint.clone().applyMatrix4(invMatrix);
|
||||
|
||||
const verts: THREE.Vector3[] = [];
|
||||
const seen = new Set<string>();
|
||||
const tmp = new THREE.Vector3();
|
||||
const radiusSq = radiusMeters * radiusMeters;
|
||||
const count = posAttr.count;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
tmp.fromBufferAttribute(posAttr, i);
|
||||
const dx = tmp.x - localPoint.x;
|
||||
const dy = tmp.y - localPoint.y;
|
||||
const dz = tmp.z - localPoint.z;
|
||||
const distSq = dx * dx + dy * dy + dz * dz;
|
||||
|
||||
if (distSq > radiusSq) continue;
|
||||
const key = `${tmp.x.toFixed(5)},${tmp.y.toFixed(5)},${tmp.z.toFixed(5)}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
verts.push(tmp.clone().applyMatrix4(matrix));
|
||||
}
|
||||
if (!edgeLines) geo.dispose();
|
||||
|
||||
if (verts.length < 6) return null;
|
||||
|
||||
const centroid = new THREE.Vector3();
|
||||
verts.forEach(v => centroid.add(v));
|
||||
centroid.divideScalar(verts.length);
|
||||
|
||||
let xx = 0, xy = 0, xz = 0, yy = 0, yz = 0, zz = 0;
|
||||
for (const v of verts) {
|
||||
const dx = v.x - centroid.x, dy = v.y - centroid.y, dz = v.z - centroid.z;
|
||||
xx += dx * dx; xy += dx * dy; xz += dx * dz;
|
||||
yy += dy * dy; yz += dy * dz; zz += dz * dz;
|
||||
}
|
||||
const axisA = new THREE.Vector3(xx, xy, xz).normalize();
|
||||
const axisB = new THREE.Vector3(xy, yy, yz).normalize();
|
||||
const normal = new THREE.Vector3().crossVectors(axisA, axisB);
|
||||
if (normal.lengthSq() < 1e-8) return null;
|
||||
normal.normalize();
|
||||
|
||||
const basisU = new THREE.Vector3();
|
||||
if (Math.abs(normal.x) < 0.9) basisU.crossVectors(normal, new THREE.Vector3(1, 0, 0)).normalize();
|
||||
else basisU.crossVectors(normal, new THREE.Vector3(0, 1, 0)).normalize();
|
||||
const basisV = new THREE.Vector3().crossVectors(normal, basisU).normalize();
|
||||
|
||||
const points2D = verts.map(v => {
|
||||
const rel = v.clone().sub(centroid);
|
||||
return { u: rel.dot(basisU), v: rel.dot(basisV) };
|
||||
});
|
||||
const fit = circleFitKasa(points2D);
|
||||
if (!fit) return null;
|
||||
|
||||
const diameterMM = fit.radius * 2 * 1000;
|
||||
if (diameterMM < 2 || diameterMM > 500) return null;
|
||||
|
||||
const center3D = centroid.clone()
|
||||
.add(basisU.clone().multiplyScalar(fit.cx))
|
||||
.add(basisV.clone().multiplyScalar(fit.cy));
|
||||
|
||||
return { center: center3D, diameterMM };
|
||||
}
|
||||
|
||||
export function resolveSnap3D(
|
||||
mesh: THREE.Mesh,
|
||||
hitPoint: THREE.Vector3,
|
||||
vertexThresholdMeters: number = 0.035,
|
||||
edgeThresholdMeters: number = 0.05
|
||||
): { point: THREE.Vector3; type: 'vertex' | 'edge' | 'surface' } {
|
||||
const v = findNearestVertex3D(mesh, hitPoint, vertexThresholdMeters);
|
||||
if (v) return { point: v, type: 'vertex' };
|
||||
const e = findNearestEdgeSegment3D(mesh, hitPoint, edgeThresholdMeters);
|
||||
if (e) return { point: e.midpoint, type: 'edge' };
|
||||
return { point: hitPoint.clone(), type: 'surface' };
|
||||
}
|
||||
|
||||
@@ -2,7 +2,14 @@ import { useRef, useState } from 'react';
|
||||
import { useFrame, useThree } from '@react-three/fiber';
|
||||
import * as THREE from 'three';
|
||||
import { useModelStore } from '@/stores/useModelStore';
|
||||
import { resolveSnap, detectCircularEdgeAtPoint, findNearestEdgeSegment } from './SmartMeasure';
|
||||
import {
|
||||
resolveSnap,
|
||||
detectCircularEdgeAtPoint,
|
||||
findNearestEdgeSegment,
|
||||
resolveSnap3D,
|
||||
detectCircularEdgeAtPoint3D,
|
||||
findNearestEdgeSegment3D
|
||||
} from './SmartMeasure';
|
||||
|
||||
const TRIG_ON = 0.7;
|
||||
const TRIG_OFF = 0.3;
|
||||
@@ -121,48 +128,37 @@ export function XRControllerMeasure() {
|
||||
});
|
||||
|
||||
if (hit && hit.object instanceof THREE.Mesh) {
|
||||
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)
|
||||
// Snap to existing registered hole centers first (within ~4cm)
|
||||
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;
|
||||
let bestHoleDist = Infinity;
|
||||
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; }
|
||||
const d = hit.point.distanceTo(c);
|
||||
if (d < 0.04 && d < bestHoleDist) {
|
||||
bestHoleDist = d;
|
||||
bestHoleCenter = c;
|
||||
}
|
||||
}
|
||||
|
||||
if (snapEnabled && bestHoleCenter) {
|
||||
snappedPoint = bestHoleCenter;
|
||||
snapKind = 'hole';
|
||||
} else if (snapEnabled) {
|
||||
// Try detecting a circular edge (hole) at hit — radius mais generoso
|
||||
const circle = detectCircularEdgeAtPoint(hit.object, hit.point, fakeCam, canvasSize, 90);
|
||||
// Try detecting a circular edge (hole) at hit (8cm radius)
|
||||
const circle = detectCircularEdgeAtPoint3D(hit.object, hit.point, 0.08);
|
||||
if (circle) {
|
||||
snappedPoint = circle.center;
|
||||
snapKind = 'hole';
|
||||
hoverDetected = { kind: 'hole', value: circle.diameterMM, position: circle.center };
|
||||
} else {
|
||||
// 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);
|
||||
// Snap 3D físico puro (3.5cm para vértices, 5cm para arestas)
|
||||
const snap = resolveSnap3D(hit.object, hit.point, 0.035, 0.05);
|
||||
snappedPoint = snap.point;
|
||||
snapKind = snap.type;
|
||||
if (snap.type === 'edge') {
|
||||
const seg = findNearestEdgeSegment(hit.object, hit.point, fakeCam, canvasSize, 48);
|
||||
const seg = findNearestEdgeSegment3D(hit.object, hit.point, 0.05);
|
||||
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 } };
|
||||
|
||||
@@ -561,11 +561,9 @@ const XRSession = () => {
|
||||
gl={{ antialias: true, alpha: true, powerPreference: 'high-performance', preserveDrawingBuffer: true }}
|
||||
camera={{ position: [1.5, 1.2, 1.5], fov: 50, near: 0.01, far: 100 }}
|
||||
className="!bg-transparent"
|
||||
onCreated={({ gl, invalidate }) => {
|
||||
onCreated={({ gl }) => {
|
||||
gl.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
|
||||
gl.setClearColor(0x000000, 0);
|
||||
const loop = () => { invalidate(); requestAnimationFrame(loop); };
|
||||
loop();
|
||||
}}
|
||||
>
|
||||
<XR store={store}>
|
||||
|
||||
Reference in New Issue
Block a user