diff --git a/src/hooks/roofcover/useEavesGableEdit.js b/src/hooks/roofcover/useEavesGableEdit.js index 77d3cd3e..d4fc2b7e 100644 --- a/src/hooks/roofcover/useEavesGableEdit.js +++ b/src/hooks/roofcover/useEavesGableEdit.js @@ -1507,48 +1507,78 @@ export function useEavesGableEdit(id) { if (!vr.source || !vr.source.startsWith('roofBase')) continue const segA = { x: vr.x1, y: vr.y1 } const segB = { x: vr.x2, y: vr.y2 } + // 사용자 규칙: "조건 다 필요 없고 확장하는 라인 절삭." + // trim 검사 ray = segA(corner) → wallEnd(polygon-wall). segB(대칭중앙) 은 segA-wallEnd 의 중점. + // ray 위에서 만나는 hip/ridge 의 진행방향 쪽 끝점(= u 투영값 큰 쪽) 을 ip 로 단축. + // collinear (valleyExt 와 평행+동일직선) 라인은 별도 처리 — segB 너머 끝점을 segB 로 단축. + const wallEnd = { x: 2 * segB.x - segA.x, y: 2 * segB.y - segA.y } + const dxV = wallEnd.x - segA.x + const dyV = wallEnd.y - segA.y + const lenV = Math.hypot(dxV, dyV) || 1 + const ux = dxV / lenV + const uy = dyV / lenV + const tB = (segB.x - segA.x) * ux + (segB.y - segA.y) * uy + const COLLINEAR_TOL = 1.0 for (const il of roof.innerLines || []) { if (!il) continue if (il.lineName === 'kerabPatternValleyExt') continue - if (il.name !== LINE_TYPE.SUBLINE.HIP && il.name !== LINE_TYPE.SUBLINE.RIDGE) continue + // 사용자 규칙: "Y 모양일때 hip 있는 쪽은 살리고 마루쪽을 절삭." + "1522 도 삭제." + // trim 대상 = ridge(마루) + 케라바 토글 생성 hip(kerabPatternHip / kerabPatternExtHip). + // 원래 지붕 hip(H-5/H-6/H-7/H-1) 은 보존. hip-presence 룰이 자기 자신 제외하므로 안전. + const isTrimCandidate = + il.name === LINE_TYPE.SUBLINE.RIDGE || + (il.name === LINE_TYPE.SUBLINE.HIP && (il.lineName === 'kerabPatternHip' || il.lineName === 'kerabPatternExtHip')) + if (!isTrimCandidate) continue if (il.visible === false) continue const a = { x: il.x1, y: il.y1 } const b = { x: il.x2, y: il.y2 } - const ip = lineLineIntersection(segA, segB, a, b) - if (!ip) continue - if (!isOnSegV(ip, segA.x, segA.y, segB.x, segB.y)) continue - if (!isOnSegV(ip, a.x, a.y, b.x, b.y)) continue - // [KERAB-VALLEY-EXT-TRIM 2026-05-27] 끝점 연결 보존: hip/ridge 의 어느 한쪽 끝점이라도 - // 교차점과 일치(= 끝점에서 valleyExt 와 만남) → 살림. - const TOL_EP = 1.0 - if (Math.hypot(il.x1 - ip.x, il.y1 - ip.y) < TOL_EP) continue - if (Math.hypot(il.x2 - ip.x, il.y2 - ip.y) < TOL_EP) continue - // [KERAB-VALLEY-EXT-TRIM-Y-GUARD 2026-05-27] Y junction 보존: - // 사용자 규칙: "Y 모양이 있는 쪽은 살리고 그 반대쪽은 절삭." - // hip/ridge 의 양 끝점이 모두 다른 inner line 의 끝점과 일치(= 양쪽 다 Y junction) - // → 절삭 skip (= 정상 polygon 구조 보존). - // 한쪽만 junction → 자유 끝점(non-Y) 쪽 단축. - const TOL_J = 1.0 - const isOtherInnerEndAt = (px, py) => { + let ip = lineLineIntersection(segA, wallEnd, a, b) + if (ip) { + if (!isOnSegV(ip, segA.x, segA.y, wallEnd.x, wallEnd.y)) ip = null + else if (!isOnSegV(ip, a.x, a.y, b.x, b.y)) ip = null + } + if (!ip) { + const perp1 = Math.abs((il.x1 - segA.x) * uy - (il.y1 - segA.y) * ux) + const perp2 = Math.abs((il.x2 - segA.x) * uy - (il.y2 - segA.y) * ux) + if (perp1 > COLLINEAR_TOL || perp2 > COLLINEAR_TOL) continue + const tc1 = (il.x1 - segA.x) * ux + (il.y1 - segA.y) * uy + const tc2 = (il.x2 - segA.x) * ux + (il.y2 - segA.y) * uy + if (Math.max(tc1, tc2) <= tB + 0.5) continue + ip = { x: segB.x, y: segB.y } + } + // 사용자 규칙: "Y 모양일때 hip 있는 쪽은 살리고 마루(ridge) 쪽을 절삭." + // ridge 의 각 끝점에 *원래 지붕* hip 끝점이 붙어있는지 검사. + // 케라바 토글 생성 hip(kerabPatternHip/ExtHip) 은 골짜기 부속물이므로 hip-presence 카운트 제외. + // hip 있는 쪽 보존, 없는 쪽 단축. 양쪽 다 원래 hip → 통째 보존(continue). + // 양쪽 다 원래 hip 없음 → Y-junction 아닌 골짜기 부속물 → 통째 hide. + const isOriginalHipEndAt = (px, py) => { for (const other of roof.innerLines || []) { if (!other || other === il) continue if (other.lineName === 'kerabPatternValleyExt') continue - if (other.name !== LINE_TYPE.SUBLINE.HIP && other.name !== LINE_TYPE.SUBLINE.RIDGE) continue + if (other.lineName === 'kerabPatternHip' || other.lineName === 'kerabPatternExtHip') continue + if (other.name !== LINE_TYPE.SUBLINE.HIP) continue if (other.visible === false) continue - if (Math.hypot(other.x1 - px, other.y1 - py) < TOL_J) return true - if (Math.hypot(other.x2 - px, other.y2 - py) < TOL_J) return true + if (Math.hypot(other.x1 - px, other.y1 - py) < 1.0) return true + if (Math.hypot(other.x2 - px, other.y2 - py) < 1.0) return true } return false } - const end1J = isOtherInnerEndAt(il.x1, il.y1) - const end2J = isOtherInnerEndAt(il.x2, il.y2) - if (end1J && end2J) continue - const d1 = Math.hypot(il.x1 - segA.x, il.y1 - segA.y) - const d2 = Math.hypot(il.x2 - segA.x, il.y2 - segA.y) + const hip1 = isOriginalHipEndAt(il.x1, il.y1) + const hip2 = isOriginalHipEndAt(il.x2, il.y2) let trimEnd - if (end1J && !end2J) trimEnd = 2 - else if (!end1J && end2J) trimEnd = 1 - else trimEnd = d1 < d2 ? 1 : 2 + if (hip1 && hip2) { + logger.log('[KERAB-VALLEY-EXT-TRIM] both-hip preserve ' + JSON.stringify({ name: il.name, lineName: il.lineName })) + continue + } else if (hip1 && !hip2) { + trimEnd = 2 + } else if (!hip1 && hip2) { + trimEnd = 1 + } else { + // 양쪽 다 원래 hip 없음 — 진행벡터 fallback (cascade pass 에서 부속물 정리) + const t1 = (il.x1 - segA.x) * ux + (il.y1 - segA.y) * uy + const t2 = (il.x2 - segA.x) * ux + (il.y2 - segA.y) * uy + trimEnd = t1 > t2 ? 1 : 2 + } const oldX = trimEnd === 1 ? il.x1 : il.x2 const oldY = trimEnd === 1 ? il.y1 : il.y2 trimRecords.push({ @@ -1560,6 +1590,7 @@ export function useEavesGableEdit(id) { }) if (trimEnd === 1) il.set({ x1: ip.x, y1: ip.y }) else il.set({ x2: ip.x, y2: ip.y }) + if (typeof il.setCoords === 'function') il.setCoords() const newSz = calcLinePlaneSize({ x1: il.x1, y1: il.y1, x2: il.x2, y2: il.y2 }) il.attributes = { ...il.attributes, planeSize: newSz, actualSize: newSz } logger.log( @@ -1568,6 +1599,50 @@ export function useEavesGableEdit(id) { ) } } + // 사용자 규칙: "원 라인이 절삭되었으면 그 밑으로 확장된 라인도 삭제." + // cascade pass — trim 결과 옛 끝점(oldPt) 에 끝점이 일치하는 케라바산 라인(kerabPatternHip/ExtHip/Ridge) 을 hide. + // 가드: 원래 지붕 ridge/hip(lineName 없거나 kerabPattern* 아닌 것) 은 cascade 대상 외 — junction 공유 의도 외 절삭 방지. + const isKerabSynthetic = (line) => { + if (!line || !line.lineName) return false + return ( + line.lineName === 'kerabPatternHip' || + line.lineName === 'kerabPatternExtHip' || + line.lineName === 'kerabPatternRidge' || + line.lineName === 'kerabPatternExtRidge' + ) + } + const cascadeHidden = new Set() + // BFS — cascade hide 된 라인의 다른 끝점에 붙은 케라바산 라인도 재귀 hide. + const cascadeQueue = trimRecords.map((r) => r.oldPt).filter(Boolean) + let cascadeGuard = 0 + while (cascadeQueue.length && cascadeGuard++ < 200) { + const pt = cascadeQueue.shift() + if (!pt) continue + for (const il of roof.innerLines || []) { + if (!il || il.visible === false) continue + if (cascadeHidden.has(il)) continue + if (il.lineName === 'kerabPatternValleyExt') continue + if (!isKerabSynthetic(il)) continue + const m1 = Math.hypot(il.x1 - pt.x, il.y1 - pt.y) < 1.0 + const m2 = Math.hypot(il.x2 - pt.x, il.y2 - pt.y) < 1.0 + if (!m1 && !m2) continue + cascadeHidden.add(il) + trimRecords.push({ + line: il, + hide: true, + originalAttrs: { ...(il.attributes || {}) }, + originalVisible: il.visible !== false, + }) + il.visible = false + if (typeof il.setCoords === 'function') il.setCoords() + logger.log( + '[KERAB-VALLEY-EXT-TRIM] cascade hide ' + + JSON.stringify({ name: il.name, lineName: il.lineName, x1: il.x1, y1: il.y1, x2: il.x2, y2: il.y2 }), + ) + // 다른 끝점도 cascade 시드에 추가 + cascadeQueue.push(m1 ? { x: il.x2, y: il.y2 } : { x: il.x1, y: il.y1 }) + } + } if (trimRecords.length) target.__valleyExtTrims = trimRecords for (const vr of valleyExtensions) { @@ -2210,9 +2285,14 @@ export function useEavesGableEdit(id) { const rec = target.__valleyExtTrims[i] const il = rec.line if (!il) continue - if (rec.end === 1) il.set({ x1: rec.oldPt.x, y1: rec.oldPt.y }) - else il.set({ x2: rec.oldPt.x, y2: rec.oldPt.y }) + if (rec.hide) { + il.visible = rec.originalVisible !== false + } else { + if (rec.end === 1) il.set({ x1: rec.oldPt.x, y1: rec.oldPt.y }) + else il.set({ x2: rec.oldPt.x, y2: rec.oldPt.y }) + } if (rec.originalAttrs) il.attributes = rec.originalAttrs + if (typeof il.setCoords === 'function') il.setCoords() } logger.log('[KERAB-VALLEY-EXT-TRIM] revert restored ' + target.__valleyExtTrims.length + ' trim(s)') delete target.__valleyExtTrims