From c5d830f6c01ffe7ac35addf7f7c8fe8cb40c24fb Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Wed, 10 Dec 2025 10:24:37 +0900 Subject: [PATCH 1/9] =?UTF-8?q?=ED=94=8C=EB=9E=9C=20=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=EC=8B=9C=20=EC=99=B8=EB=B2=BD=EC=84=A0=20=EB=82=A8=EC=95=84?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20=ED=98=84=EC=83=81=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=A7=80=EB=B6=95=EC=9E=AC=20=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20=EC=A7=80=EB=B6=95=EC=9E=AC=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=80=EC=A7=80=EA=B3=A0=20=EC=9E=88=EB=8A=94=20=EB=8B=A4?= =?UTF-8?q?=EB=A5=B8=20=EC=A7=80=EB=B6=95=EB=93=A4=EC=9D=98=20=EC=A7=80?= =?UTF-8?q?=EB=B6=95=EC=9E=AC=EB=8F=84=20=EB=B3=80=EA=B2=BD=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/floor-plan/CanvasFrame.jsx | 3 +++ src/hooks/roofcover/useRoofAllocationSetting.js | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/floor-plan/CanvasFrame.jsx b/src/components/floor-plan/CanvasFrame.jsx index 63dc523a..9441dc7c 100644 --- a/src/components/floor-plan/CanvasFrame.jsx +++ b/src/components/floor-plan/CanvasFrame.jsx @@ -32,6 +32,7 @@ import { useEvent } from '@/hooks/useEvent' import { compasDegAtom } from '@/store/orientationAtom' import { hotkeyStore } from '@/store/hotkeyAtom' import { usePopup } from '@/hooks/usePopup' +import { outerLinePointsState } from '@/store/outerLineAtom' export default function CanvasFrame() { const canvasRef = useRef(null) @@ -45,6 +46,7 @@ export default function CanvasFrame() { const totalDisplay = useRecoilValue(totalDisplaySelector) // 집계표 표시 여부 const { setIsGlobalLoading } = useContext(QcastContext) const resetModuleStatisticsState = useResetRecoilState(moduleStatisticsState) + const resetOuterLinePoints = useResetRecoilState(outerLinePointsState) const resetMakersState = useResetRecoilState(makersState) const resetSelectedMakerState = useResetRecoilState(selectedMakerState) const resetSeriesState = useResetRecoilState(seriesState) @@ -137,6 +139,7 @@ export default function CanvasFrame() { const resetRecoilData = () => { // resetModuleStatisticsState() + resetOuterLinePoints() resetMakersState() resetSelectedMakerState() resetSeriesState() diff --git a/src/hooks/roofcover/useRoofAllocationSetting.js b/src/hooks/roofcover/useRoofAllocationSetting.js index 3ea1894c..4bc26098 100644 --- a/src/hooks/roofcover/useRoofAllocationSetting.js +++ b/src/hooks/roofcover/useRoofAllocationSetting.js @@ -374,11 +374,18 @@ export function useRoofAllocationSetting(id) { setBasicSetting((prev) => { 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) setRoofMaterials(newRoofList) setRoofsStore(newRoofList) - const selectedRoofMaterial = newRoofList.find((roof) => roof.selected) + setSurfaceShapePattern(currentObject, roofDisplay.column, false, selectedRoofMaterial, true) drawDirectionArrow(currentObject) modifyModuleSelectionData() From 2205809d0b2abe09bc125f89168ac4ec9ccd426f Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 10 Dec 2025 11:20:10 +0900 Subject: [PATCH 2/9] =?UTF-8?q?=EC=8B=9C=EB=AE=AC=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/simulator/Simulator.jsx | 89 ++++++++++++-------------- 1 file changed, 42 insertions(+), 47 deletions(-) diff --git a/src/components/simulator/Simulator.jsx b/src/components/simulator/Simulator.jsx index 9831d1b3..d873049f 100644 --- a/src/components/simulator/Simulator.jsx +++ b/src/components/simulator/Simulator.jsx @@ -264,7 +264,6 @@ export default function Simulator() { style={{ width: '30%' }} className="select-light" value={pwrGnrSimType} - defaultValue={`D`} onChange={(e) => { handleChartChangeData(e.target.value) setPwrGnrSimType(e.target.value) @@ -334,33 +333,31 @@ export default function Simulator() { - {moduleInfoList.length > 0 ? ( - moduleInfoList.map((moduleInfo) => { - return ( - <> - - {/* 지붕면 */} - {moduleInfo.roofSurface} - {/* 경사각 */} - - {convertNumberToPriceDecimal(moduleInfo.slopeAngle)} - {moduleInfo.classType == 0 ? '寸' : 'º'} - - {/* 방위각(도) */} - {convertNumberToPriceDecimal(moduleInfo.azimuth)} - {/* 태양전지모듈 */} - -
{moduleInfo.itemNo}
- - {/* 매수 */} - {convertNumberToPriceDecimal(moduleInfo.amount)} - - - ) - }) - ) : ( - - {getMessage('common.message.no.data')} + {moduleInfoList.length > 0 ? ( + moduleInfoList.map((moduleInfo) => { + return ( + + {/* 지붕면 */} + {moduleInfo.roofSurface} + {/* 경사각 */} + + {convertNumberToPriceDecimal(moduleInfo.slopeAngle)} + {moduleInfo.classType == 0 ? '寸' : 'º'} + + {/* 방위각(도) */} + {convertNumberToPriceDecimal(moduleInfo.azimuth)} + {/* 태양전지모듈 */} + +
{moduleInfo.itemNo}
+ + {/* 매수 */} + {convertNumberToPriceDecimal(moduleInfo.amount)} + + ) + }) + ) : ( + + {getMessage('common.message.no.data')} )} @@ -385,25 +382,23 @@ export default function Simulator() { - {pcsInfoList.length > 0 ? ( - pcsInfoList.map((pcsInfo) => { - return ( - <> - - {/* 파워컨디셔너 */} - -
{pcsInfo.itemNo}
- - {/* 대 */} - {convertNumberToPriceDecimal(pcsInfo.amount)} - - - ) - }) - ) : ( - - {getMessage('common.message.no.data')} - + {pcsInfoList.length > 0 ? ( + pcsInfoList.map((pcsInfo) => { + return ( + + {/* 파워컨디셔너 */} + +
{pcsInfo.itemNo}
+ + {/* 대 */} + {convertNumberToPriceDecimal(pcsInfo.amount)} + + ) + }) + ) : ( + + {getMessage('common.message.no.data')} + )} From fef352933f978a4934f6df941fae8beb87245a31 Mon Sep 17 00:00:00 2001 From: yscha Date: Thu, 11 Dec 2025 01:51:04 +0900 Subject: [PATCH 3/9] =?UTF-8?q?sortRoofLines=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 239 ++++++++++++++++++++++++------------- 1 file changed, 154 insertions(+), 85 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index a7bcb46f..c9a905d7 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -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 outerLinePoints = outerLines.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 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 !usedBaseIndices.has(idx)); + if(unused !== -1) { + sortedBaseLines[i] = baseLines[unused]; + usedBaseIndices.add(unused); + } else { + sortedBaseLines[i] = baseLines[0]; // 최후의 수단 + } + } + } + return sortedBaseLines; }; \ No newline at end of file From a6e935bc15db88e0a9aee467cf49bc74241124fa Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Fri, 12 Dec 2025 15:20:28 +0900 Subject: [PATCH 4/9] =?UTF-8?q?=EB=8F=84=EB=A8=B8=20=EC=98=A4=ED=94=84?= =?UTF-8?q?=EC=85=8B=20=EC=84=A0=ED=83=9D=20=EC=8B=9C=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../floor-plan/modal/object/DormerOffset.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/floor-plan/modal/object/DormerOffset.jsx b/src/components/floor-plan/modal/object/DormerOffset.jsx index fd3eb70a..1881b18e 100644 --- a/src/components/floor-plan/modal/object/DormerOffset.jsx +++ b/src/components/floor-plan/modal/object/DormerOffset.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react' +import { useEffect, useRef, useState } from 'react' import { useMessage } from '@/hooks/useMessage' import WithDraggable from '@/components/common/draggable/WithDraggable' import { useRecoilValue } from 'recoil' @@ -15,8 +15,8 @@ export default function DormerOffset(props) { const { closePopup } = usePopup() const [arrow1, setArrow1] = useState(null) const [arrow2, setArrow2] = useState(null) - const arrow1LengthRef = useRef() - const arrow2LengthRef = useRef() + const arrow1LengthRef = useRef(0) + const arrow2LengthRef = useRef(0) const [arrow1Length, setArrow1Length] = useState(0) const [arrow2Length, setArrow2Length] = useState(0) @@ -59,12 +59,12 @@ export default function DormerOffset(props) { name="" label="" className="input-origin block" - value={arrow1LengthRef.current.value} + value={arrow1LengthRef.current.value ?? 0} ref={arrow1LengthRef} onChange={(value) => setArrow1Length(value)} options={{ allowNegative: false, - allowDecimal: false + allowDecimal: false, }} /> From 53a1f4ed0053b7833e49f71f25cbf506309da6f3 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Fri, 12 Dec 2025 15:27:26 +0900 Subject: [PATCH 5/9] =?UTF-8?q?group=20object=20=EC=A2=8C=ED=91=9C=20?= =?UTF-8?q?=EC=9E=AC=EA=B3=84=EC=82=B0=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/fabric-extensions.js | 37 +++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/util/fabric-extensions.js b/src/util/fabric-extensions.js index 9410f764..acccc601 100644 --- a/src/util/fabric-extensions.js +++ b/src/util/fabric-extensions.js @@ -29,22 +29,39 @@ fabric.Rect.prototype.getCurrentPoints = function () { /** * fabric.Group에 getCurrentPoints 메서드를 추가 (도머 그룹용) - * 그룹의 groupPoints를 다시 계산하여 반환 + * 그룹 내 객체들의 점들을 수집하여 현재 월드 좌표를 반환 */ fabric.Group.prototype.getCurrentPoints = function () { - // groupPoints를 다시 계산 + // 그룹 내 객체들로부터 실시간으로 점들을 계산 + if (this._objects && this._objects.length > 0) { + let allPoints = [] - // 그룹에 groupPoints가 있으면 해당 점들을 사용 (도머의 경우) - if (this.groupPoints && Array.isArray(this.groupPoints)) { - const matrix = this.calcTransformMatrix() - console.log('this.groupPoints', this.groupPoints) - return this.groupPoints.map(function (p) { - const point = new fabric.Point(p.x, p.y) - return fabric.util.transformPoint(point, matrix) + // 그룹 내 모든 객체의 점들을 수집 + this._objects.forEach(function (obj) { + if (obj.getCurrentPoints && typeof obj.getCurrentPoints === 'function') { + const objPoints = obj.getCurrentPoints() + allPoints = allPoints.concat(objPoints) + } else if (obj.points && Array.isArray(obj.points)) { + 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 points = [ { x: bounds.left, y: bounds.top }, From be6fdce63c06282c7f29e535de68a28893cbc63e Mon Sep 17 00:00:00 2001 From: yscha Date: Sat, 13 Dec 2025 17:04:59 +0900 Subject: [PATCH 6/9] =?UTF-8?q?=ED=95=A0=EB=8B=B9=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index c9a905d7..cbbc4127 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -766,15 +766,19 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { }); }; - const sortedWallLines = sortCurrentRoofLines(wall.lines); + // const sortedWallLines = sortCurrentRoofLines(wall.lines); // roofLines의 방향에 맞춰 currentRoofLines 조정 후 정렬 const alignedCurrentRoofLines = alignLineDirection(currentRoofLines, roofLines); const sortedCurrentRoofLines = sortCurrentRoofLines(alignedCurrentRoofLines); - const sortedRoofLines = sortCurrentRoofLines(roofLines); + // const sortedRoofLines = sortCurrentRoofLines(roofLines); 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.baseLine은 움직인라인 @@ -802,7 +806,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { const currentRoofLine = currentRoofLines[index]; const moveLine = sortedBaseLines[index] const wallBaseLine = sortedBaseLines[index] - console.log("wallBaseLine", wallBaseLine); + //console.log("wallBaseLine", wallBaseLine); //roofline 외곽선 설정 @@ -876,7 +880,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { return line } - getAddLine(roofLine.startPoint, roofLine.endPoint, ) + //getAddLine(roofLine.startPoint, roofLine.endPoint, ) //외곽선을 그린다 newPStart = { x: roofLine.x1, y: roofLine.y1 } newPEnd = { x: roofLine.x2, y: roofLine.y2 } @@ -1228,6 +1232,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { 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') + } if(isStartEnd.end){ @@ -1479,6 +1484,8 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { getAddLine(newPStart, newPEnd, 'red') //canvas.remove(roofLine) + }else{ + getAddLine(roofLine.startPoint, roofLine.endPoint, ) } canvas.renderAll() }); From 6c5a0a8a5454804c700de81a0b904cd052df5b99 Mon Sep 17 00:00:00 2001 From: yscha Date: Mon, 15 Dec 2025 01:29:04 +0900 Subject: [PATCH 7/9] =?UTF-8?q?out=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 181 ++++++++++++++++++++++++------------- 1 file changed, 117 insertions(+), 64 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index cbbc4127..edab8b87 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -794,6 +794,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { // 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) @@ -804,33 +805,17 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { }//roofLines.find(line => line.attributes.wallLineId === wallLine.attributes.wallId); const currentRoofLine = currentRoofLines[index]; - const moveLine = sortedBaseLines[index] - const wallBaseLine = sortedBaseLines[index] + const moveLine = wall.baseLines[index] + const wallBaseLine = wall.baseLines[index] //console.log("wallBaseLine", wallBaseLine); //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 if (!origin) return @@ -973,7 +958,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { const eLineY = Big(bStartY).minus(wallLine.y1).abs().toNumber() 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 const newLine = roofLines[idx-1]; @@ -1000,7 +985,16 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { getAddLine({ y: inLine.y1, x: inLine.x1},{ y: newPStart.y, x: newPStart.x }, 'purple') } }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') + } } } @@ -1015,7 +1009,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { console.log("startLines:::::::", inLine); const eLineY = Big(bStartY).minus(wallLine.y2).abs().toNumber() 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 const newLine = roofLines[idx+1]; @@ -1042,7 +1036,17 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } }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') + } } } @@ -1109,7 +1113,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { console.log("startLines:::::::", inLine); const eLineY = Big(bStartY).minus(wallLine.y1).abs().toNumber() 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 const newLine = roofLines[idx-1]; @@ -1135,7 +1139,19 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { getAddLine({ y: inLine.y1, x: inLine.x1 },{ y: newPStart.y, x: newPStart.x }, 'purple') } }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') + } + } + } } @@ -1150,7 +1166,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { console.log("startLines:::::::", inLine); const eLineY = Big(bStartY).minus(wallLine.y2).abs().toNumber() 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 const newLine = roofLines[idx+1]; if(inLine){ @@ -1175,26 +1191,26 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple') } }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 if (['top', 'bottom'].includes(mLine.position)) { @@ -1253,11 +1269,14 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') 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') } }else if(condition === 'top_out') { console.log("top_out isStartEnd:::::::", isStartEnd); + if (isStartEnd.start ) { const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() const aStartX = Big(roofLine.x1).plus(moveDist).toNumber() @@ -1265,7 +1284,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: newPEnd.y }) 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 let idx = (0 > index - 1)?roofLines.length:index const newLine = roofLines[idx-1]; @@ -1293,7 +1312,17 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } }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') + } + } } } @@ -1305,7 +1334,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: newPEnd.y }) console.log("startLines:::::::", inLine); 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 let idx = (roofLines.length < index + 1)?0:index const newLine = roofLines[idx+1]; @@ -1333,7 +1362,18 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { getAddLine({ y: inLine.y1, x: inLine.x1 },{ y: newPEnd.y, x: newPEnd.x }, 'purple') } }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') + } + } } } @@ -1392,7 +1432,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: roofLine.y1 }) console.log("startLines:::::::", inLine); 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 let idx = (0 > index - 1)?roofLines.length:index const newLine = roofLines[idx-1]; @@ -1420,7 +1460,18 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPStart.y, x: newPStart.x }, 'purple') } }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') + } + } } } @@ -1434,7 +1485,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { console.log("startLines:::::::", inLine); const eLineX = Big(bStartX).minus(wallLine.x2).abs().toNumber() 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 const newLine = roofLines[idx + 1]; @@ -1460,25 +1511,23 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { getAddLine({ y: inLine.y1, x: inLine.x1 },{ y: newPEnd.y, x: newPEnd.x }, 'purple') } }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; - // } } } @@ -1487,6 +1536,9 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { }else{ getAddLine(roofLine.startPoint, roofLine.endPoint, ) } + + + canvas.renderAll() }); } @@ -1539,6 +1591,7 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { if(!outerLine) { outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points); console.log('Has matching line:', outerLine); + //if(outerLine === null) return } let pitch = outerLine?.attributes?.pitch??0 From 9d9cf4a05d4735adeb683d5e656f9cca01f308f3 Mon Sep 17 00:00:00 2001 From: ysCha Date: Mon, 15 Dec 2025 14:24:15 +0900 Subject: [PATCH 8/9] =?UTF-8?q?followLine=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/roofcover/useMovementSetting.js | 45 +++++++++++++---------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/hooks/roofcover/useMovementSetting.js b/src/hooks/roofcover/useMovementSetting.js index cb5c0f02..2525cdf6 100644 --- a/src/hooks/roofcover/useMovementSetting.js +++ b/src/hooks/roofcover/useMovementSetting.js @@ -180,18 +180,9 @@ export function useMovementSetting(id) { name: 'followLine', }) canvas.add(followLine) + followLine.bringToFront() FOLLOW_LINE_REF.current = followLine - canvas.on('mouse:move', (event) => { - const mousePos = getIntersectMousePoint(event) - if (followLine.x1 === followLine.x2) { - followLine.left = mousePos.x - 2 - } else { - followLine.top = mousePos.y - 2 - } - canvas.renderAll() - }) - canvas.renderAll() }, [currentObject]) @@ -247,7 +238,9 @@ export function useMovementSetting(id) { const mouseMoveEvent = (e) => { //console.log('mouseMoveEvent:::::',e) - const target = canvas.getActiveObject() + // 기존에는 activeObject를 사용했으나, 이 기능에서는 선택된 라인을 비선택(selectable:false) 상태로 두므로 + // 항상 selectedObject.current를 기준으로 계산한다. + const target = selectedObject.current if (!target) return // 디버깅 로그 추가 @@ -285,11 +278,23 @@ export function useMovementSetting(id) { } } + // followLine도 포인터를 따라가도록 동기화 (하나의 mouse:move 핸들러만 사용) + const followLine = FOLLOW_LINE_REF.current + if (followLine) { + if (followLine.x1 === followLine.x2) { + // 수직 라인: x만 이동 + followLine.left = currentX.toNumber() - 2 + } else { + // 수평 라인: y만 이동 + followLine.top = currentY.toNumber() - 2 + } + followLine.bringToFront() + followLine.setCoords && followLine.setCoords() + canvas.renderAll() + } + // 방향 정보를 사용하여 라디오 버튼 상태 업데이트 - //console.log(`방향: ${direction}, 값: ${value.toNumber()}`) - - currentCalculatedValue = value.toNumber() if (typeRef.current === TYPE.FLOW_LINE) { @@ -618,11 +623,13 @@ export function useMovementSetting(id) { value = value.neg() } } else { - //console.log("error::", UP_DOWN_REF) - value = - UP_DOWN_REF.FILLED_INPUT_REF.current.value !== '' - ? Big(UP_DOWN_REF.FILLED_INPUT_REF.current.value) - : Big(UP_DOWN_REF.POINTER_INPUT_REF.current.value) + console.log("error::", UP_DOWN_REF.POINTER_INPUT_REF.current.value) + value = Big( + (UP_DOWN_REF?.FILLED_INPUT_REF?.current?.value?.trim() || + UP_DOWN_REF?.POINTER_INPUT_REF?.current?.value?.trim() || + '0' + ) + ); const midX = Big(target.x1).plus(target.x2).div(2) const midY = Big(target.y1).plus(target.y2).div(2) From ff5cd04b2713d3cd484a2ed5ace4dd3f49e5ddc9 Mon Sep 17 00:00:00 2001 From: ysCha Date: Mon, 15 Dec 2025 16:42:31 +0900 Subject: [PATCH 9/9] =?UTF-8?q?[1351]=EA=B2=BD=EC=82=AC=EB=8F=84=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EC=88=98=EC=B9=98=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?inclBase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placementShape/PlacementShapeSetting.jsx | 34 ++++---- src/hooks/option/useCanvasSetting.js | 5 +- .../roofcover/useRoofAllocationSetting.js | 80 ++++++++++--------- 3 files changed, 67 insertions(+), 52 deletions(-) diff --git a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx index 51c16c1a..e283fff0 100644 --- a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx +++ b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx @@ -170,8 +170,8 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla setCurrentRoof({ ...selectedRoofMaterial, - pitch: currentRoof?.pitch, - angle: currentRoof?.angle, + // pitch: currentRoof?.pitch, + // angle: currentRoof?.angle, index: 0, planNo: currentRoof.planNo, roofSizeSet: String(currentRoof.roofSizeSet), @@ -353,19 +353,21 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla value={index === 0 ? currentRoof?.pitch || '0' : currentRoof?.angle || '0'} onChange={(value) => { if (index === 0) { - const num = value === '' ? '' : Number(value) + const pitch = value === '' ? '' : Number(value); + const angle = pitch === '' ? '' : getDegreeByChon(pitch); setCurrentRoof(prev => ({ ...prev, - pitch: num === '' ? '' : num, - angle: num === '' ? '' : getDegreeByChon(num), - })) + pitch, + angle + })); } else { - const num = value === '' ? '' : Number(value) - setCurrentRoof( prev => ({ + const angle = value === '' ? '' : Number(value); + const pitch = angle === '' ? '' : getChonByDegree(angle); + setCurrentRoof(prev => ({ ...prev, - pitch: num === '' ? '' : getChonByDegree(num), - angle: num === '' ? '' : num, - })) + pitch, + angle + })); } }} options={{ @@ -514,13 +516,17 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla {/*/>*/} { - setCurrentRoof({ ...currentRoof, value }) + const hajebichi = value === '' ? '' : Number(value); + setCurrentRoof(prev => ({ + ...prev, + hajebichi + })); }} readOnly={currentRoof?.roofPchAuth === 'R'} disabled={currentRoof?.roofSizeSet === '3'} diff --git a/src/hooks/option/useCanvasSetting.js b/src/hooks/option/useCanvasSetting.js index af3cee04..072a2987 100644 --- a/src/hooks/option/useCanvasSetting.js +++ b/src/hooks/option/useCanvasSetting.js @@ -42,6 +42,7 @@ import { useEvent } from '@/hooks/useEvent' import { logger } from '@/util/logger' import { useText } from '@/hooks/useText' import { usePolygon } from '@/hooks/usePolygon' +import { getDegreeByChon } from '@/util/canvas-util' const defaultDotLineGridSetting = { INTERVAL: { @@ -177,8 +178,8 @@ export function useCanvasSetting(executeEffect = true) { raft: item.raftBase && parseInt(item.raftBase), layout: ['ROOF_ID_SLATE', 'ROOF_ID_SINGLE'].includes(item.roofMatlCd) ? ROOF_MATERIAL_LAYOUT.STAIRS : ROOF_MATERIAL_LAYOUT.PARALLEL, hajebichi: item.roofPchBase && parseInt(item.roofPchBase), - pitch: item.pitch ? parseInt(item.pitch) : 4, - angle: item.angle ? parseInt(item.angle) : 21.8, + pitch: item.inclBase ? parseInt(item.inclBase) : 4, + angle: getDegreeByChon(item.inclBase ? parseInt(item.inclBase): 4) //item.angle ? parseInt(item.angle) : 21.8, })) setRoofMaterials(roofLists) return roofLists diff --git a/src/hooks/roofcover/useRoofAllocationSetting.js b/src/hooks/roofcover/useRoofAllocationSetting.js index 4bc26098..f93e230d 100644 --- a/src/hooks/roofcover/useRoofAllocationSetting.js +++ b/src/hooks/roofcover/useRoofAllocationSetting.js @@ -114,46 +114,54 @@ export function useRoofAllocationSetting(id) { */ const fetchBasicSettings = async (planNo) => { try { - await get({ url: `/api/canvas-management/canvas-basic-settings/by-object/${correntObjectNo}/${planNo}` }).then((res) => { - let roofsArray = {} - - if (res.length > 0) { - roofsArray = res.map((item) => { - return { - planNo: item.planNo, - roofApply: item.roofApply, - roofSeq: item.roofSeq, - roofMatlCd: item.roofMatlCd, - roofWidth: item.roofWidth, - roofHeight: item.roofHeight, - roofHajebichi: item.roofHajebichi, - roofGap: item.roofGap, - roofLayout: item.roofLayout, - roofPitch: item.roofPitch, - roofAngle: item.roofAngle, - } - }) - } else { - if (roofList.length > 0) { - roofsArray = roofList - } else { - roofsArray = [ - { - planNo: planNo, - roofApply: true, - roofSeq: 0, - roofMatlCd: 'ROOF_ID_WA_53A', - roofWidth: 265, - roofHeight: 235, - roofHajebichi: 0, - roofGap: 'HEI_455', - roofLayout: 'P', + const response = await get({ url: `/api/canvas-management/canvas-basic-settings/by-object/${correntObjectNo}/${planNo}` }); + + let roofsArray = []; + + // API에서 데이터를 성공적으로 가져온 경우 + if (response && response.length > 0) { + roofsArray = response.map((item, index) => ({ + planNo: item.planNo, + roofApply: item.roofApply, + roofSeq: item.roofSeq || index, + roofMatlCd: item.roofMatlCd, + roofWidth: item.roofWidth, + roofHeight: item.roofHeight, + roofHajebichi: item.roofHajebichi, + roofGap: item.roofGap, + roofLayout: item.roofLayout, + roofPitch: item.roofPitch, + roofAngle: item.roofAngle, + selected: index === 0, // 첫 번째 항목을 기본 선택으로 설정 + index: index + })); + } + // API에서 데이터가 없고 기존 roofList가 있는 경우 + else if (roofList && roofList.length > 0) { + roofsArray = roofList.map((roof, index) => ({ + ...roof, + selected: index === 0 // 첫 번째 항목을 기본 선택으로 설정 + })); + } + // 둘 다 없는 경우 기본값 설정 + else { + roofsArray = [ + { + planNo: planNo, + roofApply: true, + roofSeq: 0, + roofMatlCd: 'ROOF_ID_WA_53A', + roofWidth: 265, + roofHeight: 235, + roofHajebichi: 0, + roofGap: 'HEI_455', + roofLayout: 'P', roofPitch: 4, roofAngle: 21.8, }, ] } - } + /** * 데이터 설정 @@ -205,7 +213,7 @@ export function useRoofAllocationSetting(id) { angle: roof.angle ?? '', })) setCurrentRoofList(normalizedRoofs) - }) + } catch (error) { console.error('Data fetching error:', error) }