From d8389c1d9f924aaad08ddb1b6ff0164caf56f1ed Mon Sep 17 00:00:00 2001 From: Jaeyoung Lee Date: Wed, 13 Aug 2025 11:27:46 +0900 Subject: [PATCH 01/13] =?UTF-8?q?=EC=A7=80=EB=B6=95=20=EB=8D=AE=EA=B0=9C?= =?UTF-8?q?=20=EC=9E=91=EB=8F=84=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=EC=8B=9C=EC=9E=91.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/fabric/QPolygon.js | 38 +++++++++++++++++------ src/components/floor-plan/CanvasFrame.jsx | 2 +- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index b50cdaa3..cbfbaf48 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -272,21 +272,33 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { this.lines.forEach((line) => types.push(line.attributes.type)) const gableType = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD] + const isEaves = types.every((type) => type === LINE_TYPE.WALLLINE.EAVES) + let isGable = false + if (types.includes(LINE_TYPE.WALLLINE.GABLE)) { + const gableOdd = types.filter((type, i) => i % 2 === 0) + const gableEven = types.filter((type, i) => i % 2 === 1) + if ( + (gableOdd.every((type) => type === LINE_TYPE.WALLLINE.EAVES) && gableEven.every((type) => gableType.includes(type))) || + (gableEven.every((type) => type === LINE_TYPE.WALLLINE.EAVES) && gableOdd.every((type) => gableType.includes(type))) + ) { + isGable = true + } + } const hasShed = types.includes(LINE_TYPE.WALLLINE.SHED) - - if (hasShed) { + let isShed = false + if (types.includes(LINE_TYPE.WALLLINE.SHED)) { const sheds = this.lines.filter((line) => line.attributes !== undefined && line.attributes.type === LINE_TYPE.WALLLINE.SHED) const areLinesParallel = function (line1, line2) { const angle1 = calculateAngle(line1.startPoint, line1.endPoint) const angle2 = calculateAngle(line2.startPoint, line2.endPoint) return angle1 === angle2 } - let isShedRoof = true sheds.forEach((shed, i) => { isShedRoof = areLinesParallel(shed, sheds[(i + 1) % sheds.length]) }) + if (isShedRoof) { const eaves = this.lines .filter((line) => line.attributes !== undefined && line.attributes.type === LINE_TYPE.WALLLINE.EAVES) @@ -301,17 +313,23 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { const gables = this.lines.filter((line) => sheds.includes(line) === false && eaves.includes(line) === false) const isGable = gables.every((line) => gableType.includes(line.attributes.type)) if (isGable) { - drawShedRoof(this.id, this.canvas, textMode) - } else { - drawRidgeRoof(this.id, this.canvas, textMode) + isShed = true } - } else { - drawRidgeRoof(this.id, this.canvas, textMode) } - } else { - drawRidgeRoof(this.id, this.canvas, textMode) } + } + + if (isEaves) { + // 용마루 -- straight-skeleton + console.log('용마루 지붕') + } else if (isGable) { + // A형, B형 박공 지붕 + console.log('패턴 지붕') + } else if (isShed) { + console.log('한쪽흐름 지붕') + drawShedRoof(this.id, this.canvas, textMode) } else { + console.log('변별로 설정') drawRidgeRoof(this.id, this.canvas, textMode) } }, diff --git a/src/components/floor-plan/CanvasFrame.jsx b/src/components/floor-plan/CanvasFrame.jsx index 383c0ef9..7b196bed 100644 --- a/src/components/floor-plan/CanvasFrame.jsx +++ b/src/components/floor-plan/CanvasFrame.jsx @@ -68,7 +68,7 @@ export default function CanvasFrame() { canvas?.renderAll() // 캔버스를 다시 그립니다. if (canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE).length > 0) { setSelectedMenu('module') - } else if (canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL).length > 0) { + } else if (canvas.getObjects().length === 0 || canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL).length > 0) { setSelectedMenu('outline') } else { setSelectedMenu('surface') -- 2.47.2 From c10a46e86faa4dfe9679a8f1e7668523872d7ff6 Mon Sep 17 00:00:00 2001 From: Jaeyoung Lee Date: Wed, 13 Aug 2025 11:29:54 +0900 Subject: [PATCH 02/13] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/fabric/QPolygon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index cbfbaf48..7c52a0ad 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -285,7 +285,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { isGable = true } } - const hasShed = types.includes(LINE_TYPE.WALLLINE.SHED) + let isShed = false if (types.includes(LINE_TYPE.WALLLINE.SHED)) { const sheds = this.lines.filter((line) => line.attributes !== undefined && line.attributes.type === LINE_TYPE.WALLLINE.SHED) -- 2.47.2 From 7175cd0485e8db19b250982a6eec846d91a478e2 Mon Sep 17 00:00:00 2001 From: Jaeyoung Lee Date: Wed, 13 Aug 2025 13:01:43 +0900 Subject: [PATCH 03/13] =?UTF-8?q?=EC=A7=80=EB=B6=95=20=EB=AA=A8=EC=96=91?= =?UTF-8?q?=20=ED=8C=8C=EC=95=85=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/fabric/QPolygon.js | 96 ++++++++++++++++--------------- 1 file changed, 51 insertions(+), 45 deletions(-) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 7c52a0ad..0022b820 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -268,64 +268,70 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { break } - const types = [] - this.lines.forEach((line) => types.push(line.attributes.type)) + const types = this.lines.map((line) => line.attributes.type) - const gableType = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD] - const isEaves = types.every((type) => type === LINE_TYPE.WALLLINE.EAVES) - - let isGable = false - if (types.includes(LINE_TYPE.WALLLINE.GABLE)) { - const gableOdd = types.filter((type, i) => i % 2 === 0) - const gableEven = types.filter((type, i) => i % 2 === 1) - if ( - (gableOdd.every((type) => type === LINE_TYPE.WALLLINE.EAVES) && gableEven.every((type) => gableType.includes(type))) || - (gableEven.every((type) => type === LINE_TYPE.WALLLINE.EAVES) && gableOdd.every((type) => gableType.includes(type))) - ) { - isGable = true + const isGableRoof = function (types) { + if (!types.includes(LINE_TYPE.WALLLINE.GABLE)) { + return false } + const gableTypes = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD] + const oddTypes = types.filter((type, i) => i % 2 === 0) + const evenTypes = types.filter((type, i) => i % 2 === 1) + + const oddAllEaves = oddTypes.every((type) => type === LINE_TYPE.WALLLINE.EAVES) + const evenAllGable = evenTypes.every((type) => gableTypes.includes(type)) + const evenAllEaves = evenTypes.every((type) => type === LINE_TYPE.WALLLINE.EAVES) + const oddAllGable = oddTypes.every((type) => gableTypes.includes(type)) + + return (oddAllEaves && evenAllGable) || (evenAllEaves && oddAllGable) } - let isShed = false - if (types.includes(LINE_TYPE.WALLLINE.SHED)) { - const sheds = this.lines.filter((line) => line.attributes !== undefined && line.attributes.type === LINE_TYPE.WALLLINE.SHED) - const areLinesParallel = function (line1, line2) { - const angle1 = calculateAngle(line1.startPoint, line1.endPoint) - const angle2 = calculateAngle(line2.startPoint, line2.endPoint) - return angle1 === angle2 + const isShedRoof = function (types, lines) { + const gableTypes = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD] + if (!types.includes(LINE_TYPE.WALLLINE.SHED)) { + return false + } + const shedLines = lines.filter((line) => line.attributes?.type === LINE_TYPE.WALLLINE.SHED) + const areShedLinesParallel = function (shedLines) { + return shedLines.every((shed, i) => { + const nextShed = shedLines[(i + 1) % shedLines.length] + const angle1 = calculateAngle(shed.startPoint, shed.endPoint) + const angle2 = calculateAngle(nextShed.startPoint, nextShed.endPoint) + return angle1 === angle2 + }) + } + if (!areShedLinesParallel(shedLines)) { + return false } - let isShedRoof = true - sheds.forEach((shed, i) => { - isShedRoof = areLinesParallel(shed, sheds[(i + 1) % sheds.length]) - }) - if (isShedRoof) { - const eaves = this.lines - .filter((line) => line.attributes !== undefined && line.attributes.type === LINE_TYPE.WALLLINE.EAVES) - .filter((line) => { - const angle1 = calculateAngle(sheds[0].startPoint, sheds[0].endPoint) - const angle2 = calculateAngle(line.startPoint, line.endPoint) - if (Math.abs(angle1 - angle2) === 180) { - return line - } - }) - if (eaves.length > 0) { - const gables = this.lines.filter((line) => sheds.includes(line) === false && eaves.includes(line) === false) - const isGable = gables.every((line) => gableType.includes(line.attributes.type)) - if (isGable) { - isShed = true - } - } + const getParallelEavesLines = function (shedLines, lines) { + const eavesLines = lines.filter((line) => line.attributes?.type === LINE_TYPE.WALLLINE.EAVES) + + const referenceAngle = calculateAngle(shedLines[0].startPoint, shedLines[0].endPoint) + + return eavesLines.filter((line) => { + const eavesAngle = calculateAngle(line.startPoint, line.endPoint) + return Math.abs(referenceAngle - eavesAngle) === 180 + }) } + + const parallelEaves = getParallelEavesLines(shedLines, lines) + if (parallelEaves.length === 0) { + return false + } + + const remainingLines = lines.filter((line) => !shedLines.includes(line) && !parallelEaves.includes(line)) + return remainingLines.every((line) => gableTypes.includes(line.attributes.type)) } - if (isEaves) { + if (types.every((type) => type === LINE_TYPE.WALLLINE.EAVES)) { // 용마루 -- straight-skeleton console.log('용마루 지붕') - } else if (isGable) { + } else if (isGableRoof(types)) { // A형, B형 박공 지붕 console.log('패턴 지붕') - } else if (isShed) { + isGableRoof(types) + } else if (isShedRoof(types, this.lines)) { console.log('한쪽흐름 지붕') drawShedRoof(this.id, this.canvas, textMode) } else { -- 2.47.2 From 8c153430cda494297eca21edafc9a5056c8efbf3 Mon Sep 17 00:00:00 2001 From: Jaeyoung Lee Date: Wed, 13 Aug 2025 13:23:29 +0900 Subject: [PATCH 04/13] =?UTF-8?q?=EC=9A=A9=EB=A7=88=EB=A3=A8,=20=EB=B0=95?= =?UTF-8?q?=EA=B3=B5=20=EC=A7=80=EB=B6=95=20=EC=B6=94=EA=B0=80.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/fabric/QPolygon.js | 1 - src/util/qpolygon-utils.js | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 0022b820..c7628088 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -330,7 +330,6 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { } else if (isGableRoof(types)) { // A형, B형 박공 지붕 console.log('패턴 지붕') - isGableRoof(types) } else if (isShedRoof(types, this.lines)) { console.log('한쪽흐름 지붕') drawShedRoof(this.id, this.canvas, textMode) diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 482693dc..ba451009 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -489,6 +489,26 @@ export const isSamePoint = (a, b) => { return Math.abs(Math.round(a.x) - Math.round(b.x)) <= 2 && Math.abs(Math.round(a.y) - Math.round(b.y)) <= 2 } +/** + * 용마루 지붕 + * @param roofId + * @param canvas + * @param textMode + */ +export const drawEavesRoof = (roofId, canvas, textMode) => { + +} + +/** + * 박공지붕(A,B 패턴) + * @param roofId + * @param canvas + * @param textMode + */ +export const drawGableRoof = (roofId, canvas, textMode) => { + +} + /** * 한쪽흐름 지붕 * @param roofId -- 2.47.2 From e35cacf5200f3a1db7ead244ef0c602ebc0d6cbb Mon Sep 17 00:00:00 2001 From: Jaeyoung Lee Date: Wed, 27 Aug 2025 10:02:53 +0900 Subject: [PATCH 05/13] =?UTF-8?q?a,b=20=ED=8C=A8=ED=84=B4=20=EC=A7=80?= =?UTF-8?q?=EB=B6=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 용마루 생성 끝, 지붕면 생성 중 --- src/components/fabric/QPolygon.js | 3 +- src/util/qpolygon-utils.js | 575 +++++++++++++++++++++++++++++- 2 files changed, 574 insertions(+), 4 deletions(-) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index c7628088..c049a541 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -2,7 +2,7 @@ import { fabric } from 'fabric' import { v4 as uuidv4 } from 'uuid' import { QLine } from '@/components/fabric/QLine' import { distanceBetweenPoints, findTopTwoIndexesByDistance, getDirectionByPoint, sortedPointLessEightPoint, sortedPoints } from '@/util/canvas-util' -import { calculateAngle, drawRidgeRoof, drawShedRoof, toGeoJSON } from '@/util/qpolygon-utils' +import { calculateAngle, drawGableRoof, drawRidgeRoof, drawShedRoof, toGeoJSON } from '@/util/qpolygon-utils' import * as turf from '@turf/turf' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' @@ -330,6 +330,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { } else if (isGableRoof(types)) { // A형, B형 박공 지붕 console.log('패턴 지붕') + drawGableRoof(this.id, this.canvas, textMode) } else if (isShedRoof(types, this.lines)) { console.log('한쪽흐름 지붕') drawShedRoof(this.id, this.canvas, textMode) diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index ba451009..816f4447 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -495,9 +495,7 @@ export const isSamePoint = (a, b) => { * @param canvas * @param textMode */ -export const drawEavesRoof = (roofId, canvas, textMode) => { - -} +export const drawEavesRoof = (roofId, canvas, textMode) => {} /** * 박공지붕(A,B 패턴) @@ -506,7 +504,578 @@ export const drawEavesRoof = (roofId, canvas, textMode) => { * @param textMode */ export const drawGableRoof = (roofId, canvas, textMode) => { + const roof = canvas?.getObjects().find((object) => object.id === roofId) + 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 baseLinePoints = baseLines.map((line) => ({ x: line.x1, y: line.y1 })) + + /** baseLine을 기준으로 확인용 polygon 작성 */ + const checkWallPolygon = new QPolygon(baseLinePoints, {}) + + const eavesLines = baseLines.filter((line) => line.attributes.type === LINE_TYPE.WALLLINE.EAVES) + const gableLines = baseLines.filter((line) => line.attributes.type === LINE_TYPE.WALLLINE.GABLE) + + const ridgeLines = [] + const innerLines = [] + + /** + * 처마 라인 속성 판단 + * @param line + * @returns {{startPoint: {x, y}, endPoint: {x, y}, length: number, angleDegree: number, normalizedAngle: number, isHorizontal: boolean, isVertical: boolean, isDiagonal: boolean, directionVector: {x: number, y: number}, roofLine}} + */ + const analyzeEavesLine = (line) => { + const tolerance = 1 + const dx = Big(line.x2).minus(Big(line.x1)).toNumber() + const dy = Big(line.y2).minus(Big(line.y1)).toNumber() + const length = Math.sqrt(dx * dx + dy * dy) + const angleDegree = (Math.atan2(dy, dx) * 180) / Math.PI + const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 + const directionVector = { x: dx / length, y: dy / length } + let isHorizontal = false, + isVertical = false, + isDiagonal = false + if (normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance) { + isHorizontal = true + } else if (Math.abs(normalizedAngle - 90) <= tolerance) { + isVertical = true + } else { + isDiagonal = true + } + + const originPoint = line.attributes.originPoint + const midX = (originPoint.x1 + originPoint.x2) / 2 + const midY = (originPoint.y1 + originPoint.y2) / 2 + const offset = line.attributes.offset + + const checkRoofLines = roof.lines.filter((roof) => { + const roofDx = Big(roof.x2).minus(Big(roof.x1)).toNumber() + const roofDy = Big(roof.y2).minus(Big(roof.y1)).toNumber() + const roofLength = Math.sqrt(roofDx * roofDx + roofDy * roofDy) + const roofVector = { x: roofDx / roofLength, y: roofDy / roofLength } + return directionVector.x === roofVector.x && directionVector.y === roofVector.y + }) + + let roofVector = { x: 0, y: 0 } + if (isHorizontal) { + const checkPoint = { x: midX, y: midY + offset } + if (wall.inPolygon(checkPoint)) { + roofVector = { x: 0, y: -1 } + } else { + roofVector = { x: 0, y: 1 } + } + } + if (isVertical) { + const checkPoint = { x: midX + offset, y: midY } + if (wall.inPolygon(checkPoint)) { + roofVector = { x: -1, y: 0 } + } else { + 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)).toNumber() + const edgeDy = 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)).toNumber() + const intersectDy = 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 = intersectRoofLines.find((roof) => isPointOnLineNew(roof.roofLine, roof.intersect)) + if (!currentRoof) { + currentRoof = intersectRoofLines.sort((a, b) => a.length - b.length)[0] + } + + let startPoint, endPoint + if (isHorizontal) { + startPoint = { x: Math.min(line.x1, line.x2, currentRoof.roofLine.x1, currentRoof.roofLine.x2), y: line.y1 } + endPoint = { x: Math.max(line.x1, line.x2, currentRoof.roofLine.x1, currentRoof.roofLine.x2), y: line.y2 } + } + if (isVertical) { + startPoint = { x: line.x1, y: Math.min(line.y1, line.y2, currentRoof.roofLine.y1, currentRoof.roofLine.y2) } + endPoint = { x: line.x2, y: Math.max(line.y1, line.y2, currentRoof.roofLine.y1, currentRoof.roofLine.y2) } + } + if (isDiagonal) { + startPoint = { x: line.x1, y: line.y1 } + endPoint = { x: line.x2, y: line.y2 } + } + + return { + startPoint, + endPoint, + length, + angleDegree, + normalizedAngle, + isHorizontal, + isVertical, + isDiagonal, + directionVector: { x: dx / length, y: dy / length }, + roofLine: currentRoof.roofLine, + } + } + + const isOverlapLine = (currAnalyze, checkAnalyze) => { + // 허용 오차 + const tolerance = 1 + + // 같은 방향인지 확인 + if ( + currAnalyze.isHorizontal !== checkAnalyze.isHorizontal || + currAnalyze.isVertical !== checkAnalyze.isVertical || + currAnalyze.isDiagonal !== checkAnalyze.isDiagonal + ) { + return false + } + + if (currAnalyze.isHorizontal && !(Math.abs(currAnalyze.startPoint.y - checkAnalyze.startPoint.y) < tolerance)) { + // 수평선: y좌표가 다른경우 false + return false + } else if (currAnalyze.isVertical && !(Math.abs(currAnalyze.startPoint.x - checkAnalyze.startPoint.x) < tolerance)) { + // 수직선: x좌표가 다른경우 false + return false + } + + // 3. 선분 구간 겹침 확인 + let range1, range2 + + if (currAnalyze.isHorizontal) { + // 수평선: x 범위로 비교 + range1 = { + min: Math.min(currAnalyze.startPoint.x, currAnalyze.endPoint.x), + max: Math.max(currAnalyze.startPoint.x, currAnalyze.endPoint.x), + } + range2 = { + min: Math.min(checkAnalyze.startPoint.x, checkAnalyze.endPoint.x), + max: Math.max(checkAnalyze.startPoint.x, checkAnalyze.endPoint.x), + } + } else { + // 수직선: y 범위로 비교 + range1 = { + min: Math.min(currAnalyze.startPoint.y, currAnalyze.endPoint.y), + max: Math.max(currAnalyze.startPoint.y, currAnalyze.endPoint.y), + } + range2 = { + min: Math.min(checkAnalyze.startPoint.y, checkAnalyze.endPoint.y), + max: Math.max(checkAnalyze.startPoint.y, checkAnalyze.endPoint.y), + } + } + // 구간 겹침 확인 + const overlapStart = Math.max(range1.min, range2.min) + const overlapEnd = Math.min(range1.max, range2.max) + + return Math.max(0, overlapEnd - overlapStart) > 0 + } + + /** + * 전체 처마 라인의 속성 확인 및 정리 + * @param lines + * @returns {{forwardLines: Array, backwardLines: Array}} + */ + const analyzeAllEavesLines = (lines) => { + let forwardLines = [] + let backwardLines = [] + lines.forEach((line) => { + const analyze = analyzeEavesLine(line) + if (analyze.isHorizontal) { + if (analyze.directionVector.x > 0) { + const overlapLines = forwardLines.filter((forwardLine) => forwardLine !== line && isOverlapLine(analyze, forwardLine.analyze)) + if (overlapLines.length > 0) { + overlapLines.forEach((overlap) => { + const overlapAnalyze = overlap.analyze + const startPoint = { + x: Math.min(analyze.startPoint.x, analyze.endPoint.x, overlapAnalyze.startPoint.x, overlapAnalyze.endPoint.x), + y: analyze.startPoint.y, + } + const endPoint = { + x: Math.max(analyze.startPoint.x, analyze.endPoint.x, overlapAnalyze.startPoint.x, overlapAnalyze.endPoint.x), + y: analyze.endPoint.y, + } + analyze.startPoint = startPoint + analyze.endPoint = endPoint + forwardLines = forwardLines.filter((forwardLine) => forwardLine !== overlap) + }) + } + forwardLines.push({ eaves: line, analyze }) + } + if (analyze.directionVector.x < 0) { + const overlapLines = backwardLines.filter((backwardLine) => backwardLine !== line && isOverlapLine(analyze, backwardLine.analyze)) + if (overlapLines.length > 0) { + overlapLines.forEach((overlap) => { + const overlapAnalyze = overlap.analyze + const startPoint = { + x: Math.min(analyze.startPoint.x, analyze.endPoint.x, overlapAnalyze.startPoint.x, overlapAnalyze.endPoint.x), + y: analyze.startPoint.y, + } + const endPoint = { + x: Math.max(analyze.startPoint.x, analyze.endPoint.x, overlapAnalyze.startPoint.x, overlapAnalyze.endPoint.x), + y: analyze.endPoint.y, + } + analyze.startPoint = startPoint + analyze.endPoint = endPoint + backwardLines = backwardLines.filter((backwardLine) => backwardLine !== overlap) + }) + } + backwardLines.push({ eaves: line, analyze }) + } + } + if (analyze.isVertical) { + if (analyze.directionVector.y > 0) { + const overlapLines = forwardLines.filter((forwardLine) => forwardLine !== line && isOverlapLine(analyze, forwardLine.analyze)) + if (overlapLines.length > 0) { + overlapLines.forEach((overlap) => { + const overlapAnalyze = overlap.analyze + const startPoint = { + x: analyze.startPoint.x, + y: Math.min(analyze.startPoint.y, analyze.endPoint.y, overlapAnalyze.startPoint.y, overlapAnalyze.endPoint.y), + } + const endPoint = { + x: analyze.endPoint.x, + y: Math.max(analyze.startPoint.y, analyze.endPoint.y, overlapAnalyze.startPoint.y, overlapAnalyze.endPoint.y), + } + analyze.startPoint = startPoint + analyze.endPoint = endPoint + forwardLines = forwardLines.filter((forwardLine) => forwardLine !== overlap) + }) + } + forwardLines.push({ eaves: line, analyze }) + } + if (analyze.directionVector.y < 0) { + const overlapLines = backwardLines.filter((backwardLine) => backwardLine !== line && isOverlapLine(analyze, backwardLine.analyze)) + if (overlapLines.length > 0) { + overlapLines.forEach((overlap) => { + const overlapAnalyze = overlap.analyze + const startPoint = { + x: analyze.startPoint.x, + y: Math.min(analyze.startPoint.y, analyze.endPoint.y, overlapAnalyze.startPoint.y, overlapAnalyze.endPoint.y), + } + const endPoint = { + x: analyze.endPoint.x, + y: Math.max(analyze.startPoint.y, analyze.endPoint.y, overlapAnalyze.startPoint.y, overlapAnalyze.endPoint.y), + } + analyze.startPoint = startPoint + analyze.endPoint = endPoint + backwardLines = backwardLines.filter((backwardLine) => backwardLine !== overlap) + }) + } + backwardLines.push({ eaves: line, analyze }) + } + } + }) + + forwardLines.sort((a, b) => { + if (a.analyze.isHorizontal && b.analyze.isHorizontal) { + return a.analyze.startPoint.x - b.analyze.startPoint.x + } else if (a.analyze.isVertical && b.analyze.isVertical) { + return a.analyze.startPoint.y - b.analyze.startPoint.y + } + }) + backwardLines.sort((a, b) => { + if (a.analyze.isHorizontal && b.analyze.isHorizontal) { + return a.analyze.startPoint.x - b.analyze.startPoint.x + } else if (a.analyze.isVertical && b.analyze.isVertical) { + return a.analyze.startPoint.y - b.analyze.startPoint.y + } + }) + + return { forwardLines, backwardLines } + } + + /** + * 라인의 지붕 면을 찾는다. + * @param currentLine + * @returns {*} + */ + const findCurrentRoof = (currentLine) => { + const analyze = analyzeEavesLine(currentLine) + const originPoint = currentLine.attributes.originPoint + const midX = (originPoint.x1 + originPoint.x2) / 2 + const midY = (originPoint.y1 + originPoint.y2) / 2 + const offset = currentLine.attributes.offset + + let currentRoof + let roofFindVector = { x: 0, y: 0 } + const checkRoofLines = roof.lines.filter((roof) => { + const dx = Big(roof.x2).minus(Big(roof.x1)).toNumber() + const dy = Big(roof.y2).minus(Big(roof.y1)).toNumber() + const length = Math.sqrt(dx * dx + dy * dy) + const directionVector = { x: dx / length, y: dy / length } + return analyze.directionVector.x === directionVector.x && analyze.directionVector.y === directionVector.y + }) + + if (analyze.isHorizontal) { + const checkPoint = { x: midX, y: midY + offset } + if (wall.inPolygon(checkPoint)) { + roofFindVector = { x: 0, y: -1 } + } else { + roofFindVector = { x: 0, y: 1 } + } + } + if (analyze.isVertical) { + const checkPoint = { x: midX + offset, y: midY } + if (wall.inPolygon(checkPoint)) { + roofFindVector = { x: -1, y: 0 } + } else { + roofFindVector = { x: 1, y: 0 } + } + } + const findEdge = { vertex1: { x: midX, y: midY }, vertex2: { x: midX + roofFindVector.x * offset, y: midY + roofFindVector.y * offset } } + const edgeDx = Big(findEdge.vertex2.x).minus(Big(findEdge.vertex1.x)).toNumber() + const edgeDy = 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)).toNumber() + const intersectDy = 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 }) + } + } + }) + + currentRoof = intersectRoofLines.find((roof) => isPointOnLineNew(roof.roofLine, roof.intersect)) + if (!currentRoof) { + currentRoof = intersectRoofLines.sort((a, b) => a.length - b.length)[0] + } + + return currentRoof + } + + /** + * 지점 사이의 길이와 지점별 각도에 따라서 교점까지의 길이를 구한다. + * @param distance + * @param angleA + * @param angleB + * @returns {{a: number, b: number}} + */ + const calculateIntersection = (distance, angleA, angleB) => { + // A에서 B방향으로의 각도 (0도가 B방향) + const tanA = Math.tan((angleA * Math.PI) / 180) + // B에서 A방향으로의 각도 (180도가 A방향, 실제 계산에서는 180-angleB) + const tanB = Math.tan(((180 - angleB) * Math.PI) / 180) + + const a = Math.round(((distance * tanB) / (tanB - tanA)) * 10) / 10 + const b = Math.round(Math.abs(distance - a) * 10) / 10 + return { a, b } + } + + const { forwardLines, backwardLines } = analyzeAllEavesLines(eavesLines) + forwardLines.forEach((current) => { + const currentLine = current.eaves + const analyze = current.analyze + const currentDegree = getDegreeByChon(currentLine.attributes.pitch) + const currentX1 = Math.min(currentLine.x1, currentLine.x2) + const currentX2 = Math.max(currentLine.x1, currentLine.x2) + const currentY1 = Math.min(currentLine.y1, currentLine.y2) + const currentY2 = Math.max(currentLine.y1, currentLine.y2) + + const x1 = analyze.startPoint.x + const x2 = analyze.endPoint.x + const y1 = analyze.startPoint.y + const y2 = analyze.endPoint.y + + const checkLine = new fabric.Line([analyze.startPoint.x, analyze.startPoint.y, analyze.endPoint.x, analyze.endPoint.y], { + stroke: 'red', + strokeWidth: 4, + parentId: roofId, + name: 'check', + }) + canvas.add(checkLine) + canvas.renderAll() + + const overlapLines = [] + backwardLines.forEach((backward) => { + const backX1 = Math.min(backward.eaves.x1, backward.eaves.x2) + const backX2 = Math.max(backward.eaves.x1, backward.eaves.x2) + const backY1 = Math.min(backward.eaves.y1, backward.eaves.y2) + const backY2 = Math.max(backward.eaves.y1, backward.eaves.y2) + + if ( + analyze.isHorizontal && + ((currentX1 <= backX1 && currentX2 >= backX1) || + (currentX1 <= backX2 && currentX2 >= backX2) || + (backX1 < currentX1 && backX1 < currentX2 && backX2 > currentX1 && backX2 > currentX2)) + ) { + overlapLines.push(backward) + } + if ( + analyze.isVertical && + ((currentY1 <= backY1 && currentY2 >= backY1) || + (currentY1 <= backY2 && currentY2 >= backY2) || + (backY1 < currentY1 && backY1 < currentY2 && backY2 > currentY1 && backY2 > currentY2)) + ) { + overlapLines.push(backward) + } + }) + + overlapLines.forEach((overlap) => { + const overlapAnalyze = overlap.analyze + const overlapDegree = getDegreeByChon(overlap.eaves.attributes.pitch) + const ridgePoint = [] + if (analyze.isHorizontal) { + const overlapX1 = Math.min(overlapAnalyze.startPoint.x, overlapAnalyze.endPoint.x) + const overlapX2 = Math.max(overlapAnalyze.startPoint.x, overlapAnalyze.endPoint.x) + + // 각 라인 사이의 길이를 구해서 각도에 대한 중간 지점으로 마루를 생성. + const currentMidY = (currentLine.y1 + currentLine.y2) / 2 + const overlapMidY = (overlap.eaves.y1 + overlap.eaves.y2) / 2 + const distance = calculateIntersection(Math.abs(currentMidY - overlapMidY), currentDegree, overlapDegree) + const vectorToOverlap = Math.sign(overlap.eaves.y1 - currentLine.y1) + const pointY = currentLine.y1 + vectorToOverlap * distance.a + + if (x1 <= overlapX1 && overlapX1 <= x2) { + ridgePoint.push({ x: overlapX1, y: pointY }) + } else { + ridgePoint.push({ x: x1, y: pointY }) + } + if (x1 <= overlapX2 && overlapX2 <= x2) { + ridgePoint.push({ x: overlapX2, y: pointY }) + } else { + ridgePoint.push({ x: x2, y: pointY }) + } + } + if (analyze.isVertical) { + const overlapY1 = Math.min(overlapAnalyze.startPoint.y, overlapAnalyze.endPoint.y) + const overlapY2 = (overlapAnalyze.startPoint.y, overlapAnalyze.endPoint.y) + + // 각 라인 사이의 길이를 구해서 각도에 대한 중간 지점으로 마루를 생성. + const currentMidX = (currentLine.x1 + currentLine.x2) / 2 + const overlapMidX = (overlap.eaves.x1 + overlap.eaves.x2) / 2 + const distance = calculateIntersection(Math.abs(currentMidX - overlapMidX), currentDegree, overlapDegree) + const vectorToOverlap = Math.sign(overlap.eaves.x1 - currentLine.x1) + const pointX = currentLine.x1 + vectorToOverlap * distance.a + + if (y1 <= overlapY1 && overlapY1 <= y2) { + ridgePoint.push({ x: pointX, y: overlapY1 }) + } else { + ridgePoint.push({ x: pointX, y: y1 }) + } + if (y1 <= overlapY2 && overlapY2 <= y2) { + ridgePoint.push({ x: pointX, y: overlapY2 }) + } else { + ridgePoint.push({ x: pointX, y: y2 }) + } + } + ridgeLines.push(drawRidgeLine([ridgePoint[0].x, ridgePoint[0].y, ridgePoint[1].x, ridgePoint[1].y], canvas, roof, textMode)) + }) + + canvas + .getObjects() + .filter((object) => object.name === 'check') + .forEach((object) => canvas.remove(object)) + canvas.renderAll() + }) + + /** + * 지붕면을 그린다. + * @param currentLine + */ + const drawRoofPlane = (currentLine) => { + const analyze = currentLine.analyze + const checkLine = new fabric.Line([analyze.startPoint.x, analyze.startPoint.y, analyze.endPoint.x, analyze.endPoint.y], { + stroke: 'red', + strokeWidth: 4, + parentId: roofId, + name: 'check', + }) + canvas.add(checkLine) + canvas.renderAll() + + const innerRidgeLines = [] + if (analyze.isHorizontal) { + ridgeLines + .filter((ridgeLine) => { + const tolerance = 1 + const dx = Big(ridgeLine.x2).minus(Big(ridgeLine.x1)).toNumber() + const dy = Big(ridgeLine.y2).minus(Big(ridgeLine.y1)).toNumber() + const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 + return normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance + }) + .filter((ridgeLine) => { + const minX = Math.min(analyze.startPoint.x, analyze.endPoint.x) + const maxX = Math.max(analyze.startPoint.x, analyze.endPoint.x) + const ridgeLineX = (ridgeLine.x1 + ridgeLine.x2) / 2 + return ridgeLineX >= minX && ridgeLineX <= maxX + }) + .forEach((ridgeLine) => innerRidgeLines.push(ridgeLine)) + } + if (analyze.isVertical) { + ridgeLines + .filter((ridgeLine) => { + const tolerance = 1 + const dx = Big(ridgeLine.x2).minus(Big(ridgeLine.x1)).toNumber() + const dy = Big(ridgeLine.y2).minus(Big(ridgeLine.y1)).toNumber() + const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 + return Math.abs(normalizedAngle - 90) <= tolerance + }) + .filter((ridgeLine) => { + const minY = Math.min(analyze.startPoint.y, analyze.endPoint.y) + const maxY = Math.max(analyze.startPoint.y, analyze.endPoint.y) + const ridgeLineY = (ridgeLine.y1 + ridgeLine.y2) / 2 + return ridgeLineY >= minY && ridgeLineY <= maxY + }) + .forEach((ridgeLine) => innerRidgeLines.push(ridgeLine)) + } + + innerRidgeLines.forEach((ridgeLine) => { + const checkLine = new fabric.Line([ridgeLine.x1, ridgeLine.y1, ridgeLine.x2, ridgeLine.y2], { + stroke: 'red', + strokeWidth: 4, + parentId: roofId, + name: 'check', + }) + canvas.add(checkLine) + canvas.renderAll() + }) + + if (innerRidgeLines.length === 1) { + const innerRidgeLine = innerRidgeLines[0] + if (analyze.isHorizontal) { + } + if (analyze.isVertical) { + } + } + + canvas + .getObjects() + .filter((object) => object.name === 'check') + .forEach((object) => canvas.remove(object)) + canvas.renderAll() + } + + forwardLines.forEach((forward) => { + const analyze = forward.analyze + drawRoofPlane(forward) + }) + backwardLines.forEach((backward) => { + const analyze = backward.analyze + drawRoofPlane(backward) + }) + canvas + .getObjects() + .filter((object) => object.name === 'check') + .forEach((object) => canvas.remove(object)) + canvas.renderAll() } /** -- 2.47.2 From 473f4f8d74f2717378209175dac846ea8e044285 Mon Sep 17 00:00:00 2001 From: Jaeyoung Lee Date: Wed, 10 Sep 2025 13:16:23 +0900 Subject: [PATCH 06/13] =?UTF-8?q?=EC=BC=80=EB=9D=BC=EB=B0=94=20=EC=A7=80?= =?UTF-8?q?=EB=B6=95=20=EC=9E=90=EB=8F=99=20=EC=84=A4=EC=A0=95=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/qpolygon-utils.js | 789 +++++++++++++++++++++++++++++-------- 1 file changed, 622 insertions(+), 167 deletions(-) diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 816f4447..bad9bd69 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -6,6 +6,7 @@ import { QPolygon } from '@/components/fabric/QPolygon' import * as turf from '@turf/turf' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' +import { canvas } from 'framer-motion/m' const TWO_PI = Math.PI * 2 @@ -510,11 +511,7 @@ export const drawGableRoof = (roofId, canvas, textMode) => { const baseLines = wall.baseLines.filter((line) => line.attributes.planeSize > 0) const baseLinePoints = baseLines.map((line) => ({ x: line.x1, y: line.y1 })) - /** baseLine을 기준으로 확인용 polygon 작성 */ - const checkWallPolygon = new QPolygon(baseLinePoints, {}) - const eavesLines = baseLines.filter((line) => line.attributes.type === LINE_TYPE.WALLLINE.EAVES) - const gableLines = baseLines.filter((line) => line.attributes.type === LINE_TYPE.WALLLINE.GABLE) const ridgeLines = [] const innerLines = [] @@ -625,6 +622,7 @@ export const drawGableRoof = (roofId, canvas, textMode) => { isDiagonal, directionVector: { x: dx / length, y: dy / length }, roofLine: currentRoof.roofLine, + roofVector, } } @@ -794,73 +792,6 @@ export const drawGableRoof = (roofId, canvas, textMode) => { return { forwardLines, backwardLines } } - /** - * 라인의 지붕 면을 찾는다. - * @param currentLine - * @returns {*} - */ - const findCurrentRoof = (currentLine) => { - const analyze = analyzeEavesLine(currentLine) - const originPoint = currentLine.attributes.originPoint - const midX = (originPoint.x1 + originPoint.x2) / 2 - const midY = (originPoint.y1 + originPoint.y2) / 2 - const offset = currentLine.attributes.offset - - let currentRoof - let roofFindVector = { x: 0, y: 0 } - const checkRoofLines = roof.lines.filter((roof) => { - const dx = Big(roof.x2).minus(Big(roof.x1)).toNumber() - const dy = Big(roof.y2).minus(Big(roof.y1)).toNumber() - const length = Math.sqrt(dx * dx + dy * dy) - const directionVector = { x: dx / length, y: dy / length } - return analyze.directionVector.x === directionVector.x && analyze.directionVector.y === directionVector.y - }) - - if (analyze.isHorizontal) { - const checkPoint = { x: midX, y: midY + offset } - if (wall.inPolygon(checkPoint)) { - roofFindVector = { x: 0, y: -1 } - } else { - roofFindVector = { x: 0, y: 1 } - } - } - if (analyze.isVertical) { - const checkPoint = { x: midX + offset, y: midY } - if (wall.inPolygon(checkPoint)) { - roofFindVector = { x: -1, y: 0 } - } else { - roofFindVector = { x: 1, y: 0 } - } - } - const findEdge = { vertex1: { x: midX, y: midY }, vertex2: { x: midX + roofFindVector.x * offset, y: midY + roofFindVector.y * offset } } - const edgeDx = Big(findEdge.vertex2.x).minus(Big(findEdge.vertex1.x)).toNumber() - const edgeDy = 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)).toNumber() - const intersectDy = 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 }) - } - } - }) - - currentRoof = intersectRoofLines.find((roof) => isPointOnLineNew(roof.roofLine, roof.intersect)) - if (!currentRoof) { - currentRoof = intersectRoofLines.sort((a, b) => a.length - b.length)[0] - } - - return currentRoof - } - /** * 지점 사이의 길이와 지점별 각도에 따라서 교점까지의 길이를 구한다. * @param distance @@ -879,7 +810,59 @@ export const drawGableRoof = (roofId, canvas, textMode) => { return { a, b } } + /** + * polygon에 line이 겹쳐지는 구간을 확인. + * @param polygon + * @param linePoints + * @returns + */ + const findPloygonLineOverlap = (polygon, linePoints) => { + const polygonPoints = polygon.points + const checkLine = { + x1: linePoints[0], + y1: linePoints[1], + x2: linePoints[2], + y2: linePoints[3], + } + let startPoint = { x: checkLine.x1, y: checkLine.y1 } + let endPoint = { x: checkLine.x2, y: checkLine.y2 } + const lineEdge = { vertex1: startPoint, vertex2: endPoint } + + const checkPolygon = new QPolygon(polygonPoints, {}) + + const isStartInside = checkPolygon.inPolygon(startPoint) + const isEndInside = checkPolygon.inPolygon(endPoint) + + const intersections = [] + + checkPolygon.lines.forEach((line) => { + const edge = { vertex1: { x: line.x1, y: line.y1 }, vertex2: { x: line.x2, y: line.y2 } } + const intersect = edgesIntersection(edge, lineEdge) + if ( + intersect && + isPointOnLineNew(line, intersect) && + isPointOnLineNew(checkLine, intersect) && + !(Math.abs(startPoint.x - intersect.x) < 1 && Math.abs(startPoint.y - intersect.y) < 1) && + !(Math.abs(endPoint.x - intersect.x) < 1 && Math.abs(endPoint.y - intersect.y) < 1) + ) { + intersections.push({ intersect, dist: Math.sqrt(Math.pow(startPoint.x - intersect.x, 2) + Math.pow(startPoint.y - intersect.y, 2)) }) + } + }) + + if (intersections.length > 0) { + intersections.sort((a, b) => a.dist - b.dist) + let i = 0 + if (!isStartInside) { + startPoint = { x: intersections[0].intersect.x, y: intersections[0].intersect.y } + i++ + } + endPoint = { x: intersections[i].intersect.x, y: intersections[i].intersect.y } + } + return [startPoint.x, startPoint.y, endPoint.x, endPoint.y] + } + const { forwardLines, backwardLines } = analyzeAllEavesLines(eavesLines) + forwardLines.forEach((current) => { const currentLine = current.eaves const analyze = current.analyze @@ -894,39 +877,62 @@ export const drawGableRoof = (roofId, canvas, textMode) => { const y1 = analyze.startPoint.y const y2 = analyze.endPoint.y - const checkLine = new fabric.Line([analyze.startPoint.x, analyze.startPoint.y, analyze.endPoint.x, analyze.endPoint.y], { - stroke: 'red', - strokeWidth: 4, - parentId: roofId, - name: 'check', - }) - canvas.add(checkLine) - canvas.renderAll() + 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) + } const overlapLines = [] - backwardLines.forEach((backward) => { + const findBackWardLines = backwardLines.filter((backward) => { + const backwardVector = { x: 0, y: 0 } + if (analyze.isHorizontal) { + backwardVector.x = Math.sign(analyze.roofLine.y1 - backward.analyze.startPoint.y) + } + if (analyze.isVertical) { + backwardVector.y = Math.sign(analyze.roofLine.x1 - backward.analyze.startPoint.x) + } + return backwardVector.x === lineVector.x && backwardVector.y === lineVector.y + }) + const backWardLine = findBackWardLines.find((backward) => { const backX1 = Math.min(backward.eaves.x1, backward.eaves.x2) const backX2 = Math.max(backward.eaves.x1, backward.eaves.x2) const backY1 = Math.min(backward.eaves.y1, backward.eaves.y2) const backY2 = Math.max(backward.eaves.y1, backward.eaves.y2) - - if ( - analyze.isHorizontal && - ((currentX1 <= backX1 && currentX2 >= backX1) || - (currentX1 <= backX2 && currentX2 >= backX2) || - (backX1 < currentX1 && backX1 < currentX2 && backX2 > currentX1 && backX2 > currentX2)) - ) { - overlapLines.push(backward) - } - if ( - analyze.isVertical && - ((currentY1 <= backY1 && currentY2 >= backY1) || - (currentY1 <= backY2 && currentY2 >= backY2) || - (backY1 < currentY1 && backY1 < currentY2 && backY2 > currentY1 && backY2 > currentY2)) - ) { - overlapLines.push(backward) - } + return ( + (analyze.isHorizontal && Math.abs(currentX1 - backX1) < 1 && Math.abs(currentX2 - backX2) < 1) || + (analyze.isVertical && Math.abs(currentY1 - backY1) < 1 && Math.abs(currentY2 - backY2) < 1) + ) }) + if (backWardLine) { + overlapLines.push(backWardLine) + } else { + findBackWardLines.forEach((backward) => { + const backX1 = Math.min(backward.eaves.x1, backward.eaves.x2) + const backX2 = Math.max(backward.eaves.x1, backward.eaves.x2) + const backY1 = Math.min(backward.eaves.y1, backward.eaves.y2) + const backY2 = Math.max(backward.eaves.y1, backward.eaves.y2) + + if ( + analyze.isHorizontal && + ((currentX1 <= backX1 && currentX2 >= backX1) || + (currentX1 <= backX2 && currentX2 >= backX2) || + (backX1 < currentX1 && backX1 < currentX2 && backX2 > currentX1 && backX2 > currentX2)) + ) { + overlapLines.push(backward) + } + if ( + analyze.isVertical && + ((currentY1 <= backY1 && currentY2 >= backY1) || + (currentY1 <= backY2 && currentY2 >= backY2) || + (backY1 < currentY1 && backY1 < currentY2 && backY2 > currentY1 && backY2 > currentY2)) + ) { + overlapLines.push(backward) + } + }) + } overlapLines.forEach((overlap) => { const overlapAnalyze = overlap.analyze @@ -976,7 +982,8 @@ export const drawGableRoof = (roofId, canvas, textMode) => { ridgePoint.push({ x: pointX, y: y2 }) } } - ridgeLines.push(drawRidgeLine([ridgePoint[0].x, ridgePoint[0].y, ridgePoint[1].x, ridgePoint[1].y], canvas, roof, textMode)) + const points = findPloygonLineOverlap(roof, [ridgePoint[0].x, ridgePoint[0].y, ridgePoint[1].x, ridgePoint[1].y], canvas, roofId) + ridgeLines.push(drawRidgeLine(points, canvas, roof, textMode)) }) canvas @@ -986,96 +993,408 @@ export const drawGableRoof = (roofId, canvas, textMode) => { canvas.renderAll() }) + /** + * currentLine {x1,y1}, {x2,y2} 좌표 안쪽에 있는 마루를 찾는다. + * @param currentLine + * @param roofVector + * @param lines + * @param tolerance + * @returns {*[]} + */ + const findInnerRidge = (currentLine, roofVector, lines, tolerance = 1) => { + const x1 = Math.min(currentLine.x1, currentLine.x2) + const y1 = Math.min(currentLine.y1, currentLine.y2) + const x2 = Math.max(currentLine.x1, currentLine.x2) + const y2 = Math.max(currentLine.y1, currentLine.y2) + const dx = Big(currentLine.x2).minus(Big(currentLine.x1)).toNumber() + const dy = Big(currentLine.y2).minus(Big(currentLine.y1)).toNumber() + const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 + let isHorizontal = false, + isVertical = false + if (normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance) { + isHorizontal = true + } else if (Math.abs(normalizedAngle - 90) <= tolerance) { + isVertical = true + } + let innerRidgeLines = [] + if (isHorizontal) { + lines + .filter((line) => { + const dx = Big(line.x2).minus(Big(line.x1)).toNumber() + const dy = Big(line.y2).minus(Big(line.y1)).toNumber() + const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 + return normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance + }) + .filter((line) => { + const minX = Math.min(line.x1, line.x2) + const maxX = Math.max(line.x1, line.x2) + return x1 <= minX && maxX <= x2 && roofVector.y * -1 === Math.sign(line.y1 - y1) + }) + .forEach((line) => innerRidgeLines.push({ line, dist: Math.abs(currentLine.y1 - line.y1) })) + } + if (isVertical) { + lines + .filter((line) => { + const dx = Big(line.x2).minus(Big(line.x1)).toNumber() + const dy = Big(line.y2).minus(Big(line.y1)).toNumber() + const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 + return Math.abs(normalizedAngle - 90) <= tolerance + }) + .filter((line) => { + const minY = Math.min(line.y1, line.y2) + const maxY = Math.max(line.y1, line.y2) + return y1 <= minY && maxY <= y2 && roofVector.x * -1 === Math.sign(line.x1 - x1) + }) + .forEach((line) => innerRidgeLines.push({ line, dist: Math.abs(currentLine.x1 - line.x1) })) + } + innerRidgeLines.sort((a, b) => a.dist - b.dist) + + const ridge1 = innerRidgeLines.find((ridge) => { + if (isHorizontal) { + const minX = Math.min(ridge.line.x1, ridge.line.x2) + return Math.abs(x1 - minX) <= 1 + } + if (isVertical) { + const minY = Math.min(ridge.line.y1, ridge.line.y2) + return Math.abs(y1 - minY) <= 1 + } + }) + + const ridge2 = innerRidgeLines.find((ridge) => { + if (isHorizontal) { + const maxX = Math.max(ridge.line.x1, ridge.line.x2) + return Math.abs(x2 - maxX) <= 1 + } + if (isVertical) { + const maxY = Math.max(ridge.line.y1, ridge.line.y2) + return Math.abs(y2 - maxY) <= 1 + } + }) + if (ridge1 === ridge2) { + innerRidgeLines = [ridge1] + } else { + if (ridge1 && ridge2) { + let range1, range2 + if (isHorizontal) { + // 수평선: x 범위로 비교 + range1 = { + min: Math.min(ridge1.line.x1, ridge1.line.x2), + max: Math.max(ridge1.line.x1, ridge1.line.x2), + } + range2 = { + min: Math.min(ridge2.line.x1, ridge2.line.x2), + max: Math.max(ridge2.line.x1, ridge2.line.x2), + } + } else { + // 수직선: y 범위로 비교 + range1 = { + min: Math.min(ridge1.line.y1, ridge1.line.y2), + max: Math.max(ridge1.line.y1, ridge1.line.y2), + } + range2 = { + min: Math.min(ridge2.line.y1, ridge2.line.y2), + max: Math.max(ridge2.line.y1, ridge2.line.y2), + } + } + // 구간 겹침 확인 + const overlapStart = Math.max(range1.min, range2.min) + const overlapEnd = Math.min(range1.max, range2.max) + if (Math.max(0, overlapEnd - overlapStart) > 0) { + innerRidgeLines = [ridge1, ridge2] + } + } + } + return innerRidgeLines + } + /** * 지붕면을 그린다. * @param currentLine */ const drawRoofPlane = (currentLine) => { + const currentDegree = getDegreeByChon(currentLine.eaves.attributes.pitch) const analyze = currentLine.analyze - const checkLine = new fabric.Line([analyze.startPoint.x, analyze.startPoint.y, analyze.endPoint.x, analyze.endPoint.y], { - stroke: 'red', - strokeWidth: 4, - parentId: roofId, - name: 'check', - }) - canvas.add(checkLine) - canvas.renderAll() - const innerRidgeLines = [] - if (analyze.isHorizontal) { - ridgeLines - .filter((ridgeLine) => { - const tolerance = 1 - const dx = Big(ridgeLine.x2).minus(Big(ridgeLine.x1)).toNumber() - const dy = Big(ridgeLine.y2).minus(Big(ridgeLine.y1)).toNumber() - const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 - return normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance - }) - .filter((ridgeLine) => { - const minX = Math.min(analyze.startPoint.x, analyze.endPoint.x) - const maxX = Math.max(analyze.startPoint.x, analyze.endPoint.x) - const ridgeLineX = (ridgeLine.x1 + ridgeLine.x2) / 2 - return ridgeLineX >= minX && ridgeLineX <= maxX - }) - .forEach((ridgeLine) => innerRidgeLines.push(ridgeLine)) - } - if (analyze.isVertical) { - ridgeLines - .filter((ridgeLine) => { - const tolerance = 1 - const dx = Big(ridgeLine.x2).minus(Big(ridgeLine.x1)).toNumber() - const dy = Big(ridgeLine.y2).minus(Big(ridgeLine.y1)).toNumber() - const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 - return Math.abs(normalizedAngle - 90) <= tolerance - }) - .filter((ridgeLine) => { - const minY = Math.min(analyze.startPoint.y, analyze.endPoint.y) - const maxY = Math.max(analyze.startPoint.y, analyze.endPoint.y) - const ridgeLineY = (ridgeLine.y1 + ridgeLine.y2) / 2 - return ridgeLineY >= minY && ridgeLineY <= maxY - }) - .forEach((ridgeLine) => innerRidgeLines.push(ridgeLine)) - } + // 현재라인 안쪽의 마루를 filter하여 계산에 사용. + const innerRidgeLines = findInnerRidge( + { x1: analyze.startPoint.x, y1: analyze.startPoint.y, x2: analyze.endPoint.x, y2: analyze.endPoint.y }, + analyze.roofVector, + ridgeLines, + 1, + ) - innerRidgeLines.forEach((ridgeLine) => { - const checkLine = new fabric.Line([ridgeLine.x1, ridgeLine.y1, ridgeLine.x2, ridgeLine.y2], { - stroke: 'red', - strokeWidth: 4, - parentId: roofId, - name: 'check', + // 안쪽의 마루가 있는 경우 마루의 연결 포인트를 설정 + if (innerRidgeLines.length > 0) { + const innerRidgePoints = innerRidgeLines + .map((innerRidgeLine) => [ + { x: innerRidgeLine.line.x1, y: innerRidgeLine.line.y1 }, + { x: innerRidgeLine.line.x2, y: innerRidgeLine.line.y2 }, + ]) + .flat() + const ridgeMinPoint = innerRidgePoints.reduce( + (min, curr) => (analyze.isHorizontal ? (curr.x < min.x ? curr : min) : curr.y < min.y ? curr : min), + innerRidgePoints[0], + ) + const ridgeMaxPoint = innerRidgePoints.reduce( + (max, curr) => (analyze.isHorizontal ? (curr.x > max.x ? curr : max) : curr.y > max.y ? curr : max), + innerRidgePoints[0], + ) + + const roofPlanePoint = [] + innerRidgeLines + .sort((a, b) => { + const line1 = a.line + const line2 = b.line + if (analyze.isHorizontal) { + return line1.x1 - line2.x1 + } + if (analyze.isVertical) { + return line1.y1 - line2.y1 + } + }) + .forEach((ridge, index) => { + const isFirst = index === 0 + const isLast = index === innerRidgeLines.length - 1 + const line = ridge.line + if (isFirst) { + roofPlanePoint.push({ x: line.x1, y: line.y1 }) + } + const nextRidge = innerRidgeLines[index + 1] + if (nextRidge) { + const nextLine = nextRidge.line + let range1, range2 + if (analyze.isHorizontal) { + // 수평선: x 범위로 비교 + range1 = { + min: Math.min(line.x1, line.x2), + max: Math.max(line.x1, line.x2), + } + range2 = { + min: Math.min(nextLine.x1, nextLine.x2), + max: Math.max(nextLine.x1, nextLine.x2), + } + // 겹쳐지는 구간 + const overlapX = [Math.max(range1.min, range2.min), Math.min(range1.max, range2.max)] + let linePoints = [ + { x: line.x1, y: line.y1 }, + { x: line.x2, y: line.y2 }, + { x: nextLine.x1, y: nextLine.y1 }, + { x: nextLine.x2, y: nextLine.y2 }, + ].filter((point) => overlapX.includes(point.x)) + const firstPoint = + ridge.dist > nextRidge.dist + ? linePoints.find((point) => point.x === line.x1 || point.x === line.x2) + : linePoints.find((point) => point.x === nextLine.x1 || point.x === nextLine.x2) + const lastPoint = linePoints.find((point) => point !== firstPoint) + roofPlanePoint.push({ x: firstPoint.x, y: firstPoint.y }, { x: firstPoint.x, y: lastPoint.y }) + } else { + // 수직선: y 범위로 비교 + range1 = { + min: Math.min(line.y1, line.y2), + max: Math.max(line.y1, line.y2), + } + range2 = { + min: Math.min(nextLine.y1, nextLine.y2), + max: Math.max(nextLine.y1, nextLine.y2), + } + //겹쳐지는 구간 + const overlapY = [Math.max(range1.min, range2.min), Math.min(range1.max, range2.max)] + let linePoints = [ + { x: line.x1, y: line.y1 }, + { x: line.x2, y: line.y2 }, + { x: nextLine.x1, y: nextLine.y1 }, + { x: nextLine.x2, y: nextLine.y2 }, + ].filter((point) => overlapY.includes(point.y)) + const firstPoint = + ridge.dist > nextRidge.dist + ? linePoints.find((point) => point.y === line.y1 || point.y === line.y2) + : linePoints.find((point) => point.y === nextLine.y1 || point.y === nextLine.y2) + const lastPoint = linePoints.find((point) => point !== firstPoint) + roofPlanePoint.push({ x: firstPoint.x, y: firstPoint.y }, { x: lastPoint.x, y: firstPoint.y }) + } + } + if (isLast) { + roofPlanePoint.push({ x: line.x2, y: line.y2 }) + } + }) + const maxDistRidge = innerRidgeLines.reduce((max, curr) => (curr.dist > max.dist ? curr : max), innerRidgeLines[0]) + // 지붕선에 맞닫는 포인트를 찾아서 지붕선의 모양에 따라 추가 한다. + let innerRoofLines = roof.lines + .filter((line) => { + //1.지붕선이 현재 라인의 안쪽에 있는지 판단 + const tolerance = 1 + const dx = Big(line.x2).minus(Big(line.x1)).toNumber() + const dy = Big(line.y2).minus(Big(line.y1)).toNumber() + const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 + let isHorizontal = false, + isVertical = false + if (normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance) { + isHorizontal = true + } else if (Math.abs(normalizedAngle - 90) <= tolerance) { + isVertical = true + } + const minX = Math.min(line.x1, line.x2) + const maxX = Math.max(line.x1, line.x2) + const minY = Math.min(line.y1, line.y2) + const maxY = Math.max(line.y1, line.y2) + return ( + (analyze.isHorizontal && isHorizontal && analyze.startPoint.x <= minX && maxX <= analyze.endPoint.x) || + (analyze.isVertical && isVertical && analyze.startPoint.y <= minY && maxY <= analyze.endPoint.y) + ) + }) + .filter((line) => { + //2.지붕선이 현재 라인의 바깥에 있는지 확인. + const dx = Big(line.x2).minus(Big(line.x1)).toNumber() + const dy = Big(line.y2).minus(Big(line.y1)).toNumber() + const length = Math.sqrt(dx * dx + dy * dy) + const lineVector = { x: 0, y: 0 } + if (analyze.isHorizontal) { + // lineVector.y = Math.sign(line.y1 - currentLine.eaves.attributes.originPoint.y1) + lineVector.y = Math.sign(line.y1 - maxDistRidge.line.y1) + } else if (analyze.isVertical) { + // lineVector.x = Math.sign(line.x1 - currentLine.eaves.attributes.originPoint.x1) + lineVector.x = Math.sign(line.x1 - maxDistRidge.line.x1) + } + return ( + analyze.roofVector.x === lineVector.x && + analyze.roofVector.y === lineVector.y && + analyze.directionVector.x === dx / length && + analyze.directionVector.y === dy / length + ) + }) + + // 패턴 방향에 따라 최소지점, 최대지점의 포인트에서 지붕선 방향에 만나는 포인트를 찾기위한 vector + const checkMinVector = { + vertex1: { x: ridgeMinPoint.x, y: ridgeMinPoint.y }, + vertex2: { x: ridgeMinPoint.x + analyze.roofVector.x, y: ridgeMinPoint.y + analyze.roofVector.y }, + } + const checkMaxVector = { + vertex1: { x: ridgeMaxPoint.x, y: ridgeMaxPoint.y }, + vertex2: { x: ridgeMaxPoint.x + analyze.roofVector.x, y: ridgeMaxPoint.y + analyze.roofVector.y }, + } + + // 최소, 최대 지점에 만나는 포인트들에 대한 정보 + const roofMinPoint = [], + roofMaxPoint = [] + + innerRoofLines.forEach((line) => { + const lineVector = { vertex1: { x: line.x1, y: line.y1 }, vertex2: { x: line.x2, y: line.y2 } } + const minIntersect = edgesIntersection(checkMinVector, lineVector) + const maxIntersect = edgesIntersection(checkMaxVector, lineVector) + if (minIntersect) { + const distance = Math.abs( + analyze.isHorizontal ? ridgeMinPoint.x - Math.min(line.x1, line.x2) : ridgeMinPoint.y - Math.min(line.y1, line.y2), + ) + roofMinPoint.push({ x: minIntersect.x, y: minIntersect.y, isPointOnLine: isPointOnLineNew(line, minIntersect), distance, line }) + } + if (maxIntersect) { + const distance = Math.abs( + analyze.isHorizontal ? ridgeMaxPoint.x - Math.max(line.x1, line.x2) : ridgeMaxPoint.y - Math.max(line.y1, line.y2), + ) + roofMaxPoint.push({ x: maxIntersect.x, y: maxIntersect.y, isPointOnLine: isPointOnLineNew(line, maxIntersect), distance, line }) + } }) - canvas.add(checkLine) - canvas.renderAll() - }) - if (innerRidgeLines.length === 1) { - const innerRidgeLine = innerRidgeLines[0] - if (analyze.isHorizontal) { + // 최소지점, 최대지점에 연결되는 지붕선 + let minRoof = roofMinPoint.find((point) => point.isPointOnLine) + let maxRoof = roofMaxPoint.find((point) => point.isPointOnLine) + if (!minRoof) { + minRoof = roofMinPoint.sort((a, b) => a.distance - b.distance)[0] } - if (analyze.isVertical) { + if (!maxRoof) { + maxRoof = roofMaxPoint.sort((a, b) => a.distance - b.distance)[0] } + + if (minRoof && maxRoof) { + // 1. 연결되는 지점의 포인트를 사용한다. + roofPlanePoint.push({ x: minRoof.x, y: minRoof.y }, { x: maxRoof.x, y: maxRoof.y }) + // 2. 최소지점, 최대지점에 연결되는 지붕선이 하나가 아닐경우 연결되는 지점외에 지붕선의 다른 포인트를 추가 해야 한다. + if (minRoof.line !== maxRoof.line) { + if (analyze.isHorizontal) { + Math.abs(minRoof.x - minRoof.line.x1) < Math.abs(minRoof.x - minRoof.line.x2) + ? roofPlanePoint.push({ x: minRoof.line.x2, y: minRoof.line.y2 }) + : roofPlanePoint.push({ x: minRoof.line.x1, y: minRoof.line.y1 }) + Math.abs(maxRoof.x - maxRoof.line.x1) < Math.abs(maxRoof.x - maxRoof.line.x2) + ? roofPlanePoint.push({ x: maxRoof.line.x2, y: maxRoof.line.y2 }) + : roofPlanePoint.push({ x: maxRoof.line.x1, y: maxRoof.line.y1 }) + } + if (analyze.isVertical) { + Math.abs(minRoof.y - minRoof.line.y1) < Math.abs(minRoof.y - minRoof.line.y2) + ? roofPlanePoint.push({ x: minRoof.line.x2, y: minRoof.line.y2 }) + : roofPlanePoint.push({ x: minRoof.line.x1, y: minRoof.line.y1 }) + Math.abs(maxRoof.y - maxRoof.line.y1) < Math.abs(maxRoof.y - maxRoof.line.y2) + ? roofPlanePoint.push({ x: maxRoof.line.x2, y: maxRoof.line.y2 }) + : roofPlanePoint.push({ x: maxRoof.line.x1, y: maxRoof.line.y1 }) + } + // 3.지붕선이 세개 이상일 경우 최소, 최대 연결지점의 지붕선을 제외한 나머지 지붕선은 모든 포인트를 사용한다. + const otherRoof = innerRoofLines.filter((line) => line !== minRoof.line && line !== maxRoof.line) + otherRoof.forEach((line) => { + roofPlanePoint.push({ x: line.x1, y: line.y1 }, { x: line.x2, y: line.y2 }) + }) + } + } + + //각 포인트들을 직교하도록 정렬 + const sortedPoints = getSortedOrthogonalPoints(roofPlanePoint) + sortedPoints.forEach((currPoint, index) => { + const nextPoint = sortedPoints[(index + 1) % sortedPoints.length] + const points = [currPoint.x, currPoint.y, nextPoint.x, nextPoint.y] + const isAlready = ridgeLines.find( + (ridge) => + (Math.abs(ridge.x1 - points[0]) < 1 && + Math.abs(ridge.y1 - points[1]) < 1 && + Math.abs(ridge.x2 - points[2]) < 1 && + Math.abs(ridge.y2 - points[3]) < 1) || + (Math.abs(ridge.x1 - points[2]) < 1 && + Math.abs(ridge.y1 - points[3]) < 1 && + Math.abs(ridge.x2 - points[0]) < 1 && + Math.abs(ridge.y2 - points[1]) < 1), + ) + if (isAlready) { + return true + } + + const tolerance = 1 + const dx = Big(points[2]).minus(Big(points[0])).toNumber() + const dy = Big(points[3]).minus(Big(points[1])).toNumber() + const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180 + let isHorizontal = false, + isVertical = false + if (normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance) { + isHorizontal = true + } else if (Math.abs(normalizedAngle - 90) <= tolerance) { + isVertical = true + } + if (analyze.isHorizontal) { + //현재라인이 수평선일때 + if (isHorizontal) { + //같은방향 처리 + innerLines.push(drawRoofLine(points, canvas, roof, textMode)) + } else { + //다른방향 처리 + drawHipLine(points, canvas, roof, textMode, null, currentDegree, currentDegree) + } + } else if (analyze.isVertical) { + //현재라인이 수직선일때 + if (isVertical) { + //같은방향 처리 + drawRoofLine(points, canvas, roof, textMode) + } else { + //다른방향 처리 + drawHipLine(points, canvas, roof, textMode, null, currentDegree, currentDegree) + } + } + }) } - - canvas - .getObjects() - .filter((object) => object.name === 'check') - .forEach((object) => canvas.remove(object)) - canvas.renderAll() } forwardLines.forEach((forward) => { - const analyze = forward.analyze drawRoofPlane(forward) }) backwardLines.forEach((backward) => { - const analyze = backward.analyze drawRoofPlane(backward) }) - canvas - .getObjects() - .filter((object) => object.name === 'check') - .forEach((object) => canvas.remove(object)) - canvas.renderAll() + roof.innerLines.push(...ridgeLines, ...innerLines) } /** @@ -8919,3 +9238,139 @@ const reCalculateSize = (line) => { ) return { planeSize, actualSize } } + +/** + * 직교 다각형(축에 평행한 변들로만 구성된 다각형)의 점들을 시계방향으로 정렬 + * @param points 정렬할 점들의 배열 + * @returns {[sortedPoints]} 정렬된 점들의 배열 + */ +const getSortedOrthogonalPoints = (points) => { + if (points.length < 3) return points + /** + * @param currentDirection 현재 방향 + * @param nextDirection 다음 방향 + * @param isClockWise 흐름 방향 + * @returns {boolean} 유효한 방향 전환인지 여부 + */ + const isValidNextDirection = (currentDirection, nextDirection, isClockWise = false) => { + if (!currentDirection) return true + + let validTransitions + if (!isClockWise) { + // 반 시계방향 진행 규칙 + validTransitions = { + right: ['up'], + down: ['right'], + left: ['down'], + up: ['left'], + } + } else { + // 시계방향 진행 규칙 + validTransitions = { + right: ['down'], + down: ['left'], + left: ['up'], + up: ['right'], + } + } + return !validTransitions[currentDirection] || validTransitions[currentDirection].includes(nextDirection) + } + + // 시작점: 왼쪽 위 점 (x가 최소이면서 y도 최소인 점) + const startPoint = points.reduce((min, point) => { + if (point.x < min.x || (point.x === min.x && point.y < min.y)) { + return point + } + return min + }) + + const sortedPoints = [startPoint] + const remainingPoints = points.filter((p) => p !== startPoint) + + let currentPoint = startPoint + let currentDirection = null + + while (remainingPoints.length > 0) { + let nextPoint = null + let nextDirection = null + let minDistance = Infinity + + // 현재 방향을 고려하여 다음 점 찾기 + for (const point of remainingPoints) { + const dx = point.x - currentPoint.x + const dy = point.y - currentPoint.y + + // 직교 다각형이므로 수직 또는 수평 방향만 고려 + if (Math.abs(dx) < 1e-10 && Math.abs(dy) > 1e-10) { + // 수직 이동 + const direction = dy > 0 ? 'down' : 'up' + const distance = Math.abs(dy) + + if (isValidNextDirection(currentDirection, direction) && distance < minDistance) { + nextPoint = point + nextDirection = direction + minDistance = distance + } + } else if (Math.abs(dy) < 1e-10 && Math.abs(dx) > 1e-10) { + // 수평 이동 + const direction = dx > 0 ? 'right' : 'left' + const distance = Math.abs(dx) + + if (isValidNextDirection(currentDirection, direction) && distance < minDistance) { + nextPoint = point + nextDirection = direction + minDistance = distance + } + } + } + + if (!nextPoint) { + for (const point of remainingPoints) { + const dx = point.x - currentPoint.x + const dy = point.y - currentPoint.y + + // 직교 다각형이므로 수직 또는 수평 방향만 고려 + if (Math.abs(dx) < 1e-10 && Math.abs(dy) > 1e-10) { + // 수직 이동 + const direction = dy > 0 ? 'down' : 'up' + const distance = Math.abs(dy) + + if (isValidNextDirection(currentDirection, direction, true) && distance < minDistance) { + nextPoint = point + nextDirection = direction + minDistance = distance + } + } else if (Math.abs(dy) < 1e-10 && Math.abs(dx) > 1e-10) { + // 수평 이동 + const direction = dx > 0 ? 'right' : 'left' + const distance = Math.abs(dx) + + if (isValidNextDirection(currentDirection, direction, true) && distance < minDistance) { + nextPoint = point + nextDirection = direction + minDistance = distance + } + } + } + } + if (nextPoint) { + sortedPoints.push(nextPoint) + remainingPoints.splice(remainingPoints.indexOf(nextPoint), 1) + currentPoint = nextPoint + currentDirection = nextDirection + } else { + // 다음 점을 찾을 수 없는 경우, 가장 가까운 점 선택 + const nearestPoint = remainingPoints.reduce((nearest, point) => { + const currentDist = Math.sqrt(Math.pow(point.x - currentPoint.x, 2) + Math.pow(point.y - currentPoint.y, 2)) + const nearestDist = Math.sqrt(Math.pow(nearest.x - currentPoint.x, 2) + Math.pow(nearest.y - currentPoint.y, 2)) + return currentDist < nearestDist ? point : nearest + }) + + sortedPoints.push(nearestPoint) + remainingPoints.splice(remainingPoints.indexOf(nearestPoint), 1) + currentPoint = nearestPoint + currentDirection = null + } + } + return sortedPoints +} -- 2.47.2 From 760becfb0d84d87a412afb5153a877c4dca06949 Mon Sep 17 00:00:00 2001 From: Jaeyoung Lee Date: Wed, 10 Sep 2025 13:45:01 +0900 Subject: [PATCH 07/13] =?UTF-8?q?innerLines=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/qpolygon-utils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index bad9bd69..b9e812f3 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -1372,16 +1372,16 @@ export const drawGableRoof = (roofId, canvas, textMode) => { innerLines.push(drawRoofLine(points, canvas, roof, textMode)) } else { //다른방향 처리 - drawHipLine(points, canvas, roof, textMode, null, currentDegree, currentDegree) + innerLines.push(drawHipLine(points, canvas, roof, textMode, null, currentDegree, currentDegree)) } } else if (analyze.isVertical) { //현재라인이 수직선일때 if (isVertical) { //같은방향 처리 - drawRoofLine(points, canvas, roof, textMode) + innerLines.push(drawRoofLine(points, canvas, roof, textMode)) } else { //다른방향 처리 - drawHipLine(points, canvas, roof, textMode, null, currentDegree, currentDegree) + innerLines.push(drawHipLine(points, canvas, roof, textMode, null, currentDegree, currentDegree)) } } }) -- 2.47.2 From c58146ca5362e3100a3a7cfe125dbe3821881bb5 Mon Sep 17 00:00:00 2001 From: Jaeyoung Lee Date: Mon, 22 Sep 2025 10:40:35 +0900 Subject: [PATCH 08/13] =?UTF-8?q?=EC=86=8C=EC=88=98=EC=A0=90=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=A7=80=EB=B6=95?= =?UTF-8?q?=EC=84=A0=20=EC=B0=BE=EA=B8=B0=20=EC=98=A4=EB=A5=98=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/qpolygon-utils.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index b9e812f3..e29db805 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -572,8 +572,14 @@ 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)).toNumber() - const edgeDy = Big(findEdge.vertex2.y).minus(Big(findEdge.vertex1.y)).toNumber() + 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 } @@ -582,8 +588,10 @@ export const drawGableRoof = (roofId, canvas, textMode) => { 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)).toNumber() - const intersectDy = Big(intersect.y).minus(Big(findEdge.vertex1.y)).toNumber() + 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) { -- 2.47.2 From 46710533b5e52eb5feab660128b482188a1cda45 Mon Sep 17 00:00:00 2001 From: Jaeyoung Lee Date: Wed, 24 Sep 2025 10:46:36 +0900 Subject: [PATCH 09/13] =?UTF-8?q?=EB=8F=99=EC=84=A0=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=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 | 123 +++++++++++++--------- src/util/qpolygon-utils.js | 5 +- 2 files changed, 78 insertions(+), 50 deletions(-) diff --git a/src/hooks/roofcover/useMovementSetting.js b/src/hooks/roofcover/useMovementSetting.js index e1fa2650..7d1d326a 100644 --- a/src/hooks/roofcover/useMovementSetting.js +++ b/src/hooks/roofcover/useMovementSetting.js @@ -411,6 +411,35 @@ export function useMovementSetting(id) { targetBaseLines.sort((a, b) => a.distance - b.distance) targetBaseLines = targetBaseLines.filter((line) => line.distance === targetBaseLines[0].distance) + + if (isGableRoof) { + const zeroLengthLines = targetBaseLines.filter( + (line) => Math.sqrt(Math.pow(line.line.x2 - line.line.x1, 2) + Math.pow(line.line.y2 - line.line.y1, 2)) < 1, + ) + if (zeroLengthLines.length > 0) { + zeroLengthLines.forEach((line) => { + const findLine = line.line + const findCoords = [ + { x: findLine.x1, y: findLine.y1 }, + { x: findLine.x2, y: findLine.y2 }, + ] + wall.baseLines + .filter((baseLine) => { + return findCoords.some( + (coord) => + (Math.abs(coord.x - baseLine.x1) < 0.1 && Math.abs(coord.y - baseLine.y1) < 0.1) || + (Math.abs(coord.x - baseLine.x2) < 0.1 && Math.abs(coord.y - baseLine.y2) < 0.1), + ) + }) + .forEach((baseLine) => { + const isAlready = targetBaseLines.find((target) => target.line === baseLine) + if (isAlready) return + targetBaseLines.push({ line: baseLine, distance: targetBaseLines[0].distance }) + }) + }) + } + } + let value if (typeRef.current === TYPE.FLOW_LINE) { value = @@ -445,58 +474,56 @@ export function useMovementSetting(id) { } } value = value.div(10) - targetBaseLines.forEach((target) => { - const currentLine = target.line - const index = baseLines.findIndex((line) => line === currentLine) - const nextLine = baseLines[(index + 1) % baseLines.length] - const prevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length] - let deltaX = 0 - let deltaY = 0 - if (currentLine.y1 === currentLine.y2) { - deltaY = value.toNumber() - } else { - deltaX = value.toNumber() - } + targetBaseLines + .filter((line) => Math.sqrt(Math.pow(line.line.x2 - line.line.x1, 2) + Math.pow(line.line.y2 - line.line.y1, 2)) >= 1) + .forEach((target) => { + const currentLine = target.line + const index = baseLines.findIndex((line) => line === currentLine) + const nextLine = baseLines[(index + 1) % baseLines.length] + const prevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length] + let deltaX = 0 + let deltaY = 0 + if (currentLine.y1 === currentLine.y2) { + deltaY = value.toNumber() + } else { + deltaX = value.toNumber() + } - currentLine.set({ - x1: currentLine.x1 + deltaX, - y1: currentLine.y1 + deltaY, - x2: currentLine.x2 + deltaX, - y2: currentLine.y2 + deltaY, - startPoint: { x: currentLine.x1 + deltaX, y: currentLine.y1 + deltaY }, - endPoint: { x: currentLine.x2 + deltaX, y: currentLine.y2 + deltaY }, - }) - const currentSize = calcLinePlaneSize({ - x1: currentLine.x1, - y1: currentLine.y1, - x2: currentLine.x2, - y2: currentLine.y2, - }) - currentLine.attributes.planeSize = currentSize - currentLine.attributes.actualSize = currentSize + currentLine.set({ + x1: currentLine.x1 + deltaX, + y1: currentLine.y1 + deltaY, + x2: currentLine.x2 + deltaX, + y2: currentLine.y2 + deltaY, + startPoint: { x: currentLine.x1 + deltaX, y: currentLine.y1 + deltaY }, + endPoint: { x: currentLine.x2 + deltaX, y: currentLine.y2 + deltaY }, + }) + const currentSize = calcLinePlaneSize({ + x1: currentLine.x1, + y1: currentLine.y1, + x2: currentLine.x2, + y2: currentLine.y2, + }) + currentLine.attributes.planeSize = currentSize + currentLine.attributes.actualSize = currentSize - const nextOldActualSize = nextLine.attributes.planeSize - nextLine.set({ - x1: nextLine.x1 + deltaX, - y1: nextLine.y1 + deltaY, - startPoint: { x: nextLine.x1 + deltaX, y: nextLine.y1 + deltaY }, - }) - const nextSize = calcLinePlaneSize({ x1: nextLine.x1, y1: nextLine.y1, x2: nextLine.x2, y2: nextLine.y2 }) - nextLine.attributes.planeSize = nextSize - nextLine.attributes.actualSize = nextSize + nextLine.set({ + x1: currentLine.x2, + y1: currentLine.y2, + startPoint: { x: currentLine.x2, y: currentLine.y2 }, + }) + const nextSize = calcLinePlaneSize({ x1: nextLine.x1, y1: nextLine.y1, x2: nextLine.x2, y2: nextLine.y2 }) + nextLine.attributes.planeSize = nextSize + nextLine.attributes.actualSize = nextSize - const prevOldActualSize = prevLine.attributes.planeSize - prevLine.set({ - x2: prevLine.x2 + deltaX, - y2: prevLine.y2 + deltaY, - endPoint: { x: prevLine.x2 + deltaX, y: prevLine.y2 + deltaY }, + prevLine.set({ + x2: currentLine.x1, + y2: currentLine.y1, + endPoint: { x: currentLine.x1, y: currentLine.y1 }, + }) + const prevSize = calcLinePlaneSize({ x1: prevLine.x1, y1: prevLine.y1, x2: prevLine.x2, y2: prevLine.y2 }) + prevLine.attributes.planeSize = prevSize + prevLine.attributes.actualSize = prevSize }) - const prevSize = calcLinePlaneSize({ x1: prevLine.x1, y1: prevLine.y1, x2: prevLine.x2, y2: prevLine.y2 }) - prevLine.attributes.planeSize = prevSize - prevLine.attributes.actualSize = prevSize - - canvas.renderAll() - }) roof.drawHelpLine() initEvent() diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index e29db805..7eed8fa9 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -600,9 +600,10 @@ export const drawGableRoof = (roofId, canvas, textMode) => { } }) - let currentRoof = intersectRoofLines.find((roof) => isPointOnLineNew(roof.roofLine, roof.intersect)) + 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.sort((a, b) => a.length - b.length)[0] + currentRoof = intersectRoofLines[0] } let startPoint, endPoint -- 2.47.2 From bbd8a43864e72b712b891cc48b6c0c4c0d692485 Mon Sep 17 00:00:00 2001 From: Jaeyoung Lee Date: Wed, 24 Sep 2025 10:48:10 +0900 Subject: [PATCH 10/13] =?UTF-8?q?innerLines=20=EC=B4=88=EA=B8=B0=ED=99=94?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/fabric/QPolygon.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index c049a541..106a880f 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -249,6 +249,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { // && obj.name !== 'outerLinePoint', ) .forEach((obj) => this.canvas.remove(obj)) + this.innerLines = [] this.canvas.renderAll() let textMode = 'plane' -- 2.47.2 From 8cca1e9937bfb6d7b5007000e6fa11191a5aaedd Mon Sep 17 00:00:00 2001 From: Jaeyoung Lee Date: Mon, 29 Sep 2025 10:21:45 +0900 Subject: [PATCH 11/13] =?UTF-8?q?BackwardLines=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/qpolygon-utils.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 7eed8fa9..6a16bade 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -911,8 +911,8 @@ export const drawGableRoof = (roofId, canvas, textMode) => { const backY1 = Math.min(backward.eaves.y1, backward.eaves.y2) const backY2 = Math.max(backward.eaves.y1, backward.eaves.y2) return ( - (analyze.isHorizontal && Math.abs(currentX1 - backX1) < 1 && Math.abs(currentX2 - backX2) < 1) || - (analyze.isVertical && Math.abs(currentY1 - backY1) < 1 && Math.abs(currentY2 - backY2) < 1) + (analyze.isHorizontal && Math.abs(currentX1 - backX1) < 0.1 && Math.abs(currentX2 - backX2) < 0.1) || + (analyze.isVertical && Math.abs(currentY1 - backY1) < 0.1 && Math.abs(currentY2 - backY2) < 0.1) ) }) if (backWardLine) { @@ -1395,6 +1395,11 @@ export const drawGableRoof = (roofId, canvas, textMode) => { } }) } + canvas + .getObjects() + .filter((obj) => obj.name === 'check') + .forEach((obj) => canvas.remove(obj)) + canvas.renderAll() } forwardLines.forEach((forward) => { -- 2.47.2 From bf0e1e4cb06ae27b5c9add2772fdec47a257d9db Mon Sep 17 00:00:00 2001 From: Jaeyoung Lee Date: Mon, 29 Sep 2025 10:26:22 +0900 Subject: [PATCH 12/13] =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/fabric/QPolygon.js | 1 + src/util/qpolygon-utils.js | 16 +++++----------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 48199dec..d712b3f1 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -334,6 +334,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { if (types.every((type) => type === LINE_TYPE.WALLLINE.EAVES)) { // 용마루 -- straight-skeleton console.log('용마루 지붕') + drawRidgeRoof(this.id, this.canvas, textMode) } else if (isGableRoof(types)) { // A형, B형 박공 지붕 console.log('패턴 지붕') diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 6a16bade..15b9e569 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -994,12 +994,6 @@ export const drawGableRoof = (roofId, canvas, textMode) => { const points = findPloygonLineOverlap(roof, [ridgePoint[0].x, ridgePoint[0].y, ridgePoint[1].x, ridgePoint[1].y], canvas, roofId) ridgeLines.push(drawRidgeLine(points, canvas, roof, textMode)) }) - - canvas - .getObjects() - .filter((object) => object.name === 'check') - .forEach((object) => canvas.remove(object)) - canvas.renderAll() }) /** @@ -1395,11 +1389,6 @@ export const drawGableRoof = (roofId, canvas, textMode) => { } }) } - canvas - .getObjects() - .filter((obj) => obj.name === 'check') - .forEach((obj) => canvas.remove(obj)) - canvas.renderAll() } forwardLines.forEach((forward) => { @@ -1409,6 +1398,11 @@ export const drawGableRoof = (roofId, canvas, textMode) => { drawRoofPlane(backward) }) roof.innerLines.push(...ridgeLines, ...innerLines) + canvas + .getObjects() + .filter((obj) => obj.name === 'check') + .forEach((obj) => canvas.remove(obj)) + canvas.renderAll() } /** -- 2.47.2 From c58c1f2106fb985992afe07bcb18fe3f69ea6dea Mon Sep 17 00:00:00 2001 From: ysCha Date: Mon, 29 Sep 2025 11:37:28 +0900 Subject: [PATCH 13/13] =?UTF-8?q?[1195]=20DESIGN=20=EB=B0=9C=EC=A0=84=20?= =?UTF-8?q?=EC=8B=9C=EB=AE=AC=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=83=89?= =?UTF-8?q?=EC=83=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/simulator/Simulator.jsx | 47 +++++++++++--------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/src/components/simulator/Simulator.jsx b/src/components/simulator/Simulator.jsx index d59eabe1..5d6a7e3d 100644 --- a/src/components/simulator/Simulator.jsx +++ b/src/components/simulator/Simulator.jsx @@ -49,34 +49,25 @@ export default function Simulator() { return isNaN(num) ? 0 : num }), - backgroundColor: [ - 'rgba(255, 99, 132, 0.2)', - 'rgba(54, 162, 235, 0.2)', - 'rgba(255, 206, 86, 0.2)', - 'rgba(75, 192, 192, 0.2)', - 'rgba(153, 102, 255, 0.2)', - 'rgba(255, 159, 64, 0.2)', - 'rgba(0, 99, 132, 0.2)', - 'rgba(0, 162, 235, 0.2)', - 'rgba(0, 206, 86, 0.2)', - 'rgba(0, 192, 192, 0.2)', - 'rgba(0, 102, 255, 0.2)', - 'rgba(0, 159, 64, 0.2)', - ], - borderColor: [ - 'rgba(255, 99, 132, 0.2)', - 'rgba(54, 162, 235, 0.2)', - 'rgba(255, 206, 86, 0.2)', - 'rgba(75, 192, 192, 0.2)', - 'rgba(153, 102, 255, 0.2)', - 'rgba(255, 159, 64, 0.2)', - 'rgba(0, 99, 132, 0.2)', - 'rgba(0, 162, 235, 0.2)', - 'rgba(0, 206, 86, 0.2)', - 'rgba(0, 192, 192, 0.2)', - 'rgba(0, 102, 255, 0.2)', - 'rgba(0, 159, 64, 0.2)', - ], + backgroundColor: (context) => { + const chart = context.chart + const { ctx, chartArea } = chart + + if (!chartArea) { + // This case happens on initial chart load + return null + } + + const gradient = ctx.createLinearGradient(0, chartArea.bottom, 0, chartArea.top) + gradient.addColorStop(0, '#4FC3F7') // Light blue at bottom + gradient.addColorStop(0.3, '#2FA8E0') // Original blue + gradient.addColorStop(0.7, '#1976D2') // Medium blue + gradient.addColorStop(1, '#0D47A1') // Dark blue at top + + + return gradient + }, + borderColor: '#2FA8E0' , borderWidth: 1, }, ], -- 2.47.2