[2173] valleyExt TRIM — hip-presence 원래hip-only + cascade hide

This commit is contained in:
ysCha 2026-05-27 16:18:33 +09:00
parent e9f5ceab69
commit a4b7f519bb

View File

@ -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