object 회전 기능 추가
This commit is contained in:
parent
36d16069e0
commit
7d9b6d5225
@ -216,6 +216,8 @@ export const SAVE_KEY = [
|
||||
'isMultipleOf45',
|
||||
'from',
|
||||
'originColor',
|
||||
'originWidth',
|
||||
'originHeight',
|
||||
]
|
||||
|
||||
export const OBJECT_PROTOTYPE = [fabric.Line.prototype, fabric.Polygon.prototype, fabric.Triangle.prototype, fabric.Group.prototype]
|
||||
|
||||
@ -87,6 +87,10 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
this.initLines()
|
||||
this.init()
|
||||
this.setShape()
|
||||
const originWidth = this.originWidth ?? this.width
|
||||
const originHeight = this.originHeight ?? this.height
|
||||
this.originWidth = this.angle === 90 || this.angle === 270 ? originHeight : originWidth
|
||||
this.originHeight = this.angle === 90 || this.angle === 270 ? originWidth : originHeight
|
||||
},
|
||||
|
||||
setShape() {
|
||||
@ -126,11 +130,13 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
this.on('moving', () => {
|
||||
this.initLines()
|
||||
this.addLengthText()
|
||||
this.setCoords()
|
||||
})
|
||||
|
||||
this.on('modified', (e) => {
|
||||
this.initLines()
|
||||
this.addLengthText()
|
||||
this.setCoords()
|
||||
})
|
||||
|
||||
this.on('selected', () => {
|
||||
|
||||
@ -12,7 +12,7 @@ import { useSurfaceShapeBatch } from '@/hooks/surface/useSurfaceShapeBatch'
|
||||
|
||||
export default function SizeSetting(props) {
|
||||
const contextPopupPosition = useRecoilValue(contextPopupPositionState)
|
||||
const [settingTarget, setSettingTarget] = useState(1)
|
||||
const [settingTarget, setSettingTarget] = useState(props.side || 1)
|
||||
const { id, pos = contextPopupPosition, target } = props
|
||||
const { getMessage } = useMessage()
|
||||
const { closePopup } = usePopup()
|
||||
@ -47,11 +47,11 @@ export default function SizeSetting(props) {
|
||||
<div className="size-option-top">
|
||||
<div className="size-option-wrap">
|
||||
<div className="size-option mb5">
|
||||
<input type="text" className="input-origin mr5" value={target?.width.toFixed(0) * 10} readOnly={true} />
|
||||
<input type="text" className="input-origin mr5" value={(target?.originWidth * 10).toFixed(0)} readOnly={true} />
|
||||
<span className="normal-font">mm</span>
|
||||
</div>
|
||||
<div className="size-option">
|
||||
<input type="text" className="input-origin mr5" defaultValue={target?.width.toFixed(0) * 10} ref={widthRef} />
|
||||
<input type="text" className="input-origin mr5" defaultValue={(target?.originWidth * 10).toFixed(0)} ref={widthRef} />
|
||||
<span className="normal-font">mm</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -60,11 +60,11 @@ export default function SizeSetting(props) {
|
||||
<div className="size-option-side">
|
||||
<div className="size-option-wrap">
|
||||
<div className="size-option mb5">
|
||||
<input type="text" className="input-origin mr5" value={target?.height.toFixed(0) * 10} readOnly={true} />
|
||||
<input type="text" className="input-origin mr5" value={(target?.originHeight * 10).toFixed(0)} readOnly={true} />
|
||||
<span className="normal-font">mm</span>
|
||||
</div>
|
||||
<div className="size-option">
|
||||
<input type="text" className="input-origin mr5" defaultValue={target?.height.toFixed(0) * 10} ref={heightRef} />
|
||||
<input type="text" className="input-origin mr5" defaultValue={(target?.originHeight * 10).toFixed(0)} ref={heightRef} />
|
||||
<span className="normal-font">mm</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { useRecoilValue, useResetRecoilState } from 'recoil'
|
||||
import { canvasSettingState, canvasState, currentCanvasPlanState, globalPitchState } from '@/store/canvasAtom'
|
||||
import { canvasSettingState, canvasState, currentCanvasPlanState, currentObjectState, globalPitchState } from '@/store/canvasAtom'
|
||||
import { LINE_TYPE, MENU, POLYGON_TYPE } from '@/common/common'
|
||||
import { getIntersectionPoint, toFixedWithoutRounding } from '@/util/canvas-util'
|
||||
import { getIntersectionPoint } from '@/util/canvas-util'
|
||||
import { degreesToRadians } from '@turf/turf'
|
||||
import { QPolygon } from '@/components/fabric/QPolygon'
|
||||
import { useSwal } from '@/hooks/useSwal'
|
||||
@ -21,10 +21,13 @@ import { placementShapeDrawingPointsState } from '@/store/placementShapeDrawingA
|
||||
import { getBackGroundImage } from '@/lib/imageActions'
|
||||
import { useCanvasSetting } from '@/hooks/option/useCanvasSetting'
|
||||
import { useText } from '@/hooks/useText'
|
||||
import SizeSetting from '@/components/floor-plan/modal/object/SizeSetting'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useState } from 'react'
|
||||
|
||||
export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
const { getMessage } = useMessage()
|
||||
const { drawDirectionArrow, addPolygon, addLengthText } = usePolygon()
|
||||
const { drawDirectionArrow, addPolygon, addLengthText, setPolygonLinesActualSize } = usePolygon()
|
||||
const lengthTextFont = useRecoilValue(fontSelector('lengthText'))
|
||||
const resetOuterLinePoints = useResetRecoilState(outerLinePointsState)
|
||||
const resetPlacementShapeDrawingPoints = useResetRecoilState(placementShapeDrawingPointsState)
|
||||
@ -36,11 +39,13 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
const { swalFire } = useSwal()
|
||||
const { addCanvasMouseEventListener, initEvent } = useEvent()
|
||||
// const { addCanvasMouseEventListener, initEvent } = useContext(EventContext)
|
||||
const { addPopup, closePopup } = usePopup()
|
||||
const { addPopup, closePopup, closeAll } = usePopup()
|
||||
const { setSurfaceShapePattern } = useRoofFn()
|
||||
const { changeCorridorDimensionText } = useText()
|
||||
const currentCanvasPlan = useRecoilValue(currentCanvasPlanState)
|
||||
const { fetchSettings } = useCanvasSetting(false)
|
||||
const currentObject = useRecoilValue(currentObjectState)
|
||||
const [popupId, setPopupId] = useState(uuidv4())
|
||||
|
||||
const applySurfaceShape = (surfaceRefs, selectedType, id) => {
|
||||
let length1, length2, length3, length4, length5
|
||||
@ -879,6 +884,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
drawDirectionArrow(roof)
|
||||
changeCorridorDimensionText()
|
||||
addLengthText(roof)
|
||||
roof.setCoords()
|
||||
initEvent()
|
||||
canvas.renderAll()
|
||||
})
|
||||
@ -916,71 +922,138 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
}
|
||||
|
||||
const resizeSurfaceShapeBatch = (side, target, width, height) => {
|
||||
const originTarget = { ...target }
|
||||
if (!target || target.type !== 'QPolygon') return
|
||||
|
||||
const objectWidth = target.width
|
||||
const objectHeight = target.height
|
||||
const changeWidth = width / 10 / objectWidth
|
||||
const changeHeight = height / 10 / objectHeight
|
||||
let sideX = 'left'
|
||||
let sideY = 'top'
|
||||
width = width / 10
|
||||
height = height / 10
|
||||
|
||||
//그룹 중심점 변경
|
||||
if (side === 2) {
|
||||
sideX = 'right'
|
||||
sideY = 'top'
|
||||
} else if (side === 3) {
|
||||
sideX = 'left'
|
||||
sideY = 'bottom'
|
||||
} else if (side === 4) {
|
||||
sideX = 'right'
|
||||
sideY = 'bottom'
|
||||
// 현재 QPolygon의 점들 가져오기 (변형 적용된 실제 좌표)
|
||||
const currentPoints = target.getCurrentPoints() || []
|
||||
const angle = target.angle % 360
|
||||
if (currentPoints.length === 0) return
|
||||
|
||||
// 현재 바운딩 박스 계산
|
||||
let minX = Math.min(...currentPoints.map((p) => p.x))
|
||||
let maxX = Math.max(...currentPoints.map((p) => p.x))
|
||||
let minY = Math.min(...currentPoints.map((p) => p.y))
|
||||
let maxY = Math.max(...currentPoints.map((p) => p.y))
|
||||
|
||||
let currentWidth = maxX - minX
|
||||
let currentHeight = maxY - minY
|
||||
|
||||
// 회전에 관계없이 단순한 앵커 포인트 계산
|
||||
let anchorX, anchorY
|
||||
switch (side) {
|
||||
case 1: // left-top
|
||||
anchorX = minX
|
||||
anchorY = minY
|
||||
break
|
||||
case 2: // right-top
|
||||
anchorX = maxX
|
||||
anchorY = minY
|
||||
break
|
||||
case 3: // left-bottom
|
||||
anchorX = minX
|
||||
anchorY = maxY
|
||||
break
|
||||
case 4: // right-bottom
|
||||
anchorX = maxX
|
||||
anchorY = maxY
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
//변경 전 좌표
|
||||
const newCoords = target.getPointByOrigin(sideX, sideY)
|
||||
// 목표 크기 (회전에 관계없이 동일하게 적용)
|
||||
let targetWidth = width
|
||||
let targetHeight = height
|
||||
|
||||
target.set({
|
||||
originX: sideX,
|
||||
originY: sideY,
|
||||
left: newCoords.x,
|
||||
top: newCoords.y,
|
||||
// 새로운 점들 계산 - 앵커 포인트는 고정, 나머지는 비례적으로 확장
|
||||
// 각도와 side에 따라 확장 방향 결정
|
||||
const newPoints = currentPoints.map((point) => {
|
||||
// 앵커 포인트 기준으로 새로운 위치 계산
|
||||
// side와 각도에 관계없이 일관된 방식으로 처리
|
||||
|
||||
// 앵커 포인트에서 각 점까지의 절대 거리
|
||||
const deltaX = point.x - anchorX
|
||||
const deltaY = point.y - anchorY
|
||||
|
||||
// 새로운 크기에 맞춰 비례적으로 확장
|
||||
const newDeltaX = (deltaX / currentWidth) * targetWidth
|
||||
const newDeltaY = (deltaY / currentHeight) * targetHeight
|
||||
|
||||
const newX = anchorX + newDeltaX
|
||||
const newY = anchorY + newDeltaY
|
||||
|
||||
return {
|
||||
x: newX, // 소수점 1자리로 반올림
|
||||
y: newY,
|
||||
}
|
||||
})
|
||||
|
||||
target.scaleX = changeWidth
|
||||
target.scaleY = changeHeight
|
||||
|
||||
const currentPoints = target.getCurrentPoints()
|
||||
|
||||
target.set({
|
||||
// 기존 객체의 속성들을 복사 (scale은 1로 고정)
|
||||
const originalOptions = {
|
||||
stroke: target.stroke,
|
||||
strokeWidth: target.strokeWidth,
|
||||
fill: target.fill,
|
||||
opacity: target.opacity,
|
||||
visible: target.visible,
|
||||
selectable: target.selectable,
|
||||
evented: target.evented,
|
||||
hoverCursor: target.hoverCursor,
|
||||
moveCursor: target.moveCursor,
|
||||
lockMovementX: target.lockMovementX,
|
||||
lockMovementY: target.lockMovementY,
|
||||
lockRotation: target.lockRotation,
|
||||
lockScalingX: target.lockScalingX,
|
||||
lockScalingY: target.lockScalingY,
|
||||
lockUniScaling: target.lockUniScaling,
|
||||
name: target.name,
|
||||
uuid: target.uuid,
|
||||
roofType: target.roofType,
|
||||
roofMaterial: target.roofMaterial,
|
||||
azimuth: target.azimuth,
|
||||
// tilt: target.tilt,
|
||||
// angle: target.angle,
|
||||
// scale은 항상 1로 고정
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
width: toFixedWithoutRounding(width / 10, 1),
|
||||
height: toFixedWithoutRounding(height / 10, 1),
|
||||
})
|
||||
//크기 변경후 좌표를 재 적용
|
||||
const changedCoords = target.getPointByOrigin(originTarget.originX, originTarget.originY)
|
||||
|
||||
target.set({
|
||||
originX: originTarget.originX,
|
||||
originY: originTarget.originY,
|
||||
left: changedCoords.x,
|
||||
top: changedCoords.y,
|
||||
})
|
||||
canvas.renderAll()
|
||||
|
||||
//면형상 리사이즈시에만
|
||||
target.fire('polygonMoved')
|
||||
target.points = currentPoints
|
||||
target.fire('modified')
|
||||
|
||||
setSurfaceShapePattern(target, roofDisplay.column, false, target.roofMaterial, true)
|
||||
|
||||
if (target.direction) {
|
||||
drawDirectionArrow(target)
|
||||
lines: target.lines,
|
||||
// 기타 모든 사용자 정의 속성들
|
||||
...Object.fromEntries(
|
||||
Object.entries(target).filter(
|
||||
([key, value]) =>
|
||||
!['type', 'left', 'top', 'width', 'height', 'scaleX', 'scaleY', 'points', 'lines', 'texts', 'canvas', 'angle', 'tilt'].includes(key) &&
|
||||
typeof value !== 'function',
|
||||
),
|
||||
),
|
||||
}
|
||||
target.setCoords()
|
||||
canvas.renderAll()
|
||||
|
||||
// 기존 QPolygon 제거
|
||||
canvas.remove(target)
|
||||
|
||||
// 새로운 QPolygon 생성 (scale은 1로 고정됨)
|
||||
const newPolygon = new QPolygon(newPoints, originalOptions, canvas)
|
||||
|
||||
newPolygon.set({
|
||||
originWidth: width,
|
||||
originHeight: height,
|
||||
})
|
||||
|
||||
// 캔버스에 추가
|
||||
canvas.add(newPolygon)
|
||||
|
||||
// 선택 상태 유지
|
||||
canvas.setActiveObject(newPolygon)
|
||||
|
||||
newPolygon.fire('modified')
|
||||
setSurfaceShapePattern(newPolygon, null, null, newPolygon.roofMaterial)
|
||||
drawDirectionArrow(newPolygon)
|
||||
newPolygon.setCoords()
|
||||
changeSurfaceLineType(newPolygon)
|
||||
canvas?.renderAll()
|
||||
closeAll()
|
||||
addPopup(popupId, 1, <SizeSetting id={popupId} side={side} target={newPolygon} />)
|
||||
}
|
||||
|
||||
const changeSurfaceLinePropertyEvent = () => {
|
||||
@ -1364,6 +1437,50 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
return orderedPoints
|
||||
}
|
||||
|
||||
const rotateSurfaceShapeBatch = () => {
|
||||
if (currentObject) {
|
||||
// 기존 관련 객체들 제거
|
||||
const relatedObjects = canvas
|
||||
.getObjects()
|
||||
.filter(
|
||||
(obj) =>
|
||||
obj.parentId === currentObject.id ||
|
||||
(obj.name === 'lengthText' && obj.parentId === currentObject.id) ||
|
||||
(obj.name === 'arrow' && obj.parentId === currentObject.id),
|
||||
)
|
||||
relatedObjects.forEach((obj) => canvas.remove(obj))
|
||||
|
||||
// 현재 회전값에 90도 추가
|
||||
const currentAngle = currentObject.angle || 0
|
||||
const newAngle = (currentAngle + 90) % 360
|
||||
const originWidth = currentObject.originWidth
|
||||
const originHeight = currentObject.originHeight
|
||||
// 회전 적용 (width/height 교체 제거로 도형 깨짐 방지)
|
||||
currentObject.rotate(newAngle)
|
||||
|
||||
// QPolygon 내부 구조 재구성 (선이 깨지는 문제 해결)
|
||||
if (currentObject.type === 'QPolygon' && currentObject.lines) {
|
||||
currentObject.initLines()
|
||||
}
|
||||
|
||||
currentObject.set({
|
||||
originWidth: originHeight,
|
||||
originHeight: originWidth,
|
||||
})
|
||||
|
||||
currentObject.setCoords()
|
||||
currentObject.fire('modified')
|
||||
|
||||
// 화살표와 선 다시 그리기
|
||||
drawDirectionArrow(currentObject)
|
||||
setTimeout(() => {
|
||||
setPolygonLinesActualSize(currentObject)
|
||||
changeSurfaceLineType(currentObject)
|
||||
}, 200)
|
||||
canvas.renderAll()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
applySurfaceShape,
|
||||
deleteAllSurfacesAndObjects,
|
||||
@ -1373,5 +1490,6 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
changeSurfaceLineProperty,
|
||||
changeSurfaceLinePropertyReset,
|
||||
changeSurfaceLineType,
|
||||
rotateSurfaceShapeBatch,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
import { canvasSettingState, canvasState, currentMenuState, currentObjectState } from '@/store/canvasAtom'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { MENU, POLYGON_TYPE } from '@/common/common'
|
||||
import AuxiliarySize from '@/components/floor-plan/modal/auxiliary/AuxiliarySize'
|
||||
import { usePopup } from '@/hooks/usePopup'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
@ -12,11 +11,9 @@ import { gridColorState } from '@/store/gridAtom'
|
||||
import { contextPopupPositionState, contextPopupState } from '@/store/popupAtom'
|
||||
import AuxiliaryEdit from '@/components/floor-plan/modal/auxiliary/AuxiliaryEdit'
|
||||
import SizeSetting from '@/components/floor-plan/modal/object/SizeSetting'
|
||||
import RoofMaterialSetting from '@/components/floor-plan/modal/object/RoofMaterialSetting'
|
||||
import DormerOffset from '@/components/floor-plan/modal/object/DormerOffset'
|
||||
import FontSetting from '@/components/common/font/FontSetting'
|
||||
import RoofAllocationSetting from '@/components/floor-plan/modal/roofAllocation/RoofAllocationSetting'
|
||||
import LinePropertySetting from '@/components/floor-plan/modal/lineProperty/LinePropertySetting'
|
||||
import FlowDirectionSetting from '@/components/floor-plan/modal/flowDirection/FlowDirectionSetting'
|
||||
|
||||
import { useCommonUtils } from './common/useCommonUtils'
|
||||
@ -29,7 +26,6 @@ import ColumnRemove from '@/components/floor-plan/modal/module/column/ColumnRemo
|
||||
import ColumnInsert from '@/components/floor-plan/modal/module/column/ColumnInsert'
|
||||
import RowRemove from '@/components/floor-plan/modal/module/row/RowRemove'
|
||||
import RowInsert from '@/components/floor-plan/modal/module/row/RowInsert'
|
||||
import CircuitNumberEdit from '@/components/floor-plan/modal/module/CircuitNumberEdit'
|
||||
import { useObjectBatch } from '@/hooks/object/useObjectBatch'
|
||||
import { useSurfaceShapeBatch } from '@/hooks/surface/useSurfaceShapeBatch'
|
||||
import { fontSelector, globalFontAtom } from '@/store/fontAtom'
|
||||
@ -45,6 +41,8 @@ import PlacementSurfaceLineProperty from '@/components/floor-plan/modal/placemen
|
||||
import { selectedMenuState } from '@/store/menuAtom'
|
||||
import { useTrestle } from './module/useTrestle'
|
||||
import { useCircuitTrestle } from './useCirCuitTrestle'
|
||||
import { usePolygon } from '@/hooks/usePolygon'
|
||||
import { useText } from '@/hooks/useText'
|
||||
|
||||
export function useContextMenu() {
|
||||
const canvas = useRecoilValue(canvasState)
|
||||
@ -64,7 +62,7 @@ export function useContextMenu() {
|
||||
const [column, setColumn] = useState(null)
|
||||
const { handleZoomClear } = useCanvasEvent()
|
||||
const { moveObjectBatch, copyObjectBatch } = useObjectBatch({})
|
||||
const { moveSurfaceShapeBatch } = useSurfaceShapeBatch({})
|
||||
const { moveSurfaceShapeBatch, rotateSurfaceShapeBatch } = useSurfaceShapeBatch({})
|
||||
const [globalFont, setGlobalFont] = useRecoilState(globalFontAtom)
|
||||
const { addLine, removeLine } = useLine()
|
||||
const { removeGrid } = useGrid()
|
||||
@ -73,10 +71,12 @@ export function useContextMenu() {
|
||||
const { settingsData, setSettingsDataSave } = useCanvasSetting(false)
|
||||
const { swalFire } = useSwal()
|
||||
const { alignModule, modulesRemove, moduleRoofRemove } = useModule()
|
||||
const { removeRoofMaterial, removeAllRoofMaterial, moveRoofMaterial, removeOuterLines } = useRoofFn()
|
||||
const { removeRoofMaterial, removeAllRoofMaterial, moveRoofMaterial, removeOuterLines, setSurfaceShapePattern } = useRoofFn()
|
||||
const selectedMenu = useRecoilValue(selectedMenuState)
|
||||
const { isAllComplete, clear: resetModule } = useTrestle()
|
||||
const { isExistCircuit } = useCircuitTrestle()
|
||||
const { changeCorridorDimensionText } = useText()
|
||||
const { setPolygonLinesActualSize, drawDirectionArrow } = usePolygon()
|
||||
const currentMenuSetting = () => {
|
||||
switch (selectedMenu) {
|
||||
case 'outline':
|
||||
@ -170,6 +170,11 @@ export function useContextMenu() {
|
||||
name: getMessage('contextmenu.size.edit'),
|
||||
component: <SizeSetting id={popupId} target={currentObject} />,
|
||||
},
|
||||
{
|
||||
id: 'rotate',
|
||||
name: `${getMessage('contextmenu.rotate')}`,
|
||||
fn: () => rotateSurfaceShapeBatch(),
|
||||
},
|
||||
{
|
||||
id: 'roofMaterialRemove',
|
||||
shortcut: ['d', 'D'],
|
||||
|
||||
@ -446,6 +446,7 @@
|
||||
"contextmenu.remove": "削除",
|
||||
"contextmenu.remove.all": "完全削除",
|
||||
"contextmenu.move": "移動",
|
||||
"contextmenu.rotate": "回転",
|
||||
"contextmenu.copy": "コピー",
|
||||
"contextmenu.edit": "編集",
|
||||
"contextmenu.module.vertical.align": "モジュールの垂直方向の中央揃え",
|
||||
|
||||
@ -446,6 +446,7 @@
|
||||
"contextmenu.remove": "삭제",
|
||||
"contextmenu.remove.all": "전체 삭제",
|
||||
"contextmenu.move": "이동",
|
||||
"contextmenu.rotate": "회전",
|
||||
"contextmenu.copy": "복사",
|
||||
"contextmenu.edit": "편집",
|
||||
"contextmenu.module.vertical.align": "모듈 세로 가운데 정렬",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user