🚀 Auto-deploy: melhoria no snap e medição AR em 22/05/2026 14:53:05

This commit is contained in:
2026-05-22 14:53:05 +00:00
parent bd9d0028f1
commit 69b001f316
5 changed files with 925 additions and 27 deletions
+203
View File
@@ -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' };
}
+20 -24
View File
@@ -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 } };
+1 -3
View File
@@ -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}>