This commit is contained in:
changkyu choi 2024-08-26 16:33:52 +09:00
commit c226644556
10 changed files with 459 additions and 330 deletions

View File

@ -11,6 +11,7 @@ import { useRecoilState, useRecoilValue } from 'recoil'
import { import {
canvasSizeState, canvasSizeState,
compassState, compassState,
currentObjectState,
fontSizeState, fontSizeState,
roofMaterialState, roofMaterialState,
roofState, roofState,
@ -27,6 +28,8 @@ import QContextMenu from './common/context-menu/QContextMenu'
import { modalContent, modalState } from '@/store/modalAtom' import { modalContent, modalState } from '@/store/modalAtom'
import SettingsModal from './SettingsModal' import SettingsModal from './SettingsModal'
import { useAxios } from '@/hooks/useAxios' import { useAxios } from '@/hooks/useAxios'
import QPolygonContextMenu from '@/components/common/context-menu/QPolygonContextMenu'
import QLineContextMenu from '@/components/common/context-menu/QLineContextMenu'
export default function Roof2(props) { export default function Roof2(props) {
const { name, userId, email, isLoggedIn } = props const { name, userId, email, isLoggedIn } = props
@ -67,6 +70,7 @@ export default function Roof2(props) {
const [contents, setContent] = useRecoilState(modalContent) const [contents, setContent] = useRecoilState(modalContent)
const [scale, setScale] = useState(1) const [scale, setScale] = useState(1)
const currentObject = useRecoilValue(currentObjectState)
//canvas //canvas
const [thumbnails, setThumbnails] = useState([]) const [thumbnails, setThumbnails] = useState([])
@ -80,7 +84,6 @@ export default function Roof2(props) {
setMode, setMode,
changeMode, changeMode,
handleClear, handleClear,
fillCellInPolygon,
zoomIn, zoomIn,
zoomOut, zoomOut,
zoom, zoom,
@ -126,6 +129,7 @@ export default function Roof2(props) {
if (canvas) { if (canvas) {
const line = new QLine([50, 50, 200, 50], { const line = new QLine([50, 50, 200, 50], {
stroke: 'black', stroke: 'black',
selectable: true,
strokeWidth: 2, strokeWidth: 2,
fontSize: fontSize, fontSize: fontSize,
}) })
@ -580,7 +584,7 @@ export default function Roof2(props) {
> >
그리드 설정 그리드 설정
</Button> </Button>
<Button className="m-1 p-2" color={`${mode === Mode.DEFAULT ? 'primary' : 'default'}`} onClick={fillCellInPolygon}> <Button className="m-1 p-2" color={`${mode === Mode.DEFAULT ? 'primary' : 'default'}`} onClick={() => setMode(Mode.DEFAULT)}>
모드 DEFAULT 모드 DEFAULT
</Button> </Button>
<Button className="m-1 p-2" color={`${mode === Mode.DRAW_LINE ? 'primary' : 'default'}`} onClick={() => setMode(Mode.DRAW_LINE)}> <Button className="m-1 p-2" color={`${mode === Mode.DRAW_LINE ? 'primary' : 'default'}`} onClick={() => setMode(Mode.DRAW_LINE)}>
@ -758,7 +762,13 @@ export default function Roof2(props) {
<ThumbnailList {...thumbnailProps} /> <ThumbnailList {...thumbnailProps} />
<div className="flex justify-start my-8 mx-2 w-full"> <div className="flex justify-start my-8 mx-2 w-full">
<canvas ref={canvasRef} id="canvas" style={{ border: '1px solid black' }} /> <canvas ref={canvasRef} id="canvas" style={{ border: '1px solid black' }} />
{canvas !== undefined && <QContextMenu contextRef={canvasRef} canvasProps={canvas} />} {!canvas ? null : currentObject?.type === 'QPolygon' ? (
<QPolygonContextMenu contextRef={canvasRef} canvasProps={canvas} />
) : currentObject?.type === 'QLine' ? (
<QLineContextMenu contextRef={canvasRef} canvasProps={canvas} />
) : (
<QContextMenu contextRef={canvasRef} canvasProps={canvas} />
)}
</div> </div>
</> </>
) )

View File

@ -9,69 +9,50 @@ export default function SettingsModal(props) {
const { canvasProps } = props const { canvasProps } = props
const [isCustomGridSetting, setIsCustomGridSetting] = useState(true) const [isCustomGridSetting, setIsCustomGridSetting] = useState(true)
const [gridCheckedValue, setGridCheckValue] = useState([]) const [gridCheckedValue, setGridCheckValue] = useState([])
const [ratioValue, setRatioValue] = useState('원치수') const [ratioValue, setRatioValue] = useState('1')
const moduleLength = useRef(90) // mm const moduleLength = useRef(null) // mm
const customModuleHoriLength = useRef(null)
const customModuleVertLength = useRef(null)
const [open, setOpen] = useRecoilState(modalState) const [open, setOpen] = useRecoilState(modalState)
const [guideLine, setGuideLine] = useRecoilState(guideLineState) const [guideLine, setGuideLine] = useRecoilState(guideLineState)
const gridSettingArray = []
useEffect(() => {
moduleLength.current.value = 90
customModuleHoriLength.current.value = 90
customModuleVertLength.current.value = 90
}, [])
useEffect(() => { useEffect(() => {
setIsCustomGridSetting(ratioValue !== 'custom') setIsCustomGridSetting(ratioValue !== 'custom')
}, [ratioValue]) }, [ratioValue])
const drawGridSettings = () => { const drawGridSettings = () => {
const gridSettingArray = [] //
if (!(Object.keys(guideLine).length === 0 && guideLine.constructor === Object)) {
gridSettingArray.push(...guideLine)
}
let moduleHoriLength = moduleLength.current.value //
let moduleVertLength = moduleLength.current.value //
if (ratioValue === 'custom') {
moduleHoriLength = customModuleHoriLength.current.value
moduleVertLength = customModuleVertLength.current.value
} else {
moduleHoriLength = moduleHoriLength / ratioValue
moduleVertLength = moduleVertLength / ratioValue
}
if (gridCheckedValue.includes('line')) { if (gridCheckedValue.includes('line')) {
// fabric.GuideRect = fabric.util.createClass(fabric.Rect, {
// _render: function (ctx) {
// this.callSuper('_render', ctx)
// ctx.strokeStyle = this.stroke || 'blue'
// ctx.lineWidth = this.strokeWidth || 1
// ctx.beginPath()
// ctx.moveTo(-this.width / 2, -this.height / 2)
// ctx.lineTo(this.width / 2, -this.height / 2)
// ctx.lineTo(this.width / 2, this.height / 2)
// ctx.stroke()
// },
// })
// const guideRect = new fabric.GuideRect({
// width: canvasProps.width,
// height: canvasProps.height,
// stroke: 'blue',
// strokeWidth: 1,
// selectable: false,
// fill: 'transparent',
// name: 'guideRect',
// })
// const patternSourceCanvas = new fabric.StaticCanvas(null, {
// width: moduleLength.current.value,
// height: moduleLength.current.value,
// backgroundColor: 'white',
// })
// patternSourceCanvas.add(guideRect)
// patternSourceCanvas.renderAll()
// const pattern = new fabric.Pattern({
// source: patternSourceCanvas.getElement(),
// repeat: 'repeat',
// })
const horizontalLineArray = [] const horizontalLineArray = []
const verticalLineArray = [] const verticalLineArray = []
for (let i = 0; i < canvasProps.height / moduleLength.current.value + 1; i++) { for (let i = 0; i < canvasProps.height / moduleVertLength + 1; i++) {
const horizontalLine = new fabric.Line( const horizontalLine = new fabric.Line(
[ [0, i * moduleVertLength - moduleVertLength / 2, canvasProps.width, i * moduleVertLength - moduleVertLength / 2],
0,
i * moduleLength.current.value - moduleLength.current.value / 2,
canvasProps.width,
i * moduleLength.current.value - moduleLength.current.value / 2,
],
{ {
stroke: 'gray', stroke: 'gray',
strokeWidth: 1, strokeWidth: 1,
@ -83,14 +64,9 @@ export default function SettingsModal(props) {
horizontalLineArray.push(horizontalLine) horizontalLineArray.push(horizontalLine)
} }
for (let i = 0; i < canvasProps.width / moduleLength.current.value + 1; i++) { for (let i = 0; i < canvasProps.width / moduleHoriLength + 1; i++) {
const verticalLine = new fabric.Line( const verticalLine = new fabric.Line(
[ [i * moduleHoriLength - moduleHoriLength / 2, 0, i * moduleHoriLength - moduleHoriLength / 2, canvasProps.height],
i * moduleLength.current.value - moduleLength.current.value / 2,
0,
i * moduleLength.current.value - moduleLength.current.value / 2,
canvasProps.height,
],
{ {
stroke: 'gray', stroke: 'gray',
strokeWidth: 1, strokeWidth: 1,
@ -105,60 +81,12 @@ export default function SettingsModal(props) {
const snapDistance = 10 const snapDistance = 10
canvasProps.on('mouse:move', function (e) {
let mouseObj = e
const mouseCursorX = e.pointer.x
const mouseCursorY = e.pointer.y
horizontalLineArray.forEach((line) => {
if (Math.abs(mouseCursorY - line.y1) < snapDistance) {
mouseObj.x = mouseCursorX
mouseObj.y = line.y1
}
// if (Math.abs(mouseCursorX - line.x2) < snapDistance) {
// line.set({ x2: mouseCursorX })
// }
})
})
// canvasProps.on('mouse:move', (e) => {
// console.log('event', e.pointer)
// const obj = e
// if (!obj) return
// canvasProps.forEachObject(function (target) {
// if (target.name === 'guideLine' || target.name === 'guideDot') {
// console.log('line : ', target)
// const lineStartX = target.x1,
// lineStartY = target.y1
// const lineEndX = target.x2,
// lineEndY = target.y2
// console.log('target.x1', target.x1)
// console.log('target.y1', target.y1)
// console.log(`obj.pointer.y : ${obj.pointer.y}, obj.pointer.x : ${obj.pointer.x}`)
// //
// if (Math.abs(obj.pointer.y - lineStartY) < snapDistance) {
// obj.pointer.y = lineStartY
// }
// //
// if (Math.abs(obj.pointer.x - lineStartX) < snapDistance) {
// obj.pointer.x = lineStartX
// }
// }
// })
// })
const recoilObj = { const recoilObj = {
guideMode: 'guideLine', guideMode: 'guideLine',
horizontalLineArray, horizontalLineArray,
verticalLineArray, verticalLineArray,
moduleLength: moduleLength.current.value, moduleVertLength: moduleVertLength,
moduleHoriLength: moduleHoriLength,
} }
gridSettingArray.push(recoilObj) gridSettingArray.push(recoilObj)
} }
@ -175,12 +103,11 @@ export default function SettingsModal(props) {
lockRotation: true, lockRotation: true,
lockScalingX: true, lockScalingX: true,
lockScalingY: true, lockScalingY: true,
name: 'guideDot',
}) })
const patternSourceCanvas = new fabric.StaticCanvas(null, { const patternSourceCanvas = new fabric.StaticCanvas(null, {
width: moduleLength.current.value, width: moduleHoriLength,
height: moduleLength.current.value, height: moduleVertLength,
}) })
patternSourceCanvas.add(circle) patternSourceCanvas.add(circle)
@ -207,6 +134,7 @@ export default function SettingsModal(props) {
{ {
fill: pattern, fill: pattern,
selectable: false, selectable: false,
name: 'guideDot',
}, },
) )
canvasProps.add(backgroundPolygon) canvasProps.add(backgroundPolygon)
@ -214,7 +142,8 @@ export default function SettingsModal(props) {
const recoilObj = { const recoilObj = {
guideMode: 'guideDot', guideMode: 'guideDot',
moduleLength: moduleLength.current.value, moduleVertLength: moduleVertLength,
moduleHoriLength: moduleHoriLength,
} }
gridSettingArray.push(recoilObj) gridSettingArray.push(recoilObj)
@ -223,27 +152,39 @@ export default function SettingsModal(props) {
setGuideLine(gridSettingArray) setGuideLine(gridSettingArray)
} }
const removeGuideLines = () => {
if (!(Object.keys(guideLine).length === 0 && guideLine.constructor === Object)) {
const guideLines = canvasProps._objects.filter((obj) => obj.name === 'guideLine' || obj.name === 'guideDot')
guideLines?.forEach((item) => canvasProps.remove(item))
canvasProps.renderAll()
setGuideLine([])
} else {
alert('그리드가 없습니다.')
return
}
}
return ( return (
<> <>
<div> <div>
<div className="flex w-full flex-wrap md:flex-nowrap gap-4"> <div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<label style={{ display: 'block', marginBottom: '5px' }}> <label style={{ display: 'block', marginBottom: '5px' }}>
<CheckboxGroup label="그리드 설정" value={gridCheckedValue} onValueChange={setGridCheckValue}> <CheckboxGroup label="그리드 설정" value={gridCheckedValue} defaultChecked={gridCheckedValue} onValueChange={setGridCheckValue}>
<Checkbox value="dot"> 그리드</Checkbox> <Checkbox value="dot"> 그리드</Checkbox>
<Checkbox value="line">점선 그리드</Checkbox> <Checkbox value="line">점선 그리드</Checkbox>
</CheckboxGroup> </CheckboxGroup>
</label> </label>
</div> </div>
<div className="flex w-full flex-wrap md:flex-nowrap gap-4"> <div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<Input type="number" label="모듈" ref={moduleLength} defaultValue="90" /> <Input type="number" label="모듈" ref={moduleLength} />
mm mm
</div> </div>
<div className="flex w-full flex-wrap md:flex-nowrap gap-4"> <div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<RadioGroup label="비율 설정" onValueChange={setRatioValue}> <RadioGroup label="비율 설정" value={ratioValue} defaultValue={ratioValue} onValueChange={setRatioValue}>
<Radio value="원치수">원치수</Radio> <Radio value="1">원치수</Radio>
<Radio value="1/2">1/2</Radio> <Radio value="2">1/2</Radio>
<Radio value="1/4">1/4</Radio> <Radio value="4">1/4</Radio>
<Radio value="1/10">1/10</Radio> <Radio value="10">1/10</Radio>
<Radio value="custom">임의간격</Radio> <Radio value="custom">임의간격</Radio>
</RadioGroup> </RadioGroup>
</div> </div>
@ -253,11 +194,11 @@ export default function SettingsModal(props) {
</Checkbox> </Checkbox>
</div> </div>
<div className="flex w-full flex-wrap md:flex-nowrap gap-4"> <div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<Input type="number" label="가로간격" min={0} isDisabled={isCustomGridSetting} /> <Input type="number" label="가로간격" ref={customModuleHoriLength} min={0} isDisabled={isCustomGridSetting} />
mm mm
</div> </div>
<div className="flex w-full flex-wrap md:flex-nowrap gap-4"> <div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<Input type="number" label="세로간격" min={0} isDisabled={isCustomGridSetting} /> <Input type="number" label="세로간격" ref={customModuleVertLength} min={0} isDisabled={isCustomGridSetting} />
mm mm
</div> </div>
<div className="flex gap-4 items-center"> <div className="flex gap-4 items-center">
@ -268,6 +209,9 @@ export default function SettingsModal(props) {
<Button size="sm" onClick={() => setOpen(!open)}> <Button size="sm" onClick={() => setOpen(!open)}>
취소 취소
</Button> </Button>
<Button size="sm" onClick={() => removeGuideLines()}>
그리드 삭제
</Button>
</div> </div>
</> </>
) )

View File

@ -0,0 +1,71 @@
'use client'
import { useEffect, useState } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
export default function QLineContextMenu(props) {
const { contextRef, canvasProps } = props
// const children = useRecoilValue(modalContent)
const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0 })
useEffect(() => {
if (!contextRef.current) return
const handleContextMenu = (e) => {
e.preventDefault() // contextmenu
setContextMenu({ visible: true, x: e.pageX, y: e.pageY })
canvasProps.upperCanvasEl.removeEventListener('contextmenu', handleContextMenu) //
}
const handleClick = (e) => {
e.preventDefault()
setContextMenu({ ...contextMenu, visible: false })
}
const handleOutsideClick = (e) => {
e.preventDefault()
if (contextMenu.visible && !ref.current.contains(e.target)) {
setContextMenu({ ...contextMenu, visible: false })
}
}
// Prevent the default context menu from appearing on the canvas
canvasProps.upperCanvasEl.addEventListener('contextmenu', handleContextMenu)
document.addEventListener('click', handleClick)
document.addEventListener('click', handleOutsideClick)
return () => {
// canvasProps.upperCanvasEl.removeEventListener('contextmenu', handleContextMenu)
document.removeEventListener('click', handleClick)
document.removeEventListener('click', handleOutsideClick)
}
}, [contextRef, contextMenu])
const handleMenuClick = (option) => {
alert(`option ${option} clicked`)
setContextMenu({ ...contextMenu, visible: false })
}
return (
<>
{contextMenu.visible && (
<div style={{ position: 'absolute', top: contextMenu.y, left: contextMenu.x, zIndex: 2000 }}>
<ul style={{ listStyle: 'none', margin: 0, padding: '5px' }}>
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(1)}>
line
</li>
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(1)}>
Option 1
</li>
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(2)}>
Option 2
</li>
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(3)}>
Option 3
</li>
</ul>
</div>
)}
</>
)
}

View File

@ -0,0 +1,71 @@
'use client'
import { useEffect, useState } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
export default function QPolygonContextMenu(props) {
const { contextRef, canvasProps } = props
// const children = useRecoilValue(modalContent)
const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0 })
useEffect(() => {
if (!contextRef.current) return
const handleContextMenu = (e) => {
e.preventDefault() // contextmenu
setContextMenu({ visible: true, x: e.pageX, y: e.pageY })
canvasProps.upperCanvasEl.removeEventListener('contextmenu', handleContextMenu) //
}
const handleClick = (e) => {
e.preventDefault()
setContextMenu({ ...contextMenu, visible: false })
}
const handleOutsideClick = (e) => {
e.preventDefault()
if (contextMenu.visible && !ref.current.contains(e.target)) {
setContextMenu({ ...contextMenu, visible: false })
}
}
// Prevent the default context menu from appearing on the canvas
canvasProps.upperCanvasEl.addEventListener('contextmenu', handleContextMenu)
document.addEventListener('click', handleClick)
document.addEventListener('click', handleOutsideClick)
return () => {
// canvasProps.upperCanvasEl.removeEventListener('contextmenu', handleContextMenu)
document.removeEventListener('click', handleClick)
document.removeEventListener('click', handleOutsideClick)
}
}, [contextRef, contextMenu])
const handleMenuClick = (option) => {
alert(`option ${option} clicked`)
setContextMenu({ ...contextMenu, visible: false })
}
return (
<>
{contextMenu.visible && (
<div style={{ position: 'absolute', top: contextMenu.y, left: contextMenu.x, zIndex: 2000 }}>
<ul style={{ listStyle: 'none', margin: 0, padding: '5px' }}>
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(1)}>
polygon
</li>
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(1)}>
Option 1
</li>
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(2)}>
Option 2
</li>
<li style={{ padding: '8px 12px', cursor: 'pointer' }} onClick={() => handleMenuClick(3)}>
Option 3
</li>
</ul>
</div>
)}
</>
)
}

View File

@ -11,6 +11,7 @@ export const QLine = fabric.util.createClass(fabric.Line, {
direction: null, direction: null,
idx: 0, idx: 0,
area: 0, area: 0,
children: [],
initialize: function (points, options, canvas) { initialize: function (points, options, canvas) {
this.callSuper('initialize', points, { ...options, selectable: options.selectable ?? false }) this.callSuper('initialize', points, { ...options, selectable: options.selectable ?? false })
if (options.id) { if (options.id) {
@ -122,6 +123,7 @@ export const QLine = fabric.util.createClass(fabric.Line, {
lockRotation: true, lockRotation: true,
lockScalingX: true, lockScalingX: true,
lockScalingY: true, lockScalingY: true,
parent: this,
name: 'lengthText', name: 'lengthText',
}) })

View File

@ -17,6 +17,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
cells: [], cells: [],
parentId: null, parentId: null,
innerLines: [], innerLines: [],
children: [],
initialize: function (points, options, canvas) { initialize: function (points, options, canvas) {
// 소수점 전부 제거 // 소수점 전부 제거
points.forEach((point) => { points.forEach((point) => {
@ -195,6 +196,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
lockScalingY: true, lockScalingY: true,
idx: i, idx: i,
name: 'lengthText', name: 'lengthText',
parent: this,
}) })
this.texts.push(text) this.texts.push(text)
@ -271,6 +273,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
name: 'cell', name: 'cell',
idx: idx, idx: idx,
parentId: this.id, parentId: this.id,
parent: this,
}) })
idx++ idx++

View File

@ -179,25 +179,6 @@ export function useCanvas(id) {
}) })
} }
/**
* 선택한 도형을 삭제한다.
*/
const handleDelete = () => {
const targets = canvas?.getActiveObjects()
if (targets?.length === 0) {
alert('삭제할 대상을 선택해주세요.')
return
}
if (!confirm('정말로 삭제하시겠습니까?')) {
return
}
targets?.forEach((target) => {
canvas?.remove(target)
})
}
/** /**
* 페이지 캔버스 저장 * 페이지 캔버스 저장
* todo : 현재는 localStorage에 그냥 저장하지만 나중에 변경해야함 * todo : 현재는 localStorage에 그냥 저장하지만 나중에 변경해야함
@ -469,7 +450,6 @@ export function useCanvas(id) {
handleUndo, handleUndo,
handleRedo, handleRedo,
handleCopy, handleCopy,
handleDelete,
handleSave, handleSave,
handlePaste, handlePaste,
handleRotate, handleRotate,

View File

@ -1,162 +1,146 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { fabric } from 'fabric' import { fabric } from 'fabric'
import { useRecoilValue } from 'recoil' import { useRecoilState, useRecoilValue } from 'recoil'
import { canvasSizeState, modeState } from '@/store/canvasAtom' import { canvasSizeState, currentObjectState, modeState } from '@/store/canvasAtom'
// 캔버스에 필요한 이벤트 // 캔버스에 필요한 이벤트
export function useCanvasEvent() { export function useCanvasEvent() {
const [canvas, setCanvasForEvent] = useState(null) const [canvas, setCanvasForEvent] = useState(null)
const [currentObject, setCurrentObject] = useRecoilState(currentObjectState)
const canvasSize = useRecoilValue(canvasSizeState) const canvasSize = useRecoilValue(canvasSizeState)
// 기본적인 이벤트 필요시 추가 // 기본적인 이벤트 필요시 추가
const attachDefaultEventOnCanvas = () => { const attachDefaultEventOnCanvas = () => {
removeEventOnCanvas() removeEventOnCanvas()
canvas?.on('object:added', onChange) canvas?.on('object:added', objectEvent.onChange)
canvas?.on('object:added', addEventOnObject) canvas?.on('object:added', objectEvent.addEvent)
canvas?.on('object:modified', onChange) canvas?.on('object:modified', objectEvent.onChange)
canvas?.on('object:removed', onChange) canvas?.on('object:removed', objectEvent.onChange)
canvas?.on('selection:cleared', selectionEvent.cleared)
canvas?.on('selection:created', selectionEvent.created)
canvas?.on('selection:updated', selectionEvent.updated)
canvas?.on('object:added', () => { canvas?.on('object:added', () => {
document.addEventListener('keydown', handleKeyDown) document.addEventListener('keydown', handleKeyDown)
}) })
canvas?.on('object:removed', objectEvent.removed)
} }
const onChange = (e) => { const objectEvent = {
const target = e.target onChange: (e) => {
if (target) { const target = e.target
// settleDown(target) if (target) {
} // settleDown(target)
}
},
addEvent: (e) => {
const target = e.target
if (target.type === 'QPolygon' || target.type === 'QLine') {
const textObjs = canvas?.getObjects().filter((obj) => obj.name === 'lengthText')
textObjs.forEach((obj) => {
obj.bringToFront()
})
}
if (target.name === 'cell') {
target.on('mousedown', () => {
if (target.get('selected')) {
target.set({ selected: false })
target.set({ fill: '#BFFD9F' })
} else {
target.set({ selected: true })
target.set({ fill: 'red' })
}
canvas?.renderAll()
})
}
if (target.name === 'trestle') {
target.on('mousedown', () => {
if (target.defense === 'north') {
alert('북쪽은 선택 불가합니다.')
return
}
if (target.get('selected')) {
target.set({ strokeWidth: 1 })
target.set({ strokeDashArray: [5, 5] })
target.set({ selected: false })
} else {
target.set({ strokeWidth: 5 })
target.set({ strokeDashArray: [0, 0] })
target.set({ selected: true })
}
canvas?.renderAll()
})
}
if (target.name === 'lengthText') {
const x = target.left
const y = target.top
target.on('selected', (e) => {
Object.keys(target.controls).forEach((controlKey) => {
target.setControlVisible(controlKey, false)
})
})
target.on('moving', (e) => {
if (target.parentDirection === 'left' || target.parentDirection === 'right') {
const minX = target.minX
const maxX = target.maxX
if (target.left <= minX) {
target.set({ left: minX, top: y })
} else if (target.left >= maxX) {
target.set({ left: maxX, top: y })
} else {
target.set({ top: y })
}
} else if (target.parentDirection === 'top' || target.parentDirection === 'bottom') {
const minY = target.minY
const maxY = target.maxY
if (target.top <= minY) {
target.set({ left: x, top: minY })
} else if (target.top >= maxY) {
target.set({ left: x, top: maxY })
} else {
target.set({ left: x })
}
}
canvas?.renderAll()
})
}
},
removed: (e) => {
const whiteList = ['mouseLine', 'guideLine']
if (whiteList.includes(e.target.name)) {
return
}
console.log('removed', e)
},
}
const selectionEvent = {
created: (e) => {
const target = e.selected[0]
setCurrentObject(target)
},
cleared: (e) => {
setCurrentObject(null)
},
updated: (e) => {
const target = e.selected[0]
setCurrentObject(target)
},
} }
const removeEventOnCanvas = () => { const removeEventOnCanvas = () => {
canvas?.off('object:added') canvas?.off('object:added')
canvas?.off('object:modified') canvas?.off('object:modified')
canvas?.off('object:removed') canvas?.off('object:removed')
canvas?.off('object:added') canvas?.off('selection:cleared')
canvas?.off('mouse:move') canvas?.off('selection:created')
canvas?.off('mouse:down') canvas?.off('selection:updated')
}
const addEventOnObject = (e) => {
const target = e.target
if (target.type === 'QPolygon' || target.type === 'QLine') {
const textObjs = canvas?.getObjects().filter((obj) => obj.name === 'lengthText')
textObjs.forEach((obj) => {
obj.bringToFront()
})
}
if (target.name === 'cell') {
target.on('mousedown', () => {
if (target.get('selected')) {
target.set({ selected: false })
target.set({ fill: '#BFFD9F' })
} else {
target.set({ selected: true })
target.set({ fill: 'red' })
}
canvas?.renderAll()
})
}
if (target.name === 'trestle') {
target.on('mousedown', () => {
if (target.defense === 'north') {
alert('북쪽은 선택 불가합니다.')
return
}
if (target.get('selected')) {
target.set({ strokeWidth: 1 })
target.set({ strokeDashArray: [5, 5] })
target.set({ selected: false })
} else {
target.set({ strokeWidth: 5 })
target.set({ strokeDashArray: [0, 0] })
target.set({ selected: true })
}
canvas?.renderAll()
})
}
if (target.name === 'lengthText') {
const x = target.left
const y = target.top
target.on('selected', (e) => {
Object.keys(target.controls).forEach((controlKey) => {
target.setControlVisible(controlKey, false)
})
})
target.on('moving', (e) => {
if (target.parentDirection === 'left' || target.parentDirection === 'right') {
const minX = target.minX
const maxX = target.maxX
if (target.left <= minX) {
target.set({ left: minX, top: y })
} else if (target.left >= maxX) {
target.set({ left: maxX, top: y })
} else {
target.set({ top: y })
}
} else if (target.parentDirection === 'top' || target.parentDirection === 'bottom') {
const minY = target.minY
const maxY = target.maxY
if (target.top <= minY) {
target.set({ left: x, top: minY })
} else if (target.top >= maxY) {
target.set({ left: x, top: maxY })
} else {
target.set({ left: x })
}
}
canvas?.renderAll()
})
}
}
// 마우스 가로, 세로선 그리기
const drawMouseLines = (e) => {
// 현재 마우스 포인터의 위치를 가져옵니다.
const pointer = canvas?.getPointer(e.e)
// 기존에 그려진 가이드라인을 제거합니다.
removeMouseLines()
if (canvas?.getActiveObject()) {
return
}
// 가로선을 그립니다.
const horizontalLine = new fabric.Line([0, pointer.y, canvasSize.horizontal, pointer.y], {
stroke: 'black',
strokeWidth: 1,
selectable: false,
name: 'mouseLine',
})
// 세로선을 그립니다.
const verticalLine = new fabric.Line([pointer.x, 0, pointer.x, canvasSize.vertical], {
stroke: 'black',
strokeWidth: 1,
selectable: false,
name: 'mouseLine',
})
// 선들을 캔버스에 추가합니다.
canvas?.add(horizontalLine, verticalLine)
// 캔버스를 다시 그립니다.
canvas?.renderAll()
}
//가로, 세로 선 삭제
const removeMouseLines = () => {
if (canvas?._objects.length > 0) {
const mouseLines = canvas?._objects.filter((obj) => obj.name === 'mouseLine')
mouseLines.forEach((item) => canvas?.remove(item))
}
canvas?.renderAll()
} }
/** /**
@ -197,6 +181,16 @@ export function useCanvasEvent() {
case 'Esc': // IE/Edge에서 사용되는 값 case 'Esc': // IE/Edge에서 사용되는 값
case 'Escape': case 'Escape':
break break
case 'z':
if (!e.ctrlKey) {
return
}
console.log('뒤로가기')
break
default: default:
return // 키 이벤트를 처리하지 않는다면 종료합니다. return // 키 이벤트를 처리하지 않는다면 종료합니다.
} }
@ -216,6 +210,7 @@ export function useCanvasEvent() {
} }
targetObj.set({ top: top }) targetObj.set({ top: top })
targetObj.fire('modified')
canvas?.renderAll() canvas?.renderAll()
} }
@ -232,6 +227,7 @@ export function useCanvasEvent() {
} }
targetObj.set({ top: top }) targetObj.set({ top: top })
targetObj.fire('modified')
canvas?.renderAll() canvas?.renderAll()
} }
@ -248,6 +244,7 @@ export function useCanvasEvent() {
} }
targetObj.set({ left: left }) targetObj.set({ left: left })
targetObj.fire('modified')
canvas?.renderAll() canvas?.renderAll()
} }
@ -264,9 +261,29 @@ export function useCanvasEvent() {
} }
targetObj.set({ left: left }) targetObj.set({ left: left })
targetObj.fire('modified')
canvas?.renderAll() canvas?.renderAll()
} }
/**
* 선택한 도형을 삭제한다.
*/
const handleDelete = () => {
const targets = canvas?.getActiveObjects()
if (targets?.length === 0) {
alert('삭제할 대상을 선택해주세요.')
return
}
if (!confirm('정말로 삭제하시겠습니까?')) {
return
}
targets?.forEach((target) => {
canvas?.remove(target)
})
}
return { return {
setCanvasForEvent, setCanvasForEvent,
attachDefaultEventOnCanvas, attachDefaultEventOnCanvas,

View File

@ -125,7 +125,7 @@ export function useMode() {
const drawMouseLines = (e) => { const drawMouseLines = (e) => {
let isGuideLineMode = false, let isGuideLineMode = false,
isGuideDotMode = false isGuideDotMode = false
let guideDotLength, guideLineLength, horizontalLineArray, verticalLineArray let guideDotLength, guideLineLengthHori, guideLineLengthVert, horizontalLineArray, verticalLineArray
if (isObjectNotEmpty(guideLineInfo)) { if (isObjectNotEmpty(guideLineInfo)) {
const guideLineState = guideLineInfo.filter((item) => item.guideMode === 'guideLine') const guideLineState = guideLineInfo.filter((item) => item.guideMode === 'guideLine')
@ -136,13 +136,15 @@ export function useMode() {
isGuideDotMode = guideDotState.length > 0 isGuideDotMode = guideDotState.length > 0
if (isGuideDotMode) { if (isGuideDotMode) {
guideDotLength = Number(guideDotState[0].moduleLength) guideLineLengthHori = Number(guideDotState[0].moduleHoriLength)
guideLineLengthVert = Number(guideDotState[0].moduleVertLength)
} }
if (isGuideLineMode) { if (isGuideLineMode) {
horizontalLineArray = [...guideLineState[0].horizontalLineArray] horizontalLineArray = [...guideLineState[0].horizontalLineArray]
verticalLineArray = [...guideLineState[0].verticalLineArray] verticalLineArray = [...guideLineState[0].verticalLineArray]
guideLineLength = Number(guideLineState[0].moduleLength) guideLineLengthHori = Number(guideLineState[0].moduleHoriLength)
guideLineLengthVert = Number(guideLineState[0].moduleVertLength)
} }
} }
@ -161,17 +163,17 @@ export function useMode() {
const xDiff = Math.abs(pointer.x - closetVerticalLine.x1) const xDiff = Math.abs(pointer.x - closetVerticalLine.x1)
const yDiff = Math.abs(pointer.y - closestHorizontalLine.y1) const yDiff = Math.abs(pointer.y - closestHorizontalLine.y1)
const x = pointer.x - guideLineLength * Math.floor(pointer.x / guideLineLength) const x = pointer.x - guideLineLengthHori * Math.floor(pointer.x / guideLineLengthHori)
const y = pointer.y - guideLineLength * Math.floor(pointer.y / guideLineLength) const y = pointer.y - guideLineLengthVert * Math.floor(pointer.y / guideLineLengthVert)
const xRate = x / guideLineLength const xRate = x / guideLineLengthHori
const yRate = y / guideLineLength const yRate = y / guideLineLengthVert
const isAttachX = xRate >= 0.4 && xRate <= 0.7 const isAttachX = xRate >= 0.4 && xRate <= 0.7
const isAttachY = yRate >= 0.4 && yRate <= 0.7 const isAttachY = yRate >= 0.4 && yRate <= 0.7
if (isAttachX && isAttachY) { if (isAttachX && isAttachY) {
newX = Math.floor(pointer.x / guideLineLength) * guideLineLength + guideLineLength / 2 newX = Math.floor(pointer.x / guideLineLengthHori) * guideLineLengthHori + guideLineLengthHori / 2
newY = Math.floor(pointer.y / guideLineLength) * guideLineLength + guideLineLength / 2 newY = Math.floor(pointer.y / guideLineLengthVert) * guideLineLengthVert + guideLineLengthVert / 2
} else { } else {
if (Math.min(xDiff, yDiff) <= 20) { if (Math.min(xDiff, yDiff) <= 20) {
if (xDiff < yDiff) { if (xDiff < yDiff) {
@ -184,17 +186,17 @@ export function useMode() {
} }
} }
} else if (isGuideDotMode && mode === Mode.EDIT) { } else if (isGuideDotMode && mode === Mode.EDIT) {
const x = pointer.x - guideDotLength * Math.floor(pointer.x / guideDotLength) const x = pointer.x - guideLineLengthHori * Math.floor(pointer.x / guideLineLengthHori)
const y = pointer.y - guideDotLength * Math.floor(pointer.y / guideDotLength) const y = pointer.y - guideLineLengthVert * Math.floor(pointer.y / guideLineLengthVert)
const xRate = x / guideDotLength const xRate = x / guideLineLengthHori
const yRate = y / guideDotLength const yRate = y / guideLineLengthVert
const isAttachX = xRate >= 0.4 && xRate <= 0.7 const isAttachX = xRate >= 0.4 && xRate <= 0.7
const isAttachY = yRate >= 0.4 && yRate <= 0.7 const isAttachY = yRate >= 0.4 && yRate <= 0.7
if (isAttachX && isAttachY) { if (isAttachX && isAttachY) {
newX = Math.floor(pointer.x / guideDotLength) * guideDotLength + guideDotLength / 2 newX = Math.floor(pointer.x / guideLineLengthHori) * guideLineLengthHori + guideLineLengthHori / 2
newY = Math.floor(pointer.y / guideDotLength) * guideDotLength + guideDotLength / 2 newY = Math.floor(pointer.y / guideLineLengthVert) * guideLineLengthVert + guideLineLengthVert / 2
} }
} else if (isGuideLineMode && mode === Mode.EDIT) { } else if (isGuideLineMode && mode === Mode.EDIT) {
const closestHorizontalLine = getClosestHorizontalLine(pointer, horizontalLineArray) const closestHorizontalLine = getClosestHorizontalLine(pointer, horizontalLineArray)
@ -202,17 +204,17 @@ export function useMode() {
const xDiff = Math.abs(pointer.x - closetVerticalLine.x1) const xDiff = Math.abs(pointer.x - closetVerticalLine.x1)
const yDiff = Math.abs(pointer.y - closestHorizontalLine.y1) const yDiff = Math.abs(pointer.y - closestHorizontalLine.y1)
const x = pointer.x - guideLineLength * Math.floor(pointer.x / guideLineLength) const x = pointer.x - guideLineLengthHori * Math.floor(pointer.x / guideLineLengthHori)
const y = pointer.y - guideLineLength * Math.floor(pointer.y / guideLineLength) const y = pointer.y - guideLineLengthVert * Math.floor(pointer.y / guideLineLengthVert)
const xRate = x / guideLineLength const xRate = x / guideLineLengthHori
const yRate = y / guideLineLength const yRate = y / guideLineLengthVert
const isAttachX = xRate >= 0.4 && xRate <= 0.7 const isAttachX = xRate >= 0.4 && xRate <= 0.7
const isAttachY = yRate >= 0.4 && yRate <= 0.7 const isAttachY = yRate >= 0.4 && yRate <= 0.7
if (isAttachX && isAttachY) { if (isAttachX && isAttachY) {
newX = Math.floor(pointer.x / guideLineLength) * guideLineLength + guideLineLength / 2 newX = Math.floor(pointer.x / guideLineLengthHori) * guideLineLengthHori + guideLineLengthHori / 2
newY = Math.floor(pointer.y / guideLineLength) * guideLineLength + guideLineLength / 2 newY = Math.floor(pointer.y / guideLineLengthVert) * guideLineLengthVert + guideLineLengthVert / 2
} else { } else {
if (Math.min(xDiff, yDiff) <= 20) { if (Math.min(xDiff, yDiff) <= 20) {
if (xDiff < yDiff) { if (xDiff < yDiff) {
@ -661,9 +663,12 @@ export function useMode() {
editMode: (options) => { editMode: (options) => {
// const pointer = canvas?.getPointer(options.e) // const pointer = canvas?.getPointer(options.e)
let pointer = canvas?.getPointer(options.e)
const mouseLines = canvas?._objects.filter((obj) => obj.name === 'mouseLine') const mouseLines = canvas?._objects.filter((obj) => obj.name === 'mouseLine')
const pointer = calculateIntersection(mouseLines[0], mouseLines[1]) /*if (calculateIntersection(mouseLines[0], mouseLines[1])) {
pointer = calculateIntersection(mouseLines[0], mouseLines[1])
}*/
const circle = new fabric.Circle({ const circle = new fabric.Circle({
radius: 5, radius: 5,
@ -949,6 +954,8 @@ export function useMode() {
points.current = [] points.current = []
historyPoints.current = [] historyPoints.current = []
historyLines.current = [] historyLines.current = []
setRoof(null)
setWall(null)
setSelectedCellRoofArray([]) //셀 그린거 삭제 setSelectedCellRoofArray([]) //셀 그린거 삭제
} }
@ -1487,6 +1494,11 @@ export function useMode() {
} }
const applyTemplateA = () => { const applyTemplateA = () => {
if (historyPoints.current.length === 0) {
changeMode(canvas, Mode.EDIT)
return
}
changeMode(canvas, Mode.EDIT) changeMode(canvas, Mode.EDIT)
const polygon = drawWallPolygon(false) const polygon = drawWallPolygon(false)
// handleClear() // handleClear()
@ -2933,7 +2945,11 @@ export function useMode() {
* 템플릿 B 적용 * 템플릿 B 적용
*/ */
const applyTemplateB = () => { const applyTemplateB = () => {
changeMode(canvas, Mode.EDIT) if (historyPoints.current.length === 0) {
changeMode(canvas, Mode.EDIT)
return
}
const polygon = drawWallPolygon(false) const polygon = drawWallPolygon(false)
const params = { const params = {
eaves: 50, eaves: 50,
@ -4211,38 +4227,47 @@ export function useMode() {
} }
} }
//셀이 생성될 지붕의 흐름방향을 정함 if (templateCenterLine.length > 0) {
templateCenterLine.some((centerLine) => { //셀이 생성될 지붕의 흐름방향을 정함
templateCenterLine.some((centerLine) => {
if (templateType === 2) {
trestlePolygon.set({
referenceDirection: referenceDirection,
startIndex: parallelPoint,
}) //기준면 방향
trestlePolygon.points.forEach((trestleLine, index) => {
if (trestleLine.x === centerLine.x1 - 12) {
trestlePolygon.set({ wallDirection: 'left' })
return true
} else if (trestleLine.x === centerLine.x1 + 12) {
trestlePolygon.set({ wallDirection: 'right' })
return true
}
})
} else if (templateType === 3) {
trestlePolygon.set({
referenceDirection: referenceDirection,
startIndex: parallelPoint,
}) //기준면 방향
trestlePolygon.points.forEach((trestleLine, index) => {
if (trestleLine.y === centerLine.y1 - 12) {
trestlePolygon.set({ wallDirection: 'top' })
return true
} else if (trestleLine.y === centerLine.y1 + 12) {
trestlePolygon.set({ wallDirection: 'bottom' })
return true
}
})
}
})
} else {
if (templateType === 2) { if (templateType === 2) {
trestlePolygon.set({ index === 0 ? trestlePolygon.set({ wallDirection: 'left' }) : trestlePolygon.set({ wallDirection: 'right' })
referenceDirection: referenceDirection,
startIndex: parallelPoint,
}) //기준면 방향
trestlePolygon.points.forEach((trestleLine, index) => {
if (trestleLine.x === centerLine.x1 - 12) {
trestlePolygon.set({ wallDirection: 'left' })
return true
} else if (trestleLine.x === centerLine.x1 + 12) {
trestlePolygon.set({ wallDirection: 'right' })
return true
}
})
} else if (templateType === 3) { } else if (templateType === 3) {
trestlePolygon.set({ index === 0 ? trestlePolygon.set({ wallDirection: 'top' }) : trestlePolygon.set({ wallDirection: 'bottom' })
referenceDirection: referenceDirection,
startIndex: parallelPoint,
}) //기준면 방향
trestlePolygon.points.forEach((trestleLine, index) => {
if (trestleLine.y === centerLine.y1 - 12) {
trestlePolygon.set({ wallDirection: 'top' })
return true
} else if (trestleLine.y === centerLine.y1 + 12) {
trestlePolygon.set({ wallDirection: 'bottom' })
return true
}
})
} }
}) trestlePolygon.set({ referenceDirection: 'none' })
}
/** /**
* 가대 선택 이벤트 * 가대 선택 이벤트

View File

@ -94,3 +94,9 @@ export const guideLineState = atom({
default: {}, default: {},
dangerouslyAllowMutability: true, dangerouslyAllowMutability: true,
}) })
export const currentObjectState = atom({
key: 'currentObject',
default: null,
dangerouslyAllowMutability: true,
})