import { fabric } from 'fabric' import { distanceBetweenPoints, findTopTwoIndexesByDistance, getDegreeByChon, getDirectionByPoint, getRoofHeight, getRoofHypotenuse, sortedPoints, } from '@/util/canvas-util' import { QLine } from '@/components/fabric/QLine' export default class QPolygon extends fabric.Group { type = 'QPolygon' polygon points texts = [] lines = [] canvas fontSize qCells = [] name shape = 0 // 점 6개일때의 shape 모양 helpPoints = [] helpLines = [] constructor(points, options, canvas) { if (points.length !== 4 && points.length !== 6) { throw new Error('Points must be 4 or 6.') } if (!options.fontSize) { throw new Error('Font size is required.') } const sortPoints = sortedPoints(points) const polygon = new fabric.Polygon(sortPoints, options) super([polygon], {}) this.fontSize = options.fontSize this.points = sortPoints this.polygon = polygon this.name = options.name this.#init() this.#addEvent() this.#initLines() this.setShape() } #initLines() { 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: this.stroke, strokeWidth: this.strokeWidth, fontSize: this.fontSize, direction: getDirectionByPoint(point, nextPoint), }) this.lines.push(line) }) } #init() { this.#addLengthText() } #addEvent() { this.on('scaling', (e) => { this.#updateLengthText() }) this.on('selected', function () { // 모든 컨트롤 떼기 Object.keys(this.controls).forEach((controlKey) => { if (controlKey !== 'mtr') { this.setControlVisible(controlKey, false) } }) }) } setFontSize(fontSize) { this.fontSize = fontSize this.texts.forEach((text) => { text.set({ fontSize }) }) this.getObjects().forEach((obj) => { if (obj.type[0] === 'Q') { obj.setFontSize(fontSize) } }) this.addWithUpdate() } #addLengthText() { if (this.texts.length > 0) { this.texts.forEach((text) => { this.canvas.remove(text) }) this.texts = [] } const points = this.points points.forEach((start, i) => { const end = points[(i + 1) % points.length] const dx = end.x - start.x const dy = end.y - start.y const length = Math.sqrt(dx * dx + dy * dy) const midPoint = new fabric.Point( (start.x + end.x) / 2, (start.y + end.y) / 2, ) // Create new text object if it doesn't exist const text = new fabric.Text(length.toFixed(0), { left: midPoint.x, top: midPoint.y, fontSize: this.fontSize, selectable: false, }) this.texts.push(text) this.addWithUpdate(text) }) this.canvas.renderAll() } #updateLengthText() { const points = this.getCurrentPoints() points.forEach((start, i) => { const end = points[(i + 1) % points.length] const dx = end.x - start.x const dy = end.y - start.y const length = Math.sqrt(dx * dx + dy * dy) // Update the text object with the new length this.texts[i].set({ text: length.toFixed(0) }) }) this.canvas.renderAll() } fillCell(cell = { width: 50, height: 100, padding: 10 }) { const points = this.getCurrentPoints() let bounds try { bounds = fabric.util.makeBoundingBoxFromPoints(points) } catch (error) { alert('다각형의 꼭지점이 4개 이상이어야 합니다.') return } for ( let x = bounds.left; x < bounds.left + bounds.width; x += cell.width + cell.padding ) { for ( let y = bounds.top; y < bounds.top + bounds.height; y += cell.height + cell.padding ) { const rect = new fabric.Rect({ left: x, top: y, width: cell.width, height: cell.height, fill: 'transparent', stroke: 'black', selectable: false, }) const rectPoints = [ new fabric.Point(rect.left, rect.top), new fabric.Point(rect.left + rect.width, rect.top), new fabric.Point(rect.left, rect.top + rect.height), new fabric.Point(rect.left + rect.width, rect.top + rect.height), ] const isInside = rectPoints.every( (rectPoint) => this.inPolygon(rectPoint) && this.#distanceFromEdge(rectPoint) >= cell.padding, ) if (isInside) { this.addWithUpdate(rect) } } } this.canvas.renderAll() } /** * this.lines의 direction이 top 인 line의 모든 합이 bottom 인 line의 모든 합과 같은지 확인 * this.lines의 direction이 left 인 line의 모든 합이 right 인 line의 모든 합과 같은지 확인 * return {boolean} */ isValid() { const leftLinesLengthSum = this.lines .filter((line) => line.direction === 'left') .reduce((sum, line) => sum + line.length, 0) const rightLinesLengthSum = this.lines .filter((line) => line.direction === 'right') .reduce((sum, line) => sum + line.length, 0) const topLinesLengthSum = this.lines .filter((line) => line.direction === 'top') .reduce((sum, line) => sum + line.length, 0) const bottomLinesLengthSum = this.lines .filter((line) => line.direction === 'bottom') .reduce((sum, line) => sum + line.length, 0) return ( leftLinesLengthSum === rightLinesLengthSum && topLinesLengthSum === bottomLinesLengthSum ) } inPolygon(point) { const vertices = this.getCurrentPoints() let intersects = 0 for (let i = 0; i < vertices.length; i++) { let vertex1 = vertices[i] let vertex2 = vertices[(i + 1) % vertices.length] if (vertex1.y > vertex2.y) { let tmp = vertex1 vertex1 = vertex2 vertex2 = tmp } if (point.y === vertex1.y || point.y === vertex2.y) { point.y += 0.01 } if (point.y <= vertex1.y || point.y > vertex2.y) { continue } let xInt = ((point.y - vertex1.y) * (vertex2.x - vertex1.x)) / (vertex2.y - vertex1.y) + vertex1.x if (xInt < point.x) { intersects++ } } return intersects % 2 === 1 } #distanceFromEdge(point) { const vertices = this.getCurrentPoints() let minDistance = Infinity for (let i = 0; i < vertices.length; i++) { let vertex1 = vertices[i] let vertex2 = vertices[(i + 1) % vertices.length] const dx = vertex2.x - vertex1.x const dy = vertex2.y - vertex1.y const t = ((point.x - vertex1.x) * dx + (point.y - vertex1.y) * dy) / (dx * dx + dy * dy) let closestPoint if (t < 0) { closestPoint = vertex1 } else if (t > 1) { closestPoint = vertex2 } else { closestPoint = new fabric.Point(vertex1.x + t * dx, vertex1.y + t * dy) } const distance = distanceBetweenPoints(point, closestPoint) if (distance < minDistance) { minDistance = distance } } return minDistance } setViewLengthText(boolean) { this.texts.forEach((text) => { text.visible = boolean }) this.canvas.renderAll() } getCurrentPoints() { const scaleX = this.scaleX const scaleY = this.scaleY const left = this.left const top = this.top // 시작점 const point = this.points[0] const movingX = left - point.x * scaleX const movingY = top - point.y * scaleY return this.points.map((point) => { return { x: point.x * scaleX + movingX, y: point.y * scaleY + movingY, } }) } fillBackground(pattern) { this.polygon.set({ fill: pattern }) this.canvas.requestRenderAll() } // 보조선 그리기 사각형에서만 drawHelpLine(chon = 4) { if (!this.isValid()) { return } if (this.lines.length === 4) { this.#drawHelpLineInRect(chon) } else if (this.lines.length === 6) { // TODO : 6각형 this.#drawHelpLineInHexagon(chon) } else if (this.lines.length === 8) { // TODO : 8각형 this.#drawHelpLineInOctagon(chon) } } /** * 현재 점 6개만 가능 */ setShape() { let shape = 0 if (this.lines.length !== 6) { return } //외각선 기준 const topIndex = findTopTwoIndexesByDistance(this.lines).sort( (a, b) => a - b, ) //배열중에 큰 2값을 가져옴 TODO: 나중에는 인자로 받아서 다각으로 수정 해야됨 //일단 배열 6개 짜리 기준의 선 번호 if (topIndex[0] === 4) { if (topIndex[1] === 5) { //1번 shape = 1 } } else if (topIndex[0] === 1) { //4번 if (topIndex[1] === 2) { shape = 4 } } else if (topIndex[0] === 0) { if (topIndex[1] === 1) { //2번 shape = 2 } else if (topIndex[1] === 5) { //3번 shape = 3 } } this.shape = shape } /** * 현재 점 6개만 가능 * @returns {number} */ getShape() { return this.shape } #drawHelpLineInRect(chon) { let type = 1 let smallestLength = Infinity let maxLength = 0 this.lines.forEach((line) => { if (line.length < smallestLength) { smallestLength = line.length } if (line.length > maxLength) { maxLength = line.length } }) // QPolygon 객체의 모든 선들을 가져옵니다. const lines = [...this.lines] // 이 선들을 길이에 따라 정렬합니다. lines.sort((a, b) => a.length - b.length) // 정렬된 배열에서 가장 작은 두 선을 선택합니다. let smallestLines if (smallestLength === maxLength) { // 정사각형인 경우 0, 2번째 라인이 가장 짧은 라인 smallestLines = [lines[0], lines[2]] } else { smallestLines = lines.slice(0, 2) } 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(chon)), ) // 옆으로 누워있는 지붕의 높이 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(chon)), ) // 용마루 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) if (smallestLength !== maxLength) { // 정사각형이 아닌경우에만 용마루를 추가한다. this.canvas.add(ridge) } } #drawHelpLineInHexagon(chon) { let type = this.shape // 1 = 0, 3 // 2 = 2, 5 // 3 = 1, 4 // 4 = 0, 3 // 라인 기준점 1,2 let lines, lines2 // 용마루 시작점 2개 let vPoint1, vPoint2 // 용마루 시작점과 만나는 지붕의 중앙 let centerPoint1, centerPoint2 // 용마루 길이 let ridgeLength = 0 let ridgePoint1, ridgePoint2 if (type === 1) { lines = [this.lines[0], this.lines[3]] lines2 = [this.lines[1], this.lines[4]] vPoint1 = { x: lines[0].x1 + lines[0].length / 2, y: lines[0].y1 + lines[0].length / 2, } vPoint2 = { x: lines[1].x1 + lines[1].length / 2, y: lines[1].y1 - lines[1].length / 2, } centerPoint1 = { x: (lines[0].x1 + lines[0].x2) / 2, y: (lines[0].y1 + lines[0].y2) / 2, } centerPoint2 = { x: (lines[1].x1 + lines[1].x2) / 2, y: (lines[1].y1 + lines[1].y2) / 2, } ridgeLength = Math.min(this.lines[1].length, this.lines[2].length) ridgePoint1 = [vPoint1.x, vPoint1.y, vPoint1.x + ridgeLength, vPoint1.y] ridgePoint2 = [vPoint2.x, vPoint2.y, vPoint2.x, vPoint2.y - ridgeLength] } else if (type === 2) { lines = [this.lines[2], this.lines[5]] lines2 = [this.lines[0], this.lines[3]] vPoint1 = { x: lines[0].x1 - lines[0].length / 2, y: lines[0].y1 - lines[0].length / 2, } vPoint2 = { x: lines[1].x1 - lines[1].length / 2, y: lines[1].y1 + lines[1].length / 2, } centerPoint1 = { x: (lines[0].x1 + lines[0].x2) / 2, y: (lines[0].y1 + lines[0].y2) / 2, } centerPoint2 = { x: (lines[1].x1 + lines[1].x2) / 2, y: (lines[1].y1 + lines[1].y2) / 2, } ridgeLength = Math.min(this.lines[3].length, this.lines[4].length) ridgePoint1 = [vPoint1.x, vPoint1.y, vPoint1.x - ridgeLength, vPoint1.y] ridgePoint2 = [vPoint2.x, vPoint2.y, vPoint2.x, vPoint2.y + ridgeLength] } else if (type === 3) { lines = [this.lines[1], this.lines[4]] lines2 = [this.lines[2], this.lines[5]] vPoint1 = { x: lines[0].x1 + lines[0].length / 2, y: lines[0].y1 - lines[0].length / 2, } vPoint2 = { x: lines[1].x1 - lines[1].length / 2, y: lines[1].y1 - lines[1].length / 2, } centerPoint1 = { x: (lines[0].x1 + lines[0].x2) / 2, y: (lines[0].y1 + lines[0].y2) / 2, } centerPoint2 = { x: (lines[1].x1 + lines[1].x2) / 2, y: (lines[1].y1 + lines[1].y2) / 2, } ridgeLength = Math.min(this.lines[2].length, this.lines[3].length) ridgePoint1 = [vPoint1.x, vPoint1.y, vPoint1.x, vPoint1.y - ridgeLength] ridgePoint2 = [vPoint2.x, vPoint2.y, vPoint2.x - ridgeLength, vPoint2.y] } else if (type === 4) { lines = [this.lines[0], this.lines[3]] lines2 = [this.lines[1], this.lines[4]] vPoint1 = { x: lines[0].x1 + lines[0].length / 2, y: lines[0].y1 + lines[0].length / 2, } vPoint2 = { x: lines[1].x1 - lines[1].length / 2, y: lines[1].y1 + lines[1].length / 2, } centerPoint1 = { x: (lines[0].x1 + lines[0].x2) / 2, y: (lines[0].y1 + lines[0].y2) / 2, } centerPoint2 = { x: (lines[1].x1 + lines[1].x2) / 2, y: (lines[1].y1 + lines[1].y2) / 2, } ridgeLength = Math.min(this.lines[4].length, this.lines[5].length) ridgePoint1 = [vPoint1.x, vPoint1.y, vPoint1.x + ridgeLength, vPoint1.y] ridgePoint2 = [vPoint2.x, vPoint2.y, vPoint2.x, vPoint2.y + ridgeLength] } const realLine1 = new QLine( [lines[0].x1, lines[0].y1, vPoint1.x, vPoint1.y], { fontSize: this.fontSize, stroke: 'blue', strokeWidth: 1 }, getRoofHypotenuse(lines[0].length / 2), ) const realLine2 = new QLine( [lines[0].x2, lines[0].y2, vPoint1.x, vPoint1.y], { fontSize: this.fontSize, stroke: 'blue', strokeWidth: 1 }, getRoofHypotenuse(lines[0].length / 2), ) const realLine3 = new QLine( [lines[1].x1, lines[1].y1, vPoint2.x, vPoint2.y], { fontSize: this.fontSize, stroke: 'blue', strokeWidth: 1 }, getRoofHypotenuse(lines[1].length / 2), ) const realLine4 = new QLine( [lines[1].x2, lines[1].y2, vPoint2.x, vPoint2.y], { fontSize: this.fontSize, stroke: 'blue', strokeWidth: 1 }, getRoofHypotenuse(lines[1].length / 2), ) // 옆으로 누워있는 지붕의 높이 점선 const realLine5 = new QLine( [vPoint1.x, vPoint1.y, centerPoint1.x, centerPoint1.y], { fontSize: this.fontSize, stroke: 'blue', strokeWidth: 1, strokeDashArray: [5, 5], }, getRoofHeight(lines[0].length / 2, getDegreeByChon(chon)), ) // 옆으로 누워있는 지붕의 높이 점선 const realLine6 = new QLine( [vPoint2.x, vPoint2.y, centerPoint2.x, centerPoint2.y], { fontSize: this.fontSize, stroke: 'blue', strokeWidth: 1, strokeDashArray: [5, 5], }, getRoofHeight(lines[1].length / 2, getDegreeByChon(chon)), ) // 용마루 보조선 const ridgeHelpLine1 = new QLine( [lines2[0].x2, lines2[0].y2, ridgePoint1[2], ridgePoint1[3]], { fontSize: this.fontSize, stroke: 'blue', strokeWidth: 1, }, getRoofHypotenuse(lines[0].length / 2), ) // 용마루 보조선 const ridgeHelpLine2 = new QLine( [lines2[1].x2, lines2[1].y2, ridgePoint2[2], ridgePoint2[3]], { fontSize: this.fontSize, stroke: 'blue', strokeWidth: 1, }, getRoofHypotenuse(lines[1].length / 2), ) // 용마루 const ridge1 = new QLine(ridgePoint1, { fontSize: this.fontSize, stroke: 'blue', strokeWidth: 1, }) // 용마루 const ridge2 = new QLine(ridgePoint2, { fontSize: this.fontSize, stroke: 'blue', strokeWidth: 1, }) const ridgeEndLine = new QLine( [ridgePoint1[2], ridgePoint1[3], ridgePoint2[2], ridgePoint2[3]], { fontSize: this.fontSize, stroke: 'blue', strokeWidth: 1, }, Math.abs(realLine1.length - realLine3.length), ) this.helpLines = [ realLine1, realLine2, realLine3, realLine4, realLine5, realLine6, ridge1, ridge2, ridgeEndLine, ] this.addWithUpdate(realLine1) this.addWithUpdate(realLine2) this.addWithUpdate(realLine3) this.addWithUpdate(realLine4) this.addWithUpdate(realLine5) this.addWithUpdate(realLine6) this.addWithUpdate(ridgeHelpLine1) this.addWithUpdate(ridgeHelpLine2) this.addWithUpdate(ridge1) this.addWithUpdate(ridge2) this.addWithUpdate(ridgeEndLine) this.canvas.renderAll() } #drawHelpLineInOctagon(chon) {} }