qcast-front/src/util/canvas-util.js
Jaeyoung Lee b03cea96bb Merge branch 'dev' into feature/test-jy
# Conflicts:
#	src/components/Roof2.jsx
#	src/components/fabric/QPolygon.js
#	src/hooks/useMode.js
#	src/util/qpolygon-utils.js
2024-08-01 13:51:34 +09:00

678 lines
20 KiB
JavaScript

import { intersect } from 'mathjs'
/**
* Collection of function to use on canvas
*/
// define a function that can locate the controls
export function polygonPositionHandler(dim, finalMatrix, fabricObject) {
// @ts-ignore
let x = fabricObject.points[this.pointIndex].x - fabricObject.pathOffset.x
// @ts-ignore
let y = fabricObject.points[this.pointIndex].y - fabricObject.pathOffset.y
return fabric.util.transformPoint(
{ x, y },
fabric.util.multiplyTransformMatrices(fabricObject.canvas.viewportTransform, fabricObject.calcTransformMatrix()),
)
}
function getObjectSizeWithStroke(object) {
let stroke = new fabric.Point(object.strokeUniform ? 1 / object.scaleX : 1, object.strokeUniform ? 1 / object.scaleY : 1).multiply(
object.strokeWidth,
)
return new fabric.Point(object.width + stroke.x, object.height + stroke.y)
}
// define a function that will define what the control does
export function actionHandler(eventData, transform, x, y) {
let polygon = transform.target,
currentControl = polygon.controls[polygon.__corner],
mouseLocalPosition = polygon.toLocalPoint(new fabric.Point(x, y), 'center', 'center'),
polygonBaseSize = getObjectSizeWithStroke(polygon),
size = polygon._getTransformedDimensions(0, 0)
polygon.points[currentControl.pointIndex] = {
x: (mouseLocalPosition.x * polygonBaseSize.x) / size.x + polygon.pathOffset.x,
y: (mouseLocalPosition.y * polygonBaseSize.y) / size.y + polygon.pathOffset.y,
}
return true
}
// define a function that can keep the polygon in the same position when we change its width/height/top/left
export function anchorWrapper(anchorIndex, fn) {
return function(eventData, transform, x, y) {
let fabricObject = transform.target
let originX = fabricObject?.points[anchorIndex].x - fabricObject.pathOffset.x
let originY = fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y
let absolutePoint = fabric.util.transformPoint(
{
x: originX,
y: originY,
},
fabricObject.calcTransformMatrix(),
)
let actionPerformed = fn(eventData, transform, x, y)
let newDim = fabricObject._setPositionDimensions({})
let polygonBaseSize = getObjectSizeWithStroke(fabricObject)
let newX = (fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x) / polygonBaseSize.x
let newY = (fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y) / polygonBaseSize.y
fabricObject.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5)
return actionPerformed
}
}
/**
* 두 좌표의 중간점 좌표를 계산해서 반환하는 함수
* @param {number} point1
* @param {number} point2 방향에 상관없이 항상 큰 값이 뒤에 위치해야 함
* @returns
*/
export const getCenterPoint = (point1, point2) => {
return point1 + (point2 - point1) / 2
}
/**
* 두 점 사이의 거리를 계산하는 함수
* @param {*} x1 첫번째 점 x좌표
* @param {*} y1 첫번째 점 y좌표
* @param {*} x2 두번째 점 x좌표
* @param {*} y2 두번째 점 y좌표
* @returns
*/
export const getDistance = (x1, y1, x2, y2) => {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
}
// polygon의 각 변에 해당 점과 점 사이의 거리를 나타내는 IText를 추가하는 함수
export function addDistanceTextToPolygon(polygon) {
const points = polygon.get('points')
const texts = []
for (let i = 0; i < points.length; i++) {
const start = points[i]
const end = points[(i + 1) % points.length] // 다음 점 (마지막 점의 경우 첫번째 점으로)
const distance = Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2)) // 두 점 사이의 거리 계산
const text = new fabric.Textbox(distance.toFixed(2), {
// 소수 둘째자리까지 표시
left: (start.x + end.x) / 2, // 텍스트의 위치는 두 점의 중간
top: (start.y + end.y) / 2,
fontSize: 20,
})
texts.push(text)
}
return new fabric.Group([polygon, ...texts], {
// polygon과 텍스트들을 그룹화
selectable: true,
})
}
/**
*
* @param {number} value
* @param {boolean} useDefault
* @param {string} delimeter
* @returns
* ex) 1,100 mm
*/
export const formattedWithComma = (value, unit = 'mm') => {
let formatterdData = value.toLocaleString('ko-KR')
if (unit === 'cm') {
formatterdData = value.toLocaleString('ko-KR') / 10
} else if (unit === 'm') {
formatterdData = value.toLocaleString('ko-KR') / 1000
}
return `${formatterdData} ${unit}`
}
export const distanceBetweenPoints = (point1, point2) => {
const dx = point2.x - point1.x
const dy = point2.y - point1.y
return Math.sqrt(dx * dx + dy * dy)
}
/**
* line의 시작점을 찾는 함수
* @param lines
* @returns {number}
*/
export const getStartIndex = (lines) => {
let smallestIndex = 0
let smallestX1 = lines[0].x1
let smallestY1 = lines[0].y1
for (let i = 1; i < lines.length; i++) {
if (lines[i].x1 < smallestX1 || (lines[i].x1 === smallestX1 && lines[i].y1 < smallestY1)) {
smallestIndex = i
smallestX1 = lines[i].x1
smallestY1 = lines[i].y1
}
}
return smallestIndex
}
/**
* points 배열에서 시작점을 찾는 함수
* @param points
* @returns {number}
*/
export const getStartIndexPoint = (points) => {
let smallestIndex = 0
let smallestX1 = points[0].x
let smallestY1 = points[0].y
for (let i = 1; i < points.length; i++) {
if (points[i].x < smallestX1 || (points[i].x === smallestX1 && points[i].y < smallestY1)) {
smallestIndex = i
smallestX1 = points[i].x
smallestY1 = points[i].y
}
}
return smallestIndex
}
/**
* 이 함수는 두 개의 매개변수를 받습니다: array와 index.
* array는 재배열할 대상 배열입니다.
* index는 재배열의 기준이 될 배열 내의 위치입니다.
* 함수는 먼저 index 위치부터 배열의 마지막 요소까지를 추출합니다(fromIndexToEnd).
* 그 다음, 배열의 처음부터 index 위치까지의 요소를 추출합니다(fromStartToIndex).
* 마지막으로, fromIndexToEnd와 fromStartToIndex 두 부분을 concat 메소드를 이용해 합칩니다.
* 따라서, 이 함수는 주어진 index를 기준으로 배열을 두 부분으로 나누고, index부터 시작하는 부분을 앞에 두고, 그 뒤에 index 이전의 부분을 이어붙여 새로운 배열을 생성합니다. 이는 배열의 회전(rotating) 연산을 수행하는 것과 유사합니다.
* @param array 재배열할 대상 배열
* @param index 재배열 기준이 될 배열 내의 인덱스
* @returns {*} 새로 재배열된 배열
*/
export const rearrangeArray = (array, index) => {
// 배열의 특정 인덱스부터 마지막 요소까지를 가져옵니다.
const fromIndexToEnd = array.slice(index)
// 배열의 처음부터 특정 인덱스까지의 요소를 가져옵니다.
const fromStartToIndex = array.slice(0, index)
// 두 부분을 concat 메소드를 이용해 합칩니다.
return fromIndexToEnd.concat(fromStartToIndex)
}
export const findTopTwoIndexesByDistance = (objArr) => {
if (objArr.length < 2) {
return [] // 배열의 길이가 2보다 작으면 빈 배열 반환
}
let firstIndex = -1
let secondIndex = -1
let firstDistance = -Infinity
let secondDistance = -Infinity
for (let i = 0; i < objArr.length; i++) {
const distance = objArr[i].length
if (distance > firstDistance) {
secondDistance = firstDistance
secondIndex = firstIndex
firstDistance = distance
firstIndex = i
} else if (distance > secondDistance) {
secondDistance = distance
secondIndex = i
}
}
return [firstIndex, secondIndex]
}
/**
* 지붕의 누워있는 높이
* @param base 밑변
* @param degree 각도 ex(4촌의 경우 21.8)
*/
export const getRoofHeight = (base, degree) => {
return base / Math.cos((degree * Math.PI) / 180)
}
/**
* 지붕 빗변의 길이
* @param base 밑변
*/
export const getRoofHypotenuse = (base) => {
return Math.sqrt(base * base * 2)
}
/**
* 빗변의 길이를 입력받아 밑변의 길이를 반환
* @param base 빗변
*/
export const getAdjacent = (base) => {
return Math.round(Math.sqrt(Math.pow(base, 2) / 2))
}
/**
* 촌을 입력받아 각도를 반환
* @param chon
* @returns {number}
*/
export const getDegreeByChon = (chon) => {
// tan(theta) = height / base
const radians = Math.atan(chon / 10)
// 라디안을 도 단위로 변환
return Number((radians * (180 / Math.PI)).toFixed(2))
}
/**
* 두 점 사이의 방향을 반환합니다.
* @param a {fabric.Object}
* @param b {fabric.Object}
* @returns {string}
*/
export const getDirection = (a, b) => {
const vector = {
x: b.left - a.left,
y: b.top - a.top,
}
if (Math.abs(vector.x) > Math.abs(vector.y)) {
// x축 방향으로 더 많이 이동
return vector.x > 0 ? 'right' : 'left'
} else {
// y축 방향으로 더 많이 이동
return vector.y > 0 ? 'bottom' : 'top'
}
}
/**
* 두 점 사이의 방향을 반환합니다.
*/
export const getDirectionByPoint = (a, b) => {
const vector = {
x: b.x - a.x,
y: b.y - a.y,
}
if (Math.abs(vector.x) > Math.abs(vector.y)) {
// x축 방향으로 더 많이 이동
return vector.x > 0 ? 'right' : 'left'
} else {
// y축 방향으로 더 많이 이동
return vector.y > 0 ? 'bottom' : 'top'
}
}
export const findIntersection1 = (line1, line2) => {
const { x1, y1, x2, y2 } = line1 // 첫 번째 선의 두 점
const x3 = line2.x1
const y3 = line2.y1
const x4 = line2.x2
const y4 = line2.y2
// 선의 방정식의 계수 계산
const A1 = y2 - y1
const B1 = x1 - x2
const C1 = A1 * x1 + B1 * y1
const A2 = y4 - y3
const B2 = x3 - x4
const C2 = A2 * x3 + B2 * y3
const determinant = A1 * B2 - A2 * B1
if (determinant === 0) {
// 두 선이 평행하거나 일직선일 경우
return null
}
const x = (B1 * C2 - B2 * C1) / determinant
const y = (A1 * C2 - A2 * C1) / determinant
return { x, y }
}
export const calculateIntersection = (line1, line2) => {
const result = intersect([line1.x1, line1.y1], [line1.x2, line1.y2], [line2.x1, line2.y1], [line2.x2, line2.y2])
if (!result) {
return null
}
// Determine the min and max for line1 and line2 for both x and y
const line1MinX = Math.min(line1.x1, line1.x2)
const line1MaxX = Math.max(line1.x1, line1.x2)
const line2MinX = Math.min(line2.x1, line2.x2)
const line2MaxX = Math.max(line2.x1, line2.x2)
const line1MinY = Math.min(line1.y1, line1.y2)
const line1MaxY = Math.max(line1.y1, line1.y2)
const line2MinY = Math.min(line2.y1, line2.y2)
const line2MaxY = Math.max(line2.y1, line2.y2)
// Check if the intersection X and Y are within the range of both lines
if (
result[0] >= line1MinX &&
result[0] <= line1MaxX &&
result[0] >= line2MinX &&
result[0] <= line2MaxX &&
result[1] >= line1MinY &&
result[1] <= line1MaxY &&
result[1] >= line2MinY &&
result[1] <= line2MaxY
) {
return { x: Math.round(result[0]), y: Math.round(result[1]) }
} else {
return null // Intersection is out of range
}
}
export function findOrthogonalPoint(line1, line2) {
// Calculate the intersection point of two lines
const intersectionX =
((line1.x1 * line2.y1 - line1.y1 * line2.x1) * (line2.x2 - line2.x1) - (line1.x1 - line1.x2) * (line2.x2 * line2.y2 - line2.y1 * line2.x1)) /
((line1.x1 - line1.x2) * (line2.y1 - line2.y2) - (line1.y1 - line1.y2) * (line2.x2 - line2.x1))
const intersectionY =
((line1.x1 * line2.y1 - line1.y1 * line2.x1) * (line2.y2 - line2.y1) - (line1.y1 - line1.y2) * (line2.x2 * line2.y2 - line2.y1 * line2.x1)) /
((line1.x1 - line1.x2) * (line2.y1 - line2.y2) - (line1.y1 - line1.y2) * (line2.x2 - line2.x1))
return { x: intersectionX, y: intersectionY }
}
/**
* points배열을 입력받아 반시계방향으로 정렬된 points를 반환합니다.
* @param points
*/
export const sortedPoints = (points) => {
const copyPoints = [...points]
copyPoints.forEach((point) => {
point.x1 = point.x
point.y1 = point.y
const nextPoint = copyPoints[(copyPoints.indexOf(point) + 1) % copyPoints.length]
point.x2 = nextPoint.x
point.y2 = nextPoint.y
})
// copyPoint에서 x1, y1 값을 기준으로 시작 인덱스
const startIndex = getStartIndex(copyPoints)
const startDirection = getDirectionByPoint(
{ x: copyPoints[startIndex].x1, y: copyPoints[startIndex].y1 },
{ x: copyPoints[startIndex].x2, y: copyPoints[startIndex].y2 },
)
const resultPoints = [copyPoints[startIndex]]
let currentPoint = copyPoints[startIndex]
switch (startDirection) {
case 'right': {
copyPoints.forEach((point, index) => {
if (index === startIndex) return
const nextPoint = copyPoints.find((p) => p.x2 === currentPoint.x && p.y2 === currentPoint.y)
resultPoints.push(nextPoint)
currentPoint = nextPoint
})
break
}
case 'bottom': {
copyPoints.forEach((point, index) => {
if (index === startIndex) return
const nextPoint = copyPoints.find((p) => p.x1 === currentPoint.x2 && p.y1 === currentPoint.y2)
resultPoints.push(nextPoint)
currentPoint = nextPoint
})
break
}
}
return resultPoints.map((point) => {
return { x: point.x, y: point.y }
})
}
export const sortedPointLessEightPoint = (points) => {
const copyPoints = [...points]
//points를 x,y좌표를 기준으로 정렬합니다.
copyPoints.sort((a, b) => {
if (a.x === b.x) {
return a.y - b.y
}
return a.x - b.x
})
const resultPoints = [copyPoints[0]]
let index = 1
let currentPoint = { ...copyPoints[0] }
copyPoints.splice(0, 1)
while (index < points.length) {
if (index === points.length - 1) {
resultPoints.push(copyPoints[0])
index++
break
} else if (index % 2 === 0) {
// 짝수번째는 y값이 같은 점을 찾는다.
for (let i = 0; i < copyPoints.length; i++) {
// y값이 같은 point가 많은 경우 그 중 x값이 가장 큰걸 찾는다.
let temp = copyPoints.filter((point) => point.y === currentPoint.y)
if (temp.length === 0) {
// temp가 비어있을 경우 copyPoints에서 가장 가까운 점을 찾는다.
temp = Array.of(findClosestPointByY(currentPoint, copyPoints))
}
// temp중 x값이 가장 가까운 값
const min = temp.reduce((prev, current) => (Math.abs(current.x - currentPoint.x) <= Math.abs(prev.x - currentPoint.x) ? current : prev))
resultPoints.push(min)
currentPoint = min
copyPoints.splice(copyPoints.indexOf(min), 1)
index++
break
}
} else {
// 홀수번째는 x값이 같은 점을 찾는다.
for (let i = 0; i < copyPoints.length; i++) {
// x값이 같은 point가 많은 경우 그 중 y값이 가장 큰걸 찾는다.
let temp = copyPoints.filter((point) => point.x === currentPoint.x)
if (temp.length === 0) {
// temp가 비어있을 경우 copyPoints에서 가장 가까운 점을 찾는다.
temp = Array.of(findClosestPointByX(currentPoint, copyPoints))
}
// temp중 y값이 가장 가까운 값
const min = temp.reduce((prev, current) => (Math.abs(current.y - currentPoint.y) <= Math.abs(prev.y - currentPoint.y) ? current : prev))
resultPoints.push(min)
currentPoint = min
copyPoints.splice(copyPoints.indexOf(min), 1)
index++
break
}
}
}
return resultPoints
}
/**
* point가 선 위에 있는지 확인
* @param line
* @param point
* @returns {boolean}
*/
// 직선의 방정식.
// 방정식은 ax + by + c = 0이며, 점의 좌표를 대입하여 계산된 값은 직선과 점 사이의 관계를 나타낸다.
export function isPointOnLine(line, point) {
const a = line.y2 - line.y1
const b = line.x1 - line.x2
const c = line.x2 * line.y1 - line.x1 * line.y2
return a * point.x + b * point.y + c === 0
}
/**
* 점과 가까운 line 찾기
* @param point
* @param lines {Array}
* @returns {null}
*/
export function findClosestLineToPoint(point, lines) {
let closestLine = null
let closestDistance = Infinity
for (const line of lines) {
const distance = calculateDistance(point, line)
if (distance < closestDistance) {
closestDistance = distance
closestLine = line
}
}
return closestLine
}
/**
* x값이 가장 가까운 점
* @param targetPoint
* @param points
* @returns {*|null}
*/
function findClosestPointByX(targetPoint, points) {
if (points.length === 0) {
return null // Return null if the points array is empty
}
let closestPoint = points[0]
let smallestDistance = Math.abs(targetPoint.x - points[0].x)
for (let i = 1; i < points.length; i++) {
const currentDistance = Math.abs(targetPoint.x - points[i].x)
if (currentDistance < smallestDistance) {
smallestDistance = currentDistance
closestPoint = points[i]
}
}
return closestPoint
}
/**
* y값이 가장 가까운 점
* @param targetPoint
* @param points
* @returns {*|null}
*/
function findClosestPointByY(targetPoint, points) {
if (points.length === 0) {
return null // Return null if the points array is empty
}
let closestPoint = points[0]
let smallestDistance = Math.abs(targetPoint.y - points[0].y)
for (let i = 1; i < points.length; i++) {
const currentDistance = Math.abs(targetPoint.y - points[i].y)
if (currentDistance < smallestDistance) {
smallestDistance = currentDistance
closestPoint = points[i]
}
}
return closestPoint
}
/**
* 주어진 점에서 points 배열 중 가장 가까운 점을 찾는 함수입니다.
* @param {Object} targetPoint - { x: number, y: number } 형태의 대상 점 객체
* @param {Array} points - { x: number, y: number } 형태의 점 객체들의 배열
* @returns {Object} targetPoint에 가장 가까운 점 객체
*/
export function findClosestPoint(targetPoint, points) {
if (points.length === 0) {
return null // points 배열이 비어있으면 null 반환
}
let closestPoint = points[0]
let smallestDistance = calculateDistancePoint(targetPoint, closestPoint)
for (let i = 1; i < points.length; i++) {
const currentDistance = calculateDistancePoint(targetPoint, points[i])
if (currentDistance < smallestDistance) {
smallestDistance = currentDistance
closestPoint = points[i]
}
}
return closestPoint
}
/**
* 점과 직선사이의 최단 거리
* @param point
* @param line
* @returns {number}
*/
export function calculateDistance(point, line) {
const x1 = line.x1
const y1 = line.y1
const x2 = line.x2
const y2 = line.y2
const x0 = point.x
const y0 = point.y
const numerator = Math.abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1)
const denominator = Math.sqrt(Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2))
return numerator / denominator
}
/**
* 두 점 사이의 거리를 계산하는 함수입니다.
* @param {Object} point1 - 첫 번째 점 { x: number, y: number }
* @param {Object} point2 - 두 번째 점 { x: number, y: number }
* @returns {number} 두 점 사이의 거리
*/
function calculateDistancePoint(point1, point2) {
return Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2))
}
/**
* {x, y} 형태의 배열을 받아 중복된 점을 제거하는 함수
* @param points
* @returns {*[]}
*/
export function removeDuplicatePoints(points) {
const uniquePoints = []
const seen = new Set()
points.forEach((point) => {
const identifier = `${point.x}:${point.y}`
if (!seen.has(identifier)) {
seen.add(identifier)
uniquePoints.push(point)
}
})
return uniquePoints
}
/**
* x,y가 다르면서 가장 가까운 점
* @param targetPoint
* @param points
* @returns {null}
*/
export function findClosestPointWithDifferentXY(targetPoint, points) {
let closestPoint = null
let smallestDistance = Infinity
points.forEach((point) => {
if (point.x !== targetPoint.x && point.y !== targetPoint.y) {
const distance = Math.sqrt(Math.pow(point.x - targetPoint.x, 2) + Math.pow(point.y - targetPoint.y, 2))
if (distance < smallestDistance) {
smallestDistance = distance
closestPoint = point
}
}
})
return closestPoint
}