dev #364
@ -6,6 +6,7 @@ import { QPolygon } from '@/components/fabric/QPolygon'
|
||||
import * as turf from '@turf/turf'
|
||||
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
|
||||
import Big from 'big.js'
|
||||
import { canvas } from 'framer-motion/m'
|
||||
|
||||
const TWO_PI = Math.PI * 2
|
||||
|
||||
@ -510,11 +511,7 @@ export const drawGableRoof = (roofId, canvas, textMode) => {
|
||||
const baseLines = wall.baseLines.filter((line) => line.attributes.planeSize > 0)
|
||||
const baseLinePoints = baseLines.map((line) => ({ x: line.x1, y: line.y1 }))
|
||||
|
||||
/** baseLine을 기준으로 확인용 polygon 작성 */
|
||||
const checkWallPolygon = new QPolygon(baseLinePoints, {})
|
||||
|
||||
const eavesLines = baseLines.filter((line) => line.attributes.type === LINE_TYPE.WALLLINE.EAVES)
|
||||
const gableLines = baseLines.filter((line) => line.attributes.type === LINE_TYPE.WALLLINE.GABLE)
|
||||
|
||||
const ridgeLines = []
|
||||
const innerLines = []
|
||||
@ -625,6 +622,7 @@ export const drawGableRoof = (roofId, canvas, textMode) => {
|
||||
isDiagonal,
|
||||
directionVector: { x: dx / length, y: dy / length },
|
||||
roofLine: currentRoof.roofLine,
|
||||
roofVector,
|
||||
}
|
||||
}
|
||||
|
||||
@ -794,73 +792,6 @@ export const drawGableRoof = (roofId, canvas, textMode) => {
|
||||
return { forwardLines, backwardLines }
|
||||
}
|
||||
|
||||
/**
|
||||
* 라인의 지붕 면을 찾는다.
|
||||
* @param currentLine
|
||||
* @returns {*}
|
||||
*/
|
||||
const findCurrentRoof = (currentLine) => {
|
||||
const analyze = analyzeEavesLine(currentLine)
|
||||
const originPoint = currentLine.attributes.originPoint
|
||||
const midX = (originPoint.x1 + originPoint.x2) / 2
|
||||
const midY = (originPoint.y1 + originPoint.y2) / 2
|
||||
const offset = currentLine.attributes.offset
|
||||
|
||||
let currentRoof
|
||||
let roofFindVector = { x: 0, y: 0 }
|
||||
const checkRoofLines = roof.lines.filter((roof) => {
|
||||
const dx = Big(roof.x2).minus(Big(roof.x1)).toNumber()
|
||||
const dy = Big(roof.y2).minus(Big(roof.y1)).toNumber()
|
||||
const length = Math.sqrt(dx * dx + dy * dy)
|
||||
const directionVector = { x: dx / length, y: dy / length }
|
||||
return analyze.directionVector.x === directionVector.x && analyze.directionVector.y === directionVector.y
|
||||
})
|
||||
|
||||
if (analyze.isHorizontal) {
|
||||
const checkPoint = { x: midX, y: midY + offset }
|
||||
if (wall.inPolygon(checkPoint)) {
|
||||
roofFindVector = { x: 0, y: -1 }
|
||||
} else {
|
||||
roofFindVector = { x: 0, y: 1 }
|
||||
}
|
||||
}
|
||||
if (analyze.isVertical) {
|
||||
const checkPoint = { x: midX + offset, y: midY }
|
||||
if (wall.inPolygon(checkPoint)) {
|
||||
roofFindVector = { x: -1, y: 0 }
|
||||
} else {
|
||||
roofFindVector = { x: 1, y: 0 }
|
||||
}
|
||||
}
|
||||
const findEdge = { vertex1: { x: midX, y: midY }, vertex2: { x: midX + roofFindVector.x * offset, y: midY + roofFindVector.y * offset } }
|
||||
const edgeDx = Big(findEdge.vertex2.x).minus(Big(findEdge.vertex1.x)).toNumber()
|
||||
const edgeDy = Big(findEdge.vertex2.y).minus(Big(findEdge.vertex1.y)).toNumber()
|
||||
const edgeLength = Math.sqrt(edgeDx * edgeDx + edgeDy * edgeDy)
|
||||
const edgeVector = { x: edgeDx / edgeLength, y: edgeDy / edgeLength }
|
||||
|
||||
const intersectRoofLines = []
|
||||
checkRoofLines.forEach((roofLine) => {
|
||||
const lineEdge = { vertex1: { x: roofLine.x1, y: roofLine.y1 }, vertex2: { x: roofLine.x2, y: roofLine.y2 } }
|
||||
const intersect = edgesIntersection(lineEdge, findEdge)
|
||||
if (intersect) {
|
||||
const intersectDx = Big(intersect.x).minus(Big(findEdge.vertex1.x)).toNumber()
|
||||
const intersectDy = Big(intersect.y).minus(Big(findEdge.vertex1.y)).toNumber()
|
||||
const intersectLength = Math.sqrt(intersectDx * intersectDx + intersectDy * intersectDy)
|
||||
const intersectVector = { x: intersectDx / intersectLength, y: intersectDy / intersectLength }
|
||||
if (edgeVector.x === intersectVector.x && edgeVector.y === intersectVector.y) {
|
||||
intersectRoofLines.push({ roofLine, intersect, length: intersectLength })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
currentRoof = intersectRoofLines.find((roof) => isPointOnLineNew(roof.roofLine, roof.intersect))
|
||||
if (!currentRoof) {
|
||||
currentRoof = intersectRoofLines.sort((a, b) => a.length - b.length)[0]
|
||||
}
|
||||
|
||||
return currentRoof
|
||||
}
|
||||
|
||||
/**
|
||||
* 지점 사이의 길이와 지점별 각도에 따라서 교점까지의 길이를 구한다.
|
||||
* @param distance
|
||||
@ -879,7 +810,59 @@ export const drawGableRoof = (roofId, canvas, textMode) => {
|
||||
return { a, b }
|
||||
}
|
||||
|
||||
/**
|
||||
* polygon에 line이 겹쳐지는 구간을 확인.
|
||||
* @param polygon
|
||||
* @param linePoints
|
||||
* @returns
|
||||
*/
|
||||
const findPloygonLineOverlap = (polygon, linePoints) => {
|
||||
const polygonPoints = polygon.points
|
||||
const checkLine = {
|
||||
x1: linePoints[0],
|
||||
y1: linePoints[1],
|
||||
x2: linePoints[2],
|
||||
y2: linePoints[3],
|
||||
}
|
||||
let startPoint = { x: checkLine.x1, y: checkLine.y1 }
|
||||
let endPoint = { x: checkLine.x2, y: checkLine.y2 }
|
||||
const lineEdge = { vertex1: startPoint, vertex2: endPoint }
|
||||
|
||||
const checkPolygon = new QPolygon(polygonPoints, {})
|
||||
|
||||
const isStartInside = checkPolygon.inPolygon(startPoint)
|
||||
const isEndInside = checkPolygon.inPolygon(endPoint)
|
||||
|
||||
const intersections = []
|
||||
|
||||
checkPolygon.lines.forEach((line) => {
|
||||
const edge = { vertex1: { x: line.x1, y: line.y1 }, vertex2: { x: line.x2, y: line.y2 } }
|
||||
const intersect = edgesIntersection(edge, lineEdge)
|
||||
if (
|
||||
intersect &&
|
||||
isPointOnLineNew(line, intersect) &&
|
||||
isPointOnLineNew(checkLine, intersect) &&
|
||||
!(Math.abs(startPoint.x - intersect.x) < 1 && Math.abs(startPoint.y - intersect.y) < 1) &&
|
||||
!(Math.abs(endPoint.x - intersect.x) < 1 && Math.abs(endPoint.y - intersect.y) < 1)
|
||||
) {
|
||||
intersections.push({ intersect, dist: Math.sqrt(Math.pow(startPoint.x - intersect.x, 2) + Math.pow(startPoint.y - intersect.y, 2)) })
|
||||
}
|
||||
})
|
||||
|
||||
if (intersections.length > 0) {
|
||||
intersections.sort((a, b) => a.dist - b.dist)
|
||||
let i = 0
|
||||
if (!isStartInside) {
|
||||
startPoint = { x: intersections[0].intersect.x, y: intersections[0].intersect.y }
|
||||
i++
|
||||
}
|
||||
endPoint = { x: intersections[i].intersect.x, y: intersections[i].intersect.y }
|
||||
}
|
||||
return [startPoint.x, startPoint.y, endPoint.x, endPoint.y]
|
||||
}
|
||||
|
||||
const { forwardLines, backwardLines } = analyzeAllEavesLines(eavesLines)
|
||||
|
||||
forwardLines.forEach((current) => {
|
||||
const currentLine = current.eaves
|
||||
const analyze = current.analyze
|
||||
@ -894,39 +877,62 @@ export const drawGableRoof = (roofId, canvas, textMode) => {
|
||||
const y1 = analyze.startPoint.y
|
||||
const y2 = analyze.endPoint.y
|
||||
|
||||
const checkLine = new fabric.Line([analyze.startPoint.x, analyze.startPoint.y, analyze.endPoint.x, analyze.endPoint.y], {
|
||||
stroke: 'red',
|
||||
strokeWidth: 4,
|
||||
parentId: roofId,
|
||||
name: 'check',
|
||||
})
|
||||
canvas.add(checkLine)
|
||||
canvas.renderAll()
|
||||
const lineVector = { x: 0, y: 0 }
|
||||
if (analyze.isHorizontal) {
|
||||
lineVector.x = Math.sign(analyze.roofLine.y1 - currentLine.attributes.originPoint.y1)
|
||||
}
|
||||
if (analyze.isVertical) {
|
||||
lineVector.y = Math.sign(analyze.roofLine.x1 - currentLine.attributes.originPoint.x1)
|
||||
}
|
||||
|
||||
const overlapLines = []
|
||||
backwardLines.forEach((backward) => {
|
||||
const findBackWardLines = backwardLines.filter((backward) => {
|
||||
const backwardVector = { x: 0, y: 0 }
|
||||
if (analyze.isHorizontal) {
|
||||
backwardVector.x = Math.sign(analyze.roofLine.y1 - backward.analyze.startPoint.y)
|
||||
}
|
||||
if (analyze.isVertical) {
|
||||
backwardVector.y = Math.sign(analyze.roofLine.x1 - backward.analyze.startPoint.x)
|
||||
}
|
||||
return backwardVector.x === lineVector.x && backwardVector.y === lineVector.y
|
||||
})
|
||||
const backWardLine = findBackWardLines.find((backward) => {
|
||||
const backX1 = Math.min(backward.eaves.x1, backward.eaves.x2)
|
||||
const backX2 = Math.max(backward.eaves.x1, backward.eaves.x2)
|
||||
const backY1 = Math.min(backward.eaves.y1, backward.eaves.y2)
|
||||
const backY2 = Math.max(backward.eaves.y1, backward.eaves.y2)
|
||||
|
||||
if (
|
||||
analyze.isHorizontal &&
|
||||
((currentX1 <= backX1 && currentX2 >= backX1) ||
|
||||
(currentX1 <= backX2 && currentX2 >= backX2) ||
|
||||
(backX1 < currentX1 && backX1 < currentX2 && backX2 > currentX1 && backX2 > currentX2))
|
||||
) {
|
||||
overlapLines.push(backward)
|
||||
}
|
||||
if (
|
||||
analyze.isVertical &&
|
||||
((currentY1 <= backY1 && currentY2 >= backY1) ||
|
||||
(currentY1 <= backY2 && currentY2 >= backY2) ||
|
||||
(backY1 < currentY1 && backY1 < currentY2 && backY2 > currentY1 && backY2 > currentY2))
|
||||
) {
|
||||
overlapLines.push(backward)
|
||||
}
|
||||
return (
|
||||
(analyze.isHorizontal && Math.abs(currentX1 - backX1) < 1 && Math.abs(currentX2 - backX2) < 1) ||
|
||||
(analyze.isVertical && Math.abs(currentY1 - backY1) < 1 && Math.abs(currentY2 - backY2) < 1)
|
||||
)
|
||||
})
|
||||
if (backWardLine) {
|
||||
overlapLines.push(backWardLine)
|
||||
} else {
|
||||
findBackWardLines.forEach((backward) => {
|
||||
const backX1 = Math.min(backward.eaves.x1, backward.eaves.x2)
|
||||
const backX2 = Math.max(backward.eaves.x1, backward.eaves.x2)
|
||||
const backY1 = Math.min(backward.eaves.y1, backward.eaves.y2)
|
||||
const backY2 = Math.max(backward.eaves.y1, backward.eaves.y2)
|
||||
|
||||
if (
|
||||
analyze.isHorizontal &&
|
||||
((currentX1 <= backX1 && currentX2 >= backX1) ||
|
||||
(currentX1 <= backX2 && currentX2 >= backX2) ||
|
||||
(backX1 < currentX1 && backX1 < currentX2 && backX2 > currentX1 && backX2 > currentX2))
|
||||
) {
|
||||
overlapLines.push(backward)
|
||||
}
|
||||
if (
|
||||
analyze.isVertical &&
|
||||
((currentY1 <= backY1 && currentY2 >= backY1) ||
|
||||
(currentY1 <= backY2 && currentY2 >= backY2) ||
|
||||
(backY1 < currentY1 && backY1 < currentY2 && backY2 > currentY1 && backY2 > currentY2))
|
||||
) {
|
||||
overlapLines.push(backward)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
overlapLines.forEach((overlap) => {
|
||||
const overlapAnalyze = overlap.analyze
|
||||
@ -976,7 +982,8 @@ export const drawGableRoof = (roofId, canvas, textMode) => {
|
||||
ridgePoint.push({ x: pointX, y: y2 })
|
||||
}
|
||||
}
|
||||
ridgeLines.push(drawRidgeLine([ridgePoint[0].x, ridgePoint[0].y, ridgePoint[1].x, ridgePoint[1].y], canvas, roof, textMode))
|
||||
const points = findPloygonLineOverlap(roof, [ridgePoint[0].x, ridgePoint[0].y, ridgePoint[1].x, ridgePoint[1].y], canvas, roofId)
|
||||
ridgeLines.push(drawRidgeLine(points, canvas, roof, textMode))
|
||||
})
|
||||
|
||||
canvas
|
||||
@ -986,96 +993,408 @@ export const drawGableRoof = (roofId, canvas, textMode) => {
|
||||
canvas.renderAll()
|
||||
})
|
||||
|
||||
/**
|
||||
* currentLine {x1,y1}, {x2,y2} 좌표 안쪽에 있는 마루를 찾는다.
|
||||
* @param currentLine
|
||||
* @param roofVector
|
||||
* @param lines
|
||||
* @param tolerance
|
||||
* @returns {*[]}
|
||||
*/
|
||||
const findInnerRidge = (currentLine, roofVector, lines, tolerance = 1) => {
|
||||
const x1 = Math.min(currentLine.x1, currentLine.x2)
|
||||
const y1 = Math.min(currentLine.y1, currentLine.y2)
|
||||
const x2 = Math.max(currentLine.x1, currentLine.x2)
|
||||
const y2 = Math.max(currentLine.y1, currentLine.y2)
|
||||
const dx = Big(currentLine.x2).minus(Big(currentLine.x1)).toNumber()
|
||||
const dy = Big(currentLine.y2).minus(Big(currentLine.y1)).toNumber()
|
||||
const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180
|
||||
let isHorizontal = false,
|
||||
isVertical = false
|
||||
if (normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance) {
|
||||
isHorizontal = true
|
||||
} else if (Math.abs(normalizedAngle - 90) <= tolerance) {
|
||||
isVertical = true
|
||||
}
|
||||
let innerRidgeLines = []
|
||||
if (isHorizontal) {
|
||||
lines
|
||||
.filter((line) => {
|
||||
const dx = Big(line.x2).minus(Big(line.x1)).toNumber()
|
||||
const dy = Big(line.y2).minus(Big(line.y1)).toNumber()
|
||||
const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180
|
||||
return normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance
|
||||
})
|
||||
.filter((line) => {
|
||||
const minX = Math.min(line.x1, line.x2)
|
||||
const maxX = Math.max(line.x1, line.x2)
|
||||
return x1 <= minX && maxX <= x2 && roofVector.y * -1 === Math.sign(line.y1 - y1)
|
||||
})
|
||||
.forEach((line) => innerRidgeLines.push({ line, dist: Math.abs(currentLine.y1 - line.y1) }))
|
||||
}
|
||||
if (isVertical) {
|
||||
lines
|
||||
.filter((line) => {
|
||||
const dx = Big(line.x2).minus(Big(line.x1)).toNumber()
|
||||
const dy = Big(line.y2).minus(Big(line.y1)).toNumber()
|
||||
const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180
|
||||
return Math.abs(normalizedAngle - 90) <= tolerance
|
||||
})
|
||||
.filter((line) => {
|
||||
const minY = Math.min(line.y1, line.y2)
|
||||
const maxY = Math.max(line.y1, line.y2)
|
||||
return y1 <= minY && maxY <= y2 && roofVector.x * -1 === Math.sign(line.x1 - x1)
|
||||
})
|
||||
.forEach((line) => innerRidgeLines.push({ line, dist: Math.abs(currentLine.x1 - line.x1) }))
|
||||
}
|
||||
innerRidgeLines.sort((a, b) => a.dist - b.dist)
|
||||
|
||||
const ridge1 = innerRidgeLines.find((ridge) => {
|
||||
if (isHorizontal) {
|
||||
const minX = Math.min(ridge.line.x1, ridge.line.x2)
|
||||
return Math.abs(x1 - minX) <= 1
|
||||
}
|
||||
if (isVertical) {
|
||||
const minY = Math.min(ridge.line.y1, ridge.line.y2)
|
||||
return Math.abs(y1 - minY) <= 1
|
||||
}
|
||||
})
|
||||
|
||||
const ridge2 = innerRidgeLines.find((ridge) => {
|
||||
if (isHorizontal) {
|
||||
const maxX = Math.max(ridge.line.x1, ridge.line.x2)
|
||||
return Math.abs(x2 - maxX) <= 1
|
||||
}
|
||||
if (isVertical) {
|
||||
const maxY = Math.max(ridge.line.y1, ridge.line.y2)
|
||||
return Math.abs(y2 - maxY) <= 1
|
||||
}
|
||||
})
|
||||
if (ridge1 === ridge2) {
|
||||
innerRidgeLines = [ridge1]
|
||||
} else {
|
||||
if (ridge1 && ridge2) {
|
||||
let range1, range2
|
||||
if (isHorizontal) {
|
||||
// 수평선: x 범위로 비교
|
||||
range1 = {
|
||||
min: Math.min(ridge1.line.x1, ridge1.line.x2),
|
||||
max: Math.max(ridge1.line.x1, ridge1.line.x2),
|
||||
}
|
||||
range2 = {
|
||||
min: Math.min(ridge2.line.x1, ridge2.line.x2),
|
||||
max: Math.max(ridge2.line.x1, ridge2.line.x2),
|
||||
}
|
||||
} else {
|
||||
// 수직선: y 범위로 비교
|
||||
range1 = {
|
||||
min: Math.min(ridge1.line.y1, ridge1.line.y2),
|
||||
max: Math.max(ridge1.line.y1, ridge1.line.y2),
|
||||
}
|
||||
range2 = {
|
||||
min: Math.min(ridge2.line.y1, ridge2.line.y2),
|
||||
max: Math.max(ridge2.line.y1, ridge2.line.y2),
|
||||
}
|
||||
}
|
||||
// 구간 겹침 확인
|
||||
const overlapStart = Math.max(range1.min, range2.min)
|
||||
const overlapEnd = Math.min(range1.max, range2.max)
|
||||
if (Math.max(0, overlapEnd - overlapStart) > 0) {
|
||||
innerRidgeLines = [ridge1, ridge2]
|
||||
}
|
||||
}
|
||||
}
|
||||
return innerRidgeLines
|
||||
}
|
||||
|
||||
/**
|
||||
* 지붕면을 그린다.
|
||||
* @param currentLine
|
||||
*/
|
||||
const drawRoofPlane = (currentLine) => {
|
||||
const currentDegree = getDegreeByChon(currentLine.eaves.attributes.pitch)
|
||||
const analyze = currentLine.analyze
|
||||
const checkLine = new fabric.Line([analyze.startPoint.x, analyze.startPoint.y, analyze.endPoint.x, analyze.endPoint.y], {
|
||||
stroke: 'red',
|
||||
strokeWidth: 4,
|
||||
parentId: roofId,
|
||||
name: 'check',
|
||||
})
|
||||
canvas.add(checkLine)
|
||||
canvas.renderAll()
|
||||
|
||||
const innerRidgeLines = []
|
||||
if (analyze.isHorizontal) {
|
||||
ridgeLines
|
||||
.filter((ridgeLine) => {
|
||||
const tolerance = 1
|
||||
const dx = Big(ridgeLine.x2).minus(Big(ridgeLine.x1)).toNumber()
|
||||
const dy = Big(ridgeLine.y2).minus(Big(ridgeLine.y1)).toNumber()
|
||||
const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180
|
||||
return normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance
|
||||
})
|
||||
.filter((ridgeLine) => {
|
||||
const minX = Math.min(analyze.startPoint.x, analyze.endPoint.x)
|
||||
const maxX = Math.max(analyze.startPoint.x, analyze.endPoint.x)
|
||||
const ridgeLineX = (ridgeLine.x1 + ridgeLine.x2) / 2
|
||||
return ridgeLineX >= minX && ridgeLineX <= maxX
|
||||
})
|
||||
.forEach((ridgeLine) => innerRidgeLines.push(ridgeLine))
|
||||
}
|
||||
if (analyze.isVertical) {
|
||||
ridgeLines
|
||||
.filter((ridgeLine) => {
|
||||
const tolerance = 1
|
||||
const dx = Big(ridgeLine.x2).minus(Big(ridgeLine.x1)).toNumber()
|
||||
const dy = Big(ridgeLine.y2).minus(Big(ridgeLine.y1)).toNumber()
|
||||
const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180
|
||||
return Math.abs(normalizedAngle - 90) <= tolerance
|
||||
})
|
||||
.filter((ridgeLine) => {
|
||||
const minY = Math.min(analyze.startPoint.y, analyze.endPoint.y)
|
||||
const maxY = Math.max(analyze.startPoint.y, analyze.endPoint.y)
|
||||
const ridgeLineY = (ridgeLine.y1 + ridgeLine.y2) / 2
|
||||
return ridgeLineY >= minY && ridgeLineY <= maxY
|
||||
})
|
||||
.forEach((ridgeLine) => innerRidgeLines.push(ridgeLine))
|
||||
}
|
||||
// 현재라인 안쪽의 마루를 filter하여 계산에 사용.
|
||||
const innerRidgeLines = findInnerRidge(
|
||||
{ x1: analyze.startPoint.x, y1: analyze.startPoint.y, x2: analyze.endPoint.x, y2: analyze.endPoint.y },
|
||||
analyze.roofVector,
|
||||
ridgeLines,
|
||||
1,
|
||||
)
|
||||
|
||||
innerRidgeLines.forEach((ridgeLine) => {
|
||||
const checkLine = new fabric.Line([ridgeLine.x1, ridgeLine.y1, ridgeLine.x2, ridgeLine.y2], {
|
||||
stroke: 'red',
|
||||
strokeWidth: 4,
|
||||
parentId: roofId,
|
||||
name: 'check',
|
||||
// 안쪽의 마루가 있는 경우 마루의 연결 포인트를 설정
|
||||
if (innerRidgeLines.length > 0) {
|
||||
const innerRidgePoints = innerRidgeLines
|
||||
.map((innerRidgeLine) => [
|
||||
{ x: innerRidgeLine.line.x1, y: innerRidgeLine.line.y1 },
|
||||
{ x: innerRidgeLine.line.x2, y: innerRidgeLine.line.y2 },
|
||||
])
|
||||
.flat()
|
||||
const ridgeMinPoint = innerRidgePoints.reduce(
|
||||
(min, curr) => (analyze.isHorizontal ? (curr.x < min.x ? curr : min) : curr.y < min.y ? curr : min),
|
||||
innerRidgePoints[0],
|
||||
)
|
||||
const ridgeMaxPoint = innerRidgePoints.reduce(
|
||||
(max, curr) => (analyze.isHorizontal ? (curr.x > max.x ? curr : max) : curr.y > max.y ? curr : max),
|
||||
innerRidgePoints[0],
|
||||
)
|
||||
|
||||
const roofPlanePoint = []
|
||||
innerRidgeLines
|
||||
.sort((a, b) => {
|
||||
const line1 = a.line
|
||||
const line2 = b.line
|
||||
if (analyze.isHorizontal) {
|
||||
return line1.x1 - line2.x1
|
||||
}
|
||||
if (analyze.isVertical) {
|
||||
return line1.y1 - line2.y1
|
||||
}
|
||||
})
|
||||
.forEach((ridge, index) => {
|
||||
const isFirst = index === 0
|
||||
const isLast = index === innerRidgeLines.length - 1
|
||||
const line = ridge.line
|
||||
if (isFirst) {
|
||||
roofPlanePoint.push({ x: line.x1, y: line.y1 })
|
||||
}
|
||||
const nextRidge = innerRidgeLines[index + 1]
|
||||
if (nextRidge) {
|
||||
const nextLine = nextRidge.line
|
||||
let range1, range2
|
||||
if (analyze.isHorizontal) {
|
||||
// 수평선: x 범위로 비교
|
||||
range1 = {
|
||||
min: Math.min(line.x1, line.x2),
|
||||
max: Math.max(line.x1, line.x2),
|
||||
}
|
||||
range2 = {
|
||||
min: Math.min(nextLine.x1, nextLine.x2),
|
||||
max: Math.max(nextLine.x1, nextLine.x2),
|
||||
}
|
||||
// 겹쳐지는 구간
|
||||
const overlapX = [Math.max(range1.min, range2.min), Math.min(range1.max, range2.max)]
|
||||
let linePoints = [
|
||||
{ x: line.x1, y: line.y1 },
|
||||
{ x: line.x2, y: line.y2 },
|
||||
{ x: nextLine.x1, y: nextLine.y1 },
|
||||
{ x: nextLine.x2, y: nextLine.y2 },
|
||||
].filter((point) => overlapX.includes(point.x))
|
||||
const firstPoint =
|
||||
ridge.dist > nextRidge.dist
|
||||
? linePoints.find((point) => point.x === line.x1 || point.x === line.x2)
|
||||
: linePoints.find((point) => point.x === nextLine.x1 || point.x === nextLine.x2)
|
||||
const lastPoint = linePoints.find((point) => point !== firstPoint)
|
||||
roofPlanePoint.push({ x: firstPoint.x, y: firstPoint.y }, { x: firstPoint.x, y: lastPoint.y })
|
||||
} else {
|
||||
// 수직선: y 범위로 비교
|
||||
range1 = {
|
||||
min: Math.min(line.y1, line.y2),
|
||||
max: Math.max(line.y1, line.y2),
|
||||
}
|
||||
range2 = {
|
||||
min: Math.min(nextLine.y1, nextLine.y2),
|
||||
max: Math.max(nextLine.y1, nextLine.y2),
|
||||
}
|
||||
//겹쳐지는 구간
|
||||
const overlapY = [Math.max(range1.min, range2.min), Math.min(range1.max, range2.max)]
|
||||
let linePoints = [
|
||||
{ x: line.x1, y: line.y1 },
|
||||
{ x: line.x2, y: line.y2 },
|
||||
{ x: nextLine.x1, y: nextLine.y1 },
|
||||
{ x: nextLine.x2, y: nextLine.y2 },
|
||||
].filter((point) => overlapY.includes(point.y))
|
||||
const firstPoint =
|
||||
ridge.dist > nextRidge.dist
|
||||
? linePoints.find((point) => point.y === line.y1 || point.y === line.y2)
|
||||
: linePoints.find((point) => point.y === nextLine.y1 || point.y === nextLine.y2)
|
||||
const lastPoint = linePoints.find((point) => point !== firstPoint)
|
||||
roofPlanePoint.push({ x: firstPoint.x, y: firstPoint.y }, { x: lastPoint.x, y: firstPoint.y })
|
||||
}
|
||||
}
|
||||
if (isLast) {
|
||||
roofPlanePoint.push({ x: line.x2, y: line.y2 })
|
||||
}
|
||||
})
|
||||
const maxDistRidge = innerRidgeLines.reduce((max, curr) => (curr.dist > max.dist ? curr : max), innerRidgeLines[0])
|
||||
// 지붕선에 맞닫는 포인트를 찾아서 지붕선의 모양에 따라 추가 한다.
|
||||
let innerRoofLines = roof.lines
|
||||
.filter((line) => {
|
||||
//1.지붕선이 현재 라인의 안쪽에 있는지 판단
|
||||
const tolerance = 1
|
||||
const dx = Big(line.x2).minus(Big(line.x1)).toNumber()
|
||||
const dy = Big(line.y2).minus(Big(line.y1)).toNumber()
|
||||
const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180
|
||||
let isHorizontal = false,
|
||||
isVertical = false
|
||||
if (normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance) {
|
||||
isHorizontal = true
|
||||
} else if (Math.abs(normalizedAngle - 90) <= tolerance) {
|
||||
isVertical = true
|
||||
}
|
||||
const minX = Math.min(line.x1, line.x2)
|
||||
const maxX = Math.max(line.x1, line.x2)
|
||||
const minY = Math.min(line.y1, line.y2)
|
||||
const maxY = Math.max(line.y1, line.y2)
|
||||
return (
|
||||
(analyze.isHorizontal && isHorizontal && analyze.startPoint.x <= minX && maxX <= analyze.endPoint.x) ||
|
||||
(analyze.isVertical && isVertical && analyze.startPoint.y <= minY && maxY <= analyze.endPoint.y)
|
||||
)
|
||||
})
|
||||
.filter((line) => {
|
||||
//2.지붕선이 현재 라인의 바깥에 있는지 확인.
|
||||
const dx = Big(line.x2).minus(Big(line.x1)).toNumber()
|
||||
const dy = Big(line.y2).minus(Big(line.y1)).toNumber()
|
||||
const length = Math.sqrt(dx * dx + dy * dy)
|
||||
const lineVector = { x: 0, y: 0 }
|
||||
if (analyze.isHorizontal) {
|
||||
// lineVector.y = Math.sign(line.y1 - currentLine.eaves.attributes.originPoint.y1)
|
||||
lineVector.y = Math.sign(line.y1 - maxDistRidge.line.y1)
|
||||
} else if (analyze.isVertical) {
|
||||
// lineVector.x = Math.sign(line.x1 - currentLine.eaves.attributes.originPoint.x1)
|
||||
lineVector.x = Math.sign(line.x1 - maxDistRidge.line.x1)
|
||||
}
|
||||
return (
|
||||
analyze.roofVector.x === lineVector.x &&
|
||||
analyze.roofVector.y === lineVector.y &&
|
||||
analyze.directionVector.x === dx / length &&
|
||||
analyze.directionVector.y === dy / length
|
||||
)
|
||||
})
|
||||
|
||||
// 패턴 방향에 따라 최소지점, 최대지점의 포인트에서 지붕선 방향에 만나는 포인트를 찾기위한 vector
|
||||
const checkMinVector = {
|
||||
vertex1: { x: ridgeMinPoint.x, y: ridgeMinPoint.y },
|
||||
vertex2: { x: ridgeMinPoint.x + analyze.roofVector.x, y: ridgeMinPoint.y + analyze.roofVector.y },
|
||||
}
|
||||
const checkMaxVector = {
|
||||
vertex1: { x: ridgeMaxPoint.x, y: ridgeMaxPoint.y },
|
||||
vertex2: { x: ridgeMaxPoint.x + analyze.roofVector.x, y: ridgeMaxPoint.y + analyze.roofVector.y },
|
||||
}
|
||||
|
||||
// 최소, 최대 지점에 만나는 포인트들에 대한 정보
|
||||
const roofMinPoint = [],
|
||||
roofMaxPoint = []
|
||||
|
||||
innerRoofLines.forEach((line) => {
|
||||
const lineVector = { vertex1: { x: line.x1, y: line.y1 }, vertex2: { x: line.x2, y: line.y2 } }
|
||||
const minIntersect = edgesIntersection(checkMinVector, lineVector)
|
||||
const maxIntersect = edgesIntersection(checkMaxVector, lineVector)
|
||||
if (minIntersect) {
|
||||
const distance = Math.abs(
|
||||
analyze.isHorizontal ? ridgeMinPoint.x - Math.min(line.x1, line.x2) : ridgeMinPoint.y - Math.min(line.y1, line.y2),
|
||||
)
|
||||
roofMinPoint.push({ x: minIntersect.x, y: minIntersect.y, isPointOnLine: isPointOnLineNew(line, minIntersect), distance, line })
|
||||
}
|
||||
if (maxIntersect) {
|
||||
const distance = Math.abs(
|
||||
analyze.isHorizontal ? ridgeMaxPoint.x - Math.max(line.x1, line.x2) : ridgeMaxPoint.y - Math.max(line.y1, line.y2),
|
||||
)
|
||||
roofMaxPoint.push({ x: maxIntersect.x, y: maxIntersect.y, isPointOnLine: isPointOnLineNew(line, maxIntersect), distance, line })
|
||||
}
|
||||
})
|
||||
canvas.add(checkLine)
|
||||
canvas.renderAll()
|
||||
})
|
||||
|
||||
if (innerRidgeLines.length === 1) {
|
||||
const innerRidgeLine = innerRidgeLines[0]
|
||||
if (analyze.isHorizontal) {
|
||||
// 최소지점, 최대지점에 연결되는 지붕선
|
||||
let minRoof = roofMinPoint.find((point) => point.isPointOnLine)
|
||||
let maxRoof = roofMaxPoint.find((point) => point.isPointOnLine)
|
||||
if (!minRoof) {
|
||||
minRoof = roofMinPoint.sort((a, b) => a.distance - b.distance)[0]
|
||||
}
|
||||
if (analyze.isVertical) {
|
||||
if (!maxRoof) {
|
||||
maxRoof = roofMaxPoint.sort((a, b) => a.distance - b.distance)[0]
|
||||
}
|
||||
|
||||
if (minRoof && maxRoof) {
|
||||
// 1. 연결되는 지점의 포인트를 사용한다.
|
||||
roofPlanePoint.push({ x: minRoof.x, y: minRoof.y }, { x: maxRoof.x, y: maxRoof.y })
|
||||
// 2. 최소지점, 최대지점에 연결되는 지붕선이 하나가 아닐경우 연결되는 지점외에 지붕선의 다른 포인트를 추가 해야 한다.
|
||||
if (minRoof.line !== maxRoof.line) {
|
||||
if (analyze.isHorizontal) {
|
||||
Math.abs(minRoof.x - minRoof.line.x1) < Math.abs(minRoof.x - minRoof.line.x2)
|
||||
? roofPlanePoint.push({ x: minRoof.line.x2, y: minRoof.line.y2 })
|
||||
: roofPlanePoint.push({ x: minRoof.line.x1, y: minRoof.line.y1 })
|
||||
Math.abs(maxRoof.x - maxRoof.line.x1) < Math.abs(maxRoof.x - maxRoof.line.x2)
|
||||
? roofPlanePoint.push({ x: maxRoof.line.x2, y: maxRoof.line.y2 })
|
||||
: roofPlanePoint.push({ x: maxRoof.line.x1, y: maxRoof.line.y1 })
|
||||
}
|
||||
if (analyze.isVertical) {
|
||||
Math.abs(minRoof.y - minRoof.line.y1) < Math.abs(minRoof.y - minRoof.line.y2)
|
||||
? roofPlanePoint.push({ x: minRoof.line.x2, y: minRoof.line.y2 })
|
||||
: roofPlanePoint.push({ x: minRoof.line.x1, y: minRoof.line.y1 })
|
||||
Math.abs(maxRoof.y - maxRoof.line.y1) < Math.abs(maxRoof.y - maxRoof.line.y2)
|
||||
? roofPlanePoint.push({ x: maxRoof.line.x2, y: maxRoof.line.y2 })
|
||||
: roofPlanePoint.push({ x: maxRoof.line.x1, y: maxRoof.line.y1 })
|
||||
}
|
||||
// 3.지붕선이 세개 이상일 경우 최소, 최대 연결지점의 지붕선을 제외한 나머지 지붕선은 모든 포인트를 사용한다.
|
||||
const otherRoof = innerRoofLines.filter((line) => line !== minRoof.line && line !== maxRoof.line)
|
||||
otherRoof.forEach((line) => {
|
||||
roofPlanePoint.push({ x: line.x1, y: line.y1 }, { x: line.x2, y: line.y2 })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//각 포인트들을 직교하도록 정렬
|
||||
const sortedPoints = getSortedOrthogonalPoints(roofPlanePoint)
|
||||
sortedPoints.forEach((currPoint, index) => {
|
||||
const nextPoint = sortedPoints[(index + 1) % sortedPoints.length]
|
||||
const points = [currPoint.x, currPoint.y, nextPoint.x, nextPoint.y]
|
||||
const isAlready = ridgeLines.find(
|
||||
(ridge) =>
|
||||
(Math.abs(ridge.x1 - points[0]) < 1 &&
|
||||
Math.abs(ridge.y1 - points[1]) < 1 &&
|
||||
Math.abs(ridge.x2 - points[2]) < 1 &&
|
||||
Math.abs(ridge.y2 - points[3]) < 1) ||
|
||||
(Math.abs(ridge.x1 - points[2]) < 1 &&
|
||||
Math.abs(ridge.y1 - points[3]) < 1 &&
|
||||
Math.abs(ridge.x2 - points[0]) < 1 &&
|
||||
Math.abs(ridge.y2 - points[1]) < 1),
|
||||
)
|
||||
if (isAlready) {
|
||||
return true
|
||||
}
|
||||
|
||||
const tolerance = 1
|
||||
const dx = Big(points[2]).minus(Big(points[0])).toNumber()
|
||||
const dy = Big(points[3]).minus(Big(points[1])).toNumber()
|
||||
const normalizedAngle = Math.abs((Math.atan2(dy, dx) * 180) / Math.PI) % 180
|
||||
let isHorizontal = false,
|
||||
isVertical = false
|
||||
if (normalizedAngle < tolerance || normalizedAngle >= 180 - tolerance) {
|
||||
isHorizontal = true
|
||||
} else if (Math.abs(normalizedAngle - 90) <= tolerance) {
|
||||
isVertical = true
|
||||
}
|
||||
if (analyze.isHorizontal) {
|
||||
//현재라인이 수평선일때
|
||||
if (isHorizontal) {
|
||||
//같은방향 처리
|
||||
innerLines.push(drawRoofLine(points, canvas, roof, textMode))
|
||||
} else {
|
||||
//다른방향 처리
|
||||
drawHipLine(points, canvas, roof, textMode, null, currentDegree, currentDegree)
|
||||
}
|
||||
} else if (analyze.isVertical) {
|
||||
//현재라인이 수직선일때
|
||||
if (isVertical) {
|
||||
//같은방향 처리
|
||||
drawRoofLine(points, canvas, roof, textMode)
|
||||
} else {
|
||||
//다른방향 처리
|
||||
drawHipLine(points, canvas, roof, textMode, null, currentDegree, currentDegree)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
canvas
|
||||
.getObjects()
|
||||
.filter((object) => object.name === 'check')
|
||||
.forEach((object) => canvas.remove(object))
|
||||
canvas.renderAll()
|
||||
}
|
||||
|
||||
forwardLines.forEach((forward) => {
|
||||
const analyze = forward.analyze
|
||||
drawRoofPlane(forward)
|
||||
})
|
||||
backwardLines.forEach((backward) => {
|
||||
const analyze = backward.analyze
|
||||
drawRoofPlane(backward)
|
||||
})
|
||||
canvas
|
||||
.getObjects()
|
||||
.filter((object) => object.name === 'check')
|
||||
.forEach((object) => canvas.remove(object))
|
||||
canvas.renderAll()
|
||||
roof.innerLines.push(...ridgeLines, ...innerLines)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -8919,3 +9238,139 @@ const reCalculateSize = (line) => {
|
||||
)
|
||||
return { planeSize, actualSize }
|
||||
}
|
||||
|
||||
/**
|
||||
* 직교 다각형(축에 평행한 변들로만 구성된 다각형)의 점들을 시계방향으로 정렬
|
||||
* @param points 정렬할 점들의 배열
|
||||
* @returns {[sortedPoints]} 정렬된 점들의 배열
|
||||
*/
|
||||
const getSortedOrthogonalPoints = (points) => {
|
||||
if (points.length < 3) return points
|
||||
/**
|
||||
* @param currentDirection 현재 방향
|
||||
* @param nextDirection 다음 방향
|
||||
* @param isClockWise 흐름 방향
|
||||
* @returns {boolean} 유효한 방향 전환인지 여부
|
||||
*/
|
||||
const isValidNextDirection = (currentDirection, nextDirection, isClockWise = false) => {
|
||||
if (!currentDirection) return true
|
||||
|
||||
let validTransitions
|
||||
if (!isClockWise) {
|
||||
// 반 시계방향 진행 규칙
|
||||
validTransitions = {
|
||||
right: ['up'],
|
||||
down: ['right'],
|
||||
left: ['down'],
|
||||
up: ['left'],
|
||||
}
|
||||
} else {
|
||||
// 시계방향 진행 규칙
|
||||
validTransitions = {
|
||||
right: ['down'],
|
||||
down: ['left'],
|
||||
left: ['up'],
|
||||
up: ['right'],
|
||||
}
|
||||
}
|
||||
return !validTransitions[currentDirection] || validTransitions[currentDirection].includes(nextDirection)
|
||||
}
|
||||
|
||||
// 시작점: 왼쪽 위 점 (x가 최소이면서 y도 최소인 점)
|
||||
const startPoint = points.reduce((min, point) => {
|
||||
if (point.x < min.x || (point.x === min.x && point.y < min.y)) {
|
||||
return point
|
||||
}
|
||||
return min
|
||||
})
|
||||
|
||||
const sortedPoints = [startPoint]
|
||||
const remainingPoints = points.filter((p) => p !== startPoint)
|
||||
|
||||
let currentPoint = startPoint
|
||||
let currentDirection = null
|
||||
|
||||
while (remainingPoints.length > 0) {
|
||||
let nextPoint = null
|
||||
let nextDirection = null
|
||||
let minDistance = Infinity
|
||||
|
||||
// 현재 방향을 고려하여 다음 점 찾기
|
||||
for (const point of remainingPoints) {
|
||||
const dx = point.x - currentPoint.x
|
||||
const dy = point.y - currentPoint.y
|
||||
|
||||
// 직교 다각형이므로 수직 또는 수평 방향만 고려
|
||||
if (Math.abs(dx) < 1e-10 && Math.abs(dy) > 1e-10) {
|
||||
// 수직 이동
|
||||
const direction = dy > 0 ? 'down' : 'up'
|
||||
const distance = Math.abs(dy)
|
||||
|
||||
if (isValidNextDirection(currentDirection, direction) && distance < minDistance) {
|
||||
nextPoint = point
|
||||
nextDirection = direction
|
||||
minDistance = distance
|
||||
}
|
||||
} else if (Math.abs(dy) < 1e-10 && Math.abs(dx) > 1e-10) {
|
||||
// 수평 이동
|
||||
const direction = dx > 0 ? 'right' : 'left'
|
||||
const distance = Math.abs(dx)
|
||||
|
||||
if (isValidNextDirection(currentDirection, direction) && distance < minDistance) {
|
||||
nextPoint = point
|
||||
nextDirection = direction
|
||||
minDistance = distance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!nextPoint) {
|
||||
for (const point of remainingPoints) {
|
||||
const dx = point.x - currentPoint.x
|
||||
const dy = point.y - currentPoint.y
|
||||
|
||||
// 직교 다각형이므로 수직 또는 수평 방향만 고려
|
||||
if (Math.abs(dx) < 1e-10 && Math.abs(dy) > 1e-10) {
|
||||
// 수직 이동
|
||||
const direction = dy > 0 ? 'down' : 'up'
|
||||
const distance = Math.abs(dy)
|
||||
|
||||
if (isValidNextDirection(currentDirection, direction, true) && distance < minDistance) {
|
||||
nextPoint = point
|
||||
nextDirection = direction
|
||||
minDistance = distance
|
||||
}
|
||||
} else if (Math.abs(dy) < 1e-10 && Math.abs(dx) > 1e-10) {
|
||||
// 수평 이동
|
||||
const direction = dx > 0 ? 'right' : 'left'
|
||||
const distance = Math.abs(dx)
|
||||
|
||||
if (isValidNextDirection(currentDirection, direction, true) && distance < minDistance) {
|
||||
nextPoint = point
|
||||
nextDirection = direction
|
||||
minDistance = distance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nextPoint) {
|
||||
sortedPoints.push(nextPoint)
|
||||
remainingPoints.splice(remainingPoints.indexOf(nextPoint), 1)
|
||||
currentPoint = nextPoint
|
||||
currentDirection = nextDirection
|
||||
} else {
|
||||
// 다음 점을 찾을 수 없는 경우, 가장 가까운 점 선택
|
||||
const nearestPoint = remainingPoints.reduce((nearest, point) => {
|
||||
const currentDist = Math.sqrt(Math.pow(point.x - currentPoint.x, 2) + Math.pow(point.y - currentPoint.y, 2))
|
||||
const nearestDist = Math.sqrt(Math.pow(nearest.x - currentPoint.x, 2) + Math.pow(nearest.y - currentPoint.y, 2))
|
||||
return currentDist < nearestDist ? point : nearest
|
||||
})
|
||||
|
||||
sortedPoints.push(nearestPoint)
|
||||
remainingPoints.splice(remainingPoints.indexOf(nearestPoint), 1)
|
||||
currentPoint = nearestPoint
|
||||
currentDirection = null
|
||||
}
|
||||
}
|
||||
return sortedPoints
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user