roof.moveFlowLine 수정

This commit is contained in:
ysCha 2025-10-24 18:36:38 +09:00
parent 0ad18e4f15
commit affef782f3
2 changed files with 245 additions and 113 deletions

View File

@ -102,7 +102,7 @@ export function useMovementSetting(id) {
/** outerLines 속성처리*/
const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine')
outerLines.forEach((line) => line.set({ visible: true }))
outerLines.forEach((line) => line.set({ visible: false }))
canvas.renderAll()
}, [type])
@ -321,6 +321,12 @@ export function useMovementSetting(id) {
FOLLOW_LINE_REF.current = null
canvas.renderAll()
}
if (UP_DOWN_REF.current !== null) {
canvas.remove(UP_DOWN_REF.current)
UP_DOWN_REF.current = null
canvas.renderAll()
}
const target = selectedObject.current !== null ? selectedObject.current : CONFIRM_LINE_REF.current?.target
if (!target) return
@ -329,10 +335,14 @@ export function useMovementSetting(id) {
const roof = canvas.getObjects().find((obj) => obj.id === roofId)
// 현이동, 동이동 추가
let pointValue = FLOW_LINE_REF.POINTER_INPUT_REF.current.value;
let filledValue = FLOW_LINE_REF.FILLED_INPUT_REF.current.value;
const moveFlowLine = typeRef.current === TYPE.FLOW_LINE ? (pointValue===''?filledValue:pointValue) : 0
const moveUpDown = typeRef.current === TYPE.UP_DOWN ? UP_DOWN_REF.POINTER_INPUT_REF.current.value : 0
let flPointValue = FLOW_LINE_REF.POINTER_INPUT_REF.current?.value??0;
let flFilledValue = FLOW_LINE_REF.FILLED_INPUT_REF.current?.value??0;
flPointValue = (flFilledValue > 0 || flFilledValue < 0)? flFilledValue : flPointValue;
const moveFlowLine = typeRef.current === TYPE.FLOW_LINE ? flPointValue : 0
let udPointValue = UP_DOWN_REF.POINTER_INPUT_REF.current?.value??0;
let udFilledValue = UP_DOWN_REF.FILLED_INPUT_REF.current?.value??0;
udPointValue = udFilledValue > 0 ? udFilledValue : udPointValue;
const moveUpDown = typeRef.current === TYPE.UP_DOWN ? udPointValue: 0
roof.moveFlowLine = parseInt(moveFlowLine, 10) || 0;
roof.moveUpDown = parseInt(moveUpDown, 10) || 0;
roof.moveDirect = "";

View File

@ -5,6 +5,7 @@ import { QLine } from '@/components/fabric/QLine'
import { getDegreeByChon } from '@/util/canvas-util'
import Big from 'big.js'
import { line } from 'framer-motion/m'
import { QPolygon } from '@/components/fabric/QPolygon'
/**
* 지붕 폴리곤의 스켈레톤(중심선) 생성하고 캔버스에 그립니다.
@ -25,61 +26,62 @@ export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => {
const movingRidgeFromSkeleton = (roofId, canvas) => {
let roof = canvas?.getObjects().find((object) => object.id === roofId)
let moveDirection = roof.moveDirect;
let moveFlowLine = roof.moveFlowLine??0;
const selectLine = roof.moveSelectLine;
const startPoint = selectLine.startPoint
const endPoint = selectLine.endPoint
const orgPoints = roof.points; // orgPoint를 orgPoints로 변경
const oldPoints = canvas?.skeleton.lastPoints ?? orgPoints // 여기도 변경
const orgRoofPoints = roof.points; // orgPoint를 orgPoints로 변경
const oldPoints = canvas?.skeleton.lastPoints ?? orgRoofPoints // 여기도 변경
const oppositeLine = findOppositeLine(canvas.skeleton.Edges, startPoint, endPoint, oldPoints);
const skeletonPolygon = canvas.getObjects().filter((object) => object.skeletonType === 'polygon' && object.parentId === roofId)
const skeletonLines = canvas.getObjects().filter((object) => object.skeletonType === 'line' && object.parentId === roofId)
if (oppositeLine) {
console.log('Opposite line found:', oppositeLine);
} else {
console.log('No opposite line found');
}
let baseLines = canvas.getObjects().filter((object) => object.name === 'baseLine' && object.parentId === roofId) || [];
console.log('baseLines::::', baseLines);
let baseLinePoints = baseLines.map((line) => ({x:line.x1, y:line.y1}));
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)
let baseLinePoints = [];
const pointSet = new Set();
/*
walls.forEach((wall) => {
if (wall.baseLines.length === 0) {
wall.baseLines = canvas.getObjects().filter((obj) => obj.name === 'baseLine' && obj.attributes.wallId === wall.id)
}
// Extract points from each baseLine
wall.baseLines.forEach(line => {
console.log("useSk:::", line.x1, line.y1, line.x2, line.y2);
// 시작점과 끝점을 배열에 추가
const points = [
{ x: line.x1, y: line.y1 },
{ x: line.x2, y: line.y2 }
];
/*
walls.forEach((wall) => {
if (wall.baseLines.length === 0) {
wall.baseLines = canvas.getObjects().filter((obj) => obj.name === 'baseLine' && obj.attributes.wallId === wall.id)
}
points.forEach(point => {
const key = `${point.x},${point.y}`;
if (!pointSet.has(key)) {
pointSet.add(key);
baseLinePoints.push(point);
}
// Extract points from each baseLine
wall.baseLines.forEach(line => {
console.log("useSk:::", line.x1, line.y1, line.x2, line.y2);
// 시작점과 끝점을 배열에 추가
const points = [
{ x: line.x1, y: line.y1 },
{ x: line.x2, y: line.y2 }
];
points.forEach(point => {
const key = `${point.x},${point.y}`;
if (!pointSet.has(key)) {
pointSet.add(key);
baseLinePoints.push(point);
}
});
});
});
})
return [...baseLinePoints];
*/
})
return [...baseLinePoints];
*/
return oldPoints.map((point, index) => {
console.log('oldPoint:', point);
const originalPoint = orgPoints[index]; // orgPoint를 originalPoint로 변경
console.log('originalPoint:', originalPoint);
const newPoint = { ...point };
const absMove = Big(moveFlowLine).times(2).div(10);
//console.log('absMove:', absMove);
@ -170,9 +172,9 @@ const movingRidgeFromSkeleton = (roofId, canvas) => {
for (const line of oppositeLine) {
if (line.position === 'top') {
if (isSamePoint(newPoint, line.start)) {
newPoint.y = Big(line.start.y).plus(absMove).toNumber();
newPoint.y = Big(line.start.y).minus(absMove).toNumber();
} else if (isSamePoint(newPoint, line.end)) {
newPoint.y = Big(line.end.y).plus(absMove).toNumber();
newPoint.y = Big(line.end.y).minus(absMove).toNumber();
}
break;
@ -196,6 +198,8 @@ const movingRidgeFromSkeleton = (roofId, canvas) => {
if (line.position === 'bottom') {
console.log('oldPoint:', point);
if (isSamePoint(newPoint, line.start)) {
newPoint.y = Big(line.start.y).minus(absMove).toNumber();
} else if (isSamePoint(newPoint, line.end)) {
@ -221,7 +225,9 @@ const movingRidgeFromSkeleton = (roofId, canvas) => {
break;
}
console.log('newPoint:', newPoint);
//baseline 변경
return newPoint;
})
@ -246,7 +252,16 @@ 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 = 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 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 skeletonLines = [];
// 1. 지붕 폴리곤 좌표 전처리
@ -271,6 +286,8 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
points = movingRidgeFromSkeleton(roofId, canvas)
}
//처마
if(moveUpDown !== 0) {
@ -314,11 +331,8 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
canvas.skeleton = cleanSkeleton
canvas.skeleton.lastPoints = points
canvas.set("skeleton", cleanSkeleton);
canvas.renderAll()
console.log('skeleton rendered.', canvas);
} catch (e) {
console.error('스켈레톤 생성 중 오류 발생:', e)
if (canvas.skeletonStates) {
@ -347,23 +361,8 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
// 1. 모든 Edge를 순회하며 기본 스켈레톤 선(용마루)을 수집합니다.
skeleton.Edges.forEach((edgeResult, index) => {
// const { Begin, End } = edgeResult.Edge;
// let outerLine = roof.lines.find(line =>
// line.attributes.type === 'eaves' && isSameLine(Begin.X, Begin.Y, End.X, End.Y, line)
// );
// if(!outerLine){
//
// for (const line of canvas.skeletonLines) {
// if (line.lineName === 'hip' && line.attributes.hipIndex === index)
// {
// outerLine = line;
// break; // Found the matching line, exit the loop
// }
// }
//
// }
// const pitch = outerLine.attributes?.pitch??0
// console.log("pitch", pitch)
processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines);
});
@ -431,6 +430,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
const innerLines = [];
const existingLines = new Set(); // 이미 추가된 라인을 추적하기 위한 Set
skeletonLines.forEach(line => {
const { p1, p2, attributes, lineStyle } = line;
@ -445,6 +445,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
return; // 이미 있는 라인이면 스킵
}
const direction = getLineDirection(
{ x: line.p1.x, y: line.p1.y },
{ x: line.p2.x, y: line.p2.y }
);
const innerLine = new QLine([p1.x, p1.y, p2.x, p2.y], {
parentId: roof.id,
fontSize: roof.fontSize,
@ -452,10 +457,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
strokeWidth: lineStyle.width,
name: (line.attributes.isOuterEdge)?'eaves': attributes.type,
attributes: attributes,
direction: direction,
isBaseLine: line.attributes.isOuterEdge,
lineName: (line.attributes.isOuterEdge)?'outerLine': attributes.type,
selectable:(!line.attributes.isOuterEdge),
roofId: roofId
roofId: roofId,
});
//skeleton 라인에서 처마선은 삭제
@ -504,6 +510,7 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) {
const { Begin, End } = edgeResult.Edge;
let outerLine = roof.lines.find(line =>
line.attributes.type === 'eaves' && isSameLine(Begin.X, Begin.Y, End.X, End.Y, line)
);
if(!outerLine) {
outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points);
@ -512,6 +519,25 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) {
let pitch = outerLine?.attributes?.pitch??0
const convertedPolygon = edgeResult.Polygon?.map(point => ({
x: typeof point.X === 'number' ? parseFloat(point.X) : 0,
y: typeof point.Y === 'number' ? parseFloat(point.Y) : 0
})).filter(point => point.x !== 0 || point.y !== 0) || [];
if (convertedPolygon.length > 0) {
const skeletonPolygon = new QPolygon(convertedPolygon, {
type: POLYGON_TYPE.ROOF,
fill: false,
stroke: 'blue',
strokeWidth: 8,
skeletonType: 'polygon',
polygonName: '',
parentId: roof.id,
});
//canvas?.add(skeletonPolygon)
//canvas.renderAll()
}
let eavesLines = []
for (let i = 0; i < polygonPoints.length; i++) {
const p1 = polygonPoints[i];
@ -523,8 +549,8 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) {
// 지붕 경계선과 교차 확인 및 클리핑
const clippedLine = clipLineToRoofBoundary(p1, p2, roof.lines);
//console.log('clipped line', clippedLine.p1, clippedLine.p2);
const isOuterLine = isOuterEdge(p1, p2, [edgeResult.Edge])
addRawLine(roof.id, skeletonLines, p1, p2, 'ridge', '#FF0000', 3, pitch, isOuterLine);
const isOuterLine = isOuterEdge(clippedLine.p1, clippedLine.p2, [edgeResult.Edge])
addRawLine(roof.id, skeletonLines, clippedLine.p1, clippedLine.p2, 'ridge', 'red', 5, pitch, isOuterLine);
// }
}
}
@ -1359,33 +1385,29 @@ function findOppositeLine(edges, startPoint, endPoint, points) {
}
function getLinePosition(line, referenceLine) {
// 대상선의 중점
const lineMidX = (line.start.x + line.end.x) / 2;
const lineMidY = (line.start.y + line.end.y) / 2;
// 참조선의 중점
const refMidX = (referenceLine.start.x + referenceLine.end.x) / 2;
const refMidY = (referenceLine.start.y + referenceLine.end.y) / 2;
// 참조선에 대한 벡터를 계산하여 법선 벡터 구하기
const refVecX = referenceLine.end.x - referenceLine.start.x;
const refVecY = referenceLine.end.y - referenceLine.start.y;
// 단순히 좌표 차이로 판단
const deltaX = lineMidX - refMidX;
const deltaY = lineMidY - refMidY;
// 법선 벡터 (참조선에 수직) - 방향을 수정
const normalX = refVecY; // -refVecY에서 refVecY로 변경
const normalY = -refVecX; // refVecX에서 -refVecX로 변경
// 참조선의 기울기
const refDeltaX = referenceLine.end.x - referenceLine.start.x;
const refDeltaY = referenceLine.end.y - referenceLine.start.y;
// 중점에서 중점으로의 벡터
const midVecX = lineMidX - refMidX;
const midVecY = lineMidY - refMidY;
// 내적을 이용해 위치 판단
const dotProduct = midVecX * normalX + midVecY * normalY;
// 참조선의 방향에 따라 위치 결정
if (Math.abs(refVecX) > Math.abs(refVecY)) {
// 수평에 가까운 선분인 경우
return dotProduct > 0 ? 'top' : 'bottom';
// 참조선이 더 수평인지 수직인지 판단
if (Math.abs(refDeltaX) > Math.abs(refDeltaY)) {
// 수평선에 가까운 경우 - Y 좌표로 판단
return deltaY > 0 ? 'bottom' : 'top';
} else {
// 수직에 가까운 선분인 경우
return dotProduct > 0 ? 'right' : 'left';
// 수직선에 가까운 경우 - X 좌표로 판단
return deltaX > 0 ? 'right' : 'left';
}
}
@ -1445,54 +1467,142 @@ function findPolygonsContainingLine(edges, p1, p2) {
}
/**
* roof.lines교차하는 선분(p1, p2) 찾아 교차점에서 자릅니다.
* roof.lines만들어진 다각형 내부에만 선분이 존재하도록 클리핑합니다.
* @param {Object} p1 - 선분의 시작점 {x, y}
* @param {Object} p2 - 선분의 끝점 {x, y}
* @param {Array} roofLines - 지붕 경계선 배열 (QLine 객체의 배열)
* @returns {Object} {p1: {x, y}, p2: {x, y}} - 교차점에서 자른 선분 또는 원래 선분
* @returns {Object} {p1: {x, y}, p2: {x, y}} - 다각형 내부로 클리핑된 선분
*/
function clipLineToRoofBoundary(p1, p2, roofLines) {
if (!roofLines || !roofLines.length) return { p1, p2 };
if (!roofLines || !roofLines.length) {
return { p1: { ...p1 }, p2: { ...p2 } };
}
let closestIntersection = null;
let minDistSq = Infinity;
const originalP1 = { ...p1 };
const originalP2 = { ...p2 };
// 기본값으로 원본 좌표 설정
let clippedP1 = { x: p1.x, y: p1.y };
let clippedP2 = { x: p2.x, y: p2.y };
// p1이 다각형 내부에 있는지 확인
const p1Inside = isPointInsidePolygon(p1, roofLines);
// p2가 다각형 내부에 있는지 확인
const p2Inside = isPointInsidePolygon(p2, roofLines);
console.log('p1Inside:', p1Inside, 'p2Inside:', p2Inside);
// 두 점 모두 내부에 있으면 그대로 반환
if (p1Inside && p2Inside) {
return { p1: clippedP1, p2: clippedP2 };
}
// 선분과 다각형 경계선의 교차점들을 찾음
const intersections = [];
// 모든 지붕 경계선과의 교차점을 찾음
for (const line of roofLines) {
const lineP1 = { x: line.x1, y: line.y1 };
const lineP2 = { x: line.x2, y: line.y2 };
const intersection = getLineIntersection(
p1, p2,
lineP1, lineP2
);
const intersection = getLineIntersection(p1, p2, lineP1, lineP2);
if (intersection) {
// 교차점과 p1 사이의 거리 계산
const dx = intersection.x - p1.x;
const dy = intersection.y - p1.y;
const distSq = dx * dx + dy * dy;
// p1에 가장 가까운 교차점 찾기
if (distSq < minDistSq) {
minDistSq = distSq;
closestIntersection = intersection;
// 교차점이 선분 위에 있는지 확인
const t = getParameterT(p1, p2, intersection);
if (t >= 0 && t <= 1) {
intersections.push({
point: intersection,
t: t
});
}
}
}
// 교차점이 있으면 p2를 가장 가까운 교차점으로 업데이트
if (closestIntersection) {
return {
p1: originalP1,
p2: closestIntersection
};
console.log('Found intersections:', intersections.length);
// 교차점들을 t 값으로 정렬
intersections.sort((a, b) => a.t - b.t);
if (!p1Inside && !p2Inside) {
// 두 점 모두 외부에 있는 경우
if (intersections.length >= 2) {
console.log('Both outside, using intersection points');
clippedP1.x = intersections[0].point.x;
clippedP1.y = intersections[0].point.y;
clippedP2.x = intersections[1].point.x;
clippedP2.y = intersections[1].point.y;
} else {
console.log('Both outside, no valid intersections - returning original');
// 교차점이 충분하지 않으면 원본 반환
return { p1: clippedP1, p2: clippedP2 };
}
} else if (!p1Inside && p2Inside) {
// p1이 외부, p2가 내부
if (intersections.length > 0) {
console.log('p1 outside, p2 inside - moving p1 to intersection');
clippedP1.x = intersections[0].point.x;
clippedP1.y = intersections[0].point.y;
// p2는 이미 내부에 있으므로 원본 유지
clippedP2.x = p2.x;
clippedP2.y = p2.y;
}
} else if (p1Inside && !p2Inside) {
// p1이 내부, p2가 외부
if (intersections.length > 0) {
console.log('p1 inside, p2 outside - moving p2 to intersection');
// p1은 이미 내부에 있으므로 원본 유지
clippedP1.x = p1.x;
clippedP1.y = p1.y;
clippedP2.x = intersections[0].point.x;
clippedP2.y = intersections[0].point.y;
}
}
// 교차점이 없으면 원래 선분 반환
return { p1: originalP1, p2: originalP2 };
return { p1: clippedP1, p2: clippedP2 };
}
/**
* 점이 다각형 내부에 있는지 확인합니다 (Ray Casting 알고리즘 사용).
* @param {Object} point - 확인할 {x, y}
* @param {Array} roofLines - 다각형을 구성하는 선분들
* @returns {boolean} 점이 다각형 내부에 있으면 true
*/
function isPointInsidePolygon(point, roofLines) {
let inside = false;
const x = point.x;
const y = point.y;
for (const line of roofLines) {
const x1 = line.x1;
const y1 = line.y1;
const x2 = line.x2;
const y2 = line.y2;
// Ray casting: 점에서 오른쪽으로 수평선을 그었을 때 다각형 경계와 교차하는 횟수 확인
if (((y1 > y) !== (y2 > y)) && (x < (x2 - x1) * (y - y1) / (y2 - y1) + x1)) {
inside = !inside;
}
}
return inside;
}
/**
* 선분 위의 점에 대한 매개변수 t를 계산합니다.
* p = p1 + t * (p2 - p1)에서 t 값을 구합니다.
* @param {Object} p1 - 선분의 시작점
* @param {Object} p2 - 선분의 끝점
* @param {Object} point - 선분 위의
* @returns {number} 매개변수 t (0이면 p1, 1이면 p2)
*/
function getParameterT(p1, p2, point) {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
// x 좌표가 더 큰 변화를 보이면 x로 계산, 아니면 y로 계산
if (Math.abs(dx) > Math.abs(dy)) {
return dx === 0 ? 0 : (point.x - p1.x) / dx;
} else {
return dy === 0 ? 0 : (point.y - p1.y) / dy;
}
}
export const convertBaseLinesToPoints = (baseLines) => {
const points = [];
@ -1513,3 +1623,15 @@ export const convertBaseLinesToPoints = (baseLines) => {
return points;
};
function getLineDirection(p1, p2) {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const angle = Math.atan2(dy, dx) * 180 / Math.PI;
// 각도 범위에 따라 방향 반환
if ((angle >= -45 && angle < 45)) return 'right';
if ((angle >= 45 && angle < 135)) return 'bottom';
if ((angle >= 135 || angle < -135)) return 'left';
return 'top'; // (-135 ~ -45)
}