diff --git a/src/components/floor-plan/CanvasMenu.jsx b/src/components/floor-plan/CanvasMenu.jsx index b14ff216..84cbdca2 100644 --- a/src/components/floor-plan/CanvasMenu.jsx +++ b/src/components/floor-plan/CanvasMenu.jsx @@ -10,16 +10,21 @@ import QSelectBox from '@/components/common/select/QSelectBox' import { useMessage } from '@/hooks/useMessage' import { usePlan } from '@/hooks/usePlan' import { useSwal } from '@/hooks/useSwal' +import { useEvent } from '@/hooks/useEvent' import { canvasState, canvasZoomState, currentCanvasPlanState, currentMenuState, verticalHorizontalModeState } from '@/store/canvasAtom' import { sessionStore } from '@/store/commonAtom' import { outerLinePointsState } from '@/store/outerLineAtom' import { appMessageStore, globalLocaleStore } from '@/store/localeAtom' +import { wordDisplaySelector } from '@/store/settingAtom' import { MENU } from '@/common/common' +import { checkLineOrientation, getDistance } from '@/util/canvas-util' + import KO from '@/locales/ko.json' import JA from '@/locales/ja.json' import { settingModalFirstOptionsState } from '@/store/settingAtom' import { placementShapeDrawingPointsState } from '@/store/placementShapeDrawingAtom' +import { lineSegment } from '@turf/turf' const canvasMenus = [ { index: 0, name: 'plan.menu.plan.drawing', icon: 'con00', title: MENU.PLAN_DRAWING }, @@ -68,11 +73,20 @@ export default function CanvasMenu(props) { const globalLocale = useRecoilValue(globalLocaleStore) const canvas = useRecoilValue(canvasState) const sessionState = useRecoilValue(sessionStore) + const wordDisplay = useRecoilValue(wordDisplaySelector) const { getMessage } = useMessage() const { saveCanvas } = usePlan() const { swalFire } = useSwal() + const { addCanvasMouseEventListener, initEvent, addDocumentEventListener } = useEvent() + const [commonFunction, setCommonFunction] = useState(null) + const [commonFunctionState, setCommonFunctionState] = useState({ + text: false, + dimension: false, + distance: false, + }) + const SelectOption = [{ name: '瓦53A' }, { name: '瓦53A' }] const onClickNav = (menu) => { setMenuNumber(menu.index) @@ -155,6 +169,328 @@ export default function CanvasMenu(props) { canvas.renderAll() } + const commonTextMode = () => { + let textbox + if (commonFunctionState.text) { + addCanvasMouseEventListener('mouse:down', (event) => { + const pointer = canvas?.getPointer(event.e) + textbox = new fabric.Textbox('', { + left: pointer.x, + top: pointer.y, + width: 200, + fontSize: 14, + editable: true, + name: 'commonText', + visible: wordDisplay, + }) + + canvas?.add(textbox) + canvas.setActiveObject(textbox) + textbox.enterEditing() + textbox.selectAll() + }) + + addDocumentEventListener('keydown', document, (e) => { + if (e.key === 'Enter') { + if (commonFunctionState.text) { + const activeObject = canvas.getActiveObject() + if (activeObject && activeObject.isEditing) { + if (activeObject.text === '') { + canvas?.remove(activeObject) + } else { + activeObject.exitEditing() + } + //정책 협의 + const texts = canvas.getObjects().filter((obj) => obj.name === 'commonText') + texts.forEach((text) => { + text.set({ editable: false }) + }) + + canvas.renderAll() + } + } + } + }) + } + } + + const commonDimensionMode = () => { + if (commonFunctionState.dimension) { + let points = [] + let distanceText = null + let minX, minY, maxX, maxY + + // 화살표를 생성하는 함수 + function createArrow(x, y, angle) { + return new fabric.Triangle({ + left: x, + top: y, + originX: 'center', + originY: 'center', + angle: angle, + width: 15, + height: 15, + fill: 'black', + selectable: false, + }) + } + + const circleOptions = { + radius: 5, + strokeWidth: 2, + stroke: 'red', + fill: 'white', + selectable: false, + } + + const lineOptions = { + stroke: 'black', + strokeWidth: 2, + selectable: false, + } + + // 캔버스에 클릭 이벤트 추가 + addCanvasMouseEventListener('mouse:down', (e) => { + const pointer = canvas.getPointer(e.e) + let point + + if (points.length === 0) { + // 첫 번째 포인트는 그대로 클릭한 위치에 추가 + point = new fabric.Circle({ + left: pointer.x - 5, // 반지름 반영 + top: pointer.y - 5, // 반지름 반영 + ...circleOptions, + }) + points.push(point) + canvas.add(point) + } else if (points.length === 1) { + // 두 번째 포인트는 첫 번째 포인트를 기준으로 수평 또는 수직으로만 배치 + const p1 = points[0] + const deltaX = Math.abs(pointer.x - (p1.left + p1.radius)) + const deltaY = Math.abs(pointer.y - (p1.top + p1.radius)) + + if (deltaX > deltaY) { + // 수평선 상에만 배치 (y 좌표 고정) + point = new fabric.Circle({ + left: pointer.x - 5, // 반지름 반영 + top: p1.top, // y 좌표 고정 + ...circleOptions, + }) + } else { + // 수직선 상에만 배치 (x 좌표 고정) + point = new fabric.Circle({ + left: p1.left, // x 좌표 고정 + top: pointer.y - 5, // 반지름 반영 + ...circleOptions, + }) + } + + points.push(point) + canvas.add(point) + + // 두 포인트의 중심 좌표 계산 + const p2 = points[1] + const p1CenterX = p1.left + p1.radius + const p1CenterY = p1.top + p1.radius + const p2CenterX = p2.left + p2.radius + const p2CenterY = p2.top + p2.radius + + points.forEach((point) => { + canvas?.remove(point) + }) + + // 두 포인트 간에 직선을 그림 (중심을 기준으로) + const line = new fabric.Line([p1CenterX, p1CenterY, p2CenterX, p2CenterY], lineOptions) + canvas.add(line) + + const distance = getDistance(p1CenterX, p1CenterY, p2CenterX, p2CenterY) + const lineDirection = checkLineOrientation(line) + let extendLine = [] + + if (lineDirection === 'horizontal') { + extendLine = [ + [line.x1, line.y1 - 20, line.x1, line.y1 + 20], + [line.x2, line.y2 - 20, line.x2, line.y2 + 20], + ] + } else { + extendLine = [ + [line.x1 - 20, line.y1, line.x1 + 20, line.y1], + [line.x2 - 20, line.y2, line.x2 + 20, line.y2], + ] + } + + extendLine.forEach((line) => { + const extendLine = new fabric.Line(line, lineOptions) + canvas.add(extendLine) + }) + + // 두 포인트 간의 각도를 계산하여 화살표 추가 + // const angle = calculateAngle({ x: p1CenterX, y: p1CenterY }, { x: p2CenterX, y: p2CenterY }) + + // 첫 번째 포인트에 화살표 추가 + const arrow1 = createArrow(p1CenterX + 7.5, p1CenterY + 1, lineDirection === 'horizontal' ? -90 : 0) // 반대 방향 화살표 + const arrow2 = createArrow(p2CenterX - 6.5, p2CenterY + 1, lineDirection === 'horizontal' ? 90 : 180) // 정방향 화살표 + canvas.add(arrow1) + canvas.add(arrow2) + + // 두 포인트 간의 거리 계산 + + // 거리 텍스트가 이미 있으면 업데이트하고, 없으면 새로 생성 + distanceText = new fabric.Text(`${distance}`, { + left: (p1CenterX + p2CenterX) / 2 + (lineDirection === 'horizontal' ? 0 : -15), + top: (p1CenterY + p2CenterY) / 2 + (lineDirection === 'horizontal' ? +15 : 0), + fill: 'black', + fontSize: 16, + selectable: true, + textAlign: 'center', + originX: 'center', + originY: 'center', + angle: lineDirection === 'horizontal' ? 0 : 270, + // lockMovementX: false, + // lockMovementY: false, + }) + canvas.add(distanceText) + + // minX = p1CenterX + // maxX = p2CenterX + // minY = p1CenterY + // maxY = p2CenterY + + // 거리 계산 후, 다음 측정을 위해 초기화 + points = [] + } + + // 캔버스 다시 그리기 + canvas.renderAll() + }) + + // addCanvasMouseEventListener('object:moving', function (e) { + // const obj = e.target + + // if (obj.left < minX) { + // obj.left = minX + // } + // if (obj.left + obj.width > maxX) { + // obj.left = maxX - obj.width + // } + // if (obj.top < minY) { + // obj.top = minY + // } + // if (obj.top + obj.height > maxY) { + // obj.top = maxY - obj.height + // } + // }) + } + } + + const commonDistanceMode = () => { + if (commonFunctionState.distance) { + let points = [] + let distanceText = null + + const circleOptions = { + radius: 5, + strokeWidth: 2, + stroke: 'red', + fill: 'white', + selectable: false, + } + + const lineOptions = { + stroke: 'black', + strokeWidth: 2, + selectable: false, + strokeDashArray: [9, 5], + } + + const textOptions = { + fill: 'black', + fontSize: 16, + selectable: true, + textAlign: 'center', + originX: 'center', + originY: 'center', + } + + // 캔버스에 클릭 이벤트 추가 + addCanvasMouseEventListener('mouse:down', function (options) { + const pointer = canvas.getPointer(options.e) + let point + + if (points.length === 0) { + // 첫 번째 포인트는 그대로 클릭한 위치에 추가 + point = new fabric.Circle({ + left: pointer.x - 5, // 반지름 반영 + top: pointer.y - 5, // 반지름 반영 + ...circleOptions, + }) + points.push(point) + canvas.add(point) + } else if (points.length === 1) { + // 두 번째 포인트는 첫 번째 포인트를 기준으로 수평 또는 수직으로만 배치 + const p1 = points[0] + + point = new fabric.Circle({ + left: pointer.x - 5, // 반지름 반영 + top: pointer.y - 5, // 반지름 반영 + ...circleOptions, + }) + + points.push(point) + canvas.add(point) + + // 두 포인트의 중심 좌표 계산 + const p2 = points[1] + + const p1CenterX = p1.left + p1.radius + const p1CenterY = p1.top + p1.radius + const p2CenterX = p2.left + p2.radius + const p2CenterY = p2.top + p2.radius + + const p3 = new fabric.Point(p2CenterX, p1CenterY) + + // 두 포인트 간에 직선을 그림 (중심을 기준으로) + const line = new fabric.Line([p1CenterX, p1CenterY, p2CenterX, p2CenterY], lineOptions) + const line2 = new fabric.Line([p2CenterX, p2CenterY, p3.x, p3.y], lineOptions) + const line3 = new fabric.Line([p3.x, p3.y, p1CenterX, p1CenterY], lineOptions) + canvas.add(line) + canvas.add(line2) + canvas.add(line3) + + const distance1 = getDistance(p1CenterX, p1CenterY, p2CenterX, p2CenterY) + const distance2 = getDistance(p2CenterX, p2CenterY, p3.x, p3.y) + const distance3 = getDistance(p3.x, p3.y, p1CenterX, p1CenterY) + + // 거리 텍스트가 이미 있으면 업데이트하고, 없으면 새로 생성 + distanceText = new fabric.Text(`${distance1}`, { + left: (p1CenterX + p2CenterX) / 2, + top: (p1CenterY + p2CenterY) / 2, + ...textOptions, + }) + canvas.add(distanceText) + distanceText = new fabric.Text(`${distance2}`, { + left: (p2CenterX + p3.x) / 2, + top: (p2CenterY + p3.y) / 2, + ...textOptions, + }) + canvas.add(distanceText) + distanceText = new fabric.Text(`${distance3}`, { + left: (p3.x + p1CenterX) / 2, + top: (p3.y + p1CenterY) / 2, + ...textOptions, + }) + canvas.add(distanceText) + + // 거리 계산 후, 다음 측정을 위해 초기화 + points = [] + } + + // 캔버스 다시 그리기 + canvas.renderAll() + }) + } + } + useEffect(() => { if (globalLocale === 'ko') { setAppMessageState(KO) @@ -163,6 +499,35 @@ export default function CanvasMenu(props) { } }, [menuNumber, type, globalLocale]) + const commonFunctions = (mode) => { + let tempStates = { ...commonFunctionState } + + if (tempStates[mode]) { + tempStates[mode] = false + } else { + Object.keys(tempStates).forEach((key) => { + tempStates[key] = false + }) + + if (mode !== undefined) { + tempStates[mode] = true + } + } + + setCommonFunctionState(tempStates) + } + + useEffect(() => { + initEvent() + if (commonFunctionState.text) { + commonTextMode() + } else if (commonFunctionState.dimension) { + commonDimensionMode() + } else if (commonFunctionState.distance) { + commonDistanceMode() + } + }, [commonFunctionState]) + return (