diff --git a/src/components/fabric/QLine.js b/src/components/fabric/QLine.js index f55e351b..2d4294d6 100644 --- a/src/components/fabric/QLine.js +++ b/src/components/fabric/QLine.js @@ -93,7 +93,7 @@ export const QLine = fabric.util.createClass(fabric.Line, { thisText.set({ actualSize: this.attributes.actualSize }) } if (this.attributes?.planeSize) { - thisText.set({ planeSize: this.attributes.planeSize }) + thisText.set({ planeSize: this.attributes.planeSize, text: this.attributes.planeSize.toString() }) } } else { this.setLength() @@ -119,7 +119,8 @@ export const QLine = fabric.util.createClass(fabric.Line, { const maxY = this.top + this.length const degree = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI - const text = new fabric.Textbox(this.getLength().toString(), { + const displayValue = this.attributes?.planeSize ?? this.getLength() + const text = new fabric.Textbox(displayValue.toString(), { actualSize: this.attributes?.actualSize, planeSize: this.attributes?.planeSize, left: left, diff --git a/src/hooks/common/useGrid.js b/src/hooks/common/useGrid.js index 9f8c72dd..78ae7643 100644 --- a/src/hooks/common/useGrid.js +++ b/src/hooks/common/useGrid.js @@ -50,8 +50,9 @@ export function useGrid() { const visibleBottom = visibleTop + canvasHeight / currentZoom const padding = 200 - const gridLeft = visibleLeft - padding - const gridTop = visibleTop - padding + // 원점(0,0) 기준 그리드 간격의 배수로 정렬하여 줌 시 위치 고정 + const gridLeft = Math.floor((visibleLeft - padding) / patternData.gridHorizon) * patternData.gridHorizon + const gridTop = Math.floor((visibleTop - padding) / patternData.gridVertical) * patternData.gridVertical const gridRight = visibleRight + padding const gridBottom = visibleBottom + padding @@ -100,8 +101,9 @@ export function useGrid() { // 여유 공간 추가 const padding = 200 - const gridLeft = visibleLeft - padding - const gridTop = visibleTop - padding + // 원점(0,0) 기준 그리드 간격의 배수로 정렬하여 줌 시 위치 고정 + const gridLeft = Math.floor((visibleLeft - padding) / patternData.gridHorizon) * patternData.gridHorizon + const gridTop = Math.floor((visibleTop - padding) / patternData.gridVertical) * patternData.gridVertical const gridRight = visibleRight + padding const gridBottom = visibleBottom + padding diff --git a/src/hooks/surface/usePlacementShapeDrawing.js b/src/hooks/surface/usePlacementShapeDrawing.js index 1da292e8..374616be 100644 --- a/src/hooks/surface/usePlacementShapeDrawing.js +++ b/src/hooks/surface/usePlacementShapeDrawing.js @@ -4,7 +4,6 @@ import { useEvent } from '@/hooks/useEvent' import { useMouse } from '@/hooks/useMouse' import { useLine } from '@/hooks/useLine' import { useEffect, useRef } from 'react' -import { distanceBetweenPoints } from '@/util/canvas-util' import { fabric } from 'fabric' import { calculateAngle } from '@/util/qpolygon-utils' import { @@ -44,7 +43,7 @@ export function usePlacementShapeDrawing(id) { // useContext(EventContext) const { getIntersectMousePoint } = useMouse() const { addLine, removeLine } = useLine() - const { addPolygonByLines, drawDirectionArrow } = usePolygon() + const { addPolygonByLines, drawDirectionArrow, addLengthText } = usePolygon() const { setSurfaceShapePattern } = useRoofFn() const { changeSurfaceLineType } = useSurfaceShapeBatch({}) const { handleSelectableObjects } = useObject() @@ -135,35 +134,17 @@ export function usePlacementShapeDrawing(id) { } else { const lastPoint = points[points.length - 1] let newPoint = { x: pointer.x, y: pointer.y } - const length = distanceBetweenPoints(lastPoint, newPoint) if (verticalHorizontalMode) { const vector = { - x: pointer.x - points[points.length - 1].x, - y: pointer.y - points[points.length - 1].y, + x: pointer.x - lastPoint.x, + y: pointer.y - lastPoint.y, } - const slope = Math.abs(vector.y / vector.x) // 기울기 계산 + const slope = Math.abs(vector.y / vector.x) - let scaledVector if (slope >= 1) { - // 기울기가 1 이상이면 x축 방향으로 그림 - scaledVector = { - x: 0, - y: vector.y >= 0 ? Number(length) : -Number(length), - } + newPoint = { x: lastPoint.x, y: pointer.y } } else { - // 기울기가 1 미만이면 y축 방향으로 그림 - scaledVector = { - x: vector.x >= 0 ? Number(length) : -Number(length), - y: 0, - } - } - - const verticalLength = scaledVector.y - const horizontalLength = scaledVector.x - - newPoint = { - x: lastPoint.x + horizontalLength, - y: lastPoint.y + verticalLength, + newPoint = { x: pointer.x, y: lastPoint.y } } } setPoints((prev) => [...prev, newPoint]) @@ -244,6 +225,9 @@ export function usePlacementShapeDrawing(id) { from: 'surface', }) + // 기존 도형의 흡착점을 이용해 그린 경우, 기존 도형의 planeSize를 상속 + inheritPlaneSizeFromExistingShapes(roof) + setSurfaceShapePattern(roof, roofDisplay.column) drawDirectionArrow(roof) @@ -315,8 +299,169 @@ export function usePlacementShapeDrawing(id) { } }, [points]) + // 기존 도형의 변과 일치하는 경우 planeSize를 상속하는 함수 + const inheritPlaneSizeFromExistingShapes = (newPolygon) => { + const tolerance = 2 + const existingPolygons = canvas.getObjects().filter( + (obj) => (obj.name === POLYGON_TYPE.ROOF || obj.name === POLYGON_TYPE.WALL) && obj.id !== newPolygon.id && obj.lines, + ) + + if (existingPolygons.length === 0) return + + const inheritedSet = new Set() + + // 1단계: 기존 도형의 변과 매칭하여 planeSize 상속 + newPolygon.lines.forEach((line, lineIdx) => { + const x1 = line.x1, + y1 = line.y1, + x2 = line.x2, + y2 = line.y2 + const isHorizontal = Math.abs(y1 - y2) < tolerance + const isVertical = Math.abs(x1 - x2) < tolerance + + for (const polygon of existingPolygons) { + for (const edge of polygon.lines) { + if (!edge.attributes?.planeSize) continue + + const ex1 = edge.x1, + ey1 = edge.y1, + ex2 = edge.x2, + ey2 = edge.y2 + + // 1순위: 양 끝점이 정확히 일치 + const forwardMatch = + Math.abs(x1 - ex1) < tolerance && + Math.abs(y1 - ey1) < tolerance && + Math.abs(x2 - ex2) < tolerance && + Math.abs(y2 - ey2) < tolerance + const reverseMatch = + Math.abs(x1 - ex2) < tolerance && + Math.abs(y1 - ey2) < tolerance && + Math.abs(x2 - ex1) < tolerance && + Math.abs(y2 - ey1) < tolerance + + if (forwardMatch || reverseMatch) { + line.attributes = { ...line.attributes, planeSize: edge.attributes.planeSize } + if (edge.attributes.actualSize) { + line.attributes.actualSize = edge.attributes.actualSize + } + inheritedSet.add(lineIdx) + return + } + + // 2순위: 같은 방향 + 같은 좌표 차이 (끝점 공유 불필요) + if (isHorizontal && Math.abs(ey1 - ey2) < tolerance && Math.abs(Math.abs(x2 - x1) - Math.abs(ex2 - ex1)) < tolerance) { + line.attributes = { ...line.attributes, planeSize: edge.attributes.planeSize } + if (edge.attributes.actualSize) { + line.attributes.actualSize = edge.attributes.actualSize + } + inheritedSet.add(lineIdx) + return + } + if (isVertical && Math.abs(ex1 - ex2) < tolerance && Math.abs(Math.abs(y2 - y1) - Math.abs(ey2 - ey1)) < tolerance) { + line.attributes = { ...line.attributes, planeSize: edge.attributes.planeSize } + if (edge.attributes.actualSize) { + line.attributes.actualSize = edge.attributes.actualSize + } + inheritedSet.add(lineIdx) + return + } + } + } + }) + + // 2단계: 상속받은 변과 평행하고 좌표 차이가 같은 변에 planeSize 전파 + if (inheritedSet.size > 0) { + newPolygon.lines.forEach((line, lineIdx) => { + if (inheritedSet.has(lineIdx)) return + + const x1 = line.x1, + y1 = line.y1, + x2 = line.x2, + y2 = line.y2 + const isHorizontal = Math.abs(y1 - y2) < tolerance + const isVertical = Math.abs(x1 - x2) < tolerance + + for (const idx of inheritedSet) { + const inherited = newPolygon.lines[idx] + const ix1 = inherited.x1, + iy1 = inherited.y1, + ix2 = inherited.x2, + iy2 = inherited.y2 + const iIsHorizontal = Math.abs(iy1 - iy2) < tolerance + const iIsVertical = Math.abs(ix1 - ix2) < tolerance + + if (isHorizontal && iIsHorizontal) { + if (Math.abs(Math.abs(x2 - x1) - Math.abs(ix2 - ix1)) < tolerance) { + line.attributes = { ...line.attributes, planeSize: inherited.attributes.planeSize } + if (inherited.attributes.actualSize) { + line.attributes.actualSize = inherited.attributes.actualSize + } + inheritedSet.add(lineIdx) + return + } + } else if (isVertical && iIsVertical) { + if (Math.abs(Math.abs(y2 - y1) - Math.abs(iy2 - iy1)) < tolerance) { + line.attributes = { ...line.attributes, planeSize: inherited.attributes.planeSize } + if (inherited.attributes.actualSize) { + line.attributes.actualSize = inherited.attributes.actualSize + } + inheritedSet.add(lineIdx) + return + } + } + } + }) + } + + // planeSize가 상속된 경우 길이 텍스트를 다시 렌더링 + if (inheritedSet.size > 0) { + addLengthText(newPolygon) + } + } + + // 기존 도형에서 매칭되는 변의 planeSize를 찾는 헬퍼 함수 + const findMatchingEdgePlaneSize = (x1, y1, x2, y2) => { + const tolerance = 2 + const existingPolygons = canvas.getObjects().filter( + (obj) => (obj.name === POLYGON_TYPE.ROOF || obj.name === POLYGON_TYPE.WALL) && obj.lines, + ) + + const isHorizontal = Math.abs(y1 - y2) < tolerance + const isVertical = Math.abs(x1 - x2) < tolerance + + for (const polygon of existingPolygons) { + for (const edge of polygon.lines) { + if (!edge.attributes?.planeSize) continue + const ex1 = edge.x1, + ey1 = edge.y1, + ex2 = edge.x2, + ey2 = edge.y2 + + // 1순위: 양 끝점 일치 (정방향/역방향) + const forwardMatch = + Math.abs(x1 - ex1) < tolerance && Math.abs(y1 - ey1) < tolerance && Math.abs(x2 - ex2) < tolerance && Math.abs(y2 - ey2) < tolerance + const reverseMatch = + Math.abs(x1 - ex2) < tolerance && Math.abs(y1 - ey2) < tolerance && Math.abs(x2 - ex1) < tolerance && Math.abs(y2 - ey1) < tolerance + + if (forwardMatch || reverseMatch) { + return edge.attributes.planeSize + } + + // 2순위: 같은 방향 + 같은 좌표 차이 (끝점 공유 불필요) + if (isHorizontal && Math.abs(ey1 - ey2) < tolerance && Math.abs(Math.abs(x2 - x1) - Math.abs(ex2 - ex1)) < tolerance) { + return edge.attributes.planeSize + } + if (isVertical && Math.abs(ex1 - ex2) < tolerance && Math.abs(Math.abs(y2 - y1) - Math.abs(ey2 - ey1)) < tolerance) { + return edge.attributes.planeSize + } + } + } + return null + } + const drawLine = (point1, point2, idx) => { - addLine([point1.x, point1.y, point2.x, point2.y], { + const line = addLine([point1.x, point1.y, point2.x, point2.y], { stroke: 'black', strokeWidth: 3, idx: idx, @@ -327,6 +472,14 @@ export function usePlacementShapeDrawing(id) { x2: point2.x, y2: point2.y, }) + + // 기존 도형의 변과 일치하는 경우 planeSize 상속 + if (line) { + const matchedPlaneSize = findMatchingEdgePlaneSize(point1.x, point1.y, point2.x, point2.y) + if (matchedPlaneSize !== null) { + line.attributes = { ...line.attributes, planeSize: matchedPlaneSize } + } + } } // 직각 완료될 경우 확인 diff --git a/src/hooks/useEvent.js b/src/hooks/useEvent.js index 939f69f5..5a5dfac2 100644 --- a/src/hooks/useEvent.js +++ b/src/hooks/useEvent.js @@ -218,7 +218,8 @@ export function useEvent() { }) }) - const dotGridPoints = canvas.getObjects() + const dotGridPoints = canvas + .getObjects() .filter((obj) => obj.name === 'dotGrid') .map((obj) => ({ x: obj.left, y: obj.top })) @@ -247,17 +248,12 @@ export function useEvent() { adsorptionPoints = removeDuplicatePoints(adsorptionPoints) - if (dotLineGridSetting.LINE || canvas.getObjects().filter((obj) => ['lineGrid', 'tempGrid'].includes(obj.name)).length > 1) { + if (dotLineGridSetting.LINE || canvas.getObjects().filter((obj) => ['lineGrid', 'tempGrid'].includes(obj.name)).length > 0) { const closestLine = getClosestLineGrid(pointer) const horizonLines = canvas.getObjects().filter((obj) => ['lineGrid', 'tempGrid'].includes(obj.name) && obj.direction === 'horizontal') const verticalLines = canvas.getObjects().filter((obj) => ['lineGrid', 'tempGrid'].includes(obj.name) && obj.direction === 'vertical') - if (!horizonLines || !verticalLines) { - drawMouseLine(pointer) - return - } - let closestHorizontalLine = null let closestVerticalLine = null @@ -277,12 +273,8 @@ export function useEvent() { }) } - if (!closestVerticalLine || !closestHorizontalLine) { - drawMouseLine(pointer) - return - } - - const closestIntersectionPoint = calculateIntersection(closestHorizontalLine, closestVerticalLine) + const closestIntersectionPoint = + closestHorizontalLine && closestVerticalLine ? calculateIntersection(closestHorizontalLine, closestVerticalLine) : null if (closestLine) { const distanceClosestLine = calculateDistance(pointer, closestLine) @@ -300,7 +292,7 @@ export function useEvent() { y: closestLine.y1, } - if (distanceClosestPoint * 2 < adsorptionRange) { + if (closestIntersectionPoint && distanceClosestPoint * 2 < adsorptionRange) { arrivalPoint = { ...closestIntersectionPoint } } } @@ -435,7 +427,13 @@ export function useEvent() { y: Math.round(originPointer.y), } - const tempGrid = new fabric.Line([-1500, pointer.y, 2500, pointer.y], { + const currentZoom = canvas.getZoom() + const vt = canvas.viewportTransform + const visibleLeft = -vt[4] / currentZoom + const visibleRight = visibleLeft + canvas.getWidth() / currentZoom + const padding = 500 + + const tempGrid = new fabric.Line([visibleLeft - padding, pointer.y, visibleRight + padding, pointer.y], { stroke: gridColor, strokeWidth: 1, selectable: true, diff --git a/src/hooks/useTempGrid.js b/src/hooks/useTempGrid.js index c0a7dcc1..fcb9d238 100644 --- a/src/hooks/useTempGrid.js +++ b/src/hooks/useTempGrid.js @@ -11,11 +11,28 @@ export function useTempGrid() { const isGridDisplay = useRecoilValue(gridDisplaySelector) const [tempGridMode, setTempGridMode] = useRecoilState(tempGridModeState) const { getIntersectMousePoint } = useMouse() + const getVisibleBounds = () => { + const currentZoom = canvas.getZoom() + const vt = canvas.viewportTransform + const visibleLeft = -vt[4] / currentZoom + const visibleTop = -vt[5] / currentZoom + const visibleRight = visibleLeft + canvas.getWidth() / currentZoom + const visibleBottom = visibleTop + canvas.getHeight() / currentZoom + const padding = 500 + return { + left: visibleLeft - padding, + top: visibleTop - padding, + right: visibleRight + padding, + bottom: visibleBottom + padding, + } + } + const tempGridModeStateLeftClickEvent = (e) => { //임의 그리드 모드일 경우 let pointer = getIntersectMousePoint(e) + const bounds = getVisibleBounds() - const tempGrid = new fabric.Line([pointer.x, -1500, pointer.x, 2500], { + const tempGrid = new fabric.Line([pointer.x, bounds.top, pointer.x, bounds.bottom], { stroke: gridColor, strokeWidth: 1, selectable: true,