Merge branch 'dev' of https://git.hanasys.jp/qcast3/qcast-front into dev_ysCha

This commit is contained in:
ysCha 2026-01-30 11:39:05 +09:00
commit 3aefa10ffd
3 changed files with 242 additions and 34 deletions

View File

@ -13,6 +13,7 @@ import { usePolygon } from '@/hooks/usePolygon'
import { useObjectBatch } from '@/hooks/object/useObjectBatch'
import { BATCH_TYPE } from '@/common/common'
import { useMouse } from '@/hooks/useMouse'
import { QPolygon } from '@/components/fabric/QPolygon'
export function useCommonUtils() {
const canvas = useRecoilValue(canvasState)
@ -617,6 +618,168 @@ export function useCommonUtils() {
const buttonAct = dormerName == BATCH_TYPE.TRIANGLE_DORMER ? 3 : 4
applyDormers(dormerParams, buttonAct)
} else if (obj.name === 'roof' && obj.type === 'QPolygon') {
// roof(QPolygon) 객체는 순환 참조(lines[].parent -> polygon)로 인해
// fabric.clone() 사용 시 Maximum call stack size exceeded 에러 발생
// getCurrentPoints()를 사용하여 새 QPolygon을 직접 생성
// 원본 객체의 line attributes 복사 (순환 참조 제거)
const lineAttributes = obj.lines.map((line) => ({
type: line.attributes?.type,
offset: line.attributes?.offset,
actualSize: line.attributes?.actualSize,
planeSize: line.attributes?.planeSize,
}))
// 원본 roof의 자식 오브젝트들 찾기 (개구, 그림자, 도머 등)
const childObjectTypes = [BATCH_TYPE.OPENING, BATCH_TYPE.SHADOW, BATCH_TYPE.TRIANGLE_DORMER, BATCH_TYPE.PENTAGON_DORMER]
const childObjects = canvas.getObjects().filter(
(o) => o.parentId === obj.id && childObjectTypes.includes(o.name)
)
// 원본 roof 중심점 계산
const originalPoints = obj.getCurrentPoints()
const originalCenterX = originalPoints.reduce((sum, p) => sum + p.x, 0) / originalPoints.length
const originalCenterY = originalPoints.reduce((sum, p) => sum + p.y, 0) / originalPoints.length
let clonedObj = null
let clonedChildren = []
addCanvasMouseEventListener('mouse:move', (e) => {
const pointer = canvas?.getPointer(e.e)
// 이전 임시 객체들 제거
canvas
.getObjects()
.filter((o) => o.name === 'clonedObj' || o.name === 'clonedChildTemp')
.forEach((o) => canvas?.remove(o))
// 새 QPolygon 생성 (매 move마다 생성하여 위치 업데이트)
const currentPoints = obj.getCurrentPoints()
const centerX = currentPoints.reduce((sum, p) => sum + p.x, 0) / currentPoints.length
const centerY = currentPoints.reduce((sum, p) => sum + p.y, 0) / currentPoints.length
// 이동 오프셋 계산
const offsetX = pointer.x - centerX
const offsetY = pointer.y - centerY
// 포인터 위치로 이동된 새 points 계산
const newPoints = currentPoints.map((p) => ({
x: p.x + offsetX,
y: p.y + offsetY,
}))
clonedObj = new QPolygon(newPoints, {
fill: obj.fill || 'transparent',
stroke: obj.stroke || 'black',
strokeWidth: obj.strokeWidth || 1,
fontSize: 0, // 이동 중에는 lengthText 생성하지 않음 (fontSize=0이면 addLengthText가 스킵됨)
selectable: true,
lockMovementX: true,
lockMovementY: true,
lockRotation: true,
lockScalingX: true,
lockScalingY: true,
name: 'clonedObj',
originX: 'center',
originY: 'center',
pitch: obj.pitch,
surfaceId: obj.surfaceId,
sort: false,
}, canvas)
canvas.add(clonedObj)
// 자식 오브젝트들도 이동해서 미리보기 표시
clonedChildren = []
childObjects.forEach((child) => {
child.clone((clonedChild) => {
clonedChild.set({
left: child.left + offsetX,
top: child.top + offsetY,
name: 'clonedChildTemp',
selectable: false,
evented: false,
})
clonedChildren.push({ original: child, cloned: clonedChild })
canvas.add(clonedChild)
})
})
canvas.renderAll()
})
addCanvasMouseEventListener('mouse:down', (e) => {
if (!clonedObj) return
const newRoofId = uuidv4()
clonedObj.set({
lockMovementX: true,
lockMovementY: true,
name: 'roof',
editable: false,
selectable: true,
id: newRoofId,
direction: obj.direction,
directionText: obj.directionText,
roofMaterial: obj.roofMaterial,
stroke: 'black',
evented: true,
isFixed: false,
fontSize: lengthTextFont.fontSize.value, // 최종 확정 시 fontSize 설정
})
// line attributes 복원
lineAttributes.forEach((attr, index) => {
if (clonedObj.lines[index]) {
clonedObj.lines[index].set({ attributes: attr })
}
})
// 임시 자식 오브젝트들 제거
canvas
.getObjects()
.filter((o) => o.name === 'clonedChildTemp')
.forEach((o) => canvas?.remove(o))
// 자식 오브젝트들 최종 복사 (새 roof의 id를 parentId로 설정)
const pointer = canvas?.getPointer(e.e)
const currentPoints = obj.getCurrentPoints()
const centerX = currentPoints.reduce((sum, p) => sum + p.x, 0) / currentPoints.length
const centerY = currentPoints.reduce((sum, p) => sum + p.y, 0) / currentPoints.length
const offsetX = pointer.x - centerX
const offsetY = pointer.y - centerY
childObjects.forEach((child) => {
child.clone((clonedChild) => {
clonedChild.set({
left: child.left + offsetX,
top: child.top + offsetY,
id: uuidv4(),
parentId: newRoofId, // 새 roof의 id를 부모로 설정
name: child.name,
selectable: true,
evented: true,
})
// 그룹 객체인 경우 groupId도 새로 설정
if (clonedChild.type === 'group') {
clonedChild.set({ groupId: uuidv4() })
}
canvas.add(clonedChild)
})
})
clonedObj.fire('polygonMoved')
clonedObj.fire('modified')
clonedObj.setCoords()
canvas.setActiveObject(clonedObj)
canvas.renderAll()
addLengthText(clonedObj) // fontSize가 설정된 후 lengthText 추가
drawDirectionArrow(clonedObj)
initEvent()
})
} else {
let clonedObj = null
@ -655,32 +818,6 @@ export function useCommonUtils() {
//객체가 그룹일 경우에는 그룹 아이디를 따로 넣어준다
if (clonedObj.type === 'group') clonedObj.set({ groupId: uuidv4() })
//배치면일 경우
if (obj.name === 'roof') {
clonedObj.canvas = canvas // canvas 참조 설정
clonedObj.set({
direction: obj.direction,
directionText: obj.directionText,
roofMaterial: obj.roofMaterial,
stroke: 'black', // 복사된 객체는 선택 해제 상태의 색상으로 설정
selectable: true, // 선택 가능하도록 설정
evented: true, // 마우스 이벤트를 받을 수 있도록 설정
isFixed: false, // containsPoint에서 특별 처리 방지
})
obj.lines.forEach((line, index) => {
clonedObj.lines[index].set({ attributes: line.attributes })
})
clonedObj.fire('polygonMoved') // 내부 좌표 재계산 (points, pathOffset)
clonedObj.fire('modified')
clonedObj.setCoords() // 모든 속성 설정 후 좌표 업데이트
canvas.setActiveObject(clonedObj)
canvas.renderAll()
addLengthText(clonedObj) //수치 추가
drawDirectionArrow(clonedObj) //방향 화살표 추가
}
initEvent()
})
}

View File

@ -152,11 +152,17 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
rect.set({ width: Math.abs(width), height: Math.abs(height) })
// 마우스를 왼쪽으로 드래그한 경우 left를 현재 포인터 위치로 설정
if (width < 0) {
rect.set({ left: Math.abs(pointer.x) })
rect.set({ left: pointer.x })
} else {
rect.set({ left: origX })
}
// 마우스를 위쪽으로 드래그한 경우 top을 현재 포인터 위치로 설정
if (height < 0) {
rect.set({ top: Math.abs(pointer.y) })
rect.set({ top: pointer.y })
} else {
rect.set({ top: origY })
}
canvas?.renderAll()
@ -179,7 +185,8 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
}
}
if (!isCrossChecked) {
// 그림자(SHADOW)는 중복 설치 허용, 개구(OPENING)만 중복 체크
if (!isCrossChecked && buttonAct === 1) {
const preObjects = canvas?.getObjects().filter((obj) => obj.name === BATCH_TYPE.OPENING || obj.name === BATCH_TYPE.SHADOW)
const preObjectsArray = preObjects.map((obj) => rectToPolygon(obj))
const isCross = preObjectsArray.some((object) => turf.booleanOverlap(pointsToTurfPolygon(object), rectPolygon))
@ -266,7 +273,8 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
}
}
if (!isCrossChecked) {
// 그림자(SHADOW)는 중복 설치 허용, 개구(OPENING)만 중복 체크
if (!isCrossChecked && buttonAct === 1) {
const preObjects = canvas?.getObjects().filter((obj) => obj.name === BATCH_TYPE.OPENING || obj.name === BATCH_TYPE.SHADOW)
const preObjectsArray = preObjects.map((obj) => rectToPolygon(obj))
const isCross = preObjectsArray.some((object) => turf.booleanOverlap(pointsToTurfPolygon(object), rectPolygon))

View File

@ -159,10 +159,6 @@ export function useContextMenu() {
}
break
case 'roof':
case 'auxiliaryLine':
case 'hip':
case 'ridge':
case 'eaveHelpLine':
if (selectedMenu === 'surface') {
setContextMenu([
[
@ -249,6 +245,73 @@ export function useContextMenu() {
},
},
],
])
}
break
case 'auxiliaryLine':
case 'hip':
case 'ridge':
case 'eaveHelpLine':
if (selectedMenu === 'surface') {
setContextMenu([
[
{
id: 'sizeEdit',
name: getMessage('contextmenu.size.edit'),
component: <SizeSetting id={popupId} target={currentObject} />,
},
{
id: 'rotate',
name: `${getMessage('contextmenu.rotate')}`,
fn: () => rotateSurfaceShapeBatch(),
},
{
id: 'roofMaterialRemove',
shortcut: ['d', 'D'],
name: `${getMessage('contextmenu.remove')}(D)`,
fn: () => deleteObject(),
},
{
id: 'roofMaterialMove',
shortcut: ['m', 'M'],
name: `${getMessage('contextmenu.move')}(M)`,
fn: () => moveSurfaceShapeBatch(),
},
{
id: 'roofMaterialCopy',
shortcut: ['c', 'C'],
name: `${getMessage('contextmenu.copy')}(C)`,
fn: () => copyObject(),
},
],
[
{
id: 'roofMaterialEdit',
name: getMessage('contextmenu.roof.material.edit'),
component: <ContextRoofAllocationSetting id={popupId} />,
},
{
id: 'linePropertyEdit',
name: getMessage('contextmenu.line.property.edit'),
fn: () => {
if (+canvasSetting.roofSizeSet === 3) {
swalFire({ text: getMessage('contextmenu.line.property.edit.roof.size.3') })
} else {
addPopup(popupId, 1, <PlacementSurfaceLineProperty id={popupId} roof={currentObject} />)
}
},
// component: <LinePropertySetting id={popupId} target={currentObject} />,
},
{
id: 'flowDirectionEdit',
name: getMessage('contextmenu.flow.direction.edit'),
component: <FlowDirectionSetting id={popupId} target={currentObject} />,
},
],
])
} else if (selectedMenu === 'outline') {
setContextMenu([
[
{
id: 'sizeEdit',