939 lines
38 KiB
JavaScript
939 lines
38 KiB
JavaScript
import { useRecoilValue } from 'recoil'
|
||
import { canvasState, currentObjectState } from '@/store/canvasAtom'
|
||
import { usePopup } from '@/hooks/usePopup'
|
||
import { useMessage } from '@/hooks/useMessage'
|
||
import { useEffect, useRef, useState } from 'react'
|
||
import { useEvent } from '@/hooks/useEvent'
|
||
import { useSwal } from '@/hooks/useSwal'
|
||
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
|
||
import Big from 'big.js'
|
||
import { calcLinePlaneSize } from '@/util/qpolygon-utils'
|
||
import { getSelectLinePosition } from '@/util/skeleton-utils'
|
||
import { useMouse } from '@/hooks/useMouse'
|
||
|
||
// [정책 flag] 오버 이동(인접 라인 반전 = polygon self-cross) 입력 처리 방법.
|
||
// 'clamp' : (기본/2026-04-24 최종) 평행 위치-OVER_EPS 까지 클램핑. wall.baseLines 는 항상 정상 polygon, SK 확장 정상.
|
||
// 'reject' : (2026-04-22 정책) silent skip — 오버면 해당 target 의 mutation 자체를 건너뜀. 토스트 없음.
|
||
// 'passthrough' : 가드 전면 스킵 — 사용자가 입력한 raw 값을 그대로 적용. (과거 crossed 허용 동작; polygon 왜곡 위험)
|
||
// 교체 방법: 이 상수만 변경. OVER_GUARD 분기가 자동 반응.
|
||
const OVER_MOVE_POLICY = 'clamp'
|
||
const OVER_EPS = 0.5
|
||
|
||
//동선이동 형 올림 내림
|
||
export function useMovementSetting(id) {
|
||
const TYPE = {
|
||
FLOW_LINE: 'flowLine', // 동선이동
|
||
UP_DOWN: 'updown', //형 올림내림
|
||
}
|
||
const canvas = useRecoilValue(canvasState)
|
||
const { initEvent, addCanvasMouseEventListener } = useEvent()
|
||
const { closePopup } = usePopup()
|
||
const { getMessage } = useMessage()
|
||
const { getIntersectMousePoint } = useMouse()
|
||
const currentObject = useRecoilValue(currentObjectState)
|
||
const selectedObject = useRef(null)
|
||
const buttonType = [
|
||
{ id: 1, name: getMessage('modal.movement.flow.line.move'), type: TYPE.FLOW_LINE },
|
||
{ id: 2, name: getMessage('modal.movement.flow.line.updown'), type: TYPE.UP_DOWN },
|
||
]
|
||
const [type, setType] = useState(TYPE.FLOW_LINE)
|
||
const typeRef = useRef(type)
|
||
const { swalFire } = useSwal()
|
||
|
||
const FLOW_LINE_REF = {
|
||
POINTER_INPUT_REF: useRef(null),
|
||
FILLED_INPUT_REF: useRef(null),
|
||
DOWN_LEFT_RADIO_REF: useRef(null),
|
||
UP_RIGHT_RADIO_REF: useRef(null),
|
||
}
|
||
|
||
const UP_DOWN_REF = {
|
||
POINTER_INPUT_REF: useRef(null),
|
||
FILLED_INPUT_REF: useRef(null),
|
||
UP_RADIO_REF: useRef(null),
|
||
DOWN_RADIO_REF: useRef(null),
|
||
}
|
||
|
||
const CONFIRM_LINE_REF = useRef(null)
|
||
const FOLLOW_LINE_REF = useRef(null)
|
||
// [MV-TRACE] 직전 mouseMove 가 input 박스 갱신에 성공했는지. false 면 mouseDown 에서 wallbaseLine 미그리기.
|
||
const lastMoveValidRef = useRef(false)
|
||
|
||
/** 동선이동, 형이동 선택시 속성 처리*/
|
||
useEffect(() => {
|
||
typeRef.current = type
|
||
selectedObject.current = null
|
||
if (FOLLOW_LINE_REF.current != null) {
|
||
canvas.remove(FOLLOW_LINE_REF.current)
|
||
canvas.renderAll()
|
||
FOLLOW_LINE_REF.current = null
|
||
}
|
||
if (CONFIRM_LINE_REF.current != null) {
|
||
canvas.remove(CONFIRM_LINE_REF.current)
|
||
canvas.renderAll()
|
||
CONFIRM_LINE_REF.current = null
|
||
}
|
||
clearRef()
|
||
|
||
canvas.discardActiveObject()
|
||
/** 전체 object 선택 불가 */
|
||
canvas.getObjects().forEach((obj) => {
|
||
obj.set({ selectable: false })
|
||
})
|
||
|
||
/** 지붕선 관련 속성 처리*/
|
||
const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
|
||
roofs.forEach((roof) => {
|
||
roof.set({ stroke: '#000000' })
|
||
roof.innerLines.forEach((line) => {
|
||
if (type === TYPE.FLOW_LINE && line.name === LINE_TYPE.SUBLINE.RIDGE) {
|
||
line.set({ selectable: true, strokeWidth: 5, stroke: '#1083E3' })
|
||
line.bringToFront()
|
||
} else {
|
||
line.set({ selectable: false, strokeWidth: 2, stroke: '#000000' })
|
||
}
|
||
})
|
||
})
|
||
/** 외벽선 관련 속성 처리*/
|
||
const walls = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL)
|
||
walls.forEach((wall) => {
|
||
if (wall.baseLines.length === 0) {
|
||
wall.baseLines = canvas.getObjects().filter((obj) => obj.name === 'baseLine' && obj.attributes.wallId === wall.id)
|
||
}
|
||
wall.baseLines.forEach((line) => {
|
||
if (type === TYPE.UP_DOWN) {
|
||
line.set({ selectable: true, visible: true, stroke: '#1085E5', strokeWidth: 5 })
|
||
line.setCoords()
|
||
line.bringToFront()
|
||
} else {
|
||
line.set({ selectable: false, visible: false })
|
||
}
|
||
})
|
||
})
|
||
|
||
/** outerLines 속성처리*/
|
||
const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine')
|
||
outerLines.forEach((line) => line.set({ visible: true }))
|
||
canvas.renderAll()
|
||
}, [type])
|
||
|
||
/** 팝업창이 열릴때,닫힐때 속성들을 처리*/
|
||
useEffect(() => {
|
||
addCanvasMouseEventListener('mouse:move', mouseMoveEvent)
|
||
addCanvasMouseEventListener('mouse:down', mouseDownEvent)
|
||
return () => {
|
||
canvas.discardActiveObject()
|
||
if (FOLLOW_LINE_REF.current != null) {
|
||
canvas.remove(FOLLOW_LINE_REF.current)
|
||
FOLLOW_LINE_REF.current = null
|
||
}
|
||
if (CONFIRM_LINE_REF.current != null) {
|
||
canvas.remove(CONFIRM_LINE_REF.current)
|
||
CONFIRM_LINE_REF.current = null
|
||
}
|
||
const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
|
||
roofs.forEach((roof) => {
|
||
roof.set({ stroke: '#1083E3' })
|
||
roof.innerLines.forEach((line) => line.set({ selectable: true, strokeWidth: 2, stroke: '#1083E3' }))
|
||
})
|
||
const walls = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL)
|
||
walls.forEach((wall) => {
|
||
if (wall.baseLines.length === 0) {
|
||
wall.baseLines = canvas.getObjects().filter((obj) => obj.name === 'baseLine' && obj.attributes.wallId === wall.id)
|
||
}
|
||
wall.baseLines.forEach((baseLine) => {
|
||
baseLine.set({ selectable: false, visible: false })
|
||
})
|
||
})
|
||
const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine')
|
||
outerLines.forEach((line) => line.set({ visible: true }))
|
||
canvas.renderAll()
|
||
initEvent()
|
||
}
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
const roofs = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
|
||
if (roofs.length === 0) {
|
||
swalFire({ text: getMessage('roof.line.not.found') })
|
||
closePopup(id)
|
||
return
|
||
}
|
||
}, [])
|
||
|
||
/** object 선택이 변경될 때 처리*/
|
||
useEffect(() => {
|
||
if (FOLLOW_LINE_REF.current != null) {
|
||
canvas.remove(FOLLOW_LINE_REF.current)
|
||
canvas.renderAll()
|
||
FOLLOW_LINE_REF.current = null
|
||
}
|
||
|
||
if (selectedObject.current != null) {
|
||
selectedObject.current.set({ stroke: '#1083E3' })
|
||
selectedObject.current = null
|
||
}
|
||
if (!currentObject) return
|
||
|
||
clearRef()
|
||
|
||
canvas
|
||
.getObjects()
|
||
.filter((obj) => obj.name === 'checkPoint' || obj.name === 'checkLine')
|
||
.forEach((obj) => canvas.remove(obj))
|
||
canvas.renderAll()
|
||
|
||
if (CONFIRM_LINE_REF.current != null) {
|
||
canvas.remove(CONFIRM_LINE_REF.current)
|
||
canvas.renderAll()
|
||
CONFIRM_LINE_REF.current = null
|
||
}
|
||
|
||
currentObject.set({ stroke: '#EA10AC' })
|
||
selectedObject.current = currentObject
|
||
|
||
const followLine = new fabric.Line([currentObject.x1, currentObject.y1, currentObject.x2, currentObject.y2], {
|
||
stroke: '#000000',
|
||
strokeWidth: 4,
|
||
selectable: false,
|
||
name: 'followLine',
|
||
})
|
||
canvas.add(followLine)
|
||
followLine.bringToFront()
|
||
FOLLOW_LINE_REF.current = followLine
|
||
|
||
canvas.renderAll()
|
||
}, [currentObject])
|
||
|
||
const clearRef = () => {
|
||
if (type === TYPE.FLOW_LINE) {
|
||
// 안전한 ref 접근
|
||
if (FLOW_LINE_REF.POINTER_INPUT_REF.current) {
|
||
FLOW_LINE_REF.POINTER_INPUT_REF.current.value = ''
|
||
}
|
||
if (FLOW_LINE_REF.FILLED_INPUT_REF.current) {
|
||
FLOW_LINE_REF.FILLED_INPUT_REF.current.value = ''
|
||
}
|
||
|
||
const upRightChecked = FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current?.checked || false
|
||
const downLeftChecked = FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current?.checked || false
|
||
|
||
if (upRightChecked || downLeftChecked) {
|
||
if (FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current) {
|
||
FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current.checked = !downLeftChecked
|
||
}
|
||
if (FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current) {
|
||
FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current.checked = !upRightChecked
|
||
}
|
||
} else {
|
||
if (FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current) {
|
||
FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current.checked = true
|
||
}
|
||
if (FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current) {
|
||
FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current.checked = false
|
||
}
|
||
}
|
||
}
|
||
|
||
if (type === TYPE.UP_DOWN) {
|
||
// 안전한 ref 접근
|
||
if (UP_DOWN_REF.POINTER_INPUT_REF.current) {
|
||
UP_DOWN_REF.POINTER_INPUT_REF.current.value = ''
|
||
}
|
||
if (UP_DOWN_REF.FILLED_INPUT_REF.current) {
|
||
UP_DOWN_REF.FILLED_INPUT_REF.current.value = ''
|
||
}
|
||
if (UP_DOWN_REF.UP_RADIO_REF.current) {
|
||
UP_DOWN_REF.UP_RADIO_REF.current.checked = true
|
||
}
|
||
if (UP_DOWN_REF.DOWN_RADIO_REF.current) {
|
||
UP_DOWN_REF.DOWN_RADIO_REF.current.checked = false
|
||
}
|
||
}
|
||
}
|
||
|
||
let currentCalculatedValue = 0
|
||
|
||
const mouseMoveEvent = (e) => {
|
||
// [MV-TRACE local]
|
||
const __isLocalMV = process.env.NEXT_PUBLIC_RUN_MODE === 'local'
|
||
|
||
// 기존에는 activeObject를 사용했으나, 이 기능에서는 선택된 라인을 비선택(selectable:false) 상태로 두므로
|
||
// 항상 selectedObject.current를 기준으로 계산한다.
|
||
const target = selectedObject.current
|
||
if (!target) {
|
||
if (__isLocalMV) console.warn('[MV-TRACE] selectedObject.current=null → skip')
|
||
lastMoveValidRef.current = false
|
||
return
|
||
}
|
||
|
||
const { top: targetTop, left: targetLeft } = target
|
||
const __mp = getIntersectMousePoint(e) || {}
|
||
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 direction = ''
|
||
|
||
if (Math.abs(target.y1 - target.y2) < 0.5) {
|
||
// 수평 라인
|
||
value = Big(targetTop).minus(currentY).times(10).round(0)
|
||
|
||
// 방향 감지
|
||
if (value.toNumber() > 0) {
|
||
direction = 'up' // 마우스가 라인 위쪽에 있음 (위로 움직임)
|
||
} else if (value.toNumber() < 0) {
|
||
direction = 'down' // 마우스가 라인 아래쪽에 있음 (아래로 움직임)
|
||
}
|
||
} else {
|
||
// 수직 라인
|
||
value = Big(targetLeft).minus(currentX).times(10).round(0).neg()
|
||
|
||
// 방향 감지
|
||
if (value.toNumber() > 0) {
|
||
direction = 'right' // 마우스가 라인 오른쪽에 있음 (오른쪽으로 움직임)
|
||
} else if (value.toNumber() < 0) {
|
||
direction = 'left' // 마우스가 라인 왼쪽에 있음 (왼쪽으로 움직임)
|
||
}
|
||
}
|
||
|
||
// followLine도 포인터를 따라가도록 동기화 (하나의 mouse:move 핸들러만 사용)
|
||
const followLine = FOLLOW_LINE_REF.current
|
||
if (followLine) {
|
||
if (followLine.x1 === followLine.x2) {
|
||
// 수직 라인: x만 이동
|
||
followLine.left = currentX.toNumber() - 2
|
||
} else {
|
||
// 수평 라인: y만 이동
|
||
followLine.top = currentY.toNumber() - 2
|
||
}
|
||
followLine.bringToFront()
|
||
followLine.setCoords && followLine.setCoords()
|
||
canvas.renderAll()
|
||
}
|
||
|
||
// 방향 정보를 사용하여 라디오 버튼 상태 업데이트
|
||
|
||
currentCalculatedValue = value.toNumber()
|
||
|
||
let __inputUpdated = false
|
||
if (typeRef.current === TYPE.FLOW_LINE) {
|
||
if (FLOW_LINE_REF.POINTER_INPUT_REF.current) {
|
||
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 {
|
||
if (UP_DOWN_REF.POINTER_INPUT_REF.current) {
|
||
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 midY = Big(target.y1).plus(target.y2).div(2)
|
||
const wall = canvas.getObjects().find((obj) => obj.id === target.attributes.wallId)
|
||
|
||
const result = getSelectLinePosition(wall, target, {
|
||
testDistance: 5, // 테스트 거리
|
||
debug: true, // 디버깅 로그 출력
|
||
})
|
||
//console.log("1111litarget:::::", target);
|
||
//console.log("1111linePosition:::::", result.position); // 'top', 'bottom', 'left', 'right'
|
||
|
||
let linePosition = result.position
|
||
//console.log("1111linePosition:::::", direction, linePosition);
|
||
|
||
if (target.y1 === target.y2) {
|
||
//수평벽
|
||
|
||
const setRadioStates = (isUp) => {
|
||
if (UP_DOWN_REF.UP_RADIO_REF.current) {
|
||
UP_DOWN_REF.UP_RADIO_REF.current.checked = isUp
|
||
}
|
||
if (UP_DOWN_REF.DOWN_RADIO_REF.current) {
|
||
UP_DOWN_REF.DOWN_RADIO_REF.current.checked = !isUp
|
||
}
|
||
}
|
||
|
||
if (linePosition === 'top') {
|
||
setRadioStates(value.s !== -1)
|
||
} else if (linePosition === 'bottom') {
|
||
setRadioStates(value.s !== 1)
|
||
}
|
||
|
||
if (direction === 'up') {
|
||
}
|
||
/*
|
||
checkPoint = { x: midX.toNumber(), y: midY.plus(10).toNumber() }
|
||
if (wall.inPolygon(checkPoint)) { //선택라인이 내부
|
||
if (value.s === -1) {
|
||
console.log('1value:::', value.s)
|
||
if (UP_DOWN_REF.UP_RADIO_REF.current) UP_DOWN_REF.UP_RADIO_REF.current.checked = false
|
||
if (UP_DOWN_REF.DOWN_RADIO_REF.current) UP_DOWN_REF.DOWN_RADIO_REF.current.checked = true
|
||
} else {
|
||
if (UP_DOWN_REF.UP_RADIO_REF.current) UP_DOWN_REF.UP_RADIO_REF.current.checked = true
|
||
if (UP_DOWN_REF.DOWN_RADIO_REF.current) UP_DOWN_REF.DOWN_RADIO_REF.current.checked = false
|
||
}
|
||
} else { //
|
||
if (value.s === 1) { //선택라인이 외부
|
||
console.log('2value:::', value.s)
|
||
if (UP_DOWN_REF.UP_RADIO_REF.current) UP_DOWN_REF.UP_RADIO_REF.current.checked = false
|
||
if (UP_DOWN_REF.DOWN_RADIO_REF.current) UP_DOWN_REF.DOWN_RADIO_REF.current.checked = true
|
||
} else {
|
||
if (UP_DOWN_REF.UP_RADIO_REF.current) UP_DOWN_REF.UP_RADIO_REF.current.checked = true
|
||
if (UP_DOWN_REF.DOWN_RADIO_REF.current) UP_DOWN_REF.DOWN_RADIO_REF.current.checked = false
|
||
}
|
||
}
|
||
*/
|
||
} else {
|
||
const setRadioStates = (isUp) => {
|
||
if (UP_DOWN_REF.UP_RADIO_REF.current) {
|
||
UP_DOWN_REF.UP_RADIO_REF.current.checked = isUp
|
||
}
|
||
if (UP_DOWN_REF.DOWN_RADIO_REF.current) {
|
||
UP_DOWN_REF.DOWN_RADIO_REF.current.checked = !isUp
|
||
}
|
||
}
|
||
|
||
if (linePosition === 'left') {
|
||
setRadioStates(value.s !== 1)
|
||
} else if (linePosition === 'right') {
|
||
setRadioStates(value.s !== -1)
|
||
}
|
||
/*
|
||
checkPoint = { x: midX.plus(10).toNumber(), y: midY.toNumber() }
|
||
if (wall.inPolygon(checkPoint)) {
|
||
if (value.s === 1) {
|
||
console.log('3value:::', value.s)
|
||
if (UP_DOWN_REF.UP_RADIO_REF.current) UP_DOWN_REF.UP_RADIO_REF.current.checked = false
|
||
if (UP_DOWN_REF.DOWN_RADIO_REF.current) UP_DOWN_REF.DOWN_RADIO_REF.current.checked = true
|
||
} else {
|
||
if (UP_DOWN_REF.UP_RADIO_REF.current) UP_DOWN_REF.UP_RADIO_REF.current.checked = true
|
||
if (UP_DOWN_REF.DOWN_RADIO_REF.current) UP_DOWN_REF.DOWN_RADIO_REF.current.checked = false
|
||
|
||
}
|
||
} else {
|
||
if (value.s === -1) {
|
||
console.log('-1value:::', value.s)
|
||
if (UP_DOWN_REF.UP_RADIO_REF.current) UP_DOWN_REF.UP_RADIO_REF.current.checked = false
|
||
if (UP_DOWN_REF.DOWN_RADIO_REF.current) UP_DOWN_REF.DOWN_RADIO_REF.current.checked = true
|
||
} else {
|
||
if (UP_DOWN_REF.UP_RADIO_REF.current) UP_DOWN_REF.UP_RADIO_REF.current.checked = true
|
||
if (UP_DOWN_REF.DOWN_RADIO_REF.current) UP_DOWN_REF.DOWN_RADIO_REF.current.checked = false
|
||
}
|
||
}
|
||
*/
|
||
}
|
||
}
|
||
// [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) => {
|
||
canvas
|
||
.getObjects()
|
||
.filter((obj) => obj.name === 'checkPoint' || obj.name === 'checkLine')
|
||
.forEach((obj) => canvas.remove(obj))
|
||
canvas.renderAll()
|
||
|
||
const target = selectedObject.current
|
||
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 followLine = canvas.getObjects().find((obj) => obj.name === 'followLine')
|
||
|
||
const confirmLine = new fabric.Line([followLine.x1, followLine.y1, followLine.x2, followLine.y2], {
|
||
left: followLine.left,
|
||
top: followLine.top,
|
||
stroke: '#000000',
|
||
strokeWidth: 4,
|
||
selectable: false,
|
||
parentId: roofId,
|
||
name: 'confirmLine',
|
||
target,
|
||
})
|
||
canvas.add(confirmLine)
|
||
canvas.renderAll()
|
||
CONFIRM_LINE_REF.current = confirmLine
|
||
|
||
handleSave()
|
||
}
|
||
|
||
const handleSave = () => {
|
||
if (CONFIRM_LINE_REF.current !== null) {
|
||
canvas.remove(CONFIRM_LINE_REF.current)
|
||
CONFIRM_LINE_REF.current = null
|
||
canvas.renderAll()
|
||
}
|
||
if (FOLLOW_LINE_REF.current !== null) {
|
||
canvas.remove(FOLLOW_LINE_REF.current)
|
||
FOLLOW_LINE_REF.current = null
|
||
canvas.renderAll()
|
||
}
|
||
if (UP_DOWN_REF.current !== null) {
|
||
canvas.remove(UP_DOWN_REF.current)
|
||
UP_DOWN_REF.current = null
|
||
canvas.renderAll()
|
||
}
|
||
|
||
const target = selectedObject.current !== null ? selectedObject.current : CONFIRM_LINE_REF.current?.target
|
||
if (!target) return
|
||
|
||
const roofId = target.attributes.roofId
|
||
const roof = canvas.getObjects().find((obj) => obj.id === roofId)
|
||
|
||
// 현이동, 동이동 추가
|
||
let flPointValue = FLOW_LINE_REF.POINTER_INPUT_REF.current?.value ?? 0
|
||
let flFilledValue = FLOW_LINE_REF.FILLED_INPUT_REF.current?.value ?? 0
|
||
flPointValue = flFilledValue > 0 || flFilledValue < 0 ? flFilledValue : flPointValue
|
||
const moveFlowLine = typeRef.current === TYPE.FLOW_LINE ? flPointValue : 0
|
||
|
||
let udPointValue = UP_DOWN_REF.POINTER_INPUT_REF.current?.value ?? 0
|
||
let udFilledValue = UP_DOWN_REF.FILLED_INPUT_REF.current?.value ?? 0
|
||
udPointValue = udFilledValue > 0 ? udFilledValue : udPointValue
|
||
const moveUpDown = typeRef.current === TYPE.UP_DOWN ? udPointValue : 0
|
||
roof.moveFlowLine = parseInt(moveFlowLine, 10) || 0
|
||
roof.moveUpDown = parseInt(moveUpDown, 10) || 0
|
||
roof.moveDirect = ''
|
||
roof.moveSelectLine = target
|
||
//console.log("target::::", target, roof.moveSelectLine)
|
||
const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId)
|
||
const baseLines = wall.baseLines
|
||
let centerPoint = wall.getCenterPoint()
|
||
let targetBaseLines = []
|
||
let isGableRoof
|
||
if (typeRef.current === TYPE.FLOW_LINE) {
|
||
const gableType = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.HIPANDGABLE]
|
||
if (baseLines.find((line) => gableType.includes(line.attributes.type))) {
|
||
isGableRoof = true
|
||
let gableStartIndex = baseLines.findIndex((line) => gableType.includes(line.attributes.type))
|
||
baseLines.forEach((line, index) => {
|
||
if (isGableRoof) {
|
||
const isEvenLine = (index - gableStartIndex) % 2 === 0
|
||
if (isEvenLine && !gableType.includes(line.attributes.type)) {
|
||
isGableRoof = false
|
||
}
|
||
}
|
||
})
|
||
} else {
|
||
isGableRoof = false
|
||
}
|
||
const lineVector =
|
||
Math.abs(target.y1 - target.y2) < 0.2
|
||
? FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current.checked
|
||
? 'up'
|
||
: 'down'
|
||
: FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current.checked
|
||
? 'right'
|
||
: 'left'
|
||
let checkBaseLines, currentBaseLines
|
||
roof.moveDirect = lineVector
|
||
switch (lineVector) {
|
||
case 'up':
|
||
checkBaseLines = baseLines.filter((line) => line.y1 === line.y2 && line.y1 < target.y1)
|
||
currentBaseLines = checkBaseLines.filter((line) => {
|
||
const minX = Math.min(target.x1, target.x2)
|
||
const maxX = Math.max(target.x1, target.x2)
|
||
return minX <= line.x1 && line.x1 <= maxX && minX <= line.x2 && line.x2 <= maxX
|
||
})
|
||
if (isGableRoof && currentBaseLines.length > 0) {
|
||
currentBaseLines.forEach((line) =>
|
||
targetBaseLines.push({
|
||
line,
|
||
distance: Big(line.y1).minus(target.y1).abs().toNumber(),
|
||
}),
|
||
)
|
||
} else {
|
||
checkBaseLines.forEach((line) =>
|
||
targetBaseLines.push({
|
||
line,
|
||
distance: Big(target.y1).minus(line.y1).abs().toNumber(),
|
||
}),
|
||
)
|
||
}
|
||
baseLines
|
||
.filter((line) => line.y1 === line.y2 && line.y1 < target.y1)
|
||
.forEach((line) => targetBaseLines.push({ line, distance: Big(target.y1).minus(line.y1).abs().toNumber() }))
|
||
break
|
||
case 'down':
|
||
checkBaseLines = baseLines.filter((line) => line.y1 === line.y2 && line.y1 > target.y1)
|
||
currentBaseLines = checkBaseLines.filter((line) => {
|
||
const minX = Math.min(target.x1, target.x2)
|
||
const maxX = Math.max(target.x1, target.x2)
|
||
return minX <= line.x1 && line.x1 <= maxX && minX <= line.x2 && line.x2 <= maxX
|
||
})
|
||
if (isGableRoof && currentBaseLines.length > 0) {
|
||
currentBaseLines.forEach((line) =>
|
||
targetBaseLines.push({
|
||
line,
|
||
distance: Big(line.y1).minus(target.y1).abs().toNumber(),
|
||
}),
|
||
)
|
||
} else {
|
||
checkBaseLines.forEach((line) =>
|
||
targetBaseLines.push({
|
||
line,
|
||
distance: Big(line.y1).minus(target.y1).abs().toNumber(),
|
||
}),
|
||
)
|
||
}
|
||
break
|
||
case 'right':
|
||
checkBaseLines = baseLines.filter((line) => line.x1 === line.x2 && line.x1 > target.x1)
|
||
currentBaseLines = checkBaseLines.filter((line) => {
|
||
const minY = Math.min(target.y1, target.y2)
|
||
const maxY = Math.max(target.y1, target.y2)
|
||
return minY <= line.y1 && line.y1 <= maxY && minY <= line.y2 && line.y2 <= maxY
|
||
})
|
||
if (isGableRoof && currentBaseLines.length > 0) {
|
||
currentBaseLines.forEach((line) =>
|
||
targetBaseLines.push({
|
||
line,
|
||
distance: Big(line.x1).minus(target.x1).abs().toNumber(),
|
||
}),
|
||
)
|
||
} else {
|
||
checkBaseLines.forEach((line) =>
|
||
targetBaseLines.push({
|
||
line,
|
||
distance: Big(target.x1).minus(line.x1).abs().toNumber(),
|
||
}),
|
||
)
|
||
}
|
||
break
|
||
case 'left':
|
||
checkBaseLines = baseLines.filter((line) => line.x1 === line.x2 && line.x1 < target.x1)
|
||
currentBaseLines = checkBaseLines.filter((line) => {
|
||
const minY = Math.min(target.y1, target.y2)
|
||
const maxY = Math.max(target.y1, target.y2)
|
||
return minY <= line.y1 && line.y1 <= maxY && minY <= line.y2 && line.y2 <= maxY
|
||
})
|
||
if (isGableRoof && currentBaseLines.length > 0) {
|
||
currentBaseLines.forEach((line) =>
|
||
targetBaseLines.push({
|
||
line,
|
||
distance: Big(line.x1).minus(target.x1).abs().toNumber(),
|
||
}),
|
||
)
|
||
} else {
|
||
checkBaseLines.forEach((line) =>
|
||
targetBaseLines.push({
|
||
line,
|
||
distance: Big(target.x1).minus(line.x1).abs().toNumber(),
|
||
}),
|
||
)
|
||
}
|
||
break
|
||
}
|
||
} else {
|
||
roof.moveDirect = UP_DOWN_REF.UP_RADIO_REF.current.checked ? 'out' : UP_DOWN_REF.DOWN_RADIO_REF.current.checked ? 'in' : 'out'
|
||
targetBaseLines.push({ line: target, distance: 0 })
|
||
}
|
||
|
||
// Remove duplicate lines
|
||
const uniqueLines = new Map()
|
||
targetBaseLines = targetBaseLines.filter((item) => {
|
||
const key = `${item.line.x1},${item.line.y1},${item.line.x2},${item.line.y2}`
|
||
if (!uniqueLines.has(key)) {
|
||
uniqueLines.set(key, true)
|
||
return true
|
||
}
|
||
return false
|
||
})
|
||
|
||
// Sort by distance
|
||
targetBaseLines.sort((a, b) => a.distance - b.distance)
|
||
targetBaseLines = targetBaseLines.filter((line) => line.distance === targetBaseLines[0].distance)
|
||
|
||
if (isGableRoof) {
|
||
const zeroLengthLines = targetBaseLines.filter(
|
||
(line) => Math.sqrt(Math.pow(line.line.x2 - line.line.x1, 2) + Math.pow(line.line.y2 - line.line.y1, 2)) < 1,
|
||
)
|
||
if (zeroLengthLines.length > 0) {
|
||
zeroLengthLines.forEach((line) => {
|
||
const findLine = line.line
|
||
const findCoords = [
|
||
{ x: findLine.x1, y: findLine.y1 },
|
||
{ x: findLine.x2, y: findLine.y2 },
|
||
]
|
||
wall.baseLines
|
||
.filter((baseLine) => {
|
||
return findCoords.some(
|
||
(coord) =>
|
||
(Math.abs(coord.x - baseLine.x1) < 0.1 && Math.abs(coord.y - baseLine.y1) < 0.1) ||
|
||
(Math.abs(coord.x - baseLine.x2) < 0.1 && Math.abs(coord.y - baseLine.y2) < 0.1),
|
||
)
|
||
})
|
||
.forEach((baseLine) => {
|
||
const isAlready = targetBaseLines.find((target) => target.line === baseLine)
|
||
if (isAlready) return
|
||
targetBaseLines.push({ line: baseLine, distance: targetBaseLines[0].distance })
|
||
})
|
||
})
|
||
}
|
||
}
|
||
|
||
let value
|
||
if (typeRef.current === TYPE.FLOW_LINE) {
|
||
value = (() => {
|
||
const filledValue = FLOW_LINE_REF.FILLED_INPUT_REF.current?.value
|
||
const pointerValue = FLOW_LINE_REF.POINTER_INPUT_REF.current?.value
|
||
|
||
if (filledValue && !isNaN(filledValue) && filledValue.trim() !== '') {
|
||
return Big(filledValue).times(2)
|
||
} else if (pointerValue && !isNaN(pointerValue) && pointerValue.trim() !== '') {
|
||
return Big(pointerValue).times(2)
|
||
}
|
||
return Big(0) // 기본값으로 0 반환 또는 다른 적절한 기본값
|
||
})()
|
||
if (Math.abs(target.y1 - target.y2) < 0.5) {
|
||
value = value.neg()
|
||
}
|
||
} else {
|
||
console.log('error::', UP_DOWN_REF.POINTER_INPUT_REF.current.value)
|
||
value = Big(UP_DOWN_REF?.FILLED_INPUT_REF?.current?.value?.trim() || UP_DOWN_REF?.POINTER_INPUT_REF?.current?.value?.trim() || '0')
|
||
|
||
const midX = Big(target.x1).plus(target.x2).div(2)
|
||
const midY = Big(target.y1).plus(target.y2).div(2)
|
||
const wall = canvas.getObjects().find((obj) => obj.id === target.attributes.wallId)
|
||
let checkPoint
|
||
if (target.y1 === target.y2) {
|
||
checkPoint = { x: midX.toNumber(), y: midY.plus(10).times(value.s).toNumber() }
|
||
} else {
|
||
checkPoint = { x: midX.plus(10).times(value.s).toNumber(), y: midY.toNumber() }
|
||
}
|
||
|
||
const inPolygon = wall.inPolygon(checkPoint)
|
||
|
||
// if (UP_DOWN_REF.UP_RADIO_REF.current.checked && inPolygon) {
|
||
// value = value.neg()
|
||
// } else if (UP_DOWN_REF.DOWN_RADIO_REF.current.checked && !inPolygon) {
|
||
// value = value.neg()
|
||
// }
|
||
}
|
||
// console.log("2222titarget:::::", target);
|
||
// console.log("2222저장된 moveSelectLine:", roof.moveSelectLine);
|
||
// console.log("222wall::::", wall.points)
|
||
const result = getSelectLinePosition(wall, target, {
|
||
testDistance: 5, // 테스트 거리
|
||
debug: true, // 디버깅 로그 출력
|
||
})
|
||
|
||
//console.log("2222linePosition:::::", result.position);
|
||
//console.log("222moveDirect:::::", roof.moveDirect);
|
||
|
||
// 디버깅용 분류 결과 확인
|
||
|
||
let linePosition = result.position
|
||
roof.movePosition = linePosition
|
||
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
|
||
.filter((line) => Math.sqrt(Math.pow(line.line.x2 - line.line.x1, 2) + Math.pow(line.line.y2 - line.line.y1, 2)) >= 1)
|
||
.forEach((targetItem, __iter) => {
|
||
const currentLine = targetItem.line
|
||
const __horizCurr = currentLine.y1 === currentLine.y2
|
||
|
||
// [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 (linePosition === 'bottom' || linePosition === 'right') {
|
||
localValue = localValue.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 {
|
||
if (linePosition === 'top' || linePosition === 'left') {
|
||
localValue = localValue.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()}`)
|
||
}
|
||
}
|
||
|
||
const index = baseLines.findIndex((line) => line === currentLine)
|
||
const nextLine = baseLines[(index + 1) % baseLines.length]
|
||
const prevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length]
|
||
let deltaX = 0
|
||
let deltaY = 0
|
||
if (__horizCurr) {
|
||
deltaY = localValue.toNumber()
|
||
if (__isLocal) console.log(`[BR-TRACE] iter=${__iter} 수평 currentLine idx=${index} deltaY=${deltaY}`)
|
||
} else {
|
||
deltaX = localValue.toNumber()
|
||
if (__isLocal) console.log(`[BR-TRACE] iter=${__iter} 수직 currentLine idx=${index} deltaX=${deltaX}`)
|
||
}
|
||
|
||
// [OVER_GUARD] 오버 이동(인접 라인 반전) 처리. 정책은 파일 상단 OVER_MOVE_POLICY 로 전환.
|
||
// 한계 계산 (공통):
|
||
// nLimit = (nextLine 의 free 끝점 좌표) - (currentLine 의 해당 끝점 좌표)
|
||
// pLimit = (prevLine 의 free 끝점 좌표) - (currentLine 의 해당 시작점 좌표)
|
||
// delta 와 같은 부호인 한계만 후보(반대 부호 한계는 인접 라인이 길어지는 방향).
|
||
// 정책별 동작:
|
||
// 'passthrough' → 가드 전체 스킵
|
||
// 'reject' → |delta| > |한계-EPS| 이면 해당 target 은 mutation 없이 건너뜀 (silent skip)
|
||
// 'clamp' → |delta| 를 |한계-EPS| 로 잘라 평행 직전까지 이동. roof.moveUpDown 도 동일 비율 동기화.
|
||
if (OVER_MOVE_POLICY !== 'passthrough') {
|
||
const __isHoriz = currentLine.y1 === currentLine.y2
|
||
const __raw = __isHoriz ? deltaY : deltaX
|
||
const __sgn = Math.sign(__raw)
|
||
if (__sgn !== 0) {
|
||
const __nLimit = __isHoriz ? (nextLine.y2 - currentLine.y2) : (nextLine.x2 - currentLine.x2)
|
||
const __pLimit = __isHoriz ? (prevLine.y1 - currentLine.y1) : (prevLine.x1 - currentLine.x1)
|
||
let __absLimit = Infinity
|
||
if (Math.sign(__nLimit) === __sgn) __absLimit = Math.min(__absLimit, Math.abs(__nLimit))
|
||
if (Math.sign(__pLimit) === __sgn) __absLimit = Math.min(__absLimit, Math.abs(__pLimit))
|
||
// [EPS] 평행 정확히 도달 시 인접 baseLine zero-length → polygon 중복 꼭짓점 → SkeletonBuilder 헛선 발생.
|
||
// OVER_EPS 여유 두어 인접 baseLine 길이 OVER_EPS(=0.5) 로 남기고 SHOULDER_ABSORBED(planeSize<1) 가 자연 흡수.
|
||
const __safeAbsLimit = Math.max(0, __absLimit - OVER_EPS)
|
||
const __isOver = Math.abs(__raw) > __safeAbsLimit + 0.001
|
||
|
||
if (__isOver && OVER_MOVE_POLICY === 'reject') {
|
||
// silent skip: mutation 자체 건너뜀. movingLineFromSkeleton 의 roof.points 확장도 막으려면 moveUpDown=0.
|
||
console.warn(
|
||
`[handleSave] OVER 거부(reject): ${__isHoriz ? 'deltaY' : 'deltaX'}=${__raw.toFixed(1)} > limit=${__safeAbsLimit.toFixed(1)} → target skip`
|
||
)
|
||
roof.moveUpDown = 0
|
||
return
|
||
}
|
||
|
||
if (OVER_MOVE_POLICY === 'clamp') {
|
||
const __limited = __sgn * Math.min(Math.abs(__raw), __safeAbsLimit)
|
||
if (Math.abs(__limited - __raw) > 0.001) {
|
||
console.warn(
|
||
`[handleSave] OVER 클램핑: ${__isHoriz ? 'deltaY' : 'deltaX'} ${__raw.toFixed(1)} → ${__limited.toFixed(1)} (nLimit=${__nLimit.toFixed(1)} pLimit=${__pLimit.toFixed(1)}, EPS=${OVER_EPS})`
|
||
)
|
||
if (__isHoriz) deltaY = __limited
|
||
else deltaX = __limited
|
||
// [SYNC roof.moveUpDown] movingLineFromSkeleton 은 roof.moveUpDown(mm) 로 roof.points 확장.
|
||
// delta 만 줄이고 moveUpDown 방치 시 wall 은 평행까지, roof.points 는 원본 양만큼 이동 →
|
||
// polygon 자기교차 → removeNonOrthogonalPoints 가 꼭짓점 축소 → "SK 가 wall 안에서 멈춤".
|
||
// __limited(cm) × 10 = mm.
|
||
roof.moveUpDown = Math.round(Math.abs(__limited) * 10)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
currentLine.set({
|
||
x1: currentLine.x1 + deltaX,
|
||
y1: currentLine.y1 + deltaY,
|
||
x2: currentLine.x2 + deltaX,
|
||
y2: currentLine.y2 + deltaY,
|
||
startPoint: { x: currentLine.x1 + deltaX, y: currentLine.y1 + deltaY },
|
||
endPoint: { x: currentLine.x2 + deltaX, y: currentLine.y2 + deltaY },
|
||
})
|
||
const currentSize = calcLinePlaneSize({
|
||
x1: currentLine.x1,
|
||
y1: currentLine.y1,
|
||
x2: currentLine.x2,
|
||
y2: currentLine.y2,
|
||
})
|
||
currentLine.attributes.planeSize = currentSize
|
||
currentLine.attributes.actualSize = currentSize
|
||
|
||
nextLine.set({
|
||
x1: currentLine.x2,
|
||
y1: currentLine.y2,
|
||
startPoint: { x: currentLine.x2, y: currentLine.y2 },
|
||
})
|
||
const nextSize = calcLinePlaneSize({ x1: nextLine.x1, y1: nextLine.y1, x2: nextLine.x2, y2: nextLine.y2 })
|
||
nextLine.attributes.planeSize = nextSize
|
||
nextLine.attributes.actualSize = nextSize
|
||
|
||
prevLine.set({
|
||
x2: currentLine.x1,
|
||
y2: currentLine.y1,
|
||
endPoint: { x: currentLine.x1, y: currentLine.y1 },
|
||
})
|
||
const prevSize = calcLinePlaneSize({ x1: prevLine.x1, y1: prevLine.y1, x2: prevLine.x2, y2: prevLine.y2 })
|
||
prevLine.attributes.planeSize = prevSize
|
||
prevLine.attributes.actualSize = prevSize
|
||
})
|
||
|
||
roof.drawHelpLine()
|
||
initEvent()
|
||
closePopup(id)
|
||
}
|
||
|
||
// javascript
|
||
|
||
return {
|
||
TYPE,
|
||
closePopup,
|
||
buttonType,
|
||
type,
|
||
setType,
|
||
FLOW_LINE_REF,
|
||
UP_DOWN_REF,
|
||
handleSave,
|
||
}
|
||
}
|