지붕 회전 시 내부 오브젝트(개구, 도머, 그림자) 회전 반영 #348

Merged
ysCha merged 1 commits from dev into prd-deploy 2025-09-19 13:03:59 +09:00
5 changed files with 315 additions and 35 deletions

View File

@ -714,7 +714,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
}, },
inPolygonImproved(point) { inPolygonImproved(point) {
const vertices = this.points const vertices = this.getCurrentPoints()
let inside = false let inside = false
const testX = Number(point.x.toFixed(this.toFixed)) const testX = Number(point.x.toFixed(this.toFixed))
const testY = Number(point.y.toFixed(this.toFixed)) const testY = Number(point.y.toFixed(this.toFixed))
@ -726,7 +726,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
const yj = Number(vertices[j].y.toFixed(this.toFixed)) const yj = Number(vertices[j].y.toFixed(this.toFixed))
// 점이 정점 위에 있는지 확인 // 점이 정점 위에 있는지 확인
if (Math.abs(xi - testX) < 0.01 && Math.abs(yi - testY) < 0.01) { if (Math.abs(xi - testX) <= 0.01 && Math.abs(yi - testY) <= 0.01) {
return true return true
} }

View File

@ -15,6 +15,7 @@ import {
} from '@/store/canvasAtom' } from '@/store/canvasAtom'
import { calculateVisibleModuleHeight, getDegreeByChon, polygonToTurfPolygon, rectToPolygon, toFixedWithoutRounding } from '@/util/canvas-util' import { calculateVisibleModuleHeight, getDegreeByChon, polygonToTurfPolygon, rectToPolygon, toFixedWithoutRounding } from '@/util/canvas-util'
import '@/util/fabric-extensions' // fabric 객체들에 getCurrentPoints 메서드 추가
import { basicSettingState, roofDisplaySelector } from '@/store/settingAtom' import { basicSettingState, roofDisplaySelector } from '@/store/settingAtom'
import offsetPolygon, { calculateAngle, createLinesFromPolygon } from '@/util/qpolygon-utils' import offsetPolygon, { calculateAngle, createLinesFromPolygon } from '@/util/qpolygon-utils'
import { QPolygon } from '@/components/fabric/QPolygon' import { QPolygon } from '@/components/fabric/QPolygon'
@ -265,14 +266,14 @@ export function useModuleBasicSetting(tabNum) {
batchObjects.forEach((obj) => { batchObjects.forEach((obj) => {
//도머일때 //도머일때
if (obj.name === BATCH_TYPE.TRIANGLE_DORMER || obj.name === BATCH_TYPE.PENTAGON_DORMER) { if (obj.name === BATCH_TYPE.TRIANGLE_DORMER || obj.name === BATCH_TYPE.PENTAGON_DORMER) {
const groupPoints = obj.groupPoints const groupPoints = obj.getCurrentPoints()
const offsetObjects = offsetPolygon(groupPoints, 10) const offsetObjects = offsetPolygon(groupPoints, 10)
const dormerOffset = new QPolygon(offsetObjects, batchObjectOptions) const dormerOffset = new QPolygon(offsetObjects, batchObjectOptions)
dormerOffset.setViewLengthText(false) dormerOffset.setViewLengthText(false)
canvas.add(dormerOffset) //모듈설치면 만들기 canvas.add(dormerOffset) //모듈설치면 만들기
} else { } else {
//개구, 그림자일때 //개구, 그림자일때
const points = obj.points const points = obj.getCurrentPoints()
const offsetObjects = offsetPolygon(points, 10) const offsetObjects = offsetPolygon(points, 10)
const offset = new QPolygon(offsetObjects, batchObjectOptions) const offset = new QPolygon(offsetObjects, batchObjectOptions)
offset.setViewLengthText(false) offset.setViewLengthText(false)
@ -319,7 +320,7 @@ export function useModuleBasicSetting(tabNum) {
const margin = moduleSelectionData.common.margin ? moduleSelectionData.common.margin : 200 const margin = moduleSelectionData.common.margin ? moduleSelectionData.common.margin : 200
//육지붕일때는 그냥 하드코딩 //육지붕일때는 그냥 하드코딩
offsetPoints = offsetPolygon(roof.points, -Number(margin) / 10) //육지붕일때 offsetPoints = offsetPolygon(roof.getCurrentPoints(), -Number(margin) / 10) //육지붕일때
} else { } else {
//육지붕이 아닐때 //육지붕이 아닐때
if (allPointsOutside) { if (allPointsOutside) {

View File

@ -675,6 +675,8 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
} }
}) })
objectGroup.recalculateGroupPoints()
isDown = false isDown = false
initEvent() initEvent()
// dbClickEvent() // dbClickEvent()
@ -1426,7 +1428,7 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
//그림자는 아무데나 설치 할 수 있게 해달라고 함 //그림자는 아무데나 설치 할 수 있게 해달라고 함
if (obj.name === BATCH_TYPE.OPENING) { if (obj.name === BATCH_TYPE.OPENING) {
const turfObject = pointsToTurfPolygon(obj.points) const turfObject = pointsToTurfPolygon(obj.getCurrentPoints())
if (turf.booleanWithin(turfObject, turfSurface)) { if (turf.booleanWithin(turfObject, turfSurface)) {
obj.set({ obj.set({
@ -1459,7 +1461,7 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
const calcLeft = obj.left - originLeft const calcLeft = obj.left - originLeft
const calcTop = obj.top - originTop const calcTop = obj.top - originTop
const currentDormerPoints = obj.groupPoints.map((item) => { const currentDormerPoints = obj.getCurrentPoints().map((item) => {
return { return {
x: item.x + calcLeft, x: item.x + calcLeft,
y: item.y + calcTop, y: item.y + calcTop,

View File

@ -1439,44 +1439,89 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
const rotateSurfaceShapeBatch = () => { const rotateSurfaceShapeBatch = () => {
if (currentObject) { if (currentObject) {
// 기존 관련 객체들 제거 // 관련 객체들 찾기
const relatedObjects = canvas // arrow는 제거
.getObjects() const arrow = canvas.getObjects().find((obj) => obj.parentId === currentObject.id && obj.name === 'arrow')
.filter( if (arrow) {
(obj) => canvas.remove(arrow)
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({ const relatedObjects = canvas.getObjects().filter((obj) => obj.parentId === currentObject.id)
originWidth: originHeight,
originHeight: originWidth, // 그룹화할 객체들 배열 (currentObject + relatedObjects)
const objectsToGroup = [currentObject, ...relatedObjects]
// 기존 객체들을 캔버스에서 제거
objectsToGroup.forEach((obj) => canvas.remove(obj))
// fabric.Group 생성
const group = new fabric.Group(objectsToGroup, {
originX: 'center',
originY: 'center',
}) })
currentObject.setCoords() // 그룹을 캔버스에 추가
currentObject.fire('modified') canvas.add(group)
// 현재 회전값에 90도 추가
const currentAngle = group.angle || 0
const newAngle = (currentAngle + 90) % 360
// 그룹 전체를 회전
group.rotate(newAngle)
group.setCoords()
// 그룹을 해제하고 개별 객체로 복원
group._restoreObjectsState()
canvas.remove(group)
// 개별 객체들을 다시 캔버스에 추가하고 처리
group.getObjects().forEach((obj) => {
canvas.add(obj)
obj.setCoords()
// currentObject인 경우 추가 처리
if (obj.id === currentObject.id) {
const originWidth = obj.originWidth
const originHeight = obj.originHeight
// QPolygon 내부 구조 재구성 (선이 깨지는 문제 해결)
if (obj.type === 'QPolygon' && obj.lines) {
obj.initLines()
}
obj.set({
originWidth: originHeight,
originHeight: originWidth,
})
} else {
// relatedObject인 경우에도 필요한 처리
if (obj.type === 'QPolygon' && obj.lines) {
obj.initLines()
}
if (obj.type === 'group') {
// 회전 후의 points를 groupPoints로 업데이트
// getCurrentPoints를 직접 호출하지 말고 recalculateGroupPoints만 실행
obj.recalculateGroupPoints()
obj._objects?.forEach((obj) => {
obj.initLines()
obj.fire('modified')
})
}
}
})
currentObject.fire('modified')
// 화살표와 선 다시 그리기 // 화살표와 선 다시 그리기
drawDirectionArrow(currentObject) drawDirectionArrow(currentObject)
setTimeout(() => { setTimeout(() => {
setPolygonLinesActualSize(currentObject) setPolygonLinesActualSize(currentObject)
changeSurfaceLineType(currentObject) changeSurfaceLineType(currentObject)
}, 200) }, 500)
// currentObject를 다시 선택 상태로 설정
canvas.setActiveObject(currentObject)
canvas.renderAll() canvas.renderAll()
} }
} }

View File

@ -0,0 +1,232 @@
import { fabric } from 'fabric'
/**
* fabric.Rect에 getCurrentPoints 메서드를 추가
* QPolygon의 getCurrentPoints와 동일한 방식으로 변형된 현재 점들을 반환
*/
fabric.Rect.prototype.getCurrentPoints = function () {
// 사각형의 네 모서리 점들을 계산
const width = this.width
const height = this.height
// 사각형의 로컬 좌표계에서의 네 모서리 점
const points = [
{ x: -width / 2, y: -height / 2 }, // 좌상단
{ x: width / 2, y: -height / 2 }, // 우상단
{ x: width / 2, y: height / 2 }, // 우하단
{ x: -width / 2, y: height / 2 }, // 좌하단
]
// 변형 매트릭스 계산
const matrix = this.calcTransformMatrix()
// 각 점을 변형 매트릭스로 변환
return points.map(function (p) {
const point = new fabric.Point(p.x, p.y)
return fabric.util.transformPoint(point, matrix)
})
}
/**
* fabric.Group에 getCurrentPoints 메서드를 추가 (도머 그룹용)
* 그룹의 groupPoints를 다시 계산하여 반환
*/
fabric.Group.prototype.getCurrentPoints = function () {
// groupPoints를 다시 계산
// 그룹에 groupPoints가 있으면 해당 점들을 사용 (도머의 경우)
if (this.groupPoints && Array.isArray(this.groupPoints)) {
const matrix = this.calcTransformMatrix()
console.log('this.groupPoints', this.groupPoints)
return this.groupPoints.map(function (p) {
const point = new fabric.Point(p.x, p.y)
return fabric.util.transformPoint(point, matrix)
})
}
// groupPoints가 없으면 바운딩 박스를 사용
const bounds = this.getBoundingRect()
const points = [
{ x: bounds.left, y: bounds.top },
{ x: bounds.left + bounds.width, y: bounds.top },
{ x: bounds.left + bounds.width, y: bounds.top + bounds.height },
{ x: bounds.left, y: bounds.top + bounds.height },
]
return points.map(function (p) {
return new fabric.Point(p.x, p.y)
})
}
/**
* fabric.Group에 groupPoints 재계산 메서드 추가
* 그룹 모든 객체의 점들을 기반으로 groupPoints를 새로 계산
* Convex Hull 알고리즘을 사용하여 가장 외곽의 점들만 반환
*/
fabric.Group.prototype.recalculateGroupPoints = function () {
if (!this._objects || this._objects.length === 0) {
return
}
let allPoints = []
// 그룹 내 모든 객체의 점들을 수집
this._objects.forEach(function (obj) {
if (obj.getCurrentPoints && typeof obj.getCurrentPoints === 'function') {
// getCurrentPoints가 있는 객체는 해당 메서드 사용
const objPoints = obj.getCurrentPoints()
allPoints = allPoints.concat(objPoints)
} else if (obj.points && Array.isArray(obj.points)) {
// QPolygon과 같이 points 배열이 있는 경우
const pathOffset = obj.pathOffset || { x: 0, y: 0 }
const matrix = obj.calcTransformMatrix()
const transformedPoints = obj.points
.map(function (p) {
return new fabric.Point(p.x - pathOffset.x, p.y - pathOffset.y)
})
.map(function (p) {
return fabric.util.transformPoint(p, matrix)
})
allPoints = allPoints.concat(transformedPoints)
} else {
// 일반 객체는 바운딩 박스의 네 모서리 점 사용
const bounds = obj.getBoundingRect()
const cornerPoints = [
{ x: bounds.left, y: bounds.top },
{ x: bounds.left + bounds.width, y: bounds.top },
{ x: bounds.left + bounds.width, y: bounds.top + bounds.height },
{ x: bounds.left, y: bounds.top + bounds.height },
]
allPoints = allPoints.concat(
cornerPoints.map(function (p) {
return new fabric.Point(p.x, p.y)
}),
)
}
})
if (allPoints.length > 0) {
// Convex Hull 알고리즘을 사용하여 외곽 점들만 추출
const convexHullPoints = this.getConvexHull(allPoints)
// 그룹의 로컬 좌표계로 변환하기 위해 그룹의 역변환 적용
const groupMatrix = this.calcTransformMatrix()
const invertedMatrix = fabric.util.invertTransform(groupMatrix)
this.groupPoints = convexHullPoints.map(function (p) {
const localPoint = fabric.util.transformPoint(p, invertedMatrix)
return { x: localPoint.x, y: localPoint.y }
})
}
}
/**
* Graham Scan 알고리즘을 사용한 Convex Hull 계산
* 점들의 집합에서 가장 외곽의 점들만 반환
*/
fabric.Group.prototype.getConvexHull = function (points) {
if (points.length < 3) return points
// 중복 점 제거
const uniquePoints = []
const seen = new Set()
points.forEach(function (p) {
const key = `${Math.round(p.x * 10) / 10},${Math.round(p.y * 10) / 10}`
if (!seen.has(key)) {
seen.add(key)
uniquePoints.push({ x: p.x, y: p.y })
}
})
if (uniquePoints.length < 3) return uniquePoints
// 가장 아래쪽 점을 찾기 (y가 가장 작고, 같으면 x가 가장 작은 점)
let pivot = uniquePoints[0]
for (let i = 1; i < uniquePoints.length; i++) {
if (uniquePoints[i].y < pivot.y || (uniquePoints[i].y === pivot.y && uniquePoints[i].x < pivot.x)) {
pivot = uniquePoints[i]
}
}
// 극각에 따라 정렬
const sortedPoints = uniquePoints
.filter(function (p) { return p !== pivot })
.sort(function (a, b) {
const angleA = Math.atan2(a.y - pivot.y, a.x - pivot.x)
const angleB = Math.atan2(b.y - pivot.y, b.x - pivot.x)
if (angleA !== angleB) return angleA - angleB
// 각도가 같으면 거리로 정렬
const distA = Math.pow(a.x - pivot.x, 2) + Math.pow(a.y - pivot.y, 2)
const distB = Math.pow(b.x - pivot.x, 2) + Math.pow(b.y - pivot.y, 2)
return distA - distB
})
// Graham Scan 실행
const hull = [pivot]
for (let i = 0; i < sortedPoints.length; i++) {
const current = sortedPoints[i]
// 반시계방향이 아닌 점들 제거
while (hull.length > 1) {
const p1 = hull[hull.length - 2]
const p2 = hull[hull.length - 1]
const cross = (p2.x - p1.x) * (current.y - p1.y) - (p2.y - p1.y) * (current.x - p1.x)
if (cross > 0) break // 반시계방향이면 유지
hull.pop() // 시계방향이면 제거
}
hull.push(current)
}
return hull
}
/**
* fabric.Triangle에 getCurrentPoints 메서드를 추가
* 삼각형의 꼭짓점을 반환
*/
fabric.Triangle.prototype.getCurrentPoints = function () {
const width = this.width
const height = this.height
// 삼각형의 로컬 좌표계에서의 세 꼭짓점
const points = [
{ x: 0, y: -height / 2 }, // 상단 중앙
{ x: -width / 2, y: height / 2 }, // 좌하단
{ x: width / 2, y: height / 2 }, // 우하단
]
// 변형 매트릭스 계산
const matrix = this.calcTransformMatrix()
// 각 점을 변형 매트릭스로 변환
return points.map(function (p) {
const point = new fabric.Point(p.x, p.y)
return fabric.util.transformPoint(point, matrix)
})
}
/**
* fabric.Polygon에 getCurrentPoints 메서드를 추가 (QPolygon이 아닌 일반 Polygon용)
* QPolygon과 동일한 방식으로 구현
*/
if (!fabric.Polygon.prototype.getCurrentPoints) {
fabric.Polygon.prototype.getCurrentPoints = function () {
const pathOffset = this.get('pathOffset') || { x: 0, y: 0 }
const matrix = this.calcTransformMatrix()
return this.get('points')
.map(function (p) {
return new fabric.Point(p.x - pathOffset.x, p.y - pathOffset.y)
})
.map(function (p) {
return fabric.util.transformPoint(p, matrix)
})
}
}
export default {}