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, drawGableRoof, drawRidgeRoof, drawShedRoof, toGeoJSON } from '@/util/qpolygon-utils' import * as turf from '@turf/turf' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' import { drawSkeletonRidgeRoof } from '@/util/skeleton-utils' 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, toFixed: 1, initialize: function (points, options, canvas) { this.lines = [] this.texts = [] this.hips = [] this.ridges = [] this.cells = [] this.innerLines = [] this.children = [] this.separatePolygon = [] this.toFixed = options.toFixed ?? 1 this.baseLines = [] // this.colorLines = [] // 소수점 전부 제거 points.forEach((point) => { point.x = Number(point.x.toFixed(this.toFixed)) point.y = Number(point.y.toFixed(this.toFixed)) }) options.selectable = options.selectable ?? true options.sort = options.sort ?? true options.parentId = options.parentId ?? null this.isSortedPoints = false if (!options.sort && points.length <= 8) { points = sortedPointLessEightPoint(points) this.isSortedPoints = true } 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.isSortedPoints = true } } 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.initLines() this.init() this.setShape() const originWidth = this.originWidth ?? this.width const originHeight = this.originHeight ?? this.height this.originWidth = this.angle === 90 || this.angle === 270 ? originHeight : originWidth this.originHeight = this.angle === 90 || this.angle === 270 ? originWidth : originHeight }, 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 }, init: function () { this.addLengthText() this.on('moving', () => { this.initLines() this.addLengthText() this.setCoords() }) this.on('modified', (e) => { this.initLines() this.addLengthText() this.setCoords() }) this.on('selected', () => { Object.keys(this.controls).forEach((controlKey) => { this.setControlVisible(controlKey, false) }) this.set({ hasBorders: false }) }) this.on('removed', () => { // const children = getAllRelatedObjects(this.id, this.canvas) const children = this.canvas.getObjects().filter((obj) => obj.parentId === this.id) children.forEach((child) => { this.canvas.remove(child) //그룹일때 if (child.hasOwnProperty('_objects')) { child._objects.forEach((obj) => { if (obj.hasOwnProperty('texts')) { obj.texts.forEach((text) => { this.canvas?.remove(text) }) } }) } }) }) //QPolygon 좌표 이동시 좌표 재계산 this.on('polygonMoved', () => { //폴리곤일때만 사용 let matrix = this.calcTransformMatrix() let transformedPoints = this.get('points') .map((p) => { return new fabric.Point(p.x - this.pathOffset.x, p.y - this.pathOffset.y) }) .map((p) => { return fabric.util.transformPoint(p, matrix) }) this.points = transformedPoints const { left, top } = this.calcOriginCoords() this.set('pathOffset', { x: left, y: top }) this.setCoords() this.initLines() }) // polygon.fillCell({ width: 50, height: 30, padding: 10 }) }, initLines() { let attributes = null if (this.lines.length > 0) { attributes = this.lines.map((line) => line.attributes) } this.lines = [] this.getCurrentPoints().forEach((point, i) => { const nextPoint = this.getCurrentPoints()[(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: attributes ? attributes[i] : { offset: 0, }, textVisible: false, parent: this, parentId: this.id, direction: getDirectionByPoint(point, nextPoint), idx: i + 1, }) line.startPoint = point line.endPoint = nextPoint this.lines.push(line) this.calculateDegree() }) }, calculateDegree() { const degrees = [] // polygon.lines를 순회하며 각도를 구해 출력 this.lines.forEach((line, idx) => { const dx = line.x2 - line.x1 const dy = line.y2 - line.y1 const rad = Math.atan2(dy, dx) const degree = (rad * 180) / Math.PI degrees.push(degree) }) function isMultipleOf45(degree, epsilon = 1) { return Math.abs(degree % 45) <= epsilon || Math.abs((degree % 45) - 45) <= epsilon } this.isMultipleOf45 = degrees.every((degree) => isMultipleOf45(degree)) }, /** * 보조선 그리기 * @param settingModalFirstOptions */ drawHelpLine(settingModalFirstOptions) { /* innerLines 초기화 */ this.canvas .getObjects() .filter( (obj) => obj.parentId === this.id && obj.name !== POLYGON_TYPE.WALL && obj.name !== POLYGON_TYPE.ROOF && obj.name !== 'lengthText' && obj.name !== 'outerLine' && obj.name !== 'baseLine', // && obj.name !== 'outerLinePoint', ) .forEach((obj) => this.canvas.remove(obj)) this.innerLines = [] this.canvas.renderAll() let textMode = 'plane' const dimensionDisplay = settingModalFirstOptions?.dimensionDisplay.find((opt) => opt.selected).id ? settingModalFirstOptions?.dimensionDisplay.find((opt) => opt.selected).id : 1 switch (dimensionDisplay) { case 1: textMode = 'plane' break case 2: textMode = 'actual' break case 3: textMode = 'none' break } const types = this.lines.map((line) => line.attributes.type) const isGableRoof = function (types) { if (!types.includes(LINE_TYPE.WALLLINE.GABLE)) { return false } const gableTypes = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD] const oddTypes = types.filter((type, i) => i % 2 === 0) const evenTypes = types.filter((type, i) => i % 2 === 1) const oddAllEaves = oddTypes.every((type) => type === LINE_TYPE.WALLLINE.EAVES) const evenAllGable = evenTypes.every((type) => gableTypes.includes(type)) const evenAllEaves = evenTypes.every((type) => type === LINE_TYPE.WALLLINE.EAVES) const oddAllGable = oddTypes.every((type) => gableTypes.includes(type)) return (oddAllEaves && evenAllGable) || (evenAllEaves && oddAllGable) } const isShedRoof = function (types, lines) { const gableTypes = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD] if (!types.includes(LINE_TYPE.WALLLINE.SHED)) { return false } const shedLines = lines.filter((line) => line.attributes?.type === LINE_TYPE.WALLLINE.SHED) const areShedLinesParallel = function (shedLines) { return shedLines.every((shed, i) => { const nextShed = shedLines[(i + 1) % shedLines.length] const angle1 = calculateAngle(shed.startPoint, shed.endPoint) const angle2 = calculateAngle(nextShed.startPoint, nextShed.endPoint) return angle1 === angle2 }) } if (!areShedLinesParallel(shedLines)) { return false } const getParallelEavesLines = function (shedLines, lines) { const eavesLines = lines.filter((line) => line.attributes?.type === LINE_TYPE.WALLLINE.EAVES) const referenceAngle = calculateAngle(shedLines[0].startPoint, shedLines[0].endPoint) return eavesLines.filter((line) => { const eavesAngle = calculateAngle(line.startPoint, line.endPoint) return Math.abs(referenceAngle - eavesAngle) === 180 }) } const parallelEaves = getParallelEavesLines(shedLines, lines) if (parallelEaves.length === 0) { return false } const remainingLines = lines.filter((line) => !shedLines.includes(line) && !parallelEaves.includes(line)) return remainingLines.every((line) => gableTypes.includes(line.attributes.type)) } if (types.every((type) => type === LINE_TYPE.WALLLINE.EAVES)) { // 용마루 -- straight-skeleton console.log('용마루 지붕') //drawRidgeRoof(this.id, this.canvas, textMode) drawSkeletonRidgeRoof(this.id, this.canvas, textMode); } else if (isGableRoof(types)) { // A형, B형 박공 지붕 console.log('패턴 지붕') drawGableRoof(this.id, this.canvas, textMode) } else if (isShedRoof(types, this.lines)) { console.log('한쪽흐름 지붕') drawShedRoof(this.id, this.canvas, textMode) } else { console.log('변별로 설정') drawRidgeRoof(this.id, this.canvas, textMode) } }, addLengthText() { if ([POLYGON_TYPE.MODULE, 'arrow', POLYGON_TYPE.MODULE_SETUP_SURFACE, POLYGON_TYPE.OBJECT_SURFACE].includes(this.name)) { return } if (!this.fontSize) { return } this.canvas ?.getObjects() .filter((obj) => obj.name === 'lengthText' && obj.parentId === this.id) .forEach((text) => { this.canvas.remove(text) }) let points = this.getCurrentPoints() this.texts = [] points.forEach((start, i) => { const end = points[(i + 1) % points.length] const dx = Big(end.x).minus(Big(start.x)) const dy = Big(end.y).minus(Big(start.y)) const length = dx.pow(2).plus(dy.pow(2)).sqrt().times(10).round().toNumber() const direction = getDirectionByPoint(start, end) let left, top if (direction === 'bottom') { left = (start.x + end.x) / 2 - 50 top = (start.y + end.y) / 2 } else if (direction === 'top') { left = (start.x + end.x) / 2 + 30 top = (start.y + end.y) / 2 } else if (direction === 'left') { left = (start.x + end.x) / 2 top = (start.y + end.y) / 2 - 30 } else if (direction === 'right') { left = (start.x + end.x) / 2 top = (start.y + end.y) / 2 + 30 } let midPoint midPoint = new fabric.Point(left, top) const degree = Big(Math.atan2(dy.toNumber(), dx.toNumber())).times(180).div(Math.PI).toNumber() // Create new text object if it doesn't exist const text = new fabric.Text(length.toString(), { 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, actualSize: this.lines[i].attributes?.actualSize, planeSize: this.lines[i].attributes?.planeSize, 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 }, 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 }, inPolygonImproved(point) { const vertices = this.getCurrentPoints() let inside = false const testX = Number(point.x.toFixed(this.toFixed)) const testY = Number(point.y.toFixed(this.toFixed)) for (let i = 0, j = vertices.length - 1; i < vertices.length; j = i++) { const xi = Number(vertices[i].x.toFixed(this.toFixed)) const yi = Number(vertices[i].y.toFixed(this.toFixed)) const xj = Number(vertices[j].x.toFixed(this.toFixed)) const yj = Number(vertices[j].y.toFixed(this.toFixed)) // 점이 정점 위에 있는지 확인 if (Math.abs(xi - testX) <= 0.01 && Math.abs(yi - testY) <= 0.01) { return true } // 점이 선분 위에 있는지 확인 if (this.isPointOnSegment(point, { x: xi, y: yi }, { x: xj, y: yj })) { return true } // Ray casting 알고리즘 - 부동소수점 정밀도 개선 if (yi > testY !== yj > testY) { const denominator = yj - yi if (Math.abs(denominator) > 1e-10) { // 0으로 나누기 방지 const intersection = ((xj - xi) * (testY - yi)) / denominator + xi if (testX < intersection) { inside = !inside } } } } return inside }, isPointOnSegment(point, segStart, segEnd) { const tolerance = 0.1 const dxSegment = segEnd.x - segStart.x const dySegment = segEnd.y - segStart.y const dxPoint = point.x - segStart.x const dyPoint = point.y - segStart.y // 벡터의 외적을 계산하여 점이 선분 위에 있는지 확인 const crossProduct = Math.abs(dxPoint * dySegment - dyPoint * dxSegment) if (crossProduct > tolerance) { return false } // 점이 선분의 범위 내에 있는지 확인 const dotProduct = dxPoint * dxSegment + dyPoint * dySegment const squaredLength = dxSegment * dxSegment + dySegment * dySegment return dotProduct >= 0 && dotProduct <= squaredLength }, setCoords: function () { // 부모 클래스의 setCoords 호출 this.callSuper('setCoords') // QPolygon의 경우 추가 처리 - 항상 강제로 재계산 if (this.canvas) { // 모든 좌표 관련 캐시 초기화 delete this.oCoords delete this.aCoords delete this.__corner // 다시 부모 setCoords 호출 this.callSuper('setCoords') // 한 번 더 강제로 bounding rect 재계산 this._clearCache && this._clearCache() } }, containsPoint: function (point) { // 먼저 좌표 업데이트 this.setCoords() // 캔버스 줌과 viewport transform 고려한 좌표 변환 let localPoint = point if (this.canvas) { const vpt = this.canvas.viewportTransform if (vpt) { // viewport transform 역변환 const inverted = fabric.util.invertTransform(vpt) localPoint = fabric.util.transformPoint(point, inverted) } } // 오브젝트의 transform matrix를 고려한 좌표 변환 const matrix = this.calcTransformMatrix() const invertedMatrix = fabric.util.invertTransform(matrix) const transformedPoint = fabric.util.transformPoint(localPoint, invertedMatrix) // pathOffset을 고려한 최종 좌표 계산 const pathOffset = this.get('pathOffset') const finalPoint = { x: Number((transformedPoint.x + pathOffset.x).toFixed(this.toFixed)), y: Number((transformedPoint.y + pathOffset.y).toFixed(this.toFixed)), } if (this.name === POLYGON_TYPE.ROOF && this.isFixed) { const isInside = this.inPolygonImproved(finalPoint) if (!this.selectable) { this.set('selectable', isInside) } return isInside } else { return this.inPolygonImproved(finalPoint) } }, 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.parentId === this.id) .forEach((text) => { text.set({ visible: isView }) }) }, setScaleX(scale) { this.scaleX = scale this.addLengthText() }, setScaleY(scale) { this.scaleY = scale this.addLengthText() }, calcOriginCoords() { 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)) let left = 0 let top = 0 if (this.originX === 'center') { left = (minX + maxX) / 2 } else if (this.originX === 'left') { left = minX } else if (this.originX === 'right') { left = maxX } if (this.originY === 'center') { top = (minY + maxY) / 2 } else if (this.originY === 'top') { top = minY } else if (this.originY === 'bottom') { top = maxY } return { left, top } }, })