diff --git a/src/components/three/SmartMeasure.ts b/src/components/three/SmartMeasure.ts
index fda52d5..90d7fa5 100644
--- a/src/components/three/SmartMeasure.ts
+++ b/src/components/three/SmartMeasure.ts
@@ -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' };
}
diff --git a/src/components/three/XRControllerMeasure.tsx b/src/components/three/XRControllerMeasure.tsx
index de52e40..ffb0dff 100644
--- a/src/components/three/XRControllerMeasure.tsx
+++ b/src/components/three/XRControllerMeasure.tsx
@@ -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) {
diff --git a/src/lib/modelTransforms.ts b/src/lib/modelTransforms.ts
index 58d8807..c3ef211 100644
--- a/src/lib/modelTransforms.ts
+++ b/src/lib/modelTransforms.ts
@@ -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;
+}
diff --git a/src/pages/XRSession.tsx b/src/pages/XRSession.tsx
index cc59e3d..6448e09 100644
--- a/src/pages/XRSession.tsx
+++ b/src/pages/XRSession.tsx
@@ -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 (
-
-
-
- );
+ return ;
}
// Componente para renderizar medições que pertencem a um modelo específico
diff --git a/src/stores/useModelStore.ts b/src/stores/useModelStore.ts
index e07045f..a7e1a41 100644
--- a/src/stores/useModelStore.ts
+++ b/src/stores/useModelStore.ts
@@ -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((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((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((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((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,
};
}