Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-21 13:43:46 +00:00
parent a5ecf0daeb
commit 374bfb1871
+95 -6
View File
@@ -243,7 +243,7 @@ export function findNearestEdgeSegment(
camera: THREE.Camera,
canvasSize: { width: number; height: number },
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
let edgeLines: THREE.LineSegments | null = null;
mesh.children.forEach(c => {
@@ -263,9 +263,11 @@ export function findNearestEdgeSegment(
const hitY = (-hitScreen.y * 0.5 + 0.5) * canvasSize.height;
const matrix = edgeLines ? edgeLines.matrixWorld.clone().premultiply(mesh.matrixWorld) : mesh.matrixWorld;
let bestDist = Infinity;
let bestMid: THREE.Vector3 | null = null;
let bestA: THREE.Vector3 | null = null;
let bestB: THREE.Vector3 | null = null;
let bestLen = 0;
const segCount = posAttr.count / 2;
@@ -278,7 +280,6 @@ export function findNearestEdgeSegment(
a.fromBufferAttribute(posAttr, i * 2).applyMatrix4(matrix);
b.fromBufferAttribute(posAttr, i * 2 + 1).applyMatrix4(matrix);
// Project to screen
pa.copy(a).project(camera);
pb.copy(b).project(camera);
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 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);
if (dist < bestDist) {
bestDist = dist;
bestMid = a.clone().add(b).multiplyScalar(0.5);
bestA = a.clone();
bestB = b.clone();
bestLen = a.distanceTo(b) * 1000; // mm
}
}
if (!edgeLines) geo.dispose();
if (bestDist <= thresholdPx && bestMid && bestLen > 0.5) {
return { midpoint: bestMid, lengthMM: bestLen };
if (bestDist <= thresholdPx && bestMid && bestA && bestB && bestLen > 0.5) {
return { midpoint: bestMid, lengthMM: bestLen, a: bestA, b: bestB };
}
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 {
const dx = bx - ax;
const dy = by - ay;