Merge pull request 'dev' (#803) from dev into prd-deploy
Reviewed-on: #803
This commit is contained in:
commit
1428de4b3c
@ -56,6 +56,8 @@ export function useMovementSetting(id) {
|
|||||||
|
|
||||||
const CONFIRM_LINE_REF = useRef(null)
|
const CONFIRM_LINE_REF = useRef(null)
|
||||||
const FOLLOW_LINE_REF = useRef(null)
|
const FOLLOW_LINE_REF = useRef(null)
|
||||||
|
// [MV-TRACE] 직전 mouseMove 가 input 박스 갱신에 성공했는지. false 면 mouseDown 에서 wallbaseLine 미그리기.
|
||||||
|
const lastMoveValidRef = useRef(false)
|
||||||
|
|
||||||
/** 동선이동, 형이동 선택시 속성 처리*/
|
/** 동선이동, 형이동 선택시 속성 처리*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -253,23 +255,33 @@ export function useMovementSetting(id) {
|
|||||||
let currentCalculatedValue = 0
|
let currentCalculatedValue = 0
|
||||||
|
|
||||||
const mouseMoveEvent = (e) => {
|
const mouseMoveEvent = (e) => {
|
||||||
//console.log('mouseMoveEvent:::::',e)
|
// [MV-TRACE local]
|
||||||
|
const __isLocalMV = process.env.NEXT_PUBLIC_RUN_MODE === 'local'
|
||||||
|
|
||||||
// 기존에는 activeObject를 사용했으나, 이 기능에서는 선택된 라인을 비선택(selectable:false) 상태로 두므로
|
// 기존에는 activeObject를 사용했으나, 이 기능에서는 선택된 라인을 비선택(selectable:false) 상태로 두므로
|
||||||
// 항상 selectedObject.current를 기준으로 계산한다.
|
// 항상 selectedObject.current를 기준으로 계산한다.
|
||||||
const target = selectedObject.current
|
const target = selectedObject.current
|
||||||
if (!target) return
|
if (!target) {
|
||||||
|
if (__isLocalMV) console.warn('[MV-TRACE] selectedObject.current=null → skip')
|
||||||
// 디버깅 로그 추가
|
lastMoveValidRef.current = false
|
||||||
// if (typeRef.current === TYPE.UP_DOWN) {
|
return
|
||||||
// console.log('UP_DOWN_REF.POINTER_INPUT_REF.current:', UP_DOWN_REF.POINTER_INPUT_REF.current);
|
}
|
||||||
// if (!UP_DOWN_REF.POINTER_INPUT_REF.current) {
|
|
||||||
// console.warn('UP_DOWN_REF.POINTER_INPUT_REF.current is null/undefined');
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const { top: targetTop, left: targetLeft } = target
|
const { top: targetTop, left: targetLeft } = target
|
||||||
const currentX = Big(getIntersectMousePoint(e).x)
|
const __mp = getIntersectMousePoint(e) || {}
|
||||||
const currentY = Big(getIntersectMousePoint(e).y)
|
const __mx = __mp.x
|
||||||
|
const __my = __mp.y
|
||||||
|
if (!Number.isFinite(targetTop) || !Number.isFinite(targetLeft) || !Number.isFinite(__mx) || !Number.isFinite(__my)) {
|
||||||
|
if (__isLocalMV) {
|
||||||
|
console.warn(
|
||||||
|
`[MV-TRACE] 좌표 무효 → skip targetTop=${targetTop} targetLeft=${targetLeft} mx=${__mx} my=${__my}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
lastMoveValidRef.current = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const currentX = Big(__mx)
|
||||||
|
const currentY = Big(__my)
|
||||||
|
|
||||||
let value = ''
|
let value = ''
|
||||||
let direction = ''
|
let direction = ''
|
||||||
@ -315,15 +327,20 @@ export function useMovementSetting(id) {
|
|||||||
|
|
||||||
currentCalculatedValue = value.toNumber()
|
currentCalculatedValue = value.toNumber()
|
||||||
|
|
||||||
|
let __inputUpdated = false
|
||||||
if (typeRef.current === TYPE.FLOW_LINE) {
|
if (typeRef.current === TYPE.FLOW_LINE) {
|
||||||
// ref가 존재하는지 확인 후 값 설정
|
|
||||||
if (FLOW_LINE_REF.POINTER_INPUT_REF.current) {
|
if (FLOW_LINE_REF.POINTER_INPUT_REF.current) {
|
||||||
FLOW_LINE_REF.POINTER_INPUT_REF.current.value = value.toNumber()
|
FLOW_LINE_REF.POINTER_INPUT_REF.current.value = value.toNumber()
|
||||||
|
__inputUpdated = true
|
||||||
|
} else if (__isLocalMV) {
|
||||||
|
console.warn('[MV-TRACE] FLOW_LINE.POINTER_INPUT_REF.current=null → input 갱신 실패')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// UP_DOWN 타입일 때 안전한 접근
|
|
||||||
if (UP_DOWN_REF.POINTER_INPUT_REF.current) {
|
if (UP_DOWN_REF.POINTER_INPUT_REF.current) {
|
||||||
UP_DOWN_REF.POINTER_INPUT_REF.current.value = value.abs().toNumber()
|
UP_DOWN_REF.POINTER_INPUT_REF.current.value = value.abs().toNumber()
|
||||||
|
__inputUpdated = true
|
||||||
|
} else if (__isLocalMV) {
|
||||||
|
console.warn('[MV-TRACE] UP_DOWN.POINTER_INPUT_REF.current=null → input 갱신 실패')
|
||||||
}
|
}
|
||||||
|
|
||||||
const midX = Big(target.x1).plus(target.x2).div(2)
|
const midX = Big(target.x1).plus(target.x2).div(2)
|
||||||
@ -422,6 +439,16 @@ export function useMovementSetting(id) {
|
|||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [MV-TRACE] 이번 mouseMove 가 input 박스 갱신에 성공했는지 기록.
|
||||||
|
// 실패 시 mouseDownEvent 가 wallbaseLine 그리기/handleSave 를 차단.
|
||||||
|
lastMoveValidRef.current = __inputUpdated
|
||||||
|
if (__isLocalMV) {
|
||||||
|
console.log(
|
||||||
|
`[MV-TRACE] type=${typeRef.current} value=${value?.toString?.()} dir=${direction} ` +
|
||||||
|
`inputUpdated=${__inputUpdated} ` +
|
||||||
|
`selectedObject=${selectedObject.current ? 'set' : 'null'}`
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mouseDownEvent = (e) => {
|
const mouseDownEvent = (e) => {
|
||||||
@ -431,10 +458,17 @@ export function useMovementSetting(id) {
|
|||||||
.forEach((obj) => canvas.remove(obj))
|
.forEach((obj) => canvas.remove(obj))
|
||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
|
|
||||||
//const target = selectedObject.current
|
|
||||||
const target = selectedObject.current
|
const target = selectedObject.current
|
||||||
if (!target) return
|
if (!target) return
|
||||||
|
|
||||||
|
// [MV-TRACE] 직전 mouseMove 가 input 박스 갱신에 실패했다면 wallbaseLine 미그리기.
|
||||||
|
if (!lastMoveValidRef.current) {
|
||||||
|
if (process.env.NEXT_PUBLIC_RUN_MODE === 'local') {
|
||||||
|
console.warn('[MV-TRACE] mouseDown abort: 직전 mouseMove input 갱신 실패 → wallbaseLine 미그리기')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const roofId = target.attributes.roofId
|
const roofId = target.attributes.roofId
|
||||||
const followLine = canvas.getObjects().find((obj) => obj.name === 'followLine')
|
const followLine = canvas.getObjects().find((obj) => obj.name === 'followLine')
|
||||||
|
|
||||||
@ -723,35 +757,78 @@ export function useMovementSetting(id) {
|
|||||||
let linePosition = result.position
|
let linePosition = result.position
|
||||||
roof.movePosition = linePosition
|
roof.movePosition = linePosition
|
||||||
value = value.div(10)
|
value = value.div(10)
|
||||||
|
// [BR-TRACE local] handleSave 분기 진입 컨텍스트
|
||||||
|
const __isLocal = process.env.NEXT_PUBLIC_RUN_MODE === 'local'
|
||||||
|
if (__isLocal) {
|
||||||
|
console.log(
|
||||||
|
`[BR-TRACE] handleSave.enter linePosition=${linePosition} ` +
|
||||||
|
`roof.moveDirect=${roof.moveDirect} ` +
|
||||||
|
`UP.checked=${!!UP_DOWN_REF?.UP_RADIO_REF?.current?.checked} ` +
|
||||||
|
`DOWN.checked=${!!UP_DOWN_REF?.DOWN_RADIO_REF?.current?.checked} ` +
|
||||||
|
`target=(${target.x1},${target.y1})-(${target.x2},${target.y2}) ` +
|
||||||
|
`targetBaseLines=${targetBaseLines.length}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [FIX-1] linePosition='unknown' (또는 없음) 이면 부호 결정 불가 → 이동 자체 스킵.
|
||||||
|
// 기존: unknown 일 때 negation 안 거치고 부호 그대로 들어가 in/out 의도와 반대로 적용되는 case 발생.
|
||||||
|
if (linePosition !== 'top' && linePosition !== 'bottom' && linePosition !== 'left' && linePosition !== 'right') {
|
||||||
|
if (__isLocal) console.warn(`[BR-TRACE] linePosition=${linePosition} → 이동 스킵 (early return)`)
|
||||||
|
roof.drawHelpLine()
|
||||||
|
initEvent()
|
||||||
|
closePopup(id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 클로저 격리용: value 는 단일 사실(원본 부호) 로 두고, iter 마다 localValue 로만 부호 결정.
|
||||||
|
const __valueOriginal = value
|
||||||
|
const __targetIsHoriz = (linePosition === 'top' || linePosition === 'bottom')
|
||||||
targetBaseLines
|
targetBaseLines
|
||||||
.filter((line) => Math.sqrt(Math.pow(line.line.x2 - line.line.x1, 2) + Math.pow(line.line.y2 - line.line.y1, 2)) >= 1)
|
.filter((line) => Math.sqrt(Math.pow(line.line.x2 - line.line.x1, 2) + Math.pow(line.line.y2 - line.line.y1, 2)) >= 1)
|
||||||
.forEach((target) => {
|
.forEach((targetItem, __iter) => {
|
||||||
const currentLine = target.line
|
const currentLine = targetItem.line
|
||||||
|
const __horizCurr = currentLine.y1 === currentLine.y2
|
||||||
|
|
||||||
//console.log("linePosition::::::::::::::", linePosition)
|
// [FIX-2] mixed-orientation 가드: linePosition 가족과 currentLine 방향 불일치 → 이 라인은 스킵.
|
||||||
|
// gable 보강 push (zeroLengthLines.forEach) 가 좌표 매칭만으로 인접 라인을 추가하면서
|
||||||
|
// 가로/세로 혼재된 targetBaseLines 가 만들어지는 경우 방어.
|
||||||
|
if (__horizCurr !== __targetIsHoriz) {
|
||||||
|
if (__isLocal) {
|
||||||
|
console.warn(
|
||||||
|
`[BR-TRACE] iter=${__iter} ⚠️ 방향 불일치 skip: currentLine ${__horizCurr ? '수평' : '수직'} ` +
|
||||||
|
`↔ linePosition=${linePosition} (예상 ${__targetIsHoriz ? '수평' : '수직'})`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// [FIX-3] 클로저 격리: 외부 value 변이 금지. iter 마다 localValue 로만 부호 결정.
|
||||||
|
let localValue = __valueOriginal
|
||||||
if (UP_DOWN_REF?.DOWN_RADIO_REF?.current?.checked) {
|
if (UP_DOWN_REF?.DOWN_RADIO_REF?.current?.checked) {
|
||||||
//position확인
|
|
||||||
if (linePosition === 'bottom' || linePosition === 'right') {
|
if (linePosition === 'bottom' || linePosition === 'right') {
|
||||||
//console.log("1value::::::::::::::", value.toString())
|
localValue = localValue.neg()
|
||||||
value = value.neg()
|
if (__isLocal) console.log(`[BR-TRACE] iter=${__iter} DOWN+(${linePosition}) → localValue.neg() ${__valueOriginal.toString()}→${localValue.toString()}`)
|
||||||
|
} else if (__isLocal) {
|
||||||
|
console.log(`[BR-TRACE] iter=${__iter} DOWN no-neg (linePosition=${linePosition}) localValue=${localValue.toString()}`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (linePosition === 'top' || linePosition === 'left') {
|
if (linePosition === 'top' || linePosition === 'left') {
|
||||||
//console.log("1value::::::::::::::", value.toString())
|
localValue = localValue.neg()
|
||||||
value = value.neg()
|
if (__isLocal) console.log(`[BR-TRACE] iter=${__iter} UP+(${linePosition}) → localValue.neg() ${__valueOriginal.toString()}→${localValue.toString()}`)
|
||||||
|
} else if (__isLocal) {
|
||||||
|
console.log(`[BR-TRACE] iter=${__iter} UP no-neg (linePosition=${linePosition}) localValue=${localValue.toString()}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log("2value::::::::::::::", value.toString())
|
|
||||||
const index = baseLines.findIndex((line) => line === currentLine)
|
const index = baseLines.findIndex((line) => line === currentLine)
|
||||||
const nextLine = baseLines[(index + 1) % baseLines.length]
|
const nextLine = baseLines[(index + 1) % baseLines.length]
|
||||||
const prevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length]
|
const prevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length]
|
||||||
let deltaX = 0
|
let deltaX = 0
|
||||||
let deltaY = 0
|
let deltaY = 0
|
||||||
if (currentLine.y1 === currentLine.y2) {
|
if (__horizCurr) {
|
||||||
deltaY = value.toNumber()
|
deltaY = localValue.toNumber()
|
||||||
|
if (__isLocal) console.log(`[BR-TRACE] iter=${__iter} 수평 currentLine idx=${index} deltaY=${deltaY}`)
|
||||||
} else {
|
} else {
|
||||||
deltaX = value.toNumber()
|
deltaX = localValue.toNumber()
|
||||||
|
if (__isLocal) console.log(`[BR-TRACE] iter=${__iter} 수직 currentLine idx=${index} deltaX=${deltaX}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// [OVER_GUARD] 오버 이동(인접 라인 반전) 처리. 정책은 파일 상단 OVER_MOVE_POLICY 로 전환.
|
// [OVER_GUARD] 오버 이동(인접 라인 반전) 처리. 정책은 파일 상단 OVER_MOVE_POLICY 로 전환.
|
||||||
|
|||||||
@ -436,12 +436,141 @@ export function useRoofAllocationSetting(id) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* extensionLine + 동일 직선상 SK(HIP/RIDGE/VALLEY) 1:1 통합.
|
||||||
|
* 빨강 보조선(extensionLine)과 baseLine 코너에서 같은 방향으로 뻗어나가는 SK 라인을
|
||||||
|
* 한 QLine 으로 합쳐서 roofBase.innerLines 에 둔다.
|
||||||
|
* - 대각선 길이가 단일값으로 산출됨
|
||||||
|
* - 각도 기반 면적 계산이 한 라인 단위로 가능
|
||||||
|
* - splitPolygonWithLines 의 graph 토폴로지가 정상 연결됨
|
||||||
|
* 호출 위치: apply() 내 roofBase.lines 병합 직후, split 직전.
|
||||||
|
*/
|
||||||
|
const integrateExtensionLines = (roofBase) => {
|
||||||
|
if (!roofBase?.lines || !Array.isArray(roofBase.innerLines)) return
|
||||||
|
|
||||||
|
const extLines = roofBase.lines.filter((l) => l.lineName === 'extensionLine')
|
||||||
|
if (extLines.length === 0) {
|
||||||
|
console.log(`[INTEGRATE] roofBase.id=${roofBase.id} extensionLine 없음 → skip`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOL = 1.0
|
||||||
|
const isSamePt = (p1, p2) => Math.hypot(p1.x - p2.x, p1.y - p2.y) < TOL
|
||||||
|
const isCollinear = (l1, l2) => {
|
||||||
|
const v1x = l1.x2 - l1.x1, v1y = l1.y2 - l1.y1
|
||||||
|
const v2x = l2.x2 - l2.x1, v2y = l2.y2 - l2.y1
|
||||||
|
const m1 = Math.hypot(v1x, v1y) || 1
|
||||||
|
const m2 = Math.hypot(v2x, v2y) || 1
|
||||||
|
const cross = (v1x * v2y - v1y * v2x) / (m1 * m2)
|
||||||
|
return Math.abs(cross) < 0.02 // ~1.1°
|
||||||
|
}
|
||||||
|
|
||||||
|
const removedExt = []
|
||||||
|
const removedSk = []
|
||||||
|
const merged = []
|
||||||
|
|
||||||
|
extLines.forEach((ext) => {
|
||||||
|
const extP1 = { x: ext.x1, y: ext.y1 }
|
||||||
|
const extP2 = { x: ext.x2, y: ext.y2 }
|
||||||
|
|
||||||
|
const sk = roofBase.innerLines.find((sl) => {
|
||||||
|
if (removedSk.includes(sl)) return false
|
||||||
|
const skP1 = { x: sl.x1, y: sl.y1 }
|
||||||
|
const skP2 = { x: sl.x2, y: sl.y2 }
|
||||||
|
const sharesP1 = isSamePt(extP1, skP1) || isSamePt(extP1, skP2)
|
||||||
|
const sharesP2 = isSamePt(extP2, skP1) || isSamePt(extP2, skP2)
|
||||||
|
if ((sharesP1 ? 1 : 0) + (sharesP2 ? 1 : 0) !== 1) return false
|
||||||
|
return isCollinear(ext, sl)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!sk) {
|
||||||
|
console.log(
|
||||||
|
`[INTEGRATE] ext 짝없음 ` +
|
||||||
|
`(${extP1.x.toFixed(1)},${extP1.y.toFixed(1)})→(${extP2.x.toFixed(1)},${extP2.y.toFixed(1)})`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const skP1 = { x: sk.x1, y: sk.y1 }
|
||||||
|
const skP2 = { x: sk.x2, y: sk.y2 }
|
||||||
|
const sharedPt = (isSamePt(extP1, skP1) || isSamePt(extP1, skP2)) ? extP1 : extP2
|
||||||
|
const extOuter = isSamePt(extP1, sharedPt) ? extP2 : extP1
|
||||||
|
const skOuter = isSamePt(skP1, sharedPt) ? skP2 : skP1
|
||||||
|
|
||||||
|
const extLen = Math.hypot(extP2.x - extP1.x, extP2.y - extP1.y)
|
||||||
|
const skLen = Math.hypot(skP2.x - skP1.x, skP2.y - skP1.y)
|
||||||
|
const totalLen = extLen + skLen
|
||||||
|
|
||||||
|
// SK 의 planeSize/actualSize 를 비율로 확장 (SK 길이가 0 이면 그대로)
|
||||||
|
// [정밀도 0.01 유지] 저장값 소수점 2자리 (면적 계산 정확도). 표시는 addLengthText 에서 round.
|
||||||
|
const skPlane = sk.attributes?.planeSize ?? 0
|
||||||
|
const skActual = sk.attributes?.actualSize ?? 0
|
||||||
|
const ratio = skLen > 0 ? totalLen / skLen : 1
|
||||||
|
const newPlane = Math.round(skPlane * ratio * 100) / 100
|
||||||
|
const newActual = Math.round(skActual * ratio * 100) / 100
|
||||||
|
|
||||||
|
const mergedLine = new QLine([extOuter.x, extOuter.y, skOuter.x, skOuter.y], {
|
||||||
|
parentId: sk.parentId,
|
||||||
|
parent: sk.parent,
|
||||||
|
stroke: sk.stroke,
|
||||||
|
strokeWidth: sk.strokeWidth,
|
||||||
|
fontSize: sk.fontSize,
|
||||||
|
visible: sk.visible,
|
||||||
|
selectable: sk.selectable,
|
||||||
|
name: sk.name,
|
||||||
|
lineName: sk.lineName,
|
||||||
|
direction: sk.direction,
|
||||||
|
roofId: sk.roofId,
|
||||||
|
attributes: {
|
||||||
|
...(sk.attributes || {}),
|
||||||
|
planeSize: newPlane,
|
||||||
|
actualSize: newActual,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
mergedLine.length = totalLen
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[INTEGRATE] merge ` +
|
||||||
|
`ext=(${extP1.x.toFixed(1)},${extP1.y.toFixed(1)})→(${extP2.x.toFixed(1)},${extP2.y.toFixed(1)}) len=${extLen.toFixed(1)} ` +
|
||||||
|
`sk[${sk.lineName || sk.name}]=(${skP1.x.toFixed(1)},${skP1.y.toFixed(1)})→(${skP2.x.toFixed(1)},${skP2.y.toFixed(1)}) len=${skLen.toFixed(1)} ` +
|
||||||
|
`→ total=${totalLen.toFixed(1)} (plane ${skPlane}→${newPlane}, actual ${skActual}→${newActual})`
|
||||||
|
)
|
||||||
|
|
||||||
|
removedExt.push(ext)
|
||||||
|
removedSk.push(sk)
|
||||||
|
merged.push(mergedLine)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (merged.length === 0) {
|
||||||
|
console.log(`[INTEGRATE] 통합 대상 없음 ext=${extLines.length}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
roofBase.lines = roofBase.lines.filter((l) => !removedExt.includes(l))
|
||||||
|
roofBase.innerLines = roofBase.innerLines.filter((l) => !removedSk.includes(l))
|
||||||
|
roofBase.innerLines = [...roofBase.innerLines, ...merged]
|
||||||
|
|
||||||
|
// canvas 정리:
|
||||||
|
// - 원본 ext 제거
|
||||||
|
// - 원본 sk 도 canvas 에서 제거 (innerLines 배열에서 빠졌으므로 apply() 끝의 일괄 제거가 닿지 않음)
|
||||||
|
// - 둘 다 남기면 라벨/라인이 중복 표시됨
|
||||||
|
removedExt.forEach((l) => canvas.remove(l))
|
||||||
|
removedSk.forEach((l) => canvas.remove(l))
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[INTEGRATE] 완료 roofBase.id=${roofBase.id} ` +
|
||||||
|
`ext제거=${removedExt.length} sk제거=${removedSk.length} merged=${merged.length} ` +
|
||||||
|
`→ lines=${roofBase.lines.length} innerLines=${roofBase.innerLines.length}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 지붕면 할당
|
* 지붕면 할당
|
||||||
*/
|
*/
|
||||||
const apply = () => {
|
const apply = () => {
|
||||||
const roofBases = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF && !obj.roofMaterial)
|
const roofBases = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF && !obj.roofMaterial)
|
||||||
const wallLines = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL)
|
const wallLines = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL)
|
||||||
|
console.log(`[ALLOC] apply() 진입. roofBases=${roofBases.length}`)
|
||||||
roofBases.forEach((roofBase) => {
|
roofBases.forEach((roofBase) => {
|
||||||
try {
|
try {
|
||||||
// 지붕 할당 로직에 extensionLine 추가
|
// 지붕 할당 로직에 extensionLine 추가
|
||||||
@ -449,6 +578,28 @@ export function useRoofAllocationSetting(id) {
|
|||||||
(obj.lineName === 'eaveHelpLine' || obj.lineName === 'extensionLine') &&
|
(obj.lineName === 'eaveHelpLine' || obj.lineName === 'extensionLine') &&
|
||||||
obj.roofId === roofBase.id
|
obj.roofId === roofBase.id
|
||||||
)
|
)
|
||||||
|
// [ALLOC-DEBUG] canvas 픽업 스냅샷 (삭제 직전 상태)
|
||||||
|
const snapByLineName = {}
|
||||||
|
const snapByName = {}
|
||||||
|
roofEaveHelpLines.forEach((o) => {
|
||||||
|
snapByLineName[o.lineName] = (snapByLineName[o.lineName] || 0) + 1
|
||||||
|
snapByName[o.name || '?'] = (snapByName[o.name || '?'] || 0) + 1
|
||||||
|
})
|
||||||
|
console.log(
|
||||||
|
`[ALLOC-DEBUG] roofBase.id=${roofBase.id} ` +
|
||||||
|
`roofBase.lines=${roofBase.lines?.length || 0} ` +
|
||||||
|
`roofBase.innerLines=${roofBase.innerLines?.length || 0} ` +
|
||||||
|
`canvas픽업=${roofEaveHelpLines.length} ` +
|
||||||
|
`byLineName=${JSON.stringify(snapByLineName)} ` +
|
||||||
|
`byName=${JSON.stringify(snapByName)}`
|
||||||
|
)
|
||||||
|
roofEaveHelpLines.forEach((o, i) => {
|
||||||
|
console.log(
|
||||||
|
` [ALLOC-DEBUG] picked[${i}] name=${o.name} lineName=${o.lineName} ` +
|
||||||
|
`(${o.x1?.toFixed(1)},${o.y1?.toFixed(1)})→(${o.x2?.toFixed(1)},${o.y2?.toFixed(1)}) ` +
|
||||||
|
`len=${Math.hypot((o.x2 || 0) - (o.x1 || 0), (o.y2 || 0) - (o.y1 || 0)).toFixed(1)}`
|
||||||
|
)
|
||||||
|
})
|
||||||
// console.log('roofBase.id:', roofBase.id)
|
// console.log('roofBase.id:', roofBase.id)
|
||||||
// console.log('roofEaveHelpLines:', roofEaveHelpLines)
|
// console.log('roofEaveHelpLines:', roofEaveHelpLines)
|
||||||
// console.log('extensionLines found:', roofEaveHelpLines.filter(l => l.lineName === 'extensionLine'))
|
// console.log('extensionLines found:', roofEaveHelpLines.filter(l => l.lineName === 'extensionLine'))
|
||||||
@ -517,8 +668,20 @@ export function useRoofAllocationSetting(id) {
|
|||||||
});
|
});
|
||||||
// Combine remaining lines with newEaveLines
|
// Combine remaining lines with newEaveLines
|
||||||
roofBase.lines = [...linesToKeep, ...normalEaveLines, ...extensionLines];
|
roofBase.lines = [...linesToKeep, ...normalEaveLines, ...extensionLines];
|
||||||
// console.log('Final roofBase.lines count:', roofBase.lines.length)
|
// [ALLOC-DEBUG] 병합 결과 breakdown
|
||||||
// console.log('extensionLines in final:', roofBase.lines.filter(l => l.lineName === 'extensionLine').length)
|
console.log(
|
||||||
|
`[ALLOC-DEBUG] 병합완료 roofBase.id=${roofBase.id} ` +
|
||||||
|
`linesToKeep=${linesToKeep.length} ` +
|
||||||
|
`normalEave=${normalEaveLines.length} ` +
|
||||||
|
`extension=${extensionLines.length} ` +
|
||||||
|
`total=${roofBase.lines.length}`
|
||||||
|
)
|
||||||
|
roofBase.lines.forEach((ln, i) => {
|
||||||
|
console.log(
|
||||||
|
` [ALLOC-DEBUG] merged[${i}] name=${ln.name || '?'} lineName=${ln.lineName || '?'} ` +
|
||||||
|
`(${ln.x1?.toFixed(1)},${ln.y1?.toFixed(1)})→(${ln.x2?.toFixed(1)},${ln.y2?.toFixed(1)})`
|
||||||
|
)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
roofBase.lines = [...roofEaveHelpLines]
|
roofBase.lines = [...roofEaveHelpLines]
|
||||||
}
|
}
|
||||||
@ -555,6 +718,18 @@ export function useRoofAllocationSetting(id) {
|
|||||||
})
|
})
|
||||||
roofBase.lines = newRoofLines
|
roofBase.lines = newRoofLines
|
||||||
}
|
}
|
||||||
|
// extensionLine + 동일직선 SK 1:1 통합 (대각선 단일길이/각도 면적 산출)
|
||||||
|
integrateExtensionLines(roofBase)
|
||||||
|
|
||||||
|
// [ALLOC-DEBUG] split 직전 최종 입력
|
||||||
|
console.log(
|
||||||
|
`[ALLOC-DEBUG] split 직전 roofBase.id=${roofBase.id} ` +
|
||||||
|
`separatePolygon=${roofBase.separatePolygon?.length || 0} ` +
|
||||||
|
`lines=${roofBase.lines?.length || 0} ` +
|
||||||
|
`innerLines=${roofBase.innerLines?.length || 0} ` +
|
||||||
|
`points=${roofBase.points?.length || 0} ` +
|
||||||
|
`→ ${roofBase.separatePolygon?.length > 0 ? 'splitPolygonWithSeparate' : 'splitPolygonWithLines'}`
|
||||||
|
)
|
||||||
if (roofBase.separatePolygon.length > 0) {
|
if (roofBase.separatePolygon.length > 0) {
|
||||||
splitPolygonWithSeparate(roofBase.separatePolygon)
|
splitPolygonWithSeparate(roofBase.separatePolygon)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -16,6 +16,24 @@ import wallLine from '@/components/floor-plan/modal/wallLineOffset/type/WallLine
|
|||||||
*/
|
*/
|
||||||
const EPSILON = 0.1
|
const EPSILON = 0.1
|
||||||
|
|
||||||
|
// [v1 helper 2026-04-29] B 경로: SK 입력은 그대로 두고, baseLine corner → roofLine corner 까지의
|
||||||
|
// 시각 보조 라인만 추가. processInBoth/processInStartEnd 등 working code 무손상.
|
||||||
|
// innerLines/skeletonLines 등 기존 collection 에 push 하지 않음 (canvas add 만).
|
||||||
|
// 사용자 리셋 명령 없이 리셋되지 않도록 누적 baseLines/roofLines 좌표 그대로 사용.
|
||||||
|
// 롤백: false 로 toggle.
|
||||||
|
const DRAW_BASELINE_TO_ROOFLINE_HELPER = true
|
||||||
|
const BASELINE_TO_ROOFLINE_HELPER_TYPE = 'sk_to_roofline_helper'
|
||||||
|
|
||||||
|
// [v1 architecture 2026-04-29] SK 입력 = wall.baseLines 직접 (offset 확장/movedPoints 사용 안 함).
|
||||||
|
// 사용자 요청: "마루이동/벽이동/offset 으로 wall.baseLines 가 변하면 SK 도 변해야 한다.
|
||||||
|
// SK 빌더에는 항상 wall.baseLines 를 넘겨야 함. 그 다음 확장선 그림".
|
||||||
|
// changRoofLinePoints = orderedBaseLinePoints (= createOrderedBasePoints(roof.points, wall.baseLines))
|
||||||
|
// 기존 45° 확장 / safeMovedPointsWithFallback 결과는 모두 무시.
|
||||||
|
// 이렇게 하면 wall.baseLines 의 모든 누적 변경(마루이동/벽이동/offset)이 SK 에 자동 반영.
|
||||||
|
// 확장선(B 경로 helper) 이 baseLine corner → roofLine 까지 시각화 담당.
|
||||||
|
// 롤백: false → 기존 HEAD 동작 (45° 확장 + movedPoints).
|
||||||
|
const SK_INPUT_USE_WALL_BASELINE_DIRECT = true
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 오목(concave) 폴리곤인지 판별합니다.
|
* 오목(concave) 폴리곤인지 판별합니다.
|
||||||
* cross product의 부호가 혼재하면 오목 폴리곤입니다.
|
* cross product의 부호가 혼재하면 오목 폴리곤입니다.
|
||||||
@ -238,21 +256,31 @@ const movingLineFromSkeleton = (roofId, canvas) => {
|
|||||||
affected.add((k + 1) % nPts);
|
affected.add((k + 1) % nPts);
|
||||||
});
|
});
|
||||||
console.log('absMove::', moveUpDownLength);
|
console.log('absMove::', moveUpDownLength);
|
||||||
|
// [BR-TRACE local] movingLineFromSkeleton position/direction 분기
|
||||||
|
const __isLocalMS = process.env.NEXT_PUBLIC_RUN_MODE === 'local'
|
||||||
|
if (__isLocalMS) console.log(`[BR-TRACE] movingLineFromSkeleton position=${position} dir=${moveDirection} affected=${[...affected].join(',')}`)
|
||||||
affected.forEach((i) => {
|
affected.forEach((i) => {
|
||||||
const point = newPoints[i];
|
const point = newPoints[i];
|
||||||
if (!point) return;
|
if (!point) return;
|
||||||
|
const __bx = point.x, __by = point.y;
|
||||||
if (position === 'bottom') {
|
if (position === 'bottom') {
|
||||||
if (moveDirection === 'in') point.y = Big(point.y).minus(moveUpDownLength).toNumber();
|
if (moveDirection === 'in') point.y = Big(point.y).minus(moveUpDownLength).toNumber();
|
||||||
else if (moveDirection === 'out') point.y = Big(point.y).plus(moveUpDownLength).toNumber();
|
else if (moveDirection === 'out') point.y = Big(point.y).plus(moveUpDownLength).toNumber();
|
||||||
|
if (__isLocalMS) console.log(`[BR-TRACE] i=${i} bottom/${moveDirection} y ${__by}→${point.y}`)
|
||||||
} else if (position === 'top') {
|
} else if (position === 'top') {
|
||||||
if (moveDirection === 'in') point.y = Big(point.y).plus(moveUpDownLength).toNumber();
|
if (moveDirection === 'in') point.y = Big(point.y).plus(moveUpDownLength).toNumber();
|
||||||
else if (moveDirection === 'out') point.y = Big(point.y).minus(moveUpDownLength).toNumber();
|
else if (moveDirection === 'out') point.y = Big(point.y).minus(moveUpDownLength).toNumber();
|
||||||
|
if (__isLocalMS) console.log(`[BR-TRACE] i=${i} top/${moveDirection} y ${__by}→${point.y}`)
|
||||||
} else if (position === 'left') {
|
} else if (position === 'left') {
|
||||||
if (moveDirection === 'in') point.x = Big(point.x).plus(moveUpDownLength).toNumber();
|
if (moveDirection === 'in') point.x = Big(point.x).plus(moveUpDownLength).toNumber();
|
||||||
else if (moveDirection === 'out') point.x = Big(point.x).minus(moveUpDownLength).toNumber();
|
else if (moveDirection === 'out') point.x = Big(point.x).minus(moveUpDownLength).toNumber();
|
||||||
|
if (__isLocalMS) console.log(`[BR-TRACE] i=${i} left/${moveDirection} x ${__bx}→${point.x}`)
|
||||||
} else if (position === 'right') {
|
} else if (position === 'right') {
|
||||||
if (moveDirection === 'in') point.x = Big(point.x).minus(moveUpDownLength).toNumber();
|
if (moveDirection === 'in') point.x = Big(point.x).minus(moveUpDownLength).toNumber();
|
||||||
else if (moveDirection === 'out') point.x = Big(point.x).plus(moveUpDownLength).toNumber();
|
else if (moveDirection === 'out') point.x = Big(point.x).plus(moveUpDownLength).toNumber();
|
||||||
|
if (__isLocalMS) console.log(`[BR-TRACE] i=${i} right/${moveDirection} x ${__bx}→${point.x}`)
|
||||||
|
} else if (__isLocalMS) {
|
||||||
|
console.warn(`[BR-TRACE] i=${i} position=${position} 매칭 분기 없음 (이동 안 됨)`)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -416,21 +444,31 @@ const buildRawMovedPoints = (roofId, canvas) => {
|
|||||||
affected.add(idx)
|
affected.add(idx)
|
||||||
affected.add((idx + 1) % nPts)
|
affected.add((idx + 1) % nPts)
|
||||||
})
|
})
|
||||||
|
// [BR-TRACE local] buildRawMovedPoints position/direction 분기
|
||||||
|
const __isLocalBR = process.env.NEXT_PUBLIC_RUN_MODE === 'local'
|
||||||
|
if (__isLocalBR) console.log(`[BR-TRACE] buildRawMovedPoints position=${position} dir=${moveDirection} affected=${[...affected].join(',')}`)
|
||||||
affected.forEach((i) => {
|
affected.forEach((i) => {
|
||||||
const point = newPoints[i]
|
const point = newPoints[i]
|
||||||
if (!point) return
|
if (!point) return
|
||||||
|
const __bx = point.x, __by = point.y
|
||||||
if (position === 'bottom') {
|
if (position === 'bottom') {
|
||||||
if (moveDirection === 'out') point.y = Big(point.y).plus(moveUpDownLength).toNumber()
|
if (moveDirection === 'out') point.y = Big(point.y).plus(moveUpDownLength).toNumber()
|
||||||
else if (moveDirection === 'in') point.y = Big(point.y).minus(moveUpDownLength).toNumber()
|
else if (moveDirection === 'in') point.y = Big(point.y).minus(moveUpDownLength).toNumber()
|
||||||
|
if (__isLocalBR) console.log(`[BR-TRACE] i=${i} bottom/${moveDirection} y ${__by}→${point.y}`)
|
||||||
} else if (position === 'top') {
|
} else if (position === 'top') {
|
||||||
if (moveDirection === 'out') point.y = Big(point.y).minus(moveUpDownLength).toNumber()
|
if (moveDirection === 'out') point.y = Big(point.y).minus(moveUpDownLength).toNumber()
|
||||||
else if (moveDirection === 'in') point.y = Big(point.y).plus(moveUpDownLength).toNumber()
|
else if (moveDirection === 'in') point.y = Big(point.y).plus(moveUpDownLength).toNumber()
|
||||||
|
if (__isLocalBR) console.log(`[BR-TRACE] i=${i} top/${moveDirection} y ${__by}→${point.y}`)
|
||||||
} else if (position === 'left') {
|
} else if (position === 'left') {
|
||||||
if (moveDirection === 'out') point.x = Big(point.x).minus(moveUpDownLength).toNumber()
|
if (moveDirection === 'out') point.x = Big(point.x).minus(moveUpDownLength).toNumber()
|
||||||
else if (moveDirection === 'in') point.x = Big(point.x).plus(moveUpDownLength).toNumber()
|
else if (moveDirection === 'in') point.x = Big(point.x).plus(moveUpDownLength).toNumber()
|
||||||
|
if (__isLocalBR) console.log(`[BR-TRACE] i=${i} left/${moveDirection} x ${__bx}→${point.x}`)
|
||||||
} else if (position === 'right') {
|
} else if (position === 'right') {
|
||||||
if (moveDirection === 'out') point.x = Big(point.x).plus(moveUpDownLength).toNumber()
|
if (moveDirection === 'out') point.x = Big(point.x).plus(moveUpDownLength).toNumber()
|
||||||
else if (moveDirection === 'in') point.x = Big(point.x).minus(moveUpDownLength).toNumber()
|
else if (moveDirection === 'in') point.x = Big(point.x).minus(moveUpDownLength).toNumber()
|
||||||
|
if (__isLocalBR) console.log(`[BR-TRACE] i=${i} right/${moveDirection} x ${__bx}→${point.x}`)
|
||||||
|
} else if (__isLocalBR) {
|
||||||
|
console.warn(`[BR-TRACE] i=${i} position=${position} 매칭 분기 없음 (이동 안 됨)`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -633,6 +671,180 @@ const safeMovedPointsWithFallback = (roofId, canvas) => {
|
|||||||
* @param {fabric.Object} roof - 지붕 객체
|
* @param {fabric.Object} roof - 지붕 객체
|
||||||
* @param baseLines
|
* @param baseLines
|
||||||
*/
|
*/
|
||||||
|
// [v1 helper 2026-04-29] 각 baseLine corner 에서 정확히 45° 대각 방향으로 ray cast →
|
||||||
|
// 먼저 만나는 roofLine 세그먼트와의 교점까지 라인 그리기.
|
||||||
|
// - 방향 = (signDx, signDy)/√2, 즉 4사분면 정확 대각.
|
||||||
|
// signDx = sign(roofLinePoints[i].x - baseLinePoints[i].x) ← HEAD 의 contactData 와 동일 식
|
||||||
|
// signDy = sign(roofLinePoints[i].y - baseLinePoints[i].y)
|
||||||
|
// 0 폴백 = centroid 기준 (HEAD line 852~853 동일).
|
||||||
|
// - outward bisector(변 normal 합) 식은 비대각 (= "90° 꺾임" 으로 보임) → 사용 안 함.
|
||||||
|
// - canvas.add 만 수행 (innerLines/skeletonLines 영향 0).
|
||||||
|
// - 누적 보존: 인자 baseLines/roofLines 그대로, 리셋 없음.
|
||||||
|
// - 롤백: DRAW_BASELINE_TO_ROOFLINE_HELPER = false.
|
||||||
|
const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoints) => {
|
||||||
|
if (!DRAW_BASELINE_TO_ROOFLINE_HELPER) return
|
||||||
|
if (!Array.isArray(baseLinePoints) || baseLinePoints.length < 3) return
|
||||||
|
if (!Array.isArray(roofLinePoints) || roofLinePoints.length < 3) return
|
||||||
|
if (!Array.isArray(roof.innerLines) || roof.innerLines.length === 0) return
|
||||||
|
const m = roofLinePoints.length
|
||||||
|
|
||||||
|
// ray (P, dir) ∩ segment (A, B) → t (>0). 없으면 Infinity.
|
||||||
|
const rayHitSegment = (P, dir, A, B) => {
|
||||||
|
const sx = B.x - A.x, sy = B.y - A.y
|
||||||
|
const denom = dir.x * sy - dir.y * sx
|
||||||
|
if (Math.abs(denom) < 1e-9) return Infinity
|
||||||
|
const ax = A.x - P.x, ay = A.y - P.y
|
||||||
|
const t = (ax * sy - ay * sx) / denom
|
||||||
|
const s = (ax * dir.y - ay * dir.x) / denom
|
||||||
|
if (t > 1e-6 && s >= -1e-6 && s <= 1 + 1e-6) return t
|
||||||
|
return Infinity
|
||||||
|
}
|
||||||
|
|
||||||
|
// 점-선분 거리. P 가 polygon edge 위(또는 매우 가까이)인지 검사용.
|
||||||
|
const pointSegDist = (P, A, B) => {
|
||||||
|
const sx = B.x - A.x, sy = B.y - A.y
|
||||||
|
const lenSq = sx * sx + sy * sy
|
||||||
|
if (lenSq < 1e-9) return Math.hypot(P.x - A.x, P.y - A.y)
|
||||||
|
let t = ((P.x - A.x) * sx + (P.y - A.y) * sy) / lenSq
|
||||||
|
t = Math.max(0, Math.min(1, t))
|
||||||
|
return Math.hypot(P.x - (A.x + t * sx), P.y - (A.y + t * sy))
|
||||||
|
}
|
||||||
|
const isPointOnRoofLine = (P, eps = 1.0) => {
|
||||||
|
for (let i = 0; i < m; i++) {
|
||||||
|
const d = pointSegDist(P, roofLinePoints[i], roofLinePoints[(i + 1) % m])
|
||||||
|
if (d < eps) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// baseLine corner 중 P 와 가장 가까운 거리.
|
||||||
|
const minDistToBaseLine = (P) => {
|
||||||
|
let min = Infinity
|
||||||
|
for (const c of baseLinePoints) {
|
||||||
|
const d = Math.hypot(P.x - c.x, P.y - c.y)
|
||||||
|
if (d < min) min = d
|
||||||
|
}
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
|
||||||
|
// [v1 ext 2026-04-29] HIP 라인만 추출.
|
||||||
|
// ext = SK(hip) 의 연장선이자 처마 라인이므로 항상 대각선.
|
||||||
|
// corner 갯수 무관, hip 갯수 기반 → 평행오버 후 잔여 corner 가 만들어내던 헛 ext 제거.
|
||||||
|
const hipLines = roof.innerLines.filter((il) => {
|
||||||
|
if (!il || !isFinite(il.x1) || !isFinite(il.y1) || !isFinite(il.x2) || !isFinite(il.y2)) return false
|
||||||
|
return il.lineName === 'hip' || il.name === 'hip'
|
||||||
|
})
|
||||||
|
console.log(`[v1 ext] HIP 추출 ${hipLines.length}개 / 전체 innerLines ${roof.innerLines.length}`)
|
||||||
|
|
||||||
|
// 절삭 대상: roofLine seg + canvas 의 다른 보조라인 (eaveHelpLine 등).
|
||||||
|
// 기존 helper 자체(BASELINE_TO_ROOFLINE_HELPER_TYPE) / wall/roof 본체 / 텍스트 등은 제외.
|
||||||
|
const segs = []
|
||||||
|
for (let i = 0; i < m; i++) {
|
||||||
|
segs.push({ A: roofLinePoints[i], B: roofLinePoints[(i + 1) % m] })
|
||||||
|
}
|
||||||
|
const excludedNames = new Set([
|
||||||
|
'wall', 'roof', 'WALL', 'ROOF',
|
||||||
|
'lengthText', 'outerLine', 'baseLine', 'outerLinePoint',
|
||||||
|
BASELINE_TO_ROOFLINE_HELPER_TYPE,
|
||||||
|
])
|
||||||
|
const canvasObjs = (typeof canvas?.getObjects === 'function') ? canvas.getObjects() : []
|
||||||
|
for (const obj of canvasObjs) {
|
||||||
|
if (!obj) continue
|
||||||
|
if (obj.parentId !== roof.id) continue
|
||||||
|
if (excludedNames.has(obj.name) || excludedNames.has(obj.lineName)) continue
|
||||||
|
if (!isFinite(obj.x1) || !isFinite(obj.y1) || !isFinite(obj.x2) || !isFinite(obj.y2)) continue
|
||||||
|
if (Math.hypot(obj.x2 - obj.x1, obj.y2 - obj.y1) < 0.5) continue
|
||||||
|
segs.push({ A: { x: obj.x1, y: obj.y1 }, B: { x: obj.x2, y: obj.y2 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// [v1 ext 정정] ext 는 wallbaseLine 의 꼭지점(corner) 에 outer 끝점이 일치하는 hip 만 대상.
|
||||||
|
// 내부에서 끝나는 hip(ridge 쪽으로만 향하는 hip) 은 ext 대상 아님.
|
||||||
|
const CORNER_EPS = 1.0
|
||||||
|
for (const hip of hipLines) {
|
||||||
|
const p1 = { x: hip.x1, y: hip.y1 }
|
||||||
|
const p2 = { x: hip.x2, y: hip.y2 }
|
||||||
|
|
||||||
|
// outerEnd = baseLine corner 와 가까운 끝. 반대는 inner(ridge 쪽).
|
||||||
|
const d1 = minDistToBaseLine(p1)
|
||||||
|
const d2 = minDistToBaseLine(p2)
|
||||||
|
const outerEnd = d1 < d2 ? p1 : p2
|
||||||
|
const innerEnd = d1 < d2 ? p2 : p1
|
||||||
|
const outerCornerDist = Math.min(d1, d2)
|
||||||
|
|
||||||
|
// outer 끝이 wallbaseLine corner 에 일치하지 않으면 ext 대상 아님.
|
||||||
|
if (outerCornerDist > CORNER_EPS) {
|
||||||
|
console.log(
|
||||||
|
`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) ` +
|
||||||
|
`corner 거리=${outerCornerDist.toFixed(2)} > ${CORNER_EPS} → 내부 hip skip`
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미 처마(roofLine) 위에 있으면 ext 불필요.
|
||||||
|
if (isPointOnRoofLine(outerEnd, 1.0)) {
|
||||||
|
console.log(`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) 이미 roofLine 위 → skip`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 방향 = innerEnd → outerEnd 의 unit vec (hip 자체 진행 방향 그대로).
|
||||||
|
const dx = outerEnd.x - innerEnd.x
|
||||||
|
const dy = outerEnd.y - innerEnd.y
|
||||||
|
const len = Math.hypot(dx, dy)
|
||||||
|
if (len < 1e-6) continue
|
||||||
|
const ux = dx / len, uy = dy / len
|
||||||
|
|
||||||
|
// outerEnd 에서 unit 방향으로 ray cast → 가장 먼저 만나는 segment 까지.
|
||||||
|
let bestT = Infinity
|
||||||
|
let bestSrc = ''
|
||||||
|
for (let k = 0; k < segs.length; k++) {
|
||||||
|
const { A, B } = segs[k]
|
||||||
|
const t = rayHitSegment(outerEnd, { x: ux, y: uy }, A, B)
|
||||||
|
if (t < bestT) {
|
||||||
|
bestT = t
|
||||||
|
bestSrc = (k < m) ? 'roofLine' : 'aux'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isFinite(bestT) || bestT < 0.5) {
|
||||||
|
console.log(`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) NO_HIT or 너무 가까움`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const hitX = outerEnd.x + ux * bestT
|
||||||
|
const hitY = outerEnd.y + uy * bestT
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) ` +
|
||||||
|
`dir=(${ux.toFixed(3)},${uy.toFixed(3)}) bestT=${bestT.toFixed(2)} src=${bestSrc} ` +
|
||||||
|
`→ ext=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)})→(${hitX.toFixed(1)},${hitY.toFixed(1)})`
|
||||||
|
)
|
||||||
|
|
||||||
|
// [확장선 스타일] local: 빨강 점선(디버그 가시화), dev/prd: innerLine 동일색·실선(연결된 것처럼).
|
||||||
|
const __isLocalExt = process.env.NEXT_PUBLIC_RUN_MODE === 'local'
|
||||||
|
const __extStroke = __isLocalExt ? '#FF0000' : '#1083E3'
|
||||||
|
const __extDash = __isLocalExt ? [6, 4] : null
|
||||||
|
const line = new QLine([outerEnd.x, outerEnd.y, hitX, hitY], {
|
||||||
|
parentId: roof.id,
|
||||||
|
stroke: __extStroke,
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeDashArray: __extDash,
|
||||||
|
// lineName='extensionLine' → useRoofAllocationSetting.apply() 가 canvas 에서
|
||||||
|
// 자동 수집 후 roofBase.lines 에 추가.
|
||||||
|
name: BASELINE_TO_ROOFLINE_HELPER_TYPE,
|
||||||
|
lineName: 'extensionLine',
|
||||||
|
visible: true,
|
||||||
|
roofId: roof.id,
|
||||||
|
selectable: false,
|
||||||
|
hoverCursor: 'default',
|
||||||
|
attributes: {
|
||||||
|
type: BASELINE_TO_ROOFLINE_HELPER_TYPE,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
canvas.add(line)
|
||||||
|
line.bringToFront()
|
||||||
|
}
|
||||||
|
canvas.renderAll()
|
||||||
|
}
|
||||||
|
|
||||||
export const skeletonBuilder = (roofId, canvas, textMode) => {
|
export const skeletonBuilder = (roofId, canvas, textMode) => {
|
||||||
//처마
|
//처마
|
||||||
let roof = canvas?.getObjects().find((object) => object.id === roofId)
|
let roof = canvas?.getObjects().find((object) => object.id === roofId)
|
||||||
@ -834,6 +1046,56 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
|||||||
changRoofLinePoints = movedPoints
|
changRoofLinePoints = movedPoints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [v1 architecture 2026-04-29] SK 입력 강제 = wall.baseLines 직접.
|
||||||
|
// 위에서 계산된 changRoofLinePoints (45° 확장 또는 movedPoints) 를 모두 무시하고
|
||||||
|
// orderedBaseLinePoints (= wall.baseLines corner) 그대로 사용.
|
||||||
|
// wall.baseLines 는 마루이동/벽이동/offset 의 모든 누적 변경 반영 → SK 자동 동기화.
|
||||||
|
// 확장선은 drawBaselineToRooflineHelpers (45° to first roofLine) 가 별도 그림.
|
||||||
|
if (SK_INPUT_USE_WALL_BASELINE_DIRECT) {
|
||||||
|
changRoofLinePoints = orderedBaseLinePoints.map((p) => ({ x: p.x, y: p.y }))
|
||||||
|
roofLineContactPoints = orderedBaseLinePoints.map((p) => ({ x: p.x, y: p.y }))
|
||||||
|
console.log('[v1 SK_INPUT] wall.baseLines 직접 사용 (45° 확장/movedPoints 무시):',
|
||||||
|
changRoofLinePoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`).join(' '))
|
||||||
|
|
||||||
|
// [v1 평행오버 corner 흡수 2026-04-29]
|
||||||
|
// 평행오버 시 OVER_GUARD 가 인접 baseLine 길이를 OVER_EPS(0.5) 만큼만 남김.
|
||||||
|
// 그 corner 는 사실상 평행 — 모서리가 없어진 상태이므로 SK 빌더 입력에서 제외해야
|
||||||
|
// 헛 hip / 헛 ext 가 만들어지지 않는다.
|
||||||
|
// 조건: 인접점 거리 < DUP_EPS 또는 (prev,curr,next) cross < COLLINEAR_EPS
|
||||||
|
// 여러 점 동시 흡수 가능 → 다중 패스.
|
||||||
|
const DUP_EPS = 1.0
|
||||||
|
const COLLINEAR_EPS = 50.0 // |cross| 단위는 면적의 2배. baseLine 한쪽이 1px 미만이면 cross 도 매우 작음.
|
||||||
|
let kept = changRoofLinePoints.slice()
|
||||||
|
const absorbedAll = []
|
||||||
|
for (let pass = 0; pass < 5 && kept.length > 3; pass++) {
|
||||||
|
const next = []
|
||||||
|
const absorbedPass = []
|
||||||
|
for (let i = 0; i < kept.length; i++) {
|
||||||
|
const prev = kept[(i - 1 + kept.length) % kept.length]
|
||||||
|
const curr = kept[i]
|
||||||
|
const nxt = kept[(i + 1) % kept.length]
|
||||||
|
const dPrev = Math.hypot(curr.x - prev.x, curr.y - prev.y)
|
||||||
|
const dNext = Math.hypot(nxt.x - curr.x, nxt.y - curr.y)
|
||||||
|
const cross = (curr.x - prev.x) * (nxt.y - prev.y) - (curr.y - prev.y) * (nxt.x - prev.x)
|
||||||
|
if (dPrev < DUP_EPS || dNext < DUP_EPS || Math.abs(cross) < COLLINEAR_EPS) {
|
||||||
|
absorbedPass.push(`[${i}](${Math.round(curr.x)},${Math.round(curr.y)}) dPrev=${dPrev.toFixed(2)} dNext=${dNext.toFixed(2)} cross=${cross.toFixed(2)}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
next.push(curr)
|
||||||
|
}
|
||||||
|
if (absorbedPass.length === 0) break
|
||||||
|
absorbedAll.push(...absorbedPass)
|
||||||
|
kept = next
|
||||||
|
}
|
||||||
|
if (absorbedAll.length > 0 && kept.length >= 3) {
|
||||||
|
console.log(`[v1 SK_INPUT 평행흡수] ${absorbedAll.length}개 corner 제거:\n ${absorbedAll.join('\n ')}`)
|
||||||
|
console.log(`[v1 SK_INPUT 평행흡수] 최종 ${kept.length}개 corner:`,
|
||||||
|
kept.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`).join(' '))
|
||||||
|
changRoofLinePoints = kept
|
||||||
|
roofLineContactPoints = kept.map((p) => ({ x: p.x, y: p.y }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[DEBUG] changRoofLinePoints(최종 SK입력):', changRoofLinePoints.map((p, i) => `[${i}] (${Math.round(p.x)},${Math.round(p.y)})`))
|
console.log('[DEBUG] changRoofLinePoints(최종 SK입력):', changRoofLinePoints.map((p, i) => `[${i}] (${Math.round(p.x)},${Math.round(p.y)})`))
|
||||||
|
|
||||||
// 좌표 유효성 검증
|
// 좌표 유효성 검증
|
||||||
@ -942,6 +1204,11 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
|||||||
roof.innerLines = roof.innerLines || []
|
roof.innerLines = roof.innerLines || []
|
||||||
roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode, isOverDetected, changRoofLinePoints)
|
roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode, isOverDetected, changRoofLinePoints)
|
||||||
//console.log("roofInnerLines:::", roof.innerLines);
|
//console.log("roofInnerLines:::", roof.innerLines);
|
||||||
|
|
||||||
|
// [v1 helper 2026-04-29 B 경로] SK 입력 (offset 확장 baseLine 또는 movedPoints) 과 roofLinePoints 간
|
||||||
|
// gap 을 시각화. innerLines 와 별개 collection. 기존 working code (processInBoth 등) 무영향.
|
||||||
|
drawBaselineToRooflineHelpers(roof, canvas, changRoofLinePoints, roofLinePoints)
|
||||||
|
|
||||||
// 캔버스에 스켈레톤 상태 저장
|
// 캔버스에 스켈레톤 상태 저장
|
||||||
if (!canvas.skeletonStates) {
|
if (!canvas.skeletonStates) {
|
||||||
canvas.skeletonStates = {}
|
canvas.skeletonStates = {}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user