Merge pull request '[VALLEY-SKIP] isValleyVertex 노치 top_in 잘못된 eaveHelpLine 4개 차단 — zero-length neighbor + collinear continuation 2단 가드' (#840) from dev into dev-deploy

Reviewed-on: #840
This commit is contained in:
ysCha 2026-05-13 16:44:45 +09:00
commit d0ae14967c

View File

@ -5153,6 +5153,12 @@ function isValleyVertex(targetPoint, connectedLine, allLines, isStartVertex) {
let neighborLine = null;
// [valley degenerate skip 2026-05-13]
// SHOULDER_ABSORBED 로 인접 baseLine 이 zero-length 가 되면 (p1==p2)
// cross product 가 노이즈 작은 값이 되어 valley 판정이 우연에 좌우됨
// (notch 케이스: cross=59.15 → valley=true 오판 → eaveHelpLine 4개 잘못 생성).
// length < 1.0 (관측치 0.10, 0.50) 이면 skip 하고 그 너머 진짜 인접 라인 탐색.
const DEGEN_LEN = 1.0;
if (isStartVertex) {
neighborLine = allLines.find(l => {
if (l === connectedLine) return false;
@ -5160,6 +5166,7 @@ function isValleyVertex(targetPoint, connectedLine, allLines, isStartVertex) {
const ly1 = l.y1 ?? l.get?.('y1');
const lx2 = l.x2 ?? l.get?.('x2');
const ly2 = l.y2 ?? l.get?.('y2');
if (Math.hypot(lx2 - lx1, ly2 - ly1) < DEGEN_LEN) return false; // [valley degenerate skip 2026-05-13]
const end = l.endPoint || { x: lx2, y: ly2 };
return isSamePoint(end, targetPoint, tolerance);
});
@ -5170,6 +5177,7 @@ function isValleyVertex(targetPoint, connectedLine, allLines, isStartVertex) {
const ly1 = l.y1 ?? l.get?.('y1');
const lx2 = l.x2 ?? l.get?.('x2');
const ly2 = l.y2 ?? l.get?.('y2');
if (Math.hypot(lx2 - lx1, ly2 - ly1) < DEGEN_LEN) return false; // [valley degenerate skip 2026-05-13]
const start = l.startPoint || { x: lx1, y: ly1 };
return isSamePoint(start, targetPoint, tolerance);
});
@ -5200,7 +5208,29 @@ function isValleyVertex(targetPoint, connectedLine, allLines, isStartVertex) {
}
const crossProduct = getTurnDirection(p1, p2, p3);
logger.log('crossProduct:', crossProduct);
// [collinear continuation 2026-05-13]
// neigh→target→conn 이 거의 직선이면 (노치 꼬리/연속 segment)
// drift (round 0.1) 로 cross 가 미세한 양/음수 노이즈 → valley 오판.
// |cross| / (|v1|*|v2|) = |sin(theta)|. < 0.05 (≈ 2.9°) 면 직선 연속으로 간주.
const v1len = Math.hypot(p2.x - p1.x, p2.y - p1.y);
const v2len = Math.hypot(p3.x - p2.x, p3.y - p2.y);
let collinearSkip = false;
if (v1len > 0 && v2len > 0) {
const sinTheta = Math.abs(crossProduct) / (v1len * v2len);
if (sinTheta < 0.05) collinearSkip = true;
}
// [valley diag 2026-05-13] L자(정상) vs notch(E-1~E-4 발생) 비교용
const fmt = (p) => `(${Math.round(p.x)},${Math.round(p.y)})`;
const cLen = Math.hypot(clx2 - clx1, cly2 - cly1);
const nLen = Math.hypot(nlx2 - nlx1, nly2 - nly1);
logger.log(
`[VALLEY] ${isStartVertex ? 'START' : 'END'} target=${fmt(targetPoint)} ` +
`conn=${fmt({x:clx1,y:cly1})}→${fmt({x:clx2,y:cly2})}[len=${cLen.toFixed(2)}] ` +
`neigh=${fmt({x:nlx1,y:nly1})}→${fmt({x:nlx2,y:nly2})}[len=${nLen.toFixed(2)}] ` +
`p1=${fmt(p1)} p2=${fmt(p2)} p3=${fmt(p3)} cross=${crossProduct.toFixed(2)} ` +
`valley=${collinearSkip ? false : (crossProduct > 0)}${collinearSkip ? ' (collinear-skip)' : ''}`
);
if (collinearSkip) return false;
return crossProduct > 0;
}