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, drawHippedRoof, inPolygon, lineIntersect, splitPolygonWithLines, toGeoJSON } from '@/util/qpolygon-utils' import * as turf from '@turf/turf' export const QPolygon = fabric.util.createClass(fabric.Polygon, { type: 'QPolygon', lines: [], texts: [], id: null, length: 0, hips: [], ridges: [], connectRidges: [], cells: [], parentId: null, innerLines: [], children: [], initOptions: null, initialize: function (points, options, canvas) { // 소수점 전부 제거 points.forEach((point) => { point.x = Math.round(point.x) point.y = Math.round(point.y) }) options.sort = options.sort ?? true options.parentId = options.parentId ?? null if (!options.sort && points.length <= 8) { points = sortedPointLessEightPoint(points) } else { let isDiagonal = false points.forEach((point, i) => { if (isDiagonal) { return } const nextPoint = points[(i + 1) % points.length] const angle = calculateAngle(point, nextPoint) if (!(Math.abs(angle) === 0 || Math.abs(angle) === 180 || Math.abs(angle) === 90)) { isDiagonal = true } }) if (!isDiagonal) { points = sortedPoints(points) } } this.callSuper('initialize', points, options) if (options.id) { this.id = options.id } else { this.id = uuidv4() } if (canvas) { this.canvas = canvas } this.initOptions = options this.init() this.initLines() this.setShape() }, 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 }, toObject: function (propertiesToInclude) { return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), { type: this.type, text: this.text, hips: this.hips, ridges: this.ridges, connectRidges: this.connectRidges, }) }, init: function () { this.addLengthText() this.on('moving', () => { this.addLengthText() }) this.on('modified', (e) => { this.addLengthText() }) this.on('selected', () => { Object.keys(this.controls).forEach((controlKey) => { if (controlKey !== 'ml' && controlKey !== 'mr') { this.setControlVisible(controlKey, false) } }) }) this.on('removed', () => { const thisText = this.canvas.getObjects().filter((obj) => obj.name === 'lengthText' && obj.parentId === this.id) thisText.forEach((text) => { this.canvas.remove(text) }) this.texts = null }) // polygon.fillCell({ width: 50, height: 30, padding: 10 }) }, initLines() { this.lines = [] 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), idx: i, }) line.startPoint = point line.endPoint = nextPoint this.lines.push(line) }) }, // 보조선 그리기 drawHelpLine(chon = 4) { // drawHelpLineInHexagon(this, chon) drawHippedRoof(this, chon) }, addLengthText() { if (this.texts.length > 0) { this.texts.forEach((text) => { this.canvas.remove(text) }) } let 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) const midPoint = new fabric.Point((start.x + end.x) / 2, (start.y + end.y) / 2) const degree = (Math.atan2(dy, dx) * 180) / Math.PI // Create new text object if it doesn't exist const text = new fabric.IText(length.toFixed(0), { left: midPoint.x, top: midPoint.y, fontSize: this.fontSize, parentId: this.id, minX: Math.min(start.x, end.x), maxX: Math.max(start.x, end.x), minY: Math.min(start.y, end.y), maxY: Math.max(start.y, end.y), parentDirection: getDirectionByPoint(start, end), parentDegree: degree, dirty: true, editable: true, selectable: true, lockRotation: true, lockScalingX: true, lockScalingY: true, idx: i, name: 'lengthText', parent: this, }) this.texts.push(text) this.canvas.add(text) this.canvas.renderAll() }) }, setFontSize(fontSize) { this.fontSize = fontSize this.text.set({ fontSize }) }, _render: function (ctx) { this.callSuper('_render', ctx) }, _set: function (key, value) { this.callSuper('_set', key, value) }, setCanvas(canvas) { this.canvas = canvas }, fillCell(cell = { width: 50, height: 100, padding: 10 }) { const points = this.points const minX = Math.min(...points.map((p) => p.x)) const maxX = Math.max(...points.map((p) => p.x)) const minY = Math.min(...points.map((p) => p.y)) const maxY = Math.max(...points.map((p) => p.y)) const boundingBoxWidth = maxX - minX const boundingBoxHeight = maxY - minY const rectWidth = cell.width const rectHeight = cell.height const cols = Math.floor((boundingBoxWidth + cell.padding) / (rectWidth + cell.padding)) const rows = Math.floor((boundingBoxHeight + cell.padding) / (rectHeight + cell.padding)) //전체 높이에서 패딩을 포함하고 rows를 곱해서 여백길이를 계산 후에 2로 나누면 반높이를 넣어서 중간으로 정렬 const tmpHeight = (boundingBoxHeight - (rectHeight + cell.padding) * rows) / 2 //센터 정렬시에 쓴다 체크박스가 존재함 TODO: if문 추가해서 정렬해야함 let tmpWidth = (boundingBoxWidth - (rectWidth + cell.padding) * cols) / 2 const drawCellsArray = [] //그려진 셀의 배열 let idx = 1 for (let i = 0; i < cols; i++) { for (let j = 0; j < rows; j++) { const rectLeft = minX + i * (rectWidth + cell.padding) const rectTop = minY + j * (rectHeight + cell.padding) const rectPoints = [ { x: rectLeft, y: rectTop }, { x: rectLeft, y: rectTop + rectHeight }, { x: rectLeft + rectWidth, y: rectTop + rectHeight }, { x: rectLeft + rectWidth, y: rectTop }, ] if (inPolygon(this.points, rectPoints)) { const rect = new fabric.Rect({ left: rectLeft, top: rectTop, width: rectWidth, height: rectHeight, fill: '#BFFD9F', stroke: 'black', selectable: true, // 선택 가능하게 설정 lockMovementX: true, // X 축 이동 잠금 lockMovementY: true, // Y 축 이동 잠금 lockRotation: true, // 회전 잠금 lockScalingX: true, // X 축 크기 조정 잠금 lockScalingY: true, // Y 축 크기 조정 잠금 opacity: 0.8, name: 'cell', idx: idx, parentId: this.id, parent: this, }) idx++ drawCellsArray.push(rect) //배열에 넣어서 반환한다 this.canvas.add(rect) } } } this.canvas?.renderAll() this.cells = drawCellsArray return drawCellsArray }, fillCellABType( cell = { width: 50, height: 100, padding: 5, wallDirection: 'left', referenceDirection: 'none', startIndex: -1, isCellCenter: false }, ) { const points = this.points const minX = Math.min(...points.map((p) => p.x)) //왼쪽 const maxX = Math.max(...points.map((p) => p.x)) //오른쪽 const minY = Math.min(...points.map((p) => p.y)) //위 const maxY = Math.max(...points.map((p) => p.y)) //아래 const boundingBoxWidth = maxX - minX const boundingBoxHeight = maxY - minY const rectWidth = cell.width const rectHeight = cell.height const cols = Math.floor((boundingBoxWidth + cell.padding) / (rectWidth + cell.padding)) const rows = Math.floor((boundingBoxHeight + cell.padding) / (rectHeight + cell.padding)) //전체 높이에서 패딩을 포함하고 rows를 곱해서 여백길이를 계산 후에 2로 나누면 반높이를 넣어서 중간으로 정렬 let centerHeight = rows > 1 ? (boundingBoxHeight - (rectHeight + cell.padding / 2) * rows) / 2 : (boundingBoxHeight - rectHeight * rows) / 2 //rows 1개 이상이면 cell 을 반 나눠서 중간을 맞춘다 let centerWidth = cols > 1 ? (boundingBoxWidth - (rectWidth + cell.padding / 2) * cols) / 2 : (boundingBoxWidth - rectWidth * cols) / 2 const drawCellsArray = [] //그려진 셀의 배열 let idx = 1 let startXPos, startYPos for (let i = 0; i < cols; i++) { for (let j = 0; j < rows; j++) { const rectPoints = [] if (cell.referenceDirection !== 'none') { //4각형은 기준점이 없다 if (cell.referenceDirection === 'top') { //top, bottom은 A패턴만 if (cell.wallDirection === 'left') { startXPos = minX + i * rectWidth startYPos = minY + j * rectHeight if (i > 0) { startXPos = startXPos + i * cell.padding //옆으로 패딩 } } else { startXPos = maxX - (1 + i) * rectWidth - 0.01 startYPos = minY + j * rectHeight + 0.01 if (i > 0) { startXPos = startXPos - i * cell.padding //옆으로 패딩 } } if (j > 0) { startYPos = startYPos + j * cell.padding } rectPoints.push( { x: startXPos, y: startYPos }, { x: startXPos + rectWidth, y: startYPos }, { x: startXPos, y: startYPos + rectHeight }, { x: startXPos + rectWidth, y: startYPos + rectHeight }, ) } else if (cell.referenceDirection === 'bottom') { if (cell.wallDirection === 'left') { startXPos = minX + i * rectWidth startYPos = maxY - j * rectHeight - 0.01 if (i > 0) { startXPos = startXPos + i * cell.padding } rectPoints.push( { x: startXPos, y: startYPos }, { x: startXPos + rectWidth, y: startYPos }, { x: startXPos, y: startYPos - rectHeight }, { x: startXPos + rectWidth, y: startYPos - rectHeight }, ) } else { startXPos = maxX - i * rectWidth - 0.01 startYPos = maxY - j * rectHeight - 0.01 if (i > 0) { startXPos = startXPos - i * cell.padding } rectPoints.push( { x: startXPos, y: startYPos }, { x: startXPos - rectWidth, y: startYPos }, { x: startXPos, y: startYPos - rectHeight }, { x: startXPos - rectWidth, y: startYPos - rectHeight }, ) startXPos = startXPos - rectWidth //우 -> 좌 들어가야해서 마이너스 처리 } startYPos = startYPos - rectHeight //밑에서 위로 올라가는거라 마이너스 처리 if (j > 0) { startYPos = startYPos - j * cell.padding } } else if (cell.referenceDirection === 'left') { //여기서부턴 B패턴임 if (cell.wallDirection === 'top') { startXPos = minX + i * rectWidth startYPos = minY + j * rectHeight if (i > 0) { startXPos = startXPos + i * cell.padding //밑으로 } if (j > 0) { startYPos = startYPos + j * cell.padding //옆으로 패딩 } rectPoints.push( { x: startXPos, y: startYPos }, { x: startXPos + rectWidth, y: startYPos }, { x: startXPos, y: startYPos + rectHeight }, { x: startXPos + rectWidth, y: startYPos + rectHeight }, ) } else { startXPos = minX + i * rectWidth startYPos = maxY - j * rectHeight - 0.01 if (i > 0) { startXPos = startXPos + i * cell.padding } if (j > 0) { startYPos = startYPos - j * cell.padding } rectPoints.push( { x: startXPos, y: startYPos }, { x: startXPos + rectWidth, y: startYPos }, { x: startXPos, y: startYPos - rectHeight }, { x: startXPos + rectWidth, y: startYPos - rectHeight }, ) startYPos = startYPos - rectHeight //밑에서 위로 올라가는거라 마이너스 처리 } } else if (cell.referenceDirection === 'right') { if (cell.wallDirection === 'top') { startXPos = maxX - i * rectWidth - 0.01 startYPos = minY + j * rectHeight + 0.01 if (j > 0) { startYPos = startYPos + j * cell.padding //위에서 밑으로라 + } rectPoints.push( { x: startXPos, y: startYPos }, { x: startXPos - rectWidth, y: startYPos }, { x: startXPos, y: startYPos + rectHeight }, { x: startXPos - rectWidth, y: startYPos + rectHeight }, ) } else { startXPos = maxX - i * rectWidth - 0.01 startYPos = maxY - j * rectHeight - 0.01 if (j > 0) { startYPos = startYPos - j * cell.padding } rectPoints.push( { x: startXPos, y: startYPos }, { x: startXPos - rectWidth, y: startYPos }, { x: startXPos, y: startYPos - rectHeight }, { x: startXPos - rectWidth, y: startYPos - rectHeight }, ) startYPos = startYPos - rectHeight //밑에서 위로 올라가는거라 마이너스 처리 } if (i > 0) { startXPos = startXPos - i * cell.padding //옆으로 패딩 } startXPos = startXPos - rectWidth // 우측에서 -> 좌측으로 그려짐 } } else { // centerWidth = 0 //나중에 중간 정렬 이면 어쩌구 함수 만들어서 넣음 if (['left', 'right'].includes(cell.wallDirection)) { centerWidth = cell.isCellCenter ? centerWidth : 0 } else if (['top', 'bottom'].includes(cell.wallDirection)) { centerHeight = cell.isCellCenter ? centerHeight : 0 } if (cell.wallDirection === 'left') { startXPos = minX + i * rectWidth + centerWidth startYPos = minY + j * rectHeight + centerHeight if (i > 0) { startXPos = startXPos + i * cell.padding } if (j > 0 && j < rows) { startYPos = startYPos + j * cell.padding } } else if (cell.wallDirection === 'right') { startXPos = maxX - (1 + i) * rectWidth - 0.01 - centerWidth startYPos = minY + j * rectHeight + 0.01 + centerHeight if (i > 0) { startXPos = startXPos - i * cell.padding } if (j > 0 && j < rows) { startYPos = startYPos + j * cell.padding } } else if (cell.wallDirection === 'top') { startXPos = minX + i * rectWidth - 0.01 + centerWidth startYPos = minY + j * rectHeight + 0.01 + centerHeight if (i > 0) { startXPos = startXPos + i * cell.padding } if (j > 0 && j < rows) { startYPos = startYPos + j * cell.padding } } else if (cell.wallDirection === 'bottom') { startXPos = minX + i * rectWidth + 0.01 + centerWidth startYPos = maxY - (j + 1) * rectHeight - 0.01 - centerHeight if (i > 0) { startXPos = startXPos + i * cell.padding } if (j > 0 && j < rows) { startYPos = startYPos - j * cell.padding } } } const allPointsInside = rectPoints.every((point) => this.inPolygonABType(point.x, point.y, points)) if (allPointsInside) { //먼저 그룹화를 시켜놓고 뒤에서 글씨를 넣어서 변경한다 const text = new fabric.Text(``, { fontFamily: 'serif', fontSize: 30, fill: 'black', type: 'cellText', }) const rect = new fabric.Rect({ // left: startXPos, // top: startYPos, width: rectWidth, height: rectHeight, fill: '#BFFD9F', stroke: 'black', selectable: true, // 선택 가능하게 설정 // lockMovementX: true, // X 축 이동 잠금 // lockMovementY: true, // Y 축 이동 잠금 // lockRotation: true, // 회전 잠금 // lockScalingX: true, // X 축 크기 조정 잠금 // lockScalingY: true, // Y 축 크기 조정 잠금 opacity: 0.8, name: 'cell', idx: idx, type: 'cellRect', }) const group = new fabric.Group([rect, text], { left: startXPos, top: startYPos, }) idx++ drawCellsArray.push(group) //배열에 넣어서 반환한다 this.canvas.add(group) this.canvas?.renderAll() } } } this.cells = drawCellsArray return drawCellsArray }, inPolygon(point) { const vertices = this.points 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 }, inPolygonABType(x, y, polygon) { let inside = false let n = polygon.length for (let i = 0, j = n - 1; i < n; j = i++) { let xi = polygon[i].x let yi = polygon[i].y let xj = polygon[j].x let yj = polygon[j].y // console.log('xi : ', xi, 'yi : ', yi, 'xj : ', xj, 'yj : ', yj) let intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi if (intersect) inside = !inside } return inside }, inPolygon2(rectPoints) { const polygonCoords = toGeoJSON(this.points) const rectCoords = toGeoJSON(rectPoints) const outerPolygon = turf.polygon([polygonCoords]) const innerPolygon = turf.polygon([rectCoords]) // 각 점이 다각형 내부에 있는지 확인 const allPointsInside = rectCoords.every((coord) => { const point = turf.point(coord) return turf.booleanPointInPolygon(point, outerPolygon) }) // 사각형의 변 정의 const rectEdges = [ [rectCoords[0], rectCoords[1]], [rectCoords[1], rectCoords[2]], [rectCoords[2], rectCoords[3]], [rectCoords[3], rectCoords[0]], ] // 다각형의 변 정의 const outerEdges = turf.lineString(outerPolygon.geometry.coordinates[0]) // 사각형의 변들이 다각형의 변과 교차하는지 확인 const noEdgesIntersect = rectEdges.every((edge) => { const line = turf.lineString(edge) const intersects = turf.lineIntersect(line, outerEdges) return intersects.features.length === 0 }) return allPointsInside && noEdgesIntersect }, 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 }, getCurrentPoints() { const pathOffset = this.get('pathOffset') const matrix = this.calcTransformMatrix() return this.get('points') .map(function (p) { return new fabric.Point(p.x - pathOffset.x, p.y - pathOffset.y) }) .map(function (p) { return fabric.util.transformPoint(p, matrix) }) }, setWall: function (wall) { this.wall = wall }, setViewLengthText(isView) { this.texts.forEach((text) => { text.set({ visible: isView }) }) }, divideLine() { splitPolygonWithLines(this) }, })