Merge pull request 'dev' (#344) from dev into prd-deploy
Reviewed-on: #344
This commit is contained in:
commit
d85dcd37ee
@ -216,6 +216,8 @@ export const SAVE_KEY = [
|
|||||||
'isMultipleOf45',
|
'isMultipleOf45',
|
||||||
'from',
|
'from',
|
||||||
'originColor',
|
'originColor',
|
||||||
|
'originWidth',
|
||||||
|
'originHeight',
|
||||||
]
|
]
|
||||||
|
|
||||||
export const OBJECT_PROTOTYPE = [fabric.Line.prototype, fabric.Polygon.prototype, fabric.Triangle.prototype, fabric.Group.prototype]
|
export const OBJECT_PROTOTYPE = [fabric.Line.prototype, fabric.Polygon.prototype, fabric.Triangle.prototype, fabric.Group.prototype]
|
||||||
|
|||||||
@ -87,6 +87,10 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
|||||||
this.initLines()
|
this.initLines()
|
||||||
this.init()
|
this.init()
|
||||||
this.setShape()
|
this.setShape()
|
||||||
|
const originWidth = this.originWidth ?? this.width
|
||||||
|
const originHeight = this.originHeight ?? this.height
|
||||||
|
this.originWidth = this.angle === 90 || this.angle === 270 ? originHeight : originWidth
|
||||||
|
this.originHeight = this.angle === 90 || this.angle === 270 ? originWidth : originHeight
|
||||||
},
|
},
|
||||||
|
|
||||||
setShape() {
|
setShape() {
|
||||||
@ -126,11 +130,13 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
|||||||
this.on('moving', () => {
|
this.on('moving', () => {
|
||||||
this.initLines()
|
this.initLines()
|
||||||
this.addLengthText()
|
this.addLengthText()
|
||||||
|
this.setCoords()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.on('modified', (e) => {
|
this.on('modified', (e) => {
|
||||||
this.initLines()
|
this.initLines()
|
||||||
this.addLengthText()
|
this.addLengthText()
|
||||||
|
this.setCoords()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.on('selected', () => {
|
this.on('selected', () => {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { useSurfaceShapeBatch } from '@/hooks/surface/useSurfaceShapeBatch'
|
|||||||
|
|
||||||
export default function SizeSetting(props) {
|
export default function SizeSetting(props) {
|
||||||
const contextPopupPosition = useRecoilValue(contextPopupPositionState)
|
const contextPopupPosition = useRecoilValue(contextPopupPositionState)
|
||||||
const [settingTarget, setSettingTarget] = useState(1)
|
const [settingTarget, setSettingTarget] = useState(props.side || 1)
|
||||||
const { id, pos = contextPopupPosition, target } = props
|
const { id, pos = contextPopupPosition, target } = props
|
||||||
const { getMessage } = useMessage()
|
const { getMessage } = useMessage()
|
||||||
const { closePopup } = usePopup()
|
const { closePopup } = usePopup()
|
||||||
@ -47,11 +47,11 @@ export default function SizeSetting(props) {
|
|||||||
<div className="size-option-top">
|
<div className="size-option-top">
|
||||||
<div className="size-option-wrap">
|
<div className="size-option-wrap">
|
||||||
<div className="size-option mb5">
|
<div className="size-option mb5">
|
||||||
<input type="text" className="input-origin mr5" value={target?.width.toFixed(0) * 10} readOnly={true} />
|
<input type="text" className="input-origin mr5" value={(target?.originWidth * 10).toFixed(0)} readOnly={true} />
|
||||||
<span className="normal-font">mm</span>
|
<span className="normal-font">mm</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="size-option">
|
<div className="size-option">
|
||||||
<input type="text" className="input-origin mr5" defaultValue={target?.width.toFixed(0) * 10} ref={widthRef} />
|
<input type="text" className="input-origin mr5" defaultValue={(target?.originWidth * 10).toFixed(0)} ref={widthRef} />
|
||||||
<span className="normal-font">mm</span>
|
<span className="normal-font">mm</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -60,11 +60,11 @@ export default function SizeSetting(props) {
|
|||||||
<div className="size-option-side">
|
<div className="size-option-side">
|
||||||
<div className="size-option-wrap">
|
<div className="size-option-wrap">
|
||||||
<div className="size-option mb5">
|
<div className="size-option mb5">
|
||||||
<input type="text" className="input-origin mr5" value={target?.height.toFixed(0) * 10} readOnly={true} />
|
<input type="text" className="input-origin mr5" value={(target?.originHeight * 10).toFixed(0)} readOnly={true} />
|
||||||
<span className="normal-font">mm</span>
|
<span className="normal-font">mm</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="size-option">
|
<div className="size-option">
|
||||||
<input type="text" className="input-origin mr5" defaultValue={target?.height.toFixed(0) * 10} ref={heightRef} />
|
<input type="text" className="input-origin mr5" defaultValue={(target?.originHeight * 10).toFixed(0)} ref={heightRef} />
|
||||||
<span className="normal-font">mm</span>
|
<span className="normal-font">mm</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -86,7 +86,7 @@ export default function ContextRoofAllocationSetting(props) {
|
|||||||
return (
|
return (
|
||||||
<div className="grid-option-box" key={index}>
|
<div className="grid-option-box" key={index}>
|
||||||
<div className="d-check-radio pop no-text">
|
<div className="d-check-radio pop no-text">
|
||||||
<input type="radio" name="radio01" checked={roof.selected && 'checked'} readOnly={true} />
|
<input type="radio" name="radio01" checked={!!roof.selected} readOnly={true} />
|
||||||
<label
|
<label
|
||||||
htmlFor="ra01"
|
htmlFor="ra01"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@ -189,7 +189,7 @@ export default function ContextRoofAllocationSetting(props) {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="input-origin block"
|
className="input-origin block"
|
||||||
value={roof.hajebichi === '' ? '0' : roof.hajebichi}
|
value={roof.hajebichi ?? ''}
|
||||||
readOnly={roof.roofPchAuth === 'R'}
|
readOnly={roof.roofPchAuth === 'R'}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
e.target.value = normalizeDigits(e.target.value)
|
e.target.value = normalizeDigits(e.target.value)
|
||||||
@ -211,8 +211,7 @@ export default function ContextRoofAllocationSetting(props) {
|
|||||||
e.target.value = normalizeDecimalLimit(e.target.value, 2)
|
e.target.value = normalizeDecimalLimit(e.target.value, 2)
|
||||||
handleChangePitch(e, index)
|
handleChangePitch(e, index)
|
||||||
}}
|
}}
|
||||||
value={currentAngleType === 'slope' ? (roof.pitch || '0') : (roof.angle || '0')}
|
value={currentAngleType === 'slope' ? (roof.pitch ?? '') : (roof.angle ?? '')}
|
||||||
defaultValue={currentAngleType === 'slope' ? (roof.pitch || '0') : (roof.angle || '0')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="absol">{pitchText}</span>
|
<span className="absol">{pitchText}</span>
|
||||||
|
|||||||
@ -86,7 +86,7 @@ export default function RoofAllocationSetting(props) {
|
|||||||
return (
|
return (
|
||||||
<div className="grid-option-box" key={index}>
|
<div className="grid-option-box" key={index}>
|
||||||
<div className="d-check-radio pop no-text">
|
<div className="d-check-radio pop no-text">
|
||||||
<input type="radio" name="radio01" checked={roof.selected} readOnly />
|
<input type="radio" name="radio01" checked={!!roof.selected} readOnly />
|
||||||
<label
|
<label
|
||||||
htmlFor="ra01"
|
htmlFor="ra01"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@ -194,7 +194,7 @@ export default function RoofAllocationSetting(props) {
|
|||||||
e.target.value = normalizeDigits(e.target.value)
|
e.target.value = normalizeDigits(e.target.value)
|
||||||
handleChangeInput(e, 'hajebichi', index)
|
handleChangeInput(e, 'hajebichi', index)
|
||||||
}}
|
}}
|
||||||
value={parseInt(roof.hajebichi)}
|
value={roof.hajebichi ?? ''}
|
||||||
readOnly={roof.roofPchAuth === 'R'}
|
readOnly={roof.roofPchAuth === 'R'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -212,7 +212,7 @@ export default function RoofAllocationSetting(props) {
|
|||||||
e.target.value = normalizeDecimalLimit(e.target.value, 2)
|
e.target.value = normalizeDecimalLimit(e.target.value, 2)
|
||||||
handleChangePitch(e, index)
|
handleChangePitch(e, index)
|
||||||
}}
|
}}
|
||||||
value={currentAngleType === 'slope' ? roof.pitch : roof.angle}
|
value={currentAngleType === 'slope' ? (roof.pitch ?? '') : (roof.angle ?? '')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="absol">{pitchText}</span>
|
<span className="absol">{pitchText}</span>
|
||||||
|
|||||||
@ -174,22 +174,32 @@ export function useRoofAllocationSetting(id) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const firstRes = Array.isArray(res) && res.length > 0 ? res[0] : null
|
||||||
|
|
||||||
setBasicSetting({
|
setBasicSetting({
|
||||||
...basicSetting,
|
...basicSetting,
|
||||||
planNo: res[0].planNo,
|
planNo: firstRes?.planNo ?? planNo,
|
||||||
roofSizeSet: res[0].roofSizeSet,
|
roofSizeSet: firstRes?.roofSizeSet ?? 0,
|
||||||
roofAngleSet: res[0].roofAngleSet,
|
roofAngleSet: firstRes?.roofAngleSet ?? 0,
|
||||||
roofsData: roofsArray,
|
roofsData: roofsArray,
|
||||||
selectedRoofMaterial: selectRoofs.find((roof) => roof.selected),
|
selectedRoofMaterial: selectRoofs.find((roof) => roof.selected),
|
||||||
})
|
})
|
||||||
|
|
||||||
setBasicInfo({
|
setBasicInfo({
|
||||||
planNo: '' + res[0].planNo,
|
planNo: '' + (firstRes?.planNo ?? planNo),
|
||||||
roofSizeSet: '' + res[0].roofSizeSet,
|
roofSizeSet: '' + (firstRes?.roofSizeSet ?? 0),
|
||||||
roofAngleSet: '' + res[0].roofAngleSet,
|
roofAngleSet: '' + (firstRes?.roofAngleSet ?? 0),
|
||||||
})
|
})
|
||||||
//데이터 동기화
|
// 데이터 동기화: 렌더링용 필드 기본값 보정
|
||||||
setCurrentRoofList(selectRoofs)
|
const normalizedRoofs = selectRoofs.map((roof) => ({
|
||||||
|
...roof,
|
||||||
|
width: roof.width ?? '',
|
||||||
|
length: roof.length ?? '',
|
||||||
|
hajebichi: roof.hajebichi ?? '',
|
||||||
|
pitch: roof.pitch ?? '',
|
||||||
|
angle: roof.angle ?? '',
|
||||||
|
}))
|
||||||
|
setCurrentRoofList(normalizedRoofs)
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Data fetching error:', error)
|
console.error('Data fetching error:', error)
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useRecoilValue, useResetRecoilState } from 'recoil'
|
import { useRecoilValue, useResetRecoilState } from 'recoil'
|
||||||
import { canvasSettingState, canvasState, currentCanvasPlanState, globalPitchState } from '@/store/canvasAtom'
|
import { canvasSettingState, canvasState, currentCanvasPlanState, currentObjectState, globalPitchState } from '@/store/canvasAtom'
|
||||||
import { LINE_TYPE, MENU, POLYGON_TYPE } from '@/common/common'
|
import { LINE_TYPE, MENU, POLYGON_TYPE } from '@/common/common'
|
||||||
import { getIntersectionPoint, toFixedWithoutRounding } from '@/util/canvas-util'
|
import { getIntersectionPoint } from '@/util/canvas-util'
|
||||||
import { degreesToRadians } from '@turf/turf'
|
import { degreesToRadians } from '@turf/turf'
|
||||||
import { QPolygon } from '@/components/fabric/QPolygon'
|
import { QPolygon } from '@/components/fabric/QPolygon'
|
||||||
import { useSwal } from '@/hooks/useSwal'
|
import { useSwal } from '@/hooks/useSwal'
|
||||||
@ -21,10 +21,13 @@ import { placementShapeDrawingPointsState } from '@/store/placementShapeDrawingA
|
|||||||
import { getBackGroundImage } from '@/lib/imageActions'
|
import { getBackGroundImage } from '@/lib/imageActions'
|
||||||
import { useCanvasSetting } from '@/hooks/option/useCanvasSetting'
|
import { useCanvasSetting } from '@/hooks/option/useCanvasSetting'
|
||||||
import { useText } from '@/hooks/useText'
|
import { useText } from '@/hooks/useText'
|
||||||
|
import SizeSetting from '@/components/floor-plan/modal/object/SizeSetting'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||||
const { getMessage } = useMessage()
|
const { getMessage } = useMessage()
|
||||||
const { drawDirectionArrow, addPolygon, addLengthText } = usePolygon()
|
const { drawDirectionArrow, addPolygon, addLengthText, setPolygonLinesActualSize } = usePolygon()
|
||||||
const lengthTextFont = useRecoilValue(fontSelector('lengthText'))
|
const lengthTextFont = useRecoilValue(fontSelector('lengthText'))
|
||||||
const resetOuterLinePoints = useResetRecoilState(outerLinePointsState)
|
const resetOuterLinePoints = useResetRecoilState(outerLinePointsState)
|
||||||
const resetPlacementShapeDrawingPoints = useResetRecoilState(placementShapeDrawingPointsState)
|
const resetPlacementShapeDrawingPoints = useResetRecoilState(placementShapeDrawingPointsState)
|
||||||
@ -36,11 +39,13 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
|||||||
const { swalFire } = useSwal()
|
const { swalFire } = useSwal()
|
||||||
const { addCanvasMouseEventListener, initEvent } = useEvent()
|
const { addCanvasMouseEventListener, initEvent } = useEvent()
|
||||||
// const { addCanvasMouseEventListener, initEvent } = useContext(EventContext)
|
// const { addCanvasMouseEventListener, initEvent } = useContext(EventContext)
|
||||||
const { addPopup, closePopup } = usePopup()
|
const { addPopup, closePopup, closeAll } = usePopup()
|
||||||
const { setSurfaceShapePattern } = useRoofFn()
|
const { setSurfaceShapePattern } = useRoofFn()
|
||||||
const { changeCorridorDimensionText } = useText()
|
const { changeCorridorDimensionText } = useText()
|
||||||
const currentCanvasPlan = useRecoilValue(currentCanvasPlanState)
|
const currentCanvasPlan = useRecoilValue(currentCanvasPlanState)
|
||||||
const { fetchSettings } = useCanvasSetting(false)
|
const { fetchSettings } = useCanvasSetting(false)
|
||||||
|
const currentObject = useRecoilValue(currentObjectState)
|
||||||
|
const [popupId, setPopupId] = useState(uuidv4())
|
||||||
|
|
||||||
const applySurfaceShape = (surfaceRefs, selectedType, id) => {
|
const applySurfaceShape = (surfaceRefs, selectedType, id) => {
|
||||||
let length1, length2, length3, length4, length5
|
let length1, length2, length3, length4, length5
|
||||||
@ -879,6 +884,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
|||||||
drawDirectionArrow(roof)
|
drawDirectionArrow(roof)
|
||||||
changeCorridorDimensionText()
|
changeCorridorDimensionText()
|
||||||
addLengthText(roof)
|
addLengthText(roof)
|
||||||
|
roof.setCoords()
|
||||||
initEvent()
|
initEvent()
|
||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
})
|
})
|
||||||
@ -916,71 +922,138 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resizeSurfaceShapeBatch = (side, target, width, height) => {
|
const resizeSurfaceShapeBatch = (side, target, width, height) => {
|
||||||
const originTarget = { ...target }
|
if (!target || target.type !== 'QPolygon') return
|
||||||
|
|
||||||
const objectWidth = target.width
|
width = width / 10
|
||||||
const objectHeight = target.height
|
height = height / 10
|
||||||
const changeWidth = width / 10 / objectWidth
|
|
||||||
const changeHeight = height / 10 / objectHeight
|
|
||||||
let sideX = 'left'
|
|
||||||
let sideY = 'top'
|
|
||||||
|
|
||||||
//그룹 중심점 변경
|
// 현재 QPolygon의 점들 가져오기 (변형 적용된 실제 좌표)
|
||||||
if (side === 2) {
|
const currentPoints = target.getCurrentPoints() || []
|
||||||
sideX = 'right'
|
const angle = target.angle % 360
|
||||||
sideY = 'top'
|
if (currentPoints.length === 0) return
|
||||||
} else if (side === 3) {
|
|
||||||
sideX = 'left'
|
// 현재 바운딩 박스 계산
|
||||||
sideY = 'bottom'
|
let minX = Math.min(...currentPoints.map((p) => p.x))
|
||||||
} else if (side === 4) {
|
let maxX = Math.max(...currentPoints.map((p) => p.x))
|
||||||
sideX = 'right'
|
let minY = Math.min(...currentPoints.map((p) => p.y))
|
||||||
sideY = 'bottom'
|
let maxY = Math.max(...currentPoints.map((p) => p.y))
|
||||||
|
|
||||||
|
let currentWidth = maxX - minX
|
||||||
|
let currentHeight = maxY - minY
|
||||||
|
|
||||||
|
// 회전에 관계없이 단순한 앵커 포인트 계산
|
||||||
|
let anchorX, anchorY
|
||||||
|
switch (side) {
|
||||||
|
case 1: // left-top
|
||||||
|
anchorX = minX
|
||||||
|
anchorY = minY
|
||||||
|
break
|
||||||
|
case 2: // right-top
|
||||||
|
anchorX = maxX
|
||||||
|
anchorY = minY
|
||||||
|
break
|
||||||
|
case 3: // left-bottom
|
||||||
|
anchorX = minX
|
||||||
|
anchorY = maxY
|
||||||
|
break
|
||||||
|
case 4: // right-bottom
|
||||||
|
anchorX = maxX
|
||||||
|
anchorY = maxY
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//변경 전 좌표
|
// 목표 크기 (회전에 관계없이 동일하게 적용)
|
||||||
const newCoords = target.getPointByOrigin(sideX, sideY)
|
let targetWidth = width
|
||||||
|
let targetHeight = height
|
||||||
|
|
||||||
target.set({
|
// 새로운 점들 계산 - 앵커 포인트는 고정, 나머지는 비례적으로 확장
|
||||||
originX: sideX,
|
// 각도와 side에 따라 확장 방향 결정
|
||||||
originY: sideY,
|
const newPoints = currentPoints.map((point) => {
|
||||||
left: newCoords.x,
|
// 앵커 포인트 기준으로 새로운 위치 계산
|
||||||
top: newCoords.y,
|
// side와 각도에 관계없이 일관된 방식으로 처리
|
||||||
|
|
||||||
|
// 앵커 포인트에서 각 점까지의 절대 거리
|
||||||
|
const deltaX = point.x - anchorX
|
||||||
|
const deltaY = point.y - anchorY
|
||||||
|
|
||||||
|
// 새로운 크기에 맞춰 비례적으로 확장
|
||||||
|
const newDeltaX = (deltaX / currentWidth) * targetWidth
|
||||||
|
const newDeltaY = (deltaY / currentHeight) * targetHeight
|
||||||
|
|
||||||
|
const newX = anchorX + newDeltaX
|
||||||
|
const newY = anchorY + newDeltaY
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: newX, // 소수점 1자리로 반올림
|
||||||
|
y: newY,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
target.scaleX = changeWidth
|
// 기존 객체의 속성들을 복사 (scale은 1로 고정)
|
||||||
target.scaleY = changeHeight
|
const originalOptions = {
|
||||||
|
stroke: target.stroke,
|
||||||
const currentPoints = target.getCurrentPoints()
|
strokeWidth: target.strokeWidth,
|
||||||
|
fill: target.fill,
|
||||||
target.set({
|
opacity: target.opacity,
|
||||||
|
visible: target.visible,
|
||||||
|
selectable: target.selectable,
|
||||||
|
evented: target.evented,
|
||||||
|
hoverCursor: target.hoverCursor,
|
||||||
|
moveCursor: target.moveCursor,
|
||||||
|
lockMovementX: target.lockMovementX,
|
||||||
|
lockMovementY: target.lockMovementY,
|
||||||
|
lockRotation: target.lockRotation,
|
||||||
|
lockScalingX: target.lockScalingX,
|
||||||
|
lockScalingY: target.lockScalingY,
|
||||||
|
lockUniScaling: target.lockUniScaling,
|
||||||
|
name: target.name,
|
||||||
|
uuid: target.uuid,
|
||||||
|
roofType: target.roofType,
|
||||||
|
roofMaterial: target.roofMaterial,
|
||||||
|
azimuth: target.azimuth,
|
||||||
|
// tilt: target.tilt,
|
||||||
|
// angle: target.angle,
|
||||||
|
// scale은 항상 1로 고정
|
||||||
scaleX: 1,
|
scaleX: 1,
|
||||||
scaleY: 1,
|
scaleY: 1,
|
||||||
width: toFixedWithoutRounding(width / 10, 1),
|
lines: target.lines,
|
||||||
height: toFixedWithoutRounding(height / 10, 1),
|
// 기타 모든 사용자 정의 속성들
|
||||||
})
|
...Object.fromEntries(
|
||||||
//크기 변경후 좌표를 재 적용
|
Object.entries(target).filter(
|
||||||
const changedCoords = target.getPointByOrigin(originTarget.originX, originTarget.originY)
|
([key, value]) =>
|
||||||
|
!['type', 'left', 'top', 'width', 'height', 'scaleX', 'scaleY', 'points', 'lines', 'texts', 'canvas', 'angle', 'tilt'].includes(key) &&
|
||||||
target.set({
|
typeof value !== 'function',
|
||||||
originX: originTarget.originX,
|
),
|
||||||
originY: originTarget.originY,
|
),
|
||||||
left: changedCoords.x,
|
|
||||||
top: changedCoords.y,
|
|
||||||
})
|
|
||||||
canvas.renderAll()
|
|
||||||
|
|
||||||
//면형상 리사이즈시에만
|
|
||||||
target.fire('polygonMoved')
|
|
||||||
target.points = currentPoints
|
|
||||||
target.fire('modified')
|
|
||||||
|
|
||||||
setSurfaceShapePattern(target, roofDisplay.column, false, target.roofMaterial, true)
|
|
||||||
|
|
||||||
if (target.direction) {
|
|
||||||
drawDirectionArrow(target)
|
|
||||||
}
|
}
|
||||||
target.setCoords()
|
|
||||||
canvas.renderAll()
|
// 기존 QPolygon 제거
|
||||||
|
canvas.remove(target)
|
||||||
|
|
||||||
|
// 새로운 QPolygon 생성 (scale은 1로 고정됨)
|
||||||
|
const newPolygon = new QPolygon(newPoints, originalOptions, canvas)
|
||||||
|
|
||||||
|
newPolygon.set({
|
||||||
|
originWidth: width,
|
||||||
|
originHeight: height,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 캔버스에 추가
|
||||||
|
canvas.add(newPolygon)
|
||||||
|
|
||||||
|
// 선택 상태 유지
|
||||||
|
canvas.setActiveObject(newPolygon)
|
||||||
|
|
||||||
|
newPolygon.fire('modified')
|
||||||
|
setSurfaceShapePattern(newPolygon, null, null, newPolygon.roofMaterial)
|
||||||
|
drawDirectionArrow(newPolygon)
|
||||||
|
newPolygon.setCoords()
|
||||||
|
changeSurfaceLineType(newPolygon)
|
||||||
|
canvas?.renderAll()
|
||||||
|
closeAll()
|
||||||
|
addPopup(popupId, 1, <SizeSetting id={popupId} side={side} target={newPolygon} />)
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeSurfaceLinePropertyEvent = () => {
|
const changeSurfaceLinePropertyEvent = () => {
|
||||||
@ -1364,6 +1437,50 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
|||||||
return orderedPoints
|
return orderedPoints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rotateSurfaceShapeBatch = () => {
|
||||||
|
if (currentObject) {
|
||||||
|
// 기존 관련 객체들 제거
|
||||||
|
const relatedObjects = canvas
|
||||||
|
.getObjects()
|
||||||
|
.filter(
|
||||||
|
(obj) =>
|
||||||
|
obj.parentId === currentObject.id ||
|
||||||
|
(obj.name === 'lengthText' && obj.parentId === currentObject.id) ||
|
||||||
|
(obj.name === 'arrow' && obj.parentId === currentObject.id),
|
||||||
|
)
|
||||||
|
relatedObjects.forEach((obj) => canvas.remove(obj))
|
||||||
|
|
||||||
|
// 현재 회전값에 90도 추가
|
||||||
|
const currentAngle = currentObject.angle || 0
|
||||||
|
const newAngle = (currentAngle + 90) % 360
|
||||||
|
const originWidth = currentObject.originWidth
|
||||||
|
const originHeight = currentObject.originHeight
|
||||||
|
// 회전 적용 (width/height 교체 제거로 도형 깨짐 방지)
|
||||||
|
currentObject.rotate(newAngle)
|
||||||
|
|
||||||
|
// QPolygon 내부 구조 재구성 (선이 깨지는 문제 해결)
|
||||||
|
if (currentObject.type === 'QPolygon' && currentObject.lines) {
|
||||||
|
currentObject.initLines()
|
||||||
|
}
|
||||||
|
|
||||||
|
currentObject.set({
|
||||||
|
originWidth: originHeight,
|
||||||
|
originHeight: originWidth,
|
||||||
|
})
|
||||||
|
|
||||||
|
currentObject.setCoords()
|
||||||
|
currentObject.fire('modified')
|
||||||
|
|
||||||
|
// 화살표와 선 다시 그리기
|
||||||
|
drawDirectionArrow(currentObject)
|
||||||
|
setTimeout(() => {
|
||||||
|
setPolygonLinesActualSize(currentObject)
|
||||||
|
changeSurfaceLineType(currentObject)
|
||||||
|
}, 200)
|
||||||
|
canvas.renderAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
applySurfaceShape,
|
applySurfaceShape,
|
||||||
deleteAllSurfacesAndObjects,
|
deleteAllSurfacesAndObjects,
|
||||||
@ -1373,5 +1490,6 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
|||||||
changeSurfaceLineProperty,
|
changeSurfaceLineProperty,
|
||||||
changeSurfaceLinePropertyReset,
|
changeSurfaceLinePropertyReset,
|
||||||
changeSurfaceLineType,
|
changeSurfaceLineType,
|
||||||
|
rotateSurfaceShapeBatch,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
|
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
|
||||||
import { canvasSettingState, canvasState, currentMenuState, currentObjectState } from '@/store/canvasAtom'
|
import { canvasSettingState, canvasState, currentMenuState, currentObjectState } from '@/store/canvasAtom'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { MENU, POLYGON_TYPE } from '@/common/common'
|
|
||||||
import AuxiliarySize from '@/components/floor-plan/modal/auxiliary/AuxiliarySize'
|
import AuxiliarySize from '@/components/floor-plan/modal/auxiliary/AuxiliarySize'
|
||||||
import { usePopup } from '@/hooks/usePopup'
|
import { usePopup } from '@/hooks/usePopup'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
@ -12,11 +11,9 @@ import { gridColorState } from '@/store/gridAtom'
|
|||||||
import { contextPopupPositionState, contextPopupState } from '@/store/popupAtom'
|
import { contextPopupPositionState, contextPopupState } from '@/store/popupAtom'
|
||||||
import AuxiliaryEdit from '@/components/floor-plan/modal/auxiliary/AuxiliaryEdit'
|
import AuxiliaryEdit from '@/components/floor-plan/modal/auxiliary/AuxiliaryEdit'
|
||||||
import SizeSetting from '@/components/floor-plan/modal/object/SizeSetting'
|
import SizeSetting from '@/components/floor-plan/modal/object/SizeSetting'
|
||||||
import RoofMaterialSetting from '@/components/floor-plan/modal/object/RoofMaterialSetting'
|
|
||||||
import DormerOffset from '@/components/floor-plan/modal/object/DormerOffset'
|
import DormerOffset from '@/components/floor-plan/modal/object/DormerOffset'
|
||||||
import FontSetting from '@/components/common/font/FontSetting'
|
import FontSetting from '@/components/common/font/FontSetting'
|
||||||
import RoofAllocationSetting from '@/components/floor-plan/modal/roofAllocation/RoofAllocationSetting'
|
import RoofAllocationSetting from '@/components/floor-plan/modal/roofAllocation/RoofAllocationSetting'
|
||||||
import LinePropertySetting from '@/components/floor-plan/modal/lineProperty/LinePropertySetting'
|
|
||||||
import FlowDirectionSetting from '@/components/floor-plan/modal/flowDirection/FlowDirectionSetting'
|
import FlowDirectionSetting from '@/components/floor-plan/modal/flowDirection/FlowDirectionSetting'
|
||||||
|
|
||||||
import { useCommonUtils } from './common/useCommonUtils'
|
import { useCommonUtils } from './common/useCommonUtils'
|
||||||
@ -29,7 +26,6 @@ import ColumnRemove from '@/components/floor-plan/modal/module/column/ColumnRemo
|
|||||||
import ColumnInsert from '@/components/floor-plan/modal/module/column/ColumnInsert'
|
import ColumnInsert from '@/components/floor-plan/modal/module/column/ColumnInsert'
|
||||||
import RowRemove from '@/components/floor-plan/modal/module/row/RowRemove'
|
import RowRemove from '@/components/floor-plan/modal/module/row/RowRemove'
|
||||||
import RowInsert from '@/components/floor-plan/modal/module/row/RowInsert'
|
import RowInsert from '@/components/floor-plan/modal/module/row/RowInsert'
|
||||||
import CircuitNumberEdit from '@/components/floor-plan/modal/module/CircuitNumberEdit'
|
|
||||||
import { useObjectBatch } from '@/hooks/object/useObjectBatch'
|
import { useObjectBatch } from '@/hooks/object/useObjectBatch'
|
||||||
import { useSurfaceShapeBatch } from '@/hooks/surface/useSurfaceShapeBatch'
|
import { useSurfaceShapeBatch } from '@/hooks/surface/useSurfaceShapeBatch'
|
||||||
import { fontSelector, globalFontAtom } from '@/store/fontAtom'
|
import { fontSelector, globalFontAtom } from '@/store/fontAtom'
|
||||||
@ -45,6 +41,8 @@ import PlacementSurfaceLineProperty from '@/components/floor-plan/modal/placemen
|
|||||||
import { selectedMenuState } from '@/store/menuAtom'
|
import { selectedMenuState } from '@/store/menuAtom'
|
||||||
import { useTrestle } from './module/useTrestle'
|
import { useTrestle } from './module/useTrestle'
|
||||||
import { useCircuitTrestle } from './useCirCuitTrestle'
|
import { useCircuitTrestle } from './useCirCuitTrestle'
|
||||||
|
import { usePolygon } from '@/hooks/usePolygon'
|
||||||
|
import { useText } from '@/hooks/useText'
|
||||||
|
|
||||||
export function useContextMenu() {
|
export function useContextMenu() {
|
||||||
const canvas = useRecoilValue(canvasState)
|
const canvas = useRecoilValue(canvasState)
|
||||||
@ -64,7 +62,7 @@ export function useContextMenu() {
|
|||||||
const [column, setColumn] = useState(null)
|
const [column, setColumn] = useState(null)
|
||||||
const { handleZoomClear } = useCanvasEvent()
|
const { handleZoomClear } = useCanvasEvent()
|
||||||
const { moveObjectBatch, copyObjectBatch } = useObjectBatch({})
|
const { moveObjectBatch, copyObjectBatch } = useObjectBatch({})
|
||||||
const { moveSurfaceShapeBatch } = useSurfaceShapeBatch({})
|
const { moveSurfaceShapeBatch, rotateSurfaceShapeBatch } = useSurfaceShapeBatch({})
|
||||||
const [globalFont, setGlobalFont] = useRecoilState(globalFontAtom)
|
const [globalFont, setGlobalFont] = useRecoilState(globalFontAtom)
|
||||||
const { addLine, removeLine } = useLine()
|
const { addLine, removeLine } = useLine()
|
||||||
const { removeGrid } = useGrid()
|
const { removeGrid } = useGrid()
|
||||||
@ -73,10 +71,12 @@ export function useContextMenu() {
|
|||||||
const { settingsData, setSettingsDataSave } = useCanvasSetting(false)
|
const { settingsData, setSettingsDataSave } = useCanvasSetting(false)
|
||||||
const { swalFire } = useSwal()
|
const { swalFire } = useSwal()
|
||||||
const { alignModule, modulesRemove, moduleRoofRemove } = useModule()
|
const { alignModule, modulesRemove, moduleRoofRemove } = useModule()
|
||||||
const { removeRoofMaterial, removeAllRoofMaterial, moveRoofMaterial, removeOuterLines } = useRoofFn()
|
const { removeRoofMaterial, removeAllRoofMaterial, moveRoofMaterial, removeOuterLines, setSurfaceShapePattern } = useRoofFn()
|
||||||
const selectedMenu = useRecoilValue(selectedMenuState)
|
const selectedMenu = useRecoilValue(selectedMenuState)
|
||||||
const { isAllComplete, clear: resetModule } = useTrestle()
|
const { isAllComplete, clear: resetModule } = useTrestle()
|
||||||
const { isExistCircuit } = useCircuitTrestle()
|
const { isExistCircuit } = useCircuitTrestle()
|
||||||
|
const { changeCorridorDimensionText } = useText()
|
||||||
|
const { setPolygonLinesActualSize, drawDirectionArrow } = usePolygon()
|
||||||
const currentMenuSetting = () => {
|
const currentMenuSetting = () => {
|
||||||
switch (selectedMenu) {
|
switch (selectedMenu) {
|
||||||
case 'outline':
|
case 'outline':
|
||||||
@ -170,6 +170,11 @@ export function useContextMenu() {
|
|||||||
name: getMessage('contextmenu.size.edit'),
|
name: getMessage('contextmenu.size.edit'),
|
||||||
component: <SizeSetting id={popupId} target={currentObject} />,
|
component: <SizeSetting id={popupId} target={currentObject} />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'rotate',
|
||||||
|
name: `${getMessage('contextmenu.rotate')}`,
|
||||||
|
fn: () => rotateSurfaceShapeBatch(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'roofMaterialRemove',
|
id: 'roofMaterialRemove',
|
||||||
shortcut: ['d', 'D'],
|
shortcut: ['d', 'D'],
|
||||||
|
|||||||
@ -446,6 +446,7 @@
|
|||||||
"contextmenu.remove": "削除",
|
"contextmenu.remove": "削除",
|
||||||
"contextmenu.remove.all": "完全削除",
|
"contextmenu.remove.all": "完全削除",
|
||||||
"contextmenu.move": "移動",
|
"contextmenu.move": "移動",
|
||||||
|
"contextmenu.rotate": "回転",
|
||||||
"contextmenu.copy": "コピー",
|
"contextmenu.copy": "コピー",
|
||||||
"contextmenu.edit": "編集",
|
"contextmenu.edit": "編集",
|
||||||
"contextmenu.module.vertical.align": "モジュールの垂直方向の中央揃え",
|
"contextmenu.module.vertical.align": "モジュールの垂直方向の中央揃え",
|
||||||
|
|||||||
@ -446,6 +446,7 @@
|
|||||||
"contextmenu.remove": "삭제",
|
"contextmenu.remove": "삭제",
|
||||||
"contextmenu.remove.all": "전체 삭제",
|
"contextmenu.remove.all": "전체 삭제",
|
||||||
"contextmenu.move": "이동",
|
"contextmenu.move": "이동",
|
||||||
|
"contextmenu.rotate": "회전",
|
||||||
"contextmenu.copy": "복사",
|
"contextmenu.copy": "복사",
|
||||||
"contextmenu.edit": "편집",
|
"contextmenu.edit": "편집",
|
||||||
"contextmenu.module.vertical.align": "모듈 세로 가운데 정렬",
|
"contextmenu.module.vertical.align": "모듈 세로 가운데 정렬",
|
||||||
|
|||||||
229
src/util/skeleton-utils.js
Normal file
229
src/util/skeleton-utils.js
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
/**
|
||||||
|
* @file skeleton-utils.js
|
||||||
|
* @description 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 Fabric.js 캔버스에 그리는 유틸리티 함수들을 포함합니다.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import SkeletonBuilder from '@/lib/skeletons/SkeletonBuilder';
|
||||||
|
import { fabric } from 'fabric';
|
||||||
|
import { LINE_TYPE } from '@/common/common';
|
||||||
|
import { QLine } from '@/components/fabric/QLine';
|
||||||
|
import { calcLinePlaneSize } from '@/util/qpolygon-utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지붕 폴리곤의 좌표를 스켈레톤 생성에 적합한 형태로 전처리합니다.
|
||||||
|
* - 연속된 중복 좌표를 제거합니다.
|
||||||
|
* - 폴리곤이 닫힌 형태가 되도록 마지막 좌표를 확인하고 필요시 제거합니다.
|
||||||
|
* - 좌표를 시계 방향으로 정렬합니다.
|
||||||
|
* @param {Array<object>} initialPoints - 초기 폴리곤 좌표 배열 (e.g., [{x: 10, y: 10}, ...])
|
||||||
|
* @returns {Array<Array<number>>} 전처리된 좌표 배열 (e.g., [[10, 10], ...])
|
||||||
|
*/
|
||||||
|
const preprocessPolygonCoordinates = (initialPoints) => {
|
||||||
|
// fabric.Point 객체를 [x, y] 배열로 변환
|
||||||
|
let coordinates = initialPoints.map(point => [point.x, point.y]);
|
||||||
|
|
||||||
|
// 연속된 중복 좌표 제거
|
||||||
|
coordinates = coordinates.filter((coord, index) => {
|
||||||
|
if (index === 0) return true;
|
||||||
|
const prev = coordinates[index - 1];
|
||||||
|
return !(coord[0] === prev[0] && coord[1] === prev[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 폴리곤의 첫 점과 마지막 점이 동일하면 마지막 점을 제거하여 닫힌 구조 보장
|
||||||
|
if (coordinates.length > 1 &&
|
||||||
|
coordinates[0][0] === coordinates[coordinates.length - 1][0] &&
|
||||||
|
coordinates[0][1] === coordinates[coordinates.length - 1][1]) {
|
||||||
|
coordinates.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkeletonBuilder 라이브러리는 시계 방향 순서의 좌표를 요구하므로 배열을 뒤집습니다.
|
||||||
|
coordinates.reverse();
|
||||||
|
|
||||||
|
return coordinates;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성된 스켈레톤 엣지들로부터 중복되지 않는 고유한 선분 목록을 추출합니다.
|
||||||
|
* 각 엣지는 작은 폴리곤으로 구성되며, 인접한 엣지들은 선분을 공유하므로 중복 제거가 필요합니다.
|
||||||
|
* @param {Array<object>} skeletonEdges - SkeletonBuilder로부터 반환된 엣지 배열
|
||||||
|
* @returns {Array<object>} 고유한 선분 객체의 배열 (e.g., [{x1, y1, x2, y2, edgeIndex}, ...])
|
||||||
|
*/
|
||||||
|
const extractUniqueLinesFromEdges = (skeletonEdges) => {
|
||||||
|
const uniqueLines = new Set();
|
||||||
|
const linesToDraw = [];
|
||||||
|
|
||||||
|
skeletonEdges.forEach((edge, edgeIndex) => {
|
||||||
|
// 엣지 데이터가 유효한 폴리곤인지 확인
|
||||||
|
if (!edge || !edge.Polygon || edge.Polygon.length < 2) {
|
||||||
|
console.warn(`Edge ${edgeIndex} has invalid polygon data:`, edge.Polygon);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 폴리곤의 각 변을 선분으로 변환
|
||||||
|
for (let i = 0; i < edge.Polygon.length; i++) {
|
||||||
|
const p1 = edge.Polygon[i];
|
||||||
|
const p2 = edge.Polygon[(i + 1) % edge.Polygon.length]; // 다음 점 (마지막 점은 첫 점과 연결)
|
||||||
|
|
||||||
|
// 선분의 시작점과 끝점을 일관된 순서로 정렬하여 정규화된 문자열 키 생성
|
||||||
|
// 이를 통해 동일한 선분(방향만 다른 경우 포함)을 식별하고 중복을 방지
|
||||||
|
const normalizedLineKey = p1.X < p2.X || (p1.X === p2.X && p1.Y < p2.Y)
|
||||||
|
? `${p1.X},${p1.Y}-${p2.X},${p2.Y}`
|
||||||
|
: `${p2.X},${p2.Y}-${p1.X},${p1.Y}`;
|
||||||
|
|
||||||
|
// Set에 정규화된 키가 없으면 새로운 선분으로 간주하고 추가
|
||||||
|
if (!uniqueLines.has(normalizedLineKey)) {
|
||||||
|
uniqueLines.add(normalizedLineKey);
|
||||||
|
linesToDraw.push({
|
||||||
|
x1: p1.X, y1: p1.Y,
|
||||||
|
x2: p2.X, y2: p2.Y,
|
||||||
|
edgeIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return linesToDraw;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다.
|
||||||
|
* @param {string} roofId - 대상 지붕 객체의 ID
|
||||||
|
* @param {fabric.Canvas} canvas - Fabric.js 캔버스 객체
|
||||||
|
* @param {string} textMode - 텍스트 표시 모드
|
||||||
|
*/
|
||||||
|
export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => {
|
||||||
|
try {
|
||||||
|
const roof = canvas?.getObjects().find((object) => object.id === roofId);
|
||||||
|
if (!roof) {
|
||||||
|
console.error(`Roof with id "${roofId}" not found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 기존 스켈레톤 라인 제거
|
||||||
|
const existingSkeletonLines = canvas.getObjects().filter(obj =>
|
||||||
|
obj.parentId === roofId && obj.attributes?.type === 'skeleton'
|
||||||
|
);
|
||||||
|
existingSkeletonLines.forEach(line => canvas.remove(line));
|
||||||
|
|
||||||
|
// 2. 지붕 폴리곤 좌표 전처리
|
||||||
|
const coordinates = preprocessPolygonCoordinates(roof.points);
|
||||||
|
if (coordinates.length < 3) {
|
||||||
|
console.warn("Polygon has less than 3 unique points. Cannot generate skeleton.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 스켈레톤 생성
|
||||||
|
const multiPolygon = [[coordinates]]; // GeoJSON MultiPolygon 형식
|
||||||
|
const skeleton = SkeletonBuilder.BuildFromGeoJSON(multiPolygon);
|
||||||
|
|
||||||
|
if (!skeleton || !skeleton.Edges || skeleton.Edges.length === 0) {
|
||||||
|
console.log('No valid skeleton edges found for this roof.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 스켈레톤 엣지에서 고유 선분 추출
|
||||||
|
const linesToDraw = extractUniqueLinesFromEdges(skeleton.Edges);
|
||||||
|
|
||||||
|
// 5. 캔버스에 스켈레톤 라인 렌더링
|
||||||
|
const skeletonLines = [];
|
||||||
|
const outerLines = pointsToLines(coordinates);
|
||||||
|
|
||||||
|
linesToDraw.forEach((line, index) => {
|
||||||
|
// 외곽선과 겹치는 스켈레톤 라인은 그리지 않음
|
||||||
|
const isOverlapping = outerLines.some(outerLine => linesOverlap(line, outerLine));
|
||||||
|
if (isOverlapping) {
|
||||||
|
console.log(`Skeleton line (edge ${line.edgeIndex}) is overlapping with a baseLine. It will not be drawn.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDiagonal = Math.abs(line.x1 - line.x2) > 1e-6 && Math.abs(line.y1 - line.y2) > 1e-6;
|
||||||
|
|
||||||
|
const skeletonLine = new QLine([line.x1, line.y1, line.x2, line.y2], {
|
||||||
|
parentId: roofId,
|
||||||
|
stroke: '#ff0000', // 스켈레톤은 빨간색으로 표시
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeDashArray: [3, 3], // 점선으로 표시
|
||||||
|
name: isDiagonal ? LINE_TYPE.SUBLINE.HIP : LINE_TYPE.SUBLINE.RIDGE,
|
||||||
|
fontSize: roof.fontSize || 12,
|
||||||
|
textMode: textMode,
|
||||||
|
attributes: {
|
||||||
|
roofId: roofId,
|
||||||
|
type: 'skeleton', // 스켈레톤 타입 식별자
|
||||||
|
skeletonIndex: line.edgeIndex,
|
||||||
|
lineIndex: index,
|
||||||
|
planeSize: calcLinePlaneSize(line),
|
||||||
|
actualSize: calcLinePlaneSize(line), // 실제 크기는 필요시 별도 계산 로직 추가
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
skeletonLine.startPoint = { x: line.x1, y: line.y1 };
|
||||||
|
skeletonLine.endPoint = { x: line.x2, y: line.y2 };
|
||||||
|
|
||||||
|
skeletonLines.push(skeletonLine);
|
||||||
|
canvas.add(skeletonLine);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. roof 객체에 스켈레톤 라인 정보 업데이트
|
||||||
|
roof.innerLines = [...(roof.innerLines || []), ...skeletonLines];
|
||||||
|
skeletonLines.forEach(line => line.bringToFront());
|
||||||
|
|
||||||
|
canvas.renderAll();
|
||||||
|
console.log(`Successfully drew ${linesToDraw.length} unique skeleton lines from ${skeleton.Edges.length} polygons.`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('An error occurred while generating the skeleton:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 두 선분이 동일 선상에 있으면서 서로 겹치는지 확인합니다.
|
||||||
|
* @param {object} line1 - 첫 번째 선분 객체 { x1, y1, x2, y2 }
|
||||||
|
* @param {object} line2 - 두 번째 선분 객체 { x1, y1, x2, y2 }
|
||||||
|
* @param {number} [epsilon=1e-6] - 부동 소수점 계산 오차 허용 범위
|
||||||
|
* @returns {boolean} 동일 선상에서 겹치면 true, 아니면 false
|
||||||
|
*/
|
||||||
|
function linesOverlap(line1, line2, epsilon = 1e-6) {
|
||||||
|
// 1. 세 점의 면적 계산을 통해 동일 선상에 있는지 확인 (면적이 0에 가까우면 동일 선상)
|
||||||
|
const area1 = (line1.x2 - line1.x1) * (line2.y1 - line1.y1) - (line2.x1 - line1.x1) * (line1.y2 - line1.y1);
|
||||||
|
const area2 = (line1.x2 - line1.x1) * (line2.y2 - line1.y1) - (line2.x2 - line1.x1) * (line1.y2 - line1.y1);
|
||||||
|
|
||||||
|
if (Math.abs(area1) > epsilon || Math.abs(area2) > epsilon) {
|
||||||
|
return false; // 동일 선상에 없음
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 동일 선상에 있을 경우, 두 선분의 범위(x, y)가 겹치는지 확인
|
||||||
|
const xOverlap = Math.max(line1.x1, line1.x2) >= Math.min(line2.x1, line2.x2) &&
|
||||||
|
Math.max(line2.x1, line2.x2) >= Math.min(line1.x1, line1.x2);
|
||||||
|
|
||||||
|
const yOverlap = Math.max(line1.y1, line1.y2) >= Math.min(line2.y1, line2.y2) &&
|
||||||
|
Math.max(line2.y1, line2.y2) >= Math.min(line1.y1, line1.y2);
|
||||||
|
|
||||||
|
return xOverlap && yOverlap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 점들의 배열을 닫힌 폴리곤을 구성하는 선분 객체의 배열로 변환합니다.
|
||||||
|
* @param {Array<Array<number>>} points - [x, y] 형태의 점 좌표 배열
|
||||||
|
* @returns {Array<{x1: number, y1: number, x2: number, y2: number}>} 선분 객체 배열
|
||||||
|
*/
|
||||||
|
function pointsToLines(points) {
|
||||||
|
if (!points || points.length < 2) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = [];
|
||||||
|
const numPoints = points.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < numPoints; i++) {
|
||||||
|
const startPoint = points[i];
|
||||||
|
const endPoint = points[(i + 1) % numPoints]; // 마지막 점은 첫 번째 점과 연결
|
||||||
|
|
||||||
|
lines.push({
|
||||||
|
x1: startPoint[0],
|
||||||
|
y1: startPoint[1],
|
||||||
|
x2: endPoint[0],
|
||||||
|
y2: endPoint[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user