From d43d18088ace49d80fa66a1b6748b469ec50e578 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 6 Feb 2026 15:08:21 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[1449]3.=EB=B3=80=EB=B3=84=20offset=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20sk=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 47 ++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 1c25b85b..a8a4063e 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -369,10 +369,10 @@ 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) + // roof.points 순서와 맞춰야 offset 매핑이 정확함 + const baseLinePoints = createOrderedBasePoints(roof.points, baseLines) - 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 })) @@ -391,16 +391,46 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { const moveFlowLine = roof.moveFlowLine || 0 // Provide a default value const moveUpDown = roof.moveUpDown || 0 // Provide a default value - let points = roof.points + const isClosedPolygon = (points) => + points.length > 1 && isSamePoint(points[0], points[points.length - 1]) + const roofLinePoints = isClosedPolygon(roof.points) ? roof.points.slice(0, -1) : roof.points + const orderedBaseLinePoints = isClosedPolygon(baseLinePoints) ? baseLinePoints.slice(0, -1) : baseLinePoints + + console.log('roofLinePoints:', roofLinePoints) + console.log('baseLinePoints:', orderedBaseLinePoints) + + // baseLinePoint에서 roofLinePoint 방향으로 45도 대각선 확장 후 가장 가까운 접점 계산 + let roofLineContactPoints = orderedBaseLinePoints.map((point, index) => { + const roofPoint = roofLinePoints[index] + if (!roofPoint) return point + + const dx = roofPoint.x - point.x + const dy = roofPoint.y - point.y + const absDx = Math.abs(dx) + const absDy = Math.abs(dy) + + // 거리가 0이면 이미 같은 위치 + if (absDx === 0 && absDy === 0) return point + + const step = Math.min(absDx, absDy) + if (step === 0) return point + + const nextX = point.x + Math.sign(dx) * step + const nextY = point.y + Math.sign(dy) * step + return { + x: Number(nextX.toFixed(1)), + y: Number(nextY.toFixed(1)) + } + }) //마루이동 if (moveFlowLine !== 0 || moveUpDown !== 0) { - points = movingLineFromSkeleton(roofId, canvas) + roofLineContactPoints = movingLineFromSkeleton(roofId, canvas) } - console.log('points:', points) - const geoJSONPolygon = toGeoJSON(points) + console.log('points:', roofLineContactPoints) + const geoJSONPolygon = toGeoJSON(roofLineContactPoints) try { // SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거 @@ -436,7 +466,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { } canvas.skeleton = [] canvas.skeleton = cleanSkeleton - canvas.skeleton.lastPoints = points + canvas.skeleton.lastPoints = roofLineContactPoints canvas.set('skeleton', cleanSkeleton) canvas.renderAll() @@ -3656,4 +3686,3 @@ function findInteriorPoint(line, polygonLines) { end: endIsValley }; } - From c593f007c8e6322d048b98a333a4b3b13e6ec0ee Mon Sep 17 00:00:00 2001 From: ysCha Date: Mon, 9 Feb 2026 13:20:47 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=ED=8A=B9=EC=88=98=EB=AC=B8=EC=9E=90=20?= =?UTF-8?q?=EC=A4=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/common-utils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util/common-utils.js b/src/util/common-utils.js index 2b3fe37d..57ed1d71 100644 --- a/src/util/common-utils.js +++ b/src/util/common-utils.js @@ -94,14 +94,14 @@ export const inputNumberCheck = (e) => { } } -// 영문, 숫자, 특수문자(ASCII)만 입력 체크 +// 영문, 숫자, 특수문자(_ . # ! & + - @)만 입력 체크 export const inputUserIdCheck = (e) => { const input = e.target - const allowedRegex = /^[A-Za-z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]*$/g + const allowedRegex = /^[A-Za-z0-9_.#!&+\-@]*$/g if (allowedRegex.test(input.value)) { input.value = input.value } else { - input.value = input.value.replace(/[^A-Za-z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]/g, '') + input.value = input.value.replace(/[^A-Za-z0-9_.#!&+\-@]/g, '') } } From 67e62ee90517d72380f17d6a98619fc3c2e4ea5e Mon Sep 17 00:00:00 2001 From: Jaeyoung Lee Date: Mon, 9 Feb 2026 17:47:07 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=EC=BC=80=EB=9D=BC=EB=B0=94=20=EC=A7=80?= =?UTF-8?q?=EB=B6=95=20=EC=B6=9C=ED=8F=AD=200=EC=9D=BC=EB=95=8C=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EB=B0=9C=EC=83=9D=20=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/qpolygon-utils.js | 239 ++++++++++++++++++++++--------------- 1 file changed, 146 insertions(+), 93 deletions(-) diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 931362f4..4937a0ed 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -608,39 +608,53 @@ export const drawGableRoof = (roofId, canvas, textMode) => { } } - const findEdge = { vertex1: { x: midX, y: midY }, vertex2: { x: midX + roofVector.x * offset, y: midY + roofVector.y * offset } } - const edgeDx = - Big(findEdge.vertex2.x).minus(Big(findEdge.vertex1.x)).abs().toNumber() < 0.1 - ? 0 - : Big(findEdge.vertex2.x).minus(Big(findEdge.vertex1.x)).toNumber() - const edgeDy = - Big(findEdge.vertex2.y).minus(Big(findEdge.vertex1.y)).abs().toNumber() < 0.1 - ? 0 - : Big(findEdge.vertex2.y).minus(Big(findEdge.vertex1.y)).toNumber() - const edgeLength = Math.sqrt(edgeDx * edgeDx + edgeDy * edgeDy) - const edgeVector = { x: edgeDx / edgeLength, y: edgeDy / edgeLength } - - const intersectRoofLines = [] - checkRoofLines.forEach((roofLine) => { - const lineEdge = { vertex1: { x: roofLine.x1, y: roofLine.y1 }, vertex2: { x: roofLine.x2, y: roofLine.y2 } } - const intersect = edgesIntersection(lineEdge, findEdge) - if (intersect) { - const intersectDx = - Big(intersect.x).minus(Big(findEdge.vertex1.x)).abs().toNumber() < 0.1 ? 0 : Big(intersect.x).minus(Big(findEdge.vertex1.x)).toNumber() - const intersectDy = - Big(intersect.y).minus(Big(findEdge.vertex1.y)).abs().toNumber() < 0.1 ? 0 : Big(intersect.y).minus(Big(findEdge.vertex1.y)).toNumber() - const intersectLength = Math.sqrt(intersectDx * intersectDx + intersectDy * intersectDy) - const intersectVector = { x: intersectDx / intersectLength, y: intersectDy / intersectLength } - if (edgeVector.x === intersectVector.x && edgeVector.y === intersectVector.y) { - intersectRoofLines.push({ roofLine, intersect, length: intersectLength }) + let currentRoof + if (line.attributes.offset === 0) { + checkRoofLines.forEach((roofLine) => { + const isVerticalSame = line.y1 === roofLine.y1 && line.y2 === roofLine.y2 + const isHorizontalSame = line.x1 === roofLine.x1 && line.x2 === roofLine.x2 + if ( + (isVerticalSame || isHorizontalSame) && + (isPointOnLineNew(roofLine, { x: line.x1, y: line.y1 }) || isPointOnLineNew(roofLine, { x: line.x2, y: line.y2 })) + ) { + currentRoof = { roofLine } } - } - }) + }) + } else { + const findEdge = { vertex1: { x: midX, y: midY }, vertex2: { x: midX + roofVector.x * offset, y: midY + roofVector.y * offset } } + const edgeDx = + Big(findEdge.vertex2.x).minus(Big(findEdge.vertex1.x)).abs().toNumber() < 0.1 + ? 0 + : Big(findEdge.vertex2.x).minus(Big(findEdge.vertex1.x)).toNumber() + const edgeDy = + Big(findEdge.vertex2.y).minus(Big(findEdge.vertex1.y)).abs().toNumber() < 0.1 + ? 0 + : Big(findEdge.vertex2.y).minus(Big(findEdge.vertex1.y)).toNumber() + const edgeLength = Math.sqrt(edgeDx * edgeDx + edgeDy * edgeDy) + const edgeVector = { x: edgeDx / edgeLength, y: edgeDy / edgeLength } - intersectRoofLines.sort((a, b) => a.length - b.length) - let currentRoof = intersectRoofLines.find((roof) => isPointOnLineNew(roof.roofLine, roof.intersect) && roof.length - offset < 0.1) - if (!currentRoof) { - currentRoof = intersectRoofLines[0] + const intersectRoofLines = [] + checkRoofLines.forEach((roofLine) => { + const lineEdge = { vertex1: { x: roofLine.x1, y: roofLine.y1 }, vertex2: { x: roofLine.x2, y: roofLine.y2 } } + const intersect = edgesIntersection(lineEdge, findEdge) + if (intersect) { + const intersectDx = + Big(intersect.x).minus(Big(findEdge.vertex1.x)).abs().toNumber() < 0.1 ? 0 : Big(intersect.x).minus(Big(findEdge.vertex1.x)).toNumber() + const intersectDy = + Big(intersect.y).minus(Big(findEdge.vertex1.y)).abs().toNumber() < 0.1 ? 0 : Big(intersect.y).minus(Big(findEdge.vertex1.y)).toNumber() + const intersectLength = Math.sqrt(intersectDx * intersectDx + intersectDy * intersectDy) + const intersectVector = { x: intersectDx / intersectLength, y: intersectDy / intersectLength } + if (edgeVector.x === intersectVector.x && edgeVector.y === intersectVector.y) { + intersectRoofLines.push({ roofLine, intersect, length: intersectLength }) + } + } + }) + + intersectRoofLines.sort((a, b) => a.length - b.length) + currentRoof = intersectRoofLines.find((roof) => isPointOnLineNew(roof.roofLine, roof.intersect) && roof.length - offset < 0.1) + if (!currentRoof) { + currentRoof = intersectRoofLines[0] + } } let startPoint, endPoint @@ -924,11 +938,16 @@ export const drawGableRoof = (roofId, canvas, textMode) => { const y2 = analyze.endPoint.y const lineVector = { x: 0, y: 0 } - if (analyze.isHorizontal) { - lineVector.x = Math.sign(analyze.roofLine.y1 - currentLine.attributes.originPoint.y1) - } - if (analyze.isVertical) { - lineVector.y = Math.sign(analyze.roofLine.x1 - currentLine.attributes.originPoint.x1) + if (currentLine.attributes.offset === 0) { + lineVector.x = analyze.roofVector.y + lineVector.y = analyze.roofVector.x + } else { + if (analyze.isHorizontal) { + lineVector.x = Math.sign(analyze.roofLine.y1 - currentLine.attributes.originPoint.y1) + } + if (analyze.isVertical) { + lineVector.y = Math.sign(analyze.roofLine.x1 - currentLine.attributes.originPoint.x1) + } } const overlapLines = [] @@ -1904,40 +1923,53 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { roofVector = { x: 1, y: 0 } } } - - const findEdge = { vertex1: { x: midX, y: midY }, vertex2: { x: midX + roofVector.x * offset, y: midY + roofVector.y * offset } } - const edgeDx = - Big(findEdge.vertex2.x).minus(Big(findEdge.vertex1.x)).abs().toNumber() < 0.1 - ? 0 - : Big(findEdge.vertex2.x).minus(Big(findEdge.vertex1.x)).toNumber() - const edgeDy = - Big(findEdge.vertex2.y).minus(Big(findEdge.vertex1.y)).abs().toNumber() < 0.1 - ? 0 - : Big(findEdge.vertex2.y).minus(Big(findEdge.vertex1.y)).toNumber() - const edgeLength = Math.sqrt(edgeDx * edgeDx + edgeDy * edgeDy) - const edgeVector = { x: edgeDx / edgeLength, y: edgeDy / edgeLength } - - const intersectRoofLines = [] - checkRoofLines.forEach((roofLine) => { - const lineEdge = { vertex1: { x: roofLine.x1, y: roofLine.y1 }, vertex2: { x: roofLine.x2, y: roofLine.y2 } } - const intersect = edgesIntersection(lineEdge, findEdge) - if (intersect) { - const intersectDx = - Big(intersect.x).minus(Big(findEdge.vertex1.x)).abs().toNumber() < 0.1 ? 0 : Big(intersect.x).minus(Big(findEdge.vertex1.x)).toNumber() - const intersectDy = - Big(intersect.y).minus(Big(findEdge.vertex1.y)).abs().toNumber() < 0.1 ? 0 : Big(intersect.y).minus(Big(findEdge.vertex1.y)).toNumber() - const intersectLength = Math.sqrt(intersectDx * intersectDx + intersectDy * intersectDy) - const intersectVector = { x: intersectDx / intersectLength, y: intersectDy / intersectLength } - if (edgeVector.x === intersectVector.x && edgeVector.y === intersectVector.y) { - intersectRoofLines.push({ roofLine, intersect, length: intersectLength }) + let currentRoof + if (line.attributes.offset === 0) { + checkRoofLines.forEach((roofLine) => { + const isVerticalSame = line.y1 === roofLine.y1 && line.y2 === roofLine.y2 + const isHorizontalSame = line.x1 === roofLine.x1 && line.x2 === roofLine.x2 + if ( + (isVerticalSame || isHorizontalSame) && + (isPointOnLineNew(roofLine, { x: line.x1, y: line.y1 }) || isPointOnLineNew(roofLine, { x: line.x2, y: line.y2 })) + ) { + currentRoof = { roofLine } } - } - }) + }) + } else { + const findEdge = { vertex1: { x: midX, y: midY }, vertex2: { x: midX + roofVector.x * offset, y: midY + roofVector.y * offset } } + const edgeDx = + Big(findEdge.vertex2.x).minus(Big(findEdge.vertex1.x)).abs().toNumber() < 0.1 + ? 0 + : Big(findEdge.vertex2.x).minus(Big(findEdge.vertex1.x)).toNumber() + const edgeDy = + Big(findEdge.vertex2.y).minus(Big(findEdge.vertex1.y)).abs().toNumber() < 0.1 + ? 0 + : Big(findEdge.vertex2.y).minus(Big(findEdge.vertex1.y)).toNumber() + const edgeLength = Math.sqrt(edgeDx * edgeDx + edgeDy * edgeDy) + const edgeVector = { x: edgeDx / edgeLength, y: edgeDy / edgeLength } - intersectRoofLines.sort((a, b) => a.length - b.length) - let currentRoof = intersectRoofLines.find((roof) => isPointOnLineNew(roof.roofLine, roof.intersect) && roof.length - offset < 0.1) - if (!currentRoof) { - currentRoof = intersectRoofLines[0] + const intersectRoofLines = [] + checkRoofLines.forEach((roofLine) => { + const lineEdge = { vertex1: { x: roofLine.x1, y: roofLine.y1 }, vertex2: { x: roofLine.x2, y: roofLine.y2 } } + const intersect = edgesIntersection(lineEdge, findEdge) + if (intersect) { + const intersectDx = + Big(intersect.x).minus(Big(findEdge.vertex1.x)).abs().toNumber() < 0.1 ? 0 : Big(intersect.x).minus(Big(findEdge.vertex1.x)).toNumber() + const intersectDy = + Big(intersect.y).minus(Big(findEdge.vertex1.y)).abs().toNumber() < 0.1 ? 0 : Big(intersect.y).minus(Big(findEdge.vertex1.y)).toNumber() + const intersectLength = Math.sqrt(intersectDx * intersectDx + intersectDy * intersectDy) + const intersectVector = { x: intersectDx / intersectLength, y: intersectDy / intersectLength } + if (edgeVector.x === intersectVector.x && edgeVector.y === intersectVector.y) { + intersectRoofLines.push({ roofLine, intersect, length: intersectLength }) + } + } + }) + + intersectRoofLines.sort((a, b) => a.length - b.length) + currentRoof = intersectRoofLines.find((roof) => isPointOnLineNew(roof.roofLine, roof.intersect) && roof.length - offset < 0.1) + if (!currentRoof) { + currentRoof = intersectRoofLines[0] + } } let startPoint, endPoint @@ -3289,6 +3321,8 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { prevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length] } }) + const prevIndex = baseLines.findIndex((line) => line === prevLine) + const nextIndex = baseLines.findIndex((line) => line === nextLine) //양옆이 처마가 아닐때 패스 if (prevLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES || nextLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES) { @@ -3377,14 +3411,24 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { ridgePoint[2] += ridgeVector.x * offset ridgePoint[3] += ridgeVector.y * offset - linesAnalysis.push({ - start: { x: ridgePoint[0], y: ridgePoint[1] }, - end: { x: ridgePoint[2], y: ridgePoint[3] }, - left: baseLines.findIndex((line) => line === prevLine), - right: baseLines.findIndex((line) => line === nextLine), - type: TYPES.RIDGE, - degree: 0, - }) + const alreadyRidge = linesAnalysis.find( + (line) => + line.type === TYPES.RIDGE && + line.start.x === ridgePoint[0] && + line.start.y === ridgePoint[1] && + line.end.x === ridgePoint[2] && + line.end.y === ridgePoint[3], + ) + if (!alreadyRidge) { + linesAnalysis.push({ + start: { x: ridgePoint[0], y: ridgePoint[1] }, + end: { x: ridgePoint[2], y: ridgePoint[3] }, + left: prevIndex, + right: nextIndex, + type: TYPES.RIDGE, + degree: 0, + }) + } } else { if (beforePrevLine.attributes.type === LINE_TYPE.WALLLINE.GABLE) { const beforeVector = { @@ -3983,14 +4027,24 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { endPoint = { x: point[0], y: point[1] } } } - linesAnalysis.push({ - start: startPoint, - end: endPoint, - left: baseLines.findIndex((line) => line === r.left), - right: baseLines.findIndex((line) => line === r.right), - type: TYPES.RIDGE, - degree: 0, - }) + const alreadyRidge = linesAnalysis.find( + (line) => + line.type === TYPES.RIDGE && + line.start.x === startPoint.x && + line.start.y === startPoint.y && + line.end.x === endPoint.x && + line.end.y === endPoint.y, + ) + if (!alreadyRidge) { + linesAnalysis.push({ + start: startPoint, + end: endPoint, + left: prevIndex, + right: nextIndex, + type: TYPES.RIDGE, + degree: 0, + }) + } } }) } @@ -4036,8 +4090,8 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { linesAnalysis.push({ start: startPoint, end: { x: correctPoint.x, y: correctPoint.y }, - left: baseLines.findIndex((line) => line === prevLine), - right: baseLines.findIndex((line) => line === nextLine), + left: prevIndex, + right: nextIndex, type: TYPES.GABLE_LINE, degree: getDegreeByChon(prevLine.attributes.pitch), gableId: baseLines.findIndex((line) => line === currentLine), @@ -4464,14 +4518,13 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { if (isOpposite) { const points = [currentLine.start.x, currentLine.start.y, partnerLine.start.x, partnerLine.start.y] const length = Math.sqrt((points[0] - points[2]) ** 2 + (points[1] - points[3]) ** 2) - - if (length > 0) { + const isDiagonal = Math.abs(points[0] - points[2]) >= 1 && Math.abs(points[1] - points[3]) >= 1 + if (length > EPSILON) { if (currentLine.type === TYPES.HIP) { innerLines.push(drawHipLine(points, canvas, roof, textMode, currentDegree, currentDegree)) - } else if (currentLine.type === TYPES.RIDGE) { + } else if (currentLine.type === TYPES.RIDGE && !isDiagonal) { innerLines.push(drawRidgeLine(points, canvas, roof, textMode)) } else if (currentLine.type === TYPES.NEW) { - const isDiagonal = Math.abs(points[0] - points[2]) >= 1 && Math.abs(points[1] - points[3]) >= 1 if (isDiagonal && almostEqual(Math.abs(points[0] - points[2]), Math.abs(points[1] - points[3]))) { innerLines.push(drawHipLine(points, canvas, roof, textMode, currentDegree, currentDegree)) } @@ -4496,11 +4549,12 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { const length = Math.sqrt((points[0].x - points[1].x) ** 2 + (points[0].y - points[1].y) ** 2) if (length < EPSILON) return const isDiagonal = Math.abs(points[0].x - points[1].x) >= 1 && Math.abs(points[0].y - points[1].y) >= 1 - if (isDiagonal) { + if (isDiagonal && almostEqual(Math.abs(points[0] - points[2]), Math.abs(points[1] - points[3]))) { innerLines.push( drawHipLine([points[0].x, points[0].y, points[1].x, points[1].y], canvas, roof, textMode, currentDegree, currentDegree), ) - } else { + } + if (!isDiagonal) { innerLines.push(drawRidgeLine([points[0].x, points[0].y, points[1].x, points[1].y], canvas, roof, textMode)) } proceedAnalysis.push(currentLine, partnerLine) @@ -4545,7 +4599,6 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { } }) } - //케라바에서 파생된 하단 지붕 라인처리 const downRoofGable = [] //처마에서 파생된 하단 지붕 라인 처리