741 lines
23 KiB
JavaScript

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, drawDirectionArrow, drawHippedRoof, inPolygon, 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,
direction: null,
arrow: null,
initialize: function (points, options, canvas) {
// 소수점 전부 제거
points.forEach((point) => {
point.x = Math.round(point.x)
point.y = Math.round(point.y)
})
options.selectable = options.selectable ?? true
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), {
id: this.id,
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()
if (this.arrow) {
drawDirectionArrow(this)
}
})
this.on('selected', () => {
drawDirectionArrow(this)
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
if (this.arrow) {
this.canvas.remove(this.arrow)
this.canvas
.getObjects()
.filter((obj) => obj.name === 'directionText' && obj.parent === this.arrow)
.forEach((text) => {
this.canvas.remove(text)
})
this.arrow = 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,
attributes: {
offset: 0,
},
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() {
this.canvas
?.getObjects()
.filter((obj) => obj.name === 'lengthText' && obj.parent === this)
.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 = Number(Math.sqrt(dx * dx + dy * dy).toFixed(1)) * 10
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.Text(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.canvas
?.getObjects()
.filter((obj) => obj.name === 'lengthText' && obj.parent === this)
.forEach((text) => {
text.set({ fontSize: 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.canvas
?.getObjects()
.filter((obj) => obj.name === 'lengthText' && obj.parent === this)
.forEach((text) => {
text.set({ visible: isView })
})
},
setScaleX(scale) {
this.scaleX = scale
this.addLengthText()
},
setScaleY(scale) {
this.scaleY = scale
this.addLengthText()
},
divideLine() {
// splitPolygonWithLines(this)
},
})