Ajustou cota e estabilizou snap

X-Lovable-Edit-ID: edt-c6d88e59-2709-4850-9df5-a42a0e7fce71
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-24 19:17:50 +00:00
5 changed files with 118 additions and 40 deletions
+22 -8
View File
@@ -270,7 +270,7 @@ export function findNearestEdgeSegment(
const hitX = (hitScreen.x * 0.5 + 0.5) * canvasSize.width;
const hitY = (-hitScreen.y * 0.5 + 0.5) * canvasSize.height;
const matrix = edgeLines ? edgeLines.matrixWorld.clone().premultiply(mesh.matrixWorld) : mesh.matrixWorld;
const matrix = edgeLines ? edgeLines.matrixWorld : mesh.matrixWorld;
let bestDist = Infinity;
let bestMid: THREE.Vector3 | null = null;
@@ -340,7 +340,7 @@ export function detectCircularEdgeAtPoint(
const posAttr = geo.attributes.position;
if (!posAttr) return null;
const matrix = edgeLines ? edgeLines.matrixWorld.clone().premultiply(mesh.matrixWorld) : mesh.matrixWorld;
const matrix = edgeLines ? edgeLines.matrixWorld : mesh.matrixWorld;
const hitScreen = worldPoint.clone().project(camera);
const hitX = (hitScreen.x * 0.5 + 0.5) * canvasSize.width;
@@ -443,6 +443,16 @@ const _ab = new THREE.Vector3();
const _ap = new THREE.Vector3();
const _closest = new THREE.Vector3();
function closestPointOnSegment3D(p: THREE.Vector3, a: THREE.Vector3, b: THREE.Vector3, target: THREE.Vector3): THREE.Vector3 {
_ab.subVectors(b, a);
_ap.subVectors(p, a);
const abLenSq = _ab.lengthSq();
if (abLenSq < 1e-6) return target.copy(a);
const t = THREE.MathUtils.clamp(_ap.dot(_ab) / abLenSq, 0, 1);
return target.copy(a).addScaledVector(_ab, t);
}
function pointToSegmentDistSq3D(p: THREE.Vector3, a: THREE.Vector3, b: THREE.Vector3): number {
_ab.subVectors(b, a);
_ap.subVectors(p, a);
@@ -503,7 +513,7 @@ 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 {
): { midpoint: THREE.Vector3; snapPoint: 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) {
@@ -523,12 +533,13 @@ export function findNearestEdgeSegment3D(
const posAttr = geo.attributes.position;
if (!posAttr) return null;
const matrix = edgeLines ? edgeLines.matrixWorld.clone().premultiply(mesh.matrixWorld) : mesh.matrixWorld;
const matrix = edgeLines ? edgeLines.matrixWorld : mesh.matrixWorld;
const invMatrix = new THREE.Matrix4().copy(matrix).invert();
const localPoint = worldPoint.clone().applyMatrix4(invMatrix);
let bestDistSq = Infinity;
let bestIndex = -1;
const bestClosestLocal = new THREE.Vector3();
const thresholdSq = thresholdMeters * thresholdMeters;
const segCount = posAttr.count / 2;
@@ -539,11 +550,13 @@ export function findNearestEdgeSegment3D(
aLocal.fromBufferAttribute(posAttr, i * 2);
bLocal.fromBufferAttribute(posAttr, i * 2 + 1);
const distSq = pointToSegmentDistSq3D(localPoint, aLocal, bLocal);
closestPointOnSegment3D(localPoint, aLocal, bLocal, _closest);
const distSq = localPoint.distanceToSquared(_closest);
if (distSq < bestDistSq) {
bestDistSq = distSq;
bestIndex = i;
bestClosestLocal.copy(_closest);
}
}
@@ -551,8 +564,9 @@ export function findNearestEdgeSegment3D(
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 snapPoint = bestClosestLocal.clone().applyMatrix4(matrix);
const lengthMM = a.distanceTo(b) * 1000;
return { midpoint, lengthMM, a, b };
return { midpoint, snapPoint, lengthMM, a, b };
}
return null;
}
@@ -579,7 +593,7 @@ export function detectCircularEdgeAtPoint3D(
const posAttr = geo.attributes.position;
if (!posAttr) return null;
const matrix = edgeLines ? edgeLines.matrixWorld.clone().premultiply(mesh.matrixWorld) : mesh.matrixWorld;
const matrix = edgeLines ? edgeLines.matrixWorld : mesh.matrixWorld;
const invMatrix = new THREE.Matrix4().copy(matrix).invert();
const localPoint = worldPoint.clone().applyMatrix4(invMatrix);
@@ -652,6 +666,6 @@ export function resolveSnap3D(
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' };
if (e) return { point: e.snapPoint, type: 'edge' };
return { point: hitPoint.clone(), type: 'surface' };
}
+41 -6
View File
@@ -26,6 +26,15 @@ function isPickableModelMesh(o: THREE.Object3D): boolean {
return false;
}
function getModelIdFromObject(o: THREE.Object3D): string | undefined {
let cur: THREE.Object3D | null = o;
while (cur) {
if (cur.userData?.modelId) return cur.userData.modelId as string;
cur = cur.parent;
}
return undefined;
}
/**
* Right-controller trigger driven measurement for AR.
*
@@ -60,6 +69,7 @@ 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'));
const lockedSnap = useRef<{ point: THREE.Vector3; kind: 'vertex' | 'edge' | 'hole'; modelId?: string; lastSeen: number } | null>(null);
// Dwell detection for hover-based smart measurement (1 s)
const dwellPos = useRef(new THREE.Vector3(Infinity, Infinity, Infinity));
@@ -118,11 +128,14 @@ export function XRControllerMeasure() {
let snappedPoint: THREE.Vector3 | null = null;
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; modelId?: string; endpoints?: { a: THREE.Vector3; b: THREE.Vector3 } } | null = null;
let hitModelId: string | undefined;
const nowT = performance.now();
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 (!isPickableModelMesh(o)) return false;
if (o.userData.__edgeLine) return false;
if (o.geometry instanceof THREE.SphereGeometry) return false;
@@ -132,9 +145,10 @@ export function XRControllerMeasure() {
});
if (hit && hit.object instanceof THREE.Mesh) {
hitModelId = getModelIdFromObject(hit.object);
const dist = tmpOrigin.current.distanceTo(hit.point);
const vertexThreshold = Math.max(0.06, dist * 0.08);
const edgeThreshold = Math.max(0.08, dist * 0.10);
const vertexThreshold = Math.max(0.08, dist * 0.12);
const edgeThreshold = Math.max(0.10, dist * 0.14);
// Snap to existing registered hole centers first
const existing = useModelStore.getState().measurements;
@@ -158,7 +172,7 @@ export function XRControllerMeasure() {
if (circle) {
snappedPoint = circle.center;
snapKind = 'hole';
hoverDetected = { kind: 'hole', value: circle.diameterMM, position: circle.center };
hoverDetected = { kind: 'hole', value: circle.diameterMM, position: circle.center, modelId: hitModelId };
} else {
const snap = resolveSnap3D(hit.object, hit.point, vertexThreshold, edgeThreshold);
snappedPoint = snap.point;
@@ -167,7 +181,7 @@ export function XRControllerMeasure() {
const seg = findNearestEdgeSegment3D(hit.object, hit.point, edgeThreshold);
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 } };
hoverDetected = { kind: 'edge', value: lenMM, position: seg.midpoint, modelId: hitModelId, endpoints: { a: seg.a, b: seg.b } };
}
}
}
@@ -177,9 +191,27 @@ export function XRControllerMeasure() {
}
}
const strongSnapKind = snapKind === 'vertex' || snapKind === 'edge' || snapKind === 'hole' ? snapKind : null;
if (snappedPoint && strongSnapKind) {
const locked = lockedSnap.current;
const sameTarget = locked && locked.kind === strongSnapKind && locked.modelId === hitModelId;
if (sameTarget && locked.point.distanceTo(snappedPoint) < (snapKind === 'edge' ? 0.08 : 0.12)) {
if (snapKind === 'edge') locked.point.lerp(snappedPoint, 0.25);
snappedPoint = locked.point.clone();
locked.lastSeen = nowT;
} else {
lockedSnap.current = { point: snappedPoint.clone(), kind: strongSnapKind, modelId: hitModelId, lastSeen: nowT };
}
} else if (snappedPoint && lockedSnap.current && lockedSnap.current.point.distanceTo(snappedPoint) < 0.12 && nowT - lockedSnap.current.lastSeen < 600) {
snapKind = lockedSnap.current.kind;
snappedPoint = lockedSnap.current.point.clone();
lockedSnap.current.lastSeen = nowT;
} else if (!snappedPoint || (lockedSnap.current && nowT - lockedSnap.current.lastSeen > 600)) {
lockedSnap.current = null;
}
// ── 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) {
@@ -197,6 +229,7 @@ export function XRControllerMeasure() {
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 },
},
modelId: hoverDetected.modelId,
});
}
}
@@ -246,6 +279,7 @@ export function XRControllerMeasure() {
const triggerHits = raycaster.current.intersectObjects(scene.children, true);
const triggerHit = triggerHits.find((h) => {
const o = h.object;
if (!(o instanceof THREE.Mesh)) return false;
if (!isPickableModelMesh(o)) return false;
if (o.userData.__edgeLine) return false;
if (o.geometry instanceof THREE.SphereGeometry) return false;
@@ -273,6 +307,7 @@ export function XRControllerMeasure() {
sendRemoteLog('info', 'Trigger físico pressionado no AR com snapPoint ativo', snappedPoint);
st.addMeasurePoint({
x: snappedPoint.x, y: snappedPoint.y, z: snappedPoint.z,
modelId: hitModelId,
});
}
} else if (trigState.current && trigVal < TRIG_OFF) {
+22
View File
@@ -33,3 +33,25 @@ export function worldToModelLocal(
g.worldToLocal(v);
return { x: v.x, y: v.y, z: v.z };
}
export function modelLocalToWorld(
modelId: string | null | undefined,
p: { x: number; y: number; z: number },
): { x: number; y: number; z: number } | null {
const g = getModelLocalGroup(modelId);
if (!g) return null;
g.updateWorldMatrix(true, false);
const v = new THREE.Vector3(p.x, p.y, p.z);
g.localToWorld(v);
return { x: v.x, y: v.y, z: v.z };
}
export function getModelWorldScaleFactor(modelId: string | null | undefined): number {
const g = getModelLocalGroup(modelId);
if (!g) return 1;
g.updateWorldMatrix(true, false);
const s = new THREE.Vector3();
g.getWorldScale(s);
const factor = (Math.abs(s.x) + Math.abs(s.y) + Math.abs(s.z)) / 3;
return Number.isFinite(factor) && factor > 1e-6 ? factor : 1;
}
+5 -7
View File
@@ -383,15 +383,13 @@ export function XRSafeLine({ a, b }: { a: [number, number, number]; b: [number,
new THREE.Vector3(b[0], b[1], b[2])
], [a, b]);
const geometry = useMemo(() => {
return new THREE.BufferGeometry().setFromPoints(points);
const line = useMemo(() => {
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: '#22c55e' });
return new THREE.Line(geometry, material);
}, [points]);
return (
<line geometry={geometry}>
<lineBasicMaterial color="#22c55e" linewidth={2} />
</line>
);
return <primitive object={line} />;
}
// Componente para renderizar medições que pertencem a um modelo específico
+28 -19
View File
@@ -1,5 +1,5 @@
import { create } from 'zustand';
import { worldToModelLocal } from '@/lib/modelTransforms';
import { getModelWorldScaleFactor, worldToModelLocal } from '@/lib/modelTransforms';
import { sendRemoteLog } from '@/lib/remoteLogger';
// ── Persistência de placement (fineTuning + escala) e flags WebXR ─────
@@ -133,6 +133,8 @@ export interface MeasurePoint {
x: number;
y: number;
z: number;
/** Model hit by this point, used so saved dimensions attach to the right piece. */
modelId?: string;
}
export interface Measurement {
@@ -153,6 +155,7 @@ export interface HoverInfo {
type: 'hole' | 'edge';
position: [number, number, number];
value: number; // mm (diameter or length)
modelId?: string;
/** For edges: the two endpoints in world coords. */
endpoints?: { a: MeasurePoint; b: MeasurePoint };
}
@@ -558,27 +561,25 @@ export const useModelStore = create<ModelStore>((set, get) => ({
const points = [...state.measurePoints, p];
if (points.length === 2) {
const [a, b] = points;
const dx = (a.x - b.x) * 1000;
const dy = (a.y - b.y) * 1000;
const dz = (a.z - b.z) * 1000;
const distanceWorldMM = Math.sqrt(dx * dx + dy * dy + dz * dz);
const factor = state.scaleRatio?.factor ?? 1;
const distanceMM = distanceWorldMM / factor;
const modelId = state.activeModelId ?? undefined;
const modelId = (a.modelId && a.modelId === b.modelId ? a.modelId : state.activeModelId) ?? undefined;
const aConv = modelId ? worldToModelLocal(modelId, a) : null;
const bConv = modelId ? worldToModelLocal(modelId, b) : null;
const attached = !!(aConv && bConv);
const pa = attached ? aConv! : a;
const pb = attached ? bConv! : b;
const dx = (pa.x - pb.x) * 1000;
const dy = (pa.y - pb.y) * 1000;
const dz = (pa.z - pb.z) * 1000;
const distanceMM = Math.sqrt(dx * dx + dy * dy + dz * dz);
sendRemoteLog('info', `addMeasurePoint: calculando distância`, {
distanceWorldMM,
distanceMM,
factor,
modelId,
attached
});
const measurement: Measurement = {
id: `m_${Date.now()}`,
pointA: attached ? aConv! : a,
pointB: attached ? bConv! : b,
pointA: pa,
pointB: pb,
distanceMM,
modelId: attached ? modelId : undefined,
};
@@ -602,7 +603,8 @@ export const useModelStore = create<ModelStore>((set, get) => ({
return { measurements: state.measurements.slice(0, -1) };
}),
registerHoverMeasurement: (info) => set((state) => {
const modelId = state.activeModelId ?? undefined;
const modelId = info.modelId ?? state.activeModelId ?? undefined;
const modelScale = modelId ? getModelWorldScaleFactor(modelId) : 1;
const toLocal = (p: MeasurePoint): { point: MeasurePoint; attached: boolean } => {
const conv = modelId ? worldToModelLocal(modelId, p) : null;
return conv ? { point: conv, attached: true } : { point: p, attached: false };
@@ -613,13 +615,14 @@ export const useModelStore = create<ModelStore>((set, get) => ({
const cw: MeasurePoint = { x: info.position[0], y: info.position[1], z: info.position[2] };
const c = toLocal(cw);
attached = c.attached;
const valueMM = attached ? info.value / modelScale : info.value;
measurement = {
id: `m_${Date.now()}`,
pointA: c.point,
pointB: c.point,
distanceMM: info.value,
distanceMM: valueMM,
kind: 'hole',
label: `Ø ${info.value.toFixed(1)}`,
label: `Ø ${valueMM.toFixed(1)}`,
modelId: attached ? modelId : undefined,
};
} else {
@@ -628,13 +631,19 @@ export const useModelStore = create<ModelStore>((set, get) => ({
const a = toLocal(aw);
const b = toLocal(bw);
attached = a.attached && b.attached;
const pa = attached ? a.point : aw;
const pb = attached ? b.point : bw;
const dx = (pa.x - pb.x) * 1000;
const dy = (pa.y - pb.y) * 1000;
const dz = (pa.z - pb.z) * 1000;
const valueMM = Math.sqrt(dx * dx + dy * dy + dz * dz);
measurement = {
id: `m_${Date.now()}`,
pointA: attached ? a.point : aw,
pointB: attached ? b.point : bw,
distanceMM: info.value,
pointA: pa,
pointB: pb,
distanceMM: valueMM,
kind: 'edge',
label: `${info.value.toFixed(1)} mm`,
label: `${valueMM.toFixed(1)} mm`,
modelId: attached ? modelId : undefined,
};
}