Merge pull request 'dev' (#692) from dev into dev-deploy

Reviewed-on: #692
This commit is contained in:
ysCha 2026-03-09 13:37:31 +09:00
commit 7994f0ef16
5 changed files with 155 additions and 147 deletions

View File

@ -84,12 +84,12 @@ export default function AuxiliarySize(props) {
//1 2 , 2 1 . //1 2 , 2 1 .
if (checkedRadio === 1) { if (checkedRadio === 1) {
const newX2 = Big(x1).plus(dx.times(scaleFactor)) const newX2 = Big(x1).plus(dx.times(scaleFactor)).toNumber()
const newY2 = Big(y1).plus(dy.times(scaleFactor)) const newY2 = Big(y1).plus(dy.times(scaleFactor)).toNumber()
currentObject.set({ x2: newX2, y2: newY2 }) currentObject.set({ x2: newX2, y2: newY2 })
} else if (checkedRadio === 2) { } else if (checkedRadio === 2) {
const newX1 = Big(x2).minus(dx.times(scaleFactor)) const newX1 = Big(x2).minus(dx.times(scaleFactor)).toNumber()
const newY1 = Big(y2).minus(dy.times(scaleFactor)) const newY1 = Big(y2).minus(dy.times(scaleFactor)).toNumber()
currentObject.set({ x1: newX1, y1: newY1 }) currentObject.set({ x1: newX1, y1: newY1 })
} }
//planeSize actualSize . //planeSize actualSize .

View File

@ -1,4 +1,5 @@
import { BATCH_TYPE, POLYGON_TYPE } from '@/common/common' import { BATCH_TYPE, POLYGON_TYPE } from '@/common/common'
import { QPolygon } from '@/components/fabric/QPolygon'
import { canvasState } from '@/store/canvasAtom' import { canvasState } from '@/store/canvasAtom'
import { polygonToTurfPolygon } from '@/util/canvas-util' import { polygonToTurfPolygon } from '@/util/canvas-util'
import { useRecoilValue } from 'recoil' import { useRecoilValue } from 'recoil'
@ -44,6 +45,42 @@ export function useModule() {
const { clear: removeTrestleMaterials } = useTrestle() const { clear: removeTrestleMaterials } = useTrestle()
const { checkModuleDisjointSurface } = useTurf() const { checkModuleDisjointSurface } = useTurf()
// clone() 대신 직접 새 QPolygon을 생성하여 Maximum call stack 방지
const createModuleCopy = (module, newLeft, newTop, overrides = {}) => {
const deltaX = newLeft - module.left
const deltaY = newTop - module.top
const newPoints = module.points.map((p) => ({
x: p.x + deltaX,
y: p.y + deltaY,
}))
return new QPolygon(newPoints, {
fill: module.fill,
stroke: module.stroke,
strokeWidth: module.strokeWidth,
opacity: module.opacity,
selectable: module.selectable,
lockMovementX: module.lockMovementX,
lockMovementY: module.lockMovementY,
lockRotation: module.lockRotation,
lockScalingX: module.lockScalingX,
lockScalingY: module.lockScalingY,
parentId: module.parentId,
initOptions: module.initOptions,
direction: module.direction,
arrow: module.arrow,
name: module.name,
surfaceId: module.surfaceId,
moduleInfo: module.moduleInfo,
left: newLeft,
top: newTop,
width: module.width,
height: module.height,
toFixed: 2,
sort: false,
...overrides,
})
}
const moduleMove = (length, direction) => { const moduleMove = (length, direction) => {
const selectedObj = canvas.getActiveObjects() //선택된 객체들을 가져옴 const selectedObj = canvas.getActiveObjects() //선택된 객체들을 가져옴
const selectedIds = selectedObj.map((obj) => obj.id) // selectedObj의 ID 추출 const selectedIds = selectedObj.map((obj) => obj.id) // selectedObj의 ID 추출
@ -256,27 +293,10 @@ export function useModule() {
modules.forEach((module) => { modules.forEach((module) => {
const { top, left } = getPosotion(module, direction, Number(length) + Number(moduleLength) * 10, false) const { top, left } = getPosotion(module, direction, Number(length) + Number(moduleLength) * 10, false)
module.clone((obj) => { copyModule = createModuleCopy(module, left, top)
obj.set({ canvas.add(copyModule)
parentId: module.parentId, copyModules.push(copyModule)
initOptions: module.initOptions, copyModule.setCoords()
direction: module.direction,
arrow: module.arrow,
name: module.name,
type: module.type,
length: module.length,
points: module.points,
surfaceId: module.surfaceId,
moduleInfo: module.moduleInfo,
left,
top,
id: uuidv4(),
})
copyModule = obj
canvas.add(obj)
copyModules.push(obj)
obj.setCoords()
})
if (isOverlapObjects(copyModule, objects) || isOutsideSurface(copyModule, surface)) { if (isOverlapObjects(copyModule, objects) || isOutsideSurface(copyModule, surface)) {
isWarning = true isWarning = true
copyModule.set({ fill: 'red' }) copyModule.set({ fill: 'red' })
@ -318,27 +338,10 @@ export function useModule() {
canvas.discardActiveObject() //선택해제 canvas.discardActiveObject() //선택해제
modules.forEach((module) => { modules.forEach((module) => {
const { top, left } = getPosotion(module, direction, length, true) const { top, left } = getPosotion(module, direction, length, true)
module.clone((obj) => { copyModule = createModuleCopy(module, left, top)
obj.set({ copyModules.push(copyModule)
parentId: module.parentId, canvas.add(copyModule)
initOptions: module.initOptions, canvas.renderAll()
direction: module.direction,
arrow: module.arrow,
name: module.name,
type: module.type,
length: module.length,
points: module.points,
surfaceId: module.surfaceId,
moduleInfo: module.moduleInfo,
left,
top,
id: uuidv4(),
})
copyModules.push(obj)
copyModule = obj
canvas.add(obj)
canvas.renderAll()
})
if ( if (
isOverlapOtherModules(copyModule, otherModules) || isOverlapOtherModules(copyModule, otherModules) ||
@ -412,29 +415,10 @@ export function useModule() {
modules.forEach((module) => { modules.forEach((module) => {
const { top, left } = getPosotion(module, direction, Number(length), true) const { top, left } = getPosotion(module, direction, Number(length), true)
module.clone((obj) => { copyModule = createModuleCopy(module, left, top, { stroke: 'black' })
obj.set({ canvas.add(copyModule)
parentId: module.parentId, copyModules.push(copyModule)
initOptions: module.initOptions, copyModule.setCoords()
stroke: 'black',
direction: module.direction,
arrow: module.arrow,
name: module.name,
type: module.type,
length: module.length,
points: module.points,
moduleInfo: module.moduleInfo,
surfaceId: module.surfaceId,
left,
top,
id: uuidv4(),
})
copyModule = obj
canvas.add(obj)
copyModules.push(obj)
obj.setCoords()
console.log(obj)
})
if ( if (
isOverlapOtherModules(copyModule, otherModules) || isOverlapOtherModules(copyModule, otherModules) ||
isOverlapObjects(copyModule, objects) || isOverlapObjects(copyModule, objects) ||
@ -770,28 +754,10 @@ export function useModule() {
otherModules = getOtherModules(columnModules) otherModules = getOtherModules(columnModules)
columnModules.forEach((module) => { columnModules.forEach((module) => {
const { top, left } = getPosotion(module, type, moduleIntvlHor, true) const { top, left } = getPosotion(module, type, moduleIntvlHor, true)
let copyModule = null const copyModule = createModuleCopy(module, left, top)
module.clone((obj) => { canvas.add(copyModule)
obj.set({ copyModules.push(copyModule)
parentId: module.parentId, copyModule.setCoords()
initOptions: module.initOptions,
direction: module.direction,
arrow: module.arrow,
name: module.name,
type: module.type,
length: module.length,
points: module.points,
moduleInfo: module.moduleInfo,
surfaceId: module.surfaceId,
left,
top,
id: uuidv4(),
})
copyModule = obj
canvas.add(obj)
copyModules.push(obj)
obj.setCoords()
})
canvas.renderAll() canvas.renderAll()
if ( if (
@ -879,29 +845,10 @@ export function useModule() {
otherModules = getOtherModules(rowModules) otherModules = getOtherModules(rowModules)
rowModules.forEach((module) => { rowModules.forEach((module) => {
const { top, left } = getPosotion(module, type, moduleIntvlVer, true) const { top, left } = getPosotion(module, type, moduleIntvlVer, true)
let copyModule = null const copyModule = createModuleCopy(module, left, top)
module.clone((obj) => { canvas.add(copyModule)
obj.set({ copyModules.push(copyModule)
parentId: module.parentId, copyModule.setCoords()
initOptions: module.initOptions,
direction: module.direction,
arrow: module.arrow,
name: module.name,
type: module.type,
length: module.length,
points: module.points,
surfaceId: module.surfaceId,
moduleInfo: module.moduleInfo,
fill: module.fill,
left,
top,
id: uuidv4(),
})
copyModule = obj
canvas.add(obj)
copyModules.push(obj)
obj.setCoords()
})
canvas.renderAll() canvas.renderAll()
if ( if (

View File

@ -184,14 +184,14 @@ export function useModuleBasicSetting(tabNum) {
// 방향에 따른 좌표 설정 // 방향에 따른 좌표 설정
const directionConfig = { const directionConfig = {
south: { coord1: 'y1', coord2: 'y2', findExtreme: Math.max }, south: { coord1: 'y1', coord2: 'y2', findExtreme: Math.max, offsetX: 0, offsetY: 1 },
north: { coord1: 'y1', coord2: 'y2', findExtreme: Math.min }, north: { coord1: 'y1', coord2: 'y2', findExtreme: Math.min, offsetX: 0, offsetY: -1 },
east: { coord1: 'x1', coord2: 'x2', findExtreme: Math.max }, east: { coord1: 'x1', coord2: 'x2', findExtreme: Math.max, offsetX: 1, offsetY: 0 },
west: { coord1: 'x1', coord2: 'x2', findExtreme: Math.min }, west: { coord1: 'x1', coord2: 'x2', findExtreme: Math.min, offsetX: -1, offsetY: 0 },
} }
const config = directionConfig[direction] || directionConfig.south const config = directionConfig[direction] || directionConfig.south
const { coord1, coord2, findExtreme } = config const { coord1, coord2, offsetX, offsetY } = config
// 2. 직선만 필터링 (대각선 제외) // 2. 직선만 필터링 (대각선 제외)
// 남/북: y1 === y2 인 경우 수평 직선 // 남/북: y1 === y2 인 경우 수평 직선
@ -200,22 +200,29 @@ export function useModuleBasicSetting(tabNum) {
if (straightLines.length === 0) return if (straightLines.length === 0) return
// 3. 가장 끝에 있는 직선 찾기 // 3. 폴리곤의 turf polygon 생성 (처마 방향 판별용)
// 남쪽: 가장 하단 (y값이 가장 큰), 북쪽: 가장 상단 (y값이 가장 작은) const points = polygon.getCurrentPoints()
// 동쪽: 가장 오른쪽 (x값이 가장 큰), 서쪽: 가장 왼쪽 (x값이 가장 작은) const turfCoords = points.map((p) => [p.x, p.y])
const extremeValue = findExtreme(...straightLines.map((line) => line[coord1])) turfCoords.push(turfCoords[0]) // 폴리곤 닫기
const eavesLines = straightLines.filter((line) => line[coord1] === extremeValue) const turfPolygon = turf.polygon([turfCoords])
// 4. 직선에 대해 타입 설정 // 4. 각 직선에 대해 처마/용마루 판별
// 라인 중점에서 처마 방향으로 약간 이동한 점이 폴리곤 외부이면 처마(eaves)
// 폴리곤 내부이면 용마루(ridge)
straightLines.forEach((line) => { straightLines.forEach((line) => {
if (eavesLines.includes(line)) { const midX = (line.x1 + line.x2) / 2
// 가장 끝에 있는 직선은 eaves const midY = (line.y1 + line.y2) / 2
// 처마 방향으로 약간 이동한 테스트 포인트
const testPoint = turf.point([midX + offsetX * 0.5, midY + offsetY * 0.5])
if (!turf.booleanPointInPolygon(testPoint, turfPolygon)) {
// 테스트 포인트가 폴리곤 외부 → 처마 방향을 향함
line.attributes = { line.attributes = {
...line.attributes, ...line.attributes,
type: LINE_TYPE.WALLLINE.EAVES, type: LINE_TYPE.WALLLINE.EAVES,
} }
} else { } else {
// 나머지 직선은 ridge // 테스트 포인트가 폴리곤 내부 → 용마루 방향을 향함
line.attributes = { line.attributes = {
...line.attributes, ...line.attributes,
type: LINE_TYPE.SUBLINE.RIDGE, type: LINE_TYPE.SUBLINE.RIDGE,

View File

@ -208,6 +208,13 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
isXInversion: xInversion, isXInversion: xInversion,
isYInversion: yInversion, isYInversion: yInversion,
}) })
// 최초 생성 시 planeSize를 계산된 길이로 저장 (회전 후 좌표 반올림에 의한 오차 방지)
batchSurface.lines.forEach((line) => {
if (!line.attributes.planeSize || line.attributes.planeSize === 0) {
line.attributes.planeSize = line.getLength()
}
})
canvas.setActiveObject(batchSurface) canvas.setActiveObject(batchSurface)
setSurfaceShapePattern(batchSurface, roofDisplay.column) setSurfaceShapePattern(batchSurface, roofDisplay.column)
drawDirectionArrow(batchSurface) drawDirectionArrow(batchSurface)
@ -375,9 +382,9 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
} }
points = [ points = [
{ x: pointer.x, y: pointer.y - parseInt(newLength2) / 2 }, { x: pointer.x, y: pointer.y - newLength2 / 2 },
{ x: pointer.x - parseInt(length1) / 2, y: pointer.y + parseInt(newLength2) / 2 }, { x: pointer.x - length1 / 2, y: pointer.y + newLength2 / 2 },
{ x: pointer.x + parseInt(length1) / 2, y: pointer.y + parseInt(newLength2) / 2 }, { x: pointer.x + length1 / 2, y: pointer.y + newLength2 / 2 },
] ]
break break

View File

@ -1023,6 +1023,9 @@ export const usePolygon = () => {
const line = divideLines[i] const line = divideLines[i]
const { intersections, startPoint, endPoint } = line const { intersections, startPoint, endPoint } = line
// 원본 라인의 기하학적 길이 (비율 계산용)
const originalGeomLength = Math.round(Math.hypot(line.x2 - line.x1, line.y2 - line.y1)) * 10
if (intersections.length === 1) { if (intersections.length === 1) {
const newLinePoint1 = [line.x1, line.y1, intersections[0].x, intersections[0].y] const newLinePoint1 = [line.x1, line.y1, intersections[0].x, intersections[0].y]
const newLinePoint2 = [intersections[0].x, intersections[0].y, line.x2, line.y2] const newLinePoint2 = [intersections[0].x, intersections[0].y, line.x2, line.y2]
@ -1042,25 +1045,43 @@ export const usePolygon = () => {
name: 'newLine', name: 'newLine',
}) })
// 두 라인 중 큰 길이로 통일 // 분할된 각 세그먼트의 기하학적 길이
const length1 = Math.round(Math.hypot(newLine1.x1 - newLine1.x2, newLine1.y1 - newLine1.y2)) * 10 const length1 = Math.round(Math.hypot(newLine1.x1 - newLine1.x2, newLine1.y1 - newLine1.y2)) * 10
const length2 = Math.round(Math.hypot(newLine2.x1 - newLine2.x2, newLine2.y1 - newLine2.y2)) * 10 const length2 = Math.round(Math.hypot(newLine2.x1 - newLine2.x2, newLine2.y1 - newLine2.y2)) * 10
const maxLength = Math.max(length1, length2)
const unifiedPlaneSize = line.attributes.planeSize ?? maxLength // 원본에 planeSize/actualSize가 있으면 비율로 분배, 없으면 기하학적 길이 사용
const unifiedActualSize = line.attributes.actualSize ?? maxLength let planeSize1, planeSize2, actualSize1, actualSize2
if (line.attributes.planeSize && originalGeomLength > 0) {
const ratio1 = length1 / originalGeomLength
const ratio2 = length2 / originalGeomLength
planeSize1 = Math.round(line.attributes.planeSize * ratio1)
planeSize2 = Math.round(line.attributes.planeSize * ratio2)
} else {
planeSize1 = length1
planeSize2 = length2
}
if (line.attributes.actualSize && originalGeomLength > 0) {
const ratio1 = length1 / originalGeomLength
const ratio2 = length2 / originalGeomLength
actualSize1 = Math.round(line.attributes.actualSize * ratio1)
actualSize2 = Math.round(line.attributes.actualSize * ratio2)
} else {
actualSize1 = length1
actualSize2 = length2
}
newLine1.attributes = { newLine1.attributes = {
...line.attributes, ...line.attributes,
planeSize: unifiedPlaneSize, planeSize: planeSize1,
actualSize: unifiedActualSize, actualSize: actualSize1,
} }
newLine1.length = maxLength newLine1.length = length1
newLine2.attributes = { newLine2.attributes = {
...line.attributes, ...line.attributes,
planeSize: unifiedPlaneSize, planeSize: planeSize2,
actualSize: unifiedActualSize, actualSize: actualSize2,
} }
newLine2.length = maxLength newLine2.length = length2
newLines.push(newLine1, newLine2) newLines.push(newLine1, newLine2)
divideLines.splice(i, 1) // 기존 line 제거 divideLines.splice(i, 1) // 기존 line 제거
@ -1080,12 +1101,25 @@ export const usePolygon = () => {
}) })
const calcLength = Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10 const calcLength = Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10
let segPlaneSize, segActualSize
if (line.attributes.planeSize && originalGeomLength > 0) {
segPlaneSize = Math.round(line.attributes.planeSize * (calcLength / originalGeomLength))
} else {
segPlaneSize = calcLength
}
if (line.attributes.actualSize && originalGeomLength > 0) {
segActualSize = Math.round(line.attributes.actualSize * (calcLength / originalGeomLength))
} else {
segActualSize = calcLength
}
newLine.attributes = { newLine.attributes = {
...line.attributes, ...line.attributes,
planeSize: line.attributes.planeSize ?? calcLength, planeSize: segPlaneSize,
actualSize: line.attributes.actualSize ?? calcLength, actualSize: segActualSize,
} }
newLine.length = line.attributes.planeSize ?? calcLength newLine.length = calcLength
newLines.push(newLine) newLines.push(newLine)
currentPoint = minDistancePoint currentPoint = minDistancePoint
@ -1100,12 +1134,25 @@ export const usePolygon = () => {
name: 'newLine', name: 'newLine',
}) })
const lastCalcLength = Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10 const lastCalcLength = Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10
let lastPlaneSize, lastActualSize
if (line.attributes.planeSize && originalGeomLength > 0) {
lastPlaneSize = Math.round(line.attributes.planeSize * (lastCalcLength / originalGeomLength))
} else {
lastPlaneSize = lastCalcLength
}
if (line.attributes.actualSize && originalGeomLength > 0) {
lastActualSize = Math.round(line.attributes.actualSize * (lastCalcLength / originalGeomLength))
} else {
lastActualSize = lastCalcLength
}
newLine.attributes = { newLine.attributes = {
...line.attributes, ...line.attributes,
planeSize: line.attributes.planeSize ?? lastCalcLength, planeSize: lastPlaneSize,
actualSize: line.attributes.actualSize ?? lastCalcLength, actualSize: lastActualSize,
} }
newLine.length = line.attributes.planeSize ?? lastCalcLength newLine.length = lastCalcLength
newLines.push(newLine) newLines.push(newLine)
divideLines.splice(i, 1) // 기존 line 제거 divideLines.splice(i, 1) // 기존 line 제거