594 lines
15 KiB
JavaScript
594 lines
15 KiB
JavaScript
import { useEffect, useState } from 'react'
|
|
import { fabric } from 'fabric'
|
|
|
|
import { actionHandler, anchorWrapper, polygonPositionHandler } from '@/util/canvas-util'
|
|
|
|
import { useRecoilState } from 'recoil'
|
|
import { canvasSizeState, canvasState, fontSizeState } from '@/store/canvasAtom'
|
|
import { QLine } from '@/components/fabric/QLine'
|
|
import { QPolygon } from '@/components/fabric/QPolygon'
|
|
import { defineQLine } from '@/util/qline-utils'
|
|
import { defineQPloygon } from '@/util/qpolygon-utils'
|
|
import { writeImage } from '@/lib/canvas'
|
|
import { useCanvasEvent } from '@/hooks/useCanvasEvent'
|
|
import { useAxios } from '@/hooks/useAxios'
|
|
import { useFont } from '@/hooks/common/useFont'
|
|
import { OBJECT_PROTOTYPE, POLYGON_TYPE, RELOAD_TYPE_PROTOTYPE, SAVE_KEY } from '@/common/common'
|
|
import { usePlan } from './usePlan'
|
|
|
|
export function useCanvas(id) {
|
|
const [canvas, setCanvas] = useRecoilState(canvasState)
|
|
const [isLocked, setIsLocked] = useState(false)
|
|
const [history, setHistory] = useState([])
|
|
const [backImg, setBackImg] = useState()
|
|
const [canvasSize] = useRecoilState(canvasSizeState)
|
|
const [fontSize] = useRecoilState(fontSizeState)
|
|
const { setCanvasForEvent, attachDefaultEventOnCanvas } = useCanvasEvent()
|
|
const {} = useFont()
|
|
|
|
/**
|
|
* 처음 셋팅
|
|
*/
|
|
useEffect(() => {
|
|
const c = new fabric.Canvas(id, {
|
|
height: canvasSize.vertical,
|
|
width: canvasSize.horizontal,
|
|
backgroundColor: 'white',
|
|
selection: false,
|
|
})
|
|
|
|
setCanvas(c)
|
|
setCanvasForEvent(c)
|
|
attachDefaultEventOnCanvas()
|
|
|
|
return () => {
|
|
// c.dispose()
|
|
c.clear()
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
// canvas 사이즈가 변경되면 다시
|
|
}, [canvasSize])
|
|
|
|
useEffect(() => {
|
|
canvas
|
|
?.getObjects()
|
|
?.filter((obj) => obj.type === 'textbox' || obj.type === 'text' || obj.type === 'i-text')
|
|
.forEach((obj) => {
|
|
obj.set({ fontSize: fontSize })
|
|
})
|
|
|
|
canvas
|
|
?.getObjects()
|
|
?.filter((obj) => obj.type === 'QLine' || obj.type === 'QPolygon' || obj.type === 'QRect')
|
|
.forEach((obj) => {
|
|
obj.setFontSize(fontSize)
|
|
})
|
|
canvas?.getObjects().length > 0 && canvas?.renderAll()
|
|
}, [fontSize])
|
|
|
|
/**
|
|
* 캔버스 초기화
|
|
*/
|
|
useEffect(() => {
|
|
if (canvas) {
|
|
initialize()
|
|
attachDefaultEventOnCanvas()
|
|
}
|
|
}, [canvas])
|
|
|
|
/**
|
|
* 마우스 포인터의 가이드라인을 제거합니다.
|
|
*/
|
|
const removeMouseLines = () => {
|
|
if (canvas?._objects.length > 0) {
|
|
const mouseLines = canvas?._objects.filter((obj) => obj.name === 'mouseLine')
|
|
mouseLines.forEach((item) => canvas?.remove(item))
|
|
}
|
|
canvas?.renderAll()
|
|
}
|
|
|
|
const initialize = () => {
|
|
canvas.getObjects().length > 0 && canvas?.clear()
|
|
// settings for all canvas in the app
|
|
fabric.Object.prototype.transparentCorners = false
|
|
fabric.Object.prototype.selectable = true
|
|
fabric.Object.prototype.lockMovementX = true
|
|
fabric.Object.prototype.lockMovementY = true
|
|
fabric.Object.prototype.lockRotation = true
|
|
fabric.Object.prototype.lockScalingX = true
|
|
fabric.Object.prototype.lockScalingY = true
|
|
|
|
fabric.Object.prototype.cornerColor = '#2BEBC8'
|
|
fabric.Object.prototype.cornerStyle = 'rect'
|
|
fabric.Object.prototype.cornerStrokeColor = '#2BEBC8'
|
|
fabric.Object.prototype.cornerSize = 6
|
|
|
|
// 해당 오브젝트 타입의 경우 저장한 값 그대로 불러와야함
|
|
OBJECT_PROTOTYPE.forEach((type) => {
|
|
type.toObject = function (propertiesToInclude) {
|
|
let source = {}
|
|
|
|
for (let key in this) {
|
|
if (typeof this[key] !== 'function' && SAVE_KEY.includes(key)) {
|
|
source.key = this[key]
|
|
}
|
|
}
|
|
|
|
//QLine에 커스텀 어트리뷰트 넣기
|
|
if (this.type === 'QLine') {
|
|
if (this.attributes) {
|
|
this.attributes.type = this.attributes.type || 'default'
|
|
source.attributes = {
|
|
...this.attributes,
|
|
type: this.attributes.type,
|
|
}
|
|
}
|
|
}
|
|
|
|
return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), source)
|
|
}
|
|
})
|
|
|
|
fabric.QLine = QLine
|
|
fabric.QPolygon = QPolygon
|
|
QPolygon.prototype.canvas = canvas
|
|
QLine.prototype.canvas = canvas
|
|
defineQLine()
|
|
defineQPloygon()
|
|
}
|
|
|
|
/**
|
|
* 캔버스에 도형을 추가한다. 도형은 사용하는 페이지에서 만들어서 파라미터로 넘겨주어야 한다.
|
|
*/
|
|
const addShape = (shape) => {
|
|
canvas?.add(shape)
|
|
canvas?.setActiveObject(shape)
|
|
canvas?.requestRenderAll()
|
|
}
|
|
|
|
const onChange = (e) => {
|
|
const target = e.target
|
|
if (target) {
|
|
// settleDown(target)
|
|
}
|
|
|
|
if (!isLocked) {
|
|
setHistory([])
|
|
}
|
|
setIsLocked(false)
|
|
}
|
|
|
|
/**
|
|
* 눈금 모양에 맞게 움직이도록 한다.
|
|
*/
|
|
const settleDown = (shape) => {
|
|
const left = Math.round(shape?.left / 10) * 10
|
|
const top = Math.round(shape?.top / 10) * 10
|
|
|
|
shape?.set({ left: left, top: top })
|
|
}
|
|
|
|
/**
|
|
* redo, undo가 필요한 곳에서 사용한다.
|
|
*/
|
|
const handleUndo = () => {
|
|
if (canvas) {
|
|
if (canvas?._objects.length > 0) {
|
|
const poppedObject = canvas?._objects.pop()
|
|
const group = []
|
|
group.push(poppedObject)
|
|
|
|
if (poppedObject.parent || poppedObject.parentId) {
|
|
canvas
|
|
?.getObjects()
|
|
.filter((obj) => obj.parent === poppedObject.parent || obj.parentId === poppedObject.parentId || obj === poppedObject.parent)
|
|
.forEach((obj) => {
|
|
group.push(obj)
|
|
canvas?.remove(obj)
|
|
})
|
|
}
|
|
|
|
setHistory((prev) => {
|
|
if (prev === undefined) {
|
|
return poppedObject ? [group] : []
|
|
}
|
|
return poppedObject ? [...prev, group] : prev
|
|
})
|
|
canvas?.renderAll()
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleRedo = () => {
|
|
if (canvas && history) {
|
|
if (history.length > 0) {
|
|
setIsLocked(true)
|
|
if (Array.isArray(history[history.length - 1])) {
|
|
history[history.length - 1].forEach((obj) => {
|
|
canvas?.add(obj)
|
|
})
|
|
} else {
|
|
canvas?.add(history[history.length - 1])
|
|
}
|
|
const newHistory = history.slice(0, -1)
|
|
setHistory(newHistory)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 선택한 도형을 복사한다.
|
|
*/
|
|
const handleCopy = () => {
|
|
const activeObjects = canvas?.getActiveObjects()
|
|
|
|
if (activeObjects?.length === 0) {
|
|
return
|
|
}
|
|
|
|
activeObjects?.forEach((obj) => {
|
|
obj.clone((cloned) => {
|
|
cloned.set({ left: obj.left + 10, top: obj.top + 10 })
|
|
addShape(cloned)
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 페이지 내 캔버스 저장
|
|
* todo : 현재는 localStorage에 그냥 저장하지만 나중에 변경해야함
|
|
*/
|
|
const handleSave = () => {
|
|
const objects = canvas?.getObjects()
|
|
|
|
if (objects?.length === 0) {
|
|
alert('저장할 대상이 없습니다.')
|
|
return
|
|
}
|
|
const jsonStr = JSON.stringify(canvas)
|
|
localStorage.setItem('canvas', jsonStr)
|
|
}
|
|
|
|
/**
|
|
* 페이지 내 캔버스에 저장한 내용 불러오기
|
|
* todo : 현재는 localStorage에 그냥 저장하지만 나중에 변경해야함
|
|
*/
|
|
|
|
const handlePaste = () => {
|
|
const jsonStr = localStorage.getItem('canvas')
|
|
if (!jsonStr) {
|
|
alert('붙여넣기 할 대상이 없습니다.')
|
|
return
|
|
}
|
|
|
|
canvas?.loadFromJSON(JSON.parse(jsonStr), () => {
|
|
localStorage.removeItem('canvas')
|
|
console.log('paste done')
|
|
})
|
|
}
|
|
|
|
const moveDown = () => {
|
|
const targetObj = canvas?.getActiveObject()
|
|
if (!targetObj) {
|
|
return
|
|
}
|
|
|
|
let top = targetObj.top + 10
|
|
|
|
if (top > canvasSize.vertical) {
|
|
top = canvasSize.vertical
|
|
}
|
|
|
|
targetObj.set({ top: top })
|
|
canvas?.renderAll()
|
|
}
|
|
|
|
const moveUp = () => {
|
|
const targetObj = canvas?.getActiveObject()
|
|
if (!targetObj) {
|
|
return
|
|
}
|
|
|
|
let top = targetObj.top - 10
|
|
|
|
if (top < 0) {
|
|
top = 0
|
|
}
|
|
|
|
targetObj.set({ top: top })
|
|
canvas?.renderAll()
|
|
}
|
|
|
|
const moveLeft = () => {
|
|
const targetObj = canvas?.getActiveObject()
|
|
if (!targetObj) {
|
|
return
|
|
}
|
|
|
|
let left = targetObj.left - 10
|
|
|
|
if (left < 0) {
|
|
left = 0
|
|
}
|
|
|
|
targetObj.set({ left: left })
|
|
canvas?.renderAll()
|
|
}
|
|
|
|
const moveRight = () => {
|
|
const targetObj = canvas?.getActiveObject()
|
|
if (!targetObj) {
|
|
return
|
|
}
|
|
|
|
let left = targetObj.left + 10
|
|
|
|
if (left > canvasSize.horizontal) {
|
|
left = canvasSize.horizontal
|
|
}
|
|
|
|
targetObj.set({ left: left })
|
|
canvas?.renderAll()
|
|
}
|
|
|
|
const handleRotate = (degree = 45) => {
|
|
const target = canvas?.getActiveObject()
|
|
|
|
if (!target) {
|
|
return
|
|
}
|
|
|
|
const currentAngle = target.angle
|
|
|
|
target.set({ angle: currentAngle + degree })
|
|
canvas?.renderAll()
|
|
}
|
|
|
|
/**
|
|
* Polygon 타입만 가능
|
|
* 생성한 polygon을 넘기면 해당 polygon은 꼭지점으로 컨트롤 가능한 polygon이 됨
|
|
*/
|
|
const attachCustomControlOnPolygon = (poly) => {
|
|
const lastControl = poly.points?.length - 1
|
|
poly.cornerStyle = 'rect'
|
|
poly.cornerColor = 'rgba(0,0,255,0.5)'
|
|
poly.objectCaching = false
|
|
poly.controls = poly.points.reduce(function (acc, point, index) {
|
|
acc['p' + index] = new fabric.Control({
|
|
positionHandler: polygonPositionHandler,
|
|
actionHandler: anchorWrapper(index > 0 ? index - 1 : lastControl, actionHandler),
|
|
actionName: 'modifyPolygon',
|
|
pointIndex: index,
|
|
})
|
|
return acc
|
|
}, {})
|
|
|
|
poly.hasBorders = !poly.edit
|
|
canvas?.requestRenderAll()
|
|
}
|
|
|
|
/**
|
|
* 이미지로 저장하는 함수
|
|
* @param {string} title - 저장할 이미지 이름
|
|
*/
|
|
const saveImage = async (title = 'canvas', userId, setThumbnails) => {
|
|
removeMouseLines()
|
|
await writeImage(title, canvas?.toDataURL('image/png').replace('data:image/png;base64,', ''))
|
|
.then((res) => {
|
|
console.log('success', res)
|
|
})
|
|
.catch((err) => {
|
|
console.log('err', err)
|
|
})
|
|
|
|
// const canvasStatus = addCanvas()
|
|
|
|
// const patternData = {
|
|
// userId: userId,
|
|
// imageName: title,
|
|
// objectNo: 'test123240822001',
|
|
// canvasStatus: JSON.stringify(canvasStatus).replace(/"/g, '##'),
|
|
// }
|
|
|
|
// await post({ url: '/api/canvas-management/canvas-statuses', data: patternData })
|
|
|
|
// setThumbnails((prev) => [...prev, { imageName: `/canvasState/${title}.png`, userId, canvasStatus: JSON.stringify(canvasStatus) }])
|
|
}
|
|
|
|
const handleFlip = () => {
|
|
const target = canvas?.getActiveObject()
|
|
|
|
if (!target) {
|
|
return
|
|
}
|
|
|
|
// 현재 scaleX 및 scaleY 값을 가져옵니다.
|
|
const scaleX = target.scaleX
|
|
// const scaleY = target.scaleY;
|
|
|
|
// 도형을 반전시킵니다.
|
|
target.set({
|
|
scaleX: scaleX * -1,
|
|
// scaleY: scaleY * -1
|
|
})
|
|
|
|
// 캔버스를 다시 그립니다.
|
|
canvas?.renderAll()
|
|
}
|
|
|
|
function fillCanvasWithDots(canvas, gap) {
|
|
const width = canvas.getWidth()
|
|
const height = canvas.getHeight()
|
|
|
|
for (let x = 0; x < width; x += gap) {
|
|
for (let y = 0; y < height; y += gap) {
|
|
const circle = new fabric.Circle({
|
|
radius: 1,
|
|
fill: 'black',
|
|
left: x,
|
|
top: y,
|
|
selectable: false,
|
|
})
|
|
canvas.add(circle)
|
|
}
|
|
}
|
|
|
|
canvas?.renderAll()
|
|
}
|
|
|
|
const setCanvasBackgroundWithDots = (canvas, gap) => {
|
|
// Create a new canvas and fill it with dots
|
|
const tempCanvas = new fabric.StaticCanvas()
|
|
tempCanvas.setDimensions({
|
|
width: canvas.getWidth(),
|
|
height: canvas.getHeight(),
|
|
})
|
|
fillCanvasWithDots(tempCanvas, gap)
|
|
|
|
// Convert the dotted canvas to an image
|
|
const dataUrl = tempCanvas.toDataURL({ format: 'png' })
|
|
|
|
// Set the image as the background of the original canvas
|
|
fabric.Image.fromURL(dataUrl, function (img) {
|
|
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), {
|
|
scaleX: canvas.width / img.width,
|
|
scaleY: canvas.height / img.height,
|
|
})
|
|
})
|
|
}
|
|
|
|
const addCanvas = () => {
|
|
// const canvasState = canvas
|
|
|
|
const objs = canvas?.toJSON([
|
|
'selectable',
|
|
'name',
|
|
'parentId',
|
|
'id',
|
|
'length',
|
|
'idx',
|
|
'direction',
|
|
'lines',
|
|
'points',
|
|
'lockMovementX',
|
|
'lockMovementY',
|
|
'lockRotation',
|
|
'lockScalingX',
|
|
'lockScalingY',
|
|
'opacity',
|
|
'cells',
|
|
'maxX',
|
|
'maxY',
|
|
'minX',
|
|
'minY',
|
|
'x',
|
|
'y',
|
|
'stickeyPoint',
|
|
])
|
|
|
|
const str = JSON.stringify(objs)
|
|
|
|
canvas?.clear()
|
|
return str
|
|
|
|
// setTimeout(() => {
|
|
// // 역직렬화하여 캔버스에 객체를 다시 추가합니다.
|
|
// canvas?.loadFromJSON(JSON.parse(str), function () {
|
|
// // 모든 객체가 로드되고 캔버스에 추가된 후 호출됩니다.
|
|
// console.log(canvas?.getObjects().filter((obj) => obj.name === 'roof'))
|
|
// canvas?.renderAll() // 캔버스를 다시 그립니다.
|
|
// })
|
|
// }, 1000)
|
|
}
|
|
|
|
/**
|
|
* cad 파일 사용시 이미지 로딩 함수
|
|
*/
|
|
const handleBackImageLoadToCanvas = (url) => {
|
|
canvas
|
|
.getObjects()
|
|
.filter((obj) => obj.name === 'backGroundImage')
|
|
.forEach((img) => {
|
|
canvas.remove(img)
|
|
canvas?.renderAll()
|
|
})
|
|
fabric.Image.fromURL(`${url}?${new Date().getTime()}`, function (img) {
|
|
console.log(img)
|
|
img.set({
|
|
left: 0,
|
|
top: 0,
|
|
width: img.width,
|
|
height: img.height,
|
|
name: 'backGroundImage',
|
|
selectable: false,
|
|
hasRotatingPoint: false, // 회전 핸들 활성화
|
|
lockMovementX: false,
|
|
lockMovementY: false,
|
|
lockRotation: false,
|
|
lockScalingX: false,
|
|
lockScalingY: false,
|
|
})
|
|
// image = img
|
|
canvas?.add(img)
|
|
canvas?.sendToBack(img)
|
|
canvas?.renderAll()
|
|
setBackImg(img)
|
|
})
|
|
}
|
|
|
|
const handleCadImageInit = () => {
|
|
canvas.clear()
|
|
}
|
|
|
|
const getCurrentCanvas = () => {
|
|
return canvas.toJSON([
|
|
'selectable',
|
|
'name',
|
|
'parentId',
|
|
'id',
|
|
'length',
|
|
'idx',
|
|
'direction',
|
|
'lines',
|
|
'points',
|
|
'lockMovementX',
|
|
'lockMovementY',
|
|
'lockRotation',
|
|
'lockScalingX',
|
|
'lockScalingY',
|
|
'opacity',
|
|
'cells',
|
|
'maxX',
|
|
'maxY',
|
|
'minX',
|
|
'minY',
|
|
'x',
|
|
'y',
|
|
'stickeyPoint',
|
|
])
|
|
}
|
|
|
|
return {
|
|
canvas,
|
|
addShape,
|
|
handleUndo,
|
|
handleRedo,
|
|
handleCopy,
|
|
handleSave,
|
|
handlePaste,
|
|
handleRotate,
|
|
attachCustomControlOnPolygon,
|
|
saveImage,
|
|
handleFlip,
|
|
setCanvasBackgroundWithDots,
|
|
addCanvas,
|
|
removeMouseLines,
|
|
handleBackImageLoadToCanvas,
|
|
handleCadImageInit,
|
|
backImg,
|
|
setBackImg,
|
|
}
|
|
}
|