Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -243,7 +243,7 @@ export function findNearestEdgeSegment(
|
|||||||
camera: THREE.Camera,
|
camera: THREE.Camera,
|
||||||
canvasSize: { width: number; height: number },
|
canvasSize: { width: number; height: number },
|
||||||
thresholdPx: number = 12
|
thresholdPx: number = 12
|
||||||
): { midpoint: THREE.Vector3; lengthMM: number } | null {
|
): { midpoint: THREE.Vector3; lengthMM: number; a: THREE.Vector3; b: THREE.Vector3 } | null {
|
||||||
// Look for edge line segments children
|
// Look for edge line segments children
|
||||||
let edgeLines: THREE.LineSegments | null = null;
|
let edgeLines: THREE.LineSegments | null = null;
|
||||||
mesh.children.forEach(c => {
|
mesh.children.forEach(c => {
|
||||||
@@ -263,9 +263,11 @@ export function findNearestEdgeSegment(
|
|||||||
const hitY = (-hitScreen.y * 0.5 + 0.5) * canvasSize.height;
|
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.clone().premultiply(mesh.matrixWorld) : mesh.matrixWorld;
|
||||||
|
|
||||||
let bestDist = Infinity;
|
let bestDist = Infinity;
|
||||||
let bestMid: THREE.Vector3 | null = null;
|
let bestMid: THREE.Vector3 | null = null;
|
||||||
|
let bestA: THREE.Vector3 | null = null;
|
||||||
|
let bestB: THREE.Vector3 | null = null;
|
||||||
let bestLen = 0;
|
let bestLen = 0;
|
||||||
|
|
||||||
const segCount = posAttr.count / 2;
|
const segCount = posAttr.count / 2;
|
||||||
@@ -278,7 +280,6 @@ export function findNearestEdgeSegment(
|
|||||||
a.fromBufferAttribute(posAttr, i * 2).applyMatrix4(matrix);
|
a.fromBufferAttribute(posAttr, i * 2).applyMatrix4(matrix);
|
||||||
b.fromBufferAttribute(posAttr, i * 2 + 1).applyMatrix4(matrix);
|
b.fromBufferAttribute(posAttr, i * 2 + 1).applyMatrix4(matrix);
|
||||||
|
|
||||||
// Project to screen
|
|
||||||
pa.copy(a).project(camera);
|
pa.copy(a).project(camera);
|
||||||
pb.copy(b).project(camera);
|
pb.copy(b).project(camera);
|
||||||
const ax = (pa.x * 0.5 + 0.5) * canvasSize.width;
|
const ax = (pa.x * 0.5 + 0.5) * canvasSize.width;
|
||||||
@@ -286,24 +287,112 @@ export function findNearestEdgeSegment(
|
|||||||
const bx = (pb.x * 0.5 + 0.5) * canvasSize.width;
|
const bx = (pb.x * 0.5 + 0.5) * canvasSize.width;
|
||||||
const by = (-pb.y * 0.5 + 0.5) * canvasSize.height;
|
const by = (-pb.y * 0.5 + 0.5) * canvasSize.height;
|
||||||
|
|
||||||
// Distance from point to line segment in screen space
|
|
||||||
const dist = pointToSegmentDist(hitX, hitY, ax, ay, bx, by);
|
const dist = pointToSegmentDist(hitX, hitY, ax, ay, bx, by);
|
||||||
|
|
||||||
if (dist < bestDist) {
|
if (dist < bestDist) {
|
||||||
bestDist = dist;
|
bestDist = dist;
|
||||||
bestMid = a.clone().add(b).multiplyScalar(0.5);
|
bestMid = a.clone().add(b).multiplyScalar(0.5);
|
||||||
|
bestA = a.clone();
|
||||||
|
bestB = b.clone();
|
||||||
bestLen = a.distanceTo(b) * 1000; // mm
|
bestLen = a.distanceTo(b) * 1000; // mm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!edgeLines) geo.dispose();
|
if (!edgeLines) geo.dispose();
|
||||||
|
|
||||||
if (bestDist <= thresholdPx && bestMid && bestLen > 0.5) {
|
if (bestDist <= thresholdPx && bestMid && bestA && bestB && bestLen > 0.5) {
|
||||||
return { midpoint: bestMid, lengthMM: bestLen };
|
return { midpoint: bestMid, lengthMM: bestLen, a: bestA, b: bestB };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect a circular hole by fitting a circle through edge vertices that lie
|
||||||
|
* within `radiusPx` (screen space) around `worldPoint`. Returns center + diameter.
|
||||||
|
*/
|
||||||
|
export function detectCircularEdgeAtPoint(
|
||||||
|
mesh: THREE.Mesh,
|
||||||
|
worldPoint: THREE.Vector3,
|
||||||
|
camera: THREE.Camera,
|
||||||
|
canvasSize: { width: number; height: number },
|
||||||
|
radiusPx: number = 60
|
||||||
|
): { 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 hitScreen = worldPoint.clone().project(camera);
|
||||||
|
const hitX = (hitScreen.x * 0.5 + 0.5) * canvasSize.width;
|
||||||
|
const hitY = (-hitScreen.y * 0.5 + 0.5) * canvasSize.height;
|
||||||
|
|
||||||
|
const verts: THREE.Vector3[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const tmp = new THREE.Vector3();
|
||||||
|
const proj = new THREE.Vector3();
|
||||||
|
const count = posAttr.count;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
tmp.fromBufferAttribute(posAttr, i).applyMatrix4(matrix);
|
||||||
|
proj.copy(tmp).project(camera);
|
||||||
|
const sx = (proj.x * 0.5 + 0.5) * canvasSize.width;
|
||||||
|
const sy = (-proj.y * 0.5 + 0.5) * canvasSize.height;
|
||||||
|
if (Math.hypot(sx - hitX, sy - hitY) > radiusPx) 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());
|
||||||
|
}
|
||||||
|
if (!edgeLines) geo.dispose();
|
||||||
|
|
||||||
|
if (verts.length < 6) return null;
|
||||||
|
|
||||||
|
// Compute centroid + best-fit plane normal via covariance
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
// Normal = eigenvector with smallest eigenvalue. Approximate using cross of two
|
||||||
|
// axes with largest variance.
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Build basis on plane
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
function pointToSegmentDist(px: number, py: number, ax: number, ay: number, bx: number, by: number): number {
|
function pointToSegmentDist(px: number, py: number, ax: number, ay: number, bx: number, by: number): number {
|
||||||
const dx = bx - ax;
|
const dx = bx - ax;
|
||||||
const dy = by - ay;
|
const dy = by - ay;
|
||||||
|
|||||||
Reference in New Issue
Block a user