dev #875

Merged
ysCha merged 5 commits from dev into dev-deploy 2026-06-02 17:00:36 +09:00
2 changed files with 117 additions and 8 deletions
Showing only changes of commit 8ef0810058 - Show all commits

View File

@ -1839,7 +1839,9 @@ export function useEavesGableEdit(id) {
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 }
trimCascadePts.push(oldPt)
// [KERAB-EXTRIDGE-CONNECT 2026-06-01] cascade hide 시 ExtRidge 끝점을 vExt 끝점(=ip)로 연장.
// ExtRidge 와 vExt 끝점이 분리돼 split graph 의 sub-roof 외곽이 안 닫히는 문제 해소.
trimCascadePts.push({ pt: oldPt, newPt: { x: ip.x, y: ip.y } })
newTrimRecords.push({
line: il,
end: trimEnd,
@ -1856,8 +1858,9 @@ export function useEavesGableEdit(id) {
const cascadeHidden = new Set()
let cascadeGuard = 0
while (trimCascadePts.length && cascadeGuard++ < 200) {
const pt = trimCascadePts.shift()
if (!pt) continue
const entry = trimCascadePts.shift()
if (!entry) continue
const pt = entry.pt || entry
for (const il of roof.innerLines || []) {
if (!il || il.visible === false) continue
if (cascadeHidden.has(il)) continue
@ -1880,7 +1883,7 @@ export function useEavesGableEdit(id) {
'[KERAB-VALLEY-EXT-TRIM] cascade hide ' +
JSON.stringify({ lineName: il.lineName, x1: il.x1, y1: il.y1, x2: il.x2, y2: il.y2 }),
)
trimCascadePts.push(m1 ? { x: il.x2, y: il.y2 } : { x: il.x1, y: il.y1 })
trimCascadePts.push({ pt: m1 ? { x: il.x2, y: il.y2 } : { x: il.x1, y: il.y1 } })
}
}
// [KERAB-VALLEY-EXT-TRIM 2026-05-29] revert 위해 target.__valleyExtTrims 에 누적.

View File

@ -129,7 +129,9 @@ export const applyKerabOffsetSurgical = (canvas, target, newOffset) => {
(il.lineName === 'kerabPatternRidge' ||
il.lineName === 'kerabPatternExtRidge' ||
il.lineName === 'kerabPatternHip' ||
il.lineName === 'kerabPatternExtHip')
il.lineName === 'kerabPatternExtHip' ||
il.lineName === 'kerabPatternValleyExt' ||
il.lineName === 'kerabValleyOverlapLine')
const oldSegDx = oldCorner2.x - oldCorner1.x
const oldSegDy = oldCorner2.y - oldCorner1.y
const oldSegLen2 = oldSegDx * oldSegDx + oldSegDy * oldSegDy || 1
@ -150,18 +152,87 @@ export const applyKerabOffsetSurgical = (canvas, target, newOffset) => {
if (!il) continue
// 케라바 패턴 라인: 옛 roofLine segment 위 끝점 → 새 segment 의 동일 t 점.
// 절삭/복원 흐름 skip.
// 한 끝만 segment 위(vExt 등 self-extension)일 때는 다른 끝도 같은 변위로 평행 이동 →
// 직각/형상 보존. 절삭/복원 흐름 skip.
if (isKerabPatternLine(il)) {
const oldX1 = il.x1
const oldY1 = il.y1
const oldX2 = il.x2
const oldY2 = il.y2
const np1 = mapToNewSeg({ x: il.x1, y: il.y1 })
if (np1) il.set({ x1: np1.x, y1: np1.y })
const np2 = mapToNewSeg({ x: il.x2, y: il.y2 })
if (np2) il.set({ x2: np2.x, y2: np2.y })
if (np1 && np2) {
il.set({ x1: np1.x, y1: np1.y, x2: np2.x, y2: np2.y })
} else if (np1 && !np2) {
const dx = np1.x - il.x1
const dy = np1.y - il.y1
il.set({ x1: np1.x, y1: np1.y, x2: il.x2 + dx, y2: il.y2 + dy })
} else if (!np1 && np2) {
const dx = np2.x - il.x2
const dy = np2.y - il.y2
il.set({ x1: il.x1 + dx, y1: il.y1 + dy, x2: np2.x, y2: np2.y })
}
if (typeof il.setCoords === 'function') il.setCoords()
if (np1 || np2) {
logger.log(
'[KERAB-PATTERN-CORNER-SNAP] mapped ' +
JSON.stringify({ lineName: il.lineName, np1, np2, newPts: { x1: il.x1, y1: il.y1, x2: il.x2, y2: il.y2 } }),
)
// [KERAB-PATTERN-CASCADE 2026-06-01] vExt 등 평행 이동 시 옛 끝점에 닿아있던
// 다른 innerLine 끝점도 같은 변위로 평행 이동. RG-1 의 valley-trim 결과 끝점이
// vExt 끝점과 분리되어 split graph 의 closed path 안 만들어지는 문제 해소.
// cascade: vExt 옛 segment 위에 끝점이 있는 다른 innerLine 도 같은 변위로 평행 이동.
// 끝점-끝점 일치 외에 segment 위 중간 점(예: kLine 끝점이 vExt segment 위)도 매칭.
// 케라바 패턴 라인 가드 제거 — 자체 매핑된 라인은 이미 새 좌표라 옛 segment 위 X → 자동 skip.
const dxVExt = il.x1 - oldX1
const dyVExt = il.y1 - oldY1
const pointOnOldSeg = (px, py) => {
const sdx = oldX2 - oldX1
const sdy = oldY2 - oldY1
const slen2 = sdx * sdx + sdy * sdy
if (slen2 < 1e-6) return Math.hypot(px - oldX1, py - oldY1) < 1.0
const t = ((px - oldX1) * sdx + (py - oldY1) * sdy) / slen2
if (t < -0.02 || t > 1.02) return false
const projX = oldX1 + t * sdx
const projY = oldY1 + t * sdy
return Math.hypot(px - projX, py - projY) < 1.0
}
if (Math.hypot(dxVExt, dyVExt) > 0.01) {
// cascade 대상: roof.innerLines + canvas 의 kerabValleyOverlapLine (innerLines 미포함).
// overlap 보조선들은 vExt 의 끝점과 90도로 만나므로 vExt 이동에 같이 따라가야 정합.
const overlapInCanvas = (canvas.getObjects() || []).filter(
(o) =>
o &&
o.lineName === 'kerabValleyOverlapLine' &&
(o.roofId === roof.id || o.attributes?.roofId === roof.id),
)
const cascadeTargets = [...(roof.innerLines || []), ...overlapInCanvas]
for (const other of cascadeTargets) {
if (!other || other === il) continue
let moved = false
if (pointOnOldSeg(other.x1, other.y1)) {
other.set({ x1: other.x1 + dxVExt, y1: other.y1 + dyVExt })
moved = true
}
if (pointOnOldSeg(other.x2, other.y2)) {
other.set({ x2: other.x2 + dxVExt, y2: other.y2 + dyVExt })
moved = true
}
if (moved) {
if (typeof other.setCoords === 'function') other.setCoords()
logger.log(
'[KERAB-PATTERN-CASCADE] moved ' +
JSON.stringify({
lineName: other.lineName,
name: other.name,
dx: dxVExt,
dy: dyVExt,
newPts: { x1: other.x1, y1: other.y1, x2: other.x2, y2: other.y2 },
}),
)
}
}
}
}
continue
}
@ -169,6 +240,41 @@ export const applyKerabOffsetSurgical = (canvas, target, newOffset) => {
const orig = il.__shrinkOrig || { x1: il.x1, y1: il.y1, x2: il.x2, y2: il.y2 }
const orig1 = { x: orig.x1, y: orig.y1 }
const orig2 = { x: orig.x2, y: orig.y2 }
// [KERAB-OFFSET-CORNER-SHORTCUT 2026-06-01] orig 끝점이 옛 corner 와 일치하면 새 corner 로 직접 snap.
// 일반 절삭 흐름은 il segment 와 새 roofLine 변의 lineLineIntersection 으로 ip 계산하는데,
// 끝점이 옛 corner 위에 있으면 ip 가 새 corner segment 밖으로 떨어져 segOk 가 reject → 절삭 실패.
// 그 결과 c1·c2 비대칭으로 kLine 대각선 변형됨.
const CORNER_SNAP_TOL_TRIM = 0.5
let cornerSnapped = false
const trySnap = (epx, epy, which) => {
if (Math.hypot(epx - oldCorner1.x, epy - oldCorner1.y) < CORNER_SNAP_TOL_TRIM) {
if (!il.__shrinkOrig) il.__shrinkOrig = { x1: orig.x1, y1: orig.y1, x2: orig.x2, y2: orig.y2 }
if (which === 1) il.set({ x1: newCorner1.x, y1: newCorner1.y })
else il.set({ x2: newCorner1.x, y2: newCorner1.y })
cornerSnapped = true
return true
}
if (Math.hypot(epx - oldCorner2.x, epy - oldCorner2.y) < CORNER_SNAP_TOL_TRIM) {
if (!il.__shrinkOrig) il.__shrinkOrig = { x1: orig.x1, y1: orig.y1, x2: orig.x2, y2: orig.y2 }
if (which === 1) il.set({ x1: newCorner2.x, y1: newCorner2.y })
else il.set({ x2: newCorner2.x, y2: newCorner2.y })
cornerSnapped = true
return true
}
return false
}
trySnap(orig.x1, orig.y1, 1)
trySnap(orig.x2, orig.y2, 2)
if (cornerSnapped) {
if (typeof il.setCoords === 'function') il.setCoords()
logger.log(
'[KERAB-OFFSET-CORNER-SHORTCUT] snapped ' +
JSON.stringify({ lineName: il.lineName, x1: il.x1, y1: il.y1, x2: il.x2, y2: il.y2 }),
)
continue
}
const d1 = (orig1.x - newAxisMid.x) * nx + (orig1.y - newAxisMid.y) * ny
const d2 = (orig2.x - newAxisMid.x) * nx + (orig2.y - newAxisMid.y) * ny