Merge pull request 'sortRoofLines 개선' (#460) from dev_cha into dev
Reviewed-on: #460
This commit is contained in:
commit
f898b4434b
@ -5,6 +5,7 @@ import { QLine } from '@/components/fabric/QLine'
|
||||
import { getDegreeByChon } from '@/util/canvas-util'
|
||||
import Big from 'big.js'
|
||||
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
|
||||
*/
|
||||
export const skeletonBuilder = (roofId, canvas, textMode) => {
|
||||
|
||||
//처마
|
||||
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 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 baseLines = wall.baseLines.filter((line) => line.attributes.planeSize > 0)
|
||||
|
||||
const baseLines = canvas.getObjects().filter((object) => object.name === 'baseLine' && object.parentId === roofId) || [];
|
||||
const baseLinePoints = baseLines.map((line) => ({x:line.left, y:line.top}));
|
||||
const baseLines = canvas.getObjects().filter((object) => object.name === 'baseLine' && object.parentId === roofId) || []
|
||||
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 hipLines = canvas.getObjects().filter((object) => object.name === 'hip' && object.parentId === roofId) || [];
|
||||
const ridgeLines = canvas.getObjects().filter((object) => object.name === 'ridge' && 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 skeletonLines = [];
|
||||
// 1. 지붕 폴리곤 좌표 전처리
|
||||
const coordinates = preprocessPolygonCoordinates(roof.points);
|
||||
const coordinates = preprocessPolygonCoordinates(roof.points)
|
||||
if (coordinates.length < 3) {
|
||||
console.warn("Polygon has less than 3 unique points. Cannot generate skeleton.");
|
||||
return;
|
||||
console.warn('Polygon has less than 3 unique points. Cannot generate skeleton.')
|
||||
return
|
||||
}
|
||||
|
||||
const moveFlowLine = roof.moveFlowLine || 0; // Provide a default value
|
||||
const moveUpDown = roof.moveUpDown || 0; // Provide a default value
|
||||
|
||||
|
||||
|
||||
let points = roof.points;
|
||||
const moveFlowLine = roof.moveFlowLine || 0 // Provide a default value
|
||||
const moveUpDown = roof.moveUpDown || 0 // Provide a default value
|
||||
|
||||
let points = roof.points
|
||||
|
||||
|
||||
//마루이동
|
||||
if (moveFlowLine !== 0 || moveUpDown !== 0) {
|
||||
|
||||
points = movingLineFromSkeleton(roofId, canvas)
|
||||
}
|
||||
|
||||
|
||||
console.log('points:', points);
|
||||
console.log('points:', points)
|
||||
const geoJSONPolygon = toGeoJSON(points)
|
||||
|
||||
try {
|
||||
@ -371,7 +386,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
||||
const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]])
|
||||
|
||||
// 스켈레톤 데이터를 기반으로 내부선 생성
|
||||
roof.innerLines = roof.innerLines || [];
|
||||
roof.innerLines = roof.innerLines || []
|
||||
roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode)
|
||||
|
||||
// 캔버스에 스켈레톤 상태 저장
|
||||
@ -380,12 +395,12 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
||||
canvas.skeletonLines = []
|
||||
}
|
||||
canvas.skeletonStates[roofId] = true
|
||||
canvas.skeletonLines = [];
|
||||
canvas.skeletonLines = []
|
||||
canvas.skeletonLines.push(...roof.innerLines)
|
||||
roof.skeletonLines = canvas.skeletonLines;
|
||||
roof.skeletonLines = canvas.skeletonLines
|
||||
|
||||
const cleanSkeleton = {
|
||||
Edges: skeleton.Edges.map(edge => ({
|
||||
Edges: skeleton.Edges.map((edge) => ({
|
||||
X1: edge.Edge.Begin.X,
|
||||
Y1: edge.Edge.Begin.Y,
|
||||
X2: edge.Edge.End.X,
|
||||
@ -396,15 +411,14 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
||||
})),
|
||||
roofId: roofId,
|
||||
// Add other necessary top-level properties
|
||||
};
|
||||
canvas.skeleton = [];
|
||||
}
|
||||
canvas.skeleton = []
|
||||
canvas.skeleton = cleanSkeleton
|
||||
canvas.skeleton.lastPoints = points
|
||||
canvas.set("skeleton", cleanSkeleton);
|
||||
canvas.set('skeleton', cleanSkeleton)
|
||||
canvas.renderAll()
|
||||
|
||||
|
||||
console.log('skeleton rendered.', canvas);
|
||||
console.log('skeleton rendered.', canvas)
|
||||
} catch (e) {
|
||||
console.error('스켈레톤 생성 중 오류 발생:', e)
|
||||
if (canvas.skeletonStates) {
|
||||
@ -578,7 +592,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
|
||||
);
|
||||
|
||||
//그림을 그릴때 idx 가 필요함 roof는 왼쪽부터 시작됨 - 그림그리는 순서가 필요함
|
||||
let roofIdx = 0;
|
||||
|
||||
|
||||
// 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], {
|
||||
parentId: roof.id,
|
||||
fontSize: roof.fontSize,
|
||||
stroke: (sktLine.attributes.isOuterEdge)?'orange':lineStyle.color,
|
||||
strokeWidth: lineStyle.width,
|
||||
name: (sktLine.attributes.isOuterEdge)?'eaves': attributes.type,
|
||||
attributes: attributes,
|
||||
attributes: {
|
||||
...attributes,
|
||||
|
||||
},
|
||||
direction: direction,
|
||||
isBaseLine: sktLine.attributes.isOuterEdge,
|
||||
lineName: (sktLine.attributes.isOuterEdge)?'roofLine': attributes.type,
|
||||
@ -755,6 +773,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
|
||||
const sortedRoofLines = sortCurrentRoofLines(roofLines);
|
||||
const sortedWallBaseLines = sortCurrentRoofLines(wall.baseLines);
|
||||
const sortedBaseLines = sortBaseLinesByWallLines(wall.baseLines, wallLines);
|
||||
const sortRoofLines = sortBaseLinesByWallLines(roofLines, wallLines);
|
||||
|
||||
|
||||
//wall.lines 는 기본 벽 라인
|
||||
@ -770,11 +789,20 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
|
||||
// const moveLine = 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 moveLine = sortedBaseLines[index]
|
||||
const wallBaseLine = sortedBaseLines[index]
|
||||
console.log("wallBaseLine", wallBaseLine);
|
||||
|
||||
//roofline 외곽선 설정
|
||||
|
||||
@ -1448,7 +1476,9 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
getAddLine(newPStart, newPEnd, 'red')
|
||||
//canvas.remove(roofLine)
|
||||
}
|
||||
canvas.renderAll()
|
||||
});
|
||||
@ -1475,14 +1505,30 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
|
||||
*/
|
||||
function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) {
|
||||
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 }));
|
||||
|
||||
//처마선인지 확인하고 pitch 대입 각 처마선마다 pitch가 다를수 있음
|
||||
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)
|
||||
|
||||
);
|
||||
|
||||
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) {
|
||||
outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points);
|
||||
console.log('Has matching line:', outerLine);
|
||||
@ -1519,7 +1565,7 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) {
|
||||
const clippedLine = clipLineToRoofBoundary(p1, p2, roof.lines, roof.moveSelectLine);
|
||||
//console.log('clipped line', clippedLine.p1, clippedLine.p2);
|
||||
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 +1690,7 @@ function isOuterEdge(p1, p2, edges) {
|
||||
* @param pitch
|
||||
* @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('|');
|
||||
// if (processedInnerEdges.has(edgeKey)) return;
|
||||
// processedInnerEdges.add(edgeKey);
|
||||
@ -1681,6 +1727,7 @@ function addRawLine(id, skeletonLines, p1, p2, lineType, color, width, pitch, is
|
||||
isRidge: normalizedType === LINE_TYPE.SUBLINE.RIDGE,
|
||||
isOuterEdge: isOuterLine,
|
||||
pitch: pitch,
|
||||
wallLineId: wallLineId, // [5] attributes에 wallId 저장 (이 정보가 최종 roofLines에 들어갑니다)
|
||||
...(eavesIndex !== undefined && { eavesIndex })
|
||||
},
|
||||
lineStyle: { color, width },
|
||||
@ -3322,7 +3369,8 @@ function findInteriorPoint(line, polygonLines) {
|
||||
|
||||
/**
|
||||
* baseLines의 순서를 wallLines의 순서와 일치시킵니다.
|
||||
* 각 wallLine에 대해 가장 가깝고 평행한 baseLine을 찾아 정렬된 배열을 반환합니다.
|
||||
* 1순위: 공통 ID(id, matchingId, parentId 등)를 이용한 직접 매칭
|
||||
* 2순위: 기하학적 유사성(기울기, 길이, 위치)을 점수화하여 매칭
|
||||
*
|
||||
* @param {Array} baseLines - 정렬할 원본 baseLine 배열
|
||||
* @param {Array} wallLines - 기준이 되는 wallLine 배열
|
||||
@ -3333,67 +3381,88 @@ export const sortBaseLinesByWallLines = (baseLines, wallLines) => {
|
||||
return baseLines;
|
||||
}
|
||||
|
||||
const sortedBaseLines = [];
|
||||
const usedIndices = new Set(); // 이미 매칭된 baseLine 인덱스를 추적
|
||||
const sortedBaseLines = new Array(wallLines.length).fill(null);
|
||||
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 minDistance = Infinity;
|
||||
let minDiff = Infinity;
|
||||
|
||||
// wallLine의 중점 계산
|
||||
const wMidX = (wallLine.x1 + wallLine.x2) / 2;
|
||||
const wMidY = (wallLine.y1 + wallLine.y2) / 2;
|
||||
baseLines.forEach((bLine, bIndex) => {
|
||||
if (usedBaseIndices.has(bIndex)) return;
|
||||
|
||||
// wallLine의 방향 벡터 (평행 확인용)
|
||||
const wDx = wallLine.x2 - wallLine.x1;
|
||||
const wDy = wallLine.y2 - wallLine.y1;
|
||||
const wLen = Math.hypot(wDx, wDy);
|
||||
let diff = Infinity;
|
||||
|
||||
baseLines.forEach((baseLine, index) => {
|
||||
// 이미 매칭된 라인은 건너뜀 (1:1 매칭)
|
||||
if (usedIndices.has(index)) return;
|
||||
|
||||
// baseLine의 중점 계산
|
||||
const bMidX = (baseLine.x1 + baseLine.x2) / 2;
|
||||
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;
|
||||
// 1. 수직선인 경우: X좌표가 일치해야 함 (예: 230.8 == 230.8)
|
||||
if (isVertical) {
|
||||
// bLine도 수직선인지 확인 (x1, x2 차이가 거의 없어야 함)
|
||||
if (Math.abs(bLine.x1 - bLine.x2) < 1.0) {
|
||||
// X좌표 차이를 오차(diff)로 계산
|
||||
diff = Math.abs(wStart.x - bLine.x1);
|
||||
}
|
||||
}
|
||||
// 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) {
|
||||
sortedBaseLines.push(baseLines[bestMatchIndex]);
|
||||
usedIndices.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]);
|
||||
}
|
||||
sortedBaseLines[wIndex] = baseLines[bestMatchIndex];
|
||||
usedBaseIndices.add(bestMatchIndex);
|
||||
}
|
||||
});
|
||||
|
||||
// [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;
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user