Compare commits

...

11 Commits

Author SHA1 Message Date
25c7037e86 Merge pull request 'out 완료' (#465) from dev_cha into dev
Reviewed-on: #465
2025-12-15 01:31:08 +09:00
6c5a0a8a54 out 완료 2025-12-15 01:29:04 +09:00
f9b63bca42 Merge pull request '할당완료' (#462) from dev_cha into dev
Reviewed-on: #462
2025-12-13 17:05:49 +09:00
be6fdce63c 할당완료 2025-12-13 17:04:59 +09:00
53a1f4ed00 group object 좌표 재계산 로직 수정 2025-12-12 15:27:26 +09:00
97a444bccd Merge branch 'dev' of https://git.hanasys.jp/qcast3/qcast-front into dev 2025-12-12 15:20:36 +09:00
a6e935bc15 도머 오프셋 선택 시 오류 수정 2025-12-12 15:20:28 +09:00
f898b4434b Merge pull request 'sortRoofLines 개선' (#460) from dev_cha into dev
Reviewed-on: #460
2025-12-11 01:51:43 +09:00
fef352933f sortRoofLines 개선 2025-12-11 01:51:04 +09:00
ab78399e42 Merge pull request '시뮬레이션 수정' (#458) from dev_ysCha into dev
Reviewed-on: #458
2025-12-10 11:21:20 +09:00
c5d830f6c0 플랜 이동 시 외벽선 남아있는 현상 수정
지붕재 변경 시 같은 지붕재를 가지고 있는 다른 지붕들의 지붕재도 변경 추가
2025-12-10 10:24:37 +09:00
5 changed files with 325 additions and 169 deletions

View File

@ -32,6 +32,7 @@ import { useEvent } from '@/hooks/useEvent'
import { compasDegAtom } from '@/store/orientationAtom' import { compasDegAtom } from '@/store/orientationAtom'
import { hotkeyStore } from '@/store/hotkeyAtom' import { hotkeyStore } from '@/store/hotkeyAtom'
import { usePopup } from '@/hooks/usePopup' import { usePopup } from '@/hooks/usePopup'
import { outerLinePointsState } from '@/store/outerLineAtom'
export default function CanvasFrame() { export default function CanvasFrame() {
const canvasRef = useRef(null) const canvasRef = useRef(null)
@ -45,6 +46,7 @@ export default function CanvasFrame() {
const totalDisplay = useRecoilValue(totalDisplaySelector) // const totalDisplay = useRecoilValue(totalDisplaySelector) //
const { setIsGlobalLoading } = useContext(QcastContext) const { setIsGlobalLoading } = useContext(QcastContext)
const resetModuleStatisticsState = useResetRecoilState(moduleStatisticsState) const resetModuleStatisticsState = useResetRecoilState(moduleStatisticsState)
const resetOuterLinePoints = useResetRecoilState(outerLinePointsState)
const resetMakersState = useResetRecoilState(makersState) const resetMakersState = useResetRecoilState(makersState)
const resetSelectedMakerState = useResetRecoilState(selectedMakerState) const resetSelectedMakerState = useResetRecoilState(selectedMakerState)
const resetSeriesState = useResetRecoilState(seriesState) const resetSeriesState = useResetRecoilState(seriesState)
@ -137,6 +139,7 @@ export default function CanvasFrame() {
const resetRecoilData = () => { const resetRecoilData = () => {
// resetModuleStatisticsState() // resetModuleStatisticsState()
resetOuterLinePoints()
resetMakersState() resetMakersState()
resetSelectedMakerState() resetSelectedMakerState()
resetSeriesState() resetSeriesState()

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'react' import { useEffect, useRef, useState } from 'react'
import { useMessage } from '@/hooks/useMessage' import { useMessage } from '@/hooks/useMessage'
import WithDraggable from '@/components/common/draggable/WithDraggable' import WithDraggable from '@/components/common/draggable/WithDraggable'
import { useRecoilValue } from 'recoil' import { useRecoilValue } from 'recoil'
@ -15,8 +15,8 @@ export default function DormerOffset(props) {
const { closePopup } = usePopup() const { closePopup } = usePopup()
const [arrow1, setArrow1] = useState(null) const [arrow1, setArrow1] = useState(null)
const [arrow2, setArrow2] = useState(null) const [arrow2, setArrow2] = useState(null)
const arrow1LengthRef = useRef() const arrow1LengthRef = useRef(0)
const arrow2LengthRef = useRef() const arrow2LengthRef = useRef(0)
const [arrow1Length, setArrow1Length] = useState(0) const [arrow1Length, setArrow1Length] = useState(0)
const [arrow2Length, setArrow2Length] = useState(0) const [arrow2Length, setArrow2Length] = useState(0)
@ -59,12 +59,12 @@ export default function DormerOffset(props) {
name="" name=""
label="" label=""
className="input-origin block" className="input-origin block"
value={arrow1LengthRef.current.value} value={arrow1LengthRef.current.value ?? 0}
ref={arrow1LengthRef} ref={arrow1LengthRef}
onChange={(value) => setArrow1Length(value)} onChange={(value) => setArrow1Length(value)}
options={{ options={{
allowNegative: false, allowNegative: false,
allowDecimal: false allowDecimal: false,
}} }}
/> />
</div> </div>

View File

@ -374,11 +374,18 @@ export function useRoofAllocationSetting(id) {
setBasicSetting((prev) => { setBasicSetting((prev) => {
return { ...prev, selectedRoofMaterial: newRoofList.find((roof) => roof.selected) } return { ...prev, selectedRoofMaterial: newRoofList.find((roof) => roof.selected) }
}) })
const selectedRoofMaterial = newRoofList.find((roof) => roof.selected)
const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF && obj.roofMaterial?.index === selectedRoofMaterial.index)
roofs.forEach((roof) => {
setSurfaceShapePattern(roof, roofDisplay.column, false, { ...selectedRoofMaterial }, true)
drawDirectionArrow(roof)
})
setRoofList(newRoofList) setRoofList(newRoofList)
setRoofMaterials(newRoofList) setRoofMaterials(newRoofList)
setRoofsStore(newRoofList) setRoofsStore(newRoofList)
const selectedRoofMaterial = newRoofList.find((roof) => roof.selected)
setSurfaceShapePattern(currentObject, roofDisplay.column, false, selectedRoofMaterial, true) setSurfaceShapePattern(currentObject, roofDisplay.column, false, selectedRoofMaterial, true)
drawDirectionArrow(currentObject) drawDirectionArrow(currentObject)
modifyModuleSelectionData() modifyModuleSelectionData()

View File

@ -29,22 +29,39 @@ fabric.Rect.prototype.getCurrentPoints = function () {
/** /**
* fabric.Group에 getCurrentPoints 메서드를 추가 (도머 그룹용) * fabric.Group에 getCurrentPoints 메서드를 추가 (도머 그룹용)
* 그룹groupPoints를 다시 계산하여 반환 * 그룹 객체들의 점들을 수집하여 현재 월드 좌표를 반환
*/ */
fabric.Group.prototype.getCurrentPoints = function () { fabric.Group.prototype.getCurrentPoints = function () {
// groupPoints를 다시 계산 // 그룹 내 객체들로부터 실시간으로 점들을 계산
if (this._objects && this._objects.length > 0) {
let allPoints = []
// 그룹에 groupPoints가 있으면 해당 점들을 사용 (도머의 경우) // 그룹 내 모든 객체의 점들을 수집
if (this.groupPoints && Array.isArray(this.groupPoints)) { this._objects.forEach(function (obj) {
const matrix = this.calcTransformMatrix() if (obj.getCurrentPoints && typeof obj.getCurrentPoints === 'function') {
console.log('this.groupPoints', this.groupPoints) const objPoints = obj.getCurrentPoints()
return this.groupPoints.map(function (p) { allPoints = allPoints.concat(objPoints)
const point = new fabric.Point(p.x, p.y) } else if (obj.points && Array.isArray(obj.points)) {
return fabric.util.transformPoint(point, matrix) 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)
}
}) })
if (allPoints.length > 0) {
// Convex Hull 알고리즘을 사용하여 외곽 점들만 반환
return this.getConvexHull(allPoints)
}
} }
// groupPoints가 없으면 바운딩 박스를 사용 // 객체가 없으면 바운딩 박스를 사용
const bounds = this.getBoundingRect() const bounds = this.getBoundingRect()
const points = [ const points = [
{ x: bounds.left, y: bounds.top }, { x: bounds.left, y: bounds.top },

View File

@ -5,6 +5,7 @@ import { QLine } from '@/components/fabric/QLine'
import { getDegreeByChon } from '@/util/canvas-util' import { getDegreeByChon } from '@/util/canvas-util'
import Big from 'big.js' import Big from 'big.js'
import { QPolygon } from '@/components/fabric/QPolygon' import { QPolygon } from '@/components/fabric/QPolygon'
import wallLine from '@/components/floor-plan/modal/wallLineOffset/type/WallLine'
/** /**
* 지붕 폴리곤의 스켈레톤(중심선) 생성하고 캔버스에 그립니다. * 지붕 폴리곤의 스켈레톤(중심선) 생성하고 캔버스에 그립니다.
@ -317,10 +318,29 @@ const movingLineFromSkeleton = (roofId, canvas) => {
* @param baseLines * @param baseLines
*/ */
export const skeletonBuilder = (roofId, canvas, textMode) => { export const skeletonBuilder = (roofId, canvas, textMode) => {
//처마 //처마
let roof = canvas?.getObjects().find((object) => object.id === roofId) let roof = canvas?.getObjects().find((object) => object.id === roofId)
// [추가] wall 객체를 찾아 roof.lines에 wallId를 직접 주입 (초기화)
// 지붕은 벽을 기반으로 생성되므로 라인의 순서(Index)가 동일합니다.
const wallObj = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId)
if (roof && wallObj && roof.lines && wallObj.lines) {
// 개선된 코드 (기하학적 매칭)
// or use some other unique properties
roof.lines.forEach((rLine, index) => {
// 벽 라인 중에서 시작점과 끝점이 일치하는 라인 찾기
const wLine = wallObj.lines[index]
if (wLine) {
// 안정적인 ID 생성
rLine.attributes.wallLine = wLine.id; // Use the stable ID
// ...
}
})
}
const eavesType = [LINE_TYPE.WALLLINE.EAVES, LINE_TYPE.WALLLINE.HIPANDGABLE] const eavesType = [LINE_TYPE.WALLLINE.EAVES, LINE_TYPE.WALLLINE.HIPANDGABLE]
const gableType = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD] const gableType = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD]
@ -329,40 +349,35 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId) const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId)
//const baseLines = wall.baseLines.filter((line) => line.attributes.planeSize > 0) //const baseLines = wall.baseLines.filter((line) => line.attributes.planeSize > 0)
const baseLines = canvas.getObjects().filter((object) => object.name === 'baseLine' && object.parentId === roofId) || []; const baseLines = canvas.getObjects().filter((object) => object.name === 'baseLine' && object.parentId === roofId) || []
const baseLinePoints = baseLines.map((line) => ({x:line.left, y:line.top})); const baseLinePoints = baseLines.map((line) => ({ x: line.left, y: line.top }))
const outerLines = canvas.getObjects().filter((object) => object.name === 'outerLinePoint') || []; const outerLines = canvas.getObjects().filter((object) => object.name === 'outerLinePoint') || []
const outerLinePoints = outerLines.map((line) => ({x:line.left, y:line.top})) const outerLinePoints = outerLines.map((line) => ({ x: line.left, y: line.top }))
const hipLines = canvas.getObjects().filter((object) => object.name === 'hip' && object.parentId === roofId) || []; const hipLines = canvas.getObjects().filter((object) => object.name === 'hip' && object.parentId === roofId) || []
const ridgeLines = canvas.getObjects().filter((object) => object.name === 'ridge' && object.parentId === roofId) || []; const ridgeLines = canvas.getObjects().filter((object) => object.name === 'ridge' && object.parentId === roofId) || []
//const skeletonLines = []; //const skeletonLines = [];
// 1. 지붕 폴리곤 좌표 전처리 // 1. 지붕 폴리곤 좌표 전처리
const coordinates = preprocessPolygonCoordinates(roof.points); const coordinates = preprocessPolygonCoordinates(roof.points)
if (coordinates.length < 3) { if (coordinates.length < 3) {
console.warn("Polygon has less than 3 unique points. Cannot generate skeleton."); console.warn('Polygon has less than 3 unique points. Cannot generate skeleton.')
return; return
} }
const moveFlowLine = roof.moveFlowLine || 0; // Provide a default value const moveFlowLine = roof.moveFlowLine || 0 // Provide a default value
const moveUpDown = roof.moveUpDown || 0; // Provide a default value const moveUpDown = roof.moveUpDown || 0 // Provide a default value
let points = roof.points;
let points = roof.points
//마루이동 //마루이동
if (moveFlowLine !== 0 || moveUpDown !== 0) { if (moveFlowLine !== 0 || moveUpDown !== 0) {
points = movingLineFromSkeleton(roofId, canvas) points = movingLineFromSkeleton(roofId, canvas)
} }
console.log('points:', points)
console.log('points:', points);
const geoJSONPolygon = toGeoJSON(points) const geoJSONPolygon = toGeoJSON(points)
try { try {
@ -371,7 +386,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]])
// 스켈레톤 데이터를 기반으로 내부선 생성 // 스켈레톤 데이터를 기반으로 내부선 생성
roof.innerLines = roof.innerLines || []; roof.innerLines = roof.innerLines || []
roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode) roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode)
// 캔버스에 스켈레톤 상태 저장 // 캔버스에 스켈레톤 상태 저장
@ -380,12 +395,12 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
canvas.skeletonLines = [] canvas.skeletonLines = []
} }
canvas.skeletonStates[roofId] = true canvas.skeletonStates[roofId] = true
canvas.skeletonLines = []; canvas.skeletonLines = []
canvas.skeletonLines.push(...roof.innerLines) canvas.skeletonLines.push(...roof.innerLines)
roof.skeletonLines = canvas.skeletonLines; roof.skeletonLines = canvas.skeletonLines
const cleanSkeleton = { const cleanSkeleton = {
Edges: skeleton.Edges.map(edge => ({ Edges: skeleton.Edges.map((edge) => ({
X1: edge.Edge.Begin.X, X1: edge.Edge.Begin.X,
Y1: edge.Edge.Begin.Y, Y1: edge.Edge.Begin.Y,
X2: edge.Edge.End.X, X2: edge.Edge.End.X,
@ -396,15 +411,14 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
})), })),
roofId: roofId, roofId: roofId,
// Add other necessary top-level properties // Add other necessary top-level properties
}; }
canvas.skeleton = []; canvas.skeleton = []
canvas.skeleton = cleanSkeleton canvas.skeleton = cleanSkeleton
canvas.skeleton.lastPoints = points canvas.skeleton.lastPoints = points
canvas.set("skeleton", cleanSkeleton); canvas.set('skeleton', cleanSkeleton)
canvas.renderAll() canvas.renderAll()
console.log('skeleton rendered.', canvas)
console.log('skeleton rendered.', canvas);
} catch (e) { } catch (e) {
console.error('스켈레톤 생성 중 오류 발생:', e) console.error('스켈레톤 생성 중 오류 발생:', e)
if (canvas.skeletonStates) { if (canvas.skeletonStates) {
@ -578,7 +592,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
); );
//그림을 그릴때 idx 가 필요함 roof는 왼쪽부터 시작됨 - 그림그리는 순서가 필요함 //그림을 그릴때 idx 가 필요함 roof는 왼쪽부터 시작됨 - 그림그리는 순서가 필요함
let roofIdx = 0;
// roofLines.forEach((roofLine) => { // roofLines.forEach((roofLine) => {
// //
@ -589,13 +603,17 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
// } // }
// }); // });
const skeletonLine = new QLine([p1.x, p1.y, p2.x, p2.y], { const skeletonLine = new QLine([p1.x, p1.y, p2.x, p2.y], {
parentId: roof.id, parentId: roof.id,
fontSize: roof.fontSize, fontSize: roof.fontSize,
stroke: (sktLine.attributes.isOuterEdge)?'orange':lineStyle.color, stroke: (sktLine.attributes.isOuterEdge)?'orange':lineStyle.color,
strokeWidth: lineStyle.width, strokeWidth: lineStyle.width,
name: (sktLine.attributes.isOuterEdge)?'eaves': attributes.type, name: (sktLine.attributes.isOuterEdge)?'eaves': attributes.type,
attributes: attributes, attributes: {
...attributes,
},
direction: direction, direction: direction,
isBaseLine: sktLine.attributes.isOuterEdge, isBaseLine: sktLine.attributes.isOuterEdge,
lineName: (sktLine.attributes.isOuterEdge)?'roofLine': attributes.type, lineName: (sktLine.attributes.isOuterEdge)?'roofLine': attributes.type,
@ -748,14 +766,19 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
}); });
}; };
const sortedWallLines = sortCurrentRoofLines(wall.lines); // const sortedWallLines = sortCurrentRoofLines(wall.lines);
// roofLines의 방향에 맞춰 currentRoofLines 조정 후 정렬 // roofLines의 방향에 맞춰 currentRoofLines 조정 후 정렬
const alignedCurrentRoofLines = alignLineDirection(currentRoofLines, roofLines); const alignedCurrentRoofLines = alignLineDirection(currentRoofLines, roofLines);
const sortedCurrentRoofLines = sortCurrentRoofLines(alignedCurrentRoofLines); const sortedCurrentRoofLines = sortCurrentRoofLines(alignedCurrentRoofLines);
const sortedRoofLines = sortCurrentRoofLines(roofLines); // const sortedRoofLines = sortCurrentRoofLines(roofLines);
const sortedWallBaseLines = sortCurrentRoofLines(wall.baseLines); const sortedWallBaseLines = sortCurrentRoofLines(wall.baseLines);
const sortedBaseLines = sortBaseLinesByWallLines(wall.baseLines, wallLines); // const sortedBaseLines = sortBaseLinesByWallLines(wall.baseLines, wallLines);
const sortRoofLines = sortBaseLinesByWallLines(roofLines, wallLines);
// 원본 wallLines를 복사하여 사용
const sortedWallLines = [...wallLines];
const sortedBaseLines = sortBaseLinesByWallLines(wall.baseLines, sortedWallLines);
const sortedRoofLines = sortBaseLinesByWallLines(roofLines, sortedWallLines);
//wall.lines 는 기본 벽 라인 //wall.lines 는 기본 벽 라인
//wall.baseLine은 움직인라인 //wall.baseLine은 움직인라인
@ -770,35 +793,29 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
// const moveLine = sortedWallBaseLines[index] // const moveLine = sortedWallBaseLines[index]
// const wallBaseLine = sortedWallBaseLines[index] // const wallBaseLine = sortedWallBaseLines[index]
const roofLine = sortRoofLines[index];
if(roofLine.attributes.wallLine !== wallLine.id || (roofLine.idx - 1) !== index ){
console.log("wallLine2::::", wallLine.id)
console.log('roofLine:::',roofLine.attributes.wallLine)
console.log("w:::",wallLine.startPoint, wallLine.endPoint)
console.log("R:::",roofLine.startPoint, roofLine.endPoint)
console.log("not matching roofLine", roofLine);
return false
}//roofLines.find(line => line.attributes.wallLineId === wallLine.attributes.wallId);
const roofLine = roofLines[index];
const currentRoofLine = currentRoofLines[index]; const currentRoofLine = currentRoofLines[index];
const moveLine = sortedBaseLines[index] const moveLine = wall.baseLines[index]
const wallBaseLine = sortedBaseLines[index] const wallBaseLine = wall.baseLines[index]
//console.log("wallBaseLine", wallBaseLine);
//roofline 외곽선 설정 //roofline 외곽선 설정
console.log("index::::", index)
console.log('roofLine:::',roofLine)
console.log('wallLine', wallLine)
console.log('wallBaseLine', wallBaseLine)
// Check if wallBaseLine is inside the polygon formed by sortedWallLines
/*
console.log('=== Line Coordinates ===');
console.table({
'Point' : ['X', 'Y'],
'roofLine' : [roofLine.x1, roofLine.y1],
'currentRoofLine': [currentRoofLine.x1, currentRoofLine.y1],
'moveLine' : [moveLine.x1, moveLine.y1],
'wallBaseLine' : [wallBaseLine.x1, wallBaseLine.y1]
});
console.log('End Points:');
console.table({
'Point' : ['X', 'Y'],
'roofLine' : [roofLine.x2, roofLine.y2],
'currentRoofLine': [currentRoofLine.x2, currentRoofLine.y2],
'moveLine' : [moveLine.x2, moveLine.y2],
'wallBaseLine' : [wallBaseLine.x2, wallBaseLine.y2]
});
*/
const origin = moveLine.attributes?.originPoint const origin = moveLine.attributes?.originPoint
if (!origin) return if (!origin) return
@ -848,7 +865,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
return line return line
} }
getAddLine(roofLine.startPoint, roofLine.endPoint, ) //getAddLine(roofLine.startPoint, roofLine.endPoint, ) //외곽선을 그린다
newPStart = { x: roofLine.x1, y: roofLine.y1 } newPStart = { x: roofLine.x1, y: roofLine.y1 }
newPEnd = { x: roofLine.x2, y: roofLine.y2 } newPEnd = { x: roofLine.x2, y: roofLine.y2 }
@ -941,7 +958,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
const eLineY = Big(bStartY).minus(wallLine.y1).abs().toNumber() const eLineY = Big(bStartY).minus(wallLine.y1).abs().toNumber()
newPStart.y = aStartY newPStart.y = aStartY
newPEnd.y = Big(roofLine.y2).minus(eLineY).toNumber() newPEnd.y = roofLine.y2 //Big(roofLine.y2).minus(eLineY).toNumber()
let idx = (0 >= index - 1)?roofLines.length:index let idx = (0 >= index - 1)?roofLines.length:index
const newLine = roofLines[idx-1]; const newLine = roofLines[idx-1];
@ -968,7 +985,16 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
getAddLine({ y: inLine.y1, x: inLine.x1},{ y: newPStart.y, x: newPStart.x }, 'purple') getAddLine({ y: inLine.y1, x: inLine.x1},{ y: newPStart.y, x: newPStart.x }, 'purple')
} }
}else { }else {
newPStart.y = wallLine.y1; //newPStart.y = wallLine.y1;
//외곽 라인 그리기
const rLineM = Big(wallBaseLine.x2).minus(roofLine.x2).abs().toNumber();
newPStart.y = Big(wallBaseLine.y1).minus(rLineM).abs().toNumber();
const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x })
if(inLine.x2 > inLine.x1 ) {
getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple')
}else{
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x } , 'purple')
}
} }
} }
@ -983,7 +1009,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
console.log("startLines:::::::", inLine); console.log("startLines:::::::", inLine);
const eLineY = Big(bStartY).minus(wallLine.y2).abs().toNumber() const eLineY = Big(bStartY).minus(wallLine.y2).abs().toNumber()
newPEnd.y = aStartY newPEnd.y = aStartY
newPStart.y = Big(roofLine.y1).plus(eLineY).toNumber() newPStart.y = roofLine.y1//Big(roofLine.y1).plus(eLineY).toNumber()
let idx = (roofLines.length < index + 1)?0:index let idx = (roofLines.length < index + 1)?0:index
const newLine = roofLines[idx+1]; const newLine = roofLines[idx+1];
@ -1010,7 +1036,17 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
} }
}else { }else {
newPEnd.y = wallLine.y2 // newPEnd.y = wallLine.y2
//외곽 라인 그리기
const rLineM = Big(wallBaseLine.x2).minus(roofLine.x2).abs().toNumber();
newPEnd.y = Big(wallBaseLine.y2).plus(rLineM).abs().toNumber();
const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x })
if(inLine.x2 > inLine.x1 ) {
getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple')
}else{
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x } , 'purple')
}
} }
} }
@ -1077,7 +1113,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
console.log("startLines:::::::", inLine); console.log("startLines:::::::", inLine);
const eLineY = Big(bStartY).minus(wallLine.y1).abs().toNumber() const eLineY = Big(bStartY).minus(wallLine.y1).abs().toNumber()
newPStart.y = aStartY newPStart.y = aStartY
newPEnd.y = Big(roofLine.y2).plus(eLineY).toNumber() newPEnd.y = roofLine.y2//Big(roofLine.y2).plus(eLineY).toNumber()
let idx = (0 >= index - 1)?roofLines.length:index let idx = (0 >= index - 1)?roofLines.length:index
const newLine = roofLines[idx-1]; const newLine = roofLines[idx-1];
@ -1103,7 +1139,19 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
getAddLine({ y: inLine.y1, x: inLine.x1 },{ y: newPStart.y, x: newPStart.x }, 'purple') getAddLine({ y: inLine.y1, x: inLine.x1 },{ y: newPStart.y, x: newPStart.x }, 'purple')
} }
}else { }else {
newPStart.y = wallLine.y1; //newPStart.y = wallLine.y1;
//외곽 라인 그리기
const rLineM = Big(wallBaseLine.x1).minus(roofLine.x1).abs().toNumber();
newPStart.y = Big(wallBaseLine.y1).plus(rLineM).abs().toNumber();
const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x })
if(inLine){
if(inLine.x2 > inLine.x1 ) {
getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y1, x: inLine.x1 }, 'purple')
}else{
getAddLine({ y: inLine.y2, x: inLine.x2 }, { y: newPStart.y, x: newPStart.x } , 'purple')
}
}
} }
} }
@ -1118,7 +1166,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
console.log("startLines:::::::", inLine); console.log("startLines:::::::", inLine);
const eLineY = Big(bStartY).minus(wallLine.y2).abs().toNumber() const eLineY = Big(bStartY).minus(wallLine.y2).abs().toNumber()
newPEnd.y = aStartY newPEnd.y = aStartY
newPStart.y = Big(roofLine.y1).minus(eLineY).toNumber() newPStart.y = roofLine.y1//Big(roofLine.y1).minus(eLineY).toNumber()
let idx = (roofLines.length < index + 1)?0:index let idx = (roofLines.length < index + 1)?0:index
const newLine = roofLines[idx+1]; const newLine = roofLines[idx+1];
if(inLine){ if(inLine){
@ -1143,26 +1191,26 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple') getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple')
} }
}else { }else {
newPEnd.y = wallLine.y2; //newPEnd.y = wallLine.y2;
//외곽 라인 그리기
const rLineM = Big(wallBaseLine.x2).minus(roofLine.x2).abs().toNumber();
newPEnd.y = Big(wallBaseLine.y2).minus(rLineM).abs().toNumber();
const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x })
if(inLine){
if(inLine.x2 > inLine.x1 ) {
getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y1, x: inLine.x1 }, 'purple')
}else{
getAddLine({ y: inLine.y2, x: inLine.x2}, { y: newPEnd.y, x: newPEnd.x } , 'purple')
}
}
} }
} }
} }
} }
// switch (condition) {
// case 'left_in':
// break;
// case 'left_out':
// break;
// case 'right_in':
// break;
// case 'right_out':
// break;
// }
} }
} else if (getOrientation(roofLine) === 'horizontal') { //red } else if (getOrientation(roofLine) === 'horizontal') { //red
if (['top', 'bottom'].includes(mLine.position)) { if (['top', 'bottom'].includes(mLine.position)) {
@ -1200,6 +1248,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink')
} }
//getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: roofLine.x2, y: newPointY }, 'orange') //getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: roofLine.x2, y: newPointY }, 'orange')
} }
if(isStartEnd.end){ if(isStartEnd.end){
@ -1220,11 +1269,14 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green')
getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink')
} }
//getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: roofLine.x1, y: newPointY }, 'orange') //getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: roofLine.x1, y: newPointY }, 'orange')
} }
}else if(condition === 'top_out') { }else if(condition === 'top_out') {
console.log("top_out isStartEnd:::::::", isStartEnd); console.log("top_out isStartEnd:::::::", isStartEnd);
if (isStartEnd.start ) { if (isStartEnd.start ) {
const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber()
const aStartX = Big(roofLine.x1).plus(moveDist).toNumber() const aStartX = Big(roofLine.x1).plus(moveDist).toNumber()
@ -1232,7 +1284,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: newPEnd.y }) const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: newPEnd.y })
const eLineX = Big(bStartX).minus(wallLine.x1).abs().toNumber() const eLineX = Big(bStartX).minus(wallLine.x1).abs().toNumber()
newPEnd.x = Big(newPEnd.x).plus(eLineX).toNumber() newPEnd.x = roofLine.x2 //Big(newPEnd.x).plus(eLineX).toNumber()
newPStart.x = aStartX newPStart.x = aStartX
let idx = (0 > index - 1)?roofLines.length:index let idx = (0 > index - 1)?roofLines.length:index
const newLine = roofLines[idx-1]; const newLine = roofLines[idx-1];
@ -1260,7 +1312,17 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
} }
}else { }else {
newPStart.x = wallLine.x1; //외곽 라인 그리기
const rLineM = Big(wallBaseLine.y1).minus(roofLine.y1).abs().toNumber();
newPStart.x = Big(wallBaseLine.x1).plus(rLineM).abs().toNumber();
const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x })
if(inLine){
if(inLine.y2 > inLine.y1 ) {
getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple')
}else{
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPStart.y, x: newPStart.x } , 'purple')
}
}
} }
} }
@ -1272,7 +1334,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: newPEnd.y }) const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: newPEnd.y })
console.log("startLines:::::::", inLine); console.log("startLines:::::::", inLine);
const eLineX = Big(bStartX).minus(wallLine.x2).abs().toNumber() const eLineX = Big(bStartX).minus(wallLine.x2).abs().toNumber()
newPStart.x = Big(newPStart.x).minus(eLineX).abs().toNumber() newPStart.x = roofLine.x1;//Big(newPStart.x).minus(eLineX).abs().toNumber()
newPEnd.x = aStartX newPEnd.x = aStartX
let idx = (roofLines.length < index + 1)?0:index let idx = (roofLines.length < index + 1)?0:index
const newLine = roofLines[idx+1]; const newLine = roofLines[idx+1];
@ -1300,7 +1362,18 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
getAddLine({ y: inLine.y1, x: inLine.x1 },{ y: newPEnd.y, x: newPEnd.x }, 'purple') getAddLine({ y: inLine.y1, x: inLine.x1 },{ y: newPEnd.y, x: newPEnd.x }, 'purple')
} }
}else { }else {
newPEnd.x = wallLine.x2; //newPEnd.x = wallLine.x2;
//외곽 라인 그리기
const rLineM = Big(wallBaseLine.y2).minus(roofLine.y2).abs().toNumber();
newPEnd.x = Big(wallBaseLine.x2).minus(rLineM).abs().toNumber();
const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x })
if(inLine){
if(inLine.y1 > inLine.y2 ) {
getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y1, x: inLine.x1 }, 'purple')
}else{
getAddLine({ y: inLine.y2, x: inLine.x2 }, { y: newPEnd.y, x: newPEnd.x } , 'purple')
}
}
} }
} }
@ -1359,7 +1432,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: roofLine.y1 }) const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: roofLine.y1 })
console.log("startLines:::::::", inLine); console.log("startLines:::::::", inLine);
const eLineX = Big(bStartX).minus(wallLine.x1).abs().toNumber() const eLineX = Big(bStartX).minus(wallLine.x1).abs().toNumber()
newPEnd.x = Big(roofLine.x2).minus(eLineX).toNumber() newPEnd.x = roofLine.x2//Big(roofLine.x2).minus(eLineX).toNumber()
newPStart.x = aStartX newPStart.x = aStartX
let idx = (0 > index - 1)?roofLines.length:index let idx = (0 > index - 1)?roofLines.length:index
const newLine = roofLines[idx-1]; const newLine = roofLines[idx-1];
@ -1387,7 +1460,18 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPStart.y, x: newPStart.x }, 'purple') getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPStart.y, x: newPStart.x }, 'purple')
} }
}else{ }else{
newPStart.x = wallLine.x1; //newPStart.x = wallLine.x1;
//외곽 라인 그리기
const rLineM = Big(wallBaseLine.y1).minus(roofLine.y1).abs().toNumber();
newPStart.x = Big(wallBaseLine.x1).minus(rLineM).abs().toNumber();
const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x })
if(inLine){
if(inLine.y2 > inLine.y1 ) {
getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y1, x: inLine.x1 }, 'purple')
}else{
getAddLine({ y: inLine.y2, x: inLine.x2 }, { y: newPStart.y, x: newPStart.x } , 'purple')
}
}
} }
} }
@ -1401,7 +1485,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
console.log("startLines:::::::", inLine); console.log("startLines:::::::", inLine);
const eLineX = Big(bStartX).minus(wallLine.x2).abs().toNumber() const eLineX = Big(bStartX).minus(wallLine.x2).abs().toNumber()
newPEnd.x = aStartX newPEnd.x = aStartX
newPStart.x = Big(roofLine.x1).plus(eLineX).toNumber() newPStart.x = roofLine.x1;//Big(roofLine.x1).plus(eLineX).toNumber()
let idx = (roofLines.length < index + 1)?0:index let idx = (roofLines.length < index + 1)?0:index
const newLine = roofLines[idx + 1]; const newLine = roofLines[idx + 1];
@ -1427,29 +1511,34 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
getAddLine({ y: inLine.y1, x: inLine.x1 },{ y: newPEnd.y, x: newPEnd.x }, 'purple') getAddLine({ y: inLine.y1, x: inLine.x1 },{ y: newPEnd.y, x: newPEnd.x }, 'purple')
} }
}else{ }else{
newPEnd.x = wallLine.x2; //newPEnd.x = wallLine.x2;
//외곽 라인 그리기
const rLineM = Big(wallBaseLine.y2).minus(roofLine.y2).abs().toNumber();
newPEnd.x = Big(wallBaseLine.x2).plus(rLineM).abs().toNumber();
const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x })
if(inLine){
if(inLine.y1 > inLine.y2 ) {
getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple')
}else{
getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x } , 'purple')
}
}
} }
} }
} }
} }
// switch (condition) {
// case 'top_in':
// //console.log("findInteriorPoint result:::::::", isStartEnd);
// break;
// case 'top_out':
// //console.log("findInteriorPoint result:::::::", isStartEnd);
// break;
// case 'bottom_in':
// break;
// case 'bottom_out':
// break;
// }
} }
} }
getAddLine(newPStart, newPEnd, 'red') getAddLine(newPStart, newPEnd, 'red')
//canvas.remove(roofLine)
}else{
getAddLine(roofLine.startPoint, roofLine.endPoint, )
} }
canvas.renderAll() canvas.renderAll()
}); });
} }
@ -1475,17 +1564,34 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
*/ */
function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) {
let roof = canvas?.getObjects().find((object) => object.id === roofId) let roof = canvas?.getObjects().find((object) => object.id === roofId)
// [1] 벽 객체를 가져옵니다.
let wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId);
const polygonPoints = edgeResult.Polygon.map(p => ({ x: p.X, y: p.Y })); const polygonPoints = edgeResult.Polygon.map(p => ({ x: p.X, y: p.Y }));
//처마선인지 확인하고 pitch 대입 각 처마선마다 pitch가 다를수 있음 //처마선인지 확인하고 pitch 대입 각 처마선마다 pitch가 다를수 있음
const { Begin, End } = edgeResult.Edge; const { Begin, End } = edgeResult.Edge;
let outerLine = roof.lines.find(line => // [2] 현재 처리 중인 엣지가 roof.lines의 몇 번째 인덱스인지 찾습니다.
const roofLineIndex = roof.lines.findIndex(line =>
line.attributes.type === 'eaves' && isSameLine(Begin.X, Begin.Y, End.X, End.Y, line) line.attributes.type === 'eaves' && isSameLine(Begin.X, Begin.Y, End.X, End.Y, line)
); );
let outerLine = null;
let targetWallId = null;
// [3] 인덱스를 통해 매칭되는 벽 라인의 불변 ID(wallId)를 가져옵니다.
if (roofLineIndex !== -1) {
outerLine = roof.lines[roofLineIndex];
if (wall && wall.lines && wall.lines[roofLineIndex]) {
targetWallId = wall.lines[roofLineIndex].attributes.wallId;
}
targetWallId = outerLine.attributes.wallId;
}
if(!outerLine) { if(!outerLine) {
outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points); outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points);
console.log('Has matching line:', outerLine); console.log('Has matching line:', outerLine);
//if(outerLine === null) return
} }
let pitch = outerLine?.attributes?.pitch??0 let pitch = outerLine?.attributes?.pitch??0
@ -1519,7 +1625,7 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) {
const clippedLine = clipLineToRoofBoundary(p1, p2, roof.lines, roof.moveSelectLine); const clippedLine = clipLineToRoofBoundary(p1, p2, roof.lines, roof.moveSelectLine);
//console.log('clipped line', clippedLine.p1, clippedLine.p2); //console.log('clipped line', clippedLine.p1, clippedLine.p2);
const isOuterLine = isOuterEdge(clippedLine.p1, clippedLine.p2, [edgeResult.Edge]) const isOuterLine = isOuterEdge(clippedLine.p1, clippedLine.p2, [edgeResult.Edge])
addRawLine(roof.id, skeletonLines, clippedLine.p1, clippedLine.p2, 'ridge', '#1083E3', 4, pitch, isOuterLine); addRawLine(roof.id, skeletonLines, clippedLine.p1, clippedLine.p2, 'ridge', '#1083E3', 4, pitch, isOuterLine, targetWallId);
// } // }
} }
} }
@ -1644,7 +1750,7 @@ function isOuterEdge(p1, p2, edges) {
* @param pitch * @param pitch
* @param isOuterLine * @param isOuterLine
*/ */
function addRawLine(id, skeletonLines, p1, p2, lineType, color, width, pitch, isOuterLine) { function addRawLine(id, skeletonLines, p1, p2, lineType, color, width, pitch, isOuterLine, wallLineId) {
// const edgeKey = [`${p1.x.toFixed(1)},${p1.y.toFixed(1)}`, `${p2.x.toFixed(1)},${p2.y.toFixed(1)}`].sort().join('|'); // const edgeKey = [`${p1.x.toFixed(1)},${p1.y.toFixed(1)}`, `${p2.x.toFixed(1)},${p2.y.toFixed(1)}`].sort().join('|');
// if (processedInnerEdges.has(edgeKey)) return; // if (processedInnerEdges.has(edgeKey)) return;
// processedInnerEdges.add(edgeKey); // processedInnerEdges.add(edgeKey);
@ -1681,6 +1787,7 @@ function addRawLine(id, skeletonLines, p1, p2, lineType, color, width, pitch, is
isRidge: normalizedType === LINE_TYPE.SUBLINE.RIDGE, isRidge: normalizedType === LINE_TYPE.SUBLINE.RIDGE,
isOuterEdge: isOuterLine, isOuterEdge: isOuterLine,
pitch: pitch, pitch: pitch,
wallLineId: wallLineId, // [5] attributes에 wallId 저장 (이 정보가 최종 roofLines에 들어갑니다)
...(eavesIndex !== undefined && { eavesIndex }) ...(eavesIndex !== undefined && { eavesIndex })
}, },
lineStyle: { color, width }, lineStyle: { color, width },
@ -3322,7 +3429,8 @@ function findInteriorPoint(line, polygonLines) {
/** /**
* baseLines의 순서를 wallLines의 순서와 일치시킵니다. * baseLines의 순서를 wallLines의 순서와 일치시킵니다.
* wallLine에 대해 가장 가깝고 평행한 baseLine을 찾아 정렬된 배열을 반환합니다. * 1순위: 공통 ID(id, matchingId, parentId ) 이용한 직접 매칭
* 2순위: 기하학적 유사성(기울기, 길이, 위치) 점수화하여 매칭
* *
* @param {Array} baseLines - 정렬할 원본 baseLine 배열 * @param {Array} baseLines - 정렬할 원본 baseLine 배열
* @param {Array} wallLines - 기준이 되는 wallLine 배열 * @param {Array} wallLines - 기준이 되는 wallLine 배열
@ -3333,67 +3441,88 @@ export const sortBaseLinesByWallLines = (baseLines, wallLines) => {
return baseLines; return baseLines;
} }
const sortedBaseLines = []; const sortedBaseLines = new Array(wallLines.length).fill(null);
const usedIndices = new Set(); // 이미 매칭된 baseLine 인덱스를 추적 const usedBaseIndices = new Set();
// [1단계] ID 매칭 (기존 로직 유지 - 혹시 ID가 있는 경우를 대비)
// ... (ID 매칭 코드는 생략하거나 유지) ...
// [2단계] 'originPoint' 또는 좌표 일치성을 이용한 강력한 기하학적 매칭
wallLines.forEach((wLine, wIndex) => {
if (sortedBaseLines[wIndex]) return;
// 비교할 기준 좌표 설정 (originPoint가 있으면 그것을, 없으면 현재 좌표 사용)
const wStart = wLine.attributes?.originPoint
? { x: wLine.attributes.originPoint.x1, y: wLine.attributes.originPoint.y1 }
: { x: wLine.x1, y: wLine.y1 };
const wEnd = wLine.attributes?.originPoint
? { x: wLine.attributes.originPoint.x2, y: wLine.attributes.originPoint.y2 }
: { x: wLine.x2, y: wLine.y2 };
// 수직/수평 여부 판단
const isVertical = Math.abs(wStart.x - wEnd.x) < 0.1;
const isHorizontal = Math.abs(wStart.y - wEnd.y) < 0.1;
wallLines.forEach((wallLine) => {
let bestMatchIndex = -1; let bestMatchIndex = -1;
let minDistance = Infinity; let minDiff = Infinity;
// wallLine의 중점 계산 baseLines.forEach((bLine, bIndex) => {
const wMidX = (wallLine.x1 + wallLine.x2) / 2; if (usedBaseIndices.has(bIndex)) return;
const wMidY = (wallLine.y1 + wallLine.y2) / 2;
// wallLine의 방향 벡터 (평행 확인용) let diff = Infinity;
const wDx = wallLine.x2 - wallLine.x1;
const wDy = wallLine.y2 - wallLine.y1;
const wLen = Math.hypot(wDx, wDy);
baseLines.forEach((baseLine, index) => { // 1. 수직선인 경우: X좌표가 일치해야 함 (예: 230.8 == 230.8)
// 이미 매칭된 라인은 건너뜀 (1:1 매칭) if (isVertical) {
if (usedIndices.has(index)) return; // bLine도 수직선인지 확인 (x1, x2 차이가 거의 없어야 함)
if (Math.abs(bLine.x1 - bLine.x2) < 1.0) {
// baseLine의 중점 계산 // X좌표 차이를 오차(diff)로 계산
const bMidX = (baseLine.x1 + baseLine.x2) / 2; diff = Math.abs(wStart.x - bLine.x1);
const bMidY = (baseLine.y1 + baseLine.y2) / 2;
// 두 라인의 중점 사이 거리 계산
const dist = Math.hypot(wMidX - bMidX, wMidY - bMidY);
// 평행 여부 확인 (내적 사용)
const bDx = baseLine.x2 - baseLine.x1;
const bDy = baseLine.y2 - baseLine.y1;
const bLen = Math.hypot(bDx, bDy);
if (wLen > 0 && bLen > 0) {
// 단위 벡터 내적값 (-1 ~ 1)
const dot = (wDx * bDx + wDy * bDy) / (wLen * bLen);
// 내적의 절대값이 1에 가까우면 평행 (약 10도 오차 허용)
if (Math.abs(Math.abs(dot) - 1) < 0.1) {
if (dist < minDistance) {
minDistance = dist;
bestMatchIndex = index;
}
} }
} }
// 2. 수평선인 경우: Y좌표가 일치해야 함
else if (isHorizontal) {
// bLine도 수평선인지 확인
if (Math.abs(bLine.y1 - bLine.y2) < 1.0) {
diff = Math.abs(wStart.y - bLine.y1);
}
}
// 3. 대각선인 경우: 기울기와 절편 비교 (복잡하므로 거리로 대체)
else {
// 중점 간 거리 + 기울기 차이
// (이전 답변의 로직 사용 가능)
}
// 오차가 매우 작으면(예: 1px 미만) 같은 라인으로 간주
if (diff < 1.0 && diff < minDiff) {
minDiff = diff;
bestMatchIndex = bIndex;
}
}); });
if (bestMatchIndex !== -1) { if (bestMatchIndex !== -1) {
sortedBaseLines.push(baseLines[bestMatchIndex]); sortedBaseLines[wIndex] = baseLines[bestMatchIndex];
usedIndices.add(bestMatchIndex); usedBaseIndices.add(bestMatchIndex);
} else {
// 매칭되는 라인을 찾지 못한 경우, 아직 사용되지 않은 첫 번째 라인을 할당 (Fallback)
const unusedIndex = baseLines.findIndex((_, idx) => !usedIndices.has(idx));
if (unusedIndex !== -1) {
sortedBaseLines.push(baseLines[unusedIndex]);
usedIndices.add(unusedIndex);
} else {
// 더 이상 남은 라인이 없으면 null 또는 기존 라인 중 하나(에러 방지)
sortedBaseLines.push(baseLines[0]);
}
} }
}); });
// [3단계] 남은 라인 처리 (Fallback)
// 매칭되지 않은 wallLine들에 대해 남은 baseLines를 순서대로 배정하거나
// 거리 기반 근사 매칭을 수행
// ... (기존 fallback 로직) ...
// 빈 구멍 채우기 (null 방지)
for(let i=0; i<sortedBaseLines.length; i++) {
if(!sortedBaseLines[i]) {
const unused = baseLines.findIndex((_, idx) => !usedBaseIndices.has(idx));
if(unused !== -1) {
sortedBaseLines[i] = baseLines[unused];
usedBaseIndices.add(unused);
} else {
sortedBaseLines[i] = baseLines[0]; // 최후의 수단
}
}
}
return sortedBaseLines; return sortedBaseLines;
}; };