dev #364

Merged
ysCha merged 18 commits from dev into dev-deploy 2025-09-29 11:40:03 +09:00
21 changed files with 3203 additions and 540 deletions
Showing only changes of commit 340c7669af - Show all commits

View File

@ -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]

View File

@ -2,433 +2,439 @@ import React, { useState, useRef, useEffect, forwardRef } from 'react'
import { createCalculator } from '@/util/calc-utils'
import '@/styles/calc.scss'
export const CalculatorInput = forwardRef(({ value, onChange, label, options = {}, id, className = 'calculator-input', readOnly = false, placeholder}, ref) => {
const [showKeypad, setShowKeypad] = useState(false)
const [displayValue, setDisplayValue] = useState(value || '0')
const [hasOperation, setHasOperation] = useState(false)
const calculatorRef = useRef(createCalculator(options))
const containerRef = useRef(null)
const inputRef = useRef(null)
export const CalculatorInput = forwardRef(
({ value, onChange, label, options = {}, id, className = 'calculator-input', readOnly = false, placeholder }, ref) => {
const [showKeypad, setShowKeypad] = useState(false)
const [displayValue, setDisplayValue] = useState(value || '0')
const [hasOperation, setHasOperation] = useState(false)
const calculatorRef = useRef(createCalculator(options))
const containerRef = useRef(null)
const inputRef = useRef(null)
// ref ref
useEffect(() => {
if (ref) {
if (typeof ref === 'function') {
ref(inputRef.current)
} else {
ref.current = inputRef.current
}
}
}, [ref])
// Sync displayValue with value prop
useEffect(() => {
setDisplayValue(value || '0')
}, [value])
//
useEffect(() => {
const handleClickOutside = (event) => {
if (containerRef.current && !containerRef.current.contains(event.target)) {
setShowKeypad(false)
if (hasOperation) {
// If there's an operation in progress, compute the result when losing focus
handleCompute()
// ref ref
useEffect(() => {
if (ref) {
if (typeof ref === 'function') {
ref(inputRef.current)
} else {
ref.current = inputRef.current
}
}
}
}, [ref])
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [value, onChange, hasOperation])
// Sync displayValue with value prop
useEffect(() => {
setDisplayValue(value || '0')
}, [value])
//
const handleNumber = (num) => {
const calculator = calculatorRef.current
let newDisplayValue = ''
if (hasOperation) {
//
if (calculator.currentOperand === '0' || calculator.shouldResetDisplay) {
calculator.currentOperand = num.toString()
calculator.shouldResetDisplay = false
} else {
calculator.currentOperand = (calculator.currentOperand || '') + num
//
useEffect(() => {
const handleClickOutside = (event) => {
if (containerRef.current && !containerRef.current.contains(event.target)) {
setShowKeypad(false)
if (hasOperation) {
// If there's an operation in progress, compute the result when losing focus
handleCompute()
}
}
}
newDisplayValue = calculator.previousOperand + calculator.operation + calculator.currentOperand
setDisplayValue(newDisplayValue)
} else {
//
if (displayValue === '0' || calculator.shouldResetDisplay) {
calculator.currentOperand = num.toString()
calculator.shouldResetDisplay = false
newDisplayValue = calculator.currentOperand
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [value, onChange, hasOperation])
//
const handleNumber = (num) => {
const calculator = calculatorRef.current
let newDisplayValue = ''
if (hasOperation) {
//
if (calculator.currentOperand === '0' || calculator.shouldResetDisplay) {
calculator.currentOperand = num.toString()
calculator.shouldResetDisplay = false
} else {
calculator.currentOperand = (calculator.currentOperand || '') + num
}
newDisplayValue = calculator.previousOperand + calculator.operation + calculator.currentOperand
setDisplayValue(newDisplayValue)
if (!hasOperation) {
onChange(calculator.currentOperand)
} else {
//
if (displayValue === '0' || calculator.shouldResetDisplay) {
calculator.currentOperand = num.toString()
calculator.shouldResetDisplay = false
newDisplayValue = calculator.currentOperand
setDisplayValue(newDisplayValue)
if (!hasOperation) {
onChange(calculator.currentOperand)
}
} else {
calculator.currentOperand = (calculator.currentOperand || '') + num
newDisplayValue = calculator.currentOperand
setDisplayValue(newDisplayValue)
if (!hasOperation) {
onChange(newDisplayValue)
}
}
}
//
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const len = newDisplayValue.length
inputRef.current.setSelectionRange(len, len)
//
inputRef.current.scrollLeft = inputRef.current.scrollWidth
}
})
}
//
const handleOperation = (operation) => {
const calculator = calculatorRef.current
let newDisplayValue = ''
// ( )
if (!calculator.currentOperand && calculator.previousOperand) {
calculator.operation = operation
newDisplayValue = calculator.previousOperand + operation
setDisplayValue(newDisplayValue)
setHasOperation(true)
} else if (hasOperation) {
// ,
const result = calculator.compute()
if (result !== undefined) {
calculator.previousOperand = result.toString()
calculator.operation = operation
calculator.currentOperand = ''
newDisplayValue = calculator.previousOperand + operation
setDisplayValue(newDisplayValue)
}
} else {
calculator.currentOperand = (calculator.currentOperand || '') + num
newDisplayValue = calculator.currentOperand
setDisplayValue(newDisplayValue)
if (!hasOperation) {
onChange(newDisplayValue)
}
}
}
// ( )
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const len = newDisplayValue.length
inputRef.current.setSelectionRange(len, len)
}
})
}
//
const handleOperation = (operation) => {
const calculator = calculatorRef.current
let newDisplayValue = ''
// ( )
if (!calculator.currentOperand && calculator.previousOperand) {
calculator.operation = operation
newDisplayValue = calculator.previousOperand + operation
setDisplayValue(newDisplayValue)
setHasOperation(true)
} else if (hasOperation) {
// ,
const result = calculator.compute()
if (result !== undefined) {
calculator.previousOperand = result.toString()
//
calculator.previousOperand = calculator.currentOperand || '0'
calculator.operation = operation
calculator.currentOperand = ''
setHasOperation(true)
newDisplayValue = calculator.previousOperand + operation
setDisplayValue(newDisplayValue)
}
} else {
//
calculator.previousOperand = calculator.currentOperand || '0'
calculator.operation = operation
calculator.currentOperand = ''
setHasOperation(true)
newDisplayValue = calculator.previousOperand + operation
setDisplayValue(newDisplayValue)
//
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const len = newDisplayValue.length
inputRef.current.setSelectionRange(len, len)
//
inputRef.current.scrollLeft = inputRef.current.scrollWidth
}
})
}
// ( )
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const len = newDisplayValue.length
inputRef.current.setSelectionRange(len, len)
}
})
}
// AC
const handleClear = () => {
const calculator = calculatorRef.current
const newValue = calculator.clear()
const displayValue = newValue || '0'
setDisplayValue(displayValue)
setHasOperation(false)
onChange(displayValue)
//
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const len = displayValue.length
inputRef.current.setSelectionRange(len, len)
// Ensure focus is maintained
inputRef.current.focus()
}
})
}
//
const handleCompute = (fromEnterKey = false) => {
const calculator = calculatorRef.current
if (!hasOperation || !calculator.currentOperand) return
const result = calculator.compute()
if (result !== undefined) {
const resultStr = result.toString()
setDisplayValue(resultStr)
setHasOperation(false)
// Only call onChange with the final result
onChange(resultStr)
//
if (!fromEnterKey) {
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const len = resultStr.length
inputRef.current.setSelectionRange(len, len)
}
})
}
}
}
// DEL
const handleDelete = () => {
const calculator = calculatorRef.current
const newValue = calculator.deleteNumber()
const displayValue = newValue || '0'
setDisplayValue(displayValue)
setHasOperation(!!calculator.operation)
onChange(displayValue)
//
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const len = displayValue.length
inputRef.current.setSelectionRange(len, len)
// Ensure focus is maintained
inputRef.current.focus()
}
})
}
// input onChange -
const handleInputChange = (e) => {
if (readOnly) return
const inputValue = e.target.value
// (, , )
const filteredValue = inputValue.replace(/[^0-9+\-×÷.]/g, '')
//
const lastChar = filteredValue[filteredValue.length - 1]
const prevChar = filteredValue[filteredValue.length - 2]
if (['+', '×', '÷'].includes(lastChar) && ['+', '-', '×', '÷', '.'].includes(prevChar)) {
//
return
}
//
const parts = filteredValue.split(/[+\-×÷]/)
if (parts[parts.length - 1].split('.').length > 2) {
// 2
return
}
setDisplayValue(filteredValue)
//
if (filteredValue !== displayValue) {
// AC
const handleClear = () => {
const calculator = calculatorRef.current
const hasOperation = /[+\-×÷]/.test(filteredValue)
if (hasOperation) {
const [operand1, operator, operand2] = filteredValue.split(/([+\-×÷])/)
calculator.previousOperand = operand1 || ''
calculator.operation = operator || ''
calculator.currentOperand = operand2 || ''
setHasOperation(true)
} else {
calculator.currentOperand = filteredValue
const newValue = calculator.clear()
const displayValue = newValue || '0'
setDisplayValue(displayValue)
setHasOperation(false)
onChange(displayValue)
//
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const len = displayValue.length
inputRef.current.setSelectionRange(len, len)
//
inputRef.current.scrollLeft = inputRef.current.scrollWidth
}
})
}
//
const handleCompute = (fromEnterKey = false) => {
const calculator = calculatorRef.current
if (!hasOperation || !calculator.currentOperand) return
const result = calculator.compute()
if (result !== undefined) {
const resultStr = result.toString()
setDisplayValue(resultStr)
setHasOperation(false)
}
onChange(filteredValue)
}
}
// Only call onChange with the final result
onChange(resultStr)
//
if (!fromEnterKey) {
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const len = resultStr.length
inputRef.current.setSelectionRange(len, len)
}
})
}
}
}
// DEL
const handleDelete = () => {
const calculator = calculatorRef.current
const newValue = calculator.deleteNumber()
const displayValue = newValue || '0'
setDisplayValue(displayValue)
setHasOperation(!!calculator.operation)
onChange(displayValue)
//
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
const len = displayValue.length
inputRef.current.setSelectionRange(len, len)
//
inputRef.current.scrollLeft = inputRef.current.scrollWidth
}
})
}
// input onChange -
const handleInputChange = (e) => {
if (readOnly) return
const inputValue = e.target.value
// (, , )
const filteredValue = inputValue.replace(/[^0-9+\-×÷.]/g, '')
//
const lastChar = filteredValue[filteredValue.length - 1]
const prevChar = filteredValue[filteredValue.length - 2]
if (['+', '×', '÷'].includes(lastChar) && ['+', '-', '×', '÷', '.'].includes(prevChar)) {
//
return
}
//
const parts = filteredValue.split(/[+\-×÷]/)
if (parts[parts.length - 1].split('.').length > 2) {
// 2
return
}
setDisplayValue(filteredValue)
//
if (filteredValue !== displayValue) {
const calculator = calculatorRef.current
const hasOperation = /[+\-×÷]/.test(filteredValue)
if (hasOperation) {
const [operand1, operator, operand2] = filteredValue.split(/([+\-×÷])/)
calculator.previousOperand = operand1 || ''
calculator.operation = operator || ''
calculator.currentOperand = operand2 || ''
setHasOperation(true)
} else {
calculator.currentOperand = filteredValue
setHasOperation(false)
}
onChange(filteredValue)
}
}
//
const toggleKeypad = (e) => {
if (e) {
e.preventDefault()
e.stopPropagation()
}
const newShowKeypad = !showKeypad
setShowKeypad(newShowKeypad)
// Show keypad
if (newShowKeypad) {
setTimeout(() => {
inputRef.current?.focus()
}, 0)
}
}
//
const handleKeyDown = (e) => {
if (readOnly) return
// Tab
if (e.key === 'Tab') {
setShowKeypad(false)
return
}
//
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown') {
setShowKeypad(true)
return
}
// ( )
if (/^[0-9+\-×÷.=]$/.test(e.key) || e.key === 'Backspace' || e.key === 'Delete' || e.key === '*' || e.key === '/') {
setShowKeypad(true)
}
//
if (!showKeypad && e.key === 'Enter') {
return
}
//
const toggleKeypad = (e) => {
if (e) {
e.preventDefault()
e.stopPropagation()
}
const newShowKeypad = !showKeypad
setShowKeypad(newShowKeypad)
// Show keypad
if (newShowKeypad) {
setTimeout(() => {
inputRef.current?.focus()
}, 0)
}
}
const calculator = calculatorRef.current
const { allowDecimal } = options
//
const handleKeyDown = (e) => {
if (readOnly) return
if (e.key === '.') {
// allowDecimal false
if (!allowDecimal) return
// Tab
if (e.key === 'Tab') {
setShowKeypad(false)
return
}
//
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown') {
setShowKeypad(true)
return
}
//
const currentValue = displayValue.toString()
const parts = currentValue.split(/[+\-×÷]/)
const lastPart = parts[parts.length - 1]
// ( )
if (/^[0-9+\-×÷.=]$/.test(e.key) || e.key === 'Backspace' || e.key === 'Delete' || e.key === '*' || e.key === '/') {
setShowKeypad(true)
}
//
if (!showKeypad && e.key === 'Enter') {
return
}
e.preventDefault()
const calculator = calculatorRef.current
const { allowDecimal } = options
if (e.key === '.') {
// allowDecimal false
if (!allowDecimal) return
//
const currentValue = displayValue.toString()
const parts = currentValue.split(/[+\-×÷]/)
const lastPart = parts[parts.length - 1]
//
if (!lastPart.includes('.')) {
//
if (!lastPart.includes('.')) {
handleNumber(e.key)
}
} else if (/^[0-9]$/.test(e.key)) {
handleNumber(e.key)
}
} else if (/^[0-9]$/.test(e.key)) {
handleNumber(e.key)
} else {
switch (e.key) {
case '+':
case '-':
case '*':
case '/':
const opMap = { '*': '×', '/': '÷' }
handleOperation(opMap[e.key] || e.key)
break
case 'Enter':
case '=':
if (showKeypad) {
// :
handleCompute(true) //
} else {
switch (e.key) {
case '+':
case '-':
case '*':
case '/':
const opMap = { '*': '×', '/': '÷' }
handleOperation(opMap[e.key] || e.key)
break
case 'Enter':
case '=':
if (showKeypad) {
// :
handleCompute(true) //
setShowKeypad(false)
}
// : (preventDefault )
break
case 'Backspace':
case 'Delete':
handleDelete()
break
case 'Escape':
handleClear()
setShowKeypad(false)
}
// : (preventDefault )
break
case 'Backspace':
case 'Delete':
handleDelete()
break
break
case 'Escape':
handleClear()
setShowKeypad(false)
break
default:
break
default:
break
}
}
}
}
return (
<div ref={containerRef} className="calculator-input-wrapper">
{label && (
<label htmlFor={id} className="calculator-label">
{label}
</label>
)}
<input
ref={inputRef}
type="text"
id={id}
value={displayValue}
readOnly={readOnly}
className={className}
onClick={() => !readOnly && setShowKeypad(true)}
onFocus={() => !readOnly && setShowKeypad(true)}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
tabIndex={readOnly ? -1 : 0}
placeholder={placeholder}
autoComplete={'off'}
/>
return (
<div ref={containerRef} className="calculator-input-wrapper">
{label && (
<label htmlFor={id} className="calculator-label">
{label}
</label>
)}
<input
ref={inputRef}
type="text"
id={id}
value={displayValue}
readOnly={readOnly}
className={className}
onClick={() => !readOnly && setShowKeypad(true)}
onFocus={() => !readOnly && setShowKeypad(true)}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
tabIndex={readOnly ? -1 : 0}
placeholder={placeholder}
autoComplete={'off'}
/>
{showKeypad && !readOnly && (
<div className="keypad-container">
<div className="keypad-grid">
<button
onClick={() => {
// const newValue = calculatorRef.current.clear()
// setDisplayValue(newValue || '0')
// onChange(newValue || '0')
handleClear()
setHasOperation(false)
}}
className="btn-clear"
>
AC
</button>
<button
onClick={() => {
// const newValue = calculatorRef.current.deleteNumber()
// setDisplayValue(newValue || '0')
// onChange(newValue || '0')
//setHasOperation(!!calculatorRef.current.operation)
handleDelete()
}}
className="btn-delete"
>
DEL
</button>
<button onClick={() => handleOperation('÷')} className="btn-operator">
÷
</button>
<button onClick={() => handleOperation('×')} className="btn-operator">
×
</button>
<button onClick={() => handleOperation('-')} className="btn-operator">
-
</button>
<button onClick={() => handleOperation('+')} className="btn-operator">
+
</button>
{/* 숫자 버튼 */}
{[1,2,3,4,5,6,7,8,9].map((num) => (
<button key={num} onClick={() => handleNumber(num)} className="btn-number">
{num}
{showKeypad && !readOnly && (
<div className="keypad-container">
<div className="keypad-grid">
<button
onClick={() => {
// const newValue = calculatorRef.current.clear()
// setDisplayValue(newValue || '0')
// onChange(newValue || '0')
handleClear()
setHasOperation(false)
}}
className="btn-clear"
>
AC
</button>
))}
{/* 0 버튼 */}
<button onClick={() => handleNumber('0')} className="btn-number btn-zero">
0
</button>
<button
onClick={() => {
const newValue = calculatorRef.current.appendNumber('.')
onChange(newValue)
}}
className="btn-number"
>
.
</button>
{/* = 버튼 */}
<button onClick={() => handleCompute(false)} className="btn-equals">
=
</button>
<button
onClick={() => {
// const newValue = calculatorRef.current.deleteNumber()
// setDisplayValue(newValue || '0')
// onChange(newValue || '0')
//setHasOperation(!!calculatorRef.current.operation)
handleDelete()
}}
className="btn-delete"
>
DEL
</button>
<button onClick={() => handleOperation('÷')} className="btn-operator">
÷
</button>
<button onClick={() => handleOperation('×')} className="btn-operator">
×
</button>
<button onClick={() => handleOperation('-')} className="btn-operator">
-
</button>
<button onClick={() => handleOperation('+')} className="btn-operator">
+
</button>
{/* 숫자 버튼 */}
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => (
<button key={num} onClick={() => handleNumber(num)} className="btn-number">
{num}
</button>
))}
{/* 0 버튼 */}
<button onClick={() => handleNumber('0')} className="btn-number btn-zero">
0
</button>
<button
onClick={() => {
const newValue = calculatorRef.current.appendNumber('.')
onChange(newValue)
}}
className="btn-number"
>
.
</button>
{/* = 버튼 */}
<button onClick={() => handleCompute(false)} className="btn-equals">
=
</button>
</div>
</div>
</div>
)}
</div>
)
})
)}
</div>
)
},
)
CalculatorInput.displayName = 'CalculatorInput'

View File

@ -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', () => {
@ -733,7 +739,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
},
inPolygonImproved(point) {
const vertices = this.points
const vertices = this.getCurrentPoints()
let inside = false
const testX = Number(point.x.toFixed(this.toFixed))
const testY = Number(point.y.toFixed(this.toFixed))
@ -745,7 +751,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
const yj = Number(vertices[j].y.toFixed(this.toFixed))
// 점이 정점 위에 있는지 확인
if (Math.abs(xi - testX) < 0.01 && Math.abs(yi - testY) < 0.01) {
if (Math.abs(xi - testX) <= 0.01 && Math.abs(yi - testY) <= 0.01) {
return true
}

View File

@ -196,6 +196,14 @@ export default function CanvasMenu(props) {
text: getMessage('module.delete.confirm'),
type: 'confirm',
confirmFn: () => {
const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
roofs.forEach((roof) => {
roof.set({
stroke: 'black',
strokeWidth: 3,
})
})
//
setAllModuleSurfaceIsComplete(false)

View File

@ -7,16 +7,21 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
import QSelectBox from '@/components/common/select/QSelectBox'
import { roofsState } from '@/store/roofAtom'
import { useModuleBasicSetting } from '@/hooks/module/useModuleBasicSetting'
import { useCommonCode } from '@/hooks/common/useCommonCode'
import Swal from 'sweetalert2'
import { normalizeDecimal} from '@/util/input-utils'
export const Orientation = forwardRef((props, ref) => {
const { getMessage } = useMessage()
const { findCommonCode } = useCommonCode()
const [hasAnglePassivity, setHasAnglePassivity] = useState(false)
const basicSetting = useRecoilValue(basicSettingState)
const [addedRoofs, setAddedRoofs] = useRecoilState(addedRoofsState) //
const [roofsStore, setRoofsStore] = useRecoilState(roofsState)
const [roofTab, setRoofTab] = useState(0) //
const [selectedModuleSeries, setSelectedModuleSeries] = useState(null)
const [moduleSeriesList, setModuleSeriesList] = useState([])
const [filteredModuleList, setFilteredModuleList] = useState([])
const {
roofs,
setRoofs,
@ -66,8 +71,13 @@ export const Orientation = forwardRef((props, ref) => {
],
}
const allOption = {
moduleSerCd: 'ALL',
moduleSerNm: getMessage("board.sub.total") || 'ALL'
};
useEffect(() => {
if (basicSetting.roofSizeSet == '3') {
if (basicSetting.roofSizeSet === '3') {
restoreModuleInstArea()
}
}, [])
@ -80,9 +90,22 @@ export const Orientation = forwardRef((props, ref) => {
useEffect(() => {
if (selectedModules) {
setSelectedModules(moduleList.find((module) => module.itemId === selectedModules.itemId))
const foundModule = moduleList.find((module) => module.itemId === selectedModules.itemId)
if (foundModule) {
setSelectedModules(foundModule)
// ( )
if (moduleSeriesList.length > 0 && foundModule.moduleSerCd) {
const currentSeries = moduleSeriesList.find(series => series.moduleSerCd === foundModule.moduleSerCd)
if (currentSeries && (!selectedModuleSeries || selectedModuleSeries.moduleSerCd !== currentSeries.moduleSerCd)) {
setSelectedModuleSeries(currentSeries)
}
}else{
setSelectedModuleSeries(allOption)
}
}
}
}, [selectedModules])
}, [selectedModules, moduleList, moduleSeriesList])
useEffect(() => {
if (selectedSurfaceType) {
@ -164,7 +187,7 @@ export const Orientation = forwardRef((props, ref) => {
title: getMessage('module.not.found'),
icon: 'warning',
})
return
}
}
}
@ -206,6 +229,41 @@ export const Orientation = forwardRef((props, ref) => {
return true
}
const handleChangeModuleSeries = (e) => {
resetRoofs()
setSelectedModuleSeries(e)
//
if (e && moduleList.length > 0) {
let filtered
if (e.moduleSerCd === 'ALL') {
// ""
filtered = moduleList
} else {
//
//filtered = moduleList.filter(module => module.moduleSerCd === e.moduleSerCd)
filtered = moduleList.filter(module => module && module.moduleSerCd && module.moduleSerCd === e.moduleSerCd)
}
setFilteredModuleList(filtered)
//
if (filtered.length > 0) {
const firstModule = filtered[0]
setSelectedModules(firstModule)
// handleChangeModule
if (handleChangeModule) {
handleChangeModule(firstModule)
}
}
} else {
//
setFilteredModuleList([])
setSelectedModules(null)
}
}
const handleChangeModule = (e) => {
resetRoofs()
setSelectedModules(e)
@ -264,12 +322,71 @@ export const Orientation = forwardRef((props, ref) => {
setRoofsStore(newRoofs)
}
// commonCode
useEffect(() => {
// selectedModules handleChangeModule
if (moduleList.length > 0 && (!selectedModules || !selectedModules.itemId)) {
handleChangeModule(moduleList[0]);
if (moduleList.length > 0 && moduleSeriesList.length === 0) {
const moduleSeriesCodes = findCommonCode(207100) || []
// moduleList moduleSerCd
const uniqueSeriesCd = [...new Set(moduleList.map(module => module.moduleSerCd).filter(Boolean))]
if (uniqueSeriesCd.length > 0) {
// moduleSerCd commonCode moduleSeriesList
const mappedSeries = uniqueSeriesCd.map(serCd => {
const matchedCode = moduleSeriesCodes.find(code => code.clCode === serCd)
return {
moduleSerCd: serCd,
moduleSerNm: matchedCode ? matchedCode.clCodeNm : serCd
}
})
// ""
const seriesList = [allOption, ...mappedSeries]
setModuleSeriesList(seriesList)
//
if (selectedModules && selectedModules.moduleSerCd) {
const currentSeries = seriesList.find(series => series.moduleSerCd === selectedModules.moduleSerCd)
if (currentSeries) {
setSelectedModuleSeries(currentSeries)
} else {
setSelectedModuleSeries(allOption)
// "ALL"
setTimeout(() => handleChangeModuleSeries(allOption), 0)
}
} else {
// ""
setSelectedModuleSeries(allOption)
// "ALL"
setTimeout(() => handleChangeModuleSeries(allOption), 0)
}
}
}
}, [moduleList]); //
}, [moduleList, selectedModules])
//
useEffect(() => {
if (moduleList.length > 0 && filteredModuleList.length === 0 && selectedModuleSeries) {
let filtered
if (selectedModuleSeries.moduleSerCd === 'ALL') {
// ""
filtered = moduleList
} else {
//
filtered = moduleList.filter(module => module.moduleSerCd === selectedModuleSeries.moduleSerCd)
}
setFilteredModuleList(filtered)
if (filtered.length > 0 && !selectedModules) {
setSelectedModules(filtered[0])
}
} else if (moduleList.length === 0 && filteredModuleList.length === 0 && selectedModuleSeries) {
//
setFilteredModuleList([])
}
}, [moduleList, selectedModuleSeries]);
return (
<>
<div className="properties-setting-wrap">
@ -336,16 +453,32 @@ export const Orientation = forwardRef((props, ref) => {
<div className="compas-table-wrap">
<div className="compas-table-box mb10">
<div className="outline-form mb10">
<span>{getMessage('modal.module.basic.setting.module.setting')}</span>
<span>{getMessage('modal.module.basic.setting.module.series.setting')}</span>
<div className="grid-select">
{moduleList && (
<div className="grid-select">
<QSelectBox
options={moduleList}
options={moduleSeriesList.length > 0 ? moduleSeriesList : [allOption]}
value={selectedModuleSeries}
targetKey={'moduleSerCd'}
sourceKey={'moduleSerCd'}
showKey={'moduleSerNm'}
onChange={(e) => handleChangeModuleSeries(e)}
/>
</div>
</div>
</div>
<div className="outline-form mb10">
<span>{getMessage('modal.module.basic.setting.module.setting2')}</span>
<div className="grid-select">
{filteredModuleList && (
<QSelectBox
options={filteredModuleList}
value={selectedModules}
targetKey={'itemId'}
sourceKey={'itemId'}
showKey={'itemNm'}
onChange={(e) => handleChangeModule(e)}
showFirstOptionWhenEmpty = {true}
/>
)}
</div>
@ -396,7 +529,7 @@ export const Orientation = forwardRef((props, ref) => {
</tbody>
</table>
</div>
{basicSetting && basicSetting.roofSizeSet == '3' && (
{basicSetting && basicSetting.roofSizeSet === '3' && (
<div className="outline-form mt15">
<span>{getMessage('modal.module.basic.setting.module.placement.area')}</span>
<div className="input-grid mr10" style={{ width: '60px' }}>
@ -407,7 +540,7 @@ export const Orientation = forwardRef((props, ref) => {
)}
</div>
{basicSetting && basicSetting.roofSizeSet != '3' && (
{basicSetting && basicSetting.roofSizeSet !== '3' && (
<div className="compas-table-box">
<div className="compas-grid-table">
<div className="outline-form">

View File

@ -18,6 +18,12 @@ const Trestle = forwardRef((props, ref) => {
const currentAngleType = useRecoilValue(currentAngleTypeSelector)
const pitchText = useRecoilValue(pitchTextSelector)
const [selectedRoof, setSelectedRoof] = useState(null)
const [isAutoSelecting, setIsAutoSelecting] = useState(false) //
const [autoSelectTimeout, setAutoSelectTimeout] = useState(null) //
const autoSelectTimeoutRef = useRef(null)
// ()
const AUTO_SELECT_TIMEOUT = 500 // API
const {
trestleState,
trestleDetail,
@ -60,23 +66,27 @@ const Trestle = forwardRef((props, ref) => {
const [flag, setFlag] = useState(false)
const tempModuleSelectionData = useRef(null)
const [autoSelectStep, setAutoSelectStep] = useState(null) // 'raftBase', 'trestle', 'constMthd', 'roofBase', 'construction'
const prevHajebichiRef = useRef();
useEffect(() => {
if (roofs && !selectedRoof) {
if (roofs && roofs.length > 0 && !selectedRoof) {
console.log("roofs:::::", roofs.length)
setLengthBase(roofs[0].length);
setSelectedRoof(roofs[0])
}
if (selectedRoof && selectedRoof.lenAuth === "C") {
onChangeLength(selectedRoof.length);
}
if (selectedRoof && ["C", "R"].includes(selectedRoof.raftAuth)) {
}else if (selectedRoof && ["C", "R"].includes(selectedRoof.raftAuth) && roofs && roofs.length > 0) {
onChangeRaftBase(roofs[0]);
}else if (selectedRoof && ["C", "R"].includes(selectedRoof.roofPchAuth) && roofs && roofs.length > 0 &&
roofs[0].hajebichi !== prevHajebichiRef.current ) {
prevHajebichiRef.current = roofs[0].hajebichi;
onChangeHajebichi(roofs[0].hajebichi);
}
//
restoreModuleInstArea()
}, [roofs])
}, [roofs, selectedRoof]) // selectedRoof
useEffect(() => {
if (flag && moduleSelectionData) {
@ -161,7 +171,7 @@ const Trestle = forwardRef((props, ref) => {
useEffect(() => {
if (constructionList.length > 0) {
const existingConstruction = constructionList.find((construction) => construction.constTp === trestleState?.construction?.constTp)
const existingConstruction = constructionList.find((construction) => construction.constTp === trestleState.constTp)
if (existingConstruction) {
setSelectedConstruction(existingConstruction)
} else if (autoSelectStep === 'construction') {
@ -252,7 +262,7 @@ const Trestle = forwardRef((props, ref) => {
// () -
setTimeout(() => {
setAutoSelectStep('trestle')
}, 500) // API
}, AUTO_SELECT_TIMEOUT) // API
}
const onChangeHajebichi = (e) => {
@ -274,7 +284,7 @@ const Trestle = forwardRef((props, ref) => {
roof: {
moduleTpCd: selectedModules.itemTp ?? '',
roofMatlCd: selectedRoof?.roofMatlCd ?? '',
raft: selectedRaftBase?.clCode,
raft: selectedRaftBase?.clCode ?? selectedRoof?.roofBaseCd,
hajebichi: e,
},
})
@ -282,7 +292,7 @@ const Trestle = forwardRef((props, ref) => {
// () -
setTimeout(() => {
setAutoSelectStep('trestle')
}, 500)
}, AUTO_SELECT_TIMEOUT)
}
const onChangeTrestleMaker = (e) => {
@ -297,7 +307,8 @@ const Trestle = forwardRef((props, ref) => {
roof: {
moduleTpCd: selectedModules.itemTp ?? '',
roofMatlCd: selectedRoof?.roofMatlCd ?? '',
raft: selectedRaftBase?.clCode,
raft: selectedRaftBase?.clCode ?? selectedRoof?.roofBaseCd,
//hajebichi: selectedRaftBase?.hajebichi ?? selectedRoof?.hajebichi,
trestleMkrCd: e.trestleMkrCd,
},
})
@ -305,7 +316,7 @@ const Trestle = forwardRef((props, ref) => {
// API ()
setTimeout(() => {
setAutoSelectStep('constMthd')
}, 300)
}, AUTO_SELECT_TIMEOUT)
}
const onChangeConstMthd = (e) => {
@ -319,16 +330,28 @@ const Trestle = forwardRef((props, ref) => {
roof: {
moduleTpCd: selectedModules.itemTp ?? '',
roofMatlCd: selectedRoof?.roofMatlCd ?? '',
raft: selectedRaftBase?.clCode,
raft: selectedRaftBase?.clCode ?? selectedRoof?.roofBaseCd,
//hajebichi: selectedRaftBase?.hajebichi ?? selectedRoof?.hajebichi,
trestleMkrCd: selectedTrestle?.trestleMkrCd,
constMthdCd: e.constMthdCd,
},
})
//
if (autoSelectTimeoutRef.current) {
clearTimeout(autoSelectTimeoutRef.current)
}
//
setIsAutoSelecting(true)
// API ()
setTimeout(() => {
const timeoutId = setTimeout(() => {
setAutoSelectStep('roofBase')
}, 300)
setIsAutoSelecting(false)
}, AUTO_SELECT_TIMEOUT)
autoSelectTimeoutRef.current = timeoutId
}
const onChangeRoofBase = (e) => {
@ -340,7 +363,8 @@ const Trestle = forwardRef((props, ref) => {
roof: {
moduleTpCd: selectedModules.itemTp ?? '',
roofMatlCd: selectedRoof?.roofMatlCd ?? '',
raft: selectedRaftBase?.clCode,
raft: selectedRaftBase?.clCode ?? selectedRoof?.roofBaseCd,
//hajebichi: selectedRaftBase?.hajebichi ?? selectedRoof?.hajebichi,
trestleMkrCd: selectedTrestle?.trestleMkrCd,
constMthdCd: selectedConstMthd?.constMthdCd,
roofBaseCd: e.roofBaseCd,
@ -356,7 +380,7 @@ const Trestle = forwardRef((props, ref) => {
// API (construction)
setTimeout(() => {
setAutoSelectStep('construction')
}, 300)
}, AUTO_SELECT_TIMEOUT)
}
const handleConstruction = (index) => {
@ -366,7 +390,8 @@ const Trestle = forwardRef((props, ref) => {
roof: {
moduleTpCd: selectedModules.itemTp ?? '',
roofMatlCd: selectedRoof?.roofMatlCd ?? '',
raft: selectedRaftBase?.clCode,
raft: selectedRaftBase?.clCode ?? selectedRoof?.roofBaseCd,
//hajebichi: selectedRaftBase?.hajebichi ?? selectedRoof?.hajebichi,
trestleMkrCd: selectedTrestle.trestleMkrCd,
constMthdCd: selectedConstMthd.constMthdCd,
roofBaseCd: selectedRoofBase.roofBaseCd,
@ -403,7 +428,7 @@ const Trestle = forwardRef((props, ref) => {
ridgeMargin,
kerabaMargin,
roofIndex: selectedRoof.index,
raft: selectedRaftBase?.clCode,
raft: selectedRaftBase?.clCode ?? selectedRoof?.roofBaseCd,
trestle: {
hajebichi: hajebichi,
length: lengthBase,
@ -440,8 +465,8 @@ const Trestle = forwardRef((props, ref) => {
ridgeMargin,
kerabaMargin,
roofIndex: roof.index,
raft: selectedRaftBase?.clCode,
hajebichi: hajebichi,
raft: selectedRaftBase?.clCode ?? selectedRoof?.raft ?? '',
//hajebichi: selectedRaftBase?.hajebichi ?? selectedRoof?.hajebichi ?? 0,
trestle: {
length: lengthBase,
hajebichi: hajebichi,
@ -451,7 +476,8 @@ const Trestle = forwardRef((props, ref) => {
...selectedRoofBase,
},
construction: {
...constructionList.find((data) => data.constTp === trestleState.constTp),
//...constructionList.find((data) => newAddedRoofs[index].construction.constTp === data.constTp),
...constructionList.find((data) => trestleState.constTp === data.constTp),
cvrYn,
snowGdPossYn,
cvrChecked,
@ -525,7 +551,7 @@ const Trestle = forwardRef((props, ref) => {
}
if (['C', 'R'].includes(roof.roofPchAuth)) {
if (!roof?.roofPchBase) {
if (!roof?.hajebichi) {
Swal.fire({
title: getMessage('modal.module.basic.settting.module.error7', [roof.nameJp]), // .
icon: 'warning',

View File

@ -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>

View File

@ -86,7 +86,7 @@ export default function ContextRoofAllocationSetting(props) {
return (
<div className="grid-option-box" key={index}>
<div className="d-check-radio pop no-text">
<input type="radio" name="radio01" checked={roof.selected && 'checked'} readOnly={true} />
<input type="radio" name="radio01" checked={!!roof.selected} readOnly={true} />
<label
htmlFor="ra01"
onClick={(e) => {
@ -189,7 +189,7 @@ export default function ContextRoofAllocationSetting(props) {
<input
type="text"
className="input-origin block"
value={roof.hajebichi === '' ? '0' : roof.hajebichi}
value={roof.hajebichi ?? ''}
readOnly={roof.roofPchAuth === 'R'}
onChange={(e) => {
e.target.value = normalizeDigits(e.target.value)
@ -211,8 +211,7 @@ export default function ContextRoofAllocationSetting(props) {
e.target.value = normalizeDecimalLimit(e.target.value, 2)
handleChangePitch(e, index)
}}
value={currentAngleType === 'slope' ? (roof.pitch || '0') : (roof.angle || '0')}
defaultValue={currentAngleType === 'slope' ? (roof.pitch || '0') : (roof.angle || '0')}
value={currentAngleType === 'slope' ? (roof.pitch ?? '') : (roof.angle ?? '')}
/>
</div>
<span className="absol">{pitchText}</span>

View File

@ -86,7 +86,7 @@ export default function RoofAllocationSetting(props) {
return (
<div className="grid-option-box" key={index}>
<div className="d-check-radio pop no-text">
<input type="radio" name="radio01" checked={roof.selected} readOnly />
<input type="radio" name="radio01" checked={!!roof.selected} readOnly />
<label
htmlFor="ra01"
onClick={(e) => {
@ -194,7 +194,7 @@ export default function RoofAllocationSetting(props) {
e.target.value = normalizeDigits(e.target.value)
handleChangeInput(e, 'hajebichi', index)
}}
value={parseInt(roof.hajebichi)}
value={roof.hajebichi ?? ''}
readOnly={roof.roofPchAuth === 'R'}
/>
</div>
@ -212,7 +212,7 @@ export default function RoofAllocationSetting(props) {
e.target.value = normalizeDecimalLimit(e.target.value, 2)
handleChangePitch(e, index)
}}
value={currentAngleType === 'slope' ? roof.pitch : roof.angle}
value={currentAngleType === 'slope' ? (roof.pitch ?? '') : (roof.angle ?? '')}
/>
</div>
<span className="absol">{pitchText}</span>

View File

@ -15,6 +15,7 @@ import {
} from '@/store/canvasAtom'
import { calculateVisibleModuleHeight, getDegreeByChon, polygonToTurfPolygon, rectToPolygon, toFixedWithoutRounding } from '@/util/canvas-util'
import '@/util/fabric-extensions' // fabric 객체들에 getCurrentPoints 메서드 추가
import { basicSettingState, roofDisplaySelector } from '@/store/settingAtom'
import offsetPolygon, { calculateAngle, createLinesFromPolygon } from '@/util/qpolygon-utils'
import { QPolygon } from '@/components/fabric/QPolygon'
@ -265,14 +266,14 @@ export function useModuleBasicSetting(tabNum) {
batchObjects.forEach((obj) => {
//도머일때
if (obj.name === BATCH_TYPE.TRIANGLE_DORMER || obj.name === BATCH_TYPE.PENTAGON_DORMER) {
const groupPoints = obj.groupPoints
const groupPoints = obj.getCurrentPoints()
const offsetObjects = offsetPolygon(groupPoints, 10)
const dormerOffset = new QPolygon(offsetObjects, batchObjectOptions)
dormerOffset.setViewLengthText(false)
canvas.add(dormerOffset) //모듈설치면 만들기
} else {
//개구, 그림자일때
const points = obj.points
const points = obj.getCurrentPoints()
const offsetObjects = offsetPolygon(points, 10)
const offset = new QPolygon(offsetObjects, batchObjectOptions)
offset.setViewLengthText(false)
@ -319,7 +320,7 @@ export function useModuleBasicSetting(tabNum) {
const margin = moduleSelectionData.common.margin ? moduleSelectionData.common.margin : 200
//육지붕일때는 그냥 하드코딩
offsetPoints = offsetPolygon(roof.points, -Number(margin) / 10) //육지붕일때
offsetPoints = offsetPolygon(roof.getCurrentPoints(), -Number(margin) / 10) //육지붕일때
} else {
//육지붕이 아닐때
if (allPointsOutside) {

View File

@ -675,6 +675,8 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
}
})
objectGroup.recalculateGroupPoints()
isDown = false
initEvent()
// dbClickEvent()
@ -1426,7 +1428,7 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
//그림자는 아무데나 설치 할 수 있게 해달라고 함
if (obj.name === BATCH_TYPE.OPENING) {
const turfObject = pointsToTurfPolygon(obj.points)
const turfObject = pointsToTurfPolygon(obj.getCurrentPoints())
if (turf.booleanWithin(turfObject, turfSurface)) {
obj.set({
@ -1459,7 +1461,7 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
const calcLeft = obj.left - originLeft
const calcTop = obj.top - originTop
const currentDormerPoints = obj.groupPoints.map((item) => {
const currentDormerPoints = obj.getCurrentPoints().map((item) => {
return {
x: item.x + calcLeft,
y: item.y + calcTop,

View File

@ -174,22 +174,32 @@ export function useRoofAllocationSetting(id) {
})
}
const firstRes = Array.isArray(res) && res.length > 0 ? res[0] : null
setBasicSetting({
...basicSetting,
planNo: res[0].planNo,
roofSizeSet: res[0].roofSizeSet,
roofAngleSet: res[0].roofAngleSet,
planNo: firstRes?.planNo ?? planNo,
roofSizeSet: firstRes?.roofSizeSet ?? 0,
roofAngleSet: firstRes?.roofAngleSet ?? 0,
roofsData: roofsArray,
selectedRoofMaterial: selectRoofs.find((roof) => roof.selected),
})
setBasicInfo({
planNo: '' + res[0].planNo,
roofSizeSet: '' + res[0].roofSizeSet,
roofAngleSet: '' + res[0].roofAngleSet,
planNo: '' + (firstRes?.planNo ?? planNo),
roofSizeSet: '' + (firstRes?.roofSizeSet ?? 0),
roofAngleSet: '' + (firstRes?.roofAngleSet ?? 0),
})
//데이터 동기화
setCurrentRoofList(selectRoofs)
// 데이터 동기화: 렌더링용 필드 기본값 보정
const normalizedRoofs = selectRoofs.map((roof) => ({
...roof,
width: roof.width ?? '',
length: roof.length ?? '',
hajebichi: roof.hajebichi ?? '',
pitch: roof.pitch ?? '',
angle: roof.angle ?? '',
}))
setCurrentRoofList(normalizedRoofs)
})
} catch (error) {
console.error('Data fetching error:', error)

View File

@ -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,95 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
return orderedPoints
}
const rotateSurfaceShapeBatch = () => {
if (currentObject) {
// 관련 객체들 찾기
// arrow는 제거
const arrow = canvas.getObjects().find((obj) => obj.parentId === currentObject.id && obj.name === 'arrow')
if (arrow) {
canvas.remove(arrow)
}
const relatedObjects = canvas.getObjects().filter((obj) => obj.parentId === currentObject.id)
// 그룹화할 객체들 배열 (currentObject + relatedObjects)
const objectsToGroup = [currentObject, ...relatedObjects]
// 기존 객체들을 캔버스에서 제거
objectsToGroup.forEach((obj) => canvas.remove(obj))
// fabric.Group 생성
const group = new fabric.Group(objectsToGroup, {
originX: 'center',
originY: 'center',
})
// 그룹을 캔버스에 추가
canvas.add(group)
// 현재 회전값에 90도 추가
const currentAngle = group.angle || 0
const newAngle = (currentAngle + 90) % 360
// 그룹 전체를 회전
group.rotate(newAngle)
group.setCoords()
// 그룹을 해제하고 개별 객체로 복원
group._restoreObjectsState()
canvas.remove(group)
// 개별 객체들을 다시 캔버스에 추가하고 처리
group.getObjects().forEach((obj) => {
canvas.add(obj)
obj.setCoords()
// currentObject인 경우 추가 처리
if (obj.id === currentObject.id) {
const originWidth = obj.originWidth
const originHeight = obj.originHeight
// QPolygon 내부 구조 재구성 (선이 깨지는 문제 해결)
if (obj.type === 'QPolygon' && obj.lines) {
obj.initLines()
}
obj.set({
originWidth: originHeight,
originHeight: originWidth,
})
} else {
// relatedObject인 경우에도 필요한 처리
if (obj.type === 'QPolygon' && obj.lines) {
obj.initLines()
}
if (obj.type === 'group') {
// 회전 후의 points를 groupPoints로 업데이트
// getCurrentPoints를 직접 호출하지 말고 recalculateGroupPoints만 실행
obj.recalculateGroupPoints()
obj._objects?.forEach((obj) => {
obj.initLines()
obj.fire('modified')
})
}
}
})
currentObject.fire('modified')
// 화살표와 선 다시 그리기
drawDirectionArrow(currentObject)
setTimeout(() => {
setPolygonLinesActualSize(currentObject)
changeSurfaceLineType(currentObject)
}, 500)
// currentObject를 다시 선택 상태로 설정
canvas.setActiveObject(currentObject)
canvas.renderAll()
}
}
return {
applySurfaceShape,
deleteAllSurfacesAndObjects,
@ -1373,5 +1535,6 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
changeSurfaceLineProperty,
changeSurfaceLinePropertyReset,
changeSurfaceLineType,
rotateSurfaceShapeBatch,
}
}

View File

@ -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'],

View File

@ -213,7 +213,7 @@ export function useEvent() {
const modulePoints = []
const modules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE)
modules.forEach((module) => {
module.points.forEach((point) => {
module.getCurrentPoints().forEach((point) => {
modulePoints.push({ x: point.x, y: point.y })
})
})

View File

@ -173,6 +173,13 @@ export function usePlan(params = {}) {
* @param {boolean} saveAlert - 저장 완료 알림 표시 여부
*/
const saveCanvas = async (saveAlert = true) => {
// 저장 전 선택되어 있는 object 제거
const setupSurfaces = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE)
setupSurfaces.forEach((surface) => {
surface.set({ fill: 'rgba(255,255,255,0.1)', strokeDashArray: [10, 4], strokeWidth: 1 })
})
const canvasStatus = currentCanvasData('save')
const result = await putCanvasStatus(canvasStatus, saveAlert)
//캔버스 저장 완료 후

View File

@ -99,6 +99,8 @@
"modal.module.basic.setting.module.construction.method": "工法",
"modal.module.basic.setting.module.under.roof": "屋根下地",
"modal.module.basic.setting.module.setting": "架台設定",
"modal.module.basic.setting.module.series.setting": "モジュールシリーズ",
"modal.module.basic.setting.module.setting2": "モジュール選択",
"modal.module.basic.setting.module.placement.area": "モジュール配置領域",
"modal.module.basic.setting.module.placement.margin": "モジュール間の間隙",
"modal.module.basic.setting.module.placement.area.eaves": "軒側",
@ -444,6 +446,7 @@
"contextmenu.remove": "削除",
"contextmenu.remove.all": "完全削除",
"contextmenu.move": "移動",
"contextmenu.rotate": "回転",
"contextmenu.copy": "コピー",
"contextmenu.edit": "編集",
"contextmenu.module.vertical.align": "モジュールの垂直方向の中央揃え",

View File

@ -98,7 +98,9 @@
"modal.module.basic.setting.module.rafter.margin": "서까래 간격",
"modal.module.basic.setting.module.construction.method": "공법",
"modal.module.basic.setting.module.under.roof": "지붕밑바탕",
"modal.module.basic.setting.module.setting": "모듈 선택",
"modal.module.basic.setting.module.setting": "가대 설정",
"modal.module.basic.setting.module.series.setting": "모듈 시리즈",
"modal.module.basic.setting.module.setting2": "모듈 선택",
"modal.module.basic.setting.module.placement.area": "모듈 배치 영역",
"modal.module.basic.setting.module.placement.margin": "모듈 배치 간격",
"modal.module.basic.setting.module.placement.area.eaves": "처마쪽",
@ -444,6 +446,7 @@
"contextmenu.remove": "삭제",
"contextmenu.remove.all": "전체 삭제",
"contextmenu.move": "이동",
"contextmenu.rotate": "회전",
"contextmenu.copy": "복사",
"contextmenu.edit": "편집",
"contextmenu.module.vertical.align": "모듈 세로 가운데 정렬",

View File

@ -1,17 +1,17 @@
// Variables
$colors: (
'dark-700': #1f2937,
'dark-600': #334155,
'dark-500': #4b5563,
'dark-400': #6b7280,
'dark-300': #374151,
'primary': #10b981,
'primary-dark': #059669,
'danger': #ef4444,
'danger-dark': #dc2626,
'warning': #f59e0b,
'warning-dark': #d97706,
'border': #475569
'dark-700': #1f2937,
'dark-600': #334155,
'dark-500': #4b5563,
'dark-400': #6b7280,
'dark-300': #374151,
'primary': #10b981,
'primary-dark': #059669,
'danger': #ef4444,
'danger-dark': #dc2626,
'warning': #f59e0b,
'warning-dark': #d97706,
'border': #475569,
);
// Mixins
@ -46,10 +46,15 @@ $colors: (
color: white;
font-weight: bold;
border-radius: 0.5rem;
text-align: right;
text-align: left; // 왼쪽 정렬
cursor: pointer;
font-size: 0.625rem;
box-sizing: border-box;
direction: ltr; // 왼쪽에서 오른쪽으로 입력
white-space: nowrap; // 줄바꿈 방지
// input 요소에서는 overflow 대신 다른 방법 사용
text-overflow: clip; // 텍스트 잘림 처리
unicode-bidi: bidi-override; // 텍스트 방향 강제
&:focus {
outline: none;
@ -67,7 +72,9 @@ $colors: (
width: 120px;
background-color: map-get($colors, 'dark-700');
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
padding: 0.5rem;
z-index: 1000;
animation: fadeIn 0.15s ease-out;
@ -153,4 +160,4 @@ $colors: (
padding: 0.5rem;
font-size: 0.875rem;
}
}
}

View File

@ -0,0 +1,232 @@
import { fabric } from 'fabric'
/**
* fabric.Rect에 getCurrentPoints 메서드를 추가
* QPolygon의 getCurrentPoints와 동일한 방식으로 변형된 현재 점들을 반환
*/
fabric.Rect.prototype.getCurrentPoints = function () {
// 사각형의 네 모서리 점들을 계산
const width = this.width
const height = this.height
// 사각형의 로컬 좌표계에서의 네 모서리 점
const points = [
{ x: -width / 2, y: -height / 2 }, // 좌상단
{ x: width / 2, y: -height / 2 }, // 우상단
{ x: width / 2, y: height / 2 }, // 우하단
{ x: -width / 2, y: height / 2 }, // 좌하단
]
// 변형 매트릭스 계산
const matrix = this.calcTransformMatrix()
// 각 점을 변형 매트릭스로 변환
return points.map(function (p) {
const point = new fabric.Point(p.x, p.y)
return fabric.util.transformPoint(point, matrix)
})
}
/**
* fabric.Group에 getCurrentPoints 메서드를 추가 (도머 그룹용)
* 그룹의 groupPoints를 다시 계산하여 반환
*/
fabric.Group.prototype.getCurrentPoints = function () {
// groupPoints를 다시 계산
// 그룹에 groupPoints가 있으면 해당 점들을 사용 (도머의 경우)
if (this.groupPoints && Array.isArray(this.groupPoints)) {
const matrix = this.calcTransformMatrix()
console.log('this.groupPoints', this.groupPoints)
return this.groupPoints.map(function (p) {
const point = new fabric.Point(p.x, p.y)
return fabric.util.transformPoint(point, matrix)
})
}
// groupPoints가 없으면 바운딩 박스를 사용
const bounds = this.getBoundingRect()
const points = [
{ x: bounds.left, y: bounds.top },
{ x: bounds.left + bounds.width, y: bounds.top },
{ x: bounds.left + bounds.width, y: bounds.top + bounds.height },
{ x: bounds.left, y: bounds.top + bounds.height },
]
return points.map(function (p) {
return new fabric.Point(p.x, p.y)
})
}
/**
* fabric.Group에 groupPoints 재계산 메서드 추가
* 그룹 모든 객체의 점들을 기반으로 groupPoints를 새로 계산
* Convex Hull 알고리즘을 사용하여 가장 외곽의 점들만 반환
*/
fabric.Group.prototype.recalculateGroupPoints = function () {
if (!this._objects || this._objects.length === 0) {
return
}
let allPoints = []
// 그룹 내 모든 객체의 점들을 수집
this._objects.forEach(function (obj) {
if (obj.getCurrentPoints && typeof obj.getCurrentPoints === 'function') {
// getCurrentPoints가 있는 객체는 해당 메서드 사용
const objPoints = obj.getCurrentPoints()
allPoints = allPoints.concat(objPoints)
} else if (obj.points && Array.isArray(obj.points)) {
// QPolygon과 같이 points 배열이 있는 경우
const pathOffset = obj.pathOffset || { x: 0, y: 0 }
const matrix = obj.calcTransformMatrix()
const transformedPoints = obj.points
.map(function (p) {
return new fabric.Point(p.x - pathOffset.x, p.y - pathOffset.y)
})
.map(function (p) {
return fabric.util.transformPoint(p, matrix)
})
allPoints = allPoints.concat(transformedPoints)
} else {
// 일반 객체는 바운딩 박스의 네 모서리 점 사용
const bounds = obj.getBoundingRect()
const cornerPoints = [
{ x: bounds.left, y: bounds.top },
{ x: bounds.left + bounds.width, y: bounds.top },
{ x: bounds.left + bounds.width, y: bounds.top + bounds.height },
{ x: bounds.left, y: bounds.top + bounds.height },
]
allPoints = allPoints.concat(
cornerPoints.map(function (p) {
return new fabric.Point(p.x, p.y)
}),
)
}
})
if (allPoints.length > 0) {
// Convex Hull 알고리즘을 사용하여 외곽 점들만 추출
const convexHullPoints = this.getConvexHull(allPoints)
// 그룹의 로컬 좌표계로 변환하기 위해 그룹의 역변환 적용
const groupMatrix = this.calcTransformMatrix()
const invertedMatrix = fabric.util.invertTransform(groupMatrix)
this.groupPoints = convexHullPoints.map(function (p) {
const localPoint = fabric.util.transformPoint(p, invertedMatrix)
return { x: localPoint.x, y: localPoint.y }
})
}
}
/**
* Graham Scan 알고리즘을 사용한 Convex Hull 계산
* 점들의 집합에서 가장 외곽의 점들만 반환
*/
fabric.Group.prototype.getConvexHull = function (points) {
if (points.length < 3) return points
// 중복 점 제거
const uniquePoints = []
const seen = new Set()
points.forEach(function (p) {
const key = `${Math.round(p.x * 10) / 10},${Math.round(p.y * 10) / 10}`
if (!seen.has(key)) {
seen.add(key)
uniquePoints.push({ x: p.x, y: p.y })
}
})
if (uniquePoints.length < 3) return uniquePoints
// 가장 아래쪽 점을 찾기 (y가 가장 작고, 같으면 x가 가장 작은 점)
let pivot = uniquePoints[0]
for (let i = 1; i < uniquePoints.length; i++) {
if (uniquePoints[i].y < pivot.y || (uniquePoints[i].y === pivot.y && uniquePoints[i].x < pivot.x)) {
pivot = uniquePoints[i]
}
}
// 극각에 따라 정렬
const sortedPoints = uniquePoints
.filter(function (p) { return p !== pivot })
.sort(function (a, b) {
const angleA = Math.atan2(a.y - pivot.y, a.x - pivot.x)
const angleB = Math.atan2(b.y - pivot.y, b.x - pivot.x)
if (angleA !== angleB) return angleA - angleB
// 각도가 같으면 거리로 정렬
const distA = Math.pow(a.x - pivot.x, 2) + Math.pow(a.y - pivot.y, 2)
const distB = Math.pow(b.x - pivot.x, 2) + Math.pow(b.y - pivot.y, 2)
return distA - distB
})
// Graham Scan 실행
const hull = [pivot]
for (let i = 0; i < sortedPoints.length; i++) {
const current = sortedPoints[i]
// 반시계방향이 아닌 점들 제거
while (hull.length > 1) {
const p1 = hull[hull.length - 2]
const p2 = hull[hull.length - 1]
const cross = (p2.x - p1.x) * (current.y - p1.y) - (p2.y - p1.y) * (current.x - p1.x)
if (cross > 0) break // 반시계방향이면 유지
hull.pop() // 시계방향이면 제거
}
hull.push(current)
}
return hull
}
/**
* fabric.Triangle에 getCurrentPoints 메서드를 추가
* 삼각형의 꼭짓점을 반환
*/
fabric.Triangle.prototype.getCurrentPoints = function () {
const width = this.width
const height = this.height
// 삼각형의 로컬 좌표계에서의 세 꼭짓점
const points = [
{ x: 0, y: -height / 2 }, // 상단 중앙
{ x: -width / 2, y: height / 2 }, // 좌하단
{ x: width / 2, y: height / 2 }, // 우하단
]
// 변형 매트릭스 계산
const matrix = this.calcTransformMatrix()
// 각 점을 변형 매트릭스로 변환
return points.map(function (p) {
const point = new fabric.Point(p.x, p.y)
return fabric.util.transformPoint(point, matrix)
})
}
/**
* fabric.Polygon에 getCurrentPoints 메서드를 추가 (QPolygon이 아닌 일반 Polygon용)
* QPolygon과 동일한 방식으로 구현
*/
if (!fabric.Polygon.prototype.getCurrentPoints) {
fabric.Polygon.prototype.getCurrentPoints = function () {
const pathOffset = this.get('pathOffset') || { x: 0, y: 0 }
const matrix = this.calcTransformMatrix()
return this.get('points')
.map(function (p) {
return new fabric.Point(p.x - pathOffset.x, p.y - pathOffset.y)
})
.map(function (p) {
return fabric.util.transformPoint(p, matrix)
})
}
}
export default {}

2050
src/util/skeleton-utils.js Normal file

File diff suppressed because it is too large Load Diff