From 363214654d6985fa330e1db6bf571ce20be93f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=AF=BC=EC=8B=9D?= <43837214+Minsiki@users.noreply.github.com> Date: Thu, 26 Dec 2024 13:23:57 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=EC=97=B4.=EB=8B=A8=20=EC=9D=B4=EB=8F=99/?= =?UTF-8?q?=EB=B3=B5=EC=82=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../floor-plan/modal/module/PanelEdit.jsx | 18 +- src/hooks/module/useModule.js | 330 ++++++++++++++++++ 2 files changed, 347 insertions(+), 1 deletion(-) create mode 100644 src/hooks/module/useModule.js diff --git a/src/components/floor-plan/modal/module/PanelEdit.jsx b/src/components/floor-plan/modal/module/PanelEdit.jsx index c63dd0cd..0e7fde7f 100644 --- a/src/components/floor-plan/modal/module/PanelEdit.jsx +++ b/src/components/floor-plan/modal/module/PanelEdit.jsx @@ -9,6 +9,8 @@ import { deepCopyArray } from '@/util/common-utils' import { canvasState } from '@/store/canvasAtom' import * as turf from '@turf/turf' import { POLYGON_TYPE } from '@/common/common' +import { useModal } from '@nextui-org/react' +import { useModule } from '@/hooks/module/useModule' export default function PanelEdit(props) { const contextPopupPosition = useRecoilValue(contextPopupPositionState) @@ -18,6 +20,7 @@ export default function PanelEdit(props) { const [direction, setDirection] = useState('up') const { getMessage } = useMessage() const canvas = useRecoilValue(canvasState) + const { moduleMove, moduleCopy, moduleMultiMove, moduleMultiCopy } = useModule() useEffect(() => { if (canvas) { @@ -28,7 +31,20 @@ export default function PanelEdit(props) { //모듈 이동 적용 const handleApply = () => { - contextModuleMove(length, direction) + // const activeModuleIds = canvas.getActiveObjects().map((obj) => obj.id) + if (type === 'move') { + moduleMove(length, direction) + } else if (type === 'copy') { + moduleCopy(length, direction) + } else if (type === 'columnMove') { + moduleMultiMove('column', length, direction) + } else if (type === 'columnCopy') { + moduleMultiCopy('column', length, direction) + } else if (type === 'rowMove') { + moduleMultiMove('row', length, direction) + } else if (type === 'rowCopy') { + moduleMultiCopy('row', length, direction) + } closePopup(id) } diff --git a/src/hooks/module/useModule.js b/src/hooks/module/useModule.js new file mode 100644 index 00000000..cc6ef05e --- /dev/null +++ b/src/hooks/module/useModule.js @@ -0,0 +1,330 @@ +import { BATCH_TYPE, POLYGON_TYPE } from '@/common/common' +import { canvasState } from '@/store/canvasAtom' +import { isOverlap, polygonToTurfPolygon } from '@/util/canvas-util' +import { useRecoilValue } from 'recoil' +import { v4 as uuidv4 } from 'uuid' +import * as turf from '@turf/turf' +import { useSwal } from '../useSwal' +import { QPolygon } from '@/components/fabric/QPolygon' + +export function useModule() { + const canvas = useRecoilValue(canvasState) + const { swalFire } = useSwal() + + const moduleMove = (length, direction) => { + const checkModuleDisjointSurface = (squarePolygon, turfModuleSetupSurface) => { + return turf.booleanContains(turfModuleSetupSurface, squarePolygon) || turf.booleanWithin(squarePolygon, turfModuleSetupSurface) + } + + const selectedObj = canvas.getActiveObjects() //선택된 객체들을 가져옴 + const selectedIds = selectedObj.map((obj) => obj.id) // selectedObj의 ID 추출 + + canvas.discardActiveObject() //선택해제 + + const isSetupModules = canvas.getObjects().filter((obj) => obj.name === 'module' && !selectedIds.includes(obj.id)) // selectedObj에 없는 객체만 필터링 + const selectedModules = canvas.getObjects().filter((obj) => selectedIds.includes(obj.id) && obj.name === 'module') //선택했던 객체들만 가져옴 + const setupSurface = canvas + .getObjects() + .filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE && obj.id === selectedModules[0].surfaceId)[0] + const isOverlapArray = [] + const isInSurfaceArray = [] + + if (selectedModules) { + canvas.remove(...selectedModules) + selectedModules.forEach((module) => { + module.set({ + originCoords: { + left: module.left, + top: module.top, + }, + }) + + if (direction === 'up') { + module.set({ ...module, top: module.top - Number(length) }) + } else if (direction === 'down') { + module.set({ ...module, top: module.top + Number(length) }) + } else if (direction === 'left') { + module.set({ ...module, left: module.left - Number(length) }) + } else if (direction === 'right') { + module.set({ ...module, left: module.left + Number(length) }) + } + module.setCoords() + canvas.renderAll() + + //다른 모듈과 겹치는지 확인하는 로직 + const isOverlap = isSetupModules.some((isSetupModule) => + turf.booleanOverlap(polygonToTurfPolygon(module, true), polygonToTurfPolygon(isSetupModule, true)), + ) + isOverlapArray.push(isOverlap) + + const turfModuleSetupSurface = polygonToTurfPolygon(setupSurface, true) + const turfModule = polygonToTurfPolygon(module, true) + + //나갔는지 확인하는 로직 + const isInSurface = turf.booleanContains(turfModuleSetupSurface, turfModule) || turf.booleanWithin(turfModule, turfModuleSetupSurface) + isInSurfaceArray.push(isInSurface) + }) + + const isNotOverlap = isOverlapArray.some((isOverlap) => isOverlap) // true면 겹침 + const isNotOutSurface = isInSurfaceArray.every((isOutSurface) => isOutSurface) //false면 밖으로 나감 + + //안겹치고 안나갔으면 이동시킨다 아니면 원래 위치로 돌려놓는다 + if (isNotOverlap || !isNotOutSurface) { + selectedModules.forEach((module) => { + module.set({ ...module, left: module.originCoords.left, top: module.originCoords.top }) + module.setCoords() + }) + } + + canvas.add(...selectedModules) + canvas.renderAll() + } + } + + const moduleCopy = (length, direction) => { + if (canvas.getActiveObjects().length === 0) return + const activeModuleIds = canvas.getActiveObjects().map((obj) => obj.id) + const modules = canvas.getObjects().filter((obj) => activeModuleIds.includes(obj.id)) + const otherModules = canvas.getObjects().filter((obj) => obj.surfaceId === modules[0].surfaceId && obj.name === POLYGON_TYPE.MODULE) + const moduleSetupSurface = canvas + .getObjects() + .filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE && obj.id === modules[0].surfaceId)[0] + + canvas.discardActiveObject() //선택해제 + modules.forEach((module) => { + const { top, left } = getPosotion(module, direction, length) + const moduleOptions = { ...module, left, top, id: uuidv4() } + const rect = new QPolygon(module.points, moduleOptions) + canvas.add(rect) + rect.setCoords() + canvas.renderAll() + + // const isOverlapOtherModules = otherModules.some((otherModule) => rect.intersectsWithObject(otherModule)) + const isOverlapOtherModules = otherModules.some( + (otherModule) => + turf.booleanOverlap(polygonToTurfPolygon(rect, true), polygonToTurfPolygon(otherModule, true)) || + turf.booleanWithin(polygonToTurfPolygon(rect, true), polygonToTurfPolygon(otherModule, true)), + ) + + const isOutsideSurface = + !turf.booleanContains(polygonToTurfPolygon(moduleSetupSurface, true), polygonToTurfPolygon(rect, true)) || + !turf.booleanWithin(polygonToTurfPolygon(rect, true), polygonToTurfPolygon(moduleSetupSurface, true)) + + if (isOverlapOtherModules || isOutsideSurface) { + swalFire({ + title: isOverlapOtherModules ? '겹치는 모듈이 있습니다.' : '영역 밖', + icon: 'error', + type: 'confirm', + confirmFn: () => { + canvas.remove(rect) + canvas.renderAll() + }, + }) + } + }) + } + const moduleMultiMove = (type, length, direction) => { + if (canvas.getActiveObjects().length === 0) return + if (canvas.getActiveObjects().length > 1) { + swalFire({ + title: '여러 개의 모듈을 선택할 수 없습니다.', + icon: 'error', + type: 'alert', + }) + canvas.discardActiveObject() + return + } + const activeModule = canvas.getObjects().filter((obj) => canvas.getActiveObjects()[0].id === obj.id)[0] + const modules = type === 'row' ? getRowModules(activeModule) : getColumnModules(activeModule) + const otherModules = getOtherModules(modules) + const objects = getObjects() + const moduleSetupSurface = canvas + .getObjects() + .filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE && obj.id === activeModule.surfaceId)[0] + let [isOverlapOtherModules, isOverlapObjects, isOutsideSurface] = [[], [], []] + + modules.forEach((module) => { + const { top, left } = getPosotion(module, direction, length, false) + module.originPos = { + top: module.top, + left: module.left, + } + + module.set({ top, left }) + module.setCoords() + canvas.renderAll() + + if (otherModules.length > 0) { + isOverlapOtherModules.push( + otherModules.some( + (otherModule) => + turf.booleanOverlap(polygonToTurfPolygon(module, true), polygonToTurfPolygon(otherModule, true)) || + turf.booleanWithin(polygonToTurfPolygon(module, true), polygonToTurfPolygon(otherModule, true)), + ), + ) + } + + if (objects.length > 0) { + isOverlapObjects.push( + objects.some( + (object) => + turf.booleanOverlap(polygonToTurfPolygon(module, true), polygonToTurfPolygon(object, true)) || + turf.booleanWithin(polygonToTurfPolygon(module, true), polygonToTurfPolygon(object, true)), + ), + ) + } + + isOutsideSurface.push( + !turf.booleanContains(polygonToTurfPolygon(moduleSetupSurface, true), polygonToTurfPolygon(module, true)) || + !turf.booleanWithin(polygonToTurfPolygon(module, true), polygonToTurfPolygon(moduleSetupSurface, true)), + ) + }) + + if ( + isOverlapOtherModules.some((isOverlap) => isOverlap) || + isOverlapObjects.some((isOverlap) => isOverlap) || + isOutsideSurface.some((isOutside) => isOutside) + ) { + swalFire({ + title: isOverlapOtherModules.some((isOverlap) => isOverlap) ? '겹치는 모듈이 있습니다.' : '영역 밖', + icon: 'error', + type: 'confirm', + confirmFn: () => { + modules.forEach((module) => { + module.set({ top: module.originPos.top, left: module.originPos.left }) + module.setCoords() + }) + canvas.renderAll() + }, + }) + } + } + + const moduleMultiCopy = (type, length, direction) => { + if (canvas.getActiveObjects().length === 0) return + if (canvas.getActiveObjects().length > 1) { + swalFire({ + title: '여러 개의 모듈을 선택할 수 없습니다.', + icon: 'error', + type: 'alert', + }) + canvas.discardActiveObject() + return + } + const activeModule = canvas.getObjects().filter((obj) => canvas.getActiveObjects()[0].id === obj.id)[0] + const modules = type === 'row' ? getRowModules(activeModule) : getColumnModules(activeModule) + const otherModules = canvas.getObjects().filter((obj) => obj.surfaceId === modules[0].surfaceId && obj.name === POLYGON_TYPE.MODULE) + const objects = getObjects() + const copyRects = [] + const moduleSetupSurface = canvas + .getObjects() + .filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE && obj.id === modules[0].surfaceId)[0] + let [isOverlapOtherModules, isOverlapObjects, isOutsideSurface] = [[], [], []] + + modules.forEach((module) => { + const { top, left } = getPosotion(module, direction, length, true) + const moduleOptions = { ...module, left, top, id: uuidv4() } + const rect = new QPolygon(module.points, moduleOptions) + canvas.add(rect) + copyRects.push(rect) + module.setCoords() + + if (otherModules.length > 0) { + isOverlapOtherModules.push( + otherModules.some( + (otherModule) => + turf.booleanOverlap(polygonToTurfPolygon(rect, true), polygonToTurfPolygon(otherModule, true)) || + turf.booleanWithin(polygonToTurfPolygon(rect, true), polygonToTurfPolygon(otherModule, true)), + ), + ) + } + + if (objects.length > 0) { + isOverlapObjects.push( + objects.some( + (object) => + turf.booleanOverlap(polygonToTurfPolygon(module, true), polygonToTurfPolygon(object, true)) || + turf.booleanWithin(polygonToTurfPolygon(module, true), polygonToTurfPolygon(object, true)), + ), + ) + } + + isOutsideSurface.push( + !turf.booleanContains(polygonToTurfPolygon(moduleSetupSurface, true), polygonToTurfPolygon(rect, true)) || + !turf.booleanWithin(polygonToTurfPolygon(rect, true), polygonToTurfPolygon(moduleSetupSurface, true)), + ) + }) + + console.log( + '🚀 ~ moduleMultiCopy ~ sOverlapOtherModules, isOverlapObjects, isOutsideSurface:', + isOverlapOtherModules, + isOverlapObjects, + isOutsideSurface, + ) + if ( + isOverlapOtherModules.some((isOverlap) => isOverlap) || + isOverlapObjects.some((isOverlap) => isOverlap) || + isOutsideSurface.some((isOutside) => isOutside) + ) { + swalFire({ + title: isOverlapOtherModules.some((isOverlap) => isOverlap) ? '겹치는 모듈이 있습니다.' : '영역 밖', + icon: 'error', + type: 'confirm', + confirmFn: () => { + canvas.remove(...copyRects) + canvas.renderAll() + }, + }) + } else { + canvas.renderAll() + } + } + + const getRowModules = (target) => { + return canvas.getObjects().filter((obj) => target.surfaceId === obj.surfaceId && obj.name === POLYGON_TYPE.MODULE && obj.top === target.top) + } + + const getColumnModules = (target) => { + return canvas.getObjects().filter((obj) => target.surfaceId === obj.surfaceId && obj.name === POLYGON_TYPE.MODULE && obj.left === target.left) + } + + const getPosotion = (target, direction, length, hasMargin = false) => { + let top = target.top + let left = target.left + + if (direction === 'up') { + top = Number(target.top) - Number(length) + top = hasMargin ? top - Number(target.height) : top + } else if (direction === 'down') { + top = Number(target.top) + Number(target.height) + Number(length) + top = hasMargin ? top + Number(target.height) : top + } else if (direction === 'left') { + left = Number(target.left) - Number(length) + left = hasMargin ? left - Number(target.width) : left + } else if (direction === 'right') { + left = Number(target.left) + Number(length) + left = hasMargin ? left + Number(target.width) : left + } + return { top, left } + } + + const getOtherModules = (modules) => { + const moduleIds = modules.map((module) => module.id) + return canvas + .getObjects() + .filter((obj) => obj.surfaceId === modules[0].surfaceId && obj.name === POLYGON_TYPE.MODULE && !moduleIds.includes(obj.id)) + } + + const getObjects = () => { + return canvas + ?.getObjects() + .filter((obj) => [BATCH_TYPE.OPENING, BATCH_TYPE.TRIANGLE_DORMER, BATCH_TYPE.PENTAGON_DORMER, BATCH_TYPE.SHADOW].includes(obj.name)) + } + + return { + moduleMove, + moduleMultiMove, + moduleCopy, + moduleMultiCopy, + } +} From f56e5ca4b0e2543d604a135bd2afb2dbb0743d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=AF=BC=EC=8B=9D?= <43837214+Minsiki@users.noreply.github.com> Date: Thu, 26 Dec 2024 18:07:55 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=EC=97=B4.=EB=8B=A8=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../floor-plan/modal/module/PanelEdit.jsx | 23 +- .../modal/module/column/ColumnRemove.jsx | 27 +- .../floor-plan/modal/module/row/RowRemove.jsx | 29 +- src/hooks/module/useModule.js | 279 +++++++++++++++++- src/hooks/useContextMenu.js | 14 +- 5 files changed, 326 insertions(+), 46 deletions(-) diff --git a/src/components/floor-plan/modal/module/PanelEdit.jsx b/src/components/floor-plan/modal/module/PanelEdit.jsx index 0e7fde7f..a3a89dcf 100644 --- a/src/components/floor-plan/modal/module/PanelEdit.jsx +++ b/src/components/floor-plan/modal/module/PanelEdit.jsx @@ -12,9 +12,18 @@ import { POLYGON_TYPE } from '@/common/common' import { useModal } from '@nextui-org/react' import { useModule } from '@/hooks/module/useModule' +export const PANEL_EDIT_TYPE = { + MOVE: 'move', + COPY: 'copy', + COLUMN_MOVE: 'columnMove', + COLUMN_COPY: 'columnCopy', + ROW_MOVE: 'rowMove', + ROW_COPY: 'rowCopy', +} + export default function PanelEdit(props) { const contextPopupPosition = useRecoilValue(contextPopupPositionState) - const { id, pos = contextPopupPosition, type = 'move', apply } = props + const { id, pos = contextPopupPosition, type = PANEL_EDIT_TYPE.MOVE, apply } = props const { closePopup } = usePopup() const [length, setLength] = useState(0) const [direction, setDirection] = useState('up') @@ -32,17 +41,17 @@ export default function PanelEdit(props) { //모듈 이동 적용 const handleApply = () => { // const activeModuleIds = canvas.getActiveObjects().map((obj) => obj.id) - if (type === 'move') { + if (type === PANEL_EDIT_TYPE.MOVE) { moduleMove(length, direction) - } else if (type === 'copy') { + } else if (type === PANEL_EDIT_TYPE.COPY) { moduleCopy(length, direction) - } else if (type === 'columnMove') { + } else if (type === PANEL_EDIT_TYPE.COLUMN_MOVE) { moduleMultiMove('column', length, direction) - } else if (type === 'columnCopy') { + } else if (type === PANEL_EDIT_TYPE.COLUMN_COPY) { moduleMultiCopy('column', length, direction) - } else if (type === 'rowMove') { + } else if (type === PANEL_EDIT_TYPE.ROW_MOVE) { moduleMultiMove('row', length, direction) - } else if (type === 'rowCopy') { + } else if (type === PANEL_EDIT_TYPE.ROW_COPY) { moduleMultiCopy('row', length, direction) } closePopup(id) diff --git a/src/components/floor-plan/modal/module/column/ColumnRemove.jsx b/src/components/floor-plan/modal/module/column/ColumnRemove.jsx index 24219d78..a18d7982 100644 --- a/src/components/floor-plan/modal/module/column/ColumnRemove.jsx +++ b/src/components/floor-plan/modal/module/column/ColumnRemove.jsx @@ -5,21 +5,24 @@ import { usePopup } from '@/hooks/usePopup' import { useMessage } from '@/hooks/useMessage' import { useState } from 'react' import Image from 'next/image' +import { MODULE_REMOVE_TYPE, useModule } from '@/hooks/module/useModule' export default function ColumnRemove(props) { const contextPopupPosition = useRecoilValue(contextPopupPositionState) const { id, pos = contextPopupPosition, apply } = props const { closePopup } = usePopup() - const [selectedType, setSelectedType] = useState(1) + const [selectedType, setSelectedType] = useState(MODULE_REMOVE_TYPE.LEFT) const { getMessage } = useMessage() + const { moduleColumnRemove } = useModule() const types = [ - { name: getMessage('modal.panel.column.remove.type.left'), value: 1 }, - { name: getMessage('modal.panel.column.remove.type.right'), value: 2 }, - { name: getMessage('modal.panel.column.remove.type.side'), value: 3 }, - { name: getMessage('modal.panel.column.remove.type.none'), value: 4 }, + { name: getMessage('modal.panel.column.remove.type.left'), value: MODULE_REMOVE_TYPE.LEFT }, + { name: getMessage('modal.panel.column.remove.type.right'), value: MODULE_REMOVE_TYPE.RIGHT }, + { name: getMessage('modal.panel.column.remove.type.side'), value: MODULE_REMOVE_TYPE.HORIZONTAL_SIDE }, + { name: getMessage('modal.panel.column.remove.type.none'), value: MODULE_REMOVE_TYPE.NONE }, ] const handleApply = () => { - if (apply) apply() + // if (apply) apply() + moduleColumnRemove(selectedType) closePopup(id) } @@ -39,12 +42,12 @@ export default function ColumnRemove(props) {