From aebf4e0c4294e3655a15e8148ee5b0626a365d32 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Wed, 10 Jul 2024 17:00:16 +0900 Subject: [PATCH] =?UTF-8?q?=ED=85=9C=ED=94=8C=EB=A6=BF=20=EC=82=AC?= =?UTF-8?q?=EA=B0=81=ED=98=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/Roof2.jsx | 15 +- src/components/fabric/QLine.js | 22 ++- src/components/fabric/QPolygon.js | 244 +++++++++++++++++++++--------- src/hooks/useMode.js | 23 ++- src/store/canvasAtom.js | 14 +- src/util/canvas-util.js | 31 ++++ 6 files changed, 258 insertions(+), 91 deletions(-) diff --git a/src/components/Roof2.jsx b/src/components/Roof2.jsx index 0376a4dc..b4bff818 100644 --- a/src/components/Roof2.jsx +++ b/src/components/Roof2.jsx @@ -155,7 +155,6 @@ export default function Roof2() { canvas?.add(polygon) - polygon.drawRoof(-50) addBackgroundInPolygon(polygon) const lines = togglePolygonLine(polygon) @@ -176,11 +175,15 @@ export default function Roof2() { const makeQLine = () => { if (canvas) { - const line = new QLine([50, 50, 200, 50], { - stroke: 'black', - strokeWidth: 1, - fontSize: fontSize, - }) + const line = new QLine( + [50, 50, 200, 50], + { + stroke: 'black', + strokeWidth: 1, + fontSize: fontSize, + }, + 50, + ) canvas?.add(line) } diff --git a/src/components/fabric/QLine.js b/src/components/fabric/QLine.js index c88430d2..cbc64ed6 100644 --- a/src/components/fabric/QLine.js +++ b/src/components/fabric/QLine.js @@ -4,7 +4,7 @@ export class QLine extends fabric.Group { line text fontSize - length + length = 0 x1 y1 x2 @@ -13,12 +13,13 @@ export class QLine extends fabric.Group { type = 'QLine' parent - constructor(points, option) { + constructor(points, option, lengthTxt) { const [x1, y1, x2, y2] = points if (!option.fontSize) { throw new Error('Font size is required.') } + const line = new fabric.Line(points, { ...option, strokeWidth: 1 }) super([line], {}) @@ -31,6 +32,10 @@ export class QLine extends fabric.Group { this.direction = option.direction this.parent = option.parent + if (lengthTxt > 0) { + this.length = Number(lengthTxt) + } + this.#init() this.#addControl() } @@ -66,7 +71,16 @@ export class QLine extends fabric.Group { this.removeWithUpdate(this.text) this.text = null } - + if (this.length > 0) { + const text = new fabric.Textbox(this.length.toFixed(0).toString(), { + left: (this.x1 + this.x2) / 2, + top: (this.y1 + this.y2) / 2, + fontSize: this.fontSize, + }) + this.text = text + this.addWithUpdate(text) + return + } const scaleX = this.scaleX const scaleY = this.scaleY const x1 = this.left @@ -77,7 +91,7 @@ export class QLine extends fabric.Group { const dy = y2 - y1 this.length = Number(Math.sqrt(dx * dx + dy * dy).toFixed(0)) - const text = new fabric.Textbox(this.length.toString(), { + const text = new fabric.Textbox(this.length.toFixed(0).toString(), { left: (x1 + x2) / 2, top: (y1 + y2) / 2, fontSize: this.fontSize, diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 378793f0..c60670e1 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -1,8 +1,12 @@ import { fabric } from 'fabric' import { + calculateIntersection, distanceBetweenPoints, + getDegreeByChon, getDirection, getDirectionByPoint, + getRoofHeight, + getRoofHypotenuse, } from '@/util/canvas-util' import { QLine } from '@/components/fabric/QLine' @@ -15,7 +19,6 @@ export default class QPolygon extends fabric.Group { canvas fontSize qCells = [] - roof name constructor(points, options, canvas) { if (!options.fontSize) { @@ -43,8 +46,8 @@ export default class QPolygon extends fabric.Group { this.points.forEach((point, i) => { const nextPoint = this.points[(i + 1) % this.points.length] const line = new QLine([point.x, point.y, nextPoint.x, nextPoint.y], { - stroke: 'black', - strokeWidth: 1, + stroke: this.stroke, + strokeWidth: this.strokeWidth, fontSize: this.fontSize, direction: getDirectionByPoint(point, nextPoint), }) @@ -88,7 +91,7 @@ export default class QPolygon extends fabric.Group { }) this.texts = [] } - const points = this.getCurrentPoints() + const points = this.points points.forEach((start, i) => { const end = points[(i + 1) % points.length] @@ -281,77 +284,170 @@ export default class QPolygon extends fabric.Group { }) } - /** - * 지붕 그리기 - * @param offset - */ - drawRoof(offset = -10) { - const points = this.getCurrentPoints() - const offsetPoints = [] - for (let i = 0; i < points.length; i++) { - const prev = points[(i - 1 + points.length) % points.length] - const current = points[i] - const next = points[(i + 1) % points.length] - - // 두 벡터 계산 (prev -> current, current -> next) - const vector1 = { x: current.x - prev.x, y: current.y - prev.y } - const vector2 = { x: next.x - current.x, y: next.y - current.y } - - // 벡터의 길이 계산 - const length1 = Math.sqrt(vector1.x * vector1.x + vector1.y * vector1.y) - const length2 = Math.sqrt(vector2.x * vector2.x + vector2.y * vector2.y) - - // 벡터를 단위 벡터로 정규화 - const unitVector1 = { x: vector1.x / length1, y: vector1.y / length1 } - const unitVector2 = { x: vector2.x / length2, y: vector2.y / length2 } - - // 법선 벡터 계산 (왼쪽 방향) - const normal1 = { x: -unitVector1.y, y: unitVector1.x } - const normal2 = { x: -unitVector2.y, y: unitVector2.x } - - // 법선 벡터 평균 계산 - const averageNormal = { - x: (normal1.x + normal2.x) / 2, - y: (normal1.y + normal2.y) / 2, - } - - // 평균 법선 벡터를 단위 벡터로 정규화 - const lengthNormal = Math.sqrt( - averageNormal.x * averageNormal.x + averageNormal.y * averageNormal.y, - ) - const unitNormal = { - x: averageNormal.x / lengthNormal, - y: averageNormal.y / lengthNormal, - } - - // 오프셋 적용 - const offsetPoint = { - x: current.x + unitNormal.x * offset, - y: current.y + unitNormal.y * offset, - } - - offsetPoints.push(offsetPoint) - } - - const roof = new QPolygon( - offsetPoints, - { - stroke: 'black', - strokeWidth: 1, - fill: 'transparent', - fontSize: this.fontSize, - }, - this.canvas, - ) - - this.roof = roof - - this.addWithUpdate(outPolygon) - this.canvas.renderAll() - } - fillBackground(pattern) { this.polygon.set({ fill: pattern }) - this.canvas.renderAll() + this.canvas.requestRenderAll() + } + + // 보조선 그리기 사각형에서만 + drawHelpLine(chon) { + let type + let smallestLength = Infinity + let maxLength = 0 + + this.lines.forEach((line) => { + if (line.length < smallestLength) { + smallestLength = line.length + } + if (line.length > maxLength) { + maxLength = line.length + } + }) + + const smallestLines = this.lines.filter( + (line) => line.length === smallestLength, + ) + + let needPlusLine + let needMinusLine + + const direction = smallestLines[0].direction + + if (direction === 'top' || direction === 'bottom') { + needPlusLine = + smallestLines[0].x1 < smallestLines[1].x1 + ? smallestLines[0] + : smallestLines[1] + needMinusLine = + needPlusLine === smallestLines[0] ? smallestLines[1] : smallestLines[0] + + type = 1 // 가로가 긴 사각형 + } + + if (direction === 'left' || direction === 'right') { + needPlusLine = + smallestLines[0].y1 < smallestLines[1].y1 + ? smallestLines[0] + : smallestLines[1] + needMinusLine = + needPlusLine === smallestLines[0] ? smallestLines[1] : smallestLines[0] + + type = 2 // 세로가 긴 사각형 + } + + let point1 + let point2 + + if (type === 1) { + point1 = { + x: needPlusLine.x1 + smallestLength / 2, + y: + needPlusLine.y1 > needPlusLine.y2 + ? needPlusLine.y1 - smallestLength / 2 + : needPlusLine.y2 - smallestLength / 2, + } + + point2 = { + x: needMinusLine.x1 - smallestLength / 2, + y: + needMinusLine.y1 > needMinusLine.y2 + ? needMinusLine.y1 - smallestLength / 2 + : needMinusLine.y2 - smallestLength / 2, + } + } else if (type === 2) { + point1 = { + x: + needPlusLine.x1 > needPlusLine.x2 + ? needPlusLine.x1 - smallestLength / 2 + : needPlusLine.x2 - smallestLength / 2, + y: needPlusLine.y1 + smallestLength / 2, + } + + point2 = { + x: + needMinusLine.x1 > needMinusLine.x2 + ? needMinusLine.x1 - smallestLength / 2 + : needMinusLine.x2 - smallestLength / 2, + y: needMinusLine.y1 - smallestLength / 2, + } + } + + // 빗변1 + const realLine1 = new QLine( + [needPlusLine.x1, needPlusLine.y1, point1.x, point1.y], + { fontSize: this.fontSize, stroke: 'black', strokeWidth: 1 }, + getRoofHypotenuse(smallestLength / 2), + ) + + // 빗변2 + const realLine2 = new QLine( + [needPlusLine.x2, needPlusLine.y2, point1.x, point1.y], + { fontSize: this.fontSize, stroke: 'black', strokeWidth: 1 }, + getRoofHypotenuse(smallestLength / 2), + ) + + // 빗변3 + const realLine3 = new QLine( + [needMinusLine.x1, needMinusLine.y1, point2.x, point2.y], + { fontSize: this.fontSize, stroke: 'black', strokeWidth: 1 }, + getRoofHypotenuse(smallestLength / 2), + ) + + // 빗변4 + const realLine4 = new QLine( + [needMinusLine.x2, needMinusLine.y2, point2.x, point2.y], + { fontSize: this.fontSize, stroke: 'black', strokeWidth: 1 }, + getRoofHypotenuse(smallestLength / 2), + ) + + let centerPoint1 + let centerPoint2 + + if (type === 1) { + centerPoint1 = { x: point1.x - smallestLength / 2, y: point1.y } + centerPoint2 = { x: point2.x + smallestLength / 2, y: point2.y } + } else if (type === 2) { + centerPoint1 = { x: point1.x, y: point1.y - smallestLength / 2 } + centerPoint2 = { x: point2.x, y: point2.y + smallestLength / 2 } + } + + // 옆으로 누워있는 지붕의 높이 + const realLine5 = new QLine( + [point1.x, point1.y, centerPoint1.x, centerPoint1.y], + { + fontSize: this.fontSize, + stroke: 'black', + strokeWidth: 1, + strokeDashArray: [5, 5], + }, + getRoofHeight(smallestLength / 2, getDegreeByChon(4)), + ) + + // 옆으로 누워있는 지붕의 높이 + const realLine6 = new QLine( + [point2.x, point2.y, centerPoint2.x, centerPoint2.y], + { + fontSize: this.fontSize, + stroke: 'black', + strokeWidth: 1, + strokeDashArray: [5, 5], + }, + getRoofHeight(smallestLength / 2, getDegreeByChon(4)), + ) + + // 용마루 + const ridge = new QLine([point1.x, point1.y, point2.x, point2.y], { + fontSize: this.fontSize, + stroke: 'black', + strokeWidth: 1, + }) + + this.addWithUpdate(realLine1) + this.addWithUpdate(realLine2) + this.addWithUpdate(realLine3) + this.addWithUpdate(realLine4) + this.addWithUpdate(realLine5) + this.addWithUpdate(realLine6) + this.addWithUpdate(ridge) } } diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index f92d67e8..28f7df70 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -7,8 +7,13 @@ import { findTopTwoIndexesByDistance, getDirection, } from '@/util/canvas-util' -import { useRecoilState } from 'recoil' -import { fontSizeState, sortedPolygonArray } from '@/store/canvasAtom' +import { useRecoilState, useSetRecoilState } from 'recoil' +import { + fontSizeState, + roofState, + sortedPolygonArray, + wallState, +} from '@/store/canvasAtom' import { QLine } from '@/components/fabric/QLine' export const Mode = { @@ -30,6 +35,8 @@ export function useMode() { const [fontSize] = useRecoilState(fontSizeState) const [shape, setShape] = useState(0) const [sortedArray, setSortedArray] = useRecoilState(sortedPolygonArray) + const [roof, setRoof] = useRecoilState(roofState) + const [wall, setWall] = useRecoilState(wallState) const addEvent = (mode) => { switch (mode) { @@ -210,7 +217,8 @@ export function useMode() { // handleOuterlines() handleOuterlinesTest() //외곽선 그리기 테스트 - makePolygon() + const wall = makePolygon() + setWall(wall) } } @@ -398,11 +406,13 @@ export function useMode() { // 캔버스를 다시 그립니다. if (!otherLines) { - polygon.fillCell() + // polygon.fillCell() canvas.renderAll() polygon.setViewLengthText(false) setMode(Mode.DEFAULT) } + + return polygon } /** @@ -714,7 +724,6 @@ export function useMode() { } } - console.log(newOuterlines) makePolygon(newOuterlines) } @@ -780,7 +789,9 @@ export function useMode() { offsetPoints.push(offsetPoint) } - makePolygon(offsetPoints) + const roof = makePolygon(offsetPoints) + setRoof(roof) + roof.drawHelpLine() } const togglePolygonLine = (obj) => { diff --git a/src/store/canvasAtom.js b/src/store/canvasAtom.js index c4b5dd19..16f057b1 100644 --- a/src/store/canvasAtom.js +++ b/src/store/canvasAtom.js @@ -20,6 +20,18 @@ export const canvasSizeState = atom({ export const sortedPolygonArray = atom({ key: 'sortedArray', - default : [], + default: [], + dangerouslyAllowMutability: true, +}) + +export const roofState = atom({ + key: 'roof', + default: {}, + dangerouslyAllowMutability: true, +}) + +export const wallState = atom({ + key: 'wall', + default: {}, dangerouslyAllowMutability: true, }) diff --git a/src/util/canvas-util.js b/src/util/canvas-util.js index d0e7b051..769329bb 100644 --- a/src/util/canvas-util.js +++ b/src/util/canvas-util.js @@ -264,3 +264,34 @@ export const getDirectionByPoint = (a, b) => { return vector.y > 0 ? 'bottom' : 'top' } } + +export function calculateIntersection(line1, line2) { + const x1 = line1.x1, + y1 = line1.y1, + x2 = line1.x2, + y2 = line1.y2 + const x3 = line2.x1, + y3 = line2.y1, + x4 = line2.x2, + y4 = line2.y2 + + const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) + if (denom === 0) return null // 선분이 평행하거나 일치 + + const intersectX = + ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / denom + const intersectY = + ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / denom + + // 교차점이 두 선분의 x 좌표 범위 내에 있는지 확인 + if ( + intersectX < Math.min(x1, x2) || + intersectX > Math.max(x1, x2) || + intersectX < Math.min(x3, x4) || + intersectX > Math.max(x3, x4) + ) { + return null // 교차점이 선분 범위 밖에 있음 + } + + return { x: intersectX, y: intersectY } +}