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/components/floor-plan/modal/setting01/GridOption.jsx b/src/components/floor-plan/modal/setting01/GridOption.jsx
index 76b8fc1c..5931b561 100644
--- a/src/components/floor-plan/modal/setting01/GridOption.jsx
+++ b/src/components/floor-plan/modal/setting01/GridOption.jsx
@@ -152,16 +152,12 @@ export default function GridOption(props) {
{getMessage('modal.canvas.setting.grid')}
- {gridOptions?.map((option) =>
- option.id === 2 ? (
- <>>
- ) : (
-
- ),
- )}
+ {gridOptions?.map((option) => (
+
+ ))}
{/**/}
diff --git a/src/hooks/common/useGrid.js b/src/hooks/common/useGrid.js
index 4d427b21..78ae7643 100644
--- a/src/hooks/common/useGrid.js
+++ b/src/hooks/common/useGrid.js
@@ -39,56 +39,51 @@ export function useGrid() {
//const verticalInterval = interval.verticalInterval
if (patternData.dotGridDisplay) {
- const circle = new fabric.Circle({
- radius: 2,
- fill: 'red',
- strokeWidth: 0.7,
- originX: 'center',
- originY: 'center',
- selectable: false,
- lockMovementX: true,
- lockMovementY: true,
- lockRotation: true,
- lockScalingX: true,
- lockScalingY: true,
- })
+ const canvasWidth = canvas.getWidth()
+ const canvasHeight = canvas.getHeight()
+ const currentZoom = canvas.getZoom()
+ const viewportTransform = canvas.viewportTransform
- const patternSourceCanvas = new fabric.StaticCanvas(null, {
- width: patternData.gridHorizon,
- height: patternData.gridVertical,
- })
+ const visibleLeft = -viewportTransform[4] / currentZoom
+ const visibleTop = -viewportTransform[5] / currentZoom
+ const visibleRight = visibleLeft + canvasWidth / currentZoom
+ const visibleBottom = visibleTop + canvasHeight / currentZoom
- patternSourceCanvas.add(circle)
+ const padding = 200
+ // 원점(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
- circle.set({
- left: patternSourceCanvas.width / 2,
- top: patternSourceCanvas.height / 2,
- })
+ const horizontalCount = Math.ceil((gridRight - gridLeft) / patternData.gridHorizon) + 1
+ const verticalCount = Math.ceil((gridBottom - gridTop) / patternData.gridVertical) + 1
- patternSourceCanvas.renderAll()
-
- const pattern = new fabric.Pattern({
- source: patternSourceCanvas.getElement(),
- repeat: 'repeat',
- })
-
- const backgroundPolygon = new fabric.Polygon(
- [
- { x: -1500, y: -1500 },
- { x: 2500, y: -1500 },
- { x: 2500, y: 2500 },
- { x: -1500, y: 2500 },
- ],
- {
- fill: pattern,
- selectable: false,
- name: 'dotGrid',
- visible: isGridDisplay,
- },
- )
-
- canvas.add(backgroundPolygon)
- backgroundPolygon.sendToBack()
+ for (let i = 0; i < horizontalCount; i++) {
+ for (let j = 0; j < verticalCount; j++) {
+ const dot = new fabric.Circle({
+ left: gridLeft + i * patternData.gridHorizon,
+ top: gridTop + j * patternData.gridVertical,
+ radius: 2,
+ fill: 'red',
+ stroke: null,
+ strokeWidth: 0,
+ originX: 'center',
+ originY: 'center',
+ selectable: true,
+ lockMovementX: true,
+ lockMovementY: true,
+ lockRotation: true,
+ lockScalingX: true,
+ lockScalingY: true,
+ name: 'dotGrid',
+ padding: GRID_PADDING,
+ visible: isGridDisplay,
+ })
+ canvas.add(dot)
+ dot.sendToBack()
+ }
+ }
canvas.renderAll()
}
@@ -106,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 8caa90ea..5a5dfac2 100644
--- a/src/hooks/useEvent.js
+++ b/src/hooks/useEvent.js
@@ -218,7 +218,13 @@ export function useEvent() {
})
})
+ const dotGridPoints = canvas
+ .getObjects()
+ .filter((obj) => obj.name === 'dotGrid')
+ .map((obj) => ({ x: obj.left, y: obj.top }))
+
let adsorptionPoints = [
+ ...dotGridPoints,
...getAdsorptionPoints(),
...roofAdsorptionPoints.current,
...otherAdsorptionPoints,
@@ -242,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
@@ -272,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)
@@ -295,7 +292,7 @@ export function useEvent() {
y: closestLine.y1,
}
- if (distanceClosestPoint * 2 < adsorptionRange) {
+ if (closestIntersectionPoint && distanceClosestPoint * 2 < adsorptionRange) {
arrivalPoint = { ...closestIntersectionPoint }
}
}
@@ -430,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,