diff --git a/src/components/Roof2.jsx b/src/components/Roof2.jsx index da41280b..93cc12d2 100644 --- a/src/components/Roof2.jsx +++ b/src/components/Roof2.jsx @@ -11,6 +11,7 @@ import { QLine } from '@/components/fabric/QLine' import { getCanvasState, insertCanvasState } from '@/lib/canvas' import { calculateIntersection } from '@/util/canvas-util' import { QPolygon } from '@/components/fabric/QPolygon' +import offsetPolygon from '@/util/qpolygon-utils' export default function Roof2() { const { canvas, handleRedo, handleUndo, setCanvasBackgroundWithDots, saveImage, addCanvas } = useCanvas('canvas') @@ -455,10 +456,23 @@ export default function Roof2() { return acc.length > cur.length ? acc : cur }) + const offsetPolygonPoint = offsetPolygon(roof.points, -20, 0) + + const newPoly = new QPolygon(offsetPolygonPoint, { + fill: 'transparent', + stroke: 'red', + strokeWidth: 1, + selectable: true, + fontSize: fontSize, + name: 'wall', + }) + + canvas?.add(newPoly) + if (maxLengthLine.direction === 'right' || maxLengthLine.direction === 'left') { - roof.fillCell({ width: 50, height: 100, padding: 0 }) + newPoly.fillCell({ width: 100, height: 50, padding: 10 }) } else { - roof.fillCell({ width: 100, height: 50, padding: 0 }) + newPoly.fillCell({ width: 50, height: 100, padding: 10 }) } }) } diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 685d83ee..13dcdd5f 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -13,6 +13,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { hips: [], ridges: [], connectRidges: [], + cells: [], initialize: function (points, options, canvas) { // 소수점 전부 제거 points.forEach((point) => { @@ -251,13 +252,20 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { lockScalingX: true, // X 축 크기 조정 잠금 lockScalingY: true, // Y 축 크기 조정 잠금 opacity: 0.8, + name: 'cell', }) + + rect.on('mousedown', () => { + rect.set({ fill: 'red' }) + }) + drawCellsArray.push(rect) //배열에 넣어서 반환한다 this.canvas.add(rect) } } } this.canvas?.renderAll() + this.cells = drawCellsArray return drawCellsArray }, inPolygon(point) { diff --git a/src/hooks/useCanvas.js b/src/hooks/useCanvas.js index df7f8eab..21e5e2cb 100644 --- a/src/hooks/useCanvas.js +++ b/src/hooks/useCanvas.js @@ -558,6 +558,7 @@ export function useCanvas(id) { 'lockScalingX', 'lockScalingY', 'opacity', + 'cells', ]) const str = JSON.stringify(objs) @@ -568,16 +569,12 @@ export function useCanvas(id) { // 역직렬화하여 캔버스에 객체를 다시 추가합니다. canvas?.loadFromJSON(JSON.parse(str), function () { // 모든 객체가 로드되고 캔버스에 추가된 후 호출됩니다. + console.log(canvas?.getObjects().filter((obj) => obj.name === 'roof')) canvas?.renderAll() // 캔버스를 다시 그립니다. }) }, 1000) } - const changeCanvas = (idx) => { - canvas?.clear() - const canvasState = JSON.parse(canvasList[idx]) - } - return { canvas, addShape, @@ -593,6 +590,5 @@ export function useCanvas(id) { handleFlip, setCanvasBackgroundWithDots, addCanvas, - changeCanvas, } } diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index c60fba2b..1862a981 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -17,6 +17,7 @@ import { import { QLine } from '@/components/fabric/QLine' import { fabric } from 'fabric' import { QPolygon } from '@/components/fabric/QPolygon' +import offsetPolygon from '@/util/qpolygon-utils' export const Mode = { DRAW_LINE: 'drawLine', // 기준선 긋기모드` @@ -1113,66 +1114,13 @@ export function useMode() { *벽 지붕 외곽선 생성 polygon을 입력받아 만들기 */ const handleOuterlinesTest2 = (polygon, offset = 71) => { - const offsetPoints = [] - const sortedIndex = getStartIndex(polygon.lines) - let tmpArraySorted = rearrangeArray(polygon.lines, sortedIndex) + const offsetPoints = offsetPolygon(polygon.points, 50) - if (tmpArraySorted[0].direction === 'right') { - //시계방향 - tmpArraySorted = tmpArraySorted.reverse() //그럼 배열을 거꾸로 만들어서 무조건 반시계방향으로 배열 보정 - } - - setSortedArray(tmpArraySorted) //recoil에 넣음 - - const points = tmpArraySorted.map((line) => ({ - x: line.x1, - y: line.y1, - })) - - for (let i = 0; i < points.length; i++) { - const prev = points[(i - 1 + points.length) % points.length] - const current = points[i] - const next = points[(i + 1) % points.length] - - // 두 벡터 계산 (prev -> current, current -> next) - const vector1 = { x: current.x - prev.x, y: current.y - prev.y } - const vector2 = { x: next.x - current.x, y: next.y - current.y } - - // 벡터의 길이 계산 - const length1 = Math.sqrt(vector1.x * vector1.x + vector1.y * vector1.y) - const length2 = Math.sqrt(vector2.x * vector2.x + vector2.y * vector2.y) - - // 벡터를 단위 벡터로 정규화 - const unitVector1 = { x: vector1.x / length1, y: vector1.y / length1 } - const unitVector2 = { x: vector2.x / length2, y: vector2.y / length2 } - - // 법선 벡터 계산 (왼쪽 방향) - const normal1 = { x: -unitVector1.y, y: unitVector1.x } - const normal2 = { x: -unitVector2.y, y: unitVector2.x } - - // 법선 벡터 평균 계산 - const averageNormal = { - x: (normal1.x + normal2.x) / 2, - y: (normal1.y + normal2.y) / 2, - } - - // 평균 법선 벡터를 단위 벡터로 정규화 - const lengthNormal = Math.sqrt(averageNormal.x * averageNormal.x + averageNormal.y * averageNormal.y) - const unitNormal = { - x: averageNormal.x / lengthNormal, - y: averageNormal.y / lengthNormal, - } - - // 오프셋 적용 - const offsetPoint = { - x1: current.x + unitNormal.x * offset, - y1: current.y + unitNormal.y * offset, - } - - offsetPoints.push(offsetPoint) - } - - const roof = makePolygon(offsetPoints) + const roof = makePolygon( + offsetPoints.map((point) => { + return { x1: point.x, y1: point.y } + }), + ) roof.setWall(polygon) setRoof(roof) diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 36eadd4d..b7d7f9fd 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -3,6 +3,8 @@ import { QLine } from '@/components/fabric/QLine' import { calculateIntersection, distanceBetweenPoints, findClosestPoint, getDirectionByPoint } from '@/util/canvas-util' import { QPolygon } from '@/components/fabric/QPolygon' +const TWO_PI = Math.PI * 2 + export const defineQPloygon = () => { fabric.QPolygon.fromObject = function (object, callback) { fabric.Object._fromObject('QPolygon', object, callback, 'points') @@ -676,3 +678,239 @@ const getOneSideLine = (line) => { return newLine } + +function inwardEdgeNormal(vertex1, vertex2) { + // Assuming that polygon vertices are in clockwise order + const dx = vertex2.x - vertex1.x + const dy = vertex2.y - vertex1.y + const edgeLength = Math.sqrt(dx * dx + dy * dy) + + return { + x: -dy / edgeLength, + y: dx / edgeLength, + } +} + +function outwardEdgeNormal(vertex1, vertex2) { + var n = inwardEdgeNormal(vertex1, vertex2) + + return { + x: -n.x, + y: -n.y, + } +} + +function createPolygon(vertices) { + const edges = [] + let minX = vertices.length > 0 ? vertices[0].x : undefined + let minY = vertices.length > 0 ? vertices[0].y : undefined + let maxX = minX + let maxY = minY + + for (let i = 0; i < vertices.length; i++) { + const vertex1 = vertices[i] + const vertex2 = vertices[(i + 1) % vertices.length] + + const outwardNormal = outwardEdgeNormal(vertex1, vertex2) + + const inwardNormal = inwardEdgeNormal(vertex1, vertex2) + + const edge = { + vertex1, + vertex2, + index: i, + outwardNormal, + inwardNormal, + } + + edges.push(edge) + + const x = vertices[i].x + const y = vertices[i].y + minX = Math.min(x, minX) + minY = Math.min(y, minY) + maxX = Math.max(x, maxX) + maxY = Math.max(y, maxY) + } + + return { + vertices, + edges, + minX, + minY, + maxX, + maxY, + } +} + +// based on http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/, edgeA => "line a", edgeB => "line b" + +function edgesIntersection(edgeA, edgeB) { + const den = + (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex2.x - edgeA.vertex1.x) - + (edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex2.y - edgeA.vertex1.y) + + if (den == 0) { + return null // lines are parallel or coincident + } + + const ua = + ((edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - + (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / + den + + const ub = + ((edgeA.vertex2.x - edgeA.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - + (edgeA.vertex2.y - edgeA.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / + den + + // Edges are not intersecting but the lines defined by them are + const isIntersectionOutside = ua < 0 || ub < 0 || ua > 1 || ub > 1 + + return { + x: edgeA.vertex1.x + ua * (edgeA.vertex2.x - edgeA.vertex1.x), + y: edgeA.vertex1.y + ua * (edgeA.vertex2.y - edgeA.vertex1.y), + isIntersectionOutside, + } +} + +function appendArc(arcSegments, vertices, center, radius, startVertex, endVertex, isPaddingBoundary) { + var startAngle = Math.atan2(startVertex.y - center.y, startVertex.x - center.x) + var endAngle = Math.atan2(endVertex.y - center.y, endVertex.x - center.x) + + if (startAngle < 0) { + startAngle += TWO_PI + } + + if (endAngle < 0) { + endAngle += TWO_PI + } + + const angle = startAngle > endAngle ? startAngle - endAngle : startAngle + TWO_PI - endAngle + const angleStep = (isPaddingBoundary ? -angle : TWO_PI - angle) / arcSegments + + vertices.push(startVertex) + + for (let i = 1; i < arcSegments; ++i) { + const angle = startAngle + angleStep * i + + const vertex = { + x: center.x + Math.cos(angle) * radius, + y: center.y + Math.sin(angle) * radius, + } + + vertices.push(vertex) + } + + vertices.push(endVertex) +} + +function createOffsetEdge(edge, dx, dy) { + return { + vertex1: { + x: edge.vertex1.x + dx, + y: edge.vertex1.y + dy, + }, + vertex2: { + x: edge.vertex2.x + dx, + y: edge.vertex2.y + dy, + }, + } +} + +function createMarginPolygon(polygon, offset, arcSegments = 0) { + const offsetEdges = [] + + for (let i = 0; i < polygon.edges.length; i++) { + const edge = polygon.edges[i] + const dx = edge.outwardNormal.x * offset + const dy = edge.outwardNormal.y * offset + offsetEdges.push(createOffsetEdge(edge, dx, dy)) + } + + const vertices = [] + + for (let i = 0; i < offsetEdges.length; i++) { + const thisEdge = offsetEdges[i] + const prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length] + const vertex = edgesIntersection(prevEdge, thisEdge) + + if (vertex && (!vertex.isIntersectionOutside || arcSegments < 1)) { + vertices.push({ + x: vertex.x, + y: vertex.y, + }) + } else { + const arcCenter = polygon.edges[i].vertex1 + + appendArc(arcSegments, vertices, arcCenter, offset, prevEdge.vertex2, thisEdge.vertex1, false) + } + } + + const marginPolygon = createPolygon(vertices) + + marginPolygon.offsetEdges = offsetEdges + + return marginPolygon +} + +function createPaddingPolygon(polygon, offset, arcSegments = 0) { + const offsetEdges = [] + + for (let i = 0; i < polygon.edges.length; i++) { + const edge = polygon.edges[i] + const dx = edge.inwardNormal.x * offset + const dy = edge.inwardNormal.y * offset + offsetEdges.push(createOffsetEdge(edge, dx, dy)) + } + + const vertices = [] + + for (let i = 0; i < offsetEdges.length; i++) { + const thisEdge = offsetEdges[i] + const prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length] + const vertex = edgesIntersection(prevEdge, thisEdge) + if (vertex && (!vertex.isIntersectionOutside || arcSegments < 1)) { + vertices.push({ + x: vertex.x, + y: vertex.y, + }) + } else { + const arcCenter = polygon.edges[i].vertex1 + + appendArc(arcSegments, vertices, arcCenter, offset, prevEdge.vertex2, thisEdge.vertex1, true) + } + } + + const paddingPolygon = createPolygon(vertices) + + paddingPolygon.offsetEdges = offsetEdges + + return paddingPolygon +} + +export default function offsetPolygon(vertices, offset, arcSegments = 0) { + const polygon = createPolygon(vertices) + + const originPolygon = new QPolygon(vertices, { fontSize: 0 }) + + if (offset > 0) { + let result = createMarginPolygon(polygon, offset, arcSegments).vertices + const allPointsOutside = result.every((point) => !originPolygon.inPolygon(point)) + + if (allPointsOutside) { + return createMarginPolygon(polygon, offset, arcSegments).vertices + } else { + return createPaddingPolygon(polygon, offset, arcSegments).vertices + } + } else { + let result = createPaddingPolygon(polygon, offset, arcSegments).vertices + const allPointsInside = result.every((point) => originPolygon.inPolygon(point)) + + if (allPointsInside) { + return createPaddingPolygon(polygon, offset, arcSegments).vertices + } else { + return createMarginPolygon(polygon, offset, arcSegments).vertices + } + } +}