diff --git a/src/components/floor-plan/modal/basic/step/Trestle.jsx b/src/components/floor-plan/modal/basic/step/Trestle.jsx
index c8b40ef3..77ade743 100644
--- a/src/components/floor-plan/modal/basic/step/Trestle.jsx
+++ b/src/components/floor-plan/modal/basic/step/Trestle.jsx
@@ -18,6 +18,11 @@ const Trestle = forwardRef((props, ref) => {
const currentAngleType = useRecoilValue(currentAngleTypeSelector)
const pitchText = useRecoilValue(pitchTextSelector)
const [selectedRoof, setSelectedRoof] = useState(null)
+ const [isAutoSelecting, setIsAutoSelecting] = useState(false) // 자동 선택 중인지 상태
+ const [autoSelectTimeout, setAutoSelectTimeout] = useState(null) // 타임아웃 참조
+ const autoSelectTimeoutRef = useRef(null)
+ // 공통 타임아웃 설정 (밀리초)
+ const AUTO_SELECT_TIMEOUT = 700 // API 호출 완료 대기 시간
const {
trestleState,
trestleDetail,
@@ -63,7 +68,7 @@ const Trestle = forwardRef((props, ref) => {
useEffect(() => {
- if (roofs && !selectedRoof) {
+ if (roofs && roofs.length > 0 && !selectedRoof) {
console.log("roofs:::::", roofs.length)
setLengthBase(roofs[0].length);
setSelectedRoof(roofs[0])
@@ -71,12 +76,12 @@ const Trestle = forwardRef((props, ref) => {
if (selectedRoof && selectedRoof.lenAuth === "C") {
onChangeLength(selectedRoof.length);
}
- if (selectedRoof && ["C", "R"].includes(selectedRoof.raftAuth)) {
+ if (selectedRoof && ["C", "R"].includes(selectedRoof.raftAuth) && roofs && roofs.length > 0) {
onChangeRaftBase(roofs[0]);
}
//모듈 설치 영역 복구
restoreModuleInstArea()
- }, [roofs])
+ }, [roofs, selectedRoof]) // selectedRoof 추가
useEffect(() => {
if (flag && moduleSelectionData) {
@@ -161,7 +166,7 @@ const Trestle = forwardRef((props, ref) => {
useEffect(() => {
if (constructionList.length > 0) {
- const existingConstruction = constructionList.find((construction) => construction.constTp === trestleState?.construction?.constTp)
+ const existingConstruction = constructionList.find((construction) => construction.constTp === trestleState.constTp)
if (existingConstruction) {
setSelectedConstruction(existingConstruction)
} else if (autoSelectStep === 'construction') {
@@ -252,7 +257,7 @@ const Trestle = forwardRef((props, ref) => {
// 다음 단계(가대메이커) 자동 선택 설정 - 지연 실행
setTimeout(() => {
setAutoSelectStep('trestle')
- }, 500) // API 호출 완료를 위한 더 긴 지연
+ }, AUTO_SELECT_TIMEOUT) // API 호출 완료를 위한 더 긴 지연
}
const onChangeHajebichi = (e) => {
@@ -282,7 +287,7 @@ const Trestle = forwardRef((props, ref) => {
// 다음 단계(가대메이커) 자동 선택 설정 - 지연 실행
setTimeout(() => {
setAutoSelectStep('trestle')
- }, 500)
+ }, AUTO_SELECT_TIMEOUT)
}
const onChangeTrestleMaker = (e) => {
@@ -305,7 +310,7 @@ const Trestle = forwardRef((props, ref) => {
// API 호출 완료 후 다음 단계(공법) 자동 선택 설정
setTimeout(() => {
setAutoSelectStep('constMthd')
- }, 300)
+ }, AUTO_SELECT_TIMEOUT)
}
const onChangeConstMthd = (e) => {
@@ -325,10 +330,21 @@ const Trestle = forwardRef((props, ref) => {
},
})
+ // 기존 타임아웃 취소
+ if (autoSelectTimeoutRef.current) {
+ clearTimeout(autoSelectTimeoutRef.current)
+ }
+
+ // 자동 선택 중 상태 활성화
+ setIsAutoSelecting(true)
+
// API 호출 완료 후 다음 단계(지붕밑바탕) 자동 선택 설정
- setTimeout(() => {
+ const timeoutId = setTimeout(() => {
setAutoSelectStep('roofBase')
- }, 300)
+ setIsAutoSelecting(false)
+ }, AUTO_SELECT_TIMEOUT)
+
+ autoSelectTimeoutRef.current = timeoutId
}
const onChangeRoofBase = (e) => {
@@ -356,7 +372,7 @@ const Trestle = forwardRef((props, ref) => {
// API 호출 완료 후 다음 단계(construction) 자동 선택 설정
setTimeout(() => {
setAutoSelectStep('construction')
- }, 300)
+ }, AUTO_SELECT_TIMEOUT)
}
const handleConstruction = (index) => {
@@ -451,7 +467,7 @@ const Trestle = forwardRef((props, ref) => {
...selectedRoofBase,
},
construction: {
- ...constructionList.find((data) => data.constTp === trestleState.constTp),
+ ...constructionList.find((data) => newAddedRoofs[index].construction.constTp === data.constTp),
cvrYn,
snowGdPossYn,
cvrChecked,
diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js
index 8ed97d37..70d53085 100644
--- a/src/util/skeleton-utils.js
+++ b/src/util/skeleton-utils.js
@@ -1,6 +1,6 @@
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
import { SkeletonBuilder } from '@/lib/skeletons'
-import { calcLineActualSize, calcLinePlaneSize, toGeoJSON } from '@/util/qpolygon-utils'
+import { calcLinePlaneSize, toGeoJSON } from '@/util/qpolygon-utils'
import { QLine } from '@/components/fabric/QLine'
/**
@@ -10,14 +10,14 @@ import { QLine } from '@/components/fabric/QLine'
* @param {string} textMode - 텍스트 표시 모드
* @param {Array
} existingSkeletonLines - 기존에 생성된 스켈레톤 라인
*/
-export const drawSkeletonRidgeRoof = (roofId, canvas, textMode, existingSkeletonLines = []) => {
+export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => {
const roof = canvas?.getObjects().find((object) => object.id === roofId)
if (!roof) {
console.error(`Roof with id "${roofId}" not found.`);
return;
}
-
- // 지붕 폴리곤 좌표 전처리
+ //const skeletonLines = [];
+ // 1. 지붕 폴리곤 좌표 전처리
const coordinates = preprocessPolygonCoordinates(roof.points);
if (coordinates.length < 3) {
console.warn("Polygon has less than 3 unique points. Cannot generate skeleton.");
@@ -30,8 +30,8 @@ export const drawSkeletonRidgeRoof = (roofId, canvas, textMode, existingSkeleton
return;
}
- // 스켈레톤 생성 및 그리기
- skeletonBuilder(roofId, canvas, textMode, roof, existingSkeletonLines)
+ // 2. 스켈레톤 생성 및 그리기
+ skeletonBuilder(roofId, canvas, textMode, roof)
}
/**
@@ -42,74 +42,77 @@ export const drawSkeletonRidgeRoof = (roofId, canvas, textMode, existingSkeleton
* @param {fabric.Object} roof - 지붕 객체
* @param {Array} skeletonLines - 스켈레톤 라인 배열
*/
-export const skeletonBuilder = (roofId, canvas, textMode, roof, skeletonLines) => {
+export const skeletonBuilder = (roofId, canvas, textMode, roof) => {
const geoJSONPolygon = toGeoJSON(roof.points)
-
+ const skeletonLines = []
try {
+ // SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거
geoJSONPolygon.pop()
const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]])
+
console.log(`지붕 형태: ${skeleton.roof_type}`, skeleton.edge_analysis)
- const innerLines = createInnerLinesFromSkeleton(skeleton, roof.lines, roof, canvas, textMode, skeletonLines)
- roof.innerLines = innerLines
+ // 스켈레톤 데이터를 기반으로 내부선 생성
+ roof.innerLines = createInnerLinesFromSkeleton(skeleton, roof.lines, roof, canvas, textMode, skeletonLines)
- if (!canvas.skeletonStates) canvas.skeletonStates = {}
+ // 캔버스에 스켈레톤 상태 저장
+ if (!canvas.skeletonStates) {
+ canvas.skeletonStates = {}
+ canvas.skeletonLines = []
+ }
canvas.skeletonStates[roofId] = true
+
canvas.renderAll()
} catch (e) {
console.error('스켈레톤 생성 중 오류 발생:', e)
- if (canvas.skeletonStates) canvas.skeletonStates[roofId] = false
+ if (canvas.skeletonStates) {
+ canvas.skeletonStates[roofId] = false
+ }
}
}
/**
- * 스켈레톤 결과와 외벽선 정보를 바탕으로 내부선을 생성합니다.
- * @param {object} skeleton - 스켈레톤 객체
- * @param {Array} baseLines - 외벽선 배열
- * @param {fabric.Object} roof - 지붕 객체
- * @param {fabric.Canvas} canvas - 캔버스 객체
- * @param {string} textMode - 텍스트 표시 모드
- * @param {Array} skeletonLines - 스켈레톤 라인 배열 (이 함수 내에서 생성 및 수정됨)
+ * 스켈레톤 결과와 외벽선 정보를 바탕으로 내부선(용마루, 추녀)을 생성합니다.
+ * @param {object} skeleton - SkeletonBuilder로부터 반환된 스켈레톤 객체
+ * @param {Array} baseLines - 원본 외벽선 QLine 객체 배열
+ * @param {fabric.Object} roof - 대상 지붕 객체
+ * @param {fabric.Canvas} canvas - Fabric.js 캔버스 객체
+ * @param {string} textMode - 텍스트 표시 모드 ('plane', 'actual', 'none')
+ * @param {Array} skeletonLines - 스켈레톤 라인 배열 (수정 대상)
* @returns {Array} 생성된 내부선(QLine) 배열
*/
const createInnerLinesFromSkeleton = (skeleton, baseLines, roof, canvas, textMode, skeletonLines) => {
if (!skeleton?.Edges) return []
const processedInnerEdges = new Set()
- const gableEdges = [];
- // 1단계: 모든 내부 스켈레톤 라인을 생성하고, 케라바(Gable) Edge를 식별합니다.
+ // 1. 모든 Edge를 순회하며 기본 스켈레톤 선(용마루)을 수집합니다.
skeleton.Edges.forEach(edgeResult => {
- processEavesEdge(edgeResult, skeletonLines, processedInnerEdges);
- const { Begin, End } = edgeResult.Edge;
- if (baseLines.some(line => line.attributes.type === 'gable' && isSameLine(Begin.X, Begin.Y, End.X, End.Y, line))) {
- gableEdges.push(edgeResult);
- }
+ processEavesEdge(edgeResult, skeletonLines, processedInnerEdges)
});
- // 2단계: 모든 케라바 영역에 포함된 불필요한 내부 선들을 한번에 제거합니다.
- if (gableEdges.length > 0) {
- const allGablePolygonPoints = gableEdges.flatMap(edge => edge.Polygon.map(p => ({ x: p.X, y: p.Y })));
- const gablePointSet = new Set(allGablePolygonPoints.map(p => `${p.x.toFixed(3)},${p.y.toFixed(3)}`));
- for (let i = skeletonLines.length - 1; i >= 0; i--) {
- const line = skeletonLines[i];
- const p1Key = `${line.p1.x.toFixed(3)},${line.p1.y.toFixed(3)}`;
- const p2Key = `${line.p2.x.toFixed(3)},${line.p2.y.toFixed(3)}`;
+ // 2. 케라바(Gable) 속성을 가진 외벽선에 해당하는 스켈레톤을 후처리합니다.
+ skeleton.Edges.forEach(edgeResult => {
- if (gablePointSet.has(p1Key) && gablePointSet.has(p2Key)) {
- skeletonLines.splice(i, 1);
+ const { Begin, End } = edgeResult.Edge;
+ const gableBaseLine = baseLines.find(line =>
+ line.attributes.type === 'gable' && isSameLine(Begin.X, Begin.Y, End.X, End.Y, line)
+ );
+
+ if (gableBaseLine) {
+ if(canvas.skeletonLines.length > 0){
+ skeletonLines = canvas.skeletonLines;
}
+ processGableEdge(edgeResult, baseLines, skeletonLines, gableBaseLine);
+ canvas.skeletonLines = skeletonLines;
}
- }
- // 3단계: 선 제거로 인해 발생한 모든 끊어진 선들을 한번에 연장하고 정리합니다.
- handleDisconnectedLines(skeletonLines, baseLines);
+ });
- // 4단계: 최종적으로 정리된 선들을 QLine 객체로 변환하여 캔버스에 추가합니다.
+ // 3. 최종적으로 정리된 스켈레톤 선들을 QLine 객체로 변환하여 캔버스에 추가합니다.
const innerLines = [];
skeletonLines.forEach(line => {
- if (!line.p1 || !line.p2) return;
const { p1, p2, attributes, lineStyle } = line;
const innerLine = new QLine([p1.x, p1.y, p2.x, p2.y], {
parentId: roof.id,
@@ -138,9 +141,12 @@ const createInnerLinesFromSkeleton = (skeleton, baseLines, roof, canvas, textMod
*/
function processEavesEdge(edgeResult, skeletonLines, processedInnerEdges) {
const polygonPoints = edgeResult.Polygon.map(p => ({ x: p.X, y: p.Y }));
+
for (let i = 0; i < polygonPoints.length; i++) {
const p1 = polygonPoints[i];
const p2 = polygonPoints[(i + 1) % polygonPoints.length];
+
+ // 외벽선에 해당하는 스켈레톤 선은 제외하고 내부선만 추가
if (!isOuterEdge(p1, p2, [edgeResult.Edge])) {
addRawLine(skeletonLines, processedInnerEdges, p1, p2, 'RIDGE', '#FF0000', 3);
}
@@ -148,274 +154,255 @@ function processEavesEdge(edgeResult, skeletonLines, processedInnerEdges) {
}
/**
- * 끊어진 모든 선들을 찾아 연장하고, 교차점에서 정리하는 로직을 수행합니다.
- * @param {Array} skeletonLines - 수정할 전체 스켈레톤 라인 배열
+ * GABLE(케라바) Edge를 처리하여 스켈레톤 선을 정리하고 연장합니다.
+ * @param {object} edgeResult - 스켈레톤 Edge 데이터
* @param {Array} baseLines - 전체 외벽선 배열
+ * @param {Array} skeletonLines - 전체 스켈레톤 라인 배열
*/
-function handleDisconnectedLines(skeletonLines, baseLines) {
- // 1. 연결이 끊어진 모든 스켈레톤 선을 찾아 연장합니다.
+function processGableEdge(edgeResult, baseLines, skeletonLines, selectBaseLine) {
+ const edgePoints = [{ x:selectBaseLine.startPoint.x, y:selectBaseLine.startPoint.y}, {x:selectBaseLine.endPoint.x, y:selectBaseLine.endPoint.y}]//edgeResult.Polygon.map(p => ({ x: p.X, y: p.Y }));
+ const polygons = createPolygonsFromSkeletonLines(skeletonLines, selectBaseLine);
+ // 1. 케라바 면과 관련된 불필요한 스켈레톤 선을 제거합니다.
+ for (let i = skeletonLines.length - 1; i >= 0; i--) {
+ const line = skeletonLines[i];
+ const isEdgeLine = line.p1 && line.p2 &&
+ edgePoints.some(ep => Math.abs(ep.x - line.p1.x) < 0.001 && Math.abs(ep.y - line.p1.y) < 0.001) &&
+ edgePoints.some(ep => Math.abs(ep.x - line.p2.x) < 0.001 && Math.abs(ep.y - line.p2.y) < 0.001);
+
+ if (isEdgeLine) {
+ skeletonLines.splice(i, 1);
+ }
+ }
+
+ // 2. 연결이 끊어진 스켈레톤 선을 찾아 연장합니다.
const { disconnectedLines } = findDisconnectedSkeletonLines(skeletonLines, baseLines);
- const modifiedIndices = new Set();
disconnectedLines.forEach(dLine => {
const { index, extendedLine, p1Connected, p2Connected } = dLine;
const newPoint = extendedLine?.point;
if (!newPoint) return;
-
- modifiedIndices.add(index);
- if (!p1Connected) {
- skeletonLines[index].p1 = { ...skeletonLines[index].p1, x: newPoint.x, y: newPoint.y };
- } else if (!p2Connected) {
+ // p1이 끊어졌으면 p1을, p2가 끊어졌으면 p2를 연장된 지점으로 업데이트
+ if (p1Connected) { //p2 연장
skeletonLines[index].p2 = { ...skeletonLines[index].p2, x: newPoint.x, y: newPoint.y };
- }
- });
-
- if (modifiedIndices.size === 0) return;
-
- // 2. 연장된 라인들이 서로 교차하는 경우, 가장 가까운 교차점에서 잘라냅니다.
- const extendedLinesSnapshot = JSON.parse(JSON.stringify(skeletonLines));
- const distanceSq = (p1, p2) => Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2);
-
- modifiedIndices.forEach(index => {
- const dLineInfo = disconnectedLines.find(d => d.index === index);
- if (!dLineInfo) return;
-
- const modifiedLine = extendedLinesSnapshot[index];
- const originalConnectedPoint = dLineInfo.p1Connected ? dLineInfo.line.p2 : dLineInfo.line.p1;
- let closestIntersection = null;
- let minDistanceSq = Infinity;
-
- for (let i = 0; i < extendedLinesSnapshot.length; i++) {
- if (i === index) continue;
- const otherLine = extendedLinesSnapshot[i];
- const intersection = findSegmentIntersection(modifiedLine.p1, modifiedLine.p2, otherLine.p1, otherLine.p2);
-
- if (intersection) {
- const distSq = distanceSq(originalConnectedPoint, intersection);
- if (distSq < minDistanceSq) {
- minDistanceSq = distSq;
- closestIntersection = intersection;
- }
- }
- }
-
- if (closestIntersection) {
- if (!dLineInfo.p1Connected) {
- skeletonLines[index].p1 = closestIntersection;
- } else {
- skeletonLines[index].p2 = closestIntersection;
- }
+ } else if (p2Connected) {//p1 연장
+ skeletonLines[index].p1 = { ...skeletonLines[index].p1, x: newPoint.x, y: newPoint.y };
}
});
}
+
// --- Helper Functions ---
+/**
+ * 두 점으로 이루어진 선분이 외벽선인지 확인합니다.
+ * @param {object} p1 - 점1 {x, y}
+ * @param {object} p2 - 점2 {x, y}
+ * @param {Array