오브젝트 배치 모드 추가

This commit is contained in:
hyojun.choi 2024-09-09 11:03:30 +09:00
parent 70aeaa2957
commit 958a8888c3
5 changed files with 339 additions and 4 deletions

View File

@ -12,6 +12,8 @@ export const Mode = {
CELL_POWERCON: 'cellPowercon', //파워콘
DRAW_HELP_LINE: 'drawHelpLine', // 보조선 그리기 모드 지붕 존재해야함
ADSORPTION_POINT: 'adsorptionPoint', //흡착점 모드
OPENING: 'opening', //개구 모드
SHADOW: 'shadow', //그림자 생성 모드
DEFAULT: 'default',
}

View File

@ -39,6 +39,7 @@ import GridSettingsModal from './GridSettingsModal'
import { SurfaceShapeModal } from '@/components/ui/SurfaceShape'
import { drawDirectionStringToArrow } from '@/util/qpolygon-utils'
import ThumbnailList from '@/components/ui/ThumbnailLIst'
import ObjectPlacement from '@/components/ui/ObjectPlacement'
export default function Roof2(props) {
const { name, userId, email, isLoggedIn } = props
@ -753,6 +754,15 @@ export default function Roof2(props) {
>
면형상
</Button>
<Button
className="m-1 p-2"
onClick={() => {
setContent(<ObjectPlacement canvas={canvas} />)
setOpen(true)
}}
>
오브젝트 배치
</Button>
{/*<Button className="m-1 p-2" onClick={rotateShape}>
회전
</Button>*/}

View File

@ -0,0 +1,155 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { Button, Input } from '@nextui-org/react'
import { useRecoilState, useSetRecoilState } from 'recoil'
import { modalState } from '@/store/modalAtom'
import { fabric } from 'fabric'
import { QPolygon } from '@/components/fabric/QPolygon'
import { modeState, objectPlacementModeState } from '@/store/canvasAtom'
const BATCH_TYPE = {
OPENING: 'opening',
SHADOW: 'shadow',
}
const INPUT_TYPE = {
FREE: 'free',
DIMENSION: 'dimension',
}
const ObjectPlacement = ({ canvas }) => {
const [open, setOpen] = useRecoilState(modalState)
const [mode, setMode] = useRecoilState(modeState)
const [objectPlacementMode, setObjectPlacementModeState] = useRecoilState(objectPlacementModeState)
const [width, setWidth] = useState(0)
const [height, setHeight] = useState(0)
const [areaBoundary, setAreaBoundary] = useState(true)
// opening or shadow /
const [batchType, setBatchType] = useState(BATCH_TYPE.OPENING)
// free or dimension /
const [inputType, setInputType] = useState(INPUT_TYPE.FREE)
const handleSave = () => {
setMode(batchType)
setOpen(false)
}
return (
<div className="p-4 w-full max-w-xs border border-gray-300">
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2">오브젝트 배치</label>
</div>
<div className="border-b mb-4">
<h2 className="font-semibold">개구 · 그림자 배치</h2>
</div>
<div className="mb-4">
<div className="flex">
<Button
className={`w-1/2 py-2 ${objectPlacementMode.batchType === BATCH_TYPE.OPENING ? 'bg-blue-500 text-white rounded-l' : 'bg-gray-200 text-gray-700 rounded-r'}`}
onClick={() => {
setBatchType(BATCH_TYPE.OPENING)
setObjectPlacementModeState({ ...objectPlacementMode, batchType: BATCH_TYPE.OPENING })
}}
>
개구 배치
</Button>
<Button
className={`w-1/2 py-2 ${objectPlacementMode.batchType === BATCH_TYPE.SHADOW ? 'bg-blue-500 text-white rounded-l' : 'bg-gray-200 text-gray-700 rounded-r'}`}
onClick={() => {
setBatchType(BATCH_TYPE.SHADOW)
setObjectPlacementModeState({ ...objectPlacementMode, batchType: BATCH_TYPE.SHADOW })
}}
>
그림자 배치
</Button>
</div>
</div>
<div className="mb-4">
<div className="mb-2 text-gray-700 font-semibold">설정</div>
<div className="mb-2">
<label className="inline-flex items-center">
<Input
type="radio"
name="inputType"
checked={objectPlacementMode.inputType === INPUT_TYPE.FREE}
onClick={() => {
setObjectPlacementModeState({ ...objectPlacementMode, inputType: INPUT_TYPE.FREE })
}}
className="form-radio text-blue-500"
/>
<span className="ml-2">프리입력</span>
</label>
</div>
<div className="mb-2">
<label className="inline-flex items-center">
<Input
type="radio"
name="inputType"
checked={objectPlacementMode.inputType === INPUT_TYPE.DIMENSION}
onClick={() => {
setObjectPlacementModeState({ ...objectPlacementMode, inputType: INPUT_TYPE.DIMENSION })
}}
className="form-radio text-blue-500"
/>
<span className="ml-2">치수입력</span>
</label>
</div>
<div className="flex mb-2">
<div className="mr-2">
<label className="block text-gray-700 text-sm mb-1">가로길이</label>
<Input
type="text"
className="w-full px-3 py-2 border rounded"
placeholder="mm"
value={objectPlacementMode.width}
onChange={(e) => {
setObjectPlacementModeState({ ...objectPlacementMode, width: e.target.value })
}}
/>
</div>
<div>
<label className="block text-gray-700 text-sm mb-1">세로길이</label>
<Input
type="text"
className="w-full px-3 py-2 border rounded"
placeholder="mm"
value={objectPlacementMode.height}
onChange={(e) => {
setObjectPlacementModeState({ ...objectPlacementMode, height: e.target.value })
}}
/>
</div>
</div>
<div className="mb-4">
<label className="inline-flex items-center">
<Input
type="checkbox"
name={`areaBoundary`}
checked={objectPlacementMode.areaBoundary}
onClick={() => setObjectPlacementModeState({ ...objectPlacementMode, areaBoundary: !objectPlacementMode.areaBoundary })}
className="form-checkbox text-blue-500"
/>
<span className="ml-2">영역교차</span>
</label>
</div>
<div className="text-center">
<Button onClick={handleSave} className="bg-gray-500 text-white py-2 px-4 rounded">
저장
</Button>
</div>
</div>
</div>
)
}
export default ObjectPlacement

View File

@ -27,6 +27,7 @@ import {
guideLineState,
horiGuideLinesState,
vertGuideLinesState,
objectPlacementModeState,
} from '@/store/canvasAtom'
import { QLine } from '@/components/fabric/QLine'
import { fabric } from 'fabric'
@ -74,6 +75,8 @@ export function useMode() {
const [horiGuideLines, setHoriGuideLines] = useRecoilState(horiGuideLinesState)
const [vertGuideLines, setVertGuideLines] = useRecoilState(vertGuideLinesState)
const [objectPlacementMode, setObjectPlacementModeState] = useRecoilState(objectPlacementModeState)
useEffect(() => {
// if (!canvas) {
// canvas?.setZoom(0.8)
@ -104,11 +107,7 @@ export function useMode() {
}, [endPoint])
useEffect(() => {
canvas?.off('mouse:out', removeMouseLines)
canvas?.on('mouse:out', removeMouseLines)
changeMode(canvas, mode)
canvas?.off('mouse:move')
canvas?.on('mouse:move', drawMouseLines)
}, [mode, horiGuideLines, vertGuideLines])
useEffect(() => {
@ -423,6 +422,17 @@ export function useMode() {
break
case 'adsorptionPoint':
canvas?.on('mouse:down', mouseEvent.adsorptionPoint)
break
case 'shadow':
canvas?.on('mouse:down', mouseEvent.shadowMode.down)
canvas?.on('mouse:move', mouseEvent.shadowMode.move)
canvas?.on('mouse:up', mouseEvent.shadowMode.up)
break
case 'opening':
canvas?.on('mouse:down', mouseEvent.openingMode.down)
canvas?.on('mouse:move', mouseEvent.openingMode.move)
canvas?.on('mouse:up', mouseEvent.openingMode.up)
break
case 'default':
canvas?.off('mouse:down')
@ -587,6 +597,9 @@ export function useMode() {
const mouseAndkeyboardEventClear = () => {
canvas?.off('mouse:down')
canvas?.off('mouse:move')
canvas?.off('mouse:up')
canvas?.off('mouse:out')
Object.keys(mouseEvent).forEach((key) => {
canvas?.off('mouse:down', mouseEvent[key])
document.removeEventListener('contextmenu', mouseEvent[key])
@ -682,6 +695,9 @@ export function useMode() {
changeMouseEvent(mode)
changeKeyboardEvent(mode)
canvas?.on('mouse:move', drawMouseLines)
canvas?.on('mouse:out', removeMouseLines)
switch (mode) {
case 'template':
templateMode()
@ -992,6 +1008,146 @@ export function useMode() {
canvas.add(circle)
canvas.renderAll()
},
//면 형상 배치 모드
surfaceShapeMode: (o) => {},
// 그림자 모드
shadowMode: {
rect: null,
isDown: false,
origX: 0,
origY: 0,
down: (o) => {
if (mode !== Mode.SHADOW) return
mouseEvent.shadowMode.isDown = true
const pointer = canvas.getPointer(o.e)
mouseEvent.shadowMode.origX = pointer.x
mouseEvent.shadowMode.origY = pointer.y
mouseEvent.shadowMode.rect = new fabric.Rect({
fill: 'grey',
left: mouseEvent.shadowMode.origX,
top: mouseEvent.shadowMode.origY,
originX: 'left',
originY: 'top',
opacity: 0.3,
width: 0,
height: 0,
angle: 0,
transparentCorners: false,
})
canvas.add(mouseEvent.shadowMode.rect)
},
move: (e) => {
if (!mouseEvent.shadowMode.isDown) return
const pointer = canvas.getPointer(e.e)
if (mouseEvent.shadowMode.origX > pointer.x) {
mouseEvent.shadowMode.rect.set({ left: Math.abs(pointer.x) })
}
if (mouseEvent.shadowMode.origY > pointer.y) {
mouseEvent.shadowMode.rect.set({ top: Math.abs(pointer.y) })
}
mouseEvent.shadowMode.rect.set({ width: Math.abs(mouseEvent.shadowMode.origX - pointer.x) })
mouseEvent.shadowMode.rect.set({ height: Math.abs(mouseEvent.shadowMode.origY - pointer.y) })
},
up: (o) => {
mouseEvent.shadowMode.isDown = false
setMode(Mode.DEFAULT)
},
},
openingMode: {
rect: null,
isDown: false,
origX: 0,
origY: 0,
down: (o) => {
if (mode !== Mode.OPENING) return
mouseEvent.openingMode.isDown = true
const pointer = canvas.getPointer(o.e)
mouseEvent.openingMode.origX = pointer.x
mouseEvent.openingMode.origY = pointer.y
mouseEvent.openingMode.rect = new fabric.Rect({
fill: 'white',
stroke: 'black',
strokeWidth: 1,
left: mouseEvent.openingMode.origX,
top: mouseEvent.openingMode.origY,
originX: 'left',
originY: 'top',
width: pointer.x - mouseEvent.openingMode.origX,
height: pointer.y - mouseEvent.openingMode.origY,
angle: 0,
transparentCorners: false,
})
canvas.add(mouseEvent.openingMode.rect)
},
move: (e) => {
if (!mouseEvent.openingMode.isDown) return
const pointer = canvas.getPointer(e.e)
if (mouseEvent.openingMode.origX > pointer.x) {
mouseEvent.openingMode.rect.set({ left: Math.abs(pointer.x) })
}
if (mouseEvent.openingMode.origY > pointer.y) {
mouseEvent.openingMode.rect.set({ top: Math.abs(pointer.y) })
}
mouseEvent.openingMode.rect.set({ width: Math.abs(mouseEvent.openingMode.origX - pointer.x) })
mouseEvent.openingMode.rect.set({ height: Math.abs(mouseEvent.openingMode.origY - pointer.y) })
},
up: (o) => {
mouseEvent.openingMode.isDown = false
const { areaBoundary } = objectPlacementMode
//roof의 내부에 있는지 확인
if (!checkInsideRoof(mouseEvent.openingMode.rect)) {
setMode(Mode.DEFAULT)
}
// 영역 교차인지 확인
if (!areaBoundary) {
const isCross = checkCrossAreaBoundary(mouseEvent.openingMode.rect)
if (isCross) {
alert('영역이 교차되었습니다.')
canvas.remove(mouseEvent.openingMode.rect)
}
}
mouseEvent.openingMode.rect.set({ name: 'opening' })
},
},
}
const checkCrossAreaBoundary = (rect) => {
const openings = canvas?._objects.filter((obj) => obj.name === 'opening')
if (openings.length === 0) {
return false
}
for (let i = 0; i < openings.length; i++) {
const rect2 = openings[i]
// Check if one rectangle is to the left of the other
if (rect.x + rect.width <= rect2.x || rect2.x + rect2.width <= rect.x) {
return true
}
// Check if one rectangle is above the other
if (rect.y + rect.height <= rect2.y || rect2.y + rect2.height <= rect.y) {
return true
}
}
return false
}
const checkInsideRoof = (rect) => {
let result = true
const roofs = canvas?._objects.filter((obj) => obj.name === 'roof')
if (roofs.length === 0) {
alert('지붕을 먼저 그려주세요')
canvas?.remove(rect)
return false
}
return result
}
const getInterSectPointByMouseLine = () => {

View File

@ -150,3 +150,15 @@ export const globalCompassState = atom({
default: 0,
dangerouslyAllowMutability: true,
})
// 면형상 배치 모드
export const surfacePlacementModeState = atom({
key: 'surfacePlacementMode',
default: { width: 0, height: 0, areaBoundary: true, inputType: 'free' },
})
// 오브젝트 배치 모드
export const objectPlacementModeState = atom({
key: 'objectPlacementMode',
default: { width: 0, height: 0, areaBoundary: true, inputType: 'free', batchType: 'opening' },
})