dev #870
@ -383,10 +383,11 @@ export function useEavesGableEdit(id) {
|
||||
: null,
|
||||
}),
|
||||
)
|
||||
// [KERAB-VALLEY-DIAG 2026-05-27] 평행(apex=null) 케라바: polygonPath 라인들의 valley vertex 식별.
|
||||
// 사용자 전제: 평행 케라바 조건 = polygonPath 내부에 valley 가 2개 존재. 그 중 한쪽이 변환 대상.
|
||||
// 진단 단계 — 실제 데이터에서 valley 가 어떤 vertex 로 잡히는지 확인.
|
||||
if (!apex && polygonPath) {
|
||||
// [KERAB-VALLEY-DIAG 2026-05-27] polygonPath 라인들의 valley vertex 식별 (진단).
|
||||
// apex 유무 무관 — valley 가 polygonPath 에 존재하면 valleyExt 후보 (gate 완화 2026-05-27).
|
||||
// surgical 출폭 변경으로 H-3·H-2 평행성이 살짝 깨져 apex 가 폴리곤 밖 멀리 잡히는 케이스(거의 평행)
|
||||
// 에서도 처마확장이 그려져야 함. 내부의 `h1FarIsValley || h2FarIsValley` 가드가 자동 skip 보장.
|
||||
if (polygonPath) {
|
||||
const valleyPool = [h1Match.hip, h2Match.hip, target, ...polygonPath.map((p) => p.line)]
|
||||
const valleyInfo = []
|
||||
for (const line of [h1Match.hip, h2Match.hip, ...polygonPath.map((p) => p.line)]) {
|
||||
@ -406,63 +407,48 @@ export function useEavesGableEdit(id) {
|
||||
}
|
||||
logger.log('[KERAB-VALLEY-DIAG] ' + JSON.stringify(valleyInfo))
|
||||
}
|
||||
// [KERAB-VALLEY-EXT 2026-05-27] valley vertex 검출 + target/roofLine 의 valley 측 endpoint 만 미리 결정.
|
||||
// 실제 raycast 는 applyKerabKLinePattern 호출 후 (extLines, 새 kLine 이 roof.innerLines 에 추가된 뒤)
|
||||
// 에 수행해야 hip 자체+확장, ridge 자체+확장이 모두 정지 대상으로 잡힌다.
|
||||
// [KERAB-VALLEY-EXT 2026-05-27] 모델 교체: skeleton valley → 외곽 polygon concave corner.
|
||||
// "확장" = roofLine 의 한쪽 끝점에서 self-extension (반대 끝점 → 그 끝점 방향) 으로 연장.
|
||||
// concave corner 옆 끝점 self-extension 은 polygon 내부로 향함 → 첫 hip/ridge 와 hit.
|
||||
// convex 측 끝점 self-extension 은 polygon 외부로 새서 hit 없음 → 자동 skip.
|
||||
// 양 끝점 둘 다 후보로 push → raycast 가 valley 측을 자동 결정.
|
||||
const valleyPlannedEndpoints = []
|
||||
if (!apex && polygonPath) {
|
||||
const valleyPool = [h1Match.hip, h2Match.hip, target, ...polygonPath.map((p) => p.line)]
|
||||
const h1Info = findInteriorPoint(h1Match.hip, valleyPool)
|
||||
const h2Info = findInteriorPoint(h2Match.hip, valleyPool)
|
||||
const h1FarIsEnd =
|
||||
Math.hypot(h1Match.far.x - h1Match.hip.x2, h1Match.far.y - h1Match.hip.y2) < 1
|
||||
const h2FarIsEnd =
|
||||
Math.hypot(h2Match.far.x - h2Match.hip.x2, h2Match.far.y - h2Match.hip.y2) < 1
|
||||
const h1FarIsValley = h1FarIsEnd ? h1Info.end : h1Info.start
|
||||
const h2FarIsValley = h2FarIsEnd ? h2Info.end : h2Info.start
|
||||
if (polygonPath) {
|
||||
const matchingRoofLine = Array.isArray(roof.lines)
|
||||
? roof.lines.find((rl) => rl && rl.attributes?.wallLine === target.id)
|
||||
: null
|
||||
logger.log(
|
||||
'[KERAB-VALLEY-EXT] gate ' +
|
||||
JSON.stringify({ h1FarIsValley, h2FarIsValley }),
|
||||
'[KERAB-VALLEY-EXT] roofLine-match ' +
|
||||
JSON.stringify({
|
||||
targetId: target.id,
|
||||
targetIdx: target.idx,
|
||||
found: !!matchingRoofLine,
|
||||
rl: matchingRoofLine
|
||||
? { x1: matchingRoofLine.x1, y1: matchingRoofLine.y1, x2: matchingRoofLine.x2, y2: matchingRoofLine.y2 }
|
||||
: null,
|
||||
}),
|
||||
)
|
||||
if (h1FarIsValley || h2FarIsValley) {
|
||||
// target 의 valley 측 endpoint = SK valley vertex (h1/h2.far 중 valley 인 쪽) 와 더 가까운 쪽.
|
||||
const skValley = h1FarIsValley ? h1Match.far : h2Match.far
|
||||
const dTo1 = Math.hypot(target.x1 - skValley.x, target.y1 - skValley.y)
|
||||
const dTo2 = Math.hypot(target.x2 - skValley.x, target.y2 - skValley.y)
|
||||
const valleyAtStart = dTo1 < dTo2
|
||||
const tSX = valleyAtStart ? target.x1 : target.x2
|
||||
const tSY = valleyAtStart ? target.y1 : target.y2
|
||||
const tOX = valleyAtStart ? target.x2 : target.x1
|
||||
const tOY = valleyAtStart ? target.y2 : target.y1
|
||||
logger.log(
|
||||
'[KERAB-VALLEY-EXT] valleyEndpoint ' +
|
||||
JSON.stringify({ skValley, dTo1, dTo2, valleyAtStart, sx: tSX, sy: tSY }),
|
||||
if (matchingRoofLine) {
|
||||
valleyPlannedEndpoints.push(
|
||||
{
|
||||
sx: matchingRoofLine.x1,
|
||||
sy: matchingRoofLine.y1,
|
||||
ox: matchingRoofLine.x2,
|
||||
oy: matchingRoofLine.y2,
|
||||
label: 'roofBase-s',
|
||||
parent: matchingRoofLine,
|
||||
},
|
||||
{
|
||||
sx: matchingRoofLine.x2,
|
||||
sy: matchingRoofLine.y2,
|
||||
ox: matchingRoofLine.x1,
|
||||
oy: matchingRoofLine.y1,
|
||||
label: 'roofBase-e',
|
||||
parent: matchingRoofLine,
|
||||
},
|
||||
)
|
||||
valleyPlannedEndpoints.push({ sx: tSX, sy: tSY, ox: tOX, oy: tOY, label: 'wallBase', parent: target })
|
||||
// roofLine 짝 = roof.lines 중 attributes.wallLine === target.id 인 것 (drawRoofPolygon CCW 동순서).
|
||||
const matchingRoofLine = Array.isArray(roof.lines)
|
||||
? roof.lines.find((rl) => rl && rl.attributes?.wallLine === target.id)
|
||||
: null
|
||||
logger.log(
|
||||
'[KERAB-VALLEY-EXT] roofLine-match ' +
|
||||
JSON.stringify({
|
||||
targetId: target.id,
|
||||
targetIdx: target.idx,
|
||||
found: !!matchingRoofLine,
|
||||
rl: matchingRoofLine
|
||||
? { x1: matchingRoofLine.x1, y1: matchingRoofLine.y1, x2: matchingRoofLine.x2, y2: matchingRoofLine.y2 }
|
||||
: null,
|
||||
}),
|
||||
)
|
||||
if (matchingRoofLine) {
|
||||
const rSX = valleyAtStart ? matchingRoofLine.x1 : matchingRoofLine.x2
|
||||
const rSY = valleyAtStart ? matchingRoofLine.y1 : matchingRoofLine.y2
|
||||
const rOX = valleyAtStart ? matchingRoofLine.x2 : matchingRoofLine.x1
|
||||
const rOY = valleyAtStart ? matchingRoofLine.y2 : matchingRoofLine.y1
|
||||
valleyPlannedEndpoints.push({ sx: rSX, sy: rSY, ox: rOX, oy: rOY, label: 'roofBase', parent: matchingRoofLine })
|
||||
} else {
|
||||
logger.log('[KERAB-VALLEY-EXT] no matching roof.lines for target.id=' + target.id)
|
||||
}
|
||||
} else {
|
||||
logger.log('[KERAB-VALLEY-EXT] no matching roof.lines for target.id=' + target.id)
|
||||
}
|
||||
}
|
||||
if (polygonPath === null) {
|
||||
@ -992,7 +978,17 @@ export function useEavesGableEdit(id) {
|
||||
// (d) re-resolve: stop 점이 죽은 segment(잘려나간 stub 또는 purge 된 확장)
|
||||
// 위에 있던 다른 resolved entry 는 unresolved 로 되돌리고, 새 상태로
|
||||
// wall + barrier 까지 재확장한다 (Phase A의 "다음 행위").
|
||||
if (cuts.length > 0) {
|
||||
// [KERAB-VALLEY-EXT 2026-05-27] 골짜기확장 케이스에선 cuts 적용 skip.
|
||||
// 사용자 요구: "골짜기 라인은 확장만 하라" — 처마확장만 그리고
|
||||
// 다른 hip/ridge(H-2 등) 은 일체 손대지 말 것.
|
||||
// staticMeets/cuts 는 일반 케라바 알고리즘의 ext hip pattern 정리용인데,
|
||||
// 골짜기 케이스에선 polygonPath 외부의 H-2 같은 라인까지 dir=junction 으로
|
||||
// 단축시키는 부작용이 있음.
|
||||
const isValleyExtCase = valleyPlannedEndpoints.length > 0
|
||||
if (cuts.length > 0 && isValleyExtCase) {
|
||||
logger.log('[KERAB-FIXPOINT-PHASE-A] cuts SKIPPED (valley extension case)')
|
||||
}
|
||||
if (cuts.length > 0 && !isValleyExtCase) {
|
||||
logger.log(
|
||||
'[KERAB-FIXPOINT-PHASE-A] applying cuts(initial) ' +
|
||||
JSON.stringify(
|
||||
@ -1300,6 +1296,9 @@ export function useEavesGableEdit(id) {
|
||||
const valleyExtensions = []
|
||||
for (const ep of valleyPlannedEndpoints) {
|
||||
const start = { x: ep.sx, y: ep.sy }
|
||||
// [KERAB-VALLEY-EXT 2026-05-27] self-extension 방향만 사용 (양 끝점 둘 다 시도).
|
||||
// concave corner 측 끝점 → polygon 내부로 향해 hip/ridge hit → 그려짐.
|
||||
// convex 측 끝점 → polygon 외부로 새서 hit 없음 → 자동 skip.
|
||||
const dx = ep.sx - ep.ox
|
||||
const dy = ep.sy - ep.oy
|
||||
const len = Math.hypot(dx, dy) || 1
|
||||
@ -1312,9 +1311,9 @@ export function useEavesGableEdit(id) {
|
||||
let bestHit = null
|
||||
for (const il of roof.innerLines || []) {
|
||||
if (!il) continue
|
||||
// [KERAB-VALLEY-EXT 2026-05-27] 처마확장은 자기(kerabPatternValleyExt) 외 모든 inner line 에서 정지.
|
||||
// "처마확장은 자기와 다른 속성의 라인을 만나면 멈춘다" — cascade/cut 없이 그리기만.
|
||||
// [KERAB-VALLEY-EXT 2026-05-27] stopper = HIP / RIDGE 만.
|
||||
if (il.lineName === 'kerabPatternValleyExt') continue
|
||||
if (il.name !== LINE_TYPE.SUBLINE.HIP && il.name !== LINE_TYPE.SUBLINE.RIDGE) continue
|
||||
if (il.visible === false) continue
|
||||
const a = { x: il.x1, y: il.y1 }
|
||||
const b = { x: il.x2, y: il.y2 }
|
||||
@ -1348,72 +1347,65 @@ export function useEavesGableEdit(id) {
|
||||
logger.log('[KERAB-VALLEY-EXT] no ridge/hip hit for ' + ep.label)
|
||||
}
|
||||
}
|
||||
// [KERAB-VALLEY-EXT-SPLIT 2026-05-27] hit innerLine 을 hitPoint 에서 분할.
|
||||
// splitPolygonWithLines 의 graph 가 line 양 endpoint 만 노드로 잡으므로,
|
||||
// ext 끝점이 기존 innerLine 중간에 있으면 T-junction 이 graph 에 안 잡혀 face 할당이 끊긴다.
|
||||
// revert 시 복원 위해 target.__valleyExtSplits 에 (halves, original snapshot) 보관.
|
||||
// wallBase 확장은 wall layer 라 roof face 할당에 참여하면 안 되므로 split 대상에서 제외.
|
||||
const splitRecords = []
|
||||
// 같은 hit line 을 여러 ext 가 공유할 수 있으므로 endpoint set 으로 누적 후 분할.
|
||||
const splitMap = new Map() // hitLine -> [hitPoints...]
|
||||
for (const vr of valleyExtensions) {
|
||||
if (vr.source !== 'roofBase') continue
|
||||
// [KERAB-VALLEY-EXT-TRIM 2026-05-27] 처마확장 segment 는 1순위로 보존 (위에서 그려진 hitPoint 까지 그대로).
|
||||
// 다음 단계: 처마확장이 만난 hitLine(hip/ridge) 의 외곽측 끝점을 hitPoint 로 단축(절삭).
|
||||
// 도미노: 단축된 hitLine 의 옛 외곽 끝점에 닿아있던 다른 hip/ridge 의 그 끝점도 hitPoint 로 cascade 이동.
|
||||
// revert 시 trimRecords 역순 복원 (도미노 → 원본 trim 순).
|
||||
// [2026-05-27 비활성] flag=false. junction 공유 hip 까지 cascade 되는 부작용 (예: H-2 의도 외 절삭)
|
||||
// 확인 — 절삭 모델은 다음 페이즈에서 concave-side-only 판정 추가 후 재활성.
|
||||
const ENABLE_VALLEY_EXT_TRIM = false
|
||||
const trimRecords = []
|
||||
const TOL_DOMINO = 1.0
|
||||
if (ENABLE_VALLEY_EXT_TRIM) for (const vr of valleyExtensions) {
|
||||
if (!vr.source || !vr.source.startsWith('roofBase')) continue
|
||||
if (!vr.hitLine || !vr.hitPoint) continue
|
||||
if (!splitMap.has(vr.hitLine)) splitMap.set(vr.hitLine, [])
|
||||
splitMap.get(vr.hitLine).push(vr.hitPoint)
|
||||
}
|
||||
for (const [hitLine, hitPoints] of splitMap.entries()) {
|
||||
const origPts = [hitLine.x1, hitLine.y1, hitLine.x2, hitLine.y2]
|
||||
const origAttrs = { ...(hitLine.attributes || {}) }
|
||||
const origName = hitLine.name
|
||||
const origLineName = hitLine.lineName
|
||||
const origStroke = hitLine.stroke
|
||||
const origStrokeWidth = hitLine.strokeWidth
|
||||
// start 에서 거리 순 정렬.
|
||||
const sx = hitLine.x1
|
||||
const sy = hitLine.y1
|
||||
hitPoints.sort((a, b) => Math.hypot(a.x - sx, a.y - sy) - Math.hypot(b.x - sx, b.y - sy))
|
||||
const splitSegs = []
|
||||
let cur = { x: hitLine.x1, y: hitLine.y1 }
|
||||
for (const hp of hitPoints) {
|
||||
splitSegs.push([cur.x, cur.y, hp.x, hp.y])
|
||||
cur = hp
|
||||
}
|
||||
splitSegs.push([cur.x, cur.y, hitLine.x2, hitLine.y2])
|
||||
const halves = []
|
||||
for (const segPts of splitSegs) {
|
||||
const segSz = calcLinePlaneSize({ x1: segPts[0], y1: segPts[1], x2: segPts[2], y2: segPts[3] })
|
||||
const half = new QLine(segPts, {
|
||||
parentId: hitLine.parentId,
|
||||
fontSize: hitLine.fontSize,
|
||||
stroke: origStroke,
|
||||
strokeWidth: origStrokeWidth,
|
||||
name: origName,
|
||||
textMode: hitLine.textMode,
|
||||
attributes: { ...origAttrs, planeSize: segSz, actualSize: segSz },
|
||||
})
|
||||
if (origLineName) half.lineName = origLineName
|
||||
half.__valleyExtSplitHalf = true
|
||||
half.__valleyExtSplitTargetId = target.id
|
||||
canvas.add(half)
|
||||
half.bringToFront()
|
||||
roof.innerLines.push(half)
|
||||
halves.push(half)
|
||||
}
|
||||
// 원본 제거.
|
||||
removeLine(hitLine)
|
||||
roof.innerLines = roof.innerLines.filter((il) => il !== hitLine)
|
||||
splitRecords.push({
|
||||
halves,
|
||||
originalPts: origPts,
|
||||
originalAttrs: origAttrs,
|
||||
originalName: origName,
|
||||
originalLineName: origLineName,
|
||||
originalStroke: origStroke,
|
||||
originalStrokeWidth: origStrokeWidth,
|
||||
const il = vr.hitLine
|
||||
const hp = vr.hitPoint
|
||||
const d1 = Math.hypot(il.x1 - vr.x1, il.y1 - vr.y1)
|
||||
const d2 = Math.hypot(il.x2 - vr.x1, il.y2 - vr.y1)
|
||||
const trimEnd = d1 < d2 ? 1 : 2
|
||||
const oldX = trimEnd === 1 ? il.x1 : il.x2
|
||||
const oldY = trimEnd === 1 ? il.y1 : il.y2
|
||||
trimRecords.push({
|
||||
line: il,
|
||||
end: trimEnd,
|
||||
oldPt: { x: oldX, y: oldY },
|
||||
newPt: { x: hp.x, y: hp.y },
|
||||
originalAttrs: { ...(il.attributes || {}) },
|
||||
})
|
||||
if (trimEnd === 1) il.set({ x1: hp.x, y1: hp.y })
|
||||
else il.set({ x2: hp.x, y2: hp.y })
|
||||
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(
|
||||
'[KERAB-VALLEY-EXT-TRIM] trim ' +
|
||||
JSON.stringify({ name: il.name, lineName: il.lineName, trimEnd, oldPt: { x: oldX, y: oldY }, hp }),
|
||||
)
|
||||
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
|
||||
const m1 = Math.hypot(other.x1 - oldX, other.y1 - oldY) <= TOL_DOMINO
|
||||
const m2 = Math.hypot(other.x2 - oldX, other.y2 - oldY) <= TOL_DOMINO
|
||||
if (!m1 && !m2) continue
|
||||
trimRecords.push({
|
||||
line: other,
|
||||
end: m1 ? 1 : 2,
|
||||
oldPt: { x: oldX, y: oldY },
|
||||
newPt: { x: hp.x, y: hp.y },
|
||||
originalAttrs: { ...(other.attributes || {}) },
|
||||
domino: true,
|
||||
})
|
||||
if (m1) other.set({ x1: hp.x, y1: hp.y })
|
||||
else other.set({ x2: hp.x, y2: hp.y })
|
||||
const oSz = calcLinePlaneSize({ x1: other.x1, y1: other.y1, x2: other.x2, y2: other.y2 })
|
||||
other.attributes = { ...other.attributes, planeSize: oSz, actualSize: oSz }
|
||||
logger.log(
|
||||
'[KERAB-VALLEY-EXT-TRIM] domino ' + JSON.stringify({ name: other.name, lineName: other.lineName, hp }),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (splitRecords.length) target.__valleyExtSplits = splitRecords
|
||||
if (trimRecords.length) target.__valleyExtTrims = trimRecords
|
||||
|
||||
for (const vr of valleyExtensions) {
|
||||
const pts = [vr.x1, vr.y1, vr.x2, vr.y2]
|
||||
@ -1421,7 +1413,7 @@ export function useEavesGableEdit(id) {
|
||||
// [KERAB-VALLEY-EXT 2026-05-27] 지붕면 할당 통합 (option a):
|
||||
// roofBase 확장은 처마의 연장 → attributes.type=EAVES + parentLine + innerLines push (split 참여).
|
||||
// wallBase 확장은 wall layer (roof 폴리곤 밖) → 시각만, innerLines 비추가, type/parent 부여 안 함.
|
||||
const isRoofBase = vr.source === 'roofBase'
|
||||
const isRoofBase = !!(vr.source && vr.source.startsWith('roofBase'))
|
||||
const baseAttrs = { roofId: roof.id, planeSize: sz, actualSize: sz }
|
||||
if (isRoofBase) {
|
||||
// [KERAB-VALLEY-EXT 2026-05-27] splitPolygonWithLines 의 innerLineMapping 은
|
||||
@ -2048,36 +2040,19 @@ export function useEavesGableEdit(id) {
|
||||
if (valleyExtsToRemove.length) {
|
||||
logger.log('[KERAB-VALLEY-EXT] revert removed ' + valleyExtsToRemove.length)
|
||||
}
|
||||
// [KERAB-VALLEY-EXT-SPLIT 2026-05-27] forward 에서 분할했던 hit innerLine 복원.
|
||||
// halves 제거 + 원본 snapshot 으로 1개 재생성.
|
||||
if (Array.isArray(target.__valleyExtSplits) && target.__valleyExtSplits.length) {
|
||||
for (const rec of target.__valleyExtSplits) {
|
||||
for (const half of rec.halves || []) {
|
||||
removeLine(half)
|
||||
roof.innerLines = roof.innerLines.filter((il) => il !== half)
|
||||
}
|
||||
const sz = calcLinePlaneSize({
|
||||
x1: rec.originalPts[0],
|
||||
y1: rec.originalPts[1],
|
||||
x2: rec.originalPts[2],
|
||||
y2: rec.originalPts[3],
|
||||
})
|
||||
const restored = new QLine(rec.originalPts, {
|
||||
parentId: roof.id,
|
||||
fontSize: roof.fontSize,
|
||||
stroke: rec.originalStroke,
|
||||
strokeWidth: rec.originalStrokeWidth,
|
||||
name: rec.originalName,
|
||||
textMode: roof.textMode,
|
||||
attributes: { ...rec.originalAttrs, planeSize: rec.originalAttrs?.planeSize ?? sz, actualSize: rec.originalAttrs?.actualSize ?? sz },
|
||||
})
|
||||
if (rec.originalLineName) restored.lineName = rec.originalLineName
|
||||
canvas.add(restored)
|
||||
restored.bringToFront()
|
||||
roof.innerLines.push(restored)
|
||||
// [KERAB-VALLEY-EXT-TRIM 2026-05-27] forward 에서 절삭/도미노로 단축한 hip/ridge 끝점을 옛 좌표로 복원.
|
||||
// trimRecords push 순서: 원본 trim → 도미노 cascade. 복원은 역순.
|
||||
if (Array.isArray(target.__valleyExtTrims) && target.__valleyExtTrims.length) {
|
||||
for (let i = target.__valleyExtTrims.length - 1; i >= 0; i--) {
|
||||
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.originalAttrs) il.attributes = rec.originalAttrs
|
||||
}
|
||||
logger.log('[KERAB-VALLEY-EXT-SPLIT] revert restored ' + target.__valleyExtSplits.length + ' hit-line(s)')
|
||||
delete target.__valleyExtSplits
|
||||
logger.log('[KERAB-VALLEY-EXT-TRIM] revert restored ' + target.__valleyExtTrims.length + ' trim(s)')
|
||||
delete target.__valleyExtTrims
|
||||
}
|
||||
const buildHipFromSnapshot = (snap) => {
|
||||
const pts = [snap.x1, snap.y1, snap.x2, snap.y2]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user