- canvas.selection: 모듈 기본설정 + 手動配置 비활성 조건에서만 true - refineModuleSelection: 비모듈 혼합 선택 시 모듈만 재구성, 단일/복수 이동잠금 해제 - commitModuleDragMove: 드래그 완료 후 지붕면 이탈/겹침 검증, 위반 시 시작 위치 복원 - modulesRemove: 복수선택 일괄 삭제 지원, 회로 연결 시 전체 차단 - QContextMenu: 2개 이상 선택 시 컨텍스트 메뉴 차단 Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
617 lines
19 KiB
JavaScript
617 lines
19 KiB
JavaScript
import { useEffect, useRef, useState } from 'react'
|
|
import { fabric } from 'fabric'
|
|
import { useRecoilState, useRecoilValue } from 'recoil'
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
import { canvasSizeState, canvasState, canvasZoomState, currentMenuState, currentObjectState, isManualModuleSetupState } from '@/store/canvasAtom'
|
|
import { QPolygon } from '@/components/fabric/QPolygon'
|
|
import { fontSelector } from '@/store/fontAtom'
|
|
import { MENU, POLYGON_TYPE } from '@/common/common'
|
|
import { useText } from '@/hooks/useText'
|
|
import { usePolygon } from '@/hooks/usePolygon'
|
|
import { useModule } from '@/hooks/module/useModule'
|
|
|
|
// 캔버스에 필요한 이벤트
|
|
export function useCanvasEvent() {
|
|
const canvas = useRecoilValue(canvasState)
|
|
const [canvasForEvent, setCanvasForEvent] = useState(null)
|
|
const [currentObject, setCurrentObject] = useRecoilState(currentObjectState)
|
|
const canvasSize = useRecoilValue(canvasSizeState)
|
|
const [canvasZoom, setCanvasZoom] = useRecoilState(canvasZoomState)
|
|
const [shouldCenter, setShouldCenter] = useState(false) // 중앙 정렬 플래그 추가
|
|
const lengthTextOption = useRecoilValue(fontSelector('lengthText'))
|
|
const currentMenu = useRecoilValue(currentMenuState)
|
|
const isManualModuleSetup = useRecoilValue(isManualModuleSetupState)
|
|
const { changeCorridorDimensionText } = useText()
|
|
const { setPolygonLinesActualSize } = usePolygon()
|
|
const { commitModuleDragMove } = useModule()
|
|
// [MODULE-MULTI-SELECT 2026-06-10] 모듈 복수선택 refine 재진입 가드 (rebuild 중첩 방지)
|
|
const refiningRef = useRef(false)
|
|
|
|
useEffect(() => {
|
|
canvas?.setZoom(canvasZoom / 100)
|
|
}, [canvasZoom])
|
|
|
|
// canvasZoom이 100으로 변경되고 shouldCenter가 true일 때만 중앙 정렬 실행
|
|
useEffect(() => {
|
|
if (canvasZoom === 100 && canvas && shouldCenter) {
|
|
setTimeout(() => {
|
|
zoomToAllObjects()
|
|
canvas.renderAll()
|
|
setShouldCenter(false) // 실행 후 플래그 초기화
|
|
}, 100)
|
|
}
|
|
}, [canvasZoom, shouldCenter])
|
|
|
|
useEffect(() => {
|
|
attachDefaultEventOnCanvas()
|
|
// [MODULE-MULTI-SELECT 2026-06-10] 모듈 기본설정 메뉴에서만 드래그 박스/복수선택 활성화.
|
|
// 그 외 메뉴는 selection:false 유지(지붕선·마루 등 다른 레이어가 드래그에 잡히는 부작용 방지).
|
|
// [MODULE-MULTI-SELECT 2026-06-10] 手動配置 활성 중에는 드래그 모드 중지(배치 클릭과 충돌 방지).
|
|
if (canvas) {
|
|
const isModuleMenu = currentMenu === MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING
|
|
const enableDrag = isModuleMenu && !isManualModuleSetup
|
|
canvas.selection = enableDrag
|
|
if (!enableDrag) canvas.discardActiveObject()
|
|
}
|
|
}, [currentMenu, isManualModuleSetup])
|
|
|
|
// 기본적인 이벤트 필요시 추가
|
|
const attachDefaultEventOnCanvas = () => {
|
|
removeEventOnCanvas()
|
|
canvas?.on('object:added', objectEvent.onChange)
|
|
canvas?.on('object:added', objectEvent.addEvent)
|
|
canvas?.on('object:modified', objectEvent.onChange)
|
|
canvas?.on('object:modified', handleModuleSelectionMoved)
|
|
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', () => {
|
|
document.addEventListener('keydown', handleKeyDown)
|
|
})*/
|
|
canvas?.on('object:removed', objectEvent.removed)
|
|
}
|
|
|
|
const objectEvent = {
|
|
onChange: (e) => {
|
|
const target = e.target
|
|
|
|
if (target) {
|
|
// settleDown(target)
|
|
// roof 이동 후 좌표 재계산
|
|
if (target.name === POLYGON_TYPE.ROOF && target.type === 'QPolygon') {
|
|
target.fire('polygonMoved')
|
|
}
|
|
}
|
|
},
|
|
addEvent: (e) => {
|
|
const target = e.target
|
|
|
|
if (!target.id) {
|
|
target.id = uuidv4()
|
|
}
|
|
if (!target.uuid) {
|
|
target.uuid = uuidv4()
|
|
}
|
|
|
|
if (target.type === 'QPolygon' || target.type === 'QLine') {
|
|
const textObjs = canvas?.getObjects().filter((obj) => obj.name === 'lengthText')
|
|
textObjs.forEach((obj) => {
|
|
obj.bringToFront()
|
|
})
|
|
|
|
if (target.name === POLYGON_TYPE.ROOF) {
|
|
setTimeout(() => {
|
|
setPolygonLinesActualSize(target)
|
|
changeCorridorDimensionText()
|
|
}, 300)
|
|
}
|
|
}
|
|
|
|
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' && target.type.toLowerCase().includes('text') > 0) {
|
|
const x = target.left
|
|
const y = target.top
|
|
target.lockMovementX = false
|
|
target.lockMovementY = false
|
|
|
|
target.fill = lengthTextOption.fontColor.value
|
|
target.fontFamily = lengthTextOption.fontFamily.value
|
|
target.fontSize = lengthTextOption.fontSize.value
|
|
target.fontStyle = lengthTextOption.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal'
|
|
target.fontWeight = lengthTextOption.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal'
|
|
|
|
// Add a property to store the previous value
|
|
const previousValue = target.text
|
|
target.on('selected', (e) => {
|
|
Object.keys(target.controls).forEach((controlKey) => {
|
|
target.setControlVisible(controlKey, false)
|
|
})
|
|
})
|
|
target.on('moving', (e) => {
|
|
target.uuid = uuidv4()
|
|
|
|
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)) {
|
|
}
|
|
},
|
|
}
|
|
|
|
const selectionEvent = {
|
|
created: (e) => {
|
|
const target = e.selected[0]
|
|
setCurrentObject(target)
|
|
const { selected } = e
|
|
|
|
if (selected?.length > 0) {
|
|
selected.forEach((obj) => {
|
|
// if (obj.type === 'QPolygon' && currentMenu !== MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING) {
|
|
if (obj.type === 'QPolygon') {
|
|
const originStroke = obj.stroke
|
|
obj.set({ stroke: 'red' })
|
|
|
|
if (currentMenu === MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING) {
|
|
if (obj.name === POLYGON_TYPE.MODULE) {
|
|
obj.set({ strokeWidth: 3 })
|
|
}
|
|
if (obj.name === POLYGON_TYPE.ROOF) {
|
|
canvas.discardActiveObject()
|
|
obj.set({ stroke: originStroke })
|
|
}
|
|
}
|
|
}
|
|
})
|
|
canvas.renderAll()
|
|
}
|
|
refineModuleSelection()
|
|
},
|
|
cleared: (e) => {
|
|
setCurrentObject(null)
|
|
const { deselected } = e
|
|
|
|
if (deselected?.length > 0) {
|
|
deselected.forEach((obj) => {
|
|
if (obj.type === 'QPolygon') {
|
|
if (obj.name !== 'moduleSetupSurface') {
|
|
obj.set({ stroke: 'black' })
|
|
}
|
|
if (obj.name === POLYGON_TYPE.MODULE && currentMenu === MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING) {
|
|
obj.set({ strokeWidth: 0.3 })
|
|
}
|
|
// [MODULE-MULTI-SELECT 2026-06-10] 선택 해제 시 모듈 이동잠금 복원(다른 메뉴에서 드래그 방지)
|
|
if (obj.name === POLYGON_TYPE.MODULE) {
|
|
obj.set({ lockMovementX: true, lockMovementY: true })
|
|
}
|
|
}
|
|
})
|
|
}
|
|
canvas.renderAll()
|
|
},
|
|
updated: (e) => {
|
|
const target = e.selected[0]
|
|
setCurrentObject(target)
|
|
const { selected, deselected } = e
|
|
|
|
if (deselected?.length > 0) {
|
|
deselected.forEach((obj) => {
|
|
if (obj.type === 'QPolygon') {
|
|
obj.set({ stroke: 'black' })
|
|
if (obj.name === POLYGON_TYPE.MODULE && currentMenu === MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING) {
|
|
//모듈 미선택시 라인 두께 변경
|
|
obj.set({ strokeWidth: 0.3 })
|
|
}
|
|
// [MODULE-MULTI-SELECT 2026-06-10] 선택 해제 시 모듈 이동잠금 복원(다른 메뉴에서 드래그 방지)
|
|
if (obj.name === POLYGON_TYPE.MODULE) {
|
|
obj.set({ lockMovementX: true, lockMovementY: true })
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
if (selected?.length > 0) {
|
|
selected.forEach((obj) => {
|
|
if (obj.type === 'QPolygon') {
|
|
obj.set({ stroke: 'red' })
|
|
if (obj.name === POLYGON_TYPE.MODULE && currentMenu === MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING) {
|
|
//모듈 선택시 라인 두께 변경
|
|
obj.set({ strokeWidth: 3 })
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
canvas.renderAll()
|
|
refineModuleSelection()
|
|
},
|
|
}
|
|
|
|
// [MODULE-MULTI-SELECT 2026-06-10] 선택을 모듈만으로 정제, 이동잠금 해제 (회전·스케일은 잠금 유지). refiningRef 로 재구성 중첩 차단.
|
|
const refineModuleSelection = () => {
|
|
if (!canvas) return
|
|
if (currentMenu !== MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING) return
|
|
if (refiningRef.current) return
|
|
|
|
const active = canvas.getActiveObject()
|
|
if (!active) return
|
|
|
|
// 단일 모듈 선택: 마우스 이동 가능하도록 이동잠금 해제 (회전·스케일은 잠금 유지).
|
|
if (active.type !== 'activeSelection') {
|
|
if (active.name === POLYGON_TYPE.MODULE) {
|
|
active.set({ lockMovementX: false, lockMovementY: false, lockRotation: true, lockScalingX: true, lockScalingY: true, hasControls: false })
|
|
canvas.requestRenderAll()
|
|
}
|
|
return
|
|
}
|
|
|
|
const objs = active.getObjects()
|
|
const modules = objs.filter((obj) => obj.name === POLYGON_TYPE.MODULE)
|
|
|
|
if (modules.length !== objs.length) {
|
|
refiningRef.current = true
|
|
canvas.discardActiveObject()
|
|
if (modules.length > 1) {
|
|
canvas.setActiveObject(new fabric.ActiveSelection(modules, { canvas }))
|
|
} else if (modules.length === 1) {
|
|
canvas.setActiveObject(modules[0])
|
|
}
|
|
refiningRef.current = false
|
|
}
|
|
|
|
const finalActive = canvas.getActiveObject()
|
|
if (finalActive?.type === 'activeSelection') {
|
|
finalActive.set({
|
|
lockMovementX: false,
|
|
lockMovementY: false,
|
|
lockScalingX: true,
|
|
lockScalingY: true,
|
|
lockRotation: true,
|
|
hasControls: false,
|
|
})
|
|
}
|
|
canvas.requestRenderAll()
|
|
}
|
|
|
|
// [MODULE-MULTI-SELECT 2026-06-10] object:modified 시 지붕면 경계 검증 → 위반이면 드래그 시작 좌표로 복원
|
|
const handleModuleSelectionMoved = (e) => {
|
|
if (currentMenu !== MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING) return
|
|
const target = e?.target
|
|
if (!target) return
|
|
|
|
const isGroup = target.type === 'activeSelection'
|
|
const isSingleModule = target.name === POLYGON_TYPE.MODULE
|
|
if (!isGroup && !isSingleModule) return
|
|
if (isGroup && !target.getObjects().some((obj) => obj.name === POLYGON_TYPE.MODULE)) return
|
|
|
|
const original = e?.transform?.original
|
|
const startPos = original ? { left: original.left, top: original.top } : null
|
|
commitModuleDragMove(target, startPos)
|
|
}
|
|
|
|
const removeEventOnCanvas = () => {
|
|
canvas?.off('object:added')
|
|
canvas?.off('object:modified')
|
|
canvas?.off('object:removed')
|
|
canvas?.off('selection:cleared')
|
|
canvas?.off('selection:created')
|
|
canvas?.off('selection:updated')
|
|
}
|
|
|
|
/**
|
|
* 각종 키보드 이벤트
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
|
|
*/
|
|
const handleKeyDown = (e) => {
|
|
const key = e.key
|
|
|
|
switch (key) {
|
|
case 'Delete':
|
|
case 'Backspace':
|
|
// handleDelete()
|
|
break
|
|
case 'Down': // IE/Edge에서 사용되는 값
|
|
case 'ArrowDown':
|
|
// "아래 화살표" 키가 눌렸을 때의 동작입니다.
|
|
// moveDown()
|
|
break
|
|
case 'Up': // IE/Edge에서 사용되는 값
|
|
case 'ArrowUp':
|
|
// "위 화살표" 키가 눌렸을 때의 동작입니다.
|
|
// moveUp()
|
|
break
|
|
case 'Left': // IE/Edge에서 사용되는 값
|
|
case 'ArrowLeft':
|
|
// "왼쪽 화살표" 키가 눌렸을 때의 동작입니다.
|
|
// moveLeft()
|
|
break
|
|
case 'Right': // IE/Edge에서 사용되는 값
|
|
case 'ArrowRight':
|
|
// "오른쪽 화살표" 키가 눌렸을 때의 동작입니다.
|
|
// moveRight()
|
|
break
|
|
case 'Enter':
|
|
// "enter" 또는 "return" 키가 눌렸을 때의 동작입니다.
|
|
break
|
|
case 'Esc': // IE/Edge에서 사용되는 값
|
|
case 'Escape':
|
|
break
|
|
|
|
case 'z':
|
|
if (!e.ctrlKey) {
|
|
return
|
|
}
|
|
|
|
console.log('뒤로가기')
|
|
|
|
break
|
|
|
|
default:
|
|
return // 키 이벤트를 처리하지 않는다면 종료합니다.
|
|
}
|
|
e.preventDefault()
|
|
}
|
|
|
|
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 })
|
|
targetObj.fire('modified')
|
|
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 })
|
|
targetObj.fire('modified')
|
|
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 })
|
|
targetObj.fire('modified')
|
|
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 })
|
|
targetObj.fire('modified')
|
|
canvas?.renderAll()
|
|
}
|
|
|
|
/**
|
|
* 선택한 도형을 삭제한다.
|
|
*/
|
|
const handleDelete = () => {
|
|
const targets = canvas?.getActiveObjects()
|
|
if (targets?.length === 0) {
|
|
alert('삭제할 대상을 선택해주세요.')
|
|
return
|
|
}
|
|
|
|
if (!confirm('정말로 삭제하시겠습니까?')) {
|
|
return
|
|
}
|
|
|
|
targets?.forEach((target) => {
|
|
canvas?.remove(target)
|
|
})
|
|
}
|
|
|
|
const handleZoom = (isZoom) => {
|
|
let zoom
|
|
if (isZoom) {
|
|
zoom = canvasZoom + 10
|
|
if (zoom > 500) {
|
|
return
|
|
}
|
|
} else {
|
|
zoom = canvasZoom - 10
|
|
if (zoom < 10) {
|
|
//50%->10%
|
|
return
|
|
}
|
|
}
|
|
|
|
setCanvasZoom(zoom)
|
|
}
|
|
|
|
const handleZoomClear = () => {
|
|
setShouldCenter(true) // 중앙 정렬 플래그 설정
|
|
setCanvasZoom(100)
|
|
}
|
|
|
|
const zoomToAllObjects = () => {
|
|
const allObjects = canvas.getObjects()
|
|
// 주요 도형 객체만 필터링 (roof, arrow 등)
|
|
const objects = allObjects.filter((obj) => {
|
|
// 주요 도형 객체만 포함
|
|
return obj.name === 'roof' || obj.name === 'arrow' || obj.name === 'module' || obj.name === 'dormer' || obj.name === 'object'
|
|
})
|
|
|
|
console.log('🔍 All objects:', allObjects.length)
|
|
console.log('🔍 Main objects for centering:', objects.length)
|
|
console.log(
|
|
'🔍 Main object details:',
|
|
objects.map((obj) => ({
|
|
name: obj.name,
|
|
type: obj.type,
|
|
visible: obj.visible,
|
|
id: obj.id,
|
|
})),
|
|
)
|
|
|
|
if (objects.length === 0) {
|
|
console.log('❌ No main objects found for centering')
|
|
return
|
|
}
|
|
|
|
let minX = Infinity,
|
|
minY = Infinity
|
|
let maxX = -Infinity,
|
|
maxY = -Infinity
|
|
|
|
objects.forEach((obj) => {
|
|
const bounds = obj.getBoundingRect()
|
|
minX = Math.min(minX, bounds.left)
|
|
minY = Math.min(minY, bounds.top)
|
|
maxX = Math.max(maxX, bounds.left + bounds.width)
|
|
maxY = Math.max(maxY, bounds.top + bounds.height)
|
|
})
|
|
|
|
const objectsCenterX = (minX + maxX) / 2
|
|
const objectsCenterY = (minY + maxY) / 2
|
|
|
|
// 화면 중앙 좌표 (메뉴 높이 고려하여 Y 위치 조정)
|
|
const screenCenterX = canvas.getWidth() / 2
|
|
const menuHeightOffset = 80 // 위쪽 메뉴 높이만큼 위로 조정
|
|
const screenCenterY = canvas.getHeight() / 2 - menuHeightOffset
|
|
|
|
// 객체 중심을 화면 중앙으로 이동
|
|
const deltaX = screenCenterX - objectsCenterX
|
|
const deltaY = screenCenterY - objectsCenterY
|
|
|
|
// canvas viewport transform 조정
|
|
const viewportTransform = canvas.viewportTransform
|
|
viewportTransform[4] += deltaX
|
|
viewportTransform[5] += deltaY
|
|
|
|
canvas.setViewportTransform(viewportTransform)
|
|
|
|
// 브라우저 스크롤을 중앙으로 조정하여 전체 canvas를 모니터 중앙에 위치
|
|
const canvasElement = canvas.getElement()
|
|
const canvasRect = canvasElement.getBoundingClientRect()
|
|
|
|
// 현재 스크롤 위치
|
|
const currentScrollTop = window.pageYOffset || document.documentElement.scrollTop
|
|
const currentScrollLeft = window.pageXOffset || document.documentElement.scrollLeft
|
|
|
|
// canvas의 현재 화면 위치
|
|
const canvasScreenTop = canvasRect.top + currentScrollTop
|
|
const canvasScreenLeft = canvasRect.left + currentScrollLeft
|
|
|
|
// 모니터 중앙
|
|
const monitorCenterX = window.innerWidth / 2
|
|
const monitorCenterY = window.innerHeight / 2
|
|
|
|
// canvas가 모니터 중앙에 오도록 스크롤 위치 계산
|
|
const targetScrollLeft = canvasScreenLeft + canvasRect.width / 2 - monitorCenterX
|
|
const targetScrollTop = canvasScreenTop + canvasRect.height / 2 - monitorCenterY + menuHeightOffset
|
|
|
|
// 스크롤을 중앙으로 이동
|
|
window.scrollTo({
|
|
top: targetScrollTop,
|
|
left: targetScrollLeft,
|
|
behavior: 'smooth', // 부드러운 스크롤
|
|
})
|
|
|
|
canvas.renderAll()
|
|
}
|
|
|
|
return {
|
|
setCanvasForEvent,
|
|
attachDefaultEventOnCanvas,
|
|
handleZoomClear,
|
|
handleZoom,
|
|
}
|
|
}
|