[2294] 케라바 출폭 surgical — skLine 절삭/복원 + 패턴 segment 매핑
This commit is contained in:
parent
b8ea9aba78
commit
141afa7a36
@ -243,6 +243,20 @@ export function useEavesGableEdit(id) {
|
||||
// - radio 1 의 단순 변환에만 적용. JERKINHEAD/HIPANDGABLE 등 width 가 들어가는
|
||||
// radio 2 변환은 파라미터 갱신 가능성이 있어 그대로 진행.
|
||||
if (radioTypeRef.current === '1' && target.attributes?.type === attributes?.type) {
|
||||
// [KERAB-OFFSET-ONLY-RECLICK 2026-06-01] 동일 type 재클릭이지만 出幅(offset) 만 다른 경우,
|
||||
// surgical 갱신(corner / 인접 inner-line 끝점 snap + attributes.offset 반영) 후 종료.
|
||||
// 기존 collapse 흐름(applyKerab*Pattern, polygonPath BFS 등) 은 건너뛰어 케라바 패턴 보존.
|
||||
const oldOffset = target.attributes?.offset ?? 0
|
||||
const newOffset = attributes?.offset ?? 0
|
||||
if (Math.abs(newOffset - oldOffset) > 1e-3) {
|
||||
logger.log(
|
||||
`[KERAB-OFFSET-ONLY-RECLICK] type=${attributes.type} 동일, offset ${oldOffset}→${newOffset} surgical 적용`,
|
||||
)
|
||||
applyTargetOffsetSurgical(target, newOffset)
|
||||
target.set({ attributes })
|
||||
canvas.renderAll()
|
||||
return
|
||||
}
|
||||
logger.log(`[KERAB-NOOP] 이미 ${attributes.type} → 재변환 무시`)
|
||||
return
|
||||
}
|
||||
@ -1595,7 +1609,9 @@ export function useEavesGableEdit(id) {
|
||||
// roofBase 확장은 처마의 연장 → attributes.type=EAVES + parentLine + innerLines push (split 참여).
|
||||
// wallBase 확장은 wall layer (roof 폴리곤 밖) → 시각만, innerLines 비추가, type/parent 부여 안 함.
|
||||
const isRoofBase = !!(vr.source && vr.source.startsWith('roofBase'))
|
||||
const baseAttrs = { roofId: roof.id, planeSize: sz, actualSize: sz }
|
||||
// [KERAB-VALLEY-EXT 2026-05-29] wallLine ID 를 attributes 에 명시 저장.
|
||||
// apply() 의 wallExt 재계산 RECALC 가 vExt.attributes.wallLine 으로 outerLine 매칭 — 보존 필수.
|
||||
const baseAttrs = { roofId: roof.id, planeSize: sz, actualSize: sz, wallLine: target.id }
|
||||
if (isRoofBase) {
|
||||
// [KERAB-VALLEY-EXT 2026-05-27] splitPolygonWithLines 의 innerLineMapping 은
|
||||
// EAVES polygonLine 을 skip 하므로(usePolygon.js:822) 처마확장은 자동 매핑이 안 된다.
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
selectedRoofMaterialSelector,
|
||||
} from '@/store/settingAtom'
|
||||
import { usePopup } from '@/hooks/usePopup'
|
||||
import { POLYGON_TYPE } from '@/common/common'
|
||||
import { POLYGON_TYPE, LINE_TYPE } from '@/common/common'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import ActualSizeSetting from '@/components/floor-plan/modal/roofAllocation/ActualSizeSetting'
|
||||
import { useMessage } from '@/hooks/useMessage'
|
||||
@ -909,6 +909,14 @@ export function useRoofAllocationSetting(id) {
|
||||
const roofBases = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF && !obj.roofMaterial)
|
||||
const wallLines = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL)
|
||||
logger.log(`[ALLOC] apply() 진입. roofBases=${roofBases.length}`)
|
||||
// [KERAB-VALLEY-OVERLAP-RECALC 2026-05-29] 저장→로드 사이클을 거치면 fabric 객체 좌표/캐시가 정리되어
|
||||
// case A 가 잘 동작. 저장 X 시 그 효과가 없음. canvas.toJSON 한 번 호출하여 동일 효과 시뮬레이션.
|
||||
try {
|
||||
canvas.discardActiveObject()
|
||||
canvas.toJSON()
|
||||
} catch (e) {
|
||||
logger.warn('[ALLOC] toJSON simulate failed', e)
|
||||
}
|
||||
roofBases.forEach((roofBase) => {
|
||||
try {
|
||||
// 지붕 할당 로직에 extensionLine 추가
|
||||
@ -1046,6 +1054,100 @@ export function useRoofAllocationSetting(id) {
|
||||
)
|
||||
}
|
||||
|
||||
// [KERAB-VALLEY-OVERLAP 2026-05-29] vExt 기반 wallExt 4변 in-memory 재계산.
|
||||
// case A(저장 X→케라바→할당) 에서 canvas 의 kerabValleyOverlapLine 가 silent 사라지는 문제 회피.
|
||||
// vExt(kerabPatternValleyExt) 는 roof.innerLines 에 안정적으로 보존 — 거기서 직접 재계산.
|
||||
// 조건: vExt 가 polygon 내부에 들어온 케이스 한정 (innerLines 에 있으면 자동 충족).
|
||||
const vExtsForOverlap = (roofBase.innerLines || []).filter(
|
||||
(l) => l && l.lineName === 'kerabPatternValleyExt' && l.visible !== false,
|
||||
)
|
||||
for (const vExt of vExtsForOverlap) {
|
||||
// [KERAB-VALLEY-OVERLAP-RECALC 2026-05-29] wallLineId 보장 fallback 체인.
|
||||
// 1) vExt.attributes.wallLine (정상 케이스)
|
||||
// 2) vExt.__targetId (useEavesGableEdit.js drawValleyExtensions 에서 백업)
|
||||
// 3) vExt.parentLine.attributes.wallLine
|
||||
// 4) vStart 좌표 매칭하는 roof.lines 변의 wallLine
|
||||
let wallLineId = vExt.attributes?.wallLine
|
||||
if (!wallLineId) wallLineId = vExt.__targetId
|
||||
if (!wallLineId && vExt.parentLine) wallLineId = vExt.parentLine.attributes?.wallLine
|
||||
if (!wallLineId) {
|
||||
// vStart 가 끝점인 roof.lines 변에서 wallLine 추출
|
||||
const vStartP = { x: vExt.x1, y: vExt.y1 }
|
||||
for (const rl of roofBase.lines || []) {
|
||||
if (!rl?.attributes?.wallLine) continue
|
||||
const m1 = Math.hypot(rl.x1 - vStartP.x, rl.y1 - vStartP.y) < 1
|
||||
const m2 = Math.hypot(rl.x2 - vStartP.x, rl.y2 - vStartP.y) < 1
|
||||
if (m1 || m2) {
|
||||
wallLineId = rl.attributes.wallLine
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!wallLineId) {
|
||||
logger.warn('[KERAB-VALLEY-OVERLAP-RECALC] vExt skip — wallLineId 매칭 실패 (attributes/parent/좌표 모두)')
|
||||
continue
|
||||
}
|
||||
let target = canvas.getObjects().find((o) => o.id === wallLineId && o.name === 'outerLine')
|
||||
if (!target) {
|
||||
const candidates = canvas.getObjects().filter((o) => o.id === wallLineId)
|
||||
target = candidates[0]
|
||||
if (!target) {
|
||||
logger.warn('[KERAB-VALLEY-OVERLAP-RECALC] outerLine 못찾음 wallLineId=' + wallLineId?.slice(0, 8))
|
||||
continue
|
||||
}
|
||||
}
|
||||
const vStart = { x: vExt.x1, y: vExt.y1 }
|
||||
const vEnd = { x: vExt.x2, y: vExt.y2 }
|
||||
const dxT = target.x2 - target.x1
|
||||
const dyT = target.y2 - target.y1
|
||||
const lenTSq = dxT * dxT + dyT * dyT
|
||||
const tFoot = ((vStart.x - target.x1) * dxT + (vStart.y - target.y1) * dyT) / Math.max(lenTSq, 1e-9)
|
||||
const newWStart = { x: target.x1 + tFoot * dxT, y: target.y1 + tFoot * dyT }
|
||||
const offX = newWStart.x - vStart.x
|
||||
const offY = newWStart.y - vStart.y
|
||||
const wEndProj = { x: vEnd.x + offX, y: vEnd.y + offY }
|
||||
const make = (p1, p2) => {
|
||||
const lpts = [p1.x, p1.y, p2.x, p2.y]
|
||||
const dx = lpts[2] - lpts[0]
|
||||
const dy = lpts[3] - lpts[1]
|
||||
const lsz = Math.round(Math.hypot(dx, dy) * 10)
|
||||
const ln = new QLine(lpts, {
|
||||
parentId: roofBase.id,
|
||||
fontSize: roofBase.fontSize,
|
||||
stroke: 'rgba(0,0,0,0)',
|
||||
strokeWidth: 0,
|
||||
name: 'valley',
|
||||
visible: true,
|
||||
opacity: 0,
|
||||
attributes: {
|
||||
roofId: roofBase.id,
|
||||
type: 'kerabValleyOverlapLine',
|
||||
isStart: true,
|
||||
planeSize: lsz,
|
||||
actualSize: lsz,
|
||||
},
|
||||
})
|
||||
ln.lineName = 'kerabValleyOverlapLine'
|
||||
ln.roofId = roofBase.id
|
||||
return ln
|
||||
}
|
||||
// [KERAB-VALLEY-OVERLAP-RECALC 2026-05-29] float drift 회피 — vStart 와 가장 가까운 polygon corner 로 snap.
|
||||
// 저장→로드 후엔 좌표 정수 round 로 일치하지만 저장 X 시 drift 가능 → split 알고리즘 외곽 인식 실패.
|
||||
const snapToCorner = (p) => {
|
||||
for (const rp of roofBase.points || []) {
|
||||
const d = Math.hypot(rp.x - p.x, rp.y - p.y)
|
||||
if (d < 1) return { x: rp.x, y: rp.y }
|
||||
}
|
||||
return p
|
||||
}
|
||||
const vStartSnap = snapToCorner(vStart)
|
||||
const wallLn = make(newWStart, wEndProj)
|
||||
const bsLn = make(vStartSnap, newWStart)
|
||||
const beLn = make(vEnd, wEndProj)
|
||||
roofBase.lines.push(wallLn, bsLn, beLn)
|
||||
roofBase.innerLines.push(wallLn, bsLn, beLn)
|
||||
}
|
||||
|
||||
if (roofBase.separatePolygon.length > 0) {
|
||||
splitPolygonWithSeparate(roofBase.separatePolygon)
|
||||
} else {
|
||||
|
||||
@ -9,6 +9,7 @@ import { fontSelector } from '@/store/fontAtom'
|
||||
import { QLine } from '@/components/fabric/QLine'
|
||||
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
|
||||
import { useLine } from '@/hooks/useLine'
|
||||
import { logger } from '@/util/logger'
|
||||
|
||||
export const usePolygon = () => {
|
||||
const canvas = useRecoilValue(canvasState)
|
||||
@ -744,6 +745,25 @@ export const usePolygon = () => {
|
||||
|
||||
let innerLines = [...polygon.innerLines].filter((line) => line.visible)
|
||||
|
||||
// [SPLIT-ALLOC-DIAG 2026-06-01] 분할 graph 진입 전 innerLines 전수 dump.
|
||||
// 케라바 패턴 라인(kerabPatternRidge/ExtRidge/Hip/ExtHip)과 원래 ridge(RG-2 등) 가
|
||||
// type='default' / isStart 미설정으로 graph 시드/유효점 후보에서 제외되는지 추적용.
|
||||
logger.log(`[SPLIT-ALLOC-DIAG] polygon.id=${polygon.id?.slice(0, 8)} innerLines.visible=${innerLines.length} polygonLines=${polygon.lines?.length ?? 0}`)
|
||||
innerLines.forEach((l, i) => {
|
||||
logger.log(
|
||||
` [SPLIT-ALLOC-DIAG ${i}] lineName=${l.lineName ?? 'none'} name=${l.name} ` +
|
||||
`type=${l.attributes?.type ?? 'none'} isStart=${l.attributes?.isStart ?? false} ` +
|
||||
`(${l.x1?.toFixed(1)},${l.y1?.toFixed(1)})→(${l.x2?.toFixed(1)},${l.y2?.toFixed(1)})`,
|
||||
)
|
||||
})
|
||||
;(polygon.lines || []).forEach((l, i) => {
|
||||
logger.log(
|
||||
` [SPLIT-ALLOC-DIAG outer ${i}] lineName=${l.lineName ?? 'none'} name=${l.name} ` +
|
||||
`type=${l.attributes?.type ?? 'none'} isStart=${l.attributes?.isStart ?? false} ` +
|
||||
`(${l.x1?.toFixed(1)},${l.y1?.toFixed(1)})→(${l.x2?.toFixed(1)},${l.y2?.toFixed(1)})`,
|
||||
)
|
||||
})
|
||||
|
||||
/*// innerLine이 세팅이 안되어있는경우 찾아서 세팅한다.
|
||||
if (!innerLines || innerLines.length === 0) {
|
||||
let innerLineTypes = Object.keys(LINE_TYPE.SUBLINE).map((key, value) => LINE_TYPE.SUBLINE[key])
|
||||
|
||||
@ -105,21 +105,119 @@ export const applyKerabOffsetSurgical = (canvas, target, newOffset) => {
|
||||
const nextSize = calcLinePlaneSize({ x1: nextRL.x1, y1: nextRL.y1, x2: nextRL.x2, y2: nextRL.y2 })
|
||||
nextRL.attributes = { ...nextRL.attributes, planeSize: nextSize, actualSize: nextSize }
|
||||
|
||||
// 옛 corner 좌표에 닿아있던 inner line(hip/ridge/valley/케라바 ext) 끝점을 새 corner 로 snap.
|
||||
const CORNER_SNAP_TOL = 0.5
|
||||
for (const il of roof.innerLines || []) {
|
||||
if (!il) continue
|
||||
if (Math.hypot(il.x1 - oldCorner1.x, il.y1 - oldCorner1.y) <= CORNER_SNAP_TOL) {
|
||||
il.set({ x1: newCorner1.x, y1: newCorner1.y })
|
||||
// [KERAB-OFFSET-SHRINK-TRIM 2026-06-01] 도메인 룰:
|
||||
// - 기본: skLine + ext 원본 좌표 보존.
|
||||
// - 원본 끝점이 새 roofLine 변 바깥(wall 쪽)에 있으면 새 roofLine 변과의 교점으로 절삭.
|
||||
// - 원본 끝점이 새 roofLine 변 안쪽으로 복귀하면 원본 좌표로 복원 (출폭 다시 늘린 케이스).
|
||||
// 원본 좌표는 il.__shrinkOrig 에 보관 — 첫 절삭 시점에 백업, 양 끝 모두 안쪽으로 복귀 시 삭제.
|
||||
// 매 surgical 호출마다 원본 기반으로 재계산하므로 출폭 증감 무관 idempotent.
|
||||
{
|
||||
const newAxisMid = { x: (newCorner1.x + newCorner2.x) / 2, y: (newCorner1.y + newCorner2.y) / 2 }
|
||||
const OUTSIDE_TOL = 0.5
|
||||
const segOk = (ip) => {
|
||||
const minX = Math.min(newCorner1.x, newCorner2.x) - OUTSIDE_TOL
|
||||
const maxX = Math.max(newCorner1.x, newCorner2.x) + OUTSIDE_TOL
|
||||
const minY = Math.min(newCorner1.y, newCorner2.y) - OUTSIDE_TOL
|
||||
const maxY = Math.max(newCorner1.y, newCorner2.y) + OUTSIDE_TOL
|
||||
return ip.x >= minX && ip.x <= maxX && ip.y >= minY && ip.y <= maxY
|
||||
}
|
||||
if (Math.hypot(il.x2 - oldCorner1.x, il.y2 - oldCorner1.y) <= CORNER_SNAP_TOL) {
|
||||
il.set({ x2: newCorner1.x, y2: newCorner1.y })
|
||||
// [KERAB-PATTERN-CORNER-SNAP 2026-06-01] 케라바 패턴 라인(kLine/ExtRidge/Hip/ExtHip)은
|
||||
// roofLine corner 변경에 따라 끝점도 같이 이동. corner 일치뿐 아니라 옛 segment 위의
|
||||
// 중간 점(예: kLine 끝점이 옛 roofLine 변 위)도 새 segment 의 동일 t 비율 점으로 매핑.
|
||||
const isKerabPatternLine = (il) =>
|
||||
il &&
|
||||
(il.lineName === 'kerabPatternRidge' ||
|
||||
il.lineName === 'kerabPatternExtRidge' ||
|
||||
il.lineName === 'kerabPatternHip' ||
|
||||
il.lineName === 'kerabPatternExtHip')
|
||||
const oldSegDx = oldCorner2.x - oldCorner1.x
|
||||
const oldSegDy = oldCorner2.y - oldCorner1.y
|
||||
const oldSegLen2 = oldSegDx * oldSegDx + oldSegDy * oldSegDy || 1
|
||||
const newSegDx = newCorner2.x - newCorner1.x
|
||||
const newSegDy = newCorner2.y - newCorner1.y
|
||||
const mapToNewSeg = (pt) => {
|
||||
const dx = pt.x - oldCorner1.x
|
||||
const dy = pt.y - oldCorner1.y
|
||||
const t = (dx * oldSegDx + dy * oldSegDy) / oldSegLen2
|
||||
const projX = oldCorner1.x + t * oldSegDx
|
||||
const projY = oldCorner1.y + t * oldSegDy
|
||||
const perpD = Math.hypot(pt.x - projX, pt.y - projY)
|
||||
if (perpD > 0.5) return null
|
||||
if (t < -0.05 || t > 1.05) return null
|
||||
return { x: newCorner1.x + t * newSegDx, y: newCorner1.y + t * newSegDy }
|
||||
}
|
||||
if (Math.hypot(il.x1 - oldCorner2.x, il.y1 - oldCorner2.y) <= CORNER_SNAP_TOL) {
|
||||
il.set({ x1: newCorner2.x, y1: newCorner2.y })
|
||||
}
|
||||
if (Math.hypot(il.x2 - oldCorner2.x, il.y2 - oldCorner2.y) <= CORNER_SNAP_TOL) {
|
||||
il.set({ x2: newCorner2.x, y2: newCorner2.y })
|
||||
for (const il of roof.innerLines || []) {
|
||||
if (!il) continue
|
||||
|
||||
// 케라바 패턴 라인: 옛 roofLine segment 위 끝점 → 새 segment 의 동일 t 점.
|
||||
// 절삭/복원 흐름 skip.
|
||||
if (isKerabPatternLine(il)) {
|
||||
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 (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 } }),
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
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 }
|
||||
const d1 = (orig1.x - newAxisMid.x) * nx + (orig1.y - newAxisMid.y) * ny
|
||||
const d2 = (orig2.x - newAxisMid.x) * nx + (orig2.y - newAxisMid.y) * ny
|
||||
|
||||
// 양 끝 모두 새 polygon 안쪽 → 원본 복원 + 백업 삭제
|
||||
if (d1 <= OUTSIDE_TOL && d2 <= OUTSIDE_TOL) {
|
||||
if (il.__shrinkOrig) {
|
||||
il.set({ x1: orig.x1, y1: orig.y1, x2: orig.x2, y2: orig.y2 })
|
||||
if (typeof il.setCoords === 'function') il.setCoords()
|
||||
logger.log(
|
||||
'[KERAB-OFFSET-SHRINK-TRIM] restored ' +
|
||||
JSON.stringify({ lineName: il.lineName, x1: orig.x1, y1: orig.y1, x2: orig.x2, y2: orig.y2 }),
|
||||
)
|
||||
delete il.__shrinkOrig
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 바깥 끝점 절삭 (원본 segment 와 새 roofLine 변의 교점)
|
||||
let nx1 = orig.x1
|
||||
let ny1 = orig.y1
|
||||
let nx2 = orig.x2
|
||||
let ny2 = orig.y2
|
||||
let trimmed = false
|
||||
if (d1 > OUTSIDE_TOL) {
|
||||
const ip = lineLineIntersection(orig1, orig2, newCorner1, newCorner2)
|
||||
if (ip && segOk(ip)) {
|
||||
nx1 = ip.x
|
||||
ny1 = ip.y
|
||||
trimmed = true
|
||||
}
|
||||
}
|
||||
if (d2 > OUTSIDE_TOL) {
|
||||
const ip = lineLineIntersection(orig1, orig2, newCorner1, newCorner2)
|
||||
if (ip && segOk(ip)) {
|
||||
nx2 = ip.x
|
||||
ny2 = ip.y
|
||||
trimmed = true
|
||||
}
|
||||
}
|
||||
if (!trimmed) continue
|
||||
if (!il.__shrinkOrig) {
|
||||
il.__shrinkOrig = { x1: orig.x1, y1: orig.y1, x2: orig.x2, y2: orig.y2 }
|
||||
}
|
||||
il.set({ x1: nx1, y1: ny1, x2: nx2, y2: ny2 })
|
||||
if (typeof il.setCoords === 'function') il.setCoords()
|
||||
logger.log(
|
||||
'[KERAB-OFFSET-SHRINK-TRIM] trimmed ' +
|
||||
JSON.stringify({ lineName: il.lineName, orig, newPts: { x1: nx1, y1: ny1, x2: nx2, y2: ny2 } }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user