Merge pull request 'dev' (#870) from dev into dev-deploy
Reviewed-on: #870
This commit is contained in:
commit
21b8032257
@ -383,10 +383,11 @@ export function useEavesGableEdit(id) {
|
|||||||
: null,
|
: null,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
// [KERAB-VALLEY-DIAG 2026-05-27] 평행(apex=null) 케라바: polygonPath 라인들의 valley vertex 식별.
|
// [KERAB-VALLEY-DIAG 2026-05-27] polygonPath 라인들의 valley vertex 식별 (진단).
|
||||||
// 사용자 전제: 평행 케라바 조건 = polygonPath 내부에 valley 가 2개 존재. 그 중 한쪽이 변환 대상.
|
// apex 유무 무관 — valley 가 polygonPath 에 존재하면 valleyExt 후보 (gate 완화 2026-05-27).
|
||||||
// 진단 단계 — 실제 데이터에서 valley 가 어떤 vertex 로 잡히는지 확인.
|
// surgical 출폭 변경으로 H-3·H-2 평행성이 살짝 깨져 apex 가 폴리곤 밖 멀리 잡히는 케이스(거의 평행)
|
||||||
if (!apex && polygonPath) {
|
// 에서도 처마확장이 그려져야 함. 내부의 `h1FarIsValley || h2FarIsValley` 가드가 자동 skip 보장.
|
||||||
|
if (polygonPath) {
|
||||||
const valleyPool = [h1Match.hip, h2Match.hip, target, ...polygonPath.map((p) => p.line)]
|
const valleyPool = [h1Match.hip, h2Match.hip, target, ...polygonPath.map((p) => p.line)]
|
||||||
const valleyInfo = []
|
const valleyInfo = []
|
||||||
for (const line of [h1Match.hip, h2Match.hip, ...polygonPath.map((p) => p.line)]) {
|
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))
|
logger.log('[KERAB-VALLEY-DIAG] ' + JSON.stringify(valleyInfo))
|
||||||
}
|
}
|
||||||
// [KERAB-VALLEY-EXT 2026-05-27] valley vertex 검출 + target/roofLine 의 valley 측 endpoint 만 미리 결정.
|
// [KERAB-VALLEY-EXT 2026-05-27] 모델 교체: skeleton valley → 외곽 polygon concave corner.
|
||||||
// 실제 raycast 는 applyKerabKLinePattern 호출 후 (extLines, 새 kLine 이 roof.innerLines 에 추가된 뒤)
|
// "확장" = roofLine 의 한쪽 끝점에서 self-extension (반대 끝점 → 그 끝점 방향) 으로 연장.
|
||||||
// 에 수행해야 hip 자체+확장, ridge 자체+확장이 모두 정지 대상으로 잡힌다.
|
// concave corner 옆 끝점 self-extension 은 polygon 내부로 향함 → 첫 hip/ridge 와 hit.
|
||||||
|
// convex 측 끝점 self-extension 은 polygon 외부로 새서 hit 없음 → 자동 skip.
|
||||||
|
// 양 끝점 둘 다 후보로 push → raycast 가 valley 측을 자동 결정.
|
||||||
const valleyPlannedEndpoints = []
|
const valleyPlannedEndpoints = []
|
||||||
if (!apex && polygonPath) {
|
if (polygonPath) {
|
||||||
const valleyPool = [h1Match.hip, h2Match.hip, target, ...polygonPath.map((p) => p.line)]
|
const matchingRoofLine = Array.isArray(roof.lines)
|
||||||
const h1Info = findInteriorPoint(h1Match.hip, valleyPool)
|
? roof.lines.find((rl) => rl && rl.attributes?.wallLine === target.id)
|
||||||
const h2Info = findInteriorPoint(h2Match.hip, valleyPool)
|
: null
|
||||||
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
|
|
||||||
logger.log(
|
logger.log(
|
||||||
'[KERAB-VALLEY-EXT] gate ' +
|
'[KERAB-VALLEY-EXT] roofLine-match ' +
|
||||||
JSON.stringify({ h1FarIsValley, h2FarIsValley }),
|
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) {
|
if (matchingRoofLine) {
|
||||||
// target 의 valley 측 endpoint = SK valley vertex (h1/h2.far 중 valley 인 쪽) 와 더 가까운 쪽.
|
valleyPlannedEndpoints.push(
|
||||||
const skValley = h1FarIsValley ? h1Match.far : h2Match.far
|
{
|
||||||
const dTo1 = Math.hypot(target.x1 - skValley.x, target.y1 - skValley.y)
|
sx: matchingRoofLine.x1,
|
||||||
const dTo2 = Math.hypot(target.x2 - skValley.x, target.y2 - skValley.y)
|
sy: matchingRoofLine.y1,
|
||||||
const valleyAtStart = dTo1 < dTo2
|
ox: matchingRoofLine.x2,
|
||||||
const tSX = valleyAtStart ? target.x1 : target.x2
|
oy: matchingRoofLine.y2,
|
||||||
const tSY = valleyAtStart ? target.y1 : target.y2
|
label: 'roofBase-s',
|
||||||
const tOX = valleyAtStart ? target.x2 : target.x1
|
parent: matchingRoofLine,
|
||||||
const tOY = valleyAtStart ? target.y2 : target.y1
|
},
|
||||||
logger.log(
|
{
|
||||||
'[KERAB-VALLEY-EXT] valleyEndpoint ' +
|
sx: matchingRoofLine.x2,
|
||||||
JSON.stringify({ skValley, dTo1, dTo2, valleyAtStart, sx: tSX, sy: tSY }),
|
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 })
|
} else {
|
||||||
// roofLine 짝 = roof.lines 중 attributes.wallLine === target.id 인 것 (drawRoofPolygon CCW 동순서).
|
logger.log('[KERAB-VALLEY-EXT] no matching roof.lines for target.id=' + target.id)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (polygonPath === null) {
|
if (polygonPath === null) {
|
||||||
@ -578,6 +564,90 @@ export function useEavesGableEdit(id) {
|
|||||||
const py = ay + t * dy
|
const py = ay + t * dy
|
||||||
return Math.hypot(px - pt.x, py - pt.y) <= tol
|
return Math.hypot(px - pt.x, py - pt.y) <= tol
|
||||||
}
|
}
|
||||||
|
// [KERAB-ROOF-MAX-INWARD 2026-05-27] roof polygon wall 정의를 wave 시작 전으로 이동 (이전 L896).
|
||||||
|
// 사용자 규칙: "확장을 해도 roofLine 까지이다. 절대 통과 못한다."
|
||||||
|
// wave 의 모든 meet 후보 거리를 ext.maxInwardDist 로 제한 → roofLine 너머 meet 거부.
|
||||||
|
const roofPolygonWalls = []
|
||||||
|
if (Array.isArray(roof.points) && roof.points.length >= 2) {
|
||||||
|
for (let i = 0; i < roof.points.length; i++) {
|
||||||
|
roofPolygonWalls.push({
|
||||||
|
a: roof.points[i],
|
||||||
|
b: roof.points[(i + 1) % roof.points.length],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const computeMaxInwardDist = (ext) => {
|
||||||
|
let best = Infinity
|
||||||
|
for (const wall of roofPolygonWalls) {
|
||||||
|
const ip = lineLineIntersection(ext.near, ext.far, wall.a, wall.b)
|
||||||
|
if (!ip) continue
|
||||||
|
if (!isInward(ext, ip)) continue
|
||||||
|
if (!isPointOnSegment(ip, wall.a.x, wall.a.y, wall.b.x, wall.b.y)) continue
|
||||||
|
const d = Math.hypot(ip.x - ext.near.x, ip.y - ext.near.y)
|
||||||
|
if (d < 1e-3) continue
|
||||||
|
if (d < best) best = d
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
const MAX_INWARD_TOL = 0.5
|
||||||
|
for (const ext of allExtenders) {
|
||||||
|
ext.maxInwardDist = computeMaxInwardDist(ext)
|
||||||
|
}
|
||||||
|
// [KERAB-VALLEY-EXT-BARRIER 2026-05-27] 골짜기확장라인 사전 계산 — hip/ridge wave 의 barrier 로 사용.
|
||||||
|
// 사용자 규칙: "힙/마루 라인은 골짜기 확장라인 및 roofLine 까지. 절대 통과 못한다."
|
||||||
|
// pre-wave 상태(polygonPath 라인 미삭제) 의 roof.innerLines 로 raycast. polygonLines 는 곧 삭제될 라인이라 stopper 제외.
|
||||||
|
// 최종 valleyExt 좌표는 L1280+ 에서 post-wave 상태로 다시 raycast → 약간의 차이 있을 수 있음 (수용).
|
||||||
|
const valleyExtPreSegs = []
|
||||||
|
if (valleyPlannedEndpoints.length) {
|
||||||
|
for (const ep of valleyPlannedEndpoints) {
|
||||||
|
const dx = ep.sx - ep.ox
|
||||||
|
const dy = ep.sy - ep.oy
|
||||||
|
const len = Math.hypot(dx, dy) || 1
|
||||||
|
const ux = dx / len
|
||||||
|
const uy = dy / len
|
||||||
|
const FAR_RAY = 1e5
|
||||||
|
const start = { x: ep.sx, y: ep.sy }
|
||||||
|
const rayEnd = { x: ep.sx + ux * FAR_RAY, y: ep.sy + uy * FAR_RAY }
|
||||||
|
// [KERAB-VALLEY-EXT-ALWAYS-MID 2026-05-27] 사용자 규칙: "골짜기 세로라인이 더 확장되어야 한다."
|
||||||
|
// RIDGE stopper 제거 — 무조건 polygon-wall 까지 거리의 절반 = polygon width 의 대칭중앙.
|
||||||
|
// ridge·hip 은 모두 통과 (경로 위 교차 시 trim 단계에서 절삭).
|
||||||
|
let bestPt = null
|
||||||
|
let wallT = Infinity
|
||||||
|
let wallHit = null
|
||||||
|
for (const w of roofPolygonWalls) {
|
||||||
|
const ip = lineLineIntersection(start, rayEnd, w.a, w.b)
|
||||||
|
if (!ip) continue
|
||||||
|
if (!isPointOnSegment(ip, w.a.x, w.a.y, w.b.x, w.b.y)) continue
|
||||||
|
const t = (ip.x - start.x) * ux + (ip.y - start.y) * uy
|
||||||
|
if (t < 0.5) continue
|
||||||
|
if (t < wallT) {
|
||||||
|
wallT = t
|
||||||
|
wallHit = ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (wallHit) {
|
||||||
|
bestPt = { x: (start.x + wallHit.x) / 2, y: (start.y + wallHit.y) / 2 }
|
||||||
|
logger.log(
|
||||||
|
'[KERAB-VALLEY-EXT-MID] pre label=' + ep.label +
|
||||||
|
' wallHit={' + Math.round(wallHit.x * 100) / 100 + ',' + Math.round(wallHit.y * 100) / 100 + '}' +
|
||||||
|
' mid={' + Math.round(bestPt.x * 100) / 100 + ',' + Math.round(bestPt.y * 100) / 100 + '}',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (bestPt) {
|
||||||
|
valleyExtPreSegs.push({ x1: start.x, y1: start.y, x2: bestPt.x, y2: bestPt.y, label: ep.label })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.log(
|
||||||
|
'[KERAB-VALLEY-EXT-PRE] ' +
|
||||||
|
JSON.stringify(
|
||||||
|
valleyExtPreSegs.map((s) => ({
|
||||||
|
label: s.label,
|
||||||
|
from: { x: Math.round(s.x1 * 100) / 100, y: Math.round(s.y1 * 100) / 100 },
|
||||||
|
to: { x: Math.round(s.x2 * 100) / 100, y: Math.round(s.y2 * 100) / 100 },
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
const extenderPool = [...allExtenders]
|
const extenderPool = [...allExtenders]
|
||||||
const resolved = new Map()
|
const resolved = new Map()
|
||||||
// [KERAB-MULTI-APEX 2026-05-22] hip+hip 이 90° 로 만나는 모든 apex 수집 → 각 apex 마다 kLine 1개.
|
// [KERAB-MULTI-APEX 2026-05-22] hip+hip 이 90° 로 만나는 모든 apex 수집 → 각 apex 마다 kLine 1개.
|
||||||
@ -656,6 +726,32 @@ export function useEavesGableEdit(id) {
|
|||||||
kLineMeets.push({ ext, point: ip, dist, kLine: kl })
|
kLineMeets.push({ ext, point: ip, dist, kLine: kl })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [KERAB-VALLEY-EXT-BARRIER 2026-05-27] 골짜기확장라인 segment 와의 meet — hip/ridge 가 그 점에서 정지.
|
||||||
|
// mirror/reflection 없음 (단순 정지). 거리 가장 짧은 후보면 우선 처리되어 그 너머로 못 감.
|
||||||
|
const valleyExtMeets = []
|
||||||
|
for (const ext of unresolved) {
|
||||||
|
for (const vs of valleyExtPreSegs) {
|
||||||
|
const ip = lineLineIntersection(ext.near, ext.far, { x: vs.x1, y: vs.y1 }, { x: vs.x2, y: vs.y2 })
|
||||||
|
if (!ip) continue
|
||||||
|
if (!isInward(ext, ip)) continue
|
||||||
|
if (!isPointOnSegment(ip, vs.x1, vs.y1, vs.x2, vs.y2)) continue
|
||||||
|
const dist = Math.hypot(ip.x - ext.near.x, ip.y - ext.near.y)
|
||||||
|
if (dist < 1e-3) continue
|
||||||
|
valleyExtMeets.push({ ext, point: ip, dist, valleyExtSeg: vs })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (valleyExtMeets.length > 0) {
|
||||||
|
logger.log(
|
||||||
|
'[KERAB-VALLEY-EXT-BARRIER] meets iter=' + iter + ' ' +
|
||||||
|
JSON.stringify(
|
||||||
|
valleyExtMeets.map((v) => ({
|
||||||
|
ext: labelOf(v.ext.line) || (v.ext.isHip ? 'H' : 'R'),
|
||||||
|
point: { x: Math.round(v.point.x * 100) / 100, y: Math.round(v.point.y * 100) / 100 },
|
||||||
|
dist: Math.round(v.dist * 100) / 100,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
// [KERAB-STATIC-RIDGE 2026-05-21] 정적 ridge(RG-1 등, 폴리곤 삭제대상 제외) 도 hip extender 의 거울.
|
// [KERAB-STATIC-RIDGE 2026-05-21] 정적 ridge(RG-1 등, 폴리곤 삭제대상 제외) 도 hip extender 의 거울.
|
||||||
const staticRidges = (roof.innerLines || []).filter(
|
const staticRidges = (roof.innerLines || []).filter(
|
||||||
(il) =>
|
(il) =>
|
||||||
@ -756,6 +852,16 @@ export function useEavesGableEdit(id) {
|
|||||||
for (const km of kLineMeets) {
|
for (const km of kLineMeets) {
|
||||||
candidates.push({ kind: 'kline', extenders: [km.ext], point: km.point, minDist: km.dist, mirrorLine: km.kLine })
|
candidates.push({ kind: 'kline', extenders: [km.ext], point: km.point, minDist: km.dist, mirrorLine: km.kLine })
|
||||||
}
|
}
|
||||||
|
// [KERAB-VALLEY-EXT-BARRIER 2026-05-27] valleyExt 와 만나는 hip/ridge 는 그 점에서 정지.
|
||||||
|
// mirror 없음 → L859 의 (kline/ridge/drawn) 분기에 valleyExt 추가하지 않아 reflected 생성 안 됨.
|
||||||
|
for (const vm of valleyExtMeets) {
|
||||||
|
candidates.push({
|
||||||
|
kind: 'valleyExt',
|
||||||
|
extenders: [vm.ext],
|
||||||
|
point: vm.point,
|
||||||
|
minDist: vm.dist,
|
||||||
|
})
|
||||||
|
}
|
||||||
for (const rm of ridgeMeets) {
|
for (const rm of ridgeMeets) {
|
||||||
candidates.push({
|
candidates.push({
|
||||||
kind: 'ridge',
|
kind: 'ridge',
|
||||||
@ -784,6 +890,37 @@ export function useEavesGableEdit(id) {
|
|||||||
staticLine: sm.staticLine,
|
staticLine: sm.staticLine,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// [KERAB-ROOF-MAX-INWARD 2026-05-27] roofLine 너머 meet 후보 제거.
|
||||||
|
// 사용자 규칙: "확장을 해도 roofLine 까지이다. 절대 통과 못한다."
|
||||||
|
// ext.maxInwardDist (inward 방향 roof wall 까지 최단거리) 를 초과하는 점은 폐기.
|
||||||
|
const candidatesBeforeCap = candidates.length
|
||||||
|
const capFilteredOut = []
|
||||||
|
for (let ci = candidates.length - 1; ci >= 0; ci--) {
|
||||||
|
const c = candidates[ci]
|
||||||
|
let over = false
|
||||||
|
for (const e of c.extenders) {
|
||||||
|
const cap = e.maxInwardDist
|
||||||
|
if (cap === undefined || !Number.isFinite(cap)) continue
|
||||||
|
const d = Math.hypot(c.point.x - e.near.x, c.point.y - e.near.y)
|
||||||
|
if (d > cap + MAX_INWARD_TOL) {
|
||||||
|
over = true
|
||||||
|
capFilteredOut.push({
|
||||||
|
kind: c.kind,
|
||||||
|
ext: labelOf(e.line) || (e.isHip ? 'H' : 'R'),
|
||||||
|
d: Math.round(d * 100) / 100,
|
||||||
|
cap: Math.round(cap * 100) / 100,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (over) candidates.splice(ci, 1)
|
||||||
|
}
|
||||||
|
if (capFilteredOut.length > 0) {
|
||||||
|
logger.log(
|
||||||
|
'[KERAB-ROOF-MAX-INWARD] iter=' + iter + ' filtered=' + capFilteredOut.length +
|
||||||
|
'/' + candidatesBeforeCap + ' ' + JSON.stringify(capFilteredOut),
|
||||||
|
)
|
||||||
|
}
|
||||||
candidates.sort((a, b) => a.minDist - b.minDist)
|
candidates.sort((a, b) => a.minDist - b.minDist)
|
||||||
const newReflected = []
|
const newReflected = []
|
||||||
let pendingKLineCreated = false
|
let pendingKLineCreated = false
|
||||||
@ -889,7 +1026,7 @@ export function useEavesGableEdit(id) {
|
|||||||
(c.kind === 'kline' && pendingKLines.some(
|
(c.kind === 'kline' && pendingKLines.some(
|
||||||
(pk) => pk.x1 === mirrorLine.x1 && pk.y1 === mirrorLine.y1 && pk.x2 === mirrorLine.x2 && pk.y2 === mirrorLine.y2,
|
(pk) => pk.x1 === mirrorLine.x1 && pk.y1 === mirrorLine.y1 && pk.x2 === mirrorLine.x2 && pk.y2 === mirrorLine.y2,
|
||||||
))
|
))
|
||||||
newReflected.push({
|
const reflected = {
|
||||||
line: hipExt.line,
|
line: hipExt.line,
|
||||||
near: { x: c.point.x, y: c.point.y },
|
near: { x: c.point.x, y: c.point.y },
|
||||||
far: { x: c.point.x - rdx, y: c.point.y - rdy },
|
far: { x: c.point.x - rdx, y: c.point.y - rdy },
|
||||||
@ -897,7 +1034,10 @@ export function useEavesGableEdit(id) {
|
|||||||
sourcePoint: { x: c.point.x, y: c.point.y },
|
sourcePoint: { x: c.point.x, y: c.point.y },
|
||||||
__reflected: true,
|
__reflected: true,
|
||||||
__reflectedFromPending: fromPending,
|
__reflectedFromPending: fromPending,
|
||||||
})
|
}
|
||||||
|
// [KERAB-ROOF-MAX-INWARD 2026-05-27] reflected ext 도 roof wall 까지 캡 계산.
|
||||||
|
reflected.maxInwardDist = computeMaxInwardDist(reflected)
|
||||||
|
newReflected.push(reflected)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -907,15 +1047,7 @@ export function useEavesGableEdit(id) {
|
|||||||
// [KERAB-ROOF-FALLBACK-ANYWALL 2026-05-21] 미해소 extender 를 roof 폴리곤의 어떤 wall 이라도
|
// [KERAB-ROOF-FALLBACK-ANYWALL 2026-05-21] 미해소 extender 를 roof 폴리곤의 어떤 wall 이라도
|
||||||
// inward 방향의 가장 가까운 wall 교점까지 확장. 반사 hip 이 target wall 과 반대쪽으로
|
// inward 방향의 가장 가까운 wall 교점까지 확장. 반사 hip 이 target wall 과 반대쪽으로
|
||||||
// 향해도 다른 wall 에서 정지.
|
// 향해도 다른 wall 에서 정지.
|
||||||
const roofPolygonWalls = []
|
// (roofPolygonWalls 정의는 wave 시작 전 KERAB-ROOF-MAX-INWARD 블록으로 이동됨)
|
||||||
if (Array.isArray(roof.points) && roof.points.length >= 2) {
|
|
||||||
for (let i = 0; i < roof.points.length; i++) {
|
|
||||||
roofPolygonWalls.push({
|
|
||||||
a: roof.points[i],
|
|
||||||
b: roof.points[(i + 1) % roof.points.length],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [KERAB-NO-PIERCE 2026-05-21] fallback 직진 중에도 정적 inner line(이 polygon 의 처리대상 제외)
|
// [KERAB-NO-PIERCE 2026-05-21] fallback 직진 중에도 정적 inner line(이 polygon 의 처리대상 제외)
|
||||||
// + 이번 wave 에 이미 그려질 ext 라인을 barrier 로 검사. 가장 가까운 hit 에서 정지하여
|
// + 이번 wave 에 이미 그려질 ext 라인을 barrier 로 검사. 가장 가까운 hit 에서 정지하여
|
||||||
// 다른 라인을 관통해 내부로 침입하는 케이스를 차단.
|
// 다른 라인을 관통해 내부로 침입하는 케이스를 차단.
|
||||||
@ -936,6 +1068,8 @@ export function useEavesGableEdit(id) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
for (const pk of pendingKLines) barrierLines.push(pk)
|
for (const pk of pendingKLines) barrierLines.push(pk)
|
||||||
|
// [KERAB-VALLEY-EXT-BARRIER 2026-05-27] 골짜기확장라인 — fallback 단계에서도 통과 금지.
|
||||||
|
for (const vs of valleyExtPreSegs) barrierLines.push(vs)
|
||||||
for (const ext of extenderPool) {
|
for (const ext of extenderPool) {
|
||||||
if (resolved.has(ext)) continue
|
if (resolved.has(ext)) continue
|
||||||
let bestPt = null
|
let bestPt = null
|
||||||
@ -992,7 +1126,17 @@ export function useEavesGableEdit(id) {
|
|||||||
// (d) re-resolve: stop 점이 죽은 segment(잘려나간 stub 또는 purge 된 확장)
|
// (d) re-resolve: stop 점이 죽은 segment(잘려나간 stub 또는 purge 된 확장)
|
||||||
// 위에 있던 다른 resolved entry 는 unresolved 로 되돌리고, 새 상태로
|
// 위에 있던 다른 resolved entry 는 unresolved 로 되돌리고, 새 상태로
|
||||||
// wall + barrier 까지 재확장한다 (Phase A의 "다음 행위").
|
// 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(
|
logger.log(
|
||||||
'[KERAB-FIXPOINT-PHASE-A] applying cuts(initial) ' +
|
'[KERAB-FIXPOINT-PHASE-A] applying cuts(initial) ' +
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
@ -1164,6 +1308,8 @@ export function useEavesGableEdit(id) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
for (const pk of pendingKLines) barriers2.push(pk)
|
for (const pk of pendingKLines) barriers2.push(pk)
|
||||||
|
// [KERAB-VALLEY-EXT-BARRIER 2026-05-27] re-resolve 단계에서도 골짜기확장라인 통과 금지.
|
||||||
|
for (const vs of valleyExtPreSegs) barriers2.push(vs)
|
||||||
for (const ext of staleExts) {
|
for (const ext of staleExts) {
|
||||||
let bestPt = null
|
let bestPt = null
|
||||||
let bestDist = Infinity
|
let bestDist = Infinity
|
||||||
@ -1300,6 +1446,9 @@ export function useEavesGableEdit(id) {
|
|||||||
const valleyExtensions = []
|
const valleyExtensions = []
|
||||||
for (const ep of valleyPlannedEndpoints) {
|
for (const ep of valleyPlannedEndpoints) {
|
||||||
const start = { x: ep.sx, y: ep.sy }
|
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 dx = ep.sx - ep.ox
|
||||||
const dy = ep.sy - ep.oy
|
const dy = ep.sy - ep.oy
|
||||||
const len = Math.hypot(dx, dy) || 1
|
const len = Math.hypot(dx, dy) || 1
|
||||||
@ -1307,28 +1456,31 @@ export function useEavesGableEdit(id) {
|
|||||||
const uy = dy / len
|
const uy = dy / len
|
||||||
const FAR_RAY = 1e5
|
const FAR_RAY = 1e5
|
||||||
const rayEnd = { x: start.x + ux * FAR_RAY, y: start.y + uy * FAR_RAY }
|
const rayEnd = { x: start.x + ux * FAR_RAY, y: start.y + uy * FAR_RAY }
|
||||||
|
// [KERAB-VALLEY-EXT-ALWAYS-MID 2026-05-27] 사용자 규칙: "골짜기 세로라인이 더 확장되어야 한다."
|
||||||
|
// RIDGE stopper 제거 — 무조건 polygon-wall midpoint(대칭중앙)까지.
|
||||||
|
// 경로 위 교차 hip/ridge 는 trim 단계에서 절삭 (KERAB-VALLEY-EXT-TRIM).
|
||||||
let bestStop = null
|
let bestStop = null
|
||||||
let bestT = Infinity
|
let wallT = Infinity
|
||||||
let bestHit = null
|
let wallHit = null
|
||||||
for (const il of roof.innerLines || []) {
|
for (const w of roofPolygonWalls) {
|
||||||
if (!il) continue
|
const ip = lineLineIntersection(start, rayEnd, w.a, w.b)
|
||||||
// [KERAB-VALLEY-EXT 2026-05-27] 처마확장은 자기(kerabPatternValleyExt) 외 모든 inner line 에서 정지.
|
|
||||||
// "처마확장은 자기와 다른 속성의 라인을 만나면 멈춘다" — cascade/cut 없이 그리기만.
|
|
||||||
if (il.lineName === 'kerabPatternValleyExt') 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(start, rayEnd, a, b)
|
|
||||||
if (!ip) continue
|
if (!ip) continue
|
||||||
if (!isOnSegV(ip, a.x, a.y, b.x, b.y)) continue
|
if (!isOnSegV(ip, w.a.x, w.a.y, w.b.x, w.b.y)) continue
|
||||||
const t = (ip.x - start.x) * ux + (ip.y - start.y) * uy
|
const t = (ip.x - start.x) * ux + (ip.y - start.y) * uy
|
||||||
if (t < 1.0) continue
|
if (t < 0.5) continue
|
||||||
if (t < bestT) {
|
if (t < wallT) {
|
||||||
bestT = t
|
wallT = t
|
||||||
bestStop = ip
|
wallHit = ip
|
||||||
bestHit = il
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (wallHit) {
|
||||||
|
bestStop = { x: (start.x + wallHit.x) / 2, y: (start.y + wallHit.y) / 2 }
|
||||||
|
logger.log(
|
||||||
|
'[KERAB-VALLEY-EXT-MID] post label=' + ep.label +
|
||||||
|
' wallHit={' + Math.round(wallHit.x * 100) / 100 + ',' + Math.round(wallHit.y * 100) / 100 + '}' +
|
||||||
|
' mid={' + Math.round(bestStop.x * 100) / 100 + ',' + Math.round(bestStop.y * 100) / 100 + '}',
|
||||||
|
)
|
||||||
|
}
|
||||||
if (bestStop) {
|
if (bestStop) {
|
||||||
const seg = {
|
const seg = {
|
||||||
x1: start.x,
|
x1: start.x,
|
||||||
@ -1337,10 +1489,6 @@ export function useEavesGableEdit(id) {
|
|||||||
y2: bestStop.y,
|
y2: bestStop.y,
|
||||||
source: ep.label,
|
source: ep.label,
|
||||||
parent: ep.parent || null,
|
parent: ep.parent || null,
|
||||||
hitLine: bestHit,
|
|
||||||
hitPoint: { x: bestStop.x, y: bestStop.y },
|
|
||||||
hitLineName: bestHit && bestHit.lineName,
|
|
||||||
hitName: bestHit && bestHit.name,
|
|
||||||
}
|
}
|
||||||
valleyExtensions.push(seg)
|
valleyExtensions.push(seg)
|
||||||
logger.log('[KERAB-VALLEY-EXT] generated ' + JSON.stringify(seg))
|
logger.log('[KERAB-VALLEY-EXT] generated ' + JSON.stringify(seg))
|
||||||
@ -1348,72 +1496,154 @@ export function useEavesGableEdit(id) {
|
|||||||
logger.log('[KERAB-VALLEY-EXT] no ridge/hip hit for ' + ep.label)
|
logger.log('[KERAB-VALLEY-EXT] no ridge/hip hit for ' + ep.label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [KERAB-VALLEY-EXT-SPLIT 2026-05-27] hit innerLine 을 hitPoint 에서 분할.
|
// [KERAB-VALLEY-EXT-TRIM 2026-05-27] valleyExt 경로상 교차 hip/ridge 절삭.
|
||||||
// splitPolygonWithLines 의 graph 가 line 양 endpoint 만 노드로 잡으므로,
|
// 각 valleyExt segment 에 대해 모든 hip/ridge 와 교차 검사 → 교차하면 valleyExt 시작점에
|
||||||
// ext 끝점이 기존 innerLine 중간에 있으면 T-junction 이 graph 에 안 잡혀 face 할당이 끊긴다.
|
// 가까운 끝점을 hitPoint 로 단축 (= corner 쪽 부분이 잘리고 너머 쪽이 보존).
|
||||||
// revert 시 복원 위해 target.__valleyExtSplits 에 (halves, original snapshot) 보관.
|
// 사용자 규칙: "대칭중앙까지 가는 도중에 힙 또는 마루를 만나면 힙·마루는 절삭한다."
|
||||||
// wallBase 확장은 wall layer 라 roof face 할당에 참여하면 안 되므로 split 대상에서 제외.
|
// cascade 도미노는 비활성 (junction 공유 hip 의도 외 절삭 부작용).
|
||||||
const splitRecords = []
|
// revert 시 trimRecords 역순 복원.
|
||||||
// 같은 hit line 을 여러 ext 가 공유할 수 있으므로 endpoint set 으로 누적 후 분할.
|
const trimRecords = []
|
||||||
const splitMap = new Map() // hitLine -> [hitPoints...]
|
|
||||||
for (const vr of valleyExtensions) {
|
for (const vr of valleyExtensions) {
|
||||||
if (vr.source !== 'roofBase') continue
|
if (!vr.source || !vr.source.startsWith('roofBase')) continue
|
||||||
if (!vr.hitLine || !vr.hitPoint) continue
|
const segA = { x: vr.x1, y: vr.y1 }
|
||||||
if (!splitMap.has(vr.hitLine)) splitMap.set(vr.hitLine, [])
|
const segB = { x: vr.x2, y: vr.y2 }
|
||||||
splitMap.get(vr.hitLine).push(vr.hitPoint)
|
// 사용자 규칙: "조건 다 필요 없고 확장하는 라인 절삭."
|
||||||
}
|
// trim 검사 ray = segA(corner) → wallEnd(polygon-wall). segB(대칭중앙) 은 segA-wallEnd 의 중점.
|
||||||
for (const [hitLine, hitPoints] of splitMap.entries()) {
|
// ray 위에서 만나는 hip/ridge 의 진행방향 쪽 끝점(= u 투영값 큰 쪽) 을 ip 로 단축.
|
||||||
const origPts = [hitLine.x1, hitLine.y1, hitLine.x2, hitLine.y2]
|
// collinear (valleyExt 와 평행+동일직선) 라인은 별도 처리 — segB 너머 끝점을 segB 로 단축.
|
||||||
const origAttrs = { ...(hitLine.attributes || {}) }
|
const wallEnd = { x: 2 * segB.x - segA.x, y: 2 * segB.y - segA.y }
|
||||||
const origName = hitLine.name
|
const dxV = wallEnd.x - segA.x
|
||||||
const origLineName = hitLine.lineName
|
const dyV = wallEnd.y - segA.y
|
||||||
const origStroke = hitLine.stroke
|
const lenV = Math.hypot(dxV, dyV) || 1
|
||||||
const origStrokeWidth = hitLine.strokeWidth
|
const ux = dxV / lenV
|
||||||
// start 에서 거리 순 정렬.
|
const uy = dyV / lenV
|
||||||
const sx = hitLine.x1
|
const tB = (segB.x - segA.x) * ux + (segB.y - segA.y) * uy
|
||||||
const sy = hitLine.y1
|
const COLLINEAR_TOL = 1.0
|
||||||
hitPoints.sort((a, b) => Math.hypot(a.x - sx, a.y - sy) - Math.hypot(b.x - sx, b.y - sy))
|
for (const il of roof.innerLines || []) {
|
||||||
const splitSegs = []
|
if (!il) continue
|
||||||
let cur = { x: hitLine.x1, y: hitLine.y1 }
|
if (il.lineName === 'kerabPatternValleyExt') continue
|
||||||
for (const hp of hitPoints) {
|
// 사용자 규칙: "Y 모양일때 hip 있는 쪽은 살리고 마루쪽을 절삭." + "1522 도 삭제."
|
||||||
splitSegs.push([cur.x, cur.y, hp.x, hp.y])
|
// trim 대상 = ridge(마루) + 케라바 토글 생성 hip(kerabPatternHip / kerabPatternExtHip).
|
||||||
cur = hp
|
// 원래 지붕 hip(H-5/H-6/H-7/H-1) 은 보존. hip-presence 룰이 자기 자신 제외하므로 안전.
|
||||||
}
|
const isTrimCandidate =
|
||||||
splitSegs.push([cur.x, cur.y, hitLine.x2, hitLine.y2])
|
il.name === LINE_TYPE.SUBLINE.RIDGE ||
|
||||||
const halves = []
|
(il.name === LINE_TYPE.SUBLINE.HIP && (il.lineName === 'kerabPatternHip' || il.lineName === 'kerabPatternExtHip'))
|
||||||
for (const segPts of splitSegs) {
|
if (!isTrimCandidate) continue
|
||||||
const segSz = calcLinePlaneSize({ x1: segPts[0], y1: segPts[1], x2: segPts[2], y2: segPts[3] })
|
if (il.visible === false) continue
|
||||||
const half = new QLine(segPts, {
|
const a = { x: il.x1, y: il.y1 }
|
||||||
parentId: hitLine.parentId,
|
const b = { x: il.x2, y: il.y2 }
|
||||||
fontSize: hitLine.fontSize,
|
let ip = lineLineIntersection(segA, wallEnd, a, b)
|
||||||
stroke: origStroke,
|
if (ip) {
|
||||||
strokeWidth: origStrokeWidth,
|
if (!isOnSegV(ip, segA.x, segA.y, wallEnd.x, wallEnd.y)) ip = null
|
||||||
name: origName,
|
else if (!isOnSegV(ip, a.x, a.y, b.x, b.y)) ip = null
|
||||||
textMode: hitLine.textMode,
|
}
|
||||||
attributes: { ...origAttrs, planeSize: segSz, actualSize: segSz },
|
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.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) < 1.0) return true
|
||||||
|
if (Math.hypot(other.x2 - px, other.y2 - py) < 1.0) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const hip1 = isOriginalHipEndAt(il.x1, il.y1)
|
||||||
|
const hip2 = isOriginalHipEndAt(il.x2, il.y2)
|
||||||
|
let trimEnd
|
||||||
|
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({
|
||||||
|
line: il,
|
||||||
|
end: trimEnd,
|
||||||
|
oldPt: { x: oldX, y: oldY },
|
||||||
|
newPt: { x: ip.x, y: ip.y },
|
||||||
|
originalAttrs: { ...(il.attributes || {}) },
|
||||||
})
|
})
|
||||||
if (origLineName) half.lineName = origLineName
|
if (trimEnd === 1) il.set({ x1: ip.x, y1: ip.y })
|
||||||
half.__valleyExtSplitHalf = true
|
else il.set({ x2: ip.x, y2: ip.y })
|
||||||
half.__valleyExtSplitTargetId = target.id
|
if (typeof il.setCoords === 'function') il.setCoords()
|
||||||
canvas.add(half)
|
const newSz = calcLinePlaneSize({ x1: il.x1, y1: il.y1, x2: il.x2, y2: il.y2 })
|
||||||
half.bringToFront()
|
il.attributes = { ...il.attributes, planeSize: newSz, actualSize: newSz }
|
||||||
roof.innerLines.push(half)
|
logger.log(
|
||||||
halves.push(half)
|
'[KERAB-VALLEY-EXT-TRIM] trim ' +
|
||||||
|
JSON.stringify({ name: il.name, lineName: il.lineName, trimEnd, oldPt: { x: oldX, y: oldY }, ip }),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// 원본 제거.
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if (splitRecords.length) target.__valleyExtSplits = splitRecords
|
// 사용자 규칙: "원 라인이 절삭되었으면 그 밑으로 확장된 라인도 삭제."
|
||||||
|
// 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) {
|
for (const vr of valleyExtensions) {
|
||||||
const pts = [vr.x1, vr.y1, vr.x2, vr.y2]
|
const pts = [vr.x1, vr.y1, vr.x2, vr.y2]
|
||||||
@ -1421,7 +1651,7 @@ export function useEavesGableEdit(id) {
|
|||||||
// [KERAB-VALLEY-EXT 2026-05-27] 지붕면 할당 통합 (option a):
|
// [KERAB-VALLEY-EXT 2026-05-27] 지붕면 할당 통합 (option a):
|
||||||
// roofBase 확장은 처마의 연장 → attributes.type=EAVES + parentLine + innerLines push (split 참여).
|
// roofBase 확장은 처마의 연장 → attributes.type=EAVES + parentLine + innerLines push (split 참여).
|
||||||
// wallBase 확장은 wall layer (roof 폴리곤 밖) → 시각만, innerLines 비추가, type/parent 부여 안 함.
|
// 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 }
|
const baseAttrs = { roofId: roof.id, planeSize: sz, actualSize: sz }
|
||||||
if (isRoofBase) {
|
if (isRoofBase) {
|
||||||
// [KERAB-VALLEY-EXT 2026-05-27] splitPolygonWithLines 의 innerLineMapping 은
|
// [KERAB-VALLEY-EXT 2026-05-27] splitPolygonWithLines 의 innerLineMapping 은
|
||||||
@ -2048,36 +2278,24 @@ export function useEavesGableEdit(id) {
|
|||||||
if (valleyExtsToRemove.length) {
|
if (valleyExtsToRemove.length) {
|
||||||
logger.log('[KERAB-VALLEY-EXT] revert removed ' + valleyExtsToRemove.length)
|
logger.log('[KERAB-VALLEY-EXT] revert removed ' + valleyExtsToRemove.length)
|
||||||
}
|
}
|
||||||
// [KERAB-VALLEY-EXT-SPLIT 2026-05-27] forward 에서 분할했던 hit innerLine 복원.
|
// [KERAB-VALLEY-EXT-TRIM 2026-05-27] forward 에서 절삭/도미노로 단축한 hip/ridge 끝점을 옛 좌표로 복원.
|
||||||
// halves 제거 + 원본 snapshot 으로 1개 재생성.
|
// trimRecords push 순서: 원본 trim → 도미노 cascade. 복원은 역순.
|
||||||
if (Array.isArray(target.__valleyExtSplits) && target.__valleyExtSplits.length) {
|
if (Array.isArray(target.__valleyExtTrims) && target.__valleyExtTrims.length) {
|
||||||
for (const rec of target.__valleyExtSplits) {
|
for (let i = target.__valleyExtTrims.length - 1; i >= 0; i--) {
|
||||||
for (const half of rec.halves || []) {
|
const rec = target.__valleyExtTrims[i]
|
||||||
removeLine(half)
|
const il = rec.line
|
||||||
roof.innerLines = roof.innerLines.filter((il) => il !== half)
|
if (!il) continue
|
||||||
|
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 })
|
||||||
}
|
}
|
||||||
const sz = calcLinePlaneSize({
|
if (rec.originalAttrs) il.attributes = rec.originalAttrs
|
||||||
x1: rec.originalPts[0],
|
if (typeof il.setCoords === 'function') il.setCoords()
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
logger.log('[KERAB-VALLEY-EXT-SPLIT] revert restored ' + target.__valleyExtSplits.length + ' hit-line(s)')
|
logger.log('[KERAB-VALLEY-EXT-TRIM] revert restored ' + target.__valleyExtTrims.length + ' trim(s)')
|
||||||
delete target.__valleyExtSplits
|
delete target.__valleyExtTrims
|
||||||
}
|
}
|
||||||
const buildHipFromSnapshot = (snap) => {
|
const buildHipFromSnapshot = (snap) => {
|
||||||
const pts = [snap.x1, snap.y1, snap.x2, snap.y2]
|
const pts = [snap.x1, snap.y1, snap.x2, snap.y2]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user