import { useEffect, useState } from 'react' import { fabric } from 'fabric' import { actionHandler, anchorWrapper, polygonPositionHandler } from '@/util/canvas-util' import { useRecoilState, useRecoilValue } 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' import { imageDisplaySelector } from '@/store/settingAtom' 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 isImageDisplay = useRecoilValue(imageDisplaySelector) const {} = useFont() /** * 처음 셋팅 */ useEffect(() => { const c = new fabric.Canvas(id, { height: canvasSize.vertical, width: canvasSize.horizontal, backgroundColor: 'white', preserveObjectStacking: true, 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, visible: isImageDisplay, }) // 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, } }