qcast-front/src/hooks/common/useRoofFn.js

405 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil'
import { canvasState, currentObjectState } from '@/store/canvasAtom'
import { selectedRoofMaterialSelector } from '@/store/settingAtom'
import { ROOF_MATERIAL_LAYOUT } from '@/components/floor-plan/modal/placementShape/PlacementShapeSetting'
import { POLYGON_TYPE } from '@/common/common'
import { useEvent } from '@/hooks/useEvent'
import { useLine } from '@/hooks/useLine'
import { outerLinePointsState } from '@/store/outerLineAtom'
import { usePolygon } from '@/hooks/usePolygon'
import { useText } from '@/hooks/useText'
const ROOF_COLOR = {
0: 'rgb(199,240,213)',
1: 'rgb(178,238,255)',
2: 'rgb(187,204,255)',
3: 'rgb(228,202,255)',
}
export function useRoofFn() {
const canvas = useRecoilValue(canvasState)
const selectedRoofMaterial = useRecoilValue(selectedRoofMaterialSelector)
const currentObject = useRecoilValue(currentObjectState)
const { addCanvasMouseEventListener, initEvent } = useEvent()
const resetPoints = useResetRecoilState(outerLinePointsState)
const { addPitchText } = useLine()
const { setPolygonLinesActualSize } = usePolygon()
const { changeCorridorDimensionText } = useText()
const [outerLinePoints, setOuterLinePoints] = useRecoilState(outerLinePointsState)
//면형상 선택 클릭시 지붕 패턴 입히기
function setSurfaceShapePattern(polygon, mode = 'onlyBorder', trestleMode = false, roofMaterial, isForceChange = false, isDisplay = false) {
try {
if (!polygon) {
return
}
if (polygon.wall) {
return
}
if (polygon.points.length < 3) {
return
}
if (isForceChange && !isDisplay) {
/*if (polygon.roofMaterial) {
polygon.roofMaterial = null
}*/
}
if (!roofMaterial) {
roofMaterial = polygon.roofMaterial ?? selectedRoofMaterial
}
// [ROOF-PATTERN-GUARD 2026-05-08] polygon bbox 0 또는 roofMaterial 결손 시
// 0×0 patternSourceCanvas 가 만들어져 drawImage InvalidStateError 로 페이지 500 진입.
// 손상 plan(예 S203X460260507001) 로드 케이스 차단.
if (!polygon.width || !polygon.height) {
console.warn('[ROOF-PATTERN-GUARD] skip pattern, 0-dim polygon:', polygon.id, polygon.name, polygon.width, polygon.height)
return
}
if (!roofMaterial || !roofMaterial.layout) {
console.warn('[ROOF-PATTERN-GUARD] skip pattern, invalid roofMaterial:', polygon.id, roofMaterial)
return
}
const ratio = window.devicePixelRatio || 1
const layout = roofMaterial.layout
let width = (roofMaterial.width || 226) / 10
let height = (roofMaterial.length || 158) / 10
const index = roofMaterial.index ?? 0
let roofStyle = 2
const inputPatternSize = { width: width, height: height } //임시 사이즈
const patternSize = { ...inputPatternSize } // 입력된 값을 뒤집기 위해
if (polygon.direction === 'east' || polygon.direction === 'west') {
//세로형이면 width height를 바꿈
;[patternSize.width, patternSize.height] = [inputPatternSize.height, patternSize.width]
}
// 패턴 소스를 위한 임시 캔버스 생성
const patternSourceCanvas = document.createElement('canvas')
patternSourceCanvas.width = polygon.width * ratio
patternSourceCanvas.height = polygon.height * ratio
const ctx = patternSourceCanvas.getContext('2d')
let offset = roofStyle === 1 ? 0 : patternSize.width / 2
const rows = Math.floor(patternSourceCanvas.height / patternSize.height)
const cols = Math.floor(patternSourceCanvas.width / patternSize.width)
ctx.strokeStyle = mode === 'allPainted' ? 'black' : ROOF_COLOR[index]
ctx.lineWidth = 2
ctx.fillStyle = mode === 'allPainted' ? 'rgba(0, 159, 64, 0.7)' : 'white'
if (trestleMode) {
ctx.strokeStyle = 'black'
ctx.lineWidth = 0.2
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'
} else {
ctx.fillStyle = 'rgba(255, 255, 255, 1)'
}
if (polygon.direction === 'east' || polygon.direction === 'west') {
offset = roofStyle === 1 ? 0 : patternSize.height / 2
for (let col = 0; col <= cols; col++) {
const x = col * patternSize.width
const yStart = 0
const yEnd = patternSourceCanvas.height
ctx.beginPath()
ctx.moveTo(x, yStart) // 선 시작점
ctx.lineTo(x, yEnd) // 선 끝점
ctx.stroke()
if (mode === 'allPainted' || trestleMode) {
ctx.fillRect(x, yStart, patternSize.width, yEnd - yStart)
}
for (let row = 0; row <= rows; row++) {
const y = layout === ROOF_MATERIAL_LAYOUT.STAIRS ? row * patternSize.height + (col % 2 === 0 ? 0 : offset) : row * patternSize.height
const xStart = col * patternSize.width
const xEnd = xStart + patternSize.width
ctx.beginPath()
ctx.moveTo(xStart, y) // 선 시작점
ctx.lineTo(xEnd, y) // 선 끝점
ctx.stroke()
if (mode === 'allPainted' || trestleMode) {
ctx.fillRect(xStart, y, xEnd - xStart, patternSize.height)
}
}
}
} else {
for (let row = 0; row <= rows; row++) {
const y = row * patternSize.height
ctx.beginPath()
ctx.moveTo(0, y) // 선 시작점
ctx.lineTo(patternSourceCanvas.width, y) // 선 끝점
ctx.stroke()
if (mode === 'allPainted' || trestleMode) {
ctx.fillRect(0, y, patternSourceCanvas.width, patternSize.height)
}
for (let col = 0; col <= cols; col++) {
const x = layout === ROOF_MATERIAL_LAYOUT.STAIRS ? col * patternSize.width + (row % 2 === 0 ? 0 : offset) : col * patternSize.width
const yStart = row * patternSize.height
const yEnd = yStart + patternSize.height
ctx.beginPath()
ctx.moveTo(x, yStart) // 선 시작점
ctx.lineTo(x, yEnd) // 선 끝점
ctx.stroke()
if (mode === 'allPainted' || trestleMode) {
ctx.fillRect(x, yStart, patternSize.width, yEnd - yStart)
}
}
}
}
const hachingPatternSourceCanvas = document.createElement('canvas')
if (mode === 'lineHatch') {
hachingPatternSourceCanvas.width = polygon.width * ratio
hachingPatternSourceCanvas.height = polygon.height * ratio
const ctx1 = hachingPatternSourceCanvas.getContext('2d')
const gap = 10
ctx1.strokeStyle = 'green' // 선 색상
ctx1.lineWidth = 0.3 // 선 두께
for (let x = 0; x < hachingPatternSourceCanvas.width + hachingPatternSourceCanvas.height; x += gap) {
ctx1.beginPath()
ctx1.moveTo(x, 0) // 선 시작점
ctx1.lineTo(0, x) // 선 끝점
ctx1.stroke()
}
}
const combinedPatternCanvas = document.createElement('canvas')
combinedPatternCanvas.width = polygon.width * ratio
combinedPatternCanvas.height = polygon.height * ratio
const combinedCtx = combinedPatternCanvas.getContext('2d')
// 첫 번째 패턴을 그린 후 두 번째 패턴을 덧입힘
combinedCtx.drawImage(patternSourceCanvas, 0, 0)
combinedCtx.drawImage(hachingPatternSourceCanvas, 0, 0)
// 패턴 생성
const pattern = new fabric.Pattern({
source: combinedPatternCanvas,
repeat: 'repeat',
})
polygon.set('fill', null)
polygon.set('fill', pattern)
polygon.roofMaterial = roofMaterial
setPolygonLinesActualSize(polygon, true)
changeCorridorDimensionText()
polygon.canvas?.renderAll()
} catch (e) {
console.log(e)
}
}
function removeRoofMaterial(roof = currentObject) {
if (roof === null || roof.name !== POLYGON_TYPE.ROOF) {
return
}
roof.set('fill', null)
roof.roofMaterial = null
canvas?.renderAll()
}
function removeAllRoofMaterial() {
const roofBases = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
roofBases.forEach((roofBase) => {
removeRoofMaterial(roofBase)
})
}
function moveRoofMaterial(currentMousePos) {
const roofBase = canvas
.getObjects()
.filter((obj) => obj.name === POLYGON_TYPE.ROOF)
.filter((roof) => roof.inPolygon(currentMousePos))
if (roofBase.length === 0) {
return
}
const roof = roofBase[0]
const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes?.roofId === roof.id)
const checkPolygon = new fabric.Polygon(roof.points, {
name: 'moveRoofPolygon',
stroke: '#ff0000',
strokeWidth: 5,
fill: 'transparent',
selectable: false,
originX: 'center',
originY: 'center',
})
canvas.add(checkPolygon)
canvas.renderAll()
canvas.on('mouse:move', (event) => {
const mousePos = canvas.getPointer(event.e)
checkPolygon.left = mousePos.x
checkPolygon.top = mousePos.y
canvas.renderAll()
})
canvas.on('mouse:down', (event) => {
let mousePos = canvas.getPointer(event.e)
mousePos = { x: Math.round(mousePos.x), y: Math.round(mousePos.y) }
// 'text' 뿐 아니라 'textbox' (보조선의 lengthText 는 fabric.Textbox) 도 함께 제거.
// name 기반으로 필터링하는 게 더 안전하다.
const texts = canvas.getObjects().filter((obj) => {
const isTextObj = obj.type === 'text' || obj.type === 'textbox' || obj.name === 'lengthText' || obj.name === 'pitchText'
if (!isTextObj) return false
return obj.attributes?.roofId === roof.id || obj.parentId === roof.id
})
texts.forEach((text) => canvas.remove(text))
const allRoofObject = canvas
.getObjects()
.filter((obj) => obj !== roof && (obj.attributes?.roofId === roof.id || obj.parentId === roof.id || (wall && obj.parentId === wall.id)))
/** 지붕이 움직인 만큼의 delta를 구한다. */
const originalRoofLeft = roof.left
const originalRoofTop = roof.top
roof.set({
left: mousePos.x,
top: mousePos.y,
originX: 'center',
originY: 'center',
objectCaching: false,
})
roof.fire('polygonMoved')
roof.fire('modified')
// 기존 위치와 새로운 위치의 차이를 계산
const deltaX = roof.left - originalRoofLeft
const deltaY = roof.top - originalRoofTop
const originOuterLinePoints = [...outerLinePoints]
setOuterLinePoints(originOuterLinePoints.map((point) => ({ x: point.x + deltaX, y: point.y + deltaY })))
// Move all related objects by the delta
allRoofObject.forEach((obj) => {
if (obj.points !== undefined) {
if (obj.name === 'arrow') {
// arrow 는 originX='left' 기본값이며 polygonMoved 가 left=pathOffset(중심) 으로
// 리셋하면 origin 불일치로 위치가 밀리므로, 단순 점 이동만 수행.
obj.points = obj.points.map((p) => ({ x: p.x + deltaX, y: p.y + deltaY }))
const dim = obj._calcDimensions({})
obj.set({
left: dim.left + dim.width / 2,
top: dim.top + dim.height / 2,
width: dim.width,
height: dim.height,
pathOffset: { x: dim.left + dim.width / 2, y: dim.top + dim.height / 2 },
originX: 'center',
originY: 'center',
objectCaching: false,
})
obj.setCoords()
} else {
obj.set({
left: obj.left + deltaX,
top: obj.top + deltaY,
originX: 'center',
originY: 'center',
objectCaching: false,
})
obj.fire('polygonMoved')
obj.fire('modified')
}
} else {
obj.set({
left: obj.left + deltaX,
top: obj.top + deltaY,
x1: obj.x1 + deltaX,
y1: obj.y1 + deltaY,
x2: obj.x2 + deltaX,
y2: obj.y2 + deltaY,
objectCaching: false,
})
if (obj.type === 'QLine') {
obj.set({
startPoint: { x: obj.startPoint.x + deltaX, y: obj.startPoint.y + deltaY },
endPoint: { x: obj.endPoint.x + deltaX, y: obj.endPoint.y + deltaY },
})
}
obj.setCoords()
}
if (obj.type === 'QLine') {
obj.addLengthText()
}
if (obj.name === 'outerLine') {
const { id } = obj
const pitchText = canvas.getObjects().filter((obj) => obj.name === 'pitchText' && obj.parentId === id)
canvas.remove(...pitchText)
addPitchText(obj)
}
})
canvas.off('mouse:move')
canvas.off('mouse:down')
canvas.remove(checkPolygon)
canvas.renderAll()
})
}
function convertAbsolutePoint(area) {
return area.points.map((p) =>
fabric.util.transformPoint(
{
x: p.x - area.pathOffset.x,
y: p.y - area.pathOffset.y,
},
area.calcTransformMatrix(),
),
)
}
const removeOuterLines = (currentMousePos) => {
const roofBase = canvas
.getObjects()
.filter((obj) => obj.name === POLYGON_TYPE.ROOF)
.filter((roof) => roof.inPolygon(currentMousePos))
if (roofBase.length === 0) {
return
}
const roof = roofBase[0]
const wall = roof.wall
canvas.remove(roof)
canvas.remove(wall)
const allRoofObject = canvas
.getObjects()
.filter(
(obj) => /*obj !== roof && obj !== wall &&*/ obj.attributes?.roofId === roof.id || obj.parentId === roof.id || obj.parentId === wall?.id,
)
const auxilaryObject = canvas.getObjects().filter((obj) => obj.name === 'auxiliaryLine' && !obj.isAuxiliaryFixed)
allRoofObject.forEach((obj) => {
canvas.remove(obj)
})
auxilaryObject.forEach((obj) => {
canvas.remove(obj)
})
canvas.renderAll()
resetPoints()
}
return { setSurfaceShapePattern, removeRoofMaterial, removeAllRoofMaterial, moveRoofMaterial, removeOuterLines }
}